tina4ruby 3.13.85 → 3.13.87

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: 8fcb9fe4b1fb31911d0526492fae8dd15ebc272e57d3f536b60b367be0ca55d9
4
- data.tar.gz: 497811d42911aafc4feb5b1aff1a4084ea7e00e4d074d9fd5884ac7a09e60d56
3
+ metadata.gz: 7743996e98fff70baf3c82a0f2a26dfafb24cb0c9ddc46cfc6a3be5a1df4c92f
4
+ data.tar.gz: 2b747045e71df0e69e7d6bdd3cb2b19309fcea16617770982b5743a4c6db9edc
5
5
  SHA512:
6
- metadata.gz: 28698513a1fba4a12fcf35fb08ee91cc81ad150971326661164db8ee1ff3503505bfc7f6803c40d62f64a303f1d5aabe4c10adc8dd0a708482d4ddd76ff70bd7
7
- data.tar.gz: 52fcfb7dea6f227561c3b6776a6ebb5d7357b1941baed3e1c3eeafefd9a66c2bc3dd71887b5fd22a10826fc0109cb13c7b6cab1ce1b27ece86b35822f08a1c01
6
+ metadata.gz: f94e1f6feeda985b8c5091c0235181bb34165f4ee42120fa808b5b8a39cd8deaabc8ae9fe68a9c05d33598d184680d3b49c40f9791b1f0d2fb57689a87b73054
7
+ data.tar.gz: 6d31b432dddc04f237bddd28e6580d294c9427a22b0fda2947a9e2d9e3aba469b2fd06cdb2e0560d8edb60f1383c6916ba407a7efb257202d7cb95e381514083
@@ -60,7 +60,7 @@ module Tina4
60
60
  # Execute a query and return rows as array of symbol-keyed hashes
61
61
  def query(sql, params = [])
62
62
  results = @connection.execute(sql, params)
63
- results.map { |row| symbolize_keys(row) }
63
+ symbolize_rows(results)
64
64
  end
65
65
 
66
66
  # Paginated fetch
@@ -155,11 +155,31 @@ module Tina4
155
155
 
156
156
  private
157
157
 
158
- def symbolize_keys(hash)
159
- hash.each_with_object({}) do |(k, v), h|
160
- h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
158
+ # Symbolize a whole result set's keys, computing the mapping ONCE per query
159
+ # rather than per cell. See the matching helper in drivers/sqlite_driver.rb
160
+ # for the rationale: the per-row form ran k.to_s.to_sym for every cell, so a
161
+ # 5,000-row x 6-column fetch did 30,000 conversions for 6 distinct keys. The
162
+ # is_a? guard still drops any positional Integer keys older gems emit.
163
+ def symbolize_rows(rows)
164
+ return rows if rows.empty?
165
+
166
+ str_keys = rows.first.keys.select { |k| k.is_a?(String) || k.is_a?(Symbol) }
167
+ sym_keys = str_keys.map(&:to_sym)
168
+ count = str_keys.length
169
+ rows.map do |row|
170
+ out = {}
171
+ i = 0
172
+ while i < count
173
+ out[sym_keys[i]] = row[str_keys[i]]
174
+ i += 1
175
+ end
176
+ out
161
177
  end
162
178
  end
179
+
180
+ def symbolize_keys(hash)
181
+ symbolize_rows([hash]).first
182
+ end
163
183
  end
164
184
  end
165
185
  end
@@ -517,7 +517,11 @@ module Tina4
517
517
  if drv.respond_to?(:insert)
518
518
  result = drv.insert(table, data)
519
519
  autocommit_standalone_write(drv)
520
- return result
520
+ # A driver that owns its insert (PostgreSQL, via RETURNING *) returns a
521
+ # Hash { success:, last_id: }; normalise it to a DatabaseResult so every
522
+ # engine returns the same write-result shape (parity with Python master).
523
+ last_id = result.is_a?(Hash) ? result[:last_id] : drv.last_insert_id
524
+ return Tina4::DatabaseResult.new([], affected_rows: write_affected(drv, 1), last_id: last_id)
521
525
  end
522
526
 
523
527
  columns = data.keys.map(&:to_s)
@@ -526,8 +530,17 @@ module Tina4
526
530
  drv.execute(sql, data.values)
527
531
  last_id = drv.last_insert_id
528
532
  autocommit_standalone_write(drv)
529
- { success: true, last_id: last_id }
533
+ Tina4::DatabaseResult.new([], affected_rows: write_affected(drv, 1), last_id: last_id)
534
+ end
535
+
536
+ # Rows a write affected, read from the driver where it exposes a count
537
+ # (SQLite: connection.changes), else a best-effort default (1 for a single
538
+ # insert). Mirrors the Python master + PHP, whose write DatabaseResult carries
539
+ # affected_rows/affectedRows; best-effort on drivers without a native count.
540
+ def write_affected(drv, default = 0)
541
+ drv.respond_to?(:affected_rows) ? drv.affected_rows.to_i : default
530
542
  end
543
+ private :write_affected
531
544
 
532
545
  def update(table, data, filter = {}, params = nil)
533
546
  cache_invalidate if @cache_enabled
@@ -540,7 +553,7 @@ module Tina4
540
553
  sql += " WHERE #{filter}" unless filter.empty?
541
554
  drv.execute(sql, data.values + Array(params))
542
555
  autocommit_standalone_write(drv)
543
- return { success: true }
556
+ return Tina4::DatabaseResult.new([], affected_rows: write_affected(drv))
544
557
  end
545
558
 
546
559
  set_parts = data.keys.map { |k| "#{k} = #{drv.placeholder}" }
@@ -550,7 +563,7 @@ module Tina4
550
563
  values = data.values + filter.values
551
564
  drv.execute(sql, values)
552
565
  autocommit_standalone_write(drv)
553
- { success: true }
566
+ Tina4::DatabaseResult.new([], affected_rows: write_affected(drv))
554
567
  end
555
568
 
556
569
  def delete(table, filter = {}, params = nil)
@@ -559,8 +572,9 @@ module Tina4
559
572
 
560
573
  # List of hashes — delete each row
561
574
  if filter.is_a?(Array)
562
- filter.each { |row| delete(table, row) }
563
- return { success: true }
575
+ total = 0
576
+ filter.each { |row| total += delete(table, row).affected_rows }
577
+ return Tina4::DatabaseResult.new([], affected_rows: total)
564
578
  end
565
579
 
566
580
  # String filter — raw WHERE clause with optional params
@@ -569,7 +583,7 @@ module Tina4
569
583
  sql += " WHERE #{filter}" unless filter.empty?
570
584
  drv.execute(sql, Array(params))
571
585
  autocommit_standalone_write(drv)
572
- return { success: true }
586
+ return Tina4::DatabaseResult.new([], affected_rows: write_affected(drv))
573
587
  end
574
588
 
575
589
  # Hash filter — build WHERE from keys
@@ -578,7 +592,7 @@ module Tina4
578
592
  sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
579
593
  drv.execute(sql, filter.values)
580
594
  autocommit_standalone_write(drv)
581
- { success: true }
595
+ Tina4::DatabaseResult.new([], affected_rows: write_affected(drv))
582
596
  end
583
597
 
584
598
  # Return the last execute() error message, or nil.
@@ -213,16 +213,38 @@ module Tina4
213
213
  "SELECT FIRST #{limit} SKIP #{offset} * FROM (#{sql})"
214
214
  end
215
215
 
216
+ # Transaction handling — mirrors the Python master's connection-level
217
+ # contract (tina4_python firebird.py: start_transaction sets a flag,
218
+ # commit/rollback act on the connection).
219
+ #
220
+ # In the `fb` gem the transaction lives ON the connection:
221
+ # `Fb::Connection#transaction` (no block) STARTS a transaction and returns
222
+ # `true` (a boolean — NOT a transaction object), and
223
+ # `Fb::Connection#commit` / `#rollback` end it. The old code stored that
224
+ # boolean in `@transaction` and called `@transaction&.commit` /
225
+ # `&.rollback`, i.e. `true.commit` / `true.rollback` — a NoMethodError that
226
+ # broke every explicit-transaction commit AND rollback (a rolled-back write
227
+ # was never undone). We now start the transaction on the connection and
228
+ # commit/rollback the CONNECTION, tracking open-ness with an
229
+ # `@in_transaction` boolean (parity with Python's `_in_transaction`).
230
+ #
231
+ # A standalone write auto-commits inside the gem's own `execute`, so the
232
+ # framework's autocommit_standalone_write then calls #commit with no txn
233
+ # open — `Fb::Connection#commit` is a harmless no-op there (returns nil, no
234
+ # raise), so standalone-autocommit is preserved.
216
235
  def begin_transaction
217
- @transaction = @connection.transaction
236
+ @connection.transaction
237
+ @in_transaction = true
218
238
  end
219
239
 
220
240
  def commit
221
- @transaction&.commit
241
+ @connection&.commit
242
+ @in_transaction = false
222
243
  end
223
244
 
224
245
  def rollback
225
- @transaction&.rollback
246
+ @connection&.rollback
247
+ @in_transaction = false
226
248
  end
227
249
 
228
250
  def tables
@@ -263,7 +285,7 @@ module Tina4
263
285
  # connection already gone — nothing to clean up
264
286
  end
265
287
  @connection = nil
266
- @transaction = nil
288
+ @in_transaction = false
267
289
  open_connection
268
290
  end
269
291
 
@@ -273,7 +295,7 @@ module Tina4
273
295
  def with_reconnect
274
296
  yield
275
297
  rescue StandardError => e
276
- raise unless self.class.dead_connection?(e) && @transaction.nil?
298
+ raise unless self.class.dead_connection?(e) && !@in_transaction
277
299
  reconnect!
278
300
  yield
279
301
  end
@@ -182,11 +182,9 @@ module Tina4
182
182
 
183
183
  private
184
184
 
185
- def symbolize_keys(hash)
186
- hash.each_with_object({}) do |(k, v), h|
187
- h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
188
- end
189
- end
185
+ # (No symbolize_keys here: execute_query already hydrates by zipping a
186
+ # once-computed symbol column list against each array row, so there is no
187
+ # per-cell symbolize to hoist. A leftover unused symbolize_keys was removed.)
190
188
  end
191
189
  end
192
190
  end
@@ -40,7 +40,7 @@ module Tina4
40
40
  else
41
41
  @connection.exec_params(converted_sql, params)
42
42
  end
43
- result.map { |row| decode_blobs(symbolize_keys(row)) }
43
+ symbolize_result(result)
44
44
  end
45
45
 
46
46
  def execute(sql, params = [])
@@ -297,8 +297,36 @@ module Tina4
297
297
  sql.gsub("?") { counter += 1; "$#{counter}" }
298
298
  end
299
299
 
300
- def symbolize_keys(hash)
301
- hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
300
+ # Hydrate a PG::Result into an array of symbol-keyed row hashes.
301
+ #
302
+ # The old path did `result.map { |row| decode_blobs(symbolize_keys(row)) }`,
303
+ # which made the pg gem build a string-keyed Hash PER ROW and then rebuilt
304
+ # each as symbol-keyed with a per-cell `k.to_sym` -- two hashes per row plus
305
+ # N symbol conversions per row. This reads directly from `each_row` (the gem
306
+ # yields positional String arrays, no per-row Hash) against the field names
307
+ # symbolised ONCE for the whole result. Measured on real PostgreSQL 16.14,
308
+ # a 5,000-row x 7-column fetch went 13.84ms -> 7.55ms (-45%) with
309
+ # byte-identical output (values are the same String forms `map` produced,
310
+ # e.g. "t" for a boolean, nil for NULL). Duplicate column names from a JOIN
311
+ # collapse last-wins, exactly as the gem's own row Hash did.
312
+ #
313
+ # decode_blobs stays per-row: it inspects values, not keys, so it cannot be
314
+ # hoisted. It is currently a documented no-op (pg returns bytea as raw
315
+ # ASCII-8BIT strings); keeping the call preserves the seam.
316
+ def symbolize_result(result)
317
+ fields = result.fields.map(&:to_sym)
318
+ count = fields.length
319
+ rows = []
320
+ result.each_row do |values|
321
+ row = {}
322
+ i = 0
323
+ while i < count
324
+ row[fields[i]] = values[i]
325
+ i += 1
326
+ end
327
+ rows << decode_blobs(row)
328
+ end
329
+ rows
302
330
  end
303
331
 
304
332
  # Ensure binary (bytea) columns are proper byte strings.
@@ -72,7 +72,7 @@ module Tina4
72
72
 
73
73
  def execute_query(sql, params = [])
74
74
  results = @connection.execute(sql, coerce_params(params))
75
- results.map { |row| symbolize_keys(row) }
75
+ symbolize_rows(results)
76
76
  end
77
77
 
78
78
  def execute(sql, params = [])
@@ -101,6 +101,13 @@ module Tina4
101
101
  @connection.last_insert_row_id
102
102
  end
103
103
 
104
+ # Rows changed by the most recent INSERT/UPDATE/DELETE on this connection.
105
+ # Feeds Database#insert/update/delete's DatabaseResult.affected_rows (parity
106
+ # with the Python master + PHP, which surface affectedRows on writes).
107
+ def affected_rows
108
+ @connection.changes
109
+ end
110
+
104
111
  def placeholder
105
112
  "?"
106
113
  end
@@ -174,11 +181,42 @@ module Tina4
174
181
  str.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
175
182
  end
176
183
 
177
- def symbolize_keys(hash)
178
- hash.each_with_object({}) do |(k, v), h|
179
- h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
184
+ # Symbolize the keys of a whole result set, computing the key mapping ONCE.
185
+ #
186
+ # results_as_hash=true hands back string-keyed hashes; every row of a query
187
+ # has the same columns, so the string->symbol conversion is identical for
188
+ # every row. The old code called `symbolize_keys` per row, which ran
189
+ # `k.to_s.to_sym` and an `is_a?` guard PER CELL: a 5,000-row x 6-column
190
+ # fetch did 30,000 conversions for 6 distinct keys and made Tina4 the
191
+ # slowest of the four frameworks at bulk reads (Select ALL 5,000 rows:
192
+ # 10.16ms vs raw sqlite3 2.56ms). Hoisting the mapping out of the loop
193
+ # roughly halves the hydration cost with byte-identical output.
194
+ #
195
+ # The is_a?(String|Symbol) guard is preserved: older sqlite3 gems put
196
+ # positional Integer keys in the hash alongside the string names, and those
197
+ # must still be dropped.
198
+ def symbolize_rows(rows)
199
+ return rows if rows.empty?
200
+
201
+ str_keys = rows.first.keys.select { |k| k.is_a?(String) || k.is_a?(Symbol) }
202
+ sym_keys = str_keys.map(&:to_sym)
203
+ count = str_keys.length
204
+ rows.map do |row|
205
+ out = {}
206
+ i = 0
207
+ while i < count
208
+ out[sym_keys[i]] = row[str_keys[i]]
209
+ i += 1
210
+ end
211
+ out
180
212
  end
181
213
  end
214
+
215
+ # Single-hash convenience kept for any external caller; delegates to the
216
+ # batch path so there is one implementation of the mapping rule.
217
+ def symbolize_keys(hash)
218
+ symbolize_rows([hash]).first
219
+ end
182
220
  end
183
221
  end
184
222
  end