rubydb 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
- data/.github/workflows/benchmark.yml +26 -26
- data/.github/workflows/compatibility.yml +63 -63
- data/.github/workflows/fuzz.yml +33 -33
- data/.github/workflows/lint.yml +21 -21
- data/.github/workflows/operations.yml +24 -24
- data/.github/workflows/production-validation.yml +111 -111
- data/.github/workflows/release.yml +77 -77
- data/.github/workflows/security.yml +39 -37
- data/.github/workflows/test.yml +26 -26
- data/.github/workflows/workload.yml +58 -58
- data/.gitignore +16 -5
- data/.rubocop.yml +50 -44
- data/.standard.yml +9 -14
- data/ARCHITECTURE.md +21 -21
- data/CHANGELOG.md +57 -27
- data/CODE_OF_CONDUCT.md +13 -13
- data/CONTRIBUTING.md +29 -29
- data/GOVERNANCE.md +16 -16
- data/Gemfile +18 -17
- data/Gemfile.lock +125 -71
- data/README.md +168 -12
- data/ROADMAP.md +27 -27
- data/Rakefile +76 -71
- data/SECURITY.md +54 -54
- data/SUPPORT.md +14 -14
- data/accelerator/bin/SHA256SUMS +6 -0
- data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
- data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
- data/accelerator/go.mod +3 -0
- data/accelerator/internal/execution/aggregate.go +94 -0
- data/accelerator/internal/execution/distinct.go +22 -0
- data/accelerator/internal/execution/filter.go +73 -0
- data/accelerator/internal/execution/join.go +79 -0
- data/accelerator/internal/execution/operators.go +167 -0
- data/accelerator/internal/execution/scan.go +20 -0
- data/accelerator/internal/execution/sort.go +62 -0
- data/accelerator/internal/execution/types.go +136 -0
- data/accelerator/internal/execution/value.go +67 -0
- data/accelerator/internal/memory/arena.go +47 -0
- data/accelerator/internal/memory/reuse.go +22 -0
- data/accelerator/internal/metrics/registry.go +67 -0
- data/accelerator/internal/parallel/bounded_queue.go +56 -0
- data/accelerator/internal/parallel/scheduler.go +47 -0
- data/accelerator/internal/parallel/worker_pool.go +53 -0
- data/accelerator/internal/protocol/cancellation.go +48 -0
- data/accelerator/internal/protocol/columnar.go +263 -0
- data/accelerator/internal/protocol/frame.go +187 -0
- data/accelerator/internal/runtime/worker.go +521 -0
- data/accelerator/internal/storage/page_reader.go +81 -0
- data/accelerator/internal/storage/snapshot_scan.go +539 -0
- data/accelerator/internal/wal/checksum.go +13 -0
- data/accelerator/internal/wal/compression.go +41 -0
- data/accelerator/internal/wal/group_commit.go +24 -0
- data/accelerator/internal/wal/record_encoder.go +40 -0
- data/adapters/activerecord/Gemfile +11 -11
- data/adapters/activerecord/README.md +8 -3
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +881 -879
- data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
- data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
- data/adapters/ruby/README.md +18 -18
- data/adapters/sequel/README.md +11 -11
- data/config/monitoring/prometheus-alerts.yml +39 -39
- data/config/production.yml +36 -36
- data/docs/README.md +77 -71
- data/docs/architecture/concurrency.md +14 -14
- data/docs/architecture/current-state.md +125 -125
- data/docs/architecture/execution-engine.md +25 -25
- data/docs/architecture/go-accelerator.md +179 -0
- data/docs/architecture/indexes.md +19 -19
- data/docs/architecture/mvcc.md +19 -19
- data/docs/architecture/overview.md +13 -13
- data/docs/architecture/pages.md +11 -11
- data/docs/architecture/production-roadmap.md +82 -82
- data/docs/architecture/query-planner.md +20 -20
- data/docs/architecture/recovery.md +18 -18
- data/docs/architecture/sql-engine.md +12 -12
- data/docs/architecture/storage-engine.md +14 -14
- data/docs/architecture/transactions.md +10 -10
- data/docs/architecture/wal.md +28 -28
- data/docs/cli-cheatsheet.md +98 -98
- data/docs/cli.md +299 -275
- data/docs/contributing/architecture.md +9 -9
- data/docs/contributing/benchmarking.md +30 -14
- data/docs/contributing/development.md +16 -16
- data/docs/contributing/release-process.md +49 -49
- data/docs/contributing/testing.md +16 -16
- data/docs/debugging.md +229 -229
- data/docs/developer/branching.md +10 -10
- data/docs/developer/database-diff.md +10 -10
- data/docs/developer/local-development.md +49 -17
- data/docs/developer/snapshots.md +9 -9
- data/docs/developer/temporal-data.md +10 -10
- data/docs/developer-guide.md +297 -297
- data/docs/getting-started/first-database.md +16 -16
- data/docs/getting-started/first-query.md +13 -13
- data/docs/getting-started/installation.md +19 -19
- data/docs/getting-started/local-to-production.md +300 -300
- data/docs/getting-started/quickstart.md +17 -17
- data/docs/getting-started/rails.md +16 -16
- data/docs/hardening_backlog.md +93 -93
- data/docs/lessons-learned.md +112 -112
- data/docs/operations/backups.md +33 -33
- data/docs/operations/disaster-recovery.md +31 -31
- data/docs/operations/failover.md +30 -30
- data/docs/operations/monitoring.md +25 -25
- data/docs/operations/production-guide.md +295 -295
- data/docs/operations/production-runbook.md +45 -45
- data/docs/operations/replication.md +33 -33
- data/docs/operations/restore.md +6 -6
- data/docs/operations/runbook.md +34 -34
- data/docs/operations/upgrades.md +14 -14
- data/docs/operations/workload-testing.md +17 -17
- data/docs/production-readiness.md +118 -118
- data/docs/production_validation.md +150 -150
- data/docs/rails/active-record.md +11 -11
- data/docs/rails/compatibility-guide.md +90 -90
- data/docs/rails/database-yml.md +92 -92
- data/docs/rails/installation.md +17 -17
- data/docs/rails/migrations.md +17 -17
- data/docs/rails/production.md +82 -82
- data/docs/rails/troubleshooting.md +18 -18
- data/docs/release.md +25 -25
- data/docs/server/architecture.md +10 -10
- data/docs/server/authentication.md +10 -10
- data/docs/server/configuration.md +16 -16
- data/docs/server/connection-pooling.md +10 -10
- data/docs/server/deployment.md +10 -10
- data/docs/server/protocol.md +12 -12
- data/docs/sql/compatibility-guide.md +82 -82
- data/docs/sql/compatibility.md +39 -39
- data/docs/sql/data-types.md +10 -10
- data/docs/sql/functions.md +9 -9
- data/docs/sql/joins.md +9 -9
- data/docs/sql/operators.md +9 -9
- data/docs/sql/sqlite-compatibility.md +21 -21
- data/docs/sql/syntax.md +10 -10
- data/docs/sql/transactions.md +10 -10
- data/docs/troubleshooting.md +244 -244
- data/lessons/01-foundations.md +73 -0
- data/lessons/02-local-development.md +121 -0
- data/lessons/03-embedded-rubydb.md +99 -0
- data/lessons/04-rails-complex-apps.md +138 -0
- data/lessons/05-rubydb-production-server.md +237 -0
- data/lessons/06-postgresql-massive-apps.md +96 -0
- data/lessons/07-hybrid-microservices.md +179 -0
- data/lessons/08-migrations-backups-recovery.md +86 -0
- data/lessons/09-observability-security-scale.md +87 -0
- data/lessons/10-release-readiness.md +192 -0
- data/lessons/11-community-adapter.md +323 -0
- data/lessons/12-rails-ecommerce-pressure.md +263 -0
- data/lib/rubydb/accelerator/client.rb +451 -0
- data/lib/rubydb/accelerator/error.rb +22 -0
- data/lib/rubydb/accelerator/manager.rb +606 -0
- data/lib/rubydb/accelerator.rb +13 -0
- data/lib/rubydb/backup/archive.rb +332 -334
- data/lib/rubydb/backup/backup.rb +400 -401
- data/lib/rubydb/backup/incremental.rb +349 -353
- data/lib/rubydb/backup/restore.rb +289 -290
- data/lib/rubydb/backup/snapshot.rb +265 -267
- data/lib/rubydb/backup/verification.rb +276 -279
- data/lib/rubydb/branching/branch.rb +181 -181
- data/lib/rubydb/branching/branch_manager.rb +307 -311
- data/lib/rubydb/branching/branch_metadata.rb +140 -140
- data/lib/rubydb/branching/checkout.rb +165 -166
- data/lib/rubydb/branching/copy_on_write.rb +272 -272
- data/lib/rubydb/branching/diff.rb +137 -138
- data/lib/rubydb/branching/merge.rb +282 -285
- data/lib/rubydb/build_info.rb +15 -15
- data/lib/rubydb/catalog/catalog.rb +391 -391
- data/lib/rubydb/catalog/column.rb +112 -112
- data/lib/rubydb/catalog/constraint.rb +180 -180
- data/lib/rubydb/catalog/database.rb +184 -184
- data/lib/rubydb/catalog/index.rb +97 -97
- data/lib/rubydb/catalog/schema.rb +103 -103
- data/lib/rubydb/catalog/sequence.rb +90 -90
- data/lib/rubydb/catalog/system_catalog.rb +698 -698
- data/lib/rubydb/catalog/table.rb +178 -178
- data/lib/rubydb/catalog/trigger.rb +102 -102
- data/lib/rubydb/catalog/view.rb +66 -66
- data/lib/rubydb/cli/application.rb +168 -163
- data/lib/rubydb/cli/commands/accelerator.rb +72 -0
- data/lib/rubydb/cli/commands/backup.rb +80 -81
- data/lib/rubydb/cli/commands/branch.rb +72 -72
- data/lib/rubydb/cli/commands/checkout.rb +54 -54
- data/lib/rubydb/cli/commands/create.rb +58 -58
- data/lib/rubydb/cli/commands/diff.rb +76 -77
- data/lib/rubydb/cli/commands/doctor.rb +77 -74
- data/lib/rubydb/cli/commands/drop.rb +57 -57
- data/lib/rubydb/cli/commands/init.rb +101 -102
- data/lib/rubydb/cli/commands/inspect.rb +95 -95
- data/lib/rubydb/cli/commands/merge.rb +63 -63
- data/lib/rubydb/cli/commands/migrate.rb +62 -62
- data/lib/rubydb/cli/commands/restart.rb +42 -39
- data/lib/rubydb/cli/commands/restore.rb +121 -121
- data/lib/rubydb/cli/commands/shell.rb +365 -365
- data/lib/rubydb/cli/commands/snapshot.rb +79 -79
- data/lib/rubydb/cli/commands/start.rb +88 -82
- data/lib/rubydb/cli/commands/status.rb +96 -92
- data/lib/rubydb/cli/commands/stop.rb +47 -47
- data/lib/rubydb/cli/commands/vacuum.rb +58 -58
- data/lib/rubydb/cli/formatter.rb +221 -221
- data/lib/rubydb/cli/output.rb +168 -168
- data/lib/rubydb/client/client.rb +309 -304
- data/lib/rubydb/client/connection.rb +429 -415
- data/lib/rubydb/client/connection_pool.rb +168 -168
- data/lib/rubydb/client/connection_url.rb +96 -96
- data/lib/rubydb/client/prepared_statement.rb +60 -60
- data/lib/rubydb/client/result.rb +127 -123
- data/lib/rubydb/client/statement.rb +52 -52
- data/lib/rubydb/client/transaction.rb +130 -130
- data/lib/rubydb/concurrency/concurrency.rb +19 -19
- data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
- data/lib/rubydb/concurrency/latch.rb +101 -101
- data/lib/rubydb/concurrency/lock_graph.rb +163 -165
- data/lib/rubydb/concurrency/mutex.rb +181 -183
- data/lib/rubydb/concurrency/rw_lock.rb +180 -180
- data/lib/rubydb/concurrency/scheduler.rb +248 -250
- data/lib/rubydb/concurrency/worker_pool.rb +145 -143
- data/lib/rubydb/configuration/config.rb +170 -170
- data/lib/rubydb/configuration/defaults.rb +191 -179
- data/lib/rubydb/configuration/environment.rb +152 -152
- data/lib/rubydb/configuration/parser.rb +185 -185
- data/lib/rubydb/configuration/validation.rb +228 -221
- data/lib/rubydb/constants.rb +74 -74
- data/lib/rubydb/constraints/check.rb +181 -181
- data/lib/rubydb/constraints/constraint.rb +101 -101
- data/lib/rubydb/constraints/foreign_key.rb +130 -130
- data/lib/rubydb/constraints/not_null.rb +64 -64
- data/lib/rubydb/constraints/primary_key.rb +99 -99
- data/lib/rubydb/constraints/unique.rb +106 -108
- data/lib/rubydb/constraints/validator.rb +349 -350
- data/lib/rubydb/errors/authentication_error.rb +10 -10
- data/lib/rubydb/errors/authorization_error.rb +23 -23
- data/lib/rubydb/errors/client_error.rb +10 -10
- data/lib/rubydb/errors/configuration_error.rb +10 -10
- data/lib/rubydb/errors/connection_error.rb +10 -10
- data/lib/rubydb/errors/constraint_error.rb +23 -23
- data/lib/rubydb/errors/corruption_error.rb +10 -10
- data/lib/rubydb/errors/database_error.rb +10 -10
- data/lib/rubydb/errors/error.rb +20 -20
- data/lib/rubydb/errors/execution_error.rb +10 -10
- data/lib/rubydb/errors/parser_error.rb +10 -10
- data/lib/rubydb/errors/recovery_error.rb +10 -10
- data/lib/rubydb/errors/replication_error.rb +10 -10
- data/lib/rubydb/errors/server_error.rb +6 -6
- data/lib/rubydb/errors/storage_error.rb +10 -10
- data/lib/rubydb/errors/transaction_error.rb +10 -10
- data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
- data/lib/rubydb/execution/aggregate_executor.rb +134 -138
- data/lib/rubydb/execution/cost_model.rb +72 -0
- data/lib/rubydb/execution/delete_executor.rb +110 -112
- data/lib/rubydb/execution/distinct_executor.rb +131 -135
- data/lib/rubydb/execution/executor.rb +1544 -1188
- data/lib/rubydb/execution/expression.rb +191 -193
- data/lib/rubydb/execution/index_scan.rb +142 -142
- data/lib/rubydb/execution/insert_executor.rb +215 -217
- data/lib/rubydb/execution/join_executor.rb +243 -249
- data/lib/rubydb/execution/limit_executor.rb +83 -85
- data/lib/rubydb/execution/operator_selection.rb +57 -0
- data/lib/rubydb/execution/optimizer.rb +227 -215
- data/lib/rubydb/execution/physical_plan.rb +47 -0
- data/lib/rubydb/execution/plan.rb +355 -353
- data/lib/rubydb/execution/planner.rb +508 -536
- data/lib/rubydb/execution/predicate.rb +235 -235
- data/lib/rubydb/execution/scan.rb +49 -49
- data/lib/rubydb/execution/sequential_scan.rb +63 -63
- data/lib/rubydb/execution/sort_executor.rb +194 -185
- data/lib/rubydb/execution/update_executor.rb +160 -162
- data/lib/rubydb/functions/aggregate.rb +70 -70
- data/lib/rubydb/functions/date_functions.rb +274 -278
- data/lib/rubydb/functions/function.rb +85 -85
- data/lib/rubydb/functions/json_functions.rb +231 -215
- data/lib/rubydb/functions/numeric_functions.rb +346 -346
- data/lib/rubydb/functions/scalar.rb +52 -52
- data/lib/rubydb/functions/string_functions.rb +383 -383
- data/lib/rubydb/functions/system_functions.rb +258 -246
- data/lib/rubydb/history/as_of.rb +238 -238
- data/lib/rubydb/history/change.rb +105 -105
- data/lib/rubydb/history/history.rb +131 -131
- data/lib/rubydb/history/history_manager.rb +228 -229
- data/lib/rubydb/history/temporal_query.rb +202 -202
- data/lib/rubydb/history/timeline.rb +144 -144
- data/lib/rubydb/indexes/btree.rb +215 -186
- data/lib/rubydb/indexes/btree_cursor.rb +258 -258
- data/lib/rubydb/indexes/btree_node.rb +384 -385
- data/lib/rubydb/indexes/hash_index.rb +150 -150
- data/lib/rubydb/indexes/index.rb +71 -71
- data/lib/rubydb/indexes/index_manager.rb +408 -406
- data/lib/rubydb/indexes/index_scan.rb +466 -470
- data/lib/rubydb/migrations/migration.rb +253 -254
- data/lib/rubydb/migrations/migration_lock.rb +146 -146
- data/lib/rubydb/migrations/migration_manager.rb +187 -176
- data/lib/rubydb/migrations/migration_version.rb +71 -71
- data/lib/rubydb/migrations/schema_diff.rb +211 -211
- data/lib/rubydb/migrations/schema_version.rb +64 -64
- data/lib/rubydb/monitoring/events.rb +155 -160
- data/lib/rubydb/monitoring/health.rb +216 -222
- data/lib/rubydb/monitoring/logger.rb +188 -193
- data/lib/rubydb/monitoring/metrics.rb +363 -359
- data/lib/rubydb/monitoring/performance.rb +176 -176
- data/lib/rubydb/monitoring/statistics.rb +168 -170
- data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
- data/lib/rubydb/mvcc/mvcc.rb +16 -16
- data/lib/rubydb/mvcc/snapshot.rb +146 -147
- data/lib/rubydb/mvcc/vacuum.rb +180 -180
- data/lib/rubydb/mvcc/version.rb +106 -106
- data/lib/rubydb/mvcc/version_store.rb +396 -398
- data/lib/rubydb/mvcc/visibility.rb +107 -109
- data/lib/rubydb/protocol/capabilities.rb +125 -125
- data/lib/rubydb/protocol/decoder.rb +142 -145
- data/lib/rubydb/protocol/encoder.rb +131 -136
- data/lib/rubydb/protocol/handshake.rb +306 -305
- data/lib/rubydb/protocol/message.rb +121 -121
- data/lib/rubydb/protocol/parameter_binder.rb +101 -0
- data/lib/rubydb/protocol/protocol.rb +276 -277
- data/lib/rubydb/protocol/version.rb +54 -54
- data/lib/rubydb/rails/adapter.rb +245 -239
- data/lib/rubydb/rails/connection.rb +312 -314
- data/lib/rubydb/rails/database_statements.rb +122 -122
- data/lib/rubydb/rails/migration.rb +131 -131
- data/lib/rubydb/rails/quoting.rb +109 -109
- data/lib/rubydb/rails/result.rb +117 -117
- data/lib/rubydb/rails/schema_statements.rb +339 -339
- data/lib/rubydb/rails/transaction.rb +105 -105
- data/lib/rubydb/rails/type.rb +126 -126
- data/lib/rubydb/recovery/checkpoint.rb +261 -257
- data/lib/rubydb/recovery/consistency.rb +457 -467
- data/lib/rubydb/recovery/corruption_detector.rb +5 -5
- data/lib/rubydb/recovery/crash_recovery.rb +381 -387
- data/lib/rubydb/recovery/recovery_manager.rb +204 -206
- data/lib/rubydb/recovery/redo.rb +235 -237
- data/lib/rubydb/recovery/undo.rb +204 -206
- data/lib/rubydb/replication/failover.rb +5 -5
- data/lib/rubydb/replication/fencing.rb +63 -63
- data/lib/rubydb/replication/primary.rb +461 -450
- data/lib/rubydb/replication/replica.rb +382 -384
- data/lib/rubydb/replication/replication_log.rb +194 -200
- data/lib/rubydb/replication/replication_manager.rb +307 -308
- data/lib/rubydb/replication/replication_slot.rb +293 -295
- data/lib/rubydb/replication/replication_stream.rb +198 -201
- data/lib/rubydb/rubydb.rb +570 -560
- data/lib/rubydb/security/access_control.rb +252 -254
- data/lib/rubydb/security/audit_log.rb +209 -213
- data/lib/rubydb/security/authentication.rb +302 -302
- data/lib/rubydb/security/authorization.rb +282 -282
- data/lib/rubydb/security/credentials.rb +192 -196
- data/lib/rubydb/security/password.rb +205 -215
- data/lib/rubydb/security/permissions.rb +74 -74
- data/lib/rubydb/security/role.rb +99 -101
- data/lib/rubydb/security/user.rb +86 -86
- data/lib/rubydb/server/connection.rb +383 -366
- data/lib/rubydb/server/connection_pool.rb +193 -193
- data/lib/rubydb/server/lifecycle.rb +227 -228
- data/lib/rubydb/server/listener.rb +139 -136
- data/lib/rubydb/server/request_handler.rb +277 -276
- data/lib/rubydb/server/server.rb +363 -364
- data/lib/rubydb/server/session.rb +416 -369
- data/lib/rubydb/server/worker.rb +206 -210
- data/lib/rubydb/server/worker_pool.rb +168 -168
- data/lib/rubydb/sql/ast/alter_table.rb +169 -169
- data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
- data/lib/rubydb/sql/ast/commit.rb +37 -37
- data/lib/rubydb/sql/ast/constraint.rb +92 -83
- data/lib/rubydb/sql/ast/create_database.rb +41 -41
- data/lib/rubydb/sql/ast/create_index.rb +61 -61
- data/lib/rubydb/sql/ast/create_schema.rb +52 -52
- data/lib/rubydb/sql/ast/create_table.rb +187 -187
- data/lib/rubydb/sql/ast/delete.rb +54 -54
- data/lib/rubydb/sql/ast/drop_database.rb +41 -41
- data/lib/rubydb/sql/ast/drop_index.rb +41 -41
- data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
- data/lib/rubydb/sql/ast/drop_table.rb +49 -49
- data/lib/rubydb/sql/ast/explain.rb +64 -64
- data/lib/rubydb/sql/ast/expression.rb +617 -604
- data/lib/rubydb/sql/ast/insert.rb +66 -66
- data/lib/rubydb/sql/ast/node.rb +42 -42
- data/lib/rubydb/sql/ast/rollback.rb +63 -63
- data/lib/rubydb/sql/ast/savepoint.rb +59 -59
- data/lib/rubydb/sql/ast/select.rb +88 -88
- data/lib/rubydb/sql/ast/set_operation.rb +22 -20
- data/lib/rubydb/sql/ast/trigger.rb +35 -29
- data/lib/rubydb/sql/ast/update.rb +88 -88
- data/lib/rubydb/sql/ast/vacuum.rb +19 -19
- data/lib/rubydb/sql/ast/view.rb +38 -32
- data/lib/rubydb/sql/ast/with.rb +32 -32
- data/lib/rubydb/sql/grammar.rb +86 -86
- data/lib/rubydb/sql/keywords.rb +156 -156
- data/lib/rubydb/sql/lexer.rb +209 -214
- data/lib/rubydb/sql/operators.rb +100 -100
- data/lib/rubydb/sql/parser.rb +1167 -1170
- data/lib/rubydb/sql/planner/analyzer.rb +283 -302
- data/lib/rubydb/sql/planner/binder.rb +537 -550
- data/lib/rubydb/sql/planner/type_checker.rb +427 -431
- data/lib/rubydb/sql/token.rb +210 -210
- data/lib/rubydb/storage/buffer_frame.rb +44 -44
- data/lib/rubydb/storage/buffer_pool.rb +155 -155
- data/lib/rubydb/storage/database_lock.rb +74 -74
- data/lib/rubydb/storage/deserializer.rb +332 -342
- data/lib/rubydb/storage/engine.rb +2409 -2330
- data/lib/rubydb/storage/file_manager.rb +191 -187
- data/lib/rubydb/storage/free_space_map.rb +79 -81
- data/lib/rubydb/storage/page.rb +92 -94
- data/lib/rubydb/storage/page_allocator.rb +852 -855
- data/lib/rubydb/storage/page_header.rb +63 -67
- data/lib/rubydb/storage/page_manager.rb +127 -131
- data/lib/rubydb/storage/record.rb +58 -58
- data/lib/rubydb/storage/row.rb +78 -78
- data/lib/rubydb/storage/serializer.rb +51 -51
- data/lib/rubydb/storage/snapshot_reader.rb +167 -0
- data/lib/rubydb/storage/storage_layout.rb +151 -151
- data/lib/rubydb/storage/storage_manager.rb +114 -114
- data/lib/rubydb/storage/tuple.rb +458 -461
- data/lib/rubydb/storage/visibility_map.rb +964 -973
- data/lib/rubydb/transactions/commit_manager.rb +219 -220
- data/lib/rubydb/transactions/isolation.rb +98 -98
- data/lib/rubydb/transactions/lock.rb +76 -76
- data/lib/rubydb/transactions/lock_manager.rb +359 -362
- data/lib/rubydb/transactions/savepoint.rb +142 -143
- data/lib/rubydb/transactions/transaction.rb +214 -215
- data/lib/rubydb/transactions/transaction_id.rb +84 -84
- data/lib/rubydb/transactions/transaction_log.rb +256 -257
- data/lib/rubydb/transactions/transaction_manager.rb +434 -435
- data/lib/rubydb/types/bigint.rb +36 -36
- data/lib/rubydb/types/blob.rb +37 -37
- data/lib/rubydb/types/boolean.rb +34 -34
- data/lib/rubydb/types/date.rb +39 -39
- data/lib/rubydb/types/decimal.rb +48 -48
- data/lib/rubydb/types/float.rb +34 -34
- data/lib/rubydb/types/integer.rb +36 -36
- data/lib/rubydb/types/json.rb +41 -41
- data/lib/rubydb/types/null.rb +34 -34
- data/lib/rubydb/types/smallint.rb +36 -36
- data/lib/rubydb/types/text.rb +37 -37
- data/lib/rubydb/types/time.rb +46 -46
- data/lib/rubydb/types/timestamp.rb +39 -39
- data/lib/rubydb/types/type.rb +119 -119
- data/lib/rubydb/types/uuid.rb +47 -47
- data/lib/rubydb/types/varchar.rb +37 -37
- data/lib/rubydb/version.rb +32 -32
- data/lib/rubydb/wal/archive.rb +207 -193
- data/lib/rubydb/wal/checkpoint.rb +181 -183
- data/lib/rubydb/wal/lsn.rb +94 -94
- data/lib/rubydb/wal/reader.rb +259 -260
- data/lib/rubydb/wal/record.rb +105 -105
- data/lib/rubydb/wal/segment.rb +193 -193
- data/lib/rubydb/wal/wal.rb +481 -452
- data/lib/rubydb/wal/writer.rb +236 -236
- data/lib/rubydb.rb +7 -7
- data/packaging/docker/docker-compose.failover.yml +43 -43
- data/packaging/homebrew/rubydb.rb +19 -19
- data/rubydb.gemspec +70 -57
- data/scripts/benchmark +7 -7
- data/scripts/build_accelerator +49 -0
- data/scripts/durability_drill +37 -37
- data/scripts/fuzz +63 -63
- data/scripts/release +77 -42
- data/scripts/release_check +43 -43
- data/scripts/replication_failover_drill +268 -250
- data/scripts/replication_network_failover_drill +287 -255
- data/scripts/restore_drill +45 -45
- data/scripts/security +45 -0
- metadata +102 -1
|
@@ -1,1201 +1,1557 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
module
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@
|
|
17
|
-
@
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
working = next_rows
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if
|
|
226
|
-
rows =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
rows
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module RubyDB
|
|
6
|
+
module Execution
|
|
7
|
+
# Executor - Executes query plans and returns results
|
|
8
|
+
class Executor
|
|
9
|
+
attr_reader :engine, :stats
|
|
10
|
+
|
|
11
|
+
def initialize(engine, cte_rows: {}, deadline_at: nil, cancellation: nil)
|
|
12
|
+
@engine = engine
|
|
13
|
+
@accelerator = engine.respond_to?(:accelerator) ? engine.accelerator : nil
|
|
14
|
+
@accelerator_dispatch = AcceleratorDispatch.new(@accelerator)
|
|
15
|
+
@cte_rows = cte_rows
|
|
16
|
+
@deadline_at = deadline_at && (deadline_at.is_a?(Time) ? deadline_at : Time.parse(deadline_at.to_s))
|
|
17
|
+
@cancellation = cancellation
|
|
18
|
+
@stats = {
|
|
19
|
+
queries_executed: 0,
|
|
20
|
+
rows_returned: 0,
|
|
21
|
+
total_time_ms: 0,
|
|
22
|
+
sequential_scans: 0,
|
|
23
|
+
index_scans: 0,
|
|
24
|
+
joins: 0,
|
|
25
|
+
aggregations: 0,
|
|
26
|
+
sorts: 0,
|
|
27
|
+
accelerator_requests: 0,
|
|
28
|
+
accelerator_fallbacks: 0
|
|
29
|
+
}
|
|
30
|
+
@lock = Mutex.new
|
|
31
|
+
@current_transaction = nil
|
|
32
|
+
@explain_mode = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def execute(plan, transaction_id = nil)
|
|
36
|
+
@lock.synchronize do
|
|
37
|
+
check_deadline!
|
|
38
|
+
start_time = Time.now
|
|
39
|
+
@stats[:queries_executed] += 1
|
|
40
|
+
@current_transaction = transaction_id
|
|
41
|
+
|
|
42
|
+
result = case plan
|
|
43
|
+
when Plan::With
|
|
44
|
+
execute_with(plan)
|
|
45
|
+
when Plan::Select
|
|
46
|
+
execute_select(plan)
|
|
47
|
+
when Plan::SetOperation
|
|
48
|
+
execute_set_operation(plan)
|
|
49
|
+
when Plan::Insert
|
|
50
|
+
execute_insert(plan)
|
|
51
|
+
when Plan::Update
|
|
52
|
+
execute_update(plan)
|
|
53
|
+
when Plan::Delete
|
|
54
|
+
execute_delete(plan)
|
|
55
|
+
when Plan::CreateTable
|
|
56
|
+
execute_create_table(plan)
|
|
57
|
+
when Plan::DropTable
|
|
58
|
+
execute_drop_table(plan)
|
|
59
|
+
when Plan::CreateDatabase
|
|
60
|
+
execute_create_database(plan)
|
|
61
|
+
when Plan::DropDatabase
|
|
62
|
+
execute_drop_database(plan)
|
|
63
|
+
when Plan::CreateSchema
|
|
64
|
+
execute_create_schema(plan)
|
|
65
|
+
when Plan::DropSchema
|
|
66
|
+
execute_drop_schema(plan)
|
|
67
|
+
when Plan::CreateView
|
|
68
|
+
execute_create_view(plan)
|
|
69
|
+
when Plan::DropView
|
|
70
|
+
execute_drop_view(plan)
|
|
71
|
+
when Plan::CreateTrigger
|
|
72
|
+
execute_create_trigger(plan)
|
|
73
|
+
when Plan::DropTrigger
|
|
74
|
+
execute_drop_trigger(plan)
|
|
75
|
+
when Plan::Vacuum
|
|
76
|
+
execute_vacuum(plan)
|
|
77
|
+
when Plan::AlterTableAddColumn
|
|
78
|
+
execute_alter_table_add_column(plan)
|
|
79
|
+
when Plan::AlterTableDropColumn
|
|
80
|
+
execute_alter_table_drop_column(plan)
|
|
81
|
+
when Plan::AlterTableAddConstraint
|
|
82
|
+
execute_alter_table_add_constraint(plan)
|
|
83
|
+
when Plan::AlterTableDropConstraint
|
|
84
|
+
execute_alter_table_drop_constraint(plan)
|
|
85
|
+
when Plan::CreateIndex
|
|
86
|
+
execute_create_index(plan)
|
|
87
|
+
when Plan::DropIndex
|
|
88
|
+
execute_drop_index(plan)
|
|
89
|
+
when Plan::BeginTransaction
|
|
90
|
+
execute_begin_transaction(plan)
|
|
91
|
+
when Plan::Commit
|
|
92
|
+
execute_commit(plan)
|
|
93
|
+
when Plan::Rollback
|
|
94
|
+
execute_rollback(plan)
|
|
95
|
+
when Plan::Savepoint
|
|
96
|
+
execute_savepoint(plan)
|
|
97
|
+
when Plan::RollbackToSavepoint
|
|
98
|
+
execute_rollback_to_savepoint(plan)
|
|
99
|
+
when Plan::ReleaseSavepoint
|
|
100
|
+
execute_release_savepoint(plan)
|
|
101
|
+
when Plan::Explain
|
|
102
|
+
execute_explain(plan)
|
|
103
|
+
else
|
|
104
|
+
raise ExecutionError, "Unknown plan type: #{plan.class}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
elapsed_ms = ((Time.now - start_time) * 1000).round(2)
|
|
108
|
+
@stats[:total_time_ms] += elapsed_ms
|
|
109
|
+
@stats[:rows_returned] += result[:row_count] if result && result[:row_count]
|
|
110
|
+
|
|
111
|
+
result
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def execute_set_operation(plan)
|
|
116
|
+
left = child_executor(@cte_rows).execute(plan.left_plan, @current_transaction)[:rows]
|
|
117
|
+
right = child_executor(@cte_rows).execute(plan.right_plan, @current_transaction)[:rows]
|
|
118
|
+
left_width = left.first&.size || plan.left_plan.columns.size
|
|
119
|
+
right_width = right.first&.size || plan.right_plan.columns.size
|
|
120
|
+
if left_width != right_width
|
|
121
|
+
raise ExecutionError, "Set operation requires the same number of columns on both sides"
|
|
122
|
+
end
|
|
123
|
+
key = ->(row) { row.to_a }
|
|
124
|
+
rows = case plan.operator
|
|
125
|
+
when :union then plan.all ? left + right : (left + right).uniq { |row| key.call(row) }
|
|
126
|
+
when :intersect then left.select { |row| right.any? { |other| key.call(other) == key.call(row) } }.uniq { |row| key.call(row) }
|
|
127
|
+
when :except then left.reject { |row| right.any? { |other| key.call(other) == key.call(row) } }.uniq { |row| key.call(row) }
|
|
128
|
+
else raise ExecutionError, "Unsupported set operation: #{plan.operator}"
|
|
129
|
+
end
|
|
130
|
+
{rows: rows, row_count: rows.size, column_names: rows.first&.keys || []}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def execute_with(plan)
|
|
134
|
+
available_ctes = @cte_rows.dup
|
|
135
|
+
plan.ctes.each do |name, cte_plan|
|
|
136
|
+
available_ctes[name.to_s] = if plan.recursive && cte_plan.is_a?(Plan::SetOperation) && cte_plan.operator == :union
|
|
137
|
+
execute_recursive_cte(name.to_s, cte_plan, available_ctes)
|
|
138
|
+
else
|
|
139
|
+
child_executor(available_ctes).execute(cte_plan, @current_transaction)[:rows]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
child_executor(available_ctes).execute(plan.query_plan, @current_transaction)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def execute_recursive_cte(name, cte_plan, available_ctes)
|
|
146
|
+
unless cte_plan.is_a?(Plan::SetOperation) && cte_plan.operator == :union
|
|
147
|
+
raise ExecutionError, "Recursive CTE requires UNION or UNION ALL"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
anchor = child_executor(available_ctes).execute(cte_plan.left_plan, @current_transaction)[:rows]
|
|
151
|
+
accumulated = anchor.dup
|
|
152
|
+
working = anchor
|
|
153
|
+
output_columns = anchor.first&.keys || cte_plan.left_plan.columns.map(&:to_s)
|
|
154
|
+
max_iterations = Integer(@engine.instance_variable_get(:@config)[:max_recursive_iterations] || 10_000)
|
|
155
|
+
iterations = 0
|
|
156
|
+
|
|
157
|
+
while working.any?
|
|
158
|
+
iterations += 1
|
|
159
|
+
raise ExecutionError, "Recursive CTE exceeded #{max_iterations} iterations" if iterations > max_iterations
|
|
160
|
+
|
|
161
|
+
recursive_scope = available_ctes.merge(name => working)
|
|
162
|
+
next_rows = child_executor(recursive_scope).execute(cte_plan.right_plan, @current_transaction)[:rows]
|
|
163
|
+
next_rows = next_rows.map do |row|
|
|
164
|
+
output_columns.zip(row.values).to_h
|
|
165
|
+
end
|
|
166
|
+
if cte_plan.all
|
|
167
|
+
accumulated.concat(next_rows)
|
|
168
|
+
working = next_rows
|
|
169
|
+
else
|
|
170
|
+
existing = accumulated.map { |row| row.to_a }
|
|
171
|
+
working = next_rows.reject { |row| existing.include?(row.to_a) }
|
|
172
|
+
accumulated.concat(working)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
accumulated
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def execute_select(plan)
|
|
180
|
+
@accelerator_window_applied = false
|
|
181
|
+
@stats[:sequential_scans] += 1 if plan.scan_type == :sequential
|
|
182
|
+
@stats[:index_scans] += 1 if plan.scan_type == :index
|
|
183
|
+
|
|
184
|
+
# Qualify source rows before evaluating predicates. This preserves
|
|
185
|
+
# SQL's table/alias namespace while retaining unqualified keys for
|
|
186
|
+
# existing single-table callers.
|
|
187
|
+
snapshot_rows = accelerate_snapshot_scan(plan)
|
|
188
|
+
rows = snapshot_rows || scan_table(plan)
|
|
189
|
+
accelerated = snapshot_rows || accelerate_scan(rows, plan)
|
|
190
|
+
aggregate_accelerated = false
|
|
191
|
+
unless accelerated
|
|
192
|
+
accelerated_rows = accelerate_aggregate(rows, plan)
|
|
193
|
+
if accelerated_rows
|
|
194
|
+
rows = accelerated_rows
|
|
195
|
+
aggregate_accelerated = true
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
if accelerated
|
|
199
|
+
rows = accelerated
|
|
200
|
+
end
|
|
201
|
+
rows = qualify_rows(rows, plan.source_reference || plan.table_name) unless aggregate_accelerated
|
|
202
|
+
plan.joins.each do |join|
|
|
203
|
+
check_deadline!
|
|
204
|
+
right_rows = qualify_rows(scan_table(Plan::Select.new(join[:table].name, [])), join[:table])
|
|
205
|
+
rows = accelerate_join(rows, right_rows, join) || execute_join(rows, right_rows, join)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Apply filters (WHERE clause)
|
|
209
|
+
if plan.predicate && !accelerated && !aggregate_accelerated
|
|
210
|
+
rows = rows.each_with_index.filter_map do |row, index|
|
|
211
|
+
check_deadline! if (index & 255).zero?
|
|
212
|
+
row if evaluate_predicate(plan.predicate, row)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
apply_window_functions!(rows, plan.projections) if plan.projections
|
|
217
|
+
|
|
218
|
+
# ORDER BY is evaluated against the source rows, so a column used
|
|
219
|
+
# solely for ordering remains available even when it is not selected.
|
|
220
|
+
aggregating = plan.aggregates && !plan.aggregates.empty?
|
|
221
|
+
if plan.order_by && !plan.order_by.empty? && !aggregating && !accelerated
|
|
222
|
+
rows = sort_rows(rows, plan.order_by)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if aggregating && !aggregate_accelerated
|
|
226
|
+
rows = aggregate_rows(rows, plan.group_by || [], plan.projections)
|
|
227
|
+
rows = rows.select { |row| evaluate_predicate(plan.having, row) } if plan.having
|
|
228
|
+
elsif !aggregating && plan.projections
|
|
229
|
+
# Apply projections (SELECT columns)
|
|
230
|
+
rows = rows.each_with_index.map do |row, index|
|
|
231
|
+
check_deadline! if (index & 255).zero?
|
|
232
|
+
project_row(row, plan.projections)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Apply DISTINCT
|
|
237
|
+
if plan.distinct
|
|
238
|
+
rows = distinct_rows(rows)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Apply GROUP BY
|
|
242
|
+
# Apply ORDER BY
|
|
243
|
+
if plan.order_by && !plan.order_by.empty? && aggregating
|
|
244
|
+
rows = sort_rows(rows, plan.order_by)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Apply LIMIT and OFFSET
|
|
248
|
+
if plan.limit && !@accelerator_window_applied
|
|
249
|
+
offset = plan.offset || 0
|
|
250
|
+
rows = rows[offset, plan.limit] || []
|
|
251
|
+
elsif plan.offset
|
|
252
|
+
rows = rows[plan.offset..] || []
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
{
|
|
256
|
+
rows: rows,
|
|
257
|
+
row_count: rows.size,
|
|
258
|
+
column_names: get_column_names(plan)
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def execute_insert(plan)
|
|
263
|
+
table_name = plan.table_name
|
|
264
|
+
columns = plan.columns
|
|
265
|
+
rows = plan.rows || [plan.values]
|
|
266
|
+
implicit_transaction = rows.size > 1 && !@engine.in_transaction?
|
|
267
|
+
@engine.begin_transaction if implicit_transaction
|
|
268
|
+
|
|
269
|
+
begin
|
|
270
|
+
inserted = rows.map { |values| execute_single_insert(plan, table_name, columns, values) }
|
|
271
|
+
unless !implicit_transaction || @engine.commit_transaction
|
|
272
|
+
raise ExecutionError, "Implicit multi-row INSERT transaction could not commit"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
{
|
|
276
|
+
row_count: inserted.sum { |result| result[:row_count] },
|
|
277
|
+
affected_rows: inserted.sum { |result| result[:affected_rows] },
|
|
278
|
+
row_ids: inserted.filter_map { |result| result[:row_id] },
|
|
261
279
|
row_id: inserted.reverse_each.map { |result| result[:row_id] }.compact.first,
|
|
262
280
|
inserted_ids: inserted.filter_map { |result| result[:inserted_id] },
|
|
263
281
|
inserted_id: inserted.reverse_each.map { |result| result[:inserted_id] }.compact.first,
|
|
264
282
|
message: "INSERT #{inserted.sum { |result| result[:affected_rows] }}"
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
283
|
+
}
|
|
284
|
+
# Roll back even when the request is interrupted, then re-raise it.
|
|
285
|
+
# rubocop:disable Lint/RescueException
|
|
286
|
+
rescue Exception
|
|
287
|
+
@engine.rollback_transaction if implicit_transaction && @engine.in_transaction?
|
|
288
|
+
raise
|
|
289
|
+
# rubocop:enable Lint/RescueException
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def execute_single_insert(plan, table_name, columns, values)
|
|
294
|
+
# Build row data
|
|
295
|
+
row_data = {}
|
|
296
|
+
columns.each_with_index do |col, idx|
|
|
297
|
+
row_data[col] = evaluate_expression(values[idx])
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Insert into engine
|
|
301
|
+
table_columns = @engine.table_columns(table_name)
|
|
282
302
|
row_id = @engine.insert_row(table_name, table_columns, row_data)
|
|
283
303
|
primary_key = table_columns.find(&:primary_key?)
|
|
284
304
|
inserted_row = @engine.select_row(table_name, row_id, table_columns)
|
|
285
305
|
inserted_id = if primary_key && inserted_row
|
|
286
306
|
inserted_row[primary_key.name] || inserted_row[primary_key.name.to_sym]
|
|
287
307
|
end
|
|
288
|
-
|
|
289
|
-
{
|
|
290
|
-
row_count: 1,
|
|
291
|
-
affected_rows: 1,
|
|
308
|
+
|
|
309
|
+
{
|
|
310
|
+
row_count: 1,
|
|
311
|
+
affected_rows: 1,
|
|
292
312
|
row_id: row_id,
|
|
293
313
|
inserted_id: inserted_id,
|
|
294
|
-
message: "INSERT 1"
|
|
295
|
-
}
|
|
296
|
-
rescue DatabaseError => error
|
|
297
|
-
raise unless error.message.match?(/duplicate|unique|primary key/i)
|
|
298
|
-
return {
|
|
299
|
-
raise unless plan.on_conflict.is_a?(Hash) && plan.on_conflict[:action] == :update
|
|
300
|
-
|
|
301
|
-
target = plan.on_conflict[:target]
|
|
302
|
-
target = @engine.table_columns(table_name).select(&:primary_key?).map(&:name) if target.empty?
|
|
303
|
-
if target.empty?
|
|
304
|
-
target = @engine.table_columns(table_name).select(&:unique?).map(&:name)
|
|
305
|
-
end
|
|
306
|
-
if target.empty? && @engine.respond_to?(:index_manager)
|
|
307
|
-
target = @engine.index_manager.get_indexes_for_table(table_name)
|
|
308
|
-
.select(&:unique?).min_by { |index| index.columns.size }&.columns || []
|
|
309
|
-
end
|
|
310
|
-
raise ExecutionError, "ON CONFLICT DO UPDATE requires a conflict target or primary key" if target.empty?
|
|
311
|
-
existing = @engine.select_rows(table_name, @engine.table_columns(table_name)).find do |row|
|
|
312
|
-
target.all? do |column|
|
|
313
|
-
row_value(row, column, column.to_s) == row_value(row_data, column, column.to_s)
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
raise error unless existing
|
|
317
|
-
|
|
318
|
-
context = existing.merge(row_data.transform_keys(&:to_s).transform_keys { |key| "excluded.#{key}" })
|
|
319
|
-
values = plan.on_conflict[:assignments].each_with_object({}) do |assignment, updates|
|
|
320
|
-
updates[assignment.column] = evaluate_expression(assignment.value, context)
|
|
321
|
-
end
|
|
322
|
-
row_id = existing[:_row_id] || existing["_row_id"]
|
|
323
|
-
@engine.update_row(table_name, row_id, values)
|
|
314
|
+
message: "INSERT 1"
|
|
315
|
+
}
|
|
316
|
+
rescue DatabaseError => error
|
|
317
|
+
raise unless error.message.match?(/duplicate|unique|primary key/i)
|
|
318
|
+
return {row_count: 0, affected_rows: 0, message: "INSERT 0 (conflict ignored)"} if plan.on_conflict == :nothing
|
|
319
|
+
raise unless plan.on_conflict.is_a?(Hash) && plan.on_conflict[:action] == :update
|
|
320
|
+
|
|
321
|
+
target = plan.on_conflict[:target]
|
|
322
|
+
target = @engine.table_columns(table_name).select(&:primary_key?).map(&:name) if target.empty?
|
|
323
|
+
if target.empty?
|
|
324
|
+
target = @engine.table_columns(table_name).select(&:unique?).map(&:name)
|
|
325
|
+
end
|
|
326
|
+
if target.empty? && @engine.respond_to?(:index_manager)
|
|
327
|
+
target = @engine.index_manager.get_indexes_for_table(table_name)
|
|
328
|
+
.select(&:unique?).min_by { |index| index.columns.size }&.columns || []
|
|
329
|
+
end
|
|
330
|
+
raise ExecutionError, "ON CONFLICT DO UPDATE requires a conflict target or primary key" if target.empty?
|
|
331
|
+
existing = @engine.select_rows(table_name, @engine.table_columns(table_name)).find do |row|
|
|
332
|
+
target.all? do |column|
|
|
333
|
+
row_value(row, column, column.to_s) == row_value(row_data, column, column.to_s)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
raise error unless existing
|
|
337
|
+
|
|
338
|
+
context = existing.merge(row_data.transform_keys(&:to_s).transform_keys { |key| "excluded.#{key}" })
|
|
339
|
+
values = plan.on_conflict[:assignments].each_with_object({}) do |assignment, updates|
|
|
340
|
+
updates[assignment.column] = evaluate_expression(assignment.value, context)
|
|
341
|
+
end
|
|
342
|
+
row_id = existing[:_row_id] || existing["_row_id"]
|
|
343
|
+
@engine.update_row(table_name, row_id, values)
|
|
324
344
|
inserted_id = target.map { |column| existing[column] || existing[column.to_sym] }.first
|
|
325
|
-
{
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
def execute_update(plan)
|
|
329
|
-
table_name = plan.table_name
|
|
330
|
-
assignments = plan.assignments
|
|
331
|
-
predicate = plan.predicate
|
|
332
|
-
|
|
333
|
-
# Get rows to update
|
|
334
|
-
rows = scan_table(Plan::Select.new(table_name, []))
|
|
335
|
-
rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
|
|
336
|
-
|
|
337
|
-
updated_count = 0
|
|
338
|
-
rows.each do |row|
|
|
339
|
-
# Apply updates
|
|
340
|
-
assignments.each do |assignment|
|
|
341
|
-
column = assignment.is_a?(Hash) ? (assignment[:column] || assignment["column"]) : assignment.column
|
|
342
|
-
value = assignment.is_a?(Hash) ? (assignment[:value] || assignment["value"]) : assignment.value
|
|
343
|
-
row[column] = evaluate_expression(value, row)
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
# Update in engine
|
|
347
|
-
row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
|
|
348
|
-
@engine.update_row(table_name, row_id, row)
|
|
349
|
-
|
|
350
|
-
updated_count += 1
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
{
|
|
354
|
-
row_count: updated_count,
|
|
355
|
-
affected_rows: updated_count,
|
|
356
|
-
message: "UPDATE #{updated_count}"
|
|
357
|
-
}
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
def execute_delete(plan)
|
|
361
|
-
table_name = plan.table_name
|
|
362
|
-
predicate = plan.predicate
|
|
363
|
-
|
|
364
|
-
# Get rows to delete
|
|
365
|
-
rows = scan_table(Plan::Select.new(table_name, []))
|
|
366
|
-
rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
|
|
367
|
-
|
|
368
|
-
deleted_count = 0
|
|
369
|
-
rows.each do |row|
|
|
370
|
-
row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
|
|
371
|
-
@engine.delete_row(table_name, row_id)
|
|
372
|
-
deleted_count += 1
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
{
|
|
376
|
-
row_count: deleted_count,
|
|
377
|
-
affected_rows: deleted_count,
|
|
378
|
-
message: "DELETE #{deleted_count}"
|
|
379
|
-
}
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def execute_create_table(plan)
|
|
383
|
-
table_name = plan.table_name
|
|
384
|
-
columns = plan.columns
|
|
385
|
-
options = plan.options || {}
|
|
386
|
-
|
|
387
|
-
@engine.create_table(table_name, columns, options)
|
|
388
|
-
|
|
389
|
-
{
|
|
390
|
-
row_count: 0,
|
|
391
|
-
message: "CREATE TABLE #{table_name}"
|
|
392
|
-
}
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def execute_drop_table(plan)
|
|
396
|
-
table_name = plan.table_name
|
|
397
|
-
options = plan.options || {}
|
|
398
|
-
|
|
399
|
-
@engine.drop_table(table_name, options)
|
|
400
|
-
|
|
401
|
-
{
|
|
402
|
-
row_count: 0,
|
|
403
|
-
message: "DROP TABLE #{table_name}"
|
|
404
|
-
}
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def execute_create_database(plan)
|
|
408
|
-
@engine.catalog.create_database(plan.database_name, **(plan.options || {}))
|
|
409
|
-
{
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
def execute_drop_database(plan)
|
|
413
|
-
@engine.catalog.drop_database(plan.database_name, **(plan.options || {}))
|
|
414
|
-
{
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
def execute_create_schema(plan)
|
|
418
|
-
@engine.catalog.create_schema(plan.schema_name, **(plan.options || {}))
|
|
419
|
-
{
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
def execute_drop_schema(plan)
|
|
423
|
-
@engine.catalog.drop_schema(plan.schema_name, **(plan.options || {}))
|
|
424
|
-
{
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def execute_create_view(plan)
|
|
428
|
-
query = plan.query.respond_to?(:to_sql) ? plan.query.to_sql : plan.query
|
|
429
|
-
@engine.catalog.create_view(plan.view_name, query, **(plan.options || {}))
|
|
430
|
-
{
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
def execute_drop_view(plan)
|
|
434
|
-
@engine.catalog.drop_view(plan.view_name, **(plan.options || {}))
|
|
435
|
-
{
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
def execute_create_trigger(plan)
|
|
439
|
-
definition = "EXECUTE FUNCTION #{plan.function_name}()"
|
|
440
|
-
@engine.catalog.create_trigger(plan.trigger_name, plan.event, plan.target_table, definition, timing: plan.timing, function_name: plan.function_name)
|
|
441
|
-
{
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def execute_drop_trigger(plan)
|
|
445
|
-
@engine.catalog.drop_trigger(plan.trigger_name, **(plan.options || {}))
|
|
446
|
-
{
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
def execute_vacuum(_plan)
|
|
450
|
-
result = @engine.vacuum
|
|
451
|
-
{
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
def execute_alter_table_add_column(plan)
|
|
455
|
-
@engine.add_column(plan.table_name, plan.column_name, plan.column_type, plan.options)
|
|
456
|
-
{
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def execute_alter_table_drop_column(plan)
|
|
460
|
-
@engine.drop_column(plan.table_name, plan.column_name)
|
|
461
|
-
{
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def execute_alter_table_add_constraint(plan)
|
|
465
|
-
@engine.add_constraint(plan.table_name, plan.constraint)
|
|
466
|
-
{
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
def execute_alter_table_drop_constraint(plan)
|
|
470
|
-
@engine.drop_constraint(plan.table_name, plan.constraint_name)
|
|
471
|
-
{
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def execute_create_index(plan)
|
|
475
|
-
index_name = plan.index_name
|
|
476
|
-
table_name = plan.table_name
|
|
477
|
-
columns = plan.columns
|
|
478
|
-
options = plan.options || {}
|
|
479
|
-
|
|
480
|
-
if @engine.respond_to?(:index_manager)
|
|
481
|
-
@engine.index_manager.create_index(index_name, table_name, columns, options)
|
|
482
|
-
end
|
|
483
|
-
|
|
484
|
-
{
|
|
485
|
-
row_count: 0,
|
|
486
|
-
message: "CREATE INDEX #{index_name}"
|
|
487
|
-
}
|
|
488
|
-
end
|
|
489
|
-
|
|
490
|
-
def execute_drop_index(plan)
|
|
491
|
-
index_name = plan.index_name
|
|
492
|
-
options = plan.options || {}
|
|
493
|
-
|
|
494
|
-
if @engine.respond_to?(:index_manager)
|
|
495
|
-
@engine.index_manager.drop_index(index_name, options)
|
|
496
|
-
end
|
|
497
|
-
|
|
498
|
-
{
|
|
499
|
-
row_count: 0,
|
|
500
|
-
message: "DROP INDEX #{index_name}"
|
|
501
|
-
}
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
def execute_begin_transaction(plan)
|
|
505
|
-
isolation_level = plan.isolation_level || :read_committed
|
|
506
|
-
transaction_id = @engine.begin_transaction(isolation_level)
|
|
507
|
-
|
|
508
|
-
{
|
|
509
|
-
row_count: 0,
|
|
510
|
-
transaction_id: transaction_id,
|
|
511
|
-
message: "BEGIN"
|
|
512
|
-
}
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
def execute_commit(plan)
|
|
516
|
-
@engine.commit_transaction
|
|
517
|
-
|
|
518
|
-
{
|
|
519
|
-
row_count: 0,
|
|
520
|
-
message: "COMMIT"
|
|
521
|
-
}
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def execute_rollback(plan)
|
|
525
|
-
@engine.rollback_transaction
|
|
526
|
-
|
|
527
|
-
{
|
|
528
|
-
row_count: 0,
|
|
529
|
-
message: "ROLLBACK"
|
|
530
|
-
}
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
def execute_savepoint(plan)
|
|
534
|
-
@engine.create_savepoint(plan.name)
|
|
535
|
-
{
|
|
536
|
-
end
|
|
537
|
-
|
|
538
|
-
def execute_rollback_to_savepoint(plan)
|
|
539
|
-
@engine.rollback_to_savepoint(plan.name)
|
|
540
|
-
{
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
def execute_release_savepoint(plan)
|
|
544
|
-
@engine.release_savepoint(plan.name)
|
|
545
|
-
{
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
def execute_explain(plan)
|
|
549
|
-
statement = plan.statement
|
|
550
|
-
analyze = plan.analyze || false
|
|
551
|
-
|
|
552
|
-
# Generate execution plan for the statement
|
|
553
|
-
planner = Planner.new(@engine)
|
|
554
|
-
analyzed_plan = planner.plan(statement)
|
|
555
|
-
|
|
556
|
-
# Format the plan
|
|
557
|
-
plan_text = format_plan(analyzed_plan)
|
|
558
|
-
|
|
559
|
-
if analyze
|
|
560
|
-
# Execute and get actual stats
|
|
561
|
-
start_time = Time.now
|
|
562
|
-
result = execute(analyzed_plan)
|
|
563
|
-
elapsed_ms = ((Time.now - start_time) * 1000).round(2)
|
|
564
|
-
|
|
565
|
-
plan_text = "EXPLAIN ANALYZE:\n#{plan_text}\n"
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
{
|
|
571
|
-
rows: [{
|
|
572
|
-
row_count: 1,
|
|
573
|
-
column_names: ["QUERY PLAN"]
|
|
574
|
-
}
|
|
575
|
-
end
|
|
576
|
-
|
|
577
|
-
# Scan operations
|
|
578
|
-
def
|
|
579
|
-
|
|
580
|
-
return
|
|
581
|
-
return
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
result
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
def
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return nil
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
when
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
when
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
end
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
end
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
def
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
when
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
when
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
when :
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
end
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
when
|
|
1099
|
-
when
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
case
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
when
|
|
1110
|
-
when
|
|
1111
|
-
when
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
end
|
|
1141
|
-
|
|
1142
|
-
def
|
|
1143
|
-
if
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
345
|
+
{row_count: 1, affected_rows: 1, row_id: row_id, inserted_id: inserted_id, message: "INSERT 0 UPDATE 1"}
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def execute_update(plan)
|
|
349
|
+
table_name = plan.table_name
|
|
350
|
+
assignments = plan.assignments
|
|
351
|
+
predicate = plan.predicate
|
|
352
|
+
|
|
353
|
+
# Get rows to update
|
|
354
|
+
rows = scan_table(Plan::Select.new(table_name, []))
|
|
355
|
+
rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
|
|
356
|
+
|
|
357
|
+
updated_count = 0
|
|
358
|
+
rows.each do |row|
|
|
359
|
+
# Apply updates
|
|
360
|
+
assignments.each do |assignment|
|
|
361
|
+
column = assignment.is_a?(Hash) ? (assignment[:column] || assignment["column"]) : assignment.column
|
|
362
|
+
value = assignment.is_a?(Hash) ? (assignment[:value] || assignment["value"]) : assignment.value
|
|
363
|
+
row[column] = evaluate_expression(value, row)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Update in engine
|
|
367
|
+
row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
|
|
368
|
+
@engine.update_row(table_name, row_id, row)
|
|
369
|
+
|
|
370
|
+
updated_count += 1
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
row_count: updated_count,
|
|
375
|
+
affected_rows: updated_count,
|
|
376
|
+
message: "UPDATE #{updated_count}"
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def execute_delete(plan)
|
|
381
|
+
table_name = plan.table_name
|
|
382
|
+
predicate = plan.predicate
|
|
383
|
+
|
|
384
|
+
# Get rows to delete
|
|
385
|
+
rows = scan_table(Plan::Select.new(table_name, []))
|
|
386
|
+
rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
|
|
387
|
+
|
|
388
|
+
deleted_count = 0
|
|
389
|
+
rows.each do |row|
|
|
390
|
+
row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
|
|
391
|
+
@engine.delete_row(table_name, row_id)
|
|
392
|
+
deleted_count += 1
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
{
|
|
396
|
+
row_count: deleted_count,
|
|
397
|
+
affected_rows: deleted_count,
|
|
398
|
+
message: "DELETE #{deleted_count}"
|
|
399
|
+
}
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def execute_create_table(plan)
|
|
403
|
+
table_name = plan.table_name
|
|
404
|
+
columns = plan.columns
|
|
405
|
+
options = plan.options || {}
|
|
406
|
+
|
|
407
|
+
@engine.create_table(table_name, columns, options)
|
|
408
|
+
|
|
409
|
+
{
|
|
410
|
+
row_count: 0,
|
|
411
|
+
message: "CREATE TABLE #{table_name}"
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def execute_drop_table(plan)
|
|
416
|
+
table_name = plan.table_name
|
|
417
|
+
options = plan.options || {}
|
|
418
|
+
|
|
419
|
+
@engine.drop_table(table_name, options)
|
|
420
|
+
|
|
421
|
+
{
|
|
422
|
+
row_count: 0,
|
|
423
|
+
message: "DROP TABLE #{table_name}"
|
|
424
|
+
}
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def execute_create_database(plan)
|
|
428
|
+
@engine.catalog.create_database(plan.database_name, **(plan.options || {}))
|
|
429
|
+
{row_count: 0, message: "CREATE DATABASE #{plan.database_name}"}
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def execute_drop_database(plan)
|
|
433
|
+
@engine.catalog.drop_database(plan.database_name, **(plan.options || {}))
|
|
434
|
+
{row_count: 0, message: "DROP DATABASE #{plan.database_name}"}
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def execute_create_schema(plan)
|
|
438
|
+
@engine.catalog.create_schema(plan.schema_name, **(plan.options || {}))
|
|
439
|
+
{row_count: 0, message: "CREATE SCHEMA #{plan.schema_name}"}
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def execute_drop_schema(plan)
|
|
443
|
+
@engine.catalog.drop_schema(plan.schema_name, **(plan.options || {}))
|
|
444
|
+
{row_count: 0, message: "DROP SCHEMA #{plan.schema_name}"}
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def execute_create_view(plan)
|
|
448
|
+
query = plan.query.respond_to?(:to_sql) ? plan.query.to_sql : plan.query
|
|
449
|
+
@engine.catalog.create_view(plan.view_name, query, **(plan.options || {}))
|
|
450
|
+
{row_count: 0, message: "CREATE VIEW #{plan.view_name}"}
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def execute_drop_view(plan)
|
|
454
|
+
@engine.catalog.drop_view(plan.view_name, **(plan.options || {}))
|
|
455
|
+
{row_count: 0, message: "DROP VIEW #{plan.view_name}"}
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def execute_create_trigger(plan)
|
|
459
|
+
definition = "EXECUTE FUNCTION #{plan.function_name}()"
|
|
460
|
+
@engine.catalog.create_trigger(plan.trigger_name, plan.event, plan.target_table, definition, timing: plan.timing, function_name: plan.function_name)
|
|
461
|
+
{row_count: 0, message: "CREATE TRIGGER #{plan.trigger_name}"}
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def execute_drop_trigger(plan)
|
|
465
|
+
@engine.catalog.drop_trigger(plan.trigger_name, **(plan.options || {}))
|
|
466
|
+
{row_count: 0, message: "DROP TRIGGER #{plan.trigger_name}"}
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def execute_vacuum(_plan)
|
|
470
|
+
result = @engine.vacuum
|
|
471
|
+
{row_count: 0, vacuum: result, message: "VACUUM"}
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def execute_alter_table_add_column(plan)
|
|
475
|
+
@engine.add_column(plan.table_name, plan.column_name, plan.column_type, plan.options)
|
|
476
|
+
{row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD COLUMN #{plan.column_name}"}
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def execute_alter_table_drop_column(plan)
|
|
480
|
+
@engine.drop_column(plan.table_name, plan.column_name)
|
|
481
|
+
{row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP COLUMN #{plan.column_name}"}
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def execute_alter_table_add_constraint(plan)
|
|
485
|
+
@engine.add_constraint(plan.table_name, plan.constraint)
|
|
486
|
+
{row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD CONSTRAINT #{plan.constraint.name}"}
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def execute_alter_table_drop_constraint(plan)
|
|
490
|
+
@engine.drop_constraint(plan.table_name, plan.constraint_name)
|
|
491
|
+
{row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP CONSTRAINT #{plan.constraint_name}"}
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def execute_create_index(plan)
|
|
495
|
+
index_name = plan.index_name
|
|
496
|
+
table_name = plan.table_name
|
|
497
|
+
columns = plan.columns
|
|
498
|
+
options = plan.options || {}
|
|
499
|
+
|
|
500
|
+
if @engine.respond_to?(:index_manager)
|
|
501
|
+
@engine.index_manager.create_index(index_name, table_name, columns, options)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
{
|
|
505
|
+
row_count: 0,
|
|
506
|
+
message: "CREATE INDEX #{index_name}"
|
|
507
|
+
}
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def execute_drop_index(plan)
|
|
511
|
+
index_name = plan.index_name
|
|
512
|
+
options = plan.options || {}
|
|
513
|
+
|
|
514
|
+
if @engine.respond_to?(:index_manager)
|
|
515
|
+
@engine.index_manager.drop_index(index_name, options)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
{
|
|
519
|
+
row_count: 0,
|
|
520
|
+
message: "DROP INDEX #{index_name}"
|
|
521
|
+
}
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def execute_begin_transaction(plan)
|
|
525
|
+
isolation_level = plan.isolation_level || :read_committed
|
|
526
|
+
transaction_id = @engine.begin_transaction(isolation_level)
|
|
527
|
+
|
|
528
|
+
{
|
|
529
|
+
row_count: 0,
|
|
530
|
+
transaction_id: transaction_id,
|
|
531
|
+
message: "BEGIN"
|
|
532
|
+
}
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def execute_commit(plan)
|
|
536
|
+
@engine.commit_transaction
|
|
537
|
+
|
|
538
|
+
{
|
|
539
|
+
row_count: 0,
|
|
540
|
+
message: "COMMIT"
|
|
541
|
+
}
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def execute_rollback(plan)
|
|
545
|
+
@engine.rollback_transaction
|
|
546
|
+
|
|
547
|
+
{
|
|
548
|
+
row_count: 0,
|
|
549
|
+
message: "ROLLBACK"
|
|
550
|
+
}
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def execute_savepoint(plan)
|
|
554
|
+
@engine.create_savepoint(plan.name)
|
|
555
|
+
{row_count: 0, message: "SAVEPOINT #{plan.name}"}
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def execute_rollback_to_savepoint(plan)
|
|
559
|
+
@engine.rollback_to_savepoint(plan.name)
|
|
560
|
+
{row_count: 0, message: "ROLLBACK TO SAVEPOINT #{plan.name}"}
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def execute_release_savepoint(plan)
|
|
564
|
+
@engine.release_savepoint(plan.name)
|
|
565
|
+
{row_count: 0, message: "RELEASE SAVEPOINT #{plan.name}"}
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def execute_explain(plan)
|
|
569
|
+
statement = plan.statement
|
|
570
|
+
analyze = plan.analyze || false
|
|
571
|
+
|
|
572
|
+
# Generate execution plan for the statement
|
|
573
|
+
planner = Planner.new(@engine)
|
|
574
|
+
analyzed_plan = planner.plan(statement)
|
|
575
|
+
|
|
576
|
+
# Format the plan
|
|
577
|
+
plan_text = format_plan(analyzed_plan)
|
|
578
|
+
|
|
579
|
+
if analyze
|
|
580
|
+
# Execute and get actual stats
|
|
581
|
+
start_time = Time.now
|
|
582
|
+
result = execute(analyzed_plan)
|
|
583
|
+
elapsed_ms = ((Time.now - start_time) * 1000).round(2)
|
|
584
|
+
|
|
585
|
+
plan_text = "EXPLAIN ANALYZE:\n#{plan_text}\n" \
|
|
586
|
+
"Execution Time: #{elapsed_ms}ms\n" \
|
|
587
|
+
"Rows: #{result[:row_count]}"
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
{
|
|
591
|
+
rows: [{"QUERY PLAN" => plan_text}],
|
|
592
|
+
row_count: 1,
|
|
593
|
+
column_names: ["QUERY PLAN"]
|
|
594
|
+
}
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# Scan operations
|
|
598
|
+
def accelerate_snapshot_scan(plan)
|
|
599
|
+
return nil unless @accelerator
|
|
600
|
+
return nil unless @accelerator.read_pipeline?
|
|
601
|
+
return nil unless @accelerator_dispatch.preferred?(:snapshot_scan)
|
|
602
|
+
return nil unless plan.table_name && plan.joins.empty?
|
|
603
|
+
return nil if @cte_rows.any?
|
|
604
|
+
return nil if plan.distinct || plan.having || (plan.aggregates && !plan.aggregates.empty?)
|
|
605
|
+
return nil if plan.estimated_rows.to_i < @accelerator.min_rows
|
|
606
|
+
|
|
607
|
+
filters = accelerator_filters(plan.predicate)
|
|
608
|
+
order_by = accelerator_order_by(plan.order_by)
|
|
609
|
+
return nil if filters.nil? || order_by.nil?
|
|
610
|
+
|
|
611
|
+
columns = @engine.table_columns(plan.table_name)
|
|
612
|
+
column_names = columns.map { |column| column.name.to_s }
|
|
613
|
+
column_types = columns.each_with_object({}) { |column, types| types[column.name.to_s] = column.type_class.to_s }
|
|
614
|
+
index_name = if plan.scan_type == :index && plan.index&.type&.to_sym == :btree
|
|
615
|
+
plan.index.name.to_s
|
|
616
|
+
end
|
|
617
|
+
storage_order = order_by.map { |order| order.merge(column: order[:column].to_s.split(".").last) }
|
|
618
|
+
windowed_projection = Array(plan.projections).any? do |projection|
|
|
619
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
620
|
+
expression.is_a?(SQL::AST::FunctionCall) && expression.window
|
|
621
|
+
end
|
|
622
|
+
window_limit = if !windowed_projection && !plan.distinct && !(plan.aggregates && !plan.aggregates.empty?)
|
|
623
|
+
plan.limit
|
|
624
|
+
end
|
|
625
|
+
window_eligible = !windowed_projection && !plan.distinct && !(plan.aggregates && !plan.aggregates.empty?)
|
|
626
|
+
window_offset = (window_limit || window_eligible) ? (plan.offset || 0) : 0
|
|
627
|
+
|
|
628
|
+
@stats[:accelerator_requests] += 1
|
|
629
|
+
calibrating = !@accelerator.manager.performance_calibrated?(:snapshot_scan)
|
|
630
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
631
|
+
result = nil
|
|
632
|
+
@engine.with_accelerator_snapshot do |manifest|
|
|
633
|
+
result = @accelerator.snapshot_scan(
|
|
634
|
+
manifest,
|
|
635
|
+
table: plan.table_name,
|
|
636
|
+
columns: column_names,
|
|
637
|
+
column_types: column_types,
|
|
638
|
+
filters: filters,
|
|
639
|
+
order_by: storage_order,
|
|
640
|
+
index_name: index_name,
|
|
641
|
+
limit: window_limit,
|
|
642
|
+
offset: window_offset,
|
|
643
|
+
batch_size: window_limit ? [window_limit, 1024].min : 1024
|
|
644
|
+
)
|
|
645
|
+
end
|
|
646
|
+
return nil unless result
|
|
647
|
+
@accelerator_window_applied = !window_limit.nil? || window_offset.positive?
|
|
648
|
+
return result[:rows] unless calibrating
|
|
649
|
+
|
|
650
|
+
ruby_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
651
|
+
ruby_rows = ruby_scan_rows(@engine.select_rows(plan.table_name, columns), plan, apply_window: !window_limit.nil?)
|
|
652
|
+
go_ms = elapsed_milliseconds(started_at)
|
|
653
|
+
ruby_ms = elapsed_milliseconds(ruby_started_at)
|
|
654
|
+
unless accelerator_results_equivalent?(result[:rows], ruby_rows)
|
|
655
|
+
@stats[:accelerator_fallbacks] += 1
|
|
656
|
+
@accelerator.manager.record_performance(:snapshot_scan, ruby_ms: ruby_ms, go_ms: Float::INFINITY)
|
|
657
|
+
@accelerator.manager.record_performance(:scan, ruby_ms: ruby_ms, go_ms: Float::INFINITY)
|
|
658
|
+
raise ExecutionError, "RubyDB accelerator returned a different snapshot scan result" if @accelerator.manager.mode == "required"
|
|
659
|
+
|
|
660
|
+
return ruby_rows
|
|
661
|
+
end
|
|
662
|
+
@accelerator.manager.record_performance(:snapshot_scan, ruby_ms: ruby_ms, go_ms: go_ms)
|
|
663
|
+
@accelerator.manager.record_performance(:scan, ruby_ms: ruby_ms, go_ms: go_ms)
|
|
664
|
+
@accelerator.manager.acceleration_preferred?(:snapshot_scan) ? result[:rows] : ruby_rows
|
|
665
|
+
rescue RubyDB::Accelerator::Error, RubyDB::StorageError
|
|
666
|
+
@stats[:accelerator_fallbacks] += 1
|
|
667
|
+
raise if @accelerator.manager.mode == "required"
|
|
668
|
+
|
|
669
|
+
nil
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def accelerate_scan(rows, plan)
|
|
673
|
+
return nil unless @accelerator
|
|
674
|
+
return nil unless @accelerator.read_pipeline?
|
|
675
|
+
return nil if rows.size < @accelerator.min_rows
|
|
676
|
+
return nil unless @accelerator_dispatch.preferred?(:scan)
|
|
677
|
+
return nil unless plan.joins.empty?
|
|
678
|
+
return nil if plan.distinct || (plan.aggregates && !plan.aggregates.empty?)
|
|
679
|
+
|
|
680
|
+
filters = accelerator_filters(plan.predicate)
|
|
681
|
+
order_by = accelerator_order_by(plan.order_by)
|
|
682
|
+
return nil if filters.nil? || order_by.nil?
|
|
683
|
+
return nil if filters.empty? && order_by.empty?
|
|
684
|
+
|
|
685
|
+
calibrating = !@accelerator.manager.performance_calibrated?(:scan)
|
|
686
|
+
@stats[:accelerator_requests] += 1
|
|
687
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
688
|
+
result = @accelerator.rows_pipeline(
|
|
689
|
+
rows,
|
|
690
|
+
filters: filters,
|
|
691
|
+
order_by: order_by,
|
|
692
|
+
# Ruby applies LIMIT/OFFSET after projection and aggregation. Do
|
|
693
|
+
# not apply them in Go here or the final Ruby stage would paginate
|
|
694
|
+
# the result twice.
|
|
695
|
+
limit: nil,
|
|
696
|
+
offset: 0
|
|
697
|
+
)
|
|
698
|
+
return nil unless result
|
|
699
|
+
return result[:rows] unless calibrating
|
|
700
|
+
|
|
701
|
+
ruby_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
702
|
+
ruby_rows = ruby_scan_rows(rows, plan)
|
|
703
|
+
go_ms = elapsed_milliseconds(started_at)
|
|
704
|
+
ruby_ms = elapsed_milliseconds(ruby_started_at)
|
|
705
|
+
if !accelerator_results_equivalent?(result[:rows], ruby_rows)
|
|
706
|
+
@stats[:accelerator_fallbacks] += 1
|
|
707
|
+
@accelerator.manager.record_performance(:scan, ruby_ms: ruby_ms, go_ms: Float::INFINITY)
|
|
708
|
+
if @accelerator.manager.mode == "required"
|
|
709
|
+
raise ExecutionError, "RubyDB accelerator returned a different scan result"
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
return ruby_rows
|
|
713
|
+
end
|
|
714
|
+
@accelerator.manager.record_performance(:scan, ruby_ms: ruby_ms, go_ms: go_ms)
|
|
715
|
+
@accelerator.manager.acceleration_preferred?(:scan) ? result[:rows] : ruby_rows
|
|
716
|
+
rescue RubyDB::Accelerator::Error
|
|
717
|
+
@stats[:accelerator_fallbacks] += 1
|
|
718
|
+
raise if @accelerator.manager.mode == "required"
|
|
719
|
+
|
|
720
|
+
nil
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def accelerate_aggregate(rows, plan)
|
|
724
|
+
return nil unless @accelerator
|
|
725
|
+
return nil unless @accelerator.read_pipeline?
|
|
726
|
+
return nil if rows.size < @accelerator.min_rows
|
|
727
|
+
return nil unless @accelerator_dispatch.preferred?(:aggregate)
|
|
728
|
+
return nil unless plan.joins.empty?
|
|
729
|
+
return nil if plan.distinct || plan.having || plan.order_by&.any?
|
|
730
|
+
return nil unless plan.aggregates && !plan.aggregates.empty?
|
|
731
|
+
|
|
732
|
+
filters = accelerator_filters(plan.predicate)
|
|
733
|
+
group_by = Array(plan.group_by).map { |expression| accelerator_identifier(expression) }
|
|
734
|
+
return nil if filters.nil? || group_by.any?(&:nil?)
|
|
735
|
+
|
|
736
|
+
definitions = []
|
|
737
|
+
projections = Array(plan.projections)
|
|
738
|
+
projections.each do |projection|
|
|
739
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
740
|
+
if aggregate_function?(expression)
|
|
741
|
+
return nil if expression.distinct || expression.arguments.length > 1
|
|
742
|
+
|
|
743
|
+
argument = expression.arguments.first
|
|
744
|
+
column = argument.is_a?(SQL::AST::Star) ? "*" : accelerator_identifier(argument)
|
|
745
|
+
return nil unless column
|
|
746
|
+
alias_name = projection.respond_to?(:alias_name) ? projection.alias_name : nil
|
|
747
|
+
return nil if alias_name.nil? || alias_name.to_s.empty?
|
|
748
|
+
|
|
749
|
+
definitions << {function: expression.name.to_s.upcase, column: column, alias: alias_name.to_s}
|
|
750
|
+
else
|
|
751
|
+
column = accelerator_identifier(expression)
|
|
752
|
+
return nil unless column && group_by.include?(column)
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
return nil if definitions.empty?
|
|
757
|
+
|
|
758
|
+
calibrating = !@accelerator.manager.performance_calibrated?(:aggregate)
|
|
759
|
+
@stats[:accelerator_requests] += 1
|
|
760
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
761
|
+
result = @accelerator.rows_pipeline(
|
|
762
|
+
rows,
|
|
763
|
+
filters: filters,
|
|
764
|
+
group_by: group_by,
|
|
765
|
+
aggregates: definitions,
|
|
766
|
+
limit: nil,
|
|
767
|
+
offset: 0
|
|
768
|
+
)
|
|
769
|
+
accelerated_aggregate_rows = result && result[:aggregates]
|
|
770
|
+
return nil unless accelerated_aggregate_rows
|
|
771
|
+
|
|
772
|
+
projected_rows = accelerated_aggregate_rows.map do |row|
|
|
773
|
+
projections.each_with_object({}) do |projection, projected|
|
|
774
|
+
name = projection_name(projection)
|
|
775
|
+
projected[name] = row[name]
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
return projected_rows unless calibrating
|
|
779
|
+
|
|
780
|
+
ruby_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
781
|
+
filtered_rows = plan.predicate ? rows.select { |row| evaluate_predicate(plan.predicate, row) } : rows
|
|
782
|
+
ruby_rows = aggregate_rows(filtered_rows, plan.group_by || [], projections)
|
|
783
|
+
go_ms = elapsed_milliseconds(started_at)
|
|
784
|
+
ruby_ms = elapsed_milliseconds(ruby_started_at)
|
|
785
|
+
if !accelerator_results_equivalent?(projected_rows, ruby_rows)
|
|
786
|
+
@stats[:accelerator_fallbacks] += 1
|
|
787
|
+
@accelerator.manager.record_performance(:aggregate, ruby_ms: ruby_ms, go_ms: Float::INFINITY)
|
|
788
|
+
raise ExecutionError, "RubyDB accelerator returned a different aggregate result" if @accelerator.manager.mode == "required"
|
|
789
|
+
|
|
790
|
+
return ruby_rows
|
|
791
|
+
end
|
|
792
|
+
@accelerator.manager.record_performance(:aggregate, ruby_ms: ruby_ms, go_ms: go_ms)
|
|
793
|
+
@accelerator.manager.acceleration_preferred?(:aggregate) ? projected_rows : ruby_rows
|
|
794
|
+
rescue RubyDB::Accelerator::Error
|
|
795
|
+
@stats[:accelerator_fallbacks] += 1
|
|
796
|
+
raise if @accelerator.manager.mode == "required"
|
|
797
|
+
|
|
798
|
+
nil
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def ruby_scan_rows(rows, plan, apply_window: false)
|
|
802
|
+
source_rows = rows.map { |row| row.each_with_object({}) { |(key, value), copy| copy[key.to_s] = value } }
|
|
803
|
+
result = plan.predicate ? source_rows.select { |row| evaluate_predicate(plan.predicate, row) } : source_rows
|
|
804
|
+
order_by = accelerator_order_by(plan.order_by)
|
|
805
|
+
result = sort_rows(result, order_by) if order_by && !order_by.empty?
|
|
806
|
+
if apply_window
|
|
807
|
+
offset = plan.offset || 0
|
|
808
|
+
result = result[offset, plan.limit] || [] if plan.limit
|
|
809
|
+
result = result[offset..] || [] if !plan.limit && offset.positive?
|
|
810
|
+
end
|
|
811
|
+
result
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def accelerate_join(left_rows, right_rows, join)
|
|
815
|
+
return nil unless @accelerator
|
|
816
|
+
return nil unless @accelerator.read_pipeline?
|
|
817
|
+
return nil if left_rows.size + right_rows.size < @accelerator.min_rows
|
|
818
|
+
return nil unless join[:type].to_sym == :inner
|
|
819
|
+
|
|
820
|
+
predicate = join[:predicate]
|
|
821
|
+
return nil unless predicate.is_a?(Predicate::Comparison) && predicate.operator.to_s.casecmp?("eq")
|
|
822
|
+
|
|
823
|
+
left_key = accelerator_identifier(predicate.left)
|
|
824
|
+
right_key = accelerator_identifier(predicate.right)
|
|
825
|
+
return nil unless left_key && right_key
|
|
826
|
+
return nil unless left_rows.first&.key?(left_key) && right_rows.first&.key?(right_key)
|
|
827
|
+
return nil unless @accelerator_dispatch.preferred?(:join)
|
|
828
|
+
|
|
829
|
+
calibrating = !@accelerator.manager.performance_calibrated?(:join)
|
|
830
|
+
@stats[:accelerator_requests] += 1
|
|
831
|
+
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
832
|
+
result = @accelerator.hash_join(left_rows, right_rows, left_key: left_key, right_key: right_key)
|
|
833
|
+
return nil unless result
|
|
834
|
+
return result[:rows] unless calibrating
|
|
835
|
+
|
|
836
|
+
ruby_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
837
|
+
ruby_rows = execute_join(left_rows, right_rows, join)
|
|
838
|
+
go_ms = elapsed_milliseconds(started_at)
|
|
839
|
+
ruby_ms = elapsed_milliseconds(ruby_started_at)
|
|
840
|
+
unless accelerator_results_equivalent?(result[:rows], ruby_rows)
|
|
841
|
+
@stats[:accelerator_fallbacks] += 1
|
|
842
|
+
@accelerator.manager.record_performance(:join, ruby_ms: ruby_ms, go_ms: Float::INFINITY)
|
|
843
|
+
raise ExecutionError, "RubyDB accelerator returned a different join result" if @accelerator.manager.mode == "required"
|
|
844
|
+
|
|
845
|
+
return ruby_rows
|
|
846
|
+
end
|
|
847
|
+
@accelerator.manager.record_performance(:join, ruby_ms: ruby_ms, go_ms: go_ms)
|
|
848
|
+
@accelerator.manager.acceleration_preferred?(:join) ? result[:rows] : ruby_rows
|
|
849
|
+
rescue RubyDB::Accelerator::Error
|
|
850
|
+
@stats[:accelerator_fallbacks] += 1
|
|
851
|
+
raise if @accelerator.manager.mode == "required"
|
|
852
|
+
|
|
853
|
+
nil
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def elapsed_milliseconds(started_at)
|
|
857
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def accelerator_results_equivalent?(left, right)
|
|
861
|
+
@accelerator_dispatch.equivalent?(left, right)
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def accelerator_filters(predicate)
|
|
865
|
+
return [] unless predicate
|
|
866
|
+
|
|
867
|
+
case predicate
|
|
868
|
+
when Predicate::And
|
|
869
|
+
left = accelerator_filters(predicate.left)
|
|
870
|
+
right = accelerator_filters(predicate.right)
|
|
871
|
+
return nil if left.nil? || right.nil?
|
|
872
|
+
|
|
873
|
+
left + right
|
|
874
|
+
when Predicate::Comparison
|
|
875
|
+
column = accelerator_identifier(predicate.left)
|
|
876
|
+
value = accelerator_literal(predicate.right)
|
|
877
|
+
return nil if column.nil? || value.equal?(:unsupported)
|
|
878
|
+
|
|
879
|
+
[{column: column, operator: predicate.operator.to_s, value: value}]
|
|
880
|
+
when Predicate::Between
|
|
881
|
+
column = accelerator_identifier(predicate.expression)
|
|
882
|
+
low = accelerator_literal(predicate.low)
|
|
883
|
+
high = accelerator_literal(predicate.high)
|
|
884
|
+
return nil if column.nil? || low.equal?(:unsupported) || high.equal?(:unsupported)
|
|
885
|
+
|
|
886
|
+
[
|
|
887
|
+
{column: column, operator: "gte", value: low},
|
|
888
|
+
{column: column, operator: "lte", value: high}
|
|
889
|
+
]
|
|
890
|
+
when Predicate::IsNull
|
|
891
|
+
column = accelerator_identifier(predicate.expression)
|
|
892
|
+
return nil unless column
|
|
893
|
+
|
|
894
|
+
[{column: column, operator: predicate.negated ? "is_not_null" : "is_null"}]
|
|
895
|
+
when Predicate::Like
|
|
896
|
+
column = accelerator_identifier(predicate.expression)
|
|
897
|
+
value = accelerator_literal(predicate.pattern)
|
|
898
|
+
return nil if column.nil? || value.equal?(:unsupported)
|
|
899
|
+
|
|
900
|
+
[{column: column, operator: "like", value: value}]
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def accelerator_order_by(order_by)
|
|
905
|
+
order_by.map do |order|
|
|
906
|
+
expression = order.is_a?(Hash) ? order[:column] : order.expression
|
|
907
|
+
column = accelerator_identifier(expression)
|
|
908
|
+
return nil unless column
|
|
909
|
+
|
|
910
|
+
direction = order.is_a?(Hash) ? order[:direction] : order.direction
|
|
911
|
+
{column: column, direction: direction.to_s}
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
def accelerator_identifier(expression)
|
|
916
|
+
return expression.to_s if expression.is_a?(String) || expression.is_a?(Symbol)
|
|
917
|
+
return expression.name.to_s if expression.is_a?(Expression::Column)
|
|
918
|
+
return nil unless expression.is_a?(SQL::AST::Identifier)
|
|
919
|
+
|
|
920
|
+
expression.name.to_s
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
def accelerator_literal(expression)
|
|
924
|
+
return expression.value if expression.is_a?(Expression::Literal)
|
|
925
|
+
return :unsupported unless expression.is_a?(SQL::AST::Literal)
|
|
926
|
+
|
|
927
|
+
expression.value
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def scan_table(plan)
|
|
931
|
+
table_name = plan.table_name
|
|
932
|
+
return [{}] unless table_name
|
|
933
|
+
return @cte_rows.fetch(table_name.to_s) if @cte_rows.key?(table_name.to_s)
|
|
934
|
+
if @engine.catalog.respond_to?(:find_view) && (view = @engine.catalog.find_view(table_name))
|
|
935
|
+
statement = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(view.query).tokenize).parse.first
|
|
936
|
+
return scan_table(Planner.new(@engine).plan(statement))
|
|
937
|
+
end
|
|
938
|
+
columns = @engine.table_columns(table_name)
|
|
939
|
+
|
|
940
|
+
if plan.scan_type == :index && plan.index
|
|
941
|
+
# Use index scan
|
|
942
|
+
scan = Indexes::IndexScan.new(plan.index, :full)
|
|
943
|
+
results = scan.execute
|
|
944
|
+
row_ids = results.map { |r| r[:row_id] }
|
|
945
|
+
|
|
946
|
+
# Fetch rows by row_id
|
|
947
|
+
rows = []
|
|
948
|
+
row_ids.each do |row_id|
|
|
949
|
+
row = @engine.select_row(table_name, row_id, columns)
|
|
950
|
+
rows << row if row
|
|
951
|
+
end
|
|
952
|
+
rows
|
|
953
|
+
else
|
|
954
|
+
# Sequential scan
|
|
955
|
+
@engine.select_rows(table_name, columns)
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def qualify_rows(rows, table_reference)
|
|
960
|
+
table_name = table_reference.respond_to?(:name) ? table_reference.name : table_reference
|
|
961
|
+
alias_name = table_reference.respond_to?(:alias_name) ? table_reference.alias_name : nil
|
|
962
|
+
rows.map do |row|
|
|
963
|
+
row.each_with_object({}) do |(key, value), qualified|
|
|
964
|
+
key = key.to_s
|
|
965
|
+
qualified[key] = value
|
|
966
|
+
qualified["#{table_name}.#{key}"] = value
|
|
967
|
+
qualified["#{alias_name}.#{key}"] = value if alias_name
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
def execute_join(left_rows, right_rows, join)
|
|
973
|
+
return left_rows.flat_map { |left_row| right_rows.map { |right_row| merge_join_rows(left_row, right_row) } } if join[:type] == :cross
|
|
974
|
+
|
|
975
|
+
result = []
|
|
976
|
+
matched_right = Array.new(right_rows.length, false)
|
|
977
|
+
left_rows.each do |left_row|
|
|
978
|
+
check_deadline!
|
|
979
|
+
matches = right_rows.each_index.select do |index|
|
|
980
|
+
evaluate_predicate(join[:predicate], merge_join_rows(left_row, right_rows[index]))
|
|
981
|
+
end
|
|
982
|
+
if matches.empty?
|
|
983
|
+
result << merge_join_rows(left_row, nil) if %i[left full].include?(join[:type])
|
|
984
|
+
else
|
|
985
|
+
matches.each do |index|
|
|
986
|
+
matched_right[index] = true
|
|
987
|
+
result << merge_join_rows(left_row, right_rows[index])
|
|
988
|
+
end
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
if %i[right full].include?(join[:type])
|
|
992
|
+
right_rows.each_with_index do |right_row, index|
|
|
993
|
+
result << merge_join_rows(nil, right_row) unless matched_right[index]
|
|
994
|
+
end
|
|
995
|
+
end
|
|
996
|
+
result
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def merge_join_rows(left_row, right_row)
|
|
1000
|
+
(left_row || {}).merge(right_row || {}) { |_key, left_value, _right_value| left_value }
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
# Predicate evaluation
|
|
1004
|
+
def evaluate_predicate(predicate, row)
|
|
1005
|
+
case predicate
|
|
1006
|
+
when Predicate::Comparison
|
|
1007
|
+
left_val = evaluate_expression(predicate.left, row)
|
|
1008
|
+
right_val = evaluate_expression(predicate.right, row)
|
|
1009
|
+
compare_values(left_val, right_val, predicate.operator)
|
|
1010
|
+
when Predicate::And
|
|
1011
|
+
evaluate_predicate(predicate.left, row) && evaluate_predicate(predicate.right, row)
|
|
1012
|
+
when Predicate::Or
|
|
1013
|
+
evaluate_predicate(predicate.left, row) || evaluate_predicate(predicate.right, row)
|
|
1014
|
+
when Predicate::Not
|
|
1015
|
+
!evaluate_predicate(predicate.operand, row)
|
|
1016
|
+
when Predicate::Between
|
|
1017
|
+
val = evaluate_expression(predicate.expression, row)
|
|
1018
|
+
low = evaluate_expression(predicate.low, row)
|
|
1019
|
+
high = evaluate_expression(predicate.high, row)
|
|
1020
|
+
val >= low && val <= high
|
|
1021
|
+
when Predicate::In
|
|
1022
|
+
val = evaluate_expression(predicate.expression, row)
|
|
1023
|
+
predicate.values.any? { |v| val == evaluate_expression(v, row) }
|
|
1024
|
+
when Predicate::IsNull
|
|
1025
|
+
val = evaluate_expression(predicate.expression, row)
|
|
1026
|
+
predicate.negated ? !val.nil? : val.nil?
|
|
1027
|
+
when Predicate::Like
|
|
1028
|
+
val = evaluate_expression(predicate.expression, row).to_s
|
|
1029
|
+
pattern = evaluate_expression(predicate.pattern, row).to_s
|
|
1030
|
+
like_match?(val, pattern, predicate.case_sensitive)
|
|
1031
|
+
else
|
|
1032
|
+
true
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def evaluate_expression(expr, row = nil)
|
|
1037
|
+
case expr
|
|
1038
|
+
when SQL::AST::Literal
|
|
1039
|
+
expr.value
|
|
1040
|
+
when SQL::AST::Identifier
|
|
1041
|
+
return nil unless row
|
|
1042
|
+
|
|
1043
|
+
qualified_name = expr.table && "#{expr.table}.#{expr.name}"
|
|
1044
|
+
row_value(row, qualified_name, expr.name, expr.name.to_sym)
|
|
1045
|
+
when SQL::AST::UnaryOp
|
|
1046
|
+
apply_ast_unary_op(expr.operator, evaluate_expression(expr.operand, row))
|
|
1047
|
+
when SQL::AST::BinaryOp
|
|
1048
|
+
apply_ast_binary_op(
|
|
1049
|
+
expr.operator,
|
|
1050
|
+
evaluate_expression(expr.left, row),
|
|
1051
|
+
evaluate_expression(expr.right, row)
|
|
1052
|
+
)
|
|
1053
|
+
when SQL::AST::FunctionCall
|
|
1054
|
+
if expr.window
|
|
1055
|
+
row && row[window_key(expr)]
|
|
1056
|
+
else
|
|
1057
|
+
apply_function(expr.name, expr.arguments.map { |argument| evaluate_expression(argument, row) })
|
|
1058
|
+
end
|
|
1059
|
+
when SQL::AST::Subquery
|
|
1060
|
+
result = child_executor(@cte_rows).execute(Planner.new(@engine).plan(expr.query))
|
|
1061
|
+
rows = result[:rows]
|
|
1062
|
+
raise ExecutionError, "Scalar subquery returned more than one row" if rows.size > 1
|
|
1063
|
+
rows.empty? ? nil : rows.first.values.first
|
|
1064
|
+
when Expression::Literal
|
|
1065
|
+
expr.value
|
|
1066
|
+
when Expression::Column
|
|
1067
|
+
return nil unless row
|
|
1068
|
+
|
|
1069
|
+
qualified_name = expr.table && "#{expr.table}.#{expr.name}"
|
|
1070
|
+
row_value(row, qualified_name, expr.name, expr.name.to_sym)
|
|
1071
|
+
when Expression::BinaryOp
|
|
1072
|
+
left = evaluate_expression(expr.left, row)
|
|
1073
|
+
right = evaluate_expression(expr.right, row)
|
|
1074
|
+
apply_binary_op(left, right, expr.operator)
|
|
1075
|
+
when Expression::UnaryOp
|
|
1076
|
+
operand = evaluate_expression(expr.operand, row)
|
|
1077
|
+
apply_unary_op(operand, expr.operator)
|
|
1078
|
+
when Expression::Function
|
|
1079
|
+
args = expr.arguments.map { |arg| evaluate_expression(arg, row) }
|
|
1080
|
+
apply_function(expr.name, args)
|
|
1081
|
+
when Expression::Parameter
|
|
1082
|
+
expr.value
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
# Helper methods
|
|
1087
|
+
def compare_values(left, right, operator)
|
|
1088
|
+
return false if left.nil? || right.nil?
|
|
1089
|
+
|
|
1090
|
+
case operator
|
|
1091
|
+
when :eq then left == right
|
|
1092
|
+
when :ne then left != right
|
|
1093
|
+
when :lt then left < right
|
|
1094
|
+
when :lte then left <= right
|
|
1095
|
+
when :gt then left > right
|
|
1096
|
+
when :gte then left >= right
|
|
1097
|
+
else false
|
|
1098
|
+
end
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
def apply_binary_op(left, right, operator)
|
|
1102
|
+
return nil if left.nil? || right.nil?
|
|
1103
|
+
|
|
1104
|
+
case operator
|
|
1105
|
+
when :plus then left + right
|
|
1106
|
+
when :minus then left - right
|
|
1107
|
+
when :multiply then left * right
|
|
1108
|
+
when :divide then left / right if right != 0
|
|
1109
|
+
when :modulo then left % right if right != 0
|
|
1110
|
+
when :concat then left.to_s + right.to_s
|
|
1111
|
+
end
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
def apply_ast_unary_op(operator, value)
|
|
1115
|
+
return nil if value.nil?
|
|
1116
|
+
|
|
1117
|
+
case operator
|
|
1118
|
+
when SQL::Token::Type::PLUS then value
|
|
1119
|
+
when SQL::Token::Type::MINUS then -value
|
|
1120
|
+
when SQL::Token::Type::NOT then !value
|
|
1121
|
+
else value
|
|
1122
|
+
end
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def apply_ast_binary_op(operator, left, right)
|
|
1126
|
+
case operator
|
|
1127
|
+
# SQL comparisons involving NULL evaluate to UNKNOWN, represented by
|
|
1128
|
+
# nil here. WHERE filtering already treats UNKNOWN as non-matching.
|
|
1129
|
+
when SQL::Token::Type::EQ then (left.nil? || right.nil?) ? nil : left == right
|
|
1130
|
+
when SQL::Token::Type::NE then (left.nil? || right.nil?) ? nil : left != right
|
|
1131
|
+
when SQL::Token::Type::LT then !left.nil? && !right.nil? && left < right
|
|
1132
|
+
when SQL::Token::Type::LTE then !left.nil? && !right.nil? && left <= right
|
|
1133
|
+
when SQL::Token::Type::GT then !left.nil? && !right.nil? && left > right
|
|
1134
|
+
when SQL::Token::Type::GTE then !left.nil? && !right.nil? && left >= right
|
|
1135
|
+
when SQL::Token::Type::AND then sql_and(left, right)
|
|
1136
|
+
when SQL::Token::Type::OR then sql_or(left, right)
|
|
1137
|
+
when SQL::Token::Type::PLUS then apply_binary_op(left, right, :plus)
|
|
1138
|
+
when SQL::Token::Type::MINUS then apply_binary_op(left, right, :minus)
|
|
1139
|
+
when SQL::Token::Type::STAR then apply_binary_op(left, right, :multiply)
|
|
1140
|
+
when SQL::Token::Type::SLASH then apply_binary_op(left, right, :divide)
|
|
1141
|
+
when SQL::Token::Type::PERCENT then apply_binary_op(left, right, :modulo)
|
|
1142
|
+
end
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def apply_unary_op(operand, operator)
|
|
1146
|
+
return nil if operand.nil?
|
|
1147
|
+
|
|
1148
|
+
case operator
|
|
1149
|
+
when :negate then -operand
|
|
1150
|
+
when :not then !operand
|
|
1151
|
+
else operand
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
def sql_and(left, right)
|
|
1156
|
+
return false if left == false || right == false
|
|
1157
|
+
return nil if left.nil? || right.nil?
|
|
1158
|
+
|
|
1159
|
+
!!left && !!right
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def sql_or(left, right)
|
|
1163
|
+
return true if left == true || right == true
|
|
1164
|
+
return nil if left.nil? || right.nil?
|
|
1165
|
+
|
|
1166
|
+
!!left || !!right
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
def row_value(row, *keys)
|
|
1170
|
+
keys.compact.each do |key|
|
|
1171
|
+
return row[key] if row.respond_to?(:key?) && row.key?(key)
|
|
1172
|
+
end
|
|
1173
|
+
nil
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
def child_executor(cte_rows)
|
|
1177
|
+
self.class.new(
|
|
1178
|
+
@engine,
|
|
1179
|
+
cte_rows: cte_rows,
|
|
1180
|
+
deadline_at: @deadline_at,
|
|
1181
|
+
cancellation: @cancellation
|
|
1182
|
+
)
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def check_deadline!
|
|
1186
|
+
if @cancellation&.cancelled?
|
|
1187
|
+
raise ExecutionError.new("Request cancelled by client", code: "cancelled")
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
return unless @deadline_at && Time.now >= @deadline_at
|
|
1191
|
+
|
|
1192
|
+
raise ExecutionError.new("Request deadline exceeded during execution", code: "deadline_exceeded")
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def apply_function(name, args)
|
|
1196
|
+
case name.to_s.upcase
|
|
1197
|
+
when "COUNT"
|
|
1198
|
+
args.compact.size
|
|
1199
|
+
when "SUM"
|
|
1200
|
+
args.compact.sum
|
|
1201
|
+
when "AVG"
|
|
1202
|
+
vals = args.compact
|
|
1203
|
+
vals.empty? ? 0 : vals.sum / vals.size.to_f
|
|
1204
|
+
when "MIN"
|
|
1205
|
+
args.compact.min
|
|
1206
|
+
when "MAX"
|
|
1207
|
+
args.compact.max
|
|
1208
|
+
when "LOWER"
|
|
1209
|
+
args.first.to_s.downcase
|
|
1210
|
+
when "UPPER"
|
|
1211
|
+
args.first.to_s.upcase
|
|
1212
|
+
when "LENGTH"
|
|
1213
|
+
args.first.to_s.length
|
|
1214
|
+
when "COALESCE"
|
|
1215
|
+
args.find { |arg| !arg.nil? }
|
|
1216
|
+
when "NOW"
|
|
1217
|
+
Time.now
|
|
1218
|
+
when "CURRENT_DATE"
|
|
1219
|
+
Date.today
|
|
1220
|
+
when "CURRENT_TIME"
|
|
1221
|
+
Time.now
|
|
1222
|
+
end
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def like_match?(value, pattern, case_sensitive = true)
|
|
1226
|
+
return false if value.nil? || pattern.nil?
|
|
1227
|
+
|
|
1228
|
+
str = case_sensitive ? value : value.downcase
|
|
1229
|
+
pat = case_sensitive ? pattern : pattern.downcase
|
|
1230
|
+
|
|
1231
|
+
# Convert SQL LIKE pattern to regex
|
|
1232
|
+
regex_str = Regexp.escape(pat)
|
|
1233
|
+
.gsub("%", ".*")
|
|
1234
|
+
.gsub("_", ".")
|
|
1235
|
+
Regexp.new("^#{regex_str}$").match?(str)
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
def project_row(row, projections)
|
|
1239
|
+
result = {}
|
|
1240
|
+
projections.each do |proj|
|
|
1241
|
+
expression = proj.respond_to?(:expression) ? proj.expression : proj
|
|
1242
|
+
|
|
1243
|
+
if expression.is_a?(SQL::AST::Star)
|
|
1244
|
+
if expression.table
|
|
1245
|
+
prefix = "#{expression.table}."
|
|
1246
|
+
row.each do |key, value|
|
|
1247
|
+
key = key.to_s
|
|
1248
|
+
next unless key.start_with?(prefix)
|
|
1249
|
+
|
|
1250
|
+
column = key.delete_prefix(prefix)
|
|
1251
|
+
result[column] = value unless column.start_with?("_")
|
|
1252
|
+
end
|
|
1253
|
+
else
|
|
1254
|
+
row.each do |key, value|
|
|
1255
|
+
key = key.to_s
|
|
1256
|
+
result[key] = value unless key.include?(".") || key.start_with?("_")
|
|
1257
|
+
end
|
|
1258
|
+
end
|
|
1259
|
+
elsif expression.is_a?(String) || expression.is_a?(Symbol)
|
|
1260
|
+
result[expression.to_s] = row[expression.to_s]
|
|
1261
|
+
else
|
|
1262
|
+
result[projection_name(proj)] = evaluate_expression(expression, row)
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
result
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
def distinct_rows(rows)
|
|
1269
|
+
seen = Set.new
|
|
1270
|
+
rows.select do |row|
|
|
1271
|
+
key = row.to_hash
|
|
1272
|
+
if seen.include?(key)
|
|
1273
|
+
false
|
|
1274
|
+
else
|
|
1275
|
+
seen.add(key)
|
|
1276
|
+
true
|
|
1277
|
+
end
|
|
1278
|
+
end
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
def group_rows(rows, group_by, aggregates)
|
|
1282
|
+
groups = {}
|
|
1283
|
+
rows.each do |row|
|
|
1284
|
+
key = group_by.map { |col| row[col.to_s] }
|
|
1285
|
+
groups[key] ||= []
|
|
1286
|
+
groups[key] << row
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
result = []
|
|
1290
|
+
groups.each do |key, group_rows|
|
|
1291
|
+
result_row = {}
|
|
1292
|
+
group_by.each_with_index do |col, idx|
|
|
1293
|
+
result_row[col.to_s] = key[idx]
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
aggregates&.each do |agg|
|
|
1297
|
+
result_row[agg.alias || agg.name] = apply_aggregate(agg, group_rows)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
result << result_row
|
|
1301
|
+
end
|
|
1302
|
+
result
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
def aggregate_rows(rows, group_by, projections)
|
|
1306
|
+
groups = {}
|
|
1307
|
+
if group_by.empty?
|
|
1308
|
+
groups[[]] = rows
|
|
1309
|
+
else
|
|
1310
|
+
rows.each do |row|
|
|
1311
|
+
key = group_by.map { |expression| evaluate_expression(expression, row) }
|
|
1312
|
+
(groups[key] ||= []) << row
|
|
1313
|
+
end
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1316
|
+
groups.map do |_key, group|
|
|
1317
|
+
representative = group.first || {}
|
|
1318
|
+
projections.each_with_object({}) do |projection, result|
|
|
1319
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
1320
|
+
name = projection_name(projection)
|
|
1321
|
+
result[name] = if aggregate_function?(expression)
|
|
1322
|
+
apply_aggregate_function(expression, group)
|
|
1323
|
+
else
|
|
1324
|
+
evaluate_expression(expression, representative)
|
|
1325
|
+
end
|
|
1326
|
+
end
|
|
1327
|
+
end
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
def aggregate_function?(expression)
|
|
1331
|
+
expression.is_a?(SQL::AST::FunctionCall) && !expression.window &&
|
|
1332
|
+
%w[COUNT SUM AVG MIN MAX].include?(expression.name.to_s.upcase)
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def apply_window_functions!(rows, projections)
|
|
1336
|
+
projections.each do |projection|
|
|
1337
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
1338
|
+
next unless expression.is_a?(SQL::AST::FunctionCall) && expression.window
|
|
1339
|
+
|
|
1340
|
+
spec = expression.window
|
|
1341
|
+
partitions = {}
|
|
1342
|
+
rows.each { |row| (partitions[spec[:partition_by].map { |part| evaluate_expression(part, row) }] ||= []) << row }
|
|
1343
|
+
partitions.each_value do |partition_rows|
|
|
1344
|
+
ordered_rows = sort_window_rows(partition_rows, spec[:order_by])
|
|
1345
|
+
ordered_rows.each_with_index do |row, index|
|
|
1346
|
+
row[window_key(expression)] = window_value(expression, ordered_rows, index, spec[:order_by], spec[:frame])
|
|
1347
|
+
end
|
|
1348
|
+
end
|
|
1349
|
+
end
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
def window_key(expression)
|
|
1353
|
+
"__rubydb_window_#{expression.object_id}"
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def sort_window_rows(rows, order_by)
|
|
1357
|
+
return rows.dup if order_by.empty?
|
|
1358
|
+
|
|
1359
|
+
rows.sort do |left, right|
|
|
1360
|
+
comparison = 0
|
|
1361
|
+
order_by.each do |item|
|
|
1362
|
+
left_value = evaluate_expression(item.expression, left)
|
|
1363
|
+
right_value = evaluate_expression(item.expression, right)
|
|
1364
|
+
comparison = compare_sort_values(left_value, right_value)
|
|
1365
|
+
comparison = -comparison if item.direction == :desc
|
|
1366
|
+
break unless comparison.zero?
|
|
1367
|
+
end
|
|
1368
|
+
comparison
|
|
1369
|
+
end
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
def compare_sort_values(left, right)
|
|
1373
|
+
return 0 if left.nil? && right.nil?
|
|
1374
|
+
return -1 if left.nil?
|
|
1375
|
+
return 1 if right.nil?
|
|
1376
|
+
|
|
1377
|
+
left <=> right
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
def window_value(expression, rows, index, order_by, frame = nil)
|
|
1381
|
+
rows = window_frame_rows(rows, index, frame) if frame
|
|
1382
|
+
name = expression.name.to_s.upcase
|
|
1383
|
+
if %w[ROW_NUMBER RANK DENSE_RANK].include?(name)
|
|
1384
|
+
return index + 1 if name == "ROW_NUMBER" || order_by.empty?
|
|
1385
|
+
|
|
1386
|
+
keys = rows.map { |row| order_by.map { |item| evaluate_expression(item.expression, row) } }
|
|
1387
|
+
current = keys[index]
|
|
1388
|
+
return keys.index(current) + 1 if name == "RANK"
|
|
1389
|
+
|
|
1390
|
+
return keys[0..index].uniq.index(current) + 1
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
argument = expression.arguments.first
|
|
1394
|
+
values = if argument.is_a?(SQL::AST::Star)
|
|
1395
|
+
rows
|
|
1396
|
+
else
|
|
1397
|
+
rows.map { |row| evaluate_expression(argument, row) }.compact
|
|
1398
|
+
end
|
|
1399
|
+
values = values.uniq if expression.distinct
|
|
1400
|
+
case name
|
|
1401
|
+
when "COUNT" then values.size
|
|
1402
|
+
when "SUM" then values.empty? ? nil : values.sum
|
|
1403
|
+
when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
|
|
1404
|
+
when "MIN" then values.min
|
|
1405
|
+
when "MAX" then values.max
|
|
1406
|
+
else
|
|
1407
|
+
raise ExecutionError, "Unsupported window function: #{expression.name}"
|
|
1408
|
+
end
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def window_frame_rows(rows, index, frame)
|
|
1412
|
+
start_index = window_frame_index(frame[:start], index, rows.size, start: true)
|
|
1413
|
+
finish = frame[:finish] || {kind: :current_row}
|
|
1414
|
+
end_index = window_frame_index(finish, index, rows.size, start: false)
|
|
1415
|
+
return [] if start_index > end_index
|
|
1416
|
+
|
|
1417
|
+
rows[start_index..end_index] || []
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
def window_frame_index(boundary, index, size, start:)
|
|
1421
|
+
case boundary[:kind]
|
|
1422
|
+
when :unbounded_preceding then 0
|
|
1423
|
+
when :unbounded_following then size - 1
|
|
1424
|
+
when :current_row then index
|
|
1425
|
+
when :preceding then [index - boundary[:value], 0].max
|
|
1426
|
+
when :following then [index + boundary[:value], size - 1].min
|
|
1427
|
+
else raise ExecutionError, "Unsupported window frame boundary: #{boundary[:kind]}"
|
|
1428
|
+
end
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
def apply_aggregate_function(expression, rows)
|
|
1432
|
+
argument = expression.arguments.first
|
|
1433
|
+
values = if argument.is_a?(SQL::AST::Star)
|
|
1434
|
+
rows
|
|
1435
|
+
else
|
|
1436
|
+
rows.map { |row| evaluate_expression(argument, row) }.compact
|
|
1437
|
+
end
|
|
1438
|
+
values = values.uniq if expression.distinct
|
|
1439
|
+
|
|
1440
|
+
case expression.name.to_s.upcase
|
|
1441
|
+
when "COUNT" then values.size
|
|
1442
|
+
when "SUM" then values.empty? ? nil : values.sum
|
|
1443
|
+
when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
|
|
1444
|
+
when "MIN" then values.min
|
|
1445
|
+
when "MAX" then values.max
|
|
1446
|
+
end
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
def apply_aggregate(agg, rows)
|
|
1450
|
+
values = rows.map { |row| row[agg.column.to_s] }.compact
|
|
1451
|
+
|
|
1452
|
+
case agg.name.to_s.upcase
|
|
1453
|
+
when "COUNT" then values.size
|
|
1454
|
+
when "SUM" then values.sum
|
|
1455
|
+
when "AVG" then values.empty? ? 0 : values.sum / values.size.to_f
|
|
1456
|
+
when "MIN" then values.min
|
|
1457
|
+
when "MAX" then values.max
|
|
1458
|
+
end
|
|
1459
|
+
end
|
|
1460
|
+
|
|
1461
|
+
def sort_rows(rows, order_by)
|
|
1462
|
+
rows.sort do |a, b|
|
|
1463
|
+
comparison = 0
|
|
1464
|
+
order_by.each do |order|
|
|
1465
|
+
col = (order.is_a?(Hash) ? order[:column] : order.column).to_s
|
|
1466
|
+
val_a = order_value(a, col)
|
|
1467
|
+
val_b = order_value(b, col)
|
|
1468
|
+
|
|
1469
|
+
comparison = if val_a.nil? && val_b.nil?
|
|
1470
|
+
0
|
|
1471
|
+
elsif val_a.nil?
|
|
1472
|
+
-1
|
|
1473
|
+
elsif val_b.nil?
|
|
1474
|
+
1
|
|
1475
|
+
else
|
|
1476
|
+
val_a <=> val_b
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1479
|
+
direction = order.is_a?(Hash) ? order[:direction] : order.direction
|
|
1480
|
+
comparison = -comparison if direction.to_s.casecmp?("desc")
|
|
1481
|
+
break unless comparison == 0
|
|
1482
|
+
end
|
|
1483
|
+
comparison
|
|
1484
|
+
end
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1487
|
+
def order_value(row, column)
|
|
1488
|
+
return row[column] if row.key?(column)
|
|
1489
|
+
return row[column.to_sym] if row.key?(column.to_sym)
|
|
1490
|
+
|
|
1491
|
+
short_column = column.split(".").last
|
|
1492
|
+
return row[short_column] if row.key?(short_column)
|
|
1493
|
+
return row[short_column.to_sym] if row.key?(short_column.to_sym)
|
|
1494
|
+
|
|
1495
|
+
nil
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1498
|
+
def get_column_names(plan)
|
|
1499
|
+
if plan.projections
|
|
1500
|
+
plan.projections.map { |projection| projection_name(projection) }
|
|
1501
|
+
else
|
|
1502
|
+
@engine.table_columns(plan.table_name).map(&:name)
|
|
1503
|
+
end
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
def projection_name(projection)
|
|
1507
|
+
return projection.to_s if projection.is_a?(String) || projection.is_a?(Symbol)
|
|
1508
|
+
|
|
1509
|
+
expression = projection.respond_to?(:expression) ? projection.expression : projection
|
|
1510
|
+
alias_name = if projection.respond_to?(:alias_name)
|
|
1511
|
+
projection.alias_name
|
|
1512
|
+
elsif projection.respond_to?(:alias)
|
|
1513
|
+
projection.alias
|
|
1514
|
+
end
|
|
1515
|
+
return alias_name.to_s if alias_name
|
|
1516
|
+
|
|
1517
|
+
return expression.name.to_s if expression.respond_to?(:name) && expression.name
|
|
1518
|
+
return expression.to_sql if expression.respond_to?(:to_sql)
|
|
1519
|
+
|
|
1520
|
+
expression.to_s
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1523
|
+
def format_plan(plan)
|
|
1524
|
+
lines = []
|
|
1525
|
+
lines << "QUERY PLAN"
|
|
1526
|
+
lines << "=" * 40
|
|
1527
|
+
|
|
1528
|
+
lines << if plan.scan_type == :index
|
|
1529
|
+
"Index Scan on #{plan.table_name} using #{plan.index.name}"
|
|
1530
|
+
else
|
|
1531
|
+
"Seq Scan on #{plan.table_name}"
|
|
1532
|
+
end
|
|
1533
|
+
|
|
1534
|
+
if plan.predicate
|
|
1535
|
+
lines << " Filter: #{plan.predicate}"
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
if plan.order_by&.any?
|
|
1539
|
+
order_str = plan.order_by.map { |o| "#{o.column} #{o.direction}" }.join(", ")
|
|
1540
|
+
lines << " Order By: #{order_str}"
|
|
1541
|
+
end
|
|
1542
|
+
|
|
1543
|
+
if plan.limit
|
|
1544
|
+
lines << " Limit: #{plan.limit}"
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
if plan.offset
|
|
1548
|
+
lines << " Offset: #{plan.offset}"
|
|
1549
|
+
end
|
|
1550
|
+
|
|
1551
|
+
lines << " Estimated Cost: #{plan.estimated_cost}"
|
|
1552
|
+
lines << " Estimated Rows: #{plan.estimated_rows}"
|
|
1553
|
+
lines.join("\n")
|
|
1554
|
+
end
|
|
1555
|
+
end
|
|
1556
|
+
end
|
|
1557
|
+
end
|