rubydb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +20 -0
  3. data/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  4. data/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  5. data/.github/ISSUE_TEMPLATE/performance.yml +0 -0
  6. data/.github/ISSUE_TEMPLATE/security.yml +0 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  8. data/.github/dependabot.yml +0 -0
  9. data/.github/workflows/benchmark.yml +26 -0
  10. data/.github/workflows/compatibility.yml +63 -0
  11. data/.github/workflows/fuzz.yml +33 -0
  12. data/.github/workflows/lint.yml +21 -0
  13. data/.github/workflows/operations.yml +24 -0
  14. data/.github/workflows/production-validation.yml +111 -0
  15. data/.github/workflows/release.yml +77 -0
  16. data/.github/workflows/security.yml +37 -0
  17. data/.github/workflows/test.yml +26 -0
  18. data/.github/workflows/workload.yml +58 -0
  19. data/.gitignore +43 -0
  20. data/.rspec +4 -0
  21. data/.rubocop.yml +44 -0
  22. data/.ruby-version +1 -0
  23. data/.standard.yml +14 -0
  24. data/ARCHITECTURE.md +21 -0
  25. data/CHANGELOG.md +31 -0
  26. data/CODE_OF_CONDUCT.md +13 -0
  27. data/CONTRIBUTING.md +29 -0
  28. data/GOVERNANCE.md +16 -0
  29. data/Gemfile +17 -0
  30. data/Gemfile.lock +71 -0
  31. data/LICENSE +60 -0
  32. data/README.md +162 -0
  33. data/ROADMAP.md +27 -0
  34. data/Rakefile +71 -0
  35. data/SECURITY.md +54 -0
  36. data/SUPPORT.md +14 -0
  37. data/adapters/activerecord/Gemfile +11 -0
  38. data/adapters/activerecord/README.md +9 -0
  39. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +887 -0
  40. data/adapters/activerecord/rubydb-activerecord.gemspec +27 -0
  41. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +151 -0
  42. data/adapters/ruby/README.md +18 -0
  43. data/adapters/sequel/README.md +11 -0
  44. data/bin/rubydb +19 -0
  45. data/bin/rubydb-server +126 -0
  46. data/config/development.yml +0 -0
  47. data/config/monitoring/prometheus-alerts.yml +39 -0
  48. data/config/production.yml +36 -0
  49. data/config/test.yml +0 -0
  50. data/docs/README.md +71 -0
  51. data/docs/architecture/concurrency.md +14 -0
  52. data/docs/architecture/current-state.md +125 -0
  53. data/docs/architecture/execution-engine.md +25 -0
  54. data/docs/architecture/indexes.md +19 -0
  55. data/docs/architecture/mvcc.md +19 -0
  56. data/docs/architecture/overview.md +13 -0
  57. data/docs/architecture/pages.md +11 -0
  58. data/docs/architecture/production-roadmap.md +82 -0
  59. data/docs/architecture/query-planner.md +20 -0
  60. data/docs/architecture/recovery.md +18 -0
  61. data/docs/architecture/sql-engine.md +12 -0
  62. data/docs/architecture/storage-engine.md +14 -0
  63. data/docs/architecture/transactions.md +10 -0
  64. data/docs/architecture/wal.md +28 -0
  65. data/docs/cli-cheatsheet.md +98 -0
  66. data/docs/cli.md +275 -0
  67. data/docs/contributing/architecture.md +9 -0
  68. data/docs/contributing/benchmarking.md +14 -0
  69. data/docs/contributing/development.md +16 -0
  70. data/docs/contributing/release-process.md +49 -0
  71. data/docs/contributing/testing.md +16 -0
  72. data/docs/debugging.md +229 -0
  73. data/docs/developer/branching.md +10 -0
  74. data/docs/developer/database-diff.md +10 -0
  75. data/docs/developer/local-development.md +17 -0
  76. data/docs/developer/snapshots.md +9 -0
  77. data/docs/developer/temporal-data.md +10 -0
  78. data/docs/developer-guide.md +297 -0
  79. data/docs/getting-started/first-database.md +16 -0
  80. data/docs/getting-started/first-query.md +13 -0
  81. data/docs/getting-started/installation.md +19 -0
  82. data/docs/getting-started/local-to-production.md +300 -0
  83. data/docs/getting-started/quickstart.md +17 -0
  84. data/docs/getting-started/rails.md +16 -0
  85. data/docs/hardening_backlog.md +93 -0
  86. data/docs/lessons-learned.md +112 -0
  87. data/docs/operations/backups.md +33 -0
  88. data/docs/operations/disaster-recovery.md +31 -0
  89. data/docs/operations/failover.md +30 -0
  90. data/docs/operations/monitoring.md +25 -0
  91. data/docs/operations/production-guide.md +295 -0
  92. data/docs/operations/production-runbook.md +45 -0
  93. data/docs/operations/replication.md +33 -0
  94. data/docs/operations/restore.md +6 -0
  95. data/docs/operations/runbook.md +34 -0
  96. data/docs/operations/upgrades.md +14 -0
  97. data/docs/operations/workload-testing.md +17 -0
  98. data/docs/production-readiness.md +118 -0
  99. data/docs/production_validation.md +150 -0
  100. data/docs/rails/active-record.md +11 -0
  101. data/docs/rails/compatibility-guide.md +90 -0
  102. data/docs/rails/database-yml.md +92 -0
  103. data/docs/rails/installation.md +17 -0
  104. data/docs/rails/migrations.md +17 -0
  105. data/docs/rails/production.md +82 -0
  106. data/docs/rails/troubleshooting.md +18 -0
  107. data/docs/release.md +59 -0
  108. data/docs/server/architecture.md +10 -0
  109. data/docs/server/authentication.md +10 -0
  110. data/docs/server/configuration.md +16 -0
  111. data/docs/server/connection-pooling.md +10 -0
  112. data/docs/server/deployment.md +10 -0
  113. data/docs/server/protocol.md +12 -0
  114. data/docs/sql/compatibility-guide.md +82 -0
  115. data/docs/sql/compatibility.md +39 -0
  116. data/docs/sql/data-types.md +10 -0
  117. data/docs/sql/functions.md +9 -0
  118. data/docs/sql/joins.md +9 -0
  119. data/docs/sql/operators.md +9 -0
  120. data/docs/sql/sqlite-compatibility.md +21 -0
  121. data/docs/sql/syntax.md +10 -0
  122. data/docs/sql/transactions.md +10 -0
  123. data/docs/troubleshooting.md +244 -0
  124. data/exe/rubydb +23 -0
  125. data/lib/rubydb/backup/archive.rb +334 -0
  126. data/lib/rubydb/backup/backup.rb +401 -0
  127. data/lib/rubydb/backup/incremental.rb +353 -0
  128. data/lib/rubydb/backup/restore.rb +290 -0
  129. data/lib/rubydb/backup/snapshot.rb +267 -0
  130. data/lib/rubydb/backup/verification.rb +279 -0
  131. data/lib/rubydb/branching/branch.rb +181 -0
  132. data/lib/rubydb/branching/branch_manager.rb +311 -0
  133. data/lib/rubydb/branching/branch_metadata.rb +140 -0
  134. data/lib/rubydb/branching/checkout.rb +166 -0
  135. data/lib/rubydb/branching/copy_on_write.rb +272 -0
  136. data/lib/rubydb/branching/diff.rb +138 -0
  137. data/lib/rubydb/branching/merge.rb +285 -0
  138. data/lib/rubydb/build_info.rb +15 -0
  139. data/lib/rubydb/catalog/catalog.rb +391 -0
  140. data/lib/rubydb/catalog/column.rb +112 -0
  141. data/lib/rubydb/catalog/constraint.rb +180 -0
  142. data/lib/rubydb/catalog/database.rb +184 -0
  143. data/lib/rubydb/catalog/index.rb +97 -0
  144. data/lib/rubydb/catalog/schema.rb +103 -0
  145. data/lib/rubydb/catalog/sequence.rb +90 -0
  146. data/lib/rubydb/catalog/system_catalog.rb +698 -0
  147. data/lib/rubydb/catalog/table.rb +178 -0
  148. data/lib/rubydb/catalog/trigger.rb +102 -0
  149. data/lib/rubydb/catalog/view.rb +66 -0
  150. data/lib/rubydb/cli/application.rb +163 -0
  151. data/lib/rubydb/cli/commands/backup.rb +81 -0
  152. data/lib/rubydb/cli/commands/branch.rb +72 -0
  153. data/lib/rubydb/cli/commands/checkout.rb +54 -0
  154. data/lib/rubydb/cli/commands/create.rb +58 -0
  155. data/lib/rubydb/cli/commands/diff.rb +77 -0
  156. data/lib/rubydb/cli/commands/doctor.rb +74 -0
  157. data/lib/rubydb/cli/commands/drop.rb +57 -0
  158. data/lib/rubydb/cli/commands/init.rb +102 -0
  159. data/lib/rubydb/cli/commands/inspect.rb +95 -0
  160. data/lib/rubydb/cli/commands/merge.rb +63 -0
  161. data/lib/rubydb/cli/commands/migrate.rb +62 -0
  162. data/lib/rubydb/cli/commands/restart.rb +39 -0
  163. data/lib/rubydb/cli/commands/restore.rb +121 -0
  164. data/lib/rubydb/cli/commands/shell.rb +365 -0
  165. data/lib/rubydb/cli/commands/snapshot.rb +79 -0
  166. data/lib/rubydb/cli/commands/start.rb +82 -0
  167. data/lib/rubydb/cli/commands/status.rb +92 -0
  168. data/lib/rubydb/cli/commands/stop.rb +47 -0
  169. data/lib/rubydb/cli/commands/vacuum.rb +58 -0
  170. data/lib/rubydb/cli/formatter.rb +221 -0
  171. data/lib/rubydb/cli/output.rb +168 -0
  172. data/lib/rubydb/client/client.rb +304 -0
  173. data/lib/rubydb/client/connection.rb +415 -0
  174. data/lib/rubydb/client/connection_pool.rb +168 -0
  175. data/lib/rubydb/client/connection_url.rb +96 -0
  176. data/lib/rubydb/client/prepared_statement.rb +60 -0
  177. data/lib/rubydb/client/result.rb +123 -0
  178. data/lib/rubydb/client/statement.rb +52 -0
  179. data/lib/rubydb/client/transaction.rb +130 -0
  180. data/lib/rubydb/concurrency/concurrency.rb +19 -0
  181. data/lib/rubydb/concurrency/deadlock_detector.rb +150 -0
  182. data/lib/rubydb/concurrency/latch.rb +101 -0
  183. data/lib/rubydb/concurrency/lock_graph.rb +165 -0
  184. data/lib/rubydb/concurrency/mutex.rb +183 -0
  185. data/lib/rubydb/concurrency/rw_lock.rb +180 -0
  186. data/lib/rubydb/concurrency/scheduler.rb +250 -0
  187. data/lib/rubydb/concurrency/worker_pool.rb +143 -0
  188. data/lib/rubydb/configuration/config.rb +170 -0
  189. data/lib/rubydb/configuration/defaults.rb +179 -0
  190. data/lib/rubydb/configuration/environment.rb +152 -0
  191. data/lib/rubydb/configuration/parser.rb +185 -0
  192. data/lib/rubydb/configuration/validation.rb +221 -0
  193. data/lib/rubydb/constants.rb +74 -0
  194. data/lib/rubydb/constraints/check.rb +181 -0
  195. data/lib/rubydb/constraints/constraint.rb +101 -0
  196. data/lib/rubydb/constraints/foreign_key.rb +130 -0
  197. data/lib/rubydb/constraints/not_null.rb +64 -0
  198. data/lib/rubydb/constraints/primary_key.rb +99 -0
  199. data/lib/rubydb/constraints/unique.rb +108 -0
  200. data/lib/rubydb/constraints/validator.rb +350 -0
  201. data/lib/rubydb/errors/authentication_error.rb +10 -0
  202. data/lib/rubydb/errors/authorization_error.rb +23 -0
  203. data/lib/rubydb/errors/client_error.rb +10 -0
  204. data/lib/rubydb/errors/configuration_error.rb +10 -0
  205. data/lib/rubydb/errors/connection_error.rb +10 -0
  206. data/lib/rubydb/errors/constraint_error.rb +23 -0
  207. data/lib/rubydb/errors/corruption_error.rb +10 -0
  208. data/lib/rubydb/errors/database_error.rb +10 -0
  209. data/lib/rubydb/errors/error.rb +20 -0
  210. data/lib/rubydb/errors/execution_error.rb +10 -0
  211. data/lib/rubydb/errors/parser_error.rb +10 -0
  212. data/lib/rubydb/errors/recovery_error.rb +10 -0
  213. data/lib/rubydb/errors/replication_error.rb +10 -0
  214. data/lib/rubydb/errors/server_error.rb +6 -0
  215. data/lib/rubydb/errors/storage_error.rb +10 -0
  216. data/lib/rubydb/errors/transaction_error.rb +10 -0
  217. data/lib/rubydb/execution/aggregate_executor.rb +138 -0
  218. data/lib/rubydb/execution/delete_executor.rb +112 -0
  219. data/lib/rubydb/execution/distinct_executor.rb +135 -0
  220. data/lib/rubydb/execution/executor.rb +1192 -0
  221. data/lib/rubydb/execution/expression.rb +193 -0
  222. data/lib/rubydb/execution/index_scan.rb +142 -0
  223. data/lib/rubydb/execution/insert_executor.rb +217 -0
  224. data/lib/rubydb/execution/join_executor.rb +249 -0
  225. data/lib/rubydb/execution/limit_executor.rb +85 -0
  226. data/lib/rubydb/execution/optimizer.rb +215 -0
  227. data/lib/rubydb/execution/plan.rb +361 -0
  228. data/lib/rubydb/execution/planner.rb +546 -0
  229. data/lib/rubydb/execution/predicate.rb +235 -0
  230. data/lib/rubydb/execution/scan.rb +49 -0
  231. data/lib/rubydb/execution/sequential_scan.rb +63 -0
  232. data/lib/rubydb/execution/sort_executor.rb +185 -0
  233. data/lib/rubydb/execution/update_executor.rb +162 -0
  234. data/lib/rubydb/functions/aggregate.rb +70 -0
  235. data/lib/rubydb/functions/date_functions.rb +278 -0
  236. data/lib/rubydb/functions/function.rb +85 -0
  237. data/lib/rubydb/functions/json_functions.rb +215 -0
  238. data/lib/rubydb/functions/numeric_functions.rb +346 -0
  239. data/lib/rubydb/functions/scalar.rb +52 -0
  240. data/lib/rubydb/functions/string_functions.rb +383 -0
  241. data/lib/rubydb/functions/system_functions.rb +246 -0
  242. data/lib/rubydb/history/as_of.rb +238 -0
  243. data/lib/rubydb/history/change.rb +105 -0
  244. data/lib/rubydb/history/history.rb +131 -0
  245. data/lib/rubydb/history/history_manager.rb +229 -0
  246. data/lib/rubydb/history/temporal_query.rb +202 -0
  247. data/lib/rubydb/history/timeline.rb +144 -0
  248. data/lib/rubydb/indexes/btree.rb +186 -0
  249. data/lib/rubydb/indexes/btree_cursor.rb +258 -0
  250. data/lib/rubydb/indexes/btree_node.rb +385 -0
  251. data/lib/rubydb/indexes/hash_index.rb +150 -0
  252. data/lib/rubydb/indexes/index.rb +71 -0
  253. data/lib/rubydb/indexes/index_manager.rb +406 -0
  254. data/lib/rubydb/indexes/index_scan.rb +470 -0
  255. data/lib/rubydb/migrations/migration.rb +254 -0
  256. data/lib/rubydb/migrations/migration_lock.rb +146 -0
  257. data/lib/rubydb/migrations/migration_manager.rb +176 -0
  258. data/lib/rubydb/migrations/migration_version.rb +71 -0
  259. data/lib/rubydb/migrations/schema_diff.rb +211 -0
  260. data/lib/rubydb/migrations/schema_version.rb +64 -0
  261. data/lib/rubydb/monitoring/events.rb +160 -0
  262. data/lib/rubydb/monitoring/health.rb +222 -0
  263. data/lib/rubydb/monitoring/logger.rb +193 -0
  264. data/lib/rubydb/monitoring/metrics.rb +359 -0
  265. data/lib/rubydb/monitoring/performance.rb +176 -0
  266. data/lib/rubydb/monitoring/statistics.rb +170 -0
  267. data/lib/rubydb/mvcc/garbage_collector.rb +199 -0
  268. data/lib/rubydb/mvcc/mvcc.rb +16 -0
  269. data/lib/rubydb/mvcc/snapshot.rb +147 -0
  270. data/lib/rubydb/mvcc/vacuum.rb +180 -0
  271. data/lib/rubydb/mvcc/version.rb +106 -0
  272. data/lib/rubydb/mvcc/version_store.rb +398 -0
  273. data/lib/rubydb/mvcc/visibility.rb +109 -0
  274. data/lib/rubydb/protocol/capabilities.rb +125 -0
  275. data/lib/rubydb/protocol/decoder.rb +145 -0
  276. data/lib/rubydb/protocol/encoder.rb +136 -0
  277. data/lib/rubydb/protocol/handshake.rb +305 -0
  278. data/lib/rubydb/protocol/message.rb +121 -0
  279. data/lib/rubydb/protocol/protocol.rb +277 -0
  280. data/lib/rubydb/protocol/version.rb +54 -0
  281. data/lib/rubydb/rails/adapter.rb +239 -0
  282. data/lib/rubydb/rails/connection.rb +314 -0
  283. data/lib/rubydb/rails/database_statements.rb +122 -0
  284. data/lib/rubydb/rails/migration.rb +131 -0
  285. data/lib/rubydb/rails/quoting.rb +109 -0
  286. data/lib/rubydb/rails/result.rb +120 -0
  287. data/lib/rubydb/rails/schema_statements.rb +339 -0
  288. data/lib/rubydb/rails/transaction.rb +105 -0
  289. data/lib/rubydb/rails/type.rb +126 -0
  290. data/lib/rubydb/recovery/checkpoint.rb +257 -0
  291. data/lib/rubydb/recovery/consistency.rb +467 -0
  292. data/lib/rubydb/recovery/corruption_detector.rb +5 -0
  293. data/lib/rubydb/recovery/crash_recovery.rb +387 -0
  294. data/lib/rubydb/recovery/recovery_manager.rb +206 -0
  295. data/lib/rubydb/recovery/redo.rb +237 -0
  296. data/lib/rubydb/recovery/undo.rb +206 -0
  297. data/lib/rubydb/replication/failover.rb +5 -0
  298. data/lib/rubydb/replication/fencing.rb +63 -0
  299. data/lib/rubydb/replication/primary.rb +450 -0
  300. data/lib/rubydb/replication/replica.rb +384 -0
  301. data/lib/rubydb/replication/replication_log.rb +200 -0
  302. data/lib/rubydb/replication/replication_manager.rb +308 -0
  303. data/lib/rubydb/replication/replication_slot.rb +295 -0
  304. data/lib/rubydb/replication/replication_stream.rb +201 -0
  305. data/lib/rubydb/rubydb.rb +560 -0
  306. data/lib/rubydb/security/access_control.rb +254 -0
  307. data/lib/rubydb/security/audit_log.rb +213 -0
  308. data/lib/rubydb/security/authentication.rb +302 -0
  309. data/lib/rubydb/security/authorization.rb +282 -0
  310. data/lib/rubydb/security/credentials.rb +196 -0
  311. data/lib/rubydb/security/password.rb +215 -0
  312. data/lib/rubydb/security/permissions.rb +74 -0
  313. data/lib/rubydb/security/role.rb +101 -0
  314. data/lib/rubydb/security/user.rb +86 -0
  315. data/lib/rubydb/server/connection.rb +366 -0
  316. data/lib/rubydb/server/connection_pool.rb +193 -0
  317. data/lib/rubydb/server/lifecycle.rb +228 -0
  318. data/lib/rubydb/server/listener.rb +136 -0
  319. data/lib/rubydb/server/request_handler.rb +276 -0
  320. data/lib/rubydb/server/server.rb +364 -0
  321. data/lib/rubydb/server/session.rb +369 -0
  322. data/lib/rubydb/server/worker.rb +210 -0
  323. data/lib/rubydb/server/worker_pool.rb +168 -0
  324. data/lib/rubydb/sql/ast/alter_table.rb +169 -0
  325. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -0
  326. data/lib/rubydb/sql/ast/commit.rb +37 -0
  327. data/lib/rubydb/sql/ast/constraint.rb +83 -0
  328. data/lib/rubydb/sql/ast/create_database.rb +41 -0
  329. data/lib/rubydb/sql/ast/create_index.rb +61 -0
  330. data/lib/rubydb/sql/ast/create_schema.rb +52 -0
  331. data/lib/rubydb/sql/ast/create_table.rb +187 -0
  332. data/lib/rubydb/sql/ast/delete.rb +54 -0
  333. data/lib/rubydb/sql/ast/drop_database.rb +41 -0
  334. data/lib/rubydb/sql/ast/drop_index.rb +41 -0
  335. data/lib/rubydb/sql/ast/drop_schema.rb +49 -0
  336. data/lib/rubydb/sql/ast/drop_table.rb +49 -0
  337. data/lib/rubydb/sql/ast/explain.rb +64 -0
  338. data/lib/rubydb/sql/ast/expression.rb +604 -0
  339. data/lib/rubydb/sql/ast/insert.rb +82 -0
  340. data/lib/rubydb/sql/ast/node.rb +42 -0
  341. data/lib/rubydb/sql/ast/rollback.rb +63 -0
  342. data/lib/rubydb/sql/ast/savepoint.rb +59 -0
  343. data/lib/rubydb/sql/ast/select.rb +88 -0
  344. data/lib/rubydb/sql/ast/set_operation.rb +20 -0
  345. data/lib/rubydb/sql/ast/trigger.rb +29 -0
  346. data/lib/rubydb/sql/ast/update.rb +88 -0
  347. data/lib/rubydb/sql/ast/vacuum.rb +19 -0
  348. data/lib/rubydb/sql/ast/view.rb +32 -0
  349. data/lib/rubydb/sql/ast/with.rb +32 -0
  350. data/lib/rubydb/sql/grammar.rb +86 -0
  351. data/lib/rubydb/sql/keywords.rb +156 -0
  352. data/lib/rubydb/sql/lexer.rb +214 -0
  353. data/lib/rubydb/sql/operators.rb +100 -0
  354. data/lib/rubydb/sql/parser.rb +1185 -0
  355. data/lib/rubydb/sql/planner/analyzer.rb +302 -0
  356. data/lib/rubydb/sql/planner/binder.rb +550 -0
  357. data/lib/rubydb/sql/planner/type_checker.rb +431 -0
  358. data/lib/rubydb/sql/token.rb +210 -0
  359. data/lib/rubydb/storage/buffer_frame.rb +44 -0
  360. data/lib/rubydb/storage/buffer_pool.rb +155 -0
  361. data/lib/rubydb/storage/database_lock.rb +74 -0
  362. data/lib/rubydb/storage/deserializer.rb +342 -0
  363. data/lib/rubydb/storage/engine.rb +2338 -0
  364. data/lib/rubydb/storage/file_manager.rb +187 -0
  365. data/lib/rubydb/storage/free_space_map.rb +81 -0
  366. data/lib/rubydb/storage/page.rb +94 -0
  367. data/lib/rubydb/storage/page_allocator.rb +855 -0
  368. data/lib/rubydb/storage/page_header.rb +67 -0
  369. data/lib/rubydb/storage/page_manager.rb +131 -0
  370. data/lib/rubydb/storage/record.rb +58 -0
  371. data/lib/rubydb/storage/row.rb +78 -0
  372. data/lib/rubydb/storage/serializer.rb +51 -0
  373. data/lib/rubydb/storage/storage_layout.rb +151 -0
  374. data/lib/rubydb/storage/storage_manager.rb +114 -0
  375. data/lib/rubydb/storage/tuple.rb +461 -0
  376. data/lib/rubydb/storage/visibility_map.rb +984 -0
  377. data/lib/rubydb/transactions/commit_manager.rb +220 -0
  378. data/lib/rubydb/transactions/isolation.rb +98 -0
  379. data/lib/rubydb/transactions/lock.rb +76 -0
  380. data/lib/rubydb/transactions/lock_manager.rb +362 -0
  381. data/lib/rubydb/transactions/savepoint.rb +143 -0
  382. data/lib/rubydb/transactions/transaction.rb +215 -0
  383. data/lib/rubydb/transactions/transaction_id.rb +84 -0
  384. data/lib/rubydb/transactions/transaction_log.rb +257 -0
  385. data/lib/rubydb/transactions/transaction_manager.rb +435 -0
  386. data/lib/rubydb/types/bigint.rb +36 -0
  387. data/lib/rubydb/types/blob.rb +37 -0
  388. data/lib/rubydb/types/boolean.rb +34 -0
  389. data/lib/rubydb/types/date.rb +39 -0
  390. data/lib/rubydb/types/decimal.rb +48 -0
  391. data/lib/rubydb/types/float.rb +34 -0
  392. data/lib/rubydb/types/integer.rb +36 -0
  393. data/lib/rubydb/types/json.rb +41 -0
  394. data/lib/rubydb/types/null.rb +34 -0
  395. data/lib/rubydb/types/smallint.rb +36 -0
  396. data/lib/rubydb/types/text.rb +37 -0
  397. data/lib/rubydb/types/time.rb +46 -0
  398. data/lib/rubydb/types/timestamp.rb +39 -0
  399. data/lib/rubydb/types/type.rb +119 -0
  400. data/lib/rubydb/types/uuid.rb +47 -0
  401. data/lib/rubydb/types/varchar.rb +37 -0
  402. data/lib/rubydb/version.rb +33 -0
  403. data/lib/rubydb/wal/archive.rb +193 -0
  404. data/lib/rubydb/wal/checkpoint.rb +183 -0
  405. data/lib/rubydb/wal/lsn.rb +94 -0
  406. data/lib/rubydb/wal/reader.rb +260 -0
  407. data/lib/rubydb/wal/record.rb +105 -0
  408. data/lib/rubydb/wal/segment.rb +193 -0
  409. data/lib/rubydb/wal/wal.rb +452 -0
  410. data/lib/rubydb/wal/writer.rb +236 -0
  411. data/lib/rubydb.rb +7 -0
  412. data/packaging/docker/Dockerfile +36 -0
  413. data/packaging/docker/docker-compose.failover.yml +43 -0
  414. data/packaging/docker/entrypoint.sh +15 -0
  415. data/packaging/homebrew/rubydb.rb +19 -0
  416. data/packaging/systemd/rubydb.service +22 -0
  417. data/rubydb.gemspec +57 -0
  418. data/scripts/benchmark +7 -0
  419. data/scripts/durability_drill +37 -0
  420. data/scripts/format +0 -0
  421. data/scripts/fuzz +63 -0
  422. data/scripts/lint +0 -0
  423. data/scripts/release +50 -0
  424. data/scripts/release_check +43 -0
  425. data/scripts/replication_failover_drill +250 -0
  426. data/scripts/replication_network_failover_drill +255 -0
  427. data/scripts/restore_drill +45 -0
  428. data/scripts/security +0 -0
  429. data/scripts/setup +0 -0
  430. data/scripts/test +0 -0
  431. data/scripts/test-chaos +0 -0
  432. data/scripts/test-crash +0 -0
  433. data/scripts/test-integration +0 -0
  434. data/scripts/test-unit +0 -0
  435. data/visibility_map.json +1 -0
  436. metadata +538 -0
@@ -0,0 +1,855 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module RubyDB
6
+ module Storage
7
+ # PageAllocator - Handles page allocation with free space management
8
+ # This is a production-grade implementation with proper fragmentation management,
9
+ # overflow page handling, and comprehensive statistics tracking.
10
+ class PageAllocator
11
+ attr_reader :page_manager, :free_space_map, :stats
12
+
13
+ def initialize(page_manager, buffer_pool: nil)
14
+ @page_manager = page_manager
15
+ @buffer_pool = buffer_pool
16
+ @free_space_map = FreeSpaceMap.new(page_manager)
17
+ @lock = Mutex.new
18
+ @stats = {
19
+ allocations: 0,
20
+ releases: 0,
21
+ compactions: 0,
22
+ defragmentations: 0,
23
+ allocation_failures: 0,
24
+ total_space_requested: 0,
25
+ total_space_allocated: 0,
26
+ total_space_released: 0,
27
+ overflow_allocations: 0,
28
+ overflow_releases: 0
29
+ }
30
+ @overflow_pages = {}
31
+ @page_usage = {}
32
+ @page_records_cache = {}
33
+ @cache_hits = 0
34
+ @cache_misses = 0
35
+ @initialized = true
36
+ @max_cache_size = 1000
37
+ @defragmentation_threshold = 0.30 # 30% fragmentation triggers defrag
38
+ end
39
+
40
+ # Allocate a page with at least needed_bytes of space
41
+ def allocate_page_with_space(needed_bytes, page_type = 0)
42
+ @lock.synchronize do
43
+ @stats[:allocations] += 1
44
+ @stats[:total_space_requested] += needed_bytes
45
+
46
+ page_size = @page_manager.file_manager.page_size
47
+ max_record_size = page_size - PageHeader::SIZE - 8 # 8 bytes for record header
48
+
49
+ # Validate request
50
+ if needed_bytes > max_record_size
51
+ @stats[:allocation_failures] += 1
52
+ raise StorageError, "Requested space #{needed_bytes} exceeds maximum record size #{max_record_size}"
53
+ end
54
+
55
+ if needed_bytes <= 0
56
+ @stats[:allocation_failures] += 1
57
+ raise StorageError, "Requested space must be positive"
58
+ end
59
+
60
+ page_number = nil
61
+ page = nil
62
+
63
+ # Try to find existing page with enough free space (best fit)
64
+ candidate_page = @free_space_map.find_page_with_space(needed_bytes)
65
+
66
+ if candidate_page
67
+ page = get_page(candidate_page)
68
+
69
+ # Verify page is valid and has space
70
+ if page && page.header.page_type != 3 && page.free_space >= needed_bytes
71
+ page_number = candidate_page
72
+ else
73
+ # Free space map is stale - update it
74
+ @free_space_map.update_page(candidate_page, page ? page.free_space : 0)
75
+ # Try another search
76
+ page_number = @free_space_map.find_page_with_space(needed_bytes)
77
+ page = get_page(page_number) if page_number
78
+ end
79
+ end
80
+
81
+ unless page_number && page
82
+ # Allocate new page
83
+ page_number = @page_manager.allocate_page(page_type)
84
+
85
+ # Initialize page
86
+ page = Page.new(page_number, page_size)
87
+ page.header.page_type = page_type
88
+ page.header.data_end = PageHeader::SIZE
89
+ page.write_header
90
+
91
+ write_page(page)
92
+ @free_space_map.update_page(page_number, page.free_space)
93
+
94
+ # Clear any cached records for this page
95
+ @page_records_cache.delete(page_number)
96
+ end
97
+
98
+ # Get the page and reserve space
99
+ page = get_page(page_number)
100
+
101
+ # Calculate new data end position
102
+ new_data_end = page.header.data_end + needed_bytes
103
+
104
+ # Double-check we have enough space
105
+ if new_data_end > page_size
106
+ @stats[:allocation_failures] += 1
107
+ raise StorageError, "Page #{page_number} doesn't have enough space (needed: #{needed_bytes}, available: #{page.free_space}, total: #{page_size - new_data_end})"
108
+ end
109
+
110
+ # Update page header
111
+ page.header.data_end = new_data_end
112
+ page.write_header
113
+
114
+ # Write the updated page
115
+ write_page(page)
116
+
117
+ # Update free space map
118
+ @free_space_map.update_page(page_number, page.free_space)
119
+
120
+ # Track page usage
121
+ @page_usage[page_number] ||= {
122
+ allocated: 0,
123
+ freed: 0,
124
+ record_count: 0,
125
+ created_at: Time.now,
126
+ last_modified: Time.now,
127
+ total_writes: 0
128
+ }
129
+ @page_usage[page_number][:allocated] += needed_bytes
130
+ @page_usage[page_number][:record_count] += 1
131
+ @page_usage[page_number][:last_modified] = Time.now
132
+ @page_usage[page_number][:total_writes] += 1
133
+
134
+ @stats[:total_space_allocated] += needed_bytes
135
+
136
+ # Clear cache for this page
137
+ @page_records_cache.delete(page_number)
138
+
139
+ # Return the offset where data can be written
140
+ offset = new_data_end - needed_bytes
141
+
142
+ {
143
+ page_number: page_number,
144
+ offset: offset,
145
+ page: page,
146
+ free_space_remaining: page.free_space,
147
+ page_usage: @page_usage[page_number]
148
+ }
149
+ end
150
+ end
151
+
152
+ # Release a page back to the free pool
153
+ def release_page(page_number)
154
+ @lock.synchronize do
155
+ @stats[:releases] += 1
156
+
157
+ # Validate page number
158
+ if page_number < 0 || page_number >= @page_manager.total_pages
159
+ raise StorageError, "Invalid page number: #{page_number}"
160
+ end
161
+
162
+ # Get page before freeing
163
+ page = get_page(page_number)
164
+ raise StorageError, "Page #{page_number} could not be read" unless page
165
+
166
+ if page
167
+ # Record usage stats before freeing
168
+ usage = @page_usage[page_number]
169
+ if usage
170
+ @stats[:total_space_allocated] -= usage[:allocated]
171
+ @stats[:total_space_released] += usage[:allocated]
172
+ end
173
+ end
174
+
175
+ # Release any overflow pages first
176
+ released_overflow = release_overflow_pages(page_number)
177
+ if released_overflow > 0
178
+ @stats[:overflow_releases] += released_overflow
179
+ end
180
+
181
+ # Free the page
182
+ @page_manager.free_page(page_number)
183
+ @free_space_map.remove_page(page_number)
184
+ @page_usage.delete(page_number)
185
+ @page_records_cache.delete(page_number)
186
+
187
+ true
188
+ end
189
+ end
190
+
191
+ # Get a page and verify it has enough space
192
+ def get_page_with_space(page_number, needed_bytes)
193
+ @lock.synchronize do
194
+ # Validate page exists
195
+ unless page_number < @page_manager.total_pages
196
+ raise StorageError, "Page #{page_number} does not exist"
197
+ end
198
+
199
+ page = get_page(page_number)
200
+
201
+ # Check if page is free
202
+ if page.header.page_type == 3
203
+ raise StorageError, "Page #{page_number} is marked as free"
204
+ end
205
+
206
+ # Check if page has enough free space
207
+ if page.free_space < needed_bytes
208
+ # Try to compact the page first
209
+ compact_result = compact_page(page_number)
210
+
211
+ # Re-check after compaction
212
+ page = get_page(page_number)
213
+
214
+ if page.free_space < needed_bytes
215
+ # Check if we can allocate an overflow page
216
+ needed_overflow = needed_bytes - page.free_space
217
+ overflow_page = allocate_overflow_page(page_number, needed_overflow)
218
+
219
+ if overflow_page
220
+ @stats[:overflow_allocations] += 1
221
+ return {
222
+ page_number: overflow_page.page_number,
223
+ page: overflow_page,
224
+ overflow: true,
225
+ parent_page: page_number
226
+ }
227
+ end
228
+
229
+ raise StorageError, "Not enough space on page #{page_number} (needed: #{needed_bytes}, available: #{page.free_space}, freed: #{compact_result[:space_reclaimed]})"
230
+ end
231
+ end
232
+
233
+ { page_number: page_number, page: page, overflow: false }
234
+ end
235
+ end
236
+
237
+ # Compact a page to remove holes and fragmentation
238
+ def compact_page(page_number)
239
+ @lock.synchronize do
240
+ @stats[:compactions] += 1
241
+
242
+ # Validate page
243
+ unless page_number < @page_manager.total_pages
244
+ raise StorageError, "Page #{page_number} does not exist"
245
+ end
246
+
247
+ page = get_page(page_number)
248
+
249
+ # Skip if page is free
250
+ if page.header.page_type == 3
251
+ return { page_number: page_number, compacted: false, reason: "page is free" }
252
+ end
253
+
254
+ header_size = PageHeader::SIZE
255
+ old_data_end = page.header.data_end
256
+
257
+ # Get all records on the page with their actual data
258
+ records = read_page_records(page)
259
+
260
+ if records.empty?
261
+ # No records to compact
262
+ return {
263
+ page_number: page_number,
264
+ compacted: false,
265
+ reason: "no records",
266
+ space_reclaimed: 0,
267
+ records_compacted: 0
268
+ }
269
+ end
270
+
271
+ # Sort records by offset to compact them
272
+ records.sort_by! { |r| r[:offset] }
273
+
274
+ # Check if compaction is needed (check for gaps)
275
+ needs_compaction = false
276
+ current_offset = header_size
277
+
278
+ records.each do |record|
279
+ if record[:offset] > current_offset
280
+ needs_compaction = true
281
+ break
282
+ end
283
+ current_offset += record[:length] + record[:header_size]
284
+ end
285
+
286
+ unless needs_compaction
287
+ return {
288
+ page_number: page_number,
289
+ compacted: false,
290
+ reason: "no fragmentation",
291
+ space_reclaimed: 0,
292
+ records_compacted: records.size
293
+ }
294
+ end
295
+
296
+ # Rewrite records contiguously
297
+ current_offset = header_size
298
+ compacted_records = []
299
+
300
+ records.each do |record|
301
+ # Read the full record data including header
302
+ record_data = page.read(record[:offset], record[:header_size] + record[:length])
303
+
304
+ # Write to new position
305
+ page.write(current_offset, record_data)
306
+
307
+ # Store mapping for potential updates
308
+ compacted_records << {
309
+ old_offset: record[:offset],
310
+ new_offset: current_offset,
311
+ length: record[:length],
312
+ header_size: record[:header_size]
313
+ }
314
+
315
+ current_offset += record[:header_size] + record[:length]
316
+ end
317
+
318
+ # Update data_end to the new end position
319
+ new_data_end = current_offset
320
+ page.header.data_end = new_data_end
321
+ page.write_header
322
+
323
+ write_page(page)
324
+
325
+ # Update free space map
326
+ @free_space_map.update_page(page_number, page.free_space)
327
+
328
+ # Clear cache for this page
329
+ @page_records_cache.delete(page_number)
330
+
331
+ # Track compaction in page usage
332
+ if @page_usage[page_number]
333
+ @page_usage[page_number][:compactions] = (@page_usage[page_number][:compactions] || 0) + 1
334
+ @page_usage[page_number][:space_reclaimed] = (@page_usage[page_number][:space_reclaimed] || 0) + (old_data_end - new_data_end)
335
+ @page_usage[page_number][:last_compact] = Time.now
336
+ end
337
+
338
+ # Return compaction results
339
+ {
340
+ page_number: page_number,
341
+ compacted: true,
342
+ old_end: old_data_end,
343
+ new_end: new_data_end,
344
+ records_compacted: records.size,
345
+ space_reclaimed: old_data_end - new_data_end,
346
+ compacted_records: compacted_records
347
+ }
348
+ end
349
+ end
350
+
351
+ # Allocate an overflow page for large records
352
+ def allocate_overflow_page(parent_page_number, needed_bytes)
353
+ @lock.synchronize do
354
+ page_size = @page_manager.file_manager.page_size
355
+
356
+ # Validate request
357
+ if needed_bytes > page_size - PageHeader::SIZE - 8
358
+ raise StorageError, "Overflow request #{needed_bytes} exceeds page capacity"
359
+ end
360
+
361
+ # Allocate new page for overflow
362
+ overflow_page_number = @page_manager.allocate_page(4) # PAGE_TYPE_OVERFLOW
363
+
364
+ # Initialize overflow page
365
+ overflow_page = Page.new(overflow_page_number, page_size)
366
+ overflow_page.header.page_type = 4 # PAGE_TYPE_OVERFLOW
367
+ overflow_page.header.data_end = PageHeader::SIZE + needed_bytes
368
+ overflow_page.header.next_page = 0
369
+ overflow_page.header.prev_page = parent_page_number
370
+ overflow_page.write_header
371
+
372
+ write_page(overflow_page)
373
+
374
+ # Link overflow page to parent
375
+ @overflow_pages[parent_page_number] ||= []
376
+ @overflow_pages[parent_page_number] << overflow_page_number
377
+
378
+ # Track overflow page usage
379
+ @page_usage[overflow_page_number] = {
380
+ allocated: needed_bytes,
381
+ freed: 0,
382
+ record_count: 0,
383
+ created_at: Time.now,
384
+ last_modified: Time.now,
385
+ total_writes: 1,
386
+ is_overflow: true,
387
+ parent_page: parent_page_number
388
+ }
389
+
390
+ # Update free space map
391
+ @free_space_map.update_page(overflow_page_number, overflow_page.free_space)
392
+
393
+
394
+ overflow_page
395
+ end
396
+ end
397
+
398
+ # Get overflow pages for a page
399
+ def get_overflow_pages(page_number)
400
+ @lock.synchronize do
401
+ @overflow_pages[page_number] || []
402
+ end
403
+ end
404
+
405
+ # Get all overflow page information
406
+ def all_overflow_info
407
+ @lock.synchronize do
408
+ result = {}
409
+ @overflow_pages.each do |parent, children|
410
+ result[parent] = children.map do |child|
411
+ page = get_page(child) rescue nil
412
+ if page
413
+ {
414
+ page_number: child,
415
+ size: page.header.data_end - PageHeader::SIZE,
416
+ prev_page: page.header.prev_page,
417
+ next_page: page.header.next_page
418
+ }
419
+ else
420
+ { page_number: child, valid: false }
421
+ end
422
+ end
423
+ end
424
+ result
425
+ end
426
+ end
427
+
428
+ # Release all overflow pages for a page
429
+ def release_overflow_pages(page_number)
430
+ @lock.synchronize do
431
+ overflow_pages = @overflow_pages[page_number] || []
432
+
433
+ released = 0
434
+ errors = []
435
+ overflow_pages.each do |overflow_page_number|
436
+ begin
437
+ @page_manager.free_page(overflow_page_number)
438
+ @free_space_map.remove_page(overflow_page_number)
439
+ @page_usage.delete(overflow_page_number)
440
+ @page_records_cache.delete(overflow_page_number)
441
+ released += 1
442
+ rescue => e
443
+ errors << [overflow_page_number, e]
444
+ end
445
+ end
446
+
447
+ unless errors.empty?
448
+ details = errors.map { |page_number, error| "#{page_number}: #{error.message}" }.join(", ")
449
+ raise StorageError, "Failed to release overflow pages (#{details})"
450
+ end
451
+
452
+ @overflow_pages.delete(page_number)
453
+
454
+ released
455
+ end
456
+ end
457
+
458
+ # Get total free space across all pages
459
+ def total_free_space
460
+ @free_space_map.total_free_space
461
+ end
462
+
463
+ # Get free space on a specific page
464
+ def page_free_space(page_number)
465
+ @free_space_map.get_free_space(page_number)
466
+ end
467
+
468
+ # Get page usage statistics
469
+ def page_usage(page_number)
470
+ @page_usage[page_number] || { allocated: 0, freed: 0, record_count: 0 }
471
+ end
472
+
473
+ # Get all page usage statistics
474
+ def all_page_usage
475
+ @page_usage.dup
476
+ end
477
+
478
+ # Calculate fragmentation percentage for a page
479
+ def page_fragmentation(page_number)
480
+ page = get_page(page_number) rescue nil
481
+ return 0.0 unless page
482
+
483
+ # Skip if page is free
484
+ return 0.0 if page.header.page_type == 3
485
+
486
+ # Get all records on the page
487
+ records = read_page_records(page)
488
+ return 0.0 if records.empty?
489
+
490
+ # Calculate total record data size including headers
491
+ total_data = records.sum { |r| r[:header_size] + r[:length] }
492
+
493
+ # Calculate fragmentation (holes between records)
494
+ header_size = PageHeader::SIZE
495
+ sorted_records = records.sort_by { |r| r[:offset] }
496
+
497
+ fragmentation = 0.0
498
+ last_end = header_size
499
+
500
+ sorted_records.each do |record|
501
+ if record[:offset] > last_end
502
+ fragmentation += record[:offset] - last_end
503
+ end
504
+ last_end = record[:offset] + record[:header_size] + record[:length]
505
+ end
506
+
507
+ # Add space after last record
508
+ if last_end < page.header.data_end
509
+ fragmentation += page.header.data_end - last_end
510
+ end
511
+
512
+ total_used = page.header.data_end - header_size
513
+ return 0.0 if total_used == 0
514
+
515
+ (fragmentation.to_f / total_used * 100).round(2)
516
+ end
517
+
518
+ # Defragment a page (compact and reorganize)
519
+ def defragment_page(page_number)
520
+ @lock.synchronize do
521
+ @stats[:defragmentations] += 1
522
+
523
+ # Compact the page
524
+ compaction_result = compact_page(page_number)
525
+
526
+ # Update page usage
527
+ if @page_usage[page_number]
528
+ @page_usage[page_number][:defragmented_at] = Time.now
529
+ @page_usage[page_number][:defragment_count] = (@page_usage[page_number][:defragment_count] || 0) + 1
530
+ end
531
+
532
+ {
533
+ page_number: page_number,
534
+ compacted: compaction_result[:compacted],
535
+ space_reclaimed: compaction_result[:space_reclaimed],
536
+ freed_overflow_pages: 0,
537
+ fragmentation_before: compaction_result[:fragmentation_before] || 0,
538
+ fragmentation_after: page_fragmentation(page_number)
539
+ }
540
+ end
541
+ end
542
+
543
+ # Reserve space on a page for writing
544
+ def reserve_space(page_number, needed_bytes)
545
+ @lock.synchronize do
546
+ # Validate page
547
+ unless page_number < @page_manager.total_pages
548
+ raise StorageError, "Page #{page_number} does not exist"
549
+ end
550
+
551
+ page = get_page(page_number)
552
+
553
+ # Skip if page is free
554
+ if page.header.page_type == 3
555
+ raise StorageError, "Cannot reserve space on free page #{page_number}"
556
+ end
557
+
558
+ # Check if we need to compact or allocate overflow
559
+ if page.free_space < needed_bytes
560
+ # Try compacting first
561
+ compact_result = compact_page(page_number)
562
+ page = get_page(page_number)
563
+
564
+ if page.free_space < needed_bytes
565
+ # Try allocating overflow
566
+ overflow_page = allocate_overflow_page(page_number, needed_bytes - page.free_space)
567
+ if overflow_page
568
+ return {
569
+ page_number: overflow_page.page_number,
570
+ offset: PageHeader::SIZE,
571
+ page: overflow_page,
572
+ is_overflow: true
573
+ }
574
+ end
575
+
576
+ raise StorageError, "Cannot reserve #{needed_bytes} bytes on page #{page_number} (available: #{page.free_space})"
577
+ end
578
+ end
579
+
580
+ # Reserve space on the page
581
+ offset = page.header.data_end
582
+ page.header.data_end += needed_bytes
583
+ page.write_header
584
+ write_page(page)
585
+
586
+ # Update free space map
587
+ @free_space_map.update_page(page_number, page.free_space)
588
+
589
+ # Update page usage
590
+ if @page_usage[page_number]
591
+ @page_usage[page_number][:reserved] = (@page_usage[page_number][:reserved] || 0) + needed_bytes
592
+ @page_usage[page_number][:last_modified] = Time.now
593
+ end
594
+
595
+ # Clear cache
596
+ @page_records_cache.delete(page_number)
597
+
598
+ { page_number: page_number, offset: offset, page: page, is_overflow: false }
599
+ end
600
+ end
601
+
602
+ # Release reserved space on a page
603
+ def release_reserved_space(page_number, offset, length)
604
+ @lock.synchronize do
605
+ # Validate page
606
+ unless page_number < @page_manager.total_pages
607
+ return false
608
+ end
609
+
610
+ page = get_page(page_number)
611
+
612
+ # Skip if page is free
613
+ return false if page.header.page_type == 3
614
+
615
+ # Only allow releasing space from the end (where we reserved it)
616
+ if offset + length == page.header.data_end
617
+ page.header.data_end -= length
618
+ page.write_header
619
+ write_page(page)
620
+
621
+ # Update free space map
622
+ @free_space_map.update_page(page_number, page.free_space)
623
+
624
+ # Update page usage
625
+ if @page_usage[page_number]
626
+ @page_usage[page_number][:reserved] = (@page_usage[page_number][:reserved] || 0) - length
627
+ @page_usage[page_number][:released_reserved] = (@page_usage[page_number][:released_reserved] || 0) + length
628
+ @page_usage[page_number][:last_modified] = Time.now
629
+ end
630
+
631
+ # Clear cache
632
+ @page_records_cache.delete(page_number)
633
+
634
+ true
635
+ else
636
+ # Cannot release internal space - would need compaction
637
+ false
638
+ end
639
+ end
640
+ end
641
+
642
+ # Get page allocation statistics
643
+ def allocation_stats
644
+ @lock.synchronize do
645
+ fragmentation_avg = average_fragmentation
646
+
647
+ {
648
+ total_pages: @page_manager.total_pages,
649
+ free_pages: @page_manager.free_pages,
650
+ used_pages: @page_manager.used_pages,
651
+ page_usage_count: @page_usage.size,
652
+ overflow_pages: @overflow_pages.values.flatten.size,
653
+ overflow_groups: @overflow_pages.size,
654
+ allocations: @stats[:allocations],
655
+ releases: @stats[:releases],
656
+ compactions: @stats[:compactions],
657
+ defragmentations: @stats[:defragmentations],
658
+ allocation_failures: @stats[:allocation_failures],
659
+ overflow_allocations: @stats[:overflow_allocations],
660
+ overflow_releases: @stats[:overflow_releases],
661
+ total_space_requested: @stats[:total_space_requested],
662
+ total_space_allocated: @stats[:total_space_allocated],
663
+ total_space_released: @stats[:total_space_released],
664
+ free_space_total: total_free_space,
665
+ fragmentation_avg: fragmentation_avg,
666
+ cache_hits: @cache_hits,
667
+ cache_misses: @cache_misses,
668
+ cache_hit_rate: cache_hit_rate,
669
+ defragmentation_threshold: @defragmentation_threshold
670
+ }
671
+ end
672
+ end
673
+
674
+ # Calculate average fragmentation across all pages
675
+ def average_fragmentation
676
+ pages = @page_manager.total_pages
677
+ return 0.0 if pages == 0
678
+
679
+ total_fragmentation = 0.0
680
+ count = 0
681
+
682
+ (0...pages).each do |page_number|
683
+ begin
684
+ frag = page_fragmentation(page_number)
685
+ if frag > 0
686
+ total_fragmentation += frag
687
+ count += 1
688
+ end
689
+ rescue
690
+ # Skip pages that can't be read
691
+ end
692
+ end
693
+
694
+ count > 0 ? (total_fragmentation / count).round(2) : 0.0
695
+ end
696
+
697
+ # Get pages with high fragmentation (above threshold)
698
+ def fragmented_pages(threshold = @defragmentation_threshold)
699
+ @lock.synchronize do
700
+ result = []
701
+ (0...@page_manager.total_pages).each do |page_number|
702
+ begin
703
+ frag = page_fragmentation(page_number)
704
+ if frag > threshold * 100 # threshold is 0-1, frag is 0-100
705
+ result << {
706
+ page_number: page_number,
707
+ fragmentation: frag,
708
+ free_space: page_free_space(page_number),
709
+ usage: page_usage(page_number)
710
+ }
711
+ end
712
+ rescue
713
+ # Skip pages that can't be read
714
+ end
715
+ end
716
+ result.sort_by { |p| -p[:fragmentation] }
717
+ end
718
+ end
719
+
720
+ # Automatically defragment pages above threshold
721
+ def auto_defragment(threshold = @defragmentation_threshold)
722
+ @lock.synchronize do
723
+ pages = fragmented_pages(threshold)
724
+ results = []
725
+
726
+ pages.each do |page_info|
727
+ begin
728
+ result = defragment_page(page_info[:page_number])
729
+ results << result
730
+ rescue => e
731
+ results << { page_number: page_info[:page_number], error: e.message }
732
+ end
733
+ end
734
+
735
+ {
736
+ defragmented: results.select { |r| r[:compacted] }.size,
737
+ errors: results.select { |r| r[:error] }.size,
738
+ total_space_reclaimed: results.sum { |r| r[:space_reclaimed] || 0 },
739
+ results: results
740
+ }
741
+ end
742
+ end
743
+
744
+ # Reset statistics
745
+ def reset_stats
746
+ @lock.synchronize do
747
+ @stats = {
748
+ allocations: 0,
749
+ releases: 0,
750
+ compactions: 0,
751
+ defragmentations: 0,
752
+ allocation_failures: 0,
753
+ total_space_requested: 0,
754
+ total_space_allocated: 0,
755
+ total_space_released: 0,
756
+ overflow_allocations: 0,
757
+ overflow_releases: 0
758
+ }
759
+ @cache_hits = 0
760
+ @cache_misses = 0
761
+ end
762
+ end
763
+
764
+ private
765
+
766
+ # Compaction and reservation must observe dirty pages held by the
767
+ # buffer pool. Falling back to PageManager keeps this class usable in
768
+ # isolated tests and by legacy callers.
769
+ def get_page(page_number)
770
+ return @page_manager.get_page(page_number) unless @buffer_pool
771
+
772
+ @buffer_pool.get_page(page_number).page
773
+ end
774
+
775
+ def write_page(page)
776
+ if @buffer_pool
777
+ @buffer_pool.write_page(page)
778
+ else
779
+ @page_manager.write_page(page)
780
+ end
781
+ end
782
+
783
+ # Read all records from a page with proper record header parsing
784
+ def read_page_records(page)
785
+ # Check cache first
786
+ cache_key = [page.page_number, page.header.data_end]
787
+ if @page_records_cache.key?(page.page_number)
788
+ cached = @page_records_cache[page.page_number]
789
+ if cached[:data_end] == page.header.data_end
790
+ @cache_hits += 1
791
+ return cached[:records]
792
+ end
793
+ end
794
+ @cache_misses += 1
795
+
796
+ records = []
797
+ header_size = PageHeader::SIZE
798
+ offset = header_size
799
+
800
+ while offset < page.header.data_end
801
+ begin
802
+ # Engine records use [record_id(8)] [data_length(4)]
803
+ # [flags(2)] [column_count(2)]. Keep this parser aligned with
804
+ # Engine#insert_row so compaction never treats a row id as a
805
+ # header-size field.
806
+ record_header = page.read(offset, 16)
807
+
808
+ if record_header && record_header.bytesize == 16
809
+ record_id, data_length, flags, = record_header.unpack("Q>L>S>S")
810
+ rec_header_size = 16
811
+
812
+ # Validate header
813
+ if rec_header_size >= 16 && data_length > 0 && data_length < (page.size - offset)
814
+ records << {
815
+ offset: offset,
816
+ length: data_length,
817
+ header_size: rec_header_size,
818
+ record_id: record_id,
819
+ flags: flags
820
+ }
821
+
822
+ offset += rec_header_size + data_length
823
+ else
824
+ # Invalid record - stop scanning
825
+ break
826
+ end
827
+ else
828
+ break
829
+ end
830
+ rescue => e
831
+ # Error reading record - stop
832
+ break
833
+ end
834
+ end
835
+
836
+ # Cache the result
837
+ if records.size <= @max_cache_size
838
+ @page_records_cache[page.page_number] = {
839
+ data_end: page.header.data_end,
840
+ records: records
841
+ }
842
+ end
843
+
844
+ records
845
+ end
846
+
847
+ # Calculate cache hit rate
848
+ def cache_hit_rate
849
+ total = @cache_hits + @cache_misses
850
+ return 0.0 if total == 0
851
+ (@cache_hits.to_f / total * 100).round(2)
852
+ end
853
+ end
854
+ end
855
+ end