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,1192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module RubyDB
|
|
7
|
+
module Execution
|
|
8
|
+
# Executor - Executes query plans and returns results
|
|
9
|
+
class Executor
|
|
10
|
+
attr_reader :engine, :stats
|
|
11
|
+
|
|
12
|
+
def initialize(engine, cte_rows: {}, deadline_at: nil, cancellation: nil)
|
|
13
|
+
@engine = engine
|
|
14
|
+
@cte_rows = cte_rows
|
|
15
|
+
@deadline_at = deadline_at && (deadline_at.is_a?(Time) ? deadline_at : Time.parse(deadline_at.to_s))
|
|
16
|
+
@cancellation = cancellation
|
|
17
|
+
@stats = {
|
|
18
|
+
queries_executed: 0,
|
|
19
|
+
rows_returned: 0,
|
|
20
|
+
total_time_ms: 0,
|
|
21
|
+
sequential_scans: 0,
|
|
22
|
+
index_scans: 0,
|
|
23
|
+
joins: 0,
|
|
24
|
+
aggregations: 0,
|
|
25
|
+
sorts: 0
|
|
26
|
+
}
|
|
27
|
+
@lock = Mutex.new
|
|
28
|
+
@current_transaction = nil
|
|
29
|
+
@explain_mode = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute(plan, transaction_id = nil)
|
|
33
|
+
@lock.synchronize do
|
|
34
|
+
check_deadline!
|
|
35
|
+
start_time = Time.now
|
|
36
|
+
@stats[:queries_executed] += 1
|
|
37
|
+
@current_transaction = transaction_id
|
|
38
|
+
|
|
39
|
+
result = case plan
|
|
40
|
+
when Plan::With
|
|
41
|
+
execute_with(plan)
|
|
42
|
+
when Plan::Select
|
|
43
|
+
execute_select(plan)
|
|
44
|
+
when Plan::SetOperation
|
|
45
|
+
execute_set_operation(plan)
|
|
46
|
+
when Plan::Insert
|
|
47
|
+
execute_insert(plan)
|
|
48
|
+
when Plan::Update
|
|
49
|
+
execute_update(plan)
|
|
50
|
+
when Plan::Delete
|
|
51
|
+
execute_delete(plan)
|
|
52
|
+
when Plan::CreateTable
|
|
53
|
+
execute_create_table(plan)
|
|
54
|
+
when Plan::DropTable
|
|
55
|
+
execute_drop_table(plan)
|
|
56
|
+
when Plan::CreateDatabase
|
|
57
|
+
execute_create_database(plan)
|
|
58
|
+
when Plan::DropDatabase
|
|
59
|
+
execute_drop_database(plan)
|
|
60
|
+
when Plan::CreateSchema
|
|
61
|
+
execute_create_schema(plan)
|
|
62
|
+
when Plan::DropSchema
|
|
63
|
+
execute_drop_schema(plan)
|
|
64
|
+
when Plan::CreateView
|
|
65
|
+
execute_create_view(plan)
|
|
66
|
+
when Plan::DropView
|
|
67
|
+
execute_drop_view(plan)
|
|
68
|
+
when Plan::CreateTrigger
|
|
69
|
+
execute_create_trigger(plan)
|
|
70
|
+
when Plan::DropTrigger
|
|
71
|
+
execute_drop_trigger(plan)
|
|
72
|
+
when Plan::Vacuum
|
|
73
|
+
execute_vacuum(plan)
|
|
74
|
+
when Plan::AlterTableAddColumn
|
|
75
|
+
execute_alter_table_add_column(plan)
|
|
76
|
+
when Plan::AlterTableDropColumn
|
|
77
|
+
execute_alter_table_drop_column(plan)
|
|
78
|
+
when Plan::AlterTableAddConstraint
|
|
79
|
+
execute_alter_table_add_constraint(plan)
|
|
80
|
+
when Plan::AlterTableDropConstraint
|
|
81
|
+
execute_alter_table_drop_constraint(plan)
|
|
82
|
+
when Plan::CreateIndex
|
|
83
|
+
execute_create_index(plan)
|
|
84
|
+
when Plan::DropIndex
|
|
85
|
+
execute_drop_index(plan)
|
|
86
|
+
when Plan::BeginTransaction
|
|
87
|
+
execute_begin_transaction(plan)
|
|
88
|
+
when Plan::Commit
|
|
89
|
+
execute_commit(plan)
|
|
90
|
+
when Plan::Rollback
|
|
91
|
+
execute_rollback(plan)
|
|
92
|
+
when Plan::Savepoint
|
|
93
|
+
execute_savepoint(plan)
|
|
94
|
+
when Plan::RollbackToSavepoint
|
|
95
|
+
execute_rollback_to_savepoint(plan)
|
|
96
|
+
when Plan::ReleaseSavepoint
|
|
97
|
+
execute_release_savepoint(plan)
|
|
98
|
+
when Plan::Explain
|
|
99
|
+
execute_explain(plan)
|
|
100
|
+
else
|
|
101
|
+
raise ExecutionError, "Unknown plan type: #{plan.class}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
elapsed_ms = ((Time.now - start_time) * 1000).round(2)
|
|
105
|
+
@stats[:total_time_ms] += elapsed_ms
|
|
106
|
+
@stats[:rows_returned] += result[:row_count] if result && result[:row_count]
|
|
107
|
+
|
|
108
|
+
result
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def execute_set_operation(plan)
|
|
113
|
+
left = child_executor(@cte_rows).execute(plan.left_plan, @current_transaction)[:rows]
|
|
114
|
+
right = child_executor(@cte_rows).execute(plan.right_plan, @current_transaction)[:rows]
|
|
115
|
+
left_width = left.first&.size || plan.left_plan.columns.size
|
|
116
|
+
right_width = right.first&.size || plan.right_plan.columns.size
|
|
117
|
+
if left_width != right_width
|
|
118
|
+
raise ExecutionError, "Set operation requires the same number of columns on both sides"
|
|
119
|
+
end
|
|
120
|
+
key = ->(row) { row.to_a }
|
|
121
|
+
rows = case plan.operator
|
|
122
|
+
when :union then plan.all ? left + right : (left + right).uniq { |row| key.call(row) }
|
|
123
|
+
when :intersect then left.select { |row| right.any? { |other| key.call(other) == key.call(row) } }.uniq { |row| key.call(row) }
|
|
124
|
+
when :except then left.reject { |row| right.any? { |other| key.call(other) == key.call(row) } }.uniq { |row| key.call(row) }
|
|
125
|
+
else raise ExecutionError, "Unsupported set operation: #{plan.operator}"
|
|
126
|
+
end
|
|
127
|
+
{ rows: rows, row_count: rows.size, column_names: rows.first&.keys || [] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def execute_with(plan)
|
|
131
|
+
available_ctes = @cte_rows.dup
|
|
132
|
+
plan.ctes.each do |name, cte_plan|
|
|
133
|
+
available_ctes[name.to_s] = if plan.recursive && cte_plan.is_a?(Plan::SetOperation) && cte_plan.operator == :union
|
|
134
|
+
execute_recursive_cte(name.to_s, cte_plan, available_ctes)
|
|
135
|
+
else
|
|
136
|
+
child_executor(available_ctes).execute(cte_plan, @current_transaction)[:rows]
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
child_executor(available_ctes).execute(plan.query_plan, @current_transaction)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def execute_recursive_cte(name, cte_plan, available_ctes)
|
|
143
|
+
unless cte_plan.is_a?(Plan::SetOperation) && cte_plan.operator == :union
|
|
144
|
+
raise ExecutionError, "Recursive CTE requires UNION or UNION ALL"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
anchor = child_executor(available_ctes).execute(cte_plan.left_plan, @current_transaction)[:rows]
|
|
148
|
+
accumulated = anchor.dup
|
|
149
|
+
working = anchor
|
|
150
|
+
output_columns = anchor.first&.keys || cte_plan.left_plan.columns.map(&:to_s)
|
|
151
|
+
max_iterations = Integer(@engine.instance_variable_get(:@config)[:max_recursive_iterations] || 10_000)
|
|
152
|
+
iterations = 0
|
|
153
|
+
|
|
154
|
+
while working.any?
|
|
155
|
+
iterations += 1
|
|
156
|
+
raise ExecutionError, "Recursive CTE exceeded #{max_iterations} iterations" if iterations > max_iterations
|
|
157
|
+
|
|
158
|
+
recursive_scope = available_ctes.merge(name => working)
|
|
159
|
+
next_rows = child_executor(recursive_scope).execute(cte_plan.right_plan, @current_transaction)[:rows]
|
|
160
|
+
next_rows = next_rows.map do |row|
|
|
161
|
+
output_columns.zip(row.values).to_h
|
|
162
|
+
end
|
|
163
|
+
if cte_plan.all
|
|
164
|
+
accumulated.concat(next_rows)
|
|
165
|
+
working = next_rows
|
|
166
|
+
else
|
|
167
|
+
existing = accumulated.map { |row| row.to_a }
|
|
168
|
+
working = next_rows.reject { |row| existing.include?(row.to_a) }
|
|
169
|
+
accumulated.concat(working)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
accumulated
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def execute_select(plan)
|
|
177
|
+
@stats[:sequential_scans] += 1 if plan.scan_type == :sequential
|
|
178
|
+
@stats[:index_scans] += 1 if plan.scan_type == :index
|
|
179
|
+
|
|
180
|
+
# Qualify source rows before evaluating predicates. This preserves
|
|
181
|
+
# SQL's table/alias namespace while retaining unqualified keys for
|
|
182
|
+
# existing single-table callers.
|
|
183
|
+
rows = qualify_rows(scan_table(plan), plan.source_reference || plan.table_name)
|
|
184
|
+
plan.joins.each do |join|
|
|
185
|
+
check_deadline!
|
|
186
|
+
right_rows = qualify_rows(scan_table(Plan::Select.new(join[:table].name, [])), join[:table])
|
|
187
|
+
rows = execute_join(rows, right_rows, join)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Apply filters (WHERE clause)
|
|
191
|
+
if plan.predicate
|
|
192
|
+
rows = rows.each_with_index.filter_map do |row, index|
|
|
193
|
+
check_deadline! if (index & 255).zero?
|
|
194
|
+
row if evaluate_predicate(plan.predicate, row)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
apply_window_functions!(rows, plan.projections) if plan.projections
|
|
199
|
+
|
|
200
|
+
# ORDER BY is evaluated against the source rows, so a column used
|
|
201
|
+
# solely for ordering remains available even when it is not selected.
|
|
202
|
+
aggregating = plan.aggregates && !plan.aggregates.empty?
|
|
203
|
+
if plan.order_by && !plan.order_by.empty? && !aggregating
|
|
204
|
+
rows = sort_rows(rows, plan.order_by)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if aggregating
|
|
208
|
+
rows = aggregate_rows(rows, plan.group_by || [], plan.projections)
|
|
209
|
+
rows = rows.select { |row| evaluate_predicate(plan.having, row) } if plan.having
|
|
210
|
+
elsif plan.projections
|
|
211
|
+
# Apply projections (SELECT columns)
|
|
212
|
+
rows = rows.each_with_index.map do |row, index|
|
|
213
|
+
check_deadline! if (index & 255).zero?
|
|
214
|
+
project_row(row, plan.projections)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Apply DISTINCT
|
|
219
|
+
if plan.distinct
|
|
220
|
+
rows = distinct_rows(rows)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Apply GROUP BY
|
|
224
|
+
# Apply ORDER BY
|
|
225
|
+
if plan.order_by && !plan.order_by.empty? && aggregating
|
|
226
|
+
rows = sort_rows(rows, plan.order_by)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Apply LIMIT and OFFSET
|
|
230
|
+
if plan.limit
|
|
231
|
+
offset = plan.offset || 0
|
|
232
|
+
rows = rows[offset, plan.limit] || []
|
|
233
|
+
elsif plan.offset
|
|
234
|
+
rows = rows[plan.offset..-1] || []
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
{
|
|
238
|
+
rows: rows,
|
|
239
|
+
row_count: rows.size,
|
|
240
|
+
column_names: get_column_names(plan)
|
|
241
|
+
}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def execute_insert(plan)
|
|
245
|
+
table_name = plan.table_name
|
|
246
|
+
columns = plan.columns
|
|
247
|
+
rows = plan.rows || [plan.values]
|
|
248
|
+
implicit_transaction = rows.size > 1 && !@engine.in_transaction?
|
|
249
|
+
@engine.begin_transaction if implicit_transaction
|
|
250
|
+
|
|
251
|
+
begin
|
|
252
|
+
inserted = rows.map { |values| execute_single_insert(plan, table_name, columns, values) }
|
|
253
|
+
unless !implicit_transaction || @engine.commit_transaction
|
|
254
|
+
raise ExecutionError, "Implicit multi-row INSERT transaction could not commit"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
row_count: inserted.sum { |result| result[:row_count] },
|
|
259
|
+
affected_rows: inserted.sum { |result| result[:affected_rows] },
|
|
260
|
+
row_ids: inserted.filter_map { |result| result[:row_id] },
|
|
261
|
+
row_id: inserted.reverse_each.map { |result| result[:row_id] }.compact.first,
|
|
262
|
+
message: "INSERT #{inserted.sum { |result| result[:affected_rows] }}"
|
|
263
|
+
}
|
|
264
|
+
rescue Exception
|
|
265
|
+
@engine.rollback_transaction if implicit_transaction && @engine.in_transaction?
|
|
266
|
+
raise
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def execute_single_insert(plan, table_name, columns, values)
|
|
271
|
+
|
|
272
|
+
# Build row data
|
|
273
|
+
row_data = {}
|
|
274
|
+
columns.each_with_index do |col, idx|
|
|
275
|
+
row_data[col] = evaluate_expression(values[idx])
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Insert into engine
|
|
279
|
+
table_columns = @engine.table_columns(table_name)
|
|
280
|
+
row_id = @engine.insert_row(table_name, table_columns, row_data)
|
|
281
|
+
|
|
282
|
+
{
|
|
283
|
+
row_count: 1,
|
|
284
|
+
affected_rows: 1,
|
|
285
|
+
row_id: row_id,
|
|
286
|
+
message: "INSERT 1"
|
|
287
|
+
}
|
|
288
|
+
rescue DatabaseError => error
|
|
289
|
+
raise unless error.message.match?(/duplicate|unique|primary key/i)
|
|
290
|
+
return { row_count: 0, affected_rows: 0, message: "INSERT 0 (conflict ignored)" } if plan.on_conflict == :nothing
|
|
291
|
+
raise unless plan.on_conflict.is_a?(Hash) && plan.on_conflict[:action] == :update
|
|
292
|
+
|
|
293
|
+
target = plan.on_conflict[:target]
|
|
294
|
+
target = @engine.table_columns(table_name).select(&:primary_key?).map(&:name) if target.empty?
|
|
295
|
+
if target.empty?
|
|
296
|
+
target = @engine.table_columns(table_name).select(&:unique?).map(&:name)
|
|
297
|
+
end
|
|
298
|
+
if target.empty? && @engine.respond_to?(:index_manager)
|
|
299
|
+
target = @engine.index_manager.get_indexes_for_table(table_name)
|
|
300
|
+
.select(&:unique?).min_by { |index| index.columns.size }&.columns || []
|
|
301
|
+
end
|
|
302
|
+
raise ExecutionError, "ON CONFLICT DO UPDATE requires a conflict target or primary key" if target.empty?
|
|
303
|
+
existing = @engine.select_rows(table_name, @engine.table_columns(table_name)).find do |row|
|
|
304
|
+
target.all? do |column|
|
|
305
|
+
row_value(row, column, column.to_s) == row_value(row_data, column, column.to_s)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
raise error unless existing
|
|
309
|
+
|
|
310
|
+
context = existing.merge(row_data.transform_keys(&:to_s).transform_keys { |key| "excluded.#{key}" })
|
|
311
|
+
values = plan.on_conflict[:assignments].each_with_object({}) do |assignment, updates|
|
|
312
|
+
updates[assignment.column] = evaluate_expression(assignment.value, context)
|
|
313
|
+
end
|
|
314
|
+
row_id = existing[:_row_id] || existing["_row_id"]
|
|
315
|
+
@engine.update_row(table_name, row_id, values)
|
|
316
|
+
{ row_count: 1, affected_rows: 1, row_id: row_id, message: "INSERT 0 UPDATE 1" }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def execute_update(plan)
|
|
320
|
+
table_name = plan.table_name
|
|
321
|
+
assignments = plan.assignments
|
|
322
|
+
predicate = plan.predicate
|
|
323
|
+
|
|
324
|
+
# Get rows to update
|
|
325
|
+
rows = scan_table(Plan::Select.new(table_name, []))
|
|
326
|
+
rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
|
|
327
|
+
|
|
328
|
+
updated_count = 0
|
|
329
|
+
rows.each do |row|
|
|
330
|
+
# Apply updates
|
|
331
|
+
assignments.each do |assignment|
|
|
332
|
+
column = assignment.is_a?(Hash) ? (assignment[:column] || assignment["column"]) : assignment.column
|
|
333
|
+
value = assignment.is_a?(Hash) ? (assignment[:value] || assignment["value"]) : assignment.value
|
|
334
|
+
row[column] = evaluate_expression(value, row)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Update in engine
|
|
338
|
+
row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
|
|
339
|
+
@engine.update_row(table_name, row_id, row)
|
|
340
|
+
|
|
341
|
+
updated_count += 1
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
{
|
|
345
|
+
row_count: updated_count,
|
|
346
|
+
affected_rows: updated_count,
|
|
347
|
+
message: "UPDATE #{updated_count}"
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def execute_delete(plan)
|
|
352
|
+
table_name = plan.table_name
|
|
353
|
+
predicate = plan.predicate
|
|
354
|
+
|
|
355
|
+
# Get rows to delete
|
|
356
|
+
rows = scan_table(Plan::Select.new(table_name, []))
|
|
357
|
+
rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
|
|
358
|
+
|
|
359
|
+
deleted_count = 0
|
|
360
|
+
rows.each do |row|
|
|
361
|
+
row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
|
|
362
|
+
@engine.delete_row(table_name, row_id)
|
|
363
|
+
deleted_count += 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
{
|
|
367
|
+
row_count: deleted_count,
|
|
368
|
+
affected_rows: deleted_count,
|
|
369
|
+
message: "DELETE #{deleted_count}"
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def execute_create_table(plan)
|
|
374
|
+
table_name = plan.table_name
|
|
375
|
+
columns = plan.columns
|
|
376
|
+
options = plan.options || {}
|
|
377
|
+
|
|
378
|
+
@engine.create_table(table_name, columns, options)
|
|
379
|
+
|
|
380
|
+
{
|
|
381
|
+
row_count: 0,
|
|
382
|
+
message: "CREATE TABLE #{table_name}"
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def execute_drop_table(plan)
|
|
387
|
+
table_name = plan.table_name
|
|
388
|
+
options = plan.options || {}
|
|
389
|
+
|
|
390
|
+
@engine.drop_table(table_name, options)
|
|
391
|
+
|
|
392
|
+
{
|
|
393
|
+
row_count: 0,
|
|
394
|
+
message: "DROP TABLE #{table_name}"
|
|
395
|
+
}
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def execute_create_database(plan)
|
|
399
|
+
@engine.catalog.create_database(plan.database_name, **(plan.options || {}))
|
|
400
|
+
{ row_count: 0, message: "CREATE DATABASE #{plan.database_name}" }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def execute_drop_database(plan)
|
|
404
|
+
@engine.catalog.drop_database(plan.database_name, **(plan.options || {}))
|
|
405
|
+
{ row_count: 0, message: "DROP DATABASE #{plan.database_name}" }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def execute_create_schema(plan)
|
|
409
|
+
@engine.catalog.create_schema(plan.schema_name, **(plan.options || {}))
|
|
410
|
+
{ row_count: 0, message: "CREATE SCHEMA #{plan.schema_name}" }
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def execute_drop_schema(plan)
|
|
414
|
+
@engine.catalog.drop_schema(plan.schema_name, **(plan.options || {}))
|
|
415
|
+
{ row_count: 0, message: "DROP SCHEMA #{plan.schema_name}" }
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def execute_create_view(plan)
|
|
419
|
+
query = plan.query.respond_to?(:to_sql) ? plan.query.to_sql : plan.query
|
|
420
|
+
@engine.catalog.create_view(plan.view_name, query, **(plan.options || {}))
|
|
421
|
+
{ row_count: 0, message: "CREATE VIEW #{plan.view_name}" }
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def execute_drop_view(plan)
|
|
425
|
+
@engine.catalog.drop_view(plan.view_name, **(plan.options || {}))
|
|
426
|
+
{ row_count: 0, message: "DROP VIEW #{plan.view_name}" }
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def execute_create_trigger(plan)
|
|
430
|
+
definition = "EXECUTE FUNCTION #{plan.function_name}()"
|
|
431
|
+
@engine.catalog.create_trigger(plan.trigger_name, plan.event, plan.target_table, definition, timing: plan.timing, function_name: plan.function_name)
|
|
432
|
+
{ row_count: 0, message: "CREATE TRIGGER #{plan.trigger_name}" }
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def execute_drop_trigger(plan)
|
|
436
|
+
@engine.catalog.drop_trigger(plan.trigger_name, **(plan.options || {}))
|
|
437
|
+
{ row_count: 0, message: "DROP TRIGGER #{plan.trigger_name}" }
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def execute_vacuum(_plan)
|
|
441
|
+
result = @engine.vacuum
|
|
442
|
+
{ row_count: 0, vacuum: result, message: "VACUUM" }
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def execute_alter_table_add_column(plan)
|
|
446
|
+
@engine.add_column(plan.table_name, plan.column_name, plan.column_type, plan.options)
|
|
447
|
+
{ row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD COLUMN #{plan.column_name}" }
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def execute_alter_table_drop_column(plan)
|
|
451
|
+
@engine.drop_column(plan.table_name, plan.column_name)
|
|
452
|
+
{ row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP COLUMN #{plan.column_name}" }
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def execute_alter_table_add_constraint(plan)
|
|
456
|
+
@engine.add_constraint(plan.table_name, plan.constraint)
|
|
457
|
+
{ row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD CONSTRAINT #{plan.constraint.name}" }
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def execute_alter_table_drop_constraint(plan)
|
|
461
|
+
@engine.drop_constraint(plan.table_name, plan.constraint_name)
|
|
462
|
+
{ row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP CONSTRAINT #{plan.constraint_name}" }
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def execute_create_index(plan)
|
|
466
|
+
index_name = plan.index_name
|
|
467
|
+
table_name = plan.table_name
|
|
468
|
+
columns = plan.columns
|
|
469
|
+
options = plan.options || {}
|
|
470
|
+
|
|
471
|
+
if @engine.respond_to?(:index_manager)
|
|
472
|
+
@engine.index_manager.create_index(index_name, table_name, columns, options)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
{
|
|
476
|
+
row_count: 0,
|
|
477
|
+
message: "CREATE INDEX #{index_name}"
|
|
478
|
+
}
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def execute_drop_index(plan)
|
|
482
|
+
index_name = plan.index_name
|
|
483
|
+
options = plan.options || {}
|
|
484
|
+
|
|
485
|
+
if @engine.respond_to?(:index_manager)
|
|
486
|
+
@engine.index_manager.drop_index(index_name, options)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
{
|
|
490
|
+
row_count: 0,
|
|
491
|
+
message: "DROP INDEX #{index_name}"
|
|
492
|
+
}
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def execute_begin_transaction(plan)
|
|
496
|
+
isolation_level = plan.isolation_level || :read_committed
|
|
497
|
+
transaction_id = @engine.begin_transaction(isolation_level)
|
|
498
|
+
|
|
499
|
+
{
|
|
500
|
+
row_count: 0,
|
|
501
|
+
transaction_id: transaction_id,
|
|
502
|
+
message: "BEGIN"
|
|
503
|
+
}
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def execute_commit(plan)
|
|
507
|
+
@engine.commit_transaction
|
|
508
|
+
|
|
509
|
+
{
|
|
510
|
+
row_count: 0,
|
|
511
|
+
message: "COMMIT"
|
|
512
|
+
}
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def execute_rollback(plan)
|
|
516
|
+
@engine.rollback_transaction
|
|
517
|
+
|
|
518
|
+
{
|
|
519
|
+
row_count: 0,
|
|
520
|
+
message: "ROLLBACK"
|
|
521
|
+
}
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def execute_savepoint(plan)
|
|
525
|
+
@engine.create_savepoint(plan.name)
|
|
526
|
+
{ row_count: 0, message: "SAVEPOINT #{plan.name}" }
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def execute_rollback_to_savepoint(plan)
|
|
530
|
+
@engine.rollback_to_savepoint(plan.name)
|
|
531
|
+
{ row_count: 0, message: "ROLLBACK TO SAVEPOINT #{plan.name}" }
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def execute_release_savepoint(plan)
|
|
535
|
+
@engine.release_savepoint(plan.name)
|
|
536
|
+
{ row_count: 0, message: "RELEASE SAVEPOINT #{plan.name}" }
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def execute_explain(plan)
|
|
540
|
+
statement = plan.statement
|
|
541
|
+
analyze = plan.analyze || false
|
|
542
|
+
|
|
543
|
+
# Generate execution plan for the statement
|
|
544
|
+
planner = Planner.new(@engine)
|
|
545
|
+
analyzed_plan = planner.plan(statement)
|
|
546
|
+
|
|
547
|
+
# Format the plan
|
|
548
|
+
plan_text = format_plan(analyzed_plan)
|
|
549
|
+
|
|
550
|
+
if analyze
|
|
551
|
+
# Execute and get actual stats
|
|
552
|
+
start_time = Time.now
|
|
553
|
+
result = execute(analyzed_plan)
|
|
554
|
+
elapsed_ms = ((Time.now - start_time) * 1000).round(2)
|
|
555
|
+
|
|
556
|
+
plan_text = "EXPLAIN ANALYZE:\n#{plan_text}\n" +
|
|
557
|
+
"Execution Time: #{elapsed_ms}ms\n" +
|
|
558
|
+
"Rows: #{result[:row_count]}"
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
{
|
|
562
|
+
rows: [{ "QUERY PLAN" => plan_text }],
|
|
563
|
+
row_count: 1,
|
|
564
|
+
column_names: ["QUERY PLAN"]
|
|
565
|
+
}
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Scan operations
|
|
569
|
+
def scan_table(plan)
|
|
570
|
+
table_name = plan.table_name
|
|
571
|
+
return [{}] unless table_name
|
|
572
|
+
return @cte_rows.fetch(table_name.to_s) if @cte_rows.key?(table_name.to_s)
|
|
573
|
+
if @engine.catalog.respond_to?(:find_view) && (view = @engine.catalog.find_view(table_name))
|
|
574
|
+
statement = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(view.query).tokenize).parse.first
|
|
575
|
+
return scan_table(Planner.new(@engine).plan(statement))
|
|
576
|
+
end
|
|
577
|
+
columns = @engine.table_columns(table_name)
|
|
578
|
+
|
|
579
|
+
if plan.scan_type == :index && plan.index
|
|
580
|
+
# Use index scan
|
|
581
|
+
scan = Indexes::IndexScan.new(plan.index, :full)
|
|
582
|
+
results = scan.execute
|
|
583
|
+
row_ids = results.map { |r| r[:row_id] }
|
|
584
|
+
|
|
585
|
+
# Fetch rows by row_id
|
|
586
|
+
rows = []
|
|
587
|
+
row_ids.each do |row_id|
|
|
588
|
+
row = @engine.select_row(table_name, row_id, columns)
|
|
589
|
+
rows << row if row
|
|
590
|
+
end
|
|
591
|
+
rows
|
|
592
|
+
else
|
|
593
|
+
# Sequential scan
|
|
594
|
+
@engine.select_rows(table_name, columns)
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def qualify_rows(rows, table_reference)
|
|
599
|
+
table_name = table_reference.respond_to?(:name) ? table_reference.name : table_reference
|
|
600
|
+
alias_name = table_reference.respond_to?(:alias_name) ? table_reference.alias_name : nil
|
|
601
|
+
rows.map do |row|
|
|
602
|
+
row.each_with_object({}) do |(key, value), qualified|
|
|
603
|
+
key = key.to_s
|
|
604
|
+
qualified[key] = value
|
|
605
|
+
qualified["#{table_name}.#{key}"] = value
|
|
606
|
+
qualified["#{alias_name}.#{key}"] = value if alias_name
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def execute_join(left_rows, right_rows, join)
|
|
612
|
+
return left_rows.flat_map { |left_row| right_rows.map { |right_row| merge_join_rows(left_row, right_row) } } if join[:type] == :cross
|
|
613
|
+
|
|
614
|
+
result = []
|
|
615
|
+
matched_right = Array.new(right_rows.length, false)
|
|
616
|
+
left_rows.each do |left_row|
|
|
617
|
+
check_deadline!
|
|
618
|
+
matches = right_rows.each_index.select do |index|
|
|
619
|
+
evaluate_predicate(join[:predicate], merge_join_rows(left_row, right_rows[index]))
|
|
620
|
+
end
|
|
621
|
+
if matches.empty?
|
|
622
|
+
result << merge_join_rows(left_row, nil) if %i[left full].include?(join[:type])
|
|
623
|
+
else
|
|
624
|
+
matches.each do |index|
|
|
625
|
+
matched_right[index] = true
|
|
626
|
+
result << merge_join_rows(left_row, right_rows[index])
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
if %i[right full].include?(join[:type])
|
|
631
|
+
right_rows.each_with_index do |right_row, index|
|
|
632
|
+
result << merge_join_rows(nil, right_row) unless matched_right[index]
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
result
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def merge_join_rows(left_row, right_row)
|
|
639
|
+
(left_row || {}).merge(right_row || {}) { |_key, left_value, _right_value| left_value }
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Predicate evaluation
|
|
643
|
+
def evaluate_predicate(predicate, row)
|
|
644
|
+
case predicate
|
|
645
|
+
when Predicate::Comparison
|
|
646
|
+
left_val = evaluate_expression(predicate.left, row)
|
|
647
|
+
right_val = evaluate_expression(predicate.right, row)
|
|
648
|
+
compare_values(left_val, right_val, predicate.operator)
|
|
649
|
+
when Predicate::And
|
|
650
|
+
evaluate_predicate(predicate.left, row) && evaluate_predicate(predicate.right, row)
|
|
651
|
+
when Predicate::Or
|
|
652
|
+
evaluate_predicate(predicate.left, row) || evaluate_predicate(predicate.right, row)
|
|
653
|
+
when Predicate::Not
|
|
654
|
+
!evaluate_predicate(predicate.operand, row)
|
|
655
|
+
when Predicate::Between
|
|
656
|
+
val = evaluate_expression(predicate.expression, row)
|
|
657
|
+
low = evaluate_expression(predicate.low, row)
|
|
658
|
+
high = evaluate_expression(predicate.high, row)
|
|
659
|
+
val >= low && val <= high
|
|
660
|
+
when Predicate::In
|
|
661
|
+
val = evaluate_expression(predicate.expression, row)
|
|
662
|
+
predicate.values.any? { |v| val == evaluate_expression(v, row) }
|
|
663
|
+
when Predicate::IsNull
|
|
664
|
+
val = evaluate_expression(predicate.expression, row)
|
|
665
|
+
predicate.negated ? !val.nil? : val.nil?
|
|
666
|
+
when Predicate::Like
|
|
667
|
+
val = evaluate_expression(predicate.expression, row).to_s
|
|
668
|
+
pattern = evaluate_expression(predicate.pattern, row).to_s
|
|
669
|
+
like_match?(val, pattern, predicate.case_sensitive)
|
|
670
|
+
else
|
|
671
|
+
true
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def evaluate_expression(expr, row = nil)
|
|
676
|
+
case expr
|
|
677
|
+
when SQL::AST::Literal
|
|
678
|
+
expr.value
|
|
679
|
+
when SQL::AST::Identifier
|
|
680
|
+
return nil unless row
|
|
681
|
+
|
|
682
|
+
qualified_name = expr.table && "#{expr.table}.#{expr.name}"
|
|
683
|
+
row_value(row, qualified_name, expr.name, expr.name.to_sym)
|
|
684
|
+
when SQL::AST::UnaryOp
|
|
685
|
+
apply_ast_unary_op(expr.operator, evaluate_expression(expr.operand, row))
|
|
686
|
+
when SQL::AST::BinaryOp
|
|
687
|
+
apply_ast_binary_op(
|
|
688
|
+
expr.operator,
|
|
689
|
+
evaluate_expression(expr.left, row),
|
|
690
|
+
evaluate_expression(expr.right, row)
|
|
691
|
+
)
|
|
692
|
+
when SQL::AST::FunctionCall
|
|
693
|
+
if expr.window
|
|
694
|
+
row && row[window_key(expr)]
|
|
695
|
+
else
|
|
696
|
+
apply_function(expr.name, expr.arguments.map { |argument| evaluate_expression(argument, row) })
|
|
697
|
+
end
|
|
698
|
+
when SQL::AST::Subquery
|
|
699
|
+
result = child_executor(@cte_rows).execute(Planner.new(@engine).plan(expr.query))
|
|
700
|
+
rows = result[:rows]
|
|
701
|
+
raise ExecutionError, "Scalar subquery returned more than one row" if rows.size > 1
|
|
702
|
+
rows.empty? ? nil : rows.first.values.first
|
|
703
|
+
when Expression::Literal
|
|
704
|
+
expr.value
|
|
705
|
+
when Expression::Column
|
|
706
|
+
return nil unless row
|
|
707
|
+
|
|
708
|
+
qualified_name = expr.table && "#{expr.table}.#{expr.name}"
|
|
709
|
+
row_value(row, qualified_name, expr.name, expr.name.to_sym)
|
|
710
|
+
when Expression::BinaryOp
|
|
711
|
+
left = evaluate_expression(expr.left, row)
|
|
712
|
+
right = evaluate_expression(expr.right, row)
|
|
713
|
+
apply_binary_op(left, right, expr.operator)
|
|
714
|
+
when Expression::UnaryOp
|
|
715
|
+
operand = evaluate_expression(expr.operand, row)
|
|
716
|
+
apply_unary_op(operand, expr.operator)
|
|
717
|
+
when Expression::Function
|
|
718
|
+
args = expr.arguments.map { |arg| evaluate_expression(arg, row) }
|
|
719
|
+
apply_function(expr.name, args)
|
|
720
|
+
when Expression::Parameter
|
|
721
|
+
expr.value
|
|
722
|
+
else
|
|
723
|
+
nil
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Helper methods
|
|
728
|
+
def compare_values(left, right, operator)
|
|
729
|
+
return false if left.nil? || right.nil?
|
|
730
|
+
|
|
731
|
+
case operator
|
|
732
|
+
when :eq then left == right
|
|
733
|
+
when :ne then left != right
|
|
734
|
+
when :lt then left < right
|
|
735
|
+
when :lte then left <= right
|
|
736
|
+
when :gt then left > right
|
|
737
|
+
when :gte then left >= right
|
|
738
|
+
else false
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def apply_binary_op(left, right, operator)
|
|
743
|
+
return nil if left.nil? || right.nil?
|
|
744
|
+
|
|
745
|
+
case operator
|
|
746
|
+
when :plus then left + right
|
|
747
|
+
when :minus then left - right
|
|
748
|
+
when :multiply then left * right
|
|
749
|
+
when :divide then left / right if right != 0
|
|
750
|
+
when :modulo then left % right if right != 0
|
|
751
|
+
when :concat then left.to_s + right.to_s
|
|
752
|
+
else nil
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def apply_ast_unary_op(operator, value)
|
|
757
|
+
return nil if value.nil?
|
|
758
|
+
|
|
759
|
+
case operator
|
|
760
|
+
when SQL::Token::Type::PLUS then value
|
|
761
|
+
when SQL::Token::Type::MINUS then -value
|
|
762
|
+
when SQL::Token::Type::NOT then !value
|
|
763
|
+
else value
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def apply_ast_binary_op(operator, left, right)
|
|
768
|
+
case operator
|
|
769
|
+
# SQL comparisons involving NULL evaluate to UNKNOWN, represented by
|
|
770
|
+
# nil here. WHERE filtering already treats UNKNOWN as non-matching.
|
|
771
|
+
when SQL::Token::Type::EQ then left.nil? || right.nil? ? nil : left == right
|
|
772
|
+
when SQL::Token::Type::NE then left.nil? || right.nil? ? nil : left != right
|
|
773
|
+
when SQL::Token::Type::LT then !left.nil? && !right.nil? && left < right
|
|
774
|
+
when SQL::Token::Type::LTE then !left.nil? && !right.nil? && left <= right
|
|
775
|
+
when SQL::Token::Type::GT then !left.nil? && !right.nil? && left > right
|
|
776
|
+
when SQL::Token::Type::GTE then !left.nil? && !right.nil? && left >= right
|
|
777
|
+
when SQL::Token::Type::AND then sql_and(left, right)
|
|
778
|
+
when SQL::Token::Type::OR then sql_or(left, right)
|
|
779
|
+
when SQL::Token::Type::PLUS then apply_binary_op(left, right, :plus)
|
|
780
|
+
when SQL::Token::Type::MINUS then apply_binary_op(left, right, :minus)
|
|
781
|
+
when SQL::Token::Type::STAR then apply_binary_op(left, right, :multiply)
|
|
782
|
+
when SQL::Token::Type::SLASH then apply_binary_op(left, right, :divide)
|
|
783
|
+
when SQL::Token::Type::PERCENT then apply_binary_op(left, right, :modulo)
|
|
784
|
+
else nil
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def apply_unary_op(operand, operator)
|
|
789
|
+
return nil if operand.nil?
|
|
790
|
+
|
|
791
|
+
case operator
|
|
792
|
+
when :negate then -operand
|
|
793
|
+
when :not then !operand
|
|
794
|
+
else operand
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def sql_and(left, right)
|
|
799
|
+
return false if left == false || right == false
|
|
800
|
+
return nil if left.nil? || right.nil?
|
|
801
|
+
|
|
802
|
+
!!left && !!right
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
def sql_or(left, right)
|
|
806
|
+
return true if left == true || right == true
|
|
807
|
+
return nil if left.nil? || right.nil?
|
|
808
|
+
|
|
809
|
+
!!left || !!right
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def row_value(row, *keys)
|
|
813
|
+
keys.compact.each do |key|
|
|
814
|
+
return row[key] if row.respond_to?(:key?) && row.key?(key)
|
|
815
|
+
end
|
|
816
|
+
nil
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def child_executor(cte_rows)
|
|
820
|
+
self.class.new(
|
|
821
|
+
@engine,
|
|
822
|
+
cte_rows: cte_rows,
|
|
823
|
+
deadline_at: @deadline_at,
|
|
824
|
+
cancellation: @cancellation
|
|
825
|
+
)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
def check_deadline!
|
|
829
|
+
if @cancellation&.cancelled?
|
|
830
|
+
raise ExecutionError.new("Request cancelled by client", code: "cancelled")
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
return unless @deadline_at && Time.now >= @deadline_at
|
|
834
|
+
|
|
835
|
+
raise ExecutionError.new("Request deadline exceeded during execution", code: "deadline_exceeded")
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def apply_function(name, args)
|
|
839
|
+
case name.to_s.upcase
|
|
840
|
+
when "COUNT"
|
|
841
|
+
args.compact.size
|
|
842
|
+
when "SUM"
|
|
843
|
+
args.compact.sum
|
|
844
|
+
when "AVG"
|
|
845
|
+
vals = args.compact
|
|
846
|
+
vals.empty? ? 0 : vals.sum / vals.size.to_f
|
|
847
|
+
when "MIN"
|
|
848
|
+
args.compact.min
|
|
849
|
+
when "MAX"
|
|
850
|
+
args.compact.max
|
|
851
|
+
when "LOWER"
|
|
852
|
+
args.first.to_s.downcase
|
|
853
|
+
when "UPPER"
|
|
854
|
+
args.first.to_s.upcase
|
|
855
|
+
when "LENGTH"
|
|
856
|
+
args.first.to_s.length
|
|
857
|
+
when "COALESCE"
|
|
858
|
+
args.find { |arg| !arg.nil? }
|
|
859
|
+
when "NOW"
|
|
860
|
+
Time.now
|
|
861
|
+
when "CURRENT_DATE"
|
|
862
|
+
Date.today
|
|
863
|
+
when "CURRENT_TIME"
|
|
864
|
+
Time.now
|
|
865
|
+
else
|
|
866
|
+
nil
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def like_match?(value, pattern, case_sensitive = true)
|
|
871
|
+
return false if value.nil? || pattern.nil?
|
|
872
|
+
|
|
873
|
+
str = case_sensitive ? value : value.downcase
|
|
874
|
+
pat = case_sensitive ? pattern : pattern.downcase
|
|
875
|
+
|
|
876
|
+
# Convert SQL LIKE pattern to regex
|
|
877
|
+
regex_str = Regexp.escape(pat)
|
|
878
|
+
.gsub('%', '.*')
|
|
879
|
+
.gsub('_', '.')
|
|
880
|
+
Regexp.new("^#{regex_str}$").match?(str)
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def project_row(row, projections)
|
|
884
|
+
result = {}
|
|
885
|
+
projections.each do |proj|
|
|
886
|
+
expression = proj.respond_to?(:expression) ? proj.expression : proj
|
|
887
|
+
|
|
888
|
+
if expression.is_a?(SQL::AST::Star)
|
|
889
|
+
if expression.table
|
|
890
|
+
prefix = "#{expression.table}."
|
|
891
|
+
row.each do |key, value|
|
|
892
|
+
key = key.to_s
|
|
893
|
+
next unless key.start_with?(prefix)
|
|
894
|
+
|
|
895
|
+
column = key.delete_prefix(prefix)
|
|
896
|
+
result[column] = value unless column.start_with?("_")
|
|
897
|
+
end
|
|
898
|
+
else
|
|
899
|
+
row.each do |key, value|
|
|
900
|
+
key = key.to_s
|
|
901
|
+
result[key] = value unless key.include?(".") || key.start_with?("_")
|
|
902
|
+
end
|
|
903
|
+
end
|
|
904
|
+
elsif expression.is_a?(String) || expression.is_a?(Symbol)
|
|
905
|
+
result[expression.to_s] = row[expression.to_s]
|
|
906
|
+
else
|
|
907
|
+
result[projection_name(proj)] = evaluate_expression(expression, row)
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
result
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
def distinct_rows(rows)
|
|
914
|
+
seen = Set.new
|
|
915
|
+
rows.select do |row|
|
|
916
|
+
key = row.to_hash
|
|
917
|
+
if seen.include?(key)
|
|
918
|
+
false
|
|
919
|
+
else
|
|
920
|
+
seen.add(key)
|
|
921
|
+
true
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def group_rows(rows, group_by, aggregates)
|
|
927
|
+
groups = {}
|
|
928
|
+
rows.each do |row|
|
|
929
|
+
key = group_by.map { |col| row[col.to_s] }
|
|
930
|
+
groups[key] ||= []
|
|
931
|
+
groups[key] << row
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
result = []
|
|
935
|
+
groups.each do |key, group_rows|
|
|
936
|
+
result_row = {}
|
|
937
|
+
group_by.each_with_index do |col, idx|
|
|
938
|
+
result_row[col.to_s] = key[idx]
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
aggregates&.each do |agg|
|
|
942
|
+
result_row[agg.alias || agg.name] = apply_aggregate(agg, group_rows)
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
result << result_row
|
|
946
|
+
end
|
|
947
|
+
result
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def aggregate_rows(rows, group_by, projections)
|
|
951
|
+
groups = {}
|
|
952
|
+
if group_by.empty?
|
|
953
|
+
groups[[]] = rows
|
|
954
|
+
else
|
|
955
|
+
rows.each do |row|
|
|
956
|
+
key = group_by.map { |expression| evaluate_expression(expression, row) }
|
|
957
|
+
(groups[key] ||= []) << row
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
groups.map do |_key, group|
|
|
962
|
+
representative = group.first || {}
|
|
963
|
+
projections.each_with_object({}) do |projection, result|
|
|
964
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
965
|
+
name = projection_name(projection)
|
|
966
|
+
result[name] = if aggregate_function?(expression)
|
|
967
|
+
apply_aggregate_function(expression, group)
|
|
968
|
+
else
|
|
969
|
+
evaluate_expression(expression, representative)
|
|
970
|
+
end
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def aggregate_function?(expression)
|
|
976
|
+
expression.is_a?(SQL::AST::FunctionCall) && !expression.window &&
|
|
977
|
+
%w[COUNT SUM AVG MIN MAX].include?(expression.name.to_s.upcase)
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
def apply_window_functions!(rows, projections)
|
|
981
|
+
projections.each do |projection|
|
|
982
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
983
|
+
next unless expression.is_a?(SQL::AST::FunctionCall) && expression.window
|
|
984
|
+
|
|
985
|
+
spec = expression.window
|
|
986
|
+
partitions = {}
|
|
987
|
+
rows.each { |row| (partitions[spec[:partition_by].map { |part| evaluate_expression(part, row) }] ||= []) << row }
|
|
988
|
+
partitions.each_value do |partition_rows|
|
|
989
|
+
ordered_rows = sort_window_rows(partition_rows, spec[:order_by])
|
|
990
|
+
ordered_rows.each_with_index do |row, index|
|
|
991
|
+
row[window_key(expression)] = window_value(expression, ordered_rows, index, spec[:order_by], spec[:frame])
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
def window_key(expression)
|
|
998
|
+
"__rubydb_window_#{expression.object_id}"
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def sort_window_rows(rows, order_by)
|
|
1002
|
+
return rows.dup if order_by.empty?
|
|
1003
|
+
|
|
1004
|
+
rows.sort do |left, right|
|
|
1005
|
+
comparison = 0
|
|
1006
|
+
order_by.each do |item|
|
|
1007
|
+
left_value = evaluate_expression(item.expression, left)
|
|
1008
|
+
right_value = evaluate_expression(item.expression, right)
|
|
1009
|
+
comparison = compare_sort_values(left_value, right_value)
|
|
1010
|
+
comparison = -comparison if item.direction == :desc
|
|
1011
|
+
break unless comparison.zero?
|
|
1012
|
+
end
|
|
1013
|
+
comparison
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def compare_sort_values(left, right)
|
|
1018
|
+
return 0 if left.nil? && right.nil?
|
|
1019
|
+
return -1 if left.nil?
|
|
1020
|
+
return 1 if right.nil?
|
|
1021
|
+
|
|
1022
|
+
left <=> right
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
def window_value(expression, rows, index, order_by, frame = nil)
|
|
1026
|
+
rows = window_frame_rows(rows, index, frame) if frame
|
|
1027
|
+
name = expression.name.to_s.upcase
|
|
1028
|
+
if %w[ROW_NUMBER RANK DENSE_RANK].include?(name)
|
|
1029
|
+
return index + 1 if name == "ROW_NUMBER" || order_by.empty?
|
|
1030
|
+
|
|
1031
|
+
keys = rows.map { |row| order_by.map { |item| evaluate_expression(item.expression, row) } }
|
|
1032
|
+
current = keys[index]
|
|
1033
|
+
return keys.index(current) + 1 if name == "RANK"
|
|
1034
|
+
|
|
1035
|
+
return keys[0..index].uniq.index(current) + 1
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
argument = expression.arguments.first
|
|
1039
|
+
values = if argument.is_a?(SQL::AST::Star)
|
|
1040
|
+
rows
|
|
1041
|
+
else
|
|
1042
|
+
rows.map { |row| evaluate_expression(argument, row) }.compact
|
|
1043
|
+
end
|
|
1044
|
+
values = values.uniq if expression.distinct
|
|
1045
|
+
case name
|
|
1046
|
+
when "COUNT" then values.size
|
|
1047
|
+
when "SUM" then values.empty? ? nil : values.sum
|
|
1048
|
+
when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
|
|
1049
|
+
when "MIN" then values.min
|
|
1050
|
+
when "MAX" then values.max
|
|
1051
|
+
else
|
|
1052
|
+
raise ExecutionError, "Unsupported window function: #{expression.name}"
|
|
1053
|
+
end
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
def window_frame_rows(rows, index, frame)
|
|
1057
|
+
start_index = window_frame_index(frame[:start], index, rows.size, start: true)
|
|
1058
|
+
finish = frame[:finish] || { kind: :current_row }
|
|
1059
|
+
end_index = window_frame_index(finish, index, rows.size, start: false)
|
|
1060
|
+
return [] if start_index > end_index
|
|
1061
|
+
|
|
1062
|
+
rows[start_index..end_index] || []
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
def window_frame_index(boundary, index, size, start:)
|
|
1066
|
+
case boundary[:kind]
|
|
1067
|
+
when :unbounded_preceding then 0
|
|
1068
|
+
when :unbounded_following then size - 1
|
|
1069
|
+
when :current_row then index
|
|
1070
|
+
when :preceding then [index - boundary[:value], 0].max
|
|
1071
|
+
when :following then [index + boundary[:value], size - 1].min
|
|
1072
|
+
else raise ExecutionError, "Unsupported window frame boundary: #{boundary[:kind]}"
|
|
1073
|
+
end
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
def apply_aggregate_function(expression, rows)
|
|
1077
|
+
argument = expression.arguments.first
|
|
1078
|
+
values = if argument.is_a?(SQL::AST::Star)
|
|
1079
|
+
rows
|
|
1080
|
+
else
|
|
1081
|
+
rows.map { |row| evaluate_expression(argument, row) }.compact
|
|
1082
|
+
end
|
|
1083
|
+
values = values.uniq if expression.distinct
|
|
1084
|
+
|
|
1085
|
+
case expression.name.to_s.upcase
|
|
1086
|
+
when "COUNT" then values.size
|
|
1087
|
+
when "SUM" then values.empty? ? nil : values.sum
|
|
1088
|
+
when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
|
|
1089
|
+
when "MIN" then values.min
|
|
1090
|
+
when "MAX" then values.max
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
def apply_aggregate(agg, rows)
|
|
1095
|
+
values = rows.map { |row| row[agg.column.to_s] }.compact
|
|
1096
|
+
|
|
1097
|
+
case agg.name.to_s.upcase
|
|
1098
|
+
when "COUNT" then values.size
|
|
1099
|
+
when "SUM" then values.sum
|
|
1100
|
+
when "AVG" then values.empty? ? 0 : values.sum / values.size.to_f
|
|
1101
|
+
when "MIN" then values.min
|
|
1102
|
+
when "MAX" then values.max
|
|
1103
|
+
else nil
|
|
1104
|
+
end
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
def sort_rows(rows, order_by)
|
|
1108
|
+
rows.sort do |a, b|
|
|
1109
|
+
comparison = 0
|
|
1110
|
+
order_by.each do |order|
|
|
1111
|
+
col = (order.is_a?(Hash) ? order[:column] : order.column).to_s
|
|
1112
|
+
val_a = a[col]
|
|
1113
|
+
val_b = b[col]
|
|
1114
|
+
|
|
1115
|
+
if val_a.nil? && val_b.nil?
|
|
1116
|
+
comparison = 0
|
|
1117
|
+
elsif val_a.nil?
|
|
1118
|
+
comparison = -1
|
|
1119
|
+
elsif val_b.nil?
|
|
1120
|
+
comparison = 1
|
|
1121
|
+
else
|
|
1122
|
+
comparison = val_a <=> val_b
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
direction = order.is_a?(Hash) ? order[:direction] : order.direction
|
|
1126
|
+
comparison = -comparison if direction == :desc
|
|
1127
|
+
break unless comparison == 0
|
|
1128
|
+
end
|
|
1129
|
+
comparison
|
|
1130
|
+
end
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
def get_column_names(plan)
|
|
1134
|
+
if plan.projections
|
|
1135
|
+
plan.projections.map { |projection| projection_name(projection) }
|
|
1136
|
+
else
|
|
1137
|
+
@engine.table_columns(plan.table_name).map(&:name)
|
|
1138
|
+
end
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
def projection_name(projection)
|
|
1142
|
+
return projection.to_s if projection.is_a?(String) || projection.is_a?(Symbol)
|
|
1143
|
+
|
|
1144
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
1145
|
+
alias_name = if projection.respond_to?(:alias_name)
|
|
1146
|
+
projection.alias_name
|
|
1147
|
+
elsif projection.respond_to?(:alias)
|
|
1148
|
+
projection.alias
|
|
1149
|
+
end
|
|
1150
|
+
return alias_name.to_s if alias_name
|
|
1151
|
+
|
|
1152
|
+
return expression.name.to_s if expression.respond_to?(:name) && expression.name
|
|
1153
|
+
return expression.to_sql if expression.respond_to?(:to_sql)
|
|
1154
|
+
|
|
1155
|
+
expression.to_s
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
def format_plan(plan)
|
|
1159
|
+
lines = []
|
|
1160
|
+
lines << "QUERY PLAN"
|
|
1161
|
+
lines << "=" * 40
|
|
1162
|
+
|
|
1163
|
+
if plan.scan_type == :index
|
|
1164
|
+
lines << "Index Scan on #{plan.table_name} using #{plan.index.name}"
|
|
1165
|
+
else
|
|
1166
|
+
lines << "Seq Scan on #{plan.table_name}"
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
if plan.predicate
|
|
1170
|
+
lines << " Filter: #{plan.predicate.to_s}"
|
|
1171
|
+
end
|
|
1172
|
+
|
|
1173
|
+
if plan.order_by && plan.order_by.any?
|
|
1174
|
+
order_str = plan.order_by.map { |o| "#{o.column} #{o.direction}" }.join(", ")
|
|
1175
|
+
lines << " Order By: #{order_str}"
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
if plan.limit
|
|
1179
|
+
lines << " Limit: #{plan.limit}"
|
|
1180
|
+
end
|
|
1181
|
+
|
|
1182
|
+
if plan.offset
|
|
1183
|
+
lines << " Offset: #{plan.offset}"
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
lines << " Estimated Cost: #{plan.estimated_cost}"
|
|
1187
|
+
lines << " Estimated Rows: #{plan.estimated_rows}"
|
|
1188
|
+
lines.join("\n")
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
end
|
|
1192
|
+
end
|