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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module RubyDB
6
+ module Recovery
7
+ # Checkpoint - Manages recovery checkpoints
8
+ class Checkpoint
9
+ attr_reader :last_checkpoint, :stats
10
+
11
+ def initialize(engine, wal, config = {})
12
+ @engine = engine
13
+ @wal = wal
14
+ @config = config
15
+ @last_checkpoint = nil
16
+ @checkpoint_history = []
17
+ @max_history = config[:max_history] || 100
18
+ @stats = {
19
+ checkpoints_created: 0,
20
+ checkpoints_restored: 0,
21
+ checkpoint_failures: 0,
22
+ total_checkpoint_time_ms: 0,
23
+ avg_checkpoint_time_ms: 0,
24
+ last_checkpoint_size: 0
25
+ }
26
+ @lock = Mutex.new
27
+
28
+ # Load checkpoint history
29
+ load_checkpoint_history
30
+ end
31
+
32
+ def create_checkpoint(force = false)
33
+ @lock.synchronize do
34
+ start_time = Time.now
35
+
36
+ begin
37
+ # Flush all pending changes
38
+ @engine.flush
39
+ @wal.flush
40
+
41
+ # Get current state
42
+ current_state = capture_current_state
43
+
44
+ # Create checkpoint record
45
+ checkpoint_data = {
46
+ lsn: @wal.current_lsn.to_s,
47
+ timestamp: Time.now.iso8601,
48
+ wal_segment: @wal.writer.current_segment.segment_id,
49
+ wal_offset: @wal.current_lsn.offset,
50
+ state: current_state,
51
+ tables: list_tables,
52
+ sequences: list_sequences,
53
+ indexes: list_indexes,
54
+ transaction_count: active_transaction_count
55
+ }
56
+
57
+ # Write checkpoint record to WAL
58
+ record = Record.new(:checkpoint, checkpoint_data)
59
+ @wal.write(record)
60
+
61
+ # Update checkpoint info
62
+ @last_checkpoint = checkpoint_data
63
+ @checkpoint_history << checkpoint_data
64
+
65
+ # Trim history
66
+ if @checkpoint_history.size > @max_history
67
+ @checkpoint_history.shift
68
+ end
69
+
70
+ # Save checkpoint history
71
+ save_checkpoint_history
72
+
73
+ elapsed_ms = (Time.now - start_time) * 1000
74
+ @stats[:checkpoints_created] += 1
75
+ @stats[:total_checkpoint_time_ms] += elapsed_ms
76
+ @stats[:avg_checkpoint_time_ms] = @stats[:total_checkpoint_time_ms] / @stats[:checkpoints_created]
77
+ @stats[:last_checkpoint_size] = estimate_checkpoint_size
78
+
79
+ true
80
+
81
+ rescue => e
82
+ @stats[:checkpoint_failures] += 1
83
+ raise
84
+ end
85
+ end
86
+ end
87
+
88
+ def restore_checkpoint
89
+ @lock.synchronize do
90
+ start_time = Time.now
91
+
92
+ begin
93
+ # Find the latest checkpoint in the WAL
94
+ all_records = @wal.read_all
95
+ checkpoint_records = all_records.select { |r| r.type == :checkpoint }
96
+
97
+ if checkpoint_records.empty?
98
+ return nil
99
+ end
100
+
101
+ # Get the latest checkpoint
102
+ latest = checkpoint_records.last
103
+ checkpoint_data = latest.data
104
+
105
+ @last_checkpoint = checkpoint_data
106
+ @stats[:checkpoints_restored] += 1
107
+
108
+ checkpoint_data
109
+
110
+ rescue => e
111
+ @stats[:checkpoint_failures] += 1
112
+ raise
113
+ end
114
+ end
115
+ end
116
+
117
+ def list_checkpoints
118
+ @checkpoint_history.dup
119
+ end
120
+
121
+ def checkpoint_exists?
122
+ !@last_checkpoint.nil?
123
+ end
124
+
125
+ def checkpoint_age
126
+ return nil unless @last_checkpoint
127
+ Time.now - Time.parse(@last_checkpoint[:timestamp])
128
+ end
129
+
130
+ def stats
131
+ @lock.synchronize do
132
+ @stats.merge({
133
+ last_checkpoint: @last_checkpoint,
134
+ checkpoint_age: checkpoint_age,
135
+ history_size: @checkpoint_history.size,
136
+ max_history: @max_history
137
+ })
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def capture_current_state
144
+ {
145
+ active_transactions: active_transactions,
146
+ committed_transactions: committed_transactions,
147
+ sequences: sequence_values,
148
+ database_version: database_version,
149
+ last_operation_time: Time.now.iso8601
150
+ }
151
+ end
152
+
153
+ def active_transactions
154
+ # Get active transaction IDs
155
+ if @engine.respond_to?(:transaction_manager)
156
+ @engine.transaction_manager.active_transactions.keys
157
+ else
158
+ []
159
+ end
160
+ end
161
+
162
+ def active_transaction_count
163
+ active_transactions.size
164
+ end
165
+
166
+ def committed_transactions
167
+ if @engine.respond_to?(:transaction_manager)
168
+ @engine.transaction_manager.committed_transactions.map { |t| t.id }
169
+ else
170
+ []
171
+ end
172
+ end
173
+
174
+ def sequence_values
175
+ # Get current sequence values
176
+ if @engine.respond_to?(:catalog)
177
+ values = {}
178
+ @engine.catalog.sequences.each do |name, seq|
179
+ values[name] = seq.current_value
180
+ end
181
+ values
182
+ else
183
+ {}
184
+ end
185
+ end
186
+
187
+ def database_version
188
+ @engine.respond_to?(:version) ? @engine.version : "1.0"
189
+ end
190
+
191
+ def list_tables
192
+ @engine.list_tables rescue []
193
+ end
194
+
195
+ def list_sequences
196
+ if @engine.respond_to?(:catalog)
197
+ @engine.catalog.sequences.keys
198
+ else
199
+ []
200
+ end
201
+ end
202
+
203
+ def list_indexes
204
+ if @engine.respond_to?(:index_manager)
205
+ @engine.index_manager.indexes.keys
206
+ else
207
+ []
208
+ end
209
+ end
210
+
211
+ def estimate_checkpoint_size
212
+ # Estimate checkpoint size in bytes
213
+ tables = list_tables
214
+ size = 1024 # Base overhead
215
+
216
+ tables.each do |table|
217
+ row_count = @engine.table_row_count(table) rescue 0
218
+ size += row_count * 100 # Rough estimate per row
219
+ end
220
+
221
+ size
222
+ end
223
+
224
+ def load_checkpoint_history
225
+ @lock.synchronize do
226
+ history_path = checkpoint_history_path
227
+ if File.exist?(history_path)
228
+ data = File.read(history_path)
229
+ parsed = JSON.parse(data, symbolize_names: true)
230
+ @checkpoint_history = parsed[:history] || []
231
+ @last_checkpoint = parsed[:last_checkpoint]
232
+ end
233
+ end
234
+ rescue
235
+ @checkpoint_history = []
236
+ end
237
+
238
+ def save_checkpoint_history
239
+ @lock.synchronize do
240
+ data = {
241
+ history: @checkpoint_history,
242
+ last_checkpoint: @last_checkpoint,
243
+ timestamp: Time.now.iso8601
244
+ }
245
+ File.write(checkpoint_history_path, JSON.generate(data))
246
+ end
247
+ end
248
+
249
+ def checkpoint_history_path
250
+ @checkpoint_history_path ||= File.join(
251
+ @engine.instance_variable_get(:@path),
252
+ "checkpoint_history.json"
253
+ )
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1,467 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module RubyDB
6
+ module Recovery
7
+ # CorruptionDetector - Detects database corruption
8
+ class CorruptionDetector
9
+ attr_reader :stats
10
+
11
+ def initialize(engine, config = {})
12
+ @engine = engine
13
+ @config = config
14
+ @checksums = {}
15
+ @checksum_cache = {}
16
+ @stats = {
17
+ detections: 0,
18
+ false_positives: 0,
19
+ total_checks: 0,
20
+ corrupted_pages: 0,
21
+ corrupted_records: 0,
22
+ corrupted_indexes: 0,
23
+ last_detection_time: nil,
24
+ last_detection_result: nil
25
+ }
26
+ @lock = Mutex.new
27
+ @page_checksums = {}
28
+ end
29
+
30
+ def detect_corruption(scan_all = false)
31
+ @lock.synchronize do
32
+ @stats[:total_checks] += 1
33
+ results = {
34
+ corrupted: false,
35
+ issues: [],
36
+ pages: [],
37
+ records: [],
38
+ indexes: [],
39
+ recommendations: []
40
+ }
41
+
42
+ # Check page corruption
43
+ page_results = check_pages(scan_all)
44
+ results[:pages] = page_results[:corrupted]
45
+ results[:issues].concat(page_results[:issues])
46
+ results[:corrupted] = true if page_results[:corrupted].any?
47
+
48
+ # Check record corruption
49
+ record_results = check_records(scan_all)
50
+ results[:records] = record_results[:corrupted]
51
+ results[:issues].concat(record_results[:issues])
52
+ results[:corrupted] = true if record_results[:corrupted].any?
53
+
54
+ # Check index corruption
55
+ index_results = check_indexes(scan_all)
56
+ results[:indexes] = index_results[:corrupted]
57
+ results[:issues].concat(index_results[:issues])
58
+ results[:corrupted] = true if index_results[:corrupted].any?
59
+
60
+ # Generate recommendations
61
+ results[:recommendations] = generate_recommendations(results)
62
+
63
+ # Update stats
64
+ if results[:corrupted]
65
+ @stats[:detections] += 1
66
+ @stats[:corrupted_pages] = results[:pages].size
67
+ @stats[:corrupted_records] = results[:records].size
68
+ @stats[:corrupted_indexes] = results[:indexes].size
69
+ @stats[:last_detection_time] = Time.now
70
+ @stats[:last_detection_result] = results
71
+ end
72
+
73
+ results
74
+ end
75
+ end
76
+
77
+ def check_page(page_number)
78
+ @lock.synchronize do
79
+ result = { corrupted: false, issues: [] }
80
+
81
+ begin
82
+ page = @engine.read_page(page_number)
83
+ return result unless page
84
+
85
+ # Check page header
86
+ header = page.header
87
+ if header.nil?
88
+ result[:corrupted] = true
89
+ result[:issues] << { type: "page_header", page: page_number, message: "Missing header" }
90
+ return result
91
+ end
92
+
93
+ # Check page size
94
+ if header.page_size != page.size
95
+ result[:corrupted] = true
96
+ result[:issues] << { type: "page_size", page: page_number, message: "Invalid page size" }
97
+ end
98
+
99
+ # Check page checksum
100
+ if checksum_mismatch?(page)
101
+ result[:corrupted] = true
102
+ result[:issues] << { type: "checksum", page: page_number, message: "Checksum mismatch" }
103
+ end
104
+
105
+ # Check page data
106
+ if page.data.nil? || page.data.bytesize != page.size
107
+ result[:corrupted] = true
108
+ result[:issues] << { type: "page_data", page: page_number, message: "Invalid page data" }
109
+ end
110
+
111
+ rescue => e
112
+ result[:corrupted] = true
113
+ result[:issues] << { type: "page_read", page: page_number, message: e.message }
114
+ end
115
+
116
+ result
117
+ end
118
+ end
119
+
120
+ def check_record(table_name, row_id)
121
+ @lock.synchronize do
122
+ result = { corrupted: false, issues: [] }
123
+
124
+ begin
125
+ columns = @engine.table_columns(table_name)
126
+ row = @engine.select_row(table_name, row_id, columns)
127
+ return result unless row
128
+
129
+ # Validate each column value
130
+ columns.each do |col|
131
+ value = row[col.name]
132
+ next if value.nil? && col.nullable?
133
+
134
+ # Check type validity
135
+ unless valid_value_type?(value, col.type_class)
136
+ result[:corrupted] = true
137
+ result[:issues] << {
138
+ type: "record_value",
139
+ table: table_name,
140
+ row: row_id,
141
+ column: col.name,
142
+ message: "Invalid value type"
143
+ }
144
+ end
145
+ end
146
+
147
+ rescue => e
148
+ result[:corrupted] = true
149
+ result[:issues] << {
150
+ type: "record_read",
151
+ table: table_name,
152
+ row: row_id,
153
+ message: e.message
154
+ }
155
+ end
156
+
157
+ result
158
+ end
159
+ end
160
+
161
+ def check_index(index_name)
162
+ @lock.synchronize do
163
+ result = { corrupted: false, issues: [] }
164
+
165
+ begin
166
+ if @engine.respond_to?(:index_manager)
167
+ index = @engine.index_manager.get_index(index_name)
168
+ return result unless index
169
+
170
+ # Validate index
171
+ if index.respond_to?(:validate)
172
+ index.validate
173
+ end
174
+
175
+ # Check index entries
176
+ entries = index.entries_count
177
+ if entries < 0
178
+ result[:corrupted] = true
179
+ result[:issues] << {
180
+ type: "index_entries",
181
+ index: index_name,
182
+ message: "Invalid entry count"
183
+ }
184
+ end
185
+ end
186
+
187
+ rescue => e
188
+ result[:corrupted] = true
189
+ result[:issues] << {
190
+ type: "index_check",
191
+ index: index_name,
192
+ message: e.message
193
+ }
194
+ end
195
+
196
+ result
197
+ end
198
+ end
199
+
200
+ def repair_corruption(corruption_info)
201
+ @lock.synchronize do
202
+ results = { repaired: false, actions: [] }
203
+
204
+ issues = Array(corruption_info[:issues])
205
+ issues.each do |issue|
206
+ case issue[:type]
207
+ when "page_header"
208
+ repaired = repair_page_header(issue[:page])
209
+ results[:actions] << { type: "repair_page_header", page: issue[:page], success: repaired }
210
+ when "checksum"
211
+ repaired = repair_checksum(issue[:page])
212
+ results[:actions] << { type: "repair_checksum", page: issue[:page], success: repaired }
213
+ when "record_value"
214
+ repaired = repair_record(issue[:table], issue[:row], issue[:column])
215
+ results[:actions] << {
216
+ type: "repair_record",
217
+ table: issue[:table],
218
+ row: issue[:row],
219
+ column: issue[:column],
220
+ success: repaired
221
+ }
222
+ when "index_entries"
223
+ repaired = repair_index(issue[:index])
224
+ results[:actions] << { type: "repair_index", index: issue[:index], success: repaired }
225
+ else
226
+ results[:actions] << {
227
+ type: "unsupported_repair",
228
+ issue: issue[:type],
229
+ success: false
230
+ }
231
+ end
232
+ end
233
+
234
+ results[:repaired] = results[:actions].any? && results[:actions].all? { |a| a[:success] }
235
+ results
236
+ end
237
+ end
238
+
239
+ def stats
240
+ @lock.synchronize do
241
+ @stats.merge({
242
+ checksum_cache_size: @checksum_cache.size,
243
+ page_checksums: @page_checksums.size,
244
+ last_detection: @stats[:last_detection_time]&.iso8601
245
+ })
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ def check_pages(scan_all)
252
+ result = { corrupted: [], issues: [] }
253
+
254
+ begin
255
+ page_manager = @engine.page_manager
256
+ return result unless page_manager
257
+
258
+ pages_to_check = scan_all ? (0...page_manager.total_pages) : [page_manager.current_page]
259
+
260
+ pages_to_check.each do |page_number|
261
+ page_result = check_page(page_number)
262
+ if page_result[:corrupted]
263
+ result[:corrupted] << page_number
264
+ result[:issues].concat(page_result[:issues])
265
+ end
266
+ end
267
+
268
+ rescue => e
269
+ result[:issues] << { type: "page_scan", message: e.message }
270
+ end
271
+
272
+ result
273
+ end
274
+
275
+ def check_records(scan_all)
276
+ result = { corrupted: [], issues: [] }
277
+
278
+ begin
279
+ tables = @engine.list_tables
280
+ return result if tables.empty?
281
+
282
+ tables.each do |table_name|
283
+ columns = @engine.table_columns(table_name)
284
+ rows = @engine.select_rows(table_name, columns)
285
+
286
+ sample_size = scan_all ? rows.size : [rows.size, 100].min
287
+ rows.first(sample_size).each do |row|
288
+ row_id = row[:_row_id] || row["_row_id"]
289
+ record_result = check_record(table_name, row_id)
290
+ if record_result[:corrupted]
291
+ result[:corrupted] << { table: table_name, row: row_id }
292
+ result[:issues].concat(record_result[:issues])
293
+ end
294
+ end
295
+ end
296
+
297
+ rescue => e
298
+ result[:issues] << { type: "record_scan", message: e.message }
299
+ end
300
+
301
+ result
302
+ end
303
+
304
+ def check_indexes(scan_all)
305
+ result = { corrupted: [], issues: [] }
306
+
307
+ begin
308
+ if @engine.respond_to?(:index_manager)
309
+ index_manager = @engine.index_manager
310
+ indexes = index_manager.indexes
311
+
312
+ indexes.each do |name, _|
313
+ index_result = check_index(name)
314
+ if index_result[:corrupted]
315
+ result[:corrupted] << name
316
+ result[:issues].concat(index_result[:issues])
317
+ end
318
+ end
319
+ end
320
+
321
+ rescue => e
322
+ result[:issues] << { type: "index_scan", message: e.message }
323
+ end
324
+
325
+ result
326
+ end
327
+
328
+ def checksum_mismatch?(page)
329
+ # Calculate page checksum
330
+ current_checksum = calculate_checksum(page.data)
331
+ stored_checksum = page.header.checksum rescue 0
332
+
333
+ current_checksum != stored_checksum
334
+ end
335
+
336
+ def calculate_checksum(data)
337
+ Digest::SHA256.hexdigest(data)[0...16]
338
+ end
339
+
340
+ def valid_value_type?(value, type)
341
+ return true if value.nil?
342
+
343
+ case type
344
+ when :integer, :bigint, :smallint
345
+ value.is_a?(Integer)
346
+ when :float, :decimal
347
+ value.is_a?(Numeric)
348
+ when :boolean
349
+ [true, false].include?(value)
350
+ when :text, :varchar, :char
351
+ value.is_a?(String)
352
+ when :date, :time, :timestamp
353
+ value.is_a?(Time) || value.is_a?(Date) || value.is_a?(DateTime)
354
+ when :blob
355
+ value.is_a?(String)
356
+ when :json
357
+ value.is_a?(Hash) || value.is_a?(Array)
358
+ else
359
+ true
360
+ end
361
+ end
362
+
363
+ def repair_page_header(page_number)
364
+ begin
365
+ page = @engine.read_page(page_number)
366
+ return false unless page
367
+
368
+ # Rebuild header
369
+ page.header.page_number = page_number
370
+ page.header.page_size = page.size
371
+ page.header.header_size = 64
372
+ page.header.data_end = 64
373
+ page.header.flags = 0
374
+ page.header.checksum = calculate_checksum(page.data)
375
+ page.header.version = 1
376
+ page.header.page_type = 0
377
+
378
+ page.write_header
379
+ @engine.write_page(page)
380
+ true
381
+ rescue
382
+ false
383
+ end
384
+ end
385
+
386
+ def repair_checksum(page_number)
387
+ begin
388
+ page = @engine.read_page(page_number)
389
+ return false unless page
390
+
391
+ page.header.checksum = calculate_checksum(page.data)
392
+ page.write_header
393
+ @engine.write_page(page)
394
+ true
395
+ rescue
396
+ false
397
+ end
398
+ end
399
+
400
+ def repair_record(table, row_id, column)
401
+ begin
402
+ columns = @engine.table_columns(table)
403
+ row = @engine.select_row(table, row_id, columns)
404
+ return false unless row
405
+
406
+ col_def = columns.find { |c| c.name == column }
407
+ return false unless col_def && col_def.has_default?
408
+
409
+ row[column] = col_def.default
410
+
411
+ !!@engine.update_row(table, row_id, row)
412
+ rescue
413
+ false
414
+ end
415
+ end
416
+
417
+ def repair_index(index_name)
418
+ begin
419
+ if @engine.respond_to?(:index_manager)
420
+ !!@engine.index_manager.rebuild_index(index_name)
421
+ else
422
+ false
423
+ end
424
+ rescue
425
+ false
426
+ end
427
+ end
428
+
429
+ def generate_recommendations(results)
430
+ recommendations = []
431
+
432
+ if results[:pages].any?
433
+ recommendations << {
434
+ action: "repair_pages",
435
+ pages: results[:pages],
436
+ description: "Repair corrupted pages"
437
+ }
438
+ end
439
+
440
+ if results[:records].any?
441
+ recommendations << {
442
+ action: "repair_records",
443
+ records: results[:records],
444
+ description: "Repair corrupted records"
445
+ }
446
+ end
447
+
448
+ if results[:indexes].any?
449
+ recommendations << {
450
+ action: "rebuild_indexes",
451
+ indexes: results[:indexes],
452
+ description: "Rebuild corrupted indexes"
453
+ }
454
+ end
455
+
456
+ if results[:corrupted]
457
+ recommendations << {
458
+ action: "create_backup",
459
+ description: "Create backup before repairs"
460
+ }
461
+ end
462
+
463
+ recommendations
464
+ end
465
+ end
466
+ end
467
+ end