rubydb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitattributes +20 -0
- data/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- data/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- data/.github/ISSUE_TEMPLATE/performance.yml +0 -0
- data/.github/ISSUE_TEMPLATE/security.yml +0 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- data/.github/dependabot.yml +0 -0
- data/.github/workflows/benchmark.yml +26 -0
- data/.github/workflows/compatibility.yml +63 -0
- data/.github/workflows/fuzz.yml +33 -0
- data/.github/workflows/lint.yml +21 -0
- data/.github/workflows/operations.yml +24 -0
- data/.github/workflows/production-validation.yml +111 -0
- data/.github/workflows/release.yml +77 -0
- data/.github/workflows/security.yml +37 -0
- data/.github/workflows/test.yml +26 -0
- data/.github/workflows/workload.yml +58 -0
- data/.gitignore +43 -0
- data/.rspec +4 -0
- data/.rubocop.yml +44 -0
- data/.ruby-version +1 -0
- data/.standard.yml +14 -0
- data/ARCHITECTURE.md +21 -0
- data/CHANGELOG.md +31 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/GOVERNANCE.md +16 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +60 -0
- data/README.md +162 -0
- data/ROADMAP.md +27 -0
- data/Rakefile +71 -0
- data/SECURITY.md +54 -0
- data/SUPPORT.md +14 -0
- data/adapters/activerecord/Gemfile +11 -0
- data/adapters/activerecord/README.md +9 -0
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +887 -0
- data/adapters/activerecord/rubydb-activerecord.gemspec +27 -0
- data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +151 -0
- data/adapters/ruby/README.md +18 -0
- data/adapters/sequel/README.md +11 -0
- data/bin/rubydb +19 -0
- data/bin/rubydb-server +126 -0
- data/config/development.yml +0 -0
- data/config/monitoring/prometheus-alerts.yml +39 -0
- data/config/production.yml +36 -0
- data/config/test.yml +0 -0
- data/docs/README.md +71 -0
- data/docs/architecture/concurrency.md +14 -0
- data/docs/architecture/current-state.md +125 -0
- data/docs/architecture/execution-engine.md +25 -0
- data/docs/architecture/indexes.md +19 -0
- data/docs/architecture/mvcc.md +19 -0
- data/docs/architecture/overview.md +13 -0
- data/docs/architecture/pages.md +11 -0
- data/docs/architecture/production-roadmap.md +82 -0
- data/docs/architecture/query-planner.md +20 -0
- data/docs/architecture/recovery.md +18 -0
- data/docs/architecture/sql-engine.md +12 -0
- data/docs/architecture/storage-engine.md +14 -0
- data/docs/architecture/transactions.md +10 -0
- data/docs/architecture/wal.md +28 -0
- data/docs/cli-cheatsheet.md +98 -0
- data/docs/cli.md +275 -0
- data/docs/contributing/architecture.md +9 -0
- data/docs/contributing/benchmarking.md +14 -0
- data/docs/contributing/development.md +16 -0
- data/docs/contributing/release-process.md +49 -0
- data/docs/contributing/testing.md +16 -0
- data/docs/debugging.md +229 -0
- data/docs/developer/branching.md +10 -0
- data/docs/developer/database-diff.md +10 -0
- data/docs/developer/local-development.md +17 -0
- data/docs/developer/snapshots.md +9 -0
- data/docs/developer/temporal-data.md +10 -0
- data/docs/developer-guide.md +297 -0
- data/docs/getting-started/first-database.md +16 -0
- data/docs/getting-started/first-query.md +13 -0
- data/docs/getting-started/installation.md +19 -0
- data/docs/getting-started/local-to-production.md +300 -0
- data/docs/getting-started/quickstart.md +17 -0
- data/docs/getting-started/rails.md +16 -0
- data/docs/hardening_backlog.md +93 -0
- data/docs/lessons-learned.md +112 -0
- data/docs/operations/backups.md +33 -0
- data/docs/operations/disaster-recovery.md +31 -0
- data/docs/operations/failover.md +30 -0
- data/docs/operations/monitoring.md +25 -0
- data/docs/operations/production-guide.md +295 -0
- data/docs/operations/production-runbook.md +45 -0
- data/docs/operations/replication.md +33 -0
- data/docs/operations/restore.md +6 -0
- data/docs/operations/runbook.md +34 -0
- data/docs/operations/upgrades.md +14 -0
- data/docs/operations/workload-testing.md +17 -0
- data/docs/production-readiness.md +118 -0
- data/docs/production_validation.md +150 -0
- data/docs/rails/active-record.md +11 -0
- data/docs/rails/compatibility-guide.md +90 -0
- data/docs/rails/database-yml.md +92 -0
- data/docs/rails/installation.md +17 -0
- data/docs/rails/migrations.md +17 -0
- data/docs/rails/production.md +82 -0
- data/docs/rails/troubleshooting.md +18 -0
- data/docs/release.md +59 -0
- data/docs/server/architecture.md +10 -0
- data/docs/server/authentication.md +10 -0
- data/docs/server/configuration.md +16 -0
- data/docs/server/connection-pooling.md +10 -0
- data/docs/server/deployment.md +10 -0
- data/docs/server/protocol.md +12 -0
- data/docs/sql/compatibility-guide.md +82 -0
- data/docs/sql/compatibility.md +39 -0
- data/docs/sql/data-types.md +10 -0
- data/docs/sql/functions.md +9 -0
- data/docs/sql/joins.md +9 -0
- data/docs/sql/operators.md +9 -0
- data/docs/sql/sqlite-compatibility.md +21 -0
- data/docs/sql/syntax.md +10 -0
- data/docs/sql/transactions.md +10 -0
- data/docs/troubleshooting.md +244 -0
- data/exe/rubydb +23 -0
- data/lib/rubydb/backup/archive.rb +334 -0
- data/lib/rubydb/backup/backup.rb +401 -0
- data/lib/rubydb/backup/incremental.rb +353 -0
- data/lib/rubydb/backup/restore.rb +290 -0
- data/lib/rubydb/backup/snapshot.rb +267 -0
- data/lib/rubydb/backup/verification.rb +279 -0
- data/lib/rubydb/branching/branch.rb +181 -0
- data/lib/rubydb/branching/branch_manager.rb +311 -0
- data/lib/rubydb/branching/branch_metadata.rb +140 -0
- data/lib/rubydb/branching/checkout.rb +166 -0
- data/lib/rubydb/branching/copy_on_write.rb +272 -0
- data/lib/rubydb/branching/diff.rb +138 -0
- data/lib/rubydb/branching/merge.rb +285 -0
- data/lib/rubydb/build_info.rb +15 -0
- data/lib/rubydb/catalog/catalog.rb +391 -0
- data/lib/rubydb/catalog/column.rb +112 -0
- data/lib/rubydb/catalog/constraint.rb +180 -0
- data/lib/rubydb/catalog/database.rb +184 -0
- data/lib/rubydb/catalog/index.rb +97 -0
- data/lib/rubydb/catalog/schema.rb +103 -0
- data/lib/rubydb/catalog/sequence.rb +90 -0
- data/lib/rubydb/catalog/system_catalog.rb +698 -0
- data/lib/rubydb/catalog/table.rb +178 -0
- data/lib/rubydb/catalog/trigger.rb +102 -0
- data/lib/rubydb/catalog/view.rb +66 -0
- data/lib/rubydb/cli/application.rb +163 -0
- data/lib/rubydb/cli/commands/backup.rb +81 -0
- data/lib/rubydb/cli/commands/branch.rb +72 -0
- data/lib/rubydb/cli/commands/checkout.rb +54 -0
- data/lib/rubydb/cli/commands/create.rb +58 -0
- data/lib/rubydb/cli/commands/diff.rb +77 -0
- data/lib/rubydb/cli/commands/doctor.rb +74 -0
- data/lib/rubydb/cli/commands/drop.rb +57 -0
- data/lib/rubydb/cli/commands/init.rb +102 -0
- data/lib/rubydb/cli/commands/inspect.rb +95 -0
- data/lib/rubydb/cli/commands/merge.rb +63 -0
- data/lib/rubydb/cli/commands/migrate.rb +62 -0
- data/lib/rubydb/cli/commands/restart.rb +39 -0
- data/lib/rubydb/cli/commands/restore.rb +121 -0
- data/lib/rubydb/cli/commands/shell.rb +365 -0
- data/lib/rubydb/cli/commands/snapshot.rb +79 -0
- data/lib/rubydb/cli/commands/start.rb +82 -0
- data/lib/rubydb/cli/commands/status.rb +92 -0
- data/lib/rubydb/cli/commands/stop.rb +47 -0
- data/lib/rubydb/cli/commands/vacuum.rb +58 -0
- data/lib/rubydb/cli/formatter.rb +221 -0
- data/lib/rubydb/cli/output.rb +168 -0
- data/lib/rubydb/client/client.rb +304 -0
- data/lib/rubydb/client/connection.rb +415 -0
- data/lib/rubydb/client/connection_pool.rb +168 -0
- data/lib/rubydb/client/connection_url.rb +96 -0
- data/lib/rubydb/client/prepared_statement.rb +60 -0
- data/lib/rubydb/client/result.rb +123 -0
- data/lib/rubydb/client/statement.rb +52 -0
- data/lib/rubydb/client/transaction.rb +130 -0
- data/lib/rubydb/concurrency/concurrency.rb +19 -0
- data/lib/rubydb/concurrency/deadlock_detector.rb +150 -0
- data/lib/rubydb/concurrency/latch.rb +101 -0
- data/lib/rubydb/concurrency/lock_graph.rb +165 -0
- data/lib/rubydb/concurrency/mutex.rb +183 -0
- data/lib/rubydb/concurrency/rw_lock.rb +180 -0
- data/lib/rubydb/concurrency/scheduler.rb +250 -0
- data/lib/rubydb/concurrency/worker_pool.rb +143 -0
- data/lib/rubydb/configuration/config.rb +170 -0
- data/lib/rubydb/configuration/defaults.rb +179 -0
- data/lib/rubydb/configuration/environment.rb +152 -0
- data/lib/rubydb/configuration/parser.rb +185 -0
- data/lib/rubydb/configuration/validation.rb +221 -0
- data/lib/rubydb/constants.rb +74 -0
- data/lib/rubydb/constraints/check.rb +181 -0
- data/lib/rubydb/constraints/constraint.rb +101 -0
- data/lib/rubydb/constraints/foreign_key.rb +130 -0
- data/lib/rubydb/constraints/not_null.rb +64 -0
- data/lib/rubydb/constraints/primary_key.rb +99 -0
- data/lib/rubydb/constraints/unique.rb +108 -0
- data/lib/rubydb/constraints/validator.rb +350 -0
- data/lib/rubydb/errors/authentication_error.rb +10 -0
- data/lib/rubydb/errors/authorization_error.rb +23 -0
- data/lib/rubydb/errors/client_error.rb +10 -0
- data/lib/rubydb/errors/configuration_error.rb +10 -0
- data/lib/rubydb/errors/connection_error.rb +10 -0
- data/lib/rubydb/errors/constraint_error.rb +23 -0
- data/lib/rubydb/errors/corruption_error.rb +10 -0
- data/lib/rubydb/errors/database_error.rb +10 -0
- data/lib/rubydb/errors/error.rb +20 -0
- data/lib/rubydb/errors/execution_error.rb +10 -0
- data/lib/rubydb/errors/parser_error.rb +10 -0
- data/lib/rubydb/errors/recovery_error.rb +10 -0
- data/lib/rubydb/errors/replication_error.rb +10 -0
- data/lib/rubydb/errors/server_error.rb +6 -0
- data/lib/rubydb/errors/storage_error.rb +10 -0
- data/lib/rubydb/errors/transaction_error.rb +10 -0
- data/lib/rubydb/execution/aggregate_executor.rb +138 -0
- data/lib/rubydb/execution/delete_executor.rb +112 -0
- data/lib/rubydb/execution/distinct_executor.rb +135 -0
- data/lib/rubydb/execution/executor.rb +1192 -0
- data/lib/rubydb/execution/expression.rb +193 -0
- data/lib/rubydb/execution/index_scan.rb +142 -0
- data/lib/rubydb/execution/insert_executor.rb +217 -0
- data/lib/rubydb/execution/join_executor.rb +249 -0
- data/lib/rubydb/execution/limit_executor.rb +85 -0
- data/lib/rubydb/execution/optimizer.rb +215 -0
- data/lib/rubydb/execution/plan.rb +361 -0
- data/lib/rubydb/execution/planner.rb +546 -0
- data/lib/rubydb/execution/predicate.rb +235 -0
- data/lib/rubydb/execution/scan.rb +49 -0
- data/lib/rubydb/execution/sequential_scan.rb +63 -0
- data/lib/rubydb/execution/sort_executor.rb +185 -0
- data/lib/rubydb/execution/update_executor.rb +162 -0
- data/lib/rubydb/functions/aggregate.rb +70 -0
- data/lib/rubydb/functions/date_functions.rb +278 -0
- data/lib/rubydb/functions/function.rb +85 -0
- data/lib/rubydb/functions/json_functions.rb +215 -0
- data/lib/rubydb/functions/numeric_functions.rb +346 -0
- data/lib/rubydb/functions/scalar.rb +52 -0
- data/lib/rubydb/functions/string_functions.rb +383 -0
- data/lib/rubydb/functions/system_functions.rb +246 -0
- data/lib/rubydb/history/as_of.rb +238 -0
- data/lib/rubydb/history/change.rb +105 -0
- data/lib/rubydb/history/history.rb +131 -0
- data/lib/rubydb/history/history_manager.rb +229 -0
- data/lib/rubydb/history/temporal_query.rb +202 -0
- data/lib/rubydb/history/timeline.rb +144 -0
- data/lib/rubydb/indexes/btree.rb +186 -0
- data/lib/rubydb/indexes/btree_cursor.rb +258 -0
- data/lib/rubydb/indexes/btree_node.rb +385 -0
- data/lib/rubydb/indexes/hash_index.rb +150 -0
- data/lib/rubydb/indexes/index.rb +71 -0
- data/lib/rubydb/indexes/index_manager.rb +406 -0
- data/lib/rubydb/indexes/index_scan.rb +470 -0
- data/lib/rubydb/migrations/migration.rb +254 -0
- data/lib/rubydb/migrations/migration_lock.rb +146 -0
- data/lib/rubydb/migrations/migration_manager.rb +176 -0
- data/lib/rubydb/migrations/migration_version.rb +71 -0
- data/lib/rubydb/migrations/schema_diff.rb +211 -0
- data/lib/rubydb/migrations/schema_version.rb +64 -0
- data/lib/rubydb/monitoring/events.rb +160 -0
- data/lib/rubydb/monitoring/health.rb +222 -0
- data/lib/rubydb/monitoring/logger.rb +193 -0
- data/lib/rubydb/monitoring/metrics.rb +359 -0
- data/lib/rubydb/monitoring/performance.rb +176 -0
- data/lib/rubydb/monitoring/statistics.rb +170 -0
- data/lib/rubydb/mvcc/garbage_collector.rb +199 -0
- data/lib/rubydb/mvcc/mvcc.rb +16 -0
- data/lib/rubydb/mvcc/snapshot.rb +147 -0
- data/lib/rubydb/mvcc/vacuum.rb +180 -0
- data/lib/rubydb/mvcc/version.rb +106 -0
- data/lib/rubydb/mvcc/version_store.rb +398 -0
- data/lib/rubydb/mvcc/visibility.rb +109 -0
- data/lib/rubydb/protocol/capabilities.rb +125 -0
- data/lib/rubydb/protocol/decoder.rb +145 -0
- data/lib/rubydb/protocol/encoder.rb +136 -0
- data/lib/rubydb/protocol/handshake.rb +305 -0
- data/lib/rubydb/protocol/message.rb +121 -0
- data/lib/rubydb/protocol/protocol.rb +277 -0
- data/lib/rubydb/protocol/version.rb +54 -0
- data/lib/rubydb/rails/adapter.rb +239 -0
- data/lib/rubydb/rails/connection.rb +314 -0
- data/lib/rubydb/rails/database_statements.rb +122 -0
- data/lib/rubydb/rails/migration.rb +131 -0
- data/lib/rubydb/rails/quoting.rb +109 -0
- data/lib/rubydb/rails/result.rb +120 -0
- data/lib/rubydb/rails/schema_statements.rb +339 -0
- data/lib/rubydb/rails/transaction.rb +105 -0
- data/lib/rubydb/rails/type.rb +126 -0
- data/lib/rubydb/recovery/checkpoint.rb +257 -0
- data/lib/rubydb/recovery/consistency.rb +467 -0
- data/lib/rubydb/recovery/corruption_detector.rb +5 -0
- data/lib/rubydb/recovery/crash_recovery.rb +387 -0
- data/lib/rubydb/recovery/recovery_manager.rb +206 -0
- data/lib/rubydb/recovery/redo.rb +237 -0
- data/lib/rubydb/recovery/undo.rb +206 -0
- data/lib/rubydb/replication/failover.rb +5 -0
- data/lib/rubydb/replication/fencing.rb +63 -0
- data/lib/rubydb/replication/primary.rb +450 -0
- data/lib/rubydb/replication/replica.rb +384 -0
- data/lib/rubydb/replication/replication_log.rb +200 -0
- data/lib/rubydb/replication/replication_manager.rb +308 -0
- data/lib/rubydb/replication/replication_slot.rb +295 -0
- data/lib/rubydb/replication/replication_stream.rb +201 -0
- data/lib/rubydb/rubydb.rb +560 -0
- data/lib/rubydb/security/access_control.rb +254 -0
- data/lib/rubydb/security/audit_log.rb +213 -0
- data/lib/rubydb/security/authentication.rb +302 -0
- data/lib/rubydb/security/authorization.rb +282 -0
- data/lib/rubydb/security/credentials.rb +196 -0
- data/lib/rubydb/security/password.rb +215 -0
- data/lib/rubydb/security/permissions.rb +74 -0
- data/lib/rubydb/security/role.rb +101 -0
- data/lib/rubydb/security/user.rb +86 -0
- data/lib/rubydb/server/connection.rb +366 -0
- data/lib/rubydb/server/connection_pool.rb +193 -0
- data/lib/rubydb/server/lifecycle.rb +228 -0
- data/lib/rubydb/server/listener.rb +136 -0
- data/lib/rubydb/server/request_handler.rb +276 -0
- data/lib/rubydb/server/server.rb +364 -0
- data/lib/rubydb/server/session.rb +369 -0
- data/lib/rubydb/server/worker.rb +210 -0
- data/lib/rubydb/server/worker_pool.rb +168 -0
- data/lib/rubydb/sql/ast/alter_table.rb +169 -0
- data/lib/rubydb/sql/ast/begin_transaction.rb +47 -0
- data/lib/rubydb/sql/ast/commit.rb +37 -0
- data/lib/rubydb/sql/ast/constraint.rb +83 -0
- data/lib/rubydb/sql/ast/create_database.rb +41 -0
- data/lib/rubydb/sql/ast/create_index.rb +61 -0
- data/lib/rubydb/sql/ast/create_schema.rb +52 -0
- data/lib/rubydb/sql/ast/create_table.rb +187 -0
- data/lib/rubydb/sql/ast/delete.rb +54 -0
- data/lib/rubydb/sql/ast/drop_database.rb +41 -0
- data/lib/rubydb/sql/ast/drop_index.rb +41 -0
- data/lib/rubydb/sql/ast/drop_schema.rb +49 -0
- data/lib/rubydb/sql/ast/drop_table.rb +49 -0
- data/lib/rubydb/sql/ast/explain.rb +64 -0
- data/lib/rubydb/sql/ast/expression.rb +604 -0
- data/lib/rubydb/sql/ast/insert.rb +82 -0
- data/lib/rubydb/sql/ast/node.rb +42 -0
- data/lib/rubydb/sql/ast/rollback.rb +63 -0
- data/lib/rubydb/sql/ast/savepoint.rb +59 -0
- data/lib/rubydb/sql/ast/select.rb +88 -0
- data/lib/rubydb/sql/ast/set_operation.rb +20 -0
- data/lib/rubydb/sql/ast/trigger.rb +29 -0
- data/lib/rubydb/sql/ast/update.rb +88 -0
- data/lib/rubydb/sql/ast/vacuum.rb +19 -0
- data/lib/rubydb/sql/ast/view.rb +32 -0
- data/lib/rubydb/sql/ast/with.rb +32 -0
- data/lib/rubydb/sql/grammar.rb +86 -0
- data/lib/rubydb/sql/keywords.rb +156 -0
- data/lib/rubydb/sql/lexer.rb +214 -0
- data/lib/rubydb/sql/operators.rb +100 -0
- data/lib/rubydb/sql/parser.rb +1185 -0
- data/lib/rubydb/sql/planner/analyzer.rb +302 -0
- data/lib/rubydb/sql/planner/binder.rb +550 -0
- data/lib/rubydb/sql/planner/type_checker.rb +431 -0
- data/lib/rubydb/sql/token.rb +210 -0
- data/lib/rubydb/storage/buffer_frame.rb +44 -0
- data/lib/rubydb/storage/buffer_pool.rb +155 -0
- data/lib/rubydb/storage/database_lock.rb +74 -0
- data/lib/rubydb/storage/deserializer.rb +342 -0
- data/lib/rubydb/storage/engine.rb +2338 -0
- data/lib/rubydb/storage/file_manager.rb +187 -0
- data/lib/rubydb/storage/free_space_map.rb +81 -0
- data/lib/rubydb/storage/page.rb +94 -0
- data/lib/rubydb/storage/page_allocator.rb +855 -0
- data/lib/rubydb/storage/page_header.rb +67 -0
- data/lib/rubydb/storage/page_manager.rb +131 -0
- data/lib/rubydb/storage/record.rb +58 -0
- data/lib/rubydb/storage/row.rb +78 -0
- data/lib/rubydb/storage/serializer.rb +51 -0
- data/lib/rubydb/storage/storage_layout.rb +151 -0
- data/lib/rubydb/storage/storage_manager.rb +114 -0
- data/lib/rubydb/storage/tuple.rb +461 -0
- data/lib/rubydb/storage/visibility_map.rb +984 -0
- data/lib/rubydb/transactions/commit_manager.rb +220 -0
- data/lib/rubydb/transactions/isolation.rb +98 -0
- data/lib/rubydb/transactions/lock.rb +76 -0
- data/lib/rubydb/transactions/lock_manager.rb +362 -0
- data/lib/rubydb/transactions/savepoint.rb +143 -0
- data/lib/rubydb/transactions/transaction.rb +215 -0
- data/lib/rubydb/transactions/transaction_id.rb +84 -0
- data/lib/rubydb/transactions/transaction_log.rb +257 -0
- data/lib/rubydb/transactions/transaction_manager.rb +435 -0
- data/lib/rubydb/types/bigint.rb +36 -0
- data/lib/rubydb/types/blob.rb +37 -0
- data/lib/rubydb/types/boolean.rb +34 -0
- data/lib/rubydb/types/date.rb +39 -0
- data/lib/rubydb/types/decimal.rb +48 -0
- data/lib/rubydb/types/float.rb +34 -0
- data/lib/rubydb/types/integer.rb +36 -0
- data/lib/rubydb/types/json.rb +41 -0
- data/lib/rubydb/types/null.rb +34 -0
- data/lib/rubydb/types/smallint.rb +36 -0
- data/lib/rubydb/types/text.rb +37 -0
- data/lib/rubydb/types/time.rb +46 -0
- data/lib/rubydb/types/timestamp.rb +39 -0
- data/lib/rubydb/types/type.rb +119 -0
- data/lib/rubydb/types/uuid.rb +47 -0
- data/lib/rubydb/types/varchar.rb +37 -0
- data/lib/rubydb/version.rb +33 -0
- data/lib/rubydb/wal/archive.rb +193 -0
- data/lib/rubydb/wal/checkpoint.rb +183 -0
- data/lib/rubydb/wal/lsn.rb +94 -0
- data/lib/rubydb/wal/reader.rb +260 -0
- data/lib/rubydb/wal/record.rb +105 -0
- data/lib/rubydb/wal/segment.rb +193 -0
- data/lib/rubydb/wal/wal.rb +452 -0
- data/lib/rubydb/wal/writer.rb +236 -0
- data/lib/rubydb.rb +7 -0
- data/packaging/docker/Dockerfile +36 -0
- data/packaging/docker/docker-compose.failover.yml +43 -0
- data/packaging/docker/entrypoint.sh +15 -0
- data/packaging/homebrew/rubydb.rb +19 -0
- data/packaging/systemd/rubydb.service +22 -0
- data/rubydb.gemspec +57 -0
- data/scripts/benchmark +7 -0
- data/scripts/durability_drill +37 -0
- data/scripts/format +0 -0
- data/scripts/fuzz +63 -0
- data/scripts/lint +0 -0
- data/scripts/release +50 -0
- data/scripts/release_check +43 -0
- data/scripts/replication_failover_drill +250 -0
- data/scripts/replication_network_failover_drill +255 -0
- data/scripts/restore_drill +45 -0
- data/scripts/security +0 -0
- data/scripts/setup +0 -0
- data/scripts/test +0 -0
- data/scripts/test-chaos +0 -0
- data/scripts/test-crash +0 -0
- data/scripts/test-integration +0 -0
- data/scripts/test-unit +0 -0
- data/visibility_map.json +1 -0
- metadata +538 -0
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module RubyDB
|
|
9
|
+
module Storage
|
|
10
|
+
# VisibilityMap - Tracks which transactions can see which rows
|
|
11
|
+
# Implements MVCC visibility with proper isolation levels
|
|
12
|
+
class VisibilityMap
|
|
13
|
+
# Visibility states for a row version
|
|
14
|
+
VISIBLE = :visible
|
|
15
|
+
HIDDEN = :hidden
|
|
16
|
+
DELETED = :deleted
|
|
17
|
+
COMMITTED = :committed
|
|
18
|
+
ABORTED = :aborted
|
|
19
|
+
IN_PROGRESS = :in_progress
|
|
20
|
+
|
|
21
|
+
attr_reader :visibility_info, :stats, :isolation_level, :active_transactions, :committed_transactions
|
|
22
|
+
|
|
23
|
+
def initialize(page_manager, config = {})
|
|
24
|
+
@page_manager = page_manager
|
|
25
|
+
@visibility_info = {}
|
|
26
|
+
@transaction_visibility = {}
|
|
27
|
+
@snapshot_cache = {}
|
|
28
|
+
@snapshot_cache_lru = [] # LRU order for cache eviction
|
|
29
|
+
@max_snapshot_cache_size = config[:max_snapshot_cache] || 100
|
|
30
|
+
@commit_log = []
|
|
31
|
+
@abort_log = []
|
|
32
|
+
@max_snapshot_age = config[:max_snapshot_age] || 3600 # 1 hour default
|
|
33
|
+
@isolation_level = config[:isolation_level] || :read_committed
|
|
34
|
+
@configured_visibility_path = config[:visibility_path]
|
|
35
|
+
@vacuum_threshold = config[:vacuum_threshold] || 1000
|
|
36
|
+
@vacuum_batch_size = config[:vacuum_batch_size] || 100
|
|
37
|
+
@stats = {
|
|
38
|
+
visible_checks: 0,
|
|
39
|
+
hidden_checks: 0,
|
|
40
|
+
snapshot_creations: 0,
|
|
41
|
+
snapshot_cache_hits: 0,
|
|
42
|
+
snapshot_cache_misses: 0,
|
|
43
|
+
vacuum_runs: 0,
|
|
44
|
+
vacuum_removed: 0,
|
|
45
|
+
old_snapshots_cleared: 0,
|
|
46
|
+
snapshot_evictions: 0,
|
|
47
|
+
transaction_rollbacks: 0,
|
|
48
|
+
row_version_chains: 0
|
|
49
|
+
}
|
|
50
|
+
@lock = Mutex.new
|
|
51
|
+
@vacuum_lock = Mutex.new
|
|
52
|
+
@active_transactions = {}
|
|
53
|
+
@committed_transactions = Set.new
|
|
54
|
+
@aborted_transactions = Set.new
|
|
55
|
+
@row_version_chains = {}
|
|
56
|
+
@version_history = Hash.new { |hash, row_id| hash[row_id] = {} }
|
|
57
|
+
@next_version_id = 1
|
|
58
|
+
@is_loaded = false
|
|
59
|
+
|
|
60
|
+
# Create directory for visibility data
|
|
61
|
+
@data_dir = config[:data_dir] || "."
|
|
62
|
+
FileUtils.mkdir_p(@data_dir) unless Dir.exist?(@data_dir)
|
|
63
|
+
|
|
64
|
+
load_visibility
|
|
65
|
+
start_vacuum_thread if config[:auto_vacuum] != false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Mark a row as visible to a specific transaction
|
|
69
|
+
def mark_visible(row_id, transaction_id, commit_id = nil)
|
|
70
|
+
@lock.synchronize do
|
|
71
|
+
@stats[:visible_checks] += 1
|
|
72
|
+
|
|
73
|
+
version_id = @next_version_id
|
|
74
|
+
@next_version_id += 1
|
|
75
|
+
|
|
76
|
+
existing = @visibility_info[row_id]
|
|
77
|
+
prev_version = existing ? existing[:version] : 0
|
|
78
|
+
prev_chain = existing ? @row_version_chains[row_id] : nil
|
|
79
|
+
|
|
80
|
+
@visibility_info[row_id] = {
|
|
81
|
+
state: VISIBLE,
|
|
82
|
+
visible_to: transaction_id,
|
|
83
|
+
commit_id: commit_id || (transaction_id.to_i == 0 ? 0 : nil),
|
|
84
|
+
created_at: Time.now,
|
|
85
|
+
last_modified: Time.now,
|
|
86
|
+
version: version_id,
|
|
87
|
+
prev_version: prev_version,
|
|
88
|
+
row_id: row_id,
|
|
89
|
+
deleted: false
|
|
90
|
+
}
|
|
91
|
+
remember_version(row_id, @visibility_info[row_id])
|
|
92
|
+
|
|
93
|
+
# Track version chain
|
|
94
|
+
@row_version_chains[row_id] ||= []
|
|
95
|
+
@row_version_chains[row_id] << version_id
|
|
96
|
+
|
|
97
|
+
if @row_version_chains[row_id].size > 100
|
|
98
|
+
# Trim old version chains to prevent unbounded growth
|
|
99
|
+
@row_version_chains[row_id] = @row_version_chains[row_id].last(50)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Mark a row as hidden from a specific transaction (deleted or updated)
|
|
107
|
+
def mark_hidden(row_id, transaction_id, commit_id = nil)
|
|
108
|
+
@lock.synchronize do
|
|
109
|
+
@stats[:hidden_checks] += 1
|
|
110
|
+
|
|
111
|
+
version_id = @next_version_id
|
|
112
|
+
@next_version_id += 1
|
|
113
|
+
|
|
114
|
+
existing = @visibility_info[row_id]
|
|
115
|
+
prev_version = existing ? existing[:version] : 0
|
|
116
|
+
|
|
117
|
+
@visibility_info[row_id] = {
|
|
118
|
+
state: HIDDEN,
|
|
119
|
+
hidden_from: transaction_id,
|
|
120
|
+
commit_id: commit_id || (transaction_id.to_i == 0 ? 0 : nil),
|
|
121
|
+
created_at: Time.now,
|
|
122
|
+
last_modified: Time.now,
|
|
123
|
+
version: version_id,
|
|
124
|
+
prev_version: prev_version,
|
|
125
|
+
row_id: row_id,
|
|
126
|
+
deleted: false
|
|
127
|
+
}
|
|
128
|
+
remember_version(row_id, @visibility_info[row_id])
|
|
129
|
+
|
|
130
|
+
@row_version_chains[row_id] ||= []
|
|
131
|
+
@row_version_chains[row_id] << version_id
|
|
132
|
+
|
|
133
|
+
true
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Mark a row as deleted (final state)
|
|
138
|
+
def mark_deleted(row_id, transaction_id, commit_id = nil)
|
|
139
|
+
@lock.synchronize do
|
|
140
|
+
version_id = @next_version_id
|
|
141
|
+
@next_version_id += 1
|
|
142
|
+
|
|
143
|
+
existing = @visibility_info[row_id]
|
|
144
|
+
prev_version = existing ? existing[:version] : 0
|
|
145
|
+
|
|
146
|
+
@visibility_info[row_id] = {
|
|
147
|
+
state: DELETED,
|
|
148
|
+
deleted_by: transaction_id,
|
|
149
|
+
commit_id: commit_id || (transaction_id.to_i == 0 ? 0 : nil),
|
|
150
|
+
created_at: Time.now,
|
|
151
|
+
last_modified: Time.now,
|
|
152
|
+
version: version_id,
|
|
153
|
+
prev_version: prev_version,
|
|
154
|
+
row_id: row_id,
|
|
155
|
+
deleted: true
|
|
156
|
+
}
|
|
157
|
+
remember_version(row_id, @visibility_info[row_id])
|
|
158
|
+
|
|
159
|
+
@row_version_chains[row_id] ||= []
|
|
160
|
+
@row_version_chains[row_id] << version_id
|
|
161
|
+
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check if a row is visible to a transaction
|
|
167
|
+
def is_visible?(row_id, transaction_id, snapshot_id = nil)
|
|
168
|
+
@lock.synchronize do
|
|
169
|
+
@stats[:visible_checks] += 1
|
|
170
|
+
|
|
171
|
+
info = @visibility_info[row_id]
|
|
172
|
+
|
|
173
|
+
# Rows created before visibility tracking was introduced remain
|
|
174
|
+
# visible; newly written rows always receive visibility metadata.
|
|
175
|
+
return true if info.nil?
|
|
176
|
+
|
|
177
|
+
# Check if row is deleted
|
|
178
|
+
return false if info[:state] == DELETED && info[:deleted_by] != transaction_id
|
|
179
|
+
|
|
180
|
+
# Check if there's a snapshot for this transaction
|
|
181
|
+
if snapshot_id && @snapshot_cache.key?(snapshot_id)
|
|
182
|
+
@stats[:snapshot_cache_hits] += 1
|
|
183
|
+
snapshot = @snapshot_cache[snapshot_id]
|
|
184
|
+
update_lru(snapshot_id)
|
|
185
|
+
return snapshot_visibility_check(info, snapshot, transaction_id)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Check state-based visibility
|
|
189
|
+
case info[:state]
|
|
190
|
+
when VISIBLE, COMMITTED
|
|
191
|
+
# Check if visible_to is set
|
|
192
|
+
if info[:visible_to]
|
|
193
|
+
# Row is visible to this transaction if:
|
|
194
|
+
# 1. Transaction_id matches the visible_to
|
|
195
|
+
# 2. Or transaction_id is after commit_id (committed)
|
|
196
|
+
# 3. Or transaction is active and owns the visibility
|
|
197
|
+
if info[:visible_to] == transaction_id
|
|
198
|
+
return true
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if info[:commit_id]
|
|
202
|
+
return true if info[:commit_id].to_i == 0
|
|
203
|
+
return true if @committed_transactions.include?(info[:commit_id])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
return false
|
|
207
|
+
end
|
|
208
|
+
true
|
|
209
|
+
|
|
210
|
+
when HIDDEN
|
|
211
|
+
# Row is hidden from this transaction if:
|
|
212
|
+
# 1. hidden_from matches transaction_id (same transaction hid it)
|
|
213
|
+
# 2. Or commit_id > transaction_id (committed after transaction started)
|
|
214
|
+
# 3. Or transaction is the one that hid it
|
|
215
|
+
if info[:hidden_from]
|
|
216
|
+
return false if info[:hidden_from] == transaction_id
|
|
217
|
+
|
|
218
|
+
if info[:commit_id] && info[:commit_id] > transaction_id
|
|
219
|
+
return false
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
true
|
|
223
|
+
|
|
224
|
+
when DELETED
|
|
225
|
+
# Deleted rows are not visible to anyone except the transaction that deleted them
|
|
226
|
+
return info[:deleted_by] == transaction_id if info[:deleted_by]
|
|
227
|
+
false
|
|
228
|
+
|
|
229
|
+
else
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Check visibility using a snapshot
|
|
236
|
+
def snapshot_visibility_check(info, snapshot, transaction_id)
|
|
237
|
+
snapshot_start = snapshot[:start_time]
|
|
238
|
+
snapshot_active = snapshot[:active_transactions]
|
|
239
|
+
snapshot_committed = snapshot[:committed_transactions] || []
|
|
240
|
+
|
|
241
|
+
# If row was created after snapshot, it's not visible
|
|
242
|
+
if info[:created_at] > snapshot_start
|
|
243
|
+
return false
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# If row was modified after snapshot, check if it's visible
|
|
247
|
+
if info[:last_modified] > snapshot_start
|
|
248
|
+
# Check if the modifying transaction is in the snapshot's active list
|
|
249
|
+
if info[:commit_id]
|
|
250
|
+
# If commit_id is in active transactions when snapshot was taken, it's not visible
|
|
251
|
+
return false if snapshot_active.include?(info[:commit_id])
|
|
252
|
+
|
|
253
|
+
# If commit_id was committed after snapshot, it's not visible
|
|
254
|
+
return false if snapshot_committed.include?(info[:commit_id])
|
|
255
|
+
|
|
256
|
+
# If commit_id is the current transaction, it might be visible
|
|
257
|
+
return true if info[:commit_id] == transaction_id
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Check if this is the current transaction's own changes
|
|
261
|
+
if info[:visible_to] == transaction_id || info[:hidden_from] == transaction_id
|
|
262
|
+
return true
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
return false
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Row was created before snapshot and not modified after
|
|
269
|
+
true
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Create a snapshot for a transaction
|
|
273
|
+
def create_snapshot(transaction_id)
|
|
274
|
+
@lock.synchronize do
|
|
275
|
+
@stats[:snapshot_creations] += 1
|
|
276
|
+
|
|
277
|
+
snapshot_id = "snapshot_#{transaction_id}_#{Time.now.to_i}_#{@stats[:snapshot_creations]}"
|
|
278
|
+
|
|
279
|
+
snapshot = {
|
|
280
|
+
id: snapshot_id,
|
|
281
|
+
transaction_id: transaction_id,
|
|
282
|
+
start_time: Time.now,
|
|
283
|
+
active_transactions: @active_transactions.keys.dup,
|
|
284
|
+
committed_transactions: @committed_transactions.to_a.dup,
|
|
285
|
+
isolation_level: @isolation_level,
|
|
286
|
+
created_at: Time.now,
|
|
287
|
+
last_access: Time.now
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Store snapshot with LRU management
|
|
291
|
+
if @snapshot_cache.size >= @max_snapshot_cache_size
|
|
292
|
+
evict_oldest_snapshot
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
@snapshot_cache[snapshot_id] = snapshot
|
|
296
|
+
@snapshot_cache_lru.unshift(snapshot_id)
|
|
297
|
+
|
|
298
|
+
# Clean old snapshots
|
|
299
|
+
clean_old_snapshots
|
|
300
|
+
|
|
301
|
+
snapshot_id
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Get a snapshot by ID
|
|
306
|
+
def get_snapshot(snapshot_id)
|
|
307
|
+
@lock.synchronize do
|
|
308
|
+
snapshot = @snapshot_cache[snapshot_id]
|
|
309
|
+
update_lru(snapshot_id) if snapshot
|
|
310
|
+
snapshot
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Register an active transaction
|
|
315
|
+
def register_transaction(transaction_id)
|
|
316
|
+
@lock.synchronize do
|
|
317
|
+
@active_transactions[transaction_id] = {
|
|
318
|
+
started_at: Time.now,
|
|
319
|
+
status: :active,
|
|
320
|
+
visibility: {},
|
|
321
|
+
row_locks: Set.new,
|
|
322
|
+
snapshot_id: nil
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Stronger isolation levels are rejected by Engine until historical
|
|
326
|
+
# row versions are persisted. Do not create a snapshot here while
|
|
327
|
+
# holding @lock; create_snapshot also takes this lock.
|
|
328
|
+
|
|
329
|
+
true
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Commit a transaction
|
|
334
|
+
def commit_transaction(transaction_id, commit_id = nil)
|
|
335
|
+
@lock.synchronize do
|
|
336
|
+
commit_id ||= transaction_id
|
|
337
|
+
|
|
338
|
+
# Get transaction's snapshot
|
|
339
|
+
tx_info = @active_transactions[transaction_id]
|
|
340
|
+
snapshot_id = tx_info ? tx_info[:snapshot_id] : nil
|
|
341
|
+
|
|
342
|
+
# Update all visibility info committed by this transaction
|
|
343
|
+
updated_rows = []
|
|
344
|
+
|
|
345
|
+
@visibility_info.each do |row_id, info|
|
|
346
|
+
if info[:visible_to] == transaction_id ||
|
|
347
|
+
info[:hidden_from] == transaction_id ||
|
|
348
|
+
info[:deleted_by] == transaction_id
|
|
349
|
+
|
|
350
|
+
# Mark as committed
|
|
351
|
+
info[:commit_id] = commit_id
|
|
352
|
+
info[:state] = COMMITTED if info[:state] == VISIBLE || info[:state] == HIDDEN
|
|
353
|
+
info[:last_modified] = Time.now
|
|
354
|
+
updated_rows << row_id
|
|
355
|
+
|
|
356
|
+
# Update version chain
|
|
357
|
+
if @row_version_chains[row_id]
|
|
358
|
+
@row_version_chains[row_id] << info[:version]
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Move to committed transactions
|
|
364
|
+
@committed_transactions.add(transaction_id)
|
|
365
|
+
@active_transactions.delete(transaction_id)
|
|
366
|
+
|
|
367
|
+
@commit_log << {
|
|
368
|
+
transaction_id: transaction_id,
|
|
369
|
+
commit_id: commit_id,
|
|
370
|
+
time: Time.now,
|
|
371
|
+
rows_updated: updated_rows.size,
|
|
372
|
+
snapshot_id: snapshot_id
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# Clean commit log if too large
|
|
376
|
+
if @commit_log.size > 1000
|
|
377
|
+
@commit_log = @commit_log.last(500)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Remove transaction's snapshot
|
|
381
|
+
if snapshot_id
|
|
382
|
+
@snapshot_cache.delete(snapshot_id)
|
|
383
|
+
@snapshot_cache_lru.delete(snapshot_id)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Return commit info
|
|
387
|
+
{
|
|
388
|
+
transaction_id: transaction_id,
|
|
389
|
+
commit_id: commit_id,
|
|
390
|
+
rows_updated: updated_rows.size,
|
|
391
|
+
committed_at: Time.now
|
|
392
|
+
}
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Abort a transaction
|
|
397
|
+
def abort_transaction(transaction_id)
|
|
398
|
+
@lock.synchronize do
|
|
399
|
+
@stats[:transaction_rollbacks] += 1
|
|
400
|
+
|
|
401
|
+
# Get transaction's snapshot
|
|
402
|
+
tx_info = @active_transactions[transaction_id]
|
|
403
|
+
snapshot_id = tx_info ? tx_info[:snapshot_id] : nil
|
|
404
|
+
|
|
405
|
+
# Track which rows were affected
|
|
406
|
+
affected_rows = []
|
|
407
|
+
|
|
408
|
+
# Revert visibility changes made by this transaction
|
|
409
|
+
@visibility_info.each do |row_id, info|
|
|
410
|
+
if info[:visible_to] == transaction_id ||
|
|
411
|
+
info[:hidden_from] == transaction_id ||
|
|
412
|
+
info[:deleted_by] == transaction_id
|
|
413
|
+
|
|
414
|
+
affected_rows << row_id
|
|
415
|
+
|
|
416
|
+
# Revert to previous version if exists
|
|
417
|
+
prev_version = info[:prev_version]
|
|
418
|
+
|
|
419
|
+
if prev_version > 0
|
|
420
|
+
# Find previous version in version chain
|
|
421
|
+
if @row_version_chains[row_id]
|
|
422
|
+
prev_info = find_version_by_id(row_id, prev_version)
|
|
423
|
+
if prev_info
|
|
424
|
+
# Restore previous state
|
|
425
|
+
@visibility_info[row_id] = prev_info
|
|
426
|
+
else
|
|
427
|
+
# Remove completely if previous version not found
|
|
428
|
+
@visibility_info.delete(row_id)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
else
|
|
432
|
+
# No previous version - remove completely
|
|
433
|
+
@visibility_info.delete(row_id)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Move to aborted transactions
|
|
439
|
+
@aborted_transactions.add(transaction_id)
|
|
440
|
+
@active_transactions.delete(transaction_id)
|
|
441
|
+
|
|
442
|
+
@abort_log << {
|
|
443
|
+
transaction_id: transaction_id,
|
|
444
|
+
time: Time.now,
|
|
445
|
+
rows_affected: affected_rows.size,
|
|
446
|
+
snapshot_id: snapshot_id
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# Clean abort log if too large
|
|
450
|
+
if @abort_log.size > 1000
|
|
451
|
+
@abort_log = @abort_log.last(500)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Remove transaction's snapshot
|
|
455
|
+
if snapshot_id
|
|
456
|
+
@snapshot_cache.delete(snapshot_id)
|
|
457
|
+
@snapshot_cache_lru.delete(snapshot_id)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
affected_rows.size
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Find a version by ID in the version chain
|
|
465
|
+
def find_version_by_id(row_id, version_id)
|
|
466
|
+
history = @version_history[row_id]
|
|
467
|
+
history[version_id.to_i] || begin
|
|
468
|
+
info = @visibility_info[row_id]
|
|
469
|
+
info if info && info[:version].to_i == version_id.to_i
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Check if a transaction is active
|
|
474
|
+
def transaction_active?(transaction_id)
|
|
475
|
+
@active_transactions.key?(transaction_id)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Check if a transaction is committed
|
|
479
|
+
def transaction_committed?(transaction_id)
|
|
480
|
+
@committed_transactions.include?(transaction_id)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Check if a transaction is aborted
|
|
484
|
+
def transaction_aborted?(transaction_id)
|
|
485
|
+
@aborted_transactions.include?(transaction_id)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Get all rows visible to a transaction
|
|
489
|
+
def transaction_visible_rows(transaction_id)
|
|
490
|
+
@lock.synchronize do
|
|
491
|
+
result = []
|
|
492
|
+
@visibility_info.each do |row_id, info|
|
|
493
|
+
if is_visible?(row_id, transaction_id)
|
|
494
|
+
result << row_id
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
result
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Get all rows hidden from a transaction
|
|
502
|
+
def transaction_hidden_rows(transaction_id)
|
|
503
|
+
@lock.synchronize do
|
|
504
|
+
result = []
|
|
505
|
+
@visibility_info.each do |row_id, info|
|
|
506
|
+
unless is_visible?(row_id, transaction_id)
|
|
507
|
+
result << row_id
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
result
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Get row visibility history
|
|
515
|
+
def row_history(row_id)
|
|
516
|
+
@lock.synchronize do
|
|
517
|
+
info = @visibility_info[row_id]
|
|
518
|
+
return [] unless info
|
|
519
|
+
|
|
520
|
+
history = [info.dup]
|
|
521
|
+
|
|
522
|
+
# Follow version chain backwards
|
|
523
|
+
current = info
|
|
524
|
+
while current && current[:prev_version] > 0
|
|
525
|
+
prev_info = find_version_by_id(row_id, current[:prev_version])
|
|
526
|
+
if prev_info
|
|
527
|
+
history << prev_info.dup
|
|
528
|
+
current = prev_info
|
|
529
|
+
else
|
|
530
|
+
break
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
history.reverse
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Get row version history
|
|
539
|
+
def row_versions(row_id)
|
|
540
|
+
@lock.synchronize do
|
|
541
|
+
info = @visibility_info[row_id]
|
|
542
|
+
return [] unless info
|
|
543
|
+
|
|
544
|
+
versions = []
|
|
545
|
+
current = info
|
|
546
|
+
|
|
547
|
+
while current
|
|
548
|
+
versions << {
|
|
549
|
+
version: current[:version],
|
|
550
|
+
state: current[:state],
|
|
551
|
+
transaction_id: current[:visible_to] || current[:hidden_from] || current[:deleted_by],
|
|
552
|
+
created_at: current[:created_at],
|
|
553
|
+
commit_id: current[:commit_id]
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
break if current[:prev_version] == 0
|
|
557
|
+
|
|
558
|
+
current = find_version_by_id(row_id, current[:prev_version])
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
versions
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Vacuum old/deleted rows
|
|
566
|
+
def vacuum(max_age = @vacuum_threshold, batch_size = @vacuum_batch_size)
|
|
567
|
+
@vacuum_lock.synchronize do
|
|
568
|
+
@stats[:vacuum_runs] += 1
|
|
569
|
+
|
|
570
|
+
removed = 0
|
|
571
|
+
now = Time.now
|
|
572
|
+
rows_to_remove = []
|
|
573
|
+
|
|
574
|
+
# Collect rows to vacuum
|
|
575
|
+
@visibility_info.each do |row_id, info|
|
|
576
|
+
should_remove = false
|
|
577
|
+
|
|
578
|
+
case info[:state]
|
|
579
|
+
when DELETED
|
|
580
|
+
age = now - info[:last_modified]
|
|
581
|
+
should_remove = age > max_age && !needed_for_rollback?(row_id)
|
|
582
|
+
|
|
583
|
+
when COMMITTED
|
|
584
|
+
# Check if all transactions that need this version are gone
|
|
585
|
+
should_remove = committed_version_expired?(info)
|
|
586
|
+
|
|
587
|
+
when ABORTED
|
|
588
|
+
should_remove = true
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
if should_remove
|
|
592
|
+
rows_to_remove << row_id
|
|
593
|
+
removed += 1
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
break if removed >= batch_size
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Remove rows
|
|
600
|
+
rows_to_remove.each do |row_id|
|
|
601
|
+
@visibility_info.delete(row_id)
|
|
602
|
+
@row_version_chains.delete(row_id)
|
|
603
|
+
remove_row_from_disk(row_id)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
@stats[:vacuum_removed] += removed
|
|
607
|
+
|
|
608
|
+
# Clean up old snapshots and transactions
|
|
609
|
+
clean_old_snapshots
|
|
610
|
+
clean_old_transactions
|
|
611
|
+
|
|
612
|
+
# Flush if we removed a significant number of rows
|
|
613
|
+
if removed > 0
|
|
614
|
+
flush
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
{
|
|
618
|
+
removed: removed,
|
|
619
|
+
total_rows: @visibility_info.size,
|
|
620
|
+
total_vacuumed: @stats[:vacuum_removed]
|
|
621
|
+
}
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Check if a deleted row is needed for potential rollback
|
|
626
|
+
def needed_for_rollback?(row_id)
|
|
627
|
+
# Check if any active transaction might need to rollback this row
|
|
628
|
+
@active_transactions.each do |tx_id, _info|
|
|
629
|
+
# If a transaction modified this row, it might need to rollback
|
|
630
|
+
if @visibility_info[row_id] &&
|
|
631
|
+
(@visibility_info[row_id][:visible_to] == tx_id ||
|
|
632
|
+
@visibility_info[row_id][:hidden_from] == tx_id ||
|
|
633
|
+
@visibility_info[row_id][:deleted_by] == tx_id)
|
|
634
|
+
return true
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
false
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# Check if a committed version has expired
|
|
641
|
+
def committed_version_expired?(info)
|
|
642
|
+
return true if @active_transactions.empty?
|
|
643
|
+
|
|
644
|
+
# If no active transactions need this version
|
|
645
|
+
min_active = @active_transactions.keys.min || 0
|
|
646
|
+
info[:commit_id] && info[:commit_id] < min_active
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Load visibility map from disk
|
|
650
|
+
def load_visibility
|
|
651
|
+
@lock.synchronize do
|
|
652
|
+
@visibility_info.clear
|
|
653
|
+
@active_transactions.clear
|
|
654
|
+
@committed_transactions.clear
|
|
655
|
+
@aborted_transactions.clear
|
|
656
|
+
@snapshot_cache.clear
|
|
657
|
+
@snapshot_cache_lru.clear
|
|
658
|
+
@row_version_chains.clear
|
|
659
|
+
@version_history.clear
|
|
660
|
+
|
|
661
|
+
# Try to load from storage
|
|
662
|
+
begin
|
|
663
|
+
if File.exist?(visibility_path)
|
|
664
|
+
data = File.read(visibility_path)
|
|
665
|
+
parsed = JSON.parse(data, symbolize_names: true)
|
|
666
|
+
|
|
667
|
+
if parsed[:visibility_info]
|
|
668
|
+
parsed[:visibility_info].each do |row_id_str, info|
|
|
669
|
+
row_id = row_id_str.to_s.to_i
|
|
670
|
+
symbolized_info = {}
|
|
671
|
+
info.each do |key, value|
|
|
672
|
+
value = value.to_sym if %i[state].include?(key.to_sym) && value.respond_to?(:to_sym)
|
|
673
|
+
symbolized_info[key.to_sym] = value
|
|
674
|
+
end
|
|
675
|
+
@visibility_info[row_id] = symbolized_info
|
|
676
|
+
remember_version(row_id, symbolized_info)
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
if parsed[:version_history]
|
|
681
|
+
parsed[:version_history].each do |row_id_str, versions|
|
|
682
|
+
versions.each do |version_id, info|
|
|
683
|
+
normalized = normalize_loaded_info(info)
|
|
684
|
+
@version_history[row_id_str.to_s.to_i][version_id.to_s.to_i] = normalized
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
@active_transactions = parsed[:active_transactions] || {}
|
|
690
|
+
@committed_transactions = Set.new(parsed[:committed_transactions] || [])
|
|
691
|
+
@aborted_transactions = Set.new(parsed[:aborted_transactions] || [])
|
|
692
|
+
@next_version_id = parsed[:next_version_id] || 1
|
|
693
|
+
@row_version_chains = parsed[:row_version_chains] || {}
|
|
694
|
+
|
|
695
|
+
# Clean up any invalid data
|
|
696
|
+
@active_transactions.each do |tx_id, info|
|
|
697
|
+
if info[:started_at]
|
|
698
|
+
info[:started_at] = Time.parse(info[:started_at]) if info[:started_at].is_a?(String)
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
@is_loaded = true
|
|
703
|
+
end
|
|
704
|
+
rescue => e
|
|
705
|
+
# Never discard persisted visibility state silently; a corrupt or
|
|
706
|
+
# incompatible MVCC map must fail closed so recovery can intervene.
|
|
707
|
+
raise RubyDB::CorruptionError, "Failed to load visibility map: #{e.message}"
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# Flush visibility map to disk
|
|
713
|
+
def flush
|
|
714
|
+
@lock.synchronize do
|
|
715
|
+
begin
|
|
716
|
+
data = {
|
|
717
|
+
visibility_info: @visibility_info,
|
|
718
|
+
active_transactions: @active_transactions,
|
|
719
|
+
committed_transactions: @committed_transactions.to_a,
|
|
720
|
+
aborted_transactions: @aborted_transactions.to_a,
|
|
721
|
+
next_version_id: @next_version_id,
|
|
722
|
+
row_version_chains: @row_version_chains,
|
|
723
|
+
version_history: @version_history,
|
|
724
|
+
timestamp: Time.now.iso8601,
|
|
725
|
+
version: 2
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
# Write to a temp file first, then rename
|
|
729
|
+
temp_path = "#{visibility_path}.tmp"
|
|
730
|
+
File.write(temp_path, JSON.generate(data))
|
|
731
|
+
FileUtils.mv(temp_path, visibility_path)
|
|
732
|
+
|
|
733
|
+
true
|
|
734
|
+
rescue => e
|
|
735
|
+
false
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Remove a row from disk storage
|
|
741
|
+
def remove_row_from_disk(row_id)
|
|
742
|
+
true
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def remember_version(row_id, info)
|
|
746
|
+
@version_history[row_id][info[:version].to_i] = info.dup
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def normalize_loaded_info(info)
|
|
750
|
+
info.each_with_object({}) do |(key, value), normalized|
|
|
751
|
+
normalized[key.to_sym] = if key.to_sym == :state && value.respond_to?(:to_sym)
|
|
752
|
+
value.to_sym
|
|
753
|
+
elsif %i[created_at last_modified].include?(key.to_sym) && value.is_a?(String)
|
|
754
|
+
Time.parse(value)
|
|
755
|
+
else
|
|
756
|
+
value
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# Get visibility statistics
|
|
762
|
+
def stats
|
|
763
|
+
@lock.synchronize do
|
|
764
|
+
# Calculate average version chain length
|
|
765
|
+
avg_chain = if @row_version_chains.empty?
|
|
766
|
+
0.0
|
|
767
|
+
else
|
|
768
|
+
@row_version_chains.values.map(&:size).sum.to_f / @row_version_chains.size
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
{
|
|
772
|
+
total_rows: @visibility_info.size,
|
|
773
|
+
visible_rows: @visibility_info.count { |_, info| info[:state] == VISIBLE || info[:state] == COMMITTED },
|
|
774
|
+
hidden_rows: @visibility_info.count { |_, info| info[:state] == HIDDEN },
|
|
775
|
+
deleted_rows: @visibility_info.count { |_, info| info[:state] == DELETED },
|
|
776
|
+
active_transactions: @active_transactions.size,
|
|
777
|
+
committed_transactions: @committed_transactions.size,
|
|
778
|
+
aborted_transactions: @aborted_transactions.size,
|
|
779
|
+
snapshots: @snapshot_cache.size,
|
|
780
|
+
version_chains: @row_version_chains.size,
|
|
781
|
+
avg_chain_length: avg_chain.round(2),
|
|
782
|
+
visible_checks: @stats[:visible_checks],
|
|
783
|
+
hidden_checks: @stats[:hidden_checks],
|
|
784
|
+
snapshot_creations: @stats[:snapshot_creations],
|
|
785
|
+
snapshot_cache_hits: @stats[:snapshot_cache_hits],
|
|
786
|
+
snapshot_cache_misses: @stats[:snapshot_cache_misses],
|
|
787
|
+
snapshot_cache_hit_rate: snapshot_hit_rate,
|
|
788
|
+
vacuum_runs: @stats[:vacuum_runs],
|
|
789
|
+
vacuum_removed: @stats[:vacuum_removed],
|
|
790
|
+
old_snapshots_cleared: @stats[:old_snapshots_cleared],
|
|
791
|
+
snapshot_evictions: @stats[:snapshot_evictions],
|
|
792
|
+
transaction_rollbacks: @stats[:transaction_rollbacks],
|
|
793
|
+
isolation_level: @isolation_level,
|
|
794
|
+
next_version_id: @next_version_id,
|
|
795
|
+
is_loaded: @is_loaded
|
|
796
|
+
}
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Clean up old snapshots
|
|
801
|
+
def clean_old_snapshots
|
|
802
|
+
@lock.synchronize do
|
|
803
|
+
now = Time.now
|
|
804
|
+
to_remove = []
|
|
805
|
+
|
|
806
|
+
@snapshot_cache.each do |id, snapshot|
|
|
807
|
+
age = now - snapshot[:created_at]
|
|
808
|
+
if age > @max_snapshot_age
|
|
809
|
+
to_remove << id
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
to_remove.each do |id|
|
|
814
|
+
@snapshot_cache.delete(id)
|
|
815
|
+
@snapshot_cache_lru.delete(id)
|
|
816
|
+
@stats[:old_snapshots_cleared] += 1
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
# Clean up old transaction records
|
|
822
|
+
def clean_old_transactions
|
|
823
|
+
@lock.synchronize do
|
|
824
|
+
now = Time.now
|
|
825
|
+
cutoff = now - 86400 # 24 hours
|
|
826
|
+
|
|
827
|
+
@active_transactions.each do |tx_id, info|
|
|
828
|
+
if info[:started_at] < cutoff
|
|
829
|
+
@active_transactions.delete(tx_id)
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# Clean up old committed transactions
|
|
834
|
+
old_committed = @committed_transactions.to_a.select do |tx_id|
|
|
835
|
+
# Check if transaction is older than cutoff
|
|
836
|
+
# For simplicity, we'll just keep recent ones
|
|
837
|
+
false
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# Keep committed transactions that might still be needed
|
|
841
|
+
@committed_transactions = Set.new(@committed_transactions.to_a.last(1000))
|
|
842
|
+
end
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
# Start vacuum thread for automatic cleanup
|
|
846
|
+
def start_vacuum_thread
|
|
847
|
+
Thread.new do
|
|
848
|
+
loop do
|
|
849
|
+
sleep(300) # Run every 5 minutes
|
|
850
|
+
begin
|
|
851
|
+
vacuum
|
|
852
|
+
rescue => e
|
|
853
|
+
# Log error but continue
|
|
854
|
+
puts "Vacuum error: #{e.message}" if $DEBUG
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Set isolation level
|
|
861
|
+
def isolation_level=(level)
|
|
862
|
+
@lock.synchronize do
|
|
863
|
+
@isolation_level = level
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
# Get row visibility info
|
|
868
|
+
def get_visibility_info(row_id)
|
|
869
|
+
@lock.synchronize do
|
|
870
|
+
@visibility_info[row_id]
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Check if a row exists
|
|
875
|
+
def row_exists?(row_id)
|
|
876
|
+
@lock.synchronize do
|
|
877
|
+
@visibility_info.key?(row_id)
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Get active transaction count
|
|
882
|
+
def active_transaction_count
|
|
883
|
+
@lock.synchronize do
|
|
884
|
+
@active_transactions.size
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# Get committed transaction count
|
|
889
|
+
def committed_transaction_count
|
|
890
|
+
@lock.synchronize do
|
|
891
|
+
@committed_transactions.size
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# Reset the visibility map (dangerous - for testing)
|
|
896
|
+
def reset!
|
|
897
|
+
@lock.synchronize do
|
|
898
|
+
@visibility_info.clear
|
|
899
|
+
@active_transactions.clear
|
|
900
|
+
@committed_transactions.clear
|
|
901
|
+
@aborted_transactions.clear
|
|
902
|
+
@snapshot_cache.clear
|
|
903
|
+
@snapshot_cache_lru.clear
|
|
904
|
+
@commit_log.clear
|
|
905
|
+
@abort_log.clear
|
|
906
|
+
@row_version_chains.clear
|
|
907
|
+
@next_version_id = 1
|
|
908
|
+
@stats = {
|
|
909
|
+
visible_checks: 0,
|
|
910
|
+
hidden_checks: 0,
|
|
911
|
+
snapshot_creations: 0,
|
|
912
|
+
snapshot_cache_hits: 0,
|
|
913
|
+
snapshot_cache_misses: 0,
|
|
914
|
+
vacuum_runs: 0,
|
|
915
|
+
vacuum_removed: 0,
|
|
916
|
+
old_snapshots_cleared: 0,
|
|
917
|
+
snapshot_evictions: 0,
|
|
918
|
+
transaction_rollbacks: 0,
|
|
919
|
+
row_version_chains: 0
|
|
920
|
+
}
|
|
921
|
+
File.delete(visibility_path) if File.exist?(visibility_path)
|
|
922
|
+
true
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# to_s implementation
|
|
927
|
+
def to_s
|
|
928
|
+
"VisibilityMap: #{@visibility_info.size} entries, #{@active_transactions.size} active transactions, #{@snapshot_cache.size} snapshots"
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
private
|
|
932
|
+
|
|
933
|
+
# Update LRU order for a snapshot
|
|
934
|
+
def update_lru(snapshot_id)
|
|
935
|
+
@snapshot_cache_lru.delete(snapshot_id)
|
|
936
|
+
@snapshot_cache_lru.unshift(snapshot_id)
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# Evict the oldest snapshot from cache
|
|
940
|
+
def evict_oldest_snapshot
|
|
941
|
+
if @snapshot_cache_lru.any?
|
|
942
|
+
oldest_id = @snapshot_cache_lru.last
|
|
943
|
+
@snapshot_cache.delete(oldest_id)
|
|
944
|
+
@snapshot_cache_lru.pop
|
|
945
|
+
@stats[:snapshot_evictions] += 1
|
|
946
|
+
true
|
|
947
|
+
else
|
|
948
|
+
false
|
|
949
|
+
end
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
# Calculate snapshot hit rate
|
|
953
|
+
def snapshot_hit_rate
|
|
954
|
+
total = @stats[:snapshot_cache_hits] + @stats[:snapshot_cache_misses]
|
|
955
|
+
return 0.0 if total == 0
|
|
956
|
+
(@stats[:snapshot_cache_hits].to_f / total * 100).round(2)
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Path for visibility data file
|
|
960
|
+
def visibility_path
|
|
961
|
+
@visibility_path ||= begin
|
|
962
|
+
if @configured_visibility_path
|
|
963
|
+
FileUtils.mkdir_p(File.dirname(@configured_visibility_path))
|
|
964
|
+
@configured_visibility_path
|
|
965
|
+
elsif @page_manager && @page_manager.respond_to?(:path)
|
|
966
|
+
base_path = @page_manager.path
|
|
967
|
+
if base_path
|
|
968
|
+
File.join(@data_dir, "#{File.basename(base_path)}.visibility")
|
|
969
|
+
else
|
|
970
|
+
File.join(@data_dir, "visibility_map.json")
|
|
971
|
+
end
|
|
972
|
+
else
|
|
973
|
+
File.join(@data_dir, "visibility_map.json")
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# Check if a commit exists in the log
|
|
979
|
+
def commit_exists?(commit_id)
|
|
980
|
+
@commit_log.any? { |log| log[:commit_id] == commit_id }
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
end
|
|
984
|
+
end
|