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.
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
@@ -11,5 +11,5 @@ require_relative '../pgtk'
11
11
  # License:: MIT
12
12
  module Pgtk
13
13
  # Current version of the library.
14
- VERSION = '0.14.0'
14
+ VERSION = '0.16.0'
15
15
  end
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 host can't be nil" if host.nil?
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 environmant variable can't be nil" if var.nil?
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['pgsql']['host'],
88
- port: cfg['pgsql']['port'],
89
- dbname: cfg['pgsql']['dbname'],
90
- user: cfg['pgsql']['user'],
91
- password: cfg['pgsql']['password']
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.formatter = SimpleCov::Formatter::CoberturaFormatter
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 'minitest/autorun'
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
@@ -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 < Minitest::Test
17
+ class TestLiquibaseTask < Pgtk::Test
18
18
  def test_basic
19
- Dir.mktmpdir 'test' do |dir|
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 'test' do |dir|
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'
@@ -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 < Minitest::Test
16
+ class TestPgsqlTask < Pgtk::Test
17
17
  def test_basic
18
- Dir.mktmpdir 'test' do |dir|
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 'test' do |dir|
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 < Minitest::Test
22
+ class TestPool < Pgtk::Test
23
23
  def test_reads_version
24
- bootstrap do |pool|
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
- bootstrap do |pool|
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
- bootstrap do |pool|
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
- bootstrap do |pool|
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
- bootstrap(log: log) do |pool|
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
- bootstrap(log: log) do |pool|
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
- bootstrap do |pool|
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
- bootstrap do |pool|
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
- bootstrap do |pool|
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 'test' do |dir|
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
@@ -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