Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2a2b9d1
Add Snowflake SQL integration support
jankuca Oct 27, 2025
795d710
improve styling of auth method select box
jankuca Oct 27, 2025
d664998
Refactor Snowflake integration to use discriminated union types
jankuca Oct 27, 2025
c9a64e4
test: add tests for snowflake integrations
jankuca Oct 27, 2025
8960026
polish snowflake config placeholders
jankuca Oct 27, 2025
2567e9c
consolidate snowflake types
jankuca Oct 27, 2025
1811615
make snowflake url construction safer
jankuca Oct 27, 2025
9dcb6cf
avoid white-space only input in snowflake config
jankuca Oct 27, 2025
355959b
remove unnecesary snowflake null auth method check
jankuca Oct 27, 2025
1ae2707
add accessibility attrs to snowflake form
jankuca Oct 27, 2025
890665e
polish snowflake settings copy/placeholders
jankuca Oct 29, 2025
9390478
rename snowflake auth method tests to resemble reality
jankuca Oct 29, 2025
d7248c2
consolidate supported snowflake auth method checks
jankuca Oct 29, 2025
d011224
join password and null auth method type
jankuca Oct 29, 2025
3adb3ad
unify Name input label across integrations
jankuca Oct 29, 2025
b2ebadd
remove redundant placeholders from snowflake form
jankuca Oct 29, 2025
8084a2f
split private keys strings in tests to avoid security flagging
jankuca Oct 29, 2025
af3ef77
mark private keys in tests as obviously fake
jankuca Oct 29, 2025
b6aeb42
make SnowflakeAuthMethod imports/exports type-only
jankuca Oct 29, 2025
1ae43d1
unify password|null snowflake auth method typing
jankuca Oct 29, 2025
5d8451f
conslidate unnamed integration strings
jankuca Oct 29, 2025
ef91e96
VSCode->VS Code
jankuca Oct 29, 2025
0234368
Merge branch 'main' into jk/feat/snowflake
jankuca Oct 29, 2025
38a36e5
fix snowflake private key handling
jankuca Oct 29, 2025
8e78c67
fix opening of integration configuration from status bar
jankuca Oct 29, 2025
56e9e91
Validate integration type before DEEPNOTE_TO_INTEGRATION_TYPE lookup
jankuca Oct 29, 2025
31422bd
Replace local UnsupportedIntegrationError with shared error
jankuca Oct 29, 2025
05d82e3
polish snowflake url test
jankuca Oct 29, 2025
86529eb
replace buffer with btoa for snowflake private key encoding
jankuca Oct 29, 2025
122e067
lint
jankuca Oct 29, 2025
4072bfb
Merge branch 'main' into jk/feat/snowflake
jankuca Oct 29, 2025
7cb3073
remove copyright header
jankuca Oct 29, 2025
ed9537b
add unsupported_integration error category
jankuca Oct 29, 2025
ffdb5ac
polish unsupported integration type handling
jankuca Oct 29, 2025
5f4a284
localize errors in snowflake config
jankuca Oct 29, 2025
1504827
use pyformat instead of format for snowflake
jankuca Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type LocalizedMessages = {
// Integration type labels
integrationsPostgresTypeLabel: string;
integrationsBigQueryTypeLabel: string;
integrationsSnowflakeTypeLabel: string;
// PostgreSQL form strings
integrationsPostgresNameLabel: string;
integrationsPostgresNamePlaceholder: string;
Expand All @@ -204,6 +205,34 @@ export type LocalizedMessages = {
integrationsBigQueryCredentialsLabel: string;
integrationsBigQueryCredentialsPlaceholder: string;
integrationsBigQueryCredentialsRequired: string;
// Snowflake form strings
integrationsSnowflakeNameLabel: string;
integrationsSnowflakeNamePlaceholder: string;
integrationsSnowflakeAccountLabel: string;
integrationsSnowflakeAccountPlaceholder: string;
integrationsSnowflakeAuthMethodLabel: string;
integrationsSnowflakeAuthMethodSubLabel: string;
integrationsSnowflakeAuthMethodUsernamePassword: string;
integrationsSnowflakeAuthMethodKeyPair: string;
integrationsSnowflakeUsernameLabel: string;
integrationsSnowflakeUsernamePlaceholder: string;
integrationsSnowflakePasswordLabel: string;
integrationsSnowflakePasswordPlaceholder: string;
integrationsSnowflakeServiceAccountUsernameLabel: string;
integrationsSnowflakeServiceAccountUsernameHelp: string;
integrationsSnowflakeServiceAccountUsernamePlaceholder: string;
integrationsSnowflakePrivateKeyLabel: string;
integrationsSnowflakePrivateKeyHelp: string;
integrationsSnowflakePrivateKeyPlaceholder: string;
integrationsSnowflakePrivateKeyPassphraseLabel: string;
integrationsSnowflakePrivateKeyPassphraseHelp: string;
integrationsSnowflakePrivateKeyPassphrasePlaceholder: string;
integrationsSnowflakeDatabaseLabel: string;
integrationsSnowflakeDatabasePlaceholder: string;
integrationsSnowflakeRoleLabel: string;
integrationsSnowflakeRolePlaceholder: string;
integrationsSnowflakeWarehouseLabel: string;
integrationsSnowflakeWarehousePlaceholder: string;
// Common form strings
integrationsRequiredField: string;
integrationsOptionalField: string;
Expand Down
33 changes: 32 additions & 1 deletion src/notebooks/deepnote/integrations/integrationWebview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
integrationsConfigureTitle: localize.Integrations.configureTitle,
integrationsPostgresTypeLabel: localize.Integrations.postgresTypeLabel,
integrationsBigQueryTypeLabel: localize.Integrations.bigQueryTypeLabel,
integrationsSnowflakeTypeLabel: localize.Integrations.snowflakeTypeLabel,
integrationsCancel: localize.Integrations.cancel,
integrationsSave: localize.Integrations.save,
integrationsRequiredField: localize.Integrations.requiredField,
Expand All @@ -154,7 +155,37 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
integrationsBigQueryProjectIdPlaceholder: localize.Integrations.bigQueryProjectIdPlaceholder,
integrationsBigQueryCredentialsLabel: localize.Integrations.bigQueryCredentialsLabel,
integrationsBigQueryCredentialsPlaceholder: localize.Integrations.bigQueryCredentialsPlaceholder,
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired,
integrationsSnowflakeNameLabel: localize.Integrations.snowflakeNameLabel,
integrationsSnowflakeNamePlaceholder: localize.Integrations.snowflakeNamePlaceholder,
integrationsSnowflakeAccountLabel: localize.Integrations.snowflakeAccountLabel,
integrationsSnowflakeAccountPlaceholder: localize.Integrations.snowflakeAccountPlaceholder,
integrationsSnowflakeAuthMethodLabel: localize.Integrations.snowflakeAuthMethodLabel,
integrationsSnowflakeAuthMethodSubLabel: localize.Integrations.snowflakeAuthMethodSubLabel,
integrationsSnowflakeAuthMethodUsernamePassword: localize.Integrations.snowflakeAuthMethodUsernamePassword,
integrationsSnowflakeAuthMethodKeyPair: localize.Integrations.snowflakeAuthMethodKeyPair,
integrationsSnowflakeUsernameLabel: localize.Integrations.snowflakeUsernameLabel,
integrationsSnowflakeUsernamePlaceholder: localize.Integrations.snowflakeUsernamePlaceholder,
integrationsSnowflakePasswordLabel: localize.Integrations.snowflakePasswordLabel,
integrationsSnowflakePasswordPlaceholder: localize.Integrations.snowflakePasswordPlaceholder,
integrationsSnowflakeServiceAccountUsernameLabel:
localize.Integrations.snowflakeServiceAccountUsernameLabel,
integrationsSnowflakeServiceAccountUsernameHelp: localize.Integrations.snowflakeServiceAccountUsernameHelp,
integrationsSnowflakeServiceAccountUsernamePlaceholder:
localize.Integrations.snowflakeServiceAccountUsernamePlaceholder,
integrationsSnowflakePrivateKeyLabel: localize.Integrations.snowflakePrivateKeyLabel,
integrationsSnowflakePrivateKeyHelp: localize.Integrations.snowflakePrivateKeyHelp,
integrationsSnowflakePrivateKeyPlaceholder: localize.Integrations.snowflakePrivateKeyPlaceholder,
integrationsSnowflakePrivateKeyPassphraseLabel: localize.Integrations.snowflakePrivateKeyPassphraseLabel,
integrationsSnowflakePrivateKeyPassphraseHelp: localize.Integrations.snowflakePrivateKeyPassphraseHelp,
integrationsSnowflakePrivateKeyPassphrasePlaceholder:
localize.Integrations.snowflakePrivateKeyPassphrasePlaceholder,
integrationsSnowflakeDatabaseLabel: localize.Integrations.snowflakeDatabaseLabel,
integrationsSnowflakeDatabasePlaceholder: localize.Integrations.snowflakeDatabasePlaceholder,
integrationsSnowflakeRoleLabel: localize.Integrations.snowflakeRoleLabel,
integrationsSnowflakeRolePlaceholder: localize.Integrations.snowflakeRolePlaceholder,
integrationsSnowflakeWarehouseLabel: localize.Integrations.snowflakeWarehouseLabel,
integrationsSnowflakeWarehousePlaceholder: localize.Integrations.snowflakeWarehousePlaceholder
};

await this.currentPanel.webview.postMessage({
Expand Down
2 changes: 2 additions & 0 deletions src/notebooks/deepnote/sqlCellStatusBarProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
return l10n.t('PostgreSQL');
case IntegrationType.BigQuery:
return l10n.t('BigQuery');
case IntegrationType.Snowflake:
return l10n.t('Snowflake');
default:
return String(type);
}
Expand Down
37 changes: 37 additions & 0 deletions src/platform/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ export namespace Integrations {
// Integration type labels
export const postgresTypeLabel = l10n.t('PostgreSQL');
export const bigQueryTypeLabel = l10n.t('BigQuery');
export const snowflakeTypeLabel = l10n.t('Snowflake');

// PostgreSQL form strings
export const postgresNameLabel = l10n.t('Name (optional)');
Expand Down Expand Up @@ -861,6 +862,42 @@ export namespace Integrations {
export const bigQueryCredentialsRequired = l10n.t('Credentials are required');
export const bigQueryInvalidJson = (message: string) => l10n.t('Invalid JSON: {0}', message);
export const bigQueryUnnamedIntegration = (id: string) => l10n.t('Unnamed BigQuery Integration ({0})', id);

// Snowflake form strings
export const snowflakeNameLabel = l10n.t('Integration name');
export const snowflakeNamePlaceholder = l10n.t('[Demo] Snowflake');
export const snowflakeAccountLabel = l10n.t('Account name');
export const snowflakeAccountPlaceholder = l10n.t('ptb34938.us-east-1');
export const snowflakeAuthMethodLabel = l10n.t('Authentication');
export const snowflakeAuthMethodSubLabel = l10n.t('Method');
export const snowflakeAuthMethodUsernamePassword = l10n.t('Username & password');
export const snowflakeAuthMethodKeyPair = l10n.t('Key-pair (service account)');
export const snowflakeUsernameLabel = l10n.t('Username');
export const snowflakeUsernamePlaceholder = l10n.t('WEBSITE_ANALYTICS_USER');
export const snowflakePasswordLabel = l10n.t('Password');
export const snowflakePasswordPlaceholder = l10n.t('••••••••••••••');
export const snowflakeServiceAccountUsernameLabel = l10n.t('Service Account Username');
export const snowflakeServiceAccountUsernameHelp = l10n.t(
'The username of the service account that will be used to connect to Snowflake'
);
export const snowflakeServiceAccountUsernamePlaceholder = l10n.t('WEBSITE_ANALYTICS_USER');
export const snowflakePrivateKeyLabel = l10n.t('Private Key');
export const snowflakePrivateKeyHelp = l10n.t(
'The private key in PEM format. Make sure to include the entire key, including BEGIN and END markers.'
);
export const snowflakePrivateKeyPlaceholder = l10n.t("Begins with '-----BEGIN PRIVATE KEY-----'");
export const snowflakePrivateKeyPassphraseLabel = l10n.t('Private Key Passphrase (optional)');
export const snowflakePrivateKeyPassphraseHelp = l10n.t(
'If the private key is encrypted, provide the passphrase to decrypt it'
);
export const snowflakePrivateKeyPassphrasePlaceholder = l10n.t('Private key passphrase (optional)');
export const snowflakeDatabaseLabel = l10n.t('Database (optional)');
export const snowflakeDatabasePlaceholder = l10n.t('DEEPNOTE');
export const snowflakeRoleLabel = l10n.t('Role (optional)');
export const snowflakeRolePlaceholder = l10n.t('');
export const snowflakeWarehouseLabel = l10n.t('Warehouse (optional)');
export const snowflakeWarehousePlaceholder = l10n.t('');
export const snowflakeUnnamedIntegration = (id: string) => l10n.t('Unnamed Snowflake Integration ({0})', id);
}

export namespace Deprecated {
Expand Down
38 changes: 34 additions & 4 deletions src/platform/notebooks/deepnote/integrationTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ export const DATAFRAME_SQL_INTEGRATION_ID = 'deepnote-dataframe-sql';
*/
export enum IntegrationType {
Postgres = 'postgres',
BigQuery = 'bigquery'
BigQuery = 'bigquery',
Snowflake = 'snowflake'
}

/**
* Map our IntegrationType enum to Deepnote integration type strings
*/
export const INTEGRATION_TYPE_TO_DEEPNOTE = {
[IntegrationType.Postgres]: 'pgsql',
[IntegrationType.BigQuery]: 'big-query'
[IntegrationType.BigQuery]: 'big-query',
[IntegrationType.Snowflake]: 'snowflake'
} as const satisfies { [type in IntegrationType]: string };

export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typeof INTEGRATION_TYPE_TO_DEEPNOTE];
Expand All @@ -27,7 +29,8 @@ export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typ
*/
export const DEEPNOTE_TO_INTEGRATION_TYPE: Record<RawIntegrationType, IntegrationType> = {
pgsql: IntegrationType.Postgres,
'big-query': IntegrationType.BigQuery
'big-query': IntegrationType.BigQuery,
snowflake: IntegrationType.Snowflake
};

/**
Expand Down Expand Up @@ -61,10 +64,37 @@ export interface BigQueryIntegrationConfig extends BaseIntegrationConfig {
credentials: string; // JSON string of service account credentials
}

/**
* Snowflake authentication method
*/
export enum SnowflakeAuthMethod {
UsernamePassword = 'username_password',
KeyPair = 'key_pair'
}

/**
* Snowflake integration configuration
*/
export interface SnowflakeIntegrationConfig extends BaseIntegrationConfig {
type: IntegrationType.Snowflake;
account: string;
authMethod: SnowflakeAuthMethod;
username: string;
// For username+password auth
password?: string;
// For key-pair auth
privateKey?: string;
privateKeyPassphrase?: string;
// Optional fields
database?: string;
warehouse?: string;
role?: string;
}

/**
* Union type of all integration configurations
*/
export type IntegrationConfig = PostgresIntegrationConfig | BigQueryIntegrationConfig;
export type IntegrationConfig = PostgresIntegrationConfig | BigQueryIntegrationConfig | SnowflakeIntegrationConfig;

/**
* Integration connection status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { EnvironmentVariables } from '../../common/variables/types';
import { BaseError } from '../../errors/types';
import { logger } from '../../logging';
import { IIntegrationStorage, ISqlIntegrationEnvVarsProvider } from './types';
import { DATAFRAME_SQL_INTEGRATION_ID, IntegrationConfig, IntegrationType } from './integrationTypes';
import {
DATAFRAME_SQL_INTEGRATION_ID,
IntegrationConfig,
IntegrationType,
SnowflakeAuthMethod
} from './integrationTypes';

/**
* Error thrown when an unsupported integration type is encountered.
Expand Down Expand Up @@ -73,6 +78,68 @@ function convertIntegrationConfigToJson(config: IntegrationConfig): string {
});
}

case IntegrationType.Snowflake: {
// Build Snowflake connection URL
// Format depends on auth method:
// Username+password: snowflake://{username}:{password}@{account}/{database}?warehouse={warehouse}&role={role}&application=YourApp
// Key-pair: snowflake://{username}@{account}/{database}?warehouse={warehouse}&role={role}&authenticator=snowflake_jwt&application=YourApp
const encodedUsername = encodeURIComponent(config.username);
const encodedAccount = encodeURIComponent(config.account);

let url: string;
const params: Record<string, unknown> = {};

if (config.authMethod === SnowflakeAuthMethod.UsernamePassword) {
// Username+password authentication
const encodedPassword = encodeURIComponent(config.password || '');
const database = config.database ? `/${encodeURIComponent(config.database)}` : '';
url = `snowflake://${encodedUsername}:${encodedPassword}@${encodedAccount}${database}`;

const queryParams: string[] = [];
if (config.warehouse) {
queryParams.push(`warehouse=${encodeURIComponent(config.warehouse)}`);
}
if (config.role) {
queryParams.push(`role=${encodeURIComponent(config.role)}`);
}
queryParams.push('application=Deepnote');

if (queryParams.length > 0) {
url += `?${queryParams.join('&')}`;
}
} else {
// Key-pair authentication
const database = config.database ? `/${encodeURIComponent(config.database)}` : '';
url = `snowflake://${encodedUsername}@${encodedAccount}${database}`;

const queryParams: string[] = [];
if (config.warehouse) {
queryParams.push(`warehouse=${encodeURIComponent(config.warehouse)}`);
}
if (config.role) {
queryParams.push(`role=${encodeURIComponent(config.role)}`);
}
queryParams.push('authenticator=snowflake_jwt');
queryParams.push('application=Deepnote');

if (queryParams.length > 0) {
url += `?${queryParams.join('&')}`;
}

// For key-pair auth, pass the private key and passphrase as params
params.private_key = config.privateKey || '';
if (config.privateKeyPassphrase) {
params.private_key_passphrase = config.privateKeyPassphrase;
}
}

return JSON.stringify({
url: url,
params: params,
param_style: 'format'
});
}

default:
throw new UnsupportedIntegrationError((config as IntegrationConfig).type);
}
Expand Down
16 changes: 14 additions & 2 deletions src/webviews/webview-side/integrations/ConfigurationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { getLocString } from '../react-common/locReactSide';
import { PostgresForm } from './PostgresForm';
import { BigQueryForm } from './BigQueryForm';
import { SnowflakeForm } from './SnowflakeForm';
import { IntegrationConfig, IntegrationType } from './types';

export interface IConfigurationFormProps {
Expand All @@ -22,7 +23,7 @@ export const ConfigurationForm: React.FC<IConfigurationFormProps> = ({
onCancel
}) => {
// Determine integration type from existing config, integration metadata from project, or ID
const getIntegrationType = (): 'postgres' | 'bigquery' => {
const getIntegrationType = (): 'postgres' | 'bigquery' | 'snowflake' => {
if (existingConfig) {
return existingConfig.type;
}
Expand All @@ -37,6 +38,9 @@ export const ConfigurationForm: React.FC<IConfigurationFormProps> = ({
if (integrationId.includes('bigquery')) {
return 'bigquery';
}
if (integrationId.includes('snowflake')) {
return 'snowflake';
}
// Default to postgres
return 'postgres';
};
Expand Down Expand Up @@ -67,14 +71,22 @@ export const ConfigurationForm: React.FC<IConfigurationFormProps> = ({
onSave={onSave}
onCancel={onCancel}
/>
) : (
) : selectedIntegrationType === 'bigquery' ? (
<BigQueryForm
integrationId={integrationId}
existingConfig={existingConfig?.type === 'bigquery' ? existingConfig : null}
integrationName={integrationName}
onSave={onSave}
onCancel={onCancel}
/>
) : (
<SnowflakeForm
integrationId={integrationId}
existingConfig={existingConfig?.type === 'snowflake' ? existingConfig : null}
integrationName={integrationName}
onSave={onSave}
onCancel={onCancel}
/>
)}
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/webviews/webview-side/integrations/IntegrationItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const getIntegrationTypeLabel = (type: IntegrationType): string => {
return getLocString('integrationsPostgresTypeLabel', 'PostgreSQL');
case 'bigquery':
return getLocString('integrationsBigQueryTypeLabel', 'BigQuery');
case 'snowflake':
return getLocString('integrationsSnowflakeTypeLabel', 'Snowflake');
default:
return type;
}
Expand Down
Loading
Loading