rubydb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +20 -0
  3. data/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  4. data/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  5. data/.github/ISSUE_TEMPLATE/performance.yml +0 -0
  6. data/.github/ISSUE_TEMPLATE/security.yml +0 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  8. data/.github/dependabot.yml +0 -0
  9. data/.github/workflows/benchmark.yml +26 -0
  10. data/.github/workflows/compatibility.yml +63 -0
  11. data/.github/workflows/fuzz.yml +33 -0
  12. data/.github/workflows/lint.yml +21 -0
  13. data/.github/workflows/operations.yml +24 -0
  14. data/.github/workflows/production-validation.yml +111 -0
  15. data/.github/workflows/release.yml +77 -0
  16. data/.github/workflows/security.yml +37 -0
  17. data/.github/workflows/test.yml +26 -0
  18. data/.github/workflows/workload.yml +58 -0
  19. data/.gitignore +43 -0
  20. data/.rspec +4 -0
  21. data/.rubocop.yml +44 -0
  22. data/.ruby-version +1 -0
  23. data/.standard.yml +14 -0
  24. data/ARCHITECTURE.md +21 -0
  25. data/CHANGELOG.md +31 -0
  26. data/CODE_OF_CONDUCT.md +13 -0
  27. data/CONTRIBUTING.md +29 -0
  28. data/GOVERNANCE.md +16 -0
  29. data/Gemfile +17 -0
  30. data/Gemfile.lock +71 -0
  31. data/LICENSE +60 -0
  32. data/README.md +162 -0
  33. data/ROADMAP.md +27 -0
  34. data/Rakefile +71 -0
  35. data/SECURITY.md +54 -0
  36. data/SUPPORT.md +14 -0
  37. data/adapters/activerecord/Gemfile +11 -0
  38. data/adapters/activerecord/README.md +9 -0
  39. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +887 -0
  40. data/adapters/activerecord/rubydb-activerecord.gemspec +27 -0
  41. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +151 -0
  42. data/adapters/ruby/README.md +18 -0
  43. data/adapters/sequel/README.md +11 -0
  44. data/bin/rubydb +19 -0
  45. data/bin/rubydb-server +126 -0
  46. data/config/development.yml +0 -0
  47. data/config/monitoring/prometheus-alerts.yml +39 -0
  48. data/config/production.yml +36 -0
  49. data/config/test.yml +0 -0
  50. data/docs/README.md +71 -0
  51. data/docs/architecture/concurrency.md +14 -0
  52. data/docs/architecture/current-state.md +125 -0
  53. data/docs/architecture/execution-engine.md +25 -0
  54. data/docs/architecture/indexes.md +19 -0
  55. data/docs/architecture/mvcc.md +19 -0
  56. data/docs/architecture/overview.md +13 -0
  57. data/docs/architecture/pages.md +11 -0
  58. data/docs/architecture/production-roadmap.md +82 -0
  59. data/docs/architecture/query-planner.md +20 -0
  60. data/docs/architecture/recovery.md +18 -0
  61. data/docs/architecture/sql-engine.md +12 -0
  62. data/docs/architecture/storage-engine.md +14 -0
  63. data/docs/architecture/transactions.md +10 -0
  64. data/docs/architecture/wal.md +28 -0
  65. data/docs/cli-cheatsheet.md +98 -0
  66. data/docs/cli.md +275 -0
  67. data/docs/contributing/architecture.md +9 -0
  68. data/docs/contributing/benchmarking.md +14 -0
  69. data/docs/contributing/development.md +16 -0
  70. data/docs/contributing/release-process.md +49 -0
  71. data/docs/contributing/testing.md +16 -0
  72. data/docs/debugging.md +229 -0
  73. data/docs/developer/branching.md +10 -0
  74. data/docs/developer/database-diff.md +10 -0
  75. data/docs/developer/local-development.md +17 -0
  76. data/docs/developer/snapshots.md +9 -0
  77. data/docs/developer/temporal-data.md +10 -0
  78. data/docs/developer-guide.md +297 -0
  79. data/docs/getting-started/first-database.md +16 -0
  80. data/docs/getting-started/first-query.md +13 -0
  81. data/docs/getting-started/installation.md +19 -0
  82. data/docs/getting-started/local-to-production.md +300 -0
  83. data/docs/getting-started/quickstart.md +17 -0
  84. data/docs/getting-started/rails.md +16 -0
  85. data/docs/hardening_backlog.md +93 -0
  86. data/docs/lessons-learned.md +112 -0
  87. data/docs/operations/backups.md +33 -0
  88. data/docs/operations/disaster-recovery.md +31 -0
  89. data/docs/operations/failover.md +30 -0
  90. data/docs/operations/monitoring.md +25 -0
  91. data/docs/operations/production-guide.md +295 -0
  92. data/docs/operations/production-runbook.md +45 -0
  93. data/docs/operations/replication.md +33 -0
  94. data/docs/operations/restore.md +6 -0
  95. data/docs/operations/runbook.md +34 -0
  96. data/docs/operations/upgrades.md +14 -0
  97. data/docs/operations/workload-testing.md +17 -0
  98. data/docs/production-readiness.md +118 -0
  99. data/docs/production_validation.md +150 -0
  100. data/docs/rails/active-record.md +11 -0
  101. data/docs/rails/compatibility-guide.md +90 -0
  102. data/docs/rails/database-yml.md +92 -0
  103. data/docs/rails/installation.md +17 -0
  104. data/docs/rails/migrations.md +17 -0
  105. data/docs/rails/production.md +82 -0
  106. data/docs/rails/troubleshooting.md +18 -0
  107. data/docs/release.md +59 -0
  108. data/docs/server/architecture.md +10 -0
  109. data/docs/server/authentication.md +10 -0
  110. data/docs/server/configuration.md +16 -0
  111. data/docs/server/connection-pooling.md +10 -0
  112. data/docs/server/deployment.md +10 -0
  113. data/docs/server/protocol.md +12 -0
  114. data/docs/sql/compatibility-guide.md +82 -0
  115. data/docs/sql/compatibility.md +39 -0
  116. data/docs/sql/data-types.md +10 -0
  117. data/docs/sql/functions.md +9 -0
  118. data/docs/sql/joins.md +9 -0
  119. data/docs/sql/operators.md +9 -0
  120. data/docs/sql/sqlite-compatibility.md +21 -0
  121. data/docs/sql/syntax.md +10 -0
  122. data/docs/sql/transactions.md +10 -0
  123. data/docs/troubleshooting.md +244 -0
  124. data/exe/rubydb +23 -0
  125. data/lib/rubydb/backup/archive.rb +334 -0
  126. data/lib/rubydb/backup/backup.rb +401 -0
  127. data/lib/rubydb/backup/incremental.rb +353 -0
  128. data/lib/rubydb/backup/restore.rb +290 -0
  129. data/lib/rubydb/backup/snapshot.rb +267 -0
  130. data/lib/rubydb/backup/verification.rb +279 -0
  131. data/lib/rubydb/branching/branch.rb +181 -0
  132. data/lib/rubydb/branching/branch_manager.rb +311 -0
  133. data/lib/rubydb/branching/branch_metadata.rb +140 -0
  134. data/lib/rubydb/branching/checkout.rb +166 -0
  135. data/lib/rubydb/branching/copy_on_write.rb +272 -0
  136. data/lib/rubydb/branching/diff.rb +138 -0
  137. data/lib/rubydb/branching/merge.rb +285 -0
  138. data/lib/rubydb/build_info.rb +15 -0
  139. data/lib/rubydb/catalog/catalog.rb +391 -0
  140. data/lib/rubydb/catalog/column.rb +112 -0
  141. data/lib/rubydb/catalog/constraint.rb +180 -0
  142. data/lib/rubydb/catalog/database.rb +184 -0
  143. data/lib/rubydb/catalog/index.rb +97 -0
  144. data/lib/rubydb/catalog/schema.rb +103 -0
  145. data/lib/rubydb/catalog/sequence.rb +90 -0
  146. data/lib/rubydb/catalog/system_catalog.rb +698 -0
  147. data/lib/rubydb/catalog/table.rb +178 -0
  148. data/lib/rubydb/catalog/trigger.rb +102 -0
  149. data/lib/rubydb/catalog/view.rb +66 -0
  150. data/lib/rubydb/cli/application.rb +163 -0
  151. data/lib/rubydb/cli/commands/backup.rb +81 -0
  152. data/lib/rubydb/cli/commands/branch.rb +72 -0
  153. data/lib/rubydb/cli/commands/checkout.rb +54 -0
  154. data/lib/rubydb/cli/commands/create.rb +58 -0
  155. data/lib/rubydb/cli/commands/diff.rb +77 -0
  156. data/lib/rubydb/cli/commands/doctor.rb +74 -0
  157. data/lib/rubydb/cli/commands/drop.rb +57 -0
  158. data/lib/rubydb/cli/commands/init.rb +102 -0
  159. data/lib/rubydb/cli/commands/inspect.rb +95 -0
  160. data/lib/rubydb/cli/commands/merge.rb +63 -0
  161. data/lib/rubydb/cli/commands/migrate.rb +62 -0
  162. data/lib/rubydb/cli/commands/restart.rb +39 -0
  163. data/lib/rubydb/cli/commands/restore.rb +121 -0
  164. data/lib/rubydb/cli/commands/shell.rb +365 -0
  165. data/lib/rubydb/cli/commands/snapshot.rb +79 -0
  166. data/lib/rubydb/cli/commands/start.rb +82 -0
  167. data/lib/rubydb/cli/commands/status.rb +92 -0
  168. data/lib/rubydb/cli/commands/stop.rb +47 -0
  169. data/lib/rubydb/cli/commands/vacuum.rb +58 -0
  170. data/lib/rubydb/cli/formatter.rb +221 -0
  171. data/lib/rubydb/cli/output.rb +168 -0
  172. data/lib/rubydb/client/client.rb +304 -0
  173. data/lib/rubydb/client/connection.rb +415 -0
  174. data/lib/rubydb/client/connection_pool.rb +168 -0
  175. data/lib/rubydb/client/connection_url.rb +96 -0
  176. data/lib/rubydb/client/prepared_statement.rb +60 -0
  177. data/lib/rubydb/client/result.rb +123 -0
  178. data/lib/rubydb/client/statement.rb +52 -0
  179. data/lib/rubydb/client/transaction.rb +130 -0
  180. data/lib/rubydb/concurrency/concurrency.rb +19 -0
  181. data/lib/rubydb/concurrency/deadlock_detector.rb +150 -0
  182. data/lib/rubydb/concurrency/latch.rb +101 -0
  183. data/lib/rubydb/concurrency/lock_graph.rb +165 -0
  184. data/lib/rubydb/concurrency/mutex.rb +183 -0
  185. data/lib/rubydb/concurrency/rw_lock.rb +180 -0
  186. data/lib/rubydb/concurrency/scheduler.rb +250 -0
  187. data/lib/rubydb/concurrency/worker_pool.rb +143 -0
  188. data/lib/rubydb/configuration/config.rb +170 -0
  189. data/lib/rubydb/configuration/defaults.rb +179 -0
  190. data/lib/rubydb/configuration/environment.rb +152 -0
  191. data/lib/rubydb/configuration/parser.rb +185 -0
  192. data/lib/rubydb/configuration/validation.rb +221 -0
  193. data/lib/rubydb/constants.rb +74 -0
  194. data/lib/rubydb/constraints/check.rb +181 -0
  195. data/lib/rubydb/constraints/constraint.rb +101 -0
  196. data/lib/rubydb/constraints/foreign_key.rb +130 -0
  197. data/lib/rubydb/constraints/not_null.rb +64 -0
  198. data/lib/rubydb/constraints/primary_key.rb +99 -0
  199. data/lib/rubydb/constraints/unique.rb +108 -0
  200. data/lib/rubydb/constraints/validator.rb +350 -0
  201. data/lib/rubydb/errors/authentication_error.rb +10 -0
  202. data/lib/rubydb/errors/authorization_error.rb +23 -0
  203. data/lib/rubydb/errors/client_error.rb +10 -0
  204. data/lib/rubydb/errors/configuration_error.rb +10 -0
  205. data/lib/rubydb/errors/connection_error.rb +10 -0
  206. data/lib/rubydb/errors/constraint_error.rb +23 -0
  207. data/lib/rubydb/errors/corruption_error.rb +10 -0
  208. data/lib/rubydb/errors/database_error.rb +10 -0
  209. data/lib/rubydb/errors/error.rb +20 -0
  210. data/lib/rubydb/errors/execution_error.rb +10 -0
  211. data/lib/rubydb/errors/parser_error.rb +10 -0
  212. data/lib/rubydb/errors/recovery_error.rb +10 -0
  213. data/lib/rubydb/errors/replication_error.rb +10 -0
  214. data/lib/rubydb/errors/server_error.rb +6 -0
  215. data/lib/rubydb/errors/storage_error.rb +10 -0
  216. data/lib/rubydb/errors/transaction_error.rb +10 -0
  217. data/lib/rubydb/execution/aggregate_executor.rb +138 -0
  218. data/lib/rubydb/execution/delete_executor.rb +112 -0
  219. data/lib/rubydb/execution/distinct_executor.rb +135 -0
  220. data/lib/rubydb/execution/executor.rb +1192 -0
  221. data/lib/rubydb/execution/expression.rb +193 -0
  222. data/lib/rubydb/execution/index_scan.rb +142 -0
  223. data/lib/rubydb/execution/insert_executor.rb +217 -0
  224. data/lib/rubydb/execution/join_executor.rb +249 -0
  225. data/lib/rubydb/execution/limit_executor.rb +85 -0
  226. data/lib/rubydb/execution/optimizer.rb +215 -0
  227. data/lib/rubydb/execution/plan.rb +361 -0
  228. data/lib/rubydb/execution/planner.rb +546 -0
  229. data/lib/rubydb/execution/predicate.rb +235 -0
  230. data/lib/rubydb/execution/scan.rb +49 -0
  231. data/lib/rubydb/execution/sequential_scan.rb +63 -0
  232. data/lib/rubydb/execution/sort_executor.rb +185 -0
  233. data/lib/rubydb/execution/update_executor.rb +162 -0
  234. data/lib/rubydb/functions/aggregate.rb +70 -0
  235. data/lib/rubydb/functions/date_functions.rb +278 -0
  236. data/lib/rubydb/functions/function.rb +85 -0
  237. data/lib/rubydb/functions/json_functions.rb +215 -0
  238. data/lib/rubydb/functions/numeric_functions.rb +346 -0
  239. data/lib/rubydb/functions/scalar.rb +52 -0
  240. data/lib/rubydb/functions/string_functions.rb +383 -0
  241. data/lib/rubydb/functions/system_functions.rb +246 -0
  242. data/lib/rubydb/history/as_of.rb +238 -0
  243. data/lib/rubydb/history/change.rb +105 -0
  244. data/lib/rubydb/history/history.rb +131 -0
  245. data/lib/rubydb/history/history_manager.rb +229 -0
  246. data/lib/rubydb/history/temporal_query.rb +202 -0
  247. data/lib/rubydb/history/timeline.rb +144 -0
  248. data/lib/rubydb/indexes/btree.rb +186 -0
  249. data/lib/rubydb/indexes/btree_cursor.rb +258 -0
  250. data/lib/rubydb/indexes/btree_node.rb +385 -0
  251. data/lib/rubydb/indexes/hash_index.rb +150 -0
  252. data/lib/rubydb/indexes/index.rb +71 -0
  253. data/lib/rubydb/indexes/index_manager.rb +406 -0
  254. data/lib/rubydb/indexes/index_scan.rb +470 -0
  255. data/lib/rubydb/migrations/migration.rb +254 -0
  256. data/lib/rubydb/migrations/migration_lock.rb +146 -0
  257. data/lib/rubydb/migrations/migration_manager.rb +176 -0
  258. data/lib/rubydb/migrations/migration_version.rb +71 -0
  259. data/lib/rubydb/migrations/schema_diff.rb +211 -0
  260. data/lib/rubydb/migrations/schema_version.rb +64 -0
  261. data/lib/rubydb/monitoring/events.rb +160 -0
  262. data/lib/rubydb/monitoring/health.rb +222 -0
  263. data/lib/rubydb/monitoring/logger.rb +193 -0
  264. data/lib/rubydb/monitoring/metrics.rb +359 -0
  265. data/lib/rubydb/monitoring/performance.rb +176 -0
  266. data/lib/rubydb/monitoring/statistics.rb +170 -0
  267. data/lib/rubydb/mvcc/garbage_collector.rb +199 -0
  268. data/lib/rubydb/mvcc/mvcc.rb +16 -0
  269. data/lib/rubydb/mvcc/snapshot.rb +147 -0
  270. data/lib/rubydb/mvcc/vacuum.rb +180 -0
  271. data/lib/rubydb/mvcc/version.rb +106 -0
  272. data/lib/rubydb/mvcc/version_store.rb +398 -0
  273. data/lib/rubydb/mvcc/visibility.rb +109 -0
  274. data/lib/rubydb/protocol/capabilities.rb +125 -0
  275. data/lib/rubydb/protocol/decoder.rb +145 -0
  276. data/lib/rubydb/protocol/encoder.rb +136 -0
  277. data/lib/rubydb/protocol/handshake.rb +305 -0
  278. data/lib/rubydb/protocol/message.rb +121 -0
  279. data/lib/rubydb/protocol/protocol.rb +277 -0
  280. data/lib/rubydb/protocol/version.rb +54 -0
  281. data/lib/rubydb/rails/adapter.rb +239 -0
  282. data/lib/rubydb/rails/connection.rb +314 -0
  283. data/lib/rubydb/rails/database_statements.rb +122 -0
  284. data/lib/rubydb/rails/migration.rb +131 -0
  285. data/lib/rubydb/rails/quoting.rb +109 -0
  286. data/lib/rubydb/rails/result.rb +120 -0
  287. data/lib/rubydb/rails/schema_statements.rb +339 -0
  288. data/lib/rubydb/rails/transaction.rb +105 -0
  289. data/lib/rubydb/rails/type.rb +126 -0
  290. data/lib/rubydb/recovery/checkpoint.rb +257 -0
  291. data/lib/rubydb/recovery/consistency.rb +467 -0
  292. data/lib/rubydb/recovery/corruption_detector.rb +5 -0
  293. data/lib/rubydb/recovery/crash_recovery.rb +387 -0
  294. data/lib/rubydb/recovery/recovery_manager.rb +206 -0
  295. data/lib/rubydb/recovery/redo.rb +237 -0
  296. data/lib/rubydb/recovery/undo.rb +206 -0
  297. data/lib/rubydb/replication/failover.rb +5 -0
  298. data/lib/rubydb/replication/fencing.rb +63 -0
  299. data/lib/rubydb/replication/primary.rb +450 -0
  300. data/lib/rubydb/replication/replica.rb +384 -0
  301. data/lib/rubydb/replication/replication_log.rb +200 -0
  302. data/lib/rubydb/replication/replication_manager.rb +308 -0
  303. data/lib/rubydb/replication/replication_slot.rb +295 -0
  304. data/lib/rubydb/replication/replication_stream.rb +201 -0
  305. data/lib/rubydb/rubydb.rb +560 -0
  306. data/lib/rubydb/security/access_control.rb +254 -0
  307. data/lib/rubydb/security/audit_log.rb +213 -0
  308. data/lib/rubydb/security/authentication.rb +302 -0
  309. data/lib/rubydb/security/authorization.rb +282 -0
  310. data/lib/rubydb/security/credentials.rb +196 -0
  311. data/lib/rubydb/security/password.rb +215 -0
  312. data/lib/rubydb/security/permissions.rb +74 -0
  313. data/lib/rubydb/security/role.rb +101 -0
  314. data/lib/rubydb/security/user.rb +86 -0
  315. data/lib/rubydb/server/connection.rb +366 -0
  316. data/lib/rubydb/server/connection_pool.rb +193 -0
  317. data/lib/rubydb/server/lifecycle.rb +228 -0
  318. data/lib/rubydb/server/listener.rb +136 -0
  319. data/lib/rubydb/server/request_handler.rb +276 -0
  320. data/lib/rubydb/server/server.rb +364 -0
  321. data/lib/rubydb/server/session.rb +369 -0
  322. data/lib/rubydb/server/worker.rb +210 -0
  323. data/lib/rubydb/server/worker_pool.rb +168 -0
  324. data/lib/rubydb/sql/ast/alter_table.rb +169 -0
  325. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -0
  326. data/lib/rubydb/sql/ast/commit.rb +37 -0
  327. data/lib/rubydb/sql/ast/constraint.rb +83 -0
  328. data/lib/rubydb/sql/ast/create_database.rb +41 -0
  329. data/lib/rubydb/sql/ast/create_index.rb +61 -0
  330. data/lib/rubydb/sql/ast/create_schema.rb +52 -0
  331. data/lib/rubydb/sql/ast/create_table.rb +187 -0
  332. data/lib/rubydb/sql/ast/delete.rb +54 -0
  333. data/lib/rubydb/sql/ast/drop_database.rb +41 -0
  334. data/lib/rubydb/sql/ast/drop_index.rb +41 -0
  335. data/lib/rubydb/sql/ast/drop_schema.rb +49 -0
  336. data/lib/rubydb/sql/ast/drop_table.rb +49 -0
  337. data/lib/rubydb/sql/ast/explain.rb +64 -0
  338. data/lib/rubydb/sql/ast/expression.rb +604 -0
  339. data/lib/rubydb/sql/ast/insert.rb +82 -0
  340. data/lib/rubydb/sql/ast/node.rb +42 -0
  341. data/lib/rubydb/sql/ast/rollback.rb +63 -0
  342. data/lib/rubydb/sql/ast/savepoint.rb +59 -0
  343. data/lib/rubydb/sql/ast/select.rb +88 -0
  344. data/lib/rubydb/sql/ast/set_operation.rb +20 -0
  345. data/lib/rubydb/sql/ast/trigger.rb +29 -0
  346. data/lib/rubydb/sql/ast/update.rb +88 -0
  347. data/lib/rubydb/sql/ast/vacuum.rb +19 -0
  348. data/lib/rubydb/sql/ast/view.rb +32 -0
  349. data/lib/rubydb/sql/ast/with.rb +32 -0
  350. data/lib/rubydb/sql/grammar.rb +86 -0
  351. data/lib/rubydb/sql/keywords.rb +156 -0
  352. data/lib/rubydb/sql/lexer.rb +214 -0
  353. data/lib/rubydb/sql/operators.rb +100 -0
  354. data/lib/rubydb/sql/parser.rb +1185 -0
  355. data/lib/rubydb/sql/planner/analyzer.rb +302 -0
  356. data/lib/rubydb/sql/planner/binder.rb +550 -0
  357. data/lib/rubydb/sql/planner/type_checker.rb +431 -0
  358. data/lib/rubydb/sql/token.rb +210 -0
  359. data/lib/rubydb/storage/buffer_frame.rb +44 -0
  360. data/lib/rubydb/storage/buffer_pool.rb +155 -0
  361. data/lib/rubydb/storage/database_lock.rb +74 -0
  362. data/lib/rubydb/storage/deserializer.rb +342 -0
  363. data/lib/rubydb/storage/engine.rb +2338 -0
  364. data/lib/rubydb/storage/file_manager.rb +187 -0
  365. data/lib/rubydb/storage/free_space_map.rb +81 -0
  366. data/lib/rubydb/storage/page.rb +94 -0
  367. data/lib/rubydb/storage/page_allocator.rb +855 -0
  368. data/lib/rubydb/storage/page_header.rb +67 -0
  369. data/lib/rubydb/storage/page_manager.rb +131 -0
  370. data/lib/rubydb/storage/record.rb +58 -0
  371. data/lib/rubydb/storage/row.rb +78 -0
  372. data/lib/rubydb/storage/serializer.rb +51 -0
  373. data/lib/rubydb/storage/storage_layout.rb +151 -0
  374. data/lib/rubydb/storage/storage_manager.rb +114 -0
  375. data/lib/rubydb/storage/tuple.rb +461 -0
  376. data/lib/rubydb/storage/visibility_map.rb +984 -0
  377. data/lib/rubydb/transactions/commit_manager.rb +220 -0
  378. data/lib/rubydb/transactions/isolation.rb +98 -0
  379. data/lib/rubydb/transactions/lock.rb +76 -0
  380. data/lib/rubydb/transactions/lock_manager.rb +362 -0
  381. data/lib/rubydb/transactions/savepoint.rb +143 -0
  382. data/lib/rubydb/transactions/transaction.rb +215 -0
  383. data/lib/rubydb/transactions/transaction_id.rb +84 -0
  384. data/lib/rubydb/transactions/transaction_log.rb +257 -0
  385. data/lib/rubydb/transactions/transaction_manager.rb +435 -0
  386. data/lib/rubydb/types/bigint.rb +36 -0
  387. data/lib/rubydb/types/blob.rb +37 -0
  388. data/lib/rubydb/types/boolean.rb +34 -0
  389. data/lib/rubydb/types/date.rb +39 -0
  390. data/lib/rubydb/types/decimal.rb +48 -0
  391. data/lib/rubydb/types/float.rb +34 -0
  392. data/lib/rubydb/types/integer.rb +36 -0
  393. data/lib/rubydb/types/json.rb +41 -0
  394. data/lib/rubydb/types/null.rb +34 -0
  395. data/lib/rubydb/types/smallint.rb +36 -0
  396. data/lib/rubydb/types/text.rb +37 -0
  397. data/lib/rubydb/types/time.rb +46 -0
  398. data/lib/rubydb/types/timestamp.rb +39 -0
  399. data/lib/rubydb/types/type.rb +119 -0
  400. data/lib/rubydb/types/uuid.rb +47 -0
  401. data/lib/rubydb/types/varchar.rb +37 -0
  402. data/lib/rubydb/version.rb +33 -0
  403. data/lib/rubydb/wal/archive.rb +193 -0
  404. data/lib/rubydb/wal/checkpoint.rb +183 -0
  405. data/lib/rubydb/wal/lsn.rb +94 -0
  406. data/lib/rubydb/wal/reader.rb +260 -0
  407. data/lib/rubydb/wal/record.rb +105 -0
  408. data/lib/rubydb/wal/segment.rb +193 -0
  409. data/lib/rubydb/wal/wal.rb +452 -0
  410. data/lib/rubydb/wal/writer.rb +236 -0
  411. data/lib/rubydb.rb +7 -0
  412. data/packaging/docker/Dockerfile +36 -0
  413. data/packaging/docker/docker-compose.failover.yml +43 -0
  414. data/packaging/docker/entrypoint.sh +15 -0
  415. data/packaging/homebrew/rubydb.rb +19 -0
  416. data/packaging/systemd/rubydb.service +22 -0
  417. data/rubydb.gemspec +57 -0
  418. data/scripts/benchmark +7 -0
  419. data/scripts/durability_drill +37 -0
  420. data/scripts/format +0 -0
  421. data/scripts/fuzz +63 -0
  422. data/scripts/lint +0 -0
  423. data/scripts/release +50 -0
  424. data/scripts/release_check +43 -0
  425. data/scripts/replication_failover_drill +250 -0
  426. data/scripts/replication_network_failover_drill +255 -0
  427. data/scripts/restore_drill +45 -0
  428. data/scripts/security +0 -0
  429. data/scripts/setup +0 -0
  430. data/scripts/test +0 -0
  431. data/scripts/test-chaos +0 -0
  432. data/scripts/test-crash +0 -0
  433. data/scripts/test-integration +0 -0
  434. data/scripts/test-unit +0 -0
  435. data/visibility_map.json +1 -0
  436. metadata +538 -0
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module RubyDB
6
+ module WAL
7
+ # Segment - A WAL segment file
8
+ class Segment
9
+ attr_reader :segment_id, :path, :size, :max_size
10
+ attr_reader :first_lsn, :last_lsn, :record_count
11
+
12
+ DEFAULT_MAX_SIZE = 16 * 1024 * 1024 # 16MB
13
+ FRAME_HEADER_SIZE = 4
14
+
15
+ def initialize(segment_id, wal_dir, max_size = DEFAULT_MAX_SIZE)
16
+ @segment_id = segment_id
17
+ @wal_dir = wal_dir
18
+ @max_size = max_size
19
+ @path = File.join(wal_dir, "wal_#{segment_id.to_s.rjust(8, '0')}.log")
20
+ @size = 0
21
+ @record_count = 0
22
+ @first_lsn = nil
23
+ @last_lsn = nil
24
+ @file = nil
25
+ @is_open = false
26
+ @is_full = false
27
+ @lock = Mutex.new
28
+ end
29
+
30
+ def open
31
+ @lock.synchronize do
32
+ return if @is_open
33
+
34
+ FileUtils.mkdir_p(@wal_dir) unless Dir.exist?(@wal_dir)
35
+
36
+ if File.exist?(@path)
37
+ @file = File.open(@path, "a+b")
38
+ @size = File.size(@path)
39
+ @record_count = count_records
40
+ else
41
+ @file = File.open(@path, "w+b")
42
+ @size = 0
43
+ @record_count = 0
44
+ end
45
+
46
+ @is_open = true
47
+ self
48
+ end
49
+ end
50
+
51
+ def close
52
+ @lock.synchronize do
53
+ return unless @is_open
54
+
55
+ @file.close if @file
56
+ @is_open = false
57
+ true
58
+ end
59
+ end
60
+
61
+ def append(record_data, lsn)
62
+ @lock.synchronize do
63
+ raise "Segment is full" if @is_full
64
+ raise "Segment not open" unless @is_open
65
+
66
+ # Each record is framed as a 4-byte big-endian payload length followed
67
+ # by the checksum+JSON payload. This makes records independently
68
+ # readable and lets recovery safely stop at a torn tail record.
69
+ frame = [record_data.bytesize].pack("N") + record_data
70
+
71
+ # Write record
72
+ @file.seek(0, IO::SEEK_END)
73
+ @file.write(frame)
74
+ @file.flush
75
+
76
+ @size += frame.bytesize
77
+ @record_count += 1
78
+
79
+ @first_lsn ||= lsn
80
+ @last_lsn = lsn
81
+
82
+ # Check if segment is full
83
+ if @size >= @max_size
84
+ @is_full = true
85
+ return :full
86
+ end
87
+
88
+ :ok
89
+ end
90
+ end
91
+
92
+ def read_at(offset, length)
93
+ @lock.synchronize do
94
+ raise "Segment not open" unless @is_open
95
+
96
+ @file.seek(offset)
97
+ @file.read(length)
98
+ end
99
+ end
100
+
101
+ def read_record_at(offset)
102
+ @lock.synchronize do
103
+ raise "Segment not open" unless @is_open
104
+
105
+ @file.seek(offset)
106
+ header = @file.read(FRAME_HEADER_SIZE)
107
+ return nil if header.nil? || header.empty?
108
+ return nil if header.bytesize < FRAME_HEADER_SIZE
109
+
110
+ payload_size = header.unpack1("N")
111
+ return nil if payload_size < 16
112
+
113
+ payload = @file.read(payload_size)
114
+ return nil if payload.nil? || payload.bytesize < payload_size
115
+
116
+ payload
117
+ end
118
+ end
119
+
120
+ def truncate
121
+ @lock.synchronize do
122
+ return unless @is_open
123
+
124
+ @file.truncate(0)
125
+ @size = 0
126
+ @record_count = 0
127
+ @first_lsn = nil
128
+ @last_lsn = nil
129
+ @is_full = false
130
+ true
131
+ end
132
+ end
133
+
134
+ def full?
135
+ @is_full
136
+ end
137
+
138
+ def open?
139
+ @is_open
140
+ end
141
+
142
+ def exists?
143
+ File.exist?(@path)
144
+ end
145
+
146
+ def delete
147
+ @lock.synchronize do
148
+ close if @is_open
149
+ File.delete(@path) if exists?
150
+ true
151
+ end
152
+ end
153
+
154
+ def archive
155
+ # Move segment to archive
156
+ archived_path = File.join(@wal_dir, "archive", "wal_#{@segment_id.to_s.rjust(8, '0')}.log")
157
+ FileUtils.mkdir_p(File.dirname(archived_path))
158
+ FileUtils.mv(@path, archived_path)
159
+ archived_path
160
+ end
161
+
162
+ def to_s
163
+ "Segment(#{@segment_id}, size=#{@size}, records=#{@record_count}, full=#{@is_full})"
164
+ end
165
+
166
+ def inspect
167
+ to_s
168
+ end
169
+
170
+ private
171
+
172
+ def count_records
173
+ count = 0
174
+ offset = 0
175
+ loop do
176
+ @file.seek(offset)
177
+ header = @file.read(FRAME_HEADER_SIZE)
178
+ break if header.nil? || header.bytesize < FRAME_HEADER_SIZE
179
+
180
+ payload_size = header.unpack1("N")
181
+ break if payload_size < 16
182
+
183
+ payload = @file.read(payload_size)
184
+ break if payload.nil? || payload.bytesize < payload_size
185
+
186
+ count += 1
187
+ offset += FRAME_HEADER_SIZE + payload_size
188
+ end
189
+ count
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,452 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "time"
5
+
6
+ # Import all WAL components
7
+ require_relative "lsn"
8
+ require_relative "record"
9
+ require_relative "segment"
10
+ require_relative "writer"
11
+ require_relative "reader"
12
+ require_relative "checkpoint"
13
+ require_relative "archive"
14
+ require_relative "../errors/recovery_error"
15
+
16
+ module RubyDB
17
+ module WAL
18
+ # WAL - Main Write-Ahead Log interface
19
+ class WAL
20
+ attr_reader :writer, :reader, :checkpoint, :archive, :stats
21
+ attr_reader :wal_dir, :current_lsn
22
+
23
+ def initialize(wal_dir, config = {})
24
+ @wal_dir = wal_dir
25
+ @config = config
26
+ @engine = config[:engine]
27
+ @stats = {
28
+ writes: 0,
29
+ reads: 0,
30
+ checkpoints: 0,
31
+ archives: 0,
32
+ restores: 0,
33
+ errors: 0,
34
+ total_write_time_ms: 0,
35
+ total_read_time_ms: 0,
36
+ avg_write_time_ms: 0,
37
+ avg_read_time_ms: 0
38
+ }
39
+ @lock = Mutex.new
40
+ @running = false
41
+ @shutdown = false
42
+ @current_lsn = LSN.null
43
+
44
+ # Create WAL directory
45
+ FileUtils.mkdir_p(wal_dir) unless Dir.exist?(wal_dir)
46
+
47
+ begin
48
+ # Initialize components
49
+ initialize_components(config)
50
+
51
+ # Start checkpoint if configured
52
+ if config[:auto_checkpoint] != false
53
+ @checkpoint.start
54
+ end
55
+
56
+ # Start recovery if needed
57
+ if config[:recovery] != false
58
+ recover
59
+ end
60
+
61
+ @running = true
62
+ rescue StandardError
63
+ # Recovery can fail after the writer has opened a segment. Close all
64
+ # partially initialized components before re-raising so callers can
65
+ # replace/remove a failed WAL directory on every supported platform.
66
+ @checkpoint&.stop rescue nil
67
+ @writer&.shutdown rescue nil
68
+ @reader&.close rescue nil
69
+ raise
70
+ end
71
+ end
72
+
73
+ def attach_engine(engine)
74
+ @lock.synchronize { @engine = engine }
75
+ self
76
+ end
77
+
78
+ def write(record)
79
+ @lock.synchronize do
80
+ start_time = Time.now
81
+ @stats[:writes] += 1
82
+
83
+ begin
84
+ lsn = @writer.write_record(record)
85
+ @current_lsn = lsn
86
+
87
+ elapsed_ms = (Time.now - start_time) * 1000
88
+ @stats[:total_write_time_ms] += elapsed_ms
89
+ @stats[:avg_write_time_ms] = @stats[:total_write_time_ms] / @stats[:writes]
90
+
91
+ lsn
92
+ rescue => e
93
+ @stats[:errors] += 1
94
+ raise
95
+ end
96
+ end
97
+ end
98
+
99
+ def write_batch(records)
100
+ @lock.synchronize do
101
+ start_time = Time.now
102
+ @stats[:writes] += records.size
103
+
104
+ begin
105
+ result = @writer.write_batch(records)
106
+ @current_lsn = result[:end_lsn] if result[:end_lsn]
107
+
108
+ elapsed_ms = (Time.now - start_time) * 1000
109
+ @stats[:total_write_time_ms] += elapsed_ms
110
+ @stats[:avg_write_time_ms] = @stats[:total_write_time_ms] / @stats[:writes]
111
+
112
+ result
113
+ rescue => e
114
+ @stats[:errors] += 1
115
+ raise
116
+ end
117
+ end
118
+ end
119
+
120
+ def read_all
121
+ @lock.synchronize do
122
+ start_time = Time.now
123
+ @stats[:reads] += 1
124
+
125
+ begin
126
+ records = @reader.read_all
127
+
128
+ elapsed_ms = (Time.now - start_time) * 1000
129
+ @stats[:total_read_time_ms] += elapsed_ms
130
+ @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
131
+
132
+ records
133
+ rescue => e
134
+ @stats[:errors] += 1
135
+ raise
136
+ end
137
+ end
138
+ end
139
+
140
+ def read_from(lsn)
141
+ @lock.synchronize do
142
+ start_time = Time.now
143
+ @stats[:reads] += 1
144
+
145
+ begin
146
+ records = @reader.read_from_lsn(lsn)
147
+
148
+ elapsed_ms = (Time.now - start_time) * 1000
149
+ @stats[:total_read_time_ms] += elapsed_ms
150
+ @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
151
+
152
+ records
153
+ rescue => e
154
+ @stats[:errors] += 1
155
+ raise
156
+ end
157
+ end
158
+ end
159
+
160
+ def read_range(start_lsn, end_lsn)
161
+ @lock.synchronize do
162
+ start_time = Time.now
163
+ @stats[:reads] += 1
164
+
165
+ begin
166
+ records = @reader.read_range(start_lsn, end_lsn)
167
+
168
+ elapsed_ms = (Time.now - start_time) * 1000
169
+ @stats[:total_read_time_ms] += elapsed_ms
170
+ @stats[:avg_read_time_ms] = @stats[:total_read_time_ms] / @stats[:reads]
171
+
172
+ records
173
+ rescue => e
174
+ @stats[:errors] += 1
175
+ raise
176
+ end
177
+ end
178
+ end
179
+
180
+ def create_checkpoint(force = false)
181
+ @lock.synchronize do
182
+ start_time = Time.now
183
+ @stats[:checkpoints] += 1
184
+
185
+ begin
186
+ result = @checkpoint.create_checkpoint(force)
187
+ @stats[:checkpoints] += 1 if result
188
+ result
189
+ rescue => e
190
+ @stats[:errors] += 1
191
+ raise
192
+ end
193
+ end
194
+ end
195
+
196
+ def archive_segment(segment_id)
197
+ @lock.synchronize do
198
+ @stats[:archives] += 1
199
+
200
+ begin
201
+ segment = Segment.new(segment_id, @wal_dir)
202
+ if segment.exists?
203
+ segment.open
204
+ result = @archive.archive_segment(segment)
205
+ segment.close
206
+ result
207
+ else
208
+ false
209
+ end
210
+ rescue => e
211
+ @stats[:errors] += 1
212
+ raise
213
+ end
214
+ end
215
+ end
216
+
217
+ def archive_all_segments
218
+ @lock.synchronize do
219
+ archived = 0
220
+
221
+ Dir.glob(File.join(@wal_dir, "wal_*.log")).each do |file_path|
222
+ if file_path =~ /wal_(\d+)\.log$/
223
+ segment_id = $1.to_i
224
+ if archive_segment(segment_id)
225
+ archived += 1
226
+ end
227
+ end
228
+ end
229
+
230
+ archived
231
+ end
232
+ end
233
+
234
+ def restore_from_archive(segment_id)
235
+ @lock.synchronize do
236
+ @stats[:restores] += 1
237
+
238
+ begin
239
+ result = @archive.restore_archive(segment_id)
240
+ # Reload reader after restore
241
+ @reader.reload if result
242
+ result
243
+ rescue => e
244
+ @stats[:errors] += 1
245
+ raise
246
+ end
247
+ end
248
+ end
249
+
250
+ def restore_latest_archive
251
+ @lock.synchronize do
252
+ archives = @archive.list_archives
253
+ return false if archives.empty?
254
+
255
+ latest = archives.max_by { |a| a[:segment_id] }
256
+ restore_from_archive(latest[:segment_id])
257
+ end
258
+ end
259
+
260
+ def recover
261
+ @lock.synchronize do
262
+ # Find the latest checkpoint
263
+ checkpoint_lsn = @checkpoint.restore_checkpoint(@reader)
264
+
265
+ if checkpoint_lsn
266
+ # Read records from the checkpoint marker. The reader includes
267
+ # the starting frame; the marker is ignored by replay and all
268
+ # subsequent WAL records are applied.
269
+ records = @reader.read_from_lsn(checkpoint_lsn)
270
+
271
+ # Replay committed transactions
272
+ committed_transactions = Set.new
273
+ records.each do |record|
274
+ case record.type
275
+ when :commit
276
+ committed_transactions.add(record.transaction_id)
277
+ when :prepare
278
+ # Handle prepared transactions
279
+ if can_commit_prepared?(record)
280
+ committed_transactions.add(record.transaction_id)
281
+ end
282
+ end
283
+ end
284
+
285
+ # REDO committed transactions
286
+ redo_records = records.select do |r|
287
+ committed_transactions.include?(r.transaction_id) &&
288
+ [:insert, :update, :delete, :create_table, :drop_table].include?(r.type)
289
+ end
290
+
291
+ redo_records.each do |record|
292
+ replay_record(record)
293
+ end
294
+
295
+ # UNDO uncommitted transactions
296
+ uncommitted = records.select do |r|
297
+ !committed_transactions.include?(r.transaction_id) &&
298
+ [:insert, :update, :delete].include?(r.type)
299
+ end
300
+
301
+ uncommitted.reverse_each do |record|
302
+ undo_record(record)
303
+ end
304
+ end
305
+
306
+ true
307
+ end
308
+ end
309
+
310
+ def flush
311
+ @lock.synchronize do
312
+ @writer.flush
313
+ end
314
+ end
315
+
316
+ def sync
317
+ @lock.synchronize do
318
+ @writer.sync
319
+ end
320
+ end
321
+
322
+ def shutdown(wait = true)
323
+ @lock.synchronize do
324
+ @shutdown = true
325
+ @running = false
326
+
327
+ @checkpoint.stop if @checkpoint
328
+ @writer.shutdown(wait)
329
+ @reader.close if @reader
330
+ end
331
+ end
332
+
333
+ def running?
334
+ @running
335
+ end
336
+
337
+ def stats
338
+ @lock.synchronize do
339
+ writer_stats = @writer.stats rescue {}
340
+ reader_stats = @reader.stats rescue {}
341
+ checkpoint_stats = @checkpoint.stats rescue {}
342
+ archive_stats = @archive.stats rescue {}
343
+
344
+ @stats.merge({
345
+ writer: writer_stats,
346
+ reader: reader_stats,
347
+ checkpoint: checkpoint_stats,
348
+ archive: archive_stats,
349
+ current_lsn: @current_lsn.to_s,
350
+ running: @running,
351
+ wal_dir: @wal_dir
352
+ })
353
+ end
354
+ end
355
+
356
+ private
357
+
358
+ def initialize_components(config)
359
+ @writer = Writer.new(@wal_dir, config)
360
+ @reader = Reader.new(@wal_dir, config)
361
+ @checkpoint = Checkpoint.new(@writer, config)
362
+ @archive = Archive.new(@wal_dir, config[:archive_dir], config)
363
+ end
364
+
365
+ def can_commit_prepared?(record)
366
+ data = record&.data
367
+ resources = data && data[:resources]
368
+ return true unless resources
369
+
370
+ Array(resources).all? do |resource|
371
+ case resource
372
+ when String then File.exist?(resource)
373
+ when Hash
374
+ path = resource[:path] || resource["path"] || resource[:file] || resource["file"]
375
+ path && File.exist?(path)
376
+ else
377
+ resource.respond_to?(:available?) && resource.available?
378
+ end
379
+ end
380
+ end
381
+
382
+ def replay_record(record)
383
+ engine = @engine
384
+ raise RubyDB::RecoveryError, "WAL replay requires an attached engine" unless engine
385
+ data = record.data
386
+
387
+ case record.type
388
+ when :insert
389
+ # Insert row
390
+ engine.insert_row(
391
+ data[:table],
392
+ data[:columns],
393
+ data[:values]
394
+ )
395
+ when :update
396
+ # Update row
397
+ engine.update_row(
398
+ data[:table],
399
+ data[:row_id],
400
+ data[:values]
401
+ )
402
+ when :delete
403
+ # Delete row
404
+ engine.delete_row(
405
+ data[:table],
406
+ data[:row_id]
407
+ )
408
+ when :create_table
409
+ # Create table
410
+ engine.create_table(
411
+ data[:table_name],
412
+ data[:columns]
413
+ )
414
+ when :drop_table
415
+ # Drop table
416
+ engine.drop_table(
417
+ data[:table_name]
418
+ )
419
+ end
420
+ end
421
+
422
+ def undo_record(record)
423
+ engine = @engine
424
+ raise RubyDB::RecoveryError, "WAL undo requires an attached engine" unless engine
425
+ data = record.data
426
+
427
+ case record.type
428
+ when :insert
429
+ # Delete the inserted row
430
+ engine.delete_row(
431
+ data[:table],
432
+ data[:row_id]
433
+ )
434
+ when :update
435
+ # Restore old values
436
+ engine.update_row(
437
+ data[:table],
438
+ data[:row_id],
439
+ data[:old_values]
440
+ )
441
+ when :delete
442
+ # Reinsert the deleted row
443
+ engine.insert_row(
444
+ data[:table],
445
+ data[:columns],
446
+ data[:row_data]
447
+ )
448
+ end
449
+ end
450
+ end
451
+ end
452
+ end