rubydb 0.1.4 → 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 +43 -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 +873 -879
  30. data/adapters/activerecord/rubydb-activerecord.gemspec +20 -20
  31. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -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 +1182 -1188
  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 +117 -117
  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,1201 +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] },
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] },
261
260
  row_id: inserted.reverse_each.map { |result| result[:row_id] }.compact.first,
262
261
  inserted_ids: inserted.filter_map { |result| result[:inserted_id] },
263
262
  inserted_id: inserted.reverse_each.map { |result| result[:inserted_id] }.compact.first,
264
263
  message: "INSERT #{inserted.sum { |result| result[:affected_rows] }}"
265
- }
266
- rescue Exception
267
- @engine.rollback_transaction if implicit_transaction && @engine.in_transaction?
268
- raise
269
- end
270
- end
271
-
272
- def execute_single_insert(plan, table_name, columns, values)
273
-
274
- # Build row data
275
- row_data = {}
276
- columns.each_with_index do |col, idx|
277
- row_data[col] = evaluate_expression(values[idx])
278
- end
279
-
280
- # Insert into engine
281
- table_columns = @engine.table_columns(table_name)
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)
282
283
  row_id = @engine.insert_row(table_name, table_columns, row_data)
283
284
  primary_key = table_columns.find(&:primary_key?)
284
285
  inserted_row = @engine.select_row(table_name, row_id, table_columns)
285
286
  inserted_id = if primary_key && inserted_row
286
287
  inserted_row[primary_key.name] || inserted_row[primary_key.name.to_sym]
287
288
  end
288
-
289
- {
290
- row_count: 1,
291
- affected_rows: 1,
289
+
290
+ {
291
+ row_count: 1,
292
+ affected_rows: 1,
292
293
  row_id: row_id,
293
294
  inserted_id: inserted_id,
294
- message: "INSERT 1"
295
- }
296
- rescue DatabaseError => error
297
- raise unless error.message.match?(/duplicate|unique|primary key/i)
298
- return { row_count: 0, affected_rows: 0, message: "INSERT 0 (conflict ignored)" } if plan.on_conflict == :nothing
299
- raise unless plan.on_conflict.is_a?(Hash) && plan.on_conflict[:action] == :update
300
-
301
- target = plan.on_conflict[:target]
302
- target = @engine.table_columns(table_name).select(&:primary_key?).map(&:name) if target.empty?
303
- if target.empty?
304
- target = @engine.table_columns(table_name).select(&:unique?).map(&:name)
305
- end
306
- if target.empty? && @engine.respond_to?(:index_manager)
307
- target = @engine.index_manager.get_indexes_for_table(table_name)
308
- .select(&:unique?).min_by { |index| index.columns.size }&.columns || []
309
- end
310
- raise ExecutionError, "ON CONFLICT DO UPDATE requires a conflict target or primary key" if target.empty?
311
- existing = @engine.select_rows(table_name, @engine.table_columns(table_name)).find do |row|
312
- target.all? do |column|
313
- row_value(row, column, column.to_s) == row_value(row_data, column, column.to_s)
314
- end
315
- end
316
- raise error unless existing
317
-
318
- context = existing.merge(row_data.transform_keys(&:to_s).transform_keys { |key| "excluded.#{key}" })
319
- values = plan.on_conflict[:assignments].each_with_object({}) do |assignment, updates|
320
- updates[assignment.column] = evaluate_expression(assignment.value, context)
321
- end
322
- row_id = existing[:_row_id] || existing["_row_id"]
323
- @engine.update_row(table_name, row_id, values)
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)
324
325
  inserted_id = target.map { |column| existing[column] || existing[column.to_sym] }.first
325
- { row_count: 1, affected_rows: 1, row_id: row_id, inserted_id: inserted_id, message: "INSERT 0 UPDATE 1" }
326
- end
327
-
328
- def execute_update(plan)
329
- table_name = plan.table_name
330
- assignments = plan.assignments
331
- predicate = plan.predicate
332
-
333
- # Get rows to update
334
- rows = scan_table(Plan::Select.new(table_name, []))
335
- rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
336
-
337
- updated_count = 0
338
- rows.each do |row|
339
- # Apply updates
340
- assignments.each do |assignment|
341
- column = assignment.is_a?(Hash) ? (assignment[:column] || assignment["column"]) : assignment.column
342
- value = assignment.is_a?(Hash) ? (assignment[:value] || assignment["value"]) : assignment.value
343
- row[column] = evaluate_expression(value, row)
344
- end
345
-
346
- # Update in engine
347
- row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
348
- @engine.update_row(table_name, row_id, row)
349
-
350
- updated_count += 1
351
- end
352
-
353
- {
354
- row_count: updated_count,
355
- affected_rows: updated_count,
356
- message: "UPDATE #{updated_count}"
357
- }
358
- end
359
-
360
- def execute_delete(plan)
361
- table_name = plan.table_name
362
- predicate = plan.predicate
363
-
364
- # Get rows to delete
365
- rows = scan_table(Plan::Select.new(table_name, []))
366
- rows = rows.select { |row| evaluate_predicate(predicate, row) } if predicate
367
-
368
- deleted_count = 0
369
- rows.each do |row|
370
- row_id = row[:_row_id] || row["_row_id"] || row[:id] || row["id"]
371
- @engine.delete_row(table_name, row_id)
372
- deleted_count += 1
373
- end
374
-
375
- {
376
- row_count: deleted_count,
377
- affected_rows: deleted_count,
378
- message: "DELETE #{deleted_count}"
379
- }
380
- end
381
-
382
- def execute_create_table(plan)
383
- table_name = plan.table_name
384
- columns = plan.columns
385
- options = plan.options || {}
386
-
387
- @engine.create_table(table_name, columns, options)
388
-
389
- {
390
- row_count: 0,
391
- message: "CREATE TABLE #{table_name}"
392
- }
393
- end
394
-
395
- def execute_drop_table(plan)
396
- table_name = plan.table_name
397
- options = plan.options || {}
398
-
399
- @engine.drop_table(table_name, options)
400
-
401
- {
402
- row_count: 0,
403
- message: "DROP TABLE #{table_name}"
404
- }
405
- end
406
-
407
- def execute_create_database(plan)
408
- @engine.catalog.create_database(plan.database_name, **(plan.options || {}))
409
- { row_count: 0, message: "CREATE DATABASE #{plan.database_name}" }
410
- end
411
-
412
- def execute_drop_database(plan)
413
- @engine.catalog.drop_database(plan.database_name, **(plan.options || {}))
414
- { row_count: 0, message: "DROP DATABASE #{plan.database_name}" }
415
- end
416
-
417
- def execute_create_schema(plan)
418
- @engine.catalog.create_schema(plan.schema_name, **(plan.options || {}))
419
- { row_count: 0, message: "CREATE SCHEMA #{plan.schema_name}" }
420
- end
421
-
422
- def execute_drop_schema(plan)
423
- @engine.catalog.drop_schema(plan.schema_name, **(plan.options || {}))
424
- { row_count: 0, message: "DROP SCHEMA #{plan.schema_name}" }
425
- end
426
-
427
- def execute_create_view(plan)
428
- query = plan.query.respond_to?(:to_sql) ? plan.query.to_sql : plan.query
429
- @engine.catalog.create_view(plan.view_name, query, **(plan.options || {}))
430
- { row_count: 0, message: "CREATE VIEW #{plan.view_name}" }
431
- end
432
-
433
- def execute_drop_view(plan)
434
- @engine.catalog.drop_view(plan.view_name, **(plan.options || {}))
435
- { row_count: 0, message: "DROP VIEW #{plan.view_name}" }
436
- end
437
-
438
- def execute_create_trigger(plan)
439
- definition = "EXECUTE FUNCTION #{plan.function_name}()"
440
- @engine.catalog.create_trigger(plan.trigger_name, plan.event, plan.target_table, definition, timing: plan.timing, function_name: plan.function_name)
441
- { row_count: 0, message: "CREATE TRIGGER #{plan.trigger_name}" }
442
- end
443
-
444
- def execute_drop_trigger(plan)
445
- @engine.catalog.drop_trigger(plan.trigger_name, **(plan.options || {}))
446
- { row_count: 0, message: "DROP TRIGGER #{plan.trigger_name}" }
447
- end
448
-
449
- def execute_vacuum(_plan)
450
- result = @engine.vacuum
451
- { row_count: 0, vacuum: result, message: "VACUUM" }
452
- end
453
-
454
- def execute_alter_table_add_column(plan)
455
- @engine.add_column(plan.table_name, plan.column_name, plan.column_type, plan.options)
456
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD COLUMN #{plan.column_name}" }
457
- end
458
-
459
- def execute_alter_table_drop_column(plan)
460
- @engine.drop_column(plan.table_name, plan.column_name)
461
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP COLUMN #{plan.column_name}" }
462
- end
463
-
464
- def execute_alter_table_add_constraint(plan)
465
- @engine.add_constraint(plan.table_name, plan.constraint)
466
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} ADD CONSTRAINT #{plan.constraint.name}" }
467
- end
468
-
469
- def execute_alter_table_drop_constraint(plan)
470
- @engine.drop_constraint(plan.table_name, plan.constraint_name)
471
- { row_count: 0, message: "ALTER TABLE #{plan.table_name} DROP CONSTRAINT #{plan.constraint_name}" }
472
- end
473
-
474
- def execute_create_index(plan)
475
- index_name = plan.index_name
476
- table_name = plan.table_name
477
- columns = plan.columns
478
- options = plan.options || {}
479
-
480
- if @engine.respond_to?(:index_manager)
481
- @engine.index_manager.create_index(index_name, table_name, columns, options)
482
- end
483
-
484
- {
485
- row_count: 0,
486
- message: "CREATE INDEX #{index_name}"
487
- }
488
- end
489
-
490
- def execute_drop_index(plan)
491
- index_name = plan.index_name
492
- options = plan.options || {}
493
-
494
- if @engine.respond_to?(:index_manager)
495
- @engine.index_manager.drop_index(index_name, options)
496
- end
497
-
498
- {
499
- row_count: 0,
500
- message: "DROP INDEX #{index_name}"
501
- }
502
- end
503
-
504
- def execute_begin_transaction(plan)
505
- isolation_level = plan.isolation_level || :read_committed
506
- transaction_id = @engine.begin_transaction(isolation_level)
507
-
508
- {
509
- row_count: 0,
510
- transaction_id: transaction_id,
511
- message: "BEGIN"
512
- }
513
- end
514
-
515
- def execute_commit(plan)
516
- @engine.commit_transaction
517
-
518
- {
519
- row_count: 0,
520
- message: "COMMIT"
521
- }
522
- end
523
-
524
- def execute_rollback(plan)
525
- @engine.rollback_transaction
526
-
527
- {
528
- row_count: 0,
529
- message: "ROLLBACK"
530
- }
531
- end
532
-
533
- def execute_savepoint(plan)
534
- @engine.create_savepoint(plan.name)
535
- { row_count: 0, message: "SAVEPOINT #{plan.name}" }
536
- end
537
-
538
- def execute_rollback_to_savepoint(plan)
539
- @engine.rollback_to_savepoint(plan.name)
540
- { row_count: 0, message: "ROLLBACK TO SAVEPOINT #{plan.name}" }
541
- end
542
-
543
- def execute_release_savepoint(plan)
544
- @engine.release_savepoint(plan.name)
545
- { row_count: 0, message: "RELEASE SAVEPOINT #{plan.name}" }
546
- end
547
-
548
- def execute_explain(plan)
549
- statement = plan.statement
550
- analyze = plan.analyze || false
551
-
552
- # Generate execution plan for the statement
553
- planner = Planner.new(@engine)
554
- analyzed_plan = planner.plan(statement)
555
-
556
- # Format the plan
557
- plan_text = format_plan(analyzed_plan)
558
-
559
- if analyze
560
- # Execute and get actual stats
561
- start_time = Time.now
562
- result = execute(analyzed_plan)
563
- elapsed_ms = ((Time.now - start_time) * 1000).round(2)
564
-
565
- plan_text = "EXPLAIN ANALYZE:\n#{plan_text}\n" +
566
- "Execution Time: #{elapsed_ms}ms\n" +
567
- "Rows: #{result[:row_count]}"
568
- end
569
-
570
- {
571
- rows: [{ "QUERY PLAN" => plan_text }],
572
- row_count: 1,
573
- column_names: ["QUERY PLAN"]
574
- }
575
- end
576
-
577
- # Scan operations
578
- def scan_table(plan)
579
- table_name = plan.table_name
580
- return [{}] unless table_name
581
- return @cte_rows.fetch(table_name.to_s) if @cte_rows.key?(table_name.to_s)
582
- if @engine.catalog.respond_to?(:find_view) && (view = @engine.catalog.find_view(table_name))
583
- statement = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(view.query).tokenize).parse.first
584
- return scan_table(Planner.new(@engine).plan(statement))
585
- end
586
- columns = @engine.table_columns(table_name)
587
-
588
- if plan.scan_type == :index && plan.index
589
- # Use index scan
590
- scan = Indexes::IndexScan.new(plan.index, :full)
591
- results = scan.execute
592
- row_ids = results.map { |r| r[:row_id] }
593
-
594
- # Fetch rows by row_id
595
- rows = []
596
- row_ids.each do |row_id|
597
- row = @engine.select_row(table_name, row_id, columns)
598
- rows << row if row
599
- end
600
- rows
601
- else
602
- # Sequential scan
603
- @engine.select_rows(table_name, columns)
604
- end
605
- end
606
-
607
- def qualify_rows(rows, table_reference)
608
- table_name = table_reference.respond_to?(:name) ? table_reference.name : table_reference
609
- alias_name = table_reference.respond_to?(:alias_name) ? table_reference.alias_name : nil
610
- rows.map do |row|
611
- row.each_with_object({}) do |(key, value), qualified|
612
- key = key.to_s
613
- qualified[key] = value
614
- qualified["#{table_name}.#{key}"] = value
615
- qualified["#{alias_name}.#{key}"] = value if alias_name
616
- end
617
- end
618
- end
619
-
620
- def execute_join(left_rows, right_rows, join)
621
- return left_rows.flat_map { |left_row| right_rows.map { |right_row| merge_join_rows(left_row, right_row) } } if join[:type] == :cross
622
-
623
- result = []
624
- matched_right = Array.new(right_rows.length, false)
625
- left_rows.each do |left_row|
626
- check_deadline!
627
- matches = right_rows.each_index.select do |index|
628
- evaluate_predicate(join[:predicate], merge_join_rows(left_row, right_rows[index]))
629
- end
630
- if matches.empty?
631
- result << merge_join_rows(left_row, nil) if %i[left full].include?(join[:type])
632
- else
633
- matches.each do |index|
634
- matched_right[index] = true
635
- result << merge_join_rows(left_row, right_rows[index])
636
- end
637
- end
638
- end
639
- if %i[right full].include?(join[:type])
640
- right_rows.each_with_index do |right_row, index|
641
- result << merge_join_rows(nil, right_row) unless matched_right[index]
642
- end
643
- end
644
- result
645
- end
646
-
647
- def merge_join_rows(left_row, right_row)
648
- (left_row || {}).merge(right_row || {}) { |_key, left_value, _right_value| left_value }
649
- end
650
-
651
- # Predicate evaluation
652
- def evaluate_predicate(predicate, row)
653
- case predicate
654
- when Predicate::Comparison
655
- left_val = evaluate_expression(predicate.left, row)
656
- right_val = evaluate_expression(predicate.right, row)
657
- compare_values(left_val, right_val, predicate.operator)
658
- when Predicate::And
659
- evaluate_predicate(predicate.left, row) && evaluate_predicate(predicate.right, row)
660
- when Predicate::Or
661
- evaluate_predicate(predicate.left, row) || evaluate_predicate(predicate.right, row)
662
- when Predicate::Not
663
- !evaluate_predicate(predicate.operand, row)
664
- when Predicate::Between
665
- val = evaluate_expression(predicate.expression, row)
666
- low = evaluate_expression(predicate.low, row)
667
- high = evaluate_expression(predicate.high, row)
668
- val >= low && val <= high
669
- when Predicate::In
670
- val = evaluate_expression(predicate.expression, row)
671
- predicate.values.any? { |v| val == evaluate_expression(v, row) }
672
- when Predicate::IsNull
673
- val = evaluate_expression(predicate.expression, row)
674
- predicate.negated ? !val.nil? : val.nil?
675
- when Predicate::Like
676
- val = evaluate_expression(predicate.expression, row).to_s
677
- pattern = evaluate_expression(predicate.pattern, row).to_s
678
- like_match?(val, pattern, predicate.case_sensitive)
679
- else
680
- true
681
- end
682
- end
683
-
684
- def evaluate_expression(expr, row = nil)
685
- case expr
686
- when SQL::AST::Literal
687
- expr.value
688
- when SQL::AST::Identifier
689
- return nil unless row
690
-
691
- qualified_name = expr.table && "#{expr.table}.#{expr.name}"
692
- row_value(row, qualified_name, expr.name, expr.name.to_sym)
693
- when SQL::AST::UnaryOp
694
- apply_ast_unary_op(expr.operator, evaluate_expression(expr.operand, row))
695
- when SQL::AST::BinaryOp
696
- apply_ast_binary_op(
697
- expr.operator,
698
- evaluate_expression(expr.left, row),
699
- evaluate_expression(expr.right, row)
700
- )
701
- when SQL::AST::FunctionCall
702
- if expr.window
703
- row && row[window_key(expr)]
704
- else
705
- apply_function(expr.name, expr.arguments.map { |argument| evaluate_expression(argument, row) })
706
- end
707
- when SQL::AST::Subquery
708
- result = child_executor(@cte_rows).execute(Planner.new(@engine).plan(expr.query))
709
- rows = result[:rows]
710
- raise ExecutionError, "Scalar subquery returned more than one row" if rows.size > 1
711
- rows.empty? ? nil : rows.first.values.first
712
- when Expression::Literal
713
- expr.value
714
- when Expression::Column
715
- return nil unless row
716
-
717
- qualified_name = expr.table && "#{expr.table}.#{expr.name}"
718
- row_value(row, qualified_name, expr.name, expr.name.to_sym)
719
- when Expression::BinaryOp
720
- left = evaluate_expression(expr.left, row)
721
- right = evaluate_expression(expr.right, row)
722
- apply_binary_op(left, right, expr.operator)
723
- when Expression::UnaryOp
724
- operand = evaluate_expression(expr.operand, row)
725
- apply_unary_op(operand, expr.operator)
726
- when Expression::Function
727
- args = expr.arguments.map { |arg| evaluate_expression(arg, row) }
728
- apply_function(expr.name, args)
729
- when Expression::Parameter
730
- expr.value
731
- else
732
- nil
733
- end
734
- end
735
-
736
- # Helper methods
737
- def compare_values(left, right, operator)
738
- return false if left.nil? || right.nil?
739
-
740
- case operator
741
- when :eq then left == right
742
- when :ne then left != right
743
- when :lt then left < right
744
- when :lte then left <= right
745
- when :gt then left > right
746
- when :gte then left >= right
747
- else false
748
- end
749
- end
750
-
751
- def apply_binary_op(left, right, operator)
752
- return nil if left.nil? || right.nil?
753
-
754
- case operator
755
- when :plus then left + right
756
- when :minus then left - right
757
- when :multiply then left * right
758
- when :divide then left / right if right != 0
759
- when :modulo then left % right if right != 0
760
- when :concat then left.to_s + right.to_s
761
- else nil
762
- end
763
- end
764
-
765
- def apply_ast_unary_op(operator, value)
766
- return nil if value.nil?
767
-
768
- case operator
769
- when SQL::Token::Type::PLUS then value
770
- when SQL::Token::Type::MINUS then -value
771
- when SQL::Token::Type::NOT then !value
772
- else value
773
- end
774
- end
775
-
776
- def apply_ast_binary_op(operator, left, right)
777
- case operator
778
- # SQL comparisons involving NULL evaluate to UNKNOWN, represented by
779
- # nil here. WHERE filtering already treats UNKNOWN as non-matching.
780
- when SQL::Token::Type::EQ then left.nil? || right.nil? ? nil : left == right
781
- when SQL::Token::Type::NE then left.nil? || right.nil? ? nil : left != right
782
- when SQL::Token::Type::LT then !left.nil? && !right.nil? && left < right
783
- when SQL::Token::Type::LTE then !left.nil? && !right.nil? && left <= right
784
- when SQL::Token::Type::GT then !left.nil? && !right.nil? && left > right
785
- when SQL::Token::Type::GTE then !left.nil? && !right.nil? && left >= right
786
- when SQL::Token::Type::AND then sql_and(left, right)
787
- when SQL::Token::Type::OR then sql_or(left, right)
788
- when SQL::Token::Type::PLUS then apply_binary_op(left, right, :plus)
789
- when SQL::Token::Type::MINUS then apply_binary_op(left, right, :minus)
790
- when SQL::Token::Type::STAR then apply_binary_op(left, right, :multiply)
791
- when SQL::Token::Type::SLASH then apply_binary_op(left, right, :divide)
792
- when SQL::Token::Type::PERCENT then apply_binary_op(left, right, :modulo)
793
- else nil
794
- end
795
- end
796
-
797
- def apply_unary_op(operand, operator)
798
- return nil if operand.nil?
799
-
800
- case operator
801
- when :negate then -operand
802
- when :not then !operand
803
- else operand
804
- end
805
- end
806
-
807
- def sql_and(left, right)
808
- return false if left == false || right == false
809
- return nil if left.nil? || right.nil?
810
-
811
- !!left && !!right
812
- end
813
-
814
- def sql_or(left, right)
815
- return true if left == true || right == true
816
- return nil if left.nil? || right.nil?
817
-
818
- !!left || !!right
819
- end
820
-
821
- def row_value(row, *keys)
822
- keys.compact.each do |key|
823
- return row[key] if row.respond_to?(:key?) && row.key?(key)
824
- end
825
- nil
826
- end
827
-
828
- def child_executor(cte_rows)
829
- self.class.new(
830
- @engine,
831
- cte_rows: cte_rows,
832
- deadline_at: @deadline_at,
833
- cancellation: @cancellation
834
- )
835
- end
836
-
837
- def check_deadline!
838
- if @cancellation&.cancelled?
839
- raise ExecutionError.new("Request cancelled by client", code: "cancelled")
840
- end
841
-
842
- return unless @deadline_at && Time.now >= @deadline_at
843
-
844
- raise ExecutionError.new("Request deadline exceeded during execution", code: "deadline_exceeded")
845
- end
846
-
847
- def apply_function(name, args)
848
- case name.to_s.upcase
849
- when "COUNT"
850
- args.compact.size
851
- when "SUM"
852
- args.compact.sum
853
- when "AVG"
854
- vals = args.compact
855
- vals.empty? ? 0 : vals.sum / vals.size.to_f
856
- when "MIN"
857
- args.compact.min
858
- when "MAX"
859
- args.compact.max
860
- when "LOWER"
861
- args.first.to_s.downcase
862
- when "UPPER"
863
- args.first.to_s.upcase
864
- when "LENGTH"
865
- args.first.to_s.length
866
- when "COALESCE"
867
- args.find { |arg| !arg.nil? }
868
- when "NOW"
869
- Time.now
870
- when "CURRENT_DATE"
871
- Date.today
872
- when "CURRENT_TIME"
873
- Time.now
874
- else
875
- nil
876
- end
877
- end
878
-
879
- def like_match?(value, pattern, case_sensitive = true)
880
- return false if value.nil? || pattern.nil?
881
-
882
- str = case_sensitive ? value : value.downcase
883
- pat = case_sensitive ? pattern : pattern.downcase
884
-
885
- # Convert SQL LIKE pattern to regex
886
- regex_str = Regexp.escape(pat)
887
- .gsub('%', '.*')
888
- .gsub('_', '.')
889
- Regexp.new("^#{regex_str}$").match?(str)
890
- end
891
-
892
- def project_row(row, projections)
893
- result = {}
894
- projections.each do |proj|
895
- expression = proj.respond_to?(:expression) ? proj.expression : proj
896
-
897
- if expression.is_a?(SQL::AST::Star)
898
- if expression.table
899
- prefix = "#{expression.table}."
900
- row.each do |key, value|
901
- key = key.to_s
902
- next unless key.start_with?(prefix)
903
-
904
- column = key.delete_prefix(prefix)
905
- result[column] = value unless column.start_with?("_")
906
- end
907
- else
908
- row.each do |key, value|
909
- key = key.to_s
910
- result[key] = value unless key.include?(".") || key.start_with?("_")
911
- end
912
- end
913
- elsif expression.is_a?(String) || expression.is_a?(Symbol)
914
- result[expression.to_s] = row[expression.to_s]
915
- else
916
- result[projection_name(proj)] = evaluate_expression(expression, row)
917
- end
918
- end
919
- result
920
- end
921
-
922
- def distinct_rows(rows)
923
- seen = Set.new
924
- rows.select do |row|
925
- key = row.to_hash
926
- if seen.include?(key)
927
- false
928
- else
929
- seen.add(key)
930
- true
931
- end
932
- end
933
- end
934
-
935
- def group_rows(rows, group_by, aggregates)
936
- groups = {}
937
- rows.each do |row|
938
- key = group_by.map { |col| row[col.to_s] }
939
- groups[key] ||= []
940
- groups[key] << row
941
- end
942
-
943
- result = []
944
- groups.each do |key, group_rows|
945
- result_row = {}
946
- group_by.each_with_index do |col, idx|
947
- result_row[col.to_s] = key[idx]
948
- end
949
-
950
- aggregates&.each do |agg|
951
- result_row[agg.alias || agg.name] = apply_aggregate(agg, group_rows)
952
- end
953
-
954
- result << result_row
955
- end
956
- result
957
- end
958
-
959
- def aggregate_rows(rows, group_by, projections)
960
- groups = {}
961
- if group_by.empty?
962
- groups[[]] = rows
963
- else
964
- rows.each do |row|
965
- key = group_by.map { |expression| evaluate_expression(expression, row) }
966
- (groups[key] ||= []) << row
967
- end
968
- end
969
-
970
- groups.map do |_key, group|
971
- representative = group.first || {}
972
- projections.each_with_object({}) do |projection, result|
973
- expression = projection.respond_to?(:expression) ? projection.expression : projection
974
- name = projection_name(projection)
975
- result[name] = if aggregate_function?(expression)
976
- apply_aggregate_function(expression, group)
977
- else
978
- evaluate_expression(expression, representative)
979
- end
980
- end
981
- end
982
- end
983
-
984
- def aggregate_function?(expression)
985
- expression.is_a?(SQL::AST::FunctionCall) && !expression.window &&
986
- %w[COUNT SUM AVG MIN MAX].include?(expression.name.to_s.upcase)
987
- end
988
-
989
- def apply_window_functions!(rows, projections)
990
- projections.each do |projection|
991
- expression = projection.respond_to?(:expression) ? projection.expression : projection
992
- next unless expression.is_a?(SQL::AST::FunctionCall) && expression.window
993
-
994
- spec = expression.window
995
- partitions = {}
996
- rows.each { |row| (partitions[spec[:partition_by].map { |part| evaluate_expression(part, row) }] ||= []) << row }
997
- partitions.each_value do |partition_rows|
998
- ordered_rows = sort_window_rows(partition_rows, spec[:order_by])
999
- ordered_rows.each_with_index do |row, index|
1000
- row[window_key(expression)] = window_value(expression, ordered_rows, index, spec[:order_by], spec[:frame])
1001
- end
1002
- end
1003
- end
1004
- end
1005
-
1006
- def window_key(expression)
1007
- "__rubydb_window_#{expression.object_id}"
1008
- end
1009
-
1010
- def sort_window_rows(rows, order_by)
1011
- return rows.dup if order_by.empty?
1012
-
1013
- rows.sort do |left, right|
1014
- comparison = 0
1015
- order_by.each do |item|
1016
- left_value = evaluate_expression(item.expression, left)
1017
- right_value = evaluate_expression(item.expression, right)
1018
- comparison = compare_sort_values(left_value, right_value)
1019
- comparison = -comparison if item.direction == :desc
1020
- break unless comparison.zero?
1021
- end
1022
- comparison
1023
- end
1024
- end
1025
-
1026
- def compare_sort_values(left, right)
1027
- return 0 if left.nil? && right.nil?
1028
- return -1 if left.nil?
1029
- return 1 if right.nil?
1030
-
1031
- left <=> right
1032
- end
1033
-
1034
- def window_value(expression, rows, index, order_by, frame = nil)
1035
- rows = window_frame_rows(rows, index, frame) if frame
1036
- name = expression.name.to_s.upcase
1037
- if %w[ROW_NUMBER RANK DENSE_RANK].include?(name)
1038
- return index + 1 if name == "ROW_NUMBER" || order_by.empty?
1039
-
1040
- keys = rows.map { |row| order_by.map { |item| evaluate_expression(item.expression, row) } }
1041
- current = keys[index]
1042
- return keys.index(current) + 1 if name == "RANK"
1043
-
1044
- return keys[0..index].uniq.index(current) + 1
1045
- end
1046
-
1047
- argument = expression.arguments.first
1048
- values = if argument.is_a?(SQL::AST::Star)
1049
- rows
1050
- else
1051
- rows.map { |row| evaluate_expression(argument, row) }.compact
1052
- end
1053
- values = values.uniq if expression.distinct
1054
- case name
1055
- when "COUNT" then values.size
1056
- when "SUM" then values.empty? ? nil : values.sum
1057
- when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
1058
- when "MIN" then values.min
1059
- when "MAX" then values.max
1060
- else
1061
- raise ExecutionError, "Unsupported window function: #{expression.name}"
1062
- end
1063
- end
1064
-
1065
- def window_frame_rows(rows, index, frame)
1066
- start_index = window_frame_index(frame[:start], index, rows.size, start: true)
1067
- finish = frame[:finish] || { kind: :current_row }
1068
- end_index = window_frame_index(finish, index, rows.size, start: false)
1069
- return [] if start_index > end_index
1070
-
1071
- rows[start_index..end_index] || []
1072
- end
1073
-
1074
- def window_frame_index(boundary, index, size, start:)
1075
- case boundary[:kind]
1076
- when :unbounded_preceding then 0
1077
- when :unbounded_following then size - 1
1078
- when :current_row then index
1079
- when :preceding then [index - boundary[:value], 0].max
1080
- when :following then [index + boundary[:value], size - 1].min
1081
- else raise ExecutionError, "Unsupported window frame boundary: #{boundary[:kind]}"
1082
- end
1083
- end
1084
-
1085
- def apply_aggregate_function(expression, rows)
1086
- argument = expression.arguments.first
1087
- values = if argument.is_a?(SQL::AST::Star)
1088
- rows
1089
- else
1090
- rows.map { |row| evaluate_expression(argument, row) }.compact
1091
- end
1092
- values = values.uniq if expression.distinct
1093
-
1094
- case expression.name.to_s.upcase
1095
- when "COUNT" then values.size
1096
- when "SUM" then values.empty? ? nil : values.sum
1097
- when "AVG" then values.empty? ? nil : values.sum / values.size.to_f
1098
- when "MIN" then values.min
1099
- when "MAX" then values.max
1100
- end
1101
- end
1102
-
1103
- def apply_aggregate(agg, rows)
1104
- values = rows.map { |row| row[agg.column.to_s] }.compact
1105
-
1106
- case agg.name.to_s.upcase
1107
- when "COUNT" then values.size
1108
- when "SUM" then values.sum
1109
- when "AVG" then values.empty? ? 0 : values.sum / values.size.to_f
1110
- when "MIN" then values.min
1111
- when "MAX" then values.max
1112
- else nil
1113
- end
1114
- end
1115
-
1116
- def sort_rows(rows, order_by)
1117
- rows.sort do |a, b|
1118
- comparison = 0
1119
- order_by.each do |order|
1120
- col = (order.is_a?(Hash) ? order[:column] : order.column).to_s
1121
- val_a = a[col]
1122
- val_b = b[col]
1123
-
1124
- if val_a.nil? && val_b.nil?
1125
- comparison = 0
1126
- elsif val_a.nil?
1127
- comparison = -1
1128
- elsif val_b.nil?
1129
- comparison = 1
1130
- else
1131
- comparison = val_a <=> val_b
1132
- end
1133
-
1134
- direction = order.is_a?(Hash) ? order[:direction] : order.direction
1135
- comparison = -comparison if direction == :desc
1136
- break unless comparison == 0
1137
- end
1138
- comparison
1139
- end
1140
- end
1141
-
1142
- def get_column_names(plan)
1143
- if plan.projections
1144
- plan.projections.map { |projection| projection_name(projection) }
1145
- else
1146
- @engine.table_columns(plan.table_name).map(&:name)
1147
- end
1148
- end
1149
-
1150
- def projection_name(projection)
1151
- return projection.to_s if projection.is_a?(String) || projection.is_a?(Symbol)
1152
-
1153
- expression = projection.respond_to?(:expression) ? projection.expression : projection
1154
- alias_name = if projection.respond_to?(:alias_name)
1155
- projection.alias_name
1156
- elsif projection.respond_to?(:alias)
1157
- projection.alias
1158
- end
1159
- return alias_name.to_s if alias_name
1160
-
1161
- return expression.name.to_s if expression.respond_to?(:name) && expression.name
1162
- return expression.to_sql if expression.respond_to?(:to_sql)
1163
-
1164
- expression.to_s
1165
- end
1166
-
1167
- def format_plan(plan)
1168
- lines = []
1169
- lines << "QUERY PLAN"
1170
- lines << "=" * 40
1171
-
1172
- if plan.scan_type == :index
1173
- lines << "Index Scan on #{plan.table_name} using #{plan.index.name}"
1174
- else
1175
- lines << "Seq Scan on #{plan.table_name}"
1176
- end
1177
-
1178
- if plan.predicate
1179
- lines << " Filter: #{plan.predicate.to_s}"
1180
- end
1181
-
1182
- if plan.order_by && plan.order_by.any?
1183
- order_str = plan.order_by.map { |o| "#{o.column} #{o.direction}" }.join(", ")
1184
- lines << " Order By: #{order_str}"
1185
- end
1186
-
1187
- if plan.limit
1188
- lines << " Limit: #{plan.limit}"
1189
- end
1190
-
1191
- if plan.offset
1192
- lines << " Offset: #{plan.offset}"
1193
- end
1194
-
1195
- lines << " Estimated Cost: #{plan.estimated_cost}"
1196
- lines << " Estimated Rows: #{plan.estimated_rows}"
1197
- lines.join("\n")
1198
- end
1199
- end
1200
- end
1201
- end
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