rubydb 0.1.4 → 0.1.6

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 (469) 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 +16 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +57 -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 +168 -12
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +76 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/accelerator/bin/SHA256SUMS +6 -0
  29. data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
  30. data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
  31. data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
  32. data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
  33. data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
  34. data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
  35. data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
  36. data/accelerator/go.mod +3 -0
  37. data/accelerator/internal/execution/aggregate.go +94 -0
  38. data/accelerator/internal/execution/distinct.go +22 -0
  39. data/accelerator/internal/execution/filter.go +73 -0
  40. data/accelerator/internal/execution/join.go +79 -0
  41. data/accelerator/internal/execution/operators.go +167 -0
  42. data/accelerator/internal/execution/scan.go +20 -0
  43. data/accelerator/internal/execution/sort.go +62 -0
  44. data/accelerator/internal/execution/types.go +136 -0
  45. data/accelerator/internal/execution/value.go +67 -0
  46. data/accelerator/internal/memory/arena.go +47 -0
  47. data/accelerator/internal/memory/reuse.go +22 -0
  48. data/accelerator/internal/metrics/registry.go +67 -0
  49. data/accelerator/internal/parallel/bounded_queue.go +56 -0
  50. data/accelerator/internal/parallel/scheduler.go +47 -0
  51. data/accelerator/internal/parallel/worker_pool.go +53 -0
  52. data/accelerator/internal/protocol/cancellation.go +48 -0
  53. data/accelerator/internal/protocol/columnar.go +263 -0
  54. data/accelerator/internal/protocol/frame.go +187 -0
  55. data/accelerator/internal/runtime/worker.go +521 -0
  56. data/accelerator/internal/storage/page_reader.go +81 -0
  57. data/accelerator/internal/storage/snapshot_scan.go +539 -0
  58. data/accelerator/internal/wal/checksum.go +13 -0
  59. data/accelerator/internal/wal/compression.go +41 -0
  60. data/accelerator/internal/wal/group_commit.go +24 -0
  61. data/accelerator/internal/wal/record_encoder.go +40 -0
  62. data/adapters/activerecord/Gemfile +11 -11
  63. data/adapters/activerecord/README.md +8 -3
  64. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +881 -879
  65. data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
  66. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -143
  67. data/adapters/ruby/README.md +18 -18
  68. data/adapters/sequel/README.md +11 -11
  69. data/config/monitoring/prometheus-alerts.yml +39 -39
  70. data/config/production.yml +36 -36
  71. data/docs/README.md +77 -71
  72. data/docs/architecture/concurrency.md +14 -14
  73. data/docs/architecture/current-state.md +125 -125
  74. data/docs/architecture/execution-engine.md +25 -25
  75. data/docs/architecture/go-accelerator.md +179 -0
  76. data/docs/architecture/indexes.md +19 -19
  77. data/docs/architecture/mvcc.md +19 -19
  78. data/docs/architecture/overview.md +13 -13
  79. data/docs/architecture/pages.md +11 -11
  80. data/docs/architecture/production-roadmap.md +82 -82
  81. data/docs/architecture/query-planner.md +20 -20
  82. data/docs/architecture/recovery.md +18 -18
  83. data/docs/architecture/sql-engine.md +12 -12
  84. data/docs/architecture/storage-engine.md +14 -14
  85. data/docs/architecture/transactions.md +10 -10
  86. data/docs/architecture/wal.md +28 -28
  87. data/docs/cli-cheatsheet.md +98 -98
  88. data/docs/cli.md +299 -275
  89. data/docs/contributing/architecture.md +9 -9
  90. data/docs/contributing/benchmarking.md +30 -14
  91. data/docs/contributing/development.md +16 -16
  92. data/docs/contributing/release-process.md +49 -49
  93. data/docs/contributing/testing.md +16 -16
  94. data/docs/debugging.md +229 -229
  95. data/docs/developer/branching.md +10 -10
  96. data/docs/developer/database-diff.md +10 -10
  97. data/docs/developer/local-development.md +49 -17
  98. data/docs/developer/snapshots.md +9 -9
  99. data/docs/developer/temporal-data.md +10 -10
  100. data/docs/developer-guide.md +297 -297
  101. data/docs/getting-started/first-database.md +16 -16
  102. data/docs/getting-started/first-query.md +13 -13
  103. data/docs/getting-started/installation.md +19 -19
  104. data/docs/getting-started/local-to-production.md +300 -300
  105. data/docs/getting-started/quickstart.md +17 -17
  106. data/docs/getting-started/rails.md +16 -16
  107. data/docs/hardening_backlog.md +93 -93
  108. data/docs/lessons-learned.md +112 -112
  109. data/docs/operations/backups.md +33 -33
  110. data/docs/operations/disaster-recovery.md +31 -31
  111. data/docs/operations/failover.md +30 -30
  112. data/docs/operations/monitoring.md +25 -25
  113. data/docs/operations/production-guide.md +295 -295
  114. data/docs/operations/production-runbook.md +45 -45
  115. data/docs/operations/replication.md +33 -33
  116. data/docs/operations/restore.md +6 -6
  117. data/docs/operations/runbook.md +34 -34
  118. data/docs/operations/upgrades.md +14 -14
  119. data/docs/operations/workload-testing.md +17 -17
  120. data/docs/production-readiness.md +118 -118
  121. data/docs/production_validation.md +150 -150
  122. data/docs/rails/active-record.md +11 -11
  123. data/docs/rails/compatibility-guide.md +90 -90
  124. data/docs/rails/database-yml.md +92 -92
  125. data/docs/rails/installation.md +17 -17
  126. data/docs/rails/migrations.md +17 -17
  127. data/docs/rails/production.md +82 -82
  128. data/docs/rails/troubleshooting.md +18 -18
  129. data/docs/release.md +25 -25
  130. data/docs/server/architecture.md +10 -10
  131. data/docs/server/authentication.md +10 -10
  132. data/docs/server/configuration.md +16 -16
  133. data/docs/server/connection-pooling.md +10 -10
  134. data/docs/server/deployment.md +10 -10
  135. data/docs/server/protocol.md +12 -12
  136. data/docs/sql/compatibility-guide.md +82 -82
  137. data/docs/sql/compatibility.md +39 -39
  138. data/docs/sql/data-types.md +10 -10
  139. data/docs/sql/functions.md +9 -9
  140. data/docs/sql/joins.md +9 -9
  141. data/docs/sql/operators.md +9 -9
  142. data/docs/sql/sqlite-compatibility.md +21 -21
  143. data/docs/sql/syntax.md +10 -10
  144. data/docs/sql/transactions.md +10 -10
  145. data/docs/troubleshooting.md +244 -244
  146. data/lessons/01-foundations.md +73 -0
  147. data/lessons/02-local-development.md +121 -0
  148. data/lessons/03-embedded-rubydb.md +99 -0
  149. data/lessons/04-rails-complex-apps.md +138 -0
  150. data/lessons/05-rubydb-production-server.md +237 -0
  151. data/lessons/06-postgresql-massive-apps.md +96 -0
  152. data/lessons/07-hybrid-microservices.md +179 -0
  153. data/lessons/08-migrations-backups-recovery.md +86 -0
  154. data/lessons/09-observability-security-scale.md +87 -0
  155. data/lessons/10-release-readiness.md +192 -0
  156. data/lessons/11-community-adapter.md +323 -0
  157. data/lessons/12-rails-ecommerce-pressure.md +263 -0
  158. data/lib/rubydb/accelerator/client.rb +451 -0
  159. data/lib/rubydb/accelerator/error.rb +22 -0
  160. data/lib/rubydb/accelerator/manager.rb +606 -0
  161. data/lib/rubydb/accelerator.rb +13 -0
  162. data/lib/rubydb/backup/archive.rb +332 -334
  163. data/lib/rubydb/backup/backup.rb +400 -401
  164. data/lib/rubydb/backup/incremental.rb +349 -353
  165. data/lib/rubydb/backup/restore.rb +289 -290
  166. data/lib/rubydb/backup/snapshot.rb +265 -267
  167. data/lib/rubydb/backup/verification.rb +276 -279
  168. data/lib/rubydb/branching/branch.rb +181 -181
  169. data/lib/rubydb/branching/branch_manager.rb +307 -311
  170. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  171. data/lib/rubydb/branching/checkout.rb +165 -166
  172. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  173. data/lib/rubydb/branching/diff.rb +137 -138
  174. data/lib/rubydb/branching/merge.rb +282 -285
  175. data/lib/rubydb/build_info.rb +15 -15
  176. data/lib/rubydb/catalog/catalog.rb +391 -391
  177. data/lib/rubydb/catalog/column.rb +112 -112
  178. data/lib/rubydb/catalog/constraint.rb +180 -180
  179. data/lib/rubydb/catalog/database.rb +184 -184
  180. data/lib/rubydb/catalog/index.rb +97 -97
  181. data/lib/rubydb/catalog/schema.rb +103 -103
  182. data/lib/rubydb/catalog/sequence.rb +90 -90
  183. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  184. data/lib/rubydb/catalog/table.rb +178 -178
  185. data/lib/rubydb/catalog/trigger.rb +102 -102
  186. data/lib/rubydb/catalog/view.rb +66 -66
  187. data/lib/rubydb/cli/application.rb +168 -163
  188. data/lib/rubydb/cli/commands/accelerator.rb +72 -0
  189. data/lib/rubydb/cli/commands/backup.rb +80 -81
  190. data/lib/rubydb/cli/commands/branch.rb +72 -72
  191. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  192. data/lib/rubydb/cli/commands/create.rb +58 -58
  193. data/lib/rubydb/cli/commands/diff.rb +76 -77
  194. data/lib/rubydb/cli/commands/doctor.rb +77 -74
  195. data/lib/rubydb/cli/commands/drop.rb +57 -57
  196. data/lib/rubydb/cli/commands/init.rb +101 -102
  197. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  198. data/lib/rubydb/cli/commands/merge.rb +63 -63
  199. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  200. data/lib/rubydb/cli/commands/restart.rb +42 -39
  201. data/lib/rubydb/cli/commands/restore.rb +121 -121
  202. data/lib/rubydb/cli/commands/shell.rb +365 -365
  203. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  204. data/lib/rubydb/cli/commands/start.rb +88 -82
  205. data/lib/rubydb/cli/commands/status.rb +96 -92
  206. data/lib/rubydb/cli/commands/stop.rb +47 -47
  207. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  208. data/lib/rubydb/cli/formatter.rb +221 -221
  209. data/lib/rubydb/cli/output.rb +168 -168
  210. data/lib/rubydb/client/client.rb +309 -304
  211. data/lib/rubydb/client/connection.rb +429 -415
  212. data/lib/rubydb/client/connection_pool.rb +168 -168
  213. data/lib/rubydb/client/connection_url.rb +96 -96
  214. data/lib/rubydb/client/prepared_statement.rb +60 -60
  215. data/lib/rubydb/client/result.rb +127 -123
  216. data/lib/rubydb/client/statement.rb +52 -52
  217. data/lib/rubydb/client/transaction.rb +130 -130
  218. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  219. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  220. data/lib/rubydb/concurrency/latch.rb +101 -101
  221. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  222. data/lib/rubydb/concurrency/mutex.rb +181 -183
  223. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  224. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  225. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  226. data/lib/rubydb/configuration/config.rb +170 -170
  227. data/lib/rubydb/configuration/defaults.rb +191 -179
  228. data/lib/rubydb/configuration/environment.rb +152 -152
  229. data/lib/rubydb/configuration/parser.rb +185 -185
  230. data/lib/rubydb/configuration/validation.rb +228 -221
  231. data/lib/rubydb/constants.rb +74 -74
  232. data/lib/rubydb/constraints/check.rb +181 -181
  233. data/lib/rubydb/constraints/constraint.rb +101 -101
  234. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  235. data/lib/rubydb/constraints/not_null.rb +64 -64
  236. data/lib/rubydb/constraints/primary_key.rb +99 -99
  237. data/lib/rubydb/constraints/unique.rb +106 -108
  238. data/lib/rubydb/constraints/validator.rb +349 -350
  239. data/lib/rubydb/errors/authentication_error.rb +10 -10
  240. data/lib/rubydb/errors/authorization_error.rb +23 -23
  241. data/lib/rubydb/errors/client_error.rb +10 -10
  242. data/lib/rubydb/errors/configuration_error.rb +10 -10
  243. data/lib/rubydb/errors/connection_error.rb +10 -10
  244. data/lib/rubydb/errors/constraint_error.rb +23 -23
  245. data/lib/rubydb/errors/corruption_error.rb +10 -10
  246. data/lib/rubydb/errors/database_error.rb +10 -10
  247. data/lib/rubydb/errors/error.rb +20 -20
  248. data/lib/rubydb/errors/execution_error.rb +10 -10
  249. data/lib/rubydb/errors/parser_error.rb +10 -10
  250. data/lib/rubydb/errors/recovery_error.rb +10 -10
  251. data/lib/rubydb/errors/replication_error.rb +10 -10
  252. data/lib/rubydb/errors/server_error.rb +6 -6
  253. data/lib/rubydb/errors/storage_error.rb +10 -10
  254. data/lib/rubydb/errors/transaction_error.rb +10 -10
  255. data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
  256. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  257. data/lib/rubydb/execution/cost_model.rb +72 -0
  258. data/lib/rubydb/execution/delete_executor.rb +110 -112
  259. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  260. data/lib/rubydb/execution/executor.rb +1544 -1188
  261. data/lib/rubydb/execution/expression.rb +191 -193
  262. data/lib/rubydb/execution/index_scan.rb +142 -142
  263. data/lib/rubydb/execution/insert_executor.rb +215 -217
  264. data/lib/rubydb/execution/join_executor.rb +243 -249
  265. data/lib/rubydb/execution/limit_executor.rb +83 -85
  266. data/lib/rubydb/execution/operator_selection.rb +57 -0
  267. data/lib/rubydb/execution/optimizer.rb +227 -215
  268. data/lib/rubydb/execution/physical_plan.rb +47 -0
  269. data/lib/rubydb/execution/plan.rb +355 -353
  270. data/lib/rubydb/execution/planner.rb +508 -536
  271. data/lib/rubydb/execution/predicate.rb +235 -235
  272. data/lib/rubydb/execution/scan.rb +49 -49
  273. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  274. data/lib/rubydb/execution/sort_executor.rb +194 -185
  275. data/lib/rubydb/execution/update_executor.rb +160 -162
  276. data/lib/rubydb/functions/aggregate.rb +70 -70
  277. data/lib/rubydb/functions/date_functions.rb +274 -278
  278. data/lib/rubydb/functions/function.rb +85 -85
  279. data/lib/rubydb/functions/json_functions.rb +231 -215
  280. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  281. data/lib/rubydb/functions/scalar.rb +52 -52
  282. data/lib/rubydb/functions/string_functions.rb +383 -383
  283. data/lib/rubydb/functions/system_functions.rb +258 -246
  284. data/lib/rubydb/history/as_of.rb +238 -238
  285. data/lib/rubydb/history/change.rb +105 -105
  286. data/lib/rubydb/history/history.rb +131 -131
  287. data/lib/rubydb/history/history_manager.rb +228 -229
  288. data/lib/rubydb/history/temporal_query.rb +202 -202
  289. data/lib/rubydb/history/timeline.rb +144 -144
  290. data/lib/rubydb/indexes/btree.rb +215 -186
  291. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  292. data/lib/rubydb/indexes/btree_node.rb +384 -385
  293. data/lib/rubydb/indexes/hash_index.rb +150 -150
  294. data/lib/rubydb/indexes/index.rb +71 -71
  295. data/lib/rubydb/indexes/index_manager.rb +408 -406
  296. data/lib/rubydb/indexes/index_scan.rb +466 -470
  297. data/lib/rubydb/migrations/migration.rb +253 -254
  298. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  299. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  300. data/lib/rubydb/migrations/migration_version.rb +71 -71
  301. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  302. data/lib/rubydb/migrations/schema_version.rb +64 -64
  303. data/lib/rubydb/monitoring/events.rb +155 -160
  304. data/lib/rubydb/monitoring/health.rb +216 -222
  305. data/lib/rubydb/monitoring/logger.rb +188 -193
  306. data/lib/rubydb/monitoring/metrics.rb +363 -359
  307. data/lib/rubydb/monitoring/performance.rb +176 -176
  308. data/lib/rubydb/monitoring/statistics.rb +168 -170
  309. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  310. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  311. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  312. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  313. data/lib/rubydb/mvcc/version.rb +106 -106
  314. data/lib/rubydb/mvcc/version_store.rb +396 -398
  315. data/lib/rubydb/mvcc/visibility.rb +107 -109
  316. data/lib/rubydb/protocol/capabilities.rb +125 -125
  317. data/lib/rubydb/protocol/decoder.rb +142 -145
  318. data/lib/rubydb/protocol/encoder.rb +131 -136
  319. data/lib/rubydb/protocol/handshake.rb +306 -305
  320. data/lib/rubydb/protocol/message.rb +121 -121
  321. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  322. data/lib/rubydb/protocol/protocol.rb +276 -277
  323. data/lib/rubydb/protocol/version.rb +54 -54
  324. data/lib/rubydb/rails/adapter.rb +245 -239
  325. data/lib/rubydb/rails/connection.rb +312 -314
  326. data/lib/rubydb/rails/database_statements.rb +122 -122
  327. data/lib/rubydb/rails/migration.rb +131 -131
  328. data/lib/rubydb/rails/quoting.rb +109 -109
  329. data/lib/rubydb/rails/result.rb +117 -117
  330. data/lib/rubydb/rails/schema_statements.rb +339 -339
  331. data/lib/rubydb/rails/transaction.rb +105 -105
  332. data/lib/rubydb/rails/type.rb +126 -126
  333. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  334. data/lib/rubydb/recovery/consistency.rb +457 -467
  335. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  336. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  337. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  338. data/lib/rubydb/recovery/redo.rb +235 -237
  339. data/lib/rubydb/recovery/undo.rb +204 -206
  340. data/lib/rubydb/replication/failover.rb +5 -5
  341. data/lib/rubydb/replication/fencing.rb +63 -63
  342. data/lib/rubydb/replication/primary.rb +461 -450
  343. data/lib/rubydb/replication/replica.rb +382 -384
  344. data/lib/rubydb/replication/replication_log.rb +194 -200
  345. data/lib/rubydb/replication/replication_manager.rb +307 -308
  346. data/lib/rubydb/replication/replication_slot.rb +293 -295
  347. data/lib/rubydb/replication/replication_stream.rb +198 -201
  348. data/lib/rubydb/rubydb.rb +570 -560
  349. data/lib/rubydb/security/access_control.rb +252 -254
  350. data/lib/rubydb/security/audit_log.rb +209 -213
  351. data/lib/rubydb/security/authentication.rb +302 -302
  352. data/lib/rubydb/security/authorization.rb +282 -282
  353. data/lib/rubydb/security/credentials.rb +192 -196
  354. data/lib/rubydb/security/password.rb +205 -215
  355. data/lib/rubydb/security/permissions.rb +74 -74
  356. data/lib/rubydb/security/role.rb +99 -101
  357. data/lib/rubydb/security/user.rb +86 -86
  358. data/lib/rubydb/server/connection.rb +383 -366
  359. data/lib/rubydb/server/connection_pool.rb +193 -193
  360. data/lib/rubydb/server/lifecycle.rb +227 -228
  361. data/lib/rubydb/server/listener.rb +139 -136
  362. data/lib/rubydb/server/request_handler.rb +277 -276
  363. data/lib/rubydb/server/server.rb +363 -364
  364. data/lib/rubydb/server/session.rb +416 -369
  365. data/lib/rubydb/server/worker.rb +206 -210
  366. data/lib/rubydb/server/worker_pool.rb +168 -168
  367. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  368. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  369. data/lib/rubydb/sql/ast/commit.rb +37 -37
  370. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  371. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  372. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  373. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  374. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  375. data/lib/rubydb/sql/ast/delete.rb +54 -54
  376. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  377. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  378. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  379. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  380. data/lib/rubydb/sql/ast/explain.rb +64 -64
  381. data/lib/rubydb/sql/ast/expression.rb +617 -604
  382. data/lib/rubydb/sql/ast/insert.rb +66 -66
  383. data/lib/rubydb/sql/ast/node.rb +42 -42
  384. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  385. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  386. data/lib/rubydb/sql/ast/select.rb +88 -88
  387. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  388. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  389. data/lib/rubydb/sql/ast/update.rb +88 -88
  390. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  391. data/lib/rubydb/sql/ast/view.rb +38 -32
  392. data/lib/rubydb/sql/ast/with.rb +32 -32
  393. data/lib/rubydb/sql/grammar.rb +86 -86
  394. data/lib/rubydb/sql/keywords.rb +156 -156
  395. data/lib/rubydb/sql/lexer.rb +209 -214
  396. data/lib/rubydb/sql/operators.rb +100 -100
  397. data/lib/rubydb/sql/parser.rb +1167 -1170
  398. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  399. data/lib/rubydb/sql/planner/binder.rb +537 -550
  400. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  401. data/lib/rubydb/sql/token.rb +210 -210
  402. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  403. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  404. data/lib/rubydb/storage/database_lock.rb +74 -74
  405. data/lib/rubydb/storage/deserializer.rb +332 -342
  406. data/lib/rubydb/storage/engine.rb +2409 -2330
  407. data/lib/rubydb/storage/file_manager.rb +191 -187
  408. data/lib/rubydb/storage/free_space_map.rb +79 -81
  409. data/lib/rubydb/storage/page.rb +92 -94
  410. data/lib/rubydb/storage/page_allocator.rb +852 -855
  411. data/lib/rubydb/storage/page_header.rb +63 -67
  412. data/lib/rubydb/storage/page_manager.rb +127 -131
  413. data/lib/rubydb/storage/record.rb +58 -58
  414. data/lib/rubydb/storage/row.rb +78 -78
  415. data/lib/rubydb/storage/serializer.rb +51 -51
  416. data/lib/rubydb/storage/snapshot_reader.rb +167 -0
  417. data/lib/rubydb/storage/storage_layout.rb +151 -151
  418. data/lib/rubydb/storage/storage_manager.rb +114 -114
  419. data/lib/rubydb/storage/tuple.rb +458 -461
  420. data/lib/rubydb/storage/visibility_map.rb +964 -973
  421. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  422. data/lib/rubydb/transactions/isolation.rb +98 -98
  423. data/lib/rubydb/transactions/lock.rb +76 -76
  424. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  425. data/lib/rubydb/transactions/savepoint.rb +142 -143
  426. data/lib/rubydb/transactions/transaction.rb +214 -215
  427. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  428. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  429. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  430. data/lib/rubydb/types/bigint.rb +36 -36
  431. data/lib/rubydb/types/blob.rb +37 -37
  432. data/lib/rubydb/types/boolean.rb +34 -34
  433. data/lib/rubydb/types/date.rb +39 -39
  434. data/lib/rubydb/types/decimal.rb +48 -48
  435. data/lib/rubydb/types/float.rb +34 -34
  436. data/lib/rubydb/types/integer.rb +36 -36
  437. data/lib/rubydb/types/json.rb +41 -41
  438. data/lib/rubydb/types/null.rb +34 -34
  439. data/lib/rubydb/types/smallint.rb +36 -36
  440. data/lib/rubydb/types/text.rb +37 -37
  441. data/lib/rubydb/types/time.rb +46 -46
  442. data/lib/rubydb/types/timestamp.rb +39 -39
  443. data/lib/rubydb/types/type.rb +119 -119
  444. data/lib/rubydb/types/uuid.rb +47 -47
  445. data/lib/rubydb/types/varchar.rb +37 -37
  446. data/lib/rubydb/version.rb +32 -32
  447. data/lib/rubydb/wal/archive.rb +207 -193
  448. data/lib/rubydb/wal/checkpoint.rb +181 -183
  449. data/lib/rubydb/wal/lsn.rb +94 -94
  450. data/lib/rubydb/wal/reader.rb +259 -260
  451. data/lib/rubydb/wal/record.rb +105 -105
  452. data/lib/rubydb/wal/segment.rb +193 -193
  453. data/lib/rubydb/wal/wal.rb +481 -452
  454. data/lib/rubydb/wal/writer.rb +236 -236
  455. data/lib/rubydb.rb +7 -7
  456. data/packaging/docker/docker-compose.failover.yml +43 -43
  457. data/packaging/homebrew/rubydb.rb +19 -19
  458. data/rubydb.gemspec +70 -57
  459. data/scripts/benchmark +7 -7
  460. data/scripts/build_accelerator +49 -0
  461. data/scripts/durability_drill +37 -37
  462. data/scripts/fuzz +63 -63
  463. data/scripts/release +77 -42
  464. data/scripts/release_check +43 -43
  465. data/scripts/replication_failover_drill +268 -250
  466. data/scripts/replication_network_failover_drill +287 -255
  467. data/scripts/restore_drill +45 -45
  468. data/scripts/security +45 -0
  469. metadata +102 -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