pgi 1.1.0 → 1.2.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/CHANGELOG.md +29 -0
- data/README.md +15 -4
- data/VERSION +1 -1
- data/lib/pgi/connection.rb +5 -2
- data/lib/pgi/dataset/query.rb +28 -17
- data/lib/pgi/dataset.rb +2 -3
- data/lib/pgi/db.rb +11 -6
- data/pgi.gemspec +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e57eefff746743d0e70c5c256fa2fed74f2335c07b9657c67e0f517a5a37b927
|
|
4
|
+
data.tar.gz: '0073639115a44fe351077c3577b7e355da0165e580683c17620fc50b824303fe'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b981f438da0b1f82946471622922d1eee510cb135b67f3db5fa108e24fdb2b7936648c55385841f13d67bfc1a479c48c78e6298701a4d962abc2f69e2fbb614
|
|
7
|
+
data.tar.gz: 38a762ec93398e240563e2dfc46d3b7ae8d8e3d1c08938c23c25e90ad2286789e4b1b4b553c7f0185d1146ea6c8acb7cf80ab8370678d36e43a9f753f4964c92
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **Sort on a projection** — `#order` and `#page`/`#keyset` accept a declared
|
|
6
|
+
`projections:` name as the sort column and order (and seek) on its
|
|
7
|
+
expression, so a list can page on a computed value.
|
|
8
|
+
|
|
9
|
+
- **`DB.configure` requires `pg_conn_uri`** — leaving it unset used to pass
|
|
10
|
+
`configure` and raise at the first pool checkout, naming `Connection.new`'s
|
|
11
|
+
keywords instead of the knob you set; it now raises `ArgumentError` there and
|
|
12
|
+
then. Pass `pg_conn_uri: ""` to ask libpq for its defaults on purpose.
|
|
13
|
+
|
|
14
|
+
- **`DB.configure` no longer shares state between databases** (bug fix) — the
|
|
15
|
+
options lived on the class and the pool read them at checkout time, so a second
|
|
16
|
+
`configure` silently took over the first DB's pool. Each call now keeps its
|
|
17
|
+
own options.
|
|
18
|
+
|
|
19
|
+
- **`Connection.new` requires a connection** — building one with neither
|
|
20
|
+
`conn:` nor `conn_uri:` now raises `ArgumentError` instead of connecting to
|
|
21
|
+
whatever libpq's environment defaults point at. Pass `conn_uri: ""` to ask
|
|
22
|
+
for those defaults on purpose.
|
|
23
|
+
|
|
24
|
+
- **A colliding projection name raises** (behaviour change) — a `projections:`
|
|
25
|
+
entry named after a base column landed as a second result field and silently
|
|
26
|
+
overwrote the base value; the read now raises, as an unaliased joined column
|
|
27
|
+
from `#select` already did.
|
|
28
|
+
|
|
29
|
+
- **Dependency updates**
|
|
30
|
+
- connection_pool: the dependency widens from `~> 2.4` to `>= 2.4, < 4`.
|
|
31
|
+
|
|
3
32
|
## 1.1.0 (2026-08-28)
|
|
4
33
|
|
|
5
34
|
The query release: joins, search, projected columns — and server notices that
|
data/README.md
CHANGED
|
@@ -58,8 +58,10 @@ Repository.page(nil, 20, :name, :asc) # first page of 20, sorted by name
|
|
|
58
58
|
|
|
59
59
|
The pieces:
|
|
60
60
|
|
|
61
|
-
*
|
|
62
|
-
|
|
61
|
+
* `Dataset#select(column1, ...)` — start a query limited to the specified
|
|
62
|
+
columns of the base table
|
|
63
|
+
* `Query#select(column1, ...)` — append a **joined** table's column onto the
|
|
64
|
+
base table's columns, see [Projecting joined columns](#projecting-joined-columns)
|
|
63
65
|
* `#where(...)` — can only be called once per query, so combine all conditions
|
|
64
66
|
in a single call. Two forms:
|
|
65
67
|
* `#where("name = ? AND age > ?", ['joe', 21])` — a string clause with placeholders (`?` or `$1`)
|
|
@@ -252,8 +254,17 @@ Notes:
|
|
|
252
254
|
filtering **on** a projection promotes evaluation to the whole scope; that
|
|
253
255
|
EXPLAIN is the caller's to own. If a projection ever measures hot, the
|
|
254
256
|
escalation is a trigger-maintained column, not a cleverer query.
|
|
255
|
-
-
|
|
256
|
-
|
|
257
|
+
- **Sort on a projection** — `#order` and `#page`/`#keyset` take a declared
|
|
258
|
+
projection's name as the sort column and order on its expression; the
|
|
259
|
+
read need not project it. The cursor row resolves the same expression.
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
Repository.page(nil, 20, :mates_count, :desc)
|
|
263
|
+
```
|
|
264
|
+
- **A colliding name raises** — a projection named after a base column would
|
|
265
|
+
overwrite it in the row hash, so the read raises when the rows come back
|
|
266
|
+
(same guard as an unaliased joined column). Pick names that cannot collide
|
|
267
|
+
(`mates_count`, not `count`).
|
|
257
268
|
|
|
258
269
|
### Search
|
|
259
270
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.2.0
|
data/lib/pgi/connection.rb
CHANGED
|
@@ -10,9 +10,12 @@ module PGI
|
|
|
10
10
|
|
|
11
11
|
# Create instance
|
|
12
12
|
#
|
|
13
|
-
# @param pool [ConnectionPool]
|
|
14
13
|
# @param logger [Logger]
|
|
14
|
+
# @param conn_uri [String] connection URI; "" asks libpq for its own defaults
|
|
15
|
+
# @param conn [PG::Connection] an already built connection, used as is
|
|
15
16
|
def initialize(logger:, conn_uri: nil, conn: nil)
|
|
17
|
+
raise ArgumentError, "conn or conn_uri required" unless conn || conn_uri
|
|
18
|
+
|
|
16
19
|
@logger = logger
|
|
17
20
|
|
|
18
21
|
@conn = conn || PG::Connection.new(conn_uri).tap do |new_conn|
|
|
@@ -24,7 +27,7 @@ module PGI
|
|
|
24
27
|
|
|
25
28
|
new_conn.type_map_for_results = PG::BasicTypeMapForResults.new(new_conn, registry: regi)
|
|
26
29
|
new_conn.type_map_for_queries = PG::BasicTypeMapForQueries.new(new_conn, registry: regi)
|
|
27
|
-
end
|
|
30
|
+
end
|
|
28
31
|
|
|
29
32
|
# Server notices (a DROP CASCADE's chatter, a RAISE NOTICE) route to
|
|
30
33
|
# the configured logger instead of libpq's stderr default: a library
|
data/lib/pgi/dataset/query.rb
CHANGED
|
@@ -37,7 +37,8 @@ module PGI
|
|
|
37
37
|
# Opt into declared projections for THIS read: the named computed
|
|
38
38
|
# columns join the select list as (expr) AS name. Names must be
|
|
39
39
|
# declared on the dataset (the trust boundary stays at extension
|
|
40
|
-
# time); unknown names raise.
|
|
40
|
+
# time); unknown names raise. A name that collides with a base column
|
|
41
|
+
# raises when the rows come back, rather than clobbering it.
|
|
41
42
|
#
|
|
42
43
|
# @param names [Array<Symbol>] declared projection names
|
|
43
44
|
# @return [Query] the Query instance (for method chaining)
|
|
@@ -201,7 +202,8 @@ module PGI
|
|
|
201
202
|
# Adds a ORDER BY clause to the query - suports multiple calls to the method
|
|
202
203
|
#
|
|
203
204
|
# @param column [Symbol, Hash] the column - a single-pair Hash qualifies
|
|
204
|
-
# it with a joined table: { users: :name }
|
|
205
|
+
# it with a joined table: { users: :name }; a declared projection's
|
|
206
|
+
# name sorts on its expression
|
|
205
207
|
# @param direction [Symbol] the direction the sort should take - can be either `:desc` or `:asc`
|
|
206
208
|
# @param collate [String, nil] collation for text ordering, e.g. "da-x-icu"
|
|
207
209
|
# (policy - which locale maps to which collation - belongs to the caller)
|
|
@@ -234,7 +236,7 @@ module PGI
|
|
|
234
236
|
# Do not combine with a conflicting #order call — pages are only correct when
|
|
235
237
|
# the leading sort columns match the cursor predicate.
|
|
236
238
|
#
|
|
237
|
-
# @param sort_by [Symbol] the sort column
|
|
239
|
+
# @param sort_by [Symbol] the sort column, or a declared projection's name
|
|
238
240
|
# @param cursor_id [*, nil] id of the last row from the previous page, or nil for the first page
|
|
239
241
|
# @param sort_dir [Symbol] :asc or :desc
|
|
240
242
|
# @param collate [String, nil] collation for the sort column. Must be the
|
|
@@ -367,31 +369,32 @@ module PGI
|
|
|
367
369
|
|
|
368
370
|
private
|
|
369
371
|
|
|
370
|
-
# Run the query and guard against a
|
|
371
|
-
#
|
|
372
|
+
# Run the query and guard against a duplicate result field name before
|
|
373
|
+
# the rows are handed back (see #assert_result_unambiguous!).
|
|
372
374
|
#
|
|
373
375
|
# @return [PG::Result]
|
|
374
376
|
def result
|
|
375
377
|
res = @database.exec_stmt(Utils.stmt_name(@table, sql), sql, params)
|
|
376
|
-
|
|
378
|
+
assert_result_unambiguous!(res)
|
|
377
379
|
res
|
|
378
380
|
end
|
|
379
381
|
|
|
380
|
-
# Raise
|
|
381
|
-
#
|
|
382
|
-
#
|
|
383
|
-
#
|
|
382
|
+
# Raise when two columns land on the same result field name - the row
|
|
383
|
+
# hash would silently keep only the last, losing data.
|
|
384
|
+
#
|
|
385
|
+
# A plain base.* read cannot clash, so only an appended column can:
|
|
386
|
+
# #select's joined column or an opted-in projections: entry.
|
|
384
387
|
#
|
|
385
388
|
# @param result [PG::Result]
|
|
386
389
|
# @raise [RuntimeError] listing the duplicated field name(s)
|
|
387
|
-
def
|
|
388
|
-
return if @select.empty?
|
|
390
|
+
def assert_result_unambiguous!(result)
|
|
391
|
+
return if @select.empty? && @projected.empty?
|
|
389
392
|
|
|
390
393
|
dups = result.fields.tally.select { |_, n| n > 1 }.keys
|
|
391
394
|
return if dups.empty?
|
|
392
395
|
|
|
393
|
-
raise "Ambiguous
|
|
394
|
-
"select(table => { column: :alias })
|
|
396
|
+
raise "Ambiguous result column(s): #{dups.join(", ")} - alias a joined column " \
|
|
397
|
+
"with select(table => { column: :alias }), or rename the colliding projections: entry"
|
|
395
398
|
end
|
|
396
399
|
|
|
397
400
|
# Render a #select column: a bare column (base table), a { table => column }
|
|
@@ -438,16 +441,24 @@ module PGI
|
|
|
438
441
|
term.to_s.gsub(/[\\%_]/) { |c| "\\#{c}" }
|
|
439
442
|
end
|
|
440
443
|
|
|
444
|
+
# A declared projection's trusted expression, parenthesised; nil for any other name.
|
|
445
|
+
def projected_term(name)
|
|
446
|
+
return if name.is_a?(Hash)
|
|
447
|
+
|
|
448
|
+
expr = @projections[name.to_sym] || @projections[name.to_s]
|
|
449
|
+
expr && "(#{expr})"
|
|
450
|
+
end
|
|
451
|
+
|
|
441
452
|
def assert_known_table!(table)
|
|
442
453
|
return if table.to_sym == @table.to_sym || @join_tables.include?(table.to_sym)
|
|
443
454
|
|
|
444
455
|
raise "Unknown table #{table.inspect} - qualify only the base table or joined tables"
|
|
445
456
|
end
|
|
446
457
|
|
|
447
|
-
# A
|
|
448
|
-
#
|
|
458
|
+
# A sort term with an optional COLLATE: a declared projection's
|
|
459
|
+
# expression, else a column. Collation names are validated and quoted.
|
|
449
460
|
def collated_column(column, collate)
|
|
450
|
-
col = qualified_column(column)
|
|
461
|
+
col = projected_term(column) || qualified_column(column)
|
|
451
462
|
return col unless collate
|
|
452
463
|
|
|
453
464
|
raise "Invalid collation: #{collate.inspect}" unless collate.to_s.match?(COLLATION_NAME)
|
data/lib/pgi/dataset.rb
CHANGED
|
@@ -194,9 +194,8 @@ module PGI
|
|
|
194
194
|
# @param attributes [Hash] column => value
|
|
195
195
|
# @return [Array(Array, Array, Array)] sanitized columns, placeholders, values
|
|
196
196
|
def sql_params(attributes)
|
|
197
|
-
# Projections are READ-ONLY facts computed by the dataset -
|
|
198
|
-
#
|
|
199
|
-
# update) naturally carries them; writes must shed them silently.
|
|
197
|
+
# Projections are READ-ONLY facts computed by the dataset - a model built
|
|
198
|
+
# from a projected read carries them, so writes must shed them silently.
|
|
200
199
|
projections = @options.fetch(:projections, {})
|
|
201
200
|
attrs = attributes.reject { |k, _| projections.key?(k.to_sym) }.sort.to_h
|
|
202
201
|
[Utils.sanitize_columns(attrs.keys), (1..attrs.size).map { |i| "$#{i}" }, attrs.values]
|
data/lib/pgi/db.rb
CHANGED
|
@@ -20,19 +20,24 @@ module PGI
|
|
|
20
20
|
@retry_wait = retry_wait
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Build a DB from a block of options. The options are a local, so the pool
|
|
24
|
+
# block closes over this call's values - a second DB never takes over this one.
|
|
23
25
|
def self.configure
|
|
24
|
-
|
|
26
|
+
options = Struct.new(
|
|
25
27
|
:pool_size, :pool_timeout, :pg_conn_uri, :logger, :max_retries, :retry_wait
|
|
26
28
|
).new
|
|
27
29
|
|
|
28
|
-
yield
|
|
30
|
+
yield options
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
# Fail where the knob was set, not at the first checkout; "" asks libpq for its defaults.
|
|
33
|
+
raise ArgumentError, "pg_conn_uri required" if options.pg_conn_uri.nil?
|
|
34
|
+
|
|
35
|
+
pool = ConnectionPool.new(size: options.pool_size, timeout: options.pool_timeout) do
|
|
36
|
+
Connection.new(conn_uri: options.pg_conn_uri, logger: options.logger)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
retry_options = { max_retries:
|
|
35
|
-
new(pool,
|
|
39
|
+
retry_options = { max_retries: options.max_retries, retry_wait: options.retry_wait }.compact
|
|
40
|
+
new(pool, options.logger, **retry_options)
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
# wrapper around ConnectionPool#with with auto-healing capabilities
|
data/pgi.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
|
|
|
18
18
|
gem.require_paths = ["lib"]
|
|
19
19
|
gem.files = Dir["lib/**/*", "CHANGELOG.md", "LICENSE", "README.md", "VERSION", "pgi.gemspec"]
|
|
20
20
|
|
|
21
|
-
gem.add_dependency "connection_pool", "
|
|
21
|
+
gem.add_dependency "connection_pool", ">= 2.4", "< 4"
|
|
22
22
|
gem.add_dependency "pg", "~> 1.5"
|
|
23
23
|
|
|
24
24
|
gem.metadata["rubygems_mfa_required"] = "true"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pgi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Coherify
|
|
@@ -13,16 +13,22 @@ dependencies:
|
|
|
13
13
|
name: connection_pool
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '2.4'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
19
22
|
type: :runtime
|
|
20
23
|
prerelease: false
|
|
21
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
25
|
requirements:
|
|
23
|
-
- - "
|
|
26
|
+
- - ">="
|
|
24
27
|
- !ruby/object:Gem::Version
|
|
25
28
|
version: '2.4'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
26
32
|
- !ruby/object:Gem::Dependency
|
|
27
33
|
name: pg
|
|
28
34
|
requirement: !ruby/object:Gem::Requirement
|