Skip to content

Commit 83cd9c4

Browse files
zknprclaude
andcommitted
refactor: consolidate type improvements from PRs #76/#81/#86/#87/#91/#94
- Replace `any` with `ModificationEntry`/`AbortSignal` on HostBridge applyEdits, undo, redo, commit, rollback methods - Replace `any` with `unknown` on ColumnTypeInfo index signature - Remove unused `colTypes` param from determineCellExtension - Consolidate DbParams and ExportOptions to core/types.ts (was duplicated in main.ts, hostBridge.ts, and tableExporter.ts) - Replace `any` with `unknown`/`ExportOptions` on export command signatures - Export exportToCsv and exportToSql for testability - Add unit tests for exportToCsv and exportToSql Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e7c012b commit 83cd9c4

5 files changed

Lines changed: 123 additions & 42 deletions

File tree

src/core/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,38 @@ export interface TableCountOptions {
320320
globalFilter?: string;
321321
}
322322

323+
// ============================================================================
324+
// Export Types
325+
// ============================================================================
326+
327+
/**
328+
* Parameters identifying the database and table for export.
329+
*/
330+
export interface DbParams {
331+
/** Database filename */
332+
filename?: string;
333+
/** Table name to export */
334+
table: string;
335+
/** Schema/display name */
336+
name?: string;
337+
/** Document URI */
338+
uri?: string;
339+
}
340+
341+
/**
342+
* Options controlling export format and behavior.
343+
*/
344+
export interface ExportOptions {
345+
/** Output format */
346+
format?: string;
347+
/** Include column header row */
348+
header?: boolean;
349+
/** Include table name in SQL output */
350+
includeTableName?: boolean;
351+
/** Specific row IDs to export */
352+
rowIds?: (string | number)[];
353+
}
354+
323355
// ============================================================================
324356
// Worker Communication Types
325357
// ============================================================================

src/hostBridge.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,16 @@ import { ConfigurationSection, ExtensionId, FullExtensionId, SidebarLeft, Sideba
1414
import { IsCursorIDE } from './helpers';
1515

1616
import type { DatabaseDocument, DocumentModification } from './databaseModel';
17-
import type { CellValue, RecordId, DialogConfig, DialogButton, CellUpdate, TableQueryOptions, TableCountOptions, QueryResultSet, SchemaSnapshot, ColumnMetadata, CellContentType } from './core/types';
17+
import type { CellValue, RecordId, DialogConfig, DialogButton, CellUpdate, TableQueryOptions, TableCountOptions, QueryResultSet, SchemaSnapshot, ColumnMetadata, CellContentType, ModificationEntry, DbParams, ExportOptions } from './core/types';
1818
import { generateMergePatch } from './core/json-utils';
1919
import { escapeIdentifier } from './core/sql-utils';
2020

21-
// Legacy DbParams type for backward compatibility with webview
22-
interface DbParams {
23-
filename?: string;
24-
table: string;
25-
name?: string;
26-
uri?: string;
27-
}
28-
2921
// Type for Uint8Array-like objects (transferable over postMessage)
3022
type Uint8ArrayLike = { buffer: ArrayBufferLike, byteOffset: number, byteLength: number };
3123

3224
// Column type information
3325
interface ColumnTypeInfo {
34-
[key: string]: any;
26+
[key: string]: unknown;
3527
}
3628

3729
// Toast service interface for showing dialogs
@@ -562,7 +554,7 @@ export class HostBridge implements ToastService {
562554
/**
563555
* Apply edits to the database.
564556
*/
565-
async applyEdits(edits: any, signal?: any) {
557+
async applyEdits(edits: ModificationEntry[], signal?: AbortSignal) {
566558
const { document } = this;
567559
if (!document.databaseOperations) {
568560
throw new Error("Database not initialized");
@@ -573,7 +565,7 @@ export class HostBridge implements ToastService {
573565
/**
574566
* Undo a database edit.
575567
*/
576-
async undo(edit: any) {
568+
async undo(edit: ModificationEntry) {
577569
const { document } = this;
578570
if (!document.databaseOperations) {
579571
throw new Error("Database not initialized");
@@ -584,7 +576,7 @@ export class HostBridge implements ToastService {
584576
/**
585577
* Redo a database edit.
586578
*/
587-
async redo(edit: any) {
579+
async redo(edit: ModificationEntry) {
588580
const { document } = this;
589581
if (!document.databaseOperations) {
590582
throw new Error("Database not initialized");
@@ -595,7 +587,7 @@ export class HostBridge implements ToastService {
595587
/**
596588
* Commit changes to the database.
597589
*/
598-
async commit(signal?: any) {
590+
async commit(signal?: AbortSignal) {
599591
const { document } = this;
600592
if (!document.databaseOperations) {
601593
throw new Error("Database not initialized");
@@ -606,7 +598,7 @@ export class HostBridge implements ToastService {
606598
/**
607599
* Rollback changes to the database.
608600
*/
609-
async rollback(edits: any, signal?: any) {
601+
async rollback(edits: ModificationEntry[], signal?: AbortSignal) {
610602
const { document } = this;
611603
if (!document.databaseOperations) {
612604
throw new Error("Database not initialized");
@@ -725,7 +717,7 @@ export class HostBridge implements ToastService {
725717
cellParts = [params.table, params.name || '-', '__create__.sql'];
726718
} else {
727719
// Determine file extension based on content type
728-
const extname = await determineCellExtension(colTypes, value, type);
720+
const extname = await determineCellExtension(value, type);
729721
const cellFilename = (colName || 'cell') + extname;
730722

731723
// Use simple path structure
@@ -833,7 +825,7 @@ export class HostBridge implements ToastService {
833825
* @param exportOptions - Export format options
834826
* @param extras - Additional options
835827
*/
836-
async exportTable(dbParams: DbParams, columns: string[], dbOptions?: any, tableStore?: any, exportOptions?: any, extras?: any) {
828+
async exportTable(dbParams: DbParams, columns: string[], dbOptions?: unknown, tableStore?: unknown, exportOptions?: ExportOptions, extras?: unknown) {
837829
// Inject the URI of the current document so the command knows which database to use
838830
const enrichedParams = {
839831
...dbParams,
@@ -986,7 +978,7 @@ export class HostBridge implements ToastService {
986978
* @param type - File type result
987979
* @returns File extension including the dot
988980
*/
989-
async function determineCellExtension(colTypes: ColumnTypeInfo, value?: CellValue, type?: CellContentType): Promise<string> {
981+
async function determineCellExtension(value?: CellValue, type?: CellContentType): Promise<string> {
990982
// Default to .txt for text, .bin for binary
991983
if (value instanceof Uint8Array || (value && typeof value === 'object' && 'buffer' in value)) {
992984
// Check if it's a known binary format

src/main.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,11 @@ import * as vsc from 'vscode';
33
import { TelemetryReporter } from '@vscode/extension-telemetry';
44
import { exportTableCommand } from './tableExporter';
55
import { ExtensionId, FullExtensionId, FileNestingPatternsAdded, FirstInstallMs, NestingPattern, SyncedKeys, TelemetryConnectionString, Title, UriScheme } from './config';
6+
import type { DbParams, ExportOptions } from './core/types';
67
import { disposeAll } from './lifecycle';
78
import { registerEditorProvider } from './editorController';
89
import { SQLiteFileSystemProvider } from './virtualFileSystem';
910

10-
export type DbParams = {
11-
filename: string,
12-
table: string,
13-
name: string,
14-
uri?: string,
15-
}
16-
1711
export let GlobalOutputChannel: vsc.OutputChannel|null = null;
1812

1913
/**
@@ -42,7 +36,7 @@ export async function activate(context: vsc.ExtensionContext) {
4236

4337
// Register export table command
4438
context.subscriptions.push(
45-
vsc.commands.registerCommand(`${ExtensionId}.exportTable`, (dbParams: DbParams, columns: string[], dbOptions?: any, tableStore?: any, exportOptions?: any, extras?: any) =>
39+
vsc.commands.registerCommand(`${ExtensionId}.exportTable`, (dbParams: DbParams, columns: string[], dbOptions?: unknown, tableStore?: unknown, exportOptions?: ExportOptions, extras?: unknown) =>
4640
exportTableCommand(context, reporter, dbParams, columns, dbOptions, tableStore, exportOptions, extras)),
4741
);
4842

src/tableExporter.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,11 @@
77

88
import * as vsc from 'vscode';
99
import type { TelemetryReporter } from '@vscode/extension-telemetry';
10-
import type { CellValue } from './core/types';
10+
import type { CellValue, DbParams, ExportOptions } from './core/types';
1111
import type { DatabaseDocument } from './databaseModel';
1212
import { DocumentRegistry } from './documentRegistry';
1313
import { escapeIdentifier, cellValueToSql } from './core/sql-utils';
1414

15-
// Legacy DbParams type for backward compatibility with webview
16-
interface DbParams {
17-
filename?: string;
18-
table: string;
19-
name?: string;
20-
uri?: string;
21-
}
22-
2315
/**
2416
* Export table data to CSV or JSON file.
2517
*
@@ -36,10 +28,10 @@ export async function exportTableCommand(
3628
reporter: TelemetryReporter | undefined,
3729
dbParams: DbParams,
3830
columns: string[],
39-
_dbOptions?: any,
40-
_tableStore?: any,
41-
_exportOptions?: { format?: string, header?: boolean, includeTableName?: boolean, rowIds?: (string | number)[] },
42-
_extras?: any
31+
_dbOptions?: unknown,
32+
_tableStore?: unknown,
33+
_exportOptions?: ExportOptions,
34+
_extras?: unknown
4335
) {
4436
try {
4537
const tableName = dbParams.table;
@@ -343,7 +335,7 @@ export async function exportTableCommand(
343335
* Convert data to CSV format.
344336
* Handles proper escaping of values containing commas, quotes, or newlines.
345337
*/
346-
function exportToCsv(columns: string[], rows: CellValue[][], includeHeader: boolean = true): string {
338+
export function exportToCsv(columns: string[], rows: CellValue[][], includeHeader: boolean = true): string {
347339
const escapeCsvValue = (value: CellValue): string => {
348340
if (value === null || value === undefined) return '';
349341
if (value instanceof Uint8Array) return '[BLOB]';
@@ -393,7 +385,7 @@ export function exportToJson(columns: string[], rows: CellValue[][]): string {
393385
* Convert data to SQL INSERT statements.
394386
* Generates INSERT statements that can be used to recreate the data.
395387
*/
396-
function exportToSql(tableName: string, columns: string[], rows: CellValue[][], includeTableName: boolean = true): string {
388+
export function exportToSql(tableName: string, columns: string[], rows: CellValue[][], includeTableName: boolean = true): string {
397389
// Use escapeIdentifier to prevent SQL injection via malicious column names
398390
const columnList = columns.map(c => escapeIdentifier(c)).join(', ');
399391
const targetTable = includeTableName ? escapeIdentifier(tableName) : 'table_name';

tests/unit/tableExporter.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import './vscode_mock_setup'; // Must be first
33
import { describe, it } from 'node:test';
44
import assert from 'node:assert';
5-
import { exportToJson } from '../../src/tableExporter';
5+
import { exportToJson, exportToCsv, exportToSql } from '../../src/tableExporter';
66
import { CellValue } from '../../src/core/types';
77

88
describe('exportToJson', () => {
@@ -57,3 +57,74 @@ describe('exportToJson', () => {
5757
assert.deepStrictEqual(parsed, [{}, {}]);
5858
});
5959
});
60+
61+
describe('exportToCsv', () => {
62+
it('should export with header by default', () => {
63+
const columns = ['id', 'name'];
64+
const rows: CellValue[][] = [[1, 'Alice'], [2, 'Bob']];
65+
66+
const csv = exportToCsv(columns, rows);
67+
assert.strictEqual(csv, 'id,name\n1,Alice\n2,Bob');
68+
});
69+
70+
it('should omit header when includeHeader is false', () => {
71+
const columns = ['id', 'name'];
72+
const rows: CellValue[][] = [[1, 'Alice']];
73+
74+
const csv = exportToCsv(columns, rows, false);
75+
assert.strictEqual(csv, '1,Alice');
76+
});
77+
78+
it('should escape values containing commas, quotes, and newlines', () => {
79+
const columns = ['text'];
80+
const rows: CellValue[][] = [
81+
['hello, world'],
82+
['say "hi"'],
83+
['line1\nline2']
84+
];
85+
86+
const csv = exportToCsv(columns, rows);
87+
const lines = csv.split('\n');
88+
assert.strictEqual(lines[0], 'text');
89+
assert.strictEqual(lines[1], '"hello, world"');
90+
assert.strictEqual(lines[2], '"say ""hi"""');
91+
});
92+
93+
it('should handle null and Uint8Array values', () => {
94+
const columns = ['val', 'blob'];
95+
const rows: CellValue[][] = [[null, new Uint8Array([1, 2, 3])]];
96+
97+
const csv = exportToCsv(columns, rows);
98+
assert.strictEqual(csv, 'val,blob\n,[BLOB]');
99+
});
100+
});
101+
102+
describe('exportToSql', () => {
103+
it('should generate INSERT statements with escaped identifiers', () => {
104+
const columns = ['id', 'name'];
105+
const rows: CellValue[][] = [[1, 'Alice']];
106+
107+
const sql = exportToSql('users', columns, rows);
108+
assert.ok(sql.includes('INSERT INTO'));
109+
assert.ok(sql.includes('"users"'));
110+
assert.ok(sql.includes('"id"'));
111+
assert.ok(sql.includes('"name"'));
112+
});
113+
114+
it('should use placeholder table name when includeTableName is false', () => {
115+
const columns = ['id'];
116+
const rows: CellValue[][] = [[1]];
117+
118+
const sql = exportToSql('users', columns, rows, false);
119+
assert.ok(sql.includes('table_name'));
120+
assert.ok(!sql.includes('"users"'));
121+
});
122+
123+
it('should handle empty rows', () => {
124+
const columns = ['id'];
125+
const rows: CellValue[][] = [];
126+
127+
const sql = exportToSql('t', columns, rows);
128+
assert.strictEqual(sql, '');
129+
});
130+
});

0 commit comments

Comments
 (0)