@rebasepro/server-postgres 0.13.0 → 0.13.1-canary.g06dbe5b

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 (85) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/auth/services.d.ts +19 -0
  3. package/dist/{src-DlPBctw_.js → auth-users-columns-CBEOeYqa.js} +835 -63
  4. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  5. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  6. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  7. package/dist/cli-helpers.d.ts +57 -1
  8. package/dist/data-transformer.d.ts +7 -2
  9. package/dist/data_driver-ULAyJEi9.js +193 -0
  10. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  11. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-B_JMGa5K.js} +2 -2
  12. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-B_JMGa5K.js.map} +1 -1
  13. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DzeTEvMv.js} +170 -20
  14. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  15. package/dist/index.es.js +1365 -338
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  18. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  19. package/dist/schema/auth-schema.d.ts +102 -0
  20. package/dist/schema/auth-users-columns.d.ts +97 -0
  21. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  22. package/dist/schema/doctor.d.ts +23 -25
  23. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  24. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  25. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  26. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  27. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  28. package/dist/schema/search-column.d.ts +248 -0
  29. package/dist/security/rls-enforcement.d.ts +61 -5
  30. package/dist/services/FetchService.d.ts +34 -7
  31. package/dist/services/RelationService.d.ts +30 -0
  32. package/dist/services/collection-helpers.d.ts +26 -0
  33. package/dist/services/dataService.d.ts +5 -0
  34. package/dist/services/realtimeService.d.ts +131 -21
  35. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  36. package/dist/src-C_wvdMnl.js.map +1 -0
  37. package/dist/utils/drizzle-conditions.d.ts +124 -2
  38. package/dist/{websocket-B2LsrINK.js → websocket-D1qbmLZ2.js} +75 -18
  39. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  40. package/package.json +9 -8
  41. package/src/PostgresBackendDriver.ts +172 -6
  42. package/src/PostgresBootstrapper.ts +55 -6
  43. package/src/auth/ensure-tables.ts +207 -86
  44. package/src/auth/services.ts +69 -5
  45. package/src/cli-helpers.ts +129 -10
  46. package/src/cli.ts +232 -30
  47. package/src/collections/validate-relations.ts +124 -17
  48. package/src/data-transformer.ts +120 -17
  49. package/src/history/ensure-history-table.ts +7 -0
  50. package/src/schema/auth-schema.ts +17 -1
  51. package/src/schema/auth-users-columns.ts +131 -0
  52. package/src/schema/doctor-cli.ts +12 -63
  53. package/src/schema/doctor-policy-checks.ts +105 -0
  54. package/src/schema/doctor.ts +135 -76
  55. package/src/schema/ensure-collection-tables.ts +374 -32
  56. package/src/schema/generate-drizzle-schema-logic.ts +132 -42
  57. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  58. package/src/schema/generate-postgres-ddl.ts +25 -2
  59. package/src/schema/generated-schema-staleness.ts +169 -0
  60. package/src/schema/introspect-db-logic.ts +66 -34
  61. package/src/schema/non-sql-collections.test.ts +131 -0
  62. package/src/schema/rls-bootstrap-sql.ts +288 -0
  63. package/src/schema/search-column.ts +643 -0
  64. package/src/security/anonymous-grants.test.ts +4 -2
  65. package/src/security/rls-enforcement.ts +149 -6
  66. package/src/services/BranchService.ts +5 -0
  67. package/src/services/FetchService.ts +175 -108
  68. package/src/services/PersistService.ts +38 -2
  69. package/src/services/RelationService.ts +110 -67
  70. package/src/services/channel-history.ts +14 -0
  71. package/src/services/channel-presence.ts +13 -0
  72. package/src/services/collection-helpers.ts +54 -1
  73. package/src/services/dataService.ts +5 -0
  74. package/src/services/realtimeService.ts +344 -79
  75. package/src/utils/drizzle-conditions.ts +365 -23
  76. package/src/utils/pg-error-utils.ts +8 -3
  77. package/src/websocket.ts +113 -16
  78. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  79. package/dist/policy-CeA1JcxP.js +0 -105
  80. package/dist/policy-CeA1JcxP.js.map +0 -1
  81. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  82. package/dist/src-DlPBctw_.js.map +0 -1
  83. package/dist/src-DoU9yPqq.js.map +0 -1
  84. package/dist/websocket-B2LsrINK.js.map +0 -1
  85. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -1,6 +1,7 @@
1
1
  import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo, type ResolvedForeignKeyOnTarget, hasForeignKeyOnTarget } from "@rebasepro/types";
2
2
  import { getPrimaryKeys } from "../services/collection-helpers";
3
- import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
3
+ import { buildSearchColumnSpec } from "./search-column";
4
+ import { getEnumVarName, getTableName, getTableVarName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength, relationalCollections, sortCollectionsBySlug } from "@rebasepro/common";
4
5
  import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
5
6
  import { logger } from "@rebasepro/server";
6
7
  // --- Helper Functions ---
@@ -10,6 +11,31 @@ import { logger } from "@rebasepro/server";
10
11
  * Uses the explicit `columnName` when set (e.g. from introspection),
11
12
  * falling back to `toSnakeCase(propName)` for manually-authored collections.
12
13
  */
14
+ const JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
15
+
16
+ /**
17
+ * A string literal for the generated schema file.
18
+ *
19
+ * Column names, table names and enum values are all written into this file as
20
+ * literals, and none of them is constrained to be quote-free: a Postgres
21
+ * identifier only has to be quoted, and `O'Brien` is an ordinary enum value.
22
+ * Interpolating them raw ended the literal early — for enum values, inside
23
+ * single quotes, where an apostrophe is not an edge case.
24
+ */
25
+ const quote = (value: string): string => JSON.stringify(value);
26
+
27
+ /** An object key: verbatim when it is an identifier, quoted otherwise. */
28
+ const propKey = (name: string): string => (JS_IDENTIFIER.test(name) ? name : quote(name));
29
+
30
+ /**
31
+ * A property access on a generated table variable.
32
+ *
33
+ * `users.full name` is not an expression; `users["full name"]` is, and Drizzle
34
+ * treats the two identically.
35
+ */
36
+ const member = (object: string, key: string): string =>
37
+ (JS_IDENTIFIER.test(key) ? `${object}.${key}` : `${object}[${quote(key)}]`);
38
+
13
39
  const resolveColumnName = (propName: string, prop?: Property | null): string => {
14
40
  if (prop && "columnName" in prop && typeof prop.columnName === "string") {
15
41
  return prop.columnName;
@@ -82,7 +108,14 @@ const isIdProperty = (propName: string, prop: Property, collection: CollectionCo
82
108
  return !hasExplicitId && propName === "id";
83
109
  };
84
110
 
85
- const getDrizzleColumn = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string | null => {
111
+ /**
112
+ * The Drizzle column declaration a property compiles to, or `null` when the
113
+ * property puts no column on *this* table (an inverse relation, whose column
114
+ * lives on the target). Exported so it can be checked against its DDL twin
115
+ * `getSqlColumnType` directly — the two disagreeing is what left `geopoint`
116
+ * with a database column and no Drizzle key.
117
+ */
118
+ export const getDrizzleColumn = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string | null => {
86
119
 
87
120
  const colName = resolveColumnName(propName, prop);
88
121
  let columnDefinition: string;
@@ -92,23 +125,23 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
92
125
  const stringProp = prop as unknown as StringProperty;
93
126
  if (stringProp.enum) {
94
127
  const enumName = getEnumVarName(getTableName(collection), propName);
95
- columnDefinition = `${enumName}("${colName}")`;
128
+ columnDefinition = `${enumName}(${quote(colName)})`;
96
129
  } else if ("isId" in stringProp && stringProp.isId === "uuid") {
97
- columnDefinition = `uuid("${colName}")`;
130
+ columnDefinition = `uuid(${quote(colName)})`;
98
131
  } else if (stringProp.columnType === "uuid") {
99
- columnDefinition = `uuid("${colName}")`;
132
+ columnDefinition = `uuid(${quote(colName)})`;
100
133
  } else if (stringProp.columnType === "char") {
101
- columnDefinition = `char("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
134
+ columnDefinition = `char(${quote(colName)}, { length: ${resolveStringColumnLength(stringProp)} })`;
102
135
  } else if (stringProp.columnType === "varchar") {
103
136
  // The length is not optional decoration: `varchar("col")` with
104
137
  // no length is an UNBOUNDED varchar in Postgres, which is what
105
138
  // this emitted while the DDL generator emitted VARCHAR(255) for
106
139
  // the very same property.
107
- columnDefinition = `varchar("${colName}", { length: ${resolveStringColumnLength(stringProp)} })`;
140
+ columnDefinition = `varchar(${quote(colName)}, { length: ${resolveStringColumnLength(stringProp)} })`;
108
141
  } else {
109
142
  // `text` is the default, and the only length-unbounded choice.
110
143
  // Ask for `varchar` explicitly if you want the length constraint.
111
- columnDefinition = `text("${colName}")`;
144
+ columnDefinition = `text(${quote(colName)})`;
112
145
  }
113
146
  if (isIdProperty(propName, prop, collection)) {
114
147
  columnDefinition += ".primaryKey()";
@@ -134,9 +167,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
134
167
  const numProp = prop as unknown as NumberProperty;
135
168
  const isId = isIdProperty(propName, prop, collection);
136
169
 
137
- let baseType = (numProp.validation?.integer || isId) ? `integer("${colName}")` : `numeric("${colName}")`;
170
+ let baseType = (numProp.validation?.integer || isId) ? `integer(${quote(colName)})` : `numeric(${quote(colName)})`;
138
171
  if (numProp.columnType) {
139
- if (numProp.columnType === "double precision") baseType = `doublePrecision("${colName}")`;
172
+ if (numProp.columnType === "double precision") baseType = `doublePrecision(${quote(colName)})`;
140
173
  // `bigint` and `bigserial` are the only pg-core builders that
141
174
  // *require* a config argument: without `mode`, drizzle cannot
142
175
  // know whether to hand back a `number` or a `bigint`, and the
@@ -154,9 +187,9 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
154
187
  // switching the runtime type would be a breaking change to
155
188
  // every consumer of the generated schema.
156
189
  else if (numProp.columnType === "bigint" || numProp.columnType === "bigserial") {
157
- baseType = `${numProp.columnType}("${colName}", { mode: "number" })`;
190
+ baseType = `${numProp.columnType}(${quote(colName)}, { mode: "number" })`;
158
191
  }
159
- else baseType = `${numProp.columnType}("${colName}")`;
192
+ else baseType = `${numProp.columnType}(${quote(colName)})`;
160
193
  }
161
194
 
162
195
  if ("isId" in numProp && numProp.isId === "increment") {
@@ -180,16 +213,16 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
180
213
  break;
181
214
  }
182
215
  case "boolean":
183
- columnDefinition = `boolean("${colName}")`;
216
+ columnDefinition = `boolean(${quote(colName)})`;
184
217
  break;
185
218
  case "date": {
186
219
  const dateProp = prop as DateProperty;
187
220
  if (dateProp.columnType === "date") {
188
- columnDefinition = `date("${colName}", { mode: 'string' })`;
221
+ columnDefinition = `date(${quote(colName)}, { mode: 'string' })`;
189
222
  } else if (dateProp.columnType === "time") {
190
- columnDefinition = `time("${colName}")`;
223
+ columnDefinition = `time(${quote(colName)})`;
191
224
  } else {
192
- columnDefinition = `timestamp("${colName}", { withTimezone: true, mode: 'string' })`;
225
+ columnDefinition = `timestamp(${quote(colName)}, { withTimezone: true, mode: 'string' })`;
193
226
  }
194
227
  // autoValue: database-level default for initial value on INSERT
195
228
  if (dateProp.autoValue === "on_create" || dateProp.autoValue === "on_update") {
@@ -200,12 +233,22 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
200
233
  case "map": {
201
234
  const mapProp = prop as MapProperty;
202
235
  if (mapProp.columnType === "json") {
203
- columnDefinition = `json("${colName}")`;
236
+ columnDefinition = `json(${quote(colName)})`;
204
237
  } else {
205
- columnDefinition = `jsonb("${colName}")`;
238
+ columnDefinition = `jsonb(${quote(colName)})`;
206
239
  }
207
240
  break;
208
241
  }
242
+ case "geopoint": {
243
+ // `{ latitude, longitude }`, which is what the OpenAPI schema, the
244
+ // generated TS type and the admin's field binding all describe.
245
+ // This arm did not exist: `geopoint` fell to `default: return null`
246
+ // and the caller dropped the column, so the DDL generator created a
247
+ // column the Drizzle table had no key for — and every write to it
248
+ // was silently discarded with a 201.
249
+ columnDefinition = `jsonb(${quote(colName)})`;
250
+ break;
251
+ }
209
252
  case "array": {
210
253
  const arrayProp = prop as ArrayProperty;
211
254
  let colType = arrayProp.columnType;
@@ -221,27 +264,27 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
221
264
  }
222
265
 
223
266
  if (colType === "json") {
224
- columnDefinition = `json("${colName}")`;
267
+ columnDefinition = `json(${quote(colName)})`;
225
268
  } else if (colType === "text[]") {
226
- columnDefinition = `text("${colName}").array()`;
269
+ columnDefinition = `text(${quote(colName)}).array()`;
227
270
  } else if (colType === "integer[]") {
228
- columnDefinition = `integer("${colName}").array()`;
271
+ columnDefinition = `integer(${quote(colName)}).array()`;
229
272
  } else if (colType === "boolean[]") {
230
- columnDefinition = `boolean("${colName}").array()`;
273
+ columnDefinition = `boolean(${quote(colName)}).array()`;
231
274
  } else if (colType === "numeric[]") {
232
- columnDefinition = `numeric("${colName}").array()`;
275
+ columnDefinition = `numeric(${quote(colName)}).array()`;
233
276
  } else {
234
- columnDefinition = `jsonb("${colName}")`;
277
+ columnDefinition = `jsonb(${quote(colName)})`;
235
278
  }
236
279
  break;
237
280
  }
238
281
  case "vector": {
239
282
  const vp = prop as VectorProperty;
240
- columnDefinition = `vector("${colName}", { dimensions: ${vp.dimensions} })`;
283
+ columnDefinition = `vector(${quote(colName)}, { dimensions: ${vp.dimensions} })`;
241
284
  break;
242
285
  }
243
286
  case "binary": {
244
- columnDefinition = `customType({ dataType() { return 'bytea'; } })("${colName}")`;
287
+ columnDefinition = `customType({ dataType() { return 'bytea'; } })(${quote(colName)})`;
245
288
  break;
246
289
  }
247
290
  case "relation": {
@@ -282,47 +325,60 @@ const getDrizzleColumn = (propName: string, prop: Property, collection: Collecti
282
325
  const refOptionsParts = [onUpdate, onDelete].filter(Boolean);
283
326
  const refOptions = refOptionsParts.length > 0 ? `{ ${refOptionsParts.join(", ")} }` : "";
284
327
 
285
- let columnDef = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}${refOptions ? `, ${refOptions}` : ""})`;
328
+ let columnDef = `${baseColumn}.references(() => ${member(targetTableVar, targetIdField)}${refOptions ? `, ${refOptions}` : ""})`;
286
329
 
287
330
  if (required) {
288
331
  columnDef += ".notNull()";
289
332
  }
290
333
 
291
- return ` ${relation.localKey}: ${columnDef}`;
334
+ return ` ${propKey(relation.localKey)}: ${columnDef}`;
292
335
  }
293
336
  case "reference": {
294
337
  const refProp = prop as ReferenceProperty;
295
338
  const targetCollection = collections.find(c => c.slug === refProp.path || getTableName(c) === refProp.path);
296
339
  if (!targetCollection) {
297
- columnDefinition = `text("${colName}")`;
340
+ columnDefinition = `text(${quote(colName)})`;
298
341
  break;
299
342
  }
300
343
 
301
344
  const pkProp = getPrimaryKeyProp(targetCollection);
302
345
  const targetTableVar = getTableVarName(getTableName(targetCollection));
303
346
  const targetIdField = pkProp.name;
304
- const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : (pkProp.isUuid ? `uuid("${colName}")` : `text("${colName}")`);
347
+ const baseColumn = pkProp.type === "number" ? `integer(${quote(colName)})` : (pkProp.isUuid ? `uuid(${quote(colName)})` : `text(${quote(colName)})`);
305
348
 
306
349
  const required = prop.validation?.required;
307
350
  const onDelete = required ? "cascade" : "set null";
308
351
  const refOptions = `{ onDelete: "${onDelete}" }`;
309
352
 
310
- columnDefinition = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}, ${refOptions})`;
353
+ columnDefinition = `${baseColumn}.references(() => ${member(targetTableVar, targetIdField)}, ${refOptions})`;
311
354
  if (required) {
312
355
  columnDefinition += ".notNull()";
313
356
  }
314
357
  // Skip the standard notNull() handling below because we did it here with references
315
- return ` ${propName}: ${columnDefinition}`;
358
+ return ` ${propKey(propName)}: ${columnDefinition}`;
316
359
  }
317
360
  default:
318
- return null;
361
+ // Not `return null`. A `null` here means "this property puts no
362
+ // column on this table", which is true of an inverse relation and
363
+ // false of everything else — and the caller cannot tell the two
364
+ // apart, so a type this switch simply forgot produced a table
365
+ // missing a column while the DDL generator happily created one.
366
+ // That is how `geopoint` stayed unpersistable: writes to it were
367
+ // dropped before the SQL was built, forever, with a 201. A property
368
+ // type nobody mapped is a generator bug, and it says so here rather
369
+ // than at some caller's next INSERT.
370
+ throw new Error(
371
+ `No Postgres column mapping for property '${propName}' of type ` +
372
+ `'${(prop as Property).type}' in collection '${collection.slug}'. ` +
373
+ "Add a case to `getDrizzleColumn` (and to `getSqlColumnType`, which must agree)."
374
+ );
319
375
  }
320
376
 
321
377
  if (prop.validation?.required) {
322
378
  columnDefinition += ".notNull()";
323
379
  }
324
380
 
325
- return ` ${propName}: ${columnDefinition}`;
381
+ return ` ${propKey(propName)}: ${columnDefinition}`;
326
382
  };
327
383
 
328
384
  /**
@@ -399,7 +455,7 @@ const generateSinglePolicyCode = (collection: CollectionConfig, rule: SecurityRu
399
455
  if (usingClause) parts.push(`using: ${usingClause}`);
400
456
  if (withCheckClause) parts.push(`withCheck: ${withCheckClause}`);
401
457
 
402
- return ` pgPolicy("${policyName}", { ${parts.join(", ")} }),\n`;
458
+ return ` pgPolicy(${quote(policyName)}, { ${parts.join(", ")} }),\n`;
403
459
  };
404
460
 
405
461
  /**
@@ -451,7 +507,18 @@ const computeSharedRelationName = (
451
507
  };
452
508
 
453
509
  // --- Main Schema Generation Logic ---
454
- export const generateSchema = async (collections: CollectionConfig[], stripPolicies = false): Promise<string> => {
510
+ export const generateSchema = async (allCollections: CollectionConfig[], stripPolicies = false): Promise<string> => {
511
+ // A Firestore or MongoDB collection has no table to generate, and generating
512
+ // one for it is not merely wasted output: `db push` would create it, and
513
+ // `rebase doctor` would then report the store the collection actually reads
514
+ // from as drift. Non-SQL collections leave the toolchain here, once, rather
515
+ // than being filtered again in each stage below.
516
+ //
517
+ // Sorted here rather than in the writer: the output is order-dependent and
518
+ // `rebase doctor` regenerates it in memory to diff against the file on
519
+ // disk. With the sort in the writer only, a project whose file order
520
+ // differed from its slug order was reported stale forever.
521
+ const collections = sortCollectionsBySlug(relationalCollections(allCollections));
455
522
  let schemaContent = "// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\n\n";
456
523
 
457
524
 
@@ -473,11 +540,15 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
473
540
  )
474
541
  );
475
542
 
543
+ // drizzle ships no `tsvector` builder, so an opted-in search column reaches
544
+ // the schema the same way `bytea` does — through `customType`.
545
+ const hasSearch = collections.some(c => buildSearchColumnSpec(c) !== undefined);
546
+
476
547
  // Always import pgPolicy and sql — RLS is enabled on every table (secure by default)
477
548
  const pgCoreImports = ["primaryKey", "pgTable", "integer", "varchar", "text", "char", "boolean", "timestamp", "date", "time", "jsonb", "json", "pgEnum", "numeric", "real", "doublePrecision", "bigint", "serial", "bigserial", "pgPolicy"];
478
549
  if (hasUuid) pgCoreImports.push("uuid");
479
550
  if (hasVector) pgCoreImports.push("vector");
480
- if (hasBinary) pgCoreImports.push("customType");
551
+ if (hasBinary || hasSearch) pgCoreImports.push("customType");
481
552
 
482
553
  const uniqueSchemas = Array.from(new Set(
483
554
  collections.map(c => isPostgresCollectionConfig(c) ? c.schema : undefined).filter(Boolean)
@@ -521,7 +592,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
521
592
  )
522
593
  : Object.keys(prop.enum);
523
594
  if (values.length > 0) {
524
- schemaContent += `export const ${enumVarName} = pgEnum(\"${enumDbName}\", [${values.map(v => `'${v}'`).join(", ")}]);\n`;
595
+ schemaContent += `export const ${enumVarName} = pgEnum(${quote(enumDbName)}, [${values.map(v => quote(v)).join(", ")}]);\n`;
525
596
  if (!exportedEnumVars.includes(enumVarName)) exportedEnumVars.push(enumVarName);
526
597
  }
527
598
  }
@@ -626,6 +697,25 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
626
697
 
627
698
  });
628
699
 
700
+ // The opt-in search column. Declared with its real generation
701
+ // expression rather than as a bare custom type: `schema.generated.ts`
702
+ // is what a developer running drizzle-kit themselves diffs against,
703
+ // and a column declared without its expression reads to drizzle-kit
704
+ // as one it should alter.
705
+ const searchSpec = buildSearchColumnSpec(collection);
706
+ if (searchSpec) {
707
+ columns.add(
708
+ ` ${searchSpec.column}: customType({ dataType() { return 'tsvector'; } })("${searchSpec.column}")` +
709
+ `.generatedAlwaysAs(sql\`${searchSpec.expression}\`)`
710
+ );
711
+ if (searchSpec.fuzzy) {
712
+ columns.add(
713
+ ` ${searchSpec.fuzzy.column}: text("${searchSpec.fuzzy.column}")` +
714
+ `.generatedAlwaysAs(sql\`${searchSpec.fuzzy.expression}\`)`
715
+ );
716
+ }
717
+ }
718
+
629
719
  // Backwards compatibility: if no id/primary key column is found in properties, but `id` wasn't explicitly provided
630
720
  // We should generate a basic id column if one was completely omitted.
631
721
  const hasIdColumn = Array.from(columns).some(col => col.includes(".primaryKey()"));
@@ -698,7 +788,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
698
788
  }
699
789
 
700
790
  // Source side one(): pairs with owning table's many(junctionTable, { relationName })
701
- tableRelations.push(` "${relation.through.sourceColumn}": one(${sourceTableVar}, {\n fields: [${tableVarName}.${relation.through.sourceColumn}],\n references: [${sourceTableVar}.${sourceId}],\n relationName: \"${owningRelationName}\"\n })`);
791
+ tableRelations.push(` ${quote(relation.through.sourceColumn)}: one(${sourceTableVar}, {\n fields: [${member(tableVarName, relation.through.sourceColumn)}],\n references: [${member(sourceTableVar, sourceId)}],\n relationName: ${quote(owningRelationName)}\n })`);
702
792
 
703
793
  // Target side one(): pairs with inverse table's many(junctionTable, { relationName })
704
794
  // Always emit a relationName to avoid collisions with the source-side's owningRelationName.
@@ -706,7 +796,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
706
796
  const targetRelationName = inverseRelationName
707
797
  ? inverseRelationName
708
798
  : `${tableName}_${relation.through.targetColumn}`;
709
- tableRelations.push(` "${relation.through.targetColumn}": one(${targetTableVar}, {\n fields: [${tableVarName}.${relation.through.targetColumn}],\n references: [${targetTableVar}.${targetId}],\n relationName: "${targetRelationName}"\n })`);
799
+ tableRelations.push(` ${quote(relation.through.targetColumn)}: one(${targetTableVar}, {\n fields: [${member(tableVarName, relation.through.targetColumn)}],\n references: [${member(targetTableVar, targetId)}],\n relationName: ${quote(targetRelationName)}\n })`);
710
800
  }
711
801
  } else {
712
802
  const resolvedRelations = resolveCollectionRelations(collection);
@@ -746,7 +836,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
746
836
  // schema that does not compile. The three other
747
837
  // emission sites normalise; this one did not.
748
838
  const localFieldKey = resolvePropertyKeyForColumn(collection, rel.localKey);
749
- tableRelations.push(` "${relationKey}": one(${targetTableVar}, {\n fields: [${tableVarName}.${localFieldKey}],\n references: [${targetTableVar}.${getPrimaryKeyName(target)}],\n relationName: \"${drizzleRelationName}\"\n })`);
839
+ tableRelations.push(` ${quote(relationKey)}: one(${targetTableVar}, {\n fields: [${member(tableVarName, localFieldKey)}],\n references: [${member(targetTableVar, getPrimaryKeyName(target))}],\n relationName: ${quote(drizzleRelationName)}\n })`);
750
840
  break;
751
841
  }
752
842
 
@@ -811,7 +901,7 @@ export const generateSchema = async (collections: CollectionConfig[], stripPolic
811
901
  ? resolvePropertyKeyForColumn(otherCollection, otherRel.sourceKey)
812
902
  : getPrimaryKeyName(otherCollection);
813
903
  const synthKey = `_synth_${otherTableVar}_${drizzleFieldKey}`;
814
- tableRelations.push(` "${synthKey}": one(${otherTableVar}, {\n fields: [${tableVarName}.${drizzleFieldKey}],\n references: [${otherTableVar}.${referencedKey}],\n relationName: \"${drizzleRelationName}\"\n })`);
904
+ tableRelations.push(` ${quote(synthKey)}: one(${otherTableVar}, {\n fields: [${member(tableVarName, drizzleFieldKey)}],\n references: [${member(otherTableVar, referencedKey)}],\n relationName: ${quote(drizzleRelationName)}\n })`);
815
905
  emittedRelationNames.add(deduplicationKey);
816
906
  }
817
907
  }