active_record-with_ordinality 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/MIT-LICENSE +20 -0
- data/README.md +116 -0
- data/Rakefile +11 -0
- data/lib/active_record/with_ordinality/ordinality_table.rb +160 -0
- data/lib/active_record/with_ordinality/relation_extension.rb +59 -0
- data/lib/active_record/with_ordinality/version.rb +8 -0
- data/lib/active_record/with_ordinality.rb +61 -0
- data/lib/tapioca/dsl/compilers/active_record_with_ordinality.rb +63 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f21ab71daeca46389e1fc3cc0ace3dad48ef691a4bb91d4735573305d2bbe0de
|
|
4
|
+
data.tar.gz: 27841e8dfde95dd646a72d942a5be56668be1bf40c3d6202be4cbfb94bb1621c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6d8a9745c4127e6d2e1d700bb091dca36da0765ea5d4002f9c513337633c3d02341447759e9788042c0c2143d62968204b7e3bfa7db8faf28a54739549e236c6
|
|
7
|
+
data.tar.gz: 7dd838bff8c2e5b5c0ca966780197af58a68da6d8ec4a9b346959c6037f9519adac2bee85d076f060b936ae975b45c3e0113f35e609dd60757ed483a05a3c794
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 Minty Fresh
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# ActiveRecord::WithOrdinality
|
|
2
|
+
|
|
3
|
+
Joins lists of values to ActiveRecord relations with their ordinal positions,
|
|
4
|
+
and orders relations by them with `in_order_of`.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'active_record-with_ordinality', github: 'mintyfresh/active_record-with_ordinality'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
$ bundle
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then extend your models with `ActiveRecord::WithOrdinality` (typically in `ApplicationRecord`, so all models get it):
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
24
|
+
primary_abstract_class
|
|
25
|
+
|
|
26
|
+
extend ActiveRecord::WithOrdinality
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### with_ordinality
|
|
33
|
+
|
|
34
|
+
`with_ordinality` joins a list of values to the records with matching columns,
|
|
35
|
+
along with the position of each value (starting from 1):
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
User.with_ordinality(:id, [3, 1, 2])
|
|
39
|
+
# SELECT "users".* FROM "users"
|
|
40
|
+
# INNER JOIN (
|
|
41
|
+
# SELECT (element.value->>0)::bigint AS "id", element.ordinality AS "ordinal"
|
|
42
|
+
# FROM json_array_elements('[[3],[1],[2]]'::json) WITH ORDINALITY AS element(value, ordinality)
|
|
43
|
+
# ) AS "users_id_ordinals" ON "users"."id" = "users_id_ordinals"."id"
|
|
44
|
+
# WHERE "users"."id" IN (3, 1, 2)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The ordinal is selected onto the records when it's given a name with `as`:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
User.with_ordinality(:id, [3, 1, 2], as: :position).map { |user| [user.id, user.position] }
|
|
51
|
+
# => [[1, 2], [2, 3], [3, 1]]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Otherwise, it can be used in the rest of the query by the name of the joined table
|
|
55
|
+
(which defaults to the table and columns, eg. `users_id_ordinals`, or can be given with `join_table_name`):
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
User.with_ordinality(:id, [3, 1, 2]).order('users_id_ordinals.ordinal')
|
|
59
|
+
User.with_ordinality(:id, [3, 1, 2], join_table_name: :positions).order('positions.ordinal')
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Only records matching the values are kept, unless `filter` is false (in which case other records have no ordinal):
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
User.with_ordinality(:id, [3, 1], as: :position, filter: false)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Repeated values are joined once, at their first position, so that records aren't repeated.
|
|
69
|
+
Values are converted by the types of the attributes, so eg. enums can be given by name.
|
|
70
|
+
|
|
71
|
+
### Composite columns
|
|
72
|
+
|
|
73
|
+
Several columns can be given, with a list of values for each record:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
User.with_ordinality(%i[tenant_id id], [[1, 3], [1, 1], [1, 2], [2, 4]], as: :position)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### in_order_of
|
|
80
|
+
|
|
81
|
+
`in_order_of` is overridden to order records by their ordinals, rather than by a `CASE` of every value,
|
|
82
|
+
and supports composite columns:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
User.in_order_of(:id, [3, 1, 2])
|
|
86
|
+
User.in_order_of(%i[tenant_id id], [[1, 3], [1, 1], [1, 2], [2, 4]])
|
|
87
|
+
User.in_order_of(:id, [3, 1, 2], filter: false) # other records are ordered last
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Columns that aren't attributes of the model (eg. `Arel.sql('LOWER(name)')`) are ordered by Rails' `in_order_of`.
|
|
91
|
+
|
|
92
|
+
### Databases
|
|
93
|
+
|
|
94
|
+
PostgreSQL (`json_array_elements` with `WITH ORDINALITY`), MySQL (`JSON_TABLE` with `FOR ORDINALITY`),
|
|
95
|
+
and SQLite (`json_each`) are supported.
|
|
96
|
+
|
|
97
|
+
### Sorbet
|
|
98
|
+
|
|
99
|
+
A Tapioca DSL compiler is included, which adds `with_ordinality` to the relations generated for models
|
|
100
|
+
extended by `ActiveRecord::WithOrdinality`. It's loaded automatically by `bin/tapioca dsl`.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
The specs run against a dummy Rails application in `spec/dummy`, using SQLite by default:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
$ bin/rspec
|
|
108
|
+
$ bin/rubocop
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
PostgreSQL and MySQL are selected with the `DATABASE_ADAPTER` and `DATABASE_URL` environment variables
|
|
112
|
+
(see `.github/workflows/ci.yml`).
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module WithOrdinality
|
|
6
|
+
# A table of values (decoded from JSON by the database) numbered by their position, with the SQL to join it:
|
|
7
|
+
# * PostgreSQL - `json_array_elements` with `WITH ORDINALITY`
|
|
8
|
+
# * MySQL - `JSON_TABLE` with a `FOR ORDINALITY` column
|
|
9
|
+
# * SQLite - `json_each`, numbered by array index
|
|
10
|
+
#
|
|
11
|
+
# The table has a column for each of the model's columns (of the same name and type), and an `ordinal` column.
|
|
12
|
+
class OrdinalityTable
|
|
13
|
+
ORDINAL_COLUMN_NAME = 'ordinal'
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Checks if columns are given as names of the model's attributes (rather than eg. SQL expressions).
|
|
17
|
+
#
|
|
18
|
+
#: (singleton(ActiveRecord::Base) model, untyped column_names) -> bool
|
|
19
|
+
def attributes?(model, column_names)
|
|
20
|
+
names = Array(column_names)
|
|
21
|
+
|
|
22
|
+
names.any? && names.all? do |name|
|
|
23
|
+
(name.is_a?(Symbol) || name.is_a?(String)) && model.columns_hash.key?(column_name(model, name))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Resolves aliased attributes to their columns.
|
|
28
|
+
#
|
|
29
|
+
#: (singleton(ActiveRecord::Base) model, Symbol | String name) -> String
|
|
30
|
+
def column_name(model, name)
|
|
31
|
+
model.attribute_aliases.fetch(name.to_s, name.to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#: String
|
|
36
|
+
attr_reader :name
|
|
37
|
+
|
|
38
|
+
# The rows of values, without repeats (which would join each record more than once).
|
|
39
|
+
#
|
|
40
|
+
#: Array[Array[untyped]]
|
|
41
|
+
attr_reader :values
|
|
42
|
+
|
|
43
|
+
#: (
|
|
44
|
+
#| singleton(ActiveRecord::Base) model,
|
|
45
|
+
#| Symbol | String | Array[Symbol | String] column_names,
|
|
46
|
+
#| Enumerable[untyped] values,
|
|
47
|
+
#| ?(Symbol | String)? name
|
|
48
|
+
#| ) -> void
|
|
49
|
+
def initialize(model, column_names, values, name = nil)
|
|
50
|
+
@model = model
|
|
51
|
+
@column_names = Array(column_names).map { |column_name| resolve_column_name(column_name) }
|
|
52
|
+
@values = values.map { |value| column_names.is_a?(Array) ? row(value) : [value] }.uniq
|
|
53
|
+
@name = (name || "#{model.table_name}_#{@column_names.join('_')}_ordinals").to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Matches the records to the values, as a condition of the model's table (so that its indexes may be used).
|
|
57
|
+
#
|
|
58
|
+
#: -> Hash[String | Array[String], untyped]
|
|
59
|
+
def condition
|
|
60
|
+
@column_names.one? ? { @column_names.first => @values.map(&:first) } : { @column_names => @values }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#: -> Arel::Attributes::Attribute
|
|
64
|
+
def ordinal
|
|
65
|
+
Arel::Table.new(name)[ORDINAL_COLUMN_NAME]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Joins the table to the model's table by the columns.
|
|
69
|
+
#
|
|
70
|
+
#: (String join_type) -> String
|
|
71
|
+
def join_sql(join_type)
|
|
72
|
+
@model.with_connection do |connection|
|
|
73
|
+
conditions = @column_names.map do |column_name|
|
|
74
|
+
"#{@model.quoted_table_name}.#{connection.quote_column_name(column_name)} = " \
|
|
75
|
+
"#{connection.quote_table_name(name)}.#{connection.quote_column_name(column_name)}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@model.sanitize_sql_array(
|
|
79
|
+
["#{join_type} #{source_sql(connection)} ON #{conditions.join(' AND ')}", ActiveSupport::JSON.encode(rows)]
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
#: (Symbol | String column_name) -> String
|
|
87
|
+
def resolve_column_name(column_name)
|
|
88
|
+
resolved = self.class.column_name(@model, column_name)
|
|
89
|
+
return resolved if @model.columns_hash.key?(resolved)
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, "`#{column_name}` is not a column of `#{@model.name}`."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#: (untyped value) -> Array[untyped]
|
|
95
|
+
def row(value)
|
|
96
|
+
row = Array(value)
|
|
97
|
+
return row if row.size == @column_names.size
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, "Expected #{@column_names.size} values for the columns #{@column_names.inspect}, " \
|
|
100
|
+
"got #{value.inspect}."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The values as the database stores them (eg. the integers of enums).
|
|
104
|
+
#
|
|
105
|
+
#: -> Array[Array[untyped]]
|
|
106
|
+
def rows
|
|
107
|
+
types = @column_names.map { |column_name| @model.type_for_attribute(column_name) }
|
|
108
|
+
|
|
109
|
+
@values.map { |row| row.zip(types).map { |value, type| type.serialize(value) } }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
#: (ActiveRecord::ConnectionAdapters::AbstractAdapter connection) -> String
|
|
113
|
+
def source_sql(connection)
|
|
114
|
+
case connection.adapter_name
|
|
115
|
+
when /postgres/i then postgresql_source_sql(connection)
|
|
116
|
+
when /mysql|trilogy/i then mysql_source_sql(connection)
|
|
117
|
+
when /sqlite/i then sqlite_source_sql(connection)
|
|
118
|
+
else
|
|
119
|
+
raise NotImplementedError, "with_ordinality isn't supported by the #{connection.adapter_name} adapter."
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#: (ActiveRecord::ConnectionAdapters::AbstractAdapter connection) -> String
|
|
124
|
+
def postgresql_source_sql(connection)
|
|
125
|
+
columns = @column_names.each_with_index.map do |column_name, index|
|
|
126
|
+
"(element.value->>#{index})::#{@model.columns_hash[column_name].sql_type} " \
|
|
127
|
+
"AS #{connection.quote_column_name(column_name)}"
|
|
128
|
+
end
|
|
129
|
+
columns << "element.ordinality AS #{connection.quote_column_name(ORDINAL_COLUMN_NAME)}"
|
|
130
|
+
|
|
131
|
+
elements = 'json_array_elements(?::json) WITH ORDINALITY AS element(value, ordinality)'
|
|
132
|
+
|
|
133
|
+
"(SELECT #{columns.join(', ')} FROM #{elements}) AS #{connection.quote_table_name(name)}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
#: (ActiveRecord::ConnectionAdapters::AbstractAdapter connection) -> String
|
|
137
|
+
def mysql_source_sql(connection)
|
|
138
|
+
columns = ["#{connection.quote_column_name(ORDINAL_COLUMN_NAME)} FOR ORDINALITY"]
|
|
139
|
+
columns.concat(
|
|
140
|
+
@column_names.each_with_index.map do |column_name, index|
|
|
141
|
+
"#{connection.quote_column_name(column_name)} #{@model.columns_hash[column_name].sql_type} " \
|
|
142
|
+
"PATH '$[#{index}]'"
|
|
143
|
+
end
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
"JSON_TABLE(?, '$[*]' COLUMNS(#{columns.join(', ')})) AS #{connection.quote_table_name(name)}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
#: (ActiveRecord::ConnectionAdapters::AbstractAdapter connection) -> String
|
|
150
|
+
def sqlite_source_sql(connection)
|
|
151
|
+
columns = @column_names.each_with_index.map do |column_name, index|
|
|
152
|
+
"json_extract(value, '$[#{index}]') AS #{connection.quote_column_name(column_name)}"
|
|
153
|
+
end
|
|
154
|
+
columns << "key + 1 AS #{connection.quote_column_name(ORDINAL_COLUMN_NAME)}"
|
|
155
|
+
|
|
156
|
+
"(SELECT #{columns.join(', ')} FROM json_each(?)) AS #{connection.quote_table_name(name)}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module WithOrdinality
|
|
6
|
+
module RelationExtension
|
|
7
|
+
# Joins a list of values to the records with matching columns, along with the ordinal position of each value
|
|
8
|
+
# (starting from 1). Composite columns are matched by lists of values:
|
|
9
|
+
#
|
|
10
|
+
# User.with_ordinality(:id, [3, 1, 2])
|
|
11
|
+
# User.with_ordinality(%i[tenant_id id], [[1, 3], [1, 1], [2, 4]])
|
|
12
|
+
#
|
|
13
|
+
# Only records matching the values are kept, unless `filter` is false (in which case others have no ordinal).
|
|
14
|
+
# The ordinal is selected onto the records as the attribute named by `as`, if given.
|
|
15
|
+
# Otherwise it can be referred to by the joined table (eg. `users_id_ordinals.ordinal`).
|
|
16
|
+
#
|
|
17
|
+
# Repeated values are joined once, at their first position.
|
|
18
|
+
#
|
|
19
|
+
#: (
|
|
20
|
+
#| Symbol | String | Array[Symbol | String] column_names,
|
|
21
|
+
#| Enumerable[untyped] values,
|
|
22
|
+
#| ?as: (Symbol | String)?,
|
|
23
|
+
#| ?filter: bool,
|
|
24
|
+
#| ?join_table_name: (Symbol | String)?
|
|
25
|
+
#| ) -> ActiveRecord::Relation
|
|
26
|
+
def with_ordinality(column_names, values, as: nil, filter: true, join_table_name: nil)
|
|
27
|
+
ordinality = OrdinalityTable.new(model, column_names, values, join_table_name)
|
|
28
|
+
return none if filter && ordinality.values.empty?
|
|
29
|
+
|
|
30
|
+
relation = filter ? where(ordinality.condition) : spawn
|
|
31
|
+
relation = relation.joins(ordinality.join_sql(filter ? 'INNER JOIN' : 'LEFT OUTER JOIN'))
|
|
32
|
+
return relation unless as
|
|
33
|
+
|
|
34
|
+
relation = relation.select(table[Arel.star]) if relation.select_values.empty?
|
|
35
|
+
relation.select(ordinality.ordinal.as(model.adapter_class.quote_column_name(as.to_s)))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Orders the records in the order of a list of values, by joining them `with_ordinality`.
|
|
39
|
+
# Composite columns are ordered by lists of values (see `with_ordinality`).
|
|
40
|
+
#
|
|
41
|
+
# Only records matching the values are kept, unless `filter` is false (in which case others are ordered last).
|
|
42
|
+
# Columns that aren't attributes of the model (eg. SQL expressions) are ordered by Rails' `in_order_of` instead.
|
|
43
|
+
#
|
|
44
|
+
#: (untyped column_names, Enumerable[untyped] values, ?filter: bool) -> ActiveRecord::Relation
|
|
45
|
+
def in_order_of(column_names, values, filter: true)
|
|
46
|
+
return super unless OrdinalityTable.attributes?(model, column_names)
|
|
47
|
+
|
|
48
|
+
# Rails' `in_order_of` matches no records for no values, regardless of `filter`
|
|
49
|
+
values = values.to_a
|
|
50
|
+
return none if values.empty?
|
|
51
|
+
|
|
52
|
+
ordinal = OrdinalityTable.new(model, column_names, values).ordinal
|
|
53
|
+
relation = with_ordinality(column_names, values, filter:)
|
|
54
|
+
|
|
55
|
+
filter ? relation.order(ordinal.asc) : relation.order(ordinal.eq(nil), ordinal.asc)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'active_record'
|
|
5
|
+
require 'active_record/with_ordinality/version'
|
|
6
|
+
|
|
7
|
+
module ActiveRecord
|
|
8
|
+
# Adds `with_ordinality` to the relations of a model and its subclasses, which joins a list of values to the records
|
|
9
|
+
# with their ordinal positions, and overrides `in_order_of` to order records by them (rather than by a CASE):
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationRecord < ActiveRecord::Base
|
|
12
|
+
# extend ActiveRecord::WithOrdinality
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# User.with_ordinality(:id, [3, 1, 2], as: :ordinal).map(&:ordinal)
|
|
16
|
+
# User.in_order_of(%i[tenant_id id], [[1, 3], [1, 1], [2, 4]])
|
|
17
|
+
module WithOrdinality
|
|
18
|
+
autoload :OrdinalityTable, 'active_record/with_ordinality/ordinality_table'
|
|
19
|
+
autoload :RelationExtension, 'active_record/with_ordinality/relation_extension'
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# Extends the relations of a model, and of any of its subclasses that were already defined.
|
|
23
|
+
#
|
|
24
|
+
#: (singleton(ActiveRecord::Base) base) -> void
|
|
25
|
+
def extended(base)
|
|
26
|
+
super
|
|
27
|
+
[base, *base.descendants].each(&:install_with_ordinality)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# See `RelationExtension#with_ordinality`.
|
|
32
|
+
#
|
|
33
|
+
#: (*untyped args, **untyped options) -> ActiveRecord::Relation
|
|
34
|
+
def with_ordinality(*args, **options)
|
|
35
|
+
all.with_ordinality(*args, **options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# See `RelationExtension#in_order_of`.
|
|
39
|
+
#
|
|
40
|
+
#: (*untyped args, **untyped options) -> ActiveRecord::Relation
|
|
41
|
+
def in_order_of(*args, **options)
|
|
42
|
+
all.in_order_of(*args, **options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Rails builds the relation classes of each model as it's defined (see `ActiveRecord::Delegation::DelegateCache`),
|
|
46
|
+
# so subclasses defined after the model was extended are installed here.
|
|
47
|
+
#
|
|
48
|
+
#: -> void
|
|
49
|
+
def initialize_relation_delegate_cache
|
|
50
|
+
super
|
|
51
|
+
install_with_ordinality
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#: -> void
|
|
55
|
+
def install_with_ordinality
|
|
56
|
+
ActiveRecord::Delegation.delegated_classes.each do |relation_class|
|
|
57
|
+
relation_delegate_class(relation_class).include(RelationExtension)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(ActiveRecord::Base)
|
|
5
|
+
|
|
6
|
+
require 'tapioca/dsl/compilers/active_record_relations'
|
|
7
|
+
|
|
8
|
+
module Tapioca
|
|
9
|
+
module Dsl
|
|
10
|
+
module Compilers
|
|
11
|
+
# Adds `with_ordinality` to the relations generated by the ActiveRecordRelations compiler,
|
|
12
|
+
# for models extended by ActiveRecord::WithOrdinality.
|
|
13
|
+
# (The `in_order_of` override needs no signature, since the generated one is untyped.)
|
|
14
|
+
# Compilers decorate a constant in order of their names, so this runs after ActiveRecordRelations.
|
|
15
|
+
#
|
|
16
|
+
#: [ConstantType = singleton(::ActiveRecord::Base)]
|
|
17
|
+
class ActiveRecordWithOrdinality < Compiler
|
|
18
|
+
include Helpers::ActiveRecordConstantsHelper
|
|
19
|
+
|
|
20
|
+
# @override
|
|
21
|
+
#: -> void
|
|
22
|
+
def decorate
|
|
23
|
+
return unless compiler_enabled?('ActiveRecordRelations')
|
|
24
|
+
|
|
25
|
+
root.create_path(constant) do |model|
|
|
26
|
+
model.create_module(RelationMethodsModuleName) do |mod|
|
|
27
|
+
create_with_ordinality_method(mod, RelationClassName)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
model.create_module(AssociationRelationMethodsModuleName) do |mod|
|
|
31
|
+
create_with_ordinality_method(mod, AssociationRelationClassName)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# @override
|
|
38
|
+
#: -> Enumerable[Module[top]]
|
|
39
|
+
def gather_constants
|
|
40
|
+
ActiveRecordRelations.gather_constants.grep(::ActiveRecord::WithOrdinality)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
#: (RBI::Scope scope, String return_type) -> void
|
|
47
|
+
def create_with_ordinality_method(scope, return_type)
|
|
48
|
+
scope.create_method(
|
|
49
|
+
'with_ordinality',
|
|
50
|
+
parameters: [
|
|
51
|
+
create_param('column_names', type: 'T.any(Symbol, String, T::Array[T.any(Symbol, String)])'),
|
|
52
|
+
create_param('values', type: 'T::Enumerable[T.untyped]'),
|
|
53
|
+
create_kw_opt_param('as', type: 'T.nilable(T.any(Symbol, String))', default: 'nil'),
|
|
54
|
+
create_kw_opt_param('filter', type: 'T::Boolean', default: 'true'),
|
|
55
|
+
create_kw_opt_param('join_table_name', type: 'T.nilable(T.any(Symbol, String))', default: 'nil'),
|
|
56
|
+
],
|
|
57
|
+
return_type:
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: active_record-with_ordinality
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Minty Fresh
|
|
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.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
description: Joins lists of values to ActiveRecord relations with their ordinal positions,
|
|
27
|
+
and orders relations by them with in_order_of.
|
|
28
|
+
email:
|
|
29
|
+
- 7896757+mintyfresh@users.noreply.github.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- MIT-LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- lib/active_record/with_ordinality.rb
|
|
38
|
+
- lib/active_record/with_ordinality/ordinality_table.rb
|
|
39
|
+
- lib/active_record/with_ordinality/relation_extension.rb
|
|
40
|
+
- lib/active_record/with_ordinality/version.rb
|
|
41
|
+
- lib/tapioca/dsl/compilers/active_record_with_ordinality.rb
|
|
42
|
+
homepage: https://github.com/mintyfresh/active_record-with_ordinality
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata:
|
|
46
|
+
homepage_uri: https://github.com/mintyfresh/active_record-with_ordinality
|
|
47
|
+
source_code_uri: https://github.com/mintyfresh/active_record-with_ordinality
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
49
|
+
rdoc_options: []
|
|
50
|
+
require_paths:
|
|
51
|
+
- lib
|
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '3.2'
|
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
requirements: []
|
|
63
|
+
rubygems_version: 4.0.20
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Ordinal joins for ActiveRecord relations
|
|
66
|
+
test_files: []
|