feat: v0.8.0 — typed projection (ScalarField enums, include-select, projected finders) - #75
Conversation
…select
v0.8.0 item 1 — typed projection building blocks:
- New plain enum `{Model}ScalarField` per model: one case per scalar
(non-relation) field carrying the Dart field name (compiler resolves @Map
via the registry). Near-zero codegen cost (no freezed/part files). Case
names avoid Dart enum built-ins (`values` -> `valuesField`, etc.).
- `XInclude` gains `select: List<{Model}ScalarField>?` — a scalar projection
applied when the include is NESTED under a parent include. toJson now emits
per-relation `true` | `{'include': ..., 'select': ...}`, exactly the shape
relation_compiler already consumes (select on the ROOT include is ignored;
root projection arrives with the projected finders in the next commit).
- `_freezedClass` supports extra methods (used for `selectMap()`).
Suite green at 449.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elpers v0.8.0 item 2 — the typed projection surface: - New `findManyProjected` / `findFirstProjected` on every delegate: typed inputs (XWhereInput, orderBy Map|List|XOrderByInput, cursor, XInclude with per-relation select, `select: List<XScalarField>`, `computed: Map<String, ComputedField>`, `distinct`/`distinctOn`), Map rows out — projected/computed rows never hydrate typed models. This single surface replaces every `.select()`/`.selectFields()`/computed/raw-helper call site. - `findManyRaw`/`findFirstRaw` are now `@Deprecated` (removal in 0.9.0) so consumers keep compiling mid-migration. Suite green at 449. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fix v0.8.0 item 3: - typed_projection_test.dart: nested typed relation filters compile to correctly-correlated nested EXISTS and are semantically equivalent to the legacy FilterOperators.relationPath shape (same tables, same args) — with distinct sub_<relation> aliases along the chain (same-name repetition documented as a limitation). Include-with-select compiles to projected relation columns. findManyProjected/findFirstProjected generation snapshot + @deprecated raw helpers. - relation_compiler fix surfaced by the live smoke: a per-relation `select` that omitted the child PK silently dropped the relation's rows (the deserializer groups by PK). The PK columns are now always carried in the aliased selection. - Live smoke (real Postgres): projected select/distinctOn/computed, include-with-select, nested typed relation filter, findFirstProjected — 6/6. Suite green at 456. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ScalarField enums, typed per-relation include select, findManyProjected/ findFirstProjected (typed inputs, Map rows), deprecated raw helpers, and the include-select PK-preservation fix. 456 unit tests green; live Postgres smoke 6/6; flutter analyze --fatal-infos clean; publish dry-run clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📝 WalkthroughWalkthroughThe 0.8.0 release adds typed scalar projections, relation-scoped selects, projected finder delegates returning maps, primary-key-safe relation hydration, nested relation-filter coverage, raw finder deprecation, and matching changelog and package-version updates. ChangesTyped projection support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant GeneratedDelegate
participant JsonQueryBuilder
participant QueryExecutor
Caller->>GeneratedDelegate: call findManyProjected or findFirstProjected
GeneratedDelegate->>JsonQueryBuilder: apply typed projection inputs
JsonQueryBuilder->>QueryExecutor: execute projected JSON query
QueryExecutor-->>GeneratedDelegate: return map rows
GeneratedDelegate-->>Caller: return projected maps
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8)pubspec.yamlTraceback (most recent call last): Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
test/unit/typed_projection_test.dart (1)
138-176: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTest doesn't verify the PK-preservation fix it's meant to cover.
This test only checks that
nameis included andsecretexcluded; it never asserts that the relation's primary key column ("author__id") is present in the SQL. The PK-preservation fix (relation_compiler.dart) is this release's headline bug fix — strengthen this test to assertcontains('"author__id"')so a regression of the actual fix would be caught here.✅ Suggested addition
expect(r.sql, contains('"author__name"')); expect(r.sql, isNot(contains('"author__secret"'))); + // The fix under test: PK must always be carried for relation + // hydration/dedup even when not explicitly selected. + expect(r.sql, contains('"author__id"'));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/unit/typed_projection_test.dart` around lines 138 - 176, Strengthen the test “select sub-map limits the relation columns in the SELECT list” to assert that the compiled SQL contains the relation primary-key column `"author__id"` in addition to the existing name and secret assertions. Keep the current assertions unchanged so the test covers both PK preservation and select-based column filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/src/generator/cb_model_generator.dart`:
- Around line 758-794: Guard _buildScalarFieldEnum for models with no scalar
fields so it does not generate an invalid enum with only a semicolon. Skip
generating the scalar-field enum, or apply the generator’s established handling
for empty declarations, while preserving the existing enum output for models
containing scalar fields and updating any callers that require a generated type.
In `@lib/src/runtime/query/relation_compiler.dart`:
- Around line 466-475: Scope the primary-key augmentation in _addSelectedColumns
to relation-nested selections only by applying it when relationPath != null.
Preserve selectedFields unchanged for the base-model compile() path, while
retaining PK inclusion for _compileRelation hydration and deduplication.
---
Nitpick comments:
In `@test/unit/typed_projection_test.dart`:
- Around line 138-176: Strengthen the test “select sub-map limits the relation
columns in the SELECT list” to assert that the compiled SQL contains the
relation primary-key column `"author__id"` in addition to the existing name and
secret assertions. Keep the current assertions unchanged so the test covers both
PK preservation and select-based column filtering.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 78ba450a-75da-41ad-bbec-39bd96eda94a
📒 Files selected for processing (7)
CHANGELOG.mdlib/src/generator/cb_delegate_generator.dartlib/src/generator/cb_model_generator.dartlib/src/runtime/query/relation_compiler.dartpubspec.yamltest/unit/model_generator_include_test.darttest/unit/typed_projection_test.dart
| // === Scalar-field enum (typed projection) === | ||
|
|
||
| /// Enum-case name for a scalar field, avoiding the identifiers every Dart | ||
| /// enum already declares (`values`, `index`, …). | ||
| String _scalarEnumCase(String name) { | ||
| const reserved = { | ||
| 'values', | ||
| 'index', | ||
| 'hashCode', | ||
| 'runtimeType', | ||
| 'toString', | ||
| 'noSuchMethod', | ||
| }; | ||
| return reserved.contains(name) ? '${name}Field' : name; | ||
| } | ||
|
|
||
| /// Plain enum `{Model}ScalarField` — one case per scalar (non-relation) | ||
| /// field, carrying the Dart field name for the compiler to resolve via the | ||
| /// registry (@map-aware). Used by typed projection (`select:`/`distinctOn:`) | ||
| /// and per-relation include `select`. | ||
| Spec _buildScalarFieldEnum(PrismaModel model) { | ||
| final scalars = model.fields.where((f) => !f.isRelation).toList(); | ||
| final cases = scalars | ||
| .map((f) => " ${_scalarEnumCase(f.name)}('${f.name}')") | ||
| .join(',\n'); | ||
| return Code(''' | ||
| /// Scalar fields of ${model.name} for typed projection. | ||
| enum ${model.name}ScalarField { | ||
| $cases; | ||
|
|
||
| const ${model.name}ScalarField(this.fieldName); | ||
|
|
||
| /// The Dart field name (the compiler resolves @map columns via the registry). | ||
| final String fieldName; | ||
| } | ||
| '''); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Guard against zero-scalar-field models.
If model.fields.where((f) => !f.isRelation) is empty (a model with only relation fields), cases is '' and the generated source is enum ${model.name}ScalarField {\n;\n ... } — an enum with no declared instances, which is invalid Dart (an enum must declare at least one instance). This would break codegen for that model's entire file.
🛡️ Proposed guard
Spec _buildScalarFieldEnum(PrismaModel model) {
final scalars = model.fields.where((f) => !f.isRelation).toList();
+ if (scalars.isEmpty) {
+ return Code('''
+/// Scalar fields of ${model.name} for typed projection.
+/// (${model.name} has no scalar fields.)
+enum ${model.name}ScalarField { none(''); const ${model.name}ScalarField(this.fieldName); final String fieldName; }
+''');
+ }
final cases = scalars📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // === Scalar-field enum (typed projection) === | |
| /// Enum-case name for a scalar field, avoiding the identifiers every Dart | |
| /// enum already declares (`values`, `index`, …). | |
| String _scalarEnumCase(String name) { | |
| const reserved = { | |
| 'values', | |
| 'index', | |
| 'hashCode', | |
| 'runtimeType', | |
| 'toString', | |
| 'noSuchMethod', | |
| }; | |
| return reserved.contains(name) ? '${name}Field' : name; | |
| } | |
| /// Plain enum `{Model}ScalarField` — one case per scalar (non-relation) | |
| /// field, carrying the Dart field name for the compiler to resolve via the | |
| /// registry (@map-aware). Used by typed projection (`select:`/`distinctOn:`) | |
| /// and per-relation include `select`. | |
| Spec _buildScalarFieldEnum(PrismaModel model) { | |
| final scalars = model.fields.where((f) => !f.isRelation).toList(); | |
| final cases = scalars | |
| .map((f) => " ${_scalarEnumCase(f.name)}('${f.name}')") | |
| .join(',\n'); | |
| return Code(''' | |
| /// Scalar fields of ${model.name} for typed projection. | |
| enum ${model.name}ScalarField { | |
| $cases; | |
| const ${model.name}ScalarField(this.fieldName); | |
| /// The Dart field name (the compiler resolves @map columns via the registry). | |
| final String fieldName; | |
| } | |
| '''); | |
| } | |
| // === Scalar-field enum (typed projection) === | |
| /// Enum-case name for a scalar field, avoiding the identifiers every Dart | |
| /// enum already declares (`values`, `index`, …). | |
| String _scalarEnumCase(String name) { | |
| const reserved = { | |
| 'values', | |
| 'index', | |
| 'hashCode', | |
| 'runtimeType', | |
| 'toString', | |
| 'noSuchMethod', | |
| }; | |
| return reserved.contains(name) ? '${name}Field' : name; | |
| } | |
| /// Plain enum `{Model}ScalarField` — one case per scalar (non-relation) | |
| /// field, carrying the Dart field name for the compiler to resolve via the | |
| /// registry (`@map-aware`). Used by typed projection (`select:`/`distinctOn:`) | |
| /// and per-relation include `select`. | |
| Spec _buildScalarFieldEnum(PrismaModel model) { | |
| final scalars = model.fields.where((f) => !f.isRelation).toList(); | |
| if (scalars.isEmpty) { | |
| return Code(''' | |
| /// Scalar fields of ${model.name} for typed projection. | |
| /// (${model.name} has no scalar fields.) | |
| enum ${model.name}ScalarField { none(''); const ${model.name}ScalarField(this.fieldName); final String fieldName; } | |
| '''); | |
| } | |
| final cases = scalars | |
| .map((f) => " ${_scalarEnumCase(f.name)}('${f.name}')") | |
| .join(',\n'); | |
| return Code(''' | |
| /// Scalar fields of ${model.name} for typed projection. | |
| enum ${model.name}ScalarField { | |
| $cases; | |
| const ${model.name}ScalarField(this.fieldName); | |
| /// The Dart field name (the compiler resolves `@map` columns via the registry). | |
| final String fieldName; | |
| } | |
| '''); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/generator/cb_model_generator.dart` around lines 758 - 794, Guard
_buildScalarFieldEnum for models with no scalar fields so it does not generate
an invalid enum with only a semicolon. Skip generating the scalar-field enum, or
apply the generator’s established handling for empty declarations, while
preserving the existing enum output for models containing scalar fields and
updating any callers that require a generated type.
| // Always carry the primary key: the relation deserializer groups/dedupes | ||
| // child rows by PK, so a select that omits it would silently drop the | ||
| // relation's rows from the hydrated result. | ||
| final effectiveFields = [ | ||
| ...selectedFields, | ||
| for (final pk in model.primaryKeys) | ||
| if (!selectedFields.contains(pk.name)) pk.name, | ||
| ]; | ||
|
|
||
| for (final fieldName in effectiveFields) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
PK-preservation fix leaks into base-model select, not just relation includes.
_addSelectedColumns is shared by the base-model select path (compile(), relationPath: null) and the relation-nested select path (_compileRelation, relationPath always set). The new PK-force logic applies unconditionally to both, so findManyProjected(select: [...]) at the root will always silently include the primary key even when the caller's typed select: explicitly omits it — defeating the precise-projection contract this release is built around. The documented fix is specifically about relation-row dedup/hydration (CHANGELOG "Fixed" section), not the base row.
Scope the PK-force to relation calls only, using the already-available relationPath != null signal:
🛡️ Proposed fix
- // Always carry the primary key: the relation deserializer groups/dedupes
- // child rows by PK, so a select that omits it would silently drop the
- // relation's rows from the hydrated result.
- final effectiveFields = [
- ...selectedFields,
- for (final pk in model.primaryKeys)
- if (!selectedFields.contains(pk.name)) pk.name,
- ];
+ // Relation rows are grouped/deduped by PK during hydration, so a
+ // relation-nested select that omits it would silently drop rows. This
+ // only applies to relation-nested selects (relationPath != null); the
+ // base model's own select is returned as-is with no such dedup step.
+ final effectiveFields = relationPath != null
+ ? [
+ ...selectedFields,
+ for (final pk in model.primaryKeys)
+ if (!selectedFields.contains(pk.name)) pk.name,
+ ]
+ : selectedFields;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Always carry the primary key: the relation deserializer groups/dedupes | |
| // child rows by PK, so a select that omits it would silently drop the | |
| // relation's rows from the hydrated result. | |
| final effectiveFields = [ | |
| ...selectedFields, | |
| for (final pk in model.primaryKeys) | |
| if (!selectedFields.contains(pk.name)) pk.name, | |
| ]; | |
| for (final fieldName in effectiveFields) { | |
| // Relation rows are grouped/deduped by PK during hydration, so a | |
| // relation-nested select that omits it would silently drop rows. This | |
| // only applies to relation-nested selects (relationPath != null); the | |
| // base model's own select is returned as-is with no such dedup step. | |
| final effectiveFields = relationPath != null | |
| ? [ | |
| ...selectedFields, | |
| for (final pk in model.primaryKeys) | |
| if (!selectedFields.contains(pk.name)) pk.name, | |
| ] | |
| : selectedFields; | |
| for (final fieldName in effectiveFields) { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/runtime/query/relation_compiler.dart` around lines 466 - 475, Scope
the primary-key augmentation in _addSelectedColumns to relation-nested
selections only by applying it when relationPath != null. Preserve
selectedFields unchanged for the base-model compile() path, while retaining PK
inclusion for _compileRelation hydration and deduplication.
prisma_flutter_connector v0.8.0 — typed projection
Completes the typed surface for the last raw-map query shapes, so consumers can retire hand-built
JsonQueryBuilderusage entirely. Zero runtime SQL changes — the engine already supported every shape; this release exposes them through the generated typed API (plus one deserializer-adjacent fix found by live testing).What's new
{Model}ScalarFieldenums — one plain enum per model (case per scalar field, carrying the Dart field name;@mapresolution stays in the registry). No freezed/part-file cost.Typed per-relation include
select—XIncludegainsselect: List<{Model}ScalarField>?:Emits
true|{'include': ..., 'select': ...}— the shape the relation compiler already consumes.findManyProjected/findFirstProjected— fully-typed projection finders on every delegate (XWhereInput, orderBy, take/skip/cursor,XInclude-with-select,select: List<XScalarField>,computed: Map<String, ComputedField>,distinct/distinctOn), returningMap<String, dynamic>rows — projected/computed rows never hydrate typed models. One surface replaces every.select()/.selectFields()/ computed / raw-helper call site.Deprecated
findManyRaw/findFirstRaw→ use the projected finders (removal in 0.9.0).Fixed
Verified
flutter analyze --fatal-infosclean; publish dry-run clean.EXISTS, semantically equivalent to legacyFilterOperators.relationPath(same tables + args) — with distinctsub_<relation>aliases (same-name chain repetition documented as a limitation); include-select → projected relation columns; projected-finder generation snapshot.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
selectsupport withininclude.findManyProjectedandfindFirstProjectedqueries with filtering, selection, computed fields, distinct options, and cursors.Bug Fixes
Deprecations
Release