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,450 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "time"
5
+ require "thread"
6
+ require "json"
7
+ require "monitor"
8
+ require "fileutils"
9
+ require "digest"
10
+ require_relative "replication_slot"
11
+ require_relative "fencing"
12
+
13
+ module RubyDB
14
+ module Replication
15
+ # Primary - Primary database server in replication setup
16
+ class Primary
17
+ attr_reader :config, :engine, :replication_log, :replication_slots
18
+ attr_reader :replicas, :stats
19
+
20
+ def initialize(engine, config = {})
21
+ @engine = engine
22
+ @engine.set_replication_read_only(false) if @engine.respond_to?(:set_replication_read_only)
23
+ @config = {
24
+ host: config[:host] || "localhost",
25
+ port: config[:port] || 7433,
26
+ replication_port: config[:replication_port] || 7434,
27
+ max_replicas: config[:max_replicas] || 10,
28
+ sync_mode: config[:sync_mode] || :async, # :async, :sync, :quorum
29
+ sync_replicas: config[:sync_replicas] || 1,
30
+ wal_keep_segments: config[:wal_keep_segments] || 100,
31
+ replication_timeout: config[:replication_timeout] || 60,
32
+ heartbeat_interval: config[:heartbeat_interval] || 10,
33
+ replication_auth_token: config[:replication_auth_token] || config[:auth_token],
34
+ enable_slots: config[:enable_slots] != false,
35
+ log_dir: config[:log_dir] || "#{@engine.path}.replication_log",
36
+ fence_path: config[:fence_path] || "#{@engine.path}.fence",
37
+ node_id: config[:node_id] || "primary_#{Process.pid}"
38
+ }
39
+
40
+ @replication_log = ReplicationLog.new(@engine, @config)
41
+ @replication_slots = {}
42
+ @replicas = {}
43
+ @replica_counter = 0
44
+ @stats = {
45
+ total_writes: 0,
46
+ total_bytes_replicated: 0,
47
+ replication_lag_ms: 0,
48
+ sync_status: :async,
49
+ last_commit_time: nil,
50
+ committed_lsn: nil,
51
+ replicated_lsn: nil,
52
+ active_replicas: 0,
53
+ total_replicas_connected: 0,
54
+ authentication_failures: 0
55
+ }
56
+ @lock = Monitor.new
57
+ @slot_path = config[:slot_path] || "#{@engine.path}.replication_slots.json"
58
+ @running = false
59
+ @replication_server = nil
60
+ @heartbeat_thread = nil
61
+ @recovery = nil
62
+ @fencing_lease = FencingLease.new(@config[:fence_path], @config[:node_id]).acquire!
63
+ @engine.set_write_fence(@fencing_lease) if @engine.respond_to?(:set_write_fence)
64
+ @engine_commit_listener = proc do |transaction_id, changes|
65
+ write(
66
+ id: "engine_tx_#{transaction_id}",
67
+ operation: "transaction",
68
+ transaction_id: transaction_id,
69
+ operations: changes
70
+ )
71
+ end
72
+ @engine.add_commit_listener(@engine_commit_listener) if @engine.respond_to?(:add_commit_listener)
73
+
74
+ # Initialize replication slots if enabled
75
+ initialize_slots if @config[:enable_slots]
76
+ end
77
+
78
+ def start
79
+ @lock.synchronize do
80
+ return if @running
81
+
82
+ @fencing_lease.assert_valid!
83
+ @replication_log = ReplicationLog.new(@engine, @config) if @replication_log.closed?
84
+ @engine.add_commit_listener(@engine_commit_listener) if @engine.respond_to?(:add_commit_listener)
85
+
86
+ @running = true
87
+
88
+ # Start replication server
89
+ start_replication_server
90
+
91
+ # Start heartbeat thread
92
+ start_heartbeat
93
+
94
+ puts "Primary replication started on port #{@config[:replication_port]}"
95
+ true
96
+ end
97
+ end
98
+
99
+ def stop
100
+ connections = []
101
+ @lock.synchronize do
102
+ if @running
103
+ @running = false
104
+ @engine.remove_commit_listener(@engine_commit_listener) if @engine.respond_to?(:remove_commit_listener)
105
+
106
+ @replication_server&.close
107
+ @replication_server = nil
108
+
109
+ @heartbeat_thread&.kill
110
+ @heartbeat_thread = nil
111
+
112
+ puts "Primary replication stopped"
113
+ else
114
+ @engine.remove_commit_listener(@engine_commit_listener) if @engine.respond_to?(:remove_commit_listener)
115
+ end
116
+
117
+ # Closing the listener alone does not wake established replica
118
+ # sessions. Close those sockets as part of shutdown so replicas
119
+ # observe the partition and can reconnect/catch up from the durable
120
+ # replication log.
121
+ connections = @replicas.values.filter_map { |replica| replica[:connection] }
122
+ @replicas.each_value { |replica| replica[:connection] = nil }
123
+ @replicas.clear
124
+ @stats[:active_replicas] = 0
125
+ @replication_log.close unless @replication_log.closed?
126
+ end
127
+ connections.each do |connection|
128
+ connection.shutdown(Socket::SHUT_RDWR) rescue nil
129
+ connection.close rescue nil
130
+ end
131
+ true
132
+ end
133
+
134
+ def write(transaction_data)
135
+ @lock.synchronize do
136
+ @fencing_lease.assert_valid!
137
+ # Logical replication uses a monotonic LSN over durable envelopes.
138
+ lsn = (@replication_log.get_last_lsn || 0) + 1
139
+
140
+ # Log the transaction
141
+ @replication_log.log_transaction(transaction_data, lsn)
142
+
143
+ # Update stats
144
+ @stats[:total_writes] += 1
145
+ @stats[:committed_lsn] = lsn
146
+
147
+ # Notify replicas
148
+ notify_replicas(transaction_data, lsn)
149
+
150
+ # Wait for sync replicas if configured
151
+ wait_for_sync_replicas if @config[:sync_mode] == :sync
152
+
153
+ lsn
154
+ end
155
+ end
156
+
157
+ def fencing_status
158
+ @lock.synchronize do
159
+ { node_id: @fencing_lease.node_id, epoch: @fencing_lease.epoch, valid: @fencing_lease.valid? }
160
+ end
161
+ end
162
+
163
+ def register_replica(replica_info)
164
+ @lock.synchronize do
165
+ replica_id = @replica_counter + 1
166
+ @replica_counter = replica_id
167
+
168
+ # Create replication slot if enabled
169
+ slot = nil
170
+ if @config[:enable_slots]
171
+ slot_name = "replica_#{replica_id}"
172
+ slot = create_replication_slot(slot_name)
173
+ end
174
+
175
+ replica = {
176
+ id: replica_id,
177
+ info: replica_info,
178
+ connected_at: Time.now,
179
+ last_heartbeat: Time.now,
180
+ last_lsn: nil,
181
+ last_ack_lsn: nil,
182
+ sync_standby: false,
183
+ slot: slot,
184
+ connection: nil,
185
+ lag_bytes: 0
186
+ }
187
+
188
+ @replicas[replica_id] = replica
189
+ @stats[:active_replicas] = @replicas.size
190
+ @stats[:total_replicas_connected] += 1
191
+
192
+ replica_id
193
+ end
194
+ end
195
+
196
+ def unregister_replica(replica_id)
197
+ @lock.synchronize do
198
+ replica = @replicas.delete(replica_id)
199
+ return false unless replica
200
+
201
+ @stats[:active_replicas] = @replicas.size
202
+ true
203
+ end
204
+ end
205
+
206
+ def send_heartbeat(replica_id)
207
+ @lock.synchronize do
208
+ replica = @replicas[replica_id]
209
+ return false unless replica
210
+
211
+ replica[:last_heartbeat] = Time.now
212
+
213
+ # Calculate replication lag
214
+ if replica[:last_lsn] && @stats[:committed_lsn]
215
+ replica[:lag_bytes] = [@stats[:committed_lsn].to_i - replica[:last_lsn].to_i, 0].max
216
+ end
217
+
218
+ true
219
+ end
220
+ end
221
+
222
+ def get_replication_status
223
+ @lock.synchronize do
224
+ {
225
+ running: @running,
226
+ role: "primary",
227
+ sync_mode: @config[:sync_mode],
228
+ active_replicas: @replicas.size,
229
+ total_writes: @stats[:total_writes],
230
+ committed_lsn: @stats[:committed_lsn],
231
+ replicated_lsn: @stats[:replicated_lsn],
232
+ wal_keep_segments: @config[:wal_keep_segments],
233
+ slots: @replication_slots.size,
234
+ replicas: @replicas.map do |id, r|
235
+ {
236
+ id: id,
237
+ connected_at: r[:connected_at].iso8601,
238
+ last_heartbeat: r[:last_heartbeat].iso8601,
239
+ sync_standby: r[:sync_standby],
240
+ lag_bytes: r[:lag_bytes],
241
+ acknowledged_lsn: r[:last_ack_lsn]
242
+ }
243
+ end
244
+ }
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ def start_replication_server
251
+ @replication_server = TCPServer.new(@config[:host], @config[:replication_port])
252
+
253
+ Thread.new do
254
+ while @running
255
+ begin
256
+ client = @replication_server.accept
257
+ Thread.new { handle_replica_connection(client) }
258
+ rescue => e
259
+ # Log error but continue
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ def handle_replica_connection(client)
266
+ line = client.gets
267
+ handshake = JSON.parse(line, symbolize_names: true)
268
+ unless handshake[:type].to_s == "replica_handshake"
269
+ client.write(JSON.generate(success: false, error: "Invalid replication handshake") + "\n")
270
+ return
271
+ end
272
+
273
+ expected_token = @config[:replication_auth_token]
274
+ if expected_token && !secure_token_match?(expected_token, handshake[:auth_token])
275
+ @lock.synchronize { @stats[:authentication_failures] += 1 }
276
+ client.write(JSON.generate(success: false, error: "Replication authentication failed") + "\n")
277
+ client.flush
278
+ return
279
+ end
280
+
281
+ replica_id = register_replica(handshake)
282
+ @lock.synchronize do
283
+ replica = @replicas[replica_id]
284
+ replica[:connection] = client
285
+
286
+ # A reconnecting replica may already have replayed every durable
287
+ # entry and therefore receive no catch-up frames. Record the
288
+ # handshake position as an acknowledgement, bounded by the primary
289
+ # log, so operators can distinguish a caught-up reconnect from a
290
+ # merely connected socket.
291
+ reported_lsn = handshake[:wal_position].to_i
292
+ durable_lsn = @replication_log.get_last_lsn.to_i
293
+ acknowledged_lsn = [reported_lsn, durable_lsn].min
294
+ if acknowledged_lsn.positive?
295
+ replica[:last_ack_lsn] = acknowledged_lsn
296
+ replica[:slot]&.advance(acknowledged_lsn)
297
+ persist_slots if replica[:slot]
298
+ @stats[:replicated_lsn] = [@stats[:replicated_lsn].to_i, acknowledged_lsn].max
299
+ end
300
+ end
301
+ client.write(JSON.generate(success: true, replica_id: replica_id, mode: "logical") + "\n")
302
+ client.flush
303
+
304
+ # Send the catalog before row replay so a new replica does not require
305
+ # an out-of-band schema copy. IF NOT EXISTS keeps reconnects safe.
306
+ schema = @engine.respond_to?(:schema_dump) ? @engine.schema_dump : ""
307
+ unless schema.empty?
308
+ schema = schema.gsub(/^CREATE TABLE /, "CREATE TABLE IF NOT EXISTS ")
309
+ client.write(JSON.generate(type: "replication_bootstrap", schema: schema) + "\n")
310
+ client.flush
311
+ end
312
+
313
+ from_lsn = handshake[:wal_position].to_i + 1
314
+ @replication_log.read_transactions(from_lsn).each do |entry|
315
+ send_replication_entry(client, entry)
316
+ @lock.synchronize { @replicas[replica_id][:last_lsn] = entry[:lsn] if @replicas[replica_id] }
317
+ end
318
+
319
+ while @running && !client.closed?
320
+ ready = IO.select([client], nil, nil, 0.1)
321
+ next unless ready
322
+ line = client.gets
323
+ break unless line
324
+ message = JSON.parse(line, symbolize_names: true)
325
+ if message[:type].to_s == "heartbeat"
326
+ send_heartbeat(replica_id)
327
+ next
328
+ end
329
+ next unless message[:type].to_s == "ack"
330
+
331
+ ack_lsn = message[:lsn].to_i
332
+ @lock.synchronize do
333
+ replica = @replicas[replica_id]
334
+ if replica
335
+ replica[:last_ack_lsn] = [replica[:last_ack_lsn].to_i, ack_lsn].max
336
+ replica[:slot]&.advance(ack_lsn)
337
+ persist_slots if replica[:slot]
338
+ @stats[:replicated_lsn] = [@stats[:replicated_lsn].to_i, ack_lsn].max
339
+ end
340
+ end
341
+ end
342
+ rescue StandardError
343
+ client.close rescue nil
344
+ ensure
345
+ if replica_id
346
+ unregister_replica(replica_id)
347
+ end
348
+ end
349
+
350
+ def start_heartbeat
351
+ @heartbeat_thread = Thread.new do
352
+ while @running
353
+ sleep(@config[:heartbeat_interval])
354
+
355
+ @lock.synchronize do
356
+ @replicas.each do |id, replica|
357
+ # Check if replica is still alive
358
+ if Time.now - replica[:last_heartbeat] > @config[:replication_timeout]
359
+ unregister_replica(id)
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end
366
+
367
+ def initialize_slots
368
+ return unless File.file?(@slot_path)
369
+
370
+ data = JSON.parse(File.read(@slot_path), symbolize_names: true)
371
+ data.fetch(:slots, {}).each do |name, slot_data|
372
+ slot = ReplicationSlot.new(name, self)
373
+ slot.confirmed_lsn = slot_data[:confirmed_lsn].to_i
374
+ @replication_slots[slot.name] = slot
375
+ end
376
+ rescue JSON::ParserError => error
377
+ raise RubyDB::ReplicationError, "Invalid replication slot catalog #{@slot_path}: #{error.message}"
378
+ end
379
+
380
+ def create_replication_slot(name)
381
+ existing = @replication_slots[name.to_s]
382
+ return existing if existing
383
+
384
+ slot = ReplicationSlot.new(name, self)
385
+ @replication_slots[name] = slot
386
+ persist_slots
387
+ slot
388
+ end
389
+
390
+ def drop_replication_slot(name)
391
+ removed = @replication_slots.delete(name.to_s)
392
+ persist_slots if removed
393
+ removed
394
+ end
395
+
396
+ def persist_slots
397
+ FileUtils.mkdir_p(File.dirname(@slot_path))
398
+ payload = { slots: @replication_slots.transform_values { |slot| { confirmed_lsn: slot.confirmed_lsn } } }
399
+ temporary = "#{@slot_path}.tmp-#{Process.pid}"
400
+ File.write(temporary, JSON.generate(payload))
401
+ File.rename(temporary, @slot_path)
402
+ ensure
403
+ File.delete(temporary) if defined?(temporary) && File.file?(temporary)
404
+ end
405
+
406
+ def notify_replicas(transaction_data, lsn)
407
+ @stats[:replicated_lsn] = lsn
408
+ entry = { lsn: lsn, timestamp: Time.now.iso8601, transaction_id: transaction_data[:id], data: transaction_data }
409
+ @replicas.each_value do |replica|
410
+ connection = replica[:connection]
411
+ next unless connection
412
+ next if replica[:last_lsn] && replica[:last_lsn] >= lsn
413
+ send_replication_entry(connection, entry)
414
+ replica[:last_lsn] = lsn
415
+ rescue StandardError
416
+ unregister_replica(replica[:id])
417
+ end
418
+ end
419
+
420
+ def send_replication_entry(connection, entry)
421
+ payload = JSON.generate(type: "replication_data", data: [entry]) + "\n"
422
+ connection.write(payload)
423
+ connection.flush
424
+ @stats[:total_bytes_replicated] += payload.bytesize
425
+ end
426
+
427
+ def wait_for_sync_replicas
428
+ deadline = Time.now + @config[:replication_timeout]
429
+ loop do
430
+ acknowledged = @replicas.values.count { |replica| replica[:last_ack_lsn] == @stats[:committed_lsn] }
431
+ return true if acknowledged >= @config[:sync_replicas]
432
+ raise ReplicationError, "Synchronous replica acknowledgment timed out" if Time.now >= deadline
433
+ sleep(0.01)
434
+ end
435
+ end
436
+
437
+ def secure_token_match?(expected, presented)
438
+ return false unless presented
439
+
440
+ expected_bytes = expected.to_s.b
441
+ presented_bytes = presented.to_s.b
442
+ return false unless expected_bytes.bytesize == presented_bytes.bytesize
443
+
444
+ mismatch = 0
445
+ expected_bytes.bytes.each_with_index { |byte, index| mismatch |= byte ^ presented_bytes.getbyte(index) }
446
+ mismatch.zero?
447
+ end
448
+ end
449
+ end
450
+ end