db2_query 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +209 -55
- data/Rakefile +1 -17
- data/lib/db2_query.rb +10 -17
- data/lib/db2_query/base.rb +2 -78
- data/lib/db2_query/config.rb +29 -0
- data/lib/db2_query/connection.rb +108 -30
- data/lib/db2_query/core.rb +140 -0
- data/lib/db2_query/error.rb +12 -1
- data/lib/db2_query/formatter.rb +5 -23
- data/lib/db2_query/logger.rb +42 -0
- data/lib/db2_query/railtie.rb +5 -12
- data/lib/db2_query/result.rb +47 -65
- data/lib/db2_query/tasks/database.rake +38 -0
- data/lib/{tasks → db2_query/tasks}/init.rake +0 -0
- data/lib/{tasks → db2_query/tasks}/initializer.rake +10 -4
- data/lib/db2_query/version.rb +1 -1
- metadata +45 -57
- data/lib/db2_query/column.rb +0 -42
- data/lib/db2_query/connection_handling.rb +0 -56
- data/lib/db2_query/database_configurations.rb +0 -50
- data/lib/db2_query/database_statements.rb +0 -32
- data/lib/db2_query/log_subscriber.rb +0 -50
- data/lib/db2_query/odbc_connector.rb +0 -38
- data/lib/db2_query/path.rb +0 -17
- data/lib/db2_query/schema.rb +0 -113
- data/lib/db2_query/sql_validator.rb +0 -26
- data/lib/tasks/database.rake +0 -56
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Db2Query
|
4
|
+
module Config
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
DEFAULT_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence }
|
7
|
+
|
8
|
+
included do
|
9
|
+
@@configurations = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def configurations
|
14
|
+
@@configurations
|
15
|
+
end
|
16
|
+
alias config configurations
|
17
|
+
|
18
|
+
def load_database_configurations(path = nil)
|
19
|
+
file_path = path || "#{Rails.root}/config/db2query.yml"
|
20
|
+
if File.exist?(file_path)
|
21
|
+
config_file = IO.read(file_path)
|
22
|
+
@@configurations = YAML.load(config_file)[DEFAULT_ENV.call].transform_keys(&:to_sym)
|
23
|
+
else
|
24
|
+
raise Db2Query::Error, "Could not load db2query database configuration. No such file - #{file_path}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/db2_query/connection.rb
CHANGED
@@ -1,50 +1,128 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "error"
|
4
|
+
require "connection_pool"
|
5
|
+
|
3
6
|
module Db2Query
|
4
|
-
class
|
5
|
-
|
7
|
+
class Bind < Struct.new(:name, :value, :index)
|
8
|
+
end
|
9
|
+
|
10
|
+
class Connection < ConnectionPool
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
include Logger
|
6
14
|
|
7
|
-
|
15
|
+
def initialize(config, &block)
|
16
|
+
@config = config
|
17
|
+
super(pool_config, &block)
|
18
|
+
@instrumenter = ActiveSupport::Notifications.instrumenter
|
19
|
+
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
|
20
|
+
end
|
21
|
+
|
22
|
+
alias pool with
|
23
|
+
|
24
|
+
def pool_config
|
25
|
+
{ size: config[:pool], timeout: config[:timeout] }
|
26
|
+
end
|
8
27
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
28
|
+
def query(sql)
|
29
|
+
pool do |odbc_conn|
|
30
|
+
stmt = odbc_conn.run(sql)
|
31
|
+
stmt.to_a
|
32
|
+
ensure
|
33
|
+
stmt.drop unless stmt.nil?
|
34
|
+
end
|
13
35
|
end
|
14
36
|
|
15
|
-
def
|
16
|
-
|
17
|
-
@odbc_conn.use_time = true
|
37
|
+
def query_rows(sql)
|
38
|
+
query(sql)
|
18
39
|
end
|
19
40
|
|
20
|
-
def
|
21
|
-
|
41
|
+
def query_value(sql)
|
42
|
+
single_value_from_rows(query(sql))
|
22
43
|
end
|
23
44
|
|
24
|
-
def
|
25
|
-
|
26
|
-
@odbc_conn.disconnect if active?
|
45
|
+
def query_values(sql)
|
46
|
+
query(sql).map(&:first)
|
27
47
|
end
|
28
48
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
49
|
+
def execute(sql, args = [])
|
50
|
+
pool do |odbc_conn|
|
51
|
+
odbc_conn.do(sql, *args)
|
52
|
+
end
|
32
53
|
end
|
33
|
-
alias reset! reconnect!
|
34
54
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
55
|
+
def exec_query(formatters, sql, args = [])
|
56
|
+
binds, args = extract_binds_from_sql(sql, args)
|
57
|
+
sql = db2_spec_sql(sql)
|
58
|
+
log(sql, "SQL", binds, args) do
|
59
|
+
run_query(formatters, sql, args, binds)
|
60
|
+
end
|
38
61
|
end
|
39
62
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
63
|
+
def run_query(formatters, sql, args = [], binds)
|
64
|
+
pool do |odbc_conn|
|
65
|
+
begin
|
66
|
+
if args.empty?
|
67
|
+
stmt = odbc_conn.run(sql)
|
68
|
+
else
|
69
|
+
stmt = odbc_conn.run(sql, *args)
|
70
|
+
end
|
71
|
+
columns = stmt.columns.values.map { |col| col.name.downcase }
|
72
|
+
rows = stmt.to_a
|
73
|
+
ensure
|
74
|
+
stmt.drop unless stmt.nil?
|
75
|
+
end
|
76
|
+
Db2Query::Result.new(columns, rows, formatters)
|
77
|
+
end
|
48
78
|
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def single_value_from_rows(rows)
|
82
|
+
row = rows.first
|
83
|
+
row && row.first
|
84
|
+
end
|
85
|
+
|
86
|
+
def iud_sql?(sql)
|
87
|
+
sql.match?(/insert into|update|delete/i)
|
88
|
+
end
|
89
|
+
|
90
|
+
def iud_ref_table(sql)
|
91
|
+
sql.match?(/delete/i) ? "OLD TABLE" : "NEW TABLE"
|
92
|
+
end
|
93
|
+
|
94
|
+
def db2_spec_sql(sql)
|
95
|
+
if iud_sql?(sql)
|
96
|
+
"SELECT * FROM #{iud_ref_table(sql)} (#{sql})"
|
97
|
+
else
|
98
|
+
sql
|
99
|
+
end.tr("$", "")
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_binds_from_sql(sql, args)
|
103
|
+
keys = sql.scan(/\$\S+/).map { |key| key.gsub!(/[$=]/, "") }
|
104
|
+
sql = sql.tr("$", "")
|
105
|
+
args = args[0].is_a?(Hash) ? args[0] : args
|
106
|
+
given, expected = args.length, sql.scan(/\?/i).length
|
107
|
+
|
108
|
+
if given != expected
|
109
|
+
raise Db2Query::Error, "wrong number of arguments (given #{given}, expected #{expected})"
|
110
|
+
end
|
111
|
+
|
112
|
+
if args.is_a?(Hash)
|
113
|
+
binds = *args.map do |key, value|
|
114
|
+
if args[key.to_sym].nil?
|
115
|
+
raise Db2Query::Error, "Column name: `#{key}` not found inside sql statement."
|
116
|
+
end
|
117
|
+
Db2Query::Bind.new(key.to_s, value, nil)
|
118
|
+
end
|
119
|
+
else
|
120
|
+
binds = keys.map.with_index do |key, index|
|
121
|
+
Db2Query::Bind.new(key, args[index], nil)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
[binds.map { |bind| [bind, bind.value] }, binds.map { |bind| bind.value }]
|
126
|
+
end
|
49
127
|
end
|
50
128
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Db2Query
|
4
|
+
class DbClient
|
5
|
+
attr_reader :dsn
|
6
|
+
|
7
|
+
delegate :run, :do, to: :client
|
8
|
+
|
9
|
+
def initialize(dsn)
|
10
|
+
@dsn = dsn
|
11
|
+
@client = retrieve_db_client
|
12
|
+
end
|
13
|
+
|
14
|
+
def retrieve_db_client
|
15
|
+
ODBC.connect(dsn)
|
16
|
+
end
|
17
|
+
|
18
|
+
def client
|
19
|
+
unless @client.connected?
|
20
|
+
@client = retrieve_db_client
|
21
|
+
end
|
22
|
+
@client
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Core
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
included do
|
29
|
+
@@connection = nil
|
30
|
+
@@mutex = Mutex.new
|
31
|
+
end
|
32
|
+
|
33
|
+
class_methods do
|
34
|
+
def initiation
|
35
|
+
yield(self) if block_given?
|
36
|
+
end
|
37
|
+
|
38
|
+
def attributes(attr_name, format)
|
39
|
+
formatters.store(attr_name, format)
|
40
|
+
end
|
41
|
+
|
42
|
+
def connection
|
43
|
+
@@connection || create_connection
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_connection
|
47
|
+
@@mutex.synchronize do
|
48
|
+
return @@connection if @@connection
|
49
|
+
@@connection = Connection.new(config) { DbClient.new(config[:dsn]) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def establish_connection
|
54
|
+
load_database_configurations
|
55
|
+
create_connection
|
56
|
+
end
|
57
|
+
|
58
|
+
def query(name, body)
|
59
|
+
if defined_method_name?(name)
|
60
|
+
raise Db2Query::Error, "You tried to define a scope named \"#{name}\" " \
|
61
|
+
"on the model \"#{self.name}\", but DB2Query already defined " \
|
62
|
+
"a class method with the same name."
|
63
|
+
end
|
64
|
+
|
65
|
+
if body.respond_to?(:call)
|
66
|
+
singleton_class.define_method(name) do |*args|
|
67
|
+
body.call(*args)
|
68
|
+
end
|
69
|
+
elsif body.is_a?(String)
|
70
|
+
sql = body.strip
|
71
|
+
singleton_class.define_method(name) do |*args|
|
72
|
+
connection.exec_query(formatters, sql, args)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
raise Db2Query::Error, "The query body needs to be callable or is a sql string"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
alias define query
|
79
|
+
|
80
|
+
def fetch(sql, args)
|
81
|
+
validate_sql(sql)
|
82
|
+
connection.exec_query({}, sql, args)
|
83
|
+
end
|
84
|
+
|
85
|
+
def fetch_list(sql, args)
|
86
|
+
validate_sql(sql)
|
87
|
+
raise Db2Query::Error, "Missing @list pointer at SQL" if sql.scan(/\@list+/).length == 0
|
88
|
+
raise Db2Query::Error, "The arguments should be an array of list" unless args.is_a?(Array)
|
89
|
+
connection.exec_query({}, sql.gsub("@list", "'#{args.join("', '")}'"), [])
|
90
|
+
end
|
91
|
+
|
92
|
+
def sql_with_extention(sql, extention)
|
93
|
+
validate_sql(sql)
|
94
|
+
raise Db2Query::Error, "Missing @extention pointer at SQL" if sql.scan(/\@extention+/).length == 0
|
95
|
+
sql.gsub("@extention", extention.strip)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def formatters
|
100
|
+
@formatters ||= Hash.new
|
101
|
+
end
|
102
|
+
|
103
|
+
def defined_method_name?(name)
|
104
|
+
self.class.method_defined?(name) || self.class.private_method_defined?(name)
|
105
|
+
end
|
106
|
+
|
107
|
+
def method_missing(method_name, *args, &block)
|
108
|
+
sql_methods = self.instance_methods.grep(/_sql/)
|
109
|
+
sql_method = "#{method_name}_sql".to_sym
|
110
|
+
|
111
|
+
if sql_methods.include?(sql_method)
|
112
|
+
sql_statement = allocate.method(sql_method).call
|
113
|
+
|
114
|
+
unless sql_statement.is_a? String
|
115
|
+
raise Db2Query::Error, "Query methods must return a SQL statement string!"
|
116
|
+
end
|
117
|
+
|
118
|
+
query(method_name, sql_statement)
|
119
|
+
|
120
|
+
if args[0].is_a?(Hash)
|
121
|
+
keys = sql_statement.scan(/\$\S+/).map { |key| key.gsub!(/[$=]/, "") }
|
122
|
+
rearrange_args = {}
|
123
|
+
keys.each { |key| rearrange_args[key.to_sym] = args[0][key.to_sym] }
|
124
|
+
args[0] = rearrange_args
|
125
|
+
end
|
126
|
+
|
127
|
+
method(method_name).call(*args)
|
128
|
+
elsif connection.respond_to?(method_name)
|
129
|
+
connection.send(method_name, *args)
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_sql(sql)
|
136
|
+
raise Db2Query::Error, "SQL have to be in string format" unless sql.is_a?(String)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/db2_query/error.rb
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Db2Query
|
4
|
-
class Error <
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class StatementInvalid < ActiveRecord::ActiveRecordError
|
8
|
+
def initialize(message = nil, sql: nil, binds: nil)
|
9
|
+
super(message || $!.try(:message))
|
10
|
+
@sql = sql
|
11
|
+
@binds = binds
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :sql, :binds
|
15
|
+
end
|
5
16
|
end
|
data/lib/db2_query/formatter.rb
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
module Db2Query
|
4
4
|
module Formatter
|
5
5
|
def self.register(name, klass)
|
6
|
-
self.
|
6
|
+
self.format_registry.store(name.to_sym, klass.new)
|
7
7
|
end
|
8
8
|
|
9
|
-
def self.
|
10
|
-
@@
|
9
|
+
def self.format_registry
|
10
|
+
@@format_registry ||= Hash.new
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.lookup(name)
|
14
|
-
@@
|
14
|
+
@@format_registry.fetch(name)
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.registration(&block)
|
@@ -21,25 +21,7 @@ module Db2Query
|
|
21
21
|
|
22
22
|
class AbstractFormatter
|
23
23
|
def format(value)
|
24
|
-
raise
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class BareFormatter < AbstractFormatter
|
29
|
-
def format(value)
|
30
|
-
value
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class FloatFormatter < AbstractFormatter
|
35
|
-
def format(value)
|
36
|
-
value.to_f
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class IntegerFormatter < AbstractFormatter
|
41
|
-
def format(value)
|
42
|
-
value.to_i
|
24
|
+
raise Db2Query::Error, "Implement format method in your subclass."
|
43
25
|
end
|
44
26
|
end
|
45
27
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Db2Query
|
4
|
+
module Logger
|
5
|
+
def translate_exception_class(e, sql, binds)
|
6
|
+
message = "#{e.class.name}: #{e.message}"
|
7
|
+
|
8
|
+
exception = translate_exception(
|
9
|
+
e, message: message, sql: sql, binds: binds
|
10
|
+
)
|
11
|
+
exception.set_backtrace e.backtrace
|
12
|
+
exception
|
13
|
+
end
|
14
|
+
|
15
|
+
def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc:
|
16
|
+
@instrumenter.instrument(
|
17
|
+
"sql.active_record",
|
18
|
+
sql: sql,
|
19
|
+
name: name,
|
20
|
+
binds: binds,
|
21
|
+
type_casted_binds: type_casted_binds,
|
22
|
+
statement_name: statement_name,
|
23
|
+
connection_id: object_id,
|
24
|
+
connection: self) do
|
25
|
+
@lock.synchronize do
|
26
|
+
yield
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
raise translate_exception_class(e, sql, binds)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def translate_exception(exception, message:, sql:, binds:)
|
34
|
+
case exception
|
35
|
+
when RuntimeError
|
36
|
+
exception
|
37
|
+
else
|
38
|
+
StatementInvalid.new(message, sql: sql, binds: binds)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/db2_query/railtie.rb
CHANGED
@@ -1,22 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "db2_query"
|
4
|
+
require "rails"
|
5
|
+
|
3
6
|
module Db2Query
|
4
7
|
class Railtie < ::Rails::Railtie
|
5
8
|
railtie_name :db2_query
|
6
9
|
|
7
10
|
rake_tasks do
|
8
|
-
|
9
|
-
Dir.glob("#{
|
10
|
-
end
|
11
|
-
|
12
|
-
initializer "db2_query.database_initialization" do
|
13
|
-
Path.database_config_file = "#{Rails.root}/config/db2query_database.yml"
|
14
|
-
Base.load_database_configurations
|
15
|
-
Base.establish_connection
|
16
|
-
end
|
17
|
-
|
18
|
-
initializer "db2_query.attach_log_subscription" do
|
19
|
-
LogSubscriber.attach_to :db2_query
|
11
|
+
path = File.expand_path(__dir__)
|
12
|
+
Dir.glob("#{path}/tasks/*.rake").each { |f| load f }
|
20
13
|
end
|
21
14
|
end
|
22
15
|
end
|