@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.
@@ -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
- "org.openx.data.jsonserde.jsonserde",
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
- return jsonRows;
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 lines, one record per line.
59
+ * How one table's JSON records reach its columns.
54
60
  *
55
- * A nested object or array is kept as its JSON text, which is what makes
56
- * `json_extract_scalar` and `cardinality` reach into it.
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 jsonRows(text) {
59
- return text
60
- .split("\n")
61
- .filter((line) => line.trim().length > 0)
62
- .map((line) => JSON.parse(line));
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
- return new TextDecoder().decode(Buffer.concat(chunks));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.20.10",
3
+ "version": "1.20.11",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",