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,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "base64"
5
+
6
+ module RubyDB
7
+ module Protocol
8
+ class Decoder
9
+ attr_reader :stats
10
+
11
+ def initialize(options = {})
12
+ @options = options
13
+ @encryption_key = options[:encryption_key]
14
+ @stats = {
15
+ messages_decoded: 0,
16
+ bytes_decoded: 0,
17
+ decompressions: 0,
18
+ decryption_ops: 0,
19
+ errors: 0
20
+ }
21
+ @lock = Mutex.new
22
+ end
23
+
24
+ def decode(data, format = Encoder::FORMAT_JSON)
25
+ @lock.synchronize do
26
+ @stats[:bytes_decoded] += data.bytesize
27
+
28
+ begin
29
+ if @encryption_key && encrypted?(data)
30
+ data = decrypt(data)
31
+ @stats[:decryption_ops] += 1
32
+ end
33
+
34
+ if compressed?(data)
35
+ data = decompress(data)
36
+ @stats[:decompressions] += 1
37
+ end
38
+
39
+ message = case format
40
+ when Encoder::FORMAT_JSON
41
+ decode_json(data)
42
+ when Encoder::FORMAT_MSGPACK
43
+ decode_msgpack(data)
44
+ when Encoder::FORMAT_BINARY
45
+ decode_binary(data)
46
+ else
47
+ decode_json(data)
48
+ end
49
+
50
+ @stats[:messages_decoded] += 1
51
+ message
52
+
53
+ rescue => e
54
+ @stats[:errors] += 1
55
+ raise ProtocolError, "Decoding failed: #{e.message}"
56
+ end
57
+ end
58
+ end
59
+
60
+ def decode_json(data)
61
+ Message.from_json(data.strip)
62
+ end
63
+
64
+ def decode_msgpack(data)
65
+ begin
66
+ require "msgpack"
67
+ hash = MessagePack.unpack(data)
68
+ msg = Message.new(hash[:type], hash[:payload] || {})
69
+ msg.instance_variable_set(:@id, hash[:id])
70
+ msg.instance_variable_set(:@created_at, Time.parse(hash[:created_at]))
71
+ msg
72
+ rescue LoadError
73
+ decode_json(data)
74
+ end
75
+ end
76
+
77
+ def decode_binary(data)
78
+ offset = 0
79
+
80
+ type_len = data[offset].unpack("C").first
81
+ offset += 1
82
+ type = data[offset, type_len].to_sym
83
+ offset += type_len
84
+
85
+ id_len = data[offset].unpack("C").first
86
+ offset += 1
87
+ id = data[offset, id_len]
88
+ offset += id_len
89
+
90
+ timestamp = data[offset, 8].unpack("Q>").first
91
+ offset += 8
92
+
93
+ payload_len = data[offset, 4].unpack("L>").first
94
+ offset += 4
95
+ payload_data = data[offset, payload_len]
96
+ payload = JSON.parse(payload_data, symbolize_names: true)
97
+
98
+ msg = Message.new(type, payload)
99
+ msg.instance_variable_set(:@id, id)
100
+ msg.instance_variable_set(:@created_at, Time.at(timestamp))
101
+ msg
102
+ end
103
+
104
+ def compressed?(data)
105
+ data.bytesize >= 2 && data[0].ord == 0x78 && (data[1].ord == 0x01 || data[1].ord == 0x9C || data[1].ord == 0xDA)
106
+ end
107
+
108
+ def decompress(data)
109
+ Zlib::Inflate.inflate(data)
110
+ rescue => e
111
+ raise ProtocolError, "Decompression failed: #{e.message}"
112
+ end
113
+
114
+ def encrypted?(data)
115
+ false
116
+ end
117
+
118
+ def decrypt(data)
119
+ return data unless @encryption_key
120
+
121
+ begin
122
+ require "openssl"
123
+ iv = data[0, 16]
124
+ encrypted_data = data[16..-1]
125
+
126
+ cipher = OpenSSL::Cipher.new("aes-256-cbc")
127
+ cipher.decrypt
128
+ cipher.key = @encryption_key
129
+ cipher.iv = iv
130
+ cipher.update(encrypted_data) + cipher.final
131
+ rescue => e
132
+ raise ProtocolError, "Decryption failed: #{e.message}"
133
+ end
134
+ end
135
+
136
+ def stats
137
+ @lock.synchronize do
138
+ @stats.merge({
139
+ bytes_per_message: @stats[:messages_decoded] > 0 ? @stats[:bytes_decoded] / @stats[:messages_decoded] : 0
140
+ })
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "base64"
5
+
6
+ module RubyDB
7
+ module Protocol
8
+ class Encoder
9
+ FORMAT_JSON = :json
10
+ FORMAT_MSGPACK = :msgpack
11
+ FORMAT_BINARY = :binary
12
+
13
+ attr_reader :stats
14
+
15
+ def initialize(format = FORMAT_JSON, options = {})
16
+ @format = format
17
+ @options = options
18
+ @compression = options[:compression] || false
19
+ @encryption = options[:encryption] || false
20
+ @encryption_key = options[:encryption_key]
21
+ @stats = {
22
+ messages_encoded: 0,
23
+ bytes_encoded: 0,
24
+ compressions: 0,
25
+ encryption_ops: 0,
26
+ errors: 0
27
+ }
28
+ @lock = Mutex.new
29
+ end
30
+
31
+ def encode(message)
32
+ @lock.synchronize do
33
+ @stats[:messages_encoded] += 1
34
+
35
+ begin
36
+ data = case @format
37
+ when FORMAT_JSON
38
+ encode_json(message)
39
+ when FORMAT_MSGPACK
40
+ encode_msgpack(message)
41
+ when FORMAT_BINARY
42
+ encode_binary(message)
43
+ else
44
+ encode_json(message)
45
+ end
46
+
47
+ # JSON messages are transported as one line per frame so a
48
+ # persistent TCP connection can separate responses reliably.
49
+ data << "\n" if @format == FORMAT_JSON && !data.end_with?("\n")
50
+
51
+ if @compression
52
+ data = compress(data)
53
+ @stats[:compressions] += 1
54
+ end
55
+
56
+ if @encryption && @encryption_key
57
+ data = encrypt(data)
58
+ @stats[:encryption_ops] += 1
59
+ end
60
+
61
+ @stats[:bytes_encoded] += data.bytesize
62
+ data
63
+
64
+ rescue => e
65
+ @stats[:errors] += 1
66
+ raise ProtocolError, "Encoding failed: #{e.message}"
67
+ end
68
+ end
69
+ end
70
+
71
+ def encode_json(message)
72
+ message.to_json
73
+ end
74
+
75
+ def encode_msgpack(message)
76
+ begin
77
+ require "msgpack"
78
+ MessagePack.pack(message.to_hash)
79
+ rescue LoadError
80
+ message.to_json
81
+ end
82
+ end
83
+
84
+ def encode_binary(message)
85
+ data = ""
86
+ data << [message.type.to_s.bytesize].pack("C")
87
+ data << message.type.to_s
88
+ data << [message.id.bytesize].pack("C")
89
+ data << message.id
90
+ data << [message.created_at.to_i].pack("Q>")
91
+ payload = JSON.generate(message.payload)
92
+ data << [payload.bytesize].pack("L>")
93
+ data << payload
94
+ data
95
+ end
96
+
97
+ def compress(data)
98
+ Zlib::Deflate.deflate(data)
99
+ rescue => e
100
+ raise ProtocolError, "Compression failed: #{e.message}"
101
+ end
102
+
103
+ def encrypt(data)
104
+ return data unless @encryption_key
105
+
106
+ begin
107
+ require "openssl"
108
+ cipher = OpenSSL::Cipher.new("aes-256-cbc")
109
+ cipher.encrypt
110
+ cipher.key = @encryption_key
111
+ cipher.iv = cipher.random_iv
112
+ encrypted = cipher.update(data) + cipher.final
113
+ iv = cipher.iv
114
+ iv + encrypted
115
+ rescue => e
116
+ raise ProtocolError, "Encryption failed: #{e.message}"
117
+ end
118
+ end
119
+
120
+ def format
121
+ @format
122
+ end
123
+
124
+ def stats
125
+ @lock.synchronize do
126
+ @stats.merge({
127
+ format: @format,
128
+ compression: @compression,
129
+ encryption: @encryption,
130
+ bytes_per_message: @stats[:messages_encoded] > 0 ? @stats[:bytes_encoded] / @stats[:messages_encoded] : 0
131
+ })
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "digest"
5
+ require "openssl"
6
+ require "base64"
7
+
8
+ module RubyDB
9
+ module Protocol
10
+ class Handshake
11
+ STATE_INIT = :init
12
+ STATE_AUTHENTICATING = :authenticating
13
+ STATE_AUTHENTICATED = :authenticated
14
+ STATE_NEGOTIATING = :negotiating
15
+ STATE_COMPLETE = :complete
16
+ STATE_FAILED = :failed
17
+
18
+ attr_reader :state, :client_info, :server_info
19
+
20
+ def initialize(server_info = {})
21
+ @state = STATE_INIT
22
+ @client_info = {}
23
+ server_info = (server_info || {}).dup
24
+ @credentials = server_info.delete(:authentication_credentials) || {}
25
+ @server_info = {
26
+ version: ProtocolVersion.current_string,
27
+ protocol_version: ProtocolVersion.current,
28
+ name: "RubyDB",
29
+ pid: Process.pid,
30
+ started_at: Time.now.iso8601,
31
+ auth_methods: ["none", "password", "md5", "scram-sha-256"],
32
+ default_auth: "none"
33
+ }.merge(server_info || {})
34
+ @challenge = nil
35
+ @scram_salt = (@credentials[:scram_salt] || @credentials["scram_salt"])
36
+ @scram_salt = SecureRandom.random_bytes(16) unless @scram_salt
37
+ @scram_iterations = Integer(@credentials[:scram_iterations] || @credentials["scram_iterations"] || 120_000)
38
+ @scram_server_signature = nil
39
+ @authenticated = false
40
+ @capabilities = nil
41
+ @start_time = Time.now
42
+ @timeout = 30
43
+ @max_attempts = 3
44
+ @attempts = 0
45
+ @lock = Mutex.new
46
+ end
47
+
48
+ def start(client_info = {})
49
+ @lock.synchronize do
50
+ @state = STATE_INIT
51
+ @client_info = client_info
52
+ @attempts += 1
53
+
54
+ client_version = client_info[:protocol_version]
55
+ unless client_version
56
+ return error_response("Protocol version not specified")
57
+ end
58
+
59
+ unless ProtocolVersion.supported?(client_version)
60
+ return error_response("Unsupported protocol version: #{ProtocolVersion.to_string(client_version)}")
61
+ end
62
+
63
+ unless ProtocolVersion.compatible?(client_version, @server_info[:protocol_version])
64
+ return error_response("Incompatible protocol version")
65
+ end
66
+
67
+ @state = STATE_AUTHENTICATING
68
+ authentication_response(client_info)
69
+ end
70
+ end
71
+
72
+ def authenticate(credentials)
73
+ @lock.synchronize do
74
+ return error_response("Handshake not initialized") if @state == STATE_INIT
75
+
76
+ @state = STATE_AUTHENTICATING
77
+
78
+ # The server selects the required mechanism; clients cannot lower
79
+ # the server's authentication policy by requesting "none".
80
+ auth_method = @server_info[:default_auth]
81
+
82
+ case auth_method
83
+ when "none"
84
+ @authenticated = true
85
+ @state = STATE_AUTHENTICATED
86
+ return success_response
87
+ when "password"
88
+ @authenticated = authenticate_password(credentials)
89
+ when "md5"
90
+ @authenticated = authenticate_md5(credentials)
91
+ when "scram-sha-256"
92
+ @authenticated = authenticate_scram(credentials)
93
+ else
94
+ return error_response("Unsupported authentication method: #{auth_method}")
95
+ end
96
+
97
+ if @authenticated
98
+ @state = STATE_AUTHENTICATED
99
+ success_response
100
+ else
101
+ @state = STATE_FAILED
102
+ error_response("Authentication failed")
103
+ end
104
+ end
105
+ end
106
+
107
+ def negotiate(capabilities)
108
+ @lock.synchronize do
109
+ return error_response("Not authenticated") unless @authenticated
110
+
111
+ @state = STATE_NEGOTIATING
112
+ @capabilities = capabilities
113
+
114
+ negotiated = negotiate_capabilities(capabilities)
115
+
116
+ @state = STATE_COMPLETE
117
+ negotiated_response(negotiated)
118
+ end
119
+ end
120
+
121
+ def complete?
122
+ @state == STATE_COMPLETE
123
+ end
124
+
125
+ def authenticated?
126
+ @authenticated
127
+ end
128
+
129
+ def failed?
130
+ @state == STATE_FAILED
131
+ end
132
+
133
+ def timed_out?
134
+ Time.now - @start_time > @timeout
135
+ end
136
+
137
+ def max_attempts_reached?
138
+ @attempts >= @max_attempts
139
+ end
140
+
141
+ def reset
142
+ @lock.synchronize do
143
+ @state = STATE_INIT
144
+ @authenticated = false
145
+ @challenge = nil
146
+ @capabilities = nil
147
+ @attempts = 0
148
+ @start_time = Time.now
149
+ end
150
+ end
151
+
152
+ def to_hash
153
+ {
154
+ state: @state,
155
+ authenticated: @authenticated,
156
+ client_info: @client_info,
157
+ server_info: @server_info,
158
+ capabilities: @capabilities,
159
+ attempts: @attempts,
160
+ elapsed: (Time.now - @start_time).round(2)
161
+ }
162
+ end
163
+
164
+ private
165
+
166
+ def success_response
167
+ response = {
168
+ success: true,
169
+ state: @state,
170
+ server_info: @server_info,
171
+ message: "Handshake successful"
172
+ }
173
+ response[:server_signature] = @scram_server_signature if @scram_server_signature
174
+ response
175
+ end
176
+
177
+ def error_response(message)
178
+ {
179
+ success: false,
180
+ state: @state,
181
+ error: message,
182
+ attempts_remaining: @max_attempts - @attempts
183
+ }
184
+ end
185
+
186
+ def authentication_response(client_info)
187
+ response = {
188
+ success: true,
189
+ auth_methods: @server_info[:auth_methods],
190
+ default_auth: @server_info[:default_auth],
191
+ challenge: generate_challenge
192
+ }
193
+ if @server_info[:default_auth] == "scram-sha-256"
194
+ response[:scram_salt] = Base64.strict_encode64(@scram_salt)
195
+ response[:scram_iterations] = @scram_iterations
196
+ end
197
+ response
198
+ end
199
+
200
+ def negotiated_response(negotiated)
201
+ {
202
+ success: true,
203
+ state: @state,
204
+ negotiated: negotiated,
205
+ server_info: @server_info
206
+ }
207
+ end
208
+
209
+ def generate_challenge
210
+ @challenge = SecureRandom.hex(16)
211
+ end
212
+
213
+ def authenticate_password(credentials)
214
+ username = credentials[:username]
215
+ password = credentials[:password]
216
+ return false if username.nil? || password.nil?
217
+
218
+ users = @credentials[:users] || @credentials["users"]
219
+ if users
220
+ expected = users[username] || users[username.to_s] || users[username.to_sym]
221
+ return secure_equal?(password.to_s, expected.to_s) unless expected.nil?
222
+ return false
223
+ end
224
+
225
+ expected_username = @credentials[:username] || @credentials["username"]
226
+ expected_password = @credentials[:password] || @credentials["password"]
227
+ return false if expected_username.nil? || expected_password.nil?
228
+
229
+ secure_equal?(username.to_s, expected_username.to_s) &&
230
+ secure_equal?(password.to_s, expected_password.to_s)
231
+ end
232
+
233
+ def secure_equal?(left, right)
234
+ return false unless left.bytesize == right.bytesize
235
+
236
+ result = 0
237
+ left.bytes.each_with_index { |byte, index| result |= byte ^ right.getbyte(index) }
238
+ result.zero?
239
+ end
240
+
241
+ def authenticate_md5(credentials)
242
+ username = credentials[:username]
243
+ password = credentials[:password]
244
+
245
+ return false if username.nil? || password.nil?
246
+
247
+ expected = Digest::MD5.hexdigest(password + @challenge)
248
+ actual = credentials[:md5_hash]
249
+
250
+ actual == expected
251
+ end
252
+
253
+ def authenticate_scram(credentials)
254
+ data = credentials[:scram_data]
255
+ return false unless data.is_a?(Hash)
256
+
257
+ username = credentials[:username].to_s
258
+ client_first_bare = data[:client_first_bare] || data["client_first_bare"]
259
+ client_final = data[:client_final_without_proof] || data["client_final_without_proof"]
260
+ proof = data[:proof] || data["proof"]
261
+ client_nonce = data[:client_nonce] || data["client_nonce"]
262
+ return false if [client_first_bare, client_final, proof, client_nonce].any?(&:nil?)
263
+ return false unless client_first_bare.include?("n=#{username},")
264
+ return false unless client_first_bare.include?("r=#{client_nonce}")
265
+
266
+ server_nonce = @challenge.to_s
267
+ expected_nonce = client_nonce.to_s + server_nonce
268
+ return false unless client_final == "c=biws,r=#{expected_nonce}"
269
+
270
+ password = @credentials[:password] || @credentials["password"]
271
+ stored_key = @credentials[:scram_stored_key] || @credentials["scram_stored_key"]
272
+ server_key = @credentials[:scram_server_key] || @credentials["scram_server_key"]
273
+ return false unless password || (stored_key && server_key)
274
+
275
+ salted = OpenSSL::PKCS5.pbkdf2_hmac(password.to_s, @scram_salt, @scram_iterations, 32, OpenSSL::Digest::SHA256.new) if password
276
+ stored_key ||= Digest::SHA256.digest(OpenSSL::HMAC.digest("SHA256", salted, "Client Key"))
277
+ server_key ||= OpenSSL::HMAC.digest("SHA256", salted, "Server Key")
278
+ stored_key = Base64.decode64(stored_key) if stored_key.is_a?(String) && stored_key.match?(/\A[A-Za-z0-9+\/=]+\z/) && stored_key.bytesize != 32
279
+ server_key = Base64.decode64(server_key) if server_key.is_a?(String) && server_key.match?(/\A[A-Za-z0-9+\/=]+\z/) && server_key.bytesize != 32
280
+
281
+ auth_message = [client_first_bare, "r=#{expected_nonce},s=#{Base64.strict_encode64(@scram_salt)},i=#{@scram_iterations}", client_final].join(",")
282
+ proof_bytes = Base64.decode64(proof.to_s)
283
+ client_signature = OpenSSL::HMAC.digest("SHA256", stored_key, auth_message)
284
+ client_key = proof_bytes.bytes.each_with_index.map { |byte, index| byte ^ client_signature.getbyte(index) }.pack("C*")
285
+ valid = secure_equal?(Digest::SHA256.digest(client_key), stored_key)
286
+ if valid
287
+ @scram_server_signature = Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", server_key, auth_message))
288
+ end
289
+ valid
290
+ end
291
+
292
+ def negotiate_capabilities(capabilities)
293
+ {
294
+ protocol_version: ProtocolVersion.current,
295
+ compression: capabilities[:compression] && @server_info[:compression_supported],
296
+ encryption: capabilities[:encryption] && @server_info[:encryption_supported],
297
+ pipeline: capabilities[:pipeline] && @server_info[:pipeline_supported],
298
+ batch_size: [capabilities[:batch_size] || 100, @server_info[:max_batch_size] || 1000].min,
299
+ timeout: [capabilities[:timeout] || 30, @server_info[:max_timeout] || 300].min,
300
+ max_rows: [capabilities[:max_rows] || 10000, @server_info[:max_rows] || 100000].min
301
+ }
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+ require "time"
6
+
7
+ module RubyDB
8
+ module Protocol
9
+ class Message
10
+ TYPE_HANDSHAKE = :handshake
11
+ TYPE_HANDSHAKE_RESPONSE = :handshake_response
12
+ TYPE_QUERY = :query
13
+ TYPE_QUERY_RESPONSE = :query_response
14
+ TYPE_PREPARE = :prepare
15
+ TYPE_PREPARE_RESPONSE = :prepare_response
16
+ TYPE_EXECUTE = :execute
17
+ TYPE_EXECUTE_RESPONSE = :execute_response
18
+ TYPE_CLOSE = :close
19
+ TYPE_CLOSE_RESPONSE = :close_response
20
+ TYPE_PING = :ping
21
+ TYPE_PONG = :pong
22
+ TYPE_ERROR = :error
23
+ TYPE_NOTICE = :notice
24
+ TYPE_PARAMETER_DESCRIPTION = :parameter_description
25
+ TYPE_ROW_DESCRIPTION = :row_description
26
+ TYPE_DATA_ROW = :data_row
27
+ TYPE_COMMAND_COMPLETE = :command_complete
28
+ TYPE_READY_FOR_QUERY = :ready_for_query
29
+ TYPE_BEGIN = :begin
30
+ TYPE_COMMIT = :commit
31
+ TYPE_ROLLBACK = :rollback
32
+ TYPE_SYNCHRONIZE = :synchronize
33
+ TYPE_CANCEL = :cancel
34
+ TYPE_TERMINATE = :terminate
35
+ TYPE_AUTHENTICATION = :authentication
36
+ TYPE_AUTHENTICATION_RESPONSE = :authentication_response
37
+
38
+ attr_reader :type, :id, :created_at, :payload
39
+
40
+ def initialize(type, payload = {})
41
+ @type = type
42
+ @id = "msg_#{Time.now.to_i}_#{rand(10000)}"
43
+ @created_at = Time.now
44
+ @payload = payload
45
+ @compressed = false
46
+ @encrypted = false
47
+ @checksum = nil
48
+ end
49
+
50
+ def compress
51
+ return self if @compressed
52
+ @compressed = true
53
+ self
54
+ end
55
+
56
+ def decompress
57
+ return self unless @compressed
58
+ @compressed = false
59
+ self
60
+ end
61
+
62
+ def encrypt(key)
63
+ return self if @encrypted
64
+ @encrypted = true
65
+ self
66
+ end
67
+
68
+ def decrypt(key)
69
+ return self unless @encrypted
70
+ @encrypted = false
71
+ self
72
+ end
73
+
74
+ def calculate_checksum
75
+ data = "#{@type}#{@id}#{JSON.generate(@payload)}"
76
+ Digest::SHA256.hexdigest(data)[0...16]
77
+ end
78
+
79
+ def verify_checksum
80
+ return true if @checksum.nil?
81
+ @checksum == calculate_checksum
82
+ end
83
+
84
+ def to_hash
85
+ {
86
+ type: @type,
87
+ id: @id,
88
+ created_at: @created_at.iso8601,
89
+ payload: @payload,
90
+ compressed: @compressed,
91
+ encrypted: @encrypted,
92
+ checksum: @checksum
93
+ }
94
+ end
95
+
96
+ def to_json
97
+ JSON.generate(to_hash)
98
+ end
99
+
100
+ def self.from_json(json_data)
101
+ data = JSON.parse(json_data, symbolize_names: true)
102
+ type = data[:type].respond_to?(:to_sym) ? data[:type].to_sym : data[:type]
103
+ msg = new(type, data[:payload] || {})
104
+ msg.instance_variable_set(:@id, data[:id])
105
+ msg.instance_variable_set(:@created_at, Time.parse(data[:created_at]))
106
+ msg.instance_variable_set(:@compressed, data[:compressed] || false)
107
+ msg.instance_variable_set(:@encrypted, data[:encrypted] || false)
108
+ msg.instance_variable_set(:@checksum, data[:checksum])
109
+ msg
110
+ end
111
+
112
+ def inspect
113
+ "#<Message type=#{@type} id=#{@id}>"
114
+ end
115
+
116
+ def to_s
117
+ inspect
118
+ end
119
+ end
120
+ end
121
+ end