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