rubydb 0.1.3 → 0.1.5

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 (420) hide show
  1. checksums.yaml +4 -4
  2. data/.github/PULL_REQUEST_TEMPLATE.md +15 -15
  3. data/.github/workflows/benchmark.yml +26 -26
  4. data/.github/workflows/compatibility.yml +63 -63
  5. data/.github/workflows/fuzz.yml +33 -33
  6. data/.github/workflows/lint.yml +21 -21
  7. data/.github/workflows/operations.yml +24 -24
  8. data/.github/workflows/production-validation.yml +111 -111
  9. data/.github/workflows/release.yml +77 -77
  10. data/.github/workflows/security.yml +39 -37
  11. data/.github/workflows/test.yml +26 -26
  12. data/.github/workflows/workload.yml +58 -58
  13. data/.gitignore +12 -5
  14. data/.rubocop.yml +50 -44
  15. data/.standard.yml +9 -14
  16. data/ARCHITECTURE.md +21 -21
  17. data/CHANGELOG.md +49 -27
  18. data/CODE_OF_CONDUCT.md +13 -13
  19. data/CONTRIBUTING.md +29 -29
  20. data/GOVERNANCE.md +16 -16
  21. data/Gemfile +18 -17
  22. data/Gemfile.lock +125 -71
  23. data/README.md +249 -162
  24. data/ROADMAP.md +27 -27
  25. data/Rakefile +71 -71
  26. data/SECURITY.md +54 -54
  27. data/SUPPORT.md +14 -14
  28. data/adapters/activerecord/Gemfile +11 -11
  29. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +877 -880
  30. data/adapters/activerecord/rubydb-activerecord.gemspec +21 -21
  31. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +155 -143
  32. data/adapters/ruby/README.md +18 -18
  33. data/adapters/sequel/README.md +11 -11
  34. data/config/monitoring/prometheus-alerts.yml +39 -39
  35. data/config/production.yml +36 -36
  36. data/docs/README.md +75 -71
  37. data/docs/architecture/concurrency.md +14 -14
  38. data/docs/architecture/current-state.md +125 -125
  39. data/docs/architecture/execution-engine.md +25 -25
  40. data/docs/architecture/indexes.md +19 -19
  41. data/docs/architecture/mvcc.md +19 -19
  42. data/docs/architecture/overview.md +13 -13
  43. data/docs/architecture/pages.md +11 -11
  44. data/docs/architecture/production-roadmap.md +82 -82
  45. data/docs/architecture/query-planner.md +20 -20
  46. data/docs/architecture/recovery.md +18 -18
  47. data/docs/architecture/sql-engine.md +12 -12
  48. data/docs/architecture/storage-engine.md +14 -14
  49. data/docs/architecture/transactions.md +10 -10
  50. data/docs/architecture/wal.md +28 -28
  51. data/docs/cli-cheatsheet.md +98 -98
  52. data/docs/cli.md +275 -275
  53. data/docs/contributing/architecture.md +9 -9
  54. data/docs/contributing/benchmarking.md +14 -14
  55. data/docs/contributing/development.md +16 -16
  56. data/docs/contributing/release-process.md +49 -49
  57. data/docs/contributing/testing.md +16 -16
  58. data/docs/debugging.md +229 -229
  59. data/docs/developer/branching.md +10 -10
  60. data/docs/developer/database-diff.md +10 -10
  61. data/docs/developer/local-development.md +17 -17
  62. data/docs/developer/snapshots.md +9 -9
  63. data/docs/developer/temporal-data.md +10 -10
  64. data/docs/developer-guide.md +297 -297
  65. data/docs/getting-started/first-database.md +16 -16
  66. data/docs/getting-started/first-query.md +13 -13
  67. data/docs/getting-started/installation.md +19 -19
  68. data/docs/getting-started/local-to-production.md +300 -300
  69. data/docs/getting-started/quickstart.md +17 -17
  70. data/docs/getting-started/rails.md +16 -16
  71. data/docs/hardening_backlog.md +93 -93
  72. data/docs/lessons-learned.md +112 -112
  73. data/docs/operations/backups.md +33 -33
  74. data/docs/operations/disaster-recovery.md +31 -31
  75. data/docs/operations/failover.md +30 -30
  76. data/docs/operations/monitoring.md +25 -25
  77. data/docs/operations/production-guide.md +295 -295
  78. data/docs/operations/production-runbook.md +45 -45
  79. data/docs/operations/replication.md +33 -33
  80. data/docs/operations/restore.md +6 -6
  81. data/docs/operations/runbook.md +34 -34
  82. data/docs/operations/upgrades.md +14 -14
  83. data/docs/operations/workload-testing.md +17 -17
  84. data/docs/production-readiness.md +118 -118
  85. data/docs/production_validation.md +150 -150
  86. data/docs/rails/active-record.md +11 -11
  87. data/docs/rails/compatibility-guide.md +90 -90
  88. data/docs/rails/database-yml.md +92 -92
  89. data/docs/rails/installation.md +17 -17
  90. data/docs/rails/migrations.md +17 -17
  91. data/docs/rails/production.md +82 -82
  92. data/docs/rails/troubleshooting.md +18 -18
  93. data/docs/release.md +25 -25
  94. data/docs/server/architecture.md +10 -10
  95. data/docs/server/authentication.md +10 -10
  96. data/docs/server/configuration.md +16 -16
  97. data/docs/server/connection-pooling.md +10 -10
  98. data/docs/server/deployment.md +10 -10
  99. data/docs/server/protocol.md +12 -12
  100. data/docs/sql/compatibility-guide.md +82 -82
  101. data/docs/sql/compatibility.md +39 -39
  102. data/docs/sql/data-types.md +10 -10
  103. data/docs/sql/functions.md +9 -9
  104. data/docs/sql/joins.md +9 -9
  105. data/docs/sql/operators.md +9 -9
  106. data/docs/sql/sqlite-compatibility.md +21 -21
  107. data/docs/sql/syntax.md +10 -10
  108. data/docs/sql/transactions.md +10 -10
  109. data/docs/troubleshooting.md +244 -244
  110. data/lessons/01-foundations.md +73 -0
  111. data/lessons/02-local-development.md +121 -0
  112. data/lessons/03-embedded-rubydb.md +99 -0
  113. data/lessons/04-rails-complex-apps.md +138 -0
  114. data/lessons/05-rubydb-production-server.md +237 -0
  115. data/lessons/06-postgresql-massive-apps.md +96 -0
  116. data/lessons/07-hybrid-microservices.md +94 -0
  117. data/lessons/08-migrations-backups-recovery.md +86 -0
  118. data/lessons/09-observability-security-scale.md +87 -0
  119. data/lessons/10-release-readiness.md +125 -0
  120. data/lib/rubydb/backup/archive.rb +332 -334
  121. data/lib/rubydb/backup/backup.rb +400 -401
  122. data/lib/rubydb/backup/incremental.rb +349 -353
  123. data/lib/rubydb/backup/restore.rb +289 -290
  124. data/lib/rubydb/backup/snapshot.rb +265 -267
  125. data/lib/rubydb/backup/verification.rb +276 -279
  126. data/lib/rubydb/branching/branch.rb +181 -181
  127. data/lib/rubydb/branching/branch_manager.rb +307 -311
  128. data/lib/rubydb/branching/branch_metadata.rb +140 -140
  129. data/lib/rubydb/branching/checkout.rb +165 -166
  130. data/lib/rubydb/branching/copy_on_write.rb +272 -272
  131. data/lib/rubydb/branching/diff.rb +137 -138
  132. data/lib/rubydb/branching/merge.rb +282 -285
  133. data/lib/rubydb/build_info.rb +15 -15
  134. data/lib/rubydb/catalog/catalog.rb +391 -391
  135. data/lib/rubydb/catalog/column.rb +112 -112
  136. data/lib/rubydb/catalog/constraint.rb +180 -180
  137. data/lib/rubydb/catalog/database.rb +184 -184
  138. data/lib/rubydb/catalog/index.rb +97 -97
  139. data/lib/rubydb/catalog/schema.rb +103 -103
  140. data/lib/rubydb/catalog/sequence.rb +90 -90
  141. data/lib/rubydb/catalog/system_catalog.rb +698 -698
  142. data/lib/rubydb/catalog/table.rb +178 -178
  143. data/lib/rubydb/catalog/trigger.rb +102 -102
  144. data/lib/rubydb/catalog/view.rb +66 -66
  145. data/lib/rubydb/cli/application.rb +163 -163
  146. data/lib/rubydb/cli/commands/backup.rb +80 -81
  147. data/lib/rubydb/cli/commands/branch.rb +72 -72
  148. data/lib/rubydb/cli/commands/checkout.rb +54 -54
  149. data/lib/rubydb/cli/commands/create.rb +58 -58
  150. data/lib/rubydb/cli/commands/diff.rb +76 -77
  151. data/lib/rubydb/cli/commands/doctor.rb +74 -74
  152. data/lib/rubydb/cli/commands/drop.rb +57 -57
  153. data/lib/rubydb/cli/commands/init.rb +101 -102
  154. data/lib/rubydb/cli/commands/inspect.rb +95 -95
  155. data/lib/rubydb/cli/commands/merge.rb +63 -63
  156. data/lib/rubydb/cli/commands/migrate.rb +62 -62
  157. data/lib/rubydb/cli/commands/restart.rb +42 -39
  158. data/lib/rubydb/cli/commands/restore.rb +121 -121
  159. data/lib/rubydb/cli/commands/shell.rb +365 -365
  160. data/lib/rubydb/cli/commands/snapshot.rb +79 -79
  161. data/lib/rubydb/cli/commands/start.rb +88 -82
  162. data/lib/rubydb/cli/commands/status.rb +96 -92
  163. data/lib/rubydb/cli/commands/stop.rb +47 -47
  164. data/lib/rubydb/cli/commands/vacuum.rb +58 -58
  165. data/lib/rubydb/cli/formatter.rb +221 -221
  166. data/lib/rubydb/cli/output.rb +168 -168
  167. data/lib/rubydb/client/client.rb +302 -304
  168. data/lib/rubydb/client/connection.rb +414 -415
  169. data/lib/rubydb/client/connection_pool.rb +168 -168
  170. data/lib/rubydb/client/connection_url.rb +96 -96
  171. data/lib/rubydb/client/prepared_statement.rb +60 -60
  172. data/lib/rubydb/client/result.rb +123 -123
  173. data/lib/rubydb/client/statement.rb +52 -52
  174. data/lib/rubydb/client/transaction.rb +130 -130
  175. data/lib/rubydb/concurrency/concurrency.rb +19 -19
  176. data/lib/rubydb/concurrency/deadlock_detector.rb +148 -150
  177. data/lib/rubydb/concurrency/latch.rb +101 -101
  178. data/lib/rubydb/concurrency/lock_graph.rb +163 -165
  179. data/lib/rubydb/concurrency/mutex.rb +181 -183
  180. data/lib/rubydb/concurrency/rw_lock.rb +180 -180
  181. data/lib/rubydb/concurrency/scheduler.rb +248 -250
  182. data/lib/rubydb/concurrency/worker_pool.rb +145 -143
  183. data/lib/rubydb/configuration/config.rb +170 -170
  184. data/lib/rubydb/configuration/defaults.rb +179 -179
  185. data/lib/rubydb/configuration/environment.rb +152 -152
  186. data/lib/rubydb/configuration/parser.rb +185 -185
  187. data/lib/rubydb/configuration/validation.rb +221 -221
  188. data/lib/rubydb/constants.rb +74 -74
  189. data/lib/rubydb/constraints/check.rb +181 -181
  190. data/lib/rubydb/constraints/constraint.rb +101 -101
  191. data/lib/rubydb/constraints/foreign_key.rb +130 -130
  192. data/lib/rubydb/constraints/not_null.rb +64 -64
  193. data/lib/rubydb/constraints/primary_key.rb +99 -99
  194. data/lib/rubydb/constraints/unique.rb +106 -108
  195. data/lib/rubydb/constraints/validator.rb +349 -350
  196. data/lib/rubydb/errors/authentication_error.rb +10 -10
  197. data/lib/rubydb/errors/authorization_error.rb +23 -23
  198. data/lib/rubydb/errors/client_error.rb +10 -10
  199. data/lib/rubydb/errors/configuration_error.rb +10 -10
  200. data/lib/rubydb/errors/connection_error.rb +10 -10
  201. data/lib/rubydb/errors/constraint_error.rb +23 -23
  202. data/lib/rubydb/errors/corruption_error.rb +10 -10
  203. data/lib/rubydb/errors/database_error.rb +10 -10
  204. data/lib/rubydb/errors/error.rb +20 -20
  205. data/lib/rubydb/errors/execution_error.rb +10 -10
  206. data/lib/rubydb/errors/parser_error.rb +10 -10
  207. data/lib/rubydb/errors/recovery_error.rb +10 -10
  208. data/lib/rubydb/errors/replication_error.rb +10 -10
  209. data/lib/rubydb/errors/server_error.rb +6 -6
  210. data/lib/rubydb/errors/storage_error.rb +10 -10
  211. data/lib/rubydb/errors/transaction_error.rb +10 -10
  212. data/lib/rubydb/execution/aggregate_executor.rb +134 -138
  213. data/lib/rubydb/execution/delete_executor.rb +110 -112
  214. data/lib/rubydb/execution/distinct_executor.rb +131 -135
  215. data/lib/rubydb/execution/executor.rb +1195 -1192
  216. data/lib/rubydb/execution/expression.rb +191 -193
  217. data/lib/rubydb/execution/index_scan.rb +142 -142
  218. data/lib/rubydb/execution/insert_executor.rb +215 -217
  219. data/lib/rubydb/execution/join_executor.rb +243 -249
  220. data/lib/rubydb/execution/limit_executor.rb +83 -85
  221. data/lib/rubydb/execution/optimizer.rb +227 -215
  222. data/lib/rubydb/execution/plan.rb +355 -353
  223. data/lib/rubydb/execution/planner.rb +542 -536
  224. data/lib/rubydb/execution/predicate.rb +235 -235
  225. data/lib/rubydb/execution/scan.rb +49 -49
  226. data/lib/rubydb/execution/sequential_scan.rb +63 -63
  227. data/lib/rubydb/execution/sort_executor.rb +180 -185
  228. data/lib/rubydb/execution/update_executor.rb +160 -162
  229. data/lib/rubydb/functions/aggregate.rb +70 -70
  230. data/lib/rubydb/functions/date_functions.rb +274 -278
  231. data/lib/rubydb/functions/function.rb +85 -85
  232. data/lib/rubydb/functions/json_functions.rb +231 -215
  233. data/lib/rubydb/functions/numeric_functions.rb +346 -346
  234. data/lib/rubydb/functions/scalar.rb +52 -52
  235. data/lib/rubydb/functions/string_functions.rb +383 -383
  236. data/lib/rubydb/functions/system_functions.rb +258 -246
  237. data/lib/rubydb/history/as_of.rb +238 -238
  238. data/lib/rubydb/history/change.rb +105 -105
  239. data/lib/rubydb/history/history.rb +131 -131
  240. data/lib/rubydb/history/history_manager.rb +228 -229
  241. data/lib/rubydb/history/temporal_query.rb +202 -202
  242. data/lib/rubydb/history/timeline.rb +144 -144
  243. data/lib/rubydb/indexes/btree.rb +186 -186
  244. data/lib/rubydb/indexes/btree_cursor.rb +258 -258
  245. data/lib/rubydb/indexes/btree_node.rb +384 -385
  246. data/lib/rubydb/indexes/hash_index.rb +150 -150
  247. data/lib/rubydb/indexes/index.rb +71 -71
  248. data/lib/rubydb/indexes/index_manager.rb +408 -406
  249. data/lib/rubydb/indexes/index_scan.rb +466 -470
  250. data/lib/rubydb/migrations/migration.rb +253 -254
  251. data/lib/rubydb/migrations/migration_lock.rb +146 -146
  252. data/lib/rubydb/migrations/migration_manager.rb +187 -176
  253. data/lib/rubydb/migrations/migration_version.rb +71 -71
  254. data/lib/rubydb/migrations/schema_diff.rb +211 -211
  255. data/lib/rubydb/migrations/schema_version.rb +64 -64
  256. data/lib/rubydb/monitoring/events.rb +155 -160
  257. data/lib/rubydb/monitoring/health.rb +216 -222
  258. data/lib/rubydb/monitoring/logger.rb +188 -193
  259. data/lib/rubydb/monitoring/metrics.rb +363 -359
  260. data/lib/rubydb/monitoring/performance.rb +176 -176
  261. data/lib/rubydb/monitoring/statistics.rb +168 -170
  262. data/lib/rubydb/mvcc/garbage_collector.rb +199 -199
  263. data/lib/rubydb/mvcc/mvcc.rb +16 -16
  264. data/lib/rubydb/mvcc/snapshot.rb +146 -147
  265. data/lib/rubydb/mvcc/vacuum.rb +180 -180
  266. data/lib/rubydb/mvcc/version.rb +106 -106
  267. data/lib/rubydb/mvcc/version_store.rb +396 -398
  268. data/lib/rubydb/mvcc/visibility.rb +107 -109
  269. data/lib/rubydb/protocol/capabilities.rb +125 -125
  270. data/lib/rubydb/protocol/decoder.rb +142 -145
  271. data/lib/rubydb/protocol/encoder.rb +131 -136
  272. data/lib/rubydb/protocol/handshake.rb +306 -305
  273. data/lib/rubydb/protocol/message.rb +121 -121
  274. data/lib/rubydb/protocol/parameter_binder.rb +101 -0
  275. data/lib/rubydb/protocol/protocol.rb +276 -277
  276. data/lib/rubydb/protocol/version.rb +54 -54
  277. data/lib/rubydb/rails/adapter.rb +245 -239
  278. data/lib/rubydb/rails/connection.rb +312 -314
  279. data/lib/rubydb/rails/database_statements.rb +122 -122
  280. data/lib/rubydb/rails/migration.rb +131 -131
  281. data/lib/rubydb/rails/quoting.rb +109 -109
  282. data/lib/rubydb/rails/result.rb +123 -120
  283. data/lib/rubydb/rails/schema_statements.rb +339 -339
  284. data/lib/rubydb/rails/transaction.rb +105 -105
  285. data/lib/rubydb/rails/type.rb +126 -126
  286. data/lib/rubydb/recovery/checkpoint.rb +261 -257
  287. data/lib/rubydb/recovery/consistency.rb +457 -467
  288. data/lib/rubydb/recovery/corruption_detector.rb +5 -5
  289. data/lib/rubydb/recovery/crash_recovery.rb +381 -387
  290. data/lib/rubydb/recovery/recovery_manager.rb +204 -206
  291. data/lib/rubydb/recovery/redo.rb +235 -237
  292. data/lib/rubydb/recovery/undo.rb +204 -206
  293. data/lib/rubydb/replication/failover.rb +5 -5
  294. data/lib/rubydb/replication/fencing.rb +63 -63
  295. data/lib/rubydb/replication/primary.rb +461 -450
  296. data/lib/rubydb/replication/replica.rb +382 -384
  297. data/lib/rubydb/replication/replication_log.rb +194 -200
  298. data/lib/rubydb/replication/replication_manager.rb +307 -308
  299. data/lib/rubydb/replication/replication_slot.rb +293 -295
  300. data/lib/rubydb/replication/replication_stream.rb +198 -201
  301. data/lib/rubydb/rubydb.rb +564 -560
  302. data/lib/rubydb/security/access_control.rb +252 -254
  303. data/lib/rubydb/security/audit_log.rb +209 -213
  304. data/lib/rubydb/security/authentication.rb +302 -302
  305. data/lib/rubydb/security/authorization.rb +282 -282
  306. data/lib/rubydb/security/credentials.rb +192 -196
  307. data/lib/rubydb/security/password.rb +205 -215
  308. data/lib/rubydb/security/permissions.rb +74 -74
  309. data/lib/rubydb/security/role.rb +99 -101
  310. data/lib/rubydb/security/user.rb +86 -86
  311. data/lib/rubydb/server/connection.rb +383 -366
  312. data/lib/rubydb/server/connection_pool.rb +193 -193
  313. data/lib/rubydb/server/lifecycle.rb +227 -228
  314. data/lib/rubydb/server/listener.rb +139 -136
  315. data/lib/rubydb/server/request_handler.rb +277 -276
  316. data/lib/rubydb/server/server.rb +363 -364
  317. data/lib/rubydb/server/session.rb +371 -369
  318. data/lib/rubydb/server/worker.rb +206 -210
  319. data/lib/rubydb/server/worker_pool.rb +168 -168
  320. data/lib/rubydb/sql/ast/alter_table.rb +169 -169
  321. data/lib/rubydb/sql/ast/begin_transaction.rb +47 -47
  322. data/lib/rubydb/sql/ast/commit.rb +37 -37
  323. data/lib/rubydb/sql/ast/constraint.rb +92 -83
  324. data/lib/rubydb/sql/ast/create_database.rb +41 -41
  325. data/lib/rubydb/sql/ast/create_index.rb +61 -61
  326. data/lib/rubydb/sql/ast/create_schema.rb +52 -52
  327. data/lib/rubydb/sql/ast/create_table.rb +187 -187
  328. data/lib/rubydb/sql/ast/delete.rb +54 -54
  329. data/lib/rubydb/sql/ast/drop_database.rb +41 -41
  330. data/lib/rubydb/sql/ast/drop_index.rb +41 -41
  331. data/lib/rubydb/sql/ast/drop_schema.rb +49 -49
  332. data/lib/rubydb/sql/ast/drop_table.rb +49 -49
  333. data/lib/rubydb/sql/ast/explain.rb +64 -64
  334. data/lib/rubydb/sql/ast/expression.rb +617 -604
  335. data/lib/rubydb/sql/ast/insert.rb +66 -66
  336. data/lib/rubydb/sql/ast/node.rb +42 -42
  337. data/lib/rubydb/sql/ast/rollback.rb +63 -63
  338. data/lib/rubydb/sql/ast/savepoint.rb +59 -59
  339. data/lib/rubydb/sql/ast/select.rb +88 -88
  340. data/lib/rubydb/sql/ast/set_operation.rb +22 -20
  341. data/lib/rubydb/sql/ast/trigger.rb +35 -29
  342. data/lib/rubydb/sql/ast/update.rb +88 -88
  343. data/lib/rubydb/sql/ast/vacuum.rb +19 -19
  344. data/lib/rubydb/sql/ast/view.rb +38 -32
  345. data/lib/rubydb/sql/ast/with.rb +32 -32
  346. data/lib/rubydb/sql/grammar.rb +86 -86
  347. data/lib/rubydb/sql/keywords.rb +156 -156
  348. data/lib/rubydb/sql/lexer.rb +209 -214
  349. data/lib/rubydb/sql/operators.rb +100 -100
  350. data/lib/rubydb/sql/parser.rb +1167 -1170
  351. data/lib/rubydb/sql/planner/analyzer.rb +283 -302
  352. data/lib/rubydb/sql/planner/binder.rb +537 -550
  353. data/lib/rubydb/sql/planner/type_checker.rb +427 -431
  354. data/lib/rubydb/sql/token.rb +210 -210
  355. data/lib/rubydb/storage/buffer_frame.rb +44 -44
  356. data/lib/rubydb/storage/buffer_pool.rb +155 -155
  357. data/lib/rubydb/storage/database_lock.rb +74 -74
  358. data/lib/rubydb/storage/deserializer.rb +332 -342
  359. data/lib/rubydb/storage/engine.rb +2347 -2330
  360. data/lib/rubydb/storage/file_manager.rb +191 -187
  361. data/lib/rubydb/storage/free_space_map.rb +79 -81
  362. data/lib/rubydb/storage/page.rb +92 -94
  363. data/lib/rubydb/storage/page_allocator.rb +852 -855
  364. data/lib/rubydb/storage/page_header.rb +63 -67
  365. data/lib/rubydb/storage/page_manager.rb +127 -131
  366. data/lib/rubydb/storage/record.rb +58 -58
  367. data/lib/rubydb/storage/row.rb +78 -78
  368. data/lib/rubydb/storage/serializer.rb +51 -51
  369. data/lib/rubydb/storage/storage_layout.rb +151 -151
  370. data/lib/rubydb/storage/storage_manager.rb +114 -114
  371. data/lib/rubydb/storage/tuple.rb +458 -461
  372. data/lib/rubydb/storage/visibility_map.rb +964 -973
  373. data/lib/rubydb/transactions/commit_manager.rb +219 -220
  374. data/lib/rubydb/transactions/isolation.rb +98 -98
  375. data/lib/rubydb/transactions/lock.rb +76 -76
  376. data/lib/rubydb/transactions/lock_manager.rb +359 -362
  377. data/lib/rubydb/transactions/savepoint.rb +142 -143
  378. data/lib/rubydb/transactions/transaction.rb +214 -215
  379. data/lib/rubydb/transactions/transaction_id.rb +84 -84
  380. data/lib/rubydb/transactions/transaction_log.rb +256 -257
  381. data/lib/rubydb/transactions/transaction_manager.rb +434 -435
  382. data/lib/rubydb/types/bigint.rb +36 -36
  383. data/lib/rubydb/types/blob.rb +37 -37
  384. data/lib/rubydb/types/boolean.rb +34 -34
  385. data/lib/rubydb/types/date.rb +39 -39
  386. data/lib/rubydb/types/decimal.rb +48 -48
  387. data/lib/rubydb/types/float.rb +34 -34
  388. data/lib/rubydb/types/integer.rb +36 -36
  389. data/lib/rubydb/types/json.rb +41 -41
  390. data/lib/rubydb/types/null.rb +34 -34
  391. data/lib/rubydb/types/smallint.rb +36 -36
  392. data/lib/rubydb/types/text.rb +37 -37
  393. data/lib/rubydb/types/time.rb +46 -46
  394. data/lib/rubydb/types/timestamp.rb +39 -39
  395. data/lib/rubydb/types/type.rb +119 -119
  396. data/lib/rubydb/types/uuid.rb +47 -47
  397. data/lib/rubydb/types/varchar.rb +37 -37
  398. data/lib/rubydb/version.rb +32 -32
  399. data/lib/rubydb/wal/archive.rb +190 -193
  400. data/lib/rubydb/wal/checkpoint.rb +181 -183
  401. data/lib/rubydb/wal/lsn.rb +94 -94
  402. data/lib/rubydb/wal/reader.rb +259 -260
  403. data/lib/rubydb/wal/record.rb +105 -105
  404. data/lib/rubydb/wal/segment.rb +193 -193
  405. data/lib/rubydb/wal/wal.rb +480 -452
  406. data/lib/rubydb/wal/writer.rb +236 -236
  407. data/lib/rubydb.rb +7 -7
  408. data/packaging/docker/docker-compose.failover.yml +43 -43
  409. data/packaging/homebrew/rubydb.rb +19 -19
  410. data/rubydb.gemspec +60 -57
  411. data/scripts/benchmark +7 -7
  412. data/scripts/durability_drill +37 -37
  413. data/scripts/fuzz +63 -63
  414. data/scripts/release +45 -40
  415. data/scripts/release_check +43 -43
  416. data/scripts/replication_failover_drill +268 -250
  417. data/scripts/replication_network_failover_drill +287 -255
  418. data/scripts/restore_drill +45 -45
  419. data/scripts/security +45 -0
  420. metadata +54 -1
@@ -1,761 +1,762 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
- require "active_record/connection_adapters/abstract_adapter"
5
- require "active_record/connection_adapters/abstract/schema_definitions"
6
- require "active_record/connection_adapters/abstract/schema_statements"
7
-
8
- require "rubydb"
9
- require "rubydb/rails/adapter"
10
- require "rubydb/rails/connection"
11
- require "rubydb/rails/database_statements"
12
- require "rubydb/rails/schema_statements"
13
- require "rubydb/rails/quoting"
14
- require "rubydb/rails/type"
15
- require "rubydb/rails/result"
16
-
17
- module ActiveRecord
18
- module ConnectionAdapters
19
- # RubyDB adapter for ActiveRecord
20
- class RubyDBAdapter < AbstractAdapter
21
- include RubyDB::Rails::DatabaseStatements
22
- include RubyDB::Rails::SchemaStatements
23
- include RubyDB::Rails::Quoting
24
-
25
- ADAPTER_NAME = "RubyDB"
26
-
27
- # ActiveRecord 7.2 uses adapter-level methods while compiling hash-form
28
- # order clauses (for example, `order(created_at: :desc)`). Keep these
29
- # independent of a live connection so relation construction is safe
30
- # during schema-cache and query setup as well.
31
- def self.quote_table_name(name)
32
- quote_column_name(name)
33
- end
34
-
35
- def self.quote_column_name(name)
36
- "\"#{name.to_s.gsub('"', '""')}\""
37
- end
38
-
39
- NATIVE_DATABASE_TYPES = {
40
- primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
41
- string: { name: "VARCHAR", limit: 255 },
42
- text: { name: "TEXT" },
43
- integer: { name: "INTEGER" },
44
- bigint: { name: "BIGINT" },
45
- smallint: { name: "SMALLINT" },
46
- float: { name: "FLOAT" },
47
- decimal: { name: "DECIMAL", precision: 10, scale: 2 },
48
- datetime: { name: "TIMESTAMP" },
49
- timestamp: { name: "TIMESTAMP" },
50
- time: { name: "TIME" },
51
- date: { name: "DATE" },
52
- binary: { name: "BLOB" },
53
- boolean: { name: "BOOLEAN" },
54
- json: { name: "JSON" },
55
- uuid: { name: "UUID" }
56
- }
57
-
58
- # ActiveRecord 7.2 constructs adapters with a single configuration hash.
59
- # Accept trailing deprecated arguments so applications upgrading from older
60
- # ActiveRecord versions do not fail during connection establishment.
61
- def initialize(config, *)
62
- super(config)
63
-
64
- @connection = RubyDB::Rails::Connection.new(config)
65
- @connection.connect
66
-
67
- @prepared_statements = {}
68
- @transaction_depth = 0
69
- @query_cache_enabled = false
70
- @query_cache = {}
71
- @statements = {}
72
- @statement_counter = 0
73
- # AbstractAdapter uses this monitor while creating transactions. It
74
- # must be re-entrant because ActiveRecord acquires it recursively.
75
- @lock = Monitor.new
76
- end
77
-
78
- def adapter_name
79
- ADAPTER_NAME
80
- end
81
-
82
- def supports_migrations?
83
- true
84
- end
85
-
86
- def supports_primary_key?
87
- true
88
- end
89
-
90
- def supports_index_sort_order?
91
- true
92
- end
93
-
94
- def supports_transactions?
95
- true
96
- end
97
-
98
- def supports_savepoints?
99
- true
100
- end
101
-
102
- def supports_foreign_keys?
103
- true
104
- end
105
-
106
- def supports_views?
107
- true
108
- end
109
-
110
- def supports_json?
111
- true
112
- end
113
-
114
- def supports_uuid?
115
- true
116
- end
117
-
118
- def supports_bulk_alter?
119
- false
120
- end
121
-
122
- def native_database_types
123
- NATIVE_DATABASE_TYPES
124
- end
125
-
126
- # ==================== SCHEMA METHODS ====================
127
-
128
- def primary_key(table_name)
129
- return @connection.engine.table_columns(table_name).find(&:primary_key?)&.name&.to_s || "id" if embedded?
130
-
131
- result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
132
- row = result.find { |r| r["pk"] == 1 }
133
- row ? row["name"] : "id"
134
- end
135
-
136
- def tables
137
- return @connection.engine.list_tables.map(&:to_s) if embedded?
138
-
139
- result = execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
140
- result.map { |row| row["name"] }
141
- end
142
-
143
- def table_exists?(table_name)
144
- tables.include?(table_name.to_s)
145
- end
146
-
147
- # ActiveRecord's schema cache asks for these before a model is first
148
- # instantiated. The embedded engine has catalog metadata already, so
149
- # avoid generating unsupported SQLite catalog queries.
150
- def data_sources
151
- return tables if embedded?
152
-
153
- super
154
- end
155
-
156
- def data_source_exists?(name)
157
- return table_exists?(name) if embedded?
158
-
159
- super
160
- end
161
-
162
- def views
163
- return [] if embedded?
164
-
165
- super
166
- end
167
-
168
- def indexes(table_name)
169
- if embedded?
170
- return @connection.engine.index_manager.get_indexes_for_table(table_name.to_s).map do |index|
171
- ActiveRecord::ConnectionAdapters::IndexDefinition.new(
172
- table_name.to_s,
173
- index.name.to_s,
174
- index.unique,
175
- index.columns.map(&:to_s)
176
- )
177
- end
178
- end
179
-
180
- result = execute("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name=?", [table_name])
181
- result.map do |row|
182
- {
183
- name: row["name"],
184
- columns: parse_index_columns(row["sql"]),
185
- unique: row["sql"].include?("UNIQUE")
186
- }
187
- end
188
- end
189
-
190
- def columns(table_name)
191
- return embedded_columns(table_name) if embedded?
192
-
193
- result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
194
- result.map do |row|
195
- ActiveRecord::ConnectionAdapters::Column.new(
196
- row["name"],
197
- row["default"],
198
- RubyDB::Rails::Type.to_rails(row["type"]),
199
- {
200
- null: row["notnull"] == 0,
201
- primary_key: row["pk"] == 1,
202
- limit: extract_limit(row["type"])
203
- }
204
- )
205
- end
206
- end
207
-
208
- def column_exists?(table_name, column_name)
209
- columns(table_name).any? { |c| c.name == column_name }
210
- end
211
-
212
- # ==================== QUERY METHODS ====================
213
-
214
- def execute(sql, name = nil)
215
- sql = sql_for_execution(sql)
216
- log(sql, name) do
217
- @connection.execute(sql)
218
- end
219
- end
220
-
221
- def exec_query(sql, name = nil, binds = [])
222
- sql = sql_for_execution(sql)
223
- log(sql, name) do
224
- params = bind_values(binds)
225
- active_record_result(@connection.execute(sql, params))
226
- end
227
- end
228
-
229
- def exec_delete(sql, name = nil, binds = [])
230
- sql = sql_for_execution(sql)
231
- log(sql, name) do
232
- params = bind_values(binds)
233
- result = @connection.execute(sql, params)
234
- result.affected_rows
235
- end
236
- end
237
-
238
- def exec_update(sql, name = nil, binds = [])
239
- sql = sql_for_execution(sql)
240
- log(sql, name) do
241
- params = bind_values(binds)
242
- result = @connection.execute(sql, params)
243
- result.affected_rows
244
- end
245
- end
246
-
247
- def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
248
- sql = sql_for_execution(sql)
249
- log(sql, name) do
250
- params = bind_values(binds)
251
- result = @connection.execute(sql, params)
252
- id = result.row_id
253
- ActiveRecord::Result.new([pk || "id"], id.nil? ? [] : [[id]])
254
- end
255
- end
256
-
257
- def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
258
- sql, binds = to_sql_and_binds(arel, binds)
259
- result = exec_insert(sql, name, binds, pk, sequence_name, returning: returning)
260
- return returning_column_values(result) unless returning.nil?
261
-
262
- id_value || last_inserted_id(result)
263
- end
264
-
265
- def update(arel, name = nil, binds = [])
266
- sql, binds = to_sql_and_binds(arel, binds)
267
- exec_update(sql, name, binds)
268
- end
269
-
270
- def delete(arel, name = nil, binds = [])
271
- sql, binds = to_sql_and_binds(arel, binds)
272
- exec_delete(sql, name, binds)
273
- end
274
-
275
- def select_all(sql, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
276
- sql, binds = to_sql_and_binds(sql, binds)
277
- exec_query(sql, name, binds)
278
- end
279
-
280
- def select_one(sql, name = nil, binds = [])
281
- sql, binds = to_sql_and_binds(sql, binds)
282
- result = exec_query(sql, name, binds)
283
- result.first
284
- end
285
-
286
- def select_value(sql, name = nil, binds = [])
287
- sql, binds = to_sql_and_binds(sql, binds)
288
- result = exec_query(sql, name, binds)
289
- result.first&.values&.first
290
- end
291
-
292
- def select_values(sql, name = nil, binds = [])
293
- sql, binds = to_sql_and_binds(sql, binds)
294
- result = exec_query(sql, name, binds)
295
- result.map { |row| row.values.first }
296
- end
297
-
298
- def select_rows(sql, name = nil, binds = [])
299
- sql, binds = to_sql_and_binds(sql, binds)
300
- result = exec_query(sql, name, binds)
301
- result.map { |row| row.values }
302
- end
303
-
304
- # ==================== TRANSACTION METHODS ====================
305
-
306
- def begin_db_transaction
307
- @transaction_depth += 1
308
- @connection.begin_db_transaction if @transaction_depth == 1
309
- end
310
-
311
- def commit_db_transaction
312
- return if @transaction_depth <= 0
313
-
314
- @transaction_depth -= 1
315
- @connection.commit_db_transaction if @transaction_depth == 0
316
- end
317
-
318
- def rollback_db_transaction
319
- return if @transaction_depth <= 0
320
-
321
- @transaction_depth -= 1
322
- @connection.rollback_db_transaction if @transaction_depth == 0
323
- @transaction_depth = 0 if @transaction_depth < 0
324
- end
325
-
326
- def create_savepoint(name)
327
- execute("SAVEPOINT #{name}")
328
- end
329
-
330
- def rollback_to_savepoint(name)
331
- execute("ROLLBACK TO SAVEPOINT #{name}")
332
- end
333
-
334
- def release_savepoint(name)
335
- execute("RELEASE SAVEPOINT #{name}")
336
- end
337
-
338
- def in_transaction?
339
- @transaction_depth > 0
340
- end
341
-
342
- def transaction_joinable?
343
- true
344
- end
345
-
346
- def transactional?
347
- true
348
- end
349
-
350
- # ==================== SCHEMA STATEMENT METHODS ====================
351
-
352
- def create_table(table_name, **options, &block)
353
- RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &block)
354
- end
355
-
356
- def drop_table(table_name, **options)
357
- sql = +"DROP TABLE"
358
- sql << " IF EXISTS" if options[:if_exists]
359
- sql << " #{quote_table_name(table_name)}"
360
- sql << " CASCADE" if options[:cascade]
361
- execute(sql)
362
- end
363
-
364
- def add_column(table_name, column_name, type, **options)
365
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
366
- sql << " ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options)}"
367
-
368
- if options[:null] == false
369
- sql << " NOT NULL"
370
- end
371
-
372
- if options[:default]
373
- sql << " DEFAULT #{quote_default(options[:default])}"
374
- end
375
-
376
- if options[:primary_key]
377
- sql << " PRIMARY KEY"
378
- end
379
-
380
- execute(sql)
381
- end
382
-
383
- def remove_column(table_name, column_name, type = nil, **options)
384
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
385
- sql << " DROP COLUMN #{quote_column_name(column_name)}"
386
- sql << " CASCADE" if options[:cascade]
387
- execute(sql)
388
- end
389
-
390
- def change_column(table_name, column_name, type, **options)
391
- # Change column type
392
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
393
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
394
- sql << " TYPE #{type_to_sql(type, options)}"
395
- execute(sql)
396
-
397
- # Change nullability
398
- if options.key?(:null)
399
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
400
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
401
- sql << (options[:null] ? " DROP" : " SET") + " NOT NULL"
402
- execute(sql)
403
- end
404
-
405
- # Change default
406
- if options.key?(:default)
407
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
408
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
409
- if options[:default].nil?
410
- sql << " DROP DEFAULT"
411
- else
412
- sql << " SET DEFAULT #{quote_default(options[:default])}"
413
- end
414
- execute(sql)
415
- end
416
- end
417
-
418
- def rename_column(table_name, column_name, new_column_name)
419
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
420
- sql << " RENAME COLUMN #{quote_column_name(column_name)}"
421
- sql << " TO #{quote_column_name(new_column_name)}"
422
- execute(sql)
423
- end
424
-
425
- def rename_table(old_name, new_name)
426
- sql = "ALTER TABLE #{quote_table_name(old_name)}"
427
- sql << " RENAME TO #{quote_table_name(new_name)}"
428
- execute(sql)
429
- end
430
-
431
- def add_index(table_name, column_name, **options)
432
- index_name = options[:name] || "idx_#{table_name}_#{Array(column_name).join('_')}"
433
- sql = +"CREATE"
434
- sql << " UNIQUE" if options[:unique]
435
- sql << " INDEX #{quote_column_name(index_name)}"
436
- sql << " ON #{quote_table_name(table_name)}"
437
- sql << " (#{Array(column_name).map { |c| quote_column_name(c) }.join(', ')})"
438
- sql << " WHERE #{options[:where]}" if options[:where]
439
- execute(sql)
440
- end
441
-
442
- def remove_index(table_name, column_name = nil, **options)
443
- index_name = options[:name]
444
- if index_name.nil?
445
- column_name ||= options[:column] || options[:columns]
446
- index_name = "idx_#{table_name}_#{Array(column_name).join('_')}"
447
- end
448
-
449
- sql = "DROP INDEX #{quote_column_name(index_name)}"
450
- execute(sql)
451
- end
452
-
453
- def add_foreign_key(from_table, to_table, **options)
454
- fk_name = options[:name] || "fk_#{from_table}_to_#{to_table}"
455
- sql = "ALTER TABLE #{quote_table_name(from_table)}"
456
- sql << " ADD CONSTRAINT #{quote_column_name(fk_name)}"
457
- sql << " FOREIGN KEY (#{quote_column_name(options[:column] || :id)})"
458
- sql << " REFERENCES #{quote_table_name(to_table)}"
459
- sql << " (#{quote_column_name(options[:primary_key] || :id)})"
460
- sql << " ON DELETE #{options[:on_delete]}" if options[:on_delete]
461
- sql << " ON UPDATE #{options[:on_update]}" if options[:on_update]
462
- execute(sql)
463
- end
464
-
465
- def foreign_keys(table_name)
466
- return super unless embedded?
467
-
468
- constraints = @connection.engine.table_metadata[table_name.to_s]&.fetch(:constraints, []) || []
469
- constraints.filter_map do |constraint|
470
- type = constraint[:type] || constraint["type"]
471
- next unless type.to_s.upcase == "FOREIGN_KEY"
472
-
473
- columns = constraint[:columns] || constraint["columns"] || []
474
- reference_table = constraint[:reference_table] || constraint["reference_table"]
475
- reference_columns = constraint[:reference_columns] || constraint["reference_columns"] || ["id"]
476
- options = {
477
- column: Array(columns).first.to_s,
478
- primary_key: Array(reference_columns).first.to_s,
479
- name: constraint[:name] || constraint["name"]
480
- }
481
- options[:on_delete] = (constraint[:on_delete] || constraint["on_delete"]).to_s if constraint[:on_delete] || constraint["on_delete"]
482
- options[:on_update] = (constraint[:on_update] || constraint["on_update"]).to_s if constraint[:on_update] || constraint["on_update"]
483
- ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name.to_s, reference_table.to_s, options)
484
- end
485
- end
486
-
487
- def remove_foreign_key(from_table, **options)
488
- fk_name = options[:name] || "fk_#{from_table}_to_#{options[:to_table]}"
489
- sql = "ALTER TABLE #{quote_table_name(from_table)}"
490
- sql << " DROP CONSTRAINT #{quote_column_name(fk_name)}"
491
- execute(sql)
492
- end
493
-
494
- def add_timestamps(table_name, **options)
495
- add_column(table_name, :created_at, :datetime, options)
496
- add_column(table_name, :updated_at, :datetime, options)
497
- end
498
-
499
- def remove_timestamps(table_name, **options)
500
- remove_column(table_name, :updated_at, options)
501
- remove_column(table_name, :created_at, options)
502
- end
503
-
504
- def change_column_null(table_name, column_name, null, default = nil)
505
- if default
506
- sql = "UPDATE #{quote_table_name(table_name)}"
507
- sql << " SET #{quote_column_name(column_name)} = #{quote(default)}"
508
- sql << " WHERE #{quote_column_name(column_name)} IS NULL"
509
- execute(sql)
510
- end
511
-
512
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
513
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
514
- sql << (null ? " DROP" : " SET") + " NOT NULL"
515
- execute(sql)
516
- end
517
-
518
- def change_column_default(table_name, column_name, default)
519
- sql = "ALTER TABLE #{quote_table_name(table_name)}"
520
- sql << " ALTER COLUMN #{quote_column_name(column_name)}"
521
- if default.nil?
522
- sql << " DROP DEFAULT"
523
- else
524
- sql << " SET DEFAULT #{quote_default(default)}"
525
- end
526
- execute(sql)
527
- end
528
-
529
- # ==================== QUOTING METHODS ====================
530
-
531
- def quote(value, column = nil)
532
- @connection.quote(value, column)
533
- end
534
-
535
- def quote_table_name(name)
536
- @connection.quote_table_name(name)
537
- end
538
-
539
- def quote_column_name(name)
540
- @connection.quote_column_name(name)
541
- end
542
-
543
- def quote_default(value)
544
- quote(value)
545
- end
546
-
547
- # ==================== TYPE CASTING ====================
548
-
549
- def type_cast(value, type)
550
- RubyDB::Rails::Type.serialize(value, type)
551
- end
552
-
553
- def type_cast_from_database(value, type)
554
- RubyDB::Rails::Type.deserialize(value, type)
555
- end
556
-
557
- # ==================== SCHEMA VERSION ====================
558
-
559
- def schema_version
560
- result = execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
561
- result.first ? result.first["version"] : nil
562
- end
563
-
564
- def schema_migrations
565
- result = execute("SELECT version FROM schema_migrations ORDER BY version")
566
- result.map { |row| row["version"] }
567
- end
568
-
569
- def dump_schema
570
- schema = +""
571
- tables.each do |table|
572
- table_columns = columns(table)
573
- primary_key_name = if embedded?
574
- @connection.engine.table_columns(table).find(&:primary_key?)&.name
575
- else
576
- primary_key(table)
577
- end
578
- primary_key = table_columns.find { |column| column.name.to_s == primary_key_name.to_s } if primary_key_name
579
- automatic_id = primary_key && primary_key_name.to_s == "id" && primary_key.type.to_sym == :integer
580
- table_options = automatic_id ? "" : ", id: false"
581
- schema << "create_table \"#{table}\"#{table_options} do |t|\n"
582
- table_columns.each do |col|
583
- next if automatic_id && primary_key && col.name.to_s == primary_key.name.to_s
584
-
585
- type = RubyDB::Rails::Type.to_rails(col.type)
586
- schema << " t.#{type} \"#{col.name}\""
587
- schema << ", primary_key: true" if primary_key && col.name.to_s == primary_key.name.to_s
588
- schema << ", default: #{schema_literal(col.default, col.type)}" unless col.default.nil?
589
- schema << ", null: false" unless col.null
590
- schema << "\n"
591
- end
592
- schema << "end\n\n"
593
-
594
- indexes(table).each do |index|
595
- schema << "add_index \"#{table}\", #{index.columns.map(&:to_s).inspect}"
596
- schema << ", unique: true" if index.unique
597
- schema << ", name: #{index.name.to_s.inspect}\n"
598
- end
599
- schema << "\n" if indexes(table).any?
600
- end
601
- schema
602
- end
603
-
604
- def schema_literal(value, type = nil)
605
- if type.to_sym == :boolean && value.is_a?(String) && %w[true false].include?(value.downcase)
606
- return value.downcase
607
- end
608
-
609
- case value
610
- when true then "true"
611
- when false then "false"
612
- when Numeric then value.to_s
613
- else value.to_s.inspect
614
- end
615
- end
616
-
617
- # ==================== CONNECTION MANAGEMENT ====================
618
-
619
- def reset!
620
- @connection.disconnect
621
- @connection.connect
622
- @prepared_statements.clear
623
- @query_cache.clear
624
- @statements.clear
625
- end
626
-
627
- def disconnect!
628
- @connection.disconnect
629
- end
630
-
631
- def reconnect!
632
- reset!
633
- end
634
-
635
- def active?
636
- @connection.connected?
637
- end
638
-
639
- def close
640
- @connection.disconnect
641
- end
642
-
643
- # ==================== QUERY CACHE ====================
644
-
645
- def clear_cache!
646
- @query_cache.clear
647
- end
648
-
649
- def enable_query_cache!
650
- @query_cache_enabled = true
651
- @query_cache.clear
652
- end
653
-
654
- def disable_query_cache!
655
- @query_cache_enabled = false
656
- @query_cache.clear
657
- end
658
-
659
- def query_cache_enabled
660
- @query_cache_enabled
661
- end
662
-
663
- # ==================== PREPARED STATEMENTS ====================
664
-
665
- def prepare_statement(sql)
666
- @lock.synchronize do
667
- stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
668
- @statement_counter += 1
669
-
670
- result = @connection.prepare(sql)
671
- @prepared_statements[stmt_id] = {
672
- id: result.statement_id,
673
- sql: sql,
674
- created_at: Time.now
675
- }
676
-
677
- stmt_id
678
- end
679
- end
680
-
681
- def execute_prepared_statement(stmt_id, params = [])
682
- @lock.synchronize do
683
- stmt = @prepared_statements[stmt_id]
684
- return nil unless stmt
685
-
686
- @connection.execute_prepared(stmt[:id], params)
687
- end
688
- end
689
-
690
- def close_statement(stmt_id)
691
- @lock.synchronize do
692
- stmt = @prepared_statements.delete(stmt_id)
693
- if stmt
694
- @connection.close_statement(stmt[:id])
695
- end
696
- end
697
- end
698
-
699
- # ==================== VERSION INFORMATION ====================
700
-
701
- def dbms_version
702
- RubyDB::VERSION
703
- end
704
-
705
- # ==================== FEATURE SUPPORT ====================
706
-
707
- def supports_datetime_with_precision?
708
- true
709
- end
710
-
711
- def supports_materialized_views?
712
- false
713
- end
714
-
715
- def supports_common_table_expressions?
716
- false
717
- end
718
-
719
- # ==================== PRIVATE METHODS ====================
720
-
721
- private
722
-
723
- def embedded?
724
- !@connection.engine.nil?
725
- end
726
-
727
- def embedded_columns(table_name)
728
- @connection.engine.table_columns(table_name).map do |column|
729
- ActiveRecord::ConnectionAdapters::Column.new(
730
- column.name.to_s,
731
- column.has_default? ? rails_default_value(column.default) : nil,
732
- ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
733
- sql_type: column.type.to_s.upcase,
734
- type: rails_type_for(column.type),
735
- limit: column.options[:limit]
736
- ),
737
- column.nullable?
738
- )
739
- end
740
- end
741
-
742
- def rails_type_for(type)
743
- case type.to_sym
744
- when :integer, :bigint, :smallint then :integer
745
- when :float then :float
746
- when :decimal then :decimal
747
- when :boolean then :boolean
748
- when :date then :date
749
- when :time then :time
750
- when :datetime, :timestamp then :datetime
751
- when :binary, :blob then :binary
752
- when :json then :json
753
- else :string
754
- end
755
- end
756
-
757
- # ActiveRecord's generic Column deduplication is string-oriented. RubyDB
758
- # persists typed defaults, so serialize scalar defaults at this boundary
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "active_record/connection_adapters/abstract_adapter"
5
+ require "active_record/connection_adapters/abstract/schema_definitions"
6
+ require "active_record/connection_adapters/abstract/schema_statements"
7
+
8
+ require "rubydb"
9
+ require "rubydb/rails/adapter"
10
+ require "rubydb/rails/connection"
11
+ require "rubydb/rails/database_statements"
12
+ require "rubydb/rails/schema_statements"
13
+ require "rubydb/rails/quoting"
14
+ require "rubydb/rails/type"
15
+ require "rubydb/rails/result"
16
+
17
+ module ActiveRecord
18
+ module ConnectionAdapters
19
+ # RubyDB adapter for ActiveRecord
20
+ class RubyDBAdapter < AbstractAdapter
21
+ include RubyDB::Rails::DatabaseStatements
22
+ include RubyDB::Rails::SchemaStatements
23
+ include RubyDB::Rails::Quoting
24
+
25
+ ADAPTER_NAME = "RubyDB"
26
+
27
+ # ActiveRecord 7.2 uses adapter-level methods while compiling hash-form
28
+ # order clauses (for example, `order(created_at: :desc)`). Keep these
29
+ # independent of a live connection so relation construction is safe
30
+ # during schema-cache and query setup as well.
31
+ def self.quote_table_name(name)
32
+ quote_column_name(name)
33
+ end
34
+
35
+ def self.quote_column_name(name)
36
+ "\"#{name.to_s.gsub('"', '""')}\""
37
+ end
38
+
39
+ NATIVE_DATABASE_TYPES = {
40
+ primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT",
41
+ string: {name: "VARCHAR", limit: 255},
42
+ text: {name: "TEXT"},
43
+ integer: {name: "INTEGER"},
44
+ bigint: {name: "BIGINT"},
45
+ smallint: {name: "SMALLINT"},
46
+ float: {name: "FLOAT"},
47
+ decimal: {name: "DECIMAL", precision: 10, scale: 2},
48
+ datetime: {name: "TIMESTAMP"},
49
+ timestamp: {name: "TIMESTAMP"},
50
+ time: {name: "TIME"},
51
+ date: {name: "DATE"},
52
+ binary: {name: "BLOB"},
53
+ boolean: {name: "BOOLEAN"},
54
+ json: {name: "JSON"},
55
+ uuid: {name: "UUID"}
56
+ }
57
+
58
+ # ActiveRecord 7.2 constructs adapters with a single configuration hash.
59
+ # Accept trailing deprecated arguments so applications upgrading from older
60
+ # ActiveRecord versions do not fail during connection establishment.
61
+ def initialize(config, *)
62
+ super(config)
63
+
64
+ @connection = RubyDB::Rails::Connection.new(config)
65
+ @connection.connect
66
+
67
+ @prepared_statements = {}
68
+ @transaction_depth = 0
69
+ @query_cache_enabled = false
70
+ @query_cache = {}
71
+ @statements = {}
72
+ @statement_counter = 0
73
+ # AbstractAdapter uses this monitor while creating transactions. It
74
+ # must be re-entrant because ActiveRecord acquires it recursively.
75
+ @lock = Monitor.new
76
+ end
77
+
78
+ def adapter_name
79
+ ADAPTER_NAME
80
+ end
81
+
82
+ def supports_migrations?
83
+ true
84
+ end
85
+
86
+ def supports_primary_key?
87
+ true
88
+ end
89
+
90
+ def supports_index_sort_order?
91
+ true
92
+ end
93
+
94
+ def supports_transactions?
95
+ true
96
+ end
97
+
98
+ def supports_savepoints?
99
+ true
100
+ end
101
+
102
+ def supports_foreign_keys?
103
+ true
104
+ end
105
+
106
+ def supports_views?
107
+ true
108
+ end
109
+
110
+ def supports_json?
111
+ true
112
+ end
113
+
114
+ def supports_uuid?
115
+ true
116
+ end
117
+
118
+ def supports_bulk_alter?
119
+ false
120
+ end
121
+
122
+ def native_database_types
123
+ NATIVE_DATABASE_TYPES
124
+ end
125
+
126
+ # ==================== SCHEMA METHODS ====================
127
+
128
+ def primary_key(table_name)
129
+ return @connection.engine.table_columns(table_name).find(&:primary_key?)&.name&.to_s || "id" if embedded?
130
+
131
+ result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
132
+ row = result.find { |r| r["pk"] == 1 }
133
+ row ? row["name"] : "id"
134
+ end
135
+
136
+ def tables
137
+ return @connection.engine.list_tables.map(&:to_s) if embedded?
138
+
139
+ result = execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
140
+ result.map { |row| row["name"] }
141
+ end
142
+
143
+ def table_exists?(table_name)
144
+ tables.include?(table_name.to_s)
145
+ end
146
+
147
+ # ActiveRecord's schema cache asks for these before a model is first
148
+ # instantiated. The embedded engine has catalog metadata already, so
149
+ # avoid generating unsupported SQLite catalog queries.
150
+ def data_sources
151
+ return tables if embedded?
152
+
153
+ super
154
+ end
155
+
156
+ def data_source_exists?(name)
157
+ return table_exists?(name) if embedded?
158
+
159
+ super
160
+ end
161
+
162
+ def views
163
+ return [] if embedded?
164
+
165
+ super
166
+ end
167
+
168
+ def indexes(table_name)
169
+ if embedded?
170
+ return @connection.engine.index_manager.get_indexes_for_table(table_name.to_s).map do |index|
171
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
172
+ table_name.to_s,
173
+ index.name.to_s,
174
+ index.unique,
175
+ index.columns.map(&:to_s)
176
+ )
177
+ end
178
+ end
179
+
180
+ result = execute("SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name=?", [table_name])
181
+ result.map do |row|
182
+ {
183
+ name: row["name"],
184
+ columns: parse_index_columns(row["sql"]),
185
+ unique: row["sql"].include?("UNIQUE")
186
+ }
187
+ end
188
+ end
189
+
190
+ def columns(table_name)
191
+ return embedded_columns(table_name) if embedded?
192
+
193
+ result = execute("PRAGMA table_info(#{quote_table_name(table_name)})")
194
+ result.map do |row|
195
+ ActiveRecord::ConnectionAdapters::Column.new(
196
+ row["name"],
197
+ row["default"],
198
+ RubyDB::Rails::Type.to_rails(row["type"]),
199
+ {
200
+ null: row["notnull"] == 0,
201
+ primary_key: row["pk"] == 1,
202
+ limit: extract_limit(row["type"])
203
+ }
204
+ )
205
+ end
206
+ end
207
+
208
+ def column_exists?(table_name, column_name)
209
+ columns(table_name).any? { |c| c.name == column_name }
210
+ end
211
+
212
+ # ==================== QUERY METHODS ====================
213
+
214
+ def execute(sql, name = nil)
215
+ sql = sql_for_execution(sql)
216
+ log(sql, name) do
217
+ @connection.execute(sql)
218
+ end
219
+ end
220
+
221
+ def exec_query(sql, name = nil, binds = [])
222
+ sql = sql_for_execution(sql)
223
+ log(sql, name) do
224
+ params = bind_values(binds)
225
+ active_record_result(@connection.execute(sql, params))
226
+ end
227
+ end
228
+
229
+ def exec_delete(sql, name = nil, binds = [])
230
+ sql = sql_for_execution(sql)
231
+ log(sql, name) do
232
+ params = bind_values(binds)
233
+ result = @connection.execute(sql, params)
234
+ result.affected_rows
235
+ end
236
+ end
237
+
238
+ def exec_update(sql, name = nil, binds = [])
239
+ sql = sql_for_execution(sql)
240
+ log(sql, name) do
241
+ params = bind_values(binds)
242
+ result = @connection.execute(sql, params)
243
+ result.affected_rows
244
+ end
245
+ end
246
+
247
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
248
+ sql = sql_for_execution(sql)
249
+ log(sql, name) do
250
+ params = bind_values(binds)
251
+ result = @connection.execute(sql, params)
252
+ # RubyDB keeps a physical row id for storage operations and a
253
+ # logical primary-key value for SQL/ActiveRecord. They can differ
254
+ # after deletes, so ActiveRecord must receive the logical id.
255
+ id = result.inserted_id || result.row_id
256
+ ActiveRecord::Result.new([pk || "id"], id.nil? ? [] : [[id]])
257
+ end
258
+ end
259
+
260
+ def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
261
+ sql, binds = to_sql_and_binds(arel, binds)
262
+ result = exec_insert(sql, name, binds, pk, sequence_name, returning: returning)
263
+ return returning_column_values(result) unless returning.nil?
264
+
265
+ id_value || last_inserted_id(result)
266
+ end
267
+
268
+ def update(arel, name = nil, binds = [])
269
+ sql, binds = to_sql_and_binds(arel, binds)
270
+ exec_update(sql, name, binds)
271
+ end
272
+
273
+ def delete(arel, name = nil, binds = [])
274
+ sql, binds = to_sql_and_binds(arel, binds)
275
+ exec_delete(sql, name, binds)
276
+ end
277
+
278
+ def select_all(sql, name = nil, binds = [], preparable: nil, async: false, allow_retry: false)
279
+ sql, binds = to_sql_and_binds(sql, binds)
280
+ exec_query(sql, name, binds)
281
+ end
282
+
283
+ def select_one(sql, name = nil, binds = [])
284
+ sql, binds = to_sql_and_binds(sql, binds)
285
+ result = exec_query(sql, name, binds)
286
+ result.first
287
+ end
288
+
289
+ def select_value(sql, name = nil, binds = [])
290
+ sql, binds = to_sql_and_binds(sql, binds)
291
+ result = exec_query(sql, name, binds)
292
+ result.first&.values&.first
293
+ end
294
+
295
+ def select_values(sql, name = nil, binds = [])
296
+ sql, binds = to_sql_and_binds(sql, binds)
297
+ result = exec_query(sql, name, binds)
298
+ result.map { |row| row.values.first }
299
+ end
300
+
301
+ def select_rows(sql, name = nil, binds = [])
302
+ sql, binds = to_sql_and_binds(sql, binds)
303
+ result = exec_query(sql, name, binds)
304
+ result.map { |row| row.values }
305
+ end
306
+
307
+ # ==================== TRANSACTION METHODS ====================
308
+
309
+ def begin_db_transaction
310
+ @transaction_depth += 1
311
+ @connection.begin_db_transaction if @transaction_depth == 1
312
+ end
313
+
314
+ def commit_db_transaction
315
+ return if @transaction_depth <= 0
316
+
317
+ @transaction_depth -= 1
318
+ @connection.commit_db_transaction if @transaction_depth == 0
319
+ end
320
+
321
+ def rollback_db_transaction
322
+ return if @transaction_depth <= 0
323
+
324
+ @transaction_depth -= 1
325
+ @connection.rollback_db_transaction if @transaction_depth == 0
326
+ @transaction_depth = 0 if @transaction_depth < 0
327
+ end
328
+
329
+ def create_savepoint(name)
330
+ execute("SAVEPOINT #{name}")
331
+ end
332
+
333
+ def rollback_to_savepoint(name)
334
+ execute("ROLLBACK TO SAVEPOINT #{name}")
335
+ end
336
+
337
+ def release_savepoint(name)
338
+ execute("RELEASE SAVEPOINT #{name}")
339
+ end
340
+
341
+ def in_transaction?
342
+ @transaction_depth > 0
343
+ end
344
+
345
+ def transaction_joinable?
346
+ true
347
+ end
348
+
349
+ def transactional?
350
+ true
351
+ end
352
+
353
+ # ==================== SCHEMA STATEMENT METHODS ====================
354
+
355
+ def create_table(table_name, **options, &)
356
+ RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &)
357
+ end
358
+
359
+ def drop_table(table_name, **options)
360
+ sql = +"DROP TABLE"
361
+ sql << " IF EXISTS" if options[:if_exists]
362
+ sql << " #{quote_table_name(table_name)}"
363
+ sql << " CASCADE" if options[:cascade]
364
+ execute(sql)
365
+ end
366
+
367
+ def add_column(table_name, column_name, type, **options)
368
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
369
+ sql << " ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options)}"
370
+
371
+ if options[:null] == false
372
+ sql << " NOT NULL"
373
+ end
374
+
375
+ if options[:default]
376
+ sql << " DEFAULT #{quote_default(options[:default])}"
377
+ end
378
+
379
+ if options[:primary_key]
380
+ sql << " PRIMARY KEY"
381
+ end
382
+
383
+ execute(sql)
384
+ end
385
+
386
+ def remove_column(table_name, column_name, type = nil, **options)
387
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
388
+ sql << " DROP COLUMN #{quote_column_name(column_name)}"
389
+ sql << " CASCADE" if options[:cascade]
390
+ execute(sql)
391
+ end
392
+
393
+ def change_column(table_name, column_name, type, **options)
394
+ # Change column type
395
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
396
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
397
+ sql << " TYPE #{type_to_sql(type, options)}"
398
+ execute(sql)
399
+
400
+ # Change nullability
401
+ if options.key?(:null)
402
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
403
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
404
+ sql << (options[:null] ? " DROP" : " SET") + " NOT NULL"
405
+ execute(sql)
406
+ end
407
+
408
+ # Change default
409
+ if options.key?(:default)
410
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
411
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
412
+ sql << if options[:default].nil?
413
+ " DROP DEFAULT"
414
+ else
415
+ " SET DEFAULT #{quote_default(options[:default])}"
416
+ end
417
+ execute(sql)
418
+ end
419
+ end
420
+
421
+ def rename_column(table_name, column_name, new_column_name)
422
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
423
+ sql << " RENAME COLUMN #{quote_column_name(column_name)}"
424
+ sql << " TO #{quote_column_name(new_column_name)}"
425
+ execute(sql)
426
+ end
427
+
428
+ def rename_table(old_name, new_name)
429
+ sql = "ALTER TABLE #{quote_table_name(old_name)}"
430
+ sql << " RENAME TO #{quote_table_name(new_name)}"
431
+ execute(sql)
432
+ end
433
+
434
+ def add_index(table_name, column_name, **options)
435
+ index_name = options[:name] || "idx_#{table_name}_#{Array(column_name).join("_")}"
436
+ sql = +"CREATE"
437
+ sql << " UNIQUE" if options[:unique]
438
+ sql << " INDEX #{quote_column_name(index_name)}"
439
+ sql << " ON #{quote_table_name(table_name)}"
440
+ sql << " (#{Array(column_name).map { |c| quote_column_name(c) }.join(", ")})"
441
+ sql << " WHERE #{options[:where]}" if options[:where]
442
+ execute(sql)
443
+ end
444
+
445
+ def remove_index(table_name, column_name = nil, **options)
446
+ index_name = options[:name]
447
+ if index_name.nil?
448
+ column_name ||= options[:column] || options[:columns]
449
+ index_name = "idx_#{table_name}_#{Array(column_name).join("_")}"
450
+ end
451
+
452
+ sql = "DROP INDEX #{quote_column_name(index_name)}"
453
+ execute(sql)
454
+ end
455
+
456
+ def add_foreign_key(from_table, to_table, **options)
457
+ fk_name = options[:name] || "fk_#{from_table}_to_#{to_table}"
458
+ sql = "ALTER TABLE #{quote_table_name(from_table)}"
459
+ sql << " ADD CONSTRAINT #{quote_column_name(fk_name)}"
460
+ sql << " FOREIGN KEY (#{quote_column_name(options[:column] || :id)})"
461
+ sql << " REFERENCES #{quote_table_name(to_table)}"
462
+ sql << " (#{quote_column_name(options[:primary_key] || :id)})"
463
+ sql << " ON DELETE #{options[:on_delete]}" if options[:on_delete]
464
+ sql << " ON UPDATE #{options[:on_update]}" if options[:on_update]
465
+ execute(sql)
466
+ end
467
+
468
+ def foreign_keys(table_name)
469
+ return super unless embedded?
470
+
471
+ constraints = @connection.engine.table_metadata[table_name.to_s]&.fetch(:constraints, []) || []
472
+ constraints.filter_map do |constraint|
473
+ type = constraint[:type] || constraint["type"]
474
+ next unless type.to_s.upcase == "FOREIGN_KEY"
475
+
476
+ columns = constraint[:columns] || constraint["columns"] || []
477
+ reference_table = constraint[:reference_table] || constraint["reference_table"]
478
+ reference_columns = constraint[:reference_columns] || constraint["reference_columns"] || ["id"]
479
+ options = {
480
+ column: Array(columns).first.to_s,
481
+ primary_key: Array(reference_columns).first.to_s,
482
+ name: constraint[:name] || constraint["name"]
483
+ }
484
+ options[:on_delete] = (constraint[:on_delete] || constraint["on_delete"]).to_s if constraint[:on_delete] || constraint["on_delete"]
485
+ options[:on_update] = (constraint[:on_update] || constraint["on_update"]).to_s if constraint[:on_update] || constraint["on_update"]
486
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name.to_s, reference_table.to_s, options)
487
+ end
488
+ end
489
+
490
+ def remove_foreign_key(from_table, **options)
491
+ fk_name = options[:name] || "fk_#{from_table}_to_#{options[:to_table]}"
492
+ sql = "ALTER TABLE #{quote_table_name(from_table)}"
493
+ sql << " DROP CONSTRAINT #{quote_column_name(fk_name)}"
494
+ execute(sql)
495
+ end
496
+
497
+ def add_timestamps(table_name, **options)
498
+ add_column(table_name, :created_at, :datetime, options)
499
+ add_column(table_name, :updated_at, :datetime, options)
500
+ end
501
+
502
+ def remove_timestamps(table_name, **options)
503
+ remove_column(table_name, :updated_at, options)
504
+ remove_column(table_name, :created_at, options)
505
+ end
506
+
507
+ def change_column_null(table_name, column_name, null, default = nil)
508
+ if default
509
+ sql = "UPDATE #{quote_table_name(table_name)}"
510
+ sql << " SET #{quote_column_name(column_name)} = #{quote(default)}"
511
+ sql << " WHERE #{quote_column_name(column_name)} IS NULL"
512
+ execute(sql)
513
+ end
514
+
515
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
516
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
517
+ sql << (null ? " DROP" : " SET") + " NOT NULL"
518
+ execute(sql)
519
+ end
520
+
521
+ def change_column_default(table_name, column_name, default)
522
+ sql = "ALTER TABLE #{quote_table_name(table_name)}"
523
+ sql << " ALTER COLUMN #{quote_column_name(column_name)}"
524
+ sql << if default.nil?
525
+ " DROP DEFAULT"
526
+ else
527
+ " SET DEFAULT #{quote_default(default)}"
528
+ end
529
+ execute(sql)
530
+ end
531
+
532
+ # ==================== QUOTING METHODS ====================
533
+
534
+ def quote(value, column = nil)
535
+ @connection.quote(value, column)
536
+ end
537
+
538
+ def quote_table_name(name)
539
+ @connection.quote_table_name(name)
540
+ end
541
+
542
+ def quote_column_name(name)
543
+ @connection.quote_column_name(name)
544
+ end
545
+
546
+ def quote_default(value)
547
+ quote(value)
548
+ end
549
+
550
+ # ==================== TYPE CASTING ====================
551
+
552
+ def type_cast(value, type)
553
+ RubyDB::Rails::Type.serialize(value, type)
554
+ end
555
+
556
+ def type_cast_from_database(value, type)
557
+ RubyDB::Rails::Type.deserialize(value, type)
558
+ end
559
+
560
+ # ==================== SCHEMA VERSION ====================
561
+
562
+ def schema_version
563
+ result = execute("SELECT version FROM schema_migrations ORDER BY version DESC LIMIT 1")
564
+ result.first ? result.first["version"] : nil
565
+ end
566
+
567
+ def schema_migrations
568
+ result = execute("SELECT version FROM schema_migrations ORDER BY version")
569
+ result.map { |row| row["version"] }
570
+ end
571
+
572
+ def dump_schema
573
+ schema = +""
574
+ tables.each do |table|
575
+ table_columns = columns(table)
576
+ primary_key_name = if embedded?
577
+ @connection.engine.table_columns(table).find(&:primary_key?)&.name
578
+ else
579
+ primary_key(table)
580
+ end
581
+ primary_key = table_columns.find { |column| column.name.to_s == primary_key_name.to_s } if primary_key_name
582
+ automatic_id = primary_key && primary_key_name.to_s == "id" && primary_key.type.to_sym == :integer
583
+ table_options = automatic_id ? "" : ", id: false"
584
+ schema << "create_table \"#{table}\"#{table_options} do |t|\n"
585
+ table_columns.each do |col|
586
+ next if automatic_id && primary_key && col.name.to_s == primary_key.name.to_s
587
+
588
+ type = RubyDB::Rails::Type.to_rails(col.type)
589
+ schema << " t.#{type} \"#{col.name}\""
590
+ schema << ", primary_key: true" if primary_key && col.name.to_s == primary_key.name.to_s
591
+ schema << ", default: #{schema_literal(col.default, col.type)}" unless col.default.nil?
592
+ schema << ", null: false" unless col.null
593
+ schema << "\n"
594
+ end
595
+ schema << "end\n\n"
596
+
597
+ indexes(table).each do |index|
598
+ schema << "add_index \"#{table}\", #{index.columns.map(&:to_s).inspect}"
599
+ schema << ", unique: true" if index.unique
600
+ schema << ", name: #{index.name.to_s.inspect}\n"
601
+ end
602
+ schema << "\n" if indexes(table).any?
603
+ end
604
+ schema
605
+ end
606
+
607
+ def schema_literal(value, type = nil)
608
+ if type.to_sym == :boolean && value.is_a?(String) && %w[true false].include?(value.downcase)
609
+ return value.downcase
610
+ end
611
+
612
+ case value
613
+ when true then "true"
614
+ when false then "false"
615
+ when Numeric then value.to_s
616
+ else value.to_s.inspect
617
+ end
618
+ end
619
+
620
+ # ==================== CONNECTION MANAGEMENT ====================
621
+
622
+ def reset!
623
+ @connection.disconnect
624
+ @connection.connect
625
+ @prepared_statements.clear
626
+ @query_cache.clear
627
+ @statements.clear
628
+ end
629
+
630
+ def disconnect!
631
+ @connection.disconnect
632
+ end
633
+
634
+ def reconnect!
635
+ reset!
636
+ end
637
+
638
+ def active?
639
+ @connection.connected?
640
+ end
641
+
642
+ def close
643
+ @connection.disconnect
644
+ end
645
+
646
+ # ==================== QUERY CACHE ====================
647
+
648
+ def clear_cache!
649
+ @query_cache.clear
650
+ end
651
+
652
+ def enable_query_cache!
653
+ @query_cache_enabled = true
654
+ @query_cache.clear
655
+ end
656
+
657
+ def disable_query_cache!
658
+ @query_cache_enabled = false
659
+ @query_cache.clear
660
+ end
661
+
662
+ attr_reader :query_cache_enabled
663
+
664
+ # ==================== PREPARED STATEMENTS ====================
665
+
666
+ def prepare_statement(sql)
667
+ @lock.synchronize do
668
+ stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
669
+ @statement_counter += 1
670
+
671
+ result = @connection.prepare(sql)
672
+ @prepared_statements[stmt_id] = {
673
+ id: result.statement_id,
674
+ sql: sql,
675
+ created_at: Time.now
676
+ }
677
+
678
+ stmt_id
679
+ end
680
+ end
681
+
682
+ def execute_prepared_statement(stmt_id, params = [])
683
+ @lock.synchronize do
684
+ stmt = @prepared_statements[stmt_id]
685
+ return nil unless stmt
686
+
687
+ @connection.execute_prepared(stmt[:id], params)
688
+ end
689
+ end
690
+
691
+ def close_statement(stmt_id)
692
+ @lock.synchronize do
693
+ stmt = @prepared_statements.delete(stmt_id)
694
+ if stmt
695
+ @connection.close_statement(stmt[:id])
696
+ end
697
+ end
698
+ end
699
+
700
+ # ==================== VERSION INFORMATION ====================
701
+
702
+ def dbms_version
703
+ RubyDB::VERSION
704
+ end
705
+
706
+ # ==================== FEATURE SUPPORT ====================
707
+
708
+ def supports_datetime_with_precision?
709
+ true
710
+ end
711
+
712
+ def supports_materialized_views?
713
+ false
714
+ end
715
+
716
+ def supports_common_table_expressions?
717
+ false
718
+ end
719
+
720
+ # ==================== PRIVATE METHODS ====================
721
+
722
+ private
723
+
724
+ def embedded?
725
+ !@connection.engine.nil?
726
+ end
727
+
728
+ def embedded_columns(table_name)
729
+ @connection.engine.table_columns(table_name).map do |column|
730
+ ActiveRecord::ConnectionAdapters::Column.new(
731
+ column.name.to_s,
732
+ column.has_default? ? rails_default_value(column.default) : nil,
733
+ ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
734
+ sql_type: column.type.to_s.upcase,
735
+ type: rails_type_for(column.type),
736
+ limit: column.options[:limit]
737
+ ),
738
+ column.nullable?
739
+ )
740
+ end
741
+ end
742
+
743
+ def rails_type_for(type)
744
+ case type.to_sym
745
+ when :integer, :bigint, :smallint then :integer
746
+ when :float then :float
747
+ when :decimal then :decimal
748
+ when :boolean then :boolean
749
+ when :date then :date
750
+ when :time then :time
751
+ when :datetime, :timestamp then :datetime
752
+ when :binary, :blob then :binary
753
+ when :json then :json
754
+ else :string
755
+ end
756
+ end
757
+
758
+ # ActiveRecord's generic Column deduplication is string-oriented. RubyDB
759
+ # persists typed defaults, so serialize scalar defaults at this boundary
759
760
  # and let ActiveRecord cast them through the column type map.
760
761
  def rails_default_value(value)
761
762
  # RubyDB exposes SQL defaults as AST literals. ActiveRecord expects
@@ -765,21 +766,21 @@ module ActiveRecord
765
766
  value = value.value if value.respond_to?(:value) && !value.is_a?(String)
766
767
  value.is_a?(String) ? value : value.to_s
767
768
  end
768
-
769
- def sql_for_execution(sql)
770
- sql = sql.to_sql if sql.respond_to?(:to_sql)
771
- sql
772
- end
773
-
774
- def active_record_result(result)
775
- rows = result.to_a
776
- columns = if rows.first.respond_to?(:keys)
777
- rows.first.keys.map(&:to_s)
778
- else
779
- result.columns.map do |column|
780
- column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
781
- end.map(&:to_s)
782
- end
769
+
770
+ def sql_for_execution(sql)
771
+ sql = sql.to_sql if sql.respond_to?(:to_sql)
772
+ sql
773
+ end
774
+
775
+ def active_record_result(result)
776
+ rows = result.to_a
777
+ columns = if rows.first.respond_to?(:keys)
778
+ rows.first.keys.map(&:to_s)
779
+ else
780
+ result.columns.map do |column|
781
+ column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
782
+ end.map(&:to_s)
783
+ end
783
784
  values = rows.map do |row|
784
785
  columns.map do |column|
785
786
  if row.respond_to?(:key?) && row.key?(column)
@@ -789,110 +790,106 @@ module ActiveRecord
789
790
  end
790
791
  end
791
792
  end
792
- ActiveRecord::Result.new(columns, values)
793
- end
794
-
795
- # ActiveRecord normally supplies QueryAttribute objects, but migration
796
- # and schema code can also pass raw values or two-element bind pairs.
797
- # Normalize all supported forms at the adapter boundary.
798
- def bind_values(binds)
799
- binds.map do |bind|
800
- value = if bind.respond_to?(:value_for_database)
801
- bind.value_for_database
802
- elsif bind.respond_to?(:value)
803
- bind.value
804
- elsif bind.is_a?(Array) && bind.length == 2
805
- bind.last
806
- else
807
- bind
808
- end
809
- value.respond_to?(:value_for_database) ? value.value_for_database : value
810
- end
811
- end
812
-
813
- def parse_index_columns(sql)
814
- if sql =~ /\(([^)]+)\)/
815
- $1.split(",").map(&:strip)
816
- else
817
- []
818
- end
819
- end
820
-
821
- def extract_limit(type)
822
- if type =~ /VARCHAR\((\d+)\)/
823
- $1.to_i
824
- else
825
- nil
826
- end
827
- end
828
-
829
- def log(sql, name = nil, &block)
830
- start_time = Time.now
831
- result = block.call
832
- elapsed_ms = (Time.now - start_time) * 1000
833
-
834
- if @logger
835
- @logger.debug " #{name || 'SQL'} (#{elapsed_ms.round(2)}ms) #{sql}"
836
- end
837
-
838
- result
839
- end
840
-
841
- def type_to_sql(type, options = {})
842
- case type.to_sym
843
- when :integer
844
- "INTEGER"
845
- when :bigint
846
- "BIGINT"
847
- when :smallint
848
- "SMALLINT"
849
- when :float
850
- "FLOAT"
851
- when :decimal
852
- precision = options[:precision] || 10
853
- scale = options[:scale] || 2
854
- "DECIMAL(#{precision}, #{scale})"
855
- when :boolean
856
- "BOOLEAN"
857
- when :text
858
- "TEXT"
859
- when :string
860
- limit = options[:limit] || 255
861
- "VARCHAR(#{limit})"
862
- when :binary
863
- "BLOB"
864
- when :date
865
- "DATE"
866
- when :time
867
- "TIME"
868
- when :datetime, :timestamp
869
- "TIMESTAMP"
870
- when :json
871
- "JSON"
872
- when :uuid
873
- "UUID"
874
- else
875
- "TEXT"
876
- end
877
- end
878
- end
879
- end
880
- end
881
-
882
- # Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
883
- # adapters through the conventional `rubydb_connection` hook instead.
884
- if ActiveRecord::ConnectionAdapters.respond_to?(:register)
885
- ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
886
- else
887
- module ActiveRecord
888
- module ConnectionHandling
889
- def rubydb_adapter_class
890
- ConnectionAdapters::RubyDBAdapter
891
- end
892
-
893
- def rubydb_connection(config)
894
- rubydb_adapter_class.new(config)
895
- end
896
- end
897
- end
898
- end
793
+ ActiveRecord::Result.new(columns, values)
794
+ end
795
+
796
+ # ActiveRecord normally supplies QueryAttribute objects, but migration
797
+ # and schema code can also pass raw values or two-element bind pairs.
798
+ # Normalize all supported forms at the adapter boundary.
799
+ def bind_values(binds)
800
+ binds.map do |bind|
801
+ value = if bind.respond_to?(:value_for_database)
802
+ bind.value_for_database
803
+ elsif bind.respond_to?(:value)
804
+ bind.value
805
+ elsif bind.is_a?(Array) && bind.length == 2
806
+ bind.last
807
+ else
808
+ bind
809
+ end
810
+ value.respond_to?(:value_for_database) ? value.value_for_database : value
811
+ end
812
+ end
813
+
814
+ def parse_index_columns(sql)
815
+ if sql =~ /\(([^)]+)\)/
816
+ $1.split(",").map(&:strip)
817
+ else
818
+ []
819
+ end
820
+ end
821
+
822
+ def extract_limit(type)
823
+ if type =~ /VARCHAR\((\d+)\)/
824
+ $1.to_i
825
+ end
826
+ end
827
+
828
+ def log(sql, name = nil, &block)
829
+ start_time = Time.now
830
+ result = block.call
831
+ elapsed_ms = (Time.now - start_time) * 1000
832
+
833
+ @logger&.debug " #{name || "SQL"} (#{elapsed_ms.round(2)}ms) #{sql}"
834
+
835
+ result
836
+ end
837
+
838
+ def type_to_sql(type, options = {})
839
+ case type.to_sym
840
+ when :integer
841
+ "INTEGER"
842
+ when :bigint
843
+ "BIGINT"
844
+ when :smallint
845
+ "SMALLINT"
846
+ when :float
847
+ "FLOAT"
848
+ when :decimal
849
+ precision = options[:precision] || 10
850
+ scale = options[:scale] || 2
851
+ "DECIMAL(#{precision}, #{scale})"
852
+ when :boolean
853
+ "BOOLEAN"
854
+ when :text
855
+ "TEXT"
856
+ when :string
857
+ limit = options[:limit] || 255
858
+ "VARCHAR(#{limit})"
859
+ when :binary
860
+ "BLOB"
861
+ when :date
862
+ "DATE"
863
+ when :time
864
+ "TIME"
865
+ when :datetime, :timestamp
866
+ "TIMESTAMP"
867
+ when :json
868
+ "JSON"
869
+ when :uuid
870
+ "UUID"
871
+ else
872
+ "TEXT"
873
+ end
874
+ end
875
+ end
876
+ end
877
+ end
878
+
879
+ # Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
880
+ # adapters through the conventional `rubydb_connection` hook instead.
881
+ if ActiveRecord::ConnectionAdapters.respond_to?(:register)
882
+ ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
883
+ else
884
+ module ActiveRecord
885
+ module ConnectionHandling
886
+ def rubydb_adapter_class
887
+ ConnectionAdapters::RubyDBAdapter
888
+ end
889
+
890
+ def rubydb_connection(config)
891
+ rubydb_adapter_class.new(config)
892
+ end
893
+ end
894
+ end
895
+ end