@happyvertical/smrt-core 0.49.3 → 0.49.4
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/README.md +4 -0
- package/agents/change-feed.md +19 -0
- package/agents/collection-reads.md +21 -0
- package/agents/generators.md +5 -0
- package/agents/memory.md +23 -3
- package/agents/null-equal-indexes.md +88 -0
- package/agents/object-runtime.md +85 -0
- package/agents/schema-paths.md +7 -7
- package/dist/browser.js +2 -2
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +13 -10
- package/dist/cascade.js.map +1 -1
- package/dist/change-feed.d.ts +22 -0
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +147 -6
- package/dist/change-feed.js.map +1 -1
- package/dist/class.js +1 -1
- package/dist/collection-cache.d.ts +10 -1
- package/dist/collection-cache.d.ts.map +1 -1
- package/dist/collection-cache.js +65 -10
- package/dist/collection-cache.js.map +1 -1
- package/dist/collection.d.ts +69 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +142 -9
- package/dist/collection.js.map +1 -1
- package/dist/consumer-plugin/artifact-publication.d.ts +27 -0
- package/dist/consumer-plugin/artifact-publication.d.ts.map +1 -0
- package/dist/consumer-plugin/artifact-publication.js +80 -0
- package/dist/consumer-plugin/artifact-publication.js.map +1 -0
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +37 -4
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/db-errors.js +1 -1
- package/dist/db-errors.js.map +1 -1
- package/dist/embedded-write-queue.d.ts +1 -1
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +16 -6
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/embeddings/storage.d.ts +16 -0
- package/dist/embeddings/storage.d.ts.map +1 -1
- package/dist/embeddings/storage.js +40 -0
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/interceptors.d.ts +22 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +12 -0
- package/dist/interceptors.js.map +1 -1
- package/dist/junction.d.ts +12 -3
- package/dist/junction.d.ts.map +1 -1
- package/dist/junction.js +34 -4
- package/dist/junction.js.map +1 -1
- package/dist/knowledge-config.d.ts +6 -0
- package/dist/knowledge-config.d.ts.map +1 -0
- package/dist/knowledge-config.js +48 -0
- package/dist/knowledge-config.js.map +1 -0
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +91 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +104 -1
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +2 -0
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +2 -0
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations/null-equal-indexes.d.ts +37 -0
- package/dist/migrations/null-equal-indexes.d.ts.map +1 -0
- package/dist/migrations/null-equal-indexes.js +133 -0
- package/dist/migrations/null-equal-indexes.js.map +1 -0
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +6 -0
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations.js +2 -1
- package/dist/object.d.ts +33 -0
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +316 -46
- package/dist/object.js.map +1 -1
- package/dist/plain-json.d.ts +6 -0
- package/dist/plain-json.d.ts.map +1 -0
- package/dist/plain-json.js +41 -0
- package/dist/plain-json.js.map +1 -0
- package/dist/postgres-permissions.d.ts.map +1 -1
- package/dist/postgres-permissions.js +6 -2
- package/dist/postgres-permissions.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +3 -3
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +4 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +4 -3
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/validator.d.ts +1 -0
- package/dist/registry/validator.d.ts.map +1 -1
- package/dist/registry/validator.js +8 -1
- package/dist/registry/validator.js.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +21 -0
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +2 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/conflict-target.d.ts +8 -2
- package/dist/schema/conflict-target.d.ts.map +1 -1
- package/dist/schema/conflict-target.js +11 -3
- package/dist/schema/conflict-target.js.map +1 -1
- package/dist/schema/ddl/null-equal-index.d.ts +5 -0
- package/dist/schema/ddl/null-equal-index.d.ts.map +1 -0
- package/dist/schema/ddl/null-equal-index.js +17 -0
- package/dist/schema/ddl/null-equal-index.js.map +1 -0
- package/dist/schema/ddl/postgres-strategy.d.ts +2 -1
- package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/postgres-strategy.js +7 -0
- package/dist/schema/ddl/postgres-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +8 -3
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/manifest-schema.d.ts +2 -1
- package/dist/schema/manifest-schema.d.ts.map +1 -1
- package/dist/schema/manifest-schema.js +12 -4
- package/dist/schema/manifest-schema.js.map +1 -1
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +1 -1
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +2 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/scripts/browser-plain-json-regression.mjs +85 -0
- package/dist/smrt-knowledge.json +73 -15
- package/dist/system/schema.d.ts +14 -3
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +120 -2
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +9 -7
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +24 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +47 -64
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +2 -2
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +10 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +14 -2
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -4
package/dist/system/schema.d.ts
CHANGED
|
@@ -87,10 +87,14 @@ export declare const POSTGRES_CHANGE_FEED_PENDING_TABLE = "_smrt_changes_pending
|
|
|
87
87
|
export declare const CREATE_POSTGRES_SMRT_CHANGES_TABLE: string;
|
|
88
88
|
/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */
|
|
89
89
|
export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = "_smrt_append_change";
|
|
90
|
+
/** PostgreSQL one-statement batch wrapper around the isolated append helper. */
|
|
91
|
+
export declare const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME = "_smrt_append_changes";
|
|
90
92
|
/** Legacy helper identity used before PostgreSQL Date values became instants. */
|
|
91
93
|
export declare const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp without time zone)";
|
|
92
94
|
/** Exact PostgreSQL identity used for catalog lookup of the append helper. */
|
|
93
95
|
export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_append_change(text,text,text,text,timestamp with time zone)";
|
|
96
|
+
/** PostgreSQL identity of the one-statement batch append helper. */
|
|
97
|
+
export declare const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY = "_smrt_append_changes(jsonb)";
|
|
94
98
|
/**
|
|
95
99
|
* Body marker stamped into both PostgreSQL change-feed helpers (#2649).
|
|
96
100
|
*
|
|
@@ -100,7 +104,7 @@ export declare const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = "_smrt_appe
|
|
|
100
104
|
* replaced. Bump it whenever either helper's body changes in a way an existing
|
|
101
105
|
* database must pick up.
|
|
102
106
|
*/
|
|
103
|
-
export declare const POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:
|
|
107
|
+
export declare const POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:v4";
|
|
104
108
|
/** PostgreSQL helper that sequences staged change-feed appends (#2649). */
|
|
105
109
|
export declare const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = "_smrt_drain_changes";
|
|
106
110
|
/** Exact PostgreSQL identity used for catalog lookup of the drain helper. */
|
|
@@ -131,7 +135,7 @@ export declare const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;
|
|
|
131
135
|
* never aborts the caller's transaction. A failed drain leaves the staged rows
|
|
132
136
|
* in place for the next attempt.
|
|
133
137
|
*/
|
|
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:
|
|
138
|
+
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:v4\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";
|
|
135
139
|
/**
|
|
136
140
|
* PostgreSQL-only change-feed append function.
|
|
137
141
|
*
|
|
@@ -142,7 +146,14 @@ export declare const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = "\nCREATE OR R
|
|
|
142
146
|
* semicolons, so callers must execute it whole rather than adding it to
|
|
143
147
|
* {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.
|
|
144
148
|
*/
|
|
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:
|
|
149
|
+
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:v4\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";
|
|
150
|
+
/**
|
|
151
|
+
* PostgreSQL batch append helper. It makes the staged/direct decision before
|
|
152
|
+
* its own writes, then appends every element through that one path. This is
|
|
153
|
+
* necessary because a direct first insert assigns the transaction id that
|
|
154
|
+
* later entries must not mistake for a caller-owned write transaction.
|
|
155
|
+
*/
|
|
156
|
+
export declare const CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION = "\nCREATE OR REPLACE FUNCTION _smrt_append_changes(\n p_entries JSONB\n)\nRETURNS TABLE(\n entry_index INTEGER,\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_batch$\n-- smrt-change-feed-helpers:v4\nDECLARE\n v_deferred BOOLEAN;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\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\n IF v_deferred THEN\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), staged AS (\n INSERT INTO _smrt_changes_pending (\n table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n input.value->>'table',\n input.value->>'rowId',\n input.value->>'operation',\n input.value->>'tenantId',\n (input.value->>'timestamp')::TIMESTAMPTZ\n FROM input\n ORDER BY input.entry_index\n RETURNING 1\n )\n SELECT input.entry_index, NULL::BIGINT, NULL::TEXT, NULL::TEXT\n FROM input\n ORDER BY input.entry_index;\n RETURN;\n END IF;\n\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), numbered AS (\n SELECT\n input.*,\n COALESCE((SELECT MAX(seq) FROM _smrt_changes), 0)\n + row_number() OVER (ORDER BY input.entry_index) AS new_seq\n FROM input\n ), inserted AS (\n INSERT INTO _smrt_changes (\n seq, table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n numbered.new_seq,\n numbered.value->>'table',\n numbered.value->>'rowId',\n numbered.value->>'operation',\n numbered.value->>'tenantId',\n (numbered.value->>'timestamp')::TIMESTAMPTZ\n FROM numbered\n ORDER BY numbered.entry_index\n RETURNING seq\n )\n SELECT\n numbered.entry_index,\n numbered.new_seq,\n NULL::TEXT,\n NULL::TEXT\n FROM numbered\n ORDER BY numbered.entry_index;\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::INTEGER, NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed_batch$;\n";
|
|
146
157
|
/**
|
|
147
158
|
* Serialize PostgreSQL helper replacement inside one server-side statement.
|
|
148
159
|
*
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;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,
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/system/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/system/schema.js
CHANGED
|
@@ -333,10 +333,14 @@ var POSTGRES_CHANGE_FEED_PENDING_TABLE = "_smrt_changes_pending";
|
|
|
333
333
|
var CREATE_POSTGRES_SMRT_CHANGES_TABLE = CREATE_SMRT_CHANGES_TABLE.replace(/\bTIMESTAMP\b/g, "TIMESTAMPTZ");
|
|
334
334
|
/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */
|
|
335
335
|
var POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = "_smrt_append_change";
|
|
336
|
+
/** PostgreSQL one-statement batch wrapper around the isolated append helper. */
|
|
337
|
+
var POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME = "_smrt_append_changes";
|
|
336
338
|
/** Legacy helper identity used before PostgreSQL Date values became instants. */
|
|
337
339
|
var LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;
|
|
338
340
|
/** Exact PostgreSQL identity used for catalog lookup of the append helper. */
|
|
339
341
|
var POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;
|
|
342
|
+
/** PostgreSQL identity of the one-statement batch append helper. */
|
|
343
|
+
var POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(jsonb)`;
|
|
340
344
|
/**
|
|
341
345
|
* Body marker stamped into both PostgreSQL change-feed helpers (#2649).
|
|
342
346
|
*
|
|
@@ -346,7 +350,7 @@ var POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPE
|
|
|
346
350
|
* replaced. Bump it whenever either helper's body changes in a way an existing
|
|
347
351
|
* database must pick up.
|
|
348
352
|
*/
|
|
349
|
-
var POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:
|
|
353
|
+
var POSTGRES_CHANGE_FEED_HELPER_MARKER = "smrt-change-feed-helpers:v4";
|
|
350
354
|
/** PostgreSQL helper that sequences staged change-feed appends (#2649). */
|
|
351
355
|
var POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = "_smrt_drain_changes";
|
|
352
356
|
/** Exact PostgreSQL identity used for catalog lookup of the drain helper. */
|
|
@@ -642,6 +646,103 @@ BEGIN
|
|
|
642
646
|
END;
|
|
643
647
|
$smrt_change_feed$;
|
|
644
648
|
`;
|
|
649
|
+
/**
|
|
650
|
+
* PostgreSQL batch append helper. It makes the staged/direct decision before
|
|
651
|
+
* its own writes, then appends every element through that one path. This is
|
|
652
|
+
* necessary because a direct first insert assigns the transaction id that
|
|
653
|
+
* later entries must not mistake for a caller-owned write transaction.
|
|
654
|
+
*/
|
|
655
|
+
var CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION = `
|
|
656
|
+
CREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(
|
|
657
|
+
p_entries JSONB
|
|
658
|
+
)
|
|
659
|
+
RETURNS TABLE(
|
|
660
|
+
entry_index INTEGER,
|
|
661
|
+
allocated_seq BIGINT,
|
|
662
|
+
error_code TEXT,
|
|
663
|
+
error_message TEXT
|
|
664
|
+
)
|
|
665
|
+
LANGUAGE plpgsql
|
|
666
|
+
SECURITY INVOKER
|
|
667
|
+
AS $smrt_change_feed_batch$
|
|
668
|
+
-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}
|
|
669
|
+
DECLARE
|
|
670
|
+
v_deferred BOOLEAN;
|
|
671
|
+
v_error_code TEXT;
|
|
672
|
+
v_error_message TEXT;
|
|
673
|
+
BEGIN
|
|
674
|
+
BEGIN
|
|
675
|
+
v_deferred := pg_current_xact_id_if_assigned() IS NOT NULL
|
|
676
|
+
AND to_regclass('_smrt_changes_pending') IS NOT NULL;
|
|
677
|
+
|
|
678
|
+
IF v_deferred THEN
|
|
679
|
+
RETURN QUERY
|
|
680
|
+
WITH input AS (
|
|
681
|
+
SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value
|
|
682
|
+
FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))
|
|
683
|
+
WITH ORDINALITY AS entries(value, ordinality)
|
|
684
|
+
), staged AS (
|
|
685
|
+
INSERT INTO _smrt_changes_pending (
|
|
686
|
+
table_name, row_id, operation, tenant_id, created_at
|
|
687
|
+
)
|
|
688
|
+
SELECT
|
|
689
|
+
input.value->>'table',
|
|
690
|
+
input.value->>'rowId',
|
|
691
|
+
input.value->>'operation',
|
|
692
|
+
input.value->>'tenantId',
|
|
693
|
+
(input.value->>'timestamp')::TIMESTAMPTZ
|
|
694
|
+
FROM input
|
|
695
|
+
ORDER BY input.entry_index
|
|
696
|
+
RETURNING 1
|
|
697
|
+
)
|
|
698
|
+
SELECT input.entry_index, NULL::BIGINT, NULL::TEXT, NULL::TEXT
|
|
699
|
+
FROM input
|
|
700
|
+
ORDER BY input.entry_index;
|
|
701
|
+
RETURN;
|
|
702
|
+
END IF;
|
|
703
|
+
|
|
704
|
+
RETURN QUERY
|
|
705
|
+
WITH input AS (
|
|
706
|
+
SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value
|
|
707
|
+
FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))
|
|
708
|
+
WITH ORDINALITY AS entries(value, ordinality)
|
|
709
|
+
), numbered AS (
|
|
710
|
+
SELECT
|
|
711
|
+
input.*,
|
|
712
|
+
COALESCE((SELECT MAX(seq) FROM _smrt_changes), 0)
|
|
713
|
+
+ row_number() OVER (ORDER BY input.entry_index) AS new_seq
|
|
714
|
+
FROM input
|
|
715
|
+
), inserted AS (
|
|
716
|
+
INSERT INTO _smrt_changes (
|
|
717
|
+
seq, table_name, row_id, operation, tenant_id, created_at
|
|
718
|
+
)
|
|
719
|
+
SELECT
|
|
720
|
+
numbered.new_seq,
|
|
721
|
+
numbered.value->>'table',
|
|
722
|
+
numbered.value->>'rowId',
|
|
723
|
+
numbered.value->>'operation',
|
|
724
|
+
numbered.value->>'tenantId',
|
|
725
|
+
(numbered.value->>'timestamp')::TIMESTAMPTZ
|
|
726
|
+
FROM numbered
|
|
727
|
+
ORDER BY numbered.entry_index
|
|
728
|
+
RETURNING seq
|
|
729
|
+
)
|
|
730
|
+
SELECT
|
|
731
|
+
numbered.entry_index,
|
|
732
|
+
numbered.new_seq,
|
|
733
|
+
NULL::TEXT,
|
|
734
|
+
NULL::TEXT
|
|
735
|
+
FROM numbered
|
|
736
|
+
ORDER BY numbered.entry_index;
|
|
737
|
+
EXCEPTION WHEN query_canceled OR assert_failure OR OTHERS THEN
|
|
738
|
+
GET STACKED DIAGNOSTICS
|
|
739
|
+
v_error_code = RETURNED_SQLSTATE,
|
|
740
|
+
v_error_message = MESSAGE_TEXT;
|
|
741
|
+
RETURN QUERY SELECT NULL::INTEGER, NULL::BIGINT, v_error_code, v_error_message;
|
|
742
|
+
END;
|
|
743
|
+
END;
|
|
744
|
+
$smrt_change_feed_batch$;
|
|
745
|
+
`;
|
|
645
746
|
var POSTGRES_CHANGE_FEED_PENDING_DDL = CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0).map((statement) => `EXECUTE $smrt_change_feed_pending_ddl$
|
|
646
747
|
${statement}
|
|
647
748
|
$smrt_change_feed_pending_ddl$;`).join("\n");
|
|
@@ -667,6 +768,9 @@ $smrt_change_feed_drain_ddl$;
|
|
|
667
768
|
EXECUTE $smrt_change_feed_ddl$
|
|
668
769
|
${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
|
|
669
770
|
$smrt_change_feed_ddl$;
|
|
771
|
+
EXECUTE $smrt_change_feed_batch_ddl$
|
|
772
|
+
${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}
|
|
773
|
+
$smrt_change_feed_batch_ddl$;
|
|
670
774
|
END;
|
|
671
775
|
$smrt_replace_change_feed$;
|
|
672
776
|
`;
|
|
@@ -698,6 +802,10 @@ BEGIN
|
|
|
698
802
|
FROM pg_proc
|
|
699
803
|
WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')
|
|
700
804
|
AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'
|
|
805
|
+
) OR NOT EXISTS (
|
|
806
|
+
SELECT 1 FROM pg_proc
|
|
807
|
+
WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')
|
|
808
|
+
AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'
|
|
701
809
|
);
|
|
702
810
|
${POSTGRES_CHANGE_FEED_PENDING_DDL}
|
|
703
811
|
DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};
|
|
@@ -708,6 +816,9 @@ $smrt_change_feed_drain_ddl$;
|
|
|
708
816
|
EXECUTE $smrt_change_feed_ddl$
|
|
709
817
|
${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
|
|
710
818
|
$smrt_change_feed_ddl$;
|
|
819
|
+
EXECUTE $smrt_change_feed_batch_ddl$
|
|
820
|
+
${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}
|
|
821
|
+
$smrt_change_feed_batch_ddl$;
|
|
711
822
|
END IF;
|
|
712
823
|
END;
|
|
713
824
|
$smrt_ensure_change_feed$;
|
|
@@ -734,6 +845,10 @@ BEGIN
|
|
|
734
845
|
FROM pg_proc
|
|
735
846
|
WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')
|
|
736
847
|
AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'
|
|
848
|
+
) OR NOT EXISTS (
|
|
849
|
+
SELECT 1 FROM pg_proc
|
|
850
|
+
WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')
|
|
851
|
+
AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'
|
|
737
852
|
);
|
|
738
853
|
${CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0).map((statement) => `EXECUTE $smrt_change_feed_schema_ddl$
|
|
739
854
|
${statement}
|
|
@@ -747,6 +862,9 @@ $smrt_change_feed_drain_ddl$;
|
|
|
747
862
|
EXECUTE $smrt_change_feed_ddl$
|
|
748
863
|
${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}
|
|
749
864
|
$smrt_change_feed_ddl$;
|
|
865
|
+
EXECUTE $smrt_change_feed_batch_ddl$
|
|
866
|
+
${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}
|
|
867
|
+
$smrt_change_feed_batch_ddl$;
|
|
750
868
|
END IF;
|
|
751
869
|
END;
|
|
752
870
|
$smrt_ensure_change_feed_schema$;
|
|
@@ -859,6 +977,6 @@ function getSystemTableDDLForEngine(ddl, engine) {
|
|
|
859
977
|
var SMRT_SCHEMA_VERSION = "1.10.1";
|
|
860
978
|
Object.freeze({ "1.10.1": "f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16" });
|
|
861
979
|
//#endregion
|
|
862
|
-
export { ALL_SYSTEM_TABLES, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION, CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE, CREATE_POSTGRES_SMRT_CHANGES_TABLE, CREATE_SMRT_AI_USAGE_TABLE, CREATE_SMRT_BACKFILLS_TABLE, CREATE_SMRT_CHANGES_TABLE, CREATE_SMRT_CONTEXTS_TABLE, CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, CREATE_SMRT_EMBEDDINGS_TABLE, CREATE_SMRT_MIGRATIONS_TABLE, CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, FRAMEWORK_OPERATIONAL_TABLES, LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, POSTGRES_CHANGE_FEED_DRAIN_BATCH, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME, POSTGRES_CHANGE_FEED_HELPER_MARKER, POSTGRES_CHANGE_FEED_PENDING_TABLE, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, RETIRED_SYSTEM_TABLES, SMRT_SCHEMA_VERSION, getSystemTableDDL, getSystemTableDDLForEngine };
|
|
980
|
+
export { ALL_SYSTEM_TABLES, CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION, CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE, CREATE_POSTGRES_SMRT_CHANGES_TABLE, CREATE_SMRT_AI_USAGE_TABLE, CREATE_SMRT_BACKFILLS_TABLE, CREATE_SMRT_CHANGES_TABLE, CREATE_SMRT_CONTEXTS_TABLE, CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE, CREATE_SMRT_DISPATCH_TABLE, CREATE_SMRT_EMBEDDINGS_TABLE, CREATE_SMRT_MIGRATIONS_TABLE, CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE, ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, ENSURE_POSTGRES_CHANGE_FEED_SCHEMA, FRAMEWORK_OPERATIONAL_TABLES, LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME, POSTGRES_CHANGE_FEED_DRAIN_BATCH, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY, POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME, POSTGRES_CHANGE_FEED_HELPER_MARKER, POSTGRES_CHANGE_FEED_PENDING_TABLE, REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION, RETIRED_SYSTEM_TABLES, SMRT_SCHEMA_VERSION, getSystemTableDDL, getSystemTableDDLForEngine };
|
|
863
981
|
|
|
864
982
|
//# sourceMappingURL=schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n\n-- Retention predicate for pruneExpiredContexts() (#2375). Rows that never\n-- expire carry NULL here, so the index only has to cover the ones that do.\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_expires_at\n ON _smrt_contexts(expires_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n\n-- Retention predicates of DispatchCollection.cleanup() (#2375): completed rows\n-- are aged out on processed_at, failed rows on updated_at. The status-only\n-- index above cannot serve either range.\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_processed\n ON _smrt_dispatch(status, processed_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_updated\n ON _smrt_dispatch(status, updated_at);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n\n-- Serves both the tenant-scoped retention predicate of pruneAiUsage() and the\n-- subscriptions billing meter's tenant_id + created_at range scan, neither of\n-- which the single-column indexes above can satisfy (#2375).\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant_created\n ON _smrt_ai_usage(tenant_id, created_at);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport 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`;\n\n/**\n * PostgreSQL-only staging table for change-feed appends made inside a\n * caller-managed transaction (issue #2649).\n *\n * `_smrt_changes.seq` is allocated `COALESCE(MAX(seq), 0) + 1`, so two\n * concurrent appends contend on the same primary-key value and the loser\n * *waits for the winner's transaction to end*. When the winner is a long write\n * transaction that goes on to take row locks the loser already holds, that\n * wait closes a genuine lock cycle and PostgreSQL aborts one side with\n * `40P01` — an ordinary concurrent request killing a legitimate long write.\n *\n * The staging table breaks the cycle by removing the wait: an append issued\n * inside a caller transaction inserts here instead, where its identity value\n * conflicts with nothing and it never waits on another transaction. The row is\n * still fate-shared with the caller (a rollback removes it, exactly as\n * before). A later *drain* — {@link CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION}\n * — moves committed staged rows into `_smrt_changes` with contiguous\n * sequences, so the feed's cursor guarantee is unchanged.\n *\n * PostgreSQL-only on purpose: SQLite and DuckDB do not reach the defect\n * (SQLite serializes writers outright), and keeping the portable DDL untouched\n * keeps the embedded path byte-identical.\n */\nexport 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`;\n\n/** Name of the PostgreSQL staging table for deferred change-feed appends. */\nexport const POSTGRES_CHANGE_FEED_PENDING_TABLE = '_smrt_changes_pending';\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ');\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/**\n * Body marker stamped into both PostgreSQL change-feed helpers (#2649).\n *\n * Existence is not currency: an install can hold a helper of the right name\n * and signature whose *body* predates a fix. The probe in `change-feed.ts`\n * matches this token against `pg_proc.prosrc`, so a stale body is detected and\n * replaced. Bump it whenever either helper's body changes in a way an existing\n * database must pick up.\n */\nexport const POSTGRES_CHANGE_FEED_HELPER_MARKER = 'smrt-change-feed-helpers:v3';\n\n/** PostgreSQL helper that sequences staged change-feed appends (#2649). */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = '_smrt_drain_changes';\n\n/** Exact PostgreSQL identity used for catalog lookup of the drain helper. */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(integer)`;\n\n/** How many staged entries one drain call sequences. */\nexport const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;\n\n/**\n * PostgreSQL-only drain: move committed staged appends into `_smrt_changes`.\n *\n * This is where the feed's sequence is actually allocated for entries written\n * inside a caller transaction. It runs in its own short transaction (an\n * autocommit append's, or a reader's `drainChangeFeed()` call), holds no user\n * row locks, and never waits on another transaction:\n *\n * - it only ever sees *committed* staged rows — an in-flight transaction's\n * staged row is invisible under MVCC, so the drain neither reads nor locks\n * it, and it is picked up by a later drain once that transaction commits;\n * - concurrent drains are excluded by a `pg_try_advisory_xact_lock`, which\n * *skips* rather than waits, so a drain can never become an edge in a lock\n * cycle either.\n *\n * Because exactly one drain runs at a time and it numbers rows\n * `COALESCE(MAX(seq), 0) + row_number()` in staged order, committed sequences\n * stay contiguous and no reader can observe seq N before seq N-1 — the change\n * feed's cursor guarantee is preserved verbatim (see `change-feed.ts`).\n *\n * Failures are returned as data, like the append helper, so a drain problem\n * never aborts the caller's transaction. A failed drain leaves the staged rows\n * in place for the next attempt.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(\n p_limit INTEGER DEFAULT ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}\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-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\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 — 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 — 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, ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}), 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`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\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-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\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 — 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`;\n\nconst POSTGRES_CHANGE_FEED_PENDING_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_pending_ddl$\n${statement}\n$smrt_change_feed_pending_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helpers only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n *\n * The staging table and the drain helper (#2649) are created unconditionally\n * (`IF NOT EXISTS` / `CREATE OR REPLACE`) because an install that already has\n * the append helper from an older SMRT may still be missing them; the append\n * helper itself is replaced when the staging table was absent, which is how an\n * upgraded database picks up the deferring version.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Framework-owned tables that are backed by `@smrt()` models rather than by\n * {@link ALL_SYSTEM_TABLES}, and whose write volume disqualifies them from the\n * change feed.\n *\n * This is the jobs runner's own state: the claim loop and the forge projection\n * poll every second and rewrite these rows several times per job, so feeding\n * them into an append-only log whose pruning is not scheduled (issue #2375)\n * would dominate it with data no client syncs.\n *\n * The list is deliberately short and biased toward *observing*. A table wrongly\n * excluded is an invisible bug — exactly the one issue #2376 fixes — while a\n * table wrongly included costs a few extra feed rows. Everything else keeping\n * the `_smrt_` prefix (feature flags, prompt overrides, subscription plans,\n * field policies, agent and report schedules, report state) is observed.\n */\nexport const FRAMEWORK_OPERATIONAL_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n '_smrt_workers',\n '_smrt_forge_deliveries',\n '_smrt_forge_projection_checkpoints',\n];\n\n/**\n * Framework tables that are backed by `@smrt()` models *and* reshaped by\n * `system/compatibility.ts`.\n *\n * They are dual-owned: `db:migrate` creates them from the jobs manifest, and\n * the compatibility pass adds columns/indexes that predate the manifest. On a\n * fresh install the manifest wins the race — bootstrap runs before the tables\n * exist — so their compatibility pass is deferred and re-checked until the\n * tables show up (issue #2376).\n */\nexport const DEFERRED_COMPATIBILITY_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n];\n\n/**\n * System tables SMRT used to create on every database and no longer does.\n *\n * `_smrt_signals` never had a writer and `_smrt_registry` was only written by\n * an `ObjectRegistry.persistToDatabase()` helper that nothing ever called, so\n * both were empty on every deployment. New databases no longer get them.\n * Existing databases keep the empty tables until an operator drops them:\n *\n * ```sql\n * DROP TABLE IF EXISTS _smrt_signals;\n * DROP TABLE IF EXISTS _smrt_registry;\n * ```\n *\n * The framework deliberately does not issue those drops itself — dropping a\n * table is not reversible and a deployment may have repurposed the name.\n */\nexport const RETIRED_SYSTEM_TABLES: readonly string[] = [\n '_smrt_registry',\n '_smrt_signals',\n];\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.\n * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching\n * application-table DDL; SQLite keeps its variable-width INTEGER.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n const integerDdl =\n engine === 'postgres' || engine === 'duckdb' || engine === 'json'\n ? ddl.replace(/\\bINTEGER\\b/g, 'BIGINT')\n : ddl;\n return engine === 'postgres'\n ? integerDdl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ')\n : integerDdl;\n}\n\n/**\n * Current SMRT system schema version.\n *\n * `bootstrapSystemTables()` skips replaying system DDL once this exact version\n * is recorded in `_smrt_migrations`, so every change to {@link ALL_SYSTEM_TABLES}\n * must bump it — otherwise existing databases never see the change. New\n * *columns* additionally need an `addColumnIfMissing()` entry in\n * `system/compatibility.ts`, because `CREATE TABLE IF NOT EXISTS` is a no-op on\n * a table that already exists.\n *\n * 1.10.1 retires the never-written `_smrt_registry` / `_smrt_signals` tables\n * (see {@link RETIRED_SYSTEM_TABLES}) and adds the retention-predicate indexes\n * from #2375 (`_smrt_contexts.expires_at`, `_smrt_ai_usage(tenant_id,\n * created_at)`, `_smrt_dispatch(status, processed_at)` and\n * `(status, updated_at)`) in the same replay. (`1.10.0` briefly existed only\n * on two unreleased, unmerged branches with two different, incompatible DDL\n * sets — never published, never stamped in any real `_smrt_migrations` table\n * — so merging them reuses no version an existing install could have already\n * recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)\n */\nexport const SMRT_SCHEMA_VERSION = '1.10.1';\n\n/**\n * Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.\n *\n * Whitespace is normalized so reformatting alone does not trip the guard while\n * any token change does.\n */\nexport function getSystemSchemaChecksumInput(): string {\n return ALL_SYSTEM_TABLES.join('\\n').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * SHA-256 of {@link getSystemSchemaChecksumInput} for each system schema\n * version, oldest first.\n *\n * The discipline that keeps fresh installs and upgraded installs identical was\n * documented but unenforced (issue #2376): a contributor could edit the DDL and\n * ship it without bumping the version, and no existing database would ever\n * apply the change. `system-schema-evolution.test.ts` recomputes the current\n * hash and requires it to equal this map's entry for\n * {@link SMRT_SCHEMA_VERSION}.\n *\n * It is a *history*, not a single constant, so the guard cannot be silenced by\n * overwriting one value: every recorded hash must stay distinct, which means a\n * changed DDL has nowhere to be recorded except under a new version key. Adding\n * that key is the bump. This is also why merging two branches that each\n * independently bumped to the same version string (both `1.10.0`, covering two\n * different DDL sets — #2376's table retirement and #2375's retention indexes)\n * must land under a *new* key rather than picking a \"final\" checksum for the\n * shared number: nothing published or otherwise reachable ever stamped\n * `1.10.0`, so recording the merged DDL as `1.10.1` costs nothing and removes\n * any doubt about a version string meaning two different things to two\n * installs.\n *\n * Entries before 1.10.1 are not recorded — the guard starts here.\n */\nexport const SMRT_SCHEMA_DDL_CHECKSUMS: Readonly<Record<string, string>> =\n Object.freeze({\n '1.10.1':\n 'f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16',\n });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema.js","names":[],"sources":["../../src/system/schema.ts"],"sourcesContent":["/**\n * SMRT System Tables Schema\n *\n * System tables use _smrt_ prefix to avoid conflicts with user tables.\n * All system tables are created in the same database as user data.\n */\n\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\n\n/**\n * Context memory storage\n * Stores remembered context (learned strategies, patterns, selectors) for reuse\n */\nexport const CREATE_SMRT_CONTEXTS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_contexts (\n id TEXT PRIMARY KEY,\n owner_class TEXT NOT NULL,\n owner_id TEXT NOT NULL,\n scope TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT,\n metadata TEXT,\n version INTEGER DEFAULT 1,\n confidence REAL DEFAULT 1.0,\n success_count INTEGER DEFAULT 0,\n failure_count INTEGER DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_used_at TIMESTAMP,\n expires_at TIMESTAMP,\n UNIQUE(owner_class, owner_id, scope, key, version)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_owner\n ON _smrt_contexts(owner_class, owner_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_scope\n ON _smrt_contexts(scope);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_confidence\n ON _smrt_contexts(confidence);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_last_used\n ON _smrt_contexts(last_used_at);\n\n-- Retention predicate for pruneExpiredContexts() (#2375). Rows that never\n-- expire carry NULL here, so the index only has to cover the ones that do.\nCREATE INDEX IF NOT EXISTS idx_smrt_contexts_expires_at\n ON _smrt_contexts(expires_at);\n`;\n\n/**\n * Schema version tracking\n * Records which SMRT framework versions have been applied\n */\nexport const CREATE_SMRT_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_migrations (\n id TEXT PRIMARY KEY,\n version TEXT NOT NULL UNIQUE,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n checksum TEXT\n);\n`;\n\n/**\n * Schema migration tracking\n * Tracks applied schema migrations for idempotency, audit, and rollback\n */\nexport const CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_schema_migrations (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n version TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_checksum TEXT,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n execution_time_ms INTEGER,\n package_name TEXT,\n source_file TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n error_message TEXT,\n attempts INTEGER DEFAULT 0,\n is_reversible INTEGER DEFAULT 1,\n rolled_back_at TIMESTAMP,\n applied_by TEXT,\n batch INTEGER\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_status\n ON _smrt_schema_migrations(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_applied_at\n ON _smrt_schema_migrations(applied_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_schema_migrations_batch\n ON _smrt_schema_migrations(batch);\n`;\n\n/**\n * Embedding storage for semantic search\n * Stores embedding vectors for SMRT objects to enable vector similarity search\n */\nexport const CREATE_SMRT_EMBEDDINGS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_embeddings (\n id TEXT PRIMARY KEY,\n object_class TEXT NOT NULL,\n object_id TEXT NOT NULL,\n field_name TEXT NOT NULL,\n content_hash TEXT NOT NULL,\n embedding TEXT NOT NULL,\n model TEXT NOT NULL,\n dimensions INTEGER NOT NULL,\n provider TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(object_class, object_id, field_name, model)\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_object\n ON _smrt_embeddings(object_class, object_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_class\n ON _smrt_embeddings(object_class);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_hash\n ON _smrt_embeddings(content_hash);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_embeddings_model\n ON _smrt_embeddings(model);\n`;\n\n/**\n * Dispatch queue for inter-agent communication\n * Stores dispatch messages for asynchronous agent-to-agent signaling\n */\nexport const CREATE_SMRT_DISPATCH_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n source TEXT NOT NULL,\n source_id TEXT,\n payload TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER DEFAULT 0,\n last_error TEXT,\n processed_at TIMESTAMP,\n processed_by TEXT,\n target_subscriber TEXT,\n correlation_id TEXT,\n tenant_id TEXT,\n metadata TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status\n ON _smrt_dispatch(status);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_tenant_id\n ON _smrt_dispatch(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_type\n ON _smrt_dispatch(type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_source\n ON _smrt_dispatch(source);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_created\n ON _smrt_dispatch(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_target\n ON _smrt_dispatch(target_subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_correlation\n ON _smrt_dispatch(correlation_id);\n\n-- Retention predicates of DispatchCollection.cleanup() (#2375): completed rows\n-- are aged out on processed_at, failed rows on updated_at. The status-only\n-- index above cannot serve either range.\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_processed\n ON _smrt_dispatch(status, processed_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_status_updated\n ON _smrt_dispatch(status, updated_at);\n`;\n\n/**\n * Dispatch subscriptions for persistent handlers\n * Stores subscriptions to dispatch types for agent processing\n */\nexport const CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_dispatch_subscriptions (\n id TEXT PRIMARY KEY,\n signal_type TEXT NOT NULL,\n subscriber TEXT NOT NULL,\n handler TEXT NOT NULL DEFAULT 'handleDispatch',\n delivery TEXT NOT NULL DEFAULT 'compete',\n enabled INTEGER DEFAULT 1,\n tenant_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Subscription identity is tenant-scoped (S5 #1398): the same\n-- (signal_type, subscriber) pair may exist independently in different tenants,\n-- so tenant B can no longer overwrite/delete/enable tenant A's subscription.\n-- A named UNIQUE index (rather than an inline UNIQUE constraint) is used so the\n-- compatibility migration can additively reshape existing tables. NULL tenant_id\n-- (global subscriptions) is deduped at the application layer by the NULL-aware\n-- upsert in @happyvertical/sql.\nCREATE UNIQUE INDEX IF NOT EXISTS uq_smrt_dispatch_subs_tenant_signal_subscriber\n ON _smrt_dispatch_subscriptions(tenant_id, signal_type, subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_subscriber\n ON _smrt_dispatch_subscriptions(subscriber);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_tenant_id\n ON _smrt_dispatch_subscriptions(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_signal_type\n ON _smrt_dispatch_subscriptions(signal_type);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_dispatch_subs_enabled\n ON _smrt_dispatch_subscriptions(enabled);\n`;\n\n/**\n * AI usage telemetry storage\n * Stores normalized AI usage records for reporting and billing hooks\n */\nexport const CREATE_SMRT_AI_USAGE_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_ai_usage (\n id TEXT PRIMARY KEY,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n operation TEXT NOT NULL,\n prompt_tokens INTEGER,\n completion_tokens INTEGER,\n total_tokens INTEGER,\n estimated_cost REAL,\n duration INTEGER NOT NULL,\n class_name TEXT,\n tenant_id TEXT,\n tags TEXT,\n created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_created\n ON _smrt_ai_usage(created_at);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_class\n ON _smrt_ai_usage(class_name);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant\n ON _smrt_ai_usage(tenant_id);\n\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_provider_model\n ON _smrt_ai_usage(provider, model);\n\n-- Serves both the tenant-scoped retention predicate of pruneAiUsage() and the\n-- subscriptions billing meter's tenant_id + created_at range scan, neither of\n-- which the single-column indexes above can satisfy (#2375).\nCREATE INDEX IF NOT EXISTS idx_smrt_ai_usage_tenant_created\n ON _smrt_ai_usage(tenant_id, created_at);\n`;\n\n/**\n * Append-only change feed (issue #1758)\n *\n * One row per framework save/delete: monotonic per-database sequence,\n * table, row id, operation (create/update/delete — deletes are tombstones),\n * tenant, timestamp. Written by the change-feed interceptor and read\n * through `getChangesSince()`.\n *\n * `seq` is deliberately a plain BIGINT PRIMARY KEY rather than a native\n * AUTOINCREMENT/identity/serial column: the appender allocates\n * `COALESCE(MAX(seq), 0) + 1` inside the INSERT (with a conflict retry),\n * which keeps committed sequences contiguous so commit order equals\n * sequence order on every engine. Native identity columns allocate before\n * commit, so under concurrent writers on MVCC engines a reader could\n * observe seq N+1 while seq N is still uncommitted and advance its cursor\n * past it — breaking the feed's no-missed-changes cursor guarantee. The\n * plain column also keeps this DDL portable across SQLite, Postgres and\n * DuckDB with no per-engine branching. See `change-feed.ts`.\n *\n * `row_id` is nullable: manual bumps (`bumpChangeFeed`) may record a\n * table-level change without a specific row.\n */\nexport 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`;\n\n/**\n * PostgreSQL-only staging table for change-feed appends made inside a\n * caller-managed transaction (issue #2649).\n *\n * `_smrt_changes.seq` is allocated `COALESCE(MAX(seq), 0) + 1`, so two\n * concurrent appends contend on the same primary-key value and the loser\n * *waits for the winner's transaction to end*. When the winner is a long write\n * transaction that goes on to take row locks the loser already holds, that\n * wait closes a genuine lock cycle and PostgreSQL aborts one side with\n * `40P01` — an ordinary concurrent request killing a legitimate long write.\n *\n * The staging table breaks the cycle by removing the wait: an append issued\n * inside a caller transaction inserts here instead, where its identity value\n * conflicts with nothing and it never waits on another transaction. The row is\n * still fate-shared with the caller (a rollback removes it, exactly as\n * before). A later *drain* — {@link CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION}\n * — moves committed staged rows into `_smrt_changes` with contiguous\n * sequences, so the feed's cursor guarantee is unchanged.\n *\n * PostgreSQL-only on purpose: SQLite and DuckDB do not reach the defect\n * (SQLite serializes writers outright), and keeping the portable DDL untouched\n * keeps the embedded path byte-identical.\n */\nexport 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`;\n\n/** Name of the PostgreSQL staging table for deferred change-feed appends. */\nexport const POSTGRES_CHANGE_FEED_PENDING_TABLE = '_smrt_changes_pending';\n\n/** PostgreSQL materialization of the portable change-feed table DDL. */\nexport const CREATE_POSTGRES_SMRT_CHANGES_TABLE =\n CREATE_SMRT_CHANGES_TABLE.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ');\n\n/** PostgreSQL helper used to isolate best-effort feed appends (#2026). */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME = '_smrt_append_change';\n\n/** PostgreSQL one-statement batch wrapper around the isolated append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME =\n '_smrt_append_changes';\n\n/** Legacy helper identity used before PostgreSQL Date values became instants. */\nexport const LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp without time zone)`;\n\n/** Exact PostgreSQL identity used for catalog lookup of the append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(text,text,text,text,timestamp with time zone)`;\n\n/** PostgreSQL identity of the one-statement batch append helper. */\nexport const POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(jsonb)`;\n\n/**\n * Body marker stamped into both PostgreSQL change-feed helpers (#2649).\n *\n * Existence is not currency: an install can hold a helper of the right name\n * and signature whose *body* predates a fix. The probe in `change-feed.ts`\n * matches this token against `pg_proc.prosrc`, so a stale body is detected and\n * replaced. Bump it whenever either helper's body changes in a way an existing\n * database must pick up.\n */\nexport const POSTGRES_CHANGE_FEED_HELPER_MARKER = 'smrt-change-feed-helpers:v4';\n\n/** PostgreSQL helper that sequences staged change-feed appends (#2649). */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME = '_smrt_drain_changes';\n\n/** Exact PostgreSQL identity used for catalog lookup of the drain helper. */\nexport const POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_IDENTITY = `${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(integer)`;\n\n/** How many staged entries one drain call sequences. */\nexport const POSTGRES_CHANGE_FEED_DRAIN_BATCH = 1000;\n\n/**\n * PostgreSQL-only drain: move committed staged appends into `_smrt_changes`.\n *\n * This is where the feed's sequence is actually allocated for entries written\n * inside a caller transaction. It runs in its own short transaction (an\n * autocommit append's, or a reader's `drainChangeFeed()` call), holds no user\n * row locks, and never waits on another transaction:\n *\n * - it only ever sees *committed* staged rows — an in-flight transaction's\n * staged row is invisible under MVCC, so the drain neither reads nor locks\n * it, and it is picked up by a later drain once that transaction commits;\n * - concurrent drains are excluded by a `pg_try_advisory_xact_lock`, which\n * *skips* rather than waits, so a drain can never become an edge in a lock\n * cycle either.\n *\n * Because exactly one drain runs at a time and it numbers rows\n * `COALESCE(MAX(seq), 0) + row_number()` in staged order, committed sequences\n * stay contiguous and no reader can observe seq N before seq N-1 — the change\n * feed's cursor guarantee is preserved verbatim (see `change-feed.ts`).\n *\n * Failures are returned as data, like the append helper, so a drain problem\n * never aborts the caller's transaction. A failed drain leaves the staged rows\n * in place for the next attempt.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_DRAIN_FUNCTION_NAME}(\n p_limit INTEGER DEFAULT ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}\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-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\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 — 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 — 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, ${POSTGRES_CHANGE_FEED_DRAIN_BATCH}), 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`;\n\n/**\n * PostgreSQL-only change-feed append function.\n *\n * A PL/pgSQL block with an EXCEPTION handler runs its body in an internal\n * subtransaction. Returning SQLSTATE as data lets the caller log or retry a\n * failed best-effort append without leaving its surrounding transaction in\n * PostgreSQL's aborted (25P02) state. This statement contains dollar-quoted\n * semicolons, so callers must execute it whole rather than adding it to\n * {@link ALL_SYSTEM_TABLES}, whose portable DDL entries are semicolon-split.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_NAME}(\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-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\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 — 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`;\n\n/**\n * PostgreSQL batch append helper. It makes the staged/direct decision before\n * its own writes, then appends every element through that one path. This is\n * necessary because a direct first insert assigns the transaction id that\n * later entries must not mistake for a caller-owned write transaction.\n */\nexport const CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION = `\nCREATE OR REPLACE FUNCTION ${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_NAME}(\n p_entries JSONB\n)\nRETURNS TABLE(\n entry_index INTEGER,\n allocated_seq BIGINT,\n error_code TEXT,\n error_message TEXT\n)\nLANGUAGE plpgsql\nSECURITY INVOKER\nAS $smrt_change_feed_batch$\n-- ${POSTGRES_CHANGE_FEED_HELPER_MARKER}\nDECLARE\n v_deferred BOOLEAN;\n v_error_code TEXT;\n v_error_message TEXT;\nBEGIN\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\n IF v_deferred THEN\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), staged AS (\n INSERT INTO _smrt_changes_pending (\n table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n input.value->>'table',\n input.value->>'rowId',\n input.value->>'operation',\n input.value->>'tenantId',\n (input.value->>'timestamp')::TIMESTAMPTZ\n FROM input\n ORDER BY input.entry_index\n RETURNING 1\n )\n SELECT input.entry_index, NULL::BIGINT, NULL::TEXT, NULL::TEXT\n FROM input\n ORDER BY input.entry_index;\n RETURN;\n END IF;\n\n RETURN QUERY\n WITH input AS (\n SELECT entries.ordinality::INTEGER - 1 AS entry_index, entries.value\n FROM jsonb_array_elements(COALESCE(p_entries, '[]'::jsonb))\n WITH ORDINALITY AS entries(value, ordinality)\n ), numbered AS (\n SELECT\n input.*,\n COALESCE((SELECT MAX(seq) FROM _smrt_changes), 0)\n + row_number() OVER (ORDER BY input.entry_index) AS new_seq\n FROM input\n ), inserted AS (\n INSERT INTO _smrt_changes (\n seq, table_name, row_id, operation, tenant_id, created_at\n )\n SELECT\n numbered.new_seq,\n numbered.value->>'table',\n numbered.value->>'rowId',\n numbered.value->>'operation',\n numbered.value->>'tenantId',\n (numbered.value->>'timestamp')::TIMESTAMPTZ\n FROM numbered\n ORDER BY numbered.entry_index\n RETURNING seq\n )\n SELECT\n numbered.entry_index,\n numbered.new_seq,\n NULL::TEXT,\n NULL::TEXT\n FROM numbered\n ORDER BY numbered.entry_index;\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::INTEGER, NULL::BIGINT, v_error_code, v_error_message;\n END;\nEND;\n$smrt_change_feed_batch$;\n`;\n\nconst POSTGRES_CHANGE_FEED_PENDING_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_PENDING_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_pending_ddl$\n${statement}\n$smrt_change_feed_pending_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Serialize PostgreSQL helper replacement inside one server-side statement.\n *\n * The transaction-scoped advisory lock prevents concurrent bootstraps from\n * racing on PostgreSQL's `pg_proc` uniqueness constraint. The nested dollar\n * quote keeps the complete function DDL atomic from the client's perspective.\n */\nexport const REPLACE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_replace_change_feed$\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\nEND;\n$smrt_replace_change_feed$;\n`;\n\n/**\n * Install the PostgreSQL helpers only when missing, serialized server-side.\n *\n * A client-side catalog probe remains the fast path for already-initialized\n * read handles. This guarded statement is the cold-path race boundary: both\n * the advisory lock and the post-lock catalog check run before function DDL.\n *\n * The staging table and the drain helper (#2649) are created unconditionally\n * (`IF NOT EXISTS` / `CREATE OR REPLACE`) because an install that already has\n * the append helper from an older SMRT may still be missing them; the append\n * helper itself is replaced when the staging table was absent, which is how an\n * upgraded database picks up the deferring version.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION = `\nDO $smrt_ensure_change_feed$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n ) OR NOT EXISTS (\n SELECT 1 FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed$;\n`;\n\nconst POSTGRES_CHANGE_FEED_SCHEMA_DDL =\n CREATE_POSTGRES_SMRT_CHANGES_TABLE.split(';')\n .map((statement) => statement.trim())\n .filter((statement) => statement.length > 0)\n .map(\n (statement) => `EXECUTE $smrt_change_feed_schema_ddl$\n${statement}\n$smrt_change_feed_schema_ddl$;`,\n )\n .join('\\n');\n\n/**\n * Install the complete PostgreSQL change-feed schema under the bootstrap lock.\n *\n * The lock is deliberately acquired before table or index DDL. Framework\n * bootstrap uses the same ordering, so a raw-handle cold start cannot retain\n * catalog locks while waiting behind a framework bootstrap transaction.\n */\nexport const ENSURE_POSTGRES_CHANGE_FEED_SCHEMA = `\nDO $smrt_ensure_change_feed_schema$\nDECLARE\n v_append_stale BOOLEAN;\nBEGIN\n PERFORM pg_advisory_xact_lock(\n hashtext('smrt'),\n hashtext('system-tables')\n );\n v_append_stale := to_regclass('${POSTGRES_CHANGE_FEED_PENDING_TABLE}') IS NULL\n OR NOT EXISTS (\n SELECT 1\n FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n ) OR NOT EXISTS (\n SELECT 1 FROM pg_proc\n WHERE oid = to_regprocedure('${POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION_IDENTITY}')\n AND prosrc LIKE '%${POSTGRES_CHANGE_FEED_HELPER_MARKER}%'\n );\n${POSTGRES_CHANGE_FEED_SCHEMA_DDL}\n${POSTGRES_CHANGE_FEED_PENDING_DDL}\n DROP FUNCTION IF EXISTS ${LEGACY_POSTGRES_CHANGE_FEED_APPEND_FUNCTION_IDENTITY};\n EXECUTE $smrt_change_feed_drain_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_DRAIN_FUNCTION.trim()}\n$smrt_change_feed_drain_ddl$;\n IF v_append_stale THEN\n EXECUTE $smrt_change_feed_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_FUNCTION.trim()}\n$smrt_change_feed_ddl$;\n EXECUTE $smrt_change_feed_batch_ddl$\n${CREATE_POSTGRES_CHANGE_FEED_APPEND_BATCH_FUNCTION.trim()}\n$smrt_change_feed_batch_ddl$;\n END IF;\nEND;\n$smrt_ensure_change_feed_schema$;\n`;\n\n/**\n * Data backfill tracking\n *\n * Distinct from `_smrt_schema_migrations` — backfills are app-specific\n * data corrections (slug rewrites, model splits, lookup-table seeds) that\n * don't have schema diffs or rollback semantics. Apps register backfills\n * by name and the tracker handles idempotency.\n */\nexport const CREATE_SMRT_BACKFILLS_TABLE = `\nCREATE TABLE IF NOT EXISTS _smrt_backfills (\n name TEXT PRIMARY KEY,\n applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n description TEXT,\n package_name TEXT\n);\n`;\n\n/**\n * All system table creation statements\n */\nexport const ALL_SYSTEM_TABLES = [\n CREATE_SMRT_CONTEXTS_TABLE,\n CREATE_SMRT_MIGRATIONS_TABLE,\n CREATE_SMRT_SCHEMA_MIGRATIONS_TABLE,\n CREATE_SMRT_BACKFILLS_TABLE,\n CREATE_SMRT_EMBEDDINGS_TABLE,\n CREATE_SMRT_DISPATCH_TABLE,\n CREATE_SMRT_DISPATCH_SUBSCRIPTIONS_TABLE,\n CREATE_SMRT_AI_USAGE_TABLE,\n CREATE_SMRT_CHANGES_TABLE,\n];\n\n/**\n * Framework-owned tables that are backed by `@smrt()` models rather than by\n * {@link ALL_SYSTEM_TABLES}, and whose write volume disqualifies them from the\n * change feed.\n *\n * This is the jobs runner's own state: the claim loop and the forge projection\n * poll every second and rewrite these rows several times per job, so feeding\n * them into an append-only log whose pruning is not scheduled (issue #2375)\n * would dominate it with data no client syncs.\n *\n * The list is deliberately short and biased toward *observing*. A table wrongly\n * excluded is an invisible bug — exactly the one issue #2376 fixes — while a\n * table wrongly included costs a few extra feed rows. Everything else keeping\n * the `_smrt_` prefix (feature flags, prompt overrides, subscription plans,\n * field policies, agent and report schedules, report state) is observed.\n */\nexport const FRAMEWORK_OPERATIONAL_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n '_smrt_workers',\n '_smrt_forge_deliveries',\n '_smrt_forge_projection_checkpoints',\n];\n\n/**\n * Framework tables that are backed by `@smrt()` models *and* reshaped by\n * `system/compatibility.ts`.\n *\n * They are dual-owned: `db:migrate` creates them from the jobs manifest, and\n * the compatibility pass adds columns/indexes that predate the manifest. On a\n * fresh install the manifest wins the race — bootstrap runs before the tables\n * exist — so their compatibility pass is deferred and re-checked until the\n * tables show up (issue #2376).\n */\nexport const DEFERRED_COMPATIBILITY_TABLES: readonly string[] = [\n '_smrt_jobs',\n '_smrt_job_events',\n];\n\n/**\n * System tables SMRT used to create on every database and no longer does.\n *\n * `_smrt_signals` never had a writer and `_smrt_registry` was only written by\n * an `ObjectRegistry.persistToDatabase()` helper that nothing ever called, so\n * both were empty on every deployment. New databases no longer get them.\n * Existing databases keep the empty tables until an operator drops them:\n *\n * ```sql\n * DROP TABLE IF EXISTS _smrt_signals;\n * DROP TABLE IF EXISTS _smrt_registry;\n * ```\n *\n * The framework deliberately does not issue those drops itself — dropping a\n * table is not reversible and a deployment may have repurposed the name.\n */\nexport const RETIRED_SYSTEM_TABLES: readonly string[] = [\n '_smrt_registry',\n '_smrt_signals',\n];\n\n/**\n * Materialize the portable system-table schema for a target engine.\n *\n * PostgreSQL must use TIMESTAMPTZ because every system Date is an instant.\n * PostgreSQL and DuckDB also materialize integer storage as BIGINT, matching\n * application-table DDL; SQLite keeps its variable-width INTEGER.\n */\nexport function getSystemTableDDL(engine: DatabaseEngine): string[] {\n return ALL_SYSTEM_TABLES.map((ddl) =>\n getSystemTableDDLForEngine(ddl, engine),\n );\n}\n\n/** Materialize one portable system-table statement for a target engine. */\nexport function getSystemTableDDLForEngine(\n ddl: string,\n engine: DatabaseEngine,\n): string {\n const integerDdl =\n engine === 'postgres' || engine === 'duckdb' || engine === 'json'\n ? ddl.replace(/\\bINTEGER\\b/g, 'BIGINT')\n : ddl;\n return engine === 'postgres'\n ? integerDdl.replace(/\\bTIMESTAMP\\b/g, 'TIMESTAMPTZ')\n : integerDdl;\n}\n\n/**\n * Current SMRT system schema version.\n *\n * `bootstrapSystemTables()` skips replaying system DDL once this exact version\n * is recorded in `_smrt_migrations`, so every change to {@link ALL_SYSTEM_TABLES}\n * must bump it — otherwise existing databases never see the change. New\n * *columns* additionally need an `addColumnIfMissing()` entry in\n * `system/compatibility.ts`, because `CREATE TABLE IF NOT EXISTS` is a no-op on\n * a table that already exists.\n *\n * 1.10.1 retires the never-written `_smrt_registry` / `_smrt_signals` tables\n * (see {@link RETIRED_SYSTEM_TABLES}) and adds the retention-predicate indexes\n * from #2375 (`_smrt_contexts.expires_at`, `_smrt_ai_usage(tenant_id,\n * created_at)`, `_smrt_dispatch(status, processed_at)` and\n * `(status, updated_at)`) in the same replay. (`1.10.0` briefly existed only\n * on two unreleased, unmerged branches with two different, incompatible DDL\n * sets — never published, never stamped in any real `_smrt_migrations` table\n * — so merging them reuses no version an existing install could have already\n * recorded; see the note on {@link SMRT_SCHEMA_DDL_CHECKSUMS}.)\n */\nexport const SMRT_SCHEMA_VERSION = '1.10.1';\n\n/**\n * Canonical form of the system DDL that {@link SMRT_SCHEMA_DDL_CHECKSUMS} covers.\n *\n * Whitespace is normalized so reformatting alone does not trip the guard while\n * any token change does.\n */\nexport function getSystemSchemaChecksumInput(): string {\n return ALL_SYSTEM_TABLES.join('\\n').replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * SHA-256 of {@link getSystemSchemaChecksumInput} for each system schema\n * version, oldest first.\n *\n * The discipline that keeps fresh installs and upgraded installs identical was\n * documented but unenforced (issue #2376): a contributor could edit the DDL and\n * ship it without bumping the version, and no existing database would ever\n * apply the change. `system-schema-evolution.test.ts` recomputes the current\n * hash and requires it to equal this map's entry for\n * {@link SMRT_SCHEMA_VERSION}.\n *\n * It is a *history*, not a single constant, so the guard cannot be silenced by\n * overwriting one value: every recorded hash must stay distinct, which means a\n * changed DDL has nowhere to be recorded except under a new version key. Adding\n * that key is the bump. This is also why merging two branches that each\n * independently bumped to the same version string (both `1.10.0`, covering two\n * different DDL sets — #2376's table retirement and #2375's retention indexes)\n * must land under a *new* key rather than picking a \"final\" checksum for the\n * shared number: nothing published or otherwise reachable ever stamped\n * `1.10.0`, so recording the merged DDL as `1.10.1` costs nothing and removes\n * any doubt about a version string meaning two different things to two\n * installs.\n *\n * Entries before 1.10.1 are not recorded — the guard starts here.\n */\nexport const SMRT_SCHEMA_DDL_CHECKSUMS: Readonly<Record<string, string>> =\n Object.freeze({\n '1.10.1':\n 'f796ee3b3f7ab8b9dc659ecaa68884ec01277408c3dd6edea540853fce369c16',\n });\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client-entries.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/api-client-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"api-client-entries.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/api-client-entries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EACV,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,qBAAqB,CAAC;AAS7B,MAAM,MAAM,mBAAmB,GAC3B,MAAM,GACN,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAEb,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,GAAG,EAAE,qBAAqB,CAAC;IAC3B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,sEAAsE;IACtE,aAAa,EAAE,KAAK,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;QAC7B,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,UAAU,EAAE,KAAK,CAAC;YAChB,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,QAAQ,CAAC,EAAE,OAAO,CAAC;YACnB,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC,CAAC;KACJ,CAAC,CAAC;CACJ;AAED,wBAAgB,uBAAuB,CACrC,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,mBAAmB,EAAE,EAClC,iBAAiB,GAAE,MAAM,EAAO,GAC/B,MAAM,GAAG,SAAS,CAepB;AAED,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,cAAc,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,EAC/C,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAChC,MAAM,CAyCR;AA8SD;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,mBAAmB,GAC5B,cAAc,EAAE,CAwElB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CRUD_OPERATIONS, createManifestClassNamePredicate, resolveApiMethodExposure } from "../generators/custom-action.js";
|
|
2
2
|
import { resolveApiActionRouteConfig } from "./sveltekit-generator.js";
|
|
3
|
-
import { findManifestObjectByName, isCollectionManifestClass, resolveCollectionItemObject } from "./web-collections.js";
|
|
3
|
+
import { findManifestObjectByName, isCollectionManifestClass, orderedManifestObjectEntries, resolveCollectionItemObject } from "./web-collections.js";
|
|
4
4
|
//#region src/vite-plugin/api-client-entries.ts
|
|
5
5
|
/**
|
|
6
6
|
* Manifest -> generated API-client entry selection.
|
|
@@ -95,7 +95,7 @@ function resolveCrudObject(obj, manifest) {
|
|
|
95
95
|
if (!isCollectionManifestClass(manifest, obj)) return obj;
|
|
96
96
|
const itemObject = resolveCollectionItemObject(manifest, obj);
|
|
97
97
|
if (itemObject) return itemObject;
|
|
98
|
-
return
|
|
98
|
+
return orderedManifestObjectEntries(manifest).filter(([, candidate]) => candidate.collection === obj.collection && !isCollectionManifestClass(manifest, candidate)).map(([, candidate]) => candidate)[0];
|
|
99
99
|
}
|
|
100
100
|
function resolveGeneratedClientCrudMethods(obj, manifest) {
|
|
101
101
|
const crudObject = resolveCrudObject(obj, manifest);
|
|
@@ -112,15 +112,17 @@ function resolveGeneratedClientCrudMethods(obj, manifest) {
|
|
|
112
112
|
* Resolve the CRUD surface that the generated route files actually expose for
|
|
113
113
|
* one shared collection endpoint.
|
|
114
114
|
*
|
|
115
|
-
* Route generation processes manifest objects in
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
115
|
+
* Route generation processes manifest objects in the deterministic
|
|
116
|
+
* qualified-identity order of {@link orderedManifestObjectEntries} (#2754):
|
|
117
|
+
* collection and item handlers live in separate files, and the last model
|
|
118
|
+
* that emits each file replaces the earlier file. Mirror that behavior here
|
|
119
|
+
* by iterating the same order, so every client alias for a shared endpoint
|
|
120
|
+
* has the same, real action set regardless of manifest scan order.
|
|
119
121
|
*/
|
|
120
122
|
function resolveGeneratedEndpointCrudMethods(collection, manifest) {
|
|
121
123
|
let collectionMethods = [];
|
|
122
124
|
let itemMethods = [];
|
|
123
|
-
for (const obj of
|
|
125
|
+
for (const [, obj] of orderedManifestObjectEntries(manifest)) {
|
|
124
126
|
if (obj.collection !== collection || isCollectionManifestClass(manifest, obj)) continue;
|
|
125
127
|
const methods = resolveGeneratedClientCrudMethods(obj, manifest);
|
|
126
128
|
const emittedCollectionMethods = methods.filter((method) => [
|