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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef7c1798270965128711042d7d218668b318356bf6a3fac8efc2144538c5a246
4
- data.tar.gz: a04c605cfff4659c18deb58ccdca3d49db8a6d85d98825a496ab6d8641030193
3
+ metadata.gz: ddf76c34f14ea4b7192e5bd3dbdb18b7d176e66c6327fab97ef8d4352138d964
4
+ data.tar.gz: 20223663cc72100e28b22a4ea572611d2b2bc8291519d1cde9f2b536c02865f1
5
5
  SHA512:
6
- metadata.gz: 3633bc26af9b0912735ad82840795ab81c6178c381b11ae7ade7b6fd2272576a7a9d4f65a7cd22a5786ede9f2d153bc7ccb3a5816fe0263e48cb6f5df448297b
7
- data.tar.gz: 449e2d7140b58c6d629fd17bf4641b3d698a03d10a1fb6df3444fd3bd5bf6fd6da1f146c59d96dcedd3932ddc1f5b4b086524f427385510435e36dc8de052824
6
+ metadata.gz: 266de51be70f28c748b703fc55787b0a495404a62226fba151c44dfe00ce31465c3aae5e516ba362cdfa8a22a0d597514d86fd6644f263a046cb275b0a3e3f46
7
+ data.tar.gz: b0e90b538d9b6557d7de1b58f085570097036df3a9d1ea6be500babb6b56fafb50f21a4d905f1c50e8a274df7ffdd58b6268d65f9dc2220c7e9f70680662479c
@@ -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@904f3fef85a9c80a3750cbe7d5159268fd5caa9f
15
+ uses: ruby/setup-ruby@v1
16
16
  with:
17
- ruby-version: '3.0.6'
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@904f3fef85a9c80a3750cbe7d5159268fd5caa9f
18
+ uses: ruby/setup-ruby@v1
18
19
  with:
19
- ruby-version: '3.0.6'
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@904f3fef85a9c80a3750cbe7d5159268fd5caa9f
15
+ uses: ruby/setup-ruby@v1
15
16
  with:
16
- ruby-version: '3.0.6'
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
@@ -8,6 +8,10 @@ gemspec
8
8
  gem "bundler"
9
9
  gem "rake"
10
10
 
11
+ group :development, :test do
12
+ gem "activesupport"
13
+ end
14
+
11
15
  group :development do
12
16
  gem "parallel"
13
17
  gem "pry"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rb_snowflake_client (1.3.0)
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
- base64 (0.2.0)
17
- bigdecimal (3.1.9)
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.4)
20
- connection_pool (2.4.1)
21
- diff-lcs (1.5.1)
22
- dotenv (3.1.7)
23
- json (2.9.1)
24
- jwt (2.10.1)
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
- parallel (1.26.3)
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.2.1)
50
+ rake (13.3.0)
32
51
  retryable (3.0.5)
33
- rspec (3.13.0)
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.2)
56
+ rspec-core (3.13.5)
38
57
  rspec-support (~> 3.13.0)
39
- rspec-expectations (3.13.3)
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.2)
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.2)
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
- puts row[:id] # row supports access with symbols
73
- puts row["name"] # or case insensitive strings
74
- puts row.to_h # and can produce a hash with keys/values
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
- "1": {
123
- "type": "TEXT",
124
- "value": json_string
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. Its faster to work directly with the row value and not call to_h if you don't need to
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 => "#{@organization.upcase}-#{@account.upcase}.#{@user.upcase}.#{public_key_fingerprint}",
34
- :sub => "#{@organization.upcase}-#{@account.upcase}.#{@user.upcase}",
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
- if @details == {}
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.new({}), "Either ENV['SNOWFLAKE_PRIVATE_KEY'] or ENV['SNOWFLAKE_PRIVATE_KEY_PATH'] must be set"
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
- query_start_time = Time.now.to_i
155
- response = nil
156
- connection_pool.with do |connection|
157
- request_body = {
158
- "warehouse" => warehouse&.upcase,
159
- "schema" => schema&.upcase,
160
- "database" => database&.upcase,
161
- "statement" => query,
162
- "bindings" => bindings
163
- }
164
-
165
- response = request_with_auth_and_headers(
166
- connection,
167
- Net::HTTP::Post,
168
- "/api/v2/statements?requestId=#{SecureRandom.uuid}&async=#{@_enable_polling_queries}",
169
- request_body.to_json
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.new({}),
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.new({}),
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 > @query_timeout
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
@@ -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) ? Integer(column) : @column_to_index[column]
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 to_h
53
- output = {}
60
+ def each
61
+ return to_enum __method__ unless block_given?
62
+
54
63
  @column_to_index.each_pair do |name, index|
55
- output[name.downcase] = self[index]
64
+ yield(name, self[index])
56
65
  end
57
- output
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
 
@@ -1,3 +1,3 @@
1
1
  module RubySnowflake
2
- VERSION = "1.3.0"
2
+ VERSION = "1.5.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: 1.3.0
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-01-03 00:00:00.000000000 Z
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.2.33
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: []