pgtk 0.14.0 → 0.15.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 +2 -2
- data/.rubocop.yml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +17 -15
- data/README.md +7 -8
- data/REUSE.toml +8 -7
- data/Rakefile +0 -1
- data/lib/pgtk/impatient.rb +91 -0
- data/lib/pgtk/liquibase_task.rb +31 -1
- data/lib/pgtk/pgsql_task.rb +43 -1
- data/lib/pgtk/pool.rb +34 -1
- data/lib/pgtk/spy.rb +50 -1
- data/lib/pgtk/version.rb +1 -1
- data/lib/pgtk/wire.rb +19 -7
- data/test/test__helper.rb +53 -4
- data/test/test_impatient.rb +30 -0
- data/test/test_liquibase_task.rb +2 -2
- data/test/test_pgsql_task.rb +2 -2
- data/test/test_pool.rb +2 -31
- metadata +7 -5
- data/.simplecov +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb389fe1825d50f64cd532e633a3dec67a4f928035159e750adc6e5efeec35d5
|
4
|
+
data.tar.gz: 96d345dbcb2215e77398394e4a15b0e96de53d9beb014860778920b69376355c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64eef80f0ceccb5da54db1705b58877519a6db87b6af8cc9ccce7314181abdd0715d10bbae34dc638a2f25c55a9622c6b4666ce18ce4d35e57ce2f3ae4c90693
|
7
|
+
data.tar.gz: 915a3ba9b2b9c30183a12fadb9e7cd7f91da96d7e1d5a9a5bc1ce145219231629bf574524e0c64f24928c5b7f83d9a428eb380bd8b8eacb11c25b9f144f2278d
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
---
|
4
|
+
# yamllint disable rule:line-length
|
5
|
+
name: typos
|
6
|
+
'on':
|
7
|
+
push:
|
8
|
+
branches:
|
9
|
+
- master
|
10
|
+
pull_request:
|
11
|
+
branches:
|
12
|
+
- master
|
13
|
+
jobs:
|
14
|
+
typos:
|
15
|
+
timeout-minutes: 15
|
16
|
+
runs-on: ubuntu-24.04
|
17
|
+
steps:
|
18
|
+
- uses: actions/checkout@v4
|
19
|
+
- uses: crate-ci/typos@v1.32.0
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -10,6 +10,10 @@ AllCops:
|
|
10
10
|
TargetRubyVersion: 2.3
|
11
11
|
SuggestExtensions: false
|
12
12
|
NewCops: enable
|
13
|
+
plugins:
|
14
|
+
- rubocop-rake
|
15
|
+
- rubocop-minitest
|
16
|
+
- rubocop-performance
|
13
17
|
Minitest/EmptyLineBeforeAssertionMethods:
|
14
18
|
Enabled: false
|
15
19
|
Style/ClassAndModuleChildren:
|
@@ -32,8 +36,4 @@ Metrics/PerceivedComplexity:
|
|
32
36
|
Max: 15
|
33
37
|
Metrics/ParameterLists:
|
34
38
|
Max: 6
|
35
|
-
plugins:
|
36
|
-
- rubocop-rake
|
37
|
-
- rubocop-minitest
|
38
|
-
- rubocop-performance
|
39
39
|
require: []
|
data/Gemfile
CHANGED
@@ -17,5 +17,6 @@ gem 'rubocop-rake', '>0', require: false
|
|
17
17
|
gem 'rubocop-rspec', '>0', require: false
|
18
18
|
gem 'simplecov', '~>0.22', require: false
|
19
19
|
gem 'simplecov-cobertura', '~>2.1'
|
20
|
+
gem 'timeout', '>0'
|
20
21
|
gem 'xcop', '>0', require: false
|
21
22
|
gem 'yard', '~>0.9', require: false
|
data/Gemfile.lock
CHANGED
@@ -20,7 +20,7 @@ GEM
|
|
20
20
|
elapsed (0.0.1)
|
21
21
|
loog (> 0)
|
22
22
|
tago (> 0)
|
23
|
-
json (2.
|
23
|
+
json (2.11.3)
|
24
24
|
language_server-protocol (3.17.0.4)
|
25
25
|
lint_roller (1.1.0)
|
26
26
|
loog (0.6.0)
|
@@ -30,14 +30,14 @@ GEM
|
|
30
30
|
builder
|
31
31
|
minitest (>= 5.0)
|
32
32
|
ruby-progressbar
|
33
|
-
nokogiri (1.18.
|
33
|
+
nokogiri (1.18.8-arm64-darwin)
|
34
34
|
racc (~> 1.4)
|
35
|
-
nokogiri (1.18.
|
35
|
+
nokogiri (1.18.8-x64-mingw-ucrt)
|
36
36
|
racc (~> 1.4)
|
37
|
-
nokogiri (1.18.
|
37
|
+
nokogiri (1.18.8-x86_64-linux-gnu)
|
38
38
|
racc (~> 1.4)
|
39
|
-
parallel (1.
|
40
|
-
parser (3.3.
|
39
|
+
parallel (1.27.0)
|
40
|
+
parser (3.3.8.0)
|
41
41
|
ast (~> 2.4.1)
|
42
42
|
racc
|
43
43
|
pg (1.5.9)
|
@@ -49,14 +49,14 @@ GEM
|
|
49
49
|
loog (> 0)
|
50
50
|
tago (> 0)
|
51
51
|
racc (1.8.1)
|
52
|
-
rack (3.1.
|
52
|
+
rack (3.1.13)
|
53
53
|
rainbow (3.1.1)
|
54
54
|
rake (13.2.1)
|
55
55
|
random-port (0.7.5)
|
56
56
|
tago (> 0)
|
57
57
|
regexp_parser (2.10.0)
|
58
58
|
rexml (3.4.1)
|
59
|
-
rubocop (1.75.
|
59
|
+
rubocop (1.75.5)
|
60
60
|
json (~> 2.3)
|
61
61
|
language_server-protocol (~> 3.17.0.2)
|
62
62
|
lint_roller (~> 1.1.0)
|
@@ -64,24 +64,24 @@ GEM
|
|
64
64
|
parser (>= 3.3.0.2)
|
65
65
|
rainbow (>= 2.2.2, < 4.0)
|
66
66
|
regexp_parser (>= 2.9.3, < 3.0)
|
67
|
-
rubocop-ast (>= 1.
|
67
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
68
68
|
ruby-progressbar (~> 1.7)
|
69
69
|
unicode-display_width (>= 2.4.0, < 4.0)
|
70
|
-
rubocop-ast (1.
|
70
|
+
rubocop-ast (1.44.1)
|
71
71
|
parser (>= 3.3.7.2)
|
72
72
|
prism (~> 1.4)
|
73
|
-
rubocop-minitest (0.
|
73
|
+
rubocop-minitest (0.38.0)
|
74
74
|
lint_roller (~> 1.1)
|
75
|
-
rubocop (>= 1.
|
75
|
+
rubocop (>= 1.75.0, < 2.0)
|
76
76
|
rubocop-ast (>= 1.38.0, < 2.0)
|
77
|
-
rubocop-performance (1.
|
77
|
+
rubocop-performance (1.25.0)
|
78
78
|
lint_roller (~> 1.1)
|
79
|
-
rubocop (>= 1.
|
79
|
+
rubocop (>= 1.75.0, < 2.0)
|
80
80
|
rubocop-ast (>= 1.38.0, < 2.0)
|
81
81
|
rubocop-rake (0.7.1)
|
82
82
|
lint_roller (~> 1.1)
|
83
83
|
rubocop (>= 1.72.1)
|
84
|
-
rubocop-rspec (3.
|
84
|
+
rubocop-rspec (3.6.0)
|
85
85
|
lint_roller (~> 1.1)
|
86
86
|
rubocop (~> 1.72, >= 1.72.1)
|
87
87
|
ruby-progressbar (1.13.0)
|
@@ -96,6 +96,7 @@ GEM
|
|
96
96
|
simplecov_json_formatter (0.1.4)
|
97
97
|
slop (4.10.1)
|
98
98
|
tago (0.1.0)
|
99
|
+
timeout (0.4.3)
|
99
100
|
unicode-display_width (3.1.4)
|
100
101
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
101
102
|
unicode-emoji (4.0.4)
|
@@ -126,6 +127,7 @@ DEPENDENCIES
|
|
126
127
|
rubocop-rspec (> 0)
|
127
128
|
simplecov (~> 0.22)
|
128
129
|
simplecov-cobertura (~> 2.1)
|
130
|
+
timeout (> 0)
|
129
131
|
xcop (> 0)
|
130
132
|
yard (~> 0.9)
|
131
133
|
|
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# Ruby + PostgreSQL + Liquibase + Rake
|
2
2
|
|
3
3
|
[](https://www.elegantobjects.org)
|
4
|
-
[](https://www.rultor.com/p/yegor256/pgtk)
|
5
5
|
[](https://www.jetbrains.com/ruby/)
|
6
6
|
|
7
7
|
[](https://github.com/yegor256/pgtk/actions/workflows/rake.yml)
|
8
|
-
[](
|
8
|
+
[](https://www.0pdd.com/p?name=yegor256/pgtk)
|
9
|
+
[](https://badge.fury.io/rb/pgtk)
|
10
10
|
[](https://codeclimate.com/github/yegor256/pgtk/maintainability)
|
11
11
|
[](https://github.com/yegor256/pgtk/blob/master/LICENSE.txt)
|
12
12
|
[](https://codecov.io/github/yegor256/pgtk?branch=master)
|
@@ -99,7 +99,7 @@ bundle exec rake pgsql liquibase
|
|
99
99
|
|
100
100
|
A temporary PostgreSQL server will be started and the entire set of
|
101
101
|
Liquibase SQL changes will be applied. You will be able to connect
|
102
|
-
to it from your application, using the file `target/config.yml`.
|
102
|
+
to it from your application, using the file `target/pgsql-config.yml`.
|
103
103
|
|
104
104
|
From inside your app you may find this class useful:
|
105
105
|
|
@@ -123,7 +123,7 @@ Now you can fetch some data from the DB:
|
|
123
123
|
name = pgsql.exec('SELECT name FROM user WHERE id = $1', [id])[0]['name']
|
124
124
|
```
|
125
125
|
|
126
|
-
You may also use it
|
126
|
+
You may also use it when you need to run a transaction:
|
127
127
|
|
128
128
|
```ruby
|
129
129
|
pgsql.transaction do |t|
|
@@ -132,7 +132,7 @@ pgsql.transaction do |t|
|
|
132
132
|
end
|
133
133
|
```
|
134
134
|
|
135
|
-
To make your PostgreSQL database visible in your unit
|
135
|
+
To make your PostgreSQL database visible in your unit tests, I would
|
136
136
|
recommend you create a method `test_pgsql` in your `test__helper.rb` file
|
137
137
|
(which is `required` in all unit tests) and implement it like this:
|
138
138
|
|
@@ -143,7 +143,6 @@ require 'pgtk/pool'
|
|
143
143
|
module Minitest
|
144
144
|
class Test
|
145
145
|
def test_pgsql
|
146
|
-
config = YAML.load_file()
|
147
146
|
@@test_pgsql ||= Pgtk::Pool.new(
|
148
147
|
Pgtk::Wire::Yaml.new('target/pgsql-config.yml')
|
149
148
|
).start
|
@@ -152,7 +151,7 @@ module Minitest
|
|
152
151
|
end
|
153
152
|
```
|
154
153
|
|
155
|
-
You can also track all SQL queries sent through, with the help of `Pgtk::
|
154
|
+
You can also track all SQL queries sent through the pool, with the help of `Pgtk::Spy`:
|
156
155
|
|
157
156
|
```ruby
|
158
157
|
require 'pgtk/spy'
|
data/REUSE.toml
CHANGED
@@ -4,9 +4,17 @@
|
|
4
4
|
version = 1
|
5
5
|
[[annotations]]
|
6
6
|
path = [
|
7
|
+
".DS_Store",
|
8
|
+
".gitattributes",
|
9
|
+
".gitignore",
|
10
|
+
".pdd",
|
7
11
|
"**.json",
|
8
12
|
"**.md",
|
13
|
+
"**.png",
|
9
14
|
"**.txt",
|
15
|
+
"**/.DS_Store",
|
16
|
+
"**/.gitignore",
|
17
|
+
"**/.pdd",
|
10
18
|
"**/*.csv",
|
11
19
|
"**/*.jpg",
|
12
20
|
"**/*.json",
|
@@ -16,15 +24,8 @@ path = [
|
|
16
24
|
"**/*.svg",
|
17
25
|
"**/*.txt",
|
18
26
|
"**/*.vm",
|
19
|
-
"**/.DS_Store",
|
20
|
-
"**/.gitignore",
|
21
|
-
"**/.pdd",
|
22
27
|
"**/CNAME",
|
23
28
|
"**/Gemfile.lock",
|
24
|
-
".DS_Store",
|
25
|
-
".gitattributes",
|
26
|
-
".gitignore",
|
27
|
-
".pdd",
|
28
29
|
"Gemfile.lock",
|
29
30
|
"README.md",
|
30
31
|
"renovate.json",
|
data/Rakefile
CHANGED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'timeout'
|
7
|
+
require_relative '../pgtk'
|
8
|
+
|
9
|
+
# Impatient is a decorator for Pool that enforces timeouts on all database operations.
|
10
|
+
# It ensures that SQL queries don't run indefinitely, which helps prevent application
|
11
|
+
# hangs and resource exhaustion when database operations are slow or stalled.
|
12
|
+
#
|
13
|
+
# This class implements the same interface as Pool but wraps each database operation
|
14
|
+
# in a timeout block. If a query exceeds the specified timeout, it raises a Timeout::Error
|
15
|
+
# exception, allowing the application to handle slow queries gracefully.
|
16
|
+
#
|
17
|
+
# Basic usage:
|
18
|
+
#
|
19
|
+
# # Create and configure a regular pool
|
20
|
+
# pool = Pgtk::Pool.new(wire).start(4)
|
21
|
+
#
|
22
|
+
# # Wrap the pool in an impatient decorator with a 2-second timeout
|
23
|
+
# impatient = Pgtk::Impatient.new(pool, 2)
|
24
|
+
#
|
25
|
+
# # Execute queries with automatic timeout enforcement
|
26
|
+
# begin
|
27
|
+
# impatient.exec('SELECT * FROM large_table WHERE complex_condition')
|
28
|
+
# rescue Timeout::Error
|
29
|
+
# puts "Query timed out after 2 seconds"
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # Transactions also enforce timeouts on each query
|
33
|
+
# begin
|
34
|
+
# impatient.transaction do |t|
|
35
|
+
# t.exec('UPDATE large_table SET processed = true')
|
36
|
+
# t.exec('DELETE FROM queue WHERE processed = true')
|
37
|
+
# end
|
38
|
+
# rescue Timeout::Error
|
39
|
+
# puts "Transaction timed out"
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# # Combining with Spy for timeout monitoring
|
43
|
+
# spy = Pgtk::Spy.new(impatient) do |sql, duration|
|
44
|
+
# puts "Query completed in #{duration} seconds: #{sql}"
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# # Now queries are both timed and monitored
|
48
|
+
# spy.exec('SELECT * FROM users')
|
49
|
+
#
|
50
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
51
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
52
|
+
# License:: MIT
|
53
|
+
class Pgtk::Impatient
|
54
|
+
# Constructor.
|
55
|
+
#
|
56
|
+
# @param [Pgtk::Pool] pool The pool to decorate
|
57
|
+
# @param [Integer] timeout Timeout in seconds for each SQL query
|
58
|
+
def initialize(pool, timeout = 1)
|
59
|
+
@pool = pool
|
60
|
+
@timeout = timeout
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get the version of PostgreSQL server.
|
64
|
+
#
|
65
|
+
# @return [String] Version of PostgreSQL server
|
66
|
+
def version
|
67
|
+
@pool.version
|
68
|
+
end
|
69
|
+
|
70
|
+
# Execute a SQL query with a timeout.
|
71
|
+
#
|
72
|
+
# @param [String] sql The SQL query with params inside (possibly)
|
73
|
+
# @param [Array] args List of arguments
|
74
|
+
# @return [Array] Result rows
|
75
|
+
# @raise [Timeout::Error] If the query takes too long
|
76
|
+
def exec(sql, *args)
|
77
|
+
Timeout.timeout(@timeout) do
|
78
|
+
@pool.exec(sql, *args)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Run a transaction with a timeout for each query.
|
83
|
+
#
|
84
|
+
# @yield [Pgtk::Impatient] Yields an impatient transaction
|
85
|
+
# @return [Object] Result of the block
|
86
|
+
def transaction
|
87
|
+
@pool.transaction do |t|
|
88
|
+
yield Pgtk::Impatient.new(t, @timeout)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/pgtk/liquibase_task.rb
CHANGED
@@ -16,8 +16,38 @@ require_relative '../pgtk'
|
|
16
16
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
17
17
|
# License:: MIT
|
18
18
|
class Pgtk::LiquibaseTask < Rake::TaskLib
|
19
|
-
|
19
|
+
# Task name
|
20
|
+
# @return [Symbol]
|
21
|
+
attr_accessor :name
|
20
22
|
|
23
|
+
# Path to Liquibase master XML file
|
24
|
+
# @return [String]
|
25
|
+
attr_accessor :master
|
26
|
+
|
27
|
+
# Path to YAML file with PostgreSQL connection details
|
28
|
+
# @return [String, Array<String>]
|
29
|
+
attr_accessor :yaml
|
30
|
+
|
31
|
+
# Whether to suppress output
|
32
|
+
# @return [Boolean]
|
33
|
+
attr_accessor :quiet
|
34
|
+
|
35
|
+
# Liquibase version to use
|
36
|
+
# @return [String]
|
37
|
+
attr_accessor :liquibase_version
|
38
|
+
|
39
|
+
# PostgreSQL JDBC driver version to use
|
40
|
+
# @return [String]
|
41
|
+
attr_accessor :postgresql_version
|
42
|
+
|
43
|
+
# Liquibase contexts to apply
|
44
|
+
# @return [String]
|
45
|
+
attr_accessor :contexts
|
46
|
+
|
47
|
+
# Initialize a new Liquibase task.
|
48
|
+
#
|
49
|
+
# @param [Array] args Task arguments
|
50
|
+
# @yield [Pgtk::LiquibaseTask, Object] Yields self and task arguments
|
21
51
|
def initialize(*args, &task_block)
|
22
52
|
super()
|
23
53
|
@name = args.shift || :liquibase
|
data/lib/pgtk/pgsql_task.rb
CHANGED
@@ -19,8 +19,50 @@ require_relative '../pgtk'
|
|
19
19
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
20
20
|
# License:: MIT
|
21
21
|
class Pgtk::PgsqlTask < Rake::TaskLib
|
22
|
-
|
22
|
+
# Task name
|
23
|
+
# @return [Symbol]
|
24
|
+
attr_accessor :name
|
23
25
|
|
26
|
+
# Directory where PostgreSQL server files will be stored
|
27
|
+
# @return [String]
|
28
|
+
attr_accessor :dir
|
29
|
+
|
30
|
+
# Whether to delete the PostgreSQL data directory on each run
|
31
|
+
# @return [Boolean]
|
32
|
+
attr_accessor :fresh_start
|
33
|
+
|
34
|
+
# PostgreSQL username
|
35
|
+
# @return [String]
|
36
|
+
attr_accessor :user
|
37
|
+
|
38
|
+
# PostgreSQL password
|
39
|
+
# @return [String]
|
40
|
+
attr_accessor :password
|
41
|
+
|
42
|
+
# PostgreSQL database name
|
43
|
+
# @return [String]
|
44
|
+
attr_accessor :dbname
|
45
|
+
|
46
|
+
# Path to YAML file where configuration will be written
|
47
|
+
# @return [String]
|
48
|
+
attr_accessor :yaml
|
49
|
+
|
50
|
+
# Whether to suppress output
|
51
|
+
# @return [Boolean]
|
52
|
+
attr_accessor :quiet
|
53
|
+
|
54
|
+
# TCP port for PostgreSQL server (random if nil)
|
55
|
+
# @return [Integer, nil]
|
56
|
+
attr_accessor :port
|
57
|
+
|
58
|
+
# Configuration options for PostgreSQL server
|
59
|
+
# @return [Hash]
|
60
|
+
attr_accessor :config
|
61
|
+
|
62
|
+
# Initialize a new PostgreSQL server task.
|
63
|
+
#
|
64
|
+
# @param [Array] args Task arguments
|
65
|
+
# @yield [Pgtk::PgsqlTask, Object] Yields self and task arguments
|
24
66
|
def initialize(*args, &task_block)
|
25
67
|
super()
|
26
68
|
@name = args.shift || :pgsql
|
data/lib/pgtk/pool.rb
CHANGED
@@ -8,7 +8,40 @@ require 'loog'
|
|
8
8
|
require_relative '../pgtk'
|
9
9
|
require_relative 'wire'
|
10
10
|
|
11
|
-
# Pool.
|
11
|
+
# Pool provides a connection pool for PostgreSQL database connections.
|
12
|
+
# It manages a fixed number of connections to optimize performance and
|
13
|
+
# resource usage while providing a simple interface for database operations.
|
14
|
+
#
|
15
|
+
# The Pool class handles connection lifecycle, reconnects on errors,
|
16
|
+
# and provides transaction support. It's the core class for interacting
|
17
|
+
# with a PostgreSQL database in this library.
|
18
|
+
#
|
19
|
+
# Basic usage:
|
20
|
+
#
|
21
|
+
# # Create a wire that knows how to connect to PostgreSQL
|
22
|
+
# wire = Pgtk::Wire::Direct.new(
|
23
|
+
# host: 'localhost',
|
24
|
+
# port: 5432,
|
25
|
+
# dbname: 'mydatabase',
|
26
|
+
# user: 'postgres',
|
27
|
+
# password: 'secret'
|
28
|
+
# )
|
29
|
+
#
|
30
|
+
# # Create and start a connection pool with 4 connections
|
31
|
+
# pool = Pgtk::Pool.new(wire).start(4)
|
32
|
+
#
|
33
|
+
# # Execute a simple query
|
34
|
+
# pool.exec('SELECT * FROM users')
|
35
|
+
#
|
36
|
+
# # Execute a parameterized query
|
37
|
+
# pool.exec('SELECT * FROM users WHERE email = $1', ['user@example.com'])
|
38
|
+
#
|
39
|
+
# # Use transactions for multiple operations
|
40
|
+
# pool.transaction do |t|
|
41
|
+
# t.exec('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [100, 42])
|
42
|
+
# t.exec('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [100, 43])
|
43
|
+
# end
|
44
|
+
#
|
12
45
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
13
46
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
14
47
|
# License:: MIT
|
data/lib/pgtk/spy.rb
CHANGED
@@ -8,20 +8,65 @@ require 'loog'
|
|
8
8
|
require_relative '../pgtk'
|
9
9
|
require_relative 'wire'
|
10
10
|
|
11
|
-
#
|
11
|
+
# Spy is a decorator for Pool that intercepts and tracks SQL queries.
|
12
|
+
# It provides observability into database operations by invoking a callback
|
13
|
+
# with the SQL query and its execution time for each database operation.
|
14
|
+
#
|
15
|
+
# This class implements the same interface as Pool, but adds instrumentation
|
16
|
+
# functionality while delegating actual database operations to the decorated pool.
|
17
|
+
# Use Spy for debugging, performance monitoring, or audit logging.
|
18
|
+
#
|
19
|
+
# Basic usage:
|
20
|
+
#
|
21
|
+
# # Create and configure a regular pool
|
22
|
+
# pool = Pgtk::Pool.new(wire).start(4)
|
23
|
+
#
|
24
|
+
# # Wrap the pool in a spy that tracks all executed queries
|
25
|
+
# queries = []
|
26
|
+
# spy = Pgtk::Spy.new(pool) do |sql, duration|
|
27
|
+
# puts "Query: #{sql}"
|
28
|
+
# puts "Duration: #{duration} seconds"
|
29
|
+
# queries << sql
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # Use the spy just like a regular pool, with automatic tracking
|
33
|
+
# spy.exec('SELECT * FROM users')
|
34
|
+
#
|
35
|
+
# # Transactions also track each query inside the transaction
|
36
|
+
# spy.transaction do |t|
|
37
|
+
# t.exec('UPDATE users SET active = true WHERE id = $1', [42])
|
38
|
+
# t.exec('INSERT INTO audit_log (user_id, action) VALUES ($1, $2)', [42, 'activated'])
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# # Examine collected queries for analysis
|
42
|
+
# puts "Total queries: #{queries.size}"
|
43
|
+
# puts "First query: #{queries.first}"
|
44
|
+
#
|
12
45
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
13
46
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
14
47
|
# License:: MIT
|
15
48
|
class Pgtk::Spy
|
49
|
+
# Constructor.
|
50
|
+
#
|
51
|
+
# @param [Pgtk::Pool] pool The pool to spy on
|
52
|
+
# @yield [String, Float] Yields the SQL query and execution time
|
16
53
|
def initialize(pool, &block)
|
17
54
|
@pool = pool
|
18
55
|
@block = block
|
19
56
|
end
|
20
57
|
|
58
|
+
# Get the version of PostgreSQL server.
|
59
|
+
#
|
60
|
+
# @return [String] Version of PostgreSQL server
|
21
61
|
def version
|
22
62
|
@pool.version
|
23
63
|
end
|
24
64
|
|
65
|
+
# Execute a SQL query and track its execution.
|
66
|
+
#
|
67
|
+
# @param [String] sql The SQL query with params inside (possibly)
|
68
|
+
# @param [Array] args List of arguments
|
69
|
+
# @return [Array] Result rows
|
25
70
|
def exec(sql, *args)
|
26
71
|
start = Time.now
|
27
72
|
ret = @pool.exec(sql, *args)
|
@@ -29,6 +74,10 @@ class Pgtk::Spy
|
|
29
74
|
ret
|
30
75
|
end
|
31
76
|
|
77
|
+
# Run a transaction with spying on each SQL query.
|
78
|
+
#
|
79
|
+
# @yield [Pgtk::Spy] Yields a spy transaction
|
80
|
+
# @return [Object] Result of the block
|
32
81
|
def transaction
|
33
82
|
@pool.transaction do |t|
|
34
83
|
yield Pgtk::Spy.new(t, &@block)
|
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
|
@@ -46,8 +52,10 @@ end
|
|
46
52
|
# License:: MIT
|
47
53
|
class Pgtk::Wire::Env
|
48
54
|
# Constructor.
|
55
|
+
#
|
56
|
+
# @param [String] var The name of the environment variable with the connection URL
|
49
57
|
def initialize(var = 'DATABASE_URL')
|
50
|
-
raise "The name of the
|
58
|
+
raise "The name of the environment variable can't be nil" if var.nil?
|
51
59
|
@var = var
|
52
60
|
end
|
53
61
|
|
@@ -72,6 +80,9 @@ end
|
|
72
80
|
# License:: MIT
|
73
81
|
class Pgtk::Wire::Yaml
|
74
82
|
# Constructor.
|
83
|
+
#
|
84
|
+
# @param [String] file Path to the YAML configuration file
|
85
|
+
# @param [String] node The root node name in the YAML file containing PostgreSQL configuration
|
75
86
|
def initialize(file, node = 'pgsql')
|
76
87
|
raise "The name of the file can't be nil" if file.nil?
|
77
88
|
@file = file
|
@@ -83,12 +94,13 @@ class Pgtk::Wire::Yaml
|
|
83
94
|
def connection
|
84
95
|
raise "The file #{@file.inspect} not found" unless File.exist?(@file)
|
85
96
|
cfg = YAML.load_file(@file)
|
97
|
+
raise "The node '#{@node}' not found in YAML file #{@file.inspect}" unless cfg[@node]
|
86
98
|
Pgtk::Wire::Direct.new(
|
87
|
-
host: cfg[
|
88
|
-
port: cfg[
|
89
|
-
dbname: cfg[
|
90
|
-
user: cfg[
|
91
|
-
password: cfg[
|
99
|
+
host: cfg[@node]['host'],
|
100
|
+
port: cfg[@node]['port'],
|
101
|
+
dbname: cfg[@node]['dbname'],
|
102
|
+
user: cfg[@node]['user'],
|
103
|
+
password: cfg[@node]['password']
|
92
104
|
).connection
|
93
105
|
end
|
94
106
|
end
|
data/test/test__helper.rb
CHANGED
@@ -6,13 +6,62 @@
|
|
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 70
|
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 'loog'
|
34
|
+
require 'rake'
|
35
|
+
require 'rake/tasklib'
|
18
36
|
require_relative '../lib/pgtk'
|
37
|
+
require_relative '../lib/pgtk/liquibase_task'
|
38
|
+
require_relative '../lib/pgtk/pgsql_task'
|
39
|
+
|
40
|
+
class Pgtk::Test < Minitest::Test
|
41
|
+
def bootstrap(log: Loog::NULL)
|
42
|
+
Dir.mktmpdir 'test' do |dir|
|
43
|
+
id = rand(100..999)
|
44
|
+
Pgtk::PgsqlTask.new("pgsql#{id}") do |t|
|
45
|
+
t.dir = File.join(dir, 'pgsql')
|
46
|
+
t.user = 'hello'
|
47
|
+
t.password = 'A B C привет ! & | !'
|
48
|
+
t.dbname = 'test'
|
49
|
+
t.yaml = File.join(dir, 'cfg.yml')
|
50
|
+
t.quiet = true
|
51
|
+
end
|
52
|
+
Rake::Task["pgsql#{id}"].invoke
|
53
|
+
Pgtk::LiquibaseTask.new("liquibase#{id}") do |t|
|
54
|
+
t.master = File.join(__dir__, '../test-resources/master.xml')
|
55
|
+
t.yaml = File.join(dir, 'cfg.yml')
|
56
|
+
t.quiet = true
|
57
|
+
end
|
58
|
+
Rake::Task["liquibase#{id}"].invoke
|
59
|
+
pool = Pgtk::Pool.new(
|
60
|
+
Pgtk::Wire::Yaml.new(File.join(dir, 'cfg.yml')),
|
61
|
+
log: log
|
62
|
+
)
|
63
|
+
pool.start(1)
|
64
|
+
yield pool
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,30 @@
|
|
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_doesnt_interrupt
|
22
|
+
bootstrap do |pool|
|
23
|
+
id = Pgtk::Impatient.new(pool).exec(
|
24
|
+
'INSERT INTO book (title) VALUES ($1) RETURNING id',
|
25
|
+
['1984']
|
26
|
+
).first['id'].to_i
|
27
|
+
assert_predicate(id, :positive?)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
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,7 +14,7 @@ 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
19
|
Dir.mktmpdir 'test' do |dir|
|
20
20
|
Pgtk::PgsqlTask.new(:pgsql2) do |t|
|
data/test/test_pgsql_task.rb
CHANGED
@@ -3,17 +3,17 @@
|
|
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
18
|
Dir.mktmpdir 'test' do |dir|
|
19
19
|
Pgtk::PgsqlTask.new(:p2) do |t|
|
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,7 +19,7 @@ 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
|
bootstrap do |pool|
|
25
25
|
ver = pool.version
|
@@ -181,33 +181,4 @@ class TestPool < Minitest::Test
|
|
181
181
|
end
|
182
182
|
end
|
183
183
|
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
184
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pgtk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: backtrace
|
@@ -86,8 +86,8 @@ email: yegor256@gmail.com
|
|
86
86
|
executables: []
|
87
87
|
extensions: []
|
88
88
|
extra_rdoc_files:
|
89
|
-
- README.md
|
90
89
|
- LICENSE.txt
|
90
|
+
- README.md
|
91
91
|
files:
|
92
92
|
- ".0pdd.yml"
|
93
93
|
- ".gitattributes"
|
@@ -99,13 +99,13 @@ files:
|
|
99
99
|
- ".github/workflows/pdd.yml"
|
100
100
|
- ".github/workflows/rake.yml"
|
101
101
|
- ".github/workflows/reuse.yml"
|
102
|
+
- ".github/workflows/typos.yml"
|
102
103
|
- ".github/workflows/xcop.yml"
|
103
104
|
- ".github/workflows/yamllint.yml"
|
104
105
|
- ".gitignore"
|
105
106
|
- ".pdd"
|
106
107
|
- ".rubocop.yml"
|
107
108
|
- ".rultor.yml"
|
108
|
-
- ".simplecov"
|
109
109
|
- ".yamllint.yml"
|
110
110
|
- Gemfile
|
111
111
|
- Gemfile.lock
|
@@ -116,6 +116,7 @@ files:
|
|
116
116
|
- Rakefile
|
117
117
|
- cucumber.yml
|
118
118
|
- lib/pgtk.rb
|
119
|
+
- lib/pgtk/impatient.rb
|
119
120
|
- lib/pgtk/liquibase_task.rb
|
120
121
|
- lib/pgtk/pgsql_task.rb
|
121
122
|
- lib/pgtk/pool.rb
|
@@ -128,6 +129,7 @@ files:
|
|
128
129
|
- test-resources/2019/01-test.xml
|
129
130
|
- test-resources/master.xml
|
130
131
|
- test/test__helper.rb
|
132
|
+
- test/test_impatient.rb
|
131
133
|
- test/test_liquibase_task.rb
|
132
134
|
- test/test_pgsql_task.rb
|
133
135
|
- test/test_pool.rb
|
@@ -151,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
153
|
- !ruby/object:Gem::Version
|
152
154
|
version: '0'
|
153
155
|
requirements: []
|
154
|
-
rubygems_version: 3.6.
|
156
|
+
rubygems_version: 3.6.7
|
155
157
|
specification_version: 4
|
156
158
|
summary: PostgreSQL ToolKit for Ruby apps
|
157
159
|
test_files: []
|
data/.simplecov
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
4
|
-
# SPDX-License-Identifier: MIT
|
5
|
-
|
6
|
-
if Gem.win_platform?
|
7
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
8
|
-
SimpleCov::Formatter::HTMLFormatter
|
9
|
-
]
|
10
|
-
SimpleCov.start do
|
11
|
-
add_filter '/test/'
|
12
|
-
add_filter '/features/'
|
13
|
-
end
|
14
|
-
else
|
15
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
16
|
-
[SimpleCov::Formatter::HTMLFormatter]
|
17
|
-
)
|
18
|
-
SimpleCov.start do
|
19
|
-
add_filter '/test/'
|
20
|
-
add_filter '/features/'
|
21
|
-
minimum_coverage 60
|
22
|
-
end
|
23
|
-
end
|