csv-sql-engine 0.3.0 → 0.3.2

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.
@@ -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 sanitizedTableName = removeSuffix({ value: tableName, suffix: csvExtension }).replaceAll(/[./\\]/g, '');
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
  *
@@ -1,5 +1,5 @@
1
1
  import { check } from '@augment-vir/assert';
2
- import { awaitedBlockingMap, ensureErrorAndPrependMessage, wrapInTry, } from '@augment-vir/common';
2
+ import { awaitedBlockingMap, ensureErrorAndPrependMessage, indent, wrapInTry, } from '@augment-vir/common';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { parseSqlite, rawSql } from 'sqlite-ast';
5
5
  import { SqlParseError, SqlUnsupportedOperationError } from '../errors/sql.error.js';
@@ -65,6 +65,6 @@ async function executeIndividualCommand(params) {
65
65
  }
66
66
  catch (error) {
67
67
  const errorAst = params.ast;
68
- throw ensureErrorAndPrependMessage(error, `Failed to execute '${errorAst.variant || errorAst.type}' command.`);
68
+ throw ensureErrorAndPrependMessage(error, `Failed to execute '${errorAst.variant || errorAst.type}' command:\n\n${indent(JSON.stringify(params.ast, null, 4))}\n\n`);
69
69
  }
70
70
  }
@@ -1,8 +1,8 @@
1
1
  import { nameCsvTableFile, readCsvFile, readCsvHeaders, writeCsvFile } from '../../csv/csv-file.js';
2
- import { getAstType } from '../../util/ast-node.js';
2
+ import { getAst } from '../../util/ast-node.js';
3
+ import { sortValues } from '../../util/sort-values.js';
4
+ import { findWhereMatches, MatchSort } from '../../util/where-matcher.js';
3
5
  import { defineAstHandler } from '../define-ast-handler.js';
4
- import { sortValues } from '../sort-values.js';
5
- import { findWhereMatches } from '../where-matcher.js';
6
6
  /**
7
7
  * Handles deleting rows.
8
8
  *
@@ -14,7 +14,11 @@ export const rowDeleteHandler = defineAstHandler({
14
14
  if (ast.variant !== 'delete') {
15
15
  return undefined;
16
16
  }
17
- const tableName = getAstType(ast.from, 'identifier')?.name;
17
+ const tableName = getAst({
18
+ ast: ast.from,
19
+ property: 'type',
20
+ value: 'identifier',
21
+ })?.name;
18
22
  if (!tableName) {
19
23
  throw new Error('Missing table name.');
20
24
  }
@@ -27,10 +31,14 @@ export const rowDeleteHandler = defineAstHandler({
27
31
  csvContents,
28
32
  sanitizedTableName,
29
33
  });
30
- const rowIndexesToDelete = findWhereMatches(ast.where, csvContents, tableFilePath);
34
+ const rowIndexesToDelete = findWhereMatches(ast.where, csvContents, tableFilePath, MatchSort.Descending);
31
35
  const returningRequirement = ast.returning;
32
36
  const sqlHeaders = returningRequirement?.map((column) => {
33
- const columnNode = getAstType(column, 'identifier');
37
+ const columnNode = getAst({
38
+ ast: column,
39
+ property: 'type',
40
+ value: 'identifier',
41
+ });
34
42
  if (columnNode) {
35
43
  return columnNode.name;
36
44
  }
@@ -4,8 +4,8 @@ import { existsSync } from 'node:fs';
4
4
  import { appendCsvRow, nameCsvTableFile, readCsvFile, readCsvHeaders } from '../../csv/csv-file.js';
5
5
  import { CsvTableDoesNotExistError } from '../../errors/csv.error.js';
6
6
  import { SqlUnsupportedOperationError } from '../../errors/sql.error.js';
7
+ import { sortValues } from '../../util/sort-values.js';
7
8
  import { defineAstHandler } from '../define-ast-handler.js';
8
- import { sortValues } from '../sort-values.js';
9
9
  /**
10
10
  * Handles inserting rows.
11
11
  *
@@ -1,11 +1,12 @@
1
- import { check } from '@augment-vir/assert';
1
+ import { check, checkWrap } from '@augment-vir/assert';
2
2
  import { filterMap } from '@augment-vir/common';
3
3
  import { nameCsvTableFile, readCsvFile, readCsvHeaders } from '../../csv/csv-file.js';
4
4
  import { SqlUnsupportedOperationError } from '../../errors/sql.error.js';
5
- import { getAstType } from '../../util/ast-node.js';
5
+ import { getAst } from '../../util/ast-node.js';
6
+ import { readAstText } from '../../util/ast-text.js';
7
+ import { sortValues } from '../../util/sort-values.js';
8
+ import { findWhereMatches, MatchSort } from '../../util/where-matcher.js';
6
9
  import { defineAstHandler } from '../define-ast-handler.js';
7
- import { sortValues } from '../sort-values.js';
8
- import { findWhereMatches } from '../where-matcher.js';
9
10
  /**
10
11
  * Handles SQL selection.
11
12
  *
@@ -17,7 +18,7 @@ export const rowSelectHandler = defineAstHandler({
17
18
  if (ast.variant !== 'select') {
18
19
  return undefined;
19
20
  }
20
- const tableName = getAstType(ast.from, 'identifier')?.name;
21
+ const tableName = getAst({ ast: ast.from, property: 'type', value: 'identifier' })?.name;
21
22
  if (!tableName) {
22
23
  throw new Error('No table name.');
23
24
  }
@@ -30,9 +31,20 @@ export const rowSelectHandler = defineAstHandler({
30
31
  csvContents,
31
32
  sanitizedTableName,
32
33
  });
33
- const rowIndexesToSelect = findWhereMatches(ast.where, csvContents, tableFilePath);
34
+ const limit = ast.limit ? checkWrap.isNumber(Number(readAstText(ast.limit.start))) : -1;
35
+ if (limit == undefined) {
36
+ throw new Error(`Unexpected limit: ${JSON.stringify(ast.limit)}`);
37
+ }
38
+ const offset = ast.limit?.offset
39
+ ? checkWrap.isNumber(Number(readAstText(ast.limit.offset)))
40
+ : 0;
41
+ if (offset == undefined) {
42
+ throw new Error(`Unexpected offset: ${JSON.stringify(ast.limit?.offset)}`);
43
+ }
44
+ const rawIndexes = findWhereMatches(ast.where, csvContents, tableFilePath, MatchSort.Ascending);
45
+ const rowIndexesToSelect = limit < 0 ? rawIndexes.slice(offset) : rawIndexes.slice(offset, offset + limit);
34
46
  const columnNames = filterMap(ast.result, (result) => {
35
- return getAstType(result, 'identifier')?.name;
47
+ return getAst({ ast: result, property: 'type', value: 'identifier' })?.name;
36
48
  }, (value) => {
37
49
  if (check.isString(value)) {
38
50
  return true;
@@ -1,10 +1,10 @@
1
1
  import { assertWrap } from '@augment-vir/assert';
2
2
  import { createCsvHeaderMaps, nameCsvTableFile, readCsvFile, readCsvHeaders, writeCsvFile, } from '../../csv/csv-file.js';
3
3
  import { CsvColumnDoesNotExistError } from '../../errors/csv.error.js';
4
- import { getAstType } from '../../util/ast-node.js';
4
+ import { getAst } from '../../util/ast-node.js';
5
+ import { sortValues } from '../../util/sort-values.js';
6
+ import { findWhereMatches, MatchSort } from '../../util/where-matcher.js';
5
7
  import { defineAstHandler } from '../define-ast-handler.js';
6
- import { sortValues } from '../sort-values.js';
7
- import { findWhereMatches } from '../where-matcher.js';
8
8
  /**
9
9
  * Handles updating rows.
10
10
  *
@@ -16,7 +16,7 @@ export const rowUpdateHandler = defineAstHandler({
16
16
  if (ast.variant !== 'update') {
17
17
  return undefined;
18
18
  }
19
- const tableName = getAstType(ast.into, 'identifier')?.name;
19
+ const tableName = getAst({ ast: ast.into, property: 'type', value: 'identifier' })?.name;
20
20
  if (!tableName) {
21
21
  throw new Error('No table name');
22
22
  }
@@ -30,7 +30,7 @@ export const rowUpdateHandler = defineAstHandler({
30
30
  sanitizedTableName,
31
31
  });
32
32
  const csvHeaderIndexes = createCsvHeaderMaps(csvHeaders);
33
- const rowIndexesToUpdate = findWhereMatches(ast.where, csvContents, tableFilePath);
33
+ const rowIndexesToUpdate = findWhereMatches(ast.where, csvContents, tableFilePath, MatchSort.Ascending);
34
34
  const returningRequirement = ast.returning;
35
35
  rowIndexesToUpdate.forEach((rowIndexToUpdate) => {
36
36
  const row = assertWrap.isTruthy(csvContents[rowIndexToUpdate], `Invalid row index '${rowIndexToUpdate}'.`);
@@ -40,7 +40,7 @@ export const rowUpdateHandler = defineAstHandler({
40
40
  if (!headerIndex) {
41
41
  throw new CsvColumnDoesNotExistError(sanitizedTableName, columnName);
42
42
  }
43
- const valueNode = getAstType(set.value, 'literal');
43
+ const valueNode = getAst({ ast: set.value, property: 'type', value: 'literal' });
44
44
  if (!valueNode) {
45
45
  throw new Error(`Unexpected set type: ${set.value.type}`);
46
46
  }
@@ -48,7 +48,7 @@ export const rowUpdateHandler = defineAstHandler({
48
48
  });
49
49
  });
50
50
  const sqlHeaders = returningRequirement?.map((column) => {
51
- const columnNode = getAstType(column, 'identifier');
51
+ const columnNode = getAst({ ast: column, property: 'type', value: 'identifier' });
52
52
  if (columnNode) {
53
53
  return columnNode.name;
54
54
  }
@@ -5,7 +5,7 @@ import { rename } from 'node:fs/promises';
5
5
  import { nameCsvTableFile, readCsvFile, readCsvHeaders, writeCsvFile } from '../../csv/csv-file.js';
6
6
  import { CsvColumnDoesNotExistError, CsvTableDoesNotExistError } from '../../errors/csv.error.js';
7
7
  import { SqlUnsupportedOperationError } from '../../errors/sql.error.js';
8
- import { getAstType } from '../../util/ast-node.js';
8
+ import { getAst } from '../../util/ast-node.js';
9
9
  import { defineAstHandler } from '../define-ast-handler.js';
10
10
  /**
11
11
  * Handles altering tables.
@@ -33,7 +33,7 @@ export const tableAlterHandler = defineAstHandler({
33
33
  sanitizedTableName,
34
34
  });
35
35
  if (ast.action === 'rename') {
36
- const newTableName = assertWrap.isTruthy(getAstType(ast.name, 'identifier')?.name, 'Missing new table name.');
36
+ const newTableName = assertWrap.isTruthy(getAst({ ast: ast.name, property: 'type', value: 'identifier' })?.name, 'Missing new table name.');
37
37
  await rename(tableFilePath, nameCsvTableFile({ csvDirPath, tableName: newTableName }).tableFilePath);
38
38
  return {
39
39
  columnNames: [],
@@ -45,7 +45,11 @@ export const tableAlterHandler = defineAstHandler({
45
45
  if (!ast.definition || ast.definition.type !== 'definition') {
46
46
  return;
47
47
  }
48
- const defaultValue = getAstType(ast.definition.definition.find((entry) => entry.type === 'constraint' && entry.variant === 'default')?.value, 'literal')?.value || '';
48
+ const defaultValue = getAst({
49
+ ast: ast.definition.definition.find((entry) => entry.type === 'constraint' && entry.variant === 'default')?.value,
50
+ property: 'type',
51
+ value: 'literal',
52
+ })?.value || '';
49
53
  const newHeaderName = assertWrap.isTruthy(ast.definition.name, 'Missing new column name.');
50
54
  csvContents.forEach((row, index) => {
51
55
  if (index) {
package/dist/index.d.ts CHANGED
@@ -11,8 +11,8 @@ export * from './engine/handlers/table-alter.handler.js';
11
11
  export * from './engine/handlers/table-create.handler.js';
12
12
  export * from './engine/handlers/table-drop.handler.js';
13
13
  export * from './engine/params.js';
14
- export * from './engine/sort-values.js';
15
- export * from './engine/where-matcher.js';
16
14
  export * from './errors/csv-sql-engine.error.js';
17
15
  export * from './errors/csv.error.js';
18
16
  export * from './errors/sql.error.js';
17
+ export * from './util/sort-values.js';
18
+ export * from './util/where-matcher.js';
package/dist/index.js CHANGED
@@ -11,8 +11,8 @@ export * from './engine/handlers/table-alter.handler.js';
11
11
  export * from './engine/handlers/table-create.handler.js';
12
12
  export * from './engine/handlers/table-drop.handler.js';
13
13
  export * from './engine/params.js';
14
- export * from './engine/sort-values.js';
15
- export * from './engine/where-matcher.js';
16
14
  export * from './errors/csv-sql-engine.error.js';
17
15
  export * from './errors/csv.error.js';
18
16
  export * from './errors/sql.error.js';
17
+ export * from './util/sort-values.js';
18
+ export * from './util/where-matcher.js';
@@ -1,10 +1,6 @@
1
1
  import { type SqliteAstNode } from 'sqlite-ast';
2
- /** @throws If the request type is not there. */
3
- export declare function getAstType<const Ast extends SqliteAstNode | undefined, const TypeName extends NonNullable<Ast>['type']>(node: Ast, typeName: TypeName): Extract<Ast, {
4
- type: TypeName;
5
- }> | undefined;
6
- export declare function getAstVariant<const Ast extends Extract<SqliteAstNode, {
7
- variant: string;
8
- }> | undefined, const VariantName extends NonNullable<Ast>['variant']>(node: Ast, variant: VariantName): Extract<Ast, {
9
- variant: VariantName;
10
- }> | undefined;
2
+ export declare function getAst<const Ast extends (SqliteAstNode & Record<PropertyToCheck, any>) | undefined, const PropertyToCheck extends string, const ValueToCheck>({ ast, property, value, }: {
3
+ ast: Ast;
4
+ property: PropertyToCheck;
5
+ value: ValueToCheck;
6
+ }): Extract<Ast, Record<PropertyToCheck, ValueToCheck>> | undefined;
@@ -1,21 +1,9 @@
1
- /** @throws If the request type is not there. */
2
- export function getAstType(node, typeName) {
3
- if (!node) {
1
+ export function getAst({ ast, property, value, }) {
2
+ if (!ast) {
4
3
  return undefined;
5
4
  }
6
- else if (node.type === typeName) {
7
- return node;
8
- }
9
- else {
10
- return undefined;
11
- }
12
- }
13
- export function getAstVariant(node, variant) {
14
- if (!node) {
15
- return undefined;
16
- }
17
- else if (node.variant === variant) {
18
- return node;
5
+ else if (ast[property] === value) {
6
+ return ast;
19
7
  }
20
8
  else {
21
9
  return undefined;
@@ -0,0 +1,7 @@
1
+ import { type SqliteAstNode } from 'sqlite-ast';
2
+ /**
3
+ * Recursively converts an AST node back to its SQL text representation.
4
+ *
5
+ * @category Internal
6
+ */
7
+ export declare function readAstText(node: SqliteAstNode | null | undefined): string;
@@ -0,0 +1,649 @@
1
+ /**
2
+ * Recursively converts an AST node back to its SQL text representation.
3
+ *
4
+ * @category Internal
5
+ */
6
+ export function readAstText(node) {
7
+ if (!node) {
8
+ return '';
9
+ }
10
+ switch (node.type) {
11
+ case 'literal':
12
+ return readLiteralText(node);
13
+ case 'identifier':
14
+ return readIdentifierText(node);
15
+ case 'expression':
16
+ return readExpressionText(node);
17
+ case 'function':
18
+ return readFunctionText(node);
19
+ case 'statement':
20
+ return readStatementText(node);
21
+ case 'constraint':
22
+ return readConstraintText(node);
23
+ case 'definition':
24
+ return readDefinitionText(node);
25
+ case 'condition':
26
+ return readConditionText(node);
27
+ case 'join':
28
+ return readJoinText(node);
29
+ case 'map':
30
+ return readMapText(node);
31
+ case 'assignment':
32
+ return readAssignmentText(node);
33
+ case 'compound':
34
+ return readCompoundText(node);
35
+ case 'datatype':
36
+ return readDatatypeText(node);
37
+ case 'variable':
38
+ return readVariableText(node);
39
+ case 'values':
40
+ return 'DEFAULT VALUES';
41
+ case 'error':
42
+ return readErrorText(node);
43
+ case 'event':
44
+ return readEventText(node);
45
+ case 'module':
46
+ return readModuleText(node);
47
+ default:
48
+ // Fallback for any unhandled type
49
+ return '';
50
+ }
51
+ }
52
+ function readLiteralText(node) {
53
+ if (node.variant === 'text') {
54
+ return `'${node.value}'`;
55
+ }
56
+ else if (node.variant === 'null') {
57
+ return 'NULL';
58
+ }
59
+ else if (node.variant === 'blob') {
60
+ return node.value;
61
+ }
62
+ return node.value;
63
+ }
64
+ function readIdentifierText(node) {
65
+ let text = node.name;
66
+ if (node.columns) {
67
+ text += `(${readNodeArray(node.columns)})`;
68
+ }
69
+ if (node.alias) {
70
+ text += ` AS ${node.alias}`;
71
+ }
72
+ return text;
73
+ }
74
+ function readExpressionText(node) {
75
+ if ('format' in node) {
76
+ if (node.format === 'binary') {
77
+ const left = readAstText(node.left);
78
+ const right = readAstText(node.right);
79
+ let text = `${left} ${node.operation.toUpperCase()} ${right}`;
80
+ if (node.escape) {
81
+ text += ` ESCAPE ${readAstText(node.escape)}`;
82
+ }
83
+ return withAlias(text, node);
84
+ }
85
+ else if (node.format === 'unary') {
86
+ if (node.variant === 'operation') {
87
+ if ('collate' in node) {
88
+ const expr = readAstText(node.expression);
89
+ const collation = node.collate.map((c) => c.name).join(' ');
90
+ return `${expr} COLLATE ${collation}`;
91
+ }
92
+ const expr = readAstText(node.expression);
93
+ const op = node.operator.toUpperCase();
94
+ // Prefix operators like NOT, -, +, ~
95
+ if (op === '-' || op === '+' || op === '~') {
96
+ return withAlias(`${op}${expr}`, node);
97
+ }
98
+ if (op === 'NOT') {
99
+ return withAlias(`${op} ${expr}`, node);
100
+ }
101
+ return withAlias(`${expr} ${op}`, node);
102
+ }
103
+ else if (node.variant === 'cast') {
104
+ const expr = readAstText(node.expression);
105
+ const datatype = readDatatypeText(node.as);
106
+ return withAlias(`CAST(${expr} AS ${datatype})`, node);
107
+ }
108
+ // exists variant
109
+ const expr = readAstText(node.expression);
110
+ return `${node.operator.toUpperCase()} ${expr}`;
111
+ }
112
+ // table format
113
+ const target = readAstText(node.target);
114
+ const expr = readAstText(node.expression);
115
+ const variant = node.variant === 'recursive' ? 'RECURSIVE' : '';
116
+ return `${target} AS ${variant}(${expr})`.trim();
117
+ }
118
+ switch (node.variant) {
119
+ case 'list':
120
+ return readExpressionList(node.expression);
121
+ case 'order': {
122
+ const expr = readAstText(node.expression);
123
+ const dir = node.direction ? ` ${node.direction.toUpperCase()}` : '';
124
+ return `${expr}${dir}`;
125
+ }
126
+ case 'limit': {
127
+ const start = readAstText(node.start);
128
+ const offset = node.offset ? ` OFFSET ${readAstText(node.offset)}` : '';
129
+ return `${start}${offset}`;
130
+ }
131
+ case 'exists':
132
+ return node.operator.toUpperCase();
133
+ case 'case':
134
+ return withAlias(readCaseText(node.expression), node);
135
+ default:
136
+ return '';
137
+ }
138
+ }
139
+ function readExpressionList(expression) {
140
+ if (expression == null) {
141
+ return '';
142
+ }
143
+ if (Array.isArray(expression)) {
144
+ return expression.map(readAstText).join(', ');
145
+ }
146
+ return readAstText(expression);
147
+ }
148
+ function readCaseText(conditions) {
149
+ const parts = conditions.map((cond) => {
150
+ if (cond.type === 'condition') {
151
+ if (cond.variant === 'when') {
152
+ return `WHEN ${readAstText(cond.condition)} THEN ${readAstText(cond.consequent)}`;
153
+ }
154
+ else if (cond.variant === 'else') {
155
+ return `ELSE ${readAstText(cond.consequent)}`;
156
+ }
157
+ }
158
+ return '';
159
+ });
160
+ return `CASE ${parts.join(' ')} END`;
161
+ }
162
+ function readFunctionText(node) {
163
+ const name = node.name.name.toUpperCase();
164
+ let args;
165
+ if (node.args.type === 'identifier') {
166
+ args = '*';
167
+ }
168
+ else {
169
+ args = readExpressionList(node.args.expression);
170
+ }
171
+ const text = `${name}(${args})`;
172
+ return withAlias(text, node);
173
+ }
174
+ function readStatementText(node) {
175
+ switch (node.variant) {
176
+ case 'select':
177
+ return readSelectText(node);
178
+ case 'insert':
179
+ return readInsertText(node);
180
+ case 'update':
181
+ return readUpdateText(node);
182
+ case 'delete':
183
+ return readDeleteText(node);
184
+ case 'create':
185
+ return readCreateText(node);
186
+ case 'drop':
187
+ return readDropText(node);
188
+ case 'compound':
189
+ return readCompoundStatementText(node);
190
+ case 'transaction':
191
+ return readTransactionText(node);
192
+ case 'alter table':
193
+ return readAlterTableText(node);
194
+ case 'pragma':
195
+ return readPragmaText(node);
196
+ case 'attach':
197
+ return readAttachText(node);
198
+ case 'detach':
199
+ return readDetachText(node);
200
+ case 'vacuum':
201
+ return readVacuumText(node);
202
+ case 'reindex':
203
+ return readReindexText(node);
204
+ case 'analyze':
205
+ return readAnalyzeText(node);
206
+ case 'release':
207
+ case 'savepoint':
208
+ return readSavepointText(node);
209
+ default:
210
+ return '';
211
+ }
212
+ }
213
+ function readSelectText(node) {
214
+ const parts = [];
215
+ if (node.with) {
216
+ parts.push(`WITH ${readNodeArray(node.with)}`);
217
+ }
218
+ parts.push('SELECT');
219
+ if (node.distinct) {
220
+ parts.push('DISTINCT');
221
+ }
222
+ parts.push(readNodeArray(node.result));
223
+ if (node.from) {
224
+ parts.push(`FROM ${readAstText(node.from)}`);
225
+ }
226
+ if (node.where) {
227
+ parts.push(`WHERE ${readNodeArray(node.where, ' AND ')}`);
228
+ }
229
+ if (node.group) {
230
+ parts.push(`GROUP BY ${readExpressionList(node.group.expression)}`);
231
+ }
232
+ if (node.having) {
233
+ parts.push(`HAVING ${readAstText(node.having)}`);
234
+ }
235
+ if (node.order) {
236
+ parts.push(`ORDER BY ${readNodeArray(node.order)}`);
237
+ }
238
+ if (node.limit) {
239
+ parts.push(`LIMIT ${readAstText(node.limit)}`);
240
+ }
241
+ return withAlias(parts.join(' '), node);
242
+ }
243
+ function readInsertText(node) {
244
+ const parts = [];
245
+ if (node.or) {
246
+ parts.push(`INSERT OR ${node.or.toUpperCase()}`);
247
+ }
248
+ else {
249
+ parts.push('INSERT');
250
+ }
251
+ parts.push(`INTO ${readAstText(node.into)}`);
252
+ if (Array.isArray(node.result)) {
253
+ const values = node.result
254
+ .map((r) => {
255
+ const expr = r.expression;
256
+ return `(${readExpressionList(expr)})`;
257
+ })
258
+ .join(', ');
259
+ parts.push(`VALUES ${values}`);
260
+ }
261
+ else if ('type' in node.result && node.result.type === 'values') {
262
+ parts.push('DEFAULT VALUES');
263
+ }
264
+ else {
265
+ parts.push(readAstText(node.result));
266
+ }
267
+ if (node.returning) {
268
+ parts.push(`RETURNING ${readNodeArray(node.returning)}`);
269
+ }
270
+ return parts.join(' ');
271
+ }
272
+ function readUpdateText(node) {
273
+ const parts = [];
274
+ if (node.or) {
275
+ parts.push(`UPDATE OR ${node.or.toUpperCase()}`);
276
+ }
277
+ else {
278
+ parts.push('UPDATE');
279
+ }
280
+ parts.push(readAstText(node.into));
281
+ const setClause = node.set.map((s) => readAssignmentText(s)).join(', ');
282
+ parts.push(`SET ${setClause}`);
283
+ if (node.where) {
284
+ parts.push(`WHERE ${readNodeArray(node.where, ' AND ')}`);
285
+ }
286
+ if (node.limit) {
287
+ parts.push(`LIMIT ${readAstText(node.limit)}`);
288
+ }
289
+ if (node.returning) {
290
+ parts.push(`RETURNING ${readNodeArray(node.returning)}`);
291
+ }
292
+ return parts.join(' ');
293
+ }
294
+ function readDeleteText(node) {
295
+ const parts = [
296
+ 'DELETE FROM',
297
+ readAstText(node.from),
298
+ ];
299
+ if (node.where) {
300
+ parts.push(`WHERE ${readNodeArray(node.where, ' AND ')}`);
301
+ }
302
+ if (node.limit) {
303
+ parts.push(`LIMIT ${readAstText(node.limit)}`);
304
+ }
305
+ if (node.returning) {
306
+ parts.push(`RETURNING ${readNodeArray(node.returning)}`);
307
+ }
308
+ return parts.join(' ');
309
+ }
310
+ function readCreateText(node) {
311
+ const parts = ['CREATE'];
312
+ if (node.temporary) {
313
+ parts.push('TEMPORARY');
314
+ }
315
+ if (node.unique) {
316
+ parts.push('UNIQUE');
317
+ }
318
+ parts.push(node.format.toUpperCase());
319
+ if (node.condition) {
320
+ parts.push('IF NOT EXISTS');
321
+ }
322
+ if (node.name) {
323
+ parts.push(readAstText(node.name));
324
+ }
325
+ if (node.format === 'table' && node.definition) {
326
+ parts.push(`(${readNodeArray(node.definition)})`);
327
+ }
328
+ if (node.format === 'index' && node.on) {
329
+ parts.push(`ON ${readAstText(node.on)}`);
330
+ }
331
+ if (node.format === 'view' && node.result) {
332
+ parts.push(`AS ${readAstText(node.result)}`);
333
+ }
334
+ if (node.format === 'trigger') {
335
+ if (node.event) {
336
+ parts.push(readEventText(node.event));
337
+ }
338
+ if (node.on) {
339
+ parts.push(`ON ${readAstText(node.on)}`);
340
+ }
341
+ if (node.by) {
342
+ parts.push(`FOR EACH ${node.by.toUpperCase()}`);
343
+ }
344
+ if (node.when) {
345
+ parts.push(`WHEN ${readAstText(node.when)}`);
346
+ }
347
+ if (node.action) {
348
+ parts.push(`BEGIN ${readNodeArray(node.action, '; ')}; END`);
349
+ }
350
+ }
351
+ return parts.join(' ');
352
+ }
353
+ function readDropText(node) {
354
+ const parts = [
355
+ 'DROP',
356
+ node.format.toUpperCase(),
357
+ ];
358
+ if (node.condition.length > 0) {
359
+ parts.push('IF EXISTS');
360
+ }
361
+ parts.push(readAstText(node.target));
362
+ return parts.join(' ');
363
+ }
364
+ function readCompoundStatementText(node) {
365
+ const first = readAstText(node.statement);
366
+ const rest = node.compound.map((c) => `${c.variant.toUpperCase()} ${readAstText(c.statement)}`);
367
+ return [
368
+ first,
369
+ ...rest,
370
+ ].join(' ');
371
+ }
372
+ function readTransactionText(node) {
373
+ const parts = [node.action.toUpperCase()];
374
+ if (node.defer) {
375
+ parts.push(node.defer.toUpperCase());
376
+ }
377
+ parts.push('TRANSACTION');
378
+ if (node.savepoint) {
379
+ parts.push(node.savepoint.name);
380
+ }
381
+ return parts.join(' ');
382
+ }
383
+ function readAlterTableText(node) {
384
+ const parts = [
385
+ 'ALTER TABLE',
386
+ readAstText(node.target),
387
+ ];
388
+ if (node.action === 'rename') {
389
+ if (node.column && node.newName) {
390
+ parts.push(`RENAME COLUMN ${node.column} TO ${node.newName}`);
391
+ }
392
+ else if (node.newName) {
393
+ parts.push(`RENAME TO ${node.newName}`);
394
+ }
395
+ }
396
+ else if (node.action === 'add') {
397
+ if (node.definition) {
398
+ parts.push(`ADD COLUMN ${readAstText(node.definition)}`);
399
+ }
400
+ else if (node.name) {
401
+ parts.push(`ADD COLUMN ${readAstText(node.name)}`);
402
+ }
403
+ }
404
+ else if (node.action === 'drop' && node.column) {
405
+ parts.push(`DROP COLUMN ${node.column}`);
406
+ }
407
+ return parts.join(' ');
408
+ }
409
+ function readPragmaText(node) {
410
+ const name = node.target.name;
411
+ if (node.args.expression) {
412
+ return `PRAGMA ${name} = ${readAstText(node.args.expression)}`;
413
+ }
414
+ return `PRAGMA ${name}`;
415
+ }
416
+ function readAttachText(node) {
417
+ return `ATTACH ${readAstText(node.target)} AS ${readAstText(node.attach)}`;
418
+ }
419
+ function readDetachText(node) {
420
+ return `DETACH ${node.target.name}`;
421
+ }
422
+ function readVacuumText(node) {
423
+ if (node.target) {
424
+ return `VACUUM ${node.target.name}`;
425
+ }
426
+ return 'VACUUM';
427
+ }
428
+ function readReindexText(node) {
429
+ if (node.target) {
430
+ return `REINDEX ${node.target}`;
431
+ }
432
+ return 'REINDEX';
433
+ }
434
+ function readAnalyzeText(node) {
435
+ if (node.target) {
436
+ return `ANALYZE ${node.target}`;
437
+ }
438
+ return 'ANALYZE';
439
+ }
440
+ function readSavepointText(node) {
441
+ const action = node.variant === 'release' ? 'RELEASE' : 'SAVEPOINT';
442
+ return `${action} ${node.target.savepoint.name}`;
443
+ }
444
+ function readConstraintText(node) {
445
+ const parts = [];
446
+ switch (node.variant) {
447
+ case 'primary key':
448
+ if (node.name) {
449
+ parts.push(`CONSTRAINT ${node.name}`);
450
+ }
451
+ parts.push('PRIMARY KEY');
452
+ if (node.direction) {
453
+ parts.push(node.direction.toUpperCase());
454
+ }
455
+ if (node.conflict) {
456
+ parts.push(`ON CONFLICT ${node.conflict.toUpperCase()}`);
457
+ }
458
+ if (node.autoIncrement) {
459
+ parts.push('AUTOINCREMENT');
460
+ }
461
+ break;
462
+ case 'not null':
463
+ if (node.name) {
464
+ parts.push(`CONSTRAINT ${node.name}`);
465
+ }
466
+ parts.push('NOT NULL');
467
+ if (node.conflict) {
468
+ parts.push(`ON CONFLICT ${node.conflict.toUpperCase()}`);
469
+ }
470
+ break;
471
+ case 'null':
472
+ if (node.name) {
473
+ parts.push(`CONSTRAINT ${node.name}`);
474
+ }
475
+ parts.push('NULL');
476
+ break;
477
+ case 'unique':
478
+ if (node.name) {
479
+ parts.push(`CONSTRAINT ${node.name}`);
480
+ }
481
+ parts.push('UNIQUE');
482
+ if (node.conflict) {
483
+ parts.push(`ON CONFLICT ${node.conflict.toUpperCase()}`);
484
+ }
485
+ break;
486
+ case 'check':
487
+ if (node.name) {
488
+ parts.push(`CONSTRAINT ${node.name}`);
489
+ }
490
+ parts.push(`CHECK (${readAstText(node.expression)})`);
491
+ break;
492
+ case 'default':
493
+ if (node.name) {
494
+ parts.push(`CONSTRAINT ${node.name}`);
495
+ }
496
+ parts.push(`DEFAULT ${readAstText(node.value)}`);
497
+ break;
498
+ case 'foreign key':
499
+ if (node.name) {
500
+ parts.push(`CONSTRAINT ${node.name}`);
501
+ }
502
+ parts.push(`REFERENCES ${readAstText(node.references)}`);
503
+ if (node.action) {
504
+ for (const act of node.action) {
505
+ parts.push(`ON ${act.variant.toUpperCase()} ${act.action.toUpperCase()}`);
506
+ }
507
+ }
508
+ if (node.defer) {
509
+ parts.push(node.defer.toUpperCase());
510
+ }
511
+ break;
512
+ case 'collate':
513
+ if (node.name) {
514
+ parts.push(`CONSTRAINT ${node.name}`);
515
+ }
516
+ parts.push(`COLLATE ${node.collate.collate.map((c) => c.name).join(' ')}`);
517
+ break;
518
+ case 'join':
519
+ parts.push(node.format.toUpperCase());
520
+ if (node.on) {
521
+ parts.push(`ON ${readAstText(node.on)}`);
522
+ }
523
+ if (node.using) {
524
+ const cols = node.using.columns.map((c) => c.name).join(', ');
525
+ parts.push(`USING (${cols})`);
526
+ }
527
+ break;
528
+ }
529
+ return parts.join(' ');
530
+ }
531
+ function readDefinitionText(node) {
532
+ if (node.variant === 'column') {
533
+ const parts = [node.name];
534
+ parts.push(readDatatypeText(node.datatype));
535
+ if (node.definition.length > 0) {
536
+ parts.push(readNodeArray(node.definition, ' '));
537
+ }
538
+ return parts.join(' ');
539
+ }
540
+ // constraint variant
541
+ const parts = [];
542
+ if (node.name) {
543
+ parts.push(`CONSTRAINT ${node.name}`);
544
+ }
545
+ if (node.columns) {
546
+ parts.push(`(${readNodeArray(node.columns)})`);
547
+ }
548
+ if (node.definition.length > 0) {
549
+ parts.push(readNodeArray(node.definition, ' '));
550
+ }
551
+ return parts.join(' ');
552
+ }
553
+ function readConditionText(node) {
554
+ switch (node.variant) {
555
+ case 'when':
556
+ return `WHEN ${readAstText(node.condition)} THEN ${readAstText(node.consequent)}`;
557
+ case 'else':
558
+ return `ELSE ${readAstText(node.consequent)}`;
559
+ case 'if':
560
+ return `IF ${readAstText(node.condition)}`;
561
+ default:
562
+ return '';
563
+ }
564
+ }
565
+ function readJoinText(node) {
566
+ const parts = [];
567
+ parts.push(node.variant.toUpperCase());
568
+ parts.push(readAstText(node.source));
569
+ if (node.constraint) {
570
+ parts.push(readAstText(node.constraint));
571
+ }
572
+ return parts.join(' ');
573
+ }
574
+ function readMapText(node) {
575
+ const source = readAstText(node.source);
576
+ const joins = node.map.map(readAstText).join(' ');
577
+ return `${source} ${joins}`;
578
+ }
579
+ function readAssignmentText(node) {
580
+ return `${node.target.name} = ${readAstText(node.value)}`;
581
+ }
582
+ function readCompoundText(node) {
583
+ return `${node.variant.toUpperCase()} ${readAstText(node.statement)}`;
584
+ }
585
+ function readDatatypeText(node) {
586
+ let text = node.variant.toUpperCase();
587
+ if (node.args) {
588
+ text += `(${readExpressionList(node.args.expression)})`;
589
+ }
590
+ return text;
591
+ }
592
+ function readVariableText(node) {
593
+ let text;
594
+ switch (node.format) {
595
+ case 'named':
596
+ text = `:${node.name}`;
597
+ break;
598
+ case 'numbered':
599
+ text = `?${node.name}`;
600
+ break;
601
+ case 'tcl':
602
+ text = `$${node.name}`;
603
+ break;
604
+ default:
605
+ text = `?`;
606
+ }
607
+ if (node.suffix) {
608
+ text += node.suffix;
609
+ }
610
+ return text;
611
+ }
612
+ function readErrorText(node) {
613
+ const parts = ['RAISE'];
614
+ if (node.action) {
615
+ parts.push(`(${node.action.toUpperCase()}`);
616
+ if (node.message) {
617
+ parts.push(`, ${readAstText(node.message)}`);
618
+ }
619
+ parts.push(')');
620
+ }
621
+ return parts.join('');
622
+ }
623
+ function readEventText(node) {
624
+ const parts = [];
625
+ if (node.occurs) {
626
+ parts.push(node.occurs.toUpperCase());
627
+ }
628
+ parts.push(node.event.toUpperCase());
629
+ if (node.of) {
630
+ parts.push(`OF ${readNodeArray(node.of)}`);
631
+ }
632
+ return parts.join(' ');
633
+ }
634
+ function readModuleText(node) {
635
+ const args = readExpressionList(node.args.expression);
636
+ return `USING ${node.name}(${args})`;
637
+ }
638
+ function readNodeArray(nodes, separator = ', ') {
639
+ if (!nodes) {
640
+ return '';
641
+ }
642
+ return nodes.map(readAstText).join(separator);
643
+ }
644
+ function withAlias(text, node) {
645
+ if (node.alias) {
646
+ return `${text} AS ${node.alias}`;
647
+ }
648
+ return text;
649
+ }
@@ -1,18 +1,28 @@
1
+ /**
2
+ * Extracts the column name from a potentially fully qualified identifier. E.g., `main.users.email`
3
+ * -> `email`, `users.email` -> `email`, `email` -> `email`
4
+ */
5
+ function extractColumnName(header) {
6
+ const parts = header.split('.');
7
+ return parts[parts.length - 1] || header;
8
+ }
1
9
  /**
2
10
  * Sorts values for CSV insertion or reading and handle interpolated values.
3
11
  *
4
12
  * @category Internal
5
13
  */
6
14
  export function sortValues({ csvFileHeaderOrder, sqlQueryHeaderOrder, from, unconsumedInterpolationValues, }) {
7
- const fromOrder = from.sqlQuery ? sqlQueryHeaderOrder : csvFileHeaderOrder;
8
- const toOrder = (from.sqlQuery ? csvFileHeaderOrder : sqlQueryHeaderOrder).flatMap((header) => {
15
+ const fromOrder = (from.sqlQuery ? sqlQueryHeaderOrder : csvFileHeaderOrder).map(extractColumnName);
16
+ const toOrder = (from.sqlQuery ? csvFileHeaderOrder : sqlQueryHeaderOrder)
17
+ .flatMap((header) => {
9
18
  if (header === '*') {
10
19
  return csvFileHeaderOrder;
11
20
  }
12
21
  else {
13
22
  return header;
14
23
  }
15
- });
24
+ })
25
+ .map(extractColumnName);
16
26
  const values = (from.csvFile || from.sqlQuery).map((valueRow) => {
17
27
  const mappedValueRow = valueRow.map((value) => {
18
28
  if (value === '?') {
@@ -1,10 +1,21 @@
1
1
  import { type MaybeArray } from '@augment-vir/common';
2
2
  import { type SqliteAstNode } from 'sqlite-ast';
3
3
  import { type CsvFile } from '../csv/csv-file.js';
4
+ /**
5
+ * Used to determine how matches returned from {@link findWhereMatches} should be sorted.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export declare enum MatchSort {
10
+ /** 0 index first */
11
+ Ascending = "ascending",
12
+ /** 0 index last */
13
+ Descending = "descending"
14
+ }
4
15
  /**
5
16
  * Finds all row indexes that match the given SQL where conditions.
6
17
  *
7
18
  * @category Internal
8
19
  * @returns An array of row indexes that match the given where condition.
9
20
  */
10
- export declare function findWhereMatches(expressions: Readonly<MaybeArray<Readonly<SqliteAstNode>>> | undefined, csvContents: Readonly<CsvFile>, csvFilePath: string): number[];
21
+ export declare function findWhereMatches(expressions: Readonly<MaybeArray<Readonly<SqliteAstNode>>> | undefined, csvContents: Readonly<CsvFile>, csvFilePath: string, sort: MatchSort): number[];
@@ -1,23 +1,46 @@
1
+ import { check } from '@augment-vir/assert';
1
2
  import { ensureArray, extractDuplicates, filterMap, removeDuplicates, removeSuffix, } from '@augment-vir/common';
2
3
  import { csvExtension } from '../csv/csv-file.js';
3
4
  import { CsvColumnDoesNotExistError, CsvFileMissingHeadersError } from '../errors/csv.error.js';
5
+ /**
6
+ * Used to determine how matches returned from {@link findWhereMatches} should be sorted.
7
+ *
8
+ * @category Internal
9
+ */
10
+ export var MatchSort;
11
+ (function (MatchSort) {
12
+ /** 0 index first */
13
+ MatchSort["Ascending"] = "ascending";
14
+ /** 0 index last */
15
+ MatchSort["Descending"] = "descending";
16
+ })(MatchSort || (MatchSort = {}));
4
17
  /**
5
18
  * Finds all row indexes that match the given SQL where conditions.
6
19
  *
7
20
  * @category Internal
8
21
  * @returns An array of row indexes that match the given where condition.
9
22
  */
10
- export function findWhereMatches(expressions, csvContents, csvFilePath) {
11
- /**
12
- * These must be sorted from greatest to least so that deleting rows does not mess up the
13
- * indexes.
14
- */
15
- const allIndexes = removeDuplicates((expressions ? ensureArray(expressions) : [undefined]).flatMap((expression) => innerFindWhereMatches(expression, csvContents, csvFilePath))).sort((a, b) => b - a);
23
+ export function findWhereMatches(expressions, csvContents, csvFilePath, sort) {
24
+ const allIndexes = removeDuplicates((expressions ? ensureArray(expressions) : [undefined]).flatMap((expression) => innerFindWhereMatches(expression, csvContents, csvFilePath))).sort((a, b) => {
25
+ if (sort === MatchSort.Descending) {
26
+ return b - a;
27
+ }
28
+ else {
29
+ return a - b;
30
+ }
31
+ });
16
32
  return allIndexes;
17
33
  }
34
+ function getAllIndexes(csvContents) {
35
+ return csvContents
36
+ .map((value, index) => index)
37
+ .filter(
38
+ /** Exclude the first index, which is headers. */
39
+ check.isTruthy);
40
+ }
18
41
  function innerFindWhereMatches(where, csvContents, csvFilePath) {
19
42
  if (!where) {
20
- return csvContents.map((value, index) => index);
43
+ return getAllIndexes(csvContents);
21
44
  }
22
45
  else if (where.type === 'expression' &&
23
46
  where.variant === 'operation' &&
@@ -39,6 +62,17 @@ function innerFindWhereMatches(where, csvContents, csvFilePath) {
39
62
  if (!headers) {
40
63
  throw new CsvFileMissingHeadersError(csvFilePath);
41
64
  }
65
+ /** Handle no-op WHERE clauses like `1=1` that always evaluate to true. */
66
+ if (where.left.type === 'literal' && where.right.type === 'literal') {
67
+ if (where.left.value === where.right.value) {
68
+ /** Always true - return all rows (excluding headers). */
69
+ return getAllIndexes(csvContents);
70
+ }
71
+ else {
72
+ /** Always false - return no rows. */
73
+ return [];
74
+ }
75
+ }
42
76
  if (where.left.type !== 'identifier' || where.left.variant !== 'column') {
43
77
  throw new Error(`Expected column identifier on left side of '=' operation`);
44
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "csv-sql-engine",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "API for executing SQL statements on CSV files.",
5
5
  "keywords": [
6
6
  "CSV",
File without changes