@rebasepro/server-postgres 0.13.1-canary.gf57a27e → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBootstrapper.d.ts +26 -0
- package/dist/auth/services.d.ts +21 -0
- package/dist/{auth-users-columns-Dt9g712t.js → auth-users-columns-BfQHf9JE.js} +525 -63
- package/dist/auth-users-columns-BfQHf9JE.js.map +1 -0
- package/dist/{backup-service-Bww-Lg0s.js → backup-service-BH0Dzo_h.js} +2 -3
- package/dist/{backup-service-Bww-Lg0s.js.map → backup-service-BH0Dzo_h.js.map} +1 -1
- package/dist/cli-output.d.ts +34 -0
- package/dist/data-transformer.d.ts +7 -2
- package/dist/data_driver-ULAyJEi9.js +193 -0
- package/dist/data_driver-ULAyJEi9.js.map +1 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js +124 -0
- package/dist/ensure-collection-policies-8vuu-n4r.js.map +1 -0
- package/dist/{ensure-collection-tables-DRxaUG96.js → ensure-collection-tables-CbvaGuVn.js} +89 -10
- package/dist/ensure-collection-tables-CbvaGuVn.js.map +1 -0
- package/dist/index.es.js +1310 -1060
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js → rls-bootstrap-sql-69hYT8nr.js} +2 -2
- package/dist/{rls-bootstrap-sql-Bpv3nUZo.js.map → rls-bootstrap-sql-69hYT8nr.js.map} +1 -1
- package/dist/rls-enforcement-BJ_3wxwg.js +425 -0
- package/dist/rls-enforcement-BJ_3wxwg.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +102 -0
- package/dist/schema/doctor-policy-checks.d.ts +28 -0
- package/dist/schema/doctor.d.ts +41 -25
- package/dist/schema/ensure-collection-policies.d.ts +33 -9
- package/dist/schema/ensure-collection-tables.d.ts +60 -6
- package/dist/schema/generate-drizzle-schema-logic.d.ts +9 -1
- package/dist/schema/introspect-db-inference.d.ts +8 -1
- package/dist/schema/introspect-db-logic.d.ts +49 -0
- package/dist/schema/introspect-db-project.d.ts +21 -0
- package/dist/schema/search-column.d.ts +49 -0
- package/dist/security/policy-drift.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +8 -3
- package/dist/services/FetchService.d.ts +9 -0
- package/dist/services/PersistService.d.ts +21 -17
- package/dist/services/RelationService.d.ts +9 -57
- package/dist/services/RelationWriteService.d.ts +82 -0
- package/dist/services/collection-helpers.d.ts +42 -0
- package/dist/services/dataService.d.ts +2 -0
- package/dist/services/junction-writes.d.ts +82 -0
- package/dist/services/realtimeService.d.ts +137 -2
- package/dist/services/write-denial.d.ts +36 -0
- package/dist/{src-C_wvdMnl.js → src-DCdn3Val.js} +35 -3
- package/dist/src-DCdn3Val.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +54 -1
- package/dist/{websocket-D0TBU3ia.js → websocket-C8ZqVBiV.js} +75 -18
- package/dist/websocket-C8ZqVBiV.js.map +1 -0
- package/package.json +6 -6
- package/src/PostgresBackendDriver.ts +7 -3
- package/src/PostgresBootstrapper.ts +95 -9
- package/src/auth/ensure-tables.ts +27 -5
- package/src/auth/services.ts +82 -5
- package/src/backup/backup-cli.ts +59 -57
- package/src/cli-errors.ts +6 -6
- package/src/cli-helpers.ts +4 -4
- package/src/cli-output.ts +43 -0
- package/src/cli.ts +155 -147
- package/src/collections/buildRegistry.ts +3 -1
- package/src/data-transformer.ts +111 -25
- package/src/history/ensure-history-table.ts +2 -2
- package/src/schema/auth-schema.ts +17 -1
- package/src/schema/doctor-cli.ts +14 -65
- package/src/schema/doctor-policy-checks.ts +105 -0
- package/src/schema/doctor.ts +149 -72
- package/src/schema/ensure-collection-policies.ts +99 -6
- package/src/schema/ensure-collection-tables.ts +214 -17
- package/src/schema/generate-drizzle-schema-logic.ts +121 -65
- package/src/schema/generate-drizzle-schema.ts +11 -10
- package/src/schema/generate-postgres-ddl-logic.ts +28 -1
- package/src/schema/generate-postgres-ddl.ts +14 -13
- package/src/schema/generated-schema-staleness.ts +7 -5
- package/src/schema/introspect-db-inference.ts +9 -2
- package/src/schema/introspect-db-logic.ts +251 -75
- package/src/schema/introspect-db-project.ts +78 -0
- package/src/schema/introspect-db.ts +42 -25
- package/src/schema/introspect-runtime.ts +14 -2
- package/src/schema/search-column.ts +85 -0
- package/src/security/policy-drift.test.ts +104 -3
- package/src/security/policy-drift.ts +129 -7
- package/src/security/rls-enforcement.ts +9 -4
- package/src/services/FetchService.ts +105 -7
- package/src/services/PersistService.ts +68 -42
- package/src/services/RelationService.ts +35 -695
- package/src/services/RelationWriteService.ts +653 -0
- package/src/services/cdc/trigger-cdc.ts +5 -1
- package/src/services/channel-history.ts +9 -3
- package/src/services/channel-presence.ts +10 -3
- package/src/services/collection-helpers.ts +89 -4
- package/src/services/dataService.ts +2 -0
- package/src/services/junction-writes.ts +295 -0
- package/src/services/pg-notify-listener.ts +1 -1
- package/src/services/realtimeService.ts +337 -82
- package/src/services/write-denial.ts +55 -0
- package/src/utils/drizzle-conditions.ts +211 -34
- package/src/utils/pg-error-utils.ts +8 -3
- package/src/websocket.ts +113 -16
- package/dist/auth-users-columns-Dt9g712t.js.map +0 -1
- package/dist/ensure-collection-policies-CwYUliAa.js +0 -57
- package/dist/ensure-collection-policies-CwYUliAa.js.map +0 -1
- package/dist/ensure-collection-tables-DRxaUG96.js.map +0 -1
- package/dist/policy-CPkCqVTz.js +0 -105
- package/dist/policy-CPkCqVTz.js.map +0 -1
- package/dist/src-C_wvdMnl.js.map +0 -1
- package/dist/websocket-D0TBU3ia.js.map +0 -1
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
searchExtensionStatements,
|
|
35
35
|
searchHelperFunctions,
|
|
36
36
|
searchIndexStatements,
|
|
37
|
+
searchColumnStamps,
|
|
38
|
+
SEARCH_STAMP_PREFIX,
|
|
37
39
|
SEARCH_TEXT_FN,
|
|
38
40
|
SEARCH_UNACCENT_FN,
|
|
39
41
|
type SearchColumnSpec
|
|
@@ -90,11 +92,19 @@ export interface ExistingSchema {
|
|
|
90
92
|
* constraint that then fails harmlessly as a duplicate.
|
|
91
93
|
*/
|
|
92
94
|
constraints?: Set<string>;
|
|
95
|
+
/**
|
|
96
|
+
* `schema.table.column` → that column's comment, for the columns that have
|
|
97
|
+
* one. This is where a generated search column's fingerprint lives, so it
|
|
98
|
+
* is the only evidence that a `search` block has changed since the column
|
|
99
|
+
* was built. Absent is read as "no column is stamped", which plans a stamp
|
|
100
|
+
* and reports nothing as drifted.
|
|
101
|
+
*/
|
|
102
|
+
columnComments?: Map<string, string>;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
export interface EnsureAction {
|
|
96
106
|
kind: "create-enum" | "create-table" | "add-column" | "add-constraint" | "rename-column"
|
|
97
|
-
| "create-extension" | "create-function" | "create-index";
|
|
107
|
+
| "create-extension" | "create-function" | "create-index" | "comment-column";
|
|
98
108
|
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
99
109
|
target: string;
|
|
100
110
|
sql: string;
|
|
@@ -115,6 +125,47 @@ export interface EnsurePlan {
|
|
|
115
125
|
* resolving to nothing — which is indistinguishable from having no data.
|
|
116
126
|
*/
|
|
117
127
|
legacyForeignKeys: LegacyForeignKey[];
|
|
128
|
+
/**
|
|
129
|
+
* Generated search columns whose `search` block has changed since they were
|
|
130
|
+
* built. Reported, never planned into `actions` — see
|
|
131
|
+
* {@link SearchColumnDrift} for why applying it is not this path's call.
|
|
132
|
+
*/
|
|
133
|
+
searchDrift: SearchColumnDrift[];
|
|
134
|
+
/**
|
|
135
|
+
* Generated search columns that exist but carry no fingerprint — created
|
|
136
|
+
* before this check existed, or by `search.sql` on an older CLI. The plan
|
|
137
|
+
* stamps them so the *next* change is detectable; whether they match the
|
|
138
|
+
* current block cannot be known, which is what the caller reports.
|
|
139
|
+
*/
|
|
140
|
+
searchAdopted: { table: string; column: string }[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A generated search column built from a `search` block that has since changed.
|
|
145
|
+
*
|
|
146
|
+
* Reported instead of applied because the two ways to apply it are both worse
|
|
147
|
+
* than stopping. `ALTER COLUMN … SET EXPRESSION` exists only on PG17+ and
|
|
148
|
+
* rewrites the table either way; `DROP COLUMN` + `ADD COLUMN` rewrites it under
|
|
149
|
+
* an ACCESS EXCLUSIVE lock and rebuilds the GIN index. This module runs
|
|
150
|
+
* unattended against live customer data with nobody reading a diff — the same
|
|
151
|
+
* reason it withholds `SET NOT NULL` from an adopted table — so a multi-minute
|
|
152
|
+
* outage is not a decision it may take on its own.
|
|
153
|
+
*
|
|
154
|
+
* Not applying it silently is not an option either: that is the bug this
|
|
155
|
+
* detection exists for. A collection that added a field, flipped `unaccent` or
|
|
156
|
+
* raised a weight kept indexing the *old* set forever, and the only symptom was
|
|
157
|
+
* searches returning nothing for content plainly in the row.
|
|
158
|
+
*/
|
|
159
|
+
export interface SearchColumnDrift {
|
|
160
|
+
/** `schema.table`. */
|
|
161
|
+
table: string;
|
|
162
|
+
column: string;
|
|
163
|
+
/** The fingerprint recorded on the column. */
|
|
164
|
+
found: string;
|
|
165
|
+
/** The fingerprint the current `search` block computes. */
|
|
166
|
+
expected: string;
|
|
167
|
+
/** The statements that would rebuild the column, for the operator to run. */
|
|
168
|
+
rebuild: string[];
|
|
118
169
|
}
|
|
119
170
|
|
|
120
171
|
/** A relation column whose old and new spellings both plausibly apply. */
|
|
@@ -129,14 +180,16 @@ export interface LegacyForeignKey {
|
|
|
129
180
|
|
|
130
181
|
export interface EnsureOutcome extends EnsurePlan {
|
|
131
182
|
/**
|
|
132
|
-
*
|
|
183
|
+
* Actions that could not be applied and are non-fatal by nature.
|
|
133
184
|
*
|
|
134
|
-
* A foreign key can only fail on data that already
|
|
135
|
-
* column it would police exists either way, so the
|
|
136
|
-
*
|
|
137
|
-
* outage.
|
|
185
|
+
* Two kinds qualify. A foreign key can only fail on data that already
|
|
186
|
+
* violates it, and the column it would police exists either way, so the
|
|
187
|
+
* collection still serves; refusing to boot over one would turn a
|
|
188
|
+
* pre-existing data problem into an outage. A column comment is the search
|
|
189
|
+
* fingerprint, which needs table ownership — losing it costs drift
|
|
190
|
+
* detection on the next boot, not the deployment. Both are reported loudly.
|
|
138
191
|
*/
|
|
139
|
-
failures: { target: string; error: string }[];
|
|
192
|
+
failures: { kind: EnsureAction["kind"]; target: string; error: string }[];
|
|
140
193
|
}
|
|
141
194
|
|
|
142
195
|
function schemaOf(collection: CollectionConfig): string {
|
|
@@ -424,13 +477,57 @@ export function planCollectionSchemaEnsure(
|
|
|
424
477
|
// one is not free — but it is the same additive shape as every other
|
|
425
478
|
// column here, and the alternative (leaving it out until someone runs a
|
|
426
479
|
// migration) is a declared `search` block that silently does nothing.
|
|
480
|
+
//
|
|
481
|
+
// Changing one is not additive, and `ADD COLUMN IF NOT EXISTS` is a
|
|
482
|
+
// no-op against a column that is already there — which is why a `search`
|
|
483
|
+
// block that gained a field, flipped `unaccent` or moved a weight used
|
|
484
|
+
// to be inert forever, on every path, with nothing logged. Each column
|
|
485
|
+
// therefore carries a fingerprint of the expression it was built from
|
|
486
|
+
// (in its comment), and a mismatch is reported rather than applied.
|
|
487
|
+
const searchDrift: SearchColumnDrift[] = [];
|
|
488
|
+
const searchAdopted: { table: string; column: string }[] = [];
|
|
427
489
|
for (const spec of searchSpecs) {
|
|
428
490
|
const key = `${spec.schema}.${spec.table}`;
|
|
429
|
-
|
|
430
|
-
`tsvector GENERATED ALWAYS AS (${spec.expression}) STORED`
|
|
491
|
+
const definitions: Record<string, string> = {
|
|
492
|
+
[spec.column]: `tsvector GENERATED ALWAYS AS (${spec.expression}) STORED`
|
|
493
|
+
};
|
|
431
494
|
if (spec.fuzzy) {
|
|
432
|
-
|
|
433
|
-
|
|
495
|
+
definitions[spec.fuzzy.column] = `text GENERATED ALWAYS AS (${spec.fuzzy.expression}) STORED`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (const stamp of searchColumnStamps(spec)) {
|
|
499
|
+
const definition = definitions[stamp.column];
|
|
500
|
+
const exists = existing.tables.get(key)?.has(stamp.column) === true;
|
|
501
|
+
const recorded = existing.columnComments?.get(`${key}.${stamp.column}`);
|
|
502
|
+
|
|
503
|
+
if (exists && recorded?.startsWith(SEARCH_STAMP_PREFIX) && recorded !== stamp.fingerprint) {
|
|
504
|
+
searchDrift.push({
|
|
505
|
+
table: key,
|
|
506
|
+
column: stamp.column,
|
|
507
|
+
found: recorded,
|
|
508
|
+
expected: stamp.fingerprint,
|
|
509
|
+
rebuild: [
|
|
510
|
+
`ALTER TABLE "${spec.schema}"."${spec.table}" DROP COLUMN "${stamp.column}";`,
|
|
511
|
+
`ALTER TABLE "${spec.schema}"."${spec.table}" ADD COLUMN "${stamp.column}" ${definition};`,
|
|
512
|
+
stamp.sql
|
|
513
|
+
]
|
|
514
|
+
});
|
|
515
|
+
// The old stamp is the only evidence of what the column holds;
|
|
516
|
+
// overwriting it here would erase the drift instead of fixing it.
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
addColumn(key, spec.schema, spec.table, stamp.column, definition);
|
|
521
|
+
if (exists && recorded === undefined) {
|
|
522
|
+
searchAdopted.push({ table: key, column: stamp.column });
|
|
523
|
+
}
|
|
524
|
+
if (recorded !== stamp.fingerprint) {
|
|
525
|
+
actions.push({
|
|
526
|
+
kind: "comment-column",
|
|
527
|
+
target: `${key}.${stamp.column}`,
|
|
528
|
+
sql: stamp.sql
|
|
529
|
+
});
|
|
530
|
+
}
|
|
434
531
|
}
|
|
435
532
|
}
|
|
436
533
|
|
|
@@ -498,7 +595,7 @@ export function planCollectionSchemaEnsure(
|
|
|
498
595
|
}
|
|
499
596
|
}
|
|
500
597
|
|
|
501
|
-
return { actions, statements: actions.map(a => a.sql), legacyForeignKeys };
|
|
598
|
+
return { actions, statements: actions.map(a => a.sql), legacyForeignKeys, searchDrift, searchAdopted };
|
|
502
599
|
}
|
|
503
600
|
|
|
504
601
|
/** Read what the database has, for the schemas the collections live in. */
|
|
@@ -553,7 +650,78 @@ export async function readExistingSchema(
|
|
|
553
650
|
);
|
|
554
651
|
for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);
|
|
555
652
|
|
|
556
|
-
|
|
653
|
+
// Column comments, which is where a generated search column records the
|
|
654
|
+
// expression it was built from. `objsubid > 0` is what makes a row a
|
|
655
|
+
// *column* comment rather than the table's own.
|
|
656
|
+
const columnComments = new Map<string, string>();
|
|
657
|
+
const { rows: commentRows } = await client.query<{
|
|
658
|
+
schema: string;
|
|
659
|
+
table: string;
|
|
660
|
+
column: string;
|
|
661
|
+
comment: string | null;
|
|
662
|
+
}>(
|
|
663
|
+
`SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment
|
|
664
|
+
FROM pg_description d
|
|
665
|
+
JOIN pg_class c ON d.objoid = c.oid
|
|
666
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
667
|
+
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid
|
|
668
|
+
WHERE d.objsubid > 0 AND n.nspname IN (${inList})`
|
|
669
|
+
);
|
|
670
|
+
for (const row of commentRows) {
|
|
671
|
+
if (row.comment == null) continue;
|
|
672
|
+
columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return { tables, enums, constraints, columnComments };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* What to tell an operator whose `search` block no longer matches its column.
|
|
680
|
+
*
|
|
681
|
+
* Every line here is doing work: naming the collection is not enough, because
|
|
682
|
+
* the symptom (a search that finds nothing) points at the data, not the schema;
|
|
683
|
+
* and the remediation has to be exact, because it is a table rewrite the
|
|
684
|
+
* operator is being asked to schedule rather than discover.
|
|
685
|
+
*/
|
|
686
|
+
function searchDriftMessage(drift: SearchColumnDrift[]): string {
|
|
687
|
+
const blocks = drift.map(d =>
|
|
688
|
+
` "${d.table}"."${d.column}" was generated from a different \`search\` block ` +
|
|
689
|
+
`(recorded ${d.found}, current ${d.expected}).\n` +
|
|
690
|
+
d.rebuild.map(s => ` ${s}`).join("\n")
|
|
691
|
+
);
|
|
692
|
+
return (
|
|
693
|
+
"The `search` block changed after its generated column was created, and Postgres cannot alter a " +
|
|
694
|
+
"generated expression in place.\n" +
|
|
695
|
+
"Rebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole " +
|
|
696
|
+
"table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path " +
|
|
697
|
+
"may not schedule on your behalf.\n" +
|
|
698
|
+
"Until it is rebuilt the column keeps indexing the previous fields, weights and language — searches for " +
|
|
699
|
+
"anything added since return nothing, which reads from outside as \"no such row\".\n" +
|
|
700
|
+
"Run these (or revert the block to what the column was built from), then boot again:\n" +
|
|
701
|
+
blocks.join("\n") +
|
|
702
|
+
"\n The GIN index is dropped with the column and recreated concurrently on the next boot."
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* The missing-pgvector explanation, appended to the error that reveals it.
|
|
708
|
+
*
|
|
709
|
+
* A `{ type: "vector" }` property compiles to `VECTOR(n)`, and nothing in the
|
|
710
|
+
* OSS pipeline installs pgvector — not this ensure, not `db push`, not the
|
|
711
|
+
* scaffold's `postgres:18-alpine`, which does not ship it. Installing an
|
|
712
|
+
* extension on someone's database is a decision with a deployment behind it
|
|
713
|
+
* (image, superuser, cloud allow-list), so this path stays a refusal; what it
|
|
714
|
+
* must not stay is a bare `type "vector" does not exist` on a crash-looping
|
|
715
|
+
* pod, which names nothing the reader can act on.
|
|
716
|
+
*/
|
|
717
|
+
function vectorExtensionHint(message: string): string {
|
|
718
|
+
if (!/type "(vector|halfvec|sparsevec)" does not exist/i.test(message)) return "";
|
|
719
|
+
return (
|
|
720
|
+
"\n pgvector is not installed on this database, and Rebase does not install it: it is a server extension, " +
|
|
721
|
+
"so it needs an image that ships it (e.g. `pgvector/pgvector:pg18` — the scaffold's `postgres:18-alpine` " +
|
|
722
|
+
"does not) and a role allowed to run `CREATE EXTENSION vector;`. Install it once, then boot again. " +
|
|
723
|
+
"Note also that Rebase creates no ANN index for a vector column, so `vectorSearch` is an exact scan."
|
|
724
|
+
);
|
|
557
725
|
}
|
|
558
726
|
|
|
559
727
|
/**
|
|
@@ -585,7 +753,7 @@ export async function ensureCollectionTables(
|
|
|
585
753
|
|
|
586
754
|
const existing = await readExistingSchema(client, schemas);
|
|
587
755
|
const plan = planCollectionSchemaEnsure(collections, existing);
|
|
588
|
-
const failures:
|
|
756
|
+
const failures: EnsureOutcome["failures"] = [];
|
|
589
757
|
|
|
590
758
|
// Reported, not warned: this is a rename the ensure is about to perform, and
|
|
591
759
|
// the operator should be able to see in the log why a column changed name.
|
|
@@ -602,6 +770,29 @@ export async function ensureCollectionTables(
|
|
|
602
770
|
log?.(message);
|
|
603
771
|
}
|
|
604
772
|
|
|
773
|
+
// Before anything is applied: a `search` block that changed after its column
|
|
774
|
+
// was generated cannot be honoured by an additive plan, and serving the old
|
|
775
|
+
// index while the config describes a new one is the silent failure this
|
|
776
|
+
// check exists to end. Refusing is the loud half — boot is fatal on purpose
|
|
777
|
+
// (see `ensureCollectionSchema` in the server's boot) and the message
|
|
778
|
+
// carries the exact statements that resolve it.
|
|
779
|
+
if (plan.searchDrift.length > 0) {
|
|
780
|
+
throw new Error(searchDriftMessage(plan.searchDrift));
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Said once per column, at the moment the stamp is applied: from here on a
|
|
784
|
+
// change is detected, but whether *this* column matches the block it is
|
|
785
|
+
// being stamped with is not knowable — it predates the stamp.
|
|
786
|
+
for (const adopted of plan.searchAdopted) {
|
|
787
|
+
const message =
|
|
788
|
+
`Adopting the existing generated column "${adopted.table}"."${adopted.column}" and recording what the ` +
|
|
789
|
+
"current `search` block would generate. Any later change to that block will be detected and refused; a " +
|
|
790
|
+
"change made *before* this version was deployed cannot be, so if search has been missing content, " +
|
|
791
|
+
`rebuild the column once: ALTER TABLE "${adopted.table.split(".").join('"."')}" DROP COLUMN "${adopted.column}"; and boot again.`;
|
|
792
|
+
logger.info(`[schema] ${message}`);
|
|
793
|
+
log?.(message);
|
|
794
|
+
}
|
|
795
|
+
|
|
605
796
|
if (plan.actions.length === 0) {
|
|
606
797
|
log?.("Schema is up to date; nothing to create.");
|
|
607
798
|
return { ...plan, failures };
|
|
@@ -617,12 +808,18 @@ export async function ensureCollectionTables(
|
|
|
617
808
|
// data rather than on the schema. The column it polices is already
|
|
618
809
|
// there, so the collection serves either way — record it and carry
|
|
619
810
|
// on rather than crash-looping the deployment.
|
|
620
|
-
|
|
621
|
-
|
|
811
|
+
//
|
|
812
|
+
// A comment is metadata about a column that was just created
|
|
813
|
+
// successfully, and it can only fail on ownership (COMMENT requires
|
|
814
|
+
// owning the table, which an adopted table may not grant). Losing
|
|
815
|
+
// the stamp costs drift detection on the next boot; it must not cost
|
|
816
|
+
// the deployment.
|
|
817
|
+
if (action.kind === "add-constraint" || action.kind === "comment-column") {
|
|
818
|
+
failures.push({ kind: action.kind, target: action.target, error: message });
|
|
622
819
|
continue;
|
|
623
820
|
}
|
|
624
821
|
throw new Error(
|
|
625
|
-
`Failed to ${action.kind} ${action.target}: ${message}\n ${action.sql}`
|
|
822
|
+
`Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\n ${action.sql}`
|
|
626
823
|
);
|
|
627
824
|
}
|
|
628
825
|
}
|