├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── test ├── sql │ ├── mysql_teardown.sql │ └── mysql_setup.sql ├── model_test.rb ├── trilogy_test.rb ├── test_helper.rb ├── mssql_test.rb ├── mysql2_test.rb ├── sqlite_test.rb ├── postgres_test.rb └── extension_test.rb ├── Gemfile ├── gemfiles ├── Gemfile.activerecord-7.2 ├── Gemfile.activerecord-8.0 ├── Gemfile.activerecord-8.1 ├── Gemfile.activerecord-5.1 ├── Gemfile.activerecord-5.2 ├── Gemfile.activerecord-7.1 ├── Gemfile.activerecord-7.0 ├── Gemfile.activerecord-6.0 └── Gemfile.activerecord-6.1 ├── lib └── sequel │ └── extensions │ ├── activerecord_connection │ ├── oracle.rb │ ├── jdbc.rb │ ├── tinytds.rb │ ├── utils.rb │ ├── sqlite.rb │ ├── mysql2.rb │ └── postgres.rb │ └── activerecord_connection.rb ├── sequel-activerecord_connection.gemspec ├── LICENSE.txt ├── Rakefile ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: janko 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg/ 3 | gemfiles/*.lock 4 | -------------------------------------------------------------------------------- /test/sql/mysql_teardown.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE sequel_activerecord_connection; 2 | DROP USER 'sequel_activerecord_connection'@'localhost'; 3 | -------------------------------------------------------------------------------- /test/sql/mysql_setup.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'sequel_activerecord_connection'@'localhost' IDENTIFIED BY 'sequel_activerecord_connection'; 2 | CREATE DATABASE sequel_activerecord_connection; 3 | GRANT ALL ON sequel_activerecord_connection.* TO 'sequel_activerecord_connection'@'localhost' WITH GRANT OPTION; 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake", "~> 12.0" 6 | 7 | platform :mri do 8 | gem "pg", "~> 1.0" 9 | gem "mysql2", "~> 0.5" 10 | gem "sqlite3", ">= 1.3", "< 3" 11 | gem "trilogy", "~> 2.4" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-7.2: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 7.2.0" 6 | 7 | platform :mri do 8 | gem "pg", "~> 1.0" 9 | gem "mysql2", "~> 0.5" 10 | gem "sqlite3", "~> 1.3" 11 | gem "trilogy", "~> 2.4" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-8.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 8.0.0" 6 | 7 | platform :mri do 8 | gem "pg", "~> 1.0" 9 | gem "mysql2", "~> 0.5" 10 | gem "sqlite3", "~> 2.0" 11 | gem "trilogy", "~> 2.4" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-8.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 8.1.0" 6 | 7 | platform :mri do 8 | gem "pg", "~> 1.0" 9 | gem "mysql2", "~> 0.5" 10 | gem "sqlite3", "~> 2.0" 11 | gem "trilogy", "~> 2.4" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-5.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 5.1.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | platform :mri do 9 | gem "pg", "~> 1.0" 10 | gem "mysql2", "~> 0.5" 11 | gem "sqlite3", "~> 1.3" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-5.2: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 5.2.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | platform :mri do 9 | gem "pg", "~> 1.0" 10 | gem "mysql2", "~> 0.5" 11 | gem "sqlite3", "~> 1.3" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-7.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 7.1.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | platform :mri do 9 | gem "pg", "~> 1.0" 10 | gem "mysql2", "~> 0.5" 11 | gem "sqlite3", "~> 1.3" 12 | gem "trilogy", "~> 2.4" 13 | end 14 | 15 | platform :jruby do 16 | gem "activerecord-jdbc-adapter" 17 | gem "jdbc-sqlite3" 18 | gem "jdbc-mysql" 19 | gem "jdbc-postgres" 20 | end 21 | 22 | gem "rake", "~> 12.0" 23 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/oracle.rb: -------------------------------------------------------------------------------- 1 | require_relative "utils" 2 | 3 | module Sequel 4 | module ActiveRecordConnection 5 | module Oracle 6 | def synchronize(*) 7 | super do |conn| 8 | raw_connection = conn.respond_to?(:raw_oci_connection) ? conn.raw_oci_connection : conn 9 | 10 | # required for prepared statements 11 | Utils.add_prepared_statements_cache(raw_connection) 12 | 13 | yield raw_connection 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-7.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 7.0.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | platform :mri do 9 | gem "pg", "~> 1.0" 10 | gem "mysql2", "~> 0.5" 11 | gem "sqlite3", "~> 1.3" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | 23 | if RUBY_VERSION >= "3.4.0" 24 | gem "mutex_m" 25 | end 26 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-6.0: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 6.0.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | platform :mri do 9 | gem "pg", "~> 1.0" 10 | gem "mysql2", "~> 0.5" 11 | gem "sqlite3", "~> 1.3" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | 23 | if RUBY_VERSION >= "3.4.0" 24 | gem "mutex_m" 25 | gem "base64" 26 | end 27 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.activerecord-6.1: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: ".." 4 | 5 | gem "activerecord", "~> 6.1.0" 6 | gem "after_commit_everywhere", "~> 1.1" 7 | 8 | platform :mri do 9 | gem "pg", "~> 1.0" 10 | gem "mysql2", "~> 0.5" 11 | gem "sqlite3", "~> 1.3" 12 | end 13 | 14 | platform :jruby do 15 | gem "activerecord-jdbc-adapter" 16 | gem "jdbc-sqlite3" 17 | gem "jdbc-mysql" 18 | gem "jdbc-postgres" 19 | end 20 | 21 | gem "rake", "~> 12.0" 22 | 23 | if RUBY_VERSION >= "3.4.0" 24 | gem "mutex_m" 25 | gem "base64" 26 | end 27 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/jdbc.rb: -------------------------------------------------------------------------------- 1 | module Sequel 2 | module ActiveRecordConnection 3 | module Jdbc 4 | def self.extended(db) 5 | if db.timezone == :utc && db.respond_to?(:current_timestamp_utc) 6 | db.current_timestamp_utc = true 7 | end 8 | end 9 | 10 | def synchronize(*) 11 | super do |conn| 12 | if database_type == :oracle 13 | yield conn.raw_connection 14 | else 15 | yield conn.connection 16 | end 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/tinytds.rb: -------------------------------------------------------------------------------- 1 | require_relative "utils" 2 | 3 | module Sequel 4 | module ActiveRecordConnection 5 | module Tinytds 6 | def synchronize(*) 7 | super do |conn| 8 | conn.query_options.merge!(cache_rows: false) 9 | 10 | begin 11 | yield conn 12 | ensure 13 | conn.query_options.merge!(cache_rows: true) 14 | end 15 | end 16 | end 17 | 18 | private 19 | 20 | def activerecord_connection_class 21 | ::TinyTds::Client 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/model_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe "Model integration" do 4 | before do 5 | connect_postgresql 6 | 7 | @db.create_table! :records do 8 | primary_key :id 9 | String :col 10 | end 11 | 12 | @model = Class.new(Sequel::Model) 13 | @model.set_dataset(:records) 14 | end 15 | 16 | it ".create returns model" do 17 | assert_instance_of @model, @model.create 18 | end 19 | 20 | it "#update returns model" do 21 | assert_instance_of @model, @model.new.update(col: "value") 22 | end 23 | 24 | it "#save_changes executes successfully" do 25 | @model.new(col: "value").save_changes 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/utils.rb: -------------------------------------------------------------------------------- 1 | module Sequel 2 | module ActiveRecordConnection 3 | module Utils 4 | def self.set_value(object, name, new_value) 5 | original_value = object.send(name) 6 | object.send(:"#{name}=", new_value) 7 | yield 8 | ensure 9 | object.send(:"#{name}=", original_value) 10 | end 11 | 12 | def self.add_prepared_statements_cache(conn) 13 | return if conn.respond_to?(:prepared_statements) 14 | 15 | class << conn 16 | attr_accessor :prepared_statements 17 | end 18 | conn.prepared_statements = {} 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/sqlite.rb: -------------------------------------------------------------------------------- 1 | require_relative "utils" 2 | 3 | module Sequel 4 | module ActiveRecordConnection 5 | module Sqlite 6 | def self.extended(db) 7 | if db.timezone == :utc && db.respond_to?(:current_timestamp_utc) 8 | db.current_timestamp_utc = true 9 | end 10 | end 11 | 12 | def synchronize(*) 13 | super do |conn| 14 | conn.extended_result_codes = true if conn.respond_to?(:extended_result_codes=) 15 | 16 | Utils.add_prepared_statements_cache(conn) 17 | 18 | yield conn 19 | end 20 | end 21 | 22 | private 23 | 24 | def _execute(type, sql, opts, &block) 25 | synchronize(opts[:server]) do |conn| 26 | Utils.set_value(conn, :results_as_hash, nil) do 27 | super 28 | end 29 | end 30 | end 31 | 32 | def activerecord_connection_class 33 | ::SQLite3::Database 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /sequel-activerecord_connection.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "sequel-activerecord_connection" 3 | spec.version = "2.0.1" 4 | spec.authors = ["Janko Marohnić"] 5 | spec.email = ["janko@hey.com"] 6 | 7 | spec.summary = %q{Allows Sequel to use ActiveRecord connection for database interaction.} 8 | spec.description = %q{Allows Sequel to use ActiveRecord connection for database interaction.} 9 | spec.homepage = "https://github.com/janko/sequel-activerecord_connection" 10 | spec.license = "MIT" 11 | 12 | spec.required_ruby_version = ">= 2.5" 13 | 14 | spec.add_dependency "sequel", "~> 5.38" 15 | spec.add_dependency "activerecord", ">= 5.1" 16 | 17 | spec.add_development_dependency "sequel_pg" unless RUBY_ENGINE == "jruby" 18 | spec.add_development_dependency "minitest" 19 | spec.add_development_dependency "warning" 20 | 21 | spec.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "lib/**/*.rb", "*.gemspec"] 22 | spec.require_paths = ["lib"] 23 | end 24 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/mysql2.rb: -------------------------------------------------------------------------------- 1 | require_relative "utils" 2 | 3 | module Sequel 4 | module ActiveRecordConnection 5 | module Mysql2 6 | def synchronize(*) 7 | super do |conn| 8 | # required for prepared statements 9 | Utils.add_prepared_statements_cache(conn) 10 | 11 | yield conn 12 | end 13 | end 14 | 15 | private 16 | 17 | def _execute(conn, sql, opts) 18 | if conn.instance_variable_defined?(:@sequel_default_query_options) 19 | return super 20 | end 21 | 22 | conn.instance_variable_set(:@sequel_default_query_options, conn.query_options.dup) 23 | conn.query_options.merge!(as: :hash, symbolize_keys: true, cache_rows: false) 24 | begin 25 | super 26 | ensure 27 | conn.query_options.replace(conn.remove_instance_variable(:@sequel_default_query_options)) 28 | end 29 | end 30 | 31 | def activerecord_connection_class 32 | ::Mysql2::Client 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 Janko Marohnić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << "test" 6 | t.test_files = FileList["test/**/*_test.rb"] 7 | t.warning = true 8 | end 9 | 10 | task default: :test 11 | 12 | desc "Setup database used for testing on PostgreSQL" 13 | task :db_setup_postgres do 14 | sh 'psql -U postgres -c "CREATE USER sequel_activerecord_connection PASSWORD \'sequel_activerecord_connection\'"' 15 | sh 'createdb -U postgres -O sequel_activerecord_connection sequel_activerecord_connection' 16 | end 17 | 18 | desc "Teardown database used for testing on PostgreSQL" 19 | task :db_teardown_postgres do 20 | sh 'dropdb -U postgres sequel_activerecord_connection' 21 | sh 'dropuser -U postgres sequel_activerecord_connection' 22 | end 23 | 24 | desc "Setup database used for testing on MySQL" 25 | task :db_setup_mysql do 26 | sh 'mysql -u root -p mysql < test/sql/mysql_setup.sql' 27 | end 28 | 29 | desc "Teardown database used for testing on MySQL" 30 | task :db_teardown_mysql do 31 | sh 'mysql -u root -p mysql < test/sql/mysql_teardown.sql' 32 | end 33 | 34 | desc "Setup database used for testing on Microsoft SQL Server" 35 | task :db_setup_mssql do 36 | sh 'docker exec -it sqledge /opt/mssql/bin/sqlcmd -b -r1 -i /home/mssql/setup.sql' 37 | end 38 | 39 | desc "Teardown database used for testing on Microsoft SQL Server" 40 | task :db_teardown_mssql do 41 | sh 'docker exec -it sqledge /opt/mssql/bin/sqlcmd -b -r1 -i /home/mssql/teardown.sql' 42 | end 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at janko.marohnic@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection/postgres.rb: -------------------------------------------------------------------------------- 1 | require_relative "utils" 2 | 3 | module Sequel 4 | module ActiveRecordConnection 5 | module Postgres 6 | def synchronize(*) 7 | super do |conn| 8 | conn.extend(ConnectionMethods) 9 | conn.instance_variable_set(:@db, self) 10 | 11 | Utils.add_prepared_statements_cache(conn) 12 | 13 | # compatibility for pg_streaming database extension from sequel_pg gem 14 | if defined?(Sequel::Postgres::Streaming) && is_a?(Sequel::Postgres::Streaming) 15 | conn.extend(Sequel::Postgres::Streaming::AdapterMethods) 16 | end 17 | 18 | yield conn 19 | end 20 | end 21 | 22 | # Reject unsupported Postgres-specific transaction options. 23 | def transaction(opts = OPTS) 24 | %i[deferrable read_only synchronous].each do |key| 25 | fail Error, "#{key.inspect} transaction option is currently not supported" if opts.key?(key) 26 | end 27 | 28 | super do |conn| 29 | yield conn 30 | rescue => e 31 | activerecord_connection.clear_cache! if e.class.name == "ActiveRecord::PreparedStatementCacheExpired" 32 | raise 33 | end 34 | end 35 | 36 | private 37 | 38 | def _execute(conn, *) 39 | Utils.set_value(conn, :type_map_for_results, PG::TypeMapAllStrings.new) do 40 | super 41 | end 42 | end 43 | 44 | def activerecord_connection_class 45 | ::PG::Connection 46 | end 47 | 48 | # Copy-pasted from Sequel::Postgres::Adapter. 49 | module ConnectionMethods 50 | # The underlying exception classes to reraise as disconnect errors 51 | # instead of regular database errors. 52 | DISCONNECT_ERROR_CLASSES = Sequel::Postgres::Adapter::DISCONNECT_ERROR_CLASSES 53 | 54 | # Since exception class based disconnect checking may not work, 55 | # also trying parsing the exception message to look for disconnect 56 | # errors. 57 | DISCONNECT_ERROR_REGEX = Sequel::Postgres::Adapter::DISCONNECT_ERROR_RE 58 | 59 | def async_exec_params(sql, args) 60 | defined?(super) ? super : async_exec(sql, args) 61 | end 62 | 63 | # Raise a Sequel::DatabaseDisconnectError if a one of the disconnect 64 | # error classes is raised, or a PG::Error is raised and the connection 65 | # status cannot be determined or it is not OK. 66 | def check_disconnect_errors 67 | begin 68 | yield 69 | rescue *DISCONNECT_ERROR_CLASSES => e 70 | disconnect = true 71 | raise(Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError)) 72 | rescue PG::Error => e 73 | disconnect = false 74 | begin 75 | s = status 76 | rescue PG::Error 77 | disconnect = true 78 | end 79 | status_ok = (s == PG::CONNECTION_OK) 80 | disconnect ||= !status_ok 81 | disconnect ||= e.message =~ DISCONNECT_ERROR_REGEX 82 | disconnect ? raise(Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError)) : raise 83 | ensure 84 | block if status_ok && !disconnect 85 | end 86 | end 87 | 88 | # Execute the given SQL with this connection. If a block is given, 89 | # yield the results, otherwise, return the number of changed rows. 90 | def execute(sql, args = nil) 91 | args = args.map { |v| @db.bound_variable_arg(v, self) } if args 92 | q = check_disconnect_errors { execute_query(sql, args) } 93 | 94 | block_given? ? yield(q) : q.cmd_tuples 95 | ensure 96 | q.clear if q && q.respond_to?(:clear) 97 | end 98 | 99 | private 100 | 101 | # Return the PG::Result containing the query results. 102 | def execute_query(sql, args) 103 | @db.log_connection_yield(sql, self, args) do 104 | args ? async_exec_params(sql, args) : async_exec(sql) 105 | end 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/trilogy_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe "trilogy connection" do 4 | before do 5 | connect_trilogy 6 | 7 | @db.create_table! :records do 8 | primary_key :id 9 | String :col 10 | Time :time 11 | end 12 | end 13 | 14 | it "supports Dataset#insert" do 15 | assert_equal 1, @db[:records].insert(col: "a") 16 | assert_equal 2, @db[:records].insert(col: "a") 17 | end 18 | 19 | it "supports Dataset#each" do 20 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }, { col: "c" }] 21 | 22 | records = @db[:records].order(:id).all 23 | 24 | assert_equal 1, records[0][:id] 25 | assert_equal "a", records[0][:col] 26 | assert_equal 2, records[1][:id] 27 | assert_equal "b", records[1][:col] 28 | assert_equal 3, records[2][:id] 29 | assert_equal "c", records[2][:col] 30 | 31 | assert_equal [:id, :col, :time], @db[:records].columns 32 | 33 | assert_logged <<~SQL 34 | INSERT INTO `records` (`col`) VALUES ('a'), ('b'), ('c') 35 | SELECT version() 36 | SELECT * FROM `records` ORDER BY `id` 37 | SQL 38 | end 39 | 40 | it "handles empty dataset" do 41 | assert_equal [], @db[:records].all 42 | assert_equal [:id, :col, :time], @db[:records].columns 43 | end 44 | 45 | it "supports Database#update" do 46 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }] 47 | 48 | assert_equal 0, @db[:records].where(col: "c").update(col: "x") 49 | assert_equal 1, @db[:records].where(col: "a").update(col: "y") 50 | assert_equal 2, @db[:records].update(col: "z") 51 | 52 | records = @db[:records].order(:id).all 53 | 54 | assert_equal 1, records[0][:id] 55 | assert_equal "z", records[0][:col] 56 | assert_equal 2, records[1][:id] 57 | assert_equal "z", records[1][:col] 58 | 59 | assert_logged <<~SQL 60 | UPDATE `records` SET `col` = 'x' WHERE (`col` = 'c') 61 | UPDATE `records` SET `col` = 'y' WHERE (`col` = 'a') 62 | UPDATE `records` SET `col` = 'z' 63 | SQL 64 | end 65 | 66 | it "supports Database#get" do 67 | assert_instance_of Time, @db.get(Sequel::CURRENT_TIMESTAMP) 68 | assert_equal 1, @db.get(1) 69 | assert_equal "foo", @db.get("foo") 70 | 71 | assert_logged <<~SQL 72 | SELECT CURRENT_TIMESTAMP AS `v` LIMIT 1 73 | SELECT 1 AS `v` LIMIT 1 74 | SELECT 'foo' AS `v` LIMIT 1 75 | SQL 76 | end 77 | 78 | it "raises Sequel exceptions" do 79 | assert_raises Sequel::UniqueConstraintViolation do 80 | @db[:records].multi_insert [{ id: 1 }, { id: 1 }] 81 | end 82 | 83 | @db.alter_table(:records) { add_foreign_key :fkey, :records } 84 | 85 | assert_raises Sequel::ForeignKeyConstraintViolation do 86 | @db[:records].insert(fkey: 50) 87 | end 88 | 89 | @db.alter_table(:records) { add_column :required, :text, null: false } 90 | 91 | assert_raises Sequel::NotNullConstraintViolation do 92 | @db[:records].insert(required: nil) 93 | end 94 | end 95 | 96 | it "correctly handles ActiveRecord's default UTC timezone setting" do 97 | time = Time.new(2020, 4, 26, 0, 0, 0, "+02:00") 98 | 99 | @db[:records].insert(time: time) 100 | 101 | assert_equal time, @db[:records].first[:time] 102 | 103 | assert_logged <<~SQL 104 | INSERT INTO `records` (`time`) VALUES ('2020-04-25 22:00:00') 105 | SQL 106 | end 107 | 108 | it "correctly handles ActiveRecord's local timezone setting" do 109 | set_activerecord_timezone(:local) 110 | 111 | time = Time.new(2020, 4, 26, 0, 0, 0) 112 | 113 | @db[:records].insert(time: time) 114 | 115 | assert_equal time, @db[:records].first[:time] 116 | 117 | assert_logged <<~SQL 118 | INSERT INTO `records` (`time`) VALUES ('2020-04-26 00:00:00') 119 | SQL 120 | end 121 | 122 | it "allows calling Active Record queries inside transaction" do 123 | activerecord_model = Class.new(ActiveRecord::Base) 124 | activerecord_model.table_name = :records 125 | 126 | @db.transaction do 127 | record = activerecord_model.create(col: "foo", time: Time.new(2021, 1, 10)) 128 | record = activerecord_model.find(record.id) 129 | 130 | assert_equal "foo", record.col 131 | assert_equal Time.new(2021, 1, 10), record.time 132 | end 133 | end 134 | end unless ActiveRecord.version < Gem::Version.new("7.1") || RUBY_ENGINE == "jruby" 135 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ '**' ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | tests: 14 | runs-on: ubuntu-latest 15 | 16 | services: 17 | postgres: 18 | image: postgres:11.5 19 | ports: ["5432:5432"] 20 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 21 | env: 22 | POSTGRES_DB: sequel_activerecord_connection 23 | POSTGRES_USER: sequel_activerecord_connection 24 | POSTGRES_PASSWORD: sequel_activerecord_connection 25 | 26 | mysql: 27 | image: mysql:5.7 28 | ports: ["3306:3306"] 29 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 30 | env: 31 | MYSQL_ROOT_PASSWORD: sequel_activerecord_connection 32 | MYSQL_DATABASE: sequel_activerecord_connection 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | ruby: ["ruby-2.5", "ruby-2.6", "ruby-2.7", "ruby-3.0", "ruby-3.1", "ruby-3.2", "ruby-3.3", "ruby-3.4", "jruby-9.4"] 38 | gemfile: ["activerecord-5.1", "activerecord-5.2", "activerecord-6.0", "activerecord-6.1", "activerecord-7.0", "activerecord-7.1", "activerecord-7.2", "activerecord-8.0", "activerecord-8.1"] 39 | exclude: 40 | - ruby: "ruby-3.4" 41 | gemfile: "activerecord-5.2" 42 | - ruby: "ruby-3.4" 43 | gemfile: "activerecord-5.1" 44 | - ruby: "ruby-3.3" 45 | gemfile: "activerecord-5.2" 46 | - ruby: "ruby-3.3" 47 | gemfile: "activerecord-5.1" 48 | - ruby: "ruby-3.2" 49 | gemfile: "activerecord-5.2" 50 | - ruby: "ruby-3.2" 51 | gemfile: "activerecord-5.1" 52 | - ruby: "ruby-3.1" 53 | gemfile: "activerecord-8.1" 54 | - ruby: "ruby-3.1" 55 | gemfile: "activerecord-8.0" 56 | - ruby: "ruby-3.1" 57 | gemfile: "activerecord-5.2" 58 | - ruby: "ruby-3.1" 59 | gemfile: "activerecord-5.1" 60 | - ruby: "ruby-3.0" 61 | gemfile: "activerecord-8.1" 62 | - ruby: "ruby-3.0" 63 | gemfile: "activerecord-8.0" 64 | - ruby: "ruby-3.0" 65 | gemfile: "activerecord-7.2" 66 | - ruby: "ruby-3.0" 67 | gemfile: "activerecord-5.2" 68 | - ruby: "ruby-3.0" 69 | gemfile: "activerecord-5.1" 70 | - ruby: "ruby-2.7" 71 | gemfile: "activerecord-8.1" 72 | - ruby: "ruby-2.7" 73 | gemfile: "activerecord-8.0" 74 | - ruby: "ruby-2.7" 75 | gemfile: "activerecord-7.2" 76 | - ruby: "ruby-2.6" 77 | gemfile: "activerecord-8.1" 78 | - ruby: "ruby-2.6" 79 | gemfile: "activerecord-8.0" 80 | - ruby: "ruby-2.6" 81 | gemfile: "activerecord-7.2" 82 | - ruby: "ruby-2.6" 83 | gemfile: "activerecord-7.1" 84 | - ruby: "ruby-2.6" 85 | gemfile: "activerecord-7.0" 86 | - ruby: "ruby-2.5" 87 | gemfile: "activerecord-8.1" 88 | - ruby: "ruby-2.5" 89 | gemfile: "activerecord-8.0" 90 | - ruby: "ruby-2.5" 91 | gemfile: "activerecord-7.2" 92 | - ruby: "ruby-2.5" 93 | gemfile: "activerecord-7.1" 94 | - ruby: "ruby-2.5" 95 | gemfile: "activerecord-7.0" 96 | - ruby: "jruby-9.4" 97 | gemfile: "activerecord-8.1" 98 | - ruby: "jruby-9.4" 99 | gemfile: "activerecord-8.0" 100 | - ruby: "jruby-9.4" 101 | gemfile: "activerecord-7.2" 102 | - ruby: "jruby-9.4" 103 | gemfile: "activerecord-7.1" 104 | - ruby: "jruby-9.4" 105 | gemfile: "activerecord-6.0" 106 | - ruby: "jruby-9.4" 107 | gemfile: "activerecord-5.2" 108 | - ruby: "jruby-9.4" 109 | gemfile: "activerecord-5.1" 110 | 111 | env: 112 | BUNDLE_GEMFILE: gemfiles/Gemfile.${{ matrix.gemfile }} 113 | 114 | steps: 115 | - uses: actions/checkout@v3 116 | 117 | - name: "Install database packages" 118 | run: sudo apt-get -yqq install libpq-dev libmysqlclient-dev 119 | 120 | - uses: ruby/setup-ruby@v1 121 | with: 122 | ruby-version: ${{ matrix.ruby }} 123 | bundler-cache: true 124 | 125 | - name: Run tests 126 | run: bundle exec rake test 127 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | require "warning" 4 | Gem.path.each do |path| 5 | Warning.ignore(//, path) 6 | end 7 | 8 | require "minitest/autorun" 9 | require "minitest/pride" 10 | 11 | require "logger" if ActiveRecord.version >= Gem::Version.new("6.0") && ActiveRecord.version < Gem::Version.new("7.1") 12 | require "active_record" 13 | require "sequel" 14 | 15 | require "stringio" 16 | require "active_support/core_ext/string" 17 | 18 | if ActiveRecord.respond_to?(:permanent_connection_checkout) 19 | ActiveRecord.permanent_connection_checkout = :disallowed 20 | end 21 | if ActiveRecord.respond_to?(:legacy_connection_handling) 22 | ActiveRecord.legacy_connection_handling = false 23 | end 24 | 25 | class Minitest::Test 26 | def connect_postgresql 27 | options = {} 28 | 29 | if ENV["CI"] 30 | options[:host] = "localhost" 31 | end 32 | 33 | activerecord_connect( 34 | adapter: "postgresql", 35 | database: "sequel_activerecord_connection", 36 | username: "sequel_activerecord_connection", 37 | password: "sequel_activerecord_connection", 38 | **options, 39 | ) 40 | 41 | @db = Sequel.connect "#{"jdbc:" if RUBY_ENGINE == "jruby"}postgresql://", 42 | extensions: :activerecord_connection 43 | end 44 | 45 | def connect_mysql2 46 | options = {} 47 | 48 | if ActiveRecord.version >= Gem::Version.new("7.1") 49 | options[:prepared_statements] = false 50 | end 51 | 52 | if ENV["CI"] 53 | options[:username] = "root" 54 | options[:host] = "127.0.0.1" 55 | else 56 | options[:username] = "sequel_activerecord_connection" 57 | end 58 | 59 | if RUBY_ENGINE == "jruby" 60 | options[:properties] = { allowPublicKeyRetrieval: true } 61 | end 62 | 63 | activerecord_connect( 64 | adapter: "mysql2", 65 | database: "sequel_activerecord_connection", 66 | password: "sequel_activerecord_connection", 67 | **options 68 | ) 69 | 70 | @db = Sequel.connect (RUBY_ENGINE == "jruby" ? "jdbc:mysql://" : "mysql2://"), 71 | extensions: :activerecord_connection 72 | end 73 | 74 | def connect_trilogy 75 | options = {} 76 | 77 | if ENV["CI"] 78 | options[:username] = "root" 79 | options[:host] = "127.0.0.1" 80 | else 81 | options[:username] = "sequel_activerecord_connection" 82 | end 83 | 84 | activerecord_connect( 85 | adapter: "trilogy", 86 | database: "sequel_activerecord_connection", 87 | password: "sequel_activerecord_connection", 88 | **options 89 | ) 90 | 91 | @db = Sequel.trilogy(extensions: :activerecord_connection) 92 | end 93 | 94 | def connect_sqlite3 95 | activerecord_connect( 96 | adapter: "sqlite3", 97 | database: ":memory:", 98 | password: "sequel_activerecord_connection", 99 | host: "localhost", 100 | ) 101 | 102 | @db = Sequel.connect "#{"jdbc:" if RUBY_ENGINE == "jruby"}sqlite://", 103 | extensions: :activerecord_connection 104 | end 105 | 106 | def connect_sqlserver 107 | raise "JRuby is not supported for SQL Server" if RUBY_ENGINE == "jruby" 108 | 109 | activerecord_connect( 110 | adapter: "sqlserver", 111 | database: "rodauth_test", 112 | username: "rodauth_test_password", 113 | password: "Rodauth1.", 114 | host: "localhost", 115 | ) 116 | 117 | @db = Sequel.connect "tinytds://", extensions: :activerecord_connection 118 | end 119 | 120 | def setup 121 | @log = StringIO.new 122 | ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| 123 | event = ActiveSupport::Notifications::Event.new(*args) 124 | 125 | original_pos = @log.pos 126 | @log.seek(0, IO::SEEK_END) 127 | @log.puts event.payload[:sql] 128 | @log.pos = original_pos 129 | end 130 | end 131 | 132 | def teardown 133 | ActiveRecord::Base.remove_connection 134 | set_activerecord_timezone(:utc) # reset default setting 135 | Sequel::DATABASES.delete(@db) if defined?(@db) 136 | end 137 | 138 | def set_activerecord_timezone(value) 139 | if ActiveRecord::VERSION::MAJOR >= 7 140 | ActiveRecord.default_timezone = value 141 | else 142 | ActiveRecord::Base.default_timezone = value 143 | end 144 | end 145 | 146 | def assert_logged(content) 147 | if RUBY_ENGINE == "jruby" 148 | transaction = " TRANSACTION" if Gem::Version.new(ArJdbc::VERSION) < Gem::Version.new("61.0") 149 | content.gsub!(/BEGIN\nSET TRANSACTION ISOLATION LEVEL (.+)/) do 150 | "BEGIN ISOLATED#{transaction} - #{$1.downcase.tr(" ", "_")}" 151 | end 152 | content.gsub!(/(BEGIN|COMMIT|ROLLBACK)$/, "\\1#{transaction}") 153 | end 154 | 155 | assert_includes @log.read, content 156 | end 157 | 158 | def activerecord_connect(**options) 159 | ActiveRecord::Base.establish_connection(options) 160 | ActiveRecord::Base.connection_pool.with_connection(&:disable_lazy_transactions!) if ActiveRecord.version >= Gem::Version.new("6.0") 161 | end 162 | 163 | def activerecord_config 164 | if ActiveRecord.version >= Gem::Version.new("6.1") 165 | ActiveRecord::Base.connection_db_config.configuration_hash 166 | else 167 | ActiveRecord::Base.connection_config 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /test/mssql_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "active_support/core_ext/kernel/reporting" 3 | 4 | describe "mssql connection" do 5 | before do 6 | connect_sqlserver 7 | 8 | @db.create_table! :records do 9 | primary_key :id 10 | String :col, unique: true 11 | Time :time 12 | end 13 | end 14 | 15 | it "supports Dataset#insert" do 16 | assert_equal 1, @db[:records].insert(col: "a") 17 | assert_equal 2, @db[:records].insert(col: "b") 18 | end 19 | 20 | it "supports Dataset#each" do 21 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }, { col: "c" }] 22 | 23 | records = @db[:records].order(:id).all 24 | 25 | assert_equal 1, records[0][:id] 26 | assert_equal "a", records[0][:col] 27 | assert_equal 2, records[1][:id] 28 | assert_equal "b", records[1][:col] 29 | assert_equal 3, records[2][:id] 30 | assert_equal "c", records[2][:col] 31 | 32 | assert_logged <<~SQL 33 | BEGIN TRANSACTION 34 | INSERT INTO [RECORDS] ([COL]) VALUES (N'a'), (N'b'), (N'c') 35 | COMMIT TRANSACTION 36 | SELECT * FROM [RECORDS] ORDER BY [ID] 37 | SQL 38 | end 39 | 40 | it "handles empty dataset" do 41 | assert_equal [], @db[:records].all 42 | assert_equal [:id, :col, :time], @db[:records].columns 43 | end 44 | 45 | it "supports Database#update" do 46 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }] 47 | 48 | assert_equal 0, @db[:records].where(col: "c").update(col: "x") 49 | assert_equal 1, @db[:records].where(col: "a").update(col: "y") 50 | assert_equal 2, @db[:records].update(time: Sequel::CURRENT_TIMESTAMP) 51 | 52 | records = @db[:records].order(:id).all 53 | 54 | assert_equal 1, records[0][:id] 55 | assert_equal "y", records[0][:col] 56 | assert_equal 2, records[1][:id] 57 | assert_instance_of Time, records[1][:time] 58 | 59 | assert_logged <<~SQL 60 | UPDATE [RECORDS] SET [COL] = N'x' WHERE ([COL] = N'c') 61 | UPDATE [RECORDS] SET [COL] = N'y' WHERE ([COL] = N'a') 62 | UPDATE [RECORDS] SET [TIME] = CURRENT_TIMESTAMP 63 | SQL 64 | end 65 | 66 | it "supports Database#get" do 67 | assert_instance_of Time, @db.get(Sequel::CURRENT_TIMESTAMP) 68 | assert_equal 1, @db.get(1) 69 | assert_equal "foo", @db.get("foo") 70 | 71 | assert_logged <<~SQL 72 | SELECT TOP (1) CURRENT_TIMESTAMP AS [V] 73 | SELECT TOP (1) 1 AS [V] 74 | SELECT TOP (1) N'foo' AS [V] 75 | SQL 76 | end 77 | 78 | it "supports bound variables" do 79 | record_id = @db[:records].insert(col: "foo") 80 | 81 | record = @db[:records] 82 | .where(col: :$c) 83 | .call(:first, c: "foo") 84 | 85 | assert_equal record_id, record[:id] 86 | 87 | if RUBY_ENGINE == "jruby" 88 | assert_logged <<~SQL 89 | PREPARE SELECT * FROM "records" WHERE ("col" = ?) LIMIT 1 90 | EXECUTE; ["foo"] 91 | SQL 92 | else 93 | assert_logged <<~SQL 94 | EXEC sp_executesql N'SELECT TOP (1) * FROM [RECORDS] WHERE ([COL] = @c)', N'@c nvarchar(max)', @c = N'foo' 95 | SQL 96 | end 97 | end 98 | 99 | it "supports prepared statements" do 100 | record_id = @db[:records].insert(col: "foo") 101 | 102 | record = @db[:records] 103 | .where(col: :$c) 104 | .prepare(:first, :first_by_col) 105 | .call(c: "foo") 106 | 107 | assert_equal record_id, record[:id] 108 | 109 | if RUBY_ENGINE == "jruby" 110 | assert_logged <<~SQL 111 | PREPARE first_by_col: SELECT * FROM "records" WHERE ("col" = ?) LIMIT 1 112 | EXECUTE first_by_col; ["foo"] 113 | SQL 114 | else 115 | assert_logged <<~SQL 116 | EXEC sp_executesql N'SELECT TOP (1) * FROM [RECORDS] WHERE ([COL] = @c)', N'@c nvarchar(max)', @c = N'foo' 117 | SQL 118 | end 119 | end 120 | 121 | it "raises Sequel exceptions" do 122 | assert_raises Sequel::UniqueConstraintViolation do 123 | @db[:records].multi_insert [{ col: "a" }, { col: "a" }] 124 | end 125 | 126 | @db.alter_table(:records) { add_foreign_key :fkey, :records } 127 | 128 | assert_raises Sequel::ForeignKeyConstraintViolation do 129 | @db[:records].insert(fkey: 50) 130 | end 131 | 132 | @db.alter_table(:records) { add_column :required, :text, null: false, default: "default" } 133 | 134 | assert_raises Sequel::NotNullConstraintViolation do 135 | @db[:records].insert(required: nil) 136 | end 137 | end 138 | 139 | it "converts other exceptions" do 140 | assert_raises Sequel::DatabaseError do 141 | @db[:foo].all 142 | end 143 | end 144 | 145 | it "correctly handles ActiveRecord's default UTC timezone setting" do 146 | time = Time.new(2020, 4, 26, 0, 0, 0, "+02:00") 147 | 148 | @db[:records].insert(time: time) 149 | 150 | assert_equal time, @db[:records].first[:time] 151 | 152 | assert_logged <<~SQL 153 | INSERT INTO [RECORDS] ([TIME]) VALUES ('2020-04-25T22:00:00.000') 154 | SQL 155 | end 156 | 157 | it "correctly handles ActiveRecord's local timezone setting" do 158 | set_activerecord_timezone(:local) 159 | 160 | time = Time.new(2020, 4, 26, 0, 0, 0) 161 | 162 | @db[:records].insert(time: time) 163 | 164 | assert_equal time, @db[:records].first[:time] 165 | 166 | assert_logged <<~SQL 167 | INSERT INTO [RECORDS] ([TIME]) VALUES ('2020-04-26T00:00:00.000') 168 | SQL 169 | end 170 | end unless Gem::Specification.find_all_by_name("activerecord-sqlserver-adapter").empty? 171 | -------------------------------------------------------------------------------- /test/mysql2_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe "mysql2 connection" do 4 | before do 5 | connect_mysql2 6 | 7 | @db.create_table! :records do 8 | primary_key :id 9 | String :col 10 | Time :time 11 | end 12 | end 13 | 14 | it "supports Dataset#insert" do 15 | assert_equal 1, @db[:records].insert(col: "a") 16 | assert_equal 2, @db[:records].insert(col: "a") 17 | end 18 | 19 | it "supports Dataset#each" do 20 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }, { col: "c" }] 21 | 22 | records = @db[:records].order(:id).all 23 | 24 | assert_equal 1, records[0][:id] 25 | assert_equal "a", records[0][:col] 26 | assert_equal 2, records[1][:id] 27 | assert_equal "b", records[1][:col] 28 | assert_equal 3, records[2][:id] 29 | assert_equal "c", records[2][:col] 30 | 31 | assert_equal [:id, :col, :time], @db[:records].columns 32 | 33 | assert_logged <<~SQL 34 | INSERT INTO `records` (`col`) VALUES ('a'), ('b'), ('c') 35 | SELECT version() 36 | SELECT * FROM `records` ORDER BY `id` 37 | SQL 38 | end 39 | 40 | it "handles empty dataset" do 41 | assert_equal [], @db[:records].all 42 | assert_equal [:id, :col, :time], @db[:records].columns 43 | end 44 | 45 | it "supports Database#update" do 46 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }] 47 | 48 | assert_equal 0, @db[:records].where(col: "c").update(col: "x") 49 | assert_equal 1, @db[:records].where(col: "a").update(col: "y") 50 | assert_equal 2, @db[:records].update(col: "z") 51 | 52 | records = @db[:records].order(:id).all 53 | 54 | assert_equal 1, records[0][:id] 55 | assert_equal "z", records[0][:col] 56 | assert_equal 2, records[1][:id] 57 | assert_equal "z", records[1][:col] 58 | 59 | assert_logged <<~SQL 60 | UPDATE `records` SET `col` = 'x' WHERE (`col` = 'c') 61 | UPDATE `records` SET `col` = 'y' WHERE (`col` = 'a') 62 | UPDATE `records` SET `col` = 'z' 63 | SQL 64 | end 65 | 66 | it "supports Database#get" do 67 | assert_instance_of Time, @db.get(Sequel::CURRENT_TIMESTAMP) 68 | assert_equal 1, @db.get(1) 69 | assert_equal "foo", @db.get("foo") 70 | 71 | assert_logged <<~SQL 72 | SELECT CURRENT_TIMESTAMP AS `v` LIMIT 1 73 | SELECT 1 AS `v` LIMIT 1 74 | SELECT 'foo' AS `v` LIMIT 1 75 | SQL 76 | end 77 | 78 | it "supports bound variables" do 79 | record_id = @db[:records].insert(col: "foo") 80 | 81 | record = @db[:records] 82 | .where(col: :$c) 83 | .call(:first, c: "foo") 84 | 85 | assert_equal record_id, record[:id] 86 | 87 | if RUBY_ENGINE == "jruby" 88 | assert_logged <<~SQL 89 | PREPARE SELECT * FROM `records` WHERE (`col` = ?) LIMIT 1 90 | EXECUTE; ["foo"] 91 | SQL 92 | else 93 | assert_logged <<~SQL 94 | SELECT * FROM `records` WHERE (`col` = ?) LIMIT 1; ["foo"] 95 | SQL 96 | end 97 | end 98 | 99 | it "supports prepared statements" do 100 | record_id = @db[:records].insert(col: "foo") 101 | 102 | record = @db[:records] 103 | .where(col: :$c) 104 | .prepare(:first, :first_by_col) 105 | .call(c: "foo") 106 | 107 | assert_equal record_id, record[:id] 108 | 109 | if RUBY_ENGINE == "jruby" 110 | assert_logged <<~SQL 111 | PREPARE first_by_col: SELECT * FROM `records` WHERE (`col` = ?) LIMIT 1 112 | EXECUTE first_by_col; ["foo"] 113 | SQL 114 | else 115 | assert_logged <<~SQL 116 | Preparing first_by_col: SELECT * FROM `records` WHERE (`col` = ?) LIMIT 1 117 | Executing first_by_col; ["foo"] 118 | SQL 119 | end 120 | end 121 | 122 | it "raises Sequel exceptions" do 123 | assert_raises Sequel::UniqueConstraintViolation do 124 | @db[:records].multi_insert [{ id: 1 }, { id: 1 }] 125 | end 126 | 127 | @db.alter_table(:records) { add_foreign_key :fkey, :records } 128 | 129 | assert_raises Sequel::ForeignKeyConstraintViolation do 130 | @db[:records].insert(fkey: 50) 131 | end 132 | 133 | @db.alter_table(:records) { add_column :required, :text, null: false } 134 | 135 | assert_raises Sequel::NotNullConstraintViolation do 136 | @db[:records].insert(required: nil) 137 | end 138 | end 139 | 140 | it "correctly handles ActiveRecord's default UTC timezone setting" do 141 | time = Time.new(2020, 4, 26, 0, 0, 0, "+02:00") 142 | 143 | @db[:records].insert(time: time) 144 | 145 | assert_equal time, @db[:records].first[:time] 146 | 147 | assert_logged <<~SQL 148 | INSERT INTO `records` (`time`) VALUES ('2020-04-25 22:00:00') 149 | SQL 150 | end 151 | 152 | it "correctly handles ActiveRecord's local timezone setting" do 153 | set_activerecord_timezone(:local) 154 | 155 | time = Time.new(2020, 4, 26, 0, 0, 0) 156 | 157 | @db[:records].insert(time: time) 158 | 159 | assert_equal time, @db[:records].first[:time] 160 | 161 | assert_logged <<~SQL 162 | INSERT INTO `records` (`time`) VALUES ('2020-04-26 00:00:00') 163 | SQL 164 | end 165 | 166 | it "restores original query options" do 167 | conn = @db.synchronize { |conn| conn } 168 | 169 | @db.get(1) 170 | 171 | assert_equal :array, conn.query_options[:as] 172 | assert_equal false, conn.query_options[:symbolize_keys] 173 | assert_equal true, conn.query_options[:cache_rows] 174 | 175 | assert_raises(Sequel::DatabaseError) { @db.get(Sequel.lit("invalid")) } 176 | 177 | assert_equal :array, conn.query_options[:as] 178 | assert_equal false, conn.query_options[:symbolize_keys] 179 | assert_equal true, conn.query_options[:cache_rows] 180 | 181 | @db.execute("SELECT 1") { @db.execute("SELECT 1") } 182 | 183 | assert_equal :array, conn.query_options[:as] 184 | assert_equal false, conn.query_options[:symbolize_keys] 185 | assert_equal true, conn.query_options[:cache_rows] 186 | end unless RUBY_ENGINE == "jruby" 187 | 188 | it "allows calling Active Record queries inside transaction" do 189 | activerecord_model = Class.new(ActiveRecord::Base) 190 | activerecord_model.table_name = :records 191 | 192 | @db.transaction do 193 | record = activerecord_model.create(col: "foo", time: Time.new(2021, 1, 10)) 194 | record = activerecord_model.find(record.id) 195 | 196 | assert_equal "foo", record.col 197 | assert_equal Time.new(2021, 1, 10), record.time 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /test/sqlite_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe "sqlite3 connection" do 4 | before do 5 | connect_sqlite3 6 | 7 | @db.create_table! :records do 8 | primary_key :id 9 | String :col 10 | Time :time 11 | end 12 | end 13 | 14 | it "supports Dataset#insert" do 15 | assert_equal 1, @db[:records].insert(col: "a") 16 | assert_equal 2, @db[:records].insert(col: "a") 17 | end 18 | 19 | it "supports Dataset#each" do 20 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }, { col: "c" }] 21 | 22 | records = @db[:records].order(:id).all 23 | 24 | assert_equal 1, records[0][:id] 25 | assert_equal "a", records[0][:col] 26 | assert_equal 2, records[1][:id] 27 | assert_equal "b", records[1][:col] 28 | assert_equal 3, records[2][:id] 29 | assert_equal "c", records[2][:col] 30 | 31 | assert_equal [:id, :col, :time], @db[:records].columns 32 | 33 | assert_logged <<~SQL 34 | INSERT INTO `records` (`col`) VALUES ('a'), ('b'), ('c') 35 | SELECT * FROM `records` ORDER BY `id` 36 | SQL 37 | end 38 | 39 | it "handles empty dataset" do 40 | assert_equal [], @db[:records].all 41 | assert_equal [:id, :col, :time], @db[:records].columns 42 | end 43 | 44 | it "supports Database#update" do 45 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }] 46 | 47 | assert_equal 0, @db[:records].where(col: "c").update(col: "x") 48 | assert_equal 1, @db[:records].where(col: "a").update(col: "y") 49 | assert_equal 2, @db[:records].update(col: "z") 50 | 51 | records = @db[:records].order(:id).all 52 | 53 | assert_equal 1, records[0][:id] 54 | assert_equal "z", records[0][:col] 55 | assert_equal 2, records[1][:id] 56 | assert_equal "z", records[1][:col] 57 | 58 | assert_logged <<~SQL 59 | UPDATE `records` SET `col` = 'x' WHERE (`col` = 'c') 60 | UPDATE `records` SET `col` = 'y' WHERE (`col` = 'a') 61 | UPDATE `records` SET `col` = 'z' 62 | SQL 63 | end 64 | 65 | it "supports Database#get" do 66 | assert_equal 1, @db.get(1) 67 | assert_equal "foo", @db.get("foo") 68 | 69 | assert_logged <<~SQL 70 | SELECT 1 AS 'v' LIMIT 1 71 | SELECT 'foo' AS 'v' LIMIT 1 72 | SQL 73 | end 74 | 75 | it "supports bound variables" do 76 | record_id = @db[:records].insert(col: "foo") 77 | 78 | record = @db[:records] 79 | .where(col: :$c) 80 | .call(:first, c: "foo") 81 | 82 | assert_equal record_id, record[:id] 83 | 84 | if RUBY_ENGINE == "jruby" 85 | assert_logged <<~SQL 86 | PREPARE SELECT * FROM `records` WHERE (`col` = ?) LIMIT 1 87 | EXECUTE; ["foo"] 88 | SQL 89 | else 90 | assert_logged <<~SQL 91 | SELECT * FROM `records` WHERE (`col` = :c) LIMIT 1; #{{"c" => "foo"}} 92 | SQL 93 | end 94 | end 95 | 96 | it "supports prepared statements" do 97 | record_id = @db[:records].insert(col: "foo") 98 | 99 | record = @db[:records] 100 | .where(col: :$c) 101 | .prepare(:first, :first_by_col) 102 | .call(c: "foo") 103 | 104 | assert_equal record_id, record[:id] 105 | 106 | if RUBY_ENGINE == "jruby" 107 | assert_logged <<~SQL 108 | PREPARE first_by_col: SELECT * FROM `records` WHERE (`col` = ?) LIMIT 1 109 | EXECUTE first_by_col; ["foo"] 110 | SQL 111 | else 112 | assert_logged <<~SQL 113 | PREPARE first_by_col: SELECT * FROM `records` WHERE (`col` = :c) LIMIT 1 114 | EXECUTE first_by_col; #{{"c" => "foo"}} 115 | SQL 116 | end 117 | end 118 | 119 | it "raises Sequel exceptions" do 120 | assert_raises Sequel::UniqueConstraintViolation do 121 | @db[:records].multi_insert [{ id: 1 }, { id: 1 }] 122 | end 123 | 124 | @db.alter_table(:records) { add_foreign_key :fkey, :records } 125 | 126 | assert_raises Sequel::ForeignKeyConstraintViolation do 127 | @db[:records].insert(fkey: 50) 128 | end 129 | 130 | @db.alter_table(:records) { add_column :required, :text, null: false, default: "default" } 131 | 132 | assert_raises Sequel::NotNullConstraintViolation do 133 | @db[:records].insert(required: nil) 134 | end 135 | end 136 | 137 | it "correctly handles ActiveRecord's default UTC timezone setting" do 138 | time = Time.new(2020, 4, 26, 0, 0, 0, "+02:00") 139 | 140 | @db[:records].insert(time: time) 141 | 142 | assert_equal time, @db[:records].first[:time] 143 | 144 | assert_logged <<~SQL 145 | INSERT INTO `records` (`time`) VALUES ('2020-04-25 22:00:00.000000') 146 | SQL 147 | end 148 | 149 | it "adds CURRENT_* timestamp in UTC when that's ActiveRecord's timezone" do 150 | @db.extension :date_arithmetic 151 | @db[:records].insert(time: Time.now) 152 | 153 | refute_empty @db[:records].where(Sequel[:time] < Sequel.date_add(Sequel::CURRENT_TIMESTAMP, minutes: 1)) 154 | refute_empty @db[:records].where(Sequel[:time] > Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, minutes: 1)) 155 | end 156 | 157 | it "correctly handles ActiveRecord's local timezone setting" do 158 | set_activerecord_timezone(:local) 159 | 160 | time = Time.new(2020, 4, 26, 0, 0, 0) 161 | 162 | @db[:records].insert(time: time) 163 | 164 | inserted_time = @db[:records].first[:time] 165 | # locally on jdbc/sqlite the timestamp gets returned as a String 166 | inserted_time = @db.to_application_timestamp(inserted_time) if inserted_time.is_a?(String) 167 | 168 | assert_equal time, inserted_time 169 | 170 | assert_logged <<~SQL 171 | INSERT INTO `records` (`time`) VALUES ('2020-04-26 00:00:00.000000') 172 | SQL 173 | end 174 | 175 | it "allows calling Active Record queries inside transaction" do 176 | activerecord_model = Class.new(ActiveRecord::Base) 177 | activerecord_model.table_name = :records 178 | 179 | @db.transaction do 180 | record = activerecord_model.create(col: "foo", time: Time.new(2021, 1, 10)) 181 | record = activerecord_model.find(record.id) 182 | 183 | assert_equal "foo", record.col 184 | assert_equal Time.new(2021, 1, 10), record.time 185 | end 186 | end 187 | 188 | it "clears Active Records query cache" do 189 | ActiveRecord::Base.connection_pool.with_connection(&:enable_query_cache!) 190 | 191 | activerecord_model = Class.new(ActiveRecord::Base) 192 | activerecord_model.table_name = :records 193 | 194 | assert_nil activerecord_model.find_by(col: "foo") 195 | @db[:records].insert(col: "foo") 196 | refute_nil activerecord_model.find_by(col: "foo") 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.1 (2025-10-25) 2 | 3 | * Allow Active Record 8.1+ (@janko) 4 | 5 | ## 2.0.0 (2024-11-10) 6 | 7 | * The `after_commit_everywhere` gem now needs to be added to the Gemfile manually on Active Record < 7.2 (@janko) 8 | 9 | ## 1.5.1 (2024-11-08) 10 | 11 | * Add support for Active Record 8.0 (@phlipper) 12 | 13 | ## 1.5.0 (2024-10-16) 14 | 15 | * Avoid permanent connection checkout on Active Record 7.2+ (@janko) 16 | 17 | ## 1.4.3 (2024-09-26) 18 | 19 | * Fix compatibility with adapters that don't support savepoints (@janko) 20 | 21 | ## 1.4.2 (2024-09-23) 22 | 23 | * Fix compatibility with newer versions of Oracle Enhanced adapter (@janko) 24 | 25 | * Drop support for Ruby 2.4 (@janko) 26 | 27 | ## 1.4.1 (2024-05-10) 28 | 29 | * Fix `#rollback_checker`, `#rollback_on_exit` and `#after_rollback` not working reliably on JRuby and Sequel 5.78+ (@janko) 30 | 31 | * Use native transaction callbacks on Active Record 7.2+ (@janko) 32 | 33 | ## 1.4.0 (2024-03-19) 34 | 35 | * Only warn when Sequel extension fails to initialize because there is no database (@janko) 36 | 37 | * Drop support for Active Record 4.2 (@janko) 38 | 39 | ## 1.3.1 (2023-04-22) 40 | 41 | * Fix Active Record's query cache not being cleared in SQLite adapter (@janko) 42 | 43 | ## 1.3.0 (2023-04-22) 44 | 45 | * Clear Active Record query cache after Sequel executes SQL statements (@janko) 46 | 47 | ## 1.2.11 (2023-01-09) 48 | 49 | * Raise explicit exception in case of mismatch between Active Record and Sequel adapter (@janko) 50 | 51 | ## 1.2.10 (2022-12-13) 52 | 53 | * Fix incorrect PG type mapping when using prepared statements in Sequel (@janko) 54 | 55 | ## 1.2.9 (2022-03-15) 56 | 57 | * Remove `sequel_pg` and `pg` runtime dependencies introduced in the previous version (@janko) 58 | 59 | ## 1.2.8 (2022-02-28) 60 | 61 | * Support the pg_streaming database extension from the sequel_pg gem (@janko) 62 | 63 | ## 1.2.7 (2022-01-20) 64 | 65 | * Require Sequel 5.38+ (@janko) 66 | 67 | ## 1.2.6 (2021-12-26) 68 | 69 | * Speed up connection access by avoiding checking Active Record version at runtime (@janko) 70 | 71 | ## 1.2.5 (2021-12-19) 72 | 73 | * Loosen Active Record dependency to allow any 7.x version (@janko) 74 | 75 | * Drop support for Ruby 2.3 (@janko) 76 | 77 | * Allow using the `sql_log_normalizer` Sequel database extension (@janko) 78 | 79 | ## 1.2.4 (2021-09-27) 80 | 81 | * Allow using with Active Record 7.0 (@janko) 82 | 83 | * Use `ActiveRecord.default_timezone` on Active Record 7.0 or greater (@janko) 84 | 85 | ## 1.2.3 (2021-07-17) 86 | 87 | * Bump `after_commit_everywhere` dependency to `~> 1.0` (@wivarn) 88 | 89 | ## 1.2.2 (2021-01-11) 90 | 91 | * Ensure Active Record queries inside a Sequel transaction are typemapped correctly in postgres adapter (@janko) 92 | 93 | * Fix executing Active Record queries inside a Sequel transaction not working in mysql2 adapter (@janko) 94 | 95 | ## 1.2.1 (2021-01-10) 96 | 97 | * Fix original mysql2 query options not being restored after nested `DB#synchronize` calls, e.g. when using Sequel transactions (@janko) 98 | 99 | ## 1.2.0 (2020-11-15) 100 | 101 | * Attempt support for [activerecord-sqlserver-adapter](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter) (@janko) 102 | 103 | * Attempt support for [oracle-enhanced](https://github.com/rsim/oracle-enhanced) Active Record adapter (@janko) 104 | 105 | ## 1.1.0 (2020-11-08) 106 | 107 | * Drop support for Ruby 2.2 (@janko) 108 | 109 | * Support transaction/savepoint hooks even when Active Record holds the transaction/savepoint (@janko) 110 | 111 | * Don't test the connection on `Sequel.connect` by default (@janko) 112 | 113 | ## 1.0.1 (2020-10-28) 114 | 115 | * Use Active Record connection lock in `Database#synchronize` (@janko) 116 | 117 | ## 1.0.0 (2020-10-25) 118 | 119 | * Clear AR statement cache on `ActiveRecord::PreparedStatementCacheExpired` when Sequel holds the transaction (@janko) 120 | 121 | * Pick up `ActiveRecord::Base.default_timezone` being changed on runtime (@janko) 122 | 123 | * Support prepared statements and bound variables in all adapters (@janko) 124 | 125 | * Correctly identify identity columns as primary keys in Postgres adapter (@janko) 126 | 127 | * Avoid using deprecated `sqlite3` API in SQLite adapter (@janko) 128 | 129 | * Allow using any external Active Record adapters (@janko) 130 | 131 | * Avoid potential bugs when converting Active Record exceptions into Sequel exceptions (@janko) 132 | 133 | * Don't use Active Record locks when executing queries with Sequel (@janko) 134 | 135 | * Support `Database#valid_connection?` in Postgres adapter (@janko) 136 | 137 | * Fully utilize Sequel's logic for detecting disconnects in Postgres adapter (@janko) 138 | 139 | * Support `Database#{copy_table,copy_into,listen}` in Postgres adapter (@janko) 140 | 141 | * Log all queries executed by Sequel (@janko) 142 | 143 | * Log executed queries to Sequel logger(s) as well (@janko) 144 | 145 | * Specially label queries executed by Sequel in Active Record logs (@janko) 146 | 147 | ## 0.4.1 (2020-09-28) 148 | 149 | * Require Sequel version 5.16.0 or above (@janko) 150 | 151 | ## 0.4.0 (2020-09-28) 152 | 153 | * Return correct result of `Database#in_transaction?` after ActiveRecord transaction exited (@janko) 154 | 155 | * Make ActiveRecord create a savepoint inside a Sequel transaction with `auto_savepoint: true` (@janko) 156 | 157 | * Make Sequel create a savepoint inside ActiveRecord transaction with `joinable: false` (@janko) 158 | 159 | * Improve reliability of nested transactions when combining Sequel and ActiveRecord (@janko) 160 | 161 | * Raise error when attempting to add an `after_commit`/`after_rollback` hook on ActiveRecord transaction (@janko) 162 | 163 | * Fix infinite loop that could happen with transactional Rails tests (@janko) 164 | 165 | ## 0.3.0 (2020-07-24) 166 | 167 | * Fully support Sequel transaction API (all transaction options, transaction/savepoint hooks etc.) (@janko) 168 | 169 | ## 0.2.6 (2020-07-19) 170 | 171 | * Return block result in `Sequel::Database#transaction` (@zabolotnov87, @janko) 172 | 173 | * Fix `Sequel::Model#save_changes` or `#save` with additional options not executing (@zabolotnov87, @janko) 174 | 175 | ## 0.2.5 (2020-06-04) 176 | 177 | * Use `#current_timestamp_utc` for the JDBC SQLite adapter as well (@HoneyryderChuck) 178 | 179 | ## 0.2.4 (2020-06-03) 180 | 181 | * Add JRuby support for ActiveRecord 6.0 and 5.2 (@HoneyryderChuck) 182 | 183 | * Use `#current_timestamp_utc` setting for SQLite adapter on Sequel >= 5.33 (@HoneyryderChuck) 184 | 185 | ## 0.2.3 (2020-05-25) 186 | 187 | * Fix Ruby 2.7 kwargs warnings in `#transaction` (@HoneyryderChuck) 188 | 189 | ## 0.2.2 (2020-05-02) 190 | 191 | * Add support for ActiveRecord 4.2 (@janko) 192 | 193 | ## 0.2.1 (2020-05-02) 194 | 195 | * Add support for Active Record 5.0, 5.1 and 5.2 (@janko) 196 | 197 | * Allow Sequel 4.x (@janko) 198 | 199 | ## 0.2.0 (2020-04-29) 200 | 201 | * Rename to `sequel-activerecord_connection` and make it a Sequel extension (@janko) 202 | -------------------------------------------------------------------------------- /lib/sequel/extensions/activerecord_connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Sequel 4 | module ActiveRecordConnection 5 | Error = Class.new(Sequel::Error) 6 | 7 | TRANSACTION_ISOLATION_MAP = { 8 | uncommitted: :read_uncommitted, 9 | committed: :read_committed, 10 | repeatable: :repeatable_read, 11 | serializable: :serializable, 12 | } 13 | 14 | def self.extended(db) 15 | db.activerecord_model = ActiveRecord::Base 16 | db.opts[:test] = false unless db.opts.key?(:test) 17 | db.instance_variable_set(:@transactions, {}) if RUBY_ENGINE == "jruby" 18 | 19 | begin 20 | require "sequel/extensions/activerecord_connection/#{db.adapter_scheme}" 21 | db.extend Sequel::ActiveRecordConnection.const_get(db.adapter_scheme.capitalize) 22 | rescue LoadError 23 | # assume the Sequel adapter already works with Active Record 24 | end 25 | end 26 | 27 | attr_accessor :activerecord_model 28 | 29 | # Ensure Sequel is not creating its own connection anywhere. 30 | def connect(*) 31 | raise Error, "creating a Sequel connection is not allowed" 32 | end 33 | 34 | def extension(*) 35 | super 36 | rescue ActiveRecord::NoDatabaseError 37 | warn "Sequel database extension #{@loaded_extensions.last.inspect} failed to initialize because there is no database." 38 | end 39 | 40 | # Avoid calling Sequel's connection pool, instead use Active Record's. 41 | def synchronize(*) 42 | activerecord_synchronize do 43 | conn = activerecord_connection.raw_connection 44 | 45 | if activerecord_connection_class && !conn.is_a?(activerecord_connection_class) 46 | fail Error, "expected Active Record connection to be a #{activerecord_connection_class}, got #{conn.class}" 47 | end 48 | 49 | yield conn 50 | ensure 51 | clear_activerecord_query_cache 52 | end 53 | end 54 | 55 | # Log executed queries into Active Record logger as well. 56 | def log_connection_yield(sql, conn, args = nil) 57 | sql += "; #{args.inspect}" if args 58 | 59 | activerecord_log(sql) { super } 60 | end 61 | 62 | # Match database timezone with Active Record. 63 | def timezone 64 | @timezone || activerecord_timezone 65 | end 66 | 67 | private 68 | 69 | # Synchronizes transaction state with ActiveRecord. Sequel uses this 70 | # information to know whether we're in a transaction, whether to create a 71 | # savepoint, when to run transaction/savepoint hooks etc. 72 | def _trans(conn) 73 | hash = super || { activerecord: true } 74 | 75 | # adapters that don't support savepoints won't have this assigned 76 | hash[:savepoints] ||= [] 77 | 78 | # add any ActiveRecord transactions/savepoints that have been opened 79 | # directly via ActiveRecord::Base.transaction 80 | while hash[:savepoints].length < activerecord_connection.open_transactions 81 | hash[:savepoints] << { activerecord: true } 82 | end 83 | 84 | # remove any ActiveRecord transactions/savepoints that have been closed 85 | # directly via ActiveRecord::Base.transaction 86 | while hash[:savepoints].length > activerecord_connection.open_transactions && hash[:savepoints].last[:activerecord] 87 | hash[:savepoints].pop 88 | end 89 | 90 | # sync knowledge about joinability of current ActiveRecord transaction/savepoint 91 | if activerecord_connection.transaction_open? && !activerecord_connection.current_transaction.joinable? 92 | hash[:savepoints].last[:auto_savepoint] = true 93 | end 94 | 95 | if hash[:savepoints].empty? && hash[:activerecord] 96 | Sequel.synchronize { @transactions.delete(conn) } 97 | else 98 | Sequel.synchronize { @transactions[conn] = hash } 99 | end 100 | 101 | super 102 | end 103 | 104 | def begin_transaction(conn, opts = OPTS) 105 | isolation = TRANSACTION_ISOLATION_MAP.fetch(opts[:isolation]) if opts[:isolation] 106 | joinable = !opts[:auto_savepoint] 107 | 108 | activerecord_connection.begin_transaction(isolation: isolation, joinable: joinable) 109 | end 110 | 111 | def commit_transaction(conn, opts = OPTS) 112 | activerecord_connection.commit_transaction 113 | end 114 | 115 | def rollback_transaction(conn, opts = OPTS) 116 | activerecord_connection.rollback_transaction 117 | end 118 | 119 | # When Active Record holds the transaction, we cannot use Sequel hooks, 120 | # because Sequel doesn't have knowledge of when the transaction is 121 | # committed. So in this case we register the hook using Active Record. 122 | def add_transaction_hook(conn, type, block) 123 | if _trans(conn)[:activerecord] 124 | activerecord_transaction_callback(type, &block) 125 | else 126 | super 127 | end 128 | end 129 | 130 | # When Active Record holds the savepoint, we cannot use Sequel hooks, 131 | # because Sequel doesn't have knowledge of when the savepoint is 132 | # released. So in this case we register the hook using Active Record. 133 | def add_savepoint_hook(conn, type, block) 134 | if _trans(conn)[:savepoints].last[:activerecord] 135 | activerecord_transaction_callback(type, &block) 136 | else 137 | super 138 | end 139 | end 140 | 141 | if ActiveRecord.version >= Gem::Version.new("7.2") 142 | def activerecord_transaction_callback(type, &block) 143 | activerecord_connection.current_transaction.public_send(type, &block) 144 | end 145 | else 146 | begin 147 | gem "after_commit_everywhere", "~> 1.1" 148 | require "after_commit_everywhere" 149 | rescue LoadError 150 | fail Error, %q(You need to add `gem "after_commit_everywhere", "~> 1.1"` to your Gemfile when using Active Record < 7.2) 151 | end 152 | 153 | def activerecord_transaction_callback(type, &block) 154 | AfterCommitEverywhere.public_send(type, &block) 155 | end 156 | end 157 | 158 | # Prevents sql_log_normalizer DB extension from skipping the normalization. 159 | def skip_logging? 160 | return false if @loaded_extensions.include?(:sql_log_normalizer) 161 | super 162 | end 163 | 164 | def activerecord_synchronize 165 | with_activerecord_connection do 166 | activerecord_lock do 167 | yield 168 | end 169 | end 170 | end 171 | 172 | if ActiveRecord.version >= Gem::Version.new("7.0") 173 | def clear_activerecord_query_cache 174 | activerecord_model.clear_query_caches_for_current_thread 175 | end 176 | else 177 | def clear_activerecord_query_cache 178 | activerecord_connection.clear_query_cache 179 | end 180 | end 181 | 182 | if ActiveRecord.version >= Gem::Version.new("7.2") 183 | def with_activerecord_connection 184 | activerecord_model.with_connection(prevent_permanent_checkout: true) do 185 | yield activerecord_connection 186 | end 187 | end 188 | else 189 | def with_activerecord_connection 190 | yield activerecord_connection 191 | end 192 | end 193 | 194 | # Active Record doesn't guarantee that a single connection can only be used 195 | # by one thread at a time, so we need to use locking, which is what Active 196 | # Record does internally as well. 197 | def activerecord_lock 198 | activerecord_connection.lock.synchronize do 199 | ActiveSupport::Dependencies.interlock.permit_concurrent_loads do 200 | yield 201 | end 202 | end 203 | end 204 | 205 | def activerecord_connection 206 | activerecord_model.connection 207 | end 208 | 209 | def activerecord_connection_class 210 | # defines in adapter modules 211 | end 212 | 213 | def activerecord_log(sql, &block) 214 | ActiveSupport::Notifications.instrument( 215 | "sql.active_record", 216 | sql: sql, 217 | name: "Sequel", 218 | connection: activerecord_connection, 219 | &block 220 | ) 221 | end 222 | 223 | if ActiveRecord.version >= Gem::Version.new("7.0") 224 | def activerecord_timezone 225 | ActiveRecord.default_timezone 226 | end 227 | else 228 | def activerecord_timezone 229 | ActiveRecord::Base.default_timezone 230 | end 231 | end 232 | end 233 | 234 | Database.register_extension(:activerecord_connection, ActiveRecordConnection) 235 | end 236 | -------------------------------------------------------------------------------- /test/postgres_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | describe "postgres connection" do 4 | before do 5 | connect_postgresql 6 | 7 | @db.create_table! :records do 8 | primary_key :id 9 | String :col 10 | Time :time 11 | end 12 | end 13 | 14 | it "supports Dataset#insert" do 15 | assert_equal 1, @db[:records].insert(col: "a") 16 | assert_equal 2, @db[:records].insert(col: "a") 17 | end 18 | 19 | it "supports Dataset#each" do 20 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }, { col: "c" }] 21 | 22 | records = @db[:records].order(:id).all 23 | 24 | assert_equal 1, records[0][:id] 25 | assert_equal "a", records[0][:col] 26 | assert_equal 2, records[1][:id] 27 | assert_equal "b", records[1][:col] 28 | assert_equal 3, records[2][:id] 29 | assert_equal "c", records[2][:col] 30 | 31 | assert_logged <<~SQL 32 | INSERT INTO "records" ("col") VALUES ('a'), ('b'), ('c') 33 | SELECT * FROM "records" ORDER BY "id" 34 | SQL 35 | end 36 | 37 | it "handles empty dataset" do 38 | assert_equal [], @db[:records].all 39 | assert_equal [:id, :col, :time], @db[:records].columns 40 | end 41 | 42 | it "supports Database#update" do 43 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }] 44 | 45 | assert_equal 0, @db[:records].where(col: "c").update(col: "x") 46 | assert_equal 1, @db[:records].where(col: "a").update(col: "y") 47 | assert_equal 2, @db[:records].update(col: "z") 48 | 49 | records = @db[:records].order(:id).all 50 | 51 | assert_equal 1, records[0][:id] 52 | assert_equal "z", records[0][:col] 53 | assert_equal 2, records[1][:id] 54 | assert_equal "z", records[1][:col] 55 | 56 | assert_logged <<~SQL 57 | UPDATE "records" SET "col" = 'x' WHERE ("col" = 'c') 58 | UPDATE "records" SET "col" = 'y' WHERE ("col" = 'a') 59 | UPDATE "records" SET "col" = 'z' 60 | SQL 61 | end 62 | 63 | it "supports Database#get" do 64 | assert_instance_of Time, @db.get(Sequel::CURRENT_TIMESTAMP) 65 | assert_equal 1, @db.get(1) 66 | assert_equal "foo", @db.get("foo") 67 | 68 | assert_logged <<~SQL 69 | SELECT CURRENT_TIMESTAMP AS "v" LIMIT 1 70 | SELECT 1 AS "v" LIMIT 1 71 | SELECT 'foo' AS "v" LIMIT 1 72 | SQL 73 | end 74 | 75 | it "supports bound variables" do 76 | record_id = @db[:records].insert(col: "foo") 77 | 78 | record = @db[:records] 79 | .where(col: :$c) 80 | .call(:first, c: "foo") 81 | 82 | assert_equal record_id, record[:id] 83 | 84 | if RUBY_ENGINE == "jruby" 85 | assert_logged <<~SQL 86 | PREPARE SELECT * FROM "records" WHERE ("col" = ?) LIMIT 1 87 | EXECUTE; ["foo"] 88 | SQL 89 | else 90 | assert_logged <<~SQL 91 | SELECT * FROM "records" WHERE ("col" = $1) LIMIT 1; ["foo"] 92 | SQL 93 | end 94 | end 95 | 96 | it "supports prepared statements" do 97 | record_id = @db[:records].insert(col: "foo") 98 | 99 | record = @db[:records] 100 | .where(col: :$c) 101 | .prepare(:first, :first_by_col) 102 | .call(c: "foo") 103 | 104 | assert_equal record_id, record[:id] 105 | 106 | if RUBY_ENGINE == "jruby" 107 | assert_logged <<~SQL 108 | PREPARE first_by_col: SELECT * FROM "records" WHERE ("col" = ?) LIMIT 1 109 | EXECUTE first_by_col; ["foo"] 110 | SQL 111 | else 112 | assert_logged <<~SQL 113 | PREPARE first_by_col AS SELECT * FROM "records" WHERE ("col" = $1) LIMIT 1 114 | EXECUTE first_by_col; ["foo"] 115 | SQL 116 | end 117 | end 118 | 119 | it "raises Sequel exceptions" do 120 | assert_raises Sequel::UniqueConstraintViolation do 121 | @db[:records].multi_insert [{ id: 1 }, { id: 1 }] 122 | end 123 | 124 | @db.alter_table(:records) { add_foreign_key :fkey, :records } 125 | 126 | assert_raises Sequel::ForeignKeyConstraintViolation do 127 | @db[:records].insert(fkey: 50) 128 | end 129 | 130 | @db.alter_table(:records) { add_column :required, :text, null: false, default: "default" } 131 | 132 | assert_raises Sequel::NotNullConstraintViolation do 133 | @db[:records].insert(required: nil) 134 | end 135 | end 136 | 137 | it "converts other exceptions" do 138 | assert_raises Sequel::DatabaseError do 139 | @db[:foo].all 140 | end 141 | end 142 | 143 | it "correctly handles ActiveRecord's default UTC timezone setting" do 144 | time = Time.new(2020, 4, 26, 0, 0, 0, "+02:00") 145 | 146 | @db[:records].insert(time: time) 147 | 148 | assert_equal time, @db[:records].first[:time] 149 | 150 | assert_logged <<~SQL 151 | INSERT INTO "records" ("time") VALUES ('2020-04-25 22:00:00.000000+0000') RETURNING "id" 152 | SQL 153 | end 154 | 155 | it "correctly handles ActiveRecord's local timezone setting" do 156 | set_activerecord_timezone(:local) 157 | 158 | time = Time.new(2020, 4, 26, 0, 0, 0) 159 | utc_offset = time.to_s[/\S+$/] 160 | 161 | @db[:records].insert(time: time) 162 | 163 | assert_equal time, @db[:records].first[:time] 164 | 165 | assert_logged <<~SQL 166 | INSERT INTO "records" ("time") VALUES ('2020-04-26 00:00:00.000000#{utc_offset}') RETURNING "id" 167 | SQL 168 | end 169 | 170 | next if RUBY_ENGINE == "jruby" 171 | 172 | it "raises exception on unsupported transaction options" do 173 | assert_raises(Sequel::ActiveRecordConnection::Error) do 174 | @db.transaction(deferrable: true) { } 175 | end 176 | assert_raises(Sequel::ActiveRecordConnection::Error) do 177 | @db.transaction(read_only: true) { } 178 | end 179 | assert_raises(Sequel::ActiveRecordConnection::Error) do 180 | @db.transaction(synchronous: true) { } 181 | end 182 | end 183 | 184 | it "supports #copy_table and #copy_into" do 185 | @db.copy_into(:records, format: "csv", data: ["1,foo,2021-01-10 T00:00:00"]) 186 | 187 | assert_logged <<~SQL 188 | COPY "records" FROM STDIN (FORMAT csv) 189 | SQL 190 | 191 | assert_equal Hash[id: 1, col: "foo", time: Time.utc(2021, 1, 10)], @db[:records].first 192 | 193 | assert_equal "1,foo,2021-01-10 00:00:00", @db.copy_table(:records, format: "csv").chomp 194 | 195 | assert_logged <<~SQL 196 | COPY "records" TO STDOUT (FORMAT csv) 197 | SQL 198 | end 199 | 200 | it "converts disconnects into Sequel::DatabaseDisconnectError" do 201 | @db.synchronize do |conn| 202 | @db.disconnect_connection(conn) 203 | assert_raises Sequel::DatabaseDisconnectError do 204 | @db.copy_table(@db[:records]) 205 | end 206 | end 207 | end 208 | 209 | it "clears Active Record statement cache on ActiveRecord::PreparedStatementCacheExpired" do 210 | statement_cache = ActiveRecord::Base.connection_pool.with_connection { |connection| connection.instance_variable_get(:@statements) } 211 | 212 | model = Class.new(ActiveRecord::Base) 213 | model.table_name = :records 214 | model.find(@db[:records].insert({ col: "foo" })) 215 | 216 | assert_equal 1, statement_cache.length 217 | 218 | assert_raises ActiveRecord::PreparedStatementCacheExpired do 219 | @db.transaction { raise ActiveRecord::PreparedStatementCacheExpired } 220 | end 221 | 222 | assert_equal 0, statement_cache.length 223 | end if defined?(ActiveRecord::PreparedStatementCacheExpired) 224 | 225 | it "supports pg_streaming database extension from sequel_pg" do 226 | @db.extension :pg_streaming 227 | @db[:records].multi_insert [{ col: "a" }, { col: "b" }] 228 | records = @db[:records].order(:col).stream.enum_for(:each).to_a 229 | assert_equal ["a", "b"], records.map { |r| r[:col] } 230 | end 231 | 232 | it "supports pg_auto_parameterize extension" do 233 | @db.extension :pg_auto_parameterize 234 | @db[:records].where(col: "a").to_a 235 | assert_logged <<~SQL 236 | SELECT * FROM "records" WHERE ("col" = $1); ["a"] 237 | SQL 238 | end 239 | 240 | it "reverts type maps inside a Sequel transaction" do 241 | @db.transaction do |conn| 242 | rows = ActiveRecord::Base.connection_pool.with_connection { |connection| connection.exec_query("SELECT TRUE") } 243 | 244 | assert_equal true, rows[0].values.first 245 | end 246 | end 247 | 248 | it "patches type maps for normal statements" do 249 | assert_equal true, @db.schema(:records)[0][1][:primary_key] 250 | end 251 | 252 | it "patches type maps for prepared statements" do 253 | rows = @db["SELECT TRUE"].prepare(:select, :select_true).call 254 | assert_equal true, rows[0].values.first 255 | end 256 | 257 | it "clears Active Records query cache" do 258 | ActiveRecord::Base.connection_pool.with_connection(&:enable_query_cache!) 259 | 260 | activerecord_model = Class.new(ActiveRecord::Base) 261 | activerecord_model.table_name = :records 262 | 263 | assert_nil activerecord_model.find_by(col: "foo") 264 | @db[:records].disable_insert_returning.insert(col: "foo") 265 | refute_nil activerecord_model.find_by(col: "foo") 266 | 267 | assert_nil activerecord_model.find_by(col: "bar") 268 | @db[:records].returning(:id).insert(col: "bar") 269 | refute_nil activerecord_model.find_by(col: "bar") 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sequel-activerecord_connection 2 | 3 | This is a database extension for [Sequel] that makes it to reuse an existing 4 | Active Record connection for database interaction. 5 | 6 | This can be useful if you want to use a library that uses Sequel (e.g. 7 | [Rodauth] or [rom-sql]), or you're transitioning from Active Record to Sequel, 8 | or if you just want to use Sequel for more complex queries, and you want to 9 | avoid creating new database connections. 10 | 11 | It fully supports PostgreSQL, MySQL and SQLite adapters, both the native ones 12 | and JDBC (JRuby). The [SQL Server] external adapter is supported as well 13 | (`tinytds` in Sequel), and there is attempted support for [Oracle enhanced] 14 | (`oracle` and in Sequel). Other adapters might work too, but their integration 15 | hasn't been tested. 16 | 17 | ## Why reuse the database connection? 18 | 19 | At first it might appear that, as long as you're fine with the performance 20 | impact of your database server having to maintain additional open connections, 21 | it would be fine if Sequel had its own database connection. However, there are 22 | additional caveats when you try to combine it with Active Record. 23 | 24 | If Sequel and Active Record each have their own connections, then it's not 25 | possible to combine their transactions. If we executed a Sequel query inside of 26 | an Active Record transaction, that query won't actually be executed inside a 27 | database transaction. This is because transactions are tied to the database 28 | connection; if one connection opens a transaction, this doesn't affect queries 29 | executed on a different connection, even if both connections are used in the 30 | same ruby process. 31 | 32 | With this library, transactions and queries can be seamlessly combined between 33 | Active Record and Sequel. 34 | 35 | ## Installation 36 | 37 | Add the gem to your project: 38 | 39 | ```sh 40 | $ bundle add sequel-activerecord_connection 41 | ``` 42 | 43 | If you're using Active Record 7.1 or older, you'll also need to add the [after_commit_everywhere] gem: 44 | 45 | ```sh 46 | $ bundle add after_commit_everywhere # on Active Record 7.1 or older 47 | ``` 48 | 49 | ## Usage 50 | 51 | Assuming you've configured your ActiveRecord connection, you can initialize the 52 | appropriate Sequel adapter and load the `activerecord_connection` extension: e.g. 53 | 54 | ```rb 55 | # Place in relevant initializer 56 | # e.g. Rails: config/initializers/sequel.rb 57 | 58 | require "sequel" 59 | DB = Sequel.postgres(extensions: :activerecord_connection) # for PostgreSQL 60 | ``` 61 | 62 | Now any Sequel operations that you make will internaly be done using the 63 | ActiveRecord connection, so you should see the queries in your ActiveRecord 64 | logs. 65 | 66 | ```rb 67 | DB.create_table :posts do 68 | primary_key :id 69 | String :title, null: false 70 | Stirng :body, null: false 71 | end 72 | 73 | DB[:posts].insert( 74 | title: "Sequel::ActiveRecordConnection", 75 | body: "Allows Sequel to reuse ActiveRecord's connection", 76 | ) 77 | #=> 1 78 | 79 | DB[:posts].all 80 | #=> [{ title: "Sequel::ActiveRecordConnection", body: "Allows Sequel to reuse ActiveRecord's connection" }] 81 | 82 | DB[:posts].update(title: "sequel-activerecord_connection") 83 | #=> 1 84 | ``` 85 | 86 | The database extension supports `postgresql`, `mysql2` and `sqlite3` 87 | ActiveRecord adapters, just make sure to initialize the corresponding Sequel 88 | adapter before loading the extension. 89 | 90 | ```rb 91 | Sequel.postgres(extensions: :activerecord_connection) # for "postgresql" adapter 92 | Sequel.mysql2(extensions: :activerecord_connection) # for "mysql2" adapter 93 | Sequel.sqlite(extensions: :activerecord_connection) # for "sqlite3" adapter 94 | ``` 95 | 96 | If you're on JRuby, you should be using the JDBC adapters: 97 | 98 | ```rb 99 | Sequel.connect("jdbc:postgresql://", extensions: :activerecord_connection) # for "jdbcpostgresql" adapter 100 | Sequel.connect("jdbc:mysql://", extensions: :activerecord_connection) # for "jdbcmysql" adapter 101 | Sequel.connect("jdbc:sqlite://", extensions: :activerecord_connection) # for "jdbcsqlite3" adapter 102 | ``` 103 | 104 | ### Transactions 105 | 106 | This database extension keeps the transaction state of Sequel and ActiveRecord 107 | in sync, allowing you to use Sequel and ActiveRecord transactions 108 | interchangeably (including nesting them), and have things like ActiveRecord's 109 | and Sequel's transactional callbacks still work correctly. 110 | 111 | ```rb 112 | ActiveRecord::Base.transaction do 113 | DB.in_transaction? #=> true 114 | end 115 | ``` 116 | 117 | Sequel's transaction API is fully supported: 118 | 119 | ```rb 120 | DB.transaction(isolation: :serializable) do 121 | DB.after_commit { ... } # executed after transaction commits 122 | DB.transaction(savepoint: true) do # creates a savepoint 123 | DB.after_commit(savepoint: true) { ... } # executed if all enclosing savepoints have been released 124 | end 125 | end 126 | ``` 127 | 128 | When registering transaction hooks, they will be registered on Sequel 129 | transactions when possible, in which case they will behave as described in the 130 | [Sequel docs][sequel transaction hooks]. 131 | 132 | ```rb 133 | # Sequel: An after_commit transaction hook will always get executed if the outer 134 | # transaction commits, even if it's added inside a savepoint that's rolled back. 135 | DB.transaction do 136 | ActiveRecord::Base.transaction(requires_new: true) do 137 | DB.after_commit { puts "after commit" } 138 | raise ActiveRecord::Rollback 139 | end 140 | end 141 | #>> BEGIN 142 | #>> SAVEPOINT active_record_1 143 | #>> ROLLBACK TO SAVEPOINT active_record_1 144 | #>> COMMIT 145 | #>> after commit 146 | 147 | # Sequel: An after_commit savepoint hook will get executed only after the outer 148 | # transaction commits, given that all enclosing savepoints have been released. 149 | DB.transaction(auto_savepoint: true) do 150 | DB.transaction do 151 | DB.after_commit(savepoint: true) { puts "after commit" } 152 | raise Sequel::Rollback 153 | end 154 | end 155 | #>> BEGIN 156 | #>> SAVEPOINT active_record_1 157 | #>> ROLLBACK TO SAVEPOINT active_record_1 158 | #>> COMMIT 159 | ``` 160 | 161 | In case of (a) adding a transaction hook while Active Record holds the 162 | transaction, or (b) adding a savepoint hook when Active Record holds any 163 | enclosing savepoint, Active Record transaction callbacks will be used instead 164 | of Sequel hooks, which have slightly different behaviour in some circumstances. 165 | 166 | ```rb 167 | # ActiveRecord: An after_commit transaction callback is not executed if any 168 | # if the enclosing savepoints have been rolled back 169 | ActiveRecord::Base.transaction do 170 | DB.transaction(savepoint: true) do 171 | DB.after_commit { puts "after commit" } 172 | raise Sequel::Rollback 173 | end 174 | end 175 | #>> BEGIN 176 | #>> SAVEPOINT active_record_1 177 | #>> ROLLBACK TO SAVEPOINT active_record_1 178 | #>> COMMIT 179 | 180 | # ActiveRecord: An after_commit transaction callback can be executed already 181 | # after a savepoint is released, if the enclosing transaction is not joinable. 182 | ActiveRecord::Base.transaction(joinable: false) do 183 | DB.transaction do 184 | DB.after_commit { puts "after savepoint release" } 185 | end 186 | end 187 | #>> BEGIN 188 | #>> SAVEPOINT active_record_1 189 | #>> RELEASE SAVEPOINT active_record_1 190 | #>> after savepoint release 191 | #>> COMMIT 192 | ``` 193 | 194 | ### Model 195 | 196 | By default, the connection configuration will be read from `ActiveRecord::Base`. 197 | If you want to use connection configuration from a different model, you can 198 | can assign it to the database object after loading the extension: 199 | 200 | ```rb 201 | class MyModel < ActiveRecord::Base 202 | connects_to database: { writing: :animals, reading: :animals_replica } 203 | end 204 | ``` 205 | ```rb 206 | DB.activerecord_model = MyModel 207 | ``` 208 | 209 | ### Normalizing SQL logs 210 | 211 | Active Record injects values into queries using bound variables, and displays 212 | them at the end of SQL logs: 213 | 214 | ```sql 215 | SELECT accounts.* FROM accounts WHERE accounts.email = $1 LIMIT $2 [["email", "user@example.com"], ["LIMIT", 1]] 216 | ``` 217 | 218 | Sequel interpolates values into its queries, so by default its SQL logs include 219 | them inline: 220 | 221 | ```sql 222 | SELECT accounts.* FROM accounts WHERE accounts.email = 'user@example.com' LIMIT 1 223 | ``` 224 | 225 | If you want to normalize logs to group similar queries, or you want to protect 226 | sensitive data from being stored in the logs, you can use the 227 | [sql_log_normalizer] extension to remove literal strings and numbers from 228 | logged SQL queries: 229 | 230 | ```rb 231 | Sequel.postgres(extensions: [:activerecord_connection, :sql_log_normalizer]) 232 | ``` 233 | ```sql 234 | SELECT accounts.* FROM accounts WHERE accounts.email = ? LIMIT ? 235 | ``` 236 | 237 | ## Tests 238 | 239 | You'll first want to run the rake tasks for setting up databases and users: 240 | 241 | ```sh 242 | $ rake db_setup_postgres 243 | $ rake db_setup_mysql 244 | ``` 245 | 246 | Then you can run the tests: 247 | 248 | ```sh 249 | $ rake test 250 | ``` 251 | 252 | When you're done, you can delete the created databases and users: 253 | 254 | ```sh 255 | $ rake db_teardown_postgres 256 | $ rake db_teardown_mysql 257 | ``` 258 | 259 | ## Support 260 | 261 | Please feel free to raise a new disucssion in [Github issues](https://github.com/janko/sequel-activerecord_connection/discussions), or search amongst the existing questions there. 262 | 263 | ## License 264 | 265 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 266 | 267 | ## Code of Conduct 268 | 269 | Everyone interacting in this project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/janko/sequel-activerecord-adapter/blob/master/CODE_OF_CONDUCT.md). 270 | 271 | [Sequel]: https://github.com/jeremyevans/sequel 272 | [Rodauth]: https://github.com/jeremyevans/rodauth 273 | [rom-sql]: https://github.com/rom-rb/rom-sql 274 | [sequel transaction hooks]: http://sequel.jeremyevans.net/rdoc/files/doc/transactions_rdoc.html#label-Transaction+Hooks 275 | [Oracle enhanced]: https://github.com/rsim/oracle-enhanced 276 | [SQL Server]: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter 277 | [sql_log_normalizer]: https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/sql_log_normalizer_rb.html 278 | [after_commit_everywhere]: https://github.com/Envek/after_commit_everywhere 279 | -------------------------------------------------------------------------------- /test/extension_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "logger" 3 | 4 | describe "General extension" do 5 | before do 6 | connect_postgresql 7 | 8 | @db.create_table! :records do 9 | primary_key :id 10 | String :col 11 | Time :time 12 | end 13 | end 14 | 15 | describe "#connect" do 16 | it "doesn't test the connection by default" do 17 | ActiveRecord::Base.establish_connection(**activerecord_config, database: "nonexistent") 18 | 19 | Sequel.connect "#{"jdbc:" if RUBY_ENGINE == "jruby"}postgresql://", 20 | extensions: :activerecord_connection, 21 | keep_reference: false 22 | end 23 | 24 | it "allows testing the connection" do 25 | ActiveRecord::Base.establish_connection(**activerecord_config, database: "nonexistent") 26 | 27 | assert_raises ActiveRecord::NoDatabaseError do 28 | Sequel.connect "#{"jdbc:" if RUBY_ENGINE == "jruby"}postgresql://", 29 | extensions: :activerecord_connection, 30 | keep_reference: false, 31 | test: true 32 | end 33 | end 34 | end 35 | 36 | describe "#extension" do 37 | it "only warns when database doesn't exist" do 38 | ActiveRecord::Base.establish_connection(**activerecord_config, database: "nonexistent") 39 | 40 | assert_output nil, "Sequel database extension :pg_enum failed to initialize because there is no database.\n" do 41 | @db.extension :pg_enum 42 | end 43 | end 44 | end 45 | 46 | describe "#synchronize" do 47 | it "returns the underlying connection object" do 48 | conn = @db.synchronize { |conn| conn } 49 | 50 | if RUBY_ENGINE == "jruby" 51 | assert_instance_of Java::OrgPostgresqlJdbc::PgConnection, conn 52 | else 53 | assert_instance_of PG::Connection, conn 54 | end 55 | end 56 | 57 | it "materializes transactions" do 58 | ActiveRecord::Base.connection_pool.with_connection(&:enable_lazy_transactions!) 59 | 60 | ActiveRecord::Base.transaction { @db.synchronize {} } 61 | 62 | assert_logged <<~SQL 63 | BEGIN 64 | COMMIT 65 | SQL 66 | end if ActiveRecord.version >= Gem::Version.new("6.0") 67 | 68 | it "is re-entrant" do 69 | @db.synchronize do |conn1| 70 | @db.synchronize do |conn2| 71 | assert_equal conn1, conn2 72 | @db.run "SELECT 1" 73 | end 74 | end 75 | 76 | assert_logged <<~SQL 77 | SELECT 1 78 | SQL 79 | end 80 | 81 | it "doesn't allow parallel access for the same connection" do 82 | ActiveRecord::Base.connection_pool.lock_thread = Thread.current 83 | 84 | q1 = Queue.new 85 | q2 = Queue.new 86 | 87 | thread1 = Thread.new do 88 | @db.synchronize do 89 | q1.pop 90 | end 91 | end 92 | 93 | nil until thread1.status == "sleep" # waiting on Queue#pop 94 | 95 | thread2 = Thread.new do 96 | @db.synchronize do 97 | q2.push "x" 98 | end 99 | end 100 | 101 | nil until thread2.status == "sleep" # waiting on AR lock 102 | 103 | q1.push "x" 104 | q2.pop 105 | 106 | [thread1, thread2].each(&:join) 107 | end unless ActiveRecord.version >= Gem::Version.new("7.2") 108 | 109 | it "checks the expected connection class" do 110 | db = Sequel.connect "sqlite://", 111 | extensions: :activerecord_connection, 112 | keep_reference: false 113 | 114 | error = assert_raises(Sequel::ActiveRecordConnection::Error) { db.run "SELECT 1" } 115 | assert_equal "expected Active Record connection to be a SQLite3::Database, got PG::Connection", error.message 116 | end unless RUBY_ENGINE == "jruby" 117 | end 118 | 119 | describe "#transaction" do 120 | it "creates a database transaction" do 121 | @db.transaction do 122 | assert_equal 1, ActiveRecord::Base.connection.open_transactions 123 | end 124 | 125 | assert_logged <<~SQL 126 | BEGIN 127 | COMMIT 128 | SQL 129 | end 130 | 131 | it "returns block result" do 132 | assert_equal :result, @db.transaction { :result } 133 | end 134 | 135 | it "reuses existing transaction" do 136 | @db.transaction do 137 | @db.transaction do 138 | assert_equal 1, ActiveRecord::Base.connection.open_transactions 139 | end 140 | end 141 | 142 | assert_logged <<~SQL 143 | BEGIN 144 | COMMIT 145 | SQL 146 | 147 | @db.transaction do 148 | ActiveRecord::Base.transaction do 149 | assert_equal 1, ActiveRecord::Base.connection.open_transactions 150 | end 151 | end 152 | 153 | assert_logged <<~SQL 154 | BEGIN 155 | COMMIT 156 | SQL 157 | 158 | ActiveRecord::Base.transaction do 159 | @db.transaction do 160 | assert_equal 1, ActiveRecord::Base.connection.open_transactions 161 | end 162 | end 163 | 164 | assert_logged <<~SQL 165 | BEGIN 166 | COMMIT 167 | SQL 168 | end 169 | 170 | it "reuses existing savepoint" do 171 | @db.transaction do 172 | @db.transaction(savepoint: true) do 173 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 174 | @db.transaction do 175 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 176 | end 177 | end 178 | end 179 | 180 | assert_logged <<~SQL 181 | BEGIN 182 | SAVEPOINT active_record_1 183 | RELEASE SAVEPOINT active_record_1 184 | COMMIT 185 | SQL 186 | 187 | @db.transaction do 188 | ActiveRecord::Base.transaction(requires_new: true) do 189 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 190 | @db.transaction do 191 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 192 | end 193 | end 194 | end 195 | 196 | if ActiveRecord.version < Gem::Version.new("7.1") 197 | assert_logged <<~SQL 198 | BEGIN 199 | SAVEPOINT active_record_1 200 | RELEASE SAVEPOINT active_record_1 201 | COMMIT 202 | SQL 203 | else 204 | assert_logged <<~SQL 205 | BEGIN 206 | COMMIT 207 | SQL 208 | end 209 | end 210 | 211 | it "support :savepoint option" do 212 | @db.transaction do 213 | @db.transaction(savepoint: true) do 214 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 215 | end 216 | end 217 | 218 | assert_logged <<~SQL 219 | BEGIN 220 | SAVEPOINT active_record_1 221 | RELEASE SAVEPOINT active_record_1 222 | COMMIT 223 | SQL 224 | 225 | ActiveRecord::Base.transaction do 226 | @db.transaction(savepoint: true) do 227 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 228 | end 229 | end 230 | 231 | assert_logged <<~SQL 232 | BEGIN 233 | SAVEPOINT active_record_1 234 | RELEASE SAVEPOINT active_record_1 235 | COMMIT 236 | SQL 237 | 238 | ActiveRecord::Base.transaction do 239 | @db.transaction(savepoint: :only) do 240 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 241 | end 242 | end 243 | 244 | assert_logged <<~SQL 245 | BEGIN 246 | SAVEPOINT active_record_1 247 | RELEASE SAVEPOINT active_record_1 248 | COMMIT 249 | SQL 250 | 251 | @db.transaction(auto_savepoint: true) do 252 | @db.transaction(savepoint: false) do 253 | assert_equal 1, ActiveRecord::Base.connection.open_transactions 254 | end 255 | end 256 | 257 | assert_logged <<~SQL 258 | BEGIN 259 | COMMIT 260 | SQL 261 | 262 | ActiveRecord::Base.transaction(joinable: false) do 263 | @db.transaction(savepoint: false) do 264 | assert_equal 1, ActiveRecord::Base.connection.open_transactions 265 | end 266 | end 267 | 268 | assert_logged <<~SQL 269 | BEGIN 270 | COMMIT 271 | SQL 272 | 273 | @db.transaction do 274 | @db.transaction(savepoint: true) do 275 | raise Sequel::Rollback 276 | end 277 | end 278 | 279 | assert_logged <<~SQL 280 | BEGIN 281 | SAVEPOINT active_record_1 282 | ROLLBACK TO SAVEPOINT active_record_1 283 | COMMIT 284 | SQL 285 | end 286 | 287 | it "supports :auto_savepoint option" do 288 | @db.transaction(auto_savepoint: true) do 289 | @db.transaction do 290 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 291 | end 292 | end 293 | 294 | assert_logged <<~SQL 295 | BEGIN 296 | SAVEPOINT active_record_1 297 | RELEASE SAVEPOINT active_record_1 298 | COMMIT 299 | SQL 300 | 301 | @db.transaction(auto_savepoint: true) do 302 | ActiveRecord::Base.transaction do 303 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 304 | end 305 | end 306 | 307 | assert_logged <<~SQL 308 | BEGIN 309 | SAVEPOINT active_record_1 310 | RELEASE SAVEPOINT active_record_1 311 | COMMIT 312 | SQL 313 | 314 | ActiveRecord::Base.transaction(joinable: false) do 315 | @db.transaction do 316 | assert_equal 2, ActiveRecord::Base.connection.open_transactions 317 | end 318 | end 319 | 320 | assert_logged <<~SQL 321 | BEGIN 322 | SAVEPOINT active_record_1 323 | RELEASE SAVEPOINT active_record_1 324 | COMMIT 325 | SQL 326 | 327 | @db.transaction(auto_savepoint: true) do 328 | @db.transaction do 329 | raise Sequel::Rollback 330 | end 331 | end 332 | 333 | assert_logged <<~SQL 334 | BEGIN 335 | SAVEPOINT active_record_1 336 | ROLLBACK TO SAVEPOINT active_record_1 337 | COMMIT 338 | SQL 339 | end 340 | 341 | it "supports :rollback option" do 342 | @db.transaction(rollback: :always) { } 343 | 344 | assert_logged <<~SQL 345 | BEGIN 346 | ROLLBACK 347 | SQL 348 | 349 | assert_raises Sequel::Rollback do 350 | @db.transaction(rollback: :reraise) do 351 | raise Sequel::Rollback 352 | end 353 | end 354 | 355 | assert_logged <<~SQL 356 | BEGIN 357 | ROLLBACK 358 | SQL 359 | end 360 | 361 | it "rolls back on exceptions" do 362 | assert_raises KeyError do 363 | @db.transaction do 364 | @db.run "SELECT 1" 365 | raise KeyError 366 | end 367 | end 368 | 369 | assert_logged <<~SQL 370 | BEGIN 371 | SELECT 1 372 | ROLLBACK 373 | SQL 374 | end 375 | 376 | it "rolls back on Sequel::Rollback" do 377 | @db.transaction do 378 | raise Sequel::Rollback 379 | end 380 | 381 | assert_logged <<~SQL 382 | BEGIN 383 | ROLLBACK 384 | SQL 385 | 386 | @db.transaction do 387 | @db.transaction(savepoint: true) do 388 | raise Sequel::Rollback 389 | end 390 | end 391 | 392 | assert_logged <<~SQL 393 | BEGIN 394 | SAVEPOINT active_record_1 395 | ROLLBACK TO SAVEPOINT active_record_1 396 | COMMIT 397 | SQL 398 | end 399 | 400 | it "supports :isolation option" do 401 | @db.transaction(isolation: :uncommitted) { } 402 | 403 | assert_logged <<~SQL 404 | BEGIN#{"\nSET TRANSACTION" if ActiveRecord.version < Gem::Version.new("7.1")} ISOLATION LEVEL READ UNCOMMITTED 405 | COMMIT 406 | SQL 407 | 408 | @db.transaction(isolation: :committed) { } 409 | 410 | assert_logged <<~SQL 411 | BEGIN#{"\nSET TRANSACTION" if ActiveRecord.version < Gem::Version.new("7.1")} ISOLATION LEVEL READ COMMITTED 412 | COMMIT 413 | SQL 414 | 415 | @db.transaction(isolation: :repeatable) { } 416 | 417 | assert_logged <<~SQL 418 | BEGIN#{"\nSET TRANSACTION" if ActiveRecord.version < Gem::Version.new("7.1")} ISOLATION LEVEL REPEATABLE READ 419 | COMMIT 420 | SQL 421 | 422 | @db.transaction(isolation: :serializable) { } 423 | 424 | assert_logged <<~SQL 425 | BEGIN#{"\nSET TRANSACTION" if ActiveRecord.version < Gem::Version.new("7.1")} ISOLATION LEVEL SERIALIZABLE 426 | COMMIT 427 | SQL 428 | end 429 | end 430 | 431 | describe "#in_transaction?" do 432 | it "returns true inside Sequel transaction" do 433 | @db.transaction do 434 | assert_equal true, @db.in_transaction? 435 | assert_equal true, ActiveRecord::Base.connection.transaction_open? 436 | end 437 | end 438 | 439 | it "returns true inside ActiveRecord transaction" do 440 | ActiveRecord::Base.transaction do 441 | assert_equal true, @db.in_transaction? 442 | assert_equal true, ActiveRecord::Base.connection_pool.with_connection(&:transaction_open?) 443 | end 444 | end 445 | 446 | it "returns false when outside transaction" do 447 | assert_equal false, @db.in_transaction? 448 | end 449 | 450 | it "returns false when ActiveRecord transaction finished" do 451 | ActiveRecord::Base.transaction do 452 | @db.transaction(savepoint: true) { } 453 | end 454 | assert_equal false, @db.in_transaction? 455 | end 456 | end 457 | 458 | describe "#after_commit" do 459 | it "supports transaction hooks" do 460 | after_commit_called = false 461 | @db.transaction do 462 | @db.after_commit { after_commit_called = true } 463 | refute after_commit_called 464 | end 465 | assert after_commit_called 466 | 467 | after_commit_called = false 468 | @db.transaction do 469 | @db.transaction(savepoint: true) do 470 | @db.after_commit { after_commit_called = true } 471 | end 472 | refute after_commit_called 473 | end 474 | assert after_commit_called 475 | 476 | after_commit_called = false 477 | @db.transaction do 478 | ActiveRecord::Base.transaction(requires_new: true) do 479 | @db.after_commit { after_commit_called = true } 480 | end 481 | refute after_commit_called 482 | end 483 | assert after_commit_called 484 | 485 | after_commit_called = false 486 | @db.transaction do 487 | @db.after_commit { after_commit_called = true } 488 | raise Sequel::Rollback 489 | end 490 | refute after_commit_called 491 | 492 | after_commit_called = false 493 | @db.after_commit { after_commit_called = true } 494 | assert after_commit_called 495 | end 496 | 497 | it "supports transaction hooks when Active Record holds the transaction" do 498 | after_commit_called = false 499 | ActiveRecord::Base.transaction do 500 | @db.after_commit { after_commit_called = true } 501 | refute after_commit_called 502 | end 503 | assert after_commit_called 504 | 505 | after_commit_called = false 506 | ActiveRecord::Base.transaction(joinable: false) do 507 | @db.transaction do 508 | @db.after_commit { after_commit_called = true } 509 | refute after_commit_called 510 | end 511 | assert after_commit_called 512 | end 513 | 514 | after_commit_called = false 515 | ActiveRecord::Base.transaction(joinable: false) do 516 | ActiveRecord::Base.transaction do 517 | @db.after_commit { after_commit_called = true } 518 | refute after_commit_called 519 | end 520 | assert after_commit_called 521 | end 522 | 523 | after_commit_called = false 524 | ActiveRecord::Base.transaction do 525 | @db.transaction(savepoint: true) do 526 | @db.after_commit { after_commit_called = true } 527 | end 528 | refute after_commit_called 529 | end 530 | assert after_commit_called 531 | 532 | after_commit_called = false 533 | ActiveRecord::Base.transaction do 534 | ActiveRecord::Base.transaction(requires_new: true) do 535 | @db.after_commit { after_commit_called = true } 536 | end 537 | refute after_commit_called 538 | end 539 | assert after_commit_called 540 | end 541 | 542 | it "supports savepoint hooks" do 543 | after_commit_called = false 544 | @db.transaction do 545 | @db.after_commit(savepoint: true) { after_commit_called = true } 546 | refute after_commit_called 547 | end 548 | assert after_commit_called 549 | 550 | after_commit_called = false 551 | @db.transaction do 552 | @db.transaction(savepoint: true) do 553 | @db.after_commit(savepoint: true) { after_commit_called = true } 554 | end 555 | refute after_commit_called 556 | end 557 | assert after_commit_called 558 | 559 | after_commit_called = false 560 | @db.transaction do 561 | @db.transaction(savepoint: true) do 562 | @db.after_commit(savepoint: true) { after_commit_called = true } 563 | raise Sequel::Rollback 564 | end 565 | end 566 | refute after_commit_called 567 | 568 | after_commit_called = false 569 | @db.after_commit(savepoint: true) { after_commit_called = true } 570 | assert after_commit_called 571 | end 572 | 573 | it "supports savepoint hooks when Active Record holds the savepoint" do 574 | after_commit_called = false 575 | @db.transaction do 576 | ActiveRecord::Base.transaction(requires_new: true) do 577 | @db.after_commit(savepoint: true) { after_commit_called = true } 578 | end 579 | refute after_commit_called 580 | end 581 | assert after_commit_called 582 | 583 | after_commit_called = false 584 | @db.transaction(auto_savepoint: true) do 585 | ActiveRecord::Base.transaction do 586 | @db.after_commit(savepoint: true) { after_commit_called = true } 587 | refute after_commit_called 588 | end 589 | assert after_commit_called 590 | end 591 | 592 | after_commit_called = false 593 | ActiveRecord::Base.transaction(joinable: false) do 594 | ActiveRecord::Base.transaction do 595 | @db.after_commit(savepoint: true) { after_commit_called = true } 596 | refute after_commit_called 597 | end 598 | assert after_commit_called 599 | end 600 | 601 | after_commit_called = false 602 | ActiveRecord::Base.transaction do 603 | @db.transaction(savepoint: true) do 604 | @db.after_commit(savepoint: true) { after_commit_called = true } 605 | end 606 | refute after_commit_called 607 | end 608 | assert after_commit_called 609 | 610 | after_commit_called = false 611 | @db.transaction do 612 | ActiveRecord::Base.transaction(requires_new: true) do 613 | @db.transaction(savepoint: true) do 614 | @db.after_commit(savepoint: true) { after_commit_called = true } 615 | end 616 | end 617 | refute after_commit_called 618 | end 619 | assert after_commit_called 620 | 621 | after_commit_called = false 622 | ActiveRecord::Base.transaction do 623 | ActiveRecord::Base.transaction(requires_new: true) do 624 | @db.transaction(savepoint: true) do 625 | @db.after_commit(savepoint: true) { after_commit_called = true } 626 | end 627 | end 628 | refute after_commit_called 629 | end 630 | assert after_commit_called 631 | end 632 | end 633 | 634 | describe "#after_rollback" do 635 | it "supports transaction hooks" do 636 | after_rollback_called = false 637 | @db.transaction do 638 | @db.after_rollback { after_rollback_called = true } 639 | refute after_rollback_called 640 | raise Sequel::Rollback 641 | end 642 | assert after_rollback_called 643 | 644 | after_rollback_called = false 645 | @db.transaction do 646 | @db.transaction(savepoint: true) do 647 | @db.after_rollback { after_rollback_called = true } 648 | end 649 | refute after_rollback_called 650 | raise Sequel::Rollback 651 | end 652 | assert after_rollback_called 653 | 654 | after_rollback_called = false 655 | @db.transaction do 656 | ActiveRecord::Base.transaction(requires_new: true) do 657 | @db.after_rollback { after_rollback_called = true } 658 | end 659 | refute after_rollback_called 660 | raise Sequel::Rollback 661 | end 662 | assert after_rollback_called 663 | 664 | after_rollback_called = false 665 | @db.transaction do 666 | @db.after_rollback { after_rollback_called = true } 667 | end 668 | refute after_rollback_called 669 | 670 | after_rollback_called = false 671 | @db.transaction do 672 | @db.transaction(savepoint: true) do 673 | @db.after_rollback { after_rollback_called = true } 674 | raise Sequel::Rollback 675 | end 676 | end 677 | refute after_rollback_called 678 | 679 | after_rollback_called = false 680 | @db.after_rollback { after_rollback_called = true } 681 | refute after_rollback_called 682 | end 683 | 684 | it "supports transaction hooks when Active Record holds the transaction" do 685 | after_rollback_called = false 686 | ActiveRecord::Base.transaction do 687 | @db.after_rollback { after_rollback_called = true } 688 | refute after_rollback_called 689 | raise ActiveRecord::Rollback 690 | end 691 | assert after_rollback_called 692 | 693 | after_rollback_called = false 694 | ActiveRecord::Base.transaction do 695 | @db.transaction(savepoint: true) do 696 | @db.after_rollback { after_rollback_called = true } 697 | refute after_rollback_called 698 | raise Sequel::Rollback 699 | end 700 | assert after_rollback_called 701 | end 702 | 703 | after_rollback_called = false 704 | ActiveRecord::Base.transaction do 705 | @db.transaction(savepoint: true) do 706 | @db.after_rollback { after_rollback_called = true } 707 | end 708 | refute after_rollback_called 709 | raise ActiveRecord::Rollback 710 | end 711 | assert after_rollback_called 712 | end 713 | 714 | it "supports savepoint hooks" do 715 | after_rollback_called = false 716 | @db.transaction do 717 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 718 | refute after_rollback_called 719 | raise Sequel::Rollback 720 | end 721 | assert after_rollback_called 722 | 723 | after_rollback_called = false 724 | @db.transaction do 725 | @db.transaction(savepoint: true) do 726 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 727 | raise Sequel::Rollback 728 | end 729 | assert after_rollback_called 730 | end 731 | 732 | after_rollback_called = false 733 | @db.transaction do 734 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 735 | end 736 | refute after_rollback_called 737 | 738 | after_rollback_called = false 739 | @db.transaction do 740 | @db.transaction(savepoint: true) do 741 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 742 | end 743 | end 744 | refute after_rollback_called 745 | 746 | after_rollback_called = false 747 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 748 | refute after_rollback_called 749 | end 750 | 751 | it "supports savepoint hooks when Active Record holds the savepoint" do 752 | after_rollback_called = false 753 | ActiveRecord::Base.transaction do 754 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 755 | refute after_rollback_called 756 | raise ActiveRecord::Rollback 757 | end 758 | assert after_rollback_called 759 | 760 | after_rollback_called = false 761 | @db.transaction do 762 | ActiveRecord::Base.transaction(requires_new: true) do 763 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 764 | refute after_rollback_called 765 | raise ActiveRecord::Rollback 766 | end 767 | assert after_rollback_called 768 | end 769 | 770 | after_rollback_called = false 771 | @db.transaction do 772 | ActiveRecord::Base.transaction(requires_new: true) do 773 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 774 | end 775 | refute after_rollback_called 776 | raise Sequel::Rollback 777 | end 778 | assert after_rollback_called 779 | 780 | after_rollback_called = false 781 | ActiveRecord::Base.transaction do 782 | @db.transaction(savepoint: true) do 783 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 784 | end 785 | refute after_rollback_called 786 | raise ActiveRecord::Rollback 787 | end 788 | assert after_rollback_called 789 | 790 | after_rollback_called = false 791 | ActiveRecord::Base.transaction do 792 | ActiveRecord::Base.transaction(requires_new: true) do 793 | @db.transaction(savepoint: true) do 794 | @db.after_rollback(savepoint: true) { after_rollback_called = true } 795 | end 796 | refute after_rollback_called 797 | raise ActiveRecord::Rollback 798 | end 799 | assert after_rollback_called 800 | end 801 | end 802 | end 803 | 804 | describe "#rollback_on_exit" do 805 | it "rolls back on transaction exit" do 806 | @db.transaction do 807 | @db.rollback_on_exit 808 | end 809 | 810 | assert_logged <<~SQL 811 | BEGIN 812 | ROLLBACK 813 | SQL 814 | 815 | @db.transaction do 816 | @db.transaction(savepoint: true) do 817 | @db.rollback_on_exit(savepoint: true) 818 | end 819 | end 820 | 821 | assert_logged <<~SQL 822 | BEGIN 823 | SAVEPOINT active_record_1 824 | ROLLBACK TO SAVEPOINT active_record_1 825 | COMMIT 826 | SQL 827 | end 828 | end 829 | 830 | describe "#rollback_checker" do 831 | it "returns block that returns whether transaction was rolled back" do 832 | rollback_checker = nil 833 | 834 | @db.transaction do 835 | rollback_checker = @db.rollback_checker 836 | end 837 | assert_equal false, rollback_checker.call 838 | 839 | @db.transaction do 840 | rollback_checker = @db.rollback_checker 841 | raise Sequel::Rollback 842 | end 843 | assert_equal true, rollback_checker.call 844 | end 845 | end 846 | 847 | describe "#timezone" do 848 | it "defaults to ActiveRecord::Base.default_timezone" do 849 | set_activerecord_timezone(:utc) 850 | assert_equal :utc, @db.timezone 851 | 852 | set_activerecord_timezone(:local) 853 | assert_equal :local, @db.timezone 854 | end 855 | 856 | it "picks manually set value" do 857 | set_activerecord_timezone(:utc) 858 | @db.timezone = :local 859 | 860 | assert_equal :local, @db.timezone 861 | end 862 | end 863 | 864 | describe "#log_connection_yield" do 865 | it "still logs queries to Sequel logger(s)" do 866 | @db.logger = Logger.new(output = StringIO.new) 867 | @db.run "SELECT 1" 868 | 869 | assert_match(/SELECT 1/, output.string) 870 | end 871 | 872 | it "sends normalized SQL to Active Record when using sql_log_normalizer extension" do 873 | @db.extension :sql_log_normalizer 874 | @db[:records].first(col: "15", time: Time.now) 875 | 876 | assert_logged <<~SQL 877 | SELECT * FROM "records" WHERE (("col" = ?) AND ("time" = ?)) LIMIT ? 878 | SQL 879 | end 880 | end 881 | 882 | describe "#valid_connection?" do 883 | it "returns true if connection is valid" do 884 | @db.synchronize do |conn| 885 | assert_equal true, @db.valid_connection?(conn) 886 | end 887 | end 888 | end 889 | 890 | describe "#connect" do 891 | it "is disallowed" do 892 | assert_raises Sequel::ActiveRecordConnection::Error do 893 | @db.connect 894 | end 895 | end 896 | end 897 | 898 | describe "#disconnect" do 899 | it "doesn't close the connection" do 900 | conn = @db.synchronize { |conn| conn } 901 | @db.disconnect 902 | refute conn.finished? 903 | end unless RUBY_ENGINE == "jruby" 904 | end 905 | end 906 | --------------------------------------------------------------------------------