tina4ruby 3.13.85 → 3.13.86

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: dae161f7ae0f39d834a01876bde153cd83b4df51ac355225c2c123ce20eec210
4
+ data.tar.gz: 1e12103ca5685fc65dcfa7dedd16c38ddf5d4435ccd77ad4f73ad9a987596e2f
5
5
  SHA512:
6
- metadata.gz: 28698513a1fba4a12fcf35fb08ee91cc81ad150971326661164db8ee1ff3503505bfc7f6803c40d62f64a303f1d5aabe4c10adc8dd0a708482d4ddd76ff70bd7
7
- data.tar.gz: 52fcfb7dea6f227561c3b6776a6ebb5d7357b1941baed3e1c3eeafefd9a66c2bc3dd71887b5fd22a10826fc0109cb13c7b6cab1ce1b27ece86b35822f08a1c01
6
+ metadata.gz: 5ea0ee3373aab820be175916dda33ca1e353f8cd9894ab1fa2613af2a85438195b5345a0af2b441ec444c51482e8bb64632f49c9af3e4f6acedbef8c3f66fe45
7
+ data.tar.gz: cd556ef4a20f2b6acd245a2926af62e8592ca56a11fb3827ffcb9e8fede128f928b070ab1bfd7acb850a0b4371f8ec6367499d140488216eeb7a0f65939303ab
@@ -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.
@@ -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
data/lib/tina4/frond.rb CHANGED
@@ -837,12 +837,22 @@ module Tina4
837
837
  # Find the first occurrence of +needle+ that is not inside quotes or
838
838
  # parentheses. Returns the index, or -1 if not found.
839
839
  def find_outside_quotes(expr, needle)
840
+ # Fast path. This is the hottest method in a render -- profiling the Python
841
+ # twin showed 415,200 calls and 53% of render time for a single 20-row loop
842
+ # template, and the overwhelming majority return -1 because the needle
843
+ # simply is not in the expression. include? is a C-level scan, so bailing
844
+ # here skips the whole Ruby character loop. Exact, not a heuristic: a
845
+ # needle absent from the string cannot be present outside quotes either.
846
+ return -1 unless expr.include?(needle)
847
+
840
848
  in_q = nil
841
849
  depth = 0
842
850
  bracket_depth = 0
843
851
  i = 0
844
852
  nlen = needle.length
845
- while i <= expr.length - nlen
853
+ # Hoisted: expr.length was recomputed on every iteration of the condition.
854
+ last_start = expr.length - nlen
855
+ while i <= last_start
846
856
  ch = expr[i]
847
857
  if (ch == '"' || ch == "'") && depth == 0
848
858
  if in_q.nil?
data/lib/tina4/orm.rb CHANGED
@@ -785,9 +785,9 @@ module Tina4
785
785
  # Only adopt the engine-assigned id for an auto-increment PK. A
786
786
  # natural-key PK was set by the caller; don't overwrite it with the
787
787
  # driver's last_insert_id (which may be a sequence value that
788
- # doesn't apply here).
789
- if auto_increment && result[:last_id] && respond_to?("#{pk}=")
790
- __send__("#{pk}=", result[:last_id])
788
+ # doesn't apply here). db.insert returns a DatabaseResult (.last_id).
789
+ if auto_increment && result.last_id && respond_to?("#{pk}=")
790
+ __send__("#{pk}=", result.last_id)
791
791
  end
792
792
  end
793
793
  end