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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4ea6fd0cd19dd9e2d12d963b0c7689cef8346a63f0981e3a96821faebf4f6bc
4
- data.tar.gz: f2d40a1f3790e084ebdb9e0bf5908241494aaacca55ca8604f18cf05a6dbb8d8
3
+ metadata.gz: 97ef871de183917ba98a3c35bdeb4a184c5aa39ab7b1e6cdc89b11ccb60e8874
4
+ data.tar.gz: d18b3aaada2a2a432e892f067233d6d22a45f2bd39045b1015d6b9fb34c6c154
5
5
  SHA512:
6
- metadata.gz: 908fcb421cdf254ba4a91cc8c67c304938cfd65ef7a25393ae3019b02e0b84ea634c2fa250fba333e88bd287be02480aafce0cd8f0780e083d3b38800de9ab90
7
- data.tar.gz: 927f8dc22e285bdf849e85bb850e5e2045429675ccb953b872150b0954ecf95602d6a6c84b52829e9b9af8b29fe8acc016d41be10628ae2181edd29c1df7d9a7
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
- ## [Unreleased]
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
- [Unreleased]: https://github.com/didww/pg_sql_caller/compare/v0.2.3...HEAD
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 or names a column that doesn't exist on the model.
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
- # @return [Integer] the number of rows affected
46
- def self.call(model_class, attrs_list, unique_by: :id)
47
- new(model_class, attrs_list, unique_by: unique_by).call
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
- def initialize(model_class, attrs_list, unique_by: :id)
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] the number of rows affected (0 when +attrs_list+ is empty)
66
- # @raise [ArgumentError] if a row omits a `unique_by` column, or names a column
67
- # that does not exist on the model
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
- return 0 if attrs_list.empty?
77
+ validate_returning! unless returning.nil?
78
+ return empty_result if attrs_list.empty?
70
79
 
71
- sql_caller.execute(sql, *bindings).cmd_tuples
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgSqlCaller
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_sql_caller
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Talakevich