Skip to content

Commit 7d397eb

Browse files
committed
fix(sqlite): serialize BigInt and BLOBs safely in JSON/CSV export and cell inspector
1 parent 5b0802f commit 7d397eb

2 files changed

Lines changed: 66 additions & 6 deletions

File tree

‎src/tools/SqliteViewer.svelte‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,10 +1161,12 @@
11611161
>
11621162
{#if cell === null || cell === undefined}
11631163
<span class="text-outline/60 italic text-[11px]">NULL</span>
1164-
{:else if typeof cell === 'number'}
1164+
{:else if typeof cell === 'number' || typeof cell === 'bigint'}
11651165
<span class="text-emerald-600 dark:text-emerald-400">{cell}</span>
11661166
{:else if typeof cell === 'boolean'}
11671167
<span class="text-amber-600 dark:text-amber-400">{cell ? 'true' : 'false'}</span>
1168+
{:else if cell instanceof Uint8Array}
1169+
<span class="text-outline italic text-[11px]">&lt;BLOB {cell.length}B&gt;</span>
11681170
{:else}
11691171
<span class="text-on-surface">{cell}</span>
11701172
{/if}
@@ -1375,6 +1377,12 @@
13751377
>
13761378
{#if cell === null || cell === undefined}
13771379
<span class="text-outline/60 italic text-[11px]">NULL</span>
1380+
{:else if typeof cell === 'number' || typeof cell === 'bigint'}
1381+
<span class="text-emerald-600 dark:text-emerald-400">{cell}</span>
1382+
{:else if typeof cell === 'boolean'}
1383+
<span class="text-amber-600 dark:text-amber-400">{cell ? 'true' : 'false'}</span>
1384+
{:else if cell instanceof Uint8Array}
1385+
<span class="text-outline italic text-[11px]">&lt;BLOB {cell.length}B&gt;</span>
13781386
{:else}
13791387
<span>{cell}</span>
13801388
{/if}
@@ -1822,7 +1830,13 @@
18221830
</div>
18231831
<div class="flex items-center gap-2">
18241832
<button
1825-
onclick={() => copyText(String(cellModalContent?.val))}
1833+
onclick={() => {
1834+
if (cellModalContent?.val instanceof Uint8Array) {
1835+
copyText(Array.from(cellModalContent.val).map((b) => b.toString(16).padStart(2, '0')).join(''));
1836+
} else {
1837+
copyText(String(cellModalContent?.val ?? ''));
1838+
}
1839+
}}
18261840
class="flex items-center gap-1 px-2 py-0.5 rounded border border-outline-variant hover:bg-surface-container text-on-surface-variant cursor-pointer text-[11px]"
18271841
>
18281842
{#if copied}
@@ -1842,7 +1856,18 @@
18421856
</div>
18431857
</div>
18441858
<div class="p-4 max-h-[60vh] overflow-y-auto text-xs whitespace-pre-wrap select-text leading-relaxed">
1845-
{cellModalContent.val === null ? 'NULL' : cellModalContent.val}
1859+
{#if cellModalContent.val === null || cellModalContent.val === undefined}
1860+
<span class="text-outline/60 italic">NULL</span>
1861+
{:else if cellModalContent.val instanceof Uint8Array}
1862+
<div class="space-y-2">
1863+
<div class="text-[11px] text-outline font-semibold">BLOB ({cellModalContent.val.length} bytes) — Hex dump:</div>
1864+
<div class="p-2.5 bg-surface-container rounded border border-outline-variant break-all font-mono">
1865+
{Array.from(cellModalContent.val).map((b) => b.toString(16).padStart(2, '0')).join(' ')}
1866+
</div>
1867+
</div>
1868+
{:else}
1869+
{cellModalContent.val}
1870+
{/if}
18461871
</div>
18471872
</div>
18481873
</div>

‎src/utils/sqliteEngine.ts‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
22

3+
// Ensure BigInt values serialize properly in JSON.stringify without throwing TypeError
4+
if (typeof BigInt !== 'undefined' && !(BigInt.prototype as any).toJSON) {
5+
(BigInt.prototype as any).toJSON = function () {
6+
const n = Number(this);
7+
return Number.isSafeInteger(n) ? n : this.toString();
8+
};
9+
}
10+
311
export interface TableColumnInfo {
412
cid: number;
513
name: string;
@@ -720,8 +728,14 @@ export class SqliteEngine {
720728
static toCsv(columns: string[], rows: any[][]): string {
721729
const escapeCell = (val: any) => {
722730
if (val === null || val === undefined) return '';
731+
if (typeof val === 'bigint') return val.toString();
732+
if (val instanceof Uint8Array) {
733+
return Array.from(val)
734+
.map((b) => b.toString(16).padStart(2, '0'))
735+
.join('');
736+
}
723737
const str = String(val);
724-
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
738+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
725739
return `"${str.replace(/"/g, '""')}"`;
726740
}
727741
return str;
@@ -743,7 +757,25 @@ export class SqliteEngine {
743757
});
744758
return obj;
745759
});
746-
return JSON.stringify(objects, null, 2);
760+
return JSON.stringify(
761+
objects,
762+
(_key, value) => {
763+
if (typeof value === 'bigint') {
764+
// If within JS safe integer range, serialize as number so JSON parsers read it as number
765+
if (value <= BigInt(Number.MAX_SAFE_INTEGER) && value >= BigInt(Number.MIN_SAFE_INTEGER)) {
766+
return Number(value);
767+
}
768+
return value.toString();
769+
}
770+
if (value instanceof Uint8Array) {
771+
return Array.from(value)
772+
.map((b) => b.toString(16).padStart(2, '0'))
773+
.join('');
774+
}
775+
return value;
776+
},
777+
2
778+
);
747779
}
748780

749781
/**
@@ -756,8 +788,11 @@ export class SqliteEngine {
756788
const lines = rows.map((row) => {
757789
const vals = row.map((v) => {
758790
if (v === null || v === undefined) return 'NULL';
759-
if (typeof v === 'number') return v;
791+
if (typeof v === 'number' || typeof v === 'bigint') return v.toString();
760792
if (typeof v === 'boolean') return v ? 1 : 0;
793+
if (v instanceof Uint8Array) {
794+
return `X'${Array.from(v).map((b) => b.toString(16).padStart(2, '0')).join('')}'`;
795+
}
761796
return `'${String(v).replace(/'/g, "''")}'`;
762797
});
763798
return `INSERT INTO "${tableName}" (${colList}) VALUES (${vals.join(', ')});`;

0 commit comments

Comments
 (0)