rubydb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +20 -0
  3. data/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  4. data/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  5. data/.github/ISSUE_TEMPLATE/performance.yml +0 -0
  6. data/.github/ISSUE_TEMPLATE/security.yml +0 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  8. data/.github/dependabot.yml +0 -0
  9. data/.github/workflows/benchmark.yml +26 -0
  10. data/.github/workflows/compatibility.yml +63 -0
  11. data/.github/workflows/fuzz.yml +33 -0
  12. data/.github/workflows/lint.yml +21 -0
  13. data/.github/workflows/operations.yml +24 -0
  14. data/.github/workflows/production-validation.yml +111 -0
  15. data/.github/workflows/release.yml +77 -0
  16. data/.github/workflows/security.yml +37 -0
  17. data/.github/workflows/test.yml +26 -0
  18. data/.github/workflows/workload.yml +58 -0
  19. data/.gitignore +43 -0
  20. data/.rspec +4 -0
  21. data/.rubocop.yml +44 -0
  22. data/.ruby-version +1 -0
  23. data/.standard.yml +14 -0
  24. data/ARCHITECTURE.md +21 -0
  25. data/CHANGELOG.md +31 -0
  26. data/CODE_OF_CONDUCT.md +13 -0
  27. data/CONTRIBUTING.md +29 -0
  28. data/GOVERNANCE.md +16 -0
  29. data/Gemfile +17 -0
  30. data/Gemfile.lock +71 -0
  31. data/LICENSE +60 -0
  32. data/README.md +162 -0
  33. data/ROADMAP.md +27 -0
  34. data/Rakefile +71 -0
  35. data/SECURITY.md +54 -0
  36. data/SUPPORT.md +14 -0
  37. data/adapters/activerecord/Gemfile +11 -0
  38. data/adapters/activerecord/README.md +9 -0
  39. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +887 -0
  40. data/adapters/activerecord/rubydb-activerecord.gemspec +27 -0
  41. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +151 -0
  42. data/adapters/ruby/README.md +18 -0
  43. data/adapters/sequel/README.md +11 -0
  44. data/bin/rubydb +19 -0
  45. data/bin/rubydb-server +126 -0
  46. data/config/development.yml +0 -0
  47. data/config/monitoring/prometheus-alerts.yml +39 -0
  48. data/config/production.yml +36 -0
  49. data/config/test.yml +0 -0
  50. data/docs/README.md +71 -0
  51. data/docs/architecture/concurrency.md +14 -0
  52. data/docs/architecture/current-state.md +125 -0
  53. data/docs/architecture/execution-engine.md +25 -0
  54. data/docs/architecture/indexes.md +19 -0
  55. data/docs/architecture/mvcc.md +19 -0
  56. data/docs/architecture/overview.md +13 -0
  57. data/docs/architecture/pages.md +11 -0
  58. data/docs/architecture/production-roadmap.md +82 -0
  59. data/docs/architecture/query-planner.md +20 -0
  60. data/docs/architecture/recovery.md +18 -0
  61. data/docs/architecture/sql-engine.md +12 -0
  62. data/docs/architecture/storage-engine.md +14 -0
  63. data/docs/architecture/transactions.md +10 -0
  64. data/docs/architecture/wal.md +28 -0
  65. data/docs/cli-cheatsheet.md +98 -0
  66. data/docs/cli.md +275 -0
  67. data/docs/contributing/architecture.md +9 -0
  68. data/docs/contributing/benchmarking.md +14 -0
  69. data/docs/contributing/development.md +16 -0
  70. data/docs/contributing/release-process.md +49 -0
  71. data/docs/contributing/testing.md +16 -0
  72. data/docs/debugging.md +229 -0
  73. data/docs/developer/branching.md +10 -0
  74. data/docs/developer/database-diff.md +10 -0
  75. data/docs/developer/local-development.md +17 -0
  76. data/docs/developer/snapshots.md +9 -0
  77. data/docs/developer/temporal-data.md +10 -0
  78. data/docs/developer-guide.md +297 -0
  79. data/docs/getting-started/first-database.md +16 -0
  80. data/docs/getting-started/first-query.md +13 -0
  81. data/docs/getting-started/installation.md +19 -0
  82. data/docs/getting-started/local-to-production.md +300 -0
  83. data/docs/getting-started/quickstart.md +17 -0
  84. data/docs/getting-started/rails.md +16 -0
  85. data/docs/hardening_backlog.md +93 -0
  86. data/docs/lessons-learned.md +112 -0
  87. data/docs/operations/backups.md +33 -0
  88. data/docs/operations/disaster-recovery.md +31 -0
  89. data/docs/operations/failover.md +30 -0
  90. data/docs/operations/monitoring.md +25 -0
  91. data/docs/operations/production-guide.md +295 -0
  92. data/docs/operations/production-runbook.md +45 -0
  93. data/docs/operations/replication.md +33 -0
  94. data/docs/operations/restore.md +6 -0
  95. data/docs/operations/runbook.md +34 -0
  96. data/docs/operations/upgrades.md +14 -0
  97. data/docs/operations/workload-testing.md +17 -0
  98. data/docs/production-readiness.md +118 -0
  99. data/docs/production_validation.md +150 -0
  100. data/docs/rails/active-record.md +11 -0
  101. data/docs/rails/compatibility-guide.md +90 -0
  102. data/docs/rails/database-yml.md +92 -0
  103. data/docs/rails/installation.md +17 -0
  104. data/docs/rails/migrations.md +17 -0
  105. data/docs/rails/production.md +82 -0
  106. data/docs/rails/troubleshooting.md +18 -0
  107. data/docs/release.md +59 -0
  108. data/docs/server/architecture.md +10 -0
  109. data/docs/server/authentication.md +10 -0
  110. data/docs/server/configuration.md +16 -0
  111. data/docs/server/connection-pooling.md +10 -0
  112. data/docs/server/deployment.md +10 -0
  113. data/docs/server/protocol.md +12 -0
  114. data/docs/sql/compatibility-guide.md +82 -0
  115. data/docs/sql/compatibility.md +39 -0
  116. data/docs/sql/data-types.md +10 -0
  117. data/docs/sql/functions.md +9 -0
  118. data/docs/sql/joins.md +9 -0
  119. data/docs/sql/operators.md +9 -0
  120. data/docs/sql/sqlite-compatibility.md +21 -0
  121. data/docs/sql/syntax.md +10 -0
  122. data/docs/sql/transactions.md +10 -0
  123. data/docs/troubleshooting.md +244 -0
  124. data/exe/rubydb +23 -0
  125. data/lib/rubydb/backup/archive.rb +334 -0
  126. data/lib/rubydb/backup/backup.rb +401 -0
  127. data/lib/rubydb/backup/incremental.rb +353 -0
  128. data/lib/rubydb/backup/restore.rb +290 -0
  129. data/lib/rubydb/backup/snapshot.rb +267 -0
  130. data/lib/rubydb/backup/verification.rb +279 -0
  131. data/lib/rubydb/branching/branch.rb +181 -0
  132. data/lib/rubydb/branching/branch_manager.rb +311 -0
  133. data/lib/rubydb/branching/branch_metadata.rb +140 -0
  134. data/lib/rubydb/branching/checkout.rb +166 -0
  135. data/lib/rubydb/branching/copy_on_write.rb +272 -0
  136. data/lib/rubydb/branching/diff.rb +138 -0
  137. data/lib/rubydb/branching/merge.rb +285 -0
  138. data/lib/rubydb/build_info.rb +15 -0
  139. data/lib/rubydb/catalog/catalog.rb +391 -0
  140. data/lib/rubydb/catalog/column.rb +112 -0
  141. data/lib/rubydb/catalog/constraint.rb +180 -0
  142. data/lib/rubydb/catalog/database.rb +184 -0
  143. data/lib/rubydb/catalog/index.rb +97 -0
  144. data/lib/rubydb/catalog/schema.rb +103 -0
  145. data/lib/rubydb/catalog/sequence.rb +90 -0
  146. data/lib/rubydb/catalog/system_catalog.rb +698 -0
  147. data/lib/rubydb/catalog/table.rb +178 -0
  148. data/lib/rubydb/catalog/trigger.rb +102 -0
  149. data/lib/rubydb/catalog/view.rb +66 -0
  150. data/lib/rubydb/cli/application.rb +163 -0
  151. data/lib/rubydb/cli/commands/backup.rb +81 -0
  152. data/lib/rubydb/cli/commands/branch.rb +72 -0
  153. data/lib/rubydb/cli/commands/checkout.rb +54 -0
  154. data/lib/rubydb/cli/commands/create.rb +58 -0
  155. data/lib/rubydb/cli/commands/diff.rb +77 -0
  156. data/lib/rubydb/cli/commands/doctor.rb +74 -0
  157. data/lib/rubydb/cli/commands/drop.rb +57 -0
  158. data/lib/rubydb/cli/commands/init.rb +102 -0
  159. data/lib/rubydb/cli/commands/inspect.rb +95 -0
  160. data/lib/rubydb/cli/commands/merge.rb +63 -0
  161. data/lib/rubydb/cli/commands/migrate.rb +62 -0
  162. data/lib/rubydb/cli/commands/restart.rb +39 -0
  163. data/lib/rubydb/cli/commands/restore.rb +121 -0
  164. data/lib/rubydb/cli/commands/shell.rb +365 -0
  165. data/lib/rubydb/cli/commands/snapshot.rb +79 -0
  166. data/lib/rubydb/cli/commands/start.rb +82 -0
  167. data/lib/rubydb/cli/commands/status.rb +92 -0
  168. data/lib/rubydb/cli/commands/stop.rb +47 -0
  169. data/lib/rubydb/cli/commands/vacuum.rb +58 -0
  170. data/lib/rubydb/cli/formatter.rb +221 -0
  171. data/lib/rubydb/cli/output.rb +168 -0
  172. data/lib/rubydb/client/client.rb +304 -0
  173. data/lib/rubydb/client/connection.rb +415 -0
  174. data/lib/rubydb/client/connection_pool.rb +168 -0
  175. data/lib/rubydb/client/connection_url.rb +96 -0
  176. data/lib/rubydb/client/prepared_statement.rb +60 -0
  177. data/lib/rubydb/client/result.rb +123 -0
  178. data/lib/rubydb/client/statement.rb +52 -0
  179. data/lib/rubydb/client/transaction.rb +130 -0
  180. data/lib/rubydb/concurrency/concurrency.rb +19 -0
  181. data/lib/rubydb/concurrency/deadlock_detector.rb +150 -0
  182. data/lib/rubydb/concurrency/latch.rb +101 -0
  183. data/lib/rubydb/concurrency/lock_graph.rb +165 -0
  184. data/lib/rubydb/concurrency/mutex.rb +183 -0
  185. data/lib/rubydb/concurrency/rw_lock.rb +180 -0
  186. data/lib/rubydb/concurrency/scheduler.rb +250 -0
  187. data/lib/rubydb/concurrency/worker_pool.rb +143 -0
  188. data/lib/rubydb/configuration/config.rb +170 -0
  189. data/lib/rubydb/configuration/defaults.rb +179 -0
  190. data/lib/rubydb/configuration/environment.rb +152 -0
  191. data/lib/rubydb/configuration/parser.rb +185 -0
  192. data/lib/rubydb/configuration/validation.rb +221 -0
  193. data/lib/rubydb/constants.rb +74 -0
  194. data/lib/rubydb/constraints/check.rb +181 -0
  195. data/lib/rubydb/constraints/constraint.rb +101 -0
  196. data/lib/rubydb/constraints/foreign_key.rb +130 -0
  197. data/lib/rubydb/constraints/not_null.rb +64 -0
  198. data/lib/rubydb/constraints/primary_key.rb +99 -0
  199. data/lib/rubydb/constraints/unique.rb +108 -0
  200. data/lib/rubydb/constraints/validator.rb +350 -0
  201. data/lib/rubydb/errors/authentication_error.rb +10 -0
  202. data/lib/rubydb/errors/authorization_error.rb +23 -0
  203. data/lib/rubydb/errors/client_error.rb +10 -0
  204. data/lib/rubydb/errors/configuration_error.rb +10 -0
  205. data/lib/rubydb/errors/connection_error.rb +10 -0
  206. data/lib/rubydb/errors/constraint_error.rb +23 -0
  207. data/lib/rubydb/errors/corruption_error.rb +10 -0
  208. data/lib/rubydb/errors/database_error.rb +10 -0
  209. data/lib/rubydb/errors/error.rb +20 -0
  210. data/lib/rubydb/errors/execution_error.rb +10 -0
  211. data/lib/rubydb/errors/parser_error.rb +10 -0
  212. data/lib/rubydb/errors/recovery_error.rb +10 -0
  213. data/lib/rubydb/errors/replication_error.rb +10 -0
  214. data/lib/rubydb/errors/server_error.rb +6 -0
  215. data/lib/rubydb/errors/storage_error.rb +10 -0
  216. data/lib/rubydb/errors/transaction_error.rb +10 -0
  217. data/lib/rubydb/execution/aggregate_executor.rb +138 -0
  218. data/lib/rubydb/execution/delete_executor.rb +112 -0
  219. data/lib/rubydb/execution/distinct_executor.rb +135 -0
  220. data/lib/rubydb/execution/executor.rb +1192 -0
  221. data/lib/rubydb/execution/expression.rb +193 -0
  222. data/lib/rubydb/execution/index_scan.rb +142 -0
  223. data/lib/rubydb/execution/insert_executor.rb +217 -0
  224. data/lib/rubydb/execution/join_executor.rb +249 -0
  225. data/lib/rubydb/execution/limit_executor.rb +85 -0
  226. data/lib/rubydb/execution/optimizer.rb +215 -0
  227. data/lib/rubydb/execution/plan.rb +361 -0
  228. data/lib/rubydb/execution/planner.rb +546 -0
  229. data/lib/rubydb/execution/predicate.rb +235 -0
  230. data/lib/rubydb/execution/scan.rb +49 -0
  231. data/lib/rubydb/execution/sequential_scan.rb +63 -0
  232. data/lib/rubydb/execution/sort_executor.rb +185 -0
  233. data/lib/rubydb/execution/update_executor.rb +162 -0
  234. data/lib/rubydb/functions/aggregate.rb +70 -0
  235. data/lib/rubydb/functions/date_functions.rb +278 -0
  236. data/lib/rubydb/functions/function.rb +85 -0
  237. data/lib/rubydb/functions/json_functions.rb +215 -0
  238. data/lib/rubydb/functions/numeric_functions.rb +346 -0
  239. data/lib/rubydb/functions/scalar.rb +52 -0
  240. data/lib/rubydb/functions/string_functions.rb +383 -0
  241. data/lib/rubydb/functions/system_functions.rb +246 -0
  242. data/lib/rubydb/history/as_of.rb +238 -0
  243. data/lib/rubydb/history/change.rb +105 -0
  244. data/lib/rubydb/history/history.rb +131 -0
  245. data/lib/rubydb/history/history_manager.rb +229 -0
  246. data/lib/rubydb/history/temporal_query.rb +202 -0
  247. data/lib/rubydb/history/timeline.rb +144 -0
  248. data/lib/rubydb/indexes/btree.rb +186 -0
  249. data/lib/rubydb/indexes/btree_cursor.rb +258 -0
  250. data/lib/rubydb/indexes/btree_node.rb +385 -0
  251. data/lib/rubydb/indexes/hash_index.rb +150 -0
  252. data/lib/rubydb/indexes/index.rb +71 -0
  253. data/lib/rubydb/indexes/index_manager.rb +406 -0
  254. data/lib/rubydb/indexes/index_scan.rb +470 -0
  255. data/lib/rubydb/migrations/migration.rb +254 -0
  256. data/lib/rubydb/migrations/migration_lock.rb +146 -0
  257. data/lib/rubydb/migrations/migration_manager.rb +176 -0
  258. data/lib/rubydb/migrations/migration_version.rb +71 -0
  259. data/lib/rubydb/migrations/schema_diff.rb +211 -0
  260. data/lib/rubydb/migrations/schema_version.rb +64 -0
  261. data/lib/rubydb/monitoring/events.rb +160 -0
  262. data/lib/rubydb/monitoring/health.rb +222 -0
  263. data/lib/rubydb/monitoring/logger.rb +193 -0
  264. data/lib/rubydb/monitoring/metrics.rb +359 -0
  265. data/lib/rubydb/monitoring/performance.rb +176 -0
  266. data/lib/rubydb/monitoring/statistics.rb +170 -0
  267. data/lib/rubydb/mvcc/garbage_collector.rb +199 -0
  268. data/lib/rubydb/mvcc/mvcc.rb +16 -0
  269. data/lib/rubydb/mvcc/snapshot.rb +147 -0
  270. data/lib/rubydb/mvcc/vacuum.rb +180 -0
  271. data/lib/rubydb/mvcc/version.rb +106 -0
  272. data/lib/rubydb/mvcc/version_store.rb +398 -0
  273. data/lib/rubydb/mvcc/visibility.rb +109 -0
  274. data/lib/rubydb/protocol/capabilities.rb +125 -0
  275. data/lib/rubydb/protocol/decoder.rb +145 -0
  276. data/lib/rubydb/protocol/encoder.rb +136 -0
  277. data/lib/rubydb/protocol/handshake.rb +305 -0
  278. data/lib/rubydb/protocol/message.rb +121 -0
  279. data/lib/rubydb/protocol/protocol.rb +277 -0
  280. data/lib/rubydb/protocol/version.rb +54 -0
  281. data/lib/rubydb/rails/adapter.rb +239 -0
  282. data/lib/rubydb/rails/connection.rb +314 -0
  283. data/lib/rubydb/rails/database_statements.rb +122 -0
  284. data/lib/rubydb/rails/migration.rb +131 -0
  285. data/lib/rubydb/rails/quoting.rb +109 -0
  286. data/lib/rubydb/rails/result.rb +120 -0
  287. data/lib/rubydb/rails/schema_statements.rb +339 -0
  288. data/lib/rubydb/rails/transaction.rb +105 -0
  289. data/lib/rubydb/rails/type.rb +126 -0
  290. data/lib/rubydb/recovery/checkpoint.rb +257 -0
  291. data/lib/rubydb/recovery/consistency.rb +467 -0
  292. data/lib/rubydb/recovery/corruption_detector.rb +5 -0
  293. data/lib/rubydb/recovery/crash_recovery.rb +387 -0
  294. data/lib/rubydb/recovery/recovery_manager.rb +206 -0
  295. data/lib/rubydb/recovery/redo.rb +237 -0
  296. data/lib/rubydb/recovery/undo.rb +206 -0
  297. data/lib/rubydb/replication/failover.rb +5 -0
  298. data/lib/rubydb/replication/fencing.rb +63 -0
  299. data/lib/rubydb/replication/primary.rb +450 -0
  300. data/lib/rubydb/replication/replica.rb +384 -0
  301. data/lib/rubydb/replication/replication_log.rb +200 -0
  302. data/lib/rubydb/replication/replication_manager.rb +308 -0
  303. data/lib/rubydb/replication/replication_slot.rb +295 -0
  304. data/lib/rubydb/replication/replication_stream.rb +201 -0
  305. data/lib/rubydb/rubydb.rb +560 -0
  306. data/lib/rubydb/security/access_control.rb +254 -0
  307. data/lib/rubydb/security/audit_log.rb +213 -0
  308. data/lib/rubydb/security/authentication.rb +302 -0
  309. data/lib/rubydb/security/authorization.rb +282 -0
  310. data/lib/rubydb/security/credentials.rb +196 -0
  311. data/lib/rubydb/security/password.rb +215 -0
  312. data/lib/rubydb/security/permissions.rb +74 -0
  313. data/lib/rubydb/security/role.rb +101 -0
  314. data/lib/rubydb/security/user.rb +86 -0
  315. data/lib/rubydb/server/connection.rb +366 -0
  316. data/lib/rubydb/server/connection_pool.rb +193 -0
  317. data/lib/rubydb/server/lifecycle.rb +228 -0
  318. data/lib/rubydb/server/listener.rb +136 -0
  319. data/lib/rubydb/server/request_handler.rb +276 -0
  320. data/lib/rubydb/server/server.rb +364 -0
  321. data/lib/rubydb/server/session.rb +369 -0
  322. data/lib/rubydb/server/worker.rb +210 -0
  323. data/lib/rubydb/server/worker_pool.rb +168 -0
  324. data/lib/rubydb/sql/ast/alter_table.rb +169 -0
  325. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -0
  326. data/lib/rubydb/sql/ast/commit.rb +37 -0
  327. data/lib/rubydb/sql/ast/constraint.rb +83 -0
  328. data/lib/rubydb/sql/ast/create_database.rb +41 -0
  329. data/lib/rubydb/sql/ast/create_index.rb +61 -0
  330. data/lib/rubydb/sql/ast/create_schema.rb +52 -0
  331. data/lib/rubydb/sql/ast/create_table.rb +187 -0
  332. data/lib/rubydb/sql/ast/delete.rb +54 -0
  333. data/lib/rubydb/sql/ast/drop_database.rb +41 -0
  334. data/lib/rubydb/sql/ast/drop_index.rb +41 -0
  335. data/lib/rubydb/sql/ast/drop_schema.rb +49 -0
  336. data/lib/rubydb/sql/ast/drop_table.rb +49 -0
  337. data/lib/rubydb/sql/ast/explain.rb +64 -0
  338. data/lib/rubydb/sql/ast/expression.rb +604 -0
  339. data/lib/rubydb/sql/ast/insert.rb +82 -0
  340. data/lib/rubydb/sql/ast/node.rb +42 -0
  341. data/lib/rubydb/sql/ast/rollback.rb +63 -0
  342. data/lib/rubydb/sql/ast/savepoint.rb +59 -0
  343. data/lib/rubydb/sql/ast/select.rb +88 -0
  344. data/lib/rubydb/sql/ast/set_operation.rb +20 -0
  345. data/lib/rubydb/sql/ast/trigger.rb +29 -0
  346. data/lib/rubydb/sql/ast/update.rb +88 -0
  347. data/lib/rubydb/sql/ast/vacuum.rb +19 -0
  348. data/lib/rubydb/sql/ast/view.rb +32 -0
  349. data/lib/rubydb/sql/ast/with.rb +32 -0
  350. data/lib/rubydb/sql/grammar.rb +86 -0
  351. data/lib/rubydb/sql/keywords.rb +156 -0
  352. data/lib/rubydb/sql/lexer.rb +214 -0
  353. data/lib/rubydb/sql/operators.rb +100 -0
  354. data/lib/rubydb/sql/parser.rb +1185 -0
  355. data/lib/rubydb/sql/planner/analyzer.rb +302 -0
  356. data/lib/rubydb/sql/planner/binder.rb +550 -0
  357. data/lib/rubydb/sql/planner/type_checker.rb +431 -0
  358. data/lib/rubydb/sql/token.rb +210 -0
  359. data/lib/rubydb/storage/buffer_frame.rb +44 -0
  360. data/lib/rubydb/storage/buffer_pool.rb +155 -0
  361. data/lib/rubydb/storage/database_lock.rb +74 -0
  362. data/lib/rubydb/storage/deserializer.rb +342 -0
  363. data/lib/rubydb/storage/engine.rb +2338 -0
  364. data/lib/rubydb/storage/file_manager.rb +187 -0
  365. data/lib/rubydb/storage/free_space_map.rb +81 -0
  366. data/lib/rubydb/storage/page.rb +94 -0
  367. data/lib/rubydb/storage/page_allocator.rb +855 -0
  368. data/lib/rubydb/storage/page_header.rb +67 -0
  369. data/lib/rubydb/storage/page_manager.rb +131 -0
  370. data/lib/rubydb/storage/record.rb +58 -0
  371. data/lib/rubydb/storage/row.rb +78 -0
  372. data/lib/rubydb/storage/serializer.rb +51 -0
  373. data/lib/rubydb/storage/storage_layout.rb +151 -0
  374. data/lib/rubydb/storage/storage_manager.rb +114 -0
  375. data/lib/rubydb/storage/tuple.rb +461 -0
  376. data/lib/rubydb/storage/visibility_map.rb +984 -0
  377. data/lib/rubydb/transactions/commit_manager.rb +220 -0
  378. data/lib/rubydb/transactions/isolation.rb +98 -0
  379. data/lib/rubydb/transactions/lock.rb +76 -0
  380. data/lib/rubydb/transactions/lock_manager.rb +362 -0
  381. data/lib/rubydb/transactions/savepoint.rb +143 -0
  382. data/lib/rubydb/transactions/transaction.rb +215 -0
  383. data/lib/rubydb/transactions/transaction_id.rb +84 -0
  384. data/lib/rubydb/transactions/transaction_log.rb +257 -0
  385. data/lib/rubydb/transactions/transaction_manager.rb +435 -0
  386. data/lib/rubydb/types/bigint.rb +36 -0
  387. data/lib/rubydb/types/blob.rb +37 -0
  388. data/lib/rubydb/types/boolean.rb +34 -0
  389. data/lib/rubydb/types/date.rb +39 -0
  390. data/lib/rubydb/types/decimal.rb +48 -0
  391. data/lib/rubydb/types/float.rb +34 -0
  392. data/lib/rubydb/types/integer.rb +36 -0
  393. data/lib/rubydb/types/json.rb +41 -0
  394. data/lib/rubydb/types/null.rb +34 -0
  395. data/lib/rubydb/types/smallint.rb +36 -0
  396. data/lib/rubydb/types/text.rb +37 -0
  397. data/lib/rubydb/types/time.rb +46 -0
  398. data/lib/rubydb/types/timestamp.rb +39 -0
  399. data/lib/rubydb/types/type.rb +119 -0
  400. data/lib/rubydb/types/uuid.rb +47 -0
  401. data/lib/rubydb/types/varchar.rb +37 -0
  402. data/lib/rubydb/version.rb +33 -0
  403. data/lib/rubydb/wal/archive.rb +193 -0
  404. data/lib/rubydb/wal/checkpoint.rb +183 -0
  405. data/lib/rubydb/wal/lsn.rb +94 -0
  406. data/lib/rubydb/wal/reader.rb +260 -0
  407. data/lib/rubydb/wal/record.rb +105 -0
  408. data/lib/rubydb/wal/segment.rb +193 -0
  409. data/lib/rubydb/wal/wal.rb +452 -0
  410. data/lib/rubydb/wal/writer.rb +236 -0
  411. data/lib/rubydb.rb +7 -0
  412. data/packaging/docker/Dockerfile +36 -0
  413. data/packaging/docker/docker-compose.failover.yml +43 -0
  414. data/packaging/docker/entrypoint.sh +15 -0
  415. data/packaging/homebrew/rubydb.rb +19 -0
  416. data/packaging/systemd/rubydb.service +22 -0
  417. data/rubydb.gemspec +57 -0
  418. data/scripts/benchmark +7 -0
  419. data/scripts/durability_drill +37 -0
  420. data/scripts/format +0 -0
  421. data/scripts/fuzz +63 -0
  422. data/scripts/lint +0 -0
  423. data/scripts/release +50 -0
  424. data/scripts/release_check +43 -0
  425. data/scripts/replication_failover_drill +250 -0
  426. data/scripts/replication_network_failover_drill +255 -0
  427. data/scripts/restore_drill +45 -0
  428. data/scripts/security +0 -0
  429. data/scripts/setup +0 -0
  430. data/scripts/test +0 -0
  431. data/scripts/test-chaos +0 -0
  432. data/scripts/test-crash +0 -0
  433. data/scripts/test-integration +0 -0
  434. data/scripts/test-unit +0 -0
  435. data/visibility_map.json +1 -0
  436. metadata +538 -0
@@ -0,0 +1,353 @@
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
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "zlib"
6
+ require "monitor"
7
+ require "digest"
8
+ require "pathname"
9
+ require_relative "../storage/engine"
10
+
11
+ module RubyDB
12
+ module Backup
13
+ # Restore - Restores database from backups
14
+ class Restore
15
+ attr_reader :stats
16
+
17
+ def initialize(engine, config = {})
18
+ @engine = engine
19
+ @config = config
20
+ @stats = {
21
+ restores: 0,
22
+ successful_restores: 0,
23
+ failed_restores: 0,
24
+ total_time_ms: 0,
25
+ avg_time_ms: 0,
26
+ last_restore: nil
27
+ }
28
+ @lock = Monitor.new
29
+ @incremental_restores = {}
30
+ end
31
+
32
+ def restore(backup_path, options = {})
33
+ @lock.synchronize do
34
+ start_time = Time.now
35
+ @stats[:restores] += 1
36
+
37
+ unless Dir.exist?(backup_path)
38
+ return { success: false, error: "Backup path does not exist" }
39
+ end
40
+
41
+ manifest_path = File.join(backup_path, "manifest.json")
42
+ unless File.exist?(manifest_path)
43
+ return { success: false, error: "Manifest not found" }
44
+ end
45
+
46
+ begin
47
+ metadata = JSON.parse(File.read(manifest_path), symbolize_names: true)
48
+
49
+ # Validate backup type
50
+ backup_type = metadata[:type]
51
+
52
+ # Perform restore based on type
53
+ result = case backup_type
54
+ when Backup::TYPE_FULL
55
+ restore_full(backup_path, metadata, options)
56
+ when Backup::TYPE_INCREMENTAL
57
+ restore_incremental(backup_path, metadata, options)
58
+ when Backup::TYPE_DIFFERENTIAL
59
+ restore_differential(backup_path, metadata, options)
60
+ else
61
+ restore_full(backup_path, metadata, options)
62
+ end
63
+
64
+ if result[:success]
65
+ @stats[:successful_restores] += 1
66
+ @stats[:last_restore] = Time.now
67
+
68
+ elapsed_ms = (Time.now - start_time) * 1000
69
+ @stats[:total_time_ms] += elapsed_ms
70
+ @stats[:avg_time_ms] = @stats[:total_time_ms] / @stats[:successful_restores]
71
+ else
72
+ @stats[:failed_restores] += 1
73
+ end
74
+
75
+ result.merge(elapsed_ms: (Time.now - start_time) * 1000)
76
+
77
+ rescue => e
78
+ @stats[:failed_restores] += 1
79
+ { success: false, error: e.message }
80
+ end
81
+ end
82
+ end
83
+
84
+ def restore_latest(options = {})
85
+ backups = list_available_backups
86
+ return { success: false, error: "No backups available" } if backups.empty?
87
+
88
+ latest = backups.first
89
+ restore(latest[:path], options)
90
+ end
91
+
92
+ def restore_point_in_time(time, options = {})
93
+ backups = list_available_backups
94
+ return { success: false, error: "No backups available" } if backups.empty?
95
+
96
+ # Find the closest backup before the target time
97
+ target = Time.parse(time)
98
+ candidates = backups.select do |b|
99
+ Time.parse(b[:created_at]) <= target
100
+ end
101
+
102
+ if candidates.empty?
103
+ return { success: false, error: "No backup before the specified time" }
104
+ end
105
+
106
+ backup = candidates.first
107
+ restore(backup[:path], options.merge(point_in_time: time))
108
+ end
109
+
110
+ def list_available_backups
111
+ backups = []
112
+ backup_dir = @config[:backup_dir] || "backups"
113
+ Dir.glob(File.join(backup_dir, "backup_*")).each do |path|
114
+ manifest_path = File.join(path, "manifest.json")
115
+ next unless File.exist?(manifest_path)
116
+
117
+ begin
118
+ metadata = JSON.parse(File.read(manifest_path), symbolize_names: true)
119
+ backups << metadata.merge(path: path)
120
+ rescue
121
+ # Skip corrupted manifests
122
+ end
123
+ end
124
+
125
+ backups.sort_by { |b| b[:created_at] }.reverse
126
+ end
127
+
128
+ def stats
129
+ @lock.synchronize do
130
+ @stats.merge({
131
+ incremental_restores: @incremental_restores.size
132
+ })
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def restore_full(backup_path, metadata, options)
139
+ destination = options[:destination] || @engine&.data_dir
140
+ raise ArgumentError, "Restore destination is required" unless destination
141
+ FileUtils.mkdir_p(destination)
142
+
143
+ # Restore data files
144
+ files = metadata[:files] || []
145
+ files.each do |file|
146
+ src = manifest_file_path(backup_path, file)
147
+ raise ArgumentError, "Invalid manifest file path: #{file}" unless src
148
+ next unless File.exist?(src)
149
+
150
+ # Determine destination
151
+ dest = file.start_with?("wal/") ? File.join(destination, "wal") : destination
152
+ FileUtils.mkdir_p(dest)
153
+
154
+ destination_name = File.basename(file).sub(/\.gz\z/, "")
155
+ dest_path = File.join(dest, destination_name)
156
+ restore_file(src, dest_path)
157
+ end
158
+
159
+ # Restore schema
160
+ schema_path = File.join(backup_path, "schema.sql")
161
+ if File.exist?(schema_path) && @engine
162
+ schema = File.read(schema_path)
163
+ @engine.execute_schema(schema)
164
+ end
165
+
166
+ { success: true, restored_from: metadata[:name] }
167
+ end
168
+
169
+ def restore_incremental(backup_path, metadata, options)
170
+ base_backup = metadata[:base_backup]
171
+ unless base_backup
172
+ return { success: false, error: "Base backup not specified" }
173
+ end
174
+
175
+ # Restore base backup first
176
+ base_path = backup_path_for_name(base_backup)
177
+ return { success: false, error: "Invalid base backup name" } unless base_path
178
+ unless Dir.exist?(base_path)
179
+ return { success: false, error: "Base backup not found" }
180
+ end
181
+
182
+ base_metadata = JSON.parse(File.read(File.join(base_path, "manifest.json")), symbolize_names: true)
183
+ base_result = restore_full(base_path, base_metadata, options)
184
+ return base_result unless base_result[:success]
185
+ apply_delta(backup_path, metadata, options)
186
+
187
+ { success: true, restored_from: metadata[:name], base: base_backup }
188
+ end
189
+
190
+ def restore_differential(backup_path, metadata, options)
191
+ base_backup = metadata[:base_backup]
192
+ unless base_backup
193
+ return { success: false, error: "Base backup not specified" }
194
+ end
195
+
196
+ # Restore base backup first
197
+ base_path = backup_path_for_name(base_backup)
198
+ return { success: false, error: "Invalid base backup name" } unless base_path
199
+ unless Dir.exist?(base_path)
200
+ return { success: false, error: "Base backup not found" }
201
+ end
202
+
203
+ base_metadata = JSON.parse(File.read(File.join(base_path, "manifest.json")), symbolize_names: true)
204
+ base_result = restore_full(base_path, base_metadata, options)
205
+ return base_result unless base_result[:success]
206
+ apply_delta(backup_path, metadata, options)
207
+
208
+ { success: true, restored_from: metadata[:name], base: base_backup }
209
+ end
210
+
211
+ def restore_file(src, dest)
212
+ if src.end_with?(".gz")
213
+ decompress_file(src, dest)
214
+ else
215
+ FileUtils.cp(src, dest)
216
+ end
217
+ end
218
+
219
+ def backup_path_for_name(name)
220
+ value = name.to_s
221
+ return nil if value.empty? || value != File.basename(value)
222
+
223
+ root = File.expand_path(@config[:backup_dir] || "backups")
224
+ path = File.expand_path(File.join(root, value))
225
+ path.start_with?("#{root}#{File::SEPARATOR}") ? path : nil
226
+ end
227
+
228
+ def manifest_file_path(root_path, relative_path)
229
+ relative = relative_path.to_s
230
+ return nil if relative.empty? || Pathname.new(relative).absolute?
231
+
232
+ root = File.expand_path(root_path)
233
+ path = File.expand_path(File.join(root, relative))
234
+ path.start_with?("#{root}#{File::SEPARATOR}") ? path : nil
235
+ end
236
+
237
+ def apply_delta(delta_path, metadata, options)
238
+ changes_path = File.join(delta_path, "changes.json")
239
+ return { success: false, error: "Delta changes file not found" } unless File.file?(changes_path)
240
+ if metadata[:checksum] && Digest::SHA256.file(changes_path).hexdigest != metadata[:checksum]
241
+ return { success: false, error: "Delta checksum mismatch" }
242
+ end
243
+
244
+ engine = @engine
245
+ owned_engine = false
246
+ if engine.nil?
247
+ destination = options[:destination]
248
+ database_file = Dir.glob(File.join(destination.to_s, "*.rdb")).first
249
+ return { success: false, error: "Engine or restored database is required for delta restore" } unless database_file
250
+ engine = Storage::Engine.new(database_file, auto_vacuum: false)
251
+ owned_engine = true
252
+ end
253
+ changes = JSON.parse(File.read(changes_path), symbolize_names: true).fetch(:changes)
254
+ changes.each { |change| apply_delta_change(engine, change) }
255
+ { success: true }
256
+ rescue StandardError => error
257
+ { success: false, error: error.message }
258
+ ensure
259
+ engine&.close if owned_engine
260
+ end
261
+
262
+ def apply_delta_change(engine, change)
263
+ table = change[:table_name] || change[:table]
264
+ case change[:type].to_s
265
+ when "insert"
266
+ columns = engine.table_columns(table)
267
+ values = (change[:values] || change[:data] || {}).each_with_object({}) { |(key, value), result| result[key.to_s] = value }
268
+ engine.insert_row(table, columns, values)
269
+ when "update"
270
+ values = (change[:values] || change[:data] || {}).each_with_object({}) { |(key, value), result| result[key.to_s] = value }
271
+ engine.update_row(table, change[:row_id], values, visibility_check: false)
272
+ when "delete"
273
+ engine.delete_row(table, change[:row_id], visibility_check: false)
274
+ else
275
+ raise ArgumentError, "Unsupported delta operation: #{change[:type]}"
276
+ end
277
+ end
278
+
279
+ def decompress_file(src, dest)
280
+ Zlib::GzipReader.open(src) do |gz|
281
+ File.open(dest, "wb") do |file|
282
+ while chunk = gz.read(10 * 1024 * 1024)
283
+ file.write(chunk)
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end