db2_query 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5d2946448234d56484778d59e3051573c81c2789631776f0ed543a79fdc510fa
4
+ data.tar.gz: a257fabf1a7de500f7d325c70dd11f494a08163dbf78460f61b3bf51cb626b77
5
+ SHA512:
6
+ metadata.gz: ca8c1b52138ec97d9f4e9cbbca0d8bb148e95f4ac181a929e3e5715b597ed59b70e25b779156ba45a7fe9f119203e4162a2a932c63d11c94acdc2a1eaeb56db8
7
+ data.tar.gz: 67f0d7228274d4a7a6e38771cc0743b83da27621d3ce310e0d5d7d323e937316f700ec3b278734312ff27cdc88796321cb09f02d7c2bf48b8c5607a7331a7f27
@@ -0,0 +1,20 @@
1
+ Copyright 2020 yohanes_l
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,124 @@
1
+ # Db2Query
2
+ A Rails query plugin to fetch data from Db2 database by using ODBC connection.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'db2_query'
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle
14
+ ```
15
+
16
+ Or install it yourself as:
17
+ ```bash
18
+ $ gem install db2-query
19
+ ```
20
+
21
+ ## Initialization
22
+ Execute init task at the app root
23
+ ```bash
24
+ $ rake db2query:init
25
+ ```
26
+ Db2Query will generate two required files:
27
+ - `config/db2query_database.yml`
28
+ - `config/initializers/db2query`
29
+
30
+ Edit these files according to the requirement.
31
+
32
+ ### Database Configuration
33
+ At `db2query_database.yml` we can use two type of connection:
34
+ 1. DSN connection config
35
+ 2. Connection String config
36
+ ```yml
37
+ development:
38
+ primary: # Connection String Example
39
+ conn_string:
40
+ driver: DB2
41
+ database: SAMPLE
42
+ dbalias: SAMPLE
43
+ hostname: LOCALHOST
44
+ currentschema: LIBTEST
45
+ port: "0"
46
+ protocol: IPC
47
+ uid: <%= ENV["DB2EC_UID"] %>
48
+ pwd: <%= ENV["DB2EC_PWD"] %>
49
+ secondary: # DSN Example
50
+ dsn: SAMPLE
51
+ uid: <%= ENV["DB2EC_UID"] %>
52
+ pwd: <%= ENV["DB2EC_PWD"] %>
53
+ ```
54
+
55
+ ## Usage
56
+ ### Basic Usage
57
+ Create query class that inherit from `Db2Query::Base` in `app/queries` folder
58
+ ```ruby
59
+ class Users < Db2Query::Base
60
+ query :find_by, <<-SQL
61
+ SELECT * FROM LIBTEST.USERS WHERE user_id = ?
62
+ SQL
63
+ end
64
+ ```
65
+ Or use a normal sql method (don't forget the `_sql` suffix)
66
+ ```ruby
67
+ class Users < Db2Query::Base
68
+ def find_by_sql
69
+ "SELECT * FROM LIBTEST.USERS WHERE user_id = ?"
70
+ end
71
+ end
72
+ ```
73
+ Check it at rails console
74
+ ```bash
75
+ Users.find_by 10000
76
+ Users Load (330.28ms) SELECT * FROM LIBTEST.USERS WHERE user_id = ? [[10000]]
77
+ => #<Db2Query::Result [#<Users::FindBy user_id: 10000, first_name: "Alex", last_name: "Jacobi", email: "lula_durgan@dooley.com">]>
78
+ ```
79
+ ### Formatter
80
+ In order to get different result column format, a query result can be reformatted by add a formatter class that inherit `Db2Query::AbstractFormatter` then register at `config\initializers\db2query.rb`
81
+ ```ruby
82
+ require "db2_query/formatter"
83
+
84
+ # create a formatter class
85
+ class FirstNameFormatter < Db2Query::AbstractFormatter
86
+ def format(value)
87
+ "Mr/Mrs. " + value
88
+ end
89
+ end
90
+
91
+ # register the formatter class
92
+ Db2Query::Formatter.registration do |format|
93
+ format.register(:first_name_formatter, FirstNameFormatter)
94
+ end
95
+ ```
96
+ Use it at query class
97
+ ```ruby
98
+ class Users < Db2Query::Base
99
+ attributes :first_name, :first_name_formatter
100
+
101
+ query :find_by, <<-SQL
102
+ SELECT * FROM LIBTEST.USERS WHERE user_id = ?
103
+ SQL
104
+ end
105
+ ```
106
+ Check it at rails console
107
+ ```bash
108
+ Users.find_by 10000
109
+ Users Load (330.28ms) SELECT * FROM LIBTEST.USERS WHERE user_id = ? [[10000]]
110
+ => #<Db2Query::Result [#<Users::FindBy user_id: 10000, first_name: "Mr/Mrs. Alex", last_name: "Jacobi", email: "lula_durgan@dooley.com">]>
111
+ ```
112
+ ### Available methods
113
+ Db2Query::Result has public methods as follows:
114
+ - to_a
115
+ - to_hash
116
+ - pluck
117
+ - first
118
+ - last
119
+ - size
120
+ - each
121
+
122
+
123
+ ## License
124
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ rescue LoadError
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
+ end
8
+
9
+ require "rdoc/task"
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Db2Query"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
+ end
18
+
19
+ require "bundler/gem_tasks"
20
+
21
+ require "rake/testtask"
22
+
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << "test"
25
+ t.pattern = "test/**/*_test.rb"
26
+ t.verbose = false
27
+ end
28
+
29
+ task default: :test
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal:true
2
+
3
+ require "odbc"
4
+ require "yaml"
5
+ require "erb"
6
+ require "active_record"
7
+ require "active_support"
8
+
9
+ module Db2Query
10
+ extend ActiveSupport::Autoload
11
+
12
+ autoload :Version
13
+ autoload :Error
14
+ autoload :Path
15
+ autoload :Schema
16
+ autoload :DatabaseConfigurations
17
+ autoload :ODBCConnector
18
+ autoload :Connection
19
+ autoload :ConnectionHandling
20
+ autoload :DatabaseStatements
21
+ autoload :SQLValidator
22
+ autoload :LogSubscriber
23
+ autoload :Formatter
24
+ autoload :Column
25
+ autoload :Result
26
+ autoload :Base
27
+ end
28
+
29
+ require "db2_query/railtie"
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Base
5
+ include DatabaseConfigurations
6
+ include ConnectionHandling
7
+
8
+ class << self
9
+ include SQLValidator
10
+
11
+ def attributes(attr_name, format)
12
+ attr_format.store(attr_name, format)
13
+ end
14
+
15
+ def query(method_name, sql_statement)
16
+ unless is_query?(sql_statement)
17
+ raise Error, "Query only for SQL query commands."
18
+ end
19
+
20
+ if self.class.respond_to?(method_name)
21
+ raise Error, "Query :#{method_name} has been defined before"
22
+ end
23
+
24
+ self.class.define_method(method_name) do |*args|
25
+ log(sql_statement, args) do
26
+ columns, rows = connection.exec_query(sql_statement, *args)
27
+ Result.new(self, method_name, columns, rows, attr_format)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+ def attr_format
34
+ @attr_format ||= Hash.new
35
+ end
36
+
37
+ def define_query_method(method_name, sql_statement)
38
+ if is_query?(sql_statement)
39
+ query(method_name, sql_statement)
40
+ else
41
+ raise NotImplementedError
42
+ end
43
+ end
44
+
45
+ def method_missing(method_name, *args, &block)
46
+ sql_methods = self.instance_methods.grep(/_sql/)
47
+ sql_method = "#{method_name}_sql".to_sym
48
+
49
+ if sql_methods.include?(sql_method)
50
+ sql_statement = allocate.method(sql_method).call
51
+
52
+ raise Error, "Query methods must return a SQL statement string!" unless sql_statement.is_a? String
53
+
54
+ expected_args = sql_statement.count "?"
55
+ given_args = args.size
56
+
57
+ if expected_args == given_args
58
+ define_query_method(method_name, sql_statement)
59
+ else
60
+ raise ArgumentError, "wrong number of arguments (given #{given_args}, expected #{expected_args})"
61
+ end
62
+
63
+ method(method_name).call(*args)
64
+ else
65
+ super
66
+ end
67
+ end
68
+
69
+ def instrumenter
70
+ @instrumenter ||= ActiveSupport::Notifications.instrumenter
71
+ end
72
+
73
+ def log(sql_statement, args)
74
+ instrumenter.instrument(
75
+ "sql.db2_query",
76
+ sql: sql_statement,
77
+ name: self,
78
+ binds: args) do
79
+ yield
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Column
5
+ attr_reader :name, :type, :formatter
6
+
7
+ FLOAT_TYPES = [ODBC::SQL_FLOAT, ODBC::SQL_DOUBLE, ODBC::SQL_DECIMAL, ODBC::SQL_REAL]
8
+ INTEGER_TYPES = [ODBC::SQL_TINYINT, ODBC::SQL_SMALLINT, ODBC::SQL_INTEGER, ODBC::SQL_BIGINT, ODBC::SQL_NUMERIC]
9
+
10
+ def initialize(name, type, format = nil)
11
+ @name = name
12
+ @type = type.to_i
13
+
14
+ @formatter = if custom_format?(format)
15
+ Formatter.lookup(format)
16
+ elsif float_type?
17
+ FloatFormatter.new
18
+ elsif integer_type?
19
+ IntegerFormatter.new
20
+ else
21
+ BareFormatter.new
22
+ end
23
+ end
24
+
25
+ def format(value)
26
+ formatter.format(value)
27
+ end
28
+
29
+ private
30
+ def custom_format?(format)
31
+ Formatter.registry.key?(format)
32
+ end
33
+
34
+ def float_type?
35
+ FLOAT_TYPES.include?(type)
36
+ end
37
+
38
+ def integer_type?
39
+ INTEGER_TYPES.include?(type)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Connection
5
+ include DatabaseStatements
6
+
7
+ attr_reader :connector, :db_name, :odbc_conn
8
+
9
+ def initialize(connector, db_name)
10
+ @connector = connector
11
+ @db_name = db_name.to_sym
12
+ connect
13
+ end
14
+
15
+ def connect
16
+ @odbc_conn = connector.connect
17
+ @odbc_conn.use_time = true
18
+ end
19
+
20
+ def active?
21
+ @odbc_conn.connected?
22
+ end
23
+
24
+ def disconnect!
25
+ @odbc_conn.drop_all
26
+ @odbc_conn.disconnect if active?
27
+ end
28
+
29
+ def reconnect!
30
+ disconnect!
31
+ connect
32
+ end
33
+ alias reset! reconnect!
34
+
35
+ def execute(sql, *args)
36
+ reset! unless active?
37
+ @odbc_conn.do(sql, *args)
38
+ end
39
+
40
+ def exec_query(sql, *args)
41
+ reset! unless active?
42
+ statement = @odbc_conn.run(sql, *args)
43
+ columns = statement.columns.values
44
+ rows = statement.to_a
45
+ [columns, rows]
46
+ ensure
47
+ statement.drop if statement
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module ConnectionHandling
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_DB = "primary"
8
+
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
29
+
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
33
+
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
45
+
46
+ def current_database
47
+ @connection.db_name
48
+ end
49
+
50
+ def clear_connection
51
+ @connection.disconnect!
52
+ @connection = nil
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module DatabaseConfigurations
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence }
8
+
9
+ included do |base|
10
+ @@configurations = Hash.new
11
+
12
+ base.extend ClassMethods
13
+ end
14
+
15
+ module ClassMethods
16
+ def configurations
17
+ @@configurations
18
+ end
19
+
20
+ def load_database_configurations(path = nil)
21
+ file_path = path.nil? ? Path.database_config_file : path
22
+
23
+ if File.exist?(file_path)
24
+ file = File.read(file_path)
25
+ @@configurations = YAML.load(ERB.new(file).result)[DEFAULT_ENV.call]
26
+ else
27
+ raise Error, "Could not load db2query database configuration. No such file - #{file_path}"
28
+ end
29
+ end
30
+
31
+ def configurations_databases
32
+ self.load_database_configurations if self.configurations.nil?
33
+ @@configurations.keys
34
+ end
35
+
36
+ private
37
+ def extract_configuration(db_name)
38
+ configs = @@configurations[db_name.to_s]
39
+ conn_config = configs.nil? ? configs : configs.transform_keys(&:to_sym)
40
+ conn_type = (conn_config.keys & ODBCConnector::CONNECTION_TYPES).first
41
+
42
+ if conn_type.nil?
43
+ raise Error, "No data source name (:dsn) or connection string (:conn_str) provided."
44
+ end
45
+
46
+ [conn_type, conn_config]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module DatabaseStatements
5
+ def query_value(sql) # :nodoc:
6
+ single_value_from_rows(query(sql))
7
+ end
8
+
9
+ def query(sql)
10
+ exec_query(sql).last
11
+ end
12
+
13
+ def query_values(sql)
14
+ query(sql).map(&:first)
15
+ end
16
+
17
+ def current_database
18
+ db_name.to_s
19
+ end
20
+
21
+ def current_schema
22
+ query_value("select current_schema from sysibm.sysdummy1").strip
23
+ end
24
+ alias library current_schema
25
+
26
+ private
27
+ def single_value_from_rows(rows)
28
+ row = rows.first
29
+ row && row.first
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Error < ArgumentError; end
5
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module Formatter
5
+ def self.register(name, klass)
6
+ self.registry.store(name, klass.new)
7
+ end
8
+
9
+ def self.registry
10
+ @@registry ||= Hash.new
11
+ end
12
+
13
+ def self.lookup(name)
14
+ @@registry.fetch(name.to_sym)
15
+ end
16
+
17
+ def self.registration(&block)
18
+ yield self if block_given?
19
+ end
20
+ end
21
+
22
+ class AbstractFormatter
23
+ def format(value)
24
+ raise NotImplementedError, "Implement format method in your subclass."
25
+ end
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
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class LogSubscriber < ActiveSupport::LogSubscriber
5
+ # Embed in a String to clear all previous ANSI sequences.
6
+ CLEAR = "\e[0m"
7
+ BOLD = "\e[1m"
8
+
9
+ # Colors
10
+ BLACK = "\e[30m"
11
+ RED = "\e[31m"
12
+ GREEN = "\e[32m"
13
+ YELLOW = "\e[33m"
14
+ BLUE = "\e[34m"
15
+ MAGENTA = "\e[35m"
16
+ CYAN = "\e[36m"
17
+ WHITE = "\e[37m"
18
+
19
+ def sql(event)
20
+ class_load_duration = color("#{event.payload[:name]} Load (#{event.duration.round(2)}ms)", :cyan, true)
21
+ sql_statement = color("#{event.payload[:sql]}", :blue, true)
22
+ message = " #{class_load_duration} #{sql_statement}"
23
+
24
+ if event.payload[:binds].size > 0
25
+ binds = color("#{event.payload[:binds]}", :white)
26
+ message = "#{message} [#{binds}]"
27
+ end
28
+
29
+ puts message
30
+ end
31
+
32
+ def schema_task(event)
33
+ puts color(" Done (#{(event.duration).round(2)}ms)", :green, true)
34
+ end
35
+
36
+ def schema_task_perform(event)
37
+ task_name = color(":#{event.payload[:task_name]}", :white, true)
38
+ schema = color("#{event.payload[:schema]}", :white, true)
39
+ puts "- Performing #{task_name} in #{schema} ..."
40
+ end
41
+
42
+ private
43
+ def color(text, color, bold = false) # :doc:
44
+ return text unless colorize_logging
45
+ color = self.class.const_get(color.upcase) if color.is_a?(Symbol)
46
+ bold = bold ? BOLD : ""
47
+ "#{bold}#{color}#{text}#{CLEAR}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class ODBCConnector
5
+ attr_reader :connector, :conn_type, :conn_config
6
+
7
+ CONNECTION_TYPES = %i[dsn conn_string].freeze
8
+
9
+ def initialize(type, config)
10
+ @conn_type, @conn_config = type, config.transform_keys(&:to_sym)
11
+ @connector = Db2Query.const_get("#{conn_type.to_s.camelize}Connector").new
12
+ end
13
+
14
+ def connect
15
+ connector.connect(conn_config)
16
+ end
17
+ end
18
+
19
+ class DsnConnector
20
+ def connect(config)
21
+ ::ODBC.connect(config[:dsn], config[:uid], config[:pwd])
22
+ rescue ::ODBC::Error => e
23
+ raise Error, "Unable to activate ODBC DSN connection #{e}"
24
+ end
25
+ end
26
+
27
+ class ConnStringConnector
28
+ def connect(config)
29
+ driver = ::ODBC::Driver.new.tap do |d|
30
+ d.attrs = config[:conn_string].transform_keys(&:to_s)
31
+ d.name = "odbc"
32
+ end
33
+ ::ODBC::Database.new.drvconnect(driver)
34
+ rescue ::ODBC::Error => e
35
+ raise Error, "Unable to activate ODBC Conn String connection #{e}"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module Path
5
+ def self.database_config_file
6
+ @@database_config_file ||= nil
7
+ end
8
+
9
+ def self.database_config_file=(file_path)
10
+ @@database_config_file ||= file_path
11
+ end
12
+
13
+ def self.database_config_file_exists?
14
+ File.exist?(self.database_config_file)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Railtie < ::Rails::Railtie
5
+ railtie_name :db2_query
6
+
7
+ rake_tasks do
8
+ tasks_path = "#{File.expand_path("..", __dir__)}/tasks"
9
+ Dir.glob("#{tasks_path}/*.rake").each { |file| load file }
10
+ end
11
+
12
+ 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
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Result
5
+ attr_reader :core, :klass, :rows, :columns, :column_metadatas, :attr_format
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)
14
+ end
15
+
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) }
33
+ end
34
+ end
35
+
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
+ def inspect
54
+ entries = records.take(11).map!(&:inspect)
55
+ entries[10] = "..." if entries.size == 11
56
+ "#<#{self.class} [#{entries.join(', ')}]>"
57
+ end
58
+
59
+ private
60
+ def records
61
+ @records ||= to_a
62
+ end
63
+
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])
68
+ end
69
+ end
70
+
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
79
+
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
89
+
90
+ "#<#{self.class} #{inspection}>"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ class Schema
5
+ include SQLValidator
6
+
7
+ attr_accessor :config, :current_schema
8
+
9
+ def initialize
10
+ @config = Config.new
11
+ end
12
+
13
+ def connection
14
+ Base.connection
15
+ end
16
+
17
+ def schema
18
+ config.schema
19
+ end
20
+
21
+ def sql_files_dir(path)
22
+ config.sql_files_dir = path
23
+ end
24
+
25
+ def is_valid_schema?
26
+ connection.current_schema == config.schema
27
+ end
28
+
29
+ def perform_tasks(&block)
30
+ raise Error, "#{config.main_library} is not connection's current schema" unless is_valid_schema?
31
+
32
+ if Base.connection.nil? && config.init_connection
33
+ Base.establish_connection
34
+ end
35
+
36
+ puts "\n# Perform Schema Tasks: \n\n"
37
+
38
+ instance_eval(&block)
39
+
40
+ puts "\n"
41
+ end
42
+
43
+ def sql(sql_statement)
44
+ raise Error, "Task only for SQL execute commands." if query_command?(sql_statement)
45
+ sql_statement
46
+ end
47
+
48
+ def file(file_name)
49
+ sql(File.read("#{config.sql_files_dir}/#{file_name}"))
50
+ end
51
+
52
+ def task(task_name, sql = nil, *args)
53
+ Task.new(schema, task_name).perform do
54
+ if block_given?
55
+ yield(sql)
56
+ else
57
+ execute(sql, *args)
58
+ end
59
+ end
60
+ end
61
+
62
+ def execute(sql, *args)
63
+ connection.execute(sql, *args)
64
+ rescue ::ODBC::Error => e
65
+ raise Error, "Unable to execute SQL - #{e}"
66
+ end
67
+
68
+ def tables_in_schema
69
+ connection.query_values <<-SQL
70
+ SELECT table_name FROM SYSIBM.SQLTABLES
71
+ WHERE table_schem='#{schema}' AND table_type='TABLE'
72
+ SQL
73
+ end
74
+
75
+ def self.initiation(&block)
76
+ new.initiation(&block)
77
+ end
78
+
79
+ def initiation(&block)
80
+ instance_eval(&block)
81
+ end
82
+
83
+ class Task
84
+ attr_reader :instrumenter, :schema, :task_name, :start_time, :finish_time
85
+
86
+ def initialize(schema, task_name)
87
+ @schema = schema
88
+ @task_name = task_name
89
+ @instrumenter = ActiveSupport::Notifications.instrumenter
90
+ end
91
+
92
+ def perform
93
+ instrumenter.instrument("schema_task_perform.db2_query", payload)
94
+ instrumenter.start("schema_task.db2_query", payload)
95
+ yield
96
+ instrumenter.finish("schema_task.db2_query", payload)
97
+ end
98
+
99
+ private
100
+ def payload
101
+ { task_name: task_name, schema: schema }
102
+ end
103
+ end
104
+
105
+ class Config
106
+ attr_accessor :initial_tasks, :init_connection, :schema, :sql_files_dir
107
+
108
+ def initialize
109
+ @init_connection = false
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ module SQLValidator
5
+ DEFAULT_TASK_COMMANDS = [:create, :drop, :delete, :insert, :set, :update]
6
+ COMMENT_REGEX = %r{/\*(?:[^\*]|\*[^/])*\*/}m
7
+
8
+ def task_command?(sql_statement)
9
+ sql_statement.match?(task_commands_regexp)
10
+ end
11
+
12
+ def query_command?(sql_statement)
13
+ sql_statement.match?(/select/i)
14
+ end
15
+
16
+ def is_query?(sql_statement)
17
+ query_command?(sql_statement) && !task_command?(sql_statement)
18
+ end
19
+
20
+ private
21
+ def task_commands_regexp
22
+ parts = DEFAULT_TASK_COMMANDS.map { |part| /#{part}/i }
23
+ /\A(?:[\(\s]|#{COMMENT_REGEX})*#{Regexp.union(*parts)}/i
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Db2Query
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ DB2_QUERY_DATABASE_TEMPLATE ||= <<-EOF
4
+ # frozen_string_literal: true
5
+
6
+ # Database configuration example
7
+ development:
8
+ primary:
9
+ conn_string:
10
+ driver: DB2
11
+ database: SAMPLE
12
+ dbalias: SAMPLE
13
+ hostname: LOCALHOST
14
+ currentschema: LIBTEST
15
+ port: "0"
16
+ protocol: IPC
17
+ uid: <%= ENV["DB2EC_UID"] %>
18
+ pwd: <%= ENV["DB2EC_PWD"] %>
19
+ secondary:
20
+ dsn: SAMPLE
21
+ uid: <%= ENV["DB2EC_UID"] %>
22
+ pwd: <%= ENV["DB2EC_PWD"] %>
23
+
24
+ test:
25
+ primary:
26
+ conn_string:
27
+ driver: DB2
28
+ database: SAMPLE
29
+ dbalias: SAMPLE
30
+ hostname: LOCALHOST
31
+ currentschema: LIBTEST
32
+ port: "0"
33
+ protocol: IPC
34
+ uid: <%= ENV["DB2EC_UID"] %>
35
+ pwd: <%= ENV["DB2EC_PWD"] %>
36
+ secondary:
37
+ dsn: SAMPLE
38
+ uid: <%= ENV["DB2EC_UID"] %>
39
+ pwd: <%= ENV["DB2EC_PWD"] %>
40
+ EOF
41
+
42
+ namespace :db2query do
43
+ desc "Create Database configuration file"
44
+ task :database do
45
+ database_path = "#{Rails.root}/config/db2query_database.yml"
46
+ if File.exist?(database_path)
47
+ raise ArgumentError, "File exists."
48
+ else
49
+ puts " Creating database config file ..."
50
+ File.open(database_path, "w") do |file|
51
+ file.puts DB2_QUERY_DATABASE_TEMPLATE
52
+ end
53
+ puts " File '#{database_path}' created."
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :db2query do
4
+ desc "Create Initializer and Database configuration file"
5
+ task :init do
6
+ Rake::Task["db2query:initializer"].invoke
7
+ Rake::Task["db2query:database"].invoke
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ DB2_QUERY_INITIALIZER_TEMPLATE ||= <<-EOF
4
+ # frozen_string_literal: true
5
+
6
+ # Example
7
+ require "db2_query/formatter"
8
+
9
+ class FirstNameFormatter < Db2Query::AbstractFormatter
10
+ def format(value)
11
+ "First Name: " + value
12
+ end
13
+ end
14
+
15
+ Db2Query::Formatter.registration do |format|
16
+ format.register(:first_name_formatter, FirstNameFormatter)
17
+ end
18
+ EOF
19
+
20
+ namespace :db2query do
21
+ desc "Create Initializer file"
22
+ task :initializer do
23
+ # Create initializer file
24
+ initializer_path = "#{Rails.root}/config/initializers/db2query.rb"
25
+ if File.exist?(initializer_path)
26
+ raise ArgumentError, "File exists."
27
+ else
28
+ puts " Creating initializer file ..."
29
+ File.open(initializer_path, "w") do |file|
30
+ file.puts DB2_QUERY_INITIALIZER_TEMPLATE
31
+ end
32
+ puts " File '#{initializer_path}' created."
33
+ end
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: db2_query
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - yohanes_l
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-06-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.3
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.0.3.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.3
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.0.3.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: ruby-odbc
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.99999'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.99999'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rubocop
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rubocop-performance
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rubocop-rails
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: faker
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: dotenv
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: byebug
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: sqlite3
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ description: A Rails query plugin to fetch data from Db2 database by using ODBC connection.
146
+ email:
147
+ - yohanes.lumentut@yahoo.com
148
+ executables: []
149
+ extensions: []
150
+ extra_rdoc_files: []
151
+ files:
152
+ - MIT-LICENSE
153
+ - README.md
154
+ - Rakefile
155
+ - lib/db2_query.rb
156
+ - lib/db2_query/base.rb
157
+ - lib/db2_query/column.rb
158
+ - lib/db2_query/connection.rb
159
+ - lib/db2_query/connection_handling.rb
160
+ - lib/db2_query/database_configurations.rb
161
+ - lib/db2_query/database_statements.rb
162
+ - lib/db2_query/error.rb
163
+ - lib/db2_query/formatter.rb
164
+ - lib/db2_query/log_subscriber.rb
165
+ - lib/db2_query/odbc_connector.rb
166
+ - lib/db2_query/path.rb
167
+ - lib/db2_query/railtie.rb
168
+ - lib/db2_query/result.rb
169
+ - lib/db2_query/schema.rb
170
+ - lib/db2_query/sql_validator.rb
171
+ - lib/db2_query/version.rb
172
+ - lib/tasks/database.rake
173
+ - lib/tasks/init.rake
174
+ - lib/tasks/initializer.rake
175
+ homepage: https://github.com/yohaneslumentut/db2_query
176
+ licenses:
177
+ - MIT
178
+ metadata:
179
+ allowed_push_host: https://rubygems.org
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubygems_version: 3.1.3
196
+ signing_key:
197
+ specification_version: 4
198
+ summary: Db2Query
199
+ test_files: []