turbine-orm 0.5.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 (46) hide show
  1. package/README.md +194 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +240 -41
  4. package/dist/cjs/cli/migrate.js +71 -46
  5. package/dist/cjs/cli/ui.js +5 -9
  6. package/dist/cjs/client.js +109 -46
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +33 -13
  9. package/dist/cjs/index.js +39 -20
  10. package/dist/cjs/introspect.js +3 -5
  11. package/dist/cjs/pipeline.js +9 -2
  12. package/dist/cjs/query.js +442 -109
  13. package/dist/cjs/schema-builder.js +93 -24
  14. package/dist/cjs/schema-sql.js +157 -19
  15. package/dist/cjs/schema.js +5 -2
  16. package/dist/cjs/serverless.js +87 -176
  17. package/dist/cli/config.js +6 -16
  18. package/dist/cli/index.js +245 -46
  19. package/dist/cli/migrate.d.ts +6 -1
  20. package/dist/cli/migrate.js +72 -47
  21. package/dist/cli/ui.js +5 -9
  22. package/dist/client.d.ts +77 -4
  23. package/dist/client.js +109 -46
  24. package/dist/errors.d.ts +138 -0
  25. package/dist/errors.js +278 -0
  26. package/dist/generate.d.ts +1 -1
  27. package/dist/generate.js +36 -16
  28. package/dist/index.d.ts +11 -9
  29. package/dist/index.js +16 -12
  30. package/dist/introspect.d.ts +1 -1
  31. package/dist/introspect.js +4 -6
  32. package/dist/pipeline.d.ts +1 -1
  33. package/dist/pipeline.js +9 -2
  34. package/dist/query.d.ts +257 -36
  35. package/dist/query.js +443 -110
  36. package/dist/schema-builder.d.ts +2 -2
  37. package/dist/schema-builder.js +93 -25
  38. package/dist/schema-sql.d.ts +7 -3
  39. package/dist/schema-sql.js +157 -19
  40. package/dist/schema.d.ts +1 -1
  41. package/dist/schema.js +5 -2
  42. package/dist/serverless.d.ts +91 -139
  43. package/dist/serverless.js +86 -173
  44. package/package.json +33 -16
  45. package/dist/types.d.ts +0 -93
  46. package/dist/types.js +0 -126
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @batadata/turbine — Schema Builder
2
+ * turbine-orm — Schema Builder
3
3
  *
4
4
  * TypeScript-first schema definition API. Define your database schema
5
5
  * as plain objects — no method chaining, no DSL. Fully type-checked,
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * import { defineSchema } from '@batadata/turbine';
10
+ * import { defineSchema } from 'turbine-orm';
11
11
  *
12
12
  * export default defineSchema({
13
13
  * users: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @batadata/turbine — Schema Builder
2
+ * turbine-orm — Schema Builder
3
3
  *
4
4
  * TypeScript-first schema definition API. Define your database schema
5
5
  * as plain objects — no method chaining, no DSL. Fully type-checked,
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * import { defineSchema } from '@batadata/turbine';
10
+ * import { defineSchema } from 'turbine-orm';
11
11
  *
12
12
  * export default defineSchema({
13
13
  * users: {
@@ -22,7 +22,6 @@
22
22
  * });
23
23
  * ```
24
24
  */
25
- import { camelToSnake } from './schema.js';
26
25
  /** Maps shorthand names to actual Postgres type strings */
27
26
  const TYPE_MAP = {
28
27
  serial: 'BIGSERIAL',
@@ -43,6 +42,9 @@ const TYPE_MAP = {
43
42
  };
44
43
  /** Convert a user-facing ColumnDef to the internal ColumnConfig */
45
44
  function resolveColumn(def) {
45
+ if (!(def.type in TYPE_MAP)) {
46
+ throw new Error(`Invalid column type "${def.type}". Valid types: ${Object.keys(TYPE_MAP).join(', ')}`);
47
+ }
46
48
  return {
47
49
  type: TYPE_MAP[def.type],
48
50
  isPrimaryKey: def.primaryKey ?? false,
@@ -113,28 +115,94 @@ export class ColumnBuilder {
113
115
  maxLength: null,
114
116
  };
115
117
  }
116
- serial() { this._config.type = 'BIGSERIAL'; return this; }
117
- bigint() { this._config.type = 'BIGINT'; return this; }
118
- integer() { this._config.type = 'INTEGER'; return this; }
119
- smallint() { this._config.type = 'SMALLINT'; return this; }
120
- text() { this._config.type = 'TEXT'; return this; }
121
- varchar(length) { this._config.type = 'VARCHAR'; this._config.maxLength = length; return this; }
122
- boolean() { this._config.type = 'BOOLEAN'; return this; }
123
- timestamp() { this._config.type = 'TIMESTAMPTZ'; return this; }
124
- date() { this._config.type = 'DATE'; return this; }
125
- json() { this._config.type = 'JSONB'; return this; }
126
- uuid() { this._config.type = 'UUID'; return this; }
127
- real() { this._config.type = 'REAL'; return this; }
128
- doublePrecision() { this._config.type = 'DOUBLE PRECISION'; return this; }
129
- numeric() { this._config.type = 'NUMERIC'; return this; }
130
- bytea() { this._config.type = 'BYTEA'; return this; }
131
- primaryKey() { this._config.isPrimaryKey = true; return this; }
132
- notNull() { this._config.isNotNull = true; return this; }
133
- nullable() { this._config.isNullable = true; return this; }
134
- unique() { this._config.isUnique = true; return this; }
135
- default(val) { this._config.defaultValue = val; return this; }
136
- references(target) { this._config.referencesTarget = target; return this; }
137
- build() { return { ...this._config }; }
118
+ serial() {
119
+ this._config.type = 'BIGSERIAL';
120
+ return this;
121
+ }
122
+ bigint() {
123
+ this._config.type = 'BIGINT';
124
+ return this;
125
+ }
126
+ integer() {
127
+ this._config.type = 'INTEGER';
128
+ return this;
129
+ }
130
+ smallint() {
131
+ this._config.type = 'SMALLINT';
132
+ return this;
133
+ }
134
+ text() {
135
+ this._config.type = 'TEXT';
136
+ return this;
137
+ }
138
+ varchar(length) {
139
+ this._config.type = 'VARCHAR';
140
+ this._config.maxLength = length;
141
+ return this;
142
+ }
143
+ boolean() {
144
+ this._config.type = 'BOOLEAN';
145
+ return this;
146
+ }
147
+ timestamp() {
148
+ this._config.type = 'TIMESTAMPTZ';
149
+ return this;
150
+ }
151
+ date() {
152
+ this._config.type = 'DATE';
153
+ return this;
154
+ }
155
+ json() {
156
+ this._config.type = 'JSONB';
157
+ return this;
158
+ }
159
+ uuid() {
160
+ this._config.type = 'UUID';
161
+ return this;
162
+ }
163
+ real() {
164
+ this._config.type = 'REAL';
165
+ return this;
166
+ }
167
+ doublePrecision() {
168
+ this._config.type = 'DOUBLE PRECISION';
169
+ return this;
170
+ }
171
+ numeric() {
172
+ this._config.type = 'NUMERIC';
173
+ return this;
174
+ }
175
+ bytea() {
176
+ this._config.type = 'BYTEA';
177
+ return this;
178
+ }
179
+ primaryKey() {
180
+ this._config.isPrimaryKey = true;
181
+ return this;
182
+ }
183
+ notNull() {
184
+ this._config.isNotNull = true;
185
+ return this;
186
+ }
187
+ nullable() {
188
+ this._config.isNullable = true;
189
+ return this;
190
+ }
191
+ unique() {
192
+ this._config.isUnique = true;
193
+ return this;
194
+ }
195
+ default(val) {
196
+ this._config.defaultValue = val;
197
+ return this;
198
+ }
199
+ references(target) {
200
+ this._config.referencesTarget = target;
201
+ return this;
202
+ }
203
+ build() {
204
+ return { ...this._config };
205
+ }
138
206
  }
139
207
  /** @deprecated Use defineSchema() with plain objects instead */
140
208
  export const column = new Proxy({}, {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @batadata/turbine — Schema SQL Generator
2
+ * turbine-orm — Schema SQL Generator
3
3
  *
4
4
  * Converts a SchemaDef (from defineSchema) into executable DDL statements.
5
5
  * Also provides diff and push commands for syncing schema to a live database.
@@ -16,9 +16,11 @@ export interface AlterColumnDef {
16
16
  /** Column name in snake_case */
17
17
  column: string;
18
18
  /** What changed */
19
- action: 'add' | 'drop' | 'alter_type' | 'set_not_null' | 'drop_not_null' | 'set_default' | 'drop_default';
19
+ action: 'add' | 'drop' | 'alter_type' | 'set_not_null' | 'drop_not_null' | 'set_default' | 'drop_default' | 'add_unique' | 'drop_unique';
20
20
  /** SQL fragment for the alteration */
21
21
  sql: string;
22
+ /** SQL to reverse this change (for DOWN migrations) */
23
+ reverseSql: string;
22
24
  }
23
25
  export interface AlterDef {
24
26
  /** Table name */
@@ -33,8 +35,10 @@ export interface DiffResult {
33
35
  alter: AlterDef[];
34
36
  /** Table names that exist in DB but not in schema — would need DROP TABLE */
35
37
  drop: string[];
36
- /** SQL statements to execute the diff */
38
+ /** SQL statements to execute the diff (UP direction) */
37
39
  statements: string[];
40
+ /** SQL statements to reverse the diff (DOWN direction, for migrations) */
41
+ reverseStatements: string[];
38
42
  }
39
43
  /**
40
44
  * Compare a SchemaDef against a live Postgres database and return the diff.
@@ -1,12 +1,12 @@
1
1
  /**
2
- * @batadata/turbine — Schema SQL Generator
2
+ * turbine-orm — Schema SQL Generator
3
3
  *
4
4
  * Converts a SchemaDef (from defineSchema) into executable DDL statements.
5
5
  * Also provides diff and push commands for syncing schema to a live database.
6
6
  */
7
7
  import pg from 'pg';
8
- import { camelToSnake } from './schema.js';
9
8
  import { quoteIdent } from './query.js';
9
+ import { camelToSnake } from './schema.js';
10
10
  // ---------------------------------------------------------------------------
11
11
  // SQL Generation — SchemaDef → CREATE TABLE statements
12
12
  // ---------------------------------------------------------------------------
@@ -145,9 +145,7 @@ function normalizeDefault(val) {
145
145
  return upper;
146
146
  }
147
147
  // Known SQL function calls: NOW(), CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, GEN_RANDOM_UUID()
148
- const allowedFunctions = [
149
- 'NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'GEN_RANDOM_UUID()',
150
- ];
148
+ const allowedFunctions = ['NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'GEN_RANDOM_UUID()'];
151
149
  if (allowedFunctions.includes(upper)) {
152
150
  return upper;
153
151
  }
@@ -155,8 +153,12 @@ function normalizeDefault(val) {
155
153
  if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
156
154
  return val.trim();
157
155
  }
158
- // Simple single-quoted string literals (no nested quotes)
156
+ // Simple single-quoted string literals (no semicolons, no SQL statement keywords)
159
157
  if (/^'[^']*'$/.test(val.trim())) {
158
+ const inner = val.trim().slice(1, -1);
159
+ if (/[;]/.test(inner) || /\b(DROP|ALTER|CREATE|INSERT|UPDATE|DELETE|GRANT|REVOKE|TRUNCATE)\b/i.test(inner)) {
160
+ throw new Error(`Suspicious default value: ${val}. String literals must not contain SQL statements.`);
161
+ }
160
162
  return val.trim();
161
163
  }
162
164
  throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
@@ -207,8 +209,30 @@ export async function schemaDiff(schema, connectionString) {
207
209
  maxLength: row.character_maximum_length,
208
210
  };
209
211
  }
212
+ // Get single-column UNIQUE constraints (excluding PKs)
213
+ const uniqueResult = await client.query(`SELECT tc.table_name, tc.constraint_name, kcu.column_name
214
+ FROM information_schema.table_constraints tc
215
+ JOIN information_schema.key_column_usage kcu
216
+ ON tc.constraint_name = kcu.constraint_name
217
+ AND tc.table_schema = kcu.table_schema
218
+ WHERE tc.table_schema = 'public'
219
+ AND tc.constraint_type = 'UNIQUE'
220
+ AND tc.constraint_name IN (
221
+ SELECT constraint_name
222
+ FROM information_schema.key_column_usage
223
+ WHERE table_schema = 'public'
224
+ GROUP BY constraint_name
225
+ HAVING COUNT(*) = 1
226
+ )`);
227
+ // Map: table → column → constraint_name for single-col uniques
228
+ const dbUniques = {};
229
+ for (const row of uniqueResult.rows) {
230
+ if (!dbUniques[row.table_name])
231
+ dbUniques[row.table_name] = {};
232
+ dbUniques[row.table_name][row.column_name] = row.constraint_name;
233
+ }
210
234
  const schemaTableNames = new Set(Object.keys(schema.tables));
211
- const result = { create: [], alter: [], drop: [], statements: [] };
235
+ const result = { create: [], alter: [], drop: [], statements: [], reverseStatements: [] };
212
236
  // Tables to create (in schema but not in DB)
213
237
  const sorted = topologicalSort(schema);
214
238
  for (const tableName of sorted) {
@@ -216,8 +240,10 @@ export async function schemaDiff(schema, connectionString) {
216
240
  const tableDef = schema.tables[tableName];
217
241
  result.create.push(tableDef);
218
242
  result.statements.push(generateCreateTable(tableDef));
219
- // Also add FK indexes
220
- result.statements.push(...generateForeignKeyIndexes(tableDef));
243
+ const fkIndexes = generateForeignKeyIndexes(tableDef);
244
+ result.statements.push(...fkIndexes);
245
+ // Reverse: DROP TABLE (with indexes — they drop automatically)
246
+ result.reverseStatements.unshift(`DROP TABLE IF EXISTS ${quoteIdent(tableName)} CASCADE;`);
221
247
  }
222
248
  }
223
249
  // Tables to drop (in DB but not in schema)
@@ -233,6 +259,7 @@ export async function schemaDiff(schema, connectionString) {
233
259
  continue;
234
260
  const tableDef = schema.tables[tableName];
235
261
  const dbCols = dbColumns[tableName] ?? {};
262
+ const tableUniques = dbUniques[tableName] ?? {};
236
263
  const alterDef = { table: tableName, columns: [] };
237
264
  for (const [fieldName, config] of Object.entries(tableDef.columns)) {
238
265
  const snakeName = camelToSnake(fieldName);
@@ -241,32 +268,93 @@ export async function schemaDiff(schema, connectionString) {
241
268
  // Column exists in schema but not in DB — ADD COLUMN
242
269
  const colDef = generateColumnDef(fieldName, config);
243
270
  const sql = `ALTER TABLE ${quoteIdent(tableName)} ADD COLUMN ${colDef};`;
244
- alterDef.columns.push({ column: snakeName, action: 'add', sql });
271
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} DROP COLUMN ${quoteIdent(snakeName)};`;
272
+ alterDef.columns.push({ column: snakeName, action: 'add', sql, reverseSql });
245
273
  result.statements.push(sql);
274
+ result.reverseStatements.unshift(reverseSql);
246
275
  continue;
247
276
  }
248
277
  // Check type mismatch
249
278
  const expectedUdt = schemaTypeToUdt(config);
250
279
  if (expectedUdt && dbCol.udtName !== expectedUdt) {
251
- const sqlType = config.type === 'VARCHAR' && config.maxLength
252
- ? `VARCHAR(${config.maxLength})`
253
- : config.type;
254
- const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} TYPE ${sqlType};`;
255
- alterDef.columns.push({ column: snakeName, action: 'alter_type', sql });
280
+ const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
281
+ const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
282
+ const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} TYPE ${sqlType} USING ${quoteIdent(snakeName)}::${sqlType};`;
283
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} TYPE ${oldSqlType} USING ${quoteIdent(snakeName)}::${oldSqlType};`;
284
+ alterDef.columns.push({ column: snakeName, action: 'alter_type', sql, reverseSql });
256
285
  result.statements.push(sql);
286
+ result.reverseStatements.unshift(reverseSql);
257
287
  }
258
288
  // Check NOT NULL mismatch
259
289
  const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
260
290
  const isCurrentlyNullable = dbCol.isNullable;
261
291
  if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
262
292
  const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} SET NOT NULL;`;
263
- alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql });
293
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} DROP NOT NULL;`;
294
+ alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql, reverseSql });
264
295
  result.statements.push(sql);
296
+ result.reverseStatements.unshift(reverseSql);
265
297
  }
266
298
  else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
267
299
  const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} DROP NOT NULL;`;
268
- alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql });
300
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} SET NOT NULL;`;
301
+ alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql, reverseSql });
269
302
  result.statements.push(sql);
303
+ result.reverseStatements.unshift(reverseSql);
304
+ }
305
+ // Check DEFAULT value mismatch
306
+ const isSerial = config.type === 'BIGSERIAL';
307
+ if (!isSerial) {
308
+ const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
309
+ const dbDefault = dbCol.columnDefault;
310
+ if (schemaDefault && !dbDefault) {
311
+ // Schema has default, DB doesn't
312
+ const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} SET DEFAULT ${schemaDefault};`;
313
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} DROP DEFAULT;`;
314
+ alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
315
+ result.statements.push(sql);
316
+ result.reverseStatements.unshift(reverseSql);
317
+ }
318
+ else if (!schemaDefault && dbDefault && !isSequenceDefault(dbDefault)) {
319
+ // DB has a non-sequence default, schema doesn't
320
+ const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} DROP DEFAULT;`;
321
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} SET DEFAULT ${dbDefault};`;
322
+ alterDef.columns.push({ column: snakeName, action: 'drop_default', sql, reverseSql });
323
+ result.statements.push(sql);
324
+ result.reverseStatements.unshift(reverseSql);
325
+ }
326
+ else if (schemaDefault &&
327
+ dbDefault &&
328
+ !isSequenceDefault(dbDefault) &&
329
+ !defaultsMatch(schemaDefault, dbDefault)) {
330
+ // Both have defaults but they differ
331
+ const sql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} SET DEFAULT ${schemaDefault};`;
332
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ALTER COLUMN ${quoteIdent(snakeName)} SET DEFAULT ${dbDefault};`;
333
+ alterDef.columns.push({ column: snakeName, action: 'set_default', sql, reverseSql });
334
+ result.statements.push(sql);
335
+ result.reverseStatements.unshift(reverseSql);
336
+ }
337
+ }
338
+ // Check UNIQUE constraint mismatch (skip PKs — they're implicitly unique)
339
+ if (!config.isPrimaryKey) {
340
+ const hasDbUnique = snakeName in tableUniques;
341
+ const wantsUnique = config.isUnique === true;
342
+ if (wantsUnique && !hasDbUnique) {
343
+ const constraintName = `${tableName}_${snakeName}_key`;
344
+ const sql = `ALTER TABLE ${quoteIdent(tableName)} ADD CONSTRAINT ${quoteIdent(constraintName)} UNIQUE (${quoteIdent(snakeName)});`;
345
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} DROP CONSTRAINT ${quoteIdent(constraintName)};`;
346
+ alterDef.columns.push({ column: snakeName, action: 'add_unique', sql, reverseSql });
347
+ result.statements.push(sql);
348
+ result.reverseStatements.unshift(reverseSql);
349
+ }
350
+ else if (!wantsUnique && hasDbUnique) {
351
+ const constraintName = tableUniques[snakeName];
352
+ const sql = `ALTER TABLE ${quoteIdent(tableName)} DROP CONSTRAINT ${quoteIdent(constraintName)};`;
353
+ const reverseSql = `ALTER TABLE ${quoteIdent(tableName)} ADD CONSTRAINT ${quoteIdent(constraintName)} UNIQUE (${quoteIdent(snakeName)});`;
354
+ alterDef.columns.push({ column: snakeName, action: 'drop_unique', sql, reverseSql });
355
+ result.statements.push(sql);
356
+ result.reverseStatements.unshift(reverseSql);
357
+ }
270
358
  }
271
359
  }
272
360
  // Check for columns in DB that are not in schema
@@ -274,7 +362,8 @@ export async function schemaDiff(schema, connectionString) {
274
362
  const hasField = Object.entries(tableDef.columns).some(([fieldName]) => camelToSnake(fieldName) === dbColName);
275
363
  if (!hasField) {
276
364
  const sql = `ALTER TABLE ${quoteIdent(tableName)} DROP COLUMN ${quoteIdent(dbColName)};`;
277
- alterDef.columns.push({ column: dbColName, action: 'drop', sql });
365
+ const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}" add it back manually`;
366
+ alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
278
367
  // Don't auto-add drops to statements for safety — user must opt in
279
368
  }
280
369
  }
@@ -311,6 +400,55 @@ function schemaTypeToUdt(config) {
311
400
  };
312
401
  return map[config.type] ?? null;
313
402
  }
403
+ /**
404
+ * Reverse map: PostgreSQL UDT name → SQL type (for generating reverse ALTER TYPE).
405
+ */
406
+ function udtToSqlType(udtName, maxLength) {
407
+ const map = {
408
+ int8: 'BIGINT',
409
+ int4: 'INTEGER',
410
+ int2: 'SMALLINT',
411
+ text: 'TEXT',
412
+ varchar: maxLength ? `VARCHAR(${maxLength})` : 'VARCHAR',
413
+ bool: 'BOOLEAN',
414
+ timestamptz: 'TIMESTAMPTZ',
415
+ date: 'DATE',
416
+ jsonb: 'JSONB',
417
+ uuid: 'UUID',
418
+ float4: 'REAL',
419
+ float8: 'DOUBLE PRECISION',
420
+ numeric: 'NUMERIC',
421
+ bytea: 'BYTEA',
422
+ };
423
+ return map[udtName] ?? udtName.toUpperCase();
424
+ }
425
+ /**
426
+ * Normalize a database default value for comparison.
427
+ * Strips PostgreSQL type casts (e.g. 'free'::text → 'free') and wrapping parens.
428
+ */
429
+ function normalizeDbDefault(dbDefault) {
430
+ let val = dbDefault;
431
+ // Strip type casts: 'free'::text → 'free', 0::integer → 0
432
+ val = val.replace(/::[\w\s"]+(\[\])?$/g, '').trim();
433
+ // Unwrap parens added by PostgreSQL: ('free') → 'free'
434
+ while (val.startsWith('(') && val.endsWith(')')) {
435
+ val = val.slice(1, -1).trim();
436
+ }
437
+ return val;
438
+ }
439
+ /** Check if a DB default is a sequence default (auto-generated for serial columns). */
440
+ function isSequenceDefault(dbDefault) {
441
+ return dbDefault.includes('nextval(');
442
+ }
443
+ /**
444
+ * Compare a schema default against a database default, accounting for
445
+ * PostgreSQL's normalization of default values.
446
+ */
447
+ function defaultsMatch(schemaDefault, dbDefault) {
448
+ const a = schemaDefault.toLowerCase().trim();
449
+ const b = normalizeDbDefault(dbDefault).toLowerCase().trim();
450
+ return a === b;
451
+ }
314
452
  /**
315
453
  * Push a schema definition to a live database.
316
454
  *
@@ -358,6 +496,6 @@ export async function schemaPush(schema, connectionString, options = {}) {
358
496
  */
359
497
  export function schemaToSQLString(schema) {
360
498
  const statements = schemaToSQL(schema);
361
- return statements.join('\n\n') + '\n';
499
+ return `${statements.join('\n\n')}\n`;
362
500
  }
363
501
  //# sourceMappingURL=schema-sql.js.map
package/dist/schema.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @batadata/turbine — Schema metadata types
2
+ * turbine-orm — Schema metadata types
3
3
  *
4
4
  * These types represent the introspected database schema at runtime.
5
5
  * They're used by the query builder, code generator, and CLI.
package/dist/schema.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @batadata/turbine — Schema metadata types
2
+ * turbine-orm — Schema metadata types
3
3
  *
4
4
  * These types represent the introspected database schema at runtime.
5
5
  * They're used by the query builder, code generator, and CLI.
@@ -11,6 +11,9 @@ const PG_TO_TS = {
11
11
  // Integers
12
12
  int2: 'number',
13
13
  int4: 'number',
14
+ // int8 maps to `number` for DX (auto-increment IDs, counts, etc.).
15
+ // Values exceeding Number.MAX_SAFE_INTEGER (2^53 - 1) are returned as
16
+ // `string` at runtime to avoid precision loss. See client.ts int8 parser.
14
17
  int8: 'number',
15
18
  float4: 'number',
16
19
  float8: 'number',
@@ -119,7 +122,7 @@ export function snakeToPascal(s) {
119
122
  /** Naive singularize: "posts" → "post", "categories" → "category" */
120
123
  export function singularize(s) {
121
124
  if (s.endsWith('ies'))
122
- return s.slice(0, -3) + 'y';
125
+ return `${s.slice(0, -3)}y`;
123
126
  if (s.endsWith('ses') || s.endsWith('xes') || s.endsWith('zes'))
124
127
  return s.slice(0, -2);
125
128
  if (s.endsWith('s') && !s.endsWith('ss'))