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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb389fe1825d50f64cd532e633a3dec67a4f928035159e750adc6e5efeec35d5
4
- data.tar.gz: 96d345dbcb2215e77398394e4a15b0e96de53d9beb014860778920b69376355c
3
+ metadata.gz: f5d316eba88319837e0e0076de690cb533c6d384e84c3bf02caf4bc71737ce76
4
+ data.tar.gz: ffee85649afc871e9a639769ebee131019e93ad11a23a93726d4cc0768eb1186
5
5
  SHA512:
6
- metadata.gz: 64eef80f0ceccb5da54db1705b58877519a6db87b6af8cc9ccce7314181abdd0715d10bbae34dc638a2f25c55a9622c6b4666ce18ce4d35e57ce2f3ae4c90693
7
- data.tar.gz: 915a3ba9b2b9c30183a12fadb9e7cd7f91da96d7e1d5a9a5bc1ce145219231629bf574524e0c64f24928c5b7f83d9a428eb380bd8b8eacb11c25b9f144f2278d
6
+ metadata.gz: 38d446cdd29cd57eff8ba863f7c6f9a1fb9b18f490e6655eac0259c6945d5da2f6f0f5c18a6a4e7f55f7184c0408bc5f03228514ccbb1375c3974081b176450f
7
+ data.tar.gz: d8fba0d7db2ea8645f33f843d2e304a24d3b4579854b0581a408258fa2d28eb455b05005b6a7a70ff44ec2643268632c2ab8bfc8f0ac7ddaf7f566414d03bb26
data/.gitignore CHANGED
@@ -8,3 +8,5 @@ doc/
8
8
  node_modules/
9
9
  rdoc/
10
10
  vendor/
11
+
12
+ **/.claude/settings.local.json
data/.rubocop.yml CHANGED
@@ -27,13 +27,13 @@ Layout/EmptyLineAfterGuardClause:
27
27
  Metrics/AbcSize:
28
28
  Max: 100
29
29
  Metrics/CyclomaticComplexity:
30
- Max: 15
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: 15
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
- loog (0.6.0)
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.13)
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
- You can also track all SQL queries sent through the pool, with the help of `Pgtk::Spy`:
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
- Well, it works in
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
@@ -5,7 +5,6 @@
5
5
 
6
6
  require 'rubygems'
7
7
  require 'rake'
8
- require 'rdoc'
9
8
  require 'rake/clean'
10
9
 
11
10
  def name
@@ -171,6 +171,8 @@ class Pgtk::PgsqlTask < Rake::TaskLib
171
171
  }
172
172
  }.to_yaml
173
173
  )
174
- puts "PostgreSQL has been started in process ##{pid}, port #{port}" unless @quiet
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
@@ -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.15.0'
14
+ VERSION = '0.16.0'
15
15
  end
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 70
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 bootstrap(log: Loog::NULL)
42
- Dir.mktmpdir 'test' do |dir|
43
- id = rand(100..999)
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 = File.join(dir, 'cfg.yml')
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 = File.join(dir, 'cfg.yml')
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(File.join(dir, 'cfg.yml')),
69
+ Pgtk::Wire::Yaml.new(f),
61
70
  log: log
62
71
  )
63
72
  pool.start(1)
@@ -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
- bootstrap do |pool|
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
@@ -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 '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 < Pgtk::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'
@@ -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 '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 < Pgtk::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
@@ -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
- 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 < Pgtk::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 < 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
- 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 < Pgtk::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 < Pgtk::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 < Pgtk::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 < Pgtk::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 < Pgtk::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 < Pgtk::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 < Pgtk::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')
@@ -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.15.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