associate_jsonb 0.0.4 → 0.0.10

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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/Rakefile +1 -3
  4. data/lib/associate_jsonb.rb +133 -5
  5. data/lib/associate_jsonb/arel_extensions/table.rb +1 -1
  6. data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
  7. data/lib/associate_jsonb/associations/association_scope.rb +2 -4
  8. data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
  9. data/lib/associate_jsonb/associations/builder/belongs_to.rb +2 -2
  10. data/lib/associate_jsonb/attribute_methods.rb +19 -0
  11. data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
  12. data/lib/associate_jsonb/connection_adapters/schema_creation.rb +168 -0
  13. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
  14. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
  15. data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
  16. data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
  17. data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
  18. data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
  19. data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
  20. data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
  21. data/lib/associate_jsonb/persistence.rb +14 -0
  22. data/lib/associate_jsonb/predicate_builder.rb +15 -0
  23. data/lib/associate_jsonb/reflection.rb +2 -2
  24. data/lib/associate_jsonb/relation/where_clause.rb +3 -0
  25. data/lib/associate_jsonb/version.rb +1 -1
  26. data/lib/associate_jsonb/with_store_attribute.rb +31 -23
  27. metadata +29 -12
  28. data/lib/associate_jsonb/connection_adapters/reference_definition.rb +0 -64
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module Table
7
+ def constraint(**opts)
8
+ @base.add_constraint(name, **opts)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module TableDefinition
7
+ attr_reader :constraints
8
+
9
+ def initialize(*,**)
10
+ super
11
+ @constraints = []
12
+ end
13
+
14
+ def constraint(name = nil, **opts)
15
+ unless opts[:value].present?
16
+ raise ArgumentError.new("Invalid Drop Constraint Options")
17
+ end
18
+
19
+ @constraints << ConstraintDefinition.new(
20
+ **opts.reverse_merge(name: name)
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,116 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module ConnectionAdapters
6
+ module SchemaStatements
7
+ def add_jsonb_nested_set_function
8
+ execute schema_creation.accept(AddJsonbNestedSetFunction.new)
9
+ end
10
+
11
+ def add_jsonb_foreign_key_function
12
+ execute schema_creation.accept(AddJsonbForeignKeyFunction.new)
13
+ end
14
+
15
+ def create_table(table_name, **options)
16
+ td = create_table_definition(table_name, **options)
17
+
18
+ if options[:id] != false && !options[:as]
19
+ pk = options.fetch(:primary_key) do
20
+ ActiveRecord::Base.get_primary_key table_name.to_s.singularize
21
+ end
22
+
23
+ if pk.is_a?(Array)
24
+ td.primary_keys pk
25
+ else
26
+ td.primary_key pk, options.fetch(:id, :primary_key), **options.except(:comment)
27
+ end
28
+ end
29
+
30
+ yield td if block_given?
31
+
32
+ if options[:force]
33
+ drop_table(table_name, **options, if_exists: true)
34
+ end
35
+
36
+ result = execute schema_creation.accept td
37
+
38
+ td.indexes.each do |column_name, index_options|
39
+ add_index(table_name, column_name, index_options)
40
+ end
41
+
42
+ td.constraints.each do |ct|
43
+ add_constraint(table_name, **ct)
44
+ end
45
+
46
+ if table_comment = options[:comment].presence
47
+ change_table_comment(table_name, table_comment)
48
+ end
49
+
50
+ td.columns.each do |column|
51
+ change_column_comment(table_name, column.name, column.comment) if column.comment.present?
52
+ end
53
+
54
+ result
55
+ end
56
+
57
+ def add_constraint(table_name, **options)
58
+ at = create_alter_table table_name
59
+ at.add_constraint(**options)
60
+ execute schema_creation.accept at
61
+ end
62
+
63
+ def constraints(table_name) # :nodoc:
64
+ scope = quoted_scope(table_name)
65
+
66
+ result = query(<<~SQL, "SCHEMA")
67
+ SELECT
68
+ con.oid,
69
+ con.conname,
70
+ con.connamespace,
71
+ con.contype,
72
+ con.condeferrable,
73
+ con.condeferred,
74
+ con.convalidated,
75
+ pg_get_constraintdef(con.oid) as consrc
76
+ FROM pg_catalog.pg_constraint con
77
+ INNER JOIN pg_catalog.pg_class rel
78
+ ON rel.oid = con.conrelid
79
+ INNER JOIN pg_catalog.pg_namespace nsp
80
+ ON nsp.oid = connamespace
81
+ WHERE nsp.nspname = #{scope[:schema]}
82
+ AND rel.relname = #{scope[:name]}
83
+ ORDER BY rel.relname
84
+ SQL
85
+
86
+ result.map do |row|
87
+ {
88
+ oid: row[0],
89
+ name: row[1],
90
+ deferrable: row[4],
91
+ deferred: row[5],
92
+ validated: row[6],
93
+ definition: row[7],
94
+ type:
95
+ case row[3].to_s.downcase
96
+ when "c"
97
+ "CHECK"
98
+ when "f"
99
+ "FOREIGN KEY"
100
+ when "p"
101
+ "PRIMARY KEY"
102
+ when "u"
103
+ "UNIQUE"
104
+ when "t"
105
+ "TRIGGER"
106
+ when "x"
107
+ "EXCLUDE"
108
+ else
109
+ "UNKNOWN"
110
+ end
111
+ }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module Persistence
6
+ private
7
+ def _update_row(attribute_names, attempted_action = "update")
8
+ self.class._update_record(
9
+ attributes_with_info(attribute_names),
10
+ @primary_key => id_in_database
11
+ )
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ module AssociateJsonb
5
+ module PredicateBuilder # :nodoc:
6
+ def build_bind_attribute(column_name, value)
7
+ if value.respond_to?(:value_before_type_cast)
8
+ attr = ActiveRecord::Relation::QueryAttribute.new(column_name.to_s, value.value_before_type_cast, table.type(column_name), value)
9
+ else
10
+ attr = ActiveRecord::Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name))
11
+ end
12
+ Arel::Nodes::BindParam.new(attr)
13
+ end
14
+ end
15
+ end
@@ -60,7 +60,7 @@ module AssociateJsonb
60
60
  Arel::Nodes::NamedFunction.new(
61
61
  "CAST",
62
62
  [
63
- Arel::Nodes::Jsonb::DashArrow.
63
+ Arel::Nodes::Jsonb::DashDoubleArrow.
64
64
  new(table, table[foreign_store_attr], foreign_store_key || key).
65
65
  as(foreign_klass.columns_hash[foreign_key.to_s].sql_type)
66
66
  ]
@@ -83,7 +83,7 @@ module AssociateJsonb
83
83
  Arel::Nodes::NamedFunction.new(
84
84
  "CAST",
85
85
  [
86
- Arel::Nodes::Jsonb::DashArrow.
86
+ Arel::Nodes::Jsonb::DashDoubleArrow.
87
87
  new(foreign_table, foreign_table[jsonb_store_attr], jsonb_store_key || foreign_key).
88
88
  as(klass.columns_hash[key.to_s].sql_type)
89
89
  ]
@@ -1,3 +1,6 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
1
4
  module AssociateJsonb
2
5
  module Relation
3
6
  module WhereClause
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module AssociateJsonb
5
- VERSION = "0.0.4"
5
+ VERSION = "0.0.10"
6
6
  end
@@ -34,7 +34,7 @@ module AssociateJsonb
34
34
  end
35
35
 
36
36
  included do
37
- instance_eval <<-CODE, __FILE__, __LINE__ + 1
37
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
38
38
  initialize_store_column_attribute_tracker
39
39
 
40
40
  after_initialize &set_store_column_attribute_values_on_init
@@ -106,7 +106,7 @@ module AssociateJsonb
106
106
  array = attribute_opts[:array]
107
107
  attribute attr, cast_type, **attribute_opts
108
108
 
109
- instance_eval <<-CODE, __FILE__, __LINE__ + 1
109
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
110
110
  add_store_column_attribute_name("#{attr}", :#{store}, "#{key}", { sql_type: sql_type, type: cast_type, opts: attribute_opts })
111
111
  CODE
112
112
 
@@ -117,44 +117,52 @@ module AssociateJsonb
117
117
  class InstanceMethodsOnActivation < Module
118
118
  def initialize(mixin, store, attribute, key, is_array)
119
119
  is_array = !!(is_array && attribute.to_s =~ /_ids$/)
120
- on_attr_change =
121
- is_array \
122
- ? "write_attribute(:#{attribute}, Array(given))" \
123
- : "super(given)"
124
- on_store_change = ->(var) {
125
- "write_attribute(:#{attribute}, #{
126
- is_array \
127
- ? "Array(#{var})" \
128
- : var
129
- })"
130
- }
131
120
 
121
+ array_or_attr = ->(value) {
122
+ is_array \
123
+ ? %Q(Array(#{value})) \
124
+ : %Q(#{value})
125
+ }
126
+
127
+ on_store_change = "_write_attribute(:#{attribute}, #{array_or_attr.call %Q(#{store}["#{key}"])})"
128
+ on_attr_change = "super(#{array_or_attr.call %Q(given)})"
132
129
 
133
130
  if is_array
134
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
131
+ mixin.class_eval <<~CODE, __FILE__, __LINE__ + 1
135
132
  def #{attribute}
136
133
  _read_attribute(:#{attribute}) || []
137
134
  end
138
135
  CODE
139
136
  end
140
137
 
141
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
138
+ mixin.class_eval <<~CODE, __FILE__, __LINE__ + 1
142
139
  def #{store}=(given)
143
- super(given || {})
144
- write_attribute(:#{attribute}, #{on_store_change.call %Q(#{store}["#{key}"])})
145
- if #{attribute}.blank?
146
- #{store}.delete("#{key}")
140
+ if given.is_a?(::String)
141
+ given = ActiveSupport::JSON.decode(given) rescue nil
142
+ end
143
+
144
+ if AssociateJsonb.merge_hash?(self.class.attribute_types["#{store}"])
145
+ if !given
146
+ given = {}
147
+ #{store}.keys.each do |k|
148
+ given[k] = nil
149
+ end
150
+ end
151
+ super(#{store}.deep_merge(given.deep_stringify_keys))
152
+
153
+ self.#{attribute}= #{store}["#{key}"] if #{store}.key?("#{key}")
147
154
  else
148
- #{store}["#{key}"] = #{attribute}
155
+ super given || {}
156
+ self.#{attribute}= #{store}["#{key}"]
149
157
  end
158
+
150
159
  #{store}
151
160
  end
152
161
 
153
162
  def #{attribute}=(given)
154
163
  #{on_attr_change}
155
- value = #{store}["#{key}"] = #{attribute}
156
- #{store}.delete("#{key}") if value.nil?
157
- _write_attribute(:#{store}, #{store})
164
+ value = #{store}["#{key}"] = #{attribute}.presence
165
+ #{store}.delete("#{key}") unless !value.nil? || AssociateJsonb.merge_hash?(self.class.attribute_types["#{store}"])
158
166
  value
159
167
  end
160
168
  CODE
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: associate_jsonb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampson Crowley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-16 00:00:00.000000000 Z
11
+ date: 2020-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -96,22 +96,26 @@ dependencies:
96
96
  requirements:
97
97
  - - "~>"
98
98
  - !ruby/object:Gem::Version
99
- version: 3.7.0
99
+ version: '3.9'
100
100
  type: :development
101
101
  prerelease: false
102
102
  version_requirements: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - "~>"
105
105
  - !ruby/object:Gem::Version
106
- version: 3.7.0
107
- description: |2
108
- This gem extends ActiveRecord to let you use PostgreSQL JSONB data for associations
106
+ version: '3.9'
107
+ description: |
108
+ This gem extends ActiveRecord to add additional functionality to JSONB
109
109
 
110
- Inspired by activerecord-jsonb-associations, but for use in Rails 6+ and
111
- ruby 2.7+ and with some unnecessary options and features (HABTM) removed
110
+ - use PostgreSQL JSONB data for associations
111
+ - thread-safe single-key updates to JSONB columns using `jsonb_set`
112
+ - extended `table#references` for easy migrations and indexes
113
+ - virtual JSONB foreign keys using check constraints
114
+ (NOTE: real foreign key constraints are not possible with PostgreSQL JSONB)
112
115
 
113
- BONUS: extended `table#references` for easy migrations and indexes
114
- (NOTE: real foreign key constraints are not possible with PostgreSQL JSONB)
116
+ Inspired by activerecord-jsonb-associations, but for use in Rails 6+ and
117
+ ruby 2.7+ and with some unnecessary options and features (HABTM) removed
118
+ and some additional features added
115
119
  email:
116
120
  - sampsonsprojects@gmail.com
117
121
  executables: []
@@ -125,6 +129,7 @@ files:
125
129
  - lib/associate_jsonb/arel_extensions/nodes/binary.rb
126
130
  - lib/associate_jsonb/arel_extensions/nodes/table_alias.rb
127
131
  - lib/associate_jsonb/arel_extensions/table.rb
132
+ - lib/associate_jsonb/arel_extensions/visitors/postgresql.rb
128
133
  - lib/associate_jsonb/arel_extensions/visitors/visitor.rb
129
134
  - lib/associate_jsonb/arel_nodes/jsonb/at_arrow.rb
130
135
  - lib/associate_jsonb/arel_nodes/jsonb/attribute.rb
@@ -147,8 +152,20 @@ files:
147
152
  - lib/associate_jsonb/associations/has_many_association.rb
148
153
  - lib/associate_jsonb/associations/join_dependency.rb
149
154
  - lib/associate_jsonb/associations/preloader/association.rb
155
+ - lib/associate_jsonb/attribute_methods.rb
156
+ - lib/associate_jsonb/attribute_methods/read.rb
150
157
  - lib/associate_jsonb/connection_adapters.rb
151
- - lib/associate_jsonb/connection_adapters/reference_definition.rb
158
+ - lib/associate_jsonb/connection_adapters/schema_creation.rb
159
+ - lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb
160
+ - lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb
161
+ - lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb
162
+ - lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb
163
+ - lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb
164
+ - lib/associate_jsonb/connection_adapters/schema_definitions/table.rb
165
+ - lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb
166
+ - lib/associate_jsonb/connection_adapters/schema_statements.rb
167
+ - lib/associate_jsonb/persistence.rb
168
+ - lib/associate_jsonb/predicate_builder.rb
152
169
  - lib/associate_jsonb/reflection.rb
153
170
  - lib/associate_jsonb/relation/where_clause.rb
154
171
  - lib/associate_jsonb/supported_rails_version.rb
@@ -173,7 +190,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
173
190
  - !ruby/object:Gem::Version
174
191
  version: '0'
175
192
  requirements: []
176
- rubygems_version: 3.1.3
193
+ rubygems_version: 3.1.4
177
194
  signing_key:
178
195
  specification_version: 4
179
196
  summary: Store database references in PostgreSQL Jsonb columns
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module AssociateJsonb
4
- module ConnectionAdapters
5
- module ReferenceDefinition #:nodoc:
6
- # rubocop:disable Metrics/ParameterLists
7
- def initialize(
8
- name,
9
- store: false,
10
- **options
11
- )
12
- @store = store && store.to_sym
13
-
14
- super(name, **options)
15
- end
16
- # rubocop:enable Metrics/ParameterLists
17
-
18
- def add_to(table)
19
- return super unless store
20
-
21
- should_add_col = false
22
- if table.respond_to? :column_exists?
23
- should_add_col = !table.column_exists?(store)
24
- elsif table.respond_to? :columns
25
- should_add_col = table.columns.none? {|col| col.name.to_sym == store}
26
- end
27
-
28
- table.column(store, :jsonb, null: false, default: {}) if should_add_col
29
-
30
- return unless index
31
-
32
- # should_add_idx = false
33
- # if table.respond_to? :index_exists?
34
- # should_add_idx = !table.index_exists?([ store ], using: :gin)
35
- # elsif table.respond_to? :indexes
36
- # should_add_idx = table.indexes.none? do |idx, opts|
37
- # (idx == [ store ]) \
38
- # && (opts == { using: :gin })
39
- # end
40
- # end
41
- #
42
- # table.index([ store ], using: :gin) if should_add_idx
43
-
44
- column_names.each do |column_name|
45
- table.index(
46
- "CAST (\"#{store}\"->'#{column_name}' AS #{@type || :bigint})",
47
- using: :btree,
48
- name: "index_#{table.name}_on_#{store}_#{column_name}"
49
- )
50
-
51
- table.index(
52
- "(#{store}->>'#{column_name}')",
53
- using: :btree,
54
- name: "index_#{table.name}_on_#{store}_#{column_name}_text"
55
- )
56
- end
57
- end
58
-
59
- protected
60
-
61
- attr_reader :store
62
- end
63
- end
64
- end