db2_query 0.1.0 → 0.3.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/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
|