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
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Drivers
|
|
5
|
-
class OdbcDriver
|
|
6
|
-
attr_reader :connection
|
|
7
|
-
|
|
8
|
-
# Connect to an ODBC data source.
|
|
9
|
-
#
|
|
10
|
-
# Connection string formats:
|
|
11
|
-
# odbc:///DSN=MyDSN
|
|
12
|
-
# odbc:///DSN=MyDSN;UID=user;PWD=pass
|
|
13
|
-
# odbc:///DRIVER={SQL Server};SERVER=host;DATABASE=db
|
|
14
|
-
#
|
|
15
|
-
# The leading scheme prefix "odbc:///" is stripped; the remainder is
|
|
16
|
-
# passed verbatim to ODBC::Database.new as a connection string.
|
|
17
|
-
# username: and password: are appended as UID/PWD if not already present
|
|
18
|
-
# in the connection string.
|
|
19
|
-
def connect(connection_string, username: nil, password: nil)
|
|
20
|
-
begin
|
|
21
|
-
require "odbc"
|
|
22
|
-
rescue LoadError
|
|
23
|
-
raise LoadError,
|
|
24
|
-
"The 'ruby-odbc' gem is required for ODBC connections. " \
|
|
25
|
-
"Install: gem install ruby-odbc"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
dsn_string = connection_string.to_s
|
|
29
|
-
.sub(/^odbc:\/\/\//, "")
|
|
30
|
-
.sub(/^odbc:\/\//, "")
|
|
31
|
-
.sub(/^odbc:/, "")
|
|
32
|
-
|
|
33
|
-
# Append credentials if provided and not already embedded
|
|
34
|
-
if username && !dsn_string.match?(/\bUID=/i)
|
|
35
|
-
dsn_string = "#{dsn_string};UID=#{username}"
|
|
36
|
-
end
|
|
37
|
-
if password && !dsn_string.match?(/\bPWD=/i)
|
|
38
|
-
dsn_string = "#{dsn_string};PWD=#{password}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
@connection = ODBC::Database.new(dsn_string)
|
|
42
|
-
@in_transaction = false
|
|
43
|
-
self
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def close
|
|
47
|
-
@connection&.disconnect
|
|
48
|
-
@connection = nil
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def connected?
|
|
52
|
-
!@connection.nil?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Execute a SELECT query and return rows as an array of symbol-keyed hashes.
|
|
56
|
-
def execute_query(sql, params = [])
|
|
57
|
-
stmt = if params && !params.empty?
|
|
58
|
-
s = @connection.prepare(sql)
|
|
59
|
-
s.execute(*params)
|
|
60
|
-
s
|
|
61
|
-
else
|
|
62
|
-
@connection.run(sql)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
columns = stmt.columns(true).map { |c| c.name.to_s.to_sym }
|
|
66
|
-
rows = []
|
|
67
|
-
while (row = stmt.fetch)
|
|
68
|
-
rows << columns.zip(row).to_h
|
|
69
|
-
end
|
|
70
|
-
stmt.drop
|
|
71
|
-
rows
|
|
72
|
-
rescue => e
|
|
73
|
-
stmt&.drop rescue nil
|
|
74
|
-
raise e
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Execute DDL or DML without returning rows.
|
|
78
|
-
def execute(sql, params = [])
|
|
79
|
-
if params && !params.empty?
|
|
80
|
-
stmt = @connection.prepare(sql)
|
|
81
|
-
stmt.execute(*params)
|
|
82
|
-
stmt.drop
|
|
83
|
-
else
|
|
84
|
-
@connection.do(sql)
|
|
85
|
-
end
|
|
86
|
-
nil
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# ODBC does not expose a universal last-insert-id API.
|
|
90
|
-
# Drivers that support it can be queried via execute_query after insert.
|
|
91
|
-
def last_insert_id
|
|
92
|
-
nil
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def placeholder
|
|
96
|
-
"?"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def placeholders(count)
|
|
100
|
-
(["?"] * count).join(", ")
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Build paginated SQL.
|
|
104
|
-
# Tries OFFSET/FETCH NEXT (SQL Server, newer ODBC sources) first.
|
|
105
|
-
# Falls back to LIMIT/OFFSET for sources that support it (MySQL, PostgreSQL via ODBC).
|
|
106
|
-
# The caller (Database#fetch) already gates on whether LIMIT is already present.
|
|
107
|
-
def apply_limit(sql, limit, offset = 0)
|
|
108
|
-
offset ||= 0
|
|
109
|
-
if offset > 0
|
|
110
|
-
# SQL Server / ANSI syntax — requires ORDER BY; add a no-op if absent
|
|
111
|
-
if sql.upcase.include?("ORDER BY")
|
|
112
|
-
"#{sql} OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY"
|
|
113
|
-
else
|
|
114
|
-
# LIMIT/OFFSET fallback (MySQL, PostgreSQL via ODBC, SQLite via ODBC)
|
|
115
|
-
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
116
|
-
end
|
|
117
|
-
else
|
|
118
|
-
"#{sql} LIMIT #{limit}"
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def begin_transaction
|
|
123
|
-
return if @in_transaction
|
|
124
|
-
@connection.autocommit = false
|
|
125
|
-
@in_transaction = true
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def commit
|
|
129
|
-
return unless @in_transaction
|
|
130
|
-
@connection.commit
|
|
131
|
-
@connection.autocommit = true
|
|
132
|
-
@in_transaction = false
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def rollback
|
|
136
|
-
return unless @in_transaction
|
|
137
|
-
@connection.rollback
|
|
138
|
-
@connection.autocommit = true
|
|
139
|
-
@in_transaction = false
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# List all user tables via ODBC metadata.
|
|
143
|
-
def tables
|
|
144
|
-
stmt = @connection.tables
|
|
145
|
-
rows = []
|
|
146
|
-
while (row = stmt.fetch_hash)
|
|
147
|
-
type = row["TABLE_TYPE"] || row[:TABLE_TYPE] || ""
|
|
148
|
-
name = row["TABLE_NAME"] || row[:TABLE_NAME]
|
|
149
|
-
rows << name.to_s if type.to_s.upcase == "TABLE" && name
|
|
150
|
-
end
|
|
151
|
-
stmt.drop
|
|
152
|
-
rows
|
|
153
|
-
rescue => e
|
|
154
|
-
stmt&.drop rescue nil
|
|
155
|
-
raise e
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Return column metadata for a table via ODBC metadata.
|
|
159
|
-
def columns(table_name)
|
|
160
|
-
stmt = @connection.columns(table_name.to_s)
|
|
161
|
-
result = []
|
|
162
|
-
while (row = stmt.fetch_hash)
|
|
163
|
-
name = row["COLUMN_NAME"] || row[:COLUMN_NAME]
|
|
164
|
-
type = row["TYPE_NAME"] || row[:TYPE_NAME]
|
|
165
|
-
nullable_val = row["NULLABLE"] || row[:NULLABLE]
|
|
166
|
-
default = row["COLUMN_DEF"] || row[:COLUMN_DEF]
|
|
167
|
-
result << {
|
|
168
|
-
name: name.to_s,
|
|
169
|
-
type: type.to_s,
|
|
170
|
-
nullable: nullable_val.to_i == 1,
|
|
171
|
-
default: default,
|
|
172
|
-
primary_key: false # ODBC metadata does not reliably expose PK flag here
|
|
173
|
-
}
|
|
174
|
-
end
|
|
175
|
-
stmt.drop
|
|
176
|
-
result
|
|
177
|
-
rescue => e
|
|
178
|
-
stmt&.drop rescue nil
|
|
179
|
-
raise e
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
private
|
|
183
|
-
|
|
184
|
-
def symbolize_keys(hash)
|
|
185
|
-
hash.each_with_object({}) do |(k, v), h|
|
|
186
|
-
h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Drivers
|
|
5
|
+
class OdbcDriver
|
|
6
|
+
attr_reader :connection
|
|
7
|
+
|
|
8
|
+
# Connect to an ODBC data source.
|
|
9
|
+
#
|
|
10
|
+
# Connection string formats:
|
|
11
|
+
# odbc:///DSN=MyDSN
|
|
12
|
+
# odbc:///DSN=MyDSN;UID=user;PWD=pass
|
|
13
|
+
# odbc:///DRIVER={SQL Server};SERVER=host;DATABASE=db
|
|
14
|
+
#
|
|
15
|
+
# The leading scheme prefix "odbc:///" is stripped; the remainder is
|
|
16
|
+
# passed verbatim to ODBC::Database.new as a connection string.
|
|
17
|
+
# username: and password: are appended as UID/PWD if not already present
|
|
18
|
+
# in the connection string.
|
|
19
|
+
def connect(connection_string, username: nil, password: nil)
|
|
20
|
+
begin
|
|
21
|
+
require "odbc"
|
|
22
|
+
rescue LoadError
|
|
23
|
+
raise LoadError,
|
|
24
|
+
"The 'ruby-odbc' gem is required for ODBC connections. " \
|
|
25
|
+
"Install: gem install ruby-odbc"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
dsn_string = connection_string.to_s
|
|
29
|
+
.sub(/^odbc:\/\/\//, "")
|
|
30
|
+
.sub(/^odbc:\/\//, "")
|
|
31
|
+
.sub(/^odbc:/, "")
|
|
32
|
+
|
|
33
|
+
# Append credentials if provided and not already embedded
|
|
34
|
+
if username && !dsn_string.match?(/\bUID=/i)
|
|
35
|
+
dsn_string = "#{dsn_string};UID=#{username}"
|
|
36
|
+
end
|
|
37
|
+
if password && !dsn_string.match?(/\bPWD=/i)
|
|
38
|
+
dsn_string = "#{dsn_string};PWD=#{password}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@connection = ODBC::Database.new(dsn_string)
|
|
42
|
+
@in_transaction = false
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close
|
|
47
|
+
@connection&.disconnect
|
|
48
|
+
@connection = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def connected?
|
|
52
|
+
!@connection.nil?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Execute a SELECT query and return rows as an array of symbol-keyed hashes.
|
|
56
|
+
def execute_query(sql, params = [])
|
|
57
|
+
stmt = if params && !params.empty?
|
|
58
|
+
s = @connection.prepare(sql)
|
|
59
|
+
s.execute(*params)
|
|
60
|
+
s
|
|
61
|
+
else
|
|
62
|
+
@connection.run(sql)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
columns = stmt.columns(true).map { |c| c.name.to_s.to_sym }
|
|
66
|
+
rows = []
|
|
67
|
+
while (row = stmt.fetch)
|
|
68
|
+
rows << columns.zip(row).to_h
|
|
69
|
+
end
|
|
70
|
+
stmt.drop
|
|
71
|
+
rows
|
|
72
|
+
rescue => e
|
|
73
|
+
stmt&.drop rescue nil
|
|
74
|
+
raise e
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Execute DDL or DML without returning rows.
|
|
78
|
+
def execute(sql, params = [])
|
|
79
|
+
if params && !params.empty?
|
|
80
|
+
stmt = @connection.prepare(sql)
|
|
81
|
+
stmt.execute(*params)
|
|
82
|
+
stmt.drop
|
|
83
|
+
else
|
|
84
|
+
@connection.do(sql)
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ODBC does not expose a universal last-insert-id API.
|
|
90
|
+
# Drivers that support it can be queried via execute_query after insert.
|
|
91
|
+
def last_insert_id
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def placeholder
|
|
96
|
+
"?"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def placeholders(count)
|
|
100
|
+
(["?"] * count).join(", ")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Build paginated SQL.
|
|
104
|
+
# Tries OFFSET/FETCH NEXT (SQL Server, newer ODBC sources) first.
|
|
105
|
+
# Falls back to LIMIT/OFFSET for sources that support it (MySQL, PostgreSQL via ODBC).
|
|
106
|
+
# The caller (Database#fetch) already gates on whether LIMIT is already present.
|
|
107
|
+
def apply_limit(sql, limit, offset = 0)
|
|
108
|
+
offset ||= 0
|
|
109
|
+
if offset > 0
|
|
110
|
+
# SQL Server / ANSI syntax — requires ORDER BY; add a no-op if absent
|
|
111
|
+
if sql.upcase.include?("ORDER BY")
|
|
112
|
+
"#{sql} OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY"
|
|
113
|
+
else
|
|
114
|
+
# LIMIT/OFFSET fallback (MySQL, PostgreSQL via ODBC, SQLite via ODBC)
|
|
115
|
+
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
"#{sql} LIMIT #{limit}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def begin_transaction
|
|
123
|
+
return if @in_transaction
|
|
124
|
+
@connection.autocommit = false
|
|
125
|
+
@in_transaction = true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def commit
|
|
129
|
+
return unless @in_transaction
|
|
130
|
+
@connection.commit
|
|
131
|
+
@connection.autocommit = true
|
|
132
|
+
@in_transaction = false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def rollback
|
|
136
|
+
return unless @in_transaction
|
|
137
|
+
@connection.rollback
|
|
138
|
+
@connection.autocommit = true
|
|
139
|
+
@in_transaction = false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# List all user tables via ODBC metadata.
|
|
143
|
+
def tables
|
|
144
|
+
stmt = @connection.tables
|
|
145
|
+
rows = []
|
|
146
|
+
while (row = stmt.fetch_hash)
|
|
147
|
+
type = row["TABLE_TYPE"] || row[:TABLE_TYPE] || ""
|
|
148
|
+
name = row["TABLE_NAME"] || row[:TABLE_NAME]
|
|
149
|
+
rows << name.to_s if type.to_s.upcase == "TABLE" && name
|
|
150
|
+
end
|
|
151
|
+
stmt.drop
|
|
152
|
+
rows
|
|
153
|
+
rescue => e
|
|
154
|
+
stmt&.drop rescue nil
|
|
155
|
+
raise e
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Return column metadata for a table via ODBC metadata.
|
|
159
|
+
def columns(table_name)
|
|
160
|
+
stmt = @connection.columns(table_name.to_s)
|
|
161
|
+
result = []
|
|
162
|
+
while (row = stmt.fetch_hash)
|
|
163
|
+
name = row["COLUMN_NAME"] || row[:COLUMN_NAME]
|
|
164
|
+
type = row["TYPE_NAME"] || row[:TYPE_NAME]
|
|
165
|
+
nullable_val = row["NULLABLE"] || row[:NULLABLE]
|
|
166
|
+
default = row["COLUMN_DEF"] || row[:COLUMN_DEF]
|
|
167
|
+
result << {
|
|
168
|
+
name: name.to_s,
|
|
169
|
+
type: type.to_s,
|
|
170
|
+
nullable: nullable_val.to_i == 1,
|
|
171
|
+
default: default,
|
|
172
|
+
primary_key: false # ODBC metadata does not reliably expose PK flag here
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
stmt.drop
|
|
176
|
+
result
|
|
177
|
+
rescue => e
|
|
178
|
+
stmt&.drop rescue nil
|
|
179
|
+
raise e
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def symbolize_keys(hash)
|
|
185
|
+
hash.each_with_object({}) do |(k, v), h|
|
|
186
|
+
h[k.to_s.to_sym] = v if k.is_a?(String) || k.is_a?(Symbol)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -1,106 +1,116 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Drivers
|
|
5
|
-
class PostgresDriver
|
|
6
|
-
attr_reader :connection
|
|
7
|
-
|
|
8
|
-
def connect(connection_string, username: nil, password: nil)
|
|
9
|
-
require "pg"
|
|
10
|
-
url = connection_string
|
|
11
|
-
if username || password
|
|
12
|
-
uri = URI.parse(url)
|
|
13
|
-
uri.user = username if username
|
|
14
|
-
uri.password = password if password
|
|
15
|
-
url = uri.to_s
|
|
16
|
-
end
|
|
17
|
-
@connection = PG.connect(url)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def close
|
|
21
|
-
@connection&.close
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def execute_query(sql, params = [])
|
|
25
|
-
converted_sql = convert_placeholders(sql)
|
|
26
|
-
result = if params.empty?
|
|
27
|
-
@connection.exec(converted_sql)
|
|
28
|
-
else
|
|
29
|
-
@connection.exec_params(converted_sql, params)
|
|
30
|
-
end
|
|
31
|
-
result.map { |row| symbolize_keys(row) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def execute(sql, params = [])
|
|
35
|
-
converted_sql = convert_placeholders(sql)
|
|
36
|
-
if params.empty?
|
|
37
|
-
@connection.exec(converted_sql)
|
|
38
|
-
else
|
|
39
|
-
@connection.exec_params(converted_sql, params)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def last_insert_id
|
|
44
|
-
result = @connection.exec("SELECT lastval()")
|
|
45
|
-
result.first["lastval"].to_i
|
|
46
|
-
rescue PG::Error
|
|
47
|
-
nil
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def placeholder
|
|
51
|
-
"?"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def placeholders(count)
|
|
55
|
-
(1..count).map { |i| "$#{i}" }.join(", ")
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def apply_limit(sql, limit, offset = 0)
|
|
59
|
-
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def begin_transaction
|
|
63
|
-
@connection.exec("BEGIN")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def commit
|
|
67
|
-
@connection.exec("COMMIT")
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def rollback
|
|
71
|
-
@connection.exec("ROLLBACK")
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def tables
|
|
75
|
-
sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
|
|
76
|
-
rows = execute_query(sql)
|
|
77
|
-
rows.map { |r| r[:tablename] }
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def columns(table_name)
|
|
81
|
-
sql = "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1"
|
|
82
|
-
rows = execute_query(sql, [table_name])
|
|
83
|
-
rows.map do |r|
|
|
84
|
-
{
|
|
85
|
-
name: r[:column_name],
|
|
86
|
-
type: r[:data_type],
|
|
87
|
-
nullable: r[:is_nullable] == "YES",
|
|
88
|
-
default: r[:column_default],
|
|
89
|
-
primary_key: false
|
|
90
|
-
}
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
def convert_placeholders(sql)
|
|
97
|
-
counter = 0
|
|
98
|
-
sql.gsub("?") { counter += 1; "$#{counter}" }
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def symbolize_keys(hash)
|
|
102
|
-
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module Drivers
|
|
5
|
+
class PostgresDriver
|
|
6
|
+
attr_reader :connection
|
|
7
|
+
|
|
8
|
+
def connect(connection_string, username: nil, password: nil)
|
|
9
|
+
require "pg"
|
|
10
|
+
url = connection_string
|
|
11
|
+
if username || password
|
|
12
|
+
uri = URI.parse(url)
|
|
13
|
+
uri.user = username if username
|
|
14
|
+
uri.password = password if password
|
|
15
|
+
url = uri.to_s
|
|
16
|
+
end
|
|
17
|
+
@connection = PG.connect(url)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def close
|
|
21
|
+
@connection&.close
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute_query(sql, params = [])
|
|
25
|
+
converted_sql = convert_placeholders(sql)
|
|
26
|
+
result = if params.empty?
|
|
27
|
+
@connection.exec(converted_sql)
|
|
28
|
+
else
|
|
29
|
+
@connection.exec_params(converted_sql, params)
|
|
30
|
+
end
|
|
31
|
+
result.map { |row| decode_blobs(symbolize_keys(row)) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def execute(sql, params = [])
|
|
35
|
+
converted_sql = convert_placeholders(sql)
|
|
36
|
+
if params.empty?
|
|
37
|
+
@connection.exec(converted_sql)
|
|
38
|
+
else
|
|
39
|
+
@connection.exec_params(converted_sql, params)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def last_insert_id
|
|
44
|
+
result = @connection.exec("SELECT lastval()")
|
|
45
|
+
result.first["lastval"].to_i
|
|
46
|
+
rescue PG::Error
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def placeholder
|
|
51
|
+
"?"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def placeholders(count)
|
|
55
|
+
(1..count).map { |i| "$#{i}" }.join(", ")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def apply_limit(sql, limit, offset = 0)
|
|
59
|
+
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def begin_transaction
|
|
63
|
+
@connection.exec("BEGIN")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def commit
|
|
67
|
+
@connection.exec("COMMIT")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rollback
|
|
71
|
+
@connection.exec("ROLLBACK")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def tables
|
|
75
|
+
sql = "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
|
|
76
|
+
rows = execute_query(sql)
|
|
77
|
+
rows.map { |r| r[:tablename] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def columns(table_name)
|
|
81
|
+
sql = "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1"
|
|
82
|
+
rows = execute_query(sql, [table_name])
|
|
83
|
+
rows.map do |r|
|
|
84
|
+
{
|
|
85
|
+
name: r[:column_name],
|
|
86
|
+
type: r[:data_type],
|
|
87
|
+
nullable: r[:is_nullable] == "YES",
|
|
88
|
+
default: r[:column_default],
|
|
89
|
+
primary_key: false
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def convert_placeholders(sql)
|
|
97
|
+
counter = 0
|
|
98
|
+
sql.gsub("?") { counter += 1; "$#{counter}" }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def symbolize_keys(hash)
|
|
102
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Ensure binary (bytea) columns are proper byte strings.
|
|
106
|
+
# PostgreSQL's pg gem returns bytea as ASCII-8BIT encoded strings —
|
|
107
|
+
# they're already raw bytes, just tag them so Ruby treats them right.
|
|
108
|
+
def decode_blobs(row)
|
|
109
|
+
# No conversion needed — pg gem returns bytea as ASCII-8BIT strings
|
|
110
|
+
# which are raw bytes. Users can .force_encoding("UTF-8") for text
|
|
111
|
+
# BLOBs or use the bytes directly for binary data.
|
|
112
|
+
row
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|