olaf 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: 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