Skip to content

feat: v0.8.0 — typed projection (ScalarField enums, include-select, projected finders) - #75

Merged
teetangh merged 4 commits into
mainfrom
feat/v0.8.0-typed-projection
Jul 23, 2026
Merged

feat: v0.8.0 — typed projection (ScalarField enums, include-select, projected finders)#75
teetangh merged 4 commits into
mainfrom
feat/v0.8.0-typed-projection

Conversation

@teetangh

@teetangh teetangh commented Jul 23, 2026

Copy link
Copy Markdown
Owner

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 JsonQueryBuilder usage 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}ScalarField enums — one plain enum per model (case per scalar field, carrying the Dart field name; @map resolution stays in the registry). No freezed/part-file cost.

Typed per-relation include selectXInclude gains select: List<{Model}ScalarField>?:

db.author.findManyProjected(
  include: AuthorInclude(posts: PostInclude(select: [PostScalarField.title])),
);

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), returning Map<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

  • Include-with-select silently dropped relation rows when the child PK wasn't selected (the deserializer groups child rows by PK). PK columns are now always carried in the aliased selection. Found by the live-Postgres smoke.

Verified

  • 456 unit tests green; flutter analyze --fatal-infos clean; publish dry-run clean.
  • New tests: nested typed relation filters compile to correctly-correlated nested EXISTS, semantically equivalent to legacy FilterOperators.relationPath (same tables + args) — with distinct sub_<relation> aliases (same-name chain repetition documented as a limitation); include-select → projected relation columns; projected-finder generation snapshot.
  • Live smoke vs real Postgres: projected select / distinctOn / computed / include-with-select / nested typed relation filter / findFirstProjected — 6/6.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added typed scalar-field projections for models.
    • Added per-relation select support within include.
    • Introduced typed findManyProjected and findFirstProjected queries with filtering, selection, computed fields, distinct options, and cursors.
  • Bug Fixes

    • Included relation records are now preserved when selected fields omit the child primary key.
    • Improved correlation for nested relation filters.
  • Deprecations

    • Deprecated raw finder methods; use projected queries instead. Removal is planned for version 0.9.0.
  • Release

    • Updated the package to version 0.8.0.

Kaustav Ghosh and others added 4 commits July 23, 2026 16:56
…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>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Typed projection support

Layer / File(s) Summary
Typed model projection contracts
lib/src/generator/cb_model_generator.dart, test/unit/model_generator_include_test.dart
Generated models now include scalar-field enums and relation include select values, with conditional include/select JSON serialization and selectMap() generation.
Projected finder delegates
lib/src/generator/cb_delegate_generator.dart, test/unit/typed_projection_test.dart
Generated delegates add typed findManyProjected and findFirstProjected methods that return maps, and deprecate findFirstRaw.
Query hydration and release validation
lib/src/runtime/query/relation_compiler.dart, test/unit/typed_projection_test.dart, CHANGELOG.md, pubspec.yaml
Relation compilation always retains primary-key columns; tests cover nested filters and projected selects, and package metadata records version 0.8.0.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the release and main change: typed projection with ScalarField enums, include-select, and projected finders.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v0.8.0-typed-projection

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.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/unit/typed_projection_test.dart (1)

138-176: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify the PK-preservation fix it's meant to cover.

This test only checks that name is included and secret excluded; 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 assert contains('"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

📥 Commits

Reviewing files that changed from the base of the PR and between 9128b80 and 8216c27.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • lib/src/generator/cb_delegate_generator.dart
  • lib/src/generator/cb_model_generator.dart
  • lib/src/runtime/query/relation_compiler.dart
  • pubspec.yaml
  • test/unit/model_generator_include_test.dart
  • test/unit/typed_projection_test.dart

Comment on lines +758 to +794
// === 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;
}
''');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
// === 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.

Comment on lines +466 to +475
// 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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
// 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant