diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..beb1bea0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tmp/rails"] + path = tmp/rails + url = https://github.com/rails/rails.git diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..1a2b7e84 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,14 @@ +inherit_gem: + onkcop: + - "config/rubocop.yml" + - "config/rspec.yml" + +inherit_from: + - ".rubocop_todo.yml" + +AllCops: + TargetRubyVersion: 2.2 + Exclude: + - "test/**/*" + - "tmp/**/*" + - "activerecord.rake" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..35dad93a --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,152 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2017-02-10 00:09:09 +0900 using RuboCop version 0.47.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 2 +# Configuration parameters: AllowSafeAssignment. +Lint/AssignmentInCondition: + Exclude: + - 'lib/active_record/turntable/active_record_ext/connection_handler_extension.rb' + - 'lib/active_record/turntable/active_record_ext/schema_dumper.rb' + +# Offense count: 4 +Lint/RescueException: + Exclude: + - 'lib/active_record/turntable/connection_proxy.rb' + - 'lib/active_record/turntable/mixer.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +Lint/UnneededSplatExpansion: + Exclude: + - 'Rakefile' + +# Offense count: 14 +Metrics/AbcSize: + Max: 106 + +# Offense count: 2 +# Configuration parameters: CountComments, ExcludedMethods. +Metrics/BlockLength: + Max: 88 + +# Offense count: 2 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 202 + +# Offense count: 5 +Metrics/CyclomaticComplexity: + Max: 20 + +# Offense count: 2 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 246 + +# Offense count: 10 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 56 + +# Offense count: 1 +# Configuration parameters: CountKeywordArgs. +Metrics/ParameterLists: + Max: 6 + +# Offense count: 9 +Metrics/PerceivedComplexity: + Max: 24 + +# Offense count: 3 +# Cop supports --auto-correct. +Performance/TimesMap: + Exclude: + - 'script/performance/algorithm' + - 'spec/active_record/turntable/active_record_ext/association_preloader_spec.rb' + - 'spec/active_record/turntable/active_record_ext/association_spec.rb' + +# Offense count: 1 +# Configuration parameters: CustomIncludeMethods. +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/active_record/turntable/mixer/fader_spec.rb' + +# Offense count: 3 +# Configuration parameters: CustomTransform, IgnoreMethods. +RSpec/FilePath: + Exclude: + - 'spec/active_record/turntable/active_record_ext/fixture_set_spec.rb' + - 'spec/active_record/turntable/active_record_ext/test_fixtures_spec.rb' + +# Offense count: 21 +# Configuration parameters: Max. +RSpec/NestedGroups: + Exclude: + - 'spec/active_record/turntable/active_record_ext/association_preloader_spec.rb' + - 'spec/active_record/turntable/active_record_ext/association_spec.rb' + - 'spec/active_record/turntable/active_record_ext/clever_load_spec.rb' + - 'spec/active_record/turntable/connection_proxy_spec.rb' + - 'spec/active_record/turntable/finder_spec.rb' + - 'spec/active_record/turntable/mixer_spec.rb' + +# Offense count: 15 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods. +# SupportedStyles: line_count_based, semantic, braces_for_chaining +# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object +# FunctionalMethods: let, let!, subject, watch +# IgnoredMethods: lambda, proc, it +Style/BlockDelimiters: + Exclude: + - 'spec/**/*' + - 'lib/active_record/turntable/active_record_ext/acts_as_archive_extension.rb' + - 'lib/active_record/turntable/active_record_ext/clever_load.rb' + - 'lib/active_record/turntable/active_record_ext/database_tasks.rb' + - 'lib/active_record/turntable/active_record_ext/fixtures.rb' + - 'lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb' + - 'lib/active_record/turntable/cluster_helper_methods.rb' + - 'lib/active_record/turntable/connection_proxy.rb' + - 'script/performance/algorithm' + +# Offense count: 2 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'lib/active_record/turntable/sql_tree_patch.rb' + +# Offense count: 4 +Style/MethodCalledOnDoEndBlock: + Exclude: + - 'lib/active_record/turntable/migration.rb' + - 'lib/active_record/turntable/mixer.rb' + - 'lib/active_record/turntable/mixer/fader/update_shards_merge_result.rb' + - 'lib/active_record/turntable/sql_tree_patch.rb' + +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'Rakefile' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: AllowMultipleReturnValues. +Style/RedundantReturn: + Exclude: + - 'lib/active_record/turntable/sql_tree_patch.rb' + +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'lib/active_record/turntable/sequencer.rb' + - 'lib/active_record/turntable/sql_tree_patch.rb' diff --git a/.travis.yml b/.travis.yml index c6fa76ae..d1dcc4de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,32 @@ language: ruby +sudo: false + +cache: + bundler: true + rvm: - - 2.0.0 - - 2.1 - - 2.2 + - 2.2.6 - 2.3.3 + - 2.4.0 - ruby-head + gemfile: - - gemfiles/rails4_0.gemfile - - gemfiles/rails4_1.gemfile - - gemfiles/rails4_2.gemfile + - gemfiles/rails5_0.gemfile + - gemfiles/rails_edge.gemfile + +env: + - SETUP_TASK=turntable:db:reset BUILD_TASK=spec + - SETUP_TASK=turntable:activerecord:setup BUILD_TASK=turntable:activerecord:test + +before_install: gem update --system + before_script: - - bundle exec rake turntable:db:reset -script: bundle exec rake spec + - bundle exec rake $SETUP_TASK + +script: + - bundle exec rake $BUILD_TASK + matrix: allow_failures: - rvm: ruby-head -sudo: false -cache: bundler + - gemfile: gemfiles/rails_edge.gemfile diff --git a/CHANGELOG.md b/CHANGELOG.md index eda1291a..bd5939de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## activerecord-turntable 3.0.0.alpha3 ## + +### Bugfix + +* Disable statement cache when adding shard conditions automatically + +## activerecord-turntable 3.0.0.alpha2 ## + +### Improvement + +* Fix to propagate shard conditions to `AssociationRelation` too + +## activerecord-turntable 3.0.0.alpha1 ## + +### Major Changes + +* Rails5 compatibility + * Minimum ruby requirement version is `2.2.2` + * Rails 4.x support has been dropped. + ## activerecord-turntable 2.5.0 ## ### Improvement diff --git a/Gemfile b/Gemfile index b4e2a20b..719abbd0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,6 @@ source "https://rubygems.org" gemspec + +gem "activerecord", "~> 5.0.1" +gem "activesupport", "~> 5.0.1" diff --git a/Guardfile b/Guardfile index c4082ad1..a5eabb1d 100644 --- a/Guardfile +++ b/Guardfile @@ -1,11 +1,16 @@ # A sample Guardfile # More info at https://github.com/guard/guard#readme -guard 'rspec', - cmd: "bundle exec rspec", - all_after_pass: true, - all_on_start: true do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec" } +guard "rspec", + cmd: "bundle exec rspec", + all_after_pass: true, + all_on_start: true do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch("spec/spec_helper.rb") { "spec" } +end + +guard :rubocop do + watch(/.+\.rb$/) + watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } end diff --git a/README.md b/README.md index f0bd2bff..24b30dd7 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,12 @@ ActiveRecord::Turntable is a database sharding extension for ActiveRecord. ## Dependencies -activerecord(>=4.0.0) +activerecord(>=5.0.0, <6.0) -if you are using activerecord 3.x, please use activerecord-turntable version 1.x. +If you are using with older activerecord versions, use following versions. + +* activerecord 4.x - use activerecord-turntable version 2.x.([stable-2-x branch](https://github.com/drecom/activerecord-turntable/tree/stable-2-x)) +* activerecord 3.x - use activerecord-turntable version 1.x.([stable-1-x branch](https://github.com/drecom/activerecord-turntable/tree/stable-1-x)) ## Supported Database @@ -22,7 +25,7 @@ Currently supports mysql only. Add to Gemfile: ```ruby -gem 'activerecord-turntable', '~> 2.1.1' +gem 'activerecord-turntable', '~> 3.0.0.alpha1' ``` Run a bundle install: @@ -136,7 +139,7 @@ Edit turntable.yml and database.yml. See below example config. database: sample_app_user3_development ``` -### Example Migration +### Example Migration Generate a model: @@ -282,7 +285,7 @@ First, add configuration to turntable.yml and database.yml seq: user_seq: # <-- sequencer name seq_type: mysql # <-- sequencer type - connection: user_seq_1 # <-- sequencer database connection + connection: user_seq_1 # <-- sequencer database connection ``` Add below to the migration: @@ -369,8 +372,8 @@ end transaction helper to execute transaction to all shards in the cluster: ```ruby -User.user_cluster_transaction do - # Transaction is opened all shards in "user_cluster" +User.user_cluster_transaction do + # Transaction is opened all shards in "user_cluster" end ``` @@ -432,23 +435,12 @@ Use with_all method: end ``` -### Connection Management - -Rails's ConnectionManagement middleware keeps ActiveRecord's connection during the process is alive, but Turntable keeps more connections. -This may cause flooding max connections on your database. So, we made a middleware that disconnects on each request. - -if you use turntable's ConnectionManagement middleware, add below line to your initializer. - -```ruby -app.middleware.swap ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::Turntable::Rack::ConnectionManagement -``` - ### Performance Exception To notice queries causing performance problem, Turntable has follow options. * raise\_on\_not\_specified\_shard\_query - raises on queries execute on all shards -* raise\_on\_not\_specified\_shard\_update - raises on updates executed on all shards +* raise\_on\_not\_specified\_shard\_update - raises on updates executed on all shards Add to turntable.yml: diff --git a/Rakefile b/Rakefile index 22e0bfec..9c91c9d2 100644 --- a/Rakefile +++ b/Rakefile @@ -1,25 +1,25 @@ require "bundler/gem_tasks" -require 'rubygems' +require "rubygems" -require 'rspec/core' -require 'rspec/core/rake_task' +require "rspec/core" +require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = FileList['spec/**/*_spec.rb'] + spec.pattern = FileList["spec/**/*_spec.rb"] end -require 'active_record' +require "active_record" require "active_record/turntable/active_record_ext/database_tasks" namespace :turntable do namespace :db do task :rails_env do unless defined? RAILS_ENV - RAILS_ENV = ENV['RAILS_ENV'] ||= 'test' + RAILS_ENV = ENV["RAILS_ENV"] ||= "test" end end task :load_config => :rails_env do - yaml_file = File.join(File.dirname(__FILE__), 'spec/config/database.yml') + yaml_file = File.join(File.dirname(__FILE__), "spec/config/database.yml") ActiveRecord::Base.configurations = YAML.load ERB.new(IO.read(yaml_file)).result end @@ -38,12 +38,12 @@ namespace :turntable do desc "migrate turntable test tables" task :migrate => :load_config do ActiveRecord::Base.establish_connection RAILS_ENV.to_sym - require 'active_record/turntable' - ActiveRecord::Base.send(:include, ActiveRecord::Turntable) - ActiveRecord::ConnectionAdapters::SchemaStatements.send(:include, ActiveRecord::Turntable::Migration::SchemaStatementsExt) + require "active_record/turntable" + ActiveRecord::Base.include(ActiveRecord::Turntable) + ActiveRecord::ConnectionAdapters::SchemaStatements.include(ActiveRecord::Turntable::Migration::SchemaStatementsExt) configurations = [ActiveRecord::Base.configurations[RAILS_ENV]] - configurations += ActiveRecord::Tasks::DatabaseTasks.current_turntable_cluster_configurations(RAILS_ENV).map {|v| v[1]}.flatten.uniq + configurations += ActiveRecord::Tasks::DatabaseTasks.current_turntable_cluster_configurations(RAILS_ENV).map { |v| v[1] }.flatten.uniq configurations.each do |configuration| ActiveRecord::Base.establish_connection configuration @@ -101,15 +101,15 @@ namespace :turntable do ActiveRecord::Base.connection.create_table :cards_users_histories do |t| t.belongs_to :cards_user, :null => false - t.belongs_to :user, :null => false + t.belongs_to :user, :null => false t.timestamps end ActiveRecord::Base.connection.create_sequence_for :cards_users_histories ActiveRecord::Base.connection.create_table :events_users_histories do |t| - t.belongs_to :events_user, :null => false + t.belongs_to :events_user, :null => false t.belongs_to :cards_user, :null => false - t.belongs_to :user, :null => false + t.belongs_to :user, :null => false t.timestamps end ActiveRecord::Base.connection.create_sequence_for :events_users_histories @@ -124,4 +124,44 @@ namespace :turntable do desc "reset turntable test databases" task :reset => ["turntable:db:drop", "turntable:db:create", "turntable:db:migrate"] end + + namespace :activerecord do + task(:env) do + ENV["ARCONFIG"] ||= File.expand_path("spec/config/activerecord_config.yml", __dir__) + ENV["ARVERSION"] ||= if ActiveRecord.gem_version.prerelease? + "origin/master" + else + "v#{ActiveRecord.gem_version}" + end + end + + namespace :setup do + task :rails => :env do + system(*%w|git submodule update --init|) + system(*%w|git submodule foreach git fetch origin|) + Dir.chdir("tmp/rails") do + system(*%W|git checkout #{ENV['ARVERSION']}|) + end + FileUtils.cp_r("tmp/rails/activerecord/test", ".") + FileUtils.cp_r("tmp/rails/activerecord/Rakefile", "activerecord.rake") + File.open("test/cases/helper.rb", "a") do |f| + f << "require '#{File.expand_path("spec/activerecord_helper", __dir__)}'" + end + end + + task :db => :rails do + system(*%w|bundle exec rake -f activerecord.rake db:mysql:rebuild|) + end + end + + desc "setup activerecord test" + task :setup => ["setup:rails", "setup:db"] + + desc "run unit tests on activerecord" + task :test => :env do + unless system(*%w|bundle exec rake -f activerecord.rake test:mysql2|) + exit(1) + end + end + end end diff --git a/activerecord-turntable.gemspec b/activerecord-turntable.gemspec index 4d489b53..4817f4fc 100644 --- a/activerecord-turntable.gemspec +++ b/activerecord-turntable.gemspec @@ -1,56 +1,60 @@ -$:.push File.expand_path("../lib", __FILE__) +$LOAD_PATH.push File.expand_path("../lib", __FILE__) require "active_record/turntable/version" - Gem::Specification.new do |spec| spec.name = "activerecord-turntable" spec.version = ActiveRecord::Turntable::VERSION - spec.authors = ["gussan", "sue445"] + spec.authors = %w(gussan sue445) spec.homepage = "https://github.com/drecom/activerecord-turntable" - spec.summary = %q{ActiveRecord sharding extension} - spec.description = %q{ActiveRecord sharding extension} + spec.summary = "ActiveRecord sharding extension" + spec.description = "ActiveRecord sharding extension" spec.license = "MIT" spec.rubyforge_project = "activerecord-turntable" spec.extra_rdoc_files = [ "LICENSE.txt", "README.md", - "CHANGELOG.md" + "CHANGELOG.md", ] - spec.files = `git ls-files`.split($/) + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.required_ruby_version = '>= 2.0.0' + spec.required_ruby_version = ">= 2.2.2" - spec.add_runtime_dependency "activerecord", ">= 4.0.0", "< 5.0" - spec.add_runtime_dependency "activesupport", ">= 4.0.0", "< 5.0" - spec.add_runtime_dependency "sql_tree", "= 0.2.0" + spec.add_runtime_dependency "activerecord", ">= 5.0", "< 6.0" + spec.add_runtime_dependency "activesupport", ">= 5.0", "< 6.0" spec.add_runtime_dependency "bsearch", "~> 1.5" spec.add_runtime_dependency "httpclient", ">= 0" + spec.add_runtime_dependency "sql_tree", "= 0.2.0" # optional dependencies spec.add_development_dependency "activerecord-import" spec.add_development_dependency "barrage" + spec.add_development_dependency "coveralls" + spec.add_development_dependency "fabrication" + spec.add_development_dependency "faker" + spec.add_development_dependency "guard-rspec" + spec.add_development_dependency "guard-rubocop" spec.add_development_dependency "mysql2" - + spec.add_development_dependency "onkcop" + spec.add_development_dependency "pry" + spec.add_development_dependency "pry-byebug" + spec.add_development_dependency "rack" spec.add_development_dependency "rake" - spec.add_development_dependency "rack", "< 2" spec.add_development_dependency "rspec", "~> 3.5.0" - spec.add_development_dependency "rspec-its" spec.add_development_dependency "rspec-collection_matchers" - spec.add_development_dependency "fabrication" - spec.add_development_dependency "faker" + spec.add_development_dependency "rspec-its" + spec.add_development_dependency "rubocop" + spec.add_development_dependency "rubocop-rspec" + spec.add_development_dependency "timecop" spec.add_development_dependency "webmock" - spec.add_development_dependency "pry" - if RUBY_VERSION > "2.0" - spec.add_development_dependency "pry-byebug" - end - spec.add_development_dependency "guard-rspec" - spec.add_development_dependency "listen", "= 3.0.6" - spec.add_development_dependency "coveralls" - if RUBY_PLATFORM =~ /darwin/ - spec.add_development_dependency "growl" - end + # activerecord testing dependencies + spec.add_development_dependency "actionview" + spec.add_development_dependency "bcrypt", "~> 3.1.11" + spec.add_development_dependency "minitest", "< 5.3.4" + spec.add_development_dependency "mocha", "~> 0.14" + spec.add_development_dependency "sqlite3", "~> 1.3.6" end diff --git a/gemfiles/rails4_0.gemfile b/gemfiles/rails4_0.gemfile deleted file mode 100644 index 63315513..00000000 --- a/gemfiles/rails4_0.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "https://rubygems.org" - -gem "activerecord", "~> 4.0.0" -gem "activesupport", "~> 4.0.0" -gem "mysql2", "~> 0.3.20" - -gemspec :path => "../" diff --git a/gemfiles/rails4_1.gemfile b/gemfiles/rails4_1.gemfile deleted file mode 100644 index 41543316..00000000 --- a/gemfiles/rails4_1.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "https://rubygems.org" - -gem "activerecord", "~> 4.1.0" -gem "activesupport", "~> 4.1.0" -gem "mysql2", "~> 0.3.20" - -gemspec :path => "../" diff --git a/gemfiles/rails4_2.gemfile b/gemfiles/rails4_2.gemfile deleted file mode 100644 index 27a5133d..00000000 --- a/gemfiles/rails4_2.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "https://rubygems.org" - -gem "activerecord", "~> 4.2.0" -gem "activesupport", "~> 4.2.0" -gem "mysql2", "~> 0.3.20" - -gemspec :path => "../" diff --git a/gemfiles/rails5_0.gemfile b/gemfiles/rails5_0.gemfile new file mode 100644 index 00000000..fb4a7693 --- /dev/null +++ b/gemfiles/rails5_0.gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "activerecord", "5.0.1" +gem "activesupport", "5.0.1" + +gemspec :path => '../' diff --git a/gemfiles/rails_edge.gemfile b/gemfiles/rails_edge.gemfile new file mode 100644 index 00000000..fcaf72bc --- /dev/null +++ b/gemfiles/rails_edge.gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem "arel", github: "rails/arel" +gem "activerecord", github: "rails/rails" +gem "actionview", github: "rails/rails" +gem "activesupport", github: "rails/rails" + +gemspec :path => '../' diff --git a/lib/active_record/turntable.rb b/lib/active_record/turntable.rb index 0d3814e1..db9a6826 100644 --- a/lib/active_record/turntable.rb +++ b/lib/active_record/turntable.rb @@ -3,23 +3,14 @@ # # ActiveRecord Sharding Plugin # -require 'active_record/turntable/version' -require 'active_record' -require 'active_record/fixtures' -require 'active_support/concern' -require 'active_record/turntable/error' -require 'active_record/turntable/util' -require 'logger' -require 'singleton' - -# for 4.0.x series -module ActiveRecord - unless respond_to?(:gem_version) - class << self - alias_method :gem_version, :version - end - end -end +require "active_record/turntable/version" +require "active_record" +require "active_record/fixtures" +require "active_support/concern" +require "active_record/turntable/error" +require "active_record/turntable/util" +require "logger" +require "singleton" module ActiveRecord::Turntable extend ActiveSupport::Concern @@ -37,12 +28,12 @@ module ActiveRecord::Turntable autoload :Migration autoload :Mixer autoload :PoolProxy + autoload :QueryCache autoload :Shard autoload :ShardingCondition autoload :SeqShard autoload :Sequencer end - autoload :Rack autoload :Helpers included do @@ -54,18 +45,24 @@ module ClassMethods DEFAULT_PATH = File.dirname(File.dirname(__FILE__)) def turntable_config_file - @@turntable_config_file ||= - File.join(defined?(::Rails) ? - ::Rails.root.to_s : DEFAULT_PATH, 'config/turntable.yml') + @turntable_config_file ||= File.join(turntable_app_root_path, "config/turntable.yml") end def turntable_config_file=(filename) - @@turntable_config_file = filename + @turntable_config_file = filename + end + + def turntable_app_root_path + defined?(::Rails.root) ? ::Rails.root.to_s : DEFAULT_PATH end def turntable_config ActiveRecord::Turntable::Config.instance end + + def turntable_connection_classes + ActiveRecord::Turntable::Shard.connection_classes + end end require "active_record/turntable/railtie" if defined?(Rails) diff --git a/lib/active_record/turntable/active_record_ext.rb b/lib/active_record/turntable/active_record_ext.rb index 441cf2fc..2dbf7d1e 100644 --- a/lib/active_record/turntable/active_record_ext.rb +++ b/lib/active_record/turntable/active_record_ext.rb @@ -20,22 +20,21 @@ module ActiveRecordExt included do include Transactions - ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, Sequencer) - ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, AbstractAdapter) - ActiveRecord::LogSubscriber.send(:include, LogSubscriber) - ActiveRecord::Persistence.send(:include, Persistence) - ActiveRecord::Locking::Optimistic.send(:include, LockingOptimistic) - ActiveRecord::Relation.send(:include, CleverLoad, Relation) - ActiveRecord::Migration.send(:include, ActiveRecord::Turntable::Migration) - ActiveRecord::ConnectionAdapters::ConnectionHandler.instance_exec do - include ConnectionHandlerExtension - end - ActiveRecord::Associations::Preloader::Association.send(:include, AssociationPreloader) - ActiveRecord::Associations::Association.send(:prepend, Association) - require 'active_record/turntable/active_record_ext/fixtures' - require 'active_record/turntable/active_record_ext/migration_proxy' - require 'active_record/turntable/active_record_ext/activerecord_import_ext' - require 'active_record/turntable/active_record_ext/acts_as_archive_extension' + ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Sequencer) + ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(AbstractAdapter) + ActiveRecord::LogSubscriber.prepend(LogSubscriber) + ActiveRecord::Persistence.include(Persistence) + ActiveRecord::Locking::Optimistic.include(LockingOptimistic) + ActiveRecord::Relation.include(CleverLoad) + ActiveRecord::Relation.prepend(Relation) + ActiveRecord::Migration.include(ActiveRecord::Turntable::Migration) + ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend(ConnectionHandlerExtension) + ActiveRecord::Associations::Preloader::Association.prepend(AssociationPreloader) + ActiveRecord::Associations::Association.prepend(Association) + require "active_record/turntable/active_record_ext/fixtures" + require "active_record/turntable/active_record_ext/migration_proxy" + require "active_record/turntable/active_record_ext/activerecord_import_ext" + require "active_record/turntable/active_record_ext/acts_as_archive_extension" end end end diff --git a/lib/active_record/turntable/active_record_ext/abstract_adapter.rb b/lib/active_record/turntable/active_record_ext/abstract_adapter.rb index 074a8169..6f747229 100644 --- a/lib/active_record/turntable/active_record_ext/abstract_adapter.rb +++ b/lib/active_record/turntable/active_record_ext/abstract_adapter.rb @@ -1,36 +1,42 @@ module ActiveRecord::Turntable module ActiveRecordExt module AbstractAdapter - extend ActiveSupport::Concern - - included do - protected - - # @note override for logging current shard name - def log(sql, name = "SQL", binds = [], statement_name = nil) - @instrumenter.instrument( - "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :statement_name => statement_name, - :binds => binds, - :turntable_shard_name => turntable_shard_name) { yield } - rescue Exception => e + def translate_exception_class(e, sql) + begin message = "#{e.class.name}: #{e.message}: #{sql} : #{turntable_shard_name}" - @logger.error message if @logger - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception + rescue Encoding::CompatibilityError + message = "#{e.class.name}: #{e.message.force_encoding sql.encoding}: #{sql} : #{turntable_shard_name}" end + + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + exception end + # @note override for append current shard name + # rubocop:disable Style/HashSyntax, Style/MultilineMethodCallBraceLayout + def log(sql, name = "SQL", binds = [], statement_name = nil) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :statement_name => statement_name, + :binds => binds, + :turntable_shard_name => turntable_shard_name) { yield } + rescue => e + raise translate_exception_class(e, sql) + end + # rubocop:enable Style/HashSyntax, Style/MultilineMethodCallBraceLayout + + protected :translate_exception_class, :log + def turntable_shard_name=(name) @turntable_shard_name = name.to_s end def turntable_shard_name - @turntable_shard_name + @turntable_shard_name ||= "" end end end diff --git a/lib/active_record/turntable/active_record_ext/activerecord_import_ext.rb b/lib/active_record/turntable/active_record_ext/activerecord_import_ext.rb index fb61ea42..56947e0b 100644 --- a/lib/active_record/turntable/active_record_ext/activerecord_import_ext.rb +++ b/lib/active_record/turntable/active_record_ext/activerecord_import_ext.rb @@ -2,20 +2,14 @@ module ActiveRecord::Turntable module ActiveRecordExt # activerecord-import extension module ActiverecordImportExt - extend ActiveSupport::Concern - - included do - alias_method_chain :values_sql_for_columns_and_attributes, :turntable - end - - private - # @note override for sequencer injection - # @see https://github.com/zdennis/activerecord-import/blob/ba909fed5a4785fe9c7cce89e48e1242bb6804ea/lib/activerecord-import/import.rb#L558-L581 - def values_sql_for_columns_and_attributes_with_turntable(columns, array_of_attributes) + # @see https://github.com/zdennis/activerecord-import/blob/b325ebb644160a09db6e269e414f33561cb21272/lib/activerecord-import/import.rb#L661-L689 + private def values_sql_for_columns_and_attributes(columns, array_of_attributes) connection_memo = connection + type_caster_memo = type_caster if respond_to?(:type_caster) + array_of_attributes.map do |arr| - my_values = arr.each_with_index.map do |val,j| + my_values = arr.each_with_index.map do |val, j| column = columns[j] # be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly @@ -26,13 +20,7 @@ def values_sql_for_columns_and_attributes_with_turntable(columns, array_of_attri connection_memo.next_value_for_sequence(sequence_name) end elsif column - if respond_to?(:type_caster) && type_caster.respond_to?(:type_cast_for_database) # Rails 5.0 and higher - connection_memo.quote(type_caster.type_cast_for_database(column.name, val)) - elsif column.respond_to?(:type_cast_from_user) # Rails 4.2 and higher - connection_memo.quote(column.type_cast_from_user(val), column) - else # Rails 3.1, 3.2, and 4.1 - connection_memo.quote(column.type_cast(val), column) - end + connection_memo.quote(type_caster_memo.type_cast_for_database(column.name, val)) end end "(#{my_values.join(',')})" @@ -41,10 +29,10 @@ def values_sql_for_columns_and_attributes_with_turntable(columns, array_of_attri end begin - require 'activerecord-import' - require 'activerecord-import/base' - (class << ActiveRecord::Base; self; end).send(:include, ActiverecordImportExt) - rescue LoadError + require "activerecord-import" + require "activerecord-import/base" + (class << ActiveRecord::Base; self; end).prepend(ActiverecordImportExt) + rescue LoadError # rubocop:disable Lint/HandleExceptions end end end diff --git a/lib/active_record/turntable/active_record_ext/acts_as_archive_extension.rb b/lib/active_record/turntable/active_record_ext/acts_as_archive_extension.rb index c7ab021d..9e1ca68e 100644 --- a/lib/active_record/turntable/active_record_ext/acts_as_archive_extension.rb +++ b/lib/active_record/turntable/active_record_ext/acts_as_archive_extension.rb @@ -1,22 +1,31 @@ -begin - require 'acts_as_archive' - # acts_as_archive extension - class ActsAsArchive - class << self - # @note use the same shard which `from` shard using - def move_with_turntable(config, where, merge_options={}) - if [config[:to], config[:from]].all? { |k| k.try(:turntable_enabled?) } - current_shard = config[:from].connection.current_shard.name.to_sym - config[:to].connection.with_shard(current_shard) { - move_without_turntable(config, where, merge_options) - } - else - move_without_turntable(config, where, merge_options) +module ActiveRecord::Turntable + module ActiveRecordExt + module ActsAsArchiveExt + def self.prepended(base) + class << base + prepend ClassMethods end end - alias_method_chain :move, :turntable + module ClassMethods + # @note use the same shard which `from` shard using + def move(config, where, merge_options = {}) + if [config[:to], config[:from]].all? { |k| k.try(:turntable_enabled?) } + current_shard = config[:from].connection.current_shard.name.to_sym + config[:to].connection.with_shard(current_shard) { + super + } + else + super + end + end + end + end + + begin + require "acts_as_archive" + ActsAsArchive.prepend ActsAsArchiveExt + rescue LoadError # rubocop:disable Lint/HandleExceptions end end -rescue LoadError end diff --git a/lib/active_record/turntable/active_record_ext/association.rb b/lib/active_record/turntable/active_record_ext/association.rb index 6ace138b..73356857 100644 --- a/lib/active_record/turntable/active_record_ext/association.rb +++ b/lib/active_record/turntable/active_record_ext/association.rb @@ -6,7 +6,7 @@ module Association include ShardingCondition def self.prepended(mod) - ActiveRecord::Associations::Builder::Association.valid_options << :foreign_shard_key + ActiveRecord::Associations::Builder::Association::VALID_OPTIONS << :foreign_shard_key end protected @@ -16,9 +16,9 @@ def target_scope return super unless should_use_shard_key? scope = klass.where( - klass.turntable_shard_key => - owner.send(foreign_shard_key) - ) + klass.turntable_shard_key => + owner.send(foreign_shard_key) + ) super.merge!(scope) end diff --git a/lib/active_record/turntable/active_record_ext/association_preloader.rb b/lib/active_record/turntable/active_record_ext/association_preloader.rb index 0ccef8f2..b23fd6f3 100644 --- a/lib/active_record/turntable/active_record_ext/association_preloader.rb +++ b/lib/active_record/turntable/active_record_ext/association_preloader.rb @@ -1,38 +1,18 @@ -require 'active_record/associations/preloader/association' +require "active_record/associations/preloader/association" module ActiveRecord::Turntable module ActiveRecordExt module AssociationPreloader - extend ActiveSupport::Concern - - included do - alias_method_chain :records_for, :turntable - end + include ShardingCondition # @note Override to add sharding condition on preload - def records_for_with_turntable(ids) - returning_scope = records_for_without_turntable(ids) + def records_for(ids) + returning_scope = super if should_use_shard_key? returning_scope = returning_scope.where(klass.turntable_shard_key => owners.map(&foreign_shard_key.to_sym).uniq) end returning_scope end - - private - - def foreign_shard_key - options[:foreign_shard_key] || model.turntable_shard_key - end - - def should_use_shard_key? - sharded_by_same_key? || !!options[:foreign_shard_key] - end - - def sharded_by_same_key? - model.turntable_enabled? && - klass.turntable_enabled? && - model.turntable_shard_key == klass.turntable_shard_key - end end end end diff --git a/lib/active_record/turntable/active_record_ext/clever_load.rb b/lib/active_record/turntable/active_record_ext/clever_load.rb index 068e79d4..4182d603 100644 --- a/lib/active_record/turntable/active_record_ext/clever_load.rb +++ b/lib/active_record/turntable/active_record_ext/clever_load.rb @@ -5,7 +5,7 @@ module CleverLoad included do class << ActiveRecord::Base - delegate :clever_load!, :to => :all + delegate :clever_load!, to: :all end end @@ -13,7 +13,7 @@ def clever_load!(association_name) # load records records = self.to_a klass = records.first.class - association_key = Util.ar42_or_later? ? association_name.to_s : association_name + association_key = association_name.to_s reflection = klass.reflections[association_key] if reflection @@ -30,11 +30,11 @@ def clever_load!(association_name) self.each do |obj| matched_object = case reflection.macro when :has_one - foreign_objects.find {|fo| + foreign_objects.find { |fo| obj.send(reflection.association_primary_key) == fo.send(reflection.foreign_key) } when :belongs_to - foreign_objects.find {|fo| + foreign_objects.find { |fo| obj.send(reflection.foreign_key) == fo.send(reflection.association_primary_key) } end diff --git a/lib/active_record/turntable/active_record_ext/connection_handler_extension.rb b/lib/active_record/turntable/active_record_ext/connection_handler_extension.rb index bc843458..8746e76e 100644 --- a/lib/active_record/turntable/active_record_ext/connection_handler_extension.rb +++ b/lib/active_record/turntable/active_record_ext/connection_handler_extension.rb @@ -1,31 +1,27 @@ module ActiveRecord::Turntable module ActiveRecordExt module ConnectionHandlerExtension - extend ActiveSupport::Concern - - included do - alias_method_chain :pool_for, :turntable - end - - private - # @note Override not to establish_connection destroy existing connection pool proxy object - def pool_for_with_turntable(owner) - owner_to_pool.fetch(owner.name) { - if ancestor_pool = pool_from_any_process_for(owner) + def retrieve_connection_pool(spec_name) + owner_to_pool.fetch(spec_name) do + # Check if a connection was previously established in an ancestor process, + # which may have been forked. + if ancestor_pool = pool_from_any_process_for(spec_name) if ancestor_pool.is_a?(ActiveRecord::ConnectionAdapters::ConnectionPool) # A connection was established in an ancestor process that must have # subsequently forked. We can't reuse the connection, but we can copy # the specification and establish a new connection with it. - establish_connection owner, ancestor_pool.spec + establish_connection(ancestor_pool.spec).tap do |pool| + pool.schema_cache = ancestor_pool.schema_cache if ancestor_pool.schema_cache + end else # Use same PoolProxy object - owner_to_pool[owner.name] = ancestor_pool + owner_to_pool[spec_name] = ancestor_pool end else - owner_to_pool[owner.name] = nil + owner_to_pool[spec_name] = nil end - } + end end end end diff --git a/lib/active_record/turntable/active_record_ext/database_tasks.rb b/lib/active_record/turntable/active_record_ext/database_tasks.rb index dff045c4..79b54c55 100644 --- a/lib/active_record/turntable/active_record_ext/database_tasks.rb +++ b/lib/active_record/turntable/active_record_ext/database_tasks.rb @@ -1,24 +1,24 @@ -require 'active_record/tasks/database_tasks' +require "active_record/tasks/database_tasks" module ActiveRecord module Tasks module DatabaseTasks def create_all_turntable_cluster - each_local_turntable_cluster_configuration { |name, configuration| + each_local_turntable_cluster_configuration { |_name, configuration| puts "[turntable] *** executing to database: #{configuration['database']}" create configuration } end def drop_all_turntable_cluster - each_local_turntable_cluster_configuration { |name, configuration| + each_local_turntable_cluster_configuration { |_name, configuration| puts "[turntable] *** executing to database: #{configuration['database']}" drop configuration } end def create_current_turntable_cluster(environment = env) - each_current_turntable_cluster_configuration(true, environment) { |name, configuration| + each_current_turntable_cluster_configuration(environment) { |_name, configuration| puts "[turntable] *** executing to database: #{configuration['database']}" create configuration } @@ -26,39 +26,40 @@ def create_current_turntable_cluster(environment = env) end def drop_current_turntable_cluster(environment = env) - each_current_turntable_cluster_configuration(true, environment) { |name, configuration| + each_current_turntable_cluster_configuration(environment) { |_name, configuration| puts "[turntable] *** executing to database: #{configuration['database']}" drop configuration } end - def each_current_turntable_cluster_connected(with_test = false, environment = env) - each_current_turntable_cluster_configuration(with_test, environment) do |name, configuration| + def each_current_turntable_cluster_connected(environment) + old_connection_pool = ActiveRecord::Base.connection_pool + each_current_turntable_cluster_configuration(environment) do |name, configuration| ActiveRecord::Base.clear_active_connections! ActiveRecord::Base.establish_connection(configuration) ActiveRecord::Migration.current_shard = name yield(name, configuration) end ActiveRecord::Base.clear_active_connections! - ActiveRecord::Base.establish_connection environment.to_sym + ActiveRecord::Base.establish_connection old_connection_pool.spec.config end - def each_current_turntable_cluster_configuration(with_test = false, environment = env) + def each_current_turntable_cluster_configuration(environment) environments = [environment] - environments << 'test' if with_test and environment == 'development' + environments << "test" if environment == "development" current_turntable_cluster_configurations(*environments).each do |name, configuration| - yield(name, configuration) unless configuration['database'].blank? + yield(name, configuration) unless configuration["database"].blank? end end def each_local_turntable_cluster_configuration ActiveRecord::Base.configurations.keys.each do |k| current_turntable_cluster_configurations(k).each do |name, configuration| - next if configuration['database'].blank? + next if configuration["database"].blank? if local_database?(configuration) - yield(name,configuration) + yield(name, configuration) else $stderr.puts "This task only modifies local databases. #{configuration['database']} is on a remote host." end @@ -70,8 +71,9 @@ def current_turntable_cluster_configurations(*environments) configurations = [] environments.each do |environ| config = ActiveRecord::Base.configurations[environ] + next unless config %w(shards seq).each do |name| - configurations += config[name].to_a + configurations += config[name].to_a if config.has_key?(name) end end configurations diff --git a/lib/active_record/turntable/active_record_ext/fixtures.rb b/lib/active_record/turntable/active_record_ext/fixtures.rb index 9bcaf276..b1d0455d 100644 --- a/lib/active_record/turntable/active_record_ext/fixtures.rb +++ b/lib/active_record/turntable/active_record_ext/fixtures.rb @@ -1,19 +1,16 @@ # # force TestFixtures to begin transaction with all shards. # -require 'active_record/fixtures' +require "active_record/fixtures" module ActiveRecord class FixtureSet extend ActiveRecord::Turntable::Util + # rubocop:disable Style/MultilineMethodCallBraceLayout def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) fixture_set_names = Array(fixture_set_names).map(&:to_s) - class_names = if ar41_or_later? - ClassCache.new class_names, config - else - class_names = class_names.stringify_keys - end + class_names = ClassCache.new class_names, config # FIXME: Apparently JK uses this. connection = block_given? ? yield : ActiveRecord::Base.connection @@ -27,12 +24,8 @@ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {} fixtures_map = {} fixture_sets = files_to_read.map do |fs_name| - klass = if ar41_or_later? - class_names[fs_name] - else - class_names[fs_name] || default_fixture_model_name(fs_name) - end - conn = klass.is_a?(String) ? connection : klass.connection + klass = class_names[fs_name] + conn = klass ? klass.connection : connection fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new conn, fs_name, @@ -40,19 +33,19 @@ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {} ::File.join(fixtures_directory, fs_name)) end - if ar42_or_later? - update_all_loaded_fixtures fixtures_map - else - all_loaded_fixtures.update(fixtures_map) - end + update_all_loaded_fixtures fixtures_map - ActiveRecord::Base.force_transaction_all_shards!(:requires_new => true) do + ActiveRecord::Base.force_transaction_all_shards!(requires_new: true) do + deleted_tables = Hash.new { |h, k| h[k] = Set.new } fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows table_rows.each_key do |table| - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + unless deleted_tables[conn].include? table + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" + end + deleted_tables[conn] << table end table_rows.each do |fixture_set_name, rows| @@ -60,11 +53,9 @@ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {} conn.insert_fixture(row, fixture_set_name) end end - end - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - fixture_sets.each do |fs| + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) connection.reset_pk_sequence!(fs.table_name) end end @@ -75,16 +66,14 @@ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {} end cached_fixtures(connection, fixture_set_names) end + # rubocop:enable Style/MultilineMethodCallLayout end module TestFixtures - include ActiveRecord::Turntable::Util - + # rubocop:disable Style/ClassVars, Style/RedundantException def setup_fixtures(config = ActiveRecord::Base) - return unless !ActiveRecord::Base.configurations.blank? - if pre_loaded_fixtures && !use_transactional_fixtures - raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' + raise RuntimeError, "pre_loaded_fixtures requires use_transactional_fixtures" end @fixture_cache = {} @@ -96,7 +85,7 @@ def setup_fixtures(config = ActiveRecord::Base) if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - @loaded_fixtures = turntable_load_fixtures(config) + @loaded_fixtures = load_fixtures(config) @@already_loaded_fixtures[self.class] = @loaded_fixtures end ActiveRecord::Base.force_connect_all_shards! @@ -106,36 +95,19 @@ def setup_fixtures(config = ActiveRecord::Base) end # Load fixtures for every test. else - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache @@already_loaded_fixtures[self.class] = nil - @loaded_fixtures = turntable_load_fixtures(config) + @loaded_fixtures = load_fixtures(config) end # Instantiate fixtures for every test if requested. - turntable_instantiate_fixtures(config) if use_instantiated_fixtures + instantiate_fixtures if use_instantiated_fixtures end + # rubocop:enable Style/ClassVars, Style/RedundantException def enlist_fixture_connections ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection) + ActiveRecord::Base.turntable_connections.values.map(&:connection) end - - private - - def turntable_load_fixtures(config) - if ar41_or_later? - load_fixtures(config) - else - load_fixtures - end - end - - def turntable_instantiate_fixtures(config) - if ar41_or_later? - instantiate_fixtures(config) - else - instantiate_fixtures - end - end end end diff --git a/lib/active_record/turntable/active_record_ext/locking_optimistic.rb b/lib/active_record/turntable/active_record_ext/locking_optimistic.rb index 1a286b02..607b9535 100644 --- a/lib/active_record/turntable/active_record_ext/locking_optimistic.rb +++ b/lib/active_record/turntable/active_record_ext/locking_optimistic.rb @@ -1,160 +1,53 @@ module ActiveRecord::Turntable module ActiveRecordExt module LockingOptimistic - # @note Override to add sharding condition on optimistic locking - ::ActiveRecord::Locking::Optimistic.class_eval do - - ar_version = ActiveRecord::VERSION::STRING - if Util.earlier_than_ar41? - method_name = Util.ar_version_earlier_than?("4.0.6") ? "update_record" : "_update_record" - - class_eval <<-EOD - def #{method_name}(attribute_names = @attributes.keys) #:nodoc: - return super unless locking_enabled? - return 0 if attribute_names.empty? - - klass = self.class - lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - increment_lock - - attribute_names += [lock_col] - attribute_names.uniq! - - begin - relation = self.class.unscoped - - condition_scope = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ) - if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - condition_scope = condition_scope.where( - relation.table[klass.turntable_shard_key].eq( - self.class.quote_value(self.send(turntable_shard_key), column_for_attribute(klass.turntable_shard_key)) - ) - ) - end - stmt = condition_scope.arel.compile_update(arel_attributes_with_values_for_update(attribute_names)) - - affected_rows = self.class.connection.update stmt - - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") - end - - affected_rows - - # If something went wrong, revert the version. - rescue Exception - send(lock_col + '=', previous_lock_value) - raise - end + ::ActiveRecord::Locking::Optimistic.class_eval <<-EOD + private + # @note Override to add sharding condition on optimistic locking + def _update_record(attribute_names = self.attribute_names) #:nodoc: + return super unless locking_enabled? + return 0 if attribute_names.empty? + + klass = self.class + lock_col = self.class.locking_column + previous_lock_value = send(lock_col).to_i + increment_lock + + attribute_names += [lock_col] + attribute_names.uniq! + + begin + relation = self.class.unscoped + + condition_scope = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ) + if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s + condition_scope = condition_scope.where( + klass.turntable_shard_key => self.send(klass.turntable_shard_key) + ) end - EOD - elsif Util.earlier_than_ar42? - method_name = Util.ar_version_earlier_than?("4.1.2") ? "update_record" : "_update_record" - - class_eval <<-EOD - def _update_record(attribute_names = @attributes.keys) #:nodoc: - return super unless locking_enabled? - return 0 if attribute_names.empty? - - klass = self.class - lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - increment_lock - - attribute_names += [lock_col] - attribute_names.uniq! - begin - relation = self.class.unscoped + affected_rows = condition_scope.update_all( + attributes_for_update(attribute_names).map do |name| + [name, _read_attribute(name)] + end.to_h + ) - condition_scope = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ) - if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - condition_scope = condition_scope.where( - relation.table[klass.turntable_shard_key].eq( - self.class.quote_value(self.send(turntable_shard_key), column_for_attribute(klass.turntable_shard_key)) - ) - ) - end - stmt = condition_scope.arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key - ) - - affected_rows = self.class.connection.update stmt - - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") - end - - affected_rows - - # If something went wrong, revert the version. - rescue Exception - send(lock_col + '=', previous_lock_value) - raise - end + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError.new(self, "update") end - EOD - else - class_eval <<-EOD - def _update_record(attribute_names = self.attribute_names) #:nodoc: - return super unless locking_enabled? - return 0 if attribute_names.empty? - - klass = self.class - lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - increment_lock - - attribute_names += [lock_col] - attribute_names.uniq! - begin - relation = self.class.unscoped + affected_rows - condition_scope = relation.where( - self.class.primary_key => id, - lock_col => previous_lock_value, - ) - - if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - condition_scope = condition_scope.where( - relation.table[klass.turntable_shard_key].eq( - self.class.quote_value(self.send(turntable_shard_key), column_for_attribute(klass.turntable_shard_key)) - ) - ) - end - - affected_rows = condition_scope.update_all( - Hash[attributes_for_update(attribute_names).map do |name| - [name, _read_attribute(name)] - end] - ) - - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") - end - - affected_rows - - # If something went wrong, revert the version. - rescue Exception - send(lock_col + '=', previous_lock_value) - raise - end - end - EOD + # If something went wrong, revert the version. + rescue Exception + send(lock_col + '=', previous_lock_value) + raise + end end - end + EOD end end end diff --git a/lib/active_record/turntable/active_record_ext/log_subscriber.rb b/lib/active_record/turntable/active_record_ext/log_subscriber.rb index 27aeece7..f1f2c488 100644 --- a/lib/active_record/turntable/active_record_ext/log_subscriber.rb +++ b/lib/active_record/turntable/active_record_ext/log_subscriber.rb @@ -1,46 +1,21 @@ -require 'active_record/log_subscriber' +require "active_record/log_subscriber" module ActiveRecord::Turntable module ActiveRecordExt module LogSubscriber - extend ActiveSupport::Concern - - included do - alias_method_chain :sql, :turntable - end - - protected - - # @note Override to add shard name logging - def sql_with_turntable(event) - self.class.runtime += event.duration - return unless logger.debug? - + # @note prepend to add shard name logging + def sql(event) payload = event.payload - return if ActiveRecord::LogSubscriber::IGNORE_PAYLOAD_NAMES.include?(payload[:name]) - - name = "#{payload[:name]} (#{event.duration.round(1)}ms)" - shard = '[Shard: %s]' % (event.payload[:turntable_shard_name] ? event.payload[:turntable_shard_name] : nil) - sql = payload[:sql].squeeze(' ') - binds = nil - - unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].map { |col,v| - render_bind(col, v) - }.inspect + if self.class::IGNORE_PAYLOAD_NAMES.include?(payload[:name]) + self.class.runtime += event.duration + return end - if odd? - name = color(name, ActiveRecord::LogSubscriber::CYAN, true) - shard = color(shard, ActiveRecord::LogSubscriber::CYAN, true) - sql = color(sql, nil, true) - else - name = color(name, ActiveRecord::LogSubscriber::MAGENTA, true) - shard = color(shard, ActiveRecord::LogSubscriber::MAGENTA, true) + if payload[:turntable_shard_name] + payload[:name] = "#{payload[:name]} [Shard: #{payload[:turntable_shard_name]}]" end - - debug " #{name} #{shard} #{sql}#{binds}" + super end end end diff --git a/lib/active_record/turntable/active_record_ext/migration_proxy.rb b/lib/active_record/turntable/active_record_ext/migration_proxy.rb index 00635d04..894c1ed1 100644 --- a/lib/active_record/turntable/active_record_ext/migration_proxy.rb +++ b/lib/active_record/turntable/active_record_ext/migration_proxy.rb @@ -1,4 +1,4 @@ -require 'active_record/migration' +require "active_record/migration" module ActiveRecord class MigrationProxy diff --git a/lib/active_record/turntable/active_record_ext/persistence.rb b/lib/active_record/turntable/active_record_ext/persistence.rb index 64b294d4..be78445b 100644 --- a/lib/active_record/turntable/active_record_ext/persistence.rb +++ b/lib/active_record/turntable/active_record_ext/persistence.rb @@ -6,10 +6,9 @@ module Persistence ::ActiveRecord::Persistence.class_eval do # @note Override to add sharding scope on reloading def reload(options = nil) - clear_aggregation_cache - clear_association_cache + self.class.connection.clear_query_cache - finder_scope = if turntable_enabled? and self.class.primary_key != self.class.turntable_shard_key.to_s + finder_scope = if turntable_enabled? && self.class.primary_key != self.class.turntable_shard_key.to_s self.class.unscoped.where(self.class.turntable_shard_key => self.send(turntable_shard_key)) else self.class.unscoped @@ -17,101 +16,71 @@ def reload(options = nil) fresh_object = if options && options[:lock] - finder_scope.lock.find(id) + finder_scope.lock(options[:lock]).find(id) else finder_scope.find(id) end - if Util.ar42_or_later? - @attributes = fresh_object.instance_variable_get('@attributes') - else - @attributes.update(fresh_object.instance_variable_get('@attributes')) - - @column_types = self.class.column_types - @column_types_override = fresh_object.instance_variable_get('@column_types_override') - @attributes_cache = {} - end + @attributes = fresh_object.instance_variable_get("@attributes") @new_record = false self end # @note Override to add sharding scope on `touch` - if Util.ar42_or_later? - def touch(*names) - raise ActiveRecordError, "cannot touch on a new record object" unless persisted? - - attributes = timestamp_attributes_for_update_in_model - attributes.concat(names) - - unless attributes.empty? - current_time = current_time_from_proper_timezone - changes = {} - - attributes.each do |column| - column = column.to_s - changes[column] = write_attribute(column, current_time) - end - - changes[self.class.locking_column] = increment_lock if locking_enabled? + # rubocop:disable Style/UnlessElse + def touch(*names, time: nil) + raise ActiveRecord::ActiveRecordError, "cannot touch on a new record object" unless persisted? - clear_attribute_changes(changes.keys) - primary_key = self.class.primary_key + time ||= current_time_from_proper_timezone + attributes = timestamp_attributes_for_update_in_model + attributes.concat(names) - finder_scope = if turntable_enabled? and primary_key != self.class.turntable_shard_key.to_s - self.class.unscoped.where(self.class.turntable_shard_key => self.send(turntable_shard_key)) - else - self.class.unscoped - end + unless attributes.empty? + changes = {} - finder_scope.where(primary_key => self[primary_key]).update_all(changes) == 1 - else - true + attributes.each do |column| + column = column.to_s + changes[column] = write_attribute(column, time) end - end - else - def touch(name = nil) - raise ActiveRecordError, "can not touch on a new record object" unless persisted? - - attributes = timestamp_attributes_for_update_in_model - attributes << name if name - - unless attributes.empty? - current_time = current_time_from_proper_timezone - changes = {} - attributes.each do |column| - column = column.to_s - changes[column] = write_attribute(column, current_time) - end - - changes[self.class.locking_column] = increment_lock if locking_enabled? - - @changed_attributes.except!(*changes.keys) - primary_key = self.class.primary_key + clear_attribute_changes(changes.keys) + primary_key = self.class.primary_key + scope = if turntable_enabled? && primary_key != self.class.turntable_shard_key.to_s + self.class.unscoped.where(self.class.turntable_shard_key => _read_attribute(turntable_shard_key)) + else + self.class.unscoped + end + scope = scope.where(primary_key => _read_attribute(primary_key)) + + if locking_enabled? + locking_column = self.class.locking_column + scope = scope.where(locking_column => _read_attribute(locking_column)) + changes[locking_column] = increment_lock + end - finder_scope = if turntable_enabled? and primary_key != self.class.turntable_shard_key.to_s - self.class.unscoped.where(self.class.turntable_shard_key => self.send(turntable_shard_key)) - else - self.class.unscoped - end + result = scope.update_all(changes) == 1 - finder_scope.where(primary_key => self[primary_key]).update_all(changes) == 1 - else - true + if !result && locking_enabled? + raise ActiveRecord::StaleObjectError.new(self, "touch") end + + result + else + true end end + # rubocop:enable Style/UnlessElse # @note Override to add sharding scope on `update_columns` def update_columns(attributes) - raise ActiveRecordError, "cannot update a new record" if new_record? - raise ActiveRecordError, "cannot update a destroyed record" if destroyed? + raise ActiveRecord::ActiveRecordError, "cannot update a new record" if new_record? + raise ActiveRecord::ActiveRecordError, "cannot update a destroyed record" if destroyed? attributes.each_key do |key| verify_readonly_attribute(key.to_s) end - update_scope = if turntable_enabled? and self.class.primary_key != self.class.turntable_shard_key.to_s + update_scope = if turntable_enabled? && self.class.primary_key != self.class.turntable_shard_key.to_s self.class.unscoped.where(self.class.turntable_shard_key => self.send(turntable_shard_key)) else self.class.unscoped @@ -128,91 +97,30 @@ def update_columns(attributes) private - # @note Override to add sharding scope on destroying - if Util.ar42_or_later? + # @note Override to add sharding scope on destroying def relation_for_destroy - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = self.class.connection.substitute_at(column, 0) - klass = self.class - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - relation.bind_values = [[column, id]] + klass = self.class + relation = klass.unscoped.where(klass.primary_key => id) - if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - shard_key_column = klass.columns_hash[klass.turntable_shard_key] - shard_key_substitute = klass.connection.substitute_at(shard_key_column) - - relation = relation.where(self.class.arel_table[klass.turntable_shard_key].eq(shard_key_substitute)) - relation.bind_values << [shard_key_column, self[klass.turntable_shard_key]] - end - relation - end - else - def relation_for_destroy - pk = self.class.primary_key - column = self.class.columns_hash[pk] - substitute = self.class.connection.substitute_at(column, 0) - klass = self.class - - relation = self.class.unscoped.where( - self.class.arel_table[pk].eq(substitute)) - if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - relation = relation.where(klass.turntable_shard_key => self.send(turntable_shard_key)) + if klass.turntable_enabled? && klass.primary_key != klass.turntable_shard_key.to_s + relation = relation.where(klass.turntable_shard_key => self[klass.turntable_shard_key]) end - relation.bind_values = [[column, id]] relation end - end - # @note Override to add sharding scope on updating - if Util.earlier_than_ar41? - method_name = Util.ar_version_earlier_than?("4.0.6") ? "update_record" : "_update_record" - class_eval <<-EOD - def #{method_name}(attribute_names = @attributes.keys) - attributes_with_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_with_values.empty? - 0 - else - klass = self.class - column_hash = klass.connection.schema_cache.columns_hash klass.table_name - db_columns_with_values = attributes_with_values.map { |attr,value| - real_column = column_hash[attr.name] - [real_column, value] - } - bind_attrs = attributes_with_values.dup - bind_attrs.keys.each_with_index do |column, i| - real_column = db_columns_with_values[i].first - bind_attrs[column] = klass.connection.substitute_at(real_column, i) - end - condition_scope = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id_was || id)) - if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - condition_scope = condition_scope.where(klass.turntable_shard_key => self.send(turntable_shard_key)) - end - stmt = condition_scope.arel.compile_update(bind_attrs) - klass.connection.update stmt, 'SQL', db_columns_with_values - end - end - EOD - else - method_name = Util.ar_version_earlier_than?("4.1.2") ? "update_record" : "_update_record" - attributes_method_name = Util.ar42_or_later? ? "self.attribute_names" : "@attributes.keys" - class_eval <<-EOD - def #{method_name}(attribute_names = #{attributes_method_name}) - klass = self.class - attributes_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_values.empty? - 0 - else - scope = if klass.turntable_enabled? and klass.primary_key != klass.turntable_shard_key.to_s - klass.unscoped.where(klass.turntable_shard_key => self.send(turntable_shard_key)) - end - klass.unscoped.#{method_name} attributes_values, id, id_was, scope - end + # @note Override to add sharding scope on updating + def _update_record(attribute_names = self.attribute_names) + klass = self.class + attributes_values = arel_attributes_with_values_for_update(attribute_names) + if attributes_values.empty? + 0 + else + scope = if klass.turntable_enabled? && (klass.primary_key != klass.turntable_shard_key.to_s) + klass.unscoped.where(klass.turntable_shard_key => self.send(turntable_shard_key)) + end + klass.unscoped._update_record attributes_values, id, id_was, scope end - EOD - end + end end end end diff --git a/lib/active_record/turntable/active_record_ext/relation.rb b/lib/active_record/turntable/active_record_ext/relation.rb index e43e02ff..74cb1a12 100644 --- a/lib/active_record/turntable/active_record_ext/relation.rb +++ b/lib/active_record/turntable/active_record_ext/relation.rb @@ -1,56 +1,28 @@ module ActiveRecord::Turntable module ActiveRecordExt module Relation - extend ActiveSupport::Concern - - included do - if Util.ar41_or_later? - if Util.ar_version_earlier_than?('4.1.2') - alias_method :_update_record_without_turntable, :update_record - alias_method :update_record, :_update_record_with_turntable - else - alias_method_chain :_update_record, :turntable - end - end - end - # @note Override to add sharding scope on updating - if Util.ar42_or_later? - def _update_record_with_turntable(values, id, id_was, turntable_scope = nil) # :nodoc: - substitutes, binds = substitute_values values + def _update_record(values, id, id_was, turntable_scope = nil) # :nodoc: + substitutes, binds = substitute_values values - scope = @klass.unscoped + scope = @klass.unscoped - if @klass.finder_needs_type_condition? - scope.unscope!(where: @klass.inheritance_column) - end - - relation = scope.where(@klass.primary_key => (id_was || id)) - relation = relation.merge(turntable_scope) if turntable_scope - - bvs = binds + relation.bind_values - um = relation - .arel - .compile_update(substitutes, @klass.primary_key) - - @klass.connection.update( - um, - 'SQL', - bvs, - ) + if @klass.finder_needs_type_condition? + scope.unscope!(where: @klass.inheritance_column) end - else - def _update_record_with_turntable(values, id, id_was, turntable_scope = nil) # :nodoc: - substitutes, binds = substitute_values values - condition_scope = @klass.unscoped.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)) - condition_scope = condition_scope.merge(turntable_scope) if turntable_scope - um = condition_scope.arel.compile_update(substitutes, @klass.primary_key) - @klass.connection.update( - um, - 'SQL', - binds) - end + relation = scope.where(@klass.primary_key => (id_was || id)) + relation = relation.merge(turntable_scope) if turntable_scope + bvs = binds + relation.bound_attributes + um = relation. + arel. + compile_update(substitutes, @klass.primary_key) + + @klass.connection.update( + um, + "SQL", + bvs + ) end end end diff --git a/lib/active_record/turntable/active_record_ext/schema_dumper.rb b/lib/active_record/turntable/active_record_ext/schema_dumper.rb index 892fa712..43fde7c5 100644 --- a/lib/active_record/turntable/active_record_ext/schema_dumper.rb +++ b/lib/active_record/turntable/active_record_ext/schema_dumper.rb @@ -2,103 +2,38 @@ module ActiveRecord::Turntable module ActiveRecordExt module SchemaDumper - extend ActiveSupport::Concern - - included do - alias_method_chain :table, :turntable - end + SEQUENCE_TABLE_REGEXP = /\A(.*)_id_seq\z/ private - # @note Override to dump database sequencer method - def table_with_turntable(table, stream) - columns = @connection.columns(table) - begin - tbl = StringIO.new - - # first dump primary key column - if @connection.respond_to?(:pk_and_sequence_for) - pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) - pk = @connection.primary_key(table) - end - - if table =~ /\A(.*)_id_seq\z/ - tbl.print " create_sequence_for #{remove_prefix_and_suffix($1).inspect}" - else - tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" - end - pkcol = columns.detect { |c| c.name == pk } - if pkcol - if pk != 'id' - tbl.print %Q(, primary_key: "#{pk}") - elsif pkcol.sql_type == 'bigint' - tbl.print ", id: :bigserial" - elsif pkcol.sql_type == 'uuid' - tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function.inspect}") - end - else - tbl.print ", id: false" - end - if Util.earlier_than_ar42? - tbl.print ", force: true" - else - tbl.print ", force: :cascade" + # @note Override to dump database sequencer method + def table(table, stream) + unless matchdata = table.match(SEQUENCE_TABLE_REGEXP) + return super end - tbl.puts " do |t|" - - # then dump all non-primary key columns - column_specs = columns.map do |column| - raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) - next if column.name == pk - @connection.column_spec(column, @types) - end.compact - - # find all migration keys used in this table - keys = @connection.migration_keys - # figure out the lengths for each column based on above keys - lengths = keys.map { |key| - column_specs.map { |spec| - spec[key] ? spec[key].length + 2 : 0 - }.max - } + begin + tbl = StringIO.new - # the string we're going to sprintf our values against, with standardized column widths - format_string = lengths.map{ |len| "%-#{len}s" } - - # find the max length for the 'type' column, which is special - type_length = column_specs.map{ |column| column[:type].length }.max - - # add column type definition to our format string - format_string.unshift " t.%-#{type_length}s " - - format_string *= '' + tbl.print " create_sequence_for #{remove_prefix_and_suffix(matchdata[1]).inspect}" + tbl.print ", force: :cascade" - column_specs.each do |colspec| - values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } - values.unshift colspec[:type] - tbl.print((format_string % values).gsub(/,\s*$/, '')) + table_options = @connection.table_options(table) + if table_options.present? + tbl.print ", #{format_options(table_options)}" + end tbl.puts - end - - tbl.puts " end" - tbl.puts - indexes(table, tbl) + tbl.rewind + stream.print tbl.read + rescue => e + stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" + stream.puts "# #{e.message}" + stream.puts + end - tbl.rewind - stream.print tbl.read - rescue => e - stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" - stream.puts "# #{e.message}" - stream.puts + stream end - - stream - end - end end end diff --git a/lib/active_record/turntable/active_record_ext/sequencer.rb b/lib/active_record/turntable/active_record_ext/sequencer.rb index f5169357..ec28a44f 100644 --- a/lib/active_record/turntable/active_record_ext/sequencer.rb +++ b/lib/active_record/turntable/active_record_ext/sequencer.rb @@ -1,23 +1,14 @@ module ActiveRecord::Turntable::ActiveRecordExt module Sequencer - extend ActiveSupport::Concern - - included do - include DatabaseStatements - alias_method_chain :prefetch_primary_key?, :turntable - end - - module DatabaseStatements - def default_sequence_name(table_name, pk = nil) - if ActiveRecord::Turntable::Sequencer.has_sequencer?(table_name) - ActiveRecord::Turntable::Sequencer.sequence_name(table_name, pk) - else - super - end + def default_sequence_name(table_name, pk = nil) + if ActiveRecord::Turntable::Sequencer.has_sequencer?(table_name) + ActiveRecord::Turntable::Sequencer.sequence_name(table_name, pk) + else + super end end - def prefetch_primary_key_with_turntable?(table_name = nil) + def prefetch_primary_key?(table_name = nil) ActiveRecord::Turntable::Sequencer.has_sequencer?(table_name) end diff --git a/lib/active_record/turntable/active_record_ext/transactions.rb b/lib/active_record/turntable/active_record_ext/transactions.rb index c32e411c..e2e19c9c 100644 --- a/lib/active_record/turntable/active_record_ext/transactions.rb +++ b/lib/active_record/turntable/active_record_ext/transactions.rb @@ -5,8 +5,8 @@ module Transactions def with_transaction_returning_status if self.class.turntable_enabled? status = nil - if self.new_record? and self.turntable_shard_key.to_s == self.class.primary_key and - self.id.nil? and self.class.connection.prefetch_primary_key?(self.class.table_name) + if self.new_record? && self.turntable_shard_key.to_s == self.class.primary_key && + self.id.nil? && self.class.connection.prefetch_primary_key?(self.class.table_name) self.id = self.class.connection.next_sequence_value(self.class.sequence_name) end self.class.connection.shards_transaction([self.turntable_shard]) do @@ -14,11 +14,7 @@ def with_transaction_returning_status begin status = yield rescue ActiveRecord::Rollback - if Util.ar42_or_later? - clear_transaction_record_state - else - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - end + clear_transaction_record_state status = nil end @@ -28,13 +24,22 @@ def with_transaction_returning_status else super end + + ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end end def add_to_transaction if self.class.turntable_enabled? - if self.turntable_shard.connection.add_transaction_record(self) - remember_transaction_record_state + if has_transactional_callbacks? + self.turntable_shard.connection.add_transaction_record(self) + else + sync_with_transaction_state + set_transaction_state(self.turntable_shard.connection.transaction_state) end + remember_transaction_record_state else super end diff --git a/lib/active_record/turntable/algorithm/modulo_algorithm.rb b/lib/active_record/turntable/algorithm/modulo_algorithm.rb index 0ec4f918..048ffc3b 100644 --- a/lib/active_record/turntable/algorithm/modulo_algorithm.rb +++ b/lib/active_record/turntable/algorithm/modulo_algorithm.rb @@ -6,7 +6,7 @@ def initialize(config) end def calculate(key) - @config["shards"][key % @config["shards"].size]["connection"] + @config[:shards][key % @config[:shards].size][:connection] rescue raise ActiveRecord::Turntable::CannotSpecifyShardError, "cannot specify shard for key:#{key}" end diff --git a/lib/active_record/turntable/algorithm/range_algorithm.rb b/lib/active_record/turntable/algorithm/range_algorithm.rb index 63d04d4c..b6efb80b 100644 --- a/lib/active_record/turntable/algorithm/range_algorithm.rb +++ b/lib/active_record/turntable/algorithm/range_algorithm.rb @@ -7,31 +7,30 @@ def initialize(config) def calculate(key) idx = calculate_idx(key) - @config["shards"][idx]["connection"] + @config[:shards][idx][:connection] rescue raise ActiveRecord::Turntable::CannotSpecifyShardError, "cannot specify shard for key:#{key}" end def calculate_idx(key) - @config["shards"].find_index {|h| h["less_than"] > key } + @config[:shards].find_index { |h| h[:less_than] > key } end # { connection_name => weight, ... } def calculate_used_shards_with_weight(sequence_value) - idx = calculate_idx(sequence_value) - last_connection = calculate(sequence_value) - shards = @config["shards"][0..idx] - weighted_hash = Hash.new {|h,k| h[k]=0} + current_shard_idx = calculate_idx(sequence_value) + shards = @config[:shards][0..current_shard_idx] + weighted_hash = Hash.new { |h, k| h[k] = 0 } prev_max = 0 - shards.each_with_index do |h,idx| - weighted_hash[h["connection"]] += if idx < shards.size - 1 - h["less_than"] - prev_max - 1 - else - sequence_value - prev_max - end - prev_max = h["less_than"] - 1 + shards.each_with_index do |h, idx| + weighted_hash[h[:connection]] += if idx < shards.size - 1 + h[:less_than] - prev_max - 1 + else + sequence_value - prev_max + end + prev_max = h[:less_than] - 1 end - return weighted_hash + weighted_hash end end end diff --git a/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb b/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb index cf890ac6..0d239fba 100644 --- a/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb +++ b/lib/active_record/turntable/algorithm/range_bsearch_algorithm.rb @@ -1,41 +1,40 @@ # -*- coding: utf-8 -*- -require 'bsearch' +require "bsearch" module ActiveRecord::Turntable::Algorithm class RangeBsearchAlgorithm < Base def initialize(config) @config = config - @config["shards"].sort_by! {|a| a["less_than"]} + @config[:shards].sort_by! { |a| a[:less_than] } end def calculate(key) idx = calculate_idx(key) - @config["shards"][idx]["connection"] + @config[:shards][idx][:connection] rescue raise ActiveRecord::Turntable::CannotSpecifyShardError, "cannot specify shard for key:#{key}" end def calculate_idx(key) - @config["shards"].bsearch_upper_boundary { |h| - h["less_than"] <=> key + @config[:shards].bsearch_upper_boundary { |h| + h[:less_than] <=> key } end # { connection_name => weight, ... } def calculate_used_shards_with_weight(sequence_value) - idx = calculate_idx(sequence_value) - last_connection = calculate(sequence_value) - shards = @config["shards"][0..idx] - weighted_hash = Hash.new {|h,k| h[k]=0} + current_shard_idx = calculate_idx(sequence_value) + shards = @config[:shards][0..current_shard_idx] + weighted_hash = Hash.new { |h, k| h[k] = 0 } prev_max = 0 - shards.each_with_index do |h,idx| - weighted_hash[h["connection"]] += if idx < shards.size - 1 - h["less_than"] - prev_max - 1 - else - sequence_value - prev_max - end - prev_max = h["less_than"] - 1 + shards.each_with_index do |h, idx| + weighted_hash[h[:connection]] += if idx < shards.size - 1 + h[:less_than] - prev_max - 1 + else + sequence_value - prev_max + end + prev_max = h[:less_than] - 1 end - return weighted_hash + weighted_hash end end end diff --git a/lib/active_record/turntable/base.rb b/lib/active_record/turntable/base.rb index ad66e17c..ac85b0b8 100644 --- a/lib/active_record/turntable/base.rb +++ b/lib/active_record/turntable/base.rb @@ -1,4 +1,4 @@ -require 'active_support/lazy_load_hooks' +require "active_support/lazy_load_hooks" module ActiveRecord::Turntable module Base @@ -6,14 +6,14 @@ module Base included do class_attribute :turntable_connections, :turntable_clusters, - :turntable_enabled, :turntable_sequencer_enabled + :turntable_enabled, :turntable_sequencer_enabled self.turntable_connections = {} self.turntable_clusters = {}.with_indifferent_access self.turntable_enabled = false self.turntable_sequencer_enabled = false class << self - delegate :shards_transaction, :with_all, :to => :connection + delegate :shards_transaction, :with_all, to: :connection end ActiveSupport.on_load(:turntable_config_loaded) do @@ -22,37 +22,52 @@ class << self include ClusterHelperMethods end + module ConnectionExtension + module ClassMethods + def connection_specification_name + return super unless turntable_enabled? + self.connection_specification_name = "turntable_pool_proxy::#{name}" + end + end + + def self.prepended(base) + class << base + prepend ClassMethods + end + end + end + module ClassMethods # @param [Symbol] cluster_name cluster name for this class # @param [Symbol] shard_key_name shard key attribute name # @param [Hash] options def turntable(cluster_name, shard_key_name, options = {}) class_attribute :turntable_shard_key, - :turntable_cluster, :turntable_cluster_name + :turntable_cluster, :turntable_cluster_name self.turntable_enabled = true self.turntable_cluster_name = cluster_name self.turntable_shard_key = shard_key_name self.turntable_cluster = self.turntable_clusters[cluster_name] ||= Cluster.new( - turntable_config[:clusters][cluster_name], - options - ) + turntable_config[:clusters][cluster_name], + options + ) + prepend ConnectionExtension turntable_replace_connection_pool end - def turntable_replace_connection_pool ch = connection_handler cp = ConnectionProxy.new(self, turntable_cluster) pp = PoolProxy.new(cp) - ch.class_to_pool.clear if defined?(ch.class_to_pool) - ch.send(:class_to_pool)[name] = ch.send(:owner_to_pool)[name] = pp + + ch.send(:owner_to_pool)[connection_specification_name] = pp end def initialize_clusters! turntable_config[:clusters].each do |name, spec| - self.turntable_clusters[name] ||= Cluster.new(spec, {}) + self.turntable_clusters[name] ||= Cluster.new(spec) end end @@ -67,9 +82,7 @@ def spec_for(config) end def clear_all_connections! - turntable_connections.values.each do |pool| - pool.disconnect! - end + turntable_connections.values.each(&:disconnect!) end def sequencer(sequence_name, *args) diff --git a/lib/active_record/turntable/cluster.rb b/lib/active_record/turntable/cluster.rb index 9f67480a..72d2662c 100644 --- a/lib/active_record/turntable/cluster.rb +++ b/lib/active_record/turntable/cluster.rb @@ -1,8 +1,7 @@ -require 'active_support/core_ext/hash/indifferent_access' +require "active_support/core_ext/hash/indifferent_access" module ActiveRecord::Turntable class Cluster - DEFAULT_CONFIG = { "shards" => [], "algorithm" => "range", @@ -15,19 +14,19 @@ def initialize(cluster_spec, options = {}) # setup sequencer seq = (@options[:seq] || @config[:seq]) - if seq - if seq.values.size > 0 && seq.values.first["seq_type"] == "mysql" + if seq + if seq.values.size > 0 && seq.values.first[:seq_type] == "mysql" @seq_shard = SeqShard.new(seq.values.first) end end # setup shards @config[:shards].each do |spec| - @shards[spec["connection"]] ||= Shard.new(spec) + @shards[spec[:connection]] ||= Shard.new(spec) end # setup algorithm - alg_name = "ActiveRecord::Turntable::Algorithm::#{@config["algorithm"].camelize}Algorithm" + alg_name = "ActiveRecord::Turntable::Algorithm::#{@config[:algorithm].camelize}Algorithm" @algorithm = alg_name.constantize.new(@config) end @@ -35,15 +34,13 @@ def seq @seq_shard end - def shards - @shards - end + attr_reader :shards def shard_for(key) @shards[@algorithm.calculate(key)] rescue raise ActiveRecord::Turntable::CannotSpecifyShardError, - "cannot select_shard for key:#{key}" + "cannot select_shard for key:#{key}" end def select_shard(key) @@ -65,7 +62,7 @@ def shards_transaction(shards = [], options = {}, in_recursion = false, &block) end else shard.connection.transaction(options) do - block.call + yield end end end @@ -82,12 +79,12 @@ def to_shard(shard_or_object) shards[shard_or_object] else raise ActiveRecord::Turntable::TurntableError, - "transaction cannot call to object: #{shard_or_object}" + "transaction cannot call to object: #{shard_or_object}" end end def weighted_shards(key = nil) - Hash[@algorithm.calculate_used_shards_with_weight(key).map do |k,v| + Hash[@algorithm.calculate_used_shards_with_weight(key).map do |k, v| [@shards[k], v] end] end diff --git a/lib/active_record/turntable/cluster_helper_methods.rb b/lib/active_record/turntable/cluster_helper_methods.rb index 5d6680fc..15634448 100644 --- a/lib/active_record/turntable/cluster_helper_methods.rb +++ b/lib/active_record/turntable/cluster_helper_methods.rb @@ -4,14 +4,14 @@ module ClusterHelperMethods included do ActiveSupport.on_load(:turntable_config_loaded) do - turntable_clusters.each do |name, cluster| + turntable_clusters.each do |name, _cluster| turntable_define_cluster_methods(name) end end end module ClassMethods - def force_transaction_all_shards!(options={}, &block) + def force_transaction_all_shards!(options = {}, &block) force_connect_all_shards! shards = turntable_connections.values shards += [ActiveRecord::Base.connection_pool] @@ -30,10 +30,12 @@ def recursive_transaction(pools, options, &block) end def force_connect_all_shards! - conf = configurations[Rails.env] - shards = {} - shards = shards.merge(conf["shards"]) if conf["shards"] - shards = shards.merge(conf["seq"]) if conf["seq"] + conf = configurations[::ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_sym] + return unless conf + + shards = HashWithIndifferentAccess.new + shards = shards.merge(conf[:shards]) if conf[:shards] + shards = shards.merge(conf[:seq]) if conf[:seq] shards.each do |name, config| turntable_connections[name] ||= ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec_for(config)) @@ -44,7 +46,7 @@ def weighted_random_shard_with(*klasses, &block) shards_weight = self.turntable_cluster.weighted_shards(self.current_sequence) sum = shards_weight.values.inject(&:+) idx = rand(sum) - shard, weight = shards_weight.find {|k,v| + shard = shards_weight.find { |_k, v| (idx -= v) < 0 } self.connection.with_recursive_shards(shard.name, *klasses, &block) diff --git a/lib/active_record/turntable/config.rb b/lib/active_record/turntable/config.rb index 9910ddaf..92bcfa48 100644 --- a/lib/active_record/turntable/config.rb +++ b/lib/active_record/turntable/config.rb @@ -1,5 +1,5 @@ -require 'active_support/lazy_load_hooks' -require 'active_support/core_ext/hash/indifferent_access' +require "active_support/lazy_load_hooks" +require "active_support/core_ext/hash/indifferent_access" module ActiveRecord::Turntable class Config @@ -14,7 +14,7 @@ def [](key) @config[key] end - def self.load!(config_file = ActiveRecord::Base.turntable_config_file, env = (defined?(Rails) ? Rails.env : 'development')) + def self.load!(config_file = ActiveRecord::Base.turntable_config_file, env = (defined?(Rails) ? Rails.env : "development")) instance.load!(config_file, env) end diff --git a/lib/active_record/turntable/connection_proxy.rb b/lib/active_record/turntable/connection_proxy.rb index d83fab01..b300fe91 100644 --- a/lib/active_record/turntable/connection_proxy.rb +++ b/lib/active_record/turntable/connection_proxy.rb @@ -1,10 +1,10 @@ -require 'active_record/turntable/connection_proxy/mixable' +require "active_record/turntable/connection_proxy/mixable" module ActiveRecord::Turntable class ConnectionProxy include Mixable # for expiring query cache - CLEAR_CACHE_METHODS = [:update, :insert, :delete, :exec_insert, :exec_update, :exec_delete, :insert_many] + CLEAR_CACHE_METHODS = [:update, :insert, :delete, :exec_insert, :exec_update, :exec_delete, :insert_many].freeze attr_reader :klass attr_writer :spec @@ -22,9 +22,9 @@ def initialize(klass, cluster, options = {}) delegate :shards_transaction, to: :cluster delegate :create_table, :rename_table, :drop_table, :add_column, :remove_colomn, - :change_column, :change_column_default, :rename_column, :add_index, - :remove_index, :initialize_schema_information, - :dump_schema_information, :execute_ignore_duplicate, to: :master_connection + :change_column, :change_column_default, :rename_column, :add_index, + :remove_index, :initialize_schema_information, + :dump_schema_information, :execute_ignore_duplicate, to: :master_connection def transaction(options = {}, &block) connection.transaction(options, &block) @@ -38,7 +38,7 @@ def cache end def enable_query_cache! - klass.turntable_connections.each do |k,v| + klass.turntable_connections.each do |_k, v| v.connection.enable_query_cache! end end @@ -48,25 +48,29 @@ def clear_query_cache_if_needed(method) end def clear_query_cache - klass.turntable_connections.each do |k,v| + klass.turntable_connections.each do |_k, v| v.connection.clear_query_cache end end + # rubocop:disable Style/MethodMissing def method_missing(method, *args, &block) clear_query_cache_if_needed(method) if shard_fixed? connection.send(method, *args, &block) elsif mixable?(method, *args) fader = @mixer.build_fader(method, *args, &block) - logger.debug { "[ActiveRecord::Turntable] Sending method: #{method}, " + - "sql: #{args.first}, " + - "shards: #{fader.shards_query_hash.keys.map(&:name)}" } + logger.debug { + "[ActiveRecord::Turntable] Sending method: #{method}, " \ + "sql: #{args.first}, " \ + "shards: #{fader.shards_query_hash.keys.map(&:name)}" + } fader.execute else connection.send(method, *args, &block) end end + # rubocop:enable Style/MethodMissing def respond_to_missing?(method, include_private = false) connection.send(:respond_to?, method, include_private) @@ -76,9 +80,7 @@ def to_sql(arel, binds = []) master.connection.to_sql(arel, binds) end - def cluster - @cluster - end + attr_reader :cluster def shards @cluster.shards @@ -113,7 +115,7 @@ def current_shard end def current_shard=(shard) - logger.debug { "Changing #{klass}'s shard to #{shard.name}"} + logger.debug { "Changing #{klass}'s shard to #{shard.name}" } current_shard_entry[object_id] = shard end @@ -136,7 +138,8 @@ def connection_pool def with_shard(shard) shard = cluster.to_shard(shard) - old_shard, old_fixed = current_shard, fixed_shard + old_shard = current_shard + old_fixed = fixed_shard self.current_shard = shard self.fixed_shard = shard yield @@ -197,7 +200,7 @@ def with_master(&block) end delegate :connected?, :automatic_reconnect, :automatic_reconnect=, :checkout_timeout, :dead_connection_timeout, - :spec, :connections, :size, :reaper, :table_exists?, to: :connection_pool + :spec, :connections, :size, :reaper, to: :connection_pool %w(columns columns_hash column_defaults primary_keys).each do |name| define_method(name.to_sym) do @@ -205,7 +208,7 @@ def with_master(&block) end end - %w(table_exists?).each do |name| + %w(data_source_exists?).each do |name| define_method(name.to_sym) do |*args| master.connection_pool.with_connection do |c| c.schema_cache.send(name.to_sym, *args) @@ -239,12 +242,12 @@ def spec private - def fixed_shard_entry - Thread.current[:turntable_fixed_shard] ||= ThreadSafe::Cache.new - end + def fixed_shard_entry + Thread.current[:turntable_fixed_shard] ||= ThreadSafe::Cache.new + end - def current_shard_entry - Thread.current[:turntable_current_shard] ||= ThreadSafe::Cache.new - end + def current_shard_entry + Thread.current[:turntable_current_shard] ||= ThreadSafe::Cache.new + end end end diff --git a/lib/active_record/turntable/connection_proxy/mixable.rb b/lib/active_record/turntable/connection_proxy/mixable.rb index 0e2adfea..247c4cfb 100644 --- a/lib/active_record/turntable/connection_proxy/mixable.rb +++ b/lib/active_record/turntable/connection_proxy/mixable.rb @@ -10,7 +10,7 @@ module Mixable def mixable?(method, *args) (method.to_s =~ METHODS_REGEXP && args.first !~ EXCLUDE_QUERY_REGEXP) || - (method.to_s == 'execute' && args.first =~ QUERY_REGEXP) + (method.to_s == "execute" && args.first =~ QUERY_REGEXP) end end end diff --git a/lib/active_record/turntable/helpers/test_helper.rb b/lib/active_record/turntable/helpers/test_helper.rb index 64fd5448..9c9db839 100644 --- a/lib/active_record/turntable/helpers/test_helper.rb +++ b/lib/active_record/turntable/helpers/test_helper.rb @@ -1,16 +1,16 @@ module ActiveRecord::Turntable module Helpers module TestHelper - # all shards - def FabricateAll(name, overrides={}, &block) + # Execute fabricate to all turntable shards + def FabricateAll(name, overrides = {}, &block) # rubocop:disable Style/MethodName obj = Fabrication::Fabricator.generate(name, { - :save => true - }, overrides, &block) + save: true, + }, overrides, &block) default_pool = obj.class.connection_pool connection_pools = obj.class.connection_handler.instance_variable_get(:@connection_pools) - ActiveRecord::Base.turntable_connections.each do |conn_name, conn| + ActiveRecord::Base.turntable_connections.each do |_conn_name, conn| new_obj = obj.dup connection_pools[new_obj.class.name] = conn new_obj.id = obj.id diff --git a/lib/active_record/turntable/master_shard.rb b/lib/active_record/turntable/master_shard.rb index 746482b1..8c0b4569 100644 --- a/lib/active_record/turntable/master_shard.rb +++ b/lib/active_record/turntable/master_shard.rb @@ -1,21 +1,27 @@ module ActiveRecord::Turntable class MasterShard < Shard def initialize(klass) - (klass and klass.connection_pool) or + (klass and original_connection_pool(klass)) or raise MasterShardNotConnected, "connection_pool is nil" @klass = klass - @name = 'master' + @name = "master" end def connection_pool if ActiveRecord::Base == @klass ActiveRecord::Base.connection_pool else - # use parentclass connection which is turntable disabled - klass = @klass.superclass + # use original parent class connection which is turntable disabled + original_connection_pool + end + end + + private + + def original_connection_pool(klass = @klass) candidate_connection_pool = nil - while !candidate_connection_pool - if klass == ActiveRecord::Base or !klass.turntable_enabled? + until candidate_connection_pool + if klass == ActiveRecord::Base || !klass.turntable_enabled? candidate_connection_pool = klass.connection_pool else klass = klass.superclass @@ -23,6 +29,5 @@ def connection_pool end candidate_connection_pool end - end end end diff --git a/lib/active_record/turntable/migration.rb b/lib/active_record/turntable/migration.rb index 76c4ac98..bc73e336 100644 --- a/lib/active_record/turntable/migration.rb +++ b/lib/active_record/turntable/migration.rb @@ -3,12 +3,11 @@ module ActiveRecord::Turntable::Migration included do extend ShardDefinition + prepend OverrideMethods class_attribute :target_shards, :current_shard - alias_method_chain :announce, :turntable - alias_method_chain :exec_migration, :turntable - ::ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, SchemaStatementsExt) - ::ActiveRecord::Migration::CommandRecorder.send(:include, CommandRecorder) - ::ActiveRecord::Migrator.send(:include, Migrator) + ::ActiveRecord::ConnectionAdapters::AbstractAdapter.include(SchemaStatementsExt) + ::ActiveRecord::Migration::CommandRecorder.include(CommandRecorder) + ::ActiveRecord::Migrator.prepend(Migrator) end module ShardDefinition @@ -16,13 +15,13 @@ def clusters(*cluster_names) config = ActiveRecord::Base.turntable_config (self.target_shards ||= []).concat( if cluster_names.first == :all - config['clusters'].map do |name, cluster_conf| - cluster_conf["shards"].map {|shard| shard["connection"]} + config[:clusters].map do |_name, cluster_conf| + cluster_conf[:shards].map { |shard| shard[:connection] } end else cluster_names.map do |cluster_name| - config['clusters'][cluster_name]["shards"].map do |shard| - shard["connection"] + config[:clusters][cluster_name][:shards].map do |shard| + shard[:connection] end end.flatten end @@ -34,34 +33,34 @@ def shards(*connection_names) end end - def target_shard?(shard_name) - target_shards.blank? or target_shards.include?(shard_name) - end + module OverrideMethods + def announce(message) + super("#{message} - Shard: #{current_shard}") + end - def announce_with_turntable(message) - announce_without_turntable("#{message} - Shard: #{current_shard}") - end + def exec_migration(*args) + super(*args) if target_shard?(current_shard) + end - def exec_migration_with_turntable(*args) - exec_migration_without_turntable(*args) if target_shard?(current_shard) + def target_shard?(shard_name) + target_shards.blank? or target_shards.include?(shard_name) + end end module SchemaStatementsExt - def create_sequence_for(table_name, options = { }) - options = options.merge(:id => false) + def create_sequence_for(table_name, options = {}) + options = options.merge(id: false) # TODO: pkname should be pulled from table definitions - pkname = "id" sequence_table_name = ActiveRecord::Turntable::Sequencer.sequence_name(table_name, "id") create_table(sequence_table_name, options) do |t| - t.integer :id, :limit => 8 + t.integer :id, limit: 8 end execute "INSERT INTO #{quote_table_name(sequence_table_name)} (`id`) VALUES (0)" end - def drop_sequence_for(table_name, options = { }) + def drop_sequence_for(table_name, options = {}) # TODO: pkname should be pulled from table definitions - pkname = "id" sequence_table_name = ActiveRecord::Turntable::Sequencer.sequence_name(table_name, "id") drop_table(sequence_table_name) end @@ -85,56 +84,49 @@ def rename_sequence_for(*args) private - def invert_create_sequence_for(args) - [:drop_sequence_for, args] - end + def invert_create_sequence_for(args) + [:drop_sequence_for, args] + end - def invert_rename_sequence_for(args) - [:rename_sequence_for, args.reverse] - end + def invert_rename_sequence_for(args) + [:rename_sequence_for, args.reverse] + end end module Migrator extend ActiveSupport::Concern - included do - klass = self - (class << klass; self; end).instance_eval { - [:up, :down, :run].each do |method_name| - original_method_alias = "_original_#{method_name}" - unless klass.respond_to?(original_method_alias) - alias_method original_method_alias, method_name - end - alias_method_chain method_name, :turntable - end - } + def self.prepended(base) + class << base + prepend ClassMethods + end end module ClassMethods - def up_with_turntable(migrations_paths, target_version = nil) - up_without_turntable(migrations_paths, target_version) + def up(migrations_paths, target_version = nil) + super - ActiveRecord::Tasks::DatabaseTasks.each_current_turntable_cluster_connected do |name, configuration| + ActiveRecord::Tasks::DatabaseTasks.each_current_turntable_cluster_connected(current_environment) do |name, configuration| puts "[turntable] *** Migrating database: #{configuration['database']}(Shard: #{name})" - _original_up(migrations_paths, target_version) + super(migrations_paths, target_version) end end - def down_with_turntable(migrations_paths, target_version = nil, &block) - down_without_turntable(migrations_paths, target_version, &block) + def down(migrations_paths, target_version = nil, &block) + super - ActiveRecord::Tasks::DatabaseTasks.each_current_turntable_cluster_connected do |name, configuration| + ActiveRecord::Tasks::DatabaseTasks.each_current_turntable_cluster_connected(current_environment) do |name, configuration| puts "[turntable] *** Migrating database: #{configuration['database']}(Shard: #{name})" - _original_down(migrations_paths, target_version, &block) + super(migrations_paths, target_version, &block) end end - def run_with_turntable(*args) - run_without_turntable(*args) + def run(*args) + super - ActiveRecord::Tasks::DatabaseTasks.each_current_turntable_cluster_connected do |name, configuration| + ActiveRecord::Tasks::DatabaseTasks.each_current_turntable_cluster_connected(current_environment) do |name, configuration| puts "[turntable] *** Migrating database: #{configuration['database']}(Shard: #{name})" - _original_run(*args) + super(*args) end end end diff --git a/lib/active_record/turntable/mixer.rb b/lib/active_record/turntable/mixer.rb index 069ff3ac..36276c4d 100644 --- a/lib/active_record/turntable/mixer.rb +++ b/lib/active_record/turntable/mixer.rb @@ -1,5 +1,6 @@ -require 'active_support/core_ext/object/try' -require 'active_record/turntable/sql_tree_patch' +# rubocop:disable Style/CaseEquality +require "active_support/core_ext/object/try" +require "active_record/turntable/sql_tree_patch" module ActiveRecord::Turntable class Mixer @@ -9,9 +10,9 @@ class Mixer autoload :Fader end - delegate :logger, :to => ActiveRecord::Base + delegate :logger, to: ActiveRecord::Base - NOT_USED_FOR_SHARDING_OPERATORS_REGEXP = /\A(NOT IN|IS|IS NOT|BETWEEN|LIKE|\!\=|<<|>>|<>|>\=|<=|[\*\+\-\/\%\|\&><])\z/ + NOT_USED_FOR_SHARDING_OPERATORS_REGEXP = /\A(NOT IN|IS|IS NOT|BETWEEN|LIKE|!=|<<|>>|<>|>=|<=|[*+%|&><-])\z/ def initialize(proxy) @proxy = proxy @@ -24,7 +25,7 @@ def build_fader(method_name, query, *args, &block) { @proxy.fixed_shard => query }, method, query, *args, &block) end - binds = (method == 'insert') ? args[4] : args[1] + binds = (method == "insert") ? args[4] : args[1] binded_query = bind_sql(query, binds) begin @@ -44,8 +45,8 @@ def build_fader(method_name, query, *args, &block) else # send to master shard Fader::SpecifiedShard.new(@proxy, - { @proxy.master => query }, - method, query, *args, &block) + { @proxy.master => query }, + method, query, *args, &block) end rescue Exception => err logger.warn { "[ActiveRecord::Turntable] Error on Building Fader: #{binded_query}, on_method: #{method_name}, err: #{err}" } @@ -59,7 +60,7 @@ def find_shard_keys(tree, table_name, shard_key) when "OR" lkeys = find_shard_keys(tree.lhs, table_name, shard_key) rkeys = find_shard_keys(tree.rhs, table_name, shard_key) - if lkeys.present? and rkeys.present? + if lkeys.present? && rkeys.present? lkeys + rkeys else [] @@ -67,7 +68,7 @@ def find_shard_keys(tree, table_name, shard_key) when "AND" lkeys = find_shard_keys(tree.lhs, table_name, shard_key) rkeys = find_shard_keys(tree.rhs, table_name, shard_key) - if lkeys.present? or rkeys.present? + if lkeys.present? || rkeys.present? lkeys + rkeys else [] @@ -75,15 +76,15 @@ def find_shard_keys(tree, table_name, shard_key) when "IN", "=", "==" field = tree.lhs.respond_to?(:table) ? tree.lhs : nil if tree.rhs.is_a?(SQLTree::Node::SubQuery) - if field.try(:table) == table_name and field.name == shard_key + if (field.try(:table) == table_name) && (field.name == shard_key) find_shard_keys(tree.rhs.where, table_name, shard_key) else [] end else values = Array(tree.rhs) - if field.try(:table) == table_name and field.name == shard_key and - !tree.rhs.is_a?(SQLTree::Node::SubQuery) + if (field.try(:table) == table_name) && (field.name == shard_key) && + !tree.rhs.is_a?(SQLTree::Node::SubQuery) values.map(&:value).compact else [] @@ -93,141 +94,140 @@ def find_shard_keys(tree, table_name, shard_key) [] else raise ActiveRecord::Turntable::UnknownOperatorError, - "[ActiveRecord::Turntable] Found Unknown SQL Operator:'#{tree.operator if tree.respond_to?(:operaor)}', Please report this bug." + "[ActiveRecord::Turntable] Found Unknown SQL Operator:'#{tree.operator if tree.respond_to?(:operaor)}', Please report this bug." end end private - def divide_insert_values(tree, shard_key_name) - idx = tree.fields.find_index {|f| f.name == shard_key_name.to_s } - result = {} - tree.values.each do |val| - (result[val[idx].value] ||= []) << val + def divide_insert_values(tree, shard_key_name) + idx = tree.fields.find_index { |f| f.name == shard_key_name.to_s } + result = {} + tree.values.each do |val| + (result[val[idx].value] ||= []) << val + end + result end - return result - end - - def build_shards_with_same_query(shards, query) - Hash[shards.map {|s| [s, query] }] - end - def bind_sql(sql, binds) - # TODO: substitution value should be determined by adapter - query = sql.is_a?(String) ? sql : @proxy.to_sql(sql, binds ? binds.dup : []) - query = if query.include?("\0") and binds.is_a?(Array) and binds[0].is_a?(Array) and binds[0][0].is_a?(ActiveRecord::ConnectionAdapters::Column) - binds = binds.dup - query.gsub("\0") { @proxy.master.connection.quote(*binds.shift.reverse) } - else - query - end - end + def build_shards_with_same_query(shards, query) + Hash[shards.map { |s| [s, query] }] + end - def build_select_fader(tree, method, query, *args, &block) - shard_keys = if !tree.where and tree.from.size == 1 and SQLTree::Node::SubQuery === tree.from.first.table_reference.table - find_shard_keys(tree.from.first.table_reference.table.where, - @proxy.klass.table_name, - @proxy.klass.turntable_shard_key.to_s) - else - find_shard_keys(tree.where, - @proxy.klass.table_name, - @proxy.klass.turntable_shard_key.to_s) - end - - if shard_keys.size == 1 # shard - return Fader::SpecifiedShard.new(@proxy, - { @proxy.cluster.shard_for(shard_keys.first) => query }, - method, query, *args, &block) - elsif SQLTree::Node::SelectDeclaration === tree.select.first and - tree.select.first.to_sql == '1 AS "one"' # for `SELECT 1 AS one` (AR::Base.exists?) - return Fader::SelectShardsMergeResult.new(@proxy, - build_shards_with_same_query(@proxy.shards.values, query), - method, query, *args, &block - ) - elsif tree.group_by or tree.order_by or tree.limit.try(:value).to_i > 0 - raise CannotSpecifyShardError, "cannot specify shard for query: #{tree.to_sql}" - elsif shard_keys.present? - if SQLTree::Node::SelectDeclaration === tree.select.first and - SQLTree::Node::CountAggregrate === tree.select.first.expression - return Fader::CalculateShardsSumResult.new(@proxy, - build_shards_with_same_query(@proxy.shards.values, query), - method, query, *args, &block) + def bind_sql(sql, binds) + binds = binds ? binds.dup : [] + # TODO: substitution value should be determined by adapter + query = sql.is_a?(String) ? sql : @proxy.to_sql(sql, binds) + if query.include?("\0") && binds.is_a?(Array) && binds[0].is_a?(Array) && binds[0][0].is_a?(ActiveRecord::ConnectionAdapters::Column) + binds = binds.dup + query.gsub("\0") { @proxy.master.connection.quote(*binds.shift.reverse) } else - return Fader::SelectShardsMergeResult.new(@proxy, - Hash[shard_keys.map {|k| [@proxy.cluster.shard_for(k), query] }], - method, query, *args, &block - ) + query end - else # scan all shards - if SQLTree::Node::SelectDeclaration === tree.select.first and - SQLTree::Node::CountAggregrate === tree.select.first.expression + end - if raise_on_not_specified_shard_query? - raise CannotSpecifyShardError, "[Performance Notice] PLEASE FIX: #{tree.to_sql}" - end - return Fader::CalculateShardsSumResult.new(@proxy, - build_shards_with_same_query(@proxy.shards.values, query), - method, query, *args, &block) - elsif SQLTree::Node::AllFieldsDeclaration === tree.select.first or - SQLTree::Node::Expression::Value === tree.select.first.expression or - SQLTree::Node::Expression::Variable === tree.select.first.expression - - if raise_on_not_specified_shard_query? - raise CannotSpecifyShardError, "[Performance Notice] PLEASE FIX: #{tree.to_sql}" - end + def build_select_fader(tree, method, query, *args, &block) + shard_keys = if !tree.where && tree.from.size == 1 && SQLTree::Node::SubQuery === tree.from.first.table_reference.table + find_shard_keys(tree.from.first.table_reference.table.where, + @proxy.klass.table_name, + @proxy.klass.turntable_shard_key.to_s) + else + find_shard_keys(tree.where, + @proxy.klass.table_name, + @proxy.klass.turntable_shard_key.to_s) + end + + if shard_keys.size == 1 # shard + return Fader::SpecifiedShard.new(@proxy, + { @proxy.cluster.shard_for(shard_keys.first) => query }, + method, query, *args, &block) + elsif SQLTree::Node::SelectDeclaration === tree.select.first && + tree.select.first.to_sql == '1 AS "one"' # for `SELECT 1 AS one` (AR::Base.exists?) return Fader::SelectShardsMergeResult.new(@proxy, build_shards_with_same_query(@proxy.shards.values, query), - method, query, *args, &block - ) - else + method, query, *args, &block) + elsif tree.group_by || tree.order_by || tree.limit.try(:value).to_i > 0 raise CannotSpecifyShardError, "cannot specify shard for query: #{tree.to_sql}" + elsif shard_keys.present? + if SQLTree::Node::SelectDeclaration === tree.select.first && + SQLTree::Node::CountAggregrate === tree.select.first.expression + return Fader::CalculateShardsSumResult.new(@proxy, + build_shards_with_same_query(@proxy.shards.values, query), + method, query, *args, &block) + else + return Fader::SelectShardsMergeResult.new(@proxy, + Hash[shard_keys.map { |k| [@proxy.cluster.shard_for(k), query] }], + method, query, *args, &block) + end + else # scan all shards + if SQLTree::Node::SelectDeclaration === tree.select.first && + SQLTree::Node::CountAggregrate === tree.select.first.expression + + if raise_on_not_specified_shard_query? + raise CannotSpecifyShardError, "[Performance Notice] PLEASE FIX: #{tree.to_sql}" + end + return Fader::CalculateShardsSumResult.new(@proxy, + build_shards_with_same_query(@proxy.shards.values, query), + method, query, *args, &block) + elsif SQLTree::Node::AllFieldsDeclaration === tree.select.first || + SQLTree::Node::Expression::Value === tree.select.first.expression || + SQLTree::Node::Expression::Variable === tree.select.first.expression + + if raise_on_not_specified_shard_query? + raise CannotSpecifyShardError, "[Performance Notice] PLEASE FIX: #{tree.to_sql}" + end + return Fader::SelectShardsMergeResult.new(@proxy, + build_shards_with_same_query(@proxy.shards.values, query), + method, query, *args, &block) + else + raise CannotSpecifyShardError, "cannot specify shard for query: #{tree.to_sql}" + end end end - end - - def build_update_fader(tree, method, query, *args, &block) - shard_keys = find_shard_keys(tree.where, @proxy.klass.table_name, @proxy.klass.turntable_shard_key.to_s) - shards_with_query = if shard_keys.present? - build_shards_with_same_query(shard_keys.map {|k| @proxy.cluster.shard_for(k) }, query) - else - build_shards_with_same_query(@proxy.shards.values, query) - end - if shards_with_query.size == 1 - Fader::SpecifiedShard.new(@proxy, - shards_with_query, - method, query, *args, &block) - else - if raise_on_not_specified_shard_update? - raise CannotSpecifyShardError, "[Performance Notice] PLEASE FIX: #{tree.to_sql}" + def build_update_fader(tree, method, query, *args, &block) + shard_keys = find_shard_keys(tree.where, @proxy.klass.table_name, @proxy.klass.turntable_shard_key.to_s) + shards_with_query = if shard_keys.present? + build_shards_with_same_query(shard_keys.map { |k| @proxy.cluster.shard_for(k) }, query) + else + build_shards_with_same_query(@proxy.shards.values, query) + end + + if shards_with_query.size == 1 + Fader::SpecifiedShard.new(@proxy, + shards_with_query, + method, query, *args, &block) + else + if raise_on_not_specified_shard_update? + raise CannotSpecifyShardError, "[Performance Notice] PLEASE FIX: #{tree.to_sql}" + end + Fader::UpdateShardsMergeResult.new(@proxy, + shards_with_query, + method, query, *args, &block) end - Fader::UpdateShardsMergeResult.new(@proxy, - shards_with_query, - method, query, *args, &block) end - end - def build_insert_fader(tree, method, query, *args, &block) - values_hash = divide_insert_values(tree, @proxy.klass.turntable_shard_key) - shards_with_query = {} - values_hash.each do |k,vs| - tree.values = [[SQLTree::Node::Expression::Variable.new("\\0")]] - sql = tree.to_sql - value_sql = vs.map do |val| - "(#{val.map { |v| "#{v.escape}#{@proxy.connection.quote(v.value)}" }.join(', ')})" - end.join(', ') - sql.gsub!('("\0")') { value_sql } - shards_with_query[@proxy.cluster.shard_for(k)] = sql + def build_insert_fader(tree, method, query, *args, &block) + values_hash = divide_insert_values(tree, @proxy.klass.turntable_shard_key) + shards_with_query = {} + values_hash.each do |k, vs| + tree.values = [[SQLTree::Node::Expression::Variable.new("\\0")]] + sql = tree.to_sql + value_sql = vs.map do |val| + "(#{val.map { |v| "#{v.escape}#{@proxy.connection.quote(v.value)}" }.join(', ')})" + end.join(", ") + sql.gsub!('("\0")') { value_sql } + shards_with_query[@proxy.cluster.shard_for(k)] = sql + end + Fader::InsertShardsMergeResult.new(@proxy, shards_with_query, method, query, *args, &block) end - Fader::InsertShardsMergeResult.new(@proxy, shards_with_query, method, query, *args, &block) - end - def raise_on_not_specified_shard_query? - ActiveRecord::Base.turntable_config[:raise_on_not_specified_shard_query] - end + def raise_on_not_specified_shard_query? + ActiveRecord::Base.turntable_config[:raise_on_not_specified_shard_query] + end - def raise_on_not_specified_shard_update? - ActiveRecord::Base.turntable_config[:raise_on_not_specified_shard_update] - end + def raise_on_not_specified_shard_update? + ActiveRecord::Base.turntable_config[:raise_on_not_specified_shard_update] + end end end +# rubocop:enable Style/CaseEquality diff --git a/lib/active_record/turntable/mixer/fader/calculate_shards_sum_result.rb b/lib/active_record/turntable/mixer/fader/calculate_shards_sum_result.rb index ba6f603e..f83842ca 100644 --- a/lib/active_record/turntable/mixer/fader/calculate_shards_sum_result.rb +++ b/lib/active_record/turntable/mixer/fader/calculate_shards_sum_result.rb @@ -13,13 +13,13 @@ def execute private - def merge_results(results) - ActiveRecord::Result.new( - results.first.columns, - results[0].rows.zip(*results[1..-1].map{|r| r.rows}).map {|r| [r.map {|v| v.first}.inject(&:+)]}, - results.first.column_types - ) - end + def merge_results(results) + ActiveRecord::Result.new( + results.first.columns, + results[0].rows.zip(*results[1..-1].map(&:rows)).map { |r| [r.map(&:first).inject(&:+)] }, + results.first.column_types + ) + end end end end diff --git a/lib/active_record/turntable/mixer/fader/select_shards_merge_result.rb b/lib/active_record/turntable/mixer/fader/select_shards_merge_result.rb index 0ea94e5a..9620e379 100644 --- a/lib/active_record/turntable/mixer/fader/select_shards_merge_result.rb +++ b/lib/active_record/turntable/mixer/fader/select_shards_merge_result.rb @@ -13,20 +13,20 @@ def execute private - def merge_results(results) - if results.any? {|r| r.is_a?(ActiveRecord::Result) } - first_result = results.find {|r| r.present? } - return results.first unless first_result + def merge_results(results) + if results.any? { |r| r.is_a?(ActiveRecord::Result) } + first_result = results.find(&:present?) + return results.first unless first_result - ActiveRecord::Result.new( - first_result.columns, - results.map {|r| r.rows}.flatten(1), - first_result.column_types - ) - else - results.compact.inject(&:+) + ActiveRecord::Result.new( + first_result.columns, + results.flat_map(&:rows), + first_result.column_types + ) + else + results.compact.inject(&:+) + end end - end end end end diff --git a/lib/active_record/turntable/plugin.rb b/lib/active_record/turntable/plugin.rb index dab3a68e..d925e50b 100644 --- a/lib/active_record/turntable/plugin.rb +++ b/lib/active_record/turntable/plugin.rb @@ -1,4 +1,4 @@ -require 'active_record/turntable' +require "active_record/turntable" module ActiveRecord::Turntable module Plugin diff --git a/lib/active_record/turntable/pool_proxy.rb b/lib/active_record/turntable/pool_proxy.rb index a424ab14..0fa15dca 100644 --- a/lib/active_record/turntable/pool_proxy.rb +++ b/lib/active_record/turntable/pool_proxy.rb @@ -1,22 +1,10 @@ module ActiveRecord::Turntable - module ActiveRecordConnectionMethods - def self.included(base) - base.alias_method_chain :reload, :master - end - - def reload_with_master(*args, &block) - connection.with_master { reload_without_master } - end - end - class PoolProxy def initialize(proxy) @proxy = proxy end - def proxy - @proxy - end + attr_reader :proxy alias_method :connection, :proxy def with_connection @@ -24,7 +12,7 @@ def with_connection end delegate :connected?, :automatic_reconnect, :automatic_reconnect=, :checkout_timeout, :dead_connection_timeout, - :spec, :connections, :size, :reaper, :table_exists?, to: :proxy + :spec, :connections, :size, :reaper, :table_exists?, to: :proxy %w(columns_hash column_defaults primary_keys).each do |name| define_method(name.to_sym) do @@ -38,24 +26,30 @@ def with_connection end end - %w(active_connection?).each do |name| - define_method(name.to_sym) do |*args| - @proxy.master.connection_pool.send(name.to_sym) || - @proxy.seq.connection_pool.try(name.to_sym) if @proxy.respond_to?(:seq) || - @proxy.shards.values.any? do |pool| - pool.connection_pool.send(name.to_sym) - end - end + def active_connection? + connection_pools_list.any?(&:active_connection?) end - %w(disconnect! release_connection clear_all_connections! clear_active_connections! clear_reloadable_connections! clear_stale_cached_connections! verify_active_connections!).each do |name| + %w(disconnect! + release_connection + clear_all_connections! + clear_active_connections! + clear_reloadable_connections! + clear_stale_cached_connections! + verify_active_connections!).each do |name| define_method(name.to_sym) do - @proxy.master.connection_pool.send(name.to_sym) - @proxy.seq.connection_pool.try(name.to_sym) if @proxy.respond_to?(:seq) - @proxy.shards.values.each do |pool| - pool.connection_pool.send(name.to_sym) - end + connection_pools_list.each { |cp| cp.public_send(name.to_sym) } end end + + private + + def connection_pools_list + pools = [] + pools << proxy.master.connection_pool + pools << proxy.seq.try(:connection_pool) if proxy.respond_to?(:seq) + pools.concat(proxy.shards.values.map(&:connection_pool)) + pools.compact + end end end diff --git a/lib/active_record/turntable/query_cache.rb b/lib/active_record/turntable/query_cache.rb new file mode 100644 index 00000000..42a46f87 --- /dev/null +++ b/lib/active_record/turntable/query_cache.rb @@ -0,0 +1,41 @@ +require "rack/body_proxy" +require "active_record/query_cache" + +module ActiveRecord + module Turntable + class QueryCache < ActiveRecord::QueryCache + def self.run + klasses = ActiveRecord::Base.turntable_connections.values + enables = klasses.map do |k| + enabled = k.connection.query_cache_enabled + k.connection.enable_query_cache! + + enabled + end + + enables.all? + end + + def self.complete(enabled) + klasses = ActiveRecord::Base.turntable_connections.values + klasses.each do |k| + k.connection.clear_query_cache + k.connection.disable_query_cache! unless enabled + end + end + + def self.install_executor_hooks(executor = ActiveSupport::Executor) + executor.register_hook(self) + + executor.to_complete do + klasses = ActiveRecord::Base.turntable_connection_classes + klasses.each do |k| + unless k.connected? && k.connection.transaction_open? + k.clear_active_connections! + end + end + end + end + end + end +end diff --git a/lib/active_record/turntable/rack.rb b/lib/active_record/turntable/rack.rb deleted file mode 100644 index c08706b2..00000000 --- a/lib/active_record/turntable/rack.rb +++ /dev/null @@ -1,8 +0,0 @@ -module ActiveRecord::Turntable - module Rack - extend ActiveSupport::Autoload - - autoload :ConnectionManagement - autoload :QueryCache - end -end diff --git a/lib/active_record/turntable/rack/connection_management.rb b/lib/active_record/turntable/rack/connection_management.rb deleted file mode 100644 index f0606c6e..00000000 --- a/lib/active_record/turntable/rack/connection_management.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveRecord::Turntable - module Rack - class ConnectionManagement - def initialize(app) - @app = app - end - - def call(env) - @app.call(env) - ensure - unless env.key?("rack.test") - ActiveRecord::Base.connection_handler.clear_all_connections! - ActiveRecord::Base.clear_all_connections! - end - end - end - end -end diff --git a/lib/active_record/turntable/rack/query_cache.rb b/lib/active_record/turntable/rack/query_cache.rb deleted file mode 100644 index 4332f868..00000000 --- a/lib/active_record/turntable/rack/query_cache.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'rack/body_proxy' -require 'active_record/query_cache' - -module ActiveRecord - module Turntable - module Rack - class QueryCache < ActiveRecord::QueryCache - def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled - connection_id = ActiveRecord::Base.connection_id - klasses = ActiveRecord::Base.turntable_connections.values - klasses.each do |k| - k.connection.enable_query_cache! - end - - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do - restore_query_cache_settings(connection_id, enabled) - end - - response - rescue Exception => e - restore_query_cache_settings(connection_id, enabled) - raise e - end - - private - - def restore_query_cache_settings(connection_id, enabled) - klasses = ActiveRecord::Base.turntable_connections.values - klasses.each do |k| - ActiveRecord::Base.connection_id = connection_id - k.connection.clear_query_cache - k.connection.disable_query_cache! unless enabled - end - end - end - end - end -end diff --git a/lib/active_record/turntable/railtie.rb b/lib/active_record/turntable/railtie.rb index 55b9c6c4..874d7b6e 100644 --- a/lib/active_record/turntable/railtie.rb +++ b/lib/active_record/turntable/railtie.rb @@ -1,14 +1,14 @@ module ActiveRecord::Turntable class Railtie < Rails::Railtie rake_tasks do - require 'active_record/turntable/active_record_ext/database_tasks' + require "active_record/turntable/active_record_ext/database_tasks" load "active_record/turntable/railties/databases.rake" end # rails loading hook ActiveSupport.on_load(:before_initialize) do ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.send(:include, ActiveRecord::Turntable) + ActiveRecord::Base.include(ActiveRecord::Turntable) end end @@ -23,9 +23,11 @@ class Railtie < Rails::Railtie end end - # QueryCache Middleware for turntable shards - initializer "turntable.insert_query_cache_middleware" do |app| - app.middleware.insert_after ActiveRecord::QueryCache, ActiveRecord::Turntable::Rack::QueryCache + # set QueryCache executor hooks for turntable clusters + initializer "active_record.set_executor_hooks" do + ActiveSupport.on_load(:active_record) do + ActiveRecord::Turntable::QueryCache.install_executor_hooks + end end end end diff --git a/lib/active_record/turntable/railties/databases.rake b/lib/active_record/turntable/railties/databases.rake index f9e914ed..98ebd4f2 100644 --- a/lib/active_record/turntable/railties/databases.rake +++ b/lib/active_record/turntable/railties/databases.rake @@ -1,5 +1,5 @@ -require 'active_record/turntable' -ActiveRecord::SchemaDumper.send(:include, ActiveRecord::Turntable::ActiveRecordExt::SchemaDumper) +require "active_record/turntable" +ActiveRecord::SchemaDumper.prepend(ActiveRecord::Turntable::ActiveRecordExt::SchemaDumper) turntable_namespace = nil @@ -11,9 +11,9 @@ db_namespace = namespace :db do end end - desc 'Create current turntable databases config/database.yml for the current Rails.env' + desc "Create current turntable databases config/database.yml for the current Rails.env" task :create do - unless ENV['DATABASE_URL'] + unless ENV["DATABASE_URL"] ActiveRecord::Tasks::DatabaseTasks.create_current_turntable_cluster end end @@ -24,9 +24,9 @@ db_namespace = namespace :db do end end - desc 'Drops current turntable databases for the current Rails.env' + desc "Drops current turntable databases for the current Rails.env" task :drop do - unless ENV['DATABASE_URL'] + unless ENV["DATABASE_URL"] ActiveRecord::Tasks::DatabaseTasks.drop_current_turntable_cluster end end @@ -34,47 +34,47 @@ db_namespace = namespace :db do namespace :schema do # TODO: implement schema:cache:xxxx task :dump do - require 'active_record/schema_dumper' - config = ActiveRecord::Base.configurations[Rails.env] - shard_configs = config["shards"] - shard_configs.merge!(config["seq"]) if config["seq"] + require "active_record/schema_dumper" + current_config = ActiveRecord::Base.configurations[Rails.env] + shard_configs = current_config["shards"] + shard_configs.merge!(current_config["seq"]) if current_config["seq"] if shard_configs shard_configs.each do |name, config| next unless config["database"] - filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema-#{name}.rb" + filename = ENV["SCHEMA"] || "#{Rails.root}/db/schema-#{name}.rb" File.open(filename, "w:utf-8") do |file| ActiveRecord::Base.establish_connection(config) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end end end - ActiveRecord::Base.establish_connection(config) - turntable_namespace['schema:dump'].reenable + ActiveRecord::Base.establish_connection(current_config) + turntable_namespace["schema:dump"].reenable end - desc 'Load a schema.rb file into the database' + desc "Load a schema.rb file into the database" task :load do - config = ActiveRecord::Base.configurations[Rails.env] - shard_configs = config["shards"] - shard_configs.merge!(config["seq"]) if config["seq"] + current_config = ActiveRecord::Base.configurations[Rails.env] + shard_configs = current_config["shards"] + shard_configs.merge!(current_config["seq"]) if current_config["seq"] if shard_configs shard_configs.each do |name, config| next unless config["database"] ActiveRecord::Base.establish_connection(config) - file = ENV['SCHEMA'] || "#{Rails.root}/db/schema-#{name}.rb" - if File.exists?(file) + file = ENV["SCHEMA"] || "#{Rails.root}/db/schema-#{name}.rb" + if File.exist?(file) load(file) else - abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded'} + abort %(#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded') end end end - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.establish_connection(current_config) end end namespace :structure do - desc 'Dump the database structure to an SQL file' + desc "Dump the database structure to an SQL file" task :dump do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config shard_configs = current_config["shards"] @@ -86,15 +86,14 @@ db_namespace = namespace :db do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure_#{name}.sql") ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) - if ActiveRecord::Base.connection.supports_migrations? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - end + next unless ActiveRecord::Base.connection.supports_migrations? + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information end end ActiveRecord::Base.establish_connection(current_config) end - turntable_namespace['structure:dump'].reenable + turntable_namespace["structure:dump"].reenable end # desc "Recreate the databases from the structure.sql file" @@ -117,16 +116,16 @@ db_namespace = namespace :db do namespace :test do # desc "Empty the test database" task :purge do - config = ActiveRecord::Base.configurations[Rails.env] - shard_configs = config["shards"] - shard_configs.merge!(config["seq"]) if config["seq"] + current_config = ActiveRecord::Base.configurations[Rails.env] + shard_configs = current_config["shards"] + shard_configs.merge!(current_config["seq"]) if current_config["seq"] if shard_configs - shard_configs.each do |name, config| + shard_configs.each do |_name, config| next unless config["database"] ActiveRecord::Tasks::DatabaseTasks.purge config end end - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.establish_connection(current_config) end end end diff --git a/lib/active_record/turntable/seq_shard.rb b/lib/active_record/turntable/seq_shard.rb index 00ac1616..da0cadc0 100644 --- a/lib/active_record/turntable/seq_shard.rb +++ b/lib/active_record/turntable/seq_shard.rb @@ -2,20 +2,20 @@ module ActiveRecord::Turntable class SeqShard < Shard private - def create_connection_class - klass = get_or_set_connection_class - klass.remove_connection - klass.establish_connection ActiveRecord::Base.connection_pool.spec.config[:seq][name].with_indifferent_access - klass - end - - def retrieve_connection_pool - ActiveRecord::Base.turntable_connections[name] ||= - begin - config = ActiveRecord::Base.configurations[Rails.env]["seq"][name] - raise ArgumentError, "Unknown database config: #{name}, have #{ActiveRecord::Base.configurations.inspect}" unless config - ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec_for(config)) - end - end + def create_connection_class + klass = connection_class_instance + klass.remove_connection + klass.establish_connection ActiveRecord::Base.connection_pool.spec.config[:seq][name].with_indifferent_access + klass + end + + def retrieve_connection_pool + ActiveRecord::Base.turntable_connections[name] ||= + begin + config = ActiveRecord::Base.configurations[Rails.env]["seq"][name] + raise ArgumentError, "Unknown database config: #{name}, have #{ActiveRecord::Base.configurations.inspect}" unless config + ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec_for(config)) + end + end end end diff --git a/lib/active_record/turntable/sequencer.rb b/lib/active_record/turntable/sequencer.rb index ed91635d..3ec5ef8e 100644 --- a/lib/active_record/turntable/sequencer.rb +++ b/lib/active_record/turntable/sequencer.rb @@ -13,33 +13,48 @@ class Sequencer autoload :Barrage end - @@sequence_types = { - :api => Api, - :mysql => Mysql, - :barrage => Barrage + class_attribute :sequence_types + class_attribute :sequences + class_attribute :tables + + self.sequence_types = { + api: Api, + mysql: Mysql, + barrage: Barrage, } + self.sequences = {} + self.tables = {} - @@sequences = {} - @@tables = {} - cattr_reader :sequences, :tables + class << self + def build(klass, sequence_name = nil, cluster_name = nil) + sequence_name ||= current_cluster_config_for(cluster_name || klass)[:seq].keys.first + seq_config = current_cluster_config_for(cluster_name || klass)[:seq][sequence_name] + seq_type = (seq_config[:seq_type] ? seq_config[:seq_type].to_sym : :mysql) + tables[klass.table_name] ||= (sequences[sequence_name(klass.table_name, klass.primary_key)] ||= sequence_types[seq_type].new(klass, seq_config)) + end - def self.build(klass, sequence_name = nil, cluster_name = nil) - sequence_name ||= current_cluster_config_for(cluster_name || klass)["seq"].keys.first - seq_config = current_cluster_config_for(cluster_name || klass)["seq"][sequence_name] - seq_type = (seq_config["seq_type"] ? seq_config["seq_type"].to_sym : :mysql) - @@tables[klass.table_name] ||= (@@sequences[sequence_name(klass.table_name, klass.primary_key)] ||= @@sequence_types[seq_type].new(klass, seq_config)) - end + def has_sequencer?(table_name) + !!tables[table_name] + end - def self.has_sequencer?(table_name) - !!@@tables[table_name] - end + def sequence_name(table_name, pk) + "#{table_name}_#{pk || 'id'}_seq" + end - def self.sequence_name(table_name, pk) - "#{table_name}_#{pk || 'id'}_seq" - end + def table_name(seq_name) + seq_name.split("_").first + end - def self.table_name(seq_name) - seq_name.split('_').first + private + + def current_cluster_config_for(klass_or_name) + cluster_name = if klass_or_name.is_a?(Symbol) + klass_or_name + else + klass_or_name.turntable_cluster_name.to_s + end + ActiveRecord::Base.turntable_config[:clusters][cluster_name] + end end def next_sequence_value @@ -49,16 +64,5 @@ def next_sequence_value def current_sequence_value raise NotImplementedError end - - private - - def self.current_cluster_config_for(klass_or_name) - cluster_name = if klass_or_name.is_a?(Symbol) - klass_or_name - else - klass_or_name.turntable_cluster_name.to_s - end - ActiveRecord::Base.turntable_config["clusters"][cluster_name] - end end end diff --git a/lib/active_record/turntable/sequencer/api.rb b/lib/active_record/turntable/sequencer/api.rb index df66529f..1b7f2e69 100644 --- a/lib/active_record/turntable/sequencer/api.rb +++ b/lib/active_record/turntable/sequencer/api.rb @@ -2,19 +2,19 @@ # # Sequencer via HTTP API # -require 'httpclient' +require "httpclient" module ActiveRecord::Turntable class Sequencer class Api < Sequencer - API_ENDPOINT = '/sequences/' - NEXT_VALUE_ENDPOINT = '/new' + API_ENDPOINT = "/sequences/".freeze + NEXT_VALUE_ENDPOINT = "/new".freeze def initialize(klass, options = {}) @klass = klass @options = options - @host = @options["api_host"] - @port = @options["api_port"] + @host = @options[:api_host] + @port = @options[:api_port] @client = HTTPClient.new end @@ -22,14 +22,14 @@ def next_sequence_value(sequence_name) res = @client.get_content("http://#{@host}:#{@port}#{API_ENDPOINT}#{sequence_name}#{NEXT_VALUE_ENDPOINT}") new_id = res.to_i raise SequenceNotFoundError if new_id.zero? - return new_id + new_id end def current_sequence_value(sequence_name) res = @client.get_content("http://#{@host}:#{@port}#{API_ENDPOINT}#{sequence_name}") current_id = res.to_i raise SequenceNotFoundError if current_id.zero? - return current_id + current_id end end end diff --git a/lib/active_record/turntable/sequencer/barrage.rb b/lib/active_record/turntable/sequencer/barrage.rb index 74facc9a..690daa2f 100644 --- a/lib/active_record/turntable/sequencer/barrage.rb +++ b/lib/active_record/turntable/sequencer/barrage.rb @@ -1,28 +1,28 @@ module ActiveRecord::Turntable class Sequencer class Barrage < Sequencer - @@unique_barrage_instance = {} + class_attribute :unique_barrage_instance + self.unique_barrage_instance = {} def initialize(klass, options = {}) - require 'barrage' + require "barrage" @klass = klass @options = options["options"] - @barrage = get_barrage_instance end def next_sequence_value(sequence_name) - @barrage.next + barrage.next end def current_sequence_value(sequence_name) - @barrage.current + barrage.current end private - def get_barrage_instance - @@unique_barrage_instance[@options] ||= ::Barrage.new(@options) - end + def barrage + self.unique_barrage_instance[@options] ||= ::Barrage.new(@options) + end end end end diff --git a/lib/active_record/turntable/sequencer/mysql.rb b/lib/active_record/turntable/sequencer/mysql.rb index 82d0db3b..5e4576a5 100644 --- a/lib/active_record/turntable/sequencer/mysql.rb +++ b/lib/active_record/turntable/sequencer/mysql.rb @@ -17,7 +17,7 @@ def next_sequence_value(sequence_name) res = conn.execute("SELECT LAST_INSERT_ID()") new_id = res.first.first.to_i raise SequenceNotFoundError if new_id.zero? - return new_id + new_id end def current_sequence_value(sequence_name) @@ -25,7 +25,7 @@ def current_sequence_value(sequence_name) conn.execute "UPDATE #{@klass.connection.quote_table_name(sequence_name)} SET id=LAST_INSERT_ID(id)" res = conn.execute("SELECT LAST_INSERT_ID()") current_id = res.first.first.to_i - return current_id + current_id end end end diff --git a/lib/active_record/turntable/shard.rb b/lib/active_record/turntable/shard.rb index f1b3db81..18eb1199 100644 --- a/lib/active_record/turntable/shard.rb +++ b/lib/active_record/turntable/shard.rb @@ -1,16 +1,19 @@ module ActiveRecord::Turntable class Shard module Connections; end + def self.connection_classes + Connections.constants.map { |name| Connections.const_get(name) } + end DEFAULT_CONFIG = { - "connection" => (defined?(Rails) ? Rails.env : "development") + "connection" => (defined?(Rails) ? Rails.env : "development"), }.with_indifferent_access attr_reader :name def initialize(shard_spec) @config = DEFAULT_CONFIG.merge(shard_spec) - @name = @config["connection"] + @name = @config[:connection] ActiveRecord::Base.turntable_connections[name] = connection_pool end @@ -26,26 +29,26 @@ def connection private - def connection_klass - @connection_klass ||= create_connection_class - end + def connection_klass + @connection_klass ||= create_connection_class + end - def get_or_set_connection_class - if Connections.const_defined?(name.classify) - klass = Connections.const_get(name.classify) - else - klass = Class.new(ActiveRecord::Base) - Connections.const_set(name.classify, klass) - klass.abstract_class = true + def create_connection_class + klass = connection_class_instance + klass.remove_connection + klass.establish_connection ActiveRecord::Base.connection_pool.spec.config[:shards][name].with_indifferent_access + klass end - klass - end - def create_connection_class - klass = get_or_set_connection_class - klass.remove_connection - klass.establish_connection ActiveRecord::Base.connection_pool.spec.config[:shards][name].with_indifferent_access - klass - end + def connection_class_instance + if Connections.const_defined?(name.classify) + klass = Connections.const_get(name.classify) + else + klass = Class.new(ActiveRecord::Base) + Connections.const_set(name.classify, klass) + klass.abstract_class = true + end + klass + end end end diff --git a/lib/active_record/turntable/sql_tree_patch.rb b/lib/active_record/turntable/sql_tree_patch.rb index c1754b67..e299a045 100644 --- a/lib/active_record/turntable/sql_tree_patch.rb +++ b/lib/active_record/turntable/sql_tree_patch.rb @@ -1,22 +1,23 @@ -require 'sql_tree' -require 'active_support/core_ext/kernel/reporting' +# rubocop:disable Style/CaseEquality +require "sql_tree" +require "active_support/core_ext/kernel/reporting" module SQLTree class << self attr_accessor :identifier_quote_field_char end - self.identifier_quote_field_char = '`' + self.identifier_quote_field_char = "`" end class SQLTree::Token - extended_keywords = ['BINARY', 'LIMIT', 'OFFSET', 'INDEX', 'KEY', 'USE', 'FORCE', 'IGNORE'] + extended_keywords = %w(BINARY LIMIT OFFSET INDEX KEY USE FORCE IGNORE) KEYWORDS.concat(extended_keywords) extended_keywords.each do |kwd| const_set(kwd, Class.new(SQLTree::Token::Keyword)) end - BINARY_ESCAPE = Class.new(SQLTree::Token).new('x') + BINARY_ESCAPE = Class.new(SQLTree::Token).new("x") def possible_index_hint? [SQLTree::Token::USE, SQLTree::Token::FORCE, SQLTree::Token::IGNORE].include?(self.class) @@ -29,7 +30,7 @@ def index_keyword? class SQLTree::Tokenizer def tokenize_quoted_string(&block) # :yields: SQLTree::Token::String - string = '' + string = "" until next_char.nil? || current_char == "'" string << (current_char == "\\" ? instance_eval("%@\\#{next_char.gsub('@', '\@')}@") : current_char) end @@ -37,35 +38,36 @@ def tokenize_quoted_string(&block) # :yields: SQLTree::Token::String end # @note Override to handle x'..' binary string + # rubocop:disable Lint/EmptyWhen: def each_token(&block) # :yields: SQLTree::Token - while next_char case current_char - when /^\s?$/; # whitespace, go to next character - when '('; handle_token(SQLTree::Token::LPAREN, &block) - when ')'; handle_token(SQLTree::Token::RPAREN, &block) - when '.'; handle_token(SQLTree::Token::DOT, &block) - when ','; handle_token(SQLTree::Token::COMMA, &block) - when /\d/; tokenize_number(&block) - when "'"; tokenize_quoted_string(&block) - when 'E', 'x', 'X'; tokenize_possible_escaped_string(&block) - when /\w/; tokenize_keyword(&block) - when OPERATOR_CHARS; tokenize_operator(&block) - when SQLTree.identifier_quote_char; tokenize_quoted_identifier(&block) + when /^\s?$/ then # whitespace, go to next character + when "(" then handle_token(SQLTree::Token::LPAREN, &block) + when ")" then handle_token(SQLTree::Token::RPAREN, &block) + when "." then handle_token(SQLTree::Token::DOT, &block) + when "," then handle_token(SQLTree::Token::COMMA, &block) + when /\d/ then tokenize_number(&block) + when "'" then tokenize_quoted_string(&block) + when "E", "x", "X" then tokenize_possible_escaped_string(&block) + when /\w/ then tokenize_keyword(&block) + when OPERATOR_CHARS then tokenize_operator(&block) + when SQLTree.identifier_quote_char then tokenize_quoted_identifier(&block) end end # Make sure to yield any tokens that are still stashed on the queue. empty_keyword_queue!(&block) end - alias :each :each_token + # rubocop:enable Lint/EmptyWhen: + alias_method :each, :each_token def tokenize_possible_escaped_string(&block) if peek_char == "'" token = case current_char - when 'E' + when "E" SQLTree::Token::STRING_ESCAPE - when 'x', 'X' + when "x", "X" SQLTree::Token::BINARY_ESCAPE end handle_token(token, &block) @@ -87,16 +89,16 @@ class SelectQuery < Base def to_sql(options = {}) raise "At least one SELECT expression is required" if self.select.empty? - sql = (self.distinct) ? "SELECT DISTINCT " : "SELECT " - sql << select.map { |s| s.to_sql(options) }.join(', ') - sql << " FROM " << from.map { |f| f.to_sql(options) }.join(', ') if from + sql = self.distinct ? "SELECT DISTINCT " : "SELECT " + sql << select.map { |s| s.to_sql(options) }.join(", ") + sql << " FROM " << from.map { |f| f.to_sql(options) }.join(", ") if from sql << " WHERE " << where.to_sql(options) if where - sql << " GROUP BY " << group_by.map { |g| g.to_sql(options) }.join(', ') if group_by - sql << " ORDER BY " << order_by.map { |o| o.to_sql(options) }.join(', ') if order_by + sql << " GROUP BY " << group_by.map { |g| g.to_sql(options) }.join(", ") if group_by + sql << " ORDER BY " << order_by.map { |o| o.to_sql(options) }.join(", ") if order_by sql << " HAVING " << having.to_sql(options) if having - sql << " LIMIT " << Array(limit).map {|f| f.to_sql(options) }.join(', ') if limit + sql << " LIMIT " << Array(limit).map { |f| f.to_sql(options) }.join(", ") if limit sql << " OFFSET " << offset.to_sql(options) if offset - return sql + sql end def self.parse(tokens) @@ -116,12 +118,12 @@ def self.parse(tokens) select_node.having = self.parse_having_clause(tokens) if SQLTree::Token::HAVING === tokens.peek end select_node.order_by = self.parse_order_clause(tokens) if SQLTree::Token::ORDER === tokens.peek - if SQLTree::Token::LIMIT === tokens.peek and list = self.parse_limit_clause(tokens) + if SQLTree::Token::LIMIT === tokens.peek && (list = self.parse_limit_clause(tokens)) select_node.offset = list.shift if list.size > 1 select_node.limit = list.shift end select_node.offset = self.parse_offset_clause(tokens) if SQLTree::Token::OFFSET === tokens.peek - return select_node + select_node end def self.parse_limit_clause(tokens) @@ -137,14 +139,14 @@ def self.parse_offset_clause(tokens) class SubQuery < SelectQuery def to_sql(options = {}) - "("+super(options)+")" + "(" + super(options) + ")" end def self.parse(tokens) tokens.consume(SQLTree::Token::LPAREN) select_node = super(tokens) tokens.consume(SQLTree::Token::RPAREN) - return select_node + select_node end end @@ -152,14 +154,16 @@ class TableReference < Base leaf :index_hint def initialize(table, table_alias = nil, index_hint = nil) - @table, @table_alias, @index_hint = table, table_alias, index_hint + @table = table + @table_alias = table_alias + @index_hint = index_hint end - def to_sql(options={}) + def to_sql(options = {}) sql = (SQLTree::Node::SubQuery === table) ? table.to_sql : quote_field_name(table) sql << " AS " << quote_field_name(table_alias) if table_alias sql << " " << index_hint.to_sql if index_hint - return sql + sql end def self.parse(tokens) @@ -167,7 +171,7 @@ def self.parse(tokens) tokens.next table_reference = self.new(tokens.current.literal) if tokens.peek && !tokens.peek.possible_index_hint? && - (SQLTree::Token::AS === tokens.peek || SQLTree::Token::Identifier === tokens.peek) + (SQLTree::Token::AS === tokens.peek || SQLTree::Token::Identifier === tokens.peek) tokens.consume(SQLTree::Token::AS) if SQLTree::Token::AS === tokens.peek table_reference.table_alias = tokens.next.literal end @@ -183,7 +187,7 @@ def self.parse(tokens) end table_reference else - raise SQLTree::Parser::UnexpectedToken.new(tokens.current) + raise SQLTree::Parser::UnexpectedToken, tokens.current end end end @@ -194,12 +198,14 @@ class IndexHint < Base leaf :index_list def initialize(hint_method, hint_key, index_list) - @hint_method, @hint_key, @index_list = hint_method, hint_key, index_list + @hint_method = hint_method + @hint_key = hint_key + @index_list = index_list end - def to_sql(options={}) + def to_sql(options = {}) sql = "#{hint_method} #{hint_key} " - sql << "(#{index_list.map {|idx| idx.to_sql }.join(' ')})" + sql << "(#{index_list.map(&:to_sql).join(' ')})" sql end @@ -212,7 +218,7 @@ def self.parse(tokens) tokens.consume(SQLTree::Token::RPAREN) self.new(hint_method, hint_key, index_list) else - raise SQLTree::Parser::UnexpectedToken.new(tokens.current) + raise SQLTree::Parser::UnexpectedToken, tokens.current end end end @@ -225,20 +231,20 @@ class BinaryOperator < SQLTree::Node::Expression end def self.parse_rhs(tokens, precedence, operator = nil) - if ['IN', 'NOT IN'].include?(operator) + if ["IN", "NOT IN"].include?(operator) if SQLTree::Token::SELECT === tokens.peek(2) return SQLTree::Node::SubQuery.parse(tokens) else return List.parse(tokens) end - elsif ['IS', 'IS NOT'].include?(operator) + elsif ["IS", "IS NOT"].include?(operator) tokens.consume(SQLTree::Token::NULL) return SQLTree::Node::Expression::Value.new(nil) - elsif ['BETWEEN'].include?(operator) + elsif ["BETWEEN"].include?(operator) expr = parse_atomic(tokens) operator = parse_operator(tokens) rhs = parse_rhs(tokens, precedence, operator) - expr = self.new(:operator => operator, :lhs => expr, :rhs => rhs) + expr = self.new(operator: operator, lhs: expr, rhs: rhs) return expr else return parse(tokens, precedence + 1) @@ -252,7 +258,7 @@ class PrefixOperator < SQLTree::Node::Expression class Field < Variable def to_sql(options = {}) - @table.nil? ? quote_field_name(@name) : quote_field_name(@table) + '.' + quote_field_name(@name) + @table.nil? ? quote_field_name(@name) : quote_field_name(@table) + "." + quote_field_name(@name) end end @@ -268,11 +274,11 @@ def initialize(value, escape = nil) def to_sql(options = {}) case value - when nil; 'NULL' - when String; "#{escape_string}#{quote_str(@value)}" - when Numeric; @value.to_s - when Date; @value.strftime("'%Y-%m-%d'") - when DateTime, Time; @value.strftime("'%Y-%m-%d %H:%M:%S'") + when nil then "NULL" + when String then "#{escape_string}#{quote_str(@value)}" + when Numeric then @value.to_s + when Date then @value.strftime("'%Y-%m-%d'") + when DateTime, Time then @value.strftime("'%Y-%m-%d %H:%M:%S'") else raise "Don't know how te represent this value in SQL!" end end @@ -321,14 +327,13 @@ def self.parse_atomic(tokens) end class InsertQuery < Base - - def to_sql(options = { }) - sql = "INSERT INTO #{ table.to_sql(options)} " - sql << '(' + fields.map { |f| f.to_sql(options) }.join(', ') + ') ' if fields - sql << 'VALUES' + def to_sql(options = {}) + sql = "INSERT INTO #{table.to_sql(options)} " + sql << "(" + fields.map { |f| f.to_sql(options) }.join(", ") + ") " if fields + sql << "VALUES" sql << values.map do |value| - ' (' + value.map { |v| v.to_sql(options) }.join(', ') + ')' - end.join(',') + " (" + value.map { |v| v.to_sql(options) }.join(", ") + ")" + end.join(",") sql end @@ -344,7 +349,8 @@ def self.parse_value_list(tokens) values << parse_list(tokens) tokens.consume(SQLTree::Token::RPAREN) end - return values + values end end end +# rubocop:enable Style/CaseEquality diff --git a/lib/active_record/turntable/util.rb b/lib/active_record/turntable/util.rb index c4223d69..e04c152a 100644 --- a/lib/active_record/turntable/util.rb +++ b/lib/active_record/turntable/util.rb @@ -1,7 +1,5 @@ module ActiveRecord::Turntable module Util - extend self - def ar_version_equals_or_later?(version) ar_version >= Gem::Version.new(version) end @@ -10,28 +8,12 @@ def ar_version_earlier_than?(version) ar_version < Gem::Version.new(version) end - def ar4? - ActiveRecord::VERSION::MAJOR == 4 - end - - def ar41_or_later? - ar_version_equals_or_later?("4.1") - end - - def earlier_than_ar41? - ar_version_earlier_than?("4.1") - end - - def ar42_or_later? - ar_version_equals_or_later?("4.2") - end - - def earlier_than_ar42? - ar_version_earlier_than?("4.2") - end - def ar_version - ActiveRecord::gem_version + ActiveRecord.gem_version end + + module_function :ar_version_equals_or_later?, + :ar_version_earlier_than?, + :ar_version end end diff --git a/lib/active_record/turntable/version.rb b/lib/active_record/turntable/version.rb index 17ea519e..e6cab3d1 100644 --- a/lib/active_record/turntable/version.rb +++ b/lib/active_record/turntable/version.rb @@ -1,5 +1,5 @@ module ActiveRecord module Turntable - VERSION = "2.5.0" + VERSION = "3.0.0.alpha3".freeze end end diff --git a/lib/activerecord-turntable.rb b/lib/activerecord-turntable.rb index 913108ed..aa9085f0 100644 --- a/lib/activerecord-turntable.rb +++ b/lib/activerecord-turntable.rb @@ -1,2 +1,3 @@ -require 'active_record' -require 'active_record/turntable' +# rubocop:disable Style/FileName +require "active_record" +require "active_record/turntable" diff --git a/script/performance/algorithm b/script/performance/algorithm index 37033b79..b56d4bc5 100755 --- a/script/performance/algorithm +++ b/script/performance/algorithm @@ -1,14 +1,14 @@ #!/usr/bin/env ruby -require 'rspec' -require File.join(File.dirname(__FILE__), '../../../spec/spec_helper') -require 'benchmark' +require "rspec" +require File.join(File.dirname(__FILE__), "../../spec/spec_helper") +require "benchmark" def setup_algorithm(n, alg = "Range") config = { "shards" => n.times.map do |i| - {"connection" => "connection_#{i}", "less_than" => (i+1) * 100} - end - } + { connection: "connection_#{i}", less_than: (i + 1) * 100 } + end, + }.with_indifferent_access "ActiveRecord::Turntable::Algorithm::#{alg}Algorithm".constantize.new(config) end @@ -18,15 +18,14 @@ Benchmark.bm(40) do |x| algorithm = setup_algorithm(n, alg) x.report("#{alg}: selrand(#{n}) * 1000") { 1000.times do - algorithm.calculate(rand(n*100)) + algorithm.calculate(rand(n * 100)) end } x.report("#{alg}: sellast(#{n}) * 1000") { 1000.times do - algorithm.calculate(n*100-1) + algorithm.calculate(n * 100 - 1) end } end end end - diff --git a/spec/active_record/finder_methods_spec.rb b/spec/active_record/finder_methods_spec.rb new file mode 100644 index 00000000..114bdc10 --- /dev/null +++ b/spec/active_record/finder_methods_spec.rb @@ -0,0 +1,34 @@ +require "spec_helper" + +describe ActiveRecord::FinderMethods do + before(:all) do + reload_turntable!(File.join(File.dirname(__FILE__), "../config/turntable.yml")) + end + + before do + establish_connection_to(:test) + truncate_shard + user + end + + let(:user) { + u = User.new + u.id = 1 + u.save + u + } + + context "#find" do + context "pass an ID that exists" do + subject { User.find(1) } + + it { is_expected.to eq(user) } + end + + context "pass an ID that doesn't exist" do + subject { User.find(2) } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end +end diff --git a/spec/active_record/turntable/active_record_ext/association_preloader_spec.rb b/spec/active_record/turntable/active_record_ext/association_preloader_spec.rb index 31f9381c..986b0ffc 100644 --- a/spec/active_record/turntable/active_record_ext/association_preloader_spec.rb +++ b/spec/active_record/turntable/active_record_ext/association_preloader_spec.rb @@ -1,17 +1,17 @@ -require 'spec_helper' +require "spec_helper" -describe ActiveRecord::Turntable::ActiveRecordExt::Association do +describe ActiveRecord::Turntable::ActiveRecordExt::AssociationPreloader do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard end let!(:user) do - user = User.new({:nickname => 'user1'}) + user = User.new({ nickname: "user1" }) user.id = 1 user.save user @@ -42,14 +42,14 @@ context "associated objects has same turntable_key" do subject { CardsUser.where(user: user).preload(:cards_users_histories).first } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } it "its association should be loaded" do expect(subject.association(:cards_users_histories)).to be_loaded end it "its has_many targets should be assigned all related object" do - expect(subject.cards_users_histories).to include(*cards_users_histories.select { |history| history.cards_user_id == subject.id} ) + expect(subject.cards_users_histories).to include(*cards_users_histories.select { |history| history.cards_user_id == subject.id }) end end @@ -57,21 +57,21 @@ context "when foreign_shard_key option passed" do subject { CardsUser.where(user: user).preload(:events_users_histories_with_foreign_shard_key).first } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } it "its association should be loaded" do expect(subject.association(:events_users_histories_with_foreign_shard_key)).to be_loaded end it "its has_many targets should be assigned all related object" do - expect(subject.events_users_histories_with_foreign_shard_key).to include(*events_users_histories.select { |history| history.cards_user_id == subject.id} ) + expect(subject.events_users_histories_with_foreign_shard_key).to include(*events_users_histories.select { |history| history.cards_user_id == subject.id }) end end context "when foreign_shard_key option is not passed" do subject { CardsUser.where(user: user).preload(:events_users_histories).first } - it { expect { subject }.to raise_error } + it { expect { subject }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) } end end end diff --git a/spec/active_record/turntable/active_record_ext/association_spec.rb b/spec/active_record/turntable/active_record_ext/association_spec.rb index bdbcd1e9..67bc61b3 100644 --- a/spec/active_record/turntable/active_record_ext/association_spec.rb +++ b/spec/active_record/turntable/active_record_ext/association_spec.rb @@ -1,17 +1,17 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::ActiveRecordExt::Association do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard end let!(:user) do - user = User.new({:nickname => 'user1'}) + user = User.new({ nickname: "user1" }) user.id = 1 user.save user @@ -38,7 +38,7 @@ context "When a model with has_one relation" do context "When the has_one associated object doesn't exists" do subject { user.user_status } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end end @@ -52,13 +52,13 @@ context "associated objects has same turntable_key" do context "AssociationRelation#to_a" do subject { cards_user.cards_users_histories.to_a } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } it { is_expected.to include(*cards_users_histories.select { |history| history.cards_user_id == cards_user.id }) } end context "AssociationRelation#where" do subject { cards_user.cards_users_histories.where(id: cards_users_history.id).to_a } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } it { is_expected.to include(cards_users_history) } end end @@ -67,14 +67,14 @@ context "when foreign_shard_key option passed" do subject { cards_user.events_users_histories_with_foreign_shard_key.to_a } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } it { is_expected.to include(*events_users_histories.select { |history| history.cards_user_id == cards_user.id }) } end context "when foreign_shard_key option is not passed" do - subject { CardsUser.where(user: user).events_users_histories } + subject { cards_user.events_users_histories.to_a } - it { expect { subject }.to raise_error } + it { expect { subject }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) } end end end diff --git a/spec/active_record/turntable/active_record_ext/clever_load_spec.rb b/spec/active_record/turntable/active_record_ext/clever_load_spec.rb index dbaa04ea..dd03499a 100644 --- a/spec/active_record/turntable/active_record_ext/clever_load_spec.rb +++ b/spec/active_record/turntable/active_record_ext/clever_load_spec.rb @@ -1,23 +1,23 @@ -require 'spec_helper' -require 'logger' +require "spec_helper" +require "logger" describe ActiveRecord::Turntable::ActiveRecordExt::CleverLoad do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard - @user1 = User.new({:nickname => 'user1'}) + @user1 = User.new({ nickname: "user1" }) @user1.id = 1 @user1.save - @user1_status = @user1.create_user_status(:hp => 10, :mp => 10) - @user2 = User.new({:nickname => 'user2'}) + @user1_status = @user1.create_user_status(hp: 10, mp: 10) + @user2 = User.new({ nickname: "user2" }) @user2.id = 2 @user2.save - @user2_status = @user2.create_user_status(:hp => 20, :mp => 10) + @user2_status = @user2.create_user_status(hp: 20, mp: 10) end context "When a model has has_one relation" do @@ -27,7 +27,7 @@ context "With their associations" do subject { users.map { |u| u.association(:user_status) } } - it "should be association target loaded" do + it "makes association target loaded" do is_expected.to all(be_loaded) end end @@ -35,7 +35,7 @@ context "With their targets" do subject { users.map { |u| u.association(:user_status).target } } - it "should be loaded target object" do + it "loads target object" do is_expected.to all(be_instance_of(UserStatus)) end end @@ -49,7 +49,7 @@ context "With their associations" do subject { user_statuses.map { |us| us.association(:user) } } - it "should target loaded" do + it "makes target loaded" do is_expected.to all(be_loaded) end end @@ -57,7 +57,7 @@ context "With their targets" do subject { user_statuses.map { |us| us.association(:user).target } } - it "should be loaded target object" do + it "loads target object" do is_expected.to all(be_instance_of(User)) end end @@ -65,7 +65,7 @@ end context "When a model has has_many relation" do - it "should send query only 2 times." do + it "sends query only 2 times." do skip "not implemented yet" end end diff --git a/spec/active_record/turntable/active_record_ext/fixture_set_spec.rb b/spec/active_record/turntable/active_record_ext/fixture_set_spec.rb index cc7a5b2a..00ddb0e3 100644 --- a/spec/active_record/turntable/active_record_ext/fixture_set_spec.rb +++ b/spec/active_record/turntable/active_record_ext/fixture_set_spec.rb @@ -1,7 +1,7 @@ -require 'spec_helper' +require "spec_helper" -require 'active_record' -require 'active_record/turntable/active_record_ext/fixtures' +require "active_record" +require "active_record/turntable/active_record_ext/fixtures" describe ActiveRecord::FixtureSet do before(:all) do @@ -21,7 +21,7 @@ subject { ActiveRecord::FixtureSet.create_fixtures(fixtures_root, "cards") } it { is_expected.to be_instance_of(Array) } it "creates card records" do - expect {subject}.to change {Card.count}.from(0).to(cards.size) + expect { subject }.to change { Card.count }.from(0).to(cards.size) end end end diff --git a/spec/active_record/turntable/active_record_ext/locking_optimistic_spec.rb b/spec/active_record/turntable/active_record_ext/locking_optimistic_spec.rb index 099a3be8..ab23498f 100644 --- a/spec/active_record/turntable/active_record_ext/locking_optimistic_spec.rb +++ b/spec/active_record/turntable/active_record_ext/locking_optimistic_spec.rb @@ -1,16 +1,13 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::ActiveRecordExt::LockingOptimistic do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard - end - - before(:each) do ActiveRecord::Base.turntable_config.instance_variable_get(:@config)[:raise_on_not_specified_shard_update] = true end diff --git a/spec/active_record/turntable/active_record_ext/log_subscriber_spec.rb b/spec/active_record/turntable/active_record_ext/log_subscriber_spec.rb new file mode 100644 index 00000000..8e6b9459 --- /dev/null +++ b/spec/active_record/turntable/active_record_ext/log_subscriber_spec.rb @@ -0,0 +1,47 @@ +require "spec_helper" + +describe ActiveRecord::Turntable::ActiveRecordExt::LogSubscriber do + before(:all) do + reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) + end + + before do + establish_connection_to + end + + class TestLogSubscriber < ActiveRecord::LogSubscriber + attr_reader :debugs + + def initialize + @debugs = [] + super + end + + def debug(message) + @debugs << message + end + end + + TestEvent = Struct.new(:payload) do + def sql + "foo" + end + + def duration + 0 + end + end + + describe "#sql" do + it "ignore SCHEMA log" do + subscriber = TestLogSubscriber.new + expect(subscriber.debugs.length).to eq 0 + + subscriber.sql(TestEvent.new(name: "bar", turntable_shard_name: "shard_1")) + expect(subscriber.debugs.length).to eq 1 + + subscriber.sql(TestEvent.new(name: "SCHEMA", turntable_shard_name: "shard_1")) + expect(subscriber.debugs.length).to eq 1 + end + end +end diff --git a/spec/active_record/turntable/active_record_ext/persistence_spec.rb b/spec/active_record/turntable/active_record_ext/persistence_spec.rb index f056b552..ad776636 100644 --- a/spec/active_record/turntable/active_record_ext/persistence_spec.rb +++ b/spec/active_record/turntable/active_record_ext/persistence_spec.rb @@ -1,17 +1,17 @@ -require 'spec_helper' -require 'logger' +require "spec_helper" +require "logger" describe ActiveRecord::Turntable::ActiveRecordExt::Persistence do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard end - around(:each) do |example| + around do |example| old = ActiveRecord::Base.logger ActiveRecord::Base.logger = Logger.new(STDOUT) example.run @@ -19,63 +19,63 @@ end let(:user) { - u = User.new({:nickname => 'foobar'}) + u = User.new({ nickname: "foobar" }) u.id = 1 u.updated_at = Time.current - 1.day u.save u } - let(:user_status){ - stat = user.create_user_status(:hp => 10, :mp => 10) + let(:user_status) { + stat = user.create_user_status(hp: 10, mp: 10) stat.updated_at = Time.current - 1.day stat.save stat } - let(:card){ - Card.create!(:name => 'foobar') + let(:card) { + Card.create!(name: "foobar") } - let(:cards_user){ + let(:cards_user) { user.cards_users.create(card: card) } context "When creating record" do context "with blob column" do + subject { user } let(:blob_value) { "\123\123\123" } let(:user) { - u = User.new(:nickname => 'x', :blob => blob_value ) + u = User.new(nickname: "x", blob: blob_value) u.id = 1 u.save u } - subject { user } its(:blob) { is_expected.to eq(user.reload.blob) } end end context "When the model is sharded by surrogate key" do - it "should not changed from normal operation when updating" do + it "doesn't change the behavior when updating" do user.nickname = "fizzbuzz" strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) expect { user.save! - }.to_not raise_error + }.not_to raise_error expect(strio.string).to match(/WHERE `users`\.`id` = #{user.id}[^\s]*$/) end - it "should be saved to target_shard" do + it "is saved to target_shard" do expect(user).to be_saved_to(user.turntable_shard) end - it "should change updated_at when updating" do + it "changes updated_at when updating" do user.nickname = "fizzbuzz" expect { user.save! }.to change(user, :updated_at) end - it "should not changed from normal operation when destroying" do + it "doesn't change the behavior when destroying" do strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) - expect { user.destroy }.to_not raise_error + expect { user.destroy }.not_to raise_error expect(strio.string).to match(/WHERE `users`\.`id` = #{user.id}[^\s]*$/) end end @@ -96,106 +96,111 @@ def on_update context "on update once" do it "callback should be called once" do - expect(user).to receive(:on_update).once + allow(user).to receive(:on_update) user.save + expect(user).to have_received(:on_update).once end end context "on destroy once" do it "callback should be called once" do - expect(user).to receive(:on_destroy).once + allow(user).to receive(:on_destroy) user.destroy + expect(user).to have_received(:on_destroy).once end end end context "When the model is sharded by other key" do - it "should send shard_key condition when updating" do + it "appends shard_key condition to queries when updating" do cards_user.num = 10 strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) expect { cards_user.save! - }.to_not raise_error + }.not_to raise_error expect(strio.string).to match(/`cards_users`\.`user_id` = #{cards_user.user_id}[^\s]*($|\s)/) end - it "should change updated_at when updating" do - cards_user.num = 2 + it "changes updated_at when updating" do + cards_user - expect { - cards_user.save! - }.to change(cards_user, :updated_at) + Timecop.travel(1.day.from_now) do + expect { + cards_user.num = 2 + cards_user.save! + }.to change(cards_user, :updated_at) + end end - it "should send shard_key condition when destroying" do + it "appends shard_key condition to queries when destroying" do strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) expect { cards_user.destroy - }.to_not raise_error + }.not_to raise_error expect(strio.string).to match(/`cards_users`\.`user_id` = #{cards_user.user_id}[^\s]*($|\s)/) end - it "should warn when creating without shard_key" do + it "warns when creating without shard_key" do skip "doesn't need to implemented soon" end - it "should execute one query when reloading" do + it "executes one query when reloading" do user; cards_user strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) - expect { cards_user.reload }.to_not raise_error + expect { cards_user.reload }.not_to raise_error - expect(strio.string.split("\n").select {|stmt| stmt =~ /SELECT/ and stmt !~ /Turntable/ }).to have(1).items + expect(strio.string.split("\n").select { |stmt| stmt =~ /SELECT/ and stmt !~ /Turntable/ }).to have(1).items end - it "should execute one query when touching" do + it "executes one query when touching" do user; cards_user strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) - expect { cards_user.touch }.to_not raise_error - expect(strio.string.split("\n").select {|stmt| stmt =~ /UPDATE/ and stmt !~ /Turntable/ }).to have(1).items + expect { cards_user.touch }.not_to raise_error + expect(strio.string.split("\n").select { |stmt| stmt =~ /UPDATE/ and stmt !~ /Turntable/ }).to have(1).items end - it "should execute one query when locking" do + it "executes one query when locking" do user; cards_user strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) - expect { cards_user.lock! }.to_not raise_error - expect(strio.string.split("\n").select {|stmt| stmt =~ /SELECT/ and stmt !~ /Turntable/ }).to have(1).items + expect { cards_user.lock! }.not_to raise_error + expect(strio.string.split("\n").select { |stmt| stmt =~ /SELECT/ and stmt !~ /Turntable/ }).to have(1).items end - it "should execute one query when update_columns" do + it "executes one query when update_columns" do user; cards_user strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) - expect { cards_user.update_columns(num: 10) }.to_not raise_error - expect(strio.string.split("\n").select {|stmt| stmt =~ /UPDATE/ and stmt !~ /Turntable/ }).to have(1).items + expect { cards_user.update_columns(num: 10) }.not_to raise_error + expect(strio.string.split("\n").select { |stmt| stmt =~ /UPDATE/ and stmt !~ /Turntable/ }).to have(1).items end end context "When the model is not sharded" do - it "should not send shard_key condition when updating" do + it "doesn't append shard_key condition to queries when updating" do card.name = "barbaz" strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) expect { card.save! - }.to_not raise_error + }.not_to raise_error expect(strio.string).to match(/WHERE `cards`\.`id` = #{card.id}[^\s]*$/) end - it "should not send shard_key condition when destroying" do + it "doesn't append shard_key condition to queries when destroying" do strio = StringIO.new ActiveRecord::Base.logger = Logger.new(strio) expect { card.destroy - }.to_not raise_error + }.not_to raise_error expect(strio.string).to match(/WHERE `cards`\.`id` = #{card.id}[^\s]*$/) end end @@ -205,5 +210,4 @@ def on_update it { is_expected.to be_instance_of(CardsUser) } it { is_expected.to eq(cards_user) } end - end diff --git a/spec/active_record/turntable/active_record_ext/schema_dumper_spec.rb b/spec/active_record/turntable/active_record_ext/schema_dumper_spec.rb new file mode 100644 index 00000000..9dc24701 --- /dev/null +++ b/spec/active_record/turntable/active_record_ext/schema_dumper_spec.rb @@ -0,0 +1,25 @@ +require "spec_helper" + +describe ActiveRecord::Turntable::ActiveRecordExt::SchemaDumper do + before(:all) do + reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) + ActiveRecord::SchemaDumper.prepend(ActiveRecord::Turntable::ActiveRecordExt::SchemaDumper) + end + + before do + establish_connection_to(:test) + end + + def dump_schema + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + stream.string + end + + context "#dump" do + subject { dump_schema } + it { is_expected.to match(/create_sequence_for "users", force: :cascade, options: /) } + it { is_expected.not_to match(/create_table "users_id_seq"/) } + it { is_expected.not_to match(/create_sequence_for "users_id_seq".*?do/) } + end +end diff --git a/spec/active_record/turntable/active_record_ext/sequencer_spec.rb b/spec/active_record/turntable/active_record_ext/sequencer_spec.rb index 726b1bbc..0e36f4fd 100644 --- a/spec/active_record/turntable/active_record_ext/sequencer_spec.rb +++ b/spec/active_record/turntable/active_record_ext/sequencer_spec.rb @@ -1,18 +1,18 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::ActiveRecordExt::Sequencer do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard end context "With sequencer enabled model" do subject { User } - its(:sequence_name) { is_expected.to_not be_nil } + its(:sequence_name) { is_expected.not_to be_nil } end context "With sequencer disabled model" do diff --git a/spec/active_record/turntable/active_record_ext/test_fixtures_spec.rb b/spec/active_record/turntable/active_record_ext/test_fixtures_spec.rb index 4e8bbea8..553f2dda 100644 --- a/spec/active_record/turntable/active_record_ext/test_fixtures_spec.rb +++ b/spec/active_record/turntable/active_record_ext/test_fixtures_spec.rb @@ -1,7 +1,7 @@ -require 'spec_helper' +require "spec_helper" -require 'active_record' -require 'active_record/turntable/active_record_ext/fixtures' +require "active_record" +require "active_record/turntable/active_record_ext/fixtures" describe ActiveRecord::TestFixtures do before(:all) do @@ -11,6 +11,7 @@ before do establish_connection_to(:test) truncate_shard + test_fixture_class.fixture_path = fixtures_root end let(:fixtures_root) { File.join(File.dirname(__FILE__), "../../../fixtures") } @@ -19,10 +20,6 @@ let(:test_fixture) { test_fixture_class.new("test") } let(:cards) { YAML.load(ERB.new(IO.read(fixture_file)).result) } - before do - test_fixture_class.fixture_path = fixtures_root - end - describe "#setup_fixtures" do after do test_fixture.teardown_fixtures diff --git a/spec/active_record/turntable/algorithm/modulo_algorithm_spec.rb b/spec/active_record/turntable/algorithm/modulo_algorithm_spec.rb index 610f78b7..347bdc5d 100644 --- a/spec/active_record/turntable/algorithm/modulo_algorithm_spec.rb +++ b/spec/active_record/turntable/algorithm/modulo_algorithm_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Algorithm::ModuloAlgorithm do before(:all) do @@ -27,9 +27,8 @@ context "#calculate with a value that is not a number" do it "raises ActiveRecord::Turntable::CannotSpecifyShardError" do - expect { @alg.calculate('a') }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) + expect { @alg.calculate("a") }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) end end end - end diff --git a/spec/active_record/turntable/algorithm/range_algorithm_spec.rb b/spec/active_record/turntable/algorithm/range_algorithm_spec.rb index 16ac5b92..10d346d3 100644 --- a/spec/active_record/turntable/algorithm/range_algorithm_spec.rb +++ b/spec/active_record/turntable/algorithm/range_algorithm_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Algorithm::RangeAlgorithm do before(:all) do @@ -27,9 +27,8 @@ context "#calculate with 10000000" do it "raises ActiveRecord::Turntable::CannotSpecifyShardError" do - expect { @alg.calculate(10000000) }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) + expect { @alg.calculate(10_000_000) }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) end end end - end diff --git a/spec/active_record/turntable/algorithm/range_bsearch_algorithm_spec.rb b/spec/active_record/turntable/algorithm/range_bsearch_algorithm_spec.rb index bd77eac8..2c81acc2 100644 --- a/spec/active_record/turntable/algorithm/range_bsearch_algorithm_spec.rb +++ b/spec/active_record/turntable/algorithm/range_bsearch_algorithm_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Algorithm::RangeBsearchAlgorithm do before(:all) do @@ -27,9 +27,8 @@ context "#calculate with 10000000" do it "raises ActiveRecord::Turntable::CannotSpecifyShardError" do - expect { @alg.calculate(10000000) }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) + expect { @alg.calculate(10_000_000) }.to raise_error(ActiveRecord::Turntable::CannotSpecifyShardError) end end end - end diff --git a/spec/active_record/turntable/algorithm_spec.rb b/spec/active_record/turntable/algorithm_spec.rb index a87685d8..979e4bf6 100644 --- a/spec/active_record/turntable/algorithm_spec.rb +++ b/spec/active_record/turntable/algorithm_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Algorithm do before(:all) do @@ -31,7 +31,7 @@ end it "called with 10 returns {\"user_shard_1\" => 10}" do - expect(algorithm.calculate_used_shards_with_weight(10)).to eq({"user_shard_1" => 10}) + expect(algorithm.calculate_used_shards_with_weight(10)).to eq({ "user_shard_1" => 10 }) end it "called with 65000 returns 2 items" do @@ -39,7 +39,7 @@ end it "called with 65000 returns {\"user_shard_1\" => 39999, \"user_shard_2\" => 25001}" do - expect(algorithm.calculate_used_shards_with_weight(65000)).to eq({"user_shard_1" => 39999, "user_shard_2" => 25001}) + expect(algorithm.calculate_used_shards_with_weight(65000)).to eq({ "user_shard_1" => 39999, "user_shard_2" => 25001 }) end end end @@ -70,7 +70,7 @@ end it "called with 10 returns {\"user_shard_1\" => 10}" do - expect(algorithm.calculate_used_shards_with_weight(10)).to eq({"user_shard_1" => 10}) + expect(algorithm.calculate_used_shards_with_weight(10)).to eq({ "user_shard_1" => 10 }) end it "called with 65000 returns 2 items" do @@ -78,7 +78,7 @@ end it "called with 65000 returns {\"user_shard_1\" => 39999, \"user_shard_2\" => 25001}" do - expect(algorithm.calculate_used_shards_with_weight(65000)).to eq({"user_shard_1" => 39999, "user_shard_2" => 25001}) + expect(algorithm.calculate_used_shards_with_weight(65000)).to eq({ "user_shard_1" => 39999, "user_shard_2" => 25001 }) end end end diff --git a/spec/active_record/turntable/base_spec.rb b/spec/active_record/turntable/base_spec.rb index 9851a16d..0fd28615 100644 --- a/spec/active_record/turntable/base_spec.rb +++ b/spec/active_record/turntable/base_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Base do before(:all) do diff --git a/spec/active_record/turntable/transaction_spec.rb b/spec/active_record/turntable/cluster_helper_methods_spec.rb similarity index 76% rename from spec/active_record/turntable/transaction_spec.rb rename to spec/active_record/turntable/cluster_helper_methods_spec.rb index bc5eff34..f638ef0f 100644 --- a/spec/active_record/turntable/transaction_spec.rb +++ b/spec/active_record/turntable/cluster_helper_methods_spec.rb @@ -1,19 +1,19 @@ -require 'spec_helper' +require "spec_helper" -describe "transaction" do +describe ActiveRecord::Turntable::ClusterHelperMethods do before(:all) do reload_turntable!(File.join(File.dirname(__FILE__), "../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard end let(:clusters) { ActiveRecord::Base.turntable_clusters } - describe "all_cluster_transaction" do + describe ".all_cluster_transaction" do let(:all_clusters) { clusters.values } - let(:shards) { all_clusters.map { |c| c.shards.values }.flatten(1) } + let(:shards) { all_clusters.flat_map { |c| c.shards.values } } it "all shards should begin transaction" do User.all_cluster_transaction { @@ -22,7 +22,7 @@ end end - describe "cluster_transaction" do + describe ".cluster_transaction" do let(:cluster) { clusters[:user_cluster] } let(:shards) { cluster.shards.values } diff --git a/spec/active_record/turntable/cluster_spec.rb b/spec/active_record/turntable/cluster_spec.rb index cb81a1e0..74612ea9 100644 --- a/spec/active_record/turntable/cluster_spec.rb +++ b/spec/active_record/turntable/cluster_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Cluster do before(:all) do diff --git a/spec/active_record/turntable/config_spec.rb b/spec/active_record/turntable/config_spec.rb index 74814219..19fa50b3 100644 --- a/spec/active_record/turntable/config_spec.rb +++ b/spec/active_record/turntable/config_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Config do before(:all) do diff --git a/spec/active_record/turntable/connection_proxy_spec.rb b/spec/active_record/turntable/connection_proxy_spec.rb index a4255d29..81166066 100644 --- a/spec/active_record/turntable/connection_proxy_spec.rb +++ b/spec/active_record/turntable/connection_proxy_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::ConnectionProxy do before(:all) do @@ -11,9 +11,8 @@ truncate_shard end - let(:cluster) { ActiveRecord::Turntable::Cluster.new(ActiveRecord::Base.turntable_config[:clusters][:user_cluster]) } subject { ActiveRecord::Turntable::ConnectionProxy.new(User, cluster) } - + let(:cluster) { ActiveRecord::Turntable::Cluster.new(ActiveRecord::Base.turntable_config[:clusters][:user_cluster]) } its(:master_connection) { is_expected.to eql(ActiveRecord::Base.connection) } end @@ -23,7 +22,7 @@ truncate_shard end - it "should be saved to user_shard_1 with id = 1" do + it "is saved to user_shard_1 with id = 1" do user = User.new user.id = 1 expect { @@ -31,7 +30,7 @@ }.not_to raise_error end - it "should be saved to user_shard_2 with id = 30000" do + it "is saved to user_shard_2 with id = 30000" do user = User.new user.id = 30000 expect { @@ -39,7 +38,7 @@ }.not_to raise_error end - it "should be saved to user_shard_2 with id = 30000 with SQL injection attack" do + it "is saved to user_shard_2 with id = 30000 with SQL injection attack" do user = User.new user.id = 30000 user.nickname = "hogehgoge'00" @@ -49,7 +48,7 @@ user.reload end - it "should should be saved the same string when includes escaped string" do + it "is saved the same string when includes escaped string" do user = User.new user.id = 30000 user.nickname = "hoge@\n@\\@@\\nhoge\\\nhoge\\n" @@ -86,15 +85,14 @@ @user2.save! end - it "should be saved to user_shard_1 with id = 1" do + it "is saved to user_shard_1 with id = 1" do @user1.nickname = "foobar" expect { @user1.save! }.not_to raise_error - end - it "should be saved to user_shard_2 with id = 30000" do + it "is saved to user_shard_2 with id = 30000" do @user2.nickname = "hogehoge" expect { @user2.save! @@ -102,10 +100,10 @@ end it "User.where('id IN (1, 30000)') returns 2 record" do - expect(User.where(:id => [1, 30000]).all.size).to eq(2) + expect(User.where(id: [1, 30000]).all.size).to eq(2) end - it "count should be 2" do + it "User.count is 2" do expect(User.count).to eq(2) end @@ -120,11 +118,11 @@ truncate_shard @user1 = User.new @user1.id = 1 - @user1.nickname = 'user1' + @user1.nickname = "user1" @user1.save! @user2 = User.new @user2.id = 30000 - @user2.nickname = 'user2' + @user2.nickname = "user2" @user2.save! end @@ -167,11 +165,11 @@ truncate_shard @user1 = User.new @user1.id = 1 - @user1.nickname = 'user1' + @user1.nickname = "user1" @user1.save! @user2 = User.new @user2.id = 30000 - @user2.nickname = 'user2' + @user2.nickname = "user2" @user2.save! end @@ -185,11 +183,11 @@ truncate_shard @user1 = User.new @user1.id = 1 - @user1.nickname = 'user1' + @user1.nickname = "user1" @user1.save! @user2 = User.new @user2.id = 30000 - @user2.nickname = 'user2' + @user2.nickname = "user2" @user2.save! end @@ -203,15 +201,15 @@ truncate_shard @user1 = User.new @user1.id = 1 - @user1.nickname = 'user1' + @user1.nickname = "user1" @user1.save! @user2 = User.new @user2.id = 30000 - @user2.nickname = 'user2' + @user2.nickname = "user2" @user2.save! end - subject { User.exists?(nickname: 'user2') } + subject { User.exists?(nickname: "user2") } it { is_expected.to be_truthy } end @@ -221,33 +219,33 @@ truncate_shard @user1 = User.new @user1.id = 1 - @user1.nickname = 'user1' + @user1.nickname = "user1" @user1.save! @user2 = User.new @user2.id = 30000 - @user2.nickname = 'user2' + @user2.nickname = "user2" @user2.save! end - subject { User.exists?(nickname: 'user999') } + subject { User.exists?(nickname: "user999") } it { is_expected.to be_falsey } end - context "#table_exists?" do + context "#data_source_exists?" do before do establish_connection_to(:test) truncate_shard @user1 = User.new @user1.id = 1 - @user1.nickname = 'user1' + @user1.nickname = "user1" @user1.save! @user2 = User.new @user2.id = 30000 - @user2.nickname = 'user2' + @user2.nickname = "user2" @user2.save! end - subject { User.connection.table_exists?(:users) } + subject { User.connection.data_source_exists?(:users) } it { is_expected.to be_truthy } end end diff --git a/spec/active_record/turntable/finder_spec.rb b/spec/active_record/turntable/finder_spec.rb deleted file mode 100644 index 05124a14..00000000 --- a/spec/active_record/turntable/finder_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' - -describe "ActiveRecord::FinderMethods" do - before(:all) do - reload_turntable!(File.join(File.dirname(__FILE__), "../../config/turntable.yml")) - end - - context "User insert with id" do - before do - establish_connection_to(:test) - truncate_shard - user - end - - let(:user) { - u = User.new - u.id = 1 - u.save - u - } - - describe "User#find" do - context "With existing users.id" do - subject { User.find(1) } - - it "#find should be returns user" do - is_expected.to eq(user) - end - end - - context "With users.id not existing" do - subject { User.find(2) } - - it "#find should raise error" do - expect { subject }.to raise_error - end - end - end - end -end diff --git a/spec/active_record/turntable/active_record_ext/migration_spec.rb b/spec/active_record/turntable/migration_spec.rb similarity index 61% rename from spec/active_record/turntable/active_record_ext/migration_spec.rb rename to spec/active_record/turntable/migration_spec.rb index e585d566..2564ddd9 100644 --- a/spec/active_record/turntable/active_record_ext/migration_spec.rb +++ b/spec/active_record/turntable/migration_spec.rb @@ -1,11 +1,11 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Migration do before(:all) do - reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) + reload_turntable!(File.join(File.dirname(__FILE__), "../../config/turntable.yml")) end - before(:each) do + before do establish_connection_to(:test) truncate_shard end @@ -15,19 +15,19 @@ context "With clusters definitions" do let(:migration_class) { - klass = Class.new(ActiveRecord::Migration) { - clusters :user_cluster + Class.new(ActiveRecord::Migration[5.0]) { + clusters :user_cluster } } - let(:cluster_config) { ActiveRecord::Base.turntable_config["clusters"]["user_cluster"] } - let(:user_cluster_shards) { cluster_config["shards"].map { |s| s["connection"] } } + let(:cluster_config) { ActiveRecord::Base.turntable_config[:clusters][:user_cluster] } + let(:user_cluster_shards) { cluster_config[:shards].map { |s| s[:connection] } } it { is_expected.to eq(user_cluster_shards) } end context "With shards definitions" do let(:migration_class) { - klass = Class.new(ActiveRecord::Migration) { + Class.new(ActiveRecord::Migration[5.0]) { shards :user_shard_01 } } diff --git a/spec/active_record/turntable/mixer/fader_spec.rb b/spec/active_record/turntable/mixer/fader_spec.rb index 05085a3c..d3d40aec 100644 --- a/spec/active_record/turntable/mixer/fader_spec.rb +++ b/spec/active_record/turntable/mixer/fader_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Mixer::Fader do end diff --git a/spec/active_record/turntable/mixer_spec.rb b/spec/active_record/turntable/mixer_spec.rb index a76070d2..75e2086c 100644 --- a/spec/active_record/turntable/mixer_spec.rb +++ b/spec/active_record/turntable/mixer_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Mixer do before(:all) do @@ -26,7 +26,7 @@ it { is_expected.to be_instance_of(Hash) } it { is_expected.to have_key(1) } - it { expect([1]).to have(1).item } + it { is_expected.to have(1).item } end context "When call divide_insert_values with Bulk INSERT and shard_key 'id'" do @@ -37,9 +37,7 @@ it { is_expected.to be_instance_of(Hash) } it { is_expected.to have_key(3) } - it { expect([1]).to have(1).item } - it { expect([2]).to have(1).item } - it { expect([3]).to have(1).item } + it { expect(subject.values).to all(have(1).item) } end end @@ -85,7 +83,7 @@ } it { is_expected.to be_instance_of Array } - it { is_expected.to eq([1,2,3,4,5]) } + it { is_expected.to eq([1, 2, 3, 4, 5]) } end context "When call find_shard_keys with not determine shardkey condition" do @@ -108,7 +106,5 @@ it { is_expected.to eq([]) } end end - end - end diff --git a/spec/active_record/turntable/query_cache_spec.rb b/spec/active_record/turntable/query_cache_spec.rb new file mode 100644 index 00000000..a55473ac --- /dev/null +++ b/spec/active_record/turntable/query_cache_spec.rb @@ -0,0 +1,28 @@ +require "spec_helper" +require "active_support/executor" + +describe ActiveRecord::Turntable::QueryCache do + before(:all) do + reload_turntable!(File.join(File.dirname(__FILE__), "../../config/turntable.yml")) + end + + before do + establish_connection_to(:test) + truncate_shard + end + + subject { mw.call({}) } + let(:mw) { + executor = Class.new(ActiveSupport::Executor) + ActiveRecord::Turntable::QueryCache.install_executor_hooks executor + ->(_env) { + executor.wrap { + [200, {}, nil] + } + } + } + + it "returns 200 response" do + expect(subject.first).to eq(200) + end +end diff --git a/spec/active_record/turntable/rack/query_cache_spec.rb b/spec/active_record/turntable/rack/query_cache_spec.rb deleted file mode 100644 index 759b6f78..00000000 --- a/spec/active_record/turntable/rack/query_cache_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe ActiveRecord::Turntable::Rack::QueryCache do - before(:all) do - reload_turntable!(File.join(File.dirname(__FILE__), "../../../config/turntable.yml")) - end - - before do - establish_connection_to(:test) - truncate_shard - end - - let(:mw) { ActiveRecord::Turntable::Rack::QueryCache.new lambda {|env| [200, {}]} } - subject { mw.call({}) } - - it "should returns 200 response" do - expect(subject.first).to eq(200) - end -end diff --git a/spec/active_record/turntable/sequencer/api_spec.rb b/spec/active_record/turntable/sequencer/api_spec.rb index a7bf9595..340043e4 100644 --- a/spec/active_record/turntable/sequencer/api_spec.rb +++ b/spec/active_record/turntable/sequencer/api_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Sequencer::Api do before(:all) do @@ -10,7 +10,7 @@ let(:klass) { Class.new } let(:api_host) { "example.example" } let(:api_port) { 80 } - let(:options) { { "api_host" => api_host, "api_port" => api_port } } + let(:options) { { api_host: api_host, api_port: api_port }.with_indifferent_access } let(:api_response) { 1024 } let(:next_sequence_uri) { "http://#{api_host}/sequences/#{sequence_name}/new" } @@ -18,7 +18,7 @@ describe "#next_sequence_value" do before do - stub_request(:get, next_sequence_uri).to_return(:body => api_response.to_s) + stub_request(:get, next_sequence_uri).to_return(body: api_response.to_s) end subject { sequencer.next_sequence_value(sequence_name) } @@ -28,7 +28,7 @@ describe "#current_sequence_value" do before do - stub_request(:get, current_sequence_uri).to_return(:body => api_response.to_s) + stub_request(:get, current_sequence_uri).to_return(body: api_response.to_s) end subject { sequencer.current_sequence_value(sequence_name) } diff --git a/spec/active_record/turntable/sequencer/barrage_spec.rb b/spec/active_record/turntable/sequencer/barrage_spec.rb index 2489b998..7dc0f2b3 100644 --- a/spec/active_record/turntable/sequencer/barrage_spec.rb +++ b/spec/active_record/turntable/sequencer/barrage_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Sequencer::Barrage do before(:all) do @@ -7,7 +7,7 @@ let(:sequencer) { ActiveRecord::Turntable::Sequencer::Barrage.new(klass, options) } let(:sequence_name) { "hogefuga" } - let(:options) { { "options" => { "generators" => [ {"name" => "sequence", "length" => 16} ] } } } + let(:options) { { options: { generators: [{ name: "sequence", length: 16 }] } }.with_indifferent_access } let(:klass) { Class.new } describe "#next_sequence_value" do diff --git a/spec/active_record/turntable/sequencer/mysql_spec.rb b/spec/active_record/turntable/sequencer/mysql_spec.rb index a9831043..2128671f 100644 --- a/spec/active_record/turntable/sequencer/mysql_spec.rb +++ b/spec/active_record/turntable/sequencer/mysql_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Sequencer::Mysql do before(:all) do diff --git a/spec/active_record/turntable/shard_spec.rb b/spec/active_record/turntable/shard_spec.rb index 00bb8238..ef24f3af 100644 --- a/spec/active_record/turntable/shard_spec.rb +++ b/spec/active_record/turntable/shard_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable::Shard do before(:all) do diff --git a/spec/active_record/turntable/sql_tree_patch_spec.rb b/spec/active_record/turntable/sql_tree_patch_spec.rb index 24524c8a..be02ff6e 100644 --- a/spec/active_record/turntable/sql_tree_patch_spec.rb +++ b/spec/active_record/turntable/sql_tree_patch_spec.rb @@ -1,5 +1,5 @@ -require 'spec_helper' -require 'active_record/turntable/sql_tree_patch' +require "spec_helper" +require "active_record/turntable/sql_tree_patch" describe SQLTree do before(:all) do @@ -8,7 +8,7 @@ context "Insert query with binary string" do subject { SQLTree["INSERT INTO `hogehoge` (`name`) VALUES (x'deadbeef')"] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } its(:to_sql) { is_expected.to include("x'deadbeef") } end @@ -16,7 +16,7 @@ ["FORCE INDEX", "IGNORE INDEX", "USE INDEX"].each do |hint| context hint do subject { SQLTree["SELECT * FROM table #{hint} (`foo`) WHERE field = 'value'"] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } its(:to_sql) { is_expected.to include("#{hint} (`foo`)") } end end @@ -24,11 +24,11 @@ context "Select query without index hint" do subject { SQLTree["SELECT * FROM table WHERE field = 'value'"] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context "Delete query" do subject { SQLTree["DELETE FROM table"] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end end diff --git a/spec/active_record/turntable_spec.rb b/spec/active_record/turntable_spec.rb index 1461adae..30976de6 100644 --- a/spec/active_record/turntable_spec.rb +++ b/spec/active_record/turntable_spec.rb @@ -1,22 +1,22 @@ -require 'spec_helper' +require "spec_helper" describe ActiveRecord::Turntable do before(:all) do - ActiveRecord::Base.send(:include, ActiveRecord::Turntable) + ActiveRecord::Base.include(ActiveRecord::Turntable) end context "#config_file" do - it "should return Rails.root/config/turntable.yml default" do - unless defined?(::Rails); class ::Rails; end; end + it "returns Rails.root/config/turntable.yml default" do + stub_const("Rails", Class.new) allow(Rails).to receive(:root) { "/path/to/rails_root" } ActiveRecord::Base.turntable_config_file = nil expect(ActiveRecord::Base.turntable_config_file).to eq("/path/to/rails_root/config/turntable.yml") end end - context "#config_file=" do - it "should set config_file" do - ActiveRecord::Base.send(:include, ActiveRecord::Turntable) + context "#turntable_config_file=" do + it "set `#turntable_config_file`" do + ActiveRecord::Base.include(ActiveRecord::Turntable) filename = "hogefuga" ActiveRecord::Base.turntable_config_file = filename expect(ActiveRecord::Base.turntable_config_file).to eq(filename) diff --git a/spec/activerecord_helper.rb b/spec/activerecord_helper.rb new file mode 100644 index 00000000..a6d94765 --- /dev/null +++ b/spec/activerecord_helper.rb @@ -0,0 +1,6 @@ +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) +require "active_record" +require "activerecord-turntable" +ActiveRecord::Base.include(ActiveRecord::Turntable) +require "active_record/turntable/active_record_ext/database_tasks" +warn "[turntable] turntable injected." diff --git a/spec/config/activerecord_config.yml b/spec/config/activerecord_config.yml new file mode 100644 index 00000000..ea8d1377 --- /dev/null +++ b/spec/config/activerecord_config.yml @@ -0,0 +1,13 @@ +default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %> + +with_manual_interventions: false + +connections: + mysql2: + arunit: + username: root + encoding: utf8 + collation: utf8_unicode_ci + arunit2: + username: root + encoding: utf8 diff --git a/spec/migrations/002_create_user_statuses.rb b/spec/migrations/002_create_user_statuses.rb index 8599d526..b5b5fce6 100644 --- a/spec/migrations/002_create_user_statuses.rb +++ b/spec/migrations/002_create_user_statuses.rb @@ -1,10 +1,10 @@ class CreateUserStatuses < ActiveRecord::Migration def self.up create_table :user_statuses do |t| - t.belongs_to :user, :null => false - t.integer :hp, :null => false, :default => 0 - t.integer :mp, :null => false, :default => 0 - t.datetime :deleted_at, :default => nil + t.belongs_to :user, null: false + t.integer :hp, null: false, default: 0 + t.integer :mp, null: false, default: 0 + t.datetime :deleted_at, default: nil t.timestamps end diff --git a/spec/migrations/003_create_cards.rb b/spec/migrations/003_create_cards.rb index a10be7f2..678922c3 100644 --- a/spec/migrations/003_create_cards.rb +++ b/spec/migrations/003_create_cards.rb @@ -1,9 +1,9 @@ class CreateCards < ActiveRecord::Migration def self.up create_table :cards do |t| - t.string :name, :null => false - t.integer :hp, :null => false, :default => 0 - t.integer :mp, :null => false, :default => 0 + t.string :name, null: false + t.integer :hp, null: false, default: 0 + t.integer :mp, null: false, default: 0 t.timestamps end end diff --git a/spec/migrations/004_create_cards_users.rb b/spec/migrations/004_create_cards_users.rb index 91b93d4a..6cfeae12 100644 --- a/spec/migrations/004_create_cards_users.rb +++ b/spec/migrations/004_create_cards_users.rb @@ -1,8 +1,8 @@ class CreateCardsUsers < ActiveRecord::Migration def self.up create_table :cards_users do |t| - t.belongs_to :card, :null => false - t.belongs_to :user, :null => false + t.belongs_to :card, null: false + t.belongs_to :user, null: false t.datetime :deleted_at t.timestamps diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1623c18b..c92bd7e2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,29 +1,27 @@ -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) $LOAD_PATH.unshift(File.dirname(__FILE__)) -require 'rubygems' -require 'bundler/setup' -require 'rspec/its' -require 'rspec/collection_matchers' -require 'webmock/rspec' -require 'pry' -begin - require 'pry-byebug' -rescue LoadError -end - -require 'activerecord-turntable' - -require 'coveralls' +require "rubygems" +require "bundler/setup" +require "rspec/its" +require "rspec/collection_matchers" +require "webmock/rspec" +require "pry" +require "timecop" +require "pry-byebug" + +require "activerecord-turntable" + +require "coveralls" Coveralls.wear! -MIGRATIONS_ROOT = File.expand_path(File.join(File.dirname(__FILE__), 'migrations')) +MIGRATIONS_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "migrations")) # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. -ActiveRecord::Base.configurations = YAML.load_file(File.join(File.dirname(__FILE__), 'config/database.yml')) +ActiveRecord::Base.configurations = YAML.load_file(File.join(File.dirname(__FILE__), "config/database.yml")) ActiveRecord::Base.establish_connection(:test) -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } RSpec.configure do |config| include TurntableHelper @@ -32,6 +30,6 @@ config.run_all_when_everything_filtered = true config.before(:each) do - Dir[File.join(File.dirname(File.dirname(__FILE__)), 'spec/models/*.rb')].each { |f| require f } + Dir[File.join(File.dirname(File.dirname(__FILE__)), "spec/models/*.rb")].each { |f| require f } end end diff --git a/spec/support/turntable_helper.rb b/spec/support/turntable_helper.rb index eab68582..10de2c63 100644 --- a/spec/support/turntable_helper.rb +++ b/spec/support/turntable_helper.rb @@ -1,16 +1,16 @@ -require 'active_record' +require "active_record" module TurntableHelper def reload_turntable!(config_file_name = nil) - ActiveRecord::Base.send(:include, ActiveRecord::Turntable) + ActiveRecord::Base.include(ActiveRecord::Turntable) ActiveRecord::Base.turntable_config_file = config_file_name ActiveRecord::Turntable::Config.load!(ActiveRecord::Base.turntable_config_file, :test) end def establish_connection_to(env = :test) silence_warnings { - Object.const_set('RAILS_ENV', env.to_s) - Object.const_set('Rails', Object.new) + Object.const_set("RAILS_ENV", env.to_s) + Object.const_set("Rails", Object.new) allow(Rails).to receive(:env) { ActiveSupport::StringInquirer.new(RAILS_ENV) } ActiveRecord::Base.logger = Logger.new("/dev/null") } @@ -19,7 +19,7 @@ def establish_connection_to(env = :test) def truncate_shard ActiveRecord::Base.descendants.each do |klass| - next if klass.abstract_class? + next if klass.abstract_class? || klass == ActiveRecord::SchemaMigration klass.delete_all end end diff --git a/tmp/rails b/tmp/rails new file mode 160000 index 00000000..912ae0b3 --- /dev/null +++ b/tmp/rails @@ -0,0 +1 @@ +Subproject commit 912ae0b34bf541f18d051c8a274a54aef91a5e04