turbine-orm 0.91.0 → 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.
@@ -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
- for (const [relName, relDef] of getRelationEntries(qi, table, meta)) {
1518
- const rawValue = row[relName];
1519
- if (rawValue === undefined)
1520
- continue;
1521
- // --- Short-circuit: skip JSON.parse for common empty/null cases ---
1522
- // hasMany returns '[]' (from COALESCE(..., '[]'::json)); belongsTo/hasOne returns null
1523
- if (rawValue === null || rawValue === 'null') {
1524
- parsed[relName] = null;
1525
- continue;
1526
- }
1527
- if (rawValue === '[]') {
1528
- parsed[relName] = [];
1529
- continue;
1530
- }
1531
- if (Array.isArray(rawValue) && rawValue.length === 0) {
1532
- parsed[relName] = [];
1533
- continue;
1534
- }
1535
- // --- Non-empty values: full parse path ---
1536
- if (typeof rawValue === 'string') {
1537
- try {
1538
- const jsonVal = JSON.parse(rawValue);
1539
- // After parsing, recurse via parseNestedRow so each item gets date
1540
- // coercion AND its own sub-relations parsed at arbitrary depth.
1541
- if (Array.isArray(jsonVal)) {
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
- catch (err) {
1554
- // A relation column that does not parse is a bug in the emitted SQL or
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
- else if (Array.isArray(rawValue)) {
1574
- parsed[relName] = rawValue.map((item) => typeof item === 'object' && item !== null
1575
- ? parseNestedRow(qi, item, relDef.to, true)
1576
- : item);
1577
- }
1578
- else if (typeof rawValue === 'object' && rawValue !== null) {
1579
- parsed[relName] = parseNestedRow(qi, rawValue, relDef.to, true);
1580
- }
1581
- else {
1582
- parsed[relName] = rawValue;
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
- return parsed;
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 it decodes each relation's
1772
- * positional arrays into the object form first (shapes built once, not per
1773
- * row), then delegates to parseNestedRow for date/snake-camel coercion.
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
@@ -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 (read back only via an explicit `select` or
77
- * `includePii: UNSAFE`, the imported sentinel symbol) and redacted by Studio.
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). Consumed by the PowDB DDL generator
252
- * (`powqlSchemaDDL`) and carried onto `IndexMetadata` by
253
- * `schemaDefToMetadata`; the SQL DDL generators do not consume them yet.
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
  }
@@ -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;
@@ -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
  /**
@@ -2,23 +2,23 @@
2
2
  /**
3
3
  * turbine-orm CLI
4
4
  *
5
- * Commands:
6
- * turbine init , Initialize a Turbine project
7
- * turbine generate | pull , Introspect 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 schema-builder definitions to database (--schema-file, destructive ops gated)
10
- * turbine migrate create <name> - Create a new 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 , Rollback last migration
14
- * turbine migrate status , Show migration status
15
- * turbine seed , Run seed file
16
- * turbine status , Show schema summary
17
- * turbine doctor - Index + cached-plan triage (--fix, --json, --no-concurrently, --unused, --audit, --no-plan-divergence, --allow-pooler)
18
- * turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
19
- * turbine mcp , Start read-only MCP server over JSON-RPC stdio
20
- * turbine observe , Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
21
- * turbine skill - Install the agent query skill (--print, --agents, --dir <path>)
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