associate_jsonb 0.0.4 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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