umbrellio-sequel-plugins 0.1.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 +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.rubocop.yml +10 -0
- data/.travis.yml +27 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +249 -0
- data/Rakefile +8 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/database.rb +30 -0
- data/lib/sequel-plugins.rb +3 -0
- data/lib/sequel/extensions/currency_rates.rb +65 -0
- data/lib/sequel/extensions/pg_tools.rb +32 -0
- data/lib/sequel/extensions/slave.rb +18 -0
- data/lib/sequel/extensions/synchronize.rb +67 -0
- data/lib/sequel/plugins/duplicate.rb +29 -0
- data/lib/sequel/plugins/get_column_value.rb +17 -0
- data/lib/sequel/plugins/store_accessors.rb +51 -0
- data/lib/sequel/plugins/synchronize.rb +25 -0
- data/lib/sequel/plugins/upsert.rb +60 -0
- data/lib/sequel/plugins/with_lock.rb +22 -0
- data/lib/sequel/timestamp_migrator_undo_extension.rb +62 -0
- data/lib/sequel_plugins.rb +7 -0
- data/lib/tasks/sequel/rollback_missing_migrations.rake +29 -0
- data/lib/tasks/sequel/undo.rake +15 -0
- data/umbrellio-sequel-plugins.gemspec +28 -0
- metadata +182 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 229f4459fc1056833cae7449c6026400155af8129235cb6d2c26fe0c6e9896a3
|
4
|
+
data.tar.gz: 8306b1fc77f37c9e199b8fcaa4a22260580956213d0f0fe50c989627247556a9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b2f765b0f6c292477724a56da513a94df92d424687684d3cf1690850989d42255f6897bb153f5f62710001bcfe29eb962aec38cd6f2013f8ecb98d79088b2ff
|
7
|
+
data.tar.gz: b4800cd9102df26f9d53036228d1617e646f31bbffb408b5a48feedc218ad7c4cbb6917b20e3e42bb0ed3b1935502b1a6a0bff5038686c2c86d87d15848bf3c2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
sudo: false
|
4
|
+
|
5
|
+
rvm:
|
6
|
+
- 2.4
|
7
|
+
- 2.5
|
8
|
+
- 2.6
|
9
|
+
- ruby-head
|
10
|
+
|
11
|
+
services:
|
12
|
+
- postgresql
|
13
|
+
addons:
|
14
|
+
postgresql: 9.6
|
15
|
+
before_install: gem install bundler
|
16
|
+
before_script:
|
17
|
+
- psql -c 'create database sequel_plugins;' -U postgres
|
18
|
+
script:
|
19
|
+
- bundle exec rspec
|
20
|
+
- bundle exec rubocop
|
21
|
+
|
22
|
+
matrix:
|
23
|
+
fast_finish: true
|
24
|
+
allow_failures:
|
25
|
+
- rvm: ruby-head
|
26
|
+
|
27
|
+
cache: bundler
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Umbrellio
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
# SequelPlugins
|
2
|
+
[](https://travis-ci.org/umbrellio/umbrellio-sequel-plugins)
|
3
|
+
[](https://coveralls.io/github/umbrellio/umbrellio-sequel-plugins?branch=master)
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'umbrellio-sequel-plugins'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
# Extensions
|
18
|
+
|
19
|
+
- CurrencyRates
|
20
|
+
- PGTools
|
21
|
+
- Slave
|
22
|
+
- Synchronize
|
23
|
+
|
24
|
+
# Plugins
|
25
|
+
|
26
|
+
- Duplicate
|
27
|
+
- GetColumnValue
|
28
|
+
- StoreAccessors
|
29
|
+
- Synchronize
|
30
|
+
- Upsert
|
31
|
+
- WithLock
|
32
|
+
|
33
|
+
# Tools
|
34
|
+
- TimestampMigratorUndoExtension
|
35
|
+
|
36
|
+
## CurrencyRates
|
37
|
+
|
38
|
+
Plugin for joining currency rates table to any other table and money exchange.
|
39
|
+
|
40
|
+
Enable: `DB.extension :currency_rates`
|
41
|
+
|
42
|
+
Currency rates table example:
|
43
|
+
|
44
|
+
```sql
|
45
|
+
CREATE TABLE currency_rates (
|
46
|
+
id integer NOT NULL,
|
47
|
+
currency text NOT NULL,
|
48
|
+
period tsrange NOT NULL,
|
49
|
+
rates jsonb NOT NULL
|
50
|
+
);
|
51
|
+
|
52
|
+
INSERT INTO currency_rates (currency, period, rates) VALUES
|
53
|
+
('EUR', tsrange('2019-02-07 16:00:00 +0300', '2019-02-07 16:00:00 +0300'), '{"USD": 1.1, "EUR": 1.0, "RUB": 81}'),
|
54
|
+
('EUR', tsrange('2019-02-07 17:00:00 +0300', NULL), '{"USD": 1.2, "EUR": 1.0, "RUB": 75}')
|
55
|
+
```
|
56
|
+
|
57
|
+
Usage example:
|
58
|
+
|
59
|
+
```sql
|
60
|
+
CREATE TABLE items (
|
61
|
+
id integer NOT NULL,
|
62
|
+
currency text NOT NULL,
|
63
|
+
price numeric NOT NULL,
|
64
|
+
created_at timestamp without time zone NOT NULL
|
65
|
+
);
|
66
|
+
|
67
|
+
INSERT INTO items (currency, price, created_at) VALUES ("EUR", 10, '2019-02-07 16:10:00 +0300')
|
68
|
+
```
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
DB[:items]
|
72
|
+
.with_rates
|
73
|
+
.select(Sequel[:price].exchange_to("USD").as(:usd_price))
|
74
|
+
.first
|
75
|
+
# => { "usd_price" => 12.0 }
|
76
|
+
```
|
77
|
+
|
78
|
+
|
79
|
+
## PGTools
|
80
|
+
|
81
|
+
Enable: `DB.extension :pg_tools`
|
82
|
+
|
83
|
+
### `#inherited_tables_for`
|
84
|
+
|
85
|
+
Plugins for getting all inherited tables.
|
86
|
+
|
87
|
+
Example:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
DB.inherited_tables_for(:event_log) # => [:event_log_2019_01, :event_log_2019_02]
|
91
|
+
```
|
92
|
+
|
93
|
+
## Slave
|
94
|
+
|
95
|
+
Enable: `DB.extension :slave`
|
96
|
+
|
97
|
+
Plugin for choosing slave server for query.
|
98
|
+
|
99
|
+
Example:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
DB[:users].slave.where(email: "test@test.com") # executes on a slave server
|
103
|
+
```
|
104
|
+
|
105
|
+
**Important:** you have to define a server named 'slave' in sequel config before using it.
|
106
|
+
|
107
|
+
|
108
|
+
## Synchronize
|
109
|
+
|
110
|
+
Enable: `DB.extension :synchronize`
|
111
|
+
|
112
|
+
Plugin for using transaction advisory locks for application-level mutexes.
|
113
|
+
|
114
|
+
Example:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
DB.synchronize_with([:ruby, :forever]) { p "Hey, I'm in transaction!"; sleep 5 }
|
118
|
+
# => BEGIN
|
119
|
+
# => SELECT pg_try_advisory_xact_lock(3764656399) -- 'ruby-forever'
|
120
|
+
# => COMMIT
|
121
|
+
```
|
122
|
+
|
123
|
+
## Duplicate
|
124
|
+
|
125
|
+
Enable: `Sequel::Model.plugin :duplicate`
|
126
|
+
|
127
|
+
Model plugin for creating a copies.
|
128
|
+
|
129
|
+
Example:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
User = Sequel::Model(:users)
|
133
|
+
user1 = User.create(name: "John")
|
134
|
+
user2 = user1.duplicate(name: "James")
|
135
|
+
user2.name # => "James"
|
136
|
+
```
|
137
|
+
OR
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
user2 = User.duplicate(user1, name: "James")
|
141
|
+
user2.name # => "James"
|
142
|
+
```
|
143
|
+
|
144
|
+
## GetColumnValue
|
145
|
+
|
146
|
+
Enable: `Sequel::Model.plugin :get_column_value`
|
147
|
+
|
148
|
+
Plugin for getting raw column value
|
149
|
+
|
150
|
+
Example:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
item = Item.first
|
154
|
+
item.price # => #<Money fractional:5000.0 currency:USD>
|
155
|
+
item.get_column_value(:amount) # => 0.5e2
|
156
|
+
```
|
157
|
+
|
158
|
+
## StoreAccessors
|
159
|
+
|
160
|
+
Enable: `Sequel::Model.plugin :store_accessors`
|
161
|
+
|
162
|
+
Plugin for using jsonb field keys as model properties.
|
163
|
+
|
164
|
+
Example:
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
class User < Sequel::Model
|
168
|
+
store :data, :first_name
|
169
|
+
end
|
170
|
+
|
171
|
+
user = User.create(first_name: "John")
|
172
|
+
user.first_name # => "John"
|
173
|
+
user.data # => {"first_name": "John"}
|
174
|
+
```
|
175
|
+
|
176
|
+
## Synchronize
|
177
|
+
|
178
|
+
**Important:** requires a `synchronize` extension described above.
|
179
|
+
|
180
|
+
Same as `DB#synchronize_with`
|
181
|
+
|
182
|
+
Enable:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
DB.extension :synchronize
|
186
|
+
Sequel::Model.plugin :synchronize
|
187
|
+
```
|
188
|
+
|
189
|
+
Example:
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
user = User.first
|
193
|
+
user.synchronize([:ruby, :forever]) { p "Hey, I'm in transaction!"; sleep 5 }
|
194
|
+
```
|
195
|
+
|
196
|
+
## Upsert
|
197
|
+
|
198
|
+
Enable: `Sequel::Model.plugin :upsert`
|
199
|
+
|
200
|
+
Plugin for create an "UPSERT" requests to database.
|
201
|
+
|
202
|
+
Example:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
User.upsert(name: "John", email: "jd@test.com", target: :email)
|
206
|
+
User.upsert_dataset.insert(name: "John", email: "jd@test.com")
|
207
|
+
```
|
208
|
+
|
209
|
+
## WithLock
|
210
|
+
|
211
|
+
Enable: `Sequel::Model.plugin :with_lock`
|
212
|
+
|
213
|
+
Plugin for locking row for update.
|
214
|
+
|
215
|
+
Example:
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
user = User.first
|
219
|
+
user.with_lock do
|
220
|
+
user.update(name: "James")
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
## TimestampMigratorUndoExtension
|
225
|
+
Allows to undo a specific migration
|
226
|
+
|
227
|
+
Example:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
m = Sequel::TimestampMigrator.new(DB, "db/migrations")
|
231
|
+
m.undo(1549624163) # 1549624163 is a migration version
|
232
|
+
```
|
233
|
+
|
234
|
+
Also you can use `sequel:undo` rake task for it.
|
235
|
+
Example:
|
236
|
+
|
237
|
+
```sh
|
238
|
+
rake sequel:undo VERSION=1549624163
|
239
|
+
```
|
240
|
+
|
241
|
+
## License
|
242
|
+
Released under MIT License.
|
243
|
+
|
244
|
+
## Authors
|
245
|
+
Created by Aleksey Bespalov.
|
246
|
+
|
247
|
+
<a href="https://github.com/umbrellio/">
|
248
|
+
<img style="float: left;" src="https://umbrellio.github.io/Umbrellio/supported_by_umbrellio.svg" alt="Supported by Umbrellio" width="439" height="72">
|
249
|
+
</a>
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/database.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
::DB ||= Sequel.connect(ENV["DB_URL"] || "postgres://localhost/sequel_plugins")
|
4
|
+
Sequel::Model.db = DB
|
5
|
+
DB.extension :pg_array
|
6
|
+
DB.extension :pg_json
|
7
|
+
DB.extension :pg_range
|
8
|
+
|
9
|
+
DB.extension :currency_rates
|
10
|
+
DB.extension :pg_tools
|
11
|
+
DB.extension :slave
|
12
|
+
DB.extension :synchronize
|
13
|
+
|
14
|
+
Sequel.extension :migration
|
15
|
+
Sequel.extension :pg_array_ops
|
16
|
+
Sequel.extension :pg_json_ops
|
17
|
+
Sequel.extension :pg_range_ops
|
18
|
+
|
19
|
+
Sequel::Model.plugin :duplicate
|
20
|
+
Sequel::Model.plugin :get_column_value
|
21
|
+
Sequel::Model.plugin :store_accessors
|
22
|
+
Sequel::Model.plugin :synchronize
|
23
|
+
Sequel::Model.plugin :upsert
|
24
|
+
Sequel::Model.plugin :with_lock
|
25
|
+
|
26
|
+
def clean_database!
|
27
|
+
DB.tables.each do |table_name|
|
28
|
+
DB.drop_table?(table_name, cascade: true)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
# Extension for currency-conversion via currency_rates table
|
5
|
+
module CurrencyRates
|
6
|
+
# Join a rates table
|
7
|
+
#
|
8
|
+
# @param aliaz [Symbol] alias to be used for joined table
|
9
|
+
# @param table [Symbol] table name to join to
|
10
|
+
# @param time_column [Symbol] time column by which table is joined
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# Order::Model.with_rates.select(Sequel[:amount].in_usd)
|
14
|
+
# @return [Sequel::Dataset] dataset
|
15
|
+
def with_rates(
|
16
|
+
aliaz = :currency_rates,
|
17
|
+
table: table_name,
|
18
|
+
rates_table: Sequel[:currency_rates],
|
19
|
+
time_column: :created_at
|
20
|
+
)
|
21
|
+
table = Sequel[table]
|
22
|
+
rates = Sequel[aliaz]
|
23
|
+
join_expr = table[:currency] =~ rates[:currency]
|
24
|
+
join_expr &= rates[:period].pg_range.contains(table[time_column])
|
25
|
+
left_join(rates_table.as(aliaz), join_expr)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a table name
|
29
|
+
#
|
30
|
+
# @return [Symbol] table name
|
31
|
+
def table_name
|
32
|
+
respond_to?(:first_source_alias) ? first_source_alias : super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module CurrencyRateExchange
|
37
|
+
# Exchange column value to a specific currency
|
38
|
+
#
|
39
|
+
# @param currency [String] currency
|
40
|
+
# @param rates_table [Symbol] rates table name
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# Sequel[:amount].exchange_to("EUR", :order_rates)
|
44
|
+
# @return [Sequel::SQL::NumericExpression]
|
45
|
+
def exchange_to(currency, rates_table = :currency_rates)
|
46
|
+
rate = Sequel[rates_table][:rates].pg_jsonb.get_text(currency).cast_numeric(Float)
|
47
|
+
self * rate
|
48
|
+
end
|
49
|
+
|
50
|
+
# Exchange column value to usd
|
51
|
+
#
|
52
|
+
# @param opts (see #exchange_to)
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# Sequel[:amount].in_usd
|
56
|
+
# @return (see #exchange_to)
|
57
|
+
def in_usd(*opts)
|
58
|
+
exchange_to("USD", *opts)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
Model.extend(CurrencyRates)
|
63
|
+
SQL::GenericExpression.include(CurrencyRateExchange)
|
64
|
+
Dataset.register_extension(:currency_rates, CurrencyRates)
|
65
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
# Extension with some tools that use pg internal tables and views
|
5
|
+
module PGTools
|
6
|
+
# List inherited tables for specific parent table
|
7
|
+
#
|
8
|
+
# @param table_name [String, Symbol] name of the parent table
|
9
|
+
# @param schema [String, Symbol] schema of the parent table, defaults to +:public+
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# DB.inherited_tables_for(:event_log)
|
13
|
+
# # => [:event_log_2019_01, :event_log_2019_02]
|
14
|
+
#
|
15
|
+
# DB.inherited_tables_for(:event_log, schema: :foo)
|
16
|
+
# # => []
|
17
|
+
# @return [Array<Symbol>] list of inhertied tables
|
18
|
+
def inherited_tables_for(table_name, schema: :public)
|
19
|
+
self[:pg_inherits]
|
20
|
+
.select(Sequel[:cn][:nspname].as(:schema), Sequel[:c][:relname].as(:child))
|
21
|
+
.left_join(Sequel[:pg_class].as(:c), Sequel[:inhrelid] => Sequel[:c][:oid])
|
22
|
+
.left_join(Sequel[:pg_class].as(:p), Sequel[:inhparent] => Sequel[:p][:oid])
|
23
|
+
.left_join(Sequel[:pg_namespace].as(:pn), Sequel[:pn][:oid] => Sequel[:p][:relnamespace])
|
24
|
+
.left_join(Sequel[:pg_namespace].as(:cn), Sequel[:cn][:oid] => Sequel[:c][:relnamespace])
|
25
|
+
.where(Sequel[:p][:relname] => table_name.to_s, Sequel[:pn][:nspname] => schema.to_s)
|
26
|
+
.to_a
|
27
|
+
.map { |x| x[:child].to_sym }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Database.register_extension(:pg_tools, PGTools)
|
32
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
# Extension for choosing a slave server
|
5
|
+
module Slave
|
6
|
+
# Turn to slave
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# DB[:users].slave.where(email: "test@test.com") # executes on a slave server
|
10
|
+
# @return [Sequel::Dataset] dataset
|
11
|
+
def slave
|
12
|
+
server(:slave)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Model.extend(Slave)
|
17
|
+
Dataset.register_extension(:slave, Slave)
|
18
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
# Allows you to use PostgreSQL transaction advisory locks for application-level mutexes
|
7
|
+
module Synchronize
|
8
|
+
AdvisoryLockTimeoutError = Class.new(StandardError)
|
9
|
+
LOCK_RETRY_INTERVAL = 0.5
|
10
|
+
|
11
|
+
# Use transaction advisory lock for block of code
|
12
|
+
#
|
13
|
+
# @param *args [Array[Strings]] used for build lock name (just join with "-")
|
14
|
+
# @param timeout: [Integer] hot much time (in seconds) to wait lock
|
15
|
+
# @param savepoint: [Boolean] transaction with savepoint or not.
|
16
|
+
# @param skip_if_locked: [Boolean]
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# DB.synchronize_with([:ruby, :forever]) { p "Hey, I'm in transaction!"; sleep 5 }
|
20
|
+
# @db_output
|
21
|
+
# => BEGIN
|
22
|
+
# => SELECT pg_try_advisory_xact_lock(3764656399) -- 'ruby-forever'
|
23
|
+
# => COMMIT
|
24
|
+
def synchronize_with(*args, timeout: 10, savepoint: false, skip_if_locked: false)
|
25
|
+
key = lock_key_for(args)
|
26
|
+
|
27
|
+
transaction(savepoint: savepoint) do
|
28
|
+
hash = key_hash(key)
|
29
|
+
if get_lock(key, hash, timeout: timeout, skip_if_locked: skip_if_locked)
|
30
|
+
log_info("locked with #{key} (#{hash})")
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def get_lock(key, hash, timeout:, skip_if_locked:)
|
39
|
+
return acquire_lock(key, hash) if skip_if_locked
|
40
|
+
|
41
|
+
Timeout.timeout(timeout, AdvisoryLockTimeoutError, timeout_error_message(key, timeout)) do
|
42
|
+
loop do
|
43
|
+
return true if acquire_lock(key, hash)
|
44
|
+
sleep LOCK_RETRY_INTERVAL
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def lock_key_for(args)
|
50
|
+
args.to_a.flatten.join("-")
|
51
|
+
end
|
52
|
+
|
53
|
+
def key_hash(key)
|
54
|
+
Digest::MD5.hexdigest(key)[0..7].hex
|
55
|
+
end
|
56
|
+
|
57
|
+
def timeout_error_message(key, timeout)
|
58
|
+
"Timeout exceeded for #{key} (#{timeout} seconds)"
|
59
|
+
end
|
60
|
+
|
61
|
+
def acquire_lock(key, hash)
|
62
|
+
self["SELECT pg_try_advisory_xact_lock(?) -- ?", hash, key].get
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
Database.register_extension(:synchronize, Synchronize)
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sequel analog for `ActiveRecord::Base#dup` method
|
4
|
+
module Sequel::Plugins::Duplicate
|
5
|
+
module ClassMethods
|
6
|
+
# Returns a copy of current model
|
7
|
+
#
|
8
|
+
# @param model [Sequel::Model] source object
|
9
|
+
# @param new_attrs [Hash] attributes to override
|
10
|
+
#
|
11
|
+
# @return [Sequel::Model]
|
12
|
+
def duplicate(model, **new_attrs)
|
13
|
+
pk = *primary_key
|
14
|
+
attrs = model.values.reject { |key, *| pk.include?(key) }
|
15
|
+
new(**attrs, **new_attrs)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
# Returns a copy of current model
|
21
|
+
#
|
22
|
+
# @param new_attrs [Hash] attributes to override
|
23
|
+
#
|
24
|
+
# @return [Sequel::Model]
|
25
|
+
def duplicate(**new_attrs)
|
26
|
+
self.class.duplicate(self, **new_attrs)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sequel uses send by default
|
4
|
+
module Sequel::Plugins::GetColumnValue
|
5
|
+
module InstanceMethods
|
6
|
+
# Returns a raw column value
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# o = Order::Model.first
|
10
|
+
# o.amount # => #<Money fractional:5000.0 currency:USD>
|
11
|
+
# o.get_column_value(:amount) # => 0.5e2
|
12
|
+
# @return value
|
13
|
+
def get_column_value(value)
|
14
|
+
self[value]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Creates accessors for json values
|
4
|
+
module Sequel::Plugins::StoreAccessors
|
5
|
+
module ClassMethods
|
6
|
+
# Setup a store
|
7
|
+
#
|
8
|
+
# @param column [Symbol] jsonb column
|
9
|
+
# @param fields [Array<Symbol>] keys in json, which will be accessors
|
10
|
+
# @example
|
11
|
+
# class User < Sequel::Model
|
12
|
+
# store :data, :first_name
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# user = User.create(first_name: "John")
|
16
|
+
# user.first_name # => "John"
|
17
|
+
# user.data # => {"first_name": "John"}
|
18
|
+
def store(column, *fields)
|
19
|
+
include_accessors_module
|
20
|
+
|
21
|
+
fields.each do |field|
|
22
|
+
define_store_getter(column, field)
|
23
|
+
define_store_setter(column, field)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def include_accessors_module
|
30
|
+
return if defined?(@_store_accessors_module)
|
31
|
+
@_store_accessors_module = Module.new
|
32
|
+
include @_store_accessors_module
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_store_getter(column, field)
|
36
|
+
@_store_accessors_module.module_eval do
|
37
|
+
define_method(field) do
|
38
|
+
send(column).to_h[field.to_s]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def define_store_setter(column, field)
|
44
|
+
@_store_accessors_module.module_eval do
|
45
|
+
define_method("#{field}=") do |value|
|
46
|
+
send("#{column}=", send(column).to_h.merge(field.to_s => value))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Allows you to use PostgreSQL transaction advisory locks for application-level mutexes
|
4
|
+
module Sequel::Plugins::Synchronize
|
5
|
+
module ClassMethods
|
6
|
+
# Watch Sequel::Synchronize#synchronize_with
|
7
|
+
def synchronize_with(*args, &block)
|
8
|
+
db.extension(:synchronize).synchronize_with(*args, &block)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
# Just like Sequel::Synchronize#synchronize_with,
|
14
|
+
# but name, which is joined from args, is combined with table_name and primary_key
|
15
|
+
def synchronize(*args, **options)
|
16
|
+
self.class.synchronize_with(lock_key_for(args), **options) { yield(reload) }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def lock_key_for(args)
|
22
|
+
[self.class.table_name, self[primary_key], *args].flatten.join("-")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequel::Plugins::Upsert
|
4
|
+
module ClassMethods
|
5
|
+
# Returns an upsert dataset
|
6
|
+
#
|
7
|
+
# @param target [Symbol] target column
|
8
|
+
# @example
|
9
|
+
# User.upsert_dataset.insert(name: "John", email: "jd@test.com")
|
10
|
+
#
|
11
|
+
# @return [Sequel::Dataset] dataset
|
12
|
+
def upsert_dataset(target: primary_key)
|
13
|
+
cols = columns - Array(primary_key)
|
14
|
+
update_spec = cols.map { |x| [x, Sequel[:excluded][x]] }
|
15
|
+
where_spec = cols.map { |x| [Sequel[table_name][x], Sequel[:excluded][x]] }
|
16
|
+
|
17
|
+
dataset.insert_conflict(
|
18
|
+
target: target,
|
19
|
+
update: update_spec,
|
20
|
+
update_where: Sequel.~(where_spec),
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Executes the upsert request
|
25
|
+
#
|
26
|
+
# @param row [Hash] values
|
27
|
+
# @param options [Hash] options
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# User.upsert(name: "John", email: "jd@test.com", target: :email)
|
31
|
+
# @return [Sequel::Model]
|
32
|
+
def upsert(row, **options)
|
33
|
+
upsert_dataset(**options).insert(sequel_values(row))
|
34
|
+
end
|
35
|
+
|
36
|
+
# Executes the upsert request for multiple rows
|
37
|
+
# @see #upsert
|
38
|
+
# @see #upsert_dataset
|
39
|
+
def multi_upsert(rows, **options)
|
40
|
+
rows = rows.map { |row| sequel_values(row) }
|
41
|
+
upsert_dataset(options).multi_insert(rows)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns formatted row values
|
45
|
+
#
|
46
|
+
# @param row [Hash]
|
47
|
+
#
|
48
|
+
# @return [Hash]
|
49
|
+
def sequel_values(row)
|
50
|
+
upsert_model.new(row).values
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns upsert model for current table
|
54
|
+
#
|
55
|
+
# @return [Sequel::Model]
|
56
|
+
def upsert_model
|
57
|
+
@upsert_model ||= Sequel::Model(table_name)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sequel::Plugins::WithLock
|
4
|
+
module InstanceMethods
|
5
|
+
# Execute block with lock
|
6
|
+
#
|
7
|
+
# @yield
|
8
|
+
def with_lock
|
9
|
+
return yield if @__locked
|
10
|
+
@__locked = true
|
11
|
+
|
12
|
+
begin
|
13
|
+
db.transaction do
|
14
|
+
lock!
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
ensure
|
18
|
+
@__locked = false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
# rubocop:disable Layout/ClassStructure
|
6
|
+
module Sequel
|
7
|
+
class TimestampMigrator
|
8
|
+
# Rollback a migration
|
9
|
+
def undo(version)
|
10
|
+
path = files.find { |file| migration_version_from_file(get_filename(file)) == version }
|
11
|
+
raise "Migration #{version} does not exist in the filesystem" unless path
|
12
|
+
|
13
|
+
filename = get_filename(path)
|
14
|
+
raise "Migration #{version} is not applied" unless applied_migrations.include?(filename)
|
15
|
+
|
16
|
+
migration = get_migration(path)
|
17
|
+
|
18
|
+
time = Time.now
|
19
|
+
db.log_info("Undoing migration #{filename}")
|
20
|
+
|
21
|
+
checked_transaction(migration) do
|
22
|
+
migration.apply(db, :down)
|
23
|
+
ds.filter(column => filename).delete
|
24
|
+
end
|
25
|
+
|
26
|
+
elapsed = format("%0.6f", Time.now - time)
|
27
|
+
db.log_info("Finished undoing migration #{filename}, took #{elapsed} seconds")
|
28
|
+
end
|
29
|
+
|
30
|
+
module TimestampMigratorLogger
|
31
|
+
# Setup the logger
|
32
|
+
def run
|
33
|
+
db.loggers << Logger.new($stdout, level: :info)
|
34
|
+
level = db.sql_log_level
|
35
|
+
db.sql_log_level = :debug
|
36
|
+
db.log_info("Begin applying migrations")
|
37
|
+
super
|
38
|
+
ensure
|
39
|
+
db.sql_log_level = level
|
40
|
+
db.loggers.pop
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Sequel::TimestampMigrator.prepend TimestampMigratorLogger
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def get_migration(path)
|
49
|
+
migration = load_migration_file(path)
|
50
|
+
|
51
|
+
return migration if Gem::Version.new(Sequel.version) >= Gem::Version.new("5.6")
|
52
|
+
# :nocov:
|
53
|
+
Migration.descendants.last
|
54
|
+
# :nocov:
|
55
|
+
end
|
56
|
+
|
57
|
+
def get_filename(path)
|
58
|
+
File.basename(path).downcase
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# rubocop:enable Layout/ClassStructure
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sequel/timestamp_migrator_undo_extension"
|
4
|
+
|
5
|
+
namespace :sequel do
|
6
|
+
# Rollback migrations that are absent in revision when deploying on staging
|
7
|
+
task rollback_missing_migrations: :environment do
|
8
|
+
# Extract migrations
|
9
|
+
def extract_migrations(path)
|
10
|
+
Dir.glob("#{path}/db/migrate/*.rb").map { |filename| File.basename(filename).to_i }
|
11
|
+
end
|
12
|
+
|
13
|
+
old_migrations = extract_migrations(ENV["OLD_RELEASE"])
|
14
|
+
new_migrations = extract_migrations(ENV["NEW_RELEASE"])
|
15
|
+
migrations_to_rollback = old_migrations - new_migrations
|
16
|
+
|
17
|
+
next if migrations_to_rollback.empty?
|
18
|
+
|
19
|
+
puts "Rolling back migrations:"
|
20
|
+
puts migrations_to_rollback
|
21
|
+
|
22
|
+
path = ::Rails.root.join("db/migrate")
|
23
|
+
migrator = Sequel::TimestampMigrator.new(DB, path, allow_missing_migration_files: true)
|
24
|
+
applied_migrations = migrator.applied_migrations.map(&:to_i)
|
25
|
+
migrations = applied_migrations.select { |m| m.in?(migrations_to_rollback) }.sort.reverse
|
26
|
+
|
27
|
+
migrations.each { |migration| migrator.undo(migration) }
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sequel/timestamp_migrator_undo_extension"
|
4
|
+
|
5
|
+
namespace :sequel do
|
6
|
+
# Rollback a specific migration
|
7
|
+
task undo: :environment do
|
8
|
+
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
|
9
|
+
raise "VERSION is required" unless version
|
10
|
+
|
11
|
+
path = ::Rails.root.join("db/migrate")
|
12
|
+
migrator = Sequel::TimestampMigrator.new(DB, path, allow_missing_migration_files: true)
|
13
|
+
migrator.undo(version)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "umbrellio-sequel-plugins"
|
8
|
+
spec.version = "0.1.0"
|
9
|
+
spec.authors = ["nulldef"]
|
10
|
+
spec.email = ["nulldefiner@gmail.com"]
|
11
|
+
spec.required_ruby_version = ">= 2.4"
|
12
|
+
|
13
|
+
spec.summary = "Sequel plugins"
|
14
|
+
spec.description = "Sequel plugins"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.require_paths = ["lib"]
|
18
|
+
|
19
|
+
spec.add_dependency "sequel"
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler"
|
22
|
+
spec.add_development_dependency "coveralls"
|
23
|
+
spec.add_development_dependency "pg"
|
24
|
+
spec.add_development_dependency "pry"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "simplecov"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: umbrellio-sequel-plugins
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- nulldef
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-02-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sequel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: coveralls
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pg
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: simplecov
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: Sequel plugins
|
126
|
+
email:
|
127
|
+
- nulldefiner@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rspec"
|
134
|
+
- ".rubocop.yml"
|
135
|
+
- ".travis.yml"
|
136
|
+
- Gemfile
|
137
|
+
- LICENSE
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
140
|
+
- bin/console
|
141
|
+
- bin/setup
|
142
|
+
- database.rb
|
143
|
+
- lib/sequel-plugins.rb
|
144
|
+
- lib/sequel/extensions/currency_rates.rb
|
145
|
+
- lib/sequel/extensions/pg_tools.rb
|
146
|
+
- lib/sequel/extensions/slave.rb
|
147
|
+
- lib/sequel/extensions/synchronize.rb
|
148
|
+
- lib/sequel/plugins/duplicate.rb
|
149
|
+
- lib/sequel/plugins/get_column_value.rb
|
150
|
+
- lib/sequel/plugins/store_accessors.rb
|
151
|
+
- lib/sequel/plugins/synchronize.rb
|
152
|
+
- lib/sequel/plugins/upsert.rb
|
153
|
+
- lib/sequel/plugins/with_lock.rb
|
154
|
+
- lib/sequel/timestamp_migrator_undo_extension.rb
|
155
|
+
- lib/sequel_plugins.rb
|
156
|
+
- lib/tasks/sequel/rollback_missing_migrations.rake
|
157
|
+
- lib/tasks/sequel/undo.rake
|
158
|
+
- umbrellio-sequel-plugins.gemspec
|
159
|
+
homepage:
|
160
|
+
licenses: []
|
161
|
+
metadata: {}
|
162
|
+
post_install_message:
|
163
|
+
rdoc_options: []
|
164
|
+
require_paths:
|
165
|
+
- lib
|
166
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '2.4'
|
171
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
requirements: []
|
177
|
+
rubyforge_project:
|
178
|
+
rubygems_version: 2.7.6
|
179
|
+
signing_key:
|
180
|
+
specification_version: 4
|
181
|
+
summary: Sequel plugins
|
182
|
+
test_files: []
|