tina4ruby 3.13.52 → 3.13.54
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/dev_admin.rb +3 -2
- data/lib/tina4/migration.rb +52 -8
- data/lib/tina4/orm.rb +55 -4
- data/lib/tina4/version.rb +1 -1
- 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: cd2180527148137de7bd985ed3951aefcae89d9fee30032de3c34434b3cc209e
|
|
4
|
+
data.tar.gz: 78e32b6e6021f24c455bb002339a59c54a8a134a1b219493c6dc994d66ebaff5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f422139294a6d51b34507f116dd21621e5299829b02f7ec613083bd2bc9d051a4c832c01dab8120978708fa637302b35b6b05d2f463a188f91dfc9d89d2879b
|
|
7
|
+
data.tar.gz: 96ebc4a47de1674a828dc5913c4586fc82170f0c04159724d69a1cd6a99d15596bc5f217aa76ed4e119675559fc48d7b22cbd71eccd10e231de32d1ab2bc61da
|
data/lib/tina4/dev_admin.rb
CHANGED
|
@@ -1688,8 +1688,9 @@ module Tina4
|
|
|
1688
1688
|
|
|
1689
1689
|
# Native MCP JSON-RPC endpoint (POST /__dev/mcp[/message]). Real MCP
|
|
1690
1690
|
# clients POST a JSON-RPC 2.0 request; we hand the parsed body straight
|
|
1691
|
-
# to the default server's
|
|
1692
|
-
# notification (no id) yields an empty
|
|
1691
|
+
# to the default server's dispatch_http and echo the response. A
|
|
1692
|
+
# notification (no id) yields an empty 202 Accepted (the MCP Streamable
|
|
1693
|
+
# HTTP contract), mirroring the Python master, PHP and Node.
|
|
1693
1694
|
# Streamable HTTP POST /__dev/mcp — the current MCP transport. initialize
|
|
1694
1695
|
# issues an Mcp-Session-Id; an unknown session on a non-initialize request
|
|
1695
1696
|
# is a 404; a notification is 202; anything else answers inline (200).
|
data/lib/tina4/migration.rb
CHANGED
|
@@ -229,19 +229,34 @@ module Tina4
|
|
|
229
229
|
end
|
|
230
230
|
end
|
|
231
231
|
|
|
232
|
+
# Return a result row keyed by lower-case symbols. Firebird returns rows keyed
|
|
233
|
+
# by UPPERCASE string column names while SQLite/others return lower-case symbol
|
|
234
|
+
# keys, so the tracking-table reads below are normalised to one shape. Without
|
|
235
|
+
# this, `row[:migration_name]` is nil on Firebird and every applied migration
|
|
236
|
+
# is treated as pending and re-run.
|
|
237
|
+
def normalize_row(row)
|
|
238
|
+
return {} if row.nil?
|
|
239
|
+
|
|
240
|
+
source = row.respond_to?(:to_h) ? row.to_h : row
|
|
241
|
+
source.each_with_object({}) { |(k, v), h| h[k.to_s.downcase.to_sym] = v }
|
|
242
|
+
end
|
|
243
|
+
|
|
232
244
|
def completed_migrations
|
|
233
245
|
result = @db.fetch("SELECT migration_name FROM #{TRACKING_TABLE} WHERE passed = 1 ORDER BY id")
|
|
234
|
-
result.map { |r| r[:migration_name] }
|
|
246
|
+
result.map { |r| normalize_row(r)[:migration_name] }
|
|
235
247
|
end
|
|
236
248
|
|
|
237
249
|
def completed_migrations_with_batch
|
|
238
250
|
result = @db.fetch("SELECT id, migration_name, batch FROM #{TRACKING_TABLE} WHERE passed = 1 ORDER BY id")
|
|
239
|
-
result.map
|
|
251
|
+
result.map do |r|
|
|
252
|
+
row = normalize_row(r)
|
|
253
|
+
{ id: row[:id], migration_name: row[:migration_name], batch: row[:batch] }
|
|
254
|
+
end
|
|
240
255
|
end
|
|
241
256
|
|
|
242
257
|
def next_batch_number
|
|
243
|
-
result = @db.fetch_one("SELECT MAX(batch) as max_batch FROM #{TRACKING_TABLE} WHERE passed = 1")
|
|
244
|
-
(result
|
|
258
|
+
result = normalize_row(@db.fetch_one("SELECT MAX(batch) as max_batch FROM #{TRACKING_TABLE} WHERE passed = 1"))
|
|
259
|
+
(result[:max_batch] ? result[:max_batch].to_i : 0) + 1
|
|
245
260
|
end
|
|
246
261
|
|
|
247
262
|
def pending_migrations
|
|
@@ -479,12 +494,22 @@ module Tina4
|
|
|
479
494
|
next
|
|
480
495
|
end
|
|
481
496
|
|
|
482
|
-
# Statement delimiter — only reached outside blocks/comments/strings.
|
|
497
|
+
# Statement delimiter — only reached outside blocks/comments/strings. A
|
|
498
|
+
# SET TERM directive switches the active terminator and is consumed
|
|
499
|
+
# (never emitted); any other completed statement is collected.
|
|
483
500
|
if !delimiter.empty? && sql[i, dlen] == delimiter
|
|
501
|
+
i += dlen
|
|
484
502
|
stmt = current.strip
|
|
485
|
-
statements << stmt unless stmt.empty?
|
|
486
503
|
current = +""
|
|
487
|
-
|
|
504
|
+
unless stmt.empty?
|
|
505
|
+
new_term = parse_set_term(stmt)
|
|
506
|
+
if new_term
|
|
507
|
+
delimiter = new_term
|
|
508
|
+
dlen = delimiter.length
|
|
509
|
+
else
|
|
510
|
+
statements << stmt
|
|
511
|
+
end
|
|
512
|
+
end
|
|
488
513
|
next
|
|
489
514
|
end
|
|
490
515
|
|
|
@@ -492,11 +517,30 @@ module Tina4
|
|
|
492
517
|
i += 1
|
|
493
518
|
end
|
|
494
519
|
|
|
520
|
+
# Trailing statement (may not end with a delimiter). A trailing SET TERM
|
|
521
|
+
# directive is a no-op — consume it, don't emit it.
|
|
495
522
|
stmt = current.strip
|
|
496
|
-
statements << stmt unless stmt.empty?
|
|
523
|
+
statements << stmt unless stmt.empty? || parse_set_term(stmt)
|
|
497
524
|
statements
|
|
498
525
|
end
|
|
499
526
|
|
|
527
|
+
# Return the new terminator from a `SET TERM <new> <current>` directive, or
|
|
528
|
+
# nil when +statement+ is not a SET TERM directive.
|
|
529
|
+
#
|
|
530
|
+
# SET TERM is a script-level directive (recognised by isql and other
|
|
531
|
+
# InterBase/Firebird tooling, not run by the engine) that changes the
|
|
532
|
+
# terminator separating statements. Recognising it lets a statement whose own
|
|
533
|
+
# body contains the default ";" terminator — a trigger, stored procedure or
|
|
534
|
+
# EXECUTE BLOCK — be kept intact rather than split on those inner ";". The
|
|
535
|
+
# terminator may be more than one character (e.g. "!!").
|
|
536
|
+
#
|
|
537
|
+
# @param statement [String] a single, already-stripped statement
|
|
538
|
+
# @return [String, nil] the new terminator, or nil when not a SET TERM directive
|
|
539
|
+
def parse_set_term(statement)
|
|
540
|
+
m = statement.strip.match(/\ASET\s+TERM\s+(\S+)\z/i)
|
|
541
|
+
m && m[1]
|
|
542
|
+
end
|
|
543
|
+
|
|
500
544
|
def execute_sql_file(file)
|
|
501
545
|
sql = File.read(file)
|
|
502
546
|
statements = split_sql_statements(sql)
|
data/lib/tina4/orm.rb
CHANGED
|
@@ -419,6 +419,20 @@ module Tina4
|
|
|
419
419
|
else "DATETIME"
|
|
420
420
|
end
|
|
421
421
|
|
|
422
|
+
# Engine-aware JSON column type (parity with the Python master's
|
|
423
|
+
# JSONField DDL). PostgreSQL gets native JSONB (indexable, canonical);
|
|
424
|
+
# MySQL native JSON; MSSQL stores JSON as NVARCHAR(MAX) (its JSON
|
|
425
|
+
# functions read that); Firebird has no TEXT type so it uses a text
|
|
426
|
+
# BLOB; SQLite and everything else store the JSON text in TEXT
|
|
427
|
+
# (queryable via json1).
|
|
428
|
+
json_sql = case engine
|
|
429
|
+
when "postgres", "postgresql" then "JSONB"
|
|
430
|
+
when "mysql" then "JSON"
|
|
431
|
+
when "mssql", "sqlserver" then "NVARCHAR(MAX)"
|
|
432
|
+
when "firebird" then "BLOB SUB_TYPE TEXT"
|
|
433
|
+
else "TEXT"
|
|
434
|
+
end
|
|
435
|
+
|
|
422
436
|
type_map = {
|
|
423
437
|
integer: "INTEGER",
|
|
424
438
|
string: "VARCHAR(255)",
|
|
@@ -430,7 +444,7 @@ module Tina4
|
|
|
430
444
|
datetime: datetime_sql,
|
|
431
445
|
timestamp: "TIMESTAMP",
|
|
432
446
|
blob: "BLOB",
|
|
433
|
-
json:
|
|
447
|
+
json: json_sql
|
|
434
448
|
}
|
|
435
449
|
|
|
436
450
|
col_defs = []
|
|
@@ -444,7 +458,11 @@ module Tina4
|
|
|
444
458
|
parts << "PRIMARY KEY" if opts[:primary_key]
|
|
445
459
|
parts << "AUTOINCREMENT" if opts[:auto_increment]
|
|
446
460
|
parts << "NOT NULL" if !opts[:nullable] && !opts[:primary_key]
|
|
447
|
-
|
|
461
|
+
# A JSON column carries no DDL DEFAULT (parity with the Python master):
|
|
462
|
+
# a dict/list default is an application-level concern, applied per
|
|
463
|
+
# instance, not a portable SQL literal (PG needs a ::jsonb cast, MySQL
|
|
464
|
+
# an expression default). The instance still gets its default at new.
|
|
465
|
+
if opts[:default] && !opts[:auto_increment] && opts[:type] != :json
|
|
448
466
|
parts << "DEFAULT #{default_literal(opts[:default], opts[:type], bool_sql)}"
|
|
449
467
|
end
|
|
450
468
|
col_defs << parts.join(" ")
|
|
@@ -486,6 +504,21 @@ module Tina4
|
|
|
486
504
|
hash.each do |key, value|
|
|
487
505
|
# Apply field mapping (db_col => ruby_attr)
|
|
488
506
|
attr_name = mapping_reverse[key.to_s] || key
|
|
507
|
+
# A JSON column comes back from the driver as a JSON string (SQLite
|
|
508
|
+
# TEXT, MySQL JSON, PostgreSQL JSONB via the text protocol, MSSQL
|
|
509
|
+
# NVARCHAR). Decode it to the Hash/Array the attribute expects
|
|
510
|
+
# (parity with the Python master's JSONField parse-on-read). A value
|
|
511
|
+
# already a Hash/Array is left untouched; nil stays nil; a
|
|
512
|
+
# non-decodable string keeps its raw form rather than crashing a
|
|
513
|
+
# normal read.
|
|
514
|
+
fdef = field_definitions[attr_name.to_sym]
|
|
515
|
+
if fdef && fdef[:type] == :json && value.is_a?(String)
|
|
516
|
+
begin
|
|
517
|
+
value = JSON.parse(value)
|
|
518
|
+
rescue JSON::ParserError
|
|
519
|
+
# leave the raw string in place
|
|
520
|
+
end
|
|
521
|
+
end
|
|
489
522
|
setter = "#{attr_name}="
|
|
490
523
|
instance.__send__(setter, value) if instance.respond_to?(setter)
|
|
491
524
|
end
|
|
@@ -610,6 +643,11 @@ module Tina4
|
|
|
610
643
|
if __send__(name).nil? && opts[:default]
|
|
611
644
|
d = opts[:default]
|
|
612
645
|
d = d.call if d.respond_to?(:call) && !d.is_a?(Class)
|
|
646
|
+
# Deep-copy a mutable Hash/Array default so two instances never alias
|
|
647
|
+
# the same object (e.g. `json_field :meta, default: {}` — mutating
|
|
648
|
+
# a.meta must not leak into b.meta). Parity with the Python master,
|
|
649
|
+
# which deepcopies a JSONField's dict/list default per instance.
|
|
650
|
+
d = Marshal.load(Marshal.dump(d)) if d.is_a?(Hash) || d.is_a?(Array)
|
|
613
651
|
__send__("#{name}=", d)
|
|
614
652
|
end
|
|
615
653
|
end
|
|
@@ -667,7 +705,6 @@ module Tina4
|
|
|
667
705
|
return false
|
|
668
706
|
end
|
|
669
707
|
|
|
670
|
-
data = to_db_hash(exclude_nil: true)
|
|
671
708
|
pk = self.class.primary_key_field || :id
|
|
672
709
|
pk_value = __send__(pk)
|
|
673
710
|
pk_opts = self.class.field_definitions[pk] || {}
|
|
@@ -694,6 +731,10 @@ module Tina4
|
|
|
694
731
|
end
|
|
695
732
|
|
|
696
733
|
begin
|
|
734
|
+
# Built here (inside the rescue's reach) so a JSON column that can't be
|
|
735
|
+
# serialised (to_db_hash raises JSON::GeneratorError) fails loud through
|
|
736
|
+
# the same path as a driver error — rolled back, false, cause recorded.
|
|
737
|
+
data = to_db_hash(exclude_nil: true)
|
|
697
738
|
self.class.db.transaction do |db|
|
|
698
739
|
if is_update
|
|
699
740
|
filter = { pk => pk_value }
|
|
@@ -941,9 +982,19 @@ module Tina4
|
|
|
941
982
|
def to_db_hash(exclude_nil: false)
|
|
942
983
|
hash = {}
|
|
943
984
|
mapping = self.class.field_mapping
|
|
944
|
-
self.class.field_definitions.
|
|
985
|
+
self.class.field_definitions.each do |name, opts|
|
|
945
986
|
value = __send__(name)
|
|
946
987
|
next if exclude_nil && value.nil?
|
|
988
|
+
# A JSON column serialises its Hash/Array to a JSON string for the
|
|
989
|
+
# driver (parity with the Python master's JSONField.to_db). A value
|
|
990
|
+
# that can't be encoded (e.g. Float::NAN) raises JSON::GeneratorError;
|
|
991
|
+
# save() computes this hash inside its rescue, so it fails loud —
|
|
992
|
+
# rolls back, returns false, records the cause. A value already a
|
|
993
|
+
# String is left as-is (a caller who pre-encoded stays in control);
|
|
994
|
+
# nil passes through untouched.
|
|
995
|
+
if opts[:type] == :json && !value.nil? && !value.is_a?(String)
|
|
996
|
+
value = JSON.generate(value)
|
|
997
|
+
end
|
|
947
998
|
db_col = mapping[name.to_s] || name
|
|
948
999
|
hash[db_col.to_sym] = value
|
|
949
1000
|
end
|
data/lib/tina4/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tina4ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.13.
|
|
4
|
+
version: 3.13.54
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tina4 Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|