Skip to content

feat: Snowflake adapter using show statements - #186

Open
derekbunch wants to merge 11 commits into
kndndrj:masterfrom
derekbunch:snowflake-adapter-using-show
Open

feat: Snowflake adapter using show statements#186
derekbunch wants to merge 11 commits into
kndndrj:masterfrom
derekbunch:snowflake-adapter-using-show

Conversation

@derekbunch

@derekbunch derekbunch commented Feb 19, 2025

Copy link
Copy Markdown

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

image

@derekbunch

Copy link
Copy Markdown
Author

i couldn't figure out a way to do show databases and get the current without 2 queries and that seemed to cause issues, so I left that one in there as I assume it happens much less frequently than structure and columns.

If anybody knows how i could do that, happy to add it.

@derekbunch derekbunch changed the title Snowflake adapter using show statements feat: Snowflake adapter using show statements Feb 20, 2025
@MattiasMTS

MattiasMTS commented Mar 9, 2025

Copy link
Copy Markdown
Collaborator

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!

@derekbunch

Copy link
Copy Markdown
Author

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.

@MattiasMTS MattiasMTS left a comment

Copy link
Copy Markdown
Collaborator

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 😋

Comment thread 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 {

Copy link
Copy Markdown
Collaborator

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

@MattiasMTS MattiasMTS Mar 11, 2025

Copy link
Copy Markdown
Collaborator

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) {

Copy link
Copy Markdown
Collaborator

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.

Comment thread dbee/adapters/snowflake_driver.go Outdated

table, tableType, schema := row[1].(string), row[2].(string), row[4].(string)

if strings.ToLower(schema) == "information_schema" {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just do

Suggested change
if strings.ToLower(schema) == "information_schema" {
if schema == "INFORMATION_SCHEMA" {

or do we expect this to be case insensitive?

Comment thread dbee/adapters/snowflake_driver.go Outdated
Comment on lines +93 to +102
var structure []*core.Structure

for k, v := range children {
structure = append(structure, &core.Structure{
Name: k,
Schema: k,
Type: core.StructureTypeNone,
Children: v,
})
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Comment thread dbee/adapters/snowflake_driver.go Outdated
Comment on lines +111 to +113
query := `
show terse objects;
`

Copy link
Copy Markdown
Collaborator

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 😋

Suggested change
query := `
show terse objects;
`
query := "show terse objects;"

Comment thread dbee/adapters/snowflake_test.go Outdated
wantErr bool
}{
{
name: "Valid URL",

Copy link
Copy Markdown
Collaborator

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 {

Copy link
Copy Markdown
Collaborator

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 😄

Comment thread dbee/adapters/snowflake_driver.go Outdated
url.URL
}

func (c *SnowflakeURL) String() string {

Copy link
Copy Markdown
Collaborator

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)
		})
	}
}

Comment thread dbee/adapters/snowflake_test.go Outdated
r.Error(err)
return
}
r.NoError(err)

Copy link
Copy Markdown
Collaborator

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 :)

Comment thread dbee/adapters/snowflake_test.go Outdated
Comment on lines +36 to +39
if (err != nil) != tt.wantErr {
t.Errorf("NewSnowflake() error = %v, wantErr %v", err, tt.wantErr)
return
}

Copy link
Copy Markdown
Collaborator

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 :)

@derekbunch

Copy link
Copy Markdown
Author

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!

@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

❯docker run \
    --rm -it \
    -p 127.0.0.1:4566:4566 \
    -p 127.0.0.1:4510-4559:4510-4559 \
    -p 127.0.0.1:443:443 \
    -e LOCALSTACK_AUTH_TOKEN="<redacted>" \
    localstack/snowflake
                                                                                                                                                        
LocalStack version: 4.2.1.dev76
LocalStack build date: 2025-03-20
LocalStack build git hash: 6b2781950

Docker not available
2025-03-22T05:51:45.089  INFO --- [  MainThread] l.p.c.b.licensingv2        : Successfully requested and activated new license <redacted>:hobby 🔑✅
2025-03-22T05:51:47.017  INFO --- [  MainThread] l.p.c.extensions.platform  : loaded 1 extensions
⚠ The Snowflake emulator is currently not covered by your license. ❄ Please reach out to LocalStack Support to get access to this product. ⚠

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?

@MattiasMTS

Copy link
Copy Markdown
Collaborator

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! 😄

Comment thread dbee/adapters/snowflake.go Outdated
Comment on lines +36 to +39
err = db.Ping()
if err != nil {
return nil, err
}

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Author

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!

Comment thread dbee/adapters/snowflake_driver.go Outdated
config.Database = name
connector := gosnowflake.NewConnector(gosnowflake.SnowflakeDriver{}, config)
db := sql.OpenDB(connector)
err := db.Ping()

Copy link
Copy Markdown
Collaborator

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 😋

Comment thread dbee/adapters/snowflake_test.go Outdated
"github.com/kndndrj/nvim-dbee/dbee/core/builders"
)

func TestNewSnowflake(t *testing.T) {

Copy link
Copy Markdown
Collaborator

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?

Copy link
Copy Markdown
Author

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!

Comment thread dbee/adapters/snowflake_driver.go Outdated
// snowflakeDriver is a sql client for snowflakeDriver.
type snowflakeDriver struct {
c *builders.Client
config *gosnowflake.Config

Copy link
Copy Markdown
Collaborator

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&paramN=valueN]
// https://github.com/snowflakedb/gosnowflake/blob/b034584aa6fc171c1fa02e5af1f98234f24538fe/dsn.go#L308-#L314
func (r *Snowflake) Connect(rawURL string) (core.Driver, error) {

Copy link
Copy Markdown
Collaborator

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! ✨

@derekbunch derekbunch Mar 29, 2025

Copy link
Copy Markdown
Author

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

vanducng added a commit to vanducng/nvim-dbee that referenced this pull request Jun 8, 2025
- 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
Comment thread dbee/adapters/snowflake_driver.go Outdated
if err != nil {
return "", nil, err
}
databaseName := row[2].(string)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
databaseName := row[2].(string)
databaseName := row[1].(string)

Database name is in the second column.

@derekbunch
derekbunch force-pushed the snowflake-adapter-using-show branch from 0bc71c2 to 5b29f56 Compare July 9, 2026 20:09
@derekbunch

derekbunch commented Jul 10, 2026

Copy link
Copy Markdown
Author

hey @MattiasMTS sorry for the dalay on this. i rebased this and updated it to work with the upstream snowflake oauth (since snowflake is deprecating password only access) and validated that everything works again.

would love another look! 🙏🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants