tina4 0.3.0 → 0.5.0
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/version.rb +1 -1
- data/lib/tina4ruby.rb +4 -0
- metadata +14 -258
- data/CHANGELOG.md +0 -73
- data/LICENSE.txt +0 -21
- data/README.md +0 -662
- data/exe/tina4 +0 -4
- data/lib/tina4/api.rb +0 -152
- data/lib/tina4/auth.rb +0 -139
- data/lib/tina4/cli.rb +0 -243
- data/lib/tina4/crud.rb +0 -124
- data/lib/tina4/database.rb +0 -135
- data/lib/tina4/database_result.rb +0 -89
- data/lib/tina4/debug.rb +0 -83
- data/lib/tina4/dev_reload.rb +0 -68
- data/lib/tina4/drivers/firebird_driver.rb +0 -94
- data/lib/tina4/drivers/mssql_driver.rb +0 -112
- data/lib/tina4/drivers/mysql_driver.rb +0 -90
- data/lib/tina4/drivers/postgres_driver.rb +0 -99
- data/lib/tina4/drivers/sqlite_driver.rb +0 -85
- data/lib/tina4/env.rb +0 -55
- data/lib/tina4/field_types.rb +0 -84
- data/lib/tina4/graphql.rb +0 -837
- data/lib/tina4/localization.rb +0 -100
- data/lib/tina4/middleware.rb +0 -59
- data/lib/tina4/migration.rb +0 -124
- data/lib/tina4/orm.rb +0 -168
- data/lib/tina4/queue.rb +0 -117
- data/lib/tina4/queue_backends/kafka_backend.rb +0 -80
- data/lib/tina4/queue_backends/lite_backend.rb +0 -79
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +0 -73
- data/lib/tina4/rack_app.rb +0 -150
- data/lib/tina4/request.rb +0 -158
- data/lib/tina4/response.rb +0 -172
- data/lib/tina4/router.rb +0 -142
- data/lib/tina4/scss_compiler.rb +0 -131
- data/lib/tina4/session.rb +0 -145
- data/lib/tina4/session_handlers/file_handler.rb +0 -55
- data/lib/tina4/session_handlers/mongo_handler.rb +0 -49
- data/lib/tina4/session_handlers/redis_handler.rb +0 -43
- data/lib/tina4/swagger.rb +0 -123
- data/lib/tina4/template.rb +0 -478
- data/lib/tina4/templates/base.twig +0 -25
- data/lib/tina4/templates/errors/403.twig +0 -22
- data/lib/tina4/templates/errors/404.twig +0 -22
- data/lib/tina4/templates/errors/500.twig +0 -22
- data/lib/tina4/testing.rb +0 -213
- data/lib/tina4/webserver.rb +0 -101
- data/lib/tina4/websocket.rb +0 -167
- data/lib/tina4/wsdl.rb +0 -164
- data/lib/tina4.rb +0 -234
data/lib/tina4/database.rb
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
|
|
4
|
-
module Tina4
|
|
5
|
-
class Database
|
|
6
|
-
attr_reader :driver, :driver_name, :connected
|
|
7
|
-
|
|
8
|
-
DRIVERS = {
|
|
9
|
-
"sqlite" => "Tina4::Drivers::SqliteDriver",
|
|
10
|
-
"sqlite3" => "Tina4::Drivers::SqliteDriver",
|
|
11
|
-
"postgres" => "Tina4::Drivers::PostgresDriver",
|
|
12
|
-
"postgresql" => "Tina4::Drivers::PostgresDriver",
|
|
13
|
-
"mysql" => "Tina4::Drivers::MysqlDriver",
|
|
14
|
-
"mysql2" => "Tina4::Drivers::MysqlDriver",
|
|
15
|
-
"mssql" => "Tina4::Drivers::MssqlDriver",
|
|
16
|
-
"sqlserver" => "Tina4::Drivers::MssqlDriver",
|
|
17
|
-
"firebird" => "Tina4::Drivers::FirebirdDriver"
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
def initialize(connection_string, driver_name: nil)
|
|
21
|
-
@connection_string = connection_string
|
|
22
|
-
@driver_name = driver_name || detect_driver(connection_string)
|
|
23
|
-
@driver = create_driver
|
|
24
|
-
@connected = false
|
|
25
|
-
connect
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def connect
|
|
29
|
-
@driver.connect(@connection_string)
|
|
30
|
-
@connected = true
|
|
31
|
-
Tina4::Debug.info("Database connected: #{@driver_name}")
|
|
32
|
-
rescue => e
|
|
33
|
-
Tina4::Debug.error("Database connection failed: #{e.message}")
|
|
34
|
-
@connected = false
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def close
|
|
38
|
-
@driver.close if @connected
|
|
39
|
-
@connected = false
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def fetch(sql, params = [], limit: nil, skip: nil)
|
|
43
|
-
effective_sql = sql
|
|
44
|
-
if limit
|
|
45
|
-
effective_sql = @driver.apply_limit(effective_sql, limit, skip || 0)
|
|
46
|
-
end
|
|
47
|
-
rows = @driver.execute_query(effective_sql, params)
|
|
48
|
-
Tina4::DatabaseResult.new(rows, sql: effective_sql)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def fetch_one(sql, params = [])
|
|
52
|
-
result = fetch(sql, params, limit: 1)
|
|
53
|
-
result.first
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def insert(table, data)
|
|
57
|
-
columns = data.keys.map(&:to_s)
|
|
58
|
-
placeholders = @driver.placeholders(columns.length)
|
|
59
|
-
sql = "INSERT INTO #{table} (#{columns.join(', ')}) VALUES (#{placeholders})"
|
|
60
|
-
@driver.execute(sql, data.values)
|
|
61
|
-
{ success: true, last_id: @driver.last_insert_id }
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def update(table, data, filter = {})
|
|
65
|
-
set_parts = data.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
66
|
-
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
67
|
-
sql = "UPDATE #{table} SET #{set_parts.join(', ')}"
|
|
68
|
-
sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
|
|
69
|
-
values = data.values + filter.values
|
|
70
|
-
@driver.execute(sql, values)
|
|
71
|
-
{ success: true }
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def delete(table, filter = {})
|
|
75
|
-
where_parts = filter.keys.map { |k| "#{k} = #{@driver.placeholder}" }
|
|
76
|
-
sql = "DELETE FROM #{table}"
|
|
77
|
-
sql += " WHERE #{where_parts.join(' AND ')}" unless filter.empty?
|
|
78
|
-
@driver.execute(sql, filter.values)
|
|
79
|
-
{ success: true }
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def execute(sql, params = [])
|
|
83
|
-
@driver.execute(sql, params)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def transaction
|
|
87
|
-
@driver.begin_transaction
|
|
88
|
-
yield self
|
|
89
|
-
@driver.commit
|
|
90
|
-
rescue => e
|
|
91
|
-
@driver.rollback
|
|
92
|
-
raise e
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def tables
|
|
96
|
-
@driver.tables
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def columns(table_name)
|
|
100
|
-
@driver.columns(table_name)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def table_exists?(table_name)
|
|
104
|
-
tables.any? { |t| t.downcase == table_name.to_s.downcase }
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
private
|
|
108
|
-
|
|
109
|
-
def detect_driver(conn)
|
|
110
|
-
case conn.to_s.downcase
|
|
111
|
-
when /\.db$/, /\.sqlite/, /sqlite/
|
|
112
|
-
"sqlite"
|
|
113
|
-
when /postgres/, /^pg:/
|
|
114
|
-
"postgres"
|
|
115
|
-
when /mysql/
|
|
116
|
-
"mysql"
|
|
117
|
-
when /mssql/, /sqlserver/
|
|
118
|
-
"mssql"
|
|
119
|
-
when /firebird/, /\.fdb$/
|
|
120
|
-
"firebird"
|
|
121
|
-
else
|
|
122
|
-
"sqlite"
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def create_driver
|
|
127
|
-
klass_name = DRIVERS[@driver_name]
|
|
128
|
-
raise "Unknown database driver: #{@driver_name}" unless klass_name
|
|
129
|
-
klass = Object.const_get(klass_name)
|
|
130
|
-
klass.new
|
|
131
|
-
rescue NameError
|
|
132
|
-
raise "Driver #{klass_name} not loaded. Install the required gem."
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "json"
|
|
3
|
-
|
|
4
|
-
module Tina4
|
|
5
|
-
class DatabaseResult
|
|
6
|
-
include Enumerable
|
|
7
|
-
|
|
8
|
-
attr_reader :records, :sql, :count
|
|
9
|
-
|
|
10
|
-
def initialize(records = [], sql: "")
|
|
11
|
-
@records = records || []
|
|
12
|
-
@sql = sql
|
|
13
|
-
@count = @records.length
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def each(&block)
|
|
17
|
-
@records.each(&block)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def first
|
|
21
|
-
@records.first
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def last
|
|
25
|
-
@records.last
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def empty?
|
|
29
|
-
@records.empty?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def [](index)
|
|
33
|
-
@records[index]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def to_array
|
|
37
|
-
@records.map do |record|
|
|
38
|
-
record.is_a?(Hash) ? record : record.to_h
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def to_json(*_args)
|
|
43
|
-
JSON.generate(to_array)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def to_csv(separator: ",", headers: true)
|
|
47
|
-
return "" if @records.empty?
|
|
48
|
-
lines = []
|
|
49
|
-
cols = @records.first.keys
|
|
50
|
-
lines << cols.join(separator) if headers
|
|
51
|
-
@records.each do |row|
|
|
52
|
-
lines << cols.map { |c| escape_csv(row[c], separator) }.join(separator)
|
|
53
|
-
end
|
|
54
|
-
lines.join("\n")
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def to_paginate(page: 1, per_page: 10)
|
|
58
|
-
total = @records.length
|
|
59
|
-
total_pages = (total.to_f / per_page).ceil
|
|
60
|
-
offset = (page - 1) * per_page
|
|
61
|
-
page_records = @records[offset, per_page] || []
|
|
62
|
-
{
|
|
63
|
-
data: page_records,
|
|
64
|
-
page: page,
|
|
65
|
-
per_page: per_page,
|
|
66
|
-
total: total,
|
|
67
|
-
total_pages: total_pages,
|
|
68
|
-
has_next: page < total_pages,
|
|
69
|
-
has_prev: page > 1
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def to_crud(table_name: "data", primary_key: "id", editable: true)
|
|
74
|
-
Tina4::Crud.generate_table(@records, table_name: table_name,
|
|
75
|
-
primary_key: primary_key, editable: editable)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
def escape_csv(value, separator)
|
|
81
|
-
str = value.to_s
|
|
82
|
-
if str.include?(separator) || str.include?('"') || str.include?("\n")
|
|
83
|
-
"\"#{str.gsub('"', '""')}\""
|
|
84
|
-
else
|
|
85
|
-
str
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
data/lib/tina4/debug.rb
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require "logger"
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Tina4
|
|
6
|
-
module Debug
|
|
7
|
-
LEVELS = {
|
|
8
|
-
"[TINA4_LOG_ALL]" => Logger::DEBUG,
|
|
9
|
-
"[TINA4_LOG_DEBUG]" => Logger::DEBUG,
|
|
10
|
-
"[TINA4_LOG_INFO]" => Logger::INFO,
|
|
11
|
-
"[TINA4_LOG_WARNING]" => Logger::WARN,
|
|
12
|
-
"[TINA4_LOG_ERROR]" => Logger::ERROR,
|
|
13
|
-
"[TINA4_LOG_NONE]" => Logger::FATAL
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
|
-
COLORS = {
|
|
17
|
-
reset: "\e[0m", red: "\e[31m", green: "\e[32m",
|
|
18
|
-
yellow: "\e[33m", blue: "\e[34m", magenta: "\e[35m",
|
|
19
|
-
cyan: "\e[36m", gray: "\e[90m"
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
class << self
|
|
23
|
-
def setup(root_dir = Dir.pwd)
|
|
24
|
-
log_dir = File.join(root_dir, "logs")
|
|
25
|
-
FileUtils.mkdir_p(log_dir)
|
|
26
|
-
log_file = File.join(log_dir, "debug.log")
|
|
27
|
-
@file_logger = Logger.new(log_file, 10, 5 * 1024 * 1024)
|
|
28
|
-
@file_logger.level = Logger::DEBUG
|
|
29
|
-
@console_logger = Logger.new($stdout)
|
|
30
|
-
@console_logger.level = resolve_level
|
|
31
|
-
@console_logger.formatter = method(:color_formatter)
|
|
32
|
-
@file_logger.formatter = method(:plain_formatter)
|
|
33
|
-
@initialized = true
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def info(message, *args)
|
|
37
|
-
log(:info, message, *args)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def debug(message, *args)
|
|
41
|
-
log(:debug, message, *args)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def warning(message, *args)
|
|
45
|
-
log(:warn, message, *args)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def error(message, *args)
|
|
49
|
-
log(:error, message, *args)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def log(level, message, *args)
|
|
55
|
-
setup unless @initialized
|
|
56
|
-
full_message = args.empty? ? message.to_s : "#{message} #{args.map(&:to_s).join(' ')}"
|
|
57
|
-
@console_logger.send(level, full_message)
|
|
58
|
-
@file_logger.send(level, full_message)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def resolve_level
|
|
62
|
-
env_level = ENV["TINA4_DEBUG_LEVEL"] || "[TINA4_LOG_ALL]"
|
|
63
|
-
LEVELS[env_level] || Logger::DEBUG
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def color_formatter(severity, datetime, _progname, message)
|
|
67
|
-
color = case severity
|
|
68
|
-
when "DEBUG" then COLORS[:gray]
|
|
69
|
-
when "INFO" then COLORS[:green]
|
|
70
|
-
when "WARN" then COLORS[:yellow]
|
|
71
|
-
when "ERROR" then COLORS[:red]
|
|
72
|
-
else COLORS[:reset]
|
|
73
|
-
end
|
|
74
|
-
ts = datetime.strftime("%Y-%m-%d %H:%M:%S")
|
|
75
|
-
"#{COLORS[:gray]}[#{ts}]#{COLORS[:reset]} #{color}[#{severity}]#{COLORS[:reset]} #{message}\n"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def plain_formatter(severity, datetime, _progname, message)
|
|
79
|
-
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{message}\n"
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
data/lib/tina4/dev_reload.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module DevReload
|
|
5
|
-
WATCH_EXTENSIONS = %w[.rb .twig .html .erb .scss .css .js].freeze
|
|
6
|
-
WATCH_DIRS = %w[src routes lib templates public].freeze
|
|
7
|
-
IGNORE_DIRS = %w[.git node_modules vendor logs sessions .queue .keys].freeze
|
|
8
|
-
|
|
9
|
-
class << self
|
|
10
|
-
def start(root_dir: Dir.pwd, &on_change)
|
|
11
|
-
require "listen"
|
|
12
|
-
|
|
13
|
-
dirs = WATCH_DIRS
|
|
14
|
-
.map { |d| File.join(root_dir, d) }
|
|
15
|
-
.select { |d| Dir.exist?(d) }
|
|
16
|
-
|
|
17
|
-
# Also watch root for .rb files
|
|
18
|
-
dirs << root_dir
|
|
19
|
-
|
|
20
|
-
Tina4::Debug.info("Dev reload watching: #{dirs.join(', ')}")
|
|
21
|
-
|
|
22
|
-
@listener = Listen.to(*dirs, only: /\.(#{WATCH_EXTENSIONS.map { |e| e.delete('.') }.join('|')})$/, ignore: build_ignore_regex) do |modified, added, removed|
|
|
23
|
-
changes = { modified: modified, added: added, removed: removed }
|
|
24
|
-
all_files = modified + added + removed
|
|
25
|
-
next if all_files.empty?
|
|
26
|
-
|
|
27
|
-
Tina4::Debug.info("File changes detected:")
|
|
28
|
-
modified.each { |f| Tina4::Debug.debug(" Modified: #{f}") }
|
|
29
|
-
added.each { |f| Tina4::Debug.debug(" Added: #{f}") }
|
|
30
|
-
removed.each { |f| Tina4::Debug.debug(" Removed: #{f}") }
|
|
31
|
-
|
|
32
|
-
# Reload Ruby files
|
|
33
|
-
modified.select { |f| f.end_with?(".rb") }.each do |file|
|
|
34
|
-
begin
|
|
35
|
-
load file
|
|
36
|
-
Tina4::Debug.info("Reloaded: #{file}")
|
|
37
|
-
rescue => e
|
|
38
|
-
Tina4::Debug.error("Reload failed: #{file} - #{e.message}")
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Recompile SCSS
|
|
43
|
-
scss_changes = all_files.select { |f| f.end_with?(".scss") }
|
|
44
|
-
if scss_changes.any?
|
|
45
|
-
Tina4::ScssCompiler.compile_all(root_dir)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
on_change&.call(changes)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
@listener.start
|
|
52
|
-
Tina4::Debug.info("Dev reload started")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def stop
|
|
56
|
-
@listener&.stop
|
|
57
|
-
Tina4::Debug.info("Dev reload stopped")
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
private
|
|
61
|
-
|
|
62
|
-
def build_ignore_regex
|
|
63
|
-
pattern = IGNORE_DIRS.map { |d| Regexp.escape(d) }.join("|")
|
|
64
|
-
/#{pattern}/
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Drivers
|
|
5
|
-
class FirebirdDriver
|
|
6
|
-
attr_reader :connection
|
|
7
|
-
|
|
8
|
-
def connect(connection_string)
|
|
9
|
-
require "fb"
|
|
10
|
-
db_path = connection_string.sub(/^firebird:\/\//, "")
|
|
11
|
-
@connection = Fb::Database.new(database: db_path).connect
|
|
12
|
-
rescue LoadError
|
|
13
|
-
raise "Firebird driver requires the 'fb' gem. Install it with: gem install fb"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def close
|
|
17
|
-
@connection&.close
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def execute_query(sql, params = [])
|
|
21
|
-
if params.empty?
|
|
22
|
-
@connection.query(:hash, sql)
|
|
23
|
-
else
|
|
24
|
-
@connection.query(:hash, sql, *params)
|
|
25
|
-
end.map { |row| stringify_keys(row) }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def execute(sql, params = [])
|
|
29
|
-
if params.empty?
|
|
30
|
-
@connection.execute(sql)
|
|
31
|
-
else
|
|
32
|
-
@connection.execute(sql, *params)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def last_insert_id
|
|
37
|
-
nil
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def placeholder
|
|
41
|
-
"?"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def placeholders(count)
|
|
45
|
-
(["?"] * count).join(", ")
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def apply_limit(sql, limit, offset = 0)
|
|
49
|
-
"SELECT FIRST #{limit} SKIP #{offset} * FROM (#{sql})"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def begin_transaction
|
|
53
|
-
@transaction = @connection.transaction
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def commit
|
|
57
|
-
@transaction&.commit
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def rollback
|
|
61
|
-
@transaction&.rollback
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def tables
|
|
65
|
-
sql = "SELECT RDB\$RELATION_NAME FROM RDB\$RELATIONS WHERE RDB\$SYSTEM_FLAG = 0 AND RDB\$VIEW_BLR IS NULL"
|
|
66
|
-
rows = execute_query(sql)
|
|
67
|
-
rows.map { |r| (r["RDB\$RELATION_NAME"] || r["rdb\$relation_name"] || "").strip }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def columns(table_name)
|
|
71
|
-
sql = "SELECT RF.RDB\$FIELD_NAME, F.RDB\$FIELD_TYPE, RF.RDB\$NULL_FLAG, RF.RDB\$DEFAULT_SOURCE " \
|
|
72
|
-
"FROM RDB\$RELATION_FIELDS RF " \
|
|
73
|
-
"JOIN RDB\$FIELDS F ON RF.RDB\$FIELD_SOURCE = F.RDB\$FIELD_NAME " \
|
|
74
|
-
"WHERE RF.RDB\$RELATION_NAME = ?"
|
|
75
|
-
rows = execute_query(sql, [table_name.upcase])
|
|
76
|
-
rows.map do |r|
|
|
77
|
-
{
|
|
78
|
-
name: (r["RDB\$FIELD_NAME"] || r["rdb\$field_name"] || "").strip,
|
|
79
|
-
type: r["RDB\$FIELD_TYPE"] || r["rdb\$field_type"],
|
|
80
|
-
nullable: (r["RDB\$NULL_FLAG"] || r["rdb\$null_flag"]).nil?,
|
|
81
|
-
default: r["RDB\$DEFAULT_SOURCE"] || r["rdb\$default_source"],
|
|
82
|
-
primary_key: false
|
|
83
|
-
}
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
private
|
|
88
|
-
|
|
89
|
-
def stringify_keys(hash)
|
|
90
|
-
hash.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Drivers
|
|
5
|
-
class MssqlDriver
|
|
6
|
-
attr_reader :connection
|
|
7
|
-
|
|
8
|
-
def connect(connection_string)
|
|
9
|
-
require "tiny_tds"
|
|
10
|
-
uri = parse_connection(connection_string)
|
|
11
|
-
@connection = TinyTds::Client.new(
|
|
12
|
-
host: uri[:host],
|
|
13
|
-
port: uri[:port] || 1433,
|
|
14
|
-
username: uri[:username],
|
|
15
|
-
password: uri[:password],
|
|
16
|
-
database: uri[:database]
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def close
|
|
21
|
-
@connection&.close
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def execute_query(sql, params = [])
|
|
25
|
-
effective_sql = interpolate_params(sql, params)
|
|
26
|
-
result = @connection.execute(effective_sql)
|
|
27
|
-
rows = result.each(symbolize_keys: true).to_a
|
|
28
|
-
result.cancel if result.respond_to?(:cancel)
|
|
29
|
-
rows
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def execute(sql, params = [])
|
|
33
|
-
effective_sql = interpolate_params(sql, params)
|
|
34
|
-
result = @connection.execute(effective_sql)
|
|
35
|
-
result.do
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def last_insert_id
|
|
39
|
-
result = @connection.execute("SELECT SCOPE_IDENTITY() AS id")
|
|
40
|
-
row = result.first
|
|
41
|
-
result.cancel if result.respond_to?(:cancel)
|
|
42
|
-
row[:id]&.to_i
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def placeholder
|
|
46
|
-
"?"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def placeholders(count)
|
|
50
|
-
(["?"] * count).join(", ")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def apply_limit(sql, limit, offset = 0)
|
|
54
|
-
"#{sql} OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def begin_transaction
|
|
58
|
-
@connection.execute("BEGIN TRANSACTION").do
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def commit
|
|
62
|
-
@connection.execute("COMMIT").do
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def rollback
|
|
66
|
-
@connection.execute("ROLLBACK").do
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def tables
|
|
70
|
-
rows = execute_query("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'")
|
|
71
|
-
rows.map { |r| r[:TABLE_NAME] || r[:table_name] }
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def columns(table_name)
|
|
75
|
-
sql = "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?"
|
|
76
|
-
rows = execute_query(sql, [table_name])
|
|
77
|
-
rows.map do |r|
|
|
78
|
-
{
|
|
79
|
-
name: r[:COLUMN_NAME] || r[:column_name],
|
|
80
|
-
type: r[:DATA_TYPE] || r[:data_type],
|
|
81
|
-
nullable: (r[:IS_NULLABLE] || r[:is_nullable]) == "YES",
|
|
82
|
-
default: r[:COLUMN_DEFAULT] || r[:column_default],
|
|
83
|
-
primary_key: false
|
|
84
|
-
}
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
private
|
|
89
|
-
|
|
90
|
-
def parse_connection(str)
|
|
91
|
-
# Format: mssql://user:pass@host:port/database
|
|
92
|
-
match = str.match(%r{(?:mssql|sqlserver)://(?:(\w+):([^@]+)@)?([^:/]+)(?::(\d+))?/(.+)})
|
|
93
|
-
if match
|
|
94
|
-
{ username: match[1], password: match[2], host: match[3],
|
|
95
|
-
port: match[4]&.to_i, database: match[5] }
|
|
96
|
-
else
|
|
97
|
-
{ host: "localhost", database: str }
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def interpolate_params(sql, params)
|
|
102
|
-
return sql if params.empty?
|
|
103
|
-
result = sql.dup
|
|
104
|
-
params.each do |param|
|
|
105
|
-
escaped = param.is_a?(String) ? "'#{param.gsub("'", "''")}'" : param.to_s
|
|
106
|
-
result = result.sub("?", escaped)
|
|
107
|
-
end
|
|
108
|
-
result
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tina4
|
|
4
|
-
module Drivers
|
|
5
|
-
class MysqlDriver
|
|
6
|
-
attr_reader :connection
|
|
7
|
-
|
|
8
|
-
def connect(connection_string)
|
|
9
|
-
require "mysql2"
|
|
10
|
-
uri = URI.parse(connection_string.sub(/^mysql:\/\//, "mysql2://"))
|
|
11
|
-
@connection = Mysql2::Client.new(
|
|
12
|
-
host: uri.host || "localhost",
|
|
13
|
-
port: uri.port || 3306,
|
|
14
|
-
username: uri.user,
|
|
15
|
-
password: uri.password,
|
|
16
|
-
database: uri.path&.sub("/", "")
|
|
17
|
-
)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def close
|
|
21
|
-
@connection&.close
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def execute_query(sql, params = [])
|
|
25
|
-
if params.empty?
|
|
26
|
-
results = @connection.query(sql, symbolize_keys: true)
|
|
27
|
-
else
|
|
28
|
-
stmt = @connection.prepare(sql)
|
|
29
|
-
results = stmt.execute(*params, symbolize_keys: true)
|
|
30
|
-
end
|
|
31
|
-
results.to_a
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def execute(sql, params = [])
|
|
35
|
-
if params.empty?
|
|
36
|
-
@connection.query(sql)
|
|
37
|
-
else
|
|
38
|
-
stmt = @connection.prepare(sql)
|
|
39
|
-
stmt.execute(*params)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def last_insert_id
|
|
44
|
-
@connection.last_id
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def placeholder
|
|
48
|
-
"?"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def placeholders(count)
|
|
52
|
-
(["?"] * count).join(", ")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def apply_limit(sql, limit, offset = 0)
|
|
56
|
-
"#{sql} LIMIT #{limit} OFFSET #{offset}"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def begin_transaction
|
|
60
|
-
@connection.query("START TRANSACTION")
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def commit
|
|
64
|
-
@connection.query("COMMIT")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def rollback
|
|
68
|
-
@connection.query("ROLLBACK")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def tables
|
|
72
|
-
rows = execute_query("SHOW TABLES")
|
|
73
|
-
rows.map { |r| r.values.first }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def columns(table_name)
|
|
77
|
-
rows = execute_query("DESCRIBE #{table_name}")
|
|
78
|
-
rows.map do |r|
|
|
79
|
-
{
|
|
80
|
-
name: r[:Field],
|
|
81
|
-
type: r[:Type],
|
|
82
|
-
nullable: r[:Null] == "YES",
|
|
83
|
-
default: r[:Default],
|
|
84
|
-
primary_key: r[:Key] == "PRI"
|
|
85
|
-
}
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|