@kensio/yulin 1.21.22 → 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-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/docs/services/athena/README.md +21 -2
- 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,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
|
+
}
|
|
@@ -486,7 +486,7 @@ Every ascending sort is emitted carrying `NULLS LAST`, because Trino orders null
|
|
|
486
486
|
direction it sorts and SQLite orders them first ascending. Both were cases where a query answered
|
|
487
487
|
differently while still succeeding, which is the failure that costs the most to find.
|
|
488
488
|
|
|
489
|
-
### Flattening an array or a
|
|
489
|
+
### Flattening an array, a map or a split value
|
|
490
490
|
|
|
491
491
|
`UNNEST` runs. An array or a map column is held as its JSON text, and SQLite reads that with
|
|
492
492
|
`json_each`, so a statement flattening one returns a row per element the way Athena does.
|
|
@@ -505,6 +505,22 @@ falls back rather than reading a scalar as a collection.
|
|
|
505
505
|
One flattening per statement is what this covers, joined with `CROSS JOIN`. A second `UNNEST`, a
|
|
506
506
|
`LEFT JOIN UNNEST`, a `SELECT *` beside one, and a position taken from a map all fall back.
|
|
507
507
|
|
|
508
|
+
A table with no array column in it can still be flattened. `split` answers an array, and an `UNNEST`
|
|
509
|
+
over the call cuts one stored string into a row per part. This is how a packed access log row is
|
|
510
|
+
read, since Glue builds a log table's columns from what the delivery was configured with.
|
|
511
|
+
|
|
512
|
+
```sql
|
|
513
|
+
SELECT t.part
|
|
514
|
+
FROM rainlytics.logs
|
|
515
|
+
CROSS JOIN UNNEST(split(url_extract_parameter(cs_uri_stem || '?' || cs_uri_query, 'b'), ';'))
|
|
516
|
+
AS t(part)
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Splitting an empty value answers one empty part, as it does on Trino. A row whose value is null
|
|
520
|
+
answers no parts at all and drops out, the way an empty array column does. The call is what says the
|
|
521
|
+
value is a collection (an expression has no schema entry to read). `split` and `slice` are the two
|
|
522
|
+
calls the flattening reaches, and any other expression under an `UNNEST` falls back.
|
|
523
|
+
|
|
508
524
|
### The functions a statement can call
|
|
509
525
|
|
|
510
526
|
SQLite carries a much smaller function library than Trino, and the engine fills the gap for the ones
|
|
@@ -516,7 +532,7 @@ its declared result.
|
|
|
516
532
|
| Date and time | `current_date`, `current_timestamp`, `date_add`, `date_diff`, `date_trunc`, `date_format`, `at_timezone`, `from_unixtime`, `to_unixtime`, `from_iso8601_timestamp`, `from_iso8601_date`, `to_iso8601` |
|
|
517
533
|
| JSON | `json_extract`, `json_extract_scalar`, `json_parse`, `json_size` |
|
|
518
534
|
| Array and map | `array_agg`, `cardinality`, `contains`, `element_at`, `array_join`, `slice` |
|
|
519
|
-
| String | `regexp_like`, `regexp_extract`, `regexp_replace`, `split_part`, `strpos`
|
|
535
|
+
| String | `regexp_like`, `regexp_extract`, `regexp_replace`, `split`, `split_part`, `strpos` |
|
|
520
536
|
| Binary | `md5`, `sha1`, `sha256`, `sha512`, `xxhash64`, `murmur3`, `crc32`, `to_hex`, `from_hex`, `to_base64`, `from_base64`, `to_utf8`, `from_utf8` |
|
|
521
537
|
| URL | `url_extract_host`, `url_extract_path`, `url_extract_protocol`, `url_extract_port`, `url_extract_query`, `url_extract_fragment`, `url_extract_parameter`, `url_decode`, `url_encode` |
|
|
522
538
|
| Approximate | `approx_distinct`, `approx_percentile` |
|
|
@@ -992,6 +1008,7 @@ own and authorizes work on one against the workgroup it belongs to. This asks th
|
|
|
992
1008
|
- `StartQueryExecution`, `GetQueryExecution`, `GetQueryResults` and `StopQueryExecution`
|
|
993
1009
|
- A `SELECT` run for real over JSON lines and CSV objects in simulated S3, answered by SQLite
|
|
994
1010
|
- `UNNEST` over an array or a map column, with `WITH ORDINALITY` where a query wants the position
|
|
1011
|
+
- `UNNEST` over a `split` or a `slice` call, flattening one stored string into a row per part
|
|
995
1012
|
- Trino's date, JSON, array, string and URL functions, with `current_timestamp` reading the
|
|
996
1013
|
simulated clock
|
|
997
1014
|
- Declared results, matched on the query text, ahead of the engine for one statement and behind it
|
|
@@ -1027,6 +1044,8 @@ Current documented limitations:
|
|
|
1027
1044
|
gives a map's keys rather than its positions.
|
|
1028
1045
|
- `UNNEST` over a `ROW` or a struct array falls back. The element needs field access and the
|
|
1029
1046
|
flattened column is JSON text here.
|
|
1047
|
+
- `UNNEST` over an expression reaches `split` and `slice`. The engine reads the call to decide that
|
|
1048
|
+
a value is a collection, and a call to anything else falls back.
|
|
1030
1049
|
- The Trino function library reaches as far as the table under
|
|
1031
1050
|
[the functions a statement can call](#the-functions-a-statement-can-call). A query reaching for
|
|
1032
1051
|
anything else Trino has and SQLite lacks falls back.
|
package/package.json
CHANGED