From 519faaf20428d32d557a1812ab53c4daf1b70153 Mon Sep 17 00:00:00 2001 From: Mick Staugaard Date: Fri, 9 Dec 2011 12:03:09 -0800 Subject: [PATCH] Support for ActiveRecord 3.1 --- Appraisals | 9 ++ Gemfile | 24 ++-- Rakefile | 4 +- gemfiles/activerecord-2.3.gemfile | 16 +++ gemfiles/activerecord-2.3.gemfile.lock | 76 +++++++++++ gemfiles/activerecord-3.1.gemfile | 16 +++ gemfiles/activerecord-3.1.gemfile.lock | 76 +++++++++++ kasket.gemspec | 4 +- lib/kasket.rb | 27 ++-- lib/kasket/read_mixin.rb | 17 ++- lib/kasket/relation_mixin.rb | 9 ++ lib/kasket/select_manager_mixin.rb | 26 ++++ lib/kasket/version.rb | 9 ++ lib/kasket/visitor.rb | 135 +++++++++++++++++++ test/find_some_test.rb | 2 +- test/helper.rb | 10 +- test/parser_test.rb | 176 +++++++++++++++++++++---- test/test_models.rb | 10 +- 18 files changed, 583 insertions(+), 63 deletions(-) create mode 100644 Appraisals create mode 100644 gemfiles/activerecord-2.3.gemfile create mode 100644 gemfiles/activerecord-2.3.gemfile.lock create mode 100644 gemfiles/activerecord-3.1.gemfile create mode 100644 gemfiles/activerecord-3.1.gemfile.lock create mode 100644 lib/kasket/relation_mixin.rb create mode 100644 lib/kasket/select_manager_mixin.rb create mode 100644 lib/kasket/version.rb create mode 100644 lib/kasket/visitor.rb diff --git a/Appraisals b/Appraisals new file mode 100644 index 0000000..87a45dc --- /dev/null +++ b/Appraisals @@ -0,0 +1,9 @@ +appraise "activerecord-2.3" do + gem 'activerecord', '~> 2.3.4' + gem 'temping', '~> 1.3.0' +end + +appraise "activerecord-3.1" do + gem 'activerecord', '~> 3.1.1' + gem 'temping', '~> 2.0.4' +end diff --git a/Gemfile b/Gemfile index 31b34be..da4d88a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,15 @@ source "http://rubygems.org" -gem 'activerecord', '~> 2.3.4' +gem 'appraisal' +gem 'rake' +gem 'bundler' +gem 'shoulda' +gem 'mocha' +gem 'sqlite3', :platforms => :ruby +gem 'activerecord-jdbcsqlite3-adapter', :platforms => :jruby +gem 'ruby-debug', :platforms => :mri_18 +gem 'ruby-debug19', :platforms => :mri_19 -group :development do - gem 'rake' - gem 'bundler' - gem 'shoulda' - gem 'mocha' - gem 'temping', '~> 1.3.0' - gem 'sqlite3', :platforms => :ruby - gem 'activerecord-jdbcsqlite3-adapter', :platforms => :jruby - gem 'ruby-debug', :platforms => :mri_18 - gem 'ruby-debug19', :platforms => :mri_19 -end +#these are overridden in the Appraisals file +gem 'activerecord', '~> 3.1.1' +gem 'temping', '~> 2.0.4' diff --git a/Rakefile b/Rakefile index cf815c3..a5880aa 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,8 @@ -require 'bundler' +require 'bundler/setup' Bundler::GemHelper.install_tasks +require 'appraisal' + require 'rake/testtask' Rake::TestTask.new(:test) do |test| test.libs << 'lib' << 'test' diff --git a/gemfiles/activerecord-2.3.gemfile b/gemfiles/activerecord-2.3.gemfile new file mode 100644 index 0000000..62026bc --- /dev/null +++ b/gemfiles/activerecord-2.3.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "http://rubygems.org" + +gem "appraisal" +gem "rake" +gem "bundler" +gem "shoulda" +gem "mocha" +gem "sqlite3", :platforms=>:ruby +gem "activerecord-jdbcsqlite3-adapter", :platforms=>:jruby +gem "ruby-debug", :platforms=>:mri_18 +gem "ruby-debug19", :platforms=>:mri_19 +gem "activerecord", "~> 2.3.4" +gem "temping", "~> 1.3.0" + diff --git a/gemfiles/activerecord-2.3.gemfile.lock b/gemfiles/activerecord-2.3.gemfile.lock new file mode 100644 index 0000000..6801763 --- /dev/null +++ b/gemfiles/activerecord-2.3.gemfile.lock @@ -0,0 +1,76 @@ +GEM + remote: http://rubygems.org/ + specs: + actionmailer (2.3.14) + actionpack (= 2.3.14) + actionpack (2.3.14) + activesupport (= 2.3.14) + rack (~> 1.1.0) + activerecord (2.3.14) + activesupport (= 2.3.14) + activerecord-jdbc-adapter (1.2.1) + activerecord-jdbcsqlite3-adapter (1.2.1) + activerecord-jdbc-adapter (~> 1.2.1) + jdbc-sqlite3 (~> 3.7.2) + activeresource (2.3.14) + activesupport (= 2.3.14) + activesupport (2.3.14) + appraisal (0.4.0) + bundler + rake + archive-tar-minitar (0.5.2) + columnize (0.3.5) + jdbc-sqlite3 (3.7.2) + linecache (0.46) + rbx-require-relative (> 0.0.4) + linecache19 (0.5.12) + ruby_core_source (>= 0.1.4) + metaclass (0.0.1) + mocha (0.10.0) + metaclass (~> 0.0.1) + rack (1.1.2) + rails (2.3.14) + actionmailer (= 2.3.14) + actionpack (= 2.3.14) + activerecord (= 2.3.14) + activeresource (= 2.3.14) + activesupport (= 2.3.14) + rake (>= 0.8.3) + rake (0.9.2.2) + rbx-require-relative (0.0.5) + ruby-debug (0.10.4) + columnize (>= 0.1) + ruby-debug-base (~> 0.10.4.0) + ruby-debug-base (0.10.4) + linecache (>= 0.3) + ruby-debug-base19 (0.11.25) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby_core_source (>= 0.1.4) + ruby-debug19 (0.11.6) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby-debug-base19 (>= 0.11.19) + ruby_core_source (0.1.5) + archive-tar-minitar (>= 0.5.2) + shoulda (2.11.3) + sqlite3 (1.3.5) + temping (1.3.0) + rails (>= 2.3.3) + +PLATFORMS + java + ruby + +DEPENDENCIES + activerecord (~> 2.3.4) + activerecord-jdbcsqlite3-adapter + appraisal + bundler + mocha + rake + ruby-debug + ruby-debug19 + shoulda + sqlite3 + temping (~> 1.3.0) diff --git a/gemfiles/activerecord-3.1.gemfile b/gemfiles/activerecord-3.1.gemfile new file mode 100644 index 0000000..7db8a61 --- /dev/null +++ b/gemfiles/activerecord-3.1.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "http://rubygems.org" + +gem "appraisal" +gem "rake" +gem "bundler" +gem "shoulda" +gem "mocha" +gem "sqlite3", :platforms=>:ruby +gem "activerecord-jdbcsqlite3-adapter", :platforms=>:jruby +gem "ruby-debug", :platforms=>:mri_18 +gem "ruby-debug19", :platforms=>:mri_19 +gem "activerecord", "~> 3.1.1" +gem "temping", "~> 2.0.4" + diff --git a/gemfiles/activerecord-3.1.gemfile.lock b/gemfiles/activerecord-3.1.gemfile.lock new file mode 100644 index 0000000..5172e1e --- /dev/null +++ b/gemfiles/activerecord-3.1.gemfile.lock @@ -0,0 +1,76 @@ +GEM + remote: http://rubygems.org/ + specs: + activemodel (3.1.3) + activesupport (= 3.1.3) + builder (~> 3.0.0) + i18n (~> 0.6) + activerecord (3.1.3) + activemodel (= 3.1.3) + activesupport (= 3.1.3) + arel (~> 2.2.1) + tzinfo (~> 0.3.29) + activerecord-jdbc-adapter (1.2.1) + activerecord-jdbcsqlite3-adapter (1.2.1) + activerecord-jdbc-adapter (~> 1.2.1) + jdbc-sqlite3 (~> 3.7.2) + activesupport (3.1.3) + multi_json (~> 1.0) + appraisal (0.4.0) + bundler + rake + archive-tar-minitar (0.5.2) + arel (2.2.1) + builder (3.0.0) + columnize (0.3.5) + i18n (0.6.0) + jdbc-sqlite3 (3.7.2) + linecache (0.46) + rbx-require-relative (> 0.0.4) + linecache19 (0.5.12) + ruby_core_source (>= 0.1.4) + metaclass (0.0.1) + mocha (0.10.0) + metaclass (~> 0.0.1) + multi_json (1.0.4) + rake (0.9.2.2) + rbx-require-relative (0.0.5) + ruby-debug (0.10.4) + columnize (>= 0.1) + ruby-debug-base (~> 0.10.4.0) + ruby-debug-base (0.10.4) + linecache (>= 0.3) + ruby-debug-base19 (0.11.25) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby_core_source (>= 0.1.4) + ruby-debug19 (0.11.6) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby-debug-base19 (>= 0.11.19) + ruby_core_source (0.1.5) + archive-tar-minitar (>= 0.5.2) + shoulda (2.11.3) + sqlite3 (1.3.5) + temping (2.0.4) + activerecord (~> 3.1.1) + activesupport (~> 3.1.1) + sqlite3 (~> 1.3.4) + tzinfo (0.3.31) + +PLATFORMS + java + ruby + +DEPENDENCIES + activerecord (~> 3.1.1) + activerecord-jdbcsqlite3-adapter + appraisal + bundler + mocha + rake + ruby-debug + ruby-debug19 + shoulda + sqlite3 + temping (~> 2.0.4) diff --git a/kasket.gemspec b/kasket.gemspec index e2f6e6b..b61474b 100644 --- a/kasket.gemspec +++ b/kasket.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) -require 'kasket' +require 'kasket/version' Gem::Specification.new do |s| s.name = "kasket" @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.summary = "A write back caching layer on active record" s.description = "puts a cap on your queries" - s.add_runtime_dependency("activerecord", "~> 2.3.4") + s.add_runtime_dependency("activerecord", ">= 2.3.4", "< 3.2") s.add_development_dependency("rake") s.add_development_dependency("bundler") diff --git a/lib/kasket.rb b/lib/kasket.rb index 2203438..bb9c91d 100644 --- a/lib/kasket.rb +++ b/lib/kasket.rb @@ -2,33 +2,36 @@ require 'active_record' require 'active_support' -require 'kasket/active_record_patches' +require 'kasket/version' +#require 'kasket/active_record_patches' module Kasket autoload :ConfigurationMixin, 'kasket/configuration_mixin' autoload :ReloadAssociationMixin, 'kasket/reload_association_mixin' autoload :Query, 'kasket/query' + autoload :Visitor, 'kasket/visitor' + autoload :SelectManagerMixin, 'kasket/select_manager_mixin' + autoload :RelationMixin, 'kasket/relation_mixin' CONFIGURATION = {:max_collection_size => 100} - class Version - MAJOR = 1 - MINOR = 0 - PATCH = 2 - STRING = "#{MAJOR}.#{MINOR}.#{PATCH}" - end - module_function def setup(options = {}) - return if ActiveRecord::Base.extended_by.member?(Kasket::ConfigurationMixin) + return if ActiveRecord::Base.respond_to?(:has_kasket) CONFIGURATION[:max_collection_size] = options[:max_collection_size] if options[:max_collection_size] ActiveRecord::Base.extend(Kasket::ConfigurationMixin) - ActiveRecord::Associations::BelongsToAssociation.send(:include, Kasket::ReloadAssociationMixin) - ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin) - ActiveRecord::Associations::HasOneThroughAssociation.send(:include, Kasket::ReloadAssociationMixin) + + if defined?(ActiveRecord::Relation) + ActiveRecord::Relation.send(:include, Kasket::RelationMixin) + Arel::SelectManager.send(:include, Kasket::SelectManagerMixin) + end + + # ActiveRecord::Associations::BelongsToAssociation.send(:include, Kasket::ReloadAssociationMixin) + # ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin) + # ActiveRecord::Associations::HasOneThroughAssociation.send(:include, Kasket::ReloadAssociationMixin) end def self.cache_store=(options) diff --git a/lib/kasket/read_mixin.rb b/lib/kasket/read_mixin.rb index 8e0fc8b..74921ac 100644 --- a/lib/kasket/read_mixin.rb +++ b/lib/kasket/read_mixin.rb @@ -8,8 +8,17 @@ class << base end end - def find_by_sql_with_kasket(sql) - query = kasket_parser.parse(sanitize_sql(sql)) if use_kasket? + def find_by_sql_with_kasket(*args) + sql = args[0] + + if use_kasket? + if sql.is_a?(String) + query = kasket_parser.parse(sanitize_sql(sql)) + else + query = sql.to_kasket_query(self, args[1]) + end + end + if query && has_kasket_index_on?(query[:index]) if query[:key].is_a?(Array) find_by_sql_with_kasket_on_id_array(query[:key]) @@ -21,11 +30,11 @@ def find_by_sql_with_kasket(sql) Array.wrap(value).collect { |record| instantiate(record.dup) } end else - store_in_kasket(query[:key], find_by_sql_without_kasket(sql)) + store_in_kasket(query[:key], find_by_sql_without_kasket(*args)) end end else - find_by_sql_without_kasket(sql) + find_by_sql_without_kasket(*args) end end diff --git a/lib/kasket/relation_mixin.rb b/lib/kasket/relation_mixin.rb new file mode 100644 index 0000000..c58d8f6 --- /dev/null +++ b/lib/kasket/relation_mixin.rb @@ -0,0 +1,9 @@ +module Kasket + module RelationMixin + def to_kasket_query(binds = []) + if arel.is_a?(Arel::SelectManager) + arel.to_kasket_query(klass, binds) + end + end + end +end \ No newline at end of file diff --git a/lib/kasket/select_manager_mixin.rb b/lib/kasket/select_manager_mixin.rb new file mode 100644 index 0000000..b76e076 --- /dev/null +++ b/lib/kasket/select_manager_mixin.rb @@ -0,0 +1,26 @@ +module Kasket + module SelectManagerMixin + def to_kasket_query(klass, binds = []) + query = Kasket::Visitor.new(klass, binds).accept(ast) + + return nil if query.nil? || query == :unsupported + return nil if query[:attributes].blank? + + query[:index] = query[:attributes].map(&:first) + + if query[:limit] + return nil if query[:limit] > 1 + # return nil if !query[:index].include?(:id) + end + + if query[:index].size > 1 && query[:attributes].any? { |attribute, value| value.is_a?(Array) } + return nil + end + + query[:key] = klass.kasket_key_for(query[:attributes]) + query[:key] << '/first' if query[:limit] == 1 && query[:index] != [:id] + + query + end + end +end diff --git a/lib/kasket/version.rb b/lib/kasket/version.rb new file mode 100644 index 0000000..18b5ba6 --- /dev/null +++ b/lib/kasket/version.rb @@ -0,0 +1,9 @@ +# -*- encoding: utf-8 -*- +module Kasket + class Version + MAJOR = 2 + MINOR = 0 + PATCH = 0 + STRING = "#{MAJOR}.#{MINOR}.#{PATCH}" + end +end diff --git a/lib/kasket/visitor.rb b/lib/kasket/visitor.rb new file mode 100644 index 0000000..70a7db8 --- /dev/null +++ b/lib/kasket/visitor.rb @@ -0,0 +1,135 @@ +require 'arel' + +module Kasket + class Visitor < Arel::Visitors::Visitor + def initialize(model_class, binds) + @model_class = model_class + @binds = binds.dup + end + + def accept(object) + self.last_column = nil + super + end + + def last_column=(col) + Thread.current[:arel_visitors_to_sql_last_column] = col + end + + def last_column + Thread.current[:arel_visitors_to_sql_last_column] + end + + def column_for(name) + @model_class.columns_hash[name.to_s] + end + + def visit_Arel_Nodes_SelectStatement(o) + return :unsupported if o.with + return :unsupported if o.offset + return :unsupported if o.lock + return :unsupported if o.orders.any? + return :unsupported if o.cores.size != 1 + + query = visit_Arel_Nodes_SelectCore(o.cores[0]) + return query if query == :unsupported + + query = query.inject({}) do |memo, item| + memo.merge(item) + end + + query.merge!(visit(o.limit)) if o.limit + query + end + + def visit_Arel_Nodes_SelectCore(o) + return :unsupported if o.groups.any? + return :unsupported if o.having + return :unsupported if o.set_quantifier + return :unsupported if !o.source || o.source.empty? + return :unsupported if o.projections.size != 1 + + select = o.projections[0] + select = select.name if select.respond_to?(:name) + return :unsupported if select != '*' + + parts = [visit(o.source)] + + parts += o.wheres.map {|where| visit(where) } + + parts.include?(:unsupported) ? :unsupported : parts + end + + def visit_Arel_Nodes_Limit(o) + {:limit => o.value.to_i} + end + + def visit_Arel_Nodes_JoinSource(o) + return :unsupported if !o.left || o.right.any? + return :unsupported if !o.left.is_a?(Arel::Table) + visit(o.left) + end + + def visit_Arel_Table(o) + {:from => o.name} + end + + def visit_Arel_Nodes_And(o) + attributes = o.children.map { |child| visit(child) } + return :unsupported if attributes.include?(:unsupported) + attributes.sort! { |pair1, pair2| pair1[0].to_s <=> pair2[0].to_s } + { :attributes => attributes } + end + + def visit_Arel_Nodes_In(o) + left = visit(o.left) + return :unsupported if left != :id + + [left, visit(o.right)] + end + + def visit_Arel_Nodes_Equality(o) + right = o.right + [visit(o.left), right ? visit(right) : nil] + end + + def visit_Arel_Attributes_Attribute(o) + self.last_column = column_for(o.name) + o.name.to_sym + end + + def literal(o) + if o == '?' + column, value = @binds.shift + value.to_s + else + o.to_s + end + end + + def visit_Array(o) + o.map {|value| quoted(value) } + end + + #TODO: We are actually not using this? + def quoted(o) + @model_class.connection.quote(o, self.last_column) + end + + alias :visit_String :literal + alias :visit_Fixnum :literal + alias :visit_TrueClass :literal + alias :visit_FalseClass :literal + alias :visit_Arel_Nodes_SqlLiteral :literal + + def method_missing(name, *args, &block) + return :unsupported if name.to_s.start_with?('visit_') + super + end + + def respond_to?(name, include_private = false) + return super || name.to_s.start_with?('visit_') + end + + end +end \ No newline at end of file diff --git a/test/find_some_test.rb b/test/find_some_test.rb index 94c0d16..4deb917 100644 --- a/test/find_some_test.rb +++ b/test/find_some_test.rb @@ -25,7 +25,7 @@ class FindSomeTest < ActiveSupport::TestCase assert(Kasket.cache.read(post1.kasket_key)) assert_nil(Kasket.cache.read(post2.kasket_key)) - Post.expects(:find_by_sql_without_kasket).with("SELECT * FROM \"posts\" WHERE (\"posts\".\"id\" = #{post2.id}) ").returns([post2]) + Post.expects(:find_by_sql_without_kasket).returns([post2]) found_posts = Post.find(post1.id, post2.id) assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort) diff --git a/test/helper.rb b/test/helper.rb index de95375..3e5da8b 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -21,8 +21,6 @@ class ActiveSupport::TestCase include ActiveRecord::TestFixtures - fixtures :all - def create_fixtures(*table_names) if block_given? Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } @@ -39,11 +37,19 @@ def create_fixtures(*table_names) def clear_cache Kasket.cache.clear end + + def ar3? + ActiveRecord::VERSION::MAJOR == 3 + end end ActiveSupport::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" $LOAD_PATH.unshift(ActiveSupport::TestCase.fixture_path) +class ActiveSupport::TestCase + fixtures :all +end + module Rails module_function CACHE = ActiveSupport::Cache::MemoryStore.new diff --git a/test/parser_test.rb b/test/parser_test.rb index 59d3d14..ebdbef7 100644 --- a/test/parser_test.rb +++ b/test/parser_test.rb @@ -9,35 +9,72 @@ class ParserTest < ActiveSupport::TestCase end should 'not support IN queries in combination with other conditions' do - parsed_query = @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3) AND `posts`.`is_active` = 1)') - assert(!parsed_query) + if ar3? + kasket_query = Post.where(:id => [1,2,3], :is_active => true).to_kasket_query + else + kasket_query = @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3) AND `posts`.`is_active` = 1)') + end + assert(!kasket_query) end should "extract conditions" do - assert_equal [[:blog_id, "big"], [:title, "red"]], @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`title` = red AND `posts`.`blog_id` = big)')[:attributes] + if ar3? + kasket_query = Post.where(:title => 'red', :blog_id => 1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`title` = 'red' AND `posts`.`blog_id` = 1)") + end + assert_equal [[:blog_id, "1"], [:title, "red"]], kasket_query[:attributes] end should "extract required index" do - assert_equal [:blog_id, :title], @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`title` = red AND `posts`.`blog_id` = big)')[:index] + if ar3? + kasket_query = Post.where(:title => 'red', :blog_id => 1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`title` = 'red' AND `posts`.`blog_id` = 1)") + end + assert_equal [:blog_id, :title], kasket_query[:index] end should "only support queries against its model's table" do - assert !@parser.parse('SELECT * FROM `apples` WHERE (`users`.`id` = 2) ') + if ar3? + kasket_query = Post.where('users.id' => 2).from('apples').to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `apples` WHERE (`users`.`id` = 2)") + end + assert(!kasket_query) end should "support cachable queries" do - assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) ') - assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) LIMIT 1') + if ar3? + assert Post.where(:id => 1).to_kasket_query + else + assert @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`id` = 2)") + end + + if ar3? + assert Post.where(:id => 1).limit(1).to_kasket_query + else + assert @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`id` = 2) LIMIT 1") + end end should "support IN queries on id" do - parsed_query = @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3))') - assert(parsed_query) - assert_equal([[:id, ['1', '2', '3']]], parsed_query[:attributes]) + if ar3? + kasket_query = Post.where(:id => [1,2,3]).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3))") + end + assert(kasket_query) + assert_equal([[:id, ['1', '2', '3']]], kasket_query[:attributes]) end should "not support IN queries on other attributes" do - assert(!@parser.parse('SELECT * FROM `posts` WHERE (`posts`.`hest` IN (1,2,3))')) + if ar3? + kasket_query = Post.where(:hest => [1,2,3]).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`hest` IN (1,2,3))") + end + assert(!kasket_query) end should "support vaguely formatted queries" do @@ -47,11 +84,19 @@ class ParserTest < ActiveSupport::TestCase context "extract options" do should "provide the limit" do - sql = 'SELECT * FROM `posts` WHERE (`posts`.`id` = 2)' - assert_equal nil, @parser.parse(sql)[:limit] + if ar3? + kasket_query = Post.where(:id => 2).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`id` = 2)") + end + assert_equal nil, kasket_query[:limit] - sql << ' LIMIT 1' - assert_equal 1, @parser.parse(sql)[:limit] + if ar3? + kasket_query = Post.where(:id => 2).limit(1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (`posts`.`id` = 2) LIMIT 1") + end + assert_equal 1, kasket_query[:limit] end end @@ -59,43 +104,108 @@ class ParserTest < ActiveSupport::TestCase context "unsupported queries" do should "include advanced limits" do - assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red AND blog_id = big) LIMIT 2') + if ar3? + kasket_query = Post.where(:title => 'red', :blog_id => 1).limit(2).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (title = 'red' AND blog_id = 1) LIMIT 2") + end + assert !kasket_query end should "include joins" do - assert !@parser.parse('SELECT * FROM `posts`, `trees` JOIN ON apple.tree_id = tree.id WHERE (title = red)') + if ar3? + kasket_query = Post.where(:title => 'test', 'apple.tree_id' => 'posts.id').from(['posts', 'apple']).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts`, `trees` JOIN ON apple.tree_id = tree.id") + end + assert !kasket_query + + if ar3? + kasket_query = Post.where(:title => 'test').joins(:comments).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` JOIN `trees` ON apple.tree_id = tree.id") + end + assert !kasket_query end should "include specific selects" do - assert !@parser.parse('SELECT id FROM `posts` WHERE (title = red)') + if ar3? + kasket_query = Post.where(:title => 'red').select(:id).to_kasket_query + else + kasket_query = @parser.parse("SELECT id FROM `posts` WHERE (title = 'red')") + end + assert !kasket_query end should "include offset" do - assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red) LIMIT 1 OFFSET 2') + if ar3? + kasket_query = Post.where(:title => 'red').limit(1).offset(2).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (title = 'red') LIMIT 1 OFFSET 2") + end + assert !kasket_query end should "include order" do - assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red) ORDER DESC') + if ar3? + kasket_query = Post.where(:title => 'red').order(:title).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (title = 'red') ORDER DESC") + end + assert !kasket_query end should "include the OR operator" do - assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red OR blog_id = big) LIMIT 2') + if ar3? + kasket_query = Post.where("title = 'red' OR blog_id = 1").to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (title = 'red' OR blog_id = 1) LIMIT 2") + end + assert !kasket_query end end context "key generation" do should "include the table name and version" do - assert_match(/^kasket-#{Kasket::Version::STRING}\/posts\/version=3558\//, @parser.parse('SELECT * FROM `posts` WHERE (id = 1)')[:key]) + if ar3? + kasket_query = Post.where(:id => 1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (id = 1)") + end + assert_match(/^kasket-#{Kasket::Version::STRING}\/posts\/version=3558\//, kasket_query[:key]) end should "include all indexed attributes" do - assert_match(/id=1$/, @parser.parse('SELECT * FROM `posts` WHERE (id = 1)')[:key]) - assert_match(/blog_id=2\/id=1$/, @parser.parse('SELECT * FROM `posts` WHERE (id = 1 AND blog_id = 2)')[:key]) - assert_match(/id=1\/title='title'$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1 AND title = 'title')")[:key]) + if ar3? + kasket_query = Post.where(:id => 1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (id = 1)") + end + assert_match(/id=1$/, kasket_query[:key]) + + if ar3? + kasket_query = Post.where(:id => 1, :blog_id => 2).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (id = 1 AND blog_id = 2)") + end + assert_match(/blog_id=2\/id=1$/, kasket_query[:key]) + + if ar3? + kasket_query = Post.where(:id => 1, :title => 'title').to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (id = 1 AND title = 'title')") + end + assert_match(/id=1\/title='title'$/, kasket_query[:key]) end should "generate multiple keys on IN queries" do - keys = @parser.parse('SELECT * FROM `posts` WHERE (id IN (1,2))')[:key] + if ar3? + kasket_query = Post.where(:id => [1,2]).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (id IN (1,2))") + end + + keys = kasket_query[:key] assert_instance_of(Array, keys) assert_match(/id=1$/, keys[0]) assert_match(/id=2$/, keys[1]) @@ -103,10 +213,20 @@ class ParserTest < ActiveSupport::TestCase context "when limit 1" do should "add /first to the key if the index does not include id" do - assert_match(/title='a'\/first$/, @parser.parse("SELECT * FROM `posts` WHERE (title = 'a') LIMIT 1")[:key]) + if ar3? + kasket_query = Post.where(:title => 'a').limit(1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (title = 'a') LIMIT 1") + end + assert_match(/title='a'\/first$/, kasket_query[:key]) end should "not add /first to the key when the index includes id" do - assert_match(/id=1$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1) LIMIT 1")[:key]) + if ar3? + kasket_query = Post.where(:id => 1).limit(1).to_kasket_query + else + kasket_query = @parser.parse("SELECT * FROM `posts` WHERE (id = 1) LIMIT 1") + end + assert_match(/id=1$/, kasket_query[:key]) end end end diff --git a/test/test_models.rb b/test/test_models.rb index 5bad253..314af91 100644 --- a/test/test_models.rb +++ b/test/test_models.rb @@ -1,5 +1,12 @@ require 'temping' -include Temping + +if Temping.respond_to?(:create) + def create_model(name, &block) + Temping.create(name, &block) + end +else + include Temping +end create_model :comment do with_columns do |t| @@ -33,6 +40,7 @@ def make_dirty! self.updated_at = Time.now self.connection.execute("UPDATE posts SET updated_at = '#{updated_at.utc.to_s(:db)}' WHERE id = #{id}") end + kasket_dirty_methods :make_dirty! end