@vibeorm/sql 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ast.d.ts CHANGED
@@ -868,10 +868,21 @@ export type SelectStatement = {
868
868
  * The lock a locking read takes: one strength, one wait policy. Both members
869
869
  * are REQUIRED here — defaulting happens once, in the query builder, so the
870
870
  * rendered SQL is a function of the AST alone.
871
+ *
872
+ * `of` narrows the lock to named FROM items (`FOR UPDATE OF "Parent"`). The
873
+ * join relation strategy needs it: its statement also reads child rows through
874
+ * `LEFT JOIN LATERAL` subqueries, and an unnarrowed clause is refused by
875
+ * postgres ("FOR UPDATE is not allowed with aggregate functions", from the
876
+ * children's `json_agg`). With `of` naming the statement's own table only the
877
+ * parent rows are locked. Each name must be the statement's table alias, or its
878
+ * unqualified table name when it has no alias — the only FROM item the AST
879
+ * names; the renderer refuses anything else. Dialects without a locking-read
880
+ * clause render nothing, as for a lock without `of`.
871
881
  */
872
882
  export type RowLock = {
873
883
  readonly strength: RowLockStrength;
874
884
  readonly wait: RowLockWaitPolicy;
885
+ readonly of?: readonly string[];
875
886
  };
876
887
  /**
877
888
  * Sentinel VALUES cell: render the `DEFAULT` keyword instead of a placeholder
@@ -960,9 +971,28 @@ export type ReturningColumn = string | {
960
971
  } | {
961
972
  readonly name: string;
962
973
  readonly transform: "jsonText";
974
+ } | CaptureReturningColumn;
975
+ /**
976
+ * A row-capture RETURNING column (`$transaction(…, { capture })`, dialects with
977
+ * the `rowCapture` capability: PostgreSQL 18, where RETURNING names the row
978
+ * before the statement `old` and the row after it `new`):
979
+ *
980
+ * - `"old"` / `"new"`: `old."col"::text AS "alias"` / `new."col"::text AS "alias"`,
981
+ * the value as text so every type survives exactly (`old` is all NULL for an
982
+ * inserted row, `new` for a deleted one);
983
+ * - `"inserted"`: `(old."col" IS NULL) AS "alias"` — the arm of an `INSERT …
984
+ * ON CONFLICT DO UPDATE` row, true when it was inserted (`col` must be a
985
+ * column no stored row holds NULL in: a primary-key column).
986
+ */
987
+ export type CaptureReturningColumn = {
988
+ readonly capture: "old" | "new" | "inserted";
989
+ readonly name: string;
990
+ readonly alias: string;
963
991
  };
964
992
  /** The column name a {@link ReturningColumn} exposes in the result row. */
965
993
  export declare function returningColumnName(column: ReturningColumn): string;
994
+ /** Is this RETURNING column a row-capture column ({@link CaptureReturningColumn})? */
995
+ export declare function isCaptureReturningColumn(column: ReturningColumn): column is CaptureReturningColumn;
966
996
  export type InsertStatement = {
967
997
  readonly kind: "insert";
968
998
  readonly table: TableRef;
@@ -1319,6 +1349,41 @@ export type ChainResult = {
1319
1349
  * it instead of counting result rows (effect `writeTally`).
1320
1350
  */
1321
1351
  readonly tally?: ChainTally;
1352
+ /**
1353
+ * Row capture ({@link ChainCapture}): per captured data-modifying step, its
1354
+ * row count and its distinct before and after images, on every result row
1355
+ * (uncorrelated scalar subqueries, evaluated once). Needs the dialect's
1356
+ * `rowCapture` capability. Rendered after every other result column, so
1357
+ * their bound limits come last.
1358
+ */
1359
+ readonly capture?: readonly ChainCapture[];
1360
+ };
1361
+ /**
1362
+ * One captured step of a chain ({@link ChainResult.capture}). Result columns,
1363
+ * `<alias>` being this entry's alias:
1364
+ *
1365
+ * - `<alias>_n`: `(SELECT count(*) FROM "step")`;
1366
+ * - `<alias>_i` (with `inserted`): the rows whose arm marker is true;
1367
+ * - `<alias>_b` / `<alias>_a` (with `before` / `after`): `(SELECT
1368
+ * array_agg("__vibe_i")::text FROM (SELECT DISTINCT ARRAY["c0", "c1"] AS
1369
+ * "__vibe_i" FROM "step" LIMIT $n) AS "__vibe_d")` — a two-dimensional text
1370
+ * array literal of at most `limit` distinct images, NULL when the step
1371
+ * returned no row. A before image skips the rows whose arm marker is true.
1372
+ *
1373
+ * `before` / `after` name columns the step RETURNS (its
1374
+ * {@link CaptureReturningColumn} aliases), in image order. `limit` is bound once.
1375
+ */
1376
+ export type ChainCapture = {
1377
+ readonly step: string;
1378
+ readonly alias: string;
1379
+ /** The step's `"inserted"` arm-marker column, for an `INSERT … ON CONFLICT DO UPDATE` step. */
1380
+ readonly inserted?: string;
1381
+ /** With `inserted`: images of inserted rows only — `after` reads the rows whose marker is true. */
1382
+ readonly insertedOnly?: true;
1383
+ readonly before?: readonly string[];
1384
+ readonly after?: readonly string[];
1385
+ /** The most distinct images one image column returns: a positive safe integer, bound as a parameter. */
1386
+ readonly limit: number;
1322
1387
  };
1323
1388
  /**
1324
1389
  * Fail the statement when `step` returned no row; `token` names the veto in the
@@ -1,20 +1,30 @@
1
1
  import type { SqlDialect } from "../dialect.ts";
2
2
  import type { RenderedQuery } from "../render.ts";
3
- import type { SqlStatement } from "../ast.ts";
3
+ import type { SqlStatement, TableRef } from "../ast.ts";
4
4
  import type { PostgresExplainOptions, PostgresWorkloadQuery } from "./postgres-diagnostics.ts";
5
5
  /** Fixed, parameterless diagnostics whose text never varies. */
6
6
  export type FixedDiagnosticQuery = "postgresJsonWire" | "sqliteTriggers";
7
+ /** One table of row capture's readiness read: the table as write statements name it, and the columns capture returns. */
8
+ export type CaptureReadinessTable = {
9
+ readonly table: TableRef;
10
+ readonly columns: readonly string[];
11
+ };
7
12
  /**
8
13
  * Fixed diagnostic queries; execution and result interpretation belong to callers.
9
14
  *
10
- * Three variants, discriminated by `query`:
15
+ * Four variants, discriminated by `query`:
11
16
  * - a fixed transport/catalog probe,
17
+ * - row capture's readiness read over the tables of one spec,
12
18
  * - `postgresExplain`, which wraps another statement (EPIC 5),
13
19
  * - a read-only PostgreSQL workload/catalog read (EPIC 5).
14
20
  */
15
21
  export type DiagnosticStatement = {
16
22
  readonly kind: "diagnostic";
17
23
  readonly query: FixedDiagnosticQuery;
24
+ } | {
25
+ readonly kind: "diagnostic";
26
+ readonly query: "postgresCaptureReadiness";
27
+ readonly tables: readonly CaptureReadinessTable[];
18
28
  } | {
19
29
  readonly kind: "diagnostic";
20
30
  readonly query: "postgresExplain";
@@ -34,10 +44,22 @@ export declare function renderDiagnostic(params: {
34
44
  dialect: SqlDialect;
35
45
  query: FixedDiagnosticQuery;
36
46
  }): RenderedQuery;
37
- /** True for the two fixed probes; everything else is an EPIC 5 PostgreSQL diagnostic. */
47
+ /** True for the fixed probes; everything else is the capture readiness read or an EPIC 5 PostgreSQL diagnostic. */
38
48
  export declare function isFixedDiagnostic(params: {
39
49
  query: DiagnosticStatement["query"];
40
50
  }): params is {
41
51
  query: FixedDiagnosticQuery;
42
52
  };
53
+ /**
54
+ * Row capture's readiness read (PostgreSQL): the server version, and for each
55
+ * captured table what would make its `RETURNING` change a write's outcome.
56
+ * One statement with one JSON parameter, bound as text and cast by the server
57
+ * (bun:sql re-encodes a parameter it resolves as JSON); `to_regclass` resolves
58
+ * each table from the same quoted name the write statements use
59
+ * (schema-qualified when the model has a namespace, else through `search_path`).
60
+ */
61
+ export declare function renderCaptureReadiness(params: {
62
+ dialect: SqlDialect;
63
+ tables: readonly CaptureReadinessTable[];
64
+ }): RenderedQuery;
43
65
  //# sourceMappingURL=diagnostics.d.ts.map
package/dist/index.d.ts CHANGED
@@ -40,8 +40,8 @@ export type { ResultDecoder, ResultReadDecoder, ResultParameterEncoder, ResultSc
40
40
  * lives in this package; see the root sql-catalog.md for the reviewable surface.
41
41
  */
42
42
  export { compileExclusion, renderExclusionSetup, renderExclusionDdl, renderExclusionCatalog, renderExclusionProbe } from "./exclusion.ts";
43
- export type { AdvisoryLockKey, AdvisoryLockStatement, AdvisoryLocksStatement, AggFn, AggRef, AggSelect, ChainResult, ChainRowSource, ChainStatement, ChainStep, ChainStepStatement, ChainTally, ChainVeto, ColRef, ComparisonOp, DateTruncation, CountSubquery, ScalarSubquery, DeleteStatement, ExcludedRef, InnerJoin, KeyListMember, KeyPage, KeyRows, KeyTableJoin, KeyTableMember, InsertSelectStatement, InsertStatement, ReturningColumn, JsonFilterOp, LateralJoin, LeftJoin, ListSetOperation, OnConflictClause, OnConflictDoNothing, OnConflictUpdate, OrderByAggregate, OrderByColumn, OrderBySubquery, OrderTerm, RowLock, RowNumberProjection, DbNowMarker, JsonParamCell, SelectExpr, SelectStatement, SetOperation, SetOperator, SqlExpr, SqlStatement, StepColumnRef, TableRef, UpdateStatement, } from "./ast.ts";
44
- export { ADVISORY_LOCK_RESULT_COLUMN, CHAIN_VETO_PREFIX, CHAIN_VETO_TOKEN_PATTERN, SQL_DEFAULT_CELL, SQL_KEEP_CELL, chainVetoToken, dbNowMarker, excludedRef, isDbNowMarker, isExcludedRef, isJsonParamCell, isListSetOperation, isSetOperation, isSqlDefaultCell, isSqlKeepCell, isStepColumnRef, jsonParamCell, returningColumnName, stepColumnRef, } from "./ast.ts";
43
+ export type { AdvisoryLockKey, AdvisoryLockStatement, AdvisoryLocksStatement, AggFn, AggRef, AggSelect, ChainResult, ChainRowSource, ChainCapture, CaptureReturningColumn, ChainStatement, ChainStep, ChainStepStatement, ChainTally, ChainVeto, ColRef, ComparisonOp, DateTruncation, CountSubquery, ScalarSubquery, DeleteStatement, ExcludedRef, InnerJoin, KeyListMember, KeyPage, KeyRows, KeyTableJoin, KeyTableMember, InsertSelectStatement, InsertStatement, ReturningColumn, JsonFilterOp, LateralJoin, LeftJoin, ListSetOperation, OnConflictClause, OnConflictDoNothing, OnConflictUpdate, OrderByAggregate, OrderByColumn, OrderBySubquery, OrderTerm, RowLock, RowNumberProjection, DbNowMarker, JsonParamCell, SelectExpr, SelectStatement, SetOperation, SetOperator, SqlExpr, SqlStatement, StepColumnRef, TableRef, UpdateStatement, } from "./ast.ts";
44
+ export { ADVISORY_LOCK_RESULT_COLUMN, CHAIN_VETO_PREFIX, CHAIN_VETO_TOKEN_PATTERN, SQL_DEFAULT_CELL, SQL_KEEP_CELL, chainVetoToken, dbNowMarker, excludedRef, isDbNowMarker, isExcludedRef, isJsonParamCell, isListSetOperation, isSetOperation, isSqlDefaultCell, isSqlKeepCell, isStepColumnRef, isCaptureReturningColumn, jsonParamCell, returningColumnName, stepColumnRef, } from "./ast.ts";
45
45
  export type { AdvisoryLockFunctions, AdvisoryLockModeFunctions, RowLockSyntax, EmptyInsertSyntax, SqlDialect, } from "./dialect.ts";
46
46
  export { ADVISORY_KEY_BOUNDS, describeRowLock, renderAdvisoryLock, renderAdvisoryLocks, renderRowLockClause } from "./locks.ts";
47
47
  export { JSON_TRANSPORT, projectionOutputName } from "./json-transport.ts";
@@ -76,7 +76,7 @@ export type { RlsContextStatement, RlsDisableStatement, RlsDropPolicyStatement,
76
76
  export { containsPattern, endsWithPattern, escapeGlobPattern, escapeLikePattern, startsWithPattern, strictMatchCollationPrefilter, strictMatchPattern } from "./like.ts";
77
77
  export { RLS_READINESS_SQL, NATIVE_RLS_PROBE_POLICY, renderRlsReadinessQuery, renderRlsProbeTable, renderRlsPolicyPath } from "./dialects/postgres-rls-readiness.ts";
78
78
  export type { RlsReadinessQuery } from "./dialects/postgres-rls-readiness.ts";
79
- export { isFixedDiagnostic, renderDiagnostic } from "./dialects/diagnostics.ts";
79
+ export { isFixedDiagnostic, renderCaptureReadiness, renderDiagnostic } from "./dialects/diagnostics.ts";
80
80
  export { sqliteColumnClauses } from "./dialects/sqlite-table-definition.ts";
81
81
  export type { SqliteColumnClause } from "./dialects/sqlite-table-definition.ts";
82
82
  export { parsePostgresTypeName, renderPostgresTypeName, renderPostgresTypeIdentity } from "./dialects/postgres-type-identity.ts";
@@ -92,7 +92,7 @@ export { renderMigrationInventory, renderSqliteColumnInventory, renderSqliteFore
92
92
  export { renderMigrationObjectBatch, renderMigrationObjectQuery, renderCheckProbe, renderMigrationNamespace, renderMigrationServerVersion, renderMigrationDependencies, renderMysqlObjectMembers, renderMysqlObjectBatch, renderMysqlObjectMembersBatch } from "./dialects/migration-objects.ts";
93
93
  export type { MigrationObjectBatch, MysqlObjectBatch } from "./dialects/migration-objects.ts";
94
94
  export type { SqliteIntrospectionPragma } from "./dialects/migration-inventory.ts";
95
- export type { DiagnosticStatement, FixedDiagnosticQuery } from "./dialects/diagnostics.ts";
95
+ export type { CaptureReadinessTable, DiagnosticStatement, FixedDiagnosticQuery } from "./dialects/diagnostics.ts";
96
96
  export { POSTGRES_DIAGNOSTIC_SQL, POSTGRES_WORKLOAD_LIMITS, renderPostgresExplain, renderPostgresWorkload, } from "./dialects/postgres-diagnostics.ts";
97
97
  export type { PostgresExplainOptions, PostgresWorkloadQuery } from "./dialects/postgres-diagnostics.ts";
98
98
  export { renderColumnStateQuery, renderConstraintStateQuery, renderIndexStateQuery, renderMigrationSessionRead, renderMigrationSessionRestore, renderMigrationSessionSet, renderRelationStateQuery, } from "./dialects/postgres-migration.ts";
package/dist/index.js CHANGED
@@ -2894,7 +2894,10 @@ function isExcludedRef(value) {
2894
2894
  return typeof value === "object" && value !== null && value.__excluded === true;
2895
2895
  }
2896
2896
  function returningColumnName(column) {
2897
- return typeof column === "string" ? column : column.name;
2897
+ return typeof column === "string" ? column : ("capture" in column) ? column.alias : column.name;
2898
+ }
2899
+ function isCaptureReturningColumn(column) {
2900
+ return typeof column !== "string" && "capture" in column;
2898
2901
  }
2899
2902
  var ADVISORY_LOCK_RESULT_COLUMN = "acquired";
2900
2903
  function stepColumnRef(params) {
@@ -2960,6 +2963,12 @@ function renderRowLockClause(params) {
2960
2963
  hint: `${dialect.name} can express: ${Object.keys(syntax.wait).join(", ")}`
2961
2964
  });
2962
2965
  }
2966
+ if (lock.of !== undefined) {
2967
+ if (lock.of.length === 0)
2968
+ throw internalError({ message: "a row lock's `of` list cannot be empty" });
2969
+ const tables = lock.of.map((name) => dialect.quoteIdent({ name })).join(", ");
2970
+ return `${strengthText} OF ${tables}${waitText}`;
2971
+ }
2963
2972
  return `${strengthText}${waitText}`;
2964
2973
  }
2965
2974
  var ADVISORY_KEY_BOUNDS = Object.freeze({
@@ -3646,7 +3655,7 @@ function renderSqliteJsonEquality(params) {
3646
3655
  }
3647
3656
 
3648
3657
  // packages/sql/src/dialects/diagnostics.ts
3649
- import { unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
3658
+ import { assertSqlNamespace as assertSqlNamespace2, unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
3650
3659
  var QUERIES = Object.freeze({
3651
3660
  postgresJsonWire: { dialect: "postgres", text: `SELECT '{"__vibeorm":1}'::jsonb AS "probe"` },
3652
3661
  sqliteTriggers: { dialect: "sqlite", text: "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'" }
@@ -3659,7 +3668,23 @@ function renderDiagnostic(params) {
3659
3668
  return { text: query.text, values: [], returning: "none" };
3660
3669
  }
3661
3670
  function isFixedDiagnostic(params) {
3662
- return params.query === "postgresJsonWire" || params.query === "sqliteTriggers";
3671
+ return Object.hasOwn(QUERIES, params.query);
3672
+ }
3673
+ var CAPTURE_READINESS_SQL = `SELECT pg_catalog.current_setting('server_version_num') AS "serverVersionNum", t."entry" AS "entry", c.oid IS NOT NULL AS "found", EXISTS (SELECT 1 FROM pg_catalog.pg_rewrite r WHERE r.ev_class = c.oid) AS "rules", COALESCE(pg_catalog.row_security_active(c.oid), false) AS "rowSecurity", (SELECT pg_catalog.array_agg(k.name)::text FROM pg_catalog.json_array_elements_text(t."columns") AS k(name) WHERE NOT EXISTS (SELECT 1 FROM pg_catalog.pg_attribute a WHERE a.attrelid = c.oid AND a.attname = k.name AND a.attnum > 0 AND NOT a.attisdropped)) AS "missing", (SELECT pg_catalog.array_agg(k.name)::text FROM pg_catalog.json_array_elements_text(t."columns") AS k(name) JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid AND a.attname = k.name AND a.attnum > 0 AND NOT a.attisdropped WHERE NOT pg_catalog.has_column_privilege(c.oid, a.attnum, 'SELECT')) AS "unreadable" FROM pg_catalog.json_to_recordset(%TABLES%::text::json) AS t("entry" integer, "table" text, "columns" json) LEFT JOIN pg_catalog.pg_class c ON c.oid = pg_catalog.to_regclass(t."table") ORDER BY t."entry"`;
3674
+ function renderCaptureReadiness(params) {
3675
+ const { dialect, tables } = params;
3676
+ if (dialect.name !== "postgres") {
3677
+ throw unsupportedCapability24({ dialect: dialect.name, feature: "postgresCaptureReadiness", hint: "row capture reads RETURNING old/new, PostgreSQL 18 or newer" });
3678
+ }
3679
+ const entries = tables.map((entry, index) => {
3680
+ const { name, namespace } = entry.table;
3681
+ if (namespace !== undefined)
3682
+ assertSqlNamespace2({ namespace, dialect: dialect.name });
3683
+ const relation = dialect.quoteIdent({ name });
3684
+ return { entry: index, table: namespace === undefined ? relation : `${dialect.quoteIdent({ name: namespace })}.${relation}`, columns: [...entry.columns] };
3685
+ });
3686
+ const collector = new ParamCollector(dialect);
3687
+ return { text: CAPTURE_READINESS_SQL.replace("%TABLES%", collector.add(JSON.stringify(entries))), values: collector.values, returning: "none" };
3663
3688
  }
3664
3689
 
3665
3690
  // packages/sql/src/dialects/postgres-diagnostics.ts
@@ -4084,7 +4109,7 @@ function renderRlsPolicyPath(params) {
4084
4109
  }
4085
4110
 
4086
4111
  // packages/sql/src/render.ts
4087
- import { assertSqlNamespace as assertSqlNamespace2, internalError as internalError4, unsupportedCapability as unsupportedCapability28 } from "@vibeorm/schema";
4112
+ import { assertSqlNamespace as assertSqlNamespace3, internalError as internalError4, unsupportedCapability as unsupportedCapability28 } from "@vibeorm/schema";
4088
4113
 
4089
4114
  // packages/sql/src/like.ts
4090
4115
  function escapeLikePattern(params) {
@@ -4122,6 +4147,8 @@ function renderStatement(params) {
4122
4147
  case "diagnostic": {
4123
4148
  if (isFixedDiagnostic(statement))
4124
4149
  return renderDiagnostic({ dialect, query: statement.query });
4150
+ if (statement.query === "postgresCaptureReadiness")
4151
+ return renderCaptureReadiness({ dialect, tables: statement.tables });
4125
4152
  if (statement.query === "postgresExplain") {
4126
4153
  return renderPostgresExplain({
4127
4154
  dialect,
@@ -4240,7 +4267,7 @@ function renderProjection(params) {
4240
4267
  }
4241
4268
  function renderTable(dialect, table) {
4242
4269
  if (table.namespace !== undefined)
4243
- assertSqlNamespace2({ namespace: table.namespace, dialect: dialect.name });
4270
+ assertSqlNamespace3({ namespace: table.namespace, dialect: dialect.name });
4244
4271
  const relation = dialect.quoteIdent({ name: table.name });
4245
4272
  const name = table.namespace === undefined ? relation : `${dialect.quoteIdent({ name: table.namespace })}.${relation}`;
4246
4273
  if (table.alias === undefined)
@@ -4928,11 +4955,16 @@ function applyReturning(params) {
4928
4955
  return { suffix: "", status: "none" };
4929
4956
  }
4930
4957
  if (!dialect.capabilities.returning) {
4958
+ const capture = requested.find(isCaptureReturningColumn);
4959
+ if (capture !== undefined)
4960
+ renderCaptureReturning({ dialect, column: capture });
4931
4961
  return { suffix: "", status: "omitted" };
4932
4962
  }
4933
4963
  const columns = requested.map((column) => {
4934
4964
  if (typeof column === "string")
4935
4965
  return dialect.quoteIdent({ name: column });
4966
+ if ("capture" in column)
4967
+ return renderCaptureReturning({ dialect, column });
4936
4968
  if ("transform" in column) {
4937
4969
  const name = dialect.quoteIdent({ name: column.name });
4938
4970
  return `${dialect.jsonTextProjection({ expression: name })} AS ${name}`;
@@ -4942,6 +4974,17 @@ function applyReturning(params) {
4942
4974
  }).join(", ");
4943
4975
  return { suffix: ` RETURNING ${columns}`, status: "native" };
4944
4976
  }
4977
+ function renderCaptureReturning(params) {
4978
+ const { dialect, column } = params;
4979
+ if (!dialect.capabilities.rowCapture) {
4980
+ throw unsupportedCapability28({ dialect: dialect.name, feature: "row capture (RETURNING old./new.)", hint: "row capture needs PostgreSQL 18 or later; the runtime refuses $transaction({ capture }) on this dialect" });
4981
+ }
4982
+ const name = dialect.quoteIdent({ name: column.name });
4983
+ const alias = dialect.quoteIdent({ name: column.alias });
4984
+ if (column.capture === "inserted")
4985
+ return `(old.${name} IS NULL) AS ${alias}`;
4986
+ return `${textProjection({ dialect, expression: `${column.capture}.${name}` })} AS ${alias}`;
4987
+ }
4945
4988
  function renderKeyTableJoin(params) {
4946
4989
  const { dialect, join, collector } = params;
4947
4990
  const postgresMember = (member) => member.kind === "citext" || member.kind === "text" || member.kind === "integer";
@@ -5136,6 +5179,13 @@ function renderSelect(params) {
5136
5179
  text += ` OFFSET ${statement.offset}`;
5137
5180
  }
5138
5181
  if (statement.rowLock !== undefined) {
5182
+ const target = statement.table.alias ?? statement.table.name;
5183
+ if (statement.rowLock.of !== undefined && statement.rowLock.of.some((name) => name !== target)) {
5184
+ throw internalError4({ message: `a row lock's \`of\` list may name only the statement's own table "${target}"` });
5185
+ }
5186
+ if (statement.rowLock.of !== undefined && (statement.fromSelect !== undefined || statement.keyPage !== undefined)) {
5187
+ throw internalError4({ message: "a row lock's `of` list cannot name a derived table" });
5188
+ }
5139
5189
  const clause = renderRowLockClause({ dialect, lock: statement.rowLock });
5140
5190
  if (clause !== null)
5141
5191
  text += ` ${clause}`;
@@ -5451,8 +5501,18 @@ function renderChain(params) {
5451
5501
  defined.add(step.name);
5452
5502
  }
5453
5503
  const result = statement.result;
5504
+ const captures = result.capture ?? [];
5505
+ if (captures.length > 0 && !dialect.capabilities.rowCapture) {
5506
+ throw unsupportedCapability28({ dialect: dialect.name, feature: "row capture (RETURNING old./new.)", hint: "row capture needs PostgreSQL 18 or later; the runtime refuses $transaction({ capture }) on this dialect" });
5507
+ }
5508
+ for (const alias of captureAliases({ captures })) {
5509
+ if (result.columns.includes(alias) || alias === result.verdict?.alias || alias === result.tally?.alias) {
5510
+ throw internalError4({ message: `chain capture alias "${alias}" collides with a result column` });
5511
+ }
5512
+ }
5454
5513
  if (result.rowsFrom.length === 0 && result.columns.length === 0 && result.tally !== undefined && result.verdict === undefined) {
5455
- return { text: `WITH ${steps.join(", ")} SELECT ${renderTallyColumn({ dialect, result, defined, collector })}`, values: collector.values, returning: "none" };
5514
+ const tally = renderTallyColumn({ dialect, result, defined, collector });
5515
+ return { text: `WITH ${steps.join(", ")} SELECT ${[tally, ...renderChainCaptures({ dialect, captures, defined, collector })].join(", ")}`, values: collector.values, returning: "none" };
5456
5516
  }
5457
5517
  if (result.rowsFrom.length === 0 || result.columns.length === 0) {
5458
5518
  throw internalError4({ message: "a chain result needs at least one source step and one column" });
@@ -5462,9 +5522,11 @@ function renderChain(params) {
5462
5522
  throw internalError4({ message: `chain result reads undefined step "${name}"` });
5463
5523
  }
5464
5524
  const columnList = result.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
5465
- const union = result.rowsFrom.map((name) => `SELECT ${columnList} FROM ${dialect.quoteIdent({ name })}`).join(" UNION ALL ");
5525
+ const guarded = result.verdict !== undefined || result.tally !== undefined || result.veto !== undefined && result.veto.length > 0;
5526
+ const armCapture = guarded || captures.length === 0 ? "" : `, ${renderChainCaptures({ dialect, captures, defined, collector }).join(", ")}`;
5527
+ const union = result.rowsFrom.map((name) => `SELECT ${columnList}${armCapture} FROM ${dialect.quoteIdent({ name })}`).join(" UNION ALL ");
5466
5528
  let select = union;
5467
- if (result.verdict !== undefined || result.tally !== undefined || result.veto !== undefined && result.veto.length > 0) {
5529
+ if (guarded) {
5468
5530
  const extra = [];
5469
5531
  for (const alias of [result.verdict?.alias, result.tally?.alias]) {
5470
5532
  if (alias !== undefined && result.columns.includes(alias)) {
@@ -5479,6 +5541,7 @@ function renderChain(params) {
5479
5541
  if (result.tally !== undefined || result.veto !== undefined && result.veto.length > 0) {
5480
5542
  extra.push(renderTallyColumn({ dialect, result, defined, collector }));
5481
5543
  }
5544
+ extra.push(...renderChainCaptures({ dialect, captures, defined, collector }));
5482
5545
  select = `SELECT ${[...projected, ...extra].join(", ")} FROM (SELECT 1) AS ${dialect.quoteIdent({ name: CHAIN_ONE_ALIAS })}` + ` LEFT JOIN (${union}) AS ${rows} ON TRUE`;
5483
5546
  }
5484
5547
  return { text: `WITH ${steps.join(", ")} ${select}`, values: collector.values, returning: "none" };
@@ -5498,6 +5561,47 @@ function renderChainStep(params) {
5498
5561
  return renderDelete({ dialect, statement, collector }).text;
5499
5562
  }
5500
5563
  }
5564
+ function captureAliases(params) {
5565
+ const out = [];
5566
+ for (const entry of params.captures) {
5567
+ out.push(`${entry.alias}_n`);
5568
+ if (entry.inserted !== undefined)
5569
+ out.push(`${entry.alias}_i`);
5570
+ if (entry.before !== undefined && entry.before.length > 0)
5571
+ out.push(`${entry.alias}_b`);
5572
+ if (entry.after !== undefined && entry.after.length > 0)
5573
+ out.push(`${entry.alias}_a`);
5574
+ }
5575
+ return out;
5576
+ }
5577
+ function renderChainCaptures(params) {
5578
+ const { dialect, captures, defined, collector } = params;
5579
+ const quote = (name) => dialect.quoteIdent({ name });
5580
+ const out = [];
5581
+ for (const entry of captures) {
5582
+ if (!defined.has(entry.step))
5583
+ throw internalError4({ message: `chain capture reads undefined step "${entry.step}"` });
5584
+ const step = quote(entry.step);
5585
+ const marker = entry.inserted === undefined ? undefined : quote(entry.inserted);
5586
+ out.push(`(SELECT count(*) FROM ${step}) AS ${quote(`${entry.alias}_n`)}`);
5587
+ if (marker !== undefined)
5588
+ out.push(`(SELECT count(*) FROM ${step} WHERE ${marker}) AS ${quote(`${entry.alias}_i`)}`);
5589
+ const before = entry.before ?? [];
5590
+ const after = entry.after ?? [];
5591
+ if (before.length === 0 && after.length === 0)
5592
+ continue;
5593
+ if (!Number.isSafeInteger(entry.limit) || entry.limit < 1)
5594
+ throw internalError4({ message: `chain capture "${entry.alias}" needs a positive integer limit` });
5595
+ const limit = collector.add(entry.limit);
5596
+ const item = quote("__vibe_i");
5597
+ const image = (columns, filter) => `(SELECT array_agg(${item})::text FROM (SELECT DISTINCT ARRAY[${columns.map(quote).join(", ")}] AS ${item} FROM ${step}${filter} LIMIT ${limit}) AS ${quote("__vibe_d")})`;
5598
+ if (before.length > 0)
5599
+ out.push(`${image(before, marker === undefined ? "" : ` WHERE NOT ${marker}`)} AS ${quote(`${entry.alias}_b`)}`);
5600
+ if (after.length > 0)
5601
+ out.push(`${image(after, marker !== undefined && entry.insertedOnly === true ? ` WHERE ${marker}` : "")} AS ${quote(`${entry.alias}_a`)}`);
5602
+ }
5603
+ return out;
5604
+ }
5501
5605
  function renderSelectCell(params) {
5502
5606
  const { dialect, value, collector } = params;
5503
5607
  if (isJsonParamCell(value))
@@ -6298,6 +6402,7 @@ export {
6298
6402
  extractSqliteGeneratedColumn,
6299
6403
  float32Codec,
6300
6404
  getSqlDialect,
6405
+ isCaptureReturningColumn,
6301
6406
  isDbNowMarker,
6302
6407
  isExcludedRef,
6303
6408
  isFixedDiagnostic,
@@ -6340,6 +6445,7 @@ export {
6340
6445
  registerSqlHandleCheck,
6341
6446
  renderAdvisoryLock,
6342
6447
  renderAdvisoryLocks,
6448
+ renderCaptureReadiness,
6343
6449
  renderCheckProbe,
6344
6450
  renderColumnStateQuery,
6345
6451
  renderConstraintOwnedIndexDrop,
@@ -6445,4 +6551,4 @@ export {
6445
6551
  toPgArrayLiteral
6446
6552
  };
6447
6553
 
6448
- //# debugId=A6FF0AC9CFF5903864756E2164756E21
6554
+ //# debugId=98E9DEF83EE89CED64756E2164756E21