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/cjs/generate.js
CHANGED
|
@@ -25,6 +25,7 @@ exports.generatePrismaMap = generatePrismaMap;
|
|
|
25
25
|
const node_fs_1 = require("node:fs");
|
|
26
26
|
const node_path_1 = require("node:path");
|
|
27
27
|
const errors_js_1 = require("./errors.js");
|
|
28
|
+
const introspect_js_1 = require("./introspect.js");
|
|
28
29
|
const schema_js_1 = require("./schema.js");
|
|
29
30
|
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
30
31
|
function entityName(tableName) {
|
|
@@ -402,6 +403,11 @@ function assertEmittableSchema(schema) {
|
|
|
402
403
|
for (const table of Object.values(schema.tables)) {
|
|
403
404
|
requireEmittable(entityName(table.name), `table "${table.name}"`, 'the generated entity type name');
|
|
404
405
|
requireEmittable(snakeToCamelStr(table.name), `table "${table.name}"`, 'the generated client accessor');
|
|
406
|
+
// Two columns on one field would emit a duplicate interface member (TS2300
|
|
407
|
+
// in the consumer's build) and a `columnMap` missing a column. The rule is
|
|
408
|
+
// introspect.ts's; re-asserting it here covers a hand-built or
|
|
409
|
+
// engine-introspected schema that never went through that path.
|
|
410
|
+
(0, introspect_js_1.assertDistinctColumnFields)(table.name, table.columns);
|
|
405
411
|
// Only the relations that reach the TYPE layer: a relation shadowing a
|
|
406
412
|
// column field is already dropped from types.ts, so refusing on its name
|
|
407
413
|
// would refuse a schema that generates fine.
|
|
@@ -472,18 +478,24 @@ function generateTypes(schema, options) {
|
|
|
472
478
|
lines.push('}');
|
|
473
479
|
lines.push('');
|
|
474
480
|
// --- Create input type ---
|
|
475
|
-
//
|
|
476
|
-
//
|
|
481
|
+
// Optional: server-generated columns (serial / identity), columns with a
|
|
482
|
+
// default, and nullable columns (default to NULL). Everything else is
|
|
483
|
+
// required, PRIMARY KEY MEMBERSHIP INCLUDED. A text natural key, a
|
|
484
|
+
// client-supplied uuid with no default, or a composite key of plain
|
|
485
|
+
// integers must be supplied, and the database rejects the row otherwise
|
|
486
|
+
// (E010). Marking every PK column optional let `create({ data: {} })` on
|
|
487
|
+
// a junction table typecheck and fail at runtime, which is the opposite of
|
|
488
|
+
// what a generated type is for.
|
|
477
489
|
lines.push(`/** Input type for creating a row in \`${docSafe(table.name)}\` */`);
|
|
478
490
|
lines.push(`export type ${typeName}Create = {`);
|
|
479
491
|
for (const col of table.columns) {
|
|
480
492
|
// STORED generated columns are computed by the database, never writable.
|
|
481
493
|
if (col.isGeneratedStored)
|
|
482
494
|
continue;
|
|
483
|
-
const
|
|
484
|
-
const isOptional = col.hasDefault || col.nullable
|
|
495
|
+
const isGenerated = col.isGenerated === true;
|
|
496
|
+
const isOptional = isGenerated || col.hasDefault || col.nullable;
|
|
485
497
|
if (isOptional) {
|
|
486
|
-
const reason =
|
|
498
|
+
const reason = isGenerated ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
487
499
|
lines.push(` /** Optional: ${reason} */`);
|
|
488
500
|
lines.push(` ${quoteIfNeeded(col.field)}?: ${writeColumnTsType(col, schema.enums)};`);
|
|
489
501
|
}
|
|
@@ -770,18 +782,18 @@ function generateZod(schema, options) {
|
|
|
770
782
|
}
|
|
771
783
|
lines.push('});');
|
|
772
784
|
lines.push('');
|
|
773
|
-
// Create schema, STORED generated columns can never be written;
|
|
774
|
-
// defaulted, and nullable columns are optional
|
|
785
|
+
// Create schema, STORED generated columns can never be written;
|
|
786
|
+
// server-generated, defaulted, and nullable columns are optional, the same
|
|
787
|
+
// rule as the `*Create` type (a PK with none of those is required).
|
|
775
788
|
lines.push(`/** Zod schema for creating a \`${docSafe(table.name)}\` row */`);
|
|
776
789
|
lines.push(`export const ${typeName}CreateSchema = z.object({`);
|
|
777
790
|
for (const col of table.columns) {
|
|
778
791
|
if (col.isGeneratedStored)
|
|
779
792
|
continue;
|
|
780
|
-
const isPk = table.primaryKey.includes(col.name);
|
|
781
793
|
let expr = zodBaseType(col, schema.enums, true);
|
|
782
794
|
if (col.nullable)
|
|
783
795
|
expr += '.nullable()';
|
|
784
|
-
if (col.
|
|
796
|
+
if (col.isGenerated === true || col.hasDefault || col.nullable)
|
|
785
797
|
expr += '.optional()';
|
|
786
798
|
lines.push(` ${quoteIfNeeded(col.field)}: ${expr},`);
|
|
787
799
|
}
|
|
@@ -810,6 +822,11 @@ function generateZod(schema, options) {
|
|
|
810
822
|
// metadata.ts generator
|
|
811
823
|
// ---------------------------------------------------------------------------
|
|
812
824
|
function generateMetadata(schema, options) {
|
|
825
|
+
// metadata.ts is where a field collision does its runtime damage: the
|
|
826
|
+
// emitted `columnMap` keeps whichever column was written last, so refuse it
|
|
827
|
+
// here as well as in the type emitters (see assertEmittableSchema).
|
|
828
|
+
for (const table of Object.values(schema.tables))
|
|
829
|
+
(0, introspect_js_1.assertDistinctColumnFields)(table.name, table.columns);
|
|
813
830
|
const lines = [
|
|
814
831
|
...generatedFileHeader(options),
|
|
815
832
|
"import type { SchemaMetadata } from 'turbine-orm';",
|
|
@@ -1025,15 +1042,21 @@ function generateIndex(schema, options) {
|
|
|
1025
1042
|
lines.push(' }');
|
|
1026
1043
|
lines.push('}');
|
|
1027
1044
|
lines.push('');
|
|
1028
|
-
// Augment TurbineClient via interface merging with
|
|
1029
|
-
//
|
|
1030
|
-
// so users get autocomplete on `tx.users`,
|
|
1045
|
+
// Augment TurbineClient via interface merging with typed $transaction and
|
|
1046
|
+
// $withSession overloads. The callback parameter is narrowed to
|
|
1047
|
+
// `TypedTransactionClient` so users get autocomplete on `tx.users`,
|
|
1048
|
+
// `tx.posts`, etc. $withSession is the RLS shorthand for
|
|
1049
|
+
// `$transaction(fn, { sessionContext })`; it used to be left out, so its
|
|
1050
|
+
// callback stayed the untyped base `TransactionClient` and the documented
|
|
1051
|
+
// `tx.<table>.findMany()` did not compile on a generated client.
|
|
1031
1052
|
//
|
|
1032
|
-
// IMPORTANT:
|
|
1033
|
-
//
|
|
1053
|
+
// IMPORTANT: each merged member must be compatible with the base class's
|
|
1054
|
+
// member ON ITS OWN (TS2415), since v0.26 the base $transaction also has a
|
|
1034
1055
|
// batch-array overload (`$transaction([...queries])`), so the merged
|
|
1035
1056
|
// interface must redeclare BOTH signatures. Emitting only the callback form
|
|
1036
|
-
// makes every generated client fail `tsc` with "incorrectly extends".
|
|
1057
|
+
// makes every generated client fail `tsc` with "incorrectly extends". The
|
|
1058
|
+
// $withSession overload mirrors the base parameter list exactly for the same
|
|
1059
|
+
// reason; src/test/generate-typecheck.test.ts compiles the result.
|
|
1037
1060
|
lines.push('export interface TurbineClient {');
|
|
1038
1061
|
lines.push(' /**');
|
|
1039
1062
|
lines.push(' * Run a callback inside a transaction. The callback receives a typed');
|
|
@@ -1050,6 +1073,15 @@ function generateIndex(schema, options) {
|
|
|
1050
1073
|
lines.push(' $transaction<T extends readonly DeferredQuery<unknown>[]>(');
|
|
1051
1074
|
lines.push(' queries: readonly [...T],');
|
|
1052
1075
|
lines.push(' ): Promise<PipelineResults<T>>;');
|
|
1076
|
+
lines.push(' /**');
|
|
1077
|
+
lines.push(' * Run a callback inside a transaction with the given session GUCs applied');
|
|
1078
|
+
lines.push(' * via `set_config(..., true)` (the RLS / multi-tenant shorthand). The');
|
|
1079
|
+
lines.push(' * callback receives a typed `TypedTransactionClient`, same as `$transaction`.');
|
|
1080
|
+
lines.push(' */');
|
|
1081
|
+
lines.push(' $withSession<R>(');
|
|
1082
|
+
lines.push(' context: Record<string, string | number | boolean>,');
|
|
1083
|
+
lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
|
|
1084
|
+
lines.push(' ): Promise<R>;');
|
|
1053
1085
|
lines.push('}');
|
|
1054
1086
|
lines.push('');
|
|
1055
1087
|
// Factory function with JSDoc
|
package/dist/cjs/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/cjs/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/cjs/introspect.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.assertSafeCatalogIdentifier = assertSafeCatalogIdentifier;
|
|
|
20
20
|
exports.assertSafeCatalogSchema = assertSafeCatalogSchema;
|
|
21
21
|
exports.introspect = introspect;
|
|
22
22
|
exports.applyRelationRenames = applyRelationRenames;
|
|
23
|
+
exports.assertDistinctColumnFields = assertDistinctColumnFields;
|
|
23
24
|
exports.introspectPostgresCatalog = introspectPostgresCatalog;
|
|
24
25
|
exports.parseIndexKeyEntries = parseIndexKeyEntries;
|
|
25
26
|
exports.indexKeyColumn = indexKeyColumn;
|
|
@@ -488,6 +489,50 @@ function applyRelationRenames(schema, renames) {
|
|
|
488
489
|
}
|
|
489
490
|
return { ...schema, tables };
|
|
490
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* THE field-collision rule: two columns of one table may never resolve to the
|
|
494
|
+
* same TypeScript field.
|
|
495
|
+
*
|
|
496
|
+
* Fields are derived with `snakeToCamel`, so a table carrying both a quoted
|
|
497
|
+
* `"createdAt"` and a `created_at` column (a Prisma-era column beside a
|
|
498
|
+
* hand-written one is the common way to get there) yields two `createdAt`
|
|
499
|
+
* fields. Nothing used to refuse that, and every consumer keyed by field then
|
|
500
|
+
* lost a column silently: `columnMap` kept whichever column was written last,
|
|
501
|
+
* reads folded both columns into one property, a write to the field reached
|
|
502
|
+
* only one of them, and `types.ts` carried a duplicate member that failed the
|
|
503
|
+
* consumer's `tsc` (TS2300) while `turbine generate` exited 0.
|
|
504
|
+
*
|
|
505
|
+
* A thrown error names the table, every colliding column and the fix. There is
|
|
506
|
+
* no per-column rename option, so the fix is `keepColumnNames` (the field is
|
|
507
|
+
* then the raw column name, and two distinct columns cannot collide) or a
|
|
508
|
+
* rename in the database. Applied once per table right after the catalog's
|
|
509
|
+
* columns are grouped, and again by the generate.ts emitters so a schema that
|
|
510
|
+
* never went through introspection is refused at the same boundary.
|
|
511
|
+
*/
|
|
512
|
+
function assertDistinctColumnFields(tableName, columns) {
|
|
513
|
+
const columnsByField = new Map();
|
|
514
|
+
for (const col of columns) {
|
|
515
|
+
const names = columnsByField.get(col.field);
|
|
516
|
+
if (names)
|
|
517
|
+
names.push(col.name);
|
|
518
|
+
else
|
|
519
|
+
columnsByField.set(col.field, [col.name]);
|
|
520
|
+
}
|
|
521
|
+
const collisions = [];
|
|
522
|
+
for (const [field, names] of columnsByField) {
|
|
523
|
+
if (names.length < 2)
|
|
524
|
+
continue;
|
|
525
|
+
const quoted = names.map((n) => `"${n}"`);
|
|
526
|
+
const list = `${quoted.slice(0, -1).join(', ')} and ${quoted[quoted.length - 1]}`;
|
|
527
|
+
collisions.push(`columns ${list} ${names.length === 2 ? 'both' : 'all'} resolve to the field "${field}"`);
|
|
528
|
+
}
|
|
529
|
+
if (collisions.length === 0)
|
|
530
|
+
return;
|
|
531
|
+
throw new errors_js_1.ValidationError(`Field collision on table "${tableName}": ${collisions.join('; ')}. A client addresses one column per field, ` +
|
|
532
|
+
'so reads would fold the colliding columns into one property and a write to that field would reach only ' +
|
|
533
|
+
'one of them. Rename one of the columns, or set `keepColumnNames: true` in turbine.config.ts ' +
|
|
534
|
+
'(`turbine generate --keep-column-names`) so every field is its raw column name.');
|
|
535
|
+
}
|
|
491
536
|
/**
|
|
492
537
|
* PostgreSQL catalog introspector: reads information_schema + pg_catalog and
|
|
493
538
|
* produces {@link SchemaMetadata}. This is the implementation wrapped by
|
|
@@ -559,7 +604,9 @@ async function introspectPostgresCatalog(options) {
|
|
|
559
604
|
const arrayType = dialect.arrayType?.(baseType) ?? 'text[]';
|
|
560
605
|
const col = {
|
|
561
606
|
name: row.column_name,
|
|
562
|
-
field
|
|
607
|
+
// Under keepColumnNames the field IS the column name, which is why two
|
|
608
|
+
// distinct columns can never collide there (see assertDistinctColumnFields).
|
|
609
|
+
field: options.keepColumnNames ? row.column_name : (0, schema_js_1.snakeToCamel)(row.column_name),
|
|
563
610
|
dialectType,
|
|
564
611
|
pgType: dialectType,
|
|
565
612
|
tsType: dialect.typeToTypeScript?.(isArray ? dialectType : baseType, isNullable) ??
|
|
@@ -593,6 +640,12 @@ async function introspectPostgresCatalog(options) {
|
|
|
593
640
|
columnsByTable.set(tableName, []);
|
|
594
641
|
columnsByTable.get(tableName).push(col);
|
|
595
642
|
}
|
|
643
|
+
// Every consumer below keys the table by FIELD (columnMap, the relation
|
|
644
|
+
// derivation's shadow check, the generated interface), so the field set
|
|
645
|
+
// must be sound before any of them runs. THE one place the rule is applied
|
|
646
|
+
// to a live catalog; generate.ts re-asserts it for schemas built elsewhere.
|
|
647
|
+
for (const [tableName, cols] of columnsByTable)
|
|
648
|
+
assertDistinctColumnFields(tableName, cols);
|
|
596
649
|
// ----- Group primary keys by table -----
|
|
597
650
|
const pkByTable = new Map();
|
|
598
651
|
for (const row of pkResult.rows) {
|
package/dist/cjs/mssql.js
CHANGED
|
@@ -1058,6 +1058,26 @@ function buildForJsonManyToMany(dialect, ctx, h) {
|
|
|
1058
1058
|
`FOR JSON PATH, INCLUDE_NULL_VALUES), '[]')`);
|
|
1059
1059
|
}
|
|
1060
1060
|
const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
1061
|
+
/**
|
|
1062
|
+
* A `bit` column, as the driver actually delivers it.
|
|
1063
|
+
*
|
|
1064
|
+
* tedious parses TDS `BITTYPE` with `!!value`, so `sys.indexes.is_unique`
|
|
1065
|
+
* arrives as a JavaScript BOOLEAN, never as 1. Reading it through {@link num}
|
|
1066
|
+
* and comparing to 1 therefore answered `false` for EVERY unique index on SQL
|
|
1067
|
+
* Server, which emptied `uniqueColumns` on every introspected table and set
|
|
1068
|
+
* `IndexMetadata.unique` to false on every unique index. The visible cost was a
|
|
1069
|
+
* `findUnique` (and, once it carried the same rule, an `upsert`) on a genuinely
|
|
1070
|
+
* unique non-PK column being refused as not identifying one row, on the one
|
|
1071
|
+
* engine where the metadata could not say otherwise.
|
|
1072
|
+
*
|
|
1073
|
+
* The unit test could not see it: its mock built index rows with `1` and `0`
|
|
1074
|
+
* because the helper's parameter was typed `number`, so the fixture chose the
|
|
1075
|
+
* shape the code already handled. Only the live SQL Server leg produces the
|
|
1076
|
+
* boolean. Written to accept all three spellings rather than the one this
|
|
1077
|
+
* driver happens to send, because another `mssql` transport is free to send a
|
|
1078
|
+
* number or the string `'1'` and this is metadata, not a hot path.
|
|
1079
|
+
*/
|
|
1080
|
+
const bitIsTrue = (v) => v === true || v === 1 || v === '1' || v === 'true';
|
|
1061
1081
|
/**
|
|
1062
1082
|
* Derive relations from the FK list via the SHARED introspection pipeline
|
|
1063
1083
|
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
@@ -1193,7 +1213,7 @@ async function introspectMssqlWith(exec, schema = 'dbo', options = {}) {
|
|
|
1193
1213
|
const key = `${t}.${name}`;
|
|
1194
1214
|
let g = indexGroups.get(key);
|
|
1195
1215
|
if (!g) {
|
|
1196
|
-
g = { table: t, name, unique:
|
|
1216
|
+
g = { table: t, name, unique: bitIsTrue(r.IS_UNIQUE), columns: [] };
|
|
1197
1217
|
indexGroups.set(key, g);
|
|
1198
1218
|
}
|
|
1199
1219
|
g.columns.push(String(r.COLUMN_NAME));
|
package/dist/cjs/nested-write.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.createManyShapeRuns = createManyShapeRuns;
|
|
|
20
20
|
exports.executeNestedCreate = executeNestedCreate;
|
|
21
21
|
exports.executeNestedUpdate = executeNestedUpdate;
|
|
22
22
|
const errors_js_1 = require("./errors.js");
|
|
23
|
+
const compound_unique_js_1 = require("./query/compound-unique.js");
|
|
23
24
|
const utils_js_1 = require("./query/utils.js");
|
|
24
25
|
const schema_js_1 = require("./schema.js");
|
|
25
26
|
const MAX_DEPTH = 10;
|
|
@@ -230,6 +231,65 @@ function validateOps(relationName, ops, isUpdate) {
|
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* The `with` clause that reads back the tree a nested write just wrote.
|
|
236
|
+
*
|
|
237
|
+
* Built from the DATA, recursively: a relation whose payload itself contains
|
|
238
|
+
* relation writes is requested as `{ rel: { with: { ... } } }` rather than
|
|
239
|
+
* `{ rel: true }`. It used to be the top-level keys alone, so a depth-3 create
|
|
240
|
+
* wrote all three levels and returned an object whose grandchildren were
|
|
241
|
+
* missing entirely, which a caller reads as "there are none".
|
|
242
|
+
*
|
|
243
|
+
* Only relations that were actually WRITTEN are requested, so a create with no
|
|
244
|
+
* nested data still reads back exactly what it did before. The walk is capped
|
|
245
|
+
* at {@link MAX_DEPTH}, the cap the write walk itself uses, and the builder's
|
|
246
|
+
* own relation-depth guard is the backstop below that.
|
|
247
|
+
*/
|
|
248
|
+
function readBackWith(schema, tableName, relations, depth = 0) {
|
|
249
|
+
const tableMeta = schema.tables[tableName];
|
|
250
|
+
if (!tableMeta || depth >= MAX_DEPTH)
|
|
251
|
+
return undefined;
|
|
252
|
+
const clause = {};
|
|
253
|
+
for (const [relName, ops] of Object.entries(relations)) {
|
|
254
|
+
const rel = tableMeta.relations[relName];
|
|
255
|
+
if (!rel)
|
|
256
|
+
continue;
|
|
257
|
+
// Every payload this relation was written with: `create` / `update` /
|
|
258
|
+
// `upsert` take objects (or arrays of them) whose own keys may name
|
|
259
|
+
// relations of the TARGET table. `connect` / `disconnect` / `set` name
|
|
260
|
+
// existing rows and write no nested data, so they contribute nothing here.
|
|
261
|
+
const nested = {};
|
|
262
|
+
for (const key of ['create', 'update', 'upsert']) {
|
|
263
|
+
const payload = ops[key];
|
|
264
|
+
if (payload === undefined)
|
|
265
|
+
continue;
|
|
266
|
+
for (const item of toArray(payload)) {
|
|
267
|
+
// `{ where, data }` (nested update) and `{ where, create, update }`
|
|
268
|
+
// (nested upsert) carry their written fields one level in.
|
|
269
|
+
const bodies = isPlainRecord(item) && ('data' in item || 'create' in item || 'update' in item)
|
|
270
|
+
? [item.data, item.create, item.update]
|
|
271
|
+
: [item];
|
|
272
|
+
for (const body of bodies) {
|
|
273
|
+
if (!isPlainRecord(body))
|
|
274
|
+
continue;
|
|
275
|
+
const child = schema.tables[rel.to];
|
|
276
|
+
if (!child)
|
|
277
|
+
continue;
|
|
278
|
+
for (const [k, v] of Object.entries(extractRelationFields(body, child).relations)) {
|
|
279
|
+
nested[k] = { ...(nested[k] ?? {}), ...v };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const deeper = Object.keys(nested).length > 0 ? readBackWith(schema, rel.to, nested, depth + 1) : undefined;
|
|
285
|
+
clause[relName] = deeper ? { with: deeper } : true;
|
|
286
|
+
}
|
|
287
|
+
return Object.keys(clause).length > 0 ? clause : undefined;
|
|
288
|
+
}
|
|
289
|
+
/** A plain object (not an array, Date or null), the shape nested payloads take. */
|
|
290
|
+
function isPlainRecord(value) {
|
|
291
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
292
|
+
}
|
|
233
293
|
/**
|
|
234
294
|
* Build a PK-based where clause from a parent row and its table metadata.
|
|
235
295
|
*/
|
|
@@ -316,7 +376,20 @@ function belongsToCorrelationWhere(ctx, rel, parentRow, parentTable) {
|
|
|
316
376
|
* field itself, the two are combined with `AND` instead, so neither predicate
|
|
317
377
|
* can silently overwrite the other.
|
|
318
378
|
*/
|
|
319
|
-
function scopeWhereToParent(
|
|
379
|
+
function scopeWhereToParent(
|
|
380
|
+
// `true` is the to-one "the single related row" spelling, the only non-object
|
|
381
|
+
// value assertTargetSelectsSomething lets through.
|
|
382
|
+
target, correlation) {
|
|
383
|
+
// `disconnect: true` / `delete: true` on a to-one relation (the only place
|
|
384
|
+
// `true` gets past assertTargetSelectsSomething) leaves the caller with no
|
|
385
|
+
// selector at all: the predicate below is the correlation and nothing else,
|
|
386
|
+
// written entirely by the engine. Branded so the single-row write rule reads
|
|
387
|
+
// the relation's declared cardinality as the uniqueness source, since a
|
|
388
|
+
// `hasOne` FK is frequently not ALSO declared unique in metadata and the
|
|
389
|
+
// rule would otherwise refuse the write with "Name a unique key" against a
|
|
390
|
+
// call that never named one. See markInternalRowSelector.
|
|
391
|
+
if (target === true)
|
|
392
|
+
return (0, compound_unique_js_1.markInternalRowSelector)({ ...correlation });
|
|
320
393
|
for (const key of Object.keys(correlation)) {
|
|
321
394
|
// Branded as Turbine's own: this `AND` has a FIXED arity of two, chosen
|
|
322
395
|
// here rather than reachable from a request body, so it must not cost the
|
|
@@ -830,15 +903,13 @@ async function executeNestedCreate(ctx, tableName, data, depth = 0, path = []) {
|
|
|
830
903
|
await processManyToMany(ctx, rel, relName, ops, parentRow);
|
|
831
904
|
}
|
|
832
905
|
}
|
|
833
|
-
//
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
withClause[relName] = true;
|
|
837
|
-
}
|
|
906
|
+
// The `with` clause for the final read: the whole tree that was written, at
|
|
907
|
+
// every depth (see readBackWith), not only its top level.
|
|
908
|
+
const withClause = readBackWith(ctx.schema, tableName, relations);
|
|
838
909
|
// Final read using existing json_agg machinery
|
|
839
910
|
const fullRow = await ctx.tx.table(tableName).findUnique({
|
|
840
911
|
where: pkWhere(tableMeta, parentRow),
|
|
841
|
-
with:
|
|
912
|
+
with: withClause,
|
|
842
913
|
});
|
|
843
914
|
return (fullRow ?? parentRow);
|
|
844
915
|
}
|
|
@@ -941,14 +1012,10 @@ async function executeNestedUpdate(ctx, tableName, where, data, depth = 0, path
|
|
|
941
1012
|
await processManyToMany(ctx, rel, relName, ops, parentRow);
|
|
942
1013
|
}
|
|
943
1014
|
}
|
|
944
|
-
// Final read with all touched relations
|
|
945
|
-
const withClause = {};
|
|
946
|
-
for (const relName of Object.keys(relations)) {
|
|
947
|
-
withClause[relName] = true;
|
|
948
|
-
}
|
|
1015
|
+
// Final read with all touched relations, to the depth they were written.
|
|
949
1016
|
const fullRow = await ctx.tx.table(tableName).findUnique({
|
|
950
1017
|
where: pkWhere(tableMeta, parentRow),
|
|
951
|
-
with:
|
|
1018
|
+
with: readBackWith(ctx.schema, tableName, relations),
|
|
952
1019
|
});
|
|
953
1020
|
return (fullRow ?? parentRow);
|
|
954
1021
|
}
|
|
@@ -1338,7 +1405,11 @@ async function processBelongsToUpdate(ctx, rel, updateArg, parentRow, parentTabl
|
|
|
1338
1405
|
// shared correlation helper (like every sibling operation) so a NULL parent
|
|
1339
1406
|
// FK reports not-found instead of compiling to `refField IS NULL` and
|
|
1340
1407
|
// updating EVERY row of the related table with a null reference key.
|
|
1341
|
-
|
|
1408
|
+
// No caller `where` exists on this shape (`update: { data }`), so the
|
|
1409
|
+
// predicate is entirely the engine's: branded for the single-row write rule
|
|
1410
|
+
// exactly as the to-one disconnect/delete correlation is.
|
|
1411
|
+
const correlationWhere = belongsToCorrelationWhere(ctx, rel, parentRow, parentTable);
|
|
1412
|
+
const where = correlationWhere && (0, compound_unique_js_1.markInternalRowSelector)(correlationWhere);
|
|
1342
1413
|
if (!where) {
|
|
1343
1414
|
// Parent FK is NULL: it points at nothing, so nothing is in scope to update.
|
|
1344
1415
|
const nullFk = Object.fromEntries((0, schema_js_1.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).
|
|
@@ -21,11 +21,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.pipelineClientNeedsDiscard = pipelineClientNeedsDiscard;
|
|
24
25
|
exports.runPipelined = runPipelined;
|
|
25
26
|
exports.supportsExtendedPipeline = supportsExtendedPipeline;
|
|
26
27
|
const result_1 = __importDefault(require("pg/lib/result"));
|
|
27
28
|
const utils_1 = require("pg/lib/utils");
|
|
28
29
|
const errors_js_1 = require("./errors.js");
|
|
30
|
+
/**
|
|
31
|
+
* Marks a client this module could not hand back in a reusable state, so the
|
|
32
|
+
* caller releases it WITH an error and the pool drops it instead of lending it
|
|
33
|
+
* out again.
|
|
34
|
+
*
|
|
35
|
+
* A symbol, and `Symbol.for` so an ESM and a CJS copy of this module agree on
|
|
36
|
+
* it (the same reason the internal-combinator brand uses one). Set only when
|
|
37
|
+
* the backend still reports an open or aborted transaction after this module
|
|
38
|
+
* has done what it can, which after {@link runPipelined}'s rollback means the
|
|
39
|
+
* rollback itself did not take.
|
|
40
|
+
*/
|
|
41
|
+
const PIPELINE_DISCARD = Symbol.for('turbine.pipeline.discardClient');
|
|
42
|
+
/** Did {@link runPipelined} leave this client unusable? See {@link PIPELINE_DISCARD}. */
|
|
43
|
+
function pipelineClientNeedsDiscard(client) {
|
|
44
|
+
return (typeof client === 'object' && client !== null && client[PIPELINE_DISCARD] === true);
|
|
45
|
+
}
|
|
29
46
|
// ---------------------------------------------------------------------------
|
|
30
47
|
// Event names we intercept
|
|
31
48
|
// ---------------------------------------------------------------------------
|
|
@@ -127,6 +144,10 @@ async function runPipelined(client, queries, options = {}) {
|
|
|
127
144
|
let timeoutHandle;
|
|
128
145
|
// Whether cleanup has already been performed
|
|
129
146
|
let cleaned = false;
|
|
147
|
+
/** Transaction status reported by the most recent ReadyForQuery. */
|
|
148
|
+
let txStatus;
|
|
149
|
+
/** Whether the recovery ROLLBACK has already been sent (it is sent at most once). */
|
|
150
|
+
let rollbackSent = false;
|
|
130
151
|
/**
|
|
131
152
|
* Map commandComplete index to the corresponding results[] index.
|
|
132
153
|
* In transactional mode: index 0 = BEGIN, 1..N = queries, N+1 = COMMIT
|
|
@@ -165,6 +186,62 @@ async function runPipelined(client, queries, options = {}) {
|
|
|
165
186
|
// -----------------------------------------------------------------------
|
|
166
187
|
// Finalize: called on final readyForQuery
|
|
167
188
|
// -----------------------------------------------------------------------
|
|
189
|
+
/**
|
|
190
|
+
* Return the connection to the pool in a state the next borrower can use.
|
|
191
|
+
*
|
|
192
|
+
* The hazard this exists for: a transactional batch is `BEGIN` + queries +
|
|
193
|
+
* `COMMIT` + ONE `Sync`, so a query error makes the backend skip everything
|
|
194
|
+
* up to that Sync, the `COMMIT` included, and the connection goes back to
|
|
195
|
+
* the pool `idle in transaction (aborted)`. The next borrower's first
|
|
196
|
+
* statement then failed with `25P02`, a `$transaction` that landed on it
|
|
197
|
+
* lost its write, and a non-transactional pipeline failed every slot and
|
|
198
|
+
* released it still aborted, so the poisoning survived indefinitely.
|
|
199
|
+
*
|
|
200
|
+
* The recovery is one `ROLLBACK` on the same connection, sent AFTER the
|
|
201
|
+
* ReadyForQuery that says the backend will accept a new statement, and
|
|
202
|
+
* waited for: its own ReadyForQuery brings us back here with `I`. That
|
|
203
|
+
* keeps the connection, which matters at small pool sizes where discarding
|
|
204
|
+
* is a reconnect on the caller's next query.
|
|
205
|
+
*
|
|
206
|
+
* Returns true when the caller should finalize now, false when a rollback
|
|
207
|
+
* is in flight and the next ReadyForQuery will finish the job.
|
|
208
|
+
*/
|
|
209
|
+
function settleTransactionState() {
|
|
210
|
+
// `I` (idle) is the ordinary case and needs nothing, and neither does a
|
|
211
|
+
// batch that had no error: transactional mode's COMMIT closed the
|
|
212
|
+
// transaction, non-transactional mode never opened one. That keeps the
|
|
213
|
+
// happy path at exactly one round trip, which a `ROLLBACK`-on-unknown
|
|
214
|
+
// would have cost an extra Sync (caught by the cork/uncork test). A real
|
|
215
|
+
// backend always sends the status byte; an emitter without one is only
|
|
216
|
+
// ever a test double.
|
|
217
|
+
if (txStatus === 'I')
|
|
218
|
+
return true;
|
|
219
|
+
if (txStatus === undefined && !(transactional && pipelineError))
|
|
220
|
+
return true;
|
|
221
|
+
if (!transactional || rollbackSent) {
|
|
222
|
+
// Either nothing to roll back (non-transactional mode opens no
|
|
223
|
+
// transaction of its own), or the rollback already ran and did not
|
|
224
|
+
// clear the status. Hand the client back marked for disposal.
|
|
225
|
+
if (txStatus === 'E' || txStatus === 'T') {
|
|
226
|
+
Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
rollbackSent = true;
|
|
231
|
+
try {
|
|
232
|
+
connection.parse({ text: 'ROLLBACK', name: '' });
|
|
233
|
+
connection.bind({ portal: '', statement: '', values: [], valueMapper: utils_1.prepareValue });
|
|
234
|
+
connection.execute({ portal: '', rows: 0 });
|
|
235
|
+
connection.sync();
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// The socket would not take the rollback, so this connection cannot be
|
|
240
|
+
// repaired from here. Mark it and let the caller drop it.
|
|
241
|
+
Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
168
245
|
function finalize() {
|
|
169
246
|
cleanup();
|
|
170
247
|
if (transactional && pipelineError) {
|
|
@@ -291,11 +368,19 @@ async function runPipelined(client, queries, options = {}) {
|
|
|
291
368
|
function onPortalSuspended() {
|
|
292
369
|
// We don't use row-limited portals
|
|
293
370
|
}
|
|
294
|
-
function onReadyForQuery() {
|
|
295
|
-
|
|
296
|
-
|
|
371
|
+
function onReadyForQuery(msg) {
|
|
372
|
+
txStatus = msg?.status;
|
|
373
|
+
// The recovery ROLLBACK's own ReadyForQuery is not one of the batch's, so
|
|
374
|
+
// it must not advance the counter that decides when the batch is done.
|
|
375
|
+
if (rollbackSent) {
|
|
297
376
|
finalize();
|
|
377
|
+
return;
|
|
298
378
|
}
|
|
379
|
+
rfqCount++;
|
|
380
|
+
if (rfqCount < expectedRfq)
|
|
381
|
+
return;
|
|
382
|
+
if (settleTransactionState())
|
|
383
|
+
finalize();
|
|
299
384
|
}
|
|
300
385
|
// -----------------------------------------------------------------------
|
|
301
386
|
// Attach our listeners
|
package/dist/cjs/pipeline.js
CHANGED
|
@@ -231,7 +231,19 @@ async function executePipeline(pool, queries, options) {
|
|
|
231
231
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
232
232
|
}
|
|
233
233
|
finally {
|
|
234
|
-
client
|
|
234
|
+
// A client the pipeline could not return to a clean state is released WITH
|
|
235
|
+
// an error, which is how pg-pool is told to drop it rather than lend it
|
|
236
|
+
// out again. `runPipelined` rolls back its own aborted transaction, so this
|
|
237
|
+
// is the residue: a rollback that did not take, or a backend left in a
|
|
238
|
+
// transaction by something other than this module. Releasing such a client
|
|
239
|
+
// normally is what turned one failed batch into `25P02` on somebody else's
|
|
240
|
+
// query.
|
|
241
|
+
if ((0, pipeline_submittable_js_1.pipelineClientNeedsDiscard)(client)) {
|
|
242
|
+
client.release(new Error('turbine: pipeline connection left an open transaction and was discarded'));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
client.release();
|
|
246
|
+
}
|
|
235
247
|
}
|
|
236
248
|
}
|
|
237
249
|
/**
|
|
@@ -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
|
|
@@ -41,7 +41,11 @@
|
|
|
41
41
|
* yields `primaryKey: []` and a warning; single-row ops on it fail loudly.
|
|
42
42
|
* - `isGenerated` is always `false`: `describe` does not expose PowDB's `auto`
|
|
43
43
|
* modifier, so an introspected int PK is treated as client-supplied unless
|
|
44
|
-
* the caller hand-edits the metadata.
|
|
44
|
+
* the caller hand-edits the metadata. Since 0.78.0 that costs more than it
|
|
45
|
+
* did: the generated `*Create` type marks a primary key optional only when
|
|
46
|
+
* it is server-generated, defaulted or nullable, so codegen driven from
|
|
47
|
+
* THIS metadata makes an `auto` id REQUIRED on create. `defineSchema` is
|
|
48
|
+
* the path that knows, and remains the recommended one.
|
|
45
49
|
* - Doc-field expression indexes are INVISIBLE to `describe`, so they never
|
|
46
50
|
* round-trip; only plain `unique`/`index` columns appear in `indexes`.
|
|
47
51
|
* - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
|