db2_query 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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