@kensio/yulin 1.21.21 → 1.21.23
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/service/athena/engine/sim-athena-ast-nodes.d.ts +11 -0
- package/dist/service/athena/engine/sim-athena-ast-nodes.js +22 -0
- package/dist/service/athena/engine/sim-athena-engine-result.d.ts +9 -1
- package/dist/service/athena/engine/sim-athena-engine-result.js +15 -4
- package/dist/service/athena/engine/sim-athena-engine-run.d.ts +2 -0
- package/dist/service/athena/engine/sim-athena-engine-run.js +6 -1
- package/dist/service/athena/engine/sim-athena-query-engine.js +1 -0
- package/dist/service/athena/engine/sim-athena-result-columns.d.ts +1 -1
- package/dist/service/athena/engine/sim-athena-result-columns.js +11 -4
- package/dist/service/athena/engine/sim-athena-round-scales.d.ts +19 -0
- package/dist/service/athena/engine/sim-athena-round-scales.js +80 -0
- package/dist/service/athena/engine/sim-athena-sql-translation.d.ts +9 -0
- package/dist/service/athena/engine/sim-athena-sql-translation.js +17 -2
- package/dist/service/athena/engine/sim-athena-string-shims.js +29 -1
- package/dist/service/athena/engine/sim-athena-unnest-item.js +6 -15
- package/dist/service/athena/engine/sim-athena-unnest-rewrite.d.ts +4 -4
- package/dist/service/athena/engine/sim-athena-unnest-rewrite.js +4 -4
- package/dist/service/athena/engine/sim-athena-unnest-source.d.ts +10 -9
- package/dist/service/athena/engine/sim-athena-unnest-source.js +25 -11
- package/dist/service/athena/sim-athena-flattened-events.fixture.d.ts +3 -0
- package/dist/service/athena/sim-athena-flattened-events.fixture.js +46 -0
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +2 -0
- package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-names.js +1 -5
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.d.ts +5 -0
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.js +7 -0
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-refusals.js +0 -1
- package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping.handler.js +1 -0
- package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.d.ts +2 -1
- package/dist/service/lambda/command/event-source-mapping/event-source-mapping.command.js +0 -6
- package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.d.ts +48 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filter-criteria.js +129 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.d.ts +27 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-delivery.js +15 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.d.ts +24 -0
- package/dist/service/lambda/event-source/filter/sim-lambda-filtered-records.js +35 -0
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.d.ts +7 -4
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.js +9 -6
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.d.ts +16 -4
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-delivery.js +36 -5
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.d.ts +4 -3
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event-source-poller.js +6 -7
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.d.ts +14 -2
- package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-stream-event.js +21 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.d.ts +19 -6
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-delivery.js +41 -7
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.d.ts +7 -3
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.js +10 -9
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.d.ts +6 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event.js +6 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.d.ts +21 -1
- package/dist/service/lambda/event-source/poll/sim-lambda-event-source-delivery.js +32 -2
- package/dist/service/lambda/event-source/poll/sim-lambda-sqs-delivery.js +1 -0
- package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.d.ts +8 -0
- package/dist/service/lambda/event-source/poll/sim-lambda-sqs-event.js +11 -0
- package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.d.ts +10 -0
- package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.js +11 -0
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.d.ts +35 -8
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade-guard.js +35 -12
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.d.ts +13 -4
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-cascade.error.js +12 -6
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.d.ts +18 -0
- package/dist/service/lambda/event-source/stream/sim-lambda-stream-halt.js +22 -0
- package/dist/service/scheduler/command/schedule/schedule.command.d.ts +1 -0
- package/dist/service/scheduler/command/schedule/sim-scheduler-described-schedule.js +1 -0
- package/dist/service/scheduler/command/schedule/sim-scheduler-schedule-writer.js +2 -1
- package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.d.ts +3 -3
- package/dist/service/scheduler/command/schedule/sim-scheduler-unsimulated-input.js +19 -11
- package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.d.ts +5 -3
- package/dist/service/scheduler/schedule/sim-scheduler-schedule-expression.js +6 -4
- package/dist/service/scheduler/schedule/sim-scheduler-schedule.d.ts +9 -0
- package/dist/service/scheduler/schedule/sim-scheduler-schedule.js +8 -0
- package/dist/util/schedule/at/sim-at-expression.d.ts +2 -1
- package/dist/util/schedule/at/sim-at-expression.js +7 -3
- package/dist/util/schedule/cron/sim-cron-expression.d.ts +6 -3
- package/dist/util/schedule/cron/sim-cron-expression.js +24 -21
- package/dist/util/schedule/sim-schedule-zone-clock.d.ts +26 -0
- package/dist/util/schedule/sim-schedule-zone-clock.js +56 -0
- package/dist/util/schedule/sim-schedule-zone.d.ts +58 -0
- package/dist/util/schedule/sim-schedule-zone.js +83 -0
- package/dist/util/schedule/sim-schedule.d.ts +1 -1
- package/dist/util/schedule/sim-schedule.js +6 -5
- package/docs/services/athena/README.md +26 -2
- package/docs/services/lambda/README.md +159 -15
- package/docs/services/scheduler/README.md +35 -6
- package/package.json +1 -1
|
@@ -18,6 +18,17 @@ export interface SimAthenaFromItem extends SimAthenaAstNode {
|
|
|
18
18
|
table: string;
|
|
19
19
|
as?: string | null;
|
|
20
20
|
}
|
|
21
|
+
/** One value as a node, or nothing where it is not an object. */
|
|
22
|
+
export declare function asAstNode(value: unknown): SimAthenaAstNode | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* The name a call carries, where the statement wrote it as one part.
|
|
25
|
+
*
|
|
26
|
+
* The parser holds a name as a list, one entry per piece of a qualified name,
|
|
27
|
+
* and it holds an `UNNEST` alias as a call of its own. A name written in more
|
|
28
|
+
* than one part answers with nothing, since neither the engine's functions nor
|
|
29
|
+
* an alias is reached that way.
|
|
30
|
+
*/
|
|
31
|
+
export declare function simAthenaCalledName(node: SimAthenaAstNode | undefined): string | undefined;
|
|
21
32
|
/** Every node in the tree, the outermost first. */
|
|
22
33
|
export declare function simAthenaAstNodes(root: unknown): Generator<SimAthenaAstNode>;
|
|
23
34
|
/** Whether this node references a column. */
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/** One value as a node, or nothing where it is not an object. */
|
|
2
|
+
export function asAstNode(value) {
|
|
3
|
+
return typeof value === "object" && value !== null
|
|
4
|
+
? value
|
|
5
|
+
: undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The name a call carries, where the statement wrote it as one part.
|
|
9
|
+
*
|
|
10
|
+
* The parser holds a name as a list, one entry per piece of a qualified name,
|
|
11
|
+
* and it holds an `UNNEST` alias as a call of its own. A name written in more
|
|
12
|
+
* than one part answers with nothing, since neither the engine's functions nor
|
|
13
|
+
* an alias is reached that way.
|
|
14
|
+
*/
|
|
15
|
+
export function simAthenaCalledName(node) {
|
|
16
|
+
const parts = asAstNode(node?.["name"])?.["name"];
|
|
17
|
+
const first = Array.isArray(parts) && parts.length === 1
|
|
18
|
+
? asAstNode(parts[0])
|
|
19
|
+
: undefined;
|
|
20
|
+
const name = first?.["value"];
|
|
21
|
+
return typeof name === "string" ? name : undefined;
|
|
22
|
+
}
|
|
1
23
|
/** Every node in the tree, the outermost first. */
|
|
2
24
|
export function* simAthenaAstNodes(root) {
|
|
3
25
|
if (Array.isArray(root)) {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import { SimAthenaResolvedResult } from "../result/sim-athena-resolved-result.js";
|
|
3
3
|
import type { SimAthenaLoadedTable } from "./sim-athena-table-rows.js";
|
|
4
|
+
/** What one statement is run and read back with. */
|
|
5
|
+
export interface SimAthenaEngineResultRun {
|
|
6
|
+
readonly database: DatabaseSync;
|
|
7
|
+
readonly sql: string;
|
|
8
|
+
readonly loaded: readonly SimAthenaLoadedTable[];
|
|
9
|
+
/** The decimal places each result column was rounded to, by position. */
|
|
10
|
+
readonly scales: ReadonlyMap<number, number>;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* Run one translated statement and read its answer back as a result set.
|
|
6
14
|
*
|
|
@@ -8,4 +16,4 @@ import type { SimAthenaLoadedTable } from "./sim-athena-table-rows.js";
|
|
|
8
16
|
* free to answer with two columns of one name and an object would keep one of
|
|
9
17
|
* them.
|
|
10
18
|
*/
|
|
11
|
-
export declare function simAthenaEngineResult(
|
|
19
|
+
export declare function simAthenaEngineResult(run: SimAthenaEngineResultRun): SimAthenaResolvedResult;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SimAthenaResolvedResult } from "../result/sim-athena-resolved-result.js";
|
|
2
2
|
import { simAthenaResultColumns } from "./sim-athena-result-columns.js";
|
|
3
|
+
import { simAthenaRoundedText } from "./sim-athena-round-scales.js";
|
|
3
4
|
/**
|
|
4
5
|
* Run one translated statement and read its answer back as a result set.
|
|
5
6
|
*
|
|
@@ -7,8 +8,8 @@ import { simAthenaResultColumns } from "./sim-athena-result-columns.js";
|
|
|
7
8
|
* free to answer with two columns of one name and an object would keep one of
|
|
8
9
|
* them.
|
|
9
10
|
*/
|
|
10
|
-
export function simAthenaEngineResult(
|
|
11
|
-
const statement = database.prepare(sql);
|
|
11
|
+
export function simAthenaEngineResult(run) {
|
|
12
|
+
const statement = run.database.prepare(run.sql);
|
|
12
13
|
statement.setReturnArrays(true);
|
|
13
14
|
// A whole number comes back as a BigInt, because SQLite holds a 64 bit
|
|
14
15
|
// integer and reading one past what a double represents exactly raises
|
|
@@ -16,12 +17,22 @@ export function simAthenaEngineResult(database, sql, loaded) {
|
|
|
16
17
|
// Every one of them renders the same either way.
|
|
17
18
|
statement.setReadBigInts(true);
|
|
18
19
|
const rows = statement.all();
|
|
19
|
-
const columns = simAthenaResultColumns(statement.columns(), rows, loaded);
|
|
20
|
+
const columns = simAthenaResultColumns(statement.columns(), rows, run.loaded, run.scales);
|
|
20
21
|
return new SimAthenaResolvedResult({
|
|
21
22
|
columns,
|
|
22
|
-
rows: rows.map((row) => columns.map((column, index) =>
|
|
23
|
+
rows: rows.map((row) => columns.map((column, index) => renderedCell(row[index], column, run.scales.get(index)))),
|
|
23
24
|
});
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* One value as `GetQueryResults` carries it, with the scale its column was
|
|
28
|
+
* rounded to where the statement asked for one.
|
|
29
|
+
*/
|
|
30
|
+
function renderedCell(value, column, scale) {
|
|
31
|
+
const text = rendered(value, column);
|
|
32
|
+
return scale === undefined || text === ""
|
|
33
|
+
? text
|
|
34
|
+
: simAthenaRoundedText(value, scale);
|
|
35
|
+
}
|
|
25
36
|
/**
|
|
26
37
|
* One value as `GetQueryResults` carries it.
|
|
27
38
|
*
|
|
@@ -14,6 +14,8 @@ export interface SimAthenaEngineRun {
|
|
|
14
14
|
readonly caller: SimAwsCaller | undefined;
|
|
15
15
|
readonly startedAt: Date;
|
|
16
16
|
readonly sql: string;
|
|
17
|
+
/** The decimal places each result column was rounded to, by position. */
|
|
18
|
+
readonly scales: ReadonlyMap<number, number>;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Load the tables, build the database and run the statement.
|
|
@@ -29,7 +29,12 @@ export async function simAthenaEngineRun(run) {
|
|
|
29
29
|
startedAt: run.startedAt,
|
|
30
30
|
});
|
|
31
31
|
try {
|
|
32
|
-
return simAthenaEngineAnswered(simAthenaEngineResult(
|
|
32
|
+
return simAthenaEngineAnswered(simAthenaEngineResult({
|
|
33
|
+
database,
|
|
34
|
+
sql: run.sql,
|
|
35
|
+
loaded,
|
|
36
|
+
scales: run.scales,
|
|
37
|
+
}));
|
|
33
38
|
}
|
|
34
39
|
finally {
|
|
35
40
|
database.close();
|
|
@@ -15,4 +15,4 @@ import type { SimAthenaLoadedTable } from "./sim-athena-table-rows.js";
|
|
|
15
15
|
* could not have been written as an identifier is how that is told apart from
|
|
16
16
|
* an alias a statement chose.
|
|
17
17
|
*/
|
|
18
|
-
export declare function simAthenaResultColumns(metadata: readonly StatementColumnMetadata[], rows: readonly (readonly SQLOutputValue[])[], loaded: readonly SimAthenaLoadedTable[]): readonly SimAthenaDeclaredColumn[];
|
|
18
|
+
export declare function simAthenaResultColumns(metadata: readonly StatementColumnMetadata[], rows: readonly (readonly SQLOutputValue[])[], loaded: readonly SimAthenaLoadedTable[], scales?: ReadonlyMap<number, number>): readonly SimAthenaDeclaredColumn[];
|
|
@@ -15,15 +15,22 @@ const identifier = /^[A-Za-z_][\dA-Za-z_]*$/u;
|
|
|
15
15
|
* could not have been written as an identifier is how that is told apart from
|
|
16
16
|
* an alias a statement chose.
|
|
17
17
|
*/
|
|
18
|
-
export function simAthenaResultColumns(metadata, rows, loaded) {
|
|
18
|
+
export function simAthenaResultColumns(metadata, rows, loaded, scales = new Map()) {
|
|
19
19
|
const types = glueColumnTypes(loaded);
|
|
20
20
|
return metadata.map((column, index) => ({
|
|
21
21
|
name: nameOf(column, index),
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// A column the statement rounded to a scale is a double whatever value it
|
|
23
|
+
// happens to hold, since a whole number rounded to one place is still one.
|
|
24
|
+
type: scales.has(index)
|
|
25
|
+
? "double"
|
|
26
|
+
: columnType({ column, rows, index, types }),
|
|
25
27
|
}));
|
|
26
28
|
}
|
|
29
|
+
function columnType(read) {
|
|
30
|
+
return read.column.column === null
|
|
31
|
+
? inferredType(read.rows, read.index)
|
|
32
|
+
: simAthenaResultType(read.types.get(originOf(read.column)));
|
|
33
|
+
}
|
|
27
34
|
function nameOf(column, index) {
|
|
28
35
|
const { name } = column;
|
|
29
36
|
if (column.column !== null || identifier.test(name)) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decimal places each result column was rounded to, by position.
|
|
3
|
+
*
|
|
4
|
+
* `round(x, 1)` answers `25.0` on Trino and `25` here, because SQLite hands
|
|
5
|
+
* back a double and nothing downstream knows how many places the statement
|
|
6
|
+
* asked to keep. The statement is the only thing that knows, so the scale is
|
|
7
|
+
* read off the syntax tree while it is still there and travels with the query.
|
|
8
|
+
*
|
|
9
|
+
* Only a literal scale is read. `round(x, n)` over a column is a scale that
|
|
10
|
+
* differs per row, and a result column has one rendering for every row in it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function simAthenaRoundScales(ast: unknown): ReadonlyMap<number, number>;
|
|
13
|
+
/**
|
|
14
|
+
* One rounded value as its column renders it.
|
|
15
|
+
*
|
|
16
|
+
* A whole number keeps the trailing zeros the scale asked for, which is the
|
|
17
|
+
* whole point of carrying the scale this far.
|
|
18
|
+
*/
|
|
19
|
+
export declare function simAthenaRoundedText(value: unknown, scale: number): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isRecord } from "../../../util/type-guard/record.js";
|
|
2
|
+
/**
|
|
3
|
+
* The decimal places each result column was rounded to, by position.
|
|
4
|
+
*
|
|
5
|
+
* `round(x, 1)` answers `25.0` on Trino and `25` here, because SQLite hands
|
|
6
|
+
* back a double and nothing downstream knows how many places the statement
|
|
7
|
+
* asked to keep. The statement is the only thing that knows, so the scale is
|
|
8
|
+
* read off the syntax tree while it is still there and travels with the query.
|
|
9
|
+
*
|
|
10
|
+
* Only a literal scale is read. `round(x, n)` over a column is a scale that
|
|
11
|
+
* differs per row, and a result column has one rendering for every row in it.
|
|
12
|
+
*/
|
|
13
|
+
export function simAthenaRoundScales(ast) {
|
|
14
|
+
const scales = new Map();
|
|
15
|
+
for (const [index, item] of selectItems(ast).entries()) {
|
|
16
|
+
const scale = roundScaleOf(item);
|
|
17
|
+
if (scale !== undefined) {
|
|
18
|
+
scales.set(index, scale);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return scales;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One rounded value as its column renders it.
|
|
25
|
+
*
|
|
26
|
+
* A whole number keeps the trailing zeros the scale asked for, which is the
|
|
27
|
+
* whole point of carrying the scale this far.
|
|
28
|
+
*/
|
|
29
|
+
export function simAthenaRoundedText(value, scale) {
|
|
30
|
+
return typeof value === "number" ? value.toFixed(scale) : String(value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The select list of the statement, which is the first one where a query
|
|
34
|
+
* carries several.
|
|
35
|
+
*/
|
|
36
|
+
function selectItems(ast) {
|
|
37
|
+
const statement = Array.isArray(ast) ? ast[0] : ast;
|
|
38
|
+
const columns = isRecord(statement) ? statement["columns"] : undefined;
|
|
39
|
+
return Array.isArray(columns) ? columns : [];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The scale one select item was rounded to, for an item that is a `round` call
|
|
43
|
+
* over a literal scale.
|
|
44
|
+
*/
|
|
45
|
+
function roundScaleOf(item) {
|
|
46
|
+
const expression = isRecord(item) ? item["expr"] : undefined;
|
|
47
|
+
if (!isRecord(expression) || functionNameOf(expression) !== "round") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const arguments_ = callArguments(expression);
|
|
51
|
+
return arguments_.length === 2 ? literalScale(arguments_[1]) : undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The name a function call node carries, which the parser holds as a list of
|
|
55
|
+
* the parts a qualified name would have.
|
|
56
|
+
*/
|
|
57
|
+
function functionNameOf(expression) {
|
|
58
|
+
const name = isRecord(expression["name"])
|
|
59
|
+
? expression["name"]["name"]
|
|
60
|
+
: undefined;
|
|
61
|
+
const first = Array.isArray(name) ? name[0] : undefined;
|
|
62
|
+
const value = isRecord(first) ? first["value"] : undefined;
|
|
63
|
+
return typeof value === "string" ? value.toLowerCase() : "";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The arguments a function call node carries.
|
|
67
|
+
*/
|
|
68
|
+
function callArguments(expression) {
|
|
69
|
+
const arguments_ = expression["args"];
|
|
70
|
+
const value = isRecord(arguments_) ? arguments_["value"] : undefined;
|
|
71
|
+
return Array.isArray(value) ? value : [];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A whole number written into the statement, or nothing for anything else.
|
|
75
|
+
*/
|
|
76
|
+
function literalScale(node) {
|
|
77
|
+
const value = isRecord(node) && node["type"] === "number" ? node["value"] : undefined;
|
|
78
|
+
const scale = Number(value);
|
|
79
|
+
return Number.isSafeInteger(scale) && scale >= 0 ? scale : undefined;
|
|
80
|
+
}
|
|
@@ -3,14 +3,23 @@ import type { SimAthenaSqlParser } from "./sim-athena-parser-module.js";
|
|
|
3
3
|
/** One statement translated for SQLite, or why it was not. */
|
|
4
4
|
export type SimAthenaTranslatedSql = {
|
|
5
5
|
readonly sql: string;
|
|
6
|
+
/**
|
|
7
|
+
* The decimal places each result column was rounded to, by position.
|
|
8
|
+
*
|
|
9
|
+
* Read here because the syntax tree is the only place that knows, and
|
|
10
|
+
* the tree is gone by the time the answer is rendered.
|
|
11
|
+
*/
|
|
12
|
+
readonly scales: ReadonlyMap<number, number>;
|
|
6
13
|
readonly turnedDown: undefined;
|
|
7
14
|
readonly rejected: undefined;
|
|
8
15
|
} | {
|
|
9
16
|
readonly sql: undefined;
|
|
17
|
+
readonly scales: undefined;
|
|
10
18
|
readonly turnedDown: string;
|
|
11
19
|
readonly rejected: undefined;
|
|
12
20
|
} | {
|
|
13
21
|
readonly sql: undefined;
|
|
22
|
+
readonly scales: undefined;
|
|
14
23
|
readonly turnedDown: undefined;
|
|
15
24
|
readonly rejected: string;
|
|
16
25
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { simAthenaSqlForParser, simAthenaSqlForSqlite, } from "./sim-athena-sql-rewrites.js";
|
|
2
2
|
import { simAthenaUnparsedStatement, simAthenaUnrewrittenUnnest, simAthenaUnwrittenStatement, } from "./sim-athena-turn-down.js";
|
|
3
3
|
import { simAthenaRejectedStatement } from "./sim-athena-rejected-statement.js";
|
|
4
|
+
import { simAthenaRoundScales } from "./sim-athena-round-scales.js";
|
|
4
5
|
import { simAthenaRewriteUnnest } from "./sim-athena-unnest-rewrite.js";
|
|
5
6
|
/**
|
|
6
7
|
* One Athena statement written back out for SQLite, or why it is not.
|
|
@@ -29,7 +30,12 @@ export function simAthenaSqliteSql(request) {
|
|
|
29
30
|
}
|
|
30
31
|
const rejected = simAthenaRejectedStatement(ast);
|
|
31
32
|
if (rejected !== undefined) {
|
|
32
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
sql: undefined,
|
|
35
|
+
scales: undefined,
|
|
36
|
+
turnedDown: undefined,
|
|
37
|
+
rejected,
|
|
38
|
+
};
|
|
33
39
|
}
|
|
34
40
|
try {
|
|
35
41
|
if (!simAthenaRewriteUnnest({
|
|
@@ -39,8 +45,12 @@ export function simAthenaSqliteSql(request) {
|
|
|
39
45
|
})) {
|
|
40
46
|
return turnedDown(simAthenaUnrewrittenUnnest);
|
|
41
47
|
}
|
|
48
|
+
// Read before the tree is written back out, since the rewrites above can
|
|
49
|
+
// move a select item and the scale travels by position.
|
|
50
|
+
const scales = simAthenaRoundScales(ast);
|
|
42
51
|
return {
|
|
43
52
|
sql: simAthenaSqlForSqlite(parser.sqlify(ast, { database: "sqlite" })),
|
|
53
|
+
scales,
|
|
44
54
|
turnedDown: undefined,
|
|
45
55
|
rejected: undefined,
|
|
46
56
|
};
|
|
@@ -50,5 +60,10 @@ export function simAthenaSqliteSql(request) {
|
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
62
|
function turnedDown(reason) {
|
|
53
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
sql: undefined,
|
|
65
|
+
scales: undefined,
|
|
66
|
+
turnedDown: reason,
|
|
67
|
+
rejected: undefined,
|
|
68
|
+
};
|
|
54
69
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
|
|
2
|
+
import { isExplicitNull, shimNumber, shimText, simAthenaScalarShim, } from "./sim-athena-shim-registry.js";
|
|
2
3
|
/**
|
|
3
4
|
* Trino's string functions.
|
|
4
5
|
*
|
|
@@ -8,9 +9,36 @@ import { shimNumber, shimText, simAthenaScalarShim, } from "./sim-athena-shim-re
|
|
|
8
9
|
* with something that works less well.
|
|
9
10
|
*/
|
|
10
11
|
export function simAthenaInstallStringShims(database) {
|
|
12
|
+
simAthenaScalarShim(database, "split", (...values) => isExplicitNull(values, 2)
|
|
13
|
+
? null
|
|
14
|
+
: split(shimText(values.at(0)), shimText(values.at(1)), shimNumber(values.at(2))));
|
|
11
15
|
simAthenaScalarShim(database, "split_part", (value, delimiter, index) => splitPart(shimText(value), shimText(delimiter), shimNumber(index)));
|
|
12
16
|
simAthenaScalarShim(database, "strpos", (value, search) => position(shimText(value), shimText(search)));
|
|
13
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* One value cut into an array, written as the JSON text an array is held as.
|
|
20
|
+
*
|
|
21
|
+
* Trino answers with one empty element over an empty value, and refuses an
|
|
22
|
+
* empty delimiter rather than answering a character at a time. A limit caps
|
|
23
|
+
* how many elements come back and leaves the rest of the value in the last of
|
|
24
|
+
* them.
|
|
25
|
+
*/
|
|
26
|
+
function split(value, delimiter, limit) {
|
|
27
|
+
if (value === undefined || delimiter === undefined) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (delimiter === "" || (limit !== undefined && limit < 1)) {
|
|
31
|
+
throw new SimAthenaSetUpError("split takes a delimiter of at least one character and a limit from one");
|
|
32
|
+
}
|
|
33
|
+
const parts = value.split(delimiter);
|
|
34
|
+
if (limit === undefined || parts.length <= limit) {
|
|
35
|
+
return JSON.stringify(parts);
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify([
|
|
38
|
+
...parts.slice(0, limit - 1),
|
|
39
|
+
parts.slice(limit - 1).join(delimiter),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
14
42
|
/** Trino counts the fields from one, and has no field below that. */
|
|
15
43
|
function splitPart(value, delimiter, index) {
|
|
16
44
|
if (value === undefined || delimiter === undefined || index === undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { simAthenaAstNodes, } from "./sim-athena-ast-nodes.js";
|
|
1
|
+
import { asAstNode, simAthenaAstNodes, simAthenaCalledName, } from "./sim-athena-ast-nodes.js";
|
|
2
2
|
/**
|
|
3
3
|
* The `UNNEST` a statement flattens an array or a map with.
|
|
4
4
|
*
|
|
@@ -19,8 +19,8 @@ export function simAthenaUnnestItem(ast) {
|
|
|
19
19
|
}
|
|
20
20
|
const alias = aliasOf(item);
|
|
21
21
|
const columns = aliasColumns(item);
|
|
22
|
-
const source = item["expr"];
|
|
23
|
-
if (alias === undefined || columns === undefined ||
|
|
22
|
+
const source = asAstNode(item["expr"]);
|
|
23
|
+
if (alias === undefined || columns === undefined || source === undefined) {
|
|
24
24
|
return { kind: "unreadable" };
|
|
25
25
|
}
|
|
26
26
|
return { kind: "found", item, alias, columns, source };
|
|
@@ -38,25 +38,16 @@ function isCrossJoined(item) {
|
|
|
38
38
|
}
|
|
39
39
|
/** The alias name, which the parser holds as a function's name. */
|
|
40
40
|
function aliasOf(item) {
|
|
41
|
-
|
|
42
|
-
const first = Array.isArray(parts) ? asNode(parts[0]) : undefined;
|
|
43
|
-
const value = first?.["value"];
|
|
44
|
-
return typeof value === "string" ? value : undefined;
|
|
41
|
+
return simAthenaCalledName(asAstNode(item["as"]));
|
|
45
42
|
}
|
|
46
43
|
/** The names inside the alias, which the parser holds as a function's arguments. */
|
|
47
44
|
function aliasColumns(item) {
|
|
48
|
-
const values =
|
|
45
|
+
const values = asAstNode(asAstNode(item["as"])?.["args"])?.["value"];
|
|
49
46
|
if (!Array.isArray(values) || values.length === 0) {
|
|
50
47
|
return undefined;
|
|
51
48
|
}
|
|
52
|
-
const columns = values.map((value) =>
|
|
49
|
+
const columns = values.map((value) => asAstNode(value)?.["column"]);
|
|
53
50
|
return columns.every((column) => typeof column === "string")
|
|
54
51
|
? columns
|
|
55
52
|
: undefined;
|
|
56
53
|
}
|
|
57
|
-
function isNode(value) {
|
|
58
|
-
return typeof value === "object" && value !== null;
|
|
59
|
-
}
|
|
60
|
-
function asNode(value) {
|
|
61
|
-
return isNode(value) ? value : undefined;
|
|
62
|
-
}
|
|
@@ -7,10 +7,10 @@ interface SimAthenaUnnestRewriteRequest {
|
|
|
7
7
|
/**
|
|
8
8
|
* Turn an `UNNEST` into the `json_each` SQLite flattens JSON with.
|
|
9
9
|
*
|
|
10
|
-
* An array or a map column is held as its JSON text,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* answers with.
|
|
10
|
+
* An array or a map column is held as its JSON text, as is what a function like
|
|
11
|
+
* `split` answers, and `json_each` reads that as one row per element with a
|
|
12
|
+
* `key` and a `value`. The `FROM` entry becomes a call to it and every
|
|
13
|
+
* reference to the alias is pointed at the column it answers with.
|
|
14
14
|
*
|
|
15
15
|
* Answers with whether the statement can run. A statement carrying no `UNNEST`
|
|
16
16
|
* runs untouched, and one carrying an `UNNEST` this cannot turn into
|
|
@@ -6,10 +6,10 @@ import { simAthenaUnnestTargets } from "./sim-athena-unnest-targets.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* Turn an `UNNEST` into the `json_each` SQLite flattens JSON with.
|
|
8
8
|
*
|
|
9
|
-
* An array or a map column is held as its JSON text,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* answers with.
|
|
9
|
+
* An array or a map column is held as its JSON text, as is what a function like
|
|
10
|
+
* `split` answers, and `json_each` reads that as one row per element with a
|
|
11
|
+
* `key` and a `value`. The `FROM` entry becomes a call to it and every
|
|
12
|
+
* reference to the alias is pointed at the column it answers with.
|
|
13
13
|
*
|
|
14
14
|
* Answers with whether the statement can run. A statement carrying no `UNNEST`
|
|
15
15
|
* runs untouched, and one carrying an `UNNEST` this cannot turn into
|
|
@@ -3,17 +3,18 @@ import { type SimAthenaAstNode } from "./sim-athena-ast-nodes.js";
|
|
|
3
3
|
/** The two things Athena flattens with `UNNEST`. */
|
|
4
4
|
export type SimAthenaUnnestKind = "array" | "map";
|
|
5
5
|
/**
|
|
6
|
-
* What the statement is flattening
|
|
6
|
+
* What the statement is flattening.
|
|
7
7
|
*
|
|
8
|
-
* The catalog is the only thing that says
|
|
9
|
-
* map or a scalar, and `json_each` answers
|
|
10
|
-
* two. A column the schema calls anything
|
|
11
|
-
* the query falls back rather than reading
|
|
12
|
-
* was.
|
|
8
|
+
* A column is read off the Glue schema. The catalog is the only thing that says
|
|
9
|
+
* whether a column holds an array, a map or a scalar, and `json_each` answers
|
|
10
|
+
* with different columns for the first two. A column the schema calls anything
|
|
11
|
+
* else answers with nothing here, and the query falls back rather than reading
|
|
12
|
+
* a value as a collection it never was.
|
|
13
13
|
*
|
|
14
|
-
* An expression
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* An expression is read off the function it calls. `UNNEST(split(x, ','))` is
|
|
15
|
+
* how a statement flattens a delimited string, and nothing but the call says
|
|
16
|
+
* what it comes to. A call to anything else answers with nothing, since the
|
|
17
|
+
* flattening would otherwise run over whatever SQLite made of a scalar.
|
|
17
18
|
*/
|
|
18
19
|
export declare function simAthenaUnnestKind(source: SimAthenaAstNode, ast: unknown, tables: readonly SimAthenaCatalogTable[]): SimAthenaUnnestKind | undefined;
|
|
19
20
|
/** Every catalog table the statement reads, by the name it reaches each one by. */
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import { isColumnRef, isFromItem, simAthenaAstNodes, } from "./sim-athena-ast-nodes.js";
|
|
1
|
+
import { isColumnRef, isFromItem, simAthenaAstNodes, simAthenaCalledName, } from "./sim-athena-ast-nodes.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The engine's own functions that answer an array.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
* An expression has no catalog entry to read, so the call itself is what says
|
|
6
|
+
* the value is a collection. Each of these answers with the JSON text an array
|
|
7
|
+
* column is held as, which is what `json_each` reads.
|
|
8
|
+
*/
|
|
9
|
+
const arrayFunctions = new Set(["split", "slice"]);
|
|
10
|
+
/**
|
|
11
|
+
* What the statement is flattening.
|
|
12
|
+
*
|
|
13
|
+
* A column is read off the Glue schema. The catalog is the only thing that says
|
|
14
|
+
* whether a column holds an array, a map or a scalar, and `json_each` answers
|
|
15
|
+
* with different columns for the first two. A column the schema calls anything
|
|
16
|
+
* else answers with nothing here, and the query falls back rather than reading
|
|
17
|
+
* a value as a collection it never was.
|
|
10
18
|
*
|
|
11
|
-
* An expression
|
|
12
|
-
*
|
|
13
|
-
*
|
|
19
|
+
* An expression is read off the function it calls. `UNNEST(split(x, ','))` is
|
|
20
|
+
* how a statement flattens a delimited string, and nothing but the call says
|
|
21
|
+
* what it comes to. A call to anything else answers with nothing, since the
|
|
22
|
+
* flattening would otherwise run over whatever SQLite made of a scalar.
|
|
14
23
|
*/
|
|
15
24
|
export function simAthenaUnnestKind(source, ast, tables) {
|
|
16
25
|
if (!isColumnRef(source)) {
|
|
17
|
-
return undefined;
|
|
26
|
+
return answersArray(source) ? "array" : undefined;
|
|
18
27
|
}
|
|
19
28
|
const declared = declaredType(source, ast, tables);
|
|
20
29
|
if (declared === undefined) {
|
|
@@ -26,6 +35,11 @@ export function simAthenaUnnestKind(source, ast, tables) {
|
|
|
26
35
|
}
|
|
27
36
|
return type.startsWith("map") ? "map" : undefined;
|
|
28
37
|
}
|
|
38
|
+
/** Whether this expression is a call to a function answering an array. */
|
|
39
|
+
function answersArray(source) {
|
|
40
|
+
const name = source["type"] === "function" ? simAthenaCalledName(source) : undefined;
|
|
41
|
+
return name !== undefined && arrayFunctions.has(name.toLowerCase());
|
|
42
|
+
}
|
|
29
43
|
/** Every catalog table the statement reads, by the name it reaches each one by. */
|
|
30
44
|
export function simAthenaQueryTables(ast, tables) {
|
|
31
45
|
const byName = new Map();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { aCatalogTable, anEngineSimulation, aSeededJson, } from "./sim-athena-engine.fixture.js";
|
|
2
|
+
/**
|
|
3
|
+
* The events a flattening query reads.
|
|
4
|
+
*
|
|
5
|
+
* Each holds a collection of every shape an `UNNEST` reaches. An array and a
|
|
6
|
+
* map that are empty, a delimited string that is empty, and a row carrying no
|
|
7
|
+
* delimited string at all, which is what a flattening answering no rows runs
|
|
8
|
+
* over.
|
|
9
|
+
*/
|
|
10
|
+
const events = [
|
|
11
|
+
{
|
|
12
|
+
id: 1,
|
|
13
|
+
tags: ["red", "blue"],
|
|
14
|
+
attrs: { size: "large" },
|
|
15
|
+
name: "one",
|
|
16
|
+
packed: "red;blue",
|
|
17
|
+
},
|
|
18
|
+
{ id: 2, tags: [], attrs: {}, name: "two", packed: "" },
|
|
19
|
+
{
|
|
20
|
+
id: 3,
|
|
21
|
+
tags: ["green"],
|
|
22
|
+
attrs: { size: "small", colour: "green" },
|
|
23
|
+
name: "three",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
/** A simulation holding the events, with the engine on. */
|
|
27
|
+
export async function anEventSimulation() {
|
|
28
|
+
const simulation = await anEngineSimulation();
|
|
29
|
+
aCatalogTable(simulation.simAws, {
|
|
30
|
+
name: "events",
|
|
31
|
+
columns: [
|
|
32
|
+
{ Name: "id", Type: "int" },
|
|
33
|
+
{ Name: "tags", Type: "array<string>" },
|
|
34
|
+
{ Name: "attrs", Type: "map<string,string>" },
|
|
35
|
+
{ Name: "name", Type: "string" },
|
|
36
|
+
{ Name: "packed", Type: "string" },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
await aSeededJson(simulation.simAws, "events/part-0.json", events);
|
|
40
|
+
await simulation.simAws.athena().engine().enable();
|
|
41
|
+
simulation.simAws
|
|
42
|
+
.athena()
|
|
43
|
+
.results()
|
|
44
|
+
.byDefault({ columns: ["fallback"], rows: [["declared"]] });
|
|
45
|
+
return simulation;
|
|
46
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simLambdaFilterCriteriaInput } from "../../event-source/filter/sim-lambda-filter-criteria.js";
|
|
1
2
|
import { simLambdaStreamDestinationConfig } from "../../event-source/sim-lambda-stream-destination-config.js";
|
|
2
3
|
import { SimCfnLambdaPropertyParser } from "../function/sim-cfn-lambda-property-parser.js";
|
|
3
4
|
import { recordUnsimulatedEventSourceMappingProperties } from "./sim-cfn-lambda-event-source-mapping-property-rules.js";
|
|
@@ -46,6 +47,7 @@ export class SimCfnLambdaEventSourceMappingProperties {
|
|
|
46
47
|
StartingPosition: this.parser.optionalString(this.resource, this.properties["StartingPosition"], "StartingPosition"),
|
|
47
48
|
StartingPositionTimestamp: this.startingPositionTimestamp(),
|
|
48
49
|
FunctionResponseTypes: this.functionResponseTypes(),
|
|
50
|
+
FilterCriteria: simLambdaFilterCriteriaInput(this.properties["FilterCriteria"]),
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
/**
|
|
@@ -44,13 +44,9 @@ const absentEventSource = "the mapping polls the source its EventSourceArn names
|
|
|
44
44
|
export const unsimulatedPropertyReasons = new Map([
|
|
45
45
|
["AmazonManagedKafkaEventSourceConfig", absentEventSource],
|
|
46
46
|
["DocumentDBEventSourceConfig", absentEventSource],
|
|
47
|
-
[
|
|
48
|
-
"FilterCriteria",
|
|
49
|
-
"every record on the event source is delivered to the function, unfiltered",
|
|
50
|
-
],
|
|
51
47
|
[
|
|
52
48
|
"KmsKeyArn",
|
|
53
|
-
"
|
|
49
|
+
"filter criteria are simulated in plaintext, and encrypting them is not",
|
|
54
50
|
],
|
|
55
51
|
["MetricsConfig", "the mapping publishes no CloudWatch metrics"],
|
|
56
52
|
[
|