@prisma-next/target-postgres 0.4.0-dev.9 → 0.4.2
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/dist/codec-ids-CojIXVf9.mjs +29 -0
- package/dist/codec-ids-CojIXVf9.mjs.map +1 -0
- package/dist/codec-ids.d.mts +28 -0
- package/dist/codec-ids.d.mts.map +1 -0
- package/dist/codec-ids.mjs +3 -0
- package/dist/codec-types.d.mts +42 -0
- package/dist/codec-types.d.mts.map +1 -0
- package/dist/codec-types.mjs +3 -0
- package/dist/codecs-BoahtY_Q.mjs +385 -0
- package/dist/codecs-BoahtY_Q.mjs.map +1 -0
- package/dist/codecs-D-F2KJqt.d.mts +299 -0
- package/dist/codecs-D-F2KJqt.d.mts.map +1 -0
- package/dist/codecs.d.mts +2 -0
- package/dist/codecs.mjs +3 -0
- package/dist/control.d.mts +1 -9
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +23 -5101
- package/dist/control.mjs.map +1 -1
- package/dist/data-transform-CxFRBIUp.d.mts +32 -0
- package/dist/data-transform-CxFRBIUp.d.mts.map +1 -0
- package/dist/data-transform-VfEGzXWt.mjs +39 -0
- package/dist/data-transform-VfEGzXWt.mjs.map +1 -0
- package/dist/data-transform.d.mts +2 -0
- package/dist/data-transform.mjs +3 -0
- package/dist/default-normalizer-DNOpRoOF.mjs +131 -0
- package/dist/default-normalizer-DNOpRoOF.mjs.map +1 -0
- package/dist/default-normalizer.d.mts +19 -0
- package/dist/default-normalizer.d.mts.map +1 -0
- package/dist/default-normalizer.mjs +3 -0
- package/dist/{descriptor-meta-DkvCmY98.mjs → descriptor-meta-BVoVtyp-.mjs} +1 -1
- package/dist/{descriptor-meta-DkvCmY98.mjs.map → descriptor-meta-BVoVtyp-.mjs.map} +1 -1
- package/dist/errors-AFvEPZ1R.mjs +34 -0
- package/dist/errors-AFvEPZ1R.mjs.map +1 -0
- package/dist/errors.d.mts +27 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +3 -0
- package/dist/issue-planner-CFjB0_oO.mjs +879 -0
- package/dist/issue-planner-CFjB0_oO.mjs.map +1 -0
- package/dist/issue-planner.d.mts +85 -0
- package/dist/issue-planner.d.mts.map +1 -0
- package/dist/issue-planner.mjs +3 -0
- package/dist/migration.d.mts +90 -0
- package/dist/migration.d.mts.map +1 -0
- package/dist/migration.mjs +24 -0
- package/dist/migration.mjs.map +1 -0
- package/dist/native-type-normalizer-CInai_oY.mjs +38 -0
- package/dist/native-type-normalizer-CInai_oY.mjs.map +1 -0
- package/dist/native-type-normalizer.d.mts +18 -0
- package/dist/native-type-normalizer.d.mts.map +1 -0
- package/dist/native-type-normalizer.mjs +3 -0
- package/dist/op-factory-call-BKlruaiC.mjs +605 -0
- package/dist/op-factory-call-BKlruaiC.mjs.map +1 -0
- package/dist/op-factory-call-C3bWXKSP.d.mts +304 -0
- package/dist/op-factory-call-C3bWXKSP.d.mts.map +1 -0
- package/dist/op-factory-call.d.mts +3 -0
- package/dist/op-factory-call.mjs +3 -0
- package/dist/pack.d.mts +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/planner-CLUvVhUN.mjs +98 -0
- package/dist/planner-CLUvVhUN.mjs.map +1 -0
- package/dist/planner-ddl-builders-Dxvw1LHw.mjs +132 -0
- package/dist/planner-ddl-builders-Dxvw1LHw.mjs.map +1 -0
- package/dist/planner-ddl-builders.d.mts +22 -0
- package/dist/planner-ddl-builders.d.mts.map +1 -0
- package/dist/planner-ddl-builders.mjs +3 -0
- package/dist/planner-identity-values-Dju-o5GF.mjs +91 -0
- package/dist/planner-identity-values-Dju-o5GF.mjs.map +1 -0
- package/dist/planner-identity-values.d.mts +20 -0
- package/dist/planner-identity-values.d.mts.map +1 -0
- package/dist/planner-identity-values.mjs +3 -0
- package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts +20 -0
- package/dist/planner-produced-postgres-migration-CRRTno6Z.d.mts.map +1 -0
- package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs +33 -0
- package/dist/planner-produced-postgres-migration-DSSPq8QS.mjs.map +1 -0
- package/dist/planner-produced-postgres-migration.d.mts +5 -0
- package/dist/planner-produced-postgres-migration.mjs +3 -0
- package/dist/planner-schema-lookup-B7lkypwn.mjs +29 -0
- package/dist/planner-schema-lookup-B7lkypwn.mjs.map +1 -0
- package/dist/planner-schema-lookup.d.mts +22 -0
- package/dist/planner-schema-lookup.d.mts.map +1 -0
- package/dist/planner-schema-lookup.mjs +3 -0
- package/dist/planner-sql-checks-7jkgm9TX.mjs +241 -0
- package/dist/planner-sql-checks-7jkgm9TX.mjs.map +1 -0
- package/dist/planner-sql-checks.d.mts +55 -0
- package/dist/planner-sql-checks.d.mts.map +1 -0
- package/dist/planner-sql-checks.mjs +3 -0
- package/dist/planner-target-details-DH-azLu-.d.mts +11 -0
- package/dist/planner-target-details-DH-azLu-.d.mts.map +1 -0
- package/dist/planner-target-details.d.mts +2 -0
- package/dist/planner-target-details.mjs +1 -0
- package/dist/planner.d.mts +68 -0
- package/dist/planner.d.mts.map +1 -0
- package/dist/planner.mjs +4 -0
- package/dist/postgres-migration-BjA3Zmts.d.mts +50 -0
- package/dist/postgres-migration-BjA3Zmts.d.mts.map +1 -0
- package/dist/postgres-migration-qtmtbONe.mjs +52 -0
- package/dist/postgres-migration-qtmtbONe.mjs.map +1 -0
- package/dist/render-ops-D6_DHdOK.mjs +8 -0
- package/dist/render-ops-D6_DHdOK.mjs.map +1 -0
- package/dist/render-ops.d.mts +11 -0
- package/dist/render-ops.d.mts.map +1 -0
- package/dist/render-ops.mjs +3 -0
- package/dist/render-typescript-1rF_SB4g.mjs +85 -0
- package/dist/render-typescript-1rF_SB4g.mjs.map +1 -0
- package/dist/render-typescript.d.mts +15 -0
- package/dist/render-typescript.d.mts.map +1 -0
- package/dist/render-typescript.mjs +3 -0
- package/dist/runtime.d.mts +15 -3
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +10 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/shared-Bxkt8pNO.d.mts +41 -0
- package/dist/shared-Bxkt8pNO.d.mts.map +1 -0
- package/dist/sql-utils-r-Lw535w.mjs +76 -0
- package/dist/sql-utils-r-Lw535w.mjs.map +1 -0
- package/dist/sql-utils.d.mts +59 -0
- package/dist/sql-utils.d.mts.map +1 -0
- package/dist/sql-utils.mjs +3 -0
- package/dist/statement-builders-BPnmt6wx.mjs +116 -0
- package/dist/statement-builders-BPnmt6wx.mjs.map +1 -0
- package/dist/statement-builders.d.mts +23 -0
- package/dist/statement-builders.d.mts.map +1 -0
- package/dist/statement-builders.mjs +3 -0
- package/dist/tables-BmdW_FWO.mjs +477 -0
- package/dist/tables-BmdW_FWO.mjs.map +1 -0
- package/dist/types-ClK03Ojd.d.mts +10 -0
- package/dist/types-ClK03Ojd.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +40 -18
- package/src/core/codec-ids.ts +30 -0
- package/src/core/codecs.ts +645 -0
- package/src/core/default-normalizer.ts +131 -0
- package/src/core/descriptor-meta.ts +1 -1
- package/src/core/errors.ts +33 -0
- package/src/core/json-schema-type-expression.ts +131 -0
- package/src/core/migrations/issue-planner.ts +832 -0
- package/src/core/migrations/op-factory-call.ts +858 -0
- package/src/core/migrations/operations/columns.ts +285 -0
- package/src/core/migrations/operations/constraints.ts +191 -0
- package/src/core/migrations/operations/data-transform.ts +119 -0
- package/src/core/migrations/operations/dependencies.ts +36 -0
- package/src/core/migrations/operations/enums.ts +113 -0
- package/src/core/migrations/operations/indexes.ts +61 -0
- package/src/core/migrations/operations/raw.ts +15 -0
- package/src/core/migrations/operations/shared.ts +67 -0
- package/src/core/migrations/operations/tables.ts +63 -0
- package/src/core/migrations/planner-ddl-builders.ts +1 -1
- package/src/core/migrations/planner-produced-postgres-migration.ts +67 -0
- package/src/core/migrations/planner-recipes.ts +1 -1
- package/src/core/migrations/planner-sql-checks.ts +1 -1
- package/src/core/migrations/planner-strategies.ts +592 -151
- package/src/core/migrations/planner-target-details.ts +0 -6
- package/src/core/migrations/planner.ts +65 -785
- package/src/core/migrations/postgres-migration.ts +73 -0
- package/src/core/migrations/render-ops.ts +9 -0
- package/src/core/migrations/render-typescript.ts +105 -0
- package/src/core/migrations/runner.ts +2 -4
- package/src/core/native-type-normalizer.ts +49 -0
- package/src/core/sql-utils.ts +104 -0
- package/src/exports/codec-ids.ts +1 -0
- package/src/exports/codec-types.ts +51 -0
- package/src/exports/codecs.ts +2 -0
- package/src/exports/control.ts +9 -142
- package/src/exports/data-transform.ts +1 -0
- package/src/exports/default-normalizer.ts +1 -0
- package/src/exports/errors.ts +1 -0
- package/src/exports/issue-planner.ts +1 -0
- package/src/exports/migration.ts +46 -0
- package/src/exports/native-type-normalizer.ts +1 -0
- package/src/exports/op-factory-call.ts +25 -0
- package/src/exports/planner-ddl-builders.ts +8 -0
- package/src/exports/planner-identity-values.ts +1 -0
- package/src/exports/planner-produced-postgres-migration.ts +1 -0
- package/src/exports/planner-schema-lookup.ts +6 -0
- package/src/exports/planner-sql-checks.ts +11 -0
- package/src/exports/planner-target-details.ts +1 -0
- package/src/exports/planner.ts +1 -0
- package/src/exports/render-ops.ts +1 -0
- package/src/exports/render-typescript.ts +1 -0
- package/src/exports/runtime.ts +19 -4
- package/src/exports/sql-utils.ts +7 -0
- package/src/exports/statement-builders.ts +7 -0
- package/src/exports/types.ts +1 -0
- package/dist/migration-builders.d.mts +0 -88
- package/dist/migration-builders.d.mts.map +0 -1
- package/dist/migration-builders.mjs +0 -3
- package/dist/operation-descriptors-CxymFSgK.mjs +0 -52
- package/dist/operation-descriptors-CxymFSgK.mjs.map +0 -1
- package/src/core/migrations/descriptor-planner.ts +0 -464
- package/src/core/migrations/operation-descriptors.ts +0 -166
- package/src/core/migrations/operation-resolver.ts +0 -929
- package/src/core/migrations/planner-reconciliation.ts +0 -798
- package/src/core/migrations/scaffolding.ts +0 -140
- package/src/exports/migration-builders.ts +0 -56
|
@@ -1,24 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
normalizeSchemaNativeType,
|
|
3
|
-
parsePostgresDefault,
|
|
4
|
-
quoteIdentifier,
|
|
5
|
-
} from '@prisma-next/adapter-postgres/control';
|
|
6
|
-
import { errorPlanDoesNotSupportAuthoringSurface } from '@prisma-next/errors/migration';
|
|
7
1
|
import type {
|
|
8
|
-
CodecControlHooks,
|
|
9
|
-
ComponentDatabaseDependency,
|
|
10
2
|
MigrationOperationPolicy,
|
|
11
3
|
SqlMigrationPlannerPlanOptions,
|
|
12
|
-
SqlMigrationPlanOperation,
|
|
13
|
-
SqlPlannerConflict,
|
|
14
4
|
SqlPlannerFailureResult,
|
|
15
5
|
} from '@prisma-next/family-sql/control';
|
|
16
|
-
import {
|
|
17
|
-
collectInitDependencies,
|
|
18
|
-
createMigrationPlan,
|
|
19
|
-
extractCodecControlHooks,
|
|
20
|
-
plannerFailure,
|
|
21
|
-
} from '@prisma-next/family-sql/control';
|
|
6
|
+
import { extractCodecControlHooks, plannerFailure } from '@prisma-next/family-sql/control';
|
|
22
7
|
import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
|
|
23
8
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
24
9
|
import type {
|
|
@@ -27,44 +12,11 @@ import type {
|
|
|
27
12
|
MigrationScaffoldContext,
|
|
28
13
|
SchemaIssue,
|
|
29
14
|
} from '@prisma-next/framework-components/control';
|
|
30
|
-
import
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from '
|
|
35
|
-
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
36
|
-
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
37
|
-
import { ifDefined } from '@prisma-next/utils/defined';
|
|
38
|
-
import { buildAddColumnSql, buildCreateTableSql, buildForeignKeySql } from './planner-ddl-builders';
|
|
39
|
-
import { resolveIdentityValue } from './planner-identity-values';
|
|
40
|
-
import {
|
|
41
|
-
buildAddColumnOperationIdentity,
|
|
42
|
-
buildAddNotNullColumnWithTemporaryDefaultOperation,
|
|
43
|
-
} from './planner-recipes';
|
|
44
|
-
import { buildReconciliationPlan } from './planner-reconciliation';
|
|
45
|
-
import {
|
|
46
|
-
buildSchemaLookupMap,
|
|
47
|
-
hasForeignKey,
|
|
48
|
-
hasIndex,
|
|
49
|
-
hasUniqueConstraint,
|
|
50
|
-
type SchemaTableLookup,
|
|
51
|
-
} from './planner-schema-lookup';
|
|
52
|
-
import {
|
|
53
|
-
columnExistsCheck,
|
|
54
|
-
columnNullabilityCheck,
|
|
55
|
-
constraintExistsCheck,
|
|
56
|
-
qualifyTableName,
|
|
57
|
-
tableHasPrimaryKeyCheck,
|
|
58
|
-
tableIsEmptyCheck,
|
|
59
|
-
toRegclassLiteral,
|
|
60
|
-
} from './planner-sql-checks';
|
|
61
|
-
import {
|
|
62
|
-
buildTargetDetails,
|
|
63
|
-
type OperationClass,
|
|
64
|
-
type PlanningMode,
|
|
65
|
-
type PostgresPlanTargetDetails,
|
|
66
|
-
} from './planner-target-details';
|
|
67
|
-
import { renderDescriptorTypeScript } from './scaffolding';
|
|
15
|
+
import { parsePostgresDefault } from '../default-normalizer';
|
|
16
|
+
import { normalizeSchemaNativeType } from '../native-type-normalizer';
|
|
17
|
+
import { planIssues } from './issue-planner';
|
|
18
|
+
import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
|
|
19
|
+
import { postgresPlannerStrategies } from './planner-strategies';
|
|
68
20
|
|
|
69
21
|
type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
|
|
70
22
|
readonly frameworkComponents: infer T;
|
|
@@ -80,12 +32,6 @@ type VerifySqlSchemaOptionsWithComponents = Parameters<typeof verifySqlSchema>[0
|
|
|
80
32
|
readonly frameworkComponents: PlannerFrameworkComponents;
|
|
81
33
|
};
|
|
82
34
|
|
|
83
|
-
type PlannerDatabaseDependency = {
|
|
84
|
-
readonly id: string;
|
|
85
|
-
readonly label: string;
|
|
86
|
-
readonly install: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
|
|
87
|
-
};
|
|
88
|
-
|
|
89
35
|
interface PlannerConfig {
|
|
90
36
|
readonly defaultSchema: string;
|
|
91
37
|
}
|
|
@@ -104,44 +50,31 @@ export function createPostgresMigrationPlanner(
|
|
|
104
50
|
}
|
|
105
51
|
|
|
106
52
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
typeof createMigrationPlan<PostgresPlanTargetDetails>
|
|
113
|
-
> & {
|
|
114
|
-
renderTypeScript(): string;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Result of `PostgresMigrationPlanner.plan()`. A discriminated union that
|
|
119
|
-
* satisfies both the framework's `MigrationPlannerResult` (success plan
|
|
120
|
-
* carries `renderTypeScript()`) and the SQL family's typed result shape
|
|
121
|
-
* (success plan is a `SqlMigrationPlan<PostgresPlanTargetDetails>`).
|
|
53
|
+
* Result of `PostgresMigrationPlanner.plan()`. A discriminated union whose
|
|
54
|
+
* success variant carries a `TypeScriptRenderablePostgresMigration` — a
|
|
55
|
+
* migration object that both the CLI (via `renderTypeScript()`) and the
|
|
56
|
+
* SQL-typed callers (via `operations`, `describe()`, etc.) consume
|
|
57
|
+
* uniformly.
|
|
122
58
|
*/
|
|
123
59
|
export type PostgresPlanResult =
|
|
124
|
-
| { readonly kind: 'success'; readonly plan:
|
|
60
|
+
| { readonly kind: 'success'; readonly plan: TypeScriptRenderablePostgresMigration }
|
|
125
61
|
| SqlPlannerFailureResult;
|
|
126
62
|
|
|
127
63
|
/**
|
|
128
|
-
* Postgres migration planner
|
|
129
|
-
*
|
|
130
|
-
* Implements the framework's `MigrationPlanner<'sql', 'postgres'>` directly
|
|
131
|
-
* — meaning it owns `emptyMigration()` and attaches the `renderTypeScript()`
|
|
132
|
-
* stub to success plans (Postgres uses descriptor-flow authoring, so
|
|
133
|
-
* `renderTypeScript()` throws on planner-produced plans). No external wrapper
|
|
134
|
-
* is required at the descriptor level.
|
|
135
|
-
*
|
|
136
|
-
* `plan()` accepts the framework's option shape (with `contract`/`schema`
|
|
137
|
-
* typed as `unknown`); SQL-typed callers may pass the more specific
|
|
138
|
-
* `SqlMigrationPlannerPlanOptions`, since those structurally satisfy the
|
|
139
|
-
* looser framework contract. Internally we treat options as the SQL-typed
|
|
140
|
-
* superset so the existing planner logic stays identical.
|
|
64
|
+
* Postgres migration planner — a thin wrapper over `planIssues`.
|
|
141
65
|
*
|
|
142
|
-
* `
|
|
143
|
-
*
|
|
144
|
-
* `
|
|
66
|
+
* `plan()` verifies the live schema against the target contract (producing
|
|
67
|
+
* `SchemaIssue[]`) and delegates to `planIssues` with the unified
|
|
68
|
+
* `postgresPlannerStrategies` list: enum-change, NOT-NULL backfill,
|
|
69
|
+
* type-change, nullable-tightening, codec-hook storage types,
|
|
70
|
+
* component-declared dependency installs, and shared-temp-default /
|
|
71
|
+
* empty-table-guarded NOT-NULL add-column. The same strategy list runs for
|
|
72
|
+
* `migration plan`, `db update`, and `db init`; behavior diverges purely on
|
|
73
|
+
* `policy.allowedOperationClasses` (the data-safe strategies short-circuit
|
|
74
|
+
* when `'data'` is excluded). The issue planner applies operation-class
|
|
75
|
+
* policy gates and emits a single `PostgresOpFactoryCall[]` that drives both
|
|
76
|
+
* the runtime-ops view (via `renderOps`) and the `renderTypeScript()`
|
|
77
|
+
* authoring surface.
|
|
145
78
|
*/
|
|
146
79
|
export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgres'> {
|
|
147
80
|
constructor(private readonly config: PlannerConfig) {}
|
|
@@ -151,107 +84,66 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
151
84
|
readonly schema: unknown;
|
|
152
85
|
readonly policy: MigrationOperationPolicy;
|
|
153
86
|
readonly fromHash?: string;
|
|
87
|
+
/**
|
|
88
|
+
* The "from" contract (state the planner assumes the database starts
|
|
89
|
+
* at). Only `migration plan` supplies this; `db update` / `db init`
|
|
90
|
+
* reconcile against the live schema with no old contract. When present
|
|
91
|
+
* alongside the `'data'` operation class, strategies that need from/to
|
|
92
|
+
* column shape comparisons (unsafe type change, nullability tightening)
|
|
93
|
+
* activate.
|
|
94
|
+
*/
|
|
95
|
+
readonly fromContract?: unknown;
|
|
154
96
|
readonly schemaName?: string;
|
|
155
97
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
156
98
|
}): PostgresPlanResult {
|
|
157
|
-
return this.planSql(options as SqlMigrationPlannerPlanOptions);
|
|
99
|
+
return this.planSql(options as SqlMigrationPlannerPlanOptions, options.fromHash ?? '');
|
|
158
100
|
}
|
|
159
101
|
|
|
160
102
|
emptyMigration(context: MigrationScaffoldContext): MigrationPlanWithAuthoringSurface {
|
|
161
|
-
return {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
renderTypeScript(): string {
|
|
166
|
-
return renderDescriptorTypeScript([], context);
|
|
167
|
-
},
|
|
168
|
-
};
|
|
103
|
+
return new TypeScriptRenderablePostgresMigration([], {
|
|
104
|
+
from: context.fromHash,
|
|
105
|
+
to: context.toHash,
|
|
106
|
+
});
|
|
169
107
|
}
|
|
170
108
|
|
|
171
|
-
private planSql(options: SqlMigrationPlannerPlanOptions): PostgresPlanResult {
|
|
109
|
+
private planSql(options: SqlMigrationPlannerPlanOptions, fromHash: string): PostgresPlanResult {
|
|
172
110
|
const schemaName = options.schemaName ?? this.config.defaultSchema;
|
|
173
111
|
const policyResult = this.ensureAdditivePolicy(options.policy);
|
|
174
112
|
if (policyResult) {
|
|
175
113
|
return policyResult;
|
|
176
114
|
}
|
|
177
115
|
|
|
178
|
-
const
|
|
179
|
-
const schemaIssues = this.collectSchemaIssues(options, planningMode.includeExtraObjects);
|
|
180
|
-
|
|
181
|
-
// Extract codec control hooks once at entry point for reuse across all operations.
|
|
182
|
-
// This avoids repeated iteration over frameworkComponents for each method that needs hooks.
|
|
116
|
+
const schemaIssues = this.collectSchemaIssues(options);
|
|
183
117
|
const codecHooks = extractCodecControlHooks(options.frameworkComponents);
|
|
184
|
-
|
|
185
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
186
118
|
const storageTypes = options.contract.storage.types ?? {};
|
|
187
119
|
|
|
188
|
-
const
|
|
189
|
-
contract: options.contract,
|
|
120
|
+
const result = planIssues({
|
|
190
121
|
issues: schemaIssues,
|
|
122
|
+
toContract: options.contract,
|
|
123
|
+
// `fromContract` is only supplied by `migration plan`. It is `null` for
|
|
124
|
+
// `db update` / `db init`, which means data-safety strategies needing
|
|
125
|
+
// from/to comparisons (unsafe type change, nullable tightening) are
|
|
126
|
+
// inapplicable there — reconciliation falls through to
|
|
127
|
+
// `mapIssueToCall`'s direct destructive handlers.
|
|
128
|
+
fromContract: options.fromContract ?? null,
|
|
191
129
|
schemaName,
|
|
192
|
-
mode: planningMode,
|
|
193
|
-
policy: options.policy,
|
|
194
130
|
codecHooks,
|
|
131
|
+
storageTypes,
|
|
132
|
+
schema: options.schema,
|
|
133
|
+
policy: options.policy,
|
|
134
|
+
frameworkComponents: options.frameworkComponents,
|
|
135
|
+
strategies: postgresPlannerStrategies,
|
|
195
136
|
});
|
|
196
|
-
if (reconciliationPlan.conflicts.length > 0) {
|
|
197
|
-
return plannerFailure(reconciliationPlan.conflicts);
|
|
198
|
-
}
|
|
199
137
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return plannerFailure(storageTypePlan.conflicts);
|
|
138
|
+
if (!result.ok) {
|
|
139
|
+
return plannerFailure(result.failure);
|
|
203
140
|
}
|
|
204
141
|
|
|
205
|
-
// Sort table entries once for reuse across all additive operation builders.
|
|
206
|
-
const sortedTables = sortedEntries(options.contract.storage.tables);
|
|
207
|
-
|
|
208
|
-
// Pre-compute constraint lookups once per schema table for O(1) checks across all builders.
|
|
209
|
-
const schemaLookups = buildSchemaLookupMap(options.schema);
|
|
210
|
-
|
|
211
|
-
// Build extension operations from component-owned database dependencies
|
|
212
|
-
operations.push(
|
|
213
|
-
...this.buildDatabaseDependencyOperations(options),
|
|
214
|
-
...storageTypePlan.operations,
|
|
215
|
-
...reconciliationPlan.operations,
|
|
216
|
-
...this.buildTableOperations(
|
|
217
|
-
sortedTables,
|
|
218
|
-
options.schema,
|
|
219
|
-
schemaName,
|
|
220
|
-
codecHooks,
|
|
221
|
-
storageTypes,
|
|
222
|
-
),
|
|
223
|
-
...this.buildColumnOperations(
|
|
224
|
-
sortedTables,
|
|
225
|
-
options.schema,
|
|
226
|
-
schemaLookups,
|
|
227
|
-
schemaName,
|
|
228
|
-
codecHooks,
|
|
229
|
-
storageTypes,
|
|
230
|
-
),
|
|
231
|
-
...this.buildPrimaryKeyOperations(sortedTables, options.schema, schemaName),
|
|
232
|
-
...this.buildUniqueOperations(sortedTables, schemaLookups, schemaName),
|
|
233
|
-
...this.buildIndexOperations(sortedTables, schemaLookups, schemaName),
|
|
234
|
-
...this.buildFkBackingIndexOperations(sortedTables, schemaLookups, schemaName),
|
|
235
|
-
...this.buildForeignKeyOperations(sortedTables, schemaLookups, schemaName),
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
const plan = createMigrationPlan<PostgresPlanTargetDetails>({
|
|
239
|
-
targetId: 'postgres',
|
|
240
|
-
origin: null,
|
|
241
|
-
destination: {
|
|
242
|
-
storageHash: options.contract.storage.storageHash,
|
|
243
|
-
...ifDefined('profileHash', options.contract.profileHash),
|
|
244
|
-
},
|
|
245
|
-
operations,
|
|
246
|
-
});
|
|
247
|
-
|
|
248
142
|
return Object.freeze({
|
|
249
143
|
kind: 'success' as const,
|
|
250
|
-
plan:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
throw errorPlanDoesNotSupportAuthoringSurface({ targetId: 'postgres' });
|
|
254
|
-
},
|
|
144
|
+
plan: new TypeScriptRenderablePostgresMigration(result.value.calls, {
|
|
145
|
+
from: fromHash,
|
|
146
|
+
to: options.contract.storage.storageHash,
|
|
255
147
|
}),
|
|
256
148
|
});
|
|
257
149
|
}
|
|
@@ -269,573 +161,12 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
269
161
|
return null;
|
|
270
162
|
}
|
|
271
163
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
279
|
-
const dependencies = this.collectDependencies(options);
|
|
280
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
281
|
-
const seenDependencyIds = new Set<string>();
|
|
282
|
-
const seenOperationIds = new Set<string>();
|
|
283
|
-
|
|
284
|
-
const installedIds = new Set(options.schema.dependencies.map((d) => d.id));
|
|
285
|
-
|
|
286
|
-
for (const dependency of dependencies) {
|
|
287
|
-
if (seenDependencyIds.has(dependency.id)) {
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
seenDependencyIds.add(dependency.id);
|
|
291
|
-
|
|
292
|
-
if (installedIds.has(dependency.id)) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
for (const installOp of dependency.install) {
|
|
297
|
-
if (seenOperationIds.has(installOp.id)) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
seenOperationIds.add(installOp.id);
|
|
301
|
-
operations.push(installOp as SqlMigrationPlanOperation<PostgresPlanTargetDetails>);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return operations;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
private buildStorageTypeOperations(
|
|
309
|
-
options: PlannerOptionsWithComponents,
|
|
310
|
-
schemaName: string,
|
|
311
|
-
codecHooks: Map<string, CodecControlHooks>,
|
|
312
|
-
): {
|
|
313
|
-
readonly operations: readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[];
|
|
314
|
-
readonly conflicts: readonly SqlPlannerConflict[];
|
|
315
|
-
} {
|
|
316
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
317
|
-
const conflicts: SqlPlannerConflict[] = [];
|
|
318
|
-
const storageTypes = options.contract.storage.types ?? {};
|
|
319
|
-
|
|
320
|
-
for (const [typeName, typeInstance] of sortedEntries(storageTypes)) {
|
|
321
|
-
const hook = codecHooks.get(typeInstance.codecId);
|
|
322
|
-
const planResult = hook?.planTypeOperations?.({
|
|
323
|
-
typeName,
|
|
324
|
-
typeInstance,
|
|
325
|
-
contract: options.contract,
|
|
326
|
-
schema: options.schema,
|
|
327
|
-
schemaName,
|
|
328
|
-
policy: options.policy,
|
|
329
|
-
});
|
|
330
|
-
if (!planResult) {
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
for (const operation of planResult.operations) {
|
|
334
|
-
if (!options.policy.allowedOperationClasses.includes(operation.operationClass)) {
|
|
335
|
-
conflicts.push({
|
|
336
|
-
kind: 'missingButNonAdditive',
|
|
337
|
-
summary: `Storage type "${typeName}" requires "${operation.operationClass}" operation "${operation.id}"`,
|
|
338
|
-
location: {
|
|
339
|
-
type: typeName,
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
operations.push({
|
|
345
|
-
...operation,
|
|
346
|
-
target: {
|
|
347
|
-
id: operation.target.id,
|
|
348
|
-
details: this.buildTargetDetails('type', typeName, schemaName),
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return { operations, conflicts };
|
|
355
|
-
}
|
|
356
|
-
private collectDependencies(
|
|
357
|
-
options: PlannerOptionsWithComponents,
|
|
358
|
-
): ReadonlyArray<PlannerDatabaseDependency> {
|
|
359
|
-
const dependencies = collectInitDependencies(options.frameworkComponents);
|
|
360
|
-
return sortDependencies(dependencies.filter(isPostgresPlannerDependency));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private buildTableOperations(
|
|
364
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
365
|
-
schema: SqlSchemaIR,
|
|
366
|
-
schemaName: string,
|
|
367
|
-
codecHooks: Map<string, CodecControlHooks>,
|
|
368
|
-
storageTypes: Record<string, StorageTypeInstance>,
|
|
369
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
370
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
371
|
-
for (const [tableName, table] of tables) {
|
|
372
|
-
if (schema.tables[tableName]) {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
const qualified = qualifyTableName(schemaName, tableName);
|
|
376
|
-
operations.push({
|
|
377
|
-
id: `table.${tableName}`,
|
|
378
|
-
label: `Create table ${tableName}`,
|
|
379
|
-
summary: `Creates table ${tableName} with required columns`,
|
|
380
|
-
operationClass: 'additive',
|
|
381
|
-
target: {
|
|
382
|
-
id: 'postgres',
|
|
383
|
-
details: this.buildTargetDetails('table', tableName, schemaName),
|
|
384
|
-
},
|
|
385
|
-
precheck: [
|
|
386
|
-
{
|
|
387
|
-
description: `ensure table "${tableName}" does not exist`,
|
|
388
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, tableName)}) IS NULL`,
|
|
389
|
-
},
|
|
390
|
-
],
|
|
391
|
-
execute: [
|
|
392
|
-
{
|
|
393
|
-
description: `create table "${tableName}"`,
|
|
394
|
-
sql: buildCreateTableSql(qualified, table, codecHooks, storageTypes),
|
|
395
|
-
},
|
|
396
|
-
],
|
|
397
|
-
postcheck: [
|
|
398
|
-
{
|
|
399
|
-
description: `verify table "${tableName}" exists`,
|
|
400
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, tableName)}) IS NOT NULL`,
|
|
401
|
-
},
|
|
402
|
-
],
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
return operations;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private buildColumnOperations(
|
|
409
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
410
|
-
schema: SqlSchemaIR,
|
|
411
|
-
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
412
|
-
schemaName: string,
|
|
413
|
-
codecHooks: Map<string, CodecControlHooks>,
|
|
414
|
-
storageTypes: Record<string, StorageTypeInstance>,
|
|
415
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
416
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
417
|
-
for (const [tableName, table] of tables) {
|
|
418
|
-
const schemaTable = schema.tables[tableName];
|
|
419
|
-
if (!schemaTable) {
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
const schemaLookup = schemaLookups.get(tableName);
|
|
423
|
-
for (const [columnName, column] of sortedEntries(table.columns)) {
|
|
424
|
-
if (schemaTable.columns[columnName]) {
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
operations.push(
|
|
428
|
-
this.buildAddColumnOperation({
|
|
429
|
-
schema: schemaName,
|
|
430
|
-
tableName,
|
|
431
|
-
table,
|
|
432
|
-
schemaTable,
|
|
433
|
-
schemaLookup,
|
|
434
|
-
columnName,
|
|
435
|
-
column,
|
|
436
|
-
codecHooks,
|
|
437
|
-
storageTypes,
|
|
438
|
-
}),
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return operations;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
private buildAddColumnOperation(options: {
|
|
446
|
-
readonly schema: string;
|
|
447
|
-
readonly tableName: string;
|
|
448
|
-
readonly table: StorageTable;
|
|
449
|
-
readonly schemaTable: SqlSchemaIR['tables'][string];
|
|
450
|
-
readonly schemaLookup: SchemaTableLookup | undefined;
|
|
451
|
-
readonly columnName: string;
|
|
452
|
-
readonly column: StorageColumn;
|
|
453
|
-
readonly codecHooks: Map<string, CodecControlHooks>;
|
|
454
|
-
readonly storageTypes: Record<string, StorageTypeInstance>;
|
|
455
|
-
}): SqlMigrationPlanOperation<PostgresPlanTargetDetails> {
|
|
456
|
-
const {
|
|
457
|
-
schema,
|
|
458
|
-
tableName,
|
|
459
|
-
table,
|
|
460
|
-
schemaTable,
|
|
461
|
-
schemaLookup,
|
|
462
|
-
columnName,
|
|
463
|
-
column,
|
|
464
|
-
codecHooks,
|
|
465
|
-
storageTypes,
|
|
466
|
-
} = options;
|
|
467
|
-
const notNull = !column.nullable;
|
|
468
|
-
const hasDefault = column.default !== undefined;
|
|
469
|
-
// Planner logic decides whether this column needs the coordinated multi-step
|
|
470
|
-
// strategy. The strategy recipe itself is built by a dedicated helper.
|
|
471
|
-
const needsTemporaryDefault = notNull && !hasDefault;
|
|
472
|
-
const temporaryDefault = needsTemporaryDefault
|
|
473
|
-
? resolveIdentityValue(column, codecHooks, storageTypes)
|
|
474
|
-
: null;
|
|
475
|
-
const canUseSharedTemporaryDefault =
|
|
476
|
-
needsTemporaryDefault &&
|
|
477
|
-
temporaryDefault !== null &&
|
|
478
|
-
canUseSharedTemporaryDefaultStrategy({
|
|
479
|
-
table,
|
|
480
|
-
schemaTable,
|
|
481
|
-
schemaLookup,
|
|
482
|
-
columnName,
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
if (canUseSharedTemporaryDefault) {
|
|
486
|
-
return buildAddNotNullColumnWithTemporaryDefaultOperation({
|
|
487
|
-
schema,
|
|
488
|
-
tableName,
|
|
489
|
-
columnName,
|
|
490
|
-
column,
|
|
491
|
-
codecHooks,
|
|
492
|
-
storageTypes,
|
|
493
|
-
temporaryDefault,
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const qualified = qualifyTableName(schema, tableName);
|
|
498
|
-
const requiresEmptyTableCheck = needsTemporaryDefault && !canUseSharedTemporaryDefault;
|
|
499
|
-
return {
|
|
500
|
-
...buildAddColumnOperationIdentity(schema, tableName, columnName),
|
|
501
|
-
operationClass: 'additive',
|
|
502
|
-
precheck: [
|
|
503
|
-
{
|
|
504
|
-
description: `ensure column "${columnName}" is missing`,
|
|
505
|
-
sql: columnExistsCheck({ schema, table: tableName, column: columnName, exists: false }),
|
|
506
|
-
},
|
|
507
|
-
...(requiresEmptyTableCheck
|
|
508
|
-
? [
|
|
509
|
-
{
|
|
510
|
-
description: `ensure table "${tableName}" is empty before adding NOT NULL column without default`,
|
|
511
|
-
sql: tableIsEmptyCheck(qualified),
|
|
512
|
-
},
|
|
513
|
-
]
|
|
514
|
-
: []),
|
|
515
|
-
],
|
|
516
|
-
execute: [
|
|
517
|
-
{
|
|
518
|
-
description: `add column "${columnName}"`,
|
|
519
|
-
sql: buildAddColumnSql(
|
|
520
|
-
qualified,
|
|
521
|
-
columnName,
|
|
522
|
-
column,
|
|
523
|
-
codecHooks,
|
|
524
|
-
undefined,
|
|
525
|
-
storageTypes,
|
|
526
|
-
),
|
|
527
|
-
},
|
|
528
|
-
],
|
|
529
|
-
postcheck: [
|
|
530
|
-
{
|
|
531
|
-
description: `verify column "${columnName}" exists`,
|
|
532
|
-
sql: columnExistsCheck({ schema, table: tableName, column: columnName }),
|
|
533
|
-
},
|
|
534
|
-
...(notNull
|
|
535
|
-
? [
|
|
536
|
-
{
|
|
537
|
-
description: `verify column "${columnName}" is NOT NULL`,
|
|
538
|
-
sql: columnNullabilityCheck({
|
|
539
|
-
schema,
|
|
540
|
-
table: tableName,
|
|
541
|
-
column: columnName,
|
|
542
|
-
nullable: false,
|
|
543
|
-
}),
|
|
544
|
-
},
|
|
545
|
-
]
|
|
546
|
-
: []),
|
|
547
|
-
],
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
private buildPrimaryKeyOperations(
|
|
552
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
553
|
-
schema: SqlSchemaIR,
|
|
554
|
-
schemaName: string,
|
|
555
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
556
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
557
|
-
for (const [tableName, table] of tables) {
|
|
558
|
-
if (!table.primaryKey) {
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
const schemaTable = schema.tables[tableName];
|
|
562
|
-
if (!schemaTable || schemaTable.primaryKey) {
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
const constraintName = table.primaryKey.name ?? `${tableName}_pkey`;
|
|
566
|
-
operations.push({
|
|
567
|
-
id: `primaryKey.${tableName}.${constraintName}`,
|
|
568
|
-
label: `Add primary key ${constraintName} on ${tableName}`,
|
|
569
|
-
summary: `Adds primary key ${constraintName} on ${tableName}`,
|
|
570
|
-
operationClass: 'additive',
|
|
571
|
-
target: {
|
|
572
|
-
id: 'postgres',
|
|
573
|
-
details: this.buildTargetDetails('table', tableName, schemaName),
|
|
574
|
-
},
|
|
575
|
-
precheck: [
|
|
576
|
-
{
|
|
577
|
-
description: `ensure primary key does not exist on "${tableName}"`,
|
|
578
|
-
sql: tableHasPrimaryKeyCheck(schemaName, tableName, false),
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
execute: [
|
|
582
|
-
{
|
|
583
|
-
description: `add primary key "${constraintName}"`,
|
|
584
|
-
sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
|
|
585
|
-
ADD CONSTRAINT ${quoteIdentifier(constraintName)}
|
|
586
|
-
PRIMARY KEY (${table.primaryKey.columns.map(quoteIdentifier).join(', ')})`,
|
|
587
|
-
},
|
|
588
|
-
],
|
|
589
|
-
postcheck: [
|
|
590
|
-
{
|
|
591
|
-
description: `verify primary key "${constraintName}" exists`,
|
|
592
|
-
sql: tableHasPrimaryKeyCheck(schemaName, tableName, true, constraintName),
|
|
593
|
-
},
|
|
594
|
-
],
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
return operations;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
private buildUniqueOperations(
|
|
601
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
602
|
-
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
603
|
-
schemaName: string,
|
|
604
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
605
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
606
|
-
for (const [tableName, table] of tables) {
|
|
607
|
-
const lookup = schemaLookups.get(tableName);
|
|
608
|
-
for (const unique of table.uniques) {
|
|
609
|
-
if (lookup && hasUniqueConstraint(lookup, unique.columns)) {
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
const constraintName = unique.name ?? `${tableName}_${unique.columns.join('_')}_key`;
|
|
613
|
-
operations.push({
|
|
614
|
-
id: `unique.${tableName}.${constraintName}`,
|
|
615
|
-
label: `Add unique constraint ${constraintName} on ${tableName}`,
|
|
616
|
-
summary: `Adds unique constraint ${constraintName} on ${tableName}`,
|
|
617
|
-
operationClass: 'additive',
|
|
618
|
-
target: {
|
|
619
|
-
id: 'postgres',
|
|
620
|
-
details: this.buildTargetDetails('unique', constraintName, schemaName, tableName),
|
|
621
|
-
},
|
|
622
|
-
precheck: [
|
|
623
|
-
{
|
|
624
|
-
description: `ensure unique constraint "${constraintName}" is missing`,
|
|
625
|
-
sql: constraintExistsCheck({
|
|
626
|
-
constraintName,
|
|
627
|
-
schema: schemaName,
|
|
628
|
-
table: tableName,
|
|
629
|
-
exists: false,
|
|
630
|
-
}),
|
|
631
|
-
},
|
|
632
|
-
],
|
|
633
|
-
execute: [
|
|
634
|
-
{
|
|
635
|
-
description: `add unique constraint "${constraintName}"`,
|
|
636
|
-
sql: `ALTER TABLE ${qualifyTableName(schemaName, tableName)}
|
|
637
|
-
ADD CONSTRAINT ${quoteIdentifier(constraintName)}
|
|
638
|
-
UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
639
|
-
},
|
|
640
|
-
],
|
|
641
|
-
postcheck: [
|
|
642
|
-
{
|
|
643
|
-
description: `verify unique constraint "${constraintName}" exists`,
|
|
644
|
-
sql: constraintExistsCheck({ constraintName, schema: schemaName, table: tableName }),
|
|
645
|
-
},
|
|
646
|
-
],
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
return operations;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
private buildIndexOperations(
|
|
654
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
655
|
-
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
656
|
-
schemaName: string,
|
|
657
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
658
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
659
|
-
for (const [tableName, table] of tables) {
|
|
660
|
-
const lookup = schemaLookups.get(tableName);
|
|
661
|
-
for (const index of table.indexes) {
|
|
662
|
-
if (lookup && hasIndex(lookup, index.columns)) {
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
const indexName = index.name ?? defaultIndexName(tableName, index.columns);
|
|
666
|
-
operations.push({
|
|
667
|
-
id: `index.${tableName}.${indexName}`,
|
|
668
|
-
label: `Create index ${indexName} on ${tableName}`,
|
|
669
|
-
summary: `Creates index ${indexName} on ${tableName}`,
|
|
670
|
-
operationClass: 'additive',
|
|
671
|
-
target: {
|
|
672
|
-
id: 'postgres',
|
|
673
|
-
details: this.buildTargetDetails('index', indexName, schemaName, tableName),
|
|
674
|
-
},
|
|
675
|
-
precheck: [
|
|
676
|
-
{
|
|
677
|
-
description: `ensure index "${indexName}" is missing`,
|
|
678
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NULL`,
|
|
679
|
-
},
|
|
680
|
-
],
|
|
681
|
-
execute: [
|
|
682
|
-
{
|
|
683
|
-
description: `create index "${indexName}"`,
|
|
684
|
-
sql: `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualifyTableName(
|
|
685
|
-
schemaName,
|
|
686
|
-
tableName,
|
|
687
|
-
)} (${index.columns.map(quoteIdentifier).join(', ')})`,
|
|
688
|
-
},
|
|
689
|
-
],
|
|
690
|
-
postcheck: [
|
|
691
|
-
{
|
|
692
|
-
description: `verify index "${indexName}" exists`,
|
|
693
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NOT NULL`,
|
|
694
|
-
},
|
|
695
|
-
],
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
return operations;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
/**
|
|
703
|
-
* Generates FK-backing index operations for FKs with `index: true`,
|
|
704
|
-
* but only when no matching user-declared index exists in `contractTable.indexes`.
|
|
705
|
-
*/
|
|
706
|
-
private buildFkBackingIndexOperations(
|
|
707
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
708
|
-
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
709
|
-
schemaName: string,
|
|
710
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
711
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
712
|
-
for (const [tableName, table] of tables) {
|
|
713
|
-
const lookup = schemaLookups.get(tableName);
|
|
714
|
-
// Collect column sets of user-declared indexes to avoid duplicates
|
|
715
|
-
const declaredIndexColumns = new Set(table.indexes.map((idx) => idx.columns.join(',')));
|
|
716
|
-
|
|
717
|
-
for (const fk of table.foreignKeys) {
|
|
718
|
-
if (fk.index === false) continue;
|
|
719
|
-
// Skip if user already declared an index with these columns
|
|
720
|
-
if (declaredIndexColumns.has(fk.columns.join(','))) continue;
|
|
721
|
-
// Skip if the index already exists in the database
|
|
722
|
-
if (lookup && hasIndex(lookup, fk.columns)) continue;
|
|
723
|
-
|
|
724
|
-
const indexName = defaultIndexName(tableName, fk.columns);
|
|
725
|
-
operations.push({
|
|
726
|
-
id: `index.${tableName}.${indexName}`,
|
|
727
|
-
label: `Create FK-backing index ${indexName} on ${tableName}`,
|
|
728
|
-
summary: `Creates FK-backing index ${indexName} on ${tableName}`,
|
|
729
|
-
operationClass: 'additive',
|
|
730
|
-
target: {
|
|
731
|
-
id: 'postgres',
|
|
732
|
-
details: this.buildTargetDetails('index', indexName, schemaName, tableName),
|
|
733
|
-
},
|
|
734
|
-
precheck: [
|
|
735
|
-
{
|
|
736
|
-
description: `ensure index "${indexName}" is missing`,
|
|
737
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NULL`,
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
execute: [
|
|
741
|
-
{
|
|
742
|
-
description: `create FK-backing index "${indexName}"`,
|
|
743
|
-
sql: `CREATE INDEX ${quoteIdentifier(indexName)} ON ${qualifyTableName(
|
|
744
|
-
schemaName,
|
|
745
|
-
tableName,
|
|
746
|
-
)} (${fk.columns.map(quoteIdentifier).join(', ')})`,
|
|
747
|
-
},
|
|
748
|
-
],
|
|
749
|
-
postcheck: [
|
|
750
|
-
{
|
|
751
|
-
description: `verify index "${indexName}" exists`,
|
|
752
|
-
sql: `SELECT to_regclass(${toRegclassLiteral(schemaName, indexName)}) IS NOT NULL`,
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
return operations;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
private buildForeignKeyOperations(
|
|
762
|
-
tables: ReadonlyArray<[string, StorageTable]>,
|
|
763
|
-
schemaLookups: ReadonlyMap<string, SchemaTableLookup>,
|
|
764
|
-
schemaName: string,
|
|
765
|
-
): readonly SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] {
|
|
766
|
-
const operations: SqlMigrationPlanOperation<PostgresPlanTargetDetails>[] = [];
|
|
767
|
-
for (const [tableName, table] of tables) {
|
|
768
|
-
const lookup = schemaLookups.get(tableName);
|
|
769
|
-
for (const foreignKey of table.foreignKeys) {
|
|
770
|
-
if (foreignKey.constraint === false) continue;
|
|
771
|
-
if (lookup && hasForeignKey(lookup, foreignKey)) {
|
|
772
|
-
continue;
|
|
773
|
-
}
|
|
774
|
-
const fkName = foreignKey.name ?? `${tableName}_${foreignKey.columns.join('_')}_fkey`;
|
|
775
|
-
operations.push({
|
|
776
|
-
id: `foreignKey.${tableName}.${fkName}`,
|
|
777
|
-
label: `Add foreign key ${fkName} on ${tableName}`,
|
|
778
|
-
summary: `Adds foreign key ${fkName} referencing ${foreignKey.references.table}`,
|
|
779
|
-
operationClass: 'additive',
|
|
780
|
-
target: {
|
|
781
|
-
id: 'postgres',
|
|
782
|
-
details: this.buildTargetDetails('foreignKey', fkName, schemaName, tableName),
|
|
783
|
-
},
|
|
784
|
-
precheck: [
|
|
785
|
-
{
|
|
786
|
-
description: `ensure foreign key "${fkName}" is missing`,
|
|
787
|
-
sql: constraintExistsCheck({
|
|
788
|
-
constraintName: fkName,
|
|
789
|
-
schema: schemaName,
|
|
790
|
-
table: tableName,
|
|
791
|
-
exists: false,
|
|
792
|
-
}),
|
|
793
|
-
},
|
|
794
|
-
],
|
|
795
|
-
execute: [
|
|
796
|
-
{
|
|
797
|
-
description: `add foreign key "${fkName}"`,
|
|
798
|
-
sql: buildForeignKeySql(schemaName, tableName, fkName, foreignKey),
|
|
799
|
-
},
|
|
800
|
-
],
|
|
801
|
-
postcheck: [
|
|
802
|
-
{
|
|
803
|
-
description: `verify foreign key "${fkName}" exists`,
|
|
804
|
-
sql: constraintExistsCheck({
|
|
805
|
-
constraintName: fkName,
|
|
806
|
-
schema: schemaName,
|
|
807
|
-
table: tableName,
|
|
808
|
-
}),
|
|
809
|
-
},
|
|
810
|
-
],
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return operations;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
private buildTargetDetails(
|
|
818
|
-
objectType: OperationClass,
|
|
819
|
-
name: string,
|
|
820
|
-
schema: string,
|
|
821
|
-
table?: string,
|
|
822
|
-
): PostgresPlanTargetDetails {
|
|
823
|
-
return buildTargetDetails(objectType, name, schema, table);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
private resolvePlanningMode(policy: MigrationOperationPolicy): PlanningMode {
|
|
827
|
-
const allowWidening = policy.allowedOperationClasses.includes('widening');
|
|
828
|
-
const allowDestructive = policy.allowedOperationClasses.includes('destructive');
|
|
829
|
-
// `db init` uses additive-only policy and intentionally ignores extras.
|
|
830
|
-
// Any reconciliation-capable policy should inspect extras to reconcile strict equality.
|
|
831
|
-
const includeExtraObjects = allowWidening || allowDestructive;
|
|
832
|
-
return { includeExtraObjects, allowWidening, allowDestructive };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
private collectSchemaIssues(
|
|
836
|
-
options: PlannerOptionsWithComponents,
|
|
837
|
-
strict: boolean,
|
|
838
|
-
): readonly SchemaIssue[] {
|
|
164
|
+
private collectSchemaIssues(options: PlannerOptionsWithComponents): readonly SchemaIssue[] {
|
|
165
|
+
// `db init` uses additive-only policy and intentionally ignores extra
|
|
166
|
+
// schema objects. Any reconciliation-capable policy (widening or
|
|
167
|
+
// destructive) must inspect extras to reconcile strict equality.
|
|
168
|
+
const allowed = options.policy.allowedOperationClasses;
|
|
169
|
+
const strict = allowed.includes('widening') || allowed.includes('destructive');
|
|
839
170
|
const verifyOptions: VerifySqlSchemaOptionsWithComponents = {
|
|
840
171
|
contract: options.contract,
|
|
841
172
|
schema: options.schema,
|
|
@@ -849,54 +180,3 @@ UNIQUE (${unique.columns.map(quoteIdentifier).join(', ')})`,
|
|
|
849
180
|
return verifyResult.schema.issues;
|
|
850
181
|
}
|
|
851
182
|
}
|
|
852
|
-
|
|
853
|
-
function canUseSharedTemporaryDefaultStrategy(options: {
|
|
854
|
-
readonly table: StorageTable;
|
|
855
|
-
readonly schemaTable: SqlSchemaIR['tables'][string];
|
|
856
|
-
readonly schemaLookup: SchemaTableLookup | undefined;
|
|
857
|
-
readonly columnName: string;
|
|
858
|
-
}): boolean {
|
|
859
|
-
const { table, schemaTable, schemaLookup, columnName } = options;
|
|
860
|
-
|
|
861
|
-
// Shared placeholders are only safe when later plan steps do not require
|
|
862
|
-
// row-specific values for this newly added column.
|
|
863
|
-
if (table.primaryKey?.columns.includes(columnName) && !schemaTable.primaryKey) {
|
|
864
|
-
return false;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
for (const unique of table.uniques) {
|
|
868
|
-
if (!unique.columns.includes(columnName)) {
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
if (!schemaLookup || !hasUniqueConstraint(schemaLookup, unique.columns)) {
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
for (const foreignKey of table.foreignKeys) {
|
|
877
|
-
if (foreignKey.constraint === false || !foreignKey.columns.includes(columnName)) {
|
|
878
|
-
continue;
|
|
879
|
-
}
|
|
880
|
-
if (!schemaLookup || !hasForeignKey(schemaLookup, foreignKey)) {
|
|
881
|
-
return false;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
return true;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function sortDependencies(
|
|
889
|
-
dependencies: ReadonlyArray<PlannerDatabaseDependency>,
|
|
890
|
-
): ReadonlyArray<PlannerDatabaseDependency> {
|
|
891
|
-
return [...dependencies].sort((a, b) => a.id.localeCompare(b.id));
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function isPostgresPlannerDependency(
|
|
895
|
-
dependency: ComponentDatabaseDependency<unknown>,
|
|
896
|
-
): dependency is PlannerDatabaseDependency {
|
|
897
|
-
return dependency.install.every((operation) => operation.target.id === 'postgres');
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function sortedEntries<V>(record: Readonly<Record<string, V>>): Array<[string, V]> {
|
|
901
|
-
return Object.entries(record).sort(([a], [b]) => a.localeCompare(b)) as Array<[string, V]>;
|
|
902
|
-
}
|