@kensio/yulin 1.21.29 → 1.21.31
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-array-shims.js +1 -1
- package/dist/service/athena/engine/sim-athena-binary-text.d.ts +2 -2
- package/dist/service/athena/engine/sim-athena-binary-text.js +2 -2
- package/dist/service/athena/engine/sim-athena-cast-rewrite.d.ts +30 -0
- package/dist/service/athena/engine/sim-athena-cast-rewrite.js +107 -0
- package/dist/service/athena/engine/sim-athena-cast-shims.d.ts +12 -0
- package/dist/service/athena/engine/sim-athena-cast-shims.js +29 -0
- package/dist/service/athena/engine/sim-athena-cast-text.d.ts +13 -0
- package/dist/service/athena/engine/sim-athena-cast-text.js +66 -0
- package/dist/service/athena/engine/sim-athena-comparison-shims.js +1 -2
- package/dist/service/athena/engine/sim-athena-date-arithmetic-shims.d.ts +4 -5
- package/dist/service/athena/engine/sim-athena-date-arithmetic-shims.js +6 -14
- package/dist/service/athena/engine/sim-athena-date-units.d.ts +1 -1
- package/dist/service/athena/engine/sim-athena-date-units.js +1 -1
- package/dist/service/athena/engine/sim-athena-engine-answer.d.ts +10 -5
- package/dist/service/athena/engine/sim-athena-engine-answer.js +4 -4
- package/dist/service/athena/engine/sim-athena-engine-run.d.ts +4 -0
- package/dist/service/athena/engine/sim-athena-engine-run.js +10 -2
- package/dist/service/athena/engine/sim-athena-regexp-shims.js +2 -2
- package/dist/service/athena/engine/sim-athena-shim-refusal.d.ts +17 -0
- package/dist/service/athena/engine/sim-athena-shim-refusal.js +22 -0
- package/dist/service/athena/engine/sim-athena-shim-registry.d.ts +5 -1
- package/dist/service/athena/engine/sim-athena-shim-registry.js +14 -2
- package/dist/service/athena/engine/sim-athena-shims.js +2 -0
- package/dist/service/athena/engine/sim-athena-sql-translation.d.ts +1 -23
- package/dist/service/athena/engine/sim-athena-sql-translation.js +8 -23
- package/dist/service/athena/engine/sim-athena-translated-sql.d.ts +29 -0
- package/dist/service/athena/engine/sim-athena-translated-sql.js +12 -0
- package/dist/service/athena/engine/sim-athena-turn-down.d.ts +7 -0
- package/dist/service/athena/engine/sim-athena-turn-down.js +11 -0
- package/dist/service/athena/result/sim-athena-query-answer.d.ts +2 -1
- package/dist/service/athena/result/sim-athena-query-answer.js +2 -1
- package/dist/service/athena/sim-athena-snapshot-table.fixture.d.ts +10 -0
- package/dist/service/athena/sim-athena-snapshot-table.fixture.js +39 -0
- package/dist/service/lambda/function/outbound/sim-lambda-aws-api-outbound.d.ts +7 -1
- package/dist/service/lambda/function/outbound/sim-lambda-aws-api-outbound.js +15 -7
- package/dist/service/lambda/function/outbound/sim-lambda-http2-session.d.ts +3 -0
- package/dist/service/lambda/function/outbound/sim-lambda-http2-session.js +4 -0
- package/docs/services/athena/README.md +29 -8
- package/package.json +1 -1
|
@@ -57,7 +57,7 @@ function joined(array, delimiter, absent) {
|
|
|
57
57
|
* One element written out.
|
|
58
58
|
*
|
|
59
59
|
* Trino refuses to join an array whose elements are not text, and raising here
|
|
60
|
-
*
|
|
60
|
+
* fails the query the same way.
|
|
61
61
|
*/
|
|
62
62
|
function scalarText(value) {
|
|
63
63
|
if (typeof value === "string") {
|
|
@@ -3,8 +3,8 @@ import type { SQLOutputValue } from "node:sqlite";
|
|
|
3
3
|
* One encoded string read back to bytes, or null where it is null.
|
|
4
4
|
*
|
|
5
5
|
* Trino fails the query over text the encoding cannot carry, and `Buffer` reads
|
|
6
|
-
* as much of it as it can and drops the rest. Raising here
|
|
7
|
-
*
|
|
6
|
+
* as much of it as it can and drops the rest. Raising here fails the query the
|
|
7
|
+
* way Trino does.
|
|
8
8
|
*/
|
|
9
9
|
export declare function simAthenaDecodedText(text: string | undefined, encoding: "base64" | "hex"): Uint8Array | null;
|
|
10
10
|
/**
|
|
@@ -20,8 +20,8 @@ const encodedReplacement = Buffer.from([0xef, 0xbf, 0xbd]);
|
|
|
20
20
|
* One encoded string read back to bytes, or null where it is null.
|
|
21
21
|
*
|
|
22
22
|
* Trino fails the query over text the encoding cannot carry, and `Buffer` reads
|
|
23
|
-
* as much of it as it can and drops the rest. Raising here
|
|
24
|
-
*
|
|
23
|
+
* as much of it as it can and drops the rest. Raising here fails the query the
|
|
24
|
+
* way Trino does.
|
|
25
25
|
*/
|
|
26
26
|
export function simAthenaDecodedText(text, encoding) {
|
|
27
27
|
if (text === undefined) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SimAthenaCatalogTable } from "../table/sim-athena-catalog-table.js";
|
|
2
|
+
/**
|
|
3
|
+
* The shims a cast to `DATE` and to `TIMESTAMP` are handed to.
|
|
4
|
+
*
|
|
5
|
+
* The names have the simulator's prefix so that neither can collide with a
|
|
6
|
+
* Trino function a statement calls itself.
|
|
7
|
+
*/
|
|
8
|
+
export declare const simAthenaDateCast = "sim_athena_cast_date";
|
|
9
|
+
export declare const simAthenaTimestampCast = "sim_athena_cast_timestamp";
|
|
10
|
+
/** The second argument a shim is handed where the value it casts is text. */
|
|
11
|
+
export declare const simAthenaCastSource = "varchar";
|
|
12
|
+
/**
|
|
13
|
+
* Hand every cast to `DATE` or `TIMESTAMP` in the tree to the engine's shims.
|
|
14
|
+
*
|
|
15
|
+
* SQLite gives a column declared `DATE` or `TIMESTAMP` numeric affinity, so a
|
|
16
|
+
* plain `CAST('2026-08-15' AS DATE)` keeps the leading number and answers
|
|
17
|
+
* `2026`. Every date function downstream of that cast then reads a year where
|
|
18
|
+
* it expected a date.
|
|
19
|
+
*
|
|
20
|
+
* Trino's one-argument `date(x)` is the cast to `DATE` under another name,
|
|
21
|
+
* and it goes to the same shim. SQLite's own `date()` answers null over text
|
|
22
|
+
* that is no date, where Trino fails the cast.
|
|
23
|
+
*
|
|
24
|
+
* Trino reads text more strictly than a value already typed as a timestamp,
|
|
25
|
+
* and the shim is told which it has. A string literal and a column the catalog
|
|
26
|
+
* declares as text are text. Anything else is taken to be a date or a
|
|
27
|
+
* timestamp already, since the engine cannot tell the type a function answers
|
|
28
|
+
* with.
|
|
29
|
+
*/
|
|
30
|
+
export declare function simAthenaRewriteCasts(ast: unknown, tables: readonly SimAthenaCatalogTable[]): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { asAstNode, isColumnRef, replaceNode, simAthenaAstNodes, simAthenaCalledName, } from "./sim-athena-ast-nodes.js";
|
|
2
|
+
/**
|
|
3
|
+
* The shims a cast to `DATE` and to `TIMESTAMP` are handed to.
|
|
4
|
+
*
|
|
5
|
+
* The names have the simulator's prefix so that neither can collide with a
|
|
6
|
+
* Trino function a statement calls itself.
|
|
7
|
+
*/
|
|
8
|
+
export const simAthenaDateCast = "sim_athena_cast_date";
|
|
9
|
+
export const simAthenaTimestampCast = "sim_athena_cast_timestamp";
|
|
10
|
+
/** The second argument a shim is handed where the value it casts is text. */
|
|
11
|
+
export const simAthenaCastSource = "varchar";
|
|
12
|
+
/** The shim each cast target goes to. */
|
|
13
|
+
const castShims = new Map([
|
|
14
|
+
["DATE", simAthenaDateCast],
|
|
15
|
+
["TIMESTAMP", simAthenaTimestampCast],
|
|
16
|
+
]);
|
|
17
|
+
/** The Glue types a column of text is declared with. */
|
|
18
|
+
const textTypes = /^(?:string|varchar|char)\b/iu;
|
|
19
|
+
/**
|
|
20
|
+
* Hand every cast to `DATE` or `TIMESTAMP` in the tree to the engine's shims.
|
|
21
|
+
*
|
|
22
|
+
* SQLite gives a column declared `DATE` or `TIMESTAMP` numeric affinity, so a
|
|
23
|
+
* plain `CAST('2026-08-15' AS DATE)` keeps the leading number and answers
|
|
24
|
+
* `2026`. Every date function downstream of that cast then reads a year where
|
|
25
|
+
* it expected a date.
|
|
26
|
+
*
|
|
27
|
+
* Trino's one-argument `date(x)` is the cast to `DATE` under another name,
|
|
28
|
+
* and it goes to the same shim. SQLite's own `date()` answers null over text
|
|
29
|
+
* that is no date, where Trino fails the cast.
|
|
30
|
+
*
|
|
31
|
+
* Trino reads text more strictly than a value already typed as a timestamp,
|
|
32
|
+
* and the shim is told which it has. A string literal and a column the catalog
|
|
33
|
+
* declares as text are text. Anything else is taken to be a date or a
|
|
34
|
+
* timestamp already, since the engine cannot tell the type a function answers
|
|
35
|
+
* with.
|
|
36
|
+
*/
|
|
37
|
+
export function simAthenaRewriteCasts(ast, tables) {
|
|
38
|
+
const textColumns = textColumnsOf(tables);
|
|
39
|
+
for (const node of simAthenaAstNodes(ast)) {
|
|
40
|
+
const cast = castOf(node);
|
|
41
|
+
if (cast !== undefined) {
|
|
42
|
+
replaceNode(node, {
|
|
43
|
+
type: "function",
|
|
44
|
+
name: { name: [{ type: "default", value: cast.shim }] },
|
|
45
|
+
args: {
|
|
46
|
+
type: "expr_list",
|
|
47
|
+
value: [cast.value, sourceOf(cast.value, textColumns)],
|
|
48
|
+
},
|
|
49
|
+
over: null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function castOf(node) {
|
|
55
|
+
if (node["type"] === "cast") {
|
|
56
|
+
const shim = castShims.get(targetOf(node["target"]));
|
|
57
|
+
return shim === undefined ? undefined : { shim, value: node["expr"] };
|
|
58
|
+
}
|
|
59
|
+
const values = asAstNode(node["args"])?.["value"];
|
|
60
|
+
if (node["type"] !== "function" ||
|
|
61
|
+
simAthenaCalledName(node)?.toLowerCase() !== "date" ||
|
|
62
|
+
!Array.isArray(values) ||
|
|
63
|
+
values.length !== 1) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return { shim: simAthenaDateCast, value: values[0] };
|
|
67
|
+
}
|
|
68
|
+
/** The one type a cast names, upper cased, or nothing for anything else. */
|
|
69
|
+
function targetOf(target) {
|
|
70
|
+
const only = Array.isArray(target) && target.length === 1 ? target[0] : null;
|
|
71
|
+
const dataType = asAstNode(only)?.["dataType"];
|
|
72
|
+
return typeof dataType === "string" ? dataType.toUpperCase() : "";
|
|
73
|
+
}
|
|
74
|
+
/** The second argument that tells the shim whether its value is text. */
|
|
75
|
+
function sourceOf(value, textColumns) {
|
|
76
|
+
const node = asAstNode(value);
|
|
77
|
+
const isText = node?.["type"] === "single_quote_string" ||
|
|
78
|
+
(node !== undefined &&
|
|
79
|
+
isColumnRef(node) &&
|
|
80
|
+
textColumns.has(node.column.toLowerCase()));
|
|
81
|
+
return {
|
|
82
|
+
type: "single_quote_string",
|
|
83
|
+
value: isText ? simAthenaCastSource : "",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every column name the tables declare as text and as nothing else.
|
|
88
|
+
*
|
|
89
|
+
* A name two tables declare with different types is left out, since the
|
|
90
|
+
* statement could mean either and only text is held to the stricter read.
|
|
91
|
+
*/
|
|
92
|
+
function textColumnsOf(tables) {
|
|
93
|
+
const text = new Set();
|
|
94
|
+
const other = new Set();
|
|
95
|
+
for (const table of tables) {
|
|
96
|
+
for (const column of [...table.columns, ...table.partitionKeys]) {
|
|
97
|
+
const name = column.Name.toLowerCase();
|
|
98
|
+
if (textTypes.test(column.Type ?? "")) {
|
|
99
|
+
text.add(name);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
other.add(name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return text.difference(other);
|
|
107
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
/**
|
|
3
|
+
* Trino's casts to `DATE` and `TIMESTAMP`, which SQLite cannot do itself.
|
|
4
|
+
*
|
|
5
|
+
* SQLite gives both types numeric affinity, so its own cast of
|
|
6
|
+
* `'2026-08-15'` answers `2026`. The statement's casts are rewritten onto
|
|
7
|
+
* these shims before it runs, and the rewrite says whether the value is text.
|
|
8
|
+
*
|
|
9
|
+
* A value the cast cannot read fails the query with Trino's own message, the
|
|
10
|
+
* way Athena fails it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function simAthenaInstallCastShims(database: DatabaseSync): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
|
|
2
|
+
import { simAthenaCastSource, simAthenaDateCast, simAthenaTimestampCast, } from "./sim-athena-cast-rewrite.js";
|
|
3
|
+
import { simAthenaCastInstant, simAthenaCastRendered, } from "./sim-athena-cast-text.js";
|
|
4
|
+
import { shimText, simAthenaScalarShim } from "./sim-athena-shim-registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Trino's casts to `DATE` and `TIMESTAMP`, which SQLite cannot do itself.
|
|
7
|
+
*
|
|
8
|
+
* SQLite gives both types numeric affinity, so its own cast of
|
|
9
|
+
* `'2026-08-15'` answers `2026`. The statement's casts are rewritten onto
|
|
10
|
+
* these shims before it runs, and the rewrite says whether the value is text.
|
|
11
|
+
*
|
|
12
|
+
* A value the cast cannot read fails the query with Trino's own message, the
|
|
13
|
+
* way Athena fails it.
|
|
14
|
+
*/
|
|
15
|
+
export function simAthenaInstallCastShims(database) {
|
|
16
|
+
simAthenaScalarShim(database, simAthenaDateCast, (value, source) => cast(value, source, "date"), "CAST(... AS DATE)");
|
|
17
|
+
simAthenaScalarShim(database, simAthenaTimestampCast, (value, source) => cast(value, source, "timestamp"), "CAST(... AS TIMESTAMP)");
|
|
18
|
+
}
|
|
19
|
+
function cast(value, source, type) {
|
|
20
|
+
const text = shimText(value);
|
|
21
|
+
if (text === undefined) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const instant = simAthenaCastInstant(text, type, source === simAthenaCastSource);
|
|
25
|
+
if (instant === undefined) {
|
|
26
|
+
throw new SimAthenaSetUpError(`Value cannot be cast to ${type}: ${text}`);
|
|
27
|
+
}
|
|
28
|
+
return simAthenaCastRendered(instant, type);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The two types a cast in the engine reads a value as. */
|
|
2
|
+
export type SimAthenaCastType = "date" | "timestamp";
|
|
3
|
+
/**
|
|
4
|
+
* The instant one value names as the cast reads it, or nothing where the cast
|
|
5
|
+
* fails.
|
|
6
|
+
*
|
|
7
|
+
* Text is held to the formats Trino's cast from `varchar` takes. Any other
|
|
8
|
+
* source is a value some column or function already typed as a date or a
|
|
9
|
+
* timestamp, written as the ISO-8601 text the engine holds one as.
|
|
10
|
+
*/
|
|
11
|
+
export declare function simAthenaCastInstant(text: string, type: SimAthenaCastType, fromText: boolean): number | undefined;
|
|
12
|
+
/** One instant rendered as Athena renders a value of the type. */
|
|
13
|
+
export declare function simAthenaCastRendered(instant: number, type: SimAthenaCastType): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Both patterns below are anchored at each end with every quantifier bounded,
|
|
2
|
+
// so there is nothing in either to backtrack over.
|
|
3
|
+
// oxlint-disable security/detect-unsafe-regex
|
|
4
|
+
import { simAthenaInstant } from "./sim-athena-timestamp-text.js";
|
|
5
|
+
/**
|
|
6
|
+
* What Trino's cast from `varchar` to `DATE` reads, once the text is trimmed.
|
|
7
|
+
*
|
|
8
|
+
* A time of day is refused. `CAST('2026-08-15 10:00:00' AS DATE)` fails on
|
|
9
|
+
* Athena, and only a timestamp value loses its time to a cast.
|
|
10
|
+
*/
|
|
11
|
+
const varcharDate = /^(\d{4})-(\d{1,2})-(\d{1,2})$/u;
|
|
12
|
+
/**
|
|
13
|
+
* What Trino's cast from `varchar` to `TIMESTAMP` reads.
|
|
14
|
+
*
|
|
15
|
+
* The time follows a space. A `T` between the date and the time fails the
|
|
16
|
+
* cast on Athena, and so does a zone, which belongs to `TIMESTAMP WITH TIME
|
|
17
|
+
* ZONE`.
|
|
18
|
+
*/
|
|
19
|
+
const varcharTimestamp = /^(\d{4})-(\d{1,2})-(\d{1,2})(?: (\d{1,2}):(\d{1,2})(?::(\d{1,2})(?:\.(\d{1,12}))?)?)?$/u;
|
|
20
|
+
/**
|
|
21
|
+
* The instant one value names as the cast reads it, or nothing where the cast
|
|
22
|
+
* fails.
|
|
23
|
+
*
|
|
24
|
+
* Text is held to the formats Trino's cast from `varchar` takes. Any other
|
|
25
|
+
* source is a value some column or function already typed as a date or a
|
|
26
|
+
* timestamp, written as the ISO-8601 text the engine holds one as.
|
|
27
|
+
*/
|
|
28
|
+
export function simAthenaCastInstant(text, type, fromText) {
|
|
29
|
+
if (!fromText) {
|
|
30
|
+
return simAthenaInstant(text);
|
|
31
|
+
}
|
|
32
|
+
const fields = type === "date"
|
|
33
|
+
? varcharDate.exec(text.trim())
|
|
34
|
+
: varcharTimestamp.exec(text);
|
|
35
|
+
return fields === null ? undefined : instantOf(fields);
|
|
36
|
+
}
|
|
37
|
+
/** One instant rendered as Athena renders a value of the type. */
|
|
38
|
+
export function simAthenaCastRendered(instant, type) {
|
|
39
|
+
const iso = new Date(instant).toISOString();
|
|
40
|
+
return type === "date"
|
|
41
|
+
? iso.slice(0, 10)
|
|
42
|
+
: iso.slice(0, 23).replace("T", " ");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The instant a matched value names, where every field is in range.
|
|
46
|
+
*
|
|
47
|
+
* `Date.UTC` rolls the 30th of February over into March, and Trino refuses
|
|
48
|
+
* it. Each field is checked against its calendar before anything is built.
|
|
49
|
+
* A fraction rounds to the millisecond, which is the precision Athena gives a
|
|
50
|
+
* `TIMESTAMP` with none written.
|
|
51
|
+
*/
|
|
52
|
+
function instantOf(fields) {
|
|
53
|
+
const [year, month, day, hour, minute, second] = Array.from({ length: 6 }, (_, index) => Number(fields[index + 1] ?? 0));
|
|
54
|
+
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
55
|
+
if (month < 1 ||
|
|
56
|
+
month > 12 ||
|
|
57
|
+
day < 1 ||
|
|
58
|
+
day > lastDay ||
|
|
59
|
+
hour > 23 ||
|
|
60
|
+
minute > 59 ||
|
|
61
|
+
second > 59) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const milliseconds = Math.round(Number(`0.${fields[7] ?? "0"}`) * 1000);
|
|
65
|
+
return Date.UTC(year, month - 1, day, hour, minute, second, milliseconds);
|
|
66
|
+
}
|
|
@@ -57,8 +57,7 @@ function isNumeric(value) {
|
|
|
57
57
|
* One argument as the text it orders by.
|
|
58
58
|
*
|
|
59
59
|
* A `varbinary` orders by its bytes in Trino, and nothing written out here
|
|
60
|
-
* orders the same way. A call over one raises and the query
|
|
61
|
-
* declared result.
|
|
60
|
+
* orders the same way. A call over one raises and the query fails.
|
|
62
61
|
*/
|
|
63
62
|
function comparableText(value) {
|
|
64
63
|
if (value instanceof Uint8Array) {
|
|
@@ -2,10 +2,9 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
/**
|
|
3
3
|
* Trino's date arithmetic, over ISO-8601 text.
|
|
4
4
|
*
|
|
5
|
-
* Each of these raises
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* wearing the shape of a right one.
|
|
5
|
+
* Each of these raises where it cannot answer faithfully. A unit outside the
|
|
6
|
+
* ones Trino names and a value that is no ISO-8601 timestamp both land there,
|
|
7
|
+
* and the engine fails the query with the reason. A shim answering a null
|
|
8
|
+
* would be a wrong answer wearing the shape of a right one.
|
|
10
9
|
*/
|
|
11
10
|
export declare function simAthenaInstallDateArithmeticShims(database: DatabaseSync): void;
|
|
@@ -6,11 +6,10 @@ import { simAthenaInstant, simAthenaRenderLike, } from "./sim-athena-timestamp-t
|
|
|
6
6
|
/**
|
|
7
7
|
* Trino's date arithmetic, over ISO-8601 text.
|
|
8
8
|
*
|
|
9
|
-
* Each of these raises
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* wearing the shape of a right one.
|
|
9
|
+
* Each of these raises where it cannot answer faithfully. A unit outside the
|
|
10
|
+
* ones Trino names and a value that is no ISO-8601 timestamp both land there,
|
|
11
|
+
* and the engine fails the query with the reason. A shim answering a null
|
|
12
|
+
* would be a wrong answer wearing the shape of a right one.
|
|
14
13
|
*/
|
|
15
14
|
export function simAthenaInstallDateArithmeticShims(database) {
|
|
16
15
|
simAthenaScalarShim(database, "date_add", (unit, value, timestamp) => {
|
|
@@ -47,13 +46,7 @@ function unitOf(value) {
|
|
|
47
46
|
? unit
|
|
48
47
|
: raise(`${unit} is no date unit the engine reads`);
|
|
49
48
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Turn the query down.
|
|
52
|
-
*
|
|
53
|
-
* A shim that cannot answer faithfully raises, and the engine catches it and
|
|
54
|
-
* lets the declared result answer. Answering a null instead would be a wrong
|
|
55
|
-
* answer wearing the shape of a right one.
|
|
56
|
-
*/
|
|
49
|
+
/** Fail the query, saying why the shim could not answer. */
|
|
57
50
|
function raise(reason) {
|
|
58
51
|
throw new SimAthenaSetUpError(reason);
|
|
59
52
|
}
|
|
@@ -63,8 +56,7 @@ function raise(reason) {
|
|
|
63
56
|
* Trino answers with a timestamp carrying the zone. There is nowhere to put one
|
|
64
57
|
* here, so the answer is the wall clock alone, with any `Z` the value arrived
|
|
65
58
|
* with taken off. A `Z` left on would say UTC over a time that is no longer in
|
|
66
|
-
* it. A zone nobody has heard of raises out of `Intl`, and the query
|
|
67
|
-
* back.
|
|
59
|
+
* it. A zone nobody has heard of raises out of `Intl`, and the query fails.
|
|
68
60
|
*/
|
|
69
61
|
function inZone(at, zone) {
|
|
70
62
|
if (at === undefined || zone === undefined) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A shim answering a unit it does not know would be answering wrongly, so the
|
|
5
5
|
* ones here are the whole of what `date_add` and `date_diff` take. Anything
|
|
6
|
-
* else raises, and the
|
|
6
|
+
* else raises, and the query fails.
|
|
7
7
|
*/
|
|
8
8
|
export declare function isDateUnit(unit: string): boolean;
|
|
9
9
|
/**
|
|
@@ -19,7 +19,7 @@ const calendarUnits = new Map([
|
|
|
19
19
|
*
|
|
20
20
|
* A shim answering a unit it does not know would be answering wrongly, so the
|
|
21
21
|
* ones here are the whole of what `date_add` and `date_diff` take. Anything
|
|
22
|
-
* else raises, and the
|
|
22
|
+
* else raises, and the query fails.
|
|
23
23
|
*/
|
|
24
24
|
export function isDateUnit(unit) {
|
|
25
25
|
return clockUnits.has(unit) || calendarUnits.has(unit);
|
|
@@ -10,7 +10,12 @@ export interface SimAthenaEngineAnswer {
|
|
|
10
10
|
readonly result: SimAthenaResolvedResult | undefined;
|
|
11
11
|
/** What the engine could not do, where it turned the query down. */
|
|
12
12
|
readonly turnedDown: string | undefined;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Why the query fails whether or not the engine is strict.
|
|
15
|
+
*
|
|
16
|
+
* That is a statement Athena refuses, or one the engine ran and could not
|
|
17
|
+
* evaluate.
|
|
18
|
+
*/
|
|
14
19
|
readonly rejected: string | undefined;
|
|
15
20
|
}
|
|
16
21
|
/** One answer the engine ran for real. */
|
|
@@ -18,11 +23,11 @@ export declare function simAthenaEngineAnswered(result: SimAthenaResolvedResult)
|
|
|
18
23
|
/** One query the engine turned down, and why. */
|
|
19
24
|
export declare function simAthenaEngineTurnedDown(turnedDown: string): SimAthenaEngineAnswer;
|
|
20
25
|
/**
|
|
21
|
-
* One statement
|
|
26
|
+
* One statement that fails whatever mode the engine is in, and why.
|
|
22
27
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
28
|
+
* Athena refusing the statement is one reason, and the alternative there is a
|
|
29
|
+
* green test on SQL the service rejects. A function shim refusing a value is
|
|
30
|
+
* the other, and the alternative there is a query losing every row in silence.
|
|
26
31
|
*/
|
|
27
32
|
export declare function simAthenaEngineRejected(rejected: string): SimAthenaEngineAnswer;
|
|
28
33
|
/** An engine nobody turned on, which has no opinion about the query. */
|
|
@@ -7,11 +7,11 @@ export function simAthenaEngineTurnedDown(turnedDown) {
|
|
|
7
7
|
return { result: undefined, turnedDown, rejected: undefined };
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* One statement
|
|
10
|
+
* One statement that fails whatever mode the engine is in, and why.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Athena refusing the statement is one reason, and the alternative there is a
|
|
13
|
+
* green test on SQL the service rejects. A function shim refusing a value is
|
|
14
|
+
* the other, and the alternative there is a query losing every row in silence.
|
|
15
15
|
*/
|
|
16
16
|
export function simAthenaEngineRejected(rejected) {
|
|
17
17
|
return { result: undefined, turnedDown: undefined, rejected };
|
|
@@ -29,5 +29,9 @@ export interface SimAthenaEngineRun {
|
|
|
29
29
|
*
|
|
30
30
|
* Each of them carries a reason out, which a strict engine fails the query
|
|
31
31
|
* with.
|
|
32
|
+
*
|
|
33
|
+
* A function shim refusing one of the statement's values is the exception. The
|
|
34
|
+
* engine ran the statement and could not evaluate it, and the query fails
|
|
35
|
+
* whether or not the engine is strict.
|
|
32
36
|
*/
|
|
33
37
|
export declare function simAthenaEngineRun(run: SimAthenaEngineRun): Promise<SimAthenaEngineAnswer>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { simAthenaEngineAnswered, simAthenaEngineTurnedDown, } from "./sim-athena-engine-answer.js";
|
|
2
|
-
import { simAthenaRunFailure } from "./sim-athena-turn-down.js";
|
|
1
|
+
import { simAthenaEngineAnswered, simAthenaEngineRejected, simAthenaEngineTurnedDown, } from "./sim-athena-engine-answer.js";
|
|
2
|
+
import { simAthenaRunFailure, simAthenaUnevaluable, } from "./sim-athena-turn-down.js";
|
|
3
3
|
import { simAthenaEngineResult } from "./sim-athena-engine-result.js";
|
|
4
|
+
import { SimAthenaShimRefusal } from "./sim-athena-shim-refusal.js";
|
|
4
5
|
import { simAthenaSqliteDatabase } from "./sim-athena-sqlite-database.js";
|
|
5
6
|
import { simAthenaLoadedTables } from "./sim-athena-loaded-tables.js";
|
|
6
7
|
/**
|
|
@@ -15,6 +16,10 @@ import { simAthenaLoadedTables } from "./sim-athena-loaded-tables.js";
|
|
|
15
16
|
*
|
|
16
17
|
* Each of them carries a reason out, which a strict engine fails the query
|
|
17
18
|
* with.
|
|
19
|
+
*
|
|
20
|
+
* A function shim refusing one of the statement's values is the exception. The
|
|
21
|
+
* engine ran the statement and could not evaluate it, and the query fails
|
|
22
|
+
* whether or not the engine is strict.
|
|
18
23
|
*/
|
|
19
24
|
export async function simAthenaEngineRun(run) {
|
|
20
25
|
try {
|
|
@@ -41,6 +46,9 @@ export async function simAthenaEngineRun(run) {
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
catch (error) {
|
|
49
|
+
if (error instanceof SimAthenaShimRefusal) {
|
|
50
|
+
return simAthenaEngineRejected(simAthenaUnevaluable(error));
|
|
51
|
+
}
|
|
44
52
|
return simAthenaEngineTurnedDown(simAthenaRunFailure(error));
|
|
45
53
|
}
|
|
46
54
|
}
|
|
@@ -28,8 +28,8 @@ function matches(value, pattern) {
|
|
|
28
28
|
*
|
|
29
29
|
* Trino counts the groups from one and calls the whole match zero, which is
|
|
30
30
|
* what a regular expression counts them as anywhere. Trino also refuses a group
|
|
31
|
-
* the pattern has not got, and raising here
|
|
32
|
-
*
|
|
31
|
+
* the pattern has not got, and raising here fails the query where a negative
|
|
32
|
+
* index would quietly count back from the end.
|
|
33
33
|
*/
|
|
34
34
|
function extracted(value, pattern, group) {
|
|
35
35
|
if (!Number.isSafeInteger(group) || group < 0) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Trino function shim that could not answer for one value.
|
|
3
|
+
*
|
|
4
|
+
* Each shim raises where it cannot answer faithfully, such as a date function
|
|
5
|
+
* handed text that is no timestamp. SQLite hands the error back out of the
|
|
6
|
+
* statement unchanged, and this class is how the engine tells it apart from
|
|
7
|
+
* SQLite refusing the statement itself.
|
|
8
|
+
*
|
|
9
|
+
* The engine fails the query on one of these whether or not it is strict. The
|
|
10
|
+
* statement parsed and SQLite ran it, so falling back would answer a query the
|
|
11
|
+
* engine had taken on with rows a declaration wrote for some other purpose,
|
|
12
|
+
* often no rows at all.
|
|
13
|
+
*/
|
|
14
|
+
export declare class SimAthenaShimRefusal extends Error {
|
|
15
|
+
readonly name = "SimAthenaShimRefusal";
|
|
16
|
+
constructor(described: string, cause: unknown);
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Trino function shim that could not answer for one value.
|
|
3
|
+
*
|
|
4
|
+
* Each shim raises where it cannot answer faithfully, such as a date function
|
|
5
|
+
* handed text that is no timestamp. SQLite hands the error back out of the
|
|
6
|
+
* statement unchanged, and this class is how the engine tells it apart from
|
|
7
|
+
* SQLite refusing the statement itself.
|
|
8
|
+
*
|
|
9
|
+
* The engine fails the query on one of these whether or not it is strict. The
|
|
10
|
+
* statement parsed and SQLite ran it, so falling back would answer a query the
|
|
11
|
+
* engine had taken on with rows a declaration wrote for some other purpose,
|
|
12
|
+
* often no rows at all.
|
|
13
|
+
*/
|
|
14
|
+
export class SimAthenaShimRefusal extends Error {
|
|
15
|
+
name = "SimAthenaShimRefusal";
|
|
16
|
+
constructor(described, cause) {
|
|
17
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
18
|
+
super(`${described} could not answer for this statement's values. ${reason}`, {
|
|
19
|
+
cause,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -7,8 +7,12 @@ export type SimAthenaScalarShim = (...values: SQLOutputValue[]) => SQLInputValue
|
|
|
7
7
|
* Every shim takes any number of arguments, because SQLite checks the count
|
|
8
8
|
* against the JavaScript function's own arity otherwise and a Trino function
|
|
9
9
|
* with an optional argument would stop resolving.
|
|
10
|
+
*
|
|
11
|
+
* Anything the shim raises comes back out as a `SimAthenaShimRefusal` naming
|
|
12
|
+
* the function as a statement wrote it, which fails the query. `described`
|
|
13
|
+
* is that name where it differs from the one registered.
|
|
10
14
|
*/
|
|
11
|
-
export declare function simAthenaScalarShim(database: DatabaseSync, name: string, implementation: SimAthenaScalarShim): void;
|
|
15
|
+
export declare function simAthenaScalarShim(database: DatabaseSync, name: string, implementation: SimAthenaScalarShim, described?: string): void;
|
|
12
16
|
/**
|
|
13
17
|
* Whether an optional argument was written as an explicit `NULL`.
|
|
14
18
|
*
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
+
import { SimAthenaShimRefusal } from "./sim-athena-shim-refusal.js";
|
|
1
2
|
/**
|
|
2
3
|
* Register one Trino scalar function on this database.
|
|
3
4
|
*
|
|
4
5
|
* Every shim takes any number of arguments, because SQLite checks the count
|
|
5
6
|
* against the JavaScript function's own arity otherwise and a Trino function
|
|
6
7
|
* with an optional argument would stop resolving.
|
|
8
|
+
*
|
|
9
|
+
* Anything the shim raises comes back out as a `SimAthenaShimRefusal` naming
|
|
10
|
+
* the function as a statement wrote it, which fails the query. `described`
|
|
11
|
+
* is that name where it differs from the one registered.
|
|
7
12
|
*/
|
|
8
|
-
export function simAthenaScalarShim(database, name, implementation) {
|
|
9
|
-
database.function(name, { varargs: true, deterministic: true }, (...values) =>
|
|
13
|
+
export function simAthenaScalarShim(database, name, implementation, described = name) {
|
|
14
|
+
database.function(name, { varargs: true, deterministic: true }, (...values) => {
|
|
15
|
+
try {
|
|
16
|
+
return withEmptyBytes(implementation(...values));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new SimAthenaShimRefusal(described, error);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
10
22
|
}
|
|
11
23
|
/**
|
|
12
24
|
* One answer, with an empty `varbinary` written so that SQLite keeps it.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { simAthenaInstallAggregateShims } from "./sim-athena-aggregate-shims.js";
|
|
2
2
|
import { simAthenaInstallArrayShims } from "./sim-athena-array-shims.js";
|
|
3
3
|
import { simAthenaInstallBinaryShims } from "./sim-athena-binary-shims.js";
|
|
4
|
+
import { simAthenaInstallCastShims } from "./sim-athena-cast-shims.js";
|
|
4
5
|
import { simAthenaInstallClockShims } from "./sim-athena-clock-shims.js";
|
|
5
6
|
import { simAthenaInstallComparisonShims } from "./sim-athena-comparison-shims.js";
|
|
6
7
|
import { simAthenaInstallDateArithmeticShims } from "./sim-athena-date-arithmetic-shims.js";
|
|
@@ -22,6 +23,7 @@ export function simAthenaInstallShims(database, startedAt) {
|
|
|
22
23
|
simAthenaInstallClockShims(database, startedAt);
|
|
23
24
|
simAthenaInstallDateShims(database);
|
|
24
25
|
simAthenaInstallDateArithmeticShims(database);
|
|
26
|
+
simAthenaInstallCastShims(database);
|
|
25
27
|
simAthenaInstallJsonShims(database);
|
|
26
28
|
simAthenaInstallJsonDocumentShims(database);
|
|
27
29
|
simAthenaInstallArrayShims(database);
|
|
@@ -1,28 +1,6 @@
|
|
|
1
1
|
import type { SimAthenaCatalogTable } from "../table/sim-athena-catalog-table.js";
|
|
2
2
|
import type { SimAthenaSqlParser } from "./sim-athena-parser-module.js";
|
|
3
|
-
|
|
4
|
-
export type SimAthenaTranslatedSql = {
|
|
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>;
|
|
13
|
-
readonly turnedDown: undefined;
|
|
14
|
-
readonly rejected: undefined;
|
|
15
|
-
} | {
|
|
16
|
-
readonly sql: undefined;
|
|
17
|
-
readonly scales: undefined;
|
|
18
|
-
readonly turnedDown: string;
|
|
19
|
-
readonly rejected: undefined;
|
|
20
|
-
} | {
|
|
21
|
-
readonly sql: undefined;
|
|
22
|
-
readonly scales: undefined;
|
|
23
|
-
readonly turnedDown: undefined;
|
|
24
|
-
readonly rejected: string;
|
|
25
|
-
};
|
|
3
|
+
import { type SimAthenaTranslatedSql } from "./sim-athena-translated-sql.js";
|
|
26
4
|
interface SimAthenaSqlTranslationRequest {
|
|
27
5
|
readonly parser: SimAthenaSqlParser;
|
|
28
6
|
readonly athenaSql: string;
|
|
@@ -1,7 +1,9 @@
|
|
|
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 { simAthenaRewriteCasts } from "./sim-athena-cast-rewrite.js";
|
|
4
5
|
import { simAthenaRoundScales } from "./sim-athena-round-scales.js";
|
|
6
|
+
import { simAthenaRejectedSql, simAthenaTranslated, simAthenaTurnedDownSql, } from "./sim-athena-translated-sql.js";
|
|
5
7
|
import { simAthenaRewriteUnnest } from "./sim-athena-unnest-rewrite.js";
|
|
6
8
|
/**
|
|
7
9
|
* One Athena statement written back out for SQLite, or why it is not.
|
|
@@ -26,16 +28,11 @@ export function simAthenaSqliteSql(request) {
|
|
|
26
28
|
ast = parser.astify(read.sql, { database: "athena" });
|
|
27
29
|
}
|
|
28
30
|
catch {
|
|
29
|
-
return
|
|
31
|
+
return simAthenaTurnedDownSql(simAthenaUnparsedStatement);
|
|
30
32
|
}
|
|
31
33
|
const rejected = simAthenaRejectedStatement(ast);
|
|
32
34
|
if (rejected !== undefined) {
|
|
33
|
-
return
|
|
34
|
-
sql: undefined,
|
|
35
|
-
scales: undefined,
|
|
36
|
-
turnedDown: undefined,
|
|
37
|
-
rejected,
|
|
38
|
-
};
|
|
35
|
+
return simAthenaRejectedSql(rejected);
|
|
39
36
|
}
|
|
40
37
|
try {
|
|
41
38
|
if (!simAthenaRewriteUnnest({
|
|
@@ -43,27 +40,15 @@ export function simAthenaSqliteSql(request) {
|
|
|
43
40
|
ordinality: read.ordinality,
|
|
44
41
|
tables: request.tables,
|
|
45
42
|
})) {
|
|
46
|
-
return
|
|
43
|
+
return simAthenaTurnedDownSql(simAthenaUnrewrittenUnnest);
|
|
47
44
|
}
|
|
45
|
+
simAthenaRewriteCasts(ast, request.tables);
|
|
48
46
|
// Read before the tree is written back out, since the rewrites above can
|
|
49
47
|
// move a select item and the scale travels by position.
|
|
50
48
|
const scales = simAthenaRoundScales(ast);
|
|
51
|
-
return {
|
|
52
|
-
sql: simAthenaSqlForSqlite(parser.sqlify(ast, { database: "sqlite" })),
|
|
53
|
-
scales,
|
|
54
|
-
turnedDown: undefined,
|
|
55
|
-
rejected: undefined,
|
|
56
|
-
};
|
|
49
|
+
return simAthenaTranslated(simAthenaSqlForSqlite(parser.sqlify(ast, { database: "sqlite" })), scales);
|
|
57
50
|
}
|
|
58
51
|
catch {
|
|
59
|
-
return
|
|
52
|
+
return simAthenaTurnedDownSql(simAthenaUnwrittenStatement);
|
|
60
53
|
}
|
|
61
54
|
}
|
|
62
|
-
function turnedDown(reason) {
|
|
63
|
-
return {
|
|
64
|
-
sql: undefined,
|
|
65
|
-
scales: undefined,
|
|
66
|
-
turnedDown: reason,
|
|
67
|
-
rejected: undefined,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** One statement translated for SQLite, or why it was not. */
|
|
2
|
+
export type SimAthenaTranslatedSql = {
|
|
3
|
+
readonly sql: string;
|
|
4
|
+
/**
|
|
5
|
+
* The decimal places each result column was rounded to, by position.
|
|
6
|
+
*
|
|
7
|
+
* Read here because the syntax tree is the only place that knows, and
|
|
8
|
+
* the tree is gone by the time the answer is rendered.
|
|
9
|
+
*/
|
|
10
|
+
readonly scales: ReadonlyMap<number, number>;
|
|
11
|
+
readonly turnedDown: undefined;
|
|
12
|
+
readonly rejected: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
readonly sql: undefined;
|
|
15
|
+
readonly scales: undefined;
|
|
16
|
+
readonly turnedDown: string;
|
|
17
|
+
readonly rejected: undefined;
|
|
18
|
+
} | {
|
|
19
|
+
readonly sql: undefined;
|
|
20
|
+
readonly scales: undefined;
|
|
21
|
+
readonly turnedDown: undefined;
|
|
22
|
+
readonly rejected: string;
|
|
23
|
+
};
|
|
24
|
+
/** One statement written out for SQLite, with the scales read off its tree. */
|
|
25
|
+
export declare function simAthenaTranslated(sql: string, scales: ReadonlyMap<number, number>): SimAthenaTranslatedSql;
|
|
26
|
+
/** One statement the engine turned down, and why. */
|
|
27
|
+
export declare function simAthenaTurnedDownSql(turnedDown: string): SimAthenaTranslatedSql;
|
|
28
|
+
/** One statement Athena refuses, and why. */
|
|
29
|
+
export declare function simAthenaRejectedSql(rejected: string): SimAthenaTranslatedSql;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** One statement written out for SQLite, with the scales read off its tree. */
|
|
2
|
+
export function simAthenaTranslated(sql, scales) {
|
|
3
|
+
return { sql, scales, turnedDown: undefined, rejected: undefined };
|
|
4
|
+
}
|
|
5
|
+
/** One statement the engine turned down, and why. */
|
|
6
|
+
export function simAthenaTurnedDownSql(turnedDown) {
|
|
7
|
+
return { sql: undefined, scales: undefined, turnedDown, rejected: undefined };
|
|
8
|
+
}
|
|
9
|
+
/** One statement Athena refuses, and why. */
|
|
10
|
+
export function simAthenaRejectedSql(rejected) {
|
|
11
|
+
return { sql: undefined, scales: undefined, turnedDown: undefined, rejected };
|
|
12
|
+
}
|
|
@@ -14,6 +14,13 @@ export declare const simAthenaUnrewrittenUnnest = "The simulated Athena query en
|
|
|
14
14
|
export declare const simAthenaUnwrittenStatement = "The simulated Athena query engine cannot write this statement back out for SQLite.";
|
|
15
15
|
/** Whatever went wrong while the tables were loaded or the statement ran. */
|
|
16
16
|
export declare function simAthenaRunFailure(error: unknown): string;
|
|
17
|
+
/**
|
|
18
|
+
* A function shim that could not answer for one of the statement's values.
|
|
19
|
+
*
|
|
20
|
+
* This fails the query whether or not the engine is strict, so the reason
|
|
21
|
+
* points at the one declaration that still answers it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function simAthenaUnevaluable(refusal: Error): string;
|
|
17
24
|
/**
|
|
18
25
|
* One table the engine has no reader for.
|
|
19
26
|
*
|
|
@@ -21,6 +21,17 @@ export function simAthenaRunFailure(error) {
|
|
|
21
21
|
const message = error instanceof Error ? error.message : String(error);
|
|
22
22
|
return `${engine} could not answer this statement. ${message}`;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* A function shim that could not answer for one of the statement's values.
|
|
26
|
+
*
|
|
27
|
+
* This fails the query whether or not the engine is strict, so the reason
|
|
28
|
+
* points at the one declaration that still answers it.
|
|
29
|
+
*/
|
|
30
|
+
export function simAthenaUnevaluable(refusal) {
|
|
31
|
+
return (`${engine} cannot evaluate this statement. ${refusal.message} ` +
|
|
32
|
+
`Declare what this exact query answers with through results() to answer ` +
|
|
33
|
+
`it without the engine.`);
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
36
|
* One table the engine has no reader for.
|
|
26
37
|
*
|
|
@@ -51,7 +51,8 @@ interface SimAthenaQueryAnswerRequest {
|
|
|
51
51
|
*
|
|
52
52
|
* A statement Athena refuses fails whichever mode the engine is in. That is
|
|
53
53
|
* SQL the service would never have run, and falling back would leave the query
|
|
54
|
-
* green on it.
|
|
54
|
+
* green on it. So does a statement the engine ran and could not evaluate,
|
|
55
|
+
* where falling back to the default would answer with no rows at all.
|
|
55
56
|
*/
|
|
56
57
|
export declare function simAthenaQueryAnswer(request: SimAthenaQueryAnswerRequest): Promise<SimAthenaQueryAnswer>;
|
|
57
58
|
export {};
|
|
@@ -18,7 +18,8 @@ import { simAthenaStrictRefusal } from "../engine/sim-athena-turn-down.js";
|
|
|
18
18
|
*
|
|
19
19
|
* A statement Athena refuses fails whichever mode the engine is in. That is
|
|
20
20
|
* SQL the service would never have run, and falling back would leave the query
|
|
21
|
-
* green on it.
|
|
21
|
+
* green on it. So does a statement the engine ran and could not evaluate,
|
|
22
|
+
* where falling back to the default would answer with no rows at all.
|
|
22
23
|
*/
|
|
23
24
|
export async function simAthenaQueryAnswer(request) {
|
|
24
25
|
const forQuery = request.results.declaredForQuery(request.queryString);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SimAthenaEngineSimulation } from "./sim-athena-engine.fixture.js";
|
|
2
|
+
/**
|
|
3
|
+
* A table of snapshots partitioned by a string `day`, with the engine on and
|
|
4
|
+
* a default declaration that a fallback would answer with.
|
|
5
|
+
*
|
|
6
|
+
* `taken_at` is a Glue `timestamp` written as ISO-8601 with a `T` and a `Z`.
|
|
7
|
+
* `noted_at` is a `string` holding a timestamp the way Trino's cast from text
|
|
8
|
+
* reads one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function aSnapshotSimulation(): Promise<SimAthenaEngineSimulation>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { aCatalogTable, anEngineSimulation, aSeededJson, } from "./sim-athena-engine.fixture.js";
|
|
2
|
+
/**
|
|
3
|
+
* A table of snapshots partitioned by a string `day`, with the engine on and
|
|
4
|
+
* a default declaration that a fallback would answer with.
|
|
5
|
+
*
|
|
6
|
+
* `taken_at` is a Glue `timestamp` written as ISO-8601 with a `T` and a `Z`.
|
|
7
|
+
* `noted_at` is a `string` holding a timestamp the way Trino's cast from text
|
|
8
|
+
* reads one.
|
|
9
|
+
*/
|
|
10
|
+
export async function aSnapshotSimulation() {
|
|
11
|
+
const simulation = await anEngineSimulation();
|
|
12
|
+
aCatalogTable(simulation.simAws, {
|
|
13
|
+
name: "snapshots",
|
|
14
|
+
columns: [
|
|
15
|
+
{ Name: "sku", Type: "string" },
|
|
16
|
+
{ Name: "taken_at", Type: "timestamp" },
|
|
17
|
+
{ Name: "noted_at", Type: "string" },
|
|
18
|
+
],
|
|
19
|
+
partitionKeys: [{ Name: "day", Type: "string" }],
|
|
20
|
+
});
|
|
21
|
+
await aSeededJson(simulation.simAws, "snapshots/day=2026-08-15/part-0.json", [
|
|
22
|
+
{
|
|
23
|
+
sku: "bolt",
|
|
24
|
+
taken_at: "2026-08-15T23:30:00Z",
|
|
25
|
+
noted_at: "2026-08-15 10:00:00",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
sku: "nut",
|
|
29
|
+
taken_at: "2026-08-14T08:00:00Z",
|
|
30
|
+
noted_at: "2026-08-14 09:15:00.5",
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
await simulation.simAws.athena().engine().enable();
|
|
34
|
+
simulation.simAws
|
|
35
|
+
.athena()
|
|
36
|
+
.results()
|
|
37
|
+
.byDefault({ columns: ["fallback"], rows: [["declared"]] });
|
|
38
|
+
return simulation;
|
|
39
|
+
}
|
|
@@ -35,9 +35,10 @@ interface SimLambdaAwsApiOutboundProperties {
|
|
|
35
35
|
* operation it names.
|
|
36
36
|
*/
|
|
37
37
|
export declare class SimLambdaAwsApiOutbound implements SimLambdaOutboundHttp {
|
|
38
|
+
private readonly simAws;
|
|
38
39
|
private readonly dispatcher;
|
|
39
40
|
private readonly s3;
|
|
40
|
-
constructor(
|
|
41
|
+
constructor({ simAws, regionName }: SimLambdaAwsApiOutboundProperties);
|
|
41
42
|
/**
|
|
42
43
|
* Whether a hostname is one of the AWS service API endpoints.
|
|
43
44
|
*/
|
|
@@ -51,6 +52,11 @@ export declare class SimLambdaAwsApiOutbound implements SimLambdaOutboundHttp {
|
|
|
51
52
|
* credentials the runtime puts in the environment, and the invocation is
|
|
52
53
|
* already running as the execution Role, the ambient caller every other call
|
|
53
54
|
* out of function code runs as.
|
|
55
|
+
*
|
|
56
|
+
* Every answer is stamped with simulated time, as real AWS stamps every API
|
|
57
|
+
* response with server time. The SDK corrects its clock skew from that
|
|
58
|
+
* header, and an answer missing it would be stamped with host time by the
|
|
59
|
+
* HTTP/2 server answering it.
|
|
54
60
|
*/
|
|
55
61
|
fetch(request: Request): Promise<Response>;
|
|
56
62
|
}
|
|
@@ -75,11 +75,13 @@ export function isSimAwsApiRequest(request) {
|
|
|
75
75
|
* operation it names.
|
|
76
76
|
*/
|
|
77
77
|
export class SimLambdaAwsApiOutbound {
|
|
78
|
+
simAws;
|
|
78
79
|
dispatcher;
|
|
79
80
|
s3;
|
|
80
|
-
constructor(
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
81
|
+
constructor({ simAws, regionName }) {
|
|
82
|
+
this.simAws = simAws;
|
|
83
|
+
this.dispatcher = new SimSdkWireDispatcher(simAws, regionName);
|
|
84
|
+
this.s3 = new SimS3ApiEndpoint({ simAws });
|
|
83
85
|
}
|
|
84
86
|
/**
|
|
85
87
|
* Whether a hostname is one of the AWS service API endpoints.
|
|
@@ -96,13 +98,19 @@ export class SimLambdaAwsApiOutbound {
|
|
|
96
98
|
* credentials the runtime puts in the environment, and the invocation is
|
|
97
99
|
* already running as the execution Role, the ambient caller every other call
|
|
98
100
|
* out of function code runs as.
|
|
101
|
+
*
|
|
102
|
+
* Every answer is stamped with simulated time, as real AWS stamps every API
|
|
103
|
+
* response with server time. The SDK corrects its clock skew from that
|
|
104
|
+
* header, and an answer missing it would be stamped with host time by the
|
|
105
|
+
* HTTP/2 server answering it.
|
|
99
106
|
*/
|
|
100
107
|
async fetch(request) {
|
|
101
108
|
const wireRequest = await simLambdaOutboundWireRequest(request);
|
|
102
109
|
const scope = readSimSdkWireCredentialScope(wireRequest);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
const response = scope?.signingName === s3SigningName
|
|
111
|
+
? await this.s3.handle(request, wireRequest.body, undefined, scope.regionName)
|
|
112
|
+
: simLambdaOutboundWireResponse(await this.dispatcher.dispatch(wireRequest));
|
|
113
|
+
response.headers.set("date", this.simAws.now().toUTCString());
|
|
114
|
+
return response;
|
|
107
115
|
}
|
|
108
116
|
}
|
|
@@ -26,6 +26,9 @@ export declare class SimLambdaHttp2Session {
|
|
|
26
26
|
*
|
|
27
27
|
* A request the simulation cannot answer fails the client's stream with the
|
|
28
28
|
* error that explains it, which is what the SDK rejects the call with.
|
|
29
|
+
*
|
|
30
|
+
* The answer carries the `date` the simulation gave it, or none. The server
|
|
31
|
+
* would otherwise date it from the host clock.
|
|
29
32
|
*/
|
|
30
33
|
private respond;
|
|
31
34
|
}
|
|
@@ -42,11 +42,15 @@ export class SimLambdaHttp2Session {
|
|
|
42
42
|
*
|
|
43
43
|
* A request the simulation cannot answer fails the client's stream with the
|
|
44
44
|
* error that explains it, which is what the SDK rejects the call with.
|
|
45
|
+
*
|
|
46
|
+
* The answer carries the `date` the simulation gave it, or none. The server
|
|
47
|
+
* would otherwise date it from the host clock.
|
|
45
48
|
*/
|
|
46
49
|
async respond(request, response, clientStream) {
|
|
47
50
|
try {
|
|
48
51
|
const answered = await this.outbound.fetch(await simLambdaHttp2Request(this.authority, this.outbound, request));
|
|
49
52
|
const body = Buffer.from(await answered.arrayBuffer());
|
|
53
|
+
response.sendDate = false;
|
|
50
54
|
response.writeHead(answered.status, simLambdaHttp2ResponseHeaders(answered.headers));
|
|
51
55
|
response.end(body);
|
|
52
56
|
}
|
|
@@ -383,8 +383,8 @@ test's own statement about a query the engine gets wrong.
|
|
|
383
383
|
|
|
384
384
|
### Failing a statement Athena refuses
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
outright, the query fails whether strict mode is on or off. A turn-down means the engine reached its
|
|
386
|
+
Two failures ignore strict mode. This section covers the first. Where the engine can see that
|
|
387
|
+
Athena would refuse a statement outright, the query fails whether strict mode is on or off. A turn-down means the engine reached its
|
|
388
388
|
limits, and a declaration is a fair answer to that. A rejection means the SQL is invalid, and
|
|
389
389
|
answering from a declaration would leave a test green on a query that fails against AWS.
|
|
390
390
|
|
|
@@ -411,6 +411,18 @@ well, since the two names differ and nothing shadows.
|
|
|
411
411
|
Trino and SQLite resolve names by different rules in other places too, and the engine catches this
|
|
412
412
|
one. A statement that behaves differently under the two is worth reporting.
|
|
413
413
|
|
|
414
|
+
### Failing a value a function cannot read
|
|
415
|
+
|
|
416
|
+
The second failure that ignores strict mode comes from a Trino function the engine runs. Where one
|
|
417
|
+
cannot answer faithfully for a value the statement hands it, the query fails with a
|
|
418
|
+
`StateChangeReason` naming the function and the value. `date_add('fortnight', ...)` fails this way,
|
|
419
|
+
and so does `CAST(sku AS DATE)` over a column holding no date.
|
|
420
|
+
|
|
421
|
+
The engine took the statement on and ran it, so falling back would answer from rows a declaration
|
|
422
|
+
wrote for some other purpose. A query with no declaration of its own would answer from the
|
|
423
|
+
default, which is often no rows at all, and a test reading it would see an empty result with no
|
|
424
|
+
error. A declaration written against the exact query text still answers it.
|
|
425
|
+
|
|
414
426
|
### The objects it reads
|
|
415
427
|
|
|
416
428
|
The SerDe class name in the table's storage descriptor says how its objects are decoded.
|
|
@@ -560,7 +572,7 @@ call written inside a string literal is left alone as well.
|
|
|
560
572
|
instant it froze, and the same test answers the same way on every machine. Athena reads both at the
|
|
561
573
|
instant the query started, which is what the execution records.
|
|
562
574
|
|
|
563
|
-
A function that cannot answer faithfully raises rather than guessing, and the query
|
|
575
|
+
A function that cannot answer faithfully raises rather than guessing, and the query fails.
|
|
564
576
|
`date_add` with a unit Trino does not name, `slice` starting at zero, `array_join` over an array of
|
|
565
577
|
objects, `least` over a `varbinary`, and `regexp_extract` naming a capture group the pattern has not
|
|
566
578
|
got all land there. A null
|
|
@@ -1064,7 +1076,7 @@ Current documented limitations:
|
|
|
1064
1076
|
statement comparing the answer against a bare string matches on one and not the other.
|
|
1065
1077
|
- A timestamp carrying a numeric UTC offset falls outside the date functions, and so does one
|
|
1066
1078
|
written finer than the millisecond. A value written with a `Z` or with no zone at all reads as
|
|
1067
|
-
UTC, and a value the date functions cannot read
|
|
1079
|
+
UTC, and a value the date functions cannot read fails the query.
|
|
1068
1080
|
- A JSON number beyond about fifteen significant digits loses the digits past that, wherever the
|
|
1069
1081
|
engine reads JSON. An identifier of that size in a JSON lines object comes back rounded, and a
|
|
1070
1082
|
filter on it can then match the wrong row.
|
|
@@ -1088,9 +1100,9 @@ Current documented limitations:
|
|
|
1088
1100
|
written any other way gets whatever slicing that text comes to.
|
|
1089
1101
|
- A `varbinary` in a result row reads as its bytes decoded as UTF-8. A digest read that way loses
|
|
1090
1102
|
whatever in it was no UTF-8. A query answering with one wants `to_hex` or `to_base64` around it.
|
|
1091
|
-
- `from_hex` and `from_base64`
|
|
1092
|
-
|
|
1093
|
-
- `from_utf8` with a replacement named
|
|
1103
|
+
- `from_hex` and `from_base64` fail the query over text the encoding cannot carry, as Trino does.
|
|
1104
|
+
The alternative, reading as much of the text as parses, would answer with bytes nobody wrote.
|
|
1105
|
+
- `from_utf8` with a replacement named fails the query where the bytes already carry the
|
|
1094
1106
|
replacement character. Trino writes the replacement only where a sequence was broken, and nothing
|
|
1095
1107
|
survives decoding to tell one of those apart from a character the bytes held.
|
|
1096
1108
|
- `spooky_hash_v2_32` and `spooky_hash_v2_64` are absent, and so are the other Trino hashes with no
|
|
@@ -1107,7 +1119,16 @@ Current documented limitations:
|
|
|
1107
1119
|
runs.
|
|
1108
1120
|
- `try_cast` runs as a plain cast. `try_cast('abc' AS integer)` therefore answers 0 where real
|
|
1109
1121
|
Athena answers null, which is the same forgiving direction as the cast above. Reading the
|
|
1110
|
-
statement without the rewrite would turn the whole query down.
|
|
1122
|
+
statement without the rewrite would turn the whole query down. A `try_cast` to `DATE` or
|
|
1123
|
+
`TIMESTAMP` over text the cast cannot read fails the query, where Athena answers null.
|
|
1124
|
+
- `CAST(x AS DATE)`, `date(x)` and `CAST(x AS TIMESTAMP)` read text the way Trino's cast from
|
|
1125
|
+
`varchar` does. A date is `yyyy-MM-dd` and nothing else. A timestamp is a date, optionally
|
|
1126
|
+
followed by a space and a time, with no `T` and no zone. A timestamp renders as
|
|
1127
|
+
`2026-08-15 10:00:00.000`, and a fraction finer than the millisecond is rounded.
|
|
1128
|
+
- A cast reads its value as text only where it is a string literal or a column the catalog declares
|
|
1129
|
+
as `string`, `varchar` or `char`. Anything else, such as a `timestamp` column or the answer of a
|
|
1130
|
+
function, is taken to be a date or a timestamp already, and any ISO-8601 form of it is accepted.
|
|
1131
|
+
The answer is text in a `varchar` column, where Athena reports a `date` or a `timestamp`.
|
|
1111
1132
|
- A `decimal` column is held as a double. A value carrying more than about fifteen significant
|
|
1112
1133
|
digits loses the ones past that, and a filter, a sum or a group on it can then answer
|
|
1113
1134
|
differently from Athena's exact arithmetic.
|
package/package.json
CHANGED