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