associate_jsonb 0.0.2 → 0.0.8

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/Rakefile +1 -3
  4. data/lib/associate_jsonb.rb +111 -2
  5. data/lib/associate_jsonb/arel_extensions/nodes/binary.rb +14 -0
  6. data/lib/associate_jsonb/arel_extensions/nodes/table_alias.rb +38 -0
  7. data/lib/associate_jsonb/arel_extensions/table.rb +40 -0
  8. data/lib/associate_jsonb/arel_extensions/visitors/postgresql.rb +113 -0
  9. data/lib/associate_jsonb/arel_extensions/visitors/visitor.rb +19 -0
  10. data/lib/associate_jsonb/arel_nodes/jsonb/attribute.rb +38 -0
  11. data/lib/associate_jsonb/arel_nodes/sql_casted_binary.rb +20 -0
  12. data/lib/associate_jsonb/arel_nodes/sql_casted_equality.rb +26 -12
  13. data/lib/associate_jsonb/associations/alias_tracker.rb +13 -0
  14. data/lib/associate_jsonb/associations/association_scope.rb +18 -45
  15. data/lib/associate_jsonb/associations/belongs_to_association.rb +8 -8
  16. data/lib/associate_jsonb/associations/builder/belongs_to.rb +5 -3
  17. data/lib/associate_jsonb/associations/join_dependency.rb +21 -0
  18. data/lib/associate_jsonb/attribute_methods.rb +19 -0
  19. data/lib/associate_jsonb/attribute_methods/read.rb +15 -0
  20. data/lib/associate_jsonb/connection_adapters/schema_creation.rb +167 -0
  21. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_foreign_key_function.rb +9 -0
  22. data/lib/associate_jsonb/connection_adapters/schema_definitions/add_jsonb_nested_set_function.rb +9 -0
  23. data/lib/associate_jsonb/connection_adapters/schema_definitions/alter_table.rb +40 -0
  24. data/lib/associate_jsonb/connection_adapters/schema_definitions/constraint_definition.rb +60 -0
  25. data/lib/associate_jsonb/connection_adapters/schema_definitions/reference_definition.rb +102 -0
  26. data/lib/associate_jsonb/connection_adapters/schema_definitions/table.rb +12 -0
  27. data/lib/associate_jsonb/connection_adapters/schema_definitions/table_definition.rb +25 -0
  28. data/lib/associate_jsonb/connection_adapters/schema_statements.rb +116 -0
  29. data/lib/associate_jsonb/persistence.rb +14 -0
  30. data/lib/associate_jsonb/predicate_builder.rb +15 -0
  31. data/lib/associate_jsonb/reflection.rb +2 -2
  32. data/lib/associate_jsonb/relation/where_clause.rb +19 -0
  33. data/lib/associate_jsonb/version.rb +1 -1
  34. data/lib/associate_jsonb/with_store_attribute.rb +59 -23
  35. metadata +34 -10
  36. data/lib/associate_jsonb/arel_node_extensions/binary.rb +0 -12
  37. data/lib/associate_jsonb/connection_adapters/reference_definition.rb +0 -64
@@ -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
@@ -15,6 +18,22 @@ module AssociateJsonb
15
18
  [name, value]
16
19
  }.to_h
17
20
  end
21
+
22
+ private
23
+ def equalities(predicates)
24
+ equalities = []
25
+
26
+ predicates.each do |node|
27
+ case node
28
+ when Arel::Nodes::Equality, Arel::Nodes::SqlCastedEquality
29
+ equalities << node
30
+ when Arel::Nodes::And
31
+ equalities.concat equalities(node.children)
32
+ end
33
+ end
34
+
35
+ equalities
36
+ end
18
37
  end
19
38
  end
20
39
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module AssociateJsonb
5
- VERSION = "0.0.2"
5
+ VERSION = "0.0.8"
6
6
  end
@@ -12,18 +12,29 @@ module AssociateJsonb
12
12
  @names_list ||= {}
13
13
  end
14
14
 
15
- def add_name(name, store, key)
16
- @names_list ||= {}
17
- @names_list[name.to_s.freeze] = { store: store, key: key }
15
+ def add_name(name, store, key, cast)
16
+ names_list[name.to_s.freeze] = { store: store, key: key, cast: cast }
18
17
  end
19
18
 
20
19
  def has_name?(name)
21
20
  names_list.key? name.to_s
22
21
  end
22
+
23
+ def get(name)
24
+ names_list[name.to_s]
25
+ end
26
+
27
+ def store_for(name)
28
+ (get(name) || {})[:store]
29
+ end
30
+
31
+ def key_for(name)
32
+ (get(name) || {})[:key]
33
+ end
23
34
  end
24
35
 
25
36
  included do
26
- instance_eval <<-CODE, __FILE__, __LINE__ + 1
37
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
27
38
  initialize_store_column_attribute_tracker
28
39
 
29
40
  after_initialize &set_store_column_attribute_values_on_init
@@ -37,6 +48,10 @@ module AssociateJsonb
37
48
  super
38
49
  end
39
50
 
51
+ def arel_table
52
+ super.with_store_tracker(store_column_attribute_tracker)
53
+ end
54
+
40
55
  def initialize_store_column_attribute_tracker
41
56
  @store_column_attribute_tracker = const_set(:StoreColumnAttributeTracker, StoreColumnAttributeTracker.new)
42
57
  private_constant :StoreColumnAttributeTracker
@@ -56,9 +71,9 @@ module AssociateJsonb
56
71
  end
57
72
  end
58
73
 
59
- def add_store_column_attribute_name(name, store, key)
74
+ def add_store_column_attribute_name(name, store, key, cast_opts)
60
75
  store_column_attribute_tracker.synchronize do
61
- store_column_attribute_tracker.add_name(name, store, key)
76
+ store_column_attribute_tracker.add_name(name, store, key, cast_opts)
62
77
  end
63
78
  end
64
79
 
@@ -83,16 +98,16 @@ module AssociateJsonb
83
98
  store_column_attribute :data, *args, **opts
84
99
  end
85
100
 
86
- def store_column_attribute(store, attr, *opts, key: nil, **attribute_opts)
101
+ def store_column_attribute(store, attr, cast_type = ActiveRecord::Type::Value.new, sql_type: nil, key: nil, **attribute_opts)
87
102
  store = store.to_sym
88
103
  attr = attr.to_sym
89
104
  key ||= attr
90
105
  key = key.to_s
91
106
  array = attribute_opts[:array]
92
- attribute attr, *opts, **attribute_opts
107
+ attribute attr, cast_type, **attribute_opts
93
108
 
94
- instance_eval <<-CODE, __FILE__, __LINE__ + 1
95
- add_store_column_attribute_name("#{attr}", :#{store}, "#{key}")
109
+ instance_eval <<~CODE, __FILE__, __LINE__ + 1
110
+ add_store_column_attribute_name("#{attr}", :#{store}, "#{key}", { sql_type: sql_type, type: cast_type, opts: attribute_opts })
96
111
  CODE
97
112
 
98
113
  include WithStoreAttribute::InstanceMethodsOnActivation.new(self, store, attr, key, array)
@@ -102,31 +117,52 @@ module AssociateJsonb
102
117
  class InstanceMethodsOnActivation < Module
103
118
  def initialize(mixin, store, attribute, key, is_array)
104
119
  is_array = !!(is_array && attribute.to_s =~ /_ids$/)
105
- on_attr_change =
106
- is_array \
107
- ? "write_attribute(:#{attribute}, Array(given))" \
108
- : "super(given)"
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)})"
129
+
109
130
  if is_array
110
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
131
+ mixin.class_eval <<~CODE, __FILE__, __LINE__ + 1
111
132
  def #{attribute}
112
133
  _read_attribute(:#{attribute}) || []
113
134
  end
114
135
  CODE
115
136
  end
116
137
 
117
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
138
+ mixin.class_eval <<~CODE, __FILE__, __LINE__ + 1
118
139
  def #{store}=(given)
119
- given = super(given || {}).with_indifferent_access
120
- write_attribute(:#{attribute}, given["#{key}"])
121
- given["#{key}"] = #{attribute} unless #{attribute}.nil?
122
- super(given)
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}")
154
+ else
155
+ super given || {}
156
+ self.#{attribute}= #{store}["#{key}"]
157
+ end
158
+
159
+ #{store}
123
160
  end
124
161
 
125
162
  def #{attribute}=(given)
126
163
  #{on_attr_change}
127
- value = #{store}["#{key}"] = #{attribute}
128
- #{store}.delete("#{key}") if value.nil?
129
- _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}"])
130
166
  value
131
167
  end
132
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.2
4
+ version: 0.0.8
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-14 00:00:00.000000000 Z
11
+ date: 2020-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -104,14 +104,18 @@ dependencies:
104
104
  - - "~>"
105
105
  - !ruby/object:Gem::Version
106
106
  version: 3.7.0
107
- description: |2
108
- This gem extends ActiveRecord to let you use PostgreSQL JSONB data for associations
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: []
@@ -122,15 +126,22 @@ files:
122
126
  - README.md
123
127
  - Rakefile
124
128
  - lib/associate_jsonb.rb
125
- - lib/associate_jsonb/arel_node_extensions/binary.rb
129
+ - lib/associate_jsonb/arel_extensions/nodes/binary.rb
130
+ - lib/associate_jsonb/arel_extensions/nodes/table_alias.rb
131
+ - lib/associate_jsonb/arel_extensions/table.rb
132
+ - lib/associate_jsonb/arel_extensions/visitors/postgresql.rb
133
+ - lib/associate_jsonb/arel_extensions/visitors/visitor.rb
126
134
  - lib/associate_jsonb/arel_nodes/jsonb/at_arrow.rb
135
+ - lib/associate_jsonb/arel_nodes/jsonb/attribute.rb
127
136
  - lib/associate_jsonb/arel_nodes/jsonb/bindable_operator.rb
128
137
  - lib/associate_jsonb/arel_nodes/jsonb/dash_arrow.rb
129
138
  - lib/associate_jsonb/arel_nodes/jsonb/dash_double_arrow.rb
130
139
  - lib/associate_jsonb/arel_nodes/jsonb/double_pipe.rb
131
140
  - lib/associate_jsonb/arel_nodes/jsonb/hash_arrow.rb
132
141
  - lib/associate_jsonb/arel_nodes/jsonb/operator.rb
142
+ - lib/associate_jsonb/arel_nodes/sql_casted_binary.rb
133
143
  - lib/associate_jsonb/arel_nodes/sql_casted_equality.rb
144
+ - lib/associate_jsonb/associations/alias_tracker.rb
134
145
  - lib/associate_jsonb/associations/association.rb
135
146
  - lib/associate_jsonb/associations/association_scope.rb
136
147
  - lib/associate_jsonb/associations/belongs_to_association.rb
@@ -139,9 +150,22 @@ files:
139
150
  - lib/associate_jsonb/associations/builder/has_one.rb
140
151
  - lib/associate_jsonb/associations/conflicting_association.rb
141
152
  - lib/associate_jsonb/associations/has_many_association.rb
153
+ - lib/associate_jsonb/associations/join_dependency.rb
142
154
  - lib/associate_jsonb/associations/preloader/association.rb
155
+ - lib/associate_jsonb/attribute_methods.rb
156
+ - lib/associate_jsonb/attribute_methods/read.rb
143
157
  - lib/associate_jsonb/connection_adapters.rb
144
- - 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
145
169
  - lib/associate_jsonb/reflection.rb
146
170
  - lib/associate_jsonb/relation/where_clause.rb
147
171
  - lib/associate_jsonb/supported_rails_version.rb
@@ -1,12 +0,0 @@
1
- # encoding: utf-8
2
- # frozen_string_literal: true
3
-
4
- module AssociateJsonb
5
- module ArelNodeExtensions
6
- module Binary
7
- def original_left
8
- left
9
- end
10
- end
11
- end
12
- end