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,353 +1,349 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "time"
5
- require "json"
6
- require "digest"
7
- require "monitor"
8
-
9
- module RubyDB
10
- module Backup
11
- # Incremental - Handles incremental backups
12
- class Incremental
13
- attr_reader :stats
14
-
15
- def initialize(engine, config = {})
16
- @engine = engine
17
- @config = config
18
- @backup_dir = config[:backup_dir] || "backups"
19
- @incremental_dir = config[:incremental_dir] || File.join(@backup_dir, "incremental")
20
- @max_incrementals = config[:max_incrementals] || 10
21
- @stats = {
22
- incrementals_created: 0,
23
- incrementals_restored: 0,
24
- total_size_bytes: 0,
25
- last_incremental: nil,
26
- chain_length: 0
27
- }
28
- @lock = Monitor.new
29
- @incrementals = {}
30
- @chains = {}
31
-
32
- FileUtils.mkdir_p(@incremental_dir)
33
- load_incrementals
34
- end
35
-
36
- def create_incremental(base_backup = nil)
37
- @lock.synchronize do
38
- start_time = Time.now
39
-
40
- # Find base backup if not specified
41
- base_backup ||= find_latest_backup
42
- unless base_backup
43
- return { success: false, error: "No base backup found" }
44
- end
45
-
46
- # Get last LSN from base backup or previous incremental
47
- last_lsn = get_last_lsn(base_backup)
48
-
49
- # Create incremental backup
50
- incremental_name = "inc_#{Time.now.strftime('%Y%m%d_%H%M%S')}"
51
- incremental_path = File.join(@incremental_dir, incremental_name)
52
- FileUtils.mkdir_p(incremental_path)
53
-
54
- # Capture changes since last backup
55
- changes = capture_changes_since(last_lsn)
56
-
57
- # Write changes
58
- changes_path = File.join(incremental_path, "changes.json")
59
- File.write(changes_path, JSON.generate(changes))
60
-
61
- # Write metadata
62
- metadata = {
63
- name: incremental_name,
64
- type: :incremental,
65
- base_backup: base_backup,
66
- created_at: Time.now.iso8601,
67
- last_lsn: last_lsn,
68
- new_lsn: changes[:new_lsn],
69
- change_count: changes[:changes].size,
70
- size: calculate_size(incremental_path),
71
- checksum: Digest::SHA256.file(changes_path).hexdigest
72
- }
73
-
74
- metadata_path = File.join(incremental_path, "metadata.json")
75
- File.write(metadata_path, JSON.generate(metadata))
76
- File.write(File.join(incremental_path, "manifest.json"), JSON.generate(metadata))
77
-
78
- # Update chain
79
- @chains[base_backup] ||= []
80
- @chains[base_backup] << metadata
81
- @incrementals[incremental_name] = metadata
82
-
83
- # Update stats
84
- @stats[:incrementals_created] += 1
85
- @stats[:total_size_bytes] += metadata[:size]
86
- @stats[:last_incremental] = Time.now
87
- @stats[:chain_length] = @chains[base_backup].size
88
-
89
- # Clean old incrementals
90
- clean_old_incrementals
91
-
92
- {
93
- success: true,
94
- incremental_name: incremental_name,
95
- metadata: metadata,
96
- elapsed_ms: (Time.now - start_time) * 1000
97
- }
98
- end
99
- end
100
-
101
- def create_differential(base_backup = nil)
102
- result = create_incremental(base_backup)
103
- return result unless result[:success]
104
-
105
- metadata = result[:metadata].merge(type: :differential)
106
- path = File.join(@incremental_dir, result[:incremental_name])
107
- File.write(File.join(path, "metadata.json"), JSON.generate(metadata))
108
- File.write(File.join(path, "manifest.json"), JSON.generate(metadata))
109
- @incrementals[result[:incremental_name]] = metadata
110
- result.merge(metadata: metadata)
111
- end
112
-
113
- def restore_to_lsn(target_lsn, options = {})
114
- @lock.synchronize do
115
- start_time = Time.now
116
-
117
- # Find the chain that covers the target LSN
118
- chain = find_chain_for_lsn(target_lsn)
119
- unless chain
120
- return { success: false, error: "No chain found for LSN" }
121
- end
122
-
123
- # Restore base backup
124
- base_backup = chain[:base]
125
- restore_base = restore_backup(base_backup)
126
-
127
- unless restore_base[:success]
128
- return { success: false, error: "Failed to restore base backup" }
129
- end
130
-
131
- # Apply incrementals in order
132
- applied = []
133
- chain[:incrementals].each do |inc|
134
- break if inc[:new_lsn] > target_lsn
135
-
136
- apply_incremental(inc)
137
- applied << inc[:name]
138
- end
139
-
140
- @stats[:incrementals_restored] += 1
141
-
142
- {
143
- success: true,
144
- restored_to_lsn: target_lsn,
145
- applied_incrementals: applied,
146
- count: applied.size,
147
- elapsed_ms: (Time.now - start_time) * 1000
148
- }
149
- end
150
- end
151
-
152
- def restore_to_time(target_time, options = {})
153
- @lock.synchronize do
154
- # Find incremental closest to target time
155
- target = Time.parse(target_time)
156
- best = nil
157
- best_diff = Float::INFINITY
158
-
159
- @incrementals.each do |name, inc|
160
- diff = (Time.parse(inc[:created_at]) - target).abs
161
- if diff < best_diff
162
- best_diff = diff
163
- best = inc
164
- end
165
- end
166
-
167
- unless best
168
- return { success: false, error: "No incremental found for time" }
169
- end
170
-
171
- restore_to_lsn(best[:new_lsn], options)
172
- end
173
- end
174
-
175
- def list_incrementals
176
- @lock.synchronize do
177
- @incrementals.values.sort_by { |i| i[:created_at] }.reverse
178
- end
179
- end
180
-
181
- def list_chains
182
- @lock.synchronize do
183
- @chains.transform_values { |chain| chain.map { |c| c[:name] } }
184
- end
185
- end
186
-
187
- def delete_incremental(incremental_name)
188
- @lock.synchronize do
189
- inc = @incrementals[incremental_name]
190
- return { success: false, error: "Incremental not found" } unless inc
191
-
192
- path = File.join(@incremental_dir, incremental_name)
193
- size = calculate_size(path)
194
- FileUtils.rm_rf(path)
195
-
196
- @incrementals.delete(incremental_name)
197
- @stats[:total_size_bytes] -= size
198
-
199
- { success: true }
200
- end
201
- end
202
-
203
- def stats
204
- @lock.synchronize do
205
- @stats.merge({
206
- incrementals: @incrementals.size,
207
- chains: @chains.size,
208
- incremental_dir: @incremental_dir,
209
- max_incrementals: @max_incrementals
210
- })
211
- end
212
- end
213
-
214
- private
215
-
216
- def load_incrementals
217
- Dir.glob(File.join(@incremental_dir, "inc_*")).each do |path|
218
- metadata_path = File.join(path, "metadata.json")
219
- next unless File.exist?(metadata_path)
220
-
221
- begin
222
- metadata = JSON.parse(File.read(metadata_path), symbolize_names: true)
223
- validate_incremental_metadata!(path, metadata)
224
- @incrementals[metadata[:name]] = metadata
225
-
226
- base = metadata[:base_backup]
227
- @chains[base] ||= []
228
- @chains[base] << metadata
229
- rescue StandardError => error
230
- raise error
231
- end
232
- end
233
-
234
- # Sort chains by time
235
- @chains.each do |base, chain|
236
- @chains[base] = chain.sort_by { |c| c[:created_at] }
237
- end
238
- end
239
-
240
- def find_latest_backup
241
- backups = list_available_backups
242
- backups.first
243
- end
244
-
245
- def find_chain_for_lsn(target_lsn)
246
- @chains.each do |base, chain|
247
- last = chain.last
248
- if last && last[:new_lsn] >= target_lsn
249
- return { base: base, incrementals: chain }
250
- end
251
- end
252
- nil
253
- end
254
-
255
- def get_last_lsn(backup)
256
- backup_path = File.join(@backup_dir, backup)
257
- manifest_path = File.join(backup_path, "manifest.json")
258
- raise "Base backup manifest not found: #{backup}" unless File.exist?(manifest_path)
259
-
260
- verification = Backup.new(@engine, @config.merge(backup_dir: @backup_dir)).verify_backup(backup_path)
261
- raise "Base backup verification failed: #{verification[:error]}" unless verification[:success]
262
-
263
- manifest = JSON.parse(File.read(manifest_path), symbolize_names: true)
264
- (manifest[:lsn] || 0).to_i
265
- end
266
-
267
- def capture_changes_since(last_lsn)
268
- wal = @engine.respond_to?(:wal) ? @engine.wal : nil
269
- raise "Incremental backup requires an engine WAL" unless wal
270
- records = wal.read_all.select do |record|
271
- record.lsn && record.lsn.to_i > last_lsn.to_i
272
- end
273
- changes = records.filter_map do |record|
274
- next unless %i[insert update delete].include?(record.type)
275
- data = (record.data || {}).dup
276
- data[:type] = record.type.to_s
277
- data
278
- end
279
- new_lsn = records.map { |record| record.lsn.to_i }.max || last_lsn.to_i
280
- {
281
- new_lsn: new_lsn,
282
- changes: changes
283
- }
284
- end
285
-
286
- def calculate_size(path)
287
- Dir.glob(File.join(path, "**/*")).sum do |f|
288
- File.size(f) if File.file?(f)
289
- end || 0
290
- end
291
-
292
- def restore_backup(backup_name)
293
- backup_path = File.join(@backup_dir, backup_name)
294
- restore = Restore.new(@engine, @config)
295
- restore.restore(backup_path)
296
- end
297
-
298
- def apply_incremental(inc)
299
- inc_path = File.join(@incremental_dir, inc[:name])
300
- changes_path = File.join(inc_path, "changes.json")
301
- raise "Incremental changes not found: #{inc[:name]}" unless File.exist?(changes_path)
302
- expected = inc[:checksum]
303
- if expected && Digest::SHA256.file(changes_path).hexdigest != expected
304
- raise "Incremental checksum mismatch: #{inc[:name]}"
305
- end
306
-
307
- changes = JSON.parse(File.read(changes_path), symbolize_names: true)
308
- changes[:changes].each do |change|
309
- case change[:type].to_s
310
- when "insert"
311
- columns = @engine.table_columns(change[:table])
312
- @engine.insert_row(change[:table], columns, normalize_values(change[:values] || change[:data]))
313
- when "update"
314
- @engine.update_row(change[:table], change[:row_id], normalize_values(change[:values] || change[:data]))
315
- when "delete"
316
- @engine.delete_row(change[:table], change[:row_id])
317
- end
318
- end
319
- true
320
- end
321
-
322
- def normalize_values(values)
323
- return {} unless values
324
- values.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
325
- end
326
-
327
- def validate_incremental_metadata!(path, metadata)
328
- changes_path = File.join(path, "changes.json")
329
- raise "Incremental changes file missing: #{path}" unless File.file?(changes_path)
330
- if metadata[:checksum] && Digest::SHA256.file(changes_path).hexdigest != metadata[:checksum]
331
- raise "Incremental checksum mismatch: #{metadata[:name]}"
332
- end
333
- end
334
-
335
- def list_available_backups
336
- backup = Backup.new(@engine, @config)
337
- backups = backup.list_backups
338
- backups.map { |b| b[:name] }
339
- end
340
-
341
- def clean_old_incrementals
342
- @chains.each do |base, chain|
343
- if chain.size > @max_incrementals
344
- to_remove = chain.first(chain.size - @max_incrementals)
345
- to_remove.each do |inc|
346
- delete_incremental(inc[:name])
347
- end
348
- end
349
- end
350
- end
351
- end
352
- end
353
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+ require "json"
6
+ require "digest"
7
+ require "monitor"
8
+
9
+ module RubyDB
10
+ module Backup
11
+ # Incremental - Handles incremental backups
12
+ class Incremental
13
+ attr_reader :stats
14
+
15
+ def initialize(engine, config = {})
16
+ @engine = engine
17
+ @config = config
18
+ @backup_dir = config[:backup_dir] || "backups"
19
+ @incremental_dir = config[:incremental_dir] || File.join(@backup_dir, "incremental")
20
+ @max_incrementals = config[:max_incrementals] || 10
21
+ @stats = {
22
+ incrementals_created: 0,
23
+ incrementals_restored: 0,
24
+ total_size_bytes: 0,
25
+ last_incremental: nil,
26
+ chain_length: 0
27
+ }
28
+ @lock = Monitor.new
29
+ @incrementals = {}
30
+ @chains = {}
31
+
32
+ FileUtils.mkdir_p(@incremental_dir)
33
+ load_incrementals
34
+ end
35
+
36
+ def create_incremental(base_backup = nil)
37
+ @lock.synchronize do
38
+ start_time = Time.now
39
+
40
+ # Find base backup if not specified
41
+ base_backup ||= find_latest_backup
42
+ unless base_backup
43
+ return {success: false, error: "No base backup found"}
44
+ end
45
+
46
+ # Get last LSN from base backup or previous incremental
47
+ last_lsn = get_last_lsn(base_backup)
48
+
49
+ # Create incremental backup
50
+ incremental_name = "inc_#{Time.now.strftime("%Y%m%d_%H%M%S")}"
51
+ incremental_path = File.join(@incremental_dir, incremental_name)
52
+ FileUtils.mkdir_p(incremental_path)
53
+
54
+ # Capture changes since last backup
55
+ changes = capture_changes_since(last_lsn)
56
+
57
+ # Write changes
58
+ changes_path = File.join(incremental_path, "changes.json")
59
+ File.write(changes_path, JSON.generate(changes))
60
+
61
+ # Write metadata
62
+ metadata = {
63
+ name: incremental_name,
64
+ type: :incremental,
65
+ base_backup: base_backup,
66
+ created_at: Time.now.iso8601,
67
+ last_lsn: last_lsn,
68
+ new_lsn: changes[:new_lsn],
69
+ change_count: changes[:changes].size,
70
+ size: calculate_size(incremental_path),
71
+ checksum: Digest::SHA256.file(changes_path).hexdigest
72
+ }
73
+
74
+ metadata_path = File.join(incremental_path, "metadata.json")
75
+ File.write(metadata_path, JSON.generate(metadata))
76
+ File.write(File.join(incremental_path, "manifest.json"), JSON.generate(metadata))
77
+
78
+ # Update chain
79
+ @chains[base_backup] ||= []
80
+ @chains[base_backup] << metadata
81
+ @incrementals[incremental_name] = metadata
82
+
83
+ # Update stats
84
+ @stats[:incrementals_created] += 1
85
+ @stats[:total_size_bytes] += metadata[:size]
86
+ @stats[:last_incremental] = Time.now
87
+ @stats[:chain_length] = @chains[base_backup].size
88
+
89
+ # Clean old incrementals
90
+ clean_old_incrementals
91
+
92
+ {
93
+ success: true,
94
+ incremental_name: incremental_name,
95
+ metadata: metadata,
96
+ elapsed_ms: (Time.now - start_time) * 1000
97
+ }
98
+ end
99
+ end
100
+
101
+ def create_differential(base_backup = nil)
102
+ result = create_incremental(base_backup)
103
+ return result unless result[:success]
104
+
105
+ metadata = result[:metadata].merge(type: :differential)
106
+ path = File.join(@incremental_dir, result[:incremental_name])
107
+ File.write(File.join(path, "metadata.json"), JSON.generate(metadata))
108
+ File.write(File.join(path, "manifest.json"), JSON.generate(metadata))
109
+ @incrementals[result[:incremental_name]] = metadata
110
+ result.merge(metadata: metadata)
111
+ end
112
+
113
+ def restore_to_lsn(target_lsn, options = {})
114
+ @lock.synchronize do
115
+ start_time = Time.now
116
+
117
+ # Find the chain that covers the target LSN
118
+ chain = find_chain_for_lsn(target_lsn)
119
+ unless chain
120
+ return {success: false, error: "No chain found for LSN"}
121
+ end
122
+
123
+ # Restore base backup
124
+ base_backup = chain[:base]
125
+ restore_base = restore_backup(base_backup)
126
+
127
+ unless restore_base[:success]
128
+ return {success: false, error: "Failed to restore base backup"}
129
+ end
130
+
131
+ # Apply incrementals in order
132
+ applied = []
133
+ chain[:incrementals].each do |inc|
134
+ break if inc[:new_lsn] > target_lsn
135
+
136
+ apply_incremental(inc)
137
+ applied << inc[:name]
138
+ end
139
+
140
+ @stats[:incrementals_restored] += 1
141
+
142
+ {
143
+ success: true,
144
+ restored_to_lsn: target_lsn,
145
+ applied_incrementals: applied,
146
+ count: applied.size,
147
+ elapsed_ms: (Time.now - start_time) * 1000
148
+ }
149
+ end
150
+ end
151
+
152
+ def restore_to_time(target_time, options = {})
153
+ @lock.synchronize do
154
+ # Find incremental closest to target time
155
+ target = Time.parse(target_time)
156
+ best = nil
157
+ best_diff = Float::INFINITY
158
+
159
+ @incrementals.each do |name, inc|
160
+ diff = (Time.parse(inc[:created_at]) - target).abs
161
+ if diff < best_diff
162
+ best_diff = diff
163
+ best = inc
164
+ end
165
+ end
166
+
167
+ unless best
168
+ return {success: false, error: "No incremental found for time"}
169
+ end
170
+
171
+ restore_to_lsn(best[:new_lsn], options)
172
+ end
173
+ end
174
+
175
+ def list_incrementals
176
+ @lock.synchronize do
177
+ @incrementals.values.sort_by { |i| i[:created_at] }.reverse
178
+ end
179
+ end
180
+
181
+ def list_chains
182
+ @lock.synchronize do
183
+ @chains.transform_values { |chain| chain.map { |c| c[:name] } }
184
+ end
185
+ end
186
+
187
+ def delete_incremental(incremental_name)
188
+ @lock.synchronize do
189
+ inc = @incrementals[incremental_name]
190
+ return {success: false, error: "Incremental not found"} unless inc
191
+
192
+ path = File.join(@incremental_dir, incremental_name)
193
+ size = calculate_size(path)
194
+ FileUtils.rm_rf(path)
195
+
196
+ @incrementals.delete(incremental_name)
197
+ @stats[:total_size_bytes] -= size
198
+
199
+ {success: true}
200
+ end
201
+ end
202
+
203
+ def stats
204
+ @lock.synchronize do
205
+ @stats.merge({
206
+ incrementals: @incrementals.size,
207
+ chains: @chains.size,
208
+ incremental_dir: @incremental_dir,
209
+ max_incrementals: @max_incrementals
210
+ })
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ def load_incrementals
217
+ Dir.glob(File.join(@incremental_dir, "inc_*")).each do |path|
218
+ metadata_path = File.join(path, "metadata.json")
219
+ next unless File.exist?(metadata_path)
220
+
221
+ metadata = JSON.parse(File.read(metadata_path), symbolize_names: true)
222
+ validate_incremental_metadata!(path, metadata)
223
+ @incrementals[metadata[:name]] = metadata
224
+
225
+ base = metadata[:base_backup]
226
+ @chains[base] ||= []
227
+ @chains[base] << metadata
228
+ end
229
+
230
+ # Sort chains by time
231
+ @chains.each do |base, chain|
232
+ @chains[base] = chain.sort_by { |c| c[:created_at] }
233
+ end
234
+ end
235
+
236
+ def find_latest_backup
237
+ backups = list_available_backups
238
+ backups.first
239
+ end
240
+
241
+ def find_chain_for_lsn(target_lsn)
242
+ @chains.each do |base, chain|
243
+ last = chain.last
244
+ if last && last[:new_lsn] >= target_lsn
245
+ return {base: base, incrementals: chain}
246
+ end
247
+ end
248
+ nil
249
+ end
250
+
251
+ def get_last_lsn(backup)
252
+ backup_path = File.join(@backup_dir, backup)
253
+ manifest_path = File.join(backup_path, "manifest.json")
254
+ raise "Base backup manifest not found: #{backup}" unless File.exist?(manifest_path)
255
+
256
+ verification = Backup.new(@engine, @config.merge(backup_dir: @backup_dir)).verify_backup(backup_path)
257
+ raise "Base backup verification failed: #{verification[:error]}" unless verification[:success]
258
+
259
+ manifest = JSON.parse(File.read(manifest_path), symbolize_names: true)
260
+ (manifest[:lsn] || 0).to_i
261
+ end
262
+
263
+ def capture_changes_since(last_lsn)
264
+ wal = @engine.respond_to?(:wal) ? @engine.wal : nil
265
+ raise "Incremental backup requires an engine WAL" unless wal
266
+ records = wal.read_all.select do |record|
267
+ record.lsn && record.lsn.to_i > last_lsn.to_i
268
+ end
269
+ changes = records.filter_map do |record|
270
+ next unless %i[insert update delete].include?(record.type)
271
+ data = (record.data || {}).dup
272
+ data[:type] = record.type.to_s
273
+ data
274
+ end
275
+ new_lsn = records.map { |record| record.lsn.to_i }.max || last_lsn.to_i
276
+ {
277
+ new_lsn: new_lsn,
278
+ changes: changes
279
+ }
280
+ end
281
+
282
+ def calculate_size(path)
283
+ Dir.glob(File.join(path, "**/*")).sum do |f|
284
+ File.size(f) if File.file?(f)
285
+ end || 0
286
+ end
287
+
288
+ def restore_backup(backup_name)
289
+ backup_path = File.join(@backup_dir, backup_name)
290
+ restore = Restore.new(@engine, @config)
291
+ restore.restore(backup_path)
292
+ end
293
+
294
+ def apply_incremental(inc)
295
+ inc_path = File.join(@incremental_dir, inc[:name])
296
+ changes_path = File.join(inc_path, "changes.json")
297
+ raise "Incremental changes not found: #{inc[:name]}" unless File.exist?(changes_path)
298
+ expected = inc[:checksum]
299
+ if expected && Digest::SHA256.file(changes_path).hexdigest != expected
300
+ raise "Incremental checksum mismatch: #{inc[:name]}"
301
+ end
302
+
303
+ changes = JSON.parse(File.read(changes_path), symbolize_names: true)
304
+ changes[:changes].each do |change|
305
+ case change[:type].to_s
306
+ when "insert"
307
+ columns = @engine.table_columns(change[:table])
308
+ @engine.insert_row(change[:table], columns, normalize_values(change[:values] || change[:data]))
309
+ when "update"
310
+ @engine.update_row(change[:table], change[:row_id], normalize_values(change[:values] || change[:data]))
311
+ when "delete"
312
+ @engine.delete_row(change[:table], change[:row_id])
313
+ end
314
+ end
315
+ true
316
+ end
317
+
318
+ def normalize_values(values)
319
+ return {} unless values
320
+ values.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
321
+ end
322
+
323
+ def validate_incremental_metadata!(path, metadata)
324
+ changes_path = File.join(path, "changes.json")
325
+ raise "Incremental changes file missing: #{path}" unless File.file?(changes_path)
326
+ if metadata[:checksum] && Digest::SHA256.file(changes_path).hexdigest != metadata[:checksum]
327
+ raise "Incremental checksum mismatch: #{metadata[:name]}"
328
+ end
329
+ end
330
+
331
+ def list_available_backups
332
+ backup = Backup.new(@engine, @config)
333
+ backups = backup.list_backups
334
+ backups.map { |b| b[:name] }
335
+ end
336
+
337
+ def clean_old_incrementals
338
+ @chains.each do |base, chain|
339
+ if chain.size > @max_incrementals
340
+ to_remove = chain.first(chain.size - @max_incrementals)
341
+ to_remove.each do |inc|
342
+ delete_incremental(inc[:name])
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end