rubydb 0.1.3 → 0.1.5

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