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.
Files changed (469) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
  3. data/.github/workflows/benchmark.yml +26 -26
  4. data/.github/workflows/compatibility.yml +63 -63
  5. data/.github/workflows/fuzz.yml +33 -33
  6. data/.github/workflows/lint.yml +21 -21
  7. data/.github/workflows/operations.yml +24 -24
  8. data/.github/workflows/production-validation.yml +111 -111
  9. data/.github/workflows/release.yml +77 -77
  10. data/.github/workflows/security.yml +39 -37
  11. data/.github/workflows/test.yml +26 -26
  12. data/.github/workflows/workload.yml +58 -58
  13. data/.gitignore +16 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +57 -27
  18. data/CODE_OF_CONDUCT.md +13 -13
  19. data/CONTRIBUTING.md +29 -29
  20. data/GOVERNANCE.md +16 -16
  21. data/Gemfile +18 -17
  22. data/Gemfile.lock +125 -71
  23. data/README.md +168 -12
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +76 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/accelerator/bin/SHA256SUMS +6 -0
  29. data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
  30. data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
  31. data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
  32. data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
  33. data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
  34. data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
  35. data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
  36. data/accelerator/go.mod +3 -0
  37. data/accelerator/internal/execution/aggregate.go +94 -0
  38. data/accelerator/internal/execution/distinct.go +22 -0
  39. data/accelerator/internal/execution/filter.go +73 -0
  40. data/accelerator/internal/execution/join.go +79 -0
  41. data/accelerator/internal/execution/operators.go +167 -0
  42. data/accelerator/internal/execution/scan.go +20 -0
  43. data/accelerator/internal/execution/sort.go +62 -0
  44. data/accelerator/internal/execution/types.go +136 -0
  45. data/accelerator/internal/execution/value.go +67 -0
  46. data/accelerator/internal/memory/arena.go +47 -0
  47. data/accelerator/internal/memory/reuse.go +22 -0
  48. data/accelerator/internal/metrics/registry.go +67 -0
  49. data/accelerator/internal/parallel/bounded_queue.go +56 -0
  50. data/accelerator/internal/parallel/scheduler.go +47 -0
  51. data/accelerator/internal/parallel/worker_pool.go +53 -0
  52. data/accelerator/internal/protocol/cancellation.go +48 -0
  53. data/accelerator/internal/protocol/columnar.go +263 -0
  54. data/accelerator/internal/protocol/frame.go +187 -0
  55. data/accelerator/internal/runtime/worker.go +521 -0
  56. data/accelerator/internal/storage/page_reader.go +81 -0
  57. data/accelerator/internal/storage/snapshot_scan.go +539 -0
  58. data/accelerator/internal/wal/checksum.go +13 -0
  59. data/accelerator/internal/wal/compression.go +41 -0
  60. data/accelerator/internal/wal/group_commit.go +24 -0
  61. data/accelerator/internal/wal/record_encoder.go +40 -0
  62. data/adapters/activerecord/Gemfile +11 -11
  63. data/adapters/activerecord/README.md +8 -3
  64. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +881 -879
  65. data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
  66. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
  67. data/adapters/ruby/README.md +18 -18
  68. data/adapters/sequel/README.md +11 -11
  69. data/config/monitoring/prometheus-alerts.yml +39 -39
  70. data/config/production.yml +36 -36
  71. data/docs/README.md +77 -71
  72. data/docs/architecture/concurrency.md +14 -14
  73. data/docs/architecture/current-state.md +125 -125
  74. data/docs/architecture/execution-engine.md +25 -25
  75. data/docs/architecture/go-accelerator.md +179 -0
  76. data/docs/architecture/indexes.md +19 -19
  77. data/docs/architecture/mvcc.md +19 -19
  78. data/docs/architecture/overview.md +13 -13
  79. data/docs/architecture/pages.md +11 -11
  80. data/docs/architecture/production-roadmap.md +82 -82
  81. data/docs/architecture/query-planner.md +20 -20
  82. data/docs/architecture/recovery.md +18 -18
  83. data/docs/architecture/sql-engine.md +12 -12
  84. data/docs/architecture/storage-engine.md +14 -14
  85. data/docs/architecture/transactions.md +10 -10
  86. data/docs/architecture/wal.md +28 -28
  87. data/docs/cli-cheatsheet.md +98 -98
  88. data/docs/cli.md +299 -275
  89. data/docs/contributing/architecture.md +9 -9
  90. data/docs/contributing/benchmarking.md +30 -14
  91. data/docs/contributing/development.md +16 -16
  92. data/docs/contributing/release-process.md +49 -49
  93. data/docs/contributing/testing.md +16 -16
  94. data/docs/debugging.md +229 -229
  95. data/docs/developer/branching.md +10 -10
  96. data/docs/developer/database-diff.md +10 -10
  97. data/docs/developer/local-development.md +49 -17
  98. data/docs/developer/snapshots.md +9 -9
  99. data/docs/developer/temporal-data.md +10 -10
  100. data/docs/developer-guide.md +297 -297
  101. data/docs/getting-started/first-database.md +16 -16
  102. data/docs/getting-started/first-query.md +13 -13
  103. data/docs/getting-started/installation.md +19 -19
  104. data/docs/getting-started/local-to-production.md +300 -300
  105. data/docs/getting-started/quickstart.md +17 -17
  106. data/docs/getting-started/rails.md +16 -16
  107. data/docs/hardening_backlog.md +93 -93
  108. data/docs/lessons-learned.md +112 -112
  109. data/docs/operations/backups.md +33 -33
  110. data/docs/operations/disaster-recovery.md +31 -31
  111. data/docs/operations/failover.md +30 -30
  112. data/docs/operations/monitoring.md +25 -25
  113. data/docs/operations/production-guide.md +295 -295
  114. data/docs/operations/production-runbook.md +45 -45
  115. data/docs/operations/replication.md +33 -33
  116. data/docs/operations/restore.md +6 -6
  117. data/docs/operations/runbook.md +34 -34
  118. data/docs/operations/upgrades.md +14 -14
  119. data/docs/operations/workload-testing.md +17 -17
  120. data/docs/production-readiness.md +118 -118
  121. data/docs/production_validation.md +150 -150
  122. data/docs/rails/active-record.md +11 -11
  123. data/docs/rails/compatibility-guide.md +90 -90
  124. data/docs/rails/database-yml.md +92 -92
  125. data/docs/rails/installation.md +17 -17
  126. data/docs/rails/migrations.md +17 -17
  127. data/docs/rails/production.md +82 -82
  128. data/docs/rails/troubleshooting.md +18 -18
  129. data/docs/release.md +25 -25
  130. data/docs/server/architecture.md +10 -10
  131. data/docs/server/authentication.md +10 -10
  132. data/docs/server/configuration.md +16 -16
  133. data/docs/server/connection-pooling.md +10 -10
  134. data/docs/server/deployment.md +10 -10
  135. data/docs/server/protocol.md +12 -12
  136. data/docs/sql/compatibility-guide.md +82 -82
  137. data/docs/sql/compatibility.md +39 -39
  138. data/docs/sql/data-types.md +10 -10
  139. data/docs/sql/functions.md +9 -9
  140. data/docs/sql/joins.md +9 -9
  141. data/docs/sql/operators.md +9 -9
  142. data/docs/sql/sqlite-compatibility.md +21 -21
  143. data/docs/sql/syntax.md +10 -10
  144. data/docs/sql/transactions.md +10 -10
  145. data/docs/troubleshooting.md +244 -244
  146. data/lessons/01-foundations.md +73 -0
  147. data/lessons/02-local-development.md +121 -0
  148. data/lessons/03-embedded-rubydb.md +99 -0
  149. data/lessons/04-rails-complex-apps.md +138 -0
  150. data/lessons/05-rubydb-production-server.md +237 -0
  151. data/lessons/06-postgresql-massive-apps.md +96 -0
  152. data/lessons/07-hybrid-microservices.md +179 -0
  153. data/lessons/08-migrations-backups-recovery.md +86 -0
  154. data/lessons/09-observability-security-scale.md +87 -0
  155. data/lessons/10-release-readiness.md +192 -0
  156. data/lessons/11-community-adapter.md +323 -0
  157. data/lessons/12-rails-ecommerce-pressure.md +263 -0
  158. data/lib/rubydb/accelerator/client.rb +451 -0
  159. data/lib/rubydb/accelerator/error.rb +22 -0
  160. data/lib/rubydb/accelerator/manager.rb +606 -0
  161. data/lib/rubydb/accelerator.rb +13 -0
  162. data/lib/rubydb/backup/archive.rb +332 -334
  163. data/lib/rubydb/backup/backup.rb +400 -401
  164. data/lib/rubydb/backup/incremental.rb +349 -353
  165. data/lib/rubydb/backup/restore.rb +289 -290
  166. data/lib/rubydb/backup/snapshot.rb +265 -267
  167. data/lib/rubydb/backup/verification.rb +276 -279
  168. data/lib/rubydb/branching/branch.rb +181 -181
  169. data/lib/rubydb/branching/branch_manager.rb +307 -311
  170. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  171. data/lib/rubydb/branching/checkout.rb +165 -166
  172. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  173. data/lib/rubydb/branching/diff.rb +137 -138
  174. data/lib/rubydb/branching/merge.rb +282 -285
  175. data/lib/rubydb/build_info.rb +15 -15
  176. data/lib/rubydb/catalog/catalog.rb +391 -391
  177. data/lib/rubydb/catalog/column.rb +112 -112
  178. data/lib/rubydb/catalog/constraint.rb +180 -180
  179. data/lib/rubydb/catalog/database.rb +184 -184
  180. data/lib/rubydb/catalog/index.rb +97 -97
  181. data/lib/rubydb/catalog/schema.rb +103 -103
  182. data/lib/rubydb/catalog/sequence.rb +90 -90
  183. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  184. data/lib/rubydb/catalog/table.rb +178 -178
  185. data/lib/rubydb/catalog/trigger.rb +102 -102
  186. data/lib/rubydb/catalog/view.rb +66 -66
  187. data/lib/rubydb/cli/application.rb +168 -163
  188. data/lib/rubydb/cli/commands/accelerator.rb +72 -0
  189. data/lib/rubydb/cli/commands/backup.rb +80 -81
  190. data/lib/rubydb/cli/commands/branch.rb +72 -72
  191. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  192. data/lib/rubydb/cli/commands/create.rb +58 -58
  193. data/lib/rubydb/cli/commands/diff.rb +76 -77
  194. data/lib/rubydb/cli/commands/doctor.rb +77 -74
  195. data/lib/rubydb/cli/commands/drop.rb +57 -57
  196. data/lib/rubydb/cli/commands/init.rb +101 -102
  197. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  198. data/lib/rubydb/cli/commands/merge.rb +63 -63
  199. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  200. data/lib/rubydb/cli/commands/restart.rb +42 -39
  201. data/lib/rubydb/cli/commands/restore.rb +121 -121
  202. data/lib/rubydb/cli/commands/shell.rb +365 -365
  203. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  204. data/lib/rubydb/cli/commands/start.rb +88 -82
  205. data/lib/rubydb/cli/commands/status.rb +96 -92
  206. data/lib/rubydb/cli/commands/stop.rb +47 -47
  207. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  208. data/lib/rubydb/cli/formatter.rb +221 -221
  209. data/lib/rubydb/cli/output.rb +168 -168
  210. data/lib/rubydb/client/client.rb +309 -304
  211. data/lib/rubydb/client/connection.rb +429 -415
  212. data/lib/rubydb/client/connection_pool.rb +168 -168
  213. data/lib/rubydb/client/connection_url.rb +96 -96
  214. data/lib/rubydb/client/prepared_statement.rb +60 -60
  215. data/lib/rubydb/client/result.rb +127 -123
  216. data/lib/rubydb/client/statement.rb +52 -52
  217. data/lib/rubydb/client/transaction.rb +130 -130
  218. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  219. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  220. data/lib/rubydb/concurrency/latch.rb +101 -101
  221. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  222. data/lib/rubydb/concurrency/mutex.rb +181 -183
  223. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  224. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  225. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  226. data/lib/rubydb/configuration/config.rb +170 -170
  227. data/lib/rubydb/configuration/defaults.rb +191 -179
  228. data/lib/rubydb/configuration/environment.rb +152 -152
  229. data/lib/rubydb/configuration/parser.rb +185 -185
  230. data/lib/rubydb/configuration/validation.rb +228 -221
  231. data/lib/rubydb/constants.rb +74 -74
  232. data/lib/rubydb/constraints/check.rb +181 -181
  233. data/lib/rubydb/constraints/constraint.rb +101 -101
  234. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  235. data/lib/rubydb/constraints/not_null.rb +64 -64
  236. data/lib/rubydb/constraints/primary_key.rb +99 -99
  237. data/lib/rubydb/constraints/unique.rb +106 -108
  238. data/lib/rubydb/constraints/validator.rb +349 -350
  239. data/lib/rubydb/errors/authentication_error.rb +10 -10
  240. data/lib/rubydb/errors/authorization_error.rb +23 -23
  241. data/lib/rubydb/errors/client_error.rb +10 -10
  242. data/lib/rubydb/errors/configuration_error.rb +10 -10
  243. data/lib/rubydb/errors/connection_error.rb +10 -10
  244. data/lib/rubydb/errors/constraint_error.rb +23 -23
  245. data/lib/rubydb/errors/corruption_error.rb +10 -10
  246. data/lib/rubydb/errors/database_error.rb +10 -10
  247. data/lib/rubydb/errors/error.rb +20 -20
  248. data/lib/rubydb/errors/execution_error.rb +10 -10
  249. data/lib/rubydb/errors/parser_error.rb +10 -10
  250. data/lib/rubydb/errors/recovery_error.rb +10 -10
  251. data/lib/rubydb/errors/replication_error.rb +10 -10
  252. data/lib/rubydb/errors/server_error.rb +6 -6
  253. data/lib/rubydb/errors/storage_error.rb +10 -10
  254. data/lib/rubydb/errors/transaction_error.rb +10 -10
  255. data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
  256. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  257. data/lib/rubydb/execution/cost_model.rb +72 -0
  258. data/lib/rubydb/execution/delete_executor.rb +110 -112
  259. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  260. data/lib/rubydb/execution/executor.rb +1544 -1188
  261. data/lib/rubydb/execution/expression.rb +191 -193
  262. data/lib/rubydb/execution/index_scan.rb +142 -142
  263. data/lib/rubydb/execution/insert_executor.rb +215 -217
  264. data/lib/rubydb/execution/join_executor.rb +243 -249
  265. data/lib/rubydb/execution/limit_executor.rb +83 -85
  266. data/lib/rubydb/execution/operator_selection.rb +57 -0
  267. data/lib/rubydb/execution/optimizer.rb +227 -215
  268. data/lib/rubydb/execution/physical_plan.rb +47 -0
  269. data/lib/rubydb/execution/plan.rb +355 -353
  270. data/lib/rubydb/execution/planner.rb +508 -536
  271. data/lib/rubydb/execution/predicate.rb +235 -235
  272. data/lib/rubydb/execution/scan.rb +49 -49
  273. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  274. data/lib/rubydb/execution/sort_executor.rb +194 -185
  275. data/lib/rubydb/execution/update_executor.rb +160 -162
  276. data/lib/rubydb/functions/aggregate.rb +70 -70
  277. data/lib/rubydb/functions/date_functions.rb +274 -278
  278. data/lib/rubydb/functions/function.rb +85 -85
  279. data/lib/rubydb/functions/json_functions.rb +231 -215
  280. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  281. data/lib/rubydb/functions/scalar.rb +52 -52
  282. data/lib/rubydb/functions/string_functions.rb +383 -383
  283. data/lib/rubydb/functions/system_functions.rb +258 -246
  284. data/lib/rubydb/history/as_of.rb +238 -238
  285. data/lib/rubydb/history/change.rb +105 -105
  286. data/lib/rubydb/history/history.rb +131 -131
  287. data/lib/rubydb/history/history_manager.rb +228 -229
  288. data/lib/rubydb/history/temporal_query.rb +202 -202
  289. data/lib/rubydb/history/timeline.rb +144 -144
  290. data/lib/rubydb/indexes/btree.rb +215 -186
  291. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  292. data/lib/rubydb/indexes/btree_node.rb +384 -385
  293. data/lib/rubydb/indexes/hash_index.rb +150 -150
  294. data/lib/rubydb/indexes/index.rb +71 -71
  295. data/lib/rubydb/indexes/index_manager.rb +408 -406
  296. data/lib/rubydb/indexes/index_scan.rb +466 -470
  297. data/lib/rubydb/migrations/migration.rb +253 -254
  298. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  299. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  300. data/lib/rubydb/migrations/migration_version.rb +71 -71
  301. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  302. data/lib/rubydb/migrations/schema_version.rb +64 -64
  303. data/lib/rubydb/monitoring/events.rb +155 -160
  304. data/lib/rubydb/monitoring/health.rb +216 -222
  305. data/lib/rubydb/monitoring/logger.rb +188 -193
  306. data/lib/rubydb/monitoring/metrics.rb +363 -359
  307. data/lib/rubydb/monitoring/performance.rb +176 -176
  308. data/lib/rubydb/monitoring/statistics.rb +168 -170
  309. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  310. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  311. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  312. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  313. data/lib/rubydb/mvcc/version.rb +106 -106
  314. data/lib/rubydb/mvcc/version_store.rb +396 -398
  315. data/lib/rubydb/mvcc/visibility.rb +107 -109
  316. data/lib/rubydb/protocol/capabilities.rb +125 -125
  317. data/lib/rubydb/protocol/decoder.rb +142 -145
  318. data/lib/rubydb/protocol/encoder.rb +131 -136
  319. data/lib/rubydb/protocol/handshake.rb +306 -305
  320. data/lib/rubydb/protocol/message.rb +121 -121
  321. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  322. data/lib/rubydb/protocol/protocol.rb +276 -277
  323. data/lib/rubydb/protocol/version.rb +54 -54
  324. data/lib/rubydb/rails/adapter.rb +245 -239
  325. data/lib/rubydb/rails/connection.rb +312 -314
  326. data/lib/rubydb/rails/database_statements.rb +122 -122
  327. data/lib/rubydb/rails/migration.rb +131 -131
  328. data/lib/rubydb/rails/quoting.rb +109 -109
  329. data/lib/rubydb/rails/result.rb +117 -117
  330. data/lib/rubydb/rails/schema_statements.rb +339 -339
  331. data/lib/rubydb/rails/transaction.rb +105 -105
  332. data/lib/rubydb/rails/type.rb +126 -126
  333. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  334. data/lib/rubydb/recovery/consistency.rb +457 -467
  335. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  336. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  337. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  338. data/lib/rubydb/recovery/redo.rb +235 -237
  339. data/lib/rubydb/recovery/undo.rb +204 -206
  340. data/lib/rubydb/replication/failover.rb +5 -5
  341. data/lib/rubydb/replication/fencing.rb +63 -63
  342. data/lib/rubydb/replication/primary.rb +461 -450
  343. data/lib/rubydb/replication/replica.rb +382 -384
  344. data/lib/rubydb/replication/replication_log.rb +194 -200
  345. data/lib/rubydb/replication/replication_manager.rb +307 -308
  346. data/lib/rubydb/replication/replication_slot.rb +293 -295
  347. data/lib/rubydb/replication/replication_stream.rb +198 -201
  348. data/lib/rubydb/rubydb.rb +570 -560
  349. data/lib/rubydb/security/access_control.rb +252 -254
  350. data/lib/rubydb/security/audit_log.rb +209 -213
  351. data/lib/rubydb/security/authentication.rb +302 -302
  352. data/lib/rubydb/security/authorization.rb +282 -282
  353. data/lib/rubydb/security/credentials.rb +192 -196
  354. data/lib/rubydb/security/password.rb +205 -215
  355. data/lib/rubydb/security/permissions.rb +74 -74
  356. data/lib/rubydb/security/role.rb +99 -101
  357. data/lib/rubydb/security/user.rb +86 -86
  358. data/lib/rubydb/server/connection.rb +383 -366
  359. data/lib/rubydb/server/connection_pool.rb +193 -193
  360. data/lib/rubydb/server/lifecycle.rb +227 -228
  361. data/lib/rubydb/server/listener.rb +139 -136
  362. data/lib/rubydb/server/request_handler.rb +277 -276
  363. data/lib/rubydb/server/server.rb +363 -364
  364. data/lib/rubydb/server/session.rb +416 -369
  365. data/lib/rubydb/server/worker.rb +206 -210
  366. data/lib/rubydb/server/worker_pool.rb +168 -168
  367. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  368. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  369. data/lib/rubydb/sql/ast/commit.rb +37 -37
  370. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  371. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  372. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  373. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  374. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  375. data/lib/rubydb/sql/ast/delete.rb +54 -54
  376. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  377. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  378. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  379. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  380. data/lib/rubydb/sql/ast/explain.rb +64 -64
  381. data/lib/rubydb/sql/ast/expression.rb +617 -604
  382. data/lib/rubydb/sql/ast/insert.rb +66 -66
  383. data/lib/rubydb/sql/ast/node.rb +42 -42
  384. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  385. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  386. data/lib/rubydb/sql/ast/select.rb +88 -88
  387. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  388. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  389. data/lib/rubydb/sql/ast/update.rb +88 -88
  390. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  391. data/lib/rubydb/sql/ast/view.rb +38 -32
  392. data/lib/rubydb/sql/ast/with.rb +32 -32
  393. data/lib/rubydb/sql/grammar.rb +86 -86
  394. data/lib/rubydb/sql/keywords.rb +156 -156
  395. data/lib/rubydb/sql/lexer.rb +209 -214
  396. data/lib/rubydb/sql/operators.rb +100 -100
  397. data/lib/rubydb/sql/parser.rb +1167 -1170
  398. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  399. data/lib/rubydb/sql/planner/binder.rb +537 -550
  400. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  401. data/lib/rubydb/sql/token.rb +210 -210
  402. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  403. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  404. data/lib/rubydb/storage/database_lock.rb +74 -74
  405. data/lib/rubydb/storage/deserializer.rb +332 -342
  406. data/lib/rubydb/storage/engine.rb +2409 -2330
  407. data/lib/rubydb/storage/file_manager.rb +191 -187
  408. data/lib/rubydb/storage/free_space_map.rb +79 -81
  409. data/lib/rubydb/storage/page.rb +92 -94
  410. data/lib/rubydb/storage/page_allocator.rb +852 -855
  411. data/lib/rubydb/storage/page_header.rb +63 -67
  412. data/lib/rubydb/storage/page_manager.rb +127 -131
  413. data/lib/rubydb/storage/record.rb +58 -58
  414. data/lib/rubydb/storage/row.rb +78 -78
  415. data/lib/rubydb/storage/serializer.rb +51 -51
  416. data/lib/rubydb/storage/snapshot_reader.rb +167 -0
  417. data/lib/rubydb/storage/storage_layout.rb +151 -151
  418. data/lib/rubydb/storage/storage_manager.rb +114 -114
  419. data/lib/rubydb/storage/tuple.rb +458 -461
  420. data/lib/rubydb/storage/visibility_map.rb +964 -973
  421. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  422. data/lib/rubydb/transactions/isolation.rb +98 -98
  423. data/lib/rubydb/transactions/lock.rb +76 -76
  424. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  425. data/lib/rubydb/transactions/savepoint.rb +142 -143
  426. data/lib/rubydb/transactions/transaction.rb +214 -215
  427. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  428. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  429. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  430. data/lib/rubydb/types/bigint.rb +36 -36
  431. data/lib/rubydb/types/blob.rb +37 -37
  432. data/lib/rubydb/types/boolean.rb +34 -34
  433. data/lib/rubydb/types/date.rb +39 -39
  434. data/lib/rubydb/types/decimal.rb +48 -48
  435. data/lib/rubydb/types/float.rb +34 -34
  436. data/lib/rubydb/types/integer.rb +36 -36
  437. data/lib/rubydb/types/json.rb +41 -41
  438. data/lib/rubydb/types/null.rb +34 -34
  439. data/lib/rubydb/types/smallint.rb +36 -36
  440. data/lib/rubydb/types/text.rb +37 -37
  441. data/lib/rubydb/types/time.rb +46 -46
  442. data/lib/rubydb/types/timestamp.rb +39 -39
  443. data/lib/rubydb/types/type.rb +119 -119
  444. data/lib/rubydb/types/uuid.rb +47 -47
  445. data/lib/rubydb/types/varchar.rb +37 -37
  446. data/lib/rubydb/version.rb +32 -32
  447. data/lib/rubydb/wal/archive.rb +207 -193
  448. data/lib/rubydb/wal/checkpoint.rb +181 -183
  449. data/lib/rubydb/wal/lsn.rb +94 -94
  450. data/lib/rubydb/wal/reader.rb +259 -260
  451. data/lib/rubydb/wal/record.rb +105 -105
  452. data/lib/rubydb/wal/segment.rb +193 -193
  453. data/lib/rubydb/wal/wal.rb +481 -452
  454. data/lib/rubydb/wal/writer.rb +236 -236
  455. data/lib/rubydb.rb +7 -7
  456. data/packaging/docker/docker-compose.failover.yml +43 -43
  457. data/packaging/homebrew/rubydb.rb +19 -19
  458. data/rubydb.gemspec +70 -57
  459. data/scripts/benchmark +7 -7
  460. data/scripts/build_accelerator +49 -0
  461. data/scripts/durability_drill +37 -37
  462. data/scripts/fuzz +63 -63
  463. data/scripts/release +77 -42
  464. data/scripts/release_check +43 -43
  465. data/scripts/replication_failover_drill +268 -250
  466. data/scripts/replication_network_failover_drill +287 -255
  467. data/scripts/restore_drill +45 -45
  468. data/scripts/security +45 -0
  469. metadata +102 -1
@@ -1,1201 +1,1557 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
- require "time"
5
-
6
- module RubyDB
7
- module Execution
8
- # Executor - Executes query plans and returns results
9
- class Executor
10
- attr_reader :engine, :stats
11
-
12
- def initialize(engine, cte_rows: {}, deadline_at: nil, cancellation: nil)
13
- @engine = engine
14
- @cte_rows = cte_rows
15
- @deadline_at = deadline_at && (deadline_at.is_a?(Time) ? deadline_at : Time.parse(deadline_at.to_s))
16
- @cancellation = cancellation
17
- @stats = {
18
- queries_executed: 0,
19
- rows_returned: 0,
20
- total_time_ms: 0,
21
- sequential_scans: 0,
22
- index_scans: 0,
23
- joins: 0,
24
- aggregations: 0,
25
- sorts: 0
26
- }
27
- @lock = Mutex.new
28
- @current_transaction = nil
29
- @explain_mode = false
30
- end
31
-
32
- def execute(plan, transaction_id = nil)
33
- @lock.synchronize do
34
- check_deadline!
35
- start_time = Time.now
36
- @stats[:queries_executed] += 1
37
- @current_transaction = transaction_id
38
-
39
- result = case plan
40
- when Plan::With
41
- execute_with(plan)
42
- when Plan::Select
43
- execute_select(plan)
44
- when Plan::SetOperation
45
- execute_set_operation(plan)
46
- when Plan::Insert
47
- execute_insert(plan)
48
- when Plan::Update
49
- execute_update(plan)
50
- when Plan::Delete
51
- execute_delete(plan)
52
- when Plan::CreateTable
53
- execute_create_table(plan)
54
- when Plan::DropTable
55
- execute_drop_table(plan)
56
- when Plan::CreateDatabase
57
- execute_create_database(plan)
58
- when Plan::DropDatabase
59
- execute_drop_database(plan)
60
- when Plan::CreateSchema
61
- execute_create_schema(plan)
62
- when Plan::DropSchema
63
- execute_drop_schema(plan)
64
- when Plan::CreateView
65
- execute_create_view(plan)
66
- when Plan::DropView
67
- execute_drop_view(plan)
68
- when Plan::CreateTrigger
69
- execute_create_trigger(plan)
70
- when Plan::DropTrigger
71
- execute_drop_trigger(plan)
72
- when Plan::Vacuum
73
- execute_vacuum(plan)
74
- when Plan::AlterTableAddColumn
75
- execute_alter_table_add_column(plan)
76
- when Plan::AlterTableDropColumn
77
- execute_alter_table_drop_column(plan)
78
- when Plan::AlterTableAddConstraint
79
- execute_alter_table_add_constraint(plan)
80
- when Plan::AlterTableDropConstraint
81
- execute_alter_table_drop_constraint(plan)
82
- when Plan::CreateIndex
83
- execute_create_index(plan)
84
- when Plan::DropIndex
85
- execute_drop_index(plan)
86
- when Plan::BeginTransaction
87
- execute_begin_transaction(plan)
88
- when Plan::Commit
89
- execute_commit(plan)
90
- when Plan::Rollback
91
- execute_rollback(plan)
92
- when Plan::Savepoint
93
- execute_savepoint(plan)
94
- when Plan::RollbackToSavepoint
95
- execute_rollback_to_savepoint(plan)
96
- when Plan::ReleaseSavepoint
97
- execute_release_savepoint(plan)
98
- when Plan::Explain
99
- execute_explain(plan)
100
- else
101
- raise ExecutionError, "Unknown plan type: #{plan.class}"
102
- end
103
-
104
- elapsed_ms = ((Time.now - start_time) * 1000).round(2)
105
- @stats[:total_time_ms] += elapsed_ms
106
- @stats[:rows_returned] += result[:row_count] if result && result[:row_count]
107
-
108
- result
109
- end
110
- end
111
-
112
- def execute_set_operation(plan)
113
- left = child_executor(@cte_rows).execute(plan.left_plan, @current_transaction)[:rows]
114
- right = child_executor(@cte_rows).execute(plan.right_plan, @current_transaction)[:rows]
115
- left_width = left.first&.size || plan.left_plan.columns.size
116
- right_width = right.first&.size || plan.right_plan.columns.size
117
- if left_width != right_width
118
- raise ExecutionError, "Set operation requires the same number of columns on both sides"
119
- end
120
- key = ->(row) { row.to_a }
121
- rows = case plan.operator
122
- when :union then plan.all ? left + right : (left + right).uniq { |row| key.call(row) }
123
- when :intersect then left.select { |row| right.any? { |other| key.call(other) == key.call(row) } }.uniq { |row| key.call(row) }
124
- when :except then left.reject { |row| right.any? { |other| key.call(other) == key.call(row) } }.uniq { |row| key.call(row) }
125
- else raise ExecutionError, "Unsupported set operation: #{plan.operator}"
126
- end
127
- { rows: rows, row_count: rows.size, column_names: rows.first&.keys || [] }
128
- end
129
-
130
- def execute_with(plan)
131
- available_ctes = @cte_rows.dup
132
- plan.ctes.each do |name, cte_plan|
133
- available_ctes[name.to_s] = if plan.recursive && cte_plan.is_a?(Plan::SetOperation) && cte_plan.operator == :union
134
- execute_recursive_cte(name.to_s, cte_plan, available_ctes)
135
- else
136
- child_executor(available_ctes).execute(cte_plan, @current_transaction)[:rows]
137
- end
138
- end
139
- child_executor(available_ctes).execute(plan.query_plan, @current_transaction)
140
- end
141
-
142
- def execute_recursive_cte(name, cte_plan, available_ctes)
143
- unless cte_plan.is_a?(Plan::SetOperation) && cte_plan.operator == :union
144
- raise ExecutionError, "Recursive CTE requires UNION or UNION ALL"
145
- end
146
-
147
- anchor = child_executor(available_ctes).execute(cte_plan.left_plan, @current_transaction)[:rows]
148
- accumulated = anchor.dup
149
- working = anchor
150
- output_columns = anchor.first&.keys || cte_plan.left_plan.columns.map(&:to_s)
151
- max_iterations = Integer(@engine.instance_variable_get(:@config)[:max_recursive_iterations] || 10_000)
152
- iterations = 0
153
-
154
- while working.any?
155
- iterations += 1
156
- raise ExecutionError, "Recursive CTE exceeded #{max_iterations} iterations" if iterations > max_iterations
157
-
158
- recursive_scope = available_ctes.merge(name => working)
159
- next_rows = child_executor(recursive_scope).execute(cte_plan.right_plan, @current_transaction)[:rows]
160
- next_rows = next_rows.map do |row|
161
- output_columns.zip(row.values).to_h
162
- end
163
- if cte_plan.all
164
- accumulated.concat(next_rows)
165
- working = next_rows
166
- else
167
- existing = accumulated.map { |row| row.to_a }
168
- working = next_rows.reject { |row| existing.include?(row.to_a) }
169
- accumulated.concat(working)
170
- end
171
- end
172
-
173
- accumulated
174
- end
175
-
176
- def execute_select(plan)
177
- @stats[:sequential_scans] += 1 if plan.scan_type == :sequential
178
- @stats[:index_scans] += 1 if plan.scan_type == :index
179
-
180
- # Qualify source rows before evaluating predicates. This preserves
181
- # SQL's table/alias namespace while retaining unqualified keys for
182
- # existing single-table callers.
183
- rows = qualify_rows(scan_table(plan), plan.source_reference || plan.table_name)
184
- plan.joins.each do |join|
185
- check_deadline!
186
- right_rows = qualify_rows(scan_table(Plan::Select.new(join[:table].name, [])), join[:table])
187
- rows = execute_join(rows, right_rows, join)
188
- end
189
-
190
- # Apply filters (WHERE clause)
191
- if plan.predicate
192
- rows = rows.each_with_index.filter_map do |row, index|
193
- check_deadline! if (index & 255).zero?
194
- row if evaluate_predicate(plan.predicate, row)
195
- end
196
- end
197
-
198
- apply_window_functions!(rows, plan.projections) if plan.projections
199
-
200
- # ORDER BY is evaluated against the source rows, so a column used
201
- # solely for ordering remains available even when it is not selected.
202
- aggregating = plan.aggregates && !plan.aggregates.empty?
203
- if plan.order_by && !plan.order_by.empty? && !aggregating
204
- rows = sort_rows(rows, plan.order_by)
205
- end
206
-
207
- if aggregating
208
- rows = aggregate_rows(rows, plan.group_by || [], plan.projections)
209
- rows = rows.select { |row| evaluate_predicate(plan.having, row) } if plan.having
210
- elsif plan.projections
211
- # Apply projections (SELECT columns)
212
- rows = rows.each_with_index.map do |row, index|
213
- check_deadline! if (index & 255).zero?
214
- project_row(row, plan.projections)
215
- end
216
- end
217
-
218
- # Apply DISTINCT
219
- if plan.distinct
220
- rows = distinct_rows(rows)
221
- end
222
-
223
- # Apply GROUP BY
224
- # Apply ORDER BY
225
- if plan.order_by && !plan.order_by.empty? && aggregating
226
- rows = sort_rows(rows, plan.order_by)
227
- end
228
-
229
- # Apply LIMIT and OFFSET
230
- if plan.limit
231
- offset = plan.offset || 0
232
- rows = rows[offset, plan.limit] || []
233
- elsif plan.offset
234
- rows = rows[plan.offset..-1] || []
235
- end
236
-
237
- {
238
- rows: rows,
239
- row_count: rows.size,
240
- column_names: get_column_names(plan)
241
- }
242
- end
243
-
244
- def execute_insert(plan)
245
- table_name = plan.table_name
246
- columns = plan.columns
247
- rows = plan.rows || [plan.values]
248
- implicit_transaction = rows.size > 1 && !@engine.in_transaction?
249
- @engine.begin_transaction if implicit_transaction
250
-
251
- begin
252
- inserted = rows.map { |values| execute_single_insert(plan, table_name, columns, values) }
253
- unless !implicit_transaction || @engine.commit_transaction
254
- raise ExecutionError, "Implicit multi-row INSERT transaction could not commit"
255
- end
256
-
257
- {
258
- row_count: inserted.sum { |result| result[:row_count] },
259
- affected_rows: inserted.sum { |result| result[:affected_rows] },
260
- row_ids: inserted.filter_map { |result| result[:row_id] },
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
- rescue Exception
267
- @engine.rollback_transaction if implicit_transaction && @engine.in_transaction?
268
- raise
269
- end
270
- end
271
-
272
- def execute_single_insert(plan, table_name, columns, values)
273
-
274
- # Build row data
275
- row_data = {}
276
- columns.each_with_index do |col, idx|
277
- row_data[col] = evaluate_expression(values[idx])
278
- end
279
-
280
- # Insert into engine
281
- table_columns = @engine.table_columns(table_name)
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 { row_count: 0, affected_rows: 0, message: "INSERT 0 (conflict ignored)" } if plan.on_conflict == :nothing
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
- { row_count: 1, affected_rows: 1, row_id: row_id, inserted_id: inserted_id, message: "INSERT 0 UPDATE 1" }
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
- { row_count: 0, message: "CREATE DATABASE #{plan.database_name}" }
410
- end
411
-
412
- def execute_drop_database(plan)
413
- @engine.catalog.drop_database(plan.database_name, **(plan.options || {}))
414
- { row_count: 0, message: "DROP DATABASE #{plan.database_name}" }
415
- end
416
-
417
- def execute_create_schema(plan)
418
- @engine.catalog.create_schema(plan.schema_name, **(plan.options || {}))
419
- { row_count: 0, message: "CREATE SCHEMA #{plan.schema_name}" }
420
- end
421
-
422
- def execute_drop_schema(plan)
423
- @engine.catalog.drop_schema(plan.schema_name, **(plan.options || {}))
424
- { row_count: 0, message: "DROP SCHEMA #{plan.schema_name}" }
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
- { row_count: 0, message: "CREATE VIEW #{plan.view_name}" }
431
- end
432
-
433
- def execute_drop_view(plan)
434
- @engine.catalog.drop_view(plan.view_name, **(plan.options || {}))
435
- { row_count: 0, message: "DROP VIEW #{plan.view_name}" }
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
- { row_count: 0, message: "CREATE TRIGGER #{plan.trigger_name}" }
442
- end
443
-
444
- def execute_drop_trigger(plan)
445
- @engine.catalog.drop_trigger(plan.trigger_name, **(plan.options || {}))
446
- { row_count: 0, message: "DROP TRIGGER #{plan.trigger_name}" }
447
- end
448
-
449
- def execute_vacuum(_plan)
450
- result = @engine.vacuum
451
- { row_count: 0, vacuum: result, message: "VACUUM" }
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
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD COLUMN #{plan.column_name}" }
457
- end
458
-
459
- def execute_alter_table_drop_column(plan)
460
- @engine.drop_column(plan.table_name, plan.column_name)
461
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP COLUMN #{plan.column_name}" }
462
- end
463
-
464
- def execute_alter_table_add_constraint(plan)
465
- @engine.add_constraint(plan.table_name, plan.constraint)
466
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD CONSTRAINT #{plan.constraint.name}" }
467
- end
468
-
469
- def execute_alter_table_drop_constraint(plan)
470
- @engine.drop_constraint(plan.table_name, plan.constraint_name)
471
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP CONSTRAINT #{plan.constraint_name}" }
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
- { row_count: 0, message: "SAVEPOINT #{plan.name}" }
536
- end
537
-
538
- def execute_rollback_to_savepoint(plan)
539
- @engine.rollback_to_savepoint(plan.name)
540
- { row_count: 0, message: "ROLLBACK TO SAVEPOINT #{plan.name}" }
541
- end
542
-
543
- def execute_release_savepoint(plan)
544
- @engine.release_savepoint(plan.name)
545
- { row_count: 0, message: "RELEASE SAVEPOINT #{plan.name}" }
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
- "Execution Time: #{elapsed_ms}ms\n" +
567
- "Rows: #{result[:row_count]}"
568
- end
569
-
570
- {
571
- rows: [{ "QUERY PLAN" => plan_text }],
572
- row_count: 1,
573
- column_names: ["QUERY PLAN"]
574
- }
575
- end
576
-
577
- # Scan operations
578
- def scan_table(plan)
579
- table_name = plan.table_name
580
- return [{}] unless table_name
581
- return @cte_rows.fetch(table_name.to_s) if @cte_rows.key?(table_name.to_s)
582
- if @engine.catalog.respond_to?(:find_view) && (view = @engine.catalog.find_view(table_name))
583
- statement = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(view.query).tokenize).parse.first
584
- return scan_table(Planner.new(@engine).plan(statement))
585
- end
586
- columns = @engine.table_columns(table_name)
587
-
588
- if plan.scan_type == :index && plan.index
589
- # Use index scan
590
- scan = Indexes::IndexScan.new(plan.index, :full)
591
- results = scan.execute
592
- row_ids = results.map { |r| r[:row_id] }
593
-
594
- # Fetch rows by row_id
595
- rows = []
596
- row_ids.each do |row_id|
597
- row = @engine.select_row(table_name, row_id, columns)
598
- rows << row if row
599
- end
600
- rows
601
- else
602
- # Sequential scan
603
- @engine.select_rows(table_name, columns)
604
- end
605
- end
606
-
607
- def qualify_rows(rows, table_reference)
608
- table_name = table_reference.respond_to?(:name) ? table_reference.name : table_reference
609
- alias_name = table_reference.respond_to?(:alias_name) ? table_reference.alias_name : nil
610
- rows.map do |row|
611
- row.each_with_object({}) do |(key, value), qualified|
612
- key = key.to_s
613
- qualified[key] = value
614
- qualified["#{table_name}.#{key}"] = value
615
- qualified["#{alias_name}.#{key}"] = value if alias_name
616
- end
617
- end
618
- end
619
-
620
- def execute_join(left_rows, right_rows, join)
621
- return left_rows.flat_map { |left_row| right_rows.map { |right_row| merge_join_rows(left_row, right_row) } } if join[:type] == :cross
622
-
623
- result = []
624
- matched_right = Array.new(right_rows.length, false)
625
- left_rows.each do |left_row|
626
- check_deadline!
627
- matches = right_rows.each_index.select do |index|
628
- evaluate_predicate(join[:predicate], merge_join_rows(left_row, right_rows[index]))
629
- end
630
- if matches.empty?
631
- result << merge_join_rows(left_row, nil) if %i[left full].include?(join[:type])
632
- else
633
- matches.each do |index|
634
- matched_right[index] = true
635
- result << merge_join_rows(left_row, right_rows[index])
636
- end
637
- end
638
- end
639
- if %i[right full].include?(join[:type])
640
- right_rows.each_with_index do |right_row, index|
641
- result << merge_join_rows(nil, right_row) unless matched_right[index]
642
- end
643
- end
644
- result
645
- end
646
-
647
- def merge_join_rows(left_row, right_row)
648
- (left_row || {}).merge(right_row || {}) { |_key, left_value, _right_value| left_value }
649
- end
650
-
651
- # Predicate evaluation
652
- def evaluate_predicate(predicate, row)
653
- case predicate
654
- when Predicate::Comparison
655
- left_val = evaluate_expression(predicate.left, row)
656
- right_val = evaluate_expression(predicate.right, row)
657
- compare_values(left_val, right_val, predicate.operator)
658
- when Predicate::And
659
- evaluate_predicate(predicate.left, row) && evaluate_predicate(predicate.right, row)
660
- when Predicate::Or
661
- evaluate_predicate(predicate.left, row) || evaluate_predicate(predicate.right, row)
662
- when Predicate::Not
663
- !evaluate_predicate(predicate.operand, row)
664
- when Predicate::Between
665
- val = evaluate_expression(predicate.expression, row)
666
- low = evaluate_expression(predicate.low, row)
667
- high = evaluate_expression(predicate.high, row)
668
- val >= low && val <= high
669
- when Predicate::In
670
- val = evaluate_expression(predicate.expression, row)
671
- predicate.values.any? { |v| val == evaluate_expression(v, row) }
672
- when Predicate::IsNull
673
- val = evaluate_expression(predicate.expression, row)
674
- predicate.negated ? !val.nil? : val.nil?
675
- when Predicate::Like
676
- val = evaluate_expression(predicate.expression, row).to_s
677
- pattern = evaluate_expression(predicate.pattern, row).to_s
678
- like_match?(val, pattern, predicate.case_sensitive)
679
- else
680
- true
681
- end
682
- end
683
-
684
- def evaluate_expression(expr, row = nil)
685
- case expr
686
- when SQL::AST::Literal
687
- expr.value
688
- when SQL::AST::Identifier
689
- return nil unless row
690
-
691
- qualified_name = expr.table && "#{expr.table}.#{expr.name}"
692
- row_value(row, qualified_name, expr.name, expr.name.to_sym)
693
- when SQL::AST::UnaryOp
694
- apply_ast_unary_op(expr.operator, evaluate_expression(expr.operand, row))
695
- when SQL::AST::BinaryOp
696
- apply_ast_binary_op(
697
- expr.operator,
698
- evaluate_expression(expr.left, row),
699
- evaluate_expression(expr.right, row)
700
- )
701
- when SQL::AST::FunctionCall
702
- if expr.window
703
- row && row[window_key(expr)]
704
- else
705
- apply_function(expr.name, expr.arguments.map { |argument| evaluate_expression(argument, row) })
706
- end
707
- when SQL::AST::Subquery
708
- result = child_executor(@cte_rows).execute(Planner.new(@engine).plan(expr.query))
709
- rows = result[:rows]
710
- raise ExecutionError, "Scalar subquery returned more than one row" if rows.size > 1
711
- rows.empty? ? nil : rows.first.values.first
712
- when Expression::Literal
713
- expr.value
714
- when Expression::Column
715
- return nil unless row
716
-
717
- qualified_name = expr.table && "#{expr.table}.#{expr.name}"
718
- row_value(row, qualified_name, expr.name, expr.name.to_sym)
719
- when Expression::BinaryOp
720
- left = evaluate_expression(expr.left, row)
721
- right = evaluate_expression(expr.right, row)
722
- apply_binary_op(left, right, expr.operator)
723
- when Expression::UnaryOp
724
- operand = evaluate_expression(expr.operand, row)
725
- apply_unary_op(operand, expr.operator)
726
- when Expression::Function
727
- args = expr.arguments.map { |arg| evaluate_expression(arg, row) }
728
- apply_function(expr.name, args)
729
- when Expression::Parameter
730
- expr.value
731
- else
732
- nil
733
- end
734
- end
735
-
736
- # Helper methods
737
- def compare_values(left, right, operator)
738
- return false if left.nil? || right.nil?
739
-
740
- case operator
741
- when :eq then left == right
742
- when :ne then left != right
743
- when :lt then left < right
744
- when :lte then left <= right
745
- when :gt then left > right
746
- when :gte then left >= right
747
- else false
748
- end
749
- end
750
-
751
- def apply_binary_op(left, right, operator)
752
- return nil if left.nil? || right.nil?
753
-
754
- case operator
755
- when :plus then left + right
756
- when :minus then left - right
757
- when :multiply then left * right
758
- when :divide then left / right if right != 0
759
- when :modulo then left % right if right != 0
760
- when :concat then left.to_s + right.to_s
761
- else nil
762
- end
763
- end
764
-
765
- def apply_ast_unary_op(operator, value)
766
- return nil if value.nil?
767
-
768
- case operator
769
- when SQL::Token::Type::PLUS then value
770
- when SQL::Token::Type::MINUS then -value
771
- when SQL::Token::Type::NOT then !value
772
- else value
773
- end
774
- end
775
-
776
- def apply_ast_binary_op(operator, left, right)
777
- case operator
778
- # SQL comparisons involving NULL evaluate to UNKNOWN, represented by
779
- # nil here. WHERE filtering already treats UNKNOWN as non-matching.
780
- when SQL::Token::Type::EQ then left.nil? || right.nil? ? nil : left == right
781
- when SQL::Token::Type::NE then left.nil? || right.nil? ? nil : left != right
782
- when SQL::Token::Type::LT then !left.nil? && !right.nil? && left < right
783
- when SQL::Token::Type::LTE then !left.nil? && !right.nil? && left <= right
784
- when SQL::Token::Type::GT then !left.nil? && !right.nil? && left > right
785
- when SQL::Token::Type::GTE then !left.nil? && !right.nil? && left >= right
786
- when SQL::Token::Type::AND then sql_and(left, right)
787
- when SQL::Token::Type::OR then sql_or(left, right)
788
- when SQL::Token::Type::PLUS then apply_binary_op(left, right, :plus)
789
- when SQL::Token::Type::MINUS then apply_binary_op(left, right, :minus)
790
- when SQL::Token::Type::STAR then apply_binary_op(left, right, :multiply)
791
- when SQL::Token::Type::SLASH then apply_binary_op(left, right, :divide)
792
- when SQL::Token::Type::PERCENT then apply_binary_op(left, right, :modulo)
793
- else nil
794
- end
795
- end
796
-
797
- def apply_unary_op(operand, operator)
798
- return nil if operand.nil?
799
-
800
- case operator
801
- when :negate then -operand
802
- when :not then !operand
803
- else operand
804
- end
805
- end
806
-
807
- def sql_and(left, right)
808
- return false if left == false || right == false
809
- return nil if left.nil? || right.nil?
810
-
811
- !!left && !!right
812
- end
813
-
814
- def sql_or(left, right)
815
- return true if left == true || right == true
816
- return nil if left.nil? || right.nil?
817
-
818
- !!left || !!right
819
- end
820
-
821
- def row_value(row, *keys)
822
- keys.compact.each do |key|
823
- return row[key] if row.respond_to?(:key?) && row.key?(key)
824
- end
825
- nil
826
- end
827
-
828
- def child_executor(cte_rows)
829
- self.class.new(
830
- @engine,
831
- cte_rows: cte_rows,
832
- deadline_at: @deadline_at,
833
- cancellation: @cancellation
834
- )
835
- end
836
-
837
- def check_deadline!
838
- if @cancellation&.cancelled?
839
- raise ExecutionError.new("Request cancelled by client", code: "cancelled")
840
- end
841
-
842
- return unless @deadline_at && Time.now >= @deadline_at
843
-
844
- raise ExecutionError.new("Request deadline exceeded during execution", code: "deadline_exceeded")
845
- end
846
-
847
- def apply_function(name, args)
848
- case name.to_s.upcase
849
- when "COUNT"
850
- args.compact.size
851
- when "SUM"
852
- args.compact.sum
853
- when "AVG"
854
- vals = args.compact
855
- vals.empty? ? 0 : vals.sum / vals.size.to_f
856
- when "MIN"
857
- args.compact.min
858
- when "MAX"
859
- args.compact.max
860
- when "LOWER"
861
- args.first.to_s.downcase
862
- when "UPPER"
863
- args.first.to_s.upcase
864
- when "LENGTH"
865
- args.first.to_s.length
866
- when "COALESCE"
867
- args.find { |arg| !arg.nil? }
868
- when "NOW"
869
- Time.now
870
- when "CURRENT_DATE"
871
- Date.today
872
- when "CURRENT_TIME"
873
- Time.now
874
- else
875
- nil
876
- end
877
- end
878
-
879
- def like_match?(value, pattern, case_sensitive = true)
880
- return false if value.nil? || pattern.nil?
881
-
882
- str = case_sensitive ? value : value.downcase
883
- pat = case_sensitive ? pattern : pattern.downcase
884
-
885
- # Convert SQL LIKE pattern to regex
886
- regex_str = Regexp.escape(pat)
887
- .gsub('%', '.*')
888
- .gsub('_', '.')
889
- Regexp.new("^#{regex_str}$").match?(str)
890
- end
891
-
892
- def project_row(row, projections)
893
- result = {}
894
- projections.each do |proj|
895
- expression = proj.respond_to?(:expression) ? proj.expression : proj
896
-
897
- if expression.is_a?(SQL::AST::Star)
898
- if expression.table
899
- prefix = "#{expression.table}."
900
- row.each do |key, value|
901
- key = key.to_s
902
- next unless key.start_with?(prefix)
903
-
904
- column = key.delete_prefix(prefix)
905
- result[column] = value unless column.start_with?("_")
906
- end
907
- else
908
- row.each do |key, value|
909
- key = key.to_s
910
- result[key] = value unless key.include?(".") || key.start_with?("_")
911
- end
912
- end
913
- elsif expression.is_a?(String) || expression.is_a?(Symbol)
914
- result[expression.to_s] = row[expression.to_s]
915
- else
916
- result[projection_name(proj)] = evaluate_expression(expression, row)
917
- end
918
- end
919
- result
920
- end
921
-
922
- def distinct_rows(rows)
923
- seen = Set.new
924
- rows.select do |row|
925
- key = row.to_hash
926
- if seen.include?(key)
927
- false
928
- else
929
- seen.add(key)
930
- true
931
- end
932
- end
933
- end
934
-
935
- def group_rows(rows, group_by, aggregates)
936
- groups = {}
937
- rows.each do |row|
938
- key = group_by.map { |col| row[col.to_s] }
939
- groups[key] ||= []
940
- groups[key] << row
941
- end
942
-
943
- result = []
944
- groups.each do |key, group_rows|
945
- result_row = {}
946
- group_by.each_with_index do |col, idx|
947
- result_row[col.to_s] = key[idx]
948
- end
949
-
950
- aggregates&.each do |agg|
951
- result_row[agg.alias || agg.name] = apply_aggregate(agg, group_rows)
952
- end
953
-
954
- result << result_row
955
- end
956
- result
957
- end
958
-
959
- def aggregate_rows(rows, group_by, projections)
960
- groups = {}
961
- if group_by.empty?
962
- groups[[]] = rows
963
- else
964
- rows.each do |row|
965
- key = group_by.map { |expression| evaluate_expression(expression, row) }
966
- (groups[key] ||= []) << row
967
- end
968
- end
969
-
970
- groups.map do |_key, group|
971
- representative = group.first || {}
972
- projections.each_with_object({}) do |projection, result|
973
- expression = projection.respond_to?(:expression) ? projection.expression : projection
974
- name = projection_name(projection)
975
- result[name] = if aggregate_function?(expression)
976
- apply_aggregate_function(expression, group)
977
- else
978
- evaluate_expression(expression, representative)
979
- end
980
- end
981
- end
982
- end
983
-
984
- def aggregate_function?(expression)
985
- expression.is_a?(SQL::AST::FunctionCall) && !expression.window &&
986
- %w[COUNT SUM AVG MIN MAX].include?(expression.name.to_s.upcase)
987
- end
988
-
989
- def apply_window_functions!(rows, projections)
990
- projections.each do |projection|
991
- expression = projection.respond_to?(:expression) ? projection.expression : projection
992
- next unless expression.is_a?(SQL::AST::FunctionCall) && expression.window
993
-
994
- spec = expression.window
995
- partitions = {}
996
- rows.each { |row| (partitions[spec[:partition_by].map { |part| evaluate_expression(part, row) }] ||= []) << row }
997
- partitions.each_value do |partition_rows|
998
- ordered_rows = sort_window_rows(partition_rows, spec[:order_by])
999
- ordered_rows.each_with_index do |row, index|
1000
- row[window_key(expression)] = window_value(expression, ordered_rows, index, spec[:order_by], spec[:frame])
1001
- end
1002
- end
1003
- end
1004
- end
1005
-
1006
- def window_key(expression)
1007
- "__rubydb_window_#{expression.object_id}"
1008
- end
1009
-
1010
- def sort_window_rows(rows, order_by)
1011
- return rows.dup if order_by.empty?
1012
-
1013
- rows.sort do |left, right|
1014
- comparison = 0
1015
- order_by.each do |item|
1016
- left_value = evaluate_expression(item.expression, left)
1017
- right_value = evaluate_expression(item.expression, right)
1018
- comparison = compare_sort_values(left_value, right_value)
1019
- comparison = -comparison if item.direction == :desc
1020
- break unless comparison.zero?
1021
- end
1022
- comparison
1023
- end
1024
- end
1025
-
1026
- def compare_sort_values(left, right)
1027
- return 0 if left.nil? && right.nil?
1028
- return -1 if left.nil?
1029
- return 1 if right.nil?
1030
-
1031
- left <=> right
1032
- end
1033
-
1034
- def window_value(expression, rows, index, order_by, frame = nil)
1035
- rows = window_frame_rows(rows, index, frame) if frame
1036
- name = expression.name.to_s.upcase
1037
- if %w[ROW_NUMBER RANK DENSE_RANK].include?(name)
1038
- return index + 1 if name == "ROW_NUMBER" || order_by.empty?
1039
-
1040
- keys = rows.map { |row| order_by.map { |item| evaluate_expression(item.expression, row) } }
1041
- current = keys[index]
1042
- return keys.index(current) + 1 if name == "RANK"
1043
-
1044
- return keys[0..index].uniq.index(current) + 1
1045
- end
1046
-
1047
- argument = expression.arguments.first
1048
- values = if argument.is_a?(SQL::AST::Star)
1049
- rows
1050
- else
1051
- rows.map { |row| evaluate_expression(argument, row) }.compact
1052
- end
1053
- values = values.uniq if expression.distinct
1054
- case name
1055
- when "COUNT" then values.size
1056
- when "SUM" then values.empty? ? nil : values.sum
1057
- when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
1058
- when "MIN" then values.min
1059
- when "MAX" then values.max
1060
- else
1061
- raise ExecutionError, "Unsupported window function: #{expression.name}"
1062
- end
1063
- end
1064
-
1065
- def window_frame_rows(rows, index, frame)
1066
- start_index = window_frame_index(frame[:start], index, rows.size, start: true)
1067
- finish = frame[:finish] || { kind: :current_row }
1068
- end_index = window_frame_index(finish, index, rows.size, start: false)
1069
- return [] if start_index > end_index
1070
-
1071
- rows[start_index..end_index] || []
1072
- end
1073
-
1074
- def window_frame_index(boundary, index, size, start:)
1075
- case boundary[:kind]
1076
- when :unbounded_preceding then 0
1077
- when :unbounded_following then size - 1
1078
- when :current_row then index
1079
- when :preceding then [index - boundary[:value], 0].max
1080
- when :following then [index + boundary[:value], size - 1].min
1081
- else raise ExecutionError, "Unsupported window frame boundary: #{boundary[:kind]}"
1082
- end
1083
- end
1084
-
1085
- def apply_aggregate_function(expression, rows)
1086
- argument = expression.arguments.first
1087
- values = if argument.is_a?(SQL::AST::Star)
1088
- rows
1089
- else
1090
- rows.map { |row| evaluate_expression(argument, row) }.compact
1091
- end
1092
- values = values.uniq if expression.distinct
1093
-
1094
- case expression.name.to_s.upcase
1095
- when "COUNT" then values.size
1096
- when "SUM" then values.empty? ? nil : values.sum
1097
- when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
1098
- when "MIN" then values.min
1099
- when "MAX" then values.max
1100
- end
1101
- end
1102
-
1103
- def apply_aggregate(agg, rows)
1104
- values = rows.map { |row| row[agg.column.to_s] }.compact
1105
-
1106
- case agg.name.to_s.upcase
1107
- when "COUNT" then values.size
1108
- when "SUM" then values.sum
1109
- when "AVG" then values.empty? ? 0 : values.sum / values.size.to_f
1110
- when "MIN" then values.min
1111
- when "MAX" then values.max
1112
- else nil
1113
- end
1114
- end
1115
-
1116
- def sort_rows(rows, order_by)
1117
- rows.sort do |a, b|
1118
- comparison = 0
1119
- order_by.each do |order|
1120
- col = (order.is_a?(Hash) ? order[:column] : order.column).to_s
1121
- val_a = a[col]
1122
- val_b = b[col]
1123
-
1124
- if val_a.nil? && val_b.nil?
1125
- comparison = 0
1126
- elsif val_a.nil?
1127
- comparison = -1
1128
- elsif val_b.nil?
1129
- comparison = 1
1130
- else
1131
- comparison = val_a <=> val_b
1132
- end
1133
-
1134
- direction = order.is_a?(Hash) ? order[:direction] : order.direction
1135
- comparison = -comparison if direction == :desc
1136
- break unless comparison == 0
1137
- end
1138
- comparison
1139
- end
1140
- end
1141
-
1142
- def get_column_names(plan)
1143
- if plan.projections
1144
- plan.projections.map { |projection| projection_name(projection) }
1145
- else
1146
- @engine.table_columns(plan.table_name).map(&:name)
1147
- end
1148
- end
1149
-
1150
- def projection_name(projection)
1151
- return projection.to_s if projection.is_a?(String) || projection.is_a?(Symbol)
1152
-
1153
- expression = projection.respond_to?(:expression) ? projection.expression : projection
1154
- alias_name = if projection.respond_to?(:alias_name)
1155
- projection.alias_name
1156
- elsif projection.respond_to?(:alias)
1157
- projection.alias
1158
- end
1159
- return alias_name.to_s if alias_name
1160
-
1161
- return expression.name.to_s if expression.respond_to?(:name) && expression.name
1162
- return expression.to_sql if expression.respond_to?(:to_sql)
1163
-
1164
- expression.to_s
1165
- end
1166
-
1167
- def format_plan(plan)
1168
- lines = []
1169
- lines << "QUERY PLAN"
1170
- lines << "=" * 40
1171
-
1172
- if plan.scan_type == :index
1173
- lines << "Index Scan on #{plan.table_name} using #{plan.index.name}"
1174
- else
1175
- lines << "Seq Scan on #{plan.table_name}"
1176
- end
1177
-
1178
- if plan.predicate
1179
- lines << " Filter: #{plan.predicate.to_s}"
1180
- end
1181
-
1182
- if plan.order_by && plan.order_by.any?
1183
- order_str = plan.order_by.map { |o| "#{o.column} #{o.direction}" }.join(", ")
1184
- lines << " Order By: #{order_str}"
1185
- end
1186
-
1187
- if plan.limit
1188
- lines << " Limit: #{plan.limit}"
1189
- end
1190
-
1191
- if plan.offset
1192
- lines << " Offset: #{plan.offset}"
1193
- end
1194
-
1195
- lines << " Estimated Cost: #{plan.estimated_cost}"
1196
- lines << " Estimated Rows: #{plan.estimated_rows}"
1197
- lines.join("\n")
1198
- end
1199
- end
1200
- end
1201
- end
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