turbine-orm 0.77.1 → 0.78.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 +13 -9
- package/dist/cjs/cli/config.d.ts +7 -1
- package/dist/cjs/cli/config.js +11 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +307 -9
- package/dist/cjs/cli/index.js +252 -42
- package/dist/cjs/cli/mcp.d.ts +23 -0
- package/dist/cjs/cli/mcp.js +190 -152
- package/dist/cjs/cli/migrate.d.ts +243 -3
- package/dist/cjs/cli/migrate.js +432 -43
- package/dist/cjs/cli/sql-statements.js +27 -0
- package/dist/cjs/cli/studio.d.ts +0 -1
- package/dist/cjs/cli/studio.js +9 -7
- package/dist/cjs/client.d.ts +8 -1
- package/dist/cjs/client.js +7 -0
- package/dist/cjs/connection-url.d.ts +82 -0
- package/dist/cjs/connection-url.js +187 -1
- package/dist/cjs/errors.d.ts +112 -12
- package/dist/cjs/errors.js +558 -114
- package/dist/cjs/generate.js +47 -15
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/introspect.d.ts +33 -0
- package/dist/cjs/introspect.js +54 -1
- package/dist/cjs/mssql.js +21 -1
- package/dist/cjs/nested-write.js +85 -14
- package/dist/cjs/pipeline-submittable.d.ts +2 -0
- package/dist/cjs/pipeline-submittable.js +88 -3
- package/dist/cjs/pipeline.js +13 -1
- package/dist/cjs/powdb-introspect.d.ts +5 -1
- package/dist/cjs/powdb-introspect.js +5 -1
- package/dist/cjs/powql.d.ts +14 -0
- package/dist/cjs/powql.js +44 -4
- package/dist/cjs/prisma-compat.js +95 -8
- package/dist/cjs/query/aggregates.js +56 -6
- package/dist/cjs/query/builder.d.ts +76 -13
- package/dist/cjs/query/builder.js +188 -58
- package/dist/cjs/query/compound-unique.d.ts +76 -0
- package/dist/cjs/query/compound-unique.js +129 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/types.d.ts +65 -11
- package/dist/cjs/query/where.d.ts +85 -19
- package/dist/cjs/query/where.js +262 -47
- package/dist/cjs/query/writes.d.ts +11 -2
- package/dist/cjs/query/writes.js +116 -21
- package/dist/cjs/seed.d.ts +16 -0
- package/dist/cjs/seed.js +16 -0
- package/dist/cli/config.d.ts +7 -1
- package/dist/cli/config.js +11 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +307 -9
- package/dist/cli/index.js +254 -44
- package/dist/cli/mcp.d.ts +23 -0
- package/dist/cli/mcp.js +187 -150
- package/dist/cli/migrate.d.ts +243 -3
- package/dist/cli/migrate.js +423 -45
- package/dist/cli/sql-statements.js +27 -0
- package/dist/cli/studio.d.ts +0 -1
- package/dist/cli/studio.js +10 -7
- package/dist/client.d.ts +8 -1
- package/dist/client.js +7 -0
- package/dist/connection-url.d.ts +82 -0
- package/dist/connection-url.js +183 -0
- package/dist/errors.d.ts +112 -12
- package/dist/errors.js +558 -114
- package/dist/generate.js +47 -15
- package/dist/index.d.ts +1 -1
- package/dist/introspect.d.ts +33 -0
- package/dist/introspect.js +53 -1
- package/dist/mssql.js +21 -1
- package/dist/nested-write.js +85 -14
- package/dist/pipeline-submittable.d.ts +2 -0
- package/dist/pipeline-submittable.js +87 -3
- package/dist/pipeline.js +14 -2
- package/dist/powdb-introspect.d.ts +5 -1
- package/dist/powdb-introspect.js +5 -1
- package/dist/powql.d.ts +14 -0
- package/dist/powql.js +45 -5
- package/dist/prisma-compat.js +96 -9
- package/dist/query/aggregates.js +56 -6
- package/dist/query/builder.d.ts +76 -13
- package/dist/query/builder.js +188 -58
- package/dist/query/compound-unique.d.ts +76 -0
- package/dist/query/compound-unique.js +126 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +65 -11
- package/dist/query/where.d.ts +85 -19
- package/dist/query/where.js +260 -47
- package/dist/query/writes.d.ts +11 -2
- package/dist/query/writes.js +117 -22
- package/dist/seed.d.ts +16 -0
- package/dist/seed.js +16 -0
- package/package.json +3 -3
- package/skills/turbine-orm/SKILL.md +37 -10
package/dist/query/writes.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { NotFoundError, OptimisticLockError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
15
|
-
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
15
|
+
import { assertMutationWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './compound-unique.js';
|
|
16
16
|
import { isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
|
|
17
17
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
|
|
18
18
|
import { canonicalWriteEntries, coerceTemporalValue, resolveColumnName } from './utils.js';
|
|
@@ -346,7 +346,15 @@ export function buildUpdate(qi, args) {
|
|
|
346
346
|
// The empty-`where` guard checks the USER predicate only, a global filter
|
|
347
347
|
// must never turn an unguarded mass update into an allowed one.
|
|
348
348
|
const userHasPredicate = !whereMod.userPredicateIsEmpty(qi, userWhere) || !!lock;
|
|
349
|
-
|
|
349
|
+
const allowFullTableScan = resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan');
|
|
350
|
+
whereMod.assertMutationHasPredicate(qi, 'update', userHasPredicate ? ' WHERE x' : '', allowFullTableScan);
|
|
351
|
+
// `update` returns ONE row, so its where must identify one: the 0.73
|
|
352
|
+
// `findUnique` rule (query/compound-unique.ts) applied to the write that has
|
|
353
|
+
// the same shape. On the USER's where, before the global filter is merged
|
|
354
|
+
// (a filter narrows, it never identifies), and skipped only under the
|
|
355
|
+
// explicit full-table opt-in, which already means "every row".
|
|
356
|
+
if (!allowFullTableScan)
|
|
357
|
+
assertMutationWhereIdentifiesOneRow(qi.tableMeta, qi.table, userWhere, 'update');
|
|
350
358
|
// The SQL is built from the global-filter-merged where (soft-delete keeps an
|
|
351
359
|
// update from touching already-deleted rows).
|
|
352
360
|
const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
|
|
@@ -479,7 +487,12 @@ export function buildDelete(qi, args) {
|
|
|
479
487
|
// Prisma compound-unique selector → the column conjunction (before the guard).
|
|
480
488
|
const userWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
|
|
481
489
|
// Guard the USER predicate (a global filter must not satisfy the guard).
|
|
482
|
-
|
|
490
|
+
const allowFullTableScan = resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan');
|
|
491
|
+
whereMod.assertMutationHasPredicate(qi, 'delete', whereMod.userPredicateIsEmpty(qi, userWhere) ? '' : ' WHERE x', allowFullTableScan);
|
|
492
|
+
// Same identity rule as `update` (see buildUpdate): one returned row means
|
|
493
|
+
// one addressed row. `deleteMany` is the many-row path.
|
|
494
|
+
if (!allowFullTableScan)
|
|
495
|
+
assertMutationWhereIdentifiesOneRow(qi.tableMeta, qi.table, userWhere, 'delete');
|
|
483
496
|
const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
|
|
484
497
|
const whereFp = whereMod.fingerprintWhere(qi, whereObj);
|
|
485
498
|
const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
|
|
@@ -533,6 +546,23 @@ export function buildUpsert(qi, args) {
|
|
|
533
546
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
534
547
|
// Prisma compound-unique selector on the conflict target → its member columns.
|
|
535
548
|
const upsertWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
|
|
549
|
+
// An upsert's `where` becomes the CONFLICT TARGET, so it carries the same
|
|
550
|
+
// one-row contract `update` / `delete` do and is refused by the same rule
|
|
551
|
+
// (query/compound-unique.ts), with its own sentence because the consequence
|
|
552
|
+
// differs. Without it a non-unique `where` reached the server as
|
|
553
|
+
// `ON CONFLICT ("role")` and came back as a bare SQLSTATE 42P10 on
|
|
554
|
+
// PostgreSQL, and WORSE than an error on the engines that do not read the
|
|
555
|
+
// conflict target back: MySQL's `ON DUPLICATE KEY UPDATE` ignores it and
|
|
556
|
+
// keys off whichever unique index the row happens to violate, and SQL
|
|
557
|
+
// Server's `MERGE ... ON` matches every row the predicate matches and
|
|
558
|
+
// updates all of them.
|
|
559
|
+
//
|
|
560
|
+
// Before any SQL is assembled, and this is the only where-guard `upsert`
|
|
561
|
+
// runs: `{}` and `{ email: undefined }` are refused here too, and by the
|
|
562
|
+
// right sentence. The empty-`where` guard's own message points at
|
|
563
|
+
// `allowFullTableScan: UNSAFE`, which is not an option on `UpsertArgs` at
|
|
564
|
+
// all, so borrowing it here would name a way out that does not exist.
|
|
565
|
+
assertMutationWhereIdentifiesOneRow(qi.tableMeta, qi.table, upsertWhere, 'upsert');
|
|
536
566
|
// Build the INSERT part from create data
|
|
537
567
|
const createEntries = writeEntries(qi, args.create);
|
|
538
568
|
const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
|
|
@@ -542,25 +572,47 @@ export function buildUpsert(qi, args) {
|
|
|
542
572
|
// The conflict target comes from `where` keys, must be unique/PK columns
|
|
543
573
|
const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
|
|
544
574
|
const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
|
|
545
|
-
//
|
|
575
|
+
// The conflict-UPDATE SET goes through the SAME operator-aware compiler as
|
|
576
|
+
// `update()` (buildSetClause), so `set` / `increment` / `decrement` /
|
|
577
|
+
// `multiply` / `divide` mean here what they mean there and a misspelled
|
|
578
|
+
// operator gets the same E003. This branch used to bind each value directly,
|
|
579
|
+
// which stored an operator OBJECT as the JSON text `{"set":"x"}` in a text
|
|
580
|
+
// column with no error at all. buildSetClause pushes onto `params` as it
|
|
581
|
+
// goes, so the numbering continues after the create params exactly as the
|
|
582
|
+
// hand-rolled `paramIdx` did. No SQL cache here (the statement is rebuilt per
|
|
583
|
+
// call), so the fingerprint / collect mirrors update() keeps are not needed.
|
|
546
584
|
const updateEntries = writeEntries(qi, args.update);
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
paramIdx++;
|
|
551
|
-
return clause;
|
|
552
|
-
});
|
|
553
|
-
const updateParams = updateEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
554
|
-
const params = [...createParams, ...updateParams];
|
|
585
|
+
const params = [...createParams];
|
|
586
|
+
const refQualifier = upsertReferenceQualifier(qi);
|
|
587
|
+
const setClauses = updateEntries.map(([k, v]) => buildSetClause(qi, k, v, params, refQualifier));
|
|
555
588
|
// Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
|
|
556
589
|
// upsert never resurrects a soft-deleted row or writes across tenants. Only
|
|
557
|
-
// on engines whose upsert can carry a predicate (Postgres); the gf
|
|
558
|
-
// continue the placeholder numbering after create+update params.
|
|
590
|
+
// on engines whose upsert can carry a predicate (Postgres, SQLite); the gf
|
|
591
|
+
// params continue the placeholder numbering after create+update params.
|
|
592
|
+
//
|
|
593
|
+
// Compiled against the TABLE QUALIFIER, not bare: in `ON CONFLICT ... DO
|
|
594
|
+
// UPDATE ... WHERE` both the target table and `excluded` are in scope, so an
|
|
595
|
+
// unqualified column is ambiguous and PostgreSQL rejected EVERY upsert on a
|
|
596
|
+
// globally filtered table at parse time (42702), insert path included.
|
|
559
597
|
let updateWhere;
|
|
560
598
|
if (qi.dialect.supportsUpsertUpdateWhere) {
|
|
561
599
|
const gf = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
562
|
-
if (gf)
|
|
563
|
-
|
|
600
|
+
if (gf) {
|
|
601
|
+
// Compiled through a scope whose FROM-item reference is ALREADY RENDERED
|
|
602
|
+
// (`"users"`), which is what the target table is inside `ON CONFLICT ...
|
|
603
|
+
// DO UPDATE`. `aliasWhereScope` is the wrong seam for that: it takes a
|
|
604
|
+
// BARE alias and quotes it itself for the nested-relation correlation
|
|
605
|
+
// parent, so handing it `q(table)` produced a triple-quoted reference
|
|
606
|
+
// inside an EXISTS body and the whole statement failed with 42P01
|
|
607
|
+
// whenever the configured global filter was a relation filter rather
|
|
608
|
+
// than a plain column. A bare table name is not the answer either: it
|
|
609
|
+
// would leave the qualifier unquoted and break every table whose name
|
|
610
|
+
// needs quoting. `buildRenderedRefWhere` is the rendered-reference seam,
|
|
611
|
+
// one parameter with one meaning, used for the qualifier and the
|
|
612
|
+
// correlation parent alike, and shared with the batched `_count`
|
|
613
|
+
// follow-up, which had the identical bug.
|
|
614
|
+
updateWhere = whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), gf, params) ?? undefined;
|
|
615
|
+
}
|
|
564
616
|
}
|
|
565
617
|
const sql = qi.dialect.buildUpsertStatement({
|
|
566
618
|
table: qi.q(qi.table),
|
|
@@ -577,11 +629,21 @@ export function buildUpsert(qi, args) {
|
|
|
577
629
|
transform: (result) => {
|
|
578
630
|
const row = result.rows[0];
|
|
579
631
|
if (!row) {
|
|
632
|
+
// With a conflict-UPDATE predicate in play there IS a way to get no
|
|
633
|
+
// row: the key conflicts with a row the global filter hides, the
|
|
634
|
+
// predicate is false for it, the engine skips the update, and the
|
|
635
|
+
// insert cannot happen either because the key is taken. Nothing was
|
|
636
|
+
// written and the hidden row is untouched; say so, rather than "this
|
|
637
|
+
// should never happen".
|
|
580
638
|
throw new NotFoundError({
|
|
581
639
|
table: qi.table,
|
|
582
640
|
where: args.where,
|
|
583
641
|
operation: 'upsert',
|
|
584
|
-
message:
|
|
642
|
+
message: updateWhere
|
|
643
|
+
? `upsert on "${qi.table}" wrote nothing: the row that conflicts on the given key is excluded by the ` +
|
|
644
|
+
`configured global filter for "${qi.table}", so it was neither updated (the filter hides it) nor ` +
|
|
645
|
+
'inserted (the key is taken). Pass `skipGlobalFilters: UNSAFE` to address it anyway.'
|
|
646
|
+
: `upsert on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
|
|
585
647
|
});
|
|
586
648
|
}
|
|
587
649
|
return parseWriteRow(qi, row);
|
|
@@ -597,6 +659,29 @@ export function buildUpsert(qi, args) {
|
|
|
597
659
|
: undefined,
|
|
598
660
|
};
|
|
599
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* The qualifier an upsert's arithmetic SET operator reads its column through.
|
|
664
|
+
*
|
|
665
|
+
* `INSERT ... ON CONFLICT DO UPDATE SET n = n + 1` is 42702 on PostgreSQL
|
|
666
|
+
* because `n` could be the target row's or `excluded`'s, so the reference is
|
|
667
|
+
* qualified with the table name on every engine whose upsert statement names
|
|
668
|
+
* the table directly (PostgreSQL, SQLite, MySQL all accept `"t"."n"` there).
|
|
669
|
+
* SQL Server is the exception and keeps the bare reference: its `MERGE`
|
|
670
|
+
* aliases the target as `T`, so the TABLE NAME does not resolve there at all,
|
|
671
|
+
* and the alias belongs to that dialect's own `buildUpsertStatement` rather
|
|
672
|
+
* than to this module. An arithmetic operator in an mssql upsert may therefore
|
|
673
|
+
* still be refused by the server as an ambiguous column; that shape has never
|
|
674
|
+
* worked (it previously bound the operator OBJECT as the value), and the sound
|
|
675
|
+
* fix is for the dialect to publish its target alias, which is a change to
|
|
676
|
+
* `dialect.ts`. It fails loudly either way, so no wrong answer is possible.
|
|
677
|
+
* `set` and literal values reference no column and are unaffected everywhere.
|
|
678
|
+
*
|
|
679
|
+
* Gated on the dialect NAME, never on hook presence: every engine dialect
|
|
680
|
+
* spreads `postgresDialect`, so a presence test would say yes for all of them.
|
|
681
|
+
*/
|
|
682
|
+
function upsertReferenceQualifier(qi) {
|
|
683
|
+
return qi.dialect.name === 'mssql' ? '' : `${qi.q(qi.table)}.`;
|
|
684
|
+
}
|
|
600
685
|
export function buildUpdateMany(qi, args) {
|
|
601
686
|
assertWritable(qi, 'updateMany');
|
|
602
687
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
@@ -859,9 +944,19 @@ export function assertNoGeneratedColumns(qi, data, operation) {
|
|
|
859
944
|
* Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
|
|
860
945
|
* pushes any required params onto the shared params array so that WHERE
|
|
861
946
|
* clause numbering continues correctly afterward.
|
|
947
|
+
*
|
|
948
|
+
* `refQualifier` prefixes the column REFERENCE an arithmetic operator reads
|
|
949
|
+
* from (`"view_count" = <qualifier>"view_count" + $n`). A plain `UPDATE` has
|
|
950
|
+
* one table in scope and passes nothing; `upsert` passes the table name,
|
|
951
|
+
* because inside `ON CONFLICT ... DO UPDATE SET` the target table and
|
|
952
|
+
* `excluded` are BOTH in scope and a bare reference is 42702 on PostgreSQL.
|
|
953
|
+
* The assignment target on the left stays bare on every engine (it can only
|
|
954
|
+
* ever be the target table's column), and `set` / literal values reference no
|
|
955
|
+
* column at all, so their SQL is byte-identical with or without a qualifier.
|
|
862
956
|
*/
|
|
863
|
-
export function buildSetClause(qi, key, value, params) {
|
|
957
|
+
export function buildSetClause(qi, key, value, params, refQualifier = '') {
|
|
864
958
|
const col = qi.toSqlColumn(key);
|
|
959
|
+
const ref = `${refQualifier}${col}`;
|
|
865
960
|
// Enum columns get an explicit `::"EnumName"` cast on their value bind
|
|
866
961
|
// (see enumTypeForColumn); `''` everywhere else. Value-invariant, so the
|
|
867
962
|
// SQL cache and collectSetParams are unaffected.
|
|
@@ -888,19 +983,19 @@ export function buildSetClause(qi, key, value, params) {
|
|
|
888
983
|
}
|
|
889
984
|
if (op === 'increment') {
|
|
890
985
|
params.push(opValue);
|
|
891
|
-
return `${col} = ${
|
|
986
|
+
return `${col} = ${ref} + ${qi.p(params.length)}`;
|
|
892
987
|
}
|
|
893
988
|
if (op === 'decrement') {
|
|
894
989
|
params.push(opValue);
|
|
895
|
-
return `${col} = ${
|
|
990
|
+
return `${col} = ${ref} - ${qi.p(params.length)}`;
|
|
896
991
|
}
|
|
897
992
|
if (op === 'multiply') {
|
|
898
993
|
params.push(opValue);
|
|
899
|
-
return `${col} = ${
|
|
994
|
+
return `${col} = ${ref} * ${qi.p(params.length)}`;
|
|
900
995
|
}
|
|
901
996
|
if (op === 'divide') {
|
|
902
997
|
params.push(opValue);
|
|
903
|
-
return `${col} = ${
|
|
998
|
+
return `${col} = ${ref} / ${qi.p(params.length)}`;
|
|
904
999
|
}
|
|
905
1000
|
}
|
|
906
1001
|
// Fall through: multi-key objects or non-operator single-key objects
|
package/dist/seed.d.ts
CHANGED
|
@@ -12,4 +12,20 @@ export type DefinedSeed = () => Promise<void>;
|
|
|
12
12
|
* Exported for unit testing the frame parser in isolation.
|
|
13
13
|
*/
|
|
14
14
|
export declare function parseStackFramePath(line: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Declare a seed: `fn` receives a connected `TurbineClient` and is awaited.
|
|
17
|
+
*
|
|
18
|
+
* The returned function runs the seed on demand. When the module that calls
|
|
19
|
+
* `defineSeed` is itself the process entry point (`npx tsx seed.ts`, or the
|
|
20
|
+
* child `turbine seed` spawns), the seed also runs automatically.
|
|
21
|
+
*
|
|
22
|
+
* The client connects through `DATABASE_URL`. Under `turbine seed` that value
|
|
23
|
+
* is the configured connection string with `search_path` PINNED to the
|
|
24
|
+
* configured `schema` (as the `options=-c search_path` connection parameter,
|
|
25
|
+
* so it can never leak onto a pooled backend the way a `SET` would). Every
|
|
26
|
+
* unqualified table name the seed writes therefore lands in the same schema
|
|
27
|
+
* `push`, `generate` and the generated client use; a seed that must reach
|
|
28
|
+
* another schema qualifies the name. The default `public` adds nothing, so a
|
|
29
|
+
* project that never set `schema` connects exactly as before.
|
|
30
|
+
*/
|
|
15
31
|
export declare function defineSeed(fn: SeedFunction): DefinedSeed;
|
package/dist/seed.js
CHANGED
|
@@ -139,6 +139,22 @@ async function runSeed(fn) {
|
|
|
139
139
|
await db.disconnect();
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Declare a seed: `fn` receives a connected `TurbineClient` and is awaited.
|
|
144
|
+
*
|
|
145
|
+
* The returned function runs the seed on demand. When the module that calls
|
|
146
|
+
* `defineSeed` is itself the process entry point (`npx tsx seed.ts`, or the
|
|
147
|
+
* child `turbine seed` spawns), the seed also runs automatically.
|
|
148
|
+
*
|
|
149
|
+
* The client connects through `DATABASE_URL`. Under `turbine seed` that value
|
|
150
|
+
* is the configured connection string with `search_path` PINNED to the
|
|
151
|
+
* configured `schema` (as the `options=-c search_path` connection parameter,
|
|
152
|
+
* so it can never leak onto a pooled backend the way a `SET` would). Every
|
|
153
|
+
* unqualified table name the seed writes therefore lands in the same schema
|
|
154
|
+
* `push`, `generate` and the generated client use; a seed that must reach
|
|
155
|
+
* another schema qualifies the name. The default `public` adds nothing, so a
|
|
156
|
+
* project that never set `schema` connects exactly as before.
|
|
157
|
+
*/
|
|
142
158
|
export function defineSeed(fn) {
|
|
143
159
|
const run = () => runSeed(fn);
|
|
144
160
|
if (isDirectSeedModule()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"//coverage:cli": "The CLI coverage gate, split into ONE collection run plus a per-file threshold check for every file in it, and an aggregate. c8 enforces a single threshold set per invocation and its --per-file applies the SAME numbers to every file, neither of which can express 'destructive.ts holds 100 while migrate.ts holds 70'. An aggregate-only floor lets the least-covered file spend the whole slack the best-covered file earned: at the measured 3623/2951 lines, migrate.ts could fall from 70.3% to 66.6% with the aggregate still green. So each file gets its OWN floor, checked by re-reporting the coverage already on disk (c8 report re-reads ./coverage/tmp, so this costs no extra test run). The aggregate check is kept as well: it catches all three sagging together inside their individual margins. Per-file gates run FIRST because their failure names the file. DATABASE_URL is neutralized on the collection run: these test files include live migration tests that create and drop tables, and this script runs from prepublishOnly.",
|
|
133
133
|
"test:coverage:cli": "npm run check:coverage-sync && npm run coverage:cli:collect && npm run coverage:cli:gate:destructive && npm run coverage:cli:gate:sql-statements && npm run coverage:cli:gate:pii-guard && npm run coverage:cli:gate:error-catalog && npm run coverage:cli:gate:mcp && npm run coverage:cli:gate:compile-query && npm run coverage:cli:gate:studio && npm run coverage:cli:gate:migrate && npm run coverage:cli:gate:index && npm run coverage:cli:gate:aggregate",
|
|
134
134
|
"//coverage:cli:collect": "--check-coverage=false is load-bearing, not tidying. c8 reads .c8rc.json for defaults, so this COLLECTION step was silently enforcing the MAIN gate's global thresholds against a src/cli-only file set. That is a gate nobody wrote and nobody wanted, and it went unnoticed only because the main floor was low enough (75) for the CLI aggregate (89.29) to clear it by accident. When the main floors were re-baselined to 93/89/78 (see //merge-bug in .c8rc.json) it started failing here, several steps before the per-file gates that are supposed to decide. Thresholds for these files belong to the coverage:cli:gate:* scripts, which pass their own explicitly.",
|
|
135
|
-
"coverage:cli:collect": "DATABASE_URL= c8 --all --check-coverage=false --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts --include src/cli/mcp.ts --include src/cli/compile-query.ts --include src/cli/error-catalog.ts --include src/cli/index.ts tsx --test src/test/studio-write.test.ts src/test/pii-guard-symmetry.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/tracking-table-race.test.ts src/test/destructive-dynamic-assembly.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts src/test/mcp.test.ts src/test/mcp-relations.test.ts src/test/mcp-pii.test.ts src/test/mcp-pii-round2.test.ts src/test/mcp-agent-tools.test.ts src/test/mcp-compile-query.test.ts src/test/mcp-perimeter-bounds.test.ts",
|
|
135
|
+
"coverage:cli:collect": "DATABASE_URL= c8 --all --check-coverage=false --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts --include src/cli/mcp.ts --include src/cli/compile-query.ts --include src/cli/error-catalog.ts --include src/cli/index.ts tsx --test src/test/studio-write.test.ts src/test/pii-guard-symmetry.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/tracking-table-race.test.ts src/test/destructive-dynamic-assembly.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts src/test/mcp.test.ts src/test/mcp-relations.test.ts src/test/mcp-pii.test.ts src/test/mcp-pii-round2.test.ts src/test/mcp-agent-tools.test.ts src/test/mcp-compile-query.test.ts src/test/mcp-perimeter-bounds.test.ts src/test/destructive-obfuscation.test.ts src/test/cli-init-schema.test.ts src/test/cli-introspect-options.test.ts src/test/mcp-transport-bounds.test.ts src/test/seed-schema.test.ts src/test/migrate-preview-and-report.test.ts src/test/destructive-quoting-forms.test.ts",
|
|
136
136
|
"coverage:cli:gate": "c8 report --all --exclude 'src/test/**' --reporter text --check-coverage",
|
|
137
137
|
"coverage:cli:gate:destructive": "npm run coverage:cli:gate -- --include src/cli/destructive.ts --lines 98 --statements 98 --branches 84 --functions 98",
|
|
138
138
|
"coverage:cli:gate:sql-statements": "npm run coverage:cli:gate -- --include src/cli/sql-statements.ts --lines 100 --statements 100 --branches 98 --functions 100",
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
},
|
|
255
255
|
"lint-staged": {
|
|
256
256
|
"*.ts": [
|
|
257
|
-
"biome check --no-errors-on-unmatched"
|
|
257
|
+
"biome check --error-on-warnings --no-errors-on-unmatched"
|
|
258
258
|
]
|
|
259
259
|
}
|
|
260
260
|
}
|
|
@@ -171,15 +171,29 @@ where: { tastingNotes: { path: ['panel', 'score'], gte: 9 } }
|
|
|
171
171
|
where: { credentialBlob: { path: ['tier'], equals: 'master' } }
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
A path takes a
|
|
175
|
-
`lt`, `lte`, `
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
A path takes a DIFFERENT operator set from a plain column: `equals`, `gt`,
|
|
175
|
+
`gte`, `lt`, `lte`, `hasKey`, `contains`, `stringContains`, `stringStartsWith`,
|
|
176
|
+
`stringEndsWith`, plus `mode` (which applies to the three `string*` operators).
|
|
177
|
+
`contains` on a JSON column is containment (`@>`, a whole sub-document such as
|
|
178
|
+
`{ contains: { panel: { seats: 4 } } }`), NOT a substring test; the substring
|
|
179
|
+
operators on a path are `stringContains`, `stringStartsWith` and
|
|
180
|
+
`stringEndsWith`. `not`, `in`, `notIn`, `startsWith` and `endsWith` are refused
|
|
181
|
+
with `TURBINE_E003` listing the accepted set (the last two point at their
|
|
182
|
+
`string*` spelling); express `not` / `in` with `NOT` / `OR` around the path
|
|
183
|
+
filter instead.
|
|
184
|
+
|
|
185
|
+
`groupBy` accepts a JSON path as a grouping key via `{ field, path }`, and its
|
|
186
|
+
`_sum`, `_avg`, `_min` and `_max` accept the same `{ field, path }` object under
|
|
187
|
+
an alias key: `_sum: { score: { field: 'tastingNotes', path: ['panel', 'score'] } }`
|
|
188
|
+
comes back as `_sum.score`. `_sum` / `_avg` cast the value to numeric; `_min` /
|
|
189
|
+
`_max` compare as text unless `type: 'numeric'`. **`aggregate()` has no JSON-path
|
|
190
|
+
form**: every key there must be a column (anything else is `TURBINE_E003`) and
|
|
191
|
+
every value `true`; an object value is read as `true`, so the whole column
|
|
192
|
+
reaches the database as `avg(jsonb)` and fails there with a database error, not
|
|
193
|
+
a Turbine code. **`_count` never takes a path**, in `groupBy` or `aggregate()`:
|
|
194
|
+
an object under a `_count` key is read as `true` (a count of that column's
|
|
195
|
+
non-null values) and any path in it is ignored. To count per JSON value, group
|
|
196
|
+
by the path and read `_count`.
|
|
183
197
|
|
|
184
198
|
## Aggregates
|
|
185
199
|
|
|
@@ -192,6 +206,11 @@ await db.ripeningChecks.aggregate({
|
|
|
192
206
|
});
|
|
193
207
|
```
|
|
194
208
|
|
|
209
|
+
`_sum` / `_avg` over an `int8` / `bigint` or `numeric` / `decimal` column return
|
|
210
|
+
PostgreSQL's exact text as a **string** (every value, not only large ones; `AVG`
|
|
211
|
+
is not float-cast there); over `int4` / `float` columns and over a JSON path they
|
|
212
|
+
return a number. `_min` / `_max` return the column's own type.
|
|
213
|
+
|
|
195
214
|
## groupBy and having
|
|
196
215
|
|
|
197
216
|
`by` lists the grouping columns. The `having` shape is **column first, aggregate
|
|
@@ -219,13 +238,21 @@ where: { guildId_batchRef: { guildId: 4, batchRef: 'WB-0007' } }
|
|
|
219
238
|
where: { guildId: 4, batchRef: 'WB-0007' } // equivalent
|
|
220
239
|
```
|
|
221
240
|
|
|
241
|
+
## Single-row writes carry the same rule
|
|
242
|
+
|
|
243
|
+
`update`, `delete` and `upsert` each require a `where` that identifies one row,
|
|
244
|
+
by the same rule as `findUnique`, and anything else is `TURBINE_E003` naming the
|
|
245
|
+
keys that would work. For an upsert the `where` **is** the `ON CONFLICT` target,
|
|
246
|
+
so a non-unique predicate is refused rather than sent. Use `updateMany` and
|
|
247
|
+
`deleteMany` for "every row matching a filter".
|
|
248
|
+
|
|
222
249
|
## `distinct`
|
|
223
250
|
|
|
224
251
|
`distinct: ['status']` de-duplicates on those columns.
|
|
225
252
|
|
|
226
253
|
## Errors worth branching on
|
|
227
254
|
|
|
228
|
-
Every error extends `TurbineError` and carries a stable `code`. The ones a query
|
|
255
|
+
Every error Turbine raises extends `TurbineError` and carries a stable `code`; an unmapped driver SQLSTATE stays the driver's error, with that SQLSTATE on `.code`. The ones a query
|
|
229
256
|
produces:
|
|
230
257
|
|
|
231
258
|
| code | class | means |
|