db2_query 0.2.2 → 0.3.2

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 +453 -110
  4. data/Rakefile +3 -2
  5. data/lib/db2_query.rb +41 -15
  6. data/lib/db2_query/base.rb +16 -5
  7. data/lib/db2_query/config.rb +20 -17
  8. data/lib/db2_query/core.rb +68 -55
  9. data/lib/db2_query/db_client.rb +56 -0
  10. data/lib/db2_query/db_connection.rb +67 -0
  11. data/lib/db2_query/db_statements.rb +87 -0
  12. data/lib/db2_query/definitions.rb +79 -0
  13. data/lib/db2_query/error.rb +81 -0
  14. data/lib/db2_query/field_type.rb +31 -0
  15. data/lib/db2_query/helper.rb +49 -0
  16. data/lib/db2_query/logger.rb +52 -0
  17. data/lib/db2_query/query.rb +117 -0
  18. data/lib/db2_query/quoting.rb +102 -0
  19. data/lib/db2_query/railtie.rb +5 -7
  20. data/lib/db2_query/result.rb +51 -31
  21. data/lib/db2_query/sql_statement.rb +34 -0
  22. data/lib/db2_query/tasks.rb +29 -0
  23. data/lib/db2_query/tasks/database.rake +2 -50
  24. data/lib/db2_query/tasks/init.rake +1 -1
  25. data/lib/db2_query/tasks/initializer.rake +2 -34
  26. data/lib/db2_query/tasks/templates/database.rb.tt +19 -0
  27. data/lib/db2_query/tasks/templates/initializer.rb.tt +8 -0
  28. data/lib/db2_query/type/binary.rb +19 -0
  29. data/lib/db2_query/type/boolean.rb +41 -0
  30. data/lib/db2_query/type/date.rb +34 -0
  31. data/lib/db2_query/type/decimal.rb +15 -0
  32. data/lib/db2_query/type/integer.rb +15 -0
  33. data/lib/db2_query/type/string.rb +30 -0
  34. data/lib/db2_query/type/text.rb +11 -0
  35. data/lib/db2_query/type/time.rb +30 -0
  36. data/lib/db2_query/type/timestamp.rb +30 -0
  37. data/lib/db2_query/type/value.rb +29 -0
  38. data/lib/db2_query/version.rb +2 -2
  39. data/lib/rails/generators/query/USAGE +15 -0
  40. data/lib/rails/generators/query/query_generator.rb +70 -0
  41. data/lib/rails/generators/query/templates/query.rb.tt +26 -0
  42. data/lib/rails/generators/query/templates/query_definitions.rb.tt +12 -0
  43. data/lib/rails/generators/query/templates/unit_test.rb.tt +9 -0
  44. metadata +62 -49
  45. data/lib/db2_query/connection.rb +0 -163
  46. data/lib/db2_query/connection_handling.rb +0 -112
  47. data/lib/db2_query/database_statements.rb +0 -93
  48. data/lib/db2_query/formatter.rb +0 -27
  49. data/lib/db2_query/odbc_connector.rb +0 -40
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Definitions
5
+ attr_accessor :types, :types_map
6
+
7
+ def initialize(types_map)
8
+ @types_map = types_map
9
+ describe
10
+ initialize_types
11
+ end
12
+
13
+ def describe
14
+ raise Db2Query::Error, "Please describe query definitions at #{self.class.name}"
15
+ end
16
+
17
+ def queries
18
+ @queries ||= {}
19
+ end
20
+
21
+ def query_definition(query_name, &block)
22
+ definition = Query.new(query_name)
23
+ yield definition
24
+ queries[query_name] = definition
25
+ end
26
+
27
+ def lookup(query_name)
28
+ queries.fetch(query_name)
29
+ rescue
30
+ raise Db2Query::QueryDefinitionError.new(name, query_name)
31
+ end
32
+
33
+ def lookup_query(*args)
34
+ query_name, sql = query_definitions(args)
35
+ lookup(query_name).tap do |query|
36
+ query.define_sql(sql)
37
+ end
38
+ end
39
+
40
+ private
41
+ def initialize_types
42
+ queries.each do |query_name, definition|
43
+ definition.columns.each do |column, col_def|
44
+ definition.types.store(column, data_type_instance(col_def))
45
+ end
46
+ end
47
+ end
48
+
49
+ def new_data_type(klass, options)
50
+ options.nil? ? klass.new : klass.new(**options)
51
+ rescue Exception => e
52
+ raise Db2Query::Error, e.message
53
+ end
54
+
55
+ def data_type_instance(column_definition)
56
+ data_type, options = column_definition
57
+ klass = @types_map.fetch(data_type)
58
+ new_data_type(klass, options)
59
+ rescue
60
+ raise Db2Query::Error, "Not supported `#{data_type}` data type"
61
+ end
62
+
63
+ def fetch_query_name(args)
64
+ placeholder = args.pop
65
+ placeholder.fetch(:query_name)
66
+ rescue
67
+ raise Db2Query::ImplementationError.new
68
+ end
69
+
70
+ def query_definitions(args)
71
+ case args.first
72
+ when Array
73
+ query_name = fetch_query_name(args.first)
74
+ [query_name, args.last]
75
+ else args
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ArgumentError < StandardError
8
+ def initialize(given, expected)
9
+ @given = given
10
+ @expected = expected
11
+ super(message)
12
+ end
13
+
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 ExtentionError < 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
80
+ end
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
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module Helper
5
+ def self.included(base)
6
+ base.send(:extend, ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def sql_with_list(sql, list)
11
+ if sql.scan(/\@list+/).length == 0
12
+ raise Db2Query::MissingListError, "Missing @list pointer at SQL"
13
+ elsif !list.is_a?(Array)
14
+ raise Db2Query::ListTypeError, "The arguments should be an array of list"
15
+ end
16
+ sql.gsub("@list", "'#{list.join("', '")}'")
17
+ end
18
+
19
+ def sql_with_extention(sql, extention)
20
+ if sql.scan(/\@extention+/).length == 0
21
+ raise Db2Query::ExtentionError, "Missing @extention pointer at SQL"
22
+ end
23
+ sql.gsub("@extention", extention.strip)
24
+ end
25
+
26
+ private
27
+ def sql_query_methods
28
+ self.instance_methods.grep(/_sql/)
29
+ end
30
+
31
+ def sql_query_symbol(method_name)
32
+ "#{method_name}_sql".to_sym
33
+ end
34
+
35
+ def sql_query_method?(method_name)
36
+ sql_query_name = sql_query_symbol(method_name)
37
+ sql_query_methods.include?(sql_query_name)
38
+ end
39
+
40
+ def validate_sql(sql)
41
+ raise Db2Query::Error, "SQL have to be in string format" unless sql.is_a?(String)
42
+ end
43
+
44
+ def fetch_error_message
45
+ "`fetch`, `fetch_list` and `fetch_extention` methods applied for SQL `select` statement only."
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
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, binds = [], args = [], &block)
16
+ instrument(
17
+ "sql.active_record",
18
+ sql: sql,
19
+ name: "SQL",
20
+ binds: binds,
21
+ type_casted_binds: args,
22
+ statement_name: nil,
23
+ connection_id: object_id,
24
+ connection: self) do
25
+ 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
+
42
+ class StatementInvalid < ActiveRecord::ActiveRecordError
43
+ def initialize(message = nil, sql: nil, binds: nil)
44
+ super(message || $!.try(:message))
45
+ @sql = sql
46
+ @binds = binds
47
+ end
48
+
49
+ attr_reader :sql, :binds
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Query
5
+ attr_reader :columns, :keys, :query_name, :sql, :types
6
+
7
+ include SqlStatement
8
+
9
+ def initialize(query_name = nil)
10
+ @columns = {}
11
+ @query_name = query_name
12
+ @sql_statement = nil
13
+ @types = {}
14
+ end
15
+
16
+ def define_sql(sql)
17
+ @keys ||= new_keys(sql)
18
+ @sql ||= sql.tr("$", "")
19
+ end
20
+
21
+ def map_column(name, args)
22
+ @columns[name] = args
23
+ end
24
+
25
+ def method_missing(method_name, *args, &block)
26
+ map_column(method_name, args)
27
+ end
28
+
29
+ def data_type(key)
30
+ column = column_from_key(key)
31
+ types.fetch(column.to_sym)
32
+ rescue
33
+ raise Db2Query::Error, "No column #{column} found at query: #{query_name} definitions"
34
+ end
35
+
36
+ def length
37
+ columns.length
38
+ end
39
+
40
+ def raw_query_args(args)
41
+ case args
42
+ when Array, Hash
43
+ validated_args(args)
44
+ else
45
+ args
46
+ end
47
+ end
48
+
49
+ def define_args(args)
50
+ class_eval { attr_accessor "args" }
51
+ send("args=", raw_query_args(args))
52
+ end
53
+
54
+ def sorted_args(args)
55
+ keys.map.with_index do |key, index|
56
+ serialized_arg(args.is_a?(Hash) ? args[key] : args[index], key)
57
+ end
58
+ end
59
+
60
+ def column_id
61
+ columns.fetch(:id, nil)
62
+ end
63
+
64
+ def validate_result_columns(result_columns)
65
+ res_cols, def_cols = [result_columns.length, length]
66
+ if res_cols != def_cols
67
+ raise Db2Query::ColumnError.new(def_cols, res_cols)
68
+ end
69
+ end
70
+
71
+ def exec_query_arguments(args)
72
+ [db2_spec_sql, binds(args), validated_args(args)]
73
+ end
74
+
75
+ def validate_select_query
76
+ if iud_sql?
77
+ raise Db2Query::Error, "Fetch queries are used for select statement query only."
78
+ end
79
+ end
80
+
81
+ class Bind < Struct.new(:name, :value)
82
+ end
83
+
84
+ private
85
+ def new_keys(raw_sql)
86
+ raw_sql.scan(/\$\S+/).map { |key| key.gsub!(/[$=,)]/, "").to_sym }
87
+ end
88
+
89
+ def serialized_arg(arg, key)
90
+ query_name.nil? ? arg : data_type(key).serialize(arg)
91
+ end
92
+
93
+ def column_from_key(key)
94
+ "#{key}".split(".").last.downcase
95
+ end
96
+
97
+ def new_bind(key, arg)
98
+ [Bind.new(column_from_key(key), arg), arg]
99
+ end
100
+
101
+ def binds(args)
102
+ keys.map.with_index do |key, index|
103
+ new_bind(key, args.first.is_a?(Hash)? args.first[key] : args[index])
104
+ end
105
+ end
106
+
107
+ def validate_arguments(given, expected)
108
+ raise Db2Query::ArgumentError.new(given, expected) unless given == expected
109
+ end
110
+
111
+ def validated_args(args)
112
+ args = args.first.is_a?(Hash) ? args.first : args
113
+ validate_arguments(args.length, keys.length)
114
+ sorted_args(args)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module Quoting
5
+ def self.included(base)
6
+ base.send(:extend, ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def quoted_true
11
+ "TRUE"
12
+ end
13
+
14
+ def unquoted_true
15
+ 1
16
+ end
17
+
18
+ def quoted_false
19
+ "FALSE"
20
+ end
21
+
22
+ def unquoted_false
23
+ 0
24
+ end
25
+
26
+ def quoted_binary(value)
27
+ "x'#{value.hex}'"
28
+ end
29
+
30
+ def quoted_time(value)
31
+ value = value.change(year: 2000, month: 1, day: 1)
32
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "")
33
+ end
34
+
35
+ def quoted_date(value)
36
+ if value.acts_like?(:time)
37
+ if ActiveRecord::Base.default_timezone == :utc
38
+ value = value.getutc if !value.utc?
39
+ else
40
+ value = value.getlocal
41
+ end
42
+ end
43
+
44
+ result = value.to_s(:db)
45
+ if value.respond_to?(:usec) && value.usec > 0
46
+ result << "." << sprintf("%06d", value.usec)
47
+ else
48
+ result
49
+ end
50
+ end
51
+
52
+ private
53
+ def _quote(value)
54
+ case value
55
+ when String, Symbol, ActiveSupport::Multibyte::Chars
56
+ "'#{quote_string(value.to_s)}'"
57
+ when true
58
+ quoted_true
59
+ when false
60
+ quoted_false
61
+ when nil
62
+ "NULL"
63
+ when BigDecimal
64
+ value.to_s("F")
65
+ when Numeric, ActiveSupport::Duration
66
+ value.to_s
67
+ when Db2Query::Type::Binary::Data
68
+ quoted_binary(value)
69
+ when ActiveRecord::Type::Time::Value
70
+ "'#{quoted_time(value)}'"
71
+ when Date, Time
72
+ "'#{quoted_date(value)}'"
73
+ when Class
74
+ "'#{value}'"
75
+ else raise TypeError, "can't quote #{value.class.name}"
76
+ end
77
+ end
78
+
79
+ def _type_cast(value)
80
+ case value
81
+ when Symbol, ActiveSupport::Multibyte::Chars
82
+ value.to_s
83
+ when Db2Query::Type::Binary::Data
84
+ value.hex
85
+ when true
86
+ unquoted_true
87
+ when false
88
+ unquoted_false
89
+ when BigDecimal
90
+ value.to_s("F")
91
+ when nil, Numeric, String
92
+ value
93
+ when ActiveRecord::Type::Time::Value
94
+ quoted_time(value)
95
+ when Date, Time
96
+ quoted_date(value)
97
+ else raise TypeError, "can't cast #{value.class.name}"
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end