rubydb 0.1.3 → 0.1.5
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.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
- data/.github/workflows/benchmark.yml +26 -26
- data/.github/workflows/compatibility.yml +63 -63
- data/.github/workflows/fuzz.yml +33 -33
- data/.github/workflows/lint.yml +21 -21
- data/.github/workflows/operations.yml +24 -24
- data/.github/workflows/production-validation.yml +111 -111
- data/.github/workflows/release.yml +77 -77
- data/.github/workflows/security.yml +39 -37
- data/.github/workflows/test.yml +26 -26
- data/.github/workflows/workload.yml +58 -58
- data/.gitignore +12 -5
- data/.rubocop.yml +50 -44
- data/.standard.yml +9 -14
- data/ARCHITECTURE.md +21 -21
- data/CHANGELOG.md +49 -27
- data/CODE_OF_CONDUCT.md +13 -13
- data/CONTRIBUTING.md +29 -29
- data/GOVERNANCE.md +16 -16
- data/Gemfile +18 -17
- data/Gemfile.lock +125 -71
- data/README.md +249 -162
- data/ROADMAP.md +27 -27
- data/Rakefile +71 -71
- data/SECURITY.md +54 -54
- data/SUPPORT.md +14 -14
- data/adapters/activerecord/Gemfile +11 -11
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +877 -880
- data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
- data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +155 -143
- data/adapters/ruby/README.md +18 -18
- data/adapters/sequel/README.md +11 -11
- data/config/monitoring/prometheus-alerts.yml +39 -39
- data/config/production.yml +36 -36
- data/docs/README.md +75 -71
- data/docs/architecture/concurrency.md +14 -14
- data/docs/architecture/current-state.md +125 -125
- data/docs/architecture/execution-engine.md +25 -25
- data/docs/architecture/indexes.md +19 -19
- data/docs/architecture/mvcc.md +19 -19
- data/docs/architecture/overview.md +13 -13
- data/docs/architecture/pages.md +11 -11
- data/docs/architecture/production-roadmap.md +82 -82
- data/docs/architecture/query-planner.md +20 -20
- data/docs/architecture/recovery.md +18 -18
- data/docs/architecture/sql-engine.md +12 -12
- data/docs/architecture/storage-engine.md +14 -14
- data/docs/architecture/transactions.md +10 -10
- data/docs/architecture/wal.md +28 -28
- data/docs/cli-cheatsheet.md +98 -98
- data/docs/cli.md +275 -275
- data/docs/contributing/architecture.md +9 -9
- data/docs/contributing/benchmarking.md +14 -14
- data/docs/contributing/development.md +16 -16
- data/docs/contributing/release-process.md +49 -49
- data/docs/contributing/testing.md +16 -16
- data/docs/debugging.md +229 -229
- data/docs/developer/branching.md +10 -10
- data/docs/developer/database-diff.md +10 -10
- data/docs/developer/local-development.md +17 -17
- data/docs/developer/snapshots.md +9 -9
- data/docs/developer/temporal-data.md +10 -10
- data/docs/developer-guide.md +297 -297
- data/docs/getting-started/first-database.md +16 -16
- data/docs/getting-started/first-query.md +13 -13
- data/docs/getting-started/installation.md +19 -19
- data/docs/getting-started/local-to-production.md +300 -300
- data/docs/getting-started/quickstart.md +17 -17
- data/docs/getting-started/rails.md +16 -16
- data/docs/hardening_backlog.md +93 -93
- data/docs/lessons-learned.md +112 -112
- data/docs/operations/backups.md +33 -33
- data/docs/operations/disaster-recovery.md +31 -31
- data/docs/operations/failover.md +30 -30
- data/docs/operations/monitoring.md +25 -25
- data/docs/operations/production-guide.md +295 -295
- data/docs/operations/production-runbook.md +45 -45
- data/docs/operations/replication.md +33 -33
- data/docs/operations/restore.md +6 -6
- data/docs/operations/runbook.md +34 -34
- data/docs/operations/upgrades.md +14 -14
- data/docs/operations/workload-testing.md +17 -17
- data/docs/production-readiness.md +118 -118
- data/docs/production_validation.md +150 -150
- data/docs/rails/active-record.md +11 -11
- data/docs/rails/compatibility-guide.md +90 -90
- data/docs/rails/database-yml.md +92 -92
- data/docs/rails/installation.md +17 -17
- data/docs/rails/migrations.md +17 -17
- data/docs/rails/production.md +82 -82
- data/docs/rails/troubleshooting.md +18 -18
- data/docs/release.md +25 -25
- data/docs/server/architecture.md +10 -10
- data/docs/server/authentication.md +10 -10
- data/docs/server/configuration.md +16 -16
- data/docs/server/connection-pooling.md +10 -10
- data/docs/server/deployment.md +10 -10
- data/docs/server/protocol.md +12 -12
- data/docs/sql/compatibility-guide.md +82 -82
- data/docs/sql/compatibility.md +39 -39
- data/docs/sql/data-types.md +10 -10
- data/docs/sql/functions.md +9 -9
- data/docs/sql/joins.md +9 -9
- data/docs/sql/operators.md +9 -9
- data/docs/sql/sqlite-compatibility.md +21 -21
- data/docs/sql/syntax.md +10 -10
- data/docs/sql/transactions.md +10 -10
- data/docs/troubleshooting.md +244 -244
- data/lessons/01-foundations.md +73 -0
- data/lessons/02-local-development.md +121 -0
- data/lessons/03-embedded-rubydb.md +99 -0
- data/lessons/04-rails-complex-apps.md +138 -0
- data/lessons/05-rubydb-production-server.md +237 -0
- data/lessons/06-postgresql-massive-apps.md +96 -0
- data/lessons/07-hybrid-microservices.md +94 -0
- data/lessons/08-migrations-backups-recovery.md +86 -0
- data/lessons/09-observability-security-scale.md +87 -0
- data/lessons/10-release-readiness.md +125 -0
- data/lib/rubydb/backup/archive.rb +332 -334
- data/lib/rubydb/backup/backup.rb +400 -401
- data/lib/rubydb/backup/incremental.rb +349 -353
- data/lib/rubydb/backup/restore.rb +289 -290
- data/lib/rubydb/backup/snapshot.rb +265 -267
- data/lib/rubydb/backup/verification.rb +276 -279
- data/lib/rubydb/branching/branch.rb +181 -181
- data/lib/rubydb/branching/branch_manager.rb +307 -311
- data/lib/rubydb/branching/branch_metadata.rb +140 -140
- data/lib/rubydb/branching/checkout.rb +165 -166
- data/lib/rubydb/branching/copy_on_write.rb +272 -272
- data/lib/rubydb/branching/diff.rb +137 -138
- data/lib/rubydb/branching/merge.rb +282 -285
- data/lib/rubydb/build_info.rb +15 -15
- data/lib/rubydb/catalog/catalog.rb +391 -391
- data/lib/rubydb/catalog/column.rb +112 -112
- data/lib/rubydb/catalog/constraint.rb +180 -180
- data/lib/rubydb/catalog/database.rb +184 -184
- data/lib/rubydb/catalog/index.rb +97 -97
- data/lib/rubydb/catalog/schema.rb +103 -103
- data/lib/rubydb/catalog/sequence.rb +90 -90
- data/lib/rubydb/catalog/system_catalog.rb +698 -698
- data/lib/rubydb/catalog/table.rb +178 -178
- data/lib/rubydb/catalog/trigger.rb +102 -102
- data/lib/rubydb/catalog/view.rb +66 -66
- data/lib/rubydb/cli/application.rb +163 -163
- data/lib/rubydb/cli/commands/backup.rb +80 -81
- data/lib/rubydb/cli/commands/branch.rb +72 -72
- data/lib/rubydb/cli/commands/checkout.rb +54 -54
- data/lib/rubydb/cli/commands/create.rb +58 -58
- data/lib/rubydb/cli/commands/diff.rb +76 -77
- data/lib/rubydb/cli/commands/doctor.rb +74 -74
- data/lib/rubydb/cli/commands/drop.rb +57 -57
- data/lib/rubydb/cli/commands/init.rb +101 -102
- data/lib/rubydb/cli/commands/inspect.rb +95 -95
- data/lib/rubydb/cli/commands/merge.rb +63 -63
- data/lib/rubydb/cli/commands/migrate.rb +62 -62
- data/lib/rubydb/cli/commands/restart.rb +42 -39
- data/lib/rubydb/cli/commands/restore.rb +121 -121
- data/lib/rubydb/cli/commands/shell.rb +365 -365
- data/lib/rubydb/cli/commands/snapshot.rb +79 -79
- data/lib/rubydb/cli/commands/start.rb +88 -82
- data/lib/rubydb/cli/commands/status.rb +96 -92
- data/lib/rubydb/cli/commands/stop.rb +47 -47
- data/lib/rubydb/cli/commands/vacuum.rb +58 -58
- data/lib/rubydb/cli/formatter.rb +221 -221
- data/lib/rubydb/cli/output.rb +168 -168
- data/lib/rubydb/client/client.rb +302 -304
- data/lib/rubydb/client/connection.rb +414 -415
- data/lib/rubydb/client/connection_pool.rb +168 -168
- data/lib/rubydb/client/connection_url.rb +96 -96
- data/lib/rubydb/client/prepared_statement.rb +60 -60
- data/lib/rubydb/client/result.rb +123 -123
- data/lib/rubydb/client/statement.rb +52 -52
- data/lib/rubydb/client/transaction.rb +130 -130
- data/lib/rubydb/concurrency/concurrency.rb +19 -19
- data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
- data/lib/rubydb/concurrency/latch.rb +101 -101
- data/lib/rubydb/concurrency/lock_graph.rb +163 -165
- data/lib/rubydb/concurrency/mutex.rb +181 -183
- data/lib/rubydb/concurrency/rw_lock.rb +180 -180
- data/lib/rubydb/concurrency/scheduler.rb +248 -250
- data/lib/rubydb/concurrency/worker_pool.rb +145 -143
- data/lib/rubydb/configuration/config.rb +170 -170
- data/lib/rubydb/configuration/defaults.rb +179 -179
- data/lib/rubydb/configuration/environment.rb +152 -152
- data/lib/rubydb/configuration/parser.rb +185 -185
- data/lib/rubydb/configuration/validation.rb +221 -221
- data/lib/rubydb/constants.rb +74 -74
- data/lib/rubydb/constraints/check.rb +181 -181
- data/lib/rubydb/constraints/constraint.rb +101 -101
- data/lib/rubydb/constraints/foreign_key.rb +130 -130
- data/lib/rubydb/constraints/not_null.rb +64 -64
- data/lib/rubydb/constraints/primary_key.rb +99 -99
- data/lib/rubydb/constraints/unique.rb +106 -108
- data/lib/rubydb/constraints/validator.rb +349 -350
- data/lib/rubydb/errors/authentication_error.rb +10 -10
- data/lib/rubydb/errors/authorization_error.rb +23 -23
- data/lib/rubydb/errors/client_error.rb +10 -10
- data/lib/rubydb/errors/configuration_error.rb +10 -10
- data/lib/rubydb/errors/connection_error.rb +10 -10
- data/lib/rubydb/errors/constraint_error.rb +23 -23
- data/lib/rubydb/errors/corruption_error.rb +10 -10
- data/lib/rubydb/errors/database_error.rb +10 -10
- data/lib/rubydb/errors/error.rb +20 -20
- data/lib/rubydb/errors/execution_error.rb +10 -10
- data/lib/rubydb/errors/parser_error.rb +10 -10
- data/lib/rubydb/errors/recovery_error.rb +10 -10
- data/lib/rubydb/errors/replication_error.rb +10 -10
- data/lib/rubydb/errors/server_error.rb +6 -6
- data/lib/rubydb/errors/storage_error.rb +10 -10
- data/lib/rubydb/errors/transaction_error.rb +10 -10
- data/lib/rubydb/execution/aggregate_executor.rb +134 -138
- data/lib/rubydb/execution/delete_executor.rb +110 -112
- data/lib/rubydb/execution/distinct_executor.rb +131 -135
- data/lib/rubydb/execution/executor.rb +1195 -1192
- data/lib/rubydb/execution/expression.rb +191 -193
- data/lib/rubydb/execution/index_scan.rb +142 -142
- data/lib/rubydb/execution/insert_executor.rb +215 -217
- data/lib/rubydb/execution/join_executor.rb +243 -249
- data/lib/rubydb/execution/limit_executor.rb +83 -85
- data/lib/rubydb/execution/optimizer.rb +227 -215
- data/lib/rubydb/execution/plan.rb +355 -353
- data/lib/rubydb/execution/planner.rb +542 -536
- data/lib/rubydb/execution/predicate.rb +235 -235
- data/lib/rubydb/execution/scan.rb +49 -49
- data/lib/rubydb/execution/sequential_scan.rb +63 -63
- data/lib/rubydb/execution/sort_executor.rb +180 -185
- data/lib/rubydb/execution/update_executor.rb +160 -162
- data/lib/rubydb/functions/aggregate.rb +70 -70
- data/lib/rubydb/functions/date_functions.rb +274 -278
- data/lib/rubydb/functions/function.rb +85 -85
- data/lib/rubydb/functions/json_functions.rb +231 -215
- data/lib/rubydb/functions/numeric_functions.rb +346 -346
- data/lib/rubydb/functions/scalar.rb +52 -52
- data/lib/rubydb/functions/string_functions.rb +383 -383
- data/lib/rubydb/functions/system_functions.rb +258 -246
- data/lib/rubydb/history/as_of.rb +238 -238
- data/lib/rubydb/history/change.rb +105 -105
- data/lib/rubydb/history/history.rb +131 -131
- data/lib/rubydb/history/history_manager.rb +228 -229
- data/lib/rubydb/history/temporal_query.rb +202 -202
- data/lib/rubydb/history/timeline.rb +144 -144
- data/lib/rubydb/indexes/btree.rb +186 -186
- data/lib/rubydb/indexes/btree_cursor.rb +258 -258
- data/lib/rubydb/indexes/btree_node.rb +384 -385
- data/lib/rubydb/indexes/hash_index.rb +150 -150
- data/lib/rubydb/indexes/index.rb +71 -71
- data/lib/rubydb/indexes/index_manager.rb +408 -406
- data/lib/rubydb/indexes/index_scan.rb +466 -470
- data/lib/rubydb/migrations/migration.rb +253 -254
- data/lib/rubydb/migrations/migration_lock.rb +146 -146
- data/lib/rubydb/migrations/migration_manager.rb +187 -176
- data/lib/rubydb/migrations/migration_version.rb +71 -71
- data/lib/rubydb/migrations/schema_diff.rb +211 -211
- data/lib/rubydb/migrations/schema_version.rb +64 -64
- data/lib/rubydb/monitoring/events.rb +155 -160
- data/lib/rubydb/monitoring/health.rb +216 -222
- data/lib/rubydb/monitoring/logger.rb +188 -193
- data/lib/rubydb/monitoring/metrics.rb +363 -359
- data/lib/rubydb/monitoring/performance.rb +176 -176
- data/lib/rubydb/monitoring/statistics.rb +168 -170
- data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
- data/lib/rubydb/mvcc/mvcc.rb +16 -16
- data/lib/rubydb/mvcc/snapshot.rb +146 -147
- data/lib/rubydb/mvcc/vacuum.rb +180 -180
- data/lib/rubydb/mvcc/version.rb +106 -106
- data/lib/rubydb/mvcc/version_store.rb +396 -398
- data/lib/rubydb/mvcc/visibility.rb +107 -109
- data/lib/rubydb/protocol/capabilities.rb +125 -125
- data/lib/rubydb/protocol/decoder.rb +142 -145
- data/lib/rubydb/protocol/encoder.rb +131 -136
- data/lib/rubydb/protocol/handshake.rb +306 -305
- data/lib/rubydb/protocol/message.rb +121 -121
- data/lib/rubydb/protocol/parameter_binder.rb +101 -0
- data/lib/rubydb/protocol/protocol.rb +276 -277
- data/lib/rubydb/protocol/version.rb +54 -54
- data/lib/rubydb/rails/adapter.rb +245 -239
- data/lib/rubydb/rails/connection.rb +312 -314
- data/lib/rubydb/rails/database_statements.rb +122 -122
- data/lib/rubydb/rails/migration.rb +131 -131
- data/lib/rubydb/rails/quoting.rb +109 -109
- data/lib/rubydb/rails/result.rb +123 -120
- data/lib/rubydb/rails/schema_statements.rb +339 -339
- data/lib/rubydb/rails/transaction.rb +105 -105
- data/lib/rubydb/rails/type.rb +126 -126
- data/lib/rubydb/recovery/checkpoint.rb +261 -257
- data/lib/rubydb/recovery/consistency.rb +457 -467
- data/lib/rubydb/recovery/corruption_detector.rb +5 -5
- data/lib/rubydb/recovery/crash_recovery.rb +381 -387
- data/lib/rubydb/recovery/recovery_manager.rb +204 -206
- data/lib/rubydb/recovery/redo.rb +235 -237
- data/lib/rubydb/recovery/undo.rb +204 -206
- data/lib/rubydb/replication/failover.rb +5 -5
- data/lib/rubydb/replication/fencing.rb +63 -63
- data/lib/rubydb/replication/primary.rb +461 -450
- data/lib/rubydb/replication/replica.rb +382 -384
- data/lib/rubydb/replication/replication_log.rb +194 -200
- data/lib/rubydb/replication/replication_manager.rb +307 -308
- data/lib/rubydb/replication/replication_slot.rb +293 -295
- data/lib/rubydb/replication/replication_stream.rb +198 -201
- data/lib/rubydb/rubydb.rb +564 -560
- data/lib/rubydb/security/access_control.rb +252 -254
- data/lib/rubydb/security/audit_log.rb +209 -213
- data/lib/rubydb/security/authentication.rb +302 -302
- data/lib/rubydb/security/authorization.rb +282 -282
- data/lib/rubydb/security/credentials.rb +192 -196
- data/lib/rubydb/security/password.rb +205 -215
- data/lib/rubydb/security/permissions.rb +74 -74
- data/lib/rubydb/security/role.rb +99 -101
- data/lib/rubydb/security/user.rb +86 -86
- data/lib/rubydb/server/connection.rb +383 -366
- data/lib/rubydb/server/connection_pool.rb +193 -193
- data/lib/rubydb/server/lifecycle.rb +227 -228
- data/lib/rubydb/server/listener.rb +139 -136
- data/lib/rubydb/server/request_handler.rb +277 -276
- data/lib/rubydb/server/server.rb +363 -364
- data/lib/rubydb/server/session.rb +371 -369
- data/lib/rubydb/server/worker.rb +206 -210
- data/lib/rubydb/server/worker_pool.rb +168 -168
- data/lib/rubydb/sql/ast/alter_table.rb +169 -169
- data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
- data/lib/rubydb/sql/ast/commit.rb +37 -37
- data/lib/rubydb/sql/ast/constraint.rb +92 -83
- data/lib/rubydb/sql/ast/create_database.rb +41 -41
- data/lib/rubydb/sql/ast/create_index.rb +61 -61
- data/lib/rubydb/sql/ast/create_schema.rb +52 -52
- data/lib/rubydb/sql/ast/create_table.rb +187 -187
- data/lib/rubydb/sql/ast/delete.rb +54 -54
- data/lib/rubydb/sql/ast/drop_database.rb +41 -41
- data/lib/rubydb/sql/ast/drop_index.rb +41 -41
- data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
- data/lib/rubydb/sql/ast/drop_table.rb +49 -49
- data/lib/rubydb/sql/ast/explain.rb +64 -64
- data/lib/rubydb/sql/ast/expression.rb +617 -604
- data/lib/rubydb/sql/ast/insert.rb +66 -66
- data/lib/rubydb/sql/ast/node.rb +42 -42
- data/lib/rubydb/sql/ast/rollback.rb +63 -63
- data/lib/rubydb/sql/ast/savepoint.rb +59 -59
- data/lib/rubydb/sql/ast/select.rb +88 -88
- data/lib/rubydb/sql/ast/set_operation.rb +22 -20
- data/lib/rubydb/sql/ast/trigger.rb +35 -29
- data/lib/rubydb/sql/ast/update.rb +88 -88
- data/lib/rubydb/sql/ast/vacuum.rb +19 -19
- data/lib/rubydb/sql/ast/view.rb +38 -32
- data/lib/rubydb/sql/ast/with.rb +32 -32
- data/lib/rubydb/sql/grammar.rb +86 -86
- data/lib/rubydb/sql/keywords.rb +156 -156
- data/lib/rubydb/sql/lexer.rb +209 -214
- data/lib/rubydb/sql/operators.rb +100 -100
- data/lib/rubydb/sql/parser.rb +1167 -1170
- data/lib/rubydb/sql/planner/analyzer.rb +283 -302
- data/lib/rubydb/sql/planner/binder.rb +537 -550
- data/lib/rubydb/sql/planner/type_checker.rb +427 -431
- data/lib/rubydb/sql/token.rb +210 -210
- data/lib/rubydb/storage/buffer_frame.rb +44 -44
- data/lib/rubydb/storage/buffer_pool.rb +155 -155
- data/lib/rubydb/storage/database_lock.rb +74 -74
- data/lib/rubydb/storage/deserializer.rb +332 -342
- data/lib/rubydb/storage/engine.rb +2347 -2330
- data/lib/rubydb/storage/file_manager.rb +191 -187
- data/lib/rubydb/storage/free_space_map.rb +79 -81
- data/lib/rubydb/storage/page.rb +92 -94
- data/lib/rubydb/storage/page_allocator.rb +852 -855
- data/lib/rubydb/storage/page_header.rb +63 -67
- data/lib/rubydb/storage/page_manager.rb +127 -131
- data/lib/rubydb/storage/record.rb +58 -58
- data/lib/rubydb/storage/row.rb +78 -78
- data/lib/rubydb/storage/serializer.rb +51 -51
- data/lib/rubydb/storage/storage_layout.rb +151 -151
- data/lib/rubydb/storage/storage_manager.rb +114 -114
- data/lib/rubydb/storage/tuple.rb +458 -461
- data/lib/rubydb/storage/visibility_map.rb +964 -973
- data/lib/rubydb/transactions/commit_manager.rb +219 -220
- data/lib/rubydb/transactions/isolation.rb +98 -98
- data/lib/rubydb/transactions/lock.rb +76 -76
- data/lib/rubydb/transactions/lock_manager.rb +359 -362
- data/lib/rubydb/transactions/savepoint.rb +142 -143
- data/lib/rubydb/transactions/transaction.rb +214 -215
- data/lib/rubydb/transactions/transaction_id.rb +84 -84
- data/lib/rubydb/transactions/transaction_log.rb +256 -257
- data/lib/rubydb/transactions/transaction_manager.rb +434 -435
- data/lib/rubydb/types/bigint.rb +36 -36
- data/lib/rubydb/types/blob.rb +37 -37
- data/lib/rubydb/types/boolean.rb +34 -34
- data/lib/rubydb/types/date.rb +39 -39
- data/lib/rubydb/types/decimal.rb +48 -48
- data/lib/rubydb/types/float.rb +34 -34
- data/lib/rubydb/types/integer.rb +36 -36
- data/lib/rubydb/types/json.rb +41 -41
- data/lib/rubydb/types/null.rb +34 -34
- data/lib/rubydb/types/smallint.rb +36 -36
- data/lib/rubydb/types/text.rb +37 -37
- data/lib/rubydb/types/time.rb +46 -46
- data/lib/rubydb/types/timestamp.rb +39 -39
- data/lib/rubydb/types/type.rb +119 -119
- data/lib/rubydb/types/uuid.rb +47 -47
- data/lib/rubydb/types/varchar.rb +37 -37
- data/lib/rubydb/version.rb +32 -32
- data/lib/rubydb/wal/archive.rb +190 -193
- data/lib/rubydb/wal/checkpoint.rb +181 -183
- data/lib/rubydb/wal/lsn.rb +94 -94
- data/lib/rubydb/wal/reader.rb +259 -260
- data/lib/rubydb/wal/record.rb +105 -105
- data/lib/rubydb/wal/segment.rb +193 -193
- data/lib/rubydb/wal/wal.rb +480 -452
- data/lib/rubydb/wal/writer.rb +236 -236
- data/lib/rubydb.rb +7 -7
- data/packaging/docker/docker-compose.failover.yml +43 -43
- data/packaging/homebrew/rubydb.rb +19 -19
- data/rubydb.gemspec +60 -57
- data/scripts/benchmark +7 -7
- data/scripts/durability_drill +37 -37
- data/scripts/fuzz +63 -63
- data/scripts/release +45 -40
- data/scripts/release_check +43 -43
- data/scripts/replication_failover_drill +268 -250
- data/scripts/replication_network_failover_drill +287 -255
- data/scripts/restore_drill +45 -45
- data/scripts/security +45 -0
- metadata +54 -1
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "time"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require "monitor"
|
|
6
|
-
|
|
7
|
-
module RubyDB
|
|
8
|
-
module Migrations
|
|
9
|
-
# MigrationLock - Prevents concurrent migrations
|
|
10
|
-
class MigrationLock
|
|
11
|
-
attr_reader :stats
|
|
12
|
-
|
|
13
|
-
def initialize(engine, config = {})
|
|
14
|
-
@engine = engine
|
|
15
|
-
@config = config
|
|
16
|
-
@lock_table = config[:lock_table] || "migration_lock"
|
|
17
|
-
@lock_path = config[:lock_path] || "#{engine.path}.migration.lock"
|
|
18
|
-
@lock_timeout = config[:lock_timeout] || 300 # 5 minutes
|
|
19
|
-
@lock_acquired = false
|
|
20
|
-
@lock_id = nil
|
|
21
|
-
@lock_holder = nil
|
|
22
|
-
@lock_acquired_at = nil
|
|
23
|
-
@stats = {
|
|
24
|
-
locks_acquired: 0,
|
|
25
|
-
locks_released: 0,
|
|
26
|
-
locks_failed: 0,
|
|
27
|
-
lock_contention: 0,
|
|
28
|
-
lock_timeouts: 0
|
|
29
|
-
}
|
|
30
|
-
@lock = Monitor.new
|
|
31
|
-
@lock_file = nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def acquire_lock
|
|
35
|
-
@lock.synchronize do
|
|
36
|
-
return true if @lock_acquired
|
|
37
|
-
|
|
38
|
-
@stats[:locks_acquired] += 1
|
|
39
|
-
|
|
40
|
-
# Check if lock exists
|
|
41
|
-
if lock_exists?
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Check if lock is stale
|
|
45
|
-
if Time.now - acquired_at > @lock_timeout
|
|
46
|
-
# Force release stale lock
|
|
47
|
-
release_lock_force
|
|
48
|
-
@stats[:lock_timeouts] += 1
|
|
49
|
-
else
|
|
50
|
-
@stats[:lock_contention] += 1
|
|
51
|
-
@stats[:locks_failed] += 1
|
|
52
|
-
return false
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Acquire lock
|
|
57
|
-
begin
|
|
58
|
-
acquire_lock_force
|
|
59
|
-
rescue Errno::EACCES, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
60
|
-
@stats[:lock_contention] += 1
|
|
61
|
-
@stats[:locks_failed] += 1
|
|
62
|
-
return false
|
|
63
|
-
end
|
|
64
|
-
@lock_acquired = true
|
|
65
|
-
@lock_acquired_at = Time.now
|
|
66
|
-
@lock_id = generate_lock_id
|
|
67
|
-
|
|
68
|
-
true
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def release_lock
|
|
73
|
-
@lock.synchronize do
|
|
74
|
-
return false unless @lock_acquired
|
|
75
|
-
|
|
76
|
-
release_lock_force
|
|
77
|
-
@lock_acquired = false
|
|
78
|
-
@lock_id = nil
|
|
79
|
-
@lock_acquired_at = nil
|
|
80
|
-
@stats[:locks_released] += 1
|
|
81
|
-
|
|
82
|
-
true
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def lock_acquired?
|
|
87
|
-
@lock_acquired
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def lock_holder
|
|
91
|
-
@lock.synchronize do
|
|
92
|
-
return nil unless lock_exists?
|
|
93
|
-
get_lock_info
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def lock_age
|
|
98
|
-
@lock.synchronize do
|
|
99
|
-
return nil unless @lock_acquired
|
|
100
|
-
Time.now - @lock_acquired_at
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def stats
|
|
105
|
-
@lock.synchronize do
|
|
106
|
-
@stats.merge({
|
|
107
|
-
lock_acquired: @lock_acquired,
|
|
108
|
-
lock_id: @lock_id,
|
|
109
|
-
lock_age: lock_age
|
|
110
|
-
})
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
def lock_exists?
|
|
117
|
-
!@lock_file.nil?
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def get_lock_info
|
|
121
|
-
[@lock_id || "external", @lock_acquired_at || Time.now]
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def acquire_lock_force
|
|
125
|
-
FileUtils.mkdir_p(File.dirname(@lock_path))
|
|
126
|
-
file = File.open(@lock_path, "a+")
|
|
127
|
-
unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
128
|
-
file.close
|
|
129
|
-
raise Errno::EWOULDBLOCK
|
|
130
|
-
end
|
|
131
|
-
@lock_file = file
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def release_lock_force
|
|
135
|
-
return unless @lock_file
|
|
136
|
-
@lock_file.flock(File::LOCK_UN)
|
|
137
|
-
@lock_file.close
|
|
138
|
-
@lock_file = nil
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def generate_lock_id
|
|
142
|
-
"lock_#{Time.now.to_i}_#{rand(10000)}"
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "monitor"
|
|
6
|
+
|
|
7
|
+
module RubyDB
|
|
8
|
+
module Migrations
|
|
9
|
+
# MigrationLock - Prevents concurrent migrations
|
|
10
|
+
class MigrationLock
|
|
11
|
+
attr_reader :stats
|
|
12
|
+
|
|
13
|
+
def initialize(engine, config = {})
|
|
14
|
+
@engine = engine
|
|
15
|
+
@config = config
|
|
16
|
+
@lock_table = config[:lock_table] || "migration_lock"
|
|
17
|
+
@lock_path = config[:lock_path] || "#{engine.path}.migration.lock"
|
|
18
|
+
@lock_timeout = config[:lock_timeout] || 300 # 5 minutes
|
|
19
|
+
@lock_acquired = false
|
|
20
|
+
@lock_id = nil
|
|
21
|
+
@lock_holder = nil
|
|
22
|
+
@lock_acquired_at = nil
|
|
23
|
+
@stats = {
|
|
24
|
+
locks_acquired: 0,
|
|
25
|
+
locks_released: 0,
|
|
26
|
+
locks_failed: 0,
|
|
27
|
+
lock_contention: 0,
|
|
28
|
+
lock_timeouts: 0
|
|
29
|
+
}
|
|
30
|
+
@lock = Monitor.new
|
|
31
|
+
@lock_file = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def acquire_lock
|
|
35
|
+
@lock.synchronize do
|
|
36
|
+
return true if @lock_acquired
|
|
37
|
+
|
|
38
|
+
@stats[:locks_acquired] += 1
|
|
39
|
+
|
|
40
|
+
# Check if lock exists
|
|
41
|
+
if lock_exists?
|
|
42
|
+
_, acquired_at = get_lock_info
|
|
43
|
+
|
|
44
|
+
# Check if lock is stale
|
|
45
|
+
if Time.now - acquired_at > @lock_timeout
|
|
46
|
+
# Force release stale lock
|
|
47
|
+
release_lock_force
|
|
48
|
+
@stats[:lock_timeouts] += 1
|
|
49
|
+
else
|
|
50
|
+
@stats[:lock_contention] += 1
|
|
51
|
+
@stats[:locks_failed] += 1
|
|
52
|
+
return false
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Acquire lock
|
|
57
|
+
begin
|
|
58
|
+
acquire_lock_force
|
|
59
|
+
rescue Errno::EACCES, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
60
|
+
@stats[:lock_contention] += 1
|
|
61
|
+
@stats[:locks_failed] += 1
|
|
62
|
+
return false
|
|
63
|
+
end
|
|
64
|
+
@lock_acquired = true
|
|
65
|
+
@lock_acquired_at = Time.now
|
|
66
|
+
@lock_id = generate_lock_id
|
|
67
|
+
|
|
68
|
+
true
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def release_lock
|
|
73
|
+
@lock.synchronize do
|
|
74
|
+
return false unless @lock_acquired
|
|
75
|
+
|
|
76
|
+
release_lock_force
|
|
77
|
+
@lock_acquired = false
|
|
78
|
+
@lock_id = nil
|
|
79
|
+
@lock_acquired_at = nil
|
|
80
|
+
@stats[:locks_released] += 1
|
|
81
|
+
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def lock_acquired?
|
|
87
|
+
@lock_acquired
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def lock_holder
|
|
91
|
+
@lock.synchronize do
|
|
92
|
+
return nil unless lock_exists?
|
|
93
|
+
get_lock_info
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def lock_age
|
|
98
|
+
@lock.synchronize do
|
|
99
|
+
return nil unless @lock_acquired
|
|
100
|
+
Time.now - @lock_acquired_at
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def stats
|
|
105
|
+
@lock.synchronize do
|
|
106
|
+
@stats.merge({
|
|
107
|
+
lock_acquired: @lock_acquired,
|
|
108
|
+
lock_id: @lock_id,
|
|
109
|
+
lock_age: lock_age
|
|
110
|
+
})
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def lock_exists?
|
|
117
|
+
!@lock_file.nil?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def get_lock_info
|
|
121
|
+
[@lock_id || "external", @lock_acquired_at || Time.now]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def acquire_lock_force
|
|
125
|
+
FileUtils.mkdir_p(File.dirname(@lock_path))
|
|
126
|
+
file = File.open(@lock_path, "a+")
|
|
127
|
+
unless file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
128
|
+
file.close
|
|
129
|
+
raise Errno::EWOULDBLOCK
|
|
130
|
+
end
|
|
131
|
+
@lock_file = file
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def release_lock_force
|
|
135
|
+
return unless @lock_file
|
|
136
|
+
@lock_file.flock(File::LOCK_UN)
|
|
137
|
+
@lock_file.close
|
|
138
|
+
@lock_file = nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def generate_lock_id
|
|
142
|
+
"lock_#{Time.now.to_i}_#{rand(10000)}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -1,176 +1,187 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "time"
|
|
5
|
-
|
|
6
|
-
module RubyDB
|
|
7
|
-
module Migrations
|
|
8
|
-
class MigrationError < StandardError; end unless const_defined?(:MigrationError)
|
|
9
|
-
|
|
10
|
-
# Loads and applies migrations with durable version tracking and a
|
|
11
|
-
# cross-process lock. Migration files must evaluate to a Migration.
|
|
12
|
-
class MigrationManager
|
|
13
|
-
attr_reader :database, :engine, :migrations, :stats
|
|
14
|
-
|
|
15
|
-
def initialize(database, options = {})
|
|
16
|
-
@database = database
|
|
17
|
-
@engine = database.respond_to?(:engine) ? database.engine : database
|
|
18
|
-
@options = options
|
|
19
|
-
@migration_path = options[:path] || options[:migrations_path] || "db/migrate"
|
|
20
|
-
@migrations = options[:migrations] || load_migrations
|
|
21
|
-
@lock = MigrationLock.new(@engine, options)
|
|
22
|
-
@stats = {
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def migrate
|
|
26
|
-
with_lock do
|
|
27
|
-
ensure_schema
|
|
28
|
-
applied = applied_versions
|
|
29
|
-
validate_applied_migrations(applied)
|
|
30
|
-
target = target_version
|
|
31
|
-
pending = ordered_migrations.reject { |migration| applied.key?(migration.version.to_s) }
|
|
32
|
-
pending = pending.select { |migration| migration_key(migration.version) <= migration_key(target) } if target
|
|
33
|
-
apply_migrations(pending)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def rollback(steps = 1)
|
|
38
|
-
raise ArgumentError, "steps must be a positive integer" unless steps.is_a?(Integer) && steps.positive?
|
|
39
|
-
|
|
40
|
-
with_lock do
|
|
41
|
-
ensure_schema
|
|
42
|
-
applied = applied_versions
|
|
43
|
-
validate_applied_migrations(applied)
|
|
44
|
-
to_rollback = ordered_migrations.select { |m| applied.key?(m.version.to_s) }
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
to_rollback.each { |migration| rollback_migration(migration) }
|
|
49
|
-
to_rollback
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def status
|
|
54
|
-
ensure_schema
|
|
55
|
-
applied = applied_versions
|
|
56
|
-
ordered_migrations.map do |migration|
|
|
57
|
-
record = applied[migration.version.to_s]
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def with_lock
|
|
66
|
-
raise MigrationError, "could not acquire migration lock" unless @lock.acquire_lock
|
|
67
|
-
yield
|
|
68
|
-
ensure
|
|
69
|
-
@lock.release_lock if @lock&.lock_acquired?
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def ensure_schema
|
|
73
|
-
@database.execute("CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, migration_name TEXT, applied_at TIMESTAMP, checksum TEXT)")
|
|
74
|
-
ensure_column("migration_name", "TEXT")
|
|
75
|
-
ensure_column("checksum", "TEXT")
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def ensure_column(name, type)
|
|
79
|
-
@database.execute("ALTER TABLE schema_migrations ADD COLUMN #{name} #{type}")
|
|
80
|
-
rescue
|
|
81
|
-
raise unless error.message =~ /already exists|duplicate|exists/i
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def applied_versions
|
|
85
|
-
rows = @database.query("SELECT version, migration_name, checksum FROM schema_migrations")
|
|
86
|
-
Array(rows).each_with_object({}) do |row, result|
|
|
87
|
-
version = row[:version] || row["version"]
|
|
88
|
-
result[version.to_s] = {
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def apply_migrations(migrations)
|
|
93
|
-
migrations.each do |migration|
|
|
94
|
-
existing = applied_versions[migration.version.to_s]
|
|
95
|
-
if existing && existing[:checksum] && existing[:checksum] != migration_checksum(migration)
|
|
96
|
-
raise MigrationError, "migration checksum mismatch for #{migration.version}"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
begin
|
|
100
|
-
@database.transaction do
|
|
101
|
-
migration.apply_up(@engine)
|
|
102
|
-
insert_version(migration)
|
|
103
|
-
end
|
|
104
|
-
@stats[:applied] += 1
|
|
105
|
-
rescue
|
|
106
|
-
migration.mark_failed
|
|
107
|
-
@stats[:failed] += 1
|
|
108
|
-
raise
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def rollback_migration(migration)
|
|
114
|
-
@database.transaction do
|
|
115
|
-
migration.apply_down(@engine)
|
|
116
|
-
@database.execute("DELETE FROM schema_migrations WHERE version = #{quote(migration.version.to_s)}")
|
|
117
|
-
end
|
|
118
|
-
@stats[:rolled_back] += 1
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def insert_version(migration)
|
|
122
|
-
values = [migration.version.to_s, migration.name.to_s, Time.now.iso8601, migration_checksum(migration)].map { |v| quote(v) }
|
|
123
|
-
@database.execute("INSERT INTO schema_migrations (version, migration_name, applied_at, checksum) VALUES (#{values.join(
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def quote(value)
|
|
127
|
-
"'#{value.to_s.gsub("'", "''")}'"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def ordered_migrations
|
|
131
|
-
@migrations.sort_by { |migration| migration_key(migration.version) }
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def migration_key(version)
|
|
135
|
-
version.to_s =~ /\A\d+\z/ ? version.to_i : version.to_s
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def target_version
|
|
139
|
-
@options[:version]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def migration_checksum(migration)
|
|
143
|
-
migration.checksum
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def validate_applied_migrations(applied)
|
|
147
|
-
migrations_by_version = @migrations.each_with_object({}) do |migration, result|
|
|
148
|
-
result[migration.version.to_s] = migration
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
applied.each do |version, record|
|
|
152
|
-
migration = migrations_by_version[version]
|
|
153
|
-
raise MigrationError, "applied migration #{version} is missing from the migration path" unless migration
|
|
154
|
-
|
|
155
|
-
expected = migration_checksum(migration)
|
|
156
|
-
recorded = record[:checksum].to_s
|
|
157
|
-
unless recorded.empty? || recorded == expected
|
|
158
|
-
raise MigrationError, "migration checksum mismatch for #{version}"
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def load_migrations
|
|
164
|
-
return [] unless Dir.exist?(@migration_path)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module RubyDB
|
|
7
|
+
module Migrations
|
|
8
|
+
class MigrationError < StandardError; end unless const_defined?(:MigrationError)
|
|
9
|
+
|
|
10
|
+
# Loads and applies migrations with durable version tracking and a
|
|
11
|
+
# cross-process lock. Migration files must evaluate to a Migration.
|
|
12
|
+
class MigrationManager
|
|
13
|
+
attr_reader :database, :engine, :migrations, :stats
|
|
14
|
+
|
|
15
|
+
def initialize(database, options = {})
|
|
16
|
+
@database = database
|
|
17
|
+
@engine = database.respond_to?(:engine) ? database.engine : database
|
|
18
|
+
@options = options
|
|
19
|
+
@migration_path = File.expand_path(options[:path] || options[:migrations_path] || "db/migrate")
|
|
20
|
+
@migrations = options[:migrations] || load_migrations
|
|
21
|
+
@lock = MigrationLock.new(@engine, options)
|
|
22
|
+
@stats = {applied: 0, rolled_back: 0, failed: 0}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def migrate
|
|
26
|
+
with_lock do
|
|
27
|
+
ensure_schema
|
|
28
|
+
applied = applied_versions
|
|
29
|
+
validate_applied_migrations(applied)
|
|
30
|
+
target = target_version
|
|
31
|
+
pending = ordered_migrations.reject { |migration| applied.key?(migration.version.to_s) }
|
|
32
|
+
pending = pending.select { |migration| migration_key(migration.version) <= migration_key(target) } if target
|
|
33
|
+
apply_migrations(pending)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rollback(steps = 1)
|
|
38
|
+
raise ArgumentError, "steps must be a positive integer" unless steps.is_a?(Integer) && steps.positive?
|
|
39
|
+
|
|
40
|
+
with_lock do
|
|
41
|
+
ensure_schema
|
|
42
|
+
applied = applied_versions
|
|
43
|
+
validate_applied_migrations(applied)
|
|
44
|
+
to_rollback = ordered_migrations.select { |m| applied.key?(m.version.to_s) }
|
|
45
|
+
.sort_by { |m| migration_key(m.version) }
|
|
46
|
+
.reverse
|
|
47
|
+
.first(steps)
|
|
48
|
+
to_rollback.each { |migration| rollback_migration(migration) }
|
|
49
|
+
to_rollback
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def status
|
|
54
|
+
ensure_schema
|
|
55
|
+
applied = applied_versions
|
|
56
|
+
ordered_migrations.map do |migration|
|
|
57
|
+
record = applied[migration.version.to_s]
|
|
58
|
+
{version: migration.version.to_s, name: migration.name,
|
|
59
|
+
state: record ? :applied : :pending, checksum: record && record[:checksum]}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def with_lock
|
|
66
|
+
raise MigrationError, "could not acquire migration lock" unless @lock.acquire_lock
|
|
67
|
+
yield
|
|
68
|
+
ensure
|
|
69
|
+
@lock.release_lock if @lock&.lock_acquired?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ensure_schema
|
|
73
|
+
@database.execute("CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, migration_name TEXT, applied_at TIMESTAMP, checksum TEXT)")
|
|
74
|
+
ensure_column("migration_name", "TEXT")
|
|
75
|
+
ensure_column("checksum", "TEXT")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ensure_column(name, type)
|
|
79
|
+
@database.execute("ALTER TABLE schema_migrations ADD COLUMN #{name} #{type}")
|
|
80
|
+
rescue => error
|
|
81
|
+
raise unless error.message =~ /already exists|duplicate|exists/i
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def applied_versions
|
|
85
|
+
rows = @database.query("SELECT version, migration_name, checksum FROM schema_migrations")
|
|
86
|
+
Array(rows).each_with_object({}) do |row, result|
|
|
87
|
+
version = row[:version] || row["version"]
|
|
88
|
+
result[version.to_s] = {name: row[:migration_name] || row["migration_name"], checksum: row[:checksum] || row["checksum"]}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def apply_migrations(migrations)
|
|
93
|
+
migrations.each do |migration|
|
|
94
|
+
existing = applied_versions[migration.version.to_s]
|
|
95
|
+
if existing && existing[:checksum] && existing[:checksum] != migration_checksum(migration)
|
|
96
|
+
raise MigrationError, "migration checksum mismatch for #{migration.version}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
@database.transaction do
|
|
101
|
+
migration.apply_up(@engine)
|
|
102
|
+
insert_version(migration)
|
|
103
|
+
end
|
|
104
|
+
@stats[:applied] += 1
|
|
105
|
+
rescue
|
|
106
|
+
migration.mark_failed
|
|
107
|
+
@stats[:failed] += 1
|
|
108
|
+
raise
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def rollback_migration(migration)
|
|
114
|
+
@database.transaction do
|
|
115
|
+
migration.apply_down(@engine)
|
|
116
|
+
@database.execute("DELETE FROM schema_migrations WHERE version = #{quote(migration.version.to_s)}")
|
|
117
|
+
end
|
|
118
|
+
@stats[:rolled_back] += 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def insert_version(migration)
|
|
122
|
+
values = [migration.version.to_s, migration.name.to_s, Time.now.iso8601, migration_checksum(migration)].map { |v| quote(v) }
|
|
123
|
+
@database.execute("INSERT INTO schema_migrations (version, migration_name, applied_at, checksum) VALUES (#{values.join(", ")})")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def quote(value)
|
|
127
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def ordered_migrations
|
|
131
|
+
@migrations.sort_by { |migration| migration_key(migration.version) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def migration_key(version)
|
|
135
|
+
(version.to_s =~ /\A\d+\z/) ? version.to_i : version.to_s
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def target_version
|
|
139
|
+
@options[:version]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def migration_checksum(migration)
|
|
143
|
+
migration.checksum
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def validate_applied_migrations(applied)
|
|
147
|
+
migrations_by_version = @migrations.each_with_object({}) do |migration, result|
|
|
148
|
+
result[migration.version.to_s] = migration
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
applied.each do |version, record|
|
|
152
|
+
migration = migrations_by_version[version]
|
|
153
|
+
raise MigrationError, "applied migration #{version} is missing from the migration path" unless migration
|
|
154
|
+
|
|
155
|
+
expected = migration_checksum(migration)
|
|
156
|
+
recorded = record[:checksum].to_s
|
|
157
|
+
unless recorded.empty? || recorded == expected
|
|
158
|
+
raise MigrationError, "migration checksum mismatch for #{version}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def load_migrations
|
|
164
|
+
return [] unless Dir.exist?(@migration_path)
|
|
165
|
+
|
|
166
|
+
migration_root = File.realpath(@migration_path)
|
|
167
|
+
Dir.glob(File.join(migration_root, "*.rb")).sort.map do |path|
|
|
168
|
+
path = File.realpath(path)
|
|
169
|
+
unless File.dirname(path) == migration_root
|
|
170
|
+
raise MigrationError, "migration path escapes #{@migration_path}: #{path}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Migration files are trusted deployment code, like Rails migrations.
|
|
174
|
+
# The root and every file are resolved above so a symlink cannot make
|
|
175
|
+
# the loader execute Ruby from outside the configured migration tree.
|
|
176
|
+
# rubocop:disable Security/Eval
|
|
177
|
+
migration = eval(File.binread(path), TOPLEVEL_BINDING, path, 1)
|
|
178
|
+
# rubocop:enable Security/Eval
|
|
179
|
+
unless migration.is_a?(Migration)
|
|
180
|
+
raise MigrationError, "#{path} must evaluate to a Migration"
|
|
181
|
+
end
|
|
182
|
+
migration
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|