turbine-orm 0.29.0 → 0.31.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.
- package/README.md +1 -1
- package/dist/cjs/cli/index.js +5 -0
- package/dist/cjs/cli/mcp.js +22 -92
- package/dist/cjs/client.js +47 -6
- package/dist/cjs/generate.js +71 -25
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +350 -120
- package/dist/cjs/mssql.js +42 -136
- package/dist/cjs/mysql.js +16 -129
- package/dist/cjs/optional-peer-import.cjs +122 -0
- package/dist/cjs/powdb.js +579 -89
- package/dist/cjs/powql.js +56 -26
- package/dist/cjs/query/builder.js +601 -86
- package/dist/cjs/query/filters.js +80 -2
- package/dist/cjs/schema-metadata.js +316 -0
- package/dist/cjs/sqlite.js +8 -89
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/mcp.d.ts +18 -0
- package/dist/cli/mcp.js +22 -93
- package/dist/client.d.ts +19 -2
- package/dist/client.js +47 -6
- package/dist/generate.d.ts +16 -4
- package/dist/generate.js +71 -25
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +94 -1
- package/dist/introspect.js +345 -120
- package/dist/mssql.js +40 -104
- package/dist/mysql.js +14 -97
- package/dist/optional-peer-import.cjs +89 -0
- package/dist/optional-peer-import.d.cts +53 -0
- package/dist/powdb.d.ts +118 -23
- package/dist/powdb.js +574 -88
- package/dist/powql.d.ts +6 -0
- package/dist/powql.js +58 -28
- package/dist/query/builder.d.ts +145 -8
- package/dist/query/builder.js +602 -87
- package/dist/query/deferred.d.ts +7 -2
- package/dist/query/filters.d.ts +46 -1
- package/dist/query/filters.js +76 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +85 -11
- package/dist/schema-metadata.d.ts +77 -0
- package/dist/schema-metadata.js +313 -0
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +9 -90
- package/package.json +3 -3
|
@@ -7,13 +7,15 @@
|
|
|
7
7
|
* and execution rather than filter-shape bookkeeping.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.VECTOR_DISTANCE_COMPARATORS = exports.VECTOR_METRIC_OPERATORS = exports.TEXT_SEARCH_KEYS = exports.ARRAY_UNIQUE_KEYS = exports.ARRAY_OPERATOR_KEYS = exports.JSONB_UNIQUE_KEYS = exports.JSONB_OPERATOR_KEYS = exports.UPDATE_OPERATOR_KEYS = void 0;
|
|
10
|
+
exports.VECTOR_DISTANCE_COMPARATORS = exports.VECTOR_METRIC_OPERATORS = exports.TEXT_SEARCH_KEYS = exports.ARRAY_UNIQUE_KEYS = exports.ARRAY_OPERATOR_KEYS = exports.JSONB_UNIQUE_KEYS = exports.JSON_RANGE_OPERATORS = exports.JSONB_OPERATOR_KEYS = exports.UPDATE_OPERATOR_KEYS = exports.COLUMN_REF_OPERATORS = void 0;
|
|
11
11
|
exports.isWhereOperator = isWhereOperator;
|
|
12
12
|
exports.isUnmatchedPlainObject = isUnmatchedPlainObject;
|
|
13
|
+
exports.isColumnRef = isColumnRef;
|
|
13
14
|
exports.fingerprintOperatorShape = fingerprintOperatorShape;
|
|
14
15
|
exports.assertBindableEqualsOperand = assertBindableEqualsOperand;
|
|
15
16
|
exports.sortedKeys = sortedKeys;
|
|
16
17
|
exports.sortedEntries = sortedEntries;
|
|
18
|
+
exports.fingerprintJsonFilterShape = fingerprintJsonFilterShape;
|
|
17
19
|
exports.isJsonFilter = isJsonFilter;
|
|
18
20
|
exports.findJsonUniqueKey = findJsonUniqueKey;
|
|
19
21
|
exports.isArrayFilter = isArrayFilter;
|
|
@@ -23,6 +25,7 @@ exports.validateTextSearchConfig = validateTextSearchConfig;
|
|
|
23
25
|
exports.isVectorFilter = isVectorFilter;
|
|
24
26
|
exports.isVectorOrderBy = isVectorOrderBy;
|
|
25
27
|
exports.isOrderBySpec = isOrderBySpec;
|
|
28
|
+
exports.isJsonPathOrderBy = isJsonPathOrderBy;
|
|
26
29
|
exports.normalizeOrderBy = normalizeOrderBy;
|
|
27
30
|
const errors_js_1 = require("../errors.js");
|
|
28
31
|
const utils_js_1 = require("./utils.js");
|
|
@@ -55,17 +58,48 @@ function isUnmatchedPlainObject(value) {
|
|
|
55
58
|
const proto = Object.getPrototypeOf(value);
|
|
56
59
|
return proto === Object.prototype || proto === null;
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Operator keys that accept a {@link ColumnRef} (`{ col: 'otherField' }`)
|
|
63
|
+
* value for column-to-column comparison. `in`/`notIn` and the LIKE operators
|
|
64
|
+
* take values only.
|
|
65
|
+
*/
|
|
66
|
+
exports.COLUMN_REF_OPERATORS = new Set(['equals', 'not', 'gt', 'gte', 'lt', 'lte']);
|
|
67
|
+
/**
|
|
68
|
+
* Check if an operator value is a column reference: a plain object whose ONLY
|
|
69
|
+
* key is `col` with a string value. Anything else (extra keys, non-string
|
|
70
|
+
* `col`) is treated as a plain value so JSON payloads that merely contain a
|
|
71
|
+
* `col` property keep their equality meaning.
|
|
72
|
+
*/
|
|
73
|
+
function isColumnRef(value) {
|
|
74
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
|
|
75
|
+
return false;
|
|
76
|
+
const keys = Object.keys(value);
|
|
77
|
+
return keys.length === 1 && keys[0] === 'col' && typeof value.col === 'string';
|
|
78
|
+
}
|
|
58
79
|
/**
|
|
59
80
|
* Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
|
|
60
81
|
* `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
|
|
61
82
|
* param pushed), so null-ness is part of the shape — without it a cache entry
|
|
62
83
|
* warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
|
|
84
|
+
*
|
|
85
|
+
* Column references ({@link ColumnRef}) compile the referenced column into the
|
|
86
|
+
* SQL TEXT (no param bound), so the referenced field name is part of the shape
|
|
87
|
+
*: `{ equals: { col: 'a' } }` and `{ equals: { col: 'b' } }` must never share
|
|
88
|
+
* a cache entry. The name is JSON-encoded so exotic field names cannot collide
|
|
89
|
+
* with other fingerprint tokens.
|
|
63
90
|
*/
|
|
64
91
|
function fingerprintOperatorShape(value) {
|
|
65
92
|
const obj = value;
|
|
66
93
|
const opKeys = Object.keys(obj)
|
|
67
94
|
.filter((k) => k !== 'mode')
|
|
68
|
-
.map((k) =>
|
|
95
|
+
.map((k) => {
|
|
96
|
+
const v = obj[k];
|
|
97
|
+
if ((k === 'equals' || k === 'not') && v === null)
|
|
98
|
+
return `${k}:null`;
|
|
99
|
+
if (exports.COLUMN_REF_OPERATORS.has(k) && isColumnRef(v))
|
|
100
|
+
return `${k}:col(${JSON.stringify(v.col)})`;
|
|
101
|
+
return k;
|
|
102
|
+
})
|
|
69
103
|
.sort();
|
|
70
104
|
const modeStr = value.mode === 'insensitive' ? ':i' : '';
|
|
71
105
|
return `op(${opKeys.join(',')}${modeStr})`;
|
|
@@ -109,6 +143,36 @@ function sortedEntries(obj) {
|
|
|
109
143
|
exports.UPDATE_OPERATOR_KEYS = new Set(['set', 'increment', 'decrement', 'multiply', 'divide']);
|
|
110
144
|
/** Known JSONB operator keys */
|
|
111
145
|
exports.JSONB_OPERATOR_KEYS = new Set(['path', 'equals', 'contains', 'hasKey']);
|
|
146
|
+
/**
|
|
147
|
+
* JSON range comparison operators → SQL comparison tokens, in the FIXED order
|
|
148
|
+
* the build and collect paths iterate them. These keys are deliberately NOT in
|
|
149
|
+
* {@link JSONB_OPERATOR_KEYS}: `gt`/`gte`/`lt`/`lte` overlap with
|
|
150
|
+
* `WhereOperator`, so a bare `{ gt: 5 }` must keep its column-comparison
|
|
151
|
+
* meaning. They only compile as JSON range ops when the object is already a
|
|
152
|
+
* {@link JsonFilter} (detected via `path` / `equals` / `contains` / `hasKey`),
|
|
153
|
+
* and they always require `path`.
|
|
154
|
+
*/
|
|
155
|
+
exports.JSON_RANGE_OPERATORS = {
|
|
156
|
+
gt: '>',
|
|
157
|
+
gte: '>=',
|
|
158
|
+
lt: '<',
|
|
159
|
+
lte: '<=',
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Value-invariant shape fingerprint for a {@link JsonFilter}. Range operators
|
|
163
|
+
* are annotated with the comparison value's kind (`#n` numeric / `#s` string)
|
|
164
|
+
* because a numeric comparison compiles to a `::numeric` cast — a different
|
|
165
|
+
* SQL text than the text comparison — so the two must never share a cached
|
|
166
|
+
* SQL entry.
|
|
167
|
+
*/
|
|
168
|
+
function fingerprintJsonFilterShape(filter) {
|
|
169
|
+
const obj = filter;
|
|
170
|
+
const parts = Object.keys(obj)
|
|
171
|
+
.filter((k) => obj[k] !== undefined)
|
|
172
|
+
.sort()
|
|
173
|
+
.map((k) => (k in exports.JSON_RANGE_OPERATORS ? `${k}#${typeof obj[k] === 'number' ? 'n' : 's'}` : k));
|
|
174
|
+
return `json(${parts.join(',')})`;
|
|
175
|
+
}
|
|
112
176
|
/**
|
|
113
177
|
* JSONB operator keys that are *unique* to {@link JsonFilter} — they cannot
|
|
114
178
|
* appear in any other where-filter shape, so the presence of one of these is
|
|
@@ -238,6 +302,20 @@ function isVectorOrderBy(value) {
|
|
|
238
302
|
function isOrderBySpec(value) {
|
|
239
303
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'sort' in value;
|
|
240
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Check if an orderBy value is a JSON-path ordering: `{ path: [...] }` with an
|
|
307
|
+
* ARRAY path. The array requirement disambiguates from relation orderBy values
|
|
308
|
+
* (whose entries are directions/specs keyed by target column: a target column
|
|
309
|
+
* literally named `path` maps to a string direction, never an array), and the
|
|
310
|
+
* `distance`/`sort` exclusions keep vector and spec shapes out.
|
|
311
|
+
*/
|
|
312
|
+
function isJsonPathOrderBy(value) {
|
|
313
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
314
|
+
return false;
|
|
315
|
+
if ('distance' in value || 'sort' in value)
|
|
316
|
+
return false;
|
|
317
|
+
return Array.isArray(value.path);
|
|
318
|
+
}
|
|
241
319
|
/**
|
|
242
320
|
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
243
321
|
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm — defineSchema() → SchemaMetadata bridge
|
|
4
|
+
*
|
|
5
|
+
* Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
|
|
6
|
+
* into the runtime {@link SchemaMetadata} shape that the query builder,
|
|
7
|
+
* `TurbineClient`, and the non-SQL engines consume — without touching a
|
|
8
|
+
* live database.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: the historical converter path (`introspect()` +
|
|
11
|
+
* `generate()`) requires a running SQL database, but code-first engines
|
|
12
|
+
* (PowDB, in-memory SQLite bootstraps, tests) only have the `SchemaDef`.
|
|
13
|
+
* `schemaDefToMetadata()` is the pure-function equivalent: its output
|
|
14
|
+
* matches what `turbine generate` would emit into `metadata.ts` for the
|
|
15
|
+
* same schema, minus the pieces only a live catalog can know (real index
|
|
16
|
+
* names, constraint names, view flags).
|
|
17
|
+
*
|
|
18
|
+
* Parity notes (ground truth = introspect.ts + generate.ts):
|
|
19
|
+
* - Relations are derived from `references:` exactly like introspection
|
|
20
|
+
* derives them from foreign keys: a `belongsTo` on the child table and
|
|
21
|
+
* a `hasMany` on the parent, with the same disambiguation rules when
|
|
22
|
+
* multiple FKs point at the same target.
|
|
23
|
+
* - Pure junction tables (2-column composite PK that IS the two
|
|
24
|
+
* single-column FKs to two distinct tables, no payload columns) get
|
|
25
|
+
* the same conservative auto-`manyToMany` treatment as introspection.
|
|
26
|
+
* - Explicit `manyToMany` declarations on the SchemaDef are merged via
|
|
27
|
+
* {@link applyManyToManyRelations} (additive, never clobbering).
|
|
28
|
+
* - `indexes` is always `[]` — SchemaDef cannot express indexes, and an
|
|
29
|
+
* empty list keeps `schemaHasIndexInfo()` false so the index advisor
|
|
30
|
+
* and the dev-mode missing-index warning stay silent instead of
|
|
31
|
+
* producing blanket false positives.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { defineSchema, schemaDefToMetadata } from 'turbine-orm';
|
|
36
|
+
*
|
|
37
|
+
* const def = defineSchema({
|
|
38
|
+
* users: { id: { type: 'serial', primaryKey: true }, name: { type: 'text', notNull: true } },
|
|
39
|
+
* posts: { id: { type: 'serial', primaryKey: true },
|
|
40
|
+
* userId: { type: 'integer', notNull: true, references: 'users.id' } },
|
|
41
|
+
* });
|
|
42
|
+
* const metadata = schemaDefToMetadata(def);
|
|
43
|
+
* // → usable anywhere SchemaMetadata is expected (e.g. turbinePowDB, TurbineClient)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.schemaDefToMetadata = schemaDefToMetadata;
|
|
48
|
+
const introspect_js_1 = require("./introspect.js");
|
|
49
|
+
const schema_js_1 = require("./schema.js");
|
|
50
|
+
const schema_builder_js_1 = require("./schema-builder.js");
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// DDL type → Postgres udt_name (what introspection reads from the catalog)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const DDL_TO_UDT = {
|
|
55
|
+
SERIAL: 'int4',
|
|
56
|
+
BIGSERIAL: 'int8',
|
|
57
|
+
BIGINT: 'int8',
|
|
58
|
+
INTEGER: 'int4',
|
|
59
|
+
SMALLINT: 'int2',
|
|
60
|
+
TEXT: 'text',
|
|
61
|
+
VARCHAR: 'varchar',
|
|
62
|
+
BOOLEAN: 'bool',
|
|
63
|
+
TIMESTAMPTZ: 'timestamptz',
|
|
64
|
+
DATE: 'date',
|
|
65
|
+
JSONB: 'jsonb',
|
|
66
|
+
UUID: 'uuid',
|
|
67
|
+
REAL: 'float4',
|
|
68
|
+
'DOUBLE PRECISION': 'float8',
|
|
69
|
+
NUMERIC: 'numeric',
|
|
70
|
+
BYTEA: 'bytea',
|
|
71
|
+
};
|
|
72
|
+
/** Resolve a ColumnConfig's Postgres base type name (udt_name form). */
|
|
73
|
+
function udtName(config) {
|
|
74
|
+
if (config.type === 'ENUM')
|
|
75
|
+
return config.enumName ?? 'text';
|
|
76
|
+
if (config.type === 'VECTOR')
|
|
77
|
+
return 'vector';
|
|
78
|
+
return DDL_TO_UDT[config.type];
|
|
79
|
+
}
|
|
80
|
+
/** Server-generated (sequence-backed) types — pg's `nextval(...)` default. */
|
|
81
|
+
function isSerialType(type) {
|
|
82
|
+
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the raw column part of a `references: 'table.column'` target to a
|
|
86
|
+
* snake_case column name — accepting either the camelCase field name or the
|
|
87
|
+
* snake_case DDL name, mirroring how schema-sql.ts accepts both table forms.
|
|
88
|
+
*/
|
|
89
|
+
function resolveColumnName(raw, target) {
|
|
90
|
+
if (target) {
|
|
91
|
+
const viaField = target.fieldToColumn.get(raw);
|
|
92
|
+
if (viaField !== undefined)
|
|
93
|
+
return viaField;
|
|
94
|
+
if (target.columnNames.has(raw))
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
return (0, schema_js_1.camelToSnake)(raw);
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// The converter
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
|
|
104
|
+
*
|
|
105
|
+
* Pure function — no database connection, no side effects, input untouched.
|
|
106
|
+
* The output is shaped identically to the `SCHEMA` constant `turbine generate`
|
|
107
|
+
* emits from introspection, so it can be handed to any consumer that expects
|
|
108
|
+
* introspected metadata: `new TurbineClient(config, metadata)`,
|
|
109
|
+
* `turbinePowDB(..., metadata)`, `QueryInterface`, the index advisor, etc.
|
|
110
|
+
*
|
|
111
|
+
* What maps:
|
|
112
|
+
* - Columns → full {@link ColumnMetadata} (snake_case name, camelCase field,
|
|
113
|
+
* pg type names, TS types, nullability, defaults, `isGenerated` for
|
|
114
|
+
* serial/bigserial, array + varchar length info, date-column tracking).
|
|
115
|
+
* - Column-level `primaryKey` and table-level composite `primaryKey`.
|
|
116
|
+
* - `unique: true` columns → single-column `uniqueColumns` entries.
|
|
117
|
+
* - `references:` FKs → `belongsTo` (child) + `hasMany` (parent) relations,
|
|
118
|
+
* including `onDelete`/`onUpdate` actions (the `'no action'` default is
|
|
119
|
+
* omitted, matching introspection).
|
|
120
|
+
* - Pure junction tables → auto-detected `manyToMany` relations (same
|
|
121
|
+
* conservative rules as introspection).
|
|
122
|
+
* - Explicit `manyToMany` declarations → merged additively.
|
|
123
|
+
* - Schema-level `enums`.
|
|
124
|
+
*
|
|
125
|
+
* What SchemaDef cannot express (and how it degrades):
|
|
126
|
+
* - Indexes → every table gets `indexes: []`, which keeps
|
|
127
|
+
* `schemaHasIndexInfo()` false so index-advisor consumers produce no
|
|
128
|
+
* false positives on code-first metadata.
|
|
129
|
+
* - Views → never marked (`isView` is introspection-only).
|
|
130
|
+
* - Composite foreign keys → `references:` is single-column by design.
|
|
131
|
+
*/
|
|
132
|
+
function schemaDefToMetadata(def) {
|
|
133
|
+
// ----- Pass 1: resolve every table's snake_case names for FK lookups -----
|
|
134
|
+
// Lookup accepts the accessor key (camelCase), the DDL name (snake_case),
|
|
135
|
+
// and the explicit `accessor` field — same tolerance as schema-sql.ts.
|
|
136
|
+
const lookup = new Map();
|
|
137
|
+
for (const [key, tableDef] of Object.entries(def.tables)) {
|
|
138
|
+
const fieldToColumn = new Map();
|
|
139
|
+
const columnNames = new Set();
|
|
140
|
+
for (const field of Object.keys(tableDef.columns)) {
|
|
141
|
+
const snake = (0, schema_js_1.camelToSnake)(field);
|
|
142
|
+
fieldToColumn.set(field, snake);
|
|
143
|
+
columnNames.add(snake);
|
|
144
|
+
}
|
|
145
|
+
const resolved = { name: tableDef.name, fieldToColumn, columnNames };
|
|
146
|
+
lookup.set(key, resolved);
|
|
147
|
+
if (tableDef.name)
|
|
148
|
+
lookup.set(tableDef.name, resolved);
|
|
149
|
+
if (tableDef.accessor)
|
|
150
|
+
lookup.set(tableDef.accessor, resolved);
|
|
151
|
+
}
|
|
152
|
+
// ----- Pass 2: collect resolved single-column FKs -----
|
|
153
|
+
const foreignKeys = [];
|
|
154
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
155
|
+
for (const [field, config] of Object.entries(tableDef.columns)) {
|
|
156
|
+
if (!config.referencesTarget)
|
|
157
|
+
continue;
|
|
158
|
+
const parts = config.referencesTarget.split('.');
|
|
159
|
+
if (parts.length !== 2)
|
|
160
|
+
continue;
|
|
161
|
+
const target = lookup.get(parts[0]);
|
|
162
|
+
// Reference to a table outside this SchemaDef — skip, exactly like
|
|
163
|
+
// introspection skips FKs whose target is excluded from the table set.
|
|
164
|
+
if (!target)
|
|
165
|
+
continue;
|
|
166
|
+
foreignKeys.push({
|
|
167
|
+
sourceTable: tableDef.name,
|
|
168
|
+
sourceColumn: (0, schema_js_1.camelToSnake)(field),
|
|
169
|
+
targetTable: target.name,
|
|
170
|
+
targetColumn: resolveColumnName(parts[1], target),
|
|
171
|
+
onDelete: config.onDelete ?? undefined,
|
|
172
|
+
onUpdate: config.onUpdate ?? undefined,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ----- Pass 3: derive belongsTo / hasMany relations (SHARED with introspect)
|
|
177
|
+
//
|
|
178
|
+
// Delegates to the exact same builder introspection uses
|
|
179
|
+
// (`buildRelationsFromForeignKeys`), so the same logical schema yields
|
|
180
|
+
// IDENTICAL relation names whether it arrives via `defineSchema()` or a live
|
|
181
|
+
// catalog: legacy-first naming, per-column disambiguation for several FKs to
|
|
182
|
+
// the same target, and collision resolution against scalar column fields
|
|
183
|
+
// (json/jsonb `unknown`-typed shadows keep the historical name). The old
|
|
184
|
+
// local reimplementation had NO collision guard — `posts.user` (text) +
|
|
185
|
+
// `userId references users.id` produced a relation `user` that shadowed the
|
|
186
|
+
// scalar, and two FKs deriving the same name silently clobbered each other
|
|
187
|
+
// (N-4).
|
|
188
|
+
//
|
|
189
|
+
// Constraint names are synthesized in pg's default `<table>_<column>_fkey`
|
|
190
|
+
// form; they only feed the referential-action lookup and composite-FK
|
|
191
|
+
// naming (never hit here — `references:` is single-column by design).
|
|
192
|
+
const fkEntries = [];
|
|
193
|
+
const fkActions = new Map();
|
|
194
|
+
for (const fk of foreignKeys) {
|
|
195
|
+
const constraintName = `${fk.sourceTable}_${fk.sourceColumn}_fkey`;
|
|
196
|
+
fkEntries.push({
|
|
197
|
+
sourceTable: fk.sourceTable,
|
|
198
|
+
sourceColumns: [fk.sourceColumn],
|
|
199
|
+
targetTable: fk.targetTable,
|
|
200
|
+
targetColumns: [fk.targetColumn],
|
|
201
|
+
constraintName,
|
|
202
|
+
});
|
|
203
|
+
fkActions.set(constraintName, {
|
|
204
|
+
onDelete: fk.onDelete ?? 'no action',
|
|
205
|
+
onUpdate: fk.onUpdate ?? 'no action',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Taken-name seeds: every table's camelCase column fields (the SchemaDef
|
|
209
|
+
// keys ARE the fields) + which of them are json/jsonb (`unknown`-typed).
|
|
210
|
+
const columnFieldsByTable = new Map();
|
|
211
|
+
const unknownTypedFieldsByTable = new Map();
|
|
212
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
213
|
+
const fields = Object.keys(tableDef.columns);
|
|
214
|
+
columnFieldsByTable.set(tableDef.name, new Set(fields));
|
|
215
|
+
// ENUM columns map to concrete union types in the generated layer, so
|
|
216
|
+
// only genuine json/jsonb (`unknown`-typed) columns qualify as shadows.
|
|
217
|
+
unknownTypedFieldsByTable.set(tableDef.name, new Set(fields.filter((f) => {
|
|
218
|
+
const config = tableDef.columns[f];
|
|
219
|
+
if (config.type === 'ENUM')
|
|
220
|
+
return false;
|
|
221
|
+
const wire = config.isArray ? `_${udtName(config)}` : udtName(config);
|
|
222
|
+
return (0, introspect_js_1.isUnknownTsType)((0, schema_js_1.pgTypeToTs)(wire, false));
|
|
223
|
+
})));
|
|
224
|
+
}
|
|
225
|
+
const relationsByTable = (0, introspect_js_1.buildRelationsFromForeignKeys)(fkEntries, columnFieldsByTable, fkActions, unknownTypedFieldsByTable);
|
|
226
|
+
// ----- Pass 4: conservative junction auto-m2m (SHARED with introspect) ---
|
|
227
|
+
// Same shared detector + naming/collision rules as introspection: a table J
|
|
228
|
+
// is a PURE junction only when its PK is exactly the two single-column FKs
|
|
229
|
+
// to two DISTINCT tables and it has no payload columns.
|
|
230
|
+
const pkByTable = new Map();
|
|
231
|
+
const columnNamesByTable = new Map();
|
|
232
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
233
|
+
const pkFields = tableDef.primaryKey && tableDef.primaryKey.length > 0
|
|
234
|
+
? [...tableDef.primaryKey]
|
|
235
|
+
: Object.entries(tableDef.columns)
|
|
236
|
+
.filter(([, c]) => c.isPrimaryKey)
|
|
237
|
+
.map(([f]) => f);
|
|
238
|
+
pkByTable.set(tableDef.name, pkFields.map(schema_js_1.camelToSnake));
|
|
239
|
+
columnNamesByTable.set(tableDef.name, Object.keys(tableDef.columns).map(schema_js_1.camelToSnake));
|
|
240
|
+
}
|
|
241
|
+
(0, introspect_js_1.addAutoManyToManyRelations)(Object.values(def.tables).map((t) => t.name), fkEntries, pkByTable, columnNamesByTable, relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable);
|
|
242
|
+
// ----- Pass 5: assemble TableMetadata -----
|
|
243
|
+
const tables = {};
|
|
244
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
245
|
+
const columns = [];
|
|
246
|
+
const columnMap = {};
|
|
247
|
+
const reverseColumnMap = {};
|
|
248
|
+
const dateColumns = new Set();
|
|
249
|
+
const dialectTypes = {};
|
|
250
|
+
const pgTypes = {};
|
|
251
|
+
const allColumns = [];
|
|
252
|
+
const primaryKey = [];
|
|
253
|
+
const uniqueColumns = [];
|
|
254
|
+
for (const [field, config] of Object.entries(tableDef.columns)) {
|
|
255
|
+
const name = (0, schema_js_1.camelToSnake)(field);
|
|
256
|
+
const base = udtName(config);
|
|
257
|
+
// Introspection reports array columns with pg's leading-underscore
|
|
258
|
+
// udt_name (`_text`), which pgTypeToTs also understands.
|
|
259
|
+
const wireType = config.isArray ? `_${base}` : base;
|
|
260
|
+
const serial = isSerialType(config.type);
|
|
261
|
+
// PK members and serials are NOT NULL in Postgres regardless of flags.
|
|
262
|
+
const nullable = !(config.isPrimaryKey || config.isNotNull || serial);
|
|
263
|
+
const col = {
|
|
264
|
+
name,
|
|
265
|
+
field,
|
|
266
|
+
dialectType: wireType,
|
|
267
|
+
pgType: wireType,
|
|
268
|
+
tsType: (0, schema_js_1.pgTypeToTs)(wireType, nullable),
|
|
269
|
+
nullable,
|
|
270
|
+
hasDefault: config.defaultValue != null || serial,
|
|
271
|
+
isArray: config.isArray,
|
|
272
|
+
arrayType: (0, schema_js_1.pgArrayType)(base),
|
|
273
|
+
pgArrayType: (0, schema_js_1.pgArrayType)(base),
|
|
274
|
+
...(serial ? { isGenerated: true } : {}),
|
|
275
|
+
...(config.maxLength != null ? { maxLength: config.maxLength } : {}),
|
|
276
|
+
};
|
|
277
|
+
columns.push(col);
|
|
278
|
+
columnMap[field] = name;
|
|
279
|
+
reverseColumnMap[name] = field;
|
|
280
|
+
allColumns.push(name);
|
|
281
|
+
dialectTypes[name] = wireType;
|
|
282
|
+
pgTypes[name] = wireType;
|
|
283
|
+
if ((0, schema_js_1.isDateType)(base))
|
|
284
|
+
dateColumns.add(name);
|
|
285
|
+
if (config.isPrimaryKey)
|
|
286
|
+
primaryKey.push(name);
|
|
287
|
+
if (config.isUnique)
|
|
288
|
+
uniqueColumns.push([name]);
|
|
289
|
+
}
|
|
290
|
+
// Table-level composite PK takes precedence (defineSchema already cleared
|
|
291
|
+
// the column-level flags for its members).
|
|
292
|
+
const pk = tableDef.primaryKey && tableDef.primaryKey.length > 0 ? tableDef.primaryKey.map(schema_js_1.camelToSnake) : primaryKey;
|
|
293
|
+
tables[tableDef.name] = {
|
|
294
|
+
name: tableDef.name,
|
|
295
|
+
columns,
|
|
296
|
+
columnMap,
|
|
297
|
+
reverseColumnMap,
|
|
298
|
+
dateColumns,
|
|
299
|
+
dialectTypes,
|
|
300
|
+
pgTypes,
|
|
301
|
+
allColumns,
|
|
302
|
+
primaryKey: pk,
|
|
303
|
+
uniqueColumns,
|
|
304
|
+
relations: relationsByTable.get(tableDef.name) ?? {},
|
|
305
|
+
// SchemaDef cannot express indexes. An empty list keeps
|
|
306
|
+
// schemaHasIndexInfo() false → no index-advisor false positives.
|
|
307
|
+
indexes: [],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const enums = {};
|
|
311
|
+
for (const [name, labels] of Object.entries(def.enums ?? {})) {
|
|
312
|
+
enums[name] = [...labels];
|
|
313
|
+
}
|
|
314
|
+
// ----- Pass 6: merge explicit manyToMany declarations (additive) ---------
|
|
315
|
+
return (0, schema_builder_js_1.applyManyToManyRelations)({ tables, enums }, def);
|
|
316
|
+
}
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -56,6 +56,7 @@ const node_module_1 = require("node:module");
|
|
|
56
56
|
const client_js_1 = require("./client.js");
|
|
57
57
|
const dialect_js_1 = require("./dialect.js");
|
|
58
58
|
const errors_js_1 = require("./errors.js");
|
|
59
|
+
const introspect_js_1 = require("./introspect.js");
|
|
59
60
|
const schema_js_1 = require("./schema.js");
|
|
60
61
|
let cachedDatabaseSync;
|
|
61
62
|
/**
|
|
@@ -645,95 +646,13 @@ function introspectSqliteDatabase(db, options = {}) {
|
|
|
645
646
|
indexesByTable.set(tableName, idxMeta);
|
|
646
647
|
uniqueByTable.set(tableName, uniques);
|
|
647
648
|
}
|
|
648
|
-
// ----- Build relations from foreign keys (belongsTo + hasMany) -----
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const relationsByTable =
|
|
655
|
-
for (const fk of foreignKeys) {
|
|
656
|
-
const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
|
|
657
|
-
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
658
|
-
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
659
|
-
const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
660
|
-
? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
661
|
-
: (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
|
|
662
|
-
if (!relationsByTable.has(fk.sourceTable))
|
|
663
|
-
relationsByTable.set(fk.sourceTable, {});
|
|
664
|
-
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
665
|
-
type: 'belongsTo',
|
|
666
|
-
name: belongsToName,
|
|
667
|
-
from: fk.sourceTable,
|
|
668
|
-
to: fk.targetTable,
|
|
669
|
-
foreignKey,
|
|
670
|
-
referenceKey,
|
|
671
|
-
};
|
|
672
|
-
const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
673
|
-
? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
674
|
-
: (0, schema_js_1.snakeToCamel)(fk.sourceTable);
|
|
675
|
-
if (!relationsByTable.has(fk.targetTable))
|
|
676
|
-
relationsByTable.set(fk.targetTable, {});
|
|
677
|
-
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
678
|
-
type: 'hasMany',
|
|
679
|
-
name: hasManyName,
|
|
680
|
-
from: fk.targetTable,
|
|
681
|
-
to: fk.sourceTable,
|
|
682
|
-
foreignKey,
|
|
683
|
-
referenceKey,
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
// ----- Conservative many-to-many auto-detection (additive) -----
|
|
687
|
-
// A table J is a pure junction iff: PK is exactly two columns, exactly two
|
|
688
|
-
// single-column FKs whose source columns ARE the PK, two distinct target
|
|
689
|
-
// tables, and no payload columns. Mirrors the Postgres introspector.
|
|
690
|
-
for (const tableName of tableNames) {
|
|
691
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
692
|
-
if (pk.length !== 2)
|
|
693
|
-
continue;
|
|
694
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
695
|
-
if (tableFks.length !== 2)
|
|
696
|
-
continue;
|
|
697
|
-
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
698
|
-
continue;
|
|
699
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
700
|
-
const pkSet = new Set(pk);
|
|
701
|
-
if (!fkCols.every((c) => pkSet.has(c)))
|
|
702
|
-
continue;
|
|
703
|
-
if (new Set(fkCols).size !== 2)
|
|
704
|
-
continue;
|
|
705
|
-
const [fkA, fkB] = tableFks;
|
|
706
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
707
|
-
continue;
|
|
708
|
-
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
709
|
-
if (jCols.length !== 2)
|
|
710
|
-
continue;
|
|
711
|
-
const addM2M = (self, other) => {
|
|
712
|
-
const sourceTbl = self.targetTable;
|
|
713
|
-
const targetTbl = other.targetTable;
|
|
714
|
-
const relName = (0, schema_js_1.snakeToCamel)(targetTbl);
|
|
715
|
-
if (!relationsByTable.has(sourceTbl))
|
|
716
|
-
relationsByTable.set(sourceTbl, {});
|
|
717
|
-
const existing = relationsByTable.get(sourceTbl);
|
|
718
|
-
if (existing[relName])
|
|
719
|
-
return;
|
|
720
|
-
existing[relName] = {
|
|
721
|
-
type: 'manyToMany',
|
|
722
|
-
name: relName,
|
|
723
|
-
from: sourceTbl,
|
|
724
|
-
to: targetTbl,
|
|
725
|
-
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
726
|
-
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
727
|
-
through: {
|
|
728
|
-
table: tableName,
|
|
729
|
-
sourceKey: self.sourceColumns[0],
|
|
730
|
-
targetKey: other.sourceColumns[0],
|
|
731
|
-
},
|
|
732
|
-
};
|
|
733
|
-
};
|
|
734
|
-
addM2M(fkA, fkB);
|
|
735
|
-
addM2M(fkB, fkA);
|
|
736
|
-
}
|
|
649
|
+
// ----- Build relations from foreign keys (belongsTo + hasMany + m2m) -----
|
|
650
|
+
// Delegated to the SHARED introspection pipeline (introspect.ts) so SQLite
|
|
651
|
+
// derives IDENTICAL relation names to the Postgres introspector for the
|
|
652
|
+
// same logical schema — legacy-first naming, per-column disambiguation,
|
|
653
|
+
// collision resolution against scalar column fields, and the conservative
|
|
654
|
+
// pure-junction manyToMany auto-detection included.
|
|
655
|
+
const relationsByTable = (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
737
656
|
// ----- Assemble TableMetadata -----
|
|
738
657
|
const tables = {};
|
|
739
658
|
for (const tableName of tableNames) {
|
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -102,6 +102,9 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
102
102
|
case '--include-views':
|
|
103
103
|
result.includeViews = true;
|
|
104
104
|
break;
|
|
105
|
+
case '--no-timestamp':
|
|
106
|
+
result.noTimestamp = true;
|
|
107
|
+
break;
|
|
105
108
|
case '--allow-destructive':
|
|
106
109
|
result.allowDestructive = true;
|
|
107
110
|
break;
|
|
@@ -518,6 +521,7 @@ async function cmdGenerate(args, config) {
|
|
|
518
521
|
outDir: config.out,
|
|
519
522
|
connectionString: url,
|
|
520
523
|
zod: args.zod,
|
|
524
|
+
noTimestamp: args.noTimestamp,
|
|
521
525
|
});
|
|
522
526
|
genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
|
|
523
527
|
// List files
|
|
@@ -1464,6 +1468,7 @@ function showGenerateHelp() {
|
|
|
1464
1468
|
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
1465
1469
|
console.log(` ${cyan('--zod')} Also emit ${cyan('zod.ts')} validation schemas ${dim('(needs the zod dep)')}`);
|
|
1466
1470
|
console.log(` ${cyan('--include-views')} Include views + materialized views as read-only entities`);
|
|
1471
|
+
console.log(` ${cyan('--no-timestamp')} Omit the ${dim('Generated at:')} header line ${dim('(reproducible, diff-stable output)')}`);
|
|
1467
1472
|
console.log(` ${cyan('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1468
1473
|
newline();
|
|
1469
1474
|
}
|
package/dist/cli/mcp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { type ColumnMetadata, type RelationDef } from '../schema.js';
|
|
2
3
|
export interface McpServerOptions {
|
|
3
4
|
url: string;
|
|
4
5
|
schema: string;
|
|
@@ -14,4 +15,21 @@ export interface McpServerHandle {
|
|
|
14
15
|
dispose(): Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
export declare function startMcpServer(options: McpServerOptions, transport?: McpTransport): McpServerHandle;
|
|
18
|
+
interface ForeignKeyRow {
|
|
19
|
+
source_table: string;
|
|
20
|
+
source_column: string;
|
|
21
|
+
target_table: string;
|
|
22
|
+
target_column: string;
|
|
23
|
+
constraint_name: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Group raw FK rows into constraint-level entries and delegate relation
|
|
27
|
+
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
28
|
+
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
29
|
+
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
30
|
+
* generate` derived DIFFERENT relation names from the same database (N-3).
|
|
31
|
+
* Exported for the parity unit test.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildRelations(tableNames: string[], columnsByTable: Map<string, ColumnMetadata[]>, pkByTable: Map<string, string[]>, rows: ForeignKeyRow[], enums?: Record<string, string[]>): Map<string, Record<string, RelationDef>>;
|
|
17
34
|
export declare function runMcpServer(options: McpServerOptions): Promise<void>;
|
|
35
|
+
export {};
|