rubydb 0.1.4 → 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 +43 -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 +873 -879
  30. data/adapters/activerecord/rubydb-activerecord.gemspec +20 -20
  31. data/adapters/activerecord/spec/rubydb_adapter_integration_spec.rb +143 -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 +1182 -1188
  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 +117 -117
  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,764 +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)
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
252
  # RubyDB keeps a physical row id for storage operations and a
253
253
  # logical primary-key value for SQL/ActiveRecord. They can differ
254
254
  # after deletes, so ActiveRecord must receive the logical id.
255
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, &block)
356
- RubyDB::Rails::SchemaStatements.instance_method(:create_table).bind_call(self, table_name, options, &block)
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
- if options[:default].nil?
413
- sql << " DROP DEFAULT"
414
- else
415
- sql << " 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
- if default.nil?
525
- sql << " DROP DEFAULT"
526
- else
527
- sql << " 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
- def query_cache_enabled
663
- @query_cache_enabled
664
- end
665
-
666
- # ==================== PREPARED STATEMENTS ====================
667
-
668
- def prepare_statement(sql)
669
- @lock.synchronize do
670
- stmt_id = "stmt_#{Time.now.to_i}_#{@statement_counter}"
671
- @statement_counter += 1
672
-
673
- result = @connection.prepare(sql)
674
- @prepared_statements[stmt_id] = {
675
- id: result.statement_id,
676
- sql: sql,
677
- created_at: Time.now
678
- }
679
-
680
- stmt_id
681
- end
682
- end
683
-
684
- def execute_prepared_statement(stmt_id, params = [])
685
- @lock.synchronize do
686
- stmt = @prepared_statements[stmt_id]
687
- return nil unless stmt
688
-
689
- @connection.execute_prepared(stmt[:id], params)
690
- end
691
- end
692
-
693
- def close_statement(stmt_id)
694
- @lock.synchronize do
695
- stmt = @prepared_statements.delete(stmt_id)
696
- if stmt
697
- @connection.close_statement(stmt[:id])
698
- end
699
- end
700
- end
701
-
702
- # ==================== VERSION INFORMATION ====================
703
-
704
- def dbms_version
705
- RubyDB::VERSION
706
- end
707
-
708
- # ==================== FEATURE SUPPORT ====================
709
-
710
- def supports_datetime_with_precision?
711
- true
712
- end
713
-
714
- def supports_materialized_views?
715
- false
716
- end
717
-
718
- def supports_common_table_expressions?
719
- false
720
- end
721
-
722
- # ==================== PRIVATE METHODS ====================
723
-
724
- private
725
-
726
- def embedded?
727
- !@connection.engine.nil?
728
- end
729
-
730
- def embedded_columns(table_name)
731
- @connection.engine.table_columns(table_name).map do |column|
732
- ActiveRecord::ConnectionAdapters::Column.new(
733
- column.name.to_s,
734
- column.has_default? ? rails_default_value(column.default) : nil,
735
- ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(
736
- sql_type: column.type.to_s.upcase,
737
- type: rails_type_for(column.type),
738
- limit: column.options[:limit]
739
- ),
740
- column.nullable?
741
- )
742
- end
743
- end
744
-
745
- def rails_type_for(type)
746
- case type.to_sym
747
- when :integer, :bigint, :smallint then :integer
748
- when :float then :float
749
- when :decimal then :decimal
750
- when :boolean then :boolean
751
- when :date then :date
752
- when :time then :time
753
- when :datetime, :timestamp then :datetime
754
- when :binary, :blob then :binary
755
- when :json then :json
756
- else :string
757
- end
758
- end
759
-
760
- # ActiveRecord's generic Column deduplication is string-oriented. RubyDB
761
- # persists typed defaults, so serialize scalar defaults at this boundary
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
762
760
  # and let ActiveRecord cast them through the column type map.
763
761
  def rails_default_value(value)
764
762
  # RubyDB exposes SQL defaults as AST literals. ActiveRecord expects
@@ -768,21 +766,21 @@ module ActiveRecord
768
766
  value = value.value if value.respond_to?(:value) && !value.is_a?(String)
769
767
  value.is_a?(String) ? value : value.to_s
770
768
  end
771
-
772
- def sql_for_execution(sql)
773
- sql = sql.to_sql if sql.respond_to?(:to_sql)
774
- sql
775
- end
776
-
777
- def active_record_result(result)
778
- rows = result.to_a
779
- columns = if rows.first.respond_to?(:keys)
780
- rows.first.keys.map(&:to_s)
781
- else
782
- result.columns.map do |column|
783
- column.is_a?(Hash) ? (column[:name] || column["name"] || column) : column
784
- end.map(&:to_s)
785
- 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
786
784
  values = rows.map do |row|
787
785
  columns.map do |column|
788
786
  if row.respond_to?(:key?) && row.key?(column)
@@ -792,110 +790,106 @@ module ActiveRecord
792
790
  end
793
791
  end
794
792
  end
795
- ActiveRecord::Result.new(columns, values)
796
- end
797
-
798
- # ActiveRecord normally supplies QueryAttribute objects, but migration
799
- # and schema code can also pass raw values or two-element bind pairs.
800
- # Normalize all supported forms at the adapter boundary.
801
- def bind_values(binds)
802
- binds.map do |bind|
803
- value = if bind.respond_to?(:value_for_database)
804
- bind.value_for_database
805
- elsif bind.respond_to?(:value)
806
- bind.value
807
- elsif bind.is_a?(Array) && bind.length == 2
808
- bind.last
809
- else
810
- bind
811
- end
812
- value.respond_to?(:value_for_database) ? value.value_for_database : value
813
- end
814
- end
815
-
816
- def parse_index_columns(sql)
817
- if sql =~ /\(([^)]+)\)/
818
- $1.split(",").map(&:strip)
819
- else
820
- []
821
- end
822
- end
823
-
824
- def extract_limit(type)
825
- if type =~ /VARCHAR\((\d+)\)/
826
- $1.to_i
827
- else
828
- nil
829
- end
830
- end
831
-
832
- def log(sql, name = nil, &block)
833
- start_time = Time.now
834
- result = block.call
835
- elapsed_ms = (Time.now - start_time) * 1000
836
-
837
- if @logger
838
- @logger.debug " #{name || 'SQL'} (#{elapsed_ms.round(2)}ms) #{sql}"
839
- end
840
-
841
- result
842
- end
843
-
844
- def type_to_sql(type, options = {})
845
- case type.to_sym
846
- when :integer
847
- "INTEGER"
848
- when :bigint
849
- "BIGINT"
850
- when :smallint
851
- "SMALLINT"
852
- when :float
853
- "FLOAT"
854
- when :decimal
855
- precision = options[:precision] || 10
856
- scale = options[:scale] || 2
857
- "DECIMAL(#{precision}, #{scale})"
858
- when :boolean
859
- "BOOLEAN"
860
- when :text
861
- "TEXT"
862
- when :string
863
- limit = options[:limit] || 255
864
- "VARCHAR(#{limit})"
865
- when :binary
866
- "BLOB"
867
- when :date
868
- "DATE"
869
- when :time
870
- "TIME"
871
- when :datetime, :timestamp
872
- "TIMESTAMP"
873
- when :json
874
- "JSON"
875
- when :uuid
876
- "UUID"
877
- else
878
- "TEXT"
879
- end
880
- end
881
- end
882
- end
883
- end
884
-
885
- # Rails 7.2 introduced explicit adapter registration. Rails 7.1 loads custom
886
- # adapters through the conventional `rubydb_connection` hook instead.
887
- if ActiveRecord::ConnectionAdapters.respond_to?(:register)
888
- ActiveRecord::ConnectionAdapters.register("rubydb", "ActiveRecord::ConnectionAdapters::RubyDBAdapter")
889
- else
890
- module ActiveRecord
891
- module ConnectionHandling
892
- def rubydb_adapter_class
893
- ConnectionAdapters::RubyDBAdapter
894
- end
895
-
896
- def rubydb_connection(config)
897
- rubydb_adapter_class.new(config)
898
- end
899
- end
900
- end
901
- 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