tina4ruby 3.11.13 → 3.11.15
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/CHANGELOG.md +80 -80
- data/LICENSE.txt +21 -21
- data/README.md +137 -137
- data/exe/tina4ruby +5 -5
- data/lib/tina4/ai.rb +696 -696
- data/lib/tina4/api.rb +189 -189
- data/lib/tina4/auth.rb +305 -305
- data/lib/tina4/auto_crud.rb +244 -244
- data/lib/tina4/cache.rb +154 -154
- data/lib/tina4/cli.rb +1449 -1449
- data/lib/tina4/constants.rb +46 -46
- data/lib/tina4/container.rb +74 -74
- data/lib/tina4/cors.rb +74 -74
- data/lib/tina4/crud.rb +692 -692
- data/lib/tina4/database/sqlite3_adapter.rb +165 -165
- data/lib/tina4/database.rb +625 -625
- data/lib/tina4/database_result.rb +208 -208
- data/lib/tina4/debug.rb +8 -8
- data/lib/tina4/dev.rb +14 -14
- data/lib/tina4/dev_admin.rb +935 -935
- data/lib/tina4/dev_mailbox.rb +191 -191
- data/lib/tina4/drivers/firebird_driver.rb +124 -110
- data/lib/tina4/drivers/mongodb_driver.rb +561 -561
- data/lib/tina4/drivers/mssql_driver.rb +112 -112
- data/lib/tina4/drivers/mysql_driver.rb +90 -90
- data/lib/tina4/drivers/odbc_driver.rb +191 -191
- data/lib/tina4/drivers/postgres_driver.rb +116 -106
- data/lib/tina4/drivers/sqlite_driver.rb +122 -122
- data/lib/tina4/env.rb +95 -95
- data/lib/tina4/error_overlay.rb +252 -252
- data/lib/tina4/events.rb +109 -109
- data/lib/tina4/field_types.rb +154 -154
- data/lib/tina4/frond.rb +2025 -2025
- data/lib/tina4/gallery/auth/meta.json +1 -1
- data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
- data/lib/tina4/gallery/database/meta.json +1 -1
- data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
- data/lib/tina4/gallery/error-overlay/meta.json +1 -1
- data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
- data/lib/tina4/gallery/orm/meta.json +1 -1
- data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
- data/lib/tina4/gallery/rest-api/meta.json +1 -1
- data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
- data/lib/tina4/gallery/templates/meta.json +1 -1
- data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
- data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
- data/lib/tina4/graphql.rb +966 -966
- data/lib/tina4/health.rb +39 -39
- data/lib/tina4/html_element.rb +170 -170
- data/lib/tina4/job.rb +80 -80
- data/lib/tina4/localization.rb +168 -168
- data/lib/tina4/log.rb +203 -203
- data/lib/tina4/mcp.rb +696 -696
- data/lib/tina4/messenger.rb +587 -587
- data/lib/tina4/metrics.rb +793 -793
- data/lib/tina4/middleware.rb +445 -445
- data/lib/tina4/migration.rb +451 -451
- data/lib/tina4/orm.rb +790 -790
- data/lib/tina4/public/css/tina4.css +2463 -2463
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/public/images/logo.svg +5 -5
- data/lib/tina4/public/js/frond.min.js +2 -2
- data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
- data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
- data/lib/tina4/public/js/tina4.min.js +92 -92
- data/lib/tina4/public/js/tina4js.min.js +48 -48
- data/lib/tina4/public/swagger/index.html +90 -90
- data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
- data/lib/tina4/query_builder.rb +380 -380
- data/lib/tina4/queue.rb +366 -366
- data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
- data/lib/tina4/queue_backends/lite_backend.rb +298 -298
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
- data/lib/tina4/rack_app.rb +817 -817
- data/lib/tina4/rate_limiter.rb +130 -130
- data/lib/tina4/request.rb +268 -255
- data/lib/tina4/response.rb +346 -346
- data/lib/tina4/response_cache.rb +551 -551
- data/lib/tina4/router.rb +406 -406
- data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
- data/lib/tina4/scss/tina4css/_badges.scss +22 -22
- data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
- data/lib/tina4/scss/tina4css/_cards.scss +49 -49
- data/lib/tina4/scss/tina4css/_forms.scss +156 -156
- data/lib/tina4/scss/tina4css/_grid.scss +81 -81
- data/lib/tina4/scss/tina4css/_modals.scss +84 -84
- data/lib/tina4/scss/tina4css/_nav.scss +149 -149
- data/lib/tina4/scss/tina4css/_reset.scss +94 -94
- data/lib/tina4/scss/tina4css/_tables.scss +54 -54
- data/lib/tina4/scss/tina4css/_typography.scss +55 -55
- data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
- data/lib/tina4/scss/tina4css/_variables.scss +117 -117
- data/lib/tina4/scss/tina4css/base.scss +1 -1
- data/lib/tina4/scss/tina4css/colors.scss +48 -48
- data/lib/tina4/scss/tina4css/tina4.scss +17 -17
- data/lib/tina4/scss_compiler.rb +178 -178
- data/lib/tina4/seeder.rb +567 -567
- data/lib/tina4/service_runner.rb +303 -303
- data/lib/tina4/session.rb +297 -297
- data/lib/tina4/session_handlers/database_handler.rb +72 -72
- data/lib/tina4/session_handlers/file_handler.rb +67 -67
- data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
- data/lib/tina4/session_handlers/redis_handler.rb +43 -43
- data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
- data/lib/tina4/shutdown.rb +84 -84
- data/lib/tina4/sql_translation.rb +158 -158
- data/lib/tina4/swagger.rb +124 -124
- data/lib/tina4/template.rb +894 -894
- data/lib/tina4/templates/base.twig +26 -26
- data/lib/tina4/templates/errors/302.twig +14 -14
- data/lib/tina4/templates/errors/401.twig +9 -9
- data/lib/tina4/templates/errors/403.twig +29 -29
- data/lib/tina4/templates/errors/404.twig +29 -29
- data/lib/tina4/templates/errors/500.twig +38 -38
- data/lib/tina4/templates/errors/502.twig +9 -9
- data/lib/tina4/templates/errors/503.twig +12 -12
- data/lib/tina4/templates/errors/base.twig +37 -37
- data/lib/tina4/test_client.rb +159 -159
- data/lib/tina4/testing.rb +340 -340
- data/lib/tina4/validator.rb +174 -174
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +312 -312
- data/lib/tina4/websocket.rb +343 -343
- data/lib/tina4/websocket_backplane.rb +190 -190
- data/lib/tina4/wsdl.rb +564 -564
- data/lib/tina4.rb +458 -458
- data/lib/tina4ruby.rb +4 -4
- metadata +3 -3
data/lib/tina4/shutdown.rb
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Shutdown
|
|
5
|
-
DEFAULT_TIMEOUT = 30 # seconds
|
|
6
|
-
|
|
7
|
-
class << self
|
|
8
|
-
attr_reader :in_flight_count
|
|
9
|
-
|
|
10
|
-
def setup(server: nil, timeout: nil)
|
|
11
|
-
@server = server
|
|
12
|
-
@timeout = (timeout || ENV["TINA4_SHUTDOWN_TIMEOUT"] || DEFAULT_TIMEOUT).to_i
|
|
13
|
-
@shutting_down = false
|
|
14
|
-
@mutex = Mutex.new
|
|
15
|
-
@in_flight_count = 0
|
|
16
|
-
@in_flight_cv = ConditionVariable.new
|
|
17
|
-
|
|
18
|
-
install_signal_handlers
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def shutting_down?
|
|
22
|
-
@shutting_down
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def track_request
|
|
26
|
-
@mutex.synchronize { @in_flight_count += 1 }
|
|
27
|
-
begin
|
|
28
|
-
yield
|
|
29
|
-
ensure
|
|
30
|
-
@mutex.synchronize do
|
|
31
|
-
@in_flight_count -= 1
|
|
32
|
-
@in_flight_cv.broadcast if @in_flight_count <= 0
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def initiate_shutdown
|
|
38
|
-
return if @shutting_down
|
|
39
|
-
|
|
40
|
-
@shutting_down = true
|
|
41
|
-
Tina4::Log.info("Shutdown signal received, stopping gracefully...")
|
|
42
|
-
|
|
43
|
-
# Wait for in-flight requests with timeout
|
|
44
|
-
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
45
|
-
@mutex.synchronize do
|
|
46
|
-
while @in_flight_count > 0
|
|
47
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
|
-
if remaining <= 0
|
|
49
|
-
Tina4::Log.warning("Shutdown timeout reached with #{@in_flight_count} requests still in flight")
|
|
50
|
-
break
|
|
51
|
-
end
|
|
52
|
-
@in_flight_cv.wait(@mutex, remaining)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Close database connections
|
|
57
|
-
if Tina4.database
|
|
58
|
-
begin
|
|
59
|
-
Tina4.database.close
|
|
60
|
-
Tina4::Log.info("Database connections closed")
|
|
61
|
-
rescue => e
|
|
62
|
-
Tina4::Log.error("Error closing database: #{e.message}")
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
Tina4::Log.info("Shutdown complete")
|
|
67
|
-
|
|
68
|
-
# Stop the server
|
|
69
|
-
@server&.shutdown if @server.respond_to?(:shutdown)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
def install_signal_handlers
|
|
75
|
-
%w[INT TERM].each do |signal|
|
|
76
|
-
Signal.trap(signal) do
|
|
77
|
-
# Signal handlers must be async-signal-safe; use Thread to do real work
|
|
78
|
-
Thread.new { initiate_shutdown }
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Shutdown
|
|
5
|
+
DEFAULT_TIMEOUT = 30 # seconds
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
attr_reader :in_flight_count
|
|
9
|
+
|
|
10
|
+
def setup(server: nil, timeout: nil)
|
|
11
|
+
@server = server
|
|
12
|
+
@timeout = (timeout || ENV["TINA4_SHUTDOWN_TIMEOUT"] || DEFAULT_TIMEOUT).to_i
|
|
13
|
+
@shutting_down = false
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@in_flight_count = 0
|
|
16
|
+
@in_flight_cv = ConditionVariable.new
|
|
17
|
+
|
|
18
|
+
install_signal_handlers
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def shutting_down?
|
|
22
|
+
@shutting_down
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def track_request
|
|
26
|
+
@mutex.synchronize { @in_flight_count += 1 }
|
|
27
|
+
begin
|
|
28
|
+
yield
|
|
29
|
+
ensure
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@in_flight_count -= 1
|
|
32
|
+
@in_flight_cv.broadcast if @in_flight_count <= 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initiate_shutdown
|
|
38
|
+
return if @shutting_down
|
|
39
|
+
|
|
40
|
+
@shutting_down = true
|
|
41
|
+
Tina4::Log.info("Shutdown signal received, stopping gracefully...")
|
|
42
|
+
|
|
43
|
+
# Wait for in-flight requests with timeout
|
|
44
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
while @in_flight_count > 0
|
|
47
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
|
+
if remaining <= 0
|
|
49
|
+
Tina4::Log.warning("Shutdown timeout reached with #{@in_flight_count} requests still in flight")
|
|
50
|
+
break
|
|
51
|
+
end
|
|
52
|
+
@in_flight_cv.wait(@mutex, remaining)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Close database connections
|
|
57
|
+
if Tina4.database
|
|
58
|
+
begin
|
|
59
|
+
Tina4.database.close
|
|
60
|
+
Tina4::Log.info("Database connections closed")
|
|
61
|
+
rescue => e
|
|
62
|
+
Tina4::Log.error("Error closing database: #{e.message}")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Tina4::Log.info("Shutdown complete")
|
|
67
|
+
|
|
68
|
+
# Stop the server
|
|
69
|
+
@server&.shutdown if @server.respond_to?(:shutdown)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def install_signal_handlers
|
|
75
|
+
%w[INT TERM].each do |signal|
|
|
76
|
+
Signal.trap(signal) do
|
|
77
|
+
# Signal handlers must be async-signal-safe; use Thread to do real work
|
|
78
|
+
Thread.new { initiate_shutdown }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require_relative "cache"
|
|
5
|
-
|
|
6
|
-
module Tina4
|
|
7
|
-
# Cross-engine SQL translator.
|
|
8
|
-
#
|
|
9
|
-
# Each database adapter calls the rules it needs. Rules are composable
|
|
10
|
-
# and stateless -- just string transforms.
|
|
11
|
-
#
|
|
12
|
-
# Also includes query caching with TTL support.
|
|
13
|
-
#
|
|
14
|
-
# Usage:
|
|
15
|
-
# translated = Tina4::SQLTranslator.limit_to_rows("SELECT * FROM users LIMIT 10 OFFSET 5")
|
|
16
|
-
# # => "SELECT * FROM users ROWS 6 TO 15"
|
|
17
|
-
#
|
|
18
|
-
class SQLTranslator
|
|
19
|
-
class << self
|
|
20
|
-
# Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
21
|
-
#
|
|
22
|
-
# LIMIT 10 OFFSET 5 => ROWS 6 TO 15
|
|
23
|
-
# LIMIT 10 => ROWS 1 TO 10
|
|
24
|
-
#
|
|
25
|
-
# @param sql [String]
|
|
26
|
-
# @return [String]
|
|
27
|
-
def limit_to_rows(sql)
|
|
28
|
-
# Try LIMIT X OFFSET Y first
|
|
29
|
-
if (m = sql.match(/\bLIMIT\s+(\d+)\s+OFFSET\s+(\d+)\s*$/i))
|
|
30
|
-
limit = m[1].to_i
|
|
31
|
-
offset = m[2].to_i
|
|
32
|
-
start_row = offset + 1
|
|
33
|
-
end_row = offset + limit
|
|
34
|
-
return sql[0...m.begin(0)] + "ROWS #{start_row} TO #{end_row}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Then try LIMIT X only
|
|
38
|
-
if (m = sql.match(/\bLIMIT\s+(\d+)\s*$/i))
|
|
39
|
-
limit = m[1].to_i
|
|
40
|
-
return sql[0...m.begin(0)] + "ROWS 1 TO #{limit}"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
sql
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Convert LIMIT to MSSQL TOP syntax.
|
|
47
|
-
#
|
|
48
|
-
# SELECT ... LIMIT 10 => SELECT TOP 10 ...
|
|
49
|
-
# OFFSET queries are left unchanged (not supported by TOP).
|
|
50
|
-
#
|
|
51
|
-
# @param sql [String]
|
|
52
|
-
# @return [String]
|
|
53
|
-
def limit_to_top(sql)
|
|
54
|
-
if (m = sql.match(/\bLIMIT\s+(\d+)\s*$/i)) && !sql.match?(/\bOFFSET\b/i)
|
|
55
|
-
limit = m[1].to_i
|
|
56
|
-
body = sql[0...m.begin(0)].strip
|
|
57
|
-
return body.sub(/^(SELECT)\b/i, "\\1 TOP #{limit}")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
sql
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Convert || concatenation to CONCAT() for MySQL/MSSQL.
|
|
64
|
-
#
|
|
65
|
-
# 'a' || 'b' || 'c' => CONCAT('a', 'b', 'c')
|
|
66
|
-
#
|
|
67
|
-
# @param sql [String]
|
|
68
|
-
# @return [String]
|
|
69
|
-
def concat_pipes_to_func(sql)
|
|
70
|
-
return sql unless sql.include?("||")
|
|
71
|
-
|
|
72
|
-
parts = sql.split("||")
|
|
73
|
-
if parts.length > 1
|
|
74
|
-
"CONCAT(#{parts.map(&:strip).join(', ')})"
|
|
75
|
-
else
|
|
76
|
-
sql
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Convert TRUE/FALSE to 1/0 for engines without boolean type.
|
|
81
|
-
#
|
|
82
|
-
# @param sql [String]
|
|
83
|
-
# @return [String]
|
|
84
|
-
def boolean_to_int(sql)
|
|
85
|
-
sql.gsub(/\bTRUE\b/i, "1").gsub(/\bFALSE\b/i, "0")
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Convert ILIKE to LOWER() LIKE LOWER() for engines without ILIKE.
|
|
89
|
-
#
|
|
90
|
-
# @param sql [String]
|
|
91
|
-
# @return [String]
|
|
92
|
-
def ilike_to_like(sql)
|
|
93
|
-
sql.gsub(/(\S+)\s+ILIKE\s+(\S+)/i) do
|
|
94
|
-
col = ::Regexp.last_match(1).strip
|
|
95
|
-
val = ::Regexp.last_match(2).strip
|
|
96
|
-
"LOWER(#{col}) LIKE LOWER(#{val})"
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# Translate AUTOINCREMENT across engines in DDL.
|
|
101
|
-
#
|
|
102
|
-
# @param sql [String]
|
|
103
|
-
# @param engine [String] one of: mysql, postgresql, mssql, firebird, sqlite
|
|
104
|
-
# @return [String]
|
|
105
|
-
def auto_increment_syntax(sql, engine)
|
|
106
|
-
case engine
|
|
107
|
-
when "mysql"
|
|
108
|
-
sql.gsub("AUTOINCREMENT", "AUTO_INCREMENT")
|
|
109
|
-
when "postgresql"
|
|
110
|
-
sql.gsub(/INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT/i, "SERIAL PRIMARY KEY")
|
|
111
|
-
when "mssql"
|
|
112
|
-
sql.gsub(/AUTOINCREMENT/i, "IDENTITY(1,1)")
|
|
113
|
-
when "firebird"
|
|
114
|
-
sql.gsub(/\s*AUTOINCREMENT\b/i, "")
|
|
115
|
-
else
|
|
116
|
-
sql
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Convert ? placeholders to engine-specific style.
|
|
121
|
-
#
|
|
122
|
-
# ? => %s (MySQL, PostgreSQL)
|
|
123
|
-
# ? => :1, :2 (Oracle, Firebird)
|
|
124
|
-
#
|
|
125
|
-
# @param sql [String]
|
|
126
|
-
# @param style [String] target placeholder style: "%s" or ":"
|
|
127
|
-
# @return [String]
|
|
128
|
-
def placeholder_style(sql, style)
|
|
129
|
-
case style
|
|
130
|
-
when "%s"
|
|
131
|
-
sql.gsub("?", "%s")
|
|
132
|
-
when ":"
|
|
133
|
-
count = 0
|
|
134
|
-
sql.chars.map do |ch|
|
|
135
|
-
if ch == "?"
|
|
136
|
-
count += 1
|
|
137
|
-
":#{count}"
|
|
138
|
-
else
|
|
139
|
-
ch
|
|
140
|
-
end
|
|
141
|
-
end.join
|
|
142
|
-
else
|
|
143
|
-
sql
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Generate a cache key for a query and its parameters.
|
|
148
|
-
#
|
|
149
|
-
# @param sql [String]
|
|
150
|
-
# @param params [Array, nil]
|
|
151
|
-
# @return [String]
|
|
152
|
-
def query_key(sql, params = nil)
|
|
153
|
-
raw = params ? "#{sql}|#{params.inspect}" : sql
|
|
154
|
-
"query:#{Digest::SHA256.hexdigest(raw)}"
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "cache"
|
|
5
|
+
|
|
6
|
+
module Tina4
|
|
7
|
+
# Cross-engine SQL translator.
|
|
8
|
+
#
|
|
9
|
+
# Each database adapter calls the rules it needs. Rules are composable
|
|
10
|
+
# and stateless -- just string transforms.
|
|
11
|
+
#
|
|
12
|
+
# Also includes query caching with TTL support.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# translated = Tina4::SQLTranslator.limit_to_rows("SELECT * FROM users LIMIT 10 OFFSET 5")
|
|
16
|
+
# # => "SELECT * FROM users ROWS 6 TO 15"
|
|
17
|
+
#
|
|
18
|
+
class SQLTranslator
|
|
19
|
+
class << self
|
|
20
|
+
# Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
21
|
+
#
|
|
22
|
+
# LIMIT 10 OFFSET 5 => ROWS 6 TO 15
|
|
23
|
+
# LIMIT 10 => ROWS 1 TO 10
|
|
24
|
+
#
|
|
25
|
+
# @param sql [String]
|
|
26
|
+
# @return [String]
|
|
27
|
+
def limit_to_rows(sql)
|
|
28
|
+
# Try LIMIT X OFFSET Y first
|
|
29
|
+
if (m = sql.match(/\bLIMIT\s+(\d+)\s+OFFSET\s+(\d+)\s*$/i))
|
|
30
|
+
limit = m[1].to_i
|
|
31
|
+
offset = m[2].to_i
|
|
32
|
+
start_row = offset + 1
|
|
33
|
+
end_row = offset + limit
|
|
34
|
+
return sql[0...m.begin(0)] + "ROWS #{start_row} TO #{end_row}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Then try LIMIT X only
|
|
38
|
+
if (m = sql.match(/\bLIMIT\s+(\d+)\s*$/i))
|
|
39
|
+
limit = m[1].to_i
|
|
40
|
+
return sql[0...m.begin(0)] + "ROWS 1 TO #{limit}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sql
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Convert LIMIT to MSSQL TOP syntax.
|
|
47
|
+
#
|
|
48
|
+
# SELECT ... LIMIT 10 => SELECT TOP 10 ...
|
|
49
|
+
# OFFSET queries are left unchanged (not supported by TOP).
|
|
50
|
+
#
|
|
51
|
+
# @param sql [String]
|
|
52
|
+
# @return [String]
|
|
53
|
+
def limit_to_top(sql)
|
|
54
|
+
if (m = sql.match(/\bLIMIT\s+(\d+)\s*$/i)) && !sql.match?(/\bOFFSET\b/i)
|
|
55
|
+
limit = m[1].to_i
|
|
56
|
+
body = sql[0...m.begin(0)].strip
|
|
57
|
+
return body.sub(/^(SELECT)\b/i, "\\1 TOP #{limit}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sql
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Convert || concatenation to CONCAT() for MySQL/MSSQL.
|
|
64
|
+
#
|
|
65
|
+
# 'a' || 'b' || 'c' => CONCAT('a', 'b', 'c')
|
|
66
|
+
#
|
|
67
|
+
# @param sql [String]
|
|
68
|
+
# @return [String]
|
|
69
|
+
def concat_pipes_to_func(sql)
|
|
70
|
+
return sql unless sql.include?("||")
|
|
71
|
+
|
|
72
|
+
parts = sql.split("||")
|
|
73
|
+
if parts.length > 1
|
|
74
|
+
"CONCAT(#{parts.map(&:strip).join(', ')})"
|
|
75
|
+
else
|
|
76
|
+
sql
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Convert TRUE/FALSE to 1/0 for engines without boolean type.
|
|
81
|
+
#
|
|
82
|
+
# @param sql [String]
|
|
83
|
+
# @return [String]
|
|
84
|
+
def boolean_to_int(sql)
|
|
85
|
+
sql.gsub(/\bTRUE\b/i, "1").gsub(/\bFALSE\b/i, "0")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Convert ILIKE to LOWER() LIKE LOWER() for engines without ILIKE.
|
|
89
|
+
#
|
|
90
|
+
# @param sql [String]
|
|
91
|
+
# @return [String]
|
|
92
|
+
def ilike_to_like(sql)
|
|
93
|
+
sql.gsub(/(\S+)\s+ILIKE\s+(\S+)/i) do
|
|
94
|
+
col = ::Regexp.last_match(1).strip
|
|
95
|
+
val = ::Regexp.last_match(2).strip
|
|
96
|
+
"LOWER(#{col}) LIKE LOWER(#{val})"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Translate AUTOINCREMENT across engines in DDL.
|
|
101
|
+
#
|
|
102
|
+
# @param sql [String]
|
|
103
|
+
# @param engine [String] one of: mysql, postgresql, mssql, firebird, sqlite
|
|
104
|
+
# @return [String]
|
|
105
|
+
def auto_increment_syntax(sql, engine)
|
|
106
|
+
case engine
|
|
107
|
+
when "mysql"
|
|
108
|
+
sql.gsub("AUTOINCREMENT", "AUTO_INCREMENT")
|
|
109
|
+
when "postgresql"
|
|
110
|
+
sql.gsub(/INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT/i, "SERIAL PRIMARY KEY")
|
|
111
|
+
when "mssql"
|
|
112
|
+
sql.gsub(/AUTOINCREMENT/i, "IDENTITY(1,1)")
|
|
113
|
+
when "firebird"
|
|
114
|
+
sql.gsub(/\s*AUTOINCREMENT\b/i, "")
|
|
115
|
+
else
|
|
116
|
+
sql
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Convert ? placeholders to engine-specific style.
|
|
121
|
+
#
|
|
122
|
+
# ? => %s (MySQL, PostgreSQL)
|
|
123
|
+
# ? => :1, :2 (Oracle, Firebird)
|
|
124
|
+
#
|
|
125
|
+
# @param sql [String]
|
|
126
|
+
# @param style [String] target placeholder style: "%s" or ":"
|
|
127
|
+
# @return [String]
|
|
128
|
+
def placeholder_style(sql, style)
|
|
129
|
+
case style
|
|
130
|
+
when "%s"
|
|
131
|
+
sql.gsub("?", "%s")
|
|
132
|
+
when ":"
|
|
133
|
+
count = 0
|
|
134
|
+
sql.chars.map do |ch|
|
|
135
|
+
if ch == "?"
|
|
136
|
+
count += 1
|
|
137
|
+
":#{count}"
|
|
138
|
+
else
|
|
139
|
+
ch
|
|
140
|
+
end
|
|
141
|
+
end.join
|
|
142
|
+
else
|
|
143
|
+
sql
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Generate a cache key for a query and its parameters.
|
|
148
|
+
#
|
|
149
|
+
# @param sql [String]
|
|
150
|
+
# @param params [Array, nil]
|
|
151
|
+
# @return [String]
|
|
152
|
+
def query_key(sql, params = nil)
|
|
153
|
+
raw = params ? "#{sql}|#{params.inspect}" : sql
|
|
154
|
+
"query:#{Digest::SHA256.hexdigest(raw)}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|