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.
@@ -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