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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -80
  3. data/LICENSE.txt +21 -21
  4. data/README.md +137 -137
  5. data/exe/tina4ruby +5 -5
  6. data/lib/tina4/ai.rb +696 -696
  7. data/lib/tina4/api.rb +189 -189
  8. data/lib/tina4/auth.rb +305 -305
  9. data/lib/tina4/auto_crud.rb +244 -244
  10. data/lib/tina4/cache.rb +154 -154
  11. data/lib/tina4/cli.rb +1449 -1449
  12. data/lib/tina4/constants.rb +46 -46
  13. data/lib/tina4/container.rb +74 -74
  14. data/lib/tina4/cors.rb +74 -74
  15. data/lib/tina4/crud.rb +692 -692
  16. data/lib/tina4/database/sqlite3_adapter.rb +165 -165
  17. data/lib/tina4/database.rb +625 -625
  18. data/lib/tina4/database_result.rb +208 -208
  19. data/lib/tina4/debug.rb +8 -8
  20. data/lib/tina4/dev.rb +14 -14
  21. data/lib/tina4/dev_admin.rb +935 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -110
  24. data/lib/tina4/drivers/mongodb_driver.rb +561 -561
  25. data/lib/tina4/drivers/mssql_driver.rb +112 -112
  26. data/lib/tina4/drivers/mysql_driver.rb +90 -90
  27. data/lib/tina4/drivers/odbc_driver.rb +191 -191
  28. data/lib/tina4/drivers/postgres_driver.rb +116 -106
  29. data/lib/tina4/drivers/sqlite_driver.rb +122 -122
  30. data/lib/tina4/env.rb +95 -95
  31. data/lib/tina4/error_overlay.rb +252 -252
  32. data/lib/tina4/events.rb +109 -109
  33. data/lib/tina4/field_types.rb +154 -154
  34. data/lib/tina4/frond.rb +2025 -2025
  35. data/lib/tina4/gallery/auth/meta.json +1 -1
  36. data/lib/tina4/gallery/auth/src/routes/api/gallery_auth.rb +114 -114
  37. data/lib/tina4/gallery/database/meta.json +1 -1
  38. data/lib/tina4/gallery/database/src/routes/api/gallery_db.rb +43 -43
  39. data/lib/tina4/gallery/error-overlay/meta.json +1 -1
  40. data/lib/tina4/gallery/error-overlay/src/routes/api/gallery_crash.rb +17 -17
  41. data/lib/tina4/gallery/orm/meta.json +1 -1
  42. data/lib/tina4/gallery/orm/src/routes/api/gallery_products.rb +16 -16
  43. data/lib/tina4/gallery/queue/meta.json +1 -1
  44. data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +325 -325
  45. data/lib/tina4/gallery/rest-api/meta.json +1 -1
  46. data/lib/tina4/gallery/rest-api/src/routes/api/gallery_hello.rb +14 -14
  47. data/lib/tina4/gallery/templates/meta.json +1 -1
  48. data/lib/tina4/gallery/templates/src/routes/gallery_page.rb +12 -12
  49. data/lib/tina4/gallery/templates/src/templates/gallery_page.twig +257 -257
  50. data/lib/tina4/graphql.rb +966 -966
  51. data/lib/tina4/health.rb +39 -39
  52. data/lib/tina4/html_element.rb +170 -170
  53. data/lib/tina4/job.rb +80 -80
  54. data/lib/tina4/localization.rb +168 -168
  55. data/lib/tina4/log.rb +203 -203
  56. data/lib/tina4/mcp.rb +696 -696
  57. data/lib/tina4/messenger.rb +587 -587
  58. data/lib/tina4/metrics.rb +793 -793
  59. data/lib/tina4/middleware.rb +445 -445
  60. data/lib/tina4/migration.rb +451 -451
  61. data/lib/tina4/orm.rb +790 -790
  62. data/lib/tina4/public/css/tina4.css +2463 -2463
  63. data/lib/tina4/public/css/tina4.min.css +1 -1
  64. data/lib/tina4/public/images/logo.svg +5 -5
  65. data/lib/tina4/public/js/frond.min.js +2 -2
  66. data/lib/tina4/public/js/tina4-dev-admin.js +565 -565
  67. data/lib/tina4/public/js/tina4-dev-admin.min.js +480 -480
  68. data/lib/tina4/public/js/tina4.min.js +92 -92
  69. data/lib/tina4/public/js/tina4js.min.js +48 -48
  70. data/lib/tina4/public/swagger/index.html +90 -90
  71. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  72. data/lib/tina4/query_builder.rb +380 -380
  73. data/lib/tina4/queue.rb +366 -366
  74. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  75. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  76. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  77. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  78. data/lib/tina4/rack_app.rb +817 -817
  79. data/lib/tina4/rate_limiter.rb +130 -130
  80. data/lib/tina4/request.rb +268 -255
  81. data/lib/tina4/response.rb +346 -346
  82. data/lib/tina4/response_cache.rb +551 -551
  83. data/lib/tina4/router.rb +406 -406
  84. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  85. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  86. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  87. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  88. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  89. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  90. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  91. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  92. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  93. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  94. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  95. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  96. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  97. data/lib/tina4/scss/tina4css/base.scss +1 -1
  98. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  99. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  100. data/lib/tina4/scss_compiler.rb +178 -178
  101. data/lib/tina4/seeder.rb +567 -567
  102. data/lib/tina4/service_runner.rb +303 -303
  103. data/lib/tina4/session.rb +297 -297
  104. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  105. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  106. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  107. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  108. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  109. data/lib/tina4/shutdown.rb +84 -84
  110. data/lib/tina4/sql_translation.rb +158 -158
  111. data/lib/tina4/swagger.rb +124 -124
  112. data/lib/tina4/template.rb +894 -894
  113. data/lib/tina4/templates/base.twig +26 -26
  114. data/lib/tina4/templates/errors/302.twig +14 -14
  115. data/lib/tina4/templates/errors/401.twig +9 -9
  116. data/lib/tina4/templates/errors/403.twig +29 -29
  117. data/lib/tina4/templates/errors/404.twig +29 -29
  118. data/lib/tina4/templates/errors/500.twig +38 -38
  119. data/lib/tina4/templates/errors/502.twig +9 -9
  120. data/lib/tina4/templates/errors/503.twig +12 -12
  121. data/lib/tina4/templates/errors/base.twig +37 -37
  122. data/lib/tina4/test_client.rb +159 -159
  123. data/lib/tina4/testing.rb +340 -340
  124. data/lib/tina4/validator.rb +174 -174
  125. data/lib/tina4/version.rb +1 -1
  126. data/lib/tina4/webserver.rb +312 -312
  127. data/lib/tina4/websocket.rb +343 -343
  128. data/lib/tina4/websocket_backplane.rb +190 -190
  129. data/lib/tina4/wsdl.rb +564 -564
  130. data/lib/tina4.rb +458 -458
  131. data/lib/tina4ruby.rb +4 -4
  132. metadata +3 -3
@@ -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