-
Notifications
You must be signed in to change notification settings - Fork 79
feat: Snowflake adapter using show statements #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: Snowflake adapter using show statements #186
Conversation
i couldn't figure out a way to do If anybody knows how i could do that, happy to add it. |
Hi @derekbunch sorry for slow response, feel free to ping me for faster help :) Is this ready for review? We are currently focusing on adding somewhat integration tests for existing adapters. Ideally, any new adapter should also try to follow this structure. We are using testcontainers as the tool for testing each adapter. From quickly checking, it seem there is a localstack image https://hub.docker.com/r/localstack/snowflake (edit: and testcontainer: https://golang.testcontainers.org/modules/localstack/) that you can use to emulate the snowflake warehouse locally. Could you try and take a stab adding this into this PR as well? It would be very much appreciated as it simplifies testing and debugging quite a lot. Let me know if you need any help with this! |
Hi @MattiasMTS, yes it should be ready for review. And no problem. ill look into the links you shared and try to get those tests implemented for this MR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work! Thanks a lot for the contribution!! ✨
I left some comments - first that should be addressed is the URL
part of being able to exclude the snowflake://
protocol in the URL as we are already declaring which driver via the type
attribute 😋
dbee/adapters/snowflake_driver.go
Outdated
// gosnowflake does not support the snowflake:// scheme | ||
// , it expects the full connection string excluding the scheme. | ||
// e.g. "snowflake://user:password@account/db" -> "user:password@account/db" | ||
type SnowflakeURL struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edit: actually, why do we need to care about the snowflake://
protocol prefix here? We declare that a user want to use this adapter via the type
input, see e.g.:
{
"url": "user:password@account/db",
"id": "snowflake-id-string",
"name": "snowflake-name",
"type": "snowflake" <- here
}
with this in mind, add as docstring to the Connect
method that the input URL is expected to be follow the structure as gosnowflake
wants it 😄
Or did I miss something here?
this doesn't have to be exposed as usage from other packages (i.e. capital initial letter) -> please use snowflakeURL
feel free to move this to the snowflake.go
file as well given this is focused to support the Connect
method.
Finally, you can add use *url.URL
inheritance here and ergo don't have to de-reference it in Connect
method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize that I should try and focus on adding some hosted document related to this so it is clear to the users how each adapter works and what underlying driver it is using. Another thing on the TODO list! ✏️
`, opts.Schema, opts.Table) | ||
} | ||
|
||
func getSnowflakeStructure(rows core.ResultStream) ([]*core.Structure, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, a bit shame we need to re-do a bit logic here due to the ordering of table
, tableType
and schema
is a bit different. I'll add this to refactoring later on.
dbee/adapters/snowflake_driver.go
Outdated
|
||
table, tableType, schema := row[1].(string), row[2].(string), row[4].(string) | ||
|
||
if strings.ToLower(schema) == "information_schema" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just do
if strings.ToLower(schema) == "information_schema" { | |
if schema == "INFORMATION_SCHEMA" { |
or do we expect this to be case insensitive?
dbee/adapters/snowflake_driver.go
Outdated
var structure []*core.Structure | ||
|
||
for k, v := range children { | ||
structure = append(structure, &core.Structure{ | ||
Name: k, | ||
Schema: k, | ||
Type: core.StructureTypeNone, | ||
Children: v, | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var structure []*core.Structure | |
for k, v := range children { | |
structure = append(structure, &core.Structure{ | |
Name: k, | |
Schema: k, | |
Type: core.StructureTypeNone, | |
Children: v, | |
}) | |
} | |
structure := make([]*Structure, 0, len(children)) | |
for schema, models := range children { | |
structure = append(structure, &Structure{ | |
Name: schema, | |
Schema: schema, | |
Type: StructureTypeSchema, | |
Children: models, | |
}) | |
} |
primarily the instantiation of structure
to allocate same amount as we have discovered firstly
dbee/adapters/snowflake_driver.go
Outdated
query := ` | ||
show terse objects; | ||
` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reduce some line of code 😋
query := ` | |
show terse objects; | |
` | |
query := "show terse objects;" |
dbee/adapters/snowflake_test.go
Outdated
wantErr bool | ||
}{ | ||
{ | ||
name: "Valid URL", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edit: ignore if we decide to not use prefix in URL
feel free to take a look at the example test I gave to the String
part - I believe that is the only unit test we need here given this Connect
will be tested in integration test. So, let's just add a unit test for the String
logic - or wdyt?
}, nil | ||
} | ||
|
||
func (r *Snowflake) GetHelpers(opts *core.TableOptions) map[string]string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
take another look at this function please - not sure why the switch
statement is needed here 😄
dbee/adapters/snowflake_driver.go
Outdated
url.URL | ||
} | ||
|
||
func (c *SnowflakeURL) String() string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edit: ignore this, see comment about the URL prefix
actually, can't this be simplified by doing:
func (s *snowflakeURL) String() string {
dsn := s.URL.String()
return strings.TrimPrefix(dsn, "snowflake://")
}
and also add some unit test for this wrapping logic:
func TestSnowflakeURL_String(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "should return dsn without snowflake prefix",
input: "snowflake://user:pass@myaccount/mydb",
want: "user:pass@myaccount/mydb",
},
{
name: "should return input string if snowflake prefix not present",
input: "user:pass@myaccount/mydb?warehouse=compute",
want: "user:pass@myaccount/mydb?warehouse=compute",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsed, err := url.Parse(tt.input)
require.NoError(t, err)
surl := snowflakeURL{parsed}
got := surl.String()
require.Equal(t, tt.want, got)
})
}
}
dbee/adapters/snowflake_test.go
Outdated
r.Error(err) | ||
return | ||
} | ||
r.NoError(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the want
here is never checked. Missing require.Equal(t, tt.want, got)
, no need to use a require.New
here :)
edit: feel free to ignore this if you're going to just add the String
method :)
dbee/adapters/snowflake_test.go
Outdated
if (err != nil) != tt.wantErr { | ||
t.Errorf("NewSnowflake() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can be skipped, or what are you trying to check here?
edit: feel free to ignore this if you're going to just add the String
method :)
@MattiasMTS I've looked into this a bit and tried to get it set up. Looks like it needs an auth token to run the container so I created an account, generated an auth token, and tried to run the container with it and it gives me this error
Any idea how i should proceed? Do y'all use a ci token for your ci where this would run or is this a non-starter? |
ahh, right. I guess that version of localstack requires a subscription.. That is a bit unfortunate. Feel free to skip this part/scope of this PR then and I'll try and find if we can get a sub based on OSS stuff. Thanks a lot for trying it out and feedback the info! 😄 |
dbee/adapters/snowflake.go
Outdated
err = db.Ping() | ||
if err != nil { | ||
return nil, err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's ping with a timeout here, e.g
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("unable to ping snowflake: %w", err)
}
if you want to ping it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😮 super cool, thanks! didnt know this is how you did that!
dbee/adapters/snowflake_driver.go
Outdated
config.Database = name | ||
connector := gosnowflake.NewConnector(gosnowflake.SnowflakeDriver{}, config) | ||
db := sql.OpenDB(connector) | ||
err := db.Ping() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment here on ping as in the connector 😋
dbee/adapters/snowflake_test.go
Outdated
"github.com/kndndrj/nvim-dbee/dbee/core/builders" | ||
) | ||
|
||
func TestNewSnowflake(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can remove this test since I am not sure we need it for testing anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes, was just waiting to delete this until i confirmed how to handle testing!
dbee/adapters/snowflake_driver.go
Outdated
// snowflakeDriver is a sql client for snowflakeDriver. | ||
type snowflakeDriver struct { | ||
c *builders.Client | ||
config *gosnowflake.Config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feel free to make this a non-pointer if you are only using it in SelectDatabase
with dereference, or wdyt?
// or | ||
// host:port/database/schema?account=user_account[?param1=value1¶mN=valueN] | ||
// https://github.com/snowflakedb/gosnowflake/blob/b034584aa6fc171c1fa02e5af1f98234f24538fe/dsn.go#L308-#L314 | ||
func (r *Snowflake) Connect(rawURL string) (core.Driver, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great! We can iteratore more on what kind of connection inputs a user can have here later on 😄 This looks a lot cleaner now IMO! ✨
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea, im also planning to update this (if needed) to support oauth once snowflake merges this branch since that or 2FA is snowflake's preferred method of auth. Password based auth will be disabled soon
before i knew about this branch, i tried implementing oauth myself, but seemed like there were a number of issues with their TokenAccessor
implementation, so i just opted to wait for them to work out the kinks in their branch, then update this to use it once its ready
- Implement comprehensive Snowflake database adapter - Support for password, keypair (JWT), and MFA/SSO authentication methods - Private key loading with PKCS1/PKCS8 format support and encryption - Cost-optimized queries using SHOW statements to avoid warehouse wake-up - Database operations: structure browsing, column metadata, database switching - Complete unit test suite with authentication method validation - Documentation with setup guide and troubleshooting - Environment variable template support for secure credential management Addresses: kndndrj#23, builds on kndndrj#109 and kndndrj#186
rebased #109 on master and updated it to use show commands to avoid waking warehouses to prevent extra cost
im not a go dev, but tried my best to follow the patterns i saw as much as possible