maksar-meta_where 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG +90 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +343 -0
  7. data/Rakefile +11 -0
  8. data/lib/core_ext/hash.rb +5 -0
  9. data/lib/core_ext/symbol.rb +39 -0
  10. data/lib/core_ext/symbol_operators.rb +48 -0
  11. data/lib/meta_where.rb +51 -0
  12. data/lib/meta_where/association_reflection.rb +51 -0
  13. data/lib/meta_where/column.rb +31 -0
  14. data/lib/meta_where/compound.rb +20 -0
  15. data/lib/meta_where/condition.rb +32 -0
  16. data/lib/meta_where/condition_operators.rb +19 -0
  17. data/lib/meta_where/function.rb +108 -0
  18. data/lib/meta_where/join_dependency.rb +105 -0
  19. data/lib/meta_where/join_type.rb +43 -0
  20. data/lib/meta_where/not.rb +13 -0
  21. data/lib/meta_where/relation.rb +290 -0
  22. data/lib/meta_where/utility.rb +51 -0
  23. data/lib/meta_where/version.rb +3 -0
  24. data/lib/meta_where/visitors/attribute.rb +58 -0
  25. data/lib/meta_where/visitors/predicate.rb +149 -0
  26. data/lib/meta_where/visitors/visitor.rb +52 -0
  27. data/meta_where.gemspec +48 -0
  28. data/test/fixtures/companies.yml +17 -0
  29. data/test/fixtures/company.rb +7 -0
  30. data/test/fixtures/data_type.rb +3 -0
  31. data/test/fixtures/data_types.yml +15 -0
  32. data/test/fixtures/developer.rb +5 -0
  33. data/test/fixtures/developers.yml +55 -0
  34. data/test/fixtures/developers_projects.yml +25 -0
  35. data/test/fixtures/fixed_bid_project.rb +2 -0
  36. data/test/fixtures/invalid_company.rb +4 -0
  37. data/test/fixtures/invalid_developer.rb +4 -0
  38. data/test/fixtures/note.rb +3 -0
  39. data/test/fixtures/notes.yml +95 -0
  40. data/test/fixtures/people.yml +14 -0
  41. data/test/fixtures/person.rb +4 -0
  42. data/test/fixtures/project.rb +7 -0
  43. data/test/fixtures/projects.yml +29 -0
  44. data/test/fixtures/schema.rb +53 -0
  45. data/test/fixtures/time_and_materials_project.rb +2 -0
  46. data/test/helper.rb +33 -0
  47. data/test/test_base.rb +21 -0
  48. data/test/test_relations.rb +455 -0
  49. metadata +173 -0
@@ -0,0 +1,149 @@
1
+ require 'meta_where/visitors/visitor'
2
+
3
+ module MetaWhere
4
+ module Visitors
5
+ class Predicate < Visitor
6
+
7
+ def self.visitables
8
+ [Hash, Array, MetaWhere::Or, MetaWhere::And, MetaWhere::Not, MetaWhere::Condition, MetaWhere::Function]
9
+ end
10
+
11
+ def visit_Hash(o, parent)
12
+ parent ||= join_dependency.join_base
13
+ parent = parent.name if parent.is_a? MetaWhere::JoinType
14
+ table = tables[parent]
15
+ predicates = o.map do |column, value|
16
+ if value.is_a?(Hash)
17
+ association = association_from_parent_and_column(parent, column)
18
+ accept(value, association || column)
19
+ elsif [MetaWhere::Condition, MetaWhere::And, MetaWhere::Or, MetaWhere::Not].include?(value.class)
20
+ association = association_from_parent_and_column(parent, column)
21
+ accept(value, association || column)
22
+ elsif value.is_a?(Array) && !value.empty? && value.all? {|v| can_accept?(v)}
23
+ association = association_from_parent_and_column(parent, column)
24
+ value.map {|val| accept(val, association || column)}
25
+ elsif (value.is_a?(ActiveRecord::Base) || array_of_activerecords(value)) &&
26
+ reflection = parent.active_record.reflect_on_association(column.is_a?(MetaWhere::JoinType) ? column.name : column)
27
+ accept_activerecord_values(column, value, parent, reflection)
28
+ else
29
+ if column.is_a?(MetaWhere::Column)
30
+ method = column.method
31
+ column = column.column
32
+ else
33
+ method = method_from_value(value)
34
+ end
35
+
36
+ if [String, Symbol].include?(column.class) && column.to_s.include?('.')
37
+ table_name, column = column.to_s.split('.', 2)
38
+ table = Arel::Table.new(table_name, :engine => parent.arel_engine)
39
+ end
40
+
41
+ unless valid_comparison_method?(method)
42
+ raise ::ActiveRecord::StatementInvalid, "No comparison method named `#{method}` exists for column `#{column}`"
43
+ end
44
+
45
+ if attribute = attribute_from_column_and_table(column, table)
46
+ attribute.send(method, args_for_predicate(value))
47
+ else
48
+ raise ::ActiveRecord::StatementInvalid, "No attribute named `#{column}` exists for table `#{table.name}`"
49
+ end
50
+
51
+ end
52
+ end
53
+
54
+ predicates.flatten!
55
+
56
+ if predicates.size > 1
57
+ first = predicates.shift
58
+ Arel::Nodes::Grouping.new(predicates.inject(first) {|memo, expr| Arel::Nodes::And.new([memo, expr])})
59
+ else
60
+ predicates.first
61
+ end
62
+ end
63
+
64
+ def visit_Array(o, parent)
65
+ if o.first.is_a? String
66
+ join_dependency.join_base.send(:sanitize_sql, o)
67
+ else
68
+ o.map {|e| accept(e, parent)}.flatten
69
+ end
70
+ end
71
+
72
+ def visit_MetaWhere_Or(o, parent)
73
+ accept(o.condition1, parent).or(accept(o.condition2, parent))
74
+ end
75
+
76
+ def visit_MetaWhere_And(o, parent)
77
+ accept(o.condition1, parent).and(accept(o.condition2, parent))
78
+ end
79
+
80
+ def visit_MetaWhere_Not(o, parent)
81
+ accept(o.expr, parent).not
82
+ end
83
+
84
+ def visit_MetaWhere_Condition(o, parent)
85
+ table = tables[parent]
86
+
87
+ unless attribute = attribute_from_column_and_table(o.column, table)
88
+ raise ::ActiveRecord::StatementInvalid, "No attribute named `#{o.column}` exists for table `#{table.name}`"
89
+ end
90
+
91
+ unless valid_comparison_method?(o.method)
92
+ raise ::ActiveRecord::StatementInvalid, "No comparison method named `#{o.method}` exists for column `#{o.column}`"
93
+ end
94
+ attribute.send(o.method, args_for_predicate(o.value))
95
+ end
96
+
97
+ def visit_MetaWhere_Function(o, parent)
98
+ self.table = tables[parent]
99
+
100
+ o.to_sqlliteral
101
+ end
102
+
103
+ private
104
+
105
+ def accept_activerecord_values(column, value, parent, reflection)
106
+ groups = Array.wrap(value).group_by {|v| v.class.base_class}
107
+ unless reflection.options[:polymorphic] || groups.keys.all? {|k| reflection.klass == k}
108
+ raise ArgumentError, "An object you supplied to :#{reflection.name} is not a #{reflection.klass}!"
109
+ end
110
+ case reflection.macro
111
+ when :has_many, :has_one, :has_and_belongs_to_many
112
+ conditions = nil
113
+ groups.each do |klass, values|
114
+ condition = {
115
+ (reflection.options[:foreign_key] || reflection.klass.primary_key).to_sym => values.size == 1 ? values.first.id : values.map(&:id)
116
+ }
117
+ conditions = conditions ? conditions | condition : condition
118
+ end
119
+
120
+ accept(conditions, association_from_parent_and_column(parent, column) || column)
121
+ when :belongs_to
122
+ conditions = nil
123
+ groups.each do |klass, values|
124
+ condition = if reflection.options[:polymorphic]
125
+ {
126
+ (reflection.options[:foreign_key] || reflection.foreign_key).to_sym => values.size == 1 ? values.first.id : values.map(&:id),
127
+ reflection.foreign_type.to_sym => klass.name
128
+ }
129
+ else
130
+ {(reflection.options[:foreign_key] || reflection.foreign_key).to_sym => values.size == 1 ? values.first.id : values.map(&:id)}
131
+ end
132
+ conditions = conditions ? conditions | condition : condition
133
+ end
134
+
135
+ accept(conditions, parent)
136
+ end
137
+ end
138
+
139
+ def sanitize_or_accept_reflection_conditions(reflection, parent, column)
140
+ if !can_accept?(reflection.options[:conditions])
141
+ reflection.sanitized_conditions
142
+ else
143
+ accept(reflection.options[:conditions], association_from_parent_and_column(parent, column) || column)
144
+ end
145
+ end
146
+
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,52 @@
1
+ require 'meta_where/utility'
2
+
3
+ module MetaWhere
4
+ module Visitors
5
+ class Visitor
6
+ include MetaWhere::Utility
7
+
8
+ attr_reader :tables
9
+ attr_reader :join_dependency
10
+
11
+ def initialize
12
+ @tables = Hash.new {|hash, key| hash[key] = get_table(key)}
13
+ end
14
+
15
+ def join_dependency=(jd)
16
+ @join_dependency = jd
17
+ jb = jd.join_base
18
+ @engine = jb.arel_engine
19
+ @default_table = Arel::Table.new(jb.table_name, :as => jb.aliased_table_name, :engine => @engine)
20
+ self
21
+ end
22
+
23
+ def get_table(parent_or_table_name = nil)
24
+ if parent_or_table_name.is_a?(Symbol)
25
+ Arel::Table.new(parent_or_table_name, :engine => @engine)
26
+ elsif parent_or_table_name.respond_to?(:aliased_table_name)
27
+ Arel::Table.new(parent_or_table_name.table_name, :as => parent_or_table_name.aliased_table_name, :engine => @engine)
28
+ else
29
+ @default_table
30
+ end
31
+ end
32
+
33
+ def accept(object, parent = nil)
34
+ visit(object, parent)
35
+ end
36
+
37
+ def can_accept?(object)
38
+ respond_to? DISPATCH[object.class]
39
+ end
40
+
41
+ private
42
+
43
+ DISPATCH = Hash.new do |hash, klass|
44
+ hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
45
+ end
46
+
47
+ def visit(object, parent)
48
+ send(DISPATCH[object.class], object, parent)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,48 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "meta_where/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "maksar-meta_where"
7
+ s.version = MetaWhere::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ernie Miller"]
10
+ s.email = ["ernie@metautonomo.us"]
11
+ s.homepage = "http://metautonomo.us/projects/metawhere"
12
+ s.summary = %q{ActiveRecord 3 query syntax on steroids.}
13
+ s.description = %q{
14
+ MetaWhere offers the ability to call any Arel predicate methods
15
+ (with a few convenient aliases) on your Model's attributes instead
16
+ of the ones normally offered by ActiveRecord's hash parameters. It also
17
+ adds convenient syntax for order clauses, smarter mapping of nested hash
18
+ conditions, and a debug_sql method to see the real SQL your code is
19
+ generating without running it against the database. If you like the new
20
+ AR 3.0 query interface, you'll love it with MetaWhere.
21
+ }
22
+
23
+ s.post_install_message = %q{
24
+ *** Thanks for installing MetaWhere! ***
25
+ Be sure to check out http://metautonomo.us/projects/metawhere/ for a
26
+ walkthrough of MetaWhere's features, and click the donate button if
27
+ you're feeling especially appreciative. It'd help me justify this
28
+ "open source" stuff to my lovely wife. :)
29
+
30
+ }
31
+
32
+ s.extra_rdoc_files = [
33
+ "LICENSE",
34
+ "README.rdoc"
35
+ ]
36
+
37
+ s.rubyforge_project = "meta_where"
38
+
39
+ s.add_dependency 'activerecord', '~> 3.1.0.rc1'
40
+ s.add_dependency 'activesupport', '~> 3.1.0.rc1'
41
+ s.add_development_dependency 'shoulda'
42
+ s.add_development_dependency 'sqlite3', '~> 1.3.3'
43
+
44
+ s.files = `git ls-files`.split("\n")
45
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
46
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
47
+ s.require_paths = ["lib"]
48
+ end
@@ -0,0 +1,17 @@
1
+ initech:
2
+ name : Initech
3
+ id : 1
4
+ created_at: 1999-02-19 08:00
5
+ updated_at: 1999-02-19 08:00
6
+
7
+ aos:
8
+ name: Advanced Optical Solutions
9
+ id : 2
10
+ created_at: 2004-02-01 08:00
11
+ updated_at: 2004-02-01 08:00
12
+
13
+ mission_data:
14
+ name: Mission Data
15
+ id : 3
16
+ created_at: 1996-09-21 08:00
17
+ updated_at: 1996-09-21 08:00
@@ -0,0 +1,7 @@
1
+ class Company < ActiveRecord::Base
2
+ has_many :developers
3
+ has_many :developer_notes, :through => :developers, :source => :notes
4
+ has_many :slackers, :class_name => "Developer", :conditions => {:slacker => true}
5
+ has_many :notes, :as => :notable
6
+ has_many :data_types
7
+ end
@@ -0,0 +1,3 @@
1
+ class DataType < ActiveRecord::Base
2
+ belongs_to :company
3
+ end
@@ -0,0 +1,15 @@
1
+ <% 1.upto(9) do |n| %>
2
+ dt_<%= n %>:
3
+ company_id: <%= n % 3 + 1 %>
4
+ str : This string has <%= n %> exclamation points<%= '!' * n %>
5
+ txt : <%= 'This is some text that may or may not repeat based on the value of n.' * n %>
6
+ int : <%= n ** 3 %>
7
+ flt : <%= n.to_f / 2.0 %>
8
+ dec : <%= n.to_f ** (n + 0.1) %>
9
+ dtm : <%= (Time.utc(2009, 12, 24) + 86400 * n).to_s(:db) %>
10
+ tms : <%= (Time.utc(2009, 12, 24) + 86400 * n).to_s(:db) %>
11
+ tim : <%= Time.utc(2000, 01, 01, n+8, n).to_s(:db) %>
12
+ dat : <%= (Date.new(2009, 12, 24) + n).strftime("%Y-%m-%d") %>
13
+ bin : <%= "BLOB#{n}" * n %>
14
+ bln : <%= n % 2 > 0 ? true : false %>
15
+ <% end %>
@@ -0,0 +1,5 @@
1
+ class Developer < ActiveRecord::Base
2
+ belongs_to :company
3
+ has_and_belongs_to_many :projects
4
+ has_many :notes, :as => :notable
5
+ end
@@ -0,0 +1,55 @@
1
+ peter:
2
+ id : 1
3
+ company_id: 1
4
+ name : Peter Gibbons
5
+ salary : 100000
6
+ slacker : true
7
+
8
+ michael:
9
+ id : 2
10
+ company_id: 1
11
+ name : Michael Bolton
12
+ salary : 70000
13
+ slacker : false
14
+
15
+ samir:
16
+ id : 3
17
+ company_id: 1
18
+ name : Samir Nagheenanajar
19
+ salary : 65000
20
+ slacker : false
21
+
22
+ herb:
23
+ id : 4
24
+ company_id: 2
25
+ name : Herb Myers
26
+ salary : 50000
27
+ slacker : false
28
+
29
+ dude:
30
+ id : 5
31
+ company_id: 2
32
+ name : Some Dude
33
+ salary : 84000
34
+ slacker : true
35
+
36
+ ernie:
37
+ id : 6
38
+ company_id: 3
39
+ name : Ernie Miller
40
+ salary : 45000
41
+ slacker : true
42
+
43
+ someone:
44
+ id : 7
45
+ company_id: 3
46
+ name : Someone Else
47
+ salary : 70000
48
+ slacker : true
49
+
50
+ another:
51
+ id : 8
52
+ company_id: 3
53
+ name : Another Guy
54
+ salary : 80000
55
+ slacker : false
@@ -0,0 +1,25 @@
1
+ <% 1.upto(3) do |d| %>
2
+ y2k_<%= d %>:
3
+ developer_id: <%= d %>
4
+ project_id : 1
5
+ <% end %>
6
+
7
+ virus:
8
+ developer_id: 2
9
+ project_id : 2
10
+
11
+ <% 1.upto(8) do |d| %>
12
+ awesome_<%= d %>:
13
+ developer_id: <%= d %>
14
+ project_id : 3
15
+ <% end %>
16
+
17
+ metasearch:
18
+ developer_id: 6
19
+ project_id : 4
20
+
21
+ <% 4.upto(8) do |d| %>
22
+ another_<%= d %>:
23
+ developer_id: <%= d %>
24
+ project_id : 5
25
+ <% end %>
@@ -0,0 +1,2 @@
1
+ class FixedBidProject < Project
2
+ end
@@ -0,0 +1,4 @@
1
+ class InvalidCompany < ActiveRecord::Base
2
+ set_table_name 'companies'
3
+ has_many :developers, :conditions => {:name.matches => '%Miller'}
4
+ end
@@ -0,0 +1,4 @@
1
+ class InvalidDeveloper < ActiveRecord::Base
2
+ set_table_name 'developers'
3
+ has_many :notes, :as => :notable, :conditions => [:note.eq % 'GIPE']
4
+ end
@@ -0,0 +1,3 @@
1
+ class Note < ActiveRecord::Base
2
+ belongs_to :notable, :polymorphic => true, :conditions => '1=1'
3
+ end
@@ -0,0 +1,95 @@
1
+ peter:
2
+ id : 1
3
+ notable_type: Developer
4
+ notable_id : 1
5
+ note : A straight shooter with upper management written all over him.
6
+
7
+ michael:
8
+ id : 2
9
+ notable_type: Developer
10
+ notable_id : 2
11
+ note : Doesn't like the singer of the same name. The nerve!
12
+
13
+ samir:
14
+ id : 3
15
+ notable_type: Developer
16
+ notable_id : 3
17
+ note : Naga.... Naga..... Not gonna work here anymore anyway.
18
+
19
+ herb:
20
+ id : 4
21
+ notable_type: Developer
22
+ notable_id : 4
23
+ note : Will show you what he's doing.
24
+
25
+ dude:
26
+ id : 5
27
+ notable_type: Developer
28
+ notable_id : 5
29
+ note : Nothing of note.
30
+
31
+ ernie:
32
+ id : 6
33
+ notable_type: Developer
34
+ notable_id : 6
35
+ note : Complete slacker. Should probably be fired.
36
+
37
+ someone:
38
+ id : 7
39
+ notable_type: Developer
40
+ notable_id : 7
41
+ note : Just another developer.
42
+
43
+ another:
44
+ id : 8
45
+ notable_type: Developer
46
+ notable_id : 8
47
+ note : Placing a note in this guy's file for insubordination.
48
+
49
+ initech:
50
+ id : 9
51
+ notable_type: Company
52
+ notable_id : 1
53
+ note : Innovation + Technology!
54
+
55
+ aos:
56
+ id : 10
57
+ notable_type: Company
58
+ notable_id : 2
59
+ note : Advanced solutions of an optical nature.
60
+
61
+ mission_data:
62
+ id : 11
63
+ notable_type: Company
64
+ notable_id : 3
65
+ note : Best design + development shop in the 'ville.
66
+
67
+ y2k:
68
+ id : 12
69
+ notable_type: Project
70
+ notable_id : 1
71
+ note : It may have already passed but that's no excuse to be unprepared!
72
+
73
+ virus:
74
+ id : 13
75
+ notable_type: Project
76
+ notable_id : 2
77
+ note : It could bring the company to its knees.
78
+
79
+ awesome:
80
+ id : 14
81
+ notable_type: Project
82
+ notable_id : 3
83
+ note : This note is AWESOME!!!
84
+
85
+ metasearch:
86
+ id : 15
87
+ notable_type: Project
88
+ notable_id : 4
89
+ note : A complete waste of the developer's time.
90
+
91
+ another:
92
+ id : 16
93
+ notable_type: Project
94
+ notable_id : 5
95
+ note : This is another project note.