@signal24/dk-server-foundation 26.213.615
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/.gitattributes +2 -0
- package/.gitlab-ci.yml +49 -0
- package/.oxlintrc.json +40 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +15 -0
- package/.serena/project.yml +111 -0
- package/.vscode/launch.json +15 -0
- package/.vscode/settings.json +12 -0
- package/.yarnrc.yml +5 -0
- package/CLAUDE.md +279 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/TEST_MIGRATION_GUIDE.md +348 -0
- package/dist/resources/proto/generated/test/test.d.ts +224 -0
- package/dist/resources/proto/generated/test/test.d.ts.map +1 -0
- package/dist/resources/proto/generated/test/test.js +2376 -0
- package/dist/resources/proto/generated/test/test.js.map +1 -0
- package/dist/src/app/base.d.ts +37 -0
- package/dist/src/app/base.d.ts.map +1 -0
- package/dist/src/app/base.js +244 -0
- package/dist/src/app/base.js.map +1 -0
- package/dist/src/app/config.d.ts +91 -0
- package/dist/src/app/config.d.ts.map +1 -0
- package/dist/src/app/config.js +33 -0
- package/dist/src/app/config.js.map +1 -0
- package/dist/src/app/config.loader.d.ts +14 -0
- package/dist/src/app/config.loader.d.ts.map +1 -0
- package/dist/src/app/config.loader.js +67 -0
- package/dist/src/app/config.loader.js.map +1 -0
- package/dist/src/app/const.d.ts +3 -0
- package/dist/src/app/const.d.ts.map +1 -0
- package/dist/src/app/const.js +6 -0
- package/dist/src/app/const.js.map +1 -0
- package/dist/src/app/dev.d.ts +6 -0
- package/dist/src/app/dev.d.ts.map +1 -0
- package/dist/src/app/dev.js +78 -0
- package/dist/src/app/dev.js.map +1 -0
- package/dist/src/app/index.d.ts +7 -0
- package/dist/src/app/index.d.ts.map +1 -0
- package/dist/src/app/index.js +12 -0
- package/dist/src/app/index.js.map +1 -0
- package/dist/src/app/openapi.d.ts +4 -0
- package/dist/src/app/openapi.d.ts.map +1 -0
- package/dist/src/app/openapi.js +6 -0
- package/dist/src/app/openapi.js.map +1 -0
- package/dist/src/app/resolver.d.ts +11 -0
- package/dist/src/app/resolver.d.ts.map +1 -0
- package/dist/src/app/resolver.js +60 -0
- package/dist/src/app/resolver.js.map +1 -0
- package/dist/src/app/shutdown.d.ts +12 -0
- package/dist/src/app/shutdown.d.ts.map +1 -0
- package/dist/src/app/shutdown.js +63 -0
- package/dist/src/app/shutdown.js.map +1 -0
- package/dist/src/app/state.d.ts +16 -0
- package/dist/src/app/state.d.ts.map +1 -0
- package/dist/src/app/state.js +12 -0
- package/dist/src/app/state.js.map +1 -0
- package/dist/src/auth/index.d.ts +3 -0
- package/dist/src/auth/index.d.ts.map +1 -0
- package/dist/src/auth/index.js +6 -0
- package/dist/src/auth/index.js.map +1 -0
- package/dist/src/auth/jwt.d.ts +76 -0
- package/dist/src/auth/jwt.d.ts.map +1 -0
- package/dist/src/auth/jwt.js +218 -0
- package/dist/src/auth/jwt.js.map +1 -0
- package/dist/src/auth/provider.d.ts +15 -0
- package/dist/src/auth/provider.d.ts.map +1 -0
- package/dist/src/auth/provider.js +50 -0
- package/dist/src/auth/provider.js.map +1 -0
- package/dist/src/cli/dksf-dev.d.ts +3 -0
- package/dist/src/cli/dksf-dev.d.ts.map +1 -0
- package/dist/src/cli/dksf-dev.js +359 -0
- package/dist/src/cli/dksf-dev.js.map +1 -0
- package/dist/src/cli/dksf-gen-proto.d.ts +3 -0
- package/dist/src/cli/dksf-gen-proto.d.ts.map +1 -0
- package/dist/src/cli/dksf-gen-proto.js +164 -0
- package/dist/src/cli/dksf-gen-proto.js.map +1 -0
- package/dist/src/cli/dksf-install.d.ts +3 -0
- package/dist/src/cli/dksf-install.d.ts.map +1 -0
- package/dist/src/cli/dksf-install.js +10 -0
- package/dist/src/cli/dksf-install.js.map +1 -0
- package/dist/src/cli/dksf-test.d.ts +3 -0
- package/dist/src/cli/dksf-test.d.ts.map +1 -0
- package/dist/src/cli/dksf-test.js +91 -0
- package/dist/src/cli/dksf-test.js.map +1 -0
- package/dist/src/cli/dksf-update.d.ts +3 -0
- package/dist/src/cli/dksf-update.d.ts.map +1 -0
- package/dist/src/cli/dksf-update.js +86 -0
- package/dist/src/cli/dksf-update.js.map +1 -0
- package/dist/src/database/common.d.ts +84 -0
- package/dist/src/database/common.d.ts.map +1 -0
- package/dist/src/database/common.js +380 -0
- package/dist/src/database/common.js.map +1 -0
- package/dist/src/database/dialect.d.ts +10 -0
- package/dist/src/database/dialect.d.ts.map +1 -0
- package/dist/src/database/dialect.js +56 -0
- package/dist/src/database/dialect.js.map +1 -0
- package/dist/src/database/entity.d.ts +62 -0
- package/dist/src/database/entity.d.ts.map +1 -0
- package/dist/src/database/entity.js +198 -0
- package/dist/src/database/entity.js.map +1 -0
- package/dist/src/database/index.d.ts +8 -0
- package/dist/src/database/index.d.ts.map +1 -0
- package/dist/src/database/index.js +15 -0
- package/dist/src/database/index.js.map +1 -0
- package/dist/src/database/migration/MigrationResetCommand.d.ts +11 -0
- package/dist/src/database/migration/MigrationResetCommand.d.ts.map +1 -0
- package/dist/src/database/migration/MigrationResetCommand.js +149 -0
- package/dist/src/database/migration/MigrationResetCommand.js.map +1 -0
- package/dist/src/database/migration/MigrationRunCommand.d.ts +11 -0
- package/dist/src/database/migration/MigrationRunCommand.d.ts.map +1 -0
- package/dist/src/database/migration/MigrationRunCommand.js +118 -0
- package/dist/src/database/migration/MigrationRunCommand.js.map +1 -0
- package/dist/src/database/migration/characters.d.ts +14 -0
- package/dist/src/database/migration/characters.d.ts.map +1 -0
- package/dist/src/database/migration/characters.js +56 -0
- package/dist/src/database/migration/characters.js.map +1 -0
- package/dist/src/database/migration/create/MigrationCreateCommand.d.ts +11 -0
- package/dist/src/database/migration/create/MigrationCreateCommand.d.ts.map +1 -0
- package/dist/src/database/migration/create/MigrationCreateCommand.js +104 -0
- package/dist/src/database/migration/create/MigrationCreateCommand.js.map +1 -0
- package/dist/src/database/migration/create/comparator.d.ts +3 -0
- package/dist/src/database/migration/create/comparator.d.ts.map +1 -0
- package/dist/src/database/migration/create/comparator.js +408 -0
- package/dist/src/database/migration/create/comparator.js.map +1 -0
- package/dist/src/database/migration/create/db-reader.d.ts +5 -0
- package/dist/src/database/migration/create/db-reader.d.ts.map +1 -0
- package/dist/src/database/migration/create/db-reader.js +473 -0
- package/dist/src/database/migration/create/db-reader.js.map +1 -0
- package/dist/src/database/migration/create/ddl-generator.d.ts +3 -0
- package/dist/src/database/migration/create/ddl-generator.d.ts.map +1 -0
- package/dist/src/database/migration/create/ddl-generator.js +725 -0
- package/dist/src/database/migration/create/ddl-generator.js.map +1 -0
- package/dist/src/database/migration/create/entity-reader.d.ts +4 -0
- package/dist/src/database/migration/create/entity-reader.d.ts.map +1 -0
- package/dist/src/database/migration/create/entity-reader.js +408 -0
- package/dist/src/database/migration/create/entity-reader.js.map +1 -0
- package/dist/src/database/migration/create/file-generator.d.ts +2 -0
- package/dist/src/database/migration/create/file-generator.d.ts.map +1 -0
- package/dist/src/database/migration/create/file-generator.js +55 -0
- package/dist/src/database/migration/create/file-generator.js.map +1 -0
- package/dist/src/database/migration/create/prompt.d.ts +4 -0
- package/dist/src/database/migration/create/prompt.d.ts.map +1 -0
- package/dist/src/database/migration/create/prompt.js +55 -0
- package/dist/src/database/migration/create/prompt.js.map +1 -0
- package/dist/src/database/migration/create/schema-model.d.ts +109 -0
- package/dist/src/database/migration/create/schema-model.d.ts.map +1 -0
- package/dist/src/database/migration/create/schema-model.js +24 -0
- package/dist/src/database/migration/create/schema-model.js.map +1 -0
- package/dist/src/database/migration/helpers.d.ts +2 -0
- package/dist/src/database/migration/helpers.d.ts.map +1 -0
- package/dist/src/database/migration/helpers.js +8 -0
- package/dist/src/database/migration/helpers.js.map +1 -0
- package/dist/src/database/migration/index.d.ts +9 -0
- package/dist/src/database/migration/index.d.ts.map +1 -0
- package/dist/src/database/migration/index.js +43 -0
- package/dist/src/database/migration/index.js.map +1 -0
- package/dist/src/database/migration/migration.entity.d.ts +8 -0
- package/dist/src/database/migration/migration.entity.d.ts.map +1 -0
- package/dist/src/database/migration/migration.entity.js +16 -0
- package/dist/src/database/migration/migration.entity.js.map +1 -0
- package/dist/src/database/mysql.d.ts +16 -0
- package/dist/src/database/mysql.d.ts.map +1 -0
- package/dist/src/database/mysql.js +140 -0
- package/dist/src/database/mysql.js.map +1 -0
- package/dist/src/database/postgres.d.ts +16 -0
- package/dist/src/database/postgres.d.ts.map +1 -0
- package/dist/src/database/postgres.js +91 -0
- package/dist/src/database/postgres.js.map +1 -0
- package/dist/src/database/types.d.ts +21 -0
- package/dist/src/database/types.d.ts.map +1 -0
- package/dist/src/database/types.js +27 -0
- package/dist/src/database/types.js.map +1 -0
- package/dist/src/health/health.module.d.ts +6 -0
- package/dist/src/health/health.module.d.ts.map +1 -0
- package/dist/src/health/health.module.js +32 -0
- package/dist/src/health/health.module.js.map +1 -0
- package/dist/src/health/healthcheck.controller.d.ts +10 -0
- package/dist/src/health/healthcheck.controller.d.ts.map +1 -0
- package/dist/src/health/healthcheck.controller.js +30 -0
- package/dist/src/health/healthcheck.controller.js.map +1 -0
- package/dist/src/health/healthcheck.service.d.ts +8 -0
- package/dist/src/health/healthcheck.service.d.ts.map +1 -0
- package/dist/src/health/healthcheck.service.js +20 -0
- package/dist/src/health/healthcheck.service.js.map +1 -0
- package/dist/src/health/index.d.ts +3 -0
- package/dist/src/health/index.d.ts.map +1 -0
- package/dist/src/health/index.js +6 -0
- package/dist/src/health/index.js.map +1 -0
- package/dist/src/helpers/async/context.d.ts +11 -0
- package/dist/src/helpers/async/context.d.ts.map +1 -0
- package/dist/src/helpers/async/context.js +75 -0
- package/dist/src/helpers/async/context.js.map +1 -0
- package/dist/src/helpers/async/process.d.ts +16 -0
- package/dist/src/helpers/async/process.d.ts.map +1 -0
- package/dist/src/helpers/async/process.js +44 -0
- package/dist/src/helpers/async/process.js.map +1 -0
- package/dist/src/helpers/async/promise.d.ts +5 -0
- package/dist/src/helpers/async/promise.d.ts.map +1 -0
- package/dist/src/helpers/async/promise.js +27 -0
- package/dist/src/helpers/async/promise.js.map +1 -0
- package/dist/src/helpers/data/array.d.ts +3 -0
- package/dist/src/helpers/data/array.d.ts.map +1 -0
- package/dist/src/helpers/data/array.js +17 -0
- package/dist/src/helpers/data/array.js.map +1 -0
- package/dist/src/helpers/data/objects.d.ts +12 -0
- package/dist/src/helpers/data/objects.d.ts.map +1 -0
- package/dist/src/helpers/data/objects.js +75 -0
- package/dist/src/helpers/data/objects.js.map +1 -0
- package/dist/src/helpers/data/serialization.d.ts +4 -0
- package/dist/src/helpers/data/serialization.d.ts.map +1 -0
- package/dist/src/helpers/data/serialization.js +15 -0
- package/dist/src/helpers/data/serialization.js.map +1 -0
- package/dist/src/helpers/data/transformer.d.ts +13 -0
- package/dist/src/helpers/data/transformer.d.ts.map +1 -0
- package/dist/src/helpers/data/transformer.js +55 -0
- package/dist/src/helpers/data/transformer.js.map +1 -0
- package/dist/src/helpers/framework/decorators.d.ts +5 -0
- package/dist/src/helpers/framework/decorators.d.ts.map +1 -0
- package/dist/src/helpers/framework/decorators.js +39 -0
- package/dist/src/helpers/framework/decorators.js.map +1 -0
- package/dist/src/helpers/framework/event.d.ts +3 -0
- package/dist/src/helpers/framework/event.d.ts.map +1 -0
- package/dist/src/helpers/framework/event.js +20 -0
- package/dist/src/helpers/framework/event.js.map +1 -0
- package/dist/src/helpers/framework/injection.d.ts +7 -0
- package/dist/src/helpers/framework/injection.d.ts.map +1 -0
- package/dist/src/helpers/framework/injection.js +52 -0
- package/dist/src/helpers/framework/injection.js.map +1 -0
- package/dist/src/helpers/index.d.ts +22 -0
- package/dist/src/helpers/index.d.ts.map +1 -0
- package/dist/src/helpers/index.js +32 -0
- package/dist/src/helpers/index.js.map +1 -0
- package/dist/src/helpers/io/package.d.ts +5 -0
- package/dist/src/helpers/io/package.d.ts.map +1 -0
- package/dist/src/helpers/io/package.js +31 -0
- package/dist/src/helpers/io/package.js.map +1 -0
- package/dist/src/helpers/io/stream.d.ts +18 -0
- package/dist/src/helpers/io/stream.d.ts.map +1 -0
- package/dist/src/helpers/io/stream.js +91 -0
- package/dist/src/helpers/io/stream.js.map +1 -0
- package/dist/src/helpers/redis/broadcast.d.ts +12 -0
- package/dist/src/helpers/redis/broadcast.d.ts.map +1 -0
- package/dist/src/helpers/redis/broadcast.js +99 -0
- package/dist/src/helpers/redis/broadcast.js.map +1 -0
- package/dist/src/helpers/redis/cache.d.ts +7 -0
- package/dist/src/helpers/redis/cache.d.ts.map +1 -0
- package/dist/src/helpers/redis/cache.js +28 -0
- package/dist/src/helpers/redis/cache.js.map +1 -0
- package/dist/src/helpers/redis/mutex.d.ts +24 -0
- package/dist/src/helpers/redis/mutex.d.ts.map +1 -0
- package/dist/src/helpers/redis/mutex.js +240 -0
- package/dist/src/helpers/redis/mutex.js.map +1 -0
- package/dist/src/helpers/redis/redis.d.ts +11 -0
- package/dist/src/helpers/redis/redis.d.ts.map +1 -0
- package/dist/src/helpers/redis/redis.js +59 -0
- package/dist/src/helpers/redis/redis.js.map +1 -0
- package/dist/src/helpers/security/crypto.d.ts +26 -0
- package/dist/src/helpers/security/crypto.d.ts.map +1 -0
- package/dist/src/helpers/security/crypto.js +121 -0
- package/dist/src/helpers/security/crypto.js.map +1 -0
- package/dist/src/helpers/security/validation.d.ts +4 -0
- package/dist/src/helpers/security/validation.d.ts.map +1 -0
- package/dist/src/helpers/security/validation.js +25 -0
- package/dist/src/helpers/security/validation.js.map +1 -0
- package/dist/src/helpers/utils/date.d.ts +4 -0
- package/dist/src/helpers/utils/date.d.ts.map +1 -0
- package/dist/src/helpers/utils/date.js +23 -0
- package/dist/src/helpers/utils/date.js.map +1 -0
- package/dist/src/helpers/utils/error.d.ts +24 -0
- package/dist/src/helpers/utils/error.d.ts.map +1 -0
- package/dist/src/helpers/utils/error.js +168 -0
- package/dist/src/helpers/utils/error.js.map +1 -0
- package/dist/src/helpers/utils/jsx.d.ts +3 -0
- package/dist/src/helpers/utils/jsx.d.ts.map +1 -0
- package/dist/src/helpers/utils/jsx.js +13 -0
- package/dist/src/helpers/utils/jsx.js.map +1 -0
- package/dist/src/helpers/utils/uuid.d.ts +3 -0
- package/dist/src/helpers/utils/uuid.d.ts.map +1 -0
- package/dist/src/helpers/utils/uuid.js +14 -0
- package/dist/src/helpers/utils/uuid.js.map +1 -0
- package/dist/src/http/auth.d.ts +46 -0
- package/dist/src/http/auth.d.ts.map +1 -0
- package/dist/src/http/auth.js +162 -0
- package/dist/src/http/auth.js.map +1 -0
- package/dist/src/http/context.d.ts +5 -0
- package/dist/src/http/context.d.ts.map +1 -0
- package/dist/src/http/context.js +22 -0
- package/dist/src/http/context.js.map +1 -0
- package/dist/src/http/cors.d.ts +36 -0
- package/dist/src/http/cors.d.ts.map +1 -0
- package/dist/src/http/cors.js +171 -0
- package/dist/src/http/cors.js.map +1 -0
- package/dist/src/http/errors.d.ts +3 -0
- package/dist/src/http/errors.d.ts.map +1 -0
- package/dist/src/http/errors.js +10 -0
- package/dist/src/http/errors.js.map +1 -0
- package/dist/src/http/index.d.ts +24 -0
- package/dist/src/http/index.d.ts.map +1 -0
- package/dist/src/http/index.js +25 -0
- package/dist/src/http/index.js.map +1 -0
- package/dist/src/http/kernel.d.ts +17 -0
- package/dist/src/http/kernel.d.ts.map +1 -0
- package/dist/src/http/kernel.js +133 -0
- package/dist/src/http/kernel.js.map +1 -0
- package/dist/src/http/middleware.d.ts +12 -0
- package/dist/src/http/middleware.d.ts.map +1 -0
- package/dist/src/http/middleware.js +61 -0
- package/dist/src/http/middleware.js.map +1 -0
- package/dist/src/http/overrides.d.ts +2 -0
- package/dist/src/http/overrides.d.ts.map +1 -0
- package/dist/src/http/overrides.js +19 -0
- package/dist/src/http/overrides.js.map +1 -0
- package/dist/src/http/store.d.ts +33 -0
- package/dist/src/http/store.d.ts.map +1 -0
- package/dist/src/http/store.js +102 -0
- package/dist/src/http/store.js.map +1 -0
- package/dist/src/http/uploads.d.ts +7 -0
- package/dist/src/http/uploads.d.ts.map +1 -0
- package/dist/src/http/uploads.js +8 -0
- package/dist/src/http/uploads.js.map +1 -0
- package/dist/src/http/workflow.d.ts +18 -0
- package/dist/src/http/workflow.d.ts.map +1 -0
- package/dist/src/http/workflow.js +181 -0
- package/dist/src/http/workflow.js.map +1 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +25 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/services/cli/invoke.d.ts +5 -0
- package/dist/src/services/cli/invoke.d.ts.map +1 -0
- package/dist/src/services/cli/invoke.js +45 -0
- package/dist/src/services/cli/invoke.js.map +1 -0
- package/dist/src/services/cli/repl.d.ts +5 -0
- package/dist/src/services/cli/repl.d.ts.map +1 -0
- package/dist/src/services/cli/repl.js +71 -0
- package/dist/src/services/cli/repl.js.map +1 -0
- package/dist/src/services/cli.d.ts +12 -0
- package/dist/src/services/cli.d.ts.map +1 -0
- package/dist/src/services/cli.js +76 -0
- package/dist/src/services/cli.js.map +1 -0
- package/dist/src/services/index.d.ts +7 -0
- package/dist/src/services/index.d.ts.map +1 -0
- package/dist/src/services/index.js +10 -0
- package/dist/src/services/index.js.map +1 -0
- package/dist/src/services/leader.d.ts +32 -0
- package/dist/src/services/leader.d.ts.map +1 -0
- package/dist/src/services/leader.js +174 -0
- package/dist/src/services/leader.js.map +1 -0
- package/dist/src/services/logger.d.ts +35 -0
- package/dist/src/services/logger.d.ts.map +1 -0
- package/dist/src/services/logger.js +245 -0
- package/dist/src/services/logger.js.map +1 -0
- package/dist/src/services/mail/index.d.ts +61 -0
- package/dist/src/services/mail/index.d.ts.map +1 -0
- package/dist/src/services/mail/index.js +90 -0
- package/dist/src/services/mail/index.js.map +1 -0
- package/dist/src/services/mail/postmark.d.ts +11 -0
- package/dist/src/services/mail/postmark.d.ts.map +1 -0
- package/dist/src/services/mail/postmark.js +42 -0
- package/dist/src/services/mail/postmark.js.map +1 -0
- package/dist/src/services/mail/smtp.d.ts +11 -0
- package/dist/src/services/mail/smtp.d.ts.map +1 -0
- package/dist/src/services/mail/smtp.js +61 -0
- package/dist/src/services/mail/smtp.js.map +1 -0
- package/dist/src/services/mesh.d.ts +65 -0
- package/dist/src/services/mesh.d.ts.map +1 -0
- package/dist/src/services/mesh.js +422 -0
- package/dist/src/services/mesh.js.map +1 -0
- package/dist/src/services/worker/bootstrap.d.ts +3 -0
- package/dist/src/services/worker/bootstrap.d.ts.map +1 -0
- package/dist/src/services/worker/bootstrap.js +70 -0
- package/dist/src/services/worker/bootstrap.js.map +1 -0
- package/dist/src/services/worker/cli.d.ts +23 -0
- package/dist/src/services/worker/cli.d.ts.map +1 -0
- package/dist/src/services/worker/cli.js +81 -0
- package/dist/src/services/worker/cli.js.map +1 -0
- package/dist/src/services/worker/entity.d.ts +18 -0
- package/dist/src/services/worker/entity.d.ts.map +1 -0
- package/dist/src/services/worker/entity.js +16 -0
- package/dist/src/services/worker/entity.js.map +1 -0
- package/dist/src/services/worker/index.d.ts +9 -0
- package/dist/src/services/worker/index.d.ts.map +1 -0
- package/dist/src/services/worker/index.js +40 -0
- package/dist/src/services/worker/index.js.map +1 -0
- package/dist/src/services/worker/observer.d.ts +18 -0
- package/dist/src/services/worker/observer.d.ts.map +1 -0
- package/dist/src/services/worker/observer.js +172 -0
- package/dist/src/services/worker/observer.js.map +1 -0
- package/dist/src/services/worker/queue.d.ts +8 -0
- package/dist/src/services/worker/queue.d.ts.map +1 -0
- package/dist/src/services/worker/queue.js +31 -0
- package/dist/src/services/worker/queue.js.map +1 -0
- package/dist/src/services/worker/runner.d.ts +17 -0
- package/dist/src/services/worker/runner.d.ts.map +1 -0
- package/dist/src/services/worker/runner.js +131 -0
- package/dist/src/services/worker/runner.js.map +1 -0
- package/dist/src/services/worker/types.d.ts +26 -0
- package/dist/src/services/worker/types.d.ts.map +1 -0
- package/dist/src/services/worker/types.js +29 -0
- package/dist/src/services/worker/types.js.map +1 -0
- package/dist/src/srpc/SrpcByteStream.d.ts +67 -0
- package/dist/src/srpc/SrpcByteStream.d.ts.map +1 -0
- package/dist/src/srpc/SrpcByteStream.js +319 -0
- package/dist/src/srpc/SrpcByteStream.js.map +1 -0
- package/dist/src/srpc/SrpcClient.d.ts +75 -0
- package/dist/src/srpc/SrpcClient.d.ts.map +1 -0
- package/dist/src/srpc/SrpcClient.js +445 -0
- package/dist/src/srpc/SrpcClient.js.map +1 -0
- package/dist/src/srpc/SrpcServer.d.ts +54 -0
- package/dist/src/srpc/SrpcServer.d.ts.map +1 -0
- package/dist/src/srpc/SrpcServer.js +456 -0
- package/dist/src/srpc/SrpcServer.js.map +1 -0
- package/dist/src/srpc/index.d.ts +7 -0
- package/dist/src/srpc/index.d.ts.map +1 -0
- package/dist/src/srpc/index.js +12 -0
- package/dist/src/srpc/index.js.map +1 -0
- package/dist/src/srpc/types.d.ts +129 -0
- package/dist/src/srpc/types.d.ts.map +1 -0
- package/dist/src/srpc/types.js +65 -0
- package/dist/src/srpc/types.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +2 -0
- package/dist/src/telemetry/index.d.ts.map +1 -0
- package/dist/src/telemetry/index.js +5 -0
- package/dist/src/telemetry/index.js.map +1 -0
- package/dist/src/telemetry/otel/MariaDBInstrumentation.d.ts +22 -0
- package/dist/src/telemetry/otel/MariaDBInstrumentation.d.ts.map +1 -0
- package/dist/src/telemetry/otel/MariaDBInstrumentation.js +248 -0
- package/dist/src/telemetry/otel/MariaDBInstrumentation.js.map +1 -0
- package/dist/src/telemetry/otel/helpers.d.ts +27 -0
- package/dist/src/telemetry/otel/helpers.d.ts.map +1 -0
- package/dist/src/telemetry/otel/helpers.js +126 -0
- package/dist/src/telemetry/otel/helpers.js.map +1 -0
- package/dist/src/telemetry/otel/index.d.ts +14 -0
- package/dist/src/telemetry/otel/index.d.ts.map +1 -0
- package/dist/src/telemetry/otel/index.js +132 -0
- package/dist/src/telemetry/otel/index.js.map +1 -0
- package/dist/src/telemetry/otel/metrics.controller.d.ts +6 -0
- package/dist/src/telemetry/otel/metrics.controller.d.ts.map +1 -0
- package/dist/src/telemetry/otel/metrics.controller.js +63 -0
- package/dist/src/telemetry/otel/metrics.controller.js.map +1 -0
- package/dist/src/telemetry/sentry.d.ts +9 -0
- package/dist/src/telemetry/sentry.d.ts.map +1 -0
- package/dist/src/telemetry/sentry.js +62 -0
- package/dist/src/telemetry/sentry.js.map +1 -0
- package/dist/src/testapp/bootstrap.d.ts +1 -0
- package/dist/src/testapp/bootstrap.d.ts.map +1 -0
- package/dist/src/testapp/bootstrap.js +18 -0
- package/dist/src/testapp/bootstrap.js.map +1 -0
- package/dist/src/testapp/sample.d.ts +6 -0
- package/dist/src/testapp/sample.d.ts.map +1 -0
- package/dist/src/testapp/sample.js +228 -0
- package/dist/src/testapp/sample.js.map +1 -0
- package/dist/src/testapp/srpc-test.d.ts +27 -0
- package/dist/src/testapp/srpc-test.d.ts.map +1 -0
- package/dist/src/testapp/srpc-test.js +570 -0
- package/dist/src/testapp/srpc-test.js.map +1 -0
- package/dist/src/testing/expect.d.ts +25 -0
- package/dist/src/testing/expect.d.ts.map +1 -0
- package/dist/src/testing/expect.js +151 -0
- package/dist/src/testing/expect.js.map +1 -0
- package/dist/src/testing/fixtures.d.ts +19 -0
- package/dist/src/testing/fixtures.d.ts.map +1 -0
- package/dist/src/testing/fixtures.js +69 -0
- package/dist/src/testing/fixtures.js.map +1 -0
- package/dist/src/testing/index.d.ts +260 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +345 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/requests.d.ts +10 -0
- package/dist/src/testing/requests.d.ts.map +1 -0
- package/dist/src/testing/requests.js +56 -0
- package/dist/src/testing/requests.js.map +1 -0
- package/dist/src/testing/sql.d.ts +11 -0
- package/dist/src/testing/sql.d.ts.map +1 -0
- package/dist/src/testing/sql.js +55 -0
- package/dist/src/testing/sql.js.map +1 -0
- package/dist/src/types/index.d.ts +57 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +73 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/phone.d.ts +11 -0
- package/dist/src/types/phone.d.ts.map +1 -0
- package/dist/src/types/phone.js +73 -0
- package/dist/src/types/phone.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/docs/README.md +38 -0
- package/docs/authentication.md +215 -0
- package/docs/cli.md +302 -0
- package/docs/configuration.md +176 -0
- package/docs/database.md +422 -0
- package/docs/getting-started.md +154 -0
- package/docs/health.md +53 -0
- package/docs/helpers.md +436 -0
- package/docs/http.md +253 -0
- package/docs/leader-service.md +98 -0
- package/docs/logging.md +150 -0
- package/docs/mail.md +161 -0
- package/docs/mesh-service.md +204 -0
- package/docs/srpc.md +261 -0
- package/docs/telemetry.md +166 -0
- package/docs/testing.md +222 -0
- package/docs/types.md +215 -0
- package/docs/worker.md +174 -0
- package/lefthook.yml +12 -0
- package/openapi.yaml +109 -0
- package/package.json +133 -0
- package/patches/@deepkit+type+1.0.19.patch +38 -0
- package/resources/proto/generated/test/test.ts +2721 -0
- package/resources/proto/sample.proto +85 -0
- package/resources/proto/test.proto +178 -0
- package/src/app/base.ts +257 -0
- package/src/app/config.loader.ts +66 -0
- package/src/app/config.ts +120 -0
- package/src/app/const.ts +4 -0
- package/src/app/dev.ts +70 -0
- package/src/app/index.ts +6 -0
- package/src/app/openapi.ts +3 -0
- package/src/app/resolver.ts +49 -0
- package/src/app/shutdown.ts +55 -0
- package/src/app/state.ts +19 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/jwt.ts +275 -0
- package/src/auth/provider.ts +57 -0
- package/src/cli/dksf-dev.ts +363 -0
- package/src/cli/dksf-gen-proto.ts +176 -0
- package/src/cli/dksf-install.ts +11 -0
- package/src/cli/dksf-test.ts +95 -0
- package/src/cli/dksf-update.ts +101 -0
- package/src/database/CLAUDE.md +390 -0
- package/src/database/common.ts +385 -0
- package/src/database/dialect.ts +43 -0
- package/src/database/entity.ts +285 -0
- package/src/database/index.ts +7 -0
- package/src/database/migration/MigrationResetCommand.ts +152 -0
- package/src/database/migration/MigrationRunCommand.ts +118 -0
- package/src/database/migration/characters.ts +53 -0
- package/src/database/migration/create/MigrationCreateCommand.ts +94 -0
- package/src/database/migration/create/comparator.ts +467 -0
- package/src/database/migration/create/db-reader.ts +510 -0
- package/src/database/migration/create/ddl-generator.ts +755 -0
- package/src/database/migration/create/entity-reader.ts +462 -0
- package/src/database/migration/create/file-generator.ts +52 -0
- package/src/database/migration/create/prompt.ts +49 -0
- package/src/database/migration/create/schema-model.ts +102 -0
- package/src/database/migration/helpers.ts +3 -0
- package/src/database/migration/index.ts +35 -0
- package/src/database/migration/migration.entity.ts +10 -0
- package/src/database/mysql.ts +140 -0
- package/src/database/postgres.ts +97 -0
- package/src/database/types.ts +18 -0
- package/src/health/health.module.ts +30 -0
- package/src/health/healthcheck.controller.ts +17 -0
- package/src/health/healthcheck.service.ts +15 -0
- package/src/health/index.ts +2 -0
- package/src/helpers/CLAUDE.md +71 -0
- package/src/helpers/async/context.ts +67 -0
- package/src/helpers/async/process.ts +49 -0
- package/src/helpers/async/promise.ts +16 -0
- package/src/helpers/data/array.ts +11 -0
- package/src/helpers/data/objects.ts +64 -0
- package/src/helpers/data/serialization.ts +11 -0
- package/src/helpers/data/transformer.ts +54 -0
- package/src/helpers/framework/decorators.ts +27 -0
- package/src/helpers/framework/event.ts +11 -0
- package/src/helpers/framework/injection.ts +47 -0
- package/src/helpers/index.ts +34 -0
- package/src/helpers/io/package.ts +26 -0
- package/src/helpers/io/stream.ts +79 -0
- package/src/helpers/redis/broadcast.ts +94 -0
- package/src/helpers/redis/cache.ts +28 -0
- package/src/helpers/redis/mutex.ts +260 -0
- package/src/helpers/redis/redis.ts +60 -0
- package/src/helpers/security/crypto.ts +133 -0
- package/src/helpers/security/validation.ts +16 -0
- package/src/helpers/utils/date.ts +13 -0
- package/src/helpers/utils/error.ts +155 -0
- package/src/helpers/utils/jsx.ts +8 -0
- package/src/helpers/utils/uuid.ts +8 -0
- package/src/http/auth.ts +156 -0
- package/src/http/context.ts +15 -0
- package/src/http/cors.ts +159 -0
- package/src/http/errors.ts +9 -0
- package/src/http/index.ts +19 -0
- package/src/http/kernel.ts +138 -0
- package/src/http/middleware.ts +59 -0
- package/src/http/overrides.ts +20 -0
- package/src/http/store.ts +86 -0
- package/src/http/uploads.ts +6 -0
- package/src/http/workflow.ts +167 -0
- package/src/index.ts +19 -0
- package/src/services/cli/invoke.ts +39 -0
- package/src/services/cli/repl.ts +67 -0
- package/src/services/cli.ts +74 -0
- package/src/services/index.ts +6 -0
- package/src/services/leader.ts +201 -0
- package/src/services/logger.ts +258 -0
- package/src/services/mail/index.ts +117 -0
- package/src/services/mail/postmark.ts +37 -0
- package/src/services/mail/smtp.ts +46 -0
- package/src/services/mesh.ts +508 -0
- package/src/services/worker/CLAUDE.md +77 -0
- package/src/services/worker/bootstrap.ts +58 -0
- package/src/services/worker/cli.ts +63 -0
- package/src/services/worker/entity.ts +22 -0
- package/src/services/worker/index.ts +30 -0
- package/src/services/worker/observer.ts +180 -0
- package/src/services/worker/queue.ts +34 -0
- package/src/services/worker/runner.ts +146 -0
- package/src/services/worker/types.ts +32 -0
- package/src/srpc/CLAUDE.md +194 -0
- package/src/srpc/SRPC_MIGRATION_GUIDE.md +348 -0
- package/src/srpc/SrpcByteStream.ts +382 -0
- package/src/srpc/SrpcClient.ts +512 -0
- package/src/srpc/SrpcServer.ts +575 -0
- package/src/srpc/index.ts +15 -0
- package/src/srpc/types.ts +144 -0
- package/src/telemetry/index.ts +1 -0
- package/src/telemetry/otel/MariaDBInstrumentation.ts +297 -0
- package/src/telemetry/otel/helpers.ts +117 -0
- package/src/telemetry/otel/index.ts +150 -0
- package/src/telemetry/otel/metrics.controller.ts +50 -0
- package/src/telemetry/sentry.ts +58 -0
- package/src/testapp/bootstrap.ts +17 -0
- package/src/testapp/sample.ts +220 -0
- package/src/testapp/srpc-test.ts +684 -0
- package/src/testing/expect.ts +148 -0
- package/src/testing/fixtures.ts +62 -0
- package/src/testing/index.ts +355 -0
- package/src/testing/requests.ts +68 -0
- package/src/testing/sql.ts +50 -0
- package/src/types/index.ts +64 -0
- package/src/types/phone.ts +64 -0
- package/tests/app/app.spec.ts +53 -0
- package/tests/app/type.spec.ts +22 -0
- package/tests/auth/jwt.spec.ts +90 -0
- package/tests/database/entity.spec.ts +382 -0
- package/tests/database/locks.spec.ts +142 -0
- package/tests/database/migration-create-integration.spec.ts +234 -0
- package/tests/database/migration-create-unit.spec.ts +3896 -0
- package/tests/helpers/array.spec.ts +80 -0
- package/tests/helpers/cache.spec.ts +202 -0
- package/tests/helpers/crypto.spec.ts +236 -0
- package/tests/helpers/date.spec.ts +94 -0
- package/tests/helpers/error.spec.ts +233 -0
- package/tests/helpers/mutex.spec.ts +354 -0
- package/tests/helpers/objects.spec.ts +212 -0
- package/tests/helpers/package.spec.ts +90 -0
- package/tests/helpers/promise.spec.ts +119 -0
- package/tests/helpers/redis.spec.ts +50 -0
- package/tests/helpers/serialization.spec.ts +150 -0
- package/tests/helpers/stream.spec.ts +225 -0
- package/tests/helpers/validation.spec.ts +133 -0
- package/tests/services/leader.spec.ts +257 -0
- package/tests/services/logger.spec.ts +269 -0
- package/tests/services/mesh.spec.ts +814 -0
- package/tests/shared/db.ts +105 -0
- package/tests/shared/globalSetup.ts +48 -0
- package/tests/shared/helpers.ts +40 -0
- package/tests/srpc/SrpcByteStream.spec.ts +542 -0
- package/tests/tsconfig.json +4 -0
- package/tests/types/index.spec.ts +60 -0
- package/tests/types/phone.spec.ts +140 -0
- package/tsconfig.json +106 -0
- package/tsconfig.test.json +8 -0
- package/types.d.ts +6 -0
|
@@ -0,0 +1,3896 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import { compareSchemas } from '../../src/database/migration/create/comparator';
|
|
5
|
+
import { generateDDL } from '../../src/database/migration/create/ddl-generator';
|
|
6
|
+
import { setNonInteractive } from '../../src/database/migration/create/prompt';
|
|
7
|
+
import { ColumnSchema, ColumnModification, DatabaseSchema, SchemaDiff, TableSchema } from '../../src/database/migration/create/schema-model';
|
|
8
|
+
|
|
9
|
+
// Disable interactive prompts for all tests
|
|
10
|
+
setNonInteractive(true);
|
|
11
|
+
|
|
12
|
+
// --- Helpers ---
|
|
13
|
+
|
|
14
|
+
function col(overrides: Partial<ColumnSchema> & { name: string }): ColumnSchema {
|
|
15
|
+
return {
|
|
16
|
+
type: 'varchar',
|
|
17
|
+
size: 255,
|
|
18
|
+
unsigned: false,
|
|
19
|
+
nullable: false,
|
|
20
|
+
autoIncrement: false,
|
|
21
|
+
isPrimaryKey: false,
|
|
22
|
+
ordinalPosition: 1,
|
|
23
|
+
...overrides
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function table(name: string, columns: ColumnSchema[], extra?: Partial<TableSchema>): TableSchema {
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
columns,
|
|
31
|
+
indexes: [],
|
|
32
|
+
foreignKeys: [],
|
|
33
|
+
...extra
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function schema(...tables: TableSchema[]): DatabaseSchema {
|
|
38
|
+
return new Map(tables.map(t => [t.name, t]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Comparator Tests ---
|
|
42
|
+
|
|
43
|
+
describe('comparator', () => {
|
|
44
|
+
describe('table-level changes', () => {
|
|
45
|
+
it('should detect added tables', async () => {
|
|
46
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true })]));
|
|
47
|
+
const db = schema();
|
|
48
|
+
|
|
49
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
50
|
+
|
|
51
|
+
assert.equal(diff.addedTables.length, 1);
|
|
52
|
+
assert.equal(diff.addedTables[0].name, 'users');
|
|
53
|
+
assert.equal(diff.removedTables.length, 0);
|
|
54
|
+
assert.equal(diff.modifiedTables.length, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should detect removed tables', async () => {
|
|
58
|
+
const entity = schema();
|
|
59
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true })]));
|
|
60
|
+
|
|
61
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
62
|
+
|
|
63
|
+
assert.equal(diff.addedTables.length, 0);
|
|
64
|
+
assert.equal(diff.removedTables.length, 1);
|
|
65
|
+
assert.equal(diff.removedTables[0].name, 'users');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should detect no changes when schemas match', async () => {
|
|
69
|
+
const t = table('users', [
|
|
70
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
71
|
+
col({ name: 'name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
72
|
+
]);
|
|
73
|
+
const entity = schema(t);
|
|
74
|
+
const db = schema(t);
|
|
75
|
+
|
|
76
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
77
|
+
|
|
78
|
+
assert.equal(diff.addedTables.length, 0);
|
|
79
|
+
assert.equal(diff.removedTables.length, 0);
|
|
80
|
+
assert.equal(diff.modifiedTables.length, 0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('column-level changes', () => {
|
|
85
|
+
it('should detect added columns', async () => {
|
|
86
|
+
const entity = schema(
|
|
87
|
+
table('users', [
|
|
88
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
89
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
90
|
+
col({ name: 'email', ordinalPosition: 3 })
|
|
91
|
+
])
|
|
92
|
+
);
|
|
93
|
+
const db = schema(
|
|
94
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'name', ordinalPosition: 2 })])
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
98
|
+
|
|
99
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
100
|
+
assert.equal(diff.modifiedTables[0].addedColumns.length, 1);
|
|
101
|
+
assert.equal(diff.modifiedTables[0].addedColumns[0].name, 'email');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should detect removed columns', async () => {
|
|
105
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
106
|
+
const db = schema(
|
|
107
|
+
table('users', [
|
|
108
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
109
|
+
col({ name: 'legacy_field', ordinalPosition: 2 })
|
|
110
|
+
])
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
114
|
+
|
|
115
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
116
|
+
assert.equal(diff.modifiedTables[0].removedColumns.length, 1);
|
|
117
|
+
assert.equal(diff.modifiedTables[0].removedColumns[0].name, 'legacy_field');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should detect type changes', async () => {
|
|
121
|
+
const entity = schema(
|
|
122
|
+
table('users', [
|
|
123
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
124
|
+
col({ name: 'name', type: 'text', size: undefined, ordinalPosition: 2 })
|
|
125
|
+
])
|
|
126
|
+
);
|
|
127
|
+
const db = schema(
|
|
128
|
+
table('users', [
|
|
129
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
130
|
+
col({ name: 'name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
135
|
+
|
|
136
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
137
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns.length, 1);
|
|
138
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns[0].name, 'name');
|
|
139
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns[0].typeChanged, true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should detect nullable changes', async () => {
|
|
143
|
+
const entity = schema(
|
|
144
|
+
table('users', [
|
|
145
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
146
|
+
col({ name: 'bio', nullable: true, ordinalPosition: 2 })
|
|
147
|
+
])
|
|
148
|
+
);
|
|
149
|
+
const db = schema(
|
|
150
|
+
table('users', [
|
|
151
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
152
|
+
col({ name: 'bio', nullable: false, ordinalPosition: 2 })
|
|
153
|
+
])
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
157
|
+
|
|
158
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
159
|
+
const mod = diff.modifiedTables[0].modifiedColumns[0];
|
|
160
|
+
assert.equal(mod.name, 'bio');
|
|
161
|
+
assert.equal(mod.nullableChanged, true);
|
|
162
|
+
assert.equal(mod.typeChanged, false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should detect size changes', async () => {
|
|
166
|
+
const entity = schema(
|
|
167
|
+
table('users', [
|
|
168
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
169
|
+
col({ name: 'code', type: 'varchar', size: 100, ordinalPosition: 2 })
|
|
170
|
+
])
|
|
171
|
+
);
|
|
172
|
+
const db = schema(
|
|
173
|
+
table('users', [
|
|
174
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
175
|
+
col({ name: 'code', type: 'varchar', size: 50, ordinalPosition: 2 })
|
|
176
|
+
])
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
180
|
+
|
|
181
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
182
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns[0].typeChanged, true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should detect onUpdateExpression changes', async () => {
|
|
186
|
+
const entity = schema(
|
|
187
|
+
table('users', [
|
|
188
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
189
|
+
col({
|
|
190
|
+
name: 'updated_at',
|
|
191
|
+
type: 'datetime',
|
|
192
|
+
size: undefined,
|
|
193
|
+
onUpdateExpression: 'CURRENT_TIMESTAMP',
|
|
194
|
+
ordinalPosition: 2
|
|
195
|
+
})
|
|
196
|
+
])
|
|
197
|
+
);
|
|
198
|
+
const db = schema(
|
|
199
|
+
table('users', [
|
|
200
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
201
|
+
col({ name: 'updated_at', type: 'datetime', size: undefined, ordinalPosition: 2 })
|
|
202
|
+
])
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
206
|
+
|
|
207
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
208
|
+
const mod = diff.modifiedTables[0].modifiedColumns[0];
|
|
209
|
+
assert.equal(mod.name, 'updated_at');
|
|
210
|
+
assert.equal(mod.onUpdateChanged, true);
|
|
211
|
+
assert.equal(mod.typeChanged, false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('index changes', () => {
|
|
216
|
+
it('should detect added indexes', async () => {
|
|
217
|
+
const entity = schema(
|
|
218
|
+
table(
|
|
219
|
+
'users',
|
|
220
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
221
|
+
{ indexes: [{ name: 'idx_email', columns: ['email'], unique: true, spatial: false }] }
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
const db = schema(
|
|
225
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })])
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
229
|
+
|
|
230
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
231
|
+
assert.equal(diff.modifiedTables[0].addedIndexes.length, 1);
|
|
232
|
+
assert.equal(diff.modifiedTables[0].addedIndexes[0].columns[0], 'email');
|
|
233
|
+
assert.equal(diff.modifiedTables[0].addedIndexes[0].unique, true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should detect removed indexes', async () => {
|
|
237
|
+
const entity = schema(
|
|
238
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })])
|
|
239
|
+
);
|
|
240
|
+
const db = schema(
|
|
241
|
+
table(
|
|
242
|
+
'users',
|
|
243
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
244
|
+
{ indexes: [{ name: 'idx_email', columns: ['email'], unique: false, spatial: false }] }
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
249
|
+
|
|
250
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
251
|
+
assert.equal(diff.modifiedTables[0].removedIndexes.length, 1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should match indexes by columns+uniqueness, not by name', async () => {
|
|
255
|
+
const entity = schema(
|
|
256
|
+
table(
|
|
257
|
+
'users',
|
|
258
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
259
|
+
{ indexes: [{ name: 'idx_users_email', columns: ['email'], unique: true, spatial: false }] }
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
const db = schema(
|
|
263
|
+
table(
|
|
264
|
+
'users',
|
|
265
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
266
|
+
{ indexes: [{ name: 'different_name', columns: ['email'], unique: true, spatial: false }] }
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
271
|
+
|
|
272
|
+
// Same columns + uniqueness = no change despite different name
|
|
273
|
+
assert.equal(diff.modifiedTables.length, 0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should detect spatial index flag differences', async () => {
|
|
277
|
+
const entity = schema(
|
|
278
|
+
table(
|
|
279
|
+
'locations',
|
|
280
|
+
[
|
|
281
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
282
|
+
col({ name: 'position', type: 'point', size: undefined, ordinalPosition: 2 })
|
|
283
|
+
],
|
|
284
|
+
{ indexes: [{ name: 'idx_position', columns: ['position'], unique: false, spatial: true }] }
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
const db = schema(
|
|
288
|
+
table(
|
|
289
|
+
'locations',
|
|
290
|
+
[
|
|
291
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
292
|
+
col({ name: 'position', type: 'point', size: undefined, ordinalPosition: 2 })
|
|
293
|
+
],
|
|
294
|
+
{ indexes: [{ name: 'idx_position', columns: ['position'], unique: false, spatial: false }] }
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
299
|
+
|
|
300
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
301
|
+
assert.equal(diff.modifiedTables[0].addedIndexes.length, 1);
|
|
302
|
+
assert.equal(diff.modifiedTables[0].addedIndexes[0].spatial, true);
|
|
303
|
+
assert.equal(diff.modifiedTables[0].removedIndexes.length, 1);
|
|
304
|
+
assert.equal(diff.modifiedTables[0].removedIndexes[0].spatial, false);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('foreign key changes', () => {
|
|
309
|
+
it('should detect added foreign keys', async () => {
|
|
310
|
+
const entity = schema(
|
|
311
|
+
table(
|
|
312
|
+
'posts',
|
|
313
|
+
[
|
|
314
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
315
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
316
|
+
],
|
|
317
|
+
{
|
|
318
|
+
foreignKeys: [
|
|
319
|
+
{
|
|
320
|
+
name: 'fk_posts_user_id',
|
|
321
|
+
columns: ['user_id'],
|
|
322
|
+
referencedTable: 'users',
|
|
323
|
+
referencedColumns: ['id'],
|
|
324
|
+
onDelete: 'CASCADE',
|
|
325
|
+
onUpdate: 'RESTRICT'
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
const db = schema(
|
|
332
|
+
table('posts', [
|
|
333
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
334
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
335
|
+
])
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
339
|
+
|
|
340
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
341
|
+
assert.equal(diff.modifiedTables[0].addedForeignKeys.length, 1);
|
|
342
|
+
assert.equal(diff.modifiedTables[0].addedForeignKeys[0].referencedTable, 'users');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should match foreign keys by structure, not by name', async () => {
|
|
346
|
+
const fkEntity = {
|
|
347
|
+
name: 'fk_entity_name',
|
|
348
|
+
columns: ['user_id'],
|
|
349
|
+
referencedTable: 'users',
|
|
350
|
+
referencedColumns: ['id'],
|
|
351
|
+
onDelete: 'CASCADE',
|
|
352
|
+
onUpdate: 'RESTRICT'
|
|
353
|
+
};
|
|
354
|
+
const fkDb = {
|
|
355
|
+
name: 'different_constraint_name',
|
|
356
|
+
columns: ['user_id'],
|
|
357
|
+
referencedTable: 'users',
|
|
358
|
+
referencedColumns: ['id'],
|
|
359
|
+
onDelete: 'CASCADE',
|
|
360
|
+
onUpdate: 'RESTRICT'
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const entity = schema(
|
|
364
|
+
table(
|
|
365
|
+
'posts',
|
|
366
|
+
[
|
|
367
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
368
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
369
|
+
],
|
|
370
|
+
{ foreignKeys: [fkEntity] }
|
|
371
|
+
)
|
|
372
|
+
);
|
|
373
|
+
const db = schema(
|
|
374
|
+
table(
|
|
375
|
+
'posts',
|
|
376
|
+
[
|
|
377
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
378
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
379
|
+
],
|
|
380
|
+
{ foreignKeys: [fkDb] }
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
385
|
+
|
|
386
|
+
assert.equal(diff.modifiedTables.length, 0);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should detect FK action changes (onDelete)', async () => {
|
|
390
|
+
const entity = schema(
|
|
391
|
+
table(
|
|
392
|
+
'posts',
|
|
393
|
+
[
|
|
394
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
395
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
396
|
+
],
|
|
397
|
+
{
|
|
398
|
+
foreignKeys: [
|
|
399
|
+
{
|
|
400
|
+
name: 'fk_posts_user_id',
|
|
401
|
+
columns: ['user_id'],
|
|
402
|
+
referencedTable: 'users',
|
|
403
|
+
referencedColumns: ['id'],
|
|
404
|
+
onDelete: 'RESTRICT',
|
|
405
|
+
onUpdate: 'RESTRICT'
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
const db = schema(
|
|
412
|
+
table(
|
|
413
|
+
'posts',
|
|
414
|
+
[
|
|
415
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
416
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
417
|
+
],
|
|
418
|
+
{
|
|
419
|
+
foreignKeys: [
|
|
420
|
+
{
|
|
421
|
+
name: 'fk_posts_user_id',
|
|
422
|
+
columns: ['user_id'],
|
|
423
|
+
referencedTable: 'users',
|
|
424
|
+
referencedColumns: ['id'],
|
|
425
|
+
onDelete: 'CASCADE',
|
|
426
|
+
onUpdate: 'RESTRICT'
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
434
|
+
|
|
435
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
436
|
+
assert.equal(diff.modifiedTables[0].addedForeignKeys.length, 1);
|
|
437
|
+
assert.equal(diff.modifiedTables[0].addedForeignKeys[0].onDelete, 'RESTRICT');
|
|
438
|
+
assert.equal(diff.modifiedTables[0].removedForeignKeys.length, 1);
|
|
439
|
+
assert.equal(diff.modifiedTables[0].removedForeignKeys[0].onDelete, 'CASCADE');
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('primary key changes', () => {
|
|
444
|
+
it('should detect PK changes', async () => {
|
|
445
|
+
const entity = schema(
|
|
446
|
+
table('users', [
|
|
447
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
448
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: false, ordinalPosition: 2 })
|
|
449
|
+
])
|
|
450
|
+
);
|
|
451
|
+
const db = schema(
|
|
452
|
+
table('users', [
|
|
453
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }),
|
|
454
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: true, ordinalPosition: 2 })
|
|
455
|
+
])
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
459
|
+
|
|
460
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
461
|
+
assert.equal(diff.modifiedTables[0].primaryKeyChanged, true);
|
|
462
|
+
assert.deepEqual(diff.modifiedTables[0].newPrimaryKey, ['id']);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should store old PK constraint name from DB table', async () => {
|
|
466
|
+
const entity = schema(
|
|
467
|
+
table('users', [
|
|
468
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
469
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: false, ordinalPosition: 2 })
|
|
470
|
+
])
|
|
471
|
+
);
|
|
472
|
+
const dbTable = table('users', [
|
|
473
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }),
|
|
474
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: true, ordinalPosition: 2 })
|
|
475
|
+
]);
|
|
476
|
+
dbTable.primaryKeyConstraintName = 'users_pk_custom';
|
|
477
|
+
const db = schema(dbTable);
|
|
478
|
+
|
|
479
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
480
|
+
|
|
481
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
482
|
+
assert.equal(diff.modifiedTables[0].primaryKeyChanged, true);
|
|
483
|
+
assert.equal(diff.modifiedTables[0].oldPrimaryKeyConstraintName, 'users_pk_custom');
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('enum changes (postgres)', () => {
|
|
488
|
+
it('should detect new enum types for new columns', async () => {
|
|
489
|
+
const entity = schema(
|
|
490
|
+
table('users', [
|
|
491
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
492
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
493
|
+
])
|
|
494
|
+
);
|
|
495
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
496
|
+
|
|
497
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
498
|
+
|
|
499
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
500
|
+
assert.equal(diff.modifiedTables[0].addedEnumTypes.length, 1);
|
|
501
|
+
assert.equal(diff.modifiedTables[0].addedEnumTypes[0].typeName, 'users_status');
|
|
502
|
+
assert.deepEqual(diff.modifiedTables[0].addedEnumTypes[0].values, ['active', 'inactive']);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should detect added enum values', async () => {
|
|
506
|
+
const entity = schema(
|
|
507
|
+
table('users', [
|
|
508
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
509
|
+
col({
|
|
510
|
+
name: 'status',
|
|
511
|
+
type: 'enum',
|
|
512
|
+
enumValues: ['active', 'inactive', 'banned'],
|
|
513
|
+
enumTypeName: 'users_status',
|
|
514
|
+
ordinalPosition: 2
|
|
515
|
+
})
|
|
516
|
+
])
|
|
517
|
+
);
|
|
518
|
+
const db = schema(
|
|
519
|
+
table('users', [
|
|
520
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
521
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
522
|
+
])
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
526
|
+
|
|
527
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
528
|
+
assert.equal(diff.modifiedTables[0].modifiedEnumTypes.length, 1);
|
|
529
|
+
assert.deepEqual(diff.modifiedTables[0].modifiedEnumTypes[0].added, ['banned']);
|
|
530
|
+
assert.deepEqual(diff.modifiedTables[0].modifiedEnumTypes[0].removed, []);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should store newValues, tableName, columnName for modified enums', async () => {
|
|
534
|
+
const entity = schema(
|
|
535
|
+
table('users', [
|
|
536
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
537
|
+
col({
|
|
538
|
+
name: 'status',
|
|
539
|
+
type: 'enum',
|
|
540
|
+
enumValues: ['active', 'banned'],
|
|
541
|
+
enumTypeName: 'users_status',
|
|
542
|
+
ordinalPosition: 2
|
|
543
|
+
})
|
|
544
|
+
])
|
|
545
|
+
);
|
|
546
|
+
const db = schema(
|
|
547
|
+
table('users', [
|
|
548
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
549
|
+
col({
|
|
550
|
+
name: 'status',
|
|
551
|
+
type: 'enum',
|
|
552
|
+
enumValues: ['active', 'inactive'],
|
|
553
|
+
enumTypeName: 'users_status',
|
|
554
|
+
ordinalPosition: 2
|
|
555
|
+
})
|
|
556
|
+
])
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
560
|
+
|
|
561
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
562
|
+
const enumMod = diff.modifiedTables[0].modifiedEnumTypes[0];
|
|
563
|
+
assert.deepEqual(enumMod.newValues, ['active', 'banned']);
|
|
564
|
+
assert.equal(enumMod.tableName, 'users');
|
|
565
|
+
assert.equal(enumMod.columnName, 'status');
|
|
566
|
+
assert.deepEqual(enumMod.removed, ['inactive']);
|
|
567
|
+
assert.deepEqual(enumMod.added, ['banned']);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('column reordering (MySQL)', () => {
|
|
572
|
+
it('should detect reordered columns', async () => {
|
|
573
|
+
const entity = schema(
|
|
574
|
+
table('users', [
|
|
575
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
576
|
+
col({ name: 'email', ordinalPosition: 2 }),
|
|
577
|
+
col({ name: 'name', ordinalPosition: 3 })
|
|
578
|
+
])
|
|
579
|
+
);
|
|
580
|
+
const db = schema(
|
|
581
|
+
table('users', [
|
|
582
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
583
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
584
|
+
col({ name: 'email', ordinalPosition: 3 })
|
|
585
|
+
])
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
589
|
+
|
|
590
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
591
|
+
assert.ok(diff.modifiedTables[0].reorderedColumns.length > 0);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should not detect reordering for postgres', async () => {
|
|
595
|
+
const entity = schema(
|
|
596
|
+
table('users', [
|
|
597
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
598
|
+
col({ name: 'email', ordinalPosition: 2 }),
|
|
599
|
+
col({ name: 'name', ordinalPosition: 3 })
|
|
600
|
+
])
|
|
601
|
+
);
|
|
602
|
+
const db = schema(
|
|
603
|
+
table('users', [
|
|
604
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
605
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
606
|
+
col({ name: 'email', ordinalPosition: 3 })
|
|
607
|
+
])
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
611
|
+
|
|
612
|
+
// PG doesn't support column reordering, so no reorder entries
|
|
613
|
+
assert.equal(diff.modifiedTables.length, 0);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// --- DDL Generator Tests ---
|
|
619
|
+
|
|
620
|
+
describe('ddl-generator', () => {
|
|
621
|
+
describe('MySQL', () => {
|
|
622
|
+
it('should generate CREATE TABLE', async () => {
|
|
623
|
+
const diff = await compareSchemas(
|
|
624
|
+
schema(
|
|
625
|
+
table('users', [
|
|
626
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
627
|
+
col({ name: 'name', type: 'varchar', size: 100, ordinalPosition: 2 }),
|
|
628
|
+
col({ name: 'email', type: 'varchar', size: 255, nullable: true, ordinalPosition: 3 })
|
|
629
|
+
])
|
|
630
|
+
),
|
|
631
|
+
schema(),
|
|
632
|
+
'mysql',
|
|
633
|
+
false
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const stmts = generateDDL(diff);
|
|
637
|
+
|
|
638
|
+
assert.ok(stmts.length >= 1);
|
|
639
|
+
const create = stmts[0];
|
|
640
|
+
assert.ok(create.includes('CREATE TABLE `users`'));
|
|
641
|
+
assert.ok(create.includes('`id` INT'));
|
|
642
|
+
assert.ok(create.includes('AUTO_INCREMENT'));
|
|
643
|
+
assert.ok(create.includes('`name` VARCHAR(100)'));
|
|
644
|
+
assert.ok(create.includes('NOT NULL'));
|
|
645
|
+
assert.ok(create.includes('PRIMARY KEY'));
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('should generate ADD COLUMN', async () => {
|
|
649
|
+
const diff = await compareSchemas(
|
|
650
|
+
schema(
|
|
651
|
+
table('users', [
|
|
652
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
653
|
+
col({ name: 'email', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
654
|
+
])
|
|
655
|
+
),
|
|
656
|
+
schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })])),
|
|
657
|
+
'mysql',
|
|
658
|
+
false
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const stmts = generateDDL(diff);
|
|
662
|
+
|
|
663
|
+
assert.ok(stmts.some(s => s.includes('ADD COLUMN') && s.includes('`email`') && s.includes('VARCHAR(255)')));
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should generate DROP COLUMN', async () => {
|
|
667
|
+
const diff = await compareSchemas(
|
|
668
|
+
schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })])),
|
|
669
|
+
schema(
|
|
670
|
+
table('users', [
|
|
671
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
672
|
+
col({ name: 'legacy', ordinalPosition: 2 })
|
|
673
|
+
])
|
|
674
|
+
),
|
|
675
|
+
'mysql',
|
|
676
|
+
false
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const stmts = generateDDL(diff);
|
|
680
|
+
|
|
681
|
+
assert.ok(stmts.some(s => s.includes('DROP COLUMN') && s.includes('`legacy`')));
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should generate MODIFY COLUMN for type changes', async () => {
|
|
685
|
+
const diff = await compareSchemas(
|
|
686
|
+
schema(
|
|
687
|
+
table('users', [
|
|
688
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
689
|
+
col({ name: 'bio', type: 'text', size: undefined, ordinalPosition: 2 })
|
|
690
|
+
])
|
|
691
|
+
),
|
|
692
|
+
schema(
|
|
693
|
+
table('users', [
|
|
694
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
695
|
+
col({ name: 'bio', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
696
|
+
])
|
|
697
|
+
),
|
|
698
|
+
'mysql',
|
|
699
|
+
false
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
const stmts = generateDDL(diff);
|
|
703
|
+
|
|
704
|
+
assert.ok(stmts.some(s => s.includes('MODIFY COLUMN') && s.includes('`bio`') && s.includes('TEXT')));
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('should generate ENUM type', async () => {
|
|
708
|
+
const diff = await compareSchemas(
|
|
709
|
+
schema(
|
|
710
|
+
table('users', [
|
|
711
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
712
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], ordinalPosition: 2 })
|
|
713
|
+
])
|
|
714
|
+
),
|
|
715
|
+
schema(),
|
|
716
|
+
'mysql',
|
|
717
|
+
false
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
const stmts = generateDDL(diff);
|
|
721
|
+
const create = stmts[0];
|
|
722
|
+
|
|
723
|
+
assert.ok(create.includes("ENUM('active','inactive')"));
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('should generate DROP TABLE', async () => {
|
|
727
|
+
const diff = await compareSchemas(
|
|
728
|
+
schema(),
|
|
729
|
+
schema(table('old_table', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })])),
|
|
730
|
+
'mysql',
|
|
731
|
+
false
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
const stmts = generateDDL(diff);
|
|
735
|
+
|
|
736
|
+
assert.ok(stmts.some(s => s.includes('DROP TABLE `old_table`')));
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should generate CREATE INDEX', async () => {
|
|
740
|
+
const diff = await compareSchemas(
|
|
741
|
+
schema(
|
|
742
|
+
table(
|
|
743
|
+
'users',
|
|
744
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
745
|
+
{ indexes: [{ name: 'idx_email', columns: ['email'], unique: true, spatial: false }] }
|
|
746
|
+
)
|
|
747
|
+
),
|
|
748
|
+
schema(
|
|
749
|
+
table('users', [
|
|
750
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
751
|
+
col({ name: 'email', ordinalPosition: 2 })
|
|
752
|
+
])
|
|
753
|
+
),
|
|
754
|
+
'mysql',
|
|
755
|
+
false
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
const stmts = generateDDL(diff);
|
|
759
|
+
|
|
760
|
+
assert.ok(stmts.some(s => s.includes('CREATE UNIQUE INDEX') && s.includes('`idx_email`')));
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should generate boolean column as TINYINT(1)', async () => {
|
|
764
|
+
const diff = await compareSchemas(
|
|
765
|
+
schema(
|
|
766
|
+
table('flags', [
|
|
767
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
768
|
+
col({ name: 'active', type: 'tinyint', size: 1, ordinalPosition: 2 })
|
|
769
|
+
])
|
|
770
|
+
),
|
|
771
|
+
schema(),
|
|
772
|
+
'mysql',
|
|
773
|
+
false
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
const stmts = generateDDL(diff);
|
|
777
|
+
const create = stmts[0];
|
|
778
|
+
|
|
779
|
+
assert.ok(create.includes('TINYINT(1)'));
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('should generate MODIFY COLUMN for reorder-only columns', async () => {
|
|
783
|
+
const diff = await compareSchemas(
|
|
784
|
+
schema(
|
|
785
|
+
table('users', [
|
|
786
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
787
|
+
col({ name: 'email', ordinalPosition: 2 }),
|
|
788
|
+
col({ name: 'name', ordinalPosition: 3 })
|
|
789
|
+
])
|
|
790
|
+
),
|
|
791
|
+
schema(
|
|
792
|
+
table('users', [
|
|
793
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
794
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
795
|
+
col({ name: 'email', ordinalPosition: 3 })
|
|
796
|
+
])
|
|
797
|
+
),
|
|
798
|
+
'mysql',
|
|
799
|
+
false
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
const stmts = generateDDL(diff);
|
|
803
|
+
|
|
804
|
+
// Should emit MODIFY COLUMN with AFTER clause for reordering
|
|
805
|
+
assert.ok(stmts.some(s => s.includes('MODIFY COLUMN') && s.includes('AFTER')));
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('should generate MODIFY COLUMN with ON UPDATE for onUpdateExpression changes', async () => {
|
|
809
|
+
const diff = await compareSchemas(
|
|
810
|
+
schema(
|
|
811
|
+
table('users', [
|
|
812
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
813
|
+
col({
|
|
814
|
+
name: 'updated_at',
|
|
815
|
+
type: 'datetime',
|
|
816
|
+
size: undefined,
|
|
817
|
+
onUpdateExpression: 'CURRENT_TIMESTAMP',
|
|
818
|
+
ordinalPosition: 2
|
|
819
|
+
})
|
|
820
|
+
])
|
|
821
|
+
),
|
|
822
|
+
schema(
|
|
823
|
+
table('users', [
|
|
824
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
825
|
+
col({ name: 'updated_at', type: 'datetime', size: undefined, ordinalPosition: 2 })
|
|
826
|
+
])
|
|
827
|
+
),
|
|
828
|
+
'mysql',
|
|
829
|
+
false
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
const stmts = generateDDL(diff);
|
|
833
|
+
|
|
834
|
+
assert.ok(stmts.some(s => s.includes('MODIFY COLUMN') && s.includes('ON UPDATE CURRENT_TIMESTAMP')));
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
describe('PostgreSQL', () => {
|
|
839
|
+
it('should generate CREATE TABLE', async () => {
|
|
840
|
+
const diff = await compareSchemas(
|
|
841
|
+
schema(
|
|
842
|
+
table('users', [
|
|
843
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
844
|
+
col({ name: 'name', type: 'varchar', size: 100, ordinalPosition: 2 }),
|
|
845
|
+
col({ name: 'active', type: 'boolean', size: undefined, ordinalPosition: 3 })
|
|
846
|
+
])
|
|
847
|
+
),
|
|
848
|
+
schema(),
|
|
849
|
+
'postgres',
|
|
850
|
+
false
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const stmts = generateDDL(diff);
|
|
854
|
+
|
|
855
|
+
assert.ok(stmts.length >= 1);
|
|
856
|
+
const create = stmts[0];
|
|
857
|
+
assert.ok(create.includes('CREATE TABLE "users"'));
|
|
858
|
+
assert.ok(create.includes('"id" SERIAL'));
|
|
859
|
+
assert.ok(create.includes('"name" VARCHAR(100)'));
|
|
860
|
+
assert.ok(create.includes('"active" BOOLEAN'));
|
|
861
|
+
assert.ok(create.includes('PRIMARY KEY'));
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('should generate ALTER TABLE ADD COLUMN', async () => {
|
|
865
|
+
const diff = await compareSchemas(
|
|
866
|
+
schema(
|
|
867
|
+
table('users', [
|
|
868
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
869
|
+
col({ name: 'email', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
870
|
+
])
|
|
871
|
+
),
|
|
872
|
+
schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })])),
|
|
873
|
+
'postgres',
|
|
874
|
+
false
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
const stmts = generateDDL(diff);
|
|
878
|
+
|
|
879
|
+
assert.ok(stmts.some(s => s.includes('ADD COLUMN') && s.includes('"email"') && s.includes('VARCHAR(255)')));
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
it('should generate ALTER COLUMN TYPE', async () => {
|
|
883
|
+
const diff = await compareSchemas(
|
|
884
|
+
schema(
|
|
885
|
+
table('users', [
|
|
886
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
887
|
+
col({ name: 'bio', type: 'text', size: undefined, ordinalPosition: 2 })
|
|
888
|
+
])
|
|
889
|
+
),
|
|
890
|
+
schema(
|
|
891
|
+
table('users', [
|
|
892
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
893
|
+
col({ name: 'bio', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
894
|
+
])
|
|
895
|
+
),
|
|
896
|
+
'postgres',
|
|
897
|
+
false
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
const stmts = generateDDL(diff);
|
|
901
|
+
|
|
902
|
+
assert.ok(stmts.some(s => s.includes('ALTER COLUMN "bio" TYPE TEXT')));
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('should generate SET/DROP NOT NULL', async () => {
|
|
906
|
+
const diff = await compareSchemas(
|
|
907
|
+
schema(
|
|
908
|
+
table('users', [
|
|
909
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
910
|
+
col({ name: 'bio', nullable: true, ordinalPosition: 2 })
|
|
911
|
+
])
|
|
912
|
+
),
|
|
913
|
+
schema(
|
|
914
|
+
table('users', [
|
|
915
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
916
|
+
col({ name: 'bio', nullable: false, ordinalPosition: 2 })
|
|
917
|
+
])
|
|
918
|
+
),
|
|
919
|
+
'postgres',
|
|
920
|
+
false
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
const stmts = generateDDL(diff);
|
|
924
|
+
|
|
925
|
+
assert.ok(stmts.some(s => s.includes('DROP NOT NULL')));
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('should generate CREATE TYPE for enums', async () => {
|
|
929
|
+
const diff = await compareSchemas(
|
|
930
|
+
schema(
|
|
931
|
+
table('users', [
|
|
932
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
933
|
+
col({ name: 'role', type: 'enum', enumValues: ['admin', 'user'], enumTypeName: 'users_role', ordinalPosition: 2 })
|
|
934
|
+
])
|
|
935
|
+
),
|
|
936
|
+
schema(),
|
|
937
|
+
'postgres',
|
|
938
|
+
false
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
const stmts = generateDDL(diff);
|
|
942
|
+
|
|
943
|
+
assert.ok(stmts.some(s => s.includes("CREATE TYPE \"users_role\" AS ENUM ('admin', 'user')")));
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('should generate ALTER TYPE ADD VALUE', async () => {
|
|
947
|
+
const diff = await compareSchemas(
|
|
948
|
+
schema(
|
|
949
|
+
table('users', [
|
|
950
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
951
|
+
col({
|
|
952
|
+
name: 'status',
|
|
953
|
+
type: 'enum',
|
|
954
|
+
enumValues: ['active', 'inactive', 'banned'],
|
|
955
|
+
enumTypeName: 'users_status',
|
|
956
|
+
ordinalPosition: 2
|
|
957
|
+
})
|
|
958
|
+
])
|
|
959
|
+
),
|
|
960
|
+
schema(
|
|
961
|
+
table('users', [
|
|
962
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
963
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
964
|
+
])
|
|
965
|
+
),
|
|
966
|
+
'postgres',
|
|
967
|
+
false
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
const stmts = generateDDL(diff);
|
|
971
|
+
|
|
972
|
+
assert.ok(stmts.some(s => s.includes('ALTER TYPE "users_status" ADD VALUE \'banned\'')));
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
it('should generate enum type recreation when values are removed', async () => {
|
|
976
|
+
const diff = await compareSchemas(
|
|
977
|
+
schema(
|
|
978
|
+
table('users', [
|
|
979
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
980
|
+
col({
|
|
981
|
+
name: 'status',
|
|
982
|
+
type: 'enum',
|
|
983
|
+
enumValues: ['active', 'banned'],
|
|
984
|
+
enumTypeName: 'users_status',
|
|
985
|
+
ordinalPosition: 2
|
|
986
|
+
})
|
|
987
|
+
])
|
|
988
|
+
),
|
|
989
|
+
schema(
|
|
990
|
+
table('users', [
|
|
991
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
992
|
+
col({
|
|
993
|
+
name: 'status',
|
|
994
|
+
type: 'enum',
|
|
995
|
+
enumValues: ['active', 'inactive'],
|
|
996
|
+
enumTypeName: 'users_status',
|
|
997
|
+
ordinalPosition: 2
|
|
998
|
+
})
|
|
999
|
+
])
|
|
1000
|
+
),
|
|
1001
|
+
'postgres',
|
|
1002
|
+
false
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
const stmts = generateDDL(diff);
|
|
1006
|
+
|
|
1007
|
+
// Should generate RENAME, CREATE, ALTER COLUMN TYPE, DROP for enum recreation
|
|
1008
|
+
assert.ok(stmts.some(s => s.includes('ALTER TYPE "users_status" RENAME TO "users_status_old"')));
|
|
1009
|
+
assert.ok(stmts.some(s => s.includes("CREATE TYPE \"users_status\" AS ENUM ('active', 'banned')")));
|
|
1010
|
+
assert.ok(
|
|
1011
|
+
stmts.some(
|
|
1012
|
+
s =>
|
|
1013
|
+
s.includes('ALTER TABLE "users" ALTER COLUMN "status" TYPE "users_status"') &&
|
|
1014
|
+
s.includes('USING "status"::text::"users_status"')
|
|
1015
|
+
)
|
|
1016
|
+
);
|
|
1017
|
+
assert.ok(stmts.some(s => s.includes('DROP TYPE IF EXISTS "users_status_old"')));
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('should generate JSONB for objects', async () => {
|
|
1021
|
+
const diff = await compareSchemas(
|
|
1022
|
+
schema(
|
|
1023
|
+
table('events', [
|
|
1024
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1025
|
+
col({ name: 'data', type: 'jsonb', size: undefined, ordinalPosition: 2 })
|
|
1026
|
+
])
|
|
1027
|
+
),
|
|
1028
|
+
schema(),
|
|
1029
|
+
'postgres',
|
|
1030
|
+
false
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
const stmts = generateDDL(diff);
|
|
1034
|
+
const create = stmts[0];
|
|
1035
|
+
|
|
1036
|
+
assert.ok(create.includes('JSONB'));
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it('should use BIGSERIAL for bigint auto-increment', async () => {
|
|
1040
|
+
const diff = await compareSchemas(
|
|
1041
|
+
schema(table('events', [col({ name: 'id', type: 'bigint', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })])),
|
|
1042
|
+
schema(),
|
|
1043
|
+
'postgres',
|
|
1044
|
+
false
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
const stmts = generateDDL(diff);
|
|
1048
|
+
const create = stmts[0];
|
|
1049
|
+
|
|
1050
|
+
assert.ok(create.includes('BIGSERIAL'));
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('should use custom PK constraint name in DROP CONSTRAINT', async () => {
|
|
1054
|
+
const entity = schema(
|
|
1055
|
+
table('users', [
|
|
1056
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1057
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: false, ordinalPosition: 2 })
|
|
1058
|
+
])
|
|
1059
|
+
);
|
|
1060
|
+
const dbTable = table('users', [
|
|
1061
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }),
|
|
1062
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: true, ordinalPosition: 2 })
|
|
1063
|
+
]);
|
|
1064
|
+
dbTable.primaryKeyConstraintName = 'users_pk_custom';
|
|
1065
|
+
const db = schema(dbTable);
|
|
1066
|
+
|
|
1067
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1068
|
+
const stmts = generateDDL(diff);
|
|
1069
|
+
|
|
1070
|
+
assert.ok(stmts.some(s => s.includes('DROP CONSTRAINT "users_pk_custom"')));
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
describe('FK DDL', () => {
|
|
1075
|
+
it('should generate ADD CONSTRAINT for MySQL', async () => {
|
|
1076
|
+
const diff = await compareSchemas(
|
|
1077
|
+
schema(
|
|
1078
|
+
table(
|
|
1079
|
+
'posts',
|
|
1080
|
+
[
|
|
1081
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1082
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1083
|
+
],
|
|
1084
|
+
{
|
|
1085
|
+
foreignKeys: [
|
|
1086
|
+
{
|
|
1087
|
+
name: 'fk_posts_user_id',
|
|
1088
|
+
columns: ['user_id'],
|
|
1089
|
+
referencedTable: 'users',
|
|
1090
|
+
referencedColumns: ['id'],
|
|
1091
|
+
onDelete: 'CASCADE',
|
|
1092
|
+
onUpdate: 'RESTRICT'
|
|
1093
|
+
}
|
|
1094
|
+
]
|
|
1095
|
+
}
|
|
1096
|
+
)
|
|
1097
|
+
),
|
|
1098
|
+
schema(
|
|
1099
|
+
table('posts', [
|
|
1100
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1101
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1102
|
+
])
|
|
1103
|
+
),
|
|
1104
|
+
'mysql',
|
|
1105
|
+
false
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
const stmts = generateDDL(diff);
|
|
1109
|
+
|
|
1110
|
+
assert.ok(stmts.some(s => s.includes('ADD CONSTRAINT') && s.includes('FOREIGN KEY') && s.includes('ON DELETE CASCADE')));
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
it('should generate ADD CONSTRAINT for PostgreSQL', async () => {
|
|
1114
|
+
const diff = await compareSchemas(
|
|
1115
|
+
schema(
|
|
1116
|
+
table(
|
|
1117
|
+
'posts',
|
|
1118
|
+
[
|
|
1119
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1120
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1121
|
+
],
|
|
1122
|
+
{
|
|
1123
|
+
foreignKeys: [
|
|
1124
|
+
{
|
|
1125
|
+
name: 'fk_posts_user_id',
|
|
1126
|
+
columns: ['user_id'],
|
|
1127
|
+
referencedTable: 'users',
|
|
1128
|
+
referencedColumns: ['id'],
|
|
1129
|
+
onDelete: 'CASCADE',
|
|
1130
|
+
onUpdate: 'RESTRICT'
|
|
1131
|
+
}
|
|
1132
|
+
]
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
),
|
|
1136
|
+
schema(
|
|
1137
|
+
table('posts', [
|
|
1138
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1139
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1140
|
+
])
|
|
1141
|
+
),
|
|
1142
|
+
'postgres',
|
|
1143
|
+
false
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
const stmts = generateDDL(diff);
|
|
1147
|
+
|
|
1148
|
+
assert.ok(stmts.some(s => s.includes('ADD CONSTRAINT') && s.includes('"fk_posts_user_id"') && s.includes('ON DELETE CASCADE')));
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
describe('empty diff', () => {
|
|
1153
|
+
it('should produce no statements for identical schemas', async () => {
|
|
1154
|
+
const t = table('users', [
|
|
1155
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1156
|
+
col({ name: 'name', ordinalPosition: 2 })
|
|
1157
|
+
]);
|
|
1158
|
+
|
|
1159
|
+
const diff = await compareSchemas(schema(t), schema(t), 'mysql', false);
|
|
1160
|
+
const stmts = generateDDL(diff);
|
|
1161
|
+
|
|
1162
|
+
assert.equal(stmts.length, 0);
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
describe('ADD COLUMN AFTER clause', () => {
|
|
1167
|
+
it('should generate AFTER clause for added columns using entityColumns', async () => {
|
|
1168
|
+
const diff = await compareSchemas(
|
|
1169
|
+
schema(
|
|
1170
|
+
table('users', [
|
|
1171
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1172
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
1173
|
+
col({ name: 'email', ordinalPosition: 3 })
|
|
1174
|
+
])
|
|
1175
|
+
),
|
|
1176
|
+
schema(
|
|
1177
|
+
table('users', [
|
|
1178
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1179
|
+
col({ name: 'name', ordinalPosition: 2 })
|
|
1180
|
+
])
|
|
1181
|
+
),
|
|
1182
|
+
'mysql',
|
|
1183
|
+
false
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
const stmts = generateDDL(diff);
|
|
1187
|
+
const addStmt = stmts.find(s => s.includes('ADD COLUMN') && s.includes('`email`'));
|
|
1188
|
+
assert.ok(addStmt);
|
|
1189
|
+
assert.ok(addStmt!.includes('AFTER `name`'));
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
describe('non-enum to enum conversion (PG)', () => {
|
|
1194
|
+
it('should CREATE TYPE when existing column changes from varchar to enum', async () => {
|
|
1195
|
+
const diff = await compareSchemas(
|
|
1196
|
+
schema(
|
|
1197
|
+
table('users', [
|
|
1198
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1199
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
1200
|
+
])
|
|
1201
|
+
),
|
|
1202
|
+
schema(
|
|
1203
|
+
table('users', [
|
|
1204
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1205
|
+
col({ name: 'status', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
1206
|
+
])
|
|
1207
|
+
),
|
|
1208
|
+
'postgres',
|
|
1209
|
+
false
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
const stmts = generateDDL(diff);
|
|
1213
|
+
|
|
1214
|
+
// Should CREATE TYPE before ALTER COLUMN TYPE
|
|
1215
|
+
const createTypeIdx = stmts.findIndex(s => s.includes('CREATE TYPE "users_status"'));
|
|
1216
|
+
const alterColumnIdx = stmts.findIndex(s => s.includes('ALTER COLUMN "status" TYPE'));
|
|
1217
|
+
assert.ok(createTypeIdx >= 0, 'Should generate CREATE TYPE');
|
|
1218
|
+
assert.ok(alterColumnIdx >= 0, 'Should generate ALTER COLUMN TYPE');
|
|
1219
|
+
assert.ok(createTypeIdx < alterColumnIdx, 'CREATE TYPE should come before ALTER COLUMN TYPE');
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it('should include USING cast when converting to enum type', async () => {
|
|
1223
|
+
const diff = await compareSchemas(
|
|
1224
|
+
schema(
|
|
1225
|
+
table('users', [
|
|
1226
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1227
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
1228
|
+
])
|
|
1229
|
+
),
|
|
1230
|
+
schema(
|
|
1231
|
+
table('users', [
|
|
1232
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1233
|
+
col({ name: 'status', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
1234
|
+
])
|
|
1235
|
+
),
|
|
1236
|
+
'postgres',
|
|
1237
|
+
false
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
const stmts = generateDDL(diff);
|
|
1241
|
+
|
|
1242
|
+
const alterStmt = stmts.find(s => s.includes('ALTER COLUMN "status" TYPE'));
|
|
1243
|
+
assert.ok(alterStmt);
|
|
1244
|
+
assert.ok(alterStmt!.includes('USING "status"::text::"users_status"'), 'Should include USING cast for enum type conversion');
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
describe('composite foreign keys', () => {
|
|
1249
|
+
it('should detect composite FK addition', async () => {
|
|
1250
|
+
const entity = schema(
|
|
1251
|
+
table(
|
|
1252
|
+
'order_items',
|
|
1253
|
+
[
|
|
1254
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1255
|
+
col({ name: 'order_id', type: 'int', ordinalPosition: 2 }),
|
|
1256
|
+
col({ name: 'product_id', type: 'int', ordinalPosition: 3 })
|
|
1257
|
+
],
|
|
1258
|
+
{
|
|
1259
|
+
foreignKeys: [
|
|
1260
|
+
{
|
|
1261
|
+
name: 'fk_order_items_composite',
|
|
1262
|
+
columns: ['order_id', 'product_id'],
|
|
1263
|
+
referencedTable: 'order_products',
|
|
1264
|
+
referencedColumns: ['order_id', 'product_id'],
|
|
1265
|
+
onDelete: 'CASCADE',
|
|
1266
|
+
onUpdate: 'RESTRICT'
|
|
1267
|
+
}
|
|
1268
|
+
]
|
|
1269
|
+
}
|
|
1270
|
+
)
|
|
1271
|
+
);
|
|
1272
|
+
const db = schema(
|
|
1273
|
+
table('order_items', [
|
|
1274
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1275
|
+
col({ name: 'order_id', type: 'int', ordinalPosition: 2 }),
|
|
1276
|
+
col({ name: 'product_id', type: 'int', ordinalPosition: 3 })
|
|
1277
|
+
])
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1281
|
+
|
|
1282
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
1283
|
+
assert.equal(diff.modifiedTables[0].addedForeignKeys.length, 1);
|
|
1284
|
+
const fk = diff.modifiedTables[0].addedForeignKeys[0];
|
|
1285
|
+
assert.deepEqual(fk.columns, ['order_id', 'product_id']);
|
|
1286
|
+
assert.deepEqual(fk.referencedColumns, ['order_id', 'product_id']);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it('should generate composite FK DDL for PG', async () => {
|
|
1290
|
+
const diff = await compareSchemas(
|
|
1291
|
+
schema(
|
|
1292
|
+
table(
|
|
1293
|
+
'order_items',
|
|
1294
|
+
[
|
|
1295
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1296
|
+
col({ name: 'order_id', type: 'int', ordinalPosition: 2 }),
|
|
1297
|
+
col({ name: 'product_id', type: 'int', ordinalPosition: 3 })
|
|
1298
|
+
],
|
|
1299
|
+
{
|
|
1300
|
+
foreignKeys: [
|
|
1301
|
+
{
|
|
1302
|
+
name: 'fk_composite',
|
|
1303
|
+
columns: ['order_id', 'product_id'],
|
|
1304
|
+
referencedTable: 'order_products',
|
|
1305
|
+
referencedColumns: ['order_id', 'product_id'],
|
|
1306
|
+
onDelete: 'CASCADE',
|
|
1307
|
+
onUpdate: 'RESTRICT'
|
|
1308
|
+
}
|
|
1309
|
+
]
|
|
1310
|
+
}
|
|
1311
|
+
)
|
|
1312
|
+
),
|
|
1313
|
+
schema(
|
|
1314
|
+
table('order_items', [
|
|
1315
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1316
|
+
col({ name: 'order_id', type: 'int', ordinalPosition: 2 }),
|
|
1317
|
+
col({ name: 'product_id', type: 'int', ordinalPosition: 3 })
|
|
1318
|
+
])
|
|
1319
|
+
),
|
|
1320
|
+
'postgres',
|
|
1321
|
+
false
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
const stmts = generateDDL(diff);
|
|
1325
|
+
const fkStmt = stmts.find(s => s.includes('ADD CONSTRAINT') && s.includes('fk_composite'));
|
|
1326
|
+
assert.ok(fkStmt);
|
|
1327
|
+
assert.ok(fkStmt!.includes('"order_id", "product_id"'));
|
|
1328
|
+
assert.ok(fkStmt!.includes('REFERENCES "order_products"'));
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
describe('duplicate enum types across tables', () => {
|
|
1333
|
+
it('should deduplicate CREATE TYPE when same enum used in multiple new tables', async () => {
|
|
1334
|
+
const diff = await compareSchemas(
|
|
1335
|
+
schema(
|
|
1336
|
+
table('users', [
|
|
1337
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1338
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'shared_status', ordinalPosition: 2 })
|
|
1339
|
+
]),
|
|
1340
|
+
table('admins', [
|
|
1341
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1342
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'shared_status', ordinalPosition: 2 })
|
|
1343
|
+
])
|
|
1344
|
+
),
|
|
1345
|
+
schema(),
|
|
1346
|
+
'postgres',
|
|
1347
|
+
false
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
const stmts = generateDDL(diff);
|
|
1351
|
+
const createTypes = stmts.filter(s => s.includes('CREATE TYPE "shared_status"'));
|
|
1352
|
+
assert.equal(createTypes.length, 1, 'Should only have one CREATE TYPE for shared_status');
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
describe('FK-dependent table drops', () => {
|
|
1357
|
+
it('should drop FKs before dropping tables with dependencies', async () => {
|
|
1358
|
+
const diff = await compareSchemas(
|
|
1359
|
+
schema(),
|
|
1360
|
+
schema(
|
|
1361
|
+
table(
|
|
1362
|
+
'posts',
|
|
1363
|
+
[
|
|
1364
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1365
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1366
|
+
],
|
|
1367
|
+
{
|
|
1368
|
+
foreignKeys: [
|
|
1369
|
+
{
|
|
1370
|
+
name: 'fk_posts_user',
|
|
1371
|
+
columns: ['user_id'],
|
|
1372
|
+
referencedTable: 'users',
|
|
1373
|
+
referencedColumns: ['id'],
|
|
1374
|
+
onDelete: 'CASCADE',
|
|
1375
|
+
onUpdate: 'RESTRICT'
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
}
|
|
1379
|
+
),
|
|
1380
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })])
|
|
1381
|
+
),
|
|
1382
|
+
'postgres',
|
|
1383
|
+
false
|
|
1384
|
+
);
|
|
1385
|
+
|
|
1386
|
+
const stmts = generateDDL(diff);
|
|
1387
|
+
|
|
1388
|
+
const dropFkIdx = stmts.findIndex(s => s.includes('DROP CONSTRAINT') && s.includes('fk_posts_user'));
|
|
1389
|
+
const dropTableIdx = stmts.findIndex(s => s.includes('DROP TABLE') && s.includes('"posts"'));
|
|
1390
|
+
assert.ok(dropFkIdx >= 0, 'Should drop FK');
|
|
1391
|
+
assert.ok(dropTableIdx >= 0, 'Should drop table');
|
|
1392
|
+
assert.ok(dropFkIdx < dropTableIdx, 'FK drop should come before table drop');
|
|
1393
|
+
});
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
describe('FK action validation', () => {
|
|
1397
|
+
it('should reject invalid FK action values', async () => {
|
|
1398
|
+
const diff = await compareSchemas(
|
|
1399
|
+
schema(
|
|
1400
|
+
table(
|
|
1401
|
+
'posts',
|
|
1402
|
+
[
|
|
1403
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1404
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1405
|
+
],
|
|
1406
|
+
{
|
|
1407
|
+
foreignKeys: [
|
|
1408
|
+
{
|
|
1409
|
+
name: 'fk_bad',
|
|
1410
|
+
columns: ['user_id'],
|
|
1411
|
+
referencedTable: 'users',
|
|
1412
|
+
referencedColumns: ['id'],
|
|
1413
|
+
onDelete: 'DROP TABLE users; --',
|
|
1414
|
+
onUpdate: 'RESTRICT'
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
}
|
|
1418
|
+
)
|
|
1419
|
+
),
|
|
1420
|
+
schema(
|
|
1421
|
+
table('posts', [
|
|
1422
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1423
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1424
|
+
])
|
|
1425
|
+
),
|
|
1426
|
+
'mysql',
|
|
1427
|
+
false
|
|
1428
|
+
);
|
|
1429
|
+
|
|
1430
|
+
assert.throws(() => generateDDL(diff), /Invalid foreign key action/);
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
it('should accept valid FK actions including NO ACTION', async () => {
|
|
1434
|
+
const diff = await compareSchemas(
|
|
1435
|
+
schema(
|
|
1436
|
+
table(
|
|
1437
|
+
'posts',
|
|
1438
|
+
[
|
|
1439
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1440
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1441
|
+
],
|
|
1442
|
+
{
|
|
1443
|
+
foreignKeys: [
|
|
1444
|
+
{
|
|
1445
|
+
name: 'fk_ok',
|
|
1446
|
+
columns: ['user_id'],
|
|
1447
|
+
referencedTable: 'users',
|
|
1448
|
+
referencedColumns: ['id'],
|
|
1449
|
+
onDelete: 'SET NULL',
|
|
1450
|
+
onUpdate: 'NO ACTION'
|
|
1451
|
+
}
|
|
1452
|
+
]
|
|
1453
|
+
}
|
|
1454
|
+
)
|
|
1455
|
+
),
|
|
1456
|
+
schema(
|
|
1457
|
+
table('posts', [
|
|
1458
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1459
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1460
|
+
])
|
|
1461
|
+
),
|
|
1462
|
+
'postgres',
|
|
1463
|
+
false
|
|
1464
|
+
);
|
|
1465
|
+
|
|
1466
|
+
const stmts = generateDDL(diff);
|
|
1467
|
+
assert.ok(stmts.some(s => s.includes('ON DELETE SET NULL') && s.includes('ON UPDATE NO ACTION')));
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
describe('enum type name comparison', () => {
|
|
1472
|
+
it('should detect enum type name change as a modification', async () => {
|
|
1473
|
+
const entity = schema(
|
|
1474
|
+
table('users', [
|
|
1475
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1476
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status_v2', ordinalPosition: 2 })
|
|
1477
|
+
])
|
|
1478
|
+
);
|
|
1479
|
+
const db = schema(
|
|
1480
|
+
table('users', [
|
|
1481
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1482
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
1483
|
+
])
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1487
|
+
|
|
1488
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
1489
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns.length, 1);
|
|
1490
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns[0].typeChanged, true);
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
describe('schema-qualified index and constraint DDL', () => {
|
|
1495
|
+
it('should schema-qualify table names in index DDL for non-public schema', async () => {
|
|
1496
|
+
const diff = await compareSchemas(
|
|
1497
|
+
schema(
|
|
1498
|
+
table(
|
|
1499
|
+
'users',
|
|
1500
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
1501
|
+
{ indexes: [{ name: 'idx_email', columns: ['email'], unique: true, spatial: false }] }
|
|
1502
|
+
)
|
|
1503
|
+
),
|
|
1504
|
+
schema(
|
|
1505
|
+
table('users', [
|
|
1506
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1507
|
+
col({ name: 'email', ordinalPosition: 2 })
|
|
1508
|
+
])
|
|
1509
|
+
),
|
|
1510
|
+
'postgres',
|
|
1511
|
+
false,
|
|
1512
|
+
'myapp'
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
const stmts = generateDDL(diff);
|
|
1516
|
+
const idxStmt = stmts.find(s => s.includes('CREATE UNIQUE INDEX'));
|
|
1517
|
+
assert.ok(idxStmt);
|
|
1518
|
+
assert.ok(idxStmt!.includes('"myapp"."users"'), 'Index DDL should use schema-qualified table name');
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
it('should schema-qualify table names in FK DDL for non-public schema', async () => {
|
|
1522
|
+
const diff = await compareSchemas(
|
|
1523
|
+
schema(
|
|
1524
|
+
table(
|
|
1525
|
+
'posts',
|
|
1526
|
+
[
|
|
1527
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1528
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1529
|
+
],
|
|
1530
|
+
{
|
|
1531
|
+
foreignKeys: [
|
|
1532
|
+
{
|
|
1533
|
+
name: 'fk_posts_user',
|
|
1534
|
+
columns: ['user_id'],
|
|
1535
|
+
referencedTable: 'users',
|
|
1536
|
+
referencedColumns: ['id'],
|
|
1537
|
+
onDelete: 'CASCADE',
|
|
1538
|
+
onUpdate: 'RESTRICT'
|
|
1539
|
+
}
|
|
1540
|
+
]
|
|
1541
|
+
}
|
|
1542
|
+
)
|
|
1543
|
+
),
|
|
1544
|
+
schema(
|
|
1545
|
+
table('posts', [
|
|
1546
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1547
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1548
|
+
])
|
|
1549
|
+
),
|
|
1550
|
+
'postgres',
|
|
1551
|
+
false,
|
|
1552
|
+
'myapp'
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
const stmts = generateDDL(diff);
|
|
1556
|
+
const fkStmt = stmts.find(s => s.includes('ADD CONSTRAINT'));
|
|
1557
|
+
assert.ok(fkStmt);
|
|
1558
|
+
assert.ok(fkStmt!.includes('"myapp"."posts"'), 'FK DDL should use schema-qualified source table');
|
|
1559
|
+
assert.ok(fkStmt!.includes('"myapp"."users"'), 'FK DDL should use schema-qualified referenced table');
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
describe('schema-qualified PG DDL', () => {
|
|
1564
|
+
it('should qualify table names when pgSchema is not public', async () => {
|
|
1565
|
+
const diff = await compareSchemas(
|
|
1566
|
+
schema(
|
|
1567
|
+
table('users', [
|
|
1568
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1569
|
+
col({ name: 'name', type: 'varchar', size: 100, ordinalPosition: 2 })
|
|
1570
|
+
])
|
|
1571
|
+
),
|
|
1572
|
+
schema(),
|
|
1573
|
+
'postgres',
|
|
1574
|
+
false,
|
|
1575
|
+
'myapp'
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
const stmts = generateDDL(diff);
|
|
1579
|
+
const create = stmts[0];
|
|
1580
|
+
assert.ok(create.includes('"myapp"."users"'), 'Should qualify table with schema name');
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
it('should not qualify table names when pgSchema is public', async () => {
|
|
1584
|
+
const diff = await compareSchemas(
|
|
1585
|
+
schema(
|
|
1586
|
+
table('users', [
|
|
1587
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1588
|
+
col({ name: 'name', type: 'varchar', size: 100, ordinalPosition: 2 })
|
|
1589
|
+
])
|
|
1590
|
+
),
|
|
1591
|
+
schema(),
|
|
1592
|
+
'postgres',
|
|
1593
|
+
false,
|
|
1594
|
+
'public'
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
const stmts = generateDDL(diff);
|
|
1598
|
+
const create = stmts[0];
|
|
1599
|
+
assert.ok(create.includes('CREATE TABLE "users"'), 'Should use unqualified name for public schema');
|
|
1600
|
+
assert.ok(!create.includes('"public"."users"'), 'Should not qualify with public schema');
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
it('should schema-qualify DROP INDEX for non-public schema', async () => {
|
|
1604
|
+
const diff = await compareSchemas(
|
|
1605
|
+
schema(
|
|
1606
|
+
table('users', [
|
|
1607
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1608
|
+
col({ name: 'email', ordinalPosition: 2 })
|
|
1609
|
+
])
|
|
1610
|
+
),
|
|
1611
|
+
schema(
|
|
1612
|
+
table(
|
|
1613
|
+
'users',
|
|
1614
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'email', ordinalPosition: 2 })],
|
|
1615
|
+
{ indexes: [{ name: 'idx_email', columns: ['email'], unique: false, spatial: false }] }
|
|
1616
|
+
)
|
|
1617
|
+
),
|
|
1618
|
+
'postgres',
|
|
1619
|
+
false,
|
|
1620
|
+
'myapp'
|
|
1621
|
+
);
|
|
1622
|
+
|
|
1623
|
+
const stmts = generateDDL(diff);
|
|
1624
|
+
assert.ok(
|
|
1625
|
+
stmts.some(s => s.includes('DROP INDEX "myapp"."idx_email"')),
|
|
1626
|
+
'DROP INDEX should be schema-qualified'
|
|
1627
|
+
);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
it('should schema-qualify enum types for non-public schema', async () => {
|
|
1631
|
+
const diff = await compareSchemas(
|
|
1632
|
+
schema(
|
|
1633
|
+
table('users', [
|
|
1634
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1635
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'users_status', ordinalPosition: 2 })
|
|
1636
|
+
])
|
|
1637
|
+
),
|
|
1638
|
+
schema(),
|
|
1639
|
+
'postgres',
|
|
1640
|
+
false,
|
|
1641
|
+
'myapp'
|
|
1642
|
+
);
|
|
1643
|
+
|
|
1644
|
+
const stmts = generateDDL(diff);
|
|
1645
|
+
assert.ok(
|
|
1646
|
+
stmts.some(s => s.includes('"myapp"."users_status"')),
|
|
1647
|
+
'CREATE TYPE should be schema-qualified'
|
|
1648
|
+
);
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
describe('enum type name change', () => {
|
|
1653
|
+
it('should produce CREATE TYPE and ALTER COLUMN TYPE when enum type name changes', async () => {
|
|
1654
|
+
const diff = await compareSchemas(
|
|
1655
|
+
schema(
|
|
1656
|
+
table('users', [
|
|
1657
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1658
|
+
col({
|
|
1659
|
+
name: 'status',
|
|
1660
|
+
type: 'enum',
|
|
1661
|
+
enumValues: ['active', 'inactive'],
|
|
1662
|
+
enumTypeName: 'users_status_v2',
|
|
1663
|
+
ordinalPosition: 2
|
|
1664
|
+
})
|
|
1665
|
+
])
|
|
1666
|
+
),
|
|
1667
|
+
schema(
|
|
1668
|
+
table('users', [
|
|
1669
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1670
|
+
col({
|
|
1671
|
+
name: 'status',
|
|
1672
|
+
type: 'enum',
|
|
1673
|
+
enumValues: ['active', 'inactive'],
|
|
1674
|
+
enumTypeName: 'users_status',
|
|
1675
|
+
ordinalPosition: 2
|
|
1676
|
+
})
|
|
1677
|
+
])
|
|
1678
|
+
),
|
|
1679
|
+
'postgres',
|
|
1680
|
+
false
|
|
1681
|
+
);
|
|
1682
|
+
|
|
1683
|
+
const stmts = generateDDL(diff);
|
|
1684
|
+
|
|
1685
|
+
// Should CREATE TYPE for the new enum type name
|
|
1686
|
+
assert.ok(
|
|
1687
|
+
stmts.some(s => s.includes('CREATE TYPE "users_status_v2"')),
|
|
1688
|
+
'Should create the new enum type: ' + JSON.stringify(stmts)
|
|
1689
|
+
);
|
|
1690
|
+
// Should ALTER COLUMN TYPE with USING cast to the new type
|
|
1691
|
+
assert.ok(
|
|
1692
|
+
stmts.some(s => s.includes('ALTER COLUMN "status" TYPE "users_status_v2"') && s.includes('USING')),
|
|
1693
|
+
'Should alter column to new enum type with USING cast'
|
|
1694
|
+
);
|
|
1695
|
+
});
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1698
|
+
describe('FK ordering for new interdependent tables', () => {
|
|
1699
|
+
it('should emit FKs after all tables are created', async () => {
|
|
1700
|
+
// Table A references Table B and vice versa (circular dependency)
|
|
1701
|
+
const tableA = table(
|
|
1702
|
+
'orders',
|
|
1703
|
+
[col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'user_id', type: 'int', ordinalPosition: 2 })],
|
|
1704
|
+
{
|
|
1705
|
+
foreignKeys: [
|
|
1706
|
+
{
|
|
1707
|
+
name: 'fk_orders_user',
|
|
1708
|
+
columns: ['user_id'],
|
|
1709
|
+
referencedTable: 'users',
|
|
1710
|
+
referencedColumns: ['id'],
|
|
1711
|
+
onDelete: 'CASCADE',
|
|
1712
|
+
onUpdate: 'RESTRICT'
|
|
1713
|
+
}
|
|
1714
|
+
]
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
const tableB = table(
|
|
1718
|
+
'users',
|
|
1719
|
+
[
|
|
1720
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1721
|
+
col({ name: 'last_order_id', type: 'int', nullable: true, ordinalPosition: 2 })
|
|
1722
|
+
],
|
|
1723
|
+
{
|
|
1724
|
+
foreignKeys: [
|
|
1725
|
+
{
|
|
1726
|
+
name: 'fk_users_last_order',
|
|
1727
|
+
columns: ['last_order_id'],
|
|
1728
|
+
referencedTable: 'orders',
|
|
1729
|
+
referencedColumns: ['id'],
|
|
1730
|
+
onDelete: 'SET NULL',
|
|
1731
|
+
onUpdate: 'RESTRICT'
|
|
1732
|
+
}
|
|
1733
|
+
]
|
|
1734
|
+
}
|
|
1735
|
+
);
|
|
1736
|
+
|
|
1737
|
+
const diff = await compareSchemas(schema(tableA, tableB), schema(), 'postgres', false);
|
|
1738
|
+
const stmts = generateDDL(diff);
|
|
1739
|
+
|
|
1740
|
+
// All CREATE TABLE statements should come before any ADD CONSTRAINT FK statements
|
|
1741
|
+
const createTableIdxs = stmts.map((s, i) => (s.includes('CREATE TABLE') ? i : -1)).filter(i => i >= 0);
|
|
1742
|
+
const addFkIdxs = stmts.map((s, i) => (s.includes('ADD CONSTRAINT') && s.includes('FOREIGN KEY') ? i : -1)).filter(i => i >= 0);
|
|
1743
|
+
|
|
1744
|
+
assert.ok(createTableIdxs.length === 2, 'Should have 2 CREATE TABLE statements');
|
|
1745
|
+
assert.ok(addFkIdxs.length === 2, 'Should have 2 FK constraints');
|
|
1746
|
+
const maxCreateTable = Math.max(...createTableIdxs);
|
|
1747
|
+
const minAddFk = Math.min(...addFkIdxs);
|
|
1748
|
+
assert.ok(maxCreateTable < minAddFk, `All CREATE TABLEs (last at ${maxCreateTable}) should come before FKs (first at ${minAddFk})`);
|
|
1749
|
+
});
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
describe('default expression normalization', () => {
|
|
1753
|
+
it('should treat now() and CURRENT_TIMESTAMP as equivalent', async () => {
|
|
1754
|
+
const entity = schema(
|
|
1755
|
+
table('events', [
|
|
1756
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1757
|
+
col({
|
|
1758
|
+
name: 'created_at',
|
|
1759
|
+
type: 'timestamp',
|
|
1760
|
+
size: undefined,
|
|
1761
|
+
defaultExpression: 'CURRENT_TIMESTAMP',
|
|
1762
|
+
ordinalPosition: 2
|
|
1763
|
+
})
|
|
1764
|
+
])
|
|
1765
|
+
);
|
|
1766
|
+
const db = schema(
|
|
1767
|
+
table('events', [
|
|
1768
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1769
|
+
col({ name: 'created_at', type: 'timestamp', size: undefined, defaultExpression: 'now()', ordinalPosition: 2 })
|
|
1770
|
+
])
|
|
1771
|
+
);
|
|
1772
|
+
|
|
1773
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1774
|
+
|
|
1775
|
+
// now() and CURRENT_TIMESTAMP are equivalent — no diff expected
|
|
1776
|
+
assert.equal(diff.modifiedTables.length, 0, 'now() and CURRENT_TIMESTAMP should be treated as equivalent');
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
describe('FK NO ACTION / RESTRICT normalization', () => {
|
|
1781
|
+
it('should treat NO ACTION and RESTRICT as equivalent for comparison', async () => {
|
|
1782
|
+
const entity = schema(
|
|
1783
|
+
table(
|
|
1784
|
+
'posts',
|
|
1785
|
+
[
|
|
1786
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1787
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1788
|
+
],
|
|
1789
|
+
{
|
|
1790
|
+
foreignKeys: [
|
|
1791
|
+
{
|
|
1792
|
+
name: 'fk_posts_user',
|
|
1793
|
+
columns: ['user_id'],
|
|
1794
|
+
referencedTable: 'users',
|
|
1795
|
+
referencedColumns: ['id'],
|
|
1796
|
+
onDelete: 'RESTRICT',
|
|
1797
|
+
onUpdate: 'RESTRICT'
|
|
1798
|
+
}
|
|
1799
|
+
]
|
|
1800
|
+
}
|
|
1801
|
+
)
|
|
1802
|
+
);
|
|
1803
|
+
const db = schema(
|
|
1804
|
+
table(
|
|
1805
|
+
'posts',
|
|
1806
|
+
[
|
|
1807
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1808
|
+
col({ name: 'user_id', type: 'int', ordinalPosition: 2 })
|
|
1809
|
+
],
|
|
1810
|
+
{
|
|
1811
|
+
foreignKeys: [
|
|
1812
|
+
{
|
|
1813
|
+
name: 'fk_posts_user',
|
|
1814
|
+
columns: ['user_id'],
|
|
1815
|
+
referencedTable: 'users',
|
|
1816
|
+
referencedColumns: ['id'],
|
|
1817
|
+
onDelete: 'NO ACTION',
|
|
1818
|
+
onUpdate: 'NO ACTION'
|
|
1819
|
+
}
|
|
1820
|
+
]
|
|
1821
|
+
}
|
|
1822
|
+
)
|
|
1823
|
+
);
|
|
1824
|
+
|
|
1825
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1826
|
+
|
|
1827
|
+
// NO ACTION and RESTRICT are semantically equivalent — no diff expected
|
|
1828
|
+
assert.equal(diff.modifiedTables.length, 0, 'NO ACTION and RESTRICT should be treated as equivalent');
|
|
1829
|
+
});
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
describe('conditional PK drop', () => {
|
|
1833
|
+
it('should not emit DROP PRIMARY KEY when DB has no existing PK (MySQL)', async () => {
|
|
1834
|
+
const entity = schema(
|
|
1835
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'name', ordinalPosition: 2 })])
|
|
1836
|
+
);
|
|
1837
|
+
// DB has no PK
|
|
1838
|
+
const db = schema(
|
|
1839
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }), col({ name: 'name', ordinalPosition: 2 })])
|
|
1840
|
+
);
|
|
1841
|
+
|
|
1842
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
1843
|
+
|
|
1844
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
1845
|
+
assert.equal(diff.modifiedTables[0].primaryKeyChanged, true);
|
|
1846
|
+
assert.deepEqual(diff.modifiedTables[0].oldPrimaryKey, []);
|
|
1847
|
+
|
|
1848
|
+
const stmts = generateDDL(diff);
|
|
1849
|
+
assert.ok(!stmts.some(s => s.includes('DROP PRIMARY KEY')), 'Should not DROP PRIMARY KEY when DB has none');
|
|
1850
|
+
assert.ok(
|
|
1851
|
+
stmts.some(s => s.includes('ADD PRIMARY KEY')),
|
|
1852
|
+
'Should ADD PRIMARY KEY'
|
|
1853
|
+
);
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
it('should not emit DROP CONSTRAINT for PK when DB has no existing PK (PG)', async () => {
|
|
1857
|
+
const entity = schema(
|
|
1858
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }), col({ name: 'name', ordinalPosition: 2 })])
|
|
1859
|
+
);
|
|
1860
|
+
const db = schema(
|
|
1861
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }), col({ name: 'name', ordinalPosition: 2 })])
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1865
|
+
const stmts = generateDDL(diff);
|
|
1866
|
+
|
|
1867
|
+
assert.ok(!stmts.some(s => s.includes('DROP CONSTRAINT') && s.includes('pkey')), 'Should not DROP PK constraint when DB has none');
|
|
1868
|
+
assert.ok(
|
|
1869
|
+
stmts.some(s => s.includes('ADD PRIMARY KEY')),
|
|
1870
|
+
'Should ADD PRIMARY KEY'
|
|
1871
|
+
);
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
it('should emit DROP PRIMARY KEY when DB has an existing PK (MySQL)', async () => {
|
|
1875
|
+
const entity = schema(
|
|
1876
|
+
table('users', [
|
|
1877
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1878
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: false, ordinalPosition: 2 })
|
|
1879
|
+
])
|
|
1880
|
+
);
|
|
1881
|
+
const db = schema(
|
|
1882
|
+
table('users', [
|
|
1883
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }),
|
|
1884
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: true, ordinalPosition: 2 })
|
|
1885
|
+
])
|
|
1886
|
+
);
|
|
1887
|
+
|
|
1888
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
1889
|
+
const stmts = generateDDL(diff);
|
|
1890
|
+
|
|
1891
|
+
assert.ok(
|
|
1892
|
+
stmts.some(s => s.includes('DROP PRIMARY KEY')),
|
|
1893
|
+
'Should DROP PRIMARY KEY when DB has one'
|
|
1894
|
+
);
|
|
1895
|
+
assert.ok(
|
|
1896
|
+
stmts.some(s => s.includes('ADD PRIMARY KEY')),
|
|
1897
|
+
'Should ADD new PRIMARY KEY'
|
|
1898
|
+
);
|
|
1899
|
+
});
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
describe('PG auto-increment changes', () => {
|
|
1903
|
+
it('should emit CREATE SEQUENCE and SET DEFAULT when adding auto-increment', async () => {
|
|
1904
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
1905
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
1906
|
+
|
|
1907
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1908
|
+
const stmts = generateDDL(diff);
|
|
1909
|
+
|
|
1910
|
+
assert.ok(
|
|
1911
|
+
stmts.some(s => s.includes('CREATE SEQUENCE') && s.includes('users_id_seq')),
|
|
1912
|
+
'Should create sequence'
|
|
1913
|
+
);
|
|
1914
|
+
assert.ok(
|
|
1915
|
+
stmts.some(s => s.includes('SET DEFAULT nextval')),
|
|
1916
|
+
'Should set default to nextval'
|
|
1917
|
+
);
|
|
1918
|
+
});
|
|
1919
|
+
|
|
1920
|
+
it('should emit DROP DEFAULT and DROP SEQUENCE when removing auto-increment', async () => {
|
|
1921
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
1922
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
1923
|
+
|
|
1924
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1925
|
+
const stmts = generateDDL(diff);
|
|
1926
|
+
|
|
1927
|
+
assert.ok(
|
|
1928
|
+
stmts.some(s => s.includes('DROP DEFAULT')),
|
|
1929
|
+
'Should drop default'
|
|
1930
|
+
);
|
|
1931
|
+
assert.ok(
|
|
1932
|
+
stmts.some(s => s.includes('DROP SEQUENCE') && s.includes('users_id_seq')),
|
|
1933
|
+
'Should drop sequence'
|
|
1934
|
+
);
|
|
1935
|
+
});
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
describe('shared enum type deduplication', () => {
|
|
1939
|
+
it('should emit ALTER TYPE ADD VALUE once for shared enum across multiple tables', async () => {
|
|
1940
|
+
// Two tables share the same enum type, both modified to add a value
|
|
1941
|
+
const entity = schema(
|
|
1942
|
+
table('users', [
|
|
1943
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1944
|
+
col({
|
|
1945
|
+
name: 'status',
|
|
1946
|
+
type: 'enum',
|
|
1947
|
+
enumValues: ['active', 'inactive', 'banned'],
|
|
1948
|
+
enumTypeName: 'shared_status',
|
|
1949
|
+
ordinalPosition: 2
|
|
1950
|
+
})
|
|
1951
|
+
]),
|
|
1952
|
+
table('admins', [
|
|
1953
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1954
|
+
col({
|
|
1955
|
+
name: 'status',
|
|
1956
|
+
type: 'enum',
|
|
1957
|
+
enumValues: ['active', 'inactive', 'banned'],
|
|
1958
|
+
enumTypeName: 'shared_status',
|
|
1959
|
+
ordinalPosition: 2
|
|
1960
|
+
})
|
|
1961
|
+
])
|
|
1962
|
+
);
|
|
1963
|
+
const db = schema(
|
|
1964
|
+
table('users', [
|
|
1965
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1966
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'shared_status', ordinalPosition: 2 })
|
|
1967
|
+
]),
|
|
1968
|
+
table('admins', [
|
|
1969
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1970
|
+
col({ name: 'status', type: 'enum', enumValues: ['active', 'inactive'], enumTypeName: 'shared_status', ordinalPosition: 2 })
|
|
1971
|
+
])
|
|
1972
|
+
);
|
|
1973
|
+
|
|
1974
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
1975
|
+
const stmts = generateDDL(diff);
|
|
1976
|
+
|
|
1977
|
+
const addValueStmts = stmts.filter(s => s.includes("ADD VALUE 'banned'"));
|
|
1978
|
+
assert.equal(addValueStmts.length, 1, 'Should only emit ADD VALUE once for shared enum type');
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
it('should recreate shared enum type once and cast all affected columns', async () => {
|
|
1982
|
+
const entity = schema(
|
|
1983
|
+
table('users', [
|
|
1984
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1985
|
+
col({
|
|
1986
|
+
name: 'status',
|
|
1987
|
+
type: 'enum',
|
|
1988
|
+
enumValues: ['active', 'banned'],
|
|
1989
|
+
enumTypeName: 'shared_status',
|
|
1990
|
+
ordinalPosition: 2
|
|
1991
|
+
})
|
|
1992
|
+
]),
|
|
1993
|
+
table('admins', [
|
|
1994
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
1995
|
+
col({
|
|
1996
|
+
name: 'status',
|
|
1997
|
+
type: 'enum',
|
|
1998
|
+
enumValues: ['active', 'banned'],
|
|
1999
|
+
enumTypeName: 'shared_status',
|
|
2000
|
+
ordinalPosition: 2
|
|
2001
|
+
})
|
|
2002
|
+
])
|
|
2003
|
+
);
|
|
2004
|
+
const db = schema(
|
|
2005
|
+
table('users', [
|
|
2006
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2007
|
+
col({
|
|
2008
|
+
name: 'status',
|
|
2009
|
+
type: 'enum',
|
|
2010
|
+
enumValues: ['active', 'inactive'],
|
|
2011
|
+
enumTypeName: 'shared_status',
|
|
2012
|
+
ordinalPosition: 2
|
|
2013
|
+
})
|
|
2014
|
+
]),
|
|
2015
|
+
table('admins', [
|
|
2016
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2017
|
+
col({
|
|
2018
|
+
name: 'status',
|
|
2019
|
+
type: 'enum',
|
|
2020
|
+
enumValues: ['active', 'inactive'],
|
|
2021
|
+
enumTypeName: 'shared_status',
|
|
2022
|
+
ordinalPosition: 2
|
|
2023
|
+
})
|
|
2024
|
+
])
|
|
2025
|
+
);
|
|
2026
|
+
|
|
2027
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2028
|
+
const stmts = generateDDL(diff);
|
|
2029
|
+
|
|
2030
|
+
const renameStmts = stmts.filter(s => s.includes('RENAME TO'));
|
|
2031
|
+
assert.equal(renameStmts.length, 1, 'Should only RENAME once for shared enum');
|
|
2032
|
+
|
|
2033
|
+
const createTypeStmts = stmts.filter(s => s.includes('CREATE TYPE "shared_status"'));
|
|
2034
|
+
assert.equal(createTypeStmts.length, 1, 'Should only CREATE TYPE once for shared enum');
|
|
2035
|
+
|
|
2036
|
+
// Should have ALTER COLUMN TYPE for both tables
|
|
2037
|
+
const castStmts = stmts.filter(s => s.includes('ALTER COLUMN "status" TYPE'));
|
|
2038
|
+
assert.equal(castStmts.length, 2, 'Should cast both columns');
|
|
2039
|
+
|
|
2040
|
+
// Two DROP TYPE IF EXISTS: one pre-RENAME (collision avoidance) and one deferred (cleanup)
|
|
2041
|
+
const dropStmts = stmts.filter(s => s.includes('DROP TYPE'));
|
|
2042
|
+
assert.equal(dropStmts.length, 2, 'Should have pre-RENAME and deferred DROP TYPE');
|
|
2043
|
+
});
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
describe('decimal/numeric without precision', () => {
|
|
2047
|
+
it('should generate bare DECIMAL when MySQL column has no precision', async () => {
|
|
2048
|
+
const diff = await compareSchemas(
|
|
2049
|
+
schema(
|
|
2050
|
+
table('products', [
|
|
2051
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2052
|
+
col({ name: 'price', type: 'decimal', size: undefined, ordinalPosition: 2 })
|
|
2053
|
+
])
|
|
2054
|
+
),
|
|
2055
|
+
schema(),
|
|
2056
|
+
'mysql',
|
|
2057
|
+
false
|
|
2058
|
+
);
|
|
2059
|
+
|
|
2060
|
+
const stmts = generateDDL(diff);
|
|
2061
|
+
const create = stmts[0];
|
|
2062
|
+
assert.ok(create.includes('DECIMAL'), 'Should include DECIMAL');
|
|
2063
|
+
assert.ok(!create.includes('DECIMAL('), 'Should not include DECIMAL( with parentheses');
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it('should generate bare NUMERIC when PG column has no precision', async () => {
|
|
2067
|
+
const diff = await compareSchemas(
|
|
2068
|
+
schema(
|
|
2069
|
+
table('products', [
|
|
2070
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2071
|
+
col({ name: 'price', type: 'numeric', size: undefined, ordinalPosition: 2 })
|
|
2072
|
+
])
|
|
2073
|
+
),
|
|
2074
|
+
schema(),
|
|
2075
|
+
'postgres',
|
|
2076
|
+
false
|
|
2077
|
+
);
|
|
2078
|
+
|
|
2079
|
+
const stmts = generateDDL(diff);
|
|
2080
|
+
const create = stmts[0];
|
|
2081
|
+
assert.ok(create.includes('NUMERIC'), 'Should include NUMERIC');
|
|
2082
|
+
assert.ok(!create.includes('NUMERIC('), 'Should not include NUMERIC( with parentheses');
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
it('should generate DECIMAL(10,2) when precision and scale are set', async () => {
|
|
2086
|
+
const diff = await compareSchemas(
|
|
2087
|
+
schema(
|
|
2088
|
+
table('products', [
|
|
2089
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2090
|
+
col({ name: 'price', type: 'decimal', size: 10, scale: 2, ordinalPosition: 2 })
|
|
2091
|
+
])
|
|
2092
|
+
),
|
|
2093
|
+
schema(),
|
|
2094
|
+
'mysql',
|
|
2095
|
+
false
|
|
2096
|
+
);
|
|
2097
|
+
|
|
2098
|
+
const stmts = generateDDL(diff);
|
|
2099
|
+
const create = stmts[0];
|
|
2100
|
+
assert.ok(create.includes('DECIMAL(10,2)'), 'Should include DECIMAL(10,2)');
|
|
2101
|
+
});
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
describe('enum value order normalization', () => {
|
|
2105
|
+
it('should not detect a diff when enum values are the same but in different order', async () => {
|
|
2106
|
+
const entity = schema(
|
|
2107
|
+
table('users', [
|
|
2108
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2109
|
+
col({
|
|
2110
|
+
name: 'status',
|
|
2111
|
+
type: 'enum',
|
|
2112
|
+
enumValues: ['inactive', 'active'],
|
|
2113
|
+
enumTypeName: 'users_status',
|
|
2114
|
+
ordinalPosition: 2
|
|
2115
|
+
})
|
|
2116
|
+
])
|
|
2117
|
+
);
|
|
2118
|
+
const db = schema(
|
|
2119
|
+
table('users', [
|
|
2120
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2121
|
+
col({
|
|
2122
|
+
name: 'status',
|
|
2123
|
+
type: 'enum',
|
|
2124
|
+
enumValues: ['active', 'inactive'],
|
|
2125
|
+
enumTypeName: 'users_status',
|
|
2126
|
+
ordinalPosition: 2
|
|
2127
|
+
})
|
|
2128
|
+
])
|
|
2129
|
+
);
|
|
2130
|
+
|
|
2131
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2132
|
+
assert.equal(diff.modifiedTables.length, 0, 'Enum reorder-only should not produce a diff');
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
it('should still detect enum value additions regardless of order', async () => {
|
|
2136
|
+
const entity = schema(
|
|
2137
|
+
table('users', [
|
|
2138
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2139
|
+
col({
|
|
2140
|
+
name: 'status',
|
|
2141
|
+
type: 'enum',
|
|
2142
|
+
enumValues: ['banned', 'active', 'inactive'],
|
|
2143
|
+
enumTypeName: 'users_status',
|
|
2144
|
+
ordinalPosition: 2
|
|
2145
|
+
})
|
|
2146
|
+
])
|
|
2147
|
+
);
|
|
2148
|
+
const db = schema(
|
|
2149
|
+
table('users', [
|
|
2150
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2151
|
+
col({
|
|
2152
|
+
name: 'status',
|
|
2153
|
+
type: 'enum',
|
|
2154
|
+
enumValues: ['active', 'inactive'],
|
|
2155
|
+
enumTypeName: 'users_status',
|
|
2156
|
+
ordinalPosition: 2
|
|
2157
|
+
})
|
|
2158
|
+
])
|
|
2159
|
+
);
|
|
2160
|
+
|
|
2161
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2162
|
+
assert.equal(diff.modifiedTables.length, 1, 'Should detect new enum value');
|
|
2163
|
+
assert.equal(diff.modifiedTables[0].modifiedEnumTypes.length, 1);
|
|
2164
|
+
assert.deepEqual(diff.modifiedTables[0].modifiedEnumTypes[0].added, ['banned']);
|
|
2165
|
+
});
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
describe('type alias normalization', () => {
|
|
2169
|
+
it('should treat integer and int as equivalent', async () => {
|
|
2170
|
+
const entity = schema(
|
|
2171
|
+
table('users', [
|
|
2172
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2173
|
+
col({ name: 'age', type: 'int', ordinalPosition: 2 })
|
|
2174
|
+
])
|
|
2175
|
+
);
|
|
2176
|
+
const db = schema(
|
|
2177
|
+
table('users', [
|
|
2178
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2179
|
+
col({ name: 'age', type: 'integer', ordinalPosition: 2 })
|
|
2180
|
+
])
|
|
2181
|
+
);
|
|
2182
|
+
|
|
2183
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2184
|
+
|
|
2185
|
+
assert.equal(diff.modifiedTables.length, 0, 'int and integer should be treated as equivalent');
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
it('should treat numeric and decimal as equivalent', async () => {
|
|
2189
|
+
const entity = schema(
|
|
2190
|
+
table('products', [
|
|
2191
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2192
|
+
col({ name: 'price', type: 'decimal', size: 10, scale: 2, ordinalPosition: 2 })
|
|
2193
|
+
])
|
|
2194
|
+
);
|
|
2195
|
+
const db = schema(
|
|
2196
|
+
table('products', [
|
|
2197
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2198
|
+
col({ name: 'price', type: 'numeric', size: 10, scale: 2, ordinalPosition: 2 })
|
|
2199
|
+
])
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2203
|
+
|
|
2204
|
+
assert.equal(diff.modifiedTables.length, 0, 'decimal and numeric should be treated as equivalent');
|
|
2205
|
+
});
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
describe('SET DEFAULT FK action rejection for MySQL', () => {
|
|
2209
|
+
it('should reject SET DEFAULT onDelete for MySQL', async () => {
|
|
2210
|
+
const diff = await compareSchemas(
|
|
2211
|
+
schema(
|
|
2212
|
+
table('orders', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })], {
|
|
2213
|
+
foreignKeys: [
|
|
2214
|
+
{
|
|
2215
|
+
name: 'fk_orders_user',
|
|
2216
|
+
columns: ['user_id'],
|
|
2217
|
+
referencedTable: 'users',
|
|
2218
|
+
referencedColumns: ['id'],
|
|
2219
|
+
onDelete: 'SET DEFAULT',
|
|
2220
|
+
onUpdate: 'RESTRICT'
|
|
2221
|
+
}
|
|
2222
|
+
]
|
|
2223
|
+
})
|
|
2224
|
+
),
|
|
2225
|
+
schema(),
|
|
2226
|
+
'mysql',
|
|
2227
|
+
false
|
|
2228
|
+
);
|
|
2229
|
+
|
|
2230
|
+
assert.throws(() => generateDDL(diff), /SET DEFAULT.*not supported.*MySQL/i);
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
it('should reject SET DEFAULT onUpdate for MySQL', async () => {
|
|
2234
|
+
const diff = await compareSchemas(
|
|
2235
|
+
schema(
|
|
2236
|
+
table('orders', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })], {
|
|
2237
|
+
foreignKeys: [
|
|
2238
|
+
{
|
|
2239
|
+
name: 'fk_orders_user',
|
|
2240
|
+
columns: ['user_id'],
|
|
2241
|
+
referencedTable: 'users',
|
|
2242
|
+
referencedColumns: ['id'],
|
|
2243
|
+
onDelete: 'CASCADE',
|
|
2244
|
+
onUpdate: 'SET DEFAULT'
|
|
2245
|
+
}
|
|
2246
|
+
]
|
|
2247
|
+
})
|
|
2248
|
+
),
|
|
2249
|
+
schema(),
|
|
2250
|
+
'mysql',
|
|
2251
|
+
false
|
|
2252
|
+
);
|
|
2253
|
+
|
|
2254
|
+
assert.throws(() => generateDDL(diff), /SET DEFAULT.*not supported.*MySQL/i);
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
it('should allow SET DEFAULT for PostgreSQL', async () => {
|
|
2258
|
+
const diff = await compareSchemas(
|
|
2259
|
+
schema(
|
|
2260
|
+
table('orders', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })], {
|
|
2261
|
+
foreignKeys: [
|
|
2262
|
+
{
|
|
2263
|
+
name: 'fk_orders_user',
|
|
2264
|
+
columns: ['user_id'],
|
|
2265
|
+
referencedTable: 'users',
|
|
2266
|
+
referencedColumns: ['id'],
|
|
2267
|
+
onDelete: 'SET DEFAULT',
|
|
2268
|
+
onUpdate: 'SET DEFAULT'
|
|
2269
|
+
}
|
|
2270
|
+
]
|
|
2271
|
+
})
|
|
2272
|
+
),
|
|
2273
|
+
schema(),
|
|
2274
|
+
'postgres',
|
|
2275
|
+
false
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
const stmts = generateDDL(diff);
|
|
2279
|
+
const fkStmt = stmts.find(s => s.includes('FOREIGN KEY'));
|
|
2280
|
+
assert.ok(fkStmt, 'Should generate FK statement');
|
|
2281
|
+
assert.ok(fkStmt!.includes('ON DELETE SET DEFAULT'), 'Should include SET DEFAULT');
|
|
2282
|
+
assert.ok(fkStmt!.includes('ON UPDATE SET DEFAULT'), 'Should include SET DEFAULT');
|
|
2283
|
+
});
|
|
2284
|
+
});
|
|
2285
|
+
|
|
2286
|
+
describe('entity defaults not populated', () => {
|
|
2287
|
+
it('should not produce spurious DROP DEFAULT when entity has no default info', async () => {
|
|
2288
|
+
const entity = schema(
|
|
2289
|
+
table('users', [
|
|
2290
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2291
|
+
col({ name: 'status', type: 'varchar', size: 20, ordinalPosition: 2 })
|
|
2292
|
+
])
|
|
2293
|
+
);
|
|
2294
|
+
const db = schema(
|
|
2295
|
+
table('users', [
|
|
2296
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2297
|
+
col({ name: 'status', type: 'varchar', size: 20, ordinalPosition: 2, defaultValue: 'active' })
|
|
2298
|
+
])
|
|
2299
|
+
);
|
|
2300
|
+
|
|
2301
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2302
|
+
|
|
2303
|
+
// Entity has no defaultValue/defaultExpression → treated as unspecified → no diff
|
|
2304
|
+
assert.equal(diff.modifiedTables.length, 0, 'Should not detect a diff when entity has no default info');
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
it('should detect diff when entity explicitly sets a default expression', async () => {
|
|
2308
|
+
const entity = schema(
|
|
2309
|
+
table('events', [
|
|
2310
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2311
|
+
col({ name: 'created', type: 'timestamp', ordinalPosition: 2, defaultExpression: 'CURRENT_TIMESTAMP' })
|
|
2312
|
+
])
|
|
2313
|
+
);
|
|
2314
|
+
const db = schema(
|
|
2315
|
+
table('events', [
|
|
2316
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2317
|
+
col({ name: 'created', type: 'timestamp', ordinalPosition: 2 })
|
|
2318
|
+
])
|
|
2319
|
+
);
|
|
2320
|
+
|
|
2321
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2322
|
+
|
|
2323
|
+
assert.equal(diff.modifiedTables.length, 1, 'Should detect a diff when entity has an explicit default');
|
|
2324
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns.length, 1);
|
|
2325
|
+
assert.ok(diff.modifiedTables[0].modifiedColumns[0].defaultChanged);
|
|
2326
|
+
});
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
describe('CURRENT_TIMESTAMP() normalization', () => {
|
|
2330
|
+
it('should treat CURRENT_TIMESTAMP and CURRENT_TIMESTAMP() as equivalent', async () => {
|
|
2331
|
+
const entity = schema(
|
|
2332
|
+
table('events', [
|
|
2333
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2334
|
+
col({ name: 'created', type: 'timestamp', ordinalPosition: 2, defaultExpression: 'CURRENT_TIMESTAMP' })
|
|
2335
|
+
])
|
|
2336
|
+
);
|
|
2337
|
+
const db = schema(
|
|
2338
|
+
table('events', [
|
|
2339
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2340
|
+
col({ name: 'created', type: 'timestamp', ordinalPosition: 2, defaultExpression: 'CURRENT_TIMESTAMP()' })
|
|
2341
|
+
])
|
|
2342
|
+
);
|
|
2343
|
+
|
|
2344
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2345
|
+
|
|
2346
|
+
assert.equal(diff.modifiedTables.length, 0, 'CURRENT_TIMESTAMP and CURRENT_TIMESTAMP() should be equivalent');
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
it('should treat now() and CURRENT_TIMESTAMP() as equivalent', async () => {
|
|
2350
|
+
const entity = schema(
|
|
2351
|
+
table('events', [
|
|
2352
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2353
|
+
col({ name: 'created', type: 'timestamp', ordinalPosition: 2, defaultExpression: 'now()' })
|
|
2354
|
+
])
|
|
2355
|
+
);
|
|
2356
|
+
const db = schema(
|
|
2357
|
+
table('events', [
|
|
2358
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2359
|
+
col({ name: 'created', type: 'timestamp', ordinalPosition: 2, defaultExpression: 'CURRENT_TIMESTAMP()' })
|
|
2360
|
+
])
|
|
2361
|
+
);
|
|
2362
|
+
|
|
2363
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2364
|
+
|
|
2365
|
+
assert.equal(diff.modifiedTables.length, 0, 'now() and CURRENT_TIMESTAMP() should be equivalent');
|
|
2366
|
+
});
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
describe('PG auto-increment nextval escaping', () => {
|
|
2370
|
+
it('should escape single quotes in schema name for nextval', async () => {
|
|
2371
|
+
const entity = schema(table('items', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
2372
|
+
const db = schema(table('items', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
2373
|
+
|
|
2374
|
+
// Use a schema name with a single quote to test escaping
|
|
2375
|
+
const diff = await compareSchemas(entity, db, 'postgres', false, "test'schema");
|
|
2376
|
+
const stmts = generateDDL(diff);
|
|
2377
|
+
|
|
2378
|
+
const nextvalStmt = stmts.find(s => s.includes('nextval'));
|
|
2379
|
+
assert.ok(nextvalStmt, 'Should generate nextval statement');
|
|
2380
|
+
// The single quote in the schema name should be escaped
|
|
2381
|
+
assert.ok(nextvalStmt!.includes("test''schema"), 'Should escape single quotes in schema name');
|
|
2382
|
+
});
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
describe('PG rename + property changes', () => {
|
|
2386
|
+
it('should emit RENAME COLUMN and ALTER COLUMN TYPE for PG renamed column with type change', () => {
|
|
2387
|
+
const diff = {
|
|
2388
|
+
dialect: 'postgres' as const,
|
|
2389
|
+
addedTables: [],
|
|
2390
|
+
removedTables: [],
|
|
2391
|
+
modifiedTables: [
|
|
2392
|
+
{
|
|
2393
|
+
tableName: 'users',
|
|
2394
|
+
addedColumns: [],
|
|
2395
|
+
removedColumns: [],
|
|
2396
|
+
modifiedColumns: [
|
|
2397
|
+
{
|
|
2398
|
+
name: 'full_name',
|
|
2399
|
+
oldColumn: col({ name: 'name', type: 'varchar', size: 100, ordinalPosition: 2 }),
|
|
2400
|
+
newColumn: col({ name: 'full_name', type: 'varchar', size: 255, ordinalPosition: 2 }),
|
|
2401
|
+
typeChanged: true,
|
|
2402
|
+
nullableChanged: false,
|
|
2403
|
+
defaultChanged: false,
|
|
2404
|
+
autoIncrementChanged: false,
|
|
2405
|
+
onUpdateChanged: false
|
|
2406
|
+
}
|
|
2407
|
+
],
|
|
2408
|
+
renamedColumns: [
|
|
2409
|
+
{
|
|
2410
|
+
from: 'name',
|
|
2411
|
+
to: 'full_name',
|
|
2412
|
+
column: col({ name: 'full_name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
2413
|
+
}
|
|
2414
|
+
],
|
|
2415
|
+
reorderedColumns: [],
|
|
2416
|
+
addedIndexes: [],
|
|
2417
|
+
removedIndexes: [],
|
|
2418
|
+
addedForeignKeys: [],
|
|
2419
|
+
removedForeignKeys: [],
|
|
2420
|
+
primaryKeyChanged: false,
|
|
2421
|
+
addedEnumTypes: [],
|
|
2422
|
+
removedEnumTypes: [],
|
|
2423
|
+
modifiedEnumTypes: [],
|
|
2424
|
+
entityColumns: [
|
|
2425
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2426
|
+
col({ name: 'full_name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
2427
|
+
]
|
|
2428
|
+
}
|
|
2429
|
+
]
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
const stmts = generateDDL(diff);
|
|
2433
|
+
|
|
2434
|
+
const renameStmt = stmts.find(s => s.includes('RENAME COLUMN'));
|
|
2435
|
+
assert.ok(renameStmt, 'Should emit RENAME COLUMN');
|
|
2436
|
+
assert.ok(renameStmt!.includes('"name"'), 'Should reference old name');
|
|
2437
|
+
assert.ok(renameStmt!.includes('"full_name"'), 'Should reference new name');
|
|
2438
|
+
|
|
2439
|
+
const typeStmt = stmts.find(s => s.includes('ALTER COLUMN') && s.includes('TYPE'));
|
|
2440
|
+
assert.ok(typeStmt, 'Should emit ALTER COLUMN TYPE for the renamed column');
|
|
2441
|
+
assert.ok(typeStmt!.includes('VARCHAR(255)'), 'Should use new type');
|
|
2442
|
+
});
|
|
2443
|
+
|
|
2444
|
+
it('should emit RENAME COLUMN and SET NOT NULL for PG renamed column with nullable change', () => {
|
|
2445
|
+
const diff = {
|
|
2446
|
+
dialect: 'postgres' as const,
|
|
2447
|
+
addedTables: [],
|
|
2448
|
+
removedTables: [],
|
|
2449
|
+
modifiedTables: [
|
|
2450
|
+
{
|
|
2451
|
+
tableName: 'users',
|
|
2452
|
+
addedColumns: [],
|
|
2453
|
+
removedColumns: [],
|
|
2454
|
+
modifiedColumns: [
|
|
2455
|
+
{
|
|
2456
|
+
name: 'email_addr',
|
|
2457
|
+
oldColumn: col({ name: 'email', type: 'varchar', size: 255, nullable: true, ordinalPosition: 2 }),
|
|
2458
|
+
newColumn: col({ name: 'email_addr', type: 'varchar', size: 255, nullable: false, ordinalPosition: 2 }),
|
|
2459
|
+
typeChanged: false,
|
|
2460
|
+
nullableChanged: true,
|
|
2461
|
+
defaultChanged: false,
|
|
2462
|
+
autoIncrementChanged: false,
|
|
2463
|
+
onUpdateChanged: false
|
|
2464
|
+
}
|
|
2465
|
+
],
|
|
2466
|
+
renamedColumns: [
|
|
2467
|
+
{
|
|
2468
|
+
from: 'email',
|
|
2469
|
+
to: 'email_addr',
|
|
2470
|
+
column: col({ name: 'email_addr', type: 'varchar', size: 255, nullable: false, ordinalPosition: 2 })
|
|
2471
|
+
}
|
|
2472
|
+
],
|
|
2473
|
+
reorderedColumns: [],
|
|
2474
|
+
addedIndexes: [],
|
|
2475
|
+
removedIndexes: [],
|
|
2476
|
+
addedForeignKeys: [],
|
|
2477
|
+
removedForeignKeys: [],
|
|
2478
|
+
primaryKeyChanged: false,
|
|
2479
|
+
addedEnumTypes: [],
|
|
2480
|
+
removedEnumTypes: [],
|
|
2481
|
+
modifiedEnumTypes: [],
|
|
2482
|
+
entityColumns: []
|
|
2483
|
+
}
|
|
2484
|
+
]
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
const stmts = generateDDL(diff);
|
|
2488
|
+
|
|
2489
|
+
assert.ok(
|
|
2490
|
+
stmts.some(s => s.includes('RENAME COLUMN')),
|
|
2491
|
+
'Should emit RENAME COLUMN'
|
|
2492
|
+
);
|
|
2493
|
+
assert.ok(
|
|
2494
|
+
stmts.some(s => s.includes('SET NOT NULL')),
|
|
2495
|
+
'Should emit SET NOT NULL'
|
|
2496
|
+
);
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
it('should NOT emit duplicate MODIFY COLUMN for MySQL renamed columns', () => {
|
|
2500
|
+
const diff = {
|
|
2501
|
+
dialect: 'mysql' as const,
|
|
2502
|
+
addedTables: [],
|
|
2503
|
+
removedTables: [],
|
|
2504
|
+
modifiedTables: [
|
|
2505
|
+
{
|
|
2506
|
+
tableName: 'users',
|
|
2507
|
+
addedColumns: [],
|
|
2508
|
+
removedColumns: [],
|
|
2509
|
+
modifiedColumns: [
|
|
2510
|
+
{
|
|
2511
|
+
name: 'full_name',
|
|
2512
|
+
oldColumn: col({ name: 'name', type: 'varchar', size: 100, ordinalPosition: 2 }),
|
|
2513
|
+
newColumn: col({ name: 'full_name', type: 'varchar', size: 255, ordinalPosition: 2 }),
|
|
2514
|
+
typeChanged: true,
|
|
2515
|
+
nullableChanged: false,
|
|
2516
|
+
defaultChanged: false,
|
|
2517
|
+
autoIncrementChanged: false,
|
|
2518
|
+
onUpdateChanged: false
|
|
2519
|
+
}
|
|
2520
|
+
],
|
|
2521
|
+
renamedColumns: [
|
|
2522
|
+
{
|
|
2523
|
+
from: 'name',
|
|
2524
|
+
to: 'full_name',
|
|
2525
|
+
column: col({ name: 'full_name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
2526
|
+
}
|
|
2527
|
+
],
|
|
2528
|
+
reorderedColumns: [],
|
|
2529
|
+
addedIndexes: [],
|
|
2530
|
+
removedIndexes: [],
|
|
2531
|
+
addedForeignKeys: [],
|
|
2532
|
+
removedForeignKeys: [],
|
|
2533
|
+
primaryKeyChanged: false,
|
|
2534
|
+
addedEnumTypes: [],
|
|
2535
|
+
removedEnumTypes: [],
|
|
2536
|
+
modifiedEnumTypes: [],
|
|
2537
|
+
entityColumns: [
|
|
2538
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2539
|
+
col({ name: 'full_name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
2540
|
+
]
|
|
2541
|
+
}
|
|
2542
|
+
]
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
const stmts = generateDDL(diff);
|
|
2546
|
+
|
|
2547
|
+
const changeStmts = stmts.filter(s => s.includes('CHANGE COLUMN'));
|
|
2548
|
+
assert.equal(changeStmts.length, 1, 'Should emit exactly one CHANGE COLUMN');
|
|
2549
|
+
|
|
2550
|
+
const modifyStmts = stmts.filter(s => s.includes('MODIFY COLUMN'));
|
|
2551
|
+
assert.equal(modifyStmts.length, 0, 'Should NOT emit MODIFY COLUMN for renamed column');
|
|
2552
|
+
});
|
|
2553
|
+
});
|
|
2554
|
+
|
|
2555
|
+
describe('PG auto-increment setval', () => {
|
|
2556
|
+
it('should emit setval after creating sequence for existing column', async () => {
|
|
2557
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
2558
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
2559
|
+
|
|
2560
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2561
|
+
const stmts = generateDDL(diff);
|
|
2562
|
+
|
|
2563
|
+
assert.ok(
|
|
2564
|
+
stmts.some(s => s.includes('CREATE SEQUENCE')),
|
|
2565
|
+
'Should create sequence'
|
|
2566
|
+
);
|
|
2567
|
+
assert.ok(
|
|
2568
|
+
stmts.some(s => s.includes('nextval')),
|
|
2569
|
+
'Should set default to nextval'
|
|
2570
|
+
);
|
|
2571
|
+
|
|
2572
|
+
const setvalStmt = stmts.find(s => s.includes('setval'));
|
|
2573
|
+
assert.ok(setvalStmt, 'Should emit setval to sync sequence');
|
|
2574
|
+
assert.ok(setvalStmt!.includes('MAX("id")'), 'setval should reference the column');
|
|
2575
|
+
assert.ok(setvalStmt!.includes('"users"'), 'setval should reference the table');
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
it('should use regclass-style quoting in nextval for non-public schema', async () => {
|
|
2579
|
+
const entity = schema(table('items', [col({ name: 'id', type: 'bigint', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
2580
|
+
const db = schema(table('items', [col({ name: 'id', type: 'bigint', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
2581
|
+
|
|
2582
|
+
const diff = await compareSchemas(entity, db, 'postgres', false, 'myapp');
|
|
2583
|
+
const stmts = generateDDL(diff);
|
|
2584
|
+
|
|
2585
|
+
const nextvalStmt = stmts.find(s => s.includes('nextval'));
|
|
2586
|
+
assert.ok(nextvalStmt, 'Should generate nextval');
|
|
2587
|
+
// Should contain properly quoted schema.sequence
|
|
2588
|
+
assert.ok(nextvalStmt!.includes('"myapp"."items_id_seq"'), 'Should use quoted identifiers in nextval');
|
|
2589
|
+
});
|
|
2590
|
+
});
|
|
2591
|
+
|
|
2592
|
+
describe('MySQL backslash escaping', () => {
|
|
2593
|
+
it('should escape backslashes in MySQL enum values', async () => {
|
|
2594
|
+
const diff = await compareSchemas(
|
|
2595
|
+
schema(
|
|
2596
|
+
table('items', [
|
|
2597
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2598
|
+
col({
|
|
2599
|
+
name: 'path',
|
|
2600
|
+
type: 'enum',
|
|
2601
|
+
enumValues: ['C:\\Users', 'D:\\Data'],
|
|
2602
|
+
ordinalPosition: 2
|
|
2603
|
+
})
|
|
2604
|
+
])
|
|
2605
|
+
),
|
|
2606
|
+
schema(),
|
|
2607
|
+
'mysql',
|
|
2608
|
+
false
|
|
2609
|
+
);
|
|
2610
|
+
|
|
2611
|
+
const stmts = generateDDL(diff);
|
|
2612
|
+
const create = stmts[0];
|
|
2613
|
+
assert.ok(create.includes('C:\\\\Users'), 'Should double backslashes in MySQL enum values');
|
|
2614
|
+
});
|
|
2615
|
+
|
|
2616
|
+
it('should escape backslashes in MySQL default values', () => {
|
|
2617
|
+
const diff = {
|
|
2618
|
+
dialect: 'mysql' as const,
|
|
2619
|
+
addedTables: [
|
|
2620
|
+
table('config', [
|
|
2621
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2622
|
+
col({ name: 'path', type: 'varchar', size: 255, ordinalPosition: 2, defaultValue: 'C:\\temp' })
|
|
2623
|
+
])
|
|
2624
|
+
],
|
|
2625
|
+
removedTables: [],
|
|
2626
|
+
modifiedTables: []
|
|
2627
|
+
};
|
|
2628
|
+
|
|
2629
|
+
const stmts = generateDDL(diff);
|
|
2630
|
+
const create = stmts[0];
|
|
2631
|
+
assert.ok(create.includes('C:\\\\temp'), 'Should double backslashes in MySQL default values');
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
it('should NOT escape backslashes in PG string literals', async () => {
|
|
2635
|
+
const diff = await compareSchemas(
|
|
2636
|
+
schema(
|
|
2637
|
+
table('items', [
|
|
2638
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2639
|
+
col({
|
|
2640
|
+
name: 'status',
|
|
2641
|
+
type: 'enum',
|
|
2642
|
+
enumValues: ['val\\ue'],
|
|
2643
|
+
enumTypeName: 'items_status',
|
|
2644
|
+
ordinalPosition: 2
|
|
2645
|
+
})
|
|
2646
|
+
])
|
|
2647
|
+
),
|
|
2648
|
+
schema(),
|
|
2649
|
+
'postgres',
|
|
2650
|
+
false
|
|
2651
|
+
);
|
|
2652
|
+
|
|
2653
|
+
const stmts = generateDDL(diff);
|
|
2654
|
+
// PG enum CREATE TYPE should have single backslash (not doubled)
|
|
2655
|
+
const createType = stmts.find(s => s.includes('CREATE TYPE'));
|
|
2656
|
+
assert.ok(createType, 'Should create enum type');
|
|
2657
|
+
assert.ok(createType!.includes('val\\ue'), 'PG should preserve single backslash');
|
|
2658
|
+
assert.ok(!createType!.includes('val\\\\ue'), 'PG should NOT double backslashes');
|
|
2659
|
+
});
|
|
2660
|
+
});
|
|
2661
|
+
|
|
2662
|
+
describe('enum DROP TYPE after removed tables', () => {
|
|
2663
|
+
it('should emit DROP TYPE after DROP TABLE when both exist', () => {
|
|
2664
|
+
const diff: SchemaDiff = {
|
|
2665
|
+
dialect: 'postgres',
|
|
2666
|
+
addedTables: [],
|
|
2667
|
+
removedTables: [
|
|
2668
|
+
table('old_table', [
|
|
2669
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2670
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'shared_status', ordinalPosition: 2 })
|
|
2671
|
+
])
|
|
2672
|
+
],
|
|
2673
|
+
modifiedTables: [
|
|
2674
|
+
{
|
|
2675
|
+
tableName: 'users',
|
|
2676
|
+
addedColumns: [],
|
|
2677
|
+
removedColumns: [],
|
|
2678
|
+
modifiedColumns: [
|
|
2679
|
+
{
|
|
2680
|
+
name: 'status',
|
|
2681
|
+
oldColumn: col({
|
|
2682
|
+
name: 'status',
|
|
2683
|
+
type: 'enum',
|
|
2684
|
+
enumValues: ['active', 'inactive', 'banned'],
|
|
2685
|
+
enumTypeName: 'shared_status',
|
|
2686
|
+
ordinalPosition: 2
|
|
2687
|
+
}),
|
|
2688
|
+
newColumn: col({
|
|
2689
|
+
name: 'status',
|
|
2690
|
+
type: 'enum',
|
|
2691
|
+
enumValues: ['active', 'inactive'],
|
|
2692
|
+
enumTypeName: 'shared_status',
|
|
2693
|
+
ordinalPosition: 2
|
|
2694
|
+
}),
|
|
2695
|
+
typeChanged: true,
|
|
2696
|
+
nullableChanged: false,
|
|
2697
|
+
defaultChanged: false,
|
|
2698
|
+
autoIncrementChanged: false,
|
|
2699
|
+
onUpdateChanged: false
|
|
2700
|
+
}
|
|
2701
|
+
],
|
|
2702
|
+
renamedColumns: [],
|
|
2703
|
+
reorderedColumns: [],
|
|
2704
|
+
addedIndexes: [],
|
|
2705
|
+
removedIndexes: [],
|
|
2706
|
+
addedForeignKeys: [],
|
|
2707
|
+
removedForeignKeys: [],
|
|
2708
|
+
primaryKeyChanged: false,
|
|
2709
|
+
addedEnumTypes: [],
|
|
2710
|
+
removedEnumTypes: [],
|
|
2711
|
+
modifiedEnumTypes: [
|
|
2712
|
+
{
|
|
2713
|
+
typeName: 'shared_status',
|
|
2714
|
+
added: [],
|
|
2715
|
+
removed: ['banned'],
|
|
2716
|
+
newValues: ['active', 'inactive'],
|
|
2717
|
+
tableName: 'users',
|
|
2718
|
+
columnName: 'status'
|
|
2719
|
+
}
|
|
2720
|
+
],
|
|
2721
|
+
entityColumns: []
|
|
2722
|
+
}
|
|
2723
|
+
],
|
|
2724
|
+
// users still uses shared_status — only the _old copy should be dropped
|
|
2725
|
+
entityEnumTypes: new Set(['shared_status'])
|
|
2726
|
+
};
|
|
2727
|
+
|
|
2728
|
+
const stmts = generateDDL(diff);
|
|
2729
|
+
|
|
2730
|
+
const dropTableIdx = stmts.findIndex(s => s.includes('DROP TABLE'));
|
|
2731
|
+
// Find the LAST DROP TYPE for shared_status_old (the deferred cleanup drop, not the pre-rename collision avoidance drop)
|
|
2732
|
+
const dropTypeIdx = stmts.reduce((last, s, i) => (s.includes('DROP TYPE') && s.includes('shared_status_old') ? i : last), -1);
|
|
2733
|
+
|
|
2734
|
+
assert.ok(dropTableIdx >= 0, 'Should emit DROP TABLE');
|
|
2735
|
+
assert.ok(dropTypeIdx >= 0, 'Should emit DROP TYPE for _old');
|
|
2736
|
+
assert.ok(dropTypeIdx > dropTableIdx, 'DROP TYPE should come after DROP TABLE');
|
|
2737
|
+
|
|
2738
|
+
// Should NOT drop the live shared_status type
|
|
2739
|
+
const dropLiveType = stmts.find(s => s.includes('DROP TYPE') && s.includes('"shared_status"') && !s.includes('shared_status_old'));
|
|
2740
|
+
assert.ok(!dropLiveType, 'Should not drop live shared_status type: ' + stmts.join(' | '));
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
|
|
2744
|
+
describe('comparator rename + modification detection', () => {
|
|
2745
|
+
it('should detect property changes on renamed columns', async () => {
|
|
2746
|
+
// Simulate a scenario where column "name" was renamed to "full_name" with type change
|
|
2747
|
+
// Since interactive=false, renames won't be detected by the comparator,
|
|
2748
|
+
// so we test by manually constructing the diff (as if rename was detected)
|
|
2749
|
+
// The key behavior: when renamedColumns has entries, modifiedColumns should also
|
|
2750
|
+
// include entries for the same columns if their properties differ.
|
|
2751
|
+
|
|
2752
|
+
// We verify this at the DDL level since the comparator needs interactive=true for renames
|
|
2753
|
+
const entity = schema(
|
|
2754
|
+
table('users', [
|
|
2755
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2756
|
+
col({ name: 'email', type: 'varchar', size: 255, nullable: true, ordinalPosition: 2 })
|
|
2757
|
+
])
|
|
2758
|
+
);
|
|
2759
|
+
const db = schema(
|
|
2760
|
+
table('users', [
|
|
2761
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2762
|
+
col({ name: 'email', type: 'varchar', size: 255, nullable: false, ordinalPosition: 2 })
|
|
2763
|
+
])
|
|
2764
|
+
);
|
|
2765
|
+
|
|
2766
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2767
|
+
|
|
2768
|
+
// Same-name column with nullable change should be detected
|
|
2769
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
2770
|
+
assert.equal(diff.modifiedTables[0].modifiedColumns.length, 1);
|
|
2771
|
+
assert.ok(diff.modifiedTables[0].modifiedColumns[0].nullableChanged);
|
|
2772
|
+
});
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
describe('MySQL AUTO_INCREMENT + PK change ordering', () => {
|
|
2776
|
+
it('should remove AUTO_INCREMENT before DROP PRIMARY KEY when old PK column is auto-increment', async () => {
|
|
2777
|
+
const entity = schema(
|
|
2778
|
+
table('users', [
|
|
2779
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, autoIncrement: false, ordinalPosition: 1 }),
|
|
2780
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: true, ordinalPosition: 2 })
|
|
2781
|
+
])
|
|
2782
|
+
);
|
|
2783
|
+
const db = schema(
|
|
2784
|
+
table('users', [
|
|
2785
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
2786
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: false, ordinalPosition: 2 })
|
|
2787
|
+
])
|
|
2788
|
+
);
|
|
2789
|
+
|
|
2790
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
2791
|
+
const stmts = generateDDL(diff);
|
|
2792
|
+
|
|
2793
|
+
// Should have a MODIFY COLUMN to remove AUTO_INCREMENT BEFORE DROP PRIMARY KEY
|
|
2794
|
+
const removeAIIdx = stmts.findIndex(s => s.includes('MODIFY COLUMN') && s.includes('`id`') && !s.includes('AUTO_INCREMENT'));
|
|
2795
|
+
const dropPKIdx = stmts.findIndex(s => s.includes('DROP PRIMARY KEY'));
|
|
2796
|
+
const addPKIdx = stmts.findIndex(s => s.includes('ADD PRIMARY KEY'));
|
|
2797
|
+
|
|
2798
|
+
assert.ok(removeAIIdx >= 0, 'Should emit MODIFY to remove AUTO_INCREMENT: ' + JSON.stringify(stmts));
|
|
2799
|
+
assert.ok(dropPKIdx >= 0, 'Should emit DROP PRIMARY KEY');
|
|
2800
|
+
assert.ok(addPKIdx >= 0, 'Should emit ADD PRIMARY KEY');
|
|
2801
|
+
assert.ok(removeAIIdx < dropPKIdx, 'MODIFY (remove AI) should come before DROP PRIMARY KEY');
|
|
2802
|
+
assert.ok(dropPKIdx < addPKIdx, 'DROP PRIMARY KEY should come before ADD PRIMARY KEY');
|
|
2803
|
+
});
|
|
2804
|
+
|
|
2805
|
+
it('should not emit extra MODIFY when old PK column is not auto-increment', async () => {
|
|
2806
|
+
const entity = schema(
|
|
2807
|
+
table('users', [
|
|
2808
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2809
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: false, ordinalPosition: 2 })
|
|
2810
|
+
])
|
|
2811
|
+
);
|
|
2812
|
+
const db = schema(
|
|
2813
|
+
table('users', [
|
|
2814
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, ordinalPosition: 1 }),
|
|
2815
|
+
col({ name: 'uuid', type: 'char', size: 36, isPrimaryKey: true, ordinalPosition: 2 })
|
|
2816
|
+
])
|
|
2817
|
+
);
|
|
2818
|
+
|
|
2819
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
2820
|
+
const stmts = generateDDL(diff);
|
|
2821
|
+
|
|
2822
|
+
// Should NOT have a pre-PK-drop MODIFY for AUTO_INCREMENT
|
|
2823
|
+
const dropPKIdx = stmts.findIndex(s => s.includes('DROP PRIMARY KEY'));
|
|
2824
|
+
assert.ok(dropPKIdx >= 0, 'Should emit DROP PRIMARY KEY');
|
|
2825
|
+
// No MODIFY COLUMN should appear before DROP PRIMARY KEY
|
|
2826
|
+
const stmtsBeforePK = stmts.slice(0, dropPKIdx);
|
|
2827
|
+
assert.ok(
|
|
2828
|
+
!stmtsBeforePK.some(s => s.includes('MODIFY COLUMN')),
|
|
2829
|
+
'Should not emit MODIFY before DROP PRIMARY KEY when no AUTO_INCREMENT involved'
|
|
2830
|
+
);
|
|
2831
|
+
});
|
|
2832
|
+
});
|
|
2833
|
+
|
|
2834
|
+
describe('INTERNAL_TABLES consistency', () => {
|
|
2835
|
+
it('should use the same internal table set in entity-reader and command', async () => {
|
|
2836
|
+
// Verify that INTERNAL_TABLES is exported and contains expected entries
|
|
2837
|
+
const { INTERNAL_TABLES } = await import('../../src/database/migration/create/schema-model');
|
|
2838
|
+
assert.ok(INTERNAL_TABLES.has('_migrations'), 'Should include _migrations');
|
|
2839
|
+
assert.ok(INTERNAL_TABLES.has('_locks'), 'Should include _locks');
|
|
2840
|
+
assert.ok(INTERNAL_TABLES.has('_jobs'), 'Should include _jobs');
|
|
2841
|
+
// User-defined underscore tables should NOT be excluded
|
|
2842
|
+
assert.ok(!INTERNAL_TABLES.has('_custom_table'), '_custom_table should not be internal');
|
|
2843
|
+
});
|
|
2844
|
+
});
|
|
2845
|
+
|
|
2846
|
+
describe('PK rename normalization', () => {
|
|
2847
|
+
it('should not detect PK change when PK column is only renamed', () => {
|
|
2848
|
+
// Build a diff manually with a renamed PK column to test comparator behavior
|
|
2849
|
+
// Since interactive=false won't trigger renames, we construct it directly
|
|
2850
|
+
const diff = {
|
|
2851
|
+
dialect: 'mysql' as const,
|
|
2852
|
+
addedTables: [],
|
|
2853
|
+
removedTables: [],
|
|
2854
|
+
modifiedTables: [
|
|
2855
|
+
{
|
|
2856
|
+
tableName: 'users',
|
|
2857
|
+
addedColumns: [],
|
|
2858
|
+
removedColumns: [],
|
|
2859
|
+
modifiedColumns: [],
|
|
2860
|
+
renamedColumns: [
|
|
2861
|
+
{
|
|
2862
|
+
from: 'user_id',
|
|
2863
|
+
to: 'id',
|
|
2864
|
+
column: col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })
|
|
2865
|
+
}
|
|
2866
|
+
],
|
|
2867
|
+
reorderedColumns: [],
|
|
2868
|
+
addedIndexes: [],
|
|
2869
|
+
removedIndexes: [],
|
|
2870
|
+
addedForeignKeys: [],
|
|
2871
|
+
removedForeignKeys: [],
|
|
2872
|
+
primaryKeyChanged: false,
|
|
2873
|
+
addedEnumTypes: [],
|
|
2874
|
+
removedEnumTypes: [],
|
|
2875
|
+
modifiedEnumTypes: [],
|
|
2876
|
+
entityColumns: [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]
|
|
2877
|
+
}
|
|
2878
|
+
]
|
|
2879
|
+
};
|
|
2880
|
+
|
|
2881
|
+
const stmts = generateDDL(diff);
|
|
2882
|
+
|
|
2883
|
+
// Only a CHANGE COLUMN should be emitted; no DROP/ADD PRIMARY KEY
|
|
2884
|
+
assert.ok(!stmts.some(s => s.includes('DROP PRIMARY KEY')), 'Should NOT drop PK for rename-only');
|
|
2885
|
+
assert.ok(!stmts.some(s => s.includes('ADD PRIMARY KEY')), 'Should NOT add PK for rename-only');
|
|
2886
|
+
assert.ok(
|
|
2887
|
+
stmts.some(s => s.includes('CHANGE COLUMN')),
|
|
2888
|
+
'Should emit CHANGE COLUMN for rename'
|
|
2889
|
+
);
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
it('should normalize DB PK names through rename mappings in comparator', async () => {
|
|
2893
|
+
// Simulate: entity PK is 'id', DB PK is 'user_id', column was renamed user_id → id
|
|
2894
|
+
// We need interactive=true for rename detection, which requires the prompt module.
|
|
2895
|
+
// Instead, verify at the comparator level that PK comparison happens after rename normalization.
|
|
2896
|
+
// The comparator applies renamedFromToMap to DB PK names.
|
|
2897
|
+
// This is tested indirectly: if columns differ only by name and the PK column is among them,
|
|
2898
|
+
// the comparator should see them as the same PK (no primaryKeyChanged) after rename detection.
|
|
2899
|
+
|
|
2900
|
+
// Since interactive=false means no renames detected, we test that same-named PKs are not flagged
|
|
2901
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
2902
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
2903
|
+
|
|
2904
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
2905
|
+
assert.equal(diff.modifiedTables.length, 0, 'Same PK should not produce a diff');
|
|
2906
|
+
});
|
|
2907
|
+
});
|
|
2908
|
+
|
|
2909
|
+
describe('MySQL CHANGE COLUMN AFTER excludes not-yet-added columns', () => {
|
|
2910
|
+
it('should not reference an added column in AFTER clause for rename', () => {
|
|
2911
|
+
// Scenario: entity has [id, new_col, renamed_col]
|
|
2912
|
+
// DB has [id, old_col]
|
|
2913
|
+
// rename: old_col → renamed_col; add: new_col
|
|
2914
|
+
// CHANGE COLUMN should NOT use AFTER `new_col` since it doesn't exist yet
|
|
2915
|
+
const diff = {
|
|
2916
|
+
dialect: 'mysql' as const,
|
|
2917
|
+
addedTables: [],
|
|
2918
|
+
removedTables: [],
|
|
2919
|
+
modifiedTables: [
|
|
2920
|
+
{
|
|
2921
|
+
tableName: 'users',
|
|
2922
|
+
addedColumns: [col({ name: 'new_col', ordinalPosition: 2 })],
|
|
2923
|
+
removedColumns: [],
|
|
2924
|
+
modifiedColumns: [],
|
|
2925
|
+
renamedColumns: [
|
|
2926
|
+
{
|
|
2927
|
+
from: 'old_col',
|
|
2928
|
+
to: 'renamed_col',
|
|
2929
|
+
column: col({ name: 'renamed_col', ordinalPosition: 3 })
|
|
2930
|
+
}
|
|
2931
|
+
],
|
|
2932
|
+
reorderedColumns: [],
|
|
2933
|
+
addedIndexes: [],
|
|
2934
|
+
removedIndexes: [],
|
|
2935
|
+
addedForeignKeys: [],
|
|
2936
|
+
removedForeignKeys: [],
|
|
2937
|
+
primaryKeyChanged: false,
|
|
2938
|
+
addedEnumTypes: [],
|
|
2939
|
+
removedEnumTypes: [],
|
|
2940
|
+
modifiedEnumTypes: [],
|
|
2941
|
+
entityColumns: [
|
|
2942
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2943
|
+
col({ name: 'new_col', ordinalPosition: 2 }),
|
|
2944
|
+
col({ name: 'renamed_col', ordinalPosition: 3 })
|
|
2945
|
+
]
|
|
2946
|
+
}
|
|
2947
|
+
]
|
|
2948
|
+
};
|
|
2949
|
+
|
|
2950
|
+
const stmts = generateDDL(diff);
|
|
2951
|
+
|
|
2952
|
+
const changeStmt = stmts.find(s => s.includes('CHANGE COLUMN'));
|
|
2953
|
+
assert.ok(changeStmt, 'Should emit CHANGE COLUMN');
|
|
2954
|
+
// Should NOT reference `new_col` (it doesn't exist yet at rename step)
|
|
2955
|
+
assert.ok(!changeStmt!.includes('AFTER `new_col`'), 'CHANGE COLUMN should not reference not-yet-added column: ' + changeStmt);
|
|
2956
|
+
// Should reference `id` instead (the nearest existing predecessor)
|
|
2957
|
+
assert.ok(changeStmt!.includes('AFTER `id`'), 'CHANGE COLUMN should reference existing predecessor: ' + changeStmt);
|
|
2958
|
+
});
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2961
|
+
describe('PG setval empty table safety', () => {
|
|
2962
|
+
it('should use 3-arg setval with is_called for empty table safety', async () => {
|
|
2963
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
2964
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
2965
|
+
|
|
2966
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
2967
|
+
const stmts = generateDDL(diff);
|
|
2968
|
+
|
|
2969
|
+
const setvalStmt = stmts.find(s => s.includes('setval'));
|
|
2970
|
+
assert.ok(setvalStmt, 'Should emit setval');
|
|
2971
|
+
// Should use 3-arg form: setval(seq, value, is_called)
|
|
2972
|
+
assert.ok(setvalStmt!.includes('IS NOT NULL'), 'Should use is_called based on whether MAX exists: ' + setvalStmt);
|
|
2973
|
+
// Should use COALESCE with 1 (not 0) as fallback for empty tables
|
|
2974
|
+
assert.ok(setvalStmt!.includes('COALESCE') && setvalStmt!.includes(', 1)'), 'Should use 1 as fallback for empty tables: ' + setvalStmt);
|
|
2975
|
+
});
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
describe('onUpdateExpression handling', () => {
|
|
2979
|
+
it('should flag onUpdateExpression removal when entity has no annotation but DB has it', async () => {
|
|
2980
|
+
const entity = schema(
|
|
2981
|
+
table('users', [
|
|
2982
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2983
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2 })
|
|
2984
|
+
// no onUpdateExpression — entity does not use OnUpdate<> annotation
|
|
2985
|
+
])
|
|
2986
|
+
);
|
|
2987
|
+
const db = schema(
|
|
2988
|
+
table('users', [
|
|
2989
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
2990
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2, onUpdateExpression: 'CURRENT_TIMESTAMP' })
|
|
2991
|
+
])
|
|
2992
|
+
);
|
|
2993
|
+
|
|
2994
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
2995
|
+
assert.strictEqual(diff.modifiedTables.length, 1, 'Should detect modification when DB has ON UPDATE but entity does not');
|
|
2996
|
+
const mod = diff.modifiedTables[0].modifiedColumns.find(m => m.name === 'updated_at');
|
|
2997
|
+
assert.ok(mod, 'Should find updated_at modification');
|
|
2998
|
+
assert.strictEqual(mod!.onUpdateChanged, true);
|
|
2999
|
+
});
|
|
3000
|
+
|
|
3001
|
+
it('should not flag when both entity and DB have the same onUpdateExpression', async () => {
|
|
3002
|
+
const entity = schema(
|
|
3003
|
+
table('users', [
|
|
3004
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3005
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2, onUpdateExpression: 'CURRENT_TIMESTAMP' })
|
|
3006
|
+
])
|
|
3007
|
+
);
|
|
3008
|
+
const db = schema(
|
|
3009
|
+
table('users', [
|
|
3010
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3011
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2, onUpdateExpression: 'CURRENT_TIMESTAMP' })
|
|
3012
|
+
])
|
|
3013
|
+
);
|
|
3014
|
+
|
|
3015
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3016
|
+
assert.strictEqual(diff.modifiedTables.length, 0, 'Should not detect modifications when ON UPDATE matches');
|
|
3017
|
+
});
|
|
3018
|
+
|
|
3019
|
+
it('should not flag when neither entity nor DB has onUpdateExpression', async () => {
|
|
3020
|
+
const entity = schema(
|
|
3021
|
+
table('users', [
|
|
3022
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3023
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2 })
|
|
3024
|
+
])
|
|
3025
|
+
);
|
|
3026
|
+
const db = schema(
|
|
3027
|
+
table('users', [
|
|
3028
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3029
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2 })
|
|
3030
|
+
])
|
|
3031
|
+
);
|
|
3032
|
+
|
|
3033
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3034
|
+
assert.strictEqual(diff.modifiedTables.length, 0, 'Should not detect modifications when neither has ON UPDATE');
|
|
3035
|
+
});
|
|
3036
|
+
|
|
3037
|
+
it('should flag onUpdateExpression addition when entity has it but DB does not', async () => {
|
|
3038
|
+
const entity = schema(
|
|
3039
|
+
table('users', [
|
|
3040
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3041
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2, onUpdateExpression: 'CURRENT_TIMESTAMP' })
|
|
3042
|
+
])
|
|
3043
|
+
);
|
|
3044
|
+
const db = schema(
|
|
3045
|
+
table('users', [
|
|
3046
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3047
|
+
col({ name: 'updated_at', type: 'datetime', ordinalPosition: 2 })
|
|
3048
|
+
])
|
|
3049
|
+
);
|
|
3050
|
+
|
|
3051
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3052
|
+
assert.strictEqual(diff.modifiedTables.length, 1, 'Should detect modification when entity has ON UPDATE but DB does not');
|
|
3053
|
+
const mod = diff.modifiedTables[0].modifiedColumns.find(m => m.name === 'updated_at');
|
|
3054
|
+
assert.ok(mod, 'Should find updated_at modification');
|
|
3055
|
+
assert.strictEqual(mod!.onUpdateChanged, true);
|
|
3056
|
+
});
|
|
3057
|
+
});
|
|
3058
|
+
|
|
3059
|
+
describe('MySQL PK drop with removed AUTO_INCREMENT column', () => {
|
|
3060
|
+
it('should strip AUTO_INCREMENT before DROP PRIMARY KEY for removed column', async () => {
|
|
3061
|
+
const entity = schema(
|
|
3062
|
+
table('users', [
|
|
3063
|
+
col({ name: 'new_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3064
|
+
col({ name: 'name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
3065
|
+
])
|
|
3066
|
+
);
|
|
3067
|
+
const db = schema(
|
|
3068
|
+
table('users', [
|
|
3069
|
+
col({ name: 'old_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3070
|
+
col({ name: 'name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
3071
|
+
])
|
|
3072
|
+
);
|
|
3073
|
+
|
|
3074
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3075
|
+
const stmts = generateDDL(diff);
|
|
3076
|
+
|
|
3077
|
+
// Should emit MODIFY COLUMN to strip AUTO_INCREMENT before DROP PRIMARY KEY
|
|
3078
|
+
const modifyIdx = stmts.findIndex(s => s.includes('MODIFY COLUMN') && s.includes('`old_id`') && !s.includes('AUTO_INCREMENT'));
|
|
3079
|
+
const dropPKIdx = stmts.findIndex(s => s.includes('DROP PRIMARY KEY'));
|
|
3080
|
+
const dropColIdx = stmts.findIndex(s => s.includes('DROP COLUMN') && s.includes('`old_id`'));
|
|
3081
|
+
|
|
3082
|
+
assert.ok(modifyIdx >= 0, 'Should emit MODIFY COLUMN to strip AUTO_INCREMENT: ' + stmts.join(' | '));
|
|
3083
|
+
assert.ok(dropPKIdx >= 0, 'Should emit DROP PRIMARY KEY');
|
|
3084
|
+
assert.ok(modifyIdx < dropPKIdx, 'MODIFY should come before DROP PRIMARY KEY');
|
|
3085
|
+
assert.ok(dropPKIdx < dropColIdx, 'DROP PRIMARY KEY should come before DROP COLUMN');
|
|
3086
|
+
});
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
describe('MySQL AUTO_INCREMENT addition after PK', () => {
|
|
3090
|
+
it('should add AUTO_INCREMENT after ADD PRIMARY KEY for modified columns', async () => {
|
|
3091
|
+
// Scenario: changing PK and adding AUTO_INCREMENT to the new PK column
|
|
3092
|
+
const entity = schema(
|
|
3093
|
+
table('users', [
|
|
3094
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3095
|
+
col({ name: 'name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
3096
|
+
])
|
|
3097
|
+
);
|
|
3098
|
+
const db = schema(
|
|
3099
|
+
table('users', [
|
|
3100
|
+
col({ name: 'id', type: 'int', isPrimaryKey: false, autoIncrement: false, ordinalPosition: 1 }),
|
|
3101
|
+
col({ name: 'name', type: 'varchar', size: 255, isPrimaryKey: true, ordinalPosition: 2 })
|
|
3102
|
+
])
|
|
3103
|
+
);
|
|
3104
|
+
|
|
3105
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3106
|
+
const stmts = generateDDL(diff);
|
|
3107
|
+
|
|
3108
|
+
const addPKIdx = stmts.findIndex(s => s.includes('ADD PRIMARY KEY'));
|
|
3109
|
+
const autoIncIdx = stmts.findIndex(s => s.includes('MODIFY COLUMN') && s.includes('`id`') && s.includes('AUTO_INCREMENT'));
|
|
3110
|
+
|
|
3111
|
+
assert.ok(addPKIdx >= 0, 'Should emit ADD PRIMARY KEY: ' + stmts.join(' | '));
|
|
3112
|
+
assert.ok(autoIncIdx >= 0, 'Should emit MODIFY COLUMN with AUTO_INCREMENT: ' + stmts.join(' | '));
|
|
3113
|
+
assert.ok(addPKIdx < autoIncIdx, 'ADD PRIMARY KEY should come before AUTO_INCREMENT addition');
|
|
3114
|
+
});
|
|
3115
|
+
|
|
3116
|
+
it('should add new AUTO_INCREMENT column without AUTO_INCREMENT first, then apply after PK', async () => {
|
|
3117
|
+
// Scenario: adding a new column with AUTO_INCREMENT to an existing table
|
|
3118
|
+
const entity = schema(
|
|
3119
|
+
table('users', [
|
|
3120
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3121
|
+
col({ name: 'name', type: 'varchar', size: 255, ordinalPosition: 2 })
|
|
3122
|
+
])
|
|
3123
|
+
);
|
|
3124
|
+
const db = schema(table('users', [col({ name: 'name', type: 'varchar', size: 255, isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
3125
|
+
|
|
3126
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3127
|
+
const stmts = generateDDL(diff);
|
|
3128
|
+
|
|
3129
|
+
// ADD COLUMN should NOT include AUTO_INCREMENT (it would fail before PK exists)
|
|
3130
|
+
const addColStmt = stmts.find(s => s.includes('ADD COLUMN') && s.includes('`id`'));
|
|
3131
|
+
assert.ok(addColStmt, 'Should emit ADD COLUMN for id: ' + stmts.join(' | '));
|
|
3132
|
+
assert.ok(!addColStmt!.includes('AUTO_INCREMENT'), 'ADD COLUMN should not include AUTO_INCREMENT: ' + addColStmt);
|
|
3133
|
+
|
|
3134
|
+
// ADD PRIMARY KEY should come before the MODIFY that adds AUTO_INCREMENT
|
|
3135
|
+
const addPKIdx = stmts.findIndex(s => s.includes('ADD PRIMARY KEY'));
|
|
3136
|
+
const autoIncIdx = stmts.findIndex(s => s.includes('MODIFY COLUMN') && s.includes('`id`') && s.includes('AUTO_INCREMENT'));
|
|
3137
|
+
|
|
3138
|
+
assert.ok(addPKIdx >= 0, 'Should emit ADD PRIMARY KEY');
|
|
3139
|
+
assert.ok(autoIncIdx >= 0, 'Should emit MODIFY COLUMN with AUTO_INCREMENT');
|
|
3140
|
+
assert.ok(addPKIdx < autoIncIdx, 'ADD PRIMARY KEY should come before AUTO_INCREMENT MODIFY');
|
|
3141
|
+
});
|
|
3142
|
+
});
|
|
3143
|
+
|
|
3144
|
+
describe('PG identity column removal', () => {
|
|
3145
|
+
it('should use DROP IDENTITY for identity columns', async () => {
|
|
3146
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
3147
|
+
const db = schema(
|
|
3148
|
+
table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, isIdentity: true, ordinalPosition: 1 })])
|
|
3149
|
+
);
|
|
3150
|
+
|
|
3151
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
3152
|
+
const stmts = generateDDL(diff);
|
|
3153
|
+
|
|
3154
|
+
const dropIdentity = stmts.find(s => s.includes('DROP IDENTITY'));
|
|
3155
|
+
assert.ok(dropIdentity, 'Should emit DROP IDENTITY for identity column: ' + stmts.join(' | '));
|
|
3156
|
+
// Should NOT emit DROP SEQUENCE for identity columns
|
|
3157
|
+
const dropSeq = stmts.find(s => s.includes('DROP SEQUENCE'));
|
|
3158
|
+
assert.ok(!dropSeq, 'Should not emit DROP SEQUENCE for identity columns');
|
|
3159
|
+
});
|
|
3160
|
+
|
|
3161
|
+
it('should use DROP DEFAULT + DROP SEQUENCE for sequence-backed columns', async () => {
|
|
3162
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: false, ordinalPosition: 1 })]));
|
|
3163
|
+
const db = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]));
|
|
3164
|
+
|
|
3165
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
3166
|
+
const stmts = generateDDL(diff);
|
|
3167
|
+
|
|
3168
|
+
const dropDefault = stmts.find(s => s.includes('DROP DEFAULT'));
|
|
3169
|
+
const dropSeq = stmts.find(s => s.includes('DROP SEQUENCE'));
|
|
3170
|
+
assert.ok(dropDefault, 'Should emit DROP DEFAULT for sequence-backed column');
|
|
3171
|
+
assert.ok(dropSeq, 'Should emit DROP SEQUENCE for sequence-backed column');
|
|
3172
|
+
// Should NOT emit DROP IDENTITY
|
|
3173
|
+
const dropIdentity = stmts.find(s => s.includes('DROP IDENTITY'));
|
|
3174
|
+
assert.ok(!dropIdentity, 'Should not emit DROP IDENTITY for sequence-backed columns');
|
|
3175
|
+
});
|
|
3176
|
+
});
|
|
3177
|
+
|
|
3178
|
+
describe('MySQL PK shape change with AUTO_INCREMENT column', () => {
|
|
3179
|
+
it('should strip and restore AUTO_INCREMENT when PK shape changes', async () => {
|
|
3180
|
+
// PK changes from (id) to (id, tenant_id), id keeps AUTO_INCREMENT
|
|
3181
|
+
const entity = schema(
|
|
3182
|
+
table('users', [
|
|
3183
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3184
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: true, ordinalPosition: 2 })
|
|
3185
|
+
])
|
|
3186
|
+
);
|
|
3187
|
+
const db = schema(
|
|
3188
|
+
table('users', [
|
|
3189
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3190
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: false, ordinalPosition: 2 })
|
|
3191
|
+
])
|
|
3192
|
+
);
|
|
3193
|
+
|
|
3194
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3195
|
+
const stmts = generateDDL(diff);
|
|
3196
|
+
|
|
3197
|
+
// Should strip AUTO_INCREMENT before DROP PRIMARY KEY
|
|
3198
|
+
const stripIdx = stmts.findIndex(s => s.includes('MODIFY COLUMN') && s.includes('`id`') && !s.includes('AUTO_INCREMENT'));
|
|
3199
|
+
const dropPKIdx = stmts.findIndex(s => s.includes('DROP PRIMARY KEY'));
|
|
3200
|
+
const addPKIdx = stmts.findIndex(s => s.includes('ADD PRIMARY KEY'));
|
|
3201
|
+
// Should restore AUTO_INCREMENT after ADD PRIMARY KEY
|
|
3202
|
+
const restoreIdx = stmts.findIndex(
|
|
3203
|
+
(s, i) => i > addPKIdx && s.includes('MODIFY COLUMN') && s.includes('`id`') && s.includes('AUTO_INCREMENT')
|
|
3204
|
+
);
|
|
3205
|
+
|
|
3206
|
+
assert.ok(stripIdx >= 0, 'Should strip AUTO_INCREMENT: ' + stmts.join(' | '));
|
|
3207
|
+
assert.ok(dropPKIdx >= 0, 'Should emit DROP PRIMARY KEY');
|
|
3208
|
+
assert.ok(addPKIdx >= 0, 'Should emit ADD PRIMARY KEY');
|
|
3209
|
+
assert.ok(stripIdx < dropPKIdx, 'Strip should come before DROP PRIMARY KEY');
|
|
3210
|
+
assert.ok(dropPKIdx < addPKIdx, 'DROP PK should come before ADD PK');
|
|
3211
|
+
assert.ok(restoreIdx >= 0, 'Should restore AUTO_INCREMENT after ADD PK: ' + stmts.join(' | '));
|
|
3212
|
+
});
|
|
3213
|
+
});
|
|
3214
|
+
|
|
3215
|
+
describe('MySQL renamed PK column with AUTO_INCREMENT', () => {
|
|
3216
|
+
it('should use DB column name (not entity name) when stripping AUTO_INCREMENT before DROP PK', async () => {
|
|
3217
|
+
// Column renamed from 'old_id' to 'new_id', PK changes from (old_id) to (new_id, tenant_id)
|
|
3218
|
+
// The MODIFY to strip AUTO_INCREMENT must use the DB name 'old_id' since CHANGE COLUMN hasn't happened yet
|
|
3219
|
+
const entity = schema(
|
|
3220
|
+
table('users', [
|
|
3221
|
+
col({ name: 'new_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3222
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: true, ordinalPosition: 2 })
|
|
3223
|
+
])
|
|
3224
|
+
);
|
|
3225
|
+
const db = schema(
|
|
3226
|
+
table('users', [
|
|
3227
|
+
col({ name: 'old_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3228
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: false, ordinalPosition: 2 })
|
|
3229
|
+
])
|
|
3230
|
+
);
|
|
3231
|
+
|
|
3232
|
+
// Use non-interactive mode — renames won't be detected automatically
|
|
3233
|
+
// Manually construct the diff with the rename
|
|
3234
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3235
|
+
|
|
3236
|
+
// Since non-interactive, rename won't be detected; manually construct the diff for DDL test
|
|
3237
|
+
const manualDiff = {
|
|
3238
|
+
dialect: 'mysql' as const,
|
|
3239
|
+
addedTables: [],
|
|
3240
|
+
removedTables: [],
|
|
3241
|
+
modifiedTables: [
|
|
3242
|
+
{
|
|
3243
|
+
tableName: 'users',
|
|
3244
|
+
addedColumns: [],
|
|
3245
|
+
removedColumns: [],
|
|
3246
|
+
modifiedColumns: [],
|
|
3247
|
+
renamedColumns: [
|
|
3248
|
+
{
|
|
3249
|
+
from: 'old_id',
|
|
3250
|
+
to: 'new_id',
|
|
3251
|
+
column: col({ name: 'new_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })
|
|
3252
|
+
}
|
|
3253
|
+
],
|
|
3254
|
+
reorderedColumns: [],
|
|
3255
|
+
addedIndexes: [],
|
|
3256
|
+
removedIndexes: [],
|
|
3257
|
+
addedForeignKeys: [],
|
|
3258
|
+
removedForeignKeys: [],
|
|
3259
|
+
primaryKeyChanged: true,
|
|
3260
|
+
newPrimaryKey: ['new_id', 'tenant_id'],
|
|
3261
|
+
oldPrimaryKey: ['old_id'], // raw DB name
|
|
3262
|
+
addedEnumTypes: [],
|
|
3263
|
+
removedEnumTypes: [],
|
|
3264
|
+
modifiedEnumTypes: [],
|
|
3265
|
+
entityColumns: [
|
|
3266
|
+
col({ name: 'new_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3267
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: true, ordinalPosition: 2 })
|
|
3268
|
+
]
|
|
3269
|
+
}
|
|
3270
|
+
]
|
|
3271
|
+
};
|
|
3272
|
+
|
|
3273
|
+
const stmts = generateDDL(manualDiff);
|
|
3274
|
+
|
|
3275
|
+
// The strip should reference `old_id` (current DB name), not `new_id`
|
|
3276
|
+
const stripStmt = stmts.find(s => s.includes('MODIFY COLUMN') && s.includes('`old_id`') && !s.includes('AUTO_INCREMENT'));
|
|
3277
|
+
assert.ok(stripStmt, 'Should strip AUTO_INCREMENT using DB name `old_id`: ' + stmts.join(' | '));
|
|
3278
|
+
|
|
3279
|
+
// Should NOT have a MODIFY referencing `new_id` before CHANGE COLUMN
|
|
3280
|
+
const changeIdx = stmts.findIndex(s => s.includes('CHANGE COLUMN'));
|
|
3281
|
+
const modifyNewBeforeChange = stmts.findIndex((s, i) => i < changeIdx && s.includes('MODIFY COLUMN') && s.includes('`new_id`'));
|
|
3282
|
+
assert.ok(modifyNewBeforeChange < 0, 'Should not MODIFY `new_id` before CHANGE COLUMN');
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
|
|
3286
|
+
describe('MySQL PK change with unchanged AUTO_INCREMENT column modification', () => {
|
|
3287
|
+
it('should defer MODIFY of AUTO_INCREMENT column until after ADD PK when PK changes', async () => {
|
|
3288
|
+
// Column `id` has AUTO_INCREMENT (unchanged), but PK changes AND column type changes (int→bigint)
|
|
3289
|
+
// The MODIFY should be deferred until after ADD PK
|
|
3290
|
+
const entity = schema(
|
|
3291
|
+
table('users', [
|
|
3292
|
+
col({ name: 'id', type: 'bigint', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3293
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: true, ordinalPosition: 2 })
|
|
3294
|
+
])
|
|
3295
|
+
);
|
|
3296
|
+
const db = schema(
|
|
3297
|
+
table('users', [
|
|
3298
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 }),
|
|
3299
|
+
col({ name: 'tenant_id', type: 'int', isPrimaryKey: false, ordinalPosition: 2 })
|
|
3300
|
+
])
|
|
3301
|
+
);
|
|
3302
|
+
|
|
3303
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3304
|
+
const stmts = generateDDL(diff);
|
|
3305
|
+
|
|
3306
|
+
// The final MODIFY with AUTO_INCREMENT and BIGINT should come AFTER ADD PRIMARY KEY
|
|
3307
|
+
const addPKIdx = stmts.findIndex(s => s.includes('ADD PRIMARY KEY'));
|
|
3308
|
+
assert.ok(addPKIdx >= 0, 'Should have ADD PRIMARY KEY');
|
|
3309
|
+
|
|
3310
|
+
const modifyBigintAIIdx = stmts.findIndex(
|
|
3311
|
+
(s, i) => i > addPKIdx && s.includes('MODIFY COLUMN') && s.includes('BIGINT') && s.includes('AUTO_INCREMENT')
|
|
3312
|
+
);
|
|
3313
|
+
assert.ok(modifyBigintAIIdx >= 0, 'Should defer MODIFY COLUMN with AUTO_INCREMENT to after ADD PK: ' + stmts.join(' | '));
|
|
3314
|
+
|
|
3315
|
+
// Before ADD PK, there should be no MODIFY that includes AUTO_INCREMENT
|
|
3316
|
+
|
|
3317
|
+
// The strip MODIFY (without AUTO_INCREMENT) is fine before DROP PK, but no MODIFY WITH AUTO_INCREMENT before ADD PK
|
|
3318
|
+
const anyAIModBeforePK = stmts.some(
|
|
3319
|
+
(s, i) => i > 0 && i < addPKIdx && s.includes('MODIFY COLUMN') && s.includes('AUTO_INCREMENT') && !s.includes('DROP PRIMARY KEY')
|
|
3320
|
+
);
|
|
3321
|
+
assert.ok(!anyAIModBeforePK, 'No MODIFY with AUTO_INCREMENT should appear before ADD PRIMARY KEY');
|
|
3322
|
+
});
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
describe('PG enum type cleanup', () => {
|
|
3326
|
+
it('should DROP old enum type when enum type name changes', async () => {
|
|
3327
|
+
// Column stays enum but type name changes (e.g., table_status → accounts_status)
|
|
3328
|
+
const entity = schema(
|
|
3329
|
+
table('accounts', [
|
|
3330
|
+
col({
|
|
3331
|
+
name: 'status',
|
|
3332
|
+
type: 'enum',
|
|
3333
|
+
enumValues: ['active', 'inactive'],
|
|
3334
|
+
enumTypeName: 'accounts_status',
|
|
3335
|
+
ordinalPosition: 1
|
|
3336
|
+
})
|
|
3337
|
+
])
|
|
3338
|
+
);
|
|
3339
|
+
const db = schema(
|
|
3340
|
+
table('accounts', [
|
|
3341
|
+
col({
|
|
3342
|
+
name: 'status',
|
|
3343
|
+
type: 'enum',
|
|
3344
|
+
enumValues: ['active', 'inactive'],
|
|
3345
|
+
enumTypeName: 'users_status',
|
|
3346
|
+
ordinalPosition: 1
|
|
3347
|
+
})
|
|
3348
|
+
])
|
|
3349
|
+
);
|
|
3350
|
+
|
|
3351
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
3352
|
+
const stmts = generateDDL(diff);
|
|
3353
|
+
|
|
3354
|
+
// Should CREATE the new type
|
|
3355
|
+
const createType = stmts.find(s => s.includes('CREATE TYPE') && s.includes('accounts_status'));
|
|
3356
|
+
assert.ok(createType, 'Should CREATE TYPE for new enum name: ' + stmts.join(' | '));
|
|
3357
|
+
|
|
3358
|
+
// Should DROP the old type
|
|
3359
|
+
const dropType = stmts.find(s => s.includes('DROP TYPE') && s.includes('users_status'));
|
|
3360
|
+
assert.ok(dropType, 'Should DROP TYPE for old enum name: ' + stmts.join(' | '));
|
|
3361
|
+
});
|
|
3362
|
+
|
|
3363
|
+
it('should DROP enum type when column is removed', async () => {
|
|
3364
|
+
// Column with enum type removed entirely
|
|
3365
|
+
const entity = schema(table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
3366
|
+
const db = schema(
|
|
3367
|
+
table('users', [
|
|
3368
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3369
|
+
col({
|
|
3370
|
+
name: 'status',
|
|
3371
|
+
type: 'enum',
|
|
3372
|
+
enumValues: ['active', 'inactive'],
|
|
3373
|
+
enumTypeName: 'users_status',
|
|
3374
|
+
ordinalPosition: 2
|
|
3375
|
+
})
|
|
3376
|
+
])
|
|
3377
|
+
);
|
|
3378
|
+
|
|
3379
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
3380
|
+
const stmts = generateDDL(diff);
|
|
3381
|
+
|
|
3382
|
+
// Should DROP the orphaned enum type
|
|
3383
|
+
const dropType = stmts.find(s => s.includes('DROP TYPE') && s.includes('users_status'));
|
|
3384
|
+
assert.ok(dropType, 'Should DROP TYPE for removed column enum: ' + stmts.join(' | '));
|
|
3385
|
+
|
|
3386
|
+
// DROP TYPE should come after DROP COLUMN
|
|
3387
|
+
const dropColIdx = stmts.findIndex(s => s.includes('DROP COLUMN'));
|
|
3388
|
+
const dropTypeIdx = stmts.findIndex(s => s.includes('DROP TYPE') && s.includes('users_status'));
|
|
3389
|
+
assert.ok(dropColIdx < dropTypeIdx, 'DROP TYPE should come after DROP COLUMN');
|
|
3390
|
+
});
|
|
3391
|
+
|
|
3392
|
+
it('should DROP enum type from removed table', async () => {
|
|
3393
|
+
const entity = schema();
|
|
3394
|
+
const db = schema(
|
|
3395
|
+
table('users', [
|
|
3396
|
+
col({
|
|
3397
|
+
name: 'status',
|
|
3398
|
+
type: 'enum',
|
|
3399
|
+
enumValues: ['active', 'inactive'],
|
|
3400
|
+
enumTypeName: 'users_status',
|
|
3401
|
+
ordinalPosition: 1
|
|
3402
|
+
})
|
|
3403
|
+
])
|
|
3404
|
+
);
|
|
3405
|
+
|
|
3406
|
+
const diff = await compareSchemas(entity, db, 'postgres', false);
|
|
3407
|
+
const stmts = generateDDL(diff);
|
|
3408
|
+
|
|
3409
|
+
// Should DROP TABLE
|
|
3410
|
+
const dropTable = stmts.find(s => s.includes('DROP TABLE'));
|
|
3411
|
+
assert.ok(dropTable, 'Should DROP TABLE');
|
|
3412
|
+
|
|
3413
|
+
// Should DROP the enum type after dropping the table
|
|
3414
|
+
const dropType = stmts.find(s => s.includes('DROP TYPE') && s.includes('users_status'));
|
|
3415
|
+
assert.ok(dropType, 'Should DROP TYPE for enum in removed table: ' + stmts.join(' | '));
|
|
3416
|
+
|
|
3417
|
+
const dropTableIdx = stmts.findIndex(s => s.includes('DROP TABLE'));
|
|
3418
|
+
const dropTypeIdx = stmts.findIndex(s => s.includes('DROP TYPE') && s.includes('users_status'));
|
|
3419
|
+
assert.ok(dropTableIdx < dropTypeIdx, 'DROP TYPE should come after DROP TABLE');
|
|
3420
|
+
});
|
|
3421
|
+
});
|
|
3422
|
+
|
|
3423
|
+
describe('MySQL renamed AUTO_INCREMENT column with PK change', () => {
|
|
3424
|
+
it('should defer AUTO_INCREMENT on renamed column until after ADD PK', () => {
|
|
3425
|
+
const manualDiff = {
|
|
3426
|
+
dialect: 'mysql' as const,
|
|
3427
|
+
addedTables: [],
|
|
3428
|
+
removedTables: [],
|
|
3429
|
+
modifiedTables: [
|
|
3430
|
+
{
|
|
3431
|
+
tableName: 'users',
|
|
3432
|
+
addedColumns: [],
|
|
3433
|
+
removedColumns: [],
|
|
3434
|
+
modifiedColumns: [],
|
|
3435
|
+
renamedColumns: [
|
|
3436
|
+
{
|
|
3437
|
+
from: 'old_id',
|
|
3438
|
+
to: 'new_id',
|
|
3439
|
+
column: col({ name: 'new_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })
|
|
3440
|
+
}
|
|
3441
|
+
],
|
|
3442
|
+
reorderedColumns: [],
|
|
3443
|
+
addedIndexes: [],
|
|
3444
|
+
removedIndexes: [],
|
|
3445
|
+
addedForeignKeys: [],
|
|
3446
|
+
removedForeignKeys: [],
|
|
3447
|
+
primaryKeyChanged: true,
|
|
3448
|
+
newPrimaryKey: ['new_id'],
|
|
3449
|
+
oldPrimaryKey: ['old_id'],
|
|
3450
|
+
addedEnumTypes: [],
|
|
3451
|
+
removedEnumTypes: [],
|
|
3452
|
+
modifiedEnumTypes: [],
|
|
3453
|
+
entityColumns: [col({ name: 'new_id', type: 'int', isPrimaryKey: true, autoIncrement: true, ordinalPosition: 1 })]
|
|
3454
|
+
}
|
|
3455
|
+
]
|
|
3456
|
+
};
|
|
3457
|
+
|
|
3458
|
+
const stmts = generateDDL(manualDiff);
|
|
3459
|
+
|
|
3460
|
+
// CHANGE COLUMN should NOT include AUTO_INCREMENT (deferred)
|
|
3461
|
+
const changeColStmt = stmts.find(s => s.includes('CHANGE COLUMN'));
|
|
3462
|
+
assert.ok(changeColStmt, 'Should have CHANGE COLUMN');
|
|
3463
|
+
assert.ok(!changeColStmt!.includes('AUTO_INCREMENT'), 'CHANGE COLUMN should not include AUTO_INCREMENT: ' + changeColStmt);
|
|
3464
|
+
|
|
3465
|
+
// After ADD PK, there should be a MODIFY that restores AUTO_INCREMENT
|
|
3466
|
+
const addPKIdx = stmts.findIndex(s => s.includes('ADD PRIMARY KEY'));
|
|
3467
|
+
assert.ok(addPKIdx >= 0, 'Should have ADD PRIMARY KEY');
|
|
3468
|
+
const restoreIdx = stmts.findIndex(
|
|
3469
|
+
(s, i) => i > addPKIdx && s.includes('MODIFY COLUMN') && s.includes('`new_id`') && s.includes('AUTO_INCREMENT')
|
|
3470
|
+
);
|
|
3471
|
+
assert.ok(restoreIdx >= 0, 'Should restore AUTO_INCREMENT after ADD PK: ' + stmts.join(' | '));
|
|
3472
|
+
});
|
|
3473
|
+
});
|
|
3474
|
+
|
|
3475
|
+
describe('PG enum recreation default handling', () => {
|
|
3476
|
+
it('should drop and restore default around enum type change', () => {
|
|
3477
|
+
const manualDiff = {
|
|
3478
|
+
dialect: 'postgres' as const,
|
|
3479
|
+
addedTables: [],
|
|
3480
|
+
removedTables: [],
|
|
3481
|
+
modifiedTables: [
|
|
3482
|
+
{
|
|
3483
|
+
tableName: 'users',
|
|
3484
|
+
addedColumns: [],
|
|
3485
|
+
removedColumns: [],
|
|
3486
|
+
modifiedColumns: [
|
|
3487
|
+
{
|
|
3488
|
+
name: 'status',
|
|
3489
|
+
oldColumn: col({
|
|
3490
|
+
name: 'status',
|
|
3491
|
+
type: 'enum',
|
|
3492
|
+
enumValues: ['active', 'inactive', 'banned'],
|
|
3493
|
+
enumTypeName: 'users_status',
|
|
3494
|
+
defaultValue: 'active',
|
|
3495
|
+
ordinalPosition: 2
|
|
3496
|
+
}),
|
|
3497
|
+
newColumn: col({
|
|
3498
|
+
name: 'status',
|
|
3499
|
+
type: 'enum',
|
|
3500
|
+
enumValues: ['active', 'inactive'],
|
|
3501
|
+
enumTypeName: 'users_status',
|
|
3502
|
+
ordinalPosition: 2
|
|
3503
|
+
}),
|
|
3504
|
+
typeChanged: true,
|
|
3505
|
+
nullableChanged: false,
|
|
3506
|
+
defaultChanged: false,
|
|
3507
|
+
autoIncrementChanged: false,
|
|
3508
|
+
onUpdateChanged: false
|
|
3509
|
+
}
|
|
3510
|
+
],
|
|
3511
|
+
renamedColumns: [],
|
|
3512
|
+
reorderedColumns: [],
|
|
3513
|
+
addedIndexes: [],
|
|
3514
|
+
removedIndexes: [],
|
|
3515
|
+
addedForeignKeys: [],
|
|
3516
|
+
removedForeignKeys: [],
|
|
3517
|
+
primaryKeyChanged: false,
|
|
3518
|
+
addedEnumTypes: [],
|
|
3519
|
+
removedEnumTypes: [],
|
|
3520
|
+
modifiedEnumTypes: [
|
|
3521
|
+
{
|
|
3522
|
+
typeName: 'users_status',
|
|
3523
|
+
added: [],
|
|
3524
|
+
removed: ['banned'],
|
|
3525
|
+
newValues: ['active', 'inactive'],
|
|
3526
|
+
tableName: 'users',
|
|
3527
|
+
columnName: 'status'
|
|
3528
|
+
}
|
|
3529
|
+
],
|
|
3530
|
+
entityColumns: []
|
|
3531
|
+
}
|
|
3532
|
+
]
|
|
3533
|
+
};
|
|
3534
|
+
|
|
3535
|
+
const stmts = generateDDL(manualDiff);
|
|
3536
|
+
|
|
3537
|
+
// Should drop default before TYPE change
|
|
3538
|
+
const dropDefaultIdx = stmts.findIndex(s => s.includes('DROP DEFAULT'));
|
|
3539
|
+
const typeChangeIdx = stmts.findIndex(s => s.includes('ALTER COLUMN') && s.includes('TYPE'));
|
|
3540
|
+
assert.ok(dropDefaultIdx >= 0, 'Should emit DROP DEFAULT: ' + stmts.join(' | '));
|
|
3541
|
+
assert.ok(typeChangeIdx >= 0, 'Should emit TYPE change');
|
|
3542
|
+
assert.ok(dropDefaultIdx < typeChangeIdx, 'DROP DEFAULT should come before TYPE change');
|
|
3543
|
+
|
|
3544
|
+
// Should restore default after TYPE change
|
|
3545
|
+
const setDefaultIdx = stmts.findIndex(s => s.includes('SET DEFAULT'));
|
|
3546
|
+
assert.ok(setDefaultIdx >= 0, 'Should restore default after TYPE change: ' + stmts.join(' | '));
|
|
3547
|
+
assert.ok(setDefaultIdx > typeChangeIdx, 'SET DEFAULT should come after TYPE change');
|
|
3548
|
+
});
|
|
3549
|
+
});
|
|
3550
|
+
|
|
3551
|
+
describe('PG enum recreation collision avoidance', () => {
|
|
3552
|
+
it('should emit DROP TYPE IF EXISTS before RENAME to avoid _old collision', () => {
|
|
3553
|
+
const manualDiff = {
|
|
3554
|
+
dialect: 'postgres' as const,
|
|
3555
|
+
addedTables: [],
|
|
3556
|
+
removedTables: [],
|
|
3557
|
+
modifiedTables: [
|
|
3558
|
+
{
|
|
3559
|
+
tableName: 'users',
|
|
3560
|
+
addedColumns: [],
|
|
3561
|
+
removedColumns: [],
|
|
3562
|
+
modifiedColumns: [],
|
|
3563
|
+
renamedColumns: [],
|
|
3564
|
+
reorderedColumns: [],
|
|
3565
|
+
addedIndexes: [],
|
|
3566
|
+
removedIndexes: [],
|
|
3567
|
+
addedForeignKeys: [],
|
|
3568
|
+
removedForeignKeys: [],
|
|
3569
|
+
primaryKeyChanged: false,
|
|
3570
|
+
addedEnumTypes: [],
|
|
3571
|
+
removedEnumTypes: [],
|
|
3572
|
+
modifiedEnumTypes: [
|
|
3573
|
+
{
|
|
3574
|
+
typeName: 'users_status',
|
|
3575
|
+
added: [],
|
|
3576
|
+
removed: ['banned'],
|
|
3577
|
+
newValues: ['active', 'inactive'],
|
|
3578
|
+
tableName: 'users',
|
|
3579
|
+
columnName: 'status'
|
|
3580
|
+
}
|
|
3581
|
+
],
|
|
3582
|
+
entityColumns: []
|
|
3583
|
+
}
|
|
3584
|
+
]
|
|
3585
|
+
};
|
|
3586
|
+
|
|
3587
|
+
const stmts = generateDDL(manualDiff);
|
|
3588
|
+
|
|
3589
|
+
// Should have DROP TYPE IF EXISTS before ALTER TYPE RENAME
|
|
3590
|
+
const preDropIdx = stmts.findIndex(s => s.includes('DROP TYPE IF EXISTS') && s.includes('users_status_old'));
|
|
3591
|
+
const renameIdx = stmts.findIndex(s => s.includes('ALTER TYPE') && s.includes('RENAME'));
|
|
3592
|
+
assert.ok(preDropIdx >= 0, 'Should emit pre-RENAME DROP TYPE IF EXISTS: ' + stmts.join(' | '));
|
|
3593
|
+
assert.ok(renameIdx >= 0, 'Should emit ALTER TYPE RENAME');
|
|
3594
|
+
assert.ok(preDropIdx < renameIdx, 'Pre-drop should come before RENAME');
|
|
3595
|
+
});
|
|
3596
|
+
});
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
// --- Bug fix regression tests ---
|
|
3600
|
+
|
|
3601
|
+
describe('bug fixes', () => {
|
|
3602
|
+
describe('Issue #1: skipped columns should not cause destructive DROPs', () => {
|
|
3603
|
+
it('should not report skipped columns as removed', async () => {
|
|
3604
|
+
// Entity has skippedColumns set (simulating unsupported type)
|
|
3605
|
+
const entityTable = table('users', [
|
|
3606
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3607
|
+
col({ name: 'name', ordinalPosition: 2 })
|
|
3608
|
+
]);
|
|
3609
|
+
entityTable.skippedColumns = new Set(['geo_point']);
|
|
3610
|
+
|
|
3611
|
+
const dbTable = table('users', [
|
|
3612
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3613
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
3614
|
+
col({ name: 'geo_point', type: 'point', ordinalPosition: 3 })
|
|
3615
|
+
]);
|
|
3616
|
+
|
|
3617
|
+
const entity = schema(entityTable);
|
|
3618
|
+
const db = schema(dbTable);
|
|
3619
|
+
|
|
3620
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3621
|
+
|
|
3622
|
+
// geo_point is in skippedColumns, so it should NOT appear as removed
|
|
3623
|
+
assert.equal(diff.modifiedTables.length, 0, 'Should have no modifications — geo_point is skipped');
|
|
3624
|
+
});
|
|
3625
|
+
|
|
3626
|
+
it('should still detect real column removals alongside skipped columns', async () => {
|
|
3627
|
+
const entityTable = table('users', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]);
|
|
3628
|
+
entityTable.skippedColumns = new Set(['geo_point']);
|
|
3629
|
+
|
|
3630
|
+
const dbTable = table('users', [
|
|
3631
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3632
|
+
col({ name: 'name', ordinalPosition: 2 }),
|
|
3633
|
+
col({ name: 'geo_point', type: 'point', ordinalPosition: 3 })
|
|
3634
|
+
]);
|
|
3635
|
+
|
|
3636
|
+
const entity = schema(entityTable);
|
|
3637
|
+
const db = schema(dbTable);
|
|
3638
|
+
|
|
3639
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3640
|
+
|
|
3641
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
3642
|
+
assert.equal(diff.modifiedTables[0].removedColumns.length, 1);
|
|
3643
|
+
assert.equal(diff.modifiedTables[0].removedColumns[0].name, 'name');
|
|
3644
|
+
// geo_point should NOT be in removedColumns
|
|
3645
|
+
assert.ok(!diff.modifiedTables[0].removedColumns.some(c => c.name === 'geo_point'), 'geo_point should not be in removedColumns');
|
|
3646
|
+
});
|
|
3647
|
+
});
|
|
3648
|
+
|
|
3649
|
+
describe('Issue #2: PG enum DROP should not drop types still used by other tables', () => {
|
|
3650
|
+
it('should not drop enum type used by another table (via compareSchemas)', async () => {
|
|
3651
|
+
// Entity: orders has no status column, users still has it — both share status_enum
|
|
3652
|
+
const entitySch = schema(
|
|
3653
|
+
table('orders', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]),
|
|
3654
|
+
table('users', [
|
|
3655
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3656
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3657
|
+
])
|
|
3658
|
+
);
|
|
3659
|
+
const dbSch = schema(
|
|
3660
|
+
table('orders', [
|
|
3661
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3662
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3663
|
+
]),
|
|
3664
|
+
table('users', [
|
|
3665
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3666
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3667
|
+
])
|
|
3668
|
+
);
|
|
3669
|
+
|
|
3670
|
+
const diff = await compareSchemas(entitySch, dbSch, 'postgres', false, 'public');
|
|
3671
|
+
const stmts = generateDDL(diff);
|
|
3672
|
+
|
|
3673
|
+
// Should NOT contain DROP TYPE for status_enum since users table still uses it
|
|
3674
|
+
const dropEnum = stmts.find(s => s.includes('DROP TYPE') && s.includes('status_enum') && !s.includes('_old'));
|
|
3675
|
+
assert.ok(!dropEnum, 'Should not drop status_enum still used by users table: ' + stmts.join(' | '));
|
|
3676
|
+
});
|
|
3677
|
+
|
|
3678
|
+
it('should drop enum type when no table uses it (via compareSchemas)', async () => {
|
|
3679
|
+
// Entity: orders has no status column, no other table uses orders_status_enum
|
|
3680
|
+
const entitySch = schema(table('orders', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]));
|
|
3681
|
+
const dbSch = schema(
|
|
3682
|
+
table('orders', [
|
|
3683
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3684
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'orders_status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3685
|
+
])
|
|
3686
|
+
);
|
|
3687
|
+
|
|
3688
|
+
const diff = await compareSchemas(entitySch, dbSch, 'postgres', false, 'public');
|
|
3689
|
+
const stmts = generateDDL(diff);
|
|
3690
|
+
|
|
3691
|
+
const dropEnum = stmts.find(s => s.includes('DROP TYPE IF EXISTS') && s.includes('orders_status_enum'));
|
|
3692
|
+
assert.ok(dropEnum, 'Should drop orders_status_enum when no table uses it: ' + stmts.join(' | '));
|
|
3693
|
+
});
|
|
3694
|
+
|
|
3695
|
+
it('should not drop enum type used by unchanged table (not in modifiedTables)', async () => {
|
|
3696
|
+
// Entity: orders removes status column, users is UNCHANGED (won't appear in modifiedTables)
|
|
3697
|
+
const entitySch = schema(
|
|
3698
|
+
table('orders', [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]),
|
|
3699
|
+
table('users', [
|
|
3700
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3701
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3702
|
+
])
|
|
3703
|
+
);
|
|
3704
|
+
const dbSch = schema(
|
|
3705
|
+
table('orders', [
|
|
3706
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3707
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3708
|
+
]),
|
|
3709
|
+
table('users', [
|
|
3710
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3711
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'status_enum', enumValues: ['a', 'b'], ordinalPosition: 2 })
|
|
3712
|
+
])
|
|
3713
|
+
);
|
|
3714
|
+
|
|
3715
|
+
const diff = await compareSchemas(entitySch, dbSch, 'postgres', false, 'public');
|
|
3716
|
+
|
|
3717
|
+
// users should NOT appear in modifiedTables — it's unchanged
|
|
3718
|
+
assert.ok(!diff.modifiedTables.find(t => t.tableName === 'users'), 'users should not be in modifiedTables');
|
|
3719
|
+
|
|
3720
|
+
const stmts = generateDDL(diff);
|
|
3721
|
+
|
|
3722
|
+
// But entityEnumTypes should still protect status_enum from being dropped
|
|
3723
|
+
const dropEnum = stmts.find(s => s.includes('DROP TYPE') && s.includes('status_enum'));
|
|
3724
|
+
assert.ok(!dropEnum, 'Should not drop status_enum used by unchanged users table: ' + stmts.join(' | '));
|
|
3725
|
+
});
|
|
3726
|
+
});
|
|
3727
|
+
|
|
3728
|
+
describe('Issue #3: PG enum creation should be idempotent', () => {
|
|
3729
|
+
it('should wrap CREATE TYPE in IF NOT EXISTS guard', () => {
|
|
3730
|
+
const diff: SchemaDiff = {
|
|
3731
|
+
dialect: 'postgres',
|
|
3732
|
+
addedTables: [
|
|
3733
|
+
table('users', [
|
|
3734
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3735
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'user_status', enumValues: ['active', 'inactive'], ordinalPosition: 2 })
|
|
3736
|
+
])
|
|
3737
|
+
],
|
|
3738
|
+
removedTables: [],
|
|
3739
|
+
modifiedTables: []
|
|
3740
|
+
};
|
|
3741
|
+
|
|
3742
|
+
const stmts = generateDDL(diff);
|
|
3743
|
+
|
|
3744
|
+
const createTypeStmt = stmts.find(s => s.includes('user_status') && s.includes('ENUM'));
|
|
3745
|
+
assert.ok(createTypeStmt, 'Should have a CREATE TYPE statement');
|
|
3746
|
+
assert.ok(createTypeStmt!.includes('IF NOT EXISTS'), 'CREATE TYPE should include IF NOT EXISTS guard: ' + createTypeStmt);
|
|
3747
|
+
assert.ok(createTypeStmt!.includes('DO $$'), 'Should use DO $$ block for conditional creation: ' + createTypeStmt);
|
|
3748
|
+
});
|
|
3749
|
+
|
|
3750
|
+
it('should include pg_namespace filter for non-public schema', () => {
|
|
3751
|
+
const diff: SchemaDiff = {
|
|
3752
|
+
dialect: 'postgres',
|
|
3753
|
+
pgSchema: 'tenant',
|
|
3754
|
+
addedTables: [
|
|
3755
|
+
table('users', [
|
|
3756
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3757
|
+
col({ name: 'status', type: 'enum', enumTypeName: 'user_status', enumValues: ['active', 'inactive'], ordinalPosition: 2 })
|
|
3758
|
+
])
|
|
3759
|
+
],
|
|
3760
|
+
removedTables: [],
|
|
3761
|
+
modifiedTables: []
|
|
3762
|
+
};
|
|
3763
|
+
|
|
3764
|
+
const stmts = generateDDL(diff);
|
|
3765
|
+
|
|
3766
|
+
const createTypeStmt = stmts.find(s => s.includes('user_status') && s.includes('ENUM'));
|
|
3767
|
+
assert.ok(createTypeStmt, 'Should have a CREATE TYPE statement');
|
|
3768
|
+
assert.ok(
|
|
3769
|
+
createTypeStmt!.includes('pg_namespace') && createTypeStmt!.includes("nspname = 'tenant'"),
|
|
3770
|
+
'IF NOT EXISTS guard should filter by pg_namespace for non-public schema: ' + createTypeStmt
|
|
3771
|
+
);
|
|
3772
|
+
});
|
|
3773
|
+
});
|
|
3774
|
+
|
|
3775
|
+
describe('Issue #4: rename detection should consider type-mismatched columns', () => {
|
|
3776
|
+
it('should not detect renames in non-interactive mode (baseline)', async () => {
|
|
3777
|
+
const entity = schema(
|
|
3778
|
+
table('users', [
|
|
3779
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3780
|
+
col({ name: 'new_col', type: 'bigint', ordinalPosition: 2 })
|
|
3781
|
+
])
|
|
3782
|
+
);
|
|
3783
|
+
const db = schema(
|
|
3784
|
+
table('users', [
|
|
3785
|
+
col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3786
|
+
col({ name: 'old_col', type: 'int', ordinalPosition: 2 })
|
|
3787
|
+
])
|
|
3788
|
+
);
|
|
3789
|
+
|
|
3790
|
+
// non-interactive: rename detection is skipped entirely
|
|
3791
|
+
const diff = await compareSchemas(entity, db, 'mysql', false);
|
|
3792
|
+
|
|
3793
|
+
assert.equal(diff.modifiedTables.length, 1);
|
|
3794
|
+
assert.equal(diff.modifiedTables[0].renamedColumns.length, 0);
|
|
3795
|
+
assert.equal(diff.modifiedTables[0].addedColumns.length, 1);
|
|
3796
|
+
assert.equal(diff.modifiedTables[0].removedColumns.length, 1);
|
|
3797
|
+
});
|
|
3798
|
+
});
|
|
3799
|
+
|
|
3800
|
+
describe('Issue #5: PG sequence removal should use actual sequence name', () => {
|
|
3801
|
+
it('should use stored sequenceName when removing auto-increment', () => {
|
|
3802
|
+
const diff: SchemaDiff = {
|
|
3803
|
+
dialect: 'postgres',
|
|
3804
|
+
addedTables: [],
|
|
3805
|
+
removedTables: [],
|
|
3806
|
+
modifiedTables: [
|
|
3807
|
+
{
|
|
3808
|
+
tableName: 'users',
|
|
3809
|
+
addedColumns: [],
|
|
3810
|
+
removedColumns: [],
|
|
3811
|
+
modifiedColumns: [
|
|
3812
|
+
{
|
|
3813
|
+
name: 'id',
|
|
3814
|
+
oldColumn: col({
|
|
3815
|
+
name: 'id',
|
|
3816
|
+
type: 'int',
|
|
3817
|
+
autoIncrement: true,
|
|
3818
|
+
isPrimaryKey: true,
|
|
3819
|
+
ordinalPosition: 1,
|
|
3820
|
+
sequenceName: 'public.my_custom_seq'
|
|
3821
|
+
}),
|
|
3822
|
+
newColumn: col({ name: 'id', type: 'int', autoIncrement: false, isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3823
|
+
typeChanged: false,
|
|
3824
|
+
nullableChanged: false,
|
|
3825
|
+
defaultChanged: false,
|
|
3826
|
+
autoIncrementChanged: true,
|
|
3827
|
+
onUpdateChanged: false
|
|
3828
|
+
} as ColumnModification
|
|
3829
|
+
],
|
|
3830
|
+
renamedColumns: [],
|
|
3831
|
+
reorderedColumns: [],
|
|
3832
|
+
addedIndexes: [],
|
|
3833
|
+
removedIndexes: [],
|
|
3834
|
+
addedForeignKeys: [],
|
|
3835
|
+
removedForeignKeys: [],
|
|
3836
|
+
primaryKeyChanged: false,
|
|
3837
|
+
addedEnumTypes: [],
|
|
3838
|
+
removedEnumTypes: [],
|
|
3839
|
+
modifiedEnumTypes: [],
|
|
3840
|
+
entityColumns: [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]
|
|
3841
|
+
}
|
|
3842
|
+
]
|
|
3843
|
+
};
|
|
3844
|
+
|
|
3845
|
+
const stmts = generateDDL(diff);
|
|
3846
|
+
|
|
3847
|
+
const dropSeq = stmts.find(s => s.includes('DROP SEQUENCE'));
|
|
3848
|
+
assert.ok(dropSeq, 'Should have a DROP SEQUENCE statement: ' + stmts.join(' | '));
|
|
3849
|
+
assert.ok(dropSeq!.includes('public.my_custom_seq'), 'Should use actual sequence name, not conventional: ' + dropSeq);
|
|
3850
|
+
});
|
|
3851
|
+
|
|
3852
|
+
it('should fall back to conventional sequence name when sequenceName is not set', () => {
|
|
3853
|
+
const diff: SchemaDiff = {
|
|
3854
|
+
dialect: 'postgres',
|
|
3855
|
+
addedTables: [],
|
|
3856
|
+
removedTables: [],
|
|
3857
|
+
modifiedTables: [
|
|
3858
|
+
{
|
|
3859
|
+
tableName: 'users',
|
|
3860
|
+
addedColumns: [],
|
|
3861
|
+
removedColumns: [],
|
|
3862
|
+
modifiedColumns: [
|
|
3863
|
+
{
|
|
3864
|
+
name: 'id',
|
|
3865
|
+
oldColumn: col({ name: 'id', type: 'int', autoIncrement: true, isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3866
|
+
newColumn: col({ name: 'id', type: 'int', autoIncrement: false, isPrimaryKey: true, ordinalPosition: 1 }),
|
|
3867
|
+
typeChanged: false,
|
|
3868
|
+
nullableChanged: false,
|
|
3869
|
+
defaultChanged: false,
|
|
3870
|
+
autoIncrementChanged: true,
|
|
3871
|
+
onUpdateChanged: false
|
|
3872
|
+
} as ColumnModification
|
|
3873
|
+
],
|
|
3874
|
+
renamedColumns: [],
|
|
3875
|
+
reorderedColumns: [],
|
|
3876
|
+
addedIndexes: [],
|
|
3877
|
+
removedIndexes: [],
|
|
3878
|
+
addedForeignKeys: [],
|
|
3879
|
+
removedForeignKeys: [],
|
|
3880
|
+
primaryKeyChanged: false,
|
|
3881
|
+
addedEnumTypes: [],
|
|
3882
|
+
removedEnumTypes: [],
|
|
3883
|
+
modifiedEnumTypes: [],
|
|
3884
|
+
entityColumns: [col({ name: 'id', type: 'int', isPrimaryKey: true, ordinalPosition: 1 })]
|
|
3885
|
+
}
|
|
3886
|
+
]
|
|
3887
|
+
};
|
|
3888
|
+
|
|
3889
|
+
const stmts = generateDDL(diff);
|
|
3890
|
+
|
|
3891
|
+
const dropSeq = stmts.find(s => s.includes('DROP SEQUENCE'));
|
|
3892
|
+
assert.ok(dropSeq, 'Should have a DROP SEQUENCE statement: ' + stmts.join(' | '));
|
|
3893
|
+
assert.ok(dropSeq!.includes('users_id_seq'), 'Should use conventional sequence name as fallback: ' + dropSeq);
|
|
3894
|
+
});
|
|
3895
|
+
});
|
|
3896
|
+
});
|