activerecord-refined 0.9.0 → 0.10.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/.yardopts +17 -0
- data/README.md +93 -965
- data/activerecord-refined.gemspec +12 -7
- data/docs/conditions.md +206 -0
- data/docs/ctes.md +65 -0
- data/docs/expressions.md +125 -0
- data/docs/functions.md +219 -0
- data/docs/grouping.md +55 -0
- data/docs/joins.md +73 -0
- data/docs/json.md +230 -0
- data/docs/ordering.md +55 -0
- data/docs/time_zones.md +30 -0
- data/docs/windows.md +41 -0
- data/docs/writing.md +33 -0
- data/examples/aggregations.rb +18 -0
- data/examples/expressions.rb +35 -5
- data/lib/active_record/refined/ast.rb +461 -246
- data/lib/active_record/refined/dialect/mariadb.rb +25 -0
- data/lib/active_record/refined/dialect/mysql.rb +18 -0
- data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
- data/lib/active_record/refined/dialect/oracle.rb +110 -0
- data/lib/active_record/refined/dialect/postgresql.rb +120 -0
- data/lib/active_record/refined/dialect/sql_server.rb +115 -0
- data/lib/active_record/refined/dialect/sqlite.rb +57 -0
- data/lib/active_record/refined/dialect.rb +340 -0
- data/lib/active_record/refined.rb +682 -192
- data/lib/activerecord-refined/version.rb +1 -1
- data/lib/activerecord-refined.rb +1 -0
- metadata +58 -16
- data/.github/workflows/push_gem.yml +0 -45
- data/.github/workflows/sandbox.yml +0 -295
- data/.github/workflows/test.yml +0 -104
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -393
- data/Gemfile +0 -14
- data/Rakefile +0 -53
- data/benchmark/query_building.rb +0 -129
- data/test/test_block_syntax.rb +0 -2999
- data/test/test_helper.rb +0 -238
data/test/test_helper.rb
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
|
4
|
-
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
5
|
-
|
|
6
|
-
require "minitest/autorun"
|
|
7
|
-
Bundler.require
|
|
8
|
-
|
|
9
|
-
require "active_record"
|
|
10
|
-
require "etc"
|
|
11
|
-
require "json"
|
|
12
|
-
|
|
13
|
-
# Which database to generate SQL for. The tests only build queries, so any
|
|
14
|
-
# server reachable without further setup will do; the devcontainer runs
|
|
15
|
-
# PostgreSQL and MariaDB on localhost with a passwordless account named after
|
|
16
|
-
# the container user.
|
|
17
|
-
#
|
|
18
|
-
# rake test # sqlite3
|
|
19
|
-
# ADAPTER=postgresql rake test
|
|
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")
|
|
25
|
-
|
|
26
|
-
DATABASE_NAME = "activerecord_refined_test"
|
|
27
|
-
|
|
28
|
-
DATABASE_CONFIG =
|
|
29
|
-
case ADAPTER
|
|
30
|
-
when "sqlite3"
|
|
31
|
-
{ adapter: "sqlite3", database: ":memory:" }
|
|
32
|
-
when "postgresql", "mysql2"
|
|
33
|
-
{
|
|
34
|
-
adapter: ADAPTER,
|
|
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"],
|
|
39
|
-
database: DATABASE_NAME,
|
|
40
|
-
}
|
|
41
|
-
else
|
|
42
|
-
raise ArgumentError, "unknown ADAPTER: #{ADAPTER}"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# The server databases persist between runs, so create one on first use.
|
|
46
|
-
unless ADAPTER == "sqlite3"
|
|
47
|
-
maintenance_database = ADAPTER == "postgresql" ? "postgres" : "mysql"
|
|
48
|
-
ActiveRecord::Base.establish_connection(
|
|
49
|
-
DATABASE_CONFIG.merge(database: maintenance_database))
|
|
50
|
-
begin
|
|
51
|
-
ActiveRecord::Base.lease_connection.create_database(DATABASE_NAME)
|
|
52
|
-
rescue ActiveRecord::DatabaseAlreadyExists
|
|
53
|
-
end
|
|
54
|
-
ActiveRecord::Base.remove_connection
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
ActiveRecord::Base.establish_connection(DATABASE_CONFIG)
|
|
58
|
-
|
|
59
|
-
module SqlAssertions
|
|
60
|
-
# Arel spells the regexp match differently per adapter, and raises on the
|
|
61
|
-
# ones missing from this list.
|
|
62
|
-
REGEXP_OPERATORS = {
|
|
63
|
-
"postgresql" => ["~", "!~"],
|
|
64
|
-
"mysql2" => ["REGEXP", "NOT REGEXP"],
|
|
65
|
-
}.freeze
|
|
66
|
-
|
|
67
|
-
def skip_without_regexp_support
|
|
68
|
-
skip "#{ADAPTER} has no regexp operator" unless REGEXP_OPERATORS.key?(ADAPTER)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# upsert_all wants to be told which unique index it is upserting against,
|
|
72
|
-
# except on MySQL, which does not accept being told.
|
|
73
|
-
def upsert_target
|
|
74
|
-
ADAPTER == "mysql2" ? {} : { unique_by: :page }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# JSON containment: PostgreSQL has @>, MySQL JSON_CONTAINS, SQLite neither.
|
|
78
|
-
def skip_without_json_containment
|
|
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"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# MySQL is the one without a FULL OUTER JOIN, and so is MariaDB.
|
|
91
|
-
def skip_without_full_outer_joins
|
|
92
|
-
skip "#{ADAPTER} has no full outer join" if ADAPTER == "mysql2"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# MariaDB's json is a checked longtext, which Active Record sees as a string
|
|
96
|
-
# and does not serialise a hash into -- what would go in is Ruby's inspect,
|
|
97
|
-
# which the check refuses. MySQL's json is a type of its own and takes the
|
|
98
|
-
# hash; handing that one a string would store the document as a JSON string
|
|
99
|
-
# rather than as the object, and every path would find nothing. Both answer
|
|
100
|
-
# to the mysql2 adapter, so the two have to be told apart.
|
|
101
|
-
def json_document(hash)
|
|
102
|
-
mariadb? ? JSON.generate(hash) : hash
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# How each adapter spells a cast to a whole number: MySQL casts to SIGNED
|
|
106
|
-
# and has no integer at all, PostgreSQL the other way about, SQLite either.
|
|
107
|
-
def integer_type
|
|
108
|
-
ADAPTER == "mysql2" ? "signed" : "integer"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def mariadb?
|
|
112
|
-
ADAPTER == "mysql2" && ActiveRecord::Base.connection.mariadb?
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# GROUPING SETS, ROLLUP and CUBE are PostgreSQL's; MySQL has only WITH
|
|
116
|
-
# ROLLUP, which carries rollup and only rollup, and SQLite has none.
|
|
117
|
-
def skip_without_grouping_sets
|
|
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"
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# LATERAL is PostgreSQL's and MySQL 8's; MariaDB and SQLite have none.
|
|
126
|
-
def skip_without_lateral
|
|
127
|
-
return if ADAPTER == "postgresql"
|
|
128
|
-
skip "#{mariadb? ? 'MariaDB' : ADAPTER} has no LATERAL" if ADAPTER != "mysql2" || mariadb?
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# DISTINCT ON is PostgreSQL's; Arel refuses to write it for the others.
|
|
132
|
-
def skip_without_distinct_on
|
|
133
|
-
skip "#{ADAPTER} has no DISTINCT ON" unless ADAPTER == "postgresql"
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# ANY and ALL over a subquery are PostgreSQL's and MySQL's alike; SQLite
|
|
137
|
-
# has neither quantifier.
|
|
138
|
-
def skip_without_quantifiers
|
|
139
|
-
skip "#{ADAPTER} has no ANY or ALL" if ADAPTER == "sqlite3"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# BIT_AND, BIT_OR and BIT_XOR are PostgreSQL's and MySQL's; SQLite has
|
|
143
|
-
# none of the three, nor BIT_COUNT.
|
|
144
|
-
def skip_without_bit_aggregates
|
|
145
|
-
skip "#{ADAPTER} has no bit aggregates" if ADAPTER == "sqlite3"
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def skip_without_array_columns
|
|
149
|
-
skip "#{ADAPTER} has no array columns" unless ADAPTER == "postgresql"
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# MySQL has no NULLS FIRST/LAST; Arel emulates it with a leading IS NULL
|
|
153
|
-
# ordering, so only the resulting order is portable, not the SQL.
|
|
154
|
-
def skip_without_nulls_ordering_syntax
|
|
155
|
-
skip "#{ADAPTER} emulates NULLS FIRST/LAST" if ADAPTER == "mysql2"
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def regexp_operator
|
|
159
|
-
Regexp.escape(REGEXP_OPERATORS.fetch(ADAPTER).first)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def not_regexp_operator
|
|
163
|
-
Regexp.escape(REGEXP_OPERATORS.fetch(ADAPTER).last)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# MySQL quotes identifiers with backticks instead of double quotes, and
|
|
167
|
-
# doubles the backslashes inside string literals because backslash is itself
|
|
168
|
-
# an escape character there. Normalising both lets a single expectation
|
|
169
|
-
# cover every adapter.
|
|
170
|
-
def normalize_sql(sql)
|
|
171
|
-
sql.tr("`", '"').gsub("\\\\") { "\\" }
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Takes a relation or the SQL string of one.
|
|
175
|
-
def assert_sql(pattern, relation_or_sql)
|
|
176
|
-
sql = relation_or_sql.respond_to?(:to_sql) ? relation_or_sql.to_sql : relation_or_sql
|
|
177
|
-
assert_match(pattern, normalize_sql(sql))
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
class Minitest::Test
|
|
182
|
-
include SqlAssertions
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
class User < ActiveRecord::Base
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
class Author < ActiveRecord::Base
|
|
189
|
-
has_many :posts
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
class Post < ActiveRecord::Base
|
|
193
|
-
belongs_to :author
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Self-referencing, for the recursive CTE tests.
|
|
197
|
-
class Node < ActiveRecord::Base
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# For the writing statements, which need something to conflict with.
|
|
201
|
-
class Tally < ActiveRecord::Base
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# For the JSON tests; see json_document for how the two MySQLs differ.
|
|
205
|
-
class Doc < ActiveRecord::Base
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
class CreateAllTables < ActiveRecord::Migration[8.1]
|
|
209
|
-
def up
|
|
210
|
-
drop_table(:users, if_exists: true)
|
|
211
|
-
drop_table(:authors, if_exists: true)
|
|
212
|
-
drop_table(:posts, if_exists: true)
|
|
213
|
-
drop_table(:nodes, if_exists: true)
|
|
214
|
-
drop_table(:tallies, if_exists: true)
|
|
215
|
-
drop_table(:docs, if_exists: true)
|
|
216
|
-
create_table(:users) do |t|
|
|
217
|
-
t.string :name
|
|
218
|
-
t.integer :age
|
|
219
|
-
t.boolean :active
|
|
220
|
-
t.integer :flags
|
|
221
|
-
t.string :tags, array: true if ADAPTER == "postgresql"
|
|
222
|
-
end
|
|
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 }
|
|
226
|
-
create_table(:tallies) do |t|
|
|
227
|
-
t.string :page
|
|
228
|
-
t.integer :hits
|
|
229
|
-
t.index :page, unique: true
|
|
230
|
-
end
|
|
231
|
-
create_table(:docs) do |t|
|
|
232
|
-
t.string :name
|
|
233
|
-
ADAPTER == "postgresql" ? t.jsonb(:meta) : t.json(:meta)
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
ActiveRecord::Migration.verbose = false
|
|
238
|
-
CreateAllTables.new.up
|