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 +4 -4
- data/lib/tina4/database/sqlite3_adapter.rb +24 -4
- data/lib/tina4/database.rb +22 -8
- data/lib/tina4/drivers/firebird_driver.rb +27 -5
- 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 +245 -22
- data/lib/tina4/metrics.rb +11 -2
- 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: 7743996e98fff70baf3c82a0f2a26dfafb24cb0c9ddc46cfc6a3be5a1df4c92f
|
|
4
|
+
data.tar.gz: 2b747045e71df0e69e7d6bdd3cb2b19309fcea16617770982b5743a4c6db9edc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|
|
@@ -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
|
-
@
|
|
236
|
+
@connection.transaction
|
|
237
|
+
@in_transaction = true
|
|
218
238
|
end
|
|
219
239
|
|
|
220
240
|
def commit
|
|
221
|
-
@
|
|
241
|
+
@connection&.commit
|
|
242
|
+
@in_transaction = false
|
|
222
243
|
end
|
|
223
244
|
|
|
224
245
|
def rollback
|
|
225
|
-
@
|
|
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
|
-
@
|
|
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) &&
|
|
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
|
-
|
|
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
|