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,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "time"
5
+ require "monitor"
6
+ require "json"
7
+ require "fileutils"
8
+
9
+ module RubyDB
10
+ module Replication
11
+ # Replica - Replica database server
12
+ class Replica
13
+ attr_reader :config, :engine, :primary_host, :primary_port
14
+ attr_reader :stats
15
+
16
+ # Replica states
17
+ STATE_INIT = :init
18
+ STATE_CONNECTING = :connecting
19
+ STATE_STREAMING = :streaming
20
+ STATE_REPLAYING = :replaying
21
+ STATE_CATCHING_UP = :catching_up
22
+ STATE_SYNCED = :synced
23
+ STATE_DISCONNECTED = :disconnected
24
+ STATE_FAILED = :failed
25
+
26
+ def initialize(engine, config = {})
27
+ @engine = engine
28
+ @config = config
29
+ @primary_host = config[:primary_host] || "localhost"
30
+ @primary_port = config[:primary_port] || 7433
31
+ @replication_port = config[:replication_port] || 7434
32
+ @state = STATE_INIT
33
+ @connection = nil
34
+ @replication_stream = nil
35
+ @receive_buffer = +""
36
+ @max_frame_size = config[:max_replication_frame_bytes] || 1_048_576
37
+ @last_received_lsn = nil
38
+ @last_replayed_lsn = nil
39
+ @catchup_start_time = nil
40
+ @stats = {
41
+ bytes_received: 0,
42
+ transactions_replayed: 0,
43
+ replication_lag_ms: 0,
44
+ reconnect_attempts: 0,
45
+ last_connect_time: nil,
46
+ last_disconnect_time: nil,
47
+ total_uptime_ms: 0,
48
+ last_error: nil
49
+ }
50
+ @lock = Monitor.new
51
+ @running = false
52
+ @replication_thread = nil
53
+ @recovery = nil
54
+ @retry_interval = config[:retry_interval] || 5
55
+ @max_retry_attempts = config[:max_retry_attempts] || 10
56
+ @heartbeat_interval = config[:heartbeat_interval] || 5
57
+ @retry_count = 0
58
+ @state_path = config[:state_path] || "#{@engine.path}.replica_state.json"
59
+ @state_write_lock = Mutex.new
60
+ @state_write_sequence = 0
61
+ load_state
62
+ @engine.set_replication_read_only(true) if @engine.respond_to?(:set_replication_read_only)
63
+ end
64
+
65
+ def start
66
+ @lock.synchronize do
67
+ return if @running
68
+
69
+ @running = true
70
+ @state = STATE_CONNECTING
71
+ @replication_thread = Thread.new { replication_loop }
72
+
73
+ puts "Replica started, connecting to primary at #{@primary_host}:#{@replication_port}"
74
+ true
75
+ end
76
+ end
77
+
78
+ def stop
79
+ thread, connection = @lock.synchronize do
80
+ return unless @running
81
+
82
+ @running = false
83
+ @state = STATE_DISCONNECTED
84
+ [@replication_thread, @connection]
85
+ end
86
+
87
+ # Closing the socket wakes a blocked read. Let an in-flight frame finish
88
+ # its durable state publication before joining the worker; killing it can
89
+ # interrupt File.open/rename and leave replica state ambiguous.
90
+ connection&.close
91
+ thread&.join(5)
92
+ if thread&.alive?
93
+ thread.kill
94
+ thread.join
95
+ end
96
+
97
+ @lock.synchronize do
98
+ @connection = nil
99
+ @replication_thread = nil
100
+ @stats[:last_disconnect_time] = Time.now
101
+ end
102
+ true
103
+ end
104
+
105
+ def running?
106
+ @lock.synchronize { @running }
107
+ end
108
+
109
+ def promote_to_primary(recovery_point: nil)
110
+ @lock.synchronize do
111
+ validate_promotion!(recovery_point)
112
+ end
113
+
114
+ # Stop outside the monitor so the worker can finish without waiting on
115
+ # the promotion caller's lock.
116
+ stop
117
+
118
+ # Promotion is an explicit operator action. Re-enable ordinary
119
+ # mutations only after the replication stream has been stopped.
120
+ @engine.set_replication_read_only(false) if @engine.respond_to?(:set_replication_read_only)
121
+ @lock.synchronize { @state = STATE_SYNCED }
122
+ true
123
+ end
124
+
125
+ def replay_transaction(transaction_data, lsn = nil)
126
+ @lock.synchronize do
127
+ # Apply transaction to engine
128
+ @engine.apply_transaction(transaction_data)
129
+ @stats[:transactions_replayed] += 1
130
+ @last_replayed_lsn = lsn || transaction_data[:lsn]
131
+ persist_state
132
+
133
+ true
134
+ end
135
+ end
136
+
137
+ def replication_status
138
+ @lock.synchronize do
139
+ {
140
+ state: @state,
141
+ primary_host: @primary_host,
142
+ primary_port: @primary_port,
143
+ last_received_lsn: @last_received_lsn,
144
+ last_replayed_lsn: @last_replayed_lsn,
145
+ lag_ms: @stats[:replication_lag_ms],
146
+ bytes_received: @stats[:bytes_received],
147
+ transactions_replayed: @stats[:transactions_replayed],
148
+ connected: @state == STATE_STREAMING || @state == STATE_SYNCED
149
+ }
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def replication_loop
156
+ while @running
157
+ begin
158
+ case @state
159
+ when STATE_INIT, STATE_CONNECTING
160
+ connect_to_primary
161
+ when STATE_STREAMING
162
+ stream_replication
163
+ when STATE_CATCHING_UP
164
+ catch_up
165
+ when STATE_SYNCED
166
+ sleep(1)
167
+ when STATE_DISCONNECTED, STATE_FAILED
168
+ reconnect
169
+ end
170
+ rescue => e
171
+ @stats[:last_error] = "#{e.class}: #{e.message}"
172
+ @state = STATE_FAILED
173
+ @stats[:reconnect_attempts] += 1
174
+ sleep(@retry_interval)
175
+ end
176
+ end
177
+ end
178
+
179
+ def connect_to_primary
180
+ @lock.synchronize do
181
+ begin
182
+ @connection = TCPSocket.new(@primary_host, @replication_port)
183
+ @state = STATE_CONNECTING
184
+ @stats[:last_connect_time] = Time.now
185
+
186
+ # Send replica handshake
187
+ handshake = {
188
+ type: "replica_handshake",
189
+ replica_id: @config[:replica_id] || "replica_#{Process.pid}",
190
+ protocol_version: 1,
191
+ wal_position: @last_replayed_lsn || 0,
192
+ auth_token: @config[:replication_auth_token] || @config[:auth_token]
193
+ }
194
+
195
+ @connection.write(JSON.generate(handshake) + "\n")
196
+ response = JSON.parse(@connection.readline)
197
+
198
+ if response["success"]
199
+ @state = STATE_STREAMING
200
+ @stats[:reconnect_attempts] = 0
201
+ @retry_count = 0
202
+ puts "Connected to primary as replica"
203
+ else
204
+ @state = STATE_FAILED
205
+ end
206
+
207
+ rescue => e
208
+ @stats[:last_error] = "#{e.class}: #{e.message}"
209
+ @state = STATE_FAILED
210
+ @stats[:reconnect_attempts] += 1
211
+ raise
212
+ end
213
+ end
214
+ end
215
+
216
+ def stream_replication
217
+ while @running && @state == STATE_STREAMING
218
+ begin
219
+ # Read replication data
220
+ if @connection
221
+ ready = IO.select([@connection], nil, nil, @heartbeat_interval)
222
+ unless ready
223
+ @connection.write(
224
+ JSON.generate(
225
+ type: "heartbeat",
226
+ wal_position: @last_replayed_lsn || 0
227
+ ) + "\n"
228
+ )
229
+ @connection.flush
230
+ next
231
+ end
232
+
233
+ data = @connection.readpartial(4096)
234
+ @stats[:bytes_received] += data.bytesize
235
+
236
+ # Process replication data
237
+ process_replication_data(data)
238
+ else
239
+ @state = STATE_DISCONNECTED
240
+ break
241
+ end
242
+ rescue EOFError, Errno::ECONNRESET
243
+ @state = STATE_DISCONNECTED
244
+ break
245
+ rescue => e
246
+ @state = STATE_FAILED
247
+ break
248
+ end
249
+ end
250
+ end
251
+
252
+ def process_replication_data(data)
253
+ @receive_buffer << data.to_s
254
+ if @receive_buffer.bytesize > @max_frame_size
255
+ raise RubyDB::ReplicationError, "Replication frame exceeds #{@max_frame_size} bytes"
256
+ end
257
+
258
+ while (newline = @receive_buffer.index("\n"))
259
+ line = @receive_buffer.slice!(0, newline + 1).strip
260
+ next if line.empty?
261
+
262
+ message = JSON.parse(line, symbolize_names: true)
263
+ if message[:type].to_s == "replication_bootstrap"
264
+ apply_bootstrap_schema(message[:schema].to_s)
265
+ next
266
+ end
267
+ next unless message[:type].to_s == "replication_data"
268
+
269
+ Array(message[:data]).each do |entry|
270
+ next if entry[:lsn] && @last_replayed_lsn && entry[:lsn] <= @last_replayed_lsn
271
+ replay_transaction(entry[:data] || entry, entry[:lsn])
272
+ @last_received_lsn = entry[:lsn]
273
+ persist_state
274
+ @connection&.write(JSON.generate(type: "ack", lsn: @last_received_lsn) + "\n")
275
+ end
276
+ end
277
+ end
278
+
279
+ def catch_up
280
+ @state = if @last_received_lsn && @last_received_lsn == @last_replayed_lsn
281
+ STATE_SYNCED
282
+ else
283
+ STATE_STREAMING
284
+ end
285
+ end
286
+
287
+ def apply_bootstrap_schema(schema)
288
+ return if schema.empty? || !@engine.respond_to?(:with_replication_apply)
289
+
290
+ @engine.with_replication_apply do
291
+ statements = RubyDB::SQL::Parser.new(RubyDB::SQL::Lexer.new(schema).tokenize).parse
292
+ statements.each do |statement|
293
+ plan = RubyDB::Execution::Planner.new(@engine).plan(statement)
294
+ RubyDB::Execution::Executor.new(@engine).execute(plan)
295
+ end
296
+ end
297
+ rescue StandardError => error
298
+ raise RubyDB::ReplicationError, "Replica bootstrap failed: #{error.message}"
299
+ end
300
+
301
+ def load_state
302
+ return unless File.file?(@state_path)
303
+ state = JSON.parse(File.read(@state_path), symbolize_names: true)
304
+ @last_received_lsn = state[:last_received_lsn]
305
+ @last_replayed_lsn = state[:last_replayed_lsn]
306
+ rescue JSON::ParserError => error
307
+ raise RubyDB::ReplicationError, "Invalid replica state #{@state_path}: #{error.message}"
308
+ end
309
+
310
+ def persist_state
311
+ @state_write_lock.synchronize do
312
+ FileUtils.mkdir_p(File.dirname(@state_path))
313
+ @state_write_sequence += 1
314
+ temporary = "#{@state_path}.tmp-#{Process.pid}-#{Thread.current.object_id}-#{@state_write_sequence}"
315
+ payload = JSON.generate(
316
+ last_received_lsn: @last_received_lsn,
317
+ last_replayed_lsn: @last_replayed_lsn,
318
+ updated_at: Time.now.iso8601
319
+ )
320
+ File.open(temporary, "wb") do |file|
321
+ file.write(payload)
322
+ file.flush
323
+ file.fsync
324
+ end
325
+ begin
326
+ File.rename(temporary, @state_path)
327
+ rescue Errno::EACCES, Errno::EPERM
328
+ # Windows does not replace an existing destination with rename(2).
329
+ # FileUtils performs the platform-appropriate forced move while
330
+ # retaining the fsynced temporary payload as the source.
331
+ raise unless Gem.win_platform? && File.file?(@state_path)
332
+
333
+ FileUtils.mv(temporary, @state_path, force: true)
334
+ end
335
+ ensure
336
+ File.delete(temporary) if defined?(temporary) && File.file?(temporary)
337
+ end
338
+ end
339
+
340
+ def reconnect
341
+ @retry_count += 1
342
+
343
+ if @retry_count > @max_retry_attempts
344
+ @state = STATE_FAILED
345
+ return
346
+ end
347
+
348
+ sleep(@retry_interval * @retry_count)
349
+ @state = STATE_CONNECTING
350
+ end
351
+
352
+ def disconnect
353
+ @connection&.close
354
+ @connection = nil
355
+ @state = STATE_DISCONNECTED
356
+ @stats[:last_disconnect_time] = Time.now
357
+ end
358
+
359
+ def validate_promotion!(recovery_point)
360
+ promotable_state = [STATE_STREAMING, STATE_SYNCED, STATE_DISCONNECTED, STATE_CONNECTING].include?(@state)
361
+ caught_up_after_disconnect = [STATE_DISCONNECTED, STATE_CONNECTING].include?(@state) &&
362
+ @last_replayed_lsn &&
363
+ @last_received_lsn == @last_replayed_lsn
364
+ unless promotable_state && ([STATE_STREAMING, STATE_SYNCED].include?(@state) || caught_up_after_disconnect)
365
+ raise ReplicationError, "Replica is not synchronized enough for promotion"
366
+ end
367
+
368
+ if @last_received_lsn != @last_replayed_lsn
369
+ raise ReplicationError,
370
+ "Replica has unapplied replication data (received=#{@last_received_lsn.inspect}, " \
371
+ "replayed=#{@last_replayed_lsn.inspect})"
372
+ end
373
+
374
+ return true if recovery_point.nil?
375
+
376
+ target = recovery_point.to_i
377
+ replayed = @last_replayed_lsn.to_i
378
+ raise ReplicationError, "Replica has not reached recovery point #{target}" if replayed < target
379
+
380
+ true
381
+ end
382
+ end
383
+ end
384
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "time"
6
+ require "monitor"
7
+
8
+ module RubyDB
9
+ module Replication
10
+ # ReplicationLog - Logs replication transactions
11
+ class ReplicationLog
12
+ attr_reader :stats
13
+
14
+ def initialize(engine, config = {})
15
+ @engine = engine
16
+ @config = config
17
+ # A replication log belongs to one database. A process-wide relative
18
+ # directory can mix records from unrelated clusters after restart.
19
+ @log_dir = config[:log_dir] || "#{@engine.path}.replication_log"
20
+ @max_segment_size = config[:max_segment_size] || 16 * 1024 * 1024
21
+ @keep_segments = config[:keep_segments] || 100
22
+ @current_segment = nil
23
+ @current_segment_size = 0
24
+ @segment_counter = 0
25
+ @stats = {
26
+ transactions_logged: 0,
27
+ bytes_logged: 0,
28
+ segments_created: 0,
29
+ segments_archived: 0,
30
+ segments_deleted: 0
31
+ }
32
+ @lock = Monitor.new
33
+
34
+ create_log_directory
35
+ open_current_segment
36
+ end
37
+
38
+ def log_transaction(transaction_data, lsn)
39
+ @lock.synchronize do
40
+ entry = {
41
+ lsn: lsn,
42
+ timestamp: Time.now.iso8601,
43
+ transaction_id: transaction_data[:id],
44
+ data: transaction_data
45
+ }
46
+
47
+ json = JSON.generate(entry) + "\n"
48
+ @current_segment.write(json)
49
+ @current_segment.flush
50
+
51
+ @stats[:transactions_logged] += 1
52
+ @stats[:bytes_logged] += json.bytesize
53
+ @current_segment_size += json.bytesize
54
+
55
+ # Rotate if segment is full
56
+ if @current_segment_size >= @max_segment_size
57
+ rotate_segment
58
+ end
59
+
60
+ true
61
+ end
62
+ end
63
+
64
+ def read_transactions(from_lsn = nil, to_lsn = nil)
65
+ @lock.synchronize do
66
+ transactions = []
67
+ segments = list_segments
68
+
69
+ segments.each do |segment_path|
70
+ File.open(segment_path, "r") do |file|
71
+ file.each_line do |line|
72
+ begin
73
+ entry = JSON.parse(line, symbolize_names: true)
74
+
75
+ # Filter by LSN range
76
+ if from_lsn && entry[:lsn] < from_lsn
77
+ next
78
+ end
79
+ if to_lsn && entry[:lsn] > to_lsn
80
+ next
81
+ end
82
+
83
+ transactions << entry
84
+ rescue JSON::ParserError
85
+ # Skip malformed entries
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ transactions
92
+ end
93
+ end
94
+
95
+ def get_last_lsn
96
+ @lock.synchronize do
97
+ list_segments.reverse_each do |segment_path|
98
+ begin
99
+ File.readlines(segment_path).reverse_each do |line|
100
+ begin
101
+ lsn = JSON.parse(line, symbolize_names: true)[:lsn]
102
+ return lsn unless lsn.nil?
103
+ rescue JSON::ParserError
104
+ next
105
+ end
106
+ end
107
+ rescue Errno::ENOENT
108
+ # A concurrently rotated segment may disappear between the
109
+ # directory listing and the read. Continue with the remaining
110
+ # durable segments for LSN recovery.
111
+ end
112
+ end
113
+ nil
114
+ end
115
+ end
116
+
117
+ def archive_segment(segment_name)
118
+ @lock.synchronize do
119
+ archive_dir = File.join(@log_dir, "archive")
120
+ FileUtils.mkdir_p(archive_dir)
121
+
122
+ src = File.join(@log_dir, segment_name)
123
+ dst = File.join(archive_dir, segment_name)
124
+
125
+ if File.exist?(src)
126
+ FileUtils.mv(src, dst)
127
+ @stats[:segments_archived] += 1
128
+ return true
129
+ end
130
+
131
+ false
132
+ end
133
+ end
134
+
135
+ def delete_old_segments
136
+ @lock.synchronize do
137
+ segments = list_segments
138
+ return if segments.size <= @keep_segments
139
+
140
+ to_delete = segments.first(segments.size - @keep_segments)
141
+ to_delete.each do |segment_path|
142
+ File.delete(segment_path)
143
+ @stats[:segments_deleted] += 1
144
+ end
145
+ end
146
+ end
147
+
148
+ def stats
149
+ @lock.synchronize do
150
+ stats_hash = @stats.dup
151
+ stats_hash[:current_segment] = File.basename(@current_segment.path) if @current_segment
152
+ stats_hash[:current_segment_size] = @current_segment_size
153
+ stats_hash[:segments_count] = list_segments.size
154
+ stats_hash[:log_dir] = @log_dir
155
+ stats_hash
156
+ end
157
+ end
158
+
159
+ def close
160
+ @lock.synchronize do
161
+ @current_segment&.close unless @current_segment&.closed?
162
+ @current_segment = nil
163
+ true
164
+ end
165
+ end
166
+
167
+ def closed?
168
+ @current_segment.nil? || @current_segment.closed?
169
+ end
170
+
171
+ private
172
+
173
+ def create_log_directory
174
+ FileUtils.mkdir_p(@log_dir)
175
+ FileUtils.mkdir_p(File.join(@log_dir, "archive"))
176
+ end
177
+
178
+ def open_current_segment
179
+ @segment_counter += 1
180
+ segment_name = "replication_#{Time.now.strftime('%Y%m%d_%H%M%S')}_#{@segment_counter}.log"
181
+ segment_path = File.join(@log_dir, segment_name)
182
+
183
+ @current_segment = File.open(segment_path, "a+")
184
+ @current_segment.sync = false
185
+ @current_segment_size = File.size(segment_path)
186
+ @stats[:segments_created] += 1
187
+ end
188
+
189
+ def rotate_segment
190
+ @current_segment.close
191
+ delete_old_segments
192
+ open_current_segment
193
+ end
194
+
195
+ def list_segments
196
+ Dir.glob(File.join(@log_dir, "replication_*.log")).sort
197
+ end
198
+ end
199
+ end
200
+ end