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/generate.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
13
13
|
import { ValidationError } from './errors.js';
|
|
14
|
+
import { assertDistinctColumnFields } from './introspect.js';
|
|
14
15
|
import { pgTypeToTs, singularize, snakeToPascal, timeOfDayKind, withDbFieldNames, } from './schema.js';
|
|
15
16
|
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
16
17
|
function entityName(tableName) {
|
|
@@ -388,6 +389,11 @@ export function assertEmittableSchema(schema) {
|
|
|
388
389
|
for (const table of Object.values(schema.tables)) {
|
|
389
390
|
requireEmittable(entityName(table.name), `table "${table.name}"`, 'the generated entity type name');
|
|
390
391
|
requireEmittable(snakeToCamelStr(table.name), `table "${table.name}"`, 'the generated client accessor');
|
|
392
|
+
// Two columns on one field would emit a duplicate interface member (TS2300
|
|
393
|
+
// in the consumer's build) and a `columnMap` missing a column. The rule is
|
|
394
|
+
// introspect.ts's; re-asserting it here covers a hand-built or
|
|
395
|
+
// engine-introspected schema that never went through that path.
|
|
396
|
+
assertDistinctColumnFields(table.name, table.columns);
|
|
391
397
|
// Only the relations that reach the TYPE layer: a relation shadowing a
|
|
392
398
|
// column field is already dropped from types.ts, so refusing on its name
|
|
393
399
|
// would refuse a schema that generates fine.
|
|
@@ -458,18 +464,24 @@ export function generateTypes(schema, options) {
|
|
|
458
464
|
lines.push('}');
|
|
459
465
|
lines.push('');
|
|
460
466
|
// --- Create input type ---
|
|
461
|
-
//
|
|
462
|
-
//
|
|
467
|
+
// Optional: server-generated columns (serial / identity), columns with a
|
|
468
|
+
// default, and nullable columns (default to NULL). Everything else is
|
|
469
|
+
// required, PRIMARY KEY MEMBERSHIP INCLUDED. A text natural key, a
|
|
470
|
+
// client-supplied uuid with no default, or a composite key of plain
|
|
471
|
+
// integers must be supplied, and the database rejects the row otherwise
|
|
472
|
+
// (E010). Marking every PK column optional let `create({ data: {} })` on
|
|
473
|
+
// a junction table typecheck and fail at runtime, which is the opposite of
|
|
474
|
+
// what a generated type is for.
|
|
463
475
|
lines.push(`/** Input type for creating a row in \`${docSafe(table.name)}\` */`);
|
|
464
476
|
lines.push(`export type ${typeName}Create = {`);
|
|
465
477
|
for (const col of table.columns) {
|
|
466
478
|
// STORED generated columns are computed by the database, never writable.
|
|
467
479
|
if (col.isGeneratedStored)
|
|
468
480
|
continue;
|
|
469
|
-
const
|
|
470
|
-
const isOptional = col.hasDefault || col.nullable
|
|
481
|
+
const isGenerated = col.isGenerated === true;
|
|
482
|
+
const isOptional = isGenerated || col.hasDefault || col.nullable;
|
|
471
483
|
if (isOptional) {
|
|
472
|
-
const reason =
|
|
484
|
+
const reason = isGenerated ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
473
485
|
lines.push(` /** Optional: ${reason} */`);
|
|
474
486
|
lines.push(` ${quoteIfNeeded(col.field)}?: ${writeColumnTsType(col, schema.enums)};`);
|
|
475
487
|
}
|
|
@@ -756,18 +768,18 @@ export function generateZod(schema, options) {
|
|
|
756
768
|
}
|
|
757
769
|
lines.push('});');
|
|
758
770
|
lines.push('');
|
|
759
|
-
// Create schema, STORED generated columns can never be written;
|
|
760
|
-
// defaulted, and nullable columns are optional
|
|
771
|
+
// Create schema, STORED generated columns can never be written;
|
|
772
|
+
// server-generated, defaulted, and nullable columns are optional, the same
|
|
773
|
+
// rule as the `*Create` type (a PK with none of those is required).
|
|
761
774
|
lines.push(`/** Zod schema for creating a \`${docSafe(table.name)}\` row */`);
|
|
762
775
|
lines.push(`export const ${typeName}CreateSchema = z.object({`);
|
|
763
776
|
for (const col of table.columns) {
|
|
764
777
|
if (col.isGeneratedStored)
|
|
765
778
|
continue;
|
|
766
|
-
const isPk = table.primaryKey.includes(col.name);
|
|
767
779
|
let expr = zodBaseType(col, schema.enums, true);
|
|
768
780
|
if (col.nullable)
|
|
769
781
|
expr += '.nullable()';
|
|
770
|
-
if (col.
|
|
782
|
+
if (col.isGenerated === true || col.hasDefault || col.nullable)
|
|
771
783
|
expr += '.optional()';
|
|
772
784
|
lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
|
|
773
785
|
}
|
|
@@ -796,6 +808,11 @@ export function generateZod(schema, options) {
|
|
|
796
808
|
// metadata.ts generator
|
|
797
809
|
// ---------------------------------------------------------------------------
|
|
798
810
|
export function generateMetadata(schema, options) {
|
|
811
|
+
// metadata.ts is where a field collision does its runtime damage: the
|
|
812
|
+
// emitted `columnMap` keeps whichever column was written last, so refuse it
|
|
813
|
+
// here as well as in the type emitters (see assertEmittableSchema).
|
|
814
|
+
for (const table of Object.values(schema.tables))
|
|
815
|
+
assertDistinctColumnFields(table.name, table.columns);
|
|
799
816
|
const lines = [
|
|
800
817
|
...generatedFileHeader(options),
|
|
801
818
|
"import type { SchemaMetadata } from 'turbine-orm';",
|
|
@@ -1011,15 +1028,21 @@ export function generateIndex(schema, options) {
|
|
|
1011
1028
|
lines.push(' }');
|
|
1012
1029
|
lines.push('}');
|
|
1013
1030
|
lines.push('');
|
|
1014
|
-
// Augment TurbineClient via interface merging with
|
|
1015
|
-
//
|
|
1016
|
-
// so users get autocomplete on `tx.users`,
|
|
1031
|
+
// Augment TurbineClient via interface merging with typed $transaction and
|
|
1032
|
+
// $withSession overloads. The callback parameter is narrowed to
|
|
1033
|
+
// `TypedTransactionClient` so users get autocomplete on `tx.users`,
|
|
1034
|
+
// `tx.posts`, etc. $withSession is the RLS shorthand for
|
|
1035
|
+
// `$transaction(fn, { sessionContext })`; it used to be left out, so its
|
|
1036
|
+
// callback stayed the untyped base `TransactionClient` and the documented
|
|
1037
|
+
// `tx.<table>.findMany()` did not compile on a generated client.
|
|
1017
1038
|
//
|
|
1018
|
-
// IMPORTANT:
|
|
1019
|
-
//
|
|
1039
|
+
// IMPORTANT: each merged member must be compatible with the base class's
|
|
1040
|
+
// member ON ITS OWN (TS2415), since v0.26 the base $transaction also has a
|
|
1020
1041
|
// batch-array overload (`$transaction([...queries])`), so the merged
|
|
1021
1042
|
// interface must redeclare BOTH signatures. Emitting only the callback form
|
|
1022
|
-
// makes every generated client fail `tsc` with "incorrectly extends".
|
|
1043
|
+
// makes every generated client fail `tsc` with "incorrectly extends". The
|
|
1044
|
+
// $withSession overload mirrors the base parameter list exactly for the same
|
|
1045
|
+
// reason; src/test/generate-typecheck.test.ts compiles the result.
|
|
1023
1046
|
lines.push('export interface TurbineClient {');
|
|
1024
1047
|
lines.push(' /**');
|
|
1025
1048
|
lines.push(' * Run a callback inside a transaction. The callback receives a typed');
|
|
@@ -1036,6 +1059,15 @@ export function generateIndex(schema, options) {
|
|
|
1036
1059
|
lines.push(' $transaction<T extends readonly DeferredQuery<unknown>[]>(');
|
|
1037
1060
|
lines.push(' queries: readonly [...T],');
|
|
1038
1061
|
lines.push(' ): Promise<PipelineResults<T>>;');
|
|
1062
|
+
lines.push(' /**');
|
|
1063
|
+
lines.push(' * Run a callback inside a transaction with the given session GUCs applied');
|
|
1064
|
+
lines.push(' * via `set_config(..., true)` (the RLS / multi-tenant shorthand). The');
|
|
1065
|
+
lines.push(' * callback receives a typed `TypedTransactionClient`, same as `$transaction`.');
|
|
1066
|
+
lines.push(' */');
|
|
1067
|
+
lines.push(' $withSession<R>(');
|
|
1068
|
+
lines.push(' context: Record<string, string | number | boolean>,');
|
|
1069
|
+
lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
|
|
1070
|
+
lines.push(' ): Promise<R>;');
|
|
1039
1071
|
lines.push('}');
|
|
1040
1072
|
lines.push('');
|
|
1041
1073
|
// Factory function with JSDoc
|
package/dist/index.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type Neste
|
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
46
|
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
47
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
47
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderBySpec, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationOrderBy, type RelationOrderByChain, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
48
48
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
49
49
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
50
50
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
package/dist/introspect.d.ts
CHANGED
|
@@ -91,6 +91,18 @@ export interface IntrospectOptions {
|
|
|
91
91
|
* {@link detectUniqueForeignKeySets}.
|
|
92
92
|
*/
|
|
93
93
|
legacyToManyUniques?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Use the raw database column name as each column's TypeScript field
|
|
96
|
+
* (`user_id` stays `user_id`) instead of the camelCase default (`userId`).
|
|
97
|
+
* The same identity mapping `withDbFieldNames` applies at generate time, so
|
|
98
|
+
* a schema introspected with this flag is byte-identical after that
|
|
99
|
+
* transform; declaring it here as well is what makes it the escape from the
|
|
100
|
+
* field-collision refusal (see {@link assertDistinctColumnFields}): a table
|
|
101
|
+
* carrying both `"createdAt"` and `created_at` has two distinct raw names and
|
|
102
|
+
* one shared camelCase name, and only the flag can tell introspection which
|
|
103
|
+
* of those two facts to build the field from.
|
|
104
|
+
*/
|
|
105
|
+
keepColumnNames?: boolean;
|
|
94
106
|
/**
|
|
95
107
|
* Called with any {@link DEFAULT_EXCLUDED_TABLES} that were present in the
|
|
96
108
|
* database but dropped from this run (F12), so the CLI can print a
|
|
@@ -145,6 +157,27 @@ export declare function introspect(options: IntrospectOptions): Promise<SchemaMe
|
|
|
145
157
|
* means the call sites it was supposed to fix break at runtime instead.
|
|
146
158
|
*/
|
|
147
159
|
export declare function applyRelationRenames(schema: SchemaMetadata, renames: Record<string, Record<string, string>>): SchemaMetadata;
|
|
160
|
+
/**
|
|
161
|
+
* THE field-collision rule: two columns of one table may never resolve to the
|
|
162
|
+
* same TypeScript field.
|
|
163
|
+
*
|
|
164
|
+
* Fields are derived with `snakeToCamel`, so a table carrying both a quoted
|
|
165
|
+
* `"createdAt"` and a `created_at` column (a Prisma-era column beside a
|
|
166
|
+
* hand-written one is the common way to get there) yields two `createdAt`
|
|
167
|
+
* fields. Nothing used to refuse that, and every consumer keyed by field then
|
|
168
|
+
* lost a column silently: `columnMap` kept whichever column was written last,
|
|
169
|
+
* reads folded both columns into one property, a write to the field reached
|
|
170
|
+
* only one of them, and `types.ts` carried a duplicate member that failed the
|
|
171
|
+
* consumer's `tsc` (TS2300) while `turbine generate` exited 0.
|
|
172
|
+
*
|
|
173
|
+
* A thrown error names the table, every colliding column and the fix. There is
|
|
174
|
+
* no per-column rename option, so the fix is `keepColumnNames` (the field is
|
|
175
|
+
* then the raw column name, and two distinct columns cannot collide) or a
|
|
176
|
+
* rename in the database. Applied once per table right after the catalog's
|
|
177
|
+
* columns are grouped, and again by the generate.ts emitters so a schema that
|
|
178
|
+
* never went through introspection is refused at the same boundary.
|
|
179
|
+
*/
|
|
180
|
+
export declare function assertDistinctColumnFields(tableName: string, columns: readonly ColumnMetadata[]): void;
|
|
148
181
|
/**
|
|
149
182
|
* PostgreSQL catalog introspector: reads information_schema + pg_catalog and
|
|
150
183
|
* produces {@link SchemaMetadata}. This is the implementation wrapped by
|
package/dist/introspect.js
CHANGED
|
@@ -461,6 +461,50 @@ export function applyRelationRenames(schema, renames) {
|
|
|
461
461
|
}
|
|
462
462
|
return { ...schema, tables };
|
|
463
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* THE field-collision rule: two columns of one table may never resolve to the
|
|
466
|
+
* same TypeScript field.
|
|
467
|
+
*
|
|
468
|
+
* Fields are derived with `snakeToCamel`, so a table carrying both a quoted
|
|
469
|
+
* `"createdAt"` and a `created_at` column (a Prisma-era column beside a
|
|
470
|
+
* hand-written one is the common way to get there) yields two `createdAt`
|
|
471
|
+
* fields. Nothing used to refuse that, and every consumer keyed by field then
|
|
472
|
+
* lost a column silently: `columnMap` kept whichever column was written last,
|
|
473
|
+
* reads folded both columns into one property, a write to the field reached
|
|
474
|
+
* only one of them, and `types.ts` carried a duplicate member that failed the
|
|
475
|
+
* consumer's `tsc` (TS2300) while `turbine generate` exited 0.
|
|
476
|
+
*
|
|
477
|
+
* A thrown error names the table, every colliding column and the fix. There is
|
|
478
|
+
* no per-column rename option, so the fix is `keepColumnNames` (the field is
|
|
479
|
+
* then the raw column name, and two distinct columns cannot collide) or a
|
|
480
|
+
* rename in the database. Applied once per table right after the catalog's
|
|
481
|
+
* columns are grouped, and again by the generate.ts emitters so a schema that
|
|
482
|
+
* never went through introspection is refused at the same boundary.
|
|
483
|
+
*/
|
|
484
|
+
export function assertDistinctColumnFields(tableName, columns) {
|
|
485
|
+
const columnsByField = new Map();
|
|
486
|
+
for (const col of columns) {
|
|
487
|
+
const names = columnsByField.get(col.field);
|
|
488
|
+
if (names)
|
|
489
|
+
names.push(col.name);
|
|
490
|
+
else
|
|
491
|
+
columnsByField.set(col.field, [col.name]);
|
|
492
|
+
}
|
|
493
|
+
const collisions = [];
|
|
494
|
+
for (const [field, names] of columnsByField) {
|
|
495
|
+
if (names.length < 2)
|
|
496
|
+
continue;
|
|
497
|
+
const quoted = names.map((n) => `"${n}"`);
|
|
498
|
+
const list = `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`;
|
|
499
|
+
collisions.push(`columns ${list} ${names.length === 2 ? 'both' : 'all'} resolve to the field "${field}"`);
|
|
500
|
+
}
|
|
501
|
+
if (collisions.length === 0)
|
|
502
|
+
return;
|
|
503
|
+
throw new ValidationError(`Field collision on table "${tableName}": ${collisions.join('; ')}. A client addresses one column per field, ` +
|
|
504
|
+
'so reads would fold the colliding columns into one property and a write to that field would reach only ' +
|
|
505
|
+
'one of them. Rename one of the columns, or set `keepColumnNames: true` in turbine.config.ts ' +
|
|
506
|
+
'(`turbine generate --keep-column-names`) so every field is its raw column name.');
|
|
507
|
+
}
|
|
464
508
|
/**
|
|
465
509
|
* PostgreSQL catalog introspector: reads information_schema + pg_catalog and
|
|
466
510
|
* produces {@link SchemaMetadata}. This is the implementation wrapped by
|
|
@@ -532,7 +576,9 @@ export async function introspectPostgresCatalog(options) {
|
|
|
532
576
|
const arrayType = dialect.arrayType?.(baseType) ?? 'text[]';
|
|
533
577
|
const col = {
|
|
534
578
|
name: row.column_name,
|
|
535
|
-
field
|
|
579
|
+
// Under keepColumnNames the field IS the column name, which is why two
|
|
580
|
+
// distinct columns can never collide there (see assertDistinctColumnFields).
|
|
581
|
+
field: options.keepColumnNames ? row.column_name : snakeToCamel(row.column_name),
|
|
536
582
|
dialectType,
|
|
537
583
|
pgType: dialectType,
|
|
538
584
|
tsType: dialect.typeToTypeScript?.(isArray ? dialectType : baseType, isNullable) ??
|
|
@@ -566,6 +612,12 @@ export async function introspectPostgresCatalog(options) {
|
|
|
566
612
|
columnsByTable.set(tableName, []);
|
|
567
613
|
columnsByTable.get(tableName).push(col);
|
|
568
614
|
}
|
|
615
|
+
// Every consumer below keys the table by FIELD (columnMap, the relation
|
|
616
|
+
// derivation's shadow check, the generated interface), so the field set
|
|
617
|
+
// must be sound before any of them runs. THE one place the rule is applied
|
|
618
|
+
// to a live catalog; generate.ts re-asserts it for schemas built elsewhere.
|
|
619
|
+
for (const [tableName, cols] of columnsByTable)
|
|
620
|
+
assertDistinctColumnFields(tableName, cols);
|
|
569
621
|
// ----- Group primary keys by table -----
|
|
570
622
|
const pkByTable = new Map();
|
|
571
623
|
for (const row of pkResult.rows) {
|
package/dist/mssql.js
CHANGED
|
@@ -1047,6 +1047,26 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
1047
1047
|
`FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')`);
|
|
1048
1048
|
}
|
|
1049
1049
|
const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
1050
|
+
/**
|
|
1051
|
+
* A `bit` column, as the driver actually delivers it.
|
|
1052
|
+
*
|
|
1053
|
+
* tedious parses TDS `BITTYPE` with `!!value`, so `sys.indexes.is_unique`
|
|
1054
|
+
* arrives as a JavaScript BOOLEAN, never as 1. Reading it through {@link num}
|
|
1055
|
+
* and comparing to 1 therefore answered `false` for EVERY unique index on SQL
|
|
1056
|
+
* Server, which emptied `uniqueColumns` on every introspected table and set
|
|
1057
|
+
* `IndexMetadata.unique` to false on every unique index. The visible cost was a
|
|
1058
|
+
* `findUnique` (and, once it carried the same rule, an `upsert`) on a genuinely
|
|
1059
|
+
* unique non-PK column being refused as not identifying one row, on the one
|
|
1060
|
+
* engine where the metadata could not say otherwise.
|
|
1061
|
+
*
|
|
1062
|
+
* The unit test could not see it: its mock built index rows with `1` and `0`
|
|
1063
|
+
* because the helper's parameter was typed `number`, so the fixture chose the
|
|
1064
|
+
* shape the code already handled. Only the live SQL Server leg produces the
|
|
1065
|
+
* boolean. Written to accept all three spellings rather than the one this
|
|
1066
|
+
* driver happens to send, because another `mssql` transport is free to send a
|
|
1067
|
+
* number or the string `'1'` and this is metadata, not a hot path.
|
|
1068
|
+
*/
|
|
1069
|
+
const bitIsTrue = (v) => v === true || v === 1 || v === '1' || v === 'true';
|
|
1050
1070
|
/**
|
|
1051
1071
|
* Derive relations from the FK list via the SHARED introspection pipeline
|
|
1052
1072
|
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
@@ -1182,7 +1202,7 @@ export async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
|
1182
1202
|
const key = `${t}.${name}`;
|
|
1183
1203
|
let g = indexGroups.get(key);
|
|
1184
1204
|
if (!g) {
|
|
1185
|
-
g = { table: t, name, unique:
|
|
1205
|
+
g = { table: t, name, unique: bitIsTrue(r.IS_UNIQUE), columns: [] };
|
|
1186
1206
|
indexGroups.set(key, g);
|
|
1187
1207
|
}
|
|
1188
1208
|
g.columns.push(String(r.COLUMN_NAME));
|
package/dist/nested-write.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `NestedWriteContext`.
|
|
13
13
|
*/
|
|
14
14
|
import { CircularRelationError, describeTargetForMessage, NotFoundError, RelationError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
15
|
+
import { markInternalRowSelector } from './query/compound-unique.js';
|
|
15
16
|
import { markInternalCombinator, resolveColumnName } from './query/utils.js';
|
|
16
17
|
import { normalizeKeyColumns } from './schema.js';
|
|
17
18
|
const MAX_DEPTH = 10;
|
|
@@ -222,6 +223,65 @@ function validateOps(relationName, ops, isUpdate) {
|
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* The `with` clause that reads back the tree a nested write just wrote.
|
|
228
|
+
*
|
|
229
|
+
* Built from the DATA, recursively: a relation whose payload itself contains
|
|
230
|
+
* relation writes is requested as `{ rel: { with: { ... } } }` rather than
|
|
231
|
+
* `{ rel: true }`. It used to be the top-level keys alone, so a depth-3 create
|
|
232
|
+
* wrote all three levels and returned an object whose grandchildren were
|
|
233
|
+
* missing entirely, which a caller reads as "there are none".
|
|
234
|
+
*
|
|
235
|
+
* Only relations that were actually WRITTEN are requested, so a create with no
|
|
236
|
+
* nested data still reads back exactly what it did before. The walk is capped
|
|
237
|
+
* at {@link MAX_DEPTH}, the cap the write walk itself uses, and the builder's
|
|
238
|
+
* own relation-depth guard is the backstop below that.
|
|
239
|
+
*/
|
|
240
|
+
function readBackWith(schema, tableName, relations, depth = 0) {
|
|
241
|
+
const tableMeta = schema.tables[tableName];
|
|
242
|
+
if (!tableMeta || depth >= MAX_DEPTH)
|
|
243
|
+
return undefined;
|
|
244
|
+
const clause = {};
|
|
245
|
+
for (const [relName, ops] of Object.entries(relations)) {
|
|
246
|
+
const rel = tableMeta.relations[relName];
|
|
247
|
+
if (!rel)
|
|
248
|
+
continue;
|
|
249
|
+
// Every payload this relation was written with: `create` / `update` /
|
|
250
|
+
// `upsert` take objects (or arrays of them) whose own keys may name
|
|
251
|
+
// relations of the TARGET table. `connect` / `disconnect` / `set` name
|
|
252
|
+
// existing rows and write no nested data, so they contribute nothing here.
|
|
253
|
+
const nested = {};
|
|
254
|
+
for (const key of ['create', 'update', 'upsert']) {
|
|
255
|
+
const payload = ops[key];
|
|
256
|
+
if (payload === undefined)
|
|
257
|
+
continue;
|
|
258
|
+
for (const item of toArray(payload)) {
|
|
259
|
+
// `{ where, data }` (nested update) and `{ where, create, update }`
|
|
260
|
+
// (nested upsert) carry their written fields one level in.
|
|
261
|
+
const bodies = isPlainRecord(item) && ('data' in item || 'create' in item || 'update' in item)
|
|
262
|
+
? [item.data, item.create, item.update]
|
|
263
|
+
: [item];
|
|
264
|
+
for (const body of bodies) {
|
|
265
|
+
if (!isPlainRecord(body))
|
|
266
|
+
continue;
|
|
267
|
+
const child = schema.tables[rel.to];
|
|
268
|
+
if (!child)
|
|
269
|
+
continue;
|
|
270
|
+
for (const [k, v] of Object.entries(extractRelationFields(body, child).relations)) {
|
|
271
|
+
nested[k] = { ...(nested[k] ?? {}), ...v };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const deeper = Object.keys(nested).length > 0 ? readBackWith(schema, rel.to, nested, depth + 1) : undefined;
|
|
277
|
+
clause[relName] = deeper ? { with: deeper } : true;
|
|
278
|
+
}
|
|
279
|
+
return Object.keys(clause).length > 0 ? clause : undefined;
|
|
280
|
+
}
|
|
281
|
+
/** A plain object (not an array, Date or null), the shape nested payloads take. */
|
|
282
|
+
function isPlainRecord(value) {
|
|
283
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
284
|
+
}
|
|
225
285
|
/**
|
|
226
286
|
* Build a PK-based where clause from a parent row and its table metadata.
|
|
227
287
|
*/
|
|
@@ -308,7 +368,20 @@ function belongsToCorrelationWhere(ctx, rel, parentRow, parentTable) {
|
|
|
308
368
|
* field itself, the two are combined with `AND` instead, so neither predicate
|
|
309
369
|
* can silently overwrite the other.
|
|
310
370
|
*/
|
|
311
|
-
function scopeWhereToParent(
|
|
371
|
+
function scopeWhereToParent(
|
|
372
|
+
// `true` is the to-one "the single related row" spelling, the only non-object
|
|
373
|
+
// value assertTargetSelectsSomething lets through.
|
|
374
|
+
target, correlation) {
|
|
375
|
+
// `disconnect: true` / `delete: true` on a to-one relation (the only place
|
|
376
|
+
// `true` gets past assertTargetSelectsSomething) leaves the caller with no
|
|
377
|
+
// selector at all: the predicate below is the correlation and nothing else,
|
|
378
|
+
// written entirely by the engine. Branded so the single-row write rule reads
|
|
379
|
+
// the relation's declared cardinality as the uniqueness source, since a
|
|
380
|
+
// `hasOne` FK is frequently not ALSO declared unique in metadata and the
|
|
381
|
+
// rule would otherwise refuse the write with "Name a unique key" against a
|
|
382
|
+
// call that never named one. See markInternalRowSelector.
|
|
383
|
+
if (target === true)
|
|
384
|
+
return markInternalRowSelector({ ...correlation });
|
|
312
385
|
for (const key of Object.keys(correlation)) {
|
|
313
386
|
// Branded as Turbine's own: this `AND` has a FIXED arity of two, chosen
|
|
314
387
|
// here rather than reachable from a request body, so it must not cost the
|
|
@@ -822,15 +895,13 @@ export async function executeNestedCreate(ctx, tableName, data, depth = 0, path
|
|
|
822
895
|
await processManyToMany(ctx, rel, relName, ops, parentRow);
|
|
823
896
|
}
|
|
824
897
|
}
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
withClause[relName] = true;
|
|
829
|
-
}
|
|
898
|
+
// The `with` clause for the final read: the whole tree that was written, at
|
|
899
|
+
// every depth (see readBackWith), not only its top level.
|
|
900
|
+
const withClause = readBackWith(ctx.schema, tableName, relations);
|
|
830
901
|
// Final read using existing json_agg machinery
|
|
831
902
|
const fullRow = await ctx.tx.table(tableName).findUnique({
|
|
832
903
|
where: pkWhere(tableMeta, parentRow),
|
|
833
|
-
with:
|
|
904
|
+
with: withClause,
|
|
834
905
|
});
|
|
835
906
|
return (fullRow ?? parentRow);
|
|
836
907
|
}
|
|
@@ -933,14 +1004,10 @@ export async function executeNestedUpdate(ctx, tableName, where, data, depth = 0
|
|
|
933
1004
|
await processManyToMany(ctx, rel, relName, ops, parentRow);
|
|
934
1005
|
}
|
|
935
1006
|
}
|
|
936
|
-
// Final read with all touched relations
|
|
937
|
-
const withClause = {};
|
|
938
|
-
for (const relName of Object.keys(relations)) {
|
|
939
|
-
withClause[relName] = true;
|
|
940
|
-
}
|
|
1007
|
+
// Final read with all touched relations, to the depth they were written.
|
|
941
1008
|
const fullRow = await ctx.tx.table(tableName).findUnique({
|
|
942
1009
|
where: pkWhere(tableMeta, parentRow),
|
|
943
|
-
with:
|
|
1010
|
+
with: readBackWith(ctx.schema, tableName, relations),
|
|
944
1011
|
});
|
|
945
1012
|
return (fullRow ?? parentRow);
|
|
946
1013
|
}
|
|
@@ -1330,7 +1397,11 @@ async function processBelongsToUpdate(ctx, rel, updateArg, parentRow, parentTabl
|
|
|
1330
1397
|
// shared correlation helper (like every sibling operation) so a NULL parent
|
|
1331
1398
|
// FK reports not-found instead of compiling to `refField IS NULL` and
|
|
1332
1399
|
// updating EVERY row of the related table with a null reference key.
|
|
1333
|
-
|
|
1400
|
+
// No caller `where` exists on this shape (`update: { data }`), so the
|
|
1401
|
+
// predicate is entirely the engine's: branded for the single-row write rule
|
|
1402
|
+
// exactly as the to-one disconnect/delete correlation is.
|
|
1403
|
+
const correlationWhere = belongsToCorrelationWhere(ctx, rel, parentRow, parentTable);
|
|
1404
|
+
const where = correlationWhere && markInternalRowSelector(correlationWhere);
|
|
1334
1405
|
if (!where) {
|
|
1335
1406
|
// Parent FK is NULL: it points at nothing, so nothing is in scope to update.
|
|
1336
1407
|
const nullFk = Object.fromEntries(normalizeKeyColumns(rel.foreignKey).map((c) => [c, null]));
|
|
@@ -58,6 +58,8 @@ export interface PgPoolClient {
|
|
|
58
58
|
_types?: unknown;
|
|
59
59
|
release(err?: Error | boolean): void;
|
|
60
60
|
}
|
|
61
|
+
/** Did {@link runPipelined} leave this client unusable? See {@link PIPELINE_DISCARD}. */
|
|
62
|
+
export declare function pipelineClientNeedsDiscard(client: unknown): boolean;
|
|
61
63
|
export interface PipelineRunOptions {
|
|
62
64
|
/**
|
|
63
65
|
* Whether to wrap the pipeline in BEGIN/COMMIT (default: true).
|
|
@@ -19,6 +19,22 @@
|
|
|
19
19
|
import Result from 'pg/lib/result';
|
|
20
20
|
import { prepareValue } from 'pg/lib/utils';
|
|
21
21
|
import { PipelineError, TimeoutError, wrapPgError } from './errors.js';
|
|
22
|
+
/**
|
|
23
|
+
* Marks a client this module could not hand back in a reusable state, so the
|
|
24
|
+
* caller releases it WITH an error and the pool drops it instead of lending it
|
|
25
|
+
* out again.
|
|
26
|
+
*
|
|
27
|
+
* A symbol, and `Symbol.for` so an ESM and a CJS copy of this module agree on
|
|
28
|
+
* it (the same reason the internal-combinator brand uses one). Set only when
|
|
29
|
+
* the backend still reports an open or aborted transaction after this module
|
|
30
|
+
* has done what it can, which after {@link runPipelined}'s rollback means the
|
|
31
|
+
* rollback itself did not take.
|
|
32
|
+
*/
|
|
33
|
+
const PIPELINE_DISCARD = Symbol.for('turbine.pipeline.discardClient');
|
|
34
|
+
/** Did {@link runPipelined} leave this client unusable? See {@link PIPELINE_DISCARD}. */
|
|
35
|
+
export function pipelineClientNeedsDiscard(client) {
|
|
36
|
+
return (typeof client === 'object' && client !== null && client[PIPELINE_DISCARD] === true);
|
|
37
|
+
}
|
|
22
38
|
// ---------------------------------------------------------------------------
|
|
23
39
|
// Event names we intercept
|
|
24
40
|
// ---------------------------------------------------------------------------
|
|
@@ -120,6 +136,10 @@ export async function runPipelined(client, queries, options = {}) {
|
|
|
120
136
|
let timeoutHandle;
|
|
121
137
|
// Whether cleanup has already been performed
|
|
122
138
|
let cleaned = false;
|
|
139
|
+
/** Transaction status reported by the most recent ReadyForQuery. */
|
|
140
|
+
let txStatus;
|
|
141
|
+
/** Whether the recovery ROLLBACK has already been sent (it is sent at most once). */
|
|
142
|
+
let rollbackSent = false;
|
|
123
143
|
/**
|
|
124
144
|
* Map commandComplete index to the corresponding results[] index.
|
|
125
145
|
* In transactional mode: index 0 = BEGIN, 1..N = queries, N+1 = COMMIT
|
|
@@ -158,6 +178,62 @@ export async function runPipelined(client, queries, options = {}) {
|
|
|
158
178
|
// -----------------------------------------------------------------------
|
|
159
179
|
// Finalize: called on final readyForQuery
|
|
160
180
|
// -----------------------------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* Return the connection to the pool in a state the next borrower can use.
|
|
183
|
+
*
|
|
184
|
+
* The hazard this exists for: a transactional batch is `BEGIN` + queries +
|
|
185
|
+
* `COMMIT` + ONE `Sync`, so a query error makes the backend skip everything
|
|
186
|
+
* up to that Sync, the `COMMIT` included, and the connection goes back to
|
|
187
|
+
* the pool `idle in transaction (aborted)`. The next borrower's first
|
|
188
|
+
* statement then failed with `25P02`, a `$transaction` that landed on it
|
|
189
|
+
* lost its write, and a non-transactional pipeline failed every slot and
|
|
190
|
+
* released it still aborted, so the poisoning survived indefinitely.
|
|
191
|
+
*
|
|
192
|
+
* The recovery is one `ROLLBACK` on the same connection, sent AFTER the
|
|
193
|
+
* ReadyForQuery that says the backend will accept a new statement, and
|
|
194
|
+
* waited for: its own ReadyForQuery brings us back here with `I`. That
|
|
195
|
+
* keeps the connection, which matters at small pool sizes where discarding
|
|
196
|
+
* is a reconnect on the caller's next query.
|
|
197
|
+
*
|
|
198
|
+
* Returns true when the caller should finalize now, false when a rollback
|
|
199
|
+
* is in flight and the next ReadyForQuery will finish the job.
|
|
200
|
+
*/
|
|
201
|
+
function settleTransactionState() {
|
|
202
|
+
// `I` (idle) is the ordinary case and needs nothing, and neither does a
|
|
203
|
+
// batch that had no error: transactional mode's COMMIT closed the
|
|
204
|
+
// transaction, non-transactional mode never opened one. That keeps the
|
|
205
|
+
// happy path at exactly one round trip, which a `ROLLBACK`-on-unknown
|
|
206
|
+
// would have cost an extra Sync (caught by the cork/uncork test). A real
|
|
207
|
+
// backend always sends the status byte; an emitter without one is only
|
|
208
|
+
// ever a test double.
|
|
209
|
+
if (txStatus === 'I')
|
|
210
|
+
return true;
|
|
211
|
+
if (txStatus === undefined && !(transactional && pipelineError))
|
|
212
|
+
return true;
|
|
213
|
+
if (!transactional || rollbackSent) {
|
|
214
|
+
// Either nothing to roll back (non-transactional mode opens no
|
|
215
|
+
// transaction of its own), or the rollback already ran and did not
|
|
216
|
+
// clear the status. Hand the client back marked for disposal.
|
|
217
|
+
if (txStatus === 'E' || txStatus === 'T') {
|
|
218
|
+
Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
rollbackSent = true;
|
|
223
|
+
try {
|
|
224
|
+
connection.parse({ text: 'ROLLBACK', name: '' });
|
|
225
|
+
connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
|
|
226
|
+
connection.execute({ portal: '', rows: 0 });
|
|
227
|
+
connection.sync();
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// The socket would not take the rollback, so this connection cannot be
|
|
232
|
+
// repaired from here. Mark it and let the caller drop it.
|
|
233
|
+
Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
161
237
|
function finalize() {
|
|
162
238
|
cleanup();
|
|
163
239
|
if (transactional && pipelineError) {
|
|
@@ -284,11 +360,19 @@ export async function runPipelined(client, queries, options = {}) {
|
|
|
284
360
|
function onPortalSuspended() {
|
|
285
361
|
// We don't use row-limited portals
|
|
286
362
|
}
|
|
287
|
-
function onReadyForQuery() {
|
|
288
|
-
|
|
289
|
-
|
|
363
|
+
function onReadyForQuery(msg) {
|
|
364
|
+
txStatus = msg?.status;
|
|
365
|
+
// The recovery ROLLBACK's own ReadyForQuery is not one of the batch's, so
|
|
366
|
+
// it must not advance the counter that decides when the batch is done.
|
|
367
|
+
if (rollbackSent) {
|
|
290
368
|
finalize();
|
|
369
|
+
return;
|
|
291
370
|
}
|
|
371
|
+
rfqCount++;
|
|
372
|
+
if (rfqCount < expectedRfq)
|
|
373
|
+
return;
|
|
374
|
+
if (settleTransactionState())
|
|
375
|
+
finalize();
|
|
292
376
|
}
|
|
293
377
|
// -----------------------------------------------------------------------
|
|
294
378
|
// Attach our listeners
|
package/dist/pipeline.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { postgresDialect } from './dialect.js';
|
|
22
22
|
import { PipelineError, TurbineError, wrapPgError } from './errors.js';
|
|
23
|
-
import { runPipelined, supportsExtendedPipeline } from './pipeline-submittable.js';
|
|
23
|
+
import { pipelineClientNeedsDiscard, runPipelined, supportsExtendedPipeline, } from './pipeline-submittable.js';
|
|
24
24
|
/**
|
|
25
25
|
* Execute queries sequentially on an already-acquired connection.
|
|
26
26
|
* This is the fallback path for clients that don't support the extended-query
|
|
@@ -227,7 +227,19 @@ export async function executePipeline(pool, queries, options) {
|
|
|
227
227
|
throw wrapPgError(err);
|
|
228
228
|
}
|
|
229
229
|
finally {
|
|
230
|
-
client
|
|
230
|
+
// A client the pipeline could not return to a clean state is released WITH
|
|
231
|
+
// an error, which is how pg-pool is told to drop it rather than lend it
|
|
232
|
+
// out again. `runPipelined` rolls back its own aborted transaction, so this
|
|
233
|
+
// is the residue: a rollback that did not take, or a backend left in a
|
|
234
|
+
// transaction by something other than this module. Releasing such a client
|
|
235
|
+
// normally is what turned one failed batch into `25P02` on somebody else's
|
|
236
|
+
// query.
|
|
237
|
+
if (pipelineClientNeedsDiscard(client)) {
|
|
238
|
+
client.release(new Error('turbine: pipeline connection left an open transaction and was discarded'));
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
client.release();
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
}
|
|
233
245
|
/**
|
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
* yields `primaryKey: []` and a warning; single-row ops on it fail loudly.
|
|
41
41
|
* - `isGenerated` is always `false`: `describe` does not expose PowDB's `auto`
|
|
42
42
|
* modifier, so an introspected int PK is treated as client-supplied unless
|
|
43
|
-
* the caller hand-edits the metadata.
|
|
43
|
+
* the caller hand-edits the metadata. Since 0.78.0 that costs more than it
|
|
44
|
+
* did: the generated `*Create` type marks a primary key optional only when
|
|
45
|
+
* it is server-generated, defaulted or nullable, so codegen driven from
|
|
46
|
+
* THIS metadata makes an `auto` id REQUIRED on create. `defineSchema` is
|
|
47
|
+
* the path that knows, and remains the recommended one.
|
|
44
48
|
* - Doc-field expression indexes are INVISIBLE to `describe`, so they never
|
|
45
49
|
* round-trip; only plain `unique`/`index` columns appear in `indexes`.
|
|
46
50
|
* - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
|
package/dist/powdb-introspect.js
CHANGED
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
* yields `primaryKey: []` and a warning; single-row ops on it fail loudly.
|
|
41
41
|
* - `isGenerated` is always `false`: `describe` does not expose PowDB's `auto`
|
|
42
42
|
* modifier, so an introspected int PK is treated as client-supplied unless
|
|
43
|
-
* the caller hand-edits the metadata.
|
|
43
|
+
* the caller hand-edits the metadata. Since 0.78.0 that costs more than it
|
|
44
|
+
* did: the generated `*Create` type marks a primary key optional only when
|
|
45
|
+
* it is server-generated, defaulted or nullable, so codegen driven from
|
|
46
|
+
* THIS metadata makes an `auto` id REQUIRED on create. `defineSchema` is
|
|
47
|
+
* the path that knows, and remains the recommended one.
|
|
44
48
|
* - Doc-field expression indexes are INVISIBLE to `describe`, so they never
|
|
45
49
|
* round-trip; only plain `unique`/`index` columns appear in `indexes`.
|
|
46
50
|
* - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
|