@vibeorm/sql 5.4.0 → 5.6.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
@@ -971,9 +971,28 @@ export type ReturningColumn = string | {
971
971
  } | {
972
972
  readonly name: string;
973
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;
974
991
  };
975
992
  /** The column name a {@link ReturningColumn} exposes in the result row. */
976
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;
977
996
  export type InsertStatement = {
978
997
  readonly kind: "insert";
979
998
  readonly table: TableRef;
@@ -1084,6 +1103,14 @@ export type OnConflictClause = OnConflictUpdate | OnConflictDoNothing;
1084
1103
  export type UpdateStatement = {
1085
1104
  readonly kind: "update";
1086
1105
  readonly table: TableRef;
1106
+ /** The target's alias; required with {@link UpdateStatement.from}, where two relations are in scope. */
1107
+ readonly alias?: string;
1108
+ /**
1109
+ * A joined source: `UPDATE … FROM` on PostgreSQL/SQLite (its `on` AND-ed
1110
+ * before `where`), `UPDATE … JOIN … ON` on MySQL. A
1111
+ * {@link SourceColumnRef} set value reads its columns.
1112
+ */
1113
+ readonly from?: UpdateSource;
1087
1114
  /**
1088
1115
  * Column → parameter value, or an atomic {@link SetOperation}. As a
1089
1116
  * {@link ChainStep} only (statement-maxx lane C3), a value may be a
@@ -1208,6 +1235,23 @@ export declare function stepColumnRef(params: {
1208
1235
  step: string;
1209
1236
  column: string;
1210
1237
  }): StepColumnRef;
1238
+ /** The joined relation of an {@link UpdateStatement}: its table, alias and join condition. */
1239
+ export type UpdateSource = {
1240
+ readonly table: TableRef;
1241
+ readonly alias: string;
1242
+ readonly on: SqlExpr;
1243
+ };
1244
+ /** An UPDATE set value read from the joined source's `column` (`UPDATE … FROM`). */
1245
+ export type SourceColumnRef = {
1246
+ readonly __sourceColumn: true;
1247
+ readonly column: string;
1248
+ };
1249
+ /** Read `column` of an {@link UpdateStatement}'s joined source as a set value. */
1250
+ export declare function sourceColumnRef(params: {
1251
+ column: string;
1252
+ }): SourceColumnRef;
1253
+ /** Is this UPDATE set value a {@link SourceColumnRef}? */
1254
+ export declare function isSourceColumnRef(value: unknown): value is SourceColumnRef;
1211
1255
  /** Is this INSERT … SELECT cell a {@link StepColumnRef}? */
1212
1256
  export declare function isStepColumnRef(value: unknown): value is StepColumnRef;
1213
1257
  /**
@@ -1330,6 +1374,41 @@ export type ChainResult = {
1330
1374
  * it instead of counting result rows (effect `writeTally`).
1331
1375
  */
1332
1376
  readonly tally?: ChainTally;
1377
+ /**
1378
+ * Row capture ({@link ChainCapture}): per captured data-modifying step, its
1379
+ * row count and its distinct before and after images, on every result row
1380
+ * (uncorrelated scalar subqueries, evaluated once). Needs the dialect's
1381
+ * `rowCapture` capability. Rendered after every other result column, so
1382
+ * their bound limits come last.
1383
+ */
1384
+ readonly capture?: readonly ChainCapture[];
1385
+ };
1386
+ /**
1387
+ * One captured step of a chain ({@link ChainResult.capture}). Result columns,
1388
+ * `<alias>` being this entry's alias:
1389
+ *
1390
+ * - `<alias>_n`: `(SELECT count(*) FROM "step")`;
1391
+ * - `<alias>_i` (with `inserted`): the rows whose arm marker is true;
1392
+ * - `<alias>_b` / `<alias>_a` (with `before` / `after`): `(SELECT
1393
+ * array_agg("__vibe_i")::text FROM (SELECT DISTINCT ARRAY["c0", "c1"] AS
1394
+ * "__vibe_i" FROM "step" LIMIT $n) AS "__vibe_d")` — a two-dimensional text
1395
+ * array literal of at most `limit` distinct images, NULL when the step
1396
+ * returned no row. A before image skips the rows whose arm marker is true.
1397
+ *
1398
+ * `before` / `after` name columns the step RETURNS (its
1399
+ * {@link CaptureReturningColumn} aliases), in image order. `limit` is bound once.
1400
+ */
1401
+ export type ChainCapture = {
1402
+ readonly step: string;
1403
+ readonly alias: string;
1404
+ /** The step's `"inserted"` arm-marker column, for an `INSERT … ON CONFLICT DO UPDATE` step. */
1405
+ readonly inserted?: string;
1406
+ /** With `inserted`: images of inserted rows only — `after` reads the rows whose marker is true. */
1407
+ readonly insertedOnly?: true;
1408
+ readonly before?: readonly string[];
1409
+ readonly after?: readonly string[];
1410
+ /** The most distinct images one image column returns: a positive safe integer, bound as a parameter. */
1411
+ readonly limit: number;
1333
1412
  };
1334
1413
  /**
1335
1414
  * 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, SourceColumnRef, UpdateSource, 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, isSourceColumnRef, sourceColumnRef, 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,12 +2894,24 @@ 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) {
2901
2904
  return { __stepColumn: true, step: params.step, column: params.column };
2902
2905
  }
2906
+ function sourceColumnRef(params) {
2907
+ return Object.freeze({ __sourceColumn: true, column: params.column });
2908
+ }
2909
+ function isSourceColumnRef(value) {
2910
+ if (typeof value !== "object" || value === null)
2911
+ return false;
2912
+ const candidate = value;
2913
+ return candidate.__sourceColumn === true && typeof candidate.column === "string" && Object.keys(value).length === 2;
2914
+ }
2903
2915
  function isStepColumnRef(value) {
2904
2916
  return typeof value === "object" && value !== null && value.__stepColumn === true;
2905
2917
  }
@@ -3652,7 +3664,7 @@ function renderSqliteJsonEquality(params) {
3652
3664
  }
3653
3665
 
3654
3666
  // packages/sql/src/dialects/diagnostics.ts
3655
- import { unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
3667
+ import { assertSqlNamespace as assertSqlNamespace2, unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
3656
3668
  var QUERIES = Object.freeze({
3657
3669
  postgresJsonWire: { dialect: "postgres", text: `SELECT '{"__vibeorm":1}'::jsonb AS "probe"` },
3658
3670
  sqliteTriggers: { dialect: "sqlite", text: "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'" }
@@ -3665,7 +3677,23 @@ function renderDiagnostic(params) {
3665
3677
  return { text: query.text, values: [], returning: "none" };
3666
3678
  }
3667
3679
  function isFixedDiagnostic(params) {
3668
- return params.query === "postgresJsonWire" || params.query === "sqliteTriggers";
3680
+ return Object.hasOwn(QUERIES, params.query);
3681
+ }
3682
+ 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"`;
3683
+ function renderCaptureReadiness(params) {
3684
+ const { dialect, tables } = params;
3685
+ if (dialect.name !== "postgres") {
3686
+ throw unsupportedCapability24({ dialect: dialect.name, feature: "postgresCaptureReadiness", hint: "row capture reads RETURNING old/new, PostgreSQL 18 or newer" });
3687
+ }
3688
+ const entries = tables.map((entry, index) => {
3689
+ const { name, namespace } = entry.table;
3690
+ if (namespace !== undefined)
3691
+ assertSqlNamespace2({ namespace, dialect: dialect.name });
3692
+ const relation = dialect.quoteIdent({ name });
3693
+ return { entry: index, table: namespace === undefined ? relation : `${dialect.quoteIdent({ name: namespace })}.${relation}`, columns: [...entry.columns] };
3694
+ });
3695
+ const collector = new ParamCollector(dialect);
3696
+ return { text: CAPTURE_READINESS_SQL.replace("%TABLES%", collector.add(JSON.stringify(entries))), values: collector.values, returning: "none" };
3669
3697
  }
3670
3698
 
3671
3699
  // packages/sql/src/dialects/postgres-diagnostics.ts
@@ -4090,7 +4118,7 @@ function renderRlsPolicyPath(params) {
4090
4118
  }
4091
4119
 
4092
4120
  // packages/sql/src/render.ts
4093
- import { assertSqlNamespace as assertSqlNamespace2, internalError as internalError4, unsupportedCapability as unsupportedCapability28 } from "@vibeorm/schema";
4121
+ import { assertSqlNamespace as assertSqlNamespace3, internalError as internalError4, unsupportedCapability as unsupportedCapability28 } from "@vibeorm/schema";
4094
4122
 
4095
4123
  // packages/sql/src/like.ts
4096
4124
  function escapeLikePattern(params) {
@@ -4128,6 +4156,8 @@ function renderStatement(params) {
4128
4156
  case "diagnostic": {
4129
4157
  if (isFixedDiagnostic(statement))
4130
4158
  return renderDiagnostic({ dialect, query: statement.query });
4159
+ if (statement.query === "postgresCaptureReadiness")
4160
+ return renderCaptureReadiness({ dialect, tables: statement.tables });
4131
4161
  if (statement.query === "postgresExplain") {
4132
4162
  return renderPostgresExplain({
4133
4163
  dialect,
@@ -4246,7 +4276,7 @@ function renderProjection(params) {
4246
4276
  }
4247
4277
  function renderTable(dialect, table) {
4248
4278
  if (table.namespace !== undefined)
4249
- assertSqlNamespace2({ namespace: table.namespace, dialect: dialect.name });
4279
+ assertSqlNamespace3({ namespace: table.namespace, dialect: dialect.name });
4250
4280
  const relation = dialect.quoteIdent({ name: table.name });
4251
4281
  const name = table.namespace === undefined ? relation : `${dialect.quoteIdent({ name: table.namespace })}.${relation}`;
4252
4282
  if (table.alias === undefined)
@@ -4934,20 +4964,37 @@ function applyReturning(params) {
4934
4964
  return { suffix: "", status: "none" };
4935
4965
  }
4936
4966
  if (!dialect.capabilities.returning) {
4967
+ const capture = requested.find(isCaptureReturningColumn);
4968
+ if (capture !== undefined)
4969
+ renderCaptureReturning({ dialect, column: capture });
4937
4970
  return { suffix: "", status: "omitted" };
4938
4971
  }
4972
+ const read = (name) => params.qualify === undefined ? name : `${params.qualify}.${name}`;
4939
4973
  const columns = requested.map((column) => {
4940
4974
  if (typeof column === "string")
4941
- return dialect.quoteIdent({ name: column });
4975
+ return read(dialect.quoteIdent({ name: column }));
4976
+ if ("capture" in column)
4977
+ return renderCaptureReturning({ dialect, column });
4942
4978
  if ("transform" in column) {
4943
4979
  const name = dialect.quoteIdent({ name: column.name });
4944
- return `${dialect.jsonTextProjection({ expression: name })} AS ${name}`;
4980
+ return `${dialect.jsonTextProjection({ expression: read(name) })} AS ${name}`;
4945
4981
  }
4946
4982
  const name = dialect.quoteIdent({ name: column.name });
4947
- return `${textProjection({ dialect, expression: name })} AS ${name}`;
4983
+ return `${textProjection({ dialect, expression: read(name) })} AS ${name}`;
4948
4984
  }).join(", ");
4949
4985
  return { suffix: ` RETURNING ${columns}`, status: "native" };
4950
4986
  }
4987
+ function renderCaptureReturning(params) {
4988
+ const { dialect, column } = params;
4989
+ if (!dialect.capabilities.rowCapture) {
4990
+ 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" });
4991
+ }
4992
+ const name = dialect.quoteIdent({ name: column.name });
4993
+ const alias = dialect.quoteIdent({ name: column.alias });
4994
+ if (column.capture === "inserted")
4995
+ return `(old.${name} IS NULL) AS ${alias}`;
4996
+ return `${textProjection({ dialect, expression: `${column.capture}.${name}` })} AS ${alias}`;
4997
+ }
4951
4998
  function renderKeyTableJoin(params) {
4952
4999
  const { dialect, join, collector } = params;
4953
5000
  const postgresMember = (member) => member.kind === "citext" || member.kind === "text" || member.kind === "integer";
@@ -5367,6 +5414,18 @@ function renderUpdate(params) {
5367
5414
  if (assignments.length === 0) {
5368
5415
  throw internalError4({ message: "update must set at least one column" });
5369
5416
  }
5417
+ const source = statement.from;
5418
+ if (source !== undefined) {
5419
+ if (statement.alias === undefined || statement.alias === source.alias)
5420
+ throw internalError4({ message: "UPDATE \u2026 FROM needs distinct target and source aliases" });
5421
+ const target = dialect.quoteIdent({ name: statement.alias });
5422
+ const joined = dialect.quoteIdent({ name: source.alias });
5423
+ const assign = () => renderAssignments({ dialect, entries: assignments, collector, selfRef: target, source: joined, ...dialect.name === "mysql" ? { qualifyWrite: target } : {}, ...params.earlier === undefined ? {} : { steps: params.earlier } });
5424
+ const where = () => statement.where === undefined ? "" : ` AND ${renderExpr({ dialect, expr: statement.where, collector })}`;
5425
+ const text = dialect.name === "mysql" ? `UPDATE ${renderTable(dialect, statement.table)} AS ${target} JOIN ${renderTable(dialect, source.table)} AS ${joined} ON ${renderExpr({ dialect, expr: source.on, collector })} SET ${assign()}${statement.where === undefined ? "" : ` WHERE ${renderExpr({ dialect, expr: statement.where, collector })}`}` : `UPDATE ${renderTable(dialect, statement.table)} AS ${target} SET ${assign()} FROM ${renderTable(dialect, source.table)} AS ${joined} WHERE ${renderExpr({ dialect, expr: source.on, collector })}${where()}`;
5426
+ const returning = applyReturning({ dialect, requested: statement.returning, ...dialect.name === "postgres" ? { qualify: target } : {} });
5427
+ return { text: text + returning.suffix, values: collector.values, returning: returning.status };
5428
+ }
5370
5429
  const setSql = renderAssignments({ dialect, entries: assignments, collector, ...params.earlier === undefined ? {} : { steps: params.earlier } });
5371
5430
  let text = `UPDATE ${renderTable(dialect, statement.table)} SET ${setSql}`;
5372
5431
  if (statement.where !== undefined) {
@@ -5386,6 +5445,11 @@ function renderAssignments(params) {
5386
5445
  return entries.map(([name, value]) => {
5387
5446
  const column = dialect.quoteIdent({ name });
5388
5447
  const read = selfRef === undefined ? column : `${selfRef}.${column}`;
5448
+ if (isSourceColumnRef(value)) {
5449
+ if (params.source === undefined)
5450
+ throw internalError4({ message: `an UPDATE set value reads the joined source's column ${value.column}, but the statement joins no source` });
5451
+ return `${column} = ${params.source}.${dialect.quoteIdent({ name: value.column })}`;
5452
+ }
5389
5453
  if (isStepColumnRef(value)) {
5390
5454
  if (params.steps === undefined || !params.steps.has(value.step)) {
5391
5455
  throw internalError4({ message: `an UPDATE set value reads step "${value.step}", which is not an earlier chain step` });
@@ -5429,7 +5493,7 @@ function renderAssignments(params) {
5429
5493
  return `${column} = ${read} || ${collector.add(value.value)}`;
5430
5494
  }
5431
5495
  return `${column} = ${collector.add(value)}`;
5432
- }).join(", ");
5496
+ }).map((assignment) => params.qualifyWrite === undefined ? assignment : `${params.qualifyWrite}.${assignment}`).join(", ");
5433
5497
  }
5434
5498
  function renderDelete(params) {
5435
5499
  const { dialect, statement, collector } = params;
@@ -5464,8 +5528,18 @@ function renderChain(params) {
5464
5528
  defined.add(step.name);
5465
5529
  }
5466
5530
  const result = statement.result;
5531
+ const captures = result.capture ?? [];
5532
+ if (captures.length > 0 && !dialect.capabilities.rowCapture) {
5533
+ 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" });
5534
+ }
5535
+ for (const alias of captureAliases({ captures })) {
5536
+ if (result.columns.includes(alias) || alias === result.verdict?.alias || alias === result.tally?.alias) {
5537
+ throw internalError4({ message: `chain capture alias "${alias}" collides with a result column` });
5538
+ }
5539
+ }
5467
5540
  if (result.rowsFrom.length === 0 && result.columns.length === 0 && result.tally !== undefined && result.verdict === undefined) {
5468
- return { text: `WITH ${steps.join(", ")} SELECT ${renderTallyColumn({ dialect, result, defined, collector })}`, values: collector.values, returning: "none" };
5541
+ const tally = renderTallyColumn({ dialect, result, defined, collector });
5542
+ return { text: `WITH ${steps.join(", ")} SELECT ${[tally, ...renderChainCaptures({ dialect, captures, defined, collector })].join(", ")}`, values: collector.values, returning: "none" };
5469
5543
  }
5470
5544
  if (result.rowsFrom.length === 0 || result.columns.length === 0) {
5471
5545
  throw internalError4({ message: "a chain result needs at least one source step and one column" });
@@ -5475,9 +5549,11 @@ function renderChain(params) {
5475
5549
  throw internalError4({ message: `chain result reads undefined step "${name}"` });
5476
5550
  }
5477
5551
  const columnList = result.columns.map((name) => dialect.quoteIdent({ name })).join(", ");
5478
- const union = result.rowsFrom.map((name) => `SELECT ${columnList} FROM ${dialect.quoteIdent({ name })}`).join(" UNION ALL ");
5552
+ const guarded = result.verdict !== undefined || result.tally !== undefined || result.veto !== undefined && result.veto.length > 0;
5553
+ const armCapture = guarded || captures.length === 0 ? "" : `, ${renderChainCaptures({ dialect, captures, defined, collector }).join(", ")}`;
5554
+ const union = result.rowsFrom.map((name) => `SELECT ${columnList}${armCapture} FROM ${dialect.quoteIdent({ name })}`).join(" UNION ALL ");
5479
5555
  let select = union;
5480
- if (result.verdict !== undefined || result.tally !== undefined || result.veto !== undefined && result.veto.length > 0) {
5556
+ if (guarded) {
5481
5557
  const extra = [];
5482
5558
  for (const alias of [result.verdict?.alias, result.tally?.alias]) {
5483
5559
  if (alias !== undefined && result.columns.includes(alias)) {
@@ -5492,6 +5568,7 @@ function renderChain(params) {
5492
5568
  if (result.tally !== undefined || result.veto !== undefined && result.veto.length > 0) {
5493
5569
  extra.push(renderTallyColumn({ dialect, result, defined, collector }));
5494
5570
  }
5571
+ extra.push(...renderChainCaptures({ dialect, captures, defined, collector }));
5495
5572
  select = `SELECT ${[...projected, ...extra].join(", ")} FROM (SELECT 1) AS ${dialect.quoteIdent({ name: CHAIN_ONE_ALIAS })}` + ` LEFT JOIN (${union}) AS ${rows} ON TRUE`;
5496
5573
  }
5497
5574
  return { text: `WITH ${steps.join(", ")} ${select}`, values: collector.values, returning: "none" };
@@ -5511,6 +5588,47 @@ function renderChainStep(params) {
5511
5588
  return renderDelete({ dialect, statement, collector }).text;
5512
5589
  }
5513
5590
  }
5591
+ function captureAliases(params) {
5592
+ const out = [];
5593
+ for (const entry of params.captures) {
5594
+ out.push(`${entry.alias}_n`);
5595
+ if (entry.inserted !== undefined)
5596
+ out.push(`${entry.alias}_i`);
5597
+ if (entry.before !== undefined && entry.before.length > 0)
5598
+ out.push(`${entry.alias}_b`);
5599
+ if (entry.after !== undefined && entry.after.length > 0)
5600
+ out.push(`${entry.alias}_a`);
5601
+ }
5602
+ return out;
5603
+ }
5604
+ function renderChainCaptures(params) {
5605
+ const { dialect, captures, defined, collector } = params;
5606
+ const quote = (name) => dialect.quoteIdent({ name });
5607
+ const out = [];
5608
+ for (const entry of captures) {
5609
+ if (!defined.has(entry.step))
5610
+ throw internalError4({ message: `chain capture reads undefined step "${entry.step}"` });
5611
+ const step = quote(entry.step);
5612
+ const marker = entry.inserted === undefined ? undefined : quote(entry.inserted);
5613
+ out.push(`(SELECT count(*) FROM ${step}) AS ${quote(`${entry.alias}_n`)}`);
5614
+ if (marker !== undefined)
5615
+ out.push(`(SELECT count(*) FROM ${step} WHERE ${marker}) AS ${quote(`${entry.alias}_i`)}`);
5616
+ const before = entry.before ?? [];
5617
+ const after = entry.after ?? [];
5618
+ if (before.length === 0 && after.length === 0)
5619
+ continue;
5620
+ if (!Number.isSafeInteger(entry.limit) || entry.limit < 1)
5621
+ throw internalError4({ message: `chain capture "${entry.alias}" needs a positive integer limit` });
5622
+ const limit = collector.add(entry.limit);
5623
+ const item = quote("__vibe_i");
5624
+ 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")})`;
5625
+ if (before.length > 0)
5626
+ out.push(`${image(before, marker === undefined ? "" : ` WHERE NOT ${marker}`)} AS ${quote(`${entry.alias}_b`)}`);
5627
+ if (after.length > 0)
5628
+ out.push(`${image(after, marker !== undefined && entry.insertedOnly === true ? ` WHERE ${marker}` : "")} AS ${quote(`${entry.alias}_a`)}`);
5629
+ }
5630
+ return out;
5631
+ }
5514
5632
  function renderSelectCell(params) {
5515
5633
  const { dialect, value, collector } = params;
5516
5634
  if (isJsonParamCell(value))
@@ -6311,6 +6429,7 @@ export {
6311
6429
  extractSqliteGeneratedColumn,
6312
6430
  float32Codec,
6313
6431
  getSqlDialect,
6432
+ isCaptureReturningColumn,
6314
6433
  isDbNowMarker,
6315
6434
  isExcludedRef,
6316
6435
  isFixedDiagnostic,
@@ -6321,6 +6440,7 @@ export {
6321
6440
  isReusableSqlCommand,
6322
6441
  isReusableSqlQuery,
6323
6442
  isSetOperation,
6443
+ isSourceColumnRef,
6324
6444
  isSqlBindingCollector,
6325
6445
  isSqlCommand,
6326
6446
  isSqlDefaultCell,
@@ -6353,6 +6473,7 @@ export {
6353
6473
  registerSqlHandleCheck,
6354
6474
  renderAdvisoryLock,
6355
6475
  renderAdvisoryLocks,
6476
+ renderCaptureReadiness,
6356
6477
  renderCheckProbe,
6357
6478
  renderColumnStateQuery,
6358
6479
  renderConstraintOwnedIndexDrop,
@@ -6439,6 +6560,7 @@ export {
6439
6560
  sealSqlInputScope,
6440
6561
  shortestFloat32,
6441
6562
  snapshotSqlValue,
6563
+ sourceColumnRef,
6442
6564
  sql,
6443
6565
  sqlInputContract,
6444
6566
  sqlParamContract,
@@ -6458,4 +6580,4 @@ export {
6458
6580
  toPgArrayLiteral
6459
6581
  };
6460
6582
 
6461
- //# debugId=11C29D1889ACA76B64756E2164756E21
6583
+ //# debugId=FBE116988ABE557464756E2164756E21