Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add JSONB store support to :has_one assocations
- Loading branch information
Showing
18 changed files
with
424 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,6 @@ Gemfile.lock | |
.bundle/ | ||
log/*.log | ||
pkg/ | ||
.DS_Store | ||
|
||
spec/config/database.yml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,31 @@ | ||
#!/usr/bin/env bash | ||
set -euo pipefail | ||
IFS=$'\n\t' | ||
set -vx | ||
#!/usr/bin/env ruby | ||
|
||
bundle install | ||
require 'pathname' | ||
require 'fileutils' | ||
|
||
def system!(*args) | ||
system(*args) || abort("\n== Command #{args} failed ==") | ||
end | ||
|
||
FileUtils.chdir(Pathname.new(File.expand_path('../../', __FILE__))) do | ||
puts '== Installing dependencies ==' | ||
system! 'gem install bundler --conservative' | ||
system('bundle check') || system!('bundle install') | ||
|
||
puts "\n== Creating db config file ==" | ||
unless File.exist? 'spec/config/database.yml' | ||
db_config = File.read('spec/config/database.yml.sample') | ||
|
||
print 'Enter your PostgreSQL username: ' | ||
db_username = gets.chomp | ||
|
||
File.write( | ||
'spec/config/database.yml', | ||
db_config.gsub('%USERNAME%', db_username) | ||
) | ||
end | ||
|
||
puts "\n== Creating test database ==" | ||
system! 'dropdb activerecord_jsonb_associations_test' | ||
system! 'createdb activerecord_jsonb_associations_test' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
module ActiveRecord | ||
module JSONB | ||
module Associations | ||
module AssociationScope #:nodoc: | ||
def last_chain_scope(scope, table, reflection, owner) | ||
reflection = reflection.instance_variable_get(:@reflection) | ||
|
||
if reflection.options.key?(:foreign_store) | ||
join_keys = reflection.join_keys | ||
value = transform_value(owner[join_keys.foreign_key]) | ||
|
||
if value.is_a?(Integer) | ||
scope = apply_jsonb_scope( | ||
scope, table, reflection.options[:foreign_store], | ||
join_keys.key, value | ||
) | ||
end | ||
|
||
scope | ||
else | ||
super | ||
end | ||
end | ||
|
||
def apply_jsonb_scope(scope, table, jsonb_column, key, value) | ||
scope.where!( | ||
"(#{table.name}.#{jsonb_column}->>'#{key}')::int = :id", | ||
id: value | ||
) | ||
end | ||
end | ||
end | ||
end | ||
end |
18 changes: 18 additions & 0 deletions
18
lib/activerecord/jsonb/associations/belongs_to_association.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module ActiveRecord | ||
module JSONB | ||
module Associations | ||
module BelongsToAssociation #:nodoc: | ||
def replace_keys(record) | ||
if reflection.options.key?(:store) | ||
owner[reflection.options[:store]][reflection.foreign_key] = | ||
record._read_attribute( | ||
reflection.association_primary_key(record.class) | ||
) | ||
else | ||
super | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
module ActiveRecord | ||
module JSONB | ||
module Associations | ||
module Builder | ||
module HasOne #:nodoc: | ||
def valid_options(options) | ||
super + [:foreign_store] | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
30 changes: 30 additions & 0 deletions
30
lib/activerecord/jsonb/associations/has_one_association.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
module ActiveRecord | ||
module JSONB | ||
module Associations | ||
module HasOneAssociation #:nodoc: | ||
def creation_attributes | ||
if reflection.options.key?(:foreign_store) | ||
attributes = {} | ||
jsonb_store = reflection.options[:foreign_store] | ||
attributes[jsonb_store] ||= {} | ||
attributes[jsonb_store][reflection.foreign_key] = | ||
owner[reflection.active_record_primary_key] | ||
|
||
attributes | ||
else | ||
super | ||
end | ||
end | ||
|
||
def create_scope | ||
super.tap do |scope| | ||
next unless options.key?(:foreign_store) | ||
scope[options[:foreign_store].to_s] ||= {} | ||
scope[options[:foreign_store].to_s][reflection.foreign_key] = | ||
owner[reflection.active_record_primary_key] | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pg: | ||
username: "%USERNAME%" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
RSpec.shared_examples ':has_one with JSONB store' do | ||
let(:child_name) { child_model.model_name.element } | ||
|
||
describe '#association' do | ||
before do | ||
child_model.update | ||
end | ||
end | ||
|
||
describe '#association' do | ||
before do | ||
child_model.send "#{store}=", foreign_key => parent_model.id | ||
child_model.save | ||
end | ||
|
||
it 'properly loads association from parent model' do | ||
expect(parent_model.reload.send(child_name)).to eq(child_model) | ||
end | ||
end | ||
|
||
describe '#association=' do | ||
before do | ||
parent_model.send "#{child_name}=", child_model | ||
end | ||
|
||
it 'sets and persists foreign key on child model' do | ||
expect( | ||
child_model.reload.send(store) | ||
).to eq(foreign_key.to_s => parent_model.id) | ||
end | ||
end | ||
|
||
describe 'association_id' do | ||
before do | ||
child_model.send(store)[foreign_key.to_s] = parent_model.id | ||
end | ||
|
||
it 'reads foreign id from specified :store column by foreign key' do | ||
expect(child_model.send(foreign_key)).to eq parent_model.id | ||
end | ||
end | ||
|
||
describe '#association_id=' do | ||
before do | ||
child_model.send "#{foreign_key}=", parent_model.id | ||
end | ||
|
||
it 'sets foreign id in specified :store column as hash item' do | ||
expect(child_model.send(store)[foreign_key.to_s]).to eq(parent_model.id) | ||
end | ||
end | ||
|
||
describe '#build_association' do | ||
let(:built_association) do | ||
parent_model.send "build_#{child_name}" | ||
end | ||
|
||
it 'sets foreign key on child model' do | ||
expect( | ||
built_association.send(store) | ||
).to eq(foreign_key.to_s => parent_model.id) | ||
end | ||
end | ||
|
||
describe '#create_association' do | ||
let(:created_association) do | ||
parent_model.send "create_#{child_name}" | ||
end | ||
|
||
it 'sets and persists foreign key on child model' do | ||
expect( | ||
created_association.reload.send(store) | ||
).to eq(foreign_key.to_s => parent_model.id) | ||
end | ||
end | ||
|
||
describe '#reload_association' do | ||
before do | ||
parent_model.send "#{child_name}=", child_model | ||
end | ||
|
||
it 'reloads the association' do | ||
expect(parent_model.send("reload_#{child_name}")).to eq(child_model) | ||
end | ||
end | ||
end | ||
|
||
RSpec.describe ':has_one' do | ||
context 'regular association' do | ||
let(:parent_model) { User.create } | ||
let(:child_model) { Profile.new } | ||
|
||
describe '#create_association' do | ||
let(:created_association) do | ||
parent_model.send "create_profile" | ||
end | ||
|
||
it 'sets and persists foreign key on child model' do | ||
expect( | ||
created_association.reload.user_id | ||
).to eq(parent_model.id) | ||
end | ||
end | ||
end | ||
|
||
context 'association with :store option set on child model' do | ||
let(:child_model) { Account.new } | ||
let(:store) { :extra } | ||
|
||
context 'with default options' do | ||
let(:parent_model) { User.create } | ||
let(:foreign_key) { :user_id } | ||
|
||
include_examples ':has_one with JSONB store' | ||
end | ||
|
||
context 'with non-default :options' do | ||
let(:parent_model) { GoodsSupplier.create } | ||
let(:foreign_key) { :supplier_id } | ||
|
||
include_examples ':has_one with JSONB store' | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,24 @@ | ||
require 'bundler/setup' | ||
require 'database_cleaner' | ||
require 'factory_bot' | ||
|
||
require 'activerecord/jsonb/associations' | ||
|
||
ActiveRecord::Base | ||
require 'support/schema' | ||
require 'support/models' | ||
require 'support/factories' | ||
|
||
RSpec.configure do |config| | ||
config.include FactoryBot::Syntax::Methods | ||
|
||
config.before(:suite) do | ||
DatabaseCleaner.strategy = :transaction | ||
DatabaseCleaner.clean_with(:truncation) | ||
end | ||
|
||
config.around(:each) do |example| | ||
DatabaseCleaner.cleaning do | ||
example.run | ||
end | ||
end | ||
end |
Oops, something went wrong.