sqlew 3.6.10 → 3.7.1
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/CHANGELOG.md +346 -0
- package/README.md +54 -39
- package/assets/config.example.toml +93 -0
- package/assets/kanban-visualizer.png +0 -0
- package/assets/sample-agents/sqlew-architect.md +32 -13
- package/assets/sample-agents/sqlew-researcher.md +70 -17
- package/assets/sample-agents/sqlew-scrum-master.md +60 -25
- package/assets/schema.sql +2 -2
- package/dist/adapters/auth/auth-factory.d.ts +86 -0
- package/dist/adapters/auth/auth-factory.d.ts.map +1 -0
- package/dist/adapters/auth/auth-factory.js +103 -0
- package/dist/adapters/auth/auth-factory.js.map +1 -0
- package/dist/adapters/auth/auth-types.d.ts +30 -0
- package/dist/adapters/auth/auth-types.d.ts.map +1 -0
- package/dist/adapters/auth/auth-types.js +30 -0
- package/dist/adapters/auth/auth-types.js.map +1 -0
- package/dist/adapters/auth/base-auth-provider.d.ts +327 -0
- package/dist/adapters/auth/base-auth-provider.d.ts.map +1 -0
- package/dist/adapters/auth/base-auth-provider.js +111 -0
- package/dist/adapters/auth/base-auth-provider.js.map +1 -0
- package/dist/adapters/auth/direct-auth-provider.d.ts +356 -0
- package/dist/adapters/auth/direct-auth-provider.d.ts.map +1 -0
- package/dist/adapters/auth/direct-auth-provider.js +406 -0
- package/dist/adapters/auth/direct-auth-provider.js.map +1 -0
- package/dist/adapters/base-adapter.d.ts +638 -0
- package/dist/adapters/base-adapter.d.ts.map +1 -0
- package/dist/adapters/base-adapter.js +557 -0
- package/dist/adapters/base-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +27 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/mysql-adapter.d.ts +547 -6
- package/dist/adapters/mysql-adapter.d.ts.map +1 -1
- package/dist/adapters/mysql-adapter.js +651 -32
- package/dist/adapters/mysql-adapter.js.map +1 -1
- package/dist/adapters/postgresql-adapter.d.ts +15 -4
- package/dist/adapters/postgresql-adapter.d.ts.map +1 -1
- package/dist/adapters/postgresql-adapter.js +19 -2
- package/dist/adapters/postgresql-adapter.js.map +1 -1
- package/dist/adapters/sqlite-adapter.d.ts +35 -5
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite-adapter.js +57 -18
- package/dist/adapters/sqlite-adapter.js.map +1 -1
- package/dist/cli/db-dump.d.ts +32 -0
- package/dist/cli/db-dump.d.ts.map +1 -0
- package/dist/cli/db-dump.js +409 -0
- package/dist/cli/db-dump.js.map +1 -0
- package/dist/cli.js +24 -14
- package/dist/cli.js.map +1 -1
- package/dist/config/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.js +7 -2
- package/dist/config/knex/bootstrap/20251025020452_create_master_tables.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.js +49 -50
- package/dist/config/knex/bootstrap/20251025021152_create_transaction_tables.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025021351_create_indexes.d.ts.map +1 -0
- package/dist/config/knex/bootstrap/20251025021351_create_indexes.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.js +11 -6
- package/dist/config/knex/bootstrap/20251025021416_seed_master_data.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025070349_create_views.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.js +66 -14
- package/dist/config/knex/bootstrap/20251025070349_create_views.js.map +1 -0
- package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +22 -0
- package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
- package/dist/config/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +1 -0
- package/dist/config/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.js +6 -0
- package/dist/config/knex/enhancements/20251025090000_create_help_system_tables.js.map +1 -0
- package/dist/config/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js +6 -0
- package/dist/config/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
- package/dist/config/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.js +6 -0
- package/dist/config/knex/enhancements/20251025100000_seed_help_metadata.js.map +1 -0
- package/dist/config/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +1 -0
- package/dist/config/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js +7 -0
- package/dist/config/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
- package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.js +62 -0
- package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +1 -0
- package/dist/config/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +1 -0
- package/dist/config/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.js +6 -0
- package/dist/config/knex/enhancements/20251027020000_update_agent_reusability.js.map +1 -0
- package/dist/config/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.js +6 -0
- package/dist/config/knex/enhancements/20251028000000_simplify_agent_system.js.map +1 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.d.ts +13 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.js +48 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.js.map +1 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.d.ts +24 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.js +189 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.js.map +1 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.d.ts +16 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.js +65 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.js.map +1 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.d.ts +23 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.js +118 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.js.map +1 -0
- package/dist/config/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +1 -0
- package/dist/config/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +1 -0
- package/dist/config/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +1 -0
- package/dist/config/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +1 -0
- package/dist/config/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +1 -0
- package/dist/config/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +1 -0
- package/dist/config/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +1 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.d.ts +49 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.js +864 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.js.map +1 -0
- package/dist/config/loader.d.ts +19 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +149 -4
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +261 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/writer.d.ts +65 -0
- package/dist/config/writer.d.ts.map +1 -0
- package/dist/config/writer.js +139 -0
- package/dist/config/writer.js.map +1 -0
- package/dist/database.d.ts +11 -2
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +62 -6
- package/dist/database.js.map +1 -1
- package/dist/index.js +173 -39
- package/dist/index.js.map +1 -1
- package/dist/knexfile.d.ts.map +1 -1
- package/dist/knexfile.js +88 -12
- package/dist/knexfile.js.map +1 -1
- package/dist/tests/all-features.test.js +15 -3
- package/dist/tests/all-features.test.js.map +1 -1
- package/dist/tests/config-loader.test.d.ts +6 -0
- package/dist/tests/config-loader.test.d.ts.map +1 -0
- package/dist/tests/config-loader.test.js +201 -0
- package/dist/tests/config-loader.test.js.map +1 -0
- package/dist/tests/connection-manager-integration.test.d.ts +2 -0
- package/dist/tests/connection-manager-integration.test.d.ts.map +1 -0
- package/dist/tests/connection-manager-integration.test.js +431 -0
- package/dist/tests/connection-manager-integration.test.js.map +1 -0
- package/dist/tests/connection-manager.test.d.ts +2 -0
- package/dist/tests/connection-manager.test.d.ts.map +1 -0
- package/dist/tests/connection-manager.test.js +361 -0
- package/dist/tests/connection-manager.test.js.map +1 -0
- package/dist/tests/dump-import.test.d.ts +15 -0
- package/dist/tests/dump-import.test.d.ts.map +1 -0
- package/dist/tests/dump-import.test.js +430 -0
- package/dist/tests/dump-import.test.js.map +1 -0
- package/dist/tests/migration-idempotency.test.d.ts +2 -0
- package/dist/tests/migration-idempotency.test.d.ts.map +1 -0
- package/dist/tests/migration-idempotency.test.js +330 -0
- package/dist/tests/migration-idempotency.test.js.map +1 -0
- package/dist/tests/migration-upgrade-paths.test.d.ts +2 -0
- package/dist/tests/migration-upgrade-paths.test.d.ts.map +1 -0
- package/dist/tests/migration-upgrade-paths.test.js +248 -0
- package/dist/tests/migration-upgrade-paths.test.js.map +1 -0
- package/dist/tests/migrations/test-all-versions-real.js +3 -0
- package/dist/tests/migrations/test-all-versions-real.js.map +1 -1
- package/dist/tests/multi-project-migration.test.d.ts +17 -0
- package/dist/tests/multi-project-migration.test.d.ts.map +1 -0
- package/dist/tests/multi-project-migration.test.js +399 -0
- package/dist/tests/multi-project-migration.test.js.map +1 -0
- package/dist/tests/multi-project.test.d.ts +5 -0
- package/dist/tests/multi-project.test.d.ts.map +1 -0
- package/dist/tests/multi-project.test.js +238 -0
- package/dist/tests/multi-project.test.js.map +1 -0
- package/dist/tests/schema-migration.test.d.ts +8 -0
- package/dist/tests/schema-migration.test.d.ts.map +1 -0
- package/dist/tests/schema-migration.test.js +108 -0
- package/dist/tests/schema-migration.test.js.map +1 -0
- package/dist/tests/sql-dump-converters.test.d.ts +7 -0
- package/dist/tests/sql-dump-converters.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-converters.test.js +314 -0
- package/dist/tests/sql-dump-converters.test.js.map +1 -0
- package/dist/tests/sql-dump-cross-database.test.d.ts +21 -0
- package/dist/tests/sql-dump-cross-database.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-cross-database.test.js +314 -0
- package/dist/tests/sql-dump-cross-database.test.js.map +1 -0
- package/dist/tests/sql-dump-default-conversions.test.d.ts +8 -0
- package/dist/tests/sql-dump-default-conversions.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-default-conversions.test.js +141 -0
- package/dist/tests/sql-dump-default-conversions.test.js.map +1 -0
- package/dist/tests/sql-dump-fk-constraints.test.d.ts +13 -0
- package/dist/tests/sql-dump-fk-constraints.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-fk-constraints.test.js +381 -0
- package/dist/tests/sql-dump-fk-constraints.test.js.map +1 -0
- package/dist/tests/sql-dump-indexes.test.d.ts +12 -0
- package/dist/tests/sql-dump-indexes.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-indexes.test.js +269 -0
- package/dist/tests/sql-dump-indexes.test.js.map +1 -0
- package/dist/tests/sql-dump-integration.test.d.ts +16 -0
- package/dist/tests/sql-dump-integration.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-integration.test.js +342 -0
- package/dist/tests/sql-dump-integration.test.js.map +1 -0
- package/dist/tests/sql-dump-table-ordering.test.d.ts +8 -0
- package/dist/tests/sql-dump-table-ordering.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-table-ordering.test.js +253 -0
- package/dist/tests/sql-dump-table-ordering.test.js.map +1 -0
- package/dist/tests/tasks.link-file-backward-compat.test.js +11 -1
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-action.test.js +11 -1
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
- package/dist/tests/type-conversion.test.d.ts +8 -0
- package/dist/tests/type-conversion.test.d.ts.map +1 -0
- package/dist/tests/type-conversion.test.js +312 -0
- package/dist/tests/type-conversion.test.js.map +1 -0
- package/dist/tests/utils/test-helpers.d.ts +93 -0
- package/dist/tests/utils/test-helpers.d.ts.map +1 -0
- package/dist/tests/utils/test-helpers.js +407 -0
- package/dist/tests/utils/test-helpers.js.map +1 -0
- package/dist/tools/config.d.ts +58 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +281 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +138 -122
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +216 -109
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +123 -102
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +593 -518
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +5 -0
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +176 -122
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +9 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cleanup.d.ts +3 -0
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +14 -2
- package/dist/utils/cleanup.js.map +1 -1
- package/dist/utils/connection-manager.d.ts +59 -0
- package/dist/utils/connection-manager.d.ts.map +1 -0
- package/dist/utils/connection-manager.js +178 -0
- package/dist/utils/connection-manager.js.map +1 -0
- package/dist/utils/debug-logger.d.ts +8 -4
- package/dist/utils/debug-logger.d.ts.map +1 -1
- package/dist/utils/debug-logger.js +27 -7
- package/dist/utils/debug-logger.js.map +1 -1
- package/dist/utils/error-handler.d.ts +6 -4
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +34 -9
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/parameter-validator.d.ts.map +1 -1
- package/dist/utils/parameter-validator.js +50 -16
- package/dist/utils/parameter-validator.js.map +1 -1
- package/dist/utils/project-context.d.ts +111 -0
- package/dist/utils/project-context.d.ts.map +1 -0
- package/dist/utils/project-context.js +187 -0
- package/dist/utils/project-context.js.map +1 -0
- package/dist/utils/sql-dump-converters.d.ts +188 -0
- package/dist/utils/sql-dump-converters.d.ts.map +1 -0
- package/dist/utils/sql-dump-converters.js +311 -0
- package/dist/utils/sql-dump-converters.js.map +1 -0
- package/dist/utils/sql-dump.d.ts +102 -0
- package/dist/utils/sql-dump.d.ts.map +1 -0
- package/dist/utils/sql-dump.js +1550 -0
- package/dist/utils/sql-dump.js.map +1 -0
- package/dist/utils/vcs-adapter.d.ts +42 -0
- package/dist/utils/vcs-adapter.d.ts.map +1 -1
- package/dist/utils/vcs-adapter.js +154 -0
- package/dist/utils/vcs-adapter.js.map +1 -1
- package/docs/BASEADAPTER_IMPLEMENTATION.md +399 -0
- package/docs/DATABASE_AUTH.md +445 -0
- package/docs/DATABASE_MIGRATION.md +247 -0
- package/docs/MULTI_PROJECT_ARCHITECTURE.md +497 -0
- package/package.json +12 -4
- package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025070349_create_views.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025070349_create_views.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +0 -15
- package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js +0 -34
- package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +0 -1
- /package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.js +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.js +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.js +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js +0 -0
|
@@ -0,0 +1,1550 @@
|
|
|
1
|
+
// sql-dump.ts - Utility functions for generating SQL dump files
|
|
2
|
+
import knex from 'knex';
|
|
3
|
+
import { SchemaInspector } from 'knex-schema-inspector';
|
|
4
|
+
import { convertIdentifierQuotes, convertTimestampFunctions, } from './sql-dump-converters.js';
|
|
5
|
+
import { debugLog } from './debug-logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Convert data type from source format to target format using metadata
|
|
8
|
+
*/
|
|
9
|
+
function convertDataType(columnType, targetFormat, maxLength) {
|
|
10
|
+
const upperType = columnType.toUpperCase();
|
|
11
|
+
if (targetFormat === 'mysql') {
|
|
12
|
+
// MySQL-specific conversions
|
|
13
|
+
if (upperType.includes('SERIAL') || upperType.includes('BIGSERIAL')) {
|
|
14
|
+
return 'BIGINT AUTO_INCREMENT';
|
|
15
|
+
}
|
|
16
|
+
if (upperType.includes('TEXT')) {
|
|
17
|
+
return 'TEXT';
|
|
18
|
+
}
|
|
19
|
+
if (upperType.includes('VARCHAR')) {
|
|
20
|
+
// Use maxLength from metadata
|
|
21
|
+
const length = maxLength && maxLength <= 191 ? maxLength : 191;
|
|
22
|
+
return `VARCHAR(${length})`;
|
|
23
|
+
}
|
|
24
|
+
if (upperType.includes('TIMESTAMP') || upperType.includes('TIMESTAMPTZ')) {
|
|
25
|
+
return 'DATETIME';
|
|
26
|
+
}
|
|
27
|
+
if (upperType.includes('BOOLEAN') || upperType === 'BOOL') {
|
|
28
|
+
return 'TINYINT(1)';
|
|
29
|
+
}
|
|
30
|
+
if (upperType === 'INTEGER' || upperType === 'INT') {
|
|
31
|
+
return 'INT';
|
|
32
|
+
}
|
|
33
|
+
if (upperType.includes('BIGINT')) {
|
|
34
|
+
return 'BIGINT';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (targetFormat === 'postgresql') {
|
|
38
|
+
// PostgreSQL-specific conversions
|
|
39
|
+
if (upperType.includes('AUTOINCREMENT') || upperType.includes('AUTO_INCREMENT')) {
|
|
40
|
+
return 'SERIAL';
|
|
41
|
+
}
|
|
42
|
+
if (upperType.includes('DATETIME')) {
|
|
43
|
+
return 'TIMESTAMP';
|
|
44
|
+
}
|
|
45
|
+
if (upperType.includes('TINYINT') || upperType === 'BIT') {
|
|
46
|
+
return 'BOOLEAN';
|
|
47
|
+
}
|
|
48
|
+
if (upperType.includes('TEXT')) {
|
|
49
|
+
return 'TEXT';
|
|
50
|
+
}
|
|
51
|
+
if (upperType.includes('VARCHAR')) {
|
|
52
|
+
const length = maxLength || 255;
|
|
53
|
+
return `VARCHAR(${length})`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (targetFormat === 'sqlite') {
|
|
57
|
+
// SQLite-specific conversions
|
|
58
|
+
if (upperType.includes('SERIAL') || upperType.includes('AUTO_INCREMENT') || upperType.includes('AUTOINCREMENT')) {
|
|
59
|
+
return 'INTEGER';
|
|
60
|
+
}
|
|
61
|
+
if (upperType.includes('VARCHAR') || upperType.includes('TEXT')) {
|
|
62
|
+
return 'TEXT';
|
|
63
|
+
}
|
|
64
|
+
if (upperType.includes('TINYINT') || upperType.includes('BOOLEAN')) {
|
|
65
|
+
return 'INTEGER';
|
|
66
|
+
}
|
|
67
|
+
if (upperType.includes('DATETIME') || upperType.includes('TIMESTAMP')) {
|
|
68
|
+
return 'INTEGER'; // SQLite stores as Unix timestamp
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Default: return as-is
|
|
72
|
+
return columnType;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Convert default value from SQLite functions to target format
|
|
76
|
+
* Handles: unixepoch() → UNIX_TIMESTAMP() / EXTRACT(epoch FROM NOW())
|
|
77
|
+
* strftime() → DATE_FORMAT() / TO_CHAR()
|
|
78
|
+
*/
|
|
79
|
+
function convertDefaultValue(defaultValue, targetFormat) {
|
|
80
|
+
if (!defaultValue) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const lower = defaultValue.toLowerCase().trim();
|
|
84
|
+
// SQLite unixepoch() conversions
|
|
85
|
+
if (lower.includes('unixepoch()') || lower === 'unixepoch()') {
|
|
86
|
+
if (targetFormat === 'mysql') {
|
|
87
|
+
return 'UNIX_TIMESTAMP()';
|
|
88
|
+
}
|
|
89
|
+
else if (targetFormat === 'postgresql') {
|
|
90
|
+
return 'EXTRACT(epoch FROM NOW())::INTEGER';
|
|
91
|
+
}
|
|
92
|
+
return null; // Remove for SQLite
|
|
93
|
+
}
|
|
94
|
+
// SQLite strftime('%s', 'now') - Unix timestamp (INTEGER)
|
|
95
|
+
// Must check BEFORE generic strftime to handle this specific case
|
|
96
|
+
if (lower.includes("strftime('%s'") || lower.includes('strftime("%s"')) {
|
|
97
|
+
if (targetFormat === 'mysql') {
|
|
98
|
+
return 'UNIX_TIMESTAMP()';
|
|
99
|
+
}
|
|
100
|
+
else if (targetFormat === 'postgresql') {
|
|
101
|
+
return 'EXTRACT(epoch FROM NOW())::INTEGER';
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
// SQLite strftime conversions (datetime formats)
|
|
106
|
+
if (lower.includes('strftime')) {
|
|
107
|
+
if (targetFormat === 'mysql') {
|
|
108
|
+
// strftime('%Y-%m-%d %H:%M:%S', 'now') → NOW()
|
|
109
|
+
return 'NOW()';
|
|
110
|
+
}
|
|
111
|
+
else if (targetFormat === 'postgresql') {
|
|
112
|
+
return 'NOW()';
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Remove parentheses for simple values
|
|
117
|
+
let cleanValue = defaultValue;
|
|
118
|
+
if (lower.startsWith('(') && lower.endsWith(')')) {
|
|
119
|
+
cleanValue = defaultValue.substring(1, defaultValue.length - 1);
|
|
120
|
+
}
|
|
121
|
+
// For numeric defaults, normalize floating point values
|
|
122
|
+
// Remove trailing .0 (e.g., 1.0 → 1) to avoid type mismatches
|
|
123
|
+
const trimmed = cleanValue.trim();
|
|
124
|
+
if (/^\d+\.0+$/.test(trimmed)) {
|
|
125
|
+
return trimmed.replace(/\.0+$/, '');
|
|
126
|
+
}
|
|
127
|
+
// Quote string literals if not already quoted and not a number
|
|
128
|
+
if (!/^['"]/.test(trimmed) && !/^-?\d+(\.\d+)?$/.test(trimmed) && !/^(true|false|null|current_timestamp|now\(\)|unix_timestamp\(\))$/i.test(trimmed)) {
|
|
129
|
+
return `'${trimmed}'`;
|
|
130
|
+
}
|
|
131
|
+
return trimmed;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Quote identifier (table or column name) for target database
|
|
135
|
+
*/
|
|
136
|
+
export function quoteIdentifier(name, format) {
|
|
137
|
+
switch (format) {
|
|
138
|
+
case 'mysql':
|
|
139
|
+
return `\`${name}\``;
|
|
140
|
+
case 'postgresql':
|
|
141
|
+
case 'sqlite':
|
|
142
|
+
return `"${name}"`;
|
|
143
|
+
default:
|
|
144
|
+
return `"${name}"`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build column definition from Column metadata
|
|
149
|
+
*/
|
|
150
|
+
function buildColumnDefinition(col, targetFormat) {
|
|
151
|
+
const quotedName = quoteIdentifier(col.name, targetFormat);
|
|
152
|
+
let dataType = convertDataType(col.data_type, targetFormat, col.max_length);
|
|
153
|
+
// MySQL: TEXT columns cannot be used in UNIQUE/PRIMARY KEY constraints without prefix length
|
|
154
|
+
// Convert TEXT to VARCHAR(191) for utf8mb4 compatibility (768 bytes ÷ 4 bytes/char = 191)
|
|
155
|
+
if (targetFormat === 'mysql' && dataType.toUpperCase() === 'TEXT') {
|
|
156
|
+
if (col.is_unique || col.is_primary_key || col.foreign_key_table || col.in_composite_unique) {
|
|
157
|
+
dataType = 'VARCHAR(191)';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let def = `${quotedName} ${dataType}`;
|
|
161
|
+
// Handle NOT NULL constraint
|
|
162
|
+
if (col.is_nullable === false) {
|
|
163
|
+
def += ' NOT NULL';
|
|
164
|
+
}
|
|
165
|
+
// Handle DEFAULT value
|
|
166
|
+
if (col.default_value !== null && col.default_value !== undefined) {
|
|
167
|
+
let convertedDefault = convertDefaultValue(String(col.default_value), targetFormat);
|
|
168
|
+
if (convertedDefault !== null && convertedDefault !== '') {
|
|
169
|
+
// MySQL restrictions for DEFAULT values
|
|
170
|
+
const isTextColumn = dataType.toUpperCase().includes('TEXT') || dataType.toUpperCase().includes('BLOB');
|
|
171
|
+
const isFunctionCall = convertedDefault.includes('(') && convertedDefault.includes(')');
|
|
172
|
+
const isIntegerColumn = dataType.toUpperCase().includes('INT');
|
|
173
|
+
const isBooleanColumn = dataType.toUpperCase().includes('BOOLEAN');
|
|
174
|
+
// PostgreSQL: Convert integer defaults to boolean for BOOLEAN columns
|
|
175
|
+
if (targetFormat === 'postgresql' && isBooleanColumn && /^[01]$/.test(convertedDefault)) {
|
|
176
|
+
convertedDefault = convertedDefault === '1' ? 'TRUE' : 'FALSE';
|
|
177
|
+
}
|
|
178
|
+
if (targetFormat === 'mysql') {
|
|
179
|
+
// MySQL doesn't allow DEFAULT on TEXT/BLOB columns
|
|
180
|
+
if (isTextColumn) {
|
|
181
|
+
// Skip DEFAULT - application must handle at runtime
|
|
182
|
+
}
|
|
183
|
+
// MySQL DOES support certain function calls as DEFAULT for INTEGER columns
|
|
184
|
+
// (e.g., UNIX_TIMESTAMP(), CURRENT_TIMESTAMP)
|
|
185
|
+
// Only skip if conversion returned null (meaning function not supported)
|
|
186
|
+
else {
|
|
187
|
+
def += ` DEFAULT ${convertedDefault}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
def += ` DEFAULT ${convertedDefault}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Handle AUTO_INCREMENT for MySQL
|
|
196
|
+
if (targetFormat === 'mysql' && col.is_generated && col.generation_expression === null) {
|
|
197
|
+
if (!def.includes('AUTO_INCREMENT')) {
|
|
198
|
+
def += ' AUTO_INCREMENT';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Handle UNIQUE constraint (skip if already PRIMARY KEY)
|
|
202
|
+
if (col.is_unique && !col.is_primary_key) {
|
|
203
|
+
def += ' UNIQUE';
|
|
204
|
+
}
|
|
205
|
+
return def;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Build FOREIGN KEY definition from ForeignKey metadata
|
|
209
|
+
*/
|
|
210
|
+
function buildForeignKeyDefinition(fk, targetFormat) {
|
|
211
|
+
const quotedColumn = quoteIdentifier(fk.column, targetFormat);
|
|
212
|
+
const quotedForeignTable = quoteIdentifier(fk.foreign_key_table, targetFormat);
|
|
213
|
+
const quotedForeignColumn = quoteIdentifier(fk.foreign_key_column, targetFormat);
|
|
214
|
+
let fkDef = `FOREIGN KEY (${quotedColumn}) REFERENCES ${quotedForeignTable}(${quotedForeignColumn})`;
|
|
215
|
+
// Add ON DELETE clause
|
|
216
|
+
if (fk.on_delete && fk.on_delete !== 'NO ACTION') {
|
|
217
|
+
fkDef += ` ON DELETE ${fk.on_delete}`;
|
|
218
|
+
}
|
|
219
|
+
// Add ON UPDATE clause
|
|
220
|
+
if (fk.on_update && fk.on_update !== 'NO ACTION') {
|
|
221
|
+
fkDef += ` ON UPDATE ${fk.on_update}`;
|
|
222
|
+
}
|
|
223
|
+
return fkDef;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Enforce NOT NULL constraints on PRIMARY KEY columns
|
|
227
|
+
* MySQL and PostgreSQL require all PRIMARY KEY columns to be NOT NULL
|
|
228
|
+
*/
|
|
229
|
+
function enforceNotNullOnPrimaryKey(createSql, pkColumns) {
|
|
230
|
+
if (pkColumns.length === 0) {
|
|
231
|
+
return createSql;
|
|
232
|
+
}
|
|
233
|
+
// Split the CREATE TABLE statement to process column definitions
|
|
234
|
+
const lines = createSql.split('\n');
|
|
235
|
+
const processedLines = lines.map((line) => {
|
|
236
|
+
// Check if this line defines one of the PRIMARY KEY columns
|
|
237
|
+
for (const pkCol of pkColumns) {
|
|
238
|
+
// Match column definition (handle different quote styles)
|
|
239
|
+
const colPattern = new RegExp(`^\\s*[\`"']?${pkCol}[\`"']?\\s+`, 'i');
|
|
240
|
+
if (colPattern.test(line)) {
|
|
241
|
+
// Check if NOT NULL is already present
|
|
242
|
+
if (!/NOT\s+NULL/i.test(line)) {
|
|
243
|
+
// Find position to insert NOT NULL (before DEFAULT, FOREIGN KEY, CHECK, or comma/closing paren)
|
|
244
|
+
const insertBeforePatterns = [
|
|
245
|
+
/\s+DEFAULT/i,
|
|
246
|
+
/\s+FOREIGN\s+KEY/i,
|
|
247
|
+
/\s+CHECK/i,
|
|
248
|
+
/,\s*$/,
|
|
249
|
+
/\)\s*$/,
|
|
250
|
+
];
|
|
251
|
+
let insertPos = line.length;
|
|
252
|
+
for (const pattern of insertBeforePatterns) {
|
|
253
|
+
const match = line.match(pattern);
|
|
254
|
+
if (match && match.index !== undefined) {
|
|
255
|
+
insertPos = Math.min(insertPos, match.index);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Insert NOT NULL
|
|
259
|
+
return line.slice(0, insertPos) + ' NOT NULL' + line.slice(insertPos);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return line;
|
|
264
|
+
});
|
|
265
|
+
return processedLines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get primary key columns for a table
|
|
269
|
+
*/
|
|
270
|
+
export async function getPrimaryKeyColumns(knex, table) {
|
|
271
|
+
const client = knex.client.config.client;
|
|
272
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
273
|
+
// SQLite: Use PRAGMA table_info
|
|
274
|
+
const result = await knex.raw(`PRAGMA table_info(${table})`);
|
|
275
|
+
return result
|
|
276
|
+
.filter((col) => col.pk > 0)
|
|
277
|
+
.sort((a, b) => a.pk - b.pk)
|
|
278
|
+
.map((col) => col.name);
|
|
279
|
+
}
|
|
280
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
281
|
+
// MySQL: Query information_schema
|
|
282
|
+
const result = await knex.raw(`
|
|
283
|
+
SELECT COLUMN_NAME
|
|
284
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
285
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
286
|
+
AND TABLE_NAME = ?
|
|
287
|
+
AND CONSTRAINT_NAME = 'PRIMARY'
|
|
288
|
+
ORDER BY ORDINAL_POSITION
|
|
289
|
+
`, [table]);
|
|
290
|
+
return result[0].map((row) => row.COLUMN_NAME);
|
|
291
|
+
}
|
|
292
|
+
else if (client === 'pg') {
|
|
293
|
+
// PostgreSQL: Query information_schema
|
|
294
|
+
const result = await knex.raw(`
|
|
295
|
+
SELECT a.attname AS column_name
|
|
296
|
+
FROM pg_index i
|
|
297
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
298
|
+
WHERE i.indrelid = ?::regclass
|
|
299
|
+
AND i.indisprimary
|
|
300
|
+
ORDER BY a.attnum
|
|
301
|
+
`, [table]);
|
|
302
|
+
return result.rows.map((row) => row.column_name);
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`Unsupported database client: ${client}`);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get CREATE TABLE statement for a table using knex-schema-inspector
|
|
308
|
+
* Replaces regex-based SQL conversion with metadata-driven approach
|
|
309
|
+
*/
|
|
310
|
+
export async function getCreateTableStatement(knex, table, targetFormat) {
|
|
311
|
+
const client = knex.client.config.client;
|
|
312
|
+
// Initialize schema inspector (database-agnostic)
|
|
313
|
+
const inspector = SchemaInspector(knex);
|
|
314
|
+
// Get column metadata
|
|
315
|
+
const columns = await inspector.columnInfo(table);
|
|
316
|
+
if (columns.length === 0) {
|
|
317
|
+
throw new Error(`Table ${table} not found or has no columns`);
|
|
318
|
+
}
|
|
319
|
+
// Fix: knex-schema-inspector doesn't detect composite PRIMARY KEYs and UNIQUE constraints from SQLite properly
|
|
320
|
+
// Manually detect them using PRAGMA index_list
|
|
321
|
+
const compositeUniqueConstraints = []; // Track composite UNIQUE constraints
|
|
322
|
+
let compositePrimaryKey = null; // Track composite PRIMARY KEY
|
|
323
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
324
|
+
const indexResult = await knex.raw(`PRAGMA index_list(${table})`);
|
|
325
|
+
// Knex raw() returns an array directly for SQLite
|
|
326
|
+
const indexes = Array.isArray(indexResult) ? indexResult : [];
|
|
327
|
+
for (const index of indexes) {
|
|
328
|
+
// Check for PRIMARY KEY index
|
|
329
|
+
if (index.origin === 'pk' && index.unique === 1) {
|
|
330
|
+
const indexInfoResult = await knex.raw(`PRAGMA index_info(${index.name})`);
|
|
331
|
+
const indexInfo = Array.isArray(indexInfoResult) ? indexInfoResult : [];
|
|
332
|
+
const columnNames = indexInfo.map((idxCol) => idxCol.name);
|
|
333
|
+
if (columnNames.length > 1) {
|
|
334
|
+
// Composite PRIMARY KEY detected
|
|
335
|
+
compositePrimaryKey = columnNames;
|
|
336
|
+
debugLog('DEBUG', `Found composite PRIMARY KEY on ${table}(${columnNames.join(', ')}) from ${index.name}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Check if this is a UNIQUE index (skip PRIMARY KEY indexes)
|
|
340
|
+
else if (index.unique === 1 && index.origin !== 'pk') {
|
|
341
|
+
// Get columns in this index
|
|
342
|
+
const indexInfoResult = await knex.raw(`PRAGMA index_info(${index.name})`);
|
|
343
|
+
const indexInfo = Array.isArray(indexInfoResult) ? indexInfoResult : [];
|
|
344
|
+
const columnNames = indexInfo.map((idxCol) => idxCol.name);
|
|
345
|
+
if (columnNames.length === 1) {
|
|
346
|
+
// Single-column UNIQUE - mark column as unique
|
|
347
|
+
const col = columns.find(c => c.name === columnNames[0]);
|
|
348
|
+
if (col && !col.is_primary_key) {
|
|
349
|
+
col.is_unique = true;
|
|
350
|
+
debugLog('DEBUG', `Marked ${table}.${col.name} as UNIQUE (single-column from ${index.name})`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (columnNames.length > 1) {
|
|
354
|
+
// Composite UNIQUE - add to table-level constraints
|
|
355
|
+
compositeUniqueConstraints.push(columnNames);
|
|
356
|
+
debugLog('DEBUG', `Found composite UNIQUE on ${table}(${columnNames.join(', ')}) from ${index.name}`);
|
|
357
|
+
// For MySQL: Convert TEXT to VARCHAR(191) for columns in composite UNIQUE
|
|
358
|
+
if (targetFormat === 'mysql') {
|
|
359
|
+
for (const colName of columnNames) {
|
|
360
|
+
const col = columns.find(c => c.name === colName);
|
|
361
|
+
if (col && !col.is_primary_key) {
|
|
362
|
+
// Mark as part of composite unique (will be converted to VARCHAR later)
|
|
363
|
+
col.in_composite_unique = true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Build column definitions using buildColumnDefinition()
|
|
372
|
+
const columnDefs = columns.map(col => buildColumnDefinition(col, targetFormat));
|
|
373
|
+
// Add PRIMARY KEY constraint (with MySQL prefix length handling)
|
|
374
|
+
// Use composite PRIMARY KEY if detected, otherwise fall back to column metadata
|
|
375
|
+
const pkColumns = compositePrimaryKey || columns.filter(col => col.is_primary_key).map(col => col.name);
|
|
376
|
+
if (pkColumns.length > 0) {
|
|
377
|
+
// For MySQL: Apply (191) prefix to TEXT/long VARCHAR columns
|
|
378
|
+
if (targetFormat === 'mysql') {
|
|
379
|
+
const processedPkCols = pkColumns.map((colName) => {
|
|
380
|
+
const col = columns.find(c => c.name === colName);
|
|
381
|
+
if (col && (col.data_type.toUpperCase() === 'TEXT' ||
|
|
382
|
+
(col.data_type.toUpperCase().includes('VARCHAR') && col.max_length && col.max_length > 191))) {
|
|
383
|
+
return `${quoteIdentifier(colName, targetFormat)}(191)`;
|
|
384
|
+
}
|
|
385
|
+
return quoteIdentifier(colName, targetFormat);
|
|
386
|
+
}).join(', ');
|
|
387
|
+
columnDefs.push(`PRIMARY KEY (${processedPkCols})`);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
const quotedPkColumns = pkColumns.map(col => quoteIdentifier(col, targetFormat));
|
|
391
|
+
columnDefs.push(`PRIMARY KEY (${quotedPkColumns.join(', ')})`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Add FOREIGN KEY constraints using buildForeignKeyDefinition()
|
|
395
|
+
const foreignKeys = await inspector.foreignKeys(table);
|
|
396
|
+
for (const fk of foreignKeys) {
|
|
397
|
+
columnDefs.push(buildForeignKeyDefinition(fk, targetFormat));
|
|
398
|
+
}
|
|
399
|
+
// Add composite UNIQUE constraints (from SQLite multi-column UNIQUE indexes)
|
|
400
|
+
for (const uniqueCols of compositeUniqueConstraints) {
|
|
401
|
+
const quotedCols = uniqueCols.map(col => quoteIdentifier(col, targetFormat)).join(', ');
|
|
402
|
+
columnDefs.push(`UNIQUE (${quotedCols})`);
|
|
403
|
+
}
|
|
404
|
+
// Build CREATE TABLE statement with IF NOT EXISTS for idempotency
|
|
405
|
+
const quotedTable = quoteIdentifier(table, targetFormat);
|
|
406
|
+
const createSql = `CREATE TABLE IF NOT EXISTS ${quotedTable} (\n ${columnDefs.join(',\n ')}\n)`;
|
|
407
|
+
// Add database-specific table options
|
|
408
|
+
if (targetFormat === 'mysql') {
|
|
409
|
+
return createSql + ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;';
|
|
410
|
+
}
|
|
411
|
+
return createSql + ';';
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Get all table names from the database (excluding system tables)
|
|
415
|
+
*/
|
|
416
|
+
export async function getAllTables(knex, includeKnexTables = false) {
|
|
417
|
+
const client = knex.client.config.client;
|
|
418
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
419
|
+
const knexFilter = includeKnexTables ? '' : "AND name NOT LIKE 'knex_%'";
|
|
420
|
+
const result = await knex.raw(`
|
|
421
|
+
SELECT name FROM sqlite_master
|
|
422
|
+
WHERE type='table'
|
|
423
|
+
AND name NOT LIKE 'sqlite_%'
|
|
424
|
+
${knexFilter}
|
|
425
|
+
ORDER BY name
|
|
426
|
+
`);
|
|
427
|
+
return result.map((row) => row.name);
|
|
428
|
+
}
|
|
429
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
430
|
+
const result = await knex.raw('SHOW TABLES');
|
|
431
|
+
const tableKey = Object.keys(result[0][0])[0];
|
|
432
|
+
const tables = result[0].map((row) => row[tableKey]);
|
|
433
|
+
return includeKnexTables ? tables : tables.filter((t) => !t.startsWith('knex_'));
|
|
434
|
+
}
|
|
435
|
+
else if (client === 'pg') {
|
|
436
|
+
const knexFilter = includeKnexTables ? '' : "AND tablename NOT LIKE 'knex_%'";
|
|
437
|
+
const result = await knex.raw(`
|
|
438
|
+
SELECT tablename FROM pg_tables
|
|
439
|
+
WHERE schemaname = 'public'
|
|
440
|
+
${knexFilter}
|
|
441
|
+
ORDER BY tablename
|
|
442
|
+
`);
|
|
443
|
+
return result.rows.map((row) => row.tablename);
|
|
444
|
+
}
|
|
445
|
+
throw new Error(`Unsupported database client: ${client}`);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get all view names from the database
|
|
449
|
+
*/
|
|
450
|
+
export async function getAllViews(knex) {
|
|
451
|
+
const client = knex.client.config.client;
|
|
452
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
453
|
+
const result = await knex.raw(`
|
|
454
|
+
SELECT name FROM sqlite_master
|
|
455
|
+
WHERE type='view'
|
|
456
|
+
ORDER BY name
|
|
457
|
+
`);
|
|
458
|
+
return result.map((row) => row.name);
|
|
459
|
+
}
|
|
460
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
461
|
+
const result = await knex.raw(`
|
|
462
|
+
SELECT TABLE_NAME as name
|
|
463
|
+
FROM information_schema.VIEWS
|
|
464
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
465
|
+
ORDER BY TABLE_NAME
|
|
466
|
+
`);
|
|
467
|
+
return result[0].map((row) => row.name);
|
|
468
|
+
}
|
|
469
|
+
else if (client === 'pg') {
|
|
470
|
+
const result = await knex.raw(`
|
|
471
|
+
SELECT viewname as name
|
|
472
|
+
FROM pg_views
|
|
473
|
+
WHERE schemaname = 'public'
|
|
474
|
+
ORDER BY viewname
|
|
475
|
+
`);
|
|
476
|
+
return result.rows.map((row) => row.name);
|
|
477
|
+
}
|
|
478
|
+
throw new Error(`Unsupported database client: ${client}`);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get all indexes for a table
|
|
482
|
+
*/
|
|
483
|
+
export async function getAllIndexes(knex, table) {
|
|
484
|
+
const client = knex.client.config.client;
|
|
485
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
486
|
+
const result = await knex.raw(`
|
|
487
|
+
SELECT name FROM sqlite_master
|
|
488
|
+
WHERE type='index'
|
|
489
|
+
AND tbl_name=?
|
|
490
|
+
AND sql IS NOT NULL
|
|
491
|
+
ORDER BY name
|
|
492
|
+
`, [table]);
|
|
493
|
+
return result.map((row) => row.name);
|
|
494
|
+
}
|
|
495
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
496
|
+
const result = await knex.raw(`
|
|
497
|
+
SHOW INDEXES FROM ?? WHERE Key_name != 'PRIMARY'
|
|
498
|
+
`, [table]);
|
|
499
|
+
// Group by index name (indexes can span multiple columns)
|
|
500
|
+
const indexNames = new Set();
|
|
501
|
+
for (const row of result[0]) {
|
|
502
|
+
indexNames.add(row.Key_name);
|
|
503
|
+
}
|
|
504
|
+
return Array.from(indexNames).sort();
|
|
505
|
+
}
|
|
506
|
+
else if (client === 'pg') {
|
|
507
|
+
const result = await knex.raw(`
|
|
508
|
+
SELECT indexname
|
|
509
|
+
FROM pg_indexes
|
|
510
|
+
WHERE schemaname = 'public'
|
|
511
|
+
AND tablename = ?
|
|
512
|
+
AND indexname NOT LIKE '%_pkey'
|
|
513
|
+
ORDER BY indexname
|
|
514
|
+
`, [table]);
|
|
515
|
+
return result.rows.map((row) => row.indexname);
|
|
516
|
+
}
|
|
517
|
+
// For other database clients
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Get CREATE INDEX statement for an index
|
|
522
|
+
*/
|
|
523
|
+
export async function getCreateIndexStatement(knex, indexName, targetFormat) {
|
|
524
|
+
const client = knex.client.config.client;
|
|
525
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
526
|
+
const result = await knex.raw(`
|
|
527
|
+
SELECT sql, tbl_name FROM sqlite_master
|
|
528
|
+
WHERE type='index' AND name=?
|
|
529
|
+
`, [indexName]);
|
|
530
|
+
if (result.length === 0 || !result[0].sql) {
|
|
531
|
+
throw new Error(`Index ${indexName} not found`);
|
|
532
|
+
}
|
|
533
|
+
let createSql = result[0].sql;
|
|
534
|
+
const tableName = result[0].tbl_name;
|
|
535
|
+
// Convert to target format if needed
|
|
536
|
+
if (targetFormat === 'mysql') {
|
|
537
|
+
createSql = convertIdentifierQuotes(createSql, 'mysql');
|
|
538
|
+
// MySQL has a 3072 byte limit for index keys (with utf8mb4, that's ~768 chars)
|
|
539
|
+
// Add prefix length (191 chars) to VARCHAR columns longer than 191 to stay under limit
|
|
540
|
+
// Match: CREATE [UNIQUE] INDEX name ON table (col1, col2, ...)
|
|
541
|
+
const match = createSql.match(/\((.*?)\)(?:\s|$)/);
|
|
542
|
+
if (match) {
|
|
543
|
+
const columns = match[1];
|
|
544
|
+
// Get column info for the table
|
|
545
|
+
const columnInfo = await knex(tableName).columnInfo();
|
|
546
|
+
// Process each column in the index
|
|
547
|
+
const processedColumns = columns.split(',').map((col) => {
|
|
548
|
+
// Remove quotes/backticks and DESC/ASC keywords
|
|
549
|
+
let colSpec = col.trim().replace(/[`"]/g, '');
|
|
550
|
+
const colName = colSpec.replace(/\s+(DESC|ASC)$/i, '').trim();
|
|
551
|
+
const info = columnInfo[colName];
|
|
552
|
+
// If VARCHAR/TEXT longer than 191 chars, add prefix length
|
|
553
|
+
if (info && info.type) {
|
|
554
|
+
const type = info.type.toLowerCase();
|
|
555
|
+
if (type.includes('varchar') || type.includes('text')) {
|
|
556
|
+
// Check maxLength property first (SQLite returns this separately)
|
|
557
|
+
const maxLength = info.maxLength;
|
|
558
|
+
if (maxLength && parseInt(maxLength) > 191) {
|
|
559
|
+
return `\`${colName}\`(191)`;
|
|
560
|
+
}
|
|
561
|
+
// Also try extracting length from VARCHAR(n) in type string
|
|
562
|
+
const lengthMatch = type.match(/varchar\((\d+)\)/);
|
|
563
|
+
if (lengthMatch && parseInt(lengthMatch[1]) > 191) {
|
|
564
|
+
return `\`${colName}\`(191)`;
|
|
565
|
+
}
|
|
566
|
+
else if (type.includes('text')) {
|
|
567
|
+
// TEXT columns have no fixed length, always add prefix
|
|
568
|
+
return `\`${colName}\`(191)`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return `\`${colName}\``;
|
|
573
|
+
}).join(', ');
|
|
574
|
+
createSql = createSql.replace(/\((.*?)\)(?:\s|$)/, `(${processedColumns})`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else if (targetFormat === 'postgresql') {
|
|
578
|
+
createSql = convertIdentifierQuotes(createSql, 'postgresql');
|
|
579
|
+
}
|
|
580
|
+
return createSql + ';';
|
|
581
|
+
}
|
|
582
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
583
|
+
// For MySQL, we need to find which table the index belongs to
|
|
584
|
+
// First, get all tables and search for the index
|
|
585
|
+
const tablesResult = await knex.raw('SHOW TABLES');
|
|
586
|
+
const tableKey = Object.keys(tablesResult[0][0])[0];
|
|
587
|
+
const tables = tablesResult[0].map((row) => row[tableKey]);
|
|
588
|
+
let indexInfo = null;
|
|
589
|
+
let tableName = '';
|
|
590
|
+
// Search for the index in all tables
|
|
591
|
+
for (const table of tables) {
|
|
592
|
+
const result = await knex.raw(`SHOW INDEXES FROM ?? WHERE Key_name = ?`, [table, indexName]);
|
|
593
|
+
if (result[0].length > 0) {
|
|
594
|
+
indexInfo = result[0];
|
|
595
|
+
tableName = table;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (!indexInfo || indexInfo.length === 0) {
|
|
600
|
+
throw new Error(`Index ${indexName} not found`);
|
|
601
|
+
}
|
|
602
|
+
const isUnique = indexInfo[0].Non_unique === 0;
|
|
603
|
+
// Get column info for prefix length handling
|
|
604
|
+
const columnInfo = await knex(tableName).columnInfo();
|
|
605
|
+
// Build column list with proper prefix lengths
|
|
606
|
+
const columns = indexInfo.map((row) => {
|
|
607
|
+
const colName = row.Column_name;
|
|
608
|
+
const colMeta = columnInfo[colName];
|
|
609
|
+
// Handle prefix length for long VARCHAR/TEXT columns
|
|
610
|
+
if (colMeta && colMeta.type) {
|
|
611
|
+
const type = colMeta.type.toLowerCase();
|
|
612
|
+
if (type.includes('varchar') || type.includes('text')) {
|
|
613
|
+
const maxLength = colMeta.maxLength;
|
|
614
|
+
// Extract length from type string like "varchar(255)"
|
|
615
|
+
const lengthMatch = type.match(/varchar\((\d+)\)/);
|
|
616
|
+
const typeLength = lengthMatch ? parseInt(lengthMatch[1]) : null;
|
|
617
|
+
if ((maxLength && parseInt(maxLength) > 191) || (typeLength && typeLength > 191) || type.includes('text')) {
|
|
618
|
+
return `\`${colName}\`(191)`;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return `\`${colName}\``;
|
|
623
|
+
}).join(', ');
|
|
624
|
+
const uniqueStr = isUnique ? 'UNIQUE ' : '';
|
|
625
|
+
let createSql = `CREATE ${uniqueStr}INDEX \`${indexName}\` ON \`${tableName}\` (${columns})`;
|
|
626
|
+
// Apply cross-database conversion
|
|
627
|
+
if (targetFormat === 'postgresql') {
|
|
628
|
+
createSql = convertIdentifierQuotes(createSql, 'postgresql');
|
|
629
|
+
}
|
|
630
|
+
else if (targetFormat === 'sqlite') {
|
|
631
|
+
createSql = convertIdentifierQuotes(createSql, 'sqlite');
|
|
632
|
+
// Remove prefix lengths for SQLite (not supported)
|
|
633
|
+
createSql = createSql.replace(/\(191\)/g, '');
|
|
634
|
+
}
|
|
635
|
+
return createSql + ';';
|
|
636
|
+
}
|
|
637
|
+
else if (client === 'pg') {
|
|
638
|
+
// Get index definition from PostgreSQL
|
|
639
|
+
const result = await knex.raw(`
|
|
640
|
+
SELECT indexdef
|
|
641
|
+
FROM pg_indexes
|
|
642
|
+
WHERE schemaname = 'public' AND indexname = ?
|
|
643
|
+
`, [indexName]);
|
|
644
|
+
if (result.rows.length === 0) {
|
|
645
|
+
throw new Error(`Index ${indexName} not found`);
|
|
646
|
+
}
|
|
647
|
+
let createSql = result.rows[0].indexdef;
|
|
648
|
+
// Apply cross-database conversion
|
|
649
|
+
if (targetFormat === 'mysql') {
|
|
650
|
+
createSql = convertIdentifierQuotes(createSql, 'mysql');
|
|
651
|
+
// Handle prefix length for MySQL
|
|
652
|
+
// Extract table name from the CREATE INDEX statement
|
|
653
|
+
const tableMatch = createSql.match(/ON\s+(["`]?\w+["`]?)/i);
|
|
654
|
+
if (tableMatch) {
|
|
655
|
+
const tableName = tableMatch[1].replace(/["`]/g, '');
|
|
656
|
+
const columnInfo = await knex(tableName).columnInfo();
|
|
657
|
+
// Find column list in the index definition
|
|
658
|
+
const colMatch = createSql.match(/\((.*?)\)(?:\s|$)/);
|
|
659
|
+
if (colMatch) {
|
|
660
|
+
const columns = colMatch[1];
|
|
661
|
+
const processedColumns = columns.split(',').map((col) => {
|
|
662
|
+
const colName = col.trim().replace(/["`]/g, '').replace(/\s+(DESC|ASC)$/i, '').trim();
|
|
663
|
+
const info = columnInfo[colName];
|
|
664
|
+
if (info && info.type) {
|
|
665
|
+
const type = info.type.toLowerCase();
|
|
666
|
+
if (type.includes('varchar') || type.includes('text')) {
|
|
667
|
+
const maxLength = info.maxLength;
|
|
668
|
+
const lengthMatch = type.match(/varchar\((\d+)\)/);
|
|
669
|
+
const typeLength = lengthMatch ? parseInt(lengthMatch[1]) : null;
|
|
670
|
+
if ((maxLength && parseInt(maxLength) > 191) || (typeLength && typeLength > 191) || type.includes('text')) {
|
|
671
|
+
return `\`${colName}\`(191)`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return `\`${colName}\``;
|
|
676
|
+
}).join(', ');
|
|
677
|
+
createSql = createSql.replace(/\((.*?)\)(?:\s|$)/, `(${processedColumns})`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else if (targetFormat === 'sqlite') {
|
|
682
|
+
createSql = convertIdentifierQuotes(createSql, 'sqlite');
|
|
683
|
+
}
|
|
684
|
+
return createSql + ';';
|
|
685
|
+
}
|
|
686
|
+
throw new Error(`Unsupported database client: ${client}`);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Get CREATE VIEW statement for a view
|
|
690
|
+
*/
|
|
691
|
+
export async function getCreateViewStatement(knex, viewName, targetFormat) {
|
|
692
|
+
const client = knex.client.config.client;
|
|
693
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
694
|
+
// SQLite: Get from sqlite_master
|
|
695
|
+
const result = await knex.raw(`
|
|
696
|
+
SELECT sql FROM sqlite_master
|
|
697
|
+
WHERE type='view' AND name=?
|
|
698
|
+
`, [viewName]);
|
|
699
|
+
if (result.length === 0 || !result[0].sql) {
|
|
700
|
+
throw new Error(`View ${viewName} not found`);
|
|
701
|
+
}
|
|
702
|
+
let createSql = result[0].sql;
|
|
703
|
+
// Convert SQLite syntax to target format if needed
|
|
704
|
+
if (targetFormat === 'mysql') {
|
|
705
|
+
// Convert to MySQL syntax using shared converters
|
|
706
|
+
createSql = convertIdentifierQuotes(createSql, 'mysql');
|
|
707
|
+
createSql = convertTimestampFunctions(createSql, 'mysql');
|
|
708
|
+
}
|
|
709
|
+
else if (targetFormat === 'postgresql') {
|
|
710
|
+
// Convert to PostgreSQL syntax using shared converters
|
|
711
|
+
createSql = convertIdentifierQuotes(createSql, 'postgresql');
|
|
712
|
+
createSql = convertTimestampFunctions(createSql, 'postgresql');
|
|
713
|
+
// Convert GROUP_CONCAT(col, sep) → string_agg(col, sep)
|
|
714
|
+
createSql = createSql.replace(/GROUP_CONCAT\s*\(/gi, 'string_agg(');
|
|
715
|
+
// Cast integer comparisons to be type-safe: column = 1 → column::integer = 1
|
|
716
|
+
// This works for both boolean columns (TRUE::integer = 1) and integer enum columns
|
|
717
|
+
createSql = createSql.replace(/(\w+)\s*=\s*([01])\b/g, '$1::integer = $2');
|
|
718
|
+
}
|
|
719
|
+
return createSql + ';';
|
|
720
|
+
}
|
|
721
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
722
|
+
// MySQL: Use SHOW CREATE VIEW
|
|
723
|
+
const result = await knex.raw(`SHOW CREATE VIEW ??`, [viewName]);
|
|
724
|
+
let createSql = result[0][0]['Create View'];
|
|
725
|
+
if (targetFormat === 'sqlite') {
|
|
726
|
+
// Convert MySQL to SQLite using shared converters
|
|
727
|
+
createSql = convertIdentifierQuotes(createSql, 'sqlite');
|
|
728
|
+
createSql = convertTimestampFunctions(createSql, 'sqlite');
|
|
729
|
+
return createSql + ';';
|
|
730
|
+
}
|
|
731
|
+
else if (targetFormat === 'postgresql') {
|
|
732
|
+
// Convert MySQL to PostgreSQL using shared converters
|
|
733
|
+
createSql = convertIdentifierQuotes(createSql, 'postgresql');
|
|
734
|
+
createSql = convertTimestampFunctions(createSql, 'postgresql');
|
|
735
|
+
return createSql + ';';
|
|
736
|
+
}
|
|
737
|
+
return createSql + ';';
|
|
738
|
+
}
|
|
739
|
+
else if (client === 'pg') {
|
|
740
|
+
// PostgreSQL: Get from pg_views
|
|
741
|
+
const result = await knex.raw(`
|
|
742
|
+
SELECT definition
|
|
743
|
+
FROM pg_views
|
|
744
|
+
WHERE schemaname = 'public' AND viewname = ?
|
|
745
|
+
`, [viewName]);
|
|
746
|
+
if (result.rows.length === 0) {
|
|
747
|
+
throw new Error(`View ${viewName} not found`);
|
|
748
|
+
}
|
|
749
|
+
let createSql = `CREATE VIEW "${viewName}" AS ${result.rows[0].definition}`;
|
|
750
|
+
if (targetFormat === 'mysql') {
|
|
751
|
+
createSql = convertIdentifierQuotes(createSql, 'mysql');
|
|
752
|
+
return createSql + ';';
|
|
753
|
+
}
|
|
754
|
+
else if (targetFormat === 'sqlite') {
|
|
755
|
+
createSql = convertTimestampFunctions(createSql, 'sqlite');
|
|
756
|
+
return createSql + ';';
|
|
757
|
+
}
|
|
758
|
+
return createSql + ';';
|
|
759
|
+
}
|
|
760
|
+
throw new Error(`Unsupported database client: ${client}`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Convert backtick-quoted identifiers to double-quoted identifiers
|
|
764
|
+
* Only replaces backticks that are identifier quotes, not those inside string literals
|
|
765
|
+
*/
|
|
766
|
+
function convertBackticksToDoubleQuotes(sql) {
|
|
767
|
+
// Match backtick-quoted identifiers: `identifier`
|
|
768
|
+
// Only matches word characters, dots, hyphens, and underscores (valid identifier chars)
|
|
769
|
+
return sql.replace(/`([a-zA-Z0-9_\.\-]+)`/g, '"$1"');
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Format a value for SQL insertion
|
|
773
|
+
*/
|
|
774
|
+
export function formatValue(value, format, table, column, columnType) {
|
|
775
|
+
// Handle NULL
|
|
776
|
+
if (value === null || value === undefined) {
|
|
777
|
+
return 'NULL';
|
|
778
|
+
}
|
|
779
|
+
// Special case: knex_migrations.migration_time
|
|
780
|
+
// Convert Unix timestamp (milliseconds) to datetime/timestamp string
|
|
781
|
+
if (table === 'knex_migrations' && column === 'migration_time' && typeof value === 'number') {
|
|
782
|
+
if (format === 'mysql') {
|
|
783
|
+
const date = new Date(value);
|
|
784
|
+
const year = date.getFullYear();
|
|
785
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
786
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
787
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
788
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
789
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
790
|
+
return `'${year}-${month}-${day} ${hours}:${minutes}:${seconds}'`;
|
|
791
|
+
}
|
|
792
|
+
else if (format === 'postgresql') {
|
|
793
|
+
// PostgreSQL: Use to_timestamp() function
|
|
794
|
+
return `to_timestamp(${value / 1000})`; // Convert milliseconds to seconds
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Handle numbers
|
|
798
|
+
if (typeof value === 'number') {
|
|
799
|
+
// Special case: PostgreSQL boolean columns stored as 0/1 in SQLite
|
|
800
|
+
if (format === 'postgresql' && columnType === 'boolean') {
|
|
801
|
+
return value === 1 ? 'TRUE' : 'FALSE';
|
|
802
|
+
}
|
|
803
|
+
return String(value);
|
|
804
|
+
}
|
|
805
|
+
// Handle booleans
|
|
806
|
+
if (typeof value === 'boolean') {
|
|
807
|
+
if (format === 'postgresql') {
|
|
808
|
+
return value ? 'TRUE' : 'FALSE';
|
|
809
|
+
}
|
|
810
|
+
// MySQL and SQLite use 0/1
|
|
811
|
+
return value ? '1' : '0';
|
|
812
|
+
}
|
|
813
|
+
// Handle Buffer (binary data)
|
|
814
|
+
if (Buffer.isBuffer(value)) {
|
|
815
|
+
if (format === 'postgresql') {
|
|
816
|
+
// PostgreSQL bytea hex format
|
|
817
|
+
return `'\\x${value.toString('hex')}'::bytea`;
|
|
818
|
+
}
|
|
819
|
+
// MySQL and SQLite hex format
|
|
820
|
+
return `X'${value.toString('hex')}'`;
|
|
821
|
+
}
|
|
822
|
+
// Handle strings
|
|
823
|
+
if (typeof value === 'string') {
|
|
824
|
+
// Escape single quotes by doubling them
|
|
825
|
+
const escaped = value.replace(/'/g, "''");
|
|
826
|
+
// Also escape backslashes for MySQL
|
|
827
|
+
const finalEscaped = format === 'mysql' ? escaped.replace(/\\/g, '\\\\') : escaped;
|
|
828
|
+
return `'${finalEscaped}'`;
|
|
829
|
+
}
|
|
830
|
+
// Handle objects/arrays (JSON)
|
|
831
|
+
if (typeof value === 'object') {
|
|
832
|
+
const jsonStr = JSON.stringify(value).replace(/'/g, "''");
|
|
833
|
+
return `'${jsonStr}'`;
|
|
834
|
+
}
|
|
835
|
+
// Fallback
|
|
836
|
+
return 'NULL';
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Convert value with type-aware conversion for cross-database migration
|
|
840
|
+
* Uses Knex columnInfo() metadata for accurate type detection
|
|
841
|
+
*
|
|
842
|
+
* @internal - Exported for testing only
|
|
843
|
+
*/
|
|
844
|
+
export function convertValueWithType(value, columnName, columnInfo, // From knex(table).columnInfo()
|
|
845
|
+
sourceFormat, targetFormat) {
|
|
846
|
+
// Handle NULL
|
|
847
|
+
if (value === null || value === undefined) {
|
|
848
|
+
return 'NULL';
|
|
849
|
+
}
|
|
850
|
+
const colMeta = columnInfo.get(columnName);
|
|
851
|
+
if (!colMeta) {
|
|
852
|
+
// Fallback to basic formatValue
|
|
853
|
+
return formatValue(value, targetFormat);
|
|
854
|
+
}
|
|
855
|
+
const colType = (colMeta.type || '').toLowerCase();
|
|
856
|
+
// Boolean conversion - enhanced detection
|
|
857
|
+
// Knex columnInfo types: 'boolean' (PostgreSQL), 'tinyint' (MySQL), 'integer' (SQLite boolean stored as 0/1)
|
|
858
|
+
const isBooleanColumn = colType.includes('bool') ||
|
|
859
|
+
colType === 'tinyint' ||
|
|
860
|
+
colType === 'bit' ||
|
|
861
|
+
colMeta.type === 'boolean' ||
|
|
862
|
+
// Additional heuristic: maxLength === 1 for tinyint(1) in MySQL
|
|
863
|
+
(colType === 'integer' && colMeta.maxLength === 1);
|
|
864
|
+
if (isBooleanColumn) {
|
|
865
|
+
// Normalize value to boolean
|
|
866
|
+
const boolValue = Boolean(value);
|
|
867
|
+
if (targetFormat === 'postgresql') {
|
|
868
|
+
return boolValue ? 'TRUE' : 'FALSE';
|
|
869
|
+
}
|
|
870
|
+
// SQLite and MySQL use 0/1
|
|
871
|
+
return boolValue ? '1' : '0';
|
|
872
|
+
}
|
|
873
|
+
// Timestamp/DateTime conversion - enhanced with columnInfo metadata
|
|
874
|
+
const isTimestampColumn = colType.includes('timestamp') ||
|
|
875
|
+
colType.includes('datetime') ||
|
|
876
|
+
colType.includes('date') ||
|
|
877
|
+
colType === 'time';
|
|
878
|
+
if (isTimestampColumn) {
|
|
879
|
+
if (typeof value === 'number') {
|
|
880
|
+
// Unix timestamp - check if milliseconds or seconds based on magnitude
|
|
881
|
+
const timestamp = value > 10000000000 ? value : value * 1000;
|
|
882
|
+
const date = new Date(timestamp);
|
|
883
|
+
// ISO 8601 format: YYYY-MM-DD HH:MM:SS
|
|
884
|
+
const year = date.getUTCFullYear();
|
|
885
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
886
|
+
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
887
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
888
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
889
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
890
|
+
const isoString = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
891
|
+
if (targetFormat === 'postgresql') {
|
|
892
|
+
return `'${isoString}'::timestamp`;
|
|
893
|
+
}
|
|
894
|
+
else if (targetFormat === 'mysql') {
|
|
895
|
+
return `'${isoString}'`;
|
|
896
|
+
}
|
|
897
|
+
return `'${isoString}'`;
|
|
898
|
+
}
|
|
899
|
+
else if (typeof value === 'string') {
|
|
900
|
+
// Already formatted string - ensure proper escaping
|
|
901
|
+
const escaped = value.replace(/'/g, "''");
|
|
902
|
+
if (targetFormat === 'postgresql') {
|
|
903
|
+
return `'${escaped}'::timestamp`;
|
|
904
|
+
}
|
|
905
|
+
return `'${escaped}'`;
|
|
906
|
+
}
|
|
907
|
+
else if (value instanceof Date) {
|
|
908
|
+
// Date object
|
|
909
|
+
const year = value.getUTCFullYear();
|
|
910
|
+
const month = String(value.getUTCMonth() + 1).padStart(2, '0');
|
|
911
|
+
const day = String(value.getUTCDate()).padStart(2, '0');
|
|
912
|
+
const hours = String(value.getUTCHours()).padStart(2, '0');
|
|
913
|
+
const minutes = String(value.getUTCMinutes()).padStart(2, '0');
|
|
914
|
+
const seconds = String(value.getUTCSeconds()).padStart(2, '0');
|
|
915
|
+
const isoString = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
916
|
+
if (targetFormat === 'postgresql') {
|
|
917
|
+
return `'${isoString}'::timestamp`;
|
|
918
|
+
}
|
|
919
|
+
return `'${isoString}'`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
// Binary/Buffer handling - enhanced with proper encoding
|
|
923
|
+
const isBinaryColumn = colType.includes('blob') ||
|
|
924
|
+
colType.includes('bytea') ||
|
|
925
|
+
colType.includes('binary') ||
|
|
926
|
+
colType.includes('varbinary');
|
|
927
|
+
if (Buffer.isBuffer(value) || isBinaryColumn) {
|
|
928
|
+
const bufferValue = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
929
|
+
const hexString = bufferValue.toString('hex');
|
|
930
|
+
if (targetFormat === 'postgresql') {
|
|
931
|
+
// PostgreSQL bytea hex format: '\x...'::bytea
|
|
932
|
+
return `'\\x${hexString}'::bytea`;
|
|
933
|
+
}
|
|
934
|
+
else if (targetFormat === 'mysql') {
|
|
935
|
+
// MySQL binary hex format: X'...' or 0x...
|
|
936
|
+
return `X'${hexString}'`;
|
|
937
|
+
}
|
|
938
|
+
// SQLite hex format
|
|
939
|
+
return `X'${hexString}'`;
|
|
940
|
+
}
|
|
941
|
+
// JSON handling - enhanced with proper type casting
|
|
942
|
+
const isJsonColumn = colType.includes('json') ||
|
|
943
|
+
colType === 'jsonb';
|
|
944
|
+
if (isJsonColumn) {
|
|
945
|
+
let jsonStr;
|
|
946
|
+
if (typeof value === 'string') {
|
|
947
|
+
// Already stringified by Knex - validate and escape
|
|
948
|
+
try {
|
|
949
|
+
JSON.parse(value); // Validate
|
|
950
|
+
jsonStr = value.replace(/'/g, "''");
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// Invalid JSON string - treat as regular string
|
|
954
|
+
jsonStr = JSON.stringify(value).replace(/'/g, "''");
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
else if (typeof value === 'object') {
|
|
958
|
+
// Object that needs stringification
|
|
959
|
+
jsonStr = JSON.stringify(value).replace(/'/g, "''");
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
// Primitive value - stringify
|
|
963
|
+
jsonStr = JSON.stringify(value).replace(/'/g, "''");
|
|
964
|
+
}
|
|
965
|
+
if (targetFormat === 'postgresql') {
|
|
966
|
+
// Use JSONB for better performance
|
|
967
|
+
return `'${jsonStr}'::jsonb`;
|
|
968
|
+
}
|
|
969
|
+
else if (targetFormat === 'mysql') {
|
|
970
|
+
// MySQL 5.7+ JSON type
|
|
971
|
+
return `'${jsonStr}'`;
|
|
972
|
+
}
|
|
973
|
+
// SQLite stores JSON as TEXT
|
|
974
|
+
return `'${jsonStr}'`;
|
|
975
|
+
}
|
|
976
|
+
// PostgreSQL Arrays - enhanced detection
|
|
977
|
+
const isArrayColumn = colType.includes('array') || colType.includes('[]');
|
|
978
|
+
if ((isArrayColumn || Array.isArray(value)) && targetFormat === 'postgresql') {
|
|
979
|
+
if (Array.isArray(value)) {
|
|
980
|
+
// Convert array elements recursively
|
|
981
|
+
const arrayStr = value
|
|
982
|
+
.map(v => {
|
|
983
|
+
if (v === null || v === undefined)
|
|
984
|
+
return 'NULL';
|
|
985
|
+
if (typeof v === 'string') {
|
|
986
|
+
const escaped = v.replace(/'/g, "''").replace(/\\/g, '\\\\');
|
|
987
|
+
return `'${escaped}'`;
|
|
988
|
+
}
|
|
989
|
+
if (typeof v === 'number')
|
|
990
|
+
return String(v);
|
|
991
|
+
if (typeof v === 'boolean')
|
|
992
|
+
return v ? 'TRUE' : 'FALSE';
|
|
993
|
+
// Objects - stringify
|
|
994
|
+
return `'${JSON.stringify(v).replace(/'/g, "''")}'`;
|
|
995
|
+
})
|
|
996
|
+
.join(',');
|
|
997
|
+
return `ARRAY[${arrayStr}]`;
|
|
998
|
+
}
|
|
999
|
+
else if (typeof value === 'string') {
|
|
1000
|
+
// Already formatted array string - pass through
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
// PostgreSQL Enum types
|
|
1005
|
+
const isEnumColumn = colType === 'enum' || colType.includes('user-defined');
|
|
1006
|
+
if (isEnumColumn && targetFormat === 'postgresql') {
|
|
1007
|
+
// Enum values must be quoted strings
|
|
1008
|
+
const escaped = String(value).replace(/'/g, "''");
|
|
1009
|
+
return `'${escaped}'`;
|
|
1010
|
+
}
|
|
1011
|
+
// Text columns with object values (fallback)
|
|
1012
|
+
if (colType === 'text' && typeof value === 'object' && !Buffer.isBuffer(value)) {
|
|
1013
|
+
const jsonStr = JSON.stringify(value).replace(/'/g, "''");
|
|
1014
|
+
return `'${jsonStr}'`;
|
|
1015
|
+
}
|
|
1016
|
+
// Numeric types - ensure no quotes
|
|
1017
|
+
const isNumericColumn = colType.includes('int') ||
|
|
1018
|
+
colType.includes('decimal') ||
|
|
1019
|
+
colType.includes('numeric') ||
|
|
1020
|
+
colType.includes('real') ||
|
|
1021
|
+
colType.includes('float') ||
|
|
1022
|
+
colType.includes('double');
|
|
1023
|
+
if (isNumericColumn && typeof value === 'number') {
|
|
1024
|
+
return String(value);
|
|
1025
|
+
}
|
|
1026
|
+
// Fallback to basic formatValue
|
|
1027
|
+
return formatValue(value, targetFormat);
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Extract column names from INSERT SQL statement
|
|
1031
|
+
* E.g., 'insert into "m_agents" ("id", "name", "is_reusable") values ...' => ["id", "name", "is_reusable"]
|
|
1032
|
+
*/
|
|
1033
|
+
function extractColumnNamesFromInsertSql(sql) {
|
|
1034
|
+
// Match: insert into "table" ("col1", "col2", "col3") values
|
|
1035
|
+
// Or: insert into `table` (`col1`, `col2`) values
|
|
1036
|
+
// Or: INSERT INTO table (col1, col2) VALUES
|
|
1037
|
+
const match = sql.match(/insert\s+(?:ignore\s+)?into\s+[`"]?\w+[`"]?\s*\((.*?)\)\s*values/i);
|
|
1038
|
+
if (!match) {
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
const columnsPart = match[1];
|
|
1042
|
+
// Split by comma and extract column names (removing quotes and whitespace)
|
|
1043
|
+
return columnsPart
|
|
1044
|
+
.split(',')
|
|
1045
|
+
.map(col => col.trim().replace(/[`"]/g, ''));
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Embed bindings from Knex parameterized query into plain SQL
|
|
1049
|
+
* with optional type-aware conversion for cross-database migrations
|
|
1050
|
+
*/
|
|
1051
|
+
function embedBindings(sql, bindings, format, columnTypes, columnNames, columnInfo // NEW: columnInfo from knex(table).columnInfo()
|
|
1052
|
+
) {
|
|
1053
|
+
// Detect source format (for now, assume SQLite as source)
|
|
1054
|
+
// TODO: Make this configurable or detect from the knex instance
|
|
1055
|
+
const sourceFormat = 'sqlite';
|
|
1056
|
+
if (format === 'postgresql') {
|
|
1057
|
+
// PostgreSQL: $1, $2, ... (replace in reverse order to avoid $10 matching $1)
|
|
1058
|
+
let result = sql;
|
|
1059
|
+
for (let i = bindings.length; i >= 1; i--) {
|
|
1060
|
+
const placeholder = `$${i}`;
|
|
1061
|
+
// For multi-row inserts, column names repeat: $1-$5 map to cols 0-4, $6-$10 map to cols 0-4, etc.
|
|
1062
|
+
const columnIndex = columnNames && columnNames.length > 0 ? (i - 1) % columnNames.length : i - 1;
|
|
1063
|
+
const columnName = columnNames?.[columnIndex] || '';
|
|
1064
|
+
// Use new convertValueWithType with columnInfo if available
|
|
1065
|
+
const value = columnInfo
|
|
1066
|
+
? convertValueWithType(bindings[i - 1], columnName, columnInfo, sourceFormat, format)
|
|
1067
|
+
: formatValue(bindings[i - 1], format);
|
|
1068
|
+
result = result.replace(new RegExp(`\\${placeholder}\\b`, 'g'), value);
|
|
1069
|
+
}
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
// MySQL/SQLite: ? placeholders
|
|
1074
|
+
let result = sql;
|
|
1075
|
+
let bindingIndex = 0;
|
|
1076
|
+
result = result.replace(/\?/g, () => {
|
|
1077
|
+
if (bindingIndex >= bindings.length) {
|
|
1078
|
+
throw new Error(`Not enough bindings: ${bindings.length} provided, more needed`);
|
|
1079
|
+
}
|
|
1080
|
+
// For multi-row inserts, column names repeat
|
|
1081
|
+
const columnIndex = columnNames && columnNames.length > 0 ? bindingIndex % columnNames.length : bindingIndex;
|
|
1082
|
+
const columnName = columnNames?.[columnIndex] || '';
|
|
1083
|
+
// Use new convertValueWithType with columnInfo if available
|
|
1084
|
+
const value = columnInfo
|
|
1085
|
+
? convertValueWithType(bindings[bindingIndex], columnName, columnInfo, sourceFormat, format)
|
|
1086
|
+
: formatValue(bindings[bindingIndex], format);
|
|
1087
|
+
bindingIndex++;
|
|
1088
|
+
return value;
|
|
1089
|
+
});
|
|
1090
|
+
return result;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Create a throwaway Knex instance for SQL generation
|
|
1095
|
+
*/
|
|
1096
|
+
function createKnexForFormat(format) {
|
|
1097
|
+
const client = format === 'mysql' ? 'mysql2' : format === 'postgresql' ? 'pg' : 'better-sqlite3';
|
|
1098
|
+
return knex({
|
|
1099
|
+
client,
|
|
1100
|
+
connection: client === 'better-sqlite3' ? { filename: ':memory:' } : {},
|
|
1101
|
+
useNullAsDefault: client === 'better-sqlite3',
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Generate a bulk INSERT statement for a table with conflict resolution
|
|
1106
|
+
*
|
|
1107
|
+
* REFACTORED: Uses Knex query builder instead of manual string construction
|
|
1108
|
+
*/
|
|
1109
|
+
export function generateBulkInsert(table, rows, format, options = {}) {
|
|
1110
|
+
if (rows.length === 0) {
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
const { chunkSize = 100, conflictMode = 'error', primaryKeys = [], columnTypes, columnInfo, } = options;
|
|
1114
|
+
const statements = [];
|
|
1115
|
+
const targetKnex = createKnexForFormat(format);
|
|
1116
|
+
try {
|
|
1117
|
+
// Split into chunks to avoid too-large statements
|
|
1118
|
+
for (let i = 0; i < rows.length; i += chunkSize) {
|
|
1119
|
+
const chunk = rows.slice(i, i + chunkSize);
|
|
1120
|
+
// Use Knex to generate parameterized INSERT
|
|
1121
|
+
let builder = targetKnex(table).insert(chunk);
|
|
1122
|
+
// Handle conflict modes with Knex-specific methods
|
|
1123
|
+
if (conflictMode === 'ignore') {
|
|
1124
|
+
if (format === 'mysql') {
|
|
1125
|
+
// MySQL: INSERT IGNORE is handled via raw SQL modification
|
|
1126
|
+
const { sql, bindings } = builder.toSQL().toNative();
|
|
1127
|
+
const ignoreSql = sql.replace(/^insert into/i, 'INSERT IGNORE INTO');
|
|
1128
|
+
const columnNames = extractColumnNamesFromInsertSql(ignoreSql);
|
|
1129
|
+
const embedded = embedBindings(ignoreSql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1130
|
+
statements.push(embedded + ';');
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
else if (format === 'postgresql') {
|
|
1134
|
+
// PostgreSQL: ON CONFLICT DO NOTHING
|
|
1135
|
+
// Note: Knex's onConflict() requires specifying columns, so we use raw SQL
|
|
1136
|
+
const { sql, bindings } = builder.toSQL().toNative();
|
|
1137
|
+
const conflictSql = sql + ' ON CONFLICT DO NOTHING';
|
|
1138
|
+
const columnNames = extractColumnNamesFromInsertSql(conflictSql);
|
|
1139
|
+
const embedded = embedBindings(conflictSql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1140
|
+
statements.push(embedded + ';');
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
// SQLite: INSERT OR IGNORE
|
|
1145
|
+
const { sql, bindings } = builder.toSQL().toNative();
|
|
1146
|
+
const ignoreSql = sql.replace(/^insert into/i, 'INSERT OR IGNORE INTO');
|
|
1147
|
+
const columnNames = extractColumnNamesFromInsertSql(ignoreSql);
|
|
1148
|
+
const embedded = embedBindings(ignoreSql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1149
|
+
statements.push(embedded + ';');
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else if (conflictMode === 'replace') {
|
|
1154
|
+
// REPLACE mode requires primary keys
|
|
1155
|
+
if (primaryKeys.length === 0) {
|
|
1156
|
+
throw new Error(`Cannot use 'replace' mode for table ${table}: no primary key found`);
|
|
1157
|
+
}
|
|
1158
|
+
const { sql, bindings } = builder.toSQL().toNative();
|
|
1159
|
+
const columns = Object.keys(chunk[0]);
|
|
1160
|
+
const nonPkColumns = columns.filter(col => !primaryKeys.includes(col));
|
|
1161
|
+
const columnNames = extractColumnNamesFromInsertSql(sql);
|
|
1162
|
+
if (format === 'mysql') {
|
|
1163
|
+
// MySQL: ON DUPLICATE KEY UPDATE
|
|
1164
|
+
const updateClauses = nonPkColumns.map(col => `${quoteIdentifier(col, format)} = VALUES(${quoteIdentifier(col, format)})`);
|
|
1165
|
+
const finalSql = `${sql}\nON DUPLICATE KEY UPDATE\n ${updateClauses.join(',\n ')}`;
|
|
1166
|
+
const embedded = embedBindings(finalSql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1167
|
+
statements.push(embedded + ';');
|
|
1168
|
+
}
|
|
1169
|
+
else if (format === 'postgresql') {
|
|
1170
|
+
// PostgreSQL: ON CONFLICT DO UPDATE
|
|
1171
|
+
const quotedPks = primaryKeys.map(pk => quoteIdentifier(pk, format));
|
|
1172
|
+
const updateClauses = nonPkColumns.map(col => `${quoteIdentifier(col, format)} = EXCLUDED.${quoteIdentifier(col, format)}`);
|
|
1173
|
+
const finalSql = `${sql}\nON CONFLICT (${quotedPks.join(', ')}) DO UPDATE SET\n ${updateClauses.join(',\n ')}`;
|
|
1174
|
+
const embedded = embedBindings(finalSql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1175
|
+
statements.push(embedded + ';');
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
// SQLite: ON CONFLICT DO UPDATE
|
|
1179
|
+
const quotedPks = primaryKeys.map(pk => quoteIdentifier(pk, format));
|
|
1180
|
+
const updateClauses = nonPkColumns.map(col => `${quoteIdentifier(col, format)} = excluded.${quoteIdentifier(col, format)}`);
|
|
1181
|
+
const finalSql = `${sql}\nON CONFLICT (${quotedPks.join(', ')}) DO UPDATE SET\n ${updateClauses.join(',\n ')}`;
|
|
1182
|
+
const embedded = embedBindings(finalSql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1183
|
+
statements.push(embedded + ';');
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
// ERROR mode: Standard INSERT
|
|
1188
|
+
const { sql, bindings } = builder.toSQL().toNative();
|
|
1189
|
+
const columnNames = extractColumnNamesFromInsertSql(sql);
|
|
1190
|
+
const embedded = embedBindings(sql, bindings, format, columnTypes, columnNames, columnInfo);
|
|
1191
|
+
statements.push(embedded + ';');
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return statements;
|
|
1195
|
+
}
|
|
1196
|
+
finally {
|
|
1197
|
+
// Clean up Knex instance
|
|
1198
|
+
targetKnex.destroy();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Generate header comments for SQL dump
|
|
1203
|
+
*/
|
|
1204
|
+
export function generateHeader(format) {
|
|
1205
|
+
const timestamp = new Date().toISOString();
|
|
1206
|
+
return `-- SQL Dump generated by sqlew
|
|
1207
|
+
-- Date: ${timestamp}
|
|
1208
|
+
-- Target: ${format.toUpperCase()}
|
|
1209
|
+
--
|
|
1210
|
+
-- This dump is wrapped in a transaction.
|
|
1211
|
+
-- On error, all changes will be rolled back automatically.
|
|
1212
|
+
--
|
|
1213
|
+
-- Usage (empty database):
|
|
1214
|
+
-- ${format === 'mysql' ? 'mysql mydb < dump.sql' : format === 'postgresql' ? 'psql -d mydb -f dump.sql' : 'sqlite3 mydb.db < dump.sql'}
|
|
1215
|
+
|
|
1216
|
+
`;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Generate foreign key disable/enable statements
|
|
1220
|
+
*/
|
|
1221
|
+
export function generateForeignKeyControls(format, enable) {
|
|
1222
|
+
if (format === 'mysql') {
|
|
1223
|
+
return enable
|
|
1224
|
+
? 'SET FOREIGN_KEY_CHECKS=1;'
|
|
1225
|
+
: 'SET FOREIGN_KEY_CHECKS=0;';
|
|
1226
|
+
}
|
|
1227
|
+
else if (format === 'postgresql') {
|
|
1228
|
+
return enable
|
|
1229
|
+
? 'SET session_replication_role = DEFAULT;'
|
|
1230
|
+
: 'SET session_replication_role = replica;';
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
// SQLite
|
|
1234
|
+
return enable
|
|
1235
|
+
? 'PRAGMA foreign_keys = ON;'
|
|
1236
|
+
: 'PRAGMA foreign_keys = OFF;';
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Generate transaction control statements
|
|
1241
|
+
*/
|
|
1242
|
+
export function generateTransactionControl(format, isStart) {
|
|
1243
|
+
if (isStart) {
|
|
1244
|
+
return format === 'mysql' ? 'START TRANSACTION;'
|
|
1245
|
+
: format === 'postgresql' ? 'BEGIN;'
|
|
1246
|
+
: 'BEGIN TRANSACTION;';
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
return 'COMMIT;';
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Generate sequence reset statements for PostgreSQL
|
|
1254
|
+
*/
|
|
1255
|
+
export async function generateSequenceResets(knex, tables) {
|
|
1256
|
+
const statements = [];
|
|
1257
|
+
for (const table of tables) {
|
|
1258
|
+
try {
|
|
1259
|
+
// Check if table has an id column with a sequence
|
|
1260
|
+
const result = await knex.raw(`
|
|
1261
|
+
SELECT column_name, column_default
|
|
1262
|
+
FROM information_schema.columns
|
|
1263
|
+
WHERE table_name = ?
|
|
1264
|
+
AND column_default LIKE 'nextval%'
|
|
1265
|
+
`, [table]);
|
|
1266
|
+
if (result.rows.length > 0) {
|
|
1267
|
+
const columnName = result.rows[0].column_name;
|
|
1268
|
+
const sequenceName = `${table}_${columnName}_seq`;
|
|
1269
|
+
statements.push(`SELECT setval('${sequenceName}', COALESCE((SELECT MAX(${columnName}) FROM "${table}"), 1), true);`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
// Ignore errors for tables without sequences
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return statements;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Get foreign key dependencies for tables
|
|
1280
|
+
* Returns a map of table -> array of tables it depends on
|
|
1281
|
+
*
|
|
1282
|
+
* @internal Exported for testing purposes
|
|
1283
|
+
*/
|
|
1284
|
+
export async function getTableDependencies(knex, tables) {
|
|
1285
|
+
const dependencies = new Map();
|
|
1286
|
+
const client = knex.client.config.client;
|
|
1287
|
+
for (const table of tables) {
|
|
1288
|
+
dependencies.set(table, []);
|
|
1289
|
+
}
|
|
1290
|
+
for (const table of tables) {
|
|
1291
|
+
try {
|
|
1292
|
+
if (client === 'better-sqlite3' || client === 'sqlite3') {
|
|
1293
|
+
// SQLite: Use PRAGMA foreign_key_list() for reliable FK detection
|
|
1294
|
+
// This catches both inline REFERENCES and explicit FOREIGN KEY syntax
|
|
1295
|
+
const result = await knex.raw(`PRAGMA foreign_key_list(${table})`);
|
|
1296
|
+
const fkList = Array.isArray(result) ? result : [];
|
|
1297
|
+
for (const fk of fkList) {
|
|
1298
|
+
const referencedTable = fk.table;
|
|
1299
|
+
if (tables.includes(referencedTable) && referencedTable !== table) {
|
|
1300
|
+
dependencies.get(table).push(referencedTable);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
else if (client === 'mysql' || client === 'mysql2') {
|
|
1305
|
+
// MySQL: Use information_schema
|
|
1306
|
+
const result = await knex.raw(`
|
|
1307
|
+
SELECT REFERENCED_TABLE_NAME
|
|
1308
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
1309
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
1310
|
+
AND TABLE_NAME = ?
|
|
1311
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
1312
|
+
`, [table]);
|
|
1313
|
+
for (const row of result[0]) {
|
|
1314
|
+
const referencedTable = row.REFERENCED_TABLE_NAME;
|
|
1315
|
+
if (tables.includes(referencedTable) && referencedTable !== table) {
|
|
1316
|
+
dependencies.get(table).push(referencedTable);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
else if (client === 'pg') {
|
|
1321
|
+
// PostgreSQL: Use information_schema
|
|
1322
|
+
const result = await knex.raw(`
|
|
1323
|
+
SELECT DISTINCT ccu.table_name AS referenced_table
|
|
1324
|
+
FROM information_schema.table_constraints AS tc
|
|
1325
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
1326
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1327
|
+
AND tc.table_schema = kcu.table_schema
|
|
1328
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
1329
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
1330
|
+
AND ccu.table_schema = tc.table_schema
|
|
1331
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
1332
|
+
AND tc.table_name = ?
|
|
1333
|
+
`, [table]);
|
|
1334
|
+
for (const row of result.rows) {
|
|
1335
|
+
const referencedTable = row.referenced_table;
|
|
1336
|
+
if (tables.includes(referencedTable) && referencedTable !== table) {
|
|
1337
|
+
dependencies.get(table).push(referencedTable);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
catch (err) {
|
|
1343
|
+
// Ignore errors - table might not have foreign keys
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return dependencies;
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Topologically sort tables by foreign key dependencies
|
|
1350
|
+
* Returns tables in order where parent tables come before child tables
|
|
1351
|
+
*
|
|
1352
|
+
* @internal Exported for testing purposes
|
|
1353
|
+
*/
|
|
1354
|
+
export function topologicalSort(tables, dependencies) {
|
|
1355
|
+
const sorted = [];
|
|
1356
|
+
const visited = new Set();
|
|
1357
|
+
const visiting = new Set();
|
|
1358
|
+
function visit(table) {
|
|
1359
|
+
if (visited.has(table))
|
|
1360
|
+
return;
|
|
1361
|
+
if (visiting.has(table)) {
|
|
1362
|
+
// Circular dependency detected - log warning and continue
|
|
1363
|
+
debugLog('WARN', `Circular foreign key dependency detected involving table: ${table}`);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
visiting.add(table);
|
|
1367
|
+
const deps = dependencies.get(table) || [];
|
|
1368
|
+
for (const dep of deps) {
|
|
1369
|
+
visit(dep);
|
|
1370
|
+
}
|
|
1371
|
+
visiting.delete(table);
|
|
1372
|
+
visited.add(table);
|
|
1373
|
+
sorted.push(table);
|
|
1374
|
+
}
|
|
1375
|
+
for (const table of tables) {
|
|
1376
|
+
visit(table);
|
|
1377
|
+
}
|
|
1378
|
+
return sorted;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Main function to generate complete SQL dump
|
|
1382
|
+
*/
|
|
1383
|
+
export async function generateSqlDump(knex, format, options = {}) {
|
|
1384
|
+
const { tables: requestedTables, includeHeader = true, includeSchema = true, // Default to TRUE - include schema
|
|
1385
|
+
chunkSize = 100, conflictMode = 'error' } = options;
|
|
1386
|
+
const statements = [];
|
|
1387
|
+
// Add header
|
|
1388
|
+
if (includeHeader) {
|
|
1389
|
+
statements.push(generateHeader(format));
|
|
1390
|
+
}
|
|
1391
|
+
// Disable foreign key checks
|
|
1392
|
+
statements.push(generateForeignKeyControls(format, false));
|
|
1393
|
+
statements.push('');
|
|
1394
|
+
// Start transaction
|
|
1395
|
+
statements.push(generateTransactionControl(format, true));
|
|
1396
|
+
statements.push('');
|
|
1397
|
+
// Get tables to dump
|
|
1398
|
+
// Include knex_migrations table when schema is included for complete migration state
|
|
1399
|
+
const allTables = await getAllTables(knex, includeSchema);
|
|
1400
|
+
const tablesToDump = requestedTables
|
|
1401
|
+
? allTables.filter(t => requestedTables.includes(t))
|
|
1402
|
+
: allTables;
|
|
1403
|
+
// Sort tables by foreign key dependencies for PostgreSQL compatibility
|
|
1404
|
+
// Parent tables must be created before child tables
|
|
1405
|
+
const dependencies = await getTableDependencies(knex, tablesToDump);
|
|
1406
|
+
const sortedTables = topologicalSort(tablesToDump, dependencies);
|
|
1407
|
+
// Generate CREATE TABLE statements if schema is included
|
|
1408
|
+
if (includeSchema) {
|
|
1409
|
+
statements.push('-- ============================================');
|
|
1410
|
+
statements.push('-- Schema (CREATE TABLE statements)');
|
|
1411
|
+
statements.push('-- ============================================');
|
|
1412
|
+
statements.push('');
|
|
1413
|
+
for (const table of sortedTables) {
|
|
1414
|
+
try {
|
|
1415
|
+
const createSql = await getCreateTableStatement(knex, table, format);
|
|
1416
|
+
statements.push(`-- Table: ${table}`);
|
|
1417
|
+
statements.push(createSql);
|
|
1418
|
+
statements.push('');
|
|
1419
|
+
}
|
|
1420
|
+
catch (err) {
|
|
1421
|
+
console.warn(`Warning: Could not get CREATE TABLE for ${table}:`, err);
|
|
1422
|
+
statements.push(`-- Warning: Could not create table ${table}`);
|
|
1423
|
+
statements.push('');
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
// Generate CREATE VIEW statements
|
|
1427
|
+
try {
|
|
1428
|
+
const views = await getAllViews(knex);
|
|
1429
|
+
if (views.length > 0) {
|
|
1430
|
+
statements.push('-- ============================================');
|
|
1431
|
+
statements.push('-- Views');
|
|
1432
|
+
statements.push('-- ============================================');
|
|
1433
|
+
statements.push('');
|
|
1434
|
+
for (const view of views) {
|
|
1435
|
+
try {
|
|
1436
|
+
const createViewSql = await getCreateViewStatement(knex, view, format);
|
|
1437
|
+
statements.push(`-- View: ${view}`);
|
|
1438
|
+
statements.push(createViewSql);
|
|
1439
|
+
statements.push('');
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
console.warn(`Warning: Could not get CREATE VIEW for ${view}:`, err);
|
|
1443
|
+
statements.push(`-- Warning: Could not create view ${view}`);
|
|
1444
|
+
statements.push('');
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
catch (err) {
|
|
1450
|
+
console.warn('Warning: Could not retrieve views:', err);
|
|
1451
|
+
}
|
|
1452
|
+
// Generate CREATE INDEX statements
|
|
1453
|
+
try {
|
|
1454
|
+
const indexStatements = [];
|
|
1455
|
+
for (const table of sortedTables) {
|
|
1456
|
+
const indexes = await getAllIndexes(knex, table);
|
|
1457
|
+
if (indexes.length > 0) {
|
|
1458
|
+
for (const indexName of indexes) {
|
|
1459
|
+
try {
|
|
1460
|
+
const createIndexSql = await getCreateIndexStatement(knex, indexName, format);
|
|
1461
|
+
indexStatements.push(`-- Index: ${indexName} on ${table}`);
|
|
1462
|
+
indexStatements.push(createIndexSql);
|
|
1463
|
+
indexStatements.push('');
|
|
1464
|
+
}
|
|
1465
|
+
catch (err) {
|
|
1466
|
+
console.warn(`Warning: Could not get CREATE INDEX for ${indexName}:`, err);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (indexStatements.length > 0) {
|
|
1472
|
+
statements.push('-- ============================================');
|
|
1473
|
+
statements.push('-- Indexes');
|
|
1474
|
+
statements.push('-- ============================================');
|
|
1475
|
+
statements.push('');
|
|
1476
|
+
statements.push(...indexStatements);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
catch (err) {
|
|
1480
|
+
console.warn('Warning: Could not retrieve indexes:', err);
|
|
1481
|
+
}
|
|
1482
|
+
statements.push('-- ============================================');
|
|
1483
|
+
statements.push('-- Data (INSERT statements)');
|
|
1484
|
+
statements.push('-- ============================================');
|
|
1485
|
+
statements.push('');
|
|
1486
|
+
}
|
|
1487
|
+
// Generate INSERT statements for each table (in dependency order)
|
|
1488
|
+
// Skip data insertion if chunkSize is 0 (schema-only mode)
|
|
1489
|
+
if (chunkSize > 0) {
|
|
1490
|
+
for (const table of sortedTables) {
|
|
1491
|
+
statements.push(`-- Data for table: ${table}`);
|
|
1492
|
+
const rows = await knex(table).select('*');
|
|
1493
|
+
if (rows.length === 0) {
|
|
1494
|
+
statements.push(`-- No data in table ${table}`);
|
|
1495
|
+
statements.push('');
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
// Get column types for cross-database type conversion
|
|
1499
|
+
// Query from source database to understand original types (e.g., INTEGER for booleans in SQLite)
|
|
1500
|
+
let columnTypes = new Map();
|
|
1501
|
+
let columnInfoMap = new Map();
|
|
1502
|
+
try {
|
|
1503
|
+
const columnInfo = await knex(table).columnInfo();
|
|
1504
|
+
for (const [col, info] of Object.entries(columnInfo)) {
|
|
1505
|
+
columnTypes.set(col, info.type);
|
|
1506
|
+
columnInfoMap.set(col, info); // Store full column metadata
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
catch (err) {
|
|
1510
|
+
console.warn(`Warning: Could not get column types for table ${table}:`, err);
|
|
1511
|
+
}
|
|
1512
|
+
// Get primary keys if using replace mode
|
|
1513
|
+
let primaryKeys = [];
|
|
1514
|
+
if (conflictMode === 'replace') {
|
|
1515
|
+
try {
|
|
1516
|
+
primaryKeys = await getPrimaryKeyColumns(knex, table);
|
|
1517
|
+
if (primaryKeys.length > 0) {
|
|
1518
|
+
statements.push(`-- Primary key(s): ${primaryKeys.join(', ')}`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
catch (err) {
|
|
1522
|
+
console.warn(`Warning: Could not detect primary key for table ${table}:`, err);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const inserts = generateBulkInsert(table, rows, format, {
|
|
1526
|
+
chunkSize,
|
|
1527
|
+
conflictMode,
|
|
1528
|
+
primaryKeys,
|
|
1529
|
+
columnTypes,
|
|
1530
|
+
columnInfo: columnInfoMap // Pass full columnInfo for type-aware conversion
|
|
1531
|
+
});
|
|
1532
|
+
statements.push(...inserts);
|
|
1533
|
+
statements.push('');
|
|
1534
|
+
}
|
|
1535
|
+
} // End of if (chunkSize > 0)
|
|
1536
|
+
// Reset sequences for PostgreSQL
|
|
1537
|
+
if (format === 'postgresql') {
|
|
1538
|
+
statements.push('-- Reset sequences');
|
|
1539
|
+
const sequenceResets = await generateSequenceResets(knex, tablesToDump);
|
|
1540
|
+
statements.push(...sequenceResets);
|
|
1541
|
+
statements.push('');
|
|
1542
|
+
}
|
|
1543
|
+
// Commit transaction
|
|
1544
|
+
statements.push(generateTransactionControl(format, false));
|
|
1545
|
+
statements.push('');
|
|
1546
|
+
// Re-enable foreign key checks
|
|
1547
|
+
statements.push(generateForeignKeyControls(format, true));
|
|
1548
|
+
return statements.join('\n');
|
|
1549
|
+
}
|
|
1550
|
+
//# sourceMappingURL=sql-dump.js.map
|