@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.
Files changed (96) hide show
  1. package/dist/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
  2. package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +81 -7
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
  6. package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
  7. package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
  8. package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
  9. package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
  10. package/dist/cli-errors-C3g_kHBw.js.map +1 -0
  11. package/dist/cli-errors.d.ts +19 -0
  12. package/dist/cli-helpers.d.ts +47 -0
  13. package/dist/cli.js +233 -19
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
  16. package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
  17. package/dist/collections/validate-relations.d.ts +7 -0
  18. package/dist/column-plan-helpers-CpILzHJS.js +145 -0
  19. package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
  20. package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
  21. package/dist/doctor-D5SrGJ5P.js.map +1 -0
  22. package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
  23. package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
  24. package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
  25. package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
  26. package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
  27. package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
  28. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
  29. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
  30. package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
  31. package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
  33. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
  34. package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
  35. package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
  36. package/dist/history/HistoryService.d.ts +23 -4
  37. package/dist/index.es.js +1991 -580
  38. package/dist/index.es.js.map +1 -1
  39. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
  40. package/dist/plan-schema-Hgl62S-w.js +1531 -0
  41. package/dist/plan-schema-Hgl62S-w.js.map +1 -0
  42. package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
  43. package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
  44. package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
  45. package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
  46. package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
  47. package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
  48. package/dist/schema/catalogue-schema.d.ts +97 -0
  49. package/dist/schema/collection-index.d.ts +9 -0
  50. package/dist/schema/column-plan-helpers.d.ts +125 -0
  51. package/dist/schema/config-relations.d.ts +32 -0
  52. package/dist/schema/doctor-cli.js +4 -4
  53. package/dist/schema/doctor.d.ts +9 -0
  54. package/dist/schema/dynamic-tables.d.ts +27 -6
  55. package/dist/schema/ensure-collection-tables.d.ts +31 -268
  56. package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
  57. package/dist/schema/generate-drizzle-schema.js +1 -1
  58. package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
  59. package/dist/schema/generate-postgres-ddl.js +8 -2
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  61. package/dist/schema/generated-column-conflicts.d.ts +84 -0
  62. package/dist/schema/generated-schema-diff.d.ts +56 -0
  63. package/dist/schema/generated-schema-staleness.d.ts +33 -0
  64. package/dist/schema/introspect-db-logic.d.ts +2 -0
  65. package/dist/schema/plan/diff-plan.d.ts +220 -0
  66. package/dist/schema/plan/plan-schema.d.ts +84 -0
  67. package/dist/schema/plan/render-ddl.d.ts +62 -0
  68. package/dist/schema/plan/render-drizzle.d.ts +31 -0
  69. package/dist/schema/plan/types.d.ts +410 -0
  70. package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
  71. package/dist/schema/relation-names.d.ts +28 -0
  72. package/dist/schema/search-column.d.ts +9 -0
  73. package/dist/security/rls-enforcement.d.ts +17 -0
  74. package/dist/services/FetchService.d.ts +176 -36
  75. package/dist/services/PersistService.d.ts +17 -4
  76. package/dist/services/RelationService.d.ts +11 -3
  77. package/dist/services/collection-helpers.d.ts +16 -2
  78. package/dist/services/dataService.d.ts +29 -2
  79. package/dist/services/field-op-sql.d.ts +16 -0
  80. package/dist/services/field-viewer.d.ts +19 -0
  81. package/dist/services/realtimeService.d.ts +35 -1
  82. package/dist/services/row-pipeline.d.ts +29 -5
  83. package/dist/services/soft-delete.d.ts +73 -0
  84. package/dist/utils/pg-error-utils.d.ts +41 -1
  85. package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
  86. package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
  87. package/package.json +18 -10
  88. package/dist/cli-errors-DxN4yzdH.js.map +0 -1
  89. package/dist/doctor-D7HWmGzs.js.map +0 -1
  90. package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
  91. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
  92. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
  93. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
  94. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
  95. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
  96. package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
@@ -1,42 +1,25 @@
1
- import { CollectionConfig, Property, SecurityRule } from "@rebasepro/types";
2
- export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
3
- export declare const getPrimaryKeyProp: (collection: CollectionConfig) => {
4
- name: string;
5
- type: "string" | "number";
6
- isUuid: boolean;
7
- };
8
- export declare const isNumericId: (collection: CollectionConfig) => boolean;
9
- export declare const getPrimaryKeyName: (collection: CollectionConfig) => string;
10
- export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
11
- type ResolveCollection = (slug: string) => CollectionConfig | undefined;
12
1
  /**
13
- * The `ON DELETE` a foreign key gets when the author did not say.
14
- *
15
- * One rule for every property that emits one `belongsTo` relations and
16
- * `reference` properties alike. `reference` kept its own `CASCADE` default for
17
- * a release after this rule was written, which is exactly the split this
18
- * function exists to prevent: two spellings of the same link, two data-retention
19
- * behaviours, and only one of them documented.
20
- *
21
- * An optional link is `SET NULL`: the column can hold NULL, so dropping the
22
- * parent leaves the child row with an empty pointer, which is what "optional"
23
- * already means.
24
- *
25
- * A required link is **`RESTRICT`**, not `CASCADE`. `NOT NULL` says the child
26
- * cannot exist without a parent; it does not say deleting the parent should
27
- * take the child with it. That second claim is a data-retention decision, and
28
- * defaulting to it meant `onDelete` — a field nobody has to write — silently
29
- * turned every `DELETE FROM authors` into a cascade through posts, comments and
30
- * anything else that hung off them. `RESTRICT` fails the delete instead and
31
- * names the constraint, so the author chooses `cascade` on purpose or clears
32
- * the children first.
33
- *
34
- * Both DDL paths — the `CREATE TABLE` generator and {@link planRelationalColumns},
35
- * which boot-ensure compares the live database against — call this, so a
36
- * default that appears in one and not the other cannot make `db push` plan a
37
- * constraint rewrite on every run.
2
+ * The SQL half of `rebase db generate`, as a plan and a renderer.
3
+ *
4
+ * There is no interpretation left in this file. `schema/plan/plan-schema.ts`
5
+ * reads the collections into a {@link SchemaPlan} and `schema/plan/render-ddl.ts`
6
+ * writes the files; what remains here is the public entry points those two are
7
+ * reached through, plus the smaller plans (`planRelationalColumns`,
8
+ * `planJunctionTables`, `planCollectionPolicies`) that other modules ask for as
9
+ * slices of the same plan.
10
+ *
11
+ * It used to hold `getSqlColumnType` and a `CREATE TABLE` walk with its own
12
+ * reading of relations, junctions, enums and search — one of three such
13
+ * readings. See `schema/plan/types.ts` for what that cost.
38
14
  */
39
- export declare const defaultBelongsToOnDelete: (required: boolean | undefined) => "RESTRICT" | "SET NULL";
15
+ import { CollectionConfig, SecurityRule } from "@rebasepro/types";
16
+ import { quoteSqlLiteral, defaultBelongsToOnDelete } from "./plan/plan-schema.js";
17
+ import { type DdlRenderOptions } from "./plan/render-ddl.js";
18
+ import type { ForeignKeyPlan } from "./plan/types.js";
19
+ import type { GeneratedColumnDependency } from "./generated-column-conflicts.js";
20
+ export { quoteSqlLiteral, defaultBelongsToOnDelete };
21
+ export type { ForeignKeyPlan };
22
+ type ResolveCollection = (slug: string) => CollectionConfig | undefined;
40
23
  /**
41
24
  * The individual SQL statements a single security rule compiles to: a
42
25
  * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
@@ -48,20 +31,22 @@ export declare const defaultBelongsToOnDelete: (required: boolean | undefined) =
48
31
  */
49
32
  export declare const generatePolicyStatements: (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection) => string[];
50
33
  /**
51
- * Single-quote escaping for a SQL string literal (PostgreSQL doubles the
52
- * quote). Enum labels come straight from user-authored collection config, so a
53
- * label like `it's` closes the literal early and the whole generated file stops
54
- * parsing at the `CREATE TYPE`. Lives here rather than next to its other caller
55
- * because ensure-collection-tables already imports from this module the
56
- * reverse would be a cycle.
34
+ * `drizzle/schema.sql` the desired state Atlas diffs the database against.
35
+ *
36
+ * Synchronous. It was `async` with nothing to await, which made every caller
37
+ * (the writer script, the doctor, the live editor, two gates) carry a promise
38
+ * for no reason and made the one place that forgot to await it silently compare
39
+ * `[object Promise]`.
57
40
  */
58
- export declare const quoteSqlLiteral: (value: string) => string;
59
- export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
41
+ export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: DdlRenderOptions) => string;
42
+ /** `drizzle/policies.sql` the RLS policies. Atlas has no model for a policy. */
43
+ export declare const generatePostgresPoliciesDdl: (allCollections: CollectionConfig[]) => string;
60
44
  /**
61
- * Everything a `search` block needs, as a file Rebase applies itself.
45
+ * `drizzle/search.sql` — everything a `search` block needs, as a file Rebase
46
+ * applies itself.
62
47
  *
63
- * Search is the one part of the schema Atlas does not own. Two independent
64
- * reasons, and either alone would be enough:
48
+ * Search is one of the parts of the schema Atlas does not own, for two
49
+ * independent reasons and either alone would be enough:
65
50
  *
66
51
  * 1. Its free tier refuses to *parse* a desired-state file that so much as
67
52
  * contains a function — "functions and procedures are available to
@@ -73,24 +58,7 @@ export declare const getSqlColumnType: (propName: string, prop: Property, collec
73
58
  * against, so a helper seeded there beforehand is gone by the time the plan
74
59
  * is analysed. There is no hook to reinstate it.
75
60
  *
76
- * So the column, its index and its helpers are excluded from Atlas's view
77
- * (`searchExcludePatterns`) and applied from here — the same arrangement the
78
- * RLS policies already use, and for the same underlying reason.
79
- *
80
- * Ordered as it must run: extensions, then helpers, then the column whose
81
- * expression calls them, then the index over that column. Every statement is
82
- * `IF NOT EXISTS` / `OR REPLACE`, because this is replayed on every push and
83
- * appended to migrations that run against databases at any stage of their
84
- * life. Empty when nothing opted in — the caller writes no file then.
85
- *
86
- * The dev database Atlas plans against needs none of this. `searchExcludePatterns`
87
- * removes the search column from the *inspected* state, so the state Atlas
88
- * replays there never mentions the column and never calls a helper. Seeding the
89
- * helpers into that database was tried and removed: measured on Atlas 1.2.3, a
90
- * `schema apply --dry-run` produces byte-identical output seeded or not, and
91
- * Atlas wipes the dev database before it plans anyway — so a seed could not have
92
- * helped even where the excludes are missing, which is the case that really does
93
- * fail with `function public.rebase_search_unaccent(text) does not exist`.
61
+ * Empty when nothing opted in the caller writes no file then.
94
62
  */
95
63
  export declare const generatePostgresSearchDdl: (allCollections: CollectionConfig[]) => string;
96
64
  /**
@@ -99,103 +67,57 @@ export declare const generatePostgresSearchDdl: (allCollections: CollectionConfi
99
67
  * Without these, a desired state that omits search reads to Atlas as an
100
68
  * instruction to drop the column — taking the index and the whole search
101
69
  * feature with it on the next push.
102
- *
103
- * Fully qualified, `schema.table.object`, matching the include list. The
104
- * two-part form is what Atlas wants when the connection URL scopes it to one
105
- * schema and is *silently ignored* otherwise: it reads `posts.search_vector`
106
- * as a table named `search_vector` in a schema named `posts`, matches nothing,
107
- * and reports no error for the pattern that never fired.
108
70
  */
109
71
  export declare const searchExcludePatterns: (allCollections: CollectionConfig[]) => string[];
110
72
  /**
111
- * Everything a `{ type: "vector" }` property needs, as a file Rebase applies
112
- * itself.
113
- *
114
- * Vector is the second part of the schema Atlas does not own, and it is out for
115
- * a narrower reason than search: not that Atlas refuses to parse the file, but
116
- * that it cannot *execute* it. To compute a desired state Atlas materialises
117
- * `schema.sql` in a dev database, that database is created empty, and Atlas
118
- * empties it again at the start of every run — so `VECTOR(384)` resolves
119
- * against a database that structurally cannot have pgvector, and every push
120
- * dies with `type "vector" does not exist`. Seeding the extension beforehand
121
- * does not survive the clean (measured: present before the run, gone after),
122
- * putting `CREATE EXTENSION` in `schema.sql` is refused as an Atlas Pro
123
- * feature, and `--exclude` alone cannot help because Atlas still parses and
124
- * applies the whole file.
125
- *
126
- * So the column, its ANN indexes and the extension are excluded from Atlas's
127
- * view ({@link vectorExcludePatterns}) and applied from here — the same
128
- * arrangement search and the RLS policies already use.
129
- *
130
- * `CREATE EXTENSION` appears only when the project's database named `vector` in
131
- * its `extensions` — see `DatabaseOptions.extensions`. Everything else is
132
- * emitted either way: a database that already has pgvector installed by hand
133
- * needs no permission from anyone, and one that does not gets the column
134
- * refused by Postgres with a message this repo makes name the opt-in.
135
- *
136
- * Ordered as it must run: the extension, then the drift guard, then the column
137
- * whose type needs the extension, then the indexes over that column. Every
138
- * statement is `IF NOT EXISTS`, because this is replayed on every push and
139
- * appended to migrations that run against databases at any stage of their life.
140
- * Empty when no collection declares a vector — the caller writes no file then.
73
+ * `drizzle/vector.sql` the `vector` columns, their ANN indexes and, when a
74
+ * database declared it, the extension.
75
+ *
76
+ * Vector is out of Atlas's hands for a narrower reason than search: not that
77
+ * Atlas refuses to parse the file, but that it cannot *execute* it. To compute
78
+ * a desired state Atlas materialises `schema.sql` in a dev database, that
79
+ * database is created empty, and Atlas empties it again at the start of every
80
+ * run — so `VECTOR(384)` resolves against a database that structurally cannot
81
+ * have pgvector, and every push dies with `type "vector" does not exist`.
141
82
  */
142
83
  export declare const generatePostgresVectorDdl: (allCollections: CollectionConfig[], options?: {
143
84
  extensions?: readonly string[];
144
85
  }) => string;
145
- /**
146
- * Glob patterns telling Atlas to leave the vector column and its ANN indexes
147
- * alone.
148
- *
149
- * Without these, a desired state that omits the column reads to Atlas as an
150
- * instruction to drop it, taking the embeddings and the indexes with it on the
151
- * next push.
152
- *
153
- * Fully qualified, `schema.table.object`, for the reason spelled out on
154
- * {@link searchExcludePatterns}: the two-part form matches nothing and reports
155
- * no error, which turns a protection into a `DROP COLUMN`.
156
- */
86
+ /** @see searchExcludePatterns */
157
87
  export declare const vectorExcludePatterns: (allCollections: CollectionConfig[]) => string[];
158
- export declare const generatePostgresDdl: (allCollections: CollectionConfig[], options?: {
159
- includePolicies?: boolean;
160
- includeSearch?: boolean;
161
- includeVector?: boolean;
162
- }) => Promise<string>;
163
- /** The RLS statements one declared collection's table needs, ready to run. */
164
88
  /**
165
- * A foreign key, as both its parts and the statement that creates it.
89
+ * `drizzle/triggers.sql` the `BEFORE UPDATE` triggers behind
90
+ * `autoValue: "on_update"`, and the one function they share.
166
91
  *
167
- * `ALTER TABLE ADD CONSTRAINT` has no `IF NOT EXISTS`, so a caller applying
168
- * these has to skip by name hence the name is a field and not only a substring
169
- * of the SQL.
92
+ * A trigger is a function plus a binding, so it is carved out of Atlas's view
93
+ * for the first of the two reasons search is. Empty when no property declares
94
+ * `on_update` the caller writes no file then, and removes a stale one.
170
95
  */
171
- export interface ForeignKeyPlan {
172
- constraintName: string;
173
- schema: string;
174
- /** Bare table name, no schema prefix. */
175
- table: string;
176
- column: string;
177
- targetSchema: string;
178
- targetTable: string;
179
- targetColumn: string;
180
- sql: string;
181
- }
96
+ export declare const generatePostgresTriggersDdl: (allCollections: CollectionConfig[]) => string;
97
+ /** @see searchExcludePatterns */
98
+ export declare const triggerExcludePatterns: (allCollections: CollectionConfig[]) => string[];
182
99
  /** A column a `relation` or `reference` property owns on its own table. */
183
100
  export interface RelationalColumnPlan {
184
101
  schema: string;
185
102
  /** Bare table name, no schema prefix. */
186
103
  table: string;
187
104
  column: string;
188
- /** Postgres type, exactly as the DDL generator declares it. */
105
+ /** Postgres type, exactly as the DDL renderer declares it. */
189
106
  type: string;
107
+ /** `validation.required` on the property that owns the link. */
108
+ required: boolean;
109
+ /**
110
+ * True when a declared property emits this column and the relation
111
+ * contributes only the constraint — `postId` with `columnName: "post_id"`
112
+ * beside a `belongsTo` on `post_id`.
113
+ */
114
+ columnOwnedByProperty?: boolean;
190
115
  /** Absent when the target collection is not part of this bundle. */
191
116
  foreignKey?: ForeignKeyPlan;
192
117
  /**
193
118
  * What this column would have been called before `generateForeignKeyName`
194
- * learned to singularize set only when the two differ and the column name
195
- * is the derived default rather than one the author wrote.
196
- *
197
- * Carried so the boot-time ensure can notice a database provisioned under
198
- * the old rule. It is never used to name anything.
119
+ * learned to singularize. Carried so the boot-time ensure can notice a
120
+ * database provisioned under the old rule; never used to name anything.
199
121
  */
200
122
  legacyColumn?: string;
201
123
  }
@@ -217,25 +139,17 @@ export interface JunctionTablePlan {
217
139
  * The FK columns the declared collections own — one entry per `relation`
218
140
  * (`belongsTo` side) or `reference` property.
219
141
  *
220
- * Split out of {@link generatePostgresDdl} so the boot-time schema ensure can
221
- * create the same columns with the same names, types and constraints. Before
222
- * this it skipped them outright, which was survivable only because `db push`
223
- * always followed; on a managed tenant nothing follows, so a table arrived
224
- * without the column its own collection reads and wrote 400 on every insert.
225
- *
226
- * A relation whose target is not in the bundle yields no column at all (the
227
- * generator returns early on an unresolvable target); a `reference` whose target
228
- * is unknown yields the column without a constraint. Both mirror the generator
229
- * exactly — a divergence here is a schema fork between boot and `db push`.
142
+ * A view over the schema plan. It was a second walk of the collections beside
143
+ * the `CREATE TABLE` one, which is how boot-ensure came to add every relation
144
+ * column bare while `db push` made the same column `NOT NULL`.
230
145
  */
231
146
  export declare const planRelationalColumns: (allCollections: CollectionConfig[]) => RelationalColumnPlan[];
232
147
  /**
233
148
  * The junction tables a bundle's many-to-many relations imply.
234
149
  *
235
- * Derived from {@link resolveJunctionSpecs}, the same source the junction RLS
236
- * comes from, so a table created here always has policies planned for it — a
237
- * junction with row-level security left off is readable and writable by every
238
- * signed-in user, which is why the two must ship together.
150
+ * Derived from the same plan the derived RLS comes from, so a table created
151
+ * here always has policies planned for it — a junction with row-level security
152
+ * left off is readable and writable by every signed-in user.
239
153
  */
240
154
  export declare const planJunctionTables: (allCollections: CollectionConfig[]) => JunctionTablePlan[];
241
155
  export interface CollectionPolicyPlan {
@@ -251,20 +165,22 @@ export interface CollectionPolicyPlan {
251
165
  policyStatements: string[];
252
166
  }
253
167
  /**
254
- * The per-table RLS plan for the *declared* collections, as executable
255
- * statements — what the managed runtime applies at boot so a freshly
168
+ * The per-table RLS plan for the declared collections and their junctions, as
169
+ * executable statements — what the managed runtime applies at boot so a freshly
256
170
  * provisioned tenant database serves data instead of 401ing every read.
257
171
  *
258
- * Mirrors {@link generatePostgresPoliciesDdl} exactly (same
259
- * `generatePolicyStatements`, same enable-RLS, same effective rules, same
260
- * derived junction rules), so boot and `db push` produce identical policies from
261
- * identical collections.
262
- *
263
- * Junction tables are included, and have to be: boot creates them now
264
- * ({@link planJunctionTables}), and a junction with RLS left off is readable and
265
- * writable by every signed-in user. A junction whose table is still absent is
266
- * skipped by the applier, not planned away here.
172
+ * The same policies `policies.sql` carries, from the same plan, so boot and
173
+ * `db push` produce identical RLS from identical collections.
267
174
  */
268
175
  export declare const planCollectionPolicies: (allCollections: CollectionConfig[]) => CollectionPolicyPlan[];
269
- export declare const generatePostgresPoliciesDdl: (allCollections: CollectionConfig[]) => string;
270
- export {};
176
+ /**
177
+ * The `generated column → column it reads` edges a project's `search` blocks
178
+ * imply, derived from the collections alone.
179
+ *
180
+ * The same edges `queryGeneratedColumnDependencies` reads out of `pg_depend`,
181
+ * but available where no database is: `rebase db generate` writes a migration
182
+ * that will run somewhere else, later, against a database it cannot inspect.
183
+ * A spec's `fields[].column` is the physical column each search field starts
184
+ * at, which is exactly what the generated expression depends on.
185
+ */
186
+ export declare const searchColumnDependencies: (allCollections: CollectionConfig[]) => GeneratedColumnDependency[];
@@ -1,6 +1,6 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
- import { a as generatePostgresVectorDdl, i as generatePostgresSearchDdl, n as generatePostgresDdl, r as generatePostgresPoliciesDdl } from "../generate-postgres-ddl-logic-CtnpIBVv.js";
3
+ import { a as generatePostgresVectorDdl, i as generatePostgresTriggersDdl, n as generatePostgresPoliciesDdl, r as generatePostgresSearchDdl, t as generatePostgresDdl } from "../generate-postgres-ddl-logic-CR2xcS7e.js";
4
4
  import { n as outError, t as out } from "../cli-output-CNdMql-L.js";
5
5
  import { loadCollectionsFromDirectory } from "@rebasepro/server";
6
6
  import { declaredDatabaseExtensions } from "@rebasepro/types";
@@ -49,7 +49,7 @@ var runGeneration = async (collectionsFilePath, outputPath) => {
49
49
  let collections = await loadCollectionsFromDirectory(resolvedPath);
50
50
  if (!collections || !Array.isArray(collections)) collections = [];
51
51
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
52
- const ddlContent = await generatePostgresDdl(collections, {
52
+ const ddlContent = generatePostgresDdl(collections, {
53
53
  includePolicies: false,
54
54
  includeSearch: false,
55
55
  includeVector: false
@@ -57,6 +57,7 @@ var runGeneration = async (collectionsFilePath, outputPath) => {
57
57
  const policiesContent = generatePostgresPoliciesDdl(collections);
58
58
  const searchContent = generatePostgresSearchDdl(collections);
59
59
  const vectorContent = generatePostgresVectorDdl(collections, { extensions: declaredDatabaseExtensions() });
60
+ const triggersContent = generatePostgresTriggersDdl(collections);
60
61
  if (outputPath) {
61
62
  const outputDir = path.dirname(outputPath);
62
63
  await promises.mkdir(outputDir, { recursive: true });
@@ -75,6 +76,11 @@ var runGeneration = async (collectionsFilePath, outputPath) => {
75
76
  await promises.writeFile(vectorPath, vectorContent);
76
77
  out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);
77
78
  } else if (fs$1.existsSync(vectorPath)) await promises.rm(vectorPath);
79
+ const triggersPath = path.join(outputDir, "triggers.sql");
80
+ if (triggersContent) {
81
+ await promises.writeFile(triggersPath, triggersContent);
82
+ out(`✅ PostgreSQL trigger DDL generated successfully at ${triggersPath}`);
83
+ } else if (fs$1.existsSync(triggersPath)) await promises.rm(triggersPath);
78
84
  } else {
79
85
  out("✅ PostgreSQL DDL generated successfully.");
80
86
  out(String(ddlContent));
@@ -1 +1 @@
1
- {"version":3,"file":"generate-postgres-ddl.js","names":[],"sources":["../../src/schema/generate-postgres-ddl.ts","../../src/schema/bin/generate-postgres-ddl.ts"],"sourcesContent":["import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport {\n generatePostgresDdl,\n generatePostgresPoliciesDdl,\n generatePostgresSearchDdl,\n generatePostgresVectorDdl\n} from \"./generate-postgres-ddl-logic\";\nimport { CollectionConfig, declaredDatabaseExtensions } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\n\n/** Where a project declares its resources, relative to the config directory. */\nconst RESOURCE_ENTRIES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/**\n * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can\n * answer.\n *\n * This generator runs as its own tsx subprocess and loads only the collections\n * directory, so nothing else in it would ever import the resources module — and\n * a permission that reads as absent because nobody loaded it is the worst shape\n * this could take: the push succeeds, `vector.sql` quietly omits the\n * `CREATE EXTENSION`, and the failure lands one statement later looking like a\n * database problem.\n *\n * Imported directly rather than through the config index, for the reason\n * `boot/resource-loading.ts` gives: re-exporting from the index would work only\n * for projects that remember to.\n *\n * A project with no resources module is the normal case and declares nothing.\n * An import that *throws* is not swallowed — a resources module that does not\n * evaluate is a configuration error, and treating it as \"declared nothing\"\n * would turn it into a silently missing extension.\n */\nasync function loadDeclaredResources(configDir: string): Promise<void> {\n const entry = RESOURCE_ENTRIES\n .map(name => path.join(configDir, name))\n .find(candidate => fs.existsSync(candidate));\n if (!entry) return;\n await import(pathToFileURL(entry).href);\n}\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n throw new Error(\"No collections file path provided.\");\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // `<config>/collections` is the path this is given, so the config\n // directory is its parent — the same relationship `rebase resources`\n // and the boot loader assume.\n await loadDeclaredResources(path.dirname(resolvedPath));\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n // Sort collections by slug alphabetically to ensure deterministic DDL generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n // `schema.sql` is Atlas's desired state, and it carries neither the RLS\n // policies, nor the search apparatus, nor the vector columns: Atlas\n // manages none of them, and in the search and vector cases cannot. All\n // three are written beside it and applied by the CLI in their own right.\n const ddlContent = await generatePostgresDdl(collections, {\n includePolicies: false,\n includeSearch: false,\n includeVector: false\n });\n const policiesContent = generatePostgresPoliciesDdl(collections);\n const searchContent = generatePostgresSearchDdl(collections);\n const vectorContent = generatePostgresVectorDdl(collections, {\n extensions: declaredDatabaseExtensions()\n });\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, ddlContent);\n out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);\n\n const policiesPath = path.join(outputDir, \"policies.sql\");\n await fsPromises.writeFile(policiesPath, policiesContent);\n out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);\n\n // Removed when the last `search` block goes: the CLI applies this\n // file whenever it exists, and a stale one would keep re-creating\n // functions for collections that no longer want them.\n const searchPath = path.join(outputDir, \"search.sql\");\n if (searchContent) {\n await fsPromises.writeFile(searchPath, searchContent);\n out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);\n } else if (fs.existsSync(searchPath)) {\n await fsPromises.rm(searchPath);\n }\n\n // Removed when the last `vector` property goes, for the same reason\n // `search.sql` is: the CLI applies whichever of these files exists,\n // and a stale one would keep re-adding a column the collections no\n // longer declare — which Atlas, no longer told to exclude it, would\n // then plan a DROP for on the very next push.\n const vectorPath = path.join(outputDir, \"vector.sql\");\n if (vectorContent) {\n await fsPromises.writeFile(vectorPath, vectorContent);\n out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);\n } else if (fs.existsSync(vectorPath)) {\n await fsPromises.rm(vectorPath);\n }\n } else {\n out(\"✅ PostgreSQL DDL generated successfully.\");\n out(String(ddlContent));\n out(\"\\n✅ PostgreSQL Policies DDL generated successfully.\");\n out(String(policiesContent));\n }\n\n } catch (error) {\n outError(`Error generating DDL schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n // Step 1 of `rebase db push` is this process, and its exit code is the\n // only thing the push reads. Printing and exiting 0 made a resources\n // module that does not evaluate — the single most common cause here —\n // report `✓ rebase db push completed successfully` while Atlas applied\n // whatever `drizzle/schema.sql` happened to be on disk, sometimes\n // minutes old. With no prior file the same run died inside Atlas on\n // `stat drizzle/schema.sql: no such file or directory`, which hides the\n // real cause two screens up.\n process.exitCode = 1;\n throw error;\n }\n};\n\nexport const main = async () => {\n const collectionsFilePathArg = process.argv.find(arg => arg.startsWith(\"--collections=\"));\n const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split(\"=\")[1] : process.argv[2];\n\n const outputPathArg = process.argv.find(arg => arg.startsWith(\"--output=\"));\n const outputPath = outputPathArg ? outputPathArg.split(\"=\")[1] : undefined;\n\n const watch = process.argv.includes(\"--watch\");\n\n if (!collectionsFilePath) {\n out(\"Usage: ts-node generate-postgres-ddl.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]\");\n return;\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);\n const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;\n\n if (watch) {\n out(`Watching for changes in ${resolvedPath}...`);\n // Imported here rather than at module scope, and this is not a style\n // choice: chokidar is needed only by `--watch`, which is a\n // schema-authoring path that never runs inside the runtime image. A\n // top-level import puts it on the boot path of the published driver\n // bundle, and the image installs a hand-listed set of runtime\n // dependencies that does not include it — so the whole driver failed to\n // load with \"Cannot find package 'chokidar'\", and every self-hosted\n // container answered 500 with a stack trace about a file watcher.\n //\n // Same reasoning the image already applies to @ariga/atlas: an\n // authoring-only dependency does not belong on a boot path.\n const { default: chokidar } = await import(\"chokidar\");\n const watcher = chokidar.watch(resolvedPath, {\n persistent: true,\n ignoreInitial: false\n });\n\n watcher.on(\"all\", (event, filePath) => {\n out(`[${event}] ${filePath}. Regenerating DDL schema...`);\n // A watch session outlives a bad edit: the cause is already on\n // stderr, and the next save is the retry. Only the one-shot run\n // below turns a failure into an exit code.\n void runGeneration(resolvedPath, resolvedOutputPath).catch(() => undefined);\n });\n } else {\n await runGeneration(resolvedPath, resolvedOutputPath);\n }\n};\n\n// This check ensures the script only runs when executed directly\nif (import.meta.url.endsWith(process.argv[1])) {\n // `runGeneration` has already printed the cause and set the exit code; the\n // handler is here so the last thing on the terminal is that message rather\n // than Node's unhandled-rejection dump of the same stack.\n main().catch(() => {\n process.exitCode = 1;\n });\n}\n","/**\n * The built entry point for `generate-postgres-ddl`.\n *\n * `generate-postgres-ddl.ts` guards its own `main()` with\n * `import.meta.url.endsWith(process.argv[1])`, because `src/index.ts` re-exports\n * the module and importing the driver must not run the generator. That guard is\n * correct when the file IS the process entry — which is how it runs from source\n * under tsx — and silently false once the module is bundled: the executed file\n * is a stub, the code lives in a shared chunk, and the chunk's `import.meta.url`\n * is not the stub's path. The generator then did nothing, exited 0, and the\n * caller reported success over an unwritten schema.\n *\n * So the built path gets an entry that says what it means.\n */\nimport { main } from \"../generate-postgres-ddl\";\n\nmain().catch(() => {\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;;;;;;;AAeA,IAAM,mBAAmB;CAAC;CAAgB;CAAgB;CAAsB;AAAoB;;;;;;;;;;;;;;;;;;;;;AAsBpG,eAAe,sBAAsB,WAAkC;CACnE,MAAM,QAAQ,iBACT,KAAI,SAAQ,KAAK,KAAK,WAAW,IAAI,CAAC,CAAC,CACvC,MAAK,cAAa,KAAG,WAAW,SAAS,CAAC;CAC/C,IAAI,CAAC,OAAO;CACZ,MAAM,OAAO,cAAc,KAAK,CAAC,CAAC;AACtC;AAEA,IAAM,gBAAgB,OAAO,qBAA8B,eAAwB;CAC/E,IAAI;EACA,IAAI,CAAC,qBACD,MAAM,IAAI,MAAM,oCAAoC;EAGxD,MAAM,eAAe,KAAK,QAAQ,mBAAmB;EAKrD,MAAM,sBAAsB,KAAK,QAAQ,YAAY,CAAC;EAItD,IAAI,cAAkC,MAAM,6BAA6B,YAAY;EAGrF,IAAI,CAAC,eAAe,CAAC,MAAM,QAAQ,WAAW,GAC1C,cAAc,CAAC;EAInB,YAAY,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;EAMvD,MAAM,aAAa,MAAM,oBAAoB,aAAa;GACtD,iBAAiB;GACjB,eAAe;GACf,eAAe;EACnB,CAAC;EACD,MAAM,kBAAkB,4BAA4B,WAAW;EAC/D,MAAM,gBAAgB,0BAA0B,WAAW;EAC3D,MAAM,gBAAgB,0BAA0B,aAAa,EACzD,YAAY,2BAA2B,EAC3C,CAAC;EAED,IAAI,YAAY;GACZ,MAAM,YAAY,KAAK,QAAQ,UAAU;GACzC,MAAM,SAAW,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;GACrD,MAAM,SAAW,UAAU,YAAY,UAAU;GACjD,IAAI,8CAA8C,YAAY;GAE9D,MAAM,eAAe,KAAK,KAAK,WAAW,cAAc;GACxD,MAAM,SAAW,UAAU,cAAc,eAAe;GACxD,IAAI,uDAAuD,cAAc;GAKzE,MAAM,aAAa,KAAK,KAAK,WAAW,YAAY;GACpD,IAAI,eAAe;IACf,MAAM,SAAW,UAAU,YAAY,aAAa;IACpD,IAAI,qDAAqD,YAAY;GACzE,OAAO,IAAI,KAAG,WAAW,UAAU,GAC/B,MAAM,SAAW,GAAG,UAAU;GAQlC,MAAM,aAAa,KAAK,KAAK,WAAW,YAAY;GACpD,IAAI,eAAe;IACf,MAAM,SAAW,UAAU,YAAY,aAAa;IACpD,IAAI,qDAAqD,YAAY;GACzE,OAAO,IAAI,KAAG,WAAW,UAAU,GAC/B,MAAM,SAAW,GAAG,UAAU;EAEtC,OAAO;GACH,IAAI,0CAA0C;GAC9C,IAAI,OAAO,UAAU,CAAC;GACtB,IAAI,qDAAqD;GACzD,IAAI,OAAO,eAAe,CAAC;EAC/B;CAEJ,SAAS,OAAO;EACZ,SAAS,gCAAgC,iBAAiB,QAAS,MAAM,SAAS,MAAM,UAAW,OAAO,KAAK,GAAG;EASlH,QAAQ,WAAW;EACnB,MAAM;CACV;AACJ;AAEA,IAAa,OAAO,YAAY;CAC5B,MAAM,yBAAyB,QAAQ,KAAK,MAAK,QAAO,IAAI,WAAW,gBAAgB,CAAC;CACxF,MAAM,sBAAsB,yBAAyB,uBAAuB,MAAM,GAAG,CAAC,CAAC,KAAK,QAAQ,KAAK;CAEzG,MAAM,gBAAgB,QAAQ,KAAK,MAAK,QAAO,IAAI,WAAW,WAAW,CAAC;CAC1E,MAAM,aAAa,gBAAgB,cAAc,MAAM,GAAG,CAAC,CAAC,KAAK,KAAA;CAEjE,MAAM,QAAQ,QAAQ,KAAK,SAAS,SAAS;CAE7C,IAAI,CAAC,qBAAqB;EACtB,IAAI,+GAA+G;EACnH;CACJ;CAEA,MAAM,eAAe,KAAK,QAAQ,QAAQ,IAAI,GAAG,mBAAmB;CACpE,MAAM,qBAAqB,aAAa,KAAK,QAAQ,QAAQ,IAAI,GAAG,UAAU,IAAI,KAAA;CAElF,IAAI,OAAO;EACP,IAAI,2BAA2B,aAAa,IAAI;EAYhD,MAAM,EAAE,SAAS,aAAa,MAAM,OAAO;EAM3C,SALyB,MAAM,cAAc;GACzC,YAAY;GACZ,eAAe;EACnB,CAEA,CAAA,CAAQ,GAAG,QAAQ,OAAO,aAAa;GACnC,IAAI,IAAI,MAAM,IAAI,SAAS,6BAA6B;GAIxD,cAAmB,cAAc,kBAAkB,CAAC,CAAC,YAAY,KAAA,CAAS;EAC9E,CAAC;CACL,OACI,MAAM,cAAc,cAAc,kBAAkB;AAE5D;AAGA,IAAI,OAAO,KAAK,IAAI,SAAS,QAAQ,KAAK,EAAE,GAIxC,KAAK,CAAC,CAAC,YAAY;CACf,QAAQ,WAAW;AACvB,CAAC;;;;;;;;;;;;;;;;;ACnLL,KAAK,CAAC,CAAC,YAAY;CACf,QAAQ,WAAW;AACvB,CAAC"}
1
+ {"version":3,"file":"generate-postgres-ddl.js","names":[],"sources":["../../src/schema/generate-postgres-ddl.ts","../../src/schema/bin/generate-postgres-ddl.ts"],"sourcesContent":["import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport {\n generatePostgresDdl,\n generatePostgresPoliciesDdl,\n generatePostgresSearchDdl,\n generatePostgresTriggersDdl,\n generatePostgresVectorDdl\n} from \"./generate-postgres-ddl-logic\";\nimport { CollectionConfig, declaredDatabaseExtensions } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\n\n/** Where a project declares its resources, relative to the config directory. */\nconst RESOURCE_ENTRIES = [\"resources.ts\", \"resources.js\", \"resources/index.ts\", \"resources/index.js\"];\n\n/**\n * Evaluate the project's `resources.ts`, so `declaredDatabaseExtensions()` can\n * answer.\n *\n * This generator runs as its own tsx subprocess and loads only the collections\n * directory, so nothing else in it would ever import the resources module — and\n * a permission that reads as absent because nobody loaded it is the worst shape\n * this could take: the push succeeds, `vector.sql` quietly omits the\n * `CREATE EXTENSION`, and the failure lands one statement later looking like a\n * database problem.\n *\n * Imported directly rather than through the config index, for the reason\n * `boot/resource-loading.ts` gives: re-exporting from the index would work only\n * for projects that remember to.\n *\n * A project with no resources module is the normal case and declares nothing.\n * An import that *throws* is not swallowed — a resources module that does not\n * evaluate is a configuration error, and treating it as \"declared nothing\"\n * would turn it into a silently missing extension.\n */\nasync function loadDeclaredResources(configDir: string): Promise<void> {\n const entry = RESOURCE_ENTRIES\n .map(name => path.join(configDir, name))\n .find(candidate => fs.existsSync(candidate));\n if (!entry) return;\n await import(pathToFileURL(entry).href);\n}\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n throw new Error(\"No collections file path provided.\");\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // `<config>/collections` is the path this is given, so the config\n // directory is its parent — the same relationship `rebase resources`\n // and the boot loader assume.\n await loadDeclaredResources(path.dirname(resolvedPath));\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n // Sort collections by slug alphabetically to ensure deterministic DDL generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n // `schema.sql` is Atlas's desired state, and it carries neither the RLS\n // policies, nor the search apparatus, nor the vector columns: Atlas\n // manages none of them, and in the search and vector cases cannot. All\n // three are written beside it and applied by the CLI in their own right.\n const ddlContent = generatePostgresDdl(collections, {\n includePolicies: false,\n includeSearch: false,\n includeVector: false\n });\n const policiesContent = generatePostgresPoliciesDdl(collections);\n const searchContent = generatePostgresSearchDdl(collections);\n const vectorContent = generatePostgresVectorDdl(collections, {\n extensions: declaredDatabaseExtensions()\n });\n const triggersContent = generatePostgresTriggersDdl(collections);\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, ddlContent);\n out(`✅ PostgreSQL DDL generated successfully at ${outputPath}`);\n\n const policiesPath = path.join(outputDir, \"policies.sql\");\n await fsPromises.writeFile(policiesPath, policiesContent);\n out(`✅ PostgreSQL Policies DDL generated successfully at ${policiesPath}`);\n\n // Removed when the last `search` block goes: the CLI applies this\n // file whenever it exists, and a stale one would keep re-creating\n // functions for collections that no longer want them.\n const searchPath = path.join(outputDir, \"search.sql\");\n if (searchContent) {\n await fsPromises.writeFile(searchPath, searchContent);\n out(`✅ PostgreSQL search DDL generated successfully at ${searchPath}`);\n } else if (fs.existsSync(searchPath)) {\n await fsPromises.rm(searchPath);\n }\n\n // Removed when the last `vector` property goes, for the same reason\n // `search.sql` is: the CLI applies whichever of these files exists,\n // and a stale one would keep re-adding a column the collections no\n // longer declare — which Atlas, no longer told to exclude it, would\n // then plan a DROP for on the very next push.\n const vectorPath = path.join(outputDir, \"vector.sql\");\n if (vectorContent) {\n await fsPromises.writeFile(vectorPath, vectorContent);\n out(`✅ PostgreSQL vector DDL generated successfully at ${vectorPath}`);\n } else if (fs.existsSync(vectorPath)) {\n await fsPromises.rm(vectorPath);\n }\n\n // `autoValue: \"on_update\"`, on the same terms as the two above: a\n // trigger is a function plus a binding, which Atlas's free tier\n // will not parse, and a stale file would keep re-installing a\n // trigger for a column nothing declares any more.\n const triggersPath = path.join(outputDir, \"triggers.sql\");\n if (triggersContent) {\n await fsPromises.writeFile(triggersPath, triggersContent);\n out(`✅ PostgreSQL trigger DDL generated successfully at ${triggersPath}`);\n } else if (fs.existsSync(triggersPath)) {\n await fsPromises.rm(triggersPath);\n }\n } else {\n out(\"✅ PostgreSQL DDL generated successfully.\");\n out(String(ddlContent));\n out(\"\\n✅ PostgreSQL Policies DDL generated successfully.\");\n out(String(policiesContent));\n }\n\n } catch (error) {\n outError(`Error generating DDL schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n // Step 1 of `rebase db push` is this process, and its exit code is the\n // only thing the push reads. Printing and exiting 0 made a resources\n // module that does not evaluate — the single most common cause here —\n // report `✓ rebase db push completed successfully` while Atlas applied\n // whatever `drizzle/schema.sql` happened to be on disk, sometimes\n // minutes old. With no prior file the same run died inside Atlas on\n // `stat drizzle/schema.sql: no such file or directory`, which hides the\n // real cause two screens up.\n process.exitCode = 1;\n throw error;\n }\n};\n\nexport const main = async () => {\n const collectionsFilePathArg = process.argv.find(arg => arg.startsWith(\"--collections=\"));\n const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split(\"=\")[1] : process.argv[2];\n\n const outputPathArg = process.argv.find(arg => arg.startsWith(\"--output=\"));\n const outputPath = outputPathArg ? outputPathArg.split(\"=\")[1] : undefined;\n\n const watch = process.argv.includes(\"--watch\");\n\n if (!collectionsFilePath) {\n out(\"Usage: ts-node generate-postgres-ddl.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]\");\n return;\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);\n const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;\n\n if (watch) {\n out(`Watching for changes in ${resolvedPath}...`);\n // Imported here rather than at module scope, and this is not a style\n // choice: chokidar is needed only by `--watch`, which is a\n // schema-authoring path that never runs inside the runtime image. A\n // top-level import puts it on the boot path of the published driver\n // bundle, and the image installs a hand-listed set of runtime\n // dependencies that does not include it — so the whole driver failed to\n // load with \"Cannot find package 'chokidar'\", and every self-hosted\n // container answered 500 with a stack trace about a file watcher.\n //\n // Same reasoning the image already applies to @ariga/atlas: an\n // authoring-only dependency does not belong on a boot path.\n const { default: chokidar } = await import(\"chokidar\");\n const watcher = chokidar.watch(resolvedPath, {\n persistent: true,\n ignoreInitial: false\n });\n\n watcher.on(\"all\", (event, filePath) => {\n out(`[${event}] ${filePath}. Regenerating DDL schema...`);\n // A watch session outlives a bad edit: the cause is already on\n // stderr, and the next save is the retry. Only the one-shot run\n // below turns a failure into an exit code.\n void runGeneration(resolvedPath, resolvedOutputPath).catch(() => undefined);\n });\n } else {\n await runGeneration(resolvedPath, resolvedOutputPath);\n }\n};\n\n// This check ensures the script only runs when executed directly\nif (import.meta.url.endsWith(process.argv[1])) {\n // `runGeneration` has already printed the cause and set the exit code; the\n // handler is here so the last thing on the terminal is that message rather\n // than Node's unhandled-rejection dump of the same stack.\n main().catch(() => {\n process.exitCode = 1;\n });\n}\n","/**\n * The built entry point for `generate-postgres-ddl`.\n *\n * `generate-postgres-ddl.ts` guards its own `main()` with\n * `import.meta.url.endsWith(process.argv[1])`, because `src/index.ts` re-exports\n * the module and importing the driver must not run the generator. That guard is\n * correct when the file IS the process entry — which is how it runs from source\n * under tsx — and silently false once the module is bundled: the executed file\n * is a stub, the code lives in a shared chunk, and the chunk's `import.meta.url`\n * is not the stub's path. The generator then did nothing, exited 0, and the\n * caller reported success over an unwritten schema.\n *\n * So the built path gets an entry that says what it means.\n */\nimport { main } from \"../generate-postgres-ddl\";\n\nmain().catch(() => {\n process.exitCode = 1;\n});\n"],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /** An operation on a column that PostgreSQL refuses while a dependant exists. */
2
+ export type ColumnMutationKind = "type" | "drop";
3
+ export interface ColumnMutation {
4
+ /** Absent when the plan did not qualify the table; matched loosely then. */
5
+ schema?: string;
6
+ table: string;
7
+ column: string;
8
+ kind: ColumnMutationKind;
9
+ }
10
+ /** One `generated column → column it reads` edge, as read from the catalogue. */
11
+ export interface GeneratedColumnDependency {
12
+ schema: string;
13
+ table: string;
14
+ /** The generated column. */
15
+ column: string;
16
+ /** A column its expression reads. */
17
+ dependsOn: string;
18
+ }
19
+ /** A generated column standing in the way of the plan, and why. */
20
+ export interface GeneratedColumnConflict {
21
+ schema: string;
22
+ table: string;
23
+ /** The generated column that has to go before the apply. */
24
+ column: string;
25
+ /** The mutated columns it reads, deduplicated and sorted. */
26
+ blocking: {
27
+ column: string;
28
+ kind: ColumnMutationKind;
29
+ }[];
30
+ }
31
+ /**
32
+ * Every column an Atlas plan retypes or drops, with the table it belongs to.
33
+ *
34
+ * One statement carries many clauses — Atlas emits `ALTER TABLE "public"."posts"
35
+ * ALTER COLUMN "title" TYPE text, ALTER COLUMN "slug" TYPE text, …` — so the
36
+ * table is read once per statement and the clauses are scanned within it.
37
+ */
38
+ export declare function parseColumnMutations(planSql: string): ColumnMutation[];
39
+ /**
40
+ * The generated columns that block a plan: those reading a column it retypes
41
+ * or drops.
42
+ *
43
+ * A mutation with no schema matches on table name alone. That is deliberately
44
+ * loose — a bare `ALTER TABLE posts` resolves through `search_path` at
45
+ * execution time, and guessing wrong in the *permissive* direction costs a
46
+ * generated column that is rebuilt seconds later, while guessing wrong in the
47
+ * strict direction costs the whole push.
48
+ */
49
+ export declare function findGeneratedColumnConflicts(mutations: ColumnMutation[], dependencies: GeneratedColumnDependency[]): GeneratedColumnConflict[];
50
+ /**
51
+ * Split the conflicts into the ones Rebase can rebuild and the ones it cannot.
52
+ *
53
+ * `managed` is decided by the very list that hid the column from Atlas: a
54
+ * `schema.table.column` exclude pattern means Rebase generated the column and
55
+ * `search.sql` will put it back. Anything else is somebody's hand-written
56
+ * generated column — dropping it would destroy a definition Rebase has no copy
57
+ * of, so it is reported and the push stops instead.
58
+ */
59
+ export declare function partitionByOwnership(conflicts: GeneratedColumnConflict[], excludePatterns: string[]): {
60
+ managed: GeneratedColumnConflict[];
61
+ foreign: GeneratedColumnConflict[];
62
+ };
63
+ /**
64
+ * `ALTER TABLE … DROP COLUMN …` for each conflict, in catalogue order.
65
+ *
66
+ * `ifExists` is for the migration path, where the statement is written now and
67
+ * runs later against a database nobody can inspect: on a fresh one the column
68
+ * does not exist yet (the migration is about to create the table), on a live
69
+ * one it does, and the same file has to survive both.
70
+ */
71
+ export declare function dropGeneratedColumnStatements(conflicts: GeneratedColumnConflict[], opts?: {
72
+ ifExists?: boolean;
73
+ }): string[];
74
+ /**
75
+ * The preamble a migration needs when it retypes a column a search block reads.
76
+ *
77
+ * Prepended, because the drop has to precede the `ALTER COLUMN … TYPE` that
78
+ * PostgreSQL would otherwise refuse. The column comes back at the end of the
79
+ * same migration: `db generate` appends `search.sql`, whose
80
+ * `ADD COLUMN IF NOT EXISTS` rebuilds the column, its index and its stamp.
81
+ */
82
+ export declare function migrationDropPreamble(conflicts: GeneratedColumnConflict[]): string;
83
+ /** `public.posts.search_vector`, the way every message here names one. */
84
+ export declare function describeConflict(conflict: GeneratedColumnConflict): string;
@@ -0,0 +1,56 @@
1
+ import { CollectionConfig } from "@rebasepro/types";
2
+ import type { TableMeta } from "./introspect-db-logic.js";
3
+ export type SchemaDifferenceKind = "missing-table" | "missing-column" | "extra-column" | "type" | "nullability";
4
+ export interface SchemaDifference {
5
+ kind: SchemaDifferenceKind;
6
+ /** Bare table name, as both sides spell it. */
7
+ table: string;
8
+ /** The collection backed by that table, when one is. */
9
+ collection?: string;
10
+ /** The column, in database terms. */
11
+ column?: string;
12
+ /** The field it is served under — the property key, where there is one. */
13
+ property?: string;
14
+ /** What `schema.generated.ts` says. */
15
+ generated?: string;
16
+ /** What the database says. */
17
+ live?: string;
18
+ }
19
+ /**
20
+ * The families a boot warning is worth raising over.
21
+ *
22
+ * Deliberately coarse. `varchar(120)` and `text` are the same family because the
23
+ * runtime treats them identically and a warning about the difference would fire
24
+ * on every boot of a correct project — the widths still reach Postgres, which
25
+ * enforces them. What this is looking for is the kind of drift that changes what
26
+ * a value *is*: a number that became text, a timestamp that became a date, a
27
+ * column that stopped existing.
28
+ */
29
+ export declare function typeFamily(sqlType: string | undefined): string;
30
+ /**
31
+ * Every way the generated module and the database disagree, for the tables both
32
+ * describe.
33
+ *
34
+ * One-directional in one respect only: a table the module has and the database
35
+ * does not is reported (that is the stale-file case), while a table the database
36
+ * has and the module does not is left alone — a database may hold anything, and
37
+ * the collections-vs-file question is `generated-schema-staleness`'s.
38
+ */
39
+ export declare function diffGeneratedSchemaAgainstCatalogue(options: {
40
+ /** The `tables` export of the bundle's schema module. */
41
+ generated: Record<string, unknown> | undefined;
42
+ /** What was introspected for this data source, keyed by bare table name. */
43
+ catalogue: Map<string, TableMeta>;
44
+ /** Collections of this source, so a column can be named as a property. */
45
+ collections: CollectionConfig[];
46
+ }): SchemaDifference[];
47
+ /** One line, in the terms the reader has: collection, property, column. */
48
+ export declare function describeSchemaDifference(difference: SchemaDifference): string;
49
+ /**
50
+ * Report the differences, one warning each, and never throw.
51
+ *
52
+ * Per difference rather than one summary block on purpose: these are read out of
53
+ * a log aggregator as often as a terminal, and a single multi-line message
54
+ * cannot be filtered down to the one collection somebody is looking at.
55
+ */
56
+ export declare function warnOnGeneratedSchemaDrift(differences: SchemaDifference[]): void;
@@ -93,3 +93,36 @@ export declare function staleVerdict(input: {
93
93
  /** Whether `--fix` was given. */
94
94
  fix: boolean;
95
95
  }): StaleVerdict;
96
+ /** One exported declaration that differs between two generated schemas. */
97
+ export interface DeclarationDifference {
98
+ /** The exported symbol: a table, an enum, a relations block. */
99
+ name: string;
100
+ kind: "missing" | "unexpected" | "changed";
101
+ }
102
+ /**
103
+ * What differs between the schema the current collections generate and the one
104
+ * on disk — by declaration, not by byte.
105
+ *
106
+ * `rebase doctor` used to whitespace-normalise both files and compare the
107
+ * strings, which answers "are these the same text" when the question is "does
108
+ * this file still describe the same schema". The difference is not academic:
109
+ * the answer was a single yes/no with nothing to act on, so a developer told
110
+ * their schema was stale had to regenerate and read the diff to find out what
111
+ * had changed.
112
+ *
113
+ * Comparing declarations answers the question the reader actually has. The
114
+ * generated file is a flat list of `export const`s — one per table, enum and
115
+ * relations block, each the rendering of a piece of the schema plan — and two
116
+ * files declaring the same symbols with the same bodies describe the same
117
+ * database whatever their formatting.
118
+ *
119
+ * Not a plan-to-plan comparison, and deliberately not: reading a plan back out
120
+ * of `schema.generated.ts` means interpreting drizzle-orm builder calls, which
121
+ * is a second interpreter — the exact thing this area was just rid of. The
122
+ * rendered declarations are the closest honest proxy, and the renderer is
123
+ * deterministic, so the only things normalised away are what it is free to vary
124
+ * independently of the plan: whitespace and its own comments.
125
+ */
126
+ export declare function compareGeneratedDeclarations(expected: string, actual: string): DeclarationDifference[];
127
+ /** The differences, as the line a developer reads in `rebase doctor`. */
128
+ export declare function describeDeclarationDifferences(differences: DeclarationDifference[]): string;
@@ -18,6 +18,8 @@ export interface TableColumn {
18
18
  ordinal_position?: number;
19
19
  /** `"ALWAYS"` for a generated column, `"NEVER"` otherwise. */
20
20
  is_generated?: string;
21
+ /** The `GENERATED ALWAYS AS (…)` expression, when `is_generated` says so. */
22
+ generation_expression?: string | null;
21
23
  /** `"YES"` for an identity column. */
22
24
  is_identity?: string;
23
25
  /** `"ALWAYS"` or `"BY DEFAULT"` on an identity column. */