Skip to content
Open
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: 7 additions & 7 deletions src/core/engine/wasm/WasmDatabaseEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,18 @@ import { getNodeFs } from '../../platform/fs';
// ============================================================================

interface WasmPreparedStatement {
run(params?: unknown[]): void;
bind(params?: unknown[]): boolean;
get(params?: unknown[]): CellValue[] | undefined;
run(params?: CellValue[]): void;
bind(params?: CellValue[]): boolean;
get(params?: CellValue[]): CellValue[] | undefined;
step(): boolean;
reset(): void;
free(): boolean;
getColumnNames(): string[];
}

export interface WasmDatabaseInstance {
exec(sql: string, params?: unknown[]): Array<{ columns: string[]; values: unknown[][] }>;
prepare(sql: string, params?: unknown[]): WasmPreparedStatement;
exec(sql: string, params?: CellValue[]): Array<{ columns: string[]; values: CellValue[][] }>;
prepare(sql: string, params?: CellValue[]): WasmPreparedStatement;
Comment on lines 34 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check CellValue type definition to understand the constraint
rg -A 10 "type CellValue" --type=ts

# Search for sql.js usage patterns to see what types are actually passed
rg -C 3 "\.bind\(|\.run\(|\.exec\(" src/core/engine/wasm/ --type=ts

Repository: zknpr/SQLite-Explorer

Length of output: 4829


🌐 Web query:

What parameter types does sql.js bind() method accept for prepared statement parameters?

💡 Result:

The bind method in sql.js accepts two primary parameter types to bind values to a prepared statement [1][2]. Depending on the type of parameters defined in your SQL statement, you should provide either an array or an object [1][2]: 1. Array: Use an array when binding values to anonymous (positional) parameters (e.g.,?,?NNN) [1][2]. The order of values in the array corresponds to the order of parameters in the SQL statement [2]. 2. Object: Use an object when binding values to named parameters (e.g.,:VVV, @VVV, $VVV) [1][2]. The keys of the object must match the parameter names defined in the SQL string [2]. Note that the prefixes (:, @, $) must be included in the object keys [1][2]. Mapping of JavaScript types to SQLite types: The library automatically handles the mapping of JavaScript data types to their corresponding SQLite types as follows [1][3]: JavaScript Type | SQLite Type --- | --- number | REAL, INTEGER boolean | INTEGER string | TEXT Array, Uint8Array | BLOB null | NULL The bind method resets the statement before applying new values [1][2]. If you pass null, the method does nothing and returns true [1][3].

Citations:


Add boolean to the CellValue type definition to match sql.js parameter support.

The interface declarations constrain sql.js parameters to CellValue[] (string | number | null | Uint8Array), but sql.js's official documentation shows the bind() method accepts boolean values, which it maps to SQLite INTEGER. The CellValue type should include boolean to accurately reflect the library's capabilities and prevent type safety gaps when boolean data is used with prepared statements.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/engine/wasm/WasmDatabaseEngine.ts` around lines 34 - 46, The
CellValue type used by the WasmPreparedStatement and WasmDatabaseInstance
interfaces is missing boolean support; update the CellValue type definition to
include boolean so binding boolean values is allowed (affects usages in
WasmPreparedStatement methods bind, run, get and WasmDatabaseInstance methods
exec and prepare), i.e. add boolean to the existing union for CellValue so
TypeScript reflects sql.js's accepted parameter types.

iterateStatements(sql: string): Iterable<WasmPreparedStatement>;
export(): Uint8Array;
close(): void;
Expand Down Expand Up @@ -122,7 +122,7 @@ export class WasmDatabaseEngine implements DatabaseOperations {

// Bind parameters only to the first statement to match exec behavior
if (isFirstStatement && params && params.length > 0) {
stmt.bind(params as unknown[]);
stmt.bind(params);
}
isFirstStatement = false;

Expand Down Expand Up @@ -301,7 +301,7 @@ export class WasmDatabaseEngine implements DatabaseOperations {
const stmt = this.instance.prepare(sql);
try {
for (const [rId, rowObj] of rowUpdates.entries()) {
const params: any[] = deletedColumns.map(c => rowObj[c.name] ?? null);
const params: CellValue[] = deletedColumns.map(c => rowObj[c.name] ?? null);
params.push(rId);
stmt.run(params);
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ export function processProtocolMessage(
/**
* Worker-like interface for message passing.
*/
interface WorkerPort {
export interface WorkerPort {
postMessage(data: unknown, transfer?: Transferable[]): void;
on(event: 'message', handler: (data: unknown) => void): void;
}
Expand Down
4 changes: 2 additions & 2 deletions src/editorController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export class DatabaseViewerProvider extends Disposable implements vsc.CustomRead
// Build environment data for webview
const vscodeEnv = {
webviewId,
browserExt: toBoolString(!!import.meta.env.VSCODE_BROWSER_EXT),
browserExt: toBoolString(!!import.meta.env?.VSCODE_BROWSER_EXT),
uriScheme, appHost, appName, extensionUrl,
uiKind: uiKindToString(uiKind),
firstInstall: doTry(() => new Date(this.context.globalState.get<number>(FirstInstallMs) ?? Date.now()).toISOString()),
Expand Down Expand Up @@ -378,7 +378,7 @@ export function registerEditorProvider(
outputChannel: vsc.OutputChannel | null,
{ verified, accessToken, readOnly }: { verified: boolean, accessToken?: string, readOnly?: boolean }
) {
const enableReadWrite = !import.meta.env.VSCODE_BROWSER_EXT && verified && !readOnly && SupportsWriteMode;
const enableReadWrite = !import.meta.env?.VSCODE_BROWSER_EXT && verified && !readOnly && SupportsWriteMode;
const Provider = enableReadWrite ? DatabaseEditorProvider : DatabaseViewerProvider;
return vsc.window.registerCustomEditorProvider(
viewType,
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/editorController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import './vscode_mock_setup';

import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';
import { mockVscode } from './mocks/vscode';

// SupportsWriteMode in databaseModel.ts is evaluated at module-load time from
// `vsc.env.remoteName` and `CurrentExtension?.extensionKind`. Set both so the
// read-write path resolves to true regardless of which test loads databaseModel first.
(mockVscode as any).ExtensionKind = { Workspace: 2, UI: 1 };
mockVscode.env.remoteName = 'remote';
(mockVscode as any).extensions = {
getExtension: () => ({ extensionKind: 2 })
};
Comment on lines +10 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read its content
if [ -f "tests/unit/editorController.test.ts" ]; then
  echo "=== File exists ==="
  wc -l tests/unit/editorController.test.ts
  echo ""
  echo "=== First 30 lines (including imports and lines 10-14) ==="
  head -n 30 tests/unit/editorController.test.ts
else
  echo "File not found"
fi

Repository: zknpr/SQLite-Explorer

Length of output: 1441


🏁 Script executed:

# Also check for vscode_mock_setup import
rg -n "vscode_mock_setup" tests/unit/editorController.test.ts | head -5

Repository: zknpr/SQLite-Explorer

Length of output: 97


🏁 Script executed:

# Search for any other uses of ExtensionKind, env.remoteName, or extensions in the test file
rg -n "(ExtensionKind|remoteName|extensions)" tests/unit/editorController.test.ts

Repository: zknpr/SQLite-Explorer

Length of output: 289


Use Object.defineProperty for mocked VS Code readonly fields.

Direct assignment to VS Code API fields like ExtensionKind, env.remoteName, and extensions should use Object.defineProperty instead for better test robustness and consistency with readonly property expectations.

Suggested patch
-(mockVscode as any).ExtensionKind = { Workspace: 2, UI: 1 };
-mockVscode.env.remoteName = 'remote';
-(mockVscode as any).extensions = {
-    getExtension: () => ({ extensionKind: 2 })
-};
+Object.defineProperty(mockVscode as object, 'ExtensionKind', {
+    value: { Workspace: 2, UI: 1 },
+    writable: true,
+    configurable: true,
+});
+Object.defineProperty(mockVscode.env as object, 'remoteName', {
+    value: 'remote',
+    writable: true,
+    configurable: true,
+});
+Object.defineProperty(mockVscode as object, 'extensions', {
+    value: { getExtension: () => ({ extensionKind: 2 }) },
+    writable: true,
+    configurable: true,
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(mockVscode as any).ExtensionKind = { Workspace: 2, UI: 1 };
mockVscode.env.remoteName = 'remote';
(mockVscode as any).extensions = {
getExtension: () => ({ extensionKind: 2 })
};
Object.defineProperty(mockVscode as object, 'ExtensionKind', {
value: { Workspace: 2, UI: 1 },
writable: true,
configurable: true,
});
Object.defineProperty(mockVscode.env as object, 'remoteName', {
value: 'remote',
writable: true,
configurable: true,
});
Object.defineProperty(mockVscode as object, 'extensions', {
value: { getExtension: () => ({ extensionKind: 2 }) },
writable: true,
configurable: true,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/editorController.test.ts` around lines 10 - 14, The test currently
assigns to readonly VS Code API fields directly (ExtensionKind, env.remoteName,
extensions) which is fragile; replace those direct assignments by using
Object.defineProperty on the mock object (e.g., define ExtensionKind, define env
with remoteName, and define extensions.getExtension) so properties are set as
non-writable/configurable as needed; update the mocked symbols referenced in
this file (ExtensionKind, mockVscode.env.remoteName, and
mockVscode.extensions.getExtension) to be defined via Object.defineProperty to
mirror VS Code readonly behavior.


// workerFactory imports threadPool which crashes due to bare `import.meta.env` at
// module load. Mock it in require cache before editorController is required.
const moduleCache = require('module')._cache;
const workerFactoryPath = require.resolve('../../src/workerFactory');
moduleCache[workerFactoryPath] = {
id: workerFactoryPath,
filename: workerFactoryPath,
loaded: true,
exports: {
createDatabaseConnection: () => {}
}
};

const editorControllerModule = require('../../src/editorController');
const { registerEditorProvider, DatabaseViewerProvider, DatabaseEditorProvider } = editorControllerModule;

describe('registerEditorProvider', () => {
type RegisterCall = { viewType: string; provider: unknown; options: unknown };
let calls: RegisterCall[];
let originalRegister: any;

beforeEach(() => {
calls = [];
originalRegister = (mockVscode.window as any).registerCustomEditorProvider;
(mockVscode.window as any).registerCustomEditorProvider = (
viewType: string,
provider: unknown,
options: unknown
) => {
calls.push({ viewType, provider, options });
return { dispose: () => {} };
};
});

afterEach(() => {
(mockVscode.window as any).registerCustomEditorProvider = originalRegister;
});

const ctx = { extensionUri: mockVscode.Uri.file('/ext'), globalState: { get: () => undefined } } as any;

it('registers DatabaseViewerProvider when readOnly=true regardless of verified', () => {
registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: true, readOnly: true });

assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].viewType, 'sqlite-explorer.view');
assert.ok(calls[0].provider instanceof DatabaseViewerProvider);
assert.ok(!(calls[0].provider instanceof DatabaseEditorProvider));
});

it('registers DatabaseViewerProvider when verified=false', () => {
registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: false, readOnly: false });

assert.strictEqual(calls.length, 1);
assert.ok(calls[0].provider instanceof DatabaseViewerProvider);
assert.ok(!(calls[0].provider instanceof DatabaseEditorProvider));
});

it('registers DatabaseEditorProvider when verified=true and readOnly=false (write mode enabled)', () => {
registerEditorProvider('sqlite-explorer.edit', ctx, undefined, null, { verified: true, readOnly: false });

assert.strictEqual(calls.length, 1);
assert.strictEqual(calls[0].viewType, 'sqlite-explorer.edit');
// DatabaseEditorProvider extends DatabaseViewerProvider, so check the more specific type.
assert.ok(calls[0].provider instanceof DatabaseEditorProvider);
});

it('passes retainContextWhenHidden=false in webview options', () => {
registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: true, readOnly: true });

const options = calls[0].options as { webviewOptions: { retainContextWhenHidden: boolean } };
assert.strictEqual(options.webviewOptions.retainContextWhenHidden, false);
});

it('returns a disposable from registerCustomEditorProvider', () => {
const result = registerEditorProvider('sqlite-explorer.view', ctx, undefined, null, { verified: true, readOnly: true });

assert.ok(result && typeof result.dispose === 'function');
});
});
4 changes: 2 additions & 2 deletions tests/unit/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ describe('RPC', () => {
});

it('should handle invocations', (context) => {
const methods = {
add: (a: number, b: number) => a + b
const methods: Record<string, (...args: unknown[]) => unknown> = {
add: (...args: unknown[]) => (args[0] as number) + (args[1] as number)
};
Comment on lines +14 to 16
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This change reduces type safety by using unknown[] and type casting. The original, more specific function signature (a: number, b: number) => a + b is preferable as it's more readable and safer, and it is assignable to the required (...args: unknown[]) => unknown type.

I suggest restoring the safer function implementation while keeping the explicit type annotation:

Suggested change
const methods: Record<string, (...args: unknown[]) => unknown> = {
add: (...args: unknown[]) => (args[0] as number) + (args[1] as number)
};
const methods: Record<string, (...args: unknown[]) => unknown> = {
add: (a: number, b: number) => a + b
};


let response: any = null;
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/webviewMessageHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('WebviewMessageHandler', () => {
return true;
};

const handler = new WebviewMessageHandler(postMessage, hostBridge);
const handler = new WebviewMessageHandler(postMessage, hostBridge as unknown as import('../../src/hostBridge').HostBridge);

handler.handleMessage({
channel: 'rpc',
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('WebviewMessageHandler', () => {
return true;
};

const handler = new WebviewMessageHandler(postMessage, hostBridge);
const handler = new WebviewMessageHandler(postMessage, hostBridge as unknown as import('../../src/hostBridge').HostBridge);

handler.handleMessage({
channel: 'rpc',
Expand All @@ -77,7 +77,7 @@ describe('WebviewMessageHandler', () => {
return true;
};

const handler = new WebviewMessageHandler(postMessage, hostBridge);
const handler = new WebviewMessageHandler(postMessage, hostBridge as unknown as import('../../src/hostBridge').HostBridge);

handler.handleMessage({
type: 'rpc-request',
Expand Down