turbine-orm 0.72.0 → 0.73.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 +6 -3
- package/dist/cjs/cli/index.d.ts +7 -0
- package/dist/cjs/cli/index.js +113 -0
- package/dist/cjs/powql.d.ts +17 -4
- package/dist/cjs/powql.js +43 -9
- package/dist/cjs/query/builder.d.ts +18 -0
- package/dist/cjs/query/builder.js +64 -12
- package/dist/cjs/query/compound-unique.d.ts +44 -0
- package/dist/cjs/query/compound-unique.js +119 -0
- package/dist/cjs/query/option-surface.d.ts +22 -0
- package/dist/cjs/query/option-surface.js +91 -0
- package/dist/cjs/query/types.d.ts +15 -1
- package/dist/cjs/query/utils.d.ts +23 -0
- package/dist/cjs/query/utils.js +57 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +113 -0
- package/dist/powql.d.ts +17 -4
- package/dist/powql.js +45 -11
- package/dist/query/builder.d.ts +18 -0
- package/dist/query/builder.js +66 -14
- package/dist/query/compound-unique.d.ts +44 -0
- package/dist/query/compound-unique.js +116 -0
- package/dist/query/option-surface.d.ts +22 -0
- package/dist/query/option-surface.js +90 -0
- package/dist/query/types.d.ts +15 -1
- package/dist/query/utils.d.ts +23 -0
- package/dist/query/utils.js +56 -0
- package/package.json +2 -1
- package/skills/turbine-orm/SKILL.md +247 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Six reasons, each with the mechanism that makes it true:
|
|
|
20
20
|
2. **Written from scratch.** Turbine is not a layer over Knex or a query-builder library. Query compilation is plain string building with an FNV-1a shape fingerprint into a bounded LRU of SQL templates, so there is no plan cache to size and no compiler running on your event loop.
|
|
21
21
|
3. **Nested relations in one statement.** A `with` clause compiles to correlated `json_agg` subqueries, so users with posts with comments is one round trip, typed end to end: `users[0].posts[0].comments[0].author.name` autocompletes with no annotation.
|
|
22
22
|
4. **Close to raw SQL.** In the last published run, Turbine's overhead over a hand-written `pg` control was 1.08x by geometric mean. The table is below; the losses are stated with the wins.
|
|
23
|
-
5. **Agents get typed tools, not a SQL prompt.** `npx turbine mcp` exposes eleven read-only MCP tools, including a relation graph and a join-path finder that returns the `with` clause to write. Every tool runs inside `BEGIN READ ONLY`, and PII-tagged columns are redacted before rows reach a model.
|
|
23
|
+
5. **Agents get a skill and typed tools, not a SQL prompt.** `npx turbine skill` installs a query-writing skill whose every claim is executed against a live database before release; `npx turbine mcp` exposes eleven read-only MCP tools, including a relation graph and a join-path finder that returns the `with` clause to write. Every tool runs inside `BEGIN READ ONLY`, and PII-tagged columns are redacted before rows reach a model.
|
|
24
24
|
6. **The dangerous operations ask first.** Destructive migration statements refuse to run without typed consent. `update`/`delete` with an empty `where` throws. Columns tagged `pii: true` are excluded from the emitted SQL's projections. `turbine doctor` reports missing FK indexes offline, no account, no telemetry.
|
|
25
25
|
|
|
26
26
|
## Benchmarks
|
|
@@ -216,12 +216,15 @@ Every error extends `TurbineError` with a stable code (`TURBINE_E001` through `E
|
|
|
216
216
|
|
|
217
217
|
## Built for agents
|
|
218
218
|
|
|
219
|
-
An agent pointed at a database usually gets a connection string and guesses. Turbine gives it typed tools instead:
|
|
219
|
+
An agent pointed at a database usually gets a connection string and guesses. Turbine gives it a skill and typed tools instead:
|
|
220
220
|
|
|
221
221
|
```bash
|
|
222
|
+
npx turbine skill # install the query-writing skill into the project
|
|
222
223
|
npx turbine mcp # read-only MCP server over JSON-RPC stdio, ships in the package
|
|
223
224
|
```
|
|
224
225
|
|
|
226
|
+
The skill covers what an agent gets wrong on a schema it has not seen: `with` versus Prisma's `include`, how relation names are derived, what `select` may name, relation filters, the `having` shape, JSON paths, and which error each mistake produces. **Every factual claim in it is executed against a live database before release**, so it cannot drift from the ORM the way documentation usually does. `--agents` prints a short block for `AGENTS.md` instead.
|
|
227
|
+
|
|
225
228
|
Eleven tools, every one inside `BEGIN READ ONLY` with a statement timeout, so an agent cannot mutate anything through this server:
|
|
226
229
|
|
|
227
230
|
| Tool | What it answers |
|
|
@@ -238,7 +241,7 @@ Eleven tools, every one inside `BEGIN READ ONLY` with a statement timeout, so an
|
|
|
238
241
|
| `migrate_status` | Applied vs pending migrations, without applying anything. |
|
|
239
242
|
| `doctor_report` | Missing relation indexes, from the same advisor `turbine doctor` uses. |
|
|
240
243
|
|
|
241
|
-
The rest of the agent story is structural: query args are fully typed, so a wrong query is a compile error the agent can read; errors carry stable codes it can branch on; and [llms.txt](https://turbineorm.dev/llms.txt) / [llms-full.txt](https://turbineorm.dev/llms-full.txt) give it the docs in fetchable form. Setup for Claude Code and Cursor, plus a drop-in instructions snippet: [turbineorm.dev/ai-agents](https://turbineorm.dev/ai-agents).
|
|
244
|
+
The rest of the agent story is structural: query args are fully typed, so a wrong query is a compile error the agent can read; errors carry stable codes it can branch on; an unrecognized query option warns instead of being silently dropped; and [llms.txt](https://turbineorm.dev/llms.txt) / [llms-full.txt](https://turbineorm.dev/llms-full.txt) give it the docs in fetchable form. Setup for Claude Code and Cursor, plus a drop-in instructions snippet: [turbineorm.dev/ai-agents](https://turbineorm.dev/ai-agents).
|
|
242
245
|
|
|
243
246
|
## Safety tooling
|
|
244
247
|
|
package/dist/cjs/cli/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
|
|
19
19
|
* turbine mcp , Start read-only MCP server over JSON-RPC stdio
|
|
20
20
|
* turbine observe , Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
21
|
+
* turbine skill - Install the agent query skill (--print, --agents, --dir <path>)
|
|
21
22
|
*
|
|
22
23
|
* Usage:
|
|
23
24
|
* DATABASE_URL=postgres://... npx turbine generate
|
|
@@ -113,6 +114,12 @@ export interface CliArgs {
|
|
|
113
114
|
* not be turned into a failed install.
|
|
114
115
|
*/
|
|
115
116
|
ifDb?: boolean;
|
|
117
|
+
/** `skill --print`: write the skill to stdout instead of installing it. */
|
|
118
|
+
print?: boolean;
|
|
119
|
+
/** `skill --agents`: print the AGENTS.md instructions block instead. */
|
|
120
|
+
agents?: boolean;
|
|
121
|
+
/** `skill --dir <path>`: the skills root to install into (default `.claude/skills`). */
|
|
122
|
+
dir?: string;
|
|
116
123
|
}
|
|
117
124
|
export declare function parseArgs(argv?: string[]): CliArgs;
|
|
118
125
|
/**
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* turbine studio : Launch local read-only web UI (--demo for a seeded sample DB)
|
|
20
20
|
* turbine mcp , Start read-only MCP server over JSON-RPC stdio
|
|
21
21
|
* turbine observe , Launch metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
22
|
+
* turbine skill - Install the agent query skill (--print, --agents, --dir <path>)
|
|
22
23
|
*
|
|
23
24
|
* Usage:
|
|
24
25
|
* DATABASE_URL=postgres://... npx turbine generate
|
|
@@ -227,6 +228,17 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
227
228
|
case '--allow-pooler':
|
|
228
229
|
result.allowPooler = true;
|
|
229
230
|
break;
|
|
231
|
+
// `turbine skill`
|
|
232
|
+
case '--print':
|
|
233
|
+
result.print = true;
|
|
234
|
+
break;
|
|
235
|
+
case '--agents':
|
|
236
|
+
result.agents = true;
|
|
237
|
+
break;
|
|
238
|
+
case '--dir':
|
|
239
|
+
result.dir = next;
|
|
240
|
+
i++;
|
|
241
|
+
break;
|
|
230
242
|
case '--zod':
|
|
231
243
|
result.zod = true;
|
|
232
244
|
break;
|
|
@@ -4001,6 +4013,7 @@ function showHelp() {
|
|
|
4001
4013
|
console.log(` ${(0, ui_js_1.cyan)('studio')} Launch local read-only web UI ${(0, ui_js_1.dim)('(--write for writes, --demo for a sample DB)')}`);
|
|
4002
4014
|
console.log(` ${(0, ui_js_1.cyan)('mcp')} Start read-only MCP server over stdio`);
|
|
4003
4015
|
console.log(` ${(0, ui_js_1.cyan)('observe')} Launch metrics dashboard ${(0, ui_js_1.dim)('(requires TURBINE_OBSERVE_URL)')}`);
|
|
4016
|
+
console.log(` ${(0, ui_js_1.cyan)('skill')} Install the agent query skill ${(0, ui_js_1.dim)('(--print, --agents, --dir <path>)')}`);
|
|
4004
4017
|
(0, ui_js_1.newline)();
|
|
4005
4018
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
4006
4019
|
console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string`);
|
|
@@ -4051,6 +4064,103 @@ function showHelp() {
|
|
|
4051
4064
|
(0, ui_js_1.newline)();
|
|
4052
4065
|
}
|
|
4053
4066
|
// ---------------------------------------------------------------------------
|
|
4067
|
+
// Agent skill
|
|
4068
|
+
// ---------------------------------------------------------------------------
|
|
4069
|
+
/**
|
|
4070
|
+
* turbine-orm's own package root, found by walking up from the running script.
|
|
4071
|
+
*
|
|
4072
|
+
* `process.argv[1]` rather than `import.meta.url` so the same source compiles
|
|
4073
|
+
* for both the ESM and CJS builds, and realpath first because `npx turbine`
|
|
4074
|
+
* runs through a `node_modules/.bin` symlink whose dirname is the CONSUMER's
|
|
4075
|
+
* tree, where this package.json does not exist.
|
|
4076
|
+
*/
|
|
4077
|
+
function ownPackageRoot() {
|
|
4078
|
+
try {
|
|
4079
|
+
let entry = process.argv[1] ?? '';
|
|
4080
|
+
try {
|
|
4081
|
+
entry = (0, node_fs_1.realpathSync)(entry);
|
|
4082
|
+
}
|
|
4083
|
+
catch {
|
|
4084
|
+
// keep the raw path if realpath fails (e.g. deleted cwd)
|
|
4085
|
+
}
|
|
4086
|
+
let dir = (0, node_path_1.dirname)(entry);
|
|
4087
|
+
for (let i = 0; i < 6; i++) {
|
|
4088
|
+
const candidate = (0, node_path_1.resolve)(dir, 'package.json');
|
|
4089
|
+
if ((0, node_fs_1.existsSync)(candidate)) {
|
|
4090
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(candidate, 'utf8'));
|
|
4091
|
+
if (pkg.name === 'turbine-orm')
|
|
4092
|
+
return dir;
|
|
4093
|
+
}
|
|
4094
|
+
const parent = (0, node_path_1.dirname)(dir);
|
|
4095
|
+
if (parent === dir)
|
|
4096
|
+
break;
|
|
4097
|
+
dir = parent;
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
catch {
|
|
4101
|
+
// fall through
|
|
4102
|
+
}
|
|
4103
|
+
return undefined;
|
|
4104
|
+
}
|
|
4105
|
+
/** The instructions block for a project's AGENTS.md / CLAUDE.md. */
|
|
4106
|
+
const AGENTS_SNIPPET = `## Database access (Turbine ORM)
|
|
4107
|
+
|
|
4108
|
+
- Queries go through the generated client. Read \`generated/turbine/types.ts\` for
|
|
4109
|
+
the entity and input types before writing one; the names there are the truth.
|
|
4110
|
+
- Relations are \`with\`, never Prisma's \`include\`. An unrecognized option is
|
|
4111
|
+
ignored, so an \`include\` returns rows with the relation missing.
|
|
4112
|
+
- \`select\` and \`omit\` name columns only. A relation named in \`select\` throws.
|
|
4113
|
+
- \`findUnique\` needs a unique key. Use \`findFirst\` for "any row matching a
|
|
4114
|
+
filter", with an \`orderBy\` if which row matters.
|
|
4115
|
+
- Re-run \`npx turbine generate\` after any schema change, then \`tsc --noEmit\`:
|
|
4116
|
+
an invalid query is a type error, so the type checker is the fastest reviewer.
|
|
4117
|
+
- Never write \`includePii: true\`, \`skipGlobalFilters: true\` or
|
|
4118
|
+
\`allowFullTableScan: true\`. Those options take an imported \`UNSAFE\` symbol and
|
|
4119
|
+
nothing else; any other value throws.
|
|
4120
|
+
- Full query reference: https://turbineorm.dev/llms.txt
|
|
4121
|
+
`;
|
|
4122
|
+
/**
|
|
4123
|
+
* Install the packaged query-writing skill into the project.
|
|
4124
|
+
*
|
|
4125
|
+
* The skill is a file in the published tarball rather than something generated
|
|
4126
|
+
* here, so what an agent reads is exactly what the repository tests: every
|
|
4127
|
+
* factual claim in it is executed against a live database by
|
|
4128
|
+
* `evals/src/verify-skill.ts` on each release.
|
|
4129
|
+
*/
|
|
4130
|
+
function cmdSkill(args) {
|
|
4131
|
+
if (args.agents === true) {
|
|
4132
|
+
console.log(AGENTS_SNIPPET);
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
const root = ownPackageRoot();
|
|
4136
|
+
const source = root ? (0, node_path_1.resolve)(root, 'skills', 'turbine-orm', 'SKILL.md') : undefined;
|
|
4137
|
+
if (!source || !(0, node_fs_1.existsSync)(source)) {
|
|
4138
|
+
(0, ui_js_1.error)('Could not find the packaged skill inside turbine-orm.');
|
|
4139
|
+
(0, ui_js_1.newline)();
|
|
4140
|
+
console.log(` ${(0, ui_js_1.dim)('Read it online instead:')} ${(0, ui_js_1.cyan)('https://turbineorm.dev/ai-agents')}`);
|
|
4141
|
+
(0, ui_js_1.newline)();
|
|
4142
|
+
process.exit(1);
|
|
4143
|
+
}
|
|
4144
|
+
const body = (0, node_fs_1.readFileSync)(source, 'utf8');
|
|
4145
|
+
if (args.print === true) {
|
|
4146
|
+
process.stdout.write(body);
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
const skillsDir = args.dir ?? (0, node_path_1.join)('.claude', 'skills');
|
|
4150
|
+
const target = (0, node_path_1.resolve)(process.cwd(), skillsDir, 'turbine-orm', 'SKILL.md');
|
|
4151
|
+
const existed = (0, node_fs_1.existsSync)(target);
|
|
4152
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
4153
|
+
(0, node_fs_1.writeFileSync)(target, body);
|
|
4154
|
+
(0, ui_js_1.newline)();
|
|
4155
|
+
(0, ui_js_1.success)(`${existed ? 'Updated' : 'Installed'} the Turbine query skill`);
|
|
4156
|
+
console.log(` ${(0, ui_js_1.dim)((0, node_path_1.relative)(process.cwd(), target))}`);
|
|
4157
|
+
(0, ui_js_1.newline)();
|
|
4158
|
+
console.log(` ${(0, ui_js_1.dim)('Also worth doing:')}`);
|
|
4159
|
+
console.log(` ${(0, ui_js_1.dim)('-')} connect the read-only MCP server: ${(0, ui_js_1.cyan)('npx turbine mcp')}`);
|
|
4160
|
+
console.log(` ${(0, ui_js_1.dim)('-')} add the instructions block to AGENTS.md: ${(0, ui_js_1.cyan)('npx turbine skill --agents')}`);
|
|
4161
|
+
(0, ui_js_1.newline)();
|
|
4162
|
+
}
|
|
4163
|
+
// ---------------------------------------------------------------------------
|
|
4054
4164
|
// Version
|
|
4055
4165
|
// ---------------------------------------------------------------------------
|
|
4056
4166
|
function showVersion() {
|
|
@@ -4199,6 +4309,9 @@ async function main() {
|
|
|
4199
4309
|
case 'observe':
|
|
4200
4310
|
await cmdObserve(args);
|
|
4201
4311
|
break;
|
|
4312
|
+
case 'skill':
|
|
4313
|
+
cmdSkill(args);
|
|
4314
|
+
break;
|
|
4202
4315
|
default:
|
|
4203
4316
|
(0, ui_js_1.error)(`Unknown command: ${(0, ui_js_1.bold)(args.command)}`);
|
|
4204
4317
|
(0, ui_js_1.newline)();
|
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -148,10 +148,11 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
148
148
|
*/
|
|
149
149
|
private get capabilities();
|
|
150
150
|
/**
|
|
151
|
-
* The `limit` a query actually emits: the explicit `limit`,
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* The `limit` a query actually emits: the explicit `limit`, then the
|
|
152
|
+
* client-level `defaultLimit`. Prisma's `take` alias is already folded into
|
|
153
|
+
* `limit` by `normalizeArgs`, so there is one spelling by the time this runs.
|
|
154
|
+
* Shared by {@link buildFind} and the {@link findMany} zero short-circuit so
|
|
155
|
+
* the two can never disagree about which limit is in force.
|
|
155
156
|
*/
|
|
156
157
|
private effectiveLimit;
|
|
157
158
|
/**
|
|
@@ -186,7 +187,19 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
186
187
|
* which is the divergence class the projection resolver already cost.
|
|
187
188
|
*/
|
|
188
189
|
private withDeclaredRelationNames;
|
|
190
|
+
/**
|
|
191
|
+
* Caller args in canonical form: declared relation spellings, and `take` /
|
|
192
|
+
* `skip` folded into `limit` / `offset`.
|
|
193
|
+
*
|
|
194
|
+
* Same composition as `QueryInterface.normalizeArgs` and here for the same
|
|
195
|
+
* reason the method above is here: nothing about a parallel implementation
|
|
196
|
+
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
197
|
+
* not `skip` pages differently from one that reads both.
|
|
198
|
+
*/
|
|
199
|
+
private normalizeArgs;
|
|
189
200
|
private assertNoForceCustomPlan;
|
|
201
|
+
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
202
|
+
private assertIdentifiesOneRow;
|
|
190
203
|
private assertPagination;
|
|
191
204
|
/** A predicate that is always false, the empty-`in` / contradiction sentinel. */
|
|
192
205
|
private alwaysFalse;
|
package/dist/cjs/powql.js
CHANGED
|
@@ -77,6 +77,7 @@ const powdb_js_1 = require("./powdb.js");
|
|
|
77
77
|
const aggregates_js_1 = require("./query/aggregates.js");
|
|
78
78
|
const compound_unique_js_1 = require("./query/compound-unique.js");
|
|
79
79
|
const filters_js_1 = require("./query/filters.js");
|
|
80
|
+
const option_surface_js_1 = require("./query/option-surface.js");
|
|
80
81
|
const relation_names_js_1 = require("./query/relation-names.js");
|
|
81
82
|
// The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
|
|
82
83
|
// are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
|
|
@@ -374,13 +375,14 @@ class PowqlInterface {
|
|
|
374
375
|
return this.pool.capabilities ?? powdb_js_1.ALL_POWDB_CAPABILITIES;
|
|
375
376
|
}
|
|
376
377
|
/**
|
|
377
|
-
* The `limit` a query actually emits: the explicit `limit`,
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
378
|
+
* The `limit` a query actually emits: the explicit `limit`, then the
|
|
379
|
+
* client-level `defaultLimit`. Prisma's `take` alias is already folded into
|
|
380
|
+
* `limit` by `normalizeArgs`, so there is one spelling by the time this runs.
|
|
381
|
+
* Shared by {@link buildFind} and the {@link findMany} zero short-circuit so
|
|
382
|
+
* the two can never disagree about which limit is in force.
|
|
381
383
|
*/
|
|
382
384
|
effectiveLimit(args) {
|
|
383
|
-
return args.limit ??
|
|
385
|
+
return args.limit ?? this.defaultLimit;
|
|
384
386
|
}
|
|
385
387
|
/**
|
|
386
388
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
@@ -419,12 +421,28 @@ class PowqlInterface {
|
|
|
419
421
|
const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, args.with);
|
|
420
422
|
return normalized === args.with ? args : { ...args, with: normalized };
|
|
421
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Caller args in canonical form: declared relation spellings, and `take` /
|
|
426
|
+
* `skip` folded into `limit` / `offset`.
|
|
427
|
+
*
|
|
428
|
+
* Same composition as `QueryInterface.normalizeArgs` and here for the same
|
|
429
|
+
* reason the method above is here: nothing about a parallel implementation
|
|
430
|
+
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
431
|
+
* not `skip` pages differently from one that reads both.
|
|
432
|
+
*/
|
|
433
|
+
normalizeArgs(args) {
|
|
434
|
+
return (0, utils_js_1.normalizePagination)(this.withDeclaredRelationNames(args));
|
|
435
|
+
}
|
|
422
436
|
assertNoForceCustomPlan(args) {
|
|
423
437
|
if (args?.forceCustomPlan !== true)
|
|
424
438
|
return;
|
|
425
439
|
throw new errors_js_1.UnsupportedFeatureError('The forceCustomPlan query option', 'powdb', 'Forcing a per-query custom plan means keeping the statement out of the PostgreSQL plan cache, and PowDB ' +
|
|
426
440
|
'has no such cache to keep it out of. Remove the option, or set it only on PostgreSQL queries.');
|
|
427
441
|
}
|
|
442
|
+
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
443
|
+
assertIdentifiesOneRow(where) {
|
|
444
|
+
(0, compound_unique_js_1.assertWhereIdentifiesOneRow)(this.meta, this.table, where);
|
|
445
|
+
}
|
|
428
446
|
assertPagination(limit, offset, context) {
|
|
429
447
|
for (const [name, value] of [
|
|
430
448
|
['limit', limit],
|
|
@@ -1338,6 +1356,11 @@ class PowqlInterface {
|
|
|
1338
1356
|
}
|
|
1339
1357
|
/** Run a method body through the middleware chain (mirrors QueryInterface). */
|
|
1340
1358
|
async withMiddleware(action, args, executor) {
|
|
1359
|
+
// The unknown-key diagnostic, at the same seam and for the same reason as
|
|
1360
|
+
// `QueryInterface.executeWithMiddleware`. The option surface is the CORE
|
|
1361
|
+
// one because these args ARE core's args; PowDB reads them, it does not
|
|
1362
|
+
// define them.
|
|
1363
|
+
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
|
|
1341
1364
|
if (this.middlewares.length === 0)
|
|
1342
1365
|
return executor();
|
|
1343
1366
|
let index = 0;
|
|
@@ -1363,7 +1386,7 @@ class PowqlInterface {
|
|
|
1363
1386
|
// -------------------------------------------------------------------------
|
|
1364
1387
|
async findMany(args = {}) {
|
|
1365
1388
|
this.assertNoForceCustomPlan(args);
|
|
1366
|
-
args = this.
|
|
1389
|
+
args = this.normalizeArgs(args);
|
|
1367
1390
|
return this.withMiddleware('findMany', args, async () => {
|
|
1368
1391
|
// `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
|
|
1369
1392
|
// is correct on every engine version: PowDB's projection fast path returned
|
|
@@ -1503,6 +1526,12 @@ class PowqlInterface {
|
|
|
1503
1526
|
* too, so both engines agree.
|
|
1504
1527
|
*/
|
|
1505
1528
|
async explain(args = {}) {
|
|
1529
|
+
// `explain` must compile the statement `findMany` would run, so it
|
|
1530
|
+
// normalizes the same args the same way. Without this, explaining a query
|
|
1531
|
+
// written with `take` / `skip` would report a plan for a DIFFERENT
|
|
1532
|
+
// statement than the one that executes, which is the one thing a
|
|
1533
|
+
// diagnostic must not do.
|
|
1534
|
+
args = this.normalizeArgs(args);
|
|
1506
1535
|
const params = [];
|
|
1507
1536
|
const { powql } = await this.buildFind(args, params);
|
|
1508
1537
|
const { rows } = await this.exec(`explain ${powql}`, params, args.timeout, 'explain');
|
|
@@ -1515,7 +1544,7 @@ class PowqlInterface {
|
|
|
1515
1544
|
}
|
|
1516
1545
|
async findUnique(args) {
|
|
1517
1546
|
this.assertNoForceCustomPlan(args);
|
|
1518
|
-
args = this.
|
|
1547
|
+
args = this.normalizeArgs(args);
|
|
1519
1548
|
// Prisma compound-unique selector → column conjunction (engine parity with
|
|
1520
1549
|
// the SQL findUnique family; pure metadata, so this is a one-line adoption).
|
|
1521
1550
|
if (args.where) {
|
|
@@ -1523,6 +1552,11 @@ class PowqlInterface {
|
|
|
1523
1552
|
if (expanded !== args.where)
|
|
1524
1553
|
args = { ...args, where: expanded };
|
|
1525
1554
|
}
|
|
1555
|
+
// AFTER the selector expansion, so a compound selector counts as the key it
|
|
1556
|
+
// is. Same rule and same message as the SQL engines: a `where` that matches
|
|
1557
|
+
// many rows plus `limit 1` returns an arbitrary one of them, and PowDB has
|
|
1558
|
+
// no more of an ordering guarantee there than Postgres does.
|
|
1559
|
+
this.assertIdentifiesOneRow(args.where);
|
|
1526
1560
|
return this.withMiddleware('findUnique', args, async () => {
|
|
1527
1561
|
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
|
|
1528
1562
|
if (!rows.length)
|
|
@@ -1540,7 +1574,7 @@ class PowqlInterface {
|
|
|
1540
1574
|
});
|
|
1541
1575
|
}
|
|
1542
1576
|
async findFirst(args = {}) {
|
|
1543
|
-
args = this.
|
|
1577
|
+
args = this.normalizeArgs(args);
|
|
1544
1578
|
this.assertNoForceCustomPlan(args);
|
|
1545
1579
|
return this.withMiddleware('findFirst', args, async () => {
|
|
1546
1580
|
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
@@ -1894,7 +1928,7 @@ class PowqlInterface {
|
|
|
1894
1928
|
* one loader chunk (the loader limits per chunk, the join once globally).
|
|
1895
1929
|
*/
|
|
1896
1930
|
joinEligible(rel, opt, args, parentCount) {
|
|
1897
|
-
const effLimit = args.limit ??
|
|
1931
|
+
const effLimit = args.limit ?? this.defaultLimit;
|
|
1898
1932
|
if (effLimit !== undefined || args.offset)
|
|
1899
1933
|
return false;
|
|
1900
1934
|
const options = (opt === true ? {} : opt);
|
|
@@ -551,6 +551,24 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
551
551
|
* query/relation-names.ts.
|
|
552
552
|
*/
|
|
553
553
|
private withDeclaredRelationNames;
|
|
554
|
+
/**
|
|
555
|
+
* Caller args in canonical form: declared relation spellings, and the Prisma
|
|
556
|
+
* pagination aliases folded into `limit` / `offset`.
|
|
557
|
+
*
|
|
558
|
+
* ONE method rather than two calls at each seam, because the two
|
|
559
|
+
* normalizations have the same requirement and the same failure mode: both
|
|
560
|
+
* must happen before `withFingerprint` / the cache key, and a seam that
|
|
561
|
+
* applies one but not the other is a seam where the alias survives into a
|
|
562
|
+
* fingerprint. Both return their input by reference when there was nothing to
|
|
563
|
+
* change, so the common path still allocates nothing.
|
|
564
|
+
*/
|
|
565
|
+
private normalizeArgs;
|
|
566
|
+
/**
|
|
567
|
+
* Refuse a `findUnique` whose `where` names no unique key. The rule and the
|
|
568
|
+
* message live in query/compound-unique.ts, beside the definition of what
|
|
569
|
+
* counts as unique and shared with the PowDB engine.
|
|
570
|
+
*/
|
|
571
|
+
private assertFindUniqueKey;
|
|
554
572
|
/**
|
|
555
573
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
556
574
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -57,6 +57,7 @@ const aggMod = __importStar(require("./aggregates.js"));
|
|
|
57
57
|
const batched_loader_js_1 = require("./batched-loader.js");
|
|
58
58
|
const compound_unique_js_1 = require("./compound-unique.js");
|
|
59
59
|
const filters_js_1 = require("./filters.js");
|
|
60
|
+
const option_surface_js_1 = require("./option-surface.js");
|
|
60
61
|
const relation_names_js_1 = require("./relation-names.js");
|
|
61
62
|
const relationsMod = __importStar(require("./relations.js"));
|
|
62
63
|
const types_js_1 = require("./types.js");
|
|
@@ -1121,6 +1122,28 @@ class QueryInterface {
|
|
|
1121
1122
|
const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, args.with);
|
|
1122
1123
|
return normalized === args.with ? args : { ...args, with: normalized };
|
|
1123
1124
|
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Caller args in canonical form: declared relation spellings, and the Prisma
|
|
1127
|
+
* pagination aliases folded into `limit` / `offset`.
|
|
1128
|
+
*
|
|
1129
|
+
* ONE method rather than two calls at each seam, because the two
|
|
1130
|
+
* normalizations have the same requirement and the same failure mode: both
|
|
1131
|
+
* must happen before `withFingerprint` / the cache key, and a seam that
|
|
1132
|
+
* applies one but not the other is a seam where the alias survives into a
|
|
1133
|
+
* fingerprint. Both return their input by reference when there was nothing to
|
|
1134
|
+
* change, so the common path still allocates nothing.
|
|
1135
|
+
*/
|
|
1136
|
+
normalizeArgs(args) {
|
|
1137
|
+
return (0, utils_js_1.normalizePagination)(this.withDeclaredRelationNames(args));
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Refuse a `findUnique` whose `where` names no unique key. The rule and the
|
|
1141
|
+
* message live in query/compound-unique.ts, beside the definition of what
|
|
1142
|
+
* counts as unique and shared with the PowDB engine.
|
|
1143
|
+
*/
|
|
1144
|
+
assertFindUniqueKey(where) {
|
|
1145
|
+
(0, compound_unique_js_1.assertWhereIdentifiesOneRow)(this.tableMeta, this.table, where);
|
|
1146
|
+
}
|
|
1124
1147
|
/**
|
|
1125
1148
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
1126
1149
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -1260,7 +1283,7 @@ class QueryInterface {
|
|
|
1260
1283
|
return false;
|
|
1261
1284
|
if (!isEmptyOrderBy(args.orderBy))
|
|
1262
1285
|
return false;
|
|
1263
|
-
if (args.limit !== undefined || args.
|
|
1286
|
+
if (args.limit !== undefined || args.offset !== undefined)
|
|
1264
1287
|
return true;
|
|
1265
1288
|
return this.cursorFields(args.cursor).length > 0;
|
|
1266
1289
|
}
|
|
@@ -1325,7 +1348,6 @@ class QueryInterface {
|
|
|
1325
1348
|
const shape = [
|
|
1326
1349
|
cursorFields.length > 0 ? 'cursor' : '',
|
|
1327
1350
|
args?.limit !== undefined ? 'limit' : '',
|
|
1328
|
-
args?.take !== undefined ? 'take' : '',
|
|
1329
1351
|
args?.offset !== undefined ? 'offset' : '',
|
|
1330
1352
|
]
|
|
1331
1353
|
.filter(Boolean)
|
|
@@ -1455,7 +1477,7 @@ class QueryInterface {
|
|
|
1455
1477
|
* not an estimate.
|
|
1456
1478
|
*/
|
|
1457
1479
|
autoParentBound(args) {
|
|
1458
|
-
return args?.
|
|
1480
|
+
return args?.limit ?? this.defaultLimit;
|
|
1459
1481
|
}
|
|
1460
1482
|
/**
|
|
1461
1483
|
* The parent-row count at which `'auto'` stops preferring the single-statement
|
|
@@ -1779,7 +1801,7 @@ class QueryInterface {
|
|
|
1779
1801
|
// Declared relation spellings first, exactly as the join path does in
|
|
1780
1802
|
// buildFindMany: the loader reads `args.with` itself, so without this the
|
|
1781
1803
|
// two strategies would disagree about which relation names are valid.
|
|
1782
|
-
args = this.
|
|
1804
|
+
args = this.normalizeArgs(args);
|
|
1783
1805
|
// Stable relation order (opt-in): the batched loader forwards each relation's
|
|
1784
1806
|
// orderBy into its follow-up query, so filling the synthesized PK order here
|
|
1785
1807
|
// makes the batched output deterministic exactly like the join path.
|
|
@@ -2166,6 +2188,12 @@ class QueryInterface {
|
|
|
2166
2188
|
*/
|
|
2167
2189
|
async executeWithMiddleware(action, args, executor) {
|
|
2168
2190
|
this.currentAction = action;
|
|
2191
|
+
// Every public operation passes through here with its own name and the
|
|
2192
|
+
// caller's args, which makes this the one place the unknown-key diagnostic
|
|
2193
|
+
// can be complete. Putting it in each method instead would mean fifteen
|
|
2194
|
+
// sites and a new one every time an operation is added, which is precisely
|
|
2195
|
+
// how the surface it checks came to need checking.
|
|
2196
|
+
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
|
|
2169
2197
|
if (this.middlewares.length === 0) {
|
|
2170
2198
|
return executor();
|
|
2171
2199
|
}
|
|
@@ -2194,7 +2222,7 @@ class QueryInterface {
|
|
|
2194
2222
|
// changes the warning text from the declared name back to the caller's and
|
|
2195
2223
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2196
2224
|
// depending on every consumer to re-establish it.
|
|
2197
|
-
args = this.
|
|
2225
|
+
args = this.normalizeArgs(args);
|
|
2198
2226
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
2199
2227
|
if (args.with) {
|
|
2200
2228
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -2223,7 +2251,7 @@ class QueryInterface {
|
|
|
2223
2251
|
*/
|
|
2224
2252
|
async runFindUniqueBatched(args) {
|
|
2225
2253
|
// Declared relation spellings first, see runFindManyBatched.
|
|
2226
|
-
args = this.
|
|
2254
|
+
args = this.normalizeArgs(args);
|
|
2227
2255
|
// Stable relation order (opt-in), see runFindManyBatched.
|
|
2228
2256
|
const withClause = this.resolveStableOrder(args.stableRelationOrder)
|
|
2229
2257
|
? this.applyStableRelationOrder(args.with, this.table)
|
|
@@ -2277,9 +2305,22 @@ class QueryInterface {
|
|
|
2277
2305
|
'A key whose value is `undefined` does not count, check that the value you are looking up is defined. ' +
|
|
2278
2306
|
'If you meant "any row matching an optional filter", use `findFirst`.');
|
|
2279
2307
|
}
|
|
2308
|
+
// ...and a where that HAS a predicate but does not name a unique key is the
|
|
2309
|
+
// same hazard one step along (0.73.0). `findUnique({ where: { status:
|
|
2310
|
+
// 'active' } })` used to emit `WHERE status = $1 LIMIT 1` with no ORDER BY:
|
|
2311
|
+
// one row out of many, chosen by the engine, different between two calls
|
|
2312
|
+
// with the same argument and between two plans for the same call. The
|
|
2313
|
+
// caller who wrote `findUnique` asked for the row, not a row, and the
|
|
2314
|
+
// `null` branch they wrote reads as "no such row" when it means "none
|
|
2315
|
+
// matched this filter".
|
|
2316
|
+
//
|
|
2317
|
+
// Checked against the USER's where for the same reason as the guard above:
|
|
2318
|
+
// a global filter is not an identity, and letting one satisfy this would
|
|
2319
|
+
// hand back an arbitrary row from inside the tenant.
|
|
2320
|
+
this.assertFindUniqueKey(args.where);
|
|
2280
2321
|
// Declared relation spellings first, before stable-order and the
|
|
2281
2322
|
// fingerprint (see buildFindMany).
|
|
2282
|
-
args = this.
|
|
2323
|
+
args = this.normalizeArgs(args);
|
|
2283
2324
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2284
2325
|
// relations before fingerprinting (see buildFindMany).
|
|
2285
2326
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -2428,7 +2469,7 @@ class QueryInterface {
|
|
|
2428
2469
|
// changes the warning text from the declared name back to the caller's and
|
|
2429
2470
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2430
2471
|
// depending on every consumer to re-establish it.
|
|
2431
|
-
args = this.
|
|
2472
|
+
args = this.normalizeArgs(args);
|
|
2432
2473
|
this.maybeWarnUnlimited(args);
|
|
2433
2474
|
this.maybeWarnUnorderedPage(args);
|
|
2434
2475
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -2536,7 +2577,7 @@ class QueryInterface {
|
|
|
2536
2577
|
return;
|
|
2537
2578
|
if (this.defaultLimit !== undefined)
|
|
2538
2579
|
return;
|
|
2539
|
-
const hasExplicitLimit = args?.limit !== undefined || args?.
|
|
2580
|
+
const hasExplicitLimit = args?.limit !== undefined || args?.cursor !== undefined;
|
|
2540
2581
|
if (hasExplicitLimit)
|
|
2541
2582
|
return;
|
|
2542
2583
|
if (this.whereMatchesAtMostOneRow(args?.where))
|
|
@@ -2615,7 +2656,7 @@ class QueryInterface {
|
|
|
2615
2656
|
// pass and the fingerprint below and before any of the six `with` walkers,
|
|
2616
2657
|
// so none of them needs to know a relation has two accepted spellings and
|
|
2617
2658
|
// both spellings share one cache entry. See query/relation-names.ts.
|
|
2618
|
-
args = this.
|
|
2659
|
+
args = this.normalizeArgs(args);
|
|
2619
2660
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2620
2661
|
// relations BEFORE fingerprinting, so the two orderings get distinct cache
|
|
2621
2662
|
// entries and every downstream path (SQL build, collect, parser) inherits it.
|
|
@@ -2746,7 +2787,7 @@ class QueryInterface {
|
|
|
2746
2787
|
// the caller's column order, so a permuted array rebuilds different SQL and
|
|
2747
2788
|
// must not collapse onto the same cache entry (would trip the cross-check).
|
|
2748
2789
|
const distinctFp = args?.distinct ? args.distinct.join(',') : '';
|
|
2749
|
-
const effectiveLimit = args?.
|
|
2790
|
+
const effectiveLimit = args?.limit ?? this.defaultLimit;
|
|
2750
2791
|
// On engines that inline the literal LIMIT/OFFSET into the SQL text
|
|
2751
2792
|
// (dialect.inlineLimitOffset, MySQL), the value is part of the SQL, not the
|
|
2752
2793
|
// params, so it MUST be part of the fingerprint or two different limits share
|
|
@@ -3032,8 +3073,19 @@ class QueryInterface {
|
|
|
3032
3073
|
* non-empty.
|
|
3033
3074
|
*/
|
|
3034
3075
|
async *streamRaw(args, action) {
|
|
3076
|
+
// Pagination aliases folded BEFORE the speculative build below, which
|
|
3077
|
+
// spreads `args` and overrides `limit`. A surviving `take` would reach that
|
|
3078
|
+
// spread alongside the override and be read as two different values for one
|
|
3079
|
+
// bound. Relation names are left to `buildFindMany`, which both branches go
|
|
3080
|
+
// through.
|
|
3081
|
+
args = (0, utils_js_1.normalizePagination)(args);
|
|
3035
3082
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
3036
3083
|
this.currentAction = action;
|
|
3084
|
+
// The two streaming methods do NOT go through `executeWithMiddleware`, so
|
|
3085
|
+
// the unknown-key diagnostic has to be hung here as well or a stream would
|
|
3086
|
+
// be the one read that silently drops an `include`. Both public methods
|
|
3087
|
+
// reach this, and each passes its own name.
|
|
3088
|
+
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
|
|
3037
3089
|
// Streaming is ALREADY immune to the generic-plan cliff: the speculative
|
|
3038
3090
|
// fetch has never passed a prepared name, and the cursor path runs through
|
|
3039
3091
|
// DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
|
|
@@ -3214,7 +3266,7 @@ class QueryInterface {
|
|
|
3214
3266
|
// changes the warning text from the declared name back to the caller's and
|
|
3215
3267
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
3216
3268
|
// depending on every consumer to re-establish it.
|
|
3217
|
-
args = this.
|
|
3269
|
+
args = this.normalizeArgs(args);
|
|
3218
3270
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
3219
3271
|
if (args?.with) {
|
|
3220
3272
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -49,3 +49,47 @@ import type { TableMetadata } from '../schema.js';
|
|
|
49
49
|
* unknown-column error at SQL-build time.
|
|
50
50
|
*/
|
|
51
51
|
export declare function expandCompoundUniqueWhere(meta: TableMetadata, where: Record<string, unknown>): Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Every column set that identifies AT MOST ONE ROW of this table.
|
|
54
|
+
*
|
|
55
|
+
* Same sources and the same partial-index rule as {@link syntheticKeyMap}, and
|
|
56
|
+
* in this module for that reason: "what identifies one row" is one question,
|
|
57
|
+
* and answering it in two places is how a compound selector comes to be
|
|
58
|
+
* accepted by the name and refused by its members (0.72.0 fixed exactly that).
|
|
59
|
+
* The difference is only the arity: a synthetic SELECTOR needs two or more
|
|
60
|
+
* columns to have a joined name, while a single-column unique identifies a row
|
|
61
|
+
* perfectly well.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* Throw unless `where` identifies a single row. The refusal for `findUnique` on
|
|
65
|
+
* EVERY engine, message included.
|
|
66
|
+
*
|
|
67
|
+
* Shared rather than written twice because `PowqlInterface` is a parallel
|
|
68
|
+
* implementation: two copies of a rule this specific (which sources count as
|
|
69
|
+
* unique, whether a null identifies, which keys the message lists) is how two
|
|
70
|
+
* engines come to disagree about whether a query is valid, which is the exact
|
|
71
|
+
* divergence 0.64.0 and 0.72.0 were both spent on.
|
|
72
|
+
*
|
|
73
|
+
* The message lists the keys that WOULD work, because the fix is almost always
|
|
74
|
+
* one of them and a caller cannot be expected to know which columns the
|
|
75
|
+
* database considers unique. A table with no unique key at all gets its own
|
|
76
|
+
* sentence: no `where` satisfies this, and "name a unique key" is advice that
|
|
77
|
+
* person cannot take.
|
|
78
|
+
*/
|
|
79
|
+
export declare function assertWhereIdentifiesOneRow(meta: TableMetadata, table: string, where: Record<string, unknown> | undefined): void;
|
|
80
|
+
export declare function uniqueKeyNames(meta: TableMetadata): string[][];
|
|
81
|
+
/**
|
|
82
|
+
* True when `where` pins every column of at least one unique key to a single
|
|
83
|
+
* value, so the row it names is the row it gets.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately reads only the TOP LEVEL of the user's where. A unique key
|
|
86
|
+
* buried inside an `OR` does not identify a row (the other branch matches
|
|
87
|
+
* whatever it matches), and one inside an `AND` array is a shape nobody writes
|
|
88
|
+
* for a lookup by identity. Extra predicates alongside the key are fine: they
|
|
89
|
+
* can only narrow a set that already holds at most one row.
|
|
90
|
+
*
|
|
91
|
+
* A NULL is not an identity. `WHERE email IS NULL` matches every row whose
|
|
92
|
+
* email is null, which a UNIQUE constraint permits any number of, so a null
|
|
93
|
+
* value satisfies no key here even on a unique column.
|
|
94
|
+
*/
|
|
95
|
+
export declare function whereIdentifiesOneRow(meta: TableMetadata, where: Record<string, unknown>): boolean;
|