activerecord-refined 0.3.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c6743021d5bc3d9aa3f3951ba64de77efcb51ed451aa338858d360ea1714b07
4
+ data.tar.gz: 3b2521ec7a2f0ccbbd68cf3e134461076a3ce77f3e7d545a9c27ba7d38860ca8
5
+ SHA512:
6
+ metadata.gz: 344141c84a629959a125ba4a6513274faa548f56de4442ed5333ae5346fa15256c1020ba834ec9713ee0b366fd1dda1371ba211bd6bbfbf5f2a5b705ed52c429
7
+ data.tar.gz: 29f9d6ac4ad68e031960e297fe3c9d30a50cab8f288d3599deb0f5607198238e61badf2e0e961dc78bceac3c3306d7577e7caba4e1224adea279ac741a7c37f7
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activerecord-refined.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Akira Matsuda
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # ActiveRecord::Refined
2
+
3
+ Adding clean and powerful query syntax on ActiveRecord using refinements.
4
+
5
+ ```ruby
6
+ Author.
7
+ joins(:posts) { :posts[:author_id] == :authors[:id] }.
8
+ where { (:authors[:age] == (20..40)) & (:posts[:published] == true) }
9
+ # SELECT "authors".* FROM "authors"
10
+ # INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"
11
+ # WHERE "authors"."age" BETWEEN 20 AND 40 AND "posts"."published" = TRUE
12
+ ```
13
+
14
+ ## History
15
+
16
+ This gem was formerly known as **activerecord-refinements**, created by Akira Matsuda
17
+ to experiment with the initial implementation of Ruby 2.0 Refinements. Because of the
18
+ Refinements' spec change, that implementation stopped working on Ruby 2.0.0 stable, and
19
+ the project was left dormant for a long time.
20
+
21
+ It has now been renamed to **activerecord-refined** and reimplemented on top of
22
+ `Proc#refined`, which will be introduced in Ruby 4.1. `Proc#refined` returns a new proc that
23
+ is evaluated with the given refinements activated, so a block written by the caller can
24
+ be re-interpreted under the query DSL's refinements:
25
+
26
+ ```ruby
27
+ def evaluate_block(&block)
28
+ refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
29
+ BlockContext.new.instance_exec(&refined_block)
30
+ end
31
+ ```
32
+
33
+ This is exactly what the old implementation needed and could not do, so the query syntax
34
+ works again without monkey-patching `Symbol` globally.
35
+
36
+ ## Requirements
37
+
38
+ * Ruby 4.1 or later (for `Proc#refined`; not released yet, so a `ruby-master` build is needed for now)
39
+ * ActiveRecord 7.0 or later
40
+
41
+ ## Installation
42
+
43
+ Add this line to your application's Gemfile:
44
+
45
+ gem 'activerecord-refined'
46
+
47
+ And then execute:
48
+
49
+ $ bundle
50
+
51
+ Or install it yourself as:
52
+
53
+ $ gem install activerecord-refined
54
+
55
+ ## Usage
56
+
57
+ Just require the gem, and `where`, `select`, `joins`, `left_outer_joins`, `having`,
58
+ `order` and `group` will accept a block.
59
+
60
+ ```ruby
61
+ require 'activerecord-refined'
62
+ ```
63
+
64
+ Inside the block, symbols denote columns of the receiver's table, and `:table[:column]`
65
+ denotes a qualified column.
66
+
67
+ ### Conditions
68
+
69
+ ```ruby
70
+ Author.where { :age >= 18 }
71
+ Author.where { :name =~ 'A%' } # LIKE
72
+ Author.where { :name !~ '%test%' } # NOT LIKE
73
+ Author.where { :age == (20..40) } # BETWEEN
74
+ Author.where { :country == %w[JP US] } # IN
75
+ Author.where { :country != %w[JP US] } # NOT IN
76
+ Author.where { :country.null? } # IS NULL
77
+ ```
78
+
79
+ Combine predicates with `&`, `|` and `!`. Ruby's operator precedence makes the
80
+ parentheses around each comparison necessary:
81
+
82
+ ```ruby
83
+ Author.where { (:age >= 18) & ((:country == 'JP') | (:country == 'US')) }
84
+ Author.where { !((:age == (0..17)) | :country.null?) }
85
+ ```
86
+
87
+ ### Joins
88
+
89
+ The block is the `ON` clause:
90
+
91
+ ```ruby
92
+ Author.
93
+ joins(:posts) { :posts[:author_id] == :authors[:id] }.
94
+ joins(:comments) { :comments[:post_id] == :posts[:id] }
95
+
96
+ Author.left_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }
97
+ ```
98
+
99
+ ### Aggregates, functions and aliases
100
+
101
+ `count`, `sum`, `avg`, `min` and `max` are available as methods, as are the scalar
102
+ functions `upper`, `lower`, `length`, `trim`, `coalesce`, `abs` and `round`. Use `.as`
103
+ for a column alias, and `.asc` / `.desc` for the sort direction. Return an array to
104
+ select or order by multiple expressions.
105
+
106
+ ```ruby
107
+ Author.
108
+ joins(:posts) { :posts[:author_id] == :authors[:id] }.
109
+ where { :posts[:published] == true }.
110
+ group { :authors[:id] }.
111
+ having { count(:posts[:id]) > 1 }.
112
+ order { count(:posts[:id]).desc }.
113
+ select {
114
+ [
115
+ upper(:authors[:name]).as(:author),
116
+ count(:posts[:id]).as(:post_count),
117
+ avg(:posts[:likes]).as(:avg_likes),
118
+ ]
119
+ }
120
+ ```
121
+
122
+ See `examples/` for complete, runnable scripts.
123
+
124
+ ## Contributing
125
+
126
+ 1. Fork it
127
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
128
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
129
+ 4. Push to the branch (`git push origin my-new-feature`)
130
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['test/test_*.rb']
6
+ end
7
+
8
+ task default: :test
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activerecord-refined/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "activerecord-refined"
8
+ gem.version = Activerecord::Refined::VERSION
9
+ gem.authors = ["Shugo Maeda"]
10
+ gem.email = ["shugo@ruby-lang.org"]
11
+ gem.description = 'Adding clean and powerful query syntax on AR using refinements'
12
+ gem.summary = 'ActiveRecord + Ruby 2.0 refinements'
13
+ gem.homepage = 'https://github.com/shugo/activerecord-refined'
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ # Proc#refined is available since Ruby 4.1. 4.1.0.dev is required to allow
21
+ # ruby-master builds, which sort before the 4.1.0 release.
22
+ gem.required_ruby_version = '>= 4.1.0.dev'
23
+
24
+ gem.add_dependency 'activerecord', ['>= 7.0']
25
+ gem.add_development_dependency 'sqlite3', ['>= 0']
26
+ gem.add_development_dependency 'minitest', ['>= 0']
27
+ gem.add_development_dependency 'rake', ['>= 0']
28
+ end
@@ -0,0 +1,219 @@
1
+ module ActiveRecord
2
+ module Refined
3
+ module AST
4
+ class Node
5
+ def to_arel(table)
6
+ raise ScriptError, "subclass must override this method"
7
+ end
8
+
9
+ def as(alias_name)
10
+ As.new(self, alias_name)
11
+ end
12
+
13
+ def asc
14
+ Ordering.new(self, :asc)
15
+ end
16
+
17
+ def desc
18
+ Ordering.new(self, :desc)
19
+ end
20
+ end
21
+
22
+ class Predicate < Node
23
+ def &(other)
24
+ And.new(self, other)
25
+ end
26
+
27
+ def |(other)
28
+ Or.new(self, other)
29
+ end
30
+
31
+ def !
32
+ Not.new(self)
33
+ end
34
+ end
35
+
36
+ class Column < Node
37
+ attr_reader :table_name, :column_name
38
+
39
+ def initialize(table_name, column_name)
40
+ @table_name = table_name
41
+ @column_name = column_name
42
+ end
43
+
44
+ def to_arel(_table)
45
+ Arel::Table.new(table_name)[column_name]
46
+ end
47
+
48
+ %i[== != =~ !~ > >= < <=].each do |op|
49
+ define_method(op) {|val| Comparison.new(self, op, val) }
50
+ end
51
+
52
+ def null?
53
+ Comparison.new(self, :==, nil)
54
+ end
55
+
56
+ %i[count sum average maximum minimum].each do |func|
57
+ define_method(func) { Aggregate.new(self, func) }
58
+ end
59
+ end
60
+
61
+ class Aggregate < Node
62
+ attr_reader :operand, :function
63
+
64
+ def initialize(operand, function)
65
+ @operand = operand
66
+ @function = function
67
+ end
68
+
69
+ def to_arel(table)
70
+ arel_operand = case operand
71
+ when Node then operand.to_arel(table)
72
+ else table[operand]
73
+ end
74
+ arel_operand.public_send(function)
75
+ end
76
+
77
+ %i[== != =~ !~ > >= < <=].each do |op|
78
+ define_method(op) {|val| Comparison.new(self, op, val) }
79
+ end
80
+ end
81
+
82
+ class As < Node
83
+ attr_reader :operand, :alias_name
84
+
85
+ def initialize(operand, alias_name)
86
+ @operand = operand
87
+ @alias_name = alias_name
88
+ end
89
+
90
+ def to_arel(table)
91
+ arel_operand = case operand
92
+ when Node then operand.to_arel(table)
93
+ when Symbol then table[operand]
94
+ else operand
95
+ end
96
+ arel_operand.as(alias_name.to_s)
97
+ end
98
+ end
99
+
100
+ class Ordering < Node
101
+ attr_reader :operand, :direction
102
+
103
+ def initialize(operand, direction)
104
+ @operand = operand
105
+ @direction = direction
106
+ end
107
+
108
+ def to_arel(table)
109
+ arel_operand = case operand
110
+ when Node then operand.to_arel(table)
111
+ when Symbol then table[operand]
112
+ else operand
113
+ end
114
+ arel_operand.public_send(direction)
115
+ end
116
+ end
117
+
118
+ class Function < Node
119
+ attr_reader :name, :args
120
+
121
+ def initialize(name, args)
122
+ @name = name
123
+ @args = args
124
+ end
125
+
126
+ def to_arel(table)
127
+ arel_args = args.map do |arg|
128
+ case arg
129
+ when Node then arg.to_arel(table)
130
+ when Symbol then table[arg]
131
+ else Arel::Nodes.build_quoted(arg)
132
+ end
133
+ end
134
+ Arel::Nodes::NamedFunction.new(name, arel_args)
135
+ end
136
+
137
+ %i[== != =~ !~ > >= < <=].each do |op|
138
+ define_method(op) {|val| Comparison.new(self, op, val) }
139
+ end
140
+ end
141
+
142
+ class Comparison < Predicate
143
+ OPERATOR_MAP = {
144
+ :== => :eq, :!= => :not_eq, :=~ => :matches, :!~ => :does_not_match,
145
+ :> => :gt, :>= => :gteq, :< => :lt, :<= => :lteq
146
+ }.freeze
147
+
148
+ attr_reader :column, :operator, :value
149
+
150
+ def initialize(column, operator, value)
151
+ @column = column
152
+ @operator = operator
153
+ @value = value
154
+ end
155
+
156
+ def to_arel(table)
157
+ arel_column = case column
158
+ when Node then column.to_arel(table)
159
+ else table[column]
160
+ end
161
+ arel_value = case value
162
+ when Node then value.to_arel(table)
163
+ else value
164
+ end
165
+ case
166
+ when operator == :== && Range === value
167
+ arel_column.between(value)
168
+ when operator == :!= && Range === value
169
+ arel_column.not_between(value)
170
+ when operator == :== && Array === value
171
+ arel_column.in(value)
172
+ when operator == :!= && Array === value
173
+ arel_column.not_in(value)
174
+ else
175
+ arel_column.public_send(OPERATOR_MAP.fetch(operator), arel_value)
176
+ end
177
+ end
178
+ end
179
+
180
+ class And < Predicate
181
+ attr_reader :left, :right
182
+
183
+ def initialize(left, right)
184
+ @left = left
185
+ @right = right
186
+ end
187
+
188
+ def to_arel(table)
189
+ left.to_arel(table).and(right.to_arel(table))
190
+ end
191
+ end
192
+
193
+ class Or < Predicate
194
+ attr_reader :left, :right
195
+
196
+ def initialize(left, right)
197
+ @left = left
198
+ @right = right
199
+ end
200
+
201
+ def to_arel(table)
202
+ left.to_arel(table).or(right.to_arel(table))
203
+ end
204
+ end
205
+
206
+ class Not < Predicate
207
+ attr_reader :operand
208
+
209
+ def initialize(operand)
210
+ @operand = operand
211
+ end
212
+
213
+ def to_arel(table)
214
+ Arel::Nodes::Not.new(operand.to_arel(table))
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,138 @@
1
+ module ActiveRecord
2
+ module Refined
3
+ module BlockSyntax
4
+ refine Symbol do
5
+ %i[== != =~ !~ > >= < <=].each do |op|
6
+ define_method(op) {|val| AST::Comparison.new(self, op, val) }
7
+ end
8
+
9
+ def null?
10
+ AST::Comparison.new(self, :==, nil)
11
+ end
12
+
13
+ %i[count sum average maximum minimum].each do |func|
14
+ define_method(func) { AST::Aggregate.new(self, func) }
15
+ end
16
+
17
+ def as(alias_name)
18
+ AST::As.new(self, alias_name)
19
+ end
20
+
21
+ def asc
22
+ AST::Ordering.new(self, :asc)
23
+ end
24
+
25
+ def desc
26
+ AST::Ordering.new(self, :desc)
27
+ end
28
+
29
+ def [](column_name)
30
+ AST::Column.new(self, column_name)
31
+ end
32
+ end
33
+ end
34
+
35
+ class BlockContext
36
+ AGGREGATE_FUNCTIONS = {
37
+ count: :count, sum: :sum, avg: :average, min: :minimum, max: :maximum,
38
+ }.freeze
39
+
40
+ AGGREGATE_FUNCTIONS.each do |name, arel_func|
41
+ define_method(name) {|column| AST::Aggregate.new(column, arel_func) }
42
+ end
43
+
44
+ SCALAR_FUNCTIONS = %i[upper lower length trim coalesce abs round].freeze
45
+
46
+ SCALAR_FUNCTIONS.each do |name|
47
+ define_method(name) {|*args| AST::Function.new(name.to_s.upcase, args) }
48
+ end
49
+ end
50
+
51
+ module QueryMethods
52
+ def where(opts = nil, *rest, &block)
53
+ if block
54
+ super(evaluate_block(&block).to_arel(table))
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def select(*fields, &block)
61
+ if block
62
+ result = evaluate_block(&block)
63
+ arel = Array(result).map {|node| to_arel_field(node) }
64
+ super(*arel, &nil)
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ def having(opts = nil, *rest, &block)
71
+ if block
72
+ super(evaluate_block(&block).to_arel(table))
73
+ else
74
+ super
75
+ end
76
+ end
77
+
78
+ def order(*args, &block)
79
+ if block
80
+ result = evaluate_block(&block)
81
+ arel = Array(result).map {|node| to_arel_field(node) }
82
+ super(*arel, &nil)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ def group(*args, &block)
89
+ if block
90
+ result = evaluate_block(&block)
91
+ arel = Array(result).map {|node| to_arel_field(node) }
92
+ super(*arel, &nil)
93
+ else
94
+ super
95
+ end
96
+ end
97
+
98
+ def joins(*args, &block)
99
+ if block
100
+ super(build_join_node(args.first, Arel::Nodes::InnerJoin, &block))
101
+ else
102
+ super
103
+ end
104
+ end
105
+
106
+ def left_outer_joins(*args, &block)
107
+ if block
108
+ joins(build_join_node(args.first, Arel::Nodes::OuterJoin, &block))
109
+ else
110
+ super
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def evaluate_block(&block)
117
+ refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
118
+ BlockContext.new.instance_exec(&refined_block)
119
+ end
120
+
121
+ def to_arel_field(node)
122
+ case node
123
+ when AST::Node then node.to_arel(table)
124
+ when Symbol then table[node]
125
+ else node
126
+ end
127
+ end
128
+
129
+ def build_join_node(target_table, join_class, &block)
130
+ ast = evaluate_block(&block)
131
+ join_class.new(
132
+ Arel::Table.new(target_table),
133
+ Arel::Nodes::On.new(ast.to_arel(table))
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,5 @@
1
+ module Activerecord
2
+ module Refined
3
+ VERSION = '0.3.0'
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ require 'activerecord-refined/version'
2
+ require 'active_record'
3
+ require 'active_record/relation'
4
+ require 'active_record/refined/ast'
5
+ require 'active_record/refined'
6
+
7
+ ActiveRecord::QueryMethods.prepend ActiveRecord::Refined::QueryMethods
@@ -0,0 +1,307 @@
1
+ require_relative 'test_helper'
2
+
3
+ class TestBlockSyntax < Minitest::Test
4
+ def test_equal
5
+ assert_match(/WHERE "users"."name" = 'matz'/, User.where { :name == 'matz' }.to_sql)
6
+ end
7
+
8
+ def test_not_equal
9
+ assert_match(/WHERE "users"."name" != 'nobu'/, User.where { :name != 'nobu' }.to_sql)
10
+ end
11
+
12
+ def test_greater_than
13
+ assert_match(/WHERE "users"."age" > 3/, User.where { :age > 3 }.to_sql)
14
+ end
15
+
16
+ def test_greater_than_or_equal
17
+ assert_match(/WHERE "users"."age" >= 18/, User.where { :age >= 18 }.to_sql)
18
+ end
19
+
20
+ def test_less_than
21
+ assert_match(/WHERE "users"."age" < 60/, User.where { :age < 60 }.to_sql)
22
+ end
23
+
24
+ def test_less_than_or_equal
25
+ assert_match(/WHERE "users"."age" <= 35/, User.where { :age <= 35 }.to_sql)
26
+ end
27
+
28
+ def test_like
29
+ assert_match(/WHERE "users"."name" LIKE 'tender%'/, User.where { :name =~ 'tender%' }.to_sql)
30
+ end
31
+
32
+ def test_outside_of_where_block
33
+ assert_raises(ArgumentError) { :omg > 1 }
34
+ end
35
+
36
+ def test_and
37
+ assert_match(/WHERE "users"."name" = 'matz' AND "users"."age" > 18/,
38
+ User.where { (:name == 'matz') & (:age > 18) }.to_sql)
39
+ end
40
+
41
+ def test_or
42
+ assert_match(/WHERE \(?\"users\".\"name\" = 'matz' OR \"users\".\"name\" = 'nobu'\)?/,
43
+ User.where { (:name == 'matz') | (:name == 'nobu') }.to_sql)
44
+ end
45
+
46
+ def test_not
47
+ assert_match(/WHERE NOT \(?\"users\".\"name\" = 'matz'\)?/,
48
+ User.where { !(:name == 'matz') }.to_sql)
49
+ end
50
+
51
+ def test_complex_combination
52
+ sql = User.where { ((:name == 'matz') & (:age > 18)) | !(:name == 'nobu') }.to_sql
53
+ assert_match(/"users"."name" = 'matz'/, sql)
54
+ assert_match(/"users"."age" > 18/, sql)
55
+ assert_match(/NOT/, sql)
56
+ assert_match(/OR/, sql)
57
+ end
58
+
59
+ def test_between
60
+ assert_match(/WHERE "users"."age" BETWEEN 18 AND 65/,
61
+ User.where { :age == (18..65) }.to_sql)
62
+ end
63
+
64
+ def test_not_like
65
+ assert_match(/WHERE "users"."name" NOT LIKE 'tender%'/,
66
+ User.where { :name !~ 'tender%' }.to_sql)
67
+ end
68
+
69
+ def test_not_between
70
+ # Arel expands a bounded NOT BETWEEN into an OR of comparisons
71
+ assert_match(/WHERE \("users"."age" < 18 OR "users"."age" > 65\)/,
72
+ User.where { :age != (18..65) }.to_sql)
73
+ end
74
+
75
+ def test_is_null
76
+ assert_match(/WHERE "users"."name" IS NULL/,
77
+ User.where { :name.null? }.to_sql)
78
+ end
79
+
80
+ def test_is_null_qualified
81
+ assert_match(/WHERE "users"."name" IS NULL/,
82
+ User.where { :users[:name].null? }.to_sql)
83
+ end
84
+
85
+ def test_in
86
+ assert_match(/WHERE "users"."age" IN \(1, 2, 3\)/,
87
+ User.where { :age == [1, 2, 3] }.to_sql)
88
+ end
89
+
90
+ def test_not_in
91
+ assert_match(/WHERE "users"."age" NOT IN \(1, 2, 3\)/,
92
+ User.where { :age != [1, 2, 3] }.to_sql)
93
+ end
94
+
95
+ def test_qualified_column
96
+ assert_match(/WHERE "users"."name" = 'matz'/,
97
+ User.where { :users[:name] == 'matz' }.to_sql)
98
+ end
99
+
100
+ def test_column_to_column_comparison
101
+ sql = User.where { :users[:name] == :users[:age] }.to_sql
102
+ assert_match(/"users"."name" = "users"."age"/, sql)
103
+ end
104
+
105
+ def test_joins_with_block
106
+ sql = Author.joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql
107
+ assert_match(/INNER JOIN "posts" ON "posts"."author_id" = "authors"."id"/, sql)
108
+ end
109
+
110
+ def test_left_outer_joins_with_block
111
+ sql = Author.left_outer_joins(:posts) { :posts[:author_id] == :authors[:id] }.to_sql
112
+ assert_match(/LEFT OUTER JOIN "posts" ON "posts"."author_id" = "authors"."id"/, sql)
113
+ end
114
+
115
+ def test_select_aggregate
116
+ assert_match(/SELECT SUM\("users"."age"\)/,
117
+ User.select { :age.sum }.to_sql)
118
+ end
119
+
120
+ def test_select_aggregate_qualified
121
+ assert_match(/SELECT COUNT\("users"."id"\)/,
122
+ User.select { :users[:id].count }.to_sql)
123
+ end
124
+
125
+ def test_select_average
126
+ assert_match(/SELECT AVG\("users"."age"\)/,
127
+ User.select { :age.average }.to_sql)
128
+ end
129
+
130
+ def test_select_maximum
131
+ assert_match(/SELECT MAX\("users"."age"\)/,
132
+ User.select { :age.maximum }.to_sql)
133
+ end
134
+
135
+ def test_select_minimum
136
+ assert_match(/SELECT MIN\("users"."age"\)/,
137
+ User.select { :age.minimum }.to_sql)
138
+ end
139
+
140
+ def test_having_aggregate
141
+ sql = User.group(:name).having { :age.sum > 100 }.to_sql
142
+ assert_match(/GROUP BY "users"."name"/, sql)
143
+ assert_match(/HAVING SUM\("users"."age"\) > 100/, sql)
144
+ end
145
+
146
+ def test_select_avg_function
147
+ assert_match(/SELECT AVG\("users"."age"\)/,
148
+ User.select { avg(:age) }.to_sql)
149
+ end
150
+
151
+ def test_select_count_function
152
+ assert_match(/SELECT COUNT\("users"."id"\)/,
153
+ User.select { count(:id) }.to_sql)
154
+ end
155
+
156
+ def test_select_sum_function
157
+ assert_match(/SELECT SUM\("users"."age"\)/,
158
+ User.select { sum(:age) }.to_sql)
159
+ end
160
+
161
+ def test_select_min_function
162
+ assert_match(/SELECT MIN\("users"."age"\)/,
163
+ User.select { min(:age) }.to_sql)
164
+ end
165
+
166
+ def test_select_max_function
167
+ assert_match(/SELECT MAX\("users"."age"\)/,
168
+ User.select { max(:age) }.to_sql)
169
+ end
170
+
171
+ def test_function_qualified_column
172
+ assert_match(/SELECT AVG\("users"."age"\)/,
173
+ User.select { avg(:users[:age]) }.to_sql)
174
+ end
175
+
176
+ def test_having_function
177
+ sql = User.group(:name).having { sum(:age) > 100 }.to_sql
178
+ assert_match(/GROUP BY "users"."name"/, sql)
179
+ assert_match(/HAVING SUM\("users"."age"\) > 100/, sql)
180
+ end
181
+
182
+ def test_function_and_method_syntax_match
183
+ assert_equal User.select { :age.average }.to_sql,
184
+ User.select { avg(:age) }.to_sql
185
+ end
186
+
187
+ def test_upper_function
188
+ assert_match(/SELECT UPPER\("users"."name"\)/,
189
+ User.select { upper(:name) }.to_sql)
190
+ end
191
+
192
+ def test_lower_function
193
+ assert_match(/SELECT LOWER\("users"."name"\)/,
194
+ User.select { lower(:name) }.to_sql)
195
+ end
196
+
197
+ def test_length_function_in_where
198
+ assert_match(/WHERE LENGTH\("users"."name"\) > 3/,
199
+ User.where { length(:name) > 3 }.to_sql)
200
+ end
201
+
202
+ def test_coalesce_function_with_literal
203
+ assert_match(/SELECT COALESCE\("users"."name", 'unknown'\)/,
204
+ User.select { coalesce(:name, 'unknown') }.to_sql)
205
+ end
206
+
207
+ def test_function_comparison
208
+ assert_match(/WHERE UPPER\("users"."name"\) = 'MATZ'/,
209
+ User.where { upper(:name) == 'MATZ' }.to_sql)
210
+ end
211
+
212
+ def test_nested_function
213
+ assert_match(/SELECT UPPER\(COALESCE\("users"."name", 'x'\)\)/,
214
+ User.select { upper(coalesce(:name, 'x')) }.to_sql)
215
+ end
216
+
217
+ def test_function_qualified_column_arg
218
+ assert_match(/SELECT UPPER\("users"."name"\)/,
219
+ User.select { upper(:users[:name]) }.to_sql)
220
+ end
221
+
222
+ def test_select_multiple_fields
223
+ assert_match(/SELECT UPPER\("users"."name"\), "users"."age"/,
224
+ User.select { [upper(:name), :age] }.to_sql)
225
+ end
226
+
227
+ def test_select_multiple_columns
228
+ assert_match(/SELECT "users"."name", "users"."age"/,
229
+ User.select { [:name, :age] }.to_sql)
230
+ end
231
+
232
+ def test_select_multiple_with_aggregate
233
+ assert_match(/SELECT "users"."name", SUM\("users"."age"\)/,
234
+ User.select { [:name, sum(:age)] }.to_sql)
235
+ end
236
+
237
+ def test_select_function_with_alias
238
+ assert_match(/SELECT UPPER\("users"."name"\) AS upper_name, "users"."age"/,
239
+ User.select { [upper(:name).as(:upper_name), :age] }.to_sql)
240
+ end
241
+
242
+ def test_select_column_alias
243
+ assert_match(/SELECT "users"."name" AS n/,
244
+ User.select { :name.as(:n) }.to_sql)
245
+ end
246
+
247
+ def test_select_qualified_column_alias
248
+ assert_match(/SELECT "users"."name" AS n/,
249
+ User.select { :users[:name].as(:n) }.to_sql)
250
+ end
251
+
252
+ def test_select_aggregate_alias
253
+ assert_match(/SELECT COUNT\("users"."id"\) AS cnt/,
254
+ User.select { count(:id).as(:cnt) }.to_sql)
255
+ end
256
+
257
+ def test_order_default_asc
258
+ assert_match(/ORDER BY "users"."age"/,
259
+ User.order { :age }.to_sql)
260
+ end
261
+
262
+ def test_order_desc
263
+ assert_match(/ORDER BY "users"."age" DESC/,
264
+ User.order { :age.desc }.to_sql)
265
+ end
266
+
267
+ def test_order_asc
268
+ assert_match(/ORDER BY "users"."age" ASC/,
269
+ User.order { :age.asc }.to_sql)
270
+ end
271
+
272
+ def test_order_multiple
273
+ assert_match(/ORDER BY "users"."age" DESC, "users"."name" ASC/,
274
+ User.order { [:age.desc, :name.asc] }.to_sql)
275
+ end
276
+
277
+ def test_order_qualified_column
278
+ assert_match(/ORDER BY "users"."name" DESC/,
279
+ User.order { :users[:name].desc }.to_sql)
280
+ end
281
+
282
+ def test_group_single
283
+ assert_match(/GROUP BY "users"."name"/,
284
+ User.group { :name }.to_sql)
285
+ end
286
+
287
+ def test_group_multiple
288
+ assert_match(/GROUP BY "users"."name", "users"."age"/,
289
+ User.group { [:name, :age] }.to_sql)
290
+ end
291
+
292
+ def test_group_qualified_column
293
+ assert_match(/GROUP BY "users"."name"/,
294
+ User.group { :users[:name] }.to_sql)
295
+ end
296
+
297
+ def test_group_with_having
298
+ sql = User.group { :name }.having { sum(:age) > 100 }.to_sql
299
+ assert_match(/GROUP BY "users"."name"/, sql)
300
+ assert_match(/HAVING SUM\("users"."age"\) > 100/, sql)
301
+ end
302
+
303
+ def test_default_where_syntax
304
+ assert_match(/WHERE "users"."name" = 'Ruby' AND "users"."age" = 19/,
305
+ User.where(name: 'Ruby', age: 19).to_sql)
306
+ end
307
+ end
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'minitest/autorun'
5
+ Bundler.require
6
+
7
+ require 'active_record'
8
+
9
+ config = {:adapter => 'sqlite3', :database => ':memory:'}
10
+ ActiveRecord::Base.establish_connection(config)
11
+
12
+ class User < ActiveRecord::Base
13
+ end
14
+
15
+ class Author < ActiveRecord::Base
16
+ has_many :posts
17
+ end
18
+
19
+ class Post < ActiveRecord::Base
20
+ belongs_to :author
21
+ end
22
+
23
+ class CreateAllTables < ActiveRecord::Migration[8.1]
24
+ def up
25
+ create_table(:users) {|t| t.string :name; t.integer :age}
26
+ create_table(:authors) {|t| t.string :name}
27
+ create_table(:posts) {|t| t.string :title; t.integer :author_id}
28
+ end
29
+ end
30
+ ActiveRecord::Migration.verbose = false
31
+ CreateAllTables.new.up
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-refined
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Shugo Maeda
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ -
17
+ - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: "7.0"
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ -
25
+ - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: "7.0"
28
+ - !ruby/object:Gem::Dependency
29
+ name: sqlite3
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ -
33
+ - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ -
41
+ - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ - !ruby/object:Gem::Dependency
45
+ name: minitest
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ -
49
+ - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ type: :development
53
+ prerelease: false
54
+ version_requirements: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ -
57
+ - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ - !ruby/object:Gem::Dependency
61
+ name: rake
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ -
65
+ - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ -
73
+ - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ description: Adding clean and powerful query syntax on AR using refinements
77
+ email:
78
+ - shugo@ruby-lang.org
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - .gitignore
84
+ - Gemfile
85
+ - LICENSE.txt
86
+ - README.md
87
+ - Rakefile
88
+ - activerecord-refined.gemspec
89
+ - lib/active_record/refined.rb
90
+ - lib/active_record/refined/ast.rb
91
+ - lib/activerecord-refined.rb
92
+ - lib/activerecord-refined/version.rb
93
+ - test/test_block_syntax.rb
94
+ - test/test_helper.rb
95
+ homepage: "https://github.com/shugo/activerecord-refined"
96
+ licenses: []
97
+ metadata: {}
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ -
104
+ - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 4.1.0.dev
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ -
110
+ - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: "0"
113
+ requirements: []
114
+ rubygems_version: 4.1.0.dev
115
+ specification_version: 4
116
+ summary: ActiveRecord + Ruby 2.0 refinements
117
+ test_files:
118
+ - test/test_block_syntax.rb
119
+ - test/test_helper.rb