umbrellio-sequel-plugins 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/umbrellio/umbrellio-sequel-plugins.svg?branch=master)](https://travis-ci.org/umbrellio/umbrellio-sequel-plugins)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/github/umbrellio/umbrellio-sequel-plugins/badge.svg?branch=master)](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: []
|