v0.6.0: Prisma 7 schema compatibility — @map/@@map, @updatedAt, BigInt defaults, enum[] decode, FK resolution - #72
v0.6.0: Prisma 7 schema compatibility — @map/@@map, @updatedAt, BigInt defaults, enum[] decode, FK resolution#72teetangh wants to merge 6 commits into
Conversation
…legates - Parse model-level @@Map("table") into PrismaModel.dbName (explicit @@Map wins over reserved-keyword renames) - Parse field-level @Map("column") into PrismaField.dbName (priority: explicit @Map > reserved-keyword rename > PascalCase normalization) - Stop emitting enum block attributes (@@Map) and value-level attributes as enum values — previously generated invalid Dart identifiers - Delegate generator omits findUnique/findUniqueOrThrow/update/delete for models whose only identifier is a composite @@id (no WhereUniqueInput exists for them) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BigInt has no const constructor, so freezed @default is impossible: - Model class: BigInt-with-literal-default fields become required params; fromJson supplies the schema default via BigInt.from() - CreateInput: such fields stay nullable WITHOUT @default (the database applies the default) - Tests cover required/optional/defaulted BigInt fields and @jsonkey emission + mapped fromJson/toJson keys for @map-ed fields Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- FieldInfo.isUpdatedAt, emitted by the registry generator from @updatedat fields (consumed by the SQL compiler) - One-to-one relations whose FK lives on the TARGET model (e.g. Program.licensedSeatConfig with LicensedSeatConfig.programId owning the @relation) now emit isOwner:false with the target's real FK instead of fabricating a nonexistent <fieldName>Id column on the parent — fixes 'column tN.id does not exist' on nested includes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- WHERE keys, INSERT columns, UPDATE SET keys, and ORDER BY keys resolve Dart field names to database column names via the registry, making typed-delegate CRUD correct on @map-ed columns end-to-end; keys that are not registered field names pass through unchanged (legacy JsonQueryBuilder callers keep working), including in AND/OR/NOT - create/createMany fill @updatedat columns (NOW() on pg/supabase, ISO-8601 param elsewhere); update/updateMany refresh @updatedat unless the caller supplied a value — previously every typed create on a table with @updatedat failed its NOT NULL constraint Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UndecodedBytes was blindly UTF-8 decoded, so custom enum array columns
(e.g. SessionType[]) surfaced as raw wire-format bytes. Now parses the
binary ARRAY wire format (1-D, NULL elements supported) and text array
literals ({A,"c d",NULL}) into List<String?>, falling back to UTF-8 for
scalar enum labels.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for @map and @@map attributes, enabling transparent translation of Dart field and model names to database column and table names. It also adds auto-fill and refresh capabilities for @updatedAt fields, omits unique-keyed delegate methods for models with only composite IDs, decodes custom PostgreSQL enum arrays, and implements special handling for BigInt default values. The review feedback highlights a potential precision loss issue on the web when using BigInt.from for default values, recommending BigInt.parse instead, and warns against mutating the data map directly in SqlCompiler to prevent runtime crashes on unmodifiable maps.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (dartType == 'BigInt') { | ||
| final parse = "BigInt.parse(json['$key'].toString())"; | ||
| if (hasDefault) { | ||
| return "json['$key'] != null ? $parse : BigInt.from(${f.defaultValue})"; | ||
| } | ||
| return f.isRequired ? parse : "json['$key'] != null ? $parse : null"; | ||
| } |
There was a problem hiding this comment.
Using BigInt.from(${f.defaultValue}) can lead to precision loss on the web (Dart compiled to JavaScript) if the default value exceeds double.maxSafeInteger (9007199254740991), because BigInt.from takes a num which is represented as a double-precision float in JS.\n\nUsing BigInt.parse('${f.defaultValue}') with a string literal avoids this issue and preserves full precision across all platforms.
if (dartType == 'BigInt') {\n final parse = "BigInt.parse(json['$key'].toString())";\n if (hasDefault) {\n return "json['$key'] != null ? $parse : BigInt.parse('${f.defaultValue}')";\n }\n return f.isRequired ? parse : "json['$key'] != null ? $parse : null";\n }| // Auto-generate @default(uuid()), @default(cuid()), @default(now()) and | ||
| // @updatedAt values. @updatedAt columns are NOT NULL with no database | ||
| // default — Prisma clients supply the timestamp on every create. | ||
| final effectiveSchema = schema ?? schemaRegistry; | ||
| final model = effectiveSchema.getModel(query.modelName); | ||
| if (model != null) { | ||
| for (final field in model.fields.values) { | ||
| if (data.containsKey(field.name)) continue; | ||
| if (data.containsKey(field.name) || | ||
| data.containsKey(field.columnName)) { | ||
| continue; | ||
| } | ||
| if (field.defaultValue == 'uuid()' || field.defaultValue == 'cuid()') { | ||
| if (provider == 'postgresql' || provider == 'supabase') { | ||
| data[field.name] = const _RawSql('gen_random_uuid()'); | ||
| } | ||
| } else if (field.defaultValue == 'now()') { | ||
| } else if (field.defaultValue == 'now()' || field.isUpdatedAt) { | ||
| if (provider == 'postgresql' || provider == 'supabase') { | ||
| data[field.name] = const _RawSql('NOW()'); | ||
| } else { | ||
| data[field.name] = DateTime.now().toUtc().toIso8601String(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Mutating the data map directly can cause runtime crashes (e.g., UnsupportedError: Cannot modify unmodifiable map) if the caller passes an unmodifiable map (such as a const map or a map wrapped in UnmodifiableMapView). It also introduces side effects by mutating the query arguments map owned by the caller.\n\nCreating a modifiable copy of the map via Map<String, dynamic>.from at the start of the method avoids these issues and ensures defensive programming.
final data = Map<String, dynamic>.from(args['data'] as Map<String, dynamic>);\n // Auto-generate @default(uuid()), @default(cuid()), @default(now()) and\n // @updatedAt values. @updatedAt columns are NOT NULL with no database\n // default — Prisma clients supply the timestamp on every create.\n final effectiveSchema = schema ?? schemaRegistry;\n final model = effectiveSchema.getModel(query.modelName);\n if (model != null) {\n for (final field in model.fields.values) {\n if (data.containsKey(field.name) ||\n data.containsKey(field.columnName)) {\n continue;\n }\n if (field.defaultValue == 'uuid()' || field.defaultValue == 'cuid()') {\n if (provider == 'postgresql' || provider == 'supabase') {\n data[field.name] = const _RawSql('gen_random_uuid()');\n }\n } else if (field.defaultValue == 'now()' || field.isUpdatedAt) {\n if (provider == 'postgresql' || provider == 'supabase') {\n data[field.name] = const _RawSql('NOW()');\n } else {\n data[field.name] = DateTime.now().toUtc().toIso8601String();\n }\n }\n }\n }| // Prisma semantics: @updatedAt columns refresh on every update unless | ||
| // the caller supplied them explicitly. | ||
| final model = (schema ?? schemaRegistry).getModel(query.modelName); | ||
| if (model != null) { | ||
| for (final field in model.fields.values) { | ||
| if (field.isUpdatedAt && | ||
| !data.containsKey(field.name) && | ||
| !data.containsKey(field.columnName)) { | ||
| data[field.name] = DateTime.now().toUtc().toIso8601String(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Mutating the data map directly can cause runtime crashes if the caller passes an unmodifiable map, and has side effects on the caller's query arguments.\n\nCreating a modifiable copy of the map via Map<String, dynamic>.from at the start of the method avoids these issues.
final data = Map<String, dynamic>.from(args['data'] as Map<String, dynamic>);\n // Prisma semantics: @updatedAt columns refresh on every update unless\n // the caller supplied them explicitly.\n final model = (schema ?? schemaRegistry).getModel(query.modelName);\n if (model != null) {\n for (final field in model.fields.values) {\n if (field.isUpdatedAt &&\n !data.containsKey(field.name) &&\n !data.containsKey(field.columnName)) {\n data[field.name] = DateTime.now().toUtc().toIso8601String();\n }\n }\n }| test('BigInt with default falls back to BigInt.from(<default>)', () { | ||
| final flat = flatten(generateWallet()); | ||
|
|
||
| expect( | ||
| flat, | ||
| contains("balancePaise: json['balancePaise'] != null " | ||
| "? BigInt.parse(json['balancePaise'].toString()) " | ||
| ": BigInt.from(0)")); | ||
| }); |
There was a problem hiding this comment.
Update the test expectation to match the suggested change from BigInt.from to BigInt.parse to avoid precision loss on the web.
test('BigInt with default falls back to BigInt.parse(\'<default>\')', () {\n final flat = flatten(generateWallet());\n\n expect(\n flat,\n contains("balancePaise: json['balancePaise'] != null "\n "? BigInt.parse(json['balancePaise'].toString()) "\n ": BigInt.parse('0')"));\n });|
Recreating this PR to trigger CodeRabbit (installed after this PR was opened) — same branch, same commits. Continued in the follow-up PR. |
Why
The familiarise web app's Prisma 7 schema (5,028 lines, 120 models, 100 enums — BetterAuth
@@map-ed tables, BigInt paise money columns, composite-@@idcounters) is being synced into the mobile Dart Frog backend. Generating a client from it surfaced six generator/runtime gaps, all fixed here with tests. This is the connector release the mobile schema-sync PR (Practitionist/familiarise_mobile) depends on.What
Parser & generators
@@map("table")→PrismaModel.dbName; generated SQL targets the mapped table (model User { @@map("users") }→FROM "users"). Explicit@@mapwins over reserved-keyword renames.@map("column")→PrismaField.dbName; flows into@JsonKey, fromJson/toJson keys, and registrycolumnName(e.g.status AppointmentStatus @map("requestStatus")).@@map(...)block attributes and value-level attributes are no longer emitted as enum values (previously generated invalid Dart).@@idmodels (no unique scalar field): delegates omitfindUnique/findUniqueOrThrow/update/deleteinstead of referencing a nonexistentWhereUniqueInput.BigInt @default(0): freezed@Defaultis impossible (BigInt isn't const) — model params become required with aBigInt.from()fromJson fallback; CreateInput leaves them nullable so the DB default applies.Program.licensedSeatConfigwhereLicensedSeatConfig.programIdowns the@relation): registry now emitsisOwner: falsewith the target's real FK instead of fabricating<fieldName>Id— fixescolumn tN.id does not existon nested includes.Runtime
@map-ed columns end-to-end. Unregistered keys pass through unchanged — legacy JsonQueryBuilder callers using literal column names are unaffected (verified insideAND/OR/NOTrecursion too).@updatedAtauto-fill:create/createManyfill it (NOW() on pg/supabase, ISO-8601 param elsewhere);update/updateManyrefresh it unless explicitly supplied. Previously every typed create on a table with@updatedAtfailed its NOT NULL constraint.List<String?>(NULL elements preserved) instead of returning raw bytes.Validation
flutter analyzeclean,dart formatclean.@@map-ed tables, plan CRUD with BigInt paise,@map-edConsultation.statustransitions, enum[] profile reads, nestedincludethroughProgram → LicensedSeatConfig/CreditPoolConfig. Zero open failures.Follow-ups (complete-ORM roadmap)
Tracked in #71: interactive transactions #68, nested writes #64, typed include/relation hydration #67, upsert #66, groupBy/aggregate #65, cursor pagination/Json filters #69, connection pooling #70.
Release
Merging does NOT publish. After merge:
git tag v0.6.0 && git push origin v0.6.0→ publish.yml OIDC-publishes to pub.dev.🤖 Generated with Claude Code