pg_tags_on 0.4.0 → 1.0.0

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.
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.