active_record_extended 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +77 -0
- data/lib/active_record_extended/active_record/relation_patch.rb +6 -4
- data/lib/active_record_extended/query_methods/window.rb +92 -0
- data/lib/active_record_extended/utilities/support.rb +12 -2
- data/lib/active_record_extended/version.rb +1 -1
- data/spec/query_methods/window_spec.rb +51 -0
- data/spec/sql_inspections/window_sql_spec.rb +86 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69a8ed712b33088a8aa79eb2d89ba86f645882d085d3bb752fb997eb1888b6af
|
4
|
+
data.tar.gz: a60306c9e2551606772007b683dee9477d7c87545569647c47a78e45a92b21bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5530aaaf8f2f45aa1f7daaed6398eeac033cd50c3fb542a427dac4b9097c6ebffe08928f6d7a13bacdf9e04509f26f85ed607d4622e16423ced1308e079a2cf7
|
7
|
+
data.tar.gz: 8ca7691c2c23da13e8f1a409eeb5528ed47f01736f4cabbefa06fe441f56cae24ff504b1a0b31996645ef6bba7e2c308e1a8ee6254ee795d3a5fdbd15b0de57b
|
data/README.md
CHANGED
@@ -36,6 +36,9 @@
|
|
36
36
|
- [Union As](#union-as)
|
37
37
|
- [Union Order](#union-order)
|
38
38
|
- [Union Reorder](#union-reorder)
|
39
|
+
- [Window Functions](#window-functions)
|
40
|
+
- [Define Window](#define-window)
|
41
|
+
- [Select Window](#select-window)
|
39
42
|
|
40
43
|
## Description and History
|
41
44
|
|
@@ -787,6 +790,80 @@ SELECT "people".*
|
|
787
790
|
) ) ORDER BY personal_id DESC, id DESC) people
|
788
791
|
```
|
789
792
|
|
793
|
+
#### Window Functions
|
794
|
+
[Postgres Window Functions](https://www.postgresql.org/docs/current/tutorial-window.html)
|
795
|
+
|
796
|
+
Let's address the elephant in the room. Arel has had, for a long time now, window function capabilities;
|
797
|
+
However they've never seen the lime light in ActiveRecord's query logic.
|
798
|
+
The following brings the dormant Arel methods up to the ActiveRecord Querying level.
|
799
|
+
|
800
|
+
#### Define Window
|
801
|
+
|
802
|
+
To set up a window function, we first must establish the window and we do this by using the `.define_window/1` method.
|
803
|
+
This method also requires you to call chain `.partition_by/2`
|
804
|
+
|
805
|
+
`.define_window/1` - Establishes the name of the window you'll reference later on in [.select_window](#select-window)
|
806
|
+
- Aliased name of window
|
807
|
+
|
808
|
+
`.partition_by/2` - Establishes the windows operations a [pre-defined window function](https://www.postgresql.org/docs/current/functions-window.html) will leverage.
|
809
|
+
- column name being partitioned against
|
810
|
+
- (**optional**) `order_by`: Processes how the window should be ordered
|
811
|
+
|
812
|
+
```ruby
|
813
|
+
User
|
814
|
+
.define_window(:number_window).partition_by(:number, order_by: { id: :desc })
|
815
|
+
.define_window(:name_window).partition_by(:name, order_by: :id)
|
816
|
+
.define_window(:no_order_name).partition_by(:name)
|
817
|
+
```
|
818
|
+
|
819
|
+
Query Output
|
820
|
+
```sql
|
821
|
+
SELECT *
|
822
|
+
FROM users
|
823
|
+
WINDOW number_window AS (PARTITION BY number ORDER BY id DESC),
|
824
|
+
name_window AS (PARTITION BY name ORDER BY id),
|
825
|
+
no_order_name AS (PARTITION BY name)
|
826
|
+
```
|
827
|
+
|
828
|
+
#### Select Window
|
829
|
+
|
830
|
+
Once you've define a window, the next step to to utilize it on one of the many provided postgres window functions.
|
831
|
+
|
832
|
+
`.select_window/3`
|
833
|
+
- [window function name](https://www.postgresql.org/docs/current/functions-window.html)
|
834
|
+
- (**optional**) Window function arguments (treated as a splatted array)
|
835
|
+
- (**optional**) `as:` : Alias name of the final result
|
836
|
+
- `over:` : name of [defined window](#define-window)
|
837
|
+
|
838
|
+
```ruby
|
839
|
+
User.create!(name: "Alice", number: 100) #=> id: 1
|
840
|
+
User.create!(name: "Randy", number: 100) #=> id: 2
|
841
|
+
User.create!(name: "Bob", number: 300) #=> id: 3
|
842
|
+
|
843
|
+
User
|
844
|
+
.define_window(:number_window).partition_by(:number, order_by: { id: :desc })
|
845
|
+
.select(:id, :name)
|
846
|
+
.select_window(:row_number, over: :number_window, as: :row_id)
|
847
|
+
.select_window(:first_value, :name, over: :number_window, as: :first_value_name)
|
848
|
+
#=> [
|
849
|
+
# { id: 1, name: "Alice", row_id: 2, first_value_name: "Randy" }
|
850
|
+
# { id: 2, name: "Randy", row_id: 1, first_value_name: "Randy" }
|
851
|
+
# { id: 3, name: "Bob", row_id: 1, first_value_name: "Bob" }
|
852
|
+
# ]
|
853
|
+
#
|
854
|
+
|
855
|
+
```
|
856
|
+
|
857
|
+
Query Output
|
858
|
+
```sql
|
859
|
+
SELECT "users"."id",
|
860
|
+
"users"."name",
|
861
|
+
(ROW_NUMBER() OVER number_window) AS "row_id",
|
862
|
+
(FIRST_VALUE(name) OVER number_window) AS "first_value_name"
|
863
|
+
FROM "users"
|
864
|
+
WINDOW number_window AS (PARTITION BY number ORDER BY id DESC)
|
865
|
+
```
|
866
|
+
|
790
867
|
## License
|
791
868
|
|
792
869
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -1,27 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_record_extended/query_methods/window"
|
3
4
|
require "active_record_extended/query_methods/unionize"
|
4
5
|
require "active_record_extended/query_methods/json"
|
5
6
|
|
6
7
|
module ActiveRecordExtended
|
7
8
|
module RelationPatch
|
8
9
|
module QueryDelegation
|
9
|
-
delegate :with, :foster_select, to: :all
|
10
|
+
delegate :with, :define_window, :select_window, :foster_select, to: :all
|
10
11
|
delegate(*::ActiveRecordExtended::QueryMethods::Unionize::UNIONIZE_METHODS, to: :all)
|
11
12
|
delegate(*::ActiveRecordExtended::QueryMethods::Json::JSON_QUERY_METHODS, to: :all)
|
12
13
|
end
|
13
14
|
|
14
15
|
module Merger
|
15
16
|
def normal_values
|
16
|
-
super + [:with, :union]
|
17
|
+
super + [:with, :union, :define_window]
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
21
|
module ArelBuildPatch
|
21
22
|
def build_arel(*aliases)
|
22
23
|
super.tap do |arel|
|
23
|
-
|
24
|
-
|
24
|
+
build_windows(arel) if window_values?
|
25
|
+
build_unions(arel) if union_values?
|
26
|
+
build_with(arel) if with_values?
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordExtended
|
4
|
+
module QueryMethods
|
5
|
+
module Window
|
6
|
+
class DefineWindowChain
|
7
|
+
include ::ActiveRecordExtended::Utilities::Support
|
8
|
+
include ::ActiveRecordExtended::Utilities::OrderBy
|
9
|
+
|
10
|
+
def initialize(scope, window_name)
|
11
|
+
@scope = scope
|
12
|
+
@window_name = window_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def partition_by(*partitions, order_by: nil)
|
16
|
+
@scope.window_values! << {
|
17
|
+
window_name: to_arel_sql(@window_name),
|
18
|
+
partition_by: flatten_to_sql(partitions),
|
19
|
+
order_by: order_by_expression(order_by),
|
20
|
+
}
|
21
|
+
|
22
|
+
@scope
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class WindowSelectBuilder
|
27
|
+
include ::ActiveRecordExtended::Utilities::Support
|
28
|
+
|
29
|
+
def initialize(window_function, args, window_name)
|
30
|
+
@window_function = window_function
|
31
|
+
@win_args = to_sql_array(args)
|
32
|
+
@over = to_arel_sql(window_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_select(alias_name = nil)
|
36
|
+
window_arel = generate_named_function(@window_function, *@win_args).over(@over)
|
37
|
+
|
38
|
+
if alias_name.nil?
|
39
|
+
window_arel
|
40
|
+
else
|
41
|
+
nested_alias_escape(window_arel, alias_name)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def window_values
|
47
|
+
@values.fetch(:window, [])
|
48
|
+
end
|
49
|
+
|
50
|
+
def window_values!
|
51
|
+
@values[:window] ||= []
|
52
|
+
end
|
53
|
+
|
54
|
+
def window_values?
|
55
|
+
!window_values.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def window_values=(*values)
|
59
|
+
@values[:window] = values.flatten(1)
|
60
|
+
end
|
61
|
+
|
62
|
+
def define_window(name)
|
63
|
+
spawn.define_window!(name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def define_window!(name)
|
67
|
+
DefineWindowChain.new(self, name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def select_window(window_function, *args, over:, as: nil)
|
71
|
+
spawn.select_window!(window_function, args, over: over, as: as)
|
72
|
+
end
|
73
|
+
|
74
|
+
def select_window!(window_function, *args, over:, as: nil)
|
75
|
+
args.flatten!
|
76
|
+
args.compact!
|
77
|
+
|
78
|
+
select_statement = WindowSelectBuilder.new(window_function, args, over).build_select(as)
|
79
|
+
_select!(select_statement)
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_windows(arel)
|
83
|
+
window_values.each do |window_value|
|
84
|
+
window = arel.window(window_value[:window_name]).partition(window_value[:partition_by])
|
85
|
+
window.order(window_value[:order_by]) if window_value[:order_by]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
ActiveRecord::Relation.prepend(ActiveRecordExtended::QueryMethods::Window)
|
@@ -30,7 +30,7 @@ module ActiveRecordExtended
|
|
30
30
|
# Applies aliases to the given query
|
31
31
|
# Ex: `SELECT * FROM users` => `(SELECT * FROM users) AS "members"`
|
32
32
|
def nested_alias_escape(query, alias_name)
|
33
|
-
sql_query =
|
33
|
+
sql_query = generate_grouping(query)
|
34
34
|
Arel::Nodes::As.new(sql_query, to_arel_sql(double_quote(alias_name)))
|
35
35
|
end
|
36
36
|
|
@@ -160,13 +160,23 @@ module ActiveRecordExtended
|
|
160
160
|
|
161
161
|
def group_when_needed(arel_or_rel_query)
|
162
162
|
return arel_or_rel_query unless needs_to_be_grouped?(arel_or_rel_query)
|
163
|
-
|
163
|
+
generate_grouping(arel_or_rel_query)
|
164
164
|
end
|
165
165
|
|
166
166
|
def needs_to_be_grouped?(query)
|
167
167
|
query.respond_to?(:to_sql) || (query.is_a?(String) && /^SELECT.+/i.match?(query))
|
168
168
|
end
|
169
169
|
|
170
|
+
def generate_grouping(expr)
|
171
|
+
::Arel::Nodes::Grouping.new(to_arel_sql(expr))
|
172
|
+
end
|
173
|
+
|
174
|
+
def generate_named_function(function_name, *args)
|
175
|
+
args.map!(&method(:to_arel_sql))
|
176
|
+
function_name = function_name.to_s.upcase
|
177
|
+
::Arel::Nodes::NamedFunction.new(to_arel_sql(function_name), args)
|
178
|
+
end
|
179
|
+
|
170
180
|
def key_generator
|
171
181
|
A_TO_Z_KEYS.sample
|
172
182
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Active Record Window Function Query Methods" do
|
6
|
+
let!(:user_one) { User.create! }
|
7
|
+
let!(:user_two) { User.create! }
|
8
|
+
|
9
|
+
let!(:tag_one) { Tag.create!(user: user_one, tag_number: 1) }
|
10
|
+
let!(:tag_two) { Tag.create!(user: user_two, tag_number: 2) }
|
11
|
+
|
12
|
+
let!(:tag_three) { Tag.create!(user: user_one, tag_number: 3) }
|
13
|
+
let!(:tag_four) { Tag.create!(user: user_two, tag_number: 4) }
|
14
|
+
|
15
|
+
let(:tag_group1) { [tag_one, tag_three] }
|
16
|
+
let(:tag_group2) { [tag_two, tag_four] }
|
17
|
+
|
18
|
+
describe ".window_select" do
|
19
|
+
context "when using ROW_NUMBER() ordered in asc" do
|
20
|
+
let(:base_query) do
|
21
|
+
Tag.define_window(:w).partition_by(:user_id, order_by: :tag_number).select(:id)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should return tag_one with r_id 1 and tag_three with r_id 2" do
|
25
|
+
results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
|
26
|
+
tag_group1.each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return tag_two with r_id 1 and tag_four with r_id 2" do
|
30
|
+
results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
|
31
|
+
tag_group2.each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when using ROW_NUMBER() ordered in desc" do
|
36
|
+
let(:base_query) do
|
37
|
+
Tag.define_window(:w).partition_by(:user_id, order_by: { tag_number: :desc }).select(:id)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return tag_one with r_id 2 and tag_three with r_id 1" do
|
41
|
+
results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
|
42
|
+
tag_group1.reverse_each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return tag_two with r_id 2 and tag_four with r_id 1" do
|
46
|
+
results = base_query.select_window(:row_number, over: :w, as: :r_id).group_by(&:id)
|
47
|
+
tag_group2.reverse_each.with_index { |tag, idx| expect(results[tag.id].first.r_id).to eq(idx + 1) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe "Active Record WINDOW Query inspection" do
|
6
|
+
describe "#define_window" do
|
7
|
+
context "when there is a single defined window" do
|
8
|
+
it "should only contain a single defined window statement at the bottom" do
|
9
|
+
query = Tag.define_window(:w_test).partition_by(:user_id).to_sql
|
10
|
+
expect(query).to eq('SELECT "tags".* FROM "tags" WINDOW w_test AS (PARTITION BY user_id)')
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should return a single defined window with a defined ORDER BY" do
|
14
|
+
query = Tag.define_window(:w_test).partition_by(:user_id, order_by: { tags: { user_id: :desc } }).to_sql
|
15
|
+
expect(query).to end_with("WINDOW w_test AS (PARTITION BY user_id ORDER BY tags.user_id DESC)")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should place the window function below the WHERE and GROUP BY statements" do
|
19
|
+
query = Tag.define_window(:w_test).partition_by(:user_id).where(id: 1).group(:user_id).to_sql
|
20
|
+
expect(query).to eq('SELECT "tags".* FROM "tags" WHERE "tags"."id" = 1 GROUP BY "tags"."user_id" WINDOW w_test AS (PARTITION BY user_id)')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when there are multiple defined windows" do
|
25
|
+
it "should only contain a single defined window statement at the bottom" do
|
26
|
+
query =
|
27
|
+
Tag
|
28
|
+
.define_window(:test).partition_by(:user_id)
|
29
|
+
.define_window(:other_window).partition_by(:id)
|
30
|
+
.to_sql
|
31
|
+
|
32
|
+
expect(query).to end_with("WINDOW test AS (PARTITION BY user_id), other_window AS (PARTITION BY id)")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should contain each windows order by statements" do
|
36
|
+
query =
|
37
|
+
Tag
|
38
|
+
.define_window(:test).partition_by(:user_id, order_by: :id)
|
39
|
+
.define_window(:other_window).partition_by(:id, order_by: { tags: :user_id })
|
40
|
+
.to_sql
|
41
|
+
|
42
|
+
expect(query).to end_with("WINDOW test AS (PARTITION BY user_id ORDER BY id), other_window AS (PARTITION BY id ORDER BY tags.user_id ASC)")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#window_select" do
|
48
|
+
let(:query_base) { Tag.define_window(:w).partition_by(:user_id, order_by: :id) }
|
49
|
+
let(:expected_end) { "WINDOW w AS (PARTITION BY user_id ORDER BY id)" }
|
50
|
+
|
51
|
+
context "when using no argument window methods" do
|
52
|
+
[:row_to_number, :rank, :dense_rank, :percent_rank, :cume_dist].each do |window_function|
|
53
|
+
context "#{window_function.to_s.upcase}()" do
|
54
|
+
let(:expected_function) { "#{window_function.to_s.upcase}()" }
|
55
|
+
let(:query) do
|
56
|
+
query_base.select_window(window_function, over: :w, as: :window_response).to_sql
|
57
|
+
end
|
58
|
+
|
59
|
+
it "appends the function to the select query" do
|
60
|
+
expected_start = "SELECT (#{expected_function} OVER w) AS \"window_response\""
|
61
|
+
expect(query).to start_with(expected_start).and(end_with(expected_end))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when using an argument based window method" do
|
68
|
+
let(:argument_list) { ["a", 1, :sauce] }
|
69
|
+
|
70
|
+
{ ntile: 1, lag: 2, lead: 3, first_value: 1, last_value: 1, nth_value: 2 }.each_pair do |window_function, arg_count|
|
71
|
+
context "#{window_function.to_s.upcase}/#{arg_count}" do
|
72
|
+
let(:arguments) { argument_list.first(arg_count) }
|
73
|
+
let(:expected_function) { "#{window_function.to_s.upcase}(#{arguments.join(', ')})" }
|
74
|
+
let(:query) do
|
75
|
+
query_base.select_window(window_function, *arguments, over: :w, as: :window_response).to_sql
|
76
|
+
end
|
77
|
+
|
78
|
+
it "appends the function to the select query" do
|
79
|
+
expected_start = "SELECT (#{expected_function} OVER w) AS \"window_response\""
|
80
|
+
expect(query).to start_with(expected_start).and(end_with(expected_end))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_extended
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- George Protacio-Karaszi
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2019-
|
13
|
+
date: 2019-09-04 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -166,6 +166,7 @@ files:
|
|
166
166
|
- lib/active_record_extended/query_methods/select.rb
|
167
167
|
- lib/active_record_extended/query_methods/unionize.rb
|
168
168
|
- lib/active_record_extended/query_methods/where_chain.rb
|
169
|
+
- lib/active_record_extended/query_methods/window.rb
|
169
170
|
- lib/active_record_extended/query_methods/with_cte.rb
|
170
171
|
- lib/active_record_extended/utilities/order_by.rb
|
171
172
|
- lib/active_record_extended/utilities/support.rb
|
@@ -179,6 +180,7 @@ files:
|
|
179
180
|
- spec/query_methods/json_spec.rb
|
180
181
|
- spec/query_methods/select_spec.rb
|
181
182
|
- spec/query_methods/unionize_spec.rb
|
183
|
+
- spec/query_methods/window_spec.rb
|
182
184
|
- spec/query_methods/with_cte_spec.rb
|
183
185
|
- spec/spec_helper.rb
|
184
186
|
- spec/sql_inspections/any_of_sql_spec.rb
|
@@ -189,6 +191,7 @@ files:
|
|
189
191
|
- spec/sql_inspections/either_sql_spec.rb
|
190
192
|
- spec/sql_inspections/json_sql_spec.rb
|
191
193
|
- spec/sql_inspections/unionize_sql_spec.rb
|
194
|
+
- spec/sql_inspections/window_sql_spec.rb
|
192
195
|
- spec/sql_inspections/with_cte_sql_spec.rb
|
193
196
|
- spec/support/database_cleaner.rb
|
194
197
|
- spec/support/models.rb
|
@@ -226,6 +229,7 @@ test_files:
|
|
226
229
|
- spec/query_methods/json_spec.rb
|
227
230
|
- spec/query_methods/select_spec.rb
|
228
231
|
- spec/query_methods/unionize_spec.rb
|
232
|
+
- spec/query_methods/window_spec.rb
|
229
233
|
- spec/query_methods/with_cte_spec.rb
|
230
234
|
- spec/spec_helper.rb
|
231
235
|
- spec/sql_inspections/any_of_sql_spec.rb
|
@@ -236,6 +240,7 @@ test_files:
|
|
236
240
|
- spec/sql_inspections/either_sql_spec.rb
|
237
241
|
- spec/sql_inspections/json_sql_spec.rb
|
238
242
|
- spec/sql_inspections/unionize_sql_spec.rb
|
243
|
+
- spec/sql_inspections/window_sql_spec.rb
|
239
244
|
- spec/sql_inspections/with_cte_sql_spec.rb
|
240
245
|
- spec/support/database_cleaner.rb
|
241
246
|
- spec/support/models.rb
|