tina4ruby 3.13.12 → 3.13.16

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: 303058fcf9f267e5034b659e75be74b7a5fba69740968063b7370ff046452c8c
4
- data.tar.gz: 993d93df6a783588a4b6c33bd8737a522c5dba96aafefb9dcc701bc6a30eb6c0
3
+ metadata.gz: b1cf076eb39a57f9c1cbdff22ff957b6abf2bc24ab245d603a88f4fdc780ab1f
4
+ data.tar.gz: 87a49969a5de07822dfeee8f4d6792dd0260409344519396e108ee45d8c68245
5
5
  SHA512:
6
- metadata.gz: a0c1c3f7a454e6cee7b4512290b8b0cf3de3c1683d1f797f64cc7efc8caa5e1cd6a245a0d96351f151b878b49e57e8a2461c6f120151b0f1bcc660d0f1dcdd0c
7
- data.tar.gz: 0d4eb1114071ca5173b36b73ce1af9c705e23a69d8aff1b45b335b1ba5587ca9fee4825b6dba1f9cacdab5cde01e2c64e77d73bd70950a7f976f64f3e779c583
6
+ metadata.gz: a46003bec7bcaf5c5a4df7731a80b6d4e77b4eca9a9f467dcd061e94db7372192ba8751cf29d333518c49988224172adfab0a05bd16a14bc65fa2d2a5a209d98
7
+ data.tar.gz: ff0760e9a50bfe4f07dafc1c842060693a710de0ff5c3c9c5adfe606e858aee4219ea4e5bbdda600cd0252f89f83f749b619e426a884f0a6260e0c5552ce04bf
@@ -405,6 +405,18 @@ module Tina4
405
405
  nil
406
406
  end
407
407
 
408
+ # Return the normalised engine name for this connection.
409
+ #
410
+ # Cross-framework parity with Python/PHP/Node ``get_database_type()``.
411
+ # ORM.create_table needs this to emit engine-correct DDL (SERIAL vs
412
+ # AUTOINCREMENT, BOOLEAN vs INTEGER, TIMESTAMP vs DATETIME). Returns the
413
+ # resolved driver key ("postgres", "mysql", "mssql", "firebird",
414
+ # "sqlite", ...) — the same alias-normalised value used to pick the
415
+ # driver class, so callers don't have to re-parse the connection string.
416
+ def get_database_type
417
+ @driver_name
418
+ end
419
+
408
420
  # Execute a write statement. Returns true/false for simple writes.
409
421
  # Returns DatabaseResult if SQL contains RETURNING, CALL, EXEC, or SELECT.
410
422
  def execute(sql, params = [])
@@ -489,6 +501,12 @@ module Tina4
489
501
  alias get_columns columns
490
502
 
491
503
  def table_exists?(table_name)
504
+ drv = current_driver
505
+ # v3.13.14 (#48): drivers that can resolve a schema/catalog-qualified
506
+ # name ("gift_cards.gift_card", "dbo.widget", "attached.table") answer
507
+ # directly; the rest fall back to a case-insensitive scan of tables.
508
+ return drv.table_exists?(table_name) if drv.respond_to?(:table_exists?)
509
+
492
510
  tables.any? { |t| t.downcase == table_name.to_s.downcase }
493
511
  end
494
512
 
@@ -39,8 +39,21 @@ module Tina4
39
39
  @records.empty?
40
40
  end
41
41
 
42
- def [](index)
43
- @records[index]
42
+ # Index / slice access into the result rows.
43
+ #
44
+ # ``result[0]`` is documented (book ch5 §4 "Index Access"). Delegating
45
+ # straight to the materialised rows means every Array subscript form
46
+ # works — ``result[0]``, ``result[-1]``, ``result[1, 2]`` and
47
+ # ``result[1..3]`` — and matches Python's ``DatabaseResult.__getitem__``,
48
+ # which forwards to its records list.
49
+ def [](*args)
50
+ @records[*args]
51
+ end
52
+
53
+ # Implicit array coercion, so a DatabaseResult can be splatted and used
54
+ # anywhere an Array is expected (``a, b = result``, ``[*result]``).
55
+ def to_ary
56
+ @records.dup
44
57
  end
45
58
 
46
59
  def length
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "schema_split"
4
+
3
5
  module Tina4
4
6
  module Drivers
5
7
  class MssqlDriver
8
+ include SchemaSplit
6
9
  attr_reader :connection
7
10
 
8
11
  def connect(connection_string, username: nil, password: nil)
@@ -73,14 +76,28 @@ module Tina4
73
76
  @connection.execute("ROLLBACK").do
74
77
  end
75
78
 
79
+ # v3.13.14 (#48): honour a schema-qualified name ("dbo.widget"); a bare
80
+ # name matches in any schema (NULL guard skips the schema filter).
81
+ def table_exists?(name)
82
+ schema, tbl = split_schema(name)
83
+ sql = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES " \
84
+ "WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = ? " \
85
+ "AND (? IS NULL OR TABLE_SCHEMA = ?)"
86
+ rows = execute_query(sql, [tbl, schema, schema])
87
+ !rows.empty?
88
+ end
89
+
76
90
  def tables
77
91
  rows = execute_query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'")
78
92
  rows.map { |r| r[:TABLE_NAME] || r[:table_name] }
79
93
  end
80
94
 
81
95
  def columns(table_name)
82
- sql = "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?"
83
- rows = execute_query(sql, [table_name])
96
+ # v3.13.14 (#48): honour a schema-qualified name; bare names match any schema.
97
+ schema, tbl = split_schema(table_name)
98
+ sql = "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS " \
99
+ "WHERE TABLE_NAME = ? AND (? IS NULL OR TABLE_SCHEMA = ?)"
100
+ rows = execute_query(sql, [tbl, schema, schema])
84
101
  rows.map do |r|
85
102
  {
86
103
  name: r[:COLUMN_NAME] || r[:column_name],
@@ -109,7 +126,14 @@ module Tina4
109
126
  return sql if params.empty?
110
127
  result = sql.dup
111
128
  params.each do |param|
112
- escaped = param.is_a?(String) ? "'#{param.gsub("'", "''")}'" : param.to_s
129
+ escaped =
130
+ if param.nil?
131
+ "NULL"
132
+ elsif param.is_a?(String)
133
+ "'#{param.gsub("'", "''")}'"
134
+ else
135
+ param.to_s
136
+ end
113
137
  result = result.sub("?", escaped)
114
138
  end
115
139
  result
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "schema_split"
4
+
3
5
  module Tina4
4
6
  module Drivers
5
7
  class MysqlDriver
8
+ include SchemaSplit
6
9
  attr_reader :connection
7
10
 
8
11
  def connect(connection_string, username: nil, password: nil)
@@ -75,6 +78,19 @@ module Tina4
75
78
  @connection.query("ROLLBACK")
76
79
  end
77
80
 
81
+ # v3.13.14 (#48): MySQL's "schema" is the database. A qualified name
82
+ # ("otherdb.table") checks that catalog; a bare name defaults to the
83
+ # connection's current database via DATABASE().
84
+ def table_exists?(name)
85
+ schema, tbl = split_schema(name)
86
+ rows = execute_query(
87
+ "SELECT 1 FROM information_schema.tables " \
88
+ "WHERE table_schema = COALESCE(?, DATABASE()) AND table_name = ?",
89
+ [schema, tbl]
90
+ )
91
+ !rows.empty?
92
+ end
93
+
78
94
  def tables
79
95
  rows = execute_query("SHOW TABLES")
80
96
  rows.map { |r| r.values.first }
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "schema_split"
4
+
3
5
  module Tina4
4
6
  module Drivers
5
7
  class PostgresDriver
8
+ include SchemaSplit
6
9
  attr_reader :connection
7
10
 
8
11
  def connect(connection_string, username: nil, password: nil)
@@ -112,15 +115,29 @@ module Tina4
112
115
  @connection.exec("ROLLBACK")
113
116
  end
114
117
 
118
+ # v3.13.14 (#48): to_regclass resolves a (possibly schema-qualified)
119
+ # relation name and search_path like a FROM clause; nil if absent.
120
+ def table_exists?(name)
121
+ rows = execute_query("SELECT to_regclass($1) AS oid", [name.to_s])
122
+ !rows.empty? && !rows[0][:oid].nil?
123
+ end
124
+
115
125
  def tables
116
- sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
126
+ # v3.13.14 (#48): list every user schema; public tables stay bare,
127
+ # others are returned schema-qualified.
128
+ sql = "SELECT schemaname, tablename FROM pg_tables " \
129
+ "WHERE schemaname NOT IN ('pg_catalog', 'information_schema') " \
130
+ "ORDER BY schemaname, tablename"
117
131
  rows = execute_query(sql)
118
- rows.map { |r| r[:tablename] }
132
+ rows.map { |r| r[:schemaname] == "public" ? r[:tablename] : "#{r[:schemaname]}.#{r[:tablename]}" }
119
133
  end
120
134
 
121
135
  def columns(table_name)
122
- sql = "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1"
123
- rows = execute_query(sql, [table_name])
136
+ # v3.13.14 (#48): honour a schema-qualified name; default to public.
137
+ schema, tbl = split_schema(table_name)
138
+ schema ||= "public"
139
+ sql = "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1 AND table_schema = $2"
140
+ rows = execute_query(sql, [tbl, schema])
124
141
  rows.map do |r|
125
142
  {
126
143
  name: r[:column_name],
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tina4
4
+ module Drivers
5
+ # v3.13.14 (#48): split a possibly-qualified table name into [schema, table].
6
+ #
7
+ # A model whose table name is qualified — PostgreSQL "gift_cards.gift_card",
8
+ # MSSQL "dbo.widget", MySQL "otherdb.table", SQLite "attached.table" — lives
9
+ # in that schema/catalog, not the default. Drivers use this so table_exists?
10
+ # / columns query the right namespace instead of matching the whole dotted
11
+ # string as one flat name. Returns [nil, name] for a bare name. Splits on the
12
+ # first dot. Firebird has no schemas, so its driver ignores this.
13
+ module SchemaSplit
14
+ def split_schema(name)
15
+ str = name.to_s
16
+ idx = str.index(".")
17
+ return [nil, str] if idx.nil?
18
+
19
+ [str[0...idx], str[(idx + 1)..]]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "schema_split"
4
+
3
5
  module Tina4
4
6
  module Drivers
5
7
  class SqliteDriver
8
+ include SchemaSplit
6
9
  attr_reader :connection
7
10
 
8
11
  def connect(connection_string, username: nil, password: nil)
@@ -92,13 +95,26 @@ module Tina4
92
95
  @connection.execute("ROLLBACK")
93
96
  end
94
97
 
98
+ # v3.13.14 (#48): a SQLite "schema" is an ATTACH alias ("extra.widget").
99
+ # Query that database's own sqlite_master when the prefix is a plain
100
+ # identifier; otherwise treat the whole string as a bare table name.
101
+ def table_exists?(name)
102
+ schema, tbl = split_schema(name)
103
+ master = schema && identifier?(schema) ? "#{schema}.sqlite_master" : "sqlite_master"
104
+ rows = execute_query("SELECT 1 FROM #{master} WHERE type='table' AND name=?", [tbl])
105
+ !rows.empty?
106
+ end
107
+
95
108
  def tables
96
109
  rows = execute_query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
97
110
  rows.map { |r| r[:name] }
98
111
  end
99
112
 
100
113
  def columns(table_name)
101
- rows = execute_query("PRAGMA table_info(#{table_name})")
114
+ # v3.13.14 (#48): PRAGMA accepts an attached-schema prefix.
115
+ schema, tbl = split_schema(table_name)
116
+ pragma = schema && identifier?(schema) && identifier?(tbl) ? "#{schema}.table_info(#{tbl})" : "table_info(#{table_name})"
117
+ rows = execute_query("PRAGMA #{pragma}")
102
118
  rows.map do |r|
103
119
  {
104
120
  name: r[:name],
@@ -112,6 +128,11 @@ module Tina4
112
128
 
113
129
  private
114
130
 
131
+ # A safe-to-interpolate SQL identifier (no quoting/escaping needed).
132
+ def identifier?(str)
133
+ str.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
134
+ end
135
+
115
136
  def symbolize_keys(hash)
116
137
  hash.each_with_object({}) do |(k, v), h|
117
138
  h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
data/lib/tina4/log.rb CHANGED
@@ -80,6 +80,13 @@ module Tina4
80
80
  @current_context = {}
81
81
  @mutex = Mutex.new
82
82
 
83
+ # v3.13.14: unbuffer stdout so logs reach `docker logs` / k8s
84
+ # immediately. A non-TTY $stdout (every container) is block-buffered
85
+ # by default — logs sat in the buffer until it filled or the process
86
+ # exited, so operators "weren't getting logs". No-op when output is
87
+ # file-only.
88
+ $stdout.sync = true if @output != "file"
89
+
83
90
  # Build the file logger via stdlib Logger which handles rotation natively.
84
91
  # Logger.new(path, shift_age, shift_size):
85
92
  # shift_age = number of files to keep
@@ -175,8 +182,15 @@ module Tina4
175
182
  end
176
183
 
177
184
  def resolve_level
178
- env_level = ENV["TINA4_LOG_LEVEL"] || "[TINA4_LOG_ALL]"
179
- LEVELS[env_level] || 0
185
+ # v3.13.14: default is INFO (was ALL) so a deployed app surfaces
186
+ # request/startup/warn/error without debug noise, matching
187
+ # Python/PHP/Node. Accept BOTH the legacy bracket form
188
+ # ("[TINA4_LOG_ERROR]") AND plain names ("ERROR") so the env value
189
+ # is portable across all four frameworks.
190
+ raw = (ENV["TINA4_LOG_LEVEL"] || "").strip
191
+ return 1 if raw.empty? # INFO
192
+ key = raw.start_with?("[") ? raw.upcase : "[TINA4_LOG_#{raw.upcase}]"
193
+ LEVELS[key] || 1
180
194
  end
181
195
 
182
196
  def severity_to_level(level)
@@ -295,7 +295,9 @@ module Tina4
295
295
  elapsed_ms = 0.0
296
296
  end
297
297
 
298
- Tina4::Log.info("[RequestLogger] #{request.method} #{request.path} -> #{response.status_code} (#{elapsed_ms}ms)")
298
+ # v3.13.14: dropped the "[RequestLogger]" prefix for format parity
299
+ # with Python/PHP/Node — the line is just METHOD PATH -> STATUS (Nms).
300
+ Tina4::Log.info("#{request.method} #{request.path} -> #{response.status_code} (#{elapsed_ms}ms)")
299
301
  [request, response]
300
302
  end
301
303
 
data/lib/tina4/orm.rb CHANGED
@@ -306,13 +306,29 @@ module Tina4
306
306
  def create_table
307
307
  return true if db.table_exists?(table_name)
308
308
 
309
- # v3.13.11 (BooleanField parity): pick each engine's native
310
- # bool type where it's reliable. SQLite has no native bool;
311
- # Firebird's driver round-trip for native BOOLEAN is uneven
312
- # both stay on INTEGER. PG/MySQL/MSSQL use their native types
313
- # so Python ``True``/Ruby ``true`` bind cleanly without
314
- # ``operator does not exist: boolean = integer`` errors.
309
+ # v3.13.16: engine-aware DDL. Ruby used to emit SQLite-only DDL on
310
+ # every driver INTEGER for booleans, DATETIME for datetimes, and a
311
+ # raw AUTOINCREMENT keyword then ignore db.execute()'s return value
312
+ # and report success. On PostgreSQL the CREATE blew up
313
+ # ("syntax error at or near AUTOINCREMENT"), db.execute() swallowed it
314
+ # into get_error() and returned false, yet create_table still returned
315
+ # true with no table created — a silent, misleading pass.
316
+ #
317
+ # The fix mirrors the Python reference (tina4_python.orm.model):
318
+ # • get_database_type() now exists on Database (it didn't before, so
319
+ # the v3.13.11 BooleanField check never actually fired on Ruby).
320
+ # • BooleanField → native BOOLEAN (PG/MySQL) / BIT (MSSQL) /
321
+ # INTEGER (sqlite, firebird) — both PG aliases are matched.
322
+ # • DateTimeField → TIMESTAMP on PG/Firebird (neither has a DATETIME
323
+ # type), DATETIME elsewhere.
324
+ # • boolean DEFAULT is engine-aware: TRUE/FALSE for a native BOOLEAN,
325
+ # 1/0 for INTEGER/BIT-backed bools.
326
+ # • AUTOINCREMENT is translated per engine via SQLTranslator
327
+ # (SERIAL on PG, AUTO_INCREMENT on MySQL, IDENTITY on MSSQL, dropped
328
+ # on Firebird) instead of being emitted raw.
329
+ # • return false (not true) when the DDL fails.
315
330
  engine = (db.respond_to?(:get_database_type) ? db.get_database_type : "").to_s.downcase
331
+
316
332
  bool_sql = case engine
317
333
  when "postgres", "postgresql" then "BOOLEAN"
318
334
  when "mysql" then "BOOLEAN" # alias for TINYINT(1)
@@ -320,6 +336,15 @@ module Tina4
320
336
  else "INTEGER" # sqlite, firebird, odbc, anything else
321
337
  end
322
338
 
339
+ # PostgreSQL and Firebird have no DATETIME type — CREATE TABLE fails
340
+ # with `type "datetime" does not exist`. Emit each engine's real
341
+ # timestamp type. (MySQL/MSSQL/SQLite keep DATETIME: valid there, and
342
+ # on MySQL it avoids TIMESTAMP's auto-update + 2038 surprises.)
343
+ datetime_sql = case engine
344
+ when "postgres", "postgresql", "firebird" then "TIMESTAMP"
345
+ else "DATETIME"
346
+ end
347
+
323
348
  type_map = {
324
349
  integer: "INTEGER",
325
350
  string: "VARCHAR(255)",
@@ -328,7 +353,7 @@ module Tina4
328
353
  decimal: "REAL",
329
354
  boolean: bool_sql,
330
355
  date: "DATE",
331
- datetime: "DATETIME",
356
+ datetime: datetime_sql,
332
357
  timestamp: "TIMESTAMP",
333
358
  blob: "BLOB",
334
359
  json: "TEXT"
@@ -346,15 +371,29 @@ module Tina4
346
371
  parts << "AUTOINCREMENT" if opts[:auto_increment]
347
372
  parts << "NOT NULL" if !opts[:nullable] && !opts[:primary_key]
348
373
  if opts[:default] && !opts[:auto_increment]
349
- default_val = opts[:default].is_a?(String) ? "'#{opts[:default]}'" : opts[:default]
350
- parts << "DEFAULT #{default_val}"
374
+ parts << "DEFAULT #{default_literal(opts[:default], opts[:type], bool_sql)}"
351
375
  end
352
376
  col_defs << parts.join(" ")
353
377
  end
354
378
 
355
379
  sql = "CREATE TABLE IF NOT EXISTS #{table_name} (#{col_defs.join(', ')})"
356
- db.execute(sql)
380
+
381
+ # Translate AUTOINCREMENT to the engine's auto-increment syntax
382
+ # (INTEGER PRIMARY KEY AUTOINCREMENT -> SERIAL PRIMARY KEY on PG, etc.).
383
+ # SQLTranslator keys off the -ql spelling for postgres.
384
+ translator_engine = %w[postgres postgresql].include?(engine) ? "postgresql" : engine
385
+ sql = SQLTranslator.auto_increment_syntax(sql, translator_engine)
386
+
387
+ # Don't claim success when the DDL failed. db.execute() swallows the
388
+ # driver error into get_error() and returns false, so a bad type (or
389
+ # any DDL error) used to leave create_table returning true while no
390
+ # table was actually created.
391
+ ok = db.execute(sql)
357
392
  db.commit
393
+ if ok == false
394
+ Tina4::Log.error("create_table failed for #{table_name}: #{db.get_error}", { sql: sql })
395
+ return false
396
+ end
358
397
  true
359
398
  end
360
399
 
@@ -427,6 +466,28 @@ module Tina4
427
466
  results = db.fetch(sql, filter.values)
428
467
  results.map { |row| from_hash(row) }
429
468
  end
469
+
470
+ # Render a column DEFAULT literal for create_table.
471
+ #
472
+ # Strings are quoted. Booleans are engine-aware: a native BOOLEAN
473
+ # column (PG/MySQL) needs TRUE/FALSE, while INTEGER- and BIT-backed
474
+ # bools (SQLite, Firebird, MSSQL) need 1/0 — `DEFAULT 0` on a PG
475
+ # BOOLEAN raises "default expression is of type integer". Everything
476
+ # else is emitted as-is.
477
+ def default_literal(value, type, bool_sql)
478
+ if value.is_a?(String)
479
+ "'#{value}'"
480
+ elsif value == true || value == false || type == :boolean
481
+ truthy = value == true || value == 1 || value == "1"
482
+ if bool_sql == "BOOLEAN"
483
+ truthy ? "TRUE" : "FALSE"
484
+ else
485
+ truthy ? "1" : "0"
486
+ end
487
+ else
488
+ value.to_s
489
+ end
490
+ end
430
491
  end
431
492
 
432
493
  def initialize(attributes = {})
@@ -163,6 +163,16 @@ module Tina4
163
163
  )
164
164
  end
165
165
 
166
+ # Request log line (v3.13.14). The dev inspector above only feeds the
167
+ # /__dev UI — it never reached stdout, so `tina4ruby serve` printed the
168
+ # banner then went silent. Emit a per-request line through Tina4::Log so
169
+ # it lands on stdout (docker logs / k8s). On by default in dev, opt-in in
170
+ # production via TINA4_LOG_REQUESTS. Same format across all four frameworks.
171
+ if request_logging_enabled? && !path.start_with?("/__dev")
172
+ log_elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - request_start) * 1000).round(3)
173
+ Tina4::Log.info("#{method} #{path} -> #{rack_response[0]} (#{log_elapsed}ms)")
174
+ end
175
+
166
176
  # Inject dev overlay button for HTML responses in dev mode
167
177
  if dev_mode? && !path.start_with?("/__dev")
168
178
  status, headers, body_parts = rack_response
@@ -816,6 +826,17 @@ module Tina4
816
826
  Tina4::Env.is_truthy(ENV["TINA4_DEBUG"])
817
827
  end
818
828
 
829
+ # Whether to emit a per-request log line (v3.13.14). TINA4_LOG_REQUESTS
830
+ # is the explicit control (true/false); when unset, request logging
831
+ # follows dev mode (on under TINA4_DEBUG, off in production). Same
832
+ # contract across all four frameworks.
833
+ def request_logging_enabled?
834
+ val = ENV["TINA4_LOG_REQUESTS"]
835
+ return Tina4::Env.is_truthy(val) if val && !val.empty?
836
+
837
+ dev_mode?
838
+ end
839
+
819
840
  def websocket_upgrade?(env)
820
841
  upgrade = env["HTTP_UPGRADE"] || ""
821
842
  upgrade.downcase == "websocket"
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.12"
4
+ VERSION = "3.13.16"
5
5
  end
data/lib/tina4.rb CHANGED
@@ -62,6 +62,7 @@ require_relative "tina4/mcp"
62
62
  module Tina4
63
63
  # ── Lazy-loaded: database drivers ─────────────────────────────────────
64
64
  module Drivers
65
+ autoload :SchemaSplit, File.expand_path("tina4/drivers/schema_split", __dir__)
65
66
  autoload :SqliteDriver, File.expand_path("tina4/drivers/sqlite_driver", __dir__)
66
67
  autoload :PostgresDriver, File.expand_path("tina4/drivers/postgres_driver", __dir__)
67
68
  autoload :MysqlDriver, File.expand_path("tina4/drivers/mysql_driver", __dir__)
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.12
4
+ version: 3.13.16
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-06-11 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -302,6 +302,7 @@ files:
302
302
  - lib/tina4/drivers/mysql_driver.rb
303
303
  - lib/tina4/drivers/odbc_driver.rb
304
304
  - lib/tina4/drivers/postgres_driver.rb
305
+ - lib/tina4/drivers/schema_split.rb
305
306
  - lib/tina4/drivers/sqlite_driver.rb
306
307
  - lib/tina4/env.rb
307
308
  - lib/tina4/error_overlay.rb