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 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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ inherit_gem:
2
+ rubocop-config-umbrellio: lib/rubocop.yml
3
+
4
+ AllCops:
5
+ DisplayCopNames: true
6
+ TargetRubyVersion: 2.4
7
+
8
+ Naming/FileName:
9
+ Exclude:
10
+ - lib/sequel-plugins.rb
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ group :development, :test do
6
+ gem "rubocop-config-umbrellio", github: "umbrellio/code-style"
7
+ end
8
+
9
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sequel"
5
+ require "irb"
6
+ require_relative "../database"
7
+
8
+ Dir["#{__dir__}/lib/**/*.rb"].each { |f| require f }
9
+
10
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sequel_plugins.rb"
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SequelPlugins
4
+ if defined?(::Rails)
5
+ Engine = Class.new(::Rails::Engine)
6
+ end
7
+ end
@@ -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: []