Skip to content

Commit a9a786b

Browse files
authored
feat: Add Snowflake SQL integration support (#121)
1 parent e683e42 commit a9a786b

18 files changed

+1100
-41
lines changed

src/messageTypes.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export type LocalizedMessages = {
182182
// Integration type labels
183183
integrationsPostgresTypeLabel: string;
184184
integrationsBigQueryTypeLabel: string;
185+
integrationsSnowflakeTypeLabel: string;
185186
// PostgreSQL form strings
186187
integrationsPostgresNameLabel: string;
187188
integrationsPostgresNamePlaceholder: string;
@@ -204,9 +205,36 @@ export type LocalizedMessages = {
204205
integrationsBigQueryCredentialsLabel: string;
205206
integrationsBigQueryCredentialsPlaceholder: string;
206207
integrationsBigQueryCredentialsRequired: string;
208+
// Snowflake form strings
209+
integrationsSnowflakeNameLabel: string;
210+
integrationsSnowflakeNamePlaceholder: string;
211+
integrationsSnowflakeAccountLabel: string;
212+
integrationsSnowflakeAccountPlaceholder: string;
213+
integrationsSnowflakeAuthMethodLabel: string;
214+
integrationsSnowflakeAuthMethodSubLabel: string;
215+
integrationsSnowflakeAuthMethodUsernamePassword: string;
216+
integrationsSnowflakeAuthMethodKeyPair: string;
217+
integrationsSnowflakeUnsupportedAuthMethod: string;
218+
integrationsSnowflakeUsernameLabel: string;
219+
integrationsSnowflakePasswordLabel: string;
220+
integrationsSnowflakePasswordPlaceholder: string;
221+
integrationsSnowflakeServiceAccountUsernameLabel: string;
222+
integrationsSnowflakeServiceAccountUsernameHelp: string;
223+
integrationsSnowflakePrivateKeyLabel: string;
224+
integrationsSnowflakePrivateKeyHelp: string;
225+
integrationsSnowflakePrivateKeyPlaceholder: string;
226+
integrationsSnowflakePrivateKeyPassphraseLabel: string;
227+
integrationsSnowflakePrivateKeyPassphraseHelp: string;
228+
integrationsSnowflakeDatabaseLabel: string;
229+
integrationsSnowflakeDatabasePlaceholder: string;
230+
integrationsSnowflakeRoleLabel: string;
231+
integrationsSnowflakeRolePlaceholder: string;
232+
integrationsSnowflakeWarehouseLabel: string;
233+
integrationsSnowflakeWarehousePlaceholder: string;
207234
// Common form strings
208235
integrationsRequiredField: string;
209236
integrationsOptionalField: string;
237+
integrationsUnnamedIntegration: string;
210238
};
211239
// Map all messages to specific payloads
212240
export class IInteractiveWindowMapping {

src/notebooks/deepnote/integrations/integrationManager.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import { IExtensionContext } from '../../../platform/common/types';
55
import { Commands } from '../../../platform/common/constants';
66
import { logger } from '../../../platform/logging';
77
import { IIntegrationDetector, IIntegrationManager, IIntegrationStorage, IIntegrationWebviewProvider } from './types';
8-
import { IntegrationStatus, IntegrationWithStatus } from '../../../platform/notebooks/deepnote/integrationTypes';
8+
import {
9+
DEEPNOTE_TO_INTEGRATION_TYPE,
10+
IntegrationStatus,
11+
IntegrationType,
12+
IntegrationWithStatus,
13+
RawIntegrationType
14+
} from '../../../platform/notebooks/deepnote/integrationTypes';
915
import { BlockWithIntegration, scanBlocksForIntegrations } from './integrationUtils';
16+
import { IDeepnoteNotebookManager } from '../../types';
1017

1118
/**
1219
* Manages integration UI and commands for Deepnote notebooks
@@ -21,7 +28,8 @@ export class IntegrationManager implements IIntegrationManager {
2128
@inject(IExtensionContext) private readonly extensionContext: IExtensionContext,
2229
@inject(IIntegrationDetector) private readonly integrationDetector: IIntegrationDetector,
2330
@inject(IIntegrationStorage) private readonly integrationStorage: IIntegrationStorage,
24-
@inject(IIntegrationWebviewProvider) private readonly webviewProvider: IIntegrationWebviewProvider
31+
@inject(IIntegrationWebviewProvider) private readonly webviewProvider: IIntegrationWebviewProvider,
32+
@inject(IDeepnoteNotebookManager) private readonly notebookManager: IDeepnoteNotebookManager
2533
) {}
2634

2735
public activate(): void {
@@ -150,9 +158,33 @@ export class IntegrationManager implements IIntegrationManager {
150158
if (selectedIntegrationId && !integrations.has(selectedIntegrationId)) {
151159
logger.debug(`IntegrationManager: Adding requested integration ${selectedIntegrationId} to the map`);
152160
const config = await this.integrationStorage.getIntegrationConfig(selectedIntegrationId);
161+
162+
// Try to get integration metadata from the project
163+
const project = this.notebookManager.getOriginalProject(projectId);
164+
const projectIntegration = project?.project.integrations?.find((i) => i.id === selectedIntegrationId);
165+
166+
let integrationName: string | undefined;
167+
let integrationType: IntegrationType | undefined;
168+
169+
if (projectIntegration) {
170+
integrationName = projectIntegration.name;
171+
172+
// Validate that projectIntegration.type exists in the mapping before lookup
173+
if (projectIntegration.type in DEEPNOTE_TO_INTEGRATION_TYPE) {
174+
// Map the Deepnote integration type to our IntegrationType
175+
integrationType = DEEPNOTE_TO_INTEGRATION_TYPE[projectIntegration.type as RawIntegrationType];
176+
} else {
177+
logger.warn(
178+
`IntegrationManager: Unknown integration type '${projectIntegration.type}' for integration ID '${selectedIntegrationId}' in project '${projectId}'. Integration type will be undefined.`
179+
);
180+
}
181+
}
182+
153183
integrations.set(selectedIntegrationId, {
154184
config: config || null,
155-
status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected
185+
status: config ? IntegrationStatus.Connected : IntegrationStatus.Disconnected,
186+
integrationName,
187+
integrationType
156188
});
157189
}
158190

src/notebooks/deepnote/integrations/integrationWebview.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
131131
integrationsConfigureTitle: localize.Integrations.configureTitle,
132132
integrationsPostgresTypeLabel: localize.Integrations.postgresTypeLabel,
133133
integrationsBigQueryTypeLabel: localize.Integrations.bigQueryTypeLabel,
134+
integrationsSnowflakeTypeLabel: localize.Integrations.snowflakeTypeLabel,
134135
integrationsCancel: localize.Integrations.cancel,
135136
integrationsSave: localize.Integrations.save,
136137
integrationsRequiredField: localize.Integrations.requiredField,
@@ -154,7 +155,34 @@ export class IntegrationWebviewProvider implements IIntegrationWebviewProvider {
154155
integrationsBigQueryProjectIdPlaceholder: localize.Integrations.bigQueryProjectIdPlaceholder,
155156
integrationsBigQueryCredentialsLabel: localize.Integrations.bigQueryCredentialsLabel,
156157
integrationsBigQueryCredentialsPlaceholder: localize.Integrations.bigQueryCredentialsPlaceholder,
157-
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired
158+
integrationsBigQueryCredentialsRequired: localize.Integrations.bigQueryCredentialsRequired,
159+
integrationsSnowflakeNameLabel: localize.Integrations.snowflakeNameLabel,
160+
integrationsSnowflakeNamePlaceholder: localize.Integrations.snowflakeNamePlaceholder,
161+
integrationsSnowflakeAccountLabel: localize.Integrations.snowflakeAccountLabel,
162+
integrationsSnowflakeAccountPlaceholder: localize.Integrations.snowflakeAccountPlaceholder,
163+
integrationsSnowflakeAuthMethodLabel: localize.Integrations.snowflakeAuthMethodLabel,
164+
integrationsSnowflakeAuthMethodSubLabel: localize.Integrations.snowflakeAuthMethodSubLabel,
165+
integrationsSnowflakeAuthMethodUsernamePassword: localize.Integrations.snowflakeAuthMethodUsernamePassword,
166+
integrationsSnowflakeAuthMethodKeyPair: localize.Integrations.snowflakeAuthMethodKeyPair,
167+
integrationsSnowflakeUnsupportedAuthMethod: localize.Integrations.snowflakeUnsupportedAuthMethod,
168+
integrationsSnowflakeUsernameLabel: localize.Integrations.snowflakeUsernameLabel,
169+
integrationsSnowflakePasswordLabel: localize.Integrations.snowflakePasswordLabel,
170+
integrationsSnowflakePasswordPlaceholder: localize.Integrations.snowflakePasswordPlaceholder,
171+
integrationsSnowflakeServiceAccountUsernameLabel:
172+
localize.Integrations.snowflakeServiceAccountUsernameLabel,
173+
integrationsSnowflakeServiceAccountUsernameHelp: localize.Integrations.snowflakeServiceAccountUsernameHelp,
174+
integrationsSnowflakePrivateKeyLabel: localize.Integrations.snowflakePrivateKeyLabel,
175+
integrationsSnowflakePrivateKeyHelp: localize.Integrations.snowflakePrivateKeyHelp,
176+
integrationsSnowflakePrivateKeyPlaceholder: localize.Integrations.snowflakePrivateKeyPlaceholder,
177+
integrationsSnowflakePrivateKeyPassphraseLabel: localize.Integrations.snowflakePrivateKeyPassphraseLabel,
178+
integrationsSnowflakePrivateKeyPassphraseHelp: localize.Integrations.snowflakePrivateKeyPassphraseHelp,
179+
integrationsSnowflakeDatabaseLabel: localize.Integrations.snowflakeDatabaseLabel,
180+
integrationsSnowflakeDatabasePlaceholder: localize.Integrations.snowflakeDatabasePlaceholder,
181+
integrationsSnowflakeRoleLabel: localize.Integrations.snowflakeRoleLabel,
182+
integrationsSnowflakeRolePlaceholder: localize.Integrations.snowflakeRolePlaceholder,
183+
integrationsSnowflakeWarehouseLabel: localize.Integrations.snowflakeWarehouseLabel,
184+
integrationsSnowflakeWarehousePlaceholder: localize.Integrations.snowflakeWarehousePlaceholder,
185+
integrationsUnnamedIntegration: localize.Integrations.unnamedIntegration('{0}')
158186
};
159187

160188
await this.currentPanel.webview.postMessage({

src/notebooks/deepnote/sqlCellStatusBarProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ export class SqlCellStatusBarProvider implements NotebookCellStatusBarItemProvid
437437
return l10n.t('PostgreSQL');
438438
case IntegrationType.BigQuery:
439439
return l10n.t('BigQuery');
440+
case IntegrationType.Snowflake:
441+
return l10n.t('Snowflake');
440442
default:
441443
return String(type);
442444
}

src/platform/common/utils/localize.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,10 +830,12 @@ export namespace Integrations {
830830
export const save = l10n.t('Save');
831831
export const requiredField = l10n.t('*');
832832
export const optionalField = l10n.t('(optional)');
833+
export const unnamedIntegration = (id: string) => l10n.t('Unnamed Integration ({0})', id);
833834

834835
// Integration type labels
835836
export const postgresTypeLabel = l10n.t('PostgreSQL');
836837
export const bigQueryTypeLabel = l10n.t('BigQuery');
838+
export const snowflakeTypeLabel = l10n.t('Snowflake');
837839

838840
// PostgreSQL form strings
839841
export const postgresNameLabel = l10n.t('Name (optional)');
@@ -849,7 +851,6 @@ export namespace Integrations {
849851
export const postgresPasswordLabel = l10n.t('Password');
850852
export const postgresPasswordPlaceholder = l10n.t('••••••••');
851853
export const postgresSslLabel = l10n.t('Use SSL');
852-
export const postgresUnnamedIntegration = (id: string) => l10n.t('Unnamed PostgreSQL Integration ({0})', id);
853854

854855
// BigQuery form strings
855856
export const bigQueryNameLabel = l10n.t('Name (optional)');
@@ -860,7 +861,43 @@ export namespace Integrations {
860861
export const bigQueryCredentialsPlaceholder = l10n.t('{"type": "service_account", ...}');
861862
export const bigQueryCredentialsRequired = l10n.t('Credentials are required');
862863
export const bigQueryInvalidJson = (message: string) => l10n.t('Invalid JSON: {0}', message);
863-
export const bigQueryUnnamedIntegration = (id: string) => l10n.t('Unnamed BigQuery Integration ({0})', id);
864+
865+
// Snowflake form strings
866+
export const snowflakeNameLabel = l10n.t('Name (optional)');
867+
export const snowflakeNamePlaceholder = l10n.t('My Snowflake Database');
868+
export const snowflakeAccountLabel = l10n.t('Account name');
869+
export const snowflakeAccountPlaceholder = l10n.t('ptb34938.us-east-1');
870+
export const snowflakeAuthMethodLabel = l10n.t('Authentication');
871+
export const snowflakeAuthMethodSubLabel = l10n.t('Method');
872+
export const snowflakeAuthMethodUsernamePassword = l10n.t('Username & password');
873+
export const snowflakeAuthMethodKeyPair = l10n.t('Key-pair (service account)');
874+
export const snowflakeUnsupportedAuthMethod = l10n.t(
875+
'This Snowflake integration uses an authentication method that is not supported in VS Code. You can view the integration details but cannot edit or use it.'
876+
);
877+
export const snowflakeUsernameLabel = l10n.t('Username');
878+
export const snowflakeUsernamePlaceholder = l10n.t('user');
879+
export const snowflakePasswordLabel = l10n.t('Password');
880+
export const snowflakePasswordPlaceholder = l10n.t('••••••••');
881+
export const snowflakeServiceAccountUsernameLabel = l10n.t('Service Account Username');
882+
export const snowflakeServiceAccountUsernameHelp = l10n.t(
883+
'The username of the service account that will be used to connect to Snowflake'
884+
);
885+
export const snowflakePrivateKeyLabel = l10n.t('Private Key');
886+
export const snowflakePrivateKeyHelp = l10n.t(
887+
'The private key in PEM format. Make sure to include the entire key, including BEGIN and END markers.'
888+
);
889+
export const snowflakePrivateKeyPlaceholder = l10n.t("Begins with '-----BEGIN PRIVATE KEY-----'");
890+
export const snowflakePrivateKeyPassphraseLabel = l10n.t('Private Key Passphrase (optional)');
891+
export const snowflakePrivateKeyPassphraseHelp = l10n.t(
892+
'If the private key is encrypted, provide the passphrase to decrypt it.'
893+
);
894+
export const snowflakePrivateKeyPassphrasePlaceholder = l10n.t('');
895+
export const snowflakeDatabaseLabel = l10n.t('Database (optional)');
896+
export const snowflakeDatabasePlaceholder = l10n.t('');
897+
export const snowflakeRoleLabel = l10n.t('Role (optional)');
898+
export const snowflakeRolePlaceholder = l10n.t('');
899+
export const snowflakeWarehouseLabel = l10n.t('Warehouse (optional)');
900+
export const snowflakeWarehousePlaceholder = l10n.t('');
864901
}
865902

866903
export namespace Deprecated {

src/platform/errors/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ export type ErrorCategory =
105105
| 'unknownProduct'
106106
| 'invalidInterpreter'
107107
| 'pythonAPINotInitialized'
108-
| 'deepnoteserver';
108+
| 'deepnoteserver'
109+
| 'unsupported_integration';
109110

110111
// If there are errors, then the are added to the telementry properties.
111112
export type TelemetryErrorProperties = {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { BaseError } from './types';
2+
3+
/**
4+
* Error thrown when an unsupported integration type is encountered.
5+
*
6+
* Cause:
7+
* An integration configuration has a type that is not supported by the SQL integration system,
8+
* or an integration uses an authentication method that is not supported in VSCode.
9+
*
10+
* Handled by:
11+
* Callers should handle this error and inform the user that the integration type or
12+
* authentication method is not supported.
13+
*/
14+
export class UnsupportedIntegrationError extends BaseError {
15+
constructor(message: string) {
16+
super('unsupported_integration', message);
17+
}
18+
}

src/platform/notebooks/deepnote/integrationTypes.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ export const DATAFRAME_SQL_INTEGRATION_ID = 'deepnote-dataframe-sql';
99
*/
1010
export enum IntegrationType {
1111
Postgres = 'postgres',
12-
BigQuery = 'bigquery'
12+
BigQuery = 'bigquery',
13+
Snowflake = 'snowflake'
1314
}
1415

1516
/**
1617
* Map our IntegrationType enum to Deepnote integration type strings
1718
*/
1819
export const INTEGRATION_TYPE_TO_DEEPNOTE = {
1920
[IntegrationType.Postgres]: 'pgsql',
20-
[IntegrationType.BigQuery]: 'big-query'
21+
[IntegrationType.BigQuery]: 'big-query',
22+
[IntegrationType.Snowflake]: 'snowflake'
2123
} as const satisfies { [type in IntegrationType]: string };
2224

2325
export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typeof INTEGRATION_TYPE_TO_DEEPNOTE];
@@ -27,7 +29,8 @@ export type RawIntegrationType = (typeof INTEGRATION_TYPE_TO_DEEPNOTE)[keyof typ
2729
*/
2830
export const DEEPNOTE_TO_INTEGRATION_TYPE: Record<RawIntegrationType, IntegrationType> = {
2931
pgsql: IntegrationType.Postgres,
30-
'big-query': IntegrationType.BigQuery
32+
'big-query': IntegrationType.BigQuery,
33+
snowflake: IntegrationType.Snowflake
3134
};
3235

3336
/**
@@ -61,10 +64,62 @@ export interface BigQueryIntegrationConfig extends BaseIntegrationConfig {
6164
credentials: string; // JSON string of service account credentials
6265
}
6366

67+
// Import and re-export Snowflake auth constants from shared module
68+
import {
69+
type SnowflakeAuthMethod,
70+
SnowflakeAuthMethods,
71+
SUPPORTED_SNOWFLAKE_AUTH_METHODS,
72+
isSupportedSnowflakeAuthMethod
73+
} from './snowflakeAuthConstants';
74+
export {
75+
type SnowflakeAuthMethod,
76+
SnowflakeAuthMethods,
77+
SUPPORTED_SNOWFLAKE_AUTH_METHODS,
78+
isSupportedSnowflakeAuthMethod
79+
};
80+
81+
/**
82+
* Base Snowflake configuration with common fields
83+
*/
84+
interface BaseSnowflakeConfig extends BaseIntegrationConfig {
85+
type: IntegrationType.Snowflake;
86+
account: string;
87+
warehouse?: string;
88+
database?: string;
89+
role?: string;
90+
}
91+
92+
/**
93+
* Snowflake integration configuration (discriminated union)
94+
*/
95+
export type SnowflakeIntegrationConfig = BaseSnowflakeConfig &
96+
(
97+
| {
98+
authMethod: typeof SnowflakeAuthMethods.PASSWORD | null;
99+
username: string;
100+
password: string;
101+
}
102+
| {
103+
authMethod: typeof SnowflakeAuthMethods.SERVICE_ACCOUNT_KEY_PAIR;
104+
username: string;
105+
privateKey: string;
106+
privateKeyPassphrase?: string;
107+
}
108+
| {
109+
// Unsupported auth methods - we store them but don't allow editing
110+
authMethod:
111+
| typeof SnowflakeAuthMethods.OKTA
112+
| typeof SnowflakeAuthMethods.NATIVE_SNOWFLAKE
113+
| typeof SnowflakeAuthMethods.AZURE_AD
114+
| typeof SnowflakeAuthMethods.KEY_PAIR;
115+
[key: string]: unknown; // Allow any additional fields for unsupported methods
116+
}
117+
);
118+
64119
/**
65120
* Union type of all integration configurations
66121
*/
67-
export type IntegrationConfig = PostgresIntegrationConfig | BigQueryIntegrationConfig;
122+
export type IntegrationConfig = PostgresIntegrationConfig | BigQueryIntegrationConfig | SnowflakeIntegrationConfig;
68123

69124
/**
70125
* Integration connection status
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Snowflake authentication methods
3+
*/
4+
export const SnowflakeAuthMethods = {
5+
PASSWORD: 'PASSWORD',
6+
OKTA: 'OKTA',
7+
NATIVE_SNOWFLAKE: 'NATIVE_SNOWFLAKE',
8+
AZURE_AD: 'AZURE_AD',
9+
KEY_PAIR: 'KEY_PAIR',
10+
SERVICE_ACCOUNT_KEY_PAIR: 'SERVICE_ACCOUNT_KEY_PAIR'
11+
} as const;
12+
13+
export type SnowflakeAuthMethod = (typeof SnowflakeAuthMethods)[keyof typeof SnowflakeAuthMethods];
14+
15+
/**
16+
* Supported auth methods that we can configure in VSCode
17+
*/
18+
export const SUPPORTED_SNOWFLAKE_AUTH_METHODS = [
19+
null, // Legacy username+password (no authMethod field)
20+
SnowflakeAuthMethods.PASSWORD,
21+
SnowflakeAuthMethods.SERVICE_ACCOUNT_KEY_PAIR
22+
] as const;
23+
24+
export type SupportedSnowflakeAuthMethod = (typeof SUPPORTED_SNOWFLAKE_AUTH_METHODS)[number];
25+
26+
/**
27+
* Type guard to check if a value is a supported Snowflake auth method
28+
* @param value The value to check
29+
* @returns true if the value is one of the supported auth methods
30+
*/
31+
export function isSupportedSnowflakeAuthMethod(value: unknown): value is SupportedSnowflakeAuthMethod {
32+
return (SUPPORTED_SNOWFLAKE_AUTH_METHODS as readonly unknown[]).includes(value);
33+
}

0 commit comments

Comments
 (0)