json-rest-api 1.0.28 → 2.0.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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +746 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +142 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1069
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -385
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +38 -69
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +39 -82
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -150
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -118
- package/tests/fixtures/api-configs.js +0 -2509
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-cursor-multifield.test.js +0 -643
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
* @file Creates Knex table definitions and migration plans from json-rest-schema definitions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { buildStorageInfo } from './storage/storage-mapping.js'
|
|
5
|
+
import { assertWritableKnexColumns, buildStorageInfo } from './storage/storage-mapping.js'
|
|
6
|
+
import { introspectKnexColumnConstraints } from './dbIntrospection.js'
|
|
7
|
+
import { createKnexTransaction } from '../../../lib/knex-transaction.js'
|
|
8
|
+
import { transactionMethod, withWriteOutcome } from '../../../lib/error-context.js'
|
|
9
|
+
import { assertFieldNameMap } from './querying-writing/field-utils.js'
|
|
10
|
+
import { getSchemaCapabilities } from './querying-writing/database-capabilities.js'
|
|
11
|
+
|
|
12
|
+
const runOwnedTransaction = withWriteOutcome(transactionMethod)
|
|
6
13
|
|
|
7
14
|
const MYSQL_DIALECT_PATTERN = /mysql/i
|
|
8
15
|
|
|
@@ -46,7 +53,8 @@ function formatCodeLiteral (value) {
|
|
|
46
53
|
return quoteJsString(value)
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
if (typeof value === '
|
|
56
|
+
if (typeof value === 'bigint') return `${value}n`
|
|
57
|
+
if (typeof value === 'number') {
|
|
50
58
|
return String(value)
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -94,24 +102,55 @@ function ensureStringArray (value, label) {
|
|
|
94
102
|
return normalized
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
function escapeEnumValues (values, dialect) {
|
|
106
|
+
// Knex's enum compiler interpolates its values without escaping SQL literals.
|
|
107
|
+
return values.map(value => {
|
|
108
|
+
const text = String(value)
|
|
109
|
+
return (isMysqlDialect(dialect) ? text.replace(/\\/g, '\\\\') : text).replace(/'/g, "''")
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function quotePostgresString (value) {
|
|
114
|
+
const text = String(value).replace(/'/g, "''")
|
|
115
|
+
// Keep literal question marks away from Knex's PostgreSQL binding-position pass.
|
|
116
|
+
return text.includes('?') ? `U&'${text.replace(/\\/g, '\\005c').replace(/\?/g, '\\003f')}'` : `'${text}'`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function defaultStringValue (definition) {
|
|
120
|
+
if (!hasStaticDefault(definition)) return null
|
|
121
|
+
if (typeof definition.defaultTo === 'string') return definition.defaultTo
|
|
122
|
+
if (['array', 'object', 'serialize'].includes(definition.type) && definition.defaultTo !== null) return JSON.stringify(definition.defaultTo)
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hasJsonDefault (definition) {
|
|
127
|
+
return ['array', 'object', 'serialize'].includes(definition.type) && hasStaticDefault(definition) && definition.defaultTo !== null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function columnDefaultLiteral (definition, dialect) {
|
|
131
|
+
const text = defaultStringValue(definition)
|
|
132
|
+
if (text === null) return null
|
|
133
|
+
const literal = ['pg', 'postgresql'].includes(dialect) ? quotePostgresString(text) : `'${escapeEnumValues([text], dialect)[0]}'`
|
|
134
|
+
if (hasJsonDefault(definition)) return `(${literal})`.replace(/\?/g, '\\?')
|
|
135
|
+
return ['pg', 'postgresql'].includes(dialect) && text.includes('?') ? literal : null
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
function buildSetColumnType (values = []) {
|
|
98
139
|
const normalized = values.map((value) => normalizeText(value)).filter(Boolean)
|
|
99
140
|
if (normalized.length < 1) {
|
|
100
141
|
throw new Error('setValues must contain at least one value.')
|
|
101
142
|
}
|
|
102
143
|
|
|
103
|
-
return `set(${normalized.map(
|
|
144
|
+
return `set(${escapeEnumValues(normalized, 'mysql2').map(value => `'${value}'`).join(', ')})`
|
|
104
145
|
}
|
|
105
146
|
|
|
106
147
|
function createTableBuilderContext (schemaStructure = {}, idColumn = 'id', storage = undefined) {
|
|
148
|
+
const storageInfo = buildStorageInfo({ schemaStructure, idProperty: idColumn, storage })
|
|
149
|
+
assertWritableKnexColumns(storageInfo)
|
|
107
150
|
return {
|
|
108
151
|
schemaStructure,
|
|
109
152
|
idProperty: idColumn,
|
|
110
|
-
storageInfo
|
|
111
|
-
schemaStructure,
|
|
112
|
-
idProperty: idColumn,
|
|
113
|
-
storage
|
|
114
|
-
})
|
|
153
|
+
storageInfo
|
|
115
154
|
}
|
|
116
155
|
}
|
|
117
156
|
|
|
@@ -288,6 +327,7 @@ function normalizeColumnShape (definition = {}) {
|
|
|
288
327
|
}
|
|
289
328
|
|
|
290
329
|
switch (definition.type) {
|
|
330
|
+
case 'integer':
|
|
291
331
|
case 'id':
|
|
292
332
|
return 'integer'
|
|
293
333
|
case 'number':
|
|
@@ -302,7 +342,7 @@ function normalizeColumnShape (definition = {}) {
|
|
|
302
342
|
return 'time'
|
|
303
343
|
case 'epochMilliseconds':
|
|
304
344
|
case 'epochSeconds':
|
|
305
|
-
return '
|
|
345
|
+
return 'bigint'
|
|
306
346
|
case 'array':
|
|
307
347
|
case 'object':
|
|
308
348
|
case 'serialize':
|
|
@@ -326,6 +366,7 @@ function normalizeDesiredTypeKind (definition = {}) {
|
|
|
326
366
|
}
|
|
327
367
|
|
|
328
368
|
switch (definition.type) {
|
|
369
|
+
case 'integer':
|
|
329
370
|
case 'id':
|
|
330
371
|
return 'integer'
|
|
331
372
|
case 'number':
|
|
@@ -355,11 +396,19 @@ function normalizeDesiredTypeKind (definition = {}) {
|
|
|
355
396
|
}
|
|
356
397
|
}
|
|
357
398
|
|
|
358
|
-
function normalizeComparableDefault (value) {
|
|
399
|
+
function normalizeComparableDefault (value, type) {
|
|
359
400
|
if (value === undefined) return undefined
|
|
360
401
|
if (typeof value === 'boolean') return value ? '1' : '0'
|
|
361
402
|
if (value === null) return null
|
|
362
403
|
if (typeof value === 'object') return JSON.stringify(value)
|
|
404
|
+
if (['number', 'integer', 'id', 'epochMilliseconds', 'epochSeconds'].includes(type)) {
|
|
405
|
+
const parts = /^(-?)(\d+)(?:\.(\d+))?$/.exec(String(value))
|
|
406
|
+
if (parts) {
|
|
407
|
+
const whole = parts[2].replace(/^0+(?=\d)/, '')
|
|
408
|
+
const fraction = (parts[3] || '').replace(/0+$/, '')
|
|
409
|
+
return `${whole === '0' && !fraction ? '' : parts[1]}${whole}${fraction ? `.${fraction}` : ''}`
|
|
410
|
+
}
|
|
411
|
+
}
|
|
363
412
|
return String(value)
|
|
364
413
|
}
|
|
365
414
|
|
|
@@ -381,15 +430,15 @@ function normalizeDesiredColumn (tableContext, fieldName, definition, options =
|
|
|
381
430
|
typeKind: normalizeDesiredTypeKind(definition),
|
|
382
431
|
nullable,
|
|
383
432
|
required: definition.required === true,
|
|
384
|
-
hasDefault: hasStaticDefault(definition),
|
|
385
|
-
defaultValue: hasStaticDefault(definition)
|
|
386
|
-
? normalizeComparableDefault(definition.defaultTo)
|
|
433
|
+
hasDefault: hasStaticDefault(definition) && definition.defaultTo !== null,
|
|
434
|
+
defaultValue: hasStaticDefault(definition) && definition.defaultTo !== null
|
|
435
|
+
? normalizeComparableDefault(definition.defaultTo, definition.type)
|
|
387
436
|
: undefined,
|
|
388
437
|
unsigned: definition.type === 'id' ? definition.unsigned !== false : definition.unsigned === true,
|
|
389
|
-
maxLength: definition.maxLength
|
|
438
|
+
maxLength: shape === 'string' ? (definition.maxLength || 255) : null,
|
|
390
439
|
numericPrecision: definition.precision ?? null,
|
|
391
440
|
numericScale: definition.scale ?? null,
|
|
392
|
-
datetimePrecision: definition.temporalPrecision ?? null,
|
|
441
|
+
datetimePrecision: ['datetime', 'time'].includes(shape) ? (definition.temporalPrecision ?? 6) : null,
|
|
393
442
|
enumValues: Array.isArray(definition.enum) ? definition.enum.map((value) => String(value)) : [],
|
|
394
443
|
setValues: Array.isArray(definition.setValues) ? definition.setValues.map((value) => String(value)) : [],
|
|
395
444
|
autoIncrement: options.autoIncrement === true
|
|
@@ -406,21 +455,57 @@ function buildImplicitIdDefinition (idColumn) {
|
|
|
406
455
|
}
|
|
407
456
|
}
|
|
408
457
|
|
|
458
|
+
function assertNativeSetSupport ({ schemaStructure }, dialect) {
|
|
459
|
+
if (getSchemaCapabilities(dialect).setValues) return
|
|
460
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
461
|
+
if (Array.isArray(definition?.setValues) && definition.setValues.length > 0) {
|
|
462
|
+
throw new Error(`Field '${fieldName}' uses setValues, which requires a MySQL-compatible knex client.`)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
409
467
|
function resolveTableSchemaContext (schemaLike, options = {}) {
|
|
410
|
-
const schemaStructure = schemaLike?.structure
|
|
468
|
+
const schemaStructure = schemaLike?.structure
|
|
469
|
+
if (!schemaStructure || typeof schemaStructure !== 'object' || Array.isArray(schemaStructure)) {
|
|
470
|
+
throw new Error('Table schema must provide a structure field map. Wrap field definitions as { structure: fields }.')
|
|
471
|
+
}
|
|
472
|
+
assertFieldNameMap(schemaStructure, 'table schema structure')
|
|
473
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
474
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
|
|
475
|
+
throw new Error(`Field '${fieldName}' must have an object definition. Wrap bare field maps as { structure: fields }.`)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const dialect = normalizeText(options.dialect).toLowerCase()
|
|
479
|
+
const { temporalColumnFractionDigits } = getSchemaCapabilities(dialect)
|
|
480
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
481
|
+
if (!['time', 'dateTime'].includes(definition?.type)) continue
|
|
482
|
+
const precision = definition.temporalPrecision ?? 6
|
|
483
|
+
if (!Number.isInteger(precision) || precision < 0) {
|
|
484
|
+
throw new Error(`Field '${fieldName}' temporalPrecision must be a non-negative integer.`)
|
|
485
|
+
}
|
|
486
|
+
if (temporalColumnFractionDigits !== null && precision > temporalColumnFractionDigits) {
|
|
487
|
+
throw new Error(`Field '${fieldName}' temporalPrecision exceeds the native maximum of ${temporalColumnFractionDigits} for '${dialect}'. Use a text column with an appropriate storage mapping for finer precision.`)
|
|
488
|
+
}
|
|
489
|
+
if (!dialect && precision > 6) {
|
|
490
|
+
throw new Error(`Field '${fieldName}' temporalPrecision above 6 requires an explicit migration dialect.`)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
411
493
|
const configuredIdColumn = options.idProperty || schemaStructure.id?.storage?.column || 'id'
|
|
412
494
|
const storage = options.storage ?? schemaLike?.storage
|
|
413
495
|
const tableContext = createTableBuilderContext(schemaStructure, configuredIdColumn, storage)
|
|
414
496
|
|
|
415
497
|
let resourceIdField = null
|
|
416
|
-
for (const
|
|
417
|
-
if (definition?.type !== 'id') continue
|
|
498
|
+
for (const fieldName of Object.keys(schemaStructure)) {
|
|
418
499
|
const columnName = getColumnNameForDefinition(tableContext, fieldName)
|
|
419
500
|
if (columnName === configuredIdColumn) {
|
|
420
501
|
resourceIdField = fieldName
|
|
421
502
|
break
|
|
422
503
|
}
|
|
423
504
|
}
|
|
505
|
+
const resourceIdDefinition = resourceIdField ? schemaStructure[resourceIdField] : null
|
|
506
|
+
if (resourceIdDefinition && resourceIdDefinition.primary !== true && options.autoIncrement !== false && !['id', 'integer'].includes(resourceIdDefinition.type)) {
|
|
507
|
+
throw new Error(`Cannot create an implicit auto-increment column for '${resourceIdDefinition.type}' resource ID '${resourceIdField}'. Declare primary: true for application-supplied IDs, or set autoIncrement: false.`)
|
|
508
|
+
}
|
|
424
509
|
|
|
425
510
|
const implicitIndexes = normalizeFieldIndexes(schemaStructure, options.tableName || 'table', tableContext)
|
|
426
511
|
const explicitIndexes = normalizeTopLevelIndexes(schemaLike, options.tableName || 'table', tableContext)
|
|
@@ -432,9 +517,7 @@ function resolveTableSchemaContext (schemaLike, options = {}) {
|
|
|
432
517
|
schemaStructure,
|
|
433
518
|
idColumn: configuredIdColumn,
|
|
434
519
|
resourceIdField,
|
|
435
|
-
hasPrimaryIdField:
|
|
436
|
-
? schemaStructure[resourceIdField]?.primary === true
|
|
437
|
-
: false,
|
|
520
|
+
hasPrimaryIdField: resourceIdDefinition?.primary === true,
|
|
438
521
|
tableContext,
|
|
439
522
|
indexes: dedupeBySignature(
|
|
440
523
|
[...implicitIndexes, ...explicitIndexes],
|
|
@@ -454,6 +537,12 @@ function resolveTableSchemaContext (schemaLike, options = {}) {
|
|
|
454
537
|
}
|
|
455
538
|
}
|
|
456
539
|
|
|
540
|
+
function timeColumnType (definition) {
|
|
541
|
+
const precision = definition.temporalPrecision ?? 6
|
|
542
|
+
if (!Number.isInteger(precision) || precision < 0) throw new Error('Time column precision must be a non-negative integer.')
|
|
543
|
+
return `time(${precision})`
|
|
544
|
+
}
|
|
545
|
+
|
|
457
546
|
function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
458
547
|
const {
|
|
459
548
|
precision,
|
|
@@ -467,16 +556,15 @@ function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
|
467
556
|
const dialect = normalizeText(options.dialect).toLowerCase()
|
|
468
557
|
|
|
469
558
|
if (setValues.length > 0) {
|
|
470
|
-
if (!isMysqlDialect(dialect)) {
|
|
471
|
-
throw new Error(`Field '${columnName}' uses setValues, which requires a MySQL-compatible knex client.`)
|
|
472
|
-
}
|
|
473
559
|
return table.specificType(columnName, buildSetColumnType(setValues))
|
|
474
560
|
}
|
|
475
561
|
|
|
476
562
|
switch (definition.type) {
|
|
477
563
|
case 'string':
|
|
478
564
|
if (enumValues.length > 0) {
|
|
479
|
-
return table.
|
|
565
|
+
if (options.alter === true && (['pg', 'postgresql'].includes(dialect) || isSqliteDialectName(dialect))) return table.text(columnName)
|
|
566
|
+
if (['pg', 'postgresql'].includes(dialect)) return table.specificType(columnName, `text check (${buildEnumCheckClause(columnName, enumValues, dialect)})`)
|
|
567
|
+
return table.enu(columnName, escapeEnumValues(enumValues, dialect))
|
|
480
568
|
}
|
|
481
569
|
return maxLength ? table.string(columnName, maxLength) : table.string(columnName)
|
|
482
570
|
|
|
@@ -486,6 +574,9 @@ function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
|
486
574
|
}
|
|
487
575
|
return table.float(columnName)
|
|
488
576
|
|
|
577
|
+
case 'integer':
|
|
578
|
+
return unsigned === true ? table.integer(columnName).unsigned() : table.integer(columnName)
|
|
579
|
+
|
|
489
580
|
case 'id': {
|
|
490
581
|
const col = table.integer(columnName)
|
|
491
582
|
return unsigned !== false ? col.unsigned() : col
|
|
@@ -498,14 +589,11 @@ function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
|
498
589
|
return table.date(columnName)
|
|
499
590
|
|
|
500
591
|
case 'dateTime':
|
|
501
|
-
return temporalPrecision
|
|
502
|
-
? table.datetime(columnName, { precision: temporalPrecision })
|
|
503
|
-
: table.datetime(columnName)
|
|
592
|
+
return table.datetime(columnName, { precision: temporalPrecision ?? 6 })
|
|
504
593
|
|
|
505
594
|
case 'time':
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
: table.time(columnName)
|
|
595
|
+
// Knex's PostgreSQL time builder ignores its precision argument.
|
|
596
|
+
return table.specificType(columnName, timeColumnType(definition))
|
|
509
597
|
|
|
510
598
|
case 'epochMilliseconds':
|
|
511
599
|
case 'epochSeconds':
|
|
@@ -514,6 +602,7 @@ function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
|
514
602
|
case 'array':
|
|
515
603
|
case 'object':
|
|
516
604
|
case 'serialize':
|
|
605
|
+
if (hasJsonDefault(definition)) return table.specificType(columnName, 'json')
|
|
517
606
|
return table.json(columnName)
|
|
518
607
|
|
|
519
608
|
case 'blob':
|
|
@@ -526,7 +615,7 @@ function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
|
526
615
|
}
|
|
527
616
|
}
|
|
528
617
|
|
|
529
|
-
function applyColumnConstraints (column, definition) {
|
|
618
|
+
function applyColumnConstraints (column, definition, knex, dialect) {
|
|
530
619
|
if (definition.nullable === true) {
|
|
531
620
|
column.nullable()
|
|
532
621
|
} else if (definition.required === true || definition.nullable === false) {
|
|
@@ -534,7 +623,8 @@ function applyColumnConstraints (column, definition) {
|
|
|
534
623
|
}
|
|
535
624
|
|
|
536
625
|
if (hasStaticDefault(definition)) {
|
|
537
|
-
|
|
626
|
+
const literal = columnDefaultLiteral(definition, dialect)
|
|
627
|
+
column.defaultTo(literal === null ? definition.defaultTo : knex.raw(literal))
|
|
538
628
|
}
|
|
539
629
|
|
|
540
630
|
if (definition.primary === true) {
|
|
@@ -546,7 +636,7 @@ function applyColumnConstraints (column, definition) {
|
|
|
546
636
|
}
|
|
547
637
|
}
|
|
548
638
|
|
|
549
|
-
function applyTableMetadata (table, tableSchemaContext) {
|
|
639
|
+
function applyTableMetadata (table, tableSchemaContext, knex) {
|
|
550
640
|
for (const index of tableSchemaContext.indexes) {
|
|
551
641
|
if (index.unique) {
|
|
552
642
|
table.unique(index.columns, index.name)
|
|
@@ -574,7 +664,7 @@ function applyTableMetadata (table, tableSchemaContext) {
|
|
|
574
664
|
}
|
|
575
665
|
|
|
576
666
|
for (const constraint of tableSchemaContext.checkConstraints) {
|
|
577
|
-
table.check(constraint.clause, [], constraint.name)
|
|
667
|
+
table.check(constraint.clause, [], knex.raw('??', [constraint.name]).toQuery())
|
|
578
668
|
}
|
|
579
669
|
}
|
|
580
670
|
|
|
@@ -582,18 +672,28 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
582
672
|
const dialect = normalizeText(options.dialect).toLowerCase()
|
|
583
673
|
const enumValues = Array.isArray(definition.enum) ? definition.enum : []
|
|
584
674
|
const setValues = Array.isArray(definition.setValues) ? definition.setValues : []
|
|
675
|
+
if (!dialect && (enumValues.some(value => /[\\?]/.test(String(value))) || defaultStringValue(definition)?.includes('?') || (hasJsonDefault(definition) && /[\\']/.test(defaultStringValue(definition))))) {
|
|
676
|
+
throw new Error(`Field '${columnName}' requires an explicit migration dialect for SQL-sensitive enum/default values.`)
|
|
677
|
+
}
|
|
585
678
|
let line = ''
|
|
586
679
|
|
|
587
680
|
if (setValues.length > 0) {
|
|
588
|
-
if (
|
|
681
|
+
if (!dialect) {
|
|
682
|
+
throw new Error(`Field '${columnName}' uses setValues and requires an explicit migration dialect.`)
|
|
683
|
+
}
|
|
684
|
+
if (!isMysqlDialect(dialect)) {
|
|
589
685
|
throw new Error(`Field '${columnName}' uses setValues, which requires a MySQL-compatible migration target.`)
|
|
590
686
|
}
|
|
591
687
|
line = `table.specificType(${quoteJsString(columnName)}, ${quoteJsString(buildSetColumnType(setValues))})`
|
|
592
688
|
} else {
|
|
593
689
|
switch (definition.type) {
|
|
594
690
|
case 'string':
|
|
595
|
-
if (enumValues.length > 0) {
|
|
596
|
-
line = `table.
|
|
691
|
+
if (enumValues.length > 0 && options.alter === true && (['pg', 'postgresql'].includes(dialect) || isSqliteDialectName(dialect))) {
|
|
692
|
+
line = `table.text(${quoteJsString(columnName)})`
|
|
693
|
+
} else if (enumValues.length > 0 && ['pg', 'postgresql'].includes(dialect)) {
|
|
694
|
+
line = `table.specificType(${quoteJsString(columnName)}, ${quoteJsString(`text check (${buildEnumCheckClause(columnName, enumValues, dialect)})`)})`
|
|
695
|
+
} else if (enumValues.length > 0) {
|
|
696
|
+
line = `table.enu(${quoteJsString(columnName)}, ${formatArrayLiteral(escapeEnumValues(enumValues, dialect))})`
|
|
597
697
|
} else {
|
|
598
698
|
line = definition.maxLength
|
|
599
699
|
? `table.string(${quoteJsString(columnName)}, ${definition.maxLength})`
|
|
@@ -609,6 +709,11 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
609
709
|
}
|
|
610
710
|
break
|
|
611
711
|
|
|
712
|
+
case 'integer':
|
|
713
|
+
line = `table.integer(${quoteJsString(columnName)})`
|
|
714
|
+
if (definition.unsigned === true) line += '.unsigned()'
|
|
715
|
+
break
|
|
716
|
+
|
|
612
717
|
case 'id':
|
|
613
718
|
line = `table.integer(${quoteJsString(columnName)})`
|
|
614
719
|
if (definition.unsigned !== false) {
|
|
@@ -625,19 +730,11 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
625
730
|
break
|
|
626
731
|
|
|
627
732
|
case 'dateTime':
|
|
628
|
-
|
|
629
|
-
line = `table.datetime(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision} })`
|
|
630
|
-
} else {
|
|
631
|
-
line = `table.datetime(${quoteJsString(columnName)})`
|
|
632
|
-
}
|
|
733
|
+
line = `table.datetime(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision ?? 6} })`
|
|
633
734
|
break
|
|
634
735
|
|
|
635
736
|
case 'time':
|
|
636
|
-
|
|
637
|
-
line = `table.time(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision} })`
|
|
638
|
-
} else {
|
|
639
|
-
line = `table.time(${quoteJsString(columnName)})`
|
|
640
|
-
}
|
|
737
|
+
line = `table.specificType(${quoteJsString(columnName)}, ${quoteJsString(timeColumnType(definition))})`
|
|
641
738
|
break
|
|
642
739
|
|
|
643
740
|
case 'epochMilliseconds':
|
|
@@ -648,7 +745,7 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
648
745
|
case 'array':
|
|
649
746
|
case 'object':
|
|
650
747
|
case 'serialize':
|
|
651
|
-
line = `table.json(${quoteJsString(columnName)})`
|
|
748
|
+
line = hasJsonDefault(definition) ? `table.specificType(${quoteJsString(columnName)}, 'json')` : `table.json(${quoteJsString(columnName)})`
|
|
652
749
|
break
|
|
653
750
|
|
|
654
751
|
case 'blob':
|
|
@@ -669,7 +766,8 @@ function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
|
669
766
|
}
|
|
670
767
|
|
|
671
768
|
if (hasStaticDefault(definition)) {
|
|
672
|
-
|
|
769
|
+
const literal = columnDefaultLiteral(definition, dialect)
|
|
770
|
+
line += `.defaultTo(${literal === null ? formatCodeLiteral(definition.defaultTo) : `knex.raw(${quoteJsString(literal)})`})`
|
|
673
771
|
}
|
|
674
772
|
|
|
675
773
|
if (definition.primary === true && options.includePrimary !== false) {
|
|
@@ -716,7 +814,7 @@ function buildForeignKeyLine (foreignKey) {
|
|
|
716
814
|
}
|
|
717
815
|
|
|
718
816
|
function buildCheckConstraintLine (constraint) {
|
|
719
|
-
return `table.check(${quoteJsString(constraint.clause)}, [], ${quoteJsString(constraint.name)})`
|
|
817
|
+
return `table.check(${quoteJsString(constraint.clause)}, [], knex.raw('??', [${quoteJsString(constraint.name)}]).toQuery())`
|
|
720
818
|
}
|
|
721
819
|
|
|
722
820
|
function buildDesiredColumnsMap (tableSchemaContext, options = {}) {
|
|
@@ -755,10 +853,14 @@ function valuesEqual (left, right) {
|
|
|
755
853
|
|
|
756
854
|
function normalizeSnapshotDefaultValue (column = {}) {
|
|
757
855
|
if (!column.hasDefault) return undefined
|
|
758
|
-
|
|
856
|
+
if (column.typeKind === 'boolean') {
|
|
857
|
+
if (String(column.defaultValue).toLowerCase() === 'true') return '1'
|
|
858
|
+
if (String(column.defaultValue).toLowerCase() === 'false') return '0'
|
|
859
|
+
}
|
|
860
|
+
return normalizeComparableDefault(column.defaultValue, column.typeKind)
|
|
759
861
|
}
|
|
760
862
|
|
|
761
|
-
function collectDestructiveColumnWarnings (currentColumn, desiredColumn) {
|
|
863
|
+
function collectDestructiveColumnWarnings (currentColumn, desiredColumn, dialect) {
|
|
762
864
|
const warnings = []
|
|
763
865
|
const columnName = desiredColumn.name
|
|
764
866
|
|
|
@@ -789,6 +891,9 @@ function collectDestructiveColumnWarnings (currentColumn, desiredColumn) {
|
|
|
789
891
|
) {
|
|
790
892
|
warnings.push(`Column '${columnName}' reduces numeric scale from ${currentColumn.numericScale} to ${desiredColumn.numericScale}.`)
|
|
791
893
|
}
|
|
894
|
+
if (currentColumn.datetimePrecision != null && desiredColumn.datetimePrecision != null && desiredColumn.datetimePrecision < currentColumn.datetimePrecision) {
|
|
895
|
+
warnings.push(`Column '${columnName}' reduces temporal precision from ${currentColumn.datetimePrecision} to ${desiredColumn.datetimePrecision}.`)
|
|
896
|
+
}
|
|
792
897
|
|
|
793
898
|
if (!valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues || [])) {
|
|
794
899
|
warnings.push(`Column '${columnName}' changes enum values and may invalidate existing data.`)
|
|
@@ -801,6 +906,9 @@ function collectDestructiveColumnWarnings (currentColumn, desiredColumn) {
|
|
|
801
906
|
if (normalizeText(currentColumn.typeKind) !== normalizeText(desiredColumn.typeKind)) {
|
|
802
907
|
warnings.push(`Column '${columnName}' changes type semantics from '${currentColumn.typeKind}' to '${desiredColumn.typeKind}'.`)
|
|
803
908
|
}
|
|
909
|
+
if (!isSqliteDialectName(dialect) && currentColumn.dataType === 'bigint' && desiredColumn.shape === 'integer') {
|
|
910
|
+
warnings.push(`Column '${columnName}' narrows bigint to integer and may overflow existing values.`)
|
|
911
|
+
}
|
|
804
912
|
|
|
805
913
|
return warnings
|
|
806
914
|
}
|
|
@@ -809,19 +917,22 @@ function columnNeedsAlter (currentColumn, desiredColumn, options = {}) {
|
|
|
809
917
|
const currentNumericShape = (() => {
|
|
810
918
|
const dataType = normalizeText(currentColumn.dataType)
|
|
811
919
|
if (dataType === 'decimal' || dataType === 'numeric') return 'decimal'
|
|
812
|
-
if (
|
|
920
|
+
if (['float', 'double', 'double precision', 'real'].includes(dataType)) return 'float'
|
|
813
921
|
return ''
|
|
814
922
|
})()
|
|
815
923
|
const compareUnsigned = isMysqlDialect(normalizeText(options.dialect).toLowerCase())
|
|
924
|
+
const sqlite = isSqliteDialectName(options.dialect)
|
|
816
925
|
|
|
817
926
|
return (
|
|
818
927
|
normalizeText(currentColumn.typeKind) !== normalizeText(desiredColumn.typeKind) ||
|
|
819
|
-
(desiredColumn.typeKind === 'number' && currentNumericShape && currentNumericShape !== normalizeText(desiredColumn.shape)) ||
|
|
928
|
+
(!sqlite && desiredColumn.typeKind === 'number' && currentNumericShape && currentNumericShape !== normalizeText(desiredColumn.shape)) ||
|
|
929
|
+
(desiredColumn.typeKind === 'integer' && (currentColumn.dataType === 'bigint') !== (desiredColumn.shape === 'bigint')) ||
|
|
820
930
|
Boolean(currentColumn.nullable) !== Boolean(desiredColumn.nullable) ||
|
|
821
931
|
(compareUnsigned && Boolean(currentColumn.unsigned) !== Boolean(desiredColumn.unsigned)) ||
|
|
822
|
-
(currentColumn.maxLength ?? null) !==
|
|
823
|
-
(currentColumn.numericPrecision ?? null) !==
|
|
824
|
-
(currentColumn.numericScale ?? null) !==
|
|
932
|
+
(desiredColumn.shape === 'string' && (currentColumn.maxLength ?? null) !== desiredColumn.maxLength) ||
|
|
933
|
+
(!sqlite && desiredColumn.shape === 'decimal' && (currentColumn.numericPrecision ?? null) !== desiredColumn.numericPrecision) ||
|
|
934
|
+
(!sqlite && desiredColumn.shape === 'decimal' && (currentColumn.numericScale ?? null) !== desiredColumn.numericScale) ||
|
|
935
|
+
(!sqlite && ['datetime', 'time'].includes(desiredColumn.shape) && (currentColumn.datetimePrecision ?? null) !== desiredColumn.datetimePrecision) ||
|
|
825
936
|
normalizeSnapshotDefaultValue(currentColumn) !== desiredColumn.defaultValue ||
|
|
826
937
|
Boolean(currentColumn.hasDefault) !== Boolean(desiredColumn.hasDefault) ||
|
|
827
938
|
!valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues || []) ||
|
|
@@ -843,6 +954,7 @@ function createEmptyDiffPlan () {
|
|
|
843
954
|
addForeignKeys: [],
|
|
844
955
|
dropForeignKeys: [],
|
|
845
956
|
addCheckConstraints: [],
|
|
957
|
+
dropCheckConstraints: [],
|
|
846
958
|
warnings: []
|
|
847
959
|
}
|
|
848
960
|
}
|
|
@@ -866,13 +978,22 @@ function isSqliteDialectName (dialect = '') {
|
|
|
866
978
|
}
|
|
867
979
|
|
|
868
980
|
function normalizeConstraintClause (clause = '') {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
981
|
+
const tokens = normalizeText(clause).match(/'(?:''|[^'])*'|`(?:``|[^`])*`|"(?:""|[^"])*"|[\w.]+|<>|!=|>=|<=|[^\s]/g) || []
|
|
982
|
+
while (tokens[0] === '(' && tokens.at(-1) === ')') {
|
|
983
|
+
let depth = 0
|
|
984
|
+
if (tokens.some((token, index) => {
|
|
985
|
+
if (token === '(') depth++
|
|
986
|
+
if (token === ')') depth--
|
|
987
|
+
return depth === 0 && index < tokens.length - 1
|
|
988
|
+
})) break
|
|
989
|
+
tokens.shift()
|
|
990
|
+
tokens.pop()
|
|
991
|
+
}
|
|
992
|
+
return tokens.map(token => {
|
|
993
|
+
if (token.startsWith("'")) return token
|
|
994
|
+
if (token.startsWith('"') || token.startsWith('`')) return token.slice(1, -1)
|
|
995
|
+
return token.toLowerCase()
|
|
996
|
+
}).join(' ')
|
|
876
997
|
}
|
|
877
998
|
|
|
878
999
|
function isInlineEnumCheckConstraint (currentCheck = {}, desiredColumn = {}) {
|
|
@@ -880,15 +1001,41 @@ function isInlineEnumCheckConstraint (currentCheck = {}, desiredColumn = {}) {
|
|
|
880
1001
|
return false
|
|
881
1002
|
}
|
|
882
1003
|
|
|
883
|
-
const expectedClause =
|
|
1004
|
+
const expectedClause = buildEnumCheckClause(desiredColumn.name, desiredColumn.enumValues)
|
|
884
1005
|
return normalizeConstraintClause(currentCheck.clause) === normalizeConstraintClause(expectedClause)
|
|
885
1006
|
}
|
|
886
1007
|
|
|
1008
|
+
function buildEnumCheckClause (columnName, enumValues, dialect = '') {
|
|
1009
|
+
const values = ['pg', 'postgresql'].includes(dialect)
|
|
1010
|
+
? enumValues.map(quotePostgresString)
|
|
1011
|
+
: escapeEnumValues(enumValues, 'pg').map(value => `'${value}'`)
|
|
1012
|
+
return `"${columnName.replace(/"/g, '""')}" in (${values.join(', ')})`
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function planEnumAlteration (plan, tableName, currentColumn, desiredColumn, currentChecks, dialect) {
|
|
1016
|
+
if (valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues)) return true
|
|
1017
|
+
if (isSqliteDialectName(dialect)) {
|
|
1018
|
+
plan.warnings.push(`Skipping enum alteration for column '${desiredColumn.name}': SQLite requires a table rebuild that replaces the old check constraint. Knex column ALTER retains that check.`)
|
|
1019
|
+
return false
|
|
1020
|
+
}
|
|
1021
|
+
if (['pg', 'postgresql'].includes(dialect)) {
|
|
1022
|
+
const previousChecks = currentChecks.filter(check => isInlineEnumCheckConstraint(check, currentColumn))
|
|
1023
|
+
plan.dropCheckConstraints.push(...previousChecks)
|
|
1024
|
+
if (desiredColumn.enumValues.length > 0) {
|
|
1025
|
+
plan.addCheckConstraints.push({
|
|
1026
|
+
name: previousChecks[0]?.name || `${tableName.split('.').at(-1)}_${desiredColumn.name}_check`,
|
|
1027
|
+
clause: buildEnumCheckClause(desiredColumn.name, desiredColumn.enumValues, dialect)
|
|
1028
|
+
})
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return true
|
|
1032
|
+
}
|
|
1033
|
+
|
|
887
1034
|
/**
|
|
888
1035
|
* Creates a Knex table from a json-rest-schema definition
|
|
889
1036
|
* @param {object} knex - The Knex instance
|
|
890
1037
|
* @param {object} schemaInfo - Resource schema metadata
|
|
891
|
-
* @param {object} tableSchemaInstance -
|
|
1038
|
+
* @param {object} tableSchemaInstance - A json-rest-schema instance or { structure: fields, ...metadata } object
|
|
892
1039
|
* @param {object} [options={}] - Additional options
|
|
893
1040
|
* @param {boolean} [options.autoIncrement=true] - Whether to use auto-incrementing IDs
|
|
894
1041
|
* @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
|
|
@@ -899,10 +1046,13 @@ export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, op
|
|
|
899
1046
|
const tableName = schemaInfo.tableName
|
|
900
1047
|
const dialect = detectKnexDialect(knex)
|
|
901
1048
|
const tableSchemaContext = resolveTableSchemaContext(tableSchemaInstance, {
|
|
1049
|
+
autoIncrement,
|
|
902
1050
|
idProperty: schemaInfo.idProperty,
|
|
903
1051
|
storage: schemaInfo.storage,
|
|
904
|
-
tableName
|
|
1052
|
+
tableName,
|
|
1053
|
+
dialect
|
|
905
1054
|
})
|
|
1055
|
+
assertNativeSetSupport(tableSchemaContext, dialect)
|
|
906
1056
|
|
|
907
1057
|
return knex.schema.createTable(tableName, (table) => {
|
|
908
1058
|
if (!tableSchemaContext.hasPrimaryIdField && autoIncrement) {
|
|
@@ -916,10 +1066,10 @@ export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, op
|
|
|
916
1066
|
|
|
917
1067
|
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
918
1068
|
const column = mapTypeToKnex(table, columnName, definition, { dialect })
|
|
919
|
-
applyColumnConstraints(column, definition)
|
|
1069
|
+
applyColumnConstraints(column, definition, knex, dialect)
|
|
920
1070
|
}
|
|
921
1071
|
|
|
922
|
-
applyTableMetadata(table, tableSchemaContext)
|
|
1072
|
+
applyTableMetadata(table, tableSchemaContext, knex)
|
|
923
1073
|
|
|
924
1074
|
if (timestamps) {
|
|
925
1075
|
table.timestamps(true, true)
|
|
@@ -933,42 +1083,125 @@ export async function addKnexFields (knex, tableName, schema, options = {}) {
|
|
|
933
1083
|
const tableSchemaContext = resolveTableSchemaContext(schema, {
|
|
934
1084
|
storage: options.storage,
|
|
935
1085
|
...options,
|
|
936
|
-
tableName
|
|
1086
|
+
tableName,
|
|
1087
|
+
dialect
|
|
937
1088
|
})
|
|
1089
|
+
assertNativeSetSupport(tableSchemaContext, dialect)
|
|
938
1090
|
|
|
939
1091
|
return knex.schema.alterTable(tableName, (table) => {
|
|
940
1092
|
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
941
1093
|
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
942
1094
|
const column = mapTypeToKnex(table, columnName, definition, { dialect })
|
|
943
|
-
applyColumnConstraints(column, definition)
|
|
1095
|
+
applyColumnConstraints(column, definition, knex, dialect)
|
|
944
1096
|
}
|
|
945
1097
|
})
|
|
946
1098
|
}
|
|
947
1099
|
|
|
1100
|
+
async function runSqliteAlteration (knex, execute) {
|
|
1101
|
+
const connection = await knex.client.acquireConnection()
|
|
1102
|
+
const db = {
|
|
1103
|
+
client: knex.client,
|
|
1104
|
+
raw: (...args) => knex.raw(...args).connection(connection),
|
|
1105
|
+
get schema () { return knex.schema.connection(connection) }
|
|
1106
|
+
}
|
|
1107
|
+
let foreignKeys
|
|
1108
|
+
let result
|
|
1109
|
+
const errors = []
|
|
1110
|
+
try {
|
|
1111
|
+
foreignKeys = (await db.raw('PRAGMA foreign_keys'))[0].foreign_keys
|
|
1112
|
+
result = await execute(db)
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
errors.push(error)
|
|
1115
|
+
try {
|
|
1116
|
+
// A rejected rebuild COMMIT can leave its transaction and FK setting active.
|
|
1117
|
+
if (connection.inTransaction === true) await db.raw('ROLLBACK')
|
|
1118
|
+
if (connection.inTransaction !== false || foreignKeys === undefined) {
|
|
1119
|
+
connection.__knex__disposed = error || true
|
|
1120
|
+
} else {
|
|
1121
|
+
await db.raw(`PRAGMA foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`)
|
|
1122
|
+
}
|
|
1123
|
+
} catch (cleanupError) {
|
|
1124
|
+
connection.__knex__disposed = cleanupError || true
|
|
1125
|
+
errors.push(cleanupError)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
await knex.client.releaseConnection(connection)
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
errors.push(error)
|
|
1132
|
+
}
|
|
1133
|
+
if (errors.length > 1) {
|
|
1134
|
+
throw new AggregateError(errors, 'SQLite alteration and connection cleanup failed', { cause: errors[0] })
|
|
1135
|
+
}
|
|
1136
|
+
if (errors.length) throw errors[0]
|
|
1137
|
+
return result
|
|
1138
|
+
}
|
|
1139
|
+
|
|
948
1140
|
// Helper function to alter multiple fields in an existing table
|
|
949
1141
|
export async function alterKnexFields (knex, tableName, fields, options = {}) {
|
|
950
1142
|
assertNoTopLevelTableMetadata(fields, 'alterKnexFields')
|
|
951
1143
|
const dialect = detectKnexDialect(knex)
|
|
1144
|
+
const capabilities = getSchemaCapabilities(dialect)
|
|
952
1145
|
const tableSchemaContext = resolveTableSchemaContext(fields, {
|
|
953
1146
|
storage: options.storage,
|
|
954
1147
|
...options,
|
|
955
|
-
tableName
|
|
1148
|
+
tableName,
|
|
1149
|
+
dialect
|
|
956
1150
|
})
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1151
|
+
assertNativeSetSupport(tableSchemaContext, dialect)
|
|
1152
|
+
|
|
1153
|
+
const execute = async db => {
|
|
1154
|
+
const plan = createEmptyDiffPlan()
|
|
1155
|
+
if (isSqliteDialectName(dialect) || ['pg', 'postgresql'].includes(dialect)) {
|
|
1156
|
+
const current = await introspectKnexColumnConstraints(db, tableName)
|
|
1157
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
1158
|
+
const desired = normalizeDesiredColumn(tableSchemaContext.tableContext, fieldName, definition)
|
|
1159
|
+
const previous = current.columns.find(column => column.name === desired.name)
|
|
1160
|
+
if (!previous) throw new Error(`Cannot alter missing column '${desired.name}' in '${tableName}'.`)
|
|
1161
|
+
if (!planEnumAlteration(plan, tableName, previous, desired, current.checkConstraints, dialect)) {
|
|
1162
|
+
throw new Error(plan.warnings.at(-1))
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (const check of plan.dropCheckConstraints) {
|
|
1167
|
+
await db.raw('ALTER TABLE ?? DROP CONSTRAINT ??', [tableName, check.name])
|
|
1168
|
+
}
|
|
1169
|
+
await db.schema.alterTable(tableName, table => {
|
|
1170
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
1171
|
+
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
1172
|
+
const column = mapTypeToKnex(table, columnName, definition, { dialect, alter: true })
|
|
1173
|
+
column.alter()
|
|
1174
|
+
applyColumnConstraints(column, definition, db, dialect)
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
for (const check of plan.addCheckConstraints) {
|
|
1178
|
+
await db.schema.alterTable(tableName, table => table.check(check.clause, [], db.raw('??', [check.name]).toQuery()))
|
|
964
1179
|
}
|
|
1180
|
+
}
|
|
1181
|
+
if (capabilities.fieldAlteration === 'standalone') {
|
|
1182
|
+
if (knex.isTransaction) throw new Error('MySQL field alterations cannot run in a caller transaction because DDL implicitly commits it.')
|
|
1183
|
+
return execute(knex)
|
|
1184
|
+
}
|
|
1185
|
+
if (capabilities.fieldAlteration === 'sqlite-rebuild') {
|
|
1186
|
+
if (knex.isTransaction && (await knex.raw('PRAGMA foreign_keys'))[0].foreign_keys === 1) {
|
|
1187
|
+
throw new Error('SQLite field alterations in a caller transaction require foreign_keys=OFF before that transaction starts. A rebuild cannot disable foreign keys inside a transaction.')
|
|
1188
|
+
}
|
|
1189
|
+
// Keep Knex's rebuild lease until any unfinished transaction is cleaned up.
|
|
1190
|
+
return knex.isTransaction ? execute(knex) : runSqliteAlteration(knex, execute)
|
|
1191
|
+
}
|
|
1192
|
+
// On a transaction Knex creates a savepoint; the caller still owns the outer commit.
|
|
1193
|
+
if (knex.isTransaction) return knex.transaction(execute)
|
|
1194
|
+
return runOwnedTransaction({
|
|
1195
|
+
params: execute,
|
|
1196
|
+
context: {},
|
|
1197
|
+
helpers: { newTransaction: ownerContext => createKnexTransaction(knex, ownerContext) }
|
|
965
1198
|
})
|
|
966
1199
|
}
|
|
967
1200
|
|
|
968
1201
|
/**
|
|
969
1202
|
* Generates a Knex migration string from a json-rest-schema definition
|
|
970
1203
|
* @param {string} tableName - The name of the table
|
|
971
|
-
* @param {object} schema -
|
|
1204
|
+
* @param {object} schema - A json-rest-schema instance or { structure: fields, ...metadata } object
|
|
972
1205
|
* @param {object} [options={}] - Additional options
|
|
973
1206
|
* @returns {string} The migration code as a string
|
|
974
1207
|
*/
|
|
@@ -1028,10 +1261,12 @@ exports.down = function(knex) {
|
|
|
1028
1261
|
* Generates a deterministic Knex alter migration by diffing a live table snapshot against a desired schema.
|
|
1029
1262
|
* @param {string} tableName - The table name being diffed
|
|
1030
1263
|
* @param {object} currentSnapshot - The normalized table snapshot returned by introspection
|
|
1031
|
-
* @param {object} schema - The desired
|
|
1264
|
+
* @param {object} schema - The desired { structure: fields, ...metadata } object
|
|
1032
1265
|
* @param {object} [options={}] - Diff options
|
|
1033
1266
|
* @param {boolean} [options.autoIncrement=true] - Whether the resource expects an implicit auto-increment id column
|
|
1034
1267
|
* @param {boolean} [options.allowDropColumns=false] - Whether removed columns should be dropped automatically
|
|
1268
|
+
* @param {boolean} [options.allowDropIndexes=false] - Whether undeclared indexes should be dropped
|
|
1269
|
+
* @param {boolean} [options.allowDropForeignKeys=false] - Whether undeclared foreign keys should be dropped
|
|
1035
1270
|
* @param {string} [options.dialect=''] - Optional target dialect override
|
|
1036
1271
|
* @returns {{ migration: string, warnings: string[], plan: object }} Diff result
|
|
1037
1272
|
*/
|
|
@@ -1039,13 +1274,21 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1039
1274
|
const {
|
|
1040
1275
|
autoIncrement = true,
|
|
1041
1276
|
allowDropColumns = false,
|
|
1277
|
+
allowDropIndexes = false,
|
|
1278
|
+
allowDropForeignKeys = false,
|
|
1042
1279
|
dialect = ''
|
|
1043
1280
|
} = options
|
|
1281
|
+
for (const name of ['allowDropColumns', 'allowDropIndexes', 'allowDropForeignKeys']) {
|
|
1282
|
+
if (options[name] !== undefined && typeof options[name] !== 'boolean') {
|
|
1283
|
+
throw new TypeError(`${name} must be a boolean.`)
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1044
1286
|
const resolvedDialect = normalizeText(dialect || currentSnapshot?.dialect).toLowerCase()
|
|
1045
1287
|
const tableSchemaContext = resolveTableSchemaContext(schema, {
|
|
1046
1288
|
...options,
|
|
1047
1289
|
storage: options.storage,
|
|
1048
|
-
tableName
|
|
1290
|
+
tableName,
|
|
1291
|
+
dialect: resolvedDialect
|
|
1049
1292
|
})
|
|
1050
1293
|
const desiredColumns = buildDesiredColumnsMap(tableSchemaContext, { autoIncrement })
|
|
1051
1294
|
const currentColumns = mapByName(currentSnapshot?.columns || [])
|
|
@@ -1072,6 +1315,9 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1072
1315
|
}
|
|
1073
1316
|
|
|
1074
1317
|
plan.addColumns.push(desiredColumn)
|
|
1318
|
+
if (!desiredColumn.nullable && !desiredColumn.hasDefault) {
|
|
1319
|
+
plan.warnings.push(`New not-null column '${columnName}' has no static non-null default. Adding it may fail on existing rows; plan a backfill before enforcing not-null.`)
|
|
1320
|
+
}
|
|
1075
1321
|
continue
|
|
1076
1322
|
}
|
|
1077
1323
|
|
|
@@ -1088,8 +1334,10 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1088
1334
|
continue
|
|
1089
1335
|
}
|
|
1090
1336
|
|
|
1337
|
+
if (!planEnumAlteration(plan, tableName, currentColumn, desiredColumn, [...currentChecks.values()], resolvedDialect)) continue
|
|
1338
|
+
|
|
1091
1339
|
plan.alterColumns.push(desiredColumn)
|
|
1092
|
-
plan.warnings.push(...collectDestructiveColumnWarnings(currentColumn, desiredColumn))
|
|
1340
|
+
plan.warnings.push(...collectDestructiveColumnWarnings(currentColumn, desiredColumn, resolvedDialect))
|
|
1093
1341
|
}
|
|
1094
1342
|
|
|
1095
1343
|
for (const currentColumn of currentColumns.values()) {
|
|
@@ -1108,7 +1356,8 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1108
1356
|
|
|
1109
1357
|
for (const desiredIndex of desiredIndexes.values()) {
|
|
1110
1358
|
const currentIndex = currentIndexes.get(desiredIndex.name)
|
|
1111
|
-
const
|
|
1359
|
+
const currentIndexType = normalizeText(currentIndex?.indexType).toUpperCase()
|
|
1360
|
+
const sameIndexType = currentIndexType === desiredIndex.indexType || (!desiredIndex.indexType && currentIndexType === 'BTREE')
|
|
1112
1361
|
const indexChanged = Boolean(currentIndex) && (
|
|
1113
1362
|
!valuesEqual(currentIndex.columns, desiredIndex.columns) ||
|
|
1114
1363
|
Boolean(currentIndex.unique) !== Boolean(desiredIndex.unique) ||
|
|
@@ -1119,13 +1368,23 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1119
1368
|
plan.addIndexes.push(desiredIndex)
|
|
1120
1369
|
if (indexChanged) {
|
|
1121
1370
|
plan.dropIndexes.push(currentIndex)
|
|
1371
|
+
plan.warnings.push(`Index '${currentIndex.name}' will be dropped and recreated with its changed definition; review uniqueness and query effects.`)
|
|
1122
1372
|
}
|
|
1123
1373
|
}
|
|
1124
1374
|
}
|
|
1125
1375
|
|
|
1126
1376
|
for (const currentIndex of currentIndexes.values()) {
|
|
1127
|
-
if (
|
|
1377
|
+
if (desiredIndexes.has(currentIndex.name)) continue
|
|
1378
|
+
const supportsForeignKey = !currentIndex.unique && isMysqlDialect(resolvedDialect) && [...desiredForeignKeys.values()].some(foreignKey =>
|
|
1379
|
+
foreignKey.columns.every((column, index) => currentIndex.columns[index] === column)
|
|
1380
|
+
)
|
|
1381
|
+
if (supportsForeignKey) continue
|
|
1382
|
+
const warning = `Index '${currentIndex.name}' exists in the live table but not in the desired schema.`
|
|
1383
|
+
if (allowDropIndexes) {
|
|
1128
1384
|
plan.dropIndexes.push(currentIndex)
|
|
1385
|
+
plan.warnings.push(`${warning} It will be dropped because allowDropIndexes=true; review uniqueness and query effects.`)
|
|
1386
|
+
} else {
|
|
1387
|
+
plan.warnings.push(`${warning} Skipping automatic drop.`)
|
|
1129
1388
|
}
|
|
1130
1389
|
}
|
|
1131
1390
|
|
|
@@ -1140,26 +1399,78 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1140
1399
|
desiredForeignKey.referencedColumns
|
|
1141
1400
|
)
|
|
1142
1401
|
const sameTable = normalizeText(currentForeignKey?.referencedTableName) === desiredForeignKey.referencedTableName
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1402
|
+
const defaultRule = isMysqlDialect(resolvedDialect) ? 'RESTRICT' : 'NO ACTION'
|
|
1403
|
+
const sameDeleteRule = normalizeText(currentForeignKey?.deleteRule).toUpperCase() === (desiredForeignKey.deleteRule || defaultRule)
|
|
1404
|
+
const sameUpdateRule = normalizeText(currentForeignKey?.updateRule).toUpperCase() === (desiredForeignKey.updateRule || defaultRule)
|
|
1145
1405
|
|
|
1146
1406
|
if (!currentForeignKey || !sameColumns || !sameReferencedColumns || !sameTable || !sameDeleteRule || !sameUpdateRule) {
|
|
1147
1407
|
plan.addForeignKeys.push(desiredForeignKey)
|
|
1408
|
+
if (currentForeignKey) {
|
|
1409
|
+
plan.dropForeignKeys.push(currentForeignKey)
|
|
1410
|
+
plan.warnings.push(`Foreign key '${currentForeignKey.name}' will be dropped and recreated with its changed definition; review referential actions and existing rows.`)
|
|
1411
|
+
}
|
|
1148
1412
|
}
|
|
1149
1413
|
}
|
|
1150
1414
|
|
|
1151
1415
|
for (const currentForeignKey of currentForeignKeys.values()) {
|
|
1152
1416
|
if (!desiredForeignKeys.has(currentForeignKey.name)) {
|
|
1153
|
-
|
|
1417
|
+
const warning = `Foreign key '${currentForeignKey.name}' exists in the live table but not in the desired schema.`
|
|
1418
|
+
if (allowDropForeignKeys) {
|
|
1419
|
+
plan.dropForeignKeys.push(currentForeignKey)
|
|
1420
|
+
plan.warnings.push(`${warning} It will be dropped because allowDropForeignKeys=true; referential integrity will no longer be enforced by this constraint.`)
|
|
1421
|
+
continue
|
|
1422
|
+
}
|
|
1423
|
+
plan.warnings.push(`${warning} Skipping automatic drop.`)
|
|
1424
|
+
}
|
|
1425
|
+
if (!isMysqlDialect(resolvedDialect) || plan.dropForeignKeys.some(key => key.name === currentForeignKey.name)) continue
|
|
1426
|
+
const dropsSupportingIndex = plan.dropIndexes.some(index =>
|
|
1427
|
+
currentForeignKey.columns.every((column, position) => index.columns[position] === column.name)
|
|
1428
|
+
)
|
|
1429
|
+
if (!dropsSupportingIndex) continue
|
|
1430
|
+
|
|
1431
|
+
// MySQL forbids dropping a supporting index while its foreign key exists.
|
|
1432
|
+
plan.dropForeignKeys.push(currentForeignKey)
|
|
1433
|
+
plan.addForeignKeys.push(desiredForeignKeys.get(currentForeignKey.name) || {
|
|
1434
|
+
...currentForeignKey,
|
|
1435
|
+
columns: currentForeignKey.columns.map(column => column.name),
|
|
1436
|
+
referencedColumns: currentForeignKey.columns.map(column => column.referencedName)
|
|
1437
|
+
})
|
|
1438
|
+
plan.warnings.push(`Foreign key '${currentForeignKey.name}' will be temporarily dropped and restored while replacing its supporting index.`)
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (plan.dropColumns.length > 0) {
|
|
1442
|
+
const retainedIndexes = [
|
|
1443
|
+
...plan.addIndexes,
|
|
1444
|
+
...[...currentIndexes.values()].filter(index => !plan.dropIndexes.some(dropped => dropped.name === index.name))
|
|
1445
|
+
]
|
|
1446
|
+
const retainedForeignKeys = [
|
|
1447
|
+
...plan.addForeignKeys,
|
|
1448
|
+
...[...currentForeignKeys.values()]
|
|
1449
|
+
.filter(key => !plan.dropForeignKeys.some(dropped => dropped.name === key.name))
|
|
1450
|
+
.map(key => ({
|
|
1451
|
+
...key,
|
|
1452
|
+
columns: key.columns.map(column => column.name),
|
|
1453
|
+
referencedColumns: key.columns.map(column => column.referencedName)
|
|
1454
|
+
}))
|
|
1455
|
+
]
|
|
1456
|
+
for (const column of plan.dropColumns) {
|
|
1457
|
+
const index = retainedIndexes.find(index => index.columns.includes(column.name))
|
|
1458
|
+
const foreignKey = retainedForeignKeys.find(key => key.columns.includes(column.name) ||
|
|
1459
|
+
(key.referencedTableName === tableName && key.referencedColumns.includes(column.name)))
|
|
1460
|
+
if (!index && !foreignKey) continue
|
|
1461
|
+
const dependency = index ? `index '${index.name}'` : `foreign key '${foreignKey.name}'`
|
|
1462
|
+
throw new Error(`Cannot drop column '${column.name}' while retaining ${dependency}. Remove that dependency explicitly, using allowDropIndexes or allowDropForeignKeys for undeclared metadata.`)
|
|
1154
1463
|
}
|
|
1155
1464
|
}
|
|
1156
1465
|
|
|
1157
1466
|
for (const desiredCheck of desiredChecks.values()) {
|
|
1158
1467
|
const currentCheck = currentChecks.get(desiredCheck.name)
|
|
1159
|
-
if (currentCheck
|
|
1468
|
+
if (currentCheck && normalizeConstraintClause(currentCheck.clause) === normalizeConstraintClause(desiredCheck.clause)) {
|
|
1160
1469
|
continue
|
|
1161
1470
|
}
|
|
1162
1471
|
|
|
1472
|
+
if (currentCheck) continue
|
|
1473
|
+
|
|
1163
1474
|
if (isSqliteDialectName(resolvedDialect)) {
|
|
1164
1475
|
plan.warnings.push(buildUnsupportedConstraintWarning('check constraint', desiredCheck.name, resolvedDialect))
|
|
1165
1476
|
continue
|
|
@@ -1169,6 +1480,7 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1169
1480
|
}
|
|
1170
1481
|
|
|
1171
1482
|
for (const currentCheck of currentChecks.values()) {
|
|
1483
|
+
if (plan.dropCheckConstraints.some(check => check.name === currentCheck.name)) continue
|
|
1172
1484
|
const matchesDesiredEnum = [...desiredColumns.values()].some((desiredColumn) => {
|
|
1173
1485
|
return isInlineEnumCheckConstraint(currentCheck, desiredColumn)
|
|
1174
1486
|
})
|
|
@@ -1181,7 +1493,7 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1181
1493
|
continue
|
|
1182
1494
|
}
|
|
1183
1495
|
|
|
1184
|
-
if (desiredChecks.get(currentCheck.name)?.clause !== currentCheck.clause) {
|
|
1496
|
+
if (normalizeConstraintClause(desiredChecks.get(currentCheck.name)?.clause) !== normalizeConstraintClause(currentCheck.clause)) {
|
|
1185
1497
|
plan.warnings.push(buildUnsupportedConstraintWarning('check constraint alteration', currentCheck.name, resolvedDialect))
|
|
1186
1498
|
}
|
|
1187
1499
|
}
|
|
@@ -1194,10 +1506,15 @@ export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, o
|
|
|
1194
1506
|
plan.addForeignKeys.sort(compareByName)
|
|
1195
1507
|
plan.dropForeignKeys.sort(compareByName)
|
|
1196
1508
|
plan.addCheckConstraints.sort(compareByName)
|
|
1509
|
+
plan.dropCheckConstraints.sort(compareByName)
|
|
1197
1510
|
plan.warnings = [...new Set(plan.warnings)]
|
|
1198
1511
|
|
|
1199
1512
|
const blocks = []
|
|
1200
1513
|
|
|
1514
|
+
for (const check of plan.dropCheckConstraints) {
|
|
1515
|
+
blocks.push(` await knex.raw('ALTER TABLE ?? DROP CONSTRAINT ??', [${quoteJsString(tableName)}, ${quoteJsString(check.name)}]);`)
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1201
1518
|
const addOrAlterColumnLines = [
|
|
1202
1519
|
...plan.addColumns.map((column) => buildColumnBuilderCode(column.name, column.definition, { dialect: resolvedDialect })),
|
|
1203
1520
|
...plan.alterColumns.map((column) => buildColumnBuilderCode(column.name, column.definition, {
|
|
@@ -1249,7 +1566,7 @@ ${addConstraintLines.map((line) => ` ${line};`).join('\n')}
|
|
|
1249
1566
|
}
|
|
1250
1567
|
|
|
1251
1568
|
const warningComment = plan.warnings.length > 0
|
|
1252
|
-
? ` // Warnings:\n${plan.warnings.
|
|
1569
|
+
? ` // Warnings:\n${plan.warnings.flatMap(warning => warning.split(/\r\n|[\r\n\u2028\u2029]/).map(line => ` // - ${line}`)).join('\n')}\n`
|
|
1253
1570
|
: ''
|
|
1254
1571
|
|
|
1255
1572
|
return {
|