turbine-orm 0.91.0-next.e72e9b4 → 0.92.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 +23 -24
- package/dist/cjs/cli/config.d.ts +5 -0
- package/dist/cjs/cli/config.js +1 -0
- package/dist/cjs/cli/index.d.ts +17 -17
- package/dist/cjs/cli/index.js +34 -27
- package/dist/cjs/cli/observe-ui.d.ts +1 -1
- package/dist/cjs/cli/observe-ui.js +1 -1
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/powdb.d.ts +10 -6
- package/dist/cjs/powdb.js +4 -2
- package/dist/cjs/query/builder.d.ts +8 -0
- package/dist/cjs/query/builder.js +31 -23
- package/dist/cjs/query/relations.d.ts +21 -3
- package/dist/cjs/query/relations.js +331 -68
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +44 -1
- package/dist/cjs/query/where.d.ts +15 -0
- package/dist/cjs/schema-builder.d.ts +8 -5
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.js +1 -0
- package/dist/cli/index.d.ts +17 -17
- package/dist/cli/index.js +35 -28
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +1 -1
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/powdb.d.ts +10 -6
- package/dist/powdb.js +4 -2
- package/dist/query/builder.d.ts +8 -0
- package/dist/query/builder.js +18 -10
- package/dist/query/relations.d.ts +21 -3
- package/dist/query/relations.js +330 -69
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +43 -0
- package/dist/query/where.d.ts +15 -0
- package/dist/schema-builder.d.ts +8 -5
- package/package.json +1 -1
|
@@ -83,9 +83,11 @@ exports.buildJsonRow = buildJsonRow;
|
|
|
83
83
|
exports.buildRelationShapes = buildRelationShapes;
|
|
84
84
|
exports.buildRelationShape = buildRelationShape;
|
|
85
85
|
exports.makeNestedParser = makeNestedParser;
|
|
86
|
+
exports.makeReferencePositionalParser = makeReferencePositionalParser;
|
|
86
87
|
exports.decodePositionalRelations = decodePositionalRelations;
|
|
87
88
|
exports.decodePositionalValue = decodePositionalValue;
|
|
88
89
|
exports.decodePositionalObject = decodePositionalObject;
|
|
90
|
+
exports.makePositionalRowParser = makePositionalRowParser;
|
|
89
91
|
exports.provableUniqueTargetKey = provableUniqueTargetKey;
|
|
90
92
|
exports.planFlattenWith = planFlattenWith;
|
|
91
93
|
exports.emitFlattenNode = emitFlattenNode;
|
|
@@ -1488,6 +1490,22 @@ function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1488
1490
|
const meta = qi.schema.tables[table];
|
|
1489
1491
|
if (!meta)
|
|
1490
1492
|
return qi.parseRow(row, table);
|
|
1493
|
+
const parsed = parseScalarColumns(qi, row, table, meta, fromJson);
|
|
1494
|
+
for (const [relName, relDef] of getRelationEntries(qi, table, meta)) {
|
|
1495
|
+
const rawValue = row[relName];
|
|
1496
|
+
if (rawValue === undefined)
|
|
1497
|
+
continue;
|
|
1498
|
+
parsed[relName] = parseRelationValue(qi, relName, relDef, rawValue);
|
|
1499
|
+
}
|
|
1500
|
+
return parsed;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* The scalar half of {@link parseNestedRow}: the row through `parseRow` (after
|
|
1504
|
+
* the JSON-wire decode when `fromJson`), with any `_count__<rel>` columns
|
|
1505
|
+
* folded into a `_count` object. Relation columns are copied through as they
|
|
1506
|
+
* are; the caller replaces each one it parses.
|
|
1507
|
+
*/
|
|
1508
|
+
function parseScalarColumns(qi, row, table, meta, fromJson) {
|
|
1491
1509
|
const parsed = qi.parseRow(fromJson ? decodeJsonWireRow(qi, row, table, meta) : row, table);
|
|
1492
1510
|
// Assemble reserved `_count__<rel>` scalar columns into a `_count` object.
|
|
1493
1511
|
// parseRow copies these unknown columns through under their raw key.
|
|
@@ -1514,75 +1532,66 @@ function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1514
1532
|
delete parsed[`${COUNT_PREFIX}${rel}`];
|
|
1515
1533
|
parsed._count = countObj;
|
|
1516
1534
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
parsed[relName] = jsonVal.map((item) => typeof item === 'object' && item !== null
|
|
1543
|
-
? parseNestedRow(qi, item, relDef.to, true)
|
|
1544
|
-
: item);
|
|
1545
|
-
}
|
|
1546
|
-
else if (typeof jsonVal === 'object' && jsonVal !== null) {
|
|
1547
|
-
parsed[relName] = parseNestedRow(qi, jsonVal, relDef.to, true);
|
|
1548
|
-
}
|
|
1549
|
-
else {
|
|
1550
|
-
parsed[relName] = jsonVal;
|
|
1551
|
-
}
|
|
1535
|
+
return parsed;
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* The relation half of {@link parseNestedRow}: one relation column's value
|
|
1539
|
+
* (never `undefined`, which the caller skips) as the parsed relation.
|
|
1540
|
+
*/
|
|
1541
|
+
function parseRelationValue(qi, relName, relDef, rawValue) {
|
|
1542
|
+
// --- Short-circuit: skip JSON.parse for common empty/null cases ---
|
|
1543
|
+
// hasMany returns '[]' (from COALESCE(..., '[]'::json)); belongsTo/hasOne returns null
|
|
1544
|
+
if (rawValue === null || rawValue === 'null')
|
|
1545
|
+
return null;
|
|
1546
|
+
if (rawValue === '[]')
|
|
1547
|
+
return [];
|
|
1548
|
+
if (Array.isArray(rawValue) && rawValue.length === 0)
|
|
1549
|
+
return [];
|
|
1550
|
+
// --- Non-empty values: full parse path ---
|
|
1551
|
+
if (typeof rawValue === 'string') {
|
|
1552
|
+
try {
|
|
1553
|
+
const jsonVal = JSON.parse(rawValue);
|
|
1554
|
+
// After parsing, recurse via parseNestedRow so each item gets date
|
|
1555
|
+
// coercion AND its own sub-relations parsed at arbitrary depth.
|
|
1556
|
+
if (Array.isArray(jsonVal)) {
|
|
1557
|
+
return jsonVal.map((item) => typeof item === 'object' && item !== null
|
|
1558
|
+
? parseNestedRow(qi, item, relDef.to, true)
|
|
1559
|
+
: item);
|
|
1552
1560
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
// a driver-level corruption, not a recoverable condition. Handing back
|
|
1556
|
-
// the raw string satisfied the assignment and violated the generated
|
|
1557
|
-
// type: callers got a `string` where `Post[]` was promised, and found
|
|
1558
|
-
// out at the first `.map`. Throw instead, with the context needed to
|
|
1559
|
-
// find it.
|
|
1560
|
-
//
|
|
1561
|
-
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1562
|
-
// per-relation loop: one 10,000-row page was 10,000 warnings. The
|
|
1563
|
-
// parse error stays out of the entry: its message quotes the payload,
|
|
1564
|
-
// which is row data.
|
|
1565
|
-
const log = whereMod.adviceLogger(qi);
|
|
1566
|
-
if (log && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1567
|
-
(0, logger_js_1.logTo)(log, 'warn', `Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1568
|
-
`This is a bug; please report it with the emitted SQL.`, { code: 'relation-payload-json', table: qi.table, relation: relName });
|
|
1569
|
-
}
|
|
1570
|
-
throw new errors_js_1.InvariantError('relation-payload-json', `Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1561
|
+
if (typeof jsonVal === 'object' && jsonVal !== null) {
|
|
1562
|
+
return parseNestedRow(qi, jsonVal, relDef.to, true);
|
|
1571
1563
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1564
|
+
return jsonVal;
|
|
1565
|
+
}
|
|
1566
|
+
catch (err) {
|
|
1567
|
+
// A relation column that does not parse is a bug in the emitted SQL or
|
|
1568
|
+
// a driver-level corruption, not a recoverable condition. Handing back
|
|
1569
|
+
// the raw string satisfied the assignment and violated the generated
|
|
1570
|
+
// type: callers got a `string` where `Post[]` was promised, and found
|
|
1571
|
+
// out at the first `.map`. Throw instead, with the context needed to
|
|
1572
|
+
// find it.
|
|
1573
|
+
//
|
|
1574
|
+
// The warn is gated and deduped because this sits in parseNestedRow's
|
|
1575
|
+
// per-relation loop: one 10,000-row page was 10,000 warnings. The
|
|
1576
|
+
// parse error stays out of the entry: its message quotes the payload,
|
|
1577
|
+
// which is row data.
|
|
1578
|
+
const log = whereMod.adviceLogger(qi);
|
|
1579
|
+
if (log && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.relationParseFailure, `${qi.table}.${relName}`)) {
|
|
1580
|
+
(0, logger_js_1.logTo)(log, 'warn', `Relation "${relName}" on "${qi.table}" returned a payload that is not valid JSON. ` +
|
|
1581
|
+
`This is a bug; please report it with the emitted SQL.`, { code: 'relation-payload-json', table: qi.table, relation: relName });
|
|
1582
|
+
}
|
|
1583
|
+
throw new errors_js_1.InvariantError('relation-payload-json', `Relation "${relName}" on table "${qi.table}" returned a payload that could not be parsed as JSON.`, { cause: err });
|
|
1583
1584
|
}
|
|
1584
1585
|
}
|
|
1585
|
-
|
|
1586
|
+
if (Array.isArray(rawValue)) {
|
|
1587
|
+
return rawValue.map((item) => typeof item === 'object' && item !== null
|
|
1588
|
+
? parseNestedRow(qi, item, relDef.to, true)
|
|
1589
|
+
: item);
|
|
1590
|
+
}
|
|
1591
|
+
if (typeof rawValue === 'object' && rawValue !== null) {
|
|
1592
|
+
return parseNestedRow(qi, rawValue, relDef.to, true);
|
|
1593
|
+
}
|
|
1594
|
+
return rawValue;
|
|
1586
1595
|
}
|
|
1587
1596
|
/**
|
|
1588
1597
|
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
@@ -1768,9 +1777,12 @@ function buildRelationShape(qi, relDef, spec, parentMeta, includePii) {
|
|
|
1768
1777
|
}
|
|
1769
1778
|
/**
|
|
1770
1779
|
* Build the row parser for a `with` clause. In object mode this is just
|
|
1771
|
-
* {@link parseNestedRow}. In positional mode
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1780
|
+
* {@link parseNestedRow}. In positional mode the relation shapes are built
|
|
1781
|
+
* once (not per row) and compiled into a decode plan
|
|
1782
|
+
* ({@link makePositionalRowParser}) that turns each positional array straight
|
|
1783
|
+
* into its parsed row. A query whose shapes the plan declines takes the
|
|
1784
|
+
* reference path instead, which decodes each positional array into the object
|
|
1785
|
+
* form and hands it to parseNestedRow.
|
|
1774
1786
|
*/
|
|
1775
1787
|
function makeNestedParser(qi, withClause, includePii, flattenPlan) {
|
|
1776
1788
|
// A flatten plan and the positional encoding are mutually exclusive (the
|
|
@@ -1782,6 +1794,16 @@ function makeNestedParser(qi, withClause, includePii, flattenPlan) {
|
|
|
1782
1794
|
return (row) => parseNestedRow(qi, row, qi.table);
|
|
1783
1795
|
}
|
|
1784
1796
|
const shapes = buildRelationShapes(qi, qi.table, withClause, includePii);
|
|
1797
|
+
return makePositionalRowParser(qi, qi.table, shapes) ?? makeReferencePositionalParser(qi, shapes);
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* The positional parser the plan in {@link makePositionalRowParser} is held to:
|
|
1801
|
+
* decode each relation's arrays into the object representation, then parse
|
|
1802
|
+
* that exactly as an object-encoded row is parsed. It is the path for every
|
|
1803
|
+
* query the plan declines, and is exported so the differential tests can
|
|
1804
|
+
* compare the two.
|
|
1805
|
+
*/
|
|
1806
|
+
function makeReferencePositionalParser(qi, shapes) {
|
|
1785
1807
|
return (row) => parseNestedRow(qi, decodePositionalRelations(qi, row, shapes), qi.table);
|
|
1786
1808
|
}
|
|
1787
1809
|
/**
|
|
@@ -1836,6 +1858,247 @@ function decodePositionalObject(qi, arr, shape) {
|
|
|
1836
1858
|
}
|
|
1837
1859
|
return obj;
|
|
1838
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* A key the reference path treats as an ordinary own property: not one that
|
|
1863
|
+
* resolves through `Object.prototype` when read off a plain object (`__proto__`
|
|
1864
|
+
* included), and not an integer-like key, which `Object.keys` lists ahead of
|
|
1865
|
+
* every other key whatever order it was written in.
|
|
1866
|
+
*/
|
|
1867
|
+
function isPlainKey(key) {
|
|
1868
|
+
return typeof key === 'string' && !(key in Object.prototype) && !/^\d+$/.test(key);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Does `name` reach `parseRow`'s answer for a top-level column? Any overlap
|
|
1872
|
+
* with a column name, field name, reverse-map key or date column means the
|
|
1873
|
+
* reference would give the relation column a field or cell kind of its own.
|
|
1874
|
+
*/
|
|
1875
|
+
function nameTouchesColumns(qi, table, meta, name) {
|
|
1876
|
+
if (Object.hasOwn(meta.reverseColumnMap, name) || meta.dateColumns.has(name))
|
|
1877
|
+
return true;
|
|
1878
|
+
if (getCamelDateFields(qi, table, meta).has(name))
|
|
1879
|
+
return true;
|
|
1880
|
+
return meta.columns.some((column) => column.name === name || column.field === name);
|
|
1881
|
+
}
|
|
1882
|
+
/** The indexes of `relations` in `entries` order, or `null` when that is their own order. */
|
|
1883
|
+
function relationDecodeOrder(relations, entries) {
|
|
1884
|
+
const order = [];
|
|
1885
|
+
for (const [name] of entries) {
|
|
1886
|
+
const index = relations.findIndex((rel) => rel.name === name);
|
|
1887
|
+
if (index !== -1)
|
|
1888
|
+
order.push(index);
|
|
1889
|
+
}
|
|
1890
|
+
return order.every((index, i) => index === i) ? null : order;
|
|
1891
|
+
}
|
|
1892
|
+
/** The plan for one relation shape, or `undefined` when the plan cannot take it. */
|
|
1893
|
+
function planPositionalRow(qi, table, shape) {
|
|
1894
|
+
const meta = qi.schema.tables[table];
|
|
1895
|
+
if (!meta || !qi.rowDecodePlan)
|
|
1896
|
+
return undefined;
|
|
1897
|
+
const keys = shape.keys;
|
|
1898
|
+
if (new Set(keys).size !== keys.length || !keys.every(isPlainKey))
|
|
1899
|
+
return undefined;
|
|
1900
|
+
const rowPlan = qi.rowDecodePlan(table, keys);
|
|
1901
|
+
if (!rowPlan)
|
|
1902
|
+
return undefined;
|
|
1903
|
+
const entries = getRelationEntries(qi, table, meta);
|
|
1904
|
+
if (!entries.every(([name]) => isPlainKey(name)))
|
|
1905
|
+
return undefined;
|
|
1906
|
+
const relationNames = new Set(entries.map(([name]) => name));
|
|
1907
|
+
const wireFields = jsonWireFields(qi, table, meta);
|
|
1908
|
+
const wireOrder = wireFields ? [...wireFields.keys()] : [];
|
|
1909
|
+
const fields = [];
|
|
1910
|
+
const wire = [];
|
|
1911
|
+
const cellPositions = [];
|
|
1912
|
+
const cellKinds = [];
|
|
1913
|
+
const relations = [];
|
|
1914
|
+
let lastWireRank = -1;
|
|
1915
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1916
|
+
const key = keys[i];
|
|
1917
|
+
const field = rowPlan.fields[i];
|
|
1918
|
+
const kind = rowPlan.kinds[i];
|
|
1919
|
+
// parseNestedRow folds a `_count__` field into `_count`; the plan does not.
|
|
1920
|
+
if (!isPlainKey(field) || field.startsWith(COUNT_PREFIX))
|
|
1921
|
+
return undefined;
|
|
1922
|
+
const nested = shape.nested[key];
|
|
1923
|
+
if (nested) {
|
|
1924
|
+
const def = (0, utils_js_1.ownLookup)(meta.relations, key);
|
|
1925
|
+
// The reference copies the slot through parseRow before replacing it, so
|
|
1926
|
+
// the slot must be a plain cell under its own name, and no wire field.
|
|
1927
|
+
if (!def || field !== key || kind !== utils_js_1.CELL_PLAIN || wireFields?.has(key))
|
|
1928
|
+
return undefined;
|
|
1929
|
+
const row = planPositionalRow(qi, def.to, nested);
|
|
1930
|
+
if (!row)
|
|
1931
|
+
return undefined;
|
|
1932
|
+
relations.push({ name: key, def, many: nested.cardinality === 'many', position: i, row });
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
// buildRelationShape puts every scalar ahead of every relation slot, and a
|
|
1936
|
+
// scalar named like one of the table's relations would be re-read as it.
|
|
1937
|
+
if (relations.length > 0 || relationNames.has(key))
|
|
1938
|
+
return undefined;
|
|
1939
|
+
const rule = wireFields?.get(key);
|
|
1940
|
+
if (rule !== undefined) {
|
|
1941
|
+
// The reference decodes wire columns in table-column order before any
|
|
1942
|
+
// cell decode; the plan does them in key order, so the two must agree.
|
|
1943
|
+
const rank = wireOrder.indexOf(key);
|
|
1944
|
+
if (rank < lastWireRank)
|
|
1945
|
+
return undefined;
|
|
1946
|
+
lastWireRank = rank;
|
|
1947
|
+
}
|
|
1948
|
+
fields.push(field);
|
|
1949
|
+
wire.push(rule);
|
|
1950
|
+
if (kind !== utils_js_1.CELL_PLAIN) {
|
|
1951
|
+
cellPositions.push(i);
|
|
1952
|
+
cellKinds.push(kind);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
// Two keys landing on one field, or a scalar field named like a relation
|
|
1956
|
+
// slot, would collapse into one property in a different order.
|
|
1957
|
+
const written = new Set(fields);
|
|
1958
|
+
if (written.size !== fields.length || relations.some((rel) => written.has(rel.name)))
|
|
1959
|
+
return undefined;
|
|
1960
|
+
return {
|
|
1961
|
+
table,
|
|
1962
|
+
fields,
|
|
1963
|
+
wire,
|
|
1964
|
+
cellPositions,
|
|
1965
|
+
cellKinds,
|
|
1966
|
+
relations,
|
|
1967
|
+
relationOrder: relationDecodeOrder(relations, entries),
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Compile a top-level positional parser for `shapes`, or `undefined` when any
|
|
1972
|
+
* part of it is a shape the plan does not take (the caller then uses
|
|
1973
|
+
* {@link makeReferencePositionalParser}). Rows are identical to the
|
|
1974
|
+
* reference's on every shape it does take.
|
|
1975
|
+
*/
|
|
1976
|
+
function makePositionalRowParser(qi, table, shapes) {
|
|
1977
|
+
const meta = qi.schema.tables[table];
|
|
1978
|
+
if (!meta || !qi.rowDecodePlan || !qi.decodeCell)
|
|
1979
|
+
return undefined;
|
|
1980
|
+
const entries = getRelationEntries(qi, table, meta);
|
|
1981
|
+
if (!entries.every(([name]) => isPlainKey(name)))
|
|
1982
|
+
return undefined;
|
|
1983
|
+
const plans = [];
|
|
1984
|
+
for (const [name, def] of entries) {
|
|
1985
|
+
const shape = Object.hasOwn(shapes, name) ? shapes[name] : undefined;
|
|
1986
|
+
if (!shape) {
|
|
1987
|
+
plans.push(null);
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
// The reference runs the top-level row through parseRow with the relation
|
|
1991
|
+
// columns already decoded; the plan runs it with them raw and replaces them
|
|
1992
|
+
// after. Those agree only while parseRow copies a relation column through
|
|
1993
|
+
// untouched, under its own name, and the `_count` fold never reads it.
|
|
1994
|
+
if (name.startsWith(COUNT_PREFIX) || nameTouchesColumns(qi, table, meta, name))
|
|
1995
|
+
return undefined;
|
|
1996
|
+
const row = planPositionalRow(qi, def.to, shape);
|
|
1997
|
+
if (!row)
|
|
1998
|
+
return undefined;
|
|
1999
|
+
plans.push({ name, def, many: shape.cardinality === 'many', position: -1, row });
|
|
2000
|
+
}
|
|
2001
|
+
const isOwnEnumerable = Object.prototype.propertyIsEnumerable;
|
|
2002
|
+
return (row) => {
|
|
2003
|
+
const parsed = parseScalarColumns(qi, row, table, meta, false);
|
|
2004
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2005
|
+
const [name, def] = entries[i];
|
|
2006
|
+
// The reference reads relation columns off a `{ ...row }` copy, which
|
|
2007
|
+
// holds exactly the row's own enumerable properties.
|
|
2008
|
+
if (!isOwnEnumerable.call(row, name))
|
|
2009
|
+
continue;
|
|
2010
|
+
const plan = plans[i];
|
|
2011
|
+
if (plan !== null) {
|
|
2012
|
+
parsed[name] = decodePlannedRelation(qi, row[name], plan);
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
const raw = row[name];
|
|
2016
|
+
if (raw !== undefined)
|
|
2017
|
+
parsed[name] = parseRelationValue(qi, name, def, raw);
|
|
2018
|
+
}
|
|
2019
|
+
return parsed;
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* One relation value through the plan: exactly what the reference produces by
|
|
2024
|
+
* {@link decodePositionalValue} followed by {@link parseRelationValue}. Every
|
|
2025
|
+
* value that is not the well-formed positional shape (a payload that is not
|
|
2026
|
+
* JSON, a to-many that is not an array, a to-one that is not an array) is
|
|
2027
|
+
* handed to `parseRelationValue` as the reference would hand it, so it
|
|
2028
|
+
* decodes, or throws, identically.
|
|
2029
|
+
*/
|
|
2030
|
+
function decodePlannedRelation(qi, raw, rel) {
|
|
2031
|
+
let value = raw;
|
|
2032
|
+
if (typeof value === 'string') {
|
|
2033
|
+
try {
|
|
2034
|
+
value = JSON.parse(value);
|
|
2035
|
+
}
|
|
2036
|
+
catch {
|
|
2037
|
+
return parseRelationValue(qi, rel.name, rel.def, raw);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
if (value === null || value === undefined)
|
|
2041
|
+
return rel.many ? [] : null;
|
|
2042
|
+
if (!rel.many) {
|
|
2043
|
+
return Array.isArray(value)
|
|
2044
|
+
? decodePlannedRow(qi, value, rel.row)
|
|
2045
|
+
: parseRelationValue(qi, rel.name, rel.def, value);
|
|
2046
|
+
}
|
|
2047
|
+
if (!Array.isArray(value))
|
|
2048
|
+
return parseRelationValue(qi, rel.name, rel.def, value);
|
|
2049
|
+
const out = [];
|
|
2050
|
+
for (let i = 0; i < value.length; i++) {
|
|
2051
|
+
const item = value[i];
|
|
2052
|
+
out.push(Array.isArray(item)
|
|
2053
|
+
? decodePlannedRow(qi, item, rel.row)
|
|
2054
|
+
: typeof item === 'object' && item !== null
|
|
2055
|
+
? parseNestedRow(qi, item, rel.def.to, true)
|
|
2056
|
+
: item);
|
|
2057
|
+
}
|
|
2058
|
+
return out;
|
|
2059
|
+
}
|
|
2060
|
+
/** One positional array through its row plan, into the parsed row. */
|
|
2061
|
+
function decodePlannedRow(qi, arr, plan) {
|
|
2062
|
+
const out = {};
|
|
2063
|
+
const { fields, wire, cellPositions, cellKinds, relations, table } = plan;
|
|
2064
|
+
for (let i = 0; i < fields.length; i++) {
|
|
2065
|
+
let value = arr[i];
|
|
2066
|
+
const rule = wire[i];
|
|
2067
|
+
if (rule !== undefined && typeof value === 'string')
|
|
2068
|
+
value = rule.decode(value);
|
|
2069
|
+
out[fields[i]] = value;
|
|
2070
|
+
}
|
|
2071
|
+
if (cellPositions.length > 0) {
|
|
2072
|
+
// `decodeCell` is present: the plan is only built when it is.
|
|
2073
|
+
const decodeCell = qi.decodeCell;
|
|
2074
|
+
for (let c = 0; c < cellPositions.length; c++) {
|
|
2075
|
+
const field = fields[cellPositions[c]];
|
|
2076
|
+
const kind = cellKinds[c];
|
|
2077
|
+
const value = out[field];
|
|
2078
|
+
// parseRow's own skip: a float the driver already made a number.
|
|
2079
|
+
if (kind === utils_js_1.CELL_FLOAT && typeof value !== 'string')
|
|
2080
|
+
continue;
|
|
2081
|
+
out[field] = decodeCell(kind, value, table, field);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (relations.length > 0) {
|
|
2085
|
+
const order = plan.relationOrder;
|
|
2086
|
+
if (order === null) {
|
|
2087
|
+
for (const rel of relations)
|
|
2088
|
+
out[rel.name] = decodePlannedRelation(qi, arr[rel.position], rel);
|
|
2089
|
+
}
|
|
2090
|
+
else {
|
|
2091
|
+
// Claim the keys in key order first, then decode in the reference's order.
|
|
2092
|
+
for (const rel of relations)
|
|
2093
|
+
out[rel.name] = undefined;
|
|
2094
|
+
for (const index of order) {
|
|
2095
|
+
const rel = relations[index];
|
|
2096
|
+
out[rel.name] = decodePlannedRelation(qi, arr[rel.position], rel);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return out;
|
|
2101
|
+
}
|
|
1839
2102
|
// ---------------------------------------------------------------------------
|
|
1840
2103
|
// relationLoadStrategy: 'flatten', to-one relations compiled as LEFT JOINs
|
|
1841
2104
|
// ---------------------------------------------------------------------------
|
|
@@ -423,6 +423,19 @@ export declare function coerceTemporalValue(dbType: string | undefined, value: u
|
|
|
423
423
|
* as-is.
|
|
424
424
|
*/
|
|
425
425
|
export declare function parseDbDate(value: string): Date;
|
|
426
|
+
/**
|
|
427
|
+
* Per-cell decode work in a row decode plan (`RowDecodePlan` in builder.ts).
|
|
428
|
+
* Defined here rather than in builder.ts because the positional nested-row
|
|
429
|
+
* plan in relations.ts reads the same codes, and relations.ts cannot import
|
|
430
|
+
* builder.ts.
|
|
431
|
+
*/
|
|
432
|
+
export declare const CELL_PLAIN = 0;
|
|
433
|
+
export declare const CELL_TEMPORAL = 1;
|
|
434
|
+
export declare const CELL_FLOAT = 2;
|
|
435
|
+
export declare const CELL_FLOAT_ARRAY = 3;
|
|
436
|
+
export declare const CELL_TEXT_ARRAY = 4;
|
|
437
|
+
/** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
|
|
438
|
+
export declare const CELL_DIALECT = 5;
|
|
426
439
|
/**
|
|
427
440
|
* A float column's value as the driver reads it. JSON has no NaN or infinity,
|
|
428
441
|
* so `json_build_object` renders them as the strings "NaN", "Infinity" and
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -8,7 +8,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
8
8
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
9
|
};
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.JSON_WIRE_COERCION_OIDS = exports.OPERATOR_KEYS = exports.PREPARED_STATEMENT_BUDGET = exports.LRUCache = exports.INTERNAL_COMBINATOR = void 0;
|
|
11
|
+
exports.JSON_WIRE_COERCION_OIDS = exports.CELL_DIALECT = exports.CELL_TEXT_ARRAY = exports.CELL_FLOAT_ARRAY = exports.CELL_FLOAT = exports.CELL_TEMPORAL = exports.CELL_PLAIN = exports.OPERATOR_KEYS = exports.PREPARED_STATEMENT_BUDGET = exports.LRUCache = exports.INTERNAL_COMBINATOR = void 0;
|
|
12
12
|
exports.quoteIdent = quoteIdent;
|
|
13
13
|
exports.ownLookup = ownLookup;
|
|
14
14
|
exports.resolveColumnName = resolveColumnName;
|
|
@@ -884,6 +884,13 @@ function renderTemporal(value, kind) {
|
|
|
884
884
|
* or a `±HH`, `±HHMM`, `±HH:MM` offset.
|
|
885
885
|
*/
|
|
886
886
|
const TZ_SUFFIX_RE = /(?:Z|[+-]\d{2}(?::?\d{2})?)$/;
|
|
887
|
+
const COLON_CHAR = 58;
|
|
888
|
+
const PLUS_CHAR = 43;
|
|
889
|
+
const MINUS_CHAR = 45;
|
|
890
|
+
/** `0`-`9` as a char code. */
|
|
891
|
+
function isDigitCode(code) {
|
|
892
|
+
return code >= 48 && code <= 57;
|
|
893
|
+
}
|
|
887
894
|
/**
|
|
888
895
|
* Parse a database date-time string deterministically.
|
|
889
896
|
*
|
|
@@ -906,6 +913,29 @@ function parseDbDate(value) {
|
|
|
906
913
|
return Number.isNaN(date.getTime()) ? (parseDbDateGeneral(value) ?? date) : date;
|
|
907
914
|
}
|
|
908
915
|
function parseDbDateCommon(value) {
|
|
916
|
+
// Fast path for the shape `timestamptz` arrives in, a trailing `±HH:MM`
|
|
917
|
+
// offset (`2026-07-07T17:15:41.896+00:00`). It is exactly the general path's
|
|
918
|
+
// answer for that shape, not an approximation of it: the string contains a
|
|
919
|
+
// ':' (at length-3), TZ_SUFFIX_RE matches, and neither normalizing replace
|
|
920
|
+
// below can fire (the first needs four digits after the sign, the second two
|
|
921
|
+
// digits at the very end after it), so that path also ends in
|
|
922
|
+
// `new Date(value)`. All six offset characters are checked, so a near miss
|
|
923
|
+
// (`+0a:00`, `05:30` without a sign) still takes the general path. It skips
|
|
924
|
+
// one regex test and two regex replaces per value, which on a nested read is
|
|
925
|
+
// the single largest cost in the decode. The invalid-date fallback in
|
|
926
|
+
// parseDbDate still runs on the result, so a BC or five-digit year is
|
|
927
|
+
// unchanged.
|
|
928
|
+
const n = value.length;
|
|
929
|
+
if (n >= 6 && value.charCodeAt(n - 3) === COLON_CHAR) {
|
|
930
|
+
const sign = value.charCodeAt(n - 6);
|
|
931
|
+
if ((sign === PLUS_CHAR || sign === MINUS_CHAR) &&
|
|
932
|
+
isDigitCode(value.charCodeAt(n - 5)) &&
|
|
933
|
+
isDigitCode(value.charCodeAt(n - 4)) &&
|
|
934
|
+
isDigitCode(value.charCodeAt(n - 2)) &&
|
|
935
|
+
isDigitCode(value.charCodeAt(n - 1))) {
|
|
936
|
+
return new Date(value);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
909
939
|
// Date-only values (`2026-07-07`, from `date` columns in json_agg output)
|
|
910
940
|
// have no time to zone-pin, and their `-07` tail must not be read as an
|
|
911
941
|
// offset. JS parses bare ISO dates as UTC midnight already.
|
|
@@ -945,6 +975,19 @@ function parseDbDateGeneral(value) {
|
|
|
945
975
|
}
|
|
946
976
|
return date;
|
|
947
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* Per-cell decode work in a row decode plan (`RowDecodePlan` in builder.ts).
|
|
980
|
+
* Defined here rather than in builder.ts because the positional nested-row
|
|
981
|
+
* plan in relations.ts reads the same codes, and relations.ts cannot import
|
|
982
|
+
* builder.ts.
|
|
983
|
+
*/
|
|
984
|
+
exports.CELL_PLAIN = 0;
|
|
985
|
+
exports.CELL_TEMPORAL = 1;
|
|
986
|
+
exports.CELL_FLOAT = 2;
|
|
987
|
+
exports.CELL_FLOAT_ARRAY = 3;
|
|
988
|
+
exports.CELL_TEXT_ARRAY = 4;
|
|
989
|
+
/** The dialect's own decode for a type whose storage differs (see `Dialect.decodeColumnValue`). */
|
|
990
|
+
exports.CELL_DIALECT = 5;
|
|
948
991
|
/**
|
|
949
992
|
* A float column's value as the driver reads it. JSON has no NaN or infinity,
|
|
950
993
|
* so `json_build_object` renders them as the strings "NaN", "Infinity" and
|
|
@@ -133,6 +133,21 @@ export interface BuilderCtx {
|
|
|
133
133
|
toColumn(field: string): string;
|
|
134
134
|
castAgg(expr: string, target: 'int' | 'float'): string;
|
|
135
135
|
parseRow(row: Record<string, unknown>, table: string): Record<string, unknown>;
|
|
136
|
+
/**
|
|
137
|
+
* The field name and cell kind {@link parseRow} applies to each key of a row
|
|
138
|
+
* whose keys are exactly `keys`, in order (`undefined` for a table with no
|
|
139
|
+
* metadata). Read once per relation shape by the positional nested-row plan
|
|
140
|
+
* in relations.ts, so a nested row decodes by the same answers `parseRow`
|
|
141
|
+
* would give. Optional, like the members above, so a hand-built ctx keeps
|
|
142
|
+
* compiling; without it (or {@link decodeCell}) the nested parser takes the
|
|
143
|
+
* general path.
|
|
144
|
+
*/
|
|
145
|
+
rowDecodePlan?(table: string, keys: readonly string[]): {
|
|
146
|
+
readonly fields: readonly string[];
|
|
147
|
+
readonly kinds: readonly number[];
|
|
148
|
+
} | undefined;
|
|
149
|
+
/** {@link parseRow}'s decode of one cell whose kind is not `CELL_PLAIN` (see utils.ts). */
|
|
150
|
+
decodeCell?(kind: number, value: unknown, table: string, field: string): unknown;
|
|
136
151
|
nullsSuffix(nulls: 'first' | 'last' | undefined): string;
|
|
137
152
|
isRelationOrderByValue(value: unknown): boolean;
|
|
138
153
|
resolveOrderByColumn(table: string, meta: TableMetadata, key: string): string;
|
|
@@ -73,8 +73,10 @@ export interface ColumnDef {
|
|
|
73
73
|
* code-first declaration: it carries onto {@link ColumnConfig.pii} and, via
|
|
74
74
|
* `schemaDefToMetadata` / codegen, onto
|
|
75
75
|
* {@link import('./schema.js').ColumnMetadata.pii}. A PII column is excluded
|
|
76
|
-
* from default projections
|
|
77
|
-
* `includePii: UNSAFE
|
|
76
|
+
* from default projections and comes back only from a read that passes
|
|
77
|
+
* `includePii: UNSAFE` (the imported sentinel symbol). Naming it in `select`
|
|
78
|
+
* without that flag throws (E003), a write never returns it, and Studio
|
|
79
|
+
* redacts it.
|
|
78
80
|
* Introspection never auto-tags PII.
|
|
79
81
|
*/
|
|
80
82
|
pii?: boolean;
|
|
@@ -248,9 +250,10 @@ export interface TableDef {
|
|
|
248
250
|
checks?: readonly CheckDef[];
|
|
249
251
|
/**
|
|
250
252
|
* Index declarations for this table (plain column indexes and/or PowDB
|
|
251
|
-
* doc-field expression indexes).
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
253
|
+
* doc-field expression indexes). Plain column indexes are emitted as `CREATE [UNIQUE] INDEX` by
|
|
254
|
+
* `schemaToSQL` and added by `schemaDiff`; doc-field indexes are PowDB-only
|
|
255
|
+
* (`powqlSchemaDDL`). Both are carried onto `IndexMetadata` by
|
|
256
|
+
* `schemaDefToMetadata`.
|
|
254
257
|
*/
|
|
255
258
|
indexes?: readonly SchemaIndexDef[];
|
|
256
259
|
}
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -189,6 +189,11 @@ export interface ResolvedConfig {
|
|
|
189
189
|
relationNames?: Record<string, Record<string, string>>;
|
|
190
190
|
/** Column type overrides for the generator, or undefined when none is declared. */
|
|
191
191
|
columnTypes?: Record<string, string>;
|
|
192
|
+
/**
|
|
193
|
+
* The config file's `adapter`, passed to `migrate up` / `deploy` / `down` so a
|
|
194
|
+
* CockroachDB or YugabyteDB project takes the migration lock its engine supports.
|
|
195
|
+
*/
|
|
196
|
+
adapter?: import('../adapters/index.js').DatabaseAdapter;
|
|
192
197
|
}
|
|
193
198
|
export interface CliOverrides {
|
|
194
199
|
url?: string;
|
package/dist/cli/config.js
CHANGED
|
@@ -184,6 +184,7 @@ export function resolveConfig(fileConfig, overrides) {
|
|
|
184
184
|
importExtension: overrides.importExtension ?? fileConfig.importExtension ?? 'auto',
|
|
185
185
|
keepColumnNames: overrides.keepColumnNames ?? fileConfig.keepColumnNames ?? false,
|
|
186
186
|
legacyToManyUniques: overrides.legacyToManyUniques ?? fileConfig.legacyToManyUniques ?? false,
|
|
187
|
+
adapter: fileConfig.adapter,
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
/**
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* turbine-orm CLI
|
|
4
4
|
*
|
|
5
|
-
* Commands:
|
|
6
|
-
* turbine init
|
|
7
|
-
* turbine generate | pull
|
|
8
|
-
* turbine migrate-from-prisma
|
|
9
|
-
* turbine push
|
|
10
|
-
* turbine migrate create <name>
|
|
11
|
-
* turbine migrate up
|
|
12
|
-
* turbine migrate deploy
|
|
13
|
-
* turbine migrate down
|
|
14
|
-
* turbine migrate status
|
|
15
|
-
* turbine seed
|
|
16
|
-
* turbine status
|
|
17
|
-
* turbine doctor
|
|
18
|
-
* turbine studio
|
|
19
|
-
* turbine mcp
|
|
20
|
-
* turbine observe
|
|
21
|
-
* turbine skill
|
|
5
|
+
* Commands (each has its own `--help`, which is the full flag list):
|
|
6
|
+
* turbine init Initialize a Turbine project
|
|
7
|
+
* turbine generate | pull Introspect the database and generate TypeScript types
|
|
8
|
+
* turbine migrate-from-prisma Parse a schema.prisma and emit a Prisma->Turbine name map + report
|
|
9
|
+
* turbine push Apply defineSchema() definitions to the database (destructive ops gated)
|
|
10
|
+
* turbine migrate create <name> Create a SQL migration file (--auto | --from-diff | --recipe <name>)
|
|
11
|
+
* turbine migrate up Apply pending migrations
|
|
12
|
+
* turbine migrate deploy Apply pending migrations without prompts
|
|
13
|
+
* turbine migrate down Roll back the last migration
|
|
14
|
+
* turbine migrate status Show migration status
|
|
15
|
+
* turbine seed Run the seed file
|
|
16
|
+
* turbine status Show a schema summary
|
|
17
|
+
* turbine doctor Index + cached-plan triage
|
|
18
|
+
* turbine studio Launch the local read-only web UI (--demo for a seeded sample DB)
|
|
19
|
+
* turbine mcp Start the read-only MCP server over JSON-RPC stdio
|
|
20
|
+
* turbine observe Launch the metrics dashboard (--metrics-url or TURBINE_OBSERVE_URL)
|
|
21
|
+
* turbine skill Install the agent query skill
|
|
22
22
|
*
|
|
23
23
|
* Usage:
|
|
24
24
|
* DATABASE_URL=postgres://... npx turbine generate
|