rubydb 0.1.4 → 0.1.5

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