@happyvertical/smrt-core 0.44.1 → 0.45.1
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/agents/change-feed.md +3 -2
- package/dist/browser.js +2 -1
- package/dist/change-feed.d.ts +60 -1
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +464 -27
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +13 -11
- package/dist/change-signals.js.map +1 -1
- package/dist/generators/cli.d.ts.map +1 -1
- package/dist/generators/cli.js +10 -18
- package/dist/generators/cli.js.map +1 -1
- package/dist/generators/custom-action.d.ts +216 -0
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +256 -1
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -2
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +14 -39
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +2 -0
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +2 -8
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/generators.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +283 -35
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/registry/framework-base-classes.d.ts +10 -0
- package/dist/registry/framework-base-classes.d.ts.map +1 -0
- package/dist/registry/framework-base-classes.js +92 -0
- package/dist/registry/framework-base-classes.js.map +1 -0
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +2 -0
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +27 -6
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -1
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +46 -16
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +198 -45
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/dist/system/bootstrap.d.ts.map +1 -1
- package/dist/system/bootstrap.js +2 -1
- package/dist/system/bootstrap.js.map +1 -1
- package/dist/system/schema.d.ts +75 -2
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +326 -4
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +10 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +52 -23
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/resources-route.d.ts +18 -0
- package/dist/vite-plugin/resources-route.d.ts.map +1 -0
- package/dist/vite-plugin/resources-route.js +154 -0
- package/dist/vite-plugin/resources-route.js.map +1 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts +43 -7
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +90 -17
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +2 -0
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +28 -5
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +4 -4
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-core",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.45.1",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
10
|
+
"manifest": "50129d9675f2331007fa9c64842f57e0ab731e27e6854398fe3aa8c85acc503e",
|
|
11
|
+
"packageJson": "35189a41b3ff5b1f5918b91fe4bcf1490fe47f35f1b166509f9a762dea5e6df7",
|
|
12
12
|
"agents": "72902bcfc539f0644a0b7f9dec17d3bb72194a753e569a5a6f63f85e52fb1810",
|
|
13
|
-
"moduleDoc:agents/change-feed.md": "
|
|
13
|
+
"moduleDoc:agents/change-feed.md": "c5921f2536c5f092690cc9ce5dcdf906376a34413d3c10613be6287c11f87f86",
|
|
14
14
|
"moduleDoc:agents/change-signals.md": "d9cb6a5541728ffea46607a6b1d4fa61d4621849f2b4ea86a0645fbb0af892e9",
|
|
15
15
|
"moduleDoc:agents/generators.md": "1c0243c353204b40ed4d5ad688cf812d8eb7e2bb45ffce7e3ce8dca894917b0a",
|
|
16
16
|
"moduleDoc:agents/schema-paths.md": "561e01104eda21dd5c2c6f6d290bf722a7478c12800136ce61bff8193c8bbc2f",
|
|
@@ -984,7 +984,7 @@
|
|
|
984
984
|
{
|
|
985
985
|
"path": "agents/change-feed.md",
|
|
986
986
|
"module": "change-feed",
|
|
987
|
-
"content": "# smrt-core/change feed\n\nModule semantics for `src/change-feed.ts`. Package orientation, the cross-module\ninvariants, and the traps that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\n## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. On PostgreSQL, `_smrt_append_change` catches the INSERT in an exception subtransaction and returns SQLSTATE as data, so swallowing/retrying a best-effort failure cannot leave a caller-managed transaction aborted with `25P02` (#2026); the feed row still commits or rolls back with the caller transaction. Raw-handle/read initialization checks for both the table and helper before issuing any DDL; a cold schema/helper install acquires the same transaction-scoped `('smrt', 'system-tables')` advisory lock as bootstrap before its first DDL and rechecks inside one server-side statement, while schema migration/bootstrap remains the authoritative replace path. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired?, resyncCursor? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes`, an unadvanced `cursor`, and `resyncCursor` set to the current horizon so clients can full-refetch then resume incrementally; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).\n- `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table, replica-stable
|
|
987
|
+
"content": "# smrt-core/change feed\n\nModule semantics for `src/change-feed.ts`. Package orientation, the cross-module\ninvariants, and the traps that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\n## Change Feed (#1758)\n\nAdapter-agnostic change-observation spine (`src/change-feed.ts`) — the server half of the client/mobile sync contract (PRD #1755):\n\n- `_smrt_changes` system table: one append per framework save/delete via a GlobalInterceptors writer registered at framework init. Deletes are tombstones (`operation: 'delete'`). `_smrt_*` tables are skipped. Feed-append failures log and never fail the user's write. On PostgreSQL, `_smrt_append_change` catches the INSERT in an exception subtransaction and returns SQLSTATE as data, so swallowing/retrying a best-effort failure cannot leave a caller-managed transaction aborted with `25P02` (#2026); the feed row still commits or rolls back with the caller transaction. Raw-handle/read initialization checks for both the table and helper before issuing any DDL; a cold schema/helper install acquires the same transaction-scoped `('smrt', 'system-tables')` advisory lock as bootstrap before its first DDL and rechecks inside one server-side statement, while schema migration/bootstrap remains the authoritative replace path. No dirty-check: a field-unchanged `.save()` appends a spurious `update` entry (diff-aware paths like `getOrUpsert`/sync-apply short-circuit before `save()` and append nothing); subscribers must tolerate spurious entries — they are convergent.\n- Sequences: allocated as `MAX(seq)+1` inside the INSERT with conflict retry — committed rows stay contiguous, so commit order == seq order on SQLite/Postgres/DuckDB (deliberately NOT identity/serial: those allocate before commit and break the cursor guarantee under concurrent writers).\n- Staged appends (PostgreSQL, #2649): `MAX+1` costs a *wait* — the loser of a primary-key race waits for the winner's whole transaction — so an append inside a caller transaction that already wrote rows would let a long write transaction and an ordinary concurrent request form a real lock cycle (`40P01`, found downstream in willgriffin/willgriffin.dev#457). `_smrt_append_change` therefore checks `pg_current_xact_id_if_assigned()` (PostgreSQL 13+): when the caller already has a transaction id it stages the entry in `_smrt_changes_pending` (identity key, conflicts with nothing, never waits, still rolled back with the caller) and `appendChange()` returns **`null`** instead of a sequence. `_smrt_drain_changes()` moves *committed* staged rows into `_smrt_changes` with contiguous `MAX(seq)+row_number()` sequences under a **try-only** advisory lock, so the drain never waits either. Draining is driven from JavaScript, never inside the append helper: an entry sequenced invisibly server-side would get no live `_events` signal while the append's own signal carried a higher sequence, and a subscriber resuming from that `Last-Event-ID` would skip it permanently. `drainChangeFeed(db)` is called best-effort by `getChangesSince()`, by `pruneChangeFeed()`, and by the append path itself (throttled to one drain per 250 ms per database, bypassed immediately after this process staged an append). The append path issues ONE drain statement (a single bounded pass) and nothing else — the caller may own the surrounding transaction, and a second statement there could fail and abort it behind the feed's own error-swallowing (#2026); the helper's whole body, preflight included, sits inside its exception boundary for the same reason. It still settles that drain's signals in order without spending a statement: a drain that allocated anything assigns a transaction id, so an append that then takes the DIRECT path proves the drain committed, and its signals publish ahead of the append's own; a deferred append proves nothing and queues them. Signals for drained entries publish only after a follow-up probe proves the drain committed, so an uncommitted drain can never advertise a sequence a rollback releases for reuse. `getChangesSince()` additionally holds back everything the handle's own drains allocated while a transaction id is still assigned at read time (a per-handle watermark, so later reads in the same transaction keep holding back), and `bootstrapSystemTables()` refreshes the helpers before its schema-version fast return so an already-stamped database does not keep the deadlocking function. Consequences: the cursor guarantee is unchanged (one `MAX+1` writer at a time, only over committed work), but a staged entry becomes visible one drain after its transaction commits, its log position is drain order rather than statement order, and it publishes its live `_events` signal at drain time (post-commit — the old pre-commit signal could describe a rolled-back write). SQLite/DuckDB keep the direct insert unchanged. Residual: an append issued as a transaction's *first* statement still allocates inline — it holds no row locks then, so it cannot close a cycle, but it can make other direct appenders wait for that commit; issue explicit `bumpChangeFeed()` calls after the write or outside the transaction. A deployment that writes only through transactions and reads the feed from a connection that cannot write must schedule `drainChangeFeed()` on a writable one.\n- `getChangesSince(db, { since, tables?, tenantId?, limit? }) → { changes, cursor, resyncRequired?, resyncCursor? }`: strictly monotonic cursor; polling with returned cursors misses no committed change and never repeats one. A cursor that cannot be served incrementally — pruned below the retained `[floor..horizon]` run, or foreign/ahead of the horizon — gets `resyncRequired: true` with empty `changes`, an unadvanced `cursor`, and `resyncCursor` set to the current horizon so clients can full-refetch then resume incrementally; detection runs on the UNFILTERED log so `tables`/`tenantId` filters never trigger or mask it. `getTenantScopedChangesSince()` resolves tenant via the DispatchBus resolver hook (fail-closed: tenancy on + no context → global rows only; tenant `T` sees `T` + global rows, never another tenant).\n- `getTableVersion(db, table) → number`: the per-table change version (`MAX(seq)` for the table **plus that table's staged-but-undrained count**, so a staged write still moves the ETag and cannot false-304 a client; the sum is monotonic because draining `n` staged rows raises the table's `MAX(seq)` by at least `n`, and both terms are read in ONE statement — separate reads let a drain be counted twice, minting a version a later write re-mints; replica-stable, with no per-process divergence), the ETag source for zero-query conditional GETs (#1765). Advances on any framework write to the table (CRUD and sync-apply, which all `save()`/`delete()`). A table with no retained entry of its own falls back to the global horizon (never a resettable low value) so an all-pruned table cannot false-304 a stale client; only 0 when the feed is empty.\n- Generated `_changes` routes: REST (`GET {basePath}/_changes`, requires `authMiddleware`, otherwise 401 — per-model `api.public` does NOT apply) and SvelteKit (`{routesDir}/_changes/+server.ts`, requires an authenticated principal on `locals`; opt out via `sveltekit.changesRoute.enabled: false`). Query params: `since`, `tables` (comma-separated), `limit`. Responses stay HTTP 200 in the resync state — `resyncRequired` is protocol state, not an error, and `resyncCursor` is the resume cursor after the client completes a full refetch.\n- Retention: `pruneChangeFeed(db, { maxAgeMs?, maxRows?, dryRun? })` — scheduled since #2375 by `runRetentionSweep()` (30-day default), so nothing needs to call it directly; `dryRun` counts the same predicate instead of deleting. Pruning deletes oldest-first and always retains the newest entry (a non-empty feed is never emptied), which is what makes pruned-cursor detection provable. The age bound is a **prefix** bound — everything below the oldest entry still inside the window — because `created_at` and `seq` are not co-monotonic (writer clocks skew, and a staged entry carries its stage-time stamp into a later-assigned sequence); deleting by timestamp alone could punch a hole in the middle of the retained run, where `since < floor - 1` cannot see it and keeps caught-up consumers polling normally. Raw-SQL writes are invisible to the feed (same documented gap as the #1499 cache); `bumpChangeFeed(db, { table, rowId? })` is the manual escape hatch.\n"
|
|
988
988
|
},
|
|
989
989
|
{
|
|
990
990
|
"path": "agents/change-signals.md",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/system/bootstrap.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAG5D,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/system/bootstrap.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAG5D,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,oBAAoB,CAAC;AA8H/E;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,iBAAiB,EACrB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAqCf"}
|
package/dist/system/bootstrap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SMRT_SCHEMA_VERSION, getSystemTableDDL } from "./schema.js";
|
|
2
|
-
import { ensurePostgresChangeFeedAppendFunction } from "../change-feed.js";
|
|
2
|
+
import { ensurePostgresChangeFeedAppendFunction, ensurePostgresChangeFeedHelpers } from "../change-feed.js";
|
|
3
3
|
import { assertPostgresSystemTimestampsCurrent, ensureBootstrapSystemTableCompatibility, getDatabaseEngine, tableExists } from "./compatibility.js";
|
|
4
4
|
import { createLogger } from "@happyvertical/logger";
|
|
5
5
|
//#region src/system/bootstrap.ts
|
|
@@ -44,6 +44,7 @@ async function isSystemSchemaVersionApplied(db, typeHint) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
async function bootstrapSystemTables(db, typeHint) {
|
|
47
|
+
await ensurePostgresChangeFeedHelpers(db, typeHint);
|
|
47
48
|
if (await isSystemSchemaVersionApplied(db, typeHint)) return;
|
|
48
49
|
await ensureBootstrapSystemTableCompatibility(db, typeHint);
|
|
49
50
|
const engine = getDatabaseEngine(db, typeHint);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/system/bootstrap.ts"],"sourcesContent":["/** Canonical, idempotent SMRT system-table provisioning. */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, TransactionHandle } from '@happyvertical/sql';\nimport {
|
|
1
|
+
{"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/system/bootstrap.ts"],"sourcesContent":["/** Canonical, idempotent SMRT system-table provisioning. */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface, TransactionHandle } from '@happyvertical/sql';\nimport {\n ensurePostgresChangeFeedAppendFunction,\n ensurePostgresChangeFeedHelpers,\n} from '../change-feed.js';\nimport {\n assertPostgresSystemTimestampsCurrent,\n ensureBootstrapSystemTableCompatibility,\n getDatabaseEngine,\n tableExists,\n} from './compatibility.js';\nimport { getSystemTableDDL, SMRT_SCHEMA_VERSION } from './schema.js';\n\nconst SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL =\n \"SELECT pg_advisory_xact_lock(hashtext('smrt'), hashtext('system-tables'))\";\n\n/**\n * Timeout budget for the PostgreSQL system-table bootstrap transaction.\n *\n * The runtime pool's session `lock_timeout`/`statement_timeout` (#2377) are\n * sized for request work. This transaction is not request work: it holds the\n * advisory lock across up to 29 sequential DDL round-trips — ~18.85 s on a\n * high-latency link at 650 ms per round trip — and a second replica cold-starting\n * against the same fresh database *waits* on that lock. Both GUCs bound that\n * wait, because `pg_advisory_xact_lock` is an ordinary statement in the lock\n * manager, so at the runtime defaults the second replica would abort with\n * \"canceling statement due to lock timeout\" where it previously waited and\n * succeeded.\n *\n * Five minutes is an order of magnitude above the documented worst case and\n * still bounded — this is a raise, not a disable. `SET LOCAL` scopes it to this\n * transaction, the same lever migrations use for the same reason (#2362).\n */\nconst SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL = [\n \"SET LOCAL lock_timeout = '300000ms'\",\n \"SET LOCAL statement_timeout = '300000ms'\",\n];\nconst logger = createLogger({ level: 'info' });\n\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n this: DatabaseInterface,\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n};\n\nfunction getQueryRows(result: unknown): Record<string, unknown>[] {\n if (Array.isArray(result)) return result as Record<string, unknown>[];\n if (result && typeof result === 'object' && 'rows' in result) {\n const rows = (result as { rows?: unknown }).rows;\n if (Array.isArray(rows)) return rows as Record<string, unknown>[];\n }\n return [];\n}\n\nasync function isSystemSchemaVersionApplied(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<boolean> {\n const engine = getDatabaseEngine(db, typeHint);\n if (\n engine === 'postgres' &&\n !(await tableExists(db, '_smrt_migrations', typeHint))\n ) {\n return false;\n }\n try {\n const versionParam = engine === 'postgres' ? '$1' : '?';\n const rows = await db.query(\n `SELECT 1 FROM _smrt_migrations WHERE version = ${versionParam} LIMIT 1`,\n SMRT_SCHEMA_VERSION,\n );\n return getQueryRows(rows).length > 0;\n } catch (error) {\n if (engine === 'postgres') throw error;\n return false;\n }\n}\n\nasync function bootstrapSystemTables(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n // #2649: the change-feed helpers changed without a change to the portable\n // system DDL, so a database already stamped with this version would never\n // reach the install below and would keep the deadlocking append function.\n // Ordinary model writes do not call ensureChangeFeedTable(), so nothing else\n // would repair it. One catalog probe when the helpers are already current.\n await ensurePostgresChangeFeedHelpers(db, typeHint);\n\n if (await isSystemSchemaVersionApplied(db, typeHint)) return;\n\n await ensureBootstrapSystemTableCompatibility(db, typeHint);\n const engine = getDatabaseEngine(db, typeHint);\n for (const ddl of getSystemTableDDL(engine)) {\n for (const statement of ddl\n .split(';')\n .map((value) => value.trim())\n .filter(Boolean)) {\n await db.query(statement);\n }\n }\n await ensurePostgresChangeFeedAppendFunction(db, { typeHint });\n await assertPostgresSystemTimestampsCurrent(db, typeHint);\n\n const id = crypto.randomUUID();\n const description = 'Initial SMRT system tables';\n await db.execute`\n INSERT INTO _smrt_migrations (id, version, description)\n VALUES (${id}, ${SMRT_SCHEMA_VERSION}, ${description})\n ON CONFLICT(version) DO NOTHING\n `;\n}\n\nasync function rollbackBootstrap(tx: TransactionHandle): Promise<void> {\n try {\n if (typeof tx.isActive !== 'function' || tx.isActive()) await tx.rollback();\n } catch (error) {\n logger.warn(\n `[smrt] Failed to rollback system table bootstrap transaction: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n\n/**\n * Ensure every framework-owned SMRT system table exists before use.\n *\n * PostgreSQL provisioning is serialized in a bounded advisory-locked\n * transaction; other engines use the schema's idempotent DDL directly.\n */\nexport async function ensureSystemTables(\n db: DatabaseInterface,\n typeHint?: string,\n): Promise<void> {\n if (getDatabaseEngine(db, typeHint) !== 'postgres') {\n await bootstrapSystemTables(db, typeHint);\n return;\n }\n\n const beginTransaction = db.beginTransaction;\n const transaction = (db as TransactionCapableDatabase).transaction;\n if (typeof beginTransaction === 'function') {\n const tx = await beginTransaction.call(db);\n if (!tx) throw new Error('Database transaction could not be started');\n try {\n // Raise the budget before taking the lock — the wait itself is what the\n // runtime session timeouts would otherwise cancel (#2377).\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n await bootstrapSystemTables(tx, typeHint);\n await tx.commit();\n return;\n } catch (error) {\n await rollbackBootstrap(tx);\n throw error;\n }\n }\n\n if (typeof transaction === 'function') {\n await transaction.call(db, async (tx) => {\n for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);\n await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);\n await bootstrapSystemTables(tx, typeHint);\n });\n return;\n }\n\n throw new Error(\n 'Postgres system table bootstrap requires a transaction-capable database adapter',\n );\n}\n"],"mappings":";;;;;;AAgBA,IAAM,kCACJ;;;;;;;;;;;;;;;;;;AAmBF,IAAM,qCAAqC,CACzC,uCACA,0CACF;AACA,IAAM,SAAS,aAAa,EAAE,OAAO,OAAO,CAAC;AAS7C,SAAS,aAAa,QAA4C;CAChE,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;CAClC,IAAI,UAAU,OAAO,WAAW,YAAY,UAAU,QAAQ;EAC5D,MAAM,OAAQ,OAA8B;EAC5C,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAClC;CACA,OAAO,CAAC;AACV;AAEA,eAAe,6BACb,IACA,UACkB;CAClB,MAAM,SAAS,kBAAkB,IAAI,QAAQ;CAC7C,IACE,WAAW,cACX,CAAE,MAAM,YAAY,IAAI,oBAAoB,QAAQ,GAEpD,OAAO;CAET,IAAI;EACF,MAAM,eAAe,WAAW,aAAa,OAAO;EAKpD,OAAO,aAAa,MAJD,GAAG,MACpB,kDAAkD,aAAa,WAC/D,mBACF,CACwB,CAAC,CAAC,SAAS;CACrC,SAAS,OAAO;EACd,IAAI,WAAW,YAAY,MAAM;EACjC,OAAO;CACT;AACF;AAEA,eAAe,sBACb,IACA,UACe;CAMf,MAAM,gCAAgC,IAAI,QAAQ;CAElD,IAAI,MAAM,6BAA6B,IAAI,QAAQ,GAAG;CAEtD,MAAM,wCAAwC,IAAI,QAAQ;CAC1D,MAAM,SAAS,kBAAkB,IAAI,QAAQ;CAC7C,KAAK,MAAM,OAAO,kBAAkB,MAAM,GACxC,KAAK,MAAM,aAAa,IACrB,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,GACf,MAAM,GAAG,MAAM,SAAS;CAG5B,MAAM,uCAAuC,IAAI,EAAE,SAAS,CAAC;CAC7D,MAAM,sCAAsC,IAAI,QAAQ;CAExD,MAAM,KAAK,OAAO,WAAW;CAE7B,MAAM,GAAG,OAAO;;cAEJ,GAAG,IAAI,oBAAoB,IAAI,6BAAY;;;AAGzD;AAEA,eAAe,kBAAkB,IAAsC;CACrE,IAAI;EACF,IAAI,OAAO,GAAG,aAAa,cAAc,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS;CAC5E,SAAS,OAAO;EACd,OAAO,KACL,iEACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAEzD;CACF;AACF;;;;;;;AAQA,eAAsB,mBACpB,IACA,UACe;CACf,IAAI,kBAAkB,IAAI,QAAQ,MAAM,YAAY;EAClD,MAAM,sBAAsB,IAAI,QAAQ;EACxC;CACF;CAEA,MAAM,mBAAmB,GAAG;CAC5B,MAAM,cAAe,GAAkC;CACvD,IAAI,OAAO,qBAAqB,YAAY;EAC1C,MAAM,KAAK,MAAM,iBAAiB,KAAK,EAAE;EACzC,IAAI,CAAC,IAAI,MAAM,IAAI,MAAM,2CAA2C;EACpE,IAAI;GAGF,KAAK,MAAM,OAAO,oCAAoC,MAAM,GAAG,MAAM,GAAG;GACxE,MAAM,GAAG,MAAM,+BAA+B;GAC9C,MAAM,sBAAsB,IAAI,QAAQ;GACxC,MAAM,GAAG,OAAO;GAChB;EACF,SAAS,OAAO;GACd,MAAM,kBAAkB,EAAE;GAC1B,MAAM;EACR;CACF;CAEA,IAAI,OAAO,gBAAgB,YAAY;EACrC,MAAM,YAAY,KAAK,IAAI,OAAO,OAAO;GACvC,KAAK,MAAM,OAAO,oCAAoC,MAAM,GAAG,MAAM,GAAG;GACxE,MAAM,GAAG,MAAM,+BAA+B;GAC9C,MAAM,sBAAsB,IAAI,QAAQ;EAC1C,CAAC;EACD;CACF;CAEA,MAAM,IAAI,MACR,iFACF;AACF"}
|
package/dist/system/schema.d.ts
CHANGED
|
@@ -57,6 +57,32 @@ export declare const CREATE_SMRT_AI_USAGE_TABLE = "\nCREATE TABLE IF NOT EXISTS
|
|
|
57
57
|
* table-level change without a specific row.
|
|
58
58
|
*/
|
|
59
59
|
export declare const CREATE_SMRT_CHANGES_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_changes (\n seq BIGINT PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMP NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_table_seq\n ON _smrt_changes(table_name, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_tenant_seq\n ON _smrt_changes(tenant_id, seq);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_created_at\n ON _smrt_changes(created_at);\n";
|
|
60
|
+
/**
|
|
61
|
+
* PostgreSQL-only staging table for change-feed appends made inside a
|
|
62
|
+
* caller-managed transaction (issue #2649).
|
|
63
|
+
*
|
|
64
|
+
* `_smrt_changes.seq` is allocated `COALESCE(MAX(seq), 0) + 1`, so two
|
|
65
|
+
* concurrent appends contend on the same primary-key value and the loser
|
|
66
|
+
* *waits for the winner's transaction to end*. When the winner is a long write
|
|
67
|
+
* transaction that goes on to take row locks the loser already holds, that
|
|
68
|
+
* wait closes a genuine lock cycle and PostgreSQL aborts one side with
|
|
69
|
+
* `40P01` — an ordinary concurrent request killing a legitimate long write.
|
|
70
|
+
*
|
|
71
|
+
* The staging table breaks the cycle by removing the wait: an append issued
|
|
72
|
+
* inside a caller transaction inserts here instead, where its identity value
|
|
73
|
+
* conflicts with nothing and it never waits on another transaction. The row is
|
|
74
|
+
* still fate-shared with the caller (a rollback removes it, exactly as
|
|
75
|
+
* before). A later *drain* — {@link CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION}
|
|
76
|
+
* — moves committed staged rows into `_smrt_changes` with contiguous
|
|
77
|
+
* sequences, so the feed's cursor guarantee is unchanged.
|
|
78
|
+
*
|
|
79
|
+
* PostgreSQL-only on purpose: SQLite and DuckDB do not reach the defect
|
|
80
|
+
* (SQLite serializes writers outright), and keeping the portable DDL untouched
|
|
81
|
+
* keeps the embedded path byte-identical.
|
|
82
|
+
*/
|
|
83
|
+
export declare const CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE = "\nCREATE TABLE IF NOT EXISTS _smrt_changes_pending (\n pending_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n table_name TEXT NOT NULL,\n row_id TEXT,\n operation TEXT NOT NULL,\n tenant_id TEXT,\n created_at TIMESTAMPTZ NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_changes_pending_table\n ON _smrt_changes_pending(table_name);\n";
|
|
84
|
+
/** Name of the PostgreSQL staging table for deferred change-feed appends. */
|
|
85
|
+
export declare const POSTGRES_CHANGE_FEED_PENDING_TABLE = "_smrt_changes_pending";
|
|
60
86
|
/** PostgreSQL materialization of the portable change-feed table DDL. */
|
|
61
87
|
export declare const CREATE_POSTGRES_SMRT_CHANGES_TABLE: string;
|
|
62
88
|
/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */
|
|
@@ -65,6 +91,47 @@ export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = "_smrt_append_c
|
|
|
65
91
|
export declare const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp without time zone)";
|
|
66
92
|
/** Exact PostgreSQL identity used for catalog lookup of the append helper. */
|
|
67
93
|
export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp with time zone)";
|
|
94
|
+
/**
|
|
95
|
+
* Body marker stamped into both PostgreSQL change-feed helpers (#2649).
|
|
96
|
+
*
|
|
97
|
+
* Existence is not currency: an install can hold a helper of the right name
|
|
98
|
+
* and signature whose *body* predates a fix. The probe in `change-feed.ts`
|
|
99
|
+
* matches this token against `pg_proc.prosrc`, so a stale body is detected and
|
|
100
|
+
* replaced. Bump it whenever either helper's body changes in a way an existing
|
|
101
|
+
* database must pick up.
|
|
102
|
+
*/
|
|
103
|
+
export declare const POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:v3";
|
|
104
|
+
/** PostgreSQL helper that sequences staged change-feed appends (#2649). */
|
|
105
|
+
export declare const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = "_smrt_drain_changes";
|
|
106
|
+
/** Exact PostgreSQL identity used for catalog lookup of the drain helper. */
|
|
107
|
+
export declare const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY = "_smrt_drain_changes(integer)";
|
|
108
|
+
/** How many staged entries one drain call sequences. */
|
|
109
|
+
export declare const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;
|
|
110
|
+
/**
|
|
111
|
+
* PostgreSQL-only drain: move committed staged appends into `_smrt_changes`.
|
|
112
|
+
*
|
|
113
|
+
* This is where the feed's sequence is actually allocated for entries written
|
|
114
|
+
* inside a caller transaction. It runs in its own short transaction (an
|
|
115
|
+
* autocommit append's, or a reader's `drainChangeFeed()` call), holds no user
|
|
116
|
+
* row locks, and never waits on another transaction:
|
|
117
|
+
*
|
|
118
|
+
* - it only ever sees *committed* staged rows — an in-flight transaction's
|
|
119
|
+
* staged row is invisible under MVCC, so the drain neither reads nor locks
|
|
120
|
+
* it, and it is picked up by a later drain once that transaction commits;
|
|
121
|
+
* - concurrent drains are excluded by a `pg_try_advisory_xact_lock`, which
|
|
122
|
+
* *skips* rather than waits, so a drain can never become an edge in a lock
|
|
123
|
+
* cycle either.
|
|
124
|
+
*
|
|
125
|
+
* Because exactly one drain runs at a time and it numbers rows
|
|
126
|
+
* `COALESCE(MAX(seq), 0) + row_number()` in staged order, committed sequences
|
|
127
|
+
* stay contiguous and no reader can observe seq N before seq N-1 — the change
|
|
128
|
+
* feed's cursor guarantee is preserved verbatim (see `change-feed.ts`).
|
|
129
|
+
*
|
|
130
|
+
* Failures are returned as data, like the append helper, so a drain problem
|
|
131
|
+
* never aborts the caller's transaction. A failed drain leaves the staged rows
|
|
132
|
+
* in place for the next attempt.
|
|
133
|
+
*/
|
|
134
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_drain_changes(\n p_limit INTEGER DEFAULT 1000\n)\nRETURNS TABLE(\n drained_seq BIGINT,\n drained_table TEXT,\n drained_row_id TEXT,\n drained_operation TEXT,\n drained_tenant_id TEXT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_drain$\n-- smrt-change-feed-helpers:v3\nDECLARE\n v_base BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n -- EVERY statement below sits inside this exception boundary, preflight\n -- included. The caller may own the surrounding transaction, and an error\n -- escaping this function would abort it \u2014 the #2026 contract. A probe that\n -- blocks on a table lock until statement_timeout is exactly that: it is not\n -- the insert, but it aborts the caller just the same.\n BEGIN\n IF to_regclass('_smrt_changes_pending') IS NULL THEN\n RETURN;\n END IF;\n\n -- Never sequence from inside a transaction that has already written. Doing\n -- so would hold freshly allocated sequences uncommitted for the rest of\n -- that transaction, which is precisely the wait edge #2649 removes \u2014 a\n -- reader calling getChangesSince() inside a long write transaction must\n -- not put it back. The staged rows keep for the next drain.\n IF pg_current_xact_id_if_assigned() IS NOT NULL THEN\n RETURN;\n END IF;\n\n -- Cheap common case: nothing staged, so an autocommit append pays one\n -- index probe rather than a drain.\n IF NOT EXISTS (SELECT 1 FROM _smrt_changes_pending LIMIT 1) THEN\n RETURN;\n END IF;\n\n -- Try, never wait: a drain that waited could become an edge in a cycle.\n IF NOT pg_try_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('change-feed-drain')\n ) THEN\n RETURN;\n END IF;\n\n SELECT COALESCE(MAX(changes.seq), 0) INTO v_base FROM _smrt_changes AS changes;\n\n RETURN QUERY\n WITH removed AS (\n DELETE FROM _smrt_changes_pending AS pending\n WHERE pending.pending_id IN (\n SELECT candidate.pending_id\n FROM _smrt_changes_pending AS candidate\n ORDER BY candidate.pending_id\n LIMIT GREATEST(COALESCE(p_limit, 1000), 1)\n )\n RETURNING\n pending.pending_id,\n pending.table_name,\n pending.row_id,\n pending.operation,\n pending.tenant_id,\n pending.created_at\n ),\n numbered AS (\n SELECT\n removed.*,\n v_base + row_number() OVER (ORDER BY removed.pending_id) AS new_seq\n FROM removed\n ),\n inserted AS (\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n numbered.new_seq,\n numbered.table_name,\n numbered.row_id,\n numbered.operation,\n numbered.tenant_id,\n numbered.created_at\n FROM numbered\n RETURNING\n _smrt_changes.seq,\n _smrt_changes.table_name,\n _smrt_changes.row_id,\n _smrt_changes.operation,\n _smrt_changes.tenant_id\n )\n SELECT\n inserted.seq,\n inserted.table_name,\n inserted.row_id,\n inserted.operation,\n inserted.tenant_id,\n NULL::TEXT,\n NULL::TEXT\n FROM inserted\n ORDER BY inserted.seq;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT\n NULL::BIGINT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n NULL::TEXT,\n v_error_code,\n v_error_message;\n END;\nEND;\n$smrt_change_feed_drain$;\n";
|
|
68
135
|
/**
|
|
69
136
|
* PostgreSQL-only change-feed append function.
|
|
70
137
|
*
|
|
@@ -75,7 +142,7 @@ export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_appe
|
|
|
75
142
|
* semicolons, so callers must execute it whole rather than adding it to
|
|
76
143
|
* {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.
|
|
77
144
|
*/
|
|
78
|
-
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_append_change(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::TEXT;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n";
|
|
145
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_append_change(\n p_table_name TEXT,\n p_row_id TEXT,\n p_operation TEXT,\n p_tenant_id TEXT,\n p_created_at TIMESTAMPTZ\n)\nRETURNS TABLE(\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed$\n-- smrt-change-feed-helpers:v3\nDECLARE\n v_seq BIGINT;\n v_error_code TEXT;\n v_error_message TEXT;\n v_deferred BOOLEAN;\nBEGIN\n -- Has the caller already written in this transaction?\n --\n -- PostgreSQL assigns a transaction id at the first write, so a non-NULL\n -- pg_current_xact_id_if_assigned() means the caller may be holding row\n -- locks. Allocating the feed head here would then make a concurrent appender\n -- wait for this whole transaction to end \u2014 the wait edge that closes the\n -- #2649 cycle. An autocommit append reports NULL: its caller's row write\n -- committed as its own statement, so it holds no row locks and its inline\n -- allocation cannot be part of a cycle. (Requires PostgreSQL 13+.)\n --\n -- This is exactly the condition that makes a cycle possible, not an\n -- approximation of \"inside BEGIN\": a transaction that appends *before* its\n -- first write still allocates inline, but it holds no row locks at that\n -- moment, so nothing that waits on its feed row can also be waited on by it.\n -- Preflight inside its own exception boundary for the same reason the drain\n -- wraps its own: an error here would abort a caller-owned transaction\n -- (#2026), and a catalog probe is still a statement that can fail.\n BEGIN\n v_deferred := pg_current_xact_id_if_assigned() IS NOT NULL\n AND to_regclass('_smrt_changes_pending') IS NOT NULL;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n RETURN;\n END;\n\n IF v_deferred THEN\n BEGIN\n -- Staged, not sequenced: an identity value conflicts with nothing, so\n -- this insert never waits on another transaction. It stays fate-shared\n -- with the caller (rollback removes it) and is moved into\n -- _smrt_changes, in order and with a contiguous sequence, by the next\n -- drain after the caller commits.\n INSERT INTO _smrt_changes_pending (\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n VALUES (\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n );\n\n -- NULL sequence with NULL error code is the staged marker.\n RETURN QUERY SELECT NULL::BIGINT, NULL::TEXT, NULL::TEXT;\n RETURN;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n RETURN;\n END;\n END IF;\n\n -- Autocommit append. The caller's own row write already committed as its own\n -- statement, so this transaction holds no user row locks and allocating the\n -- head here cannot join a lock cycle.\n --\n -- This deliberately does NOT drain: a drain performed here would sequence\n -- staged entries that the JavaScript caller never sees, so no live SSE\n -- signal would be published for them while this append's own signal carries\n -- a HIGHER sequence -- and an EventSource that stores that id as its\n -- Last-Event-ID resumes above them and never receives them. Draining is\n -- driven from JavaScript (drainChangeFeed) so every sequenced entry gets\n -- its signal.\n BEGIN\n INSERT INTO _smrt_changes (\n seq,\n table_name,\n row_id,\n operation,\n tenant_id,\n created_at\n )\n SELECT\n COALESCE(MAX(changes.seq), 0) + 1,\n p_table_name,\n p_row_id,\n p_operation,\n p_tenant_id,\n p_created_at\n FROM _smrt_changes AS changes\n RETURNING _smrt_changes.seq INTO v_seq;\n\n RETURN QUERY SELECT v_seq, NULL::TEXT, NULL::TEXT;\n EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN\n GET STACKED DIAGNOSTICS\n v_error_code = RETURNED_SQLSTATE,\n v_error_message = MESSAGE_TEXT;\n RETURN QUERY SELECT NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed$;\n";
|
|
79
146
|
/**
|
|
80
147
|
* Serialize PostgreSQL helper replacement inside one server-side statement.
|
|
81
148
|
*
|
|
@@ -85,11 +152,17 @@ export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = "\nCREATE OR
|
|
|
85
152
|
*/
|
|
86
153
|
export declare const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION: string;
|
|
87
154
|
/**
|
|
88
|
-
* Install the PostgreSQL
|
|
155
|
+
* Install the PostgreSQL helpers only when missing, serialized server-side.
|
|
89
156
|
*
|
|
90
157
|
* A client-side catalog probe remains the fast path for already-initialized
|
|
91
158
|
* read handles. This guarded statement is the cold-path race boundary: both
|
|
92
159
|
* the advisory lock and the post-lock catalog check run before function DDL.
|
|
160
|
+
*
|
|
161
|
+
* The staging table and the drain helper (#2649) are created unconditionally
|
|
162
|
+
* (`IF NOT EXISTS` / `CREATE OR REPLACE`) because an install that already has
|
|
163
|
+
* the append helper from an older SMRT may still be missing them; the append
|
|
164
|
+
* helper itself is replaced when the staging table was absent, which is how an
|
|
165
|
+
* upgraded database picks up the deferring version.
|
|
93
166
|
*/
|
|
94
167
|
export declare const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION: string;
|
|
95
168
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qpCAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,igDAiDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,ilCAkCtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,QACqB,CAAC;AAErE,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,iFAAiF;AACjF,eAAO,MAAM,oDAAoD,yEAAkG,CAAC;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,sEAA+F,CAAC;AAE1J;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,0BAA0B,qpCAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,4MAQxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mCAAmC,o0BA4B/C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,u0BA2BxC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,igDAiDtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wCAAwC,69CAkCpD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,ilCAkCtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,weAkBrC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,0CAA0C,uWAYtD,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,kCAAkC,0BAA0B,CAAC;AAE1E,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,QACqB,CAAC;AAErE,0EAA0E;AAC1E,eAAO,MAAM,yCAAyC,wBAAwB,CAAC;AAE/E,iFAAiF;AACjF,eAAO,MAAM,oDAAoD,yEAAkG,CAAC;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,6CAA6C,sEAA+F,CAAC;AAE1J;;;;;;;;GAQG;AACH,eAAO,MAAM,kCAAkC,gCAAgC,CAAC;AAEhF,2EAA2E;AAC3E,eAAO,MAAM,wCAAwC,wBAAwB,CAAC;AAE9E,6EAA6E;AAC7E,eAAO,MAAM,4CAA4C,iCAAyD,CAAC;AAEnH,wDAAwD;AACxD,eAAO,MAAM,gCAAgC,OAAO,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,0CAA0C,grHAiItD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2CAA2C,u5IA4HvD,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,4CAA4C,QAiBxD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2CAA2C,QA4BvD,CAAC;AAaF;;;;;;GAMG;AACH,eAAO,MAAM,kCAAkC,QA6B9C,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,gLAOvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,UAU7B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAMzD,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,EAAE,SAAS,MAAM,EAG1D,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,EAAE,CAIlE;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,GACrB,MAAM,CAQR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,WAAW,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAInE,CAAC"}
|