tina4ruby 3.13.52 → 3.13.53

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: b6a859f05cd2ac97b5e38ab9ee678cd8419f91be1aca0dedaed1e1d63b268968
4
- data.tar.gz: 4454362a269ef77b5992a0ec4d2e30e6dd64134ed64265d1e8fd6f9432fcbc0f
3
+ metadata.gz: 79a09ae5dffe2688af3e75a98dcf999929a1c560f3d8afbdc874590d6dfca275
4
+ data.tar.gz: 92d0d0b44f0e103a3f8ac894c620b075446bab88040d865095e7b9c23ea421fe
5
5
  SHA512:
6
- metadata.gz: 595d84b72e3b75a263f54ff93859a9f92f52fc76434b7633d149d11a2fa8a172a72478a13b9aa649ea0189a1f975d0a98daf7afa7cbceeb5bec9636433c95dad
7
- data.tar.gz: 66634157b45125bb796277f54a9b93d12ab721c237b0dec39450d78d69ba4f3162972a178ef742607d45e4cc33d572c3460d7d336e0133e7c29ea902e21b68ae
6
+ metadata.gz: 128df90a6f281fefb62fc1e49e3075c52474d2c9f70cddc992e1c68a083b8dff38e9c6d014ffec0019736719f49bc2f80cb93eb2f3dff2b3deb336845db1f0e2
7
+ data.tar.gz: c579de40838336727fd25581e8d4db17bd3c449034e4805ba29a8215be60f5344c7608152c6c21cc7258396525e1bc5aee86b12ec4927de62f1745f39797601d
@@ -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 handle_message and echo the response. A
1692
- # notification (no id) yields an empty 204, mirroring Python.
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/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: "TEXT"
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
- if opts[:default] && !opts[:auto_increment]
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.each_key do |name|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.52"
4
+ VERSION = "3.13.53"
5
5
  end
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.52
4
+ version: 3.13.53
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-04 00:00:00.000000000 Z
11
+ date: 2026-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack