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 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+ RuboCop::RakeTask.new
10
+
11
+ task default: %i[spec rubocop]
@@ -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,8 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module WithOrdinality
6
+ VERSION = '0.1.0'
7
+ end
8
+ 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: []