pg_tags_on 0.1.4 → 0.2.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: aac909ac252ee49fd469857036866fb22fbc78db87e19f84f8448637a780af6c
4
- data.tar.gz: 4ba45da54dc2782a11ce1cb67b5e61077447f544fcac29c0620c4e261d572ee0
3
+ metadata.gz: 01ed743033dfd067912e4ffe4878cf719e87d920220c656318eb4a780c4f9196
4
+ data.tar.gz: 03f9ea30352f7ced5e6c5b87d343c410a8d6522d7475ee34685c53870a9952f3
5
5
  SHA512:
6
- metadata.gz: 19090081b0fe790204a5384bc85c9e90d29ea824eeb0ace2b103e6a68010379f65fd05e0deef79474a7bbfaa6fdb01b59aaf62d151af2f48317f502a70b43a4c
7
- data.tar.gz: 0d2da6a30730fc639b915ceaf88a47bc6219d9ef111188a8f935f39aae936f6afbb36afbbef624ba6fb4b7f35eede5c89c3d0b3b917e526794c73cc16fa988a0
6
+ metadata.gz: 118f18d7f3dc1e5e7809b03ec03a107499b8be1e66f225b3b40d819f55e95e98e7deaf469fe4f8ad29cd790962082b2a314081ac266009d1d4bc65cf9fd94d86
7
+ data.tar.gz: 7771926231fc5013bce1e01938aeef247e9b602b6308dbfbfdf29d10b6d111bb8743a4fbc0bd97c6d0ad4daf773bb9efc0edcedcaa166e74ea7b8096d32cca12
@@ -1,3 +1,17 @@
1
+ ## 0.2.0 ( 2020-09-02)
2
+
3
+ * Added support for multiple tags creation and deletion
4
+ * Optimizations
5
+ * Updated specs and documentation
6
+
7
+ ## 0.1.4 ( 2020-04-12)
8
+
9
+ * Added support for returning columns after create,update or delete tags operations
10
+ * Updated specs
11
+ * Updated documentation
12
+
13
+
1
14
  ## 0.1.0 ( 2020-03-11 ) ##
2
15
 
3
- * First release
16
+ * First release
17
+
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # PgTagsOn
2
2
 
3
- ```pg_tags_on``` is a gem that makes working with tags stored in a Postgresql column easy. Supported column types are: ```character varying[]```, ```text[]```, ```integer[]``` and ```jsonb[]```.
3
+ ```pg_tags_on``` - keep tags in a column. Supported column types are: ```character varying[]```, ```text[]```, ```integer[]``` and ```jsonb[]```.
4
4
 
5
5
 
6
6
  Requirements:
@@ -15,7 +15,7 @@ Note: this gem is in early stage of development.
15
15
  - [Installation](#installation)
16
16
  - [Usage](#usage)
17
17
  - [ActiveRecord model setup](#activerecord-model-setup)
18
- - [Records queries](#records-queries)
18
+ - [Records queries](#queries)
19
19
  - [Tags](#tags)
20
20
  - [Set record's tags](#set-records-tags)
21
21
  - [Configuration](#configuration)
@@ -140,20 +140,22 @@ Entity.tags.taggings
140
140
  => [#<PgTagsOn::Tag name: "alpha", entity_id: 1>, #<PgTagsOn::Tag name: "beta", entity_id: 1>, #<PgTagsOn::Tag name: "alpha", entity_id: 2>, ... ]
141
141
  ```
142
142
 
143
- Create, update and delete methods are using, for performance reasons, Postgresql functions to manipulate the arrays, so ActiveRecord models are not instantiated. A frequent problem is to ensure uniqueness of the tags for a record, but this can be achieved at the database level by creating a before create/update row trigger.
143
+ Create, update and delete methods are using, for performance reasons, Postgresql functions to manipulate the arrays, so ActiveRecord models are not instantiated. A frequent problem is to ensure uniqueness of the tags for a record, and this can be achieved at the database level by creating a before create/update row trigger.
144
144
 
145
145
  ```ruby
146
146
  # create
147
- Entity.tags.create('alpha') # add tag to all records
148
- Entity.where(...).tags.create('alpha') # add tag to filtered records
147
+ Entity.tags.create('alpha') # add tag to all records
148
+ Entity.tags.create(%w[alpha beta gamma]) # add many tags to all records
149
+ Entity.where(...).tags.create('alpha') # add tag to filtered records
149
150
 
150
151
  # update
151
152
  Entity.tags.update('alpha', 'a') # rename tag for all records
152
153
  Entity.where(...).tags.update('alpha', 'a') # rename tag for filtered records
153
154
 
154
155
  # delete
155
- Entity.tags.delete('alpha') # delete tag from all records
156
- Entity.where(...).tags.delete('alpha') # delete tag from filtered records
156
+ Entity.tags.delete('alpha') # delete tag from all records
157
+ Entity.tags.delete(%w[alpha beta gamma]) # delete many tags from all records
158
+ Entity.where(...).tags.delete('alpha') # delete tag from filtered records
157
159
 
158
160
  # any of these methods accepts :returning option
159
161
  Entity.tags.update('alpha', 'a', returning: %w[id tags])
@@ -4,43 +4,56 @@ module PgTagsOn
4
4
  module Repositories
5
5
  # Operatons for 'jsonb[]' column type
6
6
  class ArrayJsonbRepository < ArrayRepository
7
- def create(tag, returning: nil)
8
- with_normalized_tags(tag) do |n_tag|
9
- super(n_tag, returning: returning)
10
- end
7
+ def create(tag_or_tags, returning: nil)
8
+ tags = normalize_tags(Array.wrap(tag_or_tags))
9
+
10
+ super(tags, returning: returning)
11
11
  end
12
12
 
13
13
  def update(tag, new_tag, returning: nil)
14
- with_normalized_tags(tag, new_tag) do |n_tag, n_new_tag|
15
- sql_set = <<-SQL.strip
16
- #{column_name}[index] = #{column_name}[index] || $2
17
- SQL
18
-
19
- update_tag(n_tag,
20
- sql_set,
21
- bindings: [query_attribute(n_new_tag.to_json)],
22
- returning: returning)
23
- end
24
- end
14
+ n_tag, n_new_tag = normalize_tags([tag, new_tag])
25
15
 
26
- def delete(tag, returning: nil)
27
- with_normalized_tags(tag) do |n_tag|
28
- sql_set = <<-SQL.strip
29
- #{column_name} = #{column_name}[1:index-1] || #{column_name}[index+1:2147483647]
30
- SQL
16
+ sql_set = <<-SQL.strip
17
+ #{column_name}[index] = #{column_name}[index] || $2
18
+ SQL
19
+
20
+ update_tag(n_tag,
21
+ sql_set,
22
+ bindings: [query_attribute(n_new_tag.to_json)],
23
+ returning: returning)
24
+ end
31
25
 
32
- update_tag(n_tag, sql_set, returning: returning)
26
+ def delete(tag_or_tags, returning: nil)
27
+ tags = Array.wrap(tag_or_tags)
28
+ normalized_tags = normalize_tags(tags)
29
+ rel = klass.where(column_name => PgTagsOn.query_class.any(tags))
30
+ sm = build_tags_select_manager
31
+ normalized_tags.each do |tag|
32
+ sm.where(arel_infix_operation('@>', Arel.sql('tag'), bind_for(tag.to_json, nil)).not)
33
33
  end
34
+ value = arel_function('array', sm)
35
+
36
+ perform_update(rel, { column_name => value }, returning: returning)
34
37
  end
35
38
 
36
39
  private
37
40
 
38
- def with_normalized_tags(*tags, &block)
39
- normalized_tags = Array.wrap(tags).flatten.map do |tag|
40
- key? && Array.wrap(key).reverse.inject(tag) { |a, n| { n => a } } || tag
41
- end
41
+ # Returns SelectManager for unnested tags.
42
+ # sql: select tag from ( select unnest(tags_jsonb) as tag ) as _tags
43
+ def build_tags_select_manager
44
+ Arel::SelectManager.new
45
+ .project('tag')
46
+ .from(Arel::SelectManager.new
47
+ .project(unnest.as('tag'))
48
+ .as('_tags'))
49
+ end
42
50
 
43
- block.call(*normalized_tags)
51
+ def normalize_tags(tags)
52
+ return tags unless key?
53
+
54
+ tags.map do |tag|
55
+ Array.wrap(key).reverse.inject(tag) { |a, n| { n => a } }
56
+ end
44
57
  end
45
58
 
46
59
  def array_to_recordset
@@ -82,6 +95,7 @@ module PgTagsOn
82
95
  FROM records
83
96
  WHERE #{table_name}.id = records.id
84
97
  SQL
98
+
85
99
  sql += " RETURNING #{Array.wrap(returning).join(', ')}" if returning.present?
86
100
 
87
101
  bindings = [query_attribute(tag.to_json)] + Array.wrap(bindings)
@@ -43,36 +43,37 @@ module PgTagsOn
43
43
  end
44
44
 
45
45
  def create(tag, returning: nil)
46
- raise 'Tag cannot be blank' if tag.blank?
46
+ value = arel_array_cat(arel_column, bind_for(Array.wrap(tag)))
47
47
 
48
- perform_update(klass,
49
- { column_name => arel_array_cat(arel_column, bind_for(Array.wrap(tag))) },
50
- returning: returning)
48
+ perform_update(klass, { column_name => value }, returning: returning)
51
49
  end
52
50
 
53
51
  def update(tag, new_tag, returning: nil)
54
- raise 'Tag cannot be blank' if tag.blank? || new_tag.blank?
55
-
56
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))
57
54
 
58
- perform_update(rel,
59
- { column_name => arel_array_replace(arel_column, bind_for(tag), bind_for(new_tag)) },
60
- returning: returning)
55
+ perform_update(rel, { column_name => value }, returning: returning)
61
56
  end
62
57
 
63
- def delete(tag, returning: nil)
64
- raise 'Tag cannot be blank' if tag.blank?
65
-
66
- rel = klass.where(column_name => PgTagsOn.query_class.one(tag))
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)
67
68
 
68
- perform_update(rel, { column_name => arel_array_remove(arel_column, bind_for(tag)) }, returning: returning)
69
+ perform_update(rel, { column_name => value }, returning: returning)
69
70
  end
70
71
 
71
72
  private
72
73
 
73
74
  def perform_update(rel, updates, returning: nil)
74
- updater = update_manager(rel, updates)
75
- sql, binds = klass.connection.send :to_sql_and_binds, updater
75
+ update_manager = get_update_manager(rel, updates)
76
+ sql, binds = klass.connection.send :to_sql_and_binds, update_manager
76
77
  sql += " RETURNING #{Array.wrap(returning).join(', ')}" if returning.present?
77
78
 
78
79
  result = klass.connection.exec_query(sql, 'SQL', binds).rows
@@ -80,26 +81,6 @@ module PgTagsOn
80
81
  returning ? result : true
81
82
  end
82
83
 
83
- # Method copied from ActiveRecord as there is no way to inject sql into update manager.
84
- def update_manager(rel, updates)
85
- raise ArgumentError, 'Empty list of attributes to change' if updates.blank?
86
-
87
- stmt = ::Arel::UpdateManager.new
88
- stmt.table(arel_table)
89
- stmt.key = klass.arel_attribute(klass.primary_key)
90
- stmt.take(rel.arel.limit)
91
- stmt.offset(rel.arel.offset)
92
- stmt.order(*rel.arel.orders)
93
- stmt.wheres = rel.arel.constraints
94
- stmt.set rel.send(:_substitute_values, updates)
95
-
96
- stmt
97
- end
98
-
99
- def array_to_recordset
100
- unnest
101
- end
102
-
103
84
  def taggings_query
104
85
  klass
105
86
  .select(
@@ -110,14 +91,14 @@ module PgTagsOn
110
91
  .as('taggings')
111
92
  end
112
93
 
113
- def ref
114
- "#{table_name}.#{column_name}"
115
- end
116
-
117
94
  def unnest
118
95
  arel_unnest(arel_column)
119
96
  end
120
97
 
98
+ def array_to_recordset
99
+ unnest
100
+ end
101
+
121
102
  def unnest_with_ordinality(alias_table: 't')
122
103
  "#{unnest.to_sql} WITH ORDINALITY #{alias_table}(name, index)"
123
104
  end
@@ -28,10 +28,6 @@ module PgTagsOn
28
28
  @table_name ||= klass.table_name
29
29
  end
30
30
 
31
- def cast_type
32
- @cast_type ||= klass.type_for_attribute(column_name)
33
- end
34
-
35
31
  def arel_table
36
32
  klass.arel_table
37
33
  end
@@ -39,6 +35,45 @@ module PgTagsOn
39
35
  def arel_column
40
36
  arel_table[column_name]
41
37
  end
38
+
39
+ # Returns ActiveRecord Column instance
40
+ def ar_column
41
+ @ar_column ||= klass.columns_hash[column_name.to_s]
42
+ end
43
+
44
+ def ref
45
+ "#{table_name}.#{column_name}"
46
+ end
47
+
48
+ # Returns Type instance
49
+ def cast_type
50
+ @cast_type ||= klass.type_for_attribute(column_name)
51
+ end
52
+
53
+ # Returns db type as string.
54
+ # Ex: character varying[], integer[]
55
+ def native_column_type
56
+ type = ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES.fetch(cast_type.type)[:name]
57
+ type += '[]' if ar_column.array?
58
+
59
+ type
60
+ end
61
+
62
+ # Method copied from ActiveRecord as there is no way to inject sql into update manager.
63
+ def get_update_manager(rel, updates)
64
+ raise ArgumentError, 'Empty list of attributes to change' if updates.blank?
65
+
66
+ stmt = ::Arel::UpdateManager.new
67
+ stmt.table(arel_table)
68
+ stmt.key = klass.arel_attribute(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
+
75
+ stmt
76
+ end
42
77
  end
43
78
  end
44
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgTagsOn
4
- VERSION = '0.1.4'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.4
4
+ version: 0.2.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: 2020-04-12 00:00:00.000000000 Z
11
+ date: 2020-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord