@rebasepro/server-postgres 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
  2. package/dist/BranchService-BRt78gfa.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +84 -12
  4. package/dist/auth/services.d.ts +72 -14
  5. package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
  6. package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
  7. package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
  8. package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
  9. package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
  10. package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
  11. package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
  12. package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
  13. package/dist/cli-errors.d.ts +42 -0
  14. package/dist/cli-helpers.d.ts +12 -12
  15. package/dist/cli.js +91 -37
  16. package/dist/cli.js.map +1 -1
  17. package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
  18. package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
  19. package/dist/data-transformer.d.ts +0 -8
  20. package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
  21. package/dist/doctor-C-sYWbmt.js.map +1 -0
  22. package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
  23. package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
  24. package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
  25. package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
  26. package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
  27. package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
  28. package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
  29. package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
  30. package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
  31. package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
  33. package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
  34. package/dist/generated-sql.d.ts +28 -0
  35. package/dist/index.es.js +3415 -917
  36. package/dist/index.es.js.map +1 -1
  37. package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
  38. package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
  39. package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
  40. package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
  41. package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
  42. package/dist/plan-schema-DU9exq6C.js.map +1 -0
  43. package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
  44. package/dist/policy-drift-B-J2hhm0.js.map +1 -0
  45. package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
  46. package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
  47. package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
  48. package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
  49. package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
  50. package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
  51. package/dist/schema/auth-schema.d.ts +170 -0
  52. package/dist/schema/classify-change.d.ts +29 -1
  53. package/dist/schema/column-plan-helpers.d.ts +33 -21
  54. package/dist/schema/destructive-sql.d.ts +20 -1
  55. package/dist/schema/doctor-cli.js +4 -4
  56. package/dist/schema/doctor.d.ts +34 -1
  57. package/dist/schema/ensure-collection-policies.d.ts +22 -0
  58. package/dist/schema/generate-drizzle-schema.js +1 -1
  59. package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
  60. package/dist/schema/generate-postgres-ddl.js +1 -1
  61. package/dist/schema/generate-schema-commit.d.ts +12 -0
  62. package/dist/schema/introspect-db-logic.d.ts +31 -0
  63. package/dist/schema/introspect-db-queries.d.ts +1 -1
  64. package/dist/schema/introspect-db-search.d.ts +22 -0
  65. package/dist/schema/introspect-db-storage.d.ts +83 -0
  66. package/dist/schema/introspect-db.js +15 -3
  67. package/dist/schema/introspect-db.js.map +1 -1
  68. package/dist/schema/plan/diff-plan.d.ts +1 -1
  69. package/dist/schema/plan/plan-schema.d.ts +16 -8
  70. package/dist/schema/plan/render-ddl.d.ts +6 -0
  71. package/dist/schema/plan/types.d.ts +37 -11
  72. package/dist/search-column-BM-GV6vH.js +442 -0
  73. package/dist/search-column-BM-GV6vH.js.map +1 -0
  74. package/dist/security/policy-drift.d.ts +1 -1
  75. package/dist/security/rls-enforcement.d.ts +7 -0
  76. package/dist/services/BranchService.d.ts +22 -1
  77. package/dist/services/FetchService.d.ts +5 -4
  78. package/dist/services/RelationService.d.ts +18 -0
  79. package/dist/services/cdc/CdcListener.d.ts +9 -4
  80. package/dist/services/cdc/identity-columns.d.ts +19 -0
  81. package/dist/services/cdc/trigger-cdc.d.ts +45 -7
  82. package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
  83. package/dist/services/channel-history.d.ts +17 -1
  84. package/dist/services/collection-helpers.d.ts +11 -1
  85. package/dist/services/pg-notify-listener.d.ts +88 -6
  86. package/dist/services/realtimeService.d.ts +159 -45
  87. package/dist/services/socket-liveness.d.ts +45 -0
  88. package/dist/services/soft-delete.d.ts +12 -0
  89. package/dist/services/sql-script.d.ts +71 -0
  90. package/dist/services/write-depth.d.ts +16 -0
  91. package/dist/services/write-transaction-scope.d.ts +6 -3
  92. package/dist/utils/drizzle-conditions.d.ts +42 -2
  93. package/dist/utils/sql-redaction.d.ts +21 -0
  94. package/dist/websocket.d.ts +50 -18
  95. package/package.json +7 -7
  96. package/dist/BranchService-DGPL6G_C.js.map +0 -1
  97. package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
  98. package/dist/doctor-Cb2thZ8s.js.map +0 -1
  99. package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
  100. package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
  101. package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
  102. package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
  103. package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
  104. package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
  105. package/dist/plan-schema-C0fxM8dY.js.map +0 -1
  106. package/dist/policy-drift-DljYdrpW.js.map +0 -1
  107. package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
  108. package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
@@ -4,7 +4,6 @@ import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
4
  import { toSnakeCase } from "@rebasepro/utils";
5
5
  //#region src/schema/column-plan-helpers.ts
6
6
  var column_plan_helpers_exports = /* @__PURE__ */ __exportAll({
7
- assertSinglePrimaryKey: () => assertSinglePrimaryKey,
8
7
  declaresEnumType: () => declaresEnumType,
9
8
  enumLabelsOf: () => enumLabelsOf,
10
9
  getPrimaryKeyColumn: () => getPrimaryKeyColumn,
@@ -12,6 +11,7 @@ var column_plan_helpers_exports = /* @__PURE__ */ __exportAll({
12
11
  getPrimaryKeyProp: () => getPrimaryKeyProp,
13
12
  idColumnDefault: () => idColumnDefault,
14
13
  isIdProperty: () => isIdProperty,
14
+ primaryKeyPropertyNames: () => primaryKeyPropertyNames,
15
15
  resolveColumnName: () => resolveColumnName
16
16
  });
17
17
  /**
@@ -27,27 +27,31 @@ var resolveColumnName = (propName, prop) => {
27
27
  /** Every property that carries a truthy `isId`, in declaration order. */
28
28
  var idPropertyEntries = (collection) => Object.entries(collection.properties ?? {}).filter(([, prop]) => prop && typeof prop === "object" && "isId" in prop && Boolean(prop.isId));
29
29
  /**
30
- * Refuse a collection that declares two primary keys.
30
+ * The properties marked `isId`, in declaration order.
31
31
  *
32
- * Postgres allows a composite primary key; Rebase does not model one, and the
33
- * three emitters each invented a different wrong answer for it. The Drizzle
34
- * generator appended `.primaryKey()` to both columns (two primary keys, which
35
- * drizzle-kit turns into an error at push time), the DDL generator wrote two
36
- * inline `PRIMARY KEY` clauses in one `CREATE TABLE` (Postgres: "multiple
37
- * primary keys for table are not allowed"), and boot-ensure created the table
38
- * with the *first* id column and silently never added the second — so a managed
39
- * tenant got a table missing a column its own collection reads.
32
+ * Several of them are one composite primary key: the table carries a single
33
+ * `PRIMARY KEY (a, b)` constraint over their columns, and a row's address lists
34
+ * the parts in this same order (`buildCompositeId`), which is the order the
35
+ * admin reads them in too.
36
+ */
37
+ var primaryKeyPropertyNames = (collection) => idPropertyEntries(collection).map(([name]) => name);
38
+ /**
39
+ * The one column a link into this collection points at.
40
40
  *
41
- * None of those is recoverable at runtime, and two of them fail long after the
42
- * config was written. Refused here instead, where the collection has a name.
41
+ * A foreign key column references a single column, and Postgres only accepts
42
+ * that column when it is unique on its own — `REFERENCES t (a)` against a
43
+ * composite key `(a, b)` fails with "there is no unique constraint matching
44
+ * given keys for referenced table". So a link into a collection whose key
45
+ * spans several columns has no column to point at, and it is refused here,
46
+ * naming the link, rather than emitted as a constraint `db push` or boot would
47
+ * then fail to create.
48
+ *
49
+ * @param via the link that needs the column, for the refusal: `relation "posts.author"`.
43
50
  */
44
- var assertSinglePrimaryKey = (collection) => {
51
+ var getPrimaryKeyProp = (collection, via) => {
45
52
  const ids = idPropertyEntries(collection);
46
- if (ids.length < 2) return;
47
- throw new Error(`Collection "${collection.slug ?? collection.name ?? "(unnamed)"}" marks ${ids.length} properties with \`isId\` (${ids.map(([name]) => `"${name}"`).join(", ")}). Composite primary keys are not supported: give exactly one property \`isId\`, and express the second key with \`indexes: [{ on: [...], unique: true, reason: "…" }]\`.`);
48
- };
49
- var getPrimaryKeyProp = (collection) => {
50
- const idPropEntry = idPropertyEntries(collection)[0];
53
+ if (ids.length > 1) throw new Error(`${via} points at collection "${collection.slug ?? collection.name ?? "(unnamed)"}", whose primary key spans ${ids.length} columns (${ids.map(([name]) => `"${name}"`).join(", ")}). A foreign key is one column and cannot reference a composite key. Point the link at a collection with a single-column key, or reach these rows with a \`via\` relation, whose \`joinPath\` compares every key column.`);
54
+ const idPropEntry = ids[0];
51
55
  if (idPropEntry) {
52
56
  const [name, prop] = idPropEntry;
53
57
  return {
@@ -63,10 +67,16 @@ var getPrimaryKeyProp = (collection) => {
63
67
  prop: idProp
64
68
  };
65
69
  };
66
- /** The primary key's property key. What a Drizzle relation's `references` names. */
67
- var getPrimaryKeyName = (collection) => getPrimaryKeyProp(collection).name;
68
- /** The primary key's column. What a foreign key's `REFERENCES` names. */
69
- var getPrimaryKeyColumn = (collection) => getPrimaryKeyProp(collection).column;
70
+ /**
71
+ * The primary key's property key. What a Drizzle relation's `references`
72
+ * names. Refused on the same terms as {@link getPrimaryKeyProp}.
73
+ */
74
+ var getPrimaryKeyName = (collection, via) => getPrimaryKeyProp(collection, via).name;
75
+ /**
76
+ * The primary key's column. What a foreign key's `REFERENCES` names. Refused
77
+ * on the same terms as {@link getPrimaryKeyProp}.
78
+ */
79
+ var getPrimaryKeyColumn = (collection, via) => getPrimaryKeyProp(collection, via).column;
70
80
  var isIdProperty = (propName, prop, collection) => {
71
81
  if ("isId" in prop && Boolean(prop.isId)) return true;
72
82
  return idPropertyEntries(collection).length === 0 && propName === "id";
@@ -136,6 +146,6 @@ var idColumnDefault = (propName, prop, collection) => {
136
146
  };
137
147
  };
138
148
  //#endregion
139
- export { getPrimaryKeyColumn as a, idColumnDefault as c, enumLabelsOf as i, isIdProperty as l, column_plan_helpers_exports as n, getPrimaryKeyName as o, declaresEnumType as r, getPrimaryKeyProp as s, assertSinglePrimaryKey as t, resolveColumnName as u };
149
+ export { getPrimaryKeyName as a, isIdProperty as c, getPrimaryKeyColumn as i, primaryKeyPropertyNames as l, declaresEnumType as n, getPrimaryKeyProp as o, enumLabelsOf as r, idColumnDefault as s, column_plan_helpers_exports as t, resolveColumnName as u };
140
150
 
141
- //# sourceMappingURL=column-plan-helpers-DF-8dTVa.js.map
151
+ //# sourceMappingURL=column-plan-helpers-1-LQD0yI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column-plan-helpers-1-LQD0yI.js","names":[],"sources":["../src/schema/column-plan-helpers.ts"],"sourcesContent":["/**\n * The questions about a property that the schema planner asks first.\n *\n * What is the primary key, what is this column called, what does an id default\n * to, what labels does this enum have. `schema/plan/plan-schema.ts` is the only\n * caller — it is the only reader of a `Property` — and these live beside it\n * rather than inside it because they are also where the *refusals* are, and a\n * refusal wants to be reachable and testable on its own.\n *\n * They were extracted while there were still three emitters, each carrying its\n * own verbatim copy of all four rules until one of them was edited: the audit\n * that produced this module counted twelve open disagreements and found that a\n * third of the commits touching those files were re-synchronisations. The\n * emitters are renderers now and there is nothing left to keep in step, but the\n * questions are still questions about a property rather than about a plan, so\n * this is where they belong.\n *\n * Everything here is pure and synchronous, and it throws rather than guesses:\n * an enum with no values, a `cuid` id on Postgres and a foreign key into a\n * composite key are all configurations that produce a broken database in\n * silence, so they are refused where every path passes.\n */\nimport type {\n CollectionConfig,\n NumberProperty,\n Property,\n StringProperty\n} from \"@rebasepro/types\";\nimport { toSnakeCase } from \"@rebasepro/utils\";\n\n/**\n * Resolve the SQL column name for a property.\n *\n * Uses the explicit `columnName` when set (e.g. from introspection), falling\n * back to `toSnakeCase(propName)` for manually-authored collections.\n */\nexport const resolveColumnName = (propName: string, prop?: Property | null): string => {\n if (prop && \"columnName\" in prop && typeof prop.columnName === \"string\") {\n return prop.columnName;\n }\n return toSnakeCase(propName);\n};\n\n/** Every property that carries a truthy `isId`, in declaration order. */\nconst idPropertyEntries = (collection: CollectionConfig): [string, Property][] =>\n Object.entries(collection.properties ?? {}).filter(\n ([, prop]) => prop && typeof prop === \"object\" && \"isId\" in prop && Boolean((prop as { isId?: unknown }).isId)\n ) as [string, Property][];\n\n/**\n * The properties marked `isId`, in declaration order.\n *\n * Several of them are one composite primary key: the table carries a single\n * `PRIMARY KEY (a, b)` constraint over their columns, and a row's address lists\n * the parts in this same order (`buildCompositeId`), which is the order the\n * admin reads them in too.\n */\nexport const primaryKeyPropertyNames = (collection: CollectionConfig): string[] =>\n idPropertyEntries(collection).map(([name]) => name);\n\n/**\n * A collection's primary key, by both of its names.\n *\n * `name` is the property key — what the generated Drizzle table is keyed by,\n * so it is what a relation's `references` names. `column` is the SQL\n * identifier, so it is what a foreign key's `REFERENCES` names. They differ\n * whenever the key is camelCase or carries a `columnName` (`authorId` is\n * `author_id`, and every table `rebase schema introspect` adopts is spelled\n * that way), and a constraint written with the key named a column that does\n * not exist: `db push` could not build its desired state, and boot failed the\n * constraint on every start.\n */\nexport interface PrimaryKey {\n /** Property key: the Drizzle object key and the JSON field. */\n name: string;\n /** SQL column. */\n column: string;\n /** The declaring property; absent for the implicit `id TEXT` of a collection that declares none. */\n prop?: Property;\n}\n\n/**\n * The one column a link into this collection points at.\n *\n * A foreign key column references a single column, and Postgres only accepts\n * that column when it is unique on its own — `REFERENCES t (a)` against a\n * composite key `(a, b)` fails with \"there is no unique constraint matching\n * given keys for referenced table\". So a link into a collection whose key\n * spans several columns has no column to point at, and it is refused here,\n * naming the link, rather than emitted as a constraint `db push` or boot would\n * then fail to create.\n *\n * @param via the link that needs the column, for the refusal: `relation \"posts.author\"`.\n */\nexport const getPrimaryKeyProp = (collection: CollectionConfig, via: string): PrimaryKey => {\n const ids = idPropertyEntries(collection);\n if (ids.length > 1) {\n throw new Error(\n `${via} points at collection \"${collection.slug ?? collection.name ?? \"(unnamed)\"}\", whose primary ` +\n `key spans ${ids.length} columns (${ids.map(([name]) => `\"${name}\"`).join(\", \")}). A foreign key is ` +\n \"one column and cannot reference a composite key. Point the link at a collection with a \" +\n \"single-column key, or reach these rows with a `via` relation, whose `joinPath` compares every \" +\n \"key column.\"\n );\n }\n const idPropEntry = ids[0];\n if (idPropEntry) {\n const [name, prop] = idPropEntry;\n return { name, column: resolveColumnName(name, prop), prop };\n }\n // Fallback: a collection that declares no `isId` gets an implicit `id`.\n const idProp = collection.properties?.[\"id\"] as Property | undefined;\n return { name: \"id\", column: resolveColumnName(\"id\", idProp), prop: idProp };\n};\n\n/**\n * The primary key's property key. What a Drizzle relation's `references`\n * names. Refused on the same terms as {@link getPrimaryKeyProp}.\n */\nexport const getPrimaryKeyName = (collection: CollectionConfig, via: string): string =>\n getPrimaryKeyProp(collection, via).name;\n\n/**\n * The primary key's column. What a foreign key's `REFERENCES` names. Refused\n * on the same terms as {@link getPrimaryKeyProp}.\n */\nexport const getPrimaryKeyColumn = (collection: CollectionConfig, via: string): string =>\n getPrimaryKeyProp(collection, via).column;\n\nexport const isIdProperty = (propName: string, prop: Property, collection: CollectionConfig): boolean => {\n if (\"isId\" in prop && Boolean(prop.isId)) return true;\n\n // Only fall back to \"id\" when NO property is explicitly marked.\n return idPropertyEntries(collection).length === 0 && propName === \"id\";\n};\n\n/**\n * The labels an `enum` declares, in order, whichever form it was written in.\n *\n * `EnumValues` is `EnumValueConfig[] | Record<id, label>` and the docs recommend\n * the record. Boot-ensure read only the array form — `(p.enum as unknown[]).map`\n * — so a record-form enum threw `p.enum.map is not a function` on a managed\n * tenant, at boot, with no developer in the loop. The other two emitters each\n * carried their own copy of the both-forms walk.\n *\n * An empty list throws rather than returning `[]`. `CREATE TYPE … AS ENUM ()`\n * is not valid SQL, so every emitter \"handled\" it by skipping the type and then\n * typing the column with it anyway: Drizzle referenced an enum variable it never\n * declared (the generated file does not compile), the DDL named a type nothing\n * creates, and boot-ensure's `ADD COLUMN` failed — and add-column is not a\n * survivable action, so the boot died. There is no useful column at the end of\n * any of those paths.\n */\nexport const enumLabelsOf = (propName: string, prop: Property, collection: CollectionConfig): string[] => {\n const values = (prop as { enum?: unknown }).enum;\n if (values === undefined || values === null) return [];\n\n const labels = Array.isArray(values)\n ? values.map((entry: unknown) =>\n String(entry !== null && typeof entry === \"object\" && \"id\" in (entry as Record<string, unknown>)\n ? (entry as Record<string, unknown>).id\n : entry))\n : Object.keys(values as Record<string, unknown>).map(String);\n\n const usable = labels.filter(label => label.length > 0);\n if (usable.length === 0) {\n throw new Error(\n `Property \"${propName}\" of collection \"${collection.slug ?? collection.name ?? \"(unnamed)\"}\" ` +\n \"declares an empty `enum`. A Postgres enum type needs at least one label — \" +\n \"`CREATE TYPE … AS ENUM ()` is not valid SQL — so the column would reference a type \" +\n \"nothing creates. List the values, or drop the `enum` for a plain column.\"\n );\n }\n return usable;\n};\n\n/**\n * Does this property compile to a Postgres enum *type*?\n *\n * Strings only. A `number` property with an `enum` used to create a type on all\n * three paths and then declare the column `NUMERIC`/`INTEGER`, so the type was\n * orphaned the moment it was created and the values it listed were enforced by\n * nothing. The dropdown in the panel is driven by the config, not the column, so\n * nothing is lost by not creating it — and a type no column uses is one more\n * object `db push` has to plan a DROP for.\n */\nexport const declaresEnumType = (prop: Property): boolean =>\n prop.type === \"string\" && Boolean((prop as StringProperty).enum);\n\n/**\n * The `DEFAULT` an id column carries, as the thing itself rather than a\n * rendered string — each emitter spells it differently.\n *\n * The three of them disagreed on all four strategies. The DDL generator wrote\n * `DEFAULT ${prop.isId}` verbatim, so the documented `` isId: \"sql`gen_id()`\" ``\n * reached Postgres with the template wrapper still on it — a syntax error in\n * `schema.sql`. Boot-ensure gave a default to `uuid` and to nothing else, so on\n * a managed tenant every other strategy produced a column with no default at\n * all, and the first insert that trusted the config failed on a NULL primary\n * key. Only the Drizzle generator stripped the wrapper.\n */\nexport type IdColumnDefault =\n /** `gen_random_uuid()` / `.defaultRandom()`. */\n | { kind: \"uuid\" }\n /** `GENERATED BY DEFAULT AS IDENTITY` / `.generatedByDefaultAsIdentity()` — part of the type, not a DEFAULT. */\n | { kind: \"identity\" }\n /** A raw SQL expression, wrapper already stripped. */\n | { kind: \"sql\"; expression: string };\n\n/**\n * Refuses `isId: \"cuid\"`.\n *\n * `DEFAULT cuid()` has been emitted since the option existed and no `cuid()`\n * function has ever been created — not by the generators, not by boot, not by\n * any migration. So the column has never had a working default on Postgres: the\n * first insert that relied on it failed with `function cuid() does not exist`.\n * There is nothing to keep compatible.\n *\n * It is refused rather than implemented. Shipping a real `rebase.cuid()` means a\n * new SQL function in the same carved-out class as the search helpers — excluded\n * from Atlas by pattern, applied by boot, re-applied by push, checked by the\n * doctor, and versioned across the fleet forever — to reproduce a monotonic id\n * that `uuid` already provides and that a project wanting cuid's exact shape can\n * write itself in one line of SQL.\n */\nexport const idColumnDefault = (propName: string, prop: Property, collection: CollectionConfig): IdColumnDefault | undefined => {\n if (!(\"isId\" in prop)) return undefined;\n const isId = (prop as { isId?: unknown }).isId;\n // `true` and `\"manual\"` both mean \"the application supplies it\".\n if (isId === undefined || isId === null || isId === true || isId === \"manual\" || isId === false) return undefined;\n if (typeof isId !== \"string\") return undefined;\n\n if (prop.type === \"string\" && (isId as StringProperty[\"isId\"]) === \"uuid\") return { kind: \"uuid\" };\n if (prop.type === \"number\" && (isId as NumberProperty[\"isId\"]) === \"increment\") return { kind: \"identity\" };\n\n if (isId === \"cuid\") {\n throw new Error(\n `Property \"${propName}\" of collection \"${collection.slug ?? collection.name ?? \"(unnamed)\"}\" ` +\n \"uses `isId: \\\"cuid\\\"`, which Postgres cannot honour: the generated default calls a `cuid()` \" +\n \"function that Rebase has never created, so every insert relying on it fails. \" +\n \"Use `isId: \\\"uuid\\\"`, or give the strategy as SQL — ``isId: \\\"sql`my_id()`\\\"`` — and create that \" +\n \"function in a migration.\"\n );\n }\n\n // Any other string is a raw SQL default. Written either bare\n // (`gen_random_uuid()::text`) or in the documented template form\n // (``sql`gen_random_uuid()::text` ``); the wrapper is markup, not SQL.\n const expression = isId.startsWith(\"sql`\") && isId.endsWith(\"`\")\n ? isId.substring(4, isId.length - 1)\n : isId;\n return { kind: \"sql\", expression };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAa,qBAAqB,UAAkB,SAAmC;CACnF,IAAI,QAAQ,gBAAgB,QAAQ,OAAO,KAAK,eAAe,UAC3D,OAAO,KAAK;CAEhB,OAAO,YAAY,QAAQ;AAC/B;;AAGA,IAAM,qBAAqB,eACvB,OAAO,QAAQ,WAAW,cAAc,CAAC,CAAC,CAAC,CAAC,QACvC,GAAG,UAAU,QAAQ,OAAO,SAAS,YAAY,UAAU,QAAQ,QAAS,KAA4B,IAAI,CACjH;;;;;;;;;AAUJ,IAAa,2BAA2B,eACpC,kBAAkB,UAAU,CAAC,CAAC,KAAK,CAAC,UAAU,IAAI;;;;;;;;;;;;;;AAoCtD,IAAa,qBAAqB,YAA8B,QAA4B;CACxF,MAAM,MAAM,kBAAkB,UAAU;CACxC,IAAI,IAAI,SAAS,GACb,MAAM,IAAI,MACN,GAAG,IAAI,yBAAyB,WAAW,QAAQ,WAAW,QAAQ,YAAY,6BACrE,IAAI,OAAO,YAAY,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,yNAIpF;CAEJ,MAAM,cAAc,IAAI;CACxB,IAAI,aAAa;EACb,MAAM,CAAC,MAAM,QAAQ;EACrB,OAAO;GAAE;GAAM,QAAQ,kBAAkB,MAAM,IAAI;GAAG;EAAK;CAC/D;CAEA,MAAM,SAAS,WAAW,aAAa;CACvC,OAAO;EAAE,MAAM;EAAM,QAAQ,kBAAkB,MAAM,MAAM;EAAG,MAAM;CAAO;AAC/E;;;;;AAMA,IAAa,qBAAqB,YAA8B,QAC5D,kBAAkB,YAAY,GAAG,CAAC,CAAC;;;;;AAMvC,IAAa,uBAAuB,YAA8B,QAC9D,kBAAkB,YAAY,GAAG,CAAC,CAAC;AAEvC,IAAa,gBAAgB,UAAkB,MAAgB,eAA0C;CACrG,IAAI,UAAU,QAAQ,QAAQ,KAAK,IAAI,GAAG,OAAO;CAGjD,OAAO,kBAAkB,UAAU,CAAC,CAAC,WAAW,KAAK,aAAa;AACtE;;;;;;;;;;;;;;;;;;AAmBA,IAAa,gBAAgB,UAAkB,MAAgB,eAA2C;CACtG,MAAM,SAAU,KAA4B;CAC5C,IAAI,WAAW,KAAA,KAAa,WAAW,MAAM,OAAO,CAAC;CASrD,MAAM,UAPS,MAAM,QAAQ,MAAM,IAC7B,OAAO,KAAK,UACV,OAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,QAAS,QACxD,MAAkC,KACnC,KAAK,CAAC,IACd,OAAO,KAAK,MAAiC,CAAC,CAAC,IAAI,MAAM,EAAA,CAEzC,QAAO,UAAS,MAAM,SAAS,CAAC;CACtD,IAAI,OAAO,WAAW,GAClB,MAAM,IAAI,MACN,aAAa,SAAS,mBAAmB,WAAW,QAAQ,WAAW,QAAQ,YAAY,8OAI/F;CAEJ,OAAO;AACX;;;;;;;;;;;AAYA,IAAa,oBAAoB,SAC7B,KAAK,SAAS,YAAY,QAAS,KAAwB,IAAI;;;;;;;;;;;;;;;;;AAsCnE,IAAa,mBAAmB,UAAkB,MAAgB,eAA8D;CAC5H,IAAI,EAAE,UAAU,OAAO,OAAO,KAAA;CAC9B,MAAM,OAAQ,KAA4B;CAE1C,IAAI,SAAS,KAAA,KAAa,SAAS,QAAQ,SAAS,QAAQ,SAAS,YAAY,SAAS,OAAO,OAAO,KAAA;CACxG,IAAI,OAAO,SAAS,UAAU,OAAO,KAAA;CAErC,IAAI,KAAK,SAAS,YAAa,SAAoC,QAAQ,OAAO,EAAE,MAAM,OAAO;CACjG,IAAI,KAAK,SAAS,YAAa,SAAoC,aAAa,OAAO,EAAE,MAAM,WAAW;CAE1G,IAAI,SAAS,QACT,MAAM,IAAI,MACN,aAAa,SAAS,mBAAmB,WAAW,QAAQ,WAAW,QAAQ,YAAY,2SAK/F;CASJ,OAAO;EAAE,MAAM;EAAO,YAHH,KAAK,WAAW,MAAM,KAAK,KAAK,SAAS,GAAG,IACzD,KAAK,UAAU,GAAG,KAAK,SAAS,CAAC,IACjC;CAC2B;AACrC"}
@@ -41,14 +41,6 @@ export declare function sanitizeAndConvertDates(obj: unknown): unknown;
41
41
  * Transform relations for database storage (relation objects to IDs)
42
42
  */
43
43
  export declare function serializeDataToServer<M extends Record<string, unknown>>(row: M, properties: Properties, collection?: CollectionConfig, registry?: PostgresCollectionRegistry): SerializedEntityData;
44
- /**
45
- * Serialize a single property value for database storage.
46
- *
47
- * `propertyKey` is only ever used to phrase errors and warnings. Without it the
48
- * one trace a bad value left was `Expected array value for array property, got
49
- * string` — no collection, no property, no value, which in the log of a
50
- * thousand-row import names nothing at all.
51
- */
52
44
  export declare function serializePropertyToServer(value: unknown, property: Property, propertyKey?: string): unknown;
53
45
  /**
54
46
  * Transform IDs back to relation objects for frontend
@@ -1,16 +1,17 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  __createRequire(import.meta.url);
3
3
  import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
4
- import { t as columnPgType } from "./plan-schema-C0fxM8dY.js";
5
- import { i as diagnoseDbError, n as deepestErrorMessage, s as parseHostInfo } from "./cli-errors-B8qHg02P.js";
6
- import { t as generateSchema } from "./generate-drizzle-schema-logic-BfzK7UQd.js";
4
+ import { i as diagnoseDbError, n as deepestErrorMessage, s as parseHostInfo } from "./cli-errors-DsA-K9uP.js";
5
+ import { n as planSchema, t as columnPgType } from "./plan-schema-DU9exq6C.js";
6
+ import { t as generateSchema } from "./generate-drizzle-schema-logic-sSFDp6LR.js";
7
+ import { r as renderPgType } from "./render-ddl-Ds2t_d9V.js";
7
8
  import { n as outError, t as out } from "./cli-output-CNdMql-L.js";
8
9
  import { t as chalk } from "./source-Br7L7GOI.js";
9
10
  import { n as describeDeclarationDifferences, t as compareGeneratedDeclarations } from "./generated-schema-staleness-Di9c4ZxN.js";
10
11
  import { loadCollectionsFromDirectory } from "@rebasepro/server";
11
12
  import { computeSchemaVersion, getDeclaredSubcollections, isManyToMany, isPostgresCollectionConfig } from "@rebasepro/types";
12
13
  import { effectiveAccess, fieldKeyForColumn, findRelation, getTableName, isRelationRequired, relationalCollections, resolveCollectionRelations, resolveJunctionSpecs, sortCollectionsBySlug } from "@rebasepro/common";
13
- import { toSnakeCase } from "@rebasepro/utils";
14
+ import { toPostgresIdentifier, toSnakeCase } from "@rebasepro/utils";
14
15
  import * as fs$1 from "fs";
15
16
  import { promises } from "fs";
16
17
  //#region ../codegen/dist/index.es.js
@@ -201,7 +202,9 @@ function includedRelationType(relation, accessors) {
201
202
  const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
202
203
  const accessor = slug ? accessors.get(slug) : void 0;
203
204
  const rowType = accessor ? `Database[${emitString(accessor)}]["Row"]` : "Record<string, unknown>";
204
- return relation.cardinality === "many" ? `Array<${rowType}>` : rowType;
205
+ const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "read") : void 0;
206
+ const elementType = pivot ? `${rowType} & { _pivot?: ${pivot} }` : rowType;
207
+ return relation.cardinality === "many" ? `Array<${elementType}>` : elementType;
205
208
  }
206
209
  /**
207
210
  * Map every slug to the property name it is reachable under on `client.data`.
@@ -228,6 +231,19 @@ function buildAccessors(collections) {
228
231
  }
229
232
  return accessors;
230
233
  }
234
+ /**
235
+ * A column's type on a write: its own type, or `null` too when it is not
236
+ * required.
237
+ *
238
+ * `Row` has typed a non-required column `T | null` for a while; its write twins
239
+ * did not, so the one way to clear a value — `update(id, { publishedAt: null })`,
240
+ * `{ authorId: null }` to unassign, the edit-form round trip of a row that came
241
+ * back with nulls in it — was a compile error the server would have accepted.
242
+ * A primary key is never null, required or not.
243
+ */
244
+ function writableType(prop, tsType) {
245
+ return prop.validation?.required || isPrimaryKey(prop) ? tsType : `${tsType} | null`;
246
+ }
231
247
  /** One emitted `key: type;` line, already indented. */
232
248
  function line(key, type, optional) {
233
249
  return ` ${emitKey(key)}${optional ? "?" : ""}: ${type};`;
@@ -239,9 +255,9 @@ function line(key, type, optional) {
239
255
  * property. `Row` already honoured that; `Insert` and `Update` deliberately did
240
256
  * not, on the reading that the column is stripped from responses rather than
241
257
  * from writes. That left the generated types as the one place a password hash
242
- * was still named, and it invited a client to send one. The server still
243
- * *accepts* such a field on a write — this describes the surface, it does not
244
- * add an enforcement point — but nothing generated advertises it.
258
+ * was still named, and it invited a client to send one. The server refuses
259
+ * such a field on a write (`VALIDATION_EXCLUDED_FIELDS`), so nothing generated
260
+ * advertises it.
245
261
  *
246
262
  * Read through `effectiveAccess`, so the flag and its longhand
247
263
  * `access: { read: [], write: [] }` produce the same file. A *role* rule is
@@ -265,6 +281,28 @@ function excludedApiKeys(properties) {
265
281
  }
266
282
  return excluded;
267
283
  }
284
+ /**
285
+ * The keys nobody may write: `access: { write: [] }`, readable or not.
286
+ *
287
+ * Off `Insert` and `Update` for the same reason `excludedApiKeys` takes a
288
+ * hidden property off all three — except that a field everyone can read but
289
+ * nobody can write stays on `Row`. The server refuses one on any write with
290
+ * `VALIDATION_EXCLUDED_FIELDS`, for every caller, `admin` included, so a
291
+ * generated `create({ computed })` compiled and could only ever be a 400.
292
+ *
293
+ * A write rule naming *roles* is not this: some caller can write it, and one
294
+ * generated shape cannot say which.
295
+ */
296
+ function unwritableKeys(properties) {
297
+ const unwritable = /* @__PURE__ */ new Set();
298
+ for (const [key, rawProp] of Object.entries(properties)) {
299
+ const prop = rawProp;
300
+ if (effectiveAccess(prop)?.write?.length !== 0) continue;
301
+ unwritable.add(key);
302
+ if (prop.columnName) unwritable.add(prop.columnName);
303
+ }
304
+ return unwritable;
305
+ }
268
306
  function generateTypedefs(input) {
269
307
  const collections = sortCollectionsBySlug(input);
270
308
  const accessors = buildAccessors(collections);
@@ -329,39 +367,42 @@ function generateTypedefs(input) {
329
367
  }
330
368
  lines.push(" };");
331
369
  lines.push(" Insert: {");
370
+ const notWritable = /* @__PURE__ */ new Set([...excluded, ...unwritableKeys(properties)]);
332
371
  emittedKeys.clear();
333
- for (const key of excluded) emittedKeys.add(key);
372
+ for (const key of notWritable) emittedKeys.add(key);
334
373
  for (const [key, rawProp] of Object.entries(properties)) {
335
374
  const prop = rawProp;
336
375
  if (prop.type === "relation") continue;
337
- if (excluded.has(key)) continue;
376
+ if (notWritable.has(key)) continue;
338
377
  const tsType = propertyToTypeScriptType(prop);
339
378
  const isOptional = !prop.validation?.required || isAutoAssignedId(prop);
340
- lines.push(line(key, tsType, isOptional));
379
+ lines.push(line(key, writableType(prop, tsType), isOptional));
341
380
  emittedKeys.add(key);
342
381
  }
343
382
  emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, false);
344
383
  lines.push(" };");
345
384
  lines.push(" Update: {");
346
385
  emittedKeys.clear();
347
- for (const key of excluded) emittedKeys.add(key);
386
+ for (const key of notWritable) emittedKeys.add(key);
348
387
  for (const [key, rawProp] of Object.entries(properties)) {
349
388
  const prop = rawProp;
350
389
  if (prop.type === "relation") continue;
351
390
  if (isPrimaryKey(prop)) continue;
352
- if (excluded.has(key)) continue;
353
- lines.push(line(key, propertyToTypeScriptType(prop), true));
391
+ if (notWritable.has(key)) continue;
392
+ lines.push(line(key, writableType(prop, propertyToTypeScriptType(prop)), true));
354
393
  emittedKeys.add(key);
355
394
  }
356
395
  emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, true);
357
396
  lines.push(" };");
358
- lines.push(" Relations: {");
397
+ if (Object.keys(resolvedRelations).length === 0) lines.push(" Relations: Record<never, never>;");
398
+ else lines.push(" Relations: {");
359
399
  for (const [key, relation] of Object.entries(resolvedRelations)) {
360
400
  const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
361
401
  const accessor = slug ? accessors.get(slug) : void 0;
362
402
  lines.push(` ${emitKey(key)}: ${accessor ? emitString(accessor) : "never"};`);
363
403
  }
364
- lines.push(" };");
404
+ if (Object.keys(resolvedRelations).length > 0) lines.push(" };");
405
+ lines.push(` Slug: ${emitString(collection.slug)};`);
365
406
  lines.push(" };");
366
407
  }
367
408
  lines.push("}");
@@ -484,8 +525,9 @@ function includeHelperLines() {
484
525
  function emitWritableRelations(lines, collection, properties, resolvedRelations, emittedKeys, allOptional) {
485
526
  const emit = (key, relation) => {
486
527
  if (emittedKeys.has(key)) return;
487
- const optional = allOptional || !isRelationRequired(collection, relation);
488
- lines.push(line(key, foreignKeyType(relation), optional));
528
+ const required = isRelationRequired(collection, relation);
529
+ const type = required ? foreignKeyType(relation) : `${foreignKeyType(relation)} | null`;
530
+ lines.push(line(key, type, allOptional || !required));
489
531
  emittedKeys.add(key);
490
532
  };
491
533
  for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(fieldKeyForColumn(collection, relation.localKey), relation);
@@ -494,6 +536,31 @@ function emitWritableRelations(lines, collection, properties, resolvedRelations,
494
536
  const relation = findRelation(resolvedRelations, key);
495
537
  if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
496
538
  }
539
+ for (const [key, relation] of Object.entries(resolvedRelations)) {
540
+ if (relation.cardinality !== "many" || !relation.writable) continue;
541
+ if (emittedKeys.has(key)) continue;
542
+ const fk = foreignKeyType(relation);
543
+ const pivot = relation.kind === "manyToMany" ? pivotType(relation.through.properties, "write") : void 0;
544
+ const element = `${fk} | { id: ${fk}${pivot ? `; _pivot?: ${pivot}` : ""} }`;
545
+ lines.push(line(key, `Array<${element}>`, true));
546
+ emittedKeys.add(key);
547
+ }
548
+ }
549
+ /**
550
+ * The `_pivot` a many-to-many link carries: the junction's payload columns,
551
+ * typed as a row's columns are on a read and as a write's on a write. `undefined`
552
+ * when the junction declares none, so there is no `_pivot` to name.
553
+ */
554
+ function pivotType(pivotProperties, direction) {
555
+ const entries = Object.entries(pivotProperties);
556
+ if (entries.length === 0) return void 0;
557
+ return `{ ${entries.map(([key, rawProp]) => {
558
+ const prop = rawProp;
559
+ const tsType = propertyToTypeScriptType(prop);
560
+ const required = Boolean(prop.validation?.required);
561
+ const type = direction === "write" ? writableType(prop, tsType) : required ? tsType : `${tsType} | null`;
562
+ return `${emitKey(key)}${required && direction === "read" ? "" : "?"}: ${type};`;
563
+ }).join(" ")} }`;
497
564
  }
498
565
  //#endregion
499
566
  //#region src/schema/doctor.ts
@@ -511,11 +578,14 @@ var doctor_exports = /* @__PURE__ */ __exportAll({
511
578
  checkCollectionsVsSchema: () => checkCollectionsVsSchema,
512
579
  checkCollectionsVsSdk: () => checkCollectionsVsSdk,
513
580
  getExpectedColumnType: () => getExpectedColumnType,
581
+ junctionKeyIssues: () => junctionKeyIssues,
514
582
  loadCollections: () => loadCollections,
515
583
  missingExtensionIssues: () => missingExtensionIssues,
584
+ primaryKeyIssues: () => primaryKeyIssues,
516
585
  renderReport: () => renderReport,
517
586
  runDoctor: () => runDoctor,
518
- schemaStampIssues: () => schemaStampIssues
587
+ schemaStampIssues: () => schemaStampIssues,
588
+ typeMismatchFix: () => typeMismatchFix
519
589
  });
520
590
  /**
521
591
  * Resolve the SQL column name for a property.
@@ -736,6 +806,91 @@ async function schemaStampIssues(pool, collections) {
736
806
  fix: "A hash says the two disagree, never which is ahead. Check you are pointed at the database you think you are; then `rebase db push` to bring it to these collections."
737
807
  }];
738
808
  }
809
+ /** `"public"."posts"` — quoted, for a statement the reader will paste. */
810
+ var quotedTable = (schema, table) => `"${schema}"."${table}"`;
811
+ /**
812
+ * What to do about a column whose type is not the one the collection plans.
813
+ *
814
+ * Both ways out, because the doctor cannot know which side is right: the
815
+ * property moved, or the database did. Putting the property back always clears
816
+ * the finding; converting the column clears it when every value converts.
817
+ */
818
+ function typeMismatchFix(schema, table, column, actual, planned) {
819
+ const back = `Either put the property back to the column's type (${actual}), or`;
820
+ if (!planned) return `${back} run a migration that converts the column.`;
821
+ const type = renderPgType(planned);
822
+ return `${back} convert the column when you can watch it (every value must convert): ALTER TABLE ${quotedTable(schema, table)} ALTER COLUMN "${column}" TYPE ${type} USING "${column}"::${type};`;
823
+ }
824
+ /**
825
+ * The table's primary key against the one the collection reads it by.
826
+ *
827
+ * Rebase addresses a row by the key the plan names — the `isId` property, or
828
+ * the implicit `id` — so a table keyed on other columns, or on none, serves
829
+ * reads and writes against a key that does not hold. An error, because that is
830
+ * what it is at runtime.
831
+ */
832
+ function primaryKeyIssues(tablePlan, dbPk, displayName) {
833
+ const expected = tablePlan.primaryKey;
834
+ if (dbPk.length === expected.length && [...dbPk].sort().join("\0") === [...expected].sort().join("\0")) return [];
835
+ const table = quotedTable(tablePlan.schema, tablePlan.table);
836
+ const keyList = expected.map((c) => `"${c}"`).join(", ");
837
+ if (dbPk.length === 0) return [{
838
+ severity: "error",
839
+ category: "primary_key_mismatch",
840
+ table: displayName,
841
+ expected: expected.join(", "),
842
+ actual: "(none)",
843
+ message: `Table "${displayName}" has no primary key; the collection reads its rows by ${keyList}.`,
844
+ fix: `ALTER TABLE ${table} ADD PRIMARY KEY (${keyList});`
845
+ }];
846
+ const only = expected.some((column) => !dbPk.includes(column)) ? "only " : "";
847
+ const fix = dbPk.length === 1 ? `Mark ${only}the property for column "${dbPk[0]}" with \`isId\` — the table is keyed on it — or re-key the table on ${keyList}.` : `Mark ${only}the properties for columns ${dbPk.map((c) => `"${c}"`).join(", ")} with \`isId\` — together they are the table's composite key — or re-key the table on ${keyList}.`;
848
+ return [{
849
+ severity: "error",
850
+ category: "primary_key_mismatch",
851
+ table: displayName,
852
+ expected: expected.join(", "),
853
+ actual: dbPk.join(", "),
854
+ message: `Table "${displayName}" is keyed on (${dbPk.join(", ")}), but the collection reads its rows by ${keyList}.`,
855
+ fix
856
+ }];
857
+ }
858
+ /**
859
+ * A junction's two key columns: present, typed like the keys they point at,
860
+ * and the table keyed on the pair. Order is not compared — boot and `db push`
861
+ * have built the key in both orders, and either one serves the same rows.
862
+ */
863
+ function junctionKeyIssues(junctionPlan, dbColumns, dbPk, displayName) {
864
+ const issues = [];
865
+ const byName = new Map(dbColumns.map((c) => [c.column_name, c]));
866
+ for (const column of junctionPlan.columns) {
867
+ const dbCol = byName.get(column.column);
868
+ if (!dbCol) {
869
+ issues.push({
870
+ severity: "error",
871
+ category: "missing_column",
872
+ table: displayName,
873
+ column: column.column,
874
+ message: `Junction table "${displayName}" has no "${column.column}" column.`,
875
+ fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
876
+ });
877
+ continue;
878
+ }
879
+ const expected = INFORMATION_SCHEMA_NAMES[column.type.kind];
880
+ if (dbCol.data_type !== expected) issues.push({
881
+ severity: "warning",
882
+ category: "type_mismatch",
883
+ table: displayName,
884
+ column: column.column,
885
+ expected,
886
+ actual: dbCol.data_type,
887
+ message: `Junction column "${column.column}" in "${displayName}" is ${dbCol.data_type}, but the key it points at is ${expected}.`,
888
+ fix: typeMismatchFix(junctionPlan.schema, junctionPlan.table, column.column, dbCol.data_type, column.type)
889
+ });
890
+ }
891
+ issues.push(...primaryKeyIssues(junctionPlan, dbPk, displayName));
892
+ return issues;
893
+ }
739
894
  async function checkCollectionsVsDatabase(collections, databaseUrl) {
740
895
  const issues = [];
741
896
  const pgModule = await import("pg");
@@ -767,15 +922,27 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
767
922
  if (!columnsByTable.has(key)) columnsByTable.set(key, []);
768
923
  columnsByTable.get(key).push(row);
769
924
  }
770
- const enumsResult = await pool.query(`SELECT t.typname as enum_name, e.enumlabel as enum_value
925
+ const enumsResult = await pool.query(`SELECT n.nspname AS enum_schema, t.typname AS enum_name, e.enumlabel AS enum_value
771
926
  FROM pg_type t
927
+ JOIN pg_namespace n ON n.oid = t.typnamespace
772
928
  JOIN pg_enum e ON t.oid = e.enumtypid
773
- ORDER BY t.typname, e.enumsortorder`);
929
+ ORDER BY n.nspname, t.typname, e.enumsortorder`);
774
930
  const enumsByName = /* @__PURE__ */ new Map();
775
931
  for (const row of enumsResult.rows) {
776
- if (!enumsByName.has(row.enum_name)) enumsByName.set(row.enum_name, []);
777
- enumsByName.get(row.enum_name).push(row.enum_value);
932
+ const key = `${row.enum_schema}.${row.enum_name}`;
933
+ if (!enumsByName.has(key)) enumsByName.set(key, []);
934
+ enumsByName.get(key).push(row.enum_value);
778
935
  }
936
+ const pksResult = await pool.query(`SELECT n.nspname AS table_schema, c.relname AS table_name,
937
+ array_agg(a.attname::text ORDER BY array_position(i.indkey::int2[], a.attnum)) AS columns
938
+ FROM pg_index i
939
+ JOIN pg_class c ON c.oid = i.indrelid
940
+ JOIN pg_namespace n ON n.oid = c.relnamespace
941
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey::int2[])
942
+ WHERE i.indisprimary AND n.nspname = ANY($1)
943
+ GROUP BY n.nspname, c.relname`, [schemas]);
944
+ const pksByTable = /* @__PURE__ */ new Map();
945
+ for (const row of pksResult.rows) pksByTable.set(row.table_schema === "public" ? row.table_name : `${row.table_schema}.${row.table_name}`, row.columns);
779
946
  const fksResult = await pool.query(`SELECT
780
947
  tc.constraint_name,
781
948
  tc.table_schema,
@@ -800,6 +967,14 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
800
967
  }
801
968
  const postgresCollections = relationalCollections(collections);
802
969
  const junctionSpecs = resolveJunctionSpecs(postgresCollections);
970
+ let plan;
971
+ try {
972
+ plan = planSchema(postgresCollections);
973
+ } catch {
974
+ plan = void 0;
975
+ }
976
+ const tablePlans = new Map((plan?.tables ?? []).map((t) => [t.qualified, t]));
977
+ const checkedJunctions = /* @__PURE__ */ new Set();
803
978
  for (const collection of postgresCollections) {
804
979
  const tableName = getTableName(collection);
805
980
  const schemaName = collection.schema || "public";
@@ -816,11 +991,8 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
816
991
  }
817
992
  const dbColumns = columnsByTable.get(fullTableName) ?? [];
818
993
  const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
819
- const systemColumns = /* @__PURE__ */ new Set([
820
- "id",
821
- "created_on",
822
- "updated_on"
823
- ]);
994
+ const tablePlan = tablePlans.get(`${schemaName}.${tableName}`);
995
+ if (tablePlan) issues.push(...primaryKeyIssues(tablePlan, pksByTable.get(fullTableName) ?? [], fullTableName));
824
996
  for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
825
997
  if (prop.type === "relation") {
826
998
  const relation = findRelation(resolveCollectionRelations(collection), prop.relation?.relationName ?? propName);
@@ -855,7 +1027,6 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
855
1027
  continue;
856
1028
  }
857
1029
  const colName = resolveColumnName(propName, prop);
858
- if (systemColumns.has(colName)) continue;
859
1030
  const dbCol = dbColumnMap.get(colName);
860
1031
  if (!dbCol) {
861
1032
  issues.push({
@@ -888,29 +1059,32 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
888
1059
  if (expectedUdt && dbCol.udt_name !== expectedUdt) isMismatch = true;
889
1060
  }
890
1061
  }
891
- if (isMismatch) issues.push({
892
- severity: "warning",
893
- category: "type_mismatch",
894
- table: fullTableName,
895
- column: colName,
896
- expected: prop.type === "vector" ? "vector" : expectedType,
897
- actual: dbCol.udt_name === "vector" ? "vector" : actualType,
898
- message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
899
- fix: "Review collection property type or run a migration"
900
- });
1062
+ if (isMismatch) {
1063
+ const planned = tablePlan?.columns.find((c) => c.column === colName)?.type;
1064
+ issues.push({
1065
+ severity: "warning",
1066
+ category: "type_mismatch",
1067
+ table: fullTableName,
1068
+ column: colName,
1069
+ expected: prop.type === "vector" ? "vector" : expectedType,
1070
+ actual: dbCol.udt_name === "vector" ? "vector" : actualType,
1071
+ message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
1072
+ fix: typeMismatchFix(schemaName, tableName, colName, actualType, planned)
1073
+ });
1074
+ }
901
1075
  }
902
1076
  if (prop.type === "string" && prop.enum) {
903
1077
  const enumValues = prop.enum;
904
1078
  if (enumValues) {
905
- const enumName = `${tableName}_${colName}`;
906
- const dbEnumValues = enumsByName.get(enumName);
1079
+ const enumName = toPostgresIdentifier(`${tableName}_${colName}`);
1080
+ const dbEnumValues = enumsByName.get(`${schemaName}.${enumName}`);
907
1081
  if (!dbEnumValues) issues.push({
908
1082
  severity: "warning",
909
1083
  category: "missing_enum",
910
1084
  table: fullTableName,
911
1085
  column: colName,
912
1086
  expected: enumName,
913
- message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
1087
+ message: `Enum type "${schemaName}"."${enumName}" is defined in collection but not found in the database.`,
914
1088
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
915
1089
  });
916
1090
  else {
@@ -948,6 +1122,11 @@ async function checkCollectionsVsDatabase(collections, databaseUrl) {
948
1122
  message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
949
1123
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
950
1124
  });
1125
+ else if (!checkedJunctions.has(fullJunctionTable)) {
1126
+ checkedJunctions.add(fullJunctionTable);
1127
+ const junctionPlan = tablePlans.get(`${junctionSchema}.${junctionTable}`);
1128
+ if (junctionPlan) issues.push(...junctionKeyIssues(junctionPlan, columnsByTable.get(fullJunctionTable) ?? [], pksByTable.get(fullJunctionTable) ?? [], fullJunctionTable));
1129
+ }
951
1130
  }
952
1131
  }
953
1132
  } finally {
@@ -1032,10 +1211,12 @@ function formatCategory(cat) {
1032
1211
  missing_column: "Missing Column",
1033
1212
  type_mismatch: "Type Mismatch",
1034
1213
  missing_constraint: "Missing Constraint",
1214
+ primary_key_mismatch: "Primary Key Mismatch",
1035
1215
  schema_stale: "Stale Schema",
1036
1216
  missing_enum: "Missing Enum",
1037
1217
  enum_value_mismatch: "Enum Value Mismatch",
1038
1218
  missing_foreign_key: "Missing Foreign Key",
1219
+ orphaned_junction: "Orphaned Junction Table",
1039
1220
  sdk_stale: "Stale SDK Types",
1040
1221
  sdk_not_generated: "SDK Types Not Generated",
1041
1222
  sdk_ungeneratable: "SDK Cannot Be Generated"
@@ -1105,4 +1286,4 @@ async function runDoctor(options) {
1105
1286
  //#endregion
1106
1287
  export { loadCollections as n, runDoctor as r, doctor_exports as t };
1107
1288
 
1108
- //# sourceMappingURL=doctor-Cb2thZ8s.js.map
1289
+ //# sourceMappingURL=doctor-C-sYWbmt.js.map