Skip to content

feat(query-builder): expression args (col/lit/param/fn) + computed SELECT columns#1353

Merged
pyramation merged 1 commit into
mainfrom
feat/query-builder-expressions
Jul 11, 2026
Merged

feat(query-builder): expression args (col/lit/param/fn) + computed SELECT columns#1353
pyramation merged 1 commit into
mainfrom
feat/query-builder-expressions

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

Extends @constructive-io/query-builder so function-call arguments are no longer limited to auto-parameterized values, and so a function call can be a computed SELECT column over a base table. This unlocks per-row getter projections (e.g. calling a scope-aware SQL getter once per row) that previously had to be hand-built as SQL template strings.

Motivation: downstream (constructive-db knative handlers) hand-writes SQL like the secrets/config projection, branching on scope to pick a getter signature. With this, that becomes programmatic: build a named-arg object, conditionally include owner_id/database_id, and the scope branch collapses.

New expression helpers

import { col, lit, param, fn } from '@constructive-io/query-builder';

col('s.owner_id')   // column reference (NOT parameterized)
lit(null)           // SQL literal -> NULL  (also numbers/strings/bools)
param('x')          // explicit bound $n
fn('priv.get', { a: col('s.a') })  // function call as an expression (schema-qualified name ok)

A function-call arg (FnArg) is now Expr | SqlValue — plain values still auto-parameterize (fully backward compatible). Positional vs named is just the container shape (array vs object); both accept expressions.

New builder methods

.selectCall(as, name, args?, opts?)  // append a computed function-call column
.selectExpr(as, expr)                // append any computed column expression
.call(name, args?, opts?)            // opts.schema now overrides the chained .schema()

Example — the scope-switch shape it enables

new QueryBuilder()
  .schema('priv').table('secrets', 's')
  .select(['s.name'])
  .selectCall('decrypted_value', 'secrets_get', {
    owner_id: col('s.owner_id'),        // column ref, no bind
    secret_name: col('s.name'),
    namespace_id: col('s.namespace_id'),
    default_value: lit(null),           // literal NULL
  }, { schema: 'priv' })
  .where('s.namespace_id', '=', nsId)
  .where('s.retired_at', 'IS', null)
  .orderBy('s.created_at', 'ASC')
  .build();
// SELECT s.name, priv.secrets_get(owner_id => s.owner_id, secret_name => s.name,
//        namespace_id => s.namespace_id, default_value => NULL) AS decrypted_value
// FROM priv.secrets AS s WHERE s.namespace_id = $1 AND s.retired_at IS NULL ORDER BY s.created_at
// values: [nsId]

Implementation note

Expressions are lazy thunks (alloc: ParamAllocator) => AstNode evaluated during build() in statement order, so $n numbering stays correct even when a computed column and the WHERE clause both bind params (target list is allocated before WHERE). No string concatenation — everything is still pg-ast nodes deparsed via pgsql-deparser.

Testing

pnpm test — 74/74 pass (67 existing + 7 new covering column-ref args, lit(null), mixed args, schema-qualified fn(), the computed-column scope-switch shape, and param-ordering). No existing behavior changed; the only internal change is _columns: string[]_selectItems: SelectItem[].

Link to Devin session: https://app.devin.ai/sessions/f4e36ac1d5854101838e6825df191c99
Requested by: @pyramation

…LECT columns

Function-call arguments can now be column references (col), SQL literals
(lit), explicit params (param), or nested function calls (fn) — not just
auto-parameterized values. Adds .selectCall()/.selectExpr() so a function
call can be a computed SELECT column over a base table, enabling per-row
getter projections like:

  SELECT s.name, priv.secrets_get(secret_name => s.name, ...) AS v
  FROM priv.secrets s WHERE ...

Positional vs named is just the arg container (array vs object); both accept
expressions or plain values. Fully backward compatible (plain values still
auto-parameterize). Param numbering stays correct via lazy expr thunks.
@pyramation pyramation self-assigned this Jul 11, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@pyramation
pyramation merged commit b223a58 into main Jul 11, 2026
37 checks passed
@pyramation
pyramation deleted the feat/query-builder-expressions branch July 11, 2026 01:10
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