pgtk 0.14.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/.github/workflows/typos.yml +19 -0
- data/.gitignore +4 -2
- data/.rubocop.yml +6 -6
- data/Gemfile +1 -0
- data/Gemfile.lock +25 -16
- data/README.md +46 -9
- data/REUSE.toml +8 -7
- data/Rakefile +0 -2
- data/lib/pgtk/impatient.rb +91 -0
- data/lib/pgtk/liquibase_task.rb +31 -1
- data/lib/pgtk/pgsql_task.rb +46 -2
- data/lib/pgtk/pool.rb +34 -1
- data/lib/pgtk/spy.rb +50 -1
- data/lib/pgtk/stash.rb +107 -0
- data/lib/pgtk/version.rb +1 -1
- data/lib/pgtk/wire.rb +24 -7
- data/pgtk.gemspec +3 -0
- data/test/test__helper.rb +62 -4
- data/test/test_impatient.rb +49 -0
- data/test/test_liquibase_task.rb +4 -4
- data/test/test_pgsql_task.rb +4 -4
- data/test/test_pool.rb +23 -41
- data/test/test_stash.rb +89 -0
- data/test/test_wire.rb +37 -0
- metadata +52 -5
- data/.simplecov +0 -23
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
@@ -21,10 +21,16 @@ end
|
|
21
21
|
# License:: MIT
|
22
22
|
class Pgtk::Wire::Direct
|
23
23
|
# Constructor.
|
24
|
+
#
|
25
|
+
# @param [String] host Host name of the PostgreSQL server
|
26
|
+
# @param [Integer] port Port number of the PostgreSQL server
|
27
|
+
# @param [String] dbname Database name
|
28
|
+
# @param [String] user Username
|
29
|
+
# @param [String] password Password
|
24
30
|
def initialize(host:, port:, dbname:, user:, password:)
|
25
31
|
raise "The host can't be nil" if host.nil?
|
26
32
|
@host = host
|
27
|
-
raise "The
|
33
|
+
raise "The port can't be nil" if port.nil?
|
28
34
|
@port = port
|
29
35
|
@dbname = dbname
|
30
36
|
@user = user
|
@@ -41,13 +47,20 @@ class Pgtk::Wire::Direct
|
|
41
47
|
end
|
42
48
|
|
43
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
|
+
#
|
44
55
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
45
56
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
46
57
|
# License:: MIT
|
47
58
|
class Pgtk::Wire::Env
|
48
59
|
# Constructor.
|
60
|
+
#
|
61
|
+
# @param [String] var The name of the environment variable with the connection URL
|
49
62
|
def initialize(var = 'DATABASE_URL')
|
50
|
-
raise "The name of the
|
63
|
+
raise "The name of the environment variable can't be nil" if var.nil?
|
51
64
|
@var = var
|
52
65
|
end
|
53
66
|
|
@@ -72,6 +85,9 @@ end
|
|
72
85
|
# License:: MIT
|
73
86
|
class Pgtk::Wire::Yaml
|
74
87
|
# Constructor.
|
88
|
+
#
|
89
|
+
# @param [String] file Path to the YAML configuration file
|
90
|
+
# @param [String] node The root node name in the YAML file containing PostgreSQL configuration
|
75
91
|
def initialize(file, node = 'pgsql')
|
76
92
|
raise "The name of the file can't be nil" if file.nil?
|
77
93
|
@file = file
|
@@ -83,12 +99,13 @@ class Pgtk::Wire::Yaml
|
|
83
99
|
def connection
|
84
100
|
raise "The file #{@file.inspect} not found" unless File.exist?(@file)
|
85
101
|
cfg = YAML.load_file(@file)
|
102
|
+
raise "The node '#{@node}' not found in YAML file #{@file.inspect}" unless cfg[@node]
|
86
103
|
Pgtk::Wire::Direct.new(
|
87
|
-
host: cfg[
|
88
|
-
port: cfg[
|
89
|
-
dbname: cfg[
|
90
|
-
user: cfg[
|
91
|
-
password: cfg[
|
104
|
+
host: cfg[@node]['host'],
|
105
|
+
port: cfg[@node]['port'],
|
106
|
+
dbname: cfg[@node]['dbname'],
|
107
|
+
user: cfg[@node]['user'],
|
108
|
+
password: cfg[@node]['password']
|
92
109
|
).connection
|
93
110
|
end
|
94
111
|
end
|
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
@@ -6,13 +6,71 @@
|
|
6
6
|
$stdout.sync = true
|
7
7
|
|
8
8
|
require 'simplecov'
|
9
|
-
SimpleCov.start
|
10
|
-
|
11
9
|
require 'simplecov-cobertura'
|
12
|
-
SimpleCov.
|
10
|
+
unless SimpleCov.running || ENV['PICKS']
|
11
|
+
SimpleCov.command_name('test')
|
12
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
13
|
+
[
|
14
|
+
SimpleCov::Formatter::HTMLFormatter,
|
15
|
+
SimpleCov::Formatter::CoberturaFormatter
|
16
|
+
]
|
17
|
+
)
|
18
|
+
SimpleCov.minimum_coverage 90
|
19
|
+
SimpleCov.minimum_coverage_by_file 90
|
20
|
+
SimpleCov.start do
|
21
|
+
add_filter 'test/'
|
22
|
+
add_filter 'vendor/'
|
23
|
+
add_filter 'target/'
|
24
|
+
track_files 'lib/**/*.rb'
|
25
|
+
track_files '*.rb'
|
26
|
+
end
|
27
|
+
end
|
13
28
|
|
29
|
+
require 'minitest/autorun'
|
14
30
|
require 'minitest/reporters'
|
15
31
|
Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
|
16
32
|
|
17
|
-
require '
|
33
|
+
require 'logger'
|
34
|
+
require 'loog'
|
35
|
+
require 'rake'
|
36
|
+
require 'rake/tasklib'
|
18
37
|
require_relative '../lib/pgtk'
|
38
|
+
require_relative '../lib/pgtk/liquibase_task'
|
39
|
+
require_relative '../lib/pgtk/pgsql_task'
|
40
|
+
|
41
|
+
class Pgtk::Test < Minitest::Test
|
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')
|
46
|
+
Pgtk::PgsqlTask.new("pgsql#{id}") do |t|
|
47
|
+
t.dir = File.join(dir, 'pgsql')
|
48
|
+
t.user = 'hello'
|
49
|
+
t.password = 'A B C привет ! & | !'
|
50
|
+
t.dbname = 'test'
|
51
|
+
t.yaml = f
|
52
|
+
t.quiet = true
|
53
|
+
end
|
54
|
+
Rake::Task["pgsql#{id}"].invoke
|
55
|
+
Pgtk::LiquibaseTask.new("liquibase#{id}") do |t|
|
56
|
+
t.master = File.join(__dir__, '../test-resources/master.xml')
|
57
|
+
t.yaml = f
|
58
|
+
t.quiet = true
|
59
|
+
end
|
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|
|
68
|
+
pool = Pgtk::Pool.new(
|
69
|
+
Pgtk::Wire::Yaml.new(f),
|
70
|
+
log: log
|
71
|
+
)
|
72
|
+
pool.start(1)
|
73
|
+
yield pool
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'loog'
|
7
|
+
require 'pg'
|
8
|
+
require 'qbash'
|
9
|
+
require 'rake'
|
10
|
+
require 'tmpdir'
|
11
|
+
require 'yaml'
|
12
|
+
require_relative 'test__helper'
|
13
|
+
require_relative '../lib/pgtk/pool'
|
14
|
+
require_relative '../lib/pgtk/impatient'
|
15
|
+
|
16
|
+
# Pool test.
|
17
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
18
|
+
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
19
|
+
# License:: MIT
|
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
|
+
|
28
|
+
def test_doesnt_interrupt
|
29
|
+
fake_pool do |pool|
|
30
|
+
id = Pgtk::Impatient.new(pool).exec(
|
31
|
+
'INSERT INTO book (title) VALUES ($1) RETURNING id',
|
32
|
+
['1984']
|
33
|
+
).first['id'].to_i
|
34
|
+
assert_predicate(id, :positive?)
|
35
|
+
end
|
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
|
49
|
+
end
|
data/test/test_liquibase_task.rb
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
|
-
require 'minitest/autorun'
|
7
6
|
require 'tmpdir'
|
8
7
|
require 'rake'
|
9
8
|
require 'yaml'
|
9
|
+
require_relative 'test__helper'
|
10
10
|
require_relative '../lib/pgtk/pgsql_task'
|
11
11
|
require_relative '../lib/pgtk/liquibase_task'
|
12
12
|
|
@@ -14,9 +14,9 @@ require_relative '../lib/pgtk/liquibase_task'
|
|
14
14
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
15
15
|
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
16
16
|
# License:: MIT
|
17
|
-
class TestLiquibaseTask <
|
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 < Minitest::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
@@ -3,19 +3,19 @@
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
|
-
require 'minitest/autorun'
|
7
6
|
require 'rake'
|
8
7
|
require 'tmpdir'
|
9
8
|
require 'yaml'
|
9
|
+
require_relative 'test__helper'
|
10
10
|
require_relative '../lib/pgtk/pgsql_task'
|
11
11
|
|
12
12
|
# Pgsql rake task test.
|
13
13
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
14
14
|
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
15
15
|
# License:: MIT
|
16
|
-
class TestPgsqlTask <
|
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 < Minitest::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
@@ -4,12 +4,12 @@
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
6
|
require 'loog'
|
7
|
-
require 'minitest/autorun'
|
8
7
|
require 'pg'
|
9
8
|
require 'qbash'
|
10
9
|
require 'rake'
|
11
10
|
require 'tmpdir'
|
12
11
|
require 'yaml'
|
12
|
+
require_relative 'test__helper'
|
13
13
|
require_relative '../lib/pgtk/liquibase_task'
|
14
14
|
require_relative '../lib/pgtk/pgsql_task'
|
15
15
|
require_relative '../lib/pgtk/pool'
|
@@ -19,9 +19,9 @@ require_relative '../lib/pgtk/spy'
|
|
19
19
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
20
20
|
# Copyright:: Copyright (c) 2017-2025 Yegor Bugayenko
|
21
21
|
# License:: MIT
|
22
|
-
class TestPool <
|
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 < Minitest::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 < Minitest::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 < Minitest::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 < Minitest::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 < Minitest::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 < Minitest::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 < Minitest::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 < Minitest::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 < Minitest::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')
|
@@ -181,33 +192,4 @@ class TestPool < Minitest::Test
|
|
181
192
|
end
|
182
193
|
end
|
183
194
|
end
|
184
|
-
|
185
|
-
private
|
186
|
-
|
187
|
-
def bootstrap(log: Loog::NULL)
|
188
|
-
Dir.mktmpdir 'test' do |dir|
|
189
|
-
id = rand(100..999)
|
190
|
-
Pgtk::PgsqlTask.new("pgsql#{id}") do |t|
|
191
|
-
t.dir = File.join(dir, 'pgsql')
|
192
|
-
t.user = 'hello'
|
193
|
-
t.password = 'A B C привет ! & | !'
|
194
|
-
t.dbname = 'test'
|
195
|
-
t.yaml = File.join(dir, 'cfg.yml')
|
196
|
-
t.quiet = true
|
197
|
-
end
|
198
|
-
Rake::Task["pgsql#{id}"].invoke
|
199
|
-
Pgtk::LiquibaseTask.new("liquibase#{id}") do |t|
|
200
|
-
t.master = File.join(__dir__, '../test-resources/master.xml')
|
201
|
-
t.yaml = File.join(dir, 'cfg.yml')
|
202
|
-
t.quiet = true
|
203
|
-
end
|
204
|
-
Rake::Task["liquibase#{id}"].invoke
|
205
|
-
pool = Pgtk::Pool.new(
|
206
|
-
Pgtk::Wire::Yaml.new(File.join(dir, 'cfg.yml')),
|
207
|
-
log: log
|
208
|
-
)
|
209
|
-
pool.start(1)
|
210
|
-
yield pool
|
211
|
-
end
|
212
|
-
end
|
213
195
|
end
|
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
|