Skip to content

Commit

Permalink
Merge pull request #7 from shinshin86/feat/csv-export-import
Browse files Browse the repository at this point in the history
Add csv export and import
  • Loading branch information
shinshin86 authored Oct 7, 2024
2 parents b8f301b + faa5eb2 commit 1f45b42
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,44 @@ const blobData = convertToUint8Array(row.blobColumn);
- **Complex Data Types**: While NeverChangeDB handles most SQLite data types seamlessly, complex types like JSON or custom data structures may require additional processing when dumping or importing.
- **Cross-Browser Compatibility**: Although the core functionality is designed to work across modern browsers, some advanced features or performance optimizations may vary between different browser environments. Always test thoroughly in your target browsers.

#### CSV Export and Import

NeverChangeDB also supports CSV export and import functionality, allowing you to easily work with CSV files in your database.

##### CSV Export

You can export a table to a CSV format using the `dumpTableToCSV` method:

```typescript
const db = new NeverChangeDB('myDatabase');
await db.init();

/* We will assume that you have added tables and information */

const csvContent = await db.dumpTableToCSV('your_table');
console.log('CSV Export:', csvContent);

await db.close();
```

This will export the contents of `your_table` to a CSV string.

##### CSV Import

You can import CSV content into a table using the `importCSVToTable` method:

```typescript
const db = new NeverChangeDB('myDatabase');
await db.init();

const csvContent = `id,name,email\n1,John Doe,john@example.com\n2,Jane Smith,jane@example.com`;
await db.importCSVToTable('your_table', csvContent);

await db.close();
```

This will insert the CSV data into the `your_table` table. Ensure the table is created beforehand and the columns match the CSV headers.

## For Developers

### Setup
Expand Down
208 changes: 208 additions & 0 deletions e2e/csv-export-and-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { test, expect } from "@playwright/test";

// sleep function for debugging
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

test.describe("NeverChangeDB CSV Export and Import", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await sleep(100);
});

test("should export table to CSV correctly", async ({ page }) => {
// Initialize the database
const csvContent = await page.evaluate(async () => {
const db = new (window as any).NeverChangeDB("csv-export-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
)
`);
await db.execute("INSERT INTO test_table (name, email) VALUES (?, ?)", [
"John Doe",
"john@example.com",
]);
await db.execute("INSERT INTO test_table (name, email) VALUES (?, ?)", [
"Jane Smith",
"jane@example.com",
]);

// Export table to CSV
return await db.dumpTableToCSV("test_table");
});

expect(csvContent).toContain("id,name,email");
expect(csvContent).toContain("John Doe,john@example.com");
expect(csvContent).toContain("Jane Smith,jane@example.com");
});

test("should import CSV content into table correctly", async ({ page }) => {
const csvContent =
"id,name,email\n1,John Doe,john@example.com\n2,Jane Smith,jane@example.com";

// Import CSV into a new database
const importedData = await page.evaluate(async (csvContent) => {
const db = new (window as any).NeverChangeDB("csv-import-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
)
`);

// Import the CSV content into the table
await db.importCSVToTable("test_table", csvContent);

return await db.query("SELECT * FROM test_table");
}, csvContent);

expect(importedData).toHaveLength(2);
expect(importedData[0].name).toBe("John Doe");
expect(importedData[0].email).toBe("john@example.com");
expect(importedData[1].name).toBe("Jane Smith");
expect(importedData[1].email).toBe("jane@example.com");
});

test("should handle empty CSV export and import", async ({ page }) => {
const csvContent = await page.evaluate(async () => {
const db = new (window as any).NeverChangeDB("empty-csv-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
)
`);

// Export empty table to CSV
return await db.dumpTableToCSV("test_table");
});

expect(csvContent).toContain("id,name,email");
expect(csvContent.split("\r\n").filter(Boolean)).toHaveLength(1); // Only headers

// Import empty CSV content back into a new table
const importedData = await page.evaluate(async (csvContent) => {
const db = new (window as any).NeverChangeDB("empty-csv-import-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
)
`);

await db.importCSVToTable("test_table", csvContent);

return await db.query("SELECT * FROM test_table");
}, csvContent);

expect(importedData).toHaveLength(0);
});

test("should handle CSV with special characters (default behavior)", async ({
page,
}) => {
const csvContent = await page.evaluate(async () => {
const db = new (window as any).NeverChangeDB("csv-special-chars-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT
)
`);
await db.execute(
"INSERT INTO test_table (name, description) VALUES (?, ?)",
["John Doe", 'This is a test with, commas and "quotes".'],
);

return await db.dumpTableToCSV("test_table");
});

expect(csvContent).toBe(
`id,name,description\r\n1,John Doe,"This is a test with, commas and ""quotes""."\r\n`,
);

// Import the CSV content back into the table and validate
const importedData = await page.evaluate(async (csvContent) => {
const db = new (window as any).NeverChangeDB("csv-special-import-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT
)
`);

await db.importCSVToTable("test_table", csvContent);

return await db.query("SELECT * FROM test_table");
}, csvContent);

expect(importedData).toHaveLength(1);
expect(importedData[0].name).toBe("John Doe");
expect(importedData[0].description).toBe(
'This is a test with, commas and "quotes".',
);
});

test("should handle CSV with special characters (quoteAllFields option)", async ({
page,
}) => {
const csvContent = await page.evaluate(async () => {
const db = new (window as any).NeverChangeDB("csv-quote-all-fields-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT
)
`);
await db.execute(
"INSERT INTO test_table (name, description) VALUES (?, ?)",
["John Doe", 'This is a test with, commas and "quotes".'],
);

// Export table to CSV with quoteAllFields option
return await db.dumpTableToCSV("test_table", { quoteAllFields: true });
});

expect(csvContent).toBe(
`"id","name","description"\r\n"1","John Doe","This is a test with, commas and ""quotes""."\r\n`,
);

// Import the CSV content back into the table and validate
const importedData = await page.evaluate(async (csvContent) => {
const db = new (window as any).NeverChangeDB("csv-special-import-db");
await db.init();
await db.execute(`
CREATE TABLE test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
description TEXT
)
`);

await db.importCSVToTable("test_table", csvContent);

return await db.query("SELECT * FROM test_table");
}, csvContent);

expect(importedData).toHaveLength(1);
expect(importedData[0].name).toBe("John Doe");
expect(importedData[0].description).toBe(
'This is a test with, commas and "quotes".',
);
});
});
60 changes: 60 additions & 0 deletions src/neverchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Migration,
} from "./types";
import { initialMigration } from "./migrations";
import { parseCSVLine } from "./parser";

export class NeverChangeDB implements INeverChangeDB {
private dbPromise: Promise<
Expand Down Expand Up @@ -315,4 +316,63 @@ export class NeverChangeDB implements INeverChangeDB {
throw error;
}
}

async dumpTableToCSV(
tableName: string,
options: { quoteAllFields?: boolean } = {},
): Promise<string> {
const rows = await this.query(`SELECT * FROM ${tableName}`);

const columnNames = Object.keys(
rows[0] ||
(await this.query(`PRAGMA table_info(${tableName})`)).reduce(
(acc: any, col: any) => ({ ...acc, [col.name]: "" }),
{},
),
)
.map((col) => (options.quoteAllFields ? `"${col}"` : col))
.join(",");

if (rows.length === 0) {
return `${columnNames}\r\n`;
}

const escapeCSVField = (field: any): string => {
const strValue = field?.toString() || "";
const needsQuoting =
options.quoteAllFields ||
strValue.includes(",") ||
strValue.includes("\n") ||
strValue.includes('"');

if (needsQuoting) {
return `"${strValue.replace(/"/g, '""')}"`;
}

return strValue;
};

const csvRows = rows.map((row) =>
Object.values(row).map(escapeCSVField).join(","),
);

return `${columnNames}\r\n${csvRows.join("\r\n")}\r\n`;
}

async importCSVToTable(tableName: string, csvContent: string): Promise<void> {
const [headerLine, ...dataLines] = csvContent
.split(/\r?\n/)
.filter(Boolean);
const columns = headerLine.split(",");

for (const line of dataLines) {
const values = parseCSVLine(line);
const placeholders = columns.map(() => "?").join(",");

await this.execute(
`INSERT INTO ${tableName} (${columns.join(",")}) VALUES (${placeholders})`,
values,
);
}
}
}
30 changes: 30 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const parseCSVLine = (line: string): string[] => {
const result: string[] = [];
let currentField = "";
let insideQuotes = false;

for (let i = 0; i < line.length; i++) {
const char = line[i];

if (char === '"') {
if (insideQuotes && line[i + 1] === '"') {
currentField += '"';
i++; // Skip the next quote
} else {
// Toggle the start or end of a quoted field
insideQuotes = !insideQuotes;
}
} else if (char === "," && !insideQuotes) {
// Treat comma as a field separator
result.push(currentField);
currentField = "";
} else {
// Add the character as part of the current field
currentField += char;
}
}

// Add the last field to the result
result.push(currentField);
return result;
};

0 comments on commit 1f45b42

Please sign in to comment.