pg_tags_on 0.1.4 → 0.2.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: 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