pgtk 0.15.0 → 0.16.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 +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +2 -2
- data/Gemfile.lock +9 -2
- data/README.md +40 -2
- data/Rakefile +0 -1
- data/lib/pgtk/pgsql_task.rb +3 -1
- data/lib/pgtk/stash.rb +107 -0
- data/lib/pgtk/version.rb +1 -1
- data/lib/pgtk/wire.rb +5 -0
- data/pgtk.gemspec +3 -0
- data/test/test__helper.rb +16 -7
- data/test/test_impatient.rb +20 -1
- data/test/test_liquibase_task.rb +2 -2
- data/test/test_pgsql_task.rb +2 -2
- data/test/test_pool.rb +21 -10
- data/test/test_stash.rb +89 -0
- data/test/test_wire.rb +37 -0
- metadata +46 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5d316eba88319837e0e0076de690cb533c6d384e84c3bf02caf4bc71737ce76
|
4
|
+
data.tar.gz: ffee85649afc871e9a639769ebee131019e93ad11a23a93726d4cc0768eb1186
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38d446cdd29cd57eff8ba863f7c6f9a1fb9b18f490e6655eac0259c6945d5da2f6f0f5c18a6a4e7f55f7184c0408bc5f03228514ccbb1375c3974081b176450f
|
7
|
+
data.tar.gz: d8fba0d7db2ea8645f33f843d2e304a24d3b4579854b0581a408258fa2d28eb455b05005b6a7a70ff44ec2643268632c2ab8bfc8f0ac7ddaf7f566414d03bb26
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -27,13 +27,13 @@ Layout/EmptyLineAfterGuardClause:
|
|
27
27
|
Metrics/AbcSize:
|
28
28
|
Max: 100
|
29
29
|
Metrics/CyclomaticComplexity:
|
30
|
-
Max:
|
30
|
+
Max: 20
|
31
31
|
Metrics/ClassLength:
|
32
32
|
Max: 200
|
33
33
|
Metrics/MethodLength:
|
34
34
|
Max: 100
|
35
35
|
Metrics/PerceivedComplexity:
|
36
|
-
Max:
|
36
|
+
Max: 20
|
37
37
|
Metrics/ParameterLists:
|
38
38
|
Max: 6
|
39
39
|
require: []
|
data/Gemfile.lock
CHANGED
@@ -3,6 +3,9 @@ PATH
|
|
3
3
|
specs:
|
4
4
|
pgtk (0.0.0)
|
5
5
|
backtrace (> 0)
|
6
|
+
concurrent-ruby (> 0)
|
7
|
+
joined (> 0)
|
8
|
+
logger (> 0)
|
6
9
|
loog (> 0)
|
7
10
|
pg (~> 1.1)
|
8
11
|
qbash (> 0)
|
@@ -15,15 +18,19 @@ GEM
|
|
15
18
|
ast (2.4.3)
|
16
19
|
backtrace (0.4.0)
|
17
20
|
builder (3.3.0)
|
21
|
+
concurrent-ruby (1.3.5)
|
18
22
|
differ (0.1.2)
|
19
23
|
docile (1.4.1)
|
20
24
|
elapsed (0.0.1)
|
21
25
|
loog (> 0)
|
22
26
|
tago (> 0)
|
27
|
+
joined (0.1.0)
|
23
28
|
json (2.11.3)
|
24
29
|
language_server-protocol (3.17.0.4)
|
25
30
|
lint_roller (1.1.0)
|
26
|
-
|
31
|
+
logger (1.7.0)
|
32
|
+
loog (0.6.1)
|
33
|
+
logger (~> 1.0)
|
27
34
|
minitest (5.25.5)
|
28
35
|
minitest-reporters (1.7.1)
|
29
36
|
ansi
|
@@ -49,7 +56,7 @@ GEM
|
|
49
56
|
loog (> 0)
|
50
57
|
tago (> 0)
|
51
58
|
racc (1.8.1)
|
52
|
-
rack (3.1.
|
59
|
+
rack (3.1.14)
|
53
60
|
rainbow (3.1.1)
|
54
61
|
rake (13.2.1)
|
55
62
|
random-port (0.7.5)
|
data/README.md
CHANGED
@@ -151,7 +151,10 @@ module Minitest
|
|
151
151
|
end
|
152
152
|
```
|
153
153
|
|
154
|
-
|
154
|
+
## Logging with `Pgtk::Spy`
|
155
|
+
|
156
|
+
You can also track all SQL queries sent through the pool,
|
157
|
+
with the help of `Pgtk::Spy`:
|
155
158
|
|
156
159
|
```ruby
|
157
160
|
require 'pgtk/spy'
|
@@ -160,11 +163,46 @@ pool = Pgtk::Spy.new(pool) do |sql|
|
|
160
163
|
end
|
161
164
|
```
|
162
165
|
|
163
|
-
|
166
|
+
## Query Caching with `Pgtk::Stash`
|
167
|
+
|
168
|
+
For applications with frequent read queries,
|
169
|
+
you can use `Pgtk::Stash` to add a caching layer:
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
require 'pgtk/stash'
|
173
|
+
stash = Pgtk::Stash.new(pgsql)
|
174
|
+
```
|
175
|
+
|
176
|
+
`Stash` automatically caches read queries and invalidates the cache
|
177
|
+
when tables are modified:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
# First execution runs the query against the database
|
181
|
+
result1 = stash.exec('SELECT * FROM users WHERE id = $1', [123])
|
182
|
+
# Second execution with the same query and parameters returns cached result
|
183
|
+
result2 = stash.exec('SELECT * FROM users WHERE id = $1', [123])
|
184
|
+
# This modifies the 'users' table, invalidating any cached queries for that table
|
185
|
+
stash.exec('UPDATE users SET name = $1 WHERE id = $2', ['John', 123])
|
186
|
+
# This will execute against the database again since cache was invalidated
|
187
|
+
result3 = stash.exec('SELECT * FROM users WHERE id = $1', [123])
|
188
|
+
```
|
189
|
+
|
190
|
+
Note that the caching implementation is basic and only suitable
|
191
|
+
for simple queries:
|
192
|
+
|
193
|
+
1. Queries must reference tables (using `FROM` or `JOIN`)
|
194
|
+
2. Cache is invalidated by table, not by specific rows
|
195
|
+
3. Write operations (`INSERT`, `UPDATE`, `DELETE`) bypass
|
196
|
+
the cache and invalidate all cached queries for affected tables
|
197
|
+
|
198
|
+
## Some Examples
|
199
|
+
|
200
|
+
This library works in
|
164
201
|
[netbout.com](https://github.com/yegor256/netbout),
|
165
202
|
[wts.zold.io](https://github.com/zold-io/wts.zold.io),
|
166
203
|
[mailanes.com](https://github.com/yegor256/mailanes), and
|
167
204
|
[0rsk.com](https://github.com/yegor256/0rsk).
|
205
|
+
|
168
206
|
They are all open source, you can see how they use `pgtk`.
|
169
207
|
|
170
208
|
## How to contribute
|
data/Rakefile
CHANGED
data/lib/pgtk/pgsql_task.rb
CHANGED
@@ -171,6 +171,8 @@ class Pgtk::PgsqlTask < Rake::TaskLib
|
|
171
171
|
}
|
172
172
|
}.to_yaml
|
173
173
|
)
|
174
|
-
|
174
|
+
return if @quiet
|
175
|
+
puts "PostgreSQL has been started in process ##{pid}, port #{port}"
|
176
|
+
puts "YAML config saved to #{@yaml}"
|
175
177
|
end
|
176
178
|
end
|
data/lib/pgtk/stash.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'concurrent-ruby'
|
7
|
+
require 'joined'
|
8
|
+
require 'loog'
|
9
|
+
require_relative '../pgtk'
|
10
|
+
|
11
|
+
# Database query cache implementation.
|
12
|
+
#
|
13
|
+
# Provides a caching layer for PostgreSQL queries, automatically invalidating
|
14
|
+
# the cache when tables are modified. Read queries are cached while write
|
15
|
+
# queries bypass the cache and invalidate related cached entries.
|
16
|
+
#
|
17
|
+
# Thread-safe with read-write locking.
|
18
|
+
#
|
19
|
+
# The implementation is very naive! Use it at your own risk.
|
20
|
+
#
|
21
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
22
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
23
|
+
# License:: MIT
|
24
|
+
class Pgtk::Stash
|
25
|
+
# Initialize a new Stash with query caching.
|
26
|
+
#
|
27
|
+
# @param [Object] pgsql PostgreSQL connection object
|
28
|
+
# @param [Hash] stash Optional existing stash to use (default: new empty stash)
|
29
|
+
# @param [Loog] loog Logger for debugging (default: null logger)
|
30
|
+
def initialize(pgsql, stash = {})
|
31
|
+
@pgsql = pgsql
|
32
|
+
@stash = stash
|
33
|
+
@stash[:queries] ||= {}
|
34
|
+
@stash[:tables] ||= {}
|
35
|
+
@entrance = Concurrent::ReentrantReadWriteLock.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Execute a SQL query with optional caching.
|
39
|
+
#
|
40
|
+
# Read queries are cached, while write queries bypass the cache and invalidate related entries.
|
41
|
+
#
|
42
|
+
# @param [String, Array<String>] query The SQL query to execute
|
43
|
+
# @param [Array] params Query parameters
|
44
|
+
# @return [PG::Result] Query result
|
45
|
+
def exec(query, params = [])
|
46
|
+
pure = (query.is_a?(Array) ? query.join(' ') : query).gsub(/\s+/, ' ').strip
|
47
|
+
if /(^|\s)(INSERT|DELETE|UPDATE|LOCK)\s/.match?(pure) || /(^|\s)pg_[a-z_]+\(/.match?(pure)
|
48
|
+
tables = pure.scan(/(?<=^|\s)(?:UPDATE|INSERT INTO|DELETE FROM|TRUNCATE)\s([a-z]+)(?=[^a-z]|$)/).map(&:first).uniq
|
49
|
+
ret = @pgsql.exec(pure, params)
|
50
|
+
@entrance.with_write_lock do
|
51
|
+
tables.each do |t|
|
52
|
+
@stash[:tables][t]&.each do |q|
|
53
|
+
@stash[:queries].delete(q)
|
54
|
+
end
|
55
|
+
@stash[:tables].delete(t)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
key = params.map(&:to_s).join(' -*&%^- ')
|
60
|
+
@entrance.with_write_lock { @stash[:queries][pure] ||= {} }
|
61
|
+
ret = @stash[:queries][pure][key]
|
62
|
+
if ret.nil?
|
63
|
+
ret = @pgsql.exec(pure, params)
|
64
|
+
unless /(?<=^|\s)(NOW\(\)|COMMIT|ROLLBACK|START TRANSACTION|TRUNCATE|TO WARNING)(?=;|\s|$)/.match?(pure)
|
65
|
+
@entrance.with_write_lock do
|
66
|
+
@stash[:queries][pure] ||= {}
|
67
|
+
@stash[:queries][pure][key] = ret
|
68
|
+
tables = pure.scan(/(?<=^|\s)(?:FROM|JOIN) ([a-z_]+)(?=\s|$)/).map(&:first).uniq
|
69
|
+
tables.each do |t|
|
70
|
+
@stash[:tables][t] = [] if @stash[:tables][t].nil?
|
71
|
+
@stash[:tables][t].append(pure).uniq!
|
72
|
+
end
|
73
|
+
raise "No tables at #{pure.inspect}" if tables.empty?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
ret
|
79
|
+
end
|
80
|
+
|
81
|
+
# Execute a database transaction.
|
82
|
+
#
|
83
|
+
# Yields a new Stash that shares the same cache but uses the transaction connection.
|
84
|
+
#
|
85
|
+
# @yield [Pgtk::Stash] A stash connected to the transaction
|
86
|
+
# @return [Object] The result of the block
|
87
|
+
def transaction
|
88
|
+
@pgsql.transaction do |t|
|
89
|
+
yield Pgtk::Stash.new(t, @stash)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Start a new connection pool with the given arguments.
|
94
|
+
#
|
95
|
+
# @param args Arguments to pass to the underlying pool's start method
|
96
|
+
# @return [Pgtk::Stash] A new stash that shares the same cache
|
97
|
+
def start(*args)
|
98
|
+
Pgtk::Stash.new(@pgsql.start(*args), @stash)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get the PostgreSQL server version.
|
102
|
+
#
|
103
|
+
# @return [String] Version string of the database server
|
104
|
+
def version
|
105
|
+
@pgsql.version
|
106
|
+
end
|
107
|
+
end
|
data/lib/pgtk/version.rb
CHANGED
data/lib/pgtk/wire.rb
CHANGED
@@ -47,6 +47,11 @@ class Pgtk::Wire::Direct
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# Using ENV variable.
|
50
|
+
#
|
51
|
+
# The value of the variable should be in this format:
|
52
|
+
#
|
53
|
+
# postgres://user:password@host:port/dbname
|
54
|
+
#
|
50
55
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
51
56
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
52
57
|
# License:: MIT
|
data/pgtk.gemspec
CHANGED
@@ -27,6 +27,9 @@ Gem::Specification.new do |s|
|
|
27
27
|
s.rdoc_options = ['--charset=UTF-8']
|
28
28
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
29
29
|
s.add_dependency 'backtrace', '>0'
|
30
|
+
s.add_dependency 'concurrent-ruby', '>0'
|
31
|
+
s.add_dependency 'joined', '>0'
|
32
|
+
s.add_dependency 'logger', '>0'
|
30
33
|
s.add_dependency 'loog', '>0'
|
31
34
|
s.add_dependency 'pg', '~>1.1'
|
32
35
|
s.add_dependency 'qbash', '>0'
|
data/test/test__helper.rb
CHANGED
@@ -16,7 +16,7 @@ unless SimpleCov.running || ENV['PICKS']
|
|
16
16
|
]
|
17
17
|
)
|
18
18
|
SimpleCov.minimum_coverage 90
|
19
|
-
SimpleCov.minimum_coverage_by_file
|
19
|
+
SimpleCov.minimum_coverage_by_file 90
|
20
20
|
SimpleCov.start do
|
21
21
|
add_filter 'test/'
|
22
22
|
add_filter 'vendor/'
|
@@ -30,6 +30,7 @@ require 'minitest/autorun'
|
|
30
30
|
require 'minitest/reporters'
|
31
31
|
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|
32
32
|
|
33
|
+
require 'logger'
|
33
34
|
require 'loog'
|
34
35
|
require 'rake'
|
35
36
|
require 'rake/tasklib'
|
@@ -38,26 +39,34 @@ require_relative '../lib/pgtk/liquibase_task'
|
|
38
39
|
require_relative '../lib/pgtk/pgsql_task'
|
39
40
|
|
40
41
|
class Pgtk::Test < Minitest::Test
|
41
|
-
def
|
42
|
-
Dir.mktmpdir
|
43
|
-
id =
|
42
|
+
def fake_config
|
43
|
+
Dir.mktmpdir do |dir|
|
44
|
+
id = (Time.now.to_f * 1_000_000).to_i % 1_000_000
|
45
|
+
f = File.join(dir, 'cfg.yml')
|
44
46
|
Pgtk::PgsqlTask.new("pgsql#{id}") do |t|
|
45
47
|
t.dir = File.join(dir, 'pgsql')
|
46
48
|
t.user = 'hello'
|
47
49
|
t.password = 'A B C привет ! & | !'
|
48
50
|
t.dbname = 'test'
|
49
|
-
t.yaml =
|
51
|
+
t.yaml = f
|
50
52
|
t.quiet = true
|
51
53
|
end
|
52
54
|
Rake::Task["pgsql#{id}"].invoke
|
53
55
|
Pgtk::LiquibaseTask.new("liquibase#{id}") do |t|
|
54
56
|
t.master = File.join(__dir__, '../test-resources/master.xml')
|
55
|
-
t.yaml =
|
57
|
+
t.yaml = f
|
56
58
|
t.quiet = true
|
57
59
|
end
|
58
60
|
Rake::Task["liquibase#{id}"].invoke
|
61
|
+
assert_path_exists(f)
|
62
|
+
yield f
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def fake_pool(log: Loog::NULL)
|
67
|
+
fake_config do |f|
|
59
68
|
pool = Pgtk::Pool.new(
|
60
|
-
Pgtk::Wire::Yaml.new(
|
69
|
+
Pgtk::Wire::Yaml.new(f),
|
61
70
|
log: log
|
62
71
|
)
|
63
72
|
pool.start(1)
|
data/test/test_impatient.rb
CHANGED
@@ -18,8 +18,15 @@ require_relative '../lib/pgtk/impatient'
|
|
18
18
|
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
19
19
|
# License:: MIT
|
20
20
|
class TestImpatient < Pgtk::Test
|
21
|
+
def test_takes_version
|
22
|
+
fake_pool do |pool|
|
23
|
+
v = Pgtk::Impatient.new(pool).version
|
24
|
+
refute_nil(v)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
21
28
|
def test_doesnt_interrupt
|
22
|
-
|
29
|
+
fake_pool do |pool|
|
23
30
|
id = Pgtk::Impatient.new(pool).exec(
|
24
31
|
'INSERT INTO book (title) VALUES ($1) RETURNING id',
|
25
32
|
['1984']
|
@@ -27,4 +34,16 @@ class TestImpatient < Pgtk::Test
|
|
27
34
|
assert_predicate(id, :positive?)
|
28
35
|
end
|
29
36
|
end
|
37
|
+
|
38
|
+
def test_doesnt_interrupt_in_transaction
|
39
|
+
fake_pool do |pool|
|
40
|
+
Pgtk::Impatient.new(pool).transaction do |t|
|
41
|
+
id = t.exec(
|
42
|
+
'INSERT INTO book (title) VALUES ($1) RETURNING id',
|
43
|
+
['1984']
|
44
|
+
).first['id'].to_i
|
45
|
+
assert_predicate(id, :positive?)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
30
49
|
end
|
data/test/test_liquibase_task.rb
CHANGED
@@ -16,7 +16,7 @@ require_relative '../lib/pgtk/liquibase_task'
|
|
16
16
|
# License:: MIT
|
17
17
|
class TestLiquibaseTask < Pgtk::Test
|
18
18
|
def test_basic
|
19
|
-
Dir.mktmpdir
|
19
|
+
Dir.mktmpdir do |dir|
|
20
20
|
Pgtk::PgsqlTask.new(:pgsql2) do |t|
|
21
21
|
t.dir = File.join(dir, 'pgsql')
|
22
22
|
t.user = 'hello'
|
@@ -39,7 +39,7 @@ class TestLiquibaseTask < Pgtk::Test
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def test_latest_version
|
42
|
-
Dir.mktmpdir
|
42
|
+
Dir.mktmpdir do |dir|
|
43
43
|
Pgtk::PgsqlTask.new(:pgsql) do |t|
|
44
44
|
t.dir = File.join(dir, 'pgsql')
|
45
45
|
t.user = 'xxx'
|
data/test/test_pgsql_task.rb
CHANGED
@@ -15,7 +15,7 @@ require_relative '../lib/pgtk/pgsql_task'
|
|
15
15
|
# License:: MIT
|
16
16
|
class TestPgsqlTask < Pgtk::Test
|
17
17
|
def test_basic
|
18
|
-
Dir.mktmpdir
|
18
|
+
Dir.mktmpdir do |dir|
|
19
19
|
Pgtk::PgsqlTask.new(:p2) do |t|
|
20
20
|
t.dir = File.join(dir, 'pgsql')
|
21
21
|
t.user = 'hello'
|
@@ -34,7 +34,7 @@ class TestPgsqlTask < Pgtk::Test
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def test_not_quiet
|
37
|
-
Dir.mktmpdir
|
37
|
+
Dir.mktmpdir do |dir|
|
38
38
|
Pgtk::PgsqlTask.new(:p3) do |t|
|
39
39
|
t.dir = File.join(dir, 'pgsql')
|
40
40
|
t.user = 'hello'
|
data/test/test_pool.rb
CHANGED
@@ -21,7 +21,7 @@ require_relative '../lib/pgtk/spy'
|
|
21
21
|
# License:: MIT
|
22
22
|
class TestPool < Pgtk::Test
|
23
23
|
def test_reads_version
|
24
|
-
|
24
|
+
fake_pool do |pool|
|
25
25
|
ver = pool.version
|
26
26
|
assert(ver.start_with?('1'))
|
27
27
|
refute_includes(ver, ' ')
|
@@ -29,7 +29,7 @@ class TestPool < Pgtk::Test
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def test_basic
|
32
|
-
|
32
|
+
fake_pool do |pool|
|
33
33
|
id = pool.exec(
|
34
34
|
'INSERT INTO book (title) VALUES ($1) RETURNING id',
|
35
35
|
['Elegant Objects']
|
@@ -38,9 +38,20 @@ class TestPool < Pgtk::Test
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
def test_queries_with_block
|
42
|
+
fake_pool do |pool|
|
43
|
+
pool.exec('INSERT INTO book (title) VALUES ($1)', ['1984'])
|
44
|
+
rows = []
|
45
|
+
pool.exec('SELECT * FROM book') do |row|
|
46
|
+
rows.append(row)
|
47
|
+
end
|
48
|
+
assert_equal(1, rows.size)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
41
52
|
def test_with_spy
|
42
53
|
queries = []
|
43
|
-
|
54
|
+
fake_pool do |pool|
|
44
55
|
pool = Pgtk::Spy.new(pool) { |sql| queries.append(sql) }
|
45
56
|
pool.exec(
|
46
57
|
['INSERT INTO book', '(title) VALUES ($1)'],
|
@@ -52,7 +63,7 @@ class TestPool < Pgtk::Test
|
|
52
63
|
end
|
53
64
|
|
54
65
|
def test_complex_query
|
55
|
-
|
66
|
+
fake_pool do |pool|
|
56
67
|
pool.exec(
|
57
68
|
"
|
58
69
|
INSERT INTO book (title) VALUES ('one');
|
@@ -64,7 +75,7 @@ class TestPool < Pgtk::Test
|
|
64
75
|
|
65
76
|
def test_logs_sql
|
66
77
|
log = Loog::Buffer.new
|
67
|
-
|
78
|
+
fake_pool(log: log) do |pool|
|
68
79
|
pool.exec(
|
69
80
|
'INSERT INTO book (title) VALUES ($1)',
|
70
81
|
['Object Thinking']
|
@@ -75,7 +86,7 @@ class TestPool < Pgtk::Test
|
|
75
86
|
|
76
87
|
def test_logs_errors
|
77
88
|
log = Loog::Buffer.new
|
78
|
-
|
89
|
+
fake_pool(log: log) do |pool|
|
79
90
|
assert_raises(PG::UndefinedTable) do
|
80
91
|
pool.exec('INSERT INTO tableDoesNotExist (a) VALUES (42)')
|
81
92
|
end
|
@@ -84,7 +95,7 @@ class TestPool < Pgtk::Test
|
|
84
95
|
end
|
85
96
|
|
86
97
|
def test_transaction
|
87
|
-
|
98
|
+
fake_pool do |pool|
|
88
99
|
id = Pgtk::Spy.new(pool).transaction do |t|
|
89
100
|
t.exec('DELETE FROM book')
|
90
101
|
t.exec(
|
@@ -100,7 +111,7 @@ class TestPool < Pgtk::Test
|
|
100
111
|
end
|
101
112
|
|
102
113
|
def test_transaction_with_error
|
103
|
-
|
114
|
+
fake_pool do |pool|
|
104
115
|
pool.exec('DELETE FROM book')
|
105
116
|
assert_empty(pool.exec('SELECT * FROM book'))
|
106
117
|
assert_raises(StandardError) do
|
@@ -116,7 +127,7 @@ class TestPool < Pgtk::Test
|
|
116
127
|
end
|
117
128
|
|
118
129
|
def test_reconnects_on_pg_error
|
119
|
-
|
130
|
+
fake_pool do |pool|
|
120
131
|
assert_raises PG::UndefinedTable do
|
121
132
|
pool.exec('SELECT * FROM thisiserror')
|
122
133
|
end
|
@@ -128,7 +139,7 @@ class TestPool < Pgtk::Test
|
|
128
139
|
|
129
140
|
def test_reconnects_on_pg_reboot
|
130
141
|
port = RandomPort::Pool::SINGLETON.acquire
|
131
|
-
Dir.mktmpdir
|
142
|
+
Dir.mktmpdir do |dir|
|
132
143
|
id = rand(100..999)
|
133
144
|
Pgtk::PgsqlTask.new("pgsql#{id}") do |t|
|
134
145
|
t.dir = File.join(dir, 'pgsql')
|
data/test/test_stash.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require_relative 'test__helper'
|
7
|
+
require_relative '../lib/pgtk/pool'
|
8
|
+
require_relative '../lib/pgtk/stash'
|
9
|
+
|
10
|
+
# Pool test.
|
11
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
12
|
+
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
13
|
+
# License:: MIT
|
14
|
+
class TestStash < Pgtk::Test
|
15
|
+
def test_simple_insert
|
16
|
+
fake_pool do |pool|
|
17
|
+
id = Pgtk::Stash.new(pool).exec(
|
18
|
+
'INSERT INTO book (title) VALUES ($1) RETURNING id',
|
19
|
+
['Elegant Objects']
|
20
|
+
)[0]['id'].to_i
|
21
|
+
assert_predicate(id, :positive?)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_caching
|
26
|
+
fake_pool do |pool|
|
27
|
+
stash = Pgtk::Stash.new(pool)
|
28
|
+
query = 'SELECT count(*) FROM book'
|
29
|
+
first_result = stash.exec(query)
|
30
|
+
second_result = stash.exec(query)
|
31
|
+
assert_equal(first_result.to_a, second_result.to_a)
|
32
|
+
assert_same(first_result, second_result)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_cache_invalidation
|
37
|
+
fake_pool do |pool|
|
38
|
+
stash = Pgtk::Stash.new(pool)
|
39
|
+
query = 'SELECT count(*) FROM book'
|
40
|
+
first_result = stash.exec(query)
|
41
|
+
stash.exec('INSERT INTO book (title) VALUES ($1)', ['New Book'])
|
42
|
+
second_result = stash.exec(query)
|
43
|
+
refute_same(first_result, second_result)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_caching_with_params
|
48
|
+
fake_pool do |pool|
|
49
|
+
stash = Pgtk::Stash.new(pool)
|
50
|
+
query = 'SELECT * FROM book WHERE title = $1'
|
51
|
+
first_result = stash.exec(query, ['Elegant Objects'])
|
52
|
+
second_result = stash.exec(query, ['Elegant Objects'])
|
53
|
+
assert_equal(first_result.to_a, second_result.to_a)
|
54
|
+
assert_same(first_result, second_result)
|
55
|
+
different_param_result = stash.exec(query, ['Different Title'])
|
56
|
+
refute_same(first_result, different_param_result)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_version
|
61
|
+
fake_pool do |pool|
|
62
|
+
stash = Pgtk::Stash.new(pool)
|
63
|
+
assert_match(/^\d+\.\d+/, stash.version)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_transaction
|
68
|
+
fake_pool do |pool|
|
69
|
+
stash = Pgtk::Stash.new(pool)
|
70
|
+
stash.exec('INSERT INTO book (title) VALUES ($1)', ['Transaction Test'])
|
71
|
+
stash.transaction do |tx|
|
72
|
+
result = tx.exec('SELECT title FROM book WHERE title = $1', ['Transaction Test'])
|
73
|
+
assert_equal('Transaction Test', result[0]['title'])
|
74
|
+
true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_start
|
80
|
+
fake_pool do |pool|
|
81
|
+
stash = Pgtk::Stash.new(pool)
|
82
|
+
stash.exec('INSERT INTO book (title) VALUES ($1)', ['Start Test'])
|
83
|
+
new_stash = stash.start(1)
|
84
|
+
assert_instance_of(Pgtk::Stash, new_stash)
|
85
|
+
result = new_stash.exec('SELECT title FROM book WHERE title = $1', ['Start Test'])
|
86
|
+
assert_equal('Start Test', result[0]['title'])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/test/test_wire.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'cgi'
|
7
|
+
require 'yaml'
|
8
|
+
require_relative 'test__helper'
|
9
|
+
require_relative '../lib/pgtk/wire'
|
10
|
+
|
11
|
+
# Wire test.
|
12
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
13
|
+
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
14
|
+
# License:: MIT
|
15
|
+
class TestWire < Pgtk::Test
|
16
|
+
def test_connects
|
17
|
+
fake_config do |f|
|
18
|
+
wire = Pgtk::Wire::Yaml.new(f)
|
19
|
+
c = wire.connection
|
20
|
+
refute_nil(c)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_connects_via_env_variable
|
25
|
+
fake_config do |f|
|
26
|
+
c = YAML.load_file(f)['pgsql']
|
27
|
+
v = 'DATABASE_URL'
|
28
|
+
ENV[v] = [
|
29
|
+
"postgres://#{CGI.escape(c['user'])}:#{CGI.escape(c['password'])}",
|
30
|
+
"@#{CGI.escape(c['host'])}:#{CGI.escape(c['port'].to_s)}/#{CGI.escape(c['dbname'])}"
|
31
|
+
].join
|
32
|
+
wire = Pgtk::Wire::Env.new(v)
|
33
|
+
c = wire.connection
|
34
|
+
refute_nil(c)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pgtk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
@@ -23,6 +23,48 @@ dependencies:
|
|
23
23
|
- - ">"
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: concurrent-ruby
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: joined
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: logger
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
26
68
|
- !ruby/object:Gem::Dependency
|
27
69
|
name: loog
|
28
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,6 +163,7 @@ files:
|
|
121
163
|
- lib/pgtk/pgsql_task.rb
|
122
164
|
- lib/pgtk/pool.rb
|
123
165
|
- lib/pgtk/spy.rb
|
166
|
+
- lib/pgtk/stash.rb
|
124
167
|
- lib/pgtk/version.rb
|
125
168
|
- lib/pgtk/wire.rb
|
126
169
|
- pgtk.gemspec
|
@@ -133,6 +176,8 @@ files:
|
|
133
176
|
- test/test_liquibase_task.rb
|
134
177
|
- test/test_pgsql_task.rb
|
135
178
|
- test/test_pool.rb
|
179
|
+
- test/test_stash.rb
|
180
|
+
- test/test_wire.rb
|
136
181
|
homepage: http://github.com/yegor256/pgtk
|
137
182
|
licenses:
|
138
183
|
- MIT
|