rubydb 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
  3. data/.github/workflows/benchmark.yml +26 -26
  4. data/.github/workflows/compatibility.yml +63 -63
  5. data/.github/workflows/fuzz.yml +33 -33
  6. data/.github/workflows/lint.yml +21 -21
  7. data/.github/workflows/operations.yml +24 -24
  8. data/.github/workflows/production-validation.yml +111 -111
  9. data/.github/workflows/release.yml +77 -77
  10. data/.github/workflows/security.yml +39 -37
  11. data/.github/workflows/test.yml +26 -26
  12. data/.github/workflows/workload.yml +58 -58
  13. data/.gitignore +16 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +57 -27
  18. data/CODE_OF_CONDUCT.md +13 -13
  19. data/CONTRIBUTING.md +29 -29
  20. data/GOVERNANCE.md +16 -16
  21. data/Gemfile +18 -17
  22. data/Gemfile.lock +125 -71
  23. data/README.md +168 -12
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +76 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/accelerator/bin/SHA256SUMS +6 -0
  29. data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
  30. data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
  31. data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
  32. data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
  33. data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
  34. data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
  35. data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
  36. data/accelerator/go.mod +3 -0
  37. data/accelerator/internal/execution/aggregate.go +94 -0
  38. data/accelerator/internal/execution/distinct.go +22 -0
  39. data/accelerator/internal/execution/filter.go +73 -0
  40. data/accelerator/internal/execution/join.go +79 -0
  41. data/accelerator/internal/execution/operators.go +167 -0
  42. data/accelerator/internal/execution/scan.go +20 -0
  43. data/accelerator/internal/execution/sort.go +62 -0
  44. data/accelerator/internal/execution/types.go +136 -0
  45. data/accelerator/internal/execution/value.go +67 -0
  46. data/accelerator/internal/memory/arena.go +47 -0
  47. data/accelerator/internal/memory/reuse.go +22 -0
  48. data/accelerator/internal/metrics/registry.go +67 -0
  49. data/accelerator/internal/parallel/bounded_queue.go +56 -0
  50. data/accelerator/internal/parallel/scheduler.go +47 -0
  51. data/accelerator/internal/parallel/worker_pool.go +53 -0
  52. data/accelerator/internal/protocol/cancellation.go +48 -0
  53. data/accelerator/internal/protocol/columnar.go +263 -0
  54. data/accelerator/internal/protocol/frame.go +187 -0
  55. data/accelerator/internal/runtime/worker.go +521 -0
  56. data/accelerator/internal/storage/page_reader.go +81 -0
  57. data/accelerator/internal/storage/snapshot_scan.go +539 -0
  58. data/accelerator/internal/wal/checksum.go +13 -0
  59. data/accelerator/internal/wal/compression.go +41 -0
  60. data/accelerator/internal/wal/group_commit.go +24 -0
  61. data/accelerator/internal/wal/record_encoder.go +40 -0
  62. data/adapters/activerecord/Gemfile +11 -11
  63. data/adapters/activerecord/README.md +8 -3
  64. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +881 -879
  65. data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
  66. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
  67. data/adapters/ruby/README.md +18 -18
  68. data/adapters/sequel/README.md +11 -11
  69. data/config/monitoring/prometheus-alerts.yml +39 -39
  70. data/config/production.yml +36 -36
  71. data/docs/README.md +77 -71
  72. data/docs/architecture/concurrency.md +14 -14
  73. data/docs/architecture/current-state.md +125 -125
  74. data/docs/architecture/execution-engine.md +25 -25
  75. data/docs/architecture/go-accelerator.md +179 -0
  76. data/docs/architecture/indexes.md +19 -19
  77. data/docs/architecture/mvcc.md +19 -19
  78. data/docs/architecture/overview.md +13 -13
  79. data/docs/architecture/pages.md +11 -11
  80. data/docs/architecture/production-roadmap.md +82 -82
  81. data/docs/architecture/query-planner.md +20 -20
  82. data/docs/architecture/recovery.md +18 -18
  83. data/docs/architecture/sql-engine.md +12 -12
  84. data/docs/architecture/storage-engine.md +14 -14
  85. data/docs/architecture/transactions.md +10 -10
  86. data/docs/architecture/wal.md +28 -28
  87. data/docs/cli-cheatsheet.md +98 -98
  88. data/docs/cli.md +299 -275
  89. data/docs/contributing/architecture.md +9 -9
  90. data/docs/contributing/benchmarking.md +30 -14
  91. data/docs/contributing/development.md +16 -16
  92. data/docs/contributing/release-process.md +49 -49
  93. data/docs/contributing/testing.md +16 -16
  94. data/docs/debugging.md +229 -229
  95. data/docs/developer/branching.md +10 -10
  96. data/docs/developer/database-diff.md +10 -10
  97. data/docs/developer/local-development.md +49 -17
  98. data/docs/developer/snapshots.md +9 -9
  99. data/docs/developer/temporal-data.md +10 -10
  100. data/docs/developer-guide.md +297 -297
  101. data/docs/getting-started/first-database.md +16 -16
  102. data/docs/getting-started/first-query.md +13 -13
  103. data/docs/getting-started/installation.md +19 -19
  104. data/docs/getting-started/local-to-production.md +300 -300
  105. data/docs/getting-started/quickstart.md +17 -17
  106. data/docs/getting-started/rails.md +16 -16
  107. data/docs/hardening_backlog.md +93 -93
  108. data/docs/lessons-learned.md +112 -112
  109. data/docs/operations/backups.md +33 -33
  110. data/docs/operations/disaster-recovery.md +31 -31
  111. data/docs/operations/failover.md +30 -30
  112. data/docs/operations/monitoring.md +25 -25
  113. data/docs/operations/production-guide.md +295 -295
  114. data/docs/operations/production-runbook.md +45 -45
  115. data/docs/operations/replication.md +33 -33
  116. data/docs/operations/restore.md +6 -6
  117. data/docs/operations/runbook.md +34 -34
  118. data/docs/operations/upgrades.md +14 -14
  119. data/docs/operations/workload-testing.md +17 -17
  120. data/docs/production-readiness.md +118 -118
  121. data/docs/production_validation.md +150 -150
  122. data/docs/rails/active-record.md +11 -11
  123. data/docs/rails/compatibility-guide.md +90 -90
  124. data/docs/rails/database-yml.md +92 -92
  125. data/docs/rails/installation.md +17 -17
  126. data/docs/rails/migrations.md +17 -17
  127. data/docs/rails/production.md +82 -82
  128. data/docs/rails/troubleshooting.md +18 -18
  129. data/docs/release.md +25 -25
  130. data/docs/server/architecture.md +10 -10
  131. data/docs/server/authentication.md +10 -10
  132. data/docs/server/configuration.md +16 -16
  133. data/docs/server/connection-pooling.md +10 -10
  134. data/docs/server/deployment.md +10 -10
  135. data/docs/server/protocol.md +12 -12
  136. data/docs/sql/compatibility-guide.md +82 -82
  137. data/docs/sql/compatibility.md +39 -39
  138. data/docs/sql/data-types.md +10 -10
  139. data/docs/sql/functions.md +9 -9
  140. data/docs/sql/joins.md +9 -9
  141. data/docs/sql/operators.md +9 -9
  142. data/docs/sql/sqlite-compatibility.md +21 -21
  143. data/docs/sql/syntax.md +10 -10
  144. data/docs/sql/transactions.md +10 -10
  145. data/docs/troubleshooting.md +244 -244
  146. data/lessons/01-foundations.md +73 -0
  147. data/lessons/02-local-development.md +121 -0
  148. data/lessons/03-embedded-rubydb.md +99 -0
  149. data/lessons/04-rails-complex-apps.md +138 -0
  150. data/lessons/05-rubydb-production-server.md +237 -0
  151. data/lessons/06-postgresql-massive-apps.md +96 -0
  152. data/lessons/07-hybrid-microservices.md +179 -0
  153. data/lessons/08-migrations-backups-recovery.md +86 -0
  154. data/lessons/09-observability-security-scale.md +87 -0
  155. data/lessons/10-release-readiness.md +192 -0
  156. data/lessons/11-community-adapter.md +323 -0
  157. data/lessons/12-rails-ecommerce-pressure.md +263 -0
  158. data/lib/rubydb/accelerator/client.rb +451 -0
  159. data/lib/rubydb/accelerator/error.rb +22 -0
  160. data/lib/rubydb/accelerator/manager.rb +606 -0
  161. data/lib/rubydb/accelerator.rb +13 -0
  162. data/lib/rubydb/backup/archive.rb +332 -334
  163. data/lib/rubydb/backup/backup.rb +400 -401
  164. data/lib/rubydb/backup/incremental.rb +349 -353
  165. data/lib/rubydb/backup/restore.rb +289 -290
  166. data/lib/rubydb/backup/snapshot.rb +265 -267
  167. data/lib/rubydb/backup/verification.rb +276 -279
  168. data/lib/rubydb/branching/branch.rb +181 -181
  169. data/lib/rubydb/branching/branch_manager.rb +307 -311
  170. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  171. data/lib/rubydb/branching/checkout.rb +165 -166
  172. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  173. data/lib/rubydb/branching/diff.rb +137 -138
  174. data/lib/rubydb/branching/merge.rb +282 -285
  175. data/lib/rubydb/build_info.rb +15 -15
  176. data/lib/rubydb/catalog/catalog.rb +391 -391
  177. data/lib/rubydb/catalog/column.rb +112 -112
  178. data/lib/rubydb/catalog/constraint.rb +180 -180
  179. data/lib/rubydb/catalog/database.rb +184 -184
  180. data/lib/rubydb/catalog/index.rb +97 -97
  181. data/lib/rubydb/catalog/schema.rb +103 -103
  182. data/lib/rubydb/catalog/sequence.rb +90 -90
  183. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  184. data/lib/rubydb/catalog/table.rb +178 -178
  185. data/lib/rubydb/catalog/trigger.rb +102 -102
  186. data/lib/rubydb/catalog/view.rb +66 -66
  187. data/lib/rubydb/cli/application.rb +168 -163
  188. data/lib/rubydb/cli/commands/accelerator.rb +72 -0
  189. data/lib/rubydb/cli/commands/backup.rb +80 -81
  190. data/lib/rubydb/cli/commands/branch.rb +72 -72
  191. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  192. data/lib/rubydb/cli/commands/create.rb +58 -58
  193. data/lib/rubydb/cli/commands/diff.rb +76 -77
  194. data/lib/rubydb/cli/commands/doctor.rb +77 -74
  195. data/lib/rubydb/cli/commands/drop.rb +57 -57
  196. data/lib/rubydb/cli/commands/init.rb +101 -102
  197. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  198. data/lib/rubydb/cli/commands/merge.rb +63 -63
  199. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  200. data/lib/rubydb/cli/commands/restart.rb +42 -39
  201. data/lib/rubydb/cli/commands/restore.rb +121 -121
  202. data/lib/rubydb/cli/commands/shell.rb +365 -365
  203. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  204. data/lib/rubydb/cli/commands/start.rb +88 -82
  205. data/lib/rubydb/cli/commands/status.rb +96 -92
  206. data/lib/rubydb/cli/commands/stop.rb +47 -47
  207. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  208. data/lib/rubydb/cli/formatter.rb +221 -221
  209. data/lib/rubydb/cli/output.rb +168 -168
  210. data/lib/rubydb/client/client.rb +309 -304
  211. data/lib/rubydb/client/connection.rb +429 -415
  212. data/lib/rubydb/client/connection_pool.rb +168 -168
  213. data/lib/rubydb/client/connection_url.rb +96 -96
  214. data/lib/rubydb/client/prepared_statement.rb +60 -60
  215. data/lib/rubydb/client/result.rb +127 -123
  216. data/lib/rubydb/client/statement.rb +52 -52
  217. data/lib/rubydb/client/transaction.rb +130 -130
  218. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  219. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  220. data/lib/rubydb/concurrency/latch.rb +101 -101
  221. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  222. data/lib/rubydb/concurrency/mutex.rb +181 -183
  223. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  224. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  225. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  226. data/lib/rubydb/configuration/config.rb +170 -170
  227. data/lib/rubydb/configuration/defaults.rb +191 -179
  228. data/lib/rubydb/configuration/environment.rb +152 -152
  229. data/lib/rubydb/configuration/parser.rb +185 -185
  230. data/lib/rubydb/configuration/validation.rb +228 -221
  231. data/lib/rubydb/constants.rb +74 -74
  232. data/lib/rubydb/constraints/check.rb +181 -181
  233. data/lib/rubydb/constraints/constraint.rb +101 -101
  234. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  235. data/lib/rubydb/constraints/not_null.rb +64 -64
  236. data/lib/rubydb/constraints/primary_key.rb +99 -99
  237. data/lib/rubydb/constraints/unique.rb +106 -108
  238. data/lib/rubydb/constraints/validator.rb +349 -350
  239. data/lib/rubydb/errors/authentication_error.rb +10 -10
  240. data/lib/rubydb/errors/authorization_error.rb +23 -23
  241. data/lib/rubydb/errors/client_error.rb +10 -10
  242. data/lib/rubydb/errors/configuration_error.rb +10 -10
  243. data/lib/rubydb/errors/connection_error.rb +10 -10
  244. data/lib/rubydb/errors/constraint_error.rb +23 -23
  245. data/lib/rubydb/errors/corruption_error.rb +10 -10
  246. data/lib/rubydb/errors/database_error.rb +10 -10
  247. data/lib/rubydb/errors/error.rb +20 -20
  248. data/lib/rubydb/errors/execution_error.rb +10 -10
  249. data/lib/rubydb/errors/parser_error.rb +10 -10
  250. data/lib/rubydb/errors/recovery_error.rb +10 -10
  251. data/lib/rubydb/errors/replication_error.rb +10 -10
  252. data/lib/rubydb/errors/server_error.rb +6 -6
  253. data/lib/rubydb/errors/storage_error.rb +10 -10
  254. data/lib/rubydb/errors/transaction_error.rb +10 -10
  255. data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
  256. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  257. data/lib/rubydb/execution/cost_model.rb +72 -0
  258. data/lib/rubydb/execution/delete_executor.rb +110 -112
  259. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  260. data/lib/rubydb/execution/executor.rb +1544 -1188
  261. data/lib/rubydb/execution/expression.rb +191 -193
  262. data/lib/rubydb/execution/index_scan.rb +142 -142
  263. data/lib/rubydb/execution/insert_executor.rb +215 -217
  264. data/lib/rubydb/execution/join_executor.rb +243 -249
  265. data/lib/rubydb/execution/limit_executor.rb +83 -85
  266. data/lib/rubydb/execution/operator_selection.rb +57 -0
  267. data/lib/rubydb/execution/optimizer.rb +227 -215
  268. data/lib/rubydb/execution/physical_plan.rb +47 -0
  269. data/lib/rubydb/execution/plan.rb +355 -353
  270. data/lib/rubydb/execution/planner.rb +508 -536
  271. data/lib/rubydb/execution/predicate.rb +235 -235
  272. data/lib/rubydb/execution/scan.rb +49 -49
  273. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  274. data/lib/rubydb/execution/sort_executor.rb +194 -185
  275. data/lib/rubydb/execution/update_executor.rb +160 -162
  276. data/lib/rubydb/functions/aggregate.rb +70 -70
  277. data/lib/rubydb/functions/date_functions.rb +274 -278
  278. data/lib/rubydb/functions/function.rb +85 -85
  279. data/lib/rubydb/functions/json_functions.rb +231 -215
  280. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  281. data/lib/rubydb/functions/scalar.rb +52 -52
  282. data/lib/rubydb/functions/string_functions.rb +383 -383
  283. data/lib/rubydb/functions/system_functions.rb +258 -246
  284. data/lib/rubydb/history/as_of.rb +238 -238
  285. data/lib/rubydb/history/change.rb +105 -105
  286. data/lib/rubydb/history/history.rb +131 -131
  287. data/lib/rubydb/history/history_manager.rb +228 -229
  288. data/lib/rubydb/history/temporal_query.rb +202 -202
  289. data/lib/rubydb/history/timeline.rb +144 -144
  290. data/lib/rubydb/indexes/btree.rb +215 -186
  291. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  292. data/lib/rubydb/indexes/btree_node.rb +384 -385
  293. data/lib/rubydb/indexes/hash_index.rb +150 -150
  294. data/lib/rubydb/indexes/index.rb +71 -71
  295. data/lib/rubydb/indexes/index_manager.rb +408 -406
  296. data/lib/rubydb/indexes/index_scan.rb +466 -470
  297. data/lib/rubydb/migrations/migration.rb +253 -254
  298. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  299. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  300. data/lib/rubydb/migrations/migration_version.rb +71 -71
  301. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  302. data/lib/rubydb/migrations/schema_version.rb +64 -64
  303. data/lib/rubydb/monitoring/events.rb +155 -160
  304. data/lib/rubydb/monitoring/health.rb +216 -222
  305. data/lib/rubydb/monitoring/logger.rb +188 -193
  306. data/lib/rubydb/monitoring/metrics.rb +363 -359
  307. data/lib/rubydb/monitoring/performance.rb +176 -176
  308. data/lib/rubydb/monitoring/statistics.rb +168 -170
  309. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  310. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  311. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  312. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  313. data/lib/rubydb/mvcc/version.rb +106 -106
  314. data/lib/rubydb/mvcc/version_store.rb +396 -398
  315. data/lib/rubydb/mvcc/visibility.rb +107 -109
  316. data/lib/rubydb/protocol/capabilities.rb +125 -125
  317. data/lib/rubydb/protocol/decoder.rb +142 -145
  318. data/lib/rubydb/protocol/encoder.rb +131 -136
  319. data/lib/rubydb/protocol/handshake.rb +306 -305
  320. data/lib/rubydb/protocol/message.rb +121 -121
  321. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  322. data/lib/rubydb/protocol/protocol.rb +276 -277
  323. data/lib/rubydb/protocol/version.rb +54 -54
  324. data/lib/rubydb/rails/adapter.rb +245 -239
  325. data/lib/rubydb/rails/connection.rb +312 -314
  326. data/lib/rubydb/rails/database_statements.rb +122 -122
  327. data/lib/rubydb/rails/migration.rb +131 -131
  328. data/lib/rubydb/rails/quoting.rb +109 -109
  329. data/lib/rubydb/rails/result.rb +117 -117
  330. data/lib/rubydb/rails/schema_statements.rb +339 -339
  331. data/lib/rubydb/rails/transaction.rb +105 -105
  332. data/lib/rubydb/rails/type.rb +126 -126
  333. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  334. data/lib/rubydb/recovery/consistency.rb +457 -467
  335. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  336. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  337. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  338. data/lib/rubydb/recovery/redo.rb +235 -237
  339. data/lib/rubydb/recovery/undo.rb +204 -206
  340. data/lib/rubydb/replication/failover.rb +5 -5
  341. data/lib/rubydb/replication/fencing.rb +63 -63
  342. data/lib/rubydb/replication/primary.rb +461 -450
  343. data/lib/rubydb/replication/replica.rb +382 -384
  344. data/lib/rubydb/replication/replication_log.rb +194 -200
  345. data/lib/rubydb/replication/replication_manager.rb +307 -308
  346. data/lib/rubydb/replication/replication_slot.rb +293 -295
  347. data/lib/rubydb/replication/replication_stream.rb +198 -201
  348. data/lib/rubydb/rubydb.rb +570 -560
  349. data/lib/rubydb/security/access_control.rb +252 -254
  350. data/lib/rubydb/security/audit_log.rb +209 -213
  351. data/lib/rubydb/security/authentication.rb +302 -302
  352. data/lib/rubydb/security/authorization.rb +282 -282
  353. data/lib/rubydb/security/credentials.rb +192 -196
  354. data/lib/rubydb/security/password.rb +205 -215
  355. data/lib/rubydb/security/permissions.rb +74 -74
  356. data/lib/rubydb/security/role.rb +99 -101
  357. data/lib/rubydb/security/user.rb +86 -86
  358. data/lib/rubydb/server/connection.rb +383 -366
  359. data/lib/rubydb/server/connection_pool.rb +193 -193
  360. data/lib/rubydb/server/lifecycle.rb +227 -228
  361. data/lib/rubydb/server/listener.rb +139 -136
  362. data/lib/rubydb/server/request_handler.rb +277 -276
  363. data/lib/rubydb/server/server.rb +363 -364
  364. data/lib/rubydb/server/session.rb +416 -369
  365. data/lib/rubydb/server/worker.rb +206 -210
  366. data/lib/rubydb/server/worker_pool.rb +168 -168
  367. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  368. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  369. data/lib/rubydb/sql/ast/commit.rb +37 -37
  370. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  371. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  372. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  373. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  374. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  375. data/lib/rubydb/sql/ast/delete.rb +54 -54
  376. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  377. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  378. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  379. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  380. data/lib/rubydb/sql/ast/explain.rb +64 -64
  381. data/lib/rubydb/sql/ast/expression.rb +617 -604
  382. data/lib/rubydb/sql/ast/insert.rb +66 -66
  383. data/lib/rubydb/sql/ast/node.rb +42 -42
  384. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  385. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  386. data/lib/rubydb/sql/ast/select.rb +88 -88
  387. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  388. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  389. data/lib/rubydb/sql/ast/update.rb +88 -88
  390. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  391. data/lib/rubydb/sql/ast/view.rb +38 -32
  392. data/lib/rubydb/sql/ast/with.rb +32 -32
  393. data/lib/rubydb/sql/grammar.rb +86 -86
  394. data/lib/rubydb/sql/keywords.rb +156 -156
  395. data/lib/rubydb/sql/lexer.rb +209 -214
  396. data/lib/rubydb/sql/operators.rb +100 -100
  397. data/lib/rubydb/sql/parser.rb +1167 -1170
  398. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  399. data/lib/rubydb/sql/planner/binder.rb +537 -550
  400. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  401. data/lib/rubydb/sql/token.rb +210 -210
  402. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  403. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  404. data/lib/rubydb/storage/database_lock.rb +74 -74
  405. data/lib/rubydb/storage/deserializer.rb +332 -342
  406. data/lib/rubydb/storage/engine.rb +2409 -2330
  407. data/lib/rubydb/storage/file_manager.rb +191 -187
  408. data/lib/rubydb/storage/free_space_map.rb +79 -81
  409. data/lib/rubydb/storage/page.rb +92 -94
  410. data/lib/rubydb/storage/page_allocator.rb +852 -855
  411. data/lib/rubydb/storage/page_header.rb +63 -67
  412. data/lib/rubydb/storage/page_manager.rb +127 -131
  413. data/lib/rubydb/storage/record.rb +58 -58
  414. data/lib/rubydb/storage/row.rb +78 -78
  415. data/lib/rubydb/storage/serializer.rb +51 -51
  416. data/lib/rubydb/storage/snapshot_reader.rb +167 -0
  417. data/lib/rubydb/storage/storage_layout.rb +151 -151
  418. data/lib/rubydb/storage/storage_manager.rb +114 -114
  419. data/lib/rubydb/storage/tuple.rb +458 -461
  420. data/lib/rubydb/storage/visibility_map.rb +964 -973
  421. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  422. data/lib/rubydb/transactions/isolation.rb +98 -98
  423. data/lib/rubydb/transactions/lock.rb +76 -76
  424. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  425. data/lib/rubydb/transactions/savepoint.rb +142 -143
  426. data/lib/rubydb/transactions/transaction.rb +214 -215
  427. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  428. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  429. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  430. data/lib/rubydb/types/bigint.rb +36 -36
  431. data/lib/rubydb/types/blob.rb +37 -37
  432. data/lib/rubydb/types/boolean.rb +34 -34
  433. data/lib/rubydb/types/date.rb +39 -39
  434. data/lib/rubydb/types/decimal.rb +48 -48
  435. data/lib/rubydb/types/float.rb +34 -34
  436. data/lib/rubydb/types/integer.rb +36 -36
  437. data/lib/rubydb/types/json.rb +41 -41
  438. data/lib/rubydb/types/null.rb +34 -34
  439. data/lib/rubydb/types/smallint.rb +36 -36
  440. data/lib/rubydb/types/text.rb +37 -37
  441. data/lib/rubydb/types/time.rb +46 -46
  442. data/lib/rubydb/types/timestamp.rb +39 -39
  443. data/lib/rubydb/types/type.rb +119 -119
  444. data/lib/rubydb/types/uuid.rb +47 -47
  445. data/lib/rubydb/types/varchar.rb +37 -37
  446. data/lib/rubydb/version.rb +32 -32
  447. data/lib/rubydb/wal/archive.rb +207 -193
  448. data/lib/rubydb/wal/checkpoint.rb +181 -183
  449. data/lib/rubydb/wal/lsn.rb +94 -94
  450. data/lib/rubydb/wal/reader.rb +259 -260
  451. data/lib/rubydb/wal/record.rb +105 -105
  452. data/lib/rubydb/wal/segment.rb +193 -193
  453. data/lib/rubydb/wal/wal.rb +481 -452
  454. data/lib/rubydb/wal/writer.rb +236 -236
  455. data/lib/rubydb.rb +7 -7
  456. data/packaging/docker/docker-compose.failover.yml +43 -43
  457. data/packaging/homebrew/rubydb.rb +19 -19
  458. data/rubydb.gemspec +70 -57
  459. data/scripts/benchmark +7 -7
  460. data/scripts/build_accelerator +49 -0
  461. data/scripts/durability_drill +37 -37
  462. data/scripts/fuzz +63 -63
  463. data/scripts/release +77 -42
  464. data/scripts/release_check +43 -43
  465. data/scripts/replication_failover_drill +268 -250
  466. data/scripts/replication_network_failover_drill +287 -255
  467. data/scripts/restore_drill +45 -45
  468. data/scripts/security +45 -0
  469. metadata +102 -1
@@ -1,452 +1,481 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "time"
5
-
6
- # Import all WAL components
7
- require_relative "lsn"
8
- require_relative "record"
9
- require_relative "segment"
10
- require_relative "writer"
11
- require_relative "reader"
12
- require_relative "checkpoint"
13
- require_relative "archive"
14
- require_relative "../errors/recovery_error"
15
-
16
- module RubyDB
17
- module WAL
18
- # WAL - Main Write-Ahead Log interface
19
- class WAL
20
- attr_reader :writer, :reader, :checkpoint, :archive, :stats
21
- attr_reader :wal_dir, :current_lsn
22
-
23
- def initialize(wal_dir, config = {})
24
- @wal_dir = wal_dir
25
- @config = config
26
- @engine = config[:engine]
27
- @stats = {
28
- writes: 0,
29
- reads: 0,
30
- checkpoints: 0,
31
- archives: 0,
32
- restores: 0,
33
- errors: 0,
34
- total_write_time_ms: 0,
35
- total_read_time_ms: 0,
36
- avg_write_time_ms: 0,
37
- avg_read_time_ms: 0
38
- }
39
- @lock = Mutex.new
40
- @running = false
41
- @shutdown = false
42
- @current_lsn = LSN.null
43
-
44
- # Create WAL directory
45
- FileUtils.mkdir_p(wal_dir) unless Dir.exist?(wal_dir)
46
-
47
- begin
48
- # Initialize components
49
- initialize_components(config)
50
-
51
- # Start checkpoint if configured
52
- if config[:auto_checkpoint] != false
53
- @checkpoint.start
54
- end
55
-
56
- # Start recovery if needed
57
- if config[:recovery] != false
58
- recover
59
- end
60
-
61
- @running = true
62
- rescue StandardError
63
- # Recovery can fail after the writer has opened a segment. Close all
64
- # partially initialized components before re-raising so callers can
65
- # replace/remove a failed WAL directory on every supported platform.
66
- @checkpoint&.stop rescue nil
67
- @writer&.shutdown rescue nil
68
- @reader&.close rescue nil
69
- raise
70
- end
71
- end
72
-
73
- def attach_engine(engine)
74
- @lock.synchronize { @engine = engine }
75
- self
76
- end
77
-
78
- def write(record)
79
- @lock.synchronize do
80
- start_time = Time.now
81
- @stats[:writes] += 1
82
-
83
- begin
84
- lsn = @writer.write_record(record)
85
- @current_lsn = lsn
86
-
87
- elapsed_ms = (Time.now - start_time) * 1000
88
- @stats[:total_write_time_ms] += elapsed_ms
89
- @stats[:avg_write_time_ms] = @stats[:total_write_time_ms] / @stats[:writes]
90
-
91
- lsn
92
- rescue => e
93
- @stats[:errors] += 1
94
- raise
95
- end
96
- end
97
- end
98
-
99
- def write_batch(records)
100
- @lock.synchronize do
101
- start_time = Time.now
102
- @stats[:writes] += records.size
103
-
104
- begin
105
- result = @writer.write_batch(records)
106
- @current_lsn = result[:end_lsn] if result[:end_lsn]
107
-
108
- elapsed_ms = (Time.now - start_time) * 1000
109
- @stats[:total_write_time_ms] += elapsed_ms
110
- @stats[:avg_write_time_ms] = @stats[:total_write_time_ms] / @stats[:writes]
111
-
112
- result
113
- rescue => e
114
- @stats[:errors] += 1
115
- raise
116
- end
117
- end
118
- end
119
-
120
- def read_all
121
- @lock.synchronize do
122
- start_time = Time.now
123
- @stats[:reads] += 1
124
-
125
- begin
126
- records = @reader.read_all
127
-
128
- elapsed_ms = (Time.now - start_time) * 1000
129
- @stats[:total_read_time_ms] += elapsed_ms
130
- @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
131
-
132
- records
133
- rescue => e
134
- @stats[:errors] += 1
135
- raise
136
- end
137
- end
138
- end
139
-
140
- def read_from(lsn)
141
- @lock.synchronize do
142
- start_time = Time.now
143
- @stats[:reads] += 1
144
-
145
- begin
146
- records = @reader.read_from_lsn(lsn)
147
-
148
- elapsed_ms = (Time.now - start_time) * 1000
149
- @stats[:total_read_time_ms] += elapsed_ms
150
- @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
151
-
152
- records
153
- rescue => e
154
- @stats[:errors] += 1
155
- raise
156
- end
157
- end
158
- end
159
-
160
- def read_range(start_lsn, end_lsn)
161
- @lock.synchronize do
162
- start_time = Time.now
163
- @stats[:reads] += 1
164
-
165
- begin
166
- records = @reader.read_range(start_lsn, end_lsn)
167
-
168
- elapsed_ms = (Time.now - start_time) * 1000
169
- @stats[:total_read_time_ms] += elapsed_ms
170
- @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
171
-
172
- records
173
- rescue => e
174
- @stats[:errors] += 1
175
- raise
176
- end
177
- end
178
- end
179
-
180
- def create_checkpoint(force = false)
181
- @lock.synchronize do
182
- start_time = Time.now
183
- @stats[:checkpoints] += 1
184
-
185
- begin
186
- result = @checkpoint.create_checkpoint(force)
187
- @stats[:checkpoints] += 1 if result
188
- result
189
- rescue => e
190
- @stats[:errors] += 1
191
- raise
192
- end
193
- end
194
- end
195
-
196
- def archive_segment(segment_id)
197
- @lock.synchronize do
198
- @stats[:archives] += 1
199
-
200
- begin
201
- segment = Segment.new(segment_id, @wal_dir)
202
- if segment.exists?
203
- segment.open
204
- result = @archive.archive_segment(segment)
205
- segment.close
206
- result
207
- else
208
- false
209
- end
210
- rescue => e
211
- @stats[:errors] += 1
212
- raise
213
- end
214
- end
215
- end
216
-
217
- def archive_all_segments
218
- @lock.synchronize do
219
- archived = 0
220
-
221
- Dir.glob(File.join(@wal_dir, "wal_*.log")).each do |file_path|
222
- if file_path =~ /wal_(\d+)\.log$/
223
- segment_id = $1.to_i
224
- if archive_segment(segment_id)
225
- archived += 1
226
- end
227
- end
228
- end
229
-
230
- archived
231
- end
232
- end
233
-
234
- def restore_from_archive(segment_id)
235
- @lock.synchronize do
236
- @stats[:restores] += 1
237
-
238
- begin
239
- result = @archive.restore_archive(segment_id)
240
- # Reload reader after restore
241
- @reader.reload if result
242
- result
243
- rescue => e
244
- @stats[:errors] += 1
245
- raise
246
- end
247
- end
248
- end
249
-
250
- def restore_latest_archive
251
- @lock.synchronize do
252
- archives = @archive.list_archives
253
- return false if archives.empty?
254
-
255
- latest = archives.max_by { |a| a[:segment_id] }
256
- restore_from_archive(latest[:segment_id])
257
- end
258
- end
259
-
260
- def recover
261
- @lock.synchronize do
262
- # Find the latest checkpoint
263
- checkpoint_lsn = @checkpoint.restore_checkpoint(@reader)
264
-
265
- if checkpoint_lsn
266
- # Read records from the checkpoint marker. The reader includes
267
- # the starting frame; the marker is ignored by replay and all
268
- # subsequent WAL records are applied.
269
- records = @reader.read_from_lsn(checkpoint_lsn)
270
-
271
- # Replay committed transactions
272
- committed_transactions = Set.new
273
- records.each do |record|
274
- case record.type
275
- when :commit
276
- committed_transactions.add(record.transaction_id)
277
- when :prepare
278
- # Handle prepared transactions
279
- if can_commit_prepared?(record)
280
- committed_transactions.add(record.transaction_id)
281
- end
282
- end
283
- end
284
-
285
- # REDO committed transactions
286
- redo_records = records.select do |r|
287
- committed_transactions.include?(r.transaction_id) &&
288
- [:insert, :update, :delete, :create_table, :drop_table].include?(r.type)
289
- end
290
-
291
- redo_records.each do |record|
292
- replay_record(record)
293
- end
294
-
295
- # UNDO uncommitted transactions
296
- uncommitted = records.select do |r|
297
- !committed_transactions.include?(r.transaction_id) &&
298
- [:insert, :update, :delete].include?(r.type)
299
- end
300
-
301
- uncommitted.reverse_each do |record|
302
- undo_record(record)
303
- end
304
- end
305
-
306
- true
307
- end
308
- end
309
-
310
- def flush
311
- @lock.synchronize do
312
- @writer.flush
313
- end
314
- end
315
-
316
- def sync
317
- @lock.synchronize do
318
- @writer.sync
319
- end
320
- end
321
-
322
- def shutdown(wait = true)
323
- @lock.synchronize do
324
- @shutdown = true
325
- @running = false
326
-
327
- @checkpoint.stop if @checkpoint
328
- @writer.shutdown(wait)
329
- @reader.close if @reader
330
- end
331
- end
332
-
333
- def running?
334
- @running
335
- end
336
-
337
- def stats
338
- @lock.synchronize do
339
- writer_stats = @writer.stats rescue {}
340
- reader_stats = @reader.stats rescue {}
341
- checkpoint_stats = @checkpoint.stats rescue {}
342
- archive_stats = @archive.stats rescue {}
343
-
344
- @stats.merge({
345
- writer: writer_stats,
346
- reader: reader_stats,
347
- checkpoint: checkpoint_stats,
348
- archive: archive_stats,
349
- current_lsn: @current_lsn.to_s,
350
- running: @running,
351
- wal_dir: @wal_dir
352
- })
353
- end
354
- end
355
-
356
- private
357
-
358
- def initialize_components(config)
359
- @writer = Writer.new(@wal_dir, config)
360
- @reader = Reader.new(@wal_dir, config)
361
- @checkpoint = Checkpoint.new(@writer, config)
362
- @archive = Archive.new(@wal_dir, config[:archive_dir], config)
363
- end
364
-
365
- def can_commit_prepared?(record)
366
- data = record&.data
367
- resources = data && data[:resources]
368
- return true unless resources
369
-
370
- Array(resources).all? do |resource|
371
- case resource
372
- when String then File.exist?(resource)
373
- when Hash
374
- path = resource[:path] || resource["path"] || resource[:file] || resource["file"]
375
- path && File.exist?(path)
376
- else
377
- resource.respond_to?(:available?) && resource.available?
378
- end
379
- end
380
- end
381
-
382
- def replay_record(record)
383
- engine = @engine
384
- raise RubyDB::RecoveryError, "WAL replay requires an attached engine" unless engine
385
- data = record.data
386
-
387
- case record.type
388
- when :insert
389
- # Insert row
390
- engine.insert_row(
391
- data[:table],
392
- data[:columns],
393
- data[:values]
394
- )
395
- when :update
396
- # Update row
397
- engine.update_row(
398
- data[:table],
399
- data[:row_id],
400
- data[:values]
401
- )
402
- when :delete
403
- # Delete row
404
- engine.delete_row(
405
- data[:table],
406
- data[:row_id]
407
- )
408
- when :create_table
409
- # Create table
410
- engine.create_table(
411
- data[:table_name],
412
- data[:columns]
413
- )
414
- when :drop_table
415
- # Drop table
416
- engine.drop_table(
417
- data[:table_name]
418
- )
419
- end
420
- end
421
-
422
- def undo_record(record)
423
- engine = @engine
424
- raise RubyDB::RecoveryError, "WAL undo requires an attached engine" unless engine
425
- data = record.data
426
-
427
- case record.type
428
- when :insert
429
- # Delete the inserted row
430
- engine.delete_row(
431
- data[:table],
432
- data[:row_id]
433
- )
434
- when :update
435
- # Restore old values
436
- engine.update_row(
437
- data[:table],
438
- data[:row_id],
439
- data[:old_values]
440
- )
441
- when :delete
442
- # Reinsert the deleted row
443
- engine.insert_row(
444
- data[:table],
445
- data[:columns],
446
- data[:row_data]
447
- )
448
- end
449
- end
450
- end
451
- end
452
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+
6
+ # Import all WAL components
7
+ require_relative "lsn"
8
+ require_relative "record"
9
+ require_relative "segment"
10
+ require_relative "writer"
11
+ require_relative "reader"
12
+ require_relative "checkpoint"
13
+ require_relative "archive"
14
+ require_relative "../errors/recovery_error"
15
+
16
+ module RubyDB
17
+ module WAL
18
+ # WAL - Main Write-Ahead Log interface
19
+ class WAL
20
+ attr_reader :writer, :reader, :checkpoint, :archive, :stats
21
+ attr_reader :wal_dir, :current_lsn
22
+
23
+ def initialize(wal_dir, config = {})
24
+ @wal_dir = wal_dir
25
+ @config = config
26
+ @engine = config[:engine]
27
+ @stats = {
28
+ writes: 0,
29
+ reads: 0,
30
+ checkpoints: 0,
31
+ archives: 0,
32
+ restores: 0,
33
+ errors: 0,
34
+ total_write_time_ms: 0,
35
+ total_read_time_ms: 0,
36
+ avg_write_time_ms: 0,
37
+ avg_read_time_ms: 0
38
+ }
39
+ @lock = Mutex.new
40
+ @running = false
41
+ @shutdown = false
42
+ @current_lsn = LSN.null
43
+
44
+ # Create WAL directory
45
+ FileUtils.mkdir_p(wal_dir) unless Dir.exist?(wal_dir)
46
+
47
+ begin
48
+ # Initialize components
49
+ initialize_components(config)
50
+
51
+ # Start checkpoint if configured
52
+ if config[:auto_checkpoint] != false
53
+ @checkpoint.start
54
+ end
55
+
56
+ # Start recovery if needed
57
+ if config[:recovery] != false
58
+ recover
59
+ end
60
+
61
+ @running = true
62
+ rescue
63
+ # Recovery can fail after the writer has opened a segment. Close all
64
+ # partially initialized components before re-raising so callers can
65
+ # replace/remove a failed WAL directory on every supported platform.
66
+ begin
67
+ @checkpoint&.stop
68
+ rescue
69
+ nil
70
+ end
71
+ begin
72
+ @writer&.shutdown
73
+ rescue
74
+ nil
75
+ end
76
+ begin
77
+ @reader&.close
78
+ rescue
79
+ nil
80
+ end
81
+ raise
82
+ end
83
+ end
84
+
85
+ def attach_engine(engine)
86
+ @lock.synchronize { @engine = engine }
87
+ self
88
+ end
89
+
90
+ def write(record)
91
+ @lock.synchronize do
92
+ start_time = Time.now
93
+ @stats[:writes] += 1
94
+
95
+ begin
96
+ lsn = @writer.write_record(record)
97
+ @current_lsn = lsn
98
+
99
+ elapsed_ms = (Time.now - start_time) * 1000
100
+ @stats[:total_write_time_ms] += elapsed_ms
101
+ @stats[:avg_write_time_ms] = @stats[:total_write_time_ms] / @stats[:writes]
102
+
103
+ lsn
104
+ rescue
105
+ @stats[:errors] += 1
106
+ raise
107
+ end
108
+ end
109
+ end
110
+
111
+ def write_batch(records)
112
+ @lock.synchronize do
113
+ start_time = Time.now
114
+ @stats[:writes] += records.size
115
+
116
+ begin
117
+ result = @writer.write_batch(records)
118
+ @current_lsn = result[:end_lsn] if result[:end_lsn]
119
+
120
+ elapsed_ms = (Time.now - start_time) * 1000
121
+ @stats[:total_write_time_ms] += elapsed_ms
122
+ @stats[:avg_write_time_ms] = @stats[:total_write_time_ms] / @stats[:writes]
123
+
124
+ result
125
+ rescue
126
+ @stats[:errors] += 1
127
+ raise
128
+ end
129
+ end
130
+ end
131
+
132
+ def read_all
133
+ @lock.synchronize do
134
+ start_time = Time.now
135
+ @stats[:reads] += 1
136
+
137
+ begin
138
+ records = @reader.read_all
139
+
140
+ elapsed_ms = (Time.now - start_time) * 1000
141
+ @stats[:total_read_time_ms] += elapsed_ms
142
+ @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
143
+
144
+ records
145
+ rescue
146
+ @stats[:errors] += 1
147
+ raise
148
+ end
149
+ end
150
+ end
151
+
152
+ def read_from(lsn)
153
+ @lock.synchronize do
154
+ start_time = Time.now
155
+ @stats[:reads] += 1
156
+
157
+ begin
158
+ records = @reader.read_from_lsn(lsn)
159
+
160
+ elapsed_ms = (Time.now - start_time) * 1000
161
+ @stats[:total_read_time_ms] += elapsed_ms
162
+ @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
163
+
164
+ records
165
+ rescue
166
+ @stats[:errors] += 1
167
+ raise
168
+ end
169
+ end
170
+ end
171
+
172
+ def read_range(start_lsn, end_lsn)
173
+ @lock.synchronize do
174
+ start_time = Time.now
175
+ @stats[:reads] += 1
176
+
177
+ begin
178
+ records = @reader.read_range(start_lsn, end_lsn)
179
+
180
+ elapsed_ms = (Time.now - start_time) * 1000
181
+ @stats[:total_read_time_ms] += elapsed_ms
182
+ @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
183
+
184
+ records
185
+ rescue
186
+ @stats[:errors] += 1
187
+ raise
188
+ end
189
+ end
190
+ end
191
+
192
+ def create_checkpoint(force = false)
193
+ @lock.synchronize do
194
+ Time.now
195
+ @stats[:checkpoints] += 1
196
+
197
+ begin
198
+ result = @checkpoint.create_checkpoint(force)
199
+ @stats[:checkpoints] += 1 if result
200
+ result
201
+ rescue
202
+ @stats[:errors] += 1
203
+ raise
204
+ end
205
+ end
206
+ end
207
+
208
+ def archive_segment(segment_id)
209
+ @lock.synchronize do
210
+ @stats[:archives] += 1
211
+
212
+ begin
213
+ segment = Segment.new(segment_id, @wal_dir)
214
+ if segment.exists?
215
+ segment.open
216
+ result = @archive.archive_segment(segment)
217
+ segment.close
218
+ result
219
+ else
220
+ false
221
+ end
222
+ rescue
223
+ @stats[:errors] += 1
224
+ raise
225
+ end
226
+ end
227
+ end
228
+
229
+ def archive_all_segments
230
+ @lock.synchronize do
231
+ archived = 0
232
+
233
+ Dir.glob(File.join(@wal_dir, "wal_*.log")).each do |file_path|
234
+ if file_path =~ /wal_(\d+)\.log$/
235
+ segment_id = $1.to_i
236
+ if archive_segment(segment_id)
237
+ archived += 1
238
+ end
239
+ end
240
+ end
241
+
242
+ archived
243
+ end
244
+ end
245
+
246
+ def restore_from_archive(segment_id)
247
+ @lock.synchronize do
248
+ @stats[:restores] += 1
249
+
250
+ begin
251
+ result = @archive.restore_archive(segment_id)
252
+ # Reload reader after restore
253
+ @reader.reload if result
254
+ result
255
+ rescue
256
+ @stats[:errors] += 1
257
+ raise
258
+ end
259
+ end
260
+ end
261
+
262
+ def restore_latest_archive
263
+ @lock.synchronize do
264
+ archives = @archive.list_archives
265
+ return false if archives.empty?
266
+
267
+ latest = archives.max_by { |a| a[:segment_id] }
268
+ restore_from_archive(latest[:segment_id])
269
+ end
270
+ end
271
+
272
+ def recover
273
+ @lock.synchronize do
274
+ # Find the latest checkpoint
275
+ checkpoint_lsn = @checkpoint.restore_checkpoint(@reader)
276
+
277
+ if checkpoint_lsn
278
+ # Read records from the checkpoint marker. The reader includes
279
+ # the starting frame; the marker is ignored by replay and all
280
+ # subsequent WAL records are applied.
281
+ records = @reader.read_from_lsn(checkpoint_lsn)
282
+
283
+ # Replay committed transactions
284
+ committed_transactions = Set.new
285
+ records.each do |record|
286
+ case record.type
287
+ when :commit
288
+ committed_transactions.add(record.transaction_id)
289
+ when :prepare
290
+ # Handle prepared transactions
291
+ if can_commit_prepared?(record)
292
+ committed_transactions.add(record.transaction_id)
293
+ end
294
+ end
295
+ end
296
+
297
+ # REDO committed transactions
298
+ redo_records = records.select do |r|
299
+ committed_transactions.include?(r.transaction_id) &&
300
+ [:insert, :update, :delete, :create_table, :drop_table].include?(r.type)
301
+ end
302
+
303
+ redo_records.each do |record|
304
+ replay_record(record)
305
+ end
306
+
307
+ # UNDO uncommitted transactions
308
+ uncommitted = records.select do |r|
309
+ !committed_transactions.include?(r.transaction_id) &&
310
+ [:insert, :update, :delete].include?(r.type)
311
+ end
312
+
313
+ uncommitted.reverse_each do |record|
314
+ undo_record(record)
315
+ end
316
+ end
317
+
318
+ true
319
+ end
320
+ end
321
+
322
+ def flush
323
+ @lock.synchronize do
324
+ @writer.flush
325
+ end
326
+ end
327
+
328
+ def sync
329
+ @lock.synchronize do
330
+ @writer.sync
331
+ end
332
+ end
333
+
334
+ def shutdown(wait = true)
335
+ @lock.synchronize do
336
+ @shutdown = true
337
+ @running = false
338
+
339
+ @checkpoint&.stop
340
+ @writer.shutdown(wait)
341
+ @reader&.close
342
+ @archive&.close
343
+ end
344
+ end
345
+
346
+ def running?
347
+ @running
348
+ end
349
+
350
+ def stats
351
+ @lock.synchronize do
352
+ writer_stats = begin
353
+ @writer.stats
354
+ rescue
355
+ {}
356
+ end
357
+ reader_stats = begin
358
+ @reader.stats
359
+ rescue
360
+ {}
361
+ end
362
+ checkpoint_stats = begin
363
+ @checkpoint.stats
364
+ rescue
365
+ {}
366
+ end
367
+ archive_stats = begin
368
+ @archive.stats
369
+ rescue
370
+ {}
371
+ end
372
+
373
+ @stats.merge({
374
+ writer: writer_stats,
375
+ reader: reader_stats,
376
+ checkpoint: checkpoint_stats,
377
+ archive: archive_stats,
378
+ current_lsn: @current_lsn.to_s,
379
+ running: @running,
380
+ wal_dir: @wal_dir
381
+ })
382
+ end
383
+ end
384
+
385
+ private
386
+
387
+ def initialize_components(config)
388
+ @writer = Writer.new(@wal_dir, config)
389
+ @reader = Reader.new(@wal_dir, config)
390
+ @checkpoint = Checkpoint.new(@writer, config)
391
+ @archive = Archive.new(@wal_dir, config[:archive_dir], config)
392
+ end
393
+
394
+ def can_commit_prepared?(record)
395
+ data = record&.data
396
+ resources = data && data[:resources]
397
+ return true unless resources
398
+
399
+ Array(resources).all? do |resource|
400
+ case resource
401
+ when String then File.exist?(resource)
402
+ when Hash
403
+ path = resource[:path] || resource["path"] || resource[:file] || resource["file"]
404
+ path && File.exist?(path)
405
+ else
406
+ resource.respond_to?(:available?) && resource.available?
407
+ end
408
+ end
409
+ end
410
+
411
+ def replay_record(record)
412
+ engine = @engine
413
+ raise RubyDB::RecoveryError, "WAL replay requires an attached engine" unless engine
414
+ data = record.data
415
+
416
+ case record.type
417
+ when :insert
418
+ # Insert row
419
+ engine.insert_row(
420
+ data[:table],
421
+ data[:columns],
422
+ data[:values]
423
+ )
424
+ when :update
425
+ # Update row
426
+ engine.update_row(
427
+ data[:table],
428
+ data[:row_id],
429
+ data[:values]
430
+ )
431
+ when :delete
432
+ # Delete row
433
+ engine.delete_row(
434
+ data[:table],
435
+ data[:row_id]
436
+ )
437
+ when :create_table
438
+ # Create table
439
+ engine.create_table(
440
+ data[:table_name],
441
+ data[:columns]
442
+ )
443
+ when :drop_table
444
+ # Drop table
445
+ engine.drop_table(
446
+ data[:table_name]
447
+ )
448
+ end
449
+ end
450
+
451
+ def undo_record(record)
452
+ engine = @engine
453
+ raise RubyDB::RecoveryError, "WAL undo requires an attached engine" unless engine
454
+ data = record.data
455
+
456
+ case record.type
457
+ when :insert
458
+ # Delete the inserted row
459
+ engine.delete_row(
460
+ data[:table],
461
+ data[:row_id]
462
+ )
463
+ when :update
464
+ # Restore old values
465
+ engine.update_row(
466
+ data[:table],
467
+ data[:row_id],
468
+ data[:old_values]
469
+ )
470
+ when :delete
471
+ # Reinsert the deleted row
472
+ engine.insert_row(
473
+ data[:table],
474
+ data[:columns],
475
+ data[:row_data]
476
+ )
477
+ end
478
+ end
479
+ end
480
+ end
481
+ end