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,470 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module RubyDB
6
+ module Indexes
7
+ # IndexScan - Executes index scans for queries with full production features
8
+ class IndexScan
9
+ attr_reader :index, :scan_type, :conditions, :stats
10
+
11
+ SCAN_TYPE_EQUAL = :equal
12
+ SCAN_TYPE_RANGE = :range
13
+ SCAN_TYPE_PREFIX = :prefix
14
+ SCAN_TYPE_FULL = :full
15
+ SCAN_TYPE_MULTI_KEY = :multi_key
16
+
17
+ def initialize(index, scan_type = SCAN_TYPE_FULL, conditions = {})
18
+ @index = index
19
+ @scan_type = scan_type
20
+ @conditions = conditions
21
+ @cursor = nil
22
+ @results = nil
23
+ @position = 0
24
+ @lock = Mutex.new
25
+ @limit = conditions[:limit]
26
+ @offset = conditions[:offset] || 0
27
+ @stats = {
28
+ rows_scanned: 0,
29
+ keys_checked: 0,
30
+ matches_found: 0,
31
+ scan_time_ms: 0,
32
+ index_pages_accessed: 0
33
+ }
34
+ @executed = false
35
+ @filter = conditions[:filter]
36
+ @order = conditions[:order] || :asc
37
+ @distinct = conditions[:distinct] || false
38
+ end
39
+
40
+ def execute
41
+ @lock.synchronize do
42
+ start_time = Time.now
43
+ @results = []
44
+
45
+ case @scan_type
46
+ when SCAN_TYPE_EQUAL
47
+ execute_equal_scan
48
+ when SCAN_TYPE_RANGE
49
+ execute_range_scan
50
+ when SCAN_TYPE_PREFIX
51
+ execute_prefix_scan
52
+ when SCAN_TYPE_MULTI_KEY
53
+ execute_multi_key_scan
54
+ else
55
+ execute_full_scan
56
+ end
57
+
58
+ # Apply limit and offset
59
+ if @limit
60
+ @results = @results[@offset, @limit]
61
+ elsif @offset > 0
62
+ @results = @results[@offset..-1] || []
63
+ end
64
+
65
+ # Apply distinct if needed
66
+ if @distinct
67
+ @results = distinct_results(@results)
68
+ end
69
+
70
+ @executed = true
71
+ @stats[:scan_time_ms] = ((Time.now - start_time) * 1000).round(2)
72
+ @stats[:matches_found] = @results.size
73
+ @results
74
+ end
75
+ end
76
+
77
+ def next
78
+ @lock.synchronize do
79
+ return nil if @results.nil?
80
+ return nil if @position >= @results.size
81
+
82
+ result = @results[@position]
83
+ @position += 1
84
+ result
85
+ end
86
+ end
87
+
88
+ def next_batch(batch_size = 100)
89
+ @lock.synchronize do
90
+ return [] if @results.nil? || @position >= @results.size
91
+
92
+ batch = @results[@position, batch_size] || []
93
+ @position += batch.size
94
+ batch
95
+ end
96
+ end
97
+
98
+ def reset
99
+ @lock.synchronize do
100
+ @position = 0
101
+ @results = nil
102
+ @executed = false
103
+ @cursor = nil if @cursor
104
+ end
105
+ end
106
+
107
+ def row_ids
108
+ @lock.synchronize do
109
+ @results ? @results.map { |r| r[:row_id] } : []
110
+ end
111
+ end
112
+
113
+ def keys
114
+ @lock.synchronize do
115
+ @results ? @results.map { |r| r[:key] } : []
116
+ end
117
+ end
118
+
119
+ def count
120
+ @results ? @results.size : 0
121
+ end
122
+
123
+ def estimate_cost
124
+ case @scan_type
125
+ when SCAN_TYPE_EQUAL
126
+ 1
127
+ when SCAN_TYPE_PREFIX
128
+ @index.entries_count / 10
129
+ when SCAN_TYPE_RANGE
130
+ @index.entries_count / 2
131
+ when SCAN_TYPE_MULTI_KEY
132
+ @index.entries_count * 0.75
133
+ else
134
+ @index.entries_count
135
+ end
136
+ end
137
+
138
+ def estimate_rows
139
+ case @scan_type
140
+ when SCAN_TYPE_EQUAL
141
+ 1
142
+ when SCAN_TYPE_PREFIX
143
+ @index.entries_count / 20
144
+ when SCAN_TYPE_RANGE
145
+ @index.entries_count / 3
146
+ else
147
+ @index.entries_count
148
+ end
149
+ end
150
+
151
+ def index_used?
152
+ @executed && @results && @results.any?
153
+ end
154
+
155
+ def scan_stats
156
+ @stats.merge({
157
+ scan_type: @scan_type,
158
+ index_type: @index.type,
159
+ index_name: @index.name,
160
+ table_name: @index.table_name,
161
+ columns: @index.columns,
162
+ entries_count: @index.entries_count,
163
+ limit: @limit,
164
+ offset: @offset
165
+ })
166
+ end
167
+
168
+ private
169
+
170
+ def execute_equal_scan
171
+ key = @conditions[:key]
172
+ return [] unless key
173
+
174
+ @stats[:keys_checked] += 1
175
+
176
+ if @index.type == :btree || @index.type == :hash
177
+ row_ids = @index.search(key)
178
+ @stats[:rows_scanned] = row_ids.size
179
+ @stats[:index_pages_accessed] = 1
180
+
181
+ @results = row_ids.map do |row_id|
182
+ { row_id: row_id, key: key }
183
+ end
184
+ else
185
+ @results = []
186
+ end
187
+
188
+ # Apply filter if present
189
+ apply_filter if @filter
190
+
191
+ @results
192
+ end
193
+
194
+ def execute_range_scan
195
+ start_key = @conditions[:start_key]
196
+ end_key = @conditions[:end_key]
197
+ inclusive_start = @conditions[:inclusive_start] != false
198
+ inclusive_end = @conditions[:inclusive_end] != false
199
+
200
+ if @index.type == :btree
201
+ results = @index.range_search(start_key, end_key)
202
+ @stats[:rows_scanned] = results.size
203
+ @stats[:index_pages_accessed] = (results.size / 10.0).ceil + 1
204
+
205
+ @results = results.map do |r|
206
+ { row_id: r[:value], key: r[:key] }
207
+ end
208
+
209
+ # Filter by inclusivity
210
+ if !inclusive_start && start_key
211
+ @results.reject! { |r| r[:key] == start_key }
212
+ end
213
+ if !inclusive_end && end_key
214
+ @results.reject! { |r| r[:key] == end_key }
215
+ end
216
+ else
217
+ # For non-BTree indexes, fall back to full scan with filtering
218
+ execute_full_scan_with_range_filter(start_key, end_key, inclusive_start, inclusive_end)
219
+ end
220
+
221
+ apply_filter if @filter
222
+ @results
223
+ end
224
+
225
+ def execute_prefix_scan
226
+ prefix = @conditions[:prefix]
227
+ return [] unless prefix
228
+
229
+ if @index.type == :btree
230
+ # For prefix scan, find keys starting with prefix
231
+ start_key = prefix
232
+ end_key = prefix + "\xFF\xFF\xFF" # Max possible suffix
233
+
234
+ results = @index.range_search(start_key, end_key)
235
+ @stats[:rows_scanned] = results.size
236
+ @stats[:index_pages_accessed] = (results.size / 20.0).ceil + 1
237
+
238
+ @results = results.map do |r|
239
+ { row_id: r[:value], key: r[:key] }
240
+ end
241
+ else
242
+ # For hash indexes, full scan with prefix filter
243
+ execute_full_scan_with_prefix_filter(prefix)
244
+ end
245
+
246
+ apply_filter if @filter
247
+ @results
248
+ end
249
+
250
+ def execute_multi_key_scan
251
+ keys = @conditions[:keys]
252
+ return [] unless keys.is_a?(Array) && keys.any?
253
+
254
+ @stats[:keys_checked] = keys.size
255
+
256
+ all_row_ids = []
257
+ keys.each do |key|
258
+ if @index.type == :btree || @index.type == :hash
259
+ row_ids = @index.search(key)
260
+ all_row_ids.concat(row_ids.map { |rid| { row_id: rid, key: key } })
261
+ end
262
+ end
263
+
264
+ # Remove duplicates if needed
265
+ if @conditions[:distinct_keys] != false
266
+ seen = Set.new
267
+ all_row_ids = all_row_ids.select do |entry|
268
+ key = entry[:key]
269
+ if seen.include?(key)
270
+ false
271
+ else
272
+ seen.add(key)
273
+ true
274
+ end
275
+ end
276
+ end
277
+
278
+ @stats[:rows_scanned] = all_row_ids.size
279
+ @stats[:index_pages_accessed] = all_row_ids.size
280
+ @results = all_row_ids
281
+
282
+ apply_filter if @filter
283
+ @results
284
+ end
285
+
286
+ def execute_full_scan
287
+ @results = []
288
+
289
+ if @index.type == :btree
290
+ # Use cursor for B-Tree
291
+ @cursor = BTreeCursor.new(@index)
292
+ @cursor.first
293
+
294
+ page_count = 0
295
+ while (entry = @cursor.next)
296
+ @stats[:rows_scanned] += 1
297
+ @stats[:keys_checked] += 1
298
+ page_count += 1 if page_count % 50 == 0
299
+
300
+ # Apply filter early if possible
301
+ if @filter
302
+ next unless apply_filter_to_entry(entry)
303
+ end
304
+
305
+ @results << { row_id: entry[:value], key: entry[:key] }
306
+ break if @limit && @results.size >= @limit + @offset
307
+ end
308
+ @stats[:index_pages_accessed] = (page_count / 50.0).ceil + 1
309
+ else
310
+ # For hash indexes, scan all buckets
311
+ @results = scan_hash_index_full
312
+ @stats[:rows_scanned] = @results.size
313
+ @stats[:index_pages_accessed] = 1
314
+ end
315
+
316
+ apply_filter if @filter && !@results.empty?
317
+ @results
318
+ end
319
+
320
+ def execute_full_scan_with_range_filter(start_key, end_key, inclusive_start, inclusive_end)
321
+ @results = []
322
+
323
+ if @index.type == :btree
324
+ @cursor = BTreeCursor.new(@index)
325
+ @cursor.seek(start_key) if start_key
326
+ @cursor.first unless start_key
327
+
328
+ while (entry = @cursor.next)
329
+ break if end_key && entry[:key] > end_key
330
+
331
+ @stats[:rows_scanned] += 1
332
+ @stats[:keys_checked] += 1
333
+
334
+ # Check inclusivity
335
+ if start_key && entry[:key] == start_key && !inclusive_start
336
+ next
337
+ end
338
+ if end_key && entry[:key] == end_key && !inclusive_end
339
+ next
340
+ end
341
+
342
+ if @filter
343
+ next unless apply_filter_to_entry(entry)
344
+ end
345
+
346
+ @results << { row_id: entry[:value], key: entry[:key] }
347
+ break if @limit && @results.size >= @limit + @offset
348
+ end
349
+ @stats[:index_pages_accessed] = (results.size / 20.0).ceil + 1
350
+ else
351
+ # Hash index - scan all and filter
352
+ scan_hash_index_full.each do |entry|
353
+ next if start_key && entry[:key] < start_key
354
+ next if end_key && entry[:key] > end_key
355
+
356
+ if start_key && entry[:key] == start_key && !inclusive_start
357
+ next
358
+ end
359
+ if end_key && entry[:key] == end_key && !inclusive_end
360
+ next
361
+ end
362
+
363
+ @results << entry
364
+ end
365
+ end
366
+
367
+ @results
368
+ end
369
+
370
+ def execute_full_scan_with_prefix_filter(prefix)
371
+ @results = []
372
+
373
+ if @index.type == :btree
374
+ @cursor = BTreeCursor.new(@index)
375
+ @cursor.first
376
+
377
+ while (entry = @cursor.next)
378
+ key_str = entry[:key].to_s
379
+ next unless key_str.start_with?(prefix.to_s)
380
+
381
+ @stats[:rows_scanned] += 1
382
+ @stats[:keys_checked] += 1
383
+
384
+ if @filter
385
+ next unless apply_filter_to_entry(entry)
386
+ end
387
+
388
+ @results << { row_id: entry[:value], key: entry[:key] }
389
+ break if @limit && @results.size >= @limit + @offset
390
+ end
391
+ else
392
+ scan_hash_index_full.each do |entry|
393
+ key_str = entry[:key].to_s
394
+ next unless key_str.start_with?(prefix.to_s)
395
+ @results << entry
396
+ end
397
+ end
398
+
399
+ @results
400
+ end
401
+
402
+ def scan_hash_index_full
403
+ # Access the internal hash table of the hash index
404
+ # This is a production implementation that iterates through all buckets
405
+ results = []
406
+ hash_table = @index.instance_variable_get(:@hash_table)
407
+
408
+ if hash_table
409
+ hash_table.each do |_bucket_key, bucket|
410
+ bucket.each do |entry|
411
+ @stats[:rows_scanned] += 1
412
+ @stats[:keys_checked] += 1
413
+
414
+ if @filter
415
+ next unless apply_filter_to_entry(entry)
416
+ end
417
+
418
+ results << { row_id: entry[:row_id], key: entry[:key] }
419
+ break if @limit && results.size >= @limit + @offset
420
+ end
421
+ break if @limit && results.size >= @limit + @offset
422
+ end
423
+ end
424
+
425
+ results
426
+ end
427
+
428
+ def apply_filter
429
+ return unless @filter && @results.any?
430
+
431
+ @results.select! do |result|
432
+ apply_filter_to_entry(result)
433
+ end
434
+ end
435
+
436
+ def apply_filter_to_entry(entry)
437
+ return true unless @filter
438
+
439
+ key = entry[:key] || entry[:row_id]
440
+ value = entry[:row_id] || entry[:value]
441
+
442
+ @filter.call(key, value)
443
+ end
444
+
445
+ def distinct_results(results)
446
+ seen = Set.new
447
+ results.select do |result|
448
+ key = result[:key]
449
+ if seen.include?(key)
450
+ false
451
+ else
452
+ seen.add(key)
453
+ true
454
+ end
455
+ end
456
+ end
457
+
458
+ def find_internal_hash_table
459
+ # Try to get hash table from index
460
+ if @index.respond_to?(:hash_table)
461
+ @index.hash_table
462
+ elsif @index.instance_variable_defined?(:@hash_table)
463
+ @index.instance_variable_get(:@hash_table)
464
+ else
465
+ {}
466
+ end
467
+ end
468
+ end
469
+ end
470
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "json"
5
+ require "digest"
6
+
7
+ module RubyDB
8
+ module Migrations
9
+ # Migration - Represents a single database migration
10
+ class Migration
11
+ attr_reader :version, :name, :description, :up_operations
12
+ attr_reader :down_operations, :created_at, :applied_at
13
+ attr_reader :author, :dependencies, :checksum
14
+
15
+ # Migration states
16
+ STATE_PENDING = :pending
17
+ STATE_APPLIED = :applied
18
+ STATE_FAILED = :failed
19
+ STATE_ROLLED_BACK = :rolled_back
20
+
21
+ def initialize(version, name, options = {})
22
+ @version = version
23
+ @name = name
24
+ @description = options[:description] || ""
25
+ @up_operations = []
26
+ @down_operations = []
27
+ @created_at = Time.now
28
+ @applied_at = nil
29
+ @state = STATE_PENDING
30
+ @author = options[:author]
31
+ @dependencies = options[:dependencies] || []
32
+ @checksum = nil
33
+ @metadata = options[:metadata] || {}
34
+ end
35
+
36
+ def up(&block)
37
+ if block_given?
38
+ @up_operations << block
39
+ end
40
+ self
41
+ end
42
+
43
+ def down(&block)
44
+ if block_given?
45
+ @down_operations << block
46
+ end
47
+ self
48
+ end
49
+
50
+ def apply_up(engine)
51
+ @up_operations.each do |operation|
52
+ operation.call(engine)
53
+ end
54
+ @state = STATE_APPLIED
55
+ @applied_at = Time.now
56
+ calculate_checksum
57
+ end
58
+
59
+ def apply_down(engine)
60
+ @down_operations.reverse_each do |operation|
61
+ operation.call(engine)
62
+ end
63
+ @state = STATE_ROLLED_BACK
64
+ end
65
+
66
+ def applied?
67
+ @state == STATE_APPLIED
68
+ end
69
+
70
+ def pending?
71
+ @state == STATE_PENDING
72
+ end
73
+
74
+ def failed?
75
+ @state == STATE_FAILED
76
+ end
77
+
78
+ # Return a stable content checksum even before the migration is applied.
79
+ # The checksum must not include timestamps or object identity so the same
80
+ # migration loaded in a later process produces the same value.
81
+ def checksum
82
+ @checksum || calculate_checksum
83
+ end
84
+
85
+ def rolled_back?
86
+ @state == STATE_ROLLED_BACK
87
+ end
88
+
89
+ def mark_failed
90
+ @state = STATE_FAILED
91
+ end
92
+
93
+ def to_sql
94
+ recorder = SQLRecorder.new
95
+ @up_operations.each { |operation| operation.call(recorder) }
96
+ recorder.to_sql
97
+ end
98
+
99
+ def to_hash
100
+ {
101
+ version: @version,
102
+ name: @name,
103
+ description: @description,
104
+ created_at: @created_at.iso8601,
105
+ applied_at: @applied_at&.iso8601,
106
+ state: @state,
107
+ author: @author,
108
+ dependencies: @dependencies,
109
+ checksum: @checksum,
110
+ metadata: @metadata,
111
+ up_operations_count: @up_operations.size,
112
+ down_operations_count: @down_operations.size
113
+ }
114
+ end
115
+
116
+ def to_json
117
+ JSON.generate(to_hash)
118
+ end
119
+
120
+ def self.from_json(json_data)
121
+ data = JSON.parse(json_data, symbolize_names: true)
122
+ migration = new(data[:version], data[:name],
123
+ description: data[:description],
124
+ author: data[:author],
125
+ dependencies: data[:dependencies] || [],
126
+ metadata: data[:metadata] || {}
127
+ )
128
+ migration.instance_variable_set(:@created_at, Time.parse(data[:created_at]))
129
+ migration.instance_variable_set(:@applied_at, Time.parse(data[:applied_at])) if data[:applied_at]
130
+ migration.instance_variable_set(:@state, data[:state].to_sym)
131
+ migration.instance_variable_set(:@checksum, data[:checksum])
132
+ migration
133
+ end
134
+
135
+ def inspect
136
+ "#<Migration version=#{@version} name=#{@name} state=#{@state}>"
137
+ end
138
+
139
+ private
140
+
141
+ class SQLRecorder
142
+ def initialize
143
+ @statements = []
144
+ end
145
+
146
+ def create_table(name, options = {})
147
+ columns = []
148
+ builder = Object.new
149
+ builder.define_singleton_method(:column) do |column_name, type, **column_options|
150
+ columns << [column_name, type, column_options]
151
+ end
152
+ yield builder if block_given?
153
+ definitions = columns.map { |column_name, type, column_options| column_sql(column_name, type, column_options) }
154
+ statement = +"CREATE TABLE"
155
+ statement << " IF NOT EXISTS" if options[:if_not_exists]
156
+ statement << " #{identifier(name)} (#{definitions.join(', ')})"
157
+ @statements << "#{statement};"
158
+ end
159
+
160
+ def drop_table(name, options = {})
161
+ statement = "DROP TABLE"
162
+ statement << " IF EXISTS" if options[:if_exists]
163
+ statement << " #{identifier(name)}"
164
+ statement << " CASCADE" if options[:cascade]
165
+ @statements << "#{statement};"
166
+ end
167
+
168
+ def add_column(table, name, type, options = {})
169
+ @statements << "ALTER TABLE #{identifier(table)} ADD COLUMN #{column_sql(name, type, options)};"
170
+ end
171
+
172
+ def remove_column(table, name, _options = {})
173
+ @statements << "ALTER TABLE #{identifier(table)} DROP COLUMN #{identifier(name)};"
174
+ end
175
+
176
+ def change_column(table, name, type, _options = {})
177
+ @statements << "ALTER TABLE #{identifier(table)} ALTER COLUMN #{identifier(name)} TYPE #{type_name(type)};"
178
+ end
179
+
180
+ def rename_column(table, old_name, new_name)
181
+ @statements << "ALTER TABLE #{identifier(table)} RENAME COLUMN #{identifier(old_name)} TO #{identifier(new_name)};"
182
+ end
183
+
184
+ def add_index(table, columns, options = {})
185
+ name = options[:name] || "idx_#{table}_#{Array(columns).join('_')}"
186
+ unique = options[:unique] ? " UNIQUE" : ""
187
+ @statements << "CREATE#{unique} INDEX #{identifier(name)} ON #{identifier(table)} (#{Array(columns).map { |column| identifier(column) }.join(', ')});"
188
+ end
189
+
190
+ def remove_index(table, options = {})
191
+ name = options[:name] || "idx_#{table}_#{Array(options[:column] || options[:columns]).join('_')}"
192
+ @statements << "DROP INDEX #{identifier(name)};"
193
+ end
194
+
195
+ def execute(sql)
196
+ @statements << sql.to_s.sub(/;?\z/, ";")
197
+ end
198
+
199
+ def to_sql
200
+ @statements.join("\n")
201
+ end
202
+
203
+ def method_missing(method, *_args, &_block)
204
+ raise ArgumentError, "Cannot serialize migration operation '#{method}' to SQL"
205
+ end
206
+
207
+ def respond_to_missing?(_method, _include_private = false)
208
+ false
209
+ end
210
+
211
+ private
212
+
213
+ def column_sql(name, type, options)
214
+ definition = [identifier(name), type_name(type)]
215
+ definition << "PRIMARY KEY" if options[:primary_key]
216
+ definition << "UNIQUE" if options[:unique] && !options[:primary_key]
217
+ definition << "NOT NULL" if options[:null] == false
218
+ definition << "DEFAULT #{literal(options[:default])}" if options.key?(:default)
219
+ definition.join(" ")
220
+ end
221
+
222
+ def type_name(type)
223
+ type.to_s.upcase
224
+ end
225
+
226
+ def identifier(value)
227
+ "\"#{value.to_s.gsub('"', '""')}\""
228
+ end
229
+
230
+ def literal(value)
231
+ case value
232
+ when nil then "NULL"
233
+ when Numeric then value.to_s
234
+ when TrueClass then "TRUE"
235
+ when FalseClass then "FALSE"
236
+ else "'#{value.to_s.gsub("'", "''")}'"
237
+ end
238
+ end
239
+ end
240
+
241
+ def calculate_checksum
242
+ data = begin
243
+ [@version, @name, to_sql].join("\0")
244
+ rescue ArgumentError
245
+ # Non-serializable Ruby operations still receive a deterministic
246
+ # source-based checksum; SQL serialization remains the preferred
247
+ # migration compatibility path.
248
+ [@version, @name, (@up_operations + @down_operations).map(&:source_location)].to_json
249
+ end
250
+ @checksum = Digest::SHA256.hexdigest(data)[0...16]
251
+ end
252
+ end
253
+ end
254
+ end