olaf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 707d135df5db1d5105754573d6fa021c43da6b87ff9d83cd9baafc879ae3401e
4
+ data.tar.gz: d8fbaa062cd9eb2c4e3438d4b40f8c8352b95e1234f2f5d59c040870013ad263
5
+ SHA512:
6
+ metadata.gz: 969ee6bec25a2f5c17dbc03fb1e5aab61ed9b6801c764c2797ac36c0a8a0482351888a9f87cdec3d78453aca5bbd82ec635d3a5d3dfca0cc03eac0df3eee193c
7
+ data.tar.gz: 7a1fe66488aa789b7f005102b668767682ae6e5bb59fa20168641897a993ebb48a9c5b359ac69795e2bafe67df78b5565be1a4b19cf11c1e68119c114cb6623b
@@ -0,0 +1,45 @@
1
+ # Olaf
2
+ [![Gem Version](https://badge.fury.io/rb/ork.svg)](http://badge.fury.io/rb/ork)
3
+ [![Build Status](https://travis-ci.org/emancu/ork.svg)](https://travis-ci.org/emancu/ork)
4
+
5
+ Olaf is a small Ruby wrapper for Snowflake queries.
6
+
7
+ ![Olaf](https://i.pinimg.com/474x/6a/76/67/6a76672b65bf989b25b1ec0fc8cda3c8.jpg)
8
+
9
+ ## Dependencies
10
+
11
+ `olaf` requires Ruby 2.2 or later, `sequel` and `odbc` driver to connect with DBs.
12
+
13
+ Install dependencies using `bundler` is easy as run:
14
+
15
+ bundle install
16
+
17
+ ## Installation
18
+
19
+ If you don't have Olaf, try this:
20
+
21
+ $ gem install olaf
22
+
23
+ ## Getting started
24
+
25
+ Olaf helps developers to represent Snowflake queries as objects, to have more
26
+ control in the code and in tests.
27
+
28
+ ### Example
29
+
30
+ ```ruby
31
+ class FetchUsers
32
+ include Olaf::QueryDefinition
33
+
34
+ template './snowflake/users_in_department.sql'
35
+
36
+ attribute :department_id
37
+
38
+ row_object User
39
+ end
40
+
41
+ query = FetchUsers.prepare(department_id: 1337)
42
+
43
+ Olaf.execute(query)
44
+ => [#<User id: 41, department_id: 1337, name: 'Ian'>]
45
+ ```
@@ -0,0 +1,46 @@
1
+ require 'sequel'
2
+ require 'odbc_utf8'
3
+
4
+ require_relative 'olaf/errors'
5
+ require_relative 'olaf/query_definition'
6
+ require_relative 'olaf/drivers/fake'
7
+ require_relative 'olaf/drivers/snowflake'
8
+
9
+ module Olaf
10
+ # Configures Olaf module to execute queries in Snowflake or in a local object
11
+ # to prevent external calls. Arguments passed will be forwarded directly
12
+ # to the `olaf_driver` class specified.
13
+ #
14
+ # By default, it will set a connection with Snowflake
15
+ def self.configure(olaf_driver: Olaf::Snowflake, **args)
16
+ @instance = olaf_driver.new(**args)
17
+ end
18
+
19
+ # Executes a query defined by Olaf::QueryDefinition with the driver
20
+ # configured previously.
21
+ #
22
+ # @return Enumerable of results.
23
+ # (i.e. Array of Hashes or `row_objects` when specified)
24
+ #
25
+ # @raises Olaf::QueryExecutionError
26
+ def self.execute(olaf_query)
27
+ row_object = olaf_query.class.row_object
28
+ row_transformer = row_object ? ->(r) { row_object.new(**r) } : Proc.new(&:itself)
29
+
30
+ instance
31
+ .fetch(olaf_query)
32
+ .map!(&row_transformer)
33
+ rescue Sequel::DatabaseError => error
34
+ raise QueryExecutionError.new(error.message, olaf_query)
35
+ end
36
+
37
+ # Returns an instance to execute queries when its configured.
38
+ #
39
+ # @return Olaf driver instance
40
+ # * Olaf::Fake - Ideal for testing
41
+ # * Olaf::Snowflake - Sequel.odbc driver to run queries in Snowflake
42
+ #
43
+ def self.instance
44
+ @instance || raise('You need to configure Olaf before using it!')
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ module Olaf
2
+ class Fake
3
+ attr_reader :config, :execution_log, :recorded_responses
4
+
5
+ def initialize(**config)
6
+ @config = config
7
+ @execution_log = []
8
+ @recorded_responses = {}
9
+ end
10
+
11
+ def fetch(olaf_query)
12
+ @execution_log << olaf_query
13
+
14
+ generic_response = @recorded_responses.dig(olaf_query.class, :generic)
15
+ specific_response = @recorded_responses.dig(olaf_query.class, olaf_query.variables)
16
+
17
+ specific_response || generic_response || raise_developer_error!(olaf_query)
18
+ end
19
+
20
+ def register_result(olaf_query_class, result, with: :generic)
21
+ @recorded_responses[olaf_query_class] ||= {}
22
+ @recorded_responses[olaf_query_class].merge!(with => result)
23
+ end
24
+
25
+ private
26
+
27
+ def raise_developer_error!(query)
28
+ error_msg = "No Query result registered for '#{query.class}' with: #{query.variables} found!"
29
+
30
+ available_responses =
31
+ @recorded_responses
32
+ .map { |k, v| [k, ['', *v.keys].join("\n\t\t=> ")] }
33
+ .map { |klass, args| "\t #{klass}#{args}" }
34
+ .join("\n\n")
35
+ .then { |str| "Queries responses registered: \n\n #{str}" unless str.empty?}
36
+
37
+ puts %{
38
+ #{error_msg}
39
+
40
+ This is a testing environment and the query has no result registered.
41
+ To register a query result call `Olaf.instance.register_result`
42
+ during the test setup or before executing the query.
43
+
44
+
45
+ Olaf.instance.register_result(#{query.class}, [])
46
+
47
+
48
+ To register a result for specific arguments you can specify the variables:
49
+
50
+
51
+ Olaf.instance.register_result(#{query.class}, [], with: #{query.variables})
52
+
53
+
54
+ #{available_responses}
55
+ }
56
+
57
+ raise error_msg
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ module Olaf
2
+ class Snowflake
3
+ def initialize(**config)
4
+ @config = config
5
+ end
6
+
7
+ def fetch(olaf_query)
8
+ conn.fetch(olaf_query.sql_template, **olaf_query.variables).all
9
+ end
10
+
11
+ private
12
+
13
+ def conn
14
+ @conn ||= Sequel.odbc('snowflake', **@config)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ module Olaf
2
+ class MissingArgumentsError < ArgumentError
3
+ def initialize(olaf_query)
4
+ @olaf_query = olaf_query
5
+ msg = "Missing arguments: #{olaf_query.missing_arguments}"
6
+
7
+ super(msg)
8
+ end
9
+
10
+ def metadata
11
+ {
12
+ query: @olaf_query.class.name,
13
+ defined_arguments: @olaf_query.defined_arguments,
14
+ missing_arguments: @olaf_query.missing_arguments,
15
+ arguments: @olaf_query.variables
16
+ }
17
+ end
18
+ end
19
+
20
+ class UndefinedArgumentsError < StandardError
21
+ def initialize(olaf_query)
22
+ super("Undefined arguments: #{olaf_query.undefined_arguments}")
23
+ end
24
+ end
25
+
26
+ class QueryExecutionError < StandardError
27
+ attr_reader :metadata
28
+
29
+ def initialize(message, olaf_query)
30
+ @query = olaf_query
31
+ @metadata = olaf_query.metadata
32
+ super(message)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,67 @@
1
+ require_relative 'errors'
2
+ require_relative 'query_definition/class_methods'
3
+
4
+ module Olaf
5
+ module QueryDefinition
6
+ attr_reader :variables, :sql_template
7
+
8
+ def self.included(base)
9
+ base.extend(Olaf::QueryDefinition::ClassMethods)
10
+ end
11
+
12
+ def initialize(**variables)
13
+ @variables = variables
14
+ @sql_template = nil
15
+ end
16
+
17
+ # Loads the SQL template and validate all the arguments are
18
+ # defined AND present.
19
+ # The instance will be ready to be executed.
20
+ #
21
+ # @return QueryDefinition instance
22
+ #
23
+ # @raises Snowflake::UndefinedArgumentsError
24
+ # @raises Snowflake::MissingArgumentsError
25
+ def prepare
26
+ @sql_template ||= File.read(self.class.template)
27
+
28
+ raise UndefinedArgumentsError, self if undefined_arguments.any?
29
+ raise MissingArgumentsError, self if missing_arguments.any?
30
+
31
+ literal_arguments = self.class.arguments.select { |_k, v| v[:literal] }.keys
32
+
33
+ variables.slice(*literal_arguments).each do |placeholder, literal_value|
34
+ @sql_template.gsub!(":#{placeholder}", literal_value)
35
+ end
36
+
37
+ self
38
+ end
39
+
40
+ def metadata
41
+ {
42
+ query_class: self.class.name,
43
+ arguments: variables,
44
+ sql_template: sql_template,
45
+ }
46
+ end
47
+
48
+ def defined_arguments
49
+ self.class.arguments.keys
50
+ end
51
+
52
+ def missing_arguments
53
+ defined_arguments - @variables.keys
54
+ end
55
+
56
+ # Find placeholders in the SQL file.
57
+ # Every placeholder MUST be declared as an `argument` of the query.
58
+ def undefined_arguments
59
+ @sql_template
60
+ .scan(/[^:]:(\w+)/)
61
+ .flatten
62
+ .map(&:to_sym)
63
+ .then { |required_arguments| required_arguments - defined_arguments }
64
+ .uniq
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,79 @@
1
+ module Olaf
2
+ module QueryDefinition
3
+ module ClassMethods
4
+ # Creates a new instance of the Query defined and validates
5
+ # the parameters passed, leaving the instance in a ready-to-execute state.
6
+ # @return Snowflake::QueryDefinition instance
7
+ def prepare(**vars)
8
+ new(vars).prepare
9
+ end
10
+
11
+ # Returns ALL the arguments defined for the query, including options.
12
+ # @return Hash
13
+ def arguments
14
+ @arguments ||= {}
15
+ end
16
+
17
+ # Define an argument for the query matching a placeholder in the template.
18
+ # The Sequel gem, will fill the placeholders in the SQL query escaping the
19
+ # value passed as an argument. Sometimes, we need to pass an argument as
20
+ # literal to avoid the single quotes added by Sequel (i.e. sending a table_name)
21
+ #
22
+ # options - Hash config for each argument
23
+ # :as - Argument Type
24
+ # * :literal - Forces a string substitution with the literal value
25
+ #
26
+ # Example:
27
+ #
28
+ # class OneQuery
29
+ # include Snowflake::QueryDefinition
30
+ #
31
+ # template 'reports/one.sql'
32
+ #
33
+ # argument :dealersip_id
34
+ # argument :table_name, as: :literal
35
+ # end
36
+ #
37
+ #
38
+ def argument(name, options = {})
39
+ return if arguments.key?(name)
40
+
41
+ arguments[name] = { literal: options[:as] == :literal }
42
+
43
+ name
44
+ end
45
+
46
+ # Define the file path to the SQL template for this query.
47
+ #
48
+ # Example:
49
+ #
50
+ # class OneQuery
51
+ # include Snowflake::QueryDefinition
52
+ #
53
+ # template 'reports/one.sql'
54
+ # end
55
+ #
56
+ #
57
+ def template(file_name = nil)
58
+ @template ||= file_name
59
+ end
60
+
61
+ # Define the object representing each row of the result.
62
+ # When not specified, each row will be a hash by default.
63
+ #
64
+ # Example:
65
+ #
66
+ # class OneQuery
67
+ # include Snowflake::QueryDefinition
68
+ #
69
+ # row_object MyOwnObject
70
+ # end
71
+ #
72
+ #
73
+ def row_object(object_class = nil)
74
+ @row_object ||= object_class
75
+ end
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'olaf'
3
+ s.version = '0.1.0'
4
+ s.date = Time.now.strftime('%Y-%m-%d')
5
+ s.summary = 'Ruby wrapper for Snowflake queries.'
6
+ s.authors = ['Emiliano Mancuso']
7
+ s.email = ['emiliano.mancuso@gmail.com', 'developers@carwow.co.uk']
8
+ s.homepage = 'http://github.com/carwow/olaf'
9
+ s.license = 'MIT'
10
+
11
+ s.files = Dir[
12
+ 'README.md',
13
+ 'rakefile',
14
+ 'lib/**/*.rb',
15
+ '*.gemspec'
16
+ ]
17
+ s.test_files = Dir['test/*.*']
18
+
19
+ s.add_runtime_dependency 'sequel', '~> 5.37'
20
+ s.add_runtime_dependency 'ruby-odbc', '~> 0.99'
21
+ end
@@ -0,0 +1,8 @@
1
+ task :default => :test
2
+
3
+ desc 'Run tests'
4
+ task :test do
5
+ require File.expand_path('./test/helper', File.dirname(__FILE__))
6
+
7
+ Dir['test/**/*_test.rb'].each { |file| load file }
8
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
2
+
3
+ require 'olaf'
4
+ require 'test/unit'
5
+
6
+ def reject(condition, message="Expected condition to be unsatisfied")
7
+ assert !condition, message
8
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'helper'
2
+
3
+ class OlafConfigureTest < Test::Unit::TestCase
4
+ class MockDriver
5
+ attr_reader :config
6
+
7
+ def initialize(**config)
8
+ @config = config
9
+ end
10
+
11
+ def instance
12
+ self
13
+ end
14
+ end
15
+
16
+ def test_configure_initializes_driver_instance
17
+ Olaf.configure(olaf_driver: MockDriver, other: 'configs', like: 'user and passwd')
18
+
19
+ assert Olaf.instance.is_a?(MockDriver)
20
+ assert_equal Olaf.instance.config, { other: 'configs', like: 'user and passwd' }
21
+ end
22
+
23
+ def test_configure_defaults_to_snowflake
24
+ Olaf.configure(random: 'stuff')
25
+
26
+ assert Olaf.instance.is_a?(Olaf::Snowflake)
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'helper'
2
+
3
+ class OlafExecuteTest < Test::Unit::TestCase
4
+ def setup
5
+ Olaf.configure(olaf_driver: Olaf::Fake)
6
+
7
+ @query = Class.new.include(Olaf::QueryDefinition)
8
+ @query.template File.join(File.dirname(__FILE__), './fixtures/query_with_arguments.sql')
9
+ @query.argument :id
10
+
11
+ Olaf.instance.register_result(@query, [{ company: 'carwow' }])
12
+ @query_instance = @query.new(id: 1).prepare
13
+ end
14
+
15
+ def test_execute_returns_an_enumerable
16
+ assert Olaf.execute(@query_instance).is_a?(Enumerable)
17
+ end
18
+
19
+ def test_execute_returns_a_list_of_row_objects_when_defined
20
+ @query.row_object OpenStruct
21
+
22
+ all_row_objects = Olaf.execute(@query_instance).all? { |e| e.is_a? OpenStruct }
23
+
24
+ assert all_row_objects
25
+ end
26
+
27
+ def test_execute_returns_hashes_when_no_row_object_defined
28
+ all_hashes = Olaf.execute(@query_instance).all? { |e| e.is_a? Hash }
29
+
30
+ assert all_hashes
31
+ end
32
+
33
+ def test_execute_raises_error
34
+ faulty_driver = Class.new do
35
+ def initialize(**args); end
36
+
37
+ def fetch(*args)
38
+ raise Sequel::DatabaseError
39
+ end
40
+ end
41
+
42
+ Olaf.configure(olaf_driver: faulty_driver)
43
+
44
+ assert_raise Olaf::QueryExecutionError do
45
+ Olaf.execute(@query_instance)
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: olaf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Emiliano Mancuso
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.37'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.37'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-odbc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.99'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.99'
41
+ description:
42
+ email:
43
+ - emiliano.mancuso@gmail.com
44
+ - developers@carwow.co.uk
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - lib/olaf.rb
51
+ - lib/olaf/drivers/fake.rb
52
+ - lib/olaf/drivers/snowflake.rb
53
+ - lib/olaf/errors.rb
54
+ - lib/olaf/query_definition.rb
55
+ - lib/olaf/query_definition/class_methods.rb
56
+ - olaf.gemspec
57
+ - rakefile
58
+ - test/helper.rb
59
+ - test/olaf_configure_test.rb
60
+ - test/olaf_execute_test.rb
61
+ homepage: http://github.com/carwow/olaf
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.1.4
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Ruby wrapper for Snowflake queries.
84
+ test_files:
85
+ - test/helper.rb
86
+ - test/olaf_execute_test.rb
87
+ - test/olaf_configure_test.rb