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 +4 -4
- data/lib/tina4/database/sqlite3_adapter.rb +24 -4
- data/lib/tina4/database.rb +22 -8
- data/lib/tina4/drivers/odbc_driver.rb +3 -5
- data/lib/tina4/drivers/postgres_driver.rb +31 -3
- data/lib/tina4/drivers/sqlite_driver.rb +42 -4
- data/lib/tina4/frond.rb +11 -1
- data/lib/tina4/orm.rb +3 -3
- data/lib/tina4/public/js/tina4-dev-admin.min.js +90 -87
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +20 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dae161f7ae0f39d834a01876bde153cd83b4df51ac355225c2c123ce20eec210
|
|
4
|
+
data.tar.gz: 1e12103ca5685fc65dcfa7dedd16c38ddf5d4435ccd77ad4f73ad9a987596e2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
data/lib/tina4/database.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
301
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
|
790
|
-
__send__("#{pk}=", result
|
|
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
|