@kensio/yulin 1.20.10 → 1.20.11
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-json-records.d.ts +23 -0
- package/dist/service/athena/engine/sim-athena-json-records.js +46 -0
- package/dist/service/athena/engine/sim-athena-object-codec.d.ts +13 -0
- package/dist/service/athena/engine/sim-athena-object-codec.js +40 -0
- package/dist/service/athena/engine/sim-athena-record-reader.js +26 -10
- package/dist/service/athena/engine/sim-athena-table-objects.d.ts +1 -1
- package/dist/service/athena/engine/sim-athena-table-objects.js +4 -2
- package/dist/service/athena/sim-athena-engine.fixture.d.ts +2 -0
- package/dist/service/athena/sim-athena-engine.fixture.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SimAthenaEngineRow } from "./sim-athena-engine-row.js";
|
|
2
|
+
/**
|
|
3
|
+
* How one table's JSON records reach its columns.
|
|
4
|
+
*
|
|
5
|
+
* `mappings` holds the column names the OpenX SerDe's `mapping.<column>`
|
|
6
|
+
* parameters declare, each against the record key it reads. A table declaring
|
|
7
|
+
* none leaves it empty, and every column is read by its own name.
|
|
8
|
+
*
|
|
9
|
+
* `caseInsensitive` is the SerDe's own `case.insensitive`, on unless a table
|
|
10
|
+
* turns it off. The SerDe folds a record's keys before looking one up. A
|
|
11
|
+
* mapping matches a key of any case until a table declares `FALSE`.
|
|
12
|
+
*/
|
|
13
|
+
export interface SimAthenaJsonFormat {
|
|
14
|
+
readonly mappings: ReadonlyMap<string, string>;
|
|
15
|
+
readonly caseInsensitive: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One object of JSON lines, read into rows.
|
|
19
|
+
*
|
|
20
|
+
* A nested object or array is kept as its JSON text, which is what makes
|
|
21
|
+
* `json_extract_scalar` and `cardinality` reach into it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function simAthenaJsonRows(text: string, format: SimAthenaJsonFormat): readonly SimAthenaEngineRow[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One object of JSON lines, read into rows.
|
|
3
|
+
*
|
|
4
|
+
* A nested object or array is kept as its JSON text, which is what makes
|
|
5
|
+
* `json_extract_scalar` and `cardinality` reach into it.
|
|
6
|
+
*/
|
|
7
|
+
export function simAthenaJsonRows(text, format) {
|
|
8
|
+
return text
|
|
9
|
+
.split("\n")
|
|
10
|
+
.filter((line) => line.trim().length > 0)
|
|
11
|
+
.map((line) => JSON.parse(line))
|
|
12
|
+
.map((record) => mappedRow(record, format));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One record with its mapped columns laid over it.
|
|
16
|
+
*
|
|
17
|
+
* The record is kept whole underneath. A column no mapping names still reads
|
|
18
|
+
* by its own name. A mapped column is written whether or not the record
|
|
19
|
+
* carries the key, since the mapping is where that column reads from.
|
|
20
|
+
*/
|
|
21
|
+
function mappedRow(record, format) {
|
|
22
|
+
if (format.mappings.size === 0) {
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
const row = { ...record };
|
|
26
|
+
for (const [column, key] of format.mappings) {
|
|
27
|
+
row[column] = mappedValue(record, key, format.caseInsensitive);
|
|
28
|
+
}
|
|
29
|
+
return row;
|
|
30
|
+
}
|
|
31
|
+
function mappedValue(record, key, caseInsensitive) {
|
|
32
|
+
const exact = record[key];
|
|
33
|
+
if (exact !== undefined) {
|
|
34
|
+
return exact;
|
|
35
|
+
}
|
|
36
|
+
if (!caseInsensitive) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const wanted = key.toLowerCase();
|
|
40
|
+
for (const [held, value] of Object.entries(record)) {
|
|
41
|
+
if (held.toLowerCase() === wanted) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One object's bytes, decompressed by the codec its key names.
|
|
3
|
+
*
|
|
4
|
+
* Athena reads the file extension, and so does this. A key ending `.gz` is
|
|
5
|
+
* gzip whatever its bytes hold, and a key ending anything else is text. The
|
|
6
|
+
* magic number would be the other way to tell, and it would read an object
|
|
7
|
+
* real Athena skips.
|
|
8
|
+
*
|
|
9
|
+
* A key naming a codec this simulation has no decompressor for raises, and the
|
|
10
|
+
* engine turns the query down. The declaration a test wrote answers it, which
|
|
11
|
+
* is what an object the engine cannot open already does.
|
|
12
|
+
*/
|
|
13
|
+
export declare function simAthenaDecompressedBytes(key: string, bytes: Buffer): Buffer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { gunzipSync, inflateSync, zstdDecompressSync } from "node:zlib";
|
|
2
|
+
import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
|
|
3
|
+
/** The file extensions naming a codec, against what decompresses each one. */
|
|
4
|
+
const codecs = new Map([
|
|
5
|
+
["gz", gunzipSync],
|
|
6
|
+
["zst", zstdDecompressSync],
|
|
7
|
+
["deflate", inflateSync],
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* The file extensions naming a codec nothing here decompresses.
|
|
11
|
+
*
|
|
12
|
+
* Node's standard library has gzip, zstd and deflate. The rest would need a
|
|
13
|
+
* dependency, and this package takes none for a test to install.
|
|
14
|
+
*/
|
|
15
|
+
const unreadableCodecs = new Set(["bz2", "bzip2", "lz4", "lzo", "snappy"]);
|
|
16
|
+
/**
|
|
17
|
+
* One object's bytes, decompressed by the codec its key names.
|
|
18
|
+
*
|
|
19
|
+
* Athena reads the file extension, and so does this. A key ending `.gz` is
|
|
20
|
+
* gzip whatever its bytes hold, and a key ending anything else is text. The
|
|
21
|
+
* magic number would be the other way to tell, and it would read an object
|
|
22
|
+
* real Athena skips.
|
|
23
|
+
*
|
|
24
|
+
* A key naming a codec this simulation has no decompressor for raises, and the
|
|
25
|
+
* engine turns the query down. The declaration a test wrote answers it, which
|
|
26
|
+
* is what an object the engine cannot open already does.
|
|
27
|
+
*/
|
|
28
|
+
export function simAthenaDecompressedBytes(key, bytes) {
|
|
29
|
+
const name = key.slice(key.lastIndexOf("/") + 1);
|
|
30
|
+
const dot = name.lastIndexOf(".");
|
|
31
|
+
if (dot === -1) {
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
const extension = name.slice(dot + 1).toLowerCase();
|
|
35
|
+
if (unreadableCodecs.has(extension)) {
|
|
36
|
+
throw new SimAthenaSetUpError(`Unsupported sim Athena compression: ${extension}`);
|
|
37
|
+
}
|
|
38
|
+
const decompress = codecs.get(extension);
|
|
39
|
+
return decompress === undefined ? bytes : decompress(bytes);
|
|
40
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { simAthenaDelimitedRows, } from "./sim-athena-delimited-records.js";
|
|
2
|
+
import { simAthenaJsonRows, } from "./sim-athena-json-records.js";
|
|
3
|
+
/** The SerDe class name that takes `mapping.<column>` parameters. */
|
|
4
|
+
const openXSerDe = "org.openx.data.jsonserde.jsonserde";
|
|
2
5
|
/** The SerDe class names that mean JSON lines. */
|
|
3
6
|
const jsonSerDes = new Set([
|
|
4
|
-
|
|
7
|
+
openXSerDe,
|
|
5
8
|
"org.apache.hive.hcatalog.data.jsonserde",
|
|
6
9
|
"org.apache.hadoop.hive.serde2.jsonserde",
|
|
7
10
|
]);
|
|
11
|
+
/** What a `mapping.<column>` parameter is named with. */
|
|
12
|
+
const mappingPrefix = "mapping.";
|
|
8
13
|
/**
|
|
9
14
|
* The SerDe class names that mean delimited text, and what each one reads
|
|
10
15
|
* before the table's own parameters are applied.
|
|
@@ -39,7 +44,8 @@ export function simAthenaRecordReader(table) {
|
|
|
39
44
|
return undefined;
|
|
40
45
|
}
|
|
41
46
|
if (jsonSerDes.has(library)) {
|
|
42
|
-
|
|
47
|
+
const format = jsonFormat(table, library);
|
|
48
|
+
return (text) => simAthenaJsonRows(text, format);
|
|
43
49
|
}
|
|
44
50
|
const defaults = delimitedSerDes.get(library);
|
|
45
51
|
if (defaults === undefined) {
|
|
@@ -50,16 +56,26 @@ export function simAthenaRecordReader(table) {
|
|
|
50
56
|
return (text) => simAthenaDelimitedRows(text, format, columns);
|
|
51
57
|
}
|
|
52
58
|
/**
|
|
53
|
-
* JSON
|
|
59
|
+
* How one table's JSON records reach its columns.
|
|
54
60
|
*
|
|
55
|
-
*
|
|
56
|
-
* `
|
|
61
|
+
* Only the OpenX SerDe takes the mappings. The Hive JSON SerDes have no
|
|
62
|
+
* `mapping` property, and a table declaring one against them reads by its
|
|
63
|
+
* column names on real Athena the same as here.
|
|
57
64
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
function jsonFormat(table, library) {
|
|
66
|
+
const parameters = library === openXSerDe
|
|
67
|
+
? (table.storageDescriptor?.SerdeInfo?.Parameters ?? {})
|
|
68
|
+
: {};
|
|
69
|
+
const mappings = new Map();
|
|
70
|
+
for (const [name, key] of Object.entries(parameters)) {
|
|
71
|
+
if (name.toLowerCase().startsWith(mappingPrefix)) {
|
|
72
|
+
mappings.set(name.slice(mappingPrefix.length), key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
mappings,
|
|
77
|
+
caseInsensitive: parameters["case.insensitive"]?.toLowerCase() !== "false",
|
|
78
|
+
};
|
|
63
79
|
}
|
|
64
80
|
function delimitedFormat(table, defaults) {
|
|
65
81
|
const parameters = table.storageDescriptor?.SerdeInfo?.Parameters ?? {};
|
|
@@ -26,5 +26,5 @@ export interface SimAthenaTableObjects extends SimAthenaScannedObjects {
|
|
|
26
26
|
* there because nothing holds the data it would answer from.
|
|
27
27
|
*/
|
|
28
28
|
export declare function simAthenaTableObjects(s3: Partial<SimAthenaTableObjects> | undefined): SimAthenaTableObjects | undefined;
|
|
29
|
-
/** One object's bytes, read as UTF-8 text. */
|
|
29
|
+
/** One object's bytes, decompressed by its key and read as UTF-8 text. */
|
|
30
30
|
export declare function simAthenaObjectText(objects: SimAthenaTableObjects, bucket: string, key: string, caller: SimAwsCaller | undefined): Promise<string>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simAthenaDecompressedBytes } from "./sim-athena-object-codec.js";
|
|
1
2
|
/**
|
|
2
3
|
* Whether this simulated S3 can open an object as well as list one.
|
|
3
4
|
*
|
|
@@ -9,9 +10,10 @@ export function simAthenaTableObjects(s3) {
|
|
|
9
10
|
? undefined
|
|
10
11
|
: s3;
|
|
11
12
|
}
|
|
12
|
-
/** One object's bytes, read as UTF-8 text. */
|
|
13
|
+
/** One object's bytes, decompressed by its key and read as UTF-8 text. */
|
|
13
14
|
export async function simAthenaObjectText(objects, bucket, key, caller) {
|
|
14
15
|
const got = await objects.getObject({ input: { Bucket: bucket, Key: key } }, caller === undefined ? undefined : { caller });
|
|
15
16
|
const chunks = await Array.fromAsync(got.Body ?? []);
|
|
16
|
-
|
|
17
|
+
const bytes = simAthenaDecompressedBytes(key, Buffer.concat(chunks));
|
|
18
|
+
return new TextDecoder().decode(bytes);
|
|
17
19
|
}
|
|
@@ -34,5 +34,7 @@ export declare function anEngineSimulation(clock?: SimClock): Promise<SimAthenaE
|
|
|
34
34
|
export declare function aCatalogTable(simAws: SimAws, table: SimAthenaEngineTableInput): void;
|
|
35
35
|
/** Put one object of literal text under the logs Bucket. */
|
|
36
36
|
export declare function aSeededObject(simAws: SimAws, key: string, body: string): Promise<void>;
|
|
37
|
+
/** Put one object of raw bytes under the logs Bucket. */
|
|
38
|
+
export declare function aSeededBytes(simAws: SimAws, key: string, body: Uint8Array): Promise<void>;
|
|
37
39
|
/** Put one object of JSON lines under the logs Bucket. */
|
|
38
40
|
export declare function aSeededJson(simAws: SimAws, key: string, records: readonly Record<string, unknown>[]): Promise<void>;
|
|
@@ -58,6 +58,12 @@ export async function aSeededObject(simAws, key, body) {
|
|
|
58
58
|
.s3()
|
|
59
59
|
.putObject({ input: { Bucket: logsBucket, Key: key, Body: body } });
|
|
60
60
|
}
|
|
61
|
+
/** Put one object of raw bytes under the logs Bucket. */
|
|
62
|
+
export async function aSeededBytes(simAws, key, body) {
|
|
63
|
+
await simAws
|
|
64
|
+
.s3()
|
|
65
|
+
.putObject({ input: { Bucket: logsBucket, Key: key, Body: body } });
|
|
66
|
+
}
|
|
61
67
|
/** Put one object of JSON lines under the logs Bucket. */
|
|
62
68
|
export async function aSeededJson(simAws, key, records) {
|
|
63
69
|
const lines = records.map((record) => JSON.stringify(record));
|
package/package.json
CHANGED