turbine-orm 0.27.0 → 0.28.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 (52) hide show
  1. package/README.md +17 -13
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/destructive.js +47 -31
  4. package/dist/cjs/cli/index.js +273 -71
  5. package/dist/cjs/cli/mcp.js +788 -0
  6. package/dist/cjs/cli/migrate.js +95 -20
  7. package/dist/cjs/cli/studio.js +3 -2
  8. package/dist/cjs/client.js +267 -34
  9. package/dist/cjs/dialect.js +2 -0
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/query/batched-loader.js +148 -0
  14. package/dist/cjs/query/builder.js +714 -133
  15. package/dist/cjs/schema-builder.js +59 -4
  16. package/dist/cjs/schema-sql.js +315 -6
  17. package/dist/cjs/seed.js +66 -0
  18. package/dist/cli/config.d.ts +9 -2
  19. package/dist/cli/config.js +19 -3
  20. package/dist/cli/destructive.js +47 -31
  21. package/dist/cli/index.d.ts +52 -1
  22. package/dist/cli/index.js +272 -74
  23. package/dist/cli/mcp.d.ts +17 -0
  24. package/dist/cli/mcp.js +781 -0
  25. package/dist/cli/migrate.d.ts +37 -0
  26. package/dist/cli/migrate.js +92 -20
  27. package/dist/cli/studio.d.ts +3 -2
  28. package/dist/cli/studio.js +3 -2
  29. package/dist/client.d.ts +136 -1
  30. package/dist/client.js +267 -34
  31. package/dist/dialect.d.ts +17 -0
  32. package/dist/dialect.js +2 -0
  33. package/dist/generate.d.ts +17 -0
  34. package/dist/generate.js +171 -10
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -0
  37. package/dist/introspect.d.ts +20 -1
  38. package/dist/introspect.js +175 -4
  39. package/dist/query/batched-loader.d.ts +29 -2
  40. package/dist/query/batched-loader.js +148 -1
  41. package/dist/query/builder.d.ts +156 -8
  42. package/dist/query/builder.js +715 -134
  43. package/dist/query/index.d.ts +1 -1
  44. package/dist/query/types.d.ts +113 -8
  45. package/dist/schema-builder.d.ts +73 -8
  46. package/dist/schema-builder.js +59 -4
  47. package/dist/schema-sql.d.ts +67 -0
  48. package/dist/schema-sql.js +310 -6
  49. package/dist/schema.d.ts +53 -0
  50. package/dist/seed.d.ts +4 -0
  51. package/dist/seed.js +63 -0
  52. package/package.json +2 -3
@@ -12,6 +12,9 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.generate = generate;
14
14
  exports.generateTypes = generateTypes;
15
+ exports.generateZod = generateZod;
16
+ exports.generateMetadata = generateMetadata;
17
+ exports.generateIndex = generateIndex;
15
18
  const node_fs_1 = require("node:fs");
16
19
  const node_path_1 = require("node:path");
17
20
  const schema_js_1 = require("./schema.js");
@@ -19,6 +22,24 @@ const schema_js_1 = require("./schema.js");
19
22
  function entityName(tableName) {
20
23
  return (0, schema_js_1.snakeToPascal)((0, schema_js_1.singularize)(tableName));
21
24
  }
25
+ /**
26
+ * Resolve the TypeScript type for a column, mapping enum-typed columns to their
27
+ * generated string-literal union (PascalCase enum name) instead of the
28
+ * `unknown` that {@link pgTypeToTs} yields for user-defined types. Falls back to
29
+ * the introspected `col.tsType` for every non-enum column.
30
+ */
31
+ function columnTsType(col, enums) {
32
+ const dt = col.dialectType ?? col.pgType;
33
+ const isArray = col.isArray || dt.startsWith('_');
34
+ const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
35
+ if (Object.hasOwn(enums, base)) {
36
+ let t = (0, schema_js_1.snakeToPascal)(base);
37
+ if (isArray)
38
+ t += '[]';
39
+ return col.nullable ? `${t} | null` : t;
40
+ }
41
+ return col.tsType;
42
+ }
22
43
  /** Escape a value for embedding in a single-quoted TypeScript string literal */
23
44
  function escSQ(value) {
24
45
  return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
@@ -48,6 +69,12 @@ function generate(options) {
48
69
  const indexContent = generateIndex(options.schema);
49
70
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'index.ts'), indexContent, 'utf-8');
50
71
  files.push('index.ts');
72
+ // Generate zod.ts (optional — --zod flag)
73
+ if (options.zod) {
74
+ const zodContent = generateZod(options.schema);
75
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'zod.ts'), zodContent, 'utf-8');
76
+ files.push('zod.ts');
77
+ }
51
78
  return { outDir, files };
52
79
  }
53
80
  // ---------------------------------------------------------------------------
@@ -108,7 +135,7 @@ function generateTypes(schema) {
108
135
  const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
109
136
  const nullNote = col.nullable ? ' (nullable)' : '';
110
137
  lines.push(` /** Column: ${col.name} — ${col.pgType}${pkNote}${nullNote} */`);
111
- lines.push(` ${col.field}: ${col.tsType};`);
138
+ lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
112
139
  }
113
140
  lines.push('}');
114
141
  lines.push('');
@@ -118,15 +145,18 @@ function generateTypes(schema) {
118
145
  lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
119
146
  lines.push(`export type ${typeName}Create = {`);
120
147
  for (const col of table.columns) {
148
+ // STORED generated columns are computed by the database — never writable.
149
+ if (col.isGeneratedStored)
150
+ continue;
121
151
  const isPk = table.primaryKey.includes(col.name);
122
152
  const isOptional = col.hasDefault || col.nullable || isPk;
123
153
  if (isOptional) {
124
154
  const reason = isPk ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
125
155
  lines.push(` /** Optional: ${reason} */`);
126
- lines.push(` ${col.field}?: ${col.tsType};`);
156
+ lines.push(` ${col.field}?: ${columnTsType(col, schema.enums)};`);
127
157
  }
128
158
  else {
129
- lines.push(` ${col.field}: ${col.tsType};`);
159
+ lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
130
160
  }
131
161
  }
132
162
  lines.push('};');
@@ -134,11 +164,11 @@ function generateTypes(schema) {
134
164
  // --- Update input type (all fields optional except PK) ---
135
165
  // Numeric columns additionally accept `UpdateOperatorInput<number>` so
136
166
  // users can write `{ viewCount: { increment: 1 } }` without an `as any`.
137
- const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
167
+ const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
138
168
  lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
139
169
  lines.push(`export type ${typeName}Update = {`);
140
170
  for (const col of nonPkCols) {
141
- lines.push(` ${col.field}?: ${updateFieldType(col.tsType)};`);
171
+ lines.push(` ${col.field}?: ${updateFieldType(columnTsType(col, schema.enums))};`);
142
172
  }
143
173
  lines.push('};');
144
174
  lines.push('');
@@ -271,6 +301,119 @@ function generateTypes(schema) {
271
301
  return lines.join('\n');
272
302
  }
273
303
  // ---------------------------------------------------------------------------
304
+ // zod.ts generator (H1 — `turbine generate --zod`)
305
+ // ---------------------------------------------------------------------------
306
+ /**
307
+ * Map a TypeScript primitive (as produced by {@link pgTypeToTs}) to its Zod
308
+ * expression. `Date` uses `z.coerce.date()` — the generated schemas double as
309
+ * request-body validators where dates arrive as ISO strings, and coercion keeps
310
+ * both a `Date` and a valid date-string acceptable (documented decision).
311
+ */
312
+ function zodScalar(ts) {
313
+ switch (ts) {
314
+ case 'number':
315
+ return 'z.number()';
316
+ case 'string':
317
+ return 'z.string()';
318
+ case 'boolean':
319
+ return 'z.boolean()';
320
+ case 'Date':
321
+ return 'z.coerce.date()';
322
+ case 'bigint':
323
+ return 'z.bigint()';
324
+ case 'Buffer':
325
+ return 'z.instanceof(Uint8Array)';
326
+ case 'number[]':
327
+ // pgvector — `pgTypeToTs('vector')` yields `number[]`.
328
+ return 'z.array(z.number())';
329
+ default:
330
+ // json/jsonb and any unmapped user-defined type.
331
+ return 'z.unknown()';
332
+ }
333
+ }
334
+ /**
335
+ * Base Zod expression for a column, resolving enums → `z.enum([...])`, arrays →
336
+ * `.array()`, and vectors → `z.array(z.number())`. Does NOT append
337
+ * `.nullable()` / `.optional()` — callers layer those on per-schema.
338
+ */
339
+ function zodBaseType(col, enums) {
340
+ const dt = col.dialectType ?? col.pgType;
341
+ const isArray = col.isArray || dt.startsWith('_');
342
+ const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
343
+ let expr;
344
+ if (Object.hasOwn(enums, base)) {
345
+ expr = `z.enum([${enums[base].map((l) => `'${escSQ(l)}'`).join(', ')}])`;
346
+ }
347
+ else {
348
+ expr = zodScalar((0, schema_js_1.pgTypeToTs)(base, false));
349
+ }
350
+ if (isArray)
351
+ expr += '.array()';
352
+ return expr;
353
+ }
354
+ /**
355
+ * Generate the contents of `zod.ts`. Emits, per table, `XSchema` (the full
356
+ * row), `XCreateSchema` (PK/defaulted/nullable columns optional, STORED
357
+ * generated columns omitted), and `XUpdateSchema` (PK + STORED generated
358
+ * columns omitted, every remaining column optional). Exported so tests can pin
359
+ * the output without writing files.
360
+ */
361
+ function generateZod(schema) {
362
+ const lines = [...generatedFileHeader()];
363
+ // `zod` is a USER dependency — this generated file imports it, but the Turbine
364
+ // library runtime never does, so Zod stays out of the package's dep graph.
365
+ lines.push("import { z } from 'zod';");
366
+ lines.push('');
367
+ for (const table of Object.values(schema.tables)) {
368
+ const typeName = entityName(table.name);
369
+ // Full-row schema.
370
+ lines.push(`/** Zod schema for a \`${table.name}\` row */`);
371
+ lines.push(`export const ${typeName}Schema = z.object({`);
372
+ for (const col of table.columns) {
373
+ let expr = zodBaseType(col, schema.enums);
374
+ if (col.nullable)
375
+ expr += '.nullable()';
376
+ lines.push(` ${col.field}: ${expr},`);
377
+ }
378
+ lines.push('});');
379
+ lines.push('');
380
+ // Create schema — STORED generated columns can never be written; PK,
381
+ // defaulted, and nullable columns are optional.
382
+ lines.push(`/** Zod schema for creating a \`${table.name}\` row */`);
383
+ lines.push(`export const ${typeName}CreateSchema = z.object({`);
384
+ for (const col of table.columns) {
385
+ if (col.isGeneratedStored)
386
+ continue;
387
+ const isPk = table.primaryKey.includes(col.name);
388
+ let expr = zodBaseType(col, schema.enums);
389
+ if (col.nullable)
390
+ expr += '.nullable()';
391
+ if (col.hasDefault || col.nullable || isPk)
392
+ expr += '.optional()';
393
+ lines.push(` ${col.field}: ${expr},`);
394
+ }
395
+ lines.push('});');
396
+ lines.push('');
397
+ // Update schema — PK and STORED generated columns omitted; all else optional.
398
+ lines.push(`/** Zod schema for updating a \`${table.name}\` row */`);
399
+ lines.push(`export const ${typeName}UpdateSchema = z.object({`);
400
+ for (const col of table.columns) {
401
+ if (col.isGeneratedStored)
402
+ continue;
403
+ if (table.primaryKey.includes(col.name))
404
+ continue;
405
+ let expr = zodBaseType(col, schema.enums);
406
+ if (col.nullable)
407
+ expr += '.nullable()';
408
+ expr += '.optional()';
409
+ lines.push(` ${col.field}: ${expr},`);
410
+ }
411
+ lines.push('});');
412
+ lines.push('');
413
+ }
414
+ return lines.join('\n');
415
+ }
416
+ // ---------------------------------------------------------------------------
274
417
  // metadata.ts generator
275
418
  // ---------------------------------------------------------------------------
276
419
  function generateMetadata(schema) {
@@ -352,6 +495,9 @@ function generateMetadata(schema) {
352
495
  lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
353
496
  }
354
497
  lines.push(' ],');
498
+ // isView — read-only marker; the runtime write guard reads it.
499
+ if (table.isView)
500
+ lines.push(' isView: true,');
355
501
  lines.push(' },');
356
502
  }
357
503
  lines.push(' },');
@@ -409,7 +555,7 @@ function generateIndex(schema) {
409
555
  const hasRelations = Object.keys(table.relations).length > 0;
410
556
  const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
411
557
  lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
412
- lines.push(` declare readonly ${accessor}: QueryInterface<${genericArgs}>;`);
558
+ lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
413
559
  }
414
560
  lines.push('}');
415
561
  lines.push('');
@@ -451,7 +597,7 @@ function generateIndex(schema) {
451
597
  const hasRelations = Object.keys(table.relations).length > 0;
452
598
  const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
453
599
  lines.push(` /** Query interface for the \`${table.name}\` table */`);
454
- lines.push(` declare readonly ${accessor}: QueryInterface<${genericArgs}>;`);
600
+ lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
455
601
  }
456
602
  lines.push('');
457
603
  lines.push(' constructor(config?: TurbineConfig) {');
@@ -495,6 +641,18 @@ function generateIndex(schema) {
495
641
  // ---------------------------------------------------------------------------
496
642
  // Helpers
497
643
  // ---------------------------------------------------------------------------
644
+ /**
645
+ * The generated table-accessor type. A view (`isView`) without a primary key
646
+ * cannot be looked up by unique key, so its `findUnique`-family methods are
647
+ * excluded via `Omit`. Everything else is a plain `QueryInterface<…>`.
648
+ */
649
+ function accessorType(table, genericArgs) {
650
+ const base = `QueryInterface<${genericArgs}>`;
651
+ if (table.isView && table.primaryKey.length === 0) {
652
+ return `Omit<${base}, 'findUnique' | 'findUniqueOrThrow'>`;
653
+ }
654
+ return base;
655
+ }
498
656
  function serializeColumn(col) {
499
657
  const parts = [
500
658
  `name: '${escSQ(col.name)}'`,
@@ -512,6 +670,12 @@ function serializeColumn(col) {
512
670
  // output stays byte-identical for the common client-default columns.
513
671
  if (col.isGenerated)
514
672
  parts.push(`isGenerated: true`);
673
+ // STORED generated columns — the runtime write guard reads isGeneratedStored.
674
+ if (col.isGeneratedStored)
675
+ parts.push(`isGeneratedStored: true`);
676
+ if (col.generationExpression !== undefined) {
677
+ parts.push(`generationExpression: '${escSQ(col.generationExpression)}'`);
678
+ }
515
679
  if (col.maxLength !== undefined)
516
680
  parts.push(`maxLength: ${col.maxLength}`);
517
681
  return `{ ${parts.join(', ')} }`;
package/dist/cjs/index.js CHANGED
@@ -35,7 +35,7 @@
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.ColumnBuilder = exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
38
- exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = void 0;
38
+ exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = void 0;
39
39
  var index_js_1 = require("./adapters/index.js");
40
40
  Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
41
41
  Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
@@ -118,6 +118,9 @@ Object.defineProperty(exports, "schemaDiff", { enumerable: true, get: function (
118
118
  Object.defineProperty(exports, "schemaPush", { enumerable: true, get: function () { return schema_sql_js_1.schemaPush; } });
119
119
  Object.defineProperty(exports, "schemaToSQL", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQL; } });
120
120
  Object.defineProperty(exports, "schemaToSQLString", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQLString; } });
121
+ // Seed helper
122
+ var seed_js_1 = require("./seed.js");
123
+ Object.defineProperty(exports, "defineSeed", { enumerable: true, get: function () { return seed_js_1.defineSeed; } });
121
124
  // Serverless / edge factory
122
125
  var serverless_js_1 = require("./serverless.js");
123
126
  Object.defineProperty(exports, "turbineHttp", { enumerable: true, get: function () { return serverless_js_1.turbineHttp; } });
@@ -12,11 +12,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.pgConfActionToReferential = pgConfActionToReferential;
15
16
  exports.introspect = introspect;
16
17
  exports.introspectPostgresCatalog = introspectPostgresCatalog;
18
+ exports.stripCheckWrapper = stripCheckWrapper;
17
19
  const pg_1 = __importDefault(require("pg"));
18
20
  const dialect_js_1 = require("./dialect.js");
19
21
  const schema_js_1 = require("./schema.js");
22
+ /**
23
+ * Map a `pg_constraint.confdeltype` / `confupdtype` character to a
24
+ * {@link ReferentialAction}. Postgres encodes: `a` = NO ACTION, `r` = RESTRICT,
25
+ * `c` = CASCADE, `n` = SET NULL, `d` = SET DEFAULT.
26
+ */
27
+ function pgConfActionToReferential(ch) {
28
+ switch (ch) {
29
+ case 'c':
30
+ return 'cascade';
31
+ case 'r':
32
+ return 'restrict';
33
+ case 'n':
34
+ return 'set null';
35
+ case 'd':
36
+ return 'set default';
37
+ default:
38
+ return 'no action';
39
+ }
40
+ }
20
41
  // ---------------------------------------------------------------------------
21
42
  // SQL queries (all parameterized, no interpolation)
22
43
  // ---------------------------------------------------------------------------
@@ -36,6 +57,8 @@ const SQL_COLUMNS = `
36
57
  is_nullable,
37
58
  column_default,
38
59
  is_identity,
60
+ is_generated,
61
+ generation_expression,
39
62
  ordinal_position,
40
63
  character_maximum_length
41
64
  FROM information_schema.columns
@@ -91,6 +114,65 @@ const SQL_INDEXES = `
91
114
  FROM pg_indexes
92
115
  WHERE schemaname = $1
93
116
  `;
117
+ // Foreign-key referential actions (ON DELETE / ON UPDATE) live in pg_catalog,
118
+ // not information_schema. Keyed by constraint name for join with SQL_FOREIGN_KEYS.
119
+ const SQL_FK_ACTIONS = `
120
+ SELECT con.conname, con.confdeltype, con.confupdtype
121
+ FROM pg_constraint con
122
+ JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
123
+ WHERE con.contype = 'f'
124
+ AND n.nspname = $1
125
+ `;
126
+ // CHECK constraints (contype = 'c'). NOT NULL is stored as attnotnull, not a
127
+ // check constraint, so it never appears here.
128
+ const SQL_CHECKS = `
129
+ SELECT rel.relname AS table_name, con.conname, pg_get_constraintdef(con.oid) AS definition
130
+ FROM pg_constraint con
131
+ JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
132
+ JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
133
+ WHERE con.contype = 'c'
134
+ AND n.nspname = $1
135
+ `;
136
+ // Views (relkind 'v') — column metadata comes free from information_schema.columns.
137
+ const SQL_VIEWS = `
138
+ SELECT table_name
139
+ FROM information_schema.views
140
+ WHERE table_schema = $1
141
+ ORDER BY table_name
142
+ `;
143
+ // Materialized views (relkind 'm') — NOT in information_schema; read from pg_catalog.
144
+ const SQL_MATVIEWS = `
145
+ SELECT matviewname AS table_name
146
+ FROM pg_matviews
147
+ WHERE schemaname = $1
148
+ ORDER BY matviewname
149
+ `;
150
+ // Materialized-view columns — information_schema.columns omits matviews, so pull
151
+ // them from pg_attribute. Aliased to mirror SQL_COLUMNS so the same row-mapping
152
+ // applies (array types surface as data_type 'ARRAY' + a '_'-prefixed udt_name).
153
+ const SQL_MATVIEW_COLUMNS = `
154
+ SELECT
155
+ c.relname AS table_name,
156
+ a.attname AS column_name,
157
+ t.typname AS udt_name,
158
+ CASE WHEN t.typcategory = 'A' THEN 'ARRAY' ELSE 'base' END AS data_type,
159
+ CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
160
+ NULL AS column_default,
161
+ 'NO' AS is_identity,
162
+ 'NEVER' AS is_generated,
163
+ NULL AS generation_expression,
164
+ a.attnum AS ordinal_position,
165
+ NULL::int AS character_maximum_length
166
+ FROM pg_catalog.pg_attribute a
167
+ JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
168
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
169
+ JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
170
+ WHERE n.nspname = $1
171
+ AND c.relkind = 'm'
172
+ AND a.attnum > 0
173
+ AND NOT a.attisdropped
174
+ ORDER BY c.relname, a.attnum
175
+ `;
94
176
  const SQL_ENUMS = `
95
177
  SELECT t.typname, e.enumlabel
96
178
  FROM pg_type t
@@ -131,17 +213,45 @@ async function introspectPostgresCatalog(options) {
131
213
  });
132
214
  try {
133
215
  // Run all information_schema queries in parallel
134
- const [tablesResult, columnsResult, pkResult, fkResult, uniqueResult, indexResult, enumResult] = await Promise.all([
216
+ const [tablesResult, columnsResult, pkResult, fkResult, fkActionsResult, uniqueResult, indexResult, checkResult, enumResult,] = await Promise.all([
135
217
  pool.query(SQL_TABLES, [schema]),
136
218
  pool.query(SQL_COLUMNS, [schema]),
137
219
  pool.query(SQL_PRIMARY_KEYS, [schema]),
138
220
  pool.query(SQL_FOREIGN_KEYS, [schema]),
221
+ pool.query(SQL_FK_ACTIONS, [schema]),
139
222
  pool.query(SQL_UNIQUE_CONSTRAINTS, [schema]),
140
223
  pool.query(SQL_INDEXES, [schema]),
224
+ pool.query(SQL_CHECKS, [schema]),
141
225
  pool.query(SQL_ENUMS, [schema]),
142
226
  ]);
143
- // Filter tables by include/exclude
144
- let tableNames = tablesResult.rows.map((r) => r.table_name);
227
+ // Views + materialized views (opt-in). Regular-view columns are already in
228
+ // columnsResult (information_schema.columns); matview columns need a separate
229
+ // pg_catalog read, which we splice into the column rows below.
230
+ const viewNameSet = new Set();
231
+ const matviewColumnRows = [];
232
+ if (options.includeViews) {
233
+ const [viewsResult, matviewsResult, matviewColsResult] = await Promise.all([
234
+ pool.query(SQL_VIEWS, [schema]),
235
+ pool.query(SQL_MATVIEWS, [schema]),
236
+ pool.query(SQL_MATVIEW_COLUMNS, [schema]),
237
+ ]);
238
+ for (const r of viewsResult.rows)
239
+ viewNameSet.add(r.table_name);
240
+ for (const r of matviewsResult.rows)
241
+ viewNameSet.add(r.table_name);
242
+ matviewColumnRows.push(...matviewColsResult.rows);
243
+ }
244
+ // constraint_name → { onDelete, onUpdate } referential actions.
245
+ const fkActions = new Map();
246
+ for (const row of fkActionsResult.rows) {
247
+ fkActions.set(row.conname, {
248
+ onDelete: pgConfActionToReferential(row.confdeltype),
249
+ onUpdate: pgConfActionToReferential(row.confupdtype),
250
+ });
251
+ }
252
+ // Filter tables by include/exclude. Views/matviews join the base tables as
253
+ // candidates so include/exclude apply uniformly.
254
+ let tableNames = [...tablesResult.rows.map((r) => r.table_name), ...viewNameSet];
145
255
  if (options.include?.length) {
146
256
  const includeSet = new Set(options.include);
147
257
  tableNames = tableNames.filter((t) => includeSet.has(t));
@@ -152,8 +262,10 @@ async function introspectPostgresCatalog(options) {
152
262
  }
153
263
  const tableSet = new Set(tableNames);
154
264
  // ----- Group columns by table -----
265
+ // Base-table + regular-view columns come from information_schema.columns;
266
+ // materialized-view columns are appended from the pg_catalog read.
155
267
  const columnsByTable = new Map();
156
- for (const row of columnsResult.rows) {
268
+ for (const row of [...columnsResult.rows, ...matviewColumnRows]) {
157
269
  const tableName = row.table_name;
158
270
  if (!tableSet.has(tableName))
159
271
  continue;
@@ -176,6 +288,11 @@ async function introspectPostgresCatalog(options) {
176
288
  // (gen_random_uuid(), now()), which Turbine must still synthesize.
177
289
  isGenerated: (typeof row.column_default === 'string' && row.column_default.includes('nextval(')) ||
178
290
  row.is_identity === 'YES',
291
+ // GENERATED ALWAYS AS (expr) STORED — computed by the database, never
292
+ // writable. Distinct from isGenerated (serial/identity, which a client
293
+ // MAY override). is_generated is 'ALWAYS' for STORED columns, else 'NEVER'.
294
+ isGeneratedStored: row.is_generated === 'ALWAYS',
295
+ generationExpression: row.is_generated === 'ALWAYS' && row.generation_expression ? row.generation_expression : undefined,
179
296
  isArray,
180
297
  arrayType,
181
298
  pgArrayType: arrayType,
@@ -230,6 +347,20 @@ async function introspectPostgresCatalog(options) {
230
347
  definition: row.indexdef,
231
348
  });
232
349
  }
350
+ // ----- Group check constraints by table -----
351
+ // pg_get_constraintdef yields e.g. `CHECK ((price >= 0))`; strip the leading
352
+ // `CHECK ` and the outermost paren pair to recover the raw expression.
353
+ const checksByTable = new Map();
354
+ for (const row of checkResult.rows) {
355
+ if (!tableSet.has(row.table_name))
356
+ continue;
357
+ if (!checksByTable.has(row.table_name))
358
+ checksByTable.set(row.table_name, []);
359
+ checksByTable.get(row.table_name).push({
360
+ name: row.conname,
361
+ expression: stripCheckWrapper(row.definition),
362
+ });
363
+ }
233
364
  // ----- Collect enums -----
234
365
  const enums = {};
235
366
  for (const row of enumResult.rows) {
@@ -279,6 +410,13 @@ async function introspectPostgresCatalog(options) {
279
410
  ? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
280
411
  : (0, schema_js_1.snakeToCamel)(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
281
412
  : (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
413
+ // Referential actions (omit the 'no action' default to keep metadata lean).
414
+ const actions = fkActions.get(fk.constraintName);
415
+ const actionFields = {};
416
+ if (actions?.onDelete && actions.onDelete !== 'no action')
417
+ actionFields.onDelete = actions.onDelete;
418
+ if (actions?.onUpdate && actions.onUpdate !== 'no action')
419
+ actionFields.onUpdate = actions.onUpdate;
282
420
  if (!relationsByTable.has(fk.sourceTable))
283
421
  relationsByTable.set(fk.sourceTable, {});
284
422
  relationsByTable.get(fk.sourceTable)[belongsToName] = {
@@ -288,6 +426,7 @@ async function introspectPostgresCatalog(options) {
288
426
  to: fk.targetTable,
289
427
  foreignKey,
290
428
  referenceKey,
429
+ ...actionFields,
291
430
  };
292
431
  // --- hasMany on the target (parent) table ---
293
432
  // e.g. posts.user_id → users.id creates users.posts (hasMany)
@@ -305,6 +444,7 @@ async function introspectPostgresCatalog(options) {
305
444
  to: fk.sourceTable,
306
445
  foreignKey,
307
446
  referenceKey,
447
+ ...actionFields,
308
448
  };
309
449
  }
310
450
  // ----- Conservative many-to-many auto-detection (PURELY ADDITIVE) -----
@@ -422,6 +562,8 @@ async function introspectPostgresCatalog(options) {
422
562
  uniqueColumns: uniqueByTable.get(tableName) ?? [],
423
563
  relations: relationsByTable.get(tableName) ?? {},
424
564
  indexes: indexesByTable.get(tableName) ?? [],
565
+ checks: checksByTable.get(tableName) ?? [],
566
+ ...(viewNameSet.has(tableName) ? { isView: true } : {}),
425
567
  };
426
568
  }
427
569
  return { tables, enums };
@@ -430,3 +572,34 @@ async function introspectPostgresCatalog(options) {
430
572
  await pool.end();
431
573
  }
432
574
  }
575
+ /**
576
+ * Recover the raw check expression from `pg_get_constraintdef` output, which
577
+ * wraps it as `CHECK ((expr))`. Strips the leading `CHECK ` keyword and one
578
+ * balanced outer paren pair; leaves anything unexpected untouched.
579
+ */
580
+ function stripCheckWrapper(def) {
581
+ let s = def.trim();
582
+ const m = /^CHECK\s*\((.*)\)$/is.exec(s);
583
+ if (m)
584
+ s = m[1].trim();
585
+ // pg double-wraps single expressions: `(price >= 0)` → unwrap one more pair
586
+ // only when the parens are balanced across the whole string.
587
+ if (s.startsWith('(') && s.endsWith(')')) {
588
+ let depth = 0;
589
+ let balanced = true;
590
+ for (let i = 0; i < s.length; i++) {
591
+ if (s[i] === '(')
592
+ depth++;
593
+ else if (s[i] === ')') {
594
+ depth--;
595
+ if (depth === 0 && i < s.length - 1) {
596
+ balanced = false;
597
+ break;
598
+ }
599
+ }
600
+ }
601
+ if (balanced)
602
+ s = s.slice(1, -1).trim();
603
+ }
604
+ return s;
605
+ }