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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +17 -0
  3. data/README.md +93 -965
  4. data/activerecord-refined.gemspec +12 -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/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 +18 -0
  17. data/examples/expressions.rb +35 -5
  18. data/lib/active_record/refined/ast.rb +461 -246
  19. data/lib/active_record/refined/dialect/mariadb.rb +25 -0
  20. data/lib/active_record/refined/dialect/mysql.rb +18 -0
  21. data/lib/active_record/refined/dialect/mysql_compat.rb +67 -0
  22. data/lib/active_record/refined/dialect/oracle.rb +110 -0
  23. data/lib/active_record/refined/dialect/postgresql.rb +120 -0
  24. data/lib/active_record/refined/dialect/sql_server.rb +115 -0
  25. data/lib/active_record/refined/dialect/sqlite.rb +57 -0
  26. data/lib/active_record/refined/dialect.rb +340 -0
  27. data/lib/active_record/refined.rb +682 -192
  28. data/lib/activerecord-refined/version.rb +1 -1
  29. data/lib/activerecord-refined.rb +1 -0
  30. metadata +58 -16
  31. data/.github/workflows/push_gem.yml +0 -45
  32. data/.github/workflows/sandbox.yml +0 -295
  33. data/.github/workflows/test.yml +0 -104
  34. data/.gitignore +0 -19
  35. data/.rubocop.yml +0 -393
  36. data/Gemfile +0 -14
  37. data/Rakefile +0 -53
  38. data/benchmark/query_building.rb +0 -129
  39. data/test/test_block_syntax.rb +0 -2999
  40. data/test/test_helper.rb +0 -238
data/.rubocop.yml DELETED
@@ -1,393 +0,0 @@
1
- plugins:
2
- - rubocop-minitest
3
- - rubocop-packaging
4
- - rubocop-performance
5
- - rubocop-rails
6
- - rubocop-md
7
-
8
- AllCops:
9
- # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
10
- # to ignore them, so only the ones explicitly set in this file are enabled.
11
- DisabledByDefault: true
12
- SuggestExtensions: false
13
- # Inferring from the gemspec would stumble over 4.1.0.dev, so say it plainly.
14
- TargetRubyVersion: 4.1
15
- Exclude:
16
- - "**/tmp/**/*"
17
- - "**/vendor/**/*"
18
- - "**/node_modules/**/*"
19
- - "pkg/**/*"
20
- # A site rather than the library, and partly vendored from wasmify-rails.
21
- - "sandbox/**/*"
22
-
23
- Performance:
24
- Exclude:
25
- - '**/test/**/*'
26
-
27
- Rails/IndexBy:
28
- Enabled: true
29
-
30
- Rails/IndexWith:
31
- Enabled: true
32
-
33
- # Prefer &&/|| over and/or.
34
- Style/AndOr:
35
- Enabled: true
36
-
37
- Layout/ClosingHeredocIndentation:
38
- Enabled: true
39
-
40
- Layout/ClosingParenthesisIndentation:
41
- Enabled: true
42
-
43
- # Align comments with method definitions.
44
- Layout/CommentIndentation:
45
- Enabled: true
46
-
47
- Layout/DefEndAlignment:
48
- Enabled: true
49
-
50
- Layout/ElseAlignment:
51
- Enabled: true
52
-
53
- # Align `end` with the matching keyword or starting expression except for
54
- # assignments, where it should be aligned with the LHS.
55
- Layout/EndAlignment:
56
- Enabled: true
57
- EnforcedStyleAlignWith: variable
58
- AutoCorrect: true
59
-
60
- Layout/EndOfLine:
61
- Enabled: true
62
-
63
- Layout/EmptyLineAfterMagicComment:
64
- Enabled: true
65
-
66
- Layout/EmptyLinesAroundAccessModifier:
67
- Enabled: true
68
- EnforcedStyle: only_before
69
-
70
- Layout/EmptyLinesAroundBlockBody:
71
- Enabled: true
72
-
73
- # In a regular class definition, no empty lines around the body.
74
- Layout/EmptyLinesAroundClassBody:
75
- Enabled: true
76
-
77
- # In a regular method definition, no empty lines around the body.
78
- Layout/EmptyLinesAroundMethodBody:
79
- Enabled: true
80
-
81
- # In a regular module definition, no empty lines around the body.
82
- Layout/EmptyLinesAroundModuleBody:
83
- Enabled: true
84
-
85
- # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
86
- Style/HashSyntax:
87
- Enabled: true
88
- EnforcedShorthandSyntax: either
89
-
90
- # Method definitions after `private` or `protected` isolated calls need one
91
- # extra level of indentation.
92
- Layout/IndentationConsistency:
93
- Enabled: true
94
- EnforcedStyle: indented_internal_methods
95
- Exclude:
96
- - '**/*.md'
97
-
98
- # Two spaces, no tabs (for indentation).
99
- Layout/IndentationWidth:
100
- Enabled: true
101
-
102
- Layout/LeadingCommentSpace:
103
- Enabled: true
104
-
105
- Layout/SpaceAfterColon:
106
- Enabled: true
107
-
108
- Layout/SpaceAfterComma:
109
- Enabled: true
110
-
111
- Layout/SpaceAfterSemicolon:
112
- Enabled: true
113
-
114
- Layout/SpaceAroundEqualsInParameterDefault:
115
- Enabled: true
116
-
117
- Layout/SpaceAroundKeyword:
118
- Enabled: true
119
-
120
- Layout/SpaceAroundOperators:
121
- Enabled: true
122
-
123
- Layout/SpaceBeforeComma:
124
- Enabled: true
125
-
126
- Layout/SpaceBeforeComment:
127
- Enabled: true
128
-
129
- Layout/SpaceBeforeFirstArg:
130
- Enabled: true
131
-
132
- Style/DefWithParentheses:
133
- Enabled: true
134
-
135
- # Defining a method with parameters needs parentheses.
136
- Style/MethodDefParentheses:
137
- Enabled: true
138
-
139
- Style/ExplicitBlockArgument:
140
- Enabled: true
141
-
142
- Style/FrozenStringLiteralComment:
143
- Enabled: true
144
- EnforcedStyle: always
145
- Exclude:
146
- - '**/*.md'
147
-
148
- Style/MutableConstant:
149
- Enabled: true
150
- EnforcedStyle: literals
151
- Exclude:
152
- - '**/*.md'
153
-
154
- Style/MapToHash:
155
- Enabled: true
156
-
157
- Style/RedundantFreeze:
158
- Enabled: true
159
-
160
- # Use `foo {}` not `foo{}`.
161
- Layout/SpaceBeforeBlockBraces:
162
- Enabled: true
163
-
164
- # Use `foo { bar }` not `foo {bar}`.
165
- Layout/SpaceInsideBlockBraces:
166
- Enabled: true
167
- EnforcedStyleForEmptyBraces: space
168
-
169
- # Use `{ a: 1 }` not `{a:1}`.
170
- Layout/SpaceInsideHashLiteralBraces:
171
- Enabled: true
172
-
173
- Layout/SpaceInsideParens:
174
- Enabled: true
175
-
176
- # Check quotes usage according to lint rule below.
177
- Style/StringLiterals:
178
- Enabled: true
179
- EnforcedStyle: double_quotes
180
-
181
- # Detect hard tabs, no hard tabs.
182
- Layout/IndentationStyle:
183
- Enabled: true
184
-
185
- # Empty lines should not have any spaces.
186
- Layout/TrailingEmptyLines:
187
- Enabled: true
188
-
189
- # No trailing whitespace.
190
- Layout/TrailingWhitespace:
191
- Enabled: true
192
-
193
- # Use quotes for string literals when they are enough.
194
- Style/RedundantPercentQ:
195
- Enabled: true
196
-
197
- Lint/NestedMethodDefinition:
198
- Enabled: true
199
-
200
- Lint/AmbiguousOperator:
201
- Enabled: true
202
-
203
- Lint/AmbiguousRegexpLiteral:
204
- Enabled: true
205
-
206
- Lint/Debugger:
207
- Enabled: true
208
- DebuggerRequires:
209
- - debug
210
-
211
- Lint/DuplicateRequire:
212
- Enabled: true
213
-
214
- Lint/DuplicateMagicComment:
215
- Enabled: true
216
-
217
- Lint/DuplicateMethods:
218
- Enabled: true
219
-
220
- Lint/ErbNewArguments:
221
- Enabled: true
222
-
223
- Lint/EnsureReturn:
224
- Enabled: true
225
-
226
- Lint/MissingCopEnableDirective:
227
- Enabled: true
228
-
229
- # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
230
- Lint/RequireParentheses:
231
- Enabled: true
232
-
233
- Lint/RedundantCopDisableDirective:
234
- Enabled: true
235
-
236
- Lint/RedundantCopEnableDirective:
237
- Enabled: true
238
-
239
- Lint/RedundantRequireStatement:
240
- Enabled: true
241
-
242
- Lint/RedundantStringCoercion:
243
- Enabled: true
244
-
245
- Lint/RedundantSafeNavigation:
246
- Enabled: true
247
-
248
- Lint/UriEscapeUnescape:
249
- Enabled: true
250
-
251
- Lint/UselessAssignment:
252
- Enabled: true
253
-
254
- Lint/DeprecatedClassMethods:
255
- Enabled: true
256
-
257
- Lint/InterpolationCheck:
258
- Enabled: true
259
- Exclude:
260
- - '**/test/**/*'
261
-
262
- Lint/SafeNavigationChain:
263
- Enabled: true
264
-
265
- Style/EvalWithLocation:
266
- Enabled: true
267
- Exclude:
268
- - '**/test/**/*'
269
-
270
- Style/ParenthesesAroundCondition:
271
- Enabled: true
272
-
273
- Style/HashTransformKeys:
274
- Enabled: true
275
-
276
- Style/HashTransformValues:
277
- Enabled: true
278
-
279
- Style/RedundantBegin:
280
- Enabled: true
281
-
282
- Style/RedundantReturn:
283
- Enabled: true
284
- AllowMultipleReturnValues: true
285
-
286
- Style/RedundantRegexpEscape:
287
- Enabled: true
288
-
289
- Style/Semicolon:
290
- Enabled: true
291
- AllowAsExpressionSeparator: true
292
-
293
- # Prefer Foo.method over Foo::method
294
- Style/ColonMethodCall:
295
- Enabled: true
296
-
297
- Style/TrivialAccessors:
298
- Enabled: true
299
-
300
- # Prefer a = b || c over a = b ? b : c
301
- Style/RedundantCondition:
302
- Enabled: true
303
-
304
- Style/RedundantDoubleSplatHashBraces:
305
- Enabled: true
306
-
307
- Style/OpenStructUse:
308
- Enabled: true
309
-
310
- Style/ArrayIntersect:
311
- Enabled: true
312
-
313
- Style/KeywordArgumentsMerging:
314
- Enabled: true
315
-
316
- Performance/BindCall:
317
- Enabled: true
318
-
319
- Performance/FlatMap:
320
- Enabled: true
321
-
322
- Performance/MapCompact:
323
- Enabled: true
324
-
325
- # Off, unlike Rails: select {}.map {} here is almost always Active Record's
326
- # select -- the query DSL -- and filter_map would break it, which the cop
327
- # cannot tell from Enumerable's.
328
- Performance/SelectMap:
329
- Enabled: false
330
-
331
- Performance/RedundantMerge:
332
- Enabled: true
333
-
334
- Performance/StartWith:
335
- Enabled: true
336
-
337
- Performance/EndWith:
338
- Enabled: true
339
-
340
- Performance/RegexpMatch:
341
- Enabled: true
342
-
343
- Performance/ReverseEach:
344
- Enabled: true
345
-
346
- Performance/StringReplacement:
347
- Enabled: true
348
-
349
- Performance/DeletePrefix:
350
- Enabled: true
351
-
352
- Performance/DeleteSuffix:
353
- Enabled: true
354
-
355
- Performance/InefficientHashSearch:
356
- Enabled: true
357
-
358
- Performance/ConstantRegexp:
359
- Enabled: true
360
-
361
- Performance/RedundantStringChars:
362
- Enabled: true
363
-
364
- Performance/StringInclude:
365
- Enabled: true
366
-
367
- Minitest/AssertNil:
368
- Enabled: true
369
-
370
- Minitest/AssertRaisesWithRegexpArgument:
371
- Enabled: true
372
-
373
- Minitest/AssertWithExpectedArgument:
374
- Enabled: true
375
-
376
- Minitest/LiteralAsActualArgument:
377
- Enabled: true
378
-
379
- Minitest/NonExecutableTestMethod:
380
- Enabled: true
381
-
382
- Minitest/SkipEnsure:
383
- Enabled: true
384
-
385
- Minitest/UnreachableAssertion:
386
- Enabled: true
387
-
388
- Markdown:
389
- # Off, unlike Rails: README abbreviates a snippet to its shape --
390
- # with_recursive(tree: [...]) -- which is not Ruby and means not to be.
391
- WarnInvalid: false
392
- # Whether to lint codeblocks without code attributes
393
- Autodetect: false
data/Gemfile DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- # Specify your gem's dependencies in activerecord-refined.gemspec
6
- gemspec
7
-
8
- # Only ADAPTER=postgresql and ADAPTER=mysql2 need these, and building them
9
- # needs the client libraries installed. `rake test` runs on SQLite, so a
10
- # checkout without them is still usable: bundle config set --local without db
11
- group :db do
12
- gem "mysql2"
13
- gem "pg"
14
- end
data/Rakefile DELETED
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
5
- require "socket"
6
-
7
- ADAPTERS = %w[sqlite3 postgresql mysql2].freeze
8
-
9
- # The devcontainer serves Oracle's MySQL beside MariaDB, on 3307. A
10
- # container from before it existed serves nothing there, and test:all says
11
- # so rather than failing or keeping quiet.
12
- def mysql8_reachable?
13
- Socket.tcp("127.0.0.1", 3307, connect_timeout: 1) { true }
14
- rescue SystemCallError
15
- false
16
- end
17
-
18
- Rake::TestTask.new do |t|
19
- t.test_files = FileList["test/test_*.rb"]
20
- end
21
-
22
- namespace :test do
23
- ADAPTERS.each do |adapter|
24
- desc "Run the tests against #{adapter}"
25
- task adapter do
26
- puts "==== #{adapter} ===="
27
- ENV["ADAPTER"] = adapter
28
- ENV.delete("DB_PORT")
29
- Rake::Task[:test].reenable
30
- Rake::Task[:test].invoke
31
- end
32
- end
33
-
34
- desc "Run the tests against MySQL, which the devcontainer serves on 3307"
35
- task :mysql8 do
36
- puts "==== mysql2 (MySQL, port 3307) ===="
37
- ENV["ADAPTER"] = "mysql2"
38
- ENV["DB_PORT"] = "3307"
39
- Rake::Task[:test].reenable
40
- Rake::Task[:test].invoke
41
- end
42
-
43
- desc "Run the tests against every adapter in turn"
44
- task all: ADAPTERS do
45
- if mysql8_reachable?
46
- Rake::Task["test:mysql8"].invoke
47
- else
48
- puts "MySQL is not listening on 3307; skipped"
49
- end
50
- end
51
- end
52
-
53
- task default: :test
@@ -1,129 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Compares the cost of building the same queries through this gem's block
4
- # DSL and through Active Record's other argument styles: hash conditions,
5
- # string conditions, raw Arel, and relation and/or chains. Only query
6
- # construction (through to_sql) is measured; every style produces the same
7
- # SQL, which the script prints first as a sanity check.
8
- #
9
- # The profiling gems are development dependencies, so:
10
- #
11
- # bundle exec ruby benchmark/query_building.rb
12
-
13
- require "benchmark/ips"
14
- require "memory_profiler"
15
- require "active_record"
16
- require "activerecord-refined"
17
-
18
- ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
19
- ActiveRecord::Schema.verbose = false
20
- ActiveRecord::Schema.define do
21
- create_table(:users) { |t| t.string :name; t.integer :age }
22
- end
23
-
24
- class User < ActiveRecord::Base; end
25
-
26
- T = User.arel_table
27
-
28
- # Each variant must generate equivalent SQL; sanity-print once.
29
- VARIANTS = {
30
- "simple equality" => {
31
- "hash" => -> { User.where(name: "alice").to_sql },
32
- "string" => -> { User.where("name = ?", "alice").to_sql },
33
- "arel" => -> { User.where(T[:name].eq("alice")).to_sql },
34
- "block" => -> { User.where { :name == "alice" }.to_sql },
35
- },
36
- "range (BETWEEN)" => {
37
- "hash" => -> { User.where(age: 20..40).to_sql },
38
- "arel" => -> { User.where(T[:age].between(20..40)).to_sql },
39
- "block" => -> { User.where { :age.in?(20..40) }.to_sql },
40
- },
41
- "LIKE" => {
42
- "string" => -> { User.where("name LIKE ?", "ma%").to_sql },
43
- "arel" => -> { User.where(T[:name].matches("ma%", nil, true)).to_sql },
44
- "block" => -> { User.where { :name.like?("al%") }.to_sql },
45
- },
46
- "compound AND/OR" => {
47
- "string" => -> { User.where("age >= ? AND (name = ? OR name = ?)", 18, "alice", "bob").to_sql },
48
- "arel" => -> { User.where(T[:age].gteq(18).and(T[:name].eq("alice").or(T[:name].eq("bob")))).to_sql },
49
- "relation" => -> { User.where(age: 18..).and(User.where(name: "alice").or(User.where(name: "bob"))).to_sql },
50
- "block" => -> { User.where { (:age >= 18) & ((:name == "alice") | (:name == "bob")) }.to_sql },
51
- },
52
- }.freeze
53
-
54
- puts "=== generated SQL (sanity) ==="
55
- VARIANTS.each do |group, variants|
56
- puts "--- #{group} ---"
57
- variants.each { |name, thunk| puts " #{name.ljust(8)} #{thunk.call}" }
58
- end
59
-
60
- puts
61
- puts "=== speed (queries built per second) ==="
62
- VARIANTS.each do |group, variants|
63
- puts "--- #{group} ---"
64
- Benchmark.ips do |x|
65
- x.config(warmup: 0.5, time: 2)
66
- variants.each { |name, thunk| x.report(name, &thunk) }
67
- x.compare!
68
- end
69
- end
70
-
71
- puts
72
- puts "=== memory (per single call) ==="
73
- fmt = "%-18s %-9s %12s %12s"
74
- puts format(fmt, "group", "variant", "allocated B", "objects")
75
- VARIANTS.each do |group, variants|
76
- variants.each do |name, thunk|
77
- thunk.call # warm caches (schema, statement caches) outside the report
78
- report = MemoryProfiler.report { thunk.call }
79
- puts format(fmt, group, name, report.total_allocated_memsize, report.total_allocated)
80
- end
81
- end
82
-
83
- puts
84
- puts "=== Proc#refined ISeq copy (memory) ==="
85
- require "objspace"
86
-
87
- # Proc#refined runs the block under the refinements by deep-copying its
88
- # instruction sequence, nested blocks included. The copy is made lazily on
89
- # the refined proc's first call and memoized per source iseq and refinement
90
- # list for the life of the VM, so it is paid once per block call site, not
91
- # per query.
92
- iseq_count = -> {
93
- counts = ObjectSpace.count_imemo_objects
94
- counts[:imemo_iseq] || counts[:iseq]
95
- }
96
- context = ActiveRecord::Refined::BlockContext.new(User)
97
- syntax = ActiveRecord::Refined::BlockSyntax
98
-
99
- {
100
- "simple equality" => proc { :name == "alice" },
101
- "compound AND/OR" => proc { (:age >= 18) & ((:name == "alice") | (:name == "bob")) },
102
- }.each do |label, blk|
103
- refined = blk.refined(syntax)
104
- context.instance_exec(&refined) # the copy is made here, on the first call
105
- original = ObjectSpace.memsize_of(RubyVM::InstructionSequence.of(blk))
106
- copy = ObjectSpace.memsize_of(RubyVM::InstructionSequence.of(refined))
107
- puts "#{label}: original iseq #{original} B, refined copy #{copy} B"
108
- end
109
-
110
- make_proc = -> { proc { :age > 20 } }
111
- context.instance_exec(&make_proc.call.refined(syntax))
112
- before = iseq_count.call
113
- 1000.times { context.instance_exec(&make_proc.call.refined(syntax)) }
114
- puts "1000 more calls from the same call site copied #{iseq_count.call - before} iseqs"
115
-
116
- puts
117
- puts "=== where the block path spends its time ==="
118
- block = proc { :name == "alice" }
119
- refined_block = block.refined(ActiveRecord::Refined::BlockSyntax)
120
- context = ActiveRecord::Refined::BlockContext.new(User)
121
- Benchmark.ips do |x|
122
- x.config(warmup: 0.5, time: 2)
123
- x.report("Proc#refined alone") { block.refined(ActiveRecord::Refined::BlockSyntax) }
124
- x.report("instance_exec of pre-refined proc") { context.instance_exec(&refined_block) }
125
- x.report("refined + instance_exec") {
126
- ActiveRecord::Refined::BlockContext.new(User).instance_exec(&block.refined(ActiveRecord::Refined::BlockSyntax))
127
- }
128
- x.compare!
129
- end