activerecord-refined 0.9.0 → 0.10.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +61 -1064
  4. data/activerecord-refined.gemspec +8 -7
  5. data/docs/conditions.md +206 -0
  6. data/docs/ctes.md +65 -0
  7. data/docs/expressions.md +125 -0
  8. data/docs/functions.md +219 -0
  9. data/docs/grouping.md +55 -0
  10. data/docs/index.md +70 -0
  11. data/docs/joins.md +73 -0
  12. data/docs/json.md +230 -0
  13. data/docs/ordering.md +55 -0
  14. data/docs/time_zones.md +30 -0
  15. data/docs/windows.md +41 -0
  16. data/docs/writing.md +33 -0
  17. data/examples/aggregations.rb +18 -0
  18. data/examples/expressions.rb +35 -5
  19. data/lib/active_record/refined/ast.rb +461 -246
  20. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  21. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  22. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  23. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  24. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  25. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  26. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  27. data/lib/active_record/refined/dialect.rb +340 -0
  28. data/lib/active_record/refined.rb +682 -192
  29. data/lib/activerecord-refined/version.rb +1 -1
  30. data/lib/activerecord-refined.rb +1 -0
  31. metadata +43 -16
  32. data/.github/workflows/push_gem.yml +0 -45
  33. data/.github/workflows/sandbox.yml +0 -295
  34. data/.github/workflows/test.yml +0 -104
  35. data/.gitignore +0 -19
  36. data/.rubocop.yml +0 -393
  37. data/Gemfile +0 -14
  38. data/Rakefile +0 -53
  39. data/benchmark/query_building.rb +0 -129
  40. data/test/test_block_syntax.rb +0 -2999
  41. 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