MemberJunction / MemberJunction/MJ
CodeGen AI validators compare UUIDs with `===`, so self-reference CHECKs silently pass when casing differs (prompt: `CodeGen: Check Constraint Parser`)
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
## Summary
The `CodeGen: Check Constraint Parser` prompt generates `Validate*()` methods that compare **UUIDs with `===`**. Since MJ returns `uniqueidentifier` values in either casing depending on how a row was loaded, a self-reference whose two UUIDs differ only in case **silently passes validation** — the save then fails on the raw DB `CHECK`, which names a constraint and no field. That is exactly the raw-error experience these generated validators exist to replace.
This is **not** the deterministic-emitter bug family (#3969 / #3978). CodeGenLib's own comparisons were already fixed to use `UUIDsEqual` — `manage-metadata.ts` uses it throughout. What is affected here is **LLM-authored code**, which that work could not reach.
## Where it comes from
`ManageMetadataBase.generateValidatorFunctionFromCheckConstraint()`
→ `AdvancedGeneration.parseCheckConstraint()`
→ the DB-stored AI prompt **`CodeGen: Check Constraint Parser`**
→ result cached in `EntityField.GeneratedValidationFunctionCode` and re-emitted verbatim into `entity_subclasses.ts`.
The prompt already receives each field's type (`entityFieldListInfo` includes `uniqueidentifier`), so the model has the information it needs and still emits `===`.
## Repro
Give a table a self-referential FK plus the natural CHECK, then run CodeGen with AI enrichment on:
```sql
CONSTRAINT CK_Contract_SupersededNotSelf
CHECK (SupersededByContractID IS NULL OR SupersededByContractID <> ID)
```
Generated output (from `bizapps-contracts`, MJ 6.x, schema `__mj_BizAppsContracts`):
```ts
public ValidateSupersededByContractIDNotSelf(result: ValidationResult) {
if (this.SupersededByContractID != null && this.SupersededByContractID === this.ID) {
result.Errors.push(new ValidationErrorInfo(
"SupersededByContractID",
"A contract cannot be superseded by itself.",
this.SupersededByContractID,
ValidationErrorType.Failure
));
}
}
```
Set the field to the record's own id in the **opposite casing** (lowercase value vs uppercase `ID`) and save → the validator does not fire.
Reproduced on **two** validators on the same entity — `ValidateSupersededByContractIDNotSelf` and `ValidateParentContractIDNotEqualToID` — both with `===`.
## Expected vs actual
- **Expected:** the field-named refusal, e.g. `"A contract cannot be superseded by itself."`
- **Actual:** validation passes; the write is refused by `CK_Contract_SupersededNotSelf` with a constraint name and no field.
Not a data-integrity hole — SQL Server compares `uniqueidentifier` by value, so the DB still rejects the row. The loss is the readable, field-attributed message, and it is **silent**: the validator looks like it checked.
## Why this is worse than a normal codegen bug
1. **Cached** — the result persists in `GeneratedValidationFunctionCode` until the constraint *text* changes, so it does not self-heal on the next CodeGen run.
2. **Non-deterministic** — a different run may emit `UUIDsEqual` or `.toLowerCase()` on one entity and `===` on another. It is not uniformly broken, so spot-checking one entity proves nothing about the rest.
3. **Invisible from the app side** — an app author reading the generated method sees a check that looks correct.
## Suggested fix
Primary: **the prompt**. Tell it that `uniqueidentifier` fields must be compared with `UUIDsEqual` from `@memberjunction/global`, never `===` / `!==`, and have it import accordingly. MJ already documents this in `guides/UUID_COMPARISON_GUIDE.md`, and the #3969 runtime work took the same case-insensitive decision for the same reason.
Belt-and-braces, because the generator is an LLM: a **post-generation check** on the emitted code — if a `Validate*` body compares two fields that are both `uniqueidentifier` using `===`/`!==`, either rewrite it or reject and regenerate. A deterministic guard is what makes a non-deterministic generator safe.
Worth also considering a one-off sweep/invalidation of cached `GeneratedValidationFunctionCode` for uniqueidentifier-comparing validators, since fixing the prompt alone will not refresh existing rows.
## Blast radius
Every MJ app with a self-referential FK and a `<> ID` CHECK. Low severity (the DB backstops it) but it defeats the feature's purpose, and the failure mode is silence.
## Workaround in place (for reference)
`bizapps-contracts` added `ContractEntityServer.refuseSelfReferences()`, which checks both axes with `UUIDsEqual` unconditionally. It is marked in-source as deletable once this is fixed.
Contributor guide
Research direction
Start at ManageMetadataBase.generateValidatorFunctionFromCheckConstraint() and follow AdvancedGeneration.parseCheckConstraint() to the DB-stored “CodeGen: Check Constraint Parser” prompt. Read manage-metadata.ts and guides/UUID_COMPARISON_GUIDE.md, then inspect emitted entity_subclasses.ts and cached EntityField.GeneratedValidationFunctionCode. Done when the opposite-case self-reference repro produces the field-named validation error reliably, including for existing cached output or with a documented invalidation path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- sql, typescript
- Domain
- backend, databases, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100