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 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: []