tina4ruby 3.11.15 → 3.11.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.
Files changed (134) 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 +1289 -935
  22. data/lib/tina4/dev_mailbox.rb +191 -191
  23. data/lib/tina4/drivers/firebird_driver.rb +124 -124
  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 -116
  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 +2087 -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 +871 -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/plan.rb +471 -0
  63. data/lib/tina4/project_index.rb +366 -0
  64. data/lib/tina4/public/css/tina4.css +2463 -2463
  65. data/lib/tina4/public/css/tina4.min.css +1 -1
  66. data/lib/tina4/public/images/logo.svg +5 -5
  67. data/lib/tina4/public/js/frond.min.js +2 -2
  68. data/lib/tina4/public/js/tina4-dev-admin.js +1264 -565
  69. data/lib/tina4/public/js/tina4-dev-admin.min.js +1264 -480
  70. data/lib/tina4/public/js/tina4.min.js +92 -92
  71. data/lib/tina4/public/js/tina4js.min.js +48 -48
  72. data/lib/tina4/public/swagger/index.html +90 -90
  73. data/lib/tina4/public/swagger/oauth2-redirect.html +63 -63
  74. data/lib/tina4/query_builder.rb +380 -380
  75. data/lib/tina4/queue.rb +366 -366
  76. data/lib/tina4/queue_backends/kafka_backend.rb +80 -80
  77. data/lib/tina4/queue_backends/lite_backend.rb +298 -298
  78. data/lib/tina4/queue_backends/mongo_backend.rb +126 -126
  79. data/lib/tina4/queue_backends/rabbitmq_backend.rb +73 -73
  80. data/lib/tina4/rack_app.rb +817 -817
  81. data/lib/tina4/rate_limiter.rb +130 -130
  82. data/lib/tina4/request.rb +268 -268
  83. data/lib/tina4/response.rb +346 -346
  84. data/lib/tina4/response_cache.rb +551 -551
  85. data/lib/tina4/router.rb +406 -406
  86. data/lib/tina4/scss/tina4css/_alerts.scss +34 -34
  87. data/lib/tina4/scss/tina4css/_badges.scss +22 -22
  88. data/lib/tina4/scss/tina4css/_buttons.scss +69 -69
  89. data/lib/tina4/scss/tina4css/_cards.scss +49 -49
  90. data/lib/tina4/scss/tina4css/_forms.scss +156 -156
  91. data/lib/tina4/scss/tina4css/_grid.scss +81 -81
  92. data/lib/tina4/scss/tina4css/_modals.scss +84 -84
  93. data/lib/tina4/scss/tina4css/_nav.scss +149 -149
  94. data/lib/tina4/scss/tina4css/_reset.scss +94 -94
  95. data/lib/tina4/scss/tina4css/_tables.scss +54 -54
  96. data/lib/tina4/scss/tina4css/_typography.scss +55 -55
  97. data/lib/tina4/scss/tina4css/_utilities.scss +197 -197
  98. data/lib/tina4/scss/tina4css/_variables.scss +117 -117
  99. data/lib/tina4/scss/tina4css/base.scss +1 -1
  100. data/lib/tina4/scss/tina4css/colors.scss +48 -48
  101. data/lib/tina4/scss/tina4css/tina4.scss +17 -17
  102. data/lib/tina4/scss_compiler.rb +178 -178
  103. data/lib/tina4/seeder.rb +567 -567
  104. data/lib/tina4/service_runner.rb +303 -303
  105. data/lib/tina4/session.rb +297 -297
  106. data/lib/tina4/session_handlers/database_handler.rb +72 -72
  107. data/lib/tina4/session_handlers/file_handler.rb +67 -67
  108. data/lib/tina4/session_handlers/mongo_handler.rb +49 -49
  109. data/lib/tina4/session_handlers/redis_handler.rb +43 -43
  110. data/lib/tina4/session_handlers/valkey_handler.rb +43 -43
  111. data/lib/tina4/shutdown.rb +84 -84
  112. data/lib/tina4/sql_translation.rb +158 -158
  113. data/lib/tina4/swagger.rb +124 -124
  114. data/lib/tina4/template.rb +894 -894
  115. data/lib/tina4/templates/base.twig +26 -26
  116. data/lib/tina4/templates/errors/302.twig +14 -14
  117. data/lib/tina4/templates/errors/401.twig +9 -9
  118. data/lib/tina4/templates/errors/403.twig +29 -29
  119. data/lib/tina4/templates/errors/404.twig +29 -29
  120. data/lib/tina4/templates/errors/500.twig +38 -38
  121. data/lib/tina4/templates/errors/502.twig +9 -9
  122. data/lib/tina4/templates/errors/503.twig +12 -12
  123. data/lib/tina4/templates/errors/base.twig +37 -37
  124. data/lib/tina4/test_client.rb +159 -159
  125. data/lib/tina4/testing.rb +340 -340
  126. data/lib/tina4/validator.rb +174 -174
  127. data/lib/tina4/version.rb +1 -1
  128. data/lib/tina4/webserver.rb +312 -312
  129. data/lib/tina4/websocket.rb +343 -343
  130. data/lib/tina4/websocket_backplane.rb +190 -190
  131. data/lib/tina4/wsdl.rb +564 -564
  132. data/lib/tina4.rb +460 -458
  133. data/lib/tina4ruby.rb +4 -4
  134. metadata +5 -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