rubydb 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (420) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
  3. data/.github/workflows/benchmark.yml +26 -26
  4. data/.github/workflows/compatibility.yml +63 -63
  5. data/.github/workflows/fuzz.yml +33 -33
  6. data/.github/workflows/lint.yml +21 -21
  7. data/.github/workflows/operations.yml +24 -24
  8. data/.github/workflows/production-validation.yml +111 -111
  9. data/.github/workflows/release.yml +77 -77
  10. data/.github/workflows/security.yml +39 -37
  11. data/.github/workflows/test.yml +26 -26
  12. data/.github/workflows/workload.yml +58 -58
  13. data/.gitignore +12 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +49 -27
  18. data/CODE_OF_CONDUCT.md +13 -13
  19. data/CONTRIBUTING.md +29 -29
  20. data/GOVERNANCE.md +16 -16
  21. data/Gemfile +18 -17
  22. data/Gemfile.lock +125 -71
  23. data/README.md +249 -162
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +71 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/adapters/activerecord/Gemfile +11 -11
  29. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +877 -880
  30. data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
  31. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +155 -143
  32. data/adapters/ruby/README.md +18 -18
  33. data/adapters/sequel/README.md +11 -11
  34. data/config/monitoring/prometheus-alerts.yml +39 -39
  35. data/config/production.yml +36 -36
  36. data/docs/README.md +75 -71
  37. data/docs/architecture/concurrency.md +14 -14
  38. data/docs/architecture/current-state.md +125 -125
  39. data/docs/architecture/execution-engine.md +25 -25
  40. data/docs/architecture/indexes.md +19 -19
  41. data/docs/architecture/mvcc.md +19 -19
  42. data/docs/architecture/overview.md +13 -13
  43. data/docs/architecture/pages.md +11 -11
  44. data/docs/architecture/production-roadmap.md +82 -82
  45. data/docs/architecture/query-planner.md +20 -20
  46. data/docs/architecture/recovery.md +18 -18
  47. data/docs/architecture/sql-engine.md +12 -12
  48. data/docs/architecture/storage-engine.md +14 -14
  49. data/docs/architecture/transactions.md +10 -10
  50. data/docs/architecture/wal.md +28 -28
  51. data/docs/cli-cheatsheet.md +98 -98
  52. data/docs/cli.md +275 -275
  53. data/docs/contributing/architecture.md +9 -9
  54. data/docs/contributing/benchmarking.md +14 -14
  55. data/docs/contributing/development.md +16 -16
  56. data/docs/contributing/release-process.md +49 -49
  57. data/docs/contributing/testing.md +16 -16
  58. data/docs/debugging.md +229 -229
  59. data/docs/developer/branching.md +10 -10
  60. data/docs/developer/database-diff.md +10 -10
  61. data/docs/developer/local-development.md +17 -17
  62. data/docs/developer/snapshots.md +9 -9
  63. data/docs/developer/temporal-data.md +10 -10
  64. data/docs/developer-guide.md +297 -297
  65. data/docs/getting-started/first-database.md +16 -16
  66. data/docs/getting-started/first-query.md +13 -13
  67. data/docs/getting-started/installation.md +19 -19
  68. data/docs/getting-started/local-to-production.md +300 -300
  69. data/docs/getting-started/quickstart.md +17 -17
  70. data/docs/getting-started/rails.md +16 -16
  71. data/docs/hardening_backlog.md +93 -93
  72. data/docs/lessons-learned.md +112 -112
  73. data/docs/operations/backups.md +33 -33
  74. data/docs/operations/disaster-recovery.md +31 -31
  75. data/docs/operations/failover.md +30 -30
  76. data/docs/operations/monitoring.md +25 -25
  77. data/docs/operations/production-guide.md +295 -295
  78. data/docs/operations/production-runbook.md +45 -45
  79. data/docs/operations/replication.md +33 -33
  80. data/docs/operations/restore.md +6 -6
  81. data/docs/operations/runbook.md +34 -34
  82. data/docs/operations/upgrades.md +14 -14
  83. data/docs/operations/workload-testing.md +17 -17
  84. data/docs/production-readiness.md +118 -118
  85. data/docs/production_validation.md +150 -150
  86. data/docs/rails/active-record.md +11 -11
  87. data/docs/rails/compatibility-guide.md +90 -90
  88. data/docs/rails/database-yml.md +92 -92
  89. data/docs/rails/installation.md +17 -17
  90. data/docs/rails/migrations.md +17 -17
  91. data/docs/rails/production.md +82 -82
  92. data/docs/rails/troubleshooting.md +18 -18
  93. data/docs/release.md +25 -25
  94. data/docs/server/architecture.md +10 -10
  95. data/docs/server/authentication.md +10 -10
  96. data/docs/server/configuration.md +16 -16
  97. data/docs/server/connection-pooling.md +10 -10
  98. data/docs/server/deployment.md +10 -10
  99. data/docs/server/protocol.md +12 -12
  100. data/docs/sql/compatibility-guide.md +82 -82
  101. data/docs/sql/compatibility.md +39 -39
  102. data/docs/sql/data-types.md +10 -10
  103. data/docs/sql/functions.md +9 -9
  104. data/docs/sql/joins.md +9 -9
  105. data/docs/sql/operators.md +9 -9
  106. data/docs/sql/sqlite-compatibility.md +21 -21
  107. data/docs/sql/syntax.md +10 -10
  108. data/docs/sql/transactions.md +10 -10
  109. data/docs/troubleshooting.md +244 -244
  110. data/lessons/01-foundations.md +73 -0
  111. data/lessons/02-local-development.md +121 -0
  112. data/lessons/03-embedded-rubydb.md +99 -0
  113. data/lessons/04-rails-complex-apps.md +138 -0
  114. data/lessons/05-rubydb-production-server.md +237 -0
  115. data/lessons/06-postgresql-massive-apps.md +96 -0
  116. data/lessons/07-hybrid-microservices.md +94 -0
  117. data/lessons/08-migrations-backups-recovery.md +86 -0
  118. data/lessons/09-observability-security-scale.md +87 -0
  119. data/lessons/10-release-readiness.md +125 -0
  120. data/lib/rubydb/backup/archive.rb +332 -334
  121. data/lib/rubydb/backup/backup.rb +400 -401
  122. data/lib/rubydb/backup/incremental.rb +349 -353
  123. data/lib/rubydb/backup/restore.rb +289 -290
  124. data/lib/rubydb/backup/snapshot.rb +265 -267
  125. data/lib/rubydb/backup/verification.rb +276 -279
  126. data/lib/rubydb/branching/branch.rb +181 -181
  127. data/lib/rubydb/branching/branch_manager.rb +307 -311
  128. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  129. data/lib/rubydb/branching/checkout.rb +165 -166
  130. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  131. data/lib/rubydb/branching/diff.rb +137 -138
  132. data/lib/rubydb/branching/merge.rb +282 -285
  133. data/lib/rubydb/build_info.rb +15 -15
  134. data/lib/rubydb/catalog/catalog.rb +391 -391
  135. data/lib/rubydb/catalog/column.rb +112 -112
  136. data/lib/rubydb/catalog/constraint.rb +180 -180
  137. data/lib/rubydb/catalog/database.rb +184 -184
  138. data/lib/rubydb/catalog/index.rb +97 -97
  139. data/lib/rubydb/catalog/schema.rb +103 -103
  140. data/lib/rubydb/catalog/sequence.rb +90 -90
  141. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  142. data/lib/rubydb/catalog/table.rb +178 -178
  143. data/lib/rubydb/catalog/trigger.rb +102 -102
  144. data/lib/rubydb/catalog/view.rb +66 -66
  145. data/lib/rubydb/cli/application.rb +163 -163
  146. data/lib/rubydb/cli/commands/backup.rb +80 -81
  147. data/lib/rubydb/cli/commands/branch.rb +72 -72
  148. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  149. data/lib/rubydb/cli/commands/create.rb +58 -58
  150. data/lib/rubydb/cli/commands/diff.rb +76 -77
  151. data/lib/rubydb/cli/commands/doctor.rb +74 -74
  152. data/lib/rubydb/cli/commands/drop.rb +57 -57
  153. data/lib/rubydb/cli/commands/init.rb +101 -102
  154. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  155. data/lib/rubydb/cli/commands/merge.rb +63 -63
  156. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  157. data/lib/rubydb/cli/commands/restart.rb +42 -39
  158. data/lib/rubydb/cli/commands/restore.rb +121 -121
  159. data/lib/rubydb/cli/commands/shell.rb +365 -365
  160. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  161. data/lib/rubydb/cli/commands/start.rb +88 -82
  162. data/lib/rubydb/cli/commands/status.rb +96 -92
  163. data/lib/rubydb/cli/commands/stop.rb +47 -47
  164. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  165. data/lib/rubydb/cli/formatter.rb +221 -221
  166. data/lib/rubydb/cli/output.rb +168 -168
  167. data/lib/rubydb/client/client.rb +302 -304
  168. data/lib/rubydb/client/connection.rb +414 -415
  169. data/lib/rubydb/client/connection_pool.rb +168 -168
  170. data/lib/rubydb/client/connection_url.rb +96 -96
  171. data/lib/rubydb/client/prepared_statement.rb +60 -60
  172. data/lib/rubydb/client/result.rb +123 -123
  173. data/lib/rubydb/client/statement.rb +52 -52
  174. data/lib/rubydb/client/transaction.rb +130 -130
  175. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  176. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  177. data/lib/rubydb/concurrency/latch.rb +101 -101
  178. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  179. data/lib/rubydb/concurrency/mutex.rb +181 -183
  180. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  181. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  182. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  183. data/lib/rubydb/configuration/config.rb +170 -170
  184. data/lib/rubydb/configuration/defaults.rb +179 -179
  185. data/lib/rubydb/configuration/environment.rb +152 -152
  186. data/lib/rubydb/configuration/parser.rb +185 -185
  187. data/lib/rubydb/configuration/validation.rb +221 -221
  188. data/lib/rubydb/constants.rb +74 -74
  189. data/lib/rubydb/constraints/check.rb +181 -181
  190. data/lib/rubydb/constraints/constraint.rb +101 -101
  191. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  192. data/lib/rubydb/constraints/not_null.rb +64 -64
  193. data/lib/rubydb/constraints/primary_key.rb +99 -99
  194. data/lib/rubydb/constraints/unique.rb +106 -108
  195. data/lib/rubydb/constraints/validator.rb +349 -350
  196. data/lib/rubydb/errors/authentication_error.rb +10 -10
  197. data/lib/rubydb/errors/authorization_error.rb +23 -23
  198. data/lib/rubydb/errors/client_error.rb +10 -10
  199. data/lib/rubydb/errors/configuration_error.rb +10 -10
  200. data/lib/rubydb/errors/connection_error.rb +10 -10
  201. data/lib/rubydb/errors/constraint_error.rb +23 -23
  202. data/lib/rubydb/errors/corruption_error.rb +10 -10
  203. data/lib/rubydb/errors/database_error.rb +10 -10
  204. data/lib/rubydb/errors/error.rb +20 -20
  205. data/lib/rubydb/errors/execution_error.rb +10 -10
  206. data/lib/rubydb/errors/parser_error.rb +10 -10
  207. data/lib/rubydb/errors/recovery_error.rb +10 -10
  208. data/lib/rubydb/errors/replication_error.rb +10 -10
  209. data/lib/rubydb/errors/server_error.rb +6 -6
  210. data/lib/rubydb/errors/storage_error.rb +10 -10
  211. data/lib/rubydb/errors/transaction_error.rb +10 -10
  212. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  213. data/lib/rubydb/execution/delete_executor.rb +110 -112
  214. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  215. data/lib/rubydb/execution/executor.rb +1195 -1192
  216. data/lib/rubydb/execution/expression.rb +191 -193
  217. data/lib/rubydb/execution/index_scan.rb +142 -142
  218. data/lib/rubydb/execution/insert_executor.rb +215 -217
  219. data/lib/rubydb/execution/join_executor.rb +243 -249
  220. data/lib/rubydb/execution/limit_executor.rb +83 -85
  221. data/lib/rubydb/execution/optimizer.rb +227 -215
  222. data/lib/rubydb/execution/plan.rb +355 -353
  223. data/lib/rubydb/execution/planner.rb +542 -536
  224. data/lib/rubydb/execution/predicate.rb +235 -235
  225. data/lib/rubydb/execution/scan.rb +49 -49
  226. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  227. data/lib/rubydb/execution/sort_executor.rb +180 -185
  228. data/lib/rubydb/execution/update_executor.rb +160 -162
  229. data/lib/rubydb/functions/aggregate.rb +70 -70
  230. data/lib/rubydb/functions/date_functions.rb +274 -278
  231. data/lib/rubydb/functions/function.rb +85 -85
  232. data/lib/rubydb/functions/json_functions.rb +231 -215
  233. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  234. data/lib/rubydb/functions/scalar.rb +52 -52
  235. data/lib/rubydb/functions/string_functions.rb +383 -383
  236. data/lib/rubydb/functions/system_functions.rb +258 -246
  237. data/lib/rubydb/history/as_of.rb +238 -238
  238. data/lib/rubydb/history/change.rb +105 -105
  239. data/lib/rubydb/history/history.rb +131 -131
  240. data/lib/rubydb/history/history_manager.rb +228 -229
  241. data/lib/rubydb/history/temporal_query.rb +202 -202
  242. data/lib/rubydb/history/timeline.rb +144 -144
  243. data/lib/rubydb/indexes/btree.rb +186 -186
  244. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  245. data/lib/rubydb/indexes/btree_node.rb +384 -385
  246. data/lib/rubydb/indexes/hash_index.rb +150 -150
  247. data/lib/rubydb/indexes/index.rb +71 -71
  248. data/lib/rubydb/indexes/index_manager.rb +408 -406
  249. data/lib/rubydb/indexes/index_scan.rb +466 -470
  250. data/lib/rubydb/migrations/migration.rb +253 -254
  251. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  252. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  253. data/lib/rubydb/migrations/migration_version.rb +71 -71
  254. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  255. data/lib/rubydb/migrations/schema_version.rb +64 -64
  256. data/lib/rubydb/monitoring/events.rb +155 -160
  257. data/lib/rubydb/monitoring/health.rb +216 -222
  258. data/lib/rubydb/monitoring/logger.rb +188 -193
  259. data/lib/rubydb/monitoring/metrics.rb +363 -359
  260. data/lib/rubydb/monitoring/performance.rb +176 -176
  261. data/lib/rubydb/monitoring/statistics.rb +168 -170
  262. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  263. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  264. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  265. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  266. data/lib/rubydb/mvcc/version.rb +106 -106
  267. data/lib/rubydb/mvcc/version_store.rb +396 -398
  268. data/lib/rubydb/mvcc/visibility.rb +107 -109
  269. data/lib/rubydb/protocol/capabilities.rb +125 -125
  270. data/lib/rubydb/protocol/decoder.rb +142 -145
  271. data/lib/rubydb/protocol/encoder.rb +131 -136
  272. data/lib/rubydb/protocol/handshake.rb +306 -305
  273. data/lib/rubydb/protocol/message.rb +121 -121
  274. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  275. data/lib/rubydb/protocol/protocol.rb +276 -277
  276. data/lib/rubydb/protocol/version.rb +54 -54
  277. data/lib/rubydb/rails/adapter.rb +245 -239
  278. data/lib/rubydb/rails/connection.rb +312 -314
  279. data/lib/rubydb/rails/database_statements.rb +122 -122
  280. data/lib/rubydb/rails/migration.rb +131 -131
  281. data/lib/rubydb/rails/quoting.rb +109 -109
  282. data/lib/rubydb/rails/result.rb +123 -120
  283. data/lib/rubydb/rails/schema_statements.rb +339 -339
  284. data/lib/rubydb/rails/transaction.rb +105 -105
  285. data/lib/rubydb/rails/type.rb +126 -126
  286. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  287. data/lib/rubydb/recovery/consistency.rb +457 -467
  288. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  289. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  290. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  291. data/lib/rubydb/recovery/redo.rb +235 -237
  292. data/lib/rubydb/recovery/undo.rb +204 -206
  293. data/lib/rubydb/replication/failover.rb +5 -5
  294. data/lib/rubydb/replication/fencing.rb +63 -63
  295. data/lib/rubydb/replication/primary.rb +461 -450
  296. data/lib/rubydb/replication/replica.rb +382 -384
  297. data/lib/rubydb/replication/replication_log.rb +194 -200
  298. data/lib/rubydb/replication/replication_manager.rb +307 -308
  299. data/lib/rubydb/replication/replication_slot.rb +293 -295
  300. data/lib/rubydb/replication/replication_stream.rb +198 -201
  301. data/lib/rubydb/rubydb.rb +564 -560
  302. data/lib/rubydb/security/access_control.rb +252 -254
  303. data/lib/rubydb/security/audit_log.rb +209 -213
  304. data/lib/rubydb/security/authentication.rb +302 -302
  305. data/lib/rubydb/security/authorization.rb +282 -282
  306. data/lib/rubydb/security/credentials.rb +192 -196
  307. data/lib/rubydb/security/password.rb +205 -215
  308. data/lib/rubydb/security/permissions.rb +74 -74
  309. data/lib/rubydb/security/role.rb +99 -101
  310. data/lib/rubydb/security/user.rb +86 -86
  311. data/lib/rubydb/server/connection.rb +383 -366
  312. data/lib/rubydb/server/connection_pool.rb +193 -193
  313. data/lib/rubydb/server/lifecycle.rb +227 -228
  314. data/lib/rubydb/server/listener.rb +139 -136
  315. data/lib/rubydb/server/request_handler.rb +277 -276
  316. data/lib/rubydb/server/server.rb +363 -364
  317. data/lib/rubydb/server/session.rb +371 -369
  318. data/lib/rubydb/server/worker.rb +206 -210
  319. data/lib/rubydb/server/worker_pool.rb +168 -168
  320. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  321. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  322. data/lib/rubydb/sql/ast/commit.rb +37 -37
  323. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  324. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  325. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  326. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  327. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  328. data/lib/rubydb/sql/ast/delete.rb +54 -54
  329. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  330. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  331. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  332. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  333. data/lib/rubydb/sql/ast/explain.rb +64 -64
  334. data/lib/rubydb/sql/ast/expression.rb +617 -604
  335. data/lib/rubydb/sql/ast/insert.rb +66 -66
  336. data/lib/rubydb/sql/ast/node.rb +42 -42
  337. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  338. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  339. data/lib/rubydb/sql/ast/select.rb +88 -88
  340. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  341. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  342. data/lib/rubydb/sql/ast/update.rb +88 -88
  343. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  344. data/lib/rubydb/sql/ast/view.rb +38 -32
  345. data/lib/rubydb/sql/ast/with.rb +32 -32
  346. data/lib/rubydb/sql/grammar.rb +86 -86
  347. data/lib/rubydb/sql/keywords.rb +156 -156
  348. data/lib/rubydb/sql/lexer.rb +209 -214
  349. data/lib/rubydb/sql/operators.rb +100 -100
  350. data/lib/rubydb/sql/parser.rb +1167 -1170
  351. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  352. data/lib/rubydb/sql/planner/binder.rb +537 -550
  353. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  354. data/lib/rubydb/sql/token.rb +210 -210
  355. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  356. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  357. data/lib/rubydb/storage/database_lock.rb +74 -74
  358. data/lib/rubydb/storage/deserializer.rb +332 -342
  359. data/lib/rubydb/storage/engine.rb +2347 -2330
  360. data/lib/rubydb/storage/file_manager.rb +191 -187
  361. data/lib/rubydb/storage/free_space_map.rb +79 -81
  362. data/lib/rubydb/storage/page.rb +92 -94
  363. data/lib/rubydb/storage/page_allocator.rb +852 -855
  364. data/lib/rubydb/storage/page_header.rb +63 -67
  365. data/lib/rubydb/storage/page_manager.rb +127 -131
  366. data/lib/rubydb/storage/record.rb +58 -58
  367. data/lib/rubydb/storage/row.rb +78 -78
  368. data/lib/rubydb/storage/serializer.rb +51 -51
  369. data/lib/rubydb/storage/storage_layout.rb +151 -151
  370. data/lib/rubydb/storage/storage_manager.rb +114 -114
  371. data/lib/rubydb/storage/tuple.rb +458 -461
  372. data/lib/rubydb/storage/visibility_map.rb +964 -973
  373. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  374. data/lib/rubydb/transactions/isolation.rb +98 -98
  375. data/lib/rubydb/transactions/lock.rb +76 -76
  376. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  377. data/lib/rubydb/transactions/savepoint.rb +142 -143
  378. data/lib/rubydb/transactions/transaction.rb +214 -215
  379. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  380. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  381. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  382. data/lib/rubydb/types/bigint.rb +36 -36
  383. data/lib/rubydb/types/blob.rb +37 -37
  384. data/lib/rubydb/types/boolean.rb +34 -34
  385. data/lib/rubydb/types/date.rb +39 -39
  386. data/lib/rubydb/types/decimal.rb +48 -48
  387. data/lib/rubydb/types/float.rb +34 -34
  388. data/lib/rubydb/types/integer.rb +36 -36
  389. data/lib/rubydb/types/json.rb +41 -41
  390. data/lib/rubydb/types/null.rb +34 -34
  391. data/lib/rubydb/types/smallint.rb +36 -36
  392. data/lib/rubydb/types/text.rb +37 -37
  393. data/lib/rubydb/types/time.rb +46 -46
  394. data/lib/rubydb/types/timestamp.rb +39 -39
  395. data/lib/rubydb/types/type.rb +119 -119
  396. data/lib/rubydb/types/uuid.rb +47 -47
  397. data/lib/rubydb/types/varchar.rb +37 -37
  398. data/lib/rubydb/version.rb +32 -32
  399. data/lib/rubydb/wal/archive.rb +190 -193
  400. data/lib/rubydb/wal/checkpoint.rb +181 -183
  401. data/lib/rubydb/wal/lsn.rb +94 -94
  402. data/lib/rubydb/wal/reader.rb +259 -260
  403. data/lib/rubydb/wal/record.rb +105 -105
  404. data/lib/rubydb/wal/segment.rb +193 -193
  405. data/lib/rubydb/wal/wal.rb +480 -452
  406. data/lib/rubydb/wal/writer.rb +236 -236
  407. data/lib/rubydb.rb +7 -7
  408. data/packaging/docker/docker-compose.failover.yml +43 -43
  409. data/packaging/homebrew/rubydb.rb +19 -19
  410. data/rubydb.gemspec +60 -57
  411. data/scripts/benchmark +7 -7
  412. data/scripts/durability_drill +37 -37
  413. data/scripts/fuzz +63 -63
  414. data/scripts/release +45 -40
  415. data/scripts/release_check +43 -43
  416. data/scripts/replication_failover_drill +268 -250
  417. data/scripts/replication_network_failover_drill +287 -255
  418. data/scripts/restore_drill +45 -45
  419. data/scripts/security +45 -0
  420. metadata +54 -1
@@ -1,470 +1,466 @@
1
- # frozen_string_literal: true
2
-
3
- require "set"
4
-
5
- module RubyDB
6
- module Indexes
7
- # IndexScan - Executes index scans for queries with full production features
8
- class IndexScan
9
- attr_reader :index, :scan_type, :conditions, :stats
10
-
11
- SCAN_TYPE_EQUAL = :equal
12
- SCAN_TYPE_RANGE = :range
13
- SCAN_TYPE_PREFIX = :prefix
14
- SCAN_TYPE_FULL = :full
15
- SCAN_TYPE_MULTI_KEY = :multi_key
16
-
17
- def initialize(index, scan_type = SCAN_TYPE_FULL, conditions = {})
18
- @index = index
19
- @scan_type = scan_type
20
- @conditions = conditions
21
- @cursor = nil
22
- @results = nil
23
- @position = 0
24
- @lock = Mutex.new
25
- @limit = conditions[:limit]
26
- @offset = conditions[:offset] || 0
27
- @stats = {
28
- rows_scanned: 0,
29
- keys_checked: 0,
30
- matches_found: 0,
31
- scan_time_ms: 0,
32
- index_pages_accessed: 0
33
- }
34
- @executed = false
35
- @filter = conditions[:filter]
36
- @order = conditions[:order] || :asc
37
- @distinct = conditions[:distinct] || false
38
- end
39
-
40
- def execute
41
- @lock.synchronize do
42
- start_time = Time.now
43
- @results = []
44
-
45
- case @scan_type
46
- when SCAN_TYPE_EQUAL
47
- execute_equal_scan
48
- when SCAN_TYPE_RANGE
49
- execute_range_scan
50
- when SCAN_TYPE_PREFIX
51
- execute_prefix_scan
52
- when SCAN_TYPE_MULTI_KEY
53
- execute_multi_key_scan
54
- else
55
- execute_full_scan
56
- end
57
-
58
- # Apply limit and offset
59
- if @limit
60
- @results = @results[@offset, @limit]
61
- elsif @offset > 0
62
- @results = @results[@offset..-1] || []
63
- end
64
-
65
- # Apply distinct if needed
66
- if @distinct
67
- @results = distinct_results(@results)
68
- end
69
-
70
- @executed = true
71
- @stats[:scan_time_ms] = ((Time.now - start_time) * 1000).round(2)
72
- @stats[:matches_found] = @results.size
73
- @results
74
- end
75
- end
76
-
77
- def next
78
- @lock.synchronize do
79
- return nil if @results.nil?
80
- return nil if @position >= @results.size
81
-
82
- result = @results[@position]
83
- @position += 1
84
- result
85
- end
86
- end
87
-
88
- def next_batch(batch_size = 100)
89
- @lock.synchronize do
90
- return [] if @results.nil? || @position >= @results.size
91
-
92
- batch = @results[@position, batch_size] || []
93
- @position += batch.size
94
- batch
95
- end
96
- end
97
-
98
- def reset
99
- @lock.synchronize do
100
- @position = 0
101
- @results = nil
102
- @executed = false
103
- @cursor = nil if @cursor
104
- end
105
- end
106
-
107
- def row_ids
108
- @lock.synchronize do
109
- @results ? @results.map { |r| r[:row_id] } : []
110
- end
111
- end
112
-
113
- def keys
114
- @lock.synchronize do
115
- @results ? @results.map { |r| r[:key] } : []
116
- end
117
- end
118
-
119
- def count
120
- @results ? @results.size : 0
121
- end
122
-
123
- def estimate_cost
124
- case @scan_type
125
- when SCAN_TYPE_EQUAL
126
- 1
127
- when SCAN_TYPE_PREFIX
128
- @index.entries_count / 10
129
- when SCAN_TYPE_RANGE
130
- @index.entries_count / 2
131
- when SCAN_TYPE_MULTI_KEY
132
- @index.entries_count * 0.75
133
- else
134
- @index.entries_count
135
- end
136
- end
137
-
138
- def estimate_rows
139
- case @scan_type
140
- when SCAN_TYPE_EQUAL
141
- 1
142
- when SCAN_TYPE_PREFIX
143
- @index.entries_count / 20
144
- when SCAN_TYPE_RANGE
145
- @index.entries_count / 3
146
- else
147
- @index.entries_count
148
- end
149
- end
150
-
151
- def index_used?
152
- @executed && @results && @results.any?
153
- end
154
-
155
- def scan_stats
156
- @stats.merge({
157
- scan_type: @scan_type,
158
- index_type: @index.type,
159
- index_name: @index.name,
160
- table_name: @index.table_name,
161
- columns: @index.columns,
162
- entries_count: @index.entries_count,
163
- limit: @limit,
164
- offset: @offset
165
- })
166
- end
167
-
168
- private
169
-
170
- def execute_equal_scan
171
- key = @conditions[:key]
172
- return [] unless key
173
-
174
- @stats[:keys_checked] += 1
175
-
176
- if @index.type == :btree || @index.type == :hash
177
- row_ids = @index.search(key)
178
- @stats[:rows_scanned] = row_ids.size
179
- @stats[:index_pages_accessed] = 1
180
-
181
- @results = row_ids.map do |row_id|
182
- { row_id: row_id, key: key }
183
- end
184
- else
185
- @results = []
186
- end
187
-
188
- # Apply filter if present
189
- apply_filter if @filter
190
-
191
- @results
192
- end
193
-
194
- def execute_range_scan
195
- start_key = @conditions[:start_key]
196
- end_key = @conditions[:end_key]
197
- inclusive_start = @conditions[:inclusive_start] != false
198
- inclusive_end = @conditions[:inclusive_end] != false
199
-
200
- if @index.type == :btree
201
- results = @index.range_search(start_key, end_key)
202
- @stats[:rows_scanned] = results.size
203
- @stats[:index_pages_accessed] = (results.size / 10.0).ceil + 1
204
-
205
- @results = results.map do |r|
206
- { row_id: r[:value], key: r[:key] }
207
- end
208
-
209
- # Filter by inclusivity
210
- if !inclusive_start && start_key
211
- @results.reject! { |r| r[:key] == start_key }
212
- end
213
- if !inclusive_end && end_key
214
- @results.reject! { |r| r[:key] == end_key }
215
- end
216
- else
217
- # For non-BTree indexes, fall back to full scan with filtering
218
- execute_full_scan_with_range_filter(start_key, end_key, inclusive_start, inclusive_end)
219
- end
220
-
221
- apply_filter if @filter
222
- @results
223
- end
224
-
225
- def execute_prefix_scan
226
- prefix = @conditions[:prefix]
227
- return [] unless prefix
228
-
229
- if @index.type == :btree
230
- # For prefix scan, find keys starting with prefix
231
- start_key = prefix
232
- end_key = prefix + "\xFF\xFF\xFF" # Max possible suffix
233
-
234
- results = @index.range_search(start_key, end_key)
235
- @stats[:rows_scanned] = results.size
236
- @stats[:index_pages_accessed] = (results.size / 20.0).ceil + 1
237
-
238
- @results = results.map do |r|
239
- { row_id: r[:value], key: r[:key] }
240
- end
241
- else
242
- # For hash indexes, full scan with prefix filter
243
- execute_full_scan_with_prefix_filter(prefix)
244
- end
245
-
246
- apply_filter if @filter
247
- @results
248
- end
249
-
250
- def execute_multi_key_scan
251
- keys = @conditions[:keys]
252
- return [] unless keys.is_a?(Array) && keys.any?
253
-
254
- @stats[:keys_checked] = keys.size
255
-
256
- all_row_ids = []
257
- keys.each do |key|
258
- if @index.type == :btree || @index.type == :hash
259
- row_ids = @index.search(key)
260
- all_row_ids.concat(row_ids.map { |rid| { row_id: rid, key: key } })
261
- end
262
- end
263
-
264
- # Remove duplicates if needed
265
- if @conditions[:distinct_keys] != false
266
- seen = Set.new
267
- all_row_ids = all_row_ids.select do |entry|
268
- key = entry[:key]
269
- if seen.include?(key)
270
- false
271
- else
272
- seen.add(key)
273
- true
274
- end
275
- end
276
- end
277
-
278
- @stats[:rows_scanned] = all_row_ids.size
279
- @stats[:index_pages_accessed] = all_row_ids.size
280
- @results = all_row_ids
281
-
282
- apply_filter if @filter
283
- @results
284
- end
285
-
286
- def execute_full_scan
287
- @results = []
288
-
289
- if @index.type == :btree
290
- # Use cursor for B-Tree
291
- @cursor = BTreeCursor.new(@index)
292
- @cursor.first
293
-
294
- page_count = 0
295
- while (entry = @cursor.next)
296
- @stats[:rows_scanned] += 1
297
- @stats[:keys_checked] += 1
298
- page_count += 1 if page_count % 50 == 0
299
-
300
- # Apply filter early if possible
301
- if @filter
302
- next unless apply_filter_to_entry(entry)
303
- end
304
-
305
- @results << { row_id: entry[:value], key: entry[:key] }
306
- break if @limit && @results.size >= @limit + @offset
307
- end
308
- @stats[:index_pages_accessed] = (page_count / 50.0).ceil + 1
309
- else
310
- # For hash indexes, scan all buckets
311
- @results = scan_hash_index_full
312
- @stats[:rows_scanned] = @results.size
313
- @stats[:index_pages_accessed] = 1
314
- end
315
-
316
- apply_filter if @filter && !@results.empty?
317
- @results
318
- end
319
-
320
- def execute_full_scan_with_range_filter(start_key, end_key, inclusive_start, inclusive_end)
321
- @results = []
322
-
323
- if @index.type == :btree
324
- @cursor = BTreeCursor.new(@index)
325
- @cursor.seek(start_key) if start_key
326
- @cursor.first unless start_key
327
-
328
- while (entry = @cursor.next)
329
- break if end_key && entry[:key] > end_key
330
-
331
- @stats[:rows_scanned] += 1
332
- @stats[:keys_checked] += 1
333
-
334
- # Check inclusivity
335
- if start_key && entry[:key] == start_key && !inclusive_start
336
- next
337
- end
338
- if end_key && entry[:key] == end_key && !inclusive_end
339
- next
340
- end
341
-
342
- if @filter
343
- next unless apply_filter_to_entry(entry)
344
- end
345
-
346
- @results << { row_id: entry[:value], key: entry[:key] }
347
- break if @limit && @results.size >= @limit + @offset
348
- end
349
- @stats[:index_pages_accessed] = (results.size / 20.0).ceil + 1
350
- else
351
- # Hash index - scan all and filter
352
- scan_hash_index_full.each do |entry|
353
- next if start_key && entry[:key] < start_key
354
- next if end_key && entry[:key] > end_key
355
-
356
- if start_key && entry[:key] == start_key && !inclusive_start
357
- next
358
- end
359
- if end_key && entry[:key] == end_key && !inclusive_end
360
- next
361
- end
362
-
363
- @results << entry
364
- end
365
- end
366
-
367
- @results
368
- end
369
-
370
- def execute_full_scan_with_prefix_filter(prefix)
371
- @results = []
372
-
373
- if @index.type == :btree
374
- @cursor = BTreeCursor.new(@index)
375
- @cursor.first
376
-
377
- while (entry = @cursor.next)
378
- key_str = entry[:key].to_s
379
- next unless key_str.start_with?(prefix.to_s)
380
-
381
- @stats[:rows_scanned] += 1
382
- @stats[:keys_checked] += 1
383
-
384
- if @filter
385
- next unless apply_filter_to_entry(entry)
386
- end
387
-
388
- @results << { row_id: entry[:value], key: entry[:key] }
389
- break if @limit && @results.size >= @limit + @offset
390
- end
391
- else
392
- scan_hash_index_full.each do |entry|
393
- key_str = entry[:key].to_s
394
- next unless key_str.start_with?(prefix.to_s)
395
- @results << entry
396
- end
397
- end
398
-
399
- @results
400
- end
401
-
402
- def scan_hash_index_full
403
- # Access the internal hash table of the hash index
404
- # This is a production implementation that iterates through all buckets
405
- results = []
406
- hash_table = @index.instance_variable_get(:@hash_table)
407
-
408
- if hash_table
409
- hash_table.each do |_bucket_key, bucket|
410
- bucket.each do |entry|
411
- @stats[:rows_scanned] += 1
412
- @stats[:keys_checked] += 1
413
-
414
- if @filter
415
- next unless apply_filter_to_entry(entry)
416
- end
417
-
418
- results << { row_id: entry[:row_id], key: entry[:key] }
419
- break if @limit && results.size >= @limit + @offset
420
- end
421
- break if @limit && results.size >= @limit + @offset
422
- end
423
- end
424
-
425
- results
426
- end
427
-
428
- def apply_filter
429
- return unless @filter && @results.any?
430
-
431
- @results.select! do |result|
432
- apply_filter_to_entry(result)
433
- end
434
- end
435
-
436
- def apply_filter_to_entry(entry)
437
- return true unless @filter
438
-
439
- key = entry[:key] || entry[:row_id]
440
- value = entry[:row_id] || entry[:value]
441
-
442
- @filter.call(key, value)
443
- end
444
-
445
- def distinct_results(results)
446
- seen = Set.new
447
- results.select do |result|
448
- key = result[:key]
449
- if seen.include?(key)
450
- false
451
- else
452
- seen.add(key)
453
- true
454
- end
455
- end
456
- end
457
-
458
- def find_internal_hash_table
459
- # Try to get hash table from index
460
- if @index.respond_to?(:hash_table)
461
- @index.hash_table
462
- elsif @index.instance_variable_defined?(:@hash_table)
463
- @index.instance_variable_get(:@hash_table)
464
- else
465
- {}
466
- end
467
- end
468
- end
469
- end
470
- end
1
+ # frozen_string_literal: true
2
+
3
+ module RubyDB
4
+ module Indexes
5
+ # IndexScan - Executes index scans for queries with full production features
6
+ class IndexScan
7
+ attr_reader :index, :scan_type, :conditions, :stats
8
+
9
+ SCAN_TYPE_EQUAL = :equal
10
+ SCAN_TYPE_RANGE = :range
11
+ SCAN_TYPE_PREFIX = :prefix
12
+ SCAN_TYPE_FULL = :full
13
+ SCAN_TYPE_MULTI_KEY = :multi_key
14
+
15
+ def initialize(index, scan_type = SCAN_TYPE_FULL, conditions = {})
16
+ @index = index
17
+ @scan_type = scan_type
18
+ @conditions = conditions
19
+ @cursor = nil
20
+ @results = nil
21
+ @position = 0
22
+ @lock = Mutex.new
23
+ @limit = conditions[:limit]
24
+ @offset = conditions[:offset] || 0
25
+ @stats = {
26
+ rows_scanned: 0,
27
+ keys_checked: 0,
28
+ matches_found: 0,
29
+ scan_time_ms: 0,
30
+ index_pages_accessed: 0
31
+ }
32
+ @executed = false
33
+ @filter = conditions[:filter]
34
+ @order = conditions[:order] || :asc
35
+ @distinct = conditions[:distinct] || false
36
+ end
37
+
38
+ def execute
39
+ @lock.synchronize do
40
+ start_time = Time.now
41
+ @results = []
42
+
43
+ case @scan_type
44
+ when SCAN_TYPE_EQUAL
45
+ execute_equal_scan
46
+ when SCAN_TYPE_RANGE
47
+ execute_range_scan
48
+ when SCAN_TYPE_PREFIX
49
+ execute_prefix_scan
50
+ when SCAN_TYPE_MULTI_KEY
51
+ execute_multi_key_scan
52
+ else
53
+ execute_full_scan
54
+ end
55
+
56
+ # Apply limit and offset
57
+ if @limit
58
+ @results = @results[@offset, @limit]
59
+ elsif @offset > 0
60
+ @results = @results[@offset..] || []
61
+ end
62
+
63
+ # Apply distinct if needed
64
+ if @distinct
65
+ @results = distinct_results(@results)
66
+ end
67
+
68
+ @executed = true
69
+ @stats[:scan_time_ms] = ((Time.now - start_time) * 1000).round(2)
70
+ @stats[:matches_found] = @results.size
71
+ @results
72
+ end
73
+ end
74
+
75
+ def next
76
+ @lock.synchronize do
77
+ return nil if @results.nil?
78
+ return nil if @position >= @results.size
79
+
80
+ result = @results[@position]
81
+ @position += 1
82
+ result
83
+ end
84
+ end
85
+
86
+ def next_batch(batch_size = 100)
87
+ @lock.synchronize do
88
+ return [] if @results.nil? || @position >= @results.size
89
+
90
+ batch = @results[@position, batch_size] || []
91
+ @position += batch.size
92
+ batch
93
+ end
94
+ end
95
+
96
+ def reset
97
+ @lock.synchronize do
98
+ @position = 0
99
+ @results = nil
100
+ @executed = false
101
+ @cursor = nil if @cursor
102
+ end
103
+ end
104
+
105
+ def row_ids
106
+ @lock.synchronize do
107
+ @results ? @results.map { |r| r[:row_id] } : []
108
+ end
109
+ end
110
+
111
+ def keys
112
+ @lock.synchronize do
113
+ @results ? @results.map { |r| r[:key] } : []
114
+ end
115
+ end
116
+
117
+ def count
118
+ @results ? @results.size : 0
119
+ end
120
+
121
+ def estimate_cost
122
+ case @scan_type
123
+ when SCAN_TYPE_EQUAL
124
+ 1
125
+ when SCAN_TYPE_PREFIX
126
+ @index.entries_count / 10
127
+ when SCAN_TYPE_RANGE
128
+ @index.entries_count / 2
129
+ when SCAN_TYPE_MULTI_KEY
130
+ @index.entries_count * 0.75
131
+ else
132
+ @index.entries_count
133
+ end
134
+ end
135
+
136
+ def estimate_rows
137
+ case @scan_type
138
+ when SCAN_TYPE_EQUAL
139
+ 1
140
+ when SCAN_TYPE_PREFIX
141
+ @index.entries_count / 20
142
+ when SCAN_TYPE_RANGE
143
+ @index.entries_count / 3
144
+ else
145
+ @index.entries_count
146
+ end
147
+ end
148
+
149
+ def index_used?
150
+ @executed && @results && @results.any?
151
+ end
152
+
153
+ def scan_stats
154
+ @stats.merge({
155
+ scan_type: @scan_type,
156
+ index_type: @index.type,
157
+ index_name: @index.name,
158
+ table_name: @index.table_name,
159
+ columns: @index.columns,
160
+ entries_count: @index.entries_count,
161
+ limit: @limit,
162
+ offset: @offset
163
+ })
164
+ end
165
+
166
+ private
167
+
168
+ def execute_equal_scan
169
+ key = @conditions[:key]
170
+ return [] unless key
171
+
172
+ @stats[:keys_checked] += 1
173
+
174
+ if @index.type == :btree || @index.type == :hash
175
+ row_ids = @index.search(key)
176
+ @stats[:rows_scanned] = row_ids.size
177
+ @stats[:index_pages_accessed] = 1
178
+
179
+ @results = row_ids.map do |row_id|
180
+ {row_id: row_id, key: key}
181
+ end
182
+ else
183
+ @results = []
184
+ end
185
+
186
+ # Apply filter if present
187
+ apply_filter if @filter
188
+
189
+ @results
190
+ end
191
+
192
+ def execute_range_scan
193
+ start_key = @conditions[:start_key]
194
+ end_key = @conditions[:end_key]
195
+ inclusive_start = @conditions[:inclusive_start] != false
196
+ inclusive_end = @conditions[:inclusive_end] != false
197
+
198
+ if @index.type == :btree
199
+ results = @index.range_search(start_key, end_key)
200
+ @stats[:rows_scanned] = results.size
201
+ @stats[:index_pages_accessed] = (results.size / 10.0).ceil + 1
202
+
203
+ @results = results.map do |r|
204
+ {row_id: r[:value], key: r[:key]}
205
+ end
206
+
207
+ # Filter by inclusivity
208
+ if !inclusive_start && start_key
209
+ @results.reject! { |r| r[:key] == start_key }
210
+ end
211
+ if !inclusive_end && end_key
212
+ @results.reject! { |r| r[:key] == end_key }
213
+ end
214
+ else
215
+ # For non-BTree indexes, fall back to full scan with filtering
216
+ execute_full_scan_with_range_filter(start_key, end_key, inclusive_start, inclusive_end)
217
+ end
218
+
219
+ apply_filter if @filter
220
+ @results
221
+ end
222
+
223
+ def execute_prefix_scan
224
+ prefix = @conditions[:prefix]
225
+ return [] unless prefix
226
+
227
+ if @index.type == :btree
228
+ # For prefix scan, find keys starting with prefix
229
+ start_key = prefix
230
+ end_key = prefix + "\xFF\xFF\xFF" # Max possible suffix
231
+
232
+ results = @index.range_search(start_key, end_key)
233
+ @stats[:rows_scanned] = results.size
234
+ @stats[:index_pages_accessed] = (results.size / 20.0).ceil + 1
235
+
236
+ @results = results.map do |r|
237
+ {row_id: r[:value], key: r[:key]}
238
+ end
239
+ else
240
+ # For hash indexes, full scan with prefix filter
241
+ execute_full_scan_with_prefix_filter(prefix)
242
+ end
243
+
244
+ apply_filter if @filter
245
+ @results
246
+ end
247
+
248
+ def execute_multi_key_scan
249
+ keys = @conditions[:keys]
250
+ return [] unless keys.is_a?(Array) && keys.any?
251
+
252
+ @stats[:keys_checked] = keys.size
253
+
254
+ all_row_ids = []
255
+ keys.each do |key|
256
+ if @index.type == :btree || @index.type == :hash
257
+ row_ids = @index.search(key)
258
+ all_row_ids.concat(row_ids.map { |rid| {row_id: rid, key: key} })
259
+ end
260
+ end
261
+
262
+ # Remove duplicates if needed
263
+ if @conditions[:distinct_keys] != false
264
+ seen = Set.new
265
+ all_row_ids = all_row_ids.select do |entry|
266
+ key = entry[:key]
267
+ if seen.include?(key)
268
+ false
269
+ else
270
+ seen.add(key)
271
+ true
272
+ end
273
+ end
274
+ end
275
+
276
+ @stats[:rows_scanned] = all_row_ids.size
277
+ @stats[:index_pages_accessed] = all_row_ids.size
278
+ @results = all_row_ids
279
+
280
+ apply_filter if @filter
281
+ @results
282
+ end
283
+
284
+ def execute_full_scan
285
+ @results = []
286
+
287
+ if @index.type == :btree
288
+ # Use cursor for B-Tree
289
+ @cursor = BTreeCursor.new(@index)
290
+ @cursor.first
291
+
292
+ page_count = 0
293
+ while (entry = @cursor.next)
294
+ @stats[:rows_scanned] += 1
295
+ @stats[:keys_checked] += 1
296
+ page_count += 1 if page_count % 50 == 0
297
+
298
+ # Apply filter early if possible
299
+ if @filter
300
+ next unless apply_filter_to_entry(entry)
301
+ end
302
+
303
+ @results << {row_id: entry[:value], key: entry[:key]}
304
+ break if @limit && @results.size >= @limit + @offset
305
+ end
306
+ @stats[:index_pages_accessed] = (page_count / 50.0).ceil + 1
307
+ else
308
+ # For hash indexes, scan all buckets
309
+ @results = scan_hash_index_full
310
+ @stats[:rows_scanned] = @results.size
311
+ @stats[:index_pages_accessed] = 1
312
+ end
313
+
314
+ apply_filter if @filter && !@results.empty?
315
+ @results
316
+ end
317
+
318
+ def execute_full_scan_with_range_filter(start_key, end_key, inclusive_start, inclusive_end)
319
+ @results = []
320
+
321
+ if @index.type == :btree
322
+ @cursor = BTreeCursor.new(@index)
323
+ @cursor.seek(start_key) if start_key
324
+ @cursor.first unless start_key
325
+
326
+ while (entry = @cursor.next)
327
+ break if end_key && entry[:key] > end_key
328
+
329
+ @stats[:rows_scanned] += 1
330
+ @stats[:keys_checked] += 1
331
+
332
+ # Check inclusivity
333
+ if start_key && entry[:key] == start_key && !inclusive_start
334
+ next
335
+ end
336
+ if end_key && entry[:key] == end_key && !inclusive_end
337
+ next
338
+ end
339
+
340
+ if @filter
341
+ next unless apply_filter_to_entry(entry)
342
+ end
343
+
344
+ @results << {row_id: entry[:value], key: entry[:key]}
345
+ break if @limit && @results.size >= @limit + @offset
346
+ end
347
+ @stats[:index_pages_accessed] = (results.size / 20.0).ceil + 1
348
+ else
349
+ # Hash index - scan all and filter
350
+ scan_hash_index_full.each do |entry|
351
+ next if start_key && entry[:key] < start_key
352
+ next if end_key && entry[:key] > end_key
353
+
354
+ if start_key && entry[:key] == start_key && !inclusive_start
355
+ next
356
+ end
357
+ if end_key && entry[:key] == end_key && !inclusive_end
358
+ next
359
+ end
360
+
361
+ @results << entry
362
+ end
363
+ end
364
+
365
+ @results
366
+ end
367
+
368
+ def execute_full_scan_with_prefix_filter(prefix)
369
+ @results = []
370
+
371
+ if @index.type == :btree
372
+ @cursor = BTreeCursor.new(@index)
373
+ @cursor.first
374
+
375
+ while (entry = @cursor.next)
376
+ key_str = entry[:key].to_s
377
+ next unless key_str.start_with?(prefix.to_s)
378
+
379
+ @stats[:rows_scanned] += 1
380
+ @stats[:keys_checked] += 1
381
+
382
+ if @filter
383
+ next unless apply_filter_to_entry(entry)
384
+ end
385
+
386
+ @results << {row_id: entry[:value], key: entry[:key]}
387
+ break if @limit && @results.size >= @limit + @offset
388
+ end
389
+ else
390
+ scan_hash_index_full.each do |entry|
391
+ key_str = entry[:key].to_s
392
+ next unless key_str.start_with?(prefix.to_s)
393
+ @results << entry
394
+ end
395
+ end
396
+
397
+ @results
398
+ end
399
+
400
+ def scan_hash_index_full
401
+ # Access the internal hash table of the hash index
402
+ # This is a production implementation that iterates through all buckets
403
+ results = []
404
+ hash_table = @index.instance_variable_get(:@hash_table)
405
+
406
+ hash_table&.each do |_bucket_key, bucket|
407
+ bucket.each do |entry|
408
+ @stats[:rows_scanned] += 1
409
+ @stats[:keys_checked] += 1
410
+
411
+ if @filter
412
+ next unless apply_filter_to_entry(entry)
413
+ end
414
+
415
+ results << {row_id: entry[:row_id], key: entry[:key]}
416
+ break if @limit && results.size >= @limit + @offset
417
+ end
418
+ break if @limit && results.size >= @limit + @offset
419
+ end
420
+
421
+ results
422
+ end
423
+
424
+ def apply_filter
425
+ return unless @filter && @results.any?
426
+
427
+ @results.select! do |result|
428
+ apply_filter_to_entry(result)
429
+ end
430
+ end
431
+
432
+ def apply_filter_to_entry(entry)
433
+ return true unless @filter
434
+
435
+ key = entry[:key] || entry[:row_id]
436
+ value = entry[:row_id] || entry[:value]
437
+
438
+ @filter.call(key, value)
439
+ end
440
+
441
+ def distinct_results(results)
442
+ seen = Set.new
443
+ results.select do |result|
444
+ key = result[:key]
445
+ if seen.include?(key)
446
+ false
447
+ else
448
+ seen.add(key)
449
+ true
450
+ end
451
+ end
452
+ end
453
+
454
+ def find_internal_hash_table
455
+ # Try to get hash table from index
456
+ if @index.respond_to?(:hash_table)
457
+ @index.hash_table
458
+ elsif @index.instance_variable_defined?(:@hash_table)
459
+ @index.instance_variable_get(:@hash_table)
460
+ else
461
+ {}
462
+ end
463
+ end
464
+ end
465
+ end
466
+ end