@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g08eed46
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/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
- package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +81 -7
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
- package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
- package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
- package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
- package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
- package/dist/cli-errors-C3g_kHBw.js.map +1 -0
- package/dist/cli-errors.d.ts +19 -0
- package/dist/cli-helpers.d.ts +47 -0
- package/dist/cli.js +233 -19
- package/dist/cli.js.map +1 -1
- package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
- package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
- package/dist/collections/validate-relations.d.ts +7 -0
- package/dist/column-plan-helpers-CpILzHJS.js +145 -0
- package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
- package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
- package/dist/doctor-D5SrGJ5P.js.map +1 -0
- package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
- package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
- package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
- package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
- package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
- package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
- package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
- package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
- package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
- package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +23 -4
- package/dist/index.es.js +1991 -580
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
- package/dist/plan-schema-Hgl62S-w.js +1531 -0
- package/dist/plan-schema-Hgl62S-w.js.map +1 -0
- package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
- package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
- package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
- package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
- package/dist/schema/catalogue-schema.d.ts +97 -0
- package/dist/schema/collection-index.d.ts +9 -0
- package/dist/schema/column-plan-helpers.d.ts +125 -0
- package/dist/schema/config-relations.d.ts +32 -0
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +9 -0
- package/dist/schema/dynamic-tables.d.ts +27 -6
- package/dist/schema/ensure-collection-tables.d.ts +31 -268
- package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
- package/dist/schema/generate-postgres-ddl.js +8 -2
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generated-column-conflicts.d.ts +84 -0
- package/dist/schema/generated-schema-diff.d.ts +56 -0
- package/dist/schema/generated-schema-staleness.d.ts +33 -0
- package/dist/schema/introspect-db-logic.d.ts +2 -0
- package/dist/schema/plan/diff-plan.d.ts +220 -0
- package/dist/schema/plan/plan-schema.d.ts +84 -0
- package/dist/schema/plan/render-ddl.d.ts +62 -0
- package/dist/schema/plan/render-drizzle.d.ts +31 -0
- package/dist/schema/plan/types.d.ts +410 -0
- package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
- package/dist/schema/relation-names.d.ts +28 -0
- package/dist/schema/search-column.d.ts +9 -0
- package/dist/security/rls-enforcement.d.ts +17 -0
- package/dist/services/FetchService.d.ts +176 -36
- package/dist/services/PersistService.d.ts +17 -4
- package/dist/services/RelationService.d.ts +11 -3
- package/dist/services/collection-helpers.d.ts +16 -2
- package/dist/services/dataService.d.ts +29 -2
- package/dist/services/field-op-sql.d.ts +16 -0
- package/dist/services/field-viewer.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +35 -1
- package/dist/services/row-pipeline.d.ts +29 -5
- package/dist/services/soft-delete.d.ts +73 -0
- package/dist/utils/pg-error-utils.d.ts +41 -1
- package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
- package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
- package/package.json +18 -10
- package/dist/cli-errors-DxN4yzdH.js.map +0 -1
- package/dist/doctor-D7HWmGzs.js.map +0 -1
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
- package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SQL } from "drizzle-orm";
|
|
2
|
+
import type { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
4
|
+
/**
|
|
5
|
+
* Soft delete, in one place.
|
|
6
|
+
*
|
|
7
|
+
* A collection with `softDelete` records a deletion as a timestamp instead of
|
|
8
|
+
* removing the row, which means two things have to hold everywhere and not
|
|
9
|
+
* almost everywhere:
|
|
10
|
+
*
|
|
11
|
+
* 1. a delete writes the stamp rather than issuing `DELETE`;
|
|
12
|
+
* 2. **every** read hides stamped rows unless asked otherwise.
|
|
13
|
+
*
|
|
14
|
+
* The second is the one that goes wrong. A listing that filters and a `count`
|
|
15
|
+
* that does not is a page saying "1 of 4 results"; a relation load that does not
|
|
16
|
+
* is a deleted comment reappearing under its post. So the condition is built by
|
|
17
|
+
* exactly one function, here, and the read paths call it — rather than each
|
|
18
|
+
* path growing its own `isNull(...)`, which is how these features rot.
|
|
19
|
+
*
|
|
20
|
+
* The field is a `date` property the collection declares itself. This module
|
|
21
|
+
* resolves it and nothing more: it does not add the column, and a config that
|
|
22
|
+
* turns the flag on without the property is refused at boot
|
|
23
|
+
* (`assertCollectionConfigs`), where the answer is a config change, rather than
|
|
24
|
+
* at the first delete, where it would be a 500 for whoever pressed the button.
|
|
25
|
+
*/
|
|
26
|
+
/** The default field, when `softDelete: true` names none. */
|
|
27
|
+
export declare const DEFAULT_SOFT_DELETE_FIELD = "deletedAt";
|
|
28
|
+
/** How a caller asks about deleted rows. See `FetchCollectionProps.withDeleted`. */
|
|
29
|
+
export type WithDeleted = boolean | "only" | undefined;
|
|
30
|
+
export interface SoftDeleteField {
|
|
31
|
+
/** The property key — the wire name, what a `PATCH` sets to `null`. */
|
|
32
|
+
field: string;
|
|
33
|
+
/** The physical column, `columnName ?? snake_case(field)`. */
|
|
34
|
+
columnName: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The soft-delete field this collection uses, or `undefined` if it has none.
|
|
38
|
+
*
|
|
39
|
+
* Returns the field even when the property is missing, because the *boot* check
|
|
40
|
+
* is what refuses that config; making this return `undefined` for it would turn
|
|
41
|
+
* a misconfiguration into "soft delete quietly off", which is the failure mode
|
|
42
|
+
* worth avoiding — deletes would silently start removing rows.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveSoftDelete(collection: CollectionConfig | undefined): SoftDeleteField | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The drizzle column behind the soft-delete field, if the table has one.
|
|
47
|
+
*
|
|
48
|
+
* Matched on the column's own `name` rather than on the key, because a
|
|
49
|
+
* generated schema keys columns by the property name while an introspected one
|
|
50
|
+
* may key them by the column — and this has to work for both.
|
|
51
|
+
*/
|
|
52
|
+
export declare function softDeleteColumn(collection: CollectionConfig | undefined, table: PgTable): AnyPgColumn | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* `deleted_at IS NULL` — or nothing, when the caller asked for the deleted rows
|
|
55
|
+
* or the collection has no soft delete at all.
|
|
56
|
+
*
|
|
57
|
+
* The one place the rule is written. Call it from every read; `undefined` means
|
|
58
|
+
* "add no condition", which composes with `and(...)` unchanged.
|
|
59
|
+
*/
|
|
60
|
+
export declare function softDeleteCondition(collection: CollectionConfig | undefined, table: PgTable, withDeleted?: WithDeleted): SQL | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Fold the soft-delete condition into a list of conditions that is being built.
|
|
63
|
+
*
|
|
64
|
+
* A convenience for the call sites that already collect `SQL[]`, so adding soft
|
|
65
|
+
* delete to one of them is a single line rather than three.
|
|
66
|
+
*/
|
|
67
|
+
export declare function withSoftDelete(conditions: SQL[], collection: CollectionConfig | undefined, table: PgTable, withDeleted?: WithDeleted): SQL[];
|
|
68
|
+
/**
|
|
69
|
+
* Combine an existing `where` with the soft-delete condition.
|
|
70
|
+
*
|
|
71
|
+
* For the call sites that hold one composed condition rather than a list.
|
|
72
|
+
*/
|
|
73
|
+
export declare function andSoftDelete(where: SQL | undefined, collection: CollectionConfig | undefined, table: PgTable, withDeleted?: WithDeleted): SQL | undefined;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 5-character alphanumeric `code` such as `42P01`) and translate it into
|
|
7
7
|
* a message that is safe and helpful to show to end-users.
|
|
8
8
|
*/
|
|
9
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
9
10
|
/** Shape of PostgreSQL errors with diagnostic metadata. */
|
|
10
11
|
export interface PostgresError extends Error {
|
|
11
12
|
code?: string;
|
|
@@ -100,6 +101,40 @@ export declare function isRoleSwitchingPermissionError(error: unknown): boolean;
|
|
|
100
101
|
* hunting for a policy bug that does not exist.
|
|
101
102
|
*/
|
|
102
103
|
export declare function isRowLevelSecurityDenial(error: unknown): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* One constraint the database refused, named in the caller's vocabulary.
|
|
106
|
+
*
|
|
107
|
+
* `field` is the **wire** name, derived from the physical column: `author_id`
|
|
108
|
+
* on the table is `authorId` on the row and in the request body, and telling a
|
|
109
|
+
* caller their `author_id` is missing sends them looking for a key their client
|
|
110
|
+
* has never seen. `code` is the SQLSTATE, which is stable, documented by
|
|
111
|
+
* Postgres and the same in every environment — unlike the message, which is
|
|
112
|
+
* deliberately thinner in production.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately no `value`. The column name is safe to publish: it is already in
|
|
115
|
+
* the OpenAPI, the generated SDK and the caller's own request. The *value* is
|
|
116
|
+
* not — `23505`'s detail reads `Key (email)=(a@b.c) already exists.`, which
|
|
117
|
+
* answers "is this person registered?" for any address on a table whose rows
|
|
118
|
+
* RLS is otherwise hiding completely.
|
|
119
|
+
*/
|
|
120
|
+
export interface PgFieldViolation {
|
|
121
|
+
field: string;
|
|
122
|
+
code: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The fields a Postgres error is about, for `details.violations`.
|
|
126
|
+
*
|
|
127
|
+
* Only the four SQLSTATEs that describe *the caller's data*: a missing value, a
|
|
128
|
+
* duplicate, a bad literal, a foreign key pointing at nothing. Everything else
|
|
129
|
+
* — a dropped connection, a missing table, a privilege problem — is the
|
|
130
|
+
* deployment's, and naming a field for it would send the reader to the wrong
|
|
131
|
+
* place.
|
|
132
|
+
*
|
|
133
|
+
* Empty when the collection is unknown or the column cannot be recovered. An
|
|
134
|
+
* empty list is the honest answer; inventing a field is worse than saying
|
|
135
|
+
* nothing, because a form will mark the wrong input red.
|
|
136
|
+
*/
|
|
137
|
+
export declare function pgFieldViolations(pgError: PostgresError, collection?: CollectionConfig): PgFieldViolation[];
|
|
103
138
|
/**
|
|
104
139
|
* Translate a raw PostgreSQL error into a user-friendly message.
|
|
105
140
|
*
|
|
@@ -109,9 +144,11 @@ export declare function isRowLevelSecurityDenial(error: unknown): boolean;
|
|
|
109
144
|
*/
|
|
110
145
|
export declare function pgErrorToFriendlyMessage(pgError: PostgresError, context: string, options?: {
|
|
111
146
|
verbose?: boolean;
|
|
147
|
+
collection?: CollectionConfig;
|
|
112
148
|
}): {
|
|
113
149
|
message: string;
|
|
114
150
|
code: string;
|
|
151
|
+
violations: PgFieldViolation[];
|
|
115
152
|
};
|
|
116
153
|
/**
|
|
117
154
|
* Sanitize any error into a message safe and helpful for the client.
|
|
@@ -126,7 +163,10 @@ export declare function pgErrorToFriendlyMessage(pgError: PostgresError, context
|
|
|
126
163
|
* @returns An object with `message` (user-friendly) and optional `code`
|
|
127
164
|
* (the `ApiError` code, or the PG SQLSTATE).
|
|
128
165
|
*/
|
|
129
|
-
export declare function sanitizeErrorForClient(error: unknown, context: string
|
|
166
|
+
export declare function sanitizeErrorForClient(error: unknown, context: string, options?: {
|
|
167
|
+
collection?: CollectionConfig;
|
|
168
|
+
}): {
|
|
130
169
|
message: string;
|
|
131
170
|
code?: string;
|
|
171
|
+
violations?: PgFieldViolation[];
|
|
132
172
|
};
|
|
@@ -23,8 +23,7 @@ var vector_index_exports = /* @__PURE__ */ __exportAll({
|
|
|
23
23
|
vectorExtensionStatement: () => vectorExtensionStatement,
|
|
24
24
|
vectorIndexNames: () => vectorIndexNames,
|
|
25
25
|
vectorIndexStatement: () => vectorIndexStatement,
|
|
26
|
-
vectorIndexStatements: () => vectorIndexStatements
|
|
27
|
-
vectorObjectNames: () => vectorObjectNames
|
|
26
|
+
vectorIndexStatements: () => vectorIndexStatements
|
|
28
27
|
});
|
|
29
28
|
/**
|
|
30
29
|
* The widest `vector` pgvector will build an HNSW or IVFFlat index over.
|
|
@@ -293,17 +292,7 @@ BEGIN
|
|
|
293
292
|
END
|
|
294
293
|
$rebase_vector$;`;
|
|
295
294
|
};
|
|
296
|
-
/**
|
|
297
|
-
* The object names a collection's vector properties own, unqualified.
|
|
298
|
-
*
|
|
299
|
-
* What Atlas has to be told to exclude. Only the column and the ANN indexes: a
|
|
300
|
-
* `UNIQUE` or `NOT NULL` on the column is a property *of* the column, and
|
|
301
|
-
* excluding the column takes them with it — measured against Atlas 1.2.3 and
|
|
302
|
-
* 1.3.2, where a target carrying `vector(3) NOT NULL UNIQUE` and a `schema.sql`
|
|
303
|
-
* carrying neither reported "Schema is synced, no changes to be made".
|
|
304
|
-
*/
|
|
305
|
-
var vectorObjectNames = (collection, resolveColumn) => [...buildVectorColumnSpecs(collection, resolveColumn).map((spec) => spec.column), ...vectorIndexNames(buildVectorIndexPlan(collection, resolveColumn))];
|
|
306
295
|
//#endregion
|
|
307
|
-
export { vectorDimensionGuard as a, vectorExtensionStatement as c,
|
|
296
|
+
export { vectorDimensionGuard as a, vectorExtensionStatement as c, vectorIndexStatements as d, vector_index_exports as f, vectorColumnDefinition as i, vectorIndexNames as l, buildVectorColumnSpecs as n, vectorExtensionDeclared as o, buildVectorIndexPlan as r, vectorExtensionHint as s, VECTOR_EXTENSION_OPT_IN as t, vectorIndexStatement as u };
|
|
308
297
|
|
|
309
|
-
//# sourceMappingURL=vector-index-
|
|
298
|
+
//# sourceMappingURL=vector-index-CMJKb3C9.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-index-DkWxm2de.js","names":[],"sources":["../src/schema/vector-index.ts"],"sourcesContent":["/**\n * The one place a `{ type: \"vector\" }` property becomes an ANN index.\n *\n * Without an index, pgvector answers `ORDER BY embedding <=> $1` by computing\n * the distance to every row and sorting — exact, and linear. That is the right\n * answer at ten thousand rows and the wrong one at a million, which is why the\n * default here is to build an index rather than to leave the column bare.\n *\n * ## The operator class is not a detail\n *\n * An index is built for exactly one operator class, and the planner uses it\n * only for the operator that class implements. `vector_cosine_ops` answers\n * `<=>` and nothing else; a query asking for `<->` against a cosine index gets\n * a sequential scan and no error. So the default indexed distance here is\n * `cosine`, because `cosine` is what `vectorSearch` measures with when the\n * caller does not say — see `DrizzleConditionBuilder.buildVectorSearch`. The\n * two defaults have to agree, and this comment is the reason they do.\n *\n * ## Why 2000 dimensions is a ceiling and not an error\n *\n * pgvector cannot index a `vector` wider than 2000 dimensions with either\n * method. A 3072-dimension embedding (`text-embedding-3-large`) is therefore\n * storable and searchable but not indexable. Refusing the boot over that would\n * make a working configuration unbootable; silently indexing it is impossible.\n * So the column is created, the index is skipped, and the reason is reported.\n *\n * Like `search-column.ts`, this module exists so that the DDL generator and the\n * boot-time ensure render the *same* specification rather than describing the\n * same index twice, differently. `contracts/derived-names.txt` records the\n * names both produce, and CI fails if they diverge.\n *\n * ## And why the column itself is described here too\n *\n * `VECTOR(n)` is a type Atlas cannot be shown. It has to materialise the\n * desired state in a dev database to diff against, that database is created\n * empty and *emptied again* by Atlas at the start of every run, and nothing in\n * the free tier can put `CREATE EXTENSION vector` back — so a `schema.sql`\n * mentioning the type fails with `type \"vector\" does not exist` on every push,\n * for good. Search hit the same wall for its own reasons and took the same way\n * out: the objects leave `schema.sql`, Atlas is told to exclude them, and\n * Rebase applies them itself. See `generatePostgresVectorDdl`.\n */\nimport type { CollectionConfig, Property, VectorDistance, VectorIndexConfig } from \"@rebasepro/types\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { toPostgresIdentifier } from \"@rebasepro/utils\";\n\n/**\n * The widest `vector` pgvector will build an HNSW or IVFFlat index over.\n * Storage and exact search are unaffected by this limit.\n */\nexport const MAX_INDEXABLE_VECTOR_DIMENSIONS = 2000;\n\n/** The distance indexed when a property does not name one. */\nexport const DEFAULT_VECTOR_DISTANCE: VectorDistance = \"cosine\";\n\n/** The index method used when a property does not name one. */\nexport const DEFAULT_VECTOR_INDEX_METHOD = \"hnsw\" as const;\n\n/**\n * Operator class per distance. These strings are part of the database contract:\n * they appear in `CREATE INDEX`, so renaming one renames an index.\n */\nconst OPERATOR_CLASS: Record<VectorDistance, string> = {\n cosine: \"vector_cosine_ops\",\n l2: \"vector_l2_ops\",\n inner_product: \"vector_ip_ops\"\n};\n\n/** Short, stable tag per distance, used to name the index. */\nconst DISTANCE_TAG: Record<VectorDistance, string> = {\n cosine: \"cosine\",\n l2: \"l2\",\n inner_product: \"ip\"\n};\n\nexport interface VectorIndexSpec {\n schema: string;\n table: string;\n column: string;\n indexName: string;\n method: \"hnsw\" | \"ivfflat\";\n distance: VectorDistance;\n operatorClass: string;\n /** Rendered into `WITH (...)`; empty when every parameter is defaulted. */\n parameters: Array<[string, number]>;\n}\n\n/** A vector column that will not be indexed, and why. */\nexport interface SkippedVectorIndex {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n reason: string;\n}\n\nexport interface VectorIndexPlan {\n specs: VectorIndexSpec[];\n skipped: SkippedVectorIndex[];\n}\n\nexport class VectorIndexConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"VectorIndexConfigError\";\n }\n}\n\nconst isVectorProperty = (prop: unknown): prop is Property & {\n type: \"vector\";\n dimensions: number;\n index?: VectorIndexConfig | false;\n} => !!prop && typeof prop === \"object\" && (prop as { type?: string }).type === \"vector\";\n\nconst asDistances = (config: VectorIndexConfig, label: string): VectorDistance[] => {\n const raw = config.distance ?? DEFAULT_VECTOR_DISTANCE;\n const list = Array.isArray(raw) ? raw : [raw];\n if (list.length === 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is an empty array. Name at least one distance, or set \\`index: false\\` to create no index.`\n );\n }\n const seen = new Set<VectorDistance>();\n for (const distance of list) {\n if (!(distance in OPERATOR_CLASS)) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is \"${distance}\", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map(d => `\"${d}\"`).join(\", \")}.`\n );\n }\n if (seen.has(distance)) {\n throw new VectorIndexConfigError(`${label}: \\`index.distance\\` lists \"${distance}\" twice.`);\n }\n seen.add(distance);\n }\n return list;\n};\n\nconst assertPositiveInteger = (value: number | undefined, key: string, label: string): void => {\n if (value === undefined) return;\n if (!Number.isInteger(value) || value <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` is ${JSON.stringify(value)}. It must be a positive integer.`\n );\n }\n};\n\n/**\n * Index parameters for one method. Parameters belonging to the *other* method\n * are rejected rather than ignored, because a silently dropped `lists` on an\n * HNSW index reads, from the config, exactly like a tuned index.\n */\nconst parametersFor = (\n method: \"hnsw\" | \"ivfflat\",\n config: VectorIndexConfig,\n label: string\n): Array<[string, number]> => {\n assertPositiveInteger(config.m, \"m\", label);\n assertPositiveInteger(config.efConstruction, \"efConstruction\", label);\n assertPositiveInteger(config.lists, \"lists\", label);\n\n if (method === \"hnsw\") {\n if (config.lists !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.lists\\` only applies to \\`method: \"ivfflat\"\\`. Remove it, or switch the method.`\n );\n }\n const params: Array<[string, number]> = [];\n if (config.m !== undefined) params.push([\"m\", config.m]);\n if (config.efConstruction !== undefined) params.push([\"ef_construction\", config.efConstruction]);\n return params;\n }\n\n for (const key of [\"m\", \"efConstruction\"] as const) {\n if (config[key] !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` only applies to \\`method: \"hnsw\"\\`. Remove it, or switch the method.`\n );\n }\n }\n return config.lists !== undefined ? [[\"lists\", config.lists]] : [];\n};\n\n/**\n * Every ANN index a collection's vector properties call for.\n *\n * `resolveColumn` is passed in rather than imported so that this module stays\n * free of the DDL generator, which imports *it*. Both callers hand it the same\n * `resolveColumnName`, and a contract test asserts the names agree.\n */\nexport const buildVectorIndexPlan = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorIndexPlan => {\n const specs: VectorIndexSpec[] = [];\n const skipped: SkippedVectorIndex[] = [];\n const properties = collection.properties ?? {};\n\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n\n for (const [propName, prop] of Object.entries(properties)) {\n if (!isVectorProperty(prop)) continue;\n if (prop.index === false) continue;\n\n const label = `${collection.slug}.${propName}`;\n const column = resolveColumn(propName, prop as Property);\n const config: VectorIndexConfig = prop.index ?? {};\n const method = config.method ?? DEFAULT_VECTOR_INDEX_METHOD;\n\n if (method !== \"hnsw\" && method !== \"ivfflat\") {\n throw new VectorIndexConfigError(\n `${label}: \\`index.method\\` is \"${method}\". Use \"hnsw\" or \"ivfflat\".`\n );\n }\n\n // Read before the dimension gate so a malformed block is reported even\n // on a column too wide to index — the config is wrong either way.\n const distances = asDistances(config, label);\n const parameters = parametersFor(method, config, label);\n\n if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`dimensions\\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`\n );\n }\n\n if (prop.dimensions > MAX_INDEXABLE_VECTOR_DIMENSIONS) {\n skipped.push({\n schema,\n table,\n column,\n dimensions: prop.dimensions,\n reason:\n `pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ` +\n `${label} declares ${prop.dimensions}. The column works and \\`vectorSearch\\` still answers, as an ` +\n `exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) ` +\n `or set \\`index: false\\` to state that the scan is intended.`\n });\n continue;\n }\n\n for (const distance of distances) {\n specs.push({\n schema,\n table,\n column,\n // Distance is in the name because one column may carry an index\n // per distance, and two indexes cannot share a name. The method\n // is in it because switching methods is a different index, not\n // a redefinition of the same one — `CREATE INDEX IF NOT EXISTS`\n // would otherwise keep the old one and report success.\n indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),\n method,\n distance,\n operatorClass: OPERATOR_CLASS[distance],\n parameters\n });\n }\n }\n\n return { specs, skipped };\n};\n\n/**\n * The `CREATE INDEX` for one spec.\n *\n * `CONCURRENTLY` is deliberately absent, for the same reason it is absent from\n * `searchIndexStatements`: this form is replayed as part of a migration, where\n * a concurrent build is not allowed. The boot-time ensure rewrites it — see\n * `ensureCollectionTables`.\n */\nexport const vectorIndexStatement = (spec: VectorIndexSpec): string => {\n const params = spec.parameters.length\n ? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(\", \")})`\n : \"\";\n return (\n `CREATE INDEX IF NOT EXISTS \"${spec.indexName}\" ON \"${spec.schema}\".\"${spec.table}\" ` +\n `USING ${spec.method} (\"${spec.column}\" ${spec.operatorClass})${params};`\n );\n};\n\n/** Every statement for a plan, in a stable order. */\nexport const vectorIndexStatements = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(vectorIndexStatement);\n\n/** The index names a plan creates — what the derived-names contract records. */\nexport const vectorIndexNames = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(spec => spec.indexName);\n\n// ── The column, for the file Rebase applies itself ──────────────────────────\n\nconst quote = (v: string): string => `'${v.replace(/'/g, \"''\")}'`;\n\n/**\n * The schema pgvector's types are installed into.\n *\n * `WITH SCHEMA public` for the same load-bearing reason `searchExtensionStatements`\n * gives: an unqualified `CREATE EXTENSION` lands in the first schema on\n * `search_path`, which is `\"$user\", public` — and the scaffold's role is named\n * `rebase`, the same as a schema the generator creates. Left to itself the\n * extension would install into `rebase`, and every unqualified `VECTOR(n)`\n * below would fail to resolve.\n */\nexport const VECTOR_EXTENSION_SCHEMA = \"public\";\n\n/** The extension name a database has to name to let Rebase install pgvector. */\nexport const VECTOR_EXTENSION = \"vector\";\n\n/**\n * How a project says Rebase may install pgvector, quoted into the messages that\n * have to name it. Spelled once so the option and the advice cannot drift.\n */\nexport const VECTOR_EXTENSION_OPT_IN = `database({ extensions: [\"${VECTOR_EXTENSION}\"] })`;\n\n/** Did the project give Rebase leave to install pgvector? */\nexport const vectorExtensionDeclared = (extensions: readonly string[] | undefined): boolean =>\n (extensions ?? []).includes(VECTOR_EXTENSION);\n\n/**\n * Install pgvector — emitted only when the database asked for it.\n *\n * Opt-in because installing an extension is a decision with a deployment behind\n * it: the image has to ship the library, the role has to be allowed to install\n * it, and a managed provider has to have it on an allow-list. None of that is\n * visible from inside the connection, so Rebase does not decide it. See\n * `DatabaseOptions.extensions`.\n *\n * Withholding the statement is not withholding the *column*: the column is\n * still created, and Postgres refuses it with `type \"vector\" does not exist`\n * on a database where pgvector was never installed by hand. That error is the\n * one this design accepts, and `vectorExtensionHint` is what makes it name\n * {@link VECTOR_EXTENSION_OPT_IN} rather than nothing.\n */\nexport const vectorExtensionStatement = (): string =>\n `CREATE EXTENSION IF NOT EXISTS ${VECTOR_EXTENSION} WITH SCHEMA ${VECTOR_EXTENSION_SCHEMA};`;\n\n/**\n * The missing-pgvector explanation, appended to whichever error revealed it.\n *\n * Two readers, needing opposite things, which is why the branch is on the error\n * text rather than on the configuration:\n *\n * - **`type \"vector\" does not exist`** — nobody opted in and the database has\n * no pgvector. The fix is one line of config they cannot guess, so naming\n * the option *is* the hint.\n * - **the install itself failed** — the config is already right, and repeating\n * the option would send them to edit a correct line. What is missing is the\n * library on the server, or the grant.\n *\n * Lives here rather than beside either caller because both need it: the boot\n * ensure raises the first through its action applier, and `rebase db push`\n * raises it out of `applyVectorDdl`. A hint on only one path is how a bare\n * `type \"vector\" does not exist` reaches somebody — which is the thing this\n * exists to prevent.\n */\nexport const vectorExtensionHint = (message: string): string => {\n const installRefused = /extension \"vector\" is not available/i.test(message)\n || /(permission denied to create extension|must be (superuser|owner).{0,40}extension)/i.test(message);\n if (installRefused) {\n return (\n \"\\n pgvector was declared and could not be installed. It is a server extension, so it needs an image \" +\n \"that ships the library (the scaffold's `pgvector/pgvector:pg18` does; a stock `postgres:18` does not) \" +\n \"and a role allowed to run `CREATE EXTENSION vector;`. Managed Postgres usually allows it once the \" +\n \"extension is on the provider's allow-list.\"\n );\n }\n if (!/type \"(vector|halfvec|sparsevec)\" does not exist/i.test(message)) return \"\";\n return (\n \"\\n pgvector is not installed on this database, and Rebase installs it only where a database says it may: \" +\n `add \\`${VECTOR_EXTENSION_OPT_IN}\\` in config/resources.ts, or install it once by hand with ` +\n \"`CREATE EXTENSION vector;`. Either way the server needs an image that ships the library — the scaffold's \" +\n \"`pgvector/pgvector:pg18` does, a stock `postgres:18` does not. Rebase then creates the column and its ANN \" +\n \"index automatically — see the `index` option on the property.\"\n );\n};\n\n/** One `{ type: \"vector\" }` property, as a column. */\nexport interface VectorColumnSpec {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n /** Rendered after the type, in the generator's order: UNIQUE then NOT NULL. */\n modifiers: string;\n}\n\n/**\n * Every vector column a collection declares.\n *\n * Wider than {@link buildVectorIndexPlan} on purpose: that one answers \"what\n * gets an ANN index\", and skips both a property with `index: false` and one too\n * wide to index. Either still needs its column.\n */\nexport const buildVectorColumnSpecs = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorColumnSpec[] => {\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const specs: VectorColumnSpec[] = [];\n\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n if (!isVectorProperty(prop)) continue;\n // Assembled in the CREATE TABLE generator's order, so the column this\n // file adds and the column `schema.sql` used to declare are the same\n // column. A vector property is never a primary key and carries no\n // SQL-level default, which is what leaves only these two.\n const validation = (prop as { validation?: { required?: boolean; unique?: boolean } }).validation;\n let modifiers = \"\";\n if (validation?.unique) modifiers += \" UNIQUE\";\n if (validation?.required) modifiers += \" NOT NULL\";\n specs.push({\n schema,\n table,\n column: resolveColumn(propName, prop as Property),\n dimensions: prop.dimensions,\n modifiers\n });\n }\n\n return specs;\n};\n\n/** The column type — the one place `VECTOR(n)` is spelled. */\nexport const vectorColumnType = (spec: Pick<VectorColumnSpec, \"dimensions\">): string =>\n `VECTOR(${spec.dimensions})`;\n\n/** The column definition as it appears inside `CREATE TABLE`. */\nexport const vectorColumnDefinition = (spec: VectorColumnSpec): string =>\n `\"${spec.column}\" ${vectorColumnType(spec)}${spec.modifiers}`;\n\n/**\n * Refuse — or perform — a `dimensions` change that `ADD COLUMN IF NOT EXISTS`\n * would otherwise swallow.\n *\n * Without this the file *launders* the change: the ADD is a no-op against a\n * column that exists, so a project that went from 384 to 768 dimensions would\n * push clean, keep a 384-wide column, and fail on the next insert with a\n * message about the row rather than about the config. Atlas used to catch this\n * — it owned the column and planned the `ALTER … TYPE` — and taking the column\n * out of its sight is exactly what makes the guard necessary.\n *\n * The widening is performed when the column holds no values, because that is\n * the case Atlas handled and it is the common one: a developer changing\n * embedding models before there are any embeddings. With values present the\n * conversion is pgvector's to reject — every stored vector is the old width —\n * so this refuses first and names the statement to run afterwards.\n *\n * `atttypmod` on a `vector` column *is* the dimension count: pgvector stores it\n * directly rather than offsetting it the way `varchar` does. `-1` means the\n * column was declared as a bare `vector`, which is drift in the same sense.\n */\nexport const vectorDimensionGuard = (spec: VectorColumnSpec): string => {\n const relation = `\"${spec.schema}\".\"${spec.table}\"`;\n return `DO $rebase_vector$\nDECLARE actual int;\nBEGIN\n SELECT a.atttypmod INTO actual\n FROM pg_attribute a\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE a.attrelid = ${quote(relation)}::regclass\n AND a.attname = ${quote(spec.column)}\n AND NOT a.attisdropped\n AND t.typname = 'vector';\n IF actual IS NOT NULL AND actual <> ${spec.dimensions} THEN\n IF EXISTS (SELECT 1 FROM ${relation} WHERE \"${spec.column}\" IS NOT NULL) THEN\n RAISE EXCEPTION 'Rebase: ${spec.schema}.${spec.table}.\"${spec.column}\" declares ${spec.dimensions} dimensions but the column is vector(%), and it already holds values of the old width. pgvector cannot convert them. Re-embed the rows at ${spec.dimensions} dimensions (or clear the column), then re-apply: ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};', actual;\n END IF;\n ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};\n END IF;\nEND\n$rebase_vector$;`;\n};\n\n/**\n * The object names a collection's vector properties own, unqualified.\n *\n * What Atlas has to be told to exclude. Only the column and the ANN indexes: a\n * `UNIQUE` or `NOT NULL` on the column is a property *of* the column, and\n * excluding the column takes them with it — measured against Atlas 1.2.3 and\n * 1.3.2, where a target carrying `vector(3) NOT NULL UNIQUE` and a `schema.sql`\n * carrying neither reported \"Schema is synced, no changes to be made\".\n */\nexport const vectorObjectNames = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): string[] => [\n ...buildVectorColumnSpecs(collection, resolveColumn).map(spec => spec.column),\n ...vectorIndexNames(buildVectorIndexPlan(collection, resolveColumn))\n];\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vector-index-CMJKb3C9.js","names":[],"sources":["../src/schema/vector-index.ts"],"sourcesContent":["/**\n * The one place a `{ type: \"vector\" }` property becomes an ANN index.\n *\n * Without an index, pgvector answers `ORDER BY embedding <=> $1` by computing\n * the distance to every row and sorting — exact, and linear. That is the right\n * answer at ten thousand rows and the wrong one at a million, which is why the\n * default here is to build an index rather than to leave the column bare.\n *\n * ## The operator class is not a detail\n *\n * An index is built for exactly one operator class, and the planner uses it\n * only for the operator that class implements. `vector_cosine_ops` answers\n * `<=>` and nothing else; a query asking for `<->` against a cosine index gets\n * a sequential scan and no error. So the default indexed distance here is\n * `cosine`, because `cosine` is what `vectorSearch` measures with when the\n * caller does not say — see `DrizzleConditionBuilder.buildVectorSearch`. The\n * two defaults have to agree, and this comment is the reason they do.\n *\n * ## Why 2000 dimensions is a ceiling and not an error\n *\n * pgvector cannot index a `vector` wider than 2000 dimensions with either\n * method. A 3072-dimension embedding (`text-embedding-3-large`) is therefore\n * storable and searchable but not indexable. Refusing the boot over that would\n * make a working configuration unbootable; silently indexing it is impossible.\n * So the column is created, the index is skipped, and the reason is reported.\n *\n * Like `search-column.ts`, this module exists so that the DDL generator and the\n * boot-time ensure render the *same* specification rather than describing the\n * same index twice, differently. `contracts/derived-names.txt` records the\n * names both produce, and CI fails if they diverge.\n *\n * ## And why the column itself is described here too\n *\n * `VECTOR(n)` is a type Atlas cannot be shown. It has to materialise the\n * desired state in a dev database to diff against, that database is created\n * empty and *emptied again* by Atlas at the start of every run, and nothing in\n * the free tier can put `CREATE EXTENSION vector` back — so a `schema.sql`\n * mentioning the type fails with `type \"vector\" does not exist` on every push,\n * for good. Search hit the same wall for its own reasons and took the same way\n * out: the objects leave `schema.sql`, Atlas is told to exclude them, and\n * Rebase applies them itself. See `generatePostgresVectorDdl`.\n */\nimport type { CollectionConfig, Property, VectorDistance, VectorIndexConfig } from \"@rebasepro/types\";\nimport { isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { toPostgresIdentifier } from \"@rebasepro/utils\";\n\n/**\n * The widest `vector` pgvector will build an HNSW or IVFFlat index over.\n * Storage and exact search are unaffected by this limit.\n */\nexport const MAX_INDEXABLE_VECTOR_DIMENSIONS = 2000;\n\n/** The distance indexed when a property does not name one. */\nexport const DEFAULT_VECTOR_DISTANCE: VectorDistance = \"cosine\";\n\n/** The index method used when a property does not name one. */\nexport const DEFAULT_VECTOR_INDEX_METHOD = \"hnsw\" as const;\n\n/**\n * Operator class per distance. These strings are part of the database contract:\n * they appear in `CREATE INDEX`, so renaming one renames an index.\n */\nconst OPERATOR_CLASS: Record<VectorDistance, string> = {\n cosine: \"vector_cosine_ops\",\n l2: \"vector_l2_ops\",\n inner_product: \"vector_ip_ops\"\n};\n\n/** Short, stable tag per distance, used to name the index. */\nconst DISTANCE_TAG: Record<VectorDistance, string> = {\n cosine: \"cosine\",\n l2: \"l2\",\n inner_product: \"ip\"\n};\n\nexport interface VectorIndexSpec {\n schema: string;\n table: string;\n column: string;\n indexName: string;\n method: \"hnsw\" | \"ivfflat\";\n distance: VectorDistance;\n operatorClass: string;\n /** Rendered into `WITH (...)`; empty when every parameter is defaulted. */\n parameters: Array<[string, number]>;\n}\n\n/** A vector column that will not be indexed, and why. */\nexport interface SkippedVectorIndex {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n reason: string;\n}\n\nexport interface VectorIndexPlan {\n specs: VectorIndexSpec[];\n skipped: SkippedVectorIndex[];\n}\n\nexport class VectorIndexConfigError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"VectorIndexConfigError\";\n }\n}\n\nconst isVectorProperty = (prop: unknown): prop is Property & {\n type: \"vector\";\n dimensions: number;\n index?: VectorIndexConfig | false;\n} => !!prop && typeof prop === \"object\" && (prop as { type?: string }).type === \"vector\";\n\nconst asDistances = (config: VectorIndexConfig, label: string): VectorDistance[] => {\n const raw = config.distance ?? DEFAULT_VECTOR_DISTANCE;\n const list = Array.isArray(raw) ? raw : [raw];\n if (list.length === 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is an empty array. Name at least one distance, or set \\`index: false\\` to create no index.`\n );\n }\n const seen = new Set<VectorDistance>();\n for (const distance of list) {\n if (!(distance in OPERATOR_CLASS)) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.distance\\` is \"${distance}\", which is not a pgvector distance. Use ${Object.keys(OPERATOR_CLASS).map(d => `\"${d}\"`).join(\", \")}.`\n );\n }\n if (seen.has(distance)) {\n throw new VectorIndexConfigError(`${label}: \\`index.distance\\` lists \"${distance}\" twice.`);\n }\n seen.add(distance);\n }\n return list;\n};\n\nconst assertPositiveInteger = (value: number | undefined, key: string, label: string): void => {\n if (value === undefined) return;\n if (!Number.isInteger(value) || value <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` is ${JSON.stringify(value)}. It must be a positive integer.`\n );\n }\n};\n\n/**\n * Index parameters for one method. Parameters belonging to the *other* method\n * are rejected rather than ignored, because a silently dropped `lists` on an\n * HNSW index reads, from the config, exactly like a tuned index.\n */\nconst parametersFor = (\n method: \"hnsw\" | \"ivfflat\",\n config: VectorIndexConfig,\n label: string\n): Array<[string, number]> => {\n assertPositiveInteger(config.m, \"m\", label);\n assertPositiveInteger(config.efConstruction, \"efConstruction\", label);\n assertPositiveInteger(config.lists, \"lists\", label);\n\n if (method === \"hnsw\") {\n if (config.lists !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.lists\\` only applies to \\`method: \"ivfflat\"\\`. Remove it, or switch the method.`\n );\n }\n const params: Array<[string, number]> = [];\n if (config.m !== undefined) params.push([\"m\", config.m]);\n if (config.efConstruction !== undefined) params.push([\"ef_construction\", config.efConstruction]);\n return params;\n }\n\n for (const key of [\"m\", \"efConstruction\"] as const) {\n if (config[key] !== undefined) {\n throw new VectorIndexConfigError(\n `${label}: \\`index.${key}\\` only applies to \\`method: \"hnsw\"\\`. Remove it, or switch the method.`\n );\n }\n }\n return config.lists !== undefined ? [[\"lists\", config.lists]] : [];\n};\n\n/**\n * Every ANN index a collection's vector properties call for.\n *\n * `resolveColumn` is passed in rather than imported so that this module stays\n * free of the DDL generator, which imports *it*. Both callers hand it the same\n * `resolveColumnName`, and a contract test asserts the names agree.\n */\nexport const buildVectorIndexPlan = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorIndexPlan => {\n const specs: VectorIndexSpec[] = [];\n const skipped: SkippedVectorIndex[] = [];\n const properties = collection.properties ?? {};\n\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n\n for (const [propName, prop] of Object.entries(properties)) {\n if (!isVectorProperty(prop)) continue;\n if (prop.index === false) continue;\n\n const label = `${collection.slug}.${propName}`;\n const column = resolveColumn(propName, prop as Property);\n const config: VectorIndexConfig = prop.index ?? {};\n const method = config.method ?? DEFAULT_VECTOR_INDEX_METHOD;\n\n if (method !== \"hnsw\" && method !== \"ivfflat\") {\n throw new VectorIndexConfigError(\n `${label}: \\`index.method\\` is \"${method}\". Use \"hnsw\" or \"ivfflat\".`\n );\n }\n\n // Read before the dimension gate so a malformed block is reported even\n // on a column too wide to index — the config is wrong either way.\n const distances = asDistances(config, label);\n const parameters = parametersFor(method, config, label);\n\n if (!Number.isInteger(prop.dimensions) || prop.dimensions <= 0) {\n throw new VectorIndexConfigError(\n `${label}: \\`dimensions\\` is ${JSON.stringify(prop.dimensions)}. It must be a positive integer.`\n );\n }\n\n if (prop.dimensions > MAX_INDEXABLE_VECTOR_DIMENSIONS) {\n skipped.push({\n schema,\n table,\n column,\n dimensions: prop.dimensions,\n reason:\n `pgvector cannot index a vector wider than ${MAX_INDEXABLE_VECTOR_DIMENSIONS} dimensions, and ` +\n `${label} declares ${prop.dimensions}. The column works and \\`vectorSearch\\` still answers, as an ` +\n `exact scan. To index it, reduce the dimensions (many embedding models support a shorter output) ` +\n `or set \\`index: false\\` to state that the scan is intended.`\n });\n continue;\n }\n\n for (const distance of distances) {\n specs.push({\n schema,\n table,\n column,\n // Distance is in the name because one column may carry an index\n // per distance, and two indexes cannot share a name. The method\n // is in it because switching methods is a different index, not\n // a redefinition of the same one — `CREATE INDEX IF NOT EXISTS`\n // would otherwise keep the old one and report success.\n indexName: toPostgresIdentifier(`${table}_${column}_${method}_${DISTANCE_TAG[distance]}`),\n method,\n distance,\n operatorClass: OPERATOR_CLASS[distance],\n parameters\n });\n }\n }\n\n return { specs, skipped };\n};\n\n/**\n * The `CREATE INDEX` for one spec.\n *\n * `CONCURRENTLY` is deliberately absent, for the same reason it is absent from\n * `searchIndexStatements`: this form is replayed as part of a migration, where\n * a concurrent build is not allowed. The boot-time ensure rewrites it — see\n * `ensureCollectionTables`.\n */\nexport const vectorIndexStatement = (spec: VectorIndexSpec): string => {\n const params = spec.parameters.length\n ? ` WITH (${spec.parameters.map(([key, value]) => `${key} = ${value}`).join(\", \")})`\n : \"\";\n return (\n `CREATE INDEX IF NOT EXISTS \"${spec.indexName}\" ON \"${spec.schema}\".\"${spec.table}\" ` +\n `USING ${spec.method} (\"${spec.column}\" ${spec.operatorClass})${params};`\n );\n};\n\n/** Every statement for a plan, in a stable order. */\nexport const vectorIndexStatements = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(vectorIndexStatement);\n\n/** The index names a plan creates — what the derived-names contract records. */\nexport const vectorIndexNames = (plan: VectorIndexPlan): string[] =>\n plan.specs.map(spec => spec.indexName);\n\n// ── The column, for the file Rebase applies itself ──────────────────────────\n\nconst quote = (v: string): string => `'${v.replace(/'/g, \"''\")}'`;\n\n/**\n * The schema pgvector's types are installed into.\n *\n * `WITH SCHEMA public` for the same load-bearing reason `searchExtensionStatements`\n * gives: an unqualified `CREATE EXTENSION` lands in the first schema on\n * `search_path`, which is `\"$user\", public` — and the scaffold's role is named\n * `rebase`, the same as a schema the generator creates. Left to itself the\n * extension would install into `rebase`, and every unqualified `VECTOR(n)`\n * below would fail to resolve.\n */\nexport const VECTOR_EXTENSION_SCHEMA = \"public\";\n\n/** The extension name a database has to name to let Rebase install pgvector. */\nexport const VECTOR_EXTENSION = \"vector\";\n\n/**\n * How a project says Rebase may install pgvector, quoted into the messages that\n * have to name it. Spelled once so the option and the advice cannot drift.\n */\nexport const VECTOR_EXTENSION_OPT_IN = `database({ extensions: [\"${VECTOR_EXTENSION}\"] })`;\n\n/** Did the project give Rebase leave to install pgvector? */\nexport const vectorExtensionDeclared = (extensions: readonly string[] | undefined): boolean =>\n (extensions ?? []).includes(VECTOR_EXTENSION);\n\n/**\n * Install pgvector — emitted only when the database asked for it.\n *\n * Opt-in because installing an extension is a decision with a deployment behind\n * it: the image has to ship the library, the role has to be allowed to install\n * it, and a managed provider has to have it on an allow-list. None of that is\n * visible from inside the connection, so Rebase does not decide it. See\n * `DatabaseOptions.extensions`.\n *\n * Withholding the statement is not withholding the *column*: the column is\n * still created, and Postgres refuses it with `type \"vector\" does not exist`\n * on a database where pgvector was never installed by hand. That error is the\n * one this design accepts, and `vectorExtensionHint` is what makes it name\n * {@link VECTOR_EXTENSION_OPT_IN} rather than nothing.\n */\nexport const vectorExtensionStatement = (): string =>\n `CREATE EXTENSION IF NOT EXISTS ${VECTOR_EXTENSION} WITH SCHEMA ${VECTOR_EXTENSION_SCHEMA};`;\n\n/**\n * The missing-pgvector explanation, appended to whichever error revealed it.\n *\n * Two readers, needing opposite things, which is why the branch is on the error\n * text rather than on the configuration:\n *\n * - **`type \"vector\" does not exist`** — nobody opted in and the database has\n * no pgvector. The fix is one line of config they cannot guess, so naming\n * the option *is* the hint.\n * - **the install itself failed** — the config is already right, and repeating\n * the option would send them to edit a correct line. What is missing is the\n * library on the server, or the grant.\n *\n * Lives here rather than beside either caller because both need it: the boot\n * ensure raises the first through its action applier, and `rebase db push`\n * raises it out of `applyVectorDdl`. A hint on only one path is how a bare\n * `type \"vector\" does not exist` reaches somebody — which is the thing this\n * exists to prevent.\n */\nexport const vectorExtensionHint = (message: string): string => {\n const installRefused = /extension \"vector\" is not available/i.test(message)\n || /(permission denied to create extension|must be (superuser|owner).{0,40}extension)/i.test(message);\n if (installRefused) {\n return (\n \"\\n pgvector was declared and could not be installed. It is a server extension, so it needs an image \" +\n \"that ships the library (the scaffold's `pgvector/pgvector:pg18` does; a stock `postgres:18` does not) \" +\n \"and a role allowed to run `CREATE EXTENSION vector;`. Managed Postgres usually allows it once the \" +\n \"extension is on the provider's allow-list.\"\n );\n }\n if (!/type \"(vector|halfvec|sparsevec)\" does not exist/i.test(message)) return \"\";\n return (\n \"\\n pgvector is not installed on this database, and Rebase installs it only where a database says it may: \" +\n `add \\`${VECTOR_EXTENSION_OPT_IN}\\` in config/resources.ts, or install it once by hand with ` +\n \"`CREATE EXTENSION vector;`. Either way the server needs an image that ships the library — the scaffold's \" +\n \"`pgvector/pgvector:pg18` does, a stock `postgres:18` does not. Rebase then creates the column and its ANN \" +\n \"index automatically — see the `index` option on the property.\"\n );\n};\n\n/** One `{ type: \"vector\" }` property, as a column. */\nexport interface VectorColumnSpec {\n schema: string;\n table: string;\n column: string;\n dimensions: number;\n /** Rendered after the type, in the generator's order: UNIQUE then NOT NULL. */\n modifiers: string;\n}\n\n/**\n * Every vector column a collection declares.\n *\n * Wider than {@link buildVectorIndexPlan} on purpose: that one answers \"what\n * gets an ANN index\", and skips both a property with `index: false` and one too\n * wide to index. Either still needs its column.\n */\nexport const buildVectorColumnSpecs = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): VectorColumnSpec[] => {\n const table = getTableName(collection);\n const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\";\n const specs: VectorColumnSpec[] = [];\n\n for (const [propName, prop] of Object.entries(collection.properties ?? {})) {\n if (!isVectorProperty(prop)) continue;\n // Assembled in the CREATE TABLE generator's order, so the column this\n // file adds and the column `schema.sql` used to declare are the same\n // column. A vector property is never a primary key and carries no\n // SQL-level default, which is what leaves only these two.\n const validation = (prop as { validation?: { required?: boolean; unique?: boolean } }).validation;\n let modifiers = \"\";\n if (validation?.unique) modifiers += \" UNIQUE\";\n if (validation?.required) modifiers += \" NOT NULL\";\n specs.push({\n schema,\n table,\n column: resolveColumn(propName, prop as Property),\n dimensions: prop.dimensions,\n modifiers\n });\n }\n\n return specs;\n};\n\n/** The column type — the one place `VECTOR(n)` is spelled. */\nexport const vectorColumnType = (spec: Pick<VectorColumnSpec, \"dimensions\">): string =>\n `VECTOR(${spec.dimensions})`;\n\n/** The column definition as it appears inside `CREATE TABLE`. */\nexport const vectorColumnDefinition = (spec: VectorColumnSpec): string =>\n `\"${spec.column}\" ${vectorColumnType(spec)}${spec.modifiers}`;\n\n/**\n * Refuse — or perform — a `dimensions` change that `ADD COLUMN IF NOT EXISTS`\n * would otherwise swallow.\n *\n * Without this the file *launders* the change: the ADD is a no-op against a\n * column that exists, so a project that went from 384 to 768 dimensions would\n * push clean, keep a 384-wide column, and fail on the next insert with a\n * message about the row rather than about the config. Atlas used to catch this\n * — it owned the column and planned the `ALTER … TYPE` — and taking the column\n * out of its sight is exactly what makes the guard necessary.\n *\n * The widening is performed when the column holds no values, because that is\n * the case Atlas handled and it is the common one: a developer changing\n * embedding models before there are any embeddings. With values present the\n * conversion is pgvector's to reject — every stored vector is the old width —\n * so this refuses first and names the statement to run afterwards.\n *\n * `atttypmod` on a `vector` column *is* the dimension count: pgvector stores it\n * directly rather than offsetting it the way `varchar` does. `-1` means the\n * column was declared as a bare `vector`, which is drift in the same sense.\n */\nexport const vectorDimensionGuard = (spec: VectorColumnSpec): string => {\n const relation = `\"${spec.schema}\".\"${spec.table}\"`;\n return `DO $rebase_vector$\nDECLARE actual int;\nBEGIN\n SELECT a.atttypmod INTO actual\n FROM pg_attribute a\n JOIN pg_type t ON t.oid = a.atttypid\n WHERE a.attrelid = ${quote(relation)}::regclass\n AND a.attname = ${quote(spec.column)}\n AND NOT a.attisdropped\n AND t.typname = 'vector';\n IF actual IS NOT NULL AND actual <> ${spec.dimensions} THEN\n IF EXISTS (SELECT 1 FROM ${relation} WHERE \"${spec.column}\" IS NOT NULL) THEN\n RAISE EXCEPTION 'Rebase: ${spec.schema}.${spec.table}.\"${spec.column}\" declares ${spec.dimensions} dimensions but the column is vector(%), and it already holds values of the old width. pgvector cannot convert them. Re-embed the rows at ${spec.dimensions} dimensions (or clear the column), then re-apply: ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};', actual;\n END IF;\n ALTER TABLE ${relation} ALTER COLUMN \"${spec.column}\" TYPE ${vectorColumnType(spec)};\n END IF;\nEND\n$rebase_vector$;`;\n};\n\n/**\n * The object names a collection's vector properties own, unqualified.\n *\n * What Atlas has to be told to exclude. Only the column and the ANN indexes: a\n * `UNIQUE` or `NOT NULL` on the column is a property *of* the column, and\n * excluding the column takes them with it — measured against Atlas 1.2.3 and\n * 1.3.2, where a target carrying `vector(3) NOT NULL UNIQUE` and a `schema.sql`\n * carrying neither reported \"Schema is synced, no changes to be made\".\n */\nexport const vectorObjectNames = (\n collection: CollectionConfig,\n resolveColumn: (propName: string, prop?: Property | null) => string\n): string[] => [\n ...buildVectorColumnSpecs(collection, resolveColumn).map(spec => spec.column),\n ...vectorIndexNames(buildVectorIndexPlan(collection, resolveColumn))\n];\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2-canary.g08eed46",
|
|
4
4
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"./package.json": "./package.json"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ariga/atlas": "^1.2.3",
|
|
46
45
|
"arg": "^5.0.2",
|
|
47
46
|
"chalk": "^5.6.2",
|
|
48
47
|
"chokidar": "5.0.0",
|
|
@@ -51,13 +50,14 @@
|
|
|
51
50
|
"execa": "^9.6.1",
|
|
52
51
|
"pg": "^8.22.0",
|
|
53
52
|
"ws": "^8.21.1",
|
|
54
|
-
"@rebasepro/codegen": "0.19.
|
|
55
|
-
"@rebasepro/common": "0.19.
|
|
56
|
-
"@rebasepro/
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
53
|
+
"@rebasepro/codegen": "0.19.2-canary.g08eed46",
|
|
54
|
+
"@rebasepro/common": "0.19.2-canary.g08eed46",
|
|
55
|
+
"@rebasepro/types": "0.19.2-canary.g08eed46",
|
|
56
|
+
"@rebasepro/utils": "0.19.2-canary.g08eed46",
|
|
57
|
+
"@rebasepro/server": "0.19.2-canary.g08eed46"
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|
|
60
|
+
"@electric-sql/pglite": "0.5.6",
|
|
61
61
|
"@hono/node-server": "^2.0.12",
|
|
62
62
|
"@jest/globals": "^30.4.1",
|
|
63
63
|
"@types/jest": "^30.0.0",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"typescript": "^6.0.3",
|
|
71
71
|
"vite": "^8.1.5",
|
|
72
72
|
"vitest": "^4.1.10",
|
|
73
|
-
"@rebasepro/client": "0.19.
|
|
73
|
+
"@rebasepro/client": "0.19.2-canary.g08eed46"
|
|
74
74
|
},
|
|
75
75
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
76
76
|
"publishConfig": {
|
|
@@ -79,12 +79,20 @@
|
|
|
79
79
|
"files": [
|
|
80
80
|
"dist"
|
|
81
81
|
],
|
|
82
|
+
"peerDependencies": {
|
|
83
|
+
"@ariga/atlas": "^1.2.3"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"@ariga/atlas": {
|
|
87
|
+
"optional": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
82
90
|
"scripts": {
|
|
83
91
|
"watch": "vite build --watch",
|
|
84
92
|
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
|
|
85
93
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
86
|
-
"test": "jest",
|
|
87
|
-
"test:watch": "jest --watch",
|
|
94
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
95
|
+
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch",
|
|
88
96
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
89
97
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
90
98
|
"smoke:baas": "tsx scripts/smoke-baas.ts",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-errors-DxN4yzdH.js","names":[],"sources":["../src/utils/pg-error-utils.ts","../src/cli-errors.ts"],"sourcesContent":["/**\n * Shared PostgreSQL error extraction and user-friendly message formatting.\n *\n * Drizzle wraps native PG errors in a `.cause` chain. These utilities\n * unwrap that chain to get the real PostgreSQL error (identified by a\n * 5-character alphanumeric `code` such as `42P01`) and translate it into\n * a message that is safe and helpful to show to end-users.\n */\n\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Shape of a deliberate client-facing error — `ApiError` from\n * `@rebasepro/server`, or anything else carrying a 4xx `statusCode`.\n *\n * Matched structurally rather than with `instanceof`: `@rebasepro/server` can\n * be loaded twice (published dist vs. workspace source), which breaks class\n * identity — `PersistService` hedges against the same thing by also accepting\n * `name === \"ApiError\"`.\n */\ninterface ClientFacingError extends Error {\n statusCode?: number;\n code?: string;\n /** See `ApiError.expected` — routine outcomes log at debug, not warn. */\n expected?: boolean;\n}\n\n/**\n * Return the error when it is a deliberate 4xx, otherwise null.\n *\n * A thrown `ApiError` is a decision the server made about the request, not a\n * database failure: its message and code are already written for the client.\n */\nfunction asClientFacingError(error: unknown): ClientFacingError | null {\n if (!(error instanceof Error)) return null;\n const e = error as ClientFacingError;\n if (typeof e.statusCode !== \"number\" || e.statusCode < 400 || e.statusCode >= 500) return null;\n return e;\n}\n\n/** Shape of PostgreSQL errors with diagnostic metadata. */\nexport interface PostgresError extends Error {\n code?: string;\n detail?: string;\n hint?: string;\n constraint?: string;\n column?: string;\n table?: string;\n dataType?: string;\n cause?: unknown;\n}\n\n/**\n * Extract the underlying PostgreSQL error from a Drizzle wrapper.\n * Drizzle wraps PG errors in a `cause` property — this function\n * recursively walks the chain until it finds an object with a PG\n * error code (5-char alphanumeric, e.g. `42P01`).\n */\nexport function extractPgError(error: unknown): PostgresError | null {\n if (!error || typeof error !== \"object\") return null;\n if (!(error instanceof Error)) {\n // Check non-Error objects for a cause chain (Drizzle sometimes wraps oddly)\n if (\"cause\" in error && (error as Record<string, unknown>).cause && typeof (error as Record<string, unknown>).cause === \"object\") {\n return extractPgError((error as Record<string, unknown>).cause);\n }\n return null;\n }\n\n // Check if the error itself has a PG error code\n if (\"code\" in error && typeof (error as PostgresError).code === \"string\" && /^[0-9A-Z]{5}$/.test((error as PostgresError).code!)) {\n return error as PostgresError;\n }\n\n // Check the cause chain (Drizzle wraps PG errors)\n if (error.cause && typeof error.cause === \"object\") {\n return extractPgError(error.cause);\n }\n\n return null;\n}\n\n/**\n * Whether the failure came back from Postgres rather than from building the\n * query — which decides whether a fallback query is worth issuing.\n *\n * Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds\n * RLS). Once a statement raises, that transaction is aborted, and every later\n * statement on it returns `25P02` — \"current transaction is aborted, commands\n * ignored until end of transaction block\". So a retry after a database error\n * cannot succeed, and it replaces a precise diagnosis (\"invalid input syntax\n * for type uuid\") with a generic one. Rethrow instead.\n *\n * A query the driver could not even build — a missing reciprocal relation, say\n * — never reached Postgres, leaves the transaction usable, and is exactly what\n * the fallback paths exist for.\n */\nexport function reachedDatabase(error: unknown): boolean {\n return extractPgError(error) !== null;\n}\n\n/**\n * Walk the error cause chain and return the deepest meaningful message.\n */\nexport function extractCauseMessage(error: unknown): string | null {\n if (!error || typeof error !== \"object\") return null;\n if (!(error instanceof Error)) return null;\n\n if (error.cause && typeof error.cause === \"object\") {\n const deeper = extractCauseMessage(error.cause);\n if (deeper) return deeper;\n // The cause itself has a message\n if (error.cause instanceof Error && error.cause.message) {\n return error.cause.message;\n }\n }\n return null;\n}\n\n/**\n * Codes that mean \"this connection will never work as configured\".\n *\n * A wrong password or a database that does not exist is a settled fact about\n * the connection string, not a transient fault — retrying produces the same\n * answer forever.\n */\nconst UNRECOVERABLE_CONNECT_CODES = new Set([\n \"28P01\", // invalid_password\n \"28000\", // invalid_authorization_specification\n \"3D000\", // invalid_catalog_name — the database does not exist\n \"42501\" // insufficient_privilege\n]);\n\nexport interface ConnectFailure {\n /** True when retrying cannot help: the connection string itself is wrong. */\n fatal: boolean;\n /** The deepest message available — the Postgres one where there is one. */\n reason: string;\n /** The `SQLSTATE`, when the failure came from Postgres rather than the socket. */\n code?: string;\n}\n\n/**\n * Describe a failed connection attempt in terms a developer can act on.\n *\n * The error a caller catches is Drizzle's wrapper: its message is\n * `Failed query: SELECT 1` and its stack runs through drizzle internals, while\n * the sentence that says what is actually wrong — \"password authentication\n * failed for user …\", \"database … does not exist\" — sits in `.cause`. Logging\n * the wrapper, as the bootstrapper used to, tells a developer with a typo in\n * their `DATABASE_URL` nothing at all.\n */\nexport function classifyConnectFailure(error: unknown): ConnectFailure {\n const pgError = extractPgError(error);\n const reason =\n pgError?.message ??\n extractCauseMessage(error) ??\n (error instanceof Error ? error.message : String(error));\n return {\n fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),\n reason,\n code: pgError?.code\n };\n}\n\n/**\n * Detect whether an error is specifically a role-switching permission failure\n * (e.g. \"permission denied to set role\" or \"must be member of role\"),\n * as opposed to a table-level permission denial.\n *\n * This is used by the backend driver to auto-disable role switching when the\n * connection user lacks SET ROLE privileges, rather than surfacing a confusing\n * error to the Studio SQL Editor user.\n */\nexport function isRoleSwitchingPermissionError(error: unknown): boolean {\n const pgError = extractPgError(error);\n if (!pgError || pgError.code !== \"42501\") return false;\n const msg = pgError.message.toLowerCase();\n return msg.includes(\"set role\") || msg.includes(\"member of role\");\n}\n\n/**\n * Was this `42501` the *caller* being refused by a policy, rather than the\n * server lacking a privilege?\n *\n * Both arrive as `insufficient_privilege`, and they are opposite kinds of\n * problem. A row-level-security refusal is a working access-control system\n * doing its job: the caller asked for something their policies do not permit,\n * which is a 403 and nobody's bug. A missing `GRANT` is the deployment being\n * wrong — the connection role cannot touch the table at all, no policy is\n * involved, and nothing the caller changes about the request will help.\n *\n * Postgres distinguishes them in the message, so this does too:\n *\n * new row violates row-level security policy for table \"notes\" → the caller\n * permission denied for table notes → the server\n *\n * Only writes reach this. A read that RLS excludes is not an error — the rows\n * are filtered and the caller gets an empty page — so the erroring case is\n * specifically an `INSERT`/`UPDATE` whose row fails a policy's `WITH CHECK`.\n *\n * Matched on the message because that is the only thing carrying the\n * distinction; the SQLSTATE is identical either way. Narrow by design: anything\n * not naming row-level security stays the server's problem, since reporting a\n * genuine privilege misconfiguration as \"forbidden\" would send an operator\n * hunting for a policy bug that does not exist.\n */\nexport function isRowLevelSecurityDenial(error: unknown): boolean {\n const pgError = extractPgError(error);\n if (!pgError || pgError.code !== \"42501\") return false;\n return pgError.message.toLowerCase().includes(\"row-level security policy\");\n}\n\n/**\n * Translate a raw PostgreSQL error into a user-friendly message.\n *\n * @param pgError - The extracted PostgreSQL error (from {@link extractPgError})\n * @param context - A human-readable context string (e.g. collection slug or path)\n * @returns An object with a `message` safe for the client and the PG `code`.\n */\nexport function pgErrorToFriendlyMessage(\n pgError: PostgresError,\n context: string,\n options: { verbose?: boolean } = {}\n): { message: string; code: string } {\n // Postgres's own `DETAIL` and `HINT`, and the raw driver message, are what\n // make these errors useful while you are building — and what makes them an\n // oracle once the server is answering strangers.\n //\n // The sharp one is `23505`: its detail reads `Key (email)=(a@b.c) already\n // exists.`, which answers \"is this person registered?\" for any address, on\n // a table whose rows RLS is otherwise hiding completely. The rest leak\n // physical column and constraint names, which are the map for the next\n // question. So in production the shape of the failure is served — what\n // rule was broken, and where — and the values are not.\n //\n // The full text is not lost: the caller logs the original error. This\n // decides what crosses the wire, not what is recorded.\n const verbose = options.verbose ?? process.env.NODE_ENV !== \"production\";\n\n const detail = verbose ? (pgError.detail as string | undefined) : undefined;\n const hint = verbose ? (pgError.hint as string | undefined) : undefined;\n const constraint = pgError.constraint as string | undefined;\n const column = pgError.column as string | undefined;\n const table = pgError.table as string | undefined;\n const dataType = verbose ? (pgError.dataType as string | undefined) : undefined;\n const rawMessage = pgError.message || \"Unknown database error\";\n const pgMessage = verbose ? rawMessage : \"\";\n const code = pgError.code || \"UNKNOWN\";\n\n const suffix = hint ? ` Hint: ${hint}` : \"\";\n const tableRef = table ?? context;\n\n /** `\": <postgres said>\"`, or nothing when the details are withheld. */\n const said = pgMessage ? `: ${pgMessage}` : \"\";\n\n switch (pgError.code) {\n case \"23503\": // foreign_key_violation\n return {\n message: detail\n ? `Foreign key constraint violated: ${detail}${suffix}`\n : `Cannot complete operation: a foreign key constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code\n };\n case \"23505\": // unique_violation\n return {\n message: detail\n ? `Duplicate value: ${detail}${suffix}`\n : `Cannot complete operation: a unique constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code\n };\n case \"23502\": // not_null_violation\n return {\n message: `Missing required field: \"${column ?? \"unknown\"}\" in \"${tableRef}\" cannot be empty.${suffix}`,\n code\n };\n case \"23514\": // check_violation\n return {\n message: `Validation failed: a check constraint${constraint ? ` (${constraint})` : \"\"} was violated in \"${context}\".${suffix}`,\n code\n };\n case \"22P02\": // invalid_text_representation (e.g. invalid UUID, wrong enum value)\n return {\n message: `Invalid data format in \"${context}\"${said}.${suffix}`,\n code\n };\n case \"22001\": // string_data_right_truncation (value too long)\n return {\n message: `Value too long for column \"${column ?? \"unknown\"}\" in \"${tableRef}\"${said}.${suffix}`,\n code\n };\n case \"22003\": // numeric_value_out_of_range\n return {\n message: `Numeric value out of range for column \"${column ?? \"unknown\"}\" in \"${tableRef}\"${said}.${suffix}`,\n code\n };\n case \"42703\": // undefined_column\n return {\n message: `Unknown column in \"${tableRef}\"${said}. Check if your schema is up to date (run migrations).${suffix}`,\n code\n };\n case \"42P01\": // undefined_table\n return {\n message: `Table not found for \"${context}\"${said}. Check if your schema is up to date (run migrations).${suffix}`,\n code\n };\n case \"42501\": // insufficient_privilege\n // Two unrelated failures share this SQLSTATE, and the old message\n // named both causes because it could not tell them apart — which\n // meant it was half wrong whichever one had happened, and sent the\n // reader to check the other. Postgres says which in its own message.\n return rawMessage.toLowerCase().includes(\"row-level security policy\")\n ? {\n // The caller. Their policies do not permit this row; the\n // deployment is working exactly as configured.\n message: `Not permitted to write this row in \"${tableRef}\": it does not satisfy the row-level security policy.${suffix}`,\n code\n }\n : {\n // The deployment. No policy is involved — the connecting\n // role cannot touch the table at all.\n message: `Permission denied on \"${tableRef}\": the database role this server connects as lacks privileges on it.${suffix}`,\n code\n };\n case \"28000\": // invalid_authorization_specification\n return {\n message: `Authorization failed for \"${context}\". Check your database credentials.${suffix}`,\n code\n };\n default: {\n // Unhandled PG code — still surface the actual database message\n const parts = [`Database error in \"${context}\" [${code}]${said}`];\n if (detail) parts.push(`Detail: ${detail}`);\n if (column) parts.push(`Column: ${column}`);\n if (dataType) parts.push(`Data type: ${dataType}`);\n if (constraint) parts.push(`Constraint: ${constraint}`);\n if (hint) parts.push(`Hint: ${hint}`);\n return { message: parts.join(\". \"), code };\n }\n }\n}\n\n/**\n * Sanitize any error into a message safe and helpful for the client.\n *\n * A deliberate 4xx (`ApiError`) passes through untouched — the server already\n * decided what the client should read. Otherwise the PG error is extracted\n * from the Drizzle cause chain, falling back to a generic message that\n * doesn't leak SQL.\n *\n * @param error - The raw caught error\n * @param context - A human-readable context string (e.g. collection path)\n * @returns An object with `message` (user-friendly) and optional `code`\n * (the `ApiError` code, or the PG SQLSTATE).\n */\nexport function sanitizeErrorForClient(error: unknown, context: string): { message: string; code?: string } {\n // ── A deliberate 4xx is not a database failure ──────────────────\n // Its message and code are written for the client; replacing them with\n // \"Check server logs\" would discard the only diagnosis the caller gets\n // (e.g. the offending filter field and the collection's valid ones).\n // Log level follows the same convention as the HTTP error handler in\n // @rebasepro/server: routine outcomes at debug, everything else at warn.\n const clientError = asClientFacingError(error);\n if (clientError) {\n const line = `[API ${clientError.statusCode} ${clientError.code ?? \"BAD_REQUEST\"}] in \"${context}\": ${clientError.message}`;\n if (clientError.expected) {\n logger.debug(line);\n } else {\n logger.warn(`⚠️ ${line}`);\n }\n return { message: clientError.message, ...(clientError.code && { code: clientError.code }) };\n }\n\n // ── Always log the full, unsanitized error server-side ──────────\n const pgError = extractPgError(error);\n\n if (pgError) {\n logger.error(`[PG ${pgError.code}] Error in \"${context}\"`, {\n code: pgError.code,\n message: pgError.message,\n detail: pgError.detail,\n hint: pgError.hint,\n column: pgError.column,\n table: pgError.table,\n constraint: pgError.constraint,\n dataType: pgError.dataType\n // The outer Drizzle wrapper message used to be logged here \"for\n // full context\": it is `Failed query: <sql>\\nparams: <values>`, so\n // it published the statement and every bound value (an email, a\n // password hash) on every realtime data failure. The SQLSTATE,\n // detail, table, column and constraint above are the diagnostic\n // value; the wrapper added only the leak. `logger` strips the\n // wrapper as well, but the field itself carried nothing else.\n });\n return pgErrorToFriendlyMessage(pgError, context);\n }\n\n // No PG error found — log the raw error as-is\n logger.error(`Database error in \"${context}\" (no PG error extracted)`, {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n cause: error instanceof Error && error.cause\n ? (error.cause instanceof Error ? error.cause.message : String(error.cause))\n : undefined\n });\n\n // Try to get the deepest cause message\n const causeMessage = extractCauseMessage(error);\n if (causeMessage) {\n return { message: `Database error in \"${context}\": ${causeMessage}` };\n }\n\n // Last resort — generic message, never leak raw SQL\n return { message: `Could not load data for \"${context}\". Check server logs for details.` };\n}\n","import chalk from \"chalk\";\nimport { outWarn, outError } from \"./cli-output\";\nimport { extractCauseMessage } from \"./utils/pg-error-utils\";\n\n/**\n * Detect whether an error (or AggregateError wrapping multiple attempts)\n * represents an ECONNREFUSED — i.e. the database is simply not running.\n *\n * Handles:\n * - Direct `{ code: \"ECONNREFUSED\" }` errors from Node `net`\n * - `AggregateError` from dual-stack IPv4+IPv6 connection attempts\n * - Drizzle's `cause`-wrapped pg errors\n */\nexport function isEconnrefused(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown; errors?: unknown[] };\n if (e.code === \"ECONNREFUSED\") return true;\n // AggregateError from Node net (dual-stack IPv4 + IPv6)\n if (Array.isArray(e.errors)) {\n return e.errors.some(inner =>\n inner && typeof inner === \"object\" && (inner as { code?: string }).code === \"ECONNREFUSED\"\n );\n }\n // Drizzle wraps the pg error in `cause`\n if (e.cause && typeof e.cause === \"object\") {\n return isEconnrefused(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL authentication failures.\n * PG error codes: 28P01 (invalid_password), 28000 (invalid_authorization_specification)\n */\nexport function isAuthFailure(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; cause?: unknown };\n if (e.code === \"28P01\" || e.code === \"28000\") return true;\n if (e.cause && typeof e.cause === \"object\") {\n return isAuthFailure(e.cause);\n }\n // Also check the message for common pg auth failure text\n if (\"message\" in e && typeof (e as { message?: string }).message === \"string\") {\n const msg = (e as { message: string }).message.toLowerCase();\n if (msg.includes(\"password authentication failed\") || msg.includes(\"no pg_hba.conf entry\")) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Detect the \"SSL is not enabled on the server\" failure — the client attempted\n * an SSL handshake against a Postgres server that doesn't support it (common\n * with a plain local dev database). The fix is `?sslmode=disable` on the URL.\n */\nexport function isSslNotEnabled(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { message?: string; cause?: unknown };\n if (typeof e.message === \"string\" && e.message.toLowerCase().includes(\"ssl is not enabled on the server\")) {\n return true;\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isSslNotEnabled(e.cause);\n }\n return false;\n}\n\n/**\n * Detect PostgreSQL \"cannot drop ... because other objects depend on it\"\n * (error code 2BP01, dependent_objects_still_exist). This is the failure that\n * strands a declarative `db push` half-applied when a collection is removed but\n * an enum type it defined is still referenced by another object.\n */\nexport function isDependencyDropError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: string; message?: string; cause?: unknown };\n if (e.code === \"2BP01\") return true;\n if (typeof e.message === \"string\") {\n const msg = e.message.toLowerCase();\n if (msg.includes(\"other objects depend on it\") || msg.includes(\"cannot drop type\")) {\n return true;\n }\n }\n if (e.cause && typeof e.cause === \"object\") {\n return isDependencyDropError(e.cause);\n }\n return false;\n}\n\n/**\n * Parse host:port from a DATABASE_URL for display purposes.\n *\n * Exported because every message about a connection has to name the thing it\n * could not reach, and the boot path needs the same rendering the CLI banners\n * use — including the same refusal to print the URL itself, which carries the\n * password.\n */\nexport function parseHostInfo(databaseUrl: string): string {\n try {\n const parsed = new URL(databaseUrl);\n return `${parsed.hostname}:${parsed.port || 5432}`;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * The sentence the operating system actually produced, dug out of the wrappers.\n *\n * `connect ECONNREFUSED 127.0.0.1:5432` is written by `net`, then wrapped by\n * `pg`, then wrapped again by Drizzle as `Failed query: …` — and on a\n * dual-stack host it is not in `.cause` at all but inside the\n * `AggregateError.errors` array of one attempt per resolved address. Printing\n * the banner without it loses the one token every search engine, runbook and\n * colleague recognises.\n */\nexport function deepestErrorMessage(err: unknown): string | null {\n if (!err || typeof err !== \"object\") return null;\n const e = err as { message?: string; code?: string; cause?: unknown; errors?: unknown[] };\n\n if (Array.isArray(e.errors)) {\n for (const inner of e.errors) {\n const deeper = deepestErrorMessage(inner);\n if (deeper) return deeper;\n }\n }\n if (e.cause) {\n const deeper = deepestErrorMessage(e.cause);\n if (deeper) return deeper;\n }\n if (typeof e.message === \"string\" && e.message && !e.message.startsWith(\"Failed query:\")) {\n return e.code ? `${e.message} (${e.code})` : e.message;\n }\n return null;\n}\n\n/** One indented line naming the driver's own reason, or nothing. */\nfunction driverReasonLine(err: unknown): string {\n const reason = err === undefined ? null : deepestErrorMessage(err);\n return reason ? ` The driver said: ${reason}\\n\\n` : \"\";\n}\n\n/**\n * Format a diagnostic banner for ECONNREFUSED errors.\n */\nfunction formatConnectionRefusedBanner(databaseUrl: string, err?: unknown): string {\n const hostInfo = parseHostInfo(databaseUrl);\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot connect to PostgreSQL at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n driverReasonLine(err) +\n ` The database server is not running or is not accepting\\n` +\n ` connections. Common fixes:\\n` +\n `\\n` +\n ` • docker compose up -d db (the service a Rebase scaffold ships)\\n` +\n ` • brew services start postgresql@18\\n` +\n ` • Verify DATABASE_URL in your .env file\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for authentication failures.\n */\nfunction formatAuthFailureBanner(databaseUrl: string, err?: unknown): string {\n const hostInfo = parseHostInfo(databaseUrl);\n let username = \"unknown\";\n try {\n username = new URL(databaseUrl).username || \"unknown\";\n } catch { /* ignore */ }\n\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Authentication failed for user \"${username}\" at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n driverReasonLine(err) +\n ` PostgreSQL rejected the credentials. Common fixes:\\n` +\n `\\n` +\n ` • Check the username and password in DATABASE_URL\\n` +\n ` • Verify the user exists: psql -c \"\\\\du\"\\n` +\n ` • Reset the password: ALTER USER ${username} PASSWORD 'new_password';\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for \"SSL is not enabled on the server\".\n */\nfunction formatSslNotEnabledBanner(databaseUrl: string): string {\n const hostInfo = parseHostInfo(databaseUrl);\n const suggestion = databaseUrl.includes(\"?\") ? \"&sslmode=disable\" : \"?sslmode=disable\";\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ SSL is not enabled on the PostgreSQL server at ${hostInfo}\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` The client tried to connect over SSL, but the server does not\\n` +\n ` support it. This is normal for a plain local dev database.\\n` +\n `\\n` +\n ` Fix: append ${chalk.bold(\"sslmode=disable\")} to DATABASE_URL, e.g.\\n` +\n `\\n` +\n ` DATABASE_URL=...${suggestion}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Format a diagnostic banner for a dependency-drop failure during `db push`.\n * Explains that the database may be left partially migrated and how to recover.\n */\nfunction formatDependencyDropBanner(): string {\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Schema push failed: a type/table could not be dropped\\n` +\n ` because other objects still depend on it.\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` ${chalk.yellow(\"The database may now be partially migrated.\")} Atlas applies\\n` +\n ` statements individually, so earlier changes in this push may\\n` +\n ` already be committed while later ones failed.\\n` +\n `\\n` +\n ` This commonly happens when a collection is removed but an enum\\n` +\n ` type it defined is still referenced. To recover:\\n` +\n `\\n` +\n ` 1. Inspect the leftover object named in the error above.\\n` +\n ` 2. Drop it with CASCADE, e.g.:\\n` +\n ` psql \"$DATABASE_URL\" -c 'DROP TYPE \"<name>\" CASCADE;'\\n` +\n ` 3. Re-run: ${chalk.bold.green(\"rebase db push\")}\\n` +\n `\\n` +\n ` Prefer a safe, versioned workflow? Use ${chalk.bold(\"rebase db generate\")}\\n` +\n ` + ${chalk.bold(\"rebase db migrate\")} instead of push for destructive changes.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/**\n * Pre-flight check: verify that the database is reachable before running\n * a heavy subprocess (Atlas, migrations, etc.).\n *\n * Exits with code 1 and a friendly banner on known failure modes.\n * On unknown errors, logs a warning and allows the caller to proceed.\n */\nexport async function checkDatabaseConnectivity(databaseUrl: string): Promise<void> {\n let client: import(\"pg\").Client | undefined;\n try {\n const { Client } = await import(\"pg\");\n client = new Client({\n connectionString: databaseUrl,\n connectionTimeoutMillis: 5000\n });\n await client.connect();\n await client.query(\"SELECT 1\");\n } catch (err: unknown) {\n if (isEconnrefused(err)) {\n outError(formatConnectionRefusedBanner(databaseUrl, err));\n process.exit(1);\n }\n if (isAuthFailure(err)) {\n outError(formatAuthFailureBanner(databaseUrl, err));\n process.exit(1);\n }\n if (isSslNotEnabled(err)) {\n outError(formatSslNotEnabledBanner(databaseUrl));\n process.exit(1);\n }\n // Unknown error — warn but don't block; let the downstream tool surface details\n outWarn(chalk.yellow(` ⚠ Could not verify database connectivity: ${err instanceof Error ? err.message : String(err)}`));\n outWarn(chalk.gray(\" Proceeding anyway — the command may fail if the database is unreachable.\"));\n } finally {\n try {\n await client?.end();\n } catch {\n // ignore cleanup errors\n }\n }\n}\n\n/**\n * Post-hoc error diagnosis for direct database operations (e.g. applyPolicies).\n * Returns a formatted diagnostic string if the error matches a known pattern,\n * or null if unrecognized.\n */\nexport function diagnoseDbError(err: unknown, databaseUrl?: string): string | null {\n if (isEconnrefused(err)) {\n return formatConnectionRefusedBanner(databaseUrl || \"\", err);\n }\n if (isAuthFailure(err)) {\n return formatAuthFailureBanner(databaseUrl || \"\", err);\n }\n if (isSslNotEnabled(err)) {\n return formatSslNotEnabledBanner(databaseUrl || \"\");\n }\n if (isDependencyDropError(err)) {\n return formatDependencyDropBanner();\n }\n return null;\n}\n\n/**\n * Say why the command failed, on the way out.\n *\n * The entry point below used to be `.catch(() => process.exit(1))`, which threw\n * the error away. Every message this file and its services raise — \"Branch\n * \\\"x\\\" already exists.\", \"the source database has active connections\", \"Branch\n * name is too long\" — was written, wrapped in the right PG error code, and then\n * discarded one frame before it reached a terminal. What a developer saw was a\n * header line, no error, and exit 1.\n *\n * Two shapes are deliberately kept quiet:\n *\n * - **A child process that already spoke.** Atlas, `pg_dump` and `psql` run\n * with inherited stdio, so their diagnosis is on the terminal already and\n * execa's wrapper adds only `Command failed with exit code 1: atlas …`.\n * `packages/cli` filters exactly these two phrasings one level up\n * (`runDbCommand`), and this is that filter, for the process that is actually\n * throwing.\n *\n * - **A message that is only a query.** Drizzle reports failures as\n * `Failed query: <sql> params:` and hides the real PostgreSQL error in\n * `cause`, so the wrapper alone tells a reader nothing they can act on. The\n * cause is appended when it says something the message does not.\n *\n * - **An error that has already printed its own diagnosis**, marked\n * `alreadyReported`. `CollectionsPathMissing` is one: it prints the path, what\n * it resolved to and the cwd it resolved against, and then throws so the entry\n * point owns the exit code. Repeating its one-line summary underneath would\n * undo the \"printed once\" this whole path exists for.\n */\nexport function reportCommandFailure(error: unknown): void {\n const message = error instanceof Error ? error.message : String(error ?? \"\");\n\n if ((error as { alreadyReported?: boolean } | null)?.alreadyReported) return;\n if (!message || /Command failed|exited with code/i.test(message)) return;\n\n outError(\"\");\n outError(chalk.red(` ✗ ${message}`));\n\n const cause = extractCauseMessage(error);\n if (cause && !message.includes(cause)) {\n outError(chalk.gray(` ${cause}`));\n }\n outError(\"\");\n}\n\n/* ------------------------------------------------------------------------- *\n * Atlas failures that have a remedy\n *\n * Atlas runs with a teed stderr, so what reaches these is its output as text,\n * not a `pg` error object carrying a `code`. They are deliberately NOT part of\n * `diagnoseDbError`: that one is shared with the boot path, and \"record a\n * baseline with `rebase db migrate`\" is advice for somebody standing at a\n * terminal, not for a container that has just failed to start.\n * ------------------------------------------------------------------------- */\n\n/**\n * `migrate apply` refusing because the database already has the schema.\n *\n * `42710` (duplicate_object) and `42P07` (duplicate_table) are what a migration\n * hits when boot-ensure — or a `db push` — has already provisioned the objects\n * it was going to create. Since boot-ensure provisions *every* production\n * database, this is the normal case rather than the exotic one, and the raw\n * failure (`pq: type \"posts_status\" already exists (42710)`, then `sql/migrate:\n * write revision: … current transaction is aborted`) names no way forward.\n */\nexport function parseAlreadyProvisioned(text: string): { object: string; code: string } | null {\n const withCode = /(?:type|relation|constraint|schema) \"([^\"]+)\" already exists \\((42710|42P07)\\)/.exec(text);\n if (withCode) return { object: withCode[1], code: withCode[2] };\n // Some Atlas builds keep the SQLSTATE on a following line instead.\n const bare = /(?:type|relation|constraint|schema) \"([^\"]+)\" already exists/.exec(text);\n if (bare && /42710|42P07/.test(text)) return { object: bare[1], code: /42P07/.test(text) ? \"42P07\" : \"42710\" };\n return null;\n}\n\n/**\n * What to do about it: tell Atlas the database is already at a version.\n *\n * Atlas's own mechanism (`migrate apply --baseline <version>`), not a ledger of\n * ours — it writes the revision row Atlas reads, so every later `rebase db\n * migrate` is an ordinary one.\n */\nexport function formatBaselineRemedy(version: string | null, object?: string): string {\n const versionToken = version ?? \"<version>\";\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ This database already has the schema this migration creates\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n (object ? ` ${chalk.gray(`\"${object}\" is already there, so the migration cannot create it.`)}\\n\\n` : \"\") +\n ` Nothing is wrong with the migration — the database was provisioned\\n` +\n ` another way. Every Rebase boot ensures the schema, and ${chalk.bold(\"rebase db push\")}\\n` +\n ` applies it directly, so a database that has ever run either one is\\n` +\n ` ahead of a migration history that was never recorded.\\n` +\n `\\n` +\n ` Record where it already is, then migrate normally:\\n` +\n `\\n` +\n ` ${chalk.bold.green(`rebase db migrate --baseline ${versionToken}`)}\\n` +\n ` ${chalk.bold.green(\"rebase db migrate\")}\\n` +\n `\\n` +\n ` The baseline is the version already applied — the numeric prefix of\\n` +\n ` the migration file describing what is in the database now. Migrations\\n` +\n ` after it run; it and everything before it are marked done.\\n` +\n `\\n` +\n ` On a database nothing has ever booted against, none of this is needed.\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/** `ALTER TABLE … SET NOT NULL` on a table that already holds rows (23502). */\nexport function parseNotNullViolation(text: string): { table: string; column: string } | null {\n const match = /column \"([^\"]+)\" of relation \"([^\"]+)\" contains null values/.exec(text);\n return match ? { table: match[2], column: match[1] } : null;\n}\n\n/**\n * The three ways out, because there is no fourth.\n *\n * Boot-ensure handles this case — it adds the column nullable and sets NOT NULL\n * only when the table is empty — so a push that dies here is strictly worse\n * than the boot that would have run instead, and the developer deserves to be\n * told which of the three they want rather than left with `pq: … contains null\n * values (23502)` and no next step.\n */\nexport function formatNotNullViolationBanner(\n violation: { table: string; column: string },\n rowCount: number | null\n): string {\n const { table, column } = violation;\n const rows = rowCount === null\n ? ` \"${table}\" already holds rows, and none of them has a value for \"${column}\".\\n`\n : ` \"${table}\" already holds ${rowCount} row${rowCount === 1 ? \"\" : \"s\"}, and they have no value for \"${column}\".\\n`;\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ Cannot make \"${table}\".\"${column}\" required: existing rows are null\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n rows +\n ` PostgreSQL will not add the NOT NULL until every one of them does.\\n` +\n `\\n` +\n ` Three ways forward:\\n` +\n `\\n` +\n ` 1. Give the property a default, so the push can backfill:\\n` +\n ` ${chalk.gray(`{ type: \"string\", defaultValue: \"…\", validation: { required: true } }`)}\\n` +\n ` 2. Backfill by hand first, then push again:\\n` +\n ` ${chalk.gray(`psql \"$DATABASE_URL\" -c 'UPDATE \"${table}\" SET \"${column}\" = … WHERE \"${column}\" IS NULL;'`)}\\n` +\n ` 3. Leave it optional — take \\`validation.required\\` off the property.\\n` +\n `\\n` +\n ` ${chalk.gray(\"Nothing was applied for this column. `rebase dev` would have added it\")}\\n` +\n ` ${chalk.gray(\"nullable instead: boot sets NOT NULL only when it is safe.\")}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/** Atlas refusing to remove a label from an enum — a renamed `enum` option id. */\nexport function parseEnumLabelDrop(text: string): { label: string; enumType: string } | null {\n const match = /dropping (?:enum )?value \"([^\"]+)\" from enum \"([^\"]+)\" is not supported/.exec(text);\n return match ? { label: match[1], enumType: match[2] } : null;\n}\n\n/**\n * Why the two paths disagree, and what retiring an option id actually costs.\n *\n * Boot-ensure adds enum labels and never removes one, so `rebase dev` accepts\n * the very edit that stops `db push` dead. PostgreSQL has no `ALTER TYPE …\n * DROP VALUE`: a label goes only by rewriting the type, which is a data\n * migration and not a schema push.\n */\nexport function formatEnumLabelDropBanner(drop: { label: string; enumType: string }): string {\n const { label, enumType } = drop;\n return (\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n ` ❌ \"${label}\" cannot be removed from the enum \"${enumType}\"\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n` +\n `\\n` +\n ` PostgreSQL has no ALTER TYPE … DROP VALUE. An option id can be added\\n` +\n ` to an enum but never taken out of one, so renaming an id reads as a\\n` +\n ` removal and a push cannot carry it.\\n` +\n `\\n` +\n ` ${chalk.yellow(\"Nothing was applied.\")} ${chalk.bold(\"rebase dev\")} accepts this same edit — boot adds\\n` +\n ` new labels and never removes one — so the two paths disagree by\\n` +\n ` design, and the old label would simply have stayed behind, unused.\\n` +\n `\\n` +\n ` To keep the id: put \"${label}\" back and change only its label text.\\n` +\n `\\n` +\n ` To retire it for real, it is a data migration:\\n` +\n `\\n` +\n ` 1. Add the new id alongside the old one, and push.\\n` +\n ` 2. Move the rows: ${chalk.gray(`UPDATE … SET … = '<new>' WHERE … = '${label}';`)}\\n` +\n ` 3. Rewrite the type, which is what actually drops the label:\\n` +\n ` ${chalk.gray(\"rebase db generate retire_option && rebase db migrate\")}\\n` +\n ` ${chalk.gray(\"(a migration can CREATE TYPE …, ALTER TABLE … TYPE … USING, DROP TYPE)\")}\\n` +\n `\\n` +\n `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n`\n );\n}\n\n/** How many rows a table holds, or `null` when we cannot say. */\nasync function countRows(databaseUrl: string | undefined, table: string): Promise<number | null> {\n if (!databaseUrl) return null;\n try {\n const { Client } = await import(\"pg\");\n const client = new Client({ connectionString: databaseUrl, connectionTimeoutMillis: 5000 });\n await client.connect();\n try {\n // Identifier interpolation, and it has to be: the name comes from\n // PostgreSQL's own error text, and `count(*)` takes no parameter in\n // that position. Quoted, with any embedded quote doubled.\n const res = await client.query(`SELECT count(*)::int AS n FROM \"${table.replace(/\"/g, \"\\\"\\\"\")}\"`);\n return (res.rows[0] as { n: number } | undefined)?.n ?? null;\n } finally {\n await client.end();\n }\n } catch {\n return null;\n }\n}\n\n/**\n * The remedy for an Atlas invocation that failed, or `null`.\n *\n * Scoped by the invocation, because the same database state means different\n * things to different subcommands: \"already exists\" under `migrate apply` wants\n * a baseline, while under `schema apply` it is a genuine conflict.\n */\nexport async function diagnoseAtlasFailure(context: {\n domain: string;\n args: string[];\n stderr: string;\n databaseUrl?: string;\n /** The newest migration version on disk, named in the baseline remedy. */\n latestMigrationVersion?: string | null;\n}): Promise<string | null> {\n const { domain, args, stderr, databaseUrl } = context;\n\n if (domain === \"migrate\" && args.includes(\"apply\")) {\n const provisioned = parseAlreadyProvisioned(stderr);\n if (provisioned) {\n return formatBaselineRemedy(context.latestMigrationVersion ?? null, provisioned.object);\n }\n }\n\n if (domain === \"schema\" && args.includes(\"apply\")) {\n const enumDrop = parseEnumLabelDrop(stderr);\n if (enumDrop) return formatEnumLabelDropBanner(enumDrop);\n\n const notNull = parseNotNullViolation(stderr);\n if (notNull) return formatNotNullViolationBanner(notNull, await countRows(databaseUrl, notNull.table));\n }\n\n return null;\n}\n"],"mappings":"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"}
|