csv-sql-engine 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/csv/csv-file.js +10 -8
- package/dist/engine/define-ast-handler.d.ts +2 -2
- package/dist/engine/engine.d.ts +2 -2
- package/dist/engine/engine.js +14 -10
- package/dist/engine/handlers/row-delete.handler.js +50 -43
- package/dist/engine/handlers/row-insert.handler.js +107 -46
- package/dist/engine/handlers/row-select.handler.js +46 -33
- package/dist/engine/handlers/row-update.handler.js +64 -53
- package/dist/engine/handlers/table-alter.handler.js +89 -70
- package/dist/engine/handlers/table-create.handler.js +37 -23
- package/dist/engine/handlers/table-drop.handler.js +29 -21
- package/dist/engine/params.d.ts +5 -18
- package/dist/errors/sql.error.d.ts +9 -2
- package/dist/errors/sql.error.js +15 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +3 -5
- package/dist/util/ast-node.d.ts +10 -0
- package/dist/util/ast-node.js +23 -0
- package/dist/{engine → util}/sort-values.d.ts +1 -1
- package/dist/util/where-matcher.d.ts +10 -0
- package/dist/util/where-matcher.js +66 -0
- package/package.json +7 -7
- package/dist/engine/where-matcher.d.ts +0 -9
- package/dist/engine/where-matcher.js +0 -52
- package/dist/sql/ast.d.ts +0 -472
- package/dist/sql/ast.js +0 -266
- package/dist/sql/parse-sql.d.ts +0 -9
- package/dist/sql/parse-sql.js +0 -38
- package/dist/sql/sql.d.ts +0 -33
- package/dist/sql/sql.js +0 -39
- /package/dist/{engine → util}/sort-values.js +0 -0
package/dist/csv/csv-file.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { assertWrap, check } from '@augment-vir/assert';
|
|
1
2
|
import { addSuffix, awaitedForEach, removeSuffix } from '@augment-vir/common';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { CsvFileMissingHeadersError } from '../errors/csv.error.js';
|
|
@@ -26,6 +26,7 @@ export async function appendCsvRows(valueMatrix, csvFilePath) {
|
|
|
26
26
|
* @category CSV
|
|
27
27
|
*/
|
|
28
28
|
export async function appendCsvRow(row, csvFilePath) {
|
|
29
|
+
await mkdir(dirname(csvFilePath), { recursive: true });
|
|
29
30
|
await appendFile(csvFilePath, convertRowToCsv(row) + '\n');
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
@@ -34,10 +35,15 @@ export async function appendCsvRow(row, csvFilePath) {
|
|
|
34
35
|
* @category CSV
|
|
35
36
|
*/
|
|
36
37
|
export function nameCsvTableFile({ csvDirPath, tableName, }) {
|
|
37
|
-
const
|
|
38
|
+
const tableNameSplit = tableName.split('.').filter(check.isTruthy);
|
|
39
|
+
const databaseName = assertWrap.isTruthy((tableNameSplit.length > 1 ? tableNameSplit[0] || '' : 'main').replaceAll(/[./\\]/g, ''), 'No database name found.');
|
|
40
|
+
const sanitizedTableName = assertWrap.isTruthy(removeSuffix({
|
|
41
|
+
value: tableNameSplit.length > 1 ? tableNameSplit[1] || '' : tableName,
|
|
42
|
+
suffix: csvExtension,
|
|
43
|
+
}).replaceAll(/[./\\]/g, ''), 'No table name found.');
|
|
38
44
|
const newCsvFileName = addSuffix({ value: sanitizedTableName, suffix: csvExtension });
|
|
39
45
|
return {
|
|
40
|
-
tableFilePath: join(csvDirPath, newCsvFileName),
|
|
46
|
+
tableFilePath: join(csvDirPath, databaseName, newCsvFileName),
|
|
41
47
|
sanitizedTableName,
|
|
42
48
|
};
|
|
43
49
|
}
|
|
@@ -57,12 +63,8 @@ export async function readCsvFile(filePath) {
|
|
|
57
63
|
* @category CSV
|
|
58
64
|
*/
|
|
59
65
|
export async function writeCsvFile(filePath, contents) {
|
|
60
|
-
if (!existsSync(filePath)) {
|
|
61
|
-
await mkdir(dirname(filePath), {
|
|
62
|
-
recursive: true,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
66
|
const fileContents = convertRowsToCsv(contents);
|
|
67
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
66
68
|
await writeFile(filePath, fileContents);
|
|
67
69
|
}
|
|
68
70
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type MaybePromise } from '@augment-vir/common';
|
|
2
|
+
import { type SortValuesOutput } from '../util/sort-values.js';
|
|
2
3
|
import { type AstHandlerParams } from './params.js';
|
|
3
|
-
import { type SortValuesOutput } from './sort-values.js';
|
|
4
4
|
/**
|
|
5
5
|
* Output from a handler that handled a SQL query.
|
|
6
6
|
*
|
|
@@ -20,7 +20,7 @@ export type AstHandler = {
|
|
|
20
20
|
* Return `undefined` to mark this AST as not-handled. That means that other handlers should be
|
|
21
21
|
* used instead.
|
|
22
22
|
*/
|
|
23
|
-
handler: (params: Readonly<AstHandlerParams>) => MaybePromise<AstHandlerResult
|
|
23
|
+
handler: (params: Readonly<AstHandlerParams>) => MaybePromise<AstHandlerResult | undefined>;
|
|
24
24
|
};
|
|
25
25
|
/**
|
|
26
26
|
* Used to define new handlers.
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Sql } from '
|
|
1
|
+
import { type Sql } from 'sqlite-ast';
|
|
2
2
|
import { type AstHandler, type AstHandlerResult } from './define-ast-handler.js';
|
|
3
3
|
import { type ExecuteSqlParams } from './params.js';
|
|
4
4
|
/**
|
|
@@ -13,4 +13,4 @@ export declare const allAstHandlers: ReadonlyArray<Readonly<AstHandler>>;
|
|
|
13
13
|
*
|
|
14
14
|
* @category Main
|
|
15
15
|
*/
|
|
16
|
-
export declare function executeSql(sqlInput: Sql | string, params: Readonly<ExecuteSqlParams>): Promise<AstHandlerResult[]
|
|
16
|
+
export declare function executeSql(sqlInput: Sql | string, params: Readonly<ExecuteSqlParams>): Promise<AstHandlerResult[]>;
|
package/dist/engine/engine.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
|
-
import { awaitedBlockingMap, ensureErrorAndPrependMessage } from '@augment-vir/common';
|
|
2
|
+
import { awaitedBlockingMap, ensureErrorAndPrependMessage, wrapInTry, } from '@augment-vir/common';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { rawSql } from '../sql/sql.js';
|
|
4
|
+
import { parseSqlite, rawSql } from 'sqlite-ast';
|
|
5
|
+
import { SqlParseError, SqlUnsupportedOperationError } from '../errors/sql.error.js';
|
|
7
6
|
import { rowDeleteHandler } from './handlers/row-delete.handler.js';
|
|
8
7
|
import { rowInsertHandler } from './handlers/row-insert.handler.js';
|
|
9
8
|
import { rowSelectHandler } from './handlers/row-select.handler.js';
|
|
@@ -34,17 +33,21 @@ export const allAstHandlers = [
|
|
|
34
33
|
export async function executeSql(sqlInput, params) {
|
|
35
34
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
36
35
|
const sql = check.isString(sqlInput) ? rawSql(sqlInput) : sqlInput;
|
|
37
|
-
const
|
|
36
|
+
const astList = wrapInTry(() => parseSqlite(sql), {
|
|
37
|
+
handleError(error) {
|
|
38
|
+
throw new SqlParseError(sql, error);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
38
41
|
await mkdir(params.csvDirPath, {
|
|
39
42
|
recursive: true,
|
|
40
43
|
});
|
|
41
|
-
return await awaitedBlockingMap(
|
|
44
|
+
return (await awaitedBlockingMap(astList, async (ast) => {
|
|
42
45
|
return await executeIndividualCommand({
|
|
43
46
|
...params,
|
|
44
47
|
ast,
|
|
45
48
|
sql,
|
|
46
49
|
});
|
|
47
|
-
});
|
|
50
|
+
})).filter(check.isTruthy);
|
|
48
51
|
}
|
|
49
52
|
async function executeIndividualCommand(params) {
|
|
50
53
|
try {
|
|
@@ -56,11 +59,12 @@ async function executeIndividualCommand(params) {
|
|
|
56
59
|
}
|
|
57
60
|
/** If nothing handled the query, then we don't support it. */
|
|
58
61
|
if (params.rejectUnsupportedOperations) {
|
|
59
|
-
throw new SqlUnsupportedOperationError(params.sql, undefined);
|
|
62
|
+
throw new SqlUnsupportedOperationError(params.sql, undefined, params.ast);
|
|
60
63
|
}
|
|
61
|
-
return
|
|
64
|
+
return undefined;
|
|
62
65
|
}
|
|
63
66
|
catch (error) {
|
|
64
|
-
|
|
67
|
+
const errorAst = params.ast;
|
|
68
|
+
throw ensureErrorAndPrependMessage(error, `Failed to execute '${errorAst.variant || errorAst.type}' command.`);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { check } from '@augment-vir/assert';
|
|
2
|
-
import { awaitedBlockingMap } from '@augment-vir/common';
|
|
3
1
|
import { nameCsvTableFile, readCsvFile, readCsvHeaders, writeCsvFile } from '../../csv/csv-file.js';
|
|
4
|
-
import {
|
|
2
|
+
import { getAstType } from '../../util/ast-node.js';
|
|
3
|
+
import { sortValues } from '../../util/sort-values.js';
|
|
4
|
+
import { findWhereMatches } from '../../util/where-matcher.js';
|
|
5
5
|
import { defineAstHandler } from '../define-ast-handler.js';
|
|
6
|
-
import { sortValues } from '../sort-values.js';
|
|
7
|
-
import { findWhereMatches } from '../where-matcher.js';
|
|
8
6
|
/**
|
|
9
7
|
* Handles deleting rows.
|
|
10
8
|
*
|
|
@@ -13,44 +11,53 @@ import { findWhereMatches } from '../where-matcher.js';
|
|
|
13
11
|
export const rowDeleteHandler = defineAstHandler({
|
|
14
12
|
name: 'row-delete',
|
|
15
13
|
async handler({ ast, csvDirPath, sql }) {
|
|
16
|
-
if (ast.
|
|
17
|
-
|
|
18
|
-
const results = await awaitedBlockingMap(tableNames, async (tableName) => {
|
|
19
|
-
const { tableFilePath, sanitizedTableName } = nameCsvTableFile({
|
|
20
|
-
csvDirPath,
|
|
21
|
-
tableName,
|
|
22
|
-
});
|
|
23
|
-
const csvContents = await readCsvFile(tableFilePath);
|
|
24
|
-
const csvHeaders = await readCsvHeaders({
|
|
25
|
-
csvContents,
|
|
26
|
-
sanitizedTableName,
|
|
27
|
-
});
|
|
28
|
-
const rowIndexesToDelete = findWhereMatches(ast.where, csvContents, tableFilePath);
|
|
29
|
-
const returningRequirement = ast.returning;
|
|
30
|
-
const result = returningRequirement
|
|
31
|
-
? sortValues({
|
|
32
|
-
csvFileHeaderOrder: csvHeaders,
|
|
33
|
-
sqlQueryHeaderOrder: returningRequirement.columns.map((column) => column.expr.column),
|
|
34
|
-
from: {
|
|
35
|
-
csvFile: csvContents.filter((row, index) => rowIndexesToDelete.includes(index)),
|
|
36
|
-
},
|
|
37
|
-
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
38
|
-
})
|
|
39
|
-
: {
|
|
40
|
-
columnNames: [],
|
|
41
|
-
values: [],
|
|
42
|
-
};
|
|
43
|
-
rowIndexesToDelete.forEach((rowIndexToDelete) => {
|
|
44
|
-
csvContents.splice(rowIndexToDelete, 1);
|
|
45
|
-
});
|
|
46
|
-
await writeCsvFile(tableFilePath, csvContents);
|
|
47
|
-
return {
|
|
48
|
-
...result,
|
|
49
|
-
numberOfRowsAffected: rowIndexesToDelete.length,
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
return results.flat().filter(check.isTruthy);
|
|
14
|
+
if (ast.variant !== 'delete') {
|
|
15
|
+
return undefined;
|
|
53
16
|
}
|
|
54
|
-
|
|
17
|
+
const tableName = getAstType(ast.from, 'identifier')?.name;
|
|
18
|
+
if (!tableName) {
|
|
19
|
+
throw new Error('Missing table name.');
|
|
20
|
+
}
|
|
21
|
+
const { tableFilePath, sanitizedTableName } = nameCsvTableFile({
|
|
22
|
+
csvDirPath,
|
|
23
|
+
tableName,
|
|
24
|
+
});
|
|
25
|
+
const csvContents = await readCsvFile(tableFilePath);
|
|
26
|
+
const csvHeaders = await readCsvHeaders({
|
|
27
|
+
csvContents,
|
|
28
|
+
sanitizedTableName,
|
|
29
|
+
});
|
|
30
|
+
const rowIndexesToDelete = findWhereMatches(ast.where, csvContents, tableFilePath);
|
|
31
|
+
const returningRequirement = ast.returning;
|
|
32
|
+
const sqlHeaders = returningRequirement?.map((column) => {
|
|
33
|
+
const columnNode = getAstType(column, 'identifier');
|
|
34
|
+
if (columnNode) {
|
|
35
|
+
return columnNode.name;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new Error(`Unexpected return column type: ${column.type}`);
|
|
39
|
+
}
|
|
40
|
+
}) || [];
|
|
41
|
+
const result = returningRequirement
|
|
42
|
+
? sortValues({
|
|
43
|
+
csvFileHeaderOrder: csvHeaders,
|
|
44
|
+
sqlQueryHeaderOrder: sqlHeaders,
|
|
45
|
+
from: {
|
|
46
|
+
csvFile: csvContents.filter((row, index) => rowIndexesToDelete.includes(index)),
|
|
47
|
+
},
|
|
48
|
+
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
49
|
+
})
|
|
50
|
+
: {
|
|
51
|
+
columnNames: [],
|
|
52
|
+
values: [],
|
|
53
|
+
};
|
|
54
|
+
rowIndexesToDelete.forEach((rowIndexToDelete) => {
|
|
55
|
+
csvContents.splice(rowIndexToDelete, 1);
|
|
56
|
+
});
|
|
57
|
+
await writeCsvFile(tableFilePath, csvContents);
|
|
58
|
+
return {
|
|
59
|
+
...result,
|
|
60
|
+
numberOfRowsAffected: rowIndexesToDelete.length,
|
|
61
|
+
};
|
|
55
62
|
},
|
|
56
63
|
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { filterMap } from '@augment-vir/common';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { appendCsvRow, nameCsvTableFile, readCsvFile, readCsvHeaders } from '../../csv/csv-file.js';
|
|
5
|
+
import { CsvTableDoesNotExistError } from '../../errors/csv.error.js';
|
|
6
|
+
import { SqlUnsupportedOperationError } from '../../errors/sql.error.js';
|
|
7
|
+
import { sortValues } from '../../util/sort-values.js';
|
|
5
8
|
import { defineAstHandler } from '../define-ast-handler.js';
|
|
6
|
-
import { sortValues } from '../sort-values.js';
|
|
7
9
|
/**
|
|
8
10
|
* Handles inserting rows.
|
|
9
11
|
*
|
|
@@ -11,48 +13,107 @@ import { sortValues } from '../sort-values.js';
|
|
|
11
13
|
*/
|
|
12
14
|
export const rowInsertHandler = defineAstHandler({
|
|
13
15
|
name: 'row-insert',
|
|
14
|
-
async handler({ ast, csvDirPath, sql }) {
|
|
15
|
-
if (ast.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
csvDirPath,
|
|
20
|
-
tableName,
|
|
21
|
-
});
|
|
22
|
-
const rawValues = ast.values.values.flatMap((value) => value.value.flatMap((value) => String(value.value)));
|
|
23
|
-
const csvFileHeaderOrder = await readCsvHeaders({
|
|
24
|
-
csvFilePath: tableFilePath,
|
|
25
|
-
sanitizedTableName,
|
|
26
|
-
});
|
|
27
|
-
const newRow = assertWrap.isDefined(sortValues({
|
|
28
|
-
csvFileHeaderOrder,
|
|
29
|
-
sqlQueryHeaderOrder: ast.columns || csvFileHeaderOrder,
|
|
30
|
-
from: {
|
|
31
|
-
sqlQuery: [rawValues],
|
|
32
|
-
},
|
|
33
|
-
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
34
|
-
}).values[0], 'No sorted row retrieved.');
|
|
35
|
-
await appendCsvRow(newRow, tableFilePath);
|
|
36
|
-
const readResult = ast.returning
|
|
37
|
-
? sortValues({
|
|
38
|
-
csvFileHeaderOrder,
|
|
39
|
-
sqlQueryHeaderOrder: ast.returning.columns.map((column) => column.expr.column),
|
|
40
|
-
from: {
|
|
41
|
-
csvFile: [newRow],
|
|
42
|
-
},
|
|
43
|
-
unconsumedInterpolationValues: undefined,
|
|
44
|
-
})
|
|
45
|
-
: {
|
|
46
|
-
columnNames: [],
|
|
47
|
-
values: [],
|
|
48
|
-
};
|
|
49
|
-
return {
|
|
50
|
-
...readResult,
|
|
51
|
-
numberOfRowsAffected: 1,
|
|
52
|
-
};
|
|
53
|
-
});
|
|
54
|
-
return results.filter(check.isTruthy);
|
|
16
|
+
async handler({ ast, csvDirPath, sql, rejectUnsupportedOperations }) {
|
|
17
|
+
if (ast.variant !== 'insert' ||
|
|
18
|
+
ast.into.type !== 'identifier' ||
|
|
19
|
+
(ast.into.variant !== 'table' && ast.into.format !== 'table')) {
|
|
20
|
+
return;
|
|
55
21
|
}
|
|
56
|
-
|
|
22
|
+
const tableName = ast.into.name;
|
|
23
|
+
const { tableFilePath, sanitizedTableName } = nameCsvTableFile({
|
|
24
|
+
csvDirPath,
|
|
25
|
+
tableName,
|
|
26
|
+
});
|
|
27
|
+
if (!existsSync(tableFilePath)) {
|
|
28
|
+
throw new CsvTableDoesNotExistError(sanitizedTableName);
|
|
29
|
+
}
|
|
30
|
+
/** Mutate this to apply the table alterations. */
|
|
31
|
+
const csvContents = await readCsvFile(tableFilePath);
|
|
32
|
+
const csvHeaders = await readCsvHeaders({
|
|
33
|
+
csvContents,
|
|
34
|
+
sanitizedTableName,
|
|
35
|
+
});
|
|
36
|
+
const sqlHeaders = ast.into.columns
|
|
37
|
+
? filterMap(ast.into.columns, (column) => {
|
|
38
|
+
if (column.type === 'identifier' && column.variant === 'column') {
|
|
39
|
+
return column.name;
|
|
40
|
+
}
|
|
41
|
+
else if (rejectUnsupportedOperations) {
|
|
42
|
+
throw new SqlUnsupportedOperationError(sql, `Unsupported column definition: '${JSON.stringify(column)}'`, ast);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}, check.isTruthy)
|
|
48
|
+
: csvHeaders;
|
|
49
|
+
if (!check.isArray(ast.result)) {
|
|
50
|
+
if (rejectUnsupportedOperations) {
|
|
51
|
+
throw new SqlUnsupportedOperationError(sql, `Unsupported result: '${JSON.stringify(ast.result)}'`, ast);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const resultExpression = assertWrap.isTruthy(ast.result[0], 'No result.');
|
|
58
|
+
if (resultExpression.type !== 'expression' ||
|
|
59
|
+
resultExpression.variant !== 'list' ||
|
|
60
|
+
!check.isArray(resultExpression.expression)) {
|
|
61
|
+
if (rejectUnsupportedOperations) {
|
|
62
|
+
throw new SqlUnsupportedOperationError(sql, `Expected expression: ${JSON.stringify(resultExpression)}`, ast);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const rawValues = filterMap(resultExpression.expression, (entry) => {
|
|
69
|
+
if (entry.type === 'identifier' || entry.type === 'variable') {
|
|
70
|
+
return entry.name;
|
|
71
|
+
}
|
|
72
|
+
else if (entry.type === 'literal') {
|
|
73
|
+
return entry.value;
|
|
74
|
+
}
|
|
75
|
+
else if (rejectUnsupportedOperations) {
|
|
76
|
+
throw new SqlUnsupportedOperationError(sql, `Unsupported expression entry: ${JSON.stringify(entry)}`, ast);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}, check.isTruthy);
|
|
82
|
+
const newRow = assertWrap.isTruthy(sortValues({
|
|
83
|
+
csvFileHeaderOrder: csvHeaders,
|
|
84
|
+
sqlQueryHeaderOrder: sqlHeaders,
|
|
85
|
+
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
86
|
+
from: {
|
|
87
|
+
sqlQuery: [rawValues],
|
|
88
|
+
},
|
|
89
|
+
}).values[0], 'No sorted row retrieved.');
|
|
90
|
+
await appendCsvRow(newRow, tableFilePath);
|
|
91
|
+
const readResult = ast.returning
|
|
92
|
+
? sortValues({
|
|
93
|
+
csvFileHeaderOrder: csvHeaders,
|
|
94
|
+
sqlQueryHeaderOrder: filterMap(ast.returning, (entry) => {
|
|
95
|
+
if (entry.type === 'identifier') {
|
|
96
|
+
return entry.name;
|
|
97
|
+
}
|
|
98
|
+
else if (rejectUnsupportedOperations) {
|
|
99
|
+
throw new Error(`Unsupported returning entry: ${JSON.stringify(entry)}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}, check.isTruthy),
|
|
105
|
+
from: {
|
|
106
|
+
csvFile: [newRow],
|
|
107
|
+
},
|
|
108
|
+
unconsumedInterpolationValues: undefined,
|
|
109
|
+
})
|
|
110
|
+
: {
|
|
111
|
+
columnNames: [],
|
|
112
|
+
values: [],
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
...readResult,
|
|
116
|
+
numberOfRowsAffected: 1,
|
|
117
|
+
};
|
|
57
118
|
},
|
|
58
119
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { check } from '@augment-vir/assert';
|
|
2
|
-
import {
|
|
2
|
+
import { filterMap } from '@augment-vir/common';
|
|
3
3
|
import { nameCsvTableFile, readCsvFile, readCsvHeaders } from '../../csv/csv-file.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SqlUnsupportedOperationError } from '../../errors/sql.error.js';
|
|
5
|
+
import { getAstType } from '../../util/ast-node.js';
|
|
6
|
+
import { sortValues } from '../../util/sort-values.js';
|
|
7
|
+
import { findWhereMatches } from '../../util/where-matcher.js';
|
|
5
8
|
import { defineAstHandler } from '../define-ast-handler.js';
|
|
6
|
-
import { sortValues } from '../sort-values.js';
|
|
7
|
-
import { findWhereMatches } from '../where-matcher.js';
|
|
8
9
|
/**
|
|
9
10
|
* Handles SQL selection.
|
|
10
11
|
*
|
|
@@ -12,35 +13,47 @@ import { findWhereMatches } from '../where-matcher.js';
|
|
|
12
13
|
*/
|
|
13
14
|
export const rowSelectHandler = defineAstHandler({
|
|
14
15
|
name: 'row-select',
|
|
15
|
-
async handler({ ast, csvDirPath, sql }) {
|
|
16
|
-
if (ast.
|
|
17
|
-
|
|
18
|
-
const allSelections = await awaitedBlockingMap(tableNames, async (tableName) => {
|
|
19
|
-
const { tableFilePath, sanitizedTableName } = nameCsvTableFile({
|
|
20
|
-
csvDirPath,
|
|
21
|
-
tableName,
|
|
22
|
-
});
|
|
23
|
-
const csvContents = await readCsvFile(tableFilePath);
|
|
24
|
-
const csvHeaders = await readCsvHeaders({
|
|
25
|
-
csvContents,
|
|
26
|
-
sanitizedTableName,
|
|
27
|
-
});
|
|
28
|
-
const rowIndexesToSelect = findWhereMatches(ast.where, csvContents, tableFilePath);
|
|
29
|
-
const columnNames = ast.columns.map((column) => column.expr.column);
|
|
30
|
-
return {
|
|
31
|
-
...sortValues({
|
|
32
|
-
csvFileHeaderOrder: csvHeaders,
|
|
33
|
-
sqlQueryHeaderOrder: columnNames,
|
|
34
|
-
from: {
|
|
35
|
-
csvFile: csvContents.filter((row, index) => rowIndexesToSelect.includes(index)),
|
|
36
|
-
},
|
|
37
|
-
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
38
|
-
}),
|
|
39
|
-
numberOfRowsAffected: 0,
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
return allSelections.flat().filter(check.isTruthy);
|
|
16
|
+
async handler({ ast, csvDirPath, sql, rejectUnsupportedOperations }) {
|
|
17
|
+
if (ast.variant !== 'select') {
|
|
18
|
+
return undefined;
|
|
43
19
|
}
|
|
44
|
-
|
|
20
|
+
const tableName = getAstType(ast.from, 'identifier')?.name;
|
|
21
|
+
if (!tableName) {
|
|
22
|
+
throw new Error('No table name.');
|
|
23
|
+
}
|
|
24
|
+
const { tableFilePath, sanitizedTableName } = nameCsvTableFile({
|
|
25
|
+
csvDirPath,
|
|
26
|
+
tableName,
|
|
27
|
+
});
|
|
28
|
+
const csvContents = await readCsvFile(tableFilePath);
|
|
29
|
+
const csvHeaders = await readCsvHeaders({
|
|
30
|
+
csvContents,
|
|
31
|
+
sanitizedTableName,
|
|
32
|
+
});
|
|
33
|
+
const rowIndexesToSelect = findWhereMatches(ast.where, csvContents, tableFilePath);
|
|
34
|
+
const columnNames = filterMap(ast.result, (result) => {
|
|
35
|
+
return getAstType(result, 'identifier')?.name;
|
|
36
|
+
}, (value) => {
|
|
37
|
+
if (check.isString(value)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
else if (rejectUnsupportedOperations) {
|
|
41
|
+
throw new SqlUnsupportedOperationError(sql, 'Result name is not a string.', ast);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
...sortValues({
|
|
49
|
+
csvFileHeaderOrder: csvHeaders,
|
|
50
|
+
sqlQueryHeaderOrder: columnNames,
|
|
51
|
+
from: {
|
|
52
|
+
csvFile: csvContents.filter((row, index) => rowIndexesToSelect.includes(index)),
|
|
53
|
+
},
|
|
54
|
+
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
55
|
+
}),
|
|
56
|
+
numberOfRowsAffected: 0,
|
|
57
|
+
};
|
|
45
58
|
},
|
|
46
59
|
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { assertWrap
|
|
2
|
-
import { awaitedBlockingMap } from '@augment-vir/common';
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
3
2
|
import { createCsvHeaderMaps, nameCsvTableFile, readCsvFile, readCsvHeaders, writeCsvFile, } from '../../csv/csv-file.js';
|
|
4
3
|
import { CsvColumnDoesNotExistError } from '../../errors/csv.error.js';
|
|
5
|
-
import {
|
|
4
|
+
import { getAstType } from '../../util/ast-node.js';
|
|
5
|
+
import { sortValues } from '../../util/sort-values.js';
|
|
6
|
+
import { findWhereMatches } from '../../util/where-matcher.js';
|
|
6
7
|
import { defineAstHandler } from '../define-ast-handler.js';
|
|
7
|
-
import { sortValues } from '../sort-values.js';
|
|
8
|
-
import { findWhereMatches } from '../where-matcher.js';
|
|
9
8
|
/**
|
|
10
9
|
* Handles updating rows.
|
|
11
10
|
*
|
|
@@ -14,54 +13,66 @@ import { findWhereMatches } from '../where-matcher.js';
|
|
|
14
13
|
export const rowUpdateHandler = defineAstHandler({
|
|
15
14
|
name: 'row-update',
|
|
16
15
|
async handler({ ast, csvDirPath, sql }) {
|
|
17
|
-
if (ast.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
24
|
-
const csvContents = await readCsvFile(tableFilePath);
|
|
25
|
-
const csvHeaders = await readCsvHeaders({
|
|
26
|
-
csvContents,
|
|
27
|
-
sanitizedTableName,
|
|
28
|
-
});
|
|
29
|
-
const csvHeaderIndexes = createCsvHeaderMaps(csvHeaders);
|
|
30
|
-
const rowIndexesToUpdate = findWhereMatches(ast.where, csvContents, tableFilePath);
|
|
31
|
-
const returningRequirement = ast.returning;
|
|
32
|
-
rowIndexesToUpdate.forEach((rowIndexToUpdate) => {
|
|
33
|
-
const row = assertWrap.isDefined(csvContents[rowIndexToUpdate], `Invalid row index '${rowIndexToUpdate}'.`);
|
|
34
|
-
ast.set.forEach((set) => {
|
|
35
|
-
const columnName = set.column;
|
|
36
|
-
const headerIndex = csvHeaderIndexes.byName[columnName];
|
|
37
|
-
if (!headerIndex) {
|
|
38
|
-
throw new CsvColumnDoesNotExistError(sanitizedTableName, columnName);
|
|
39
|
-
}
|
|
40
|
-
row[headerIndex] = set.value.value;
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
const sqlHeaders = returningRequirement?.columns.map((column) => column.expr.column) || [];
|
|
44
|
-
const result = returningRequirement
|
|
45
|
-
? sortValues({
|
|
46
|
-
csvFileHeaderOrder: csvHeaders,
|
|
47
|
-
sqlQueryHeaderOrder: sqlHeaders,
|
|
48
|
-
from: {
|
|
49
|
-
csvFile: csvContents.filter((row, index) => rowIndexesToUpdate.includes(index)),
|
|
50
|
-
},
|
|
51
|
-
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
52
|
-
})
|
|
53
|
-
: {
|
|
54
|
-
columnNames: [],
|
|
55
|
-
values: [],
|
|
56
|
-
};
|
|
57
|
-
await writeCsvFile(tableFilePath, csvContents);
|
|
58
|
-
return {
|
|
59
|
-
...result,
|
|
60
|
-
numberOfRowsAffected: rowIndexesToUpdate.length,
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
return results.flat().filter(check.isTruthy);
|
|
16
|
+
if (ast.variant !== 'update') {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const tableName = getAstType(ast.into, 'identifier')?.name;
|
|
20
|
+
if (!tableName) {
|
|
21
|
+
throw new Error('No table name');
|
|
64
22
|
}
|
|
65
|
-
|
|
23
|
+
const { tableFilePath, sanitizedTableName } = nameCsvTableFile({
|
|
24
|
+
csvDirPath,
|
|
25
|
+
tableName,
|
|
26
|
+
});
|
|
27
|
+
const csvContents = await readCsvFile(tableFilePath);
|
|
28
|
+
const csvHeaders = await readCsvHeaders({
|
|
29
|
+
csvContents,
|
|
30
|
+
sanitizedTableName,
|
|
31
|
+
});
|
|
32
|
+
const csvHeaderIndexes = createCsvHeaderMaps(csvHeaders);
|
|
33
|
+
const rowIndexesToUpdate = findWhereMatches(ast.where, csvContents, tableFilePath);
|
|
34
|
+
const returningRequirement = ast.returning;
|
|
35
|
+
rowIndexesToUpdate.forEach((rowIndexToUpdate) => {
|
|
36
|
+
const row = assertWrap.isTruthy(csvContents[rowIndexToUpdate], `Invalid row index '${rowIndexToUpdate}'.`);
|
|
37
|
+
ast.set.forEach((set) => {
|
|
38
|
+
const columnName = set.target.name;
|
|
39
|
+
const headerIndex = csvHeaderIndexes.byName[columnName];
|
|
40
|
+
if (!headerIndex) {
|
|
41
|
+
throw new CsvColumnDoesNotExistError(sanitizedTableName, columnName);
|
|
42
|
+
}
|
|
43
|
+
const valueNode = getAstType(set.value, 'literal');
|
|
44
|
+
if (!valueNode) {
|
|
45
|
+
throw new Error(`Unexpected set type: ${set.value.type}`);
|
|
46
|
+
}
|
|
47
|
+
row[headerIndex] = valueNode.value;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
const sqlHeaders = returningRequirement?.map((column) => {
|
|
51
|
+
const columnNode = getAstType(column, 'identifier');
|
|
52
|
+
if (columnNode) {
|
|
53
|
+
return columnNode.name;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw new Error(`Unexpected return column type: ${column.type}`);
|
|
57
|
+
}
|
|
58
|
+
}) || [];
|
|
59
|
+
const result = returningRequirement
|
|
60
|
+
? sortValues({
|
|
61
|
+
csvFileHeaderOrder: csvHeaders,
|
|
62
|
+
sqlQueryHeaderOrder: sqlHeaders,
|
|
63
|
+
from: {
|
|
64
|
+
csvFile: csvContents.filter((row, index) => rowIndexesToUpdate.includes(index)),
|
|
65
|
+
},
|
|
66
|
+
unconsumedInterpolationValues: sql.unconsumedValues,
|
|
67
|
+
})
|
|
68
|
+
: {
|
|
69
|
+
columnNames: [],
|
|
70
|
+
values: [],
|
|
71
|
+
};
|
|
72
|
+
await writeCsvFile(tableFilePath, csvContents);
|
|
73
|
+
return {
|
|
74
|
+
...result,
|
|
75
|
+
numberOfRowsAffected: rowIndexesToUpdate.length,
|
|
76
|
+
};
|
|
66
77
|
},
|
|
67
78
|
});
|