Skip to content

Commit d784db0

Browse files
committed
Optimise query CSV export memory usage
1 parent 52c05b3 commit d784db0

File tree

2 files changed

+121
-7
lines changed

2 files changed

+121
-7
lines changed

source/ts/core/CsvFileWriter.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as csv_stringify from 'csv-stringify';
2+
import * as fs from 'fs';
3+
4+
//Wrap the needed filesystem methods in a Promise-based interface
5+
require('util.promisify/shim')();
6+
import { promisify } from 'util';
7+
const fsOpen = promisify(fs.open);
8+
const fsClose = promisify(fs.close);
9+
const fsWrite = promisify(fs.write);
10+
11+
export class CsvFileWriter
12+
{
13+
//private fileStream : fs.WriteStream;
14+
//private csvStringifier : csv_stringify.Stringifier;
15+
16+
//The file descriptor of our output file
17+
private outfile : number = -1;
18+
19+
public static async createWriter(csvFile : string)
20+
{
21+
try
22+
{
23+
let writer = new CsvFileWriter();
24+
await writer.open(csvFile);
25+
return writer;
26+
}
27+
catch (err)
28+
{
29+
//Propagate any errors
30+
throw err;
31+
}
32+
}
33+
34+
protected constructor() {}
35+
36+
public async open(csvFile : string)
37+
{
38+
try
39+
{
40+
//Attempt to open our output file
41+
this.outfile = await fsOpen(csvFile, 'w');
42+
}
43+
catch (err)
44+
{
45+
//Propagate any errors
46+
throw err;
47+
}
48+
}
49+
50+
public write(rows : any)
51+
{
52+
return new Promise((resolve : Function, reject : Function) =>
53+
{
54+
csv_stringify(rows, (err : Error, output : any) =>
55+
{
56+
if (err)
57+
{
58+
Error.captureStackTrace(err);
59+
reject(err);
60+
}
61+
else
62+
{
63+
fsWrite(this.outfile, output).then((result : any) => {
64+
resolve(true);
65+
})
66+
.catch((err : Error) => {
67+
reject(err);
68+
});
69+
}
70+
});
71+
});
72+
}
73+
74+
public async close()
75+
{
76+
try
77+
{
78+
if (this.outfile != -1)
79+
{
80+
await fsClose(this.outfile);
81+
this.outfile = -1;
82+
}
83+
}
84+
catch (err)
85+
{
86+
//Propagate any errors
87+
throw err;
88+
}
89+
}
90+
}

source/ts/core/DatabaseUtil.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CsvDataUtil } from './CsvDataUtil';
1+
import { CsvFileWriter } from './CsvFileWriter';
22
import { DataUtils } from './DataUtils';
33
import * as sqlite3 from 'sqlite3';
44

@@ -355,16 +355,40 @@ export class DatabaseUtil
355355
{
356356
try
357357
{
358-
//Unpack the params Map to a plain key-value pair object to pass to sqlite
358+
//Create our CSV file writer
359+
let writer = await CsvFileWriter.createWriter(csvFile);
360+
361+
//Unpack the params Map to a plain key-value pair object to pass to SQLite
359362
let paramsUnpacked : any = {};
360363
params.forEach((value : Object, key : string) => { paramsUnpacked[key] = value; });
361364

362-
//Perform the query
363-
let rows = await DatabaseUtil.all(db, query, paramsUnpacked);
364-
let data = DatabaseUtil.reshapeForCsv(rows);
365+
//Determine how many rows will be returned by the query
366+
//(Note that this will only function correctly for simple queries that only feature one FROM clause)
367+
let countQuery = query.replace(/SELECT .+ FROM/i, 'SELECT COUNT(*) AS total FROM');
368+
let totalRows = (await DatabaseUtil.get(db, countQuery, paramsUnpacked))['total'];
369+
370+
//Strip any trailing semicolon from the query string so we can append our offset and limit clauses
371+
query = query.trim();
372+
query = (query.endsWith(';') ? query.substr(0, query.length-1) : query);
373+
374+
//Process the data in batches
375+
const batchSize = 50000;
376+
for (let offset = 0; offset < totalRows; offset += batchSize)
377+
{
378+
//Retrieve the results for the current batch
379+
let batch = await DatabaseUtil.all(db, query + ` LIMIT ${batchSize} OFFSET ${offset};`, paramsUnpacked);
380+
381+
//Only include the header row in the first batch
382+
let data = DatabaseUtil.reshapeForCsv(batch);
383+
if (offset > 0) {
384+
data = data.slice(1);
385+
}
386+
387+
//Write the current batch of rows to the CSV file
388+
await writer.write(data);
389+
}
365390

366-
//Write the data to the CSV file
367-
await CsvDataUtil.writeCsv(csvFile, data);
391+
await writer.close();
368392
return true;
369393
}
370394
catch (err)

0 commit comments

Comments
 (0)