pg_tags_on 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c61bd469e5458dd169e871a3af323806c36380beba46a575506d33cd18c730dd
4
- data.tar.gz: c88ad1665a25c7e9ec406de248bdd0e845c5a1fe20d8fffd93e7e979a5a227fd
3
+ metadata.gz: cf52d9810e0c765b90218fc5ef9f4bbd5ceb3f9f092e7fa4ade42d946165a8a1
4
+ data.tar.gz: 9911f4f06c06b5f3673220292b6cc0e36122a5ad2a86b3d587ed2565e8c66080
5
5
  SHA512:
6
- metadata.gz: 05c51681cf87704fb8363f147470e5d4ce85a1c7390f064c055f7da19cdb79a419d046eed1432992ca0e002afe50685fff0d1f84f14b472f8ebc4bf6e7df5d44
7
- data.tar.gz: 7481b613c53054dd15ba0cef620dd25ba3f1d09d8b10b5d5a69d03ddc1f02c5c3cedca1f9787bc68fdbb925dd612ff0bc18ca6b1b7413fce70aebb81e50bb887
6
+ metadata.gz: 572db674007f2555744fb8420a847ae813caac80a2f9a803399f90207be5bf0f1c54efc536259d5395eb69248bba8af76ae345623e0972cebe782084d397132f
7
+ data.tar.gz: 8731695a2ac40afcba1dcf22d410fb78234b29ff67033d443a121ff2baf4838fab4d9859ac670d5713d718d91d416177246c5e50b5b08e4e066594ff97cc5727
@@ -7,7 +7,7 @@ module PgTagsOn
7
7
  # Transforms value in Hash if :key option is set
8
8
  def value
9
9
  @value ||= begin
10
- return query_value unless key?
10
+ return query_value unless nested?
11
11
 
12
12
  query_value.each.map do |val|
13
13
  key.reverse.inject(val) { |a, n| { n => a } }
@@ -23,7 +23,7 @@ module PgTagsOn
23
23
  @key ||= Array.wrap(settings[:key])
24
24
  end
25
25
 
26
- def key?
26
+ def nested?
27
27
  key.present?
28
28
  end
29
29
  end
@@ -13,11 +13,9 @@ module PgTagsOn
13
13
  }.freeze
14
14
 
15
15
  def left
16
- node = arel_function('array_to_json', attribute)
17
- node = arel_cast(node, 'jsonb')
18
- node = arel_function('jsonb_path_query_array', node, arel_build_quoted("$[*].#{key.join('.')}"))
19
-
20
- node
16
+ arel_function 'jsonb_path_query_array',
17
+ arel_cast(arel_function('array_to_json', attribute), 'jsonb'),
18
+ arel_build_quoted("$[*].#{key.join('.')}")
21
19
  end
22
20
 
23
21
  def right
@@ -2,7 +2,7 @@
2
2
 
3
3
  module PgTagsOn
4
4
  # Models' predicate handlers register this class
5
- class PredicateHandler < ::ActiveRecord::PredicateBuilder::BaseHandler
5
+ class PredicateHandler < ::ActiveRecord::PredicateBuilder::BasicObjectHandler
6
6
  def call(attribute, query)
7
7
  handler = Builder.new(attribute, query, predicate_builder).call
8
8
 
@@ -21,7 +21,7 @@ module PgTagsOn
21
21
  if column.array?
22
22
  array_handler
23
23
  else
24
- BaseHandler.new(attribute, query, predicate_builder)
24
+ BasicObjectHandler.new attribute, query, predicate_builder
25
25
  end
26
26
  end
27
27
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module Repositories
5
+ module ArrayJsonb
6
+ # This class is respnsible to create tags stored as JSON objects.
7
+ class Create
8
+ include ::PgTagsOn::ActiveRecord::Arel
9
+
10
+ delegate :column_name, :arel_column, :bind_for, to: :repository
11
+
12
+ def call(repository:, relation:, tags:, returning: nil)
13
+ @repository = repository
14
+ value = arel_array_cat arel_column, bind_for(Array.wrap(tags))
15
+
16
+ repository.update_attributes relation: relation,
17
+ attributes: { column_name => value },
18
+ returning: returning
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :repository
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module Repositories
5
+ module ArrayJsonb
6
+ # This class is respnsible to delete tags stored as JSON objects.
7
+ class Delete
8
+ include ::PgTagsOn::ActiveRecord::Arel
9
+
10
+ delegate :column_name, :tag_alias,
11
+ :arel_column, :bind_for, to: :repository
12
+
13
+ def call(repository:, relation:, tags:, returning: nil)
14
+ @repository = repository
15
+ @tags = tags
16
+
17
+ repository.update_attributes relation: relation,
18
+ attributes: { column_name => arel_function('array', active_tags) },
19
+ returning: returning
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :repository, :tags
25
+
26
+ ##
27
+ # For each record explode tags and filter out tags to be removed
28
+ #
29
+ def active_tags
30
+ query = Arel::SelectManager.new
31
+ .project(tag_alias)
32
+ .from(Arel::SelectManager.new
33
+ .project(arel_unnest(arel_column).as(tag_alias))
34
+ .as('update_tags'))
35
+ Array.wrap(tags).each do |tag|
36
+ query.where arel_infix_operation('@>', Arel.sql(tag_alias), bind_for(tag.to_json, nil)).not
37
+ end
38
+
39
+ query
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module Repositories
5
+ module ArrayJsonb
6
+ # This class is respnsible to update tags stored as JSON objects.
7
+ class Update
8
+ include ::PgTagsOn::ActiveRecord::Arel
9
+
10
+ delegate :table_name, :column_name, :tag_alias,
11
+ :arel_table, :arel_column, :cast_type, :bind_for, to: :repository
12
+
13
+ def call(repository:, relation:, tag:, new_tag:, returning: nil)
14
+ @repository = repository
15
+ @relation = relation
16
+ sql = <<-SQL
17
+ WITH tag_positions AS ( #{select_tag_positions(tag).to_sql} )
18
+ UPDATE #{table_name}
19
+ SET #{column_name}[index] = #{column_name}[index] || $2
20
+ FROM tag_positions
21
+ WHERE #{table_name}.id = tag_positions.id
22
+ SQL
23
+ sql += " RETURNING #{returning}" if returning.present?
24
+ bindings = [
25
+ arel_query_attribute(arel_column, tag.to_json, cast_type),
26
+ arel_query_attribute(arel_column, new_tag.to_json, cast_type)
27
+ ]
28
+
29
+ result = relation.connection.exec_query(sql, 'SQL', bindings).rows
30
+
31
+ returning ? result : true
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :repository, :relation
37
+
38
+ ##
39
+ # For each record explode tags and their position index
40
+ # and select only occurences of the given tag value
41
+ #
42
+ # @param tag [String|Hash] Normalized tag value.
43
+ #
44
+ def select_tag_positions(tag)
45
+ condition1 = arel_infix_operation '@>',
46
+ Arel::Table.new('t')[tag_alias],
47
+ bind_for(tag.to_json, nil)
48
+ condition2 = arel_table[:id].in arel_sql(relation.reselect('id').to_sql)
49
+
50
+ arel_table
51
+ .project("id, #{tag_alias}, index")
52
+ .from("#{table_name}, #{arel_unnest(arel_column).to_sql} WITH ORDINALITY t(#{tag_alias}, index)")
53
+ .where(condition1)
54
+ .where(condition2)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -4,107 +4,134 @@ module PgTagsOn
4
4
  module Repositories
5
5
  # Operatons for 'jsonb[]' column type
6
6
  class ArrayJsonbRepository < ArrayRepository
7
- def create(tag_or_tags, returning: nil)
8
- tags = normalize_tags(tag_or_tags)
7
+ ##
8
+ # Tag's key in the JSON object.
9
+ # Can be a String or Array for nested attributes.
10
+ #
11
+ # @example
12
+ # { "name" => "lorem" } ; key = 'tag'
13
+ # { "tag" => { "name" => "lorem" } } ; key = ['tag', 'name']
14
+ #
15
+ def key
16
+ @key ||= options[:key]
17
+ end
9
18
 
10
- super(tags, returning: returning)
19
+ ##
20
+ # Returns true if JSON object has nested attributes.
21
+ #
22
+ def nested?
23
+ key.present?
11
24
  end
12
25
 
13
- def update(tag, new_tag, returning: nil)
14
- n_tag, n_new_tag = normalize_tags([tag, new_tag])
26
+ ##
27
+ # Add tag(s) to records.
28
+ #
29
+ # @param tag_or_tags [String|Array] Tags to be added.
30
+ # @param returning [String] Model's columns that'll be returned.
31
+ #
32
+ # @return [Array|Boolean] Returns boolean if :returning argument is nil
33
+ #
34
+ # @example
35
+ # Entity.tags.create "lorem", returning: "id,name"
36
+ # => [[1, 'row1'], [2, 'row2']]
37
+ #
38
+ def create(tag_or_tags, returning: nil)
39
+ tags = normalize_tags tag_or_tags
15
40
 
16
- sql_set = <<-SQL.strip
17
- #{column_name}[index] = #{column_name}[index] || $2
18
- SQL
41
+ ArrayJsonb::Create.new.call repository: self,
42
+ relation: klass,
43
+ tags: tags,
44
+ returning: returning
45
+ end
19
46
 
20
- update_tag(n_tag,
21
- sql_set,
22
- bindings: [query_attribute(n_new_tag.to_json)],
23
- returning: returning)
47
+ ##
48
+ # Rename tag(s).
49
+ #
50
+ # @param tag [String] Tag that'll be renamed.
51
+ # @param new_tag [String] New tag name.
52
+ # @param returning [String] Model's columns that'll be returned.
53
+ #
54
+ # @return [Array|Boolean] Returns boolean if :returning argument is nil
55
+ #
56
+ # @example
57
+ # Entity.tags.update "lorem", "ipsum", returning: "id,name"
58
+ # => [[1, 'row1'], [2, 'row2']]
59
+ #
60
+ def update(tag, new_tag, returning: nil)
61
+ tag = normalize_one_tag tag
62
+ new_tag = normalize_one_tag new_tag
63
+
64
+ ArrayJsonb::Update.new.call repository: self,
65
+ relation: klass,
66
+ tag: tag,
67
+ new_tag: new_tag,
68
+ returning: returning
24
69
  end
25
70
 
71
+ ##
72
+ # Delete tag(s).
73
+ #
74
+ # @param tag_or_tags [String] Tag that'll be renamed.
75
+ # @param returning [String] Model's columns that'll be returned.
76
+ #
77
+ # @return [Array|Boolean] Returns boolean if :returning argument is nil
78
+ #
79
+ # @example
80
+ # Entity.tags.delete "lorem", returning: "id,name"
81
+ # => [[1, 'row1'], [2, 'row2']]
82
+ #
26
83
  def delete(tag_or_tags, returning: nil)
27
- tags = normalize_tags(tag_or_tags)
28
- sm = build_tags_select_manager
84
+ tags = normalize_tags tag_or_tags
85
+ relation = klass.where(column_name => PgTagsOn.query_class.any(tag_or_tags))
29
86
 
30
- tags.each do |tag|
31
- sm.where(arel_infix_operation('@>', Arel.sql('tag'), bind_for(tag.to_json, nil)).not)
32
- end
33
-
34
- rel = klass.where(column_name => PgTagsOn.query_class.any(tag_or_tags))
35
- value = arel_function('array', sm)
36
-
37
- perform_update(rel, { column_name => value }, returning: returning)
87
+ ArrayJsonb::Delete.new.call repository: self,
88
+ relation: relation,
89
+ tags: tags,
90
+ returning: returning
38
91
  end
39
92
 
40
- private
93
+ def normalize_tags(tags)
94
+ return normalize_one_tag(tags) unless tags.is_a?(Array)
41
95
 
42
- # Returns SelectManager for unnested tags.
43
- # sql: select tag from ( select unnest(tags_jsonb) as tag ) as _tags
44
- def build_tags_select_manager
45
- Arel::SelectManager.new
46
- .project('tag')
47
- .from(Arel::SelectManager.new
48
- .project(unnest.as('tag'))
49
- .as('_tags'))
96
+ tags.map { |t| normalize_one_tag(t) }
50
97
  end
51
98
 
52
- def normalize_tags(tag_or_tags)
53
- tags = Array.wrap(tag_or_tags)
54
-
55
- return tags unless key?
99
+ def normalize_one_tag(tag)
100
+ return tag unless nested?
101
+ return { key => tag } unless key.is_a?(Array)
56
102
 
57
- tags.map do |tag|
58
- Array.wrap(key).reverse.inject(tag) { |a, n| { n => a } }
59
- end
103
+ key.reverse.inject(tag) { |a, k| { k => a } }
60
104
  end
61
105
 
62
- def array_to_recordset
63
- return unnest unless key?
106
+ ##
107
+ # @return [Arel::SelectManager]
108
+ #
109
+ def select_tags
110
+ return super unless nested?
64
111
 
65
- arel_jsonb_extract_path(unnest, *key_sql)
112
+ klass
113
+ .select(arel_distinct(extract_tag_by_path).as(tag_alias))
114
+ .arel
66
115
  end
67
116
 
68
- def key
69
- @key ||= options[:key]
70
- end
117
+ ##
118
+ # @return [Arel::SelectManager]
119
+ #
120
+ def select_taggings
121
+ return super unless nested?
71
122
 
72
- def key_sql
73
- @key_sql ||= Array.wrap(key).map { |k| Arel.sql("'#{k}'") }
123
+ klass
124
+ .select(extract_tag_by_path.as(tag_alias),
125
+ arel_table['id'].as('entity_id'))
126
+ .arel
74
127
  end
75
128
 
76
- def key?
77
- key.present?
129
+ def tag_path_array
130
+ @tag_path_array ||= Array.wrap(key).map { |k| Arel.sql("'#{k}'") }
78
131
  end
79
132
 
80
- def taggings_with_ordinality_query(tag)
81
- column = Arel::Table.new('t')['name']
82
- value = bind_for(tag.to_json, nil)
83
-
84
- arel_table
85
- .project('id, name, index')
86
- .from("#{table_name}, #{unnest_with_ordinality}")
87
- .where(arel_infix_operation('@>', column, value))
88
- end
89
-
90
- def update_tag(tag, set_sql, bindings: [], returning: nil)
91
- subquery = taggings_with_ordinality_query(tag)
92
- .where(arel_table[:id].in(arel_sql(klass.reselect('id').to_sql)))
93
-
94
- sql = <<-SQL.strip
95
- WITH records as ( #{subquery.to_sql} )
96
- UPDATE #{table_name}
97
- SET #{set_sql}
98
- FROM records
99
- WHERE #{table_name}.id = records.id
100
- SQL
101
-
102
- sql += " RETURNING #{Array.wrap(returning).join(', ')}" if returning.present?
103
-
104
- bindings = [query_attribute(tag.to_json)] + Array.wrap(bindings)
105
- result = klass.connection.exec_query(sql, 'SQL', bindings).rows
106
-
107
- returning ? result : true
133
+ def extract_tag_by_path
134
+ arel_jsonb_extract_path arel_unnest(arel_column), *tag_path_array
108
135
  end
109
136
  end
110
137
  end
@@ -4,112 +4,163 @@ module PgTagsOn
4
4
  module Repositories
5
5
  # This repository works with "character varying[]" and "integer[]" column types
6
6
  class ArrayRepository < BaseRepository
7
+ ##
8
+ # Select all tags ordered by name
9
+ #
10
+ # @return [Array[PgTagsOn::Tag]] Tags list.
11
+ #
12
+ # @example
13
+ # Entity.tags.all
14
+ #
7
15
  def all
8
- subquery = klass
9
- .select(arel_distinct(array_to_recordset).as('name'))
10
- .arel
11
- .as('tags')
12
-
13
16
  PgTagsOn::Tag
14
17
  .select(Arel.star)
15
- .from(subquery)
16
- .order('tags.name') # override rails' default order by id
17
- end
18
-
18
+ .from(select_tags.as('tags'))
19
+ .order(tag_alias)
20
+ end
21
+
22
+ ##
23
+ # Select all tags with counts
24
+ #
25
+ # @return [Array[PgTagsOn::Tag]] Tags list.
26
+ #
27
+ # @example
28
+ # Entity.tags.all_with_counts
29
+ #
19
30
  def all_with_counts
20
31
  taggings
21
- .except(:select)
22
- .select('name, count(name) as count')
23
- .group('name')
32
+ .reselect("#{tag_alias}, count(*) as count")
33
+ .group(tag_alias)
24
34
  end
25
35
 
36
+ ##
37
+ # Find one tag.
38
+ #
39
+ # @return [PgTagsOn::Tag]
40
+ #
26
41
  def find(tag)
27
42
  all.where(name: tag).first
28
43
  end
29
44
 
45
+ ##
46
+ # Returns true if tag exists.
47
+ #
48
+ # @return [Boolean]
49
+ #
30
50
  def exists?(tag)
31
- all.exists?(tag)
51
+ all.exists? tag
32
52
  end
33
53
 
54
+ ##
55
+ # Select taggings.
56
+ #
57
+ # @return [[PgTagsOn::Tag<entity_id, name>]]
58
+ #
34
59
  def taggings
35
60
  PgTagsOn::Tag
36
61
  .select(Arel.star)
37
- .from(taggings_query)
38
- .order('taggings.name')
62
+ .from(select_taggings.as('taggings'))
63
+ .order(tag_alias)
39
64
  end
40
65
 
66
+ ##
67
+ # Get tags count.
68
+ #
41
69
  def count
42
70
  all.count
43
71
  end
44
72
 
73
+ ##
74
+ # Add tag(s) to records.
75
+ #
76
+ # @param tag_or_tags [String|Array] Tags to be added.
77
+ # @param returning [String] Model's columns that'll be returned.
78
+ #
79
+ # @return [Array|Boolean] Returns boolean if :returning argument is nil
80
+ #
81
+ # @example
82
+ # Entity.tags.create "lorem", returning: "id,name"
83
+ # => [[1, 'row1'], [2, 'row2']]
84
+ #
45
85
  def create(tag_or_tags, returning: nil)
46
- value = arel_array_cat(arel_column, bind_for(Array.wrap(tag_or_tags)))
47
-
48
- perform_update(klass, { column_name => value }, returning: returning)
49
- end
50
-
86
+ ArrayValue::Create.new.call repository: self,
87
+ relation: klass,
88
+ tags: tag_or_tags,
89
+ returning: returning
90
+ end
91
+
92
+ ##
93
+ # Rename tag(s).
94
+ #
95
+ # @param tag [String] Tag that'll be renamed.
96
+ # @param new_tag [String] New tag name.
97
+ # @param returning [String] Model's columns that'll be returned.
98
+ #
99
+ # @return [Array|Boolean] Returns boolean if :returning argument is nil
100
+ #
101
+ # @example
102
+ # Entity.tags.update "lorem", "ipsum", returning: "id,name"
103
+ # => [[1, 'row1'], [2, 'row2']]
104
+ #
51
105
  def update(tag, new_tag, returning: nil)
52
- rel = klass.where(column_name => PgTagsOn.query_class.one(tag))
53
- value = arel_array_replace(arel_column, bind_for(tag), bind_for(new_tag))
106
+ relation = klass.where(column_name => PgTagsOn.query_class.one(tag))
107
+
108
+ ArrayValue::Update.new.call repository: self,
109
+ relation: relation,
110
+ tag: tag,
111
+ new_tag: new_tag,
112
+ returning: returning
113
+ end
114
+
115
+ ##
116
+ # Delete tag(s).
117
+ #
118
+ # @param tag_or_tags [String] Tag that'll be renamed.
119
+ # @param returning [String] Model's columns that'll be returned.
120
+ #
121
+ # @return [Array|Boolean] Returns boolean if :returning argument is nil
122
+ #
123
+ # @example
124
+ # Entity.tags.delete "lorem", returning: "id,name"
125
+ # => [[1, 'row1'], [2, 'row2']]
126
+ #
127
+ def delete(tag_or_tags, returning: nil)
128
+ relation = klass.where(column_name => PgTagsOn.query_class.any(tag_or_tags))
54
129
 
55
- perform_update(rel, { column_name => value }, returning: returning)
130
+ ArrayValue::Delete.new.call repository: self,
131
+ relation: relation,
132
+ tags: tag_or_tags,
133
+ returning: returning
56
134
  end
57
135
 
58
- def delete(tag_or_tags, returning: nil)
59
- tags = Array.wrap(tag_or_tags)
60
- rel = klass.where(column_name => PgTagsOn.query_class.any(tags))
61
- sm = Arel::SelectManager.new
62
- .project(unnest)
63
- .except(
64
- Arel::SelectManager.new
65
- .project(arel_unnest(arel_cast(bind_for(tags), native_column_type)))
66
- )
67
- value = arel_function('array', sm)
68
-
69
- perform_update(rel, { column_name => value }, returning: returning)
136
+ def tag_alias
137
+ 'name'
70
138
  end
71
139
 
72
- private
73
-
74
- def perform_update(rel, updates, returning: nil)
75
- update_manager = get_update_manager(rel, updates)
76
- sql, binds = klass.connection.send :to_sql_and_binds, update_manager
77
- sql += " RETURNING #{Array.wrap(returning).join(', ')}" if returning.present?
78
-
79
- result = klass.connection.exec_query(sql, 'SQL', binds).rows
140
+ # protected
80
141
 
81
- returning ? result : true
82
- end
83
-
84
- def taggings_query
142
+ ##
143
+ # @return [Arel::SelectManager]
144
+ #
145
+ def select_tags
85
146
  klass
86
- .select(
87
- array_to_recordset.as('name'),
88
- arel_table['id'].as('entity_id')
89
- )
147
+ .select(arel_distinct(arel_unnest(arel_column)).as(tag_alias))
90
148
  .arel
91
- .as('taggings')
92
- end
93
-
94
- def unnest
95
- arel_unnest(arel_column)
96
- end
97
-
98
- def array_to_recordset
99
- unnest
100
- end
101
-
102
- def unnest_with_ordinality(alias_table: 't')
103
- "#{unnest.to_sql} WITH ORDINALITY #{alias_table}(name, index)"
104
149
  end
105
150
 
106
- def query_attribute(value)
107
- arel_query_attribute(arel_column, value, cast_type)
151
+ ##
152
+ # @return [Arel::SelectManager]
153
+ #
154
+ def select_taggings
155
+ klass
156
+ .select(arel_unnest(arel_column).as(tag_alias),
157
+ arel_table['id'].as('entity_id'))
158
+ .arel
108
159
  end
109
160
 
110
161
  def bind_for(value, attr = arel_column)
111
- query_attr = arel_query_attribute(attr, value, cast_type)
112
- arel_bind(query_attr)
162
+ query_attr = arel_query_attribute attr, value, cast_type
163
+ arel_bind query_attr
113
164
  end
114
165
  end
115
166
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module Repositories
5
+ module ArrayValue
6
+ # This class is respnsible to delete tags stored as JSON objects.
7
+ class Create
8
+ include ::PgTagsOn::ActiveRecord::Arel
9
+
10
+ delegate :column_name, :arel_column, :bind_for, to: :repository
11
+
12
+ def call(repository:, relation:, tags:, returning: nil)
13
+ @repository = repository
14
+ value = arel_array_cat arel_column, bind_for(Array.wrap(tags))
15
+
16
+ repository.update_attributes relation: relation,
17
+ attributes: { column_name => value },
18
+ returning: returning
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :repository, :tags
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module Repositories
5
+ module ArrayValue
6
+ # This class is respnsible to delete tags stored as JSON objects.
7
+ class Delete
8
+ include ::PgTagsOn::ActiveRecord::Arel
9
+
10
+ delegate :column_name, :arel_column, :bind_for, :native_column_type, to: :repository
11
+
12
+ def call(repository:, relation:, tags:, returning: nil)
13
+ @repository = repository
14
+ @tags = tags
15
+
16
+ repository.update_attributes relation: relation,
17
+ attributes: { column_name => arel_function('array', active_tags) },
18
+ returning: returning
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :repository, :tags
24
+
25
+ ##
26
+ # For each record explode tags and filter out tags to be removed
27
+ #
28
+ def active_tags
29
+ Arel::SelectManager.new
30
+ .project(arel_unnest(arel_column))
31
+ .except(Arel::SelectManager.new
32
+ .project(arel_unnest(arel_cast(bind_for(Array.wrap(tags)), native_column_type))))
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module Repositories
5
+ module ArrayValue
6
+ # This class is respnsible to delete tags stored as JSON objects.
7
+ class Update
8
+ include ::PgTagsOn::ActiveRecord::Arel
9
+
10
+ delegate :column_name, :arel_column, :bind_for, to: :repository
11
+
12
+ def call(repository:, relation:, tag:, new_tag:, returning: nil)
13
+ @repository = repository
14
+ value = arel_array_replace arel_column, bind_for(tag), bind_for(new_tag)
15
+
16
+ repository.update_attributes relation: relation,
17
+ attributes: { column_name => value },
18
+ returning: returning
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :repository, :tags
24
+ end
25
+ end
26
+ end
27
+ end
@@ -36,22 +36,27 @@ module PgTagsOn
36
36
  arel_table[column_name]
37
37
  end
38
38
 
39
- # Returns ActiveRecord Column instance
39
+ ##
40
+ # ActiveRecord Column instance
41
+ #
40
42
  def ar_column
41
43
  @ar_column ||= klass.columns_hash[column_name.to_s]
42
44
  end
43
45
 
44
- def ref
45
- "#{table_name}.#{column_name}"
46
- end
47
-
48
- # Returns Type instance
46
+ ##
47
+ # Column's Type instance
48
+ #
49
+ # @return [ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array]
50
+ #
49
51
  def cast_type
50
52
  @cast_type ||= klass.type_for_attribute(column_name)
51
53
  end
52
54
 
53
- # Returns db type as string.
54
- # Ex: character varying[], integer[]
55
+ ##
56
+ # Database column type as string.
57
+ #
58
+ # @return [String] "character varying[]", "integer[]"
59
+ #
55
60
  def native_column_type
56
61
  type = ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES.fetch(cast_type.type)[:name]
57
62
  type += '[]' if ar_column.array?
@@ -60,20 +65,30 @@ module PgTagsOn
60
65
  end
61
66
 
62
67
  # Method copied from ActiveRecord as there is no way to inject sql into update manager.
63
- def get_update_manager(rel, updates)
68
+ def get_update_manager(relation:, updates:)
64
69
  raise ArgumentError, 'Empty list of attributes to change' if updates.blank?
65
70
 
66
71
  stmt = ::Arel::UpdateManager.new
67
72
  stmt.table(arel_table)
68
73
  stmt.key = arel_table[klass.primary_key]
69
- stmt.take(rel.arel.limit)
70
- stmt.offset(rel.arel.offset)
71
- stmt.order(*rel.arel.orders)
72
- stmt.wheres = rel.arel.constraints
73
- stmt.set rel.send(:_substitute_values, updates)
74
+ stmt.take(relation.arel.limit)
75
+ stmt.offset(relation.arel.offset)
76
+ stmt.order(*relation.arel.orders)
77
+ stmt.wheres = relation.arel.constraints
78
+ stmt.set relation.send(:_substitute_values, updates)
74
79
 
75
80
  stmt
76
81
  end
82
+
83
+ def update_attributes(relation:, attributes:, returning: nil)
84
+ manager = get_update_manager relation: relation, updates: attributes
85
+ sql, binds = klass.connection.send :to_sql_and_binds, manager
86
+ sql += " RETURNING #{returning}" if returning.present?
87
+
88
+ result = klass.connection.exec_query(sql, 'SQL', binds).rows
89
+
90
+ returning ? result : true
91
+ end
77
92
  end
78
93
  end
79
94
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgTagsOn
4
+ ##
4
5
  # Repository class for tags.
6
+ #
5
7
  # Examples:
6
8
  #
7
9
  # repo = PgTagsOn::Repository.new(Entity, :tags)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgTagsOn
4
+ ##
4
5
  # Helper class to construct queries.
5
6
  # This class is registered in models' predicate builders.
6
7
  # See configuration in order to create an alias for it.
@@ -14,9 +15,7 @@ module PgTagsOn
14
15
  RUBY
15
16
  end
16
17
 
17
- attr_reader :value
18
- attr_reader :predicate
19
- attr_reader :options
18
+ attr_reader :value, :predicate, :options
20
19
 
21
20
  def initialize(value, predicate, options = {})
22
21
  @value = value
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgTagsOn
4
- VERSION = '0.4.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/pg_tags_on.rb CHANGED
@@ -18,7 +18,13 @@ require 'pg_tags_on/validations/validator'
18
18
  require 'pg_tags_on/repository'
19
19
  require 'pg_tags_on/repositories/base_repository'
20
20
  require 'pg_tags_on/repositories/array_repository'
21
+ require 'pg_tags_on/repositories/array_value/create'
22
+ require 'pg_tags_on/repositories/array_value/update'
23
+ require 'pg_tags_on/repositories/array_value/delete'
21
24
  require 'pg_tags_on/repositories/array_jsonb_repository'
25
+ require 'pg_tags_on/repositories/array_jsonb/create'
26
+ require 'pg_tags_on/repositories/array_jsonb/update'
27
+ require 'pg_tags_on/repositories/array_jsonb/delete'
22
28
  require 'pg_tags_on/benchmark/benchmark'
23
29
 
24
30
  # PgTagsOn configuration methods
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_tags_on
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Catalin Marinescu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-02 00:00:00.000000000 Z
11
+ date: 2022-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '6.1'
19
+ version: '7.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '6.1'
26
+ version: '7.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '6.1'
33
+ version: '7.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '6.1'
40
+ version: '7.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: pg
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: '1.2'
47
+ version: 1.2.2
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: '1.2'
54
+ version: 1.2.2
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -106,8 +106,14 @@ files:
106
106
  - lib/pg_tags_on/predicate_handler/array_string_handler.rb
107
107
  - lib/pg_tags_on/predicate_handler/array_text_handler.rb
108
108
  - lib/pg_tags_on/predicate_handler/base_handler.rb
109
+ - lib/pg_tags_on/repositories/array_jsonb/create.rb
110
+ - lib/pg_tags_on/repositories/array_jsonb/delete.rb
111
+ - lib/pg_tags_on/repositories/array_jsonb/update.rb
109
112
  - lib/pg_tags_on/repositories/array_jsonb_repository.rb
110
113
  - lib/pg_tags_on/repositories/array_repository.rb
114
+ - lib/pg_tags_on/repositories/array_value/create.rb
115
+ - lib/pg_tags_on/repositories/array_value/delete.rb
116
+ - lib/pg_tags_on/repositories/array_value/update.rb
111
117
  - lib/pg_tags_on/repositories/base_repository.rb
112
118
  - lib/pg_tags_on/repository.rb
113
119
  - lib/pg_tags_on/tag.rb
@@ -130,14 +136,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
130
136
  requirements:
131
137
  - - ">="
132
138
  - !ruby/object:Gem::Version
133
- version: 2.5.0
139
+ version: 2.7.0
134
140
  required_rubygems_version: !ruby/object:Gem::Requirement
135
141
  requirements:
136
142
  - - ">="
137
143
  - !ruby/object:Gem::Version
138
144
  version: '0'
139
145
  requirements: []
140
- rubygems_version: 3.0.9
146
+ rubygems_version: 3.1.6
141
147
  signing_key:
142
148
  specification_version: 4
143
149
  summary: Query and manage tags stored in a Postgresql column.