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,304 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "json"
5
+ require "time"
6
+ require "digest"
7
+
8
+ require_relative "connection"
9
+ require_relative "connection_url"
10
+ require_relative "result"
11
+ require_relative "statement"
12
+ require_relative "prepared_statement"
13
+ require_relative "transaction"
14
+ require_relative "connection_pool"
15
+ require_relative "../protocol/protocol"
16
+ require_relative "../protocol/message"
17
+
18
+ module RubyDB
19
+ module Client
20
+ # Client - Main database client
21
+ class Client
22
+ attr_reader :config, :connection, :pool, :stats, :last_commit_ack
23
+
24
+ def initialize(config = {})
25
+ url = config[:url] || config["url"]
26
+ config = ConnectionURL.parse(url).merge(config.each_with_object({}) { |(key, value), result| result[key.to_sym] = value }) if url
27
+ @config = {
28
+ host: config[:host] || "localhost",
29
+ port: config[:port] || 7432,
30
+ username: config[:username] || "rubydb",
31
+ password: config[:password] || "",
32
+ database: config[:database] || "rubydb",
33
+ timeout: config[:timeout] || 30,
34
+ pool_size: config[:pool_size] || 5,
35
+ auto_connect: config.fetch(:auto_connect, true),
36
+ ssl: config[:ssl] || false,
37
+ compress: config[:compress] || false,
38
+ format: config[:format] || :json
39
+ }
40
+
41
+ @stats = {
42
+ queries_executed: 0,
43
+ queries_failed: 0,
44
+ transactions_started: 0,
45
+ transactions_committed: 0,
46
+ transactions_rolled_back: 0,
47
+ prepared_statements: 0,
48
+ bytes_sent: 0,
49
+ bytes_received: 0,
50
+ connection_time_ms: 0,
51
+ total_query_time_ms: 0,
52
+ avg_query_time_ms: 0
53
+ }
54
+
55
+ @lock = Mutex.new
56
+ @connected = false
57
+ @connection = nil
58
+ @pool = nil
59
+ @transaction = nil
60
+
61
+ # Initialize connection pool if pool_size > 1
62
+ if @config[:pool_size] > 1
63
+ @pool = ConnectionPool.new(@config)
64
+ end
65
+
66
+ # Auto-connect if configured
67
+ connect if @config[:auto_connect]
68
+ end
69
+
70
+ def connect
71
+ @lock.synchronize do
72
+ return if @connected
73
+
74
+ start_time = Time.now
75
+
76
+ begin
77
+ if @pool
78
+ @connection = @pool.get_connection
79
+ else
80
+ @connection = Connection.new(@config)
81
+ @connection.connect
82
+ end
83
+
84
+ @connected = true
85
+ @stats[:connection_time_ms] = (Time.now - start_time) * 1000
86
+
87
+ true
88
+ rescue => e
89
+ raise ClientError, "Connection failed: #{e.message}"
90
+ end
91
+ end
92
+ end
93
+
94
+ def disconnect
95
+ @lock.synchronize do
96
+ return unless @connected
97
+
98
+ if @pool
99
+ @pool.release_connection(@connection)
100
+ else
101
+ @connection.disconnect
102
+ end
103
+
104
+ @connection = nil
105
+ @connected = false
106
+ @transaction = nil
107
+
108
+ true
109
+ end
110
+ end
111
+
112
+ def connected?
113
+ @connected && @connection && @connection.connected?
114
+ end
115
+
116
+ def query(sql, params = [], timeout: nil)
117
+ @lock.synchronize do
118
+ ensure_connected
119
+
120
+ start_time = Time.now
121
+
122
+ begin
123
+ # Send query
124
+ deadline_at = timeout && (Time.now + Float(timeout)).iso8601(6)
125
+ response = @connection.send_query(sql, params, deadline_at: deadline_at)
126
+ @stats[:queries_executed] += 1
127
+
128
+ # Parse result
129
+ result = Result.new(response)
130
+
131
+ elapsed_ms = (Time.now - start_time) * 1000
132
+ @stats[:total_query_time_ms] += elapsed_ms
133
+ @stats[:avg_query_time_ms] = @stats[:total_query_time_ms] / @stats[:queries_executed] if @stats[:queries_executed] > 0
134
+
135
+ result
136
+
137
+ rescue => e
138
+ @stats[:queries_failed] += 1
139
+ raise
140
+ end
141
+ end
142
+ end
143
+
144
+ def query_async(sql, params = [], timeout: nil)
145
+ @lock.synchronize do
146
+ ensure_connected
147
+ deadline_at = timeout && (Time.now + Float(timeout)).iso8601(6)
148
+ handle = @connection.send_query_async(sql, params, deadline_at: deadline_at)
149
+ AsyncResult.new(handle)
150
+ end
151
+ end
152
+
153
+ class AsyncResult
154
+ attr_reader :request_id
155
+
156
+ def initialize(handle)
157
+ @handle = handle
158
+ @request_id = handle.request_id
159
+ end
160
+
161
+ def cancel
162
+ @handle.cancel
163
+ end
164
+
165
+ def wait(timeout = nil)
166
+ Result.new(@handle.wait(timeout))
167
+ end
168
+ end
169
+
170
+ def prepare(sql)
171
+ @lock.synchronize do
172
+ ensure_connected
173
+
174
+ begin
175
+ response = @connection.send_prepare(sql)
176
+ @stats[:prepared_statements] += 1
177
+
178
+ PreparedStatement.new(response[:statement_id], sql, self)
179
+ rescue => e
180
+ @stats[:queries_failed] += 1
181
+ raise
182
+ end
183
+ end
184
+ end
185
+
186
+ def execute(statement_id, params = [])
187
+ @lock.synchronize do
188
+ ensure_connected
189
+
190
+ start_time = Time.now
191
+
192
+ begin
193
+ response = @connection.send_execute(statement_id, params)
194
+ @stats[:queries_executed] += 1
195
+
196
+ result = Result.new(response)
197
+
198
+ elapsed_ms = (Time.now - start_time) * 1000
199
+ @stats[:total_query_time_ms] += elapsed_ms
200
+ @stats[:avg_query_time_ms] = @stats[:total_query_time_ms] / @stats[:queries_executed] if @stats[:queries_executed] > 0
201
+
202
+ result
203
+
204
+ rescue => e
205
+ @stats[:queries_failed] += 1
206
+ raise
207
+ end
208
+ end
209
+ end
210
+
211
+ def close_statement(statement_id)
212
+ @lock.synchronize do
213
+ ensure_connected
214
+ @connection.send_close(statement_id)
215
+ end
216
+ end
217
+
218
+ def begin_transaction(options = {})
219
+ @lock.synchronize do
220
+ ensure_connected
221
+
222
+ if @transaction && @transaction.active?
223
+ raise ClientError, "Transaction already active"
224
+ end
225
+
226
+ @connection.send_begin(options)
227
+ @transaction = Transaction.new(self, options)
228
+ @stats[:transactions_started] += 1
229
+
230
+ @transaction
231
+ end
232
+ end
233
+
234
+ def commit
235
+ @lock.synchronize do
236
+ ensure_connected
237
+
238
+ unless @transaction && @transaction.active?
239
+ raise ClientError, "No active transaction"
240
+ end
241
+
242
+ response = @connection.send_commit
243
+ unless response[:success] != false
244
+ raise ClientError, response[:error] || "Transaction commit failed"
245
+ end
246
+ @last_commit_ack = response[:commit_ack]
247
+ @last_commit_ack[:status] = @last_commit_ack[:status].to_sym if @last_commit_ack && @last_commit_ack[:status]
248
+ @transaction.mark_committed
249
+ @stats[:transactions_committed] += 1
250
+
251
+ @transaction = nil
252
+ end
253
+ end
254
+
255
+ def rollback
256
+ @lock.synchronize do
257
+ ensure_connected
258
+
259
+ unless @transaction && @transaction.active?
260
+ raise ClientError, "No active transaction"
261
+ end
262
+
263
+ @connection.send_rollback
264
+ @transaction.mark_rolled_back
265
+ @stats[:transactions_rolled_back] += 1
266
+
267
+ @transaction = nil
268
+ end
269
+ end
270
+
271
+ def in_transaction?
272
+ @transaction && @transaction.active?
273
+ end
274
+
275
+ def ping
276
+ @lock.synchronize do
277
+ ensure_connected
278
+ @connection.send_ping
279
+ true
280
+ end
281
+ end
282
+
283
+ def stats
284
+ @lock.synchronize do
285
+ @stats.merge({
286
+ connected: @connected,
287
+ host: @config[:host],
288
+ port: @config[:port],
289
+ database: @config[:database],
290
+ pool_size: @config[:pool_size],
291
+ pool_active: @pool ? @pool.active_count : 0
292
+ })
293
+ end
294
+ end
295
+
296
+ private
297
+
298
+ def ensure_connected
299
+ connect unless @connected
300
+ raise ClientError, "Not connected" unless @connected
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "json"
5
+ require "time"
6
+ require "monitor"
7
+ require "openssl"
8
+ require "base64"
9
+ require "securerandom"
10
+
11
+ module RubyDB
12
+ module Client
13
+ # Connection - Client connection to database server
14
+ class Connection
15
+ class QueryHandle
16
+ attr_reader :request_id
17
+
18
+ def initialize(connection, request_id, thread)
19
+ @connection = connection
20
+ @request_id = request_id
21
+ @thread = thread
22
+ end
23
+
24
+ def cancel
25
+ @connection.cancel(@request_id)
26
+ end
27
+
28
+ def cancelled?
29
+ @cancelled == true
30
+ end
31
+
32
+ def wait(timeout = nil)
33
+ @thread.join(timeout)
34
+ raise ConnectionError, "Query did not finish within #{timeout} seconds" if @thread.alive?
35
+
36
+ @thread.value
37
+ end
38
+ end
39
+
40
+ attr_reader :config, :socket, :stats
41
+
42
+ def initialize(config = {})
43
+ @config = config
44
+ @socket = nil
45
+ @connected = false
46
+ @protocol = nil
47
+ @authenticated = false
48
+ @session_id = nil
49
+ @stats = {
50
+ bytes_sent: 0,
51
+ bytes_received: 0,
52
+ messages_sent: 0,
53
+ messages_received: 0,
54
+ errors: 0,
55
+ reconnects: 0
56
+ }
57
+ @lock = Monitor.new
58
+ @buffer = ""
59
+ end
60
+
61
+ def connect
62
+ @lock.synchronize do
63
+ return if @connected
64
+
65
+ begin
66
+ tcp_socket = TCPSocket.new(@config[:host], @config[:port])
67
+ ssl = @config[:ssl]
68
+ if ssl == true || ssl.is_a?(Hash) && (ssl[:enabled] || ssl["enabled"])
69
+ context = OpenSSL::SSL::SSLContext.new
70
+ context.verify_mode = if ssl[:verify_peer] || ssl["verify_peer"]
71
+ OpenSSL::SSL::VERIFY_PEER
72
+ else
73
+ OpenSSL::SSL::VERIFY_NONE
74
+ end
75
+ context.ca_file = ssl[:ca_file] || ssl["ca_file"] if ssl.is_a?(Hash) && (ssl[:ca_file] || ssl["ca_file"])
76
+ if ssl.is_a?(Hash) && (ssl[:cert_file] || ssl["cert_file"])
77
+ context.cert = OpenSSL::X509::Certificate.new(File.binread(ssl[:cert_file] || ssl["cert_file"]))
78
+ end
79
+ if ssl.is_a?(Hash) && (ssl[:key_file] || ssl["key_file"])
80
+ context.key = OpenSSL::PKey.read(File.binread(ssl[:key_file] || ssl["key_file"]))
81
+ end
82
+ if ssl.is_a?(Hash) && (ssl[:min_version] || ssl["min_version"])
83
+ context.min_version = ssl[:min_version] || ssl["min_version"]
84
+ end
85
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, context)
86
+ @socket.hostname = @config[:host] if @socket.respond_to?(:hostname=)
87
+ @socket.connect
88
+ else
89
+ @socket = tcp_socket
90
+ end
91
+
92
+ # Set timeouts
93
+ if @config[:timeout]
94
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [@config[:timeout], 0].pack("l_2"))
95
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, [@config[:timeout], 0].pack("l_2"))
96
+ end
97
+
98
+ # Initialize protocol
99
+ @protocol = Protocol::Protocol.new(
100
+ format: @config[:format] || :json,
101
+ compression: @config[:compress] || false,
102
+ encryption: @config[:ssl] || false
103
+ )
104
+
105
+ # Perform handshake
106
+ perform_handshake
107
+
108
+ @connected = true
109
+
110
+ rescue => e
111
+ raise ConnectionError, "Connection failed: #{e.message}"
112
+ end
113
+ end
114
+ end
115
+
116
+ def disconnect
117
+ @lock.synchronize do
118
+ return unless @connected
119
+
120
+ begin
121
+ # Send terminate message
122
+ send_message(Protocol::Message.new(Protocol::Message::TYPE_TERMINATE))
123
+ rescue
124
+ # Ignore errors on disconnect
125
+ end
126
+
127
+ @socket.close if @socket
128
+ @socket = nil
129
+ @connected = false
130
+ @authenticated = false
131
+ end
132
+ end
133
+
134
+ def connected?
135
+ @connected && @socket && !@socket.closed?
136
+ end
137
+
138
+ def send_query(sql, params = [], deadline_at: nil)
139
+ @lock.synchronize do
140
+ ensure_connected
141
+
142
+ payload = { sql: sql, params: params }
143
+ payload[:deadline_at] = deadline_at if deadline_at
144
+ message = Protocol::Message.new(
145
+ Protocol::Message::TYPE_QUERY,
146
+ payload
147
+ )
148
+
149
+ send_message(message)
150
+ unwrap_result(receive_message.payload)
151
+ end
152
+ end
153
+
154
+ # Starts a query and returns a handle that can issue a wire-level
155
+ # cancellation while the server is executing it. The same connection
156
+ # must not be used for another application request until wait returns.
157
+ def send_query_async(sql, params = [], deadline_at: nil)
158
+ ensure_connected
159
+ payload = { sql: sql, params: params }
160
+ payload[:deadline_at] = deadline_at if deadline_at
161
+ message = Protocol::Message.new(Protocol::Message::TYPE_QUERY, payload)
162
+ @lock.synchronize { send_message(message) }
163
+ thread = Thread.new do
164
+ loop do
165
+ response = receive_message
166
+ next if response.type == :cancel_response
167
+ next if response.type == :notice && response.payload[:target_request_id].to_s != message.id.to_s
168
+
169
+ break unwrap_result(response.payload.merge(request_id: message.id))
170
+ end
171
+ end
172
+ QueryHandle.new(self, message.id, thread)
173
+ end
174
+
175
+ def cancel(request_id)
176
+ ensure_connected
177
+ message = Protocol::Message.new(
178
+ Protocol::Message::TYPE_CANCEL,
179
+ { target_request_id: request_id.to_s }
180
+ )
181
+ @lock.synchronize { send_message(message) }
182
+ true
183
+ end
184
+
185
+ def send_prepare(sql)
186
+ @lock.synchronize do
187
+ ensure_connected
188
+
189
+ message = Protocol::Message.new(
190
+ Protocol::Message::TYPE_PREPARE,
191
+ { sql: sql }
192
+ )
193
+
194
+ send_message(message)
195
+ receive_message.payload
196
+ end
197
+ end
198
+
199
+ def send_execute(statement_id, params = [])
200
+ @lock.synchronize do
201
+ ensure_connected
202
+
203
+ message = Protocol::Message.new(
204
+ Protocol::Message::TYPE_EXECUTE,
205
+ { statement_id: statement_id, params: params }
206
+ )
207
+
208
+ send_message(message)
209
+ unwrap_result(receive_message.payload)
210
+ end
211
+ end
212
+
213
+ def send_close(statement_id)
214
+ @lock.synchronize do
215
+ ensure_connected
216
+
217
+ message = Protocol::Message.new(
218
+ Protocol::Message::TYPE_CLOSE,
219
+ { statement_id: statement_id }
220
+ )
221
+
222
+ send_message(message)
223
+ receive_message.payload
224
+ end
225
+ end
226
+
227
+ def send_begin(options = {})
228
+ @lock.synchronize do
229
+ ensure_connected
230
+
231
+ message = Protocol::Message.new(
232
+ Protocol::Message::TYPE_BEGIN,
233
+ options
234
+ )
235
+
236
+ send_message(message)
237
+ receive_message.payload
238
+ end
239
+ end
240
+
241
+ def send_commit
242
+ @lock.synchronize do
243
+ ensure_connected
244
+
245
+ message = Protocol::Message.new(Protocol::Message::TYPE_COMMIT)
246
+ send_message(message)
247
+ receive_message.payload
248
+ end
249
+ end
250
+
251
+ def send_rollback
252
+ @lock.synchronize do
253
+ ensure_connected
254
+
255
+ message = Protocol::Message.new(Protocol::Message::TYPE_ROLLBACK)
256
+ send_message(message)
257
+ receive_message.payload
258
+ end
259
+ end
260
+
261
+ def send_ping
262
+ @lock.synchronize do
263
+ ensure_connected
264
+
265
+ message = Protocol::Message.new(Protocol::Message::TYPE_PING)
266
+ send_message(message)
267
+ receive_message
268
+ end
269
+ end
270
+
271
+ def stats
272
+ @lock.synchronize do
273
+ @stats.merge({
274
+ connected: @connected,
275
+ authenticated: @authenticated,
276
+ host: @config[:host],
277
+ port: @config[:port]
278
+ })
279
+ end
280
+ end
281
+
282
+ private
283
+
284
+ def ensure_connected
285
+ unless @connected
286
+ connect
287
+ end
288
+ end
289
+
290
+ def send_message(message)
291
+ data = @protocol.encoder.encode(message)
292
+ @socket.write(data)
293
+ @socket.flush
294
+
295
+ @stats[:bytes_sent] += data.bytesize
296
+ @stats[:messages_sent] += 1
297
+ end
298
+
299
+ def receive_message
300
+ data = @socket.gets
301
+ raise EOFError, "server closed connection" unless data
302
+ @stats[:bytes_received] += data.bytesize
303
+ @stats[:messages_received] += 1
304
+
305
+ @protocol.decoder.decode(data, @protocol.encoder.format)
306
+ rescue => e
307
+ @stats[:errors] += 1
308
+ raise ConnectionError, "Receive failed: #{e.message}"
309
+ end
310
+
311
+ def unwrap_result(payload)
312
+ result = payload[:result]
313
+ return payload unless result.is_a?(Hash)
314
+
315
+ result.merge(success: payload[:success] != false)
316
+ end
317
+
318
+ def perform_handshake
319
+ # Send handshake request
320
+ handshake_msg = Protocol::Message.new(
321
+ Protocol::Message::TYPE_HANDSHAKE,
322
+ {
323
+ protocol_version: Protocol::ProtocolVersion.current,
324
+ client_name: "RubyDB Client",
325
+ client_version: RubyDB::VERSION,
326
+ username: @config[:username],
327
+ database: @config[:database]
328
+ }
329
+ )
330
+
331
+ send_message(handshake_msg)
332
+ response = receive_message
333
+
334
+ if response.type == Protocol::Message::TYPE_HANDSHAKE_RESPONSE
335
+ if response.payload[:success]
336
+ # Send authentication, including a real SCRAM proof when required.
337
+ auth_payload = {
338
+ username: @config[:username],
339
+ password: @config[:password],
340
+ auth_method: @config[:auth_method] || "none"
341
+ }
342
+ if response.payload[:default_auth].to_s == "scram-sha-256"
343
+ auth_payload.delete(:password)
344
+ scram_payload = scram_data(
345
+ @config[:username].to_s,
346
+ @config[:password].to_s,
347
+ response.payload[:challenge].to_s,
348
+ response.payload[:scram_salt].to_s,
349
+ response.payload[:scram_iterations].to_i
350
+ )
351
+ expected_server_signature = scram_payload.delete(:expected_server_signature)
352
+ auth_payload[:scram_data] = scram_payload
353
+ end
354
+ auth_msg = Protocol::Message.new(
355
+ Protocol::Message::TYPE_AUTHENTICATION_RESPONSE,
356
+ auth_payload
357
+ )
358
+
359
+ send_message(auth_msg)
360
+ auth_response = receive_message
361
+
362
+ if auth_response.type == Protocol::Message::TYPE_AUTHENTICATION && auth_response.payload[:success]
363
+ if expected_server_signature && auth_response.payload[:server_signature] != expected_server_signature
364
+ raise ConnectionError, "SCRAM server signature verification failed"
365
+ end
366
+ @authenticated = true
367
+ @session_id = auth_response.payload[:session_id]
368
+
369
+ # Complete capability negotiation before exposing the connection
370
+ # to callers. The server will not process application requests
371
+ # until it receives this synchronization frame.
372
+ sync_msg = Protocol::Message.new(
373
+ Protocol::Message::TYPE_SYNCHRONIZE,
374
+ { capabilities: Protocol::Capabilities.default_client.to_hash }
375
+ )
376
+ send_message(sync_msg)
377
+ ready_response = receive_message
378
+ unless ready_response.type == Protocol::Message::TYPE_READY_FOR_QUERY
379
+ raise ConnectionError, "Protocol synchronization failed"
380
+ end
381
+ else
382
+ raise ConnectionError, "Authentication failed"
383
+ end
384
+ else
385
+ raise ConnectionError, "Handshake failed: #{response.payload[:error]}"
386
+ end
387
+ else
388
+ raise ConnectionError, "Unexpected handshake response"
389
+ end
390
+ end
391
+
392
+ def scram_data(username, password, challenge, encoded_salt, iterations)
393
+ client_nonce = SecureRandom.hex(18)
394
+ client_first_bare = "n=#{username},r=#{client_nonce}"
395
+ server_nonce = client_nonce + challenge
396
+ client_final = "c=biws,r=#{server_nonce}"
397
+ salt = Base64.decode64(encoded_salt)
398
+ salted = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, 32, OpenSSL::Digest::SHA256.new)
399
+ auth_message = [client_first_bare, "r=#{server_nonce},s=#{encoded_salt},i=#{iterations}", client_final].join(",")
400
+ client_key = OpenSSL::HMAC.digest("SHA256", salted, "Client Key")
401
+ stored_key = OpenSSL::Digest::SHA256.digest(client_key)
402
+ signature = OpenSSL::HMAC.digest("SHA256", stored_key, auth_message)
403
+ proof = client_key.bytes.each_with_index.map { |byte, index| byte ^ signature.getbyte(index) }.pack("C*")
404
+ server_key = OpenSSL::HMAC.digest("SHA256", salted, "Server Key")
405
+ {
406
+ client_first_bare: client_first_bare,
407
+ client_final_without_proof: client_final,
408
+ client_nonce: client_nonce,
409
+ proof: Base64.strict_encode64(proof),
410
+ expected_server_signature: Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", server_key, auth_message))
411
+ }
412
+ end
413
+ end
414
+ end
415
+ end