associate_jsonb 0.0.3 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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 +168 -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 +54 -31
  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.3"
5
+ VERSION = "0.0.9"
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,44 +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)"
109
- on_store_change = ->(var) {
110
- "write_attribute(:#{attribute}, #{
111
- is_array \
112
- ? "Array(#{var})" \
113
- : var
114
- })"
115
- }
116
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)})"
117
129
 
118
130
  if is_array
119
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
131
+ mixin.class_eval <<~CODE, __FILE__, __LINE__ + 1
120
132
  def #{attribute}
121
133
  _read_attribute(:#{attribute}) || []
122
134
  end
123
135
  CODE
124
136
  end
125
137
 
126
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
138
+ mixin.class_eval <<~CODE, __FILE__, __LINE__ + 1
127
139
  def #{store}=(given)
128
- super(given || {})
129
- write_attribute(:#{attribute}, #{on_store_change.call %Q(#{store}["#{key}"])})
130
- if #{attribute}.blank?
131
- #{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}")
132
154
  else
133
- #{store}["#{key}"] = #{attribute}
155
+ super given || {}
156
+ self.#{attribute}= #{store}["#{key}"]
134
157
  end
158
+
135
159
  #{store}
136
160
  end
137
161
 
138
162
  def #{attribute}=(given)
139
163
  #{on_attr_change}
140
- value = #{store}["#{key}"] = #{attribute}
141
- #{store}.delete("#{key}") if value.nil?
142
- _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}"])
143
166
  value
144
167
  end
145
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.3
4
+ version: 0.0.9
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-13 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