turbine-orm 0.76.0 → 0.77.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/dist/cjs/cli/index.js +2 -2
- package/dist/cjs/cli/migrate.js +8 -8
- package/dist/cjs/cli/studio.js +6 -3
- package/dist/cjs/client.js +10 -10
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/errors.d.ts +2 -2
- package/dist/cjs/errors.js +21 -25
- package/dist/cjs/generate.js +1 -1
- package/dist/cjs/introspect.js +5 -5
- package/dist/cjs/mssql.js +12 -12
- package/dist/cjs/mysql.js +5 -5
- package/dist/cjs/nested-write.js +31 -31
- package/dist/cjs/observe.js +1 -1
- package/dist/cjs/pipeline-submittable.js +5 -1
- package/dist/cjs/pipeline.js +8 -0
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +4 -4
- package/dist/cjs/powdb-shared.d.ts +348 -0
- package/dist/cjs/powdb-shared.js +587 -0
- package/dist/cjs/powdb.d.ts +12 -299
- package/dist/cjs/powdb.js +106 -567
- package/dist/cjs/powql.d.ts +1 -1
- package/dist/cjs/powql.js +89 -89
- package/dist/cjs/prisma-compat.js +16 -16
- package/dist/cjs/query/aggregates.js +19 -20
- package/dist/cjs/query/batched-loader.js +8 -8
- package/dist/cjs/query/builder.js +8 -8
- package/dist/cjs/query/compound-unique.js +2 -2
- package/dist/cjs/query/filters.js +1 -1
- package/dist/cjs/query/relations.js +60 -48
- package/dist/cjs/query/types.js +4 -4
- package/dist/cjs/query/utils.js +5 -5
- package/dist/cjs/query/warn-registry.d.ts +7 -0
- package/dist/cjs/query/warn-registry.js +7 -0
- package/dist/cjs/query/where-compile.js +1 -1
- package/dist/cjs/query/where.js +30 -31
- package/dist/cjs/query/writes.js +7 -7
- package/dist/cjs/realtime.js +4 -4
- package/dist/cjs/schema-metadata.js +1 -1
- package/dist/cjs/schema-sql.js +3 -3
- package/dist/cjs/seed.js +1 -1
- package/dist/cjs/sqlite.js +7 -7
- package/dist/cjs/typed-sql.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/migrate.js +8 -8
- package/dist/cli/studio.js +6 -3
- package/dist/client.js +10 -10
- package/dist/dialect.js +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +21 -25
- package/dist/generate.js +1 -1
- package/dist/introspect.js +5 -5
- package/dist/mssql.js +12 -12
- package/dist/mysql.js +5 -5
- package/dist/nested-write.js +31 -31
- package/dist/observe.js +1 -1
- package/dist/pipeline-submittable.js +6 -2
- package/dist/pipeline.js +9 -1
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +2 -2
- package/dist/powdb-shared.d.ts +348 -0
- package/dist/powdb-shared.js +570 -0
- package/dist/powdb.d.ts +12 -299
- package/dist/powdb.js +71 -534
- package/dist/powql.d.ts +1 -1
- package/dist/powql.js +43 -43
- package/dist/prisma-compat.js +16 -16
- package/dist/query/aggregates.js +19 -20
- package/dist/query/batched-loader.js +8 -8
- package/dist/query/builder.js +8 -8
- package/dist/query/compound-unique.js +2 -2
- package/dist/query/filters.js +1 -1
- package/dist/query/relations.js +60 -48
- package/dist/query/types.js +4 -4
- package/dist/query/utils.js +5 -5
- package/dist/query/warn-registry.d.ts +7 -0
- package/dist/query/warn-registry.js +7 -0
- package/dist/query/where-compile.js +1 -1
- package/dist/query/where.js +30 -31
- package/dist/query/writes.js +7 -7
- package/dist/realtime.js +4 -4
- package/dist/schema-metadata.js +1 -1
- package/dist/schema-sql.js +3 -3
- package/dist/seed.js +1 -1
- package/dist/sqlite.js +7 -7
- package/dist/typed-sql.js +1 -1
- package/package.json +15 -7
package/dist/cjs/query/where.js
CHANGED
|
@@ -601,7 +601,7 @@ allowFullTableScan) {
|
|
|
601
601
|
return;
|
|
602
602
|
if (allowFullTableScan === true)
|
|
603
603
|
return;
|
|
604
|
-
throw new errors_js_1.ValidationError(
|
|
604
|
+
throw new errors_js_1.ValidationError(`${operation} on "${qi.table}" refused: the \`where\` clause is empty. ` +
|
|
605
605
|
"Pass `allowFullTableScan: UNSAFE` to opt in (import { UNSAFE } from 'turbine-orm'), " +
|
|
606
606
|
'or check that your filter values are defined.');
|
|
607
607
|
}
|
|
@@ -693,13 +693,13 @@ function buildScalarClause(qi, key, value, params, andClauses) {
|
|
|
693
693
|
// A JSON-only operator on a non-JSON column was almost certainly a typo
|
|
694
694
|
// or schema mismatch. `contains`/`equals` are shared with WhereOperator
|
|
695
695
|
// (LIKE / equality), so only shape-unique keys reach here.
|
|
696
|
-
throw new errors_js_1.ValidationError(`
|
|
696
|
+
throw new errors_js_1.ValidationError(`Column "${rawColumn}" on table "${qi.table}" is not a JSON column ` +
|
|
697
697
|
`(actual type: ${getColumnPgType(qi, rawColumn)}); cannot apply JSON operator '${cls.jsonKey}'.`);
|
|
698
698
|
case 'array':
|
|
699
699
|
andClauses.push(...buildArrayFilterClauses(qi, column, value, params, cls.colType));
|
|
700
700
|
return;
|
|
701
701
|
case 'arrayThrow':
|
|
702
|
-
throw new errors_js_1.ValidationError(`
|
|
702
|
+
throw new errors_js_1.ValidationError(`Column "${rawColumn}" on table "${qi.table}" is not an array column ` +
|
|
703
703
|
`(actual type: ${getColumnPgType(qi, rawColumn)}); cannot apply array operator '${cls.arrayKey}'.`);
|
|
704
704
|
case 'textsearch':
|
|
705
705
|
andClauses.push(buildTextSearchClause(qi, column, value, params));
|
|
@@ -757,7 +757,7 @@ function relationWhereScope(qi, targetTable, meta) {
|
|
|
757
757
|
qualifier: `${qi.q(targetTable)}.`,
|
|
758
758
|
relationParent: targetTable,
|
|
759
759
|
host: scopedWhereHost(qi, meta),
|
|
760
|
-
unknownColumn: (field) => new errors_js_1.ValidationError(`
|
|
760
|
+
unknownColumn: (field) => new errors_js_1.ValidationError(`Unknown field "${field}" in relation filter for table "${targetTable}". ` +
|
|
761
761
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`),
|
|
762
762
|
};
|
|
763
763
|
}
|
|
@@ -769,7 +769,7 @@ function aliasWhereScope(qi, targetTable, meta, alias) {
|
|
|
769
769
|
qualifier: `${alias}.`,
|
|
770
770
|
relationParent: alias,
|
|
771
771
|
host: scopedWhereHost(qi, meta),
|
|
772
|
-
unknownColumn: (field) => new errors_js_1.ValidationError(`
|
|
772
|
+
unknownColumn: (field) => new errors_js_1.ValidationError(`Unknown column "${field}" in where for table "${targetTable}"`),
|
|
773
773
|
};
|
|
774
774
|
}
|
|
775
775
|
/**
|
|
@@ -844,7 +844,7 @@ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
|
844
844
|
}
|
|
845
845
|
const jsonKey = (0, filters_js_1.findJsonUniqueKey)(value);
|
|
846
846
|
if (jsonKey) {
|
|
847
|
-
throw new errors_js_1.ValidationError(`
|
|
847
|
+
throw new errors_js_1.ValidationError(`Column "${col}" on table "${scope.table}" is not a JSON column ` +
|
|
848
848
|
`(actual type: ${colType}); cannot apply JSON operator '${jsonKey}'.`);
|
|
849
849
|
}
|
|
850
850
|
}
|
|
@@ -856,7 +856,7 @@ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
|
856
856
|
}
|
|
857
857
|
const arrayKey = (0, filters_js_1.findArrayUniqueKey)(value);
|
|
858
858
|
if (arrayKey) {
|
|
859
|
-
throw new errors_js_1.ValidationError(`
|
|
859
|
+
throw new errors_js_1.ValidationError(`Column "${col}" on table "${scope.table}" is not an array column ` +
|
|
860
860
|
`(actual type: ${colType}); cannot apply array operator '${arrayKey}'.`);
|
|
861
861
|
}
|
|
862
862
|
}
|
|
@@ -997,23 +997,23 @@ depth = 0) {
|
|
|
997
997
|
// nested relation filters inside the branch keep their qualification. The
|
|
998
998
|
// fragment binds no params, so collectRelationFilterParams needs no mirror.
|
|
999
999
|
if (!relDef.through) {
|
|
1000
|
-
throw new errors_js_1.ValidationError(`
|
|
1000
|
+
throw new errors_js_1.ValidationError(`manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
1001
1001
|
}
|
|
1002
1002
|
const qJunction = qi.q(relDef.through.table);
|
|
1003
1003
|
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
|
|
1004
1004
|
const targetPk = targetMeta.primaryKey;
|
|
1005
1005
|
if (targetPk.length === 0) {
|
|
1006
|
-
throw new errors_js_1.ValidationError(`
|
|
1006
|
+
throw new errors_js_1.ValidationError(`manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
|
|
1007
1007
|
`cannot correlate the relation filter through the junction.`);
|
|
1008
1008
|
}
|
|
1009
1009
|
if (targetKeys.length !== targetPk.length) {
|
|
1010
|
-
throw new errors_js_1.ValidationError(`
|
|
1010
|
+
throw new errors_js_1.ValidationError(`manyToMany relation "${relDef.name}": through.targetKey has ${targetKeys.length} column(s) ` +
|
|
1011
1011
|
`but target "${targetTable}" primary key has ${targetPk.length}. Composite keys must pair positionally.`);
|
|
1012
1012
|
}
|
|
1013
1013
|
const sourceKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.sourceKey);
|
|
1014
1014
|
const refKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.referenceKey);
|
|
1015
1015
|
if (sourceKeys.length !== refKeys.length) {
|
|
1016
|
-
throw new errors_js_1.ValidationError(`
|
|
1016
|
+
throw new errors_js_1.ValidationError(`manyToMany relation "${relDef.name}": through.sourceKey has ${sourceKeys.length} column(s) ` +
|
|
1017
1017
|
`but referenceKey has ${refKeys.length}. Composite keys must pair positionally.`);
|
|
1018
1018
|
}
|
|
1019
1019
|
const targetLink = targetKeys
|
|
@@ -1202,9 +1202,9 @@ function assertBindableEqualityValue(qi, rawColumn, value, columnPgType, table)
|
|
|
1202
1202
|
return;
|
|
1203
1203
|
const badKeys = Object.keys(value);
|
|
1204
1204
|
throw new errors_js_1.ValidationError(badKeys.length === 0
|
|
1205
|
-
? `
|
|
1205
|
+
? `Empty filter object on "${rawColumn}" for table "${table}". ` +
|
|
1206
1206
|
`Provide a value or an operator like { gt: 1 }.`
|
|
1207
|
-
: `
|
|
1207
|
+
: `Unknown operator${badKeys.length > 1 ? 's' : ''} ` +
|
|
1208
1208
|
`${badKeys.map((k) => `"${k}"`).join(', ')} on "${rawColumn}" for table "${table}". ` +
|
|
1209
1209
|
`Supported operators: ${[...utils_js_1.OPERATOR_KEYS].join(', ')}.`);
|
|
1210
1210
|
}
|
|
@@ -1247,13 +1247,13 @@ function fingerprintAliasWhere(qi, where, targetTable) {
|
|
|
1247
1247
|
*/
|
|
1248
1248
|
function resolveColumnRef(_qi, ref, ctx, mode) {
|
|
1249
1249
|
if (mode === 'insensitive') {
|
|
1250
|
-
throw new errors_js_1.ValidationError(`
|
|
1250
|
+
throw new errors_js_1.ValidationError(`mode: 'insensitive' cannot be combined with a column reference ({ col: "${ref.col}" }). ` +
|
|
1251
1251
|
`Case-insensitive column-to-column comparison is not supported: use client.sql\`...\` ` +
|
|
1252
1252
|
`for lower(a) = lower(b).`);
|
|
1253
1253
|
}
|
|
1254
1254
|
const col = (0, utils_js_1.resolveColumnName)(ctx.meta, ref.col);
|
|
1255
1255
|
if (col === undefined) {
|
|
1256
|
-
throw new errors_js_1.ValidationError(`
|
|
1256
|
+
throw new errors_js_1.ValidationError(`Unknown field "${ref.col}" referenced by { col } in where on table "${ctx.table}". ` +
|
|
1257
1257
|
`Known fields: ${Object.keys(ctx.meta.columnMap).join(', ') || '(none)'}.`);
|
|
1258
1258
|
}
|
|
1259
1259
|
return col;
|
|
@@ -1265,7 +1265,7 @@ function resolveColumnRef(_qi, ref, ctx, mode) {
|
|
|
1265
1265
|
*/
|
|
1266
1266
|
function columnRefSql(qi, ref, ctx, mode) {
|
|
1267
1267
|
if (!ctx) {
|
|
1268
|
-
throw new errors_js_1.ValidationError(`
|
|
1268
|
+
throw new errors_js_1.ValidationError(`Column reference { col: "${ref.col}" } is not supported in this filter context.`);
|
|
1269
1269
|
}
|
|
1270
1270
|
return `${ctx.prefix}${qi.q(resolveColumnRef(qi, ref, ctx, mode))}`;
|
|
1271
1271
|
}
|
|
@@ -1434,7 +1434,7 @@ function vectorOperator(qi, field, rawColumn, metric) {
|
|
|
1434
1434
|
}
|
|
1435
1435
|
const colType = getColumnPgType(qi, rawColumn);
|
|
1436
1436
|
if (colType !== 'vector') {
|
|
1437
|
-
throw new errors_js_1.ValidationError(`
|
|
1437
|
+
throw new errors_js_1.ValidationError(`Column "${field}" on table "${qi.table}" is not a vector column ` +
|
|
1438
1438
|
`(actual type: ${colType}); cannot apply a vector distance operation.`);
|
|
1439
1439
|
}
|
|
1440
1440
|
// ownLookup, not a bare index: an inherited Object.prototype member
|
|
@@ -1442,7 +1442,7 @@ function vectorOperator(qi, field, rawColumn, metric) {
|
|
|
1442
1442
|
// and be spliced into the ORDER BY / WHERE clause as its source text.
|
|
1443
1443
|
const op = (0, utils_js_1.ownLookup)(filters_js_1.VECTOR_METRIC_OPERATORS, metric);
|
|
1444
1444
|
if (!op) {
|
|
1445
|
-
throw new errors_js_1.ValidationError(`
|
|
1445
|
+
throw new errors_js_1.ValidationError(`Unknown vector metric "${metric}" for column "${field}". ` +
|
|
1446
1446
|
`Valid metrics: ${Object.keys(filters_js_1.VECTOR_METRIC_OPERATORS).join(', ')}.`);
|
|
1447
1447
|
}
|
|
1448
1448
|
return op;
|
|
@@ -1459,11 +1459,11 @@ function pushVectorParam(qi, field, _rawColumn, to, params) {
|
|
|
1459
1459
|
throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', qi.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
|
|
1460
1460
|
}
|
|
1461
1461
|
if (!Array.isArray(to) || to.length === 0) {
|
|
1462
|
-
throw new errors_js_1.ValidationError(`
|
|
1462
|
+
throw new errors_js_1.ValidationError(`Vector distance on "${field}" requires a non-empty array of numbers for "to".`);
|
|
1463
1463
|
}
|
|
1464
1464
|
for (const el of to) {
|
|
1465
1465
|
if (typeof el !== 'number' || !Number.isFinite(el)) {
|
|
1466
|
-
throw new errors_js_1.ValidationError(`
|
|
1466
|
+
throw new errors_js_1.ValidationError(`Vector "to" for column "${field}" must contain only finite numbers; ` + `got ${JSON.stringify(el)}.`);
|
|
1467
1467
|
}
|
|
1468
1468
|
}
|
|
1469
1469
|
// Bind as a pgvector text literal '[1,2,3]'. Elements are already validated
|
|
@@ -1569,14 +1569,14 @@ function assertJsonFilterKeys(filter, column) {
|
|
|
1569
1569
|
if (filters_js_1.JSON_FILTER_KEYS.has(key))
|
|
1570
1570
|
continue;
|
|
1571
1571
|
const suggestion = JSON_PRISMA_SPELLINGS[key];
|
|
1572
|
-
throw new errors_js_1.ValidationError(`
|
|
1572
|
+
throw new errors_js_1.ValidationError(`Unknown JSON filter operator "${key}" on ${column}.` +
|
|
1573
1573
|
(suggestion ? ` Did you mean \`${suggestion}\`?` : '') +
|
|
1574
1574
|
` Supported operators: ${[...filters_js_1.JSON_FILTER_KEYS].sort().join(', ')}.`);
|
|
1575
1575
|
}
|
|
1576
1576
|
// `mode` and `path` are modifiers, not comparisons: a filter made only of
|
|
1577
1577
|
// those compares nothing, which is exactly the silent no-op shape above.
|
|
1578
1578
|
if (present.length > 0 && present.every((k) => k === 'path' || k === 'mode')) {
|
|
1579
|
-
throw new errors_js_1.ValidationError(`
|
|
1579
|
+
throw new errors_js_1.ValidationError(`JSON filter on ${column} selects a \`path\` but has no comparison. ` +
|
|
1580
1580
|
`Add one of: ${[...filters_js_1.JSON_FILTER_KEYS]
|
|
1581
1581
|
.filter((k) => k !== 'path' && k !== 'mode')
|
|
1582
1582
|
.sort()
|
|
@@ -1608,14 +1608,13 @@ function jsonStringEntries(filter, column) {
|
|
|
1608
1608
|
if (value === undefined)
|
|
1609
1609
|
continue;
|
|
1610
1610
|
if (filter.path === undefined) {
|
|
1611
|
-
throw new errors_js_1.ValidationError(`
|
|
1611
|
+
throw new errors_js_1.ValidationError(`JSON operator '${op}' on ${column} requires a \`path\` ` +
|
|
1612
1612
|
`(e.g. { path: ['meta', 'title'], ${op}: ${valueForMessage(value)} }).`);
|
|
1613
1613
|
}
|
|
1614
1614
|
if (typeof value !== 'string') {
|
|
1615
1615
|
// `typeof` rather than the value: it says everything the reader needs
|
|
1616
1616
|
// (they passed a number where a string belongs) and carries no data.
|
|
1617
|
-
throw new errors_js_1.ValidationError(`
|
|
1618
|
-
`(${valueForMessage(value)}).`);
|
|
1617
|
+
throw new errors_js_1.ValidationError(`JSON operator '${op}' on ${column} requires a string, got ${typeof value} ` + `(${valueForMessage(value)}).`);
|
|
1619
1618
|
}
|
|
1620
1619
|
entries.push({ op, pattern, value });
|
|
1621
1620
|
}
|
|
@@ -1628,15 +1627,15 @@ function jsonRangeEntries(_qi, filter, column) {
|
|
|
1628
1627
|
if (value === undefined)
|
|
1629
1628
|
continue;
|
|
1630
1629
|
if (filter.path === undefined) {
|
|
1631
|
-
throw new errors_js_1.ValidationError(`
|
|
1630
|
+
throw new errors_js_1.ValidationError(`JSON range operator '${op}' on ${column} requires a \`path\` ` +
|
|
1632
1631
|
`(e.g. { path: ['meta', 'score'], ${op}: ${valueForMessage(value)} }).`);
|
|
1633
1632
|
}
|
|
1634
1633
|
if (typeof value !== 'number' && typeof value !== 'string') {
|
|
1635
|
-
throw new errors_js_1.ValidationError(`
|
|
1634
|
+
throw new errors_js_1.ValidationError(`JSON range operator '${op}' on ${column} requires a number or string, ` +
|
|
1636
1635
|
`got ${typeof value} (${valueForMessage(value)}).`);
|
|
1637
1636
|
}
|
|
1638
1637
|
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
1639
|
-
throw new errors_js_1.ValidationError(`
|
|
1638
|
+
throw new errors_js_1.ValidationError(`JSON range operator '${op}' on ${column} requires a finite number.`);
|
|
1640
1639
|
}
|
|
1641
1640
|
entries.push({ sqlOp, value });
|
|
1642
1641
|
}
|
|
@@ -1821,13 +1820,13 @@ function vectorThresholdEntries(filter, field) {
|
|
|
1821
1820
|
if (threshold === undefined)
|
|
1822
1821
|
continue;
|
|
1823
1822
|
if (typeof threshold !== 'number' || !Number.isFinite(threshold)) {
|
|
1824
|
-
throw new errors_js_1.ValidationError(`
|
|
1823
|
+
throw new errors_js_1.ValidationError(`Vector distance threshold "${cmp}" on "${field}" must be a finite number; ` +
|
|
1825
1824
|
`got ${typeof threshold} (${valueForMessage(threshold)}).`);
|
|
1826
1825
|
}
|
|
1827
1826
|
entries.push({ sqlOp, threshold });
|
|
1828
1827
|
}
|
|
1829
1828
|
if (entries.length === 0) {
|
|
1830
|
-
throw new errors_js_1.ValidationError(`
|
|
1829
|
+
throw new errors_js_1.ValidationError(`Vector distance filter on "${field}" requires at least one comparison (lt / lte / gt / gte).`);
|
|
1831
1830
|
}
|
|
1832
1831
|
return entries;
|
|
1833
1832
|
}
|
|
@@ -1839,7 +1838,7 @@ function buildTextSearchClause(qi, column, filter, params) {
|
|
|
1839
1838
|
requireFullTextSearch(qi);
|
|
1840
1839
|
const config = filter.config ?? 'english';
|
|
1841
1840
|
if (!(0, filters_js_1.validateTextSearchConfig)(config)) {
|
|
1842
|
-
throw new errors_js_1.ValidationError(`
|
|
1841
|
+
throw new errors_js_1.ValidationError(`Invalid text search config "${config}": only alphanumeric characters and underscores are allowed.`);
|
|
1843
1842
|
}
|
|
1844
1843
|
params.push(filter.search);
|
|
1845
1844
|
return `to_tsvector('${config}', ${column}) @@ to_tsquery('${config}', ${qi.p(params.length)})`;
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -195,7 +195,7 @@ function buildCreate(qi, args) {
|
|
|
195
195
|
throw new errors_js_1.NotFoundError({
|
|
196
196
|
table: qi.table,
|
|
197
197
|
operation: 'create',
|
|
198
|
-
message: `
|
|
198
|
+
message: `create on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
return parseWriteRow(qi, row);
|
|
@@ -307,7 +307,7 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
|
307
307
|
parts.push(`does not supply ${quoteList(missing)}`);
|
|
308
308
|
if (unexpected.length > 0)
|
|
309
309
|
parts.push(`supplies ${quoteList(unexpected)}, which the first row does not`);
|
|
310
|
-
throw new errors_js_1.ValidationError(`
|
|
310
|
+
throw new errors_js_1.ValidationError(`createMany on "${qi.table}": row ${i} ${parts.join(' and ')}. ` +
|
|
311
311
|
'Every row must supply the same fields: createMany builds ONE statement whose column list comes from the ' +
|
|
312
312
|
"first row, so a field a later row omits would be written as NULL over that column's default, and a field " +
|
|
313
313
|
'only a later row names would be dropped. Supply the field explicitly on every row (a field set to ' +
|
|
@@ -638,7 +638,7 @@ function buildUpsert(qi, args) {
|
|
|
638
638
|
table: qi.table,
|
|
639
639
|
where: args.where,
|
|
640
640
|
operation: 'upsert',
|
|
641
|
-
message: `
|
|
641
|
+
message: `upsert on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
642
642
|
});
|
|
643
643
|
}
|
|
644
644
|
return parseWriteRow(qi, row);
|
|
@@ -880,7 +880,7 @@ function parseWriteRow(qi, row) {
|
|
|
880
880
|
*/
|
|
881
881
|
function assertWritable(qi, operation) {
|
|
882
882
|
if (qi.tableMeta.isView) {
|
|
883
|
-
throw new errors_js_1.ValidationError(`
|
|
883
|
+
throw new errors_js_1.ValidationError(`Cannot ${operation} "${qi.table}": it is a view (read-only). ` +
|
|
884
884
|
'Views support reads (findMany/findFirst/…) but not writes.');
|
|
885
885
|
}
|
|
886
886
|
}
|
|
@@ -897,7 +897,7 @@ function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
897
897
|
continue;
|
|
898
898
|
const col = qi.tableMeta.columns.find((c) => c.field === key || c.name === key || c.name === (0, schema_js_1.camelToSnake)(key));
|
|
899
899
|
if (col?.isGeneratedStored) {
|
|
900
|
-
throw new errors_js_1.ValidationError(`
|
|
900
|
+
throw new errors_js_1.ValidationError(`Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
|
|
901
901
|
'column whose value the database computes, remove it from your data.');
|
|
902
902
|
}
|
|
903
903
|
}
|
|
@@ -941,7 +941,7 @@ function buildSetClause(qi, key, value, params) {
|
|
|
941
941
|
}
|
|
942
942
|
// Arithmetic operators: must be finite numbers
|
|
943
943
|
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
944
|
-
throw new errors_js_1.ValidationError(`
|
|
944
|
+
throw new errors_js_1.ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
|
|
945
945
|
}
|
|
946
946
|
if (op === 'increment') {
|
|
947
947
|
params.push(opValue);
|
|
@@ -995,7 +995,7 @@ function assertBindableSetValue(qi, key, value) {
|
|
|
995
995
|
const column = qi.toColumn(key);
|
|
996
996
|
if (whereMod.isJsonColumnType(qi, whereMod.getColumnPgType(qi, column)))
|
|
997
997
|
return;
|
|
998
|
-
throw new errors_js_1.ValidationError(`
|
|
998
|
+
throw new errors_js_1.ValidationError(`Unknown update operator "${keys[0]}" on "${qi.table}.${key}". ` +
|
|
999
999
|
`Supported: ${[...filters_js_1.UPDATE_OPERATOR_KEYS].join(', ')}. ` +
|
|
1000
1000
|
'A plain object is only written as a value on a json/jsonb column.');
|
|
1001
1001
|
}
|
package/dist/cjs/realtime.js
CHANGED
|
@@ -52,13 +52,13 @@ const MAX_CHANNEL_LEN = 63;
|
|
|
52
52
|
*/
|
|
53
53
|
function validateChannel(channel) {
|
|
54
54
|
if (typeof channel !== 'string' || channel.length === 0) {
|
|
55
|
-
throw new errors_js_1.ValidationError('
|
|
55
|
+
throw new errors_js_1.ValidationError('$listen/$notify channel must be a non-empty string');
|
|
56
56
|
}
|
|
57
57
|
if (channel.length > MAX_CHANNEL_LEN) {
|
|
58
|
-
throw new errors_js_1.ValidationError(
|
|
58
|
+
throw new errors_js_1.ValidationError(`$listen/$notify channel "${channel}" exceeds the ${MAX_CHANNEL_LEN}-character Postgres identifier limit`);
|
|
59
59
|
}
|
|
60
60
|
if (!CHANNEL_REGEX.test(channel)) {
|
|
61
|
-
throw new errors_js_1.ValidationError(`
|
|
61
|
+
throw new errors_js_1.ValidationError(`Invalid $listen/$notify channel "${channel}", must match /^[A-Za-z_][A-Za-z0-9_]*$/ ` +
|
|
62
62
|
'(letters, digits, underscores; cannot start with a digit)');
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -86,7 +86,7 @@ async function createSubscription(pool, channel, quotedChannel, handler, onClose
|
|
|
86
86
|
// give the connection straight back.
|
|
87
87
|
if (typeof client.on !== 'function') {
|
|
88
88
|
client.release?.();
|
|
89
|
-
throw new errors_js_1.ConnectionError('
|
|
89
|
+
throw new errors_js_1.ConnectionError('$listen requires a persistent connection that can push notifications. ' +
|
|
90
90
|
'The configured pool returned a client with no event support (stateless HTTP drivers ' +
|
|
91
91
|
'like Neon HTTP / Vercel Postgres cannot LISTEN). Use a TCP pg.Pool for LISTEN/NOTIFY.');
|
|
92
92
|
}
|
|
@@ -133,7 +133,7 @@ function mapIndexes(tableDef, declared) {
|
|
|
133
133
|
// index instead of emitting malformed PowQL later.
|
|
134
134
|
for (const seg of idx.path) {
|
|
135
135
|
if (typeof seg === 'number' && (!Number.isInteger(seg) || seg < 0)) {
|
|
136
|
-
throw new errors_js_1.ValidationError(`
|
|
136
|
+
throw new errors_js_1.ValidationError(`Doc-field index "${name}" on "${tableDef.name}": array-index path segment ${seg} must be a ` +
|
|
137
137
|
'non-negative integer (a JSON array index). Use a string for an object key.');
|
|
138
138
|
}
|
|
139
139
|
}
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -90,7 +90,7 @@ function resolveDdlType(config, dialect, columnName) {
|
|
|
90
90
|
const dims = config.vectorDimensions;
|
|
91
91
|
// pgvector caps a `vector` column at 16000 dimensions.
|
|
92
92
|
if (typeof dims !== 'number' || !Number.isInteger(dims) || dims < 1 || dims > 16000) {
|
|
93
|
-
throw new errors_js_1.ValidationError(`
|
|
93
|
+
throw new errors_js_1.ValidationError(`Column "${columnName}": vector dimensions must be an integer between 1 and 16000, got ${String(dims)}.`);
|
|
94
94
|
}
|
|
95
95
|
base = `vector(${dims})`;
|
|
96
96
|
}
|
|
@@ -1545,7 +1545,7 @@ async function pinSearchPath(client, pgSchema) {
|
|
|
1545
1545
|
pgSchema,
|
|
1546
1546
|
]);
|
|
1547
1547
|
if (!present.rows[0]?.present) {
|
|
1548
|
-
throw new errors_js_1.ValidationError(`
|
|
1548
|
+
throw new errors_js_1.ValidationError(`Schema "${pgSchema}" does not exist in this database. ` +
|
|
1549
1549
|
`Postgres accepts a missing namespace in search_path without complaint, so pushing anyway would ` +
|
|
1550
1550
|
`create every table in whichever schema resolves next (usually public) and report success. ` +
|
|
1551
1551
|
`Create the schema first (CREATE SCHEMA "${pgSchema}") or correct the configured schema name.`);
|
|
@@ -1561,7 +1561,7 @@ async function pinSearchPath(client, pgSchema) {
|
|
|
1561
1561
|
}
|
|
1562
1562
|
/** Format the destructive-push refusal message (mirrors the migrate gate copy). */
|
|
1563
1563
|
function formatDestructivePushError(hits) {
|
|
1564
|
-
const lines = ['
|
|
1564
|
+
const lines = ['Refusing to apply schema changes containing DESTRUCTIVE statements:', ''];
|
|
1565
1565
|
for (const h of hits) {
|
|
1566
1566
|
lines.push(` - [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
1567
1567
|
}
|
package/dist/cjs/seed.js
CHANGED
|
@@ -132,7 +132,7 @@ function markSeedRan() {
|
|
|
132
132
|
async function runSeed(fn) {
|
|
133
133
|
const connectionString = process.env.DATABASE_URL;
|
|
134
134
|
if (!connectionString) {
|
|
135
|
-
throw new errors_js_1.ConnectionError('
|
|
135
|
+
throw new errors_js_1.ConnectionError('DATABASE_URL is required to run this seed.');
|
|
136
136
|
}
|
|
137
137
|
const db = new client_js_1.TurbineClient({ connectionString }, emptySchema);
|
|
138
138
|
try {
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -115,11 +115,11 @@ function loadDatabaseSync() {
|
|
|
115
115
|
ctor = req('node:sqlite').DatabaseSync;
|
|
116
116
|
}
|
|
117
117
|
catch (err) {
|
|
118
|
-
throw new errors_js_1.ConnectionError("
|
|
118
|
+
throw new errors_js_1.ConnectionError("turbine-orm/sqlite requires Node's built-in 'node:sqlite' module (Node >= 22.5). " +
|
|
119
119
|
`Upgrade Node to >= 22.5, or pass an already-open better-sqlite3-compatible handle. (${err.message})`);
|
|
120
120
|
}
|
|
121
121
|
if (typeof ctor !== 'function') {
|
|
122
|
-
throw new errors_js_1.ConnectionError("
|
|
122
|
+
throw new errors_js_1.ConnectionError("'node:sqlite' loaded but did not export a DatabaseSync constructor, this Node build may lack SQLite support.");
|
|
123
123
|
}
|
|
124
124
|
cachedDatabaseSync = ctor;
|
|
125
125
|
return cachedDatabaseSync;
|
|
@@ -370,17 +370,17 @@ function augmentSqliteError(err) {
|
|
|
370
370
|
function sqliteLogicError(message, cause) {
|
|
371
371
|
const missingTable = /^no such table:\s*(\S+)/i.exec(message);
|
|
372
372
|
if (missingTable) {
|
|
373
|
-
return new errors_js_1.ValidationError(`
|
|
373
|
+
return new errors_js_1.ValidationError(`SQLite has no table "${missingTable[1]}": ${message}. ` +
|
|
374
374
|
'Create it first (run your migrations, or execute the CREATE TABLE statements for this schema); ' +
|
|
375
375
|
'an in-memory database starts empty on every connection.', { cause });
|
|
376
376
|
}
|
|
377
377
|
const missingColumn = /^no such column:\s*(\S+)/i.exec(message);
|
|
378
378
|
if (missingColumn) {
|
|
379
|
-
return new errors_js_1.ValidationError(`
|
|
379
|
+
return new errors_js_1.ValidationError(`SQLite has no column "${missingColumn[1]}": ${message}. ` +
|
|
380
380
|
'Check the spelling against the table as it exists in this database, ' +
|
|
381
381
|
'and re-run `turbine generate` if the schema has changed.', { cause });
|
|
382
382
|
}
|
|
383
|
-
return new errors_js_1.ValidationError(`
|
|
383
|
+
return new errors_js_1.ValidationError(`SQLite rejected the statement: ${message}`, { cause });
|
|
384
384
|
}
|
|
385
385
|
function normalizeQueryArgs(arg, values) {
|
|
386
386
|
if (typeof arg === 'string')
|
|
@@ -995,7 +995,7 @@ async function introspectSqlite(options) {
|
|
|
995
995
|
db = new DatabaseSync(options.connectionString);
|
|
996
996
|
}
|
|
997
997
|
catch (err) {
|
|
998
|
-
throw new errors_js_1.ConnectionError(`
|
|
998
|
+
throw new errors_js_1.ConnectionError(`Failed to open SQLite database "${options.connectionString}": ${err.message}`);
|
|
999
999
|
}
|
|
1000
1000
|
try {
|
|
1001
1001
|
return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude });
|
|
@@ -1011,7 +1011,7 @@ function openSqliteDatabase(target, options) {
|
|
|
1011
1011
|
db = new DatabaseSync(target);
|
|
1012
1012
|
}
|
|
1013
1013
|
catch (err) {
|
|
1014
|
-
throw new errors_js_1.ConnectionError(`
|
|
1014
|
+
throw new errors_js_1.ConnectionError(`Failed to open SQLite database "${target}": ${err.message}`);
|
|
1015
1015
|
}
|
|
1016
1016
|
db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
|
|
1017
1017
|
if (options.foreignKeys !== false)
|
package/dist/cjs/typed-sql.js
CHANGED
|
@@ -70,7 +70,7 @@ function buildTypedSql(strings, values, dialect = dialect_js_1.postgresDialect)
|
|
|
70
70
|
// silently desync placeholders from params (pg would reject it, but fail
|
|
71
71
|
// loudly here instead).
|
|
72
72
|
if (strings.length !== values.length + 1) {
|
|
73
|
-
throw new errors_js_1.ValidationError(`
|
|
73
|
+
throw new errors_js_1.ValidationError(`sql template segment/value count mismatch: ${strings.length} segments, ${values.length} values.`);
|
|
74
74
|
}
|
|
75
75
|
let sql = '';
|
|
76
76
|
for (let i = 0; i < strings.length; i++) {
|
package/dist/cli/index.js
CHANGED
|
@@ -2587,7 +2587,7 @@ async function cmdMigrateDeploy(args, config) {
|
|
|
2587
2587
|
error('Deploy blocked by migration drift');
|
|
2588
2588
|
errorLine();
|
|
2589
2589
|
for (const line of formatChecksumMismatchError(plan.mismatches).split('\n')) {
|
|
2590
|
-
errorLine(
|
|
2590
|
+
errorLine(line);
|
|
2591
2591
|
}
|
|
2592
2592
|
errorLine();
|
|
2593
2593
|
process.exit(1);
|
|
@@ -2661,7 +2661,7 @@ async function confirmDestructive(report) {
|
|
|
2661
2661
|
error('DESTRUCTIVE MIGRATION DETECTED');
|
|
2662
2662
|
newline();
|
|
2663
2663
|
for (const line of report.split('\n'))
|
|
2664
|
-
console.log(` ${line
|
|
2664
|
+
console.log(` ${line}`);
|
|
2665
2665
|
newline();
|
|
2666
2666
|
if (!process.stdin.isTTY) {
|
|
2667
2667
|
console.log(` ${dim('Non-interactive shell: rerun with')} ${cyan('--allow-destructive')} ${dim('to proceed.')}`);
|
package/dist/cli/migrate.js
CHANGED
|
@@ -55,7 +55,7 @@ export function assertNoEmbeddedTransactions(files, section) {
|
|
|
55
55
|
if (offenders.length === 0)
|
|
56
56
|
return;
|
|
57
57
|
const lines = [
|
|
58
|
-
`
|
|
58
|
+
`Refusing to run migrations that manage transactions themselves (${section.toUpperCase()} section):`,
|
|
59
59
|
'',
|
|
60
60
|
];
|
|
61
61
|
for (const o of offenders) {
|
|
@@ -307,7 +307,7 @@ export function parseMigrationContent(content, source) {
|
|
|
307
307
|
}
|
|
308
308
|
if (!sawUpMarker) {
|
|
309
309
|
throw new MigrationError([
|
|
310
|
-
`
|
|
310
|
+
`Migration file has no \`-- UP\` section marker${source ? `: ${source}` : '.'}`,
|
|
311
311
|
'',
|
|
312
312
|
'A migration must contain a line reading `-- UP` (a `-- DOWN` line is optional).',
|
|
313
313
|
'Without it the whole file is a header comment: nothing would run, and the',
|
|
@@ -586,7 +586,7 @@ export function createMigration(migrationsDir, name, autoContent, options) {
|
|
|
586
586
|
const recipe = MIGRATION_RECIPES[options.recipe];
|
|
587
587
|
if (!recipe) {
|
|
588
588
|
const known = Object.keys(MIGRATION_RECIPES).join(', ') || '(none)';
|
|
589
|
-
throw new MigrationError(`
|
|
589
|
+
throw new MigrationError(`Unknown migration recipe "${options.recipe}". Available recipes: ${known}`);
|
|
590
590
|
}
|
|
591
591
|
// A recipe builds the BODY, below `-- UP`, where a newline is not merely a
|
|
592
592
|
// comment break but directly executable, so it gets the safe name too.
|
|
@@ -818,7 +818,7 @@ export function formatChecksumMismatchError(mismatches) {
|
|
|
818
818
|
const modified = mismatches.filter((m) => m.type === 'modified');
|
|
819
819
|
const missing = mismatches.filter((m) => m.type === 'missing');
|
|
820
820
|
const lines = [
|
|
821
|
-
'
|
|
821
|
+
'Migration drift detected, refusing to apply pending migrations.',
|
|
822
822
|
'',
|
|
823
823
|
'Applied migrations should be immutable. The following files no longer match their applied state:',
|
|
824
824
|
'',
|
|
@@ -935,7 +935,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
935
935
|
const adapter = options?.adapter;
|
|
936
936
|
const lock = await acquireMigrationLock(client, lockId, adapter, () => openLockConnection(connectionString));
|
|
937
937
|
if (!lock.acquired) {
|
|
938
|
-
throw new MigrationError('
|
|
938
|
+
throw new MigrationError('Could not acquire migration lock, another migration is already running');
|
|
939
939
|
}
|
|
940
940
|
try {
|
|
941
941
|
await ensureTrackingTable(client, dialect);
|
|
@@ -979,7 +979,7 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
979
979
|
// pass `allowDestructive: true`. Safe-by-default is the whole point, a
|
|
980
980
|
// DROP TABLE should never run just because a file exists.
|
|
981
981
|
if (!options?.allowDestructive && destructive.length > 0) {
|
|
982
|
-
const lines = ['
|
|
982
|
+
const lines = ['Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
983
983
|
for (const o of destructive) {
|
|
984
984
|
lines.push(` ${o.file}`);
|
|
985
985
|
for (const h of o.hits) {
|
|
@@ -1159,7 +1159,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
1159
1159
|
const adapter = options?.adapter;
|
|
1160
1160
|
const lock = await acquireMigrationLock(client, lockId, adapter, () => openLockConnection(connectionString));
|
|
1161
1161
|
if (!lock.acquired) {
|
|
1162
|
-
throw new MigrationError('
|
|
1162
|
+
throw new MigrationError('Could not acquire migration lock, another migration is already running');
|
|
1163
1163
|
}
|
|
1164
1164
|
try {
|
|
1165
1165
|
await ensureTrackingTable(client, dialect);
|
|
@@ -1191,7 +1191,7 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
1191
1191
|
offenders.push({ file: file.filename, hits });
|
|
1192
1192
|
}
|
|
1193
1193
|
if (offenders.length > 0) {
|
|
1194
|
-
const lines = ['
|
|
1194
|
+
const lines = ['Refusing to roll back migrations whose DOWN sections are DESTRUCTIVE:', ''];
|
|
1195
1195
|
for (const o of offenders) {
|
|
1196
1196
|
lines.push(` ${o.file}`);
|
|
1197
1197
|
for (const h of o.hits) {
|
package/dist/cli/studio.js
CHANGED
|
@@ -391,6 +391,9 @@ function constantTimeEqual(a, b) {
|
|
|
391
391
|
function unknownTableMessage(name, ctx) {
|
|
392
392
|
const available = Object.keys(ctx.metadata.tables);
|
|
393
393
|
const list = available.length ? available.join(', ') : '(none)';
|
|
394
|
+
// turbine-prefix-ok: every caller passes this straight to sendJson as an HTTP
|
|
395
|
+
// body, never to a TurbineError, so nothing prepends a `[TURBINE_EXXX]` tag
|
|
396
|
+
// and the prefix is the browser's only signal of who produced the message.
|
|
394
397
|
return `[turbine] Unknown table "${name}" in schema "${ctx.options.schema}". Available: ${list}`;
|
|
395
398
|
}
|
|
396
399
|
// ---------------------------------------------------------------------------
|
|
@@ -816,17 +819,17 @@ function assertNoPiiPredicates(args, tableName, metadata, showPii) {
|
|
|
816
819
|
metadata,
|
|
817
820
|
hiddenReason: (table, column) => (isRedactedColumn(table, column, showPii) ? 'is PII-tagged and redacted' : null),
|
|
818
821
|
refuseColumn: (table, column, reason) => {
|
|
819
|
-
throw new ValidationError(`
|
|
822
|
+
throw new ValidationError(`Column "${column}" on "${table.name}" ${reason}, so it cannot be used ` +
|
|
820
823
|
`in a where, orderBy, cursor, or distinct: filtering, sorting, paging, or de-duplicating on a hidden ` +
|
|
821
824
|
`value reveals it. Restart Studio with --show-pii to query it.`);
|
|
822
825
|
},
|
|
823
826
|
refuseDepth: (maxDepth) => {
|
|
824
|
-
throw new ValidationError(`
|
|
827
|
+
throw new ValidationError(`Query is nested more than ${maxDepth} levels deep, which is past the point where ` +
|
|
825
828
|
`Studio can prove it does not filter or sort on a PII-tagged and redacted column, so it is refused. ` +
|
|
826
829
|
`Flatten the query, or restart Studio with --show-pii.`);
|
|
827
830
|
},
|
|
828
831
|
refuseShape: (table, key) => {
|
|
829
|
-
throw new ValidationError(`
|
|
832
|
+
throw new ValidationError(`Studio does not recognize "${key}" in a query on "${table.name}", so it cannot prove the ` +
|
|
830
833
|
`query does not filter or sort on a PII-tagged and redacted column, and refuses it rather than ` +
|
|
831
834
|
`guessing. Remove it, or restart Studio with --show-pii.`);
|
|
832
835
|
},
|