Skip to content
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to Simple DB are documented in this file.

## Unreleased

### Added

- One readable JSON file per database connection, stored locally by Simple DB.
- Automatic migration of existing 0.1.1 connection profiles to JSON files.
- `Open Connection JSON`, `Set Password`, and `Open Connections Folder` actions.
- A native `Simple DB` editor context submenu containing the six main database actions.

### Changed

- Simplified connection creation: choose the engine and name, optionally enter a secure network password, then edit all non-secret connection parameters together in JSON.
- Passwords remain in VS Code `SecretStorage` and are explicitly rejected from connection JSON files.

## 0.1.1 - 2026-08-07

### Added
Expand Down
52 changes: 50 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The extension opens regular VS Code SQL documents. `F5` launches `src/extension.
## Main features

- Create, edit, test, and delete multiple connections for each database engine.
- Keep every connection in its own readable JSON file and edit all connection parameters in one place.
- Store passwords in `SecretStorage`, never inside profiles or the repository.
- Connect to several database engines simultaneously and disconnect them explicitly.
- Explore databases, schemas, and engine-specific objects.
Expand Down Expand Up @@ -86,13 +87,60 @@ CSV export protects values that spreadsheet applications could interpret as form

## Connections and security

- Connection profiles do not contain passwords.
- Passwords are stored with the VS Code `SecretStorage` API.
- **Create Connection** asks only for the database engine, a connection name, and a password for network databases. SQLite uses the native file picker.
- Simple DB then creates one readable JSON file per connection and opens it in VS Code so host, port, database/service, username, TLS options, and timeouts can be edited together.
- Saving a connection JSON with `Ctrl+S` reloads that connection automatically. Existing connections from Simple DB 0.1.1 are migrated to JSON files on first launch.
- **Open Connection JSON** opens the selected profile, **Set Password** changes its secure password, and **Open Connections Folder** reveals all local connection files.
- Connection JSON files never contain passwords. Passwords are stored with the VS Code `SecretStorage` API.
- SSL/TLS, encryption, and certificate trust are explicit options where supported by the database engine.
- `simpleDb.confirmDestructiveQueries` is enabled by default.
- `simpleDb.warnUnsafeDml` is enabled by default.
- Query history can contain literals written in SQL. It can be disabled with `simpleDb.history.enabled` or cleared from the **History** view.

Example Oracle connection JSON:

```json
{
"id": "generated-by-simple-db",
"name": "Oracle Production",
"engine": "oracle",
"host": "192.168.1.20",
"port": 1521,
"serviceName": "ORCLPDB1",
"connectString": "",
"user": "report_user",
"connectTimeoutMs": 15000,
"queryTimeoutMs": 300000
}
```

Example SQLite connection JSON:

```json
{
"id": "generated-by-simple-db",
"name": "Local SQLite",
"engine": "sqlite",
"filePath": "C:\\data\\sample.db",
"readOnly": false,
"connectTimeoutMs": 15000,
"queryTimeoutMs": 300000
}
```

The `id` is generated and managed by Simple DB. Do not change it. Use **Simple DB: Set Password** instead of adding a `password` field to a JSON file.

### Editor context menu

Right-clicking inside an editor now shows a native **Simple DB** submenu with the main actions:

- Create Connection
- New Query
- Execute Selection or Current Statement
- Execute Entire Document
- Change Editor Connection
- Show History

### SQLite note

SQLite runs in a dedicated WebAssembly Worker so long-running queries do not block the UI and can be cancelled by terminating the Worker. The database file is held as an in-memory snapshot while connected. Before every operation, Simple DB checks whether the main file, WAL, or journal changed externally. If a conflict is detected, it refuses to continue and asks the user to reconnect. If an active WAL exists when the connection is opened, the connection is rejected until the owning process checkpoints/closes the WAL, preventing Simple DB from loading or overwriting an incomplete snapshot.
Expand Down
62 changes: 54 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"commands": [
{
"command": "simpleDb.addConnection",
"title": "Add Connection",
"title": "Create Connection",
"category": "Simple DB",
"icon": "$(add)"
},
Expand Down Expand Up @@ -91,10 +91,22 @@
},
{
"command": "simpleDb.editConnection",
"title": "Edit Connection",
"title": "Open Connection JSON",
"category": "Simple DB",
"icon": "$(edit)"
},
{
"command": "simpleDb.setPassword",
"title": "Set Password",
"category": "Simple DB",
"icon": "$(key)"
},
{
"command": "simpleDb.openConnectionsFolder",
"title": "Open Connections Folder",
"category": "Simple DB",
"icon": "$(folder-opened)"
},
{
"command": "simpleDb.deleteConnection",
"title": "Delete Connection",
Expand Down Expand Up @@ -219,6 +231,12 @@
"icon": "$(copy)"
}
],
"submenus": [
{
"id": "simpleDb.editorContextMenu",
"label": "Simple DB"
}
],
"viewsContainers": {
"activitybar": [
{
Expand Down Expand Up @@ -254,6 +272,11 @@
"when": "view == simpleDb.connections",
"group": "navigation@2"
},
{
"command": "simpleDb.openConnectionsFolder",
"when": "view == simpleDb.connections",
"group": "navigation@3"
},
{
"command": "simpleDb.clearHistory",
"when": "view == simpleDb.history",
Expand Down Expand Up @@ -297,10 +320,15 @@
"group": "connection@2"
},
{
"command": "simpleDb.deleteConnection",
"command": "simpleDb.setPassword",
"when": "view == simpleDb.connections && viewItem =~ /simpleDb.connection/",
"group": "connection@3"
},
{
"command": "simpleDb.deleteConnection",
"when": "view == simpleDb.connections && viewItem =~ /simpleDb.connection/",
"group": "connection@4"
},
{
"command": "simpleDb.selectTable",
"when": "view == simpleDb.connections && viewItem =~ /simpleDb.(table|view|materializedView)/",
Expand Down Expand Up @@ -370,20 +398,38 @@
}
],
"editor/context": [
{
"submenu": "simpleDb.editorContextMenu",
"group": "simpleDb@1"
}
],
"simpleDb.editorContextMenu": [
{
"command": "simpleDb.addConnection",
"group": "1_connection@1"
},
{
"command": "simpleDb.newQuery",
"group": "1_connection@2"
},
{
"command": "simpleDb.executeCurrent",
"when": "editorLangId == sql",
"group": "simpleDb@1"
"group": "2_query@1"
},
{
"command": "simpleDb.executeSelection",
"when": "editorLangId == sql && editorHasSelection",
"group": "simpleDb@2"
"command": "simpleDb.executeDocument",
"when": "editorLangId == sql",
"group": "2_query@2"
},
{
"command": "simpleDb.changeEditorConnection",
"when": "editorLangId == sql",
"group": "simpleDb@3"
"group": "2_query@3"
},
{
"command": "simpleDb.showHistory",
"group": "3_history@1"
}
]
},
Expand Down
129 changes: 90 additions & 39 deletions src/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const {
const { ConnectionStore } = require('./storage/connectionStore');
const { HistoryStore } = require('./storage/historyStore');
const { ResultStore } = require('./storage/resultStore');
const { promptConnection } = require('./ui/connectionForm');
const { promptConnection, promptPassword } = require('./ui/connectionForm');
const { ConnectionsTreeProvider } = require('./views/connectionsTreeProvider');
const { HistoryTreeProvider } = require('./views/historyTreeProvider');
const { ResultPanel } = require('./views/resultPanel');
Expand Down Expand Up @@ -124,7 +124,15 @@ function registerCommand(context, commandId, handler) {
}

async function activate(context) {
const connectionStore = new ConnectionStore(context.globalState, context.secrets);
const connectionStore = new ConnectionStore(context.globalState, context.secrets, {
directoryPath: path.join(context.globalStorageUri.fsPath, 'connections'),
});
const connectionLoad = await connectionStore.initialize();
if (connectionLoad.errors.length > 0) {
vscode.window.showWarningMessage(
`Simple DB: ${connectionLoad.errors.length} connection JSON file(s) could not be loaded. Open the Connections folder to review them.`,
);
}
const connectionManager = new ConnectionManager(connectionStore);
const editorSessionManager = new EditorSessionManager(
connectionStore,
Expand Down Expand Up @@ -174,21 +182,28 @@ async function activate(context) {
registerCommand(context, 'simpleDb.addConnection', async (node) => {
const form = await promptConnection({ engineId: node?.engineId });
if (!form) return;
if (form.testBeforeSave) {
const result = await connectionManager.testProfile(
form.profile,
form.effectivePassword,
);
vscode.window.showInformationMessage(
`Simple DB: connection successful (${result.elapsedMs} ms) · ${result.serverVersion}`,
);
}
const saved = await connectionStore.save(form.profile, form.password);
connectionsProvider.refresh();
vscode.window.showInformationMessage(`Simple DB: connection "${saved.name}" saved.`);
const filePath = connectionStore.connectionFile(saved.id);
const document = await vscode.workspace.openTextDocument(filePath);
await vscode.window.showTextDocument(document, { preview: false });
vscode.window.showInformationMessage(
`Simple DB: "${saved.name}" created. Edit the JSON, press Ctrl+S, then test or connect.`,
);
});

registerCommand(context, 'simpleDb.refreshConnections', () => {
registerCommand(context, 'simpleDb.refreshConnections', async () => {
const hasActiveConnection = connectionStore
.list()
.some((profile) => connectionManager.isConnected(profile.id));
if (!hasActiveConnection) {
const result = await connectionStore.reload();
if (result.errors.length > 0) {
vscode.window.showWarningMessage(
`Simple DB: ${result.errors.length} connection JSON file(s) could not be loaded.`,
);
}
}
connectionsProvider.refresh();
});

Expand Down Expand Up @@ -222,7 +237,7 @@ async function activate(context) {
});

registerCommand(context, 'simpleDb.testConnection', async (node) => {
const profile = profileForNode(connectionStore, node);
const profile = profileForNode(connectionStore, node) || (await pickProfile(connectionStore));
if (!profile) throw new Error('Connection not found.');
const result = await connectionManager.testConnection(profile.id);
vscode.window.showInformationMessage(
Expand All @@ -233,43 +248,48 @@ async function activate(context) {
registerCommand(context, 'simpleDb.editConnection', async (node) => {
const profile = profileForNode(connectionStore, node);
if (!profile) throw new Error('Connection not found.');
const existingPassword = await connectionStore.getPassword(profile.id);
const form = await promptConnection({
existingProfile: profile,
existingPassword,
});
if (!form) return;

if (form.testBeforeSave) {
const result = await connectionManager.testProfile(
form.profile,
form.effectivePassword,
);
vscode.window.showInformationMessage(
`Simple DB: settings verified in ${result.elapsedMs} ms · ${result.serverVersion}`,
);
}

if (connectionManager.isConnected(profile.id)) {
const transactions = connectionManager.transactionCount(profile.id);
const executions = connectionManager.executionCount(profile.id);
const text = transactions > 0 || executions > 0
? `Saving requires disconnecting: ${executions} query/queries will be cancelled and ${transactions} transaction(s) will be rolled back.`
: 'Saving requires disconnecting the active connection.';
? `Editing requires disconnecting: ${executions} query/queries will be cancelled and ${transactions} transaction(s) will be rolled back.`
: 'Editing requires disconnecting the active connection.';
const answer = await vscode.window.showWarningMessage(
text,
{ modal: true },
transactions > 0 || executions > 0
? 'Save, cancel queries, and ROLLBACK'
: 'Save and disconnect',
? 'Disconnect, cancel queries, and ROLLBACK'
: 'Disconnect and edit',
);
if (!answer) return;
await connectionManager.disconnect(profile.id);
}
await connectionStore.save(form.profile, form.password, {
keepExistingPassword: form.password === undefined,
});
connectionsProvider.refresh();
const filePath = connectionStore.connectionFile(profile.id);
if (!filePath) throw new Error('Connection JSON file not found.');
const document = await vscode.workspace.openTextDocument(filePath);
await vscode.window.showTextDocument(document, { preview: false });
});

registerCommand(context, 'simpleDb.setPassword', async (node) => {
const profile = profileForNode(connectionStore, node) || (await pickProfile(connectionStore));
if (!profile) return;
if (profile.engine === 'sqlite') {
vscode.window.showInformationMessage('Simple DB: SQLite connections do not use a password.');
return;
}
const password = await promptPassword(profile);
if (password === undefined) return;
await connectionStore.setPassword(profile.id, password);
vscode.window.showInformationMessage(
`Simple DB: password for "${profile.name}" stored securely.`,
);
});

registerCommand(context, 'simpleDb.openConnectionsFolder', async () => {
await vscode.commands.executeCommand(
'revealFileInOS',
vscode.Uri.file(connectionStore.connectionDirectory()),
);
});

registerCommand(context, 'simpleDb.deleteConnection', async (node) => {
Expand Down Expand Up @@ -601,6 +621,37 @@ async function activate(context) {
if (entry) await historyStore.delete(entry.id);
});

const connectionFileSaveDisposable = vscode.workspace.onDidSaveTextDocument(
async (document) => {
const filePath = document.uri.fsPath;
if (!connectionStore.isConnectionFile(filePath)) return;
try {
const profileId = connectionStore.profileIdForFile(filePath);
if (profileId && connectionManager.isConnected(profileId)) {
const transactions = connectionManager.transactionCount(profileId);
const executions = connectionManager.executionCount(profileId);
if (transactions > 0 || executions > 0) {
vscode.window.showWarningMessage(
'Simple DB: connection settings were saved on disk but cannot be applied while queries or transactions are active. Disconnect, then refresh Connections.',
);
return;
}
await connectionManager.disconnect(profileId);
}
const saved = await connectionStore.reloadFile(filePath);
connectionsProvider.refresh();
vscode.window.showInformationMessage(
`Simple DB: connection "${saved.name}" updated from JSON.`,
);
} catch (error) {
vscode.window.showErrorMessage(
`Simple DB: could not load this connection JSON — ${error.message}`,
);
}
},
);
context.subscriptions.push(connectionFileSaveDisposable);

const closeDisposable = vscode.workspace.onDidCloseTextDocument(async (document) => {
const session = editorSessionManager.detach(document);
if (!session) return;
Expand Down
Loading
Loading