Skip to content

feat(sql-builder): add arithmetic operations and type casts#286

Draft
SevInf wants to merge 1 commit intomainfrom
sql-builder-arithmetic-cast
Draft

feat(sql-builder): add arithmetic operations and type casts#286
SevInf wants to merge 1 commit intomainfrom
sql-builder-arithmetic-cast

Conversation

@SevInf
Copy link
Copy Markdown
Contributor

@SevInf SevInf commented Apr 2, 2026

Summary

Adds missing SQL builder expression features needed for running drizzle benchmarks with the sql-builder DSL.

  • Add arithmetic operators (add, sub, mul, div, mod) by extending BinaryOp and reusing BinaryExpr
  • Add CastExpr AST node for type casts, rendering as Postgres (expr)::type syntax
  • Introduce RenderCtx in the postgres adapter to thread CodecRegistry through render functions, resolving cast target types from the registry instead of global codec definitions

Summary by CodeRabbit

Release Notes

  • New Features

    • Added arithmetic operators (add, subtract, multiply, divide, modulo) for SQL expressions
    • Added type casting support to transform expression types in queries
  • Tests

    • Added comprehensive test coverage for arithmetic operations and type casting functionality

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 096406ee-e60d-469a-b299-92d3cec7faca

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes introduce arithmetic operators (add, sub, mul, div, mod) and type casting functionality across the SQL query builder stack. New AST nodes and operator types are added, exposed through builtin functions in the expression API, implemented in the runtime, and rendered by the Postgres adapter with comprehensive test coverage.

Changes

Cohort / File(s) Summary
AST Types Extension
packages/2-sql/4-lanes/relational-core/src/ast/types.ts, packages/2-sql/4-lanes/relational-core/test/ast/kind-discriminants.test.ts
Extended BinaryOp with arithmetic operators (`'add'
SQL Builder API
packages/2-sql/4-lanes/sql-builder/src/expression.ts
Added six new builtin functions to BuiltinFunctions<CT>: arithmetic operators add, sub, mul, div, mod (each accepting two operands and returning typed expression), and cast (accepting expression and target codec descriptor).
Runtime Implementation
packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts, packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts
Implemented arithmetic operators via arithmetic helper that builds BinaryExpr nodes with appropriate metadata, and cast function that creates CastExpr nodes. Added unit tests validating correct BinaryExpr/CastExpr construction, field preservation, and operator values.
Postgres Adapter Rendering
packages/3-targets/6-adapters/postgres/src/core/adapter.ts
Refactored rendering state into RenderCtx containing contract, param map, and codec registry. Added resolveNativeType and renderCast functions to support cast expressions. Extended binary operator rendering to handle arithmetic operators with parentheses. Updated all rendering functions to thread ctx consistently.
Test Coverage
packages/2-sql/4-lanes/sql-builder/test/integration/arithmetic-and-cast.test.ts, packages/3-targets/6-adapters/postgres/test/adapter.test.ts
Added integration tests validating arithmetic operators in SELECT projections, WHERE conditions, and nested expressions with expected numeric results. Added Postgres adapter tests verifying correct SQL generation with parentheses for arithmetic and cast expressions using :: syntax.

Sequence Diagram

sequenceDiagram
    participant User as User Code
    participant Builder as Expression Builder
    participant Runtime as Runtime Functions
    participant AST as AST Layer
    participant Adapter as Postgres Adapter
    participant SQL as SQL Output

    rect rgba(100, 150, 200, 0.5)
    Note over User,SQL: Arithmetic Operation Flow
    User->>Builder: fns.add(expr1, expr2)
    Builder->>Runtime: create ExpressionImpl
    Runtime->>AST: BinaryExpr.add(left, right)
    AST->>Runtime: return BinaryExpr('add', ...)
    Runtime->>Builder: wrap in ExpressionImpl
    Builder->>User: Expression<T>
    
    User->>Adapter: lower query with expression
    Adapter->>Adapter: renderBinary('add', left, right)
    Adapter->>SQL: ("col" + 1)
    SQL->>User: SQL text
    end

    rect rgba(200, 150, 100, 0.5)
    Note over User,SQL: Cast Operation Flow
    User->>Builder: fns.cast(expr, {codecId, nullable})
    Builder->>Runtime: create ExpressionImpl
    Runtime->>AST: CastExpr.of(buildAst(), codecId)
    AST->>Runtime: return CastExpr
    Runtime->>Builder: wrap in ExpressionImpl
    Builder->>User: Expression<Target>
    
    User->>Adapter: lower query with cast expression
    Adapter->>Adapter: renderCast(CastExpr)
    Adapter->>Adapter: resolveNativeType(codecId)
    Adapter->>SQL: ("col")::text
    SQL->>User: SQL text
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through columns bright,
With arithmetic shining light,
Add, divide, and multiply true,
Cast to types both old and new,
From AST through adapter's sight,
SQL math gets done just right! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(sql-builder): add arithmetic operations and type casts' accurately and concisely summarizes the main changes: adding arithmetic operators (add, sub, mul, div, mod) and type cast support to the SQL builder.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sql-builder-arithmetic-cast

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 and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@286

@prisma-next/mongo-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-core@286

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@286

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@286

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@286

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@286

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@286

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@286

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@286

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@286

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@286

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@286

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@286

@prisma-next/contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-ts@286

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@286

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@286

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@286

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@286

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@286

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@286

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@286

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@286

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@286

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@286

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@286

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@286

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@286

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@286

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@286

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@286

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@286

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@286

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@286

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@286

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@286

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@286

@prisma-next/core-control-plane

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/core-control-plane@286

@prisma-next/core-execution-plane

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/core-execution-plane@286

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@286

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@286

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@286

@prisma-next/plan

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/plan@286

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@286

commit: ec30b9c

@SevInf SevInf force-pushed the sql-builder-arithmetic-cast branch from 22f3799 to 48da40c Compare April 2, 2026 16:36
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/2-sql/4-lanes/sql-builder/src/expression.ts`:
- Around line 132-135: The cast function currently takes a target nullable flag
and uses it for the resulting Expression, allowing callers to incorrectly cast a
nullable source to a non-null type; update the cast signature and implementation
so the resulting Expression preserves the source expression's nullability
instead of trusting target.nullable: keep TargetCodecId generic for codecId but
derive Nullable from the input expr (e.g. read expr.field.codecId.nullable or
the expr type parameter) and construct the returned Expression<{ codecId:
TargetCodecId; nullable: SourceNullable }>, and ensure ExpressionImpl.field is
assigned using that source-nullable value rather than target.nullable.

In `@packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts`:
- Around line 80-87: The arithmetic function currently copies a single operand's
field, causing incorrect metadata for mixed codecs and nullability; update
arithmetic(a,b,op) (which constructs new ExpressionImpl(new BinaryExpr(...))) to
derive a new field instead of reusing a.field or b.field: compute codecId by
resolving both operand codecs and promoting/merging them (e.g., choose a
compatible numeric supertype or fallback to 'unknown' when incompatible), and
set nullable = (leftField?.nullable || rightField?.nullable); ensure you extract
field metadata from ExpressionImpl instances only (leftField/rightField), handle
missing metadata safely, and use that derived field when creating the new
ExpressionImpl for the BinaryExpr.

In
`@packages/2-sql/4-lanes/sql-builder/test/integration/arithmetic-and-cast.test.ts`:
- Around line 63-70: The test currently uses rows.every(...) which is vacuously
true for an empty result set; update the test in arithmetic-and-cast.test.ts to
assert the query returns results before checking the predicate (e.g., assert
rows.length > 0) or replace the loose predicate with a concrete expected result
comparison; locate the test using
runtime().execute(db().posts.select('id','views').where((f,fns) =>
fns.gt(fns.add(f.views,50),150)).build()) and add a precondition on rows (or an
explicit expectedRows assertion) before calling rows.every(...) to ensure the
filter actually matched rows.
🪄 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: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 2bd0a21a-1223-4217-8c48-2f25bef9f714

📥 Commits

Reviewing files that changed from the base of the PR and between 50d26af and 22f3799.

📒 Files selected for processing (8)
  • packages/2-sql/4-lanes/relational-core/src/ast/types.ts
  • packages/2-sql/4-lanes/relational-core/test/ast/kind-discriminants.test.ts
  • packages/2-sql/4-lanes/sql-builder/src/expression.ts
  • packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts
  • packages/2-sql/4-lanes/sql-builder/test/integration/arithmetic-and-cast.test.ts
  • packages/2-sql/4-lanes/sql-builder/test/runtime/functions.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/adapter.ts
  • packages/3-targets/6-adapters/postgres/test/adapter.test.ts

Comment on lines +132 to +135
cast: <TargetCodecId extends string, Nullable extends boolean>(
expr: Expression<ScopeField>,
target: { codecId: TargetCodecId; nullable: Nullable },
) => Expression<{ codecId: TargetCodecId; nullable: Nullable }>;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cast() should preserve source nullability.

This signature lets fns.cast(nullableExpr, { codecId: 'pg/text@1', nullable: false }) type-check as non-null, and the runtime implementation copies that flag straight into ExpressionImpl.field. SQL casts do not turn NULL into non-NULL, so the output nullability should be derived from the source expression instead of caller input.

🐛 Proposed fix
-  cast: <TargetCodecId extends string, Nullable extends boolean>(
-    expr: Expression<ScopeField>,
-    target: { codecId: TargetCodecId; nullable: Nullable },
-  ) => Expression<{ codecId: TargetCodecId; nullable: Nullable }>;
+  cast: <Source extends ScopeField, TargetCodecId extends string>(
+    expr: Expression<Source>,
+    target: { codecId: TargetCodecId },
+  ) => Expression<{ codecId: TargetCodecId; nullable: Source['nullable'] }>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/4-lanes/sql-builder/src/expression.ts` around lines 132 - 135,
The cast function currently takes a target nullable flag and uses it for the
resulting Expression, allowing callers to incorrectly cast a nullable source to
a non-null type; update the cast signature and implementation so the resulting
Expression preserves the source expression's nullability instead of trusting
target.nullable: keep TargetCodecId generic for codecId but derive Nullable from
the input expr (e.g. read expr.field.codecId.nullable or the expr type
parameter) and construct the returned Expression<{ codecId: TargetCodecId;
nullable: SourceNullable }>, and ensure ExpressionImpl.field is assigned using
that source-nullable value rather than target.nullable.

Comment on lines +80 to +87
function arithmetic(a: ExprOrVal, b: ExprOrVal, op: BinaryOp): ExpressionImpl {
const field =
a instanceof ExpressionImpl
? a.field
: b instanceof ExpressionImpl
? b.field
: { codecId: 'unknown', nullable: false };
return new ExpressionImpl(new BinaryExpr(op, resolve(a), resolve(b)), field);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Derive arithmetic result metadata instead of copying one operand.

Lines 82-86 reuse a single operand field verbatim. That makes mixed-codec arithmetic like int4 + float8 keep whichever codec happens to come first, and nonNull + nullable stay non-nullable even though the SQL result can still be NULL.

🐛 Proposed fix
 function arithmetic(a: ExprOrVal, b: ExprOrVal, op: BinaryOp): ExpressionImpl {
-  const field =
-    a instanceof ExpressionImpl
-      ? a.field
-      : b instanceof ExpressionImpl
-        ? b.field
-        : { codecId: 'unknown', nullable: false };
+  const leftField = a instanceof ExpressionImpl ? a.field : undefined;
+  const rightField = b instanceof ExpressionImpl ? b.field : undefined;
+
+  if (leftField && rightField && leftField.codecId !== rightField.codecId) {
+    throw new Error(
+      `Arithmetic between ${leftField.codecId} and ${rightField.codecId} is not typed yet`,
+    );
+  }
+
+  const field = {
+    codecId: leftField?.codecId ?? rightField?.codecId ?? 'unknown',
+    nullable: Boolean(leftField?.nullable || rightField?.nullable),
+  };
+
   return new ExpressionImpl(new BinaryExpr(op, resolve(a), resolve(b)), field);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts` around lines 80
- 87, The arithmetic function currently copies a single operand's field, causing
incorrect metadata for mixed codecs and nullability; update arithmetic(a,b,op)
(which constructs new ExpressionImpl(new BinaryExpr(...))) to derive a new field
instead of reusing a.field or b.field: compute codecId by resolving both operand
codecs and promoting/merging them (e.g., choose a compatible numeric supertype
or fallback to 'unknown' when incompatible), and set nullable =
(leftField?.nullable || rightField?.nullable); ensure you extract field metadata
from ExpressionImpl instances only (leftField/rightField), handle missing
metadata safely, and use that derived field when creating the new ExpressionImpl
for the BinaryExpr.

Comment on lines +63 to +70
it('arithmetic in WHERE clause filters correctly', async () => {
const rows = await runtime().execute(
db()
.posts.select('id', 'views')
.where((f, fns) => fns.gt(fns.add(f.views, 50), 150))
.build(),
);
expect(rows.every((r) => r.views + 50 > 150)).toBe(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

This WHERE assertion is vacuously true on an empty result set.

rows.every(...) returns true for [], so this test still passes if the predicate is rendered incorrectly and filters everything out. Assert against a concrete expected row set, or at least verify the query returned rows before checking the predicate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/2-sql/4-lanes/sql-builder/test/integration/arithmetic-and-cast.test.ts`
around lines 63 - 70, The test currently uses rows.every(...) which is vacuously
true for an empty result set; update the test in arithmetic-and-cast.test.ts to
assert the query returns results before checking the predicate (e.g., assert
rows.length > 0) or replace the loose predicate with a concrete expected result
comparison; locate the test using
runtime().execute(db().posts.select('id','views').where((f,fns) =>
fns.gt(fns.add(f.views,50),150)).build()) and add a precondition on rows (or an
explicit expectedRows assertion) before calling rows.every(...) to ensure the
filter actually matched rows.

Comment on lines +584 to +588
function renderInsert(ast: InsertAst, ctx: RenderCtx): string {
const contract = ctx.contract;
if (!contract) {
throw new Error('INSERT requires a contract');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Only require contract when INSERT rendering actually needs schema metadata.

Line 586 now throws for every INSERT, including explicit row inserts and single-row DEFAULT VALUES that can already be rendered without a contract. That is a regression for callers lowering raw AST inserts outside the contract-aware builder path.

🐛 Proposed fix
 function renderInsert(ast: InsertAst, ctx: RenderCtx): string {
   const contract = ctx.contract;
-  if (!contract) {
-    throw new Error('INSERT requires a contract');
-  }
   const table = quoteIdentifier(ast.table.name);
   const rows = ast.rows;
@@
     if (!hasExplicitValues) {
       if (rows.length === 1) {
         return `INSERT INTO ${table} DEFAULT VALUES`;
       }
+      if (!contract) {
+        throw new Error('INSERT with multiple DEFAULT rows requires a contract');
+      }
       const defaultColumns = getInsertColumnOrder(rows, contract, ast.table.name);
@@
-    const columnOrder = getInsertColumnOrder(rows, contract, ast.table.name);
+    const columnOrder = [...new Set(rows.flatMap((row) => Object.keys(row)))];

@SevInf SevInf force-pushed the sql-builder-arithmetic-cast branch from 48da40c to a9e4240 Compare April 2, 2026 16:48
@SevInf SevInf marked this pull request as draft April 2, 2026 16:49
Add arithmetic operators (add, sub, mul, div, mod) and type cast
expressions to the SQL builder DSL, needed for drizzle-benchmark parity.

- Extend BinaryOp with arithmetic ops, reusing BinaryExpr
- Add CastExpr AST node rendering as Postgres (expr)::type syntax
- Resolve cast native types from CodecRegistry via RenderCtx
- Refactor adapter render functions to use RenderCtx object
@SevInf SevInf force-pushed the sql-builder-arithmetic-cast branch from a9e4240 to ec30b9c Compare April 2, 2026 17:56
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