Skip to content
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

Connections: Create SQLite connection #6380

Merged
merged 8 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
191 changes: 186 additions & 5 deletions extensions/positron-connections/src/drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This needs some exception handling; if callMethod throws I think the exception should be logged (and we should probably presume the dependencies to be installed since otherwise the user will be blocked as I understand it?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In theory this is handled on the call site of checkDependencies and installDependencies:

try {
if (!props.selectedDriver.connect) {
throw new Error(
localize('positron.newConnectionModalDialog.createConnection.connectNotImplemented', "Connect method not implemented")
);
}
if (props.selectedDriver.checkDependencies && props.selectedDriver.installDependencies) {
const dependenciesInstalled = await props.selectedDriver.checkDependencies();
if (!dependenciesInstalled) {
await props.selectedDriver.installDependencies();
}
}
await props.selectedDriver.connect(code);
} catch (err) {
services.notificationService.error(err);
}

But indeed, they wouldn't be able to run connect() if checking for the dependencies fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With 2bfb463 we now presume that upon failure to check if dependencies are installed we can continue and try to connect.

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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

As above, need exception handling here

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',
Expand Down Expand Up @@ -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,
Expand All @@ -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
`;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ export const CreateConnection = (props: PropsWithChildren<CreateConnectionProps>
);
}

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);
Expand Down