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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +55 -35
- data/lib/ruby_snowflake/client/http_connection_wrapper.rb +2 -0
- data/lib/ruby_snowflake/client/key_pair_jwt_auth_manager.rb +59 -0
- data/lib/ruby_snowflake/client.rb +91 -70
- data/lib/ruby_snowflake/row.rb +10 -5
- data/lib/ruby_snowflake/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b345626b574463c788bc7ff125c6297eac071ce62cd77fadb77c5da896f6089d
|
4
|
+
data.tar.gz: 3bed923cc293d33af04eb846241637d6c72a8bef8917958e50b013fa8d120f70
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 952d6ffe14f350158abbe2342815687e36564233f7de8a452c27174f2f483f76c32e331502a1a35e7e20a654f11a74c109f7018410a9a3add7f7fa42ea3e32c7
|
7
|
+
data.tar.gz: 3dcc696ab574bebb3b0416dbc9595df656c6547f7b4a36b70d32d63030dfedc54d9f84e8b9838c0fd22c3cb5a9b9ce0efdc9c74b78e3bb41a57e58c29757c120
|
data/Gemfile.lock
CHANGED
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"), #
|
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
|
-
|
47
|
-
`
|
48
|
-
`
|
49
|
-
`
|
50
|
-
`
|
51
|
-
`
|
52
|
-
`
|
53
|
-
`
|
54
|
-
`
|
55
|
-
`
|
56
|
-
`
|
57
|
-
`
|
58
|
-
`
|
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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
83
|
-
|
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.
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
|
@@ -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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
@
|
95
|
-
|
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
|
-
#
|
113
|
-
|
114
|
-
@
|
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,
|
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
|
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
|
233
|
-
|
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 =
|
258
|
-
bm = Benchmark.measure { partition_json = Oj.load(partition_response.body,
|
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
|
data/lib/ruby_snowflake/row.rb
CHANGED
@@ -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
|
-
|
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
|
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.
|
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-
|
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
|