db2_query 0.2.0 → 0.3.1

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,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Bind < Struct.new(:name, :value, :index)
5
+ end
6
+
7
+ class Connection < ConnectionPool
8
+ attr_reader :config
9
+
10
+ include Logger
11
+
12
+ def initialize(config, &block)
13
+ @config = config
14
+ super(pool_config, &block)
15
+ @instrumenter = ActiveSupport::Notifications.instrumenter
16
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
17
+ end
18
+
19
+ alias pool with
20
+
21
+ def pool_config
22
+ { size: config[:pool], timeout: config[:timeout] }
23
+ end
24
+
25
+ def query(sql)
26
+ pool do |odbc_conn|
27
+ stmt = odbc_conn.run(sql)
28
+ stmt.to_a
29
+ ensure
30
+ stmt.drop unless stmt.nil?
31
+ end
32
+ end
33
+
34
+ def query_rows(sql)
35
+ query(sql)
36
+ end
37
+
38
+ def query_value(sql)
39
+ single_value_from_rows(query(sql))
40
+ end
41
+
42
+ def query_values(sql)
43
+ query(sql).map(&:first)
44
+ end
45
+
46
+ def execute(sql, args = [])
47
+ pool do |odbc_conn|
48
+ odbc_conn.do(sql, *args)
49
+ end
50
+ end
51
+
52
+ def exec_query(formatters, sql, args = [])
53
+ binds, args = extract_binds_from_sql(sql, args)
54
+ sql = db2_spec_sql(sql)
55
+ log(sql, "SQL", binds, args) do
56
+ run_query(formatters, sql, binds, args)
57
+ end
58
+ end
59
+
60
+ def run_query(formatters, sql, binds, args = [])
61
+ pool do |odbc_conn|
62
+ begin
63
+ if args.empty?
64
+ stmt = odbc_conn.run(sql)
65
+ else
66
+ stmt = odbc_conn.run(sql, *args)
67
+ end
68
+ columns = stmt.columns.values.map { |col| col.name.downcase }
69
+ rows = stmt.to_a
70
+ ensure
71
+ stmt.drop unless stmt.nil?
72
+ end
73
+ Db2Query::Result.new(columns, rows, formatters)
74
+ end
75
+ end
76
+
77
+ private
78
+ def single_value_from_rows(rows)
79
+ row = rows.first
80
+ row && row.first
81
+ end
82
+
83
+ def iud_sql?(sql)
84
+ sql.match?(/insert into|update|delete/i)
85
+ end
86
+
87
+ def iud_ref_table(sql)
88
+ sql.match?(/delete/i) ? "OLD TABLE" : "NEW TABLE"
89
+ end
90
+
91
+ def db2_spec_sql(sql)
92
+ if iud_sql?(sql)
93
+ "SELECT * FROM #{iud_ref_table(sql)} (#{sql})"
94
+ else
95
+ sql
96
+ end.tr("$", "")
97
+ end
98
+
99
+ def extract_binds_from_sql(sql, args)
100
+ keys = sql.scan(/\$\S+/).map { |key| key.gsub!(/[$=]/, "") }
101
+ sql = sql.tr("$", "")
102
+ args = args[0].is_a?(Hash) ? args[0] : args
103
+ given, expected = args.length, sql.scan(/\?/i).length
104
+
105
+ if given != expected
106
+ raise Db2Query::Error, "wrong number of arguments (given #{given}, expected #{expected})"
107
+ end
108
+
109
+ if args.is_a?(Hash)
110
+ binds = *args.map do |key, value|
111
+ if args[key.to_sym].nil?
112
+ raise Db2Query::Error, "Column name: `#{key}` not found inside sql statement."
113
+ end
114
+ Db2Query::Bind.new(key.to_s, value, nil)
115
+ end
116
+ else
117
+ binds = keys.map.with_index do |key, index|
118
+ Db2Query::Bind.new(key, args[index], nil)
119
+ end
120
+ end
121
+
122
+ [binds.map { |bind| [bind, bind.value] }, binds.map { |bind| bind.value }]
123
+ end
124
+ end
125
+ end
@@ -1,59 +1,118 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DB2Query
4
- module Core
5
- extend ActiveSupport::Concern
3
+ module Db2Query
4
+ class DbClient
5
+ attr_reader :dsn
6
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
7
+ delegate :run, :do, to: :client
16
8
 
17
- class_attribute :default_connection_handler
9
+ def initialize(config)
10
+ @dsn = config[:dsn]
11
+ @idle_time_limit = config[:idle] || 5
12
+ @client = new_db_client
13
+ @last_active = Time.now
14
+ end
18
15
 
19
- mattr_accessor :connection_handlers, instance_accessor: false, default: {}
16
+ def expire?
17
+ Time.now - @last_active > 60 * @idle_time_limit
18
+ end
20
19
 
21
- mattr_accessor :writing_role, instance_accessor: false, default: :writing
20
+ def active?
21
+ @client.connected?
22
+ end
22
23
 
23
- mattr_accessor :reading_role, instance_accessor: false, default: :reading
24
+ def connected_and_persist?
25
+ active? && !expire?
26
+ end
24
27
 
25
- def self.connection_handler
26
- Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler
27
- end
28
+ def disconnect!
29
+ @client.drop_all
30
+ @client.disconnect if active?
31
+ @client = nil
32
+ end
28
33
 
29
- def self.connection_handler=(handler)
30
- Thread.current.thread_variable_set("ar_connection_handler", handler)
31
- end
34
+ def new_db_client
35
+ ODBC.connect(dsn)
36
+ end
32
37
 
33
- self.default_connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
38
+ def client
39
+ return @client if connected_and_persist?
40
+ disconnect!
41
+ @last_active = Time.now
42
+ @client = new_db_client
43
+ end
44
+ end
34
45
 
35
- base.extend ClassMethods
46
+ module Core
47
+ extend ActiveSupport::Concern
48
+ included do
49
+ @@connection = nil
50
+ @@mutex = Mutex.new
36
51
  end
37
52
 
38
- module ClassMethods
53
+ class_methods do
54
+ def initiation
55
+ yield(self) if block_given?
56
+ end
57
+
39
58
  def attributes(attr_name, format)
40
59
  formatters.store(attr_name, format)
41
60
  end
42
61
 
43
- def query(name, sql_statement)
62
+ def connection
63
+ @@connection || create_connection
64
+ end
65
+
66
+ def create_connection
67
+ @@mutex.synchronize do
68
+ return @@connection if @@connection
69
+ @@connection = Connection.new(config) { DbClient.new(config) }
70
+ end
71
+ end
72
+
73
+ def establish_connection
74
+ load_database_configurations
75
+ create_connection
76
+ end
77
+
78
+ def query(name, body)
44
79
  if defined_method_name?(name)
45
- raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
80
+ raise Db2Query::Error, "You tried to define a scope named \"#{name}\" " \
46
81
  "on the model \"#{self.name}\", but DB2Query already defined " \
47
82
  "a class method with the same name."
48
83
  end
49
84
 
50
- unless sql_statement.strip.match?(/^select/i)
51
- raise NotImplementedError
85
+ if body.respond_to?(:call)
86
+ singleton_class.define_method(name) do |*args|
87
+ body.call(*args)
88
+ end
89
+ elsif body.is_a?(String)
90
+ sql = body.strip
91
+ singleton_class.define_method(name) do |*args|
92
+ connection.exec_query(formatters, sql, args)
93
+ end
94
+ else
95
+ raise Db2Query::Error, "The query body needs to be callable or is a sql string"
52
96
  end
97
+ end
98
+ alias define query
53
99
 
54
- self.class.define_method(name) do |*args|
55
- connection.exec_query(sql_statement, formatters, args)
56
- end
100
+ def fetch(sql, args)
101
+ validate_sql(sql)
102
+ connection.exec_query({}, sql, args)
103
+ end
104
+
105
+ def fetch_list(sql, args)
106
+ validate_sql(sql)
107
+ raise Db2Query::Error, "Missing @list pointer at SQL" if sql.scan(/\@list+/).length == 0
108
+ raise Db2Query::Error, "The arguments should be an array of list" unless args.is_a?(Array)
109
+ connection.exec_query({}, sql.gsub("@list", "'#{args.join("', '")}'"), [])
110
+ end
111
+
112
+ def sql_with_extention(sql, extention)
113
+ validate_sql(sql)
114
+ raise Db2Query::Error, "Missing @extention pointer at SQL" if sql.scan(/\@extention+/).length == 0
115
+ sql.gsub("@extention", extention.strip)
57
116
  end
58
117
 
59
118
  private
@@ -73,16 +132,29 @@ module DB2Query
73
132
  sql_statement = allocate.method(sql_method).call
74
133
 
75
134
  unless sql_statement.is_a? String
76
- raise ArgumentError, "Query methods must return a SQL statement string!"
135
+ raise Db2Query::Error, "Query methods must return a SQL statement string!"
77
136
  end
78
137
 
79
138
  query(method_name, sql_statement)
80
139
 
140
+ if args[0].is_a?(Hash)
141
+ keys = sql_statement.scan(/\$\S+/).map { |key| key.gsub!(/[$=]/, "") }
142
+ rearrange_args = {}
143
+ keys.each { |key| rearrange_args[key.to_sym] = args[0][key.to_sym] }
144
+ args[0] = rearrange_args
145
+ end
146
+
81
147
  method(method_name).call(*args)
148
+ elsif connection.respond_to?(method_name)
149
+ connection.send(method_name, *args)
82
150
  else
83
151
  super
84
152
  end
85
153
  end
154
+
155
+ def validate_sql(sql)
156
+ raise Db2Query::Error, "SQL have to be in string format" unless sql.is_a?(String)
157
+ end
86
158
  end
87
159
  end
88
160
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
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
16
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DB2Query
3
+ module Db2Query
4
4
  module Formatter
5
5
  def self.register(name, klass)
6
6
  self.format_registry.store(name.to_sym, klass.new)
@@ -21,7 +21,7 @@ module DB2Query
21
21
 
22
22
  class AbstractFormatter
23
23
  def format(value)
24
- raise NotImplementedError, "Implement format method in your subclass."
24
+ raise Db2Query::Error, "Implement format method in your subclass."
25
25
  end
26
26
  end
27
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 = [], args = [], &block)
16
+ @instrumenter.instrument(
17
+ "sql.active_record",
18
+ sql: sql,
19
+ name: name,
20
+ binds: binds,
21
+ type_casted_binds: args,
22
+ statement_name: nil,
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,20 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'db2_query'
4
- require 'rails'
3
+ require "db2_query"
4
+ require "rails"
5
5
 
6
- module DB2Query
7
- class Railtie < Rails::Railtie
6
+ module Db2Query
7
+ class Railtie < ::Rails::Railtie
8
8
  railtie_name :db2_query
9
9
 
10
10
  rake_tasks do
11
11
  path = File.expand_path(__dir__)
12
12
  Dir.glob("#{path}/tasks/*.rake").each { |f| load f }
13
13
  end
14
-
15
- initializer "db2_query.database_initialization" do
16
- DB2Query::Base.configurations = DB2Query.config
17
- DB2Query::Base.establish_connection :primary
18
- end
19
14
  end
20
15
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DB2Query
3
+ module Db2Query
4
4
  class Result < ActiveRecord::Result
5
5
  attr_reader :formatters
6
6
 
@@ -9,23 +9,37 @@ module DB2Query
9
9
  super(columns, rows, column_types)
10
10
  end
11
11
 
12
+ def includes_column?(name)
13
+ @columns.include? name
14
+ end
15
+
16
+ def record
17
+ @record ||= Record.new(rows[0], columns, formatters)
18
+ end
19
+
12
20
  def records
13
21
  @records ||= rows.map do |row|
14
22
  Record.new(row, columns, formatters)
15
23
  end
16
24
  end
17
25
 
26
+ def to_h
27
+ rows.map do |row|
28
+ columns.zip(row).each_with_object({}) { |cr, h| h[cr[0].to_sym] = cr[1] }
29
+ end
30
+ end
31
+
18
32
  def inspect
19
33
  entries = records.take(11).map!(&:inspect)
20
34
 
21
35
  entries[10] = "..." if entries.size == 11
22
36
 
23
- "#<#{self.class.name} @records=[#{entries.join(', ')}]>"
37
+ "#<#{self.class.name} [#{entries.join(', ')}]>"
24
38
  end
25
39
 
26
40
  class Record
27
41
  attr_reader :formatters
28
-
42
+
29
43
  def initialize(row, columns, formatters)
30
44
  @formatters = formatters
31
45
  columns.zip(row) do |col, val|
@@ -53,7 +67,7 @@ module DB2Query
53
67
  column = col.downcase
54
68
  format_name = formatters[column.to_sym]
55
69
  unless format_name.nil?
56
- formatter = DB2Query::Formatter.lookup(format_name)
70
+ formatter = Db2Query::Formatter.lookup(format_name)
57
71
  val = formatter.format(val)
58
72
  end
59
73
  [column, val]