active_record-json_associations 0.13.1 → 1.0.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 +4 -4
- data/.github/workflows/ci.yml +46 -10
- data/Appraisals +6 -8
- data/CLAUDE.md +38 -0
- data/README.md +9 -6
- data/active_record-json_associations.gemspec +3 -0
- data/gemfiles/{rails_7.1.gemfile → rails_8.0.gemfile} +1 -1
- data/gemfiles/{rails_7.0.gemfile → rails_8.1.gemfile} +1 -2
- data/lib/active_record/json_associations/version.rb +1 -1
- data/lib/active_record/json_associations.rb +61 -44
- data/spec/belongs_to_many_spec.rb +6 -7
- data/spec/has_many_spec.rb +14 -6
- data/spec/spec_helper.rb +8 -0
- metadata +37 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0425a5d0c7935d72007daa4961178471c58a88dac30522a9137e89923f158dbd
|
|
4
|
+
data.tar.gz: cfbc21cae4d8655ad0c62c2a73c95715d6207f882e3de6433dc079abc3eff9d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15b8f1ea8aebceea06a143635cfb8242fec793f319196d1af11e90a5dbc77bbc68e43d08ff6e9c893d94f79180bb9d1f11db2bf8c4a6f3048e2c870a9f70cffc
|
|
7
|
+
data.tar.gz: 2862a70dfb5c0db59dbf30facb73f0c5d810e7c4c00404c5c5eb74daf7aa6c1cacdca86560db5727b1c7e6537f3d0d3bdea2515837c4af0ee3a18b4a5b680fb4
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -1,21 +1,57 @@
|
|
|
1
1
|
name: CI
|
|
2
|
+
|
|
2
3
|
on: [push, pull_request]
|
|
4
|
+
|
|
3
5
|
jobs:
|
|
4
6
|
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
5
8
|
strategy:
|
|
6
9
|
fail-fast: false
|
|
7
10
|
matrix:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
ruby: ["3.2", "3.3", "3.4", "4.0"]
|
|
12
|
+
gemfile:
|
|
13
|
+
- gemfiles/rails_7.2.gemfile
|
|
14
|
+
- gemfiles/rails_8.0.gemfile
|
|
15
|
+
- gemfiles/rails_8.1.gemfile
|
|
16
|
+
database: [sqlite, postgres, mysql]
|
|
17
|
+
services:
|
|
18
|
+
postgres:
|
|
19
|
+
image: postgres:16
|
|
20
|
+
env:
|
|
21
|
+
POSTGRES_PASSWORD: postgres
|
|
22
|
+
options: >-
|
|
23
|
+
--health-cmd pg_isready
|
|
24
|
+
--health-interval 10s
|
|
25
|
+
--health-timeout 5s
|
|
26
|
+
--health-retries 5
|
|
27
|
+
ports:
|
|
28
|
+
- 5432:5432
|
|
29
|
+
mysql:
|
|
30
|
+
image: mysql:8
|
|
31
|
+
env:
|
|
32
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
|
33
|
+
options: >-
|
|
34
|
+
--health-cmd "mysqladmin ping"
|
|
35
|
+
--health-interval 10s
|
|
36
|
+
--health-timeout 5s
|
|
37
|
+
--health-retries 5
|
|
38
|
+
ports:
|
|
39
|
+
- 3306:3306
|
|
40
|
+
env:
|
|
41
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
14
42
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
43
|
+
- uses: actions/checkout@v4
|
|
16
44
|
- uses: ruby/setup-ruby@v1
|
|
17
45
|
with:
|
|
18
46
|
ruby-version: ${{ matrix.ruby }}
|
|
19
|
-
bundler-cache: true
|
|
20
|
-
-
|
|
21
|
-
|
|
47
|
+
bundler-cache: true
|
|
48
|
+
- name: Create test database
|
|
49
|
+
if: matrix.database == 'postgres'
|
|
50
|
+
run: PGPASSWORD=postgres createdb -h localhost -U postgres test
|
|
51
|
+
- name: Create test database
|
|
52
|
+
if: matrix.database == 'mysql'
|
|
53
|
+
run: mysql -h 127.0.0.1 -u root -e "CREATE DATABASE test"
|
|
54
|
+
- name: Run tests
|
|
55
|
+
run: bundle exec rake
|
|
56
|
+
env:
|
|
57
|
+
DATABASE_URL: ${{ matrix.database == 'postgres' && 'postgres://postgres:postgres@localhost:5432/test' || matrix.database == 'mysql' && 'trilogy://root@127.0.0.1:3306/test' || 'sqlite3::memory:' }}
|
data/Appraisals
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
appraise "rails-7.
|
|
2
|
-
gem "rails", "~>7.
|
|
3
|
-
gem "sqlite3", "~>1.0"
|
|
1
|
+
appraise "rails-7.2" do
|
|
2
|
+
gem "rails", "~>7.2.0"
|
|
4
3
|
end
|
|
5
4
|
|
|
6
|
-
appraise "rails-
|
|
7
|
-
gem "rails", "~>
|
|
5
|
+
appraise "rails-8.0" do
|
|
6
|
+
gem "rails", "~>8.0.0"
|
|
8
7
|
end
|
|
9
8
|
|
|
10
|
-
appraise "rails-
|
|
11
|
-
gem "rails", "~>
|
|
9
|
+
appraise "rails-8.1" do
|
|
10
|
+
gem "rails", "~>8.1.0"
|
|
12
11
|
end
|
|
13
|
-
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## About
|
|
6
|
+
|
|
7
|
+
ActiveRecord::JsonAssociations is a Ruby gem that provides an alternative to traditional many-to-many join tables by storing foreign keys in a JSON array on the parent record.
|
|
8
|
+
|
|
9
|
+
## Development Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Run all tests
|
|
13
|
+
bundle exec rake
|
|
14
|
+
|
|
15
|
+
# Run a single spec file
|
|
16
|
+
bundle exec rspec spec/belongs_to_many_spec.rb
|
|
17
|
+
|
|
18
|
+
# Run a specific test by line number
|
|
19
|
+
bundle exec rspec spec/belongs_to_many_spec.rb:42
|
|
20
|
+
|
|
21
|
+
# Test against specific Rails versions using Appraisal
|
|
22
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake
|
|
23
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.0.gemfile bundle exec rake
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
The gem extends `ActiveRecord::Base` with a single module (`ActiveRecord::JsonAssociations`) that provides two main class methods:
|
|
29
|
+
|
|
30
|
+
- **`belongs_to_many`** - Stores foreign keys as a JSON array in a text/json column on the parent model. Provides `children`, `children=`, `child_ids`, `child_ids=`, `children?` methods plus a `child_ids_including` scope for querying.
|
|
31
|
+
|
|
32
|
+
- **`has_many :json_foreign_key`** - The inverse relationship. When a child model uses this option, it can find parents that reference it via their JSON arrays. Also provides `build_parent`, `create_parent`, `create_parent!` builder methods.
|
|
33
|
+
|
|
34
|
+
Both methods support native JSON columns (using `JSON_CONTAINS`) or text columns (using `LIKE` queries with serialized JSON).
|
|
35
|
+
|
|
36
|
+
## Testing
|
|
37
|
+
|
|
38
|
+
Tests use RSpec with an in-memory SQLite database. Each spec file sets up its own schema and model classes. Use `focus: true` on individual specs during development.
|
data/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# ActiveRecord::JsonAssociations
|
|
2
2
|
|
|
3
3
|
[](https://github.com/botandrose/active_record-json_associations/actions/workflows/ci.yml)
|
|
4
|
-
[](https://codeclimate.com/github/botandrose/active_record-json_associations)
|
|
5
4
|
|
|
6
5
|
Instead of keeping the foreign keys on the children, or in a many-to-many join table, let's keep them in a JSON array on the parent.
|
|
7
6
|
|
|
@@ -12,7 +11,7 @@ require "active_record/json_associations"
|
|
|
12
11
|
|
|
13
12
|
ActiveRecord::Schema.define do
|
|
14
13
|
create_table :parents do |t|
|
|
15
|
-
t.
|
|
14
|
+
t.json :child_ids, default: []
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
create_table :children
|
|
@@ -23,6 +22,8 @@ class Parent < ActiveRecord::Base
|
|
|
23
22
|
end
|
|
24
23
|
```
|
|
25
24
|
|
|
25
|
+
**Note:** The `child_ids` column must be a native JSON type. Text columns are not supported.
|
|
26
|
+
|
|
26
27
|
This will add some familiar `has_many`-style methods:
|
|
27
28
|
|
|
28
29
|
```ruby
|
|
@@ -37,16 +38,16 @@ parent.child_ids #=> [1,2]
|
|
|
37
38
|
parent.children? #=> true
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
And a scope method for finding records
|
|
41
|
+
And a scope method for finding records associated with an id:
|
|
41
42
|
|
|
42
43
|
```ruby
|
|
43
44
|
Parent.child_ids_including(2) # => [<Parent child_ids: [1,2,3]>]
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
Or any of specified array of ids:
|
|
47
|
+
Or any of a specified array of ids:
|
|
47
48
|
|
|
48
49
|
```ruby
|
|
49
|
-
Parent.child_ids_including([2,4,5]) # => [<Parent child_ids: [1,2,3]>]
|
|
50
|
+
Parent.child_ids_including(any: [2,4,5]) # => [<Parent child_ids: [1,2,3]>]
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
`touch: true` can be specified on belongs_to_many to touch the associated records' timestamps when the record is modified.
|
|
@@ -77,7 +78,9 @@ child.build_parent(name: "Momma")
|
|
|
77
78
|
|
|
78
79
|
## Requirements
|
|
79
80
|
|
|
80
|
-
*
|
|
81
|
+
* Ruby 3.2+
|
|
82
|
+
* ActiveRecord 7.2+
|
|
83
|
+
* Database with JSON column support (MySQL, PostgreSQL, SQLite 3.9+)
|
|
81
84
|
|
|
82
85
|
## Contributing
|
|
83
86
|
|
|
@@ -12,6 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = %q{Instead of a many-to-many join table, serialize the ids into a JSON array.}
|
|
13
13
|
spec.homepage = "https://github.com/botandrose/active_record-json_associations"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.2"
|
|
15
16
|
|
|
16
17
|
spec.files = `git ls-files -z`.split("\x0")
|
|
17
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
@@ -25,6 +26,8 @@ Gem::Specification.new do |spec|
|
|
|
25
26
|
spec.add_development_dependency "rake"
|
|
26
27
|
spec.add_development_dependency "rspec"
|
|
27
28
|
spec.add_development_dependency "sqlite3"
|
|
29
|
+
spec.add_development_dependency "pg"
|
|
30
|
+
spec.add_development_dependency "trilogy"
|
|
28
31
|
spec.add_development_dependency "byebug"
|
|
29
32
|
spec.add_development_dependency "timecop"
|
|
30
33
|
end
|
|
@@ -1,18 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "active_record"
|
|
2
4
|
require "json"
|
|
3
5
|
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module JsonAssociations
|
|
8
|
+
ORDER_BY_IDS_PROC = proc do |scope, ids|
|
|
9
|
+
if ids.empty?
|
|
10
|
+
scope
|
|
11
|
+
else
|
|
12
|
+
pk = scope.klass.primary_key
|
|
13
|
+
quoted_ids = ids.map { |id| scope.connection.quote(id) }
|
|
14
|
+
order_sql = case scope.connection.adapter_name
|
|
15
|
+
when "Mysql2", "Trilogy"
|
|
16
|
+
"FIELD(#{pk}, #{quoted_ids.join(",")})"
|
|
17
|
+
when "PostgreSQL"
|
|
18
|
+
"array_position(ARRAY[#{quoted_ids.join(",")}], #{pk})"
|
|
19
|
+
else
|
|
20
|
+
fragments = ids.each_with_index.map { |id, i| "WHEN #{quoted_ids[i]} THEN #{i}" }
|
|
21
|
+
"CASE #{pk} #{fragments.join(" ")} END"
|
|
22
|
+
end
|
|
23
|
+
scope.order!(Arel.sql(order_sql))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
private_constant :ORDER_BY_IDS_PROC
|
|
27
|
+
|
|
6
28
|
FIELD_INCLUDE_SCOPE_BUILDER_PROC = proc do |context, field, id|
|
|
7
|
-
|
|
29
|
+
unless context.columns_hash[field.to_s].type == :json
|
|
30
|
+
raise ArgumentError, "#{field} column must be of type :json"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sanitized_id = id.to_i
|
|
8
34
|
|
|
9
|
-
|
|
10
|
-
|
|
35
|
+
case context.connection.adapter_name
|
|
36
|
+
when "Mysql2", "Trilogy"
|
|
37
|
+
context.where("JSON_CONTAINS(#{field}, ?, '$')", sanitized_id.to_json)
|
|
38
|
+
when "PostgreSQL"
|
|
39
|
+
context.where("#{field}::jsonb @> ?::jsonb", [sanitized_id].to_json)
|
|
11
40
|
else
|
|
12
|
-
context.where("#{field}=
|
|
13
|
-
context.where("#{field} LIKE '[#{id},%'")).or(
|
|
14
|
-
context.where("#{field} LIKE '%,#{id},%'")).or(
|
|
15
|
-
context.where("#{field} LIKE '%,#{id}]'"))
|
|
41
|
+
context.where("EXISTS (SELECT 1 FROM json_each(#{field}) WHERE value = ?)", sanitized_id)
|
|
16
42
|
end
|
|
17
43
|
end
|
|
18
44
|
private_constant :FIELD_INCLUDE_SCOPE_BUILDER_PROC
|
|
@@ -26,34 +52,16 @@ module ActiveRecord
|
|
|
26
52
|
|
|
27
53
|
class_name ||= one.classify
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if !using_json
|
|
32
|
-
if ActiveRecord.version >= Gem::Version.new("7.1")
|
|
33
|
-
serialize one_ids, coder: JSON
|
|
34
|
-
else
|
|
35
|
-
serialize one_ids, JSON
|
|
36
|
-
end
|
|
55
|
+
unless columns_hash[one_ids.to_s].type == :json
|
|
56
|
+
raise ArgumentError, "#{one_ids} column must be of type :json"
|
|
37
57
|
end
|
|
38
58
|
|
|
39
59
|
if touch
|
|
40
60
|
after_commit do
|
|
41
61
|
unless no_touching?
|
|
42
|
-
|
|
43
|
-
old_ids, new_ids = send(method)[one_ids.to_s]
|
|
62
|
+
old_ids, new_ids = saved_changes[one_ids.to_s]
|
|
44
63
|
ids = Array(send(one_ids)) | Array(old_ids) | Array(new_ids)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if scope.respond_to?(:touch) # AR 6.0+
|
|
48
|
-
scope.touch_all
|
|
49
|
-
elsif self.class.respond_to?(:touch_attributes_with_time) # AR 5.1+
|
|
50
|
-
scope.update_all self.class.touch_attributes_with_time
|
|
51
|
-
else # AR 5.0
|
|
52
|
-
attributes = timestamp_attributes_for_update_in_model.inject({}) do |attributes, key|
|
|
53
|
-
attributes.merge(key => current_time_from_proper_timezone)
|
|
54
|
-
end
|
|
55
|
-
scope.update_all attributes
|
|
56
|
-
end
|
|
64
|
+
class_name.constantize.where(self.class.primary_key => ids).touch_all
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
end
|
|
@@ -84,16 +92,9 @@ module ActiveRecord
|
|
|
84
92
|
|
|
85
93
|
define_method many do
|
|
86
94
|
klass = class_name.constantize
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
ids
|
|
90
|
-
scope.where!(klass.primary_key => ids)
|
|
91
|
-
|
|
92
|
-
fragments = []
|
|
93
|
-
fragments += ["#{klass.primary_key} NOT IN (#{ids.map(&:to_s).join(",")})"] if ids.any?
|
|
94
|
-
fragments += ids.reverse.map { |id| "#{klass.primary_key}=#{id}" }
|
|
95
|
-
order_by_ids = fragments.join(", ")
|
|
96
|
-
scope.order!(Arel.sql(order_by_ids))
|
|
95
|
+
ids = send(one_ids).map(&:to_i)
|
|
96
|
+
scope = klass.where(klass.primary_key => ids)
|
|
97
|
+
ORDER_BY_IDS_PROC.call(scope, ids)
|
|
97
98
|
end
|
|
98
99
|
|
|
99
100
|
define_method many_equals do |collection|
|
|
@@ -126,30 +127,43 @@ module ActiveRecord
|
|
|
126
127
|
create_one_bang = :"create_#{one}!"
|
|
127
128
|
|
|
128
129
|
class_name = options[:class_name] || one.classify
|
|
129
|
-
klass = class_name.constantize
|
|
130
130
|
|
|
131
131
|
foreign_key = options[:json_foreign_key]
|
|
132
132
|
foreign_key = :"#{model_name.singular}_ids" if foreign_key == true
|
|
133
133
|
|
|
134
|
+
pending_associations_var = :"@pending_#{many}"
|
|
135
|
+
|
|
136
|
+
after_create do
|
|
137
|
+
if (pending = instance_variable_get(pending_associations_var))
|
|
138
|
+
instance_variable_set(pending_associations_var, nil)
|
|
139
|
+
send(many_equals, pending)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
134
143
|
include Module.new {
|
|
135
144
|
define_method one_ids do
|
|
136
145
|
send(many).pluck(:id)
|
|
137
146
|
end
|
|
138
147
|
|
|
139
148
|
define_method one_ids_equals do |ids|
|
|
149
|
+
klass = class_name.constantize
|
|
140
150
|
normalized_ids = Array(ids).select(&:present?).map(&:to_i).uniq
|
|
141
151
|
send many_equals, klass.find(normalized_ids)
|
|
142
152
|
end
|
|
143
153
|
|
|
144
154
|
define_method many do
|
|
155
|
+
klass = class_name.constantize
|
|
145
156
|
FIELD_INCLUDE_SCOPE_BUILDER_PROC.call(klass, foreign_key, id)
|
|
146
157
|
end
|
|
147
158
|
|
|
148
159
|
define_method many_equals do |collection|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
if new_record?
|
|
161
|
+
instance_variable_set(pending_associations_var, collection)
|
|
162
|
+
else
|
|
163
|
+
collection.each do |record|
|
|
164
|
+
new_id_array = Array(record.send(foreign_key)) | [id]
|
|
165
|
+
record.update foreign_key => new_id_array
|
|
166
|
+
end
|
|
153
167
|
end
|
|
154
168
|
end
|
|
155
169
|
|
|
@@ -158,14 +172,17 @@ module ActiveRecord
|
|
|
158
172
|
end
|
|
159
173
|
|
|
160
174
|
define_method build_one do |attributes={}|
|
|
175
|
+
klass = class_name.constantize
|
|
161
176
|
klass.new attributes.merge!(foreign_key => [id])
|
|
162
177
|
end
|
|
163
178
|
|
|
164
179
|
define_method create_one do |attributes={}|
|
|
180
|
+
klass = class_name.constantize
|
|
165
181
|
klass.create attributes.merge!(foreign_key => [id])
|
|
166
182
|
end
|
|
167
183
|
|
|
168
184
|
define_method create_one_bang do |attributes={}|
|
|
185
|
+
klass = class_name.constantize
|
|
169
186
|
klass.create! attributes.merge!(foreign_key => [id])
|
|
170
187
|
end
|
|
171
188
|
}
|
|
@@ -2,23 +2,22 @@ require "active_record/json_associations"
|
|
|
2
2
|
|
|
3
3
|
describe ActiveRecord::JsonAssociations do
|
|
4
4
|
before do
|
|
5
|
-
ActiveRecord::Base.establish_connection
|
|
5
|
+
ActiveRecord::Base.establish_connection database_config
|
|
6
6
|
|
|
7
7
|
silence_stream(STDOUT) do
|
|
8
8
|
ActiveRecord::Schema.define do
|
|
9
|
-
create_table :parents do |t|
|
|
9
|
+
create_table :parents, force: true do |t|
|
|
10
10
|
t.string :name
|
|
11
|
-
t.
|
|
12
|
-
t.
|
|
13
|
-
t.json :dependent_types_and_ids
|
|
11
|
+
t.json :child_ids
|
|
12
|
+
t.json :fuzzy_ids
|
|
14
13
|
t.timestamps
|
|
15
14
|
end
|
|
16
15
|
|
|
17
|
-
create_table :children do |t|
|
|
16
|
+
create_table :children, force: true do |t|
|
|
18
17
|
t.timestamps
|
|
19
18
|
end
|
|
20
19
|
|
|
21
|
-
create_table :pets do |t|
|
|
20
|
+
create_table :pets, force: true do |t|
|
|
22
21
|
t.timestamps
|
|
23
22
|
end
|
|
24
23
|
end
|
data/spec/has_many_spec.rb
CHANGED
|
@@ -2,22 +2,22 @@ require "active_record/json_associations"
|
|
|
2
2
|
|
|
3
3
|
describe ActiveRecord::JsonAssociations do
|
|
4
4
|
before do
|
|
5
|
-
ActiveRecord::Base.establish_connection
|
|
5
|
+
ActiveRecord::Base.establish_connection database_config
|
|
6
6
|
|
|
7
7
|
silence_stream(STDOUT) do
|
|
8
8
|
ActiveRecord::Schema.define do
|
|
9
|
-
create_table :parents do |t|
|
|
9
|
+
create_table :parents, force: true do |t|
|
|
10
10
|
t.string :name
|
|
11
|
-
t.
|
|
12
|
-
t.
|
|
11
|
+
t.json :child_ids
|
|
12
|
+
t.json :fuzzy_ids
|
|
13
13
|
t.timestamps
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
create_table :children do |t|
|
|
16
|
+
create_table :children, force: true do |t|
|
|
17
17
|
t.timestamps
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
create_table :pets do |t|
|
|
20
|
+
create_table :pets, force: true do |t|
|
|
21
21
|
t.timestamps
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -87,6 +87,14 @@ describe ActiveRecord::JsonAssociations do
|
|
|
87
87
|
expect(subject.parents).to eq parents
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
it "defers assignment until after create for new records" do
|
|
91
|
+
child = Child.new
|
|
92
|
+
child.parents = parents
|
|
93
|
+
expect(parents.map { |p| p.reload.child_ids }).to eq [[], [], []]
|
|
94
|
+
child.save!
|
|
95
|
+
expect(parents.map { |p| p.reload.child_ids }).to eq [[child.id], [child.id], [child.id]]
|
|
96
|
+
end
|
|
97
|
+
|
|
90
98
|
context "finds records with the specified id" do
|
|
91
99
|
let(:child) { Child.create! }
|
|
92
100
|
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
require "byebug"
|
|
2
2
|
require "timecop"
|
|
3
3
|
|
|
4
|
+
def database_config
|
|
5
|
+
if ENV["DATABASE_URL"]
|
|
6
|
+
{ url: ENV["DATABASE_URL"] }
|
|
7
|
+
else
|
|
8
|
+
{ adapter: "sqlite3", database: ":memory:" }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
4
12
|
RSpec.configure do |config|
|
|
5
13
|
config.filter_run focus: true
|
|
6
14
|
config.run_all_when_everything_filtered = true
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_record-json_associations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micah Geisel
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -94,6 +94,34 @@ dependencies:
|
|
|
94
94
|
- - ">="
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: pg
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: trilogy
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
97
125
|
- !ruby/object:Gem::Dependency
|
|
98
126
|
name: byebug
|
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -134,15 +162,16 @@ files:
|
|
|
134
162
|
- ".gitignore"
|
|
135
163
|
- ".rspec"
|
|
136
164
|
- Appraisals
|
|
165
|
+
- CLAUDE.md
|
|
137
166
|
- Gemfile
|
|
138
167
|
- LICENSE.txt
|
|
139
168
|
- README.md
|
|
140
169
|
- Rakefile
|
|
141
170
|
- active_record-json_associations.gemspec
|
|
142
171
|
- bin/setup
|
|
143
|
-
- gemfiles/rails_7.0.gemfile
|
|
144
|
-
- gemfiles/rails_7.1.gemfile
|
|
145
172
|
- gemfiles/rails_7.2.gemfile
|
|
173
|
+
- gemfiles/rails_8.0.gemfile
|
|
174
|
+
- gemfiles/rails_8.1.gemfile
|
|
146
175
|
- lib/active_record/json_associations.rb
|
|
147
176
|
- lib/active_record/json_associations/version.rb
|
|
148
177
|
- spec/belongs_to_many_spec.rb
|
|
@@ -152,7 +181,7 @@ homepage: https://github.com/botandrose/active_record-json_associations
|
|
|
152
181
|
licenses:
|
|
153
182
|
- MIT
|
|
154
183
|
metadata: {}
|
|
155
|
-
post_install_message:
|
|
184
|
+
post_install_message:
|
|
156
185
|
rdoc_options: []
|
|
157
186
|
require_paths:
|
|
158
187
|
- lib
|
|
@@ -160,7 +189,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
160
189
|
requirements:
|
|
161
190
|
- - ">="
|
|
162
191
|
- !ruby/object:Gem::Version
|
|
163
|
-
version: '
|
|
192
|
+
version: '3.2'
|
|
164
193
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
165
194
|
requirements:
|
|
166
195
|
- - ">="
|
|
@@ -168,7 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
168
197
|
version: '0'
|
|
169
198
|
requirements: []
|
|
170
199
|
rubygems_version: 3.5.11
|
|
171
|
-
signing_key:
|
|
200
|
+
signing_key:
|
|
172
201
|
specification_version: 4
|
|
173
202
|
summary: Instead of a many-to-many join table, serialize the ids into a JSON array.
|
|
174
203
|
test_files:
|