turbine-orm 0.49.0 → 0.50.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 +122 -39
- package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
- package/dist/cjs/adapters/index.d.ts +110 -0
- package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
- package/dist/cjs/cli/config.d.ts +181 -0
- package/dist/cjs/cli/config.js +32 -6
- package/dist/cjs/cli/destructive.d.ts +38 -0
- package/dist/cjs/cli/index.d.ts +359 -0
- package/dist/cjs/cli/index.js +228 -56
- package/dist/cjs/cli/loader.d.ts +61 -0
- package/dist/cjs/cli/mcp.d.ts +42 -0
- package/dist/cjs/cli/migrate.d.ts +356 -0
- package/dist/cjs/cli/migrate.js +131 -40
- package/dist/cjs/cli/observe-ui.d.ts +1 -0
- package/dist/cjs/cli/observe-ui.js +14 -5
- package/dist/cjs/cli/observe.d.ts +25 -0
- package/dist/cjs/cli/observe.js +49 -12
- package/dist/cjs/cli/pii-tags.d.ts +53 -0
- package/dist/cjs/cli/prisma-report.d.ts +33 -0
- package/dist/cjs/cli/prisma-report.js +73 -0
- package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
- package/dist/cjs/cli/prisma-resolve.js +1 -0
- package/dist/cjs/cli/prisma-schema.d.ts +176 -0
- package/dist/cjs/cli/prisma-schema.js +82 -4
- package/dist/cjs/cli/rate-limit.d.ts +32 -0
- package/dist/cjs/cli/rate-limit.js +45 -0
- package/dist/cjs/cli/studio-demo.d.ts +43 -0
- package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
- package/dist/cjs/cli/studio.d.ts +207 -0
- package/dist/cjs/cli/studio.js +136 -71
- package/dist/cjs/cli/ui.d.ts +73 -0
- package/dist/cjs/cli/ui.js +51 -9
- package/dist/cjs/client.d.ts +837 -0
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/dialect.d.ts +516 -0
- package/dist/cjs/dialect.js +37 -12
- package/dist/cjs/errors.d.ts +370 -0
- package/dist/cjs/generate.d.ts +137 -0
- package/dist/cjs/generate.js +39 -6
- package/dist/cjs/index-advisor.d.ts +153 -0
- package/dist/cjs/index-stats.d.ts +384 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/introspect.d.ts +269 -0
- package/dist/cjs/mssql.d.ts +232 -0
- package/dist/cjs/mssql.js +6 -0
- package/dist/cjs/mysql.d.ts +173 -0
- package/dist/cjs/mysql.js +16 -0
- package/dist/cjs/nested-write.d.ts +96 -0
- package/dist/cjs/nested-write.js +414 -24
- package/dist/cjs/observe.d.ts +115 -0
- package/dist/cjs/optional-peer-import.d.cts +72 -0
- package/dist/cjs/pipeline-submittable.d.ts +93 -0
- package/dist/cjs/pipeline.d.ts +71 -0
- package/dist/cjs/powdb-introspect.d.ts +84 -0
- package/dist/cjs/powdb.d.ts +931 -0
- package/dist/cjs/powdb.js +106 -21
- package/dist/cjs/powql.d.ts +592 -0
- package/dist/cjs/powql.js +42 -6
- package/dist/cjs/prisma-compat.d.ts +283 -0
- package/dist/cjs/prisma-compat.js +167 -9
- package/dist/cjs/query/aggregates.d.ts +92 -0
- package/dist/cjs/query/aggregates.js +7 -3
- package/dist/cjs/query/batched-loader.d.ts +193 -0
- package/dist/cjs/query/builder.d.ts +849 -0
- package/dist/cjs/query/builder.js +571 -65
- package/dist/cjs/query/compound-unique.d.ts +51 -0
- package/dist/cjs/query/deferred.d.ts +223 -0
- package/dist/cjs/query/filters.d.ts +201 -0
- package/dist/cjs/query/index.d.ts +14 -0
- package/dist/cjs/query/index.js +6 -1
- package/dist/cjs/query/relations.d.ts +609 -0
- package/dist/cjs/query/relations.js +693 -46
- package/dist/cjs/query/types.d.ts +1300 -0
- package/dist/cjs/query/utils.d.ts +209 -0
- package/dist/cjs/query/utils.js +208 -1
- package/dist/cjs/query/warn-registry.d.ts +68 -0
- package/dist/cjs/query/warn-registry.js +9 -0
- package/dist/cjs/query/where-compile.d.ts +139 -0
- package/dist/cjs/query/where.d.ts +548 -0
- package/dist/cjs/query/where.js +58 -22
- package/dist/cjs/query/writes.d.ts +172 -0
- package/dist/cjs/query/writes.js +105 -12
- package/dist/cjs/realtime.d.ts +70 -0
- package/dist/cjs/schema-builder.d.ts +354 -0
- package/dist/cjs/schema-metadata.d.ts +83 -0
- package/dist/cjs/schema-sql.d.ts +217 -0
- package/dist/cjs/schema-sql.js +23 -5
- package/dist/cjs/schema.d.ts +356 -0
- package/dist/cjs/schema.js +125 -0
- package/dist/cjs/seed.d.ts +15 -0
- package/dist/cjs/serverless.d.ts +142 -0
- package/dist/cjs/sqlite.d.ts +143 -0
- package/dist/cjs/sqlite.js +4 -0
- package/dist/cjs/typed-sql.d.ts +102 -0
- package/dist/cli/config.d.ts +18 -4
- package/dist/cli/config.js +31 -6
- package/dist/cli/index.d.ts +123 -0
- package/dist/cli/index.js +223 -58
- package/dist/cli/migrate.d.ts +59 -10
- package/dist/cli/migrate.js +128 -41
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +14 -5
- package/dist/cli/observe.d.ts +7 -1
- package/dist/cli/observe.js +48 -12
- package/dist/cli/prisma-report.d.ts +14 -0
- package/dist/cli/prisma-report.js +72 -0
- package/dist/cli/prisma-resolve.d.ts +6 -0
- package/dist/cli/prisma-resolve.js +1 -0
- package/dist/cli/prisma-schema.d.ts +62 -2
- package/dist/cli/prisma-schema.js +81 -4
- package/dist/cli/rate-limit.d.ts +32 -0
- package/dist/cli/rate-limit.js +40 -0
- package/dist/cli/studio.d.ts +5 -5
- package/dist/cli/studio.js +135 -70
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +51 -9
- package/dist/client.d.ts +40 -0
- package/dist/client.js +3 -0
- package/dist/dialect.d.ts +17 -1
- package/dist/dialect.js +37 -12
- package/dist/generate.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +6 -0
- package/dist/mysql.js +16 -0
- package/dist/nested-write.d.ts +2 -0
- package/dist/nested-write.js +415 -25
- package/dist/powdb.d.ts +4 -2
- package/dist/powdb.js +106 -21
- package/dist/powql.d.ts +5 -0
- package/dist/powql.js +42 -6
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +166 -8
- package/dist/query/aggregates.js +7 -3
- package/dist/query/builder.d.ts +292 -21
- package/dist/query/builder.js +570 -64
- package/dist/query/deferred.d.ts +39 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +173 -5
- package/dist/query/relations.js +688 -47
- package/dist/query/types.d.ts +123 -39
- package/dist/query/utils.d.ts +116 -0
- package/dist/query/utils.js +198 -0
- package/dist/query/warn-registry.d.ts +9 -0
- package/dist/query/warn-registry.js +9 -0
- package/dist/query/where.d.ts +38 -1
- package/dist/query/where.js +58 -23
- package/dist/query/writes.d.ts +42 -1
- package/dist/query/writes.js +104 -13
- package/dist/schema-sql.d.ts +14 -0
- package/dist/schema-sql.js +23 -5
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +123 -0
- package/dist/sqlite.js +4 -0
- package/package.json +77 -28
package/dist/powdb.js
CHANGED
|
@@ -141,8 +141,11 @@ export class PowdbFloatParam {
|
|
|
141
141
|
*/
|
|
142
142
|
export class PowdbJsonParam {
|
|
143
143
|
value;
|
|
144
|
-
|
|
144
|
+
column;
|
|
145
|
+
/** `column` is diagnostic only: it names the target column when serialization fails. */
|
|
146
|
+
constructor(value, column) {
|
|
145
147
|
this.value = value;
|
|
148
|
+
this.column = column;
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
/** Minimum PowDB server version the networked transport requires. */
|
|
@@ -597,9 +600,10 @@ export function powqlSchemaDDL(schema, opts = {}) {
|
|
|
597
600
|
// Declared indexes: PowDB doc-field expression indexes (docPath) and plain
|
|
598
601
|
// single-column indexes. A doc-field index MUST be parenthesized (the engine
|
|
599
602
|
// rejects a bare JSON path); string path segments emit lexer-exact via the
|
|
600
|
-
// shared `encodePowqlString`, integer array indexes emit bare.
|
|
601
|
-
// document column
|
|
602
|
-
//
|
|
603
|
+
// shared `encodePowqlString`, integer array indexes emit bare. The json
|
|
604
|
+
// document column goes through `quotePowqlIdent` exactly like the plain
|
|
605
|
+
// index branch below, so a keyword or non-bare identifier is backtick-quoted
|
|
606
|
+
// instead of being spliced raw into the statement.
|
|
603
607
|
for (const idx of meta.indexes) {
|
|
604
608
|
const kind = idx.unique ? 'unique' : 'index';
|
|
605
609
|
if (idx.docPath) {
|
|
@@ -610,7 +614,7 @@ export function powqlSchemaDDL(schema, opts = {}) {
|
|
|
610
614
|
throw new ValidationError(`[turbine] Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
|
|
611
615
|
}
|
|
612
616
|
const segs = idx.docPath.map((s) => (typeof s === 'number' ? `->${s}` : `->${encodePowqlString(s)}`)).join('');
|
|
613
|
-
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${column}${segs})`);
|
|
617
|
+
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${quotePowqlIdent(column)}${segs})`);
|
|
614
618
|
}
|
|
615
619
|
else {
|
|
616
620
|
// Plain column index. PowDB has no composite index (`add index` takes a
|
|
@@ -718,6 +722,36 @@ async function listPowdbLinks(exec) {
|
|
|
718
722
|
function powdbCell(v) {
|
|
719
723
|
return v === null || v === undefined ? '' : String(v);
|
|
720
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Serialize a json-column value to the canonical JSON text both transports
|
|
727
|
+
* send. `JSON.stringify` can throw (circular structure, a BigInt cell, a
|
|
728
|
+
* throwing `toJSON`) and can return `undefined` (a value that serializes to
|
|
729
|
+
* nothing, e.g. a `toJSON` returning undefined); both surface as a typed E003
|
|
730
|
+
* naming the column instead of a raw TypeError or an undefined param.
|
|
731
|
+
*/
|
|
732
|
+
function powdbJsonText(param, position) {
|
|
733
|
+
const where = `for column "${param.column ?? '(unknown)'}"${position ? ` (${position})` : ''}`;
|
|
734
|
+
let json;
|
|
735
|
+
try {
|
|
736
|
+
json = JSON.stringify(param.value);
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
throw new ValidationError(`[turbine] The json value ${where} cannot be serialized for PowDB: ${err.message}`);
|
|
740
|
+
}
|
|
741
|
+
if (json === undefined) {
|
|
742
|
+
throw new ValidationError(`[turbine] The json value ${where} serializes to nothing (JSON.stringify returned undefined); ` +
|
|
743
|
+
'write `null` explicitly instead.');
|
|
744
|
+
}
|
|
745
|
+
return json;
|
|
746
|
+
}
|
|
747
|
+
/** Convert a Date to PowDB epoch micros, refusing an invalid Date (getTime() is NaN). */
|
|
748
|
+
function powdbDateMicros(value, position) {
|
|
749
|
+
const ms = value.getTime();
|
|
750
|
+
if (!Number.isFinite(ms)) {
|
|
751
|
+
throw new ValidationError(`[turbine] Invalid Date${position ? ` (${position})` : ''} cannot be encoded as a PowDB timestamp.`);
|
|
752
|
+
}
|
|
753
|
+
return BigInt(ms) * 1000n;
|
|
754
|
+
}
|
|
721
755
|
/** Coerce a JS value into a PowDB positional param (the write side). */
|
|
722
756
|
function toPowdbParam(value, col) {
|
|
723
757
|
if (value instanceof PowdbFloatParam)
|
|
@@ -725,11 +759,11 @@ function toPowdbParam(value, col) {
|
|
|
725
759
|
// json document: serialize to canonical JSON text and bind as a str param,
|
|
726
760
|
// the engine validates it as JSON and stores the canonical binary form.
|
|
727
761
|
if (value instanceof PowdbJsonParam)
|
|
728
|
-
return
|
|
762
|
+
return powdbJsonText(value);
|
|
729
763
|
if (value === undefined || value === null)
|
|
730
764
|
return null;
|
|
731
765
|
if (value instanceof Date)
|
|
732
|
-
return
|
|
766
|
+
return powdbDateMicros(value); // ms → micros (int column)
|
|
733
767
|
if (col && isDateColumn(col) && typeof value === 'number')
|
|
734
768
|
return BigInt(value) * 1000n;
|
|
735
769
|
if (typeof value === 'boolean' ||
|
|
@@ -1585,36 +1619,74 @@ function normalizeEmbeddedResult(r) {
|
|
|
1585
1619
|
* quotes, backslashes, `$N`, `"); drop … --`, raw CR, and emoji all round-trip
|
|
1586
1620
|
* as data and cannot break out of the literal or inject a second statement.
|
|
1587
1621
|
*/
|
|
1588
|
-
export function encodePowqlLiteral(value) {
|
|
1622
|
+
export function encodePowqlLiteral(value, position) {
|
|
1623
|
+
const at = position ? ` (${position})` : '';
|
|
1589
1624
|
if (value instanceof PowdbFloatParam) {
|
|
1590
1625
|
const n = value.value;
|
|
1591
1626
|
if (!Number.isFinite(n))
|
|
1592
|
-
throw new ValidationError(`[turbine] Non-finite float cannot be encoded for PowDB.`);
|
|
1627
|
+
throw new ValidationError(`[turbine] Non-finite float cannot be encoded for PowDB${at}.`);
|
|
1593
1628
|
// Force a float-form literal so an integer-valued float column stays a float.
|
|
1594
|
-
|
|
1629
|
+
const text = powqlNumberText(n);
|
|
1630
|
+
return text.includes('.') ? text : `${text}.0`;
|
|
1595
1631
|
}
|
|
1596
1632
|
// json document: emit the canonical JSON text as a PowQL string literal (the
|
|
1597
1633
|
// embedded engine validates and stores it as a json document).
|
|
1598
1634
|
if (value instanceof PowdbJsonParam)
|
|
1599
|
-
return encodePowqlString(
|
|
1635
|
+
return encodePowqlString(powdbJsonText(value, position), position);
|
|
1600
1636
|
if (value === undefined || value === null)
|
|
1601
1637
|
return 'null';
|
|
1602
1638
|
if (value instanceof Date)
|
|
1603
|
-
return `${
|
|
1639
|
+
return `${powdbDateMicros(value, position)}`; // epoch micros (int column)
|
|
1604
1640
|
if (typeof value === 'boolean')
|
|
1605
1641
|
return value ? 'true' : 'false';
|
|
1606
1642
|
if (typeof value === 'bigint')
|
|
1607
1643
|
return value.toString();
|
|
1608
1644
|
if (typeof value === 'number') {
|
|
1609
1645
|
if (!Number.isFinite(value))
|
|
1610
|
-
throw new ValidationError(`[turbine] Non-finite number cannot be encoded for PowDB.`);
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
return
|
|
1646
|
+
throw new ValidationError(`[turbine] Non-finite number cannot be encoded for PowDB${at}.`);
|
|
1647
|
+
// Renders an integer as an int literal (`42`) and a fractional number as a
|
|
1648
|
+
// float literal (`4.2`); PowQL distinguishes them by the dot.
|
|
1649
|
+
return powqlNumberText(value);
|
|
1614
1650
|
}
|
|
1615
1651
|
if (typeof value === 'string')
|
|
1616
|
-
return encodePowqlString(value);
|
|
1617
|
-
throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal.`);
|
|
1652
|
+
return encodePowqlString(value, position);
|
|
1653
|
+
throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal${at}.`);
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Render a finite JS number as PowQL numeric text.
|
|
1657
|
+
*
|
|
1658
|
+
* `String(n)` is used verbatim for every ordinary magnitude (an integer becomes
|
|
1659
|
+
* `42`, a fractional number `4.2`), but it switches to exponential notation at
|
|
1660
|
+
* `1e21` and below `1e-6` (`1e+21`, `1.5e-7`), which the PowQL lexer does not
|
|
1661
|
+
* accept as a number token (and which the float path would further mangle into
|
|
1662
|
+
* `1e+21.0`). Those two ranges are expanded into plain decimal digits so any
|
|
1663
|
+
* finite double round-trips to a literal the engine can parse.
|
|
1664
|
+
*/
|
|
1665
|
+
function powqlNumberText(n) {
|
|
1666
|
+
const s = String(n);
|
|
1667
|
+
if (!s.includes('e') && !s.includes('E'))
|
|
1668
|
+
return s;
|
|
1669
|
+
// Integer-valued doubles (|n| >= 1e21) expand exactly through BigInt.
|
|
1670
|
+
if (Number.isInteger(n))
|
|
1671
|
+
return BigInt(n).toString();
|
|
1672
|
+
const m = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
|
|
1673
|
+
if (!m) {
|
|
1674
|
+
throw new ValidationError(`[turbine] Number ${s} cannot be rendered as a PowDB literal.`);
|
|
1675
|
+
}
|
|
1676
|
+
const sign = m[1] ?? '';
|
|
1677
|
+
const intPart = m[2] ?? '';
|
|
1678
|
+
const fracPart = m[3] ?? '';
|
|
1679
|
+
const exp = Number(m[4]);
|
|
1680
|
+
const digits = intPart + fracPart;
|
|
1681
|
+
const pointPos = intPart.length + exp;
|
|
1682
|
+
let body;
|
|
1683
|
+
if (pointPos <= 0)
|
|
1684
|
+
body = `0.${'0'.repeat(-pointPos)}${digits}`;
|
|
1685
|
+
else if (pointPos >= digits.length)
|
|
1686
|
+
body = digits + '0'.repeat(pointPos - digits.length);
|
|
1687
|
+
else
|
|
1688
|
+
body = `${digits.slice(0, pointPos)}.${digits.slice(pointPos)}`;
|
|
1689
|
+
return sign + body;
|
|
1618
1690
|
}
|
|
1619
1691
|
/**
|
|
1620
1692
|
* The newest PowDB engine LINE (major.minor) whose lexer escape handling
|
|
@@ -1646,8 +1718,21 @@ export function encodePowqlLiteral(value) {
|
|
|
1646
1718
|
* already covers every 0.19.x patch — no bump is needed for 0.19.1.
|
|
1647
1719
|
*/
|
|
1648
1720
|
export const POWQL_LEXER_TESTED_CEILING = '0.19';
|
|
1649
|
-
/**
|
|
1650
|
-
|
|
1721
|
+
/**
|
|
1722
|
+
* Escape a string into a PowQL `"…"` literal, matching the engine lexer's
|
|
1723
|
+
* escape rules.
|
|
1724
|
+
*
|
|
1725
|
+
* A raw NUL (U+0000) is REFUSED rather than emitted: the lexer defines no `\0`
|
|
1726
|
+
* escape (its escape set is exactly `\"`, `\\`, `\n`, `\t`, see
|
|
1727
|
+
* {@link POWQL_LEXER_TESTED_CEILING}), so a NUL would travel into the query
|
|
1728
|
+
* text as a raw byte and any layer that treats that text as a C string would
|
|
1729
|
+
* silently truncate the statement there.
|
|
1730
|
+
*/
|
|
1731
|
+
function encodePowqlString(s, position) {
|
|
1732
|
+
if (s.includes('\0')) {
|
|
1733
|
+
throw new ValidationError(`[turbine] String value${position ? ` (${position})` : ''} contains a NUL byte (U+0000), which PowQL string ` +
|
|
1734
|
+
'literals cannot represent. Strip it before writing.');
|
|
1735
|
+
}
|
|
1651
1736
|
let out = '"';
|
|
1652
1737
|
for (const ch of s) {
|
|
1653
1738
|
if (ch === '\\')
|
|
@@ -1677,7 +1762,7 @@ export function materializePowql(powql, params) {
|
|
|
1677
1762
|
if (idx < 0 || idx >= params.length) {
|
|
1678
1763
|
throw new ValidationError(`[turbine] PowQL placeholder $${n} has no bound parameter (have ${params.length}).`);
|
|
1679
1764
|
}
|
|
1680
|
-
return encodePowqlLiteral(params[idx]);
|
|
1765
|
+
return encodePowqlLiteral(params[idx], `parameter $${n}`);
|
|
1681
1766
|
});
|
|
1682
1767
|
}
|
|
1683
1768
|
/**
|
package/dist/powql.d.ts
CHANGED
|
@@ -556,6 +556,11 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
556
556
|
* with the projection); a per-field aggregate re-emits its inner expression
|
|
557
557
|
* (from `aggInner` when the field is a requested aggregate, so a JSON-path
|
|
558
558
|
* aggregate reuses its bound placeholders, else `.field` for a plain column).
|
|
559
|
+
*
|
|
560
|
+
* Every token that reaches the PowQL text is builder-controlled: the field is
|
|
561
|
+
* validated by `ref()`, the aggregate function comes from
|
|
562
|
+
* {@link POWQL_HAVING_AGG_FNS} (unknown keys throw E003), the operator from a
|
|
563
|
+
* fixed map, and every compared value is bound as a `$N` param.
|
|
559
564
|
*/
|
|
560
565
|
private buildHaving;
|
|
561
566
|
/**
|
package/dist/powql.js
CHANGED
|
@@ -107,6 +107,21 @@ const OPERATOR_KEYS = new Set([
|
|
|
107
107
|
'endsWith',
|
|
108
108
|
'mode',
|
|
109
109
|
]);
|
|
110
|
+
/**
|
|
111
|
+
* Aggregate keys accepted inside a per-field `having` filter, mapped to the
|
|
112
|
+
* PowQL function each one emits. The set is FIXED: any other key is rejected by
|
|
113
|
+
* {@link PowqlInterface.buildHaving} rather than interpolated into the query
|
|
114
|
+
* text (the SQL builder holds the same contract, see `aggFnByKey` in
|
|
115
|
+
* query/aggregates.ts). A Map is used deliberately so an inherited object key
|
|
116
|
+
* (`constructor`, `toString`, `__proto__`) can never resolve to a function name.
|
|
117
|
+
*/
|
|
118
|
+
const POWQL_HAVING_AGG_FNS = new Map([
|
|
119
|
+
['_sum', 'sum'],
|
|
120
|
+
['_avg', 'avg'],
|
|
121
|
+
['_min', 'min'],
|
|
122
|
+
['_max', 'max'],
|
|
123
|
+
['_count', 'count'],
|
|
124
|
+
]);
|
|
110
125
|
/** Filters that have no PowDB representation and must throw E017. */
|
|
111
126
|
function rejectUnsupportedFilter(value, field) {
|
|
112
127
|
if ('distance' in value || 'metric' in value) {
|
|
@@ -206,7 +221,7 @@ export class PowqlInterface {
|
|
|
206
221
|
// json document column: a JS object/array is serialized to canonical JSON
|
|
207
222
|
// text and stored as a json document (a JS string passes through raw, same
|
|
208
223
|
// contract as pg jsonb; `null` stays `null`).
|
|
209
|
-
tagged = new PowdbJsonParam(value);
|
|
224
|
+
tagged = new PowdbJsonParam(value, col.name);
|
|
210
225
|
}
|
|
211
226
|
params.push(tagged);
|
|
212
227
|
return `$${params.length}`;
|
|
@@ -2460,6 +2475,11 @@ export class PowqlInterface {
|
|
|
2460
2475
|
* with the projection); a per-field aggregate re-emits its inner expression
|
|
2461
2476
|
* (from `aggInner` when the field is a requested aggregate, so a JSON-path
|
|
2462
2477
|
* aggregate reuses its bound placeholders, else `.field` for a plain column).
|
|
2478
|
+
*
|
|
2479
|
+
* Every token that reaches the PowQL text is builder-controlled: the field is
|
|
2480
|
+
* validated by `ref()`, the aggregate function comes from
|
|
2481
|
+
* {@link POWQL_HAVING_AGG_FNS} (unknown keys throw E003), the operator from a
|
|
2482
|
+
* fixed map, and every compared value is bound as a `$N` param.
|
|
2463
2483
|
*/
|
|
2464
2484
|
buildHaving(having, params, aggInner) {
|
|
2465
2485
|
if (!having)
|
|
@@ -2469,10 +2489,19 @@ export class PowqlInterface {
|
|
|
2469
2489
|
if (typeof filter === 'number')
|
|
2470
2490
|
return `${expr} = ${this.param(filter, params)}`;
|
|
2471
2491
|
const f = filter;
|
|
2472
|
-
|
|
2492
|
+
// Map (not an object literal) so an inherited key such as `constructor`
|
|
2493
|
+
// cannot resolve to a truthy value and reach the emitted PowQL.
|
|
2494
|
+
const ops = new Map([
|
|
2495
|
+
['equals', '='],
|
|
2496
|
+
['gt', '>'],
|
|
2497
|
+
['gte', '>='],
|
|
2498
|
+
['lt', '<'],
|
|
2499
|
+
['lte', '<='],
|
|
2500
|
+
['not', '!='],
|
|
2501
|
+
]);
|
|
2473
2502
|
return Object.entries(f)
|
|
2474
|
-
.filter(([k]) => ops
|
|
2475
|
-
.map(([k, v]) => `${expr} ${ops
|
|
2503
|
+
.filter(([k]) => ops.has(k))
|
|
2504
|
+
.map(([k, v]) => `${expr} ${ops.get(k)} ${this.param(v, params)}`)
|
|
2476
2505
|
.join(' and ');
|
|
2477
2506
|
};
|
|
2478
2507
|
for (const [key, spec] of Object.entries(having)) {
|
|
@@ -2482,11 +2511,18 @@ export class PowqlInterface {
|
|
|
2482
2511
|
conds.push(cmp('count(*)', spec));
|
|
2483
2512
|
}
|
|
2484
2513
|
else {
|
|
2485
|
-
for (const [
|
|
2514
|
+
for (const [aggKey, filter] of Object.entries(spec)) {
|
|
2486
2515
|
if (filter == null)
|
|
2487
2516
|
continue;
|
|
2517
|
+
// The function token is emitted verbatim into the PowQL text, so it
|
|
2518
|
+
// must come from the fixed allowlist, never from the caller's key.
|
|
2519
|
+
const fn = POWQL_HAVING_AGG_FNS.get(aggKey);
|
|
2520
|
+
if (!fn) {
|
|
2521
|
+
throw new ValidationError(`[turbine] Unknown aggregate "${aggKey}" in having for field "${key}" on table "${this.table}". ` +
|
|
2522
|
+
`Supported: ${[...POWQL_HAVING_AGG_FNS.keys()].join(', ')}.`);
|
|
2523
|
+
}
|
|
2488
2524
|
const inner = aggInner.get(key) ?? this.ref(key);
|
|
2489
|
-
conds.push(cmp(`${fn
|
|
2525
|
+
conds.push(cmp(`${fn}(${inner})`, filter));
|
|
2490
2526
|
}
|
|
2491
2527
|
}
|
|
2492
2528
|
}
|
package/dist/prisma-compat.d.ts
CHANGED
|
@@ -260,6 +260,8 @@ export type PrismaCompatClient<S extends Record<string, PrismaModelTypes> = Reco
|
|
|
260
260
|
} & {
|
|
261
261
|
[K in keyof S as Uncapitalize<K & string>]: PrismaModelDelegate<S[K]>;
|
|
262
262
|
} & PrismaCompatClientBase<S>;
|
|
263
|
+
/** @internal Names on a compat client that are NOT model delegates. */
|
|
264
|
+
export declare const CLIENT_RESERVED_KEYS: ReadonlySet<string>;
|
|
263
265
|
/**
|
|
264
266
|
* Create a PrismaClient-surface adapter over a {@link TurbineClient}, driven by a
|
|
265
267
|
* {@link PrismaCompatMap} (the `prisma-map.ts` that `turbine
|
package/dist/prisma-compat.js
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
* ```
|
|
83
83
|
*/
|
|
84
84
|
import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
|
|
85
|
+
import { shouldWarnOnce } from './query/warn-registry.js';
|
|
85
86
|
// ---------------------------------------------------------------------------
|
|
86
87
|
// Prisma.sql-style raw fragments (local, minimal, never imports @prisma/client)
|
|
87
88
|
// ---------------------------------------------------------------------------
|
|
@@ -399,7 +400,7 @@ function modelName(ctx, mm) {
|
|
|
399
400
|
* `where` / `orderBy` are translated first so the cursor step (which may inject
|
|
400
401
|
* a keyset predicate) operates in the translated turbine-field space.
|
|
401
402
|
*/
|
|
402
|
-
function translateReadArgs(ctx, mm, prismaArgs) {
|
|
403
|
+
function translateReadArgs(ctx, mm, prismaArgs, kind) {
|
|
403
404
|
const t = {};
|
|
404
405
|
if (prismaArgs.where !== undefined)
|
|
405
406
|
t.where = translateWhere(ctx, mm, prismaArgs.where);
|
|
@@ -424,9 +425,85 @@ function translateReadArgs(ctx, mm, prismaArgs) {
|
|
|
424
425
|
t.includePii = prismaArgs.includePii;
|
|
425
426
|
if (ctx.options.stablePkOrder)
|
|
426
427
|
t.stableRelationOrder = true;
|
|
428
|
+
// ORDER IS LOAD-BEARING: translateCursor MUST run BEFORE applyImplicitPkOrder.
|
|
429
|
+
// The cursor translation reads `t.orderBy` to decide the seek direction and to
|
|
430
|
+
// validate that a bare inclusive cursor names the sort key; it must see the
|
|
431
|
+
// USER's ordering, never an ordering this layer invented. Swap these two lines
|
|
432
|
+
// and a bare inclusive cursor on a non-PK field silently compiles against the
|
|
433
|
+
// injected primary-key order instead of throwing. See the guard test
|
|
434
|
+
// "cursor decisions are made against the user's own orderBy".
|
|
427
435
|
translateCursor(ctx, mm, prismaArgs, t);
|
|
436
|
+
applyImplicitPkOrder(ctx, mm, t, kind);
|
|
428
437
|
return t;
|
|
429
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Prisma appends an implicit `ORDER BY <primary key> ASC` to a paginated read;
|
|
441
|
+
* Turbine emits a bare `LIMIT`. An unordered `LIMIT` is not stable in Postgres:
|
|
442
|
+
* once the heap changes underneath it (autovacuum, a heap rewrite, a plan flip)
|
|
443
|
+
* successive pages can repeat a row or skip one entirely, and the unordered
|
|
444
|
+
* plan is also the slower one (a seq scan instead of a primary-key index scan).
|
|
445
|
+
*
|
|
446
|
+
* So a top-level compat read whose page is not already ordered gets the model's
|
|
447
|
+
* primary key ascending, matching Prisma. An explicit `orderBy` always wins, a
|
|
448
|
+
* composite primary key orders on every column in declaration order, and a model
|
|
449
|
+
* with no primary key is left exactly as it was (nothing is invented).
|
|
450
|
+
*
|
|
451
|
+
* ## Which reads this covers
|
|
452
|
+
*
|
|
453
|
+
* - `findMany` only when it paginates (`take` / `skip`): without a limit there
|
|
454
|
+
* is no page to be unstable.
|
|
455
|
+
* - `findFirst` / `findFirstOrThrow` ALWAYS: core compiles both to a bare
|
|
456
|
+
* `LIMIT 1`, which is the single most common nondeterministic read shape in
|
|
457
|
+
* Prisma-shaped code ("give me any one row" quietly becoming "give me
|
|
458
|
+
* whichever row the heap hands back today").
|
|
459
|
+
* - `findUnique` / `findUniqueOrThrow` NEVER: they are addressed by a unique
|
|
460
|
+
* key, so at most one row matches and an ordering is pure overhead.
|
|
461
|
+
* - Nested relation includes (`include: { posts: { take: 3 } }`) are NOT
|
|
462
|
+
* touched here, deliberately. Relation ordering is a core concern (core owns
|
|
463
|
+
* the target table's metadata and the subquery shape), and core already
|
|
464
|
+
* exposes it as `stableRelationOrder`, which this layer forwards from the
|
|
465
|
+
* {@link PrismaCompatOptions.stablePkOrder} option. Re-walking the `with` tree
|
|
466
|
+
* here would duplicate that mechanism and fight it inside the subquery.
|
|
467
|
+
*
|
|
468
|
+
* ## Mirrored exclusions (keep in sync with core `isUnorderedPage`)
|
|
469
|
+
*
|
|
470
|
+
* Core's `QueryInterface.isUnorderedPage` (query/builder.ts) excludes both
|
|
471
|
+
* `distinct` and `cursor`. This layer mirrors them one at a time rather than
|
|
472
|
+
* copying the predicate, because the two layers do NOT have the same default:
|
|
473
|
+
*
|
|
474
|
+
* - `distinct`: EXCLUDED, same as core, and this one is a hard requirement.
|
|
475
|
+
* Adding an `orderBy` flips core's `DISTINCT ON` out of its plain branch into
|
|
476
|
+
* the two-level derived-table rewrite whose OUTER `ORDER BY` can only see the
|
|
477
|
+
* projected columns, so `distinct` + `select` + `take` would fail at runtime
|
|
478
|
+
* with `column "id" does not exist`. Injecting also lifts the `LIMIT` outside
|
|
479
|
+
* the `DISTINCT ON`, so the inner scan stops being limited. `distinct` reads
|
|
480
|
+
* keep the exact SQL they emitted before this ordering existed.
|
|
481
|
+
* - `cursor`: NOT excluded here, unlike core. Core is a general-purpose layer
|
|
482
|
+
* whose cursor callers chose their own ordering; this layer's contract is
|
|
483
|
+
* Prisma parity, and Prisma pairs cursor pagination with the same implicit
|
|
484
|
+
* primary-key order. The injection is safe because it runs strictly AFTER
|
|
485
|
+
* {@link translateCursor}: the cursor comparison direction was already
|
|
486
|
+
* resolved from the user's own `orderBy` (ascending by default, matching an
|
|
487
|
+
* ascending primary key), and the bare-inclusive-cursor branch has already
|
|
488
|
+
* set or demanded an `orderBy`, so only the exclusive `cursor` + `skip`
|
|
489
|
+
* branch reaches here. It gains an `ORDER BY <pk> ASC` and nothing else.
|
|
490
|
+
*/
|
|
491
|
+
function applyImplicitPkOrder(ctx, mm, t, kind) {
|
|
492
|
+
if (kind === 'unique')
|
|
493
|
+
return;
|
|
494
|
+
if (t.orderBy !== undefined)
|
|
495
|
+
return;
|
|
496
|
+
if (t.distinct !== undefined)
|
|
497
|
+
return;
|
|
498
|
+
if (kind === 'many' && t.limit === undefined && t.offset === undefined)
|
|
499
|
+
return;
|
|
500
|
+
const meta = ctx.schema.tables[mm.table];
|
|
501
|
+
const pk = meta?.primaryKey ?? [];
|
|
502
|
+
if (pk.length === 0)
|
|
503
|
+
return;
|
|
504
|
+
const fields = pk.map((col) => meta?.reverseColumnMap?.[col] ?? col);
|
|
505
|
+
t.orderBy = fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
|
|
506
|
+
}
|
|
430
507
|
/** turbine field names of the model's single-column primary key, if any. */
|
|
431
508
|
function singleColumnPkField(ctx, mm) {
|
|
432
509
|
const pk = ctx.schema.tables[mm.table]?.primaryKey;
|
|
@@ -1039,11 +1116,11 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1039
1116
|
return args;
|
|
1040
1117
|
};
|
|
1041
1118
|
return {
|
|
1042
|
-
findMany: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
|
|
1043
|
-
findFirst: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
|
|
1044
|
-
findUnique: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique')), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
|
|
1045
|
-
findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
1046
|
-
findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow')), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
1119
|
+
findMany: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'many'), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
|
|
1120
|
+
findFirst: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
|
|
1121
|
+
findUnique: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique'), 'unique'), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
|
|
1122
|
+
findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
1123
|
+
findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow'), 'unique'), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
1047
1124
|
create: (args) => defer(() => {
|
|
1048
1125
|
const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
|
|
1049
1126
|
if (typeof args.timeout === 'number')
|
|
@@ -1181,6 +1258,79 @@ function flattenTemplate(strings, values, ph) {
|
|
|
1181
1258
|
// ---------------------------------------------------------------------------
|
|
1182
1259
|
// createPrismaCompatClient
|
|
1183
1260
|
// ---------------------------------------------------------------------------
|
|
1261
|
+
/**
|
|
1262
|
+
* The client-level members every compat client carries, as a total map over
|
|
1263
|
+
* {@link PrismaCompatClientBase}. Declaring it `Record<keyof …, true>` makes the
|
|
1264
|
+
* compiler keep it in sync: adding a `$method` to the interface without listing
|
|
1265
|
+
* it here is a type error, and listing one that does not exist is too. The
|
|
1266
|
+
* derived key set is what {@link junctionModels} refuses to shadow.
|
|
1267
|
+
*/
|
|
1268
|
+
const CLIENT_RESERVED_KEY_MAP = {
|
|
1269
|
+
$transaction: true,
|
|
1270
|
+
$queryRaw: true,
|
|
1271
|
+
$queryRawUnsafe: true,
|
|
1272
|
+
$executeRaw: true,
|
|
1273
|
+
$executeRawUnsafe: true,
|
|
1274
|
+
$connect: true,
|
|
1275
|
+
$disconnect: true,
|
|
1276
|
+
};
|
|
1277
|
+
/** @internal Names on a compat client that are NOT model delegates. */
|
|
1278
|
+
export const CLIENT_RESERVED_KEYS = new Set(Object.keys(CLIENT_RESERVED_KEY_MAP));
|
|
1279
|
+
/** Warn-once namespace for junction accessors dropped on a name collision. */
|
|
1280
|
+
const JUNCTION_WARN_NS = 'prismaCompatJunction';
|
|
1281
|
+
/**
|
|
1282
|
+
* Synthesize a model map per implicit many-to-many JUNCTION table, keyed by the
|
|
1283
|
+
* junction's raw table name (`_ReportToSchedule`, `post_tags`, ...).
|
|
1284
|
+
*
|
|
1285
|
+
* Prisma's schema has no model for an implicit junction, so `PRISMA_MAP` has no
|
|
1286
|
+
* entry for it and the adapter would expose no accessor: a compat caller had no
|
|
1287
|
+
* way to touch link rows at all, not even inside a `$transaction`. The junction
|
|
1288
|
+
* tables ARE in the Turbine schema metadata (`through.table` on every
|
|
1289
|
+
* many-to-many relation), so the delegate is a plain identity mapping over the
|
|
1290
|
+
* real table: no field renames, no relations, no compound uniques.
|
|
1291
|
+
*
|
|
1292
|
+
* A junction accessor is an ESCAPE HATCH, never worth breaking a real member of
|
|
1293
|
+
* the client for, so it FAILS SAFE: the name is skipped (leaving the existing
|
|
1294
|
+
* member intact) whenever it collides with anything the client already exposes,
|
|
1295
|
+
* which is a Prisma model name, a table some model maps to, a model's
|
|
1296
|
+
* lowercased Prisma-property alias (`compat.user` for `model User`), or one of
|
|
1297
|
+
* the client-level {@link CLIENT_RESERVED_KEYS} (a junction table named
|
|
1298
|
+
* `$transaction` must not turn the transaction surface into a delegate).
|
|
1299
|
+
*
|
|
1300
|
+
* Every skip except the mapped-table one loses a capability the caller might
|
|
1301
|
+
* expect, so those warn once per junction name in dev; a table that a model
|
|
1302
|
+
* already maps to is not a loss (the model IS that table's accessor).
|
|
1303
|
+
*/
|
|
1304
|
+
function junctionModels(ctx, map, tableToModel) {
|
|
1305
|
+
const out = [];
|
|
1306
|
+
const seen = new Set(CLIENT_RESERVED_KEYS);
|
|
1307
|
+
for (const model of Object.keys(map.models)) {
|
|
1308
|
+
seen.add(model);
|
|
1309
|
+
const alias = prismaPropertyAlias(model);
|
|
1310
|
+
if (alias)
|
|
1311
|
+
seen.add(alias);
|
|
1312
|
+
}
|
|
1313
|
+
for (const table of Object.values(ctx.schema.tables)) {
|
|
1314
|
+
for (const rel of Object.values(table.relations ?? {})) {
|
|
1315
|
+
if (rel.type !== 'manyToMany')
|
|
1316
|
+
continue;
|
|
1317
|
+
const name = rel.through?.table;
|
|
1318
|
+
if (!name || !ctx.schema.tables[name] || tableToModel.has(name))
|
|
1319
|
+
continue;
|
|
1320
|
+
if (seen.has(name)) {
|
|
1321
|
+
if (process.env.NODE_ENV !== 'production' && shouldWarnOnce(JUNCTION_WARN_NS, name)) {
|
|
1322
|
+
console.warn(`[turbine] prisma-compat: the many-to-many junction table "${name}" collides with an ` +
|
|
1323
|
+
'existing client member of the same name, so no junction accessor was created for it. ' +
|
|
1324
|
+
'Reach its rows through $queryRaw / $executeRaw, or through the owning model relation.');
|
|
1325
|
+
}
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
seen.add(name);
|
|
1329
|
+
out.push([name, { table: name, accessor: name, fields: {}, relations: {}, compoundUniques: {} }]);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return out;
|
|
1333
|
+
}
|
|
1184
1334
|
/**
|
|
1185
1335
|
* Create a PrismaClient-surface adapter over a {@link TurbineClient}, driven by a
|
|
1186
1336
|
* {@link PrismaCompatMap} (the `prisma-map.ts` that `turbine
|
|
@@ -1216,9 +1366,17 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1216
1366
|
prismaErrorCodes: options.prismaErrorCodes ?? false,
|
|
1217
1367
|
},
|
|
1218
1368
|
};
|
|
1369
|
+
// Every model the client exposes: the map's Prisma models, plus the implicit
|
|
1370
|
+
// many-to-many junction tables (see below). Both delegate loops (base and
|
|
1371
|
+
// transaction-scoped) build from this ONE list, so an accessor can never
|
|
1372
|
+
// exist on the client but be missing inside `$transaction`.
|
|
1373
|
+
const delegateModels = [
|
|
1374
|
+
...Object.entries(map.models),
|
|
1375
|
+
...junctionModels(ctx, map, tableToModel),
|
|
1376
|
+
];
|
|
1219
1377
|
// Delegates bound to the base client (each call reads db.table(...) lazily).
|
|
1220
1378
|
const delegates = new Map();
|
|
1221
|
-
for (const [prismaModel, mm] of
|
|
1379
|
+
for (const [prismaModel, mm] of delegateModels) {
|
|
1222
1380
|
delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
|
|
1223
1381
|
}
|
|
1224
1382
|
const ph = placeholderOf(db);
|
|
@@ -1271,7 +1429,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1271
1429
|
const fn = arg;
|
|
1272
1430
|
return db.$transaction((tx) => {
|
|
1273
1431
|
const txDelegates = {};
|
|
1274
|
-
for (const [prismaModel, mm] of
|
|
1432
|
+
for (const [prismaModel, mm] of delegateModels) {
|
|
1275
1433
|
txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n)));
|
|
1276
1434
|
const alias = prismaPropertyAlias(prismaModel);
|
|
1277
1435
|
if (alias && !(alias in map.models) && !(alias in txDelegates)) {
|
package/dist/query/aggregates.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
12
12
|
import { snakeToCamel } from '../schema.js';
|
|
13
13
|
import { isJsonPathOrderBy, isVectorOrderBy, normalizeOrderBy, orderByEntries } from './filters.js';
|
|
14
|
+
import { ownLookup } from './utils.js';
|
|
14
15
|
import * as whereMod from './where.js';
|
|
15
16
|
/**
|
|
16
17
|
* Enforce the PII contract on the aggregate surface. A PII-tagged
|
|
@@ -232,8 +233,8 @@ export function buildGroupBy(qi, args) {
|
|
|
232
233
|
// no `$n` renumbering. `offset` without a deterministic `orderBy` yields an
|
|
233
234
|
// arbitrary window (same caveat as findMany).
|
|
234
235
|
if (args.limit !== undefined || args.offset !== undefined) {
|
|
235
|
-
const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params) : undefined;
|
|
236
|
-
const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params) : undefined;
|
|
236
|
+
const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params, 'limit') : undefined;
|
|
237
|
+
const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params, 'skip/offset') : undefined;
|
|
237
238
|
sql += qi.buildPagination(limitPh, offsetPh, args.orderBy !== undefined);
|
|
238
239
|
}
|
|
239
240
|
return {
|
|
@@ -513,7 +514,10 @@ export function buildHavingClauses(qi, having, params, jsonAggExprs) {
|
|
|
513
514
|
for (const [aggKey, filter] of Object.entries(value)) {
|
|
514
515
|
if (filter === undefined)
|
|
515
516
|
continue;
|
|
516
|
-
|
|
517
|
+
// ownLookup, not a bare index: an inherited Object.prototype member
|
|
518
|
+
// ("constructor", "toString", …) would otherwise resolve to a truthy
|
|
519
|
+
// builtin and be spliced into the HAVING clause as its source text.
|
|
520
|
+
const fn = ownLookup(aggFnByKey, aggKey);
|
|
517
521
|
if (!fn) {
|
|
518
522
|
throw new ValidationError(`[turbine] Unknown aggregate "${aggKey}" in having for field "${key}" on table "${qi.table}". ` +
|
|
519
523
|
`Supported: ${Object.keys(aggFnByKey).join(', ')}.`);
|