json-rest-api 1.0.29 → 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.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. 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 === 'number' || typeof value === 'bigint') {
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((value) => quoteJsString(value)).join(', ')})`
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: buildStorageInfo({
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 'integer'
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 ?? null,
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 || schemaLike || {}
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 [fieldName, definition] of Object.entries(schemaStructure)) {
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: resourceIdField
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.enu(columnName, enumValues)
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 != null
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
- return temporalPrecision != null
507
- ? table.time(columnName, { precision: temporalPrecision })
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
- column.defaultTo(definition.defaultTo)
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 (dialect && !isMysqlDialect(dialect)) {
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.enu(${quoteJsString(columnName)}, ${formatArrayLiteral(enumValues)})`
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
- if (definition.temporalPrecision != null) {
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
- if (definition.temporalPrecision != null) {
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
- line += `.defaultTo(${formatCodeLiteral(definition.defaultTo)})`
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
- return normalizeComparableDefault(column.defaultValue)
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 (dataType === 'float' || dataType === 'double' || dataType === 'real') return 'float'
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) !== (desiredColumn.maxLength ?? null) ||
823
- (currentColumn.numericPrecision ?? null) !== (desiredColumn.numericPrecision ?? null) ||
824
- (currentColumn.numericScale ?? null) !== (desiredColumn.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
- return normalizeText(clause)
870
- .replace(/[`"]/g, '')
871
- .replace(/\s*\(\s*/g, '(')
872
- .replace(/\s*\)\s*/g, ')')
873
- .replace(/\s*,\s*/g, ',')
874
- .replace(/\s+/g, ' ')
875
- .toLowerCase()
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 = `${desiredColumn.name} in (${desiredColumn.enumValues.map((value) => quoteJsString(value)).join(', ')})`
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 - The json-rest-schema instance or schema-like table metadata object
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
- return knex.schema.alterTable(tableName, (table) => {
959
- for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
960
- const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
961
- const column = mapTypeToKnex(table, columnName, definition, { dialect })
962
- column.alter()
963
- applyColumnConstraints(column, definition)
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 - The json-rest-schema instance or schema-like table metadata object
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 schema-like table metadata object
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 sameIndexType = normalizeText(currentIndex?.indexType).toUpperCase() === desiredIndex.indexType
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 (!desiredIndexes.has(currentIndex.name)) {
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 sameDeleteRule = normalizeText(currentForeignKey?.deleteRule).toUpperCase() === desiredForeignKey.deleteRule
1144
- const sameUpdateRule = normalizeText(currentForeignKey?.updateRule).toUpperCase() === desiredForeignKey.updateRule
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
- plan.dropForeignKeys.push(currentForeignKey)
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?.clause === desiredCheck.clause) {
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.map((warning) => ` // - ${warning}`).join('\n')}\n`
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 {