@vibeorm/sql 5.4.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
@@ -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;
@@ -1330,6 +1349,41 @@ export type ChainResult = {
1330
1349
  * it instead of counting result rows (effect `writeTally`).
1331
1350
  */
1332
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;
1333
1387
  };
1334
1388
  /**
1335
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) {
@@ -3652,7 +3655,7 @@ function renderSqliteJsonEquality(params) {
3652
3655
  }
3653
3656
 
3654
3657
  // packages/sql/src/dialects/diagnostics.ts
3655
- import { unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
3658
+ import { assertSqlNamespace as assertSqlNamespace2, unsupportedCapability as unsupportedCapability24 } from "@vibeorm/schema";
3656
3659
  var QUERIES = Object.freeze({
3657
3660
  postgresJsonWire: { dialect: "postgres", text: `SELECT '{"__vibeorm":1}'::jsonb AS "probe"` },
3658
3661
  sqliteTriggers: { dialect: "sqlite", text: "SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'" }
@@ -3665,7 +3668,23 @@ function renderDiagnostic(params) {
3665
3668
  return { text: query.text, values: [], returning: "none" };
3666
3669
  }
3667
3670
  function isFixedDiagnostic(params) {
3668
- 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" };
3669
3688
  }
3670
3689
 
3671
3690
  // packages/sql/src/dialects/postgres-diagnostics.ts
@@ -4090,7 +4109,7 @@ function renderRlsPolicyPath(params) {
4090
4109
  }
4091
4110
 
4092
4111
  // packages/sql/src/render.ts
4093
- 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";
4094
4113
 
4095
4114
  // packages/sql/src/like.ts
4096
4115
  function escapeLikePattern(params) {
@@ -4128,6 +4147,8 @@ function renderStatement(params) {
4128
4147
  case "diagnostic": {
4129
4148
  if (isFixedDiagnostic(statement))
4130
4149
  return renderDiagnostic({ dialect, query: statement.query });
4150
+ if (statement.query === "postgresCaptureReadiness")
4151
+ return renderCaptureReadiness({ dialect, tables: statement.tables });
4131
4152
  if (statement.query === "postgresExplain") {
4132
4153
  return renderPostgresExplain({
4133
4154
  dialect,
@@ -4246,7 +4267,7 @@ function renderProjection(params) {
4246
4267
  }
4247
4268
  function renderTable(dialect, table) {
4248
4269
  if (table.namespace !== undefined)
4249
- assertSqlNamespace2({ namespace: table.namespace, dialect: dialect.name });
4270
+ assertSqlNamespace3({ namespace: table.namespace, dialect: dialect.name });
4250
4271
  const relation = dialect.quoteIdent({ name: table.name });
4251
4272
  const name = table.namespace === undefined ? relation : `${dialect.quoteIdent({ name: table.namespace })}.${relation}`;
4252
4273
  if (table.alias === undefined)
@@ -4934,11 +4955,16 @@ function applyReturning(params) {
4934
4955
  return { suffix: "", status: "none" };
4935
4956
  }
4936
4957
  if (!dialect.capabilities.returning) {
4958
+ const capture = requested.find(isCaptureReturningColumn);
4959
+ if (capture !== undefined)
4960
+ renderCaptureReturning({ dialect, column: capture });
4937
4961
  return { suffix: "", status: "omitted" };
4938
4962
  }
4939
4963
  const columns = requested.map((column) => {
4940
4964
  if (typeof column === "string")
4941
4965
  return dialect.quoteIdent({ name: column });
4966
+ if ("capture" in column)
4967
+ return renderCaptureReturning({ dialect, column });
4942
4968
  if ("transform" in column) {
4943
4969
  const name = dialect.quoteIdent({ name: column.name });
4944
4970
  return `${dialect.jsonTextProjection({ expression: name })} AS ${name}`;
@@ -4948,6 +4974,17 @@ function applyReturning(params) {
4948
4974
  }).join(", ");
4949
4975
  return { suffix: ` RETURNING ${columns}`, status: "native" };
4950
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
+ }
4951
4988
  function renderKeyTableJoin(params) {
4952
4989
  const { dialect, join, collector } = params;
4953
4990
  const postgresMember = (member) => member.kind === "citext" || member.kind === "text" || member.kind === "integer";
@@ -5464,8 +5501,18 @@ function renderChain(params) {
5464
5501
  defined.add(step.name);
5465
5502
  }
5466
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
+ }
5467
5513
  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" };
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" };
5469
5516
  }
5470
5517
  if (result.rowsFrom.length === 0 || result.columns.length === 0) {
5471
5518
  throw internalError4({ message: "a chain result needs at least one source step and one column" });
@@ -5475,9 +5522,11 @@ function renderChain(params) {
5475
5522
  throw internalError4({ message: `chain result reads undefined step "${name}"` });
5476
5523
  }
5477
5524
  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 ");
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 ");
5479
5528
  let select = union;
5480
- if (result.verdict !== undefined || result.tally !== undefined || result.veto !== undefined && result.veto.length > 0) {
5529
+ if (guarded) {
5481
5530
  const extra = [];
5482
5531
  for (const alias of [result.verdict?.alias, result.tally?.alias]) {
5483
5532
  if (alias !== undefined && result.columns.includes(alias)) {
@@ -5492,6 +5541,7 @@ function renderChain(params) {
5492
5541
  if (result.tally !== undefined || result.veto !== undefined && result.veto.length > 0) {
5493
5542
  extra.push(renderTallyColumn({ dialect, result, defined, collector }));
5494
5543
  }
5544
+ extra.push(...renderChainCaptures({ dialect, captures, defined, collector }));
5495
5545
  select = `SELECT ${[...projected, ...extra].join(", ")} FROM (SELECT 1) AS ${dialect.quoteIdent({ name: CHAIN_ONE_ALIAS })}` + ` LEFT JOIN (${union}) AS ${rows} ON TRUE`;
5496
5546
  }
5497
5547
  return { text: `WITH ${steps.join(", ")} ${select}`, values: collector.values, returning: "none" };
@@ -5511,6 +5561,47 @@ function renderChainStep(params) {
5511
5561
  return renderDelete({ dialect, statement, collector }).text;
5512
5562
  }
5513
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
+ }
5514
5605
  function renderSelectCell(params) {
5515
5606
  const { dialect, value, collector } = params;
5516
5607
  if (isJsonParamCell(value))
@@ -6311,6 +6402,7 @@ export {
6311
6402
  extractSqliteGeneratedColumn,
6312
6403
  float32Codec,
6313
6404
  getSqlDialect,
6405
+ isCaptureReturningColumn,
6314
6406
  isDbNowMarker,
6315
6407
  isExcludedRef,
6316
6408
  isFixedDiagnostic,
@@ -6353,6 +6445,7 @@ export {
6353
6445
  registerSqlHandleCheck,
6354
6446
  renderAdvisoryLock,
6355
6447
  renderAdvisoryLocks,
6448
+ renderCaptureReadiness,
6356
6449
  renderCheckProbe,
6357
6450
  renderColumnStateQuery,
6358
6451
  renderConstraintOwnedIndexDrop,
@@ -6458,4 +6551,4 @@ export {
6458
6551
  toPgArrayLiteral
6459
6552
  };
6460
6553
 
6461
- //# debugId=11C29D1889ACA76B64756E2164756E21
6554
+ //# debugId=98E9DEF83EE89CED64756E2164756E21