tina4ruby 3.13.12 → 3.13.14
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.rb +6 -0
- data/lib/tina4/drivers/mssql_driver.rb +27 -3
- data/lib/tina4/drivers/mysql_driver.rb +16 -0
- data/lib/tina4/drivers/postgres_driver.rb +21 -4
- data/lib/tina4/drivers/schema_split.rb +23 -0
- data/lib/tina4/drivers/sqlite_driver.rb +22 -1
- data/lib/tina4/log.rb +16 -2
- data/lib/tina4/middleware.rb +3 -1
- data/lib/tina4/rack_app.rb +21 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c7aa0bfb2393540123bc96f0251850236854b27b8cb64afbe3a57dc7036c952
|
|
4
|
+
data.tar.gz: 675898e92f1d9fa832703afca63d833a2d895a37ce2d981963f24fe4a12ef451
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51d150d7e359a5ec89e3daaf02b4d77b41c632fe8e57ad838fcebe3a517607aea026e4d1faa575c6b1b277631c1fa21e4c43c927c49de2b2859f79b1e9b04c46
|
|
7
|
+
data.tar.gz: 5b002d55c8f5b822677b443ccffee40a88b5b46fe76bdd34df2e2ec3d2591f52010b93afd5ef6504cc22e55ce5d883726f80dba992a3886884a5f3a8e6acad42
|
data/lib/tina4/database.rb
CHANGED
|
@@ -489,6 +489,12 @@ module Tina4
|
|
|
489
489
|
alias get_columns columns
|
|
490
490
|
|
|
491
491
|
def table_exists?(table_name)
|
|
492
|
+
drv = current_driver
|
|
493
|
+
# v3.13.14 (#48): drivers that can resolve a schema/catalog-qualified
|
|
494
|
+
# name ("gift_cards.gift_card", "dbo.widget", "attached.table") answer
|
|
495
|
+
# directly; the rest fall back to a case-insensitive scan of tables.
|
|
496
|
+
return drv.table_exists?(table_name) if drv.respond_to?(:table_exists?)
|
|
497
|
+
|
|
492
498
|
tables.any? { |t| t.downcase == table_name.to_s.downcase }
|
|
493
499
|
end
|
|
494
500
|
|
|
@@ -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
|
-
|
|
83
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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)
|
data/lib/tina4/middleware.rb
CHANGED
|
@@ -295,7 +295,9 @@ module Tina4
|
|
|
295
295
|
elapsed_ms = 0.0
|
|
296
296
|
end
|
|
297
297
|
|
|
298
|
-
|
|
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/rack_app.rb
CHANGED
|
@@ -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
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.
|
|
4
|
+
version: 3.13.14
|
|
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
|
+
date: 2026-06-13 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
|