pg_sql_caller 1.0.0 → 1.1.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 +12 -2
- data/README.md +16 -2
- data/lib/pg_sql_caller/bulk_update.rb +58 -12
- data/lib/pg_sql_caller/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97ef871de183917ba98a3c35bdeb4a184c5aa39ab7b1e6cdc89b11ccb60e8874
|
|
4
|
+
data.tar.gz: d18b3aaada2a2a432e892f067233d6d22a45f2bd39045b1015d6b9fb34c6c154
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75e47d26186bec99d485e7661343f8096386bea04f380385df624e53ba50e0859e89fec3d6de621efe077710a8919dfe28bcd6000c7b45695588132deddd5f36
|
|
7
|
+
data.tar.gz: 5863e6f4406d10316fbfe76f4ee7504ff79e3175786e702315b39ceef2d78ade4249fdd370cbfc0b39210ba339927e04e9ac8d58e8748b86c5619cd808b819ce
|
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [1.1.0] - 2026-06-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `PgSqlCaller::BulkUpdate` now accepts an optional `returning:` keyword — pass one or more
|
|
13
|
+
column names to read them back from each updated row via SQL `RETURNING`. The result is one
|
|
14
|
+
`Symbol`-keyed, type-cast hash per updated row (`[]` when `attrs_list` is empty). Omitting
|
|
15
|
+
`returning:` keeps the existing behavior of returning the affected-row count.
|
|
16
|
+
|
|
17
|
+
## [1.0.0] - 2026-06-08
|
|
9
18
|
|
|
10
19
|
### Added
|
|
11
20
|
|
|
@@ -84,7 +93,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
84
93
|
`transaction_open?`, `explain_analyze`, `typecast_array`, `sanitize_sql_array`, and
|
|
85
94
|
`current_database_name`.
|
|
86
95
|
|
|
87
|
-
[
|
|
96
|
+
[1.1.0]: https://github.com/didww/pg_sql_caller/compare/v1.0.0...v1.1.0
|
|
97
|
+
[1.0.0]: https://github.com/didww/pg_sql_caller/compare/v0.2.3...v1.0.0
|
|
88
98
|
[0.2.3]: https://github.com/didww/pg_sql_caller/compare/v0.2.2...v0.2.3
|
|
89
99
|
[0.2.2]: https://github.com/didww/pg_sql_caller/compare/v0.2.1...v0.2.2
|
|
90
100
|
[0.2.1]: https://github.com/didww/pg_sql_caller/compare/v0.2.0...v0.2.1
|
data/README.md
CHANGED
|
@@ -328,13 +328,27 @@ PgSqlCaller::BulkUpdate.call(Employee, attrs_list, unique_by: :employee_number)
|
|
|
328
328
|
PgSqlCaller::BulkUpdate.call(Employee, attrs_list, unique_by: %i[department_id name])
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
+
### Reading back updated rows
|
|
332
|
+
|
|
333
|
+
Pass `returning` to read columns back from each updated row (SQL `RETURNING`) instead of a row count. The result is one `Symbol`-keyed hash per updated row, with values cast to their Ruby types (the same casting as [`select_all_serialized`](#serialized-reads-ruby-type-casting)):
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
PgSqlCaller::BulkUpdate.call(Employee, [
|
|
337
|
+
{ id: 1, name: 'John', department_id: 10 },
|
|
338
|
+
{ id: 2, name: 'Jane', department_id: 20 }
|
|
339
|
+
], returning: %i[id name])
|
|
340
|
+
# => [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
A single column may be passed as a `Symbol` (`returning: :id`). Without `returning` (the default) the call returns the affected-row **count** exactly as before — the behavior is unchanged.
|
|
344
|
+
|
|
331
345
|
### Rules and behavior
|
|
332
346
|
|
|
333
347
|
- **Every row must include each `unique_by` column**, and all hashes must share the same set of keys.
|
|
334
348
|
- Only the columns you list are written; `unique_by` columns are used for matching, the rest are updated. Columns you omit (e.g. `created_at`) are left untouched.
|
|
335
349
|
- Rows that don't match an existing row are simply not updated — this **never inserts**.
|
|
336
|
-
- Returns the number of rows affected (`0` when `attrs_list` is empty — a no-op).
|
|
337
|
-
- Raises `ArgumentError` (before touching the database) if a row omits a `unique_by` column
|
|
350
|
+
- Returns the number of rows affected (`0` when `attrs_list` is empty — a no-op). With `returning`, it instead returns the updated rows as `Symbol`-keyed hashes (`[]` when `attrs_list` is empty).
|
|
351
|
+
- Raises `ArgumentError` (before touching the database) if a row omits a `unique_by` column, names a column that doesn't exist on the model, or `returning` is empty or names an unknown column.
|
|
338
352
|
|
|
339
353
|
### Why not `upsert_all` or a loop of `update_all`?
|
|
340
354
|
|
|
@@ -42,37 +42,71 @@ module PgSqlCaller
|
|
|
42
42
|
# `unique_by` column, and all hashes MUST share the same keys
|
|
43
43
|
# @param unique_by [Symbol, Array<Symbol>] the match column(s) — a single column,
|
|
44
44
|
# or all parts of a composite key (default +:id+)
|
|
45
|
-
# @
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
# @param returning [Symbol, Array<Symbol>, nil] column(s) to read back from each
|
|
46
|
+
# updated row via SQL `RETURNING`; +nil+ (default) keeps the row-count behavior
|
|
47
|
+
# @return [Integer, Array<Hash{Symbol => Object}>] the number of rows affected, or —
|
|
48
|
+
# when +returning+ is given — the updated rows as type-cast, Symbol-keyed hashes
|
|
49
|
+
def self.call(model_class, attrs_list, unique_by: :id, returning: nil)
|
|
50
|
+
new(model_class, attrs_list, unique_by: unique_by, returning: returning).call
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
attr_reader :model_class, :unique_by, :attrs_list
|
|
53
|
+
attr_reader :model_class, :unique_by, :attrs_list, :returning
|
|
51
54
|
|
|
52
55
|
# @param model_class [Class<ActiveRecord::Base>] the model whose table is updated
|
|
53
56
|
# @param attrs_list [Array<Hash>] one hash per row; each MUST include every
|
|
54
57
|
# `unique_by` column, and all hashes MUST share the same keys
|
|
55
58
|
# @param unique_by [Symbol, Array<Symbol>] the match column(s) — a single column,
|
|
56
59
|
# or all parts of a composite key (default +:id+)
|
|
57
|
-
|
|
60
|
+
# @param returning [Symbol, Array<Symbol>, nil] column(s) to read back from each
|
|
61
|
+
# updated row via SQL `RETURNING`; +nil+ (default) keeps the row-count behavior
|
|
62
|
+
def initialize(model_class, attrs_list, unique_by: :id, returning: nil)
|
|
58
63
|
@model_class = model_class
|
|
59
64
|
@attrs_list = attrs_list
|
|
60
65
|
@unique_by = Array(unique_by)
|
|
66
|
+
@returning = returning.nil? ? nil : Array(returning)
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
# Execute the bulk update as a single `UPDATE ... FROM unnest(...)` statement.
|
|
64
70
|
#
|
|
65
|
-
# @return [Integer
|
|
66
|
-
#
|
|
67
|
-
#
|
|
71
|
+
# @return [Integer, Array<Hash{Symbol => Object}>] without +returning+, the number of
|
|
72
|
+
# rows affected (0 when +attrs_list+ is empty); with +returning+, the updated rows as
|
|
73
|
+
# type-cast, Symbol-keyed hashes (+[]+ when +attrs_list+ is empty)
|
|
74
|
+
# @raise [ArgumentError] if a row omits a `unique_by` column, names a column that does
|
|
75
|
+
# not exist on the model, or +returning+ is empty or names an unknown column
|
|
68
76
|
def call
|
|
69
|
-
|
|
77
|
+
validate_returning! unless returning.nil?
|
|
78
|
+
return empty_result if attrs_list.empty?
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
if returning.nil?
|
|
81
|
+
sql_caller.execute(sql, *bindings).cmd_tuples
|
|
82
|
+
else
|
|
83
|
+
sql_caller.select_all_serialized(sql, *bindings)
|
|
84
|
+
end
|
|
72
85
|
end
|
|
73
86
|
|
|
74
87
|
private
|
|
75
88
|
|
|
89
|
+
# The value returned for an empty +attrs_list+: a zero row count, or an empty row set
|
|
90
|
+
# when +returning+ was requested — mirroring the shape {#call} returns when it runs.
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer, Array]
|
|
93
|
+
def empty_result
|
|
94
|
+
returning.nil? ? 0 : []
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validate the requested `RETURNING` columns before any SQL runs: at least one column
|
|
98
|
+
# must be named, and every column must exist on the model (each is qualified with the
|
|
99
|
+
# target alias `t`, so it must be a real column, never an expression).
|
|
100
|
+
#
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @raise [ArgumentError] if +returning+ is empty or names a column unknown to the model
|
|
103
|
+
def validate_returning!
|
|
104
|
+
raise ArgumentError, 'returning must name at least one column' if returning.empty?
|
|
105
|
+
|
|
106
|
+
unknown = returning.map(&:to_s) - model_class.column_names
|
|
107
|
+
raise ArgumentError, "unknown #{model_class} returning columns: #{unknown.join(', ')}" if unknown.any?
|
|
108
|
+
end
|
|
109
|
+
|
|
76
110
|
# The SQL executor, built from the model's own connection: it sanitizes the bound
|
|
77
111
|
# values, runs the statement and encodes the typed PostgreSQL arrays.
|
|
78
112
|
#
|
|
@@ -123,16 +157,28 @@ module PgSqlCaller
|
|
|
123
157
|
end
|
|
124
158
|
|
|
125
159
|
# The full `UPDATE ... FROM unnest(...)` statement, with one `?` placeholder per
|
|
126
|
-
# column for the value arrays.
|
|
160
|
+
# column for the value arrays, plus a `RETURNING` clause when +returning+ was given.
|
|
127
161
|
#
|
|
128
162
|
# @return [String]
|
|
129
163
|
def sql
|
|
130
|
-
<<~SQL.squish
|
|
164
|
+
statement = <<~SQL.squish
|
|
131
165
|
UPDATE #{model_class.quoted_table_name} AS t
|
|
132
166
|
SET #{set_clause}
|
|
133
167
|
FROM unnest(#{unnest_args}) AS v(#{column_aliases})
|
|
134
168
|
WHERE #{match_clause}
|
|
135
169
|
SQL
|
|
170
|
+
return statement if returning.nil?
|
|
171
|
+
|
|
172
|
+
"#{statement} RETURNING #{returning_clause}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The `RETURNING t.col, ...` projection. Each column is qualified with the target
|
|
176
|
+
# alias `t` because the `unnest` source alias `v` shares the same column names, so an
|
|
177
|
+
# unqualified `RETURNING` would be ambiguous.
|
|
178
|
+
#
|
|
179
|
+
# @return [String]
|
|
180
|
+
def returning_clause
|
|
181
|
+
returning.map { |col| "t.#{quoted(col)}" }.join(', ')
|
|
136
182
|
end
|
|
137
183
|
|
|
138
184
|
# The `SET col = v.col, ...` assignments for the value columns.
|