activerecord_extras 0.1.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 +7 -0
- data/LICENSE.md +21 -0
- data/README.md +108 -0
- data/lib/active_record/extras/association_scopes.rb +105 -0
- data/lib/active_record/extras/engine.rb +11 -0
- data/lib/active_record/extras/version.rb +6 -0
- data/lib/active_record/extras.rb +7 -0
- data/lib/activerecord_extras.rb +1 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4faa1738c4c2baf22f8e3503ee92b411373ec7d5b99810dd67e7544af47833b2
|
4
|
+
data.tar.gz: 909628842c1d2f26e1017b1415ced32dccf63435c5733b8f99f5c0c08679eee2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 436f270c9237b4f14744ba39f771260104e23c6d562b5739db01d30b77d3b3bd4f53566fef6919813ccf605be6f4ff1047934dc6d18c6a7db2b98f99a2770a61
|
7
|
+
data.tar.gz: c576cdb811f946cda9cc581edb3bce96d9cbd7f224f7f77eaa06a75f552968ea2df8f50790a35d324df1c03b153f30430182d753d0e7d3e9a05bb7358c99d1e3
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Till Schulte-Coerne
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# ActiveRecord Extras
|
2
|
+
|
3
|
+
**ActiveRecord Extras** provides helpful utility methods and query extensions for ActiveRecord models.
|
4
|
+
It focuses on building SQL subqueries for `has_many` associations using `EXISTS` and `COUNT`, while keeping full composability within ActiveRecord queries.
|
5
|
+
|
6
|
+
---
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- `exists_association` and `count_association` methods for Arel-based subqueries
|
11
|
+
- Scopes:
|
12
|
+
- `with_existing(:association)`
|
13
|
+
- `without_existing(:association)`
|
14
|
+
- Extended select support:
|
15
|
+
- `with_counts(:association)` — includes counts in result sets
|
16
|
+
- Accepts optional block for filtering join conditions
|
17
|
+
- Fully composable ActiveRecord relations
|
18
|
+
- No monkey-patching, Rails 6+ compatible
|
19
|
+
|
20
|
+
---
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add to your `Gemfile`:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem "activerecord_extras"
|
28
|
+
```
|
29
|
+
|
30
|
+
Then run:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
bundle install
|
34
|
+
```
|
35
|
+
|
36
|
+
Or install manually:
|
37
|
+
|
38
|
+
```bash
|
39
|
+
gem install activerecord_extras
|
40
|
+
```
|
41
|
+
|
42
|
+
---
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
### `with_existing` and `without_existing`
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
User.with_existing(:posts)
|
50
|
+
User.without_existing(:comments)
|
51
|
+
```
|
52
|
+
|
53
|
+
### `with_counts`
|
54
|
+
|
55
|
+
Adds a `SELECT COUNT(*)` subquery per association:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
User.with_counts(:posts)
|
59
|
+
```
|
60
|
+
|
61
|
+
Supports filtering via block:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
User.with_counts(:posts) do |join_condition, posts|
|
65
|
+
join_condition.and(posts[:published].eq(true))
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
This produces SQL like:
|
70
|
+
|
71
|
+
```sql
|
72
|
+
SELECT users.*, (
|
73
|
+
SELECT COUNT(*) FROM posts
|
74
|
+
WHERE posts.user_id = users.id AND posts.published = TRUE
|
75
|
+
) AS posts_count
|
76
|
+
```
|
77
|
+
|
78
|
+
---
|
79
|
+
|
80
|
+
## API Reference
|
81
|
+
|
82
|
+
### `exists_association(association_name, &block)`
|
83
|
+
|
84
|
+
Returns an Arel EXISTS subquery.
|
85
|
+
|
86
|
+
### `count_association(association_name, &block)`
|
87
|
+
|
88
|
+
Returns a grouped COUNT(*) Arel subquery.
|
89
|
+
|
90
|
+
### `with_counts(*association_names, &block)`
|
91
|
+
|
92
|
+
Selects all columns from the model and adds one COUNT subquery per named association. The optional block modifies join conditions.
|
93
|
+
|
94
|
+
---
|
95
|
+
|
96
|
+
## Development
|
97
|
+
|
98
|
+
```bash
|
99
|
+
bundle exec rake test
|
100
|
+
```
|
101
|
+
|
102
|
+
Tests use SQLite in-memory schema and require no database setup.
|
103
|
+
|
104
|
+
---
|
105
|
+
|
106
|
+
## License
|
107
|
+
|
108
|
+
MIT License © [Till Schulte-Coerne](https://github.com/tillsc)
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Extras
|
3
|
+
module AssociationScopes
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
# Builds a subquery for a has_many association.
|
8
|
+
# Can be used to generate EXISTS or COUNT subqueries.
|
9
|
+
#
|
10
|
+
# @param association_name [Symbol, String] the name of the has_many association
|
11
|
+
# @param mode [:exists, :count] determines the kind of subquery
|
12
|
+
# @yield [join_conditions, target_table] optional block to modify the join conditions
|
13
|
+
# @return [Arel::Nodes::SqlLiteral] the subquery as an Arel node
|
14
|
+
def association_subquery(association_name, mode: :exists, &extra_conditions_block)
|
15
|
+
reflection = reflections[association_name.to_s]
|
16
|
+
|
17
|
+
unless reflection
|
18
|
+
raise ArgumentError, "Unknown association: #{association_name}"
|
19
|
+
end
|
20
|
+
|
21
|
+
unless reflection.macro == :has_many
|
22
|
+
raise ArgumentError, "Only has_many associations are supported (got #{reflection.macro})"
|
23
|
+
end
|
24
|
+
|
25
|
+
foreign_keys = Array(reflection.foreign_key)
|
26
|
+
primary_keys = Array(reflection.active_record_primary_key)
|
27
|
+
|
28
|
+
unless foreign_keys.size == primary_keys.size
|
29
|
+
raise ArgumentError, "Mismatch in key counts: #{foreign_keys} vs #{primary_keys}"
|
30
|
+
end
|
31
|
+
|
32
|
+
source_table = arel_table
|
33
|
+
target_table = reflection.klass.arel_table
|
34
|
+
|
35
|
+
join_conditions = foreign_keys.zip(primary_keys).map do |fk, pk|
|
36
|
+
target_table[fk].eq(source_table[pk])
|
37
|
+
end.reduce(&:and)
|
38
|
+
|
39
|
+
# Allow caller to modify the join condition via block
|
40
|
+
if extra_conditions_block
|
41
|
+
new_conditions = extra_conditions_block.call(join_conditions, target_table)
|
42
|
+
join_conditions = new_conditions if new_conditions
|
43
|
+
end
|
44
|
+
|
45
|
+
case mode
|
46
|
+
when :exists
|
47
|
+
target_table.project(Arel.sql("1")).where(join_conditions).exists
|
48
|
+
when :count
|
49
|
+
Arel::Nodes::Grouping.new(
|
50
|
+
target_table.project(Arel.star.count).where(join_conditions)
|
51
|
+
)
|
52
|
+
else
|
53
|
+
raise ArgumentError, "Unknown mode: #{mode.inspect} (expected :exists or :count)"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Builds an EXISTS subquery for the given association.
|
58
|
+
# Shorthand for association_subquery(..., mode: :exists)
|
59
|
+
def exists_association(association_name, &block)
|
60
|
+
association_subquery(association_name, mode: :exists, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Builds a COUNT subquery for the given association.
|
64
|
+
# Shorthand for association_subquery(..., mode: :count)
|
65
|
+
def count_association(association_name, &block)
|
66
|
+
association_subquery(association_name, mode: :count, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Adds a WHERE EXISTS (...) clause for the given association.
|
70
|
+
#
|
71
|
+
# Example:
|
72
|
+
# User.with_existing(:posts)
|
73
|
+
def with_existing(association_name, &block)
|
74
|
+
where(exists_association(association_name, &block))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Adds a WHERE NOT EXISTS (...) clause for the given association.
|
78
|
+
#
|
79
|
+
# Example:
|
80
|
+
# User.without_existing(:comments)
|
81
|
+
def without_existing (association_name, &block)
|
82
|
+
where.not(exists_association(association_name, &block))
|
83
|
+
end
|
84
|
+
|
85
|
+
# Selects all columns plus COUNT subqueries for the given associations.
|
86
|
+
#
|
87
|
+
# Example:
|
88
|
+
# User.with_counts(:posts, :comments)
|
89
|
+
#
|
90
|
+
# Produces:
|
91
|
+
# SELECT users.*, (SELECT COUNT(*) FROM posts WHERE ...) AS posts_count,
|
92
|
+
# (SELECT COUNT(*) FROM comments WHERE ...) AS comments_count
|
93
|
+
def with_counts (*association_names, &block)
|
94
|
+
selections = [arel_table[Arel.star]] +
|
95
|
+
association_names.map do |name|
|
96
|
+
count_association(name, &block).as("#{name}_count")
|
97
|
+
end
|
98
|
+
|
99
|
+
select(*selections)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Extras
|
3
|
+
class Engine < ::Rails::Engine
|
4
|
+
initializer "active_record.extras.active_record_integration" do
|
5
|
+
ActiveSupport.on_load(:active_record) do
|
6
|
+
include ActiveRecord::Extras::AssociationScopes
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_record/extras'
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_extras
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Till Schulte-Coerne
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activerecord
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '8.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '8.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activesupport
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '8.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '8.0'
|
40
|
+
description: activerecord_extras provides utility methods and scopes for ActiveRecord
|
41
|
+
models, including EXISTS and COUNT subqueries on has_many associations, and SQL
|
42
|
+
helpers for boolean casting.
|
43
|
+
email:
|
44
|
+
- ruby@trsnet.de
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- LICENSE.md
|
50
|
+
- README.md
|
51
|
+
- lib/active_record/extras.rb
|
52
|
+
- lib/active_record/extras/association_scopes.rb
|
53
|
+
- lib/active_record/extras/engine.rb
|
54
|
+
- lib/active_record/extras/version.rb
|
55
|
+
- lib/activerecord_extras.rb
|
56
|
+
homepage: https://github.com/tillsc/activerecord_extras
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata:
|
60
|
+
homepage_uri: https://github.com/tillsc/activerecord_extras
|
61
|
+
source_code_uri: https://github.com/tillsc/activerecord_extras
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.7'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.6.7
|
77
|
+
specification_version: 4
|
78
|
+
summary: Extensions and utilities for ActiveRecord, including association subqueries
|
79
|
+
and convenience scopes.
|
80
|
+
test_files: []
|