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,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyDB
4
+ module Configuration
5
+ # Validation - Configuration validation
6
+ class Validation
7
+ attr_reader :errors, :warnings
8
+
9
+ # Validation rules
10
+ RULES = {
11
+ "server.host" => { type: :string, required: true },
12
+ "server.port" => { type: :integer, required: true, min: 1, max: 65535 },
13
+ "server.max_connections" => { type: :integer, required: true, min: 1, max: 10000 },
14
+ "server.min_workers" => { type: :integer, required: true, min: 1 },
15
+ "server.max_workers" => { type: :integer, required: true, min: 1 },
16
+ "server.worker_queue_size" => { type: :integer, required: true, min: 1 },
17
+ "server.read_timeout" => { type: :integer, required: true, min: 1 },
18
+ "server.write_timeout" => { type: :integer, required: true, min: 1 },
19
+ "server.idle_timeout" => { type: :integer, required: true, min: 1 },
20
+ "server.max_request_size" => { type: :integer, required: true, min: 1024 },
21
+ "server.daemonize" => { type: :boolean, required: true },
22
+ "server.log_level" => { type: :string, required: true, enum: ["debug", "info", "warn", "error", "fatal"] },
23
+
24
+ "storage.data_dir" => { type: :string, required: true },
25
+ "storage.page_size" => { type: :integer, required: true, enum: [4096, 8192, 16384, 32768] },
26
+ "storage.buffer_pool_size" => { type: :integer, required: true, min: 10 },
27
+ "storage.wal_enabled" => { type: :boolean, required: true },
28
+ "storage.wal_dir" => { type: :string, required: true },
29
+ "storage.wal_segment_size" => { type: :integer, required: true, min: 1024 * 1024 },
30
+ "storage.wal_keep_segments" => { type: :integer, required: true, min: 1 },
31
+ "storage.fsync" => { type: :boolean, required: true },
32
+
33
+ "database.name" => { type: :string, required: true },
34
+ "database.encoding" => { type: :string, required: true },
35
+ "database.locale" => { type: :string, required: true },
36
+ "database.default_isolation_level" => { type: :string, required: true, enum: ["read_uncommitted", "read_committed", "repeatable_read", "serializable"] },
37
+ "database.max_connections" => { type: :integer, required: true, min: 1 },
38
+ "database.statement_timeout" => { type: :integer, required: true, min: 1 },
39
+ "database.idle_in_transaction_timeout" => { type: :integer, required: true, min: 1 },
40
+
41
+ "auth.method" => { type: :string, required: true, enum: ["none", "password", "md5", "scram_sha256", "token", "certificate"] },
42
+ "auth.session_timeout" => { type: :integer, required: true, min: 60 },
43
+ "auth.max_attempts" => { type: :integer, required: true, min: 1 },
44
+ "auth.lockout_duration" => { type: :integer, required: true, min: 1 },
45
+ "auth.password_algorithm" => { type: :string, required: true, enum: ["sha256", "sha512", "bcrypt", "pbkdf2", "argon2"] },
46
+ "auth.password_iterations" => { type: :integer, required: true, min: 1000 },
47
+ "auth.salt_length" => { type: :integer, required: true, min: 8 },
48
+
49
+ "ssl.enabled" => { type: :boolean, required: true },
50
+
51
+ "replication.enabled" => { type: :boolean, required: true },
52
+ "replication.role" => { type: :string, required: true, enum: ["standalone", "primary", "replica", "standby"] },
53
+ "replication.sync_mode" => { type: :string, required: true, enum: ["async", "sync", "quorum"] },
54
+ "replication.sync_replicas" => { type: :integer, required: true, min: 0 },
55
+ "replication.replication_port" => { type: :integer, required: true, min: 1, max: 65535 },
56
+ "replication.max_replicas" => { type: :integer, required: true, min: 0 },
57
+
58
+ "backup.backup_dir" => { type: :string, required: true },
59
+ "backup.retention_days" => { type: :integer, required: true, min: 1 },
60
+ "backup.max_backups" => { type: :integer, required: true, min: 1 },
61
+ "backup.verify_after_backup" => { type: :boolean, required: true },
62
+ "backup.include_wal" => { type: :boolean, required: true },
63
+ "backup.include_schema" => { type: :boolean, required: true },
64
+ "backup.compress" => { type: :boolean, required: true },
65
+ "backup.compression_level" => { type: :integer, required: true, min: 1, max: 9 },
66
+
67
+ "logging.log_dir" => { type: :string, required: true },
68
+ "logging.log_level" => { type: :string, required: true, enum: ["debug", "info", "warn", "error", "fatal"] },
69
+ "logging.log_format" => { type: :string, required: true, enum: ["json", "text"] },
70
+ "logging.rotate_size" => { type: :integer, required: true, min: 1024 * 1024 },
71
+ "logging.rotate_count" => { type: :integer, required: true, min: 1 },
72
+ "logging.audit_log" => { type: :boolean, required: true },
73
+ "logging.slow_query_threshold" => { type: :integer, required: true, min: 0 },
74
+
75
+ "monitoring.enabled" => { type: :boolean, required: true },
76
+ "monitoring.metrics_interval" => { type: :integer, required: true, min: 1 },
77
+ "monitoring.health_check_interval" => { type: :integer, required: true, min: 1 },
78
+ "monitoring.stats_collector" => { type: :boolean, required: true },
79
+ "monitoring.prometheus" => { type: :boolean, required: true },
80
+ "monitoring.prometheus_port" => { type: :integer, required: true, min: 1, max: 65535 }
81
+ }
82
+
83
+ def initialize
84
+ @errors = []
85
+ @warnings = []
86
+ @valid = false
87
+ end
88
+
89
+ def validate(config)
90
+ @errors = []
91
+ @warnings = []
92
+ @valid = true
93
+
94
+ RULES.each do |path, rules|
95
+ value = get_value(config, path)
96
+ validate_value(path, value, rules)
97
+ end
98
+
99
+ # Cross-field validations
100
+ validate_cross_fields(config)
101
+
102
+ @valid = @errors.empty?
103
+ @valid
104
+ end
105
+
106
+ def valid?
107
+ @valid
108
+ end
109
+
110
+ def errors
111
+ @errors.dup
112
+ end
113
+
114
+ def warnings
115
+ @warnings.dup
116
+ end
117
+
118
+ private
119
+
120
+ def get_value(config, path)
121
+ parts = path.split(".")
122
+ value = config
123
+ parts.each do |part|
124
+ return nil unless value.is_a?(Hash) && value.key?(part.to_sym)
125
+ value = value[part.to_sym]
126
+ end
127
+ value
128
+ end
129
+
130
+ def validate_value(path, value, rules)
131
+ # Check required
132
+ if rules[:required] && value.nil?
133
+ @errors << "#{path} is required"
134
+ return
135
+ end
136
+
137
+ return if value.nil?
138
+
139
+ # Check type
140
+ case rules[:type]
141
+ when :string
142
+ unless value.is_a?(String)
143
+ @errors << "#{path} must be a string"
144
+ return
145
+ end
146
+ when :integer
147
+ unless value.is_a?(Integer)
148
+ @errors << "#{path} must be an integer"
149
+ return
150
+ end
151
+ when :boolean
152
+ unless [true, false].include?(value)
153
+ @errors << "#{path} must be a boolean"
154
+ return
155
+ end
156
+ end
157
+
158
+ # Check min/max
159
+ if rules[:min] && value < rules[:min]
160
+ @errors << "#{path} must be at least #{rules[:min]}"
161
+ end
162
+
163
+ if rules[:max] && value > rules[:max]
164
+ @errors << "#{path} must be at most #{rules[:max]}"
165
+ end
166
+
167
+ # Check enum
168
+ if rules[:enum] && !rules[:enum].include?(value)
169
+ @errors << "#{path} must be one of: #{rules[:enum].join(', ')}"
170
+ end
171
+
172
+ # Check format
173
+ if rules[:format] && value !~ rules[:format]
174
+ @errors << "#{path} has invalid format"
175
+ end
176
+ end
177
+
178
+ def validate_cross_fields(config)
179
+ # Server validation
180
+ if config[:server]
181
+ if config[:server][:min_workers] > config[:server][:max_workers]
182
+ @errors << "server.min_workers must be less than or equal to server.max_workers"
183
+ end
184
+
185
+ if config[:server][:max_connections] < config[:server][:max_workers]
186
+ @warnings << "server.max_connections should be greater than server.max_workers"
187
+ end
188
+ end
189
+
190
+ # Storage validation
191
+ if config[:storage]
192
+ if config[:storage][:page_size] && config[:storage][:page_size] < 4096
193
+ @warnings << "storage.page_size is unusually small (recommended: 8192)"
194
+ end
195
+
196
+ if config[:storage][:buffer_pool_size] && config[:storage][:buffer_pool_size] < 10
197
+ @errors << "storage.buffer_pool_size must be at least 10"
198
+ end
199
+ end
200
+
201
+ # Replication validation
202
+ if config[:replication] && config[:replication][:enabled]
203
+ if config[:replication][:role] == "replica" && config[:replication][:primary_host].nil?
204
+ @errors << "replication.primary_host is required for replica role"
205
+ end
206
+
207
+ if config[:replication][:role] == "primary" && config[:replication][:sync_replicas] > config[:replication][:max_replicas]
208
+ @warnings << "replication.sync_replicas should not exceed replication.max_replicas"
209
+ end
210
+ end
211
+
212
+ # SSL validation
213
+ if config[:ssl] && config[:ssl][:enabled]
214
+ if config[:ssl][:cert_file].nil? || config[:ssl][:key_file].nil?
215
+ @errors << "ssl.cert_file and ssl.key_file are required when SSL is enabled"
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RubyDB constants and configuration
4
+
5
+ module RubyDB
6
+ # Database constants
7
+ module Constants
8
+ # Page size in bytes - 8KB default (same as PostgreSQL)
9
+ # This is a good balance between performance and storage efficiency
10
+ DEFAULT_PAGE_SIZE = 8192
11
+
12
+ # Page size options
13
+ PAGE_SIZES = [4096, 8192, 16384, 32768].freeze
14
+
15
+ # Database file extension
16
+ FILE_EXTENSION = ".rdb"
17
+
18
+ # WAL file extension
19
+ WAL_EXTENSION = ".rdb-wal"
20
+
21
+ # Shared memory file extension (for WAL)
22
+ SHM_EXTENSION = ".rdb-shm"
23
+
24
+ # Default port for RubyDB server
25
+ DEFAULT_PORT = 7432
26
+
27
+ # Maximum connections (server mode)
28
+ DEFAULT_MAX_CONNECTIONS = 100
29
+
30
+ # Buffer pool size (number of pages cached in memory)
31
+ DEFAULT_BUFFER_POOL_SIZE = 1000
32
+
33
+ # WAL segment size (16MB default)
34
+ DEFAULT_WAL_SEGMENT_SIZE = 16 * 1024 * 1024
35
+
36
+ # Maximum SQL statement length
37
+ MAX_SQL_LENGTH = 1_000_000
38
+
39
+ # Maximum identifier length
40
+ MAX_IDENTIFIER_LENGTH = 63
41
+
42
+ # Transaction isolation levels
43
+ module IsolationLevel
44
+ READ_UNCOMMITTED = 0
45
+ READ_COMMITTED = 1
46
+ REPEATABLE_READ = 2
47
+ SERIALIZABLE = 3
48
+ end
49
+
50
+ # Log levels
51
+ module LogLevel
52
+ DEBUG = 0
53
+ INFO = 1
54
+ WARN = 2
55
+ ERROR = 3
56
+ FATAL = 4
57
+ end
58
+ end
59
+
60
+ # Error codes
61
+ module ErrorCodes
62
+ OK = 0
63
+ ERROR = 1
64
+ IO_ERROR = 2
65
+ CORRUPTION = 3
66
+ CONSTRAINT_VIOLATION = 4
67
+ TRANSACTION_ABORTED = 5
68
+ LOCK_TIMEOUT = 6
69
+ DEADLOCK_DETECTED = 7
70
+ PERMISSION_DENIED = 8
71
+ NOT_FOUND = 9
72
+ ALREADY_EXISTS = 10
73
+ end
74
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyDB
4
+ module Constraints
5
+ # CheckConstraint - Ensures row satisfies a condition
6
+ class CheckConstraint < Constraint
7
+ attr_reader :expression
8
+
9
+ def initialize(table_name, expression, options = {})
10
+ super(
11
+ options[:name] || "chk_#{table_name}_#{Time.now.to_i}",
12
+ TYPE_CHECK,
13
+ table_name,
14
+ options
15
+ )
16
+ @expression = expression
17
+ @expression_type = options[:expression_type] || :sql
18
+ @validated = false
19
+ end
20
+
21
+ def validate(row)
22
+ return true unless enabled?
23
+
24
+ result = evaluate_expression(row)
25
+ if result == false
26
+ @validation_errors << "Check constraint failed: #{@expression}"
27
+ return false
28
+ end
29
+
30
+ true
31
+ end
32
+
33
+ def validate_batch(rows)
34
+ results = { valid: [], invalid: [] }
35
+
36
+ rows.each do |row|
37
+ if validate(row)
38
+ results[:valid] << row
39
+ else
40
+ results[:invalid] << row
41
+ end
42
+ end
43
+
44
+ results
45
+ end
46
+
47
+ def evaluate_expression(row)
48
+ case @expression_type
49
+ when :ruby
50
+ # Evaluate Ruby expression
51
+ eval_expression(@expression, row)
52
+ when :lambda
53
+ # Call lambda with row
54
+ @expression.call(row)
55
+ else
56
+ # Evaluate SQL expression - simplified
57
+ eval_sql_expression(@expression, row)
58
+ end
59
+ rescue => e
60
+ @validation_errors << "Expression evaluation error: #{e.message}"
61
+ false
62
+ end
63
+
64
+ def eval_expression(expr, row)
65
+ case expr
66
+ when Symbol, String
67
+ evaluate_predicate(expr.to_s.strip, row)
68
+ when Proc
69
+ expr.call(row)
70
+ else
71
+ true
72
+ end
73
+ end
74
+
75
+ def eval_sql_expression(expr, row)
76
+ evaluate_predicate(expr.to_s.strip, row)
77
+ end
78
+
79
+ def evaluate_predicate(expression, row)
80
+ expression = strip_outer_parentheses(expression.strip)
81
+
82
+ split_top_level(expression, "OR").any? do |part|
83
+ split_top_level(part, "AND").all? { |term| evaluate_term(term, row) }
84
+ end
85
+ end
86
+
87
+ def evaluate_term(term, row)
88
+ term = strip_outer_parentheses(term.strip)
89
+ if term =~ /\A([\w.]+)\s+IS\s+(NOT\s+)?NULL\z/i
90
+ value = row_value(row, Regexp.last_match(1))
91
+ return Regexp.last_match(2).nil? ? value.nil? : !value.nil?
92
+ end
93
+
94
+ match = term.match(/\A([\w.]+)\s*(>=|<=|<>|!=|=|>|<)\s*(.+)\z/i)
95
+ return false unless match
96
+
97
+ compare_value(row_value(row, match[1]), match[3].strip, match[2])
98
+ end
99
+
100
+ def row_value(row, column)
101
+ key = column.to_s.split(".").last
102
+ row.key?(key) ? row[key] : row[key.to_sym]
103
+ end
104
+
105
+ def strip_outer_parentheses(expression)
106
+ while expression.start_with?("(") && expression.end_with?( ")") && balanced_parentheses?(expression[1...-1])
107
+ expression = expression[1...-1].strip
108
+ end
109
+ expression
110
+ end
111
+
112
+ def balanced_parentheses?(expression)
113
+ depth = 0
114
+ expression.each_char do |char|
115
+ depth += 1 if char == "("
116
+ depth -= 1 if char == ")"
117
+ return false if depth.negative?
118
+ end
119
+ depth.zero?
120
+ end
121
+
122
+ def split_top_level(expression, operator)
123
+ parts = []
124
+ depth = 0
125
+ start = 0
126
+ expression.to_enum(:scan, /\(|\)|\b#{operator}\b/i).each do
127
+ match = Regexp.last_match
128
+ token = match[0]
129
+ if token == "("
130
+ depth += 1
131
+ elsif token == ")"
132
+ depth -= 1
133
+ elsif depth.zero?
134
+ parts << expression[start...match.begin(0)].strip
135
+ start = match.end(0)
136
+ end
137
+ end
138
+ parts << expression[start..].strip
139
+ parts.size > 1 ? parts : [expression]
140
+ end
141
+
142
+ def compare_value(row_val, val, op)
143
+ # Parse the value
144
+ parsed_val = case val
145
+ when /^'(.+)'$/ then $1
146
+ when /^\d+$/ then val.to_i
147
+ when /^\d+\.\d+$/ then val.to_f
148
+ when /^true$/i then true
149
+ when /^false$/i then false
150
+ when /^null$/i then nil
151
+ else val
152
+ end
153
+
154
+ case op
155
+ when "=" then row_val == parsed_val
156
+ when "!=", "<>" then row_val != parsed_val
157
+ when ">" then row_val > parsed_val
158
+ when ">=" then row_val >= parsed_val
159
+ when "<" then row_val < parsed_val
160
+ when "<=" then row_val <= parsed_val
161
+ else false
162
+ end
163
+ end
164
+
165
+ def to_sql
166
+ "CHECK (#{@expression})"
167
+ end
168
+
169
+ def to_hash
170
+ super.merge({
171
+ expression: @expression,
172
+ expression_type: @expression_type
173
+ })
174
+ end
175
+
176
+ def inspect
177
+ "#<CheckConstraint name=#{@name} expression=#{@expression}>"
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyDB
4
+ module Constraints
5
+ # Constraint - Base class for all constraints
6
+ class Constraint
7
+ attr_reader :name, :type, :table_name, :options, :created_at, :validation_errors
8
+
9
+ # Constraint types
10
+ TYPE_PRIMARY_KEY = :primary_key
11
+ TYPE_FOREIGN_KEY = :foreign_key
12
+ TYPE_UNIQUE = :unique
13
+ TYPE_NOT_NULL = :not_null
14
+ TYPE_CHECK = :check
15
+
16
+ def initialize(name, type, table_name, options = {})
17
+ @name = name || generate_name(type, table_name)
18
+ @type = type
19
+ @table_name = table_name
20
+ @options = options
21
+ @created_at = Time.now
22
+ @enabled = options[:enabled] != false
23
+ @deferrable = options[:deferrable] || false
24
+ @deferred = options[:deferred] || false
25
+ @validated = false
26
+ @validation_errors = []
27
+ end
28
+
29
+ def validate(row)
30
+ raise NotImplementedError, "#{self.class} must implement #validate"
31
+ end
32
+
33
+ def validate_batch(rows)
34
+ results = { valid: [], invalid: [] }
35
+ rows.each do |row|
36
+ if validate(row)
37
+ results[:valid] << row
38
+ else
39
+ results[:invalid] << row
40
+ end
41
+ end
42
+ results
43
+ end
44
+
45
+ def enabled?
46
+ @enabled
47
+ end
48
+
49
+ def enable
50
+ @enabled = true
51
+ end
52
+
53
+ def disable
54
+ @enabled = false
55
+ end
56
+
57
+ def deferrable?
58
+ @deferrable
59
+ end
60
+
61
+ def deferred?
62
+ @deferred
63
+ end
64
+
65
+ def defer
66
+ @deferred = true if @deferrable
67
+ end
68
+
69
+ def immediate
70
+ @deferred = false if @deferrable
71
+ end
72
+
73
+ def to_sql
74
+ raise NotImplementedError, "#{self.class} must implement #to_sql"
75
+ end
76
+
77
+ def to_hash
78
+ {
79
+ name: @name,
80
+ type: @type,
81
+ table_name: @table_name,
82
+ options: @options,
83
+ enabled: @enabled,
84
+ deferrable: @deferrable,
85
+ deferred: @deferred,
86
+ created_at: @created_at.iso8601
87
+ }
88
+ end
89
+
90
+ def inspect
91
+ "#<Constraint name=#{@name} type=#{@type} table=#{@table_name}>"
92
+ end
93
+
94
+ private
95
+
96
+ def generate_name(type, table_name)
97
+ "#{table_name}_#{type}_#{Time.now.to_i}_#{rand(1000)}"
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyDB
4
+ module Constraints
5
+ # ForeignKeyConstraint - Ensures referential integrity
6
+ class ForeignKeyConstraint < Constraint
7
+ attr_reader :columns, :reference_table, :reference_columns
8
+ attr_reader :on_delete, :on_update, :match_type
9
+
10
+ # Referential actions
11
+ ACTION_RESTRICT = :restrict
12
+ ACTION_CASCADE = :cascade
13
+ ACTION_SET_NULL = :set_null
14
+ ACTION_SET_DEFAULT = :set_default
15
+ ACTION_NO_ACTION = :no_action
16
+
17
+ def initialize(table_name, columns, reference_table, reference_columns = nil, options = {})
18
+ super(
19
+ options[:name] || "fk_#{table_name}_#{reference_table}",
20
+ TYPE_FOREIGN_KEY,
21
+ table_name,
22
+ options
23
+ )
24
+ @columns = columns.is_a?(Array) ? columns : [columns]
25
+ @reference_table = reference_table
26
+ @reference_columns = reference_columns.is_a?(Array) ? reference_columns : [reference_columns || :id]
27
+ @on_delete = options[:on_delete] || ACTION_NO_ACTION
28
+ @on_update = options[:on_update] || ACTION_NO_ACTION
29
+ @match_type = options[:match] || :simple
30
+ @validated = false
31
+ @reference_cache = {}
32
+ end
33
+
34
+ def validate(row)
35
+ return true unless enabled?
36
+
37
+ # Check all columns are present
38
+ @columns.each do |col|
39
+ if row[col].nil?
40
+ @validation_errors << "Column '#{col}' cannot be NULL"
41
+ return false
42
+ end
43
+ end
44
+
45
+ # Check if referenced row exists
46
+ key = @columns.map { |col| row[col] }.join("||")
47
+ return true if @reference_cache.key?(key)
48
+
49
+ # Check reference table
50
+ ref_exists = check_reference_exists(row)
51
+ if ref_exists
52
+ @reference_cache[key] = true
53
+ true
54
+ else
55
+ @validation_errors << "Referenced row does not exist: #{key}"
56
+ false
57
+ end
58
+ end
59
+
60
+ def validate_batch(rows)
61
+ @reference_cache.clear
62
+ results = { valid: [], invalid: [] }
63
+
64
+ rows.each do |row|
65
+ if validate(row)
66
+ results[:valid] << row
67
+ else
68
+ results[:invalid] << row
69
+ end
70
+ end
71
+
72
+ results
73
+ end
74
+
75
+ def check_reference_exists(row)
76
+ lookup = @options[:reference_lookup]
77
+ return false unless lookup.respond_to?(:call)
78
+
79
+ conditions = {}
80
+ @columns.each_with_index do |col, idx|
81
+ ref_col = @reference_columns[idx] || @reference_columns[0]
82
+ value = row.key?(col) ? row[col] : row[col.to_sym]
83
+ conditions[ref_col] = value
84
+ end
85
+
86
+ !!lookup.call(@reference_table, conditions)
87
+ end
88
+
89
+ def check_referential_integrity(table_name, row_id)
90
+ return false unless table_name.to_s == @table_name.to_s
91
+
92
+ lookup = @options[:reference_lookup]
93
+ return false unless lookup.respond_to?(:call)
94
+
95
+ # This API is used for a referenced-row delete check. The lookup is
96
+ # intentionally authoritative: inability to query the referencing
97
+ # table fails closed instead of allowing an orphaned row.
98
+ conditions = { @reference_columns.first => row_id }
99
+ !lookup.call(@reference_table, conditions)
100
+ end
101
+
102
+ def to_sql
103
+ cols = @columns.join(", ")
104
+ ref_cols = @reference_columns.join(", ")
105
+ sql = "FOREIGN KEY (#{cols}) REFERENCES #{@reference_table}(#{ref_cols})"
106
+ sql << " ON DELETE #{@on_delete.to_s.upcase}" if @on_delete != ACTION_NO_ACTION
107
+ sql << " ON UPDATE #{@on_update.to_s.upcase}" if @on_update != ACTION_NO_ACTION
108
+ # MATCH SIMPLE is the SQL default and is not part of RubyDB's
109
+ # documented parser surface. Emit only an explicitly non-default mode.
110
+ sql << " MATCH #{@match_type.to_s.upcase}" unless @match_type == :simple
111
+ sql
112
+ end
113
+
114
+ def to_hash
115
+ super.merge({
116
+ columns: @columns,
117
+ reference_table: @reference_table,
118
+ reference_columns: @reference_columns,
119
+ on_delete: @on_delete,
120
+ on_update: @on_update,
121
+ match_type: @match_type
122
+ })
123
+ end
124
+
125
+ def inspect
126
+ "#<ForeignKeyConstraint name=#{@name} columns=#{@columns.join(', ')} references=#{@reference_table}>"
127
+ end
128
+ end
129
+ end
130
+ end