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,887 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_record/connection_adapters/abstract_adapter"
|
|
5
|
+
require "active_record/connection_adapters/abstract/schema_definitions"
|
|
6
|
+
require "active_record/connection_adapters/abstract/schema_statements"
|
|
7
|
+
|
|
8
|
+
require "rubydb"
|
|
9
|
+
require "rubydb/rails/adapter"
|
|
10
|
+
require "rubydb/rails/connection"
|
|
11
|
+
require "rubydb/rails/database_statements"
|
|
12
|
+
require "rubydb/rails/schema_statements"
|
|
13
|
+
require "rubydb/rails/quoting"
|
|
14
|
+
require "rubydb/rails/type"
|
|
15
|
+
require "rubydb/rails/result"
|
|
16
|
+
|
|
17
|
+
module ActiveRecord
|
|
18
|
+
module ConnectionAdapters
|
|
19
|
+
# RubyDB adapter for ActiveRecord
|
|
20
|
+
class RubyDBAdapter < AbstractAdapter
|
|
21
|
+
include RubyDB::Rails::DatabaseStatements
|
|
22
|
+
include RubyDB::Rails::SchemaStatements
|
|
23
|
+
include RubyDB::Rails::Quoting
|
|
24
|
+
|
|
25
|
+
ADAPTER_NAME = "RubyDB"
|
|
26
|
+
|
|
27
|
+
# ActiveRecord 7.2 uses adapter-level methods while compiling hash-form
|
|
28
|
+
# order clauses (for example, `order(created_at: :desc)`). Keep these
|
|
29
|
+
# independent of a live connection so relation construction is safe
|
|
30
|
+
# during schema-cache and query setup as well.
|
|
31
|
+
def self.quote_table_name(name)
|
|
32
|
+
quote_column_name(name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.quote_column_name(name)
|
|
36
|
+
"\"#{name.to_s.gsub('"', '""')}\""
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
NATIVE_DATABASE_TYPES = {
|
|
40
|
+
primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
41
|
+
string: { name: "VARCHAR", limit: 255 },
|
|
42
|
+
text: { name: "TEXT" },
|
|
43
|
+
integer: { name: "INTEGER" },
|
|
44
|
+
bigint: { name: "BIGINT" },
|
|
45
|
+
smallint: { name: "SMALLINT" },
|
|
46
|
+
float: { name: "FLOAT" },
|
|
47
|
+
decimal: { name: "DECIMAL", precision: 10, scale: 2 },
|
|
48
|
+
datetime: { name: "TIMESTAMP" },
|
|
49
|
+
timestamp: { name: "TIMESTAMP" },
|
|
50
|
+
time: { name: "TIME" },
|
|
51
|
+
date: { name: "DATE" },
|
|
52
|
+
binary: { name: "BLOB" },
|
|
53
|
+
boolean: { name: "BOOLEAN" },
|
|
54
|
+
json: { name: "JSON" },
|
|
55
|
+
uuid: { name: "UUID" }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ActiveRecord 7.2 constructs adapters with a single configuration hash.
|
|
59
|
+
# Accept trailing deprecated arguments so applications upgrading from older
|
|
60
|
+
# ActiveRecord versions do not fail during connection establishment.
|
|
61
|
+
def initialize(config, *)
|
|
62
|
+
super(config)
|
|
63
|
+
|
|
64
|
+
@connection = RubyDB::Rails::Connection.new(config)
|
|
65
|
+
@connection.connect
|
|
66
|
+
|
|
67
|
+
@prepared_statements = {}
|
|
68
|
+
@transaction_depth = 0
|
|
69
|
+
@query_cache_enabled = false
|
|
70
|
+
@query_cache = {}
|
|
71
|
+
@statements = {}
|
|
72
|
+
@statement_counter = 0
|
|
73
|
+
# AbstractAdapter uses this monitor while creating transactions. It
|
|
74
|
+
# must be re-entrant because ActiveRecord acquires it recursively.
|
|
75
|
+
@lock = Monitor.new
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def adapter_name
|
|
79
|
+
ADAPTER_NAME
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def supports_migrations?
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def supports_primary_key?
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def supports_index_sort_order?
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def supports_transactions?
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def supports_savepoints?
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def supports_foreign_keys?
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def supports_views?
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def supports_json?
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def supports_uuid?
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def supports_bulk_alter?
|
|
119
|
+
false
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def native_database_types
|
|
123
|
+
NATIVE_DATABASE_TYPES
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ==================== SCHEMA METHODS ====================
|
|
127
|
+
|
|
128
|
+
def primary_key(table_name)
|
|
129
|
+
return @connection.engine.table_columns(table_name).find(&:primary_key?)&.name&.to_s || "id" if embedded?
|
|
130
|
+
|
|
131
|
+
result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
|
|
132
|
+
row = result.find { |r| r["pk"] == 1 }
|
|
133
|
+
row ? row["name"] : "id"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def tables
|
|
137
|
+
return @connection.engine.list_tables.map(&:to_s) if embedded?
|
|
138
|
+
|
|
139
|
+
result = execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
|
140
|
+
result.map { |row| row["name"] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def table_exists?(table_name)
|
|
144
|
+
tables.include?(table_name.to_s)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ActiveRecord's schema cache asks for these before a model is first
|
|
148
|
+
# instantiated. The embedded engine has catalog metadata already, so
|
|
149
|
+
# avoid generating unsupported SQLite catalog queries.
|
|
150
|
+
def data_sources
|
|
151
|
+
return tables if embedded?
|
|
152
|
+
|
|
153
|
+
super
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def data_source_exists?(name)
|
|
157
|
+
return table_exists?(name) if embedded?
|
|
158
|
+
|
|
159
|
+
super
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def views
|
|
163
|
+
return [] if embedded?
|
|
164
|
+
|
|
165
|
+
super
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def indexes(table_name)
|
|
169
|
+
if embedded?
|
|
170
|
+
return @connection.engine.index_manager.get_indexes_for_table(table_name.to_s).map do |index|
|
|
171
|
+
ActiveRecord::ConnectionAdapters::IndexDefinition.new(
|
|
172
|
+
table_name.to_s,
|
|
173
|
+
index.name.to_s,
|
|
174
|
+
index.unique,
|
|
175
|
+
index.columns.map(&:to_s)
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result = execute("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name=?", [table_name])
|
|
181
|
+
result.map do |row|
|
|
182
|
+
{
|
|
183
|
+
name: row["name"],
|
|
184
|
+
columns: parse_index_columns(row["sql"]),
|
|
185
|
+
unique: row["sql"].include?("UNIQUE")
|
|
186
|
+
}
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def columns(table_name)
|
|
191
|
+
return embedded_columns(table_name) if embedded?
|
|
192
|
+
|
|
193
|
+
result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
|
|
194
|
+
result.map do |row|
|
|
195
|
+
ActiveRecord::ConnectionAdapters::Column.new(
|
|
196
|
+
row["name"],
|
|
197
|
+
row["default"],
|
|
198
|
+
RubyDB::Rails::Type.to_rails(row["type"]),
|
|
199
|
+
{
|
|
200
|
+
null: row["notnull"] == 0,
|
|
201
|
+
primary_key: row["pk"] == 1,
|
|
202
|
+
limit: extract_limit(row["type"])
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def column_exists?(table_name, column_name)
|
|
209
|
+
columns(table_name).any? { |c| c.name == column_name }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# ==================== QUERY METHODS ====================
|
|
213
|
+
|
|
214
|
+
def execute(sql, name = nil)
|
|
215
|
+
sql = sql_for_execution(sql)
|
|
216
|
+
log(sql, name) do
|
|
217
|
+
@connection.execute(sql)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def exec_query(sql, name = nil, binds = [])
|
|
222
|
+
sql = sql_for_execution(sql)
|
|
223
|
+
log(sql, name) do
|
|
224
|
+
params = bind_values(binds)
|
|
225
|
+
active_record_result(@connection.execute(sql, params))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def exec_delete(sql, name = nil, binds = [])
|
|
230
|
+
sql = sql_for_execution(sql)
|
|
231
|
+
log(sql, name) do
|
|
232
|
+
params = bind_values(binds)
|
|
233
|
+
result = @connection.execute(sql, params)
|
|
234
|
+
result.affected_rows
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def exec_update(sql, name = nil, binds = [])
|
|
239
|
+
sql = sql_for_execution(sql)
|
|
240
|
+
log(sql, name) do
|
|
241
|
+
params = bind_values(binds)
|
|
242
|
+
result = @connection.execute(sql, params)
|
|
243
|
+
result.affected_rows
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
|
|
248
|
+
sql = sql_for_execution(sql)
|
|
249
|
+
log(sql, name) do
|
|
250
|
+
params = bind_values(binds)
|
|
251
|
+
result = @connection.execute(sql, params)
|
|
252
|
+
id = result.row_id
|
|
253
|
+
ActiveRecord::Result.new([pk || "id"], id.nil? ? [] : [[id]])
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
|
|
258
|
+
sql, binds = to_sql_and_binds(arel, binds)
|
|
259
|
+
result = exec_insert(sql, name, binds, pk, sequence_name, returning: returning)
|
|
260
|
+
return returning_column_values(result) unless returning.nil?
|
|
261
|
+
|
|
262
|
+
id_value || last_inserted_id(result)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def update(arel, name = nil, binds = [])
|
|
266
|
+
sql, binds = to_sql_and_binds(arel, binds)
|
|
267
|
+
exec_update(sql, name, binds)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def delete(arel, name = nil, binds = [])
|
|
271
|
+
sql, binds = to_sql_and_binds(arel, binds)
|
|
272
|
+
exec_delete(sql, name, binds)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def select_all(sql, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
|
|
276
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
277
|
+
exec_query(sql, name, binds)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def select_one(sql, name = nil, binds = [])
|
|
281
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
282
|
+
result = exec_query(sql, name, binds)
|
|
283
|
+
result.first
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def select_value(sql, name = nil, binds = [])
|
|
287
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
288
|
+
result = exec_query(sql, name, binds)
|
|
289
|
+
result.first&.values&.first
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def select_values(sql, name = nil, binds = [])
|
|
293
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
294
|
+
result = exec_query(sql, name, binds)
|
|
295
|
+
result.map { |row| row.values.first }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def select_rows(sql, name = nil, binds = [])
|
|
299
|
+
sql, binds = to_sql_and_binds(sql, binds)
|
|
300
|
+
result = exec_query(sql, name, binds)
|
|
301
|
+
result.map { |row| row.values }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# ==================== TRANSACTION METHODS ====================
|
|
305
|
+
|
|
306
|
+
def begin_db_transaction
|
|
307
|
+
@transaction_depth += 1
|
|
308
|
+
@connection.begin_db_transaction if @transaction_depth == 1
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def commit_db_transaction
|
|
312
|
+
return if @transaction_depth <= 0
|
|
313
|
+
|
|
314
|
+
@transaction_depth -= 1
|
|
315
|
+
@connection.commit_db_transaction if @transaction_depth == 0
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def rollback_db_transaction
|
|
319
|
+
return if @transaction_depth <= 0
|
|
320
|
+
|
|
321
|
+
@transaction_depth -= 1
|
|
322
|
+
@connection.rollback_db_transaction if @transaction_depth == 0
|
|
323
|
+
@transaction_depth = 0 if @transaction_depth < 0
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def create_savepoint(name)
|
|
327
|
+
execute("SAVEPOINT #{name}")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def rollback_to_savepoint(name)
|
|
331
|
+
execute("ROLLBACK TO SAVEPOINT #{name}")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def release_savepoint(name)
|
|
335
|
+
execute("RELEASE SAVEPOINT #{name}")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def in_transaction?
|
|
339
|
+
@transaction_depth > 0
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def transaction_joinable?
|
|
343
|
+
true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def transactional?
|
|
347
|
+
true
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# ==================== SCHEMA STATEMENT METHODS ====================
|
|
351
|
+
|
|
352
|
+
def create_table(table_name, **options, &block)
|
|
353
|
+
RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &block)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def drop_table(table_name, **options)
|
|
357
|
+
sql = +"DROP TABLE"
|
|
358
|
+
sql << " IF EXISTS" if options[:if_exists]
|
|
359
|
+
sql << " #{quote_table_name(table_name)}"
|
|
360
|
+
sql << " CASCADE" if options[:cascade]
|
|
361
|
+
execute(sql)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def add_column(table_name, column_name, type, **options)
|
|
365
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
366
|
+
sql << " ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options)}"
|
|
367
|
+
|
|
368
|
+
if options[:null] == false
|
|
369
|
+
sql << " NOT NULL"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if options[:default]
|
|
373
|
+
sql << " DEFAULT #{quote_default(options[:default])}"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
if options[:primary_key]
|
|
377
|
+
sql << " PRIMARY KEY"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
execute(sql)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def remove_column(table_name, column_name, type = nil, **options)
|
|
384
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
385
|
+
sql << " DROP COLUMN #{quote_column_name(column_name)}"
|
|
386
|
+
sql << " CASCADE" if options[:cascade]
|
|
387
|
+
execute(sql)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def change_column(table_name, column_name, type, **options)
|
|
391
|
+
# Change column type
|
|
392
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
393
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
394
|
+
sql << " TYPE #{type_to_sql(type, options)}"
|
|
395
|
+
execute(sql)
|
|
396
|
+
|
|
397
|
+
# Change nullability
|
|
398
|
+
if options.key?(:null)
|
|
399
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
400
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
401
|
+
sql << (options[:null] ? " DROP" : " SET") + " NOT NULL"
|
|
402
|
+
execute(sql)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Change default
|
|
406
|
+
if options.key?(:default)
|
|
407
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
408
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
409
|
+
if options[:default].nil?
|
|
410
|
+
sql << " DROP DEFAULT"
|
|
411
|
+
else
|
|
412
|
+
sql << " SET DEFAULT #{quote_default(options[:default])}"
|
|
413
|
+
end
|
|
414
|
+
execute(sql)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
419
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
420
|
+
sql << " RENAME COLUMN #{quote_column_name(column_name)}"
|
|
421
|
+
sql << " TO #{quote_column_name(new_column_name)}"
|
|
422
|
+
execute(sql)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def rename_table(old_name, new_name)
|
|
426
|
+
sql = "ALTER TABLE #{quote_table_name(old_name)}"
|
|
427
|
+
sql << " RENAME TO #{quote_table_name(new_name)}"
|
|
428
|
+
execute(sql)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def add_index(table_name, column_name, **options)
|
|
432
|
+
index_name = options[:name] || "idx_#{table_name}_#{Array(column_name).join('_')}"
|
|
433
|
+
sql = +"CREATE"
|
|
434
|
+
sql << " UNIQUE" if options[:unique]
|
|
435
|
+
sql << " INDEX #{quote_column_name(index_name)}"
|
|
436
|
+
sql << " ON #{quote_table_name(table_name)}"
|
|
437
|
+
sql << " (#{Array(column_name).map { |c| quote_column_name(c) }.join(', ')})"
|
|
438
|
+
sql << " WHERE #{options[:where]}" if options[:where]
|
|
439
|
+
execute(sql)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def remove_index(table_name, column_name = nil, **options)
|
|
443
|
+
index_name = options[:name]
|
|
444
|
+
if index_name.nil?
|
|
445
|
+
column_name ||= options[:column] || options[:columns]
|
|
446
|
+
index_name = "idx_#{table_name}_#{Array(column_name).join('_')}"
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
sql = "DROP INDEX #{quote_column_name(index_name)}"
|
|
450
|
+
execute(sql)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def add_foreign_key(from_table, to_table, **options)
|
|
454
|
+
fk_name = options[:name] || "fk_#{from_table}_to_#{to_table}"
|
|
455
|
+
sql = "ALTER TABLE #{quote_table_name(from_table)}"
|
|
456
|
+
sql << " ADD CONSTRAINT #{quote_column_name(fk_name)}"
|
|
457
|
+
sql << " FOREIGN KEY (#{quote_column_name(options[:column] || :id)})"
|
|
458
|
+
sql << " REFERENCES #{quote_table_name(to_table)}"
|
|
459
|
+
sql << " (#{quote_column_name(options[:primary_key] || :id)})"
|
|
460
|
+
sql << " ON DELETE #{options[:on_delete]}" if options[:on_delete]
|
|
461
|
+
sql << " ON UPDATE #{options[:on_update]}" if options[:on_update]
|
|
462
|
+
execute(sql)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def foreign_keys(table_name)
|
|
466
|
+
return super unless embedded?
|
|
467
|
+
|
|
468
|
+
constraints = @connection.engine.table_metadata[table_name.to_s]&.fetch(:constraints, []) || []
|
|
469
|
+
constraints.filter_map do |constraint|
|
|
470
|
+
type = constraint[:type] || constraint["type"]
|
|
471
|
+
next unless type.to_s.upcase == "FOREIGN_KEY"
|
|
472
|
+
|
|
473
|
+
columns = constraint[:columns] || constraint["columns"] || []
|
|
474
|
+
reference_table = constraint[:reference_table] || constraint["reference_table"]
|
|
475
|
+
reference_columns = constraint[:reference_columns] || constraint["reference_columns"] || ["id"]
|
|
476
|
+
options = {
|
|
477
|
+
column: Array(columns).first.to_s,
|
|
478
|
+
primary_key: Array(reference_columns).first.to_s,
|
|
479
|
+
name: constraint[:name] || constraint["name"]
|
|
480
|
+
}
|
|
481
|
+
options[:on_delete] = (constraint[:on_delete] || constraint["on_delete"]).to_s if constraint[:on_delete] || constraint["on_delete"]
|
|
482
|
+
options[:on_update] = (constraint[:on_update] || constraint["on_update"]).to_s if constraint[:on_update] || constraint["on_update"]
|
|
483
|
+
ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name.to_s, reference_table.to_s, options)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def remove_foreign_key(from_table, **options)
|
|
488
|
+
fk_name = options[:name] || "fk_#{from_table}_to_#{options[:to_table]}"
|
|
489
|
+
sql = "ALTER TABLE #{quote_table_name(from_table)}"
|
|
490
|
+
sql << " DROP CONSTRAINT #{quote_column_name(fk_name)}"
|
|
491
|
+
execute(sql)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def add_timestamps(table_name, **options)
|
|
495
|
+
add_column(table_name, :created_at, :datetime, options)
|
|
496
|
+
add_column(table_name, :updated_at, :datetime, options)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def remove_timestamps(table_name, **options)
|
|
500
|
+
remove_column(table_name, :updated_at, options)
|
|
501
|
+
remove_column(table_name, :created_at, options)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
|
505
|
+
if default
|
|
506
|
+
sql = "UPDATE #{quote_table_name(table_name)}"
|
|
507
|
+
sql << " SET #{quote_column_name(column_name)} = #{quote(default)}"
|
|
508
|
+
sql << " WHERE #{quote_column_name(column_name)} IS NULL"
|
|
509
|
+
execute(sql)
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
513
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
514
|
+
sql << (null ? " DROP" : " SET") + " NOT NULL"
|
|
515
|
+
execute(sql)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def change_column_default(table_name, column_name, default)
|
|
519
|
+
sql = "ALTER TABLE #{quote_table_name(table_name)}"
|
|
520
|
+
sql << " ALTER COLUMN #{quote_column_name(column_name)}"
|
|
521
|
+
if default.nil?
|
|
522
|
+
sql << " DROP DEFAULT"
|
|
523
|
+
else
|
|
524
|
+
sql << " SET DEFAULT #{quote_default(default)}"
|
|
525
|
+
end
|
|
526
|
+
execute(sql)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# ==================== QUOTING METHODS ====================
|
|
530
|
+
|
|
531
|
+
def quote(value, column = nil)
|
|
532
|
+
@connection.quote(value, column)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def quote_table_name(name)
|
|
536
|
+
@connection.quote_table_name(name)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def quote_column_name(name)
|
|
540
|
+
@connection.quote_column_name(name)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def quote_default(value)
|
|
544
|
+
quote(value)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# ==================== TYPE CASTING ====================
|
|
548
|
+
|
|
549
|
+
def type_cast(value, type)
|
|
550
|
+
RubyDB::Rails::Type.serialize(value, type)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def type_cast_from_database(value, type)
|
|
554
|
+
RubyDB::Rails::Type.deserialize(value, type)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# ==================== SCHEMA VERSION ====================
|
|
558
|
+
|
|
559
|
+
def schema_version
|
|
560
|
+
result = execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
|
|
561
|
+
result.first ? result.first["version"] : nil
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def schema_migrations
|
|
565
|
+
result = execute("SELECT version FROM schema_migrations ORDER BY version")
|
|
566
|
+
result.map { |row| row["version"] }
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def dump_schema
|
|
570
|
+
schema = +""
|
|
571
|
+
tables.each do |table|
|
|
572
|
+
table_columns = columns(table)
|
|
573
|
+
primary_key_name = if embedded?
|
|
574
|
+
@connection.engine.table_columns(table).find(&:primary_key?)&.name
|
|
575
|
+
else
|
|
576
|
+
primary_key(table)
|
|
577
|
+
end
|
|
578
|
+
primary_key = table_columns.find { |column| column.name.to_s == primary_key_name.to_s } if primary_key_name
|
|
579
|
+
automatic_id = primary_key && primary_key_name.to_s == "id" && primary_key.type.to_sym == :integer
|
|
580
|
+
table_options = automatic_id ? "" : ", id: false"
|
|
581
|
+
schema << "create_table \"#{table}\"#{table_options} do |t|\n"
|
|
582
|
+
table_columns.each do |col|
|
|
583
|
+
next if automatic_id && primary_key && col.name.to_s == primary_key.name.to_s
|
|
584
|
+
|
|
585
|
+
type = RubyDB::Rails::Type.to_rails(col.type)
|
|
586
|
+
schema << " t.#{type} \"#{col.name}\""
|
|
587
|
+
schema << ", primary_key: true" if primary_key && col.name.to_s == primary_key.name.to_s
|
|
588
|
+
schema << ", default: #{schema_literal(col.default, col.type)}" unless col.default.nil?
|
|
589
|
+
schema << ", null: false" unless col.null
|
|
590
|
+
schema << "\n"
|
|
591
|
+
end
|
|
592
|
+
schema << "end\n\n"
|
|
593
|
+
|
|
594
|
+
indexes(table).each do |index|
|
|
595
|
+
schema << "add_index \"#{table}\", #{index.columns.map(&:to_s).inspect}"
|
|
596
|
+
schema << ", unique: true" if index.unique
|
|
597
|
+
schema << ", name: #{index.name.to_s.inspect}\n"
|
|
598
|
+
end
|
|
599
|
+
schema << "\n" if indexes(table).any?
|
|
600
|
+
end
|
|
601
|
+
schema
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def schema_literal(value, type = nil)
|
|
605
|
+
if type.to_sym == :boolean && value.is_a?(String) && %w[true false].include?(value.downcase)
|
|
606
|
+
return value.downcase
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
case value
|
|
610
|
+
when true then "true"
|
|
611
|
+
when false then "false"
|
|
612
|
+
when Numeric then value.to_s
|
|
613
|
+
else value.to_s.inspect
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# ==================== CONNECTION MANAGEMENT ====================
|
|
618
|
+
|
|
619
|
+
def reset!
|
|
620
|
+
@connection.disconnect
|
|
621
|
+
@connection.connect
|
|
622
|
+
@prepared_statements.clear
|
|
623
|
+
@query_cache.clear
|
|
624
|
+
@statements.clear
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def disconnect!
|
|
628
|
+
@connection.disconnect
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def reconnect!
|
|
632
|
+
reset!
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def active?
|
|
636
|
+
@connection.connected?
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def close
|
|
640
|
+
@connection.disconnect
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# ==================== QUERY CACHE ====================
|
|
644
|
+
|
|
645
|
+
def clear_cache!
|
|
646
|
+
@query_cache.clear
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def enable_query_cache!
|
|
650
|
+
@query_cache_enabled = true
|
|
651
|
+
@query_cache.clear
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def disable_query_cache!
|
|
655
|
+
@query_cache_enabled = false
|
|
656
|
+
@query_cache.clear
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def query_cache_enabled
|
|
660
|
+
@query_cache_enabled
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# ==================== PREPARED STATEMENTS ====================
|
|
664
|
+
|
|
665
|
+
def prepare_statement(sql)
|
|
666
|
+
@lock.synchronize do
|
|
667
|
+
stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
|
|
668
|
+
@statement_counter += 1
|
|
669
|
+
|
|
670
|
+
result = @connection.prepare(sql)
|
|
671
|
+
@prepared_statements[stmt_id] = {
|
|
672
|
+
id: result.statement_id,
|
|
673
|
+
sql: sql,
|
|
674
|
+
created_at: Time.now
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
stmt_id
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def execute_prepared_statement(stmt_id, params = [])
|
|
682
|
+
@lock.synchronize do
|
|
683
|
+
stmt = @prepared_statements[stmt_id]
|
|
684
|
+
return nil unless stmt
|
|
685
|
+
|
|
686
|
+
@connection.execute_prepared(stmt[:id], params)
|
|
687
|
+
end
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def close_statement(stmt_id)
|
|
691
|
+
@lock.synchronize do
|
|
692
|
+
stmt = @prepared_statements.delete(stmt_id)
|
|
693
|
+
if stmt
|
|
694
|
+
@connection.close_statement(stmt[:id])
|
|
695
|
+
end
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# ==================== VERSION INFORMATION ====================
|
|
700
|
+
|
|
701
|
+
def dbms_version
|
|
702
|
+
RubyDB::VERSION
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# ==================== FEATURE SUPPORT ====================
|
|
706
|
+
|
|
707
|
+
def supports_datetime_with_precision?
|
|
708
|
+
true
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def supports_materialized_views?
|
|
712
|
+
false
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def supports_common_table_expressions?
|
|
716
|
+
false
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# ==================== PRIVATE METHODS ====================
|
|
720
|
+
|
|
721
|
+
private
|
|
722
|
+
|
|
723
|
+
def embedded?
|
|
724
|
+
!@connection.engine.nil?
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def embedded_columns(table_name)
|
|
728
|
+
@connection.engine.table_columns(table_name).map do |column|
|
|
729
|
+
ActiveRecord::ConnectionAdapters::Column.new(
|
|
730
|
+
column.name.to_s,
|
|
731
|
+
column.has_default? ? rails_default_value(column.default) : nil,
|
|
732
|
+
ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
|
|
733
|
+
sql_type: column.type.to_s.upcase,
|
|
734
|
+
type: rails_type_for(column.type),
|
|
735
|
+
limit: column.options[:limit]
|
|
736
|
+
),
|
|
737
|
+
column.nullable?
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def rails_type_for(type)
|
|
743
|
+
case type.to_sym
|
|
744
|
+
when :integer, :bigint, :smallint then :integer
|
|
745
|
+
when :float then :float
|
|
746
|
+
when :decimal then :decimal
|
|
747
|
+
when :boolean then :boolean
|
|
748
|
+
when :date then :date
|
|
749
|
+
when :time then :time
|
|
750
|
+
when :datetime, :timestamp then :datetime
|
|
751
|
+
when :binary, :blob then :binary
|
|
752
|
+
when :json then :json
|
|
753
|
+
else :string
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# ActiveRecord's generic Column deduplication is string-oriented. RubyDB
|
|
758
|
+
# persists typed defaults, so serialize scalar defaults at this boundary
|
|
759
|
+
# and let ActiveRecord cast them through the column type map.
|
|
760
|
+
def rails_default_value(value)
|
|
761
|
+
value.is_a?(String) ? value : value.to_s
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def sql_for_execution(sql)
|
|
765
|
+
sql = sql.to_sql if sql.respond_to?(:to_sql)
|
|
766
|
+
sql
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def active_record_result(result)
|
|
770
|
+
rows = result.to_a
|
|
771
|
+
columns = if rows.first.respond_to?(:keys)
|
|
772
|
+
rows.first.keys.map(&:to_s)
|
|
773
|
+
else
|
|
774
|
+
result.columns.map do |column|
|
|
775
|
+
column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
|
|
776
|
+
end.map(&:to_s)
|
|
777
|
+
end
|
|
778
|
+
values = rows.map do |row|
|
|
779
|
+
columns.map { |column| row[column] || row[column.to_sym] }
|
|
780
|
+
end
|
|
781
|
+
ActiveRecord::Result.new(columns, values)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# ActiveRecord normally supplies QueryAttribute objects, but migration
|
|
785
|
+
# and schema code can also pass raw values or two-element bind pairs.
|
|
786
|
+
# Normalize all supported forms at the adapter boundary.
|
|
787
|
+
def bind_values(binds)
|
|
788
|
+
binds.map do |bind|
|
|
789
|
+
value = if bind.respond_to?(:value_for_database)
|
|
790
|
+
bind.value_for_database
|
|
791
|
+
elsif bind.respond_to?(:value)
|
|
792
|
+
bind.value
|
|
793
|
+
elsif bind.is_a?(Array) && bind.length == 2
|
|
794
|
+
bind.last
|
|
795
|
+
else
|
|
796
|
+
bind
|
|
797
|
+
end
|
|
798
|
+
value.respond_to?(:value_for_database) ? value.value_for_database : value
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def parse_index_columns(sql)
|
|
803
|
+
if sql =~ /\(([^)]+)\)/
|
|
804
|
+
$1.split(",").map(&:strip)
|
|
805
|
+
else
|
|
806
|
+
[]
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def extract_limit(type)
|
|
811
|
+
if type =~ /VARCHAR\((\d+)\)/
|
|
812
|
+
$1.to_i
|
|
813
|
+
else
|
|
814
|
+
nil
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def log(sql, name = nil, &block)
|
|
819
|
+
start_time = Time.now
|
|
820
|
+
result = block.call
|
|
821
|
+
elapsed_ms = (Time.now - start_time) * 1000
|
|
822
|
+
|
|
823
|
+
if @logger
|
|
824
|
+
@logger.debug " #{name || 'SQL'} (#{elapsed_ms.round(2)}ms) #{sql}"
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
result
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def type_to_sql(type, options = {})
|
|
831
|
+
case type.to_sym
|
|
832
|
+
when :integer
|
|
833
|
+
"INTEGER"
|
|
834
|
+
when :bigint
|
|
835
|
+
"BIGINT"
|
|
836
|
+
when :smallint
|
|
837
|
+
"SMALLINT"
|
|
838
|
+
when :float
|
|
839
|
+
"FLOAT"
|
|
840
|
+
when :decimal
|
|
841
|
+
precision = options[:precision] || 10
|
|
842
|
+
scale = options[:scale] || 2
|
|
843
|
+
"DECIMAL(#{precision}, #{scale})"
|
|
844
|
+
when :boolean
|
|
845
|
+
"BOOLEAN"
|
|
846
|
+
when :text
|
|
847
|
+
"TEXT"
|
|
848
|
+
when :string
|
|
849
|
+
limit = options[:limit] || 255
|
|
850
|
+
"VARCHAR(#{limit})"
|
|
851
|
+
when :binary
|
|
852
|
+
"BLOB"
|
|
853
|
+
when :date
|
|
854
|
+
"DATE"
|
|
855
|
+
when :time
|
|
856
|
+
"TIME"
|
|
857
|
+
when :datetime, :timestamp
|
|
858
|
+
"TIMESTAMP"
|
|
859
|
+
when :json
|
|
860
|
+
"JSON"
|
|
861
|
+
when :uuid
|
|
862
|
+
"UUID"
|
|
863
|
+
else
|
|
864
|
+
"TEXT"
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
# Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
|
|
872
|
+
# adapters through the conventional `rubydb_connection` hook instead.
|
|
873
|
+
if ActiveRecord::ConnectionAdapters.respond_to?(:register)
|
|
874
|
+
ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
|
|
875
|
+
else
|
|
876
|
+
module ActiveRecord
|
|
877
|
+
module ConnectionHandling
|
|
878
|
+
def rubydb_adapter_class
|
|
879
|
+
ConnectionAdapters::RubyDBAdapter
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def rubydb_connection(config)
|
|
883
|
+
rubydb_adapter_class.new(config)
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
end
|