rubydb 0.1.0

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