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,984 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+ require "fileutils"
6
+ require "time"
7
+
8
+ module RubyDB
9
+ module Storage
10
+ # VisibilityMap - Tracks which transactions can see which rows
11
+ # Implements MVCC visibility with proper isolation levels
12
+ class VisibilityMap
13
+ # Visibility states for a row version
14
+ VISIBLE = :visible
15
+ HIDDEN = :hidden
16
+ DELETED = :deleted
17
+ COMMITTED = :committed
18
+ ABORTED = :aborted
19
+ IN_PROGRESS = :in_progress
20
+
21
+ attr_reader :visibility_info, :stats, :isolation_level, :active_transactions, :committed_transactions
22
+
23
+ def initialize(page_manager, config = {})
24
+ @page_manager = page_manager
25
+ @visibility_info = {}
26
+ @transaction_visibility = {}
27
+ @snapshot_cache = {}
28
+ @snapshot_cache_lru = [] # LRU order for cache eviction
29
+ @max_snapshot_cache_size = config[:max_snapshot_cache] || 100
30
+ @commit_log = []
31
+ @abort_log = []
32
+ @max_snapshot_age = config[:max_snapshot_age] || 3600 # 1 hour default
33
+ @isolation_level = config[:isolation_level] || :read_committed
34
+ @configured_visibility_path = config[:visibility_path]
35
+ @vacuum_threshold = config[:vacuum_threshold] || 1000
36
+ @vacuum_batch_size = config[:vacuum_batch_size] || 100
37
+ @stats = {
38
+ visible_checks: 0,
39
+ hidden_checks: 0,
40
+ snapshot_creations: 0,
41
+ snapshot_cache_hits: 0,
42
+ snapshot_cache_misses: 0,
43
+ vacuum_runs: 0,
44
+ vacuum_removed: 0,
45
+ old_snapshots_cleared: 0,
46
+ snapshot_evictions: 0,
47
+ transaction_rollbacks: 0,
48
+ row_version_chains: 0
49
+ }
50
+ @lock = Mutex.new
51
+ @vacuum_lock = Mutex.new
52
+ @active_transactions = {}
53
+ @committed_transactions = Set.new
54
+ @aborted_transactions = Set.new
55
+ @row_version_chains = {}
56
+ @version_history = Hash.new { |hash, row_id| hash[row_id] = {} }
57
+ @next_version_id = 1
58
+ @is_loaded = false
59
+
60
+ # Create directory for visibility data
61
+ @data_dir = config[:data_dir] || "."
62
+ FileUtils.mkdir_p(@data_dir) unless Dir.exist?(@data_dir)
63
+
64
+ load_visibility
65
+ start_vacuum_thread if config[:auto_vacuum] != false
66
+ end
67
+
68
+ # Mark a row as visible to a specific transaction
69
+ def mark_visible(row_id, transaction_id, commit_id = nil)
70
+ @lock.synchronize do
71
+ @stats[:visible_checks] += 1
72
+
73
+ version_id = @next_version_id
74
+ @next_version_id += 1
75
+
76
+ existing = @visibility_info[row_id]
77
+ prev_version = existing ? existing[:version] : 0
78
+ prev_chain = existing ? @row_version_chains[row_id] : nil
79
+
80
+ @visibility_info[row_id] = {
81
+ state: VISIBLE,
82
+ visible_to: transaction_id,
83
+ commit_id: commit_id || (transaction_id.to_i == 0 ? 0 : nil),
84
+ created_at: Time.now,
85
+ last_modified: Time.now,
86
+ version: version_id,
87
+ prev_version: prev_version,
88
+ row_id: row_id,
89
+ deleted: false
90
+ }
91
+ remember_version(row_id, @visibility_info[row_id])
92
+
93
+ # Track version chain
94
+ @row_version_chains[row_id] ||= []
95
+ @row_version_chains[row_id] << version_id
96
+
97
+ if @row_version_chains[row_id].size > 100
98
+ # Trim old version chains to prevent unbounded growth
99
+ @row_version_chains[row_id] = @row_version_chains[row_id].last(50)
100
+ end
101
+
102
+ true
103
+ end
104
+ end
105
+
106
+ # Mark a row as hidden from a specific transaction (deleted or updated)
107
+ def mark_hidden(row_id, transaction_id, commit_id = nil)
108
+ @lock.synchronize do
109
+ @stats[:hidden_checks] += 1
110
+
111
+ version_id = @next_version_id
112
+ @next_version_id += 1
113
+
114
+ existing = @visibility_info[row_id]
115
+ prev_version = existing ? existing[:version] : 0
116
+
117
+ @visibility_info[row_id] = {
118
+ state: HIDDEN,
119
+ hidden_from: transaction_id,
120
+ commit_id: commit_id || (transaction_id.to_i == 0 ? 0 : nil),
121
+ created_at: Time.now,
122
+ last_modified: Time.now,
123
+ version: version_id,
124
+ prev_version: prev_version,
125
+ row_id: row_id,
126
+ deleted: false
127
+ }
128
+ remember_version(row_id, @visibility_info[row_id])
129
+
130
+ @row_version_chains[row_id] ||= []
131
+ @row_version_chains[row_id] << version_id
132
+
133
+ true
134
+ end
135
+ end
136
+
137
+ # Mark a row as deleted (final state)
138
+ def mark_deleted(row_id, transaction_id, commit_id = nil)
139
+ @lock.synchronize do
140
+ version_id = @next_version_id
141
+ @next_version_id += 1
142
+
143
+ existing = @visibility_info[row_id]
144
+ prev_version = existing ? existing[:version] : 0
145
+
146
+ @visibility_info[row_id] = {
147
+ state: DELETED,
148
+ deleted_by: transaction_id,
149
+ commit_id: commit_id || (transaction_id.to_i == 0 ? 0 : nil),
150
+ created_at: Time.now,
151
+ last_modified: Time.now,
152
+ version: version_id,
153
+ prev_version: prev_version,
154
+ row_id: row_id,
155
+ deleted: true
156
+ }
157
+ remember_version(row_id, @visibility_info[row_id])
158
+
159
+ @row_version_chains[row_id] ||= []
160
+ @row_version_chains[row_id] << version_id
161
+
162
+ true
163
+ end
164
+ end
165
+
166
+ # Check if a row is visible to a transaction
167
+ def is_visible?(row_id, transaction_id, snapshot_id = nil)
168
+ @lock.synchronize do
169
+ @stats[:visible_checks] += 1
170
+
171
+ info = @visibility_info[row_id]
172
+
173
+ # Rows created before visibility tracking was introduced remain
174
+ # visible; newly written rows always receive visibility metadata.
175
+ return true if info.nil?
176
+
177
+ # Check if row is deleted
178
+ return false if info[:state] == DELETED && info[:deleted_by] != transaction_id
179
+
180
+ # Check if there's a snapshot for this transaction
181
+ if snapshot_id && @snapshot_cache.key?(snapshot_id)
182
+ @stats[:snapshot_cache_hits] += 1
183
+ snapshot = @snapshot_cache[snapshot_id]
184
+ update_lru(snapshot_id)
185
+ return snapshot_visibility_check(info, snapshot, transaction_id)
186
+ end
187
+
188
+ # Check state-based visibility
189
+ case info[:state]
190
+ when VISIBLE, COMMITTED
191
+ # Check if visible_to is set
192
+ if info[:visible_to]
193
+ # Row is visible to this transaction if:
194
+ # 1. Transaction_id matches the visible_to
195
+ # 2. Or transaction_id is after commit_id (committed)
196
+ # 3. Or transaction is active and owns the visibility
197
+ if info[:visible_to] == transaction_id
198
+ return true
199
+ end
200
+
201
+ if info[:commit_id]
202
+ return true if info[:commit_id].to_i == 0
203
+ return true if @committed_transactions.include?(info[:commit_id])
204
+ end
205
+
206
+ return false
207
+ end
208
+ true
209
+
210
+ when HIDDEN
211
+ # Row is hidden from this transaction if:
212
+ # 1. hidden_from matches transaction_id (same transaction hid it)
213
+ # 2. Or commit_id > transaction_id (committed after transaction started)
214
+ # 3. Or transaction is the one that hid it
215
+ if info[:hidden_from]
216
+ return false if info[:hidden_from] == transaction_id
217
+
218
+ if info[:commit_id] && info[:commit_id] > transaction_id
219
+ return false
220
+ end
221
+ end
222
+ true
223
+
224
+ when DELETED
225
+ # Deleted rows are not visible to anyone except the transaction that deleted them
226
+ return info[:deleted_by] == transaction_id if info[:deleted_by]
227
+ false
228
+
229
+ else
230
+ true
231
+ end
232
+ end
233
+ end
234
+
235
+ # Check visibility using a snapshot
236
+ def snapshot_visibility_check(info, snapshot, transaction_id)
237
+ snapshot_start = snapshot[:start_time]
238
+ snapshot_active = snapshot[:active_transactions]
239
+ snapshot_committed = snapshot[:committed_transactions] || []
240
+
241
+ # If row was created after snapshot, it's not visible
242
+ if info[:created_at] > snapshot_start
243
+ return false
244
+ end
245
+
246
+ # If row was modified after snapshot, check if it's visible
247
+ if info[:last_modified] > snapshot_start
248
+ # Check if the modifying transaction is in the snapshot's active list
249
+ if info[:commit_id]
250
+ # If commit_id is in active transactions when snapshot was taken, it's not visible
251
+ return false if snapshot_active.include?(info[:commit_id])
252
+
253
+ # If commit_id was committed after snapshot, it's not visible
254
+ return false if snapshot_committed.include?(info[:commit_id])
255
+
256
+ # If commit_id is the current transaction, it might be visible
257
+ return true if info[:commit_id] == transaction_id
258
+ end
259
+
260
+ # Check if this is the current transaction's own changes
261
+ if info[:visible_to] == transaction_id || info[:hidden_from] == transaction_id
262
+ return true
263
+ end
264
+
265
+ return false
266
+ end
267
+
268
+ # Row was created before snapshot and not modified after
269
+ true
270
+ end
271
+
272
+ # Create a snapshot for a transaction
273
+ def create_snapshot(transaction_id)
274
+ @lock.synchronize do
275
+ @stats[:snapshot_creations] += 1
276
+
277
+ snapshot_id = "snapshot_#{transaction_id}_#{Time.now.to_i}_#{@stats[:snapshot_creations]}"
278
+
279
+ snapshot = {
280
+ id: snapshot_id,
281
+ transaction_id: transaction_id,
282
+ start_time: Time.now,
283
+ active_transactions: @active_transactions.keys.dup,
284
+ committed_transactions: @committed_transactions.to_a.dup,
285
+ isolation_level: @isolation_level,
286
+ created_at: Time.now,
287
+ last_access: Time.now
288
+ }
289
+
290
+ # Store snapshot with LRU management
291
+ if @snapshot_cache.size >= @max_snapshot_cache_size
292
+ evict_oldest_snapshot
293
+ end
294
+
295
+ @snapshot_cache[snapshot_id] = snapshot
296
+ @snapshot_cache_lru.unshift(snapshot_id)
297
+
298
+ # Clean old snapshots
299
+ clean_old_snapshots
300
+
301
+ snapshot_id
302
+ end
303
+ end
304
+
305
+ # Get a snapshot by ID
306
+ def get_snapshot(snapshot_id)
307
+ @lock.synchronize do
308
+ snapshot = @snapshot_cache[snapshot_id]
309
+ update_lru(snapshot_id) if snapshot
310
+ snapshot
311
+ end
312
+ end
313
+
314
+ # Register an active transaction
315
+ def register_transaction(transaction_id)
316
+ @lock.synchronize do
317
+ @active_transactions[transaction_id] = {
318
+ started_at: Time.now,
319
+ status: :active,
320
+ visibility: {},
321
+ row_locks: Set.new,
322
+ snapshot_id: nil
323
+ }
324
+
325
+ # Stronger isolation levels are rejected by Engine until historical
326
+ # row versions are persisted. Do not create a snapshot here while
327
+ # holding @lock; create_snapshot also takes this lock.
328
+
329
+ true
330
+ end
331
+ end
332
+
333
+ # Commit a transaction
334
+ def commit_transaction(transaction_id, commit_id = nil)
335
+ @lock.synchronize do
336
+ commit_id ||= transaction_id
337
+
338
+ # Get transaction's snapshot
339
+ tx_info = @active_transactions[transaction_id]
340
+ snapshot_id = tx_info ? tx_info[:snapshot_id] : nil
341
+
342
+ # Update all visibility info committed by this transaction
343
+ updated_rows = []
344
+
345
+ @visibility_info.each do |row_id, info|
346
+ if info[:visible_to] == transaction_id ||
347
+ info[:hidden_from] == transaction_id ||
348
+ info[:deleted_by] == transaction_id
349
+
350
+ # Mark as committed
351
+ info[:commit_id] = commit_id
352
+ info[:state] = COMMITTED if info[:state] == VISIBLE || info[:state] == HIDDEN
353
+ info[:last_modified] = Time.now
354
+ updated_rows << row_id
355
+
356
+ # Update version chain
357
+ if @row_version_chains[row_id]
358
+ @row_version_chains[row_id] << info[:version]
359
+ end
360
+ end
361
+ end
362
+
363
+ # Move to committed transactions
364
+ @committed_transactions.add(transaction_id)
365
+ @active_transactions.delete(transaction_id)
366
+
367
+ @commit_log << {
368
+ transaction_id: transaction_id,
369
+ commit_id: commit_id,
370
+ time: Time.now,
371
+ rows_updated: updated_rows.size,
372
+ snapshot_id: snapshot_id
373
+ }
374
+
375
+ # Clean commit log if too large
376
+ if @commit_log.size > 1000
377
+ @commit_log = @commit_log.last(500)
378
+ end
379
+
380
+ # Remove transaction's snapshot
381
+ if snapshot_id
382
+ @snapshot_cache.delete(snapshot_id)
383
+ @snapshot_cache_lru.delete(snapshot_id)
384
+ end
385
+
386
+ # Return commit info
387
+ {
388
+ transaction_id: transaction_id,
389
+ commit_id: commit_id,
390
+ rows_updated: updated_rows.size,
391
+ committed_at: Time.now
392
+ }
393
+ end
394
+ end
395
+
396
+ # Abort a transaction
397
+ def abort_transaction(transaction_id)
398
+ @lock.synchronize do
399
+ @stats[:transaction_rollbacks] += 1
400
+
401
+ # Get transaction's snapshot
402
+ tx_info = @active_transactions[transaction_id]
403
+ snapshot_id = tx_info ? tx_info[:snapshot_id] : nil
404
+
405
+ # Track which rows were affected
406
+ affected_rows = []
407
+
408
+ # Revert visibility changes made by this transaction
409
+ @visibility_info.each do |row_id, info|
410
+ if info[:visible_to] == transaction_id ||
411
+ info[:hidden_from] == transaction_id ||
412
+ info[:deleted_by] == transaction_id
413
+
414
+ affected_rows << row_id
415
+
416
+ # Revert to previous version if exists
417
+ prev_version = info[:prev_version]
418
+
419
+ if prev_version > 0
420
+ # Find previous version in version chain
421
+ if @row_version_chains[row_id]
422
+ prev_info = find_version_by_id(row_id, prev_version)
423
+ if prev_info
424
+ # Restore previous state
425
+ @visibility_info[row_id] = prev_info
426
+ else
427
+ # Remove completely if previous version not found
428
+ @visibility_info.delete(row_id)
429
+ end
430
+ end
431
+ else
432
+ # No previous version - remove completely
433
+ @visibility_info.delete(row_id)
434
+ end
435
+ end
436
+ end
437
+
438
+ # Move to aborted transactions
439
+ @aborted_transactions.add(transaction_id)
440
+ @active_transactions.delete(transaction_id)
441
+
442
+ @abort_log << {
443
+ transaction_id: transaction_id,
444
+ time: Time.now,
445
+ rows_affected: affected_rows.size,
446
+ snapshot_id: snapshot_id
447
+ }
448
+
449
+ # Clean abort log if too large
450
+ if @abort_log.size > 1000
451
+ @abort_log = @abort_log.last(500)
452
+ end
453
+
454
+ # Remove transaction's snapshot
455
+ if snapshot_id
456
+ @snapshot_cache.delete(snapshot_id)
457
+ @snapshot_cache_lru.delete(snapshot_id)
458
+ end
459
+
460
+ affected_rows.size
461
+ end
462
+ end
463
+
464
+ # Find a version by ID in the version chain
465
+ def find_version_by_id(row_id, version_id)
466
+ history = @version_history[row_id]
467
+ history[version_id.to_i] || begin
468
+ info = @visibility_info[row_id]
469
+ info if info && info[:version].to_i == version_id.to_i
470
+ end
471
+ end
472
+
473
+ # Check if a transaction is active
474
+ def transaction_active?(transaction_id)
475
+ @active_transactions.key?(transaction_id)
476
+ end
477
+
478
+ # Check if a transaction is committed
479
+ def transaction_committed?(transaction_id)
480
+ @committed_transactions.include?(transaction_id)
481
+ end
482
+
483
+ # Check if a transaction is aborted
484
+ def transaction_aborted?(transaction_id)
485
+ @aborted_transactions.include?(transaction_id)
486
+ end
487
+
488
+ # Get all rows visible to a transaction
489
+ def transaction_visible_rows(transaction_id)
490
+ @lock.synchronize do
491
+ result = []
492
+ @visibility_info.each do |row_id, info|
493
+ if is_visible?(row_id, transaction_id)
494
+ result << row_id
495
+ end
496
+ end
497
+ result
498
+ end
499
+ end
500
+
501
+ # Get all rows hidden from a transaction
502
+ def transaction_hidden_rows(transaction_id)
503
+ @lock.synchronize do
504
+ result = []
505
+ @visibility_info.each do |row_id, info|
506
+ unless is_visible?(row_id, transaction_id)
507
+ result << row_id
508
+ end
509
+ end
510
+ result
511
+ end
512
+ end
513
+
514
+ # Get row visibility history
515
+ def row_history(row_id)
516
+ @lock.synchronize do
517
+ info = @visibility_info[row_id]
518
+ return [] unless info
519
+
520
+ history = [info.dup]
521
+
522
+ # Follow version chain backwards
523
+ current = info
524
+ while current && current[:prev_version] > 0
525
+ prev_info = find_version_by_id(row_id, current[:prev_version])
526
+ if prev_info
527
+ history << prev_info.dup
528
+ current = prev_info
529
+ else
530
+ break
531
+ end
532
+ end
533
+
534
+ history.reverse
535
+ end
536
+ end
537
+
538
+ # Get row version history
539
+ def row_versions(row_id)
540
+ @lock.synchronize do
541
+ info = @visibility_info[row_id]
542
+ return [] unless info
543
+
544
+ versions = []
545
+ current = info
546
+
547
+ while current
548
+ versions << {
549
+ version: current[:version],
550
+ state: current[:state],
551
+ transaction_id: current[:visible_to] || current[:hidden_from] || current[:deleted_by],
552
+ created_at: current[:created_at],
553
+ commit_id: current[:commit_id]
554
+ }
555
+
556
+ break if current[:prev_version] == 0
557
+
558
+ current = find_version_by_id(row_id, current[:prev_version])
559
+ end
560
+
561
+ versions
562
+ end
563
+ end
564
+
565
+ # Vacuum old/deleted rows
566
+ def vacuum(max_age = @vacuum_threshold, batch_size = @vacuum_batch_size)
567
+ @vacuum_lock.synchronize do
568
+ @stats[:vacuum_runs] += 1
569
+
570
+ removed = 0
571
+ now = Time.now
572
+ rows_to_remove = []
573
+
574
+ # Collect rows to vacuum
575
+ @visibility_info.each do |row_id, info|
576
+ should_remove = false
577
+
578
+ case info[:state]
579
+ when DELETED
580
+ age = now - info[:last_modified]
581
+ should_remove = age > max_age && !needed_for_rollback?(row_id)
582
+
583
+ when COMMITTED
584
+ # Check if all transactions that need this version are gone
585
+ should_remove = committed_version_expired?(info)
586
+
587
+ when ABORTED
588
+ should_remove = true
589
+ end
590
+
591
+ if should_remove
592
+ rows_to_remove << row_id
593
+ removed += 1
594
+ end
595
+
596
+ break if removed >= batch_size
597
+ end
598
+
599
+ # Remove rows
600
+ rows_to_remove.each do |row_id|
601
+ @visibility_info.delete(row_id)
602
+ @row_version_chains.delete(row_id)
603
+ remove_row_from_disk(row_id)
604
+ end
605
+
606
+ @stats[:vacuum_removed] += removed
607
+
608
+ # Clean up old snapshots and transactions
609
+ clean_old_snapshots
610
+ clean_old_transactions
611
+
612
+ # Flush if we removed a significant number of rows
613
+ if removed > 0
614
+ flush
615
+ end
616
+
617
+ {
618
+ removed: removed,
619
+ total_rows: @visibility_info.size,
620
+ total_vacuumed: @stats[:vacuum_removed]
621
+ }
622
+ end
623
+ end
624
+
625
+ # Check if a deleted row is needed for potential rollback
626
+ def needed_for_rollback?(row_id)
627
+ # Check if any active transaction might need to rollback this row
628
+ @active_transactions.each do |tx_id, _info|
629
+ # If a transaction modified this row, it might need to rollback
630
+ if @visibility_info[row_id] &&
631
+ (@visibility_info[row_id][:visible_to] == tx_id ||
632
+ @visibility_info[row_id][:hidden_from] == tx_id ||
633
+ @visibility_info[row_id][:deleted_by] == tx_id)
634
+ return true
635
+ end
636
+ end
637
+ false
638
+ end
639
+
640
+ # Check if a committed version has expired
641
+ def committed_version_expired?(info)
642
+ return true if @active_transactions.empty?
643
+
644
+ # If no active transactions need this version
645
+ min_active = @active_transactions.keys.min || 0
646
+ info[:commit_id] && info[:commit_id] < min_active
647
+ end
648
+
649
+ # Load visibility map from disk
650
+ def load_visibility
651
+ @lock.synchronize do
652
+ @visibility_info.clear
653
+ @active_transactions.clear
654
+ @committed_transactions.clear
655
+ @aborted_transactions.clear
656
+ @snapshot_cache.clear
657
+ @snapshot_cache_lru.clear
658
+ @row_version_chains.clear
659
+ @version_history.clear
660
+
661
+ # Try to load from storage
662
+ begin
663
+ if File.exist?(visibility_path)
664
+ data = File.read(visibility_path)
665
+ parsed = JSON.parse(data, symbolize_names: true)
666
+
667
+ if parsed[:visibility_info]
668
+ parsed[:visibility_info].each do |row_id_str, info|
669
+ row_id = row_id_str.to_s.to_i
670
+ symbolized_info = {}
671
+ info.each do |key, value|
672
+ value = value.to_sym if %i[state].include?(key.to_sym) && value.respond_to?(:to_sym)
673
+ symbolized_info[key.to_sym] = value
674
+ end
675
+ @visibility_info[row_id] = symbolized_info
676
+ remember_version(row_id, symbolized_info)
677
+ end
678
+ end
679
+
680
+ if parsed[:version_history]
681
+ parsed[:version_history].each do |row_id_str, versions|
682
+ versions.each do |version_id, info|
683
+ normalized = normalize_loaded_info(info)
684
+ @version_history[row_id_str.to_s.to_i][version_id.to_s.to_i] = normalized
685
+ end
686
+ end
687
+ end
688
+
689
+ @active_transactions = parsed[:active_transactions] || {}
690
+ @committed_transactions = Set.new(parsed[:committed_transactions] || [])
691
+ @aborted_transactions = Set.new(parsed[:aborted_transactions] || [])
692
+ @next_version_id = parsed[:next_version_id] || 1
693
+ @row_version_chains = parsed[:row_version_chains] || {}
694
+
695
+ # Clean up any invalid data
696
+ @active_transactions.each do |tx_id, info|
697
+ if info[:started_at]
698
+ info[:started_at] = Time.parse(info[:started_at]) if info[:started_at].is_a?(String)
699
+ end
700
+ end
701
+
702
+ @is_loaded = true
703
+ end
704
+ rescue => e
705
+ # Never discard persisted visibility state silently; a corrupt or
706
+ # incompatible MVCC map must fail closed so recovery can intervene.
707
+ raise RubyDB::CorruptionError, "Failed to load visibility map: #{e.message}"
708
+ end
709
+ end
710
+ end
711
+
712
+ # Flush visibility map to disk
713
+ def flush
714
+ @lock.synchronize do
715
+ begin
716
+ data = {
717
+ visibility_info: @visibility_info,
718
+ active_transactions: @active_transactions,
719
+ committed_transactions: @committed_transactions.to_a,
720
+ aborted_transactions: @aborted_transactions.to_a,
721
+ next_version_id: @next_version_id,
722
+ row_version_chains: @row_version_chains,
723
+ version_history: @version_history,
724
+ timestamp: Time.now.iso8601,
725
+ version: 2
726
+ }
727
+
728
+ # Write to a temp file first, then rename
729
+ temp_path = "#{visibility_path}.tmp"
730
+ File.write(temp_path, JSON.generate(data))
731
+ FileUtils.mv(temp_path, visibility_path)
732
+
733
+ true
734
+ rescue => e
735
+ false
736
+ end
737
+ end
738
+ end
739
+
740
+ # Remove a row from disk storage
741
+ def remove_row_from_disk(row_id)
742
+ true
743
+ end
744
+
745
+ def remember_version(row_id, info)
746
+ @version_history[row_id][info[:version].to_i] = info.dup
747
+ end
748
+
749
+ def normalize_loaded_info(info)
750
+ info.each_with_object({}) do |(key, value), normalized|
751
+ normalized[key.to_sym] = if key.to_sym == :state && value.respond_to?(:to_sym)
752
+ value.to_sym
753
+ elsif %i[created_at last_modified].include?(key.to_sym) && value.is_a?(String)
754
+ Time.parse(value)
755
+ else
756
+ value
757
+ end
758
+ end
759
+ end
760
+
761
+ # Get visibility statistics
762
+ def stats
763
+ @lock.synchronize do
764
+ # Calculate average version chain length
765
+ avg_chain = if @row_version_chains.empty?
766
+ 0.0
767
+ else
768
+ @row_version_chains.values.map(&:size).sum.to_f / @row_version_chains.size
769
+ end
770
+
771
+ {
772
+ total_rows: @visibility_info.size,
773
+ visible_rows: @visibility_info.count { |_, info| info[:state] == VISIBLE || info[:state] == COMMITTED },
774
+ hidden_rows: @visibility_info.count { |_, info| info[:state] == HIDDEN },
775
+ deleted_rows: @visibility_info.count { |_, info| info[:state] == DELETED },
776
+ active_transactions: @active_transactions.size,
777
+ committed_transactions: @committed_transactions.size,
778
+ aborted_transactions: @aborted_transactions.size,
779
+ snapshots: @snapshot_cache.size,
780
+ version_chains: @row_version_chains.size,
781
+ avg_chain_length: avg_chain.round(2),
782
+ visible_checks: @stats[:visible_checks],
783
+ hidden_checks: @stats[:hidden_checks],
784
+ snapshot_creations: @stats[:snapshot_creations],
785
+ snapshot_cache_hits: @stats[:snapshot_cache_hits],
786
+ snapshot_cache_misses: @stats[:snapshot_cache_misses],
787
+ snapshot_cache_hit_rate: snapshot_hit_rate,
788
+ vacuum_runs: @stats[:vacuum_runs],
789
+ vacuum_removed: @stats[:vacuum_removed],
790
+ old_snapshots_cleared: @stats[:old_snapshots_cleared],
791
+ snapshot_evictions: @stats[:snapshot_evictions],
792
+ transaction_rollbacks: @stats[:transaction_rollbacks],
793
+ isolation_level: @isolation_level,
794
+ next_version_id: @next_version_id,
795
+ is_loaded: @is_loaded
796
+ }
797
+ end
798
+ end
799
+
800
+ # Clean up old snapshots
801
+ def clean_old_snapshots
802
+ @lock.synchronize do
803
+ now = Time.now
804
+ to_remove = []
805
+
806
+ @snapshot_cache.each do |id, snapshot|
807
+ age = now - snapshot[:created_at]
808
+ if age > @max_snapshot_age
809
+ to_remove << id
810
+ end
811
+ end
812
+
813
+ to_remove.each do |id|
814
+ @snapshot_cache.delete(id)
815
+ @snapshot_cache_lru.delete(id)
816
+ @stats[:old_snapshots_cleared] += 1
817
+ end
818
+ end
819
+ end
820
+
821
+ # Clean up old transaction records
822
+ def clean_old_transactions
823
+ @lock.synchronize do
824
+ now = Time.now
825
+ cutoff = now - 86400 # 24 hours
826
+
827
+ @active_transactions.each do |tx_id, info|
828
+ if info[:started_at] < cutoff
829
+ @active_transactions.delete(tx_id)
830
+ end
831
+ end
832
+
833
+ # Clean up old committed transactions
834
+ old_committed = @committed_transactions.to_a.select do |tx_id|
835
+ # Check if transaction is older than cutoff
836
+ # For simplicity, we'll just keep recent ones
837
+ false
838
+ end
839
+
840
+ # Keep committed transactions that might still be needed
841
+ @committed_transactions = Set.new(@committed_transactions.to_a.last(1000))
842
+ end
843
+ end
844
+
845
+ # Start vacuum thread for automatic cleanup
846
+ def start_vacuum_thread
847
+ Thread.new do
848
+ loop do
849
+ sleep(300) # Run every 5 minutes
850
+ begin
851
+ vacuum
852
+ rescue => e
853
+ # Log error but continue
854
+ puts "Vacuum error: #{e.message}" if $DEBUG
855
+ end
856
+ end
857
+ end
858
+ end
859
+
860
+ # Set isolation level
861
+ def isolation_level=(level)
862
+ @lock.synchronize do
863
+ @isolation_level = level
864
+ end
865
+ end
866
+
867
+ # Get row visibility info
868
+ def get_visibility_info(row_id)
869
+ @lock.synchronize do
870
+ @visibility_info[row_id]
871
+ end
872
+ end
873
+
874
+ # Check if a row exists
875
+ def row_exists?(row_id)
876
+ @lock.synchronize do
877
+ @visibility_info.key?(row_id)
878
+ end
879
+ end
880
+
881
+ # Get active transaction count
882
+ def active_transaction_count
883
+ @lock.synchronize do
884
+ @active_transactions.size
885
+ end
886
+ end
887
+
888
+ # Get committed transaction count
889
+ def committed_transaction_count
890
+ @lock.synchronize do
891
+ @committed_transactions.size
892
+ end
893
+ end
894
+
895
+ # Reset the visibility map (dangerous - for testing)
896
+ def reset!
897
+ @lock.synchronize do
898
+ @visibility_info.clear
899
+ @active_transactions.clear
900
+ @committed_transactions.clear
901
+ @aborted_transactions.clear
902
+ @snapshot_cache.clear
903
+ @snapshot_cache_lru.clear
904
+ @commit_log.clear
905
+ @abort_log.clear
906
+ @row_version_chains.clear
907
+ @next_version_id = 1
908
+ @stats = {
909
+ visible_checks: 0,
910
+ hidden_checks: 0,
911
+ snapshot_creations: 0,
912
+ snapshot_cache_hits: 0,
913
+ snapshot_cache_misses: 0,
914
+ vacuum_runs: 0,
915
+ vacuum_removed: 0,
916
+ old_snapshots_cleared: 0,
917
+ snapshot_evictions: 0,
918
+ transaction_rollbacks: 0,
919
+ row_version_chains: 0
920
+ }
921
+ File.delete(visibility_path) if File.exist?(visibility_path)
922
+ true
923
+ end
924
+ end
925
+
926
+ # to_s implementation
927
+ def to_s
928
+ "VisibilityMap: #{@visibility_info.size} entries, #{@active_transactions.size} active transactions, #{@snapshot_cache.size} snapshots"
929
+ end
930
+
931
+ private
932
+
933
+ # Update LRU order for a snapshot
934
+ def update_lru(snapshot_id)
935
+ @snapshot_cache_lru.delete(snapshot_id)
936
+ @snapshot_cache_lru.unshift(snapshot_id)
937
+ end
938
+
939
+ # Evict the oldest snapshot from cache
940
+ def evict_oldest_snapshot
941
+ if @snapshot_cache_lru.any?
942
+ oldest_id = @snapshot_cache_lru.last
943
+ @snapshot_cache.delete(oldest_id)
944
+ @snapshot_cache_lru.pop
945
+ @stats[:snapshot_evictions] += 1
946
+ true
947
+ else
948
+ false
949
+ end
950
+ end
951
+
952
+ # Calculate snapshot hit rate
953
+ def snapshot_hit_rate
954
+ total = @stats[:snapshot_cache_hits] + @stats[:snapshot_cache_misses]
955
+ return 0.0 if total == 0
956
+ (@stats[:snapshot_cache_hits].to_f / total * 100).round(2)
957
+ end
958
+
959
+ # Path for visibility data file
960
+ def visibility_path
961
+ @visibility_path ||= begin
962
+ if @configured_visibility_path
963
+ FileUtils.mkdir_p(File.dirname(@configured_visibility_path))
964
+ @configured_visibility_path
965
+ elsif @page_manager && @page_manager.respond_to?(:path)
966
+ base_path = @page_manager.path
967
+ if base_path
968
+ File.join(@data_dir, "#{File.basename(base_path)}.visibility")
969
+ else
970
+ File.join(@data_dir, "visibility_map.json")
971
+ end
972
+ else
973
+ File.join(@data_dir, "visibility_map.json")
974
+ end
975
+ end
976
+ end
977
+
978
+ # Check if a commit exists in the log
979
+ def commit_exists?(commit_id)
980
+ @commit_log.any? { |log| log[:commit_id] == commit_id }
981
+ end
982
+ end
983
+ end
984
+ end