turbine-orm 0.28.3 → 0.30.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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli/index.js +5 -0
  3. package/dist/cjs/cli/mcp.js +22 -92
  4. package/dist/cjs/client.js +69 -5
  5. package/dist/cjs/generate.js +71 -25
  6. package/dist/cjs/index.js +4 -1
  7. package/dist/cjs/introspect.js +350 -120
  8. package/dist/cjs/mssql.js +18 -133
  9. package/dist/cjs/mysql.js +16 -129
  10. package/dist/cjs/optional-peer-import.cjs +122 -0
  11. package/dist/cjs/powdb.js +440 -81
  12. package/dist/cjs/powql.js +49 -25
  13. package/dist/cjs/query/builder.js +290 -23
  14. package/dist/cjs/query/filters.js +32 -1
  15. package/dist/cjs/schema-metadata.js +316 -0
  16. package/dist/cjs/sqlite.js +8 -89
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +5 -0
  19. package/dist/cli/mcp.d.ts +18 -0
  20. package/dist/cli/mcp.js +22 -93
  21. package/dist/client.d.ts +44 -6
  22. package/dist/client.js +69 -5
  23. package/dist/generate.d.ts +16 -4
  24. package/dist/generate.js +71 -25
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +2 -0
  27. package/dist/introspect.d.ts +94 -1
  28. package/dist/introspect.js +345 -120
  29. package/dist/mssql.js +16 -101
  30. package/dist/mysql.js +14 -97
  31. package/dist/optional-peer-import.cjs +89 -0
  32. package/dist/optional-peer-import.d.cts +53 -0
  33. package/dist/powdb.d.ts +94 -26
  34. package/dist/powdb.js +435 -80
  35. package/dist/powql.d.ts +6 -0
  36. package/dist/powql.js +51 -27
  37. package/dist/query/builder.d.ts +60 -3
  38. package/dist/query/builder.js +291 -24
  39. package/dist/query/deferred.d.ts +7 -2
  40. package/dist/query/filters.d.ts +18 -0
  41. package/dist/query/filters.js +30 -0
  42. package/dist/query/types.d.ts +19 -0
  43. package/dist/schema-metadata.d.ts +77 -0
  44. package/dist/schema-metadata.js +313 -0
  45. package/dist/schema.d.ts +10 -0
  46. package/dist/sqlite.js +9 -90
  47. package/package.json +3 -3
@@ -0,0 +1,77 @@
1
+ /**
2
+ * turbine-orm — defineSchema() → SchemaMetadata bridge
3
+ *
4
+ * Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
5
+ * into the runtime {@link SchemaMetadata} shape that the query builder,
6
+ * `TurbineClient`, and the non-SQL engines consume — without touching a
7
+ * live database.
8
+ *
9
+ * Why this exists: the historical converter path (`introspect()` +
10
+ * `generate()`) requires a running SQL database, but code-first engines
11
+ * (PowDB, in-memory SQLite bootstraps, tests) only have the `SchemaDef`.
12
+ * `schemaDefToMetadata()` is the pure-function equivalent: its output
13
+ * matches what `turbine generate` would emit into `metadata.ts` for the
14
+ * same schema, minus the pieces only a live catalog can know (real index
15
+ * names, constraint names, view flags).
16
+ *
17
+ * Parity notes (ground truth = introspect.ts + generate.ts):
18
+ * - Relations are derived from `references:` exactly like introspection
19
+ * derives them from foreign keys: a `belongsTo` on the child table and
20
+ * a `hasMany` on the parent, with the same disambiguation rules when
21
+ * multiple FKs point at the same target.
22
+ * - Pure junction tables (2-column composite PK that IS the two
23
+ * single-column FKs to two distinct tables, no payload columns) get
24
+ * the same conservative auto-`manyToMany` treatment as introspection.
25
+ * - Explicit `manyToMany` declarations on the SchemaDef are merged via
26
+ * {@link applyManyToManyRelations} (additive, never clobbering).
27
+ * - `indexes` is always `[]` — SchemaDef cannot express indexes, and an
28
+ * empty list keeps `schemaHasIndexInfo()` false so the index advisor
29
+ * and the dev-mode missing-index warning stay silent instead of
30
+ * producing blanket false positives.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { defineSchema, schemaDefToMetadata } from 'turbine-orm';
35
+ *
36
+ * const def = defineSchema({
37
+ * users: { id: { type: 'serial', primaryKey: true }, name: { type: 'text', notNull: true } },
38
+ * posts: { id: { type: 'serial', primaryKey: true },
39
+ * userId: { type: 'integer', notNull: true, references: 'users.id' } },
40
+ * });
41
+ * const metadata = schemaDefToMetadata(def);
42
+ * // → usable anywhere SchemaMetadata is expected (e.g. turbinePowDB, TurbineClient)
43
+ * ```
44
+ */
45
+ import { type SchemaMetadata } from './schema.js';
46
+ import { type SchemaDef } from './schema-builder.js';
47
+ /**
48
+ * Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
49
+ *
50
+ * Pure function — no database connection, no side effects, input untouched.
51
+ * The output is shaped identically to the `SCHEMA` constant `turbine generate`
52
+ * emits from introspection, so it can be handed to any consumer that expects
53
+ * introspected metadata: `new TurbineClient(config, metadata)`,
54
+ * `turbinePowDB(..., metadata)`, `QueryInterface`, the index advisor, etc.
55
+ *
56
+ * What maps:
57
+ * - Columns → full {@link ColumnMetadata} (snake_case name, camelCase field,
58
+ * pg type names, TS types, nullability, defaults, `isGenerated` for
59
+ * serial/bigserial, array + varchar length info, date-column tracking).
60
+ * - Column-level `primaryKey` and table-level composite `primaryKey`.
61
+ * - `unique: true` columns → single-column `uniqueColumns` entries.
62
+ * - `references:` FKs → `belongsTo` (child) + `hasMany` (parent) relations,
63
+ * including `onDelete`/`onUpdate` actions (the `'no action'` default is
64
+ * omitted, matching introspection).
65
+ * - Pure junction tables → auto-detected `manyToMany` relations (same
66
+ * conservative rules as introspection).
67
+ * - Explicit `manyToMany` declarations → merged additively.
68
+ * - Schema-level `enums`.
69
+ *
70
+ * What SchemaDef cannot express (and how it degrades):
71
+ * - Indexes → every table gets `indexes: []`, which keeps
72
+ * `schemaHasIndexInfo()` false so index-advisor consumers produce no
73
+ * false positives on code-first metadata.
74
+ * - Views → never marked (`isView` is introspection-only).
75
+ * - Composite foreign keys → `references:` is single-column by design.
76
+ */
77
+ export declare function schemaDefToMetadata(def: SchemaDef): SchemaMetadata;
@@ -0,0 +1,313 @@
1
+ /**
2
+ * turbine-orm — defineSchema() → SchemaMetadata bridge
3
+ *
4
+ * Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
5
+ * into the runtime {@link SchemaMetadata} shape that the query builder,
6
+ * `TurbineClient`, and the non-SQL engines consume — without touching a
7
+ * live database.
8
+ *
9
+ * Why this exists: the historical converter path (`introspect()` +
10
+ * `generate()`) requires a running SQL database, but code-first engines
11
+ * (PowDB, in-memory SQLite bootstraps, tests) only have the `SchemaDef`.
12
+ * `schemaDefToMetadata()` is the pure-function equivalent: its output
13
+ * matches what `turbine generate` would emit into `metadata.ts` for the
14
+ * same schema, minus the pieces only a live catalog can know (real index
15
+ * names, constraint names, view flags).
16
+ *
17
+ * Parity notes (ground truth = introspect.ts + generate.ts):
18
+ * - Relations are derived from `references:` exactly like introspection
19
+ * derives them from foreign keys: a `belongsTo` on the child table and
20
+ * a `hasMany` on the parent, with the same disambiguation rules when
21
+ * multiple FKs point at the same target.
22
+ * - Pure junction tables (2-column composite PK that IS the two
23
+ * single-column FKs to two distinct tables, no payload columns) get
24
+ * the same conservative auto-`manyToMany` treatment as introspection.
25
+ * - Explicit `manyToMany` declarations on the SchemaDef are merged via
26
+ * {@link applyManyToManyRelations} (additive, never clobbering).
27
+ * - `indexes` is always `[]` — SchemaDef cannot express indexes, and an
28
+ * empty list keeps `schemaHasIndexInfo()` false so the index advisor
29
+ * and the dev-mode missing-index warning stay silent instead of
30
+ * producing blanket false positives.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { defineSchema, schemaDefToMetadata } from 'turbine-orm';
35
+ *
36
+ * const def = defineSchema({
37
+ * users: { id: { type: 'serial', primaryKey: true }, name: { type: 'text', notNull: true } },
38
+ * posts: { id: { type: 'serial', primaryKey: true },
39
+ * userId: { type: 'integer', notNull: true, references: 'users.id' } },
40
+ * });
41
+ * const metadata = schemaDefToMetadata(def);
42
+ * // → usable anywhere SchemaMetadata is expected (e.g. turbinePowDB, TurbineClient)
43
+ * ```
44
+ */
45
+ import { addAutoManyToManyRelations, buildRelationsFromForeignKeys, isUnknownTsType, } from './introspect.js';
46
+ import { camelToSnake, isDateType, pgArrayType, pgTypeToTs, } from './schema.js';
47
+ import { applyManyToManyRelations } from './schema-builder.js';
48
+ // ---------------------------------------------------------------------------
49
+ // DDL type → Postgres udt_name (what introspection reads from the catalog)
50
+ // ---------------------------------------------------------------------------
51
+ const DDL_TO_UDT = {
52
+ SERIAL: 'int4',
53
+ BIGSERIAL: 'int8',
54
+ BIGINT: 'int8',
55
+ INTEGER: 'int4',
56
+ SMALLINT: 'int2',
57
+ TEXT: 'text',
58
+ VARCHAR: 'varchar',
59
+ BOOLEAN: 'bool',
60
+ TIMESTAMPTZ: 'timestamptz',
61
+ DATE: 'date',
62
+ JSONB: 'jsonb',
63
+ UUID: 'uuid',
64
+ REAL: 'float4',
65
+ 'DOUBLE PRECISION': 'float8',
66
+ NUMERIC: 'numeric',
67
+ BYTEA: 'bytea',
68
+ };
69
+ /** Resolve a ColumnConfig's Postgres base type name (udt_name form). */
70
+ function udtName(config) {
71
+ if (config.type === 'ENUM')
72
+ return config.enumName ?? 'text';
73
+ if (config.type === 'VECTOR')
74
+ return 'vector';
75
+ return DDL_TO_UDT[config.type];
76
+ }
77
+ /** Server-generated (sequence-backed) types — pg's `nextval(...)` default. */
78
+ function isSerialType(type) {
79
+ return type === 'SERIAL' || type === 'BIGSERIAL';
80
+ }
81
+ /**
82
+ * Resolve the raw column part of a `references: 'table.column'` target to a
83
+ * snake_case column name — accepting either the camelCase field name or the
84
+ * snake_case DDL name, mirroring how schema-sql.ts accepts both table forms.
85
+ */
86
+ function resolveColumnName(raw, target) {
87
+ if (target) {
88
+ const viaField = target.fieldToColumn.get(raw);
89
+ if (viaField !== undefined)
90
+ return viaField;
91
+ if (target.columnNames.has(raw))
92
+ return raw;
93
+ }
94
+ return camelToSnake(raw);
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // The converter
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
101
+ *
102
+ * Pure function — no database connection, no side effects, input untouched.
103
+ * The output is shaped identically to the `SCHEMA` constant `turbine generate`
104
+ * emits from introspection, so it can be handed to any consumer that expects
105
+ * introspected metadata: `new TurbineClient(config, metadata)`,
106
+ * `turbinePowDB(..., metadata)`, `QueryInterface`, the index advisor, etc.
107
+ *
108
+ * What maps:
109
+ * - Columns → full {@link ColumnMetadata} (snake_case name, camelCase field,
110
+ * pg type names, TS types, nullability, defaults, `isGenerated` for
111
+ * serial/bigserial, array + varchar length info, date-column tracking).
112
+ * - Column-level `primaryKey` and table-level composite `primaryKey`.
113
+ * - `unique: true` columns → single-column `uniqueColumns` entries.
114
+ * - `references:` FKs → `belongsTo` (child) + `hasMany` (parent) relations,
115
+ * including `onDelete`/`onUpdate` actions (the `'no action'` default is
116
+ * omitted, matching introspection).
117
+ * - Pure junction tables → auto-detected `manyToMany` relations (same
118
+ * conservative rules as introspection).
119
+ * - Explicit `manyToMany` declarations → merged additively.
120
+ * - Schema-level `enums`.
121
+ *
122
+ * What SchemaDef cannot express (and how it degrades):
123
+ * - Indexes → every table gets `indexes: []`, which keeps
124
+ * `schemaHasIndexInfo()` false so index-advisor consumers produce no
125
+ * false positives on code-first metadata.
126
+ * - Views → never marked (`isView` is introspection-only).
127
+ * - Composite foreign keys → `references:` is single-column by design.
128
+ */
129
+ export function schemaDefToMetadata(def) {
130
+ // ----- Pass 1: resolve every table's snake_case names for FK lookups -----
131
+ // Lookup accepts the accessor key (camelCase), the DDL name (snake_case),
132
+ // and the explicit `accessor` field — same tolerance as schema-sql.ts.
133
+ const lookup = new Map();
134
+ for (const [key, tableDef] of Object.entries(def.tables)) {
135
+ const fieldToColumn = new Map();
136
+ const columnNames = new Set();
137
+ for (const field of Object.keys(tableDef.columns)) {
138
+ const snake = camelToSnake(field);
139
+ fieldToColumn.set(field, snake);
140
+ columnNames.add(snake);
141
+ }
142
+ const resolved = { name: tableDef.name, fieldToColumn, columnNames };
143
+ lookup.set(key, resolved);
144
+ if (tableDef.name)
145
+ lookup.set(tableDef.name, resolved);
146
+ if (tableDef.accessor)
147
+ lookup.set(tableDef.accessor, resolved);
148
+ }
149
+ // ----- Pass 2: collect resolved single-column FKs -----
150
+ const foreignKeys = [];
151
+ for (const tableDef of Object.values(def.tables)) {
152
+ for (const [field, config] of Object.entries(tableDef.columns)) {
153
+ if (!config.referencesTarget)
154
+ continue;
155
+ const parts = config.referencesTarget.split('.');
156
+ if (parts.length !== 2)
157
+ continue;
158
+ const target = lookup.get(parts[0]);
159
+ // Reference to a table outside this SchemaDef — skip, exactly like
160
+ // introspection skips FKs whose target is excluded from the table set.
161
+ if (!target)
162
+ continue;
163
+ foreignKeys.push({
164
+ sourceTable: tableDef.name,
165
+ sourceColumn: camelToSnake(field),
166
+ targetTable: target.name,
167
+ targetColumn: resolveColumnName(parts[1], target),
168
+ onDelete: config.onDelete ?? undefined,
169
+ onUpdate: config.onUpdate ?? undefined,
170
+ });
171
+ }
172
+ }
173
+ // ----- Pass 3: derive belongsTo / hasMany relations (SHARED with introspect)
174
+ //
175
+ // Delegates to the exact same builder introspection uses
176
+ // (`buildRelationsFromForeignKeys`), so the same logical schema yields
177
+ // IDENTICAL relation names whether it arrives via `defineSchema()` or a live
178
+ // catalog: legacy-first naming, per-column disambiguation for several FKs to
179
+ // the same target, and collision resolution against scalar column fields
180
+ // (json/jsonb `unknown`-typed shadows keep the historical name). The old
181
+ // local reimplementation had NO collision guard — `posts.user` (text) +
182
+ // `userId references users.id` produced a relation `user` that shadowed the
183
+ // scalar, and two FKs deriving the same name silently clobbered each other
184
+ // (N-4).
185
+ //
186
+ // Constraint names are synthesized in pg's default `<table>_<column>_fkey`
187
+ // form; they only feed the referential-action lookup and composite-FK
188
+ // naming (never hit here — `references:` is single-column by design).
189
+ const fkEntries = [];
190
+ const fkActions = new Map();
191
+ for (const fk of foreignKeys) {
192
+ const constraintName = `${fk.sourceTable}_${fk.sourceColumn}_fkey`;
193
+ fkEntries.push({
194
+ sourceTable: fk.sourceTable,
195
+ sourceColumns: [fk.sourceColumn],
196
+ targetTable: fk.targetTable,
197
+ targetColumns: [fk.targetColumn],
198
+ constraintName,
199
+ });
200
+ fkActions.set(constraintName, {
201
+ onDelete: fk.onDelete ?? 'no action',
202
+ onUpdate: fk.onUpdate ?? 'no action',
203
+ });
204
+ }
205
+ // Taken-name seeds: every table's camelCase column fields (the SchemaDef
206
+ // keys ARE the fields) + which of them are json/jsonb (`unknown`-typed).
207
+ const columnFieldsByTable = new Map();
208
+ const unknownTypedFieldsByTable = new Map();
209
+ for (const tableDef of Object.values(def.tables)) {
210
+ const fields = Object.keys(tableDef.columns);
211
+ columnFieldsByTable.set(tableDef.name, new Set(fields));
212
+ // ENUM columns map to concrete union types in the generated layer, so
213
+ // only genuine json/jsonb (`unknown`-typed) columns qualify as shadows.
214
+ unknownTypedFieldsByTable.set(tableDef.name, new Set(fields.filter((f) => {
215
+ const config = tableDef.columns[f];
216
+ if (config.type === 'ENUM')
217
+ return false;
218
+ const wire = config.isArray ? `_${udtName(config)}` : udtName(config);
219
+ return isUnknownTsType(pgTypeToTs(wire, false));
220
+ })));
221
+ }
222
+ const relationsByTable = buildRelationsFromForeignKeys(fkEntries, columnFieldsByTable, fkActions, unknownTypedFieldsByTable);
223
+ // ----- Pass 4: conservative junction auto-m2m (SHARED with introspect) ---
224
+ // Same shared detector + naming/collision rules as introspection: a table J
225
+ // is a PURE junction only when its PK is exactly the two single-column FKs
226
+ // to two DISTINCT tables and it has no payload columns.
227
+ const pkByTable = new Map();
228
+ const columnNamesByTable = new Map();
229
+ for (const tableDef of Object.values(def.tables)) {
230
+ const pkFields = tableDef.primaryKey && tableDef.primaryKey.length > 0
231
+ ? [...tableDef.primaryKey]
232
+ : Object.entries(tableDef.columns)
233
+ .filter(([, c]) => c.isPrimaryKey)
234
+ .map(([f]) => f);
235
+ pkByTable.set(tableDef.name, pkFields.map(camelToSnake));
236
+ columnNamesByTable.set(tableDef.name, Object.keys(tableDef.columns).map(camelToSnake));
237
+ }
238
+ addAutoManyToManyRelations(Object.values(def.tables).map((t) => t.name), fkEntries, pkByTable, columnNamesByTable, relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable);
239
+ // ----- Pass 5: assemble TableMetadata -----
240
+ const tables = {};
241
+ for (const tableDef of Object.values(def.tables)) {
242
+ const columns = [];
243
+ const columnMap = {};
244
+ const reverseColumnMap = {};
245
+ const dateColumns = new Set();
246
+ const dialectTypes = {};
247
+ const pgTypes = {};
248
+ const allColumns = [];
249
+ const primaryKey = [];
250
+ const uniqueColumns = [];
251
+ for (const [field, config] of Object.entries(tableDef.columns)) {
252
+ const name = camelToSnake(field);
253
+ const base = udtName(config);
254
+ // Introspection reports array columns with pg's leading-underscore
255
+ // udt_name (`_text`), which pgTypeToTs also understands.
256
+ const wireType = config.isArray ? `_${base}` : base;
257
+ const serial = isSerialType(config.type);
258
+ // PK members and serials are NOT NULL in Postgres regardless of flags.
259
+ const nullable = !(config.isPrimaryKey || config.isNotNull || serial);
260
+ const col = {
261
+ name,
262
+ field,
263
+ dialectType: wireType,
264
+ pgType: wireType,
265
+ tsType: pgTypeToTs(wireType, nullable),
266
+ nullable,
267
+ hasDefault: config.defaultValue != null || serial,
268
+ isArray: config.isArray,
269
+ arrayType: pgArrayType(base),
270
+ pgArrayType: pgArrayType(base),
271
+ ...(serial ? { isGenerated: true } : {}),
272
+ ...(config.maxLength != null ? { maxLength: config.maxLength } : {}),
273
+ };
274
+ columns.push(col);
275
+ columnMap[field] = name;
276
+ reverseColumnMap[name] = field;
277
+ allColumns.push(name);
278
+ dialectTypes[name] = wireType;
279
+ pgTypes[name] = wireType;
280
+ if (isDateType(base))
281
+ dateColumns.add(name);
282
+ if (config.isPrimaryKey)
283
+ primaryKey.push(name);
284
+ if (config.isUnique)
285
+ uniqueColumns.push([name]);
286
+ }
287
+ // Table-level composite PK takes precedence (defineSchema already cleared
288
+ // the column-level flags for its members).
289
+ const pk = tableDef.primaryKey && tableDef.primaryKey.length > 0 ? tableDef.primaryKey.map(camelToSnake) : primaryKey;
290
+ tables[tableDef.name] = {
291
+ name: tableDef.name,
292
+ columns,
293
+ columnMap,
294
+ reverseColumnMap,
295
+ dateColumns,
296
+ dialectTypes,
297
+ pgTypes,
298
+ allColumns,
299
+ primaryKey: pk,
300
+ uniqueColumns,
301
+ relations: relationsByTable.get(tableDef.name) ?? {},
302
+ // SchemaDef cannot express indexes. An empty list keeps
303
+ // schemaHasIndexInfo() false → no index-advisor false positives.
304
+ indexes: [],
305
+ };
306
+ }
307
+ const enums = {};
308
+ for (const [name, labels] of Object.entries(def.enums ?? {})) {
309
+ enums[name] = [...labels];
310
+ }
311
+ // ----- Pass 6: merge explicit manyToMany declarations (additive) ---------
312
+ return applyManyToManyRelations({ tables, enums }, def);
313
+ }
package/dist/schema.d.ts CHANGED
@@ -67,6 +67,16 @@ export interface ColumnMetadata {
67
67
  dialectType?: string;
68
68
  /** Postgres base type (e.g. 'int8', 'text', 'timestamptz'). Back-compat alias for dialectType. */
69
69
  pgType: string;
70
+ /**
71
+ * Schema the column's Postgres type lives in, recorded by introspection
72
+ * ONLY when it differs from the introspected schema (and isn't a
73
+ * `pg_catalog` builtin) — e.g. an enum or domain owned by another schema.
74
+ * Consumers use it as a cross-schema guard: a same-named enum in another
75
+ * schema must not receive this schema's `::"enum"` cast (search_path would
76
+ * resolve it to the wrong type). Absent for same-schema types, builtins,
77
+ * `defineSchema()` output, and legacy generated metadata.
78
+ */
79
+ pgTypeSchema?: string;
70
80
  /** TypeScript type string (e.g. 'number', 'string', 'Date') */
71
81
  tsType: string;
72
82
  /** Whether the column allows NULL */
package/dist/sqlite.js CHANGED
@@ -49,7 +49,8 @@ import { createRequire } from 'node:module';
49
49
  import { TurbineClient } from './client.js';
50
50
  import { postgresDialect, } from './dialect.js';
51
51
  import { ConnectionError } from './errors.js';
52
- import { isDateType, singularize, snakeToCamel, } from './schema.js';
52
+ import { deriveEngineRelations } from './introspect.js';
53
+ import { isDateType, snakeToCamel, } from './schema.js';
53
54
  let cachedDatabaseSync;
54
55
  /**
55
56
  * Lazily load `node:sqlite`'s `DatabaseSync` constructor.
@@ -637,95 +638,13 @@ export function introspectSqliteDatabase(db, options = {}) {
637
638
  indexesByTable.set(tableName, idxMeta);
638
639
  uniqueByTable.set(tableName, uniques);
639
640
  }
640
- // ----- Build relations from foreign keys (belongsTo + hasMany) -----
641
- const fkCounts = new Map();
642
- for (const fk of foreignKeys) {
643
- const key = `${fk.sourceTable}->${fk.targetTable}`;
644
- fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
645
- }
646
- const relationsByTable = new Map();
647
- for (const fk of foreignKeys) {
648
- const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
649
- const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
650
- const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
651
- const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
652
- ? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
653
- : singularize(snakeToCamel(fk.targetTable));
654
- if (!relationsByTable.has(fk.sourceTable))
655
- relationsByTable.set(fk.sourceTable, {});
656
- relationsByTable.get(fk.sourceTable)[belongsToName] = {
657
- type: 'belongsTo',
658
- name: belongsToName,
659
- from: fk.sourceTable,
660
- to: fk.targetTable,
661
- foreignKey,
662
- referenceKey,
663
- };
664
- const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
665
- ? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
666
- : snakeToCamel(fk.sourceTable);
667
- if (!relationsByTable.has(fk.targetTable))
668
- relationsByTable.set(fk.targetTable, {});
669
- relationsByTable.get(fk.targetTable)[hasManyName] = {
670
- type: 'hasMany',
671
- name: hasManyName,
672
- from: fk.targetTable,
673
- to: fk.sourceTable,
674
- foreignKey,
675
- referenceKey,
676
- };
677
- }
678
- // ----- Conservative many-to-many auto-detection (additive) -----
679
- // A table J is a pure junction iff: PK is exactly two columns, exactly two
680
- // single-column FKs whose source columns ARE the PK, two distinct target
681
- // tables, and no payload columns. Mirrors the Postgres introspector.
682
- for (const tableName of tableNames) {
683
- const pk = pkByTable.get(tableName) ?? [];
684
- if (pk.length !== 2)
685
- continue;
686
- const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
687
- if (tableFks.length !== 2)
688
- continue;
689
- if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
690
- continue;
691
- const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
692
- const pkSet = new Set(pk);
693
- if (!fkCols.every((c) => pkSet.has(c)))
694
- continue;
695
- if (new Set(fkCols).size !== 2)
696
- continue;
697
- const [fkA, fkB] = tableFks;
698
- if (fkA.targetTable === fkB.targetTable)
699
- continue;
700
- const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
701
- if (jCols.length !== 2)
702
- continue;
703
- const addM2M = (self, other) => {
704
- const sourceTbl = self.targetTable;
705
- const targetTbl = other.targetTable;
706
- const relName = snakeToCamel(targetTbl);
707
- if (!relationsByTable.has(sourceTbl))
708
- relationsByTable.set(sourceTbl, {});
709
- const existing = relationsByTable.get(sourceTbl);
710
- if (existing[relName])
711
- return;
712
- existing[relName] = {
713
- type: 'manyToMany',
714
- name: relName,
715
- from: sourceTbl,
716
- to: targetTbl,
717
- referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
718
- foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
719
- through: {
720
- table: tableName,
721
- sourceKey: self.sourceColumns[0],
722
- targetKey: other.sourceColumns[0],
723
- },
724
- };
725
- };
726
- addM2M(fkA, fkB);
727
- addM2M(fkB, fkA);
728
- }
641
+ // ----- Build relations from foreign keys (belongsTo + hasMany + m2m) -----
642
+ // Delegated to the SHARED introspection pipeline (introspect.ts) so SQLite
643
+ // derives IDENTICAL relation names to the Postgres introspector for the
644
+ // same logical schema — legacy-first naming, per-column disambiguation,
645
+ // collision resolution against scalar column fields, and the conservative
646
+ // pure-junction manyToMany auto-detection included.
647
+ const relationsByTable = deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
729
648
  // ----- Assemble TableMetadata -----
730
649
  const tables = {};
731
650
  for (const tableName of tableNames) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.28.3",
3
+ "version": "0.30.0",
4
4
  "description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "exports": {
@@ -114,8 +114,8 @@
114
114
  "typescript": "^6.0.3"
115
115
  },
116
116
  "peerDependencies": {
117
- "@zvndev/powdb-client": "^0.7.1 || ^0.8.0",
118
- "@zvndev/powdb-embedded": "^0.7.1 || ^0.8.0",
117
+ "@zvndev/powdb-client": ">=0.7.1 <1.0.0",
118
+ "@zvndev/powdb-embedded": ">=0.7.1 <1.0.0",
119
119
  "mssql": "^10.0.0 || ^11.0.0 || ^12.0.0",
120
120
  "mysql2": "^3.0.0"
121
121
  },