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,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module RubyDB
6
+ module History
7
+ # AsOf - Handles AS OF queries (time travel)
8
+ class AsOf
9
+ attr_reader :stats
10
+
11
+ def initialize(engine, history_manager, config = {})
12
+ @engine = engine
13
+ @history_manager = history_manager
14
+ @config = config
15
+ @stats = {
16
+ as_of_queries: 0,
17
+ as_of_restores: 0,
18
+ snapshot_queries: 0,
19
+ query_time_ms: 0,
20
+ avg_query_time_ms: 0
21
+ }
22
+ @lock = Mutex.new
23
+ @snapshot_cache = {}
24
+ @cache_size = config[:cache_size] || 100
25
+ end
26
+
27
+ def as_of(table_name, time, options = {})
28
+ @lock.synchronize do
29
+ start_time = Time.now
30
+ @stats[:as_of_queries] += 1
31
+
32
+ # Parse time
33
+ query_time = parse_time(time)
34
+
35
+ # Get changes up to time
36
+ changes = @history_manager.get_changes_for_table(table_name, query_time)
37
+
38
+ # Reconstruct state
39
+ result = reconstruct_state(table_name, changes, query_time, options)
40
+
41
+ elapsed_ms = (Time.now - start_time) * 1000
42
+ @stats[:query_time_ms] += elapsed_ms
43
+ @stats[:avg_query_time_ms] = @stats[:query_time_ms] / @stats[:as_of_queries]
44
+
45
+ result
46
+ end
47
+ end
48
+
49
+ def as_of_row(table_name, row_id, time)
50
+ @lock.synchronize do
51
+ start_time = Time.now
52
+ @stats[:as_of_queries] += 1
53
+
54
+ query_time = parse_time(time)
55
+ changes = @history_manager.get_changes_for_row(table_name, row_id)
56
+
57
+ # Find version at time
58
+ version = find_version_at_time(changes, row_id, query_time)
59
+
60
+ elapsed_ms = (Time.now - start_time) * 1000
61
+ @stats[:query_time_ms] += elapsed_ms
62
+ @stats[:avg_query_time_ms] = @stats[:query_time_ms] / @stats[:as_of_queries]
63
+
64
+ {
65
+ row_id: row_id,
66
+ table: table_name,
67
+ as_of: query_time.iso8601,
68
+ data: version,
69
+ exists: !version.nil?
70
+ }
71
+ end
72
+ end
73
+
74
+ def as_of_snapshot(time, options = {})
75
+ @lock.synchronize do
76
+ start_time = Time.now
77
+ @stats[:snapshot_queries] += 1
78
+
79
+ query_time = parse_time(time)
80
+
81
+ # Check cache
82
+ cache_key = "snapshot_#{query_time.to_i}"
83
+ if @snapshot_cache.key?(cache_key)
84
+ return @snapshot_cache[cache_key]
85
+ end
86
+
87
+ # Get all changes up to time
88
+ tables = options[:tables] || @engine.list_tables
89
+ snapshot = {}
90
+
91
+ tables.each do |table|
92
+ changes = @history_manager.get_changes_for_table(table, query_time)
93
+ snapshot[table] = reconstruct_state(table, changes, query_time)
94
+ end
95
+
96
+ result = {
97
+ as_of: query_time.iso8601,
98
+ snapshot: snapshot,
99
+ tables: tables.size
100
+ }
101
+
102
+ # Cache snapshot
103
+ if @snapshot_cache.size < @cache_size
104
+ @snapshot_cache[cache_key] = result
105
+ end
106
+
107
+ elapsed_ms = (Time.now - start_time) * 1000
108
+ @stats[:query_time_ms] += elapsed_ms
109
+ @stats[:avg_query_time_ms] = @stats[:query_time_ms] / @stats[:snapshot_queries]
110
+
111
+ result
112
+ end
113
+ end
114
+
115
+ def restore_to_time(time, options = {})
116
+ @lock.synchronize do
117
+ start_time = Time.now
118
+ @stats[:as_of_restores] += 1
119
+
120
+ query_time = parse_time(time)
121
+
122
+ # Get all changes up to time
123
+ changes = @history_manager.get_changes_until(query_time)
124
+
125
+ # Apply changes in reverse to reach desired state
126
+ restore_changes = changes.reverse
127
+ restore_changes.each do |change|
128
+ undo_change(change)
129
+ end
130
+
131
+ elapsed_ms = (Time.now - start_time) * 1000
132
+ @stats[:query_time_ms] += elapsed_ms
133
+ @stats[:avg_query_time_ms] = @stats[:query_time_ms] / @stats[:as_of_restores]
134
+
135
+ {
136
+ success: true,
137
+ restored_to: query_time.iso8601,
138
+ changes_applied: changes.size
139
+ }
140
+ end
141
+ end
142
+
143
+ def clear_cache
144
+ @lock.synchronize do
145
+ @snapshot_cache.clear
146
+ end
147
+ end
148
+
149
+ def stats
150
+ @lock.synchronize do
151
+ @stats.merge({
152
+ cache_size: @snapshot_cache.size,
153
+ max_cache_size: @cache_size
154
+ })
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def parse_time(time)
161
+ case time
162
+ when Time
163
+ time
164
+ when String
165
+ Time.parse(time)
166
+ when Integer
167
+ Time.at(time)
168
+ when DateTime
169
+ time.to_time
170
+ else
171
+ raise ArgumentError, "Invalid time format: #{time}"
172
+ end
173
+ end
174
+
175
+ def reconstruct_state(table_name, changes, time, options = {})
176
+ rows = {}
177
+ columns = options[:columns]
178
+
179
+ changes.each do |change|
180
+ case change.operation
181
+ when Change::OP_INSERT
182
+ rows[change.row_id] = change.new_values
183
+ when Change::OP_UPDATE
184
+ if rows.key?(change.row_id)
185
+ rows[change.row_id] = rows[change.row_id].merge(change.new_values)
186
+ else
187
+ # Row might have been inserted before tracking started
188
+ rows[change.row_id] = change.new_values
189
+ end
190
+ when Change::OP_DELETE
191
+ rows.delete(change.row_id)
192
+ end
193
+ end
194
+
195
+ # Apply column filter
196
+ if columns
197
+ rows.each do |row_id, data|
198
+ rows[row_id] = data.select { |k| columns.include?(k) }
199
+ end
200
+ end
201
+
202
+ {
203
+ table: table_name,
204
+ as_of: time.iso8601,
205
+ rows: rows.values,
206
+ count: rows.size
207
+ }
208
+ end
209
+
210
+ def find_version_at_time(changes, row_id, time)
211
+ current = nil
212
+
213
+ changes.each do |change|
214
+ if change.row_id == row_id && change.timestamp <= time
215
+ if change.operation == Change::OP_DELETE
216
+ current = nil
217
+ elsif change.operation == Change::OP_UPDATE || change.operation == Change::OP_INSERT
218
+ current = (current || {}).merge(change.new_values)
219
+ end
220
+ end
221
+ end
222
+
223
+ current
224
+ end
225
+
226
+ def undo_change(change)
227
+ case change.operation
228
+ when Change::OP_INSERT
229
+ @engine.delete_row(change.table_name, change.row_id)
230
+ when Change::OP_UPDATE
231
+ @engine.update_row(change.table_name, change.row_id, change.old_values)
232
+ when Change::OP_DELETE
233
+ @engine.insert_row(change.table_name, change.old_values.keys, change.old_values)
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "json"
5
+
6
+ module RubyDB
7
+ module History
8
+ # Change - Represents a single change in history
9
+ class Change
10
+ attr_reader :id, :timestamp, :transaction_id, :table_name
11
+ attr_reader :row_id, :operation, :old_values, :new_values
12
+ attr_reader :user, :branch, :lsn, :metadata
13
+
14
+ # Operation types
15
+ OP_INSERT = :insert
16
+ OP_UPDATE = :update
17
+ OP_DELETE = :delete
18
+ OP_CREATE_TABLE = :create_table
19
+ OP_DROP_TABLE = :drop_table
20
+ OP_ALTER_TABLE = :alter_table
21
+ OP_CREATE_INDEX = :create_index
22
+ OP_DROP_INDEX = :drop_index
23
+
24
+ def initialize(attributes = {})
25
+ @id = attributes[:id] || generate_change_id
26
+ @timestamp = attributes[:timestamp] || Time.now
27
+ @transaction_id = attributes[:transaction_id]
28
+ @table_name = attributes[:table_name]
29
+ @row_id = attributes[:row_id]
30
+ @operation = attributes[:operation]
31
+ @old_values = attributes[:old_values] || {}
32
+ @new_values = attributes[:new_values] || {}
33
+ @user = attributes[:user]
34
+ @branch = attributes[:branch]
35
+ @lsn = attributes[:lsn]
36
+ @metadata = attributes[:metadata] || {}
37
+ end
38
+
39
+ def insert?
40
+ @operation == OP_INSERT
41
+ end
42
+
43
+ def update?
44
+ @operation == OP_UPDATE
45
+ end
46
+
47
+ def delete?
48
+ @operation == OP_DELETE
49
+ end
50
+
51
+ def ddl?
52
+ [:create_table, :drop_table, :alter_table, :create_index, :drop_index].include?(@operation)
53
+ end
54
+
55
+ def to_hash
56
+ {
57
+ id: @id,
58
+ timestamp: @timestamp.iso8601,
59
+ transaction_id: @transaction_id,
60
+ table_name: @table_name,
61
+ row_id: @row_id,
62
+ operation: @operation,
63
+ old_values: @old_values,
64
+ new_values: @new_values,
65
+ user: @user,
66
+ branch: @branch,
67
+ lsn: @lsn,
68
+ metadata: @metadata
69
+ }
70
+ end
71
+
72
+ def to_json
73
+ JSON.generate(to_hash)
74
+ end
75
+
76
+ def self.from_json(json_data)
77
+ data = JSON.parse(json_data, symbolize_names: true)
78
+ new(
79
+ id: data[:id],
80
+ timestamp: Time.parse(data[:timestamp]),
81
+ transaction_id: data[:transaction_id],
82
+ table_name: data[:table_name],
83
+ row_id: data[:row_id],
84
+ operation: data[:operation].to_sym,
85
+ old_values: data[:old_values],
86
+ new_values: data[:new_values],
87
+ user: data[:user],
88
+ branch: data[:branch],
89
+ lsn: data[:lsn],
90
+ metadata: data[:metadata] || {}
91
+ )
92
+ end
93
+
94
+ def inspect
95
+ "#<Change id=#{@id} op=#{@operation} table=#{@table_name} row=#{@row_id}>"
96
+ end
97
+
98
+ private
99
+
100
+ def generate_change_id
101
+ "change_#{Time.now.to_i}_#{rand(10000)}"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module RubyDB
6
+ module History
7
+ # History - Main history interface
8
+ class History
9
+ attr_reader :manager, :as_of, :temporal_query, :stats
10
+
11
+ def initialize(engine, config = {})
12
+ @engine = engine
13
+ @config = config
14
+ @manager = HistoryManager.new(engine, config)
15
+ @as_of = AsOf.new(engine, @manager, config)
16
+ @temporal_query = TemporalQuery.new(engine, @manager, config)
17
+ @stats = {
18
+ operations: 0,
19
+ records: 0,
20
+ queries: 0,
21
+ restores: 0
22
+ }
23
+ @lock = Mutex.new
24
+ end
25
+
26
+ def record(change_data)
27
+ @lock.synchronize do
28
+ change = Change.new(change_data)
29
+ @manager.record_change(change)
30
+ @stats[:records] += 1
31
+ change
32
+ end
33
+ end
34
+
35
+ def record_insert(table_name, row_id, values, options = {})
36
+ record({
37
+ table_name: table_name,
38
+ row_id: row_id,
39
+ operation: Change::OP_INSERT,
40
+ new_values: values,
41
+ user: options[:user],
42
+ branch: options[:branch],
43
+ transaction_id: options[:transaction_id]
44
+ })
45
+ end
46
+
47
+ def record_update(table_name, row_id, old_values, new_values, options = {})
48
+ record({
49
+ table_name: table_name,
50
+ row_id: row_id,
51
+ operation: Change::OP_UPDATE,
52
+ old_values: old_values,
53
+ new_values: new_values,
54
+ user: options[:user],
55
+ branch: options[:branch],
56
+ transaction_id: options[:transaction_id]
57
+ })
58
+ end
59
+
60
+ def record_delete(table_name, row_id, old_values, options = {})
61
+ record({
62
+ table_name: table_name,
63
+ row_id: row_id,
64
+ operation: Change::OP_DELETE,
65
+ old_values: old_values,
66
+ user: options[:user],
67
+ branch: options[:branch],
68
+ transaction_id: options[:transaction_id]
69
+ })
70
+ end
71
+
72
+ def query_as_of(table_name, time, columns = nil)
73
+ @stats[:queries] += 1
74
+ @as_of.as_of(table_name, time, columns: columns)
75
+ end
76
+
77
+ def query_between(table_name, start_time, end_time, columns = nil)
78
+ @stats[:queries] += 1
79
+ @temporal_query.query_between(table_name, start_time, end_time, columns)
80
+ end
81
+
82
+ def query_version(table_name, row_id, time)
83
+ @stats[:queries] += 1
84
+ @as_of.as_of_row(table_name, row_id, time)
85
+ end
86
+
87
+ def query_history(table_name, row_id)
88
+ @stats[:queries] += 1
89
+ @temporal_query.query_history(table_name, row_id)
90
+ end
91
+
92
+ def query_snapshot(time, options = {})
93
+ @stats[:queries] += 1
94
+ @as_of.as_of_snapshot(time, options)
95
+ end
96
+
97
+ def restore_to_time(time, options = {})
98
+ @stats[:restores] += 1
99
+ @as_of.restore_to_time(time, options)
100
+ end
101
+
102
+ def get_timeline(name = "main")
103
+ @manager.get_timeline(name)
104
+ end
105
+
106
+ def delete_timeline(name)
107
+ @manager.delete_timeline(name)
108
+ end
109
+
110
+ def prune(keep_count = nil)
111
+ @manager.prune_history(keep_count)
112
+ end
113
+
114
+ def clear
115
+ @manager.clear_history
116
+ @as_of.clear_cache
117
+ @temporal_query.clear_cache
118
+ end
119
+
120
+ def stats
121
+ @lock.synchronize do
122
+ @stats.merge(@manager.stats)
123
+ end
124
+ end
125
+
126
+ def history_size
127
+ @manager.history_size
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require "monitor"
7
+
8
+ module RubyDB
9
+ module History
10
+ # HistoryManager - Manages all history operations
11
+ class HistoryManager
12
+ attr_reader :stats
13
+
14
+ def initialize(engine, config = {})
15
+ @engine = engine
16
+ @config = config
17
+ @history_dir = config[:history_dir] || "history"
18
+ @max_history = config[:max_history] || 10000
19
+ @retention_days = config[:retention_days] || 30
20
+ @changes = []
21
+ @timelines = {}
22
+ @change_index = {}
23
+ @stats = {
24
+ changes_recorded: 0,
25
+ changes_queried: 0,
26
+ changes_deleted: 0,
27
+ timelines_created: 0,
28
+ timelines_deleted: 0,
29
+ history_size: 0
30
+ }
31
+ @lock = Monitor.new
32
+
33
+ FileUtils.mkdir_p(@history_dir)
34
+ load_history
35
+ end
36
+
37
+ def record_change(change)
38
+ @lock.synchronize do
39
+ @changes << change
40
+ @stats[:changes_recorded] += 1
41
+ @stats[:history_size] = @changes.size
42
+
43
+ # Update index
44
+ @change_index[change.id] = change
45
+ @change_index["#{change.table_name}:#{change.row_id}"] ||= []
46
+ @change_index["#{change.table_name}:#{change.row_id}"] << change
47
+
48
+ # Update timeline
49
+ timeline_name = change.branch || "main"
50
+ get_timeline(timeline_name).add_change(change)
51
+
52
+ # Prune if needed
53
+ prune_history if @changes.size > @max_history
54
+
55
+ save_history
56
+ end
57
+ end
58
+
59
+ def get_changes_for_table(table_name, time = nil)
60
+ @lock.synchronize do
61
+ @stats[:changes_queried] += 1
62
+
63
+ changes = @changes.select do |c|
64
+ c.table_name == table_name && (time.nil? || c.timestamp <= time)
65
+ end
66
+
67
+ changes.sort_by(&:timestamp)
68
+ end
69
+ end
70
+
71
+ def get_changes_for_row(table_name, row_id)
72
+ @lock.synchronize do
73
+ @stats[:changes_queried] += 1
74
+ @change_index["#{table_name}:#{row_id}"] || []
75
+ end
76
+ end
77
+
78
+ def get_changes_between(table_name, start_time, end_time)
79
+ @lock.synchronize do
80
+ @stats[:changes_queried] += 1
81
+
82
+ @changes.select do |c|
83
+ c.table_name == table_name &&
84
+ c.timestamp >= start_time &&
85
+ c.timestamp <= end_time
86
+ end
87
+ end
88
+ end
89
+
90
+ def get_changes_until(time)
91
+ @lock.synchronize do
92
+ @stats[:changes_queried] += 1
93
+
94
+ @changes.select { |c| c.timestamp <= time }
95
+ end
96
+ end
97
+
98
+ def get_changes_by_user(user)
99
+ @lock.synchronize do
100
+ @stats[:changes_queried] += 1
101
+
102
+ @changes.select { |c| c.user == user }
103
+ end
104
+ end
105
+
106
+ def get_changes_by_branch(branch)
107
+ @lock.synchronize do
108
+ @stats[:changes_queried] += 1
109
+
110
+ @changes.select { |c| c.branch == branch }
111
+ end
112
+ end
113
+
114
+ def get_timeline(name)
115
+ @lock.synchronize do
116
+ @timelines[name] ||= Timeline.new(name)
117
+ end
118
+ end
119
+
120
+ def delete_timeline(name)
121
+ @lock.synchronize do
122
+ timeline = @timelines.delete(name)
123
+ @stats[:timelines_deleted] += 1 if timeline
124
+ timeline
125
+ end
126
+ end
127
+
128
+ def prune_history(keep_count = nil)
129
+ @lock.synchronize do
130
+ keep_count ||= @max_history
131
+
132
+ if @changes.size > keep_count
133
+ removed = @changes.shift(@changes.size - keep_count)
134
+ @stats[:changes_deleted] += removed.size
135
+
136
+ # Clean up index
137
+ removed.each do |change|
138
+ @change_index.delete(change.id)
139
+ key = "#{change.table_name}:#{change.row_id}"
140
+ if @change_index[key]
141
+ @change_index[key].delete(change)
142
+ @change_index.delete(key) if @change_index[key].empty?
143
+ end
144
+ end
145
+
146
+ @stats[:history_size] = @changes.size
147
+ end
148
+ end
149
+ end
150
+
151
+ def clear_history
152
+ @lock.synchronize do
153
+ @changes.clear
154
+ @change_index.clear
155
+ @timelines.clear
156
+ @stats[:history_size] = 0
157
+ @stats[:changes_deleted] += 1
158
+ save_history
159
+ end
160
+ end
161
+
162
+ def history_size
163
+ @changes.size
164
+ end
165
+
166
+ def stats
167
+ @lock.synchronize do
168
+ @stats.merge({
169
+ timelines: @timelines.size,
170
+ max_history: @max_history,
171
+ retention_days: @retention_days,
172
+ history_dir: @history_dir
173
+ })
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def load_history
180
+ history_file = File.join(@history_dir, "history.json")
181
+ return unless File.exist?(history_file)
182
+
183
+ begin
184
+ data = JSON.parse(File.read(history_file), symbolize_names: true)
185
+
186
+ data[:changes].each do |change_data|
187
+ change = Change.from_json(change_data.to_json)
188
+ @changes << change
189
+ @change_index[change.id] = change
190
+ key = "#{change.table_name}:#{change.row_id}"
191
+ @change_index[key] ||= []
192
+ @change_index[key] << change
193
+ end
194
+
195
+ data[:timelines].each do |name, timeline_data|
196
+ timeline = Timeline.from_json(timeline_data.to_json)
197
+ @timelines[name] = timeline
198
+ end
199
+
200
+ @stats[:changes_recorded] = data[:stats][:changes_recorded] || 0
201
+ @stats[:history_size] = @changes.size
202
+
203
+ rescue => e
204
+ @changes = []
205
+ @change_index = {}
206
+ @timelines = {}
207
+ end
208
+ end
209
+
210
+ def save_history
211
+ data = {
212
+ changes: @changes.map(&:to_hash),
213
+ timelines: @timelines.transform_values(&:to_hash),
214
+ stats: {
215
+ changes_recorded: @stats[:changes_recorded],
216
+ changes_deleted: @stats[:changes_deleted],
217
+ timelines_created: @stats[:timelines_created],
218
+ updated_at: Time.now.iso8601
219
+ }
220
+ }
221
+
222
+ history_file = File.join(@history_dir, "history.json")
223
+ temp_file = "#{history_file}.tmp"
224
+ File.write(temp_file, JSON.generate(data))
225
+ File.rename(temp_file, history_file)
226
+ end
227
+ end
228
+ end
229
+ end