metka 1.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/Gemfile.lock +21 -19
- data/README.md +256 -53
- data/lib/generators/metka/strategies/materialized_view/materialized_view_generator.rb +21 -6
- data/lib/generators/metka/strategies/materialized_view/templates/migration.rb.erb +19 -12
- data/lib/generators/metka/strategies/view/templates/migration.rb.erb +11 -8
- data/lib/generators/metka/strategies/view/view_generator.rb +17 -5
- data/lib/metka.rb +1 -1
- data/lib/metka/generic_parser.rb +10 -6
- data/lib/metka/model.rb +64 -23
- data/lib/metka/query_builder.rb +55 -10
- data/lib/metka/version.rb +1 -1
- data/metka.gemspec +13 -12
- metadata +28 -31
- data/lib/metka/query_builder/exclude_all_tags_query.rb +0 -11
- data/lib/metka/query_builder/exclude_any_tags_query.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e518eab14726a263564f8bd8e2ef036be41dfae35e835339aa3236c36e064120
|
4
|
+
data.tar.gz: 66f5dcf4ecb93a301c28b900bbc2eaeb2d97e7f95d169d76409df7c47ec34134
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a742e76cae372c76d21b4ddd0c2c7aa9e208e5756eb02f7cf1f65d0b44e58ec162a06d6dad4811340b4199803dbd176c02937b34c61248cdd6c9b0e316aba9ad
|
7
|
+
data.tar.gz: 430d84f7b3f5187b4921479eb1293ed09920f8190d025889bcc56f9f59aaac26393b74e47381788feae79805c8a3c54a16d3d1496a9f5190a90cb6bad7273226
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
metka (
|
5
|
-
dry-configurable
|
6
|
-
rails (
|
4
|
+
metka (2.0.1)
|
5
|
+
dry-configurable (>= 0.8)
|
6
|
+
rails (>= 5.1)
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
@@ -53,15 +53,17 @@ GEM
|
|
53
53
|
ast (2.4.0)
|
54
54
|
builder (3.2.3)
|
55
55
|
coderay (1.1.2)
|
56
|
-
concurrent-ruby (1.1.
|
56
|
+
concurrent-ruby (1.1.6)
|
57
57
|
crass (1.0.5)
|
58
58
|
database_cleaner (1.7.0)
|
59
59
|
diff-lcs (1.3)
|
60
|
-
dry-configurable (0.
|
60
|
+
dry-configurable (0.11.5)
|
61
61
|
concurrent-ruby (~> 1.0)
|
62
62
|
dry-core (~> 0.4, >= 0.4.7)
|
63
|
+
dry-equalizer (~> 0.2)
|
63
64
|
dry-core (0.4.9)
|
64
65
|
concurrent-ruby (~> 1.0)
|
66
|
+
dry-equalizer (0.3.0)
|
65
67
|
erubi (1.9.0)
|
66
68
|
faker (2.8.0)
|
67
69
|
i18n (>= 1.6, < 1.8)
|
@@ -84,7 +86,7 @@ GEM
|
|
84
86
|
mini_portile2 (2.4.0)
|
85
87
|
minitest (5.13.0)
|
86
88
|
nio4r (2.5.2)
|
87
|
-
nokogiri (1.10.
|
89
|
+
nokogiri (1.10.8)
|
88
90
|
mini_portile2 (~> 2.4.0)
|
89
91
|
parallel (1.19.1)
|
90
92
|
parser (2.6.5.0)
|
@@ -93,7 +95,7 @@ GEM
|
|
93
95
|
pry (0.12.2)
|
94
96
|
coderay (~> 1.1.0)
|
95
97
|
method_source (~> 0.9.0)
|
96
|
-
rack (2.0.
|
98
|
+
rack (2.0.8)
|
97
99
|
rack-test (1.1.0)
|
98
100
|
rack (>= 1.0, < 3)
|
99
101
|
rails (5.1.7)
|
@@ -175,25 +177,25 @@ GEM
|
|
175
177
|
unicode-display_width (1.6.0)
|
176
178
|
websocket-driver (0.6.5)
|
177
179
|
websocket-extensions (>= 0.1.0)
|
178
|
-
websocket-extensions (0.1.
|
180
|
+
websocket-extensions (0.1.5)
|
179
181
|
|
180
182
|
PLATFORMS
|
181
183
|
ruby
|
182
184
|
|
183
185
|
DEPENDENCIES
|
184
186
|
activerecord (~> 5.1.1)
|
185
|
-
ammeter
|
186
|
-
bundler
|
187
|
-
database_cleaner
|
188
|
-
faker
|
187
|
+
ammeter (>= 1.1)
|
188
|
+
bundler (>= 1.3)
|
189
|
+
database_cleaner (>= 1.7)
|
190
|
+
faker (>= 2.8)
|
189
191
|
jetrockets-standard (~> 1.0.1)
|
190
192
|
metka!
|
191
|
-
pg
|
192
|
-
pry (
|
193
|
-
rake
|
194
|
-
rspec (
|
195
|
-
rspec-rails (
|
196
|
-
timecop
|
193
|
+
pg (>= 1.1)
|
194
|
+
pry (>= 0.12.2)
|
195
|
+
rake (>= 0.8.7)
|
196
|
+
rspec (>= 3.9)
|
197
|
+
rspec-rails (>= 3.9)
|
198
|
+
timecop (>= 0.9)
|
197
199
|
|
198
200
|
BUNDLED WITH
|
199
|
-
2.
|
201
|
+
2.1.4
|
data/README.md
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/metka.svg)](https://badge.fury.io/rb/metka)
|
1
2
|
[![Build Status](https://travis-ci.org/jetrockets/metka.svg?branch=master)](https://travis-ci.org/jetrockets/metka)
|
2
3
|
[![Open Source Helpers](https://www.codetriage.com/jetrockets/metka/badges/users.svg)](https://www.codetriage.com/jetrockets/metka)
|
3
4
|
|
4
5
|
# Metka
|
5
6
|
|
6
|
-
Rails gem to manage tags with
|
7
|
+
Rails gem to manage tags with PostgreSQL array columns.
|
7
8
|
|
8
9
|
## Installation
|
9
10
|
|
@@ -23,16 +24,32 @@ Or install it yourself as:
|
|
23
24
|
|
24
25
|
## Tag objects
|
25
26
|
|
27
|
+
```bash
|
28
|
+
rails g migration CreateSongs
|
29
|
+
```
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class CreateSongs < ActiveRecord::Migration[5.0]
|
33
|
+
def change
|
34
|
+
create_table :songs do |t|
|
35
|
+
t.string :title
|
36
|
+
t.string :tags, array: true
|
37
|
+
t.string :genres, array: true
|
38
|
+
t.timestamps
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
26
44
|
```ruby
|
27
45
|
class Song < ActiveRecord::Base
|
28
|
-
include Metka::Model(
|
29
|
-
include Metka::Model(column: 'genres')
|
46
|
+
include Metka::Model(columns: %w[genres tags])
|
30
47
|
end
|
31
48
|
|
32
|
-
@
|
33
|
-
@
|
34
|
-
@
|
35
|
-
@
|
49
|
+
@song = Song.new(title: 'Migrate tags in Rails to PostgreSQL')
|
50
|
+
@song.tag_list = 'top, chill'
|
51
|
+
@song.genre_list = 'rock, jazz, pop'
|
52
|
+
@song.save
|
36
53
|
```
|
37
54
|
|
38
55
|
## Find tagged objects
|
@@ -40,7 +57,7 @@ end
|
|
40
57
|
### .with_all_#{column_name}
|
41
58
|
```ruby
|
42
59
|
Song.with_all_tags('top')
|
43
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
60
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
44
61
|
|
45
62
|
Song.with_all_tags('top, 1990')
|
46
63
|
=> []
|
@@ -49,22 +66,22 @@ Song.with_all_tags('')
|
|
49
66
|
=> []
|
50
67
|
|
51
68
|
Song.with_all_genres('rock')
|
52
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
69
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
53
70
|
```
|
54
71
|
|
55
72
|
### .with_any_#{column_name}
|
56
73
|
```ruby
|
57
74
|
Song.with_any_tags('chill')
|
58
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
75
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
59
76
|
|
60
77
|
Song.with_any_tags('chill, 1980')
|
61
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
78
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
62
79
|
|
63
80
|
Song.with_any_tags('')
|
64
81
|
=> []
|
65
82
|
|
66
83
|
Song.with_any_genres('rock, rap')
|
67
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
84
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
68
85
|
```
|
69
86
|
### .without_all_#{column_name}
|
70
87
|
```ruby
|
@@ -72,13 +89,13 @@ Song.without_all_tags('top')
|
|
72
89
|
=> []
|
73
90
|
|
74
91
|
Song.without_all_tags('top, 1990')
|
75
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
92
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
76
93
|
|
77
94
|
Song.without_all_tags('')
|
78
|
-
=> [
|
95
|
+
=> []
|
79
96
|
|
80
97
|
Song.without_all_genres('rock, pop')
|
81
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
98
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
82
99
|
|
83
100
|
Song.without_all_genres('rock')
|
84
101
|
=> []
|
@@ -90,23 +107,71 @@ Song.without_any_tags('top, 1990')
|
|
90
107
|
=> []
|
91
108
|
|
92
109
|
Song.without_any_tags('1990, 1980')
|
93
|
-
=> [#<Song id: 1, title: 'Migrate tags in Rails to
|
110
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
94
111
|
|
95
112
|
Song.without_any_genres('rock, pop')
|
96
113
|
=> []
|
97
114
|
|
98
115
|
Song.without_any_genres('')
|
99
|
-
=> [
|
116
|
+
=> []
|
117
|
+
```
|
118
|
+
|
119
|
+
### .tagged_with
|
120
|
+
```ruby
|
121
|
+
Song.tagged_with('top')
|
122
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
123
|
+
|
124
|
+
Song.tagged_with('top, 1990')
|
125
|
+
=> []
|
126
|
+
|
127
|
+
Song.tagged_with('')
|
128
|
+
=> []
|
129
|
+
|
130
|
+
Song.tagged_with('rock')
|
131
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
132
|
+
|
133
|
+
Song.tagged_with('rock', join_operator: Metka::And)
|
134
|
+
=> []
|
135
|
+
|
136
|
+
Song.tagged_with('chill', any: true)
|
137
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
138
|
+
|
139
|
+
Song.tagged_with('chill, 1980', any: true)
|
140
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
141
|
+
|
142
|
+
Song.tagged_with('', any: true)
|
143
|
+
=> []
|
144
|
+
|
145
|
+
Song.tagged_with('rock, rap', any: true, on: ['genres'])
|
146
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
147
|
+
|
148
|
+
Song.without_all_tags('top')
|
149
|
+
=> []
|
150
|
+
|
151
|
+
Song.tagged_with('top, 1990', exclude: true)
|
152
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
153
|
+
|
154
|
+
Song.tagged_with('', exclude: true)
|
155
|
+
=> []
|
156
|
+
|
157
|
+
Song.tagged_with('top, 1990', any: true, exclude: true)
|
158
|
+
=> []
|
159
|
+
|
160
|
+
Song.tagged_with('1990, 1980', any: true, exclude: true)
|
161
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to PostgreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
162
|
+
|
163
|
+
Song.without_any_genres('rock, pop')
|
164
|
+
=> []
|
100
165
|
```
|
101
166
|
|
102
167
|
## Custom delimiter
|
103
168
|
By default, a comma is used as a delimiter to create tags from a string.
|
104
169
|
You can make your own custom separator:
|
105
170
|
```ruby
|
106
|
-
Metka.config.delimiter =
|
171
|
+
Metka.config.delimiter = '|'
|
107
172
|
parsed_data = Metka::GenericParser.instance.call('cool, data|I have')
|
108
173
|
parsed_data.to_a
|
109
|
-
=>['cool
|
174
|
+
=>['cool, data', 'I have']
|
110
175
|
```
|
111
176
|
|
112
177
|
## Tags with quote
|
@@ -118,11 +183,10 @@ parsed_data.to_a
|
|
118
183
|
|
119
184
|
## Custom parser
|
120
185
|
By default we use [generic_parser](lib/metka/generic_parser.rb "generic_parser")
|
121
|
-
If you want use your custom parser you can do:
|
186
|
+
If you want to use your custom parser you can do:
|
122
187
|
```ruby
|
123
188
|
class Song < ActiveRecord::Base
|
124
|
-
include Metka::Model(
|
125
|
-
include Metka::Model(column: 'genres')
|
189
|
+
include Metka::Model(columns: %w[genres tags], parser: Your::Custom::Parser.instance)
|
126
190
|
end
|
127
191
|
```
|
128
192
|
Custom parser must be a singleton class that has a `.call` method that accepts the tag string
|
@@ -131,15 +195,35 @@ Custom parser must be a singleton class that has a `.call` method that accepts t
|
|
131
195
|
|
132
196
|
There are several strategies to get tag statistics
|
133
197
|
|
198
|
+
### ActiveRecord Strategy (Default)
|
199
|
+
|
200
|
+
Data about taggings is accessible via class methods of your model with `Metka::Model` attached. You can calculate a cloud for a single tagged column or multiple columns, the latter case would return to you a sum of taggings from multiple tagged columns, that are provided as arguments, for each tag present. ActiveRecord Strategy is an easiest way to implement, since it wouldn't require any additional code, but it's the slowest one on SELECT.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
class Book < ActiveRecord::Base
|
204
|
+
include Metka::Model(column: 'authors')
|
205
|
+
include Metka::Model(column: 'co_authors')
|
206
|
+
end
|
207
|
+
|
208
|
+
tag_cloud = Book.author_cloud
|
209
|
+
=> [["L.N. Tolstoy", 3], ["F.M. Dostoevsky", 6]]
|
210
|
+
genre_cloud = Book.co_author_cloud
|
211
|
+
=> [["A.P. Chekhov", 5], ["N.V. Gogol", 8], ["L.N. Tolstoy", 2]]
|
212
|
+
summary_cloud = Book.metka_cloud('authors', 'co_authors')
|
213
|
+
=> [["L.N. Tolstoy", 5], ["F.M. Dostoevsky", 6], ["A.P. Chekhov", 5], ["N.V. Gogol", 8]]
|
214
|
+
```
|
215
|
+
|
134
216
|
### View Strategy
|
135
217
|
|
136
|
-
Data about taggings will be agregated in SQL View.
|
218
|
+
Data about taggings will be agregated in SQL View. Performance-wise that strategy has no benefits over ActiveRecord Strategy, but if you need to store tags aggregations in a distinct model, that's an easiest way to achieve it.
|
137
219
|
|
138
220
|
```bash
|
139
|
-
rails g metka:strategies:view --source-table-name=NAME_OF_TABLE_WITH_TAGS
|
221
|
+
rails g metka:strategies:view --source-table-name=NAME_OF_TABLE_WITH_TAGS [--source-columns=NAME_OF_COLUMN_1 NAME_OF_COLUMN_2] [--view-name=NAME_OF_RESULTING_VIEW]
|
140
222
|
```
|
141
223
|
|
142
|
-
The code above will generate a migration that creates view
|
224
|
+
The code above will generate a migration that creates view with specified `NAME_OF_RESULTING_VIEW`, that would aggregate tags data from specified array of tagged columns [`NAME_OF_COLUMN_1`, `NAME_OF_COLUMN_2`, ...], that are present within specified table `NAME_OF_TABLE_WITH_TAGS`.
|
225
|
+
If `source-columns` option is not provided, then `tags` column would be used as defaults. If array of multiple values would be provided to the option, then the aggregation would be made with the tags from multiple tagged columns, so if a single tag would be found within multiple tagged columns, the resulting aggregation inside the view would have a single row for that tag with a sum of it's occurences across all stated tagged columns.
|
226
|
+
`view-name` option is also optional, it would just force the resulting view's name to the one of your choice. If it's not provided, then view name would be generated automatically, you could check it within generated migration.
|
143
227
|
|
144
228
|
Lets take a look at real example. We have a `notes` table with `tags` column.
|
145
229
|
|
@@ -163,15 +247,18 @@ The result would be:
|
|
163
247
|
class CreateTaggedNotesView < ActiveRecord::Migration[5.0]
|
164
248
|
def up
|
165
249
|
execute <<-SQL
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
250
|
+
CREATE OR REPLACE VIEW tagged_notes AS
|
251
|
+
SELECT
|
252
|
+
tag_name,
|
253
|
+
COUNT ( * ) AS taggings_count
|
254
|
+
FROM (
|
255
|
+
SELECT UNNEST
|
256
|
+
( tags ) AS tag_name
|
257
|
+
FROM
|
258
|
+
view_posts
|
259
|
+
) subquery
|
260
|
+
GROUP BY
|
261
|
+
tag_name;
|
175
262
|
SQL
|
176
263
|
end
|
177
264
|
|
@@ -197,33 +284,27 @@ Now you can create `TaggedNote` model and work with the view like you usually do
|
|
197
284
|
|
198
285
|
### Materialized View Strategy
|
199
286
|
|
200
|
-
|
287
|
+
Data about taggings will be aggregated in SQL Materialized View, that would be refreshed with the trigger on each change of the tagged column's data. Except for the another type of view being used, that strategy behaves the same way, as a View Strategy above.
|
201
288
|
|
202
289
|
```bash
|
203
|
-
rails g metka:strategies:materialized_view --source-table-name=NAME_OF_TABLE_WITH_TAGS
|
290
|
+
rails g metka:strategies:materialized_view --source-table-name=NAME_OF_TABLE_WITH_TAGS --source-columns=NAME_OF_COLUMN_1 NAME_OF_COLUMN_2 --view-name=NAME_OF_RESULTING_VIEW
|
204
291
|
```
|
205
292
|
|
206
|
-
|
207
|
-
|
208
|
-
Lets take a look at real example. We have a `notes` table with `tags` column.
|
209
|
-
|
210
|
-
| Column | Type | Default |
|
211
|
-
|--------|---------------------|-----------------------------------|
|
212
|
-
| id | integer | nextval('notes_id_seq'::regclass) |
|
213
|
-
| body | text | |
|
214
|
-
| tags | character varying[] | '{}'::character varying[] |
|
215
|
-
|
216
|
-
Now lets generate a migration.
|
293
|
+
All of the options for that stategy's generation command are the same as for the View Strategy.
|
217
294
|
|
218
|
-
|
219
|
-
rails g metka:strategies:materialized_view --source-table-name=notes
|
220
|
-
```
|
295
|
+
The migration template can be seen [here](spec/dummy/db/migrate/06_create_tagged_materialized_view_posts_materialized_view.rb "here")
|
221
296
|
|
222
|
-
|
297
|
+
With the same `notes` table with `tags` column the resulting view would have the same two columns
|
223
298
|
|
224
|
-
|
299
|
+
| tag_name | taggings_count |
|
300
|
+
|----------|----------------|
|
301
|
+
| Ruby | 124056 |
|
302
|
+
| React | 30632 |
|
303
|
+
| Rails | 28696 |
|
304
|
+
| Crystal | 6566 |
|
305
|
+
| Elixir | 3475 |
|
225
306
|
|
226
|
-
|
307
|
+
And you can also create `TaggedNote` model to work with the view as with a Rails model.
|
227
308
|
|
228
309
|
### Table Strategy with Triggers
|
229
310
|
|
@@ -236,6 +317,128 @@ TBD
|
|
236
317
|
2. [ActsAsTaggableArrayOn](https://github.com/tmiyamon/acts-as-taggable-array-on)
|
237
318
|
3. [TagColumns](https://github.com/hopsoft/tag_columns)
|
238
319
|
|
320
|
+
## Benchmark Comparison
|
321
|
+
|
322
|
+
There are some results of benchmarking a performance of write, read and find operations for different gems, that provide solution for tagging. Keep in mind, that those results can't be used as a proof, that some solution is better than the others, since each of the benchmarked gems has their unique features. You could run the benchmarks yourself or check, what exact operations has been used for benchmarking, with [MetkaBench application](https://github.com/jetrockets/metka_bench).
|
323
|
+
|
324
|
+
```bash
|
325
|
+
$ rake bench:all
|
326
|
+
Deleted all MetkaSong
|
327
|
+
Deleted all ActsAsTaggableOn::Tagging
|
328
|
+
Deleted all ActsAsTaggableOn::Tag
|
329
|
+
Deleted all ActsAsTaggableSong
|
330
|
+
Deleted all ActsAsTaggableArraySong
|
331
|
+
Deleted all TagColumnsSong
|
332
|
+
Finished to clean
|
333
|
+
|
334
|
+
###################################################################
|
335
|
+
|
336
|
+
bench:write
|
337
|
+
|
338
|
+
Time measurements:
|
339
|
+
|
340
|
+
Rehearsal ----------------------------------------------------------
|
341
|
+
Metka: 2.192410 0.161092 2.353502 ( 2.754766)
|
342
|
+
ActsAsTaggableOn: 13.769918 0.554951 14.324869 ( 16.990127)
|
343
|
+
ActsAsTaggableOnArray: 2.150441 0.154127 2.304568 ( 2.700022)
|
344
|
+
TagColumns: 2.202647 0.156162 2.358809 ( 2.753400)
|
345
|
+
------------------------------------------------ total: 21.341748sec
|
346
|
+
|
347
|
+
user system total real
|
348
|
+
Metka: 2.137315 0.154046 2.291361 ( 2.643363)
|
349
|
+
ActsAsTaggableOn: 11.302848 0.448674 11.751522 ( 14.019458)
|
350
|
+
ActsAsTaggableOnArray: 2.143134 0.128655 2.271789 ( 2.670797)
|
351
|
+
TagColumns: 2.133780 0.125749 2.259529 ( 2.653404)
|
352
|
+
|
353
|
+
Memory measurements:
|
354
|
+
|
355
|
+
Calculating -------------------------------------
|
356
|
+
Metka: 179.064M memsize ( 0.000 retained)
|
357
|
+
1.689M objects ( 0.000 retained)
|
358
|
+
50.000 strings ( 0.000 retained)
|
359
|
+
ActsAsTaggableOn: 843.949M memsize ( 0.000 retained)
|
360
|
+
8.550M objects ( 0.000 retained)
|
361
|
+
50.000 strings ( 0.000 retained)
|
362
|
+
ActsAsTaggableOnArray: 178.807M memsize ( 0.000 retained)
|
363
|
+
1.684M objects ( 0.000 retained)
|
364
|
+
50.000 strings ( 0.000 retained)
|
365
|
+
TagColumns: 180.009M memsize ( 0.000 retained)
|
366
|
+
1.699M objects ( 0.000 retained)
|
367
|
+
50.000 strings ( 0.000 retained)
|
368
|
+
|
369
|
+
###################################################################
|
370
|
+
|
371
|
+
bench:read
|
372
|
+
|
373
|
+
Time measurements:
|
374
|
+
|
375
|
+
Rehearsal ----------------------------------------------------------
|
376
|
+
Metka: 0.479695 0.044399 0.524094 ( 0.590616)
|
377
|
+
ActsAsTaggableOn: 2.436328 0.140581 2.576909 ( 3.096142)
|
378
|
+
ActsAsTaggableOnArray: 0.515198 0.042127 0.557325 ( 0.623205)
|
379
|
+
TagColumns: 0.518363 0.042661 0.561024 ( 0.626968)
|
380
|
+
------------------------------------------------- total: 4.219352sec
|
381
|
+
|
382
|
+
user system total real
|
383
|
+
Metka: 0.446751 0.041886 0.488637 ( 0.554018)
|
384
|
+
ActsAsTaggableOn: 2.395166 0.164500 2.559666 ( 3.069655)
|
385
|
+
ActsAsTaggableOnArray: 0.439608 0.041682 0.481290 ( 0.544679)
|
386
|
+
TagColumns: 0.435404 0.041623 0.477027 ( 0.540359)
|
387
|
+
|
388
|
+
Memory measurements:
|
389
|
+
|
390
|
+
Calculating -------------------------------------
|
391
|
+
Metka: 42.291M memsize ( 0.000 retained)
|
392
|
+
388.694k objects ( 0.000 retained)
|
393
|
+
50.000 strings ( 0.000 retained)
|
394
|
+
ActsAsTaggableOn: 178.664M memsize ( 0.000 retained)
|
395
|
+
1.812M objects ( 0.000 retained)
|
396
|
+
50.000 strings ( 0.000 retained)
|
397
|
+
ActsAsTaggableOnArray: 42.173M memsize ( 0.000 retained)
|
398
|
+
383.003k objects ( 0.000 retained)
|
399
|
+
50.000 strings ( 0.000 retained)
|
400
|
+
TagColumns: 41.948M memsize ( 0.000 retained)
|
401
|
+
383.003k objects ( 0.000 retained)
|
402
|
+
50.000 strings ( 0.000 retained)
|
403
|
+
|
404
|
+
###################################################################
|
405
|
+
|
406
|
+
bench:find_by_tag
|
407
|
+
|
408
|
+
Time measurements:
|
409
|
+
|
410
|
+
Rehearsal ----------------------------------------------------------
|
411
|
+
Metka: 0.029961 0.000059 0.030020 ( 0.030052)
|
412
|
+
ActsAsTaggableOn: 0.067095 0.000068 0.067163 ( 0.067205)
|
413
|
+
ActsAsTaggableOnArray: 0.043156 0.000133 0.043289 ( 0.043440)
|
414
|
+
TagColumns: 0.056475 0.000143 0.056618 ( 0.056697)
|
415
|
+
------------------------------------------------- total: 0.197090sec
|
416
|
+
|
417
|
+
user system total real
|
418
|
+
Metka: 0.028291 0.000019 0.028310 ( 0.028321)
|
419
|
+
ActsAsTaggableOn: 0.065925 0.000036 0.065961 ( 0.065989)
|
420
|
+
ActsAsTaggableOnArray: 0.043214 0.000079 0.043293 ( 0.043361)
|
421
|
+
TagColumns: 0.056390 0.000160 0.056550 ( 0.056666)
|
422
|
+
|
423
|
+
Memory measurements:
|
424
|
+
|
425
|
+
Calculating -------------------------------------
|
426
|
+
Metka: 4.752M memsize ( 0.000 retained)
|
427
|
+
43.000k objects ( 0.000 retained)
|
428
|
+
1.000 strings ( 0.000 retained)
|
429
|
+
ActsAsTaggableOn: 8.967M memsize ( 0.000 retained)
|
430
|
+
81.002k objects ( 0.000 retained)
|
431
|
+
9.000 strings ( 0.000 retained)
|
432
|
+
ActsAsTaggableOnArray: 5.211M memsize ( 0.000 retained)
|
433
|
+
57.003k objects ( 0.000 retained)
|
434
|
+
6.000 strings ( 0.000 retained)
|
435
|
+
TagColumns: 6.696M memsize ( 0.000 retained)
|
436
|
+
94.003k objects ( 0.000 retained)
|
437
|
+
8.000 strings ( 0.000 retained)
|
438
|
+
|
439
|
+
Finished all benchmarks
|
440
|
+
```
|
441
|
+
|
239
442
|
## Development
|
240
443
|
|
241
444
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -9,10 +9,15 @@ module Metka
|
|
9
9
|
class MaterializedViewGenerator < ::Rails::Generators::Base # :nodoc:
|
10
10
|
include Rails::Generators::Migration
|
11
11
|
|
12
|
+
DEFAULT_SOURCE_COLUMNS = ['tags'].freeze
|
13
|
+
|
12
14
|
desc <<~LONGDESC
|
13
15
|
Generates migration to implement view strategy for Metka
|
14
16
|
|
15
|
-
> $ rails g metka:strategies:materialized_view
|
17
|
+
> $ rails g metka:strategies:materialized_view \
|
18
|
+
--source-table-name=NAME_OF_TABLE_WITH_TAGS \
|
19
|
+
--source-columns=NAME_OF_TAGGED_COLUMN_1 NAME_OF_TAGGED_COLUMN_2 \
|
20
|
+
--view-name=NAME_OF_VIEW
|
16
21
|
LONGDESC
|
17
22
|
|
18
23
|
source_root File.expand_path('templates', __dir__)
|
@@ -20,8 +25,11 @@ module Metka
|
|
20
25
|
class_option :source_table_name, type: :string, required: true,
|
21
26
|
desc: 'Name of the table that has a column with tags'
|
22
27
|
|
23
|
-
class_option :
|
24
|
-
|
28
|
+
class_option :source_columns, type: :array, default: DEFAULT_SOURCE_COLUMNS,
|
29
|
+
desc: 'List of the tagged columns names'
|
30
|
+
|
31
|
+
class_option :view_name, type: :string,
|
32
|
+
desc: 'Custom name for the resulting view'
|
25
33
|
|
26
34
|
def generate_migration
|
27
35
|
migration_template 'migration.rb.erb', "db/migrate/#{migration_name}.rb"
|
@@ -32,12 +40,19 @@ module Metka
|
|
32
40
|
options[:source_table_name]
|
33
41
|
end
|
34
42
|
|
35
|
-
def
|
36
|
-
options[:
|
43
|
+
def source_columns
|
44
|
+
options[:source_columns]
|
45
|
+
end
|
46
|
+
|
47
|
+
def source_columns_names
|
48
|
+
source_columns.join('_and_')
|
37
49
|
end
|
38
50
|
|
39
51
|
def view_name
|
40
|
-
|
52
|
+
return options[:view_name] if options[:view_name]
|
53
|
+
|
54
|
+
columns_sequence = source_columns == DEFAULT_SOURCE_COLUMNS ? nil : "_with_#{source_columns_names}"
|
55
|
+
"tagged#{columns_sequence}_#{source_table_name}"
|
41
56
|
end
|
42
57
|
|
43
58
|
def migration_name
|
@@ -5,15 +5,18 @@ class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VE
|
|
5
5
|
execute <<-SQL
|
6
6
|
CREATE OR REPLACE FUNCTION metka_refresh_<%= view_name %>_materialized_view() RETURNS trigger LANGUAGE plpgsql AS $$
|
7
7
|
BEGIN
|
8
|
-
IF TG_OP = 'INSERT' AND
|
8
|
+
IF TG_OP = 'INSERT' AND
|
9
|
+
(<%= source_columns.map { |column| "NEW.#{column} IS NOT NULL" }.join(' OR ') %>) THEN
|
9
10
|
REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
|
10
11
|
END IF;
|
11
12
|
|
12
|
-
IF TG_OP = 'UPDATE' AND
|
13
|
+
IF TG_OP = 'UPDATE' AND
|
14
|
+
(<%= source_columns.map { |column| "OLD.#{column} IS DISTINCT FROM NEW.#{column}" }.join(' OR ') %>) THEN
|
13
15
|
REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
|
14
16
|
END IF;
|
15
17
|
|
16
|
-
IF TG_OP = 'DELETE' AND
|
18
|
+
IF TG_OP = 'DELETE' AND
|
19
|
+
(<%= source_columns.map { |column| "OLD.#{column} IS NOT NULL" }.join(' OR ') %>) THEN
|
17
20
|
REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
|
18
21
|
END IF;
|
19
22
|
RETURN NEW;
|
@@ -21,17 +24,21 @@ class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VE
|
|
21
24
|
|
22
25
|
DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
|
23
26
|
CREATE MATERIALIZED VIEW <%= view_name %> AS
|
24
|
-
SELECT
|
25
|
-
|
26
|
-
COUNT
|
27
|
-
FROM
|
28
|
-
|
27
|
+
SELECT
|
28
|
+
tag_name,
|
29
|
+
COUNT(*) AS taggings_count
|
30
|
+
FROM (
|
31
|
+
SELECT UNNEST
|
32
|
+
(<%= source_columns.join(' || ') %>) AS tag_name
|
33
|
+
FROM
|
34
|
+
<%= source_table_name %>
|
35
|
+
) subquery
|
29
36
|
GROUP BY
|
30
|
-
|
37
|
+
tag_name;
|
31
38
|
|
32
|
-
CREATE UNIQUE INDEX idx_<%= source_table_name %>_<%=
|
39
|
+
CREATE UNIQUE INDEX idx_<%= source_table_name %>_<%= source_columns_names %> ON <%= view_name %>(tag_name);
|
33
40
|
|
34
|
-
CREATE TRIGGER metka_on_<%= source_table_name %>
|
41
|
+
CREATE TRIGGER metka_on_<%= source_table_name %>_<%= source_columns_names %>
|
35
42
|
AFTER UPDATE OR INSERT OR DELETE ON <%= source_table_name %> FOR EACH ROW
|
36
43
|
EXECUTE PROCEDURE metka_refresh_<%= view_name %>_materialized_view();
|
37
44
|
SQL
|
@@ -39,7 +46,7 @@ class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VE
|
|
39
46
|
|
40
47
|
def down
|
41
48
|
execute <<-SQL
|
42
|
-
DROP TRIGGER IF EXISTS metka_on_<%= source_table_name %> ON <%= source_table_name %>;
|
49
|
+
DROP TRIGGER IF EXISTS metka_on_<%= source_table_name %>_<%= source_columns_names %> ON <%= source_table_name %>;
|
43
50
|
DROP FUNCTION IF EXISTS metka_refresh_<%= view_name %>_materialized_view;
|
44
51
|
DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
|
45
52
|
SQL
|
@@ -4,14 +4,17 @@ class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VE
|
|
4
4
|
def up
|
5
5
|
execute <<-SQL
|
6
6
|
CREATE OR REPLACE VIEW <%= view_name %> AS
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
SELECT
|
8
|
+
tag_name,
|
9
|
+
COUNT(*) AS taggings_count
|
10
|
+
FROM (
|
11
|
+
SELECT UNNEST
|
12
|
+
(<%= source_columns.join(' || ') %>) AS tag_name
|
13
|
+
FROM
|
14
|
+
<%= source_table_name %>
|
15
|
+
) subquery
|
16
|
+
GROUP BY
|
17
|
+
tag_name;
|
15
18
|
SQL
|
16
19
|
end
|
17
20
|
|
@@ -9,6 +9,8 @@ module Metka
|
|
9
9
|
class ViewGenerator < ::Rails::Generators::Base # :nodoc:
|
10
10
|
include Rails::Generators::Migration
|
11
11
|
|
12
|
+
DEFAULT_SOURCE_COLUMNS = ['tags'].freeze
|
13
|
+
|
12
14
|
desc <<~LONGDESC
|
13
15
|
Generates migration to implement view strategy for Metka
|
14
16
|
|
@@ -20,8 +22,11 @@ module Metka
|
|
20
22
|
class_option :source_table_name, type: :string, required: true,
|
21
23
|
desc: 'Name of the table that has a column with tags'
|
22
24
|
|
23
|
-
class_option :
|
24
|
-
|
25
|
+
class_option :source_columns, type: :array, default: DEFAULT_SOURCE_COLUMNS,
|
26
|
+
desc: 'List of the tagged columns names'
|
27
|
+
|
28
|
+
class_option :view_name, type: :string,
|
29
|
+
desc: 'Custom name for the resulting view'
|
25
30
|
|
26
31
|
def generate_migration
|
27
32
|
migration_template 'migration.rb.erb', "db/migrate/#{migration_name}.rb"
|
@@ -32,12 +37,19 @@ module Metka
|
|
32
37
|
options[:source_table_name]
|
33
38
|
end
|
34
39
|
|
35
|
-
def
|
36
|
-
options[:
|
40
|
+
def source_columns
|
41
|
+
options[:source_columns]
|
42
|
+
end
|
43
|
+
|
44
|
+
def source_columns_names
|
45
|
+
source_columns.join('_and_')
|
37
46
|
end
|
38
47
|
|
39
48
|
def view_name
|
40
|
-
|
49
|
+
return options[:view_name] if options[:view_name]
|
50
|
+
|
51
|
+
columns_sequence = source_columns == DEFAULT_SOURCE_COLUMNS ? nil : "_with_#{source_columns_names}"
|
52
|
+
"tagged#{columns_sequence}_#{source_table_name}"
|
41
53
|
end
|
42
54
|
|
43
55
|
def migration_name
|
data/lib/metka.rb
CHANGED
data/lib/metka/generic_parser.rb
CHANGED
@@ -12,6 +12,11 @@ module Metka
|
|
12
12
|
class GenericParser
|
13
13
|
include Singleton
|
14
14
|
|
15
|
+
def initialize
|
16
|
+
@single_quote_pattern ||= {}
|
17
|
+
@double_quote_pattern ||= {}
|
18
|
+
end
|
19
|
+
|
15
20
|
def call(value)
|
16
21
|
TagList.new.tap do |tag_list|
|
17
22
|
case value
|
@@ -20,7 +25,7 @@ module Metka
|
|
20
25
|
gsub_quote_pattern!(tag_list, value, double_quote_pattern)
|
21
26
|
gsub_quote_pattern!(tag_list, value, single_quote_pattern)
|
22
27
|
|
23
|
-
tag_list.merge value.split(Regexp.new
|
28
|
+
tag_list.merge value.split(Regexp.new delimiter).map(&:strip).reject(&:empty?)
|
24
29
|
when Enumerable
|
25
30
|
tag_list.merge value.reject(&:empty?)
|
26
31
|
end
|
@@ -36,17 +41,16 @@ module Metka
|
|
36
41
|
}
|
37
42
|
end
|
38
43
|
|
39
|
-
def
|
40
|
-
|
41
|
-
delimeter.is_a?(Array) ? delimeter.join('|') : delimeter
|
44
|
+
def delimiter
|
45
|
+
Metka.delimiter
|
42
46
|
end
|
43
47
|
|
44
48
|
def single_quote_pattern
|
45
|
-
|
49
|
+
@single_quote_pattern[delimiter] ||= /(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
|
46
50
|
end
|
47
51
|
|
48
52
|
def double_quote_pattern
|
49
|
-
/(\A|#{
|
53
|
+
@double_quote_pattern[delimiter] ||= /(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
|
50
54
|
end
|
51
55
|
end
|
52
56
|
end
|
data/lib/metka/model.rb
CHANGED
@@ -1,47 +1,88 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'arel'
|
4
|
+
|
3
5
|
module Metka
|
4
|
-
|
5
|
-
|
6
|
+
OR = Arel::Nodes::Or
|
7
|
+
AND = Arel::Nodes::And
|
8
|
+
|
9
|
+
def self.Model(column: nil, columns: nil, **options)
|
10
|
+
columns = [column, *columns].uniq.compact
|
11
|
+
raise ArgumentError, 'Columns not specified' unless columns.present?
|
12
|
+
|
13
|
+
Metka::Model.new(columns: columns, **options)
|
6
14
|
end
|
7
15
|
|
8
16
|
class Model < Module
|
9
|
-
def initialize(
|
10
|
-
@
|
11
|
-
@options = options
|
17
|
+
def initialize(columns:, **options)
|
18
|
+
@columns = columns.dup.freeze
|
19
|
+
@options = options.dup.freeze
|
12
20
|
end
|
13
21
|
|
14
22
|
def included(base)
|
15
|
-
|
23
|
+
columns = @columns
|
16
24
|
parser = ->(tags) {
|
17
25
|
@options[:parser] ? @options[:parser].call(tags) : Metka.config.parser.instance.call(tags)
|
18
26
|
}
|
19
27
|
|
20
|
-
|
28
|
+
# @param model [ActiveRecord::Base] model on which to execute search
|
29
|
+
# @param tags [Object] list of tags, representation depends on parser used
|
30
|
+
# @param options [Hash] options
|
31
|
+
# @option :join_operator [Metka::AND, Metka::OR]
|
32
|
+
# @option :on [Array<String>] list of column names to include in query
|
33
|
+
# @returns ViewPost::ActiveRecord_Relation
|
34
|
+
tagged_with_lambda = ->(model, tags, **options) {
|
35
|
+
cols = options.delete(:on)
|
21
36
|
parsed_tag_list = parser.call(tags)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
end
|
37
|
+
|
38
|
+
return model.none if parsed_tag_list.empty?
|
39
|
+
|
40
|
+
request = ::Metka::QueryBuilder.new.call(model, cols, parsed_tag_list, options)
|
41
|
+
model.where(request)
|
28
42
|
}
|
29
43
|
|
30
44
|
base.class_eval do
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
45
|
+
columns.each do |column|
|
46
|
+
scope "with_all_#{column}", ->(tags) { tagged_with(tags, on: [ column ]) }
|
47
|
+
scope "with_any_#{column}", ->(tags) { tagged_with(tags, on: [ column ], any: true) }
|
48
|
+
scope "without_all_#{column}", ->(tags) { tagged_with(tags, on: [ column ], exclude: true) }
|
49
|
+
scope "without_any_#{column}", ->(tags) { tagged_with(tags, on: [ column ], any: true, exclude: true) }
|
50
|
+
end
|
51
|
+
|
52
|
+
unless respond_to?(:tagged_with)
|
53
|
+
scope :tagged_with, ->(tags = '', options = {}) {
|
54
|
+
options[:join_operator] ||= ::Metka::OR
|
55
|
+
options = {any: false}.merge(options)
|
56
|
+
options[:on] ||= columns
|
57
|
+
|
58
|
+
tagged_with_lambda.call(self, tags, **options)
|
59
|
+
}
|
60
|
+
end
|
35
61
|
end
|
36
62
|
|
37
|
-
base.
|
38
|
-
|
39
|
-
|
63
|
+
base.define_singleton_method :metka_cloud do |*columns|
|
64
|
+
return [] if columns.blank?
|
65
|
+
|
66
|
+
prepared_unnest = columns.map { |column| "#{table_name}.#{column}" }.join(' || ')
|
67
|
+
subquery = all.select("UNNEST(#{prepared_unnest}) AS tag_name")
|
68
|
+
|
69
|
+
unscoped.from(subquery).group(:tag_name).pluck(:tag_name, 'COUNT(*) AS taggings_count')
|
40
70
|
end
|
41
71
|
|
42
|
-
|
43
|
-
|
72
|
+
columns.each do |column|
|
73
|
+
base.define_method(column.singularize + '_list=') do |v|
|
74
|
+
write_attribute(column, parser.call(v).to_a)
|
75
|
+
write_attribute(column, nil) if send(column).empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
base.define_method(column.singularize + '_list') do
|
79
|
+
parser.call(send(column))
|
80
|
+
end
|
81
|
+
|
82
|
+
base.define_singleton_method :"#{column.singularize}_cloud" do
|
83
|
+
metka_cloud(column)
|
84
|
+
end
|
44
85
|
end
|
45
86
|
end
|
46
87
|
end
|
47
|
-
end
|
88
|
+
end
|
data/lib/metka/query_builder.rb
CHANGED
@@ -1,23 +1,68 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'arel'
|
3
4
|
require_relative 'query_builder/base_query'
|
4
|
-
require_relative 'query_builder/exclude_all_tags_query'
|
5
|
-
require_relative 'query_builder/exclude_any_tags_query'
|
6
5
|
require_relative 'query_builder/any_tags_query'
|
7
6
|
require_relative 'query_builder/all_tags_query'
|
8
7
|
|
9
8
|
module Metka
|
10
9
|
class QueryBuilder
|
11
|
-
def call(
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
def call(model, columns, tags, options)
|
11
|
+
strategy = options_to_strategy(options)
|
12
|
+
|
13
|
+
query = join(options[:join_operator]) do
|
14
|
+
columns.map do |column|
|
15
|
+
build_query(strategy, model, column, tags)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if options[:exclude].present?
|
20
|
+
Arel::Nodes::Not.new(query)
|
21
|
+
else
|
22
|
+
query
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def options_to_strategy options
|
29
|
+
if options[:any].present?
|
30
|
+
AnyTagsQuery
|
18
31
|
else
|
19
|
-
AllTagsQuery
|
32
|
+
AllTagsQuery
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def join(operator, &block)
|
37
|
+
nodes = block.call
|
38
|
+
|
39
|
+
if operator == ::Metka::AND
|
40
|
+
join_and(nodes)
|
41
|
+
elsif operator == ::Metka::OR
|
42
|
+
join_or(nodes)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param nodes [Array<Arel::Node>, Arel::Node]
|
47
|
+
# @return [Arel::Node]
|
48
|
+
def join_or(nodes)
|
49
|
+
case nodes
|
50
|
+
when ::Arel::Node
|
51
|
+
nodes
|
52
|
+
when Array
|
53
|
+
l, *r = nodes
|
54
|
+
return l if r.empty?
|
55
|
+
|
56
|
+
l.or(join_or(r))
|
20
57
|
end
|
21
58
|
end
|
59
|
+
|
60
|
+
def join_and(queries)
|
61
|
+
Arel::Nodes::And.new(queries)
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_query(strategy, model, column, tags)
|
65
|
+
strategy.instance.call(model, column, tags)
|
66
|
+
end
|
22
67
|
end
|
23
68
|
end
|
data/lib/metka/version.rb
CHANGED
data/metka.gemspec
CHANGED
@@ -24,18 +24,19 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
25
|
spec.require_paths = ['lib']
|
26
26
|
|
27
|
-
spec.add_dependency 'dry-configurable'
|
28
|
-
spec.add_dependency 'rails', '
|
27
|
+
spec.add_dependency 'dry-configurable', '>= 0.8'
|
28
|
+
spec.add_dependency 'rails', '>= 5.1'
|
29
29
|
|
30
|
-
spec.add_development_dependency 'ammeter'
|
31
|
-
spec.add_development_dependency 'pry', '
|
32
|
-
spec.add_development_dependency 'bundler'
|
33
|
-
spec.add_development_dependency 'faker'
|
30
|
+
spec.add_development_dependency 'ammeter', '>= 1.1'
|
31
|
+
spec.add_development_dependency 'pry', '>= 0.12.2'
|
32
|
+
spec.add_development_dependency 'bundler', '>= 1.3'
|
33
|
+
spec.add_development_dependency 'faker', '>= 2.8'
|
34
34
|
spec.add_development_dependency 'jetrockets-standard', '~> 1.0.1'
|
35
|
-
spec.add_development_dependency 'pg'
|
36
|
-
spec.add_development_dependency 'rake'
|
37
|
-
spec.add_development_dependency 'rspec', '
|
38
|
-
spec.add_development_dependency 'rspec-rails', '
|
39
|
-
spec.add_development_dependency 'timecop'
|
40
|
-
spec.add_development_dependency 'database_cleaner'
|
35
|
+
spec.add_development_dependency 'pg', '>= 1.1'
|
36
|
+
spec.add_development_dependency 'rake', '>= 0.8.7'
|
37
|
+
spec.add_development_dependency 'rspec', '>= 3.9'
|
38
|
+
spec.add_development_dependency 'rspec-rails', '>= 3.9'
|
39
|
+
spec.add_development_dependency 'timecop', '>= 0.9'
|
40
|
+
spec.add_development_dependency 'database_cleaner', '>= 1.7'
|
41
|
+
spec.required_ruby_version = '>= 2.5'
|
41
42
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: metka
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Alexandrov
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-06-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dry-configurable
|
@@ -17,26 +17,26 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '0'
|
20
|
+
version: '0.8'
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: '0'
|
27
|
+
version: '0.8'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: rails
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - ">="
|
33
33
|
- !ruby/object:Gem::Version
|
34
34
|
version: '5.1'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- - "
|
39
|
+
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '5.1'
|
42
42
|
- !ruby/object:Gem::Dependency
|
@@ -45,26 +45,26 @@ dependencies:
|
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '
|
48
|
+
version: '1.1'
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - ">="
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
55
|
+
version: '1.1'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: pry
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- - "
|
60
|
+
- - ">="
|
61
61
|
- !ruby/object:Gem::Version
|
62
62
|
version: 0.12.2
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
69
|
version: 0.12.2
|
70
70
|
- !ruby/object:Gem::Dependency
|
@@ -73,28 +73,28 @@ dependencies:
|
|
73
73
|
requirements:
|
74
74
|
- - ">="
|
75
75
|
- !ruby/object:Gem::Version
|
76
|
-
version: '
|
76
|
+
version: '1.3'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
81
|
- - ">="
|
82
82
|
- !ruby/object:Gem::Version
|
83
|
-
version: '
|
83
|
+
version: '1.3'
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
85
|
name: faker
|
86
86
|
requirement: !ruby/object:Gem::Requirement
|
87
87
|
requirements:
|
88
88
|
- - ">="
|
89
89
|
- !ruby/object:Gem::Version
|
90
|
-
version: '
|
90
|
+
version: '2.8'
|
91
91
|
type: :development
|
92
92
|
prerelease: false
|
93
93
|
version_requirements: !ruby/object:Gem::Requirement
|
94
94
|
requirements:
|
95
95
|
- - ">="
|
96
96
|
- !ruby/object:Gem::Version
|
97
|
-
version: '
|
97
|
+
version: '2.8'
|
98
98
|
- !ruby/object:Gem::Dependency
|
99
99
|
name: jetrockets-standard
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,54 +115,54 @@ dependencies:
|
|
115
115
|
requirements:
|
116
116
|
- - ">="
|
117
117
|
- !ruby/object:Gem::Version
|
118
|
-
version: '
|
118
|
+
version: '1.1'
|
119
119
|
type: :development
|
120
120
|
prerelease: false
|
121
121
|
version_requirements: !ruby/object:Gem::Requirement
|
122
122
|
requirements:
|
123
123
|
- - ">="
|
124
124
|
- !ruby/object:Gem::Version
|
125
|
-
version: '
|
125
|
+
version: '1.1'
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
127
|
name: rake
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
129
129
|
requirements:
|
130
130
|
- - ">="
|
131
131
|
- !ruby/object:Gem::Version
|
132
|
-
version:
|
132
|
+
version: 0.8.7
|
133
133
|
type: :development
|
134
134
|
prerelease: false
|
135
135
|
version_requirements: !ruby/object:Gem::Requirement
|
136
136
|
requirements:
|
137
137
|
- - ">="
|
138
138
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
139
|
+
version: 0.8.7
|
140
140
|
- !ruby/object:Gem::Dependency
|
141
141
|
name: rspec
|
142
142
|
requirement: !ruby/object:Gem::Requirement
|
143
143
|
requirements:
|
144
|
-
- - "
|
144
|
+
- - ">="
|
145
145
|
- !ruby/object:Gem::Version
|
146
146
|
version: '3.9'
|
147
147
|
type: :development
|
148
148
|
prerelease: false
|
149
149
|
version_requirements: !ruby/object:Gem::Requirement
|
150
150
|
requirements:
|
151
|
-
- - "
|
151
|
+
- - ">="
|
152
152
|
- !ruby/object:Gem::Version
|
153
153
|
version: '3.9'
|
154
154
|
- !ruby/object:Gem::Dependency
|
155
155
|
name: rspec-rails
|
156
156
|
requirement: !ruby/object:Gem::Requirement
|
157
157
|
requirements:
|
158
|
-
- - "
|
158
|
+
- - ">="
|
159
159
|
- !ruby/object:Gem::Version
|
160
160
|
version: '3.9'
|
161
161
|
type: :development
|
162
162
|
prerelease: false
|
163
163
|
version_requirements: !ruby/object:Gem::Requirement
|
164
164
|
requirements:
|
165
|
-
- - "
|
165
|
+
- - ">="
|
166
166
|
- !ruby/object:Gem::Version
|
167
167
|
version: '3.9'
|
168
168
|
- !ruby/object:Gem::Dependency
|
@@ -171,28 +171,28 @@ dependencies:
|
|
171
171
|
requirements:
|
172
172
|
- - ">="
|
173
173
|
- !ruby/object:Gem::Version
|
174
|
-
version: '0'
|
174
|
+
version: '0.9'
|
175
175
|
type: :development
|
176
176
|
prerelease: false
|
177
177
|
version_requirements: !ruby/object:Gem::Requirement
|
178
178
|
requirements:
|
179
179
|
- - ">="
|
180
180
|
- !ruby/object:Gem::Version
|
181
|
-
version: '0'
|
181
|
+
version: '0.9'
|
182
182
|
- !ruby/object:Gem::Dependency
|
183
183
|
name: database_cleaner
|
184
184
|
requirement: !ruby/object:Gem::Requirement
|
185
185
|
requirements:
|
186
186
|
- - ">="
|
187
187
|
- !ruby/object:Gem::Version
|
188
|
-
version: '
|
188
|
+
version: '1.7'
|
189
189
|
type: :development
|
190
190
|
prerelease: false
|
191
191
|
version_requirements: !ruby/object:Gem::Requirement
|
192
192
|
requirements:
|
193
193
|
- - ">="
|
194
194
|
- !ruby/object:Gem::Version
|
195
|
-
version: '
|
195
|
+
version: '1.7'
|
196
196
|
description: Rails tagging system based on PostgreSQL arrays
|
197
197
|
email:
|
198
198
|
- igor.alexandrov@gmail.com
|
@@ -228,8 +228,6 @@ files:
|
|
228
228
|
- lib/metka/query_builder/all_tags_query.rb
|
229
229
|
- lib/metka/query_builder/any_tags_query.rb
|
230
230
|
- lib/metka/query_builder/base_query.rb
|
231
|
-
- lib/metka/query_builder/exclude_all_tags_query.rb
|
232
|
-
- lib/metka/query_builder/exclude_any_tags_query.rb
|
233
231
|
- lib/metka/tag_list.rb
|
234
232
|
- lib/metka/version.rb
|
235
233
|
- metka.gemspec
|
@@ -245,15 +243,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
245
243
|
requirements:
|
246
244
|
- - ">="
|
247
245
|
- !ruby/object:Gem::Version
|
248
|
-
version: '
|
246
|
+
version: '2.5'
|
249
247
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
250
248
|
requirements:
|
251
249
|
- - ">="
|
252
250
|
- !ruby/object:Gem::Version
|
253
251
|
version: '0'
|
254
252
|
requirements: []
|
255
|
-
|
256
|
-
rubygems_version: 2.7.6
|
253
|
+
rubygems_version: 3.0.6
|
257
254
|
signing_key:
|
258
255
|
specification_version: 4
|
259
256
|
summary: Rails tagging system based on PostgreSQL arrays
|