turbine-orm 0.4.0 → 0.7.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 (57) hide show
  1. package/README.md +243 -26
  2. package/dist/cjs/cli/config.js +151 -0
  3. package/dist/cjs/cli/index.js +1176 -0
  4. package/dist/cjs/cli/migrate.js +446 -0
  5. package/dist/cjs/cli/ui.js +233 -0
  6. package/dist/cjs/client.js +512 -0
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +321 -0
  9. package/dist/cjs/index.js +94 -0
  10. package/dist/cjs/introspect.js +287 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pipeline.js +78 -0
  13. package/dist/cjs/query.js +1891 -0
  14. package/dist/cjs/schema-builder.js +238 -0
  15. package/dist/cjs/schema-sql.js +509 -0
  16. package/dist/cjs/schema.js +140 -0
  17. package/dist/cjs/serverless.js +110 -0
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +256 -49
  20. package/dist/cli/migrate.d.ts +35 -6
  21. package/dist/cli/migrate.js +124 -76
  22. package/dist/cli/ui.js +5 -9
  23. package/dist/client.d.ts +87 -3
  24. package/dist/client.js +122 -46
  25. package/dist/errors.d.ts +138 -0
  26. package/dist/errors.js +278 -0
  27. package/dist/generate.js +37 -11
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +15 -11
  30. package/dist/introspect.js +3 -5
  31. package/dist/pipeline.js +8 -1
  32. package/dist/query.d.ts +310 -45
  33. package/dist/query.js +565 -237
  34. package/dist/schema-builder.js +91 -23
  35. package/dist/schema-sql.d.ts +6 -2
  36. package/dist/schema-sql.js +180 -26
  37. package/dist/schema.js +4 -1
  38. package/dist/serverless.d.ts +91 -139
  39. package/dist/serverless.js +86 -173
  40. package/package.json +44 -21
  41. package/dist/cli/config.d.ts.map +0 -1
  42. package/dist/cli/index.d.ts.map +0 -1
  43. package/dist/cli/migrate.d.ts.map +0 -1
  44. package/dist/cli/ui.d.ts.map +0 -1
  45. package/dist/client.d.ts.map +0 -1
  46. package/dist/generate.d.ts.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/introspect.d.ts.map +0 -1
  49. package/dist/pipeline.d.ts.map +0 -1
  50. package/dist/query.d.ts.map +0 -1
  51. package/dist/schema-builder.d.ts.map +0 -1
  52. package/dist/schema-sql.d.ts.map +0 -1
  53. package/dist/schema.d.ts.map +0 -1
  54. package/dist/serverless.d.ts.map +0 -1
  55. package/dist/types.d.ts +0 -93
  56. package/dist/types.d.ts.map +0 -1
  57. package/dist/types.js +0 -126
@@ -0,0 +1,509 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm — Schema SQL Generator
4
+ *
5
+ * Converts a SchemaDef (from defineSchema) into executable DDL statements.
6
+ * Also provides diff and push commands for syncing schema to a live database.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.schemaToSQL = schemaToSQL;
13
+ exports.schemaDiff = schemaDiff;
14
+ exports.schemaPush = schemaPush;
15
+ exports.schemaToSQLString = schemaToSQLString;
16
+ const pg_1 = __importDefault(require("pg"));
17
+ const query_js_1 = require("./query.js");
18
+ const schema_js_1 = require("./schema.js");
19
+ // ---------------------------------------------------------------------------
20
+ // SQL Generation — SchemaDef → CREATE TABLE statements
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Convert a SchemaDef into an ordered array of SQL DDL statements.
24
+ *
25
+ * Returns CREATE TABLE statements (in dependency order based on references)
26
+ * followed by CREATE INDEX statements for foreign key columns.
27
+ */
28
+ function schemaToSQL(schema) {
29
+ const statements = [];
30
+ // Topologically sort tables by their foreign key references
31
+ const sorted = topologicalSort(schema);
32
+ // Generate CREATE TABLE statements
33
+ for (const tableName of sorted) {
34
+ const table = schema.tables[tableName];
35
+ statements.push(generateCreateTable(table));
36
+ }
37
+ // Generate CREATE INDEX for foreign key columns
38
+ for (const tableName of sorted) {
39
+ const table = schema.tables[tableName];
40
+ const indexes = generateForeignKeyIndexes(table);
41
+ statements.push(...indexes);
42
+ }
43
+ return statements;
44
+ }
45
+ /**
46
+ * Topologically sort tables so that referenced tables come before referencing ones.
47
+ * Falls back to input order for tables with no dependency ordering.
48
+ */
49
+ function topologicalSort(schema) {
50
+ const tableNames = Object.keys(schema.tables);
51
+ const resolved = new Set();
52
+ const result = [];
53
+ const visiting = new Set();
54
+ function visit(name) {
55
+ if (resolved.has(name))
56
+ return;
57
+ if (visiting.has(name)) {
58
+ // Circular reference — just add it
59
+ return;
60
+ }
61
+ visiting.add(name);
62
+ const table = schema.tables[name];
63
+ if (table) {
64
+ // Visit all tables this table references
65
+ for (const col of Object.values(table.columns)) {
66
+ if (col.referencesTarget) {
67
+ const refTable = col.referencesTarget.split('.')[0];
68
+ if (refTable !== name && schema.tables[refTable]) {
69
+ visit(refTable);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ visiting.delete(name);
75
+ resolved.add(name);
76
+ result.push(name);
77
+ }
78
+ for (const name of tableNames) {
79
+ visit(name);
80
+ }
81
+ return result;
82
+ }
83
+ /**
84
+ * Generate a CREATE TABLE statement for a single table definition.
85
+ */
86
+ function generateCreateTable(table) {
87
+ const tableName = table.name;
88
+ const columnDefs = [];
89
+ for (const [fieldName, config] of Object.entries(table.columns)) {
90
+ columnDefs.push(generateColumnDef(fieldName, config));
91
+ }
92
+ const body = columnDefs.map((d) => ` ${d}`).join(',\n');
93
+ return `CREATE TABLE ${(0, query_js_1.quoteIdent)(tableName)} (\n${body}\n);`;
94
+ }
95
+ /**
96
+ * Generate a single column definition line (e.g. "id BIGSERIAL PRIMARY KEY").
97
+ */
98
+ function generateColumnDef(fieldName, config) {
99
+ const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
100
+ const parts = [(0, query_js_1.quoteIdent)(snakeName)];
101
+ // Type
102
+ if (config.type === 'VARCHAR' && config.maxLength != null) {
103
+ parts.push(`VARCHAR(${config.maxLength})`);
104
+ }
105
+ else {
106
+ parts.push(config.type);
107
+ }
108
+ // PRIMARY KEY
109
+ if (config.isPrimaryKey) {
110
+ parts.push('PRIMARY KEY');
111
+ }
112
+ // UNIQUE (only if not primary key — PK is implicitly unique)
113
+ if (config.isUnique && !config.isPrimaryKey) {
114
+ parts.push('UNIQUE');
115
+ }
116
+ // NOT NULL — serial types are implicitly NOT NULL, but explicit is fine.
117
+ // A column is NOT NULL if:
118
+ // 1. Explicitly marked .notNull(), OR
119
+ // 2. Is a serial (BIGSERIAL implies NOT NULL), OR
120
+ // 3. Has a primary key (PKs are NOT NULL)
121
+ // A column is left nullable if .nullable() was called.
122
+ const isSerial = config.type === 'BIGSERIAL';
123
+ const implicitNotNull = isSerial || config.isPrimaryKey;
124
+ if (config.isNotNull && !implicitNotNull) {
125
+ parts.push('NOT NULL');
126
+ }
127
+ // DEFAULT
128
+ if (config.defaultValue != null) {
129
+ const sqlDefault = normalizeDefault(config.defaultValue);
130
+ parts.push(`DEFAULT ${sqlDefault}`);
131
+ }
132
+ // REFERENCES
133
+ if (config.referencesTarget) {
134
+ const refParts = config.referencesTarget.split('.');
135
+ if (refParts.length === 2) {
136
+ parts.push(`REFERENCES ${(0, query_js_1.quoteIdent)(refParts[0])}(${(0, query_js_1.quoteIdent)(refParts[1])})`);
137
+ }
138
+ }
139
+ return parts.join(' ');
140
+ }
141
+ /**
142
+ * Normalize a default value from the user's schema definition to valid SQL.
143
+ *
144
+ * Examples:
145
+ * 'now()' → NOW()
146
+ * "'free'" → 'free'
147
+ * 'false' → false
148
+ * '0' → 0
149
+ */
150
+ function normalizeDefault(val) {
151
+ const upper = val.toUpperCase().trim();
152
+ // Known SQL constants
153
+ if (['TRUE', 'FALSE', 'NULL'].includes(upper)) {
154
+ return upper;
155
+ }
156
+ // Known SQL function calls: NOW(), CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, GEN_RANDOM_UUID()
157
+ const allowedFunctions = ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'GEN_RANDOM_UUID()'];
158
+ if (allowedFunctions.includes(upper)) {
159
+ return upper;
160
+ }
161
+ // Numeric literals (integer or decimal, optionally negative)
162
+ if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
163
+ return val.trim();
164
+ }
165
+ // Simple single-quoted string literals (no semicolons, no SQL statement keywords)
166
+ if (/^'[^']*'$/.test(val.trim())) {
167
+ const inner = val.trim().slice(1, -1);
168
+ if (/[;]/.test(inner) || /\b(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE|GRANT|REVOKE|TRUNCATE)\b/i.test(inner)) {
169
+ throw new Error(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
170
+ }
171
+ return val.trim();
172
+ }
173
+ throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
174
+ }
175
+ /**
176
+ * Generate CREATE INDEX statements for foreign key columns.
177
+ * Only generates indexes for columns that have a REFERENCES clause.
178
+ */
179
+ function generateForeignKeyIndexes(table) {
180
+ const indexes = [];
181
+ for (const [fieldName, config] of Object.entries(table.columns)) {
182
+ if (config.referencesTarget) {
183
+ const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
184
+ const indexName = `idx_${table.name}_${snakeName}`;
185
+ indexes.push(`CREATE INDEX ${(0, query_js_1.quoteIdent)(indexName)} ON ${(0, query_js_1.quoteIdent)(table.name)}(${(0, query_js_1.quoteIdent)(snakeName)});`);
186
+ }
187
+ }
188
+ return indexes;
189
+ }
190
+ /**
191
+ * Compare a SchemaDef against a live Postgres database and return the diff.
192
+ *
193
+ * Connects to the database, inspects the public schema, and computes what
194
+ * DDL is needed to make the database match the schema definition.
195
+ */
196
+ async function schemaDiff(schema, connectionString) {
197
+ const client = new pg_1.default.Client({ connectionString });
198
+ await client.connect();
199
+ try {
200
+ // Get existing tables in the public schema
201
+ const tableResult = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`);
202
+ const existingTables = new Set(tableResult.rows.map((r) => r.tablename));
203
+ // Get existing columns for all tables
204
+ const columnResult = await client.query(`SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length
205
+ FROM information_schema.columns
206
+ WHERE table_schema = 'public'
207
+ ORDER BY table_name, ordinal_position`);
208
+ const dbColumns = {};
209
+ for (const row of columnResult.rows) {
210
+ if (!dbColumns[row.table_name]) {
211
+ dbColumns[row.table_name] = {};
212
+ }
213
+ dbColumns[row.table_name][row.column_name] = {
214
+ dataType: row.data_type,
215
+ udtName: row.udt_name,
216
+ isNullable: row.is_nullable === 'YES',
217
+ columnDefault: row.column_default,
218
+ maxLength: row.character_maximum_length,
219
+ };
220
+ }
221
+ // Get single-column UNIQUE constraints (excluding PKs)
222
+ const uniqueResult = await client.query(`SELECT tc.table_name, tc.constraint_name, kcu.column_name
223
+ FROM information_schema.table_constraints tc
224
+ JOIN information_schema.key_column_usage kcu
225
+ ON tc.constraint_name = kcu.constraint_name
226
+ AND tc.table_schema = kcu.table_schema
227
+ WHERE tc.table_schema = 'public'
228
+ AND tc.constraint_type = 'UNIQUE'
229
+ AND tc.constraint_name IN (
230
+ SELECT constraint_name
231
+ FROM information_schema.key_column_usage
232
+ WHERE table_schema = 'public'
233
+ GROUP BY constraint_name
234
+ HAVING COUNT(*) = 1
235
+ )`);
236
+ // Map: table → column → constraint_name for single-col uniques
237
+ const dbUniques = {};
238
+ for (const row of uniqueResult.rows) {
239
+ if (!dbUniques[row.table_name])
240
+ dbUniques[row.table_name] = {};
241
+ dbUniques[row.table_name][row.column_name] = row.constraint_name;
242
+ }
243
+ const schemaTableNames = new Set(Object.keys(schema.tables));
244
+ const result = { create: [], alter: [], drop: [], statements: [], reverseStatements: [] };
245
+ // Tables to create (in schema but not in DB)
246
+ const sorted = topologicalSort(schema);
247
+ for (const tableName of sorted) {
248
+ if (!existingTables.has(tableName)) {
249
+ const tableDef = schema.tables[tableName];
250
+ result.create.push(tableDef);
251
+ result.statements.push(generateCreateTable(tableDef));
252
+ const fkIndexes = generateForeignKeyIndexes(tableDef);
253
+ result.statements.push(...fkIndexes);
254
+ // Reverse: DROP TABLE (with indexes — they drop automatically)
255
+ result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${(0, query_js_1.quoteIdent)(tableName)} CASCADE;`);
256
+ }
257
+ }
258
+ // Tables to drop (in DB but not in schema)
259
+ for (const existingTable of existingTables) {
260
+ if (!schemaTableNames.has(existingTable)) {
261
+ result.drop.push(existingTable);
262
+ // We don't auto-generate DROP statements for safety
263
+ }
264
+ }
265
+ // Tables to alter (exist in both)
266
+ for (const tableName of sorted) {
267
+ if (!existingTables.has(tableName))
268
+ continue;
269
+ const tableDef = schema.tables[tableName];
270
+ const dbCols = dbColumns[tableName] ?? {};
271
+ const tableUniques = dbUniques[tableName] ?? {};
272
+ const alterDef = { table: tableName, columns: [] };
273
+ for (const [fieldName, config] of Object.entries(tableDef.columns)) {
274
+ const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
275
+ const dbCol = dbCols[snakeName];
276
+ if (!dbCol) {
277
+ // Column exists in schema but not in DB — ADD COLUMN
278
+ const colDef = generateColumnDef(fieldName, config);
279
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD COLUMN ${colDef};`;
280
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP COLUMN ${(0, query_js_1.quoteIdent)(snakeName)};`;
281
+ alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
282
+ result.statements.push(sql);
283
+ result.reverseStatements.unshift(reverseSql);
284
+ continue;
285
+ }
286
+ // Check type mismatch
287
+ const expectedUdt = schemaTypeToUdt(config);
288
+ if (expectedUdt && dbCol.udtName !== expectedUdt) {
289
+ const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
290
+ const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
291
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} TYPE ${sqlType} USING ${(0, query_js_1.quoteIdent)(snakeName)}::${sqlType};`;
292
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} TYPE ${oldSqlType} USING ${(0, query_js_1.quoteIdent)(snakeName)}::${oldSqlType};`;
293
+ alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
294
+ result.statements.push(sql);
295
+ result.reverseStatements.unshift(reverseSql);
296
+ }
297
+ // Check NOT NULL mismatch
298
+ const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
299
+ const isCurrentlyNullable = dbCol.isNullable;
300
+ if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
301
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET NOT NULL;`;
302
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP NOT NULL;`;
303
+ alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql, reverseSql });
304
+ result.statements.push(sql);
305
+ result.reverseStatements.unshift(reverseSql);
306
+ }
307
+ else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
308
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP NOT NULL;`;
309
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET NOT NULL;`;
310
+ alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql, reverseSql });
311
+ result.statements.push(sql);
312
+ result.reverseStatements.unshift(reverseSql);
313
+ }
314
+ // Check DEFAULT value mismatch
315
+ const isSerial = config.type === 'BIGSERIAL';
316
+ if (!isSerial) {
317
+ const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
318
+ const dbDefault = dbCol.columnDefault;
319
+ if (schemaDefault && !dbDefault) {
320
+ // Schema has default, DB doesn't
321
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${schemaDefault};`;
322
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP DEFAULT;`;
323
+ alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
324
+ result.statements.push(sql);
325
+ result.reverseStatements.unshift(reverseSql);
326
+ }
327
+ else if (!schemaDefault && dbDefault && !isSequenceDefault(dbDefault)) {
328
+ // DB has a non-sequence default, schema doesn't
329
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP DEFAULT;`;
330
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${dbDefault};`;
331
+ alterDef.columns.push({ column: snakeName, action: 'drop_default', sql, reverseSql });
332
+ result.statements.push(sql);
333
+ result.reverseStatements.unshift(reverseSql);
334
+ }
335
+ else if (schemaDefault &&
336
+ dbDefault &&
337
+ !isSequenceDefault(dbDefault) &&
338
+ !defaultsMatch(schemaDefault, dbDefault)) {
339
+ // Both have defaults but they differ
340
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${schemaDefault};`;
341
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET DEFAULT ${dbDefault};`;
342
+ alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
343
+ result.statements.push(sql);
344
+ result.reverseStatements.unshift(reverseSql);
345
+ }
346
+ }
347
+ // Check UNIQUE constraint mismatch (skip PKs — they're implicitly unique)
348
+ if (!config.isPrimaryKey) {
349
+ const hasDbUnique = snakeName in tableUniques;
350
+ const wantsUnique = config.isUnique === true;
351
+ if (wantsUnique && !hasDbUnique) {
352
+ const constraintName = `${tableName}_${snakeName}_key`;
353
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)} UNIQUE (${(0, query_js_1.quoteIdent)(snakeName)});`;
354
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)};`;
355
+ alterDef.columns.push({ column: snakeName, action: 'add_unique', sql, reverseSql });
356
+ result.statements.push(sql);
357
+ result.reverseStatements.unshift(reverseSql);
358
+ }
359
+ else if (!wantsUnique && hasDbUnique) {
360
+ const constraintName = tableUniques[snakeName];
361
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)};`;
362
+ const reverseSql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD CONSTRAINT ${(0, query_js_1.quoteIdent)(constraintName)} UNIQUE (${(0, query_js_1.quoteIdent)(snakeName)});`;
363
+ alterDef.columns.push({ column: snakeName, action: 'drop_unique', sql, reverseSql });
364
+ result.statements.push(sql);
365
+ result.reverseStatements.unshift(reverseSql);
366
+ }
367
+ }
368
+ }
369
+ // Check for columns in DB that are not in schema
370
+ for (const dbColName of Object.keys(dbCols)) {
371
+ const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
372
+ if (!hasField) {
373
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP COLUMN ${(0, query_js_1.quoteIdent)(dbColName)};`;
374
+ const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" — add it back manually`;
375
+ alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
376
+ // Don't auto-add drops to statements for safety — user must opt in
377
+ }
378
+ }
379
+ if (alterDef.columns.length > 0) {
380
+ result.alter.push(alterDef);
381
+ }
382
+ }
383
+ return result;
384
+ }
385
+ finally {
386
+ await client.end();
387
+ }
388
+ }
389
+ /**
390
+ * Map a schema column type to its expected PostgreSQL UDT name.
391
+ */
392
+ function schemaTypeToUdt(config) {
393
+ const map = {
394
+ BIGSERIAL: 'int8',
395
+ BIGINT: 'int8',
396
+ INTEGER: 'int4',
397
+ SMALLINT: 'int2',
398
+ TEXT: 'text',
399
+ VARCHAR: 'varchar',
400
+ BOOLEAN: 'bool',
401
+ TIMESTAMPTZ: 'timestamptz',
402
+ DATE: 'date',
403
+ JSONB: 'jsonb',
404
+ UUID: 'uuid',
405
+ REAL: 'float4',
406
+ 'DOUBLE PRECISION': 'float8',
407
+ NUMERIC: 'numeric',
408
+ BYTEA: 'bytea',
409
+ };
410
+ return map[config.type] ?? null;
411
+ }
412
+ /**
413
+ * Reverse map: PostgreSQL UDT name → SQL type (for generating reverse ALTER TYPE).
414
+ */
415
+ function udtToSqlType(udtName, maxLength) {
416
+ const map = {
417
+ int8: 'BIGINT',
418
+ int4: 'INTEGER',
419
+ int2: 'SMALLINT',
420
+ text: 'TEXT',
421
+ varchar: maxLength ? `VARCHAR(${maxLength})` : 'VARCHAR',
422
+ bool: 'BOOLEAN',
423
+ timestamptz: 'TIMESTAMPTZ',
424
+ date: 'DATE',
425
+ jsonb: 'JSONB',
426
+ uuid: 'UUID',
427
+ float4: 'REAL',
428
+ float8: 'DOUBLE PRECISION',
429
+ numeric: 'NUMERIC',
430
+ bytea: 'BYTEA',
431
+ };
432
+ return map[udtName] ?? udtName.toUpperCase();
433
+ }
434
+ /**
435
+ * Normalize a database default value for comparison.
436
+ * Strips PostgreSQL type casts (e.g. 'free'::text → 'free') and wrapping parens.
437
+ */
438
+ function normalizeDbDefault(dbDefault) {
439
+ let val = dbDefault;
440
+ // Strip type casts: 'free'::text → 'free', 0::integer → 0
441
+ val = val.replace(/::[\w\s"]+(\[\])?$/g, '').trim();
442
+ // Unwrap parens added by PostgreSQL: ('free') → 'free'
443
+ while (val.startsWith('(') && val.endsWith(')')) {
444
+ val = val.slice(1, -1).trim();
445
+ }
446
+ return val;
447
+ }
448
+ /** Check if a DB default is a sequence default (auto-generated for serial columns). */
449
+ function isSequenceDefault(dbDefault) {
450
+ return dbDefault.includes('nextval(');
451
+ }
452
+ /**
453
+ * Compare a schema default against a database default, accounting for
454
+ * PostgreSQL's normalization of default values.
455
+ */
456
+ function defaultsMatch(schemaDefault, dbDefault) {
457
+ const a = schemaDefault.toLowerCase().trim();
458
+ const b = normalizeDbDefault(dbDefault).toLowerCase().trim();
459
+ return a === b;
460
+ }
461
+ /**
462
+ * Push a schema definition to a live database.
463
+ *
464
+ * Computes the diff, then executes the resulting DDL statements in a
465
+ * single transaction. This is a destructive operation for ADD/ALTER —
466
+ * it will NOT drop tables or columns unless explicitly configured.
467
+ */
468
+ async function schemaPush(schema, connectionString, options = {}) {
469
+ const diff = await schemaDiff(schema, connectionString);
470
+ const result = {
471
+ statementsExecuted: 0,
472
+ statements: diff.statements,
473
+ tablesCreated: diff.create.map((t) => t.name),
474
+ tablesAltered: diff.alter.map((a) => a.table),
475
+ };
476
+ if (options.dryRun || diff.statements.length === 0) {
477
+ return result;
478
+ }
479
+ // Execute all statements in a transaction
480
+ const client = new pg_1.default.Client({ connectionString });
481
+ await client.connect();
482
+ try {
483
+ await client.query('BEGIN');
484
+ for (const sql of diff.statements) {
485
+ await client.query(sql);
486
+ result.statementsExecuted++;
487
+ }
488
+ await client.query('COMMIT');
489
+ }
490
+ catch (err) {
491
+ await client.query('ROLLBACK');
492
+ throw err;
493
+ }
494
+ finally {
495
+ await client.end();
496
+ }
497
+ return result;
498
+ }
499
+ // ---------------------------------------------------------------------------
500
+ // Utility: format schema as SQL string (convenience for debugging/printing)
501
+ // ---------------------------------------------------------------------------
502
+ /**
503
+ * Generate the full DDL as a single formatted string.
504
+ * Useful for printing or saving to a .sql file.
505
+ */
506
+ function schemaToSQLString(schema) {
507
+ const statements = schemaToSQL(schema);
508
+ return `${statements.join('\n\n')}\n`;
509
+ }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm — Schema metadata types
4
+ *
5
+ * These types represent the introspected database schema at runtime.
6
+ * They're used by the query builder, code generator, and CLI.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.pgTypeToTs = pgTypeToTs;
10
+ exports.isDateType = isDateType;
11
+ exports.pgArrayType = pgArrayType;
12
+ exports.snakeToCamel = snakeToCamel;
13
+ exports.camelToSnake = camelToSnake;
14
+ exports.snakeToPascal = snakeToPascal;
15
+ exports.singularize = singularize;
16
+ // ---------------------------------------------------------------------------
17
+ // Type mapping: Postgres → TypeScript
18
+ // ---------------------------------------------------------------------------
19
+ const PG_TO_TS = {
20
+ // Integers
21
+ int2: 'number',
22
+ int4: 'number',
23
+ // int8 maps to `number` for DX (auto-increment IDs, counts, etc.).
24
+ // Values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1) are returned as
25
+ // `string` at runtime to avoid precision loss. See client.ts int8 parser.
26
+ int8: 'number',
27
+ float4: 'number',
28
+ float8: 'number',
29
+ oid: 'number',
30
+ // Precision-sensitive — keep as string to avoid JS float issues
31
+ numeric: 'string',
32
+ money: 'string',
33
+ // Boolean
34
+ bool: 'boolean',
35
+ // Strings
36
+ text: 'string',
37
+ varchar: 'string',
38
+ char: 'string',
39
+ bpchar: 'string',
40
+ name: 'string',
41
+ uuid: 'string',
42
+ citext: 'string',
43
+ xml: 'string',
44
+ // Dates & times
45
+ timestamptz: 'Date',
46
+ timestamp: 'Date',
47
+ date: 'Date',
48
+ time: 'string',
49
+ timetz: 'string',
50
+ interval: 'string',
51
+ // JSON
52
+ json: 'unknown',
53
+ jsonb: 'unknown',
54
+ // Binary
55
+ bytea: 'Buffer',
56
+ // Network
57
+ inet: 'string',
58
+ cidr: 'string',
59
+ macaddr: 'string',
60
+ // Geometric
61
+ point: 'string',
62
+ line: 'string',
63
+ lseg: 'string',
64
+ box: 'string',
65
+ path: 'string',
66
+ polygon: 'string',
67
+ circle: 'string',
68
+ // TSVector
69
+ tsvector: 'string',
70
+ tsquery: 'string',
71
+ };
72
+ const DATE_TYPES = new Set(['timestamptz', 'timestamp', 'date']);
73
+ const PG_TO_ARRAY = {
74
+ int2: 'smallint[]',
75
+ int4: 'integer[]',
76
+ int8: 'bigint[]',
77
+ float4: 'real[]',
78
+ float8: 'double precision[]',
79
+ numeric: 'numeric[]',
80
+ bool: 'boolean[]',
81
+ text: 'text[]',
82
+ varchar: 'text[]',
83
+ char: 'text[]',
84
+ bpchar: 'text[]',
85
+ uuid: 'uuid[]',
86
+ timestamptz: 'timestamptz[]',
87
+ timestamp: 'timestamp[]',
88
+ date: 'date[]',
89
+ json: 'json[]',
90
+ jsonb: 'jsonb[]',
91
+ bytea: 'bytea[]',
92
+ inet: 'inet[]',
93
+ };
94
+ /** Map a Postgres type to its TypeScript equivalent */
95
+ function pgTypeToTs(pgType, nullable) {
96
+ // Array types: udt_name starts with '_'
97
+ if (pgType.startsWith('_')) {
98
+ const elementType = pgTypeToTs(pgType.slice(1), false);
99
+ const tsType = `${elementType}[]`;
100
+ return nullable ? `${tsType} | null` : tsType;
101
+ }
102
+ const tsType = PG_TO_TS[pgType] ?? 'unknown';
103
+ return nullable ? `${tsType} | null` : tsType;
104
+ }
105
+ /** Check if a Postgres type is a date/timestamp that needs Date parsing */
106
+ function isDateType(pgType) {
107
+ return DATE_TYPES.has(pgType);
108
+ }
109
+ /** Get the Postgres array cast type for UNNEST batch inserts */
110
+ function pgArrayType(pgType) {
111
+ return PG_TO_ARRAY[pgType] ?? 'text[]';
112
+ }
113
+ // ---------------------------------------------------------------------------
114
+ // Name conversion utilities
115
+ // ---------------------------------------------------------------------------
116
+ /** snake_case → camelCase */
117
+ function snakeToCamel(s) {
118
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
119
+ }
120
+ /** camelCase → snake_case */
121
+ function camelToSnake(s) {
122
+ return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
123
+ }
124
+ /** snake_case → PascalCase (for type names) */
125
+ function snakeToPascal(s) {
126
+ return s
127
+ .split('_')
128
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
129
+ .join('');
130
+ }
131
+ /** Naive singularize: "posts" → "post", "categories" → "category" */
132
+ function singularize(s) {
133
+ if (s.endsWith('ies'))
134
+ return `${s.slice(0, -3)}y`;
135
+ if (s.endsWith('ses') || s.endsWith('xes') || s.endsWith('zes'))
136
+ return s.slice(0, -2);
137
+ if (s.endsWith('s') && !s.endsWith('ss'))
138
+ return s.slice(0, -1);
139
+ return s;
140
+ }