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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +453 -110
- data/Rakefile +3 -2
- data/lib/db2_query.rb +41 -15
- data/lib/db2_query/base.rb +16 -5
- data/lib/db2_query/config.rb +20 -17
- data/lib/db2_query/core.rb +68 -55
- data/lib/db2_query/db_client.rb +56 -0
- data/lib/db2_query/db_connection.rb +67 -0
- data/lib/db2_query/db_statements.rb +87 -0
- data/lib/db2_query/definitions.rb +79 -0
- data/lib/db2_query/error.rb +81 -0
- data/lib/db2_query/field_type.rb +31 -0
- data/lib/db2_query/helper.rb +49 -0
- data/lib/db2_query/logger.rb +52 -0
- data/lib/db2_query/query.rb +117 -0
- data/lib/db2_query/quoting.rb +102 -0
- data/lib/db2_query/railtie.rb +5 -7
- data/lib/db2_query/result.rb +51 -31
- data/lib/db2_query/sql_statement.rb +34 -0
- data/lib/db2_query/tasks.rb +29 -0
- data/lib/db2_query/tasks/database.rake +2 -50
- data/lib/db2_query/tasks/init.rake +1 -1
- data/lib/db2_query/tasks/initializer.rake +2 -34
- data/lib/db2_query/tasks/templates/database.rb.tt +19 -0
- data/lib/db2_query/tasks/templates/initializer.rb.tt +8 -0
- data/lib/db2_query/type/binary.rb +19 -0
- data/lib/db2_query/type/boolean.rb +41 -0
- data/lib/db2_query/type/date.rb +34 -0
- data/lib/db2_query/type/decimal.rb +15 -0
- data/lib/db2_query/type/integer.rb +15 -0
- data/lib/db2_query/type/string.rb +30 -0
- data/lib/db2_query/type/text.rb +11 -0
- data/lib/db2_query/type/time.rb +30 -0
- data/lib/db2_query/type/timestamp.rb +30 -0
- data/lib/db2_query/type/value.rb +29 -0
- data/lib/db2_query/version.rb +2 -2
- data/lib/rails/generators/query/USAGE +15 -0
- data/lib/rails/generators/query/query_generator.rb +70 -0
- data/lib/rails/generators/query/templates/query.rb.tt +26 -0
- data/lib/rails/generators/query/templates/query_definitions.rb.tt +12 -0
- data/lib/rails/generators/query/templates/unit_test.rb.tt +9 -0
- metadata +62 -49
- data/lib/db2_query/connection.rb +0 -163
- data/lib/db2_query/connection_handling.rb +0 -112
- data/lib/db2_query/database_statements.rb +0 -93
- data/lib/db2_query/formatter.rb +0 -27
- 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
|