rb_snowflake_client 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4aa627645364f92fc0e0d9ef949519a7649088e43de1e9ca7480ba284a9efba5
4
- data.tar.gz: e18d9a6a1f297cbbb5a10aafcac9dce440c43e0ca430f0c6310c31cea28b4507
3
+ metadata.gz: b345626b574463c788bc7ff125c6297eac071ce62cd77fadb77c5da896f6089d
4
+ data.tar.gz: 3bed923cc293d33af04eb846241637d6c72a8bef8917958e50b013fa8d120f70
5
5
  SHA512:
6
- metadata.gz: 80ac86515ff08d723c5eff134ac090b60c079695da9c68cf39b06f8db5d1661f700b78930f5da2c19b3f4686563d4dbcda47df70f9ad35200af9e7582b09ea8b
7
- data.tar.gz: d4c10ff8bba434ccd0ee380bbaaf9d14bfc7aed22543c34397fd6e1f83611daab65efeacf8caf241a98fd5e2a0f3591592b47f7c76de4997cfe2ea2fb951a5b8
6
+ metadata.gz: 952d6ffe14f350158abbe2342815687e36564233f7de8a452c27174f2f483f76c32e331502a1a35e7e20a654f11a74c109f7018410a9a3add7f7fa42ea3e32c7
7
+ data.tar.gz: 3dcc696ab574bebb3b0416dbc9595df656c6547f7b4a36b70d32d63030dfedc54d9f84e8b9838c0fd22c3cb5a9b9ce0efdc9c74b78e3bb41a57e58c29757c120
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rb_snowflake_client (0.1.2)
4
+ rb_snowflake_client (0.3.0)
5
5
  concurrent-ruby (>= 1.2)
6
6
  connection_pool (>= 2.4)
7
7
  dotenv (>= 2.8)
data/README.md CHANGED
@@ -13,6 +13,8 @@ This library is implemented in ruby and while it leverages some libraries that h
13
13
 
14
14
  # Usage
15
15
 
16
+ ## Create a client
17
+
16
18
  Add to your Gemfile or use `gem install rb-snowflake-client`
17
19
  ```ruby
18
20
  gem "rb-snowflake-client"
@@ -27,7 +29,7 @@ require "rb_snowflake_client"
27
29
  # see: https://github.com/rinsed-org/pure-ruby-snowflake-client/blob/master/lib/ruby_snowflake/client.rb#L43
28
30
  client = RubySnowflake::Client.new(
29
31
  "https://yourinstance.region.snowflakecomputing.com", # insert your URL here
30
- File.read("secrets/my_key.pem"), # path to your private key
32
+ File.read("secrets/my_key.pem"), # your private key in PEM format (scroll down for instructions)
31
33
  "snowflake-organization", # your account name (doesn't match your URL)
32
34
  "snowflake-account", # typically your subdomain
33
35
  "snowflake-user", # Your snowflake user
@@ -35,27 +37,30 @@ client = RubySnowflake::Client.new(
35
37
  "some_database", # The name of the database in the context of which the queries will run
36
38
  max_connections: 12, # Config options can be passed in
37
39
  connection_timeout: 45, # See below for the full set of options
40
+ query_timeout: 1200, # how long to wait for queries, in seconds
38
41
  )
39
42
 
40
43
  # alternatively you can use the `from_env` method, which will pull these values from the following environment variables. You can either provide the path to the PEM file, or it's contents in an ENV variable.
41
44
  RubySnowflake::Client.from_env
42
45
  ```
43
46
  Available ENV variables (see below in the config section for details)
44
- `SNOWFLAKE_URI`
45
- `SNOWFLAKE_PRIVATE_KEY_PATH`
46
- or (use either the key, or the path, key takes precedence if both are provided)
47
- `SNOWFLAKE_PRIVATE_KEY`
48
- `SNOWFLAKE_ORGANIZATION`
49
- `SNOWFLAKE_ACCOUNT`
50
- `SNOWFLAKE_USER`
51
- `SNOWFLAKE_DEFAULT_WAREHOUSE`
52
- `SNOWFLAKE_DEFAULT_DATABASE`
53
- `SNOWFLAKE_JWT_TOKEN_TTL`
54
- `SNOWFLAKE_CONNECTION_TIMEOUT`
55
- `SNOWFLAKE_MAX_CONNECTIONS`
56
- `SNOWFLAKE_MAX_THREADS_PER_QUERY`
57
- `SNOWFLAKE_THREAD_SCALE_FACTOR`
58
- `SNOWFLAKE_HTTP_RETRIES`
47
+ - `SNOWFLAKE_URI`
48
+ - `SNOWFLAKE_PRIVATE_KEY_PATH` or `SNOWFLAKE_PRIVATE_KEY`
49
+ - Use either the key or the path. Key takes precedence if both are provided.
50
+ - `SNOWFLAKE_ORGANIZATION`
51
+ - `SNOWFLAKE_ACCOUNT`
52
+ - `SNOWFLAKE_USER`
53
+ - `SNOWFLAKE_DEFAULT_WAREHOUSE`
54
+ - `SNOWFLAKE_DEFAULT_DATABASE`
55
+ - `SNOWFLAKE_JWT_TOKEN_TTL`
56
+ - `SNOWFLAKE_CONNECTION_TIMEOUT`
57
+ - `SNOWFLAKE_MAX_CONNECTIONS`
58
+ - `SNOWFLAKE_MAX_THREADS_PER_QUERY`
59
+ - `SNOWFLAKE_THREAD_SCALE_FACTOR`
60
+ - `SNOWFLAKE_HTTP_RETRIES`
61
+ - `SNOWFLAKE_QUERY_TIMEOUT`
62
+
63
+ ## Make queries
59
64
 
60
65
  Once you have a client, make queries
61
66
  ```ruby
@@ -68,44 +73,59 @@ result.each do |row|
68
73
  puts row["name"] # or case insensitive strings
69
74
  puts row.to_h # and can produce a hash with keys/values
70
75
  end
76
+ ```
77
+
78
+ ## Stream results
71
79
 
72
- # You can also stream results and not hold them all in memory.
73
- # The client will prefetch the next data partition only. If you
74
- # have some IO in your processing there should usually be data
75
- # available for you.
80
+ You can also stream results and not hold them all in memory. The client will prefetch the next data partition only. If you have some IO in your processing there should usually be data available for you.
81
+
82
+ ```ruby
76
83
  result = client.query("SELECT * FROM HUGETABLE", streaming: true)
77
84
  result.each do |row|
78
85
  puts row
79
86
  end
87
+ ```
80
88
 
89
+ ## Switching databases
81
90
 
82
- # You can also overwrite the database specified in the initializer, and run
83
- # your query with a different context.
91
+ You can also overwrite the database specified in the initializer, and run your query with a different context.
92
+
93
+ ```ruby
84
94
  result = client.query("SELECT * FROM SECRET_TABLE", database: "OTHER_DB")
85
95
  result.each do |row|
86
96
  puts row
87
97
  end
88
98
  ```
89
99
 
100
+ ## Switching warehouses
101
+
102
+ Clients are not warehouse specific, you can override the default warehouse per query
103
+
104
+ ```ruby
105
+ client.query("SELECT * FROM BIGTABLE", warehouse: "FAST_WH")
106
+ ```
107
+
90
108
  # Configuration Options
91
109
 
92
- The client supports the following configuration options, each with their own getter/setter except connection pool options which must be set at construction. Additionally, all except logger can be configured with environment variables (see above, but the pattern is like: "SNOWFLAKE_HTTP_RETRIES".
110
+ The client supports the following configuration options, each with their own getter/setter except connection pool options which must be set at construction. Additionally, all except logger can be configured with environment variables (see above, but the pattern is like: "SNOWFLAKE_HTTP_RETRIES". Configuration options can only be set on initiialization through `new` or `from_env`.
93
111
 
94
- `logger` - takes any ruby logger (by default it's a std lib Logger.new(STDOUT), set at DEBUG level. Not available as an ENV variable config option
95
- `log_level` - takes a log level, type is dependent on logger, for the default ruby Logger, use a level like `Logger::WARN`. Not available as an ENV variable config option.
96
- `jwt_token_ttl` - The time to live set on JWT token in seconds, defaults to 3540 (59 minutes, the longest Snowflake supports is 60)
97
- `connection_timeout` - The amount of time in seconds that the client's connection pool will wait before erroring in handing out a valid connection, defaults to 60 seconds
98
- `max_connections` - The maximum number of http connections to hold open in the connection pool. If you use the client in a threaded context, you may need to increase this to be threads * client.max_threads_per_query, defaults to 16. Can only be set on initialization.
99
- `max_threads_per_query` - The maximum number of threads the client should use to retreive data, per query, defaults to 8. If you want the client to act in a single threaded way, set this to 1
100
- `thread_scale_factor` - When downloading a result set into memory, thread count is calculated by dividing a query's partition count by this number. For details on implementation see the code in `client.rb`.
101
- `http_retries` - By default the client will retry common typically transient errors (http responses) twice, you can change the number of retries with this.
112
+ - `logger` - takes any ruby logger (by default it's a std lib Logger.new(STDOUT), set at DEBUG level. Not available as an ENV variable config option
113
+ - `log_level` - takes a log level, type is dependent on logger, for the default ruby Logger, use a level like `Logger::WARN`. Not available as an ENV variable config option.
114
+ - `jwt_token_ttl` - The time to live set on JWT token in seconds, defaults to 3540 (59 minutes, the longest Snowflake supports is 60).
115
+ - `connection_timeout` - The amount of time in seconds that the client's connection pool will wait before erroring in handing out a valid connection, defaults to 60 seconds
116
+ - `max_connections` - The maximum number of http connections to hold open in the connection pool. If you use the client in a threaded context, you may need to increase this to be threads * client.max_threads_per_query, defaults to 16.
117
+ - `max_threads_per_query` - The maximum number of threads the client should use to retreive data, per query, defaults to 8. If you want the client to act in a single threaded way, set this to 1
118
+ - `thread_scale_factor` - When downloading a result set into memory, thread count is calculated by dividing a query's partition count by this number. For details on implementation see the code in `client.rb`.
119
+ - `http_retries` - By default the client will retry common typically transient errors (http responses) twice, you can change the number of retries with this.
120
+ - `query_timeout` - By default the client will wait 10 minutes (600s) for a query to finish, you can change this default, will also set this limit in the query for snowflake to obey. Set in seconds.
102
121
 
103
122
  Example configuration:
104
123
  ```ruby
105
- client = RubySnowflake::Client.from_env
106
- client.logger = Rails.logger
107
- client.max_connections = 24
108
- client.http_retries = 1
124
+ client = RubySnowflake::Client.from_env(
125
+ logger: Rails.logger
126
+ max_connections: 24
127
+ http_retries 1
128
+ )
109
129
  end
110
130
  ```
111
131
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubySnowflake
2
4
  class Client
3
5
  class HttpConnectionWrapper
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+ require "openssl"
5
+ require "concurrent"
6
+
7
+ module RubySnowflake
8
+ class Client
9
+ class KeyPairJwtAuthManager
10
+ # requires text of a PEM formatted RSA private key
11
+ def initialize(organization, account, user, private_key, jwt_token_ttl)
12
+ @organization = organization
13
+ @account = account
14
+ @user = user
15
+ @private_key_pem = private_key
16
+ @jwt_token_ttl = jwt_token_ttl
17
+
18
+ # start with an expired value to force creation
19
+ @token_expires_at = Time.now.to_i - 1
20
+ @token_semaphore = Concurrent::Semaphore.new(1)
21
+ end
22
+
23
+ def jwt_token
24
+ return @token unless jwt_token_expired?
25
+
26
+ @token_semaphore.acquire do
27
+ now = Time.now.to_i
28
+ @token_expires_at = now + @jwt_token_ttl
29
+
30
+ private_key = OpenSSL::PKey.read(@private_key_pem)
31
+
32
+ payload = {
33
+ :iss => "#{@organization.upcase}-#{@account.upcase}.#{@user}.#{public_key_fingerprint}",
34
+ :sub => "#{@organization.upcase}-#{@account.upcase}.#{@user}",
35
+ :iat => now,
36
+ :exp => @token_expires_at
37
+ }
38
+
39
+ @token = JWT.encode payload, private_key, "RS256"
40
+ end
41
+ end
42
+
43
+ private
44
+ def jwt_token_expired?
45
+ Time.now.to_i > @token_expires_at
46
+ end
47
+
48
+ def public_key_fingerprint
49
+ reutrn @public_key_fingerprint unless @public_key_fingerprint.nil?
50
+
51
+ public_key_der = OpenSSL::PKey::RSA.new(@private_key_pem).public_key.to_der
52
+ digest = OpenSSL::Digest::SHA256.new.digest(public_key_der)
53
+ fingerprint = Base64.strict_encode64(digest)
54
+
55
+ @public_key_fingerprint = "SHA256:#{fingerprint}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,22 +5,20 @@ require "benchmark"
5
5
  require "concurrent"
6
6
  require "connection_pool"
7
7
  require "json"
8
- require "jwt"
9
8
  require "logger"
10
9
  require "net/http"
11
10
  require "oj"
12
- require "openssl"
13
11
  require "retryable"
14
12
  require "securerandom"
15
13
  require "uri"
16
14
 
17
-
18
- require_relative "result"
19
- require_relative "streaming_result"
20
15
  require_relative "client/http_connection_wrapper"
16
+ require_relative "client/key_pair_jwt_auth_manager"
21
17
  require_relative "client/single_thread_in_memory_strategy"
22
18
  require_relative "client/streaming_result_strategy"
23
19
  require_relative "client/threaded_in_memory_strategy"
20
+ require_relative "result"
21
+ require_relative "streaming_result"
24
22
 
25
23
  module RubySnowflake
26
24
  class Error < StandardError
@@ -37,6 +35,7 @@ module RubySnowflake
37
35
  class ConnectionStarvedError < Error ; end
38
36
  class RetryableBadResponseError < Error ; end
39
37
  class RequestError < Error ; end
38
+ class QueryTimeoutError < Error ; end
40
39
 
41
40
  class Client
42
41
  DEFAULT_LOGGER = Logger.new(STDOUT)
@@ -54,12 +53,26 @@ module RubySnowflake
54
53
  DEFAULT_THREAD_SCALE_FACTOR = 4
55
54
  # how many times to retry common retryable HTTP responses (i.e. 429, 504)
56
55
  DEFAULT_HTTP_RETRIES = 2
56
+ # how long to wait to allow a query to complete, in seconds
57
+ DEFAULT_QUERY_TIMEOUT = 600 # 10 minutes
57
58
 
58
- # can't be set after initialization
59
- attr_reader :connection_timeout, :max_connections
60
- attr_accessor :logger, :jwt_token_ttl, :max_threads_per_query, :thread_scale_factor, :http_retries
59
+ OJ_OPTIONS = { :bigdecimal_load => :bigdecimal }.freeze
60
+ VALID_RESPONSE_CODES = %w(200 202).freeze
61
+ POLLING_RESPONSE_CODE = "202"
62
+ POLLING_INTERVAL = 2 # seconds
61
63
 
62
- def self.from_env
64
+ # can't be set after initialization
65
+ attr_reader :connection_timeout, :max_connections, :logger, :max_threads_per_query, :thread_scale_factor, :http_retries, :query_timeout
66
+
67
+ def self.from_env(logger: DEFAULT_LOGGER,
68
+ log_level: DEFAULT_LOG_LEVEL,
69
+ jwt_token_ttl: env_option("SNOWFLAKE_JWT_TOKEN_TTL", DEFAULT_JWT_TOKEN_TTL),
70
+ connection_timeout: env_option("SNOWFLAKE_CONNECTION_TIMEOUT", DEFAULT_CONNECTION_TIMEOUT ),
71
+ max_connections: env_option("SNOWFLAKE_MAX_CONNECTIONS", DEFAULT_MAX_CONNECTIONS ),
72
+ max_threads_per_query: env_option("SNOWFLAKE_MAX_THREADS_PER_QUERY", DEFAULT_MAX_THREADS_PER_QUERY),
73
+ thread_scale_factor: env_option("SNOWFLAKE_THREAD_SCALE_FACTOR", DEFAULT_THREAD_SCALE_FACTOR),
74
+ http_retries: env_option("SNOWFLAKE_HTTP_RETRIES", DEFAULT_HTTP_RETRIES),
75
+ query_timeout: env_option("SNOWFLAKE_QUERY_TIMEOUT", DEFAULT_QUERY_TIMEOUT))
63
76
  private_key = ENV["SNOWFLAKE_PRIVATE_KEY"] || File.read(ENV["SNOWFLAKE_PRIVATE_KEY_PATH"])
64
77
 
65
78
  new(
@@ -70,12 +83,15 @@ module RubySnowflake
70
83
  ENV["SNOWFLAKE_USER"],
71
84
  ENV["SNOWFLAKE_DEFAULT_WAREHOUSE"],
72
85
  ENV["SNOWFLAKE_DEFAULT_DATABASE"],
73
- jwt_token_ttl: env_option("SNOWFLAKE_JWT_TOKEN_TTL", DEFAULT_JWT_TOKEN_TTL),
74
- connection_timeout: env_option("SNOWFLAKE_CONNECTION_TIMEOUT", DEFAULT_CONNECTION_TIMEOUT ),
75
- max_connections: env_option("SNOWFLAKE_MAX_CONNECTIONS", DEFAULT_MAX_CONNECTIONS ),
76
- max_threads_per_query: env_option("SNOWFLAKE_MAX_THREADS_PER_QUERY", DEFAULT_MAX_THREADS_PER_QUERY),
77
- thread_scale_factor: env_option("SNOWFLAKE_THREAD_SCALE_FACTOR", DEFAULT_THREAD_SCALE_FACTOR),
78
- http_retries: env_option("SNOWFLAKE_HTTP_RETRIES", DEFAULT_HTTP_RETRIES),
86
+ logger: logger,
87
+ log_level: log_level,
88
+ jwt_token_ttl: jwt_token_ttl,
89
+ connection_timeout: connection_timeout,
90
+ max_connections: max_connections,
91
+ max_threads_per_query: max_threads_per_query,
92
+ thread_scale_factor: thread_scale_factor,
93
+ http_retries: http_retries,
94
+ query_timeout: query_timeout,
79
95
  )
80
96
  end
81
97
 
@@ -88,50 +104,50 @@ module RubySnowflake
88
104
  max_connections: DEFAULT_MAX_CONNECTIONS,
89
105
  max_threads_per_query: DEFAULT_MAX_THREADS_PER_QUERY,
90
106
  thread_scale_factor: DEFAULT_THREAD_SCALE_FACTOR,
91
- http_retries: DEFAULT_HTTP_RETRIES
107
+ http_retries: DEFAULT_HTTP_RETRIES,
108
+ query_timeout: DEFAULT_QUERY_TIMEOUT
92
109
  )
93
110
  @base_uri = uri
94
- @private_key_pem = private_key
95
- @organization = organization
96
- @account = account
97
- @user = user
111
+ @key_pair_jwt_auth_manager =
112
+ KeyPairJwtAuthManager.new(organization, account, user, private_key, jwt_token_ttl)
98
113
  @default_warehouse = default_warehouse
99
- @public_key_fingerprint = public_key_fingerprint(@private_key_pem)
100
114
  @default_database = default_database
101
115
 
102
116
  # set defaults for config settings
103
117
  @logger = logger
104
118
  @logger.level = log_level
105
- @jwt_token_ttl = jwt_token_ttl
106
119
  @connection_timeout = connection_timeout
107
120
  @max_connections = max_connections
108
121
  @max_threads_per_query = max_threads_per_query
109
122
  @thread_scale_factor = thread_scale_factor
110
123
  @http_retries = http_retries
124
+ @query_timeout = query_timeout
111
125
 
112
- # start with an expired value to force creation
113
- @token_expires_at = Time.now.to_i - 1
114
- @token_semaphore = Concurrent::Semaphore.new(1)
126
+ # Do NOT use normally, this exists for tests so we can reliably trigger the polling
127
+ # response workflow from snowflake in tests
128
+ @_enable_polling_queries = false
115
129
  end
116
130
 
117
131
  def query(query, warehouse: nil, streaming: false, database: nil)
118
132
  warehouse ||= @default_warehouse
119
133
  database ||= @default_database
120
134
 
135
+ query_start_time = Time.now.to_i
121
136
  response = nil
122
137
  connection_pool.with do |connection|
123
138
  request_body = {
124
- "statement" => query, "warehouse" => warehouse, "database" => database
139
+ "statement" => query, "warehouse" => warehouse,
140
+ "database" => database, "timeout" => @query_timeout
125
141
  }
126
142
 
127
143
  response = request_with_auth_and_headers(
128
144
  connection,
129
145
  Net::HTTP::Post,
130
- "/api/v2/statements?requestId=#{SecureRandom.uuid}",
146
+ "/api/v2/statements?requestId=#{SecureRandom.uuid}&async=#{@_enable_polling_queries}",
131
147
  Oj.dump(request_body)
132
148
  )
133
149
  end
134
- retreive_result_set(response, streaming)
150
+ retreive_result_set(query_start_time, query, response, streaming)
135
151
  end
136
152
 
137
153
  alias fetch query
@@ -157,36 +173,12 @@ module RubySnowflake
157
173
  @port ||= URI.parse(@base_uri).port
158
174
  end
159
175
 
160
- def jwt_token
161
- return @token unless jwt_token_expired?
162
-
163
- @token_semaphore.acquire do
164
- now = Time.now.to_i
165
- @token_expires_at = now + @jwt_token_ttl
166
-
167
- private_key = OpenSSL::PKey.read(@private_key_pem)
168
-
169
- payload = {
170
- :iss => "#{@organization.upcase}-#{@account.upcase}.#{@user}.#{@public_key_fingerprint}",
171
- :sub => "#{@organization.upcase}-#{@account.upcase}.#{@user}",
172
- :iat => now,
173
- :exp => @token_expires_at
174
- }
175
-
176
- @token = JWT.encode payload, private_key, "RS256"
177
- end
178
- end
179
-
180
- def jwt_token_expired?
181
- Time.now.to_i > @token_expires_at
182
- end
183
-
184
176
  def request_with_auth_and_headers(connection, request_class, path, body=nil)
185
177
  uri = URI.parse("#{@base_uri}#{path}")
186
178
  request = request_class.new(uri)
187
179
  request["Content-Type"] = "application/json"
188
180
  request["Accept"] = "application/json"
189
- request["Authorization"] = "Bearer #{jwt_token}"
181
+ request["Authorization"] = "Bearer #{@key_pair_jwt_auth_manager.jwt_token}"
190
182
  request["X-Snowflake-Authorization-Token-Type"] = "KEYPAIR_JWT"
191
183
  request.body = body unless body.nil?
192
184
 
@@ -202,7 +194,7 @@ module RubySnowflake
202
194
  end
203
195
 
204
196
  def raise_on_bad_response(response)
205
- return if response.code == "200"
197
+ return if VALID_RESPONSE_CODES.include? response.code
206
198
 
207
199
  # there are a class of errors we want to retry rather than just giving up
208
200
  if retryable_http_response_code?(response.code)
@@ -229,9 +221,50 @@ module RubySnowflake
229
221
  end
230
222
  end
231
223
 
232
- def retreive_result_set(response, streaming)
233
- json_body = Oj.load(response.body, oj_options)
224
+ def poll_for_completion_or_timeout(query_start_time, query, statement_handle)
225
+ first_data_json_body = nil
226
+
227
+ connection_pool.with do |connection|
228
+ loop do
229
+ sleep POLLING_INTERVAL
230
+
231
+ if Time.now.to_i - query_start_time > @query_timeout
232
+ cancelled = attempt_to_cancel_and_silence_errors(connection, statement_handle)
233
+ raise QueryTimeoutError.new("Query timed out. Query cancelled? #{cancelled} Query: #{query}")
234
+ end
235
+
236
+ poll_response = request_with_auth_and_headers(connection, Net::HTTP::Get,
237
+ "/api/v2/statements/#{statement_handle}")
238
+ if poll_response.code == POLLING_RESPONSE_CODE
239
+ next
240
+ else
241
+ return poll_response
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ def attempt_to_cancel_and_silence_errors(connection, statement_handle)
248
+ cancel_response = request_with_auth_and_headers(connection, Net::HTTP::Post,
249
+ "/api/v2/#{statement_handle}/cancel")
250
+ true
251
+ rescue Error => error
252
+ if error.is_a?(BadResponseError) && error.message.include?("404")
253
+ return true # snowflake cancelled it before we did
254
+ end
255
+ @logger.error("Error on attempting to cancel query #{statement_handle}, will raise a QueryTimeoutError")
256
+ false
257
+ end
258
+
259
+ def retreive_result_set(query_start_time, query, response, streaming)
260
+ json_body = Oj.load(response.body, OJ_OPTIONS)
234
261
  statement_handle = json_body["statementHandle"]
262
+
263
+ if response.code == POLLING_RESPONSE_CODE
264
+ result_response = poll_for_completion_or_timeout(query_start_time, query, statement_handle)
265
+ json_body = Oj.load(result_response.body, OJ_OPTIONS)
266
+ end
267
+
235
268
  num_threads = number_of_threads_to_use(json_body["resultSetMetaData"]["partitionInfo"].size)
236
269
  retreive_proc = ->(index) { retreive_partition_data(statement_handle, index) }
237
270
 
@@ -254,8 +287,8 @@ module RubySnowflake
254
287
  )
255
288
  end
256
289
 
257
- partition_json = nil
258
- bm = Benchmark.measure { partition_json = Oj.load(partition_response.body, oj_options) }
290
+ partition_json = {}
291
+ bm = Benchmark.measure { partition_json = Oj.load(partition_response.body, OJ_OPTIONS) }
259
292
  logger.debug { "JSON parsing took: #{bm.real}" }
260
293
  partition_data = partition_json["data"]
261
294
 
@@ -265,17 +298,5 @@ module RubySnowflake
265
298
  def number_of_threads_to_use(partition_count)
266
299
  [[1, (partition_count / @thread_scale_factor.to_f).ceil].max, @max_threads_per_query].min
267
300
  end
268
-
269
- def oj_options
270
- { :bigdecimal_load => :bigdecimal }
271
- end
272
-
273
- def public_key_fingerprint(private_key_pem_string)
274
- public_key_der = OpenSSL::PKey::RSA.new(private_key_pem_string).public_key.to_der
275
- digest = OpenSSL::Digest::SHA256.new.digest(public_key_der)
276
- fingerprint = Base64.strict_encode64(digest)
277
-
278
- "SHA256:#{fingerprint}"
279
- end
280
301
  end
281
302
  end
@@ -31,14 +31,19 @@ module RubySnowflake
31
31
  else
32
32
  BigDecimal(@data[index]).round(@row_types[index][:scale])
33
33
  end
34
+
35
+ # snowflake treats these all as 64 bit IEEE 754 floating point numbers, and will we too
34
36
  when :float, :double, :"double precision", :real
35
- # snowflake treats these all as 64 bit IEEE 754 floating point numbers, and will we too
36
37
  Float(@data[index])
37
- when :time, :datetime, :timestamp, :timestamp_ltz, :timestamp_ntz
38
+
39
+ # Despite snowflake indicating that it sends the offset in minutes, the actual time in UTC
40
+ # is always sent in the first half of the data. If an offset is sent it looks like:
41
+ # "1641008096.123000000 1980"
42
+ # If there isn't one, it's just like this:
43
+ # "1641065696.123000000"
44
+ # in all cases, the actual time, in UTC is the float value, and the offset is ignorable
45
+ when :time, :datetime, :timestamp, :timestamp_ntz, :timestamp_ltz, :timestamp_tz
38
46
  Time.strptime(@data[index], TIME_FORMAT).utc
39
- when :timestamp_tz
40
- timestamp, offset_minutes = @data[index].split(" ")
41
- Time.strptime(@data[index], TIME_FORMAT).utc - (Integer(offset_minutes) * 60)
42
47
  else
43
48
  @data[index]
44
49
  end
@@ -1,3 +1,3 @@
1
1
  module RubySnowflake
2
- VERSION = '0.1.2'
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb_snowflake_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rinsed
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-04 00:00:00.000000000 Z
11
+ date: 2023-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -117,6 +117,7 @@ files:
117
117
  - lib/rb_snowflake_client.rb
118
118
  - lib/ruby_snowflake/client.rb
119
119
  - lib/ruby_snowflake/client/http_connection_wrapper.rb
120
+ - lib/ruby_snowflake/client/key_pair_jwt_auth_manager.rb
120
121
  - lib/ruby_snowflake/client/single_thread_in_memory_strategy.rb
121
122
  - lib/ruby_snowflake/client/streaming_result_strategy.rb
122
123
  - lib/ruby_snowflake/client/threaded_in_memory_strategy.rb