db2_query 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +38 -37
- data/Rakefile +2 -19
- data/lib/active_record/connection_adapters/db2_query_adapter.rb +203 -0
- data/lib/db2_query.rb +8 -14
- data/lib/db2_query/base.rb +6 -79
- data/lib/db2_query/config.rb +30 -0
- data/lib/db2_query/connection_handling.rb +20 -43
- data/lib/db2_query/core.rb +88 -0
- data/lib/db2_query/database_statements.rb +71 -15
- data/lib/db2_query/formatter.rb +5 -23
- data/lib/db2_query/odbc_connector.rb +7 -7
- data/lib/db2_query/railtie.rb +9 -11
- data/lib/db2_query/result.rb +41 -73
- data/lib/{tasks → db2_query/tasks}/database.rake +16 -14
- data/lib/{tasks → db2_query/tasks}/init.rake +1 -1
- data/lib/{tasks → db2_query/tasks}/initializer.rake +2 -3
- data/lib/db2_query/version.rb +2 -2
- metadata +32 -17
- data/lib/db2_query/column.rb +0 -42
- data/lib/db2_query/connection.rb +0 -50
- data/lib/db2_query/database_configurations.rb +0 -50
- data/lib/db2_query/error.rb +0 -5
- data/lib/db2_query/log_subscriber.rb +0 -50
- 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
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DB2Query
|
4
|
+
class << self
|
5
|
+
def config
|
6
|
+
@config ||= read_config
|
7
|
+
end
|
8
|
+
|
9
|
+
def config_path
|
10
|
+
if defined?(Rails)
|
11
|
+
"#{Rails.root}/config/db2query_database.yml"
|
12
|
+
else
|
13
|
+
ENV["DQ_CONFIG_PATH"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def config_file
|
18
|
+
Pathname.new(config_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_config
|
22
|
+
erb = ERB.new(config_file.read)
|
23
|
+
YAML.parse(erb.result(binding)).transform
|
24
|
+
end
|
25
|
+
|
26
|
+
def connection_env
|
27
|
+
ENV["RAILS_ENV"].to_sym
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -1,56 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
3
|
+
module DB2Query
|
4
4
|
module ConnectionHandling
|
5
|
-
|
5
|
+
RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence }
|
6
|
+
DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
|
6
7
|
|
7
|
-
|
8
|
+
def resolve_config_for_connection(config_or_env) # :nodoc:
|
9
|
+
raise "Anonymous class is not allowed." unless name
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
base.extend ClassMethods
|
15
|
-
base.connection = nil
|
16
|
-
end
|
17
|
-
|
18
|
-
module ClassMethods
|
19
|
-
attr_reader :connection
|
20
|
-
|
21
|
-
def connection=(connection)
|
22
|
-
@connection = connection
|
23
|
-
update_descendants_connection unless self.descendants.empty?
|
24
|
-
end
|
25
|
-
|
26
|
-
def update_descendants_connection
|
27
|
-
self.descendants.each { |child| child.connection = @connection }
|
28
|
-
end
|
11
|
+
config_or_env ||= DEFAULT_ENV.call.to_sym
|
12
|
+
pool_name = primary_class? ? "primary" : name
|
13
|
+
self.connection_specification_name = pool_name
|
14
|
+
resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations)
|
29
15
|
|
30
|
-
|
31
|
-
|
32
|
-
db_name = db_name.nil? ? DEFAULT_DB : db_name.to_s
|
16
|
+
config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys
|
17
|
+
config_hash[:name] = pool_name
|
33
18
|
|
34
|
-
|
35
|
-
|
36
|
-
if self.configurations[db_name].nil?
|
37
|
-
raise Error, "Database (:#{db_name}) not found at database configurations."
|
38
|
-
end
|
39
|
-
|
40
|
-
conn_type, conn_config = extract_configuration(db_name)
|
41
|
-
|
42
|
-
connector = ODBCConnector.new(conn_type, conn_config)
|
43
|
-
self.connection = Connection.new(connector, db_name)
|
44
|
-
end
|
19
|
+
config_hash
|
20
|
+
end
|
45
21
|
|
46
|
-
|
47
|
-
|
22
|
+
def connection_specification_name
|
23
|
+
if !defined?(@connection_specification_name) || @connection_specification_name.nil?
|
24
|
+
return self == Base ? "primary" : superclass.connection_specification_name
|
48
25
|
end
|
26
|
+
@connection_specification_name
|
27
|
+
end
|
49
28
|
|
50
|
-
|
51
|
-
|
52
|
-
@connection = nil
|
53
|
-
end
|
29
|
+
def primary_class?
|
30
|
+
self == Base || defined?(ApplicationRecord) && self == ApplicationRecord
|
54
31
|
end
|
55
32
|
end
|
56
33
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DB2Query
|
4
|
+
module Core
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do |base|
|
8
|
+
def self.configurations=(config)
|
9
|
+
@@configurations = ActiveRecord::DatabaseConfigurations.new(config)
|
10
|
+
end
|
11
|
+
self.configurations = {}
|
12
|
+
|
13
|
+
def self.configurations
|
14
|
+
@@configurations
|
15
|
+
end
|
16
|
+
|
17
|
+
class_attribute :default_connection_handler
|
18
|
+
|
19
|
+
mattr_accessor :connection_handlers, instance_accessor: false, default: {}
|
20
|
+
|
21
|
+
mattr_accessor :writing_role, instance_accessor: false, default: :writing
|
22
|
+
|
23
|
+
mattr_accessor :reading_role, instance_accessor: false, default: :reading
|
24
|
+
|
25
|
+
def self.connection_handler
|
26
|
+
Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.connection_handler=(handler)
|
30
|
+
Thread.current.thread_variable_set("ar_connection_handler", handler)
|
31
|
+
end
|
32
|
+
|
33
|
+
self.default_connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
34
|
+
|
35
|
+
base.extend ClassMethods
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
def attributes(attr_name, format)
|
40
|
+
formatters.store(attr_name, format)
|
41
|
+
end
|
42
|
+
|
43
|
+
def query(name, sql_statement)
|
44
|
+
if defined_method_name?(name)
|
45
|
+
raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
|
46
|
+
"on the model \"#{self.name}\", but DB2Query already defined " \
|
47
|
+
"a class method with the same name."
|
48
|
+
end
|
49
|
+
|
50
|
+
unless sql_statement.strip.match?(/^select/i)
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
self.class.define_method(name) do |*args|
|
55
|
+
connection.exec_query(sql_statement, formatters, args)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def formatters
|
61
|
+
@formatters ||= Hash.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def defined_method_name?(name)
|
65
|
+
self.class.method_defined?(name) || self.class.private_method_defined?(name)
|
66
|
+
end
|
67
|
+
|
68
|
+
def method_missing(method_name, *args, &block)
|
69
|
+
sql_methods = self.instance_methods.grep(/_sql/)
|
70
|
+
sql_method = "#{method_name}_sql".to_sym
|
71
|
+
|
72
|
+
if sql_methods.include?(sql_method)
|
73
|
+
sql_statement = allocate.method(sql_method).call
|
74
|
+
|
75
|
+
unless sql_statement.is_a? String
|
76
|
+
raise ArgumentError, "Query methods must return a SQL statement string!"
|
77
|
+
end
|
78
|
+
|
79
|
+
query(method_name, sql_statement)
|
80
|
+
|
81
|
+
method(method_name).call(*args)
|
82
|
+
else
|
83
|
+
super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -1,32 +1,88 @@
|
|
1
|
-
#
|
1
|
+
# frozen_String_literal: true
|
2
2
|
|
3
|
-
module
|
3
|
+
module DB2Query
|
4
4
|
module DatabaseStatements
|
5
|
-
def
|
6
|
-
|
5
|
+
def query(sql)
|
6
|
+
stmt = @connection.run(sql)
|
7
|
+
stmt.to_a
|
8
|
+
ensure
|
9
|
+
stmt.drop unless stmt.nil?
|
7
10
|
end
|
8
11
|
|
9
|
-
def
|
10
|
-
|
12
|
+
def query_rows(sql)
|
13
|
+
query(sql)
|
11
14
|
end
|
12
15
|
|
13
|
-
def
|
16
|
+
def query_value(sql, name = nil)
|
17
|
+
single_value_from_rows(query(sql))
|
18
|
+
end
|
19
|
+
|
20
|
+
def query_values(sql, name = nil)
|
14
21
|
query(sql).map(&:first)
|
15
22
|
end
|
16
23
|
|
17
|
-
def
|
18
|
-
|
24
|
+
def execute(sql, args = [])
|
25
|
+
if args.empty?
|
26
|
+
@connection.do(sql)
|
27
|
+
else
|
28
|
+
@connection.do(sql, *args)
|
29
|
+
end
|
19
30
|
end
|
20
31
|
|
21
|
-
def
|
22
|
-
|
32
|
+
def exec_query(sql, formatter = {}, args = [], name = "SQL")
|
33
|
+
binds, args = extract_binds_from_sql(sql, args)
|
34
|
+
log(sql, name, binds, args) do
|
35
|
+
begin
|
36
|
+
if args.empty?
|
37
|
+
stmt = @connection.run(sql)
|
38
|
+
else
|
39
|
+
stmt = @connection.run(sql, *args)
|
40
|
+
end
|
41
|
+
columns = stmt.columns.values.map { |col| col.name.downcase }
|
42
|
+
rows = stmt.to_a
|
43
|
+
ensure
|
44
|
+
stmt.drop unless stmt.nil?
|
45
|
+
end
|
46
|
+
DB2Query::Result.new(columns, rows, formatter)
|
47
|
+
end
|
23
48
|
end
|
24
|
-
alias library current_schema
|
25
49
|
|
26
50
|
private
|
27
|
-
def
|
28
|
-
|
29
|
-
|
51
|
+
def key_finder_regex(k)
|
52
|
+
/#{k} =\\? | #{k}=\\? | #{k}= \\? /i
|
53
|
+
end
|
54
|
+
|
55
|
+
def extract_binds_from_sql(sql, args)
|
56
|
+
question_mark_positions = sql.enum_for(:scan, /\?/i).map { Regexp.last_match.begin(0) }
|
57
|
+
args = args.first.is_a?(Hash) ? args.first : args.is_a?(Array) ? args : [args]
|
58
|
+
given, expected = args.length, question_mark_positions.length
|
59
|
+
|
60
|
+
if given != expected
|
61
|
+
raise ArgumentError, "wrong number of arguments (given #{given}, expected #{expected})"
|
62
|
+
end
|
63
|
+
|
64
|
+
if args.is_a?(Hash)
|
65
|
+
binds = args.map do |key, value|
|
66
|
+
position = sql.enum_for(:scan, key_finder_regex(key)).map { Regexp.last_match.begin(0) }
|
67
|
+
if position.empty?
|
68
|
+
raise ArgumentError, "Column name: `#{key}` not found inside sql statement."
|
69
|
+
elsif position.length > 1
|
70
|
+
raise ArgumentError, "Can't handle such this kind of sql. Please refactor your sql."
|
71
|
+
else
|
72
|
+
index = position[0]
|
73
|
+
end
|
74
|
+
|
75
|
+
OpenStruct.new({ name: key.to_s, value: value, index: index })
|
76
|
+
end
|
77
|
+
binds = binds.sort_by { |bind| bind.index }
|
78
|
+
[binds.map { |bind| [bind, bind.value] }, binds.map { |bind| bind.value }]
|
79
|
+
elsif question_mark_positions.length == 1 && args.length == 1
|
80
|
+
column = sql[/(.*?) = \?|(.*?) =\?|(.*?)= \?|(.*?)=\?/m, 1].split.last.downcase
|
81
|
+
bind = OpenStruct.new({ name: column, value: args })
|
82
|
+
[[[bind, bind.value]], bind.value]
|
83
|
+
else
|
84
|
+
[args.map { |arg| [nil, arg] }, args]
|
85
|
+
end
|
30
86
|
end
|
31
87
|
end
|
32
88
|
end
|
data/lib/db2_query/formatter.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
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)
|
@@ -24,22 +24,4 @@ module Db2Query
|
|
24
24
|
raise NotImplementedError, "Implement format method in your subclass."
|
25
25
|
end
|
26
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
|
43
|
-
end
|
44
|
-
end
|
45
27
|
end
|
@@ -1,14 +1,14 @@
|
|
1
|
-
#
|
1
|
+
# frozen_String_literal: true
|
2
|
+
|
3
|
+
module DB2Query
|
4
|
+
CONNECTION_TYPES = %i[dsn conn_string].freeze
|
2
5
|
|
3
|
-
module Db2Query
|
4
6
|
class ODBCConnector
|
5
7
|
attr_reader :connector, :conn_type, :conn_config
|
6
8
|
|
7
|
-
CONNECTION_TYPES = %i[dsn conn_string].freeze
|
8
|
-
|
9
9
|
def initialize(type, config)
|
10
10
|
@conn_type, @conn_config = type, config.transform_keys(&:to_sym)
|
11
|
-
@connector =
|
11
|
+
@connector = DB2Query.const_get("#{conn_type.to_s.camelize}Connector").new
|
12
12
|
end
|
13
13
|
|
14
14
|
def connect
|
@@ -20,7 +20,7 @@ module Db2Query
|
|
20
20
|
def connect(config)
|
21
21
|
::ODBC.connect(config[:dsn], config[:uid], config[:pwd])
|
22
22
|
rescue ::ODBC::Error => e
|
23
|
-
raise
|
23
|
+
raise ArgumentError, "Unable to activate ODBC DSN connection #{e}"
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
@@ -32,7 +32,7 @@ module Db2Query
|
|
32
32
|
end
|
33
33
|
::ODBC::Database.new.drvconnect(driver)
|
34
34
|
rescue ::ODBC::Error => e
|
35
|
-
raise
|
35
|
+
raise ArgumentError, "Unable to activate ODBC Conn String connection #{e}"
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
data/lib/db2_query/railtie.rb
CHANGED
@@ -1,22 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require 'db2_query'
|
4
|
+
require 'rails'
|
5
|
+
|
6
|
+
module DB2Query
|
7
|
+
class Railtie < Rails::Railtie
|
5
8
|
railtie_name :db2_query
|
6
9
|
|
7
10
|
rake_tasks do
|
8
|
-
|
9
|
-
Dir.glob("#{
|
11
|
+
path = File.expand_path(__dir__)
|
12
|
+
Dir.glob("#{path}/tasks/*.rake").each { |f| load f }
|
10
13
|
end
|
11
14
|
|
12
15
|
initializer "db2_query.database_initialization" do
|
13
|
-
|
14
|
-
Base.
|
15
|
-
Base.establish_connection
|
16
|
-
end
|
17
|
-
|
18
|
-
initializer "db2_query.attach_log_subscription" do
|
19
|
-
LogSubscriber.attach_to :db2_query
|
16
|
+
DB2Query::Base.configurations = DB2Query.config
|
17
|
+
DB2Query::Base.establish_connection :primary
|
20
18
|
end
|
21
19
|
end
|
22
20
|
end
|
data/lib/db2_query/result.rb
CHANGED
@@ -1,95 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module
|
4
|
-
class Result
|
5
|
-
attr_reader :
|
3
|
+
module DB2Query
|
4
|
+
class Result < ActiveRecord::Result
|
5
|
+
attr_reader :formatters
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
|
10
|
-
@rows = rows
|
11
|
-
@attr_format = attr_format
|
12
|
-
@columns = []
|
13
|
-
@column_metadatas = extract_metadatas(columns)
|
7
|
+
def initialize(columns, rows, formatters = {}, column_types = {})
|
8
|
+
@formatters = formatters
|
9
|
+
super(columns, rows, column_types)
|
14
10
|
end
|
15
11
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
rows.map do |row|
|
20
|
-
(core.const_get klass).new(row, column_metadatas)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def to_hash
|
25
|
-
rows.map do |row|
|
26
|
-
Hash[columns.zip(row)]
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def pluck(*column_names)
|
31
|
-
records.map do |record|
|
32
|
-
column_names.map { |column_name| record.send(column_name) }
|
12
|
+
def records
|
13
|
+
@records ||= rows.map do |row|
|
14
|
+
Record.new(row, columns, formatters)
|
33
15
|
end
|
34
16
|
end
|
35
17
|
|
36
|
-
def first
|
37
|
-
records.first
|
38
|
-
end
|
39
|
-
|
40
|
-
def last
|
41
|
-
records.last
|
42
|
-
end
|
43
|
-
|
44
|
-
def size
|
45
|
-
records.size
|
46
|
-
end
|
47
|
-
alias length size
|
48
|
-
|
49
|
-
def each(&block)
|
50
|
-
records.each(&block)
|
51
|
-
end
|
52
|
-
|
53
18
|
def inspect
|
54
19
|
entries = records.take(11).map!(&:inspect)
|
20
|
+
|
55
21
|
entries[10] = "..." if entries.size == 11
|
56
|
-
"#<#{self.class} [#{entries.join(', ')}]>"
|
57
|
-
end
|
58
22
|
|
59
|
-
|
60
|
-
|
61
|
-
@records ||= to_a
|
62
|
-
end
|
23
|
+
"#<#{self.class.name} @records=[#{entries.join(', ')}]>"
|
24
|
+
end
|
63
25
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
26
|
+
class Record
|
27
|
+
attr_reader :formatters
|
28
|
+
|
29
|
+
def initialize(row, columns, formatters)
|
30
|
+
@formatters = formatters
|
31
|
+
columns.zip(row) do |col, val|
|
32
|
+
column, value = format(col, val)
|
33
|
+
singleton_class.class_eval { attr_accessor "#{column}" }
|
34
|
+
send("#{column}=", value)
|
68
35
|
end
|
69
36
|
end
|
70
37
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
38
|
+
def inspect
|
39
|
+
inspection = if defined?(instance_variables) && instance_variables
|
40
|
+
instance_variables.reject { |var| var == :@formatters }.map do |attr|
|
41
|
+
value = instance_variable_get(attr)
|
42
|
+
"#{attr[1..-1]}: #{(value.kind_of? String) ? %Q{"#{value}"} : value}"
|
43
|
+
end.compact.join(", ")
|
44
|
+
else
|
45
|
+
"not initialized"
|
46
|
+
end
|
79
47
|
|
80
|
-
|
81
|
-
|
82
|
-
instance_variables.collect do |attr|
|
83
|
-
value = instance_variable_get(attr)
|
84
|
-
"#{attr[1..-1]}: #{(value.kind_of? String) ? %Q{"#{value}"} : value}"
|
85
|
-
end.compact.join(", ")
|
86
|
-
else
|
87
|
-
"not initialized"
|
88
|
-
end
|
48
|
+
"#<Record #{inspection}>"
|
49
|
+
end
|
89
50
|
|
90
|
-
|
51
|
+
private
|
52
|
+
def format(col, val)
|
53
|
+
column = col.downcase
|
54
|
+
format_name = formatters[column.to_sym]
|
55
|
+
unless format_name.nil?
|
56
|
+
formatter = DB2Query::Formatter.lookup(format_name)
|
57
|
+
val = formatter.format(val)
|
91
58
|
end
|
59
|
+
[column, val]
|
92
60
|
end
|
93
|
-
|
61
|
+
end
|
94
62
|
end
|
95
63
|
end
|