MemberJunction / MemberJunction/MJ

CodeGen AI validators compare UUIDs with `===`, so self-reference CHECKs silently pass when casing differs (prompt: `CodeGen: Check Constraint Parser`)

Open
#3,984 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.