@kensio/yulin 1.21.21 → 1.21.23

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