rubydb 0.1.4 → 0.1.5

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 (420) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
  3. data/.github/workflows/benchmark.yml +26 -26
  4. data/.github/workflows/compatibility.yml +63 -63
  5. data/.github/workflows/fuzz.yml +33 -33
  6. data/.github/workflows/lint.yml +21 -21
  7. data/.github/workflows/operations.yml +24 -24
  8. data/.github/workflows/production-validation.yml +111 -111
  9. data/.github/workflows/release.yml +77 -77
  10. data/.github/workflows/security.yml +39 -37
  11. data/.github/workflows/test.yml +26 -26
  12. data/.github/workflows/workload.yml +58 -58
  13. data/.gitignore +12 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +43 -27
  18. data/CODE_OF_CONDUCT.md +13 -13
  19. data/CONTRIBUTING.md +29 -29
  20. data/GOVERNANCE.md +16 -16
  21. data/Gemfile +18 -17
  22. data/Gemfile.lock +125 -71
  23. data/README.md +249 -162
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +71 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/adapters/activerecord/Gemfile +11 -11
  29. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +873 -879
  30. data/adapters/activerecord/rubydb-activerecord.gemspec +20 -20
  31. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
  32. data/adapters/ruby/README.md +18 -18
  33. data/adapters/sequel/README.md +11 -11
  34. data/config/monitoring/prometheus-alerts.yml +39 -39
  35. data/config/production.yml +36 -36
  36. data/docs/README.md +75 -71
  37. data/docs/architecture/concurrency.md +14 -14
  38. data/docs/architecture/current-state.md +125 -125
  39. data/docs/architecture/execution-engine.md +25 -25
  40. data/docs/architecture/indexes.md +19 -19
  41. data/docs/architecture/mvcc.md +19 -19
  42. data/docs/architecture/overview.md +13 -13
  43. data/docs/architecture/pages.md +11 -11
  44. data/docs/architecture/production-roadmap.md +82 -82
  45. data/docs/architecture/query-planner.md +20 -20
  46. data/docs/architecture/recovery.md +18 -18
  47. data/docs/architecture/sql-engine.md +12 -12
  48. data/docs/architecture/storage-engine.md +14 -14
  49. data/docs/architecture/transactions.md +10 -10
  50. data/docs/architecture/wal.md +28 -28
  51. data/docs/cli-cheatsheet.md +98 -98
  52. data/docs/cli.md +275 -275
  53. data/docs/contributing/architecture.md +9 -9
  54. data/docs/contributing/benchmarking.md +14 -14
  55. data/docs/contributing/development.md +16 -16
  56. data/docs/contributing/release-process.md +49 -49
  57. data/docs/contributing/testing.md +16 -16
  58. data/docs/debugging.md +229 -229
  59. data/docs/developer/branching.md +10 -10
  60. data/docs/developer/database-diff.md +10 -10
  61. data/docs/developer/local-development.md +17 -17
  62. data/docs/developer/snapshots.md +9 -9
  63. data/docs/developer/temporal-data.md +10 -10
  64. data/docs/developer-guide.md +297 -297
  65. data/docs/getting-started/first-database.md +16 -16
  66. data/docs/getting-started/first-query.md +13 -13
  67. data/docs/getting-started/installation.md +19 -19
  68. data/docs/getting-started/local-to-production.md +300 -300
  69. data/docs/getting-started/quickstart.md +17 -17
  70. data/docs/getting-started/rails.md +16 -16
  71. data/docs/hardening_backlog.md +93 -93
  72. data/docs/lessons-learned.md +112 -112
  73. data/docs/operations/backups.md +33 -33
  74. data/docs/operations/disaster-recovery.md +31 -31
  75. data/docs/operations/failover.md +30 -30
  76. data/docs/operations/monitoring.md +25 -25
  77. data/docs/operations/production-guide.md +295 -295
  78. data/docs/operations/production-runbook.md +45 -45
  79. data/docs/operations/replication.md +33 -33
  80. data/docs/operations/restore.md +6 -6
  81. data/docs/operations/runbook.md +34 -34
  82. data/docs/operations/upgrades.md +14 -14
  83. data/docs/operations/workload-testing.md +17 -17
  84. data/docs/production-readiness.md +118 -118
  85. data/docs/production_validation.md +150 -150
  86. data/docs/rails/active-record.md +11 -11
  87. data/docs/rails/compatibility-guide.md +90 -90
  88. data/docs/rails/database-yml.md +92 -92
  89. data/docs/rails/installation.md +17 -17
  90. data/docs/rails/migrations.md +17 -17
  91. data/docs/rails/production.md +82 -82
  92. data/docs/rails/troubleshooting.md +18 -18
  93. data/docs/release.md +25 -25
  94. data/docs/server/architecture.md +10 -10
  95. data/docs/server/authentication.md +10 -10
  96. data/docs/server/configuration.md +16 -16
  97. data/docs/server/connection-pooling.md +10 -10
  98. data/docs/server/deployment.md +10 -10
  99. data/docs/server/protocol.md +12 -12
  100. data/docs/sql/compatibility-guide.md +82 -82
  101. data/docs/sql/compatibility.md +39 -39
  102. data/docs/sql/data-types.md +10 -10
  103. data/docs/sql/functions.md +9 -9
  104. data/docs/sql/joins.md +9 -9
  105. data/docs/sql/operators.md +9 -9
  106. data/docs/sql/sqlite-compatibility.md +21 -21
  107. data/docs/sql/syntax.md +10 -10
  108. data/docs/sql/transactions.md +10 -10
  109. data/docs/troubleshooting.md +244 -244
  110. data/lessons/01-foundations.md +73 -0
  111. data/lessons/02-local-development.md +121 -0
  112. data/lessons/03-embedded-rubydb.md +99 -0
  113. data/lessons/04-rails-complex-apps.md +138 -0
  114. data/lessons/05-rubydb-production-server.md +237 -0
  115. data/lessons/06-postgresql-massive-apps.md +96 -0
  116. data/lessons/07-hybrid-microservices.md +94 -0
  117. data/lessons/08-migrations-backups-recovery.md +86 -0
  118. data/lessons/09-observability-security-scale.md +87 -0
  119. data/lessons/10-release-readiness.md +125 -0
  120. data/lib/rubydb/backup/archive.rb +332 -334
  121. data/lib/rubydb/backup/backup.rb +400 -401
  122. data/lib/rubydb/backup/incremental.rb +349 -353
  123. data/lib/rubydb/backup/restore.rb +289 -290
  124. data/lib/rubydb/backup/snapshot.rb +265 -267
  125. data/lib/rubydb/backup/verification.rb +276 -279
  126. data/lib/rubydb/branching/branch.rb +181 -181
  127. data/lib/rubydb/branching/branch_manager.rb +307 -311
  128. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  129. data/lib/rubydb/branching/checkout.rb +165 -166
  130. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  131. data/lib/rubydb/branching/diff.rb +137 -138
  132. data/lib/rubydb/branching/merge.rb +282 -285
  133. data/lib/rubydb/build_info.rb +15 -15
  134. data/lib/rubydb/catalog/catalog.rb +391 -391
  135. data/lib/rubydb/catalog/column.rb +112 -112
  136. data/lib/rubydb/catalog/constraint.rb +180 -180
  137. data/lib/rubydb/catalog/database.rb +184 -184
  138. data/lib/rubydb/catalog/index.rb +97 -97
  139. data/lib/rubydb/catalog/schema.rb +103 -103
  140. data/lib/rubydb/catalog/sequence.rb +90 -90
  141. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  142. data/lib/rubydb/catalog/table.rb +178 -178
  143. data/lib/rubydb/catalog/trigger.rb +102 -102
  144. data/lib/rubydb/catalog/view.rb +66 -66
  145. data/lib/rubydb/cli/application.rb +163 -163
  146. data/lib/rubydb/cli/commands/backup.rb +80 -81
  147. data/lib/rubydb/cli/commands/branch.rb +72 -72
  148. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  149. data/lib/rubydb/cli/commands/create.rb +58 -58
  150. data/lib/rubydb/cli/commands/diff.rb +76 -77
  151. data/lib/rubydb/cli/commands/doctor.rb +74 -74
  152. data/lib/rubydb/cli/commands/drop.rb +57 -57
  153. data/lib/rubydb/cli/commands/init.rb +101 -102
  154. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  155. data/lib/rubydb/cli/commands/merge.rb +63 -63
  156. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  157. data/lib/rubydb/cli/commands/restart.rb +42 -39
  158. data/lib/rubydb/cli/commands/restore.rb +121 -121
  159. data/lib/rubydb/cli/commands/shell.rb +365 -365
  160. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  161. data/lib/rubydb/cli/commands/start.rb +88 -82
  162. data/lib/rubydb/cli/commands/status.rb +96 -92
  163. data/lib/rubydb/cli/commands/stop.rb +47 -47
  164. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  165. data/lib/rubydb/cli/formatter.rb +221 -221
  166. data/lib/rubydb/cli/output.rb +168 -168
  167. data/lib/rubydb/client/client.rb +302 -304
  168. data/lib/rubydb/client/connection.rb +414 -415
  169. data/lib/rubydb/client/connection_pool.rb +168 -168
  170. data/lib/rubydb/client/connection_url.rb +96 -96
  171. data/lib/rubydb/client/prepared_statement.rb +60 -60
  172. data/lib/rubydb/client/result.rb +123 -123
  173. data/lib/rubydb/client/statement.rb +52 -52
  174. data/lib/rubydb/client/transaction.rb +130 -130
  175. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  176. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  177. data/lib/rubydb/concurrency/latch.rb +101 -101
  178. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  179. data/lib/rubydb/concurrency/mutex.rb +181 -183
  180. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  181. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  182. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  183. data/lib/rubydb/configuration/config.rb +170 -170
  184. data/lib/rubydb/configuration/defaults.rb +179 -179
  185. data/lib/rubydb/configuration/environment.rb +152 -152
  186. data/lib/rubydb/configuration/parser.rb +185 -185
  187. data/lib/rubydb/configuration/validation.rb +221 -221
  188. data/lib/rubydb/constants.rb +74 -74
  189. data/lib/rubydb/constraints/check.rb +181 -181
  190. data/lib/rubydb/constraints/constraint.rb +101 -101
  191. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  192. data/lib/rubydb/constraints/not_null.rb +64 -64
  193. data/lib/rubydb/constraints/primary_key.rb +99 -99
  194. data/lib/rubydb/constraints/unique.rb +106 -108
  195. data/lib/rubydb/constraints/validator.rb +349 -350
  196. data/lib/rubydb/errors/authentication_error.rb +10 -10
  197. data/lib/rubydb/errors/authorization_error.rb +23 -23
  198. data/lib/rubydb/errors/client_error.rb +10 -10
  199. data/lib/rubydb/errors/configuration_error.rb +10 -10
  200. data/lib/rubydb/errors/connection_error.rb +10 -10
  201. data/lib/rubydb/errors/constraint_error.rb +23 -23
  202. data/lib/rubydb/errors/corruption_error.rb +10 -10
  203. data/lib/rubydb/errors/database_error.rb +10 -10
  204. data/lib/rubydb/errors/error.rb +20 -20
  205. data/lib/rubydb/errors/execution_error.rb +10 -10
  206. data/lib/rubydb/errors/parser_error.rb +10 -10
  207. data/lib/rubydb/errors/recovery_error.rb +10 -10
  208. data/lib/rubydb/errors/replication_error.rb +10 -10
  209. data/lib/rubydb/errors/server_error.rb +6 -6
  210. data/lib/rubydb/errors/storage_error.rb +10 -10
  211. data/lib/rubydb/errors/transaction_error.rb +10 -10
  212. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  213. data/lib/rubydb/execution/delete_executor.rb +110 -112
  214. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  215. data/lib/rubydb/execution/executor.rb +1182 -1188
  216. data/lib/rubydb/execution/expression.rb +191 -193
  217. data/lib/rubydb/execution/index_scan.rb +142 -142
  218. data/lib/rubydb/execution/insert_executor.rb +215 -217
  219. data/lib/rubydb/execution/join_executor.rb +243 -249
  220. data/lib/rubydb/execution/limit_executor.rb +83 -85
  221. data/lib/rubydb/execution/optimizer.rb +227 -215
  222. data/lib/rubydb/execution/plan.rb +355 -353
  223. data/lib/rubydb/execution/planner.rb +542 -536
  224. data/lib/rubydb/execution/predicate.rb +235 -235
  225. data/lib/rubydb/execution/scan.rb +49 -49
  226. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  227. data/lib/rubydb/execution/sort_executor.rb +180 -185
  228. data/lib/rubydb/execution/update_executor.rb +160 -162
  229. data/lib/rubydb/functions/aggregate.rb +70 -70
  230. data/lib/rubydb/functions/date_functions.rb +274 -278
  231. data/lib/rubydb/functions/function.rb +85 -85
  232. data/lib/rubydb/functions/json_functions.rb +231 -215
  233. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  234. data/lib/rubydb/functions/scalar.rb +52 -52
  235. data/lib/rubydb/functions/string_functions.rb +383 -383
  236. data/lib/rubydb/functions/system_functions.rb +258 -246
  237. data/lib/rubydb/history/as_of.rb +238 -238
  238. data/lib/rubydb/history/change.rb +105 -105
  239. data/lib/rubydb/history/history.rb +131 -131
  240. data/lib/rubydb/history/history_manager.rb +228 -229
  241. data/lib/rubydb/history/temporal_query.rb +202 -202
  242. data/lib/rubydb/history/timeline.rb +144 -144
  243. data/lib/rubydb/indexes/btree.rb +186 -186
  244. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  245. data/lib/rubydb/indexes/btree_node.rb +384 -385
  246. data/lib/rubydb/indexes/hash_index.rb +150 -150
  247. data/lib/rubydb/indexes/index.rb +71 -71
  248. data/lib/rubydb/indexes/index_manager.rb +408 -406
  249. data/lib/rubydb/indexes/index_scan.rb +466 -470
  250. data/lib/rubydb/migrations/migration.rb +253 -254
  251. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  252. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  253. data/lib/rubydb/migrations/migration_version.rb +71 -71
  254. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  255. data/lib/rubydb/migrations/schema_version.rb +64 -64
  256. data/lib/rubydb/monitoring/events.rb +155 -160
  257. data/lib/rubydb/monitoring/health.rb +216 -222
  258. data/lib/rubydb/monitoring/logger.rb +188 -193
  259. data/lib/rubydb/monitoring/metrics.rb +363 -359
  260. data/lib/rubydb/monitoring/performance.rb +176 -176
  261. data/lib/rubydb/monitoring/statistics.rb +168 -170
  262. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  263. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  264. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  265. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  266. data/lib/rubydb/mvcc/version.rb +106 -106
  267. data/lib/rubydb/mvcc/version_store.rb +396 -398
  268. data/lib/rubydb/mvcc/visibility.rb +107 -109
  269. data/lib/rubydb/protocol/capabilities.rb +125 -125
  270. data/lib/rubydb/protocol/decoder.rb +142 -145
  271. data/lib/rubydb/protocol/encoder.rb +131 -136
  272. data/lib/rubydb/protocol/handshake.rb +306 -305
  273. data/lib/rubydb/protocol/message.rb +121 -121
  274. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  275. data/lib/rubydb/protocol/protocol.rb +276 -277
  276. data/lib/rubydb/protocol/version.rb +54 -54
  277. data/lib/rubydb/rails/adapter.rb +245 -239
  278. data/lib/rubydb/rails/connection.rb +312 -314
  279. data/lib/rubydb/rails/database_statements.rb +122 -122
  280. data/lib/rubydb/rails/migration.rb +131 -131
  281. data/lib/rubydb/rails/quoting.rb +109 -109
  282. data/lib/rubydb/rails/result.rb +117 -117
  283. data/lib/rubydb/rails/schema_statements.rb +339 -339
  284. data/lib/rubydb/rails/transaction.rb +105 -105
  285. data/lib/rubydb/rails/type.rb +126 -126
  286. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  287. data/lib/rubydb/recovery/consistency.rb +457 -467
  288. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  289. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  290. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  291. data/lib/rubydb/recovery/redo.rb +235 -237
  292. data/lib/rubydb/recovery/undo.rb +204 -206
  293. data/lib/rubydb/replication/failover.rb +5 -5
  294. data/lib/rubydb/replication/fencing.rb +63 -63
  295. data/lib/rubydb/replication/primary.rb +461 -450
  296. data/lib/rubydb/replication/replica.rb +382 -384
  297. data/lib/rubydb/replication/replication_log.rb +194 -200
  298. data/lib/rubydb/replication/replication_manager.rb +307 -308
  299. data/lib/rubydb/replication/replication_slot.rb +293 -295
  300. data/lib/rubydb/replication/replication_stream.rb +198 -201
  301. data/lib/rubydb/rubydb.rb +564 -560
  302. data/lib/rubydb/security/access_control.rb +252 -254
  303. data/lib/rubydb/security/audit_log.rb +209 -213
  304. data/lib/rubydb/security/authentication.rb +302 -302
  305. data/lib/rubydb/security/authorization.rb +282 -282
  306. data/lib/rubydb/security/credentials.rb +192 -196
  307. data/lib/rubydb/security/password.rb +205 -215
  308. data/lib/rubydb/security/permissions.rb +74 -74
  309. data/lib/rubydb/security/role.rb +99 -101
  310. data/lib/rubydb/security/user.rb +86 -86
  311. data/lib/rubydb/server/connection.rb +383 -366
  312. data/lib/rubydb/server/connection_pool.rb +193 -193
  313. data/lib/rubydb/server/lifecycle.rb +227 -228
  314. data/lib/rubydb/server/listener.rb +139 -136
  315. data/lib/rubydb/server/request_handler.rb +277 -276
  316. data/lib/rubydb/server/server.rb +363 -364
  317. data/lib/rubydb/server/session.rb +371 -369
  318. data/lib/rubydb/server/worker.rb +206 -210
  319. data/lib/rubydb/server/worker_pool.rb +168 -168
  320. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  321. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  322. data/lib/rubydb/sql/ast/commit.rb +37 -37
  323. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  324. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  325. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  326. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  327. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  328. data/lib/rubydb/sql/ast/delete.rb +54 -54
  329. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  330. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  331. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  332. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  333. data/lib/rubydb/sql/ast/explain.rb +64 -64
  334. data/lib/rubydb/sql/ast/expression.rb +617 -604
  335. data/lib/rubydb/sql/ast/insert.rb +66 -66
  336. data/lib/rubydb/sql/ast/node.rb +42 -42
  337. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  338. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  339. data/lib/rubydb/sql/ast/select.rb +88 -88
  340. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  341. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  342. data/lib/rubydb/sql/ast/update.rb +88 -88
  343. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  344. data/lib/rubydb/sql/ast/view.rb +38 -32
  345. data/lib/rubydb/sql/ast/with.rb +32 -32
  346. data/lib/rubydb/sql/grammar.rb +86 -86
  347. data/lib/rubydb/sql/keywords.rb +156 -156
  348. data/lib/rubydb/sql/lexer.rb +209 -214
  349. data/lib/rubydb/sql/operators.rb +100 -100
  350. data/lib/rubydb/sql/parser.rb +1167 -1170
  351. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  352. data/lib/rubydb/sql/planner/binder.rb +537 -550
  353. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  354. data/lib/rubydb/sql/token.rb +210 -210
  355. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  356. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  357. data/lib/rubydb/storage/database_lock.rb +74 -74
  358. data/lib/rubydb/storage/deserializer.rb +332 -342
  359. data/lib/rubydb/storage/engine.rb +2347 -2330
  360. data/lib/rubydb/storage/file_manager.rb +191 -187
  361. data/lib/rubydb/storage/free_space_map.rb +79 -81
  362. data/lib/rubydb/storage/page.rb +92 -94
  363. data/lib/rubydb/storage/page_allocator.rb +852 -855
  364. data/lib/rubydb/storage/page_header.rb +63 -67
  365. data/lib/rubydb/storage/page_manager.rb +127 -131
  366. data/lib/rubydb/storage/record.rb +58 -58
  367. data/lib/rubydb/storage/row.rb +78 -78
  368. data/lib/rubydb/storage/serializer.rb +51 -51
  369. data/lib/rubydb/storage/storage_layout.rb +151 -151
  370. data/lib/rubydb/storage/storage_manager.rb +114 -114
  371. data/lib/rubydb/storage/tuple.rb +458 -461
  372. data/lib/rubydb/storage/visibility_map.rb +964 -973
  373. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  374. data/lib/rubydb/transactions/isolation.rb +98 -98
  375. data/lib/rubydb/transactions/lock.rb +76 -76
  376. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  377. data/lib/rubydb/transactions/savepoint.rb +142 -143
  378. data/lib/rubydb/transactions/transaction.rb +214 -215
  379. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  380. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  381. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  382. data/lib/rubydb/types/bigint.rb +36 -36
  383. data/lib/rubydb/types/blob.rb +37 -37
  384. data/lib/rubydb/types/boolean.rb +34 -34
  385. data/lib/rubydb/types/date.rb +39 -39
  386. data/lib/rubydb/types/decimal.rb +48 -48
  387. data/lib/rubydb/types/float.rb +34 -34
  388. data/lib/rubydb/types/integer.rb +36 -36
  389. data/lib/rubydb/types/json.rb +41 -41
  390. data/lib/rubydb/types/null.rb +34 -34
  391. data/lib/rubydb/types/smallint.rb +36 -36
  392. data/lib/rubydb/types/text.rb +37 -37
  393. data/lib/rubydb/types/time.rb +46 -46
  394. data/lib/rubydb/types/timestamp.rb +39 -39
  395. data/lib/rubydb/types/type.rb +119 -119
  396. data/lib/rubydb/types/uuid.rb +47 -47
  397. data/lib/rubydb/types/varchar.rb +37 -37
  398. data/lib/rubydb/version.rb +32 -32
  399. data/lib/rubydb/wal/archive.rb +190 -193
  400. data/lib/rubydb/wal/checkpoint.rb +181 -183
  401. data/lib/rubydb/wal/lsn.rb +94 -94
  402. data/lib/rubydb/wal/reader.rb +259 -260
  403. data/lib/rubydb/wal/record.rb +105 -105
  404. data/lib/rubydb/wal/segment.rb +193 -193
  405. data/lib/rubydb/wal/wal.rb +480 -452
  406. data/lib/rubydb/wal/writer.rb +236 -236
  407. data/lib/rubydb.rb +7 -7
  408. data/packaging/docker/docker-compose.failover.yml +43 -43
  409. data/packaging/homebrew/rubydb.rb +19 -19
  410. data/rubydb.gemspec +60 -57
  411. data/scripts/benchmark +7 -7
  412. data/scripts/durability_drill +37 -37
  413. data/scripts/fuzz +63 -63
  414. data/scripts/release +45 -40
  415. data/scripts/release_check +43 -43
  416. data/scripts/replication_failover_drill +268 -250
  417. data/scripts/replication_network_failover_drill +287 -255
  418. data/scripts/restore_drill +45 -45
  419. data/scripts/security +45 -0
  420. metadata +54 -1
@@ -1,691 +1,686 @@
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
-
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "time"
6
+
7
+ module RubyDB
8
+ module Storage
9
+ # VisibilityMap - Tracks which transactions can see which rows
10
+ # Implements MVCC visibility with proper isolation levels
11
+ class VisibilityMap
12
+ # Visibility states for a row version
13
+ VISIBLE = :visible
14
+ HIDDEN = :hidden
15
+ DELETED = :deleted
16
+ COMMITTED = :committed
17
+ ABORTED = :aborted
18
+ IN_PROGRESS = :in_progress
19
+
20
+ attr_reader :visibility_info, :stats, :isolation_level, :active_transactions, :committed_transactions
21
+
22
+ def initialize(page_manager, config = {})
23
+ @page_manager = page_manager
24
+ @visibility_info = {}
25
+ @transaction_visibility = {}
26
+ @snapshot_cache = {}
27
+ @snapshot_cache_lru = [] # LRU order for cache eviction
28
+ @max_snapshot_cache_size = config[:max_snapshot_cache] || 100
29
+ @commit_log = []
30
+ @abort_log = []
31
+ @max_snapshot_age = config[:max_snapshot_age] || 3600 # 1 hour default
32
+ @isolation_level = config[:isolation_level] || :read_committed
33
+ @configured_visibility_path = config[:visibility_path]
34
+ @vacuum_threshold = config[:vacuum_threshold] || 1000
35
+ @vacuum_batch_size = config[:vacuum_batch_size] || 100
36
+ @stats = {
37
+ visible_checks: 0,
38
+ hidden_checks: 0,
39
+ snapshot_creations: 0,
40
+ snapshot_cache_hits: 0,
41
+ snapshot_cache_misses: 0,
42
+ vacuum_runs: 0,
43
+ vacuum_removed: 0,
44
+ old_snapshots_cleared: 0,
45
+ snapshot_evictions: 0,
46
+ transaction_rollbacks: 0,
47
+ row_version_chains: 0
48
+ }
49
+ @lock = Mutex.new
50
+ @vacuum_lock = Mutex.new
51
+ @active_transactions = {}
52
+ @committed_transactions = Set.new
53
+ @aborted_transactions = Set.new
54
+ @row_version_chains = {}
55
+ @version_history = Hash.new { |hash, row_id| hash[row_id] = {} }
56
+ @next_version_id = 1
57
+ @is_loaded = false
58
+
59
+ # Create directory for visibility data
60
+ @data_dir = config[:data_dir] || "."
61
+ FileUtils.mkdir_p(@data_dir) unless Dir.exist?(@data_dir)
62
+
63
+ load_visibility
64
+ start_vacuum_thread if config[:auto_vacuum] != false
65
+ end
66
+
67
+ # Mark a row as visible to a specific transaction
68
+ def mark_visible(row_id, transaction_id, commit_id = nil)
69
+ @lock.synchronize do
70
+ @stats[:visible_checks] += 1
71
+
72
+ version_id = @next_version_id
73
+ @next_version_id += 1
74
+
75
+ existing = @visibility_info[row_id]
76
+ prev_version = existing ? existing[:version] : 0
77
+ existing ? @row_version_chains[row_id] : nil
78
+
79
+ @visibility_info[row_id] = {
80
+ state: VISIBLE,
81
+ visible_to: transaction_id,
82
+ commit_id: commit_id || ((transaction_id.to_i == 0) ? 0 : nil),
83
+ created_at: Time.now,
84
+ last_modified: Time.now,
85
+ version: version_id,
86
+ prev_version: prev_version,
87
+ row_id: row_id,
88
+ deleted: false
89
+ }
90
+ remember_version(row_id, @visibility_info[row_id])
91
+
92
+ # Track version chain
93
+ @row_version_chains[row_id] ||= []
94
+ @row_version_chains[row_id] << version_id
95
+
96
+ if @row_version_chains[row_id].size > 100
97
+ # Trim old version chains to prevent unbounded growth
98
+ @row_version_chains[row_id] = @row_version_chains[row_id].last(50)
99
+ end
100
+
101
+ true
102
+ end
103
+ end
104
+
105
+ # Mark a row as hidden from a specific transaction (deleted or updated)
106
+ def mark_hidden(row_id, transaction_id, commit_id = nil)
107
+ @lock.synchronize do
108
+ @stats[:hidden_checks] += 1
109
+
110
+ version_id = @next_version_id
111
+ @next_version_id += 1
112
+
113
+ existing = @visibility_info[row_id]
114
+ prev_version = existing ? existing[:version] : 0
115
+
116
+ @visibility_info[row_id] = {
117
+ state: HIDDEN,
118
+ hidden_from: transaction_id,
119
+ commit_id: commit_id || ((transaction_id.to_i == 0) ? 0 : nil),
120
+ created_at: Time.now,
121
+ last_modified: Time.now,
122
+ version: version_id,
123
+ prev_version: prev_version,
124
+ row_id: row_id,
125
+ deleted: false
126
+ }
127
+ remember_version(row_id, @visibility_info[row_id])
128
+
129
+ @row_version_chains[row_id] ||= []
130
+ @row_version_chains[row_id] << version_id
131
+
132
+ true
133
+ end
134
+ end
135
+
136
+ # Mark a row as deleted (final state)
137
+ def mark_deleted(row_id, transaction_id, commit_id = nil)
138
+ @lock.synchronize do
139
+ version_id = @next_version_id
140
+ @next_version_id += 1
141
+
142
+ existing = @visibility_info[row_id]
143
+ prev_version = existing ? existing[:version] : 0
144
+
145
+ @visibility_info[row_id] = {
146
+ state: DELETED,
147
+ deleted_by: transaction_id,
148
+ commit_id: commit_id || ((transaction_id.to_i == 0) ? 0 : nil),
149
+ created_at: Time.now,
150
+ last_modified: Time.now,
151
+ version: version_id,
152
+ prev_version: prev_version,
153
+ row_id: row_id,
154
+ deleted: true
155
+ }
156
+ remember_version(row_id, @visibility_info[row_id])
157
+
158
+ @row_version_chains[row_id] ||= []
159
+ @row_version_chains[row_id] << version_id
160
+
161
+ true
162
+ end
163
+ end
164
+
165
+ # Check if a row is visible to a transaction
166
+ def is_visible?(row_id, transaction_id, snapshot_id = nil)
167
+ @lock.synchronize do
168
+ @stats[:visible_checks] += 1
169
+
170
+ info = @visibility_info[row_id]
171
+
172
+ # Rows created before visibility tracking was introduced remain
173
+ # visible; newly written rows always receive visibility metadata.
174
+ return true if info.nil?
175
+
176
+ # Check if row is deleted
177
+ return false if info[:state] == DELETED && info[:deleted_by] != transaction_id
178
+
179
+ # Check if there's a snapshot for this transaction
180
+ if snapshot_id && @snapshot_cache.key?(snapshot_id)
181
+ @stats[:snapshot_cache_hits] += 1
182
+ snapshot = @snapshot_cache[snapshot_id]
183
+ update_lru(snapshot_id)
184
+ return snapshot_visibility_check(info, snapshot, transaction_id)
185
+ end
186
+
187
+ # Check state-based visibility
188
+ case info[:state]
189
+ when VISIBLE, COMMITTED
190
+ # Check if visible_to is set
191
+ if info[:visible_to]
192
+ # Row is visible to this transaction if:
193
+ # 1. Transaction_id matches the visible_to
194
+ # 2. Or transaction_id is after commit_id (committed)
195
+ # 3. Or transaction is active and owns the visibility
196
+ if info[:visible_to] == transaction_id
197
+ return true
198
+ end
199
+
200
+ if info[:commit_id]
201
+ return true if info[:commit_id].to_i == 0
202
+ return true if @committed_transactions.include?(info[:commit_id])
203
+ end
204
+
205
+ return false
206
+ end
207
+ true
208
+
209
+ when HIDDEN
210
+ # Row is hidden from this transaction if:
211
+ # 1. hidden_from matches transaction_id (same transaction hid it)
212
+ # 2. Or commit_id > transaction_id (committed after transaction started)
213
+ # 3. Or transaction is the one that hid it
214
+ if info[:hidden_from]
215
+ return false if info[:hidden_from] == transaction_id
216
+
217
+ if info[:commit_id] && info[:commit_id] > transaction_id
218
+ return false
219
+ end
220
+ end
221
+ true
222
+
223
+ when DELETED
224
+ # Deleted rows are not visible to anyone except the transaction that deleted them
225
+ return info[:deleted_by] == transaction_id if info[:deleted_by]
226
+ false
227
+
228
+ else
229
+ true
230
+ end
231
+ end
232
+ end
233
+
234
+ # Check visibility using a snapshot
235
+ def snapshot_visibility_check(info, snapshot, transaction_id)
236
+ snapshot_start = snapshot[:start_time]
237
+ snapshot_active = snapshot[:active_transactions]
238
+ snapshot_committed = snapshot[:committed_transactions] || []
239
+
240
+ # If row was created after snapshot, it's not visible
241
+ if info[:created_at] > snapshot_start
242
+ return false
243
+ end
244
+
245
+ # If row was modified after snapshot, check if it's visible
246
+ if info[:last_modified] > snapshot_start
247
+ # Check if the modifying transaction is in the snapshot's active list
248
+ if info[:commit_id]
249
+ # If commit_id is in active transactions when snapshot was taken, it's not visible
250
+ return false if snapshot_active.include?(info[:commit_id])
251
+
252
+ # If commit_id was committed after snapshot, it's not visible
253
+ return false if snapshot_committed.include?(info[:commit_id])
254
+
255
+ # If commit_id is the current transaction, it might be visible
256
+ return true if info[:commit_id] == transaction_id
257
+ end
258
+
259
+ # Check if this is the current transaction's own changes
260
+ if info[:visible_to] == transaction_id || info[:hidden_from] == transaction_id
261
+ return true
262
+ end
263
+
264
+ return false
265
+ end
266
+
267
+ # Row was created before snapshot and not modified after
268
+ true
269
+ end
270
+
271
+ # Create a snapshot for a transaction
272
+ def create_snapshot(transaction_id)
273
+ @lock.synchronize do
274
+ @stats[:snapshot_creations] += 1
275
+
276
+ snapshot_id = "snapshot_#{transaction_id}_#{Time.now.to_i}_#{@stats[:snapshot_creations]}"
277
+
278
+ snapshot = {
279
+ id: snapshot_id,
280
+ transaction_id: transaction_id,
281
+ start_time: Time.now,
282
+ active_transactions: @active_transactions.keys.dup,
283
+ committed_transactions: @committed_transactions.to_a.dup,
284
+ isolation_level: @isolation_level,
285
+ created_at: Time.now,
286
+ last_access: Time.now
287
+ }
288
+
289
+ # Store snapshot with LRU management
290
+ if @snapshot_cache.size >= @max_snapshot_cache_size
291
+ evict_oldest_snapshot
292
+ end
293
+
294
+ @snapshot_cache[snapshot_id] = snapshot
295
+ @snapshot_cache_lru.unshift(snapshot_id)
296
+
297
+ # Clean old snapshots
298
+ clean_old_snapshots
299
+
300
+ snapshot_id
301
+ end
302
+ end
303
+
304
+ # Get a snapshot by ID
305
+ def get_snapshot(snapshot_id)
306
+ @lock.synchronize do
307
+ snapshot = @snapshot_cache[snapshot_id]
308
+ update_lru(snapshot_id) if snapshot
309
+ snapshot
310
+ end
311
+ end
312
+
313
+ # Register an active transaction
314
+ def register_transaction(transaction_id)
315
+ @lock.synchronize do
316
+ @active_transactions[transaction_id] = {
317
+ started_at: Time.now,
318
+ status: :active,
319
+ visibility: {},
320
+ row_locks: Set.new,
321
+ snapshot_id: nil
322
+ }
323
+
324
+ # Stronger isolation levels are rejected by Engine until historical
325
+ # row versions are persisted. Do not create a snapshot here while
326
+ # holding @lock; create_snapshot also takes this lock.
327
+
328
+ true
329
+ end
330
+ end
331
+
332
+ # Commit a transaction
333
+ def commit_transaction(transaction_id, commit_id = nil)
334
+ @lock.synchronize do
335
+ commit_id ||= transaction_id
336
+
337
+ # Get transaction's snapshot
338
+ tx_info = @active_transactions[transaction_id]
339
+ snapshot_id = tx_info ? tx_info[:snapshot_id] : nil
340
+
341
+ # Update all visibility info committed by this transaction
342
+ updated_rows = []
343
+
344
+ @visibility_info.each do |row_id, info|
345
+ if info[:visible_to] == transaction_id ||
346
+ info[:hidden_from] == transaction_id ||
347
+ info[:deleted_by] == transaction_id
348
+
349
+ # Mark as committed
350
+ info[:commit_id] = commit_id
351
+ info[:state] = COMMITTED if info[:state] == VISIBLE || info[:state] == HIDDEN
352
+ info[:last_modified] = Time.now
353
+ updated_rows << row_id
354
+
355
+ # Update version chain
356
+ if @row_version_chains[row_id]
357
+ @row_version_chains[row_id] << info[:version]
358
+ end
359
+ end
360
+ end
361
+
362
+ # Move to committed transactions
363
+ @committed_transactions.add(transaction_id)
364
+ @active_transactions.delete(transaction_id)
365
+
366
+ @commit_log << {
367
+ transaction_id: transaction_id,
368
+ commit_id: commit_id,
369
+ time: Time.now,
370
+ rows_updated: updated_rows.size,
371
+ snapshot_id: snapshot_id
372
+ }
373
+
374
+ # Clean commit log if too large
375
+ if @commit_log.size > 1000
376
+ @commit_log = @commit_log.last(500)
377
+ end
378
+
379
+ # Remove transaction's snapshot
380
+ if snapshot_id
381
+ @snapshot_cache.delete(snapshot_id)
382
+ @snapshot_cache_lru.delete(snapshot_id)
383
+ end
384
+
385
+ # Return commit info
386
+ {
387
+ transaction_id: transaction_id,
388
+ commit_id: commit_id,
389
+ rows_updated: updated_rows.size,
390
+ committed_at: Time.now
391
+ }
392
+ end
393
+ end
394
+
395
+ # Abort a transaction
396
+ def abort_transaction(transaction_id)
397
+ @lock.synchronize do
398
+ @stats[:transaction_rollbacks] += 1
399
+
400
+ # Get transaction's snapshot
401
+ tx_info = @active_transactions[transaction_id]
402
+ snapshot_id = tx_info ? tx_info[:snapshot_id] : nil
403
+
404
+ # Track which rows were affected
405
+ affected_rows = []
406
+
407
+ # Revert visibility changes made by this transaction
408
+ @visibility_info.each do |row_id, info|
409
+ if info[:visible_to] == transaction_id ||
410
+ info[:hidden_from] == transaction_id ||
411
+ info[:deleted_by] == transaction_id
412
+
413
+ affected_rows << row_id
414
+
415
+ # Revert to previous version if exists
416
+ prev_version = info[:prev_version]
417
+
418
+ if prev_version > 0
419
+ # Find previous version in version chain
420
+ if @row_version_chains[row_id]
421
+ prev_info = find_version_by_id(row_id, prev_version)
422
+ if prev_info
423
+ # Restore previous state
424
+ @visibility_info[row_id] = prev_info
425
+ else
426
+ # Remove completely if previous version not found
427
+ @visibility_info.delete(row_id)
428
+ end
429
+ end
430
+ else
431
+ # No previous version - remove completely
432
+ @visibility_info.delete(row_id)
433
+ end
434
+ end
435
+ end
436
+
437
+ # Move to aborted transactions
438
+ @aborted_transactions.add(transaction_id)
439
+ @active_transactions.delete(transaction_id)
440
+
441
+ @abort_log << {
442
+ transaction_id: transaction_id,
443
+ time: Time.now,
444
+ rows_affected: affected_rows.size,
445
+ snapshot_id: snapshot_id
446
+ }
447
+
448
+ # Clean abort log if too large
449
+ if @abort_log.size > 1000
450
+ @abort_log = @abort_log.last(500)
451
+ end
452
+
453
+ # Remove transaction's snapshot
454
+ if snapshot_id
455
+ @snapshot_cache.delete(snapshot_id)
456
+ @snapshot_cache_lru.delete(snapshot_id)
457
+ end
458
+
459
+ affected_rows.size
460
+ end
461
+ end
462
+
463
+ # Find a version by ID in the version chain
464
+ def find_version_by_id(row_id, version_id)
465
+ history = @version_history[row_id]
466
+ history[version_id.to_i] || begin
467
+ info = @visibility_info[row_id]
468
+ info if info && info[:version].to_i == version_id.to_i
469
+ end
470
+ end
471
+
472
+ # Check if a transaction is active
473
+ def transaction_active?(transaction_id)
474
+ @active_transactions.key?(transaction_id)
475
+ end
476
+
477
+ # Check if a transaction is committed
478
+ def transaction_committed?(transaction_id)
479
+ @committed_transactions.include?(transaction_id)
480
+ end
481
+
482
+ # Check if a transaction is aborted
483
+ def transaction_aborted?(transaction_id)
484
+ @aborted_transactions.include?(transaction_id)
485
+ end
486
+
487
+ # Get all rows visible to a transaction
488
+ def transaction_visible_rows(transaction_id)
489
+ @lock.synchronize do
490
+ result = []
491
+ @visibility_info.each do |row_id, info|
492
+ if is_visible?(row_id, transaction_id)
493
+ result << row_id
494
+ end
495
+ end
496
+ result
497
+ end
498
+ end
499
+
500
+ # Get all rows hidden from a transaction
501
+ def transaction_hidden_rows(transaction_id)
502
+ @lock.synchronize do
503
+ result = []
504
+ @visibility_info.each do |row_id, info|
505
+ unless is_visible?(row_id, transaction_id)
506
+ result << row_id
507
+ end
508
+ end
509
+ result
510
+ end
511
+ end
512
+
513
+ # Get row visibility history
514
+ def row_history(row_id)
515
+ @lock.synchronize do
516
+ info = @visibility_info[row_id]
517
+ return [] unless info
518
+
519
+ history = [info.dup]
520
+
521
+ # Follow version chain backwards
522
+ current = info
523
+ while current && current[:prev_version] > 0
524
+ prev_info = find_version_by_id(row_id, current[:prev_version])
525
+ if prev_info
526
+ history << prev_info.dup
527
+ current = prev_info
528
+ else
529
+ break
530
+ end
531
+ end
532
+
533
+ history.reverse
534
+ end
535
+ end
536
+
537
+ # Get row version history
538
+ def row_versions(row_id)
539
+ @lock.synchronize do
540
+ info = @visibility_info[row_id]
541
+ return [] unless info
542
+
543
+ versions = []
544
+ current = info
545
+
546
+ while current
547
+ versions << {
548
+ version: current[:version],
549
+ state: current[:state],
550
+ transaction_id: current[:visible_to] || current[:hidden_from] || current[:deleted_by],
551
+ created_at: current[:created_at],
552
+ commit_id: current[:commit_id]
553
+ }
554
+
555
+ break if current[:prev_version] == 0
556
+
557
+ current = find_version_by_id(row_id, current[:prev_version])
558
+ end
559
+
560
+ versions
561
+ end
562
+ end
563
+
564
+ # Vacuum old/deleted rows
565
+ def vacuum(max_age = @vacuum_threshold, batch_size = @vacuum_batch_size)
566
+ @vacuum_lock.synchronize do
567
+ @stats[:vacuum_runs] += 1
568
+
569
+ removed = 0
570
+ now = Time.now
571
+ rows_to_remove = []
572
+
573
+ # Collect rows to vacuum
574
+ @visibility_info.each do |row_id, info|
575
+ should_remove = false
576
+
577
+ case info[:state]
578
+ when DELETED
579
+ age = now - info[:last_modified]
580
+ should_remove = age > max_age && !needed_for_rollback?(row_id)
581
+
582
+ when COMMITTED
583
+ # Check if all transactions that need this version are gone
584
+ should_remove = committed_version_expired?(info)
585
+
586
+ when ABORTED
587
+ should_remove = true
588
+ end
589
+
590
+ if should_remove
591
+ rows_to_remove << row_id
592
+ removed += 1
593
+ end
594
+
595
+ break if removed >= batch_size
596
+ end
597
+
598
+ # Remove rows
599
+ rows_to_remove.each do |row_id|
600
+ @visibility_info.delete(row_id)
601
+ @row_version_chains.delete(row_id)
602
+ remove_row_from_disk(row_id)
603
+ end
604
+
605
+ @stats[:vacuum_removed] += removed
606
+
607
+ # Clean up old snapshots and transactions
608
+ clean_old_snapshots
609
+ clean_old_transactions
610
+
611
+ # Flush if we removed a significant number of rows
612
+ if removed > 0
613
+ flush
614
+ end
615
+
616
+ {
617
+ removed: removed,
618
+ total_rows: @visibility_info.size,
619
+ total_vacuumed: @stats[:vacuum_removed]
620
+ }
621
+ end
622
+ end
623
+
624
+ # Check if a deleted row is needed for potential rollback
625
+ def needed_for_rollback?(row_id)
626
+ # Check if any active transaction might need to rollback this row
627
+ @active_transactions.each do |tx_id, _info|
628
+ # If a transaction modified this row, it might need to rollback
629
+ if @visibility_info[row_id] &&
630
+ (@visibility_info[row_id][:visible_to] == tx_id ||
631
+ @visibility_info[row_id][:hidden_from] == tx_id ||
632
+ @visibility_info[row_id][:deleted_by] == tx_id)
633
+ return true
634
+ end
635
+ end
636
+ false
637
+ end
638
+
639
+ # Check if a committed version has expired
640
+ def committed_version_expired?(info)
641
+ return true if @active_transactions.empty?
642
+
643
+ # If no active transactions need this version
644
+ min_active = @active_transactions.keys.min || 0
645
+ info[:commit_id] && info[:commit_id] < min_active
646
+ end
647
+
648
+ # Load visibility map from disk
649
+ def load_visibility
650
+ @lock.synchronize do
651
+ @visibility_info.clear
652
+ @active_transactions.clear
653
+ @committed_transactions.clear
654
+ @aborted_transactions.clear
655
+ @snapshot_cache.clear
656
+ @snapshot_cache_lru.clear
657
+ @row_version_chains.clear
658
+ @version_history.clear
659
+
660
+ # Try to load from storage
661
+ begin
662
+ if File.exist?(visibility_path)
663
+ data = File.read(visibility_path)
664
+ parsed = JSON.parse(data, symbolize_names: true)
665
+
666
+ parsed[:visibility_info]&.each do |row_id_str, info|
667
+ row_id = row_id_str.to_s.to_i
668
+ symbolized_info = {}
669
+ info.each do |key, value|
670
+ value = value.to_sym if %i[state].include?(key.to_sym) && value.respond_to?(:to_sym)
671
+ symbolized_info[key.to_sym] = value
672
+ end
673
+ @visibility_info[row_id] = symbolized_info
674
+ remember_version(row_id, symbolized_info)
675
+ end
676
+
677
+ parsed[:version_history]&.each do |row_id_str, versions|
678
+ versions.each do |version_id, info|
679
+ normalized = normalize_loaded_info(info)
680
+ @version_history[row_id_str.to_s.to_i][version_id.to_s.to_i] = normalized
681
+ end
682
+ end
683
+
689
684
  @active_transactions = normalize_numeric_keyed_hash(parsed[:active_transactions])
690
685
  @committed_transactions = Set.new(parsed[:committed_transactions] || [])
691
686
  @aborted_transactions = Set.new(parsed[:aborted_transactions] || [])
@@ -693,58 +688,56 @@ module RubyDB
693
688
  @row_version_chains = normalize_numeric_keyed_hash(parsed[:row_version_chains]) do |versions|
694
689
  Array(versions).map(&:to_i)
695
690
  end
696
-
697
- # Clean up any invalid data
698
- @active_transactions.each do |tx_id, info|
699
- if info[:started_at]
700
- info[:started_at] = Time.parse(info[:started_at]) if info[:started_at].is_a?(String)
701
- end
702
- end
703
-
704
- @is_loaded = true
705
- end
706
- rescue => e
707
- # Never discard persisted visibility state silently; a corrupt or
708
- # incompatible MVCC map must fail closed so recovery can intervene.
709
- raise RubyDB::CorruptionError, "Failed to load visibility map: #{e.message}"
710
- end
711
- end
712
- end
713
-
714
- # Flush visibility map to disk
715
- def flush
716
- @lock.synchronize do
717
- begin
718
- data = {
719
- visibility_info: @visibility_info,
720
- active_transactions: @active_transactions,
721
- committed_transactions: @committed_transactions.to_a,
722
- aborted_transactions: @aborted_transactions.to_a,
723
- next_version_id: @next_version_id,
724
- row_version_chains: @row_version_chains,
725
- version_history: @version_history,
726
- timestamp: Time.now.iso8601,
727
- version: 2
728
- }
729
-
730
- # Write to a temp file first, then rename
731
- temp_path = "#{visibility_path}.tmp"
732
- File.write(temp_path, JSON.generate(data))
733
- FileUtils.mv(temp_path, visibility_path)
734
-
735
- true
691
+
692
+ # Clean up any invalid data
693
+ @active_transactions.each do |tx_id, info|
694
+ if info[:started_at]
695
+ info[:started_at] = Time.parse(info[:started_at]) if info[:started_at].is_a?(String)
696
+ end
697
+ end
698
+
699
+ @is_loaded = true
700
+ end
736
701
  rescue => e
737
- File.delete(temp_path) if defined?(temp_path) && File.file?(temp_path)
738
- raise StorageError, "Failed to flush visibility map: #{e.message}"
702
+ # Never discard persisted visibility state silently; a corrupt or
703
+ # incompatible MVCC map must fail closed so recovery can intervene.
704
+ raise RubyDB::CorruptionError, "Failed to load visibility map: #{e.message}"
739
705
  end
740
706
  end
741
707
  end
742
-
743
- # Remove a row from disk storage
744
- def remove_row_from_disk(row_id)
745
- true
746
- end
747
-
708
+
709
+ # Flush visibility map to disk
710
+ def flush
711
+ @lock.synchronize do
712
+ data = {
713
+ visibility_info: @visibility_info,
714
+ active_transactions: @active_transactions,
715
+ committed_transactions: @committed_transactions.to_a,
716
+ aborted_transactions: @aborted_transactions.to_a,
717
+ next_version_id: @next_version_id,
718
+ row_version_chains: @row_version_chains,
719
+ version_history: @version_history,
720
+ timestamp: Time.now.iso8601,
721
+ version: 2
722
+ }
723
+
724
+ # Write to a temp file first, then rename
725
+ temp_path = "#{visibility_path}.tmp"
726
+ File.write(temp_path, JSON.generate(data))
727
+ FileUtils.mv(temp_path, visibility_path)
728
+
729
+ true
730
+ rescue => e
731
+ File.delete(temp_path) if defined?(temp_path) && File.file?(temp_path)
732
+ raise StorageError, "Failed to flush visibility map: #{e.message}"
733
+ end
734
+ end
735
+
736
+ # Remove a row from disk storage
737
+ def remove_row_from_disk(row_id)
738
+ true
739
+ end
740
+
748
741
  def remember_version(row_id, info)
749
742
  @version_history[row_id][info[:version].to_i] = info.dup
750
743
  end
@@ -758,240 +751,238 @@ module RubyDB
758
751
  end
759
752
  normalized
760
753
  end
761
-
762
- def normalize_loaded_info(info)
763
- info.each_with_object({}) do |(key, value), normalized|
764
- normalized[key.to_sym] = if key.to_sym == :state && value.respond_to?(:to_sym)
765
- value.to_sym
766
- elsif %i[created_at last_modified].include?(key.to_sym) && value.is_a?(String)
767
- Time.parse(value)
768
- else
769
- value
770
- end
771
- end
772
- end
773
-
774
- # Get visibility statistics
775
- def stats
776
- @lock.synchronize do
777
- # Calculate average version chain length
778
- avg_chain = if @row_version_chains.empty?
779
- 0.0
780
- else
781
- @row_version_chains.values.map(&:size).sum.to_f / @row_version_chains.size
782
- end
783
-
784
- {
785
- total_rows: @visibility_info.size,
786
- visible_rows: @visibility_info.count { |_, info| info[:state] == VISIBLE || info[:state] == COMMITTED },
787
- hidden_rows: @visibility_info.count { |_, info| info[:state] == HIDDEN },
788
- deleted_rows: @visibility_info.count { |_, info| info[:state] == DELETED },
789
- active_transactions: @active_transactions.size,
790
- committed_transactions: @committed_transactions.size,
791
- aborted_transactions: @aborted_transactions.size,
792
- snapshots: @snapshot_cache.size,
793
- version_chains: @row_version_chains.size,
794
- avg_chain_length: avg_chain.round(2),
795
- visible_checks: @stats[:visible_checks],
796
- hidden_checks: @stats[:hidden_checks],
797
- snapshot_creations: @stats[:snapshot_creations],
798
- snapshot_cache_hits: @stats[:snapshot_cache_hits],
799
- snapshot_cache_misses: @stats[:snapshot_cache_misses],
800
- snapshot_cache_hit_rate: snapshot_hit_rate,
801
- vacuum_runs: @stats[:vacuum_runs],
802
- vacuum_removed: @stats[:vacuum_removed],
803
- old_snapshots_cleared: @stats[:old_snapshots_cleared],
804
- snapshot_evictions: @stats[:snapshot_evictions],
805
- transaction_rollbacks: @stats[:transaction_rollbacks],
806
- isolation_level: @isolation_level,
807
- next_version_id: @next_version_id,
808
- is_loaded: @is_loaded
809
- }
810
- end
811
- end
812
-
813
- # Clean up old snapshots
814
- def clean_old_snapshots
815
- @lock.synchronize do
816
- now = Time.now
817
- to_remove = []
818
-
819
- @snapshot_cache.each do |id, snapshot|
820
- age = now - snapshot[:created_at]
821
- if age > @max_snapshot_age
822
- to_remove << id
823
- end
824
- end
825
-
826
- to_remove.each do |id|
827
- @snapshot_cache.delete(id)
828
- @snapshot_cache_lru.delete(id)
829
- @stats[:old_snapshots_cleared] += 1
830
- end
831
- end
832
- end
833
-
834
- # Clean up old transaction records
835
- def clean_old_transactions
836
- @lock.synchronize do
837
- now = Time.now
838
- cutoff = now - 86400 # 24 hours
839
-
840
- @active_transactions.each do |tx_id, info|
841
- if info[:started_at] < cutoff
842
- @active_transactions.delete(tx_id)
843
- end
844
- end
845
-
846
- # Clean up old committed transactions
847
- old_committed = @committed_transactions.to_a.select do |tx_id|
848
- # Check if transaction is older than cutoff
849
- # For simplicity, we'll just keep recent ones
850
- false
851
- end
852
-
853
- # Keep committed transactions that might still be needed
854
- @committed_transactions = Set.new(@committed_transactions.to_a.last(1000))
855
- end
856
- end
857
-
858
- # Start vacuum thread for automatic cleanup
859
- def start_vacuum_thread
860
- Thread.new do
861
- loop do
862
- sleep(300) # Run every 5 minutes
863
- begin
864
- vacuum
865
- rescue => e
866
- # Log error but continue
867
- puts "Vacuum error: #{e.message}" if $DEBUG
868
- end
869
- end
870
- end
871
- end
872
-
873
- # Set isolation level
874
- def isolation_level=(level)
875
- @lock.synchronize do
876
- @isolation_level = level
877
- end
878
- end
879
-
880
- # Get row visibility info
881
- def get_visibility_info(row_id)
882
- @lock.synchronize do
883
- @visibility_info[row_id]
884
- end
885
- end
886
-
887
- # Check if a row exists
888
- def row_exists?(row_id)
889
- @lock.synchronize do
890
- @visibility_info.key?(row_id)
891
- end
892
- end
893
-
894
- # Get active transaction count
895
- def active_transaction_count
896
- @lock.synchronize do
897
- @active_transactions.size
898
- end
899
- end
900
-
901
- # Get committed transaction count
902
- def committed_transaction_count
903
- @lock.synchronize do
904
- @committed_transactions.size
905
- end
906
- end
907
-
908
- # Reset the visibility map (dangerous - for testing)
909
- def reset!
910
- @lock.synchronize do
911
- @visibility_info.clear
912
- @active_transactions.clear
913
- @committed_transactions.clear
914
- @aborted_transactions.clear
915
- @snapshot_cache.clear
916
- @snapshot_cache_lru.clear
917
- @commit_log.clear
918
- @abort_log.clear
919
- @row_version_chains.clear
920
- @next_version_id = 1
921
- @stats = {
922
- visible_checks: 0,
923
- hidden_checks: 0,
924
- snapshot_creations: 0,
925
- snapshot_cache_hits: 0,
926
- snapshot_cache_misses: 0,
927
- vacuum_runs: 0,
928
- vacuum_removed: 0,
929
- old_snapshots_cleared: 0,
930
- snapshot_evictions: 0,
931
- transaction_rollbacks: 0,
932
- row_version_chains: 0
933
- }
934
- File.delete(visibility_path) if File.exist?(visibility_path)
935
- true
936
- end
937
- end
938
-
939
- # to_s implementation
940
- def to_s
941
- "VisibilityMap: #{@visibility_info.size} entries, #{@active_transactions.size} active transactions, #{@snapshot_cache.size} snapshots"
942
- end
943
-
944
- private
945
-
946
- # Update LRU order for a snapshot
947
- def update_lru(snapshot_id)
948
- @snapshot_cache_lru.delete(snapshot_id)
949
- @snapshot_cache_lru.unshift(snapshot_id)
950
- end
951
-
952
- # Evict the oldest snapshot from cache
953
- def evict_oldest_snapshot
954
- if @snapshot_cache_lru.any?
955
- oldest_id = @snapshot_cache_lru.last
956
- @snapshot_cache.delete(oldest_id)
957
- @snapshot_cache_lru.pop
958
- @stats[:snapshot_evictions] += 1
959
- true
960
- else
961
- false
962
- end
963
- end
964
-
965
- # Calculate snapshot hit rate
966
- def snapshot_hit_rate
967
- total = @stats[:snapshot_cache_hits] + @stats[:snapshot_cache_misses]
968
- return 0.0 if total == 0
969
- (@stats[:snapshot_cache_hits].to_f / total * 100).round(2)
970
- end
971
-
972
- # Path for visibility data file
973
- def visibility_path
974
- @visibility_path ||= begin
975
- if @configured_visibility_path
976
- FileUtils.mkdir_p(File.dirname(@configured_visibility_path))
977
- @configured_visibility_path
978
- elsif @page_manager && @page_manager.respond_to?(:path)
979
- base_path = @page_manager.path
980
- if base_path
981
- File.join(@data_dir, "#{File.basename(base_path)}.visibility")
982
- else
983
- File.join(@data_dir, "visibility_map.json")
984
- end
985
- else
986
- File.join(@data_dir, "visibility_map.json")
987
- end
988
- end
989
- end
990
-
991
- # Check if a commit exists in the log
992
- def commit_exists?(commit_id)
993
- @commit_log.any? { |log| log[:commit_id] == commit_id }
994
- end
995
- end
996
- end
997
- end
754
+
755
+ def normalize_loaded_info(info)
756
+ info.each_with_object({}) do |(key, value), normalized|
757
+ normalized[key.to_sym] = if key.to_sym == :state && value.respond_to?(:to_sym)
758
+ value.to_sym
759
+ elsif %i[created_at last_modified].include?(key.to_sym) && value.is_a?(String)
760
+ Time.parse(value)
761
+ else
762
+ value
763
+ end
764
+ end
765
+ end
766
+
767
+ # Get visibility statistics
768
+ def stats
769
+ @lock.synchronize do
770
+ # Calculate average version chain length
771
+ avg_chain = if @row_version_chains.empty?
772
+ 0.0
773
+ else
774
+ @row_version_chains.values.map(&:size).sum.to_f / @row_version_chains.size
775
+ end
776
+
777
+ {
778
+ total_rows: @visibility_info.size,
779
+ visible_rows: @visibility_info.count { |_, info| info[:state] == VISIBLE || info[:state] == COMMITTED },
780
+ hidden_rows: @visibility_info.count { |_, info| info[:state] == HIDDEN },
781
+ deleted_rows: @visibility_info.count { |_, info| info[:state] == DELETED },
782
+ active_transactions: @active_transactions.size,
783
+ committed_transactions: @committed_transactions.size,
784
+ aborted_transactions: @aborted_transactions.size,
785
+ snapshots: @snapshot_cache.size,
786
+ version_chains: @row_version_chains.size,
787
+ avg_chain_length: avg_chain.round(2),
788
+ visible_checks: @stats[:visible_checks],
789
+ hidden_checks: @stats[:hidden_checks],
790
+ snapshot_creations: @stats[:snapshot_creations],
791
+ snapshot_cache_hits: @stats[:snapshot_cache_hits],
792
+ snapshot_cache_misses: @stats[:snapshot_cache_misses],
793
+ snapshot_cache_hit_rate: snapshot_hit_rate,
794
+ vacuum_runs: @stats[:vacuum_runs],
795
+ vacuum_removed: @stats[:vacuum_removed],
796
+ old_snapshots_cleared: @stats[:old_snapshots_cleared],
797
+ snapshot_evictions: @stats[:snapshot_evictions],
798
+ transaction_rollbacks: @stats[:transaction_rollbacks],
799
+ isolation_level: @isolation_level,
800
+ next_version_id: @next_version_id,
801
+ is_loaded: @is_loaded
802
+ }
803
+ end
804
+ end
805
+
806
+ # Clean up old snapshots
807
+ def clean_old_snapshots
808
+ @lock.synchronize do
809
+ now = Time.now
810
+ to_remove = []
811
+
812
+ @snapshot_cache.each do |id, snapshot|
813
+ age = now - snapshot[:created_at]
814
+ if age > @max_snapshot_age
815
+ to_remove << id
816
+ end
817
+ end
818
+
819
+ to_remove.each do |id|
820
+ @snapshot_cache.delete(id)
821
+ @snapshot_cache_lru.delete(id)
822
+ @stats[:old_snapshots_cleared] += 1
823
+ end
824
+ end
825
+ end
826
+
827
+ # Clean up old transaction records
828
+ def clean_old_transactions
829
+ @lock.synchronize do
830
+ now = Time.now
831
+ cutoff = now - 86400 # 24 hours
832
+
833
+ @active_transactions.each do |tx_id, info|
834
+ if info[:started_at] < cutoff
835
+ @active_transactions.delete(tx_id)
836
+ end
837
+ end
838
+
839
+ # Clean up old committed transactions
840
+ @committed_transactions.to_a.select do |tx_id|
841
+ # Check if transaction is older than cutoff
842
+ # For simplicity, we'll just keep recent ones
843
+ false
844
+ end
845
+
846
+ # Keep committed transactions that might still be needed
847
+ @committed_transactions = Set.new(@committed_transactions.to_a.last(1000))
848
+ end
849
+ end
850
+
851
+ # Start vacuum thread for automatic cleanup
852
+ def start_vacuum_thread
853
+ Thread.new do
854
+ loop do
855
+ sleep(300) # Run every 5 minutes
856
+ begin
857
+ vacuum
858
+ rescue => e
859
+ # Log error but continue
860
+ puts "Vacuum error: #{e.message}" if $DEBUG
861
+ end
862
+ end
863
+ end
864
+ end
865
+
866
+ # Set isolation level
867
+ def isolation_level=(level)
868
+ @lock.synchronize do
869
+ @isolation_level = level
870
+ end
871
+ end
872
+
873
+ # Get row visibility info
874
+ def get_visibility_info(row_id)
875
+ @lock.synchronize do
876
+ @visibility_info[row_id]
877
+ end
878
+ end
879
+
880
+ # Check if a row exists
881
+ def row_exists?(row_id)
882
+ @lock.synchronize do
883
+ @visibility_info.key?(row_id)
884
+ end
885
+ end
886
+
887
+ # Get active transaction count
888
+ def active_transaction_count
889
+ @lock.synchronize do
890
+ @active_transactions.size
891
+ end
892
+ end
893
+
894
+ # Get committed transaction count
895
+ def committed_transaction_count
896
+ @lock.synchronize do
897
+ @committed_transactions.size
898
+ end
899
+ end
900
+
901
+ # Reset the visibility map (dangerous - for testing)
902
+ def reset!
903
+ @lock.synchronize do
904
+ @visibility_info.clear
905
+ @active_transactions.clear
906
+ @committed_transactions.clear
907
+ @aborted_transactions.clear
908
+ @snapshot_cache.clear
909
+ @snapshot_cache_lru.clear
910
+ @commit_log.clear
911
+ @abort_log.clear
912
+ @row_version_chains.clear
913
+ @next_version_id = 1
914
+ @stats = {
915
+ visible_checks: 0,
916
+ hidden_checks: 0,
917
+ snapshot_creations: 0,
918
+ snapshot_cache_hits: 0,
919
+ snapshot_cache_misses: 0,
920
+ vacuum_runs: 0,
921
+ vacuum_removed: 0,
922
+ old_snapshots_cleared: 0,
923
+ snapshot_evictions: 0,
924
+ transaction_rollbacks: 0,
925
+ row_version_chains: 0
926
+ }
927
+ File.delete(visibility_path) if File.exist?(visibility_path)
928
+ true
929
+ end
930
+ end
931
+
932
+ # to_s implementation
933
+ def to_s
934
+ "VisibilityMap: #{@visibility_info.size} entries, #{@active_transactions.size} active transactions, #{@snapshot_cache.size} snapshots"
935
+ end
936
+
937
+ private
938
+
939
+ # Update LRU order for a snapshot
940
+ def update_lru(snapshot_id)
941
+ @snapshot_cache_lru.delete(snapshot_id)
942
+ @snapshot_cache_lru.unshift(snapshot_id)
943
+ end
944
+
945
+ # Evict the oldest snapshot from cache
946
+ def evict_oldest_snapshot
947
+ if @snapshot_cache_lru.any?
948
+ oldest_id = @snapshot_cache_lru.last
949
+ @snapshot_cache.delete(oldest_id)
950
+ @snapshot_cache_lru.pop
951
+ @stats[:snapshot_evictions] += 1
952
+ true
953
+ else
954
+ false
955
+ end
956
+ end
957
+
958
+ # Calculate snapshot hit rate
959
+ def snapshot_hit_rate
960
+ total = @stats[:snapshot_cache_hits] + @stats[:snapshot_cache_misses]
961
+ return 0.0 if total == 0
962
+ (@stats[:snapshot_cache_hits].to_f / total * 100).round(2)
963
+ end
964
+
965
+ # Path for visibility data file
966
+ def visibility_path
967
+ @visibility_path ||= if @configured_visibility_path
968
+ FileUtils.mkdir_p(File.dirname(@configured_visibility_path))
969
+ @configured_visibility_path
970
+ elsif @page_manager&.respond_to?(:path)
971
+ base_path = @page_manager.path
972
+ if base_path
973
+ File.join(@data_dir, "#{File.basename(base_path)}.visibility")
974
+ else
975
+ File.join(@data_dir, "visibility_map.json")
976
+ end
977
+ else
978
+ File.join(@data_dir, "visibility_map.json")
979
+ end
980
+ end
981
+
982
+ # Check if a commit exists in the log
983
+ def commit_exists?(commit_id)
984
+ @commit_log.any? { |log| log[:commit_id] == commit_id }
985
+ end
986
+ end
987
+ end
988
+ end