turbine-orm 0.4.0 → 0.5.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 (59) hide show
  1. package/README.md +51 -2
  2. package/dist/cjs/cli/config.js +161 -0
  3. package/dist/cjs/cli/index.js +977 -0
  4. package/dist/cjs/cli/migrate.js +421 -0
  5. package/dist/cjs/cli/ui.js +237 -0
  6. package/dist/cjs/client.js +449 -0
  7. package/dist/cjs/generate.js +301 -0
  8. package/dist/cjs/index.js +75 -0
  9. package/dist/cjs/introspect.js +289 -0
  10. package/dist/cjs/package.json +1 -0
  11. package/dist/cjs/pipeline.js +71 -0
  12. package/dist/cjs/query.js +1558 -0
  13. package/dist/cjs/schema-builder.js +169 -0
  14. package/dist/cjs/schema-sql.js +371 -0
  15. package/dist/cjs/schema.js +137 -0
  16. package/dist/cjs/serverless.js +199 -0
  17. package/dist/cli/config.js +1 -1
  18. package/dist/cli/index.js +16 -8
  19. package/dist/cli/migrate.d.ts +29 -5
  20. package/dist/cli/migrate.js +58 -35
  21. package/dist/cli/ui.js +1 -1
  22. package/dist/client.d.ts +15 -4
  23. package/dist/client.js +28 -15
  24. package/dist/generate.d.ts +1 -1
  25. package/dist/generate.js +13 -7
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/introspect.d.ts +1 -1
  29. package/dist/introspect.js +1 -1
  30. package/dist/pipeline.d.ts +1 -1
  31. package/dist/pipeline.js +1 -1
  32. package/dist/query.d.ts +55 -11
  33. package/dist/query.js +135 -140
  34. package/dist/schema-builder.d.ts +2 -2
  35. package/dist/schema-builder.js +2 -2
  36. package/dist/schema-sql.d.ts +1 -1
  37. package/dist/schema-sql.js +31 -15
  38. package/dist/schema.d.ts +1 -1
  39. package/dist/schema.js +1 -1
  40. package/dist/serverless.d.ts +3 -3
  41. package/dist/serverless.js +4 -4
  42. package/dist/types.d.ts +1 -1
  43. package/dist/types.js +1 -1
  44. package/package.json +17 -11
  45. package/dist/cli/config.d.ts.map +0 -1
  46. package/dist/cli/index.d.ts.map +0 -1
  47. package/dist/cli/migrate.d.ts.map +0 -1
  48. package/dist/cli/ui.d.ts.map +0 -1
  49. package/dist/client.d.ts.map +0 -1
  50. package/dist/generate.d.ts.map +0 -1
  51. package/dist/index.d.ts.map +0 -1
  52. package/dist/introspect.d.ts.map +0 -1
  53. package/dist/pipeline.d.ts.map +0 -1
  54. package/dist/query.d.ts.map +0 -1
  55. package/dist/schema-builder.d.ts.map +0 -1
  56. package/dist/schema-sql.d.ts.map +0 -1
  57. package/dist/schema.d.ts.map +0 -1
  58. package/dist/serverless.d.ts.map +0 -1
  59. package/dist/types.d.ts.map +0 -1
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * @batadata/turbine — Schema Builder
4
+ *
5
+ * TypeScript-first schema definition API. Define your database schema
6
+ * as plain objects — no method chaining, no DSL. Fully type-checked,
7
+ * JSON-serializable, and easy to read.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { defineSchema } from '@batadata/turbine';
12
+ *
13
+ * export default defineSchema({
14
+ * users: {
15
+ * id: { type: 'serial', primaryKey: true },
16
+ * email: { type: 'text', unique: true, notNull: true },
17
+ * name: { type: 'text', notNull: true },
18
+ * bio: { type: 'text' },
19
+ * role: { type: 'varchar', maxLength: 50, default: "'user'" },
20
+ * orgId: { type: 'bigint', notNull: true, references: 'organizations.id' },
21
+ * createdAt: { type: 'timestamp', default: 'now()' },
22
+ * },
23
+ * });
24
+ * ```
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.camelToSnake = exports.column = exports.ColumnBuilder = void 0;
28
+ exports.defineSchema = defineSchema;
29
+ exports.table = table;
30
+ /** Maps shorthand names to actual Postgres type strings */
31
+ const TYPE_MAP = {
32
+ serial: 'BIGSERIAL',
33
+ bigint: 'BIGINT',
34
+ integer: 'INTEGER',
35
+ smallint: 'SMALLINT',
36
+ text: 'TEXT',
37
+ varchar: 'VARCHAR',
38
+ boolean: 'BOOLEAN',
39
+ timestamp: 'TIMESTAMPTZ',
40
+ date: 'DATE',
41
+ json: 'JSONB',
42
+ uuid: 'UUID',
43
+ real: 'REAL',
44
+ double: 'DOUBLE PRECISION',
45
+ numeric: 'NUMERIC',
46
+ bytea: 'BYTEA',
47
+ };
48
+ /** Convert a user-facing ColumnDef to the internal ColumnConfig */
49
+ function resolveColumn(def) {
50
+ return {
51
+ type: TYPE_MAP[def.type],
52
+ isPrimaryKey: def.primaryKey ?? false,
53
+ isNotNull: def.notNull ?? false,
54
+ isNullable: def.nullable ?? false,
55
+ isUnique: def.unique ?? false,
56
+ defaultValue: def.default ?? null,
57
+ referencesTarget: def.references ?? null,
58
+ maxLength: def.maxLength ?? null,
59
+ };
60
+ }
61
+ /** Check if a value is a TableDef (from legacy table() builder) */
62
+ function isTableDef(v) {
63
+ return typeof v === 'object' && v !== null && 'columns' in v && 'name' in v;
64
+ }
65
+ /**
66
+ * Define the full database schema using plain objects.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * export default defineSchema({
71
+ * users: {
72
+ * id: { type: 'serial', primaryKey: true },
73
+ * email: { type: 'text', unique: true, notNull: true },
74
+ * name: { type: 'text', notNull: true },
75
+ * },
76
+ * posts: {
77
+ * id: { type: 'serial', primaryKey: true },
78
+ * userId: { type: 'bigint', notNull: true, references: 'users.id' },
79
+ * title: { type: 'text', notNull: true },
80
+ * },
81
+ * });
82
+ * ```
83
+ */
84
+ function defineSchema(input) {
85
+ const tables = {};
86
+ for (const [tableName, value] of Object.entries(input)) {
87
+ if (isTableDef(value)) {
88
+ // Legacy format: defineSchema({ users: table({ ... }) })
89
+ value.name = tableName;
90
+ tables[tableName] = value;
91
+ }
92
+ else {
93
+ // Object format: defineSchema({ users: { id: { type: 'serial' }, ... } })
94
+ const columns = {};
95
+ for (const [fieldName, def] of Object.entries(value)) {
96
+ columns[fieldName] = resolveColumn(def);
97
+ }
98
+ tables[tableName] = { name: tableName, columns };
99
+ }
100
+ }
101
+ return { tables };
102
+ }
103
+ // ---------------------------------------------------------------------------
104
+ // Legacy compat — ColumnBuilder still works for existing code
105
+ // ---------------------------------------------------------------------------
106
+ class ColumnBuilder {
107
+ _config;
108
+ constructor() {
109
+ this._config = {
110
+ type: 'TEXT',
111
+ isPrimaryKey: false,
112
+ isNotNull: false,
113
+ isNullable: false,
114
+ isUnique: false,
115
+ defaultValue: null,
116
+ referencesTarget: null,
117
+ maxLength: null,
118
+ };
119
+ }
120
+ serial() { this._config.type = 'BIGSERIAL'; return this; }
121
+ bigint() { this._config.type = 'BIGINT'; return this; }
122
+ integer() { this._config.type = 'INTEGER'; return this; }
123
+ smallint() { this._config.type = 'SMALLINT'; return this; }
124
+ text() { this._config.type = 'TEXT'; return this; }
125
+ varchar(length) { this._config.type = 'VARCHAR'; this._config.maxLength = length; return this; }
126
+ boolean() { this._config.type = 'BOOLEAN'; return this; }
127
+ timestamp() { this._config.type = 'TIMESTAMPTZ'; return this; }
128
+ date() { this._config.type = 'DATE'; return this; }
129
+ json() { this._config.type = 'JSONB'; return this; }
130
+ uuid() { this._config.type = 'UUID'; return this; }
131
+ real() { this._config.type = 'REAL'; return this; }
132
+ doublePrecision() { this._config.type = 'DOUBLE PRECISION'; return this; }
133
+ numeric() { this._config.type = 'NUMERIC'; return this; }
134
+ bytea() { this._config.type = 'BYTEA'; return this; }
135
+ primaryKey() { this._config.isPrimaryKey = true; return this; }
136
+ notNull() { this._config.isNotNull = true; return this; }
137
+ nullable() { this._config.isNullable = true; return this; }
138
+ unique() { this._config.isUnique = true; return this; }
139
+ default(val) { this._config.defaultValue = val; return this; }
140
+ references(target) { this._config.referencesTarget = target; return this; }
141
+ build() { return { ...this._config }; }
142
+ }
143
+ exports.ColumnBuilder = ColumnBuilder;
144
+ /** @deprecated Use defineSchema() with plain objects instead */
145
+ exports.column = new Proxy({}, {
146
+ get(_target, prop) {
147
+ if (prop === 'varchar')
148
+ return (length) => new ColumnBuilder().varchar(length);
149
+ return () => {
150
+ const builder = new ColumnBuilder();
151
+ if (typeof builder[prop] === 'function')
152
+ return builder[prop].call(builder);
153
+ throw new Error(`Unknown column type: ${prop}`);
154
+ };
155
+ },
156
+ });
157
+ /** @deprecated Use defineSchema() with plain objects instead */
158
+ function table(columns) {
159
+ const built = {};
160
+ for (const [fieldName, builder] of Object.entries(columns)) {
161
+ built[fieldName] = builder.build();
162
+ }
163
+ return { name: '', columns: built };
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Helpers
167
+ // ---------------------------------------------------------------------------
168
+ var schema_js_1 = require("./schema.js");
169
+ Object.defineProperty(exports, "camelToSnake", { enumerable: true, get: function () { return schema_js_1.camelToSnake; } });
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+ /**
3
+ * @batadata/turbine — 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 schema_js_1 = require("./schema.js");
18
+ const query_js_1 = require("./query.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 = [
158
+ 'NOW()', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'GEN_RANDOM_UUID()',
159
+ ];
160
+ if (allowedFunctions.includes(upper)) {
161
+ return upper;
162
+ }
163
+ // Numeric literals (integer or decimal, optionally negative)
164
+ if (/^-?\d+(\.\d+)?$/.test(val.trim())) {
165
+ return val.trim();
166
+ }
167
+ // Simple single-quoted string literals (no nested quotes)
168
+ if (/^'[^']*'$/.test(val.trim())) {
169
+ return val.trim();
170
+ }
171
+ throw new Error(`Unsupported default value: ${val}. Use a SQL function, numeric, string literal, or NULL.`);
172
+ }
173
+ /**
174
+ * Generate CREATE INDEX statements for foreign key columns.
175
+ * Only generates indexes for columns that have a REFERENCES clause.
176
+ */
177
+ function generateForeignKeyIndexes(table) {
178
+ const indexes = [];
179
+ for (const [fieldName, config] of Object.entries(table.columns)) {
180
+ if (config.referencesTarget) {
181
+ const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
182
+ const indexName = `idx_${table.name}_${snakeName}`;
183
+ indexes.push(`CREATE INDEX ${(0, query_js_1.quoteIdent)(indexName)} ON ${(0, query_js_1.quoteIdent)(table.name)}(${(0, query_js_1.quoteIdent)(snakeName)});`);
184
+ }
185
+ }
186
+ return indexes;
187
+ }
188
+ /**
189
+ * Compare a SchemaDef against a live Postgres database and return the diff.
190
+ *
191
+ * Connects to the database, inspects the public schema, and computes what
192
+ * DDL is needed to make the database match the schema definition.
193
+ */
194
+ async function schemaDiff(schema, connectionString) {
195
+ const client = new pg_1.default.Client({ connectionString });
196
+ await client.connect();
197
+ try {
198
+ // Get existing tables in the public schema
199
+ const tableResult = await client.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public'`);
200
+ const existingTables = new Set(tableResult.rows.map((r) => r.tablename));
201
+ // Get existing columns for all tables
202
+ const columnResult = await client.query(`SELECT table_name, column_name, data_type, udt_name, is_nullable, column_default, character_maximum_length
203
+ FROM information_schema.columns
204
+ WHERE table_schema = 'public'
205
+ ORDER BY table_name, ordinal_position`);
206
+ const dbColumns = {};
207
+ for (const row of columnResult.rows) {
208
+ if (!dbColumns[row.table_name]) {
209
+ dbColumns[row.table_name] = {};
210
+ }
211
+ dbColumns[row.table_name][row.column_name] = {
212
+ dataType: row.data_type,
213
+ udtName: row.udt_name,
214
+ isNullable: row.is_nullable === 'YES',
215
+ columnDefault: row.column_default,
216
+ maxLength: row.character_maximum_length,
217
+ };
218
+ }
219
+ const schemaTableNames = new Set(Object.keys(schema.tables));
220
+ const result = { create: [], alter: [], drop: [], statements: [] };
221
+ // Tables to create (in schema but not in DB)
222
+ const sorted = topologicalSort(schema);
223
+ for (const tableName of sorted) {
224
+ if (!existingTables.has(tableName)) {
225
+ const tableDef = schema.tables[tableName];
226
+ result.create.push(tableDef);
227
+ result.statements.push(generateCreateTable(tableDef));
228
+ // Also add FK indexes
229
+ result.statements.push(...generateForeignKeyIndexes(tableDef));
230
+ }
231
+ }
232
+ // Tables to drop (in DB but not in schema)
233
+ for (const existingTable of existingTables) {
234
+ if (!schemaTableNames.has(existingTable)) {
235
+ result.drop.push(existingTable);
236
+ // We don't auto-generate DROP statements for safety
237
+ }
238
+ }
239
+ // Tables to alter (exist in both)
240
+ for (const tableName of sorted) {
241
+ if (!existingTables.has(tableName))
242
+ continue;
243
+ const tableDef = schema.tables[tableName];
244
+ const dbCols = dbColumns[tableName] ?? {};
245
+ const alterDef = { table: tableName, columns: [] };
246
+ for (const [fieldName, config] of Object.entries(tableDef.columns)) {
247
+ const snakeName = (0, schema_js_1.camelToSnake)(fieldName);
248
+ const dbCol = dbCols[snakeName];
249
+ if (!dbCol) {
250
+ // Column exists in schema but not in DB — ADD COLUMN
251
+ const colDef = generateColumnDef(fieldName, config);
252
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ADD COLUMN ${colDef};`;
253
+ alterDef.columns.push({ column: snakeName, action: 'add', sql });
254
+ result.statements.push(sql);
255
+ continue;
256
+ }
257
+ // Check type mismatch
258
+ const expectedUdt = schemaTypeToUdt(config);
259
+ if (expectedUdt && dbCol.udtName !== expectedUdt) {
260
+ const sqlType = config.type === 'VARCHAR' && config.maxLength
261
+ ? `VARCHAR(${config.maxLength})`
262
+ : config.type;
263
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} TYPE ${sqlType};`;
264
+ alterDef.columns.push({ column: snakeName, action: 'alter_type', sql });
265
+ result.statements.push(sql);
266
+ }
267
+ // Check NOT NULL mismatch
268
+ const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type === 'BIGSERIAL';
269
+ const isCurrentlyNullable = dbCol.isNullable;
270
+ if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
271
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} SET NOT NULL;`;
272
+ alterDef.columns.push({ column: snakeName, action: 'set_not_null', sql });
273
+ result.statements.push(sql);
274
+ }
275
+ else if (!shouldBeNotNull && !isCurrentlyNullable && config.isNullable) {
276
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} ALTER COLUMN ${(0, query_js_1.quoteIdent)(snakeName)} DROP NOT NULL;`;
277
+ alterDef.columns.push({ column: snakeName, action: 'drop_not_null', sql });
278
+ result.statements.push(sql);
279
+ }
280
+ }
281
+ // Check for columns in DB that are not in schema
282
+ for (const dbColName of Object.keys(dbCols)) {
283
+ const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
284
+ if (!hasField) {
285
+ const sql = `ALTER TABLE ${(0, query_js_1.quoteIdent)(tableName)} DROP COLUMN ${(0, query_js_1.quoteIdent)(dbColName)};`;
286
+ alterDef.columns.push({ column: dbColName, action: 'drop', sql });
287
+ // Don't auto-add drops to statements for safety — user must opt in
288
+ }
289
+ }
290
+ if (alterDef.columns.length > 0) {
291
+ result.alter.push(alterDef);
292
+ }
293
+ }
294
+ return result;
295
+ }
296
+ finally {
297
+ await client.end();
298
+ }
299
+ }
300
+ /**
301
+ * Map a schema column type to its expected PostgreSQL UDT name.
302
+ */
303
+ function schemaTypeToUdt(config) {
304
+ const map = {
305
+ BIGSERIAL: 'int8',
306
+ BIGINT: 'int8',
307
+ INTEGER: 'int4',
308
+ SMALLINT: 'int2',
309
+ TEXT: 'text',
310
+ VARCHAR: 'varchar',
311
+ BOOLEAN: 'bool',
312
+ TIMESTAMPTZ: 'timestamptz',
313
+ DATE: 'date',
314
+ JSONB: 'jsonb',
315
+ UUID: 'uuid',
316
+ REAL: 'float4',
317
+ 'DOUBLE PRECISION': 'float8',
318
+ NUMERIC: 'numeric',
319
+ BYTEA: 'bytea',
320
+ };
321
+ return map[config.type] ?? null;
322
+ }
323
+ /**
324
+ * Push a schema definition to a live database.
325
+ *
326
+ * Computes the diff, then executes the resulting DDL statements in a
327
+ * single transaction. This is a destructive operation for ADD/ALTER —
328
+ * it will NOT drop tables or columns unless explicitly configured.
329
+ */
330
+ async function schemaPush(schema, connectionString, options = {}) {
331
+ const diff = await schemaDiff(schema, connectionString);
332
+ const result = {
333
+ statementsExecuted: 0,
334
+ statements: diff.statements,
335
+ tablesCreated: diff.create.map((t) => t.name),
336
+ tablesAltered: diff.alter.map((a) => a.table),
337
+ };
338
+ if (options.dryRun || diff.statements.length === 0) {
339
+ return result;
340
+ }
341
+ // Execute all statements in a transaction
342
+ const client = new pg_1.default.Client({ connectionString });
343
+ await client.connect();
344
+ try {
345
+ await client.query('BEGIN');
346
+ for (const sql of diff.statements) {
347
+ await client.query(sql);
348
+ result.statementsExecuted++;
349
+ }
350
+ await client.query('COMMIT');
351
+ }
352
+ catch (err) {
353
+ await client.query('ROLLBACK');
354
+ throw err;
355
+ }
356
+ finally {
357
+ await client.end();
358
+ }
359
+ return result;
360
+ }
361
+ // ---------------------------------------------------------------------------
362
+ // Utility: format schema as SQL string (convenience for debugging/printing)
363
+ // ---------------------------------------------------------------------------
364
+ /**
365
+ * Generate the full DDL as a single formatted string.
366
+ * Useful for printing or saving to a .sql file.
367
+ */
368
+ function schemaToSQLString(schema) {
369
+ const statements = schemaToSQL(schema);
370
+ return statements.join('\n\n') + '\n';
371
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * @batadata/turbine — 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: 'number',
24
+ float4: 'number',
25
+ float8: 'number',
26
+ oid: 'number',
27
+ // Precision-sensitive — keep as string to avoid JS float issues
28
+ numeric: 'string',
29
+ money: 'string',
30
+ // Boolean
31
+ bool: 'boolean',
32
+ // Strings
33
+ text: 'string',
34
+ varchar: 'string',
35
+ char: 'string',
36
+ bpchar: 'string',
37
+ name: 'string',
38
+ uuid: 'string',
39
+ citext: 'string',
40
+ xml: 'string',
41
+ // Dates & times
42
+ timestamptz: 'Date',
43
+ timestamp: 'Date',
44
+ date: 'Date',
45
+ time: 'string',
46
+ timetz: 'string',
47
+ interval: 'string',
48
+ // JSON
49
+ json: 'unknown',
50
+ jsonb: 'unknown',
51
+ // Binary
52
+ bytea: 'Buffer',
53
+ // Network
54
+ inet: 'string',
55
+ cidr: 'string',
56
+ macaddr: 'string',
57
+ // Geometric
58
+ point: 'string',
59
+ line: 'string',
60
+ lseg: 'string',
61
+ box: 'string',
62
+ path: 'string',
63
+ polygon: 'string',
64
+ circle: 'string',
65
+ // TSVector
66
+ tsvector: 'string',
67
+ tsquery: 'string',
68
+ };
69
+ const DATE_TYPES = new Set(['timestamptz', 'timestamp', 'date']);
70
+ const PG_TO_ARRAY = {
71
+ int2: 'smallint[]',
72
+ int4: 'integer[]',
73
+ int8: 'bigint[]',
74
+ float4: 'real[]',
75
+ float8: 'double precision[]',
76
+ numeric: 'numeric[]',
77
+ bool: 'boolean[]',
78
+ text: 'text[]',
79
+ varchar: 'text[]',
80
+ char: 'text[]',
81
+ bpchar: 'text[]',
82
+ uuid: 'uuid[]',
83
+ timestamptz: 'timestamptz[]',
84
+ timestamp: 'timestamp[]',
85
+ date: 'date[]',
86
+ json: 'json[]',
87
+ jsonb: 'jsonb[]',
88
+ bytea: 'bytea[]',
89
+ inet: 'inet[]',
90
+ };
91
+ /** Map a Postgres type to its TypeScript equivalent */
92
+ function pgTypeToTs(pgType, nullable) {
93
+ // Array types: udt_name starts with '_'
94
+ if (pgType.startsWith('_')) {
95
+ const elementType = pgTypeToTs(pgType.slice(1), false);
96
+ const tsType = `${elementType}[]`;
97
+ return nullable ? `${tsType} | null` : tsType;
98
+ }
99
+ const tsType = PG_TO_TS[pgType] ?? 'unknown';
100
+ return nullable ? `${tsType} | null` : tsType;
101
+ }
102
+ /** Check if a Postgres type is a date/timestamp that needs Date parsing */
103
+ function isDateType(pgType) {
104
+ return DATE_TYPES.has(pgType);
105
+ }
106
+ /** Get the Postgres array cast type for UNNEST batch inserts */
107
+ function pgArrayType(pgType) {
108
+ return PG_TO_ARRAY[pgType] ?? 'text[]';
109
+ }
110
+ // ---------------------------------------------------------------------------
111
+ // Name conversion utilities
112
+ // ---------------------------------------------------------------------------
113
+ /** snake_case → camelCase */
114
+ function snakeToCamel(s) {
115
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
116
+ }
117
+ /** camelCase → snake_case */
118
+ function camelToSnake(s) {
119
+ return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
120
+ }
121
+ /** snake_case → PascalCase (for type names) */
122
+ function snakeToPascal(s) {
123
+ return s
124
+ .split('_')
125
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
126
+ .join('');
127
+ }
128
+ /** Naive singularize: "posts" → "post", "categories" → "category" */
129
+ function singularize(s) {
130
+ if (s.endsWith('ies'))
131
+ return s.slice(0, -3) + 'y';
132
+ if (s.endsWith('ses') || s.endsWith('xes') || s.endsWith('zes'))
133
+ return s.slice(0, -2);
134
+ if (s.endsWith('s') && !s.endsWith('ss'))
135
+ return s.slice(0, -1);
136
+ return s;
137
+ }