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,250 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Process-level replication/failover drill. The parent starts independent
5
+ # primary and replica Ruby processes, interrupts the primary, validates
6
+ # reconnect/catch-up, fences a stale writer, and verifies a fresh primary can
7
+ # continue the durable log.
8
+
9
+ require "json"
10
+ require "open3"
11
+ require "rbconfig"
12
+ require "socket"
13
+ require "tmpdir"
14
+ require "timeout"
15
+
16
+ root = File.expand_path("..", __dir__)
17
+ $LOAD_PATH.unshift(File.join(root, "lib"))
18
+ require "rubydb"
19
+
20
+ TABLE = "failover_events"
21
+ COLUMNS = [RubyDB::Catalog::Column.new(:id, :integer, primary_key: true, null: false)].freeze
22
+
23
+ def emit(output, payload)
24
+ output.puts(JSON.generate(payload))
25
+ output.flush
26
+ end
27
+
28
+ def available_port
29
+ server = TCPServer.new("127.0.0.1", 0)
30
+ server.addr[1]
31
+ ensure
32
+ server&.close
33
+ end
34
+
35
+ def run_node(role, directory, port, fence_path)
36
+ output = STDOUT
37
+ quiet = File.open(File::NULL, "w")
38
+ $stdout = quiet
39
+
40
+ engine_path = File.join(directory, "#{role}.rdb")
41
+ engine = RubyDB::Storage::Engine.new(engine_path, auto_cleanup: false, auto_vacuum: false)
42
+
43
+ if role == "primary" && !engine.table_exists?(TABLE)
44
+ engine.create_table(TABLE, COLUMNS)
45
+ end
46
+
47
+ primary = if role == "primary"
48
+ RubyDB::Replication::Primary.new(
49
+ engine,
50
+ host: "127.0.0.1",
51
+ replication_port: port,
52
+ log_dir: File.join(directory, "replication-log"),
53
+ fence_path: fence_path,
54
+ heartbeat_interval: 0.05,
55
+ replication_timeout: 0.5,
56
+ node_id: "primary-#{Process.pid}"
57
+ )
58
+ end
59
+ replica = if role == "replica"
60
+ RubyDB::Replication::Replica.new(
61
+ engine,
62
+ primary_host: "127.0.0.1",
63
+ replication_port: port,
64
+ retry_interval: 0.02,
65
+ max_retry_attempts: 500,
66
+ heartbeat_interval: 0.05,
67
+ state_path: File.join(directory, "replica-state.json")
68
+ )
69
+ end
70
+
71
+ role == "primary" ? primary.start : replica.start
72
+ emit(output, event: "ready", role: role, pid: Process.pid)
73
+
74
+ while (line = STDIN.gets)
75
+ command = JSON.parse(line, symbolize_names: true)
76
+ case command[:command].to_s
77
+ when "status"
78
+ status = if primary
79
+ primary.get_replication_status.merge(row_count: engine.table_row_count(TABLE))
80
+ else
81
+ replica.replication_status.merge(
82
+ row_count: engine.table_exists?(TABLE) ? engine.table_row_count(TABLE) : 0
83
+ )
84
+ end
85
+ emit(output, event: "status", status: status)
86
+ when "insert"
87
+ begin
88
+ engine.begin_transaction
89
+ engine.insert_row(TABLE, COLUMNS, id: command.fetch(:id))
90
+ committed = engine.commit_transaction
91
+ emit(output, event: "insert", success: committed == true)
92
+ rescue StandardError => error
93
+ engine.rollback_transaction rescue nil
94
+ emit(output, event: "insert", success: false, error: "#{error.class}: #{error.message}")
95
+ end
96
+ when "stop"
97
+ primary&.stop
98
+ emit(output, event: "stopped", success: true)
99
+ when "start"
100
+ primary&.start
101
+ emit(output, event: "started", success: true)
102
+ when "exit"
103
+ emit(output, event: "exiting", success: true)
104
+ break
105
+ else
106
+ emit(output, event: "error", success: false, error: "unknown command")
107
+ end
108
+ end
109
+ ensure
110
+ replica&.stop
111
+ primary&.stop
112
+ engine&.close if engine&.open?
113
+ quiet&.close
114
+ end
115
+
116
+ def spawn_node(role, directory, port, fence_path)
117
+ stdin, stdout, stderr, wait_thread = Open3.popen3(
118
+ RbConfig.ruby, __FILE__, "--node", role, directory, port.to_s, fence_path
119
+ )
120
+ stderr_thread = Thread.new { stderr.each_line { |_line| } }
121
+ { stdin: stdin, stdout: stdout, stderr: stderr, wait: wait_thread, stderr_thread: stderr_thread }
122
+ end
123
+
124
+ def read_event(node, timeout: 10)
125
+ Timeout.timeout(timeout) do
126
+ loop do
127
+ ready = IO.select([node[:stdout]], nil, nil, 0.1)
128
+ next unless ready
129
+
130
+ line = node[:stdout].gets
131
+ raise "node exited before responding" unless line
132
+
133
+ return JSON.parse(line, symbolize_names: true)
134
+ end
135
+ end
136
+ end
137
+
138
+ def command(node, name, payload = {})
139
+ node[:stdin].puts(JSON.generate(payload.merge(command: name)))
140
+ node[:stdin].flush
141
+ read_event(node)
142
+ end
143
+
144
+ def wait_until(timeout: 30)
145
+ Timeout.timeout(timeout) do
146
+ loop do
147
+ return true if yield
148
+ sleep 0.05
149
+ end
150
+ end
151
+ end
152
+
153
+ def node_status(node)
154
+ command(node, "status").fetch(:status)
155
+ end
156
+
157
+ def close_node(node, force: false)
158
+ return unless node
159
+
160
+ if force && node[:wait].alive?
161
+ Process.kill("KILL", node[:wait].pid)
162
+ elsif node[:wait].alive?
163
+ command(node, "exit") rescue nil
164
+ node[:stdin].close rescue nil
165
+ end
166
+ node[:wait].join(5)
167
+ Process.kill("KILL", node[:wait].pid) if node[:wait].alive?
168
+ node[:stdout].close rescue nil
169
+ node[:stderr].close rescue nil
170
+ node[:stderr_thread]&.join(1)
171
+ end
172
+
173
+ if ARGV.first == "--node"
174
+ role, directory, port, fence_path = ARGV.drop(1)
175
+ abort "invalid node arguments" unless role && directory && port && fence_path
176
+ run_node(role, directory, Integer(port, 10), fence_path)
177
+ elsif ARGV.first == "--fence"
178
+ fence_path = ARGV.fetch(1)
179
+ lease = RubyDB::Replication::FencingLease.new(fence_path, "external-fencer").acquire!
180
+ puts JSON.generate(success: true, epoch: lease.epoch)
181
+ else
182
+ begin
183
+ primary = replica = replacement = final_primary = nil
184
+ Dir.mktmpdir("rubydb-process-failover-") do |directory|
185
+ begin
186
+ port = available_port
187
+ fence_path = File.join(directory, "cluster.fence")
188
+ primary = spawn_node("primary", directory, port, fence_path)
189
+ replica = spawn_node("replica", directory, port, fence_path)
190
+
191
+ raise "primary did not start" unless read_event(primary).fetch(:event) == "ready"
192
+ raise "replica did not start" unless read_event(replica).fetch(:event) == "ready"
193
+ wait_until { node_status(replica)[:state].to_s == "streaming" }
194
+ wait_until { node_status(primary)[:active_replicas].to_i >= 1 }
195
+
196
+ raise "initial insert failed" unless command(primary, "insert", id: 1).fetch(:success)
197
+ wait_until { node_status(replica)[:row_count].to_i == 1 }
198
+
199
+ close_node(primary, force: true)
200
+ primary = nil
201
+ wait_until do
202
+ %w[disconnected failed].include?(node_status(replica)[:state].to_s)
203
+ end
204
+
205
+ replacement = spawn_node("primary", directory, port, fence_path)
206
+ raise "replacement primary did not start" unless read_event(replacement).fetch(:event) == "ready"
207
+ wait_until { node_status(replica)[:state].to_s == "streaming" }
208
+ wait_until { node_status(replacement)[:active_replicas].to_i >= 1 }
209
+ wait_until { node_status(replica)[:row_count].to_i == 1 }
210
+
211
+ fence_result, fence_error, fence_status = Open3.capture3(
212
+ RbConfig.ruby, __FILE__, "--fence", fence_path
213
+ )
214
+ raise "external fencing failed: #{fence_error}" unless fence_status.success?
215
+ raise "fence response invalid" unless JSON.parse(fence_result).fetch("success")
216
+ raise "stale primary accepted a write" if command(replacement, "insert", id: 2).fetch(:success)
217
+
218
+ close_node(replacement, force: true)
219
+ replacement = nil
220
+ final_primary = spawn_node("primary", directory, port, fence_path)
221
+ raise "final primary did not start" unless read_event(final_primary).fetch(:event) == "ready"
222
+ wait_until { node_status(replica)[:state].to_s == "streaming" }
223
+ raise "final insert failed" unless command(final_primary, "insert", id: 2).fetch(:success)
224
+ wait_until { node_status(replica)[:row_count].to_i == 2 }
225
+
226
+ puts JSON.generate(
227
+ success: true,
228
+ primary_processes: 3,
229
+ replica_processes: 1,
230
+ stale_write_rejected: true,
231
+ rows_replayed_after_failover: node_status(replica)[:row_count]
232
+ )
233
+ ensure
234
+ close_node(final_primary) if final_primary
235
+ final_primary = nil
236
+ close_node(replacement) if replacement
237
+ replacement = nil
238
+ close_node(replica) if replica
239
+ replica = nil
240
+ close_node(primary) if primary
241
+ primary = nil
242
+ end
243
+ end
244
+ ensure
245
+ close_node(final_primary) if defined?(final_primary) && final_primary
246
+ close_node(replacement) if defined?(replacement) && replacement
247
+ close_node(replica) if defined?(replica) && replica
248
+ close_node(primary) if defined?(primary) && primary
249
+ end
250
+ end
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Exercises the real replication sockets through a fault-injecting TCP proxy.
5
+ # The proxy is deliberately small and local: it drops the live stream to model
6
+ # a network partition while both database processes remain alive. This makes
7
+ # the scenario reproducible on developer machines and in CI without changing
8
+ # production replication code or requiring host firewall privileges.
9
+
10
+ require "json"
11
+ require "socket"
12
+ require "stringio"
13
+ require "tmpdir"
14
+ require "timeout"
15
+
16
+ root = File.expand_path("..", __dir__)
17
+ $LOAD_PATH.unshift(File.join(root, "lib"))
18
+ require "rubydb"
19
+
20
+ TABLE = "network_failover_events"
21
+ COLUMNS = [RubyDB::Catalog::Column.new(:id, :integer, primary_key: true, null: false)].freeze
22
+
23
+ class TcpPartitionProxy
24
+ attr_reader :port
25
+
26
+ def initialize(target_host, target_port)
27
+ @target_host = target_host
28
+ @target_port = target_port
29
+ @server = TCPServer.new("127.0.0.1", 0)
30
+ @port = @server.addr[1]
31
+ @lock = Mutex.new
32
+ @pairs = []
33
+ @partitioned = false
34
+ @running = false
35
+ end
36
+
37
+ def start
38
+ @lock.synchronize { @running = true }
39
+ @thread = Thread.new { accept_loop }
40
+ self
41
+ end
42
+
43
+ def partition!
44
+ pairs = @lock.synchronize do
45
+ @partitioned = true
46
+ current = @pairs.dup
47
+ @pairs.clear
48
+ current
49
+ end
50
+ pairs.each { |client, target| close_pair(client, target) }
51
+ true
52
+ end
53
+
54
+ def heal!
55
+ @lock.synchronize { @partitioned = false }
56
+ true
57
+ end
58
+
59
+ def stop
60
+ pairs = @lock.synchronize do
61
+ @running = false
62
+ current = @pairs.dup
63
+ @pairs.clear
64
+ current
65
+ end
66
+ @server.close rescue nil
67
+ pairs.each { |client, target| close_pair(client, target) }
68
+ @thread&.join(2)
69
+ @thread&.kill if @thread&.alive?
70
+ true
71
+ end
72
+
73
+ private
74
+
75
+ def accept_loop
76
+ while @lock.synchronize { @running }
77
+ client = @server.accept
78
+ partitioned = @lock.synchronize { @partitioned }
79
+ if partitioned
80
+ client.close
81
+ next
82
+ end
83
+
84
+ target = TCPSocket.new(@target_host, @target_port)
85
+ pair = [client, target]
86
+ @lock.synchronize { @pairs << pair }
87
+ Thread.new { forward(pair, client, target) }
88
+ Thread.new { forward(pair, target, client) }
89
+ end
90
+ rescue IOError, Errno::EBADF
91
+ # Expected when stop closes the listener.
92
+ rescue StandardError
93
+ retry if @lock.synchronize { @running }
94
+ end
95
+
96
+ def forward(pair, source, destination)
97
+ loop do
98
+ ready = IO.select([source], nil, nil, 0.2)
99
+ next unless ready
100
+
101
+ destination.write(source.readpartial(16_384))
102
+ destination.flush
103
+ end
104
+ rescue EOFError, IOError, SystemCallError
105
+ # A dropped stream is the modeled partition.
106
+ ensure
107
+ @lock.synchronize { @pairs.delete(pair) }
108
+ close_pair(*pair)
109
+ end
110
+
111
+ def close_pair(first, second)
112
+ first.close rescue nil
113
+ second.close rescue nil
114
+ end
115
+ end
116
+
117
+ def available_port
118
+ probe = TCPServer.new("127.0.0.1", 0)
119
+ probe.addr[1]
120
+ ensure
121
+ probe&.close
122
+ end
123
+
124
+ def wait_until(timeout: 10)
125
+ Timeout.timeout(timeout) do
126
+ loop do
127
+ return true if yield
128
+ sleep 0.02
129
+ end
130
+ end
131
+ end
132
+
133
+ def insert(engine, id)
134
+ engine.begin_transaction
135
+ engine.insert_row(TABLE, COLUMNS, id: id)
136
+ engine.commit_transaction
137
+ rescue StandardError
138
+ engine.rollback_transaction rescue nil
139
+ raise
140
+ end
141
+
142
+ def rows(engine)
143
+ return [] unless engine.table_exists?(TABLE)
144
+
145
+ engine.select_rows(TABLE, COLUMNS).map { |row| (row[:id] || row["id"]).to_i }.sort
146
+ end
147
+
148
+ result = nil
149
+ original_stdout = $stdout
150
+ $stdout = StringIO.new
151
+ begin
152
+ Dir.mktmpdir("rubydb-network-failover-") do |directory|
153
+ primary_engine = RubyDB::Storage::Engine.new(File.join(directory, "primary.rdb"), auto_cleanup: false, auto_vacuum: false)
154
+ replica_engine = RubyDB::Storage::Engine.new(File.join(directory, "replica.rdb"), auto_cleanup: false, auto_vacuum: false)
155
+ primary = replica = promoted = proxy = nil
156
+ fence_path = File.join(directory, "cluster.fence")
157
+
158
+ primary_engine.create_table(TABLE, COLUMNS)
159
+ primary_port = available_port
160
+ proxy = TcpPartitionProxy.new("127.0.0.1", primary_port).start
161
+
162
+ begin
163
+ primary = RubyDB::Replication::Primary.new(
164
+ primary_engine,
165
+ host: "127.0.0.1",
166
+ replication_port: primary_port,
167
+ log_dir: File.join(directory, "primary-log"),
168
+ fence_path: fence_path,
169
+ heartbeat_interval: 0.05,
170
+ replication_timeout: 0.5,
171
+ node_id: "network-primary"
172
+ )
173
+ replica = RubyDB::Replication::Replica.new(
174
+ replica_engine,
175
+ primary_host: "127.0.0.1",
176
+ replication_port: proxy.port,
177
+ retry_interval: 0.02,
178
+ max_retry_attempts: 500,
179
+ heartbeat_interval: 0.05,
180
+ state_path: File.join(directory, "replica-state.json")
181
+ )
182
+ primary.start
183
+ replica.start
184
+ end
185
+
186
+ wait_until { replica.replication_status[:state].to_s == "streaming" }
187
+ insert(primary_engine, 1)
188
+ wait_until { rows(replica_engine) == [1] }
189
+
190
+ proxy.partition!
191
+ wait_until { %w[disconnected failed].include?(replica.replication_status[:state].to_s) }
192
+ insert(primary_engine, 2)
193
+ raise "partition did not isolate replica" unless rows(replica_engine) == [1]
194
+
195
+ proxy.heal!
196
+ wait_until { replica.replication_status[:state].to_s == "streaming" }
197
+ wait_until { rows(replica_engine) == [1, 2] }
198
+
199
+ # Partition a synchronized stream, fence the old writer, and promote only
200
+ # the replica that is known to have replayed through the recovery point.
201
+ proxy.partition!
202
+ wait_until { %w[disconnected failed].include?(replica.replication_status[:state].to_s) }
203
+ # Put the replica into the stable disconnected state before promotion. A
204
+ # continuously retrying, failed connection is not itself proof of a safe
205
+ # promotion even when its replay positions match.
206
+ replica.stop
207
+ RubyDB::Replication::FencingLease.new(fence_path, "external-fencer").acquire!
208
+ old_primary_rejected = false
209
+ begin
210
+ insert(primary_engine, 3)
211
+ rescue RubyDB::ReplicationError
212
+ old_primary_rejected = true
213
+ end
214
+ raise "stale primary accepted a write" unless old_primary_rejected
215
+
216
+ replica.promote_to_primary(recovery_point: 2)
217
+ promoted_port = available_port
218
+ begin
219
+ promoted = RubyDB::Replication::Primary.new(
220
+ replica_engine,
221
+ host: "127.0.0.1",
222
+ replication_port: promoted_port,
223
+ log_dir: File.join(directory, "promoted-log"),
224
+ fence_path: fence_path,
225
+ heartbeat_interval: 0.05,
226
+ replication_timeout: 0.5,
227
+ node_id: "promoted-primary"
228
+ )
229
+ promoted.start
230
+ end
231
+ insert(replica_engine, 3)
232
+ raise "promoted primary did not accept a write" unless rows(replica_engine) == [1, 2, 3]
233
+
234
+ result = {
235
+ success: true,
236
+ network_partition: true,
237
+ partitioned_primary_stayed_running: true,
238
+ replica_caught_up_after_heal: true,
239
+ stale_primary_rejected: old_primary_rejected,
240
+ promoted_rows: rows(replica_engine),
241
+ fencing_epoch_after_promotion: promoted.fencing_status[:epoch]
242
+ }
243
+ ensure
244
+ promoted&.stop rescue nil
245
+ primary&.stop rescue nil
246
+ replica&.stop rescue nil
247
+ proxy&.stop rescue nil
248
+ replica_engine&.close if replica_engine&.open?
249
+ primary_engine&.close if primary_engine&.open?
250
+ end
251
+ ensure
252
+ $stdout = original_stdout
253
+ end
254
+
255
+ puts JSON.generate(result) if result
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # End-to-end backup/restore drill. This intentionally uses a fresh destination
5
+ # and opens the restored files with the real storage engine.
6
+
7
+ require "tmpdir"
8
+
9
+ root = File.expand_path("..", __dir__)
10
+ $LOAD_PATH.unshift(File.join(root, "lib"))
11
+ require "rubydb"
12
+
13
+ Dir.mktmpdir("rubydb-restore-drill-") do |directory|
14
+ source = File.join(directory, "source", "rubydb.rdb")
15
+ backup_dir = File.join(directory, "backups")
16
+ verification_dir = File.join(directory, "verification")
17
+ restore_dir = File.join(directory, "restored")
18
+ columns = [RubyDB::Catalog::Column.new("id", :integer, primary_key: true, null: false)]
19
+
20
+ engine = RubyDB::Storage::Engine.new(source, auto_cleanup: false, auto_vacuum: false)
21
+ engine.create_table("restore_drill", columns)
22
+ engine.insert_row("restore_drill", columns, [1])
23
+ engine.insert_row("restore_drill", columns, [2])
24
+
25
+ backup = RubyDB::Backup::Backup.new(engine, backup_dir: backup_dir, include_schema: true)
26
+ created = backup.create_backup
27
+ abort "backup failed: #{created[:error]}" unless created[:success]
28
+
29
+ verification = RubyDB::Backup::Verification.new(verification_dir: verification_dir)
30
+ result = verification.verify_restore(created[:backup_path])
31
+ abort "restore verification failed: #{result[:error]}" unless result[:success]
32
+ abort "restore row-count mismatch" unless result[:row_counts]["restore_drill"] == 2
33
+
34
+ restored = RubyDB::Backup::Restore.new(nil, backup_dir: backup_dir).restore(
35
+ created[:backup_path], destination: restore_dir
36
+ )
37
+ abort "restore failed: #{restored[:error]}" unless restored[:success]
38
+ restored_engine = RubyDB::Storage::Engine.new(File.join(restore_dir, "rubydb.rdb"), auto_cleanup: false, auto_vacuum: false)
39
+ abort "restored data mismatch" unless restored_engine.table_row_count("restore_drill") == 2
40
+
41
+ puts "Restore drill passed: #{created[:backup_name]} (2 rows restored and reopened)"
42
+ ensure
43
+ restored_engine&.close
44
+ engine&.close
45
+ end
data/scripts/security ADDED
File without changes
data/scripts/setup ADDED
File without changes
data/scripts/test ADDED
File without changes
File without changes
File without changes
File without changes
data/scripts/test-unit ADDED
File without changes
@@ -0,0 +1 @@
1
+ {"visibility_info":{},"active_transactions":{},"committed_transactions":[],"aborted_transactions":[],"next_version_id":1,"row_version_chains":{},"timestamp":"2026-09-01T09:49:06+00:00","version":2}