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,855 +1,852 @@
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
1
+ # frozen_string_literal: true
2
+
3
+ module RubyDB
4
+ module Storage
5
+ # PageAllocator - Handles page allocation with free space management
6
+ # This is a production-grade implementation with proper fragmentation management,
7
+ # overflow page handling, and comprehensive statistics tracking.
8
+ class PageAllocator
9
+ attr_reader :page_manager, :free_space_map, :stats
10
+
11
+ def initialize(page_manager, buffer_pool: nil)
12
+ @page_manager = page_manager
13
+ @buffer_pool = buffer_pool
14
+ @free_space_map = FreeSpaceMap.new(page_manager)
15
+ @lock = Mutex.new
16
+ @stats = {
17
+ allocations: 0,
18
+ releases: 0,
19
+ compactions: 0,
20
+ defragmentations: 0,
21
+ allocation_failures: 0,
22
+ total_space_requested: 0,
23
+ total_space_allocated: 0,
24
+ total_space_released: 0,
25
+ overflow_allocations: 0,
26
+ overflow_releases: 0
27
+ }
28
+ @overflow_pages = {}
29
+ @page_usage = {}
30
+ @page_records_cache = {}
31
+ @cache_hits = 0
32
+ @cache_misses = 0
33
+ @initialized = true
34
+ @max_cache_size = 1000
35
+ @defragmentation_threshold = 0.30 # 30% fragmentation triggers defrag
36
+ end
37
+
38
+ # Allocate a page with at least needed_bytes of space
39
+ def allocate_page_with_space(needed_bytes, page_type = 0)
40
+ @lock.synchronize do
41
+ @stats[:allocations] += 1
42
+ @stats[:total_space_requested] += needed_bytes
43
+
44
+ page_size = @page_manager.file_manager.page_size
45
+ max_record_size = page_size - PageHeader::SIZE - 8 # 8 bytes for record header
46
+
47
+ # Validate request
48
+ if needed_bytes > max_record_size
49
+ @stats[:allocation_failures] += 1
50
+ raise StorageError, "Requested space #{needed_bytes} exceeds maximum record size #{max_record_size}"
51
+ end
52
+
53
+ if needed_bytes <= 0
54
+ @stats[:allocation_failures] += 1
55
+ raise StorageError, "Requested space must be positive"
56
+ end
57
+
58
+ page_number = nil
59
+ page = nil
60
+
61
+ # Try to find existing page with enough free space (best fit)
62
+ candidate_page = @free_space_map.find_page_with_space(needed_bytes)
63
+
64
+ if candidate_page
65
+ page = get_page(candidate_page)
66
+
67
+ # Verify page is valid and has space
68
+ if page && page.header.page_type != 3 && page.free_space >= needed_bytes
69
+ page_number = candidate_page
70
+ else
71
+ # Free space map is stale - update it
72
+ @free_space_map.update_page(candidate_page, page ? page.free_space : 0)
73
+ # Try another search
74
+ page_number = @free_space_map.find_page_with_space(needed_bytes)
75
+ page = get_page(page_number) if page_number
76
+ end
77
+ end
78
+
79
+ unless page_number && page
80
+ # Allocate new page
81
+ page_number = @page_manager.allocate_page(page_type)
82
+
83
+ # Initialize page
84
+ page = Page.new(page_number, page_size)
85
+ page.header.page_type = page_type
86
+ page.header.data_end = PageHeader::SIZE
87
+ page.write_header
88
+
89
+ write_page(page)
90
+ @free_space_map.update_page(page_number, page.free_space)
91
+
92
+ # Clear any cached records for this page
93
+ @page_records_cache.delete(page_number)
94
+ end
95
+
96
+ # Get the page and reserve space
97
+ page = get_page(page_number)
98
+
99
+ # Calculate new data end position
100
+ new_data_end = page.header.data_end + needed_bytes
101
+
102
+ # Double-check we have enough space
103
+ if new_data_end > page_size
104
+ @stats[:allocation_failures] += 1
105
+ raise StorageError, "Page #{page_number} doesn't have enough space (needed: #{needed_bytes}, available: #{page.free_space}, total: #{page_size - new_data_end})"
106
+ end
107
+
108
+ # Update page header
109
+ page.header.data_end = new_data_end
110
+ page.write_header
111
+
112
+ # Write the updated page
113
+ write_page(page)
114
+
115
+ # Update free space map
116
+ @free_space_map.update_page(page_number, page.free_space)
117
+
118
+ # Track page usage
119
+ @page_usage[page_number] ||= {
120
+ allocated: 0,
121
+ freed: 0,
122
+ record_count: 0,
123
+ created_at: Time.now,
124
+ last_modified: Time.now,
125
+ total_writes: 0
126
+ }
127
+ @page_usage[page_number][:allocated] += needed_bytes
128
+ @page_usage[page_number][:record_count] += 1
129
+ @page_usage[page_number][:last_modified] = Time.now
130
+ @page_usage[page_number][:total_writes] += 1
131
+
132
+ @stats[:total_space_allocated] += needed_bytes
133
+
134
+ # Clear cache for this page
135
+ @page_records_cache.delete(page_number)
136
+
137
+ # Return the offset where data can be written
138
+ offset = new_data_end - needed_bytes
139
+
140
+ {
141
+ page_number: page_number,
142
+ offset: offset,
143
+ page: page,
144
+ free_space_remaining: page.free_space,
145
+ page_usage: @page_usage[page_number]
146
+ }
147
+ end
148
+ end
149
+
150
+ # Release a page back to the free pool
151
+ def release_page(page_number)
152
+ @lock.synchronize do
153
+ @stats[:releases] += 1
154
+
155
+ # Validate page number
156
+ if page_number < 0 || page_number >= @page_manager.total_pages
157
+ raise StorageError, "Invalid page number: #{page_number}"
158
+ end
159
+
160
+ # Get page before freeing
161
+ page = get_page(page_number)
162
+ raise StorageError, "Page #{page_number} could not be read" unless page
163
+
164
+ if page
165
+ # Record usage stats before freeing
166
+ usage = @page_usage[page_number]
167
+ if usage
168
+ @stats[:total_space_allocated] -= usage[:allocated]
169
+ @stats[:total_space_released] += usage[:allocated]
170
+ end
171
+ end
172
+
173
+ # Release any overflow pages first
174
+ released_overflow = release_overflow_pages(page_number)
175
+ if released_overflow > 0
176
+ @stats[:overflow_releases] += released_overflow
177
+ end
178
+
179
+ # Free the page
180
+ @page_manager.free_page(page_number)
181
+ @free_space_map.remove_page(page_number)
182
+ @page_usage.delete(page_number)
183
+ @page_records_cache.delete(page_number)
184
+
185
+ true
186
+ end
187
+ end
188
+
189
+ # Get a page and verify it has enough space
190
+ def get_page_with_space(page_number, needed_bytes)
191
+ @lock.synchronize do
192
+ # Validate page exists
193
+ unless page_number < @page_manager.total_pages
194
+ raise StorageError, "Page #{page_number} does not exist"
195
+ end
196
+
197
+ page = get_page(page_number)
198
+
199
+ # Check if page is free
200
+ if page.header.page_type == 3
201
+ raise StorageError, "Page #{page_number} is marked as free"
202
+ end
203
+
204
+ # Check if page has enough free space
205
+ if page.free_space < needed_bytes
206
+ # Try to compact the page first
207
+ compact_result = compact_page(page_number)
208
+
209
+ # Re-check after compaction
210
+ page = get_page(page_number)
211
+
212
+ if page.free_space < needed_bytes
213
+ # Check if we can allocate an overflow page
214
+ needed_overflow = needed_bytes - page.free_space
215
+ overflow_page = allocate_overflow_page(page_number, needed_overflow)
216
+
217
+ if overflow_page
218
+ @stats[:overflow_allocations] += 1
219
+ return {
220
+ page_number: overflow_page.page_number,
221
+ page: overflow_page,
222
+ overflow: true,
223
+ parent_page: page_number
224
+ }
225
+ end
226
+
227
+ raise StorageError, "Not enough space on page #{page_number} (needed: #{needed_bytes}, available: #{page.free_space}, freed: #{compact_result[:space_reclaimed]})"
228
+ end
229
+ end
230
+
231
+ {page_number: page_number, page: page, overflow: false}
232
+ end
233
+ end
234
+
235
+ # Compact a page to remove holes and fragmentation
236
+ def compact_page(page_number)
237
+ @lock.synchronize do
238
+ @stats[:compactions] += 1
239
+
240
+ # Validate page
241
+ unless page_number < @page_manager.total_pages
242
+ raise StorageError, "Page #{page_number} does not exist"
243
+ end
244
+
245
+ page = get_page(page_number)
246
+
247
+ # Skip if page is free
248
+ if page.header.page_type == 3
249
+ return {page_number: page_number, compacted: false, reason: "page is free"}
250
+ end
251
+
252
+ header_size = PageHeader::SIZE
253
+ old_data_end = page.header.data_end
254
+
255
+ # Get all records on the page with their actual data
256
+ records = read_page_records(page)
257
+
258
+ if records.empty?
259
+ # No records to compact
260
+ return {
261
+ page_number: page_number,
262
+ compacted: false,
263
+ reason: "no records",
264
+ space_reclaimed: 0,
265
+ records_compacted: 0
266
+ }
267
+ end
268
+
269
+ # Sort records by offset to compact them
270
+ records.sort_by! { |r| r[:offset] }
271
+
272
+ # Check if compaction is needed (check for gaps)
273
+ needs_compaction = false
274
+ current_offset = header_size
275
+
276
+ records.each do |record|
277
+ if record[:offset] > current_offset
278
+ needs_compaction = true
279
+ break
280
+ end
281
+ current_offset += record[:length] + record[:header_size]
282
+ end
283
+
284
+ unless needs_compaction
285
+ return {
286
+ page_number: page_number,
287
+ compacted: false,
288
+ reason: "no fragmentation",
289
+ space_reclaimed: 0,
290
+ records_compacted: records.size
291
+ }
292
+ end
293
+
294
+ # Rewrite records contiguously
295
+ current_offset = header_size
296
+ compacted_records = []
297
+
298
+ records.each do |record|
299
+ # Read the full record data including header
300
+ record_data = page.read(record[:offset], record[:header_size] + record[:length])
301
+
302
+ # Write to new position
303
+ page.write(current_offset, record_data)
304
+
305
+ # Store mapping for potential updates
306
+ compacted_records << {
307
+ old_offset: record[:offset],
308
+ new_offset: current_offset,
309
+ length: record[:length],
310
+ header_size: record[:header_size]
311
+ }
312
+
313
+ current_offset += record[:header_size] + record[:length]
314
+ end
315
+
316
+ # Update data_end to the new end position
317
+ new_data_end = current_offset
318
+ page.header.data_end = new_data_end
319
+ page.write_header
320
+
321
+ write_page(page)
322
+
323
+ # Update free space map
324
+ @free_space_map.update_page(page_number, page.free_space)
325
+
326
+ # Clear cache for this page
327
+ @page_records_cache.delete(page_number)
328
+
329
+ # Track compaction in page usage
330
+ if @page_usage[page_number]
331
+ @page_usage[page_number][:compactions] = (@page_usage[page_number][:compactions] || 0) + 1
332
+ @page_usage[page_number][:space_reclaimed] = (@page_usage[page_number][:space_reclaimed] || 0) + (old_data_end - new_data_end)
333
+ @page_usage[page_number][:last_compact] = Time.now
334
+ end
335
+
336
+ # Return compaction results
337
+ {
338
+ page_number: page_number,
339
+ compacted: true,
340
+ old_end: old_data_end,
341
+ new_end: new_data_end,
342
+ records_compacted: records.size,
343
+ space_reclaimed: old_data_end - new_data_end,
344
+ compacted_records: compacted_records
345
+ }
346
+ end
347
+ end
348
+
349
+ # Allocate an overflow page for large records
350
+ def allocate_overflow_page(parent_page_number, needed_bytes)
351
+ @lock.synchronize do
352
+ page_size = @page_manager.file_manager.page_size
353
+
354
+ # Validate request
355
+ if needed_bytes > page_size - PageHeader::SIZE - 8
356
+ raise StorageError, "Overflow request #{needed_bytes} exceeds page capacity"
357
+ end
358
+
359
+ # Allocate new page for overflow
360
+ overflow_page_number = @page_manager.allocate_page(4) # PAGE_TYPE_OVERFLOW
361
+
362
+ # Initialize overflow page
363
+ overflow_page = Page.new(overflow_page_number, page_size)
364
+ overflow_page.header.page_type = 4 # PAGE_TYPE_OVERFLOW
365
+ overflow_page.header.data_end = PageHeader::SIZE + needed_bytes
366
+ overflow_page.header.next_page = 0
367
+ overflow_page.header.prev_page = parent_page_number
368
+ overflow_page.write_header
369
+
370
+ write_page(overflow_page)
371
+
372
+ # Link overflow page to parent
373
+ @overflow_pages[parent_page_number] ||= []
374
+ @overflow_pages[parent_page_number] << overflow_page_number
375
+
376
+ # Track overflow page usage
377
+ @page_usage[overflow_page_number] = {
378
+ allocated: needed_bytes,
379
+ freed: 0,
380
+ record_count: 0,
381
+ created_at: Time.now,
382
+ last_modified: Time.now,
383
+ total_writes: 1,
384
+ is_overflow: true,
385
+ parent_page: parent_page_number
386
+ }
387
+
388
+ # Update free space map
389
+ @free_space_map.update_page(overflow_page_number, overflow_page.free_space)
390
+
391
+ overflow_page
392
+ end
393
+ end
394
+
395
+ # Get overflow pages for a page
396
+ def get_overflow_pages(page_number)
397
+ @lock.synchronize do
398
+ @overflow_pages[page_number] || []
399
+ end
400
+ end
401
+
402
+ # Get all overflow page information
403
+ def all_overflow_info
404
+ @lock.synchronize do
405
+ result = {}
406
+ @overflow_pages.each do |parent, children|
407
+ result[parent] = children.map do |child|
408
+ page = begin
409
+ get_page(child)
410
+ rescue
411
+ nil
412
+ end
413
+ if page
414
+ {
415
+ page_number: child,
416
+ size: page.header.data_end - PageHeader::SIZE,
417
+ prev_page: page.header.prev_page,
418
+ next_page: page.header.next_page
419
+ }
420
+ else
421
+ {page_number: child, valid: false}
422
+ end
423
+ end
424
+ end
425
+ result
426
+ end
427
+ end
428
+
429
+ # Release all overflow pages for a page
430
+ def release_overflow_pages(page_number)
431
+ @lock.synchronize do
432
+ overflow_pages = @overflow_pages[page_number] || []
433
+
434
+ released = 0
435
+ errors = []
436
+ overflow_pages.each do |overflow_page_number|
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
+
446
+ unless errors.empty?
447
+ details = errors.map { |page_number, error| "#{page_number}: #{error.message}" }.join(", ")
448
+ raise StorageError, "Failed to release overflow pages (#{details})"
449
+ end
450
+
451
+ @overflow_pages.delete(page_number)
452
+
453
+ released
454
+ end
455
+ end
456
+
457
+ # Get total free space across all pages
458
+ def total_free_space
459
+ @free_space_map.total_free_space
460
+ end
461
+
462
+ # Get free space on a specific page
463
+ def page_free_space(page_number)
464
+ @free_space_map.get_free_space(page_number)
465
+ end
466
+
467
+ # Get page usage statistics
468
+ def page_usage(page_number)
469
+ @page_usage[page_number] || {allocated: 0, freed: 0, record_count: 0}
470
+ end
471
+
472
+ # Get all page usage statistics
473
+ def all_page_usage
474
+ @page_usage.dup
475
+ end
476
+
477
+ # Calculate fragmentation percentage for a page
478
+ def page_fragmentation(page_number)
479
+ page = begin
480
+ get_page(page_number)
481
+ rescue
482
+ nil
483
+ end
484
+ return 0.0 unless page
485
+
486
+ # Skip if page is free
487
+ return 0.0 if page.header.page_type == 3
488
+
489
+ # Get all records on the page
490
+ records = read_page_records(page)
491
+ return 0.0 if records.empty?
492
+
493
+ # Calculate total record data size including headers
494
+ records.sum { |r| r[:header_size] + r[:length] }
495
+
496
+ # Calculate fragmentation (holes between records)
497
+ header_size = PageHeader::SIZE
498
+ sorted_records = records.sort_by { |r| r[:offset] }
499
+
500
+ fragmentation = 0.0
501
+ last_end = header_size
502
+
503
+ sorted_records.each do |record|
504
+ if record[:offset] > last_end
505
+ fragmentation += record[:offset] - last_end
506
+ end
507
+ last_end = record[:offset] + record[:header_size] + record[:length]
508
+ end
509
+
510
+ # Add space after last record
511
+ if last_end < page.header.data_end
512
+ fragmentation += page.header.data_end - last_end
513
+ end
514
+
515
+ total_used = page.header.data_end - header_size
516
+ return 0.0 if total_used == 0
517
+
518
+ (fragmentation.to_f / total_used * 100).round(2)
519
+ end
520
+
521
+ # Defragment a page (compact and reorganize)
522
+ def defragment_page(page_number)
523
+ @lock.synchronize do
524
+ @stats[:defragmentations] += 1
525
+
526
+ # Compact the page
527
+ compaction_result = compact_page(page_number)
528
+
529
+ # Update page usage
530
+ if @page_usage[page_number]
531
+ @page_usage[page_number][:defragmented_at] = Time.now
532
+ @page_usage[page_number][:defragment_count] = (@page_usage[page_number][:defragment_count] || 0) + 1
533
+ end
534
+
535
+ {
536
+ page_number: page_number,
537
+ compacted: compaction_result[:compacted],
538
+ space_reclaimed: compaction_result[:space_reclaimed],
539
+ freed_overflow_pages: 0,
540
+ fragmentation_before: compaction_result[:fragmentation_before] || 0,
541
+ fragmentation_after: page_fragmentation(page_number)
542
+ }
543
+ end
544
+ end
545
+
546
+ # Reserve space on a page for writing
547
+ def reserve_space(page_number, needed_bytes)
548
+ @lock.synchronize do
549
+ # Validate page
550
+ unless page_number < @page_manager.total_pages
551
+ raise StorageError, "Page #{page_number} does not exist"
552
+ end
553
+
554
+ page = get_page(page_number)
555
+
556
+ # Skip if page is free
557
+ if page.header.page_type == 3
558
+ raise StorageError, "Cannot reserve space on free page #{page_number}"
559
+ end
560
+
561
+ # Check if we need to compact or allocate overflow
562
+ if page.free_space < needed_bytes
563
+ # Try compacting first
564
+ compact_page(page_number)
565
+ page = get_page(page_number)
566
+
567
+ if page.free_space < needed_bytes
568
+ # Try allocating overflow
569
+ overflow_page = allocate_overflow_page(page_number, needed_bytes - page.free_space)
570
+ if overflow_page
571
+ return {
572
+ page_number: overflow_page.page_number,
573
+ offset: PageHeader::SIZE,
574
+ page: overflow_page,
575
+ is_overflow: true
576
+ }
577
+ end
578
+
579
+ raise StorageError, "Cannot reserve #{needed_bytes} bytes on page #{page_number} (available: #{page.free_space})"
580
+ end
581
+ end
582
+
583
+ # Reserve space on the page
584
+ offset = page.header.data_end
585
+ page.header.data_end += needed_bytes
586
+ page.write_header
587
+ write_page(page)
588
+
589
+ # Update free space map
590
+ @free_space_map.update_page(page_number, page.free_space)
591
+
592
+ # Update page usage
593
+ if @page_usage[page_number]
594
+ @page_usage[page_number][:reserved] = (@page_usage[page_number][:reserved] || 0) + needed_bytes
595
+ @page_usage[page_number][:last_modified] = Time.now
596
+ end
597
+
598
+ # Clear cache
599
+ @page_records_cache.delete(page_number)
600
+
601
+ {page_number: page_number, offset: offset, page: page, is_overflow: false}
602
+ end
603
+ end
604
+
605
+ # Release reserved space on a page
606
+ def release_reserved_space(page_number, offset, length)
607
+ @lock.synchronize do
608
+ # Validate page
609
+ unless page_number < @page_manager.total_pages
610
+ return false
611
+ end
612
+
613
+ page = get_page(page_number)
614
+
615
+ # Skip if page is free
616
+ return false if page.header.page_type == 3
617
+
618
+ # Only allow releasing space from the end (where we reserved it)
619
+ if offset + length == page.header.data_end
620
+ page.header.data_end -= length
621
+ page.write_header
622
+ write_page(page)
623
+
624
+ # Update free space map
625
+ @free_space_map.update_page(page_number, page.free_space)
626
+
627
+ # Update page usage
628
+ if @page_usage[page_number]
629
+ @page_usage[page_number][:reserved] = (@page_usage[page_number][:reserved] || 0) - length
630
+ @page_usage[page_number][:released_reserved] = (@page_usage[page_number][:released_reserved] || 0) + length
631
+ @page_usage[page_number][:last_modified] = Time.now
632
+ end
633
+
634
+ # Clear cache
635
+ @page_records_cache.delete(page_number)
636
+
637
+ true
638
+ else
639
+ # Cannot release internal space - would need compaction
640
+ false
641
+ end
642
+ end
643
+ end
644
+
645
+ # Get page allocation statistics
646
+ def allocation_stats
647
+ @lock.synchronize do
648
+ fragmentation_avg = average_fragmentation
649
+
650
+ {
651
+ total_pages: @page_manager.total_pages,
652
+ free_pages: @page_manager.free_pages,
653
+ used_pages: @page_manager.used_pages,
654
+ page_usage_count: @page_usage.size,
655
+ overflow_pages: @overflow_pages.values.flatten.size,
656
+ overflow_groups: @overflow_pages.size,
657
+ allocations: @stats[:allocations],
658
+ releases: @stats[:releases],
659
+ compactions: @stats[:compactions],
660
+ defragmentations: @stats[:defragmentations],
661
+ allocation_failures: @stats[:allocation_failures],
662
+ overflow_allocations: @stats[:overflow_allocations],
663
+ overflow_releases: @stats[:overflow_releases],
664
+ total_space_requested: @stats[:total_space_requested],
665
+ total_space_allocated: @stats[:total_space_allocated],
666
+ total_space_released: @stats[:total_space_released],
667
+ free_space_total: total_free_space,
668
+ fragmentation_avg: fragmentation_avg,
669
+ cache_hits: @cache_hits,
670
+ cache_misses: @cache_misses,
671
+ cache_hit_rate: cache_hit_rate,
672
+ defragmentation_threshold: @defragmentation_threshold
673
+ }
674
+ end
675
+ end
676
+
677
+ # Calculate average fragmentation across all pages
678
+ def average_fragmentation
679
+ pages = @page_manager.total_pages
680
+ return 0.0 if pages == 0
681
+
682
+ total_fragmentation = 0.0
683
+ count = 0
684
+
685
+ (0...pages).each do |page_number|
686
+ frag = page_fragmentation(page_number)
687
+ if frag > 0
688
+ total_fragmentation += frag
689
+ count += 1
690
+ end
691
+ rescue
692
+ # Skip pages that can't be read
693
+ end
694
+
695
+ (count > 0) ? (total_fragmentation / count).round(2) : 0.0
696
+ end
697
+
698
+ # Get pages with high fragmentation (above threshold)
699
+ def fragmented_pages(threshold = @defragmentation_threshold)
700
+ @lock.synchronize do
701
+ result = []
702
+ (0...@page_manager.total_pages).each do |page_number|
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
+ result.sort_by { |p| -p[:fragmentation] }
716
+ end
717
+ end
718
+
719
+ # Automatically defragment pages above threshold
720
+ def auto_defragment(threshold = @defragmentation_threshold)
721
+ @lock.synchronize do
722
+ pages = fragmented_pages(threshold)
723
+ results = []
724
+
725
+ pages.each do |page_info|
726
+ result = defragment_page(page_info[:page_number])
727
+ results << result
728
+ rescue => e
729
+ results << {page_number: page_info[:page_number], error: e.message}
730
+ end
731
+
732
+ {
733
+ defragmented: results.select { |r| r[:compacted] }.size,
734
+ errors: results.select { |r| r[:error] }.size,
735
+ total_space_reclaimed: results.sum { |r| r[:space_reclaimed] || 0 },
736
+ results: results
737
+ }
738
+ end
739
+ end
740
+
741
+ # Reset statistics
742
+ def reset_stats
743
+ @lock.synchronize do
744
+ @stats = {
745
+ allocations: 0,
746
+ releases: 0,
747
+ compactions: 0,
748
+ defragmentations: 0,
749
+ allocation_failures: 0,
750
+ total_space_requested: 0,
751
+ total_space_allocated: 0,
752
+ total_space_released: 0,
753
+ overflow_allocations: 0,
754
+ overflow_releases: 0
755
+ }
756
+ @cache_hits = 0
757
+ @cache_misses = 0
758
+ end
759
+ end
760
+
761
+ private
762
+
763
+ # Compaction and reservation must observe dirty pages held by the
764
+ # buffer pool. Falling back to PageManager keeps this class usable in
765
+ # isolated tests and by legacy callers.
766
+ def get_page(page_number)
767
+ return @page_manager.get_page(page_number) unless @buffer_pool
768
+
769
+ @buffer_pool.get_page(page_number).page
770
+ end
771
+
772
+ def write_page(page)
773
+ if @buffer_pool
774
+ @buffer_pool.write_page(page)
775
+ else
776
+ @page_manager.write_page(page)
777
+ end
778
+ end
779
+
780
+ # Read all records from a page with proper record header parsing
781
+ def read_page_records(page)
782
+ # Check cache first
783
+ [page.page_number, page.header.data_end]
784
+ if @page_records_cache.key?(page.page_number)
785
+ cached = @page_records_cache[page.page_number]
786
+ if cached[:data_end] == page.header.data_end
787
+ @cache_hits += 1
788
+ return cached[:records]
789
+ end
790
+ end
791
+ @cache_misses += 1
792
+
793
+ records = []
794
+ header_size = PageHeader::SIZE
795
+ offset = header_size
796
+
797
+ while offset < page.header.data_end
798
+ begin
799
+ # Engine records use [record_id(8)] [data_length(4)]
800
+ # [flags(2)] [column_count(2)]. Keep this parser aligned with
801
+ # Engine#insert_row so compaction never treats a row id as a
802
+ # header-size field.
803
+ record_header = page.read(offset, 16)
804
+
805
+ if record_header && record_header.bytesize == 16
806
+ record_id, data_length, flags, = record_header.unpack("Q>L>S>S")
807
+ rec_header_size = 16
808
+
809
+ # Validate header
810
+ if rec_header_size >= 16 && data_length > 0 && data_length < (page.size - offset)
811
+ records << {
812
+ offset: offset,
813
+ length: data_length,
814
+ header_size: rec_header_size,
815
+ record_id: record_id,
816
+ flags: flags
817
+ }
818
+
819
+ offset += rec_header_size + data_length
820
+ else
821
+ # Invalid record - stop scanning
822
+ break
823
+ end
824
+ else
825
+ break
826
+ end
827
+ rescue
828
+ # Error reading record - stop
829
+ break
830
+ end
831
+ end
832
+
833
+ # Cache the result
834
+ if records.size <= @max_cache_size
835
+ @page_records_cache[page.page_number] = {
836
+ data_end: page.header.data_end,
837
+ records: records
838
+ }
839
+ end
840
+
841
+ records
842
+ end
843
+
844
+ # Calculate cache hit rate
845
+ def cache_hit_rate
846
+ total = @cache_hits + @cache_misses
847
+ return 0.0 if total == 0
848
+ (@cache_hits.to_f / total * 100).round(2)
849
+ end
850
+ end
851
+ end
852
+ end