From a1959ec52fc950f7324e395afe71a727ea07fdbe Mon Sep 17 00:00:00 2001 From: Daniel Falbel Date: Thu, 20 Feb 2025 10:42:55 -0300 Subject: [PATCH] Connections: Create SQLite connection (#6380) Addresses https://github.com/posit-dev/positron/issues/6185 Additionaly, add support for checking and installing package dependencies when they are not installed. --- .../positron-connections/src/drivers.ts | 191 +++++++++++++++++- .../createConnectionState.tsx | 20 ++ 2 files changed, 206 insertions(+), 5 deletions(-) diff --git a/extensions/positron-connections/src/drivers.ts b/extensions/positron-connections/src/drivers.ts index 8bdc06b95b1..fac8a17d1aa 100644 --- a/extensions/positron-connections/src/drivers.ts +++ b/extensions/positron-connections/src/drivers.ts @@ -7,12 +7,107 @@ import * as positron from 'positron'; import * as vscode from 'vscode'; export function registerConnectionDrivers(context: vscode.ExtensionContext) { - context.subscriptions.push( - positron.connections.registerConnectionDriver(new RPostgreSQLDriver()) - ); + for (const driver of [new RSQLiteDriver(), new RPostgreSQLDriver(), new PythonSQLiteDriver()]) { + context.subscriptions.push( + positron.connections.registerConnectionDriver(driver) + ); + } +} + +/// A generic driver implementation +class RDriver implements positron.ConnectionsDriver { + + driverId: string = 'unknown'; + metadata: positron.ConnectionsDriverMetadata = { + languageId: 'r', + name: 'Unknown', + inputs: [] + }; + + constructor(readonly packages: string[]) { } + + async connect(code: string) { + const exec = await positron.runtime.executeCode( + 'r', + code, + true, + false, + positron.RuntimeCodeExecutionMode.Interactive, + positron.RuntimeErrorBehavior.Continue + ); + if (!exec) { + throw new Error('Failed to execute code'); + } + return; + } + + async checkDependencies() { + // Currently we skip dependency checks if there's no active R session + // in the foreground. + if (this.packages.length === 0) { + return true; + } + + const session = await positron.runtime.getForegroundSession(); + if (session) { + + if (session.runtimeMetadata.languageId !== 'r') { + return true; + } + + for (const pkg of this.packages) { + const installed = await session.callMethod?.('is_installed', pkg); + if (!installed) { + return false; + } + } + } + + return true; + } + + async installDependencies() { + // Similar to checkDependencies, we skip dependency installation if there's + // no active R session in the foreground. + if (this.packages.length === 0) { + return true; + } + const session = await positron.runtime.getForegroundSession(); + if (session) { + + if (session.runtimeMetadata.languageId !== 'r') { + return true; + } + + const allow_install = await positron.window.showSimpleModalDialogPrompt( + vscode.l10n.t("Installing dependencies"), + vscode.l10n.t("The following R packages are required for this connection: {0}. Would you like to install them now?", this.packages.join(', ')) + ); + + if (!allow_install) { + return false; + } + + for (const pkg of this.packages) { + const installed = await session.callMethod?.('is_installed', pkg); + if (!installed) { + const install_succeed = await session.callMethod?.('install_packages', pkg); + if (!install_succeed) { + throw new Error('Failed to install dependencies'); + } + } + } + } + return true; + } } -class RPostgreSQLDriver implements positron.ConnectionsDriver { +class RPostgreSQLDriver extends RDriver implements positron.ConnectionsDriver { + + constructor() { + super(['RPostgres', 'DBI']); + } + driverId: string = 'postgres'; metadata: positron.ConnectionsDriverMetadata = { languageId: 'r', @@ -83,10 +178,66 @@ con <- dbConnect( ) `; } +} + +class RSQLiteDriver extends RDriver implements positron.ConnectionsDriver { + + constructor() { + super(['RSQLite', 'DBI', 'connections']); + } + + driverId: string = 'sqlite'; + metadata: positron.ConnectionsDriverMetadata = { + languageId: 'r', + name: 'SQLite', + inputs: [ + { + 'id': 'dbname', + 'label': 'Database Name', + 'type': 'string', + 'value': 'database.db' + }, + { + 'id': 'bigint', + 'label': 'Integer representation', + 'type': 'option', + 'options': [ + { 'identifier': 'integer64', 'title': 'integer64' }, + { 'identifier': 'integer', 'title': 'integer' }, + { 'identifier': 'numeric', 'title': 'numeric' }, + { 'identifier': 'character', 'title': 'character' } + ], + 'value': 'integer64' + }, + ] + }; + + generateCode(inputs: positron.ConnectionsInput[]) { + const dbname = inputs.find(input => input.id === 'dbname')?.value ?? ''; + const bigint = inputs.find(input => input.id === 'bigint')?.value ?? ''; + + return `library(DBI) +con <- dbConnect( + RSQLite::SQLite(), + dbname = ${JSON.stringify(dbname)}, + bigint = ${JSON.stringify(bigint)} +) +connections::connection_view(con) +`; + } +} + +class PythonDriver implements positron.ConnectionsDriver { + driverId: string = 'python'; + metadata: positron.ConnectionsDriverMetadata = { + languageId: 'python', + name: 'Unknown', + inputs: [] + }; async connect(code: string) { const exec = await positron.runtime.executeCode( - 'r', + 'python', code, true, false, @@ -100,3 +251,33 @@ con <- dbConnect( } } +class PythonSQLiteDriver extends PythonDriver implements positron.ConnectionsDriver { + driverId: string = 'py-sqlite'; + metadata: positron.ConnectionsDriverMetadata = { + languageId: 'python', + name: 'SQLite', + inputs: [ + { + 'id': 'dbname', + 'label': 'Database Name', + 'type': 'string', + 'value': 'database.db' + }, + { + 'id': 'timeout', + 'label': 'Timeout', + 'type': 'number', + 'value': '5.0' + }, + ] + }; + + generateCode(inputs: positron.ConnectionsInput[]) { + const dbname = inputs.find(input => input.id === 'dbname')?.value; + + return `import sqlite3 +conn = sqlite3.connect(${JSON.stringify(dbname) ?? JSON.stringify('')}) +%connection_show conn +`; + } +} diff --git a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx index 9f802f6ff4b..69953dc466c 100644 --- a/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx +++ b/src/vs/workbench/contrib/positronConnections/browser/components/newConnectionModalDialog/createConnectionState.tsx @@ -70,6 +70,26 @@ export const CreateConnection = (props: PropsWithChildren ); } + if (props.selectedDriver.checkDependencies && props.selectedDriver.installDependencies) { + let dependenciesInstalled = false; + try { + dependenciesInstalled = await props.selectedDriver.checkDependencies(); + } catch (err) { + services.notificationService.error(localize( + 'positron.newConnectionModalDialog.createConnection.failedToCheckDependencies', + 'Failed to check if dependencies are installed: {}', + err + )); + // If we fail to check if dependencies are installed, we presume they are installed + // and let the user try to connect anyway so they don't get blocked. + dependenciesInstalled = true; + } + + if (!dependenciesInstalled) { + await props.selectedDriver.installDependencies(); + } + } + await props.selectedDriver.connect(code); } catch (err) { services.notificationService.error(err);