olaf 0.2.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24eb5d8b71bc154b8992eed23b9763dc12fee835e8f5acdd8382aa7ac39242d0
4
- data.tar.gz: 7949ac30157805f95507deba9f3190cb34dd35646eb161c99800344828d38b17
3
+ metadata.gz: 9d45d092623bf97e4a23624cfcc145afd281dcb91011bee43e9086e35f1af9ff
4
+ data.tar.gz: 54ae40103a4ed018b04be956f86ad9316a2239ab719e721e33eb9d79a11e1b2c
5
5
  SHA512:
6
- metadata.gz: af792e74b54c3f674bfe40fe1ddbcf1e2870ce329434e6cbffe639ab7c2818be4945481162b0d7b38b2bae7415715cde352c2a6f51a5d2b3f0250bc8a276795d
7
- data.tar.gz: 461661997ad0507dff54b36dd73688b46d2ab241236a79abecb7afc2394ab178f2d519cc761fae7e05dc10b6d78566a77bfd88955931df4476973441b5a51578
6
+ metadata.gz: f874ca55bef0c740c90084a7db2e0487668a55c4af33e6d8879c798677aa4921eb0aa1c1f146d0af54c2de854b61c783c0a70e362b31c2cc5baf8cbee8ab37d4
7
+ data.tar.gz: 97331d8aa92d9b735b45c7931a08bccc49db2e97caee1ab11c1b0f19c067b63192a29625c356a90d5d85eee8f1f59ed924c4712d71ba5bbfbe581a058a911cc2
data/README.md CHANGED
@@ -1,21 +1,19 @@
1
1
  # Olaf
2
- [![Gem Version](https://badge.fury.io/rb/olaf.svg)](https://badge.fury.io/rb/olaf)
3
- [![CircleCI](https://circleci.com/gh/carwow/olaf.svg?style=shield&circle-token=:circle-ci-badge-token)](https://circleci.com/gh/carwow/olaf)
4
2
 
5
-
6
-
7
- Olaf is a small Ruby wrapper for Snowflake queries.
3
+ Olaf is a small Ruby wrapper for warehouse queries, running them in BigQuery.
8
4
 
9
5
  ![Olaf](https://user-images.githubusercontent.com/56375/96335285-8c86f080-106f-11eb-9489-999a884f1246.jpg)
10
6
 
11
7
 
12
8
  ## Dependencies
13
9
 
14
- `olaf` requires Ruby 2.6 or later, `sequel` and `odbc` driver to connect with DBs.
15
-
16
- Install dependencies using `bundler` is easy as run:
10
+ `olaf` depends on nothing by itself. Each driver loads its own client library,
11
+ and only when it is used, so add to your `Gemfile` the ones you configure:
17
12
 
18
- bundle install
13
+ | Driver | Gems | Also needs |
14
+ | ----------------- | ----------------------- | ----------------------- |
15
+ | `Olaf::BigQuery` | `google-cloud-bigquery` | |
16
+ | `Olaf::Fake` | — | |
19
17
 
20
18
  ## Installation
21
19
 
@@ -25,7 +23,7 @@ If you don't have Olaf, try this:
25
23
 
26
24
  ## Getting started
27
25
 
28
- Olaf helps developers to represent Snowflake queries as objects, to have more
26
+ Olaf helps developers to represent warehouse queries as objects, to have more
29
27
  control in the code and in tests.
30
28
 
31
29
  ### Example
@@ -34,7 +32,7 @@ control in the code and in tests.
34
32
  class FetchUsers
35
33
  include Olaf::QueryDefinition
36
34
 
37
- template './snowflake/users_in_department.sql'
35
+ template './big_query/users_in_department.sql'
38
36
 
39
37
  argument :department_id
40
38
 
@@ -46,3 +44,83 @@ query = FetchUsers.prepare(department_id: 1337)
46
44
  Olaf.execute(query)
47
45
  => [#<User id: 41, department_id: 1337, name: 'Ian'>]
48
46
  ```
47
+
48
+ ## Configuration
49
+
50
+ One driver, which every query runs on:
51
+
52
+ ```ruby
53
+ Olaf.configure(olaf_driver: Olaf::Fake) # ideal for testing
54
+ ```
55
+
56
+ Or several, which queries pick by name. The ones that don't declare a `driver`
57
+ run on the `default:`, the first one given when it is not specified:
58
+
59
+ ```ruby
60
+ Olaf.configure(
61
+ drivers: {
62
+ big_query: Olaf::BigQuery.new(project: 'carwow', labels: { service: 'flatmin' }),
63
+ fake: Olaf::Fake.new
64
+ },
65
+ default: :big_query
66
+ )
67
+
68
+ class FetchUsers
69
+ include Olaf::QueryDefinition
70
+
71
+ driver :big_query
72
+
73
+ template './big_query/users_in_department.sql'
74
+
75
+ argument :department_id
76
+ end
77
+ ```
78
+
79
+ `Olaf.instance(:big_query)` returns that driver. A single configured driver
80
+ serves every query, whatever they declare, which is what keeps `Olaf::Fake`
81
+ covering all of them in tests:
82
+
83
+ ```ruby
84
+ Olaf.configure(olaf_driver: Olaf::Fake)
85
+
86
+ Olaf.instance.register_result(FetchUsers, [{ id: 41 }])
87
+ Olaf.instance.register_result(FetchUsers, [{ id: 42 }], with: { department_id: 1337 })
88
+ ```
89
+
90
+ Whichever driver runs the query, a failure it reports is wrapped into
91
+ `Olaf::QueryExecutionError`, carrying the query metadata.
92
+
93
+ ## BigQuery
94
+
95
+ ```ruby
96
+ Olaf::BigQuery.new(
97
+ project: 'carwow',
98
+ credentials: JSON.parse(ENV['BIGQUERY_CREDENTIALS']), # ambient Google credentials when omitted
99
+ maximum_bytes_billed: 5 * Olaf::BigQuery::GIGABYTE, # default: 1 GiB
100
+ labels: { service: 'flatmin', country: 'uk' }
101
+ )
102
+ ```
103
+
104
+ The query runs as a job, so that it can be capped and labelled:
105
+
106
+ * Olaf's `:placeholders` are rewritten to BigQuery's `@named` parameters, for
107
+ declared arguments only, and bound as query parameters. Arguments declared
108
+ `as: :literal` are substituted into the SQL before that, as usual.
109
+ * Every job is capped with `maximum_bytes_billed`, and labelled with the driver
110
+ labels plus the name of the template. A query that scans more than the cap
111
+ raises its own, which keeps a regression failing instead of quietly costing
112
+ money:
113
+
114
+ ```ruby
115
+ driver :big_query, maximum_bytes_billed: 10 * Olaf::BigQuery::GIGABYTE
116
+ ```
117
+
118
+ `Olaf.execute` returns the rows. `Olaf.instance(:big_query).run(query)` returns
119
+ the finished job instead, for its statistics:
120
+
121
+ ```ruby
122
+ job = Olaf.instance(:big_query).run(query)
123
+ job.bytes_processed
124
+ job.cache_hit?
125
+ job.data.all.to_a
126
+ ```
@@ -0,0 +1,104 @@
1
+ # Not a dependency of the gem: consumers of this driver add it themselves.
2
+ require 'google/cloud/bigquery'
3
+
4
+ # The API client debug-logs whole response bodies, which for a warehouse query
5
+ # is the entire result set.
6
+ Google::Apis.logger.level = [Google::Apis.logger.level, Logger::INFO].max
7
+
8
+ module Olaf
9
+ class BigQuery
10
+ GIGABYTE = 1024**3
11
+
12
+ # Restrictive on purpose: a query that scans more says so with
13
+ # `driver :big_query, maximum_bytes_billed: ...`.
14
+ DEFAULT_MAXIMUM_BYTES_BILLED = GIGABYTE
15
+
16
+ # Labels only accept lowercase letters, numbers, dashes and underscores.
17
+ LABEL_FORMAT = /[^a-z0-9_-]/.freeze
18
+ LABEL_MAX_LENGTH = 63
19
+
20
+ # Credentials default to the ambient Google ones, and the client is built
21
+ # from the arguments unless one is given.
22
+ def initialize(project: nil, credentials: nil, maximum_bytes_billed: DEFAULT_MAXIMUM_BYTES_BILLED,
23
+ labels: {}, client: nil)
24
+ @project = project
25
+ @credentials = credentials
26
+ @maximum_bytes_billed = maximum_bytes_billed
27
+ @labels = labels
28
+ @client = client
29
+ end
30
+
31
+ def fetch(olaf_query)
32
+ run(olaf_query).data.all.to_a
33
+ end
34
+
35
+ # Returns the finished job, so callers can read its stats (bytes billed,
36
+ # cache hit) on top of its rows.
37
+ #
38
+ # @raises Olaf::QueryExecutionError
39
+ def run(olaf_query)
40
+ job = client.query_job(
41
+ sql(olaf_query),
42
+ params: params(olaf_query),
43
+ maximum_bytes_billed: maximum_bytes_billed(olaf_query),
44
+ labels: labels(olaf_query)
45
+ )
46
+ job.wait_until_done!
47
+
48
+ raise QueryExecutionError.new(job.error.to_h['message'].to_s, olaf_query) if job.failed?
49
+
50
+ job
51
+ rescue *wrapped_errors => error
52
+ raise QueryExecutionError.new(error.message, olaf_query)
53
+ end
54
+
55
+ private
56
+
57
+ def client
58
+ @client ||= Google::Cloud::Bigquery.new(project: @project, credentials: @credentials)
59
+ end
60
+
61
+ # Google::Cloud::Error covers everything the API reports, Google::Auth::Error
62
+ # credentials missing or expired before a call is made. The second one only
63
+ # exists in googleauth 1.14 and later.
64
+ def wrapped_errors
65
+ @wrapped_errors ||= [
66
+ Google::Cloud::Error,
67
+ (Google::Auth::Error if defined?(Google::Auth::Error))
68
+ ].compact
69
+ end
70
+
71
+ # olaf validates `:foo` placeholders, BigQuery binds `@foo`. Only declared
72
+ # argument names are rewritten, so casts and time literals are left alone.
73
+ def sql(olaf_query)
74
+ params(olaf_query).keys.reduce(olaf_query.sql_template) do |sql, name|
75
+ sql.gsub(/(?<!:):#{name}\b/, "@#{name}")
76
+ end
77
+ end
78
+
79
+ # Literal arguments were substituted into the template by `prepare`.
80
+ def params(olaf_query)
81
+ literal_arguments = literal_arguments(olaf_query)
82
+
83
+ olaf_query.variables.reject { |name, _value| literal_arguments.include?(name) }
84
+ end
85
+
86
+ def literal_arguments(olaf_query)
87
+ olaf_query.class.arguments.select { |_name, options| options[:literal] }.keys
88
+ end
89
+
90
+ def maximum_bytes_billed(olaf_query)
91
+ olaf_query.class.driver_options.fetch(:maximum_bytes_billed, @maximum_bytes_billed)
92
+ end
93
+
94
+ def labels(olaf_query)
95
+ @labels
96
+ .merge(query: File.basename(olaf_query.class.template.to_s, '.sql'))
97
+ .transform_values { |value| label_value(value) }
98
+ end
99
+
100
+ def label_value(value)
101
+ value.to_s.downcase.gsub(LABEL_FORMAT, '_')[0, LABEL_MAX_LENGTH]
102
+ end
103
+ end
104
+ end
data/lib/olaf/errors.rb CHANGED
@@ -29,6 +29,12 @@ module Olaf
29
29
  end
30
30
  end
31
31
 
32
+ class UnknownDriverError < StandardError
33
+ def initialize(name, configured_drivers)
34
+ super("Unknown driver #{name.inspect}, configured: #{configured_drivers.inspect}")
35
+ end
36
+ end
37
+
32
38
  class QueryExecutionError < StandardError
33
39
  attr_reader :metadata
34
40
 
@@ -3,7 +3,7 @@ module Olaf
3
3
  module ClassMethods
4
4
  # Creates a new instance of the Query defined and validates
5
5
  # the parameters passed, leaving the instance in a ready-to-execute state.
6
- # @return Snowflake::QueryDefinition instance
6
+ # @return Olaf::QueryDefinition instance
7
7
  def prepare(**vars)
8
8
  new(**vars).prepare
9
9
  end
@@ -15,9 +15,9 @@ module Olaf
15
15
  end
16
16
 
17
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)
18
+ # The driver binds the value passed as a query parameter. Sometimes, we need
19
+ # to pass an argument as literal to substitute it into the SQL instead
20
+ # (i.e. sending a table_name)
21
21
  #
22
22
  # options - Hash config for each argument
23
23
  # :as - Argument Type
@@ -26,7 +26,7 @@ module Olaf
26
26
  # Example:
27
27
  #
28
28
  # class OneQuery
29
- # include Snowflake::QueryDefinition
29
+ # include Olaf::QueryDefinition
30
30
  #
31
31
  # template 'reports/one.sql'
32
32
  #
@@ -48,7 +48,7 @@ module Olaf
48
48
  # Example:
49
49
  #
50
50
  # class OneQuery
51
- # include Snowflake::QueryDefinition
51
+ # include Olaf::QueryDefinition
52
52
  #
53
53
  # template 'reports/one.sql'
54
54
  # end
@@ -58,13 +58,42 @@ module Olaf
58
58
  @template ||= file_name
59
59
  end
60
60
 
61
+ # Define the driver this query runs on, one of the names given to
62
+ # `Olaf.configure`. Defaults to the default driver.
63
+ #
64
+ # Options are driver specific: the BigQuery driver reads
65
+ # :maximum_bytes_billed.
66
+ #
67
+ # Example:
68
+ #
69
+ # class OneQuery
70
+ # include Olaf::QueryDefinition
71
+ #
72
+ # driver :big_query, maximum_bytes_billed: 5 * Olaf::BigQuery::GIGABYTE
73
+ # end
74
+ #
75
+ #
76
+ def driver(name = nil, **options)
77
+ return @driver if name.nil?
78
+
79
+ @driver ||= name
80
+ @driver_options ||= options
81
+
82
+ @driver
83
+ end
84
+
85
+ # @return Hash of the options given to `driver`
86
+ def driver_options
87
+ @driver_options ||= {}
88
+ end
89
+
61
90
  # Define the object representing each row of the result.
62
91
  # When not specified, each row will be a hash by default.
63
92
  #
64
93
  # Example:
65
94
  #
66
95
  # class OneQuery
67
- # include Snowflake::QueryDefinition
96
+ # include Olaf::QueryDefinition
68
97
  #
69
98
  # row_object MyOwnObject
70
99
  # end
@@ -20,8 +20,8 @@ module Olaf
20
20
  #
21
21
  # @return QueryDefinition instance
22
22
  #
23
- # @raises Snowflake::UndefinedArgumentsError
24
- # @raises Snowflake::MissingArgumentsError
23
+ # @raises Olaf::UndefinedArgumentsError
24
+ # @raises Olaf::MissingArgumentsError
25
25
  def prepare
26
26
  @sql_template ||= File.read(self.class.template)
27
27
 
data/lib/olaf.rb CHANGED
@@ -1,46 +1,82 @@
1
- require 'sequel'
2
- require 'odbc_utf8'
3
-
4
1
  require_relative 'olaf/errors'
5
2
  require_relative 'olaf/query_definition'
6
- require_relative 'olaf/drivers/fake'
7
- require_relative 'olaf/drivers/snowflake'
8
3
 
9
4
  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.
5
+ # Autoloaded: each driver pulls in its own client library.
6
+ autoload :BigQuery, File.expand_path('olaf/drivers/big_query', __dir__)
7
+ autoload :Fake, File.expand_path('olaf/drivers/fake', __dir__)
8
+
9
+ DEFAULT_DRIVER = :default
10
+
11
+ # Configures the drivers used to execute queries, either a single one built
12
+ # from the arguments given:
13
+ #
14
+ # Olaf.configure(olaf_driver: Olaf::Fake) # ideal for testing
13
15
  #
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)
16
+ # or several instantiated ones, which queries pick by name with `driver`:
17
+ #
18
+ # Olaf.configure(
19
+ # drivers: { big_query: Olaf::BigQuery.new(project: 'carwow'), fake: Olaf::Fake.new },
20
+ # default: :big_query
21
+ # )
22
+ #
23
+ # @return the default Olaf driver instance
24
+ def self.configure(olaf_driver: nil, drivers: nil, default: nil, **args)
25
+ @drivers, @default_driver =
26
+ if drivers
27
+ raise ArgumentError, 'Pass `drivers:` or a single `olaf_driver:`, not both' if olaf_driver || args.any?
28
+ raise ArgumentError, 'No drivers given to configure' if drivers.empty?
29
+
30
+ default_driver = default || drivers.keys.first
31
+ unless drivers.key?(default_driver)
32
+ raise ArgumentError, "Unknown default driver #{default_driver.inspect}, given: #{drivers.keys.inspect}"
33
+ end
34
+
35
+ [drivers.dup, default_driver]
36
+ else
37
+ raise ArgumentError, 'Pass `drivers:` or a single `olaf_driver:` to configure' unless olaf_driver
38
+
39
+ [{ DEFAULT_DRIVER => olaf_driver.new(**args) }, DEFAULT_DRIVER]
40
+ end
41
+
42
+ instance
17
43
  end
18
44
 
19
- # Executes a query defined by Olaf::QueryDefinition with the driver
20
- # configured previously.
45
+ # Executes a query defined by Olaf::QueryDefinition with the driver the query
46
+ # declares, or with the default driver when it declares none.
21
47
  #
22
48
  # @return Enumerable of results.
23
49
  # (i.e. Array of Hashes or `row_objects` when specified)
24
50
  #
25
51
  # @raises Olaf::QueryExecutionError
52
+ # @raises Olaf::UnknownDriverError
26
53
  def self.execute(olaf_query)
27
54
  row_object = olaf_query.class.row_object
28
55
  row_transformer = row_object ? ->(r) { row_object.new(**r) } : Proc.new(&:itself)
29
56
 
30
- instance
57
+ instance(driver_name(olaf_query))
31
58
  .fetch(olaf_query)
32
59
  .map!(&row_transformer)
33
- rescue Sequel::DatabaseError => error
34
- raise QueryExecutionError.new(error.message, olaf_query)
35
60
  end
36
61
 
37
- # Returns an instance to execute queries when its configured.
62
+ # Returns an instance to execute queries when its configured. Without a name,
63
+ # the default driver. A single configured driver serves every name, which is
64
+ # what keeps `Olaf::Fake` covering every query in tests.
38
65
  #
39
66
  # @return Olaf driver instance
40
- # * Olaf::Fake - Ideal for testing
41
- # * Olaf::Snowflake - Sequel.odbc driver to run queries in Snowflake
67
+ # * Olaf::Fake - Ideal for testing
68
+ # * Olaf::BigQuery - google-cloud-bigquery driver to run queries in BigQuery
42
69
  #
43
- def self.instance
44
- @instance || raise('You need to configure Olaf before using it!')
70
+ def self.instance(name = nil)
71
+ drivers = @drivers || raise('You need to configure Olaf before using it!')
72
+
73
+ return drivers.fetch(@default_driver) if name.nil? || drivers.size == 1
74
+
75
+ drivers.fetch(name) { raise UnknownDriverError.new(name, drivers.keys) }
76
+ end
77
+
78
+ def self.driver_name(olaf_query)
79
+ olaf_query.class.driver if olaf_query.class.respond_to?(:driver)
45
80
  end
81
+ private_class_method :driver_name
46
82
  end
data/olaf.gemspec CHANGED
@@ -1,8 +1,8 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'olaf'
3
- s.version = '0.2.0'
3
+ s.version = '1.0.0'
4
4
  s.date = Time.now.strftime('%Y-%m-%d')
5
- s.summary = 'Ruby wrapper for Snowflake queries.'
5
+ s.summary = 'Ruby wrapper for warehouse queries.'
6
6
  s.authors = ['Emiliano Mancuso']
7
7
  s.email = ['emiliano.mancuso@gmail.com', 'developers@carwow.co.uk']
8
8
  s.homepage = 'http://github.com/carwow/olaf'
@@ -16,7 +16,9 @@ Gem::Specification.new do |s|
16
16
  ]
17
17
  s.test_files = Dir['test/*.*']
18
18
 
19
- s.add_runtime_dependency 'sequel', '~> 5.37'
20
- s.add_runtime_dependency 'ruby-odbc', '~> 0.99'
19
+ # Drivers load their own client libraries, so consumers only install the ones
20
+ # they configure:
21
+ # Olaf::BigQuery - google-cloud-bigquery
22
+ s.add_development_dependency 'google-cloud-bigquery', '~> 1.64'
21
23
  s.add_development_dependency 'test-unit', '~> 3.3'
22
24
  end
@@ -20,9 +20,75 @@ class OlafConfigureTest < Test::Unit::TestCase
20
20
  assert_equal Olaf.instance.config, { other: 'configs', like: 'user and passwd' }
21
21
  end
22
22
 
23
- def test_configure_defaults_to_snowflake
24
- Olaf.configure(random: 'stuff')
23
+ def test_configure_requires_a_driver
24
+ assert_raise ArgumentError do
25
+ Olaf.configure(random: 'stuff')
26
+ end
27
+ end
28
+
29
+ def test_configure_returns_the_default_driver
30
+ assert_equal Olaf.configure(olaf_driver: Olaf::Fake), Olaf.instance
31
+ end
32
+
33
+ def test_configure_registers_several_drivers
34
+ postgres = MockDriver.new
35
+ big_query = MockDriver.new
36
+
37
+ Olaf.configure(drivers: { postgres: postgres, big_query: big_query }, default: :postgres)
38
+
39
+ assert_equal Olaf.instance, postgres
40
+ assert_equal Olaf.instance(:postgres), postgres
41
+ assert_equal Olaf.instance(:big_query), big_query
42
+ end
43
+
44
+ def test_configure_defaults_to_the_first_driver_registered
45
+ postgres = MockDriver.new
46
+
47
+ Olaf.configure(drivers: { postgres: postgres, big_query: MockDriver.new })
48
+
49
+ assert_equal Olaf.instance, postgres
50
+ end
51
+
52
+ def test_configure_rejects_a_default_that_was_not_registered
53
+ assert_raise_message(/Unknown default driver :redshift/) do
54
+ Olaf.configure(drivers: { postgres: MockDriver.new }, default: :redshift)
55
+ end
56
+ end
25
57
 
26
- assert Olaf.instance.is_a?(Olaf::Snowflake)
58
+ def test_configure_rejects_both_forms_at_once
59
+ assert_raise ArgumentError do
60
+ Olaf.configure(olaf_driver: MockDriver, drivers: { postgres: MockDriver.new })
61
+ end
62
+ end
63
+
64
+ def test_configure_rejects_an_empty_list_of_drivers
65
+ assert_raise ArgumentError do
66
+ Olaf.configure(drivers: {})
67
+ end
68
+ end
69
+
70
+ def test_instance_serves_every_name_when_a_single_driver_is_configured
71
+ Olaf.configure(olaf_driver: Olaf::Fake)
72
+
73
+ assert_equal Olaf.instance(:big_query), Olaf.instance
74
+ end
75
+
76
+ def test_instance_raises_for_a_driver_that_was_not_registered
77
+ Olaf.configure(drivers: { postgres: MockDriver.new, big_query: MockDriver.new })
78
+
79
+ assert_raise Olaf::UnknownDriverError do
80
+ Olaf.instance(:redshift)
81
+ end
82
+ end
83
+
84
+ def test_instance_raises_until_olaf_is_configured
85
+ configured_drivers = Olaf.instance_variable_get(:@drivers)
86
+ Olaf.instance_variable_set(:@drivers, nil)
87
+
88
+ assert_raise_message(/You need to configure Olaf before using it!/) do
89
+ Olaf.instance
90
+ end
91
+ ensure
92
+ Olaf.instance_variable_set(:@drivers, configured_drivers)
27
93
  end
28
94
  end
@@ -1,6 +1,23 @@
1
1
  require_relative 'helper'
2
2
 
3
3
  class OlafExecuteTest < Test::Unit::TestCase
4
+ Company = Struct.new(:company, keyword_init: true)
5
+
6
+ class RecordingDriver
7
+ attr_reader :executed
8
+
9
+ def initialize(**config)
10
+ @config = config
11
+ @executed = []
12
+ end
13
+
14
+ def fetch(olaf_query)
15
+ @executed << olaf_query
16
+
17
+ [{ company: 'carwow' }]
18
+ end
19
+ end
20
+
4
21
  def setup
5
22
  Olaf.configure(olaf_driver: Olaf::Fake)
6
23
 
@@ -17,9 +34,9 @@ class OlafExecuteTest < Test::Unit::TestCase
17
34
  end
18
35
 
19
36
  def test_execute_returns_a_list_of_row_objects_when_defined
20
- @query.row_object OpenStruct
37
+ @query.row_object Company
21
38
 
22
- all_row_objects = Olaf.execute(@query_instance).all? { |e| e.is_a? OpenStruct }
39
+ all_row_objects = Olaf.execute(@query_instance).all? { |e| e.is_a? Company }
23
40
 
24
41
  assert all_row_objects
25
42
  end
@@ -30,12 +47,12 @@ class OlafExecuteTest < Test::Unit::TestCase
30
47
  assert all_hashes
31
48
  end
32
49
 
33
- def test_execute_raises_error
50
+ def test_execute_lets_the_error_wrapped_by_the_driver_through
34
51
  faulty_driver = Class.new do
35
52
  def initialize(**args); end
36
53
 
37
- def fetch(*args)
38
- raise Sequel::DatabaseError
54
+ def fetch(olaf_query)
55
+ raise Olaf::QueryExecutionError.new('something went wrong', olaf_query)
39
56
  end
40
57
  end
41
58
 
@@ -45,4 +62,48 @@ class OlafExecuteTest < Test::Unit::TestCase
45
62
  Olaf.execute(@query_instance)
46
63
  end
47
64
  end
65
+
66
+ def test_execute_uses_the_driver_declared_by_the_query
67
+ default_driver = RecordingDriver.new
68
+ other_driver = RecordingDriver.new
69
+
70
+ Olaf.configure(drivers: { default: default_driver, big_query: other_driver })
71
+
72
+ @query.driver :big_query
73
+
74
+ Olaf.execute(@query_instance)
75
+
76
+ assert_equal other_driver.executed, [@query_instance]
77
+ assert_equal default_driver.executed, []
78
+ end
79
+
80
+ def test_execute_uses_the_default_driver_when_the_query_declares_none
81
+ default_driver = RecordingDriver.new
82
+ other_driver = RecordingDriver.new
83
+
84
+ Olaf.configure(drivers: { big_query: other_driver, postgres: default_driver }, default: :postgres)
85
+
86
+ Olaf.execute(@query_instance)
87
+
88
+ assert_equal default_driver.executed, [@query_instance]
89
+ assert_equal other_driver.executed, []
90
+ end
91
+
92
+ def test_execute_uses_the_only_driver_configured_whatever_the_query_declares
93
+ @query.driver :big_query
94
+
95
+ Olaf.execute(@query_instance)
96
+
97
+ assert_equal Olaf.instance.execution_log, [@query_instance]
98
+ end
99
+
100
+ def test_execute_raises_when_the_driver_declared_is_not_configured
101
+ Olaf.configure(drivers: { default: RecordingDriver.new, postgres: RecordingDriver.new })
102
+
103
+ @query.driver :big_query
104
+
105
+ assert_raise_message(/Unknown driver :big_query/) do
106
+ Olaf.execute(@query_instance)
107
+ end
108
+ end
48
109
  end
@@ -0,0 +1,38 @@
1
+ require_relative 'helper'
2
+
3
+ # Drivers pull in client libraries that not every consumer installs, so what
4
+ # gets loaded, and when, is part of the interface. These run in a subprocess:
5
+ # the rest of the suite has all of them loaded already.
6
+ class OlafLazyLoadingTest < Test::Unit::TestCase
7
+ def test_requiring_olaf_loads_no_driver_dependency
8
+ assert_equal loaded_after('require "olaf"'), []
9
+ end
10
+
11
+ def test_configuring_the_fake_driver_loads_no_driver_dependency
12
+ assert_equal loaded_after('require "olaf"; Olaf.configure(olaf_driver: Olaf::Fake)'), []
13
+ end
14
+
15
+ def test_configuring_the_big_query_driver_loads_its_client_only
16
+ assert_equal loaded_after('require "olaf"; Olaf.configure(olaf_driver: Olaf::BigQuery)'), ['google-cloud-bigquery']
17
+ end
18
+
19
+ private
20
+
21
+ DEPENDENCIES = {
22
+ 'google-cloud-bigquery' => 'Google::Cloud::Bigquery'
23
+ }.freeze
24
+
25
+ def loaded_after(script)
26
+ report = "print #{DEPENDENCIES.values.inspect}.map { |c| Object.const_defined?(c) }.inspect"
27
+ # RUBYOPT carries bundler into the subprocess, which is not what is measured.
28
+ output = IO.popen({ 'RUBYOPT' => nil }, [RbConfig.ruby, '-I', lib_path, '-e', "#{script}; #{report}"], &:read)
29
+
30
+ assert $?.success?, "Failed to run: #{script}"
31
+
32
+ DEPENDENCIES.keys.zip(eval(output)).select { |_name, loaded| loaded }.map(&:first)
33
+ end
34
+
35
+ def lib_path
36
+ File.expand_path('../lib', File.dirname(__FILE__))
37
+ end
38
+ end
metadata CHANGED
@@ -1,43 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: olaf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emiliano Mancuso
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2020-11-23 00:00:00.000000000 Z
10
+ date: 2026-10-09 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: sequel
13
+ name: google-cloud-bigquery
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !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
18
+ version: '1.64'
19
+ type: :development
35
20
  prerelease: false
36
21
  version_requirements: !ruby/object:Gem::Requirement
37
22
  requirements:
38
23
  - - "~>"
39
24
  - !ruby/object:Gem::Version
40
- version: '0.99'
25
+ version: '1.64'
41
26
  - !ruby/object:Gem::Dependency
42
27
  name: test-unit
43
28
  requirement: !ruby/object:Gem::Requirement
@@ -52,7 +37,6 @@ dependencies:
52
37
  - - "~>"
53
38
  - !ruby/object:Gem::Version
54
39
  version: '3.3'
55
- description:
56
40
  email:
57
41
  - emiliano.mancuso@gmail.com
58
42
  - developers@carwow.co.uk
@@ -62,8 +46,8 @@ extra_rdoc_files: []
62
46
  files:
63
47
  - README.md
64
48
  - lib/olaf.rb
49
+ - lib/olaf/drivers/big_query.rb
65
50
  - lib/olaf/drivers/fake.rb
66
- - lib/olaf/drivers/snowflake.rb
67
51
  - lib/olaf/errors.rb
68
52
  - lib/olaf/query_definition.rb
69
53
  - lib/olaf/query_definition/class_methods.rb
@@ -72,11 +56,11 @@ files:
72
56
  - test/helper.rb
73
57
  - test/olaf_configure_test.rb
74
58
  - test/olaf_execute_test.rb
59
+ - test/olaf_lazy_loading_test.rb
75
60
  homepage: http://github.com/carwow/olaf
76
61
  licenses:
77
62
  - MIT
78
63
  metadata: {}
79
- post_install_message:
80
64
  rdoc_options: []
81
65
  require_paths:
82
66
  - lib
@@ -91,11 +75,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
75
  - !ruby/object:Gem::Version
92
76
  version: '0'
93
77
  requirements: []
94
- rubygems_version: 3.1.2
95
- signing_key:
78
+ rubygems_version: 4.0.20
96
79
  specification_version: 4
97
- summary: Ruby wrapper for Snowflake queries.
80
+ summary: Ruby wrapper for warehouse queries.
98
81
  test_files:
99
82
  - test/helper.rb
100
- - test/olaf_execute_test.rb
101
83
  - test/olaf_configure_test.rb
84
+ - test/olaf_execute_test.rb
85
+ - test/olaf_lazy_loading_test.rb
@@ -1,17 +0,0 @@
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