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.
@@ -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
@@ -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 Connection
5
- include DatabaseStatements
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
- attr_reader :connector, :db_name, :odbc_conn
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 initialize(connector, db_name)
10
- @connector = connector
11
- @db_name = db_name.to_sym
12
- connect
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 connect
16
- @odbc_conn = connector.connect
17
- @odbc_conn.use_time = true
37
+ def query_rows(sql)
38
+ query(sql)
18
39
  end
19
40
 
20
- def active?
21
- @odbc_conn.connected?
41
+ def query_value(sql)
42
+ single_value_from_rows(query(sql))
22
43
  end
23
44
 
24
- def disconnect!
25
- @odbc_conn.drop_all
26
- @odbc_conn.disconnect if active?
45
+ def query_values(sql)
46
+ query(sql).map(&:first)
27
47
  end
28
48
 
29
- def reconnect!
30
- disconnect!
31
- connect
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 execute(sql, *args)
36
- reset! unless active?
37
- @odbc_conn.do(sql, *args)
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 exec_query(sql, *args)
41
- reset! unless active?
42
- statement = @odbc_conn.run(sql, *args)
43
- columns = statement.columns.values
44
- rows = statement.to_a
45
- [columns, rows]
46
- ensure
47
- statement.drop if statement
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
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Db2Query
4
- class Error < ArgumentError; end
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
@@ -3,15 +3,15 @@
3
3
  module Db2Query
4
4
  module Formatter
5
5
  def self.register(name, klass)
6
- self.registry.store(name, klass.new)
6
+ self.format_registry.store(name.to_sym, klass.new)
7
7
  end
8
8
 
9
- def self.registry
10
- @@registry ||= Hash.new
9
+ def self.format_registry
10
+ @@format_registry ||= Hash.new
11
11
  end
12
12
 
13
13
  def self.lookup(name)
14
- @@registry.fetch(name.to_sym)
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 NotImplementedError, "Implement format method in your subclass."
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
@@ -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
- tasks_path = "#{File.expand_path("..", __dir__)}/tasks"
9
- Dir.glob("#{tasks_path}/*.rake").each { |file| load file }
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