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 +4 -4
- data/lib/pg_tags_on/predicate_handler/array_jsonb_handler.rb +2 -2
- data/lib/pg_tags_on/predicate_handler/array_jsonb_with_attrs_handler.rb +3 -5
- data/lib/pg_tags_on/predicate_handler.rb +2 -2
- data/lib/pg_tags_on/repositories/array_jsonb/create.rb +27 -0
- data/lib/pg_tags_on/repositories/array_jsonb/delete.rb +44 -0
- data/lib/pg_tags_on/repositories/array_jsonb/update.rb +59 -0
- data/lib/pg_tags_on/repositories/array_jsonb_repository.rb +103 -76
- data/lib/pg_tags_on/repositories/array_repository.rb +119 -68
- data/lib/pg_tags_on/repositories/array_value/create.rb +27 -0
- data/lib/pg_tags_on/repositories/array_value/delete.rb +37 -0
- data/lib/pg_tags_on/repositories/array_value/update.rb +27 -0
- data/lib/pg_tags_on/repositories/base_repository.rb +29 -14
- data/lib/pg_tags_on/repository.rb +2 -0
- data/lib/pg_tags_on/tags_query.rb +2 -3
- data/lib/pg_tags_on/version.rb +1 -1
- data/lib/pg_tags_on.rb +6 -0
- metadata +18 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cf52d9810e0c765b90218fc5ef9f4bbd5ceb3f9f092e7fa4ade42d946165a8a1
|
4
|
+
data.tar.gz: 9911f4f06c06b5f3673220292b6cc0e36122a5ad2a86b3d587ed2565e8c66080
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
-
|
17
|
-
|
18
|
-
|
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::
|
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
|
-
|
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
|
-
|
8
|
-
|
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
|
-
|
19
|
+
##
|
20
|
+
# Returns true if JSON object has nested attributes.
|
21
|
+
#
|
22
|
+
def nested?
|
23
|
+
key.present?
|
11
24
|
end
|
12
25
|
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
41
|
+
ArrayJsonb::Create.new.call repository: self,
|
42
|
+
relation: klass,
|
43
|
+
tags: tags,
|
44
|
+
returning: returning
|
45
|
+
end
|
19
46
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
28
|
-
|
84
|
+
tags = normalize_tags tag_or_tags
|
85
|
+
relation = klass.where(column_name => PgTagsOn.query_class.any(tag_or_tags))
|
29
86
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
93
|
+
def normalize_tags(tags)
|
94
|
+
return normalize_one_tag(tags) unless tags.is_a?(Array)
|
41
95
|
|
42
|
-
|
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
|
53
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
106
|
+
##
|
107
|
+
# @return [Arel::SelectManager]
|
108
|
+
#
|
109
|
+
def select_tags
|
110
|
+
return super unless nested?
|
64
111
|
|
65
|
-
|
112
|
+
klass
|
113
|
+
.select(arel_distinct(extract_tag_by_path).as(tag_alias))
|
114
|
+
.arel
|
66
115
|
end
|
67
116
|
|
68
|
-
|
69
|
-
|
70
|
-
|
117
|
+
##
|
118
|
+
# @return [Arel::SelectManager]
|
119
|
+
#
|
120
|
+
def select_taggings
|
121
|
+
return super unless nested?
|
71
122
|
|
72
|
-
|
73
|
-
|
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
|
77
|
-
key.
|
129
|
+
def tag_path_array
|
130
|
+
@tag_path_array ||= Array.wrap(key).map { |k| Arel.sql("'#{k}'") }
|
78
131
|
end
|
79
132
|
|
80
|
-
def
|
81
|
-
|
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(
|
16
|
-
.order(
|
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
|
-
.
|
22
|
-
.
|
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?
|
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(
|
38
|
-
.order(
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
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
|
59
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
def
|
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
|
-
|
107
|
-
|
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
|
112
|
-
arel_bind
|
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
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
#
|
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
|
-
|
54
|
-
#
|
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(
|
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(
|
70
|
-
stmt.offset(
|
71
|
-
stmt.order(*
|
72
|
-
stmt.wheres =
|
73
|
-
stmt.set
|
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,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
|
data/lib/pg_tags_on/version.rb
CHANGED
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
|
+
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:
|
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: '
|
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: '
|
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: '
|
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: '
|
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:
|
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:
|
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.
|
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.
|
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.
|