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.
@@ -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 Db2Query
3
+ module DB2Query
4
4
  module ConnectionHandling
5
- extend ActiveSupport::Concern
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
- DEFAULT_DB = "primary"
8
+ def resolve_config_for_connection(config_or_env) # :nodoc:
9
+ raise "Anonymous class is not allowed." unless name
8
10
 
9
- included do |base|
10
- def base.inherited(child)
11
- child.connection = @connection
12
- end
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
- def establish_connection(db_name = nil)
31
- clear_connection unless self.connection.nil?
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
- self.load_database_configurations if self.configurations.nil?
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
- def current_database
47
- @connection.db_name
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
- def clear_connection
51
- @connection.disconnect!
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
- # frozen_string_literal: true
1
+ # frozen_String_literal: true
2
2
 
3
- module Db2Query
3
+ module DB2Query
4
4
  module DatabaseStatements
5
- def query_value(sql) # :nodoc:
6
- single_value_from_rows(query(sql))
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 query(sql)
10
- exec_query(sql).last
12
+ def query_rows(sql)
13
+ query(sql)
11
14
  end
12
15
 
13
- def query_values(sql)
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 current_database
18
- db_name.to_s
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 current_schema
22
- query_value("select current_schema from sysibm.sysdummy1").strip
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 single_value_from_rows(rows)
28
- row = rows.first
29
- row && row.first
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
@@ -1,17 +1,17 @@
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
- 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)
@@ -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
- # frozen_string_literal: true
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 = Db2Query.const_get("#{conn_type.to_s.camelize}Connector").new
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 Error, "Unable to activate ODBC DSN connection #{e}"
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 Error, "Unable to activate ODBC Conn String connection #{e}"
35
+ raise ArgumentError, "Unable to activate ODBC Conn String connection #{e}"
36
36
  end
37
37
  end
38
38
  end
@@ -1,22 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Db2Query
4
- class Railtie < ::Rails::Railtie
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
- tasks_path = "#{File.expand_path("..", __dir__)}/tasks"
9
- Dir.glob("#{tasks_path}/*.rake").each { |file| load file }
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
- 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
16
+ DB2Query::Base.configurations = DB2Query.config
17
+ DB2Query::Base.establish_connection :primary
20
18
  end
21
19
  end
22
20
  end
@@ -1,95 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Db2Query
4
- class Result
5
- attr_reader :core, :klass, :rows, :columns, :column_metadatas, :attr_format
3
+ module DB2Query
4
+ class Result < ActiveRecord::Result
5
+ attr_reader :formatters
6
6
 
7
- def initialize(core, klass, columns, rows, attr_format = {})
8
- @core = core
9
- @klass = klass.to_s.camelize
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 to_a
17
- core.const_set(klass, row_class) unless core.const_defined?(klass)
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
- private
60
- def records
61
- @records ||= to_a
62
- end
23
+ "#<#{self.class.name} @records=[#{entries.join(', ')}]>"
24
+ end
63
25
 
64
- def extract_metadatas(columns)
65
- columns.map do |col|
66
- @columns << column_name = col.name.downcase
67
- Column.new(column_name, col.type, attr_format[column_name.to_sym])
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 row_class
72
- Class.new do
73
- def initialize(row, columns_metadata)
74
- columns_metadata.zip(row) do |column, val|
75
- self.class.send(:attr_accessor, column.name.to_sym)
76
- instance_variable_set("@#{column.name}", column.format(val))
77
- end
78
- end
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
- def inspect
81
- inspection = if defined?(instance_variables) && instance_variables
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
- "#<#{self.class} #{inspection}>"
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
- end
61
+ end
94
62
  end
95
63
  end