db2_query 0.2.3 → 0.3.3

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +472 -124
  4. data/Rakefile +3 -2
  5. data/lib/db2_query/base.rb +15 -5
  6. data/lib/db2_query/config.rb +20 -17
  7. data/lib/db2_query/core.rb +79 -60
  8. data/lib/db2_query/db_client.rb +56 -0
  9. data/lib/db2_query/db_connection.rb +68 -0
  10. data/lib/db2_query/db_statements.rb +87 -0
  11. data/lib/db2_query/definitions.rb +93 -0
  12. data/lib/db2_query/error.rb +72 -7
  13. data/lib/db2_query/field_type.rb +31 -0
  14. data/lib/db2_query/helper.rb +50 -0
  15. data/lib/db2_query/logger.rb +52 -0
  16. data/lib/db2_query/query.rb +128 -0
  17. data/lib/db2_query/railtie.rb +5 -10
  18. data/lib/db2_query/result.rb +51 -31
  19. data/lib/db2_query/sql_statement.rb +34 -0
  20. data/lib/db2_query/tasks/database.rake +2 -46
  21. data/lib/db2_query/tasks/init.rake +1 -1
  22. data/lib/db2_query/tasks/initializer.rake +2 -34
  23. data/lib/db2_query/tasks/templates/database.rb.tt +19 -0
  24. data/lib/db2_query/tasks/templates/initializer.rb.tt +8 -0
  25. data/lib/db2_query/tasks.rb +29 -0
  26. data/lib/db2_query/type/binary.rb +19 -0
  27. data/lib/db2_query/type/boolean.rb +41 -0
  28. data/lib/db2_query/type/date.rb +34 -0
  29. data/lib/db2_query/type/decimal.rb +15 -0
  30. data/lib/db2_query/type/integer.rb +15 -0
  31. data/lib/db2_query/type/string.rb +30 -0
  32. data/lib/db2_query/type/text.rb +11 -0
  33. data/lib/db2_query/type/time.rb +30 -0
  34. data/lib/db2_query/type/timestamp.rb +30 -0
  35. data/lib/db2_query/type/value.rb +29 -0
  36. data/lib/db2_query/version.rb +2 -2
  37. data/lib/db2_query.rb +42 -18
  38. data/lib/rails/generators/query/USAGE +15 -0
  39. data/lib/rails/generators/query/query_generator.rb +70 -0
  40. data/lib/rails/generators/query/templates/query.rb.tt +26 -0
  41. data/lib/rails/generators/query/templates/query_definitions.rb.tt +18 -0
  42. data/lib/rails/generators/query/templates/unit_test.rb.tt +9 -0
  43. metadata +74 -36
  44. data/lib/db2_query/bind.rb +0 -6
  45. data/lib/db2_query/connection.rb +0 -164
  46. data/lib/db2_query/connection_handling.rb +0 -112
  47. data/lib/db2_query/database_statements.rb +0 -89
  48. data/lib/db2_query/formatter.rb +0 -27
  49. data/lib/db2_query/odbc_connector.rb +0 -44
data/Rakefile CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
3
4
  require "bundler/gem_tasks"
4
5
  require "rake/testtask"
5
6
 
6
7
  Rake::TestTask.new(:test) do |t|
7
8
  t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/*_test.rb"]
9
+ t.pattern = "test/**/*_test.rb"
10
+ t.verbose = false
10
11
  end
11
12
 
12
13
  task default: :test
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DB2Query
3
+ module Db2Query
4
4
  class Base
5
- include ActiveRecord::Inheritance
6
- include DB2Query::Core
7
- extend ActiveRecord::ConnectionHandling
8
- extend DB2Query::ConnectionHandling
5
+ include Config
6
+ include Helper
7
+ include DbConnection
8
+ include FieldType
9
+ include Core
10
+
11
+ def self.inherited(subclass)
12
+ subclass.define_query_definitions
13
+ end
14
+
15
+ def self.establish_connection
16
+ load_database_configurations
17
+ new_database_connection
18
+ end
9
19
  end
10
20
  end
@@ -1,26 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DB2Query
4
- class << self
5
- def config
6
- @config ||= read_config
7
- end
3
+ module Db2Query
4
+ module Config
5
+ DEFAULT_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence }
8
6
 
9
- def config_path
10
- if defined?(Rails)
11
- "#{Rails.root}/config/db2query_database.yml"
12
- else
13
- ENV["DQ_CONFIG_PATH"]
14
- end
7
+ def self.included(base)
8
+ base.send(:extend, ClassMethods)
15
9
  end
16
10
 
17
- def config_file
18
- Pathname.new(config_path)
19
- end
11
+ module ClassMethods
12
+ mattr_accessor :configurations
13
+ @@configurations = nil
14
+
15
+ alias config configurations
20
16
 
21
- def read_config
22
- erb = ERB.new(config_file.read)
23
- YAML.parse(erb.result(binding)).transform.transform_keys(&:to_sym)
17
+ def default_path
18
+ "#{Rails.root}/config/db2query.yml"
19
+ end
20
+
21
+ def load_database_configurations(path = nil)
22
+ config_file = IO.read(path || default_path)
23
+ @@configurations = YAML.load(config_file)[DEFAULT_ENV.call].transform_keys(&:to_sym)
24
+ rescue Exception => e
25
+ raise Db2Query::Error, e.message
26
+ end
24
27
  end
25
28
  end
26
29
  end
@@ -1,91 +1,110 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_record/database_configurations"
4
-
5
- module DB2Query
3
+ module Db2Query
6
4
  module Core
7
- extend ActiveSupport::Concern
5
+ def self.included(base)
6
+ base.send(:extend, ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ attr_reader :definitions
11
+
12
+ delegate :query_rows, :query_value, :query_values, :execute, to: :connection
8
13
 
9
- included do
10
- def self.configurations=(config)
11
- @@configurations = ActiveRecord::DatabaseConfigurations.new(config)
14
+ def initiation
15
+ yield(self) if block_given?
12
16
  end
13
- self.configurations = {}
14
17
 
15
- def self.configurations
16
- @@configurations
18
+ def define_query_definitions
19
+ @definitions = new_definitions
17
20
  end
18
21
 
19
- mattr_accessor :connection_handlers, instance_accessor: false, default: {}
22
+ def exec_query_result(query, args)
23
+ reset_id_when_required(query)
24
+ connection.exec_query(query, args)
25
+ end
20
26
 
21
- class_attribute :default_connection_handler
27
+ def query(*query_args)
28
+ if query_args.first.is_a?(Symbol)
29
+ query_name, body = query_args
22
30
 
23
- def self.connection_handler
24
- Thread.current.thread_variable_get("db2q_connection_handler") || default_connection_handler
31
+ body_lambda = if body.is_a?(Proc)
32
+ -> args { body.call(args << { query_name: query_name }) }
33
+ elsif body.is_a?(String)
34
+ definition = definitions.lookup_query(query_name, body.strip)
35
+ -> args { exec_query_result(definition, args) }
36
+ else
37
+ raise Db2Query::QueryMethodError.new
38
+ end
39
+
40
+ singleton_class.define_method(query_name) do |*args|
41
+ body_lambda.call(args)
42
+ end
43
+ elsif query_args.first.is_a?(String)
44
+ sql, args = [query_args.first.strip, query_args.drop(1)]
45
+
46
+ definition = Query.new.tap do |d|
47
+ d.define_sql(sql)
48
+ d.define_args(args)
49
+ end
50
+
51
+ connection.raw_query(definition.db2_spec_sql, definition.args)
52
+ else
53
+ raise Db2Query::Error, "Wrong query implementation"
54
+ end
25
55
  end
56
+ alias define query
26
57
 
27
- def self.connection_handler=(handler)
28
- Thread.current.thread_variable_set("db2q_connection_handler", handler)
58
+ def query_arguments_map
59
+ @query_arguments_map ||= {}
29
60
  end
30
61
 
31
- self.default_connection_handler = DB2Query::ConnectionHandler.new
32
- end
62
+ def query_arguments(query_name, argument_types)
63
+ query_arguments_map[query_name] = argument_types
64
+ end
33
65
 
34
- module ClassMethods
35
- def initiation
36
- yield(self) if block_given?
66
+ def fetch(sql, args = [])
67
+ query = definitions.lookup_query(args, sql)
68
+ query.validate_select_query
69
+ connection.exec_query(query, args)
37
70
  end
38
71
 
39
- def attributes(attr_name, format)
40
- formatters.store(attr_name, format)
72
+ def fetch_list(sql, args)
73
+ list = args.first
74
+ fetch(sql_with_list(sql, list), args.drop(1))
41
75
  end
42
76
 
43
- def query(name, body)
44
- if defined_method_name?(name)
45
- raise DB2Query::Error, "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."
77
+ private
78
+ def new_definitions
79
+ definition_class = "Definitions::#{name}Definitions"
80
+ Object.const_get(definition_class).new(
81
+ query_arguments_map,
82
+ field_types_map
83
+ )
84
+ rescue Exception => e
85
+ raise Db2Query::Error, e.message
48
86
  end
49
87
 
50
- if body.respond_to?(:call)
51
- singleton_class.define_method(name) do |*args|
52
- body.call(*args)
88
+ def reset_id_when_required(query)
89
+ if query.insert_sql? && !query.column_id.nil?
90
+ connection.reset_id_sequence!(query.table_name)
53
91
  end
54
- elsif body.is_a?(String)
55
- sql = body
56
- singleton_class.define_method(name) do |*args|
57
- connection.exec_query(formatters, sql, args)
58
- end
59
- else
60
- raise DB2Query::Error, "The query body needs to be callable or is a sql string"
61
- end
62
- end
63
-
64
- private
65
- def formatters
66
- @formatters ||= Hash.new
67
92
  end
68
93
 
69
- def defined_method_name?(name)
70
- self.class.method_defined?(name) || self.class.private_method_defined?(name)
94
+ def define_sql_query(method_name)
95
+ sql_query_name = sql_query_symbol(method_name)
96
+ sql_statement = allocate.method(sql_query_name).call
97
+ define(method_name, sql_statement)
71
98
  end
72
99
 
73
100
  def method_missing(method_name, *args, &block)
74
- sql_methods = self.instance_methods.grep(/_sql/)
75
- sql_method = "#{method_name}_sql".to_sym
76
-
77
- if sql_methods.include?(sql_method)
78
- sql_statement = allocate.method(sql_method).call
79
-
80
- unless sql_statement.is_a? String
81
- raise DB2Query::Error, "Query methods must return a SQL statement string!"
82
- end
83
-
84
- query(method_name, sql_statement)
85
-
101
+ if sql_query_method?(method_name)
102
+ define_sql_query(method_name)
86
103
  method(method_name).call(*args)
87
- elsif connection.respond_to?(method_name)
88
- connection.send(method_name, *args)
104
+ elsif method_name == :exec_query
105
+ sql, args = [args.shift, args.first]
106
+ query = definitions.lookup_query(args, sql)
107
+ exec_query_result(query, args)
89
108
  else
90
109
  super
91
110
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class DbClient
5
+ attr_reader :dsn
6
+
7
+ include ActiveModel::Type::Helpers::Timezone
8
+
9
+ delegate :run, :do, to: :client
10
+
11
+ def initialize(config)
12
+ @dsn = config[:dsn]
13
+ @idle_time_limit = config[:idle] || 5
14
+ @client = new_client
15
+ @last_transaction = Time.now
16
+ end
17
+
18
+ def expire?
19
+ Time.now - @last_transaction > 60 * @idle_time_limit
20
+ end
21
+
22
+ def active?
23
+ @client.connected?
24
+ end
25
+
26
+ def connected_and_persist?
27
+ active? && !expire?
28
+ end
29
+
30
+ def disconnect!
31
+ @client.drop_all
32
+ @client.disconnect if active?
33
+ @client = nil
34
+ end
35
+
36
+ def new_client
37
+ ODBC.connect(dsn).tap do |odbc_conn|
38
+ odbc_conn.use_time = true
39
+ odbc_conn.use_utc = is_utc?
40
+ end
41
+ rescue ::ODBC::Error => e
42
+ raise Db2Query::ConnectionError.new(e.message)
43
+ end
44
+
45
+ def reconnect!
46
+ disconnect!
47
+ @client = new_client
48
+ end
49
+
50
+ def client
51
+ reconnect! unless connected_and_persist?
52
+ @last_transaction = Time.now
53
+ @client
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module DbConnection
5
+ def self.included(base)
6
+ base.send(:extend, ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ mattr_reader :connection
11
+ @@connection = nil
12
+
13
+ def new_database_connection
14
+ @@connection = Connection.new(config)
15
+ end
16
+ end
17
+ end
18
+
19
+ class Connection
20
+ class Pool < ConnectionPool
21
+ def initialize(config, &block)
22
+ super(config, &block)
23
+ end
24
+
25
+ def current_state
26
+ { size: self.size, available: self.available }
27
+ end
28
+
29
+ def disconnect!
30
+ shutdown { |client| client.disconnect! }
31
+ end
32
+
33
+ def reload
34
+ super { |client| client.disconnect! }
35
+ end
36
+ end
37
+
38
+ attr_reader :config, :connection_pool, :instrumenter, :lock
39
+
40
+ delegate :with, :current_state, :disconnect!, :reload, to: :connection_pool
41
+ delegate :instrument, to: :instrumenter
42
+ delegate :synchronize, to: :lock
43
+
44
+ include Logger
45
+ include DbStatements
46
+
47
+ def initialize(config)
48
+ @config = config
49
+ @instrumenter = ActiveSupport::Notifications.instrumenter
50
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
51
+ @connection_pool = nil
52
+ create_connection_pool
53
+ end
54
+
55
+ alias pool with
56
+
57
+ def pool_config
58
+ { size: config[:pool], timeout: config[:timeout] }
59
+ end
60
+
61
+ def create_connection_pool
62
+ synchronize do
63
+ return @connection_pool if @connection_pool
64
+ @connection_pool = Pool.new(pool_config) { DbClient.new(config) }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module DbStatements
5
+ def query(sql)
6
+ pool do |client|
7
+ stmt = client.run(sql)
8
+ stmt.to_a
9
+ ensure
10
+ stmt.drop unless stmt.nil?
11
+ end
12
+ end
13
+
14
+ def query_rows(sql)
15
+ query(sql)
16
+ end
17
+
18
+ def query_value(sql)
19
+ rows = query(sql)
20
+ row = rows.first
21
+ row && row.first
22
+ end
23
+
24
+ def query_values(sql)
25
+ query(sql).map(&:first)
26
+ end
27
+
28
+ def execute(sql, args = [])
29
+ pool do |client|
30
+ client.do(sql, *args)
31
+ end
32
+ end
33
+
34
+ def raw_query(sql, args = [])
35
+ pool do |client|
36
+ stmt = client.run(sql, *args)
37
+ raw_result(stmt)
38
+ end
39
+ end
40
+
41
+ def exec_query(query, args = [])
42
+ sql, binds, args = query.exec_query_arguments(args)
43
+ log(sql, binds, args) do
44
+ pool do |client|
45
+ stmt = client.run(sql, *args)
46
+ columns = stmt.columns.values.map { |col| col.name.downcase }
47
+ rows = stmt.to_a
48
+ Db2Query::Result.new(columns, rows, query)
49
+ ensure
50
+ stmt.drop unless stmt.nil?
51
+ end
52
+ end
53
+ end
54
+
55
+ def reset_id_sequence!(table_name)
56
+ next_val = max_id(table_name) + 1
57
+ execute <<-SQL
58
+ ALTER TABLE #{table_name}
59
+ ALTER COLUMN ID
60
+ RESTART WITH #{next_val}
61
+ SET INCREMENT BY 1
62
+ SET NO CYCLE
63
+ SET CACHE 500
64
+ SET NO ORDER;
65
+ SQL
66
+ end
67
+
68
+ private
69
+ def max_id(table_name)
70
+ query_value("SELECT COALESCE(MAX (ID),0) FROM #{table_name}")
71
+ end
72
+
73
+ def raw_result(stmt)
74
+ columns = stmt.columns.values.map { |col| col.name.downcase }
75
+ stmt.to_a.map do |row|
76
+ index, hash = [0, {}]
77
+ while index < columns.length
78
+ hash[columns[index].to_sym] = row[index]
79
+ index += 1
80
+ end
81
+ hash
82
+ end
83
+ ensure
84
+ stmt.drop unless stmt.nil?
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Definitions
5
+ attr_accessor :types, :types_map
6
+ attr_reader :arguments_map
7
+
8
+ def initialize(query_arguments_map, field_types_map)
9
+ @arguments_map = query_arguments_map
10
+ @types_map = field_types_map
11
+ describe
12
+ initialize_types
13
+ end
14
+
15
+ def describe
16
+ raise Db2Query::Error, "Please describe query definitions at #{self.class.name}"
17
+ end
18
+
19
+ def queries
20
+ @queries ||= {}
21
+ end
22
+
23
+ def query_definition(query_name, &block)
24
+ definition = Query.new(query_name)
25
+ yield definition
26
+ queries[query_name] = definition
27
+ end
28
+
29
+ def lookup(query_name)
30
+ queries.fetch(query_name)
31
+ rescue
32
+ raise Db2Query::QueryDefinitionError.new(name, query_name)
33
+ end
34
+
35
+ def lookup_query(*args)
36
+ query_name, sql = query_definitions(args)
37
+ lookup(query_name).tap do |query|
38
+ query.define_sql(sql)
39
+ query.argument_keys.each do |key|
40
+ key, key_def = query_arg_key(query, key)
41
+ query.argument_types.store(key, data_type_instance(key_def))
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+ def initialize_types
48
+ queries.each do |query_name, definition|
49
+ definition.columns.each do |column, col_def|
50
+ definition.types.store(column, data_type_instance(col_def))
51
+ end
52
+ end
53
+ end
54
+
55
+ def new_data_type(klass, options)
56
+ options.nil? ? klass.new : klass.new(**options)
57
+ rescue Exception => e
58
+ raise Db2Query::Error, e.message
59
+ end
60
+
61
+ def data_type_instance(column_definition)
62
+ data_type, options = column_definition
63
+ klass = @types_map.fetch(data_type)
64
+ new_data_type(klass, options)
65
+ rescue
66
+ raise Db2Query::Error, "Not supported `#{data_type}` data type"
67
+ end
68
+
69
+ def fetch_query_name(args)
70
+ placeholder = args.pop
71
+ placeholder.fetch(:query_name)
72
+ rescue
73
+ raise Db2Query::ImplementationError.new
74
+ end
75
+
76
+ def query_definitions(args)
77
+ case args.first
78
+ when Array
79
+ query_name = fetch_query_name(args.first)
80
+ [query_name, args.last]
81
+ else args
82
+ end
83
+ end
84
+
85
+ def query_arg_key(query, key)
86
+ [key, unless arguments_map[query.query_name].nil?
87
+ arguments_map[query.query_name][key]
88
+ else
89
+ query.columns[key]
90
+ end]
91
+ end
92
+ end
93
+ end
@@ -1,16 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DB2Query
3
+ module Db2Query
4
4
  class Error < StandardError
5
5
  end
6
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
7
+ class ArgumentError < StandardError
8
+ def initialize(given, expected)
9
+ @given = given
10
+ @expected = expected
11
+ super(message)
12
12
  end
13
13
 
14
- attr_reader :sql, :binds
14
+ def message
15
+ "Wrong number of arguments (given #{@given}, expected #{@expected})"
16
+ end
17
+ end
18
+
19
+ class ColumnError < StandardError
20
+ def initialize(def_cols, res_cols)
21
+ @def_cols = def_cols
22
+ @res_cols = res_cols
23
+ super(message)
24
+ end
25
+
26
+ def message
27
+ "Wrong number of columns (query definitions #{@def_cols}, query result #{@res_cols})"
28
+ end
29
+ end
30
+
31
+ class ConnectionError < StandardError
32
+ def initialize(odbc_message)
33
+ @odbc_message = odbc_message
34
+ super(message)
35
+ end
36
+
37
+ def message
38
+ "Unable to activate ODBC DSN connection #{@odbc_message}"
39
+ end
40
+ end
41
+
42
+ class ExtensionError < StandardError
43
+ end
44
+
45
+ class ImplementationError < StandardError
46
+ def message
47
+ "Method `fetch`, `fetch_list`, and `exec_query` can only be implemented inside a lambda query"
48
+ end
49
+ end
50
+
51
+ class ListTypeError < StandardError
52
+ end
53
+
54
+ class MissingListError < StandardError
55
+ end
56
+
57
+ class QueryDefinitionError < StandardError
58
+ def initialize(klass, query_name = nil, column = nil)
59
+ @klass = klass
60
+ @query_name = query_name
61
+ @column = column
62
+ super(message)
63
+ end
64
+
65
+ def message
66
+ if @query_name.nil?
67
+ "Definitions::#{@klass}Definitions file not found."
68
+ elsif @column.nil?
69
+ "No query definition found for #{@klass}:#{@query_name}"
70
+ else
71
+ "Column `#{@column}` not found at `#{@klass} query:#{@query_name}` Query Definitions."
72
+ end
73
+ end
74
+ end
75
+
76
+ class QueryMethodError < StandardError
77
+ def message
78
+ "The query body needs to be callable or is a SQL statement string"
79
+ end
15
80
  end
16
81
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module FieldType
5
+ DEFAULT_FIELD_TYPES = {
6
+ binary: Db2Query::Type::Binary,
7
+ boolean: Db2Query::Type::Boolean,
8
+ string: Db2Query::Type::String,
9
+ varchar: Db2Query::Type::String,
10
+ longvarchar: Db2Query::Type::String,
11
+ decimal: Db2Query::Type::Decimal,
12
+ integer: Db2Query::Type::Integer,
13
+ date: Db2Query::Type::Date,
14
+ time: Db2Query::Type::Time,
15
+ timestamp: Db2Query::Type::Timestamp
16
+ }
17
+
18
+ def self.included(base)
19
+ base.send(:extend, ClassMethods)
20
+ end
21
+
22
+ module ClassMethods
23
+ mattr_reader :field_types_map
24
+ @@field_types_map = nil
25
+
26
+ def set_field_types(types = DEFAULT_FIELD_TYPES)
27
+ @@field_types_map = types
28
+ end
29
+ end
30
+ end
31
+ end