rb_snowflake_client 1.3.0 → 1.5.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 +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/release-gh-packages.yml +3 -2
- data/.github/workflows/release-rubygems.yml +3 -2
- data/CHANGELOG.md +138 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +40 -16
- data/README.md +75 -11
- data/lib/ruby_snowflake/client/key_pair_jwt_auth_manager.rb +10 -2
- data/lib/ruby_snowflake/client.rb +58 -39
- data/lib/ruby_snowflake/row.rb +25 -5
- data/lib/ruby_snowflake/streaming_result.rb +10 -0
- data/lib/ruby_snowflake/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddf76c34f14ea4b7192e5bd3dbdb18b7d176e66c6327fab97ef8d4352138d964
|
4
|
+
data.tar.gz: 20223663cc72100e28b22a4ea572611d2b2bc8291519d1cde9f2b536c02865f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 266de51be70f28c748b703fc55787b0a495404a62226fba151c44dfe00ce31465c3aae5e516ba362cdfa8a22a0d597514d86fd6644f263a046cb275b0a3e3f46
|
7
|
+
data.tar.gz: b0e90b538d9b6557d7de1b58f085570097036df3a9d1ea6be500babb6b56fafb50f21a4d905f1c50e8a274df7ffdd58b6268d65f9dc2220c7e9f70680662479c
|
data/.github/workflows/ci.yml
CHANGED
@@ -12,9 +12,9 @@ jobs:
|
|
12
12
|
steps:
|
13
13
|
- uses: actions/checkout@v3
|
14
14
|
- name: Set up Ruby
|
15
|
-
uses: ruby/setup-ruby@
|
15
|
+
uses: ruby/setup-ruby@v1
|
16
16
|
with:
|
17
|
-
ruby-version: '3.
|
17
|
+
ruby-version: '3.3'
|
18
18
|
- name: Install dependencies
|
19
19
|
run: bundle install
|
20
20
|
- name: Build gem
|
@@ -1,5 +1,6 @@
|
|
1
1
|
name: Release to Github Packages
|
2
2
|
on:
|
3
|
+
workflow_dispatch:
|
3
4
|
push:
|
4
5
|
branches:
|
5
6
|
- "master"
|
@@ -14,9 +15,9 @@ jobs:
|
|
14
15
|
steps:
|
15
16
|
- uses: actions/checkout@v3
|
16
17
|
- name: Set up Ruby
|
17
|
-
uses: ruby/setup-ruby@
|
18
|
+
uses: ruby/setup-ruby@v1
|
18
19
|
with:
|
19
|
-
ruby-version: '3.
|
20
|
+
ruby-version: '3.3'
|
20
21
|
- name: Install dependencies
|
21
22
|
run: bundle install
|
22
23
|
- name: Build gem
|
@@ -1,5 +1,6 @@
|
|
1
1
|
name: Release to Rubygems
|
2
2
|
on:
|
3
|
+
workflow_dispatch:
|
3
4
|
push:
|
4
5
|
branches:
|
5
6
|
- "master"
|
@@ -11,9 +12,9 @@ jobs:
|
|
11
12
|
steps:
|
12
13
|
- uses: actions/checkout@v3
|
13
14
|
- name: Set up Ruby
|
14
|
-
uses: ruby/setup-ruby@
|
15
|
+
uses: ruby/setup-ruby@v1
|
15
16
|
with:
|
16
|
-
ruby-version: '3.
|
17
|
+
ruby-version: '3.3'
|
17
18
|
- name: Install dependencies
|
18
19
|
run: bundle install
|
19
20
|
- name: Publish to RubyGems
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## Unreleased
|
9
|
+
|
10
|
+
## [1.5.0] - 2025-10-14
|
11
|
+
### Added
|
12
|
+
- Instrumentation feature added for Active Support users
|
13
|
+
- Added `query_timeout` as a per-query parameter, allowing timeout override on individual queries
|
14
|
+
### Fixed
|
15
|
+
- `query_timeout` now properly sends timeout parameter to Snowflake API for server-side enforcement
|
16
|
+
- Streaming mode now releases consumed records, fixing memory leak. Note: if you were iterating over streaming results more than once, this is a breaking change (though that was not its intended usage).
|
17
|
+
|
18
|
+
## [1.4.0] - 2025-05-01
|
19
|
+
### Added
|
20
|
+
- Enhanced Row API to implement Enumerable interface
|
21
|
+
- Added case-insensitive access to Row columns via both symbol and string keys
|
22
|
+
- Added numeric column access to Row (e.g., `row[0]`)
|
23
|
+
- Support setting organization or ENV["SNOWFLAKE_ORGANIZATION"] to nil or "" in JWT authentication
|
24
|
+
- Added default_role parameter and SNOWFLAKE_DEFAULT_ROLE env variable
|
25
|
+
|
26
|
+
## [1.3.0] - 2025-01-03
|
27
|
+
### Changed
|
28
|
+
- Bumped gem dependencies to newer versions
|
29
|
+
- Added support for role parameter in Client and query method
|
30
|
+
|
31
|
+
## [1.2.0] - 2025-01-03
|
32
|
+
### Changed
|
33
|
+
- Switched from Oj to JSON gem for parsing
|
34
|
+
- Improved performance by utilizing the optimized JSON gem
|
35
|
+
|
36
|
+
## [1.1.5] - 2024-12-19
|
37
|
+
### Fixed
|
38
|
+
- Parse exception detail OR message for better error handling
|
39
|
+
|
40
|
+
## [1.1.4] - 2024-11-05
|
41
|
+
### Fixed
|
42
|
+
- Fixed ENV variable issue
|
43
|
+
|
44
|
+
## [1.1.3] - 2024-08-09
|
45
|
+
### Added
|
46
|
+
- Retry HTTP codes in the 3xx range
|
47
|
+
|
48
|
+
## [1.1.2] - 2024-08-06
|
49
|
+
### Fixed
|
50
|
+
- CI error fixes
|
51
|
+
|
52
|
+
## [1.1.1] - 2024-07-12
|
53
|
+
### Fixed
|
54
|
+
- Added 502 to specific list of retryable HTTP error codes
|
55
|
+
- Fixed issue with checking string code presence in an array of integer values
|
56
|
+
|
57
|
+
## [1.1.0] - 2024-06-05
|
58
|
+
### Added
|
59
|
+
- Support for specifying a schema in query method
|
60
|
+
- Merged multiple community contributions
|
61
|
+
|
62
|
+
## [1.0.6] - 2024-06-05
|
63
|
+
### Added
|
64
|
+
- Allow specifying schema in query method
|
65
|
+
|
66
|
+
## [1.0.5] - 2024-03-20
|
67
|
+
### Added
|
68
|
+
- Added exponential backoff to retryable calls
|
69
|
+
- Improved handling of rate limiting (429 responses)
|
70
|
+
|
71
|
+
## [1.0.4] - 2024-01-30
|
72
|
+
### Fixed
|
73
|
+
- Fixed raise arguments
|
74
|
+
- Now properly raising OpenSSL errors to retry them
|
75
|
+
|
76
|
+
## [1.0.3] - 2024-01-17
|
77
|
+
### Fixed
|
78
|
+
- Now upcasing database and warehouse fields in requests
|
79
|
+
- Fixed error where lowercase field names would result in "Unable to run command without specifying database/warehouse"
|
80
|
+
|
81
|
+
## [1.0.2] - 2024-01-16
|
82
|
+
### Fixed
|
83
|
+
- Fixed typo in key pair memoization
|
84
|
+
|
85
|
+
## [1.0.1] - 2024-01-09
|
86
|
+
### Added
|
87
|
+
- Added `create_jwt_token` helper method for testing
|
88
|
+
- Support for time travel in tests
|
89
|
+
|
90
|
+
## [1.0.0] - 2023-12-11
|
91
|
+
### Changed
|
92
|
+
- First stable release
|
93
|
+
- Fixed markdown links in documentation
|
94
|
+
|
95
|
+
## [0.3.0] - 2023-12-08
|
96
|
+
### Added
|
97
|
+
- Support for Snowflake polling responses
|
98
|
+
- Handle async query execution
|
99
|
+
|
100
|
+
## [0.2.0] - 2023-12-07
|
101
|
+
### Added
|
102
|
+
- Extracted authentication logic into its own class
|
103
|
+
- Improved time handling for various Snowflake date/time types
|
104
|
+
- Support for TIME, DATETIME, TIMESTAMP, TIMESTAMP_LTZ, TIMESTAMP_NTZ, TIMESTAMP_TZ
|
105
|
+
|
106
|
+
## [0.1.2] - 2023-12-04
|
107
|
+
### Added
|
108
|
+
- Support for database parameter in requests
|
109
|
+
- Added missing dependencies to gemspec
|
110
|
+
|
111
|
+
## [0.1.1] - 2023-12-01
|
112
|
+
### Added
|
113
|
+
- Added `fetch` as an alias for `query` for compatibility with other clients
|
114
|
+
|
115
|
+
## [0.1.0] - 2023-11-28
|
116
|
+
### Added
|
117
|
+
- First minor version release with basic functionality
|
118
|
+
- Support for querying Snowflake with the HTTP API
|
119
|
+
- Support for streaming results
|
120
|
+
|
121
|
+
## [0.0.6] - 2023-11-27
|
122
|
+
### Changed
|
123
|
+
- Cleaned up key pair authentication
|
124
|
+
- Improved documentation with better setup instructions
|
125
|
+
|
126
|
+
## [0.0.5] - 2023-11-27
|
127
|
+
### Fixed
|
128
|
+
- Various bug fixes and improvements
|
129
|
+
|
130
|
+
## [0.0.4] - 2023-11-22
|
131
|
+
### Changed
|
132
|
+
- Fixed type handling for query results
|
133
|
+
- All specs now pass
|
134
|
+
|
135
|
+
## [0.0.3] - 2023-11-21
|
136
|
+
### Changed
|
137
|
+
- Renamed to RubySnowflake namespace
|
138
|
+
- Initial gem structure
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rb_snowflake_client (1.
|
4
|
+
rb_snowflake_client (1.5.0)
|
5
5
|
bigdecimal (>= 3.0)
|
6
6
|
concurrent-ruby (>= 1.2)
|
7
7
|
connection_pool (>= 2.4)
|
@@ -13,42 +13,66 @@ PATH
|
|
13
13
|
GEM
|
14
14
|
remote: https://rubygems.org/
|
15
15
|
specs:
|
16
|
-
|
17
|
-
|
16
|
+
activesupport (8.0.3)
|
17
|
+
base64
|
18
|
+
benchmark (>= 0.3)
|
19
|
+
bigdecimal
|
20
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
21
|
+
connection_pool (>= 2.2.5)
|
22
|
+
drb
|
23
|
+
i18n (>= 1.6, < 2)
|
24
|
+
logger (>= 1.4.2)
|
25
|
+
minitest (>= 5.1)
|
26
|
+
securerandom (>= 0.3)
|
27
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
28
|
+
uri (>= 0.13.1)
|
29
|
+
base64 (0.3.0)
|
30
|
+
benchmark (0.4.1)
|
31
|
+
bigdecimal (3.3.1)
|
18
32
|
coderay (1.1.3)
|
19
|
-
concurrent-ruby (1.3.
|
20
|
-
connection_pool (2.4
|
21
|
-
diff-lcs (1.
|
22
|
-
dotenv (3.1.
|
23
|
-
|
24
|
-
|
33
|
+
concurrent-ruby (1.3.5)
|
34
|
+
connection_pool (2.5.4)
|
35
|
+
diff-lcs (1.6.2)
|
36
|
+
dotenv (3.1.8)
|
37
|
+
drb (2.2.3)
|
38
|
+
i18n (1.14.7)
|
39
|
+
concurrent-ruby (~> 1.0)
|
40
|
+
json (2.15.1)
|
41
|
+
jwt (3.1.2)
|
25
42
|
base64
|
43
|
+
logger (1.7.0)
|
26
44
|
method_source (1.1.0)
|
27
|
-
|
45
|
+
minitest (5.26.0)
|
46
|
+
parallel (1.27.0)
|
28
47
|
pry (0.15.2)
|
29
48
|
coderay (~> 1.1)
|
30
49
|
method_source (~> 1.0)
|
31
|
-
rake (13.
|
50
|
+
rake (13.3.0)
|
32
51
|
retryable (3.0.5)
|
33
|
-
rspec (3.13.
|
52
|
+
rspec (3.13.1)
|
34
53
|
rspec-core (~> 3.13.0)
|
35
54
|
rspec-expectations (~> 3.13.0)
|
36
55
|
rspec-mocks (~> 3.13.0)
|
37
|
-
rspec-core (3.13.
|
56
|
+
rspec-core (3.13.5)
|
38
57
|
rspec-support (~> 3.13.0)
|
39
|
-
rspec-expectations (3.13.
|
58
|
+
rspec-expectations (3.13.5)
|
40
59
|
diff-lcs (>= 1.2.0, < 2.0)
|
41
60
|
rspec-support (~> 3.13.0)
|
42
|
-
rspec-mocks (3.13.
|
61
|
+
rspec-mocks (3.13.5)
|
43
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
44
63
|
rspec-support (~> 3.13.0)
|
45
|
-
rspec-support (3.13.
|
64
|
+
rspec-support (3.13.6)
|
65
|
+
securerandom (0.4.1)
|
66
|
+
tzinfo (2.0.6)
|
67
|
+
concurrent-ruby (~> 1.0)
|
68
|
+
uri (1.0.4)
|
46
69
|
|
47
70
|
PLATFORMS
|
48
71
|
arm64-darwin-22
|
49
72
|
ruby
|
50
73
|
|
51
74
|
DEPENDENCIES
|
75
|
+
activesupport
|
52
76
|
bundler
|
53
77
|
parallel
|
54
78
|
pry
|
data/README.md
CHANGED
@@ -30,11 +30,12 @@ require "rb_snowflake_client"
|
|
30
30
|
client = RubySnowflake::Client.new(
|
31
31
|
"https://yourinstance.region.snowflakecomputing.com", # insert your URL here
|
32
32
|
File.read("secrets/my_key.pem"), # your private key in PEM format (scroll down for instructions)
|
33
|
-
"snowflake-organization", # your account name (doesn't match your URL)
|
33
|
+
"snowflake-organization", # your account name (doesn't match your URL), using nil may be required depending on your snowflake account
|
34
34
|
"snowflake-account", # typically your subdomain
|
35
35
|
"snowflake-user", # Your snowflake user
|
36
36
|
"some_warehouse", # The name of your warehouse to use by default
|
37
37
|
"some_database", # The name of the database in the context of which the queries will run
|
38
|
+
default_role: "some_role", # The name of the role with which the queries will run. A `nil` value uses the primary role of the user.
|
38
39
|
max_connections: 12, # Config options can be passed in
|
39
40
|
connection_timeout: 45, # See below for the full set of options
|
40
41
|
query_timeout: 1200, # how long to wait for queries, in seconds
|
@@ -48,10 +49,12 @@ Available ENV variables (see below in the config section for details)
|
|
48
49
|
- `SNOWFLAKE_PRIVATE_KEY_PATH` or `SNOWFLAKE_PRIVATE_KEY`
|
49
50
|
- Use either the key or the path. Key takes precedence if both are provided.
|
50
51
|
- `SNOWFLAKE_ORGANIZATION`
|
52
|
+
- Optional, if you leave it off, the library will authenticate with an account name of only SNOWFLAKE_ACCOUNT
|
51
53
|
- `SNOWFLAKE_ACCOUNT`
|
52
54
|
- `SNOWFLAKE_USER`
|
53
55
|
- `SNOWFLAKE_DEFAULT_WAREHOUSE`
|
54
56
|
- `SNOWFLAKE_DEFAULT_DATABASE`
|
57
|
+
- `SNOWFLAKE_DEFAULT_ROLE`
|
55
58
|
- `SNOWFLAKE_JWT_TOKEN_TTL`
|
56
59
|
- `SNOWFLAKE_CONNECTION_TIMEOUT`
|
57
60
|
- `SNOWFLAKE_MAX_CONNECTIONS`
|
@@ -69,9 +72,19 @@ result = client.query("SELECT ID, NAME FROM SOMETABLE")
|
|
69
72
|
|
70
73
|
# result is Enumerable
|
71
74
|
result.each do |row|
|
72
|
-
|
73
|
-
puts row[
|
74
|
-
puts row
|
75
|
+
# Row implements Enumerable and provides flexible column access:
|
76
|
+
puts row[:id] # access with symbols (case-insensitive)
|
77
|
+
puts row["name"] # access with strings (case-insensitive)
|
78
|
+
puts row[0] # access with numeric indices
|
79
|
+
|
80
|
+
# Row has Enumerable methods
|
81
|
+
puts row.keys # get all column names
|
82
|
+
puts row.values # get all values
|
83
|
+
puts row.to_h # convert to Hash with column names as keys
|
84
|
+
|
85
|
+
# Use all Enumerable methods
|
86
|
+
row.each { |column_name, value| puts "#{column_name}: #{value}" }
|
87
|
+
filtered = row.select { |column, value| column.start_with?("i") }
|
75
88
|
end
|
76
89
|
```
|
77
90
|
|
@@ -111,6 +124,22 @@ client.query("SELECT * FROM BIGTABLE", warehouse: "FAST_WH")
|
|
111
124
|
client.query("SELECT * FROM BIGTABLE", schema: "MY_SCHEMA")
|
112
125
|
```
|
113
126
|
|
127
|
+
## Specifying role
|
128
|
+
|
129
|
+
Queries by default use the primary role assigned to the account. If there are multiple roles you can switch between them on a per query basis.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
client.query("SELECT * FROM BIGTABLE", role: "MY_ROLE")
|
133
|
+
```
|
134
|
+
|
135
|
+
## Query timeout
|
136
|
+
|
137
|
+
You can override the query timeout on a per-query basis. The timeout is specified in seconds and will be enforced by both Snowflake server-side and the client-side polling mechanism.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
client.query("SELECT * FROM BIGTABLE", query_timeout: 30)
|
141
|
+
```
|
142
|
+
|
114
143
|
## Binding parameters
|
115
144
|
|
116
145
|
Say we have `BIGTABLE` with a `data` column of a type `VARIANT`.
|
@@ -119,16 +148,45 @@ Say we have `BIGTABLE` with a `data` column of a type `VARIANT`.
|
|
119
148
|
json_string = '{"valid": "json"}'
|
120
149
|
query = "insert into BIGTABLE(data) select parse_json(?)"
|
121
150
|
bindings = {
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
}
|
151
|
+
"1" => {
|
152
|
+
"type" => "TEXT",
|
153
|
+
"value" => "Other Event"
|
154
|
+
}
|
155
|
+
}
|
127
156
|
client.query(query, bindings: bindings)
|
128
157
|
```
|
129
158
|
|
130
159
|
For additional information about binding parameters refer to snowflake documentation: https://docs.snowflake.com/en/developer-guide/sql-api/submitting-requests#using-bind-variables-in-a-statement
|
131
160
|
|
161
|
+
## Instrumentation
|
162
|
+
|
163
|
+
If ActiveSupport is available, this library additionally emits [notification events](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) around queries. You can subscribe to those to track timing, query counts, etc.
|
164
|
+
|
165
|
+
* `rb_snowflake_client.snowflake_query.finish`: published at query end
|
166
|
+
|
167
|
+
Events receive a payload with the following properties:
|
168
|
+
* `database`: snowflake database
|
169
|
+
* `schema`: snowflake schema
|
170
|
+
* `warehouse`: snowflake warehouse
|
171
|
+
* `query_id`: random UUID for the query
|
172
|
+
* `query_name`: argument passed to query/fetch
|
173
|
+
* `exception`: present if the query raised an error, see [Notifications documentation](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html#module-ActiveSupport::Notifications-label-Subscribers) for details
|
174
|
+
* `exception_object`: present if the query raised an error, see [Notifications documentation](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html#module-ActiveSupport::Notifications-label-Subscribers) for details
|
175
|
+
|
176
|
+
An example integration with [Datadog](https://www.rubydoc.info/gems/datadog) might look like this:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
ActiveSupport::Notifications.subscribe("rb_snowflake_client.snowflake_query.finish") do |name, start, finish, id, payload|
|
180
|
+
span = Datadog::Tracing.trace(payload[:query_name] || "snowflake_query",
|
181
|
+
resource: "snowflake",
|
182
|
+
start_time: start,
|
183
|
+
tags: payload,
|
184
|
+
type: Datadog::Tracing::Metadata::Ext::AppTypes::TYPE_DB)
|
185
|
+
|
186
|
+
span.finish(finish)
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
132
190
|
# Configuration Options
|
133
191
|
|
134
192
|
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 initialization through `new` or `from_env`.
|
@@ -157,7 +215,9 @@ end
|
|
157
215
|
|
158
216
|
1. Does not yet support multiple statements (work around is to wrap in `BEGIN ... END`)
|
159
217
|
2. Only supports key pair authentication
|
160
|
-
3.
|
218
|
+
3. It's faster to work directly with the row value and not call to_h if you don't need to
|
219
|
+
4. Rows are Enumerable, providing access to methods like `each`, `map`, `select`, `keys`, and `values`
|
220
|
+
5. Row column access is case-insensitive and supports string keys, symbol keys, and numeric indices
|
161
221
|
|
162
222
|
# Setting up a user for key pair authentication
|
163
223
|
|
@@ -205,7 +265,7 @@ or alternatively, use the client to verify:
|
|
205
265
|
client = RubySnowflake::Client.new(
|
206
266
|
"https://yourinstance.region.snowflakecomputing.com", # insert your URL here
|
207
267
|
File.read("secrets/my_key.pem"), # path to your private key
|
208
|
-
"snowflake-organization", # your account name (doesn't match your URL)
|
268
|
+
"snowflake-organization", # your account name (doesn't match your URL), using nil may be required depending on your snowflake account
|
209
269
|
"snowflake-account", # typically your subdomain
|
210
270
|
"snowflake-user", # Your snowflake user
|
211
271
|
"some_warehouse", # The name of your warehouse to use by default
|
@@ -213,6 +273,10 @@ client = RubySnowflake::Client.new(
|
|
213
273
|
)
|
214
274
|
```
|
215
275
|
|
276
|
+
# Change Log
|
277
|
+
|
278
|
+
See [Change Log](CHANGELOG.md)
|
279
|
+
|
216
280
|
# Code of conduct
|
217
281
|
|
218
282
|
See [Code of Coduct](CODE_OF_CONDUCT.md)
|
@@ -30,8 +30,8 @@ module RubySnowflake
|
|
30
30
|
private_key = OpenSSL::PKey.read(@private_key_pem)
|
31
31
|
|
32
32
|
payload = {
|
33
|
-
:iss => "#{
|
34
|
-
:sub => "#{
|
33
|
+
:iss => "#{account_name}.#{@user.upcase}.#{public_key_fingerprint}",
|
34
|
+
:sub => "#{account_name}.#{@user.upcase}",
|
35
35
|
:iat => now,
|
36
36
|
:exp => @token_expires_at
|
37
37
|
}
|
@@ -45,6 +45,14 @@ module RubySnowflake
|
|
45
45
|
Time.now.to_i > @token_expires_at
|
46
46
|
end
|
47
47
|
|
48
|
+
def account_name
|
49
|
+
if @organization == nil || @organization == ""
|
50
|
+
@account.upcase
|
51
|
+
else
|
52
|
+
"#{@organization.upcase}-#{@account.upcase}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
48
56
|
def public_key_fingerprint
|
49
57
|
return @public_key_fingerprint unless @public_key_fingerprint.nil?
|
50
58
|
|
@@ -12,6 +12,13 @@ require "retryable"
|
|
12
12
|
require "securerandom"
|
13
13
|
require "uri"
|
14
14
|
|
15
|
+
begin
|
16
|
+
require "active_support"
|
17
|
+
require "active_support/notifications"
|
18
|
+
rescue LoadError
|
19
|
+
# This isn't required
|
20
|
+
end
|
21
|
+
|
15
22
|
require_relative "client/http_connection_wrapper"
|
16
23
|
require_relative "client/key_pair_jwt_auth_manager"
|
17
24
|
require_relative "client/single_thread_in_memory_strategy"
|
@@ -22,22 +29,12 @@ require_relative "streaming_result"
|
|
22
29
|
|
23
30
|
module RubySnowflake
|
24
31
|
class Error < StandardError
|
25
|
-
# This will get pulled through to Sentry, see:
|
26
|
-
# https://github.com/getsentry/sentry-ruby/blob/11ecd254c0d2cae2b327f0348074e849095aa32d/sentry-ruby/lib/sentry/error_event.rb#L31-L33
|
27
|
-
attr_reader :sentry_context
|
28
|
-
|
29
32
|
def initialize(details)
|
30
|
-
@sentry_context = details
|
31
33
|
@details = details
|
32
34
|
end
|
33
35
|
|
34
36
|
def message
|
35
|
-
|
36
|
-
super
|
37
|
-
else
|
38
|
-
@details.to_s
|
39
|
-
end
|
40
|
-
|
37
|
+
@details.to_s
|
41
38
|
end
|
42
39
|
end
|
43
40
|
|
@@ -67,6 +64,8 @@ module RubySnowflake
|
|
67
64
|
DEFAULT_HTTP_RETRIES = 2
|
68
65
|
# how long to wait to allow a query to complete, in seconds
|
69
66
|
DEFAULT_QUERY_TIMEOUT = 600 # 10 minutes
|
67
|
+
# default role to use
|
68
|
+
DEFAULT_ROLE = nil
|
70
69
|
|
71
70
|
JSON_PARSE_OPTIONS = { decimal_class: BigDecimal }.freeze
|
72
71
|
VALID_RESPONSE_CODES = %w(200 202).freeze
|
@@ -74,7 +73,7 @@ module RubySnowflake
|
|
74
73
|
POLLING_INTERVAL = 2 # seconds
|
75
74
|
|
76
75
|
# can't be set after initialization
|
77
|
-
attr_reader :connection_timeout, :max_connections, :logger, :max_threads_per_query, :thread_scale_factor, :http_retries, :query_timeout
|
76
|
+
attr_reader :connection_timeout, :max_connections, :logger, :max_threads_per_query, :thread_scale_factor, :http_retries, :query_timeout, :default_role
|
78
77
|
|
79
78
|
def self.from_env(logger: DEFAULT_LOGGER,
|
80
79
|
log_level: DEFAULT_LOG_LEVEL,
|
@@ -84,14 +83,15 @@ module RubySnowflake
|
|
84
83
|
max_threads_per_query: env_option("SNOWFLAKE_MAX_THREADS_PER_QUERY", DEFAULT_MAX_THREADS_PER_QUERY),
|
85
84
|
thread_scale_factor: env_option("SNOWFLAKE_THREAD_SCALE_FACTOR", DEFAULT_THREAD_SCALE_FACTOR),
|
86
85
|
http_retries: env_option("SNOWFLAKE_HTTP_RETRIES", DEFAULT_HTTP_RETRIES),
|
87
|
-
query_timeout: env_option("SNOWFLAKE_QUERY_TIMEOUT", DEFAULT_QUERY_TIMEOUT)
|
86
|
+
query_timeout: env_option("SNOWFLAKE_QUERY_TIMEOUT", DEFAULT_QUERY_TIMEOUT),
|
87
|
+
default_role: env_option("SNOWFLAKE_DEFAULT_ROLE", DEFAULT_ROLE))
|
88
88
|
private_key =
|
89
89
|
if key = ENV["SNOWFLAKE_PRIVATE_KEY"]
|
90
90
|
key
|
91
91
|
elsif path = ENV["SNOWFLAKE_PRIVATE_KEY_PATH"]
|
92
92
|
File.read(path)
|
93
93
|
else
|
94
|
-
raise MissingConfig
|
94
|
+
raise MissingConfig, "Either ENV['SNOWFLAKE_PRIVATE_KEY'] or ENV['SNOWFLAKE_PRIVATE_KEY_PATH'] must be set"
|
95
95
|
end
|
96
96
|
|
97
97
|
new(
|
@@ -102,6 +102,7 @@ module RubySnowflake
|
|
102
102
|
ENV.fetch("SNOWFLAKE_USER"),
|
103
103
|
ENV["SNOWFLAKE_DEFAULT_WAREHOUSE"],
|
104
104
|
ENV["SNOWFLAKE_DEFAULT_DATABASE"],
|
105
|
+
default_role: ENV.fetch("SNOWFLAKE_DEFAULT_ROLE", nil),
|
105
106
|
logger: logger,
|
106
107
|
log_level: log_level,
|
107
108
|
jwt_token_ttl: jwt_token_ttl,
|
@@ -116,6 +117,7 @@ module RubySnowflake
|
|
116
117
|
|
117
118
|
def initialize(
|
118
119
|
uri, private_key, organization, account, user, default_warehouse, default_database,
|
120
|
+
default_role: nil,
|
119
121
|
logger: DEFAULT_LOGGER,
|
120
122
|
log_level: DEFAULT_LOG_LEVEL,
|
121
123
|
jwt_token_ttl: DEFAULT_JWT_TOKEN_TTL,
|
@@ -131,6 +133,7 @@ module RubySnowflake
|
|
131
133
|
KeyPairJwtAuthManager.new(organization, account, user, private_key, jwt_token_ttl)
|
132
134
|
@default_warehouse = default_warehouse
|
133
135
|
@default_database = default_database
|
136
|
+
@default_role = default_role
|
134
137
|
|
135
138
|
# set defaults for config settings
|
136
139
|
@logger = logger
|
@@ -147,29 +150,35 @@ module RubySnowflake
|
|
147
150
|
@_enable_polling_queries = false
|
148
151
|
end
|
149
152
|
|
150
|
-
def query(query, warehouse: nil, streaming: false, database: nil, schema: nil, bindings: nil)
|
153
|
+
def query(query, warehouse: nil, streaming: false, database: nil, schema: nil, bindings: nil, role: nil, query_name: nil, query_timeout: nil)
|
151
154
|
warehouse ||= @default_warehouse
|
152
155
|
database ||= @default_database
|
156
|
+
role ||= @default_role
|
157
|
+
query_timeout ||= @query_timeout
|
153
158
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
159
|
+
with_instrumentation({ database:, schema:, warehouse:, query_name: }) do
|
160
|
+
query_start_time = Time.now.to_i
|
161
|
+
response = nil
|
162
|
+
connection_pool.with do |connection|
|
163
|
+
request_body = {
|
164
|
+
"warehouse" => warehouse&.upcase,
|
165
|
+
"schema" => schema&.upcase,
|
166
|
+
"database" => database&.upcase,
|
167
|
+
"statement" => query,
|
168
|
+
"bindings" => bindings,
|
169
|
+
"role" => role,
|
170
|
+
"timeout" => query_timeout
|
171
|
+
}
|
172
|
+
|
173
|
+
response = request_with_auth_and_headers(
|
174
|
+
connection,
|
175
|
+
Net::HTTP::Post,
|
176
|
+
"/api/v2/statements?requestId=#{SecureRandom.uuid}&async=#{@_enable_polling_queries}",
|
177
|
+
request_body.to_json
|
178
|
+
)
|
179
|
+
end
|
180
|
+
retrieve_result_set(query_start_time, query, response, streaming, query_timeout)
|
171
181
|
end
|
172
|
-
retrieve_result_set(query_start_time, query, response, streaming)
|
173
182
|
end
|
174
183
|
|
175
184
|
alias fetch query
|
@@ -228,11 +237,11 @@ module RubySnowflake
|
|
228
237
|
|
229
238
|
# there are a class of errors we want to retry rather than just giving up
|
230
239
|
if retryable_http_response_code?(response.code)
|
231
|
-
raise RetryableBadResponseError
|
240
|
+
raise RetryableBadResponseError,
|
232
241
|
"Retryable bad response! Got code: #{response.code}, w/ message #{response.body}"
|
233
242
|
|
234
243
|
else # not one we should retry
|
235
|
-
raise BadResponseError
|
244
|
+
raise BadResponseError,
|
236
245
|
"Bad response! Got code: #{response.code}, w/ message #{response.body}"
|
237
246
|
end
|
238
247
|
end
|
@@ -253,7 +262,7 @@ module RubySnowflake
|
|
253
262
|
end
|
254
263
|
end
|
255
264
|
|
256
|
-
def poll_for_completion_or_timeout(query_start_time, query, statement_handle)
|
265
|
+
def poll_for_completion_or_timeout(query_start_time, query, statement_handle, query_timeout)
|
257
266
|
first_data_json_body = nil
|
258
267
|
|
259
268
|
connection_pool.with do |connection|
|
@@ -261,7 +270,7 @@ module RubySnowflake
|
|
261
270
|
sleep POLLING_INTERVAL
|
262
271
|
|
263
272
|
elapsed_time = Time.now.to_i - query_start_time
|
264
|
-
if elapsed_time >
|
273
|
+
if elapsed_time > query_timeout
|
265
274
|
cancelled = attempt_to_cancel_and_silence_errors(connection, statement_handle)
|
266
275
|
raise QueryTimeoutError.new("Query timed out. Query cancelled? #{cancelled}; Duration: #{elapsed_time}; Query: '#{query}'")
|
267
276
|
end
|
@@ -289,12 +298,12 @@ module RubySnowflake
|
|
289
298
|
false
|
290
299
|
end
|
291
300
|
|
292
|
-
def retrieve_result_set(query_start_time, query, response, streaming)
|
301
|
+
def retrieve_result_set(query_start_time, query, response, streaming, query_timeout)
|
293
302
|
json_body = JSON.parse(response.body, JSON_PARSE_OPTIONS)
|
294
303
|
statement_handle = json_body["statementHandle"]
|
295
304
|
|
296
305
|
if response.code == POLLING_RESPONSE_CODE
|
297
|
-
result_response = poll_for_completion_or_timeout(query_start_time, query, statement_handle)
|
306
|
+
result_response = poll_for_completion_or_timeout(query_start_time, query, statement_handle, query_timeout)
|
298
307
|
json_body = JSON.parse(result_response.body, JSON_PARSE_OPTIONS)
|
299
308
|
end
|
300
309
|
|
@@ -331,5 +340,15 @@ module RubySnowflake
|
|
331
340
|
def number_of_threads_to_use(partition_count)
|
332
341
|
[[1, (partition_count / @thread_scale_factor.to_f).ceil].max, @max_threads_per_query].min
|
333
342
|
end
|
343
|
+
|
344
|
+
def with_instrumentation(tags, &block)
|
345
|
+
return block.call unless defined?(::ActiveSupport) && ::ActiveSupport
|
346
|
+
|
347
|
+
::ActiveSupport::Notifications.instrument(
|
348
|
+
"rb_snowflake_client.snowflake_query.finish",
|
349
|
+
tags.merge(query_id: SecureRandom.uuid)) do
|
350
|
+
block.call
|
351
|
+
end
|
352
|
+
end
|
334
353
|
end
|
335
354
|
end
|
data/lib/ruby_snowflake/row.rb
CHANGED
@@ -5,6 +5,8 @@ require "time"
|
|
5
5
|
|
6
6
|
module RubySnowflake
|
7
7
|
class Row
|
8
|
+
include Enumerable
|
9
|
+
|
8
10
|
EPOCH_JULIAN_DAY_NUMBER = Date.new(1970,1,1).jd
|
9
11
|
TIME_FORMAT = "%s.%N".freeze
|
10
12
|
|
@@ -16,7 +18,13 @@ module RubySnowflake
|
|
16
18
|
|
17
19
|
# see: https://docs.snowflake.com/en/developer-guide/sql-api/handling-responses#getting-the-data-from-the-results
|
18
20
|
def [](column)
|
19
|
-
index = column.is_a?(Numeric)
|
21
|
+
index = if column.is_a?(Numeric)
|
22
|
+
Integer(column)
|
23
|
+
else
|
24
|
+
# Handle column names case-insensitively regardless of string or symbol
|
25
|
+
@column_to_index[column.to_s.downcase]
|
26
|
+
end
|
27
|
+
|
20
28
|
return nil if index.nil?
|
21
29
|
return nil if @data[index].nil?
|
22
30
|
|
@@ -49,12 +57,24 @@ module RubySnowflake
|
|
49
57
|
end
|
50
58
|
end
|
51
59
|
|
52
|
-
def
|
53
|
-
|
60
|
+
def each
|
61
|
+
return to_enum __method__ unless block_given?
|
62
|
+
|
54
63
|
@column_to_index.each_pair do |name, index|
|
55
|
-
|
64
|
+
yield(name, self[index])
|
56
65
|
end
|
57
|
-
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def keys
|
71
|
+
map { |k, _| k }
|
72
|
+
end
|
73
|
+
|
74
|
+
alias columns keys
|
75
|
+
|
76
|
+
def values
|
77
|
+
map { |_, v| v }
|
58
78
|
end
|
59
79
|
|
60
80
|
def to_s
|
@@ -27,9 +27,19 @@ module RubySnowflake
|
|
27
27
|
if data[index].is_a? Concurrent::Future
|
28
28
|
data[index] = data[index].value # wait for it to finish
|
29
29
|
end
|
30
|
+
|
30
31
|
data[index].each do |row|
|
31
32
|
yield wrap_row(row)
|
32
33
|
end
|
34
|
+
|
35
|
+
# After iterating over the current partition, clear the data to release memory
|
36
|
+
data[index].clear
|
37
|
+
|
38
|
+
# Reassign to a symbol so:
|
39
|
+
# - When looking at the list of partitions in `data` it is easier to detect
|
40
|
+
# - Will raise an exception if `data.each` is attempted to be called again
|
41
|
+
# - It won't trigger prefetch detection as `next_index`
|
42
|
+
data[index] = :finished
|
33
43
|
end
|
34
44
|
end
|
35
45
|
|
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: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rinsed
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bigdecimal
|
@@ -124,6 +124,7 @@ files:
|
|
124
124
|
- ".github/workflows/release-gh-packages.yml"
|
125
125
|
- ".github/workflows/release-rubygems.yml"
|
126
126
|
- ".gitignore"
|
127
|
+
- CHANGELOG.md
|
127
128
|
- CODE_OF_CONDUCT.md
|
128
129
|
- Gemfile
|
129
130
|
- Gemfile.lock
|
@@ -146,7 +147,7 @@ homepage: https://github.com/rinsed-org/rb-snowflake-client
|
|
146
147
|
licenses:
|
147
148
|
- MIT
|
148
149
|
metadata: {}
|
149
|
-
post_install_message:
|
150
|
+
post_install_message:
|
150
151
|
rdoc_options: []
|
151
152
|
require_paths:
|
152
153
|
- lib
|
@@ -161,8 +162,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
161
162
|
- !ruby/object:Gem::Version
|
162
163
|
version: '0'
|
163
164
|
requirements: []
|
164
|
-
rubygems_version: 3.
|
165
|
-
signing_key:
|
165
|
+
rubygems_version: 3.5.22
|
166
|
+
signing_key:
|
166
167
|
specification_version: 4
|
167
168
|
summary: Snowflake connector for Ruby
|
168
169
|
test_files: []
|