activerecord-refined 0.8.0 → 0.9.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 +4 -4
- data/.github/workflows/test.yml +14 -0
- data/.rubocop.yml +393 -0
- data/Gemfile +5 -3
- data/README.md +254 -74
- data/Rakefile +31 -3
- data/activerecord-refined.gemspec +29 -15
- data/benchmark/query_building.rb +11 -11
- data/examples/aggregations.rb +13 -11
- data/examples/complex_joins.rb +12 -10
- data/examples/ctes.rb +22 -20
- data/examples/expressions.rb +79 -44
- data/examples/json.rb +77 -38
- data/examples/postgresql.rb +64 -53
- data/examples/predicates.rb +35 -33
- data/examples/subqueries.rb +20 -18
- data/examples/windows.rb +23 -21
- data/examples/writes.rb +26 -24
- data/lib/active_record/refined/ast.rb +862 -272
- data/lib/active_record/refined.rb +260 -180
- data/lib/activerecord-refined/version.rb +3 -1
- data/lib/activerecord-refined.rb +8 -5
- data/test/test_block_syntax.rb +879 -323
- data/test/test_helper.rb +56 -39
- metadata +130 -1
data/test/test_helper.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
|
2
4
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
3
5
|
|
|
4
|
-
require
|
|
6
|
+
require "minitest/autorun"
|
|
5
7
|
Bundler.require
|
|
6
8
|
|
|
7
|
-
require
|
|
8
|
-
require
|
|
9
|
-
require
|
|
9
|
+
require "active_record"
|
|
10
|
+
require "etc"
|
|
11
|
+
require "json"
|
|
10
12
|
|
|
11
13
|
# Which database to generate SQL for. The tests only build queries, so any
|
|
12
14
|
# server reachable without further setup will do; the devcontainer runs
|
|
@@ -15,22 +17,25 @@ require 'json'
|
|
|
15
17
|
#
|
|
16
18
|
# rake test # sqlite3
|
|
17
19
|
# ADAPTER=postgresql rake test
|
|
18
|
-
# ADAPTER=mysql2 rake test
|
|
19
|
-
# rake test
|
|
20
|
-
|
|
20
|
+
# ADAPTER=mysql2 rake test # MariaDB, on 3306
|
|
21
|
+
# ADAPTER=mysql2 DB_PORT=3307 rake test # MySQL, where the devcontainer
|
|
22
|
+
# # serves it (rake test:mysql8)
|
|
23
|
+
# rake test:all # all of the above in turn
|
|
24
|
+
ADAPTER = ENV.fetch("ADAPTER", "sqlite3")
|
|
21
25
|
|
|
22
|
-
DATABASE_NAME =
|
|
26
|
+
DATABASE_NAME = "activerecord_refined_test"
|
|
23
27
|
|
|
24
28
|
DATABASE_CONFIG =
|
|
25
29
|
case ADAPTER
|
|
26
|
-
when
|
|
27
|
-
{adapter:
|
|
28
|
-
when
|
|
30
|
+
when "sqlite3"
|
|
31
|
+
{ adapter: "sqlite3", database: ":memory:" }
|
|
32
|
+
when "postgresql", "mysql2"
|
|
29
33
|
{
|
|
30
34
|
adapter: ADAPTER,
|
|
31
|
-
host: ENV.fetch(
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
host: ENV.fetch("DB_HOST", "127.0.0.1"),
|
|
36
|
+
port: ENV["DB_PORT"]&.to_i,
|
|
37
|
+
username: ENV.fetch("DB_USERNAME") { Etc.getlogin },
|
|
38
|
+
password: ENV["DB_PASSWORD"],
|
|
34
39
|
database: DATABASE_NAME,
|
|
35
40
|
}
|
|
36
41
|
else
|
|
@@ -38,8 +43,8 @@ DATABASE_CONFIG =
|
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
# The server databases persist between runs, so create one on first use.
|
|
41
|
-
unless ADAPTER ==
|
|
42
|
-
maintenance_database = ADAPTER ==
|
|
46
|
+
unless ADAPTER == "sqlite3"
|
|
47
|
+
maintenance_database = ADAPTER == "postgresql" ? "postgres" : "mysql"
|
|
43
48
|
ActiveRecord::Base.establish_connection(
|
|
44
49
|
DATABASE_CONFIG.merge(database: maintenance_database))
|
|
45
50
|
begin
|
|
@@ -55,8 +60,8 @@ module SqlAssertions
|
|
|
55
60
|
# Arel spells the regexp match differently per adapter, and raises on the
|
|
56
61
|
# ones missing from this list.
|
|
57
62
|
REGEXP_OPERATORS = {
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
"postgresql" => ["~", "!~"],
|
|
64
|
+
"mysql2" => ["REGEXP", "NOT REGEXP"],
|
|
60
65
|
}.freeze
|
|
61
66
|
|
|
62
67
|
def skip_without_regexp_support
|
|
@@ -66,17 +71,25 @@ module SqlAssertions
|
|
|
66
71
|
# upsert_all wants to be told which unique index it is upserting against,
|
|
67
72
|
# except on MySQL, which does not accept being told.
|
|
68
73
|
def upsert_target
|
|
69
|
-
ADAPTER ==
|
|
74
|
+
ADAPTER == "mysql2" ? {} : { unique_by: :page }
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
# JSON containment: PostgreSQL has @>, MySQL JSON_CONTAINS, SQLite neither.
|
|
73
78
|
def skip_without_json_containment
|
|
74
|
-
skip "#{ADAPTER} has no JSON containment" if ADAPTER ==
|
|
79
|
+
skip "#{ADAPTER} has no JSON containment" if ADAPTER == "sqlite3"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Comparing a JSON value with a Ruby one is for jsonb and MySQL's JSON
|
|
83
|
+
# type; SQLite and MariaDB have only the text.
|
|
84
|
+
def skip_without_json_comparisons
|
|
85
|
+
return if ADAPTER == "postgresql"
|
|
86
|
+
return if ADAPTER == "mysql2" && !mariadb?
|
|
87
|
+
skip "#{mariadb? ? 'MariaDB' : ADAPTER} has no JSON comparison"
|
|
75
88
|
end
|
|
76
89
|
|
|
77
90
|
# MySQL is the one without a FULL OUTER JOIN, and so is MariaDB.
|
|
78
91
|
def skip_without_full_outer_joins
|
|
79
|
-
skip "#{ADAPTER} has no full outer join" if ADAPTER ==
|
|
92
|
+
skip "#{ADAPTER} has no full outer join" if ADAPTER == "mysql2"
|
|
80
93
|
end
|
|
81
94
|
|
|
82
95
|
# MariaDB's json is a checked longtext, which Active Record sees as a string
|
|
@@ -92,50 +105,54 @@ module SqlAssertions
|
|
|
92
105
|
# How each adapter spells a cast to a whole number: MySQL casts to SIGNED
|
|
93
106
|
# and has no integer at all, PostgreSQL the other way about, SQLite either.
|
|
94
107
|
def integer_type
|
|
95
|
-
ADAPTER ==
|
|
108
|
+
ADAPTER == "mysql2" ? "signed" : "integer"
|
|
96
109
|
end
|
|
97
110
|
|
|
98
111
|
def mariadb?
|
|
99
|
-
ADAPTER ==
|
|
112
|
+
ADAPTER == "mysql2" && ActiveRecord::Base.connection.mariadb?
|
|
100
113
|
end
|
|
101
114
|
|
|
102
115
|
# GROUPING SETS, ROLLUP and CUBE are PostgreSQL's; MySQL has only WITH
|
|
103
|
-
# ROLLUP, which
|
|
116
|
+
# ROLLUP, which carries rollup and only rollup, and SQLite has none.
|
|
104
117
|
def skip_without_grouping_sets
|
|
105
|
-
skip "#{ADAPTER} has no GROUPING SETS" unless ADAPTER ==
|
|
118
|
+
skip "#{ADAPTER} has no GROUPING SETS" unless ADAPTER == "postgresql"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def skip_without_rollup
|
|
122
|
+
skip "#{ADAPTER} has no ROLLUP" if ADAPTER == "sqlite3"
|
|
106
123
|
end
|
|
107
124
|
|
|
108
125
|
# LATERAL is PostgreSQL's and MySQL 8's; MariaDB and SQLite have none.
|
|
109
126
|
def skip_without_lateral
|
|
110
|
-
return if ADAPTER ==
|
|
111
|
-
skip "#{mariadb? ? 'MariaDB' : ADAPTER} has no LATERAL" if ADAPTER !=
|
|
127
|
+
return if ADAPTER == "postgresql"
|
|
128
|
+
skip "#{mariadb? ? 'MariaDB' : ADAPTER} has no LATERAL" if ADAPTER != "mysql2" || mariadb?
|
|
112
129
|
end
|
|
113
130
|
|
|
114
131
|
# DISTINCT ON is PostgreSQL's; Arel refuses to write it for the others.
|
|
115
132
|
def skip_without_distinct_on
|
|
116
|
-
skip "#{ADAPTER} has no DISTINCT ON" unless ADAPTER ==
|
|
133
|
+
skip "#{ADAPTER} has no DISTINCT ON" unless ADAPTER == "postgresql"
|
|
117
134
|
end
|
|
118
135
|
|
|
119
136
|
# ANY and ALL over a subquery are PostgreSQL's and MySQL's alike; SQLite
|
|
120
137
|
# has neither quantifier.
|
|
121
138
|
def skip_without_quantifiers
|
|
122
|
-
skip "#{ADAPTER} has no ANY or ALL" if ADAPTER ==
|
|
139
|
+
skip "#{ADAPTER} has no ANY or ALL" if ADAPTER == "sqlite3"
|
|
123
140
|
end
|
|
124
141
|
|
|
125
142
|
# BIT_AND, BIT_OR and BIT_XOR are PostgreSQL's and MySQL's; SQLite has
|
|
126
143
|
# none of the three, nor BIT_COUNT.
|
|
127
144
|
def skip_without_bit_aggregates
|
|
128
|
-
skip "#{ADAPTER} has no bit aggregates" if ADAPTER ==
|
|
145
|
+
skip "#{ADAPTER} has no bit aggregates" if ADAPTER == "sqlite3"
|
|
129
146
|
end
|
|
130
147
|
|
|
131
148
|
def skip_without_array_columns
|
|
132
|
-
skip "#{ADAPTER} has no array columns" unless ADAPTER ==
|
|
149
|
+
skip "#{ADAPTER} has no array columns" unless ADAPTER == "postgresql"
|
|
133
150
|
end
|
|
134
151
|
|
|
135
152
|
# MySQL has no NULLS FIRST/LAST; Arel emulates it with a leading IS NULL
|
|
136
153
|
# ordering, so only the resulting order is portable, not the SQL.
|
|
137
154
|
def skip_without_nulls_ordering_syntax
|
|
138
|
-
skip "#{ADAPTER} emulates NULLS FIRST/LAST" if ADAPTER ==
|
|
155
|
+
skip "#{ADAPTER} emulates NULLS FIRST/LAST" if ADAPTER == "mysql2"
|
|
139
156
|
end
|
|
140
157
|
|
|
141
158
|
def regexp_operator
|
|
@@ -151,7 +168,7 @@ module SqlAssertions
|
|
|
151
168
|
# an escape character there. Normalising both lets a single expectation
|
|
152
169
|
# cover every adapter.
|
|
153
170
|
def normalize_sql(sql)
|
|
154
|
-
sql.tr(
|
|
171
|
+
sql.tr("`", '"').gsub("\\\\") { "\\" }
|
|
155
172
|
end
|
|
156
173
|
|
|
157
174
|
# Takes a relation or the SQL string of one.
|
|
@@ -201,11 +218,11 @@ class CreateAllTables < ActiveRecord::Migration[8.1]
|
|
|
201
218
|
t.integer :age
|
|
202
219
|
t.boolean :active
|
|
203
220
|
t.integer :flags
|
|
204
|
-
t.string :tags, array: true if ADAPTER ==
|
|
221
|
+
t.string :tags, array: true if ADAPTER == "postgresql"
|
|
205
222
|
end
|
|
206
|
-
create_table(:authors) {|t| t.string :name}
|
|
207
|
-
create_table(:posts) {|t| t.string :title; t.integer :author_id}
|
|
208
|
-
create_table(:nodes) {|t| t.string :name; t.integer :parent_id}
|
|
223
|
+
create_table(:authors) { |t| t.string :name }
|
|
224
|
+
create_table(:posts) { |t| t.string :title; t.integer :author_id }
|
|
225
|
+
create_table(:nodes) { |t| t.string :name; t.integer :parent_id }
|
|
209
226
|
create_table(:tallies) do |t|
|
|
210
227
|
t.string :page
|
|
211
228
|
t.integer :hits
|
|
@@ -213,7 +230,7 @@ class CreateAllTables < ActiveRecord::Migration[8.1]
|
|
|
213
230
|
end
|
|
214
231
|
create_table(:docs) do |t|
|
|
215
232
|
t.string :name
|
|
216
|
-
ADAPTER ==
|
|
233
|
+
ADAPTER == "postgresql" ? t.jsonb(:meta) : t.json(:meta)
|
|
217
234
|
end
|
|
218
235
|
end
|
|
219
236
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-refined
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shugo Maeda
|
|
@@ -89,6 +89,134 @@ dependencies:
|
|
|
89
89
|
- ">="
|
|
90
90
|
- !ruby/object:Gem::Version
|
|
91
91
|
version: "0"
|
|
92
|
+
- !ruby/object:Gem::Dependency
|
|
93
|
+
name: benchmark-ips
|
|
94
|
+
requirement: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
-
|
|
97
|
+
- ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: "0"
|
|
100
|
+
type: :development
|
|
101
|
+
prerelease: false
|
|
102
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
-
|
|
105
|
+
- ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: "0"
|
|
108
|
+
- !ruby/object:Gem::Dependency
|
|
109
|
+
name: memory_profiler
|
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
-
|
|
113
|
+
- ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: "0"
|
|
116
|
+
type: :development
|
|
117
|
+
prerelease: false
|
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
-
|
|
121
|
+
- ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: "0"
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: rubocop
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
-
|
|
129
|
+
- ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: "0"
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
-
|
|
137
|
+
- ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: "0"
|
|
140
|
+
- !ruby/object:Gem::Dependency
|
|
141
|
+
name: rubocop-minitest
|
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
|
143
|
+
requirements:
|
|
144
|
+
-
|
|
145
|
+
- ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: "0"
|
|
148
|
+
type: :development
|
|
149
|
+
prerelease: false
|
|
150
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
-
|
|
153
|
+
- ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: "0"
|
|
156
|
+
- !ruby/object:Gem::Dependency
|
|
157
|
+
name: rubocop-packaging
|
|
158
|
+
requirement: !ruby/object:Gem::Requirement
|
|
159
|
+
requirements:
|
|
160
|
+
-
|
|
161
|
+
- ">="
|
|
162
|
+
- !ruby/object:Gem::Version
|
|
163
|
+
version: "0"
|
|
164
|
+
type: :development
|
|
165
|
+
prerelease: false
|
|
166
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
167
|
+
requirements:
|
|
168
|
+
-
|
|
169
|
+
- ">="
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: "0"
|
|
172
|
+
- !ruby/object:Gem::Dependency
|
|
173
|
+
name: rubocop-performance
|
|
174
|
+
requirement: !ruby/object:Gem::Requirement
|
|
175
|
+
requirements:
|
|
176
|
+
-
|
|
177
|
+
- ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: "0"
|
|
180
|
+
type: :development
|
|
181
|
+
prerelease: false
|
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
183
|
+
requirements:
|
|
184
|
+
-
|
|
185
|
+
- ">="
|
|
186
|
+
- !ruby/object:Gem::Version
|
|
187
|
+
version: "0"
|
|
188
|
+
- !ruby/object:Gem::Dependency
|
|
189
|
+
name: rubocop-rails
|
|
190
|
+
requirement: !ruby/object:Gem::Requirement
|
|
191
|
+
requirements:
|
|
192
|
+
-
|
|
193
|
+
- ">="
|
|
194
|
+
- !ruby/object:Gem::Version
|
|
195
|
+
version: "0"
|
|
196
|
+
type: :development
|
|
197
|
+
prerelease: false
|
|
198
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
199
|
+
requirements:
|
|
200
|
+
-
|
|
201
|
+
- ">="
|
|
202
|
+
- !ruby/object:Gem::Version
|
|
203
|
+
version: "0"
|
|
204
|
+
- !ruby/object:Gem::Dependency
|
|
205
|
+
name: rubocop-md
|
|
206
|
+
requirement: !ruby/object:Gem::Requirement
|
|
207
|
+
requirements:
|
|
208
|
+
-
|
|
209
|
+
- ">="
|
|
210
|
+
- !ruby/object:Gem::Version
|
|
211
|
+
version: "0"
|
|
212
|
+
type: :development
|
|
213
|
+
prerelease: false
|
|
214
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
215
|
+
requirements:
|
|
216
|
+
-
|
|
217
|
+
- ">="
|
|
218
|
+
- !ruby/object:Gem::Version
|
|
219
|
+
version: "0"
|
|
92
220
|
description: Adding clean and powerful query syntax on Active Record using refinements
|
|
93
221
|
email:
|
|
94
222
|
- shugo@ruby-lang.org
|
|
@@ -100,6 +228,7 @@ files:
|
|
|
100
228
|
- .github/workflows/sandbox.yml
|
|
101
229
|
- .github/workflows/test.yml
|
|
102
230
|
- .gitignore
|
|
231
|
+
- .rubocop.yml
|
|
103
232
|
- Gemfile
|
|
104
233
|
- LICENSE.txt
|
|
105
234
|
- README.md
|