db2_query 0.2.3 → 0.3.3

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