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.
@@ -41,6 +41,9 @@
41
41
  */
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.expandCompoundUniqueWhere = expandCompoundUniqueWhere;
44
+ exports.assertWhereIdentifiesOneRow = assertWhereIdentifiesOneRow;
45
+ exports.uniqueKeyNames = uniqueKeyNames;
46
+ exports.whereIdentifiesOneRow = whereIdentifiesOneRow;
44
47
  const errors_js_1 = require("../errors.js");
45
48
  const filters_js_1 = require("./filters.js");
46
49
  const utils_js_1 = require("./utils.js");
@@ -203,3 +206,119 @@ function expandCompoundUniqueWhere(meta, where) {
203
206
  }
204
207
  return result ?? where;
205
208
  }
209
+ /**
210
+ * Every column set that identifies AT MOST ONE ROW of this table.
211
+ *
212
+ * Same sources and the same partial-index rule as {@link syntheticKeyMap}, and
213
+ * in this module for that reason: "what identifies one row" is one question,
214
+ * and answering it in two places is how a compound selector comes to be
215
+ * accepted by the name and refused by its members (0.72.0 fixed exactly that).
216
+ * The difference is only the arity: a synthetic SELECTOR needs two or more
217
+ * columns to have a joined name, while a single-column unique identifies a row
218
+ * perfectly well.
219
+ */
220
+ /**
221
+ * Throw unless `where` identifies a single row. The refusal for `findUnique` on
222
+ * EVERY engine, message included.
223
+ *
224
+ * Shared rather than written twice because `PowqlInterface` is a parallel
225
+ * implementation: two copies of a rule this specific (which sources count as
226
+ * unique, whether a null identifies, which keys the message lists) is how two
227
+ * engines come to disagree about whether a query is valid, which is the exact
228
+ * divergence 0.64.0 and 0.72.0 were both spent on.
229
+ *
230
+ * The message lists the keys that WOULD work, because the fix is almost always
231
+ * one of them and a caller cannot be expected to know which columns the
232
+ * database considers unique. A table with no unique key at all gets its own
233
+ * sentence: no `where` satisfies this, and "name a unique key" is advice that
234
+ * person cannot take.
235
+ */
236
+ function assertWhereIdentifiesOneRow(meta, table, where) {
237
+ if (whereIdentifiesOneRow(meta, where ?? {}))
238
+ return;
239
+ const field = (c) => meta.reverseColumnMap[c] ?? c;
240
+ const keys = uniqueKeyNames(meta).map((cols) => cols.length === 1 ? `\`${field(cols[0])}\`` : `\`{ ${cols.map(field).join(', ')} }\``);
241
+ const advice = keys.length > 0
242
+ ? `Name a unique key (${keys.join(', ')}), or use \`findFirst\` if you meant "any row matching a filter".`
243
+ : `Table "${table}" declares no primary key and no unique constraint, so no \`where\` can identify one row ` +
244
+ 'here. Use `findFirst` (add an `orderBy` to make which row it is deterministic).';
245
+ throw new errors_js_1.ValidationError(`[turbine] findUnique on "${table}" refused: the \`where\` clause does not identify a single row, ` +
246
+ `so this would return an arbitrary one of the rows that match. ${advice}`);
247
+ }
248
+ function uniqueKeyNames(meta) {
249
+ return dedupeColumnSets(uniqueColumnSets(meta));
250
+ }
251
+ /** Distinct column sets, preserving first-seen order (a PK is often also a declared unique). */
252
+ function dedupeColumnSets(sets) {
253
+ const seen = new Set();
254
+ const out = [];
255
+ for (const cols of sets) {
256
+ const sig = cols.join('\u0000');
257
+ if (seen.has(sig))
258
+ continue;
259
+ seen.add(sig);
260
+ out.push(cols);
261
+ }
262
+ return out;
263
+ }
264
+ function uniqueColumnSets(meta) {
265
+ const sets = [];
266
+ if (meta.primaryKey.length > 0)
267
+ sets.push(meta.primaryKey);
268
+ for (const uc of meta.uniqueColumns)
269
+ if (uc.length > 0)
270
+ sets.push(uc);
271
+ for (const idx of meta.indexes) {
272
+ if (idx.unique && !idx.docPath && !idx.partial && idx.columns.length > 0)
273
+ sets.push(idx.columns);
274
+ }
275
+ return sets;
276
+ }
277
+ /**
278
+ * True when `where` pins every column of at least one unique key to a single
279
+ * value, so the row it names is the row it gets.
280
+ *
281
+ * Deliberately reads only the TOP LEVEL of the user's where. A unique key
282
+ * buried inside an `OR` does not identify a row (the other branch matches
283
+ * whatever it matches), and one inside an `AND` array is a shape nobody writes
284
+ * for a lookup by identity. Extra predicates alongside the key are fine: they
285
+ * can only narrow a set that already holds at most one row.
286
+ *
287
+ * A NULL is not an identity. `WHERE email IS NULL` matches every row whose
288
+ * email is null, which a UNIQUE constraint permits any number of, so a null
289
+ * value satisfies no key here even on a unique column.
290
+ */
291
+ function whereIdentifiesOneRow(meta, where) {
292
+ const pinned = new Set();
293
+ for (const [key, value] of Object.entries(where)) {
294
+ if (!isPinnedToOneValue(value))
295
+ continue;
296
+ const column = (0, utils_js_1.resolveColumnName)(meta, key);
297
+ if (column !== undefined)
298
+ pinned.add(column);
299
+ }
300
+ if (pinned.size === 0)
301
+ return false;
302
+ return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
303
+ }
304
+ /** A bare value, or an operator object whose `equals` is a value. */
305
+ function isPinnedToOneValue(value) {
306
+ if (value === undefined || value === null)
307
+ return false;
308
+ if ((0, filters_js_1.isWhereOperator)(value)) {
309
+ const eq = value.equals;
310
+ return eq !== undefined && eq !== null;
311
+ }
312
+ // A JSON / array / vector filter narrows, it does not identify.
313
+ if ((0, filters_js_1.isJsonFilter)(value) || (0, filters_js_1.isArrayFilter)(value) || (0, filters_js_1.isVectorFilter)(value))
314
+ return false;
315
+ // Anything else that is a plain object is a relation filter or a sub-where,
316
+ // neither of which pins a column. Dates, Buffers and primitives are values.
317
+ return !isPlainObject(value);
318
+ }
319
+ function isPlainObject(value) {
320
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
321
+ return false;
322
+ const proto = Object.getPrototypeOf(value);
323
+ return proto === Object.prototype || proto === null;
324
+ }
@@ -108,4 +108,26 @@ export declare const ALL_OPTION_TABLES: Readonly<Record<string, Readonly<Record<
108
108
  export declare function applyNativeOptions(table: Readonly<Record<string, OptionKind>>, src: Record<string, unknown>, dst: Record<string, unknown>): void;
109
109
  /** The keys of `table` with the given kind, as a set. */
110
110
  export declare function optionKeysOfKind(table: Readonly<Record<string, OptionKind>>, ...kinds: OptionKind[]): string[];
111
+ /**
112
+ * Dev-mode warning for a key that is not part of the operation's option
113
+ * surface, and is therefore doing nothing.
114
+ *
115
+ * The motivating case is `include`. It is Prisma's word for `with`, it is what
116
+ * a model or a developer coming from Prisma reaches for first, and an
117
+ * unrecognized key is simply ignored: the query runs, returns rows, and the
118
+ * relation the caller asked for is absent. No error, no empty array, just a
119
+ * missing key on every row. A cross-model eval measured this as the single
120
+ * largest source of confidently-wrong queries against Turbine, and every one of
121
+ * them looked like a success from inside the process.
122
+ *
123
+ * A WARNING and never an error, deliberately. Refusing an unknown key would
124
+ * break `findMany({ ...someOptionsBag })`, which is ordinary code, and the
125
+ * option surface grows: a caller pinned to an older minor would have their
126
+ * working query start throwing. A warning costs a correct program nothing and
127
+ * tells an incorrect one exactly what happened.
128
+ *
129
+ * Dev-only, once per `table.operation.key` per process, and total: the whole
130
+ * body is wrapped, because a diagnostic must never be the reason a query fails.
131
+ */
132
+ export declare function warnUnknownQueryOptions(table: string, operation: string, args: unknown): void;
111
133
  export {};
@@ -56,6 +56,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
56
56
  exports.ALL_OPTION_TABLES = exports.GROUP_BY_OPTIONS = exports.AGGREGATE_OPTIONS = exports.COUNT_OPTIONS = exports.UPSERT_OPTIONS = exports.DELETE_MANY_OPTIONS = exports.DELETE_OPTIONS = exports.UPDATE_MANY_OPTIONS = exports.UPDATE_OPTIONS = exports.CREATE_MANY_OPTIONS = exports.CREATE_OPTIONS = exports.FIND_MANY_STREAM_OPTIONS = exports.FIND_MANY_OPTIONS = exports.FIND_UNIQUE_OPTIONS = void 0;
57
57
  exports.applyNativeOptions = applyNativeOptions;
58
58
  exports.optionKeysOfKind = optionKeysOfKind;
59
+ exports.warnUnknownQueryOptions = warnUnknownQueryOptions;
60
+ // Runtime imports, and the only ones in this file: the unknown-key warning at
61
+ // the bottom needs the once-per-process registry and the name suggester. Both
62
+ // are leaves that do not import this module, so the type-only shape of
63
+ // everything above is unaffected.
64
+ const utils_js_1 = require("./utils.js");
65
+ const warn_registry_js_1 = require("./warn-registry.js");
59
66
  exports.FIND_UNIQUE_OPTIONS = {
60
67
  where: 'prisma',
61
68
  select: 'prisma',
@@ -84,7 +91,13 @@ exports.FIND_MANY_OPTIONS = {
84
91
  omit: 'prisma',
85
92
  orderBy: 'prisma',
86
93
  cursor: 'prisma',
94
+ // `take` and `skip` are Prisma's own spellings and core accepts them as
95
+ // aliases, but they stay hand-translated because Prisma gives them meanings
96
+ // core does not have: a NEGATIVE `take` pages from the end, and `skip: 1`
97
+ // beside a `cursor` is the exclusive-pagination idiom. Forwarding either
98
+ // verbatim would hand core a number that means something else.
87
99
  take: 'prisma',
100
+ skip: 'prisma',
88
101
  distinct: 'prisma',
89
102
  relationLoadStrategy: 'prisma',
90
103
  with: 'nativeAlias',
@@ -231,3 +244,81 @@ function applyNativeOptions(table, src, dst) {
231
244
  function optionKeysOfKind(table, ...kinds) {
232
245
  return Object.keys(table).filter((k) => kinds.includes(table[k]));
233
246
  }
247
+ // ---------------------------------------------------------------------------
248
+ // Unknown-key diagnostic
249
+ // ---------------------------------------------------------------------------
250
+ /**
251
+ * The operations a caller can invoke, mapped to the table that describes their
252
+ * legal keys. The `*OrThrow` and `findFirst` variants take the same args as the
253
+ * method they are built on, so they share its table rather than getting a copy
254
+ * that could drift from it.
255
+ */
256
+ const OPERATION_TABLE = {
257
+ ...exports.ALL_OPTION_TABLES,
258
+ findFirst: exports.FIND_MANY_OPTIONS,
259
+ findFirstOrThrow: exports.FIND_MANY_OPTIONS,
260
+ findUniqueOrThrow: exports.FIND_UNIQUE_OPTIONS,
261
+ findManyStreamBatches: exports.FIND_MANY_STREAM_OPTIONS,
262
+ };
263
+ /**
264
+ * Prisma spellings that name a real Turbine option under a different word.
265
+ *
266
+ * Only spellings whose Turbine equivalent EXISTS belong here: the message tells
267
+ * the caller what to write instead, so a key with no equivalent would produce
268
+ * advice that does not work. `take` / `skip` were once on this list and are now
269
+ * accepted outright.
270
+ */
271
+ const PRISMA_SPELLING = {
272
+ include: 'with',
273
+ };
274
+ /**
275
+ * Dev-mode warning for a key that is not part of the operation's option
276
+ * surface, and is therefore doing nothing.
277
+ *
278
+ * The motivating case is `include`. It is Prisma's word for `with`, it is what
279
+ * a model or a developer coming from Prisma reaches for first, and an
280
+ * unrecognized key is simply ignored: the query runs, returns rows, and the
281
+ * relation the caller asked for is absent. No error, no empty array, just a
282
+ * missing key on every row. A cross-model eval measured this as the single
283
+ * largest source of confidently-wrong queries against Turbine, and every one of
284
+ * them looked like a success from inside the process.
285
+ *
286
+ * A WARNING and never an error, deliberately. Refusing an unknown key would
287
+ * break `findMany({ ...someOptionsBag })`, which is ordinary code, and the
288
+ * option surface grows: a caller pinned to an older minor would have their
289
+ * working query start throwing. A warning costs a correct program nothing and
290
+ * tells an incorrect one exactly what happened.
291
+ *
292
+ * Dev-only, once per `table.operation.key` per process, and total: the whole
293
+ * body is wrapped, because a diagnostic must never be the reason a query fails.
294
+ */
295
+ function warnUnknownQueryOptions(table, operation, args) {
296
+ if (process.env.NODE_ENV === 'production')
297
+ return;
298
+ if (args === null || typeof args !== 'object' || Array.isArray(args))
299
+ return;
300
+ const known = OPERATION_TABLE[operation];
301
+ if (!known)
302
+ return;
303
+ try {
304
+ for (const key of Object.keys(args)) {
305
+ // `{ ...maybeOptions }` routinely materializes keys with no value.
306
+ // Nothing is being ignored when the value is undefined.
307
+ if (args[key] === undefined)
308
+ continue;
309
+ if (Object.hasOwn(known, key))
310
+ continue;
311
+ if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unknownQueryOption, `${table}.${operation}.${key}`))
312
+ continue;
313
+ const prismaSpelling = PRISMA_SPELLING[key];
314
+ const suggestion = prismaSpelling ?? (0, utils_js_1.suggestKey)(key, Object.keys(known));
315
+ const because = prismaSpelling ? ` Turbine spells this "${prismaSpelling}".` : '';
316
+ console.warn(`[turbine] unknown option "${key}" in ${table}.${operation}(), it is ignored.${because}` +
317
+ (!prismaSpelling && suggestion ? ` Did you mean "${suggestion}"?` : ''));
318
+ }
319
+ }
320
+ catch {
321
+ // Key enumeration is the only thing that can fail here (a Proxy whose
322
+ // ownKeys throws), and it must not take the query with it.
323
+ }
324
+ }
@@ -629,8 +629,22 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
629
629
  with?: W;
630
630
  /** Cursor-based pagination: start after this row */
631
631
  cursor?: Partial<T>;
632
- /** Number of records to take (used with cursor) */
632
+ /**
633
+ * Prisma's spelling of {@link FindManyArgs.limit}. Folded into `limit` before
634
+ * anything reads it; passing both with different values is a
635
+ * `ValidationError`.
636
+ */
633
637
  take?: number;
638
+ /**
639
+ * Prisma's spelling of {@link FindManyArgs.offset}. Folded into `offset`
640
+ * before anything reads it; passing both with different values is a
641
+ * `ValidationError`.
642
+ *
643
+ * Accepted since 0.73.0. Before that `take` was recognized and `skip` was
644
+ * not, so the Prisma pair `{ take, skip }` silently returned the first page
645
+ * however far the caller thought they had paged.
646
+ */
647
+ skip?: number;
634
648
  /** De-duplicate results by specified fields */
635
649
  distinct?: (keyof T & string)[];
636
650
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
@@ -728,3 +728,26 @@ export declare function warnRedundantSortTerm(table: string, clause: string, dro
728
728
  */
729
729
  export declare function selectNamesNothingMessage(table: string): string;
730
730
  export declare function selectOmitExclusiveMessage(table: string): string;
731
+ /**
732
+ * The Prisma pagination aliases, folded into Turbine's own spelling ONCE,
733
+ * before anything reads them.
734
+ *
735
+ * Turbine's names are `limit` / `offset`; Prisma's are `take` / `skip`. `take`
736
+ * was accepted and `skip` was not, which is the worst of the three possible
737
+ * states: `{ take: 20, skip: 40 }` is what a Prisma habit writes, it looks
738
+ * accepted because half of it is, and the query silently returns page one
739
+ * forever. An unknown key is at least inert on its own; a HALF-recognized pair
740
+ * changes the answer.
741
+ *
742
+ * Folded here rather than read at each site deliberately. `take` used to be
743
+ * handled by six separate `args?.take ?? args?.limit` reads, one of which is
744
+ * the SQL-cache FINGERPRINT, so adding `skip` the same way would have meant
745
+ * teaching six places about it and a miss in the fingerprint is not a missing
746
+ * feature, it is two different pages sharing one cached statement. Normalizing
747
+ * up front leaves `limit` / `offset` as the single authority and the aliases
748
+ * cease to exist below this line.
749
+ *
750
+ * Returns the SAME object when neither alias is present, so the common path
751
+ * allocates nothing.
752
+ */
753
+ export declare function normalizePagination<A extends object | undefined>(args: A): A;
@@ -49,7 +49,9 @@ exports.relationInProjectionMessage = relationInProjectionMessage;
49
49
  exports.warnRedundantSortTerm = warnRedundantSortTerm;
50
50
  exports.selectNamesNothingMessage = selectNamesNothingMessage;
51
51
  exports.selectOmitExclusiveMessage = selectOmitExclusiveMessage;
52
+ exports.normalizePagination = normalizePagination;
52
53
  const pg_1 = __importDefault(require("pg"));
54
+ const errors_js_1 = require("../errors.js");
53
55
  const schema_js_1 = require("../schema.js");
54
56
  const warn_registry_js_1 = require("./warn-registry.js");
55
57
  // ---------------------------------------------------------------------------
@@ -1539,3 +1541,58 @@ function selectOmitExclusiveMessage(table) {
1539
1541
  return (`[turbine] "select" and "omit" are mutually exclusive (on table "${table}"). ` +
1540
1542
  `A select already lists exactly the fields you want.`);
1541
1543
  }
1544
+ /**
1545
+ * The Prisma pagination aliases, folded into Turbine's own spelling ONCE,
1546
+ * before anything reads them.
1547
+ *
1548
+ * Turbine's names are `limit` / `offset`; Prisma's are `take` / `skip`. `take`
1549
+ * was accepted and `skip` was not, which is the worst of the three possible
1550
+ * states: `{ take: 20, skip: 40 }` is what a Prisma habit writes, it looks
1551
+ * accepted because half of it is, and the query silently returns page one
1552
+ * forever. An unknown key is at least inert on its own; a HALF-recognized pair
1553
+ * changes the answer.
1554
+ *
1555
+ * Folded here rather than read at each site deliberately. `take` used to be
1556
+ * handled by six separate `args?.take ?? args?.limit` reads, one of which is
1557
+ * the SQL-cache FINGERPRINT, so adding `skip` the same way would have meant
1558
+ * teaching six places about it and a miss in the fingerprint is not a missing
1559
+ * feature, it is two different pages sharing one cached statement. Normalizing
1560
+ * up front leaves `limit` / `offset` as the single authority and the aliases
1561
+ * cease to exist below this line.
1562
+ *
1563
+ * Returns the SAME object when neither alias is present, so the common path
1564
+ * allocates nothing.
1565
+ */
1566
+ function normalizePagination(args) {
1567
+ if (!args)
1568
+ return args;
1569
+ const a = args;
1570
+ if (a.take === undefined && a.skip === undefined)
1571
+ return args;
1572
+ const out = { ...args };
1573
+ if (a.take !== undefined) {
1574
+ assertAliasAgrees('take', a.take, 'limit', a.limit);
1575
+ out.limit = a.take;
1576
+ delete out.take;
1577
+ }
1578
+ if (a.skip !== undefined) {
1579
+ assertAliasAgrees('skip', a.skip, 'offset', a.offset);
1580
+ out.offset = a.skip;
1581
+ delete out.skip;
1582
+ }
1583
+ return out;
1584
+ }
1585
+ /**
1586
+ * Both spellings of one bound, disagreeing. Refused rather than resolved: the
1587
+ * old `take ?? limit` silently preferred one of the two numbers the caller
1588
+ * wrote, and there is no reading of `{ limit: 10, take: 5 }` that makes one of
1589
+ * them the intended answer. Equal values are accepted, since there is nothing
1590
+ * to choose between.
1591
+ */
1592
+ function assertAliasAgrees(alias, aliasValue, native, nativeValue) {
1593
+ if (nativeValue === undefined || nativeValue === aliasValue)
1594
+ return;
1595
+ throw new errors_js_1.ValidationError(`[turbine] "${alias}" and "${native}" are the same option and were given different values ` +
1596
+ `(${alias}: ${String(aliasValue)}, ${native}: ${String(nativeValue)}). ` +
1597
+ `"${alias}" is Prisma's spelling of "${native}"; pass one of them.`);
1598
+ }
@@ -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/cli/index.js 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
@@ -171,6 +172,17 @@ export function parseArgs(argv = process.argv.slice(2)) {
171
172
  case '--allow-pooler':
172
173
  result.allowPooler = true;
173
174
  break;
175
+ // `turbine skill`
176
+ case '--print':
177
+ result.print = true;
178
+ break;
179
+ case '--agents':
180
+ result.agents = true;
181
+ break;
182
+ case '--dir':
183
+ result.dir = next;
184
+ i++;
185
+ break;
174
186
  case '--zod':
175
187
  result.zod = true;
176
188
  break;
@@ -3945,6 +3957,7 @@ function showHelp() {
3945
3957
  console.log(` ${cyan('studio')} Launch local read-only web UI ${dim('(--write for writes, --demo for a sample DB)')}`);
3946
3958
  console.log(` ${cyan('mcp')} Start read-only MCP server over stdio`);
3947
3959
  console.log(` ${cyan('observe')} Launch metrics dashboard ${dim('(requires TURBINE_OBSERVE_URL)')}`);
3960
+ console.log(` ${cyan('skill')} Install the agent query skill ${dim('(--print, --agents, --dir <path>)')}`);
3948
3961
  newline();
3949
3962
  console.log(` ${bold('Options:')}`);
3950
3963
  console.log(` ${cyan('--url, -u')} ${dim('<url>')} Postgres connection string`);
@@ -3995,6 +4008,103 @@ function showHelp() {
3995
4008
  newline();
3996
4009
  }
3997
4010
  // ---------------------------------------------------------------------------
4011
+ // Agent skill
4012
+ // ---------------------------------------------------------------------------
4013
+ /**
4014
+ * turbine-orm's own package root, found by walking up from the running script.
4015
+ *
4016
+ * `process.argv[1]` rather than `import.meta.url` so the same source compiles
4017
+ * for both the ESM and CJS builds, and realpath first because `npx turbine`
4018
+ * runs through a `node_modules/.bin` symlink whose dirname is the CONSUMER's
4019
+ * tree, where this package.json does not exist.
4020
+ */
4021
+ function ownPackageRoot() {
4022
+ try {
4023
+ let entry = process.argv[1] ?? '';
4024
+ try {
4025
+ entry = realpathSync(entry);
4026
+ }
4027
+ catch {
4028
+ // keep the raw path if realpath fails (e.g. deleted cwd)
4029
+ }
4030
+ let dir = dirname(entry);
4031
+ for (let i = 0; i < 6; i++) {
4032
+ const candidate = resolve(dir, 'package.json');
4033
+ if (existsSync(candidate)) {
4034
+ const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
4035
+ if (pkg.name === 'turbine-orm')
4036
+ return dir;
4037
+ }
4038
+ const parent = dirname(dir);
4039
+ if (parent === dir)
4040
+ break;
4041
+ dir = parent;
4042
+ }
4043
+ }
4044
+ catch {
4045
+ // fall through
4046
+ }
4047
+ return undefined;
4048
+ }
4049
+ /** The instructions block for a project's AGENTS.md / CLAUDE.md. */
4050
+ const AGENTS_SNIPPET = `## Database access (Turbine ORM)
4051
+
4052
+ - Queries go through the generated client. Read \`generated/turbine/types.ts\` for
4053
+ the entity and input types before writing one; the names there are the truth.
4054
+ - Relations are \`with\`, never Prisma's \`include\`. An unrecognized option is
4055
+ ignored, so an \`include\` returns rows with the relation missing.
4056
+ - \`select\` and \`omit\` name columns only. A relation named in \`select\` throws.
4057
+ - \`findUnique\` needs a unique key. Use \`findFirst\` for "any row matching a
4058
+ filter", with an \`orderBy\` if which row matters.
4059
+ - Re-run \`npx turbine generate\` after any schema change, then \`tsc --noEmit\`:
4060
+ an invalid query is a type error, so the type checker is the fastest reviewer.
4061
+ - Never write \`includePii: true\`, \`skipGlobalFilters: true\` or
4062
+ \`allowFullTableScan: true\`. Those options take an imported \`UNSAFE\` symbol and
4063
+ nothing else; any other value throws.
4064
+ - Full query reference: https://turbineorm.dev/llms.txt
4065
+ `;
4066
+ /**
4067
+ * Install the packaged query-writing skill into the project.
4068
+ *
4069
+ * The skill is a file in the published tarball rather than something generated
4070
+ * here, so what an agent reads is exactly what the repository tests: every
4071
+ * factual claim in it is executed against a live database by
4072
+ * `evals/src/verify-skill.ts` on each release.
4073
+ */
4074
+ function cmdSkill(args) {
4075
+ if (args.agents === true) {
4076
+ console.log(AGENTS_SNIPPET);
4077
+ return;
4078
+ }
4079
+ const root = ownPackageRoot();
4080
+ const source = root ? resolve(root, 'skills', 'turbine-orm', 'SKILL.md') : undefined;
4081
+ if (!source || !existsSync(source)) {
4082
+ error('Could not find the packaged skill inside turbine-orm.');
4083
+ newline();
4084
+ console.log(` ${dim('Read it online instead:')} ${cyan('https://turbineorm.dev/ai-agents')}`);
4085
+ newline();
4086
+ process.exit(1);
4087
+ }
4088
+ const body = readFileSync(source, 'utf8');
4089
+ if (args.print === true) {
4090
+ process.stdout.write(body);
4091
+ return;
4092
+ }
4093
+ const skillsDir = args.dir ?? join('.claude', 'skills');
4094
+ const target = resolve(process.cwd(), skillsDir, 'turbine-orm', 'SKILL.md');
4095
+ const existed = existsSync(target);
4096
+ mkdirSync(dirname(target), { recursive: true });
4097
+ writeFileSync(target, body);
4098
+ newline();
4099
+ success(`${existed ? 'Updated' : 'Installed'} the Turbine query skill`);
4100
+ console.log(` ${dim(relative(process.cwd(), target))}`);
4101
+ newline();
4102
+ console.log(` ${dim('Also worth doing:')}`);
4103
+ console.log(` ${dim('-')} connect the read-only MCP server: ${cyan('npx turbine mcp')}`);
4104
+ console.log(` ${dim('-')} add the instructions block to AGENTS.md: ${cyan('npx turbine skill --agents')}`);
4105
+ newline();
4106
+ }
4107
+ // ---------------------------------------------------------------------------
3998
4108
  // Version
3999
4109
  // ---------------------------------------------------------------------------
4000
4110
  function showVersion() {
@@ -4143,6 +4253,9 @@ async function main() {
4143
4253
  case 'observe':
4144
4254
  await cmdObserve(args);
4145
4255
  break;
4256
+ case 'skill':
4257
+ cmdSkill(args);
4258
+ break;
4146
4259
  default:
4147
4260
  error(`Unknown command: ${bold(args.command)}`);
4148
4261
  newline();
package/dist/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`, Prisma's `take`
152
- * alias, then the client-level `defaultLimit`. Shared by {@link buildFind} and
153
- * the {@link findMany} zero short-circuit so the two can never disagree about
154
- * which limit is in force.
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;