@gugananuvem/aws-local-simulator 1.0.14 → 1.0.15
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/README.md +594 -481
- package/bin/aws-local-simulator.js +63 -63
- package/package.json +11 -10
- package/src/config/config-loader.js +114 -114
- package/src/config/default-config.js +68 -68
- package/src/config/env-loader.js +68 -68
- package/src/index.js +146 -146
- package/src/index.mjs +123 -123
- package/src/server.js +227 -227
- package/src/services/apigateway/index.js +73 -73
- package/src/services/apigateway/server.js +507 -507
- package/src/services/apigateway/simulator.js +1261 -1261
- package/src/services/athena/index.js +75 -75
- package/src/services/athena/server.js +101 -101
- package/src/services/athena/simulador.js +998 -998
- package/src/services/athena/simulator.js +346 -346
- package/src/services/cloudformation/index.js +106 -106
- package/src/services/cloudformation/server.js +417 -417
- package/src/services/cloudformation/simulador.js +1045 -1045
- package/src/services/cloudtrail/index.js +84 -84
- package/src/services/cloudtrail/server.js +235 -235
- package/src/services/cloudtrail/simulador.js +719 -719
- package/src/services/cloudwatch/index.js +84 -84
- package/src/services/cloudwatch/server.js +366 -366
- package/src/services/cloudwatch/simulador.js +1173 -1173
- package/src/services/cognito/index.js +79 -70
- package/src/services/cognito/server.js +301 -279
- package/src/services/cognito/simulator.js +1655 -1119
- package/src/services/config/index.js +96 -96
- package/src/services/config/server.js +215 -215
- package/src/services/config/simulador.js +1260 -1260
- package/src/services/dynamodb/index.js +74 -74
- package/src/services/dynamodb/server.js +125 -123
- package/src/services/dynamodb/simulator.js +630 -630
- package/src/services/ecs/index.js +65 -65
- package/src/services/ecs/server.js +235 -233
- package/src/services/ecs/simulator.js +844 -844
- package/src/services/eventbridge/index.js +89 -89
- package/src/services/eventbridge/server.js +209 -209
- package/src/services/eventbridge/simulator.js +684 -684
- package/src/services/index.js +45 -45
- package/src/services/kms/index.js +75 -75
- package/src/services/kms/server.js +67 -67
- package/src/services/kms/simulator.js +324 -324
- package/src/services/lambda/handler-loader.js +183 -183
- package/src/services/lambda/index.js +78 -78
- package/src/services/lambda/route-registry.js +274 -274
- package/src/services/lambda/server.js +145 -145
- package/src/services/lambda/simulator.js +199 -182
- package/src/services/parameter-store/index.js +80 -80
- package/src/services/parameter-store/server.js +50 -50
- package/src/services/parameter-store/simulator.js +201 -201
- package/src/services/s3/index.js +73 -73
- package/src/services/s3/server.js +329 -245
- package/src/services/s3/simulator.js +565 -496
- package/src/services/secret-manager/index.js +80 -80
- package/src/services/secret-manager/server.js +50 -50
- package/src/services/secret-manager/simulator.js +171 -171
- package/src/services/sns/index.js +89 -89
- package/src/services/sns/server.js +580 -580
- package/src/services/sns/simulator.js +1482 -1482
- package/src/services/sqs/index.js +93 -93
- package/src/services/sqs/server.js +349 -347
- package/src/services/sqs/simulator.js +441 -441
- package/src/services/sts/index.js +37 -37
- package/src/services/sts/server.js +144 -142
- package/src/services/sts/simulator.js +69 -69
- package/src/services/xray/index.js +83 -83
- package/src/services/xray/server.js +308 -308
- package/src/services/xray/simulador.js +994 -994
- package/src/template/aws-config-template.js +87 -87
- package/src/template/aws-config-template.mjs +90 -90
- package/src/template/config-template.json +203 -203
- package/src/utils/aws-config.js +91 -91
- package/src/utils/cloudtrail-audit.js +129 -129
- package/src/utils/local-store.js +83 -83
- package/src/utils/logger.js +59 -59
|
@@ -1,998 +1,998 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @fileoverview Athena Simulator
|
|
5
|
-
*
|
|
6
|
-
* Suporta:
|
|
7
|
-
* Query Execution:
|
|
8
|
-
* - StartQueryExecution
|
|
9
|
-
* - StopQueryExecution
|
|
10
|
-
* - GetQueryExecution
|
|
11
|
-
* - ListQueryExecutions
|
|
12
|
-
* - BatchGetQueryExecution
|
|
13
|
-
*
|
|
14
|
-
* Query Results:
|
|
15
|
-
* - GetQueryResults
|
|
16
|
-
*
|
|
17
|
-
* Named Queries:
|
|
18
|
-
* - CreateNamedQuery
|
|
19
|
-
* - DeleteNamedQuery
|
|
20
|
-
* - GetNamedQuery
|
|
21
|
-
* - ListNamedQueries
|
|
22
|
-
* - BatchGetNamedQuery
|
|
23
|
-
*
|
|
24
|
-
* Workgroups:
|
|
25
|
-
* - CreateWorkGroup
|
|
26
|
-
* - DeleteWorkGroup
|
|
27
|
-
* - UpdateWorkGroup
|
|
28
|
-
* - GetWorkGroup
|
|
29
|
-
* - ListWorkGroups
|
|
30
|
-
*
|
|
31
|
-
* Data Catalogs:
|
|
32
|
-
* - CreateDataCatalog
|
|
33
|
-
* - DeleteDataCatalog
|
|
34
|
-
* - UpdateDataCatalog
|
|
35
|
-
* - GetDataCatalog
|
|
36
|
-
* - ListDataCatalogs
|
|
37
|
-
*
|
|
38
|
-
* Databases:
|
|
39
|
-
* - ListDatabases
|
|
40
|
-
* - GetDatabase
|
|
41
|
-
*
|
|
42
|
-
* TableMetadata:
|
|
43
|
-
* - ListTableMetadata
|
|
44
|
-
* - GetTableMetadata
|
|
45
|
-
*
|
|
46
|
-
* Prepared Statements:
|
|
47
|
-
* - CreatePreparedStatement
|
|
48
|
-
* - UpdatePreparedStatement
|
|
49
|
-
* - DeletePreparedStatement
|
|
50
|
-
* - GetPreparedStatement
|
|
51
|
-
* - ListPreparedStatements
|
|
52
|
-
*
|
|
53
|
-
* Tags:
|
|
54
|
-
* - TagResource / UntagResource / ListTagsForResource
|
|
55
|
-
*
|
|
56
|
-
* Integração:
|
|
57
|
-
* - Execução de queries simuladas contra dados do S3
|
|
58
|
-
* - Suporte a DDL (CREATE TABLE, CREATE DATABASE, DROP TABLE, DROP DATABASE)
|
|
59
|
-
* - Suporte a DML (SELECT, INSERT, UPDATE, DELETE)
|
|
60
|
-
* - Parser simples de SQL para retornar resultados simulados
|
|
61
|
-
* - Persistência via LocalStore
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
const { randomUUID } = require('crypto');
|
|
65
|
-
|
|
66
|
-
// ─── Erros tipados ────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
class AthenaError extends Error {
|
|
69
|
-
constructor(code, message, statusCode = 400) {
|
|
70
|
-
super(message);
|
|
71
|
-
this.code = code;
|
|
72
|
-
this.statusCode = statusCode;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const Errors = {
|
|
77
|
-
InvalidRequest: (msg) =>
|
|
78
|
-
new AthenaError('InvalidRequestException', msg, 400),
|
|
79
|
-
QueryNotFound: (id) =>
|
|
80
|
-
new AthenaError('InvalidRequestException', `Query execution not found: ${id}`, 400),
|
|
81
|
-
NamedQueryNotFound: (id) =>
|
|
82
|
-
new AthenaError('InvalidRequestException', `Named query not found: ${id}`, 400),
|
|
83
|
-
WorkGroupNotFound: (name) =>
|
|
84
|
-
new AthenaError('InvalidRequestException', `WorkGroup not found: ${name}`, 400),
|
|
85
|
-
WorkGroupAlreadyExists: (name) =>
|
|
86
|
-
new AthenaError('InvalidRequestException', `WorkGroup already exists: ${name}`, 400),
|
|
87
|
-
DataCatalogNotFound: (name) =>
|
|
88
|
-
new AthenaError('InvalidRequestException', `DataCatalog not found: ${name}`, 404),
|
|
89
|
-
DataCatalogAlreadyExists: (name) =>
|
|
90
|
-
new AthenaError('InvalidRequestException', `DataCatalog already exists: ${name}`, 400),
|
|
91
|
-
DatabaseNotFound: (name) =>
|
|
92
|
-
new AthenaError('MetadataException', `Database not found: ${name}`, 404),
|
|
93
|
-
TableNotFound: (name) =>
|
|
94
|
-
new AthenaError('MetadataException', `Table not found: ${name}`, 404),
|
|
95
|
-
PreparedStatementNotFound: (name) =>
|
|
96
|
-
new AthenaError('ResourceNotFoundException', `Prepared statement not found: ${name}`, 404),
|
|
97
|
-
TooManyRequests: () =>
|
|
98
|
-
new AthenaError('TooManyRequestsException', 'Too many requests', 429),
|
|
99
|
-
QueryAlreadyStopped: (id) =>
|
|
100
|
-
new AthenaError('InvalidRequestException', `Query execution already stopped: ${id}`, 400),
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// ─── Parser SQL simples ───────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
function parseSql(sql) {
|
|
106
|
-
const normalized = sql.trim().replace(/\s+/g, ' ').toUpperCase();
|
|
107
|
-
|
|
108
|
-
if (normalized.startsWith('SELECT')) return { type: 'SELECT', sql };
|
|
109
|
-
if (normalized.startsWith('INSERT')) return { type: 'INSERT', sql };
|
|
110
|
-
if (normalized.startsWith('UPDATE')) return { type: 'UPDATE', sql };
|
|
111
|
-
if (normalized.startsWith('DELETE')) return { type: 'DELETE', sql };
|
|
112
|
-
if (normalized.startsWith('CREATE TABLE')) return { type: 'CREATE_TABLE', sql };
|
|
113
|
-
if (normalized.startsWith('CREATE DATABASE') || normalized.startsWith('CREATE SCHEMA')) return { type: 'CREATE_DATABASE', sql };
|
|
114
|
-
if (normalized.startsWith('DROP TABLE')) return { type: 'DROP_TABLE', sql };
|
|
115
|
-
if (normalized.startsWith('DROP DATABASE') || normalized.startsWith('DROP SCHEMA')) return { type: 'DROP_DATABASE', sql };
|
|
116
|
-
if (normalized.startsWith('SHOW')) return { type: 'SHOW', sql };
|
|
117
|
-
if (normalized.startsWith('DESCRIBE') || normalized.startsWith('DESC')) return { type: 'DESCRIBE', sql };
|
|
118
|
-
if (normalized.startsWith('ALTER')) return { type: 'ALTER', sql };
|
|
119
|
-
if (normalized.startsWith('MSCK REPAIR TABLE')) return { type: 'MSCK_REPAIR', sql };
|
|
120
|
-
|
|
121
|
-
return { type: 'UNKNOWN', sql };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function generateSimulatedResults(parsed) {
|
|
125
|
-
switch (parsed.type) {
|
|
126
|
-
case 'SELECT': {
|
|
127
|
-
const columns = ['id', 'name', 'value', 'timestamp'];
|
|
128
|
-
const rows = Array.from({ length: 3 }, (_, i) => ({
|
|
129
|
-
Data: [
|
|
130
|
-
{ VarCharValue: String(i + 1) },
|
|
131
|
-
{ VarCharValue: `item_${i + 1}` },
|
|
132
|
-
{ VarCharValue: String(Math.floor(Math.random() * 1000)) },
|
|
133
|
-
{ VarCharValue: new Date().toISOString() },
|
|
134
|
-
],
|
|
135
|
-
}));
|
|
136
|
-
return {
|
|
137
|
-
ResultSet: {
|
|
138
|
-
Rows: [
|
|
139
|
-
{ Data: columns.map((c) => ({ VarCharValue: c })) },
|
|
140
|
-
...rows,
|
|
141
|
-
],
|
|
142
|
-
ResultSetMetadata: {
|
|
143
|
-
ColumnInfo: columns.map((c, i) => ({
|
|
144
|
-
CatalogName: 'hive',
|
|
145
|
-
SchemaName: '',
|
|
146
|
-
TableName: '',
|
|
147
|
-
Name: c,
|
|
148
|
-
Label: c,
|
|
149
|
-
Type: i === 3 ? 'varchar' : i === 2 ? 'bigint' : 'varchar',
|
|
150
|
-
Precision: 2147483647,
|
|
151
|
-
Scale: 0,
|
|
152
|
-
Nullable: 'UNKNOWN',
|
|
153
|
-
CaseSensitive: i !== 2,
|
|
154
|
-
})),
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
NextToken: null,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
case 'CREATE_TABLE':
|
|
161
|
-
case 'CREATE_DATABASE':
|
|
162
|
-
case 'DROP_TABLE':
|
|
163
|
-
case 'DROP_DATABASE':
|
|
164
|
-
case 'INSERT':
|
|
165
|
-
case 'UPDATE':
|
|
166
|
-
case 'DELETE':
|
|
167
|
-
case 'ALTER':
|
|
168
|
-
case 'MSCK_REPAIR':
|
|
169
|
-
return {
|
|
170
|
-
ResultSet: { Rows: [], ResultSetMetadata: { ColumnInfo: [] } },
|
|
171
|
-
NextToken: null,
|
|
172
|
-
UpdateCount: 1,
|
|
173
|
-
};
|
|
174
|
-
case 'SHOW': {
|
|
175
|
-
return {
|
|
176
|
-
ResultSet: {
|
|
177
|
-
Rows: [{ Data: [{ VarCharValue: 'tab_name' }] }],
|
|
178
|
-
ResultSetMetadata: { ColumnInfo: [{ Name: 'tab_name', Type: 'varchar' }] },
|
|
179
|
-
},
|
|
180
|
-
NextToken: null,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
default:
|
|
184
|
-
return {
|
|
185
|
-
ResultSet: { Rows: [], ResultSetMetadata: { ColumnInfo: [] } },
|
|
186
|
-
NextToken: null,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ─── Simulador ────────────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
class AthenaSimulator {
|
|
194
|
-
constructor(config, store, logger) {
|
|
195
|
-
this.config = config;
|
|
196
|
-
this.store = store;
|
|
197
|
-
this.logger = logger;
|
|
198
|
-
this.region = config?.region || 'us-east-1';
|
|
199
|
-
this.accountId = config?.accountId || '000000000000';
|
|
200
|
-
|
|
201
|
-
// State
|
|
202
|
-
this.queryExecutions = new Map(); // executionId → execution
|
|
203
|
-
this.queryResults = new Map(); // executionId → results
|
|
204
|
-
this.namedQueries = new Map(); // queryId → namedQuery
|
|
205
|
-
this.workgroups = new Map(); // name → workgroup
|
|
206
|
-
this.dataCatalogs = new Map(); // name → catalog
|
|
207
|
-
this.databases = new Map(); // catalogName.dbName → database
|
|
208
|
-
this.tables = new Map(); // catalogName.dbName.tableName → table
|
|
209
|
-
this.preparedStatements = new Map();// workgroup.name → statement
|
|
210
|
-
this.tags = new Map(); // arn → tags
|
|
211
|
-
|
|
212
|
-
// Injeções cross-service
|
|
213
|
-
this.s3Simulator = null;
|
|
214
|
-
|
|
215
|
-
// Workgroup padrão
|
|
216
|
-
this._initDefaults();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
_initDefaults() {
|
|
220
|
-
this.workgroups.set('primary', {
|
|
221
|
-
Name: 'primary',
|
|
222
|
-
State: 'ENABLED',
|
|
223
|
-
Description: 'Primary workgroup',
|
|
224
|
-
Configuration: {
|
|
225
|
-
ResultConfiguration: {
|
|
226
|
-
OutputLocation: 's3://aws-athena-query-results-local/',
|
|
227
|
-
EncryptionConfiguration: null,
|
|
228
|
-
},
|
|
229
|
-
EnforceWorkGroupConfiguration: false,
|
|
230
|
-
PublishCloudWatchMetricsEnabled: false,
|
|
231
|
-
BytesScannedCutoffPerQuery: null,
|
|
232
|
-
RequesterPaysEnabled: false,
|
|
233
|
-
EngineVersion: { SelectedEngineVersion: 'Athena engine version 3', EffectiveEngineVersion: 'Athena engine version 3' },
|
|
234
|
-
},
|
|
235
|
-
CreationTime: new Date().toISOString(),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
this.dataCatalogs.set('AwsDataCatalog', {
|
|
239
|
-
Name: 'AwsDataCatalog',
|
|
240
|
-
Description: 'AWS Glue based Data Catalog',
|
|
241
|
-
Type: 'GLUE',
|
|
242
|
-
Parameters: {},
|
|
243
|
-
Tags: [],
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Banco de dados padrão
|
|
247
|
-
this.databases.set('AwsDataCatalog.default', {
|
|
248
|
-
Name: 'default',
|
|
249
|
-
Description: 'Default Hive database',
|
|
250
|
-
Parameters: {},
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ── Persistência ──────────────────────────────────────────────────────────
|
|
255
|
-
|
|
256
|
-
async load() {
|
|
257
|
-
try {
|
|
258
|
-
const data = await this.store.load('athena');
|
|
259
|
-
if (!data) return;
|
|
260
|
-
|
|
261
|
-
if (data.queryExecutions) {
|
|
262
|
-
for (const [k, v] of Object.entries(data.queryExecutions)) {
|
|
263
|
-
this.queryExecutions.set(k, v);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (data.queryResults) {
|
|
267
|
-
for (const [k, v] of Object.entries(data.queryResults)) {
|
|
268
|
-
this.queryResults.set(k, v);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (data.namedQueries) {
|
|
272
|
-
for (const [k, v] of Object.entries(data.namedQueries)) {
|
|
273
|
-
this.namedQueries.set(k, v);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (data.workgroups) {
|
|
277
|
-
for (const [k, v] of Object.entries(data.workgroups)) {
|
|
278
|
-
this.workgroups.set(k, v);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (data.dataCatalogs) {
|
|
282
|
-
for (const [k, v] of Object.entries(data.dataCatalogs)) {
|
|
283
|
-
this.dataCatalogs.set(k, v);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (data.databases) {
|
|
287
|
-
for (const [k, v] of Object.entries(data.databases)) {
|
|
288
|
-
this.databases.set(k, v);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (data.tables) {
|
|
292
|
-
for (const [k, v] of Object.entries(data.tables)) {
|
|
293
|
-
this.tables.set(k, v);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (data.preparedStatements) {
|
|
297
|
-
for (const [k, v] of Object.entries(data.preparedStatements)) {
|
|
298
|
-
this.preparedStatements.set(k, v);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (data.tags) {
|
|
302
|
-
for (const [k, v] of Object.entries(data.tags)) {
|
|
303
|
-
this.tags.set(k, v);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
this.logger.debug('[Athena] State loaded from store');
|
|
308
|
-
} catch (err) {
|
|
309
|
-
this.logger.warn('[Athena] Could not load state:', err.message);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async save() {
|
|
314
|
-
try {
|
|
315
|
-
await this.store.save('athena', {
|
|
316
|
-
queryExecutions: Object.fromEntries(this.queryExecutions),
|
|
317
|
-
queryResults: Object.fromEntries(this.queryResults),
|
|
318
|
-
namedQueries: Object.fromEntries(this.namedQueries),
|
|
319
|
-
workgroups: Object.fromEntries(this.workgroups),
|
|
320
|
-
dataCatalogs: Object.fromEntries(this.dataCatalogs),
|
|
321
|
-
databases: Object.fromEntries(this.databases),
|
|
322
|
-
tables: Object.fromEntries(this.tables),
|
|
323
|
-
preparedStatements: Object.fromEntries(this.preparedStatements),
|
|
324
|
-
tags: Object.fromEntries(this.tags),
|
|
325
|
-
});
|
|
326
|
-
} catch (err) {
|
|
327
|
-
this.logger.warn('[Athena] Could not save state:', err.message);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
reset() {
|
|
332
|
-
this.queryExecutions.clear();
|
|
333
|
-
this.queryResults.clear();
|
|
334
|
-
this.namedQueries.clear();
|
|
335
|
-
this.workgroups.clear();
|
|
336
|
-
this.dataCatalogs.clear();
|
|
337
|
-
this.databases.clear();
|
|
338
|
-
this.tables.clear();
|
|
339
|
-
this.preparedStatements.clear();
|
|
340
|
-
this.tags.clear();
|
|
341
|
-
this._initDefaults();
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
_arn(type, name) {
|
|
347
|
-
return `arn:aws:athena:${this.region}:${this.accountId}:${type}/${name}`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
_wgArn(name) { return this._arn('workgroup', name); }
|
|
351
|
-
_catalogArn(name) { return this._arn('datacatalog', name); }
|
|
352
|
-
|
|
353
|
-
_resolveWorkgroup(name) {
|
|
354
|
-
const wgName = name || 'primary';
|
|
355
|
-
const wg = this.workgroups.get(wgName);
|
|
356
|
-
if (!wg) throw Errors.WorkGroupNotFound(wgName);
|
|
357
|
-
return wg;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
_resolveOutputLocation(params, wg) {
|
|
361
|
-
return (
|
|
362
|
-
params.ResultConfiguration?.OutputLocation ||
|
|
363
|
-
wg.Configuration?.ResultConfiguration?.OutputLocation ||
|
|
364
|
-
`s3://aws-athena-query-results-${this.accountId}-${this.region}/`
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
_simulateQueryAsync(executionId, parsed) {
|
|
369
|
-
const delay = 200 + Math.floor(Math.random() * 300);
|
|
370
|
-
setTimeout(() => {
|
|
371
|
-
const execution = this.queryExecutions.get(executionId);
|
|
372
|
-
if (!execution || execution.Status.State === 'CANCELLED') return;
|
|
373
|
-
|
|
374
|
-
const results = generateSimulatedResults(parsed);
|
|
375
|
-
const dataScanned = Math.floor(Math.random() * 10000000);
|
|
376
|
-
|
|
377
|
-
execution.Status.State = 'SUCCEEDED';
|
|
378
|
-
execution.Status.CompletionDateTime = new Date().toISOString();
|
|
379
|
-
execution.Statistics = {
|
|
380
|
-
EngineExecutionTimeInMillis: delay,
|
|
381
|
-
DataScannedInBytes: dataScanned,
|
|
382
|
-
DataManifestLocation: execution.ResultConfiguration.OutputLocation + executionId + '-manifest.csv',
|
|
383
|
-
TotalExecutionTimeInMillis: delay + 50,
|
|
384
|
-
QueryQueueTimeInMillis: 50,
|
|
385
|
-
QueryPlanningTimeInMillis: 30,
|
|
386
|
-
ServiceProcessingTimeInMillis: 20,
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
this.queryResults.set(executionId, results);
|
|
390
|
-
this.queryExecutions.set(executionId, execution);
|
|
391
|
-
this.save();
|
|
392
|
-
}, delay);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// ── Query Execution ────────────────────────────────────────────────────────
|
|
396
|
-
|
|
397
|
-
startQueryExecution(params) {
|
|
398
|
-
const { QueryString, ClientRequestToken, QueryExecutionContext, ResultConfiguration, WorkGroup } = params;
|
|
399
|
-
|
|
400
|
-
if (!QueryString) throw Errors.InvalidRequest('QueryString is required');
|
|
401
|
-
|
|
402
|
-
const wg = this._resolveWorkgroup(WorkGroup);
|
|
403
|
-
const executionId = ClientRequestToken || randomUUID();
|
|
404
|
-
|
|
405
|
-
if (this.queryExecutions.has(executionId)) {
|
|
406
|
-
return { QueryExecutionId: executionId };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const parsed = parseSql(QueryString);
|
|
410
|
-
const outputLocation = this._resolveOutputLocation(params, wg);
|
|
411
|
-
|
|
412
|
-
const execution = {
|
|
413
|
-
QueryExecutionId: executionId,
|
|
414
|
-
Query: QueryString,
|
|
415
|
-
StatementType: this._getStatementType(parsed.type),
|
|
416
|
-
ResultConfiguration: {
|
|
417
|
-
OutputLocation: outputLocation + executionId + '.csv',
|
|
418
|
-
EncryptionConfiguration: params.ResultConfiguration?.EncryptionConfiguration || null,
|
|
419
|
-
},
|
|
420
|
-
QueryExecutionContext: {
|
|
421
|
-
Database: QueryExecutionContext?.Database || 'default',
|
|
422
|
-
Catalog: QueryExecutionContext?.Catalog || 'AwsDataCatalog',
|
|
423
|
-
},
|
|
424
|
-
Status: {
|
|
425
|
-
State: 'RUNNING',
|
|
426
|
-
SubmissionDateTime: new Date().toISOString(),
|
|
427
|
-
CompletionDateTime: null,
|
|
428
|
-
StateChangeReason: null,
|
|
429
|
-
AthenaError: null,
|
|
430
|
-
},
|
|
431
|
-
Statistics: null,
|
|
432
|
-
WorkGroup: wg.Name,
|
|
433
|
-
EngineVersion: wg.Configuration?.EngineVersion || { SelectedEngineVersion: 'AUTO', EffectiveEngineVersion: 'Athena engine version 3' },
|
|
434
|
-
ExecutionParameters: params.ExecutionParameters || null,
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
this.queryExecutions.set(executionId, execution);
|
|
438
|
-
this._simulateQueryAsync(executionId, parsed);
|
|
439
|
-
this.save();
|
|
440
|
-
|
|
441
|
-
this.logger.debug(`[Athena] StartQueryExecution: ${executionId} - ${parsed.type}`);
|
|
442
|
-
return { QueryExecutionId: executionId };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
_getStatementType(type) {
|
|
446
|
-
if (['SELECT', 'SHOW', 'DESCRIBE'].includes(type)) return 'DQL';
|
|
447
|
-
if (['INSERT', 'UPDATE', 'DELETE'].includes(type)) return 'DML';
|
|
448
|
-
return 'DDL';
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
stopQueryExecution(params) {
|
|
452
|
-
const { QueryExecutionId } = params;
|
|
453
|
-
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
454
|
-
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
455
|
-
|
|
456
|
-
if (['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(execution.Status.State)) {
|
|
457
|
-
throw Errors.QueryAlreadyStopped(QueryExecutionId);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
execution.Status.State = 'CANCELLED';
|
|
461
|
-
execution.Status.CompletionDateTime = new Date().toISOString();
|
|
462
|
-
execution.Status.StateChangeReason = 'Query was cancelled by the user';
|
|
463
|
-
this.queryExecutions.set(QueryExecutionId, execution);
|
|
464
|
-
this.save();
|
|
465
|
-
|
|
466
|
-
return {};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
getQueryExecution(params) {
|
|
470
|
-
const { QueryExecutionId } = params;
|
|
471
|
-
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
472
|
-
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
473
|
-
return { QueryExecution: execution };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
listQueryExecutions(params) {
|
|
477
|
-
const { WorkGroup, NextToken, MaxResults = 50 } = params;
|
|
478
|
-
let executions = [...this.queryExecutions.values()];
|
|
479
|
-
|
|
480
|
-
if (WorkGroup) {
|
|
481
|
-
executions = executions.filter((e) => e.WorkGroup === WorkGroup);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const ids = executions.map((e) => e.QueryExecutionId);
|
|
485
|
-
const start = NextToken ? ids.indexOf(NextToken) + 1 : 0;
|
|
486
|
-
const page = ids.slice(start, start + MaxResults);
|
|
487
|
-
const next = start + MaxResults < ids.length ? ids[start + MaxResults] : null;
|
|
488
|
-
|
|
489
|
-
return { QueryExecutionIds: page, NextToken: next };
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
batchGetQueryExecution(params) {
|
|
493
|
-
const { QueryExecutionIds } = params;
|
|
494
|
-
if (!Array.isArray(QueryExecutionIds)) throw Errors.InvalidRequest('QueryExecutionIds is required');
|
|
495
|
-
|
|
496
|
-
const found = [];
|
|
497
|
-
const unprocessed = [];
|
|
498
|
-
|
|
499
|
-
for (const id of QueryExecutionIds) {
|
|
500
|
-
const ex = this.queryExecutions.get(id);
|
|
501
|
-
if (ex) found.push(ex);
|
|
502
|
-
else unprocessed.push({ QueryExecutionId: id, ErrorCode: 'InvalidRequestException', ErrorMessage: `Query execution not found: ${id}` });
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return { QueryExecutions: found, UnprocessedQueryExecutionIds: unprocessed };
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
getQueryResults(params) {
|
|
509
|
-
const { QueryExecutionId, NextToken, MaxResults = 1000 } = params;
|
|
510
|
-
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
511
|
-
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
512
|
-
|
|
513
|
-
if (execution.Status.State === 'RUNNING' || execution.Status.State === 'QUEUED') {
|
|
514
|
-
throw Errors.InvalidRequest(`Query execution ${QueryExecutionId} is still running`);
|
|
515
|
-
}
|
|
516
|
-
if (execution.Status.State === 'CANCELLED') {
|
|
517
|
-
throw Errors.InvalidRequest(`Query execution ${QueryExecutionId} was cancelled`);
|
|
518
|
-
}
|
|
519
|
-
if (execution.Status.State === 'FAILED') {
|
|
520
|
-
throw Errors.InvalidRequest(`Query execution ${QueryExecutionId} failed: ${execution.Status.StateChangeReason}`);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const results = this.queryResults.get(QueryExecutionId) || {
|
|
524
|
-
ResultSet: { Rows: [], ResultSetMetadata: { ColumnInfo: [] } },
|
|
525
|
-
NextToken: null,
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
const rows = results.ResultSet.Rows;
|
|
529
|
-
const start = NextToken ? parseInt(NextToken, 10) : 0;
|
|
530
|
-
const page = rows.slice(start, start + MaxResults);
|
|
531
|
-
const next = start + MaxResults < rows.length ? String(start + MaxResults) : null;
|
|
532
|
-
|
|
533
|
-
return {
|
|
534
|
-
ResultSet: { ...results.ResultSet, Rows: page },
|
|
535
|
-
NextToken: next,
|
|
536
|
-
UpdateCount: results.UpdateCount || 0,
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
getQueryRuntimeStatistics(params) {
|
|
541
|
-
const { QueryExecutionId } = params;
|
|
542
|
-
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
543
|
-
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
QueryRuntimeStatistics: {
|
|
547
|
-
Timeline: {
|
|
548
|
-
QueryQueueTimeInMillis: 50,
|
|
549
|
-
QueryPlanningTimeInMillis: 30,
|
|
550
|
-
EngineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis || 0,
|
|
551
|
-
ServiceProcessingTimeInMillis: 20,
|
|
552
|
-
TotalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis || 0,
|
|
553
|
-
},
|
|
554
|
-
Rows: {
|
|
555
|
-
InputRows: 100,
|
|
556
|
-
InputBytes: execution.Statistics?.DataScannedInBytes || 0,
|
|
557
|
-
OutputRows: 3,
|
|
558
|
-
OutputBytes: 1024,
|
|
559
|
-
},
|
|
560
|
-
OutputStage: null,
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ── Named Queries ─────────────────────────────────────────────────────────
|
|
566
|
-
|
|
567
|
-
createNamedQuery(params) {
|
|
568
|
-
const { Name, Description, Database, QueryString, ClientRequestToken, WorkGroup } = params;
|
|
569
|
-
if (!Name) throw Errors.InvalidRequest('Name is required');
|
|
570
|
-
if (!QueryString) throw Errors.InvalidRequest('QueryString is required');
|
|
571
|
-
if (!Database) throw Errors.InvalidRequest('Database is required');
|
|
572
|
-
|
|
573
|
-
const queryId = ClientRequestToken || randomUUID();
|
|
574
|
-
|
|
575
|
-
const query = {
|
|
576
|
-
QueryId: queryId,
|
|
577
|
-
Name,
|
|
578
|
-
Description: Description || '',
|
|
579
|
-
Database,
|
|
580
|
-
QueryString,
|
|
581
|
-
WorkGroup: WorkGroup || 'primary',
|
|
582
|
-
NamedQueryId: queryId,
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
this.namedQueries.set(queryId, query);
|
|
586
|
-
this.save();
|
|
587
|
-
|
|
588
|
-
this.logger.debug(`[Athena] CreateNamedQuery: ${queryId} (${Name})`);
|
|
589
|
-
return { NamedQueryId: queryId };
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
deleteNamedQuery(params) {
|
|
593
|
-
const { NamedQueryId } = params;
|
|
594
|
-
if (!this.namedQueries.has(NamedQueryId)) throw Errors.NamedQueryNotFound(NamedQueryId);
|
|
595
|
-
this.namedQueries.delete(NamedQueryId);
|
|
596
|
-
this.save();
|
|
597
|
-
return {};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
getNamedQuery(params) {
|
|
601
|
-
const { NamedQueryId } = params;
|
|
602
|
-
const query = this.namedQueries.get(NamedQueryId);
|
|
603
|
-
if (!query) throw Errors.NamedQueryNotFound(NamedQueryId);
|
|
604
|
-
return { NamedQuery: query };
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
listNamedQueries(params) {
|
|
608
|
-
const { WorkGroup, NextToken, MaxResults = 50 } = params;
|
|
609
|
-
let queries = [...this.namedQueries.values()];
|
|
610
|
-
|
|
611
|
-
if (WorkGroup) {
|
|
612
|
-
queries = queries.filter((q) => q.WorkGroup === WorkGroup);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const ids = queries.map((q) => q.QueryId);
|
|
616
|
-
const start = NextToken ? ids.indexOf(NextToken) + 1 : 0;
|
|
617
|
-
const page = ids.slice(start, start + MaxResults);
|
|
618
|
-
const next = start + MaxResults < ids.length ? ids[start + MaxResults] : null;
|
|
619
|
-
|
|
620
|
-
return { NamedQueryIds: page, NextToken: next };
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
batchGetNamedQuery(params) {
|
|
624
|
-
const { NamedQueryIds } = params;
|
|
625
|
-
if (!Array.isArray(NamedQueryIds)) throw Errors.InvalidRequest('NamedQueryIds is required');
|
|
626
|
-
|
|
627
|
-
const found = [];
|
|
628
|
-
const unprocessed = [];
|
|
629
|
-
|
|
630
|
-
for (const id of NamedQueryIds) {
|
|
631
|
-
const q = this.namedQueries.get(id);
|
|
632
|
-
if (q) found.push(q);
|
|
633
|
-
else unprocessed.push({ NamedQueryId: id, ErrorCode: 'InvalidRequestException', ErrorMessage: `Named query not found: ${id}` });
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return { NamedQueries: found, UnprocessedNamedQueryIds: unprocessed };
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// ── WorkGroups ────────────────────────────────────────────────────────────
|
|
640
|
-
|
|
641
|
-
createWorkGroup(params) {
|
|
642
|
-
const { Name, Description, Configuration, Tags } = params;
|
|
643
|
-
if (!Name) throw Errors.InvalidRequest('Name is required');
|
|
644
|
-
if (this.workgroups.has(Name)) throw Errors.WorkGroupAlreadyExists(Name);
|
|
645
|
-
|
|
646
|
-
const wg = {
|
|
647
|
-
Name,
|
|
648
|
-
State: 'ENABLED',
|
|
649
|
-
Description: Description || '',
|
|
650
|
-
Configuration: Configuration || {
|
|
651
|
-
ResultConfiguration: { OutputLocation: `s3://aws-athena-query-results-${Name}/` },
|
|
652
|
-
EnforceWorkGroupConfiguration: false,
|
|
653
|
-
PublishCloudWatchMetricsEnabled: false,
|
|
654
|
-
BytesScannedCutoffPerQuery: null,
|
|
655
|
-
RequesterPaysEnabled: false,
|
|
656
|
-
EngineVersion: { SelectedEngineVersion: 'AUTO', EffectiveEngineVersion: 'Athena engine version 3' },
|
|
657
|
-
},
|
|
658
|
-
CreationTime: new Date().toISOString(),
|
|
659
|
-
};
|
|
660
|
-
|
|
661
|
-
this.workgroups.set(Name, wg);
|
|
662
|
-
|
|
663
|
-
const arn = this._wgArn(Name);
|
|
664
|
-
if (Tags && Tags.length > 0) {
|
|
665
|
-
this.tags.set(arn, Tags);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
this.save();
|
|
669
|
-
this.logger.debug(`[Athena] CreateWorkGroup: ${Name}`);
|
|
670
|
-
return {};
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
deleteWorkGroup(params) {
|
|
674
|
-
const { WorkGroup, RecursiveDeleteOption } = params;
|
|
675
|
-
if (!this.workgroups.has(WorkGroup)) throw Errors.WorkGroupNotFound(WorkGroup);
|
|
676
|
-
if (WorkGroup === 'primary') throw Errors.InvalidRequest('Cannot delete the primary workgroup');
|
|
677
|
-
|
|
678
|
-
if (RecursiveDeleteOption) {
|
|
679
|
-
for (const [id, ex] of this.queryExecutions) {
|
|
680
|
-
if (ex.WorkGroup === WorkGroup) {
|
|
681
|
-
this.queryExecutions.delete(id);
|
|
682
|
-
this.queryResults.delete(id);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
for (const [id, q] of this.namedQueries) {
|
|
686
|
-
if (q.WorkGroup === WorkGroup) this.namedQueries.delete(id);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
this.workgroups.delete(WorkGroup);
|
|
691
|
-
this.save();
|
|
692
|
-
return {};
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
updateWorkGroup(params) {
|
|
696
|
-
const { WorkGroup, Description, Configuration, State } = params;
|
|
697
|
-
const wg = this.workgroups.get(WorkGroup);
|
|
698
|
-
if (!wg) throw Errors.WorkGroupNotFound(WorkGroup);
|
|
699
|
-
|
|
700
|
-
if (Description !== undefined) wg.Description = Description;
|
|
701
|
-
if (State !== undefined) wg.State = State;
|
|
702
|
-
if (Configuration) {
|
|
703
|
-
wg.Configuration = { ...wg.Configuration, ...Configuration };
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
this.workgroups.set(WorkGroup, wg);
|
|
707
|
-
this.save();
|
|
708
|
-
return {};
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
getWorkGroup(params) {
|
|
712
|
-
const { WorkGroup } = params;
|
|
713
|
-
const wg = this.workgroups.get(WorkGroup);
|
|
714
|
-
if (!wg) throw Errors.WorkGroupNotFound(WorkGroup);
|
|
715
|
-
return { WorkGroup: wg };
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
listWorkGroups(params) {
|
|
719
|
-
const { NextToken, MaxResults = 50 } = params;
|
|
720
|
-
const all = [...this.workgroups.values()].map((wg) => ({
|
|
721
|
-
Name: wg.Name,
|
|
722
|
-
State: wg.State,
|
|
723
|
-
Description: wg.Description,
|
|
724
|
-
CreationTime: wg.CreationTime,
|
|
725
|
-
EngineVersion: wg.Configuration?.EngineVersion || null,
|
|
726
|
-
}));
|
|
727
|
-
|
|
728
|
-
const start = NextToken ? all.findIndex((w) => w.Name === NextToken) + 1 : 0;
|
|
729
|
-
const page = all.slice(start, start + MaxResults);
|
|
730
|
-
const next = start + MaxResults < all.length ? all[start + MaxResults].Name : null;
|
|
731
|
-
|
|
732
|
-
return { WorkGroups: page, NextToken: next };
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// ── Data Catalogs ─────────────────────────────────────────────────────────
|
|
736
|
-
|
|
737
|
-
createDataCatalog(params) {
|
|
738
|
-
const { Name, Type, Description, Parameters, Tags } = params;
|
|
739
|
-
if (!Name) throw Errors.InvalidRequest('Name is required');
|
|
740
|
-
if (!Type) throw Errors.InvalidRequest('Type is required');
|
|
741
|
-
if (this.dataCatalogs.has(Name)) throw Errors.DataCatalogAlreadyExists(Name);
|
|
742
|
-
|
|
743
|
-
const catalog = {
|
|
744
|
-
Name,
|
|
745
|
-
Description: Description || '',
|
|
746
|
-
Type,
|
|
747
|
-
Parameters: Parameters || {},
|
|
748
|
-
Tags: Tags || [],
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
this.dataCatalogs.set(Name, catalog);
|
|
752
|
-
const arn = this._catalogArn(Name);
|
|
753
|
-
if (Tags && Tags.length > 0) this.tags.set(arn, Tags);
|
|
754
|
-
|
|
755
|
-
this.save();
|
|
756
|
-
this.logger.debug(`[Athena] CreateDataCatalog: ${Name}`);
|
|
757
|
-
return {};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
deleteDataCatalog(params) {
|
|
761
|
-
const { Name } = params;
|
|
762
|
-
if (!this.dataCatalogs.has(Name)) throw Errors.DataCatalogNotFound(Name);
|
|
763
|
-
if (Name === 'AwsDataCatalog') throw Errors.InvalidRequest('Cannot delete the default AwsDataCatalog');
|
|
764
|
-
|
|
765
|
-
this.dataCatalogs.delete(Name);
|
|
766
|
-
this.save();
|
|
767
|
-
return {};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
updateDataCatalog(params) {
|
|
771
|
-
const { Name, Type, Description, Parameters } = params;
|
|
772
|
-
const catalog = this.dataCatalogs.get(Name);
|
|
773
|
-
if (!catalog) throw Errors.DataCatalogNotFound(Name);
|
|
774
|
-
|
|
775
|
-
if (Type !== undefined) catalog.Type = Type;
|
|
776
|
-
if (Description !== undefined) catalog.Description = Description;
|
|
777
|
-
if (Parameters !== undefined) catalog.Parameters = Parameters;
|
|
778
|
-
|
|
779
|
-
this.dataCatalogs.set(Name, catalog);
|
|
780
|
-
this.save();
|
|
781
|
-
return {};
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
getDataCatalog(params) {
|
|
785
|
-
const { Name } = params;
|
|
786
|
-
const catalog = this.dataCatalogs.get(Name);
|
|
787
|
-
if (!catalog) throw Errors.DataCatalogNotFound(Name);
|
|
788
|
-
return { DataCatalog: catalog };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
listDataCatalogs(params) {
|
|
792
|
-
const { NextToken, MaxResults = 50 } = params;
|
|
793
|
-
const all = [...this.dataCatalogs.values()].map(({ Name, Description, Type }) => ({ CatalogName: Name, Description, Type }));
|
|
794
|
-
const start = NextToken ? all.findIndex((c) => c.CatalogName === NextToken) + 1 : 0;
|
|
795
|
-
const page = all.slice(start, start + MaxResults);
|
|
796
|
-
const next = start + MaxResults < all.length ? all[start + MaxResults].CatalogName : null;
|
|
797
|
-
return { DataCatalogsSummary: page, NextToken: next };
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// ── Databases ─────────────────────────────────────────────────────────────
|
|
801
|
-
|
|
802
|
-
listDatabases(params) {
|
|
803
|
-
const { CatalogName, NextToken, MaxResults = 50 } = params;
|
|
804
|
-
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
805
|
-
if (!this.dataCatalogs.has(CatalogName)) throw Errors.DataCatalogNotFound(CatalogName);
|
|
806
|
-
|
|
807
|
-
const all = [...this.databases.entries()]
|
|
808
|
-
.filter(([k]) => k.startsWith(CatalogName + '.'))
|
|
809
|
-
.map(([, v]) => v);
|
|
810
|
-
|
|
811
|
-
const start = NextToken ? all.findIndex((d) => d.Name === NextToken) + 1 : 0;
|
|
812
|
-
const page = all.slice(start, start + MaxResults);
|
|
813
|
-
const next = start + MaxResults < all.length ? all[start + MaxResults].Name : null;
|
|
814
|
-
|
|
815
|
-
return { DatabaseList: page, NextToken: next };
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
getDatabase(params) {
|
|
819
|
-
const { CatalogName, DatabaseName } = params;
|
|
820
|
-
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
821
|
-
if (!DatabaseName) throw Errors.InvalidRequest('DatabaseName is required');
|
|
822
|
-
|
|
823
|
-
const key = `${CatalogName}.${DatabaseName}`;
|
|
824
|
-
const db = this.databases.get(key);
|
|
825
|
-
if (!db) throw Errors.DatabaseNotFound(DatabaseName);
|
|
826
|
-
return { Database: db };
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// ── Table Metadata ────────────────────────────────────────────────────────
|
|
830
|
-
|
|
831
|
-
listTableMetadata(params) {
|
|
832
|
-
const { CatalogName, DatabaseName, Expression, NextToken, MaxResults = 50 } = params;
|
|
833
|
-
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
834
|
-
if (!DatabaseName) throw Errors.InvalidRequest('DatabaseName is required');
|
|
835
|
-
|
|
836
|
-
const prefix = `${CatalogName}.${DatabaseName}.`;
|
|
837
|
-
let tables = [...this.tables.entries()]
|
|
838
|
-
.filter(([k]) => k.startsWith(prefix))
|
|
839
|
-
.map(([, v]) => v);
|
|
840
|
-
|
|
841
|
-
if (Expression) {
|
|
842
|
-
const re = new RegExp(Expression.replace(/\*/g, '.*'), 'i');
|
|
843
|
-
tables = tables.filter((t) => re.test(t.Name));
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const start = NextToken ? tables.findIndex((t) => t.Name === NextToken) + 1 : 0;
|
|
847
|
-
const page = tables.slice(start, start + MaxResults);
|
|
848
|
-
const next = start + MaxResults < tables.length ? tables[start + MaxResults].Name : null;
|
|
849
|
-
|
|
850
|
-
return { TableMetadataList: page, NextToken: next };
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
getTableMetadata(params) {
|
|
854
|
-
const { CatalogName, DatabaseName, TableName } = params;
|
|
855
|
-
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
856
|
-
if (!DatabaseName) throw Errors.InvalidRequest('DatabaseName is required');
|
|
857
|
-
if (!TableName) throw Errors.InvalidRequest('TableName is required');
|
|
858
|
-
|
|
859
|
-
const key = `${CatalogName}.${DatabaseName}.${TableName}`;
|
|
860
|
-
const table = this.tables.get(key);
|
|
861
|
-
if (!table) throw Errors.TableNotFound(TableName);
|
|
862
|
-
return { TableMetadata: table };
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// ── Prepared Statements ───────────────────────────────────────────────────
|
|
866
|
-
|
|
867
|
-
createPreparedStatement(params) {
|
|
868
|
-
const { StatementName, WorkGroup, QueryStatement, Description } = params;
|
|
869
|
-
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
870
|
-
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
871
|
-
if (!QueryStatement) throw Errors.InvalidRequest('QueryStatement is required');
|
|
872
|
-
|
|
873
|
-
this._resolveWorkgroup(WorkGroup);
|
|
874
|
-
|
|
875
|
-
const key = `${WorkGroup}.${StatementName}`;
|
|
876
|
-
const stmt = {
|
|
877
|
-
StatementName,
|
|
878
|
-
WorkGroupName: WorkGroup,
|
|
879
|
-
QueryStatement,
|
|
880
|
-
Description: Description || '',
|
|
881
|
-
LastModifiedTime: new Date().toISOString(),
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
this.preparedStatements.set(key, stmt);
|
|
885
|
-
this.save();
|
|
886
|
-
this.logger.debug(`[Athena] CreatePreparedStatement: ${key}`);
|
|
887
|
-
return {};
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
updatePreparedStatement(params) {
|
|
891
|
-
const { StatementName, WorkGroup, QueryStatement, Description } = params;
|
|
892
|
-
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
893
|
-
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
894
|
-
|
|
895
|
-
const key = `${WorkGroup}.${StatementName}`;
|
|
896
|
-
const stmt = this.preparedStatements.get(key);
|
|
897
|
-
if (!stmt) throw Errors.PreparedStatementNotFound(StatementName);
|
|
898
|
-
|
|
899
|
-
if (QueryStatement) stmt.QueryStatement = QueryStatement;
|
|
900
|
-
if (Description !== undefined) stmt.Description = Description;
|
|
901
|
-
stmt.LastModifiedTime = new Date().toISOString();
|
|
902
|
-
|
|
903
|
-
this.preparedStatements.set(key, stmt);
|
|
904
|
-
this.save();
|
|
905
|
-
return {};
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
deletePreparedStatement(params) {
|
|
909
|
-
const { StatementName, WorkGroup } = params;
|
|
910
|
-
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
911
|
-
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
912
|
-
|
|
913
|
-
const key = `${WorkGroup}.${StatementName}`;
|
|
914
|
-
if (!this.preparedStatements.has(key)) throw Errors.PreparedStatementNotFound(StatementName);
|
|
915
|
-
|
|
916
|
-
this.preparedStatements.delete(key);
|
|
917
|
-
this.save();
|
|
918
|
-
return {};
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
getPreparedStatement(params) {
|
|
922
|
-
const { StatementName, WorkGroup } = params;
|
|
923
|
-
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
924
|
-
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
925
|
-
|
|
926
|
-
const key = `${WorkGroup}.${StatementName}`;
|
|
927
|
-
const stmt = this.preparedStatements.get(key);
|
|
928
|
-
if (!stmt) throw Errors.PreparedStatementNotFound(StatementName);
|
|
929
|
-
return { PreparedStatement: stmt };
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
listPreparedStatements(params) {
|
|
933
|
-
const { WorkGroup, NextToken, MaxResults = 50 } = params;
|
|
934
|
-
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
935
|
-
this._resolveWorkgroup(WorkGroup);
|
|
936
|
-
|
|
937
|
-
const all = [...this.preparedStatements.entries()]
|
|
938
|
-
.filter(([k]) => k.startsWith(WorkGroup + '.'))
|
|
939
|
-
.map(([, v]) => ({ StatementName: v.StatementName, LastModifiedTime: v.LastModifiedTime }));
|
|
940
|
-
|
|
941
|
-
const start = NextToken ? all.findIndex((s) => s.StatementName === NextToken) + 1 : 0;
|
|
942
|
-
const page = all.slice(start, start + MaxResults);
|
|
943
|
-
const next = start + MaxResults < all.length ? all[start + MaxResults].StatementName : null;
|
|
944
|
-
|
|
945
|
-
return { PreparedStatements: page, NextToken: next };
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
// ── Tags ──────────────────────────────────────────────────────────────────
|
|
949
|
-
|
|
950
|
-
tagResource(params) {
|
|
951
|
-
const { ResourceARN, Tags } = params;
|
|
952
|
-
if (!ResourceARN) throw Errors.InvalidRequest('ResourceARN is required');
|
|
953
|
-
if (!Tags || !Array.isArray(Tags)) throw Errors.InvalidRequest('Tags is required');
|
|
954
|
-
|
|
955
|
-
const existing = this.tags.get(ResourceARN) || [];
|
|
956
|
-
const tagMap = {};
|
|
957
|
-
for (const t of existing) tagMap[t.Key] = t.Value;
|
|
958
|
-
for (const t of Tags) tagMap[t.Key] = t.Value;
|
|
959
|
-
|
|
960
|
-
this.tags.set(ResourceARN, Object.entries(tagMap).map(([Key, Value]) => ({ Key, Value })));
|
|
961
|
-
this.save();
|
|
962
|
-
return {};
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
untagResource(params) {
|
|
966
|
-
const { ResourceARN, TagKeys } = params;
|
|
967
|
-
if (!ResourceARN) throw Errors.InvalidRequest('ResourceARN is required');
|
|
968
|
-
if (!TagKeys || !Array.isArray(TagKeys)) throw Errors.InvalidRequest('TagKeys is required');
|
|
969
|
-
|
|
970
|
-
const existing = this.tags.get(ResourceARN) || [];
|
|
971
|
-
this.tags.set(ResourceARN, existing.filter((t) => !TagKeys.includes(t.Key)));
|
|
972
|
-
this.save();
|
|
973
|
-
return {};
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
listTagsForResource(params) {
|
|
977
|
-
const { ResourceARN } = params;
|
|
978
|
-
if (!ResourceARN) throw Errors.InvalidRequest('ResourceARN is required');
|
|
979
|
-
const tags = this.tags.get(ResourceARN) || [];
|
|
980
|
-
return { Tags: tags };
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// ── Admin helpers ─────────────────────────────────────────────────────────
|
|
984
|
-
|
|
985
|
-
getAdminStatus() {
|
|
986
|
-
return {
|
|
987
|
-
queryExecutions: this.queryExecutions.size,
|
|
988
|
-
namedQueries: this.namedQueries.size,
|
|
989
|
-
workgroups: this.workgroups.size,
|
|
990
|
-
dataCatalogs: this.dataCatalogs.size,
|
|
991
|
-
databases: this.databases.size,
|
|
992
|
-
tables: this.tables.size,
|
|
993
|
-
preparedStatements: this.preparedStatements.size,
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
module.exports = { AthenaSimulator };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Athena Simulator
|
|
5
|
+
*
|
|
6
|
+
* Suporta:
|
|
7
|
+
* Query Execution:
|
|
8
|
+
* - StartQueryExecution
|
|
9
|
+
* - StopQueryExecution
|
|
10
|
+
* - GetQueryExecution
|
|
11
|
+
* - ListQueryExecutions
|
|
12
|
+
* - BatchGetQueryExecution
|
|
13
|
+
*
|
|
14
|
+
* Query Results:
|
|
15
|
+
* - GetQueryResults
|
|
16
|
+
*
|
|
17
|
+
* Named Queries:
|
|
18
|
+
* - CreateNamedQuery
|
|
19
|
+
* - DeleteNamedQuery
|
|
20
|
+
* - GetNamedQuery
|
|
21
|
+
* - ListNamedQueries
|
|
22
|
+
* - BatchGetNamedQuery
|
|
23
|
+
*
|
|
24
|
+
* Workgroups:
|
|
25
|
+
* - CreateWorkGroup
|
|
26
|
+
* - DeleteWorkGroup
|
|
27
|
+
* - UpdateWorkGroup
|
|
28
|
+
* - GetWorkGroup
|
|
29
|
+
* - ListWorkGroups
|
|
30
|
+
*
|
|
31
|
+
* Data Catalogs:
|
|
32
|
+
* - CreateDataCatalog
|
|
33
|
+
* - DeleteDataCatalog
|
|
34
|
+
* - UpdateDataCatalog
|
|
35
|
+
* - GetDataCatalog
|
|
36
|
+
* - ListDataCatalogs
|
|
37
|
+
*
|
|
38
|
+
* Databases:
|
|
39
|
+
* - ListDatabases
|
|
40
|
+
* - GetDatabase
|
|
41
|
+
*
|
|
42
|
+
* TableMetadata:
|
|
43
|
+
* - ListTableMetadata
|
|
44
|
+
* - GetTableMetadata
|
|
45
|
+
*
|
|
46
|
+
* Prepared Statements:
|
|
47
|
+
* - CreatePreparedStatement
|
|
48
|
+
* - UpdatePreparedStatement
|
|
49
|
+
* - DeletePreparedStatement
|
|
50
|
+
* - GetPreparedStatement
|
|
51
|
+
* - ListPreparedStatements
|
|
52
|
+
*
|
|
53
|
+
* Tags:
|
|
54
|
+
* - TagResource / UntagResource / ListTagsForResource
|
|
55
|
+
*
|
|
56
|
+
* Integração:
|
|
57
|
+
* - Execução de queries simuladas contra dados do S3
|
|
58
|
+
* - Suporte a DDL (CREATE TABLE, CREATE DATABASE, DROP TABLE, DROP DATABASE)
|
|
59
|
+
* - Suporte a DML (SELECT, INSERT, UPDATE, DELETE)
|
|
60
|
+
* - Parser simples de SQL para retornar resultados simulados
|
|
61
|
+
* - Persistência via LocalStore
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
const { randomUUID } = require('crypto');
|
|
65
|
+
|
|
66
|
+
// ─── Erros tipados ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
class AthenaError extends Error {
|
|
69
|
+
constructor(code, message, statusCode = 400) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.statusCode = statusCode;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const Errors = {
|
|
77
|
+
InvalidRequest: (msg) =>
|
|
78
|
+
new AthenaError('InvalidRequestException', msg, 400),
|
|
79
|
+
QueryNotFound: (id) =>
|
|
80
|
+
new AthenaError('InvalidRequestException', `Query execution not found: ${id}`, 400),
|
|
81
|
+
NamedQueryNotFound: (id) =>
|
|
82
|
+
new AthenaError('InvalidRequestException', `Named query not found: ${id}`, 400),
|
|
83
|
+
WorkGroupNotFound: (name) =>
|
|
84
|
+
new AthenaError('InvalidRequestException', `WorkGroup not found: ${name}`, 400),
|
|
85
|
+
WorkGroupAlreadyExists: (name) =>
|
|
86
|
+
new AthenaError('InvalidRequestException', `WorkGroup already exists: ${name}`, 400),
|
|
87
|
+
DataCatalogNotFound: (name) =>
|
|
88
|
+
new AthenaError('InvalidRequestException', `DataCatalog not found: ${name}`, 404),
|
|
89
|
+
DataCatalogAlreadyExists: (name) =>
|
|
90
|
+
new AthenaError('InvalidRequestException', `DataCatalog already exists: ${name}`, 400),
|
|
91
|
+
DatabaseNotFound: (name) =>
|
|
92
|
+
new AthenaError('MetadataException', `Database not found: ${name}`, 404),
|
|
93
|
+
TableNotFound: (name) =>
|
|
94
|
+
new AthenaError('MetadataException', `Table not found: ${name}`, 404),
|
|
95
|
+
PreparedStatementNotFound: (name) =>
|
|
96
|
+
new AthenaError('ResourceNotFoundException', `Prepared statement not found: ${name}`, 404),
|
|
97
|
+
TooManyRequests: () =>
|
|
98
|
+
new AthenaError('TooManyRequestsException', 'Too many requests', 429),
|
|
99
|
+
QueryAlreadyStopped: (id) =>
|
|
100
|
+
new AthenaError('InvalidRequestException', `Query execution already stopped: ${id}`, 400),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ─── Parser SQL simples ───────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function parseSql(sql) {
|
|
106
|
+
const normalized = sql.trim().replace(/\s+/g, ' ').toUpperCase();
|
|
107
|
+
|
|
108
|
+
if (normalized.startsWith('SELECT')) return { type: 'SELECT', sql };
|
|
109
|
+
if (normalized.startsWith('INSERT')) return { type: 'INSERT', sql };
|
|
110
|
+
if (normalized.startsWith('UPDATE')) return { type: 'UPDATE', sql };
|
|
111
|
+
if (normalized.startsWith('DELETE')) return { type: 'DELETE', sql };
|
|
112
|
+
if (normalized.startsWith('CREATE TABLE')) return { type: 'CREATE_TABLE', sql };
|
|
113
|
+
if (normalized.startsWith('CREATE DATABASE') || normalized.startsWith('CREATE SCHEMA')) return { type: 'CREATE_DATABASE', sql };
|
|
114
|
+
if (normalized.startsWith('DROP TABLE')) return { type: 'DROP_TABLE', sql };
|
|
115
|
+
if (normalized.startsWith('DROP DATABASE') || normalized.startsWith('DROP SCHEMA')) return { type: 'DROP_DATABASE', sql };
|
|
116
|
+
if (normalized.startsWith('SHOW')) return { type: 'SHOW', sql };
|
|
117
|
+
if (normalized.startsWith('DESCRIBE') || normalized.startsWith('DESC')) return { type: 'DESCRIBE', sql };
|
|
118
|
+
if (normalized.startsWith('ALTER')) return { type: 'ALTER', sql };
|
|
119
|
+
if (normalized.startsWith('MSCK REPAIR TABLE')) return { type: 'MSCK_REPAIR', sql };
|
|
120
|
+
|
|
121
|
+
return { type: 'UNKNOWN', sql };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function generateSimulatedResults(parsed) {
|
|
125
|
+
switch (parsed.type) {
|
|
126
|
+
case 'SELECT': {
|
|
127
|
+
const columns = ['id', 'name', 'value', 'timestamp'];
|
|
128
|
+
const rows = Array.from({ length: 3 }, (_, i) => ({
|
|
129
|
+
Data: [
|
|
130
|
+
{ VarCharValue: String(i + 1) },
|
|
131
|
+
{ VarCharValue: `item_${i + 1}` },
|
|
132
|
+
{ VarCharValue: String(Math.floor(Math.random() * 1000)) },
|
|
133
|
+
{ VarCharValue: new Date().toISOString() },
|
|
134
|
+
],
|
|
135
|
+
}));
|
|
136
|
+
return {
|
|
137
|
+
ResultSet: {
|
|
138
|
+
Rows: [
|
|
139
|
+
{ Data: columns.map((c) => ({ VarCharValue: c })) },
|
|
140
|
+
...rows,
|
|
141
|
+
],
|
|
142
|
+
ResultSetMetadata: {
|
|
143
|
+
ColumnInfo: columns.map((c, i) => ({
|
|
144
|
+
CatalogName: 'hive',
|
|
145
|
+
SchemaName: '',
|
|
146
|
+
TableName: '',
|
|
147
|
+
Name: c,
|
|
148
|
+
Label: c,
|
|
149
|
+
Type: i === 3 ? 'varchar' : i === 2 ? 'bigint' : 'varchar',
|
|
150
|
+
Precision: 2147483647,
|
|
151
|
+
Scale: 0,
|
|
152
|
+
Nullable: 'UNKNOWN',
|
|
153
|
+
CaseSensitive: i !== 2,
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
NextToken: null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
case 'CREATE_TABLE':
|
|
161
|
+
case 'CREATE_DATABASE':
|
|
162
|
+
case 'DROP_TABLE':
|
|
163
|
+
case 'DROP_DATABASE':
|
|
164
|
+
case 'INSERT':
|
|
165
|
+
case 'UPDATE':
|
|
166
|
+
case 'DELETE':
|
|
167
|
+
case 'ALTER':
|
|
168
|
+
case 'MSCK_REPAIR':
|
|
169
|
+
return {
|
|
170
|
+
ResultSet: { Rows: [], ResultSetMetadata: { ColumnInfo: [] } },
|
|
171
|
+
NextToken: null,
|
|
172
|
+
UpdateCount: 1,
|
|
173
|
+
};
|
|
174
|
+
case 'SHOW': {
|
|
175
|
+
return {
|
|
176
|
+
ResultSet: {
|
|
177
|
+
Rows: [{ Data: [{ VarCharValue: 'tab_name' }] }],
|
|
178
|
+
ResultSetMetadata: { ColumnInfo: [{ Name: 'tab_name', Type: 'varchar' }] },
|
|
179
|
+
},
|
|
180
|
+
NextToken: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return {
|
|
185
|
+
ResultSet: { Rows: [], ResultSetMetadata: { ColumnInfo: [] } },
|
|
186
|
+
NextToken: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Simulador ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
class AthenaSimulator {
|
|
194
|
+
constructor(config, store, logger) {
|
|
195
|
+
this.config = config;
|
|
196
|
+
this.store = store;
|
|
197
|
+
this.logger = logger;
|
|
198
|
+
this.region = config?.region || 'us-east-1';
|
|
199
|
+
this.accountId = config?.accountId || '000000000000';
|
|
200
|
+
|
|
201
|
+
// State
|
|
202
|
+
this.queryExecutions = new Map(); // executionId → execution
|
|
203
|
+
this.queryResults = new Map(); // executionId → results
|
|
204
|
+
this.namedQueries = new Map(); // queryId → namedQuery
|
|
205
|
+
this.workgroups = new Map(); // name → workgroup
|
|
206
|
+
this.dataCatalogs = new Map(); // name → catalog
|
|
207
|
+
this.databases = new Map(); // catalogName.dbName → database
|
|
208
|
+
this.tables = new Map(); // catalogName.dbName.tableName → table
|
|
209
|
+
this.preparedStatements = new Map();// workgroup.name → statement
|
|
210
|
+
this.tags = new Map(); // arn → tags
|
|
211
|
+
|
|
212
|
+
// Injeções cross-service
|
|
213
|
+
this.s3Simulator = null;
|
|
214
|
+
|
|
215
|
+
// Workgroup padrão
|
|
216
|
+
this._initDefaults();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
_initDefaults() {
|
|
220
|
+
this.workgroups.set('primary', {
|
|
221
|
+
Name: 'primary',
|
|
222
|
+
State: 'ENABLED',
|
|
223
|
+
Description: 'Primary workgroup',
|
|
224
|
+
Configuration: {
|
|
225
|
+
ResultConfiguration: {
|
|
226
|
+
OutputLocation: 's3://aws-athena-query-results-local/',
|
|
227
|
+
EncryptionConfiguration: null,
|
|
228
|
+
},
|
|
229
|
+
EnforceWorkGroupConfiguration: false,
|
|
230
|
+
PublishCloudWatchMetricsEnabled: false,
|
|
231
|
+
BytesScannedCutoffPerQuery: null,
|
|
232
|
+
RequesterPaysEnabled: false,
|
|
233
|
+
EngineVersion: { SelectedEngineVersion: 'Athena engine version 3', EffectiveEngineVersion: 'Athena engine version 3' },
|
|
234
|
+
},
|
|
235
|
+
CreationTime: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
this.dataCatalogs.set('AwsDataCatalog', {
|
|
239
|
+
Name: 'AwsDataCatalog',
|
|
240
|
+
Description: 'AWS Glue based Data Catalog',
|
|
241
|
+
Type: 'GLUE',
|
|
242
|
+
Parameters: {},
|
|
243
|
+
Tags: [],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Banco de dados padrão
|
|
247
|
+
this.databases.set('AwsDataCatalog.default', {
|
|
248
|
+
Name: 'default',
|
|
249
|
+
Description: 'Default Hive database',
|
|
250
|
+
Parameters: {},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Persistência ──────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
async load() {
|
|
257
|
+
try {
|
|
258
|
+
const data = await this.store.load('athena');
|
|
259
|
+
if (!data) return;
|
|
260
|
+
|
|
261
|
+
if (data.queryExecutions) {
|
|
262
|
+
for (const [k, v] of Object.entries(data.queryExecutions)) {
|
|
263
|
+
this.queryExecutions.set(k, v);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (data.queryResults) {
|
|
267
|
+
for (const [k, v] of Object.entries(data.queryResults)) {
|
|
268
|
+
this.queryResults.set(k, v);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (data.namedQueries) {
|
|
272
|
+
for (const [k, v] of Object.entries(data.namedQueries)) {
|
|
273
|
+
this.namedQueries.set(k, v);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (data.workgroups) {
|
|
277
|
+
for (const [k, v] of Object.entries(data.workgroups)) {
|
|
278
|
+
this.workgroups.set(k, v);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (data.dataCatalogs) {
|
|
282
|
+
for (const [k, v] of Object.entries(data.dataCatalogs)) {
|
|
283
|
+
this.dataCatalogs.set(k, v);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (data.databases) {
|
|
287
|
+
for (const [k, v] of Object.entries(data.databases)) {
|
|
288
|
+
this.databases.set(k, v);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (data.tables) {
|
|
292
|
+
for (const [k, v] of Object.entries(data.tables)) {
|
|
293
|
+
this.tables.set(k, v);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (data.preparedStatements) {
|
|
297
|
+
for (const [k, v] of Object.entries(data.preparedStatements)) {
|
|
298
|
+
this.preparedStatements.set(k, v);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (data.tags) {
|
|
302
|
+
for (const [k, v] of Object.entries(data.tags)) {
|
|
303
|
+
this.tags.set(k, v);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.logger.debug('[Athena] State loaded from store');
|
|
308
|
+
} catch (err) {
|
|
309
|
+
this.logger.warn('[Athena] Could not load state:', err.message);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async save() {
|
|
314
|
+
try {
|
|
315
|
+
await this.store.save('athena', {
|
|
316
|
+
queryExecutions: Object.fromEntries(this.queryExecutions),
|
|
317
|
+
queryResults: Object.fromEntries(this.queryResults),
|
|
318
|
+
namedQueries: Object.fromEntries(this.namedQueries),
|
|
319
|
+
workgroups: Object.fromEntries(this.workgroups),
|
|
320
|
+
dataCatalogs: Object.fromEntries(this.dataCatalogs),
|
|
321
|
+
databases: Object.fromEntries(this.databases),
|
|
322
|
+
tables: Object.fromEntries(this.tables),
|
|
323
|
+
preparedStatements: Object.fromEntries(this.preparedStatements),
|
|
324
|
+
tags: Object.fromEntries(this.tags),
|
|
325
|
+
});
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.logger.warn('[Athena] Could not save state:', err.message);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
reset() {
|
|
332
|
+
this.queryExecutions.clear();
|
|
333
|
+
this.queryResults.clear();
|
|
334
|
+
this.namedQueries.clear();
|
|
335
|
+
this.workgroups.clear();
|
|
336
|
+
this.dataCatalogs.clear();
|
|
337
|
+
this.databases.clear();
|
|
338
|
+
this.tables.clear();
|
|
339
|
+
this.preparedStatements.clear();
|
|
340
|
+
this.tags.clear();
|
|
341
|
+
this._initDefaults();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
_arn(type, name) {
|
|
347
|
+
return `arn:aws:athena:${this.region}:${this.accountId}:${type}/${name}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_wgArn(name) { return this._arn('workgroup', name); }
|
|
351
|
+
_catalogArn(name) { return this._arn('datacatalog', name); }
|
|
352
|
+
|
|
353
|
+
_resolveWorkgroup(name) {
|
|
354
|
+
const wgName = name || 'primary';
|
|
355
|
+
const wg = this.workgroups.get(wgName);
|
|
356
|
+
if (!wg) throw Errors.WorkGroupNotFound(wgName);
|
|
357
|
+
return wg;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_resolveOutputLocation(params, wg) {
|
|
361
|
+
return (
|
|
362
|
+
params.ResultConfiguration?.OutputLocation ||
|
|
363
|
+
wg.Configuration?.ResultConfiguration?.OutputLocation ||
|
|
364
|
+
`s3://aws-athena-query-results-${this.accountId}-${this.region}/`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_simulateQueryAsync(executionId, parsed) {
|
|
369
|
+
const delay = 200 + Math.floor(Math.random() * 300);
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
const execution = this.queryExecutions.get(executionId);
|
|
372
|
+
if (!execution || execution.Status.State === 'CANCELLED') return;
|
|
373
|
+
|
|
374
|
+
const results = generateSimulatedResults(parsed);
|
|
375
|
+
const dataScanned = Math.floor(Math.random() * 10000000);
|
|
376
|
+
|
|
377
|
+
execution.Status.State = 'SUCCEEDED';
|
|
378
|
+
execution.Status.CompletionDateTime = new Date().toISOString();
|
|
379
|
+
execution.Statistics = {
|
|
380
|
+
EngineExecutionTimeInMillis: delay,
|
|
381
|
+
DataScannedInBytes: dataScanned,
|
|
382
|
+
DataManifestLocation: execution.ResultConfiguration.OutputLocation + executionId + '-manifest.csv',
|
|
383
|
+
TotalExecutionTimeInMillis: delay + 50,
|
|
384
|
+
QueryQueueTimeInMillis: 50,
|
|
385
|
+
QueryPlanningTimeInMillis: 30,
|
|
386
|
+
ServiceProcessingTimeInMillis: 20,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
this.queryResults.set(executionId, results);
|
|
390
|
+
this.queryExecutions.set(executionId, execution);
|
|
391
|
+
this.save();
|
|
392
|
+
}, delay);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Query Execution ────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
startQueryExecution(params) {
|
|
398
|
+
const { QueryString, ClientRequestToken, QueryExecutionContext, ResultConfiguration, WorkGroup } = params;
|
|
399
|
+
|
|
400
|
+
if (!QueryString) throw Errors.InvalidRequest('QueryString is required');
|
|
401
|
+
|
|
402
|
+
const wg = this._resolveWorkgroup(WorkGroup);
|
|
403
|
+
const executionId = ClientRequestToken || randomUUID();
|
|
404
|
+
|
|
405
|
+
if (this.queryExecutions.has(executionId)) {
|
|
406
|
+
return { QueryExecutionId: executionId };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const parsed = parseSql(QueryString);
|
|
410
|
+
const outputLocation = this._resolveOutputLocation(params, wg);
|
|
411
|
+
|
|
412
|
+
const execution = {
|
|
413
|
+
QueryExecutionId: executionId,
|
|
414
|
+
Query: QueryString,
|
|
415
|
+
StatementType: this._getStatementType(parsed.type),
|
|
416
|
+
ResultConfiguration: {
|
|
417
|
+
OutputLocation: outputLocation + executionId + '.csv',
|
|
418
|
+
EncryptionConfiguration: params.ResultConfiguration?.EncryptionConfiguration || null,
|
|
419
|
+
},
|
|
420
|
+
QueryExecutionContext: {
|
|
421
|
+
Database: QueryExecutionContext?.Database || 'default',
|
|
422
|
+
Catalog: QueryExecutionContext?.Catalog || 'AwsDataCatalog',
|
|
423
|
+
},
|
|
424
|
+
Status: {
|
|
425
|
+
State: 'RUNNING',
|
|
426
|
+
SubmissionDateTime: new Date().toISOString(),
|
|
427
|
+
CompletionDateTime: null,
|
|
428
|
+
StateChangeReason: null,
|
|
429
|
+
AthenaError: null,
|
|
430
|
+
},
|
|
431
|
+
Statistics: null,
|
|
432
|
+
WorkGroup: wg.Name,
|
|
433
|
+
EngineVersion: wg.Configuration?.EngineVersion || { SelectedEngineVersion: 'AUTO', EffectiveEngineVersion: 'Athena engine version 3' },
|
|
434
|
+
ExecutionParameters: params.ExecutionParameters || null,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
this.queryExecutions.set(executionId, execution);
|
|
438
|
+
this._simulateQueryAsync(executionId, parsed);
|
|
439
|
+
this.save();
|
|
440
|
+
|
|
441
|
+
this.logger.debug(`[Athena] StartQueryExecution: ${executionId} - ${parsed.type}`);
|
|
442
|
+
return { QueryExecutionId: executionId };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_getStatementType(type) {
|
|
446
|
+
if (['SELECT', 'SHOW', 'DESCRIBE'].includes(type)) return 'DQL';
|
|
447
|
+
if (['INSERT', 'UPDATE', 'DELETE'].includes(type)) return 'DML';
|
|
448
|
+
return 'DDL';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
stopQueryExecution(params) {
|
|
452
|
+
const { QueryExecutionId } = params;
|
|
453
|
+
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
454
|
+
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
455
|
+
|
|
456
|
+
if (['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(execution.Status.State)) {
|
|
457
|
+
throw Errors.QueryAlreadyStopped(QueryExecutionId);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
execution.Status.State = 'CANCELLED';
|
|
461
|
+
execution.Status.CompletionDateTime = new Date().toISOString();
|
|
462
|
+
execution.Status.StateChangeReason = 'Query was cancelled by the user';
|
|
463
|
+
this.queryExecutions.set(QueryExecutionId, execution);
|
|
464
|
+
this.save();
|
|
465
|
+
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
getQueryExecution(params) {
|
|
470
|
+
const { QueryExecutionId } = params;
|
|
471
|
+
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
472
|
+
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
473
|
+
return { QueryExecution: execution };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
listQueryExecutions(params) {
|
|
477
|
+
const { WorkGroup, NextToken, MaxResults = 50 } = params;
|
|
478
|
+
let executions = [...this.queryExecutions.values()];
|
|
479
|
+
|
|
480
|
+
if (WorkGroup) {
|
|
481
|
+
executions = executions.filter((e) => e.WorkGroup === WorkGroup);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const ids = executions.map((e) => e.QueryExecutionId);
|
|
485
|
+
const start = NextToken ? ids.indexOf(NextToken) + 1 : 0;
|
|
486
|
+
const page = ids.slice(start, start + MaxResults);
|
|
487
|
+
const next = start + MaxResults < ids.length ? ids[start + MaxResults] : null;
|
|
488
|
+
|
|
489
|
+
return { QueryExecutionIds: page, NextToken: next };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
batchGetQueryExecution(params) {
|
|
493
|
+
const { QueryExecutionIds } = params;
|
|
494
|
+
if (!Array.isArray(QueryExecutionIds)) throw Errors.InvalidRequest('QueryExecutionIds is required');
|
|
495
|
+
|
|
496
|
+
const found = [];
|
|
497
|
+
const unprocessed = [];
|
|
498
|
+
|
|
499
|
+
for (const id of QueryExecutionIds) {
|
|
500
|
+
const ex = this.queryExecutions.get(id);
|
|
501
|
+
if (ex) found.push(ex);
|
|
502
|
+
else unprocessed.push({ QueryExecutionId: id, ErrorCode: 'InvalidRequestException', ErrorMessage: `Query execution not found: ${id}` });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { QueryExecutions: found, UnprocessedQueryExecutionIds: unprocessed };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
getQueryResults(params) {
|
|
509
|
+
const { QueryExecutionId, NextToken, MaxResults = 1000 } = params;
|
|
510
|
+
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
511
|
+
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
512
|
+
|
|
513
|
+
if (execution.Status.State === 'RUNNING' || execution.Status.State === 'QUEUED') {
|
|
514
|
+
throw Errors.InvalidRequest(`Query execution ${QueryExecutionId} is still running`);
|
|
515
|
+
}
|
|
516
|
+
if (execution.Status.State === 'CANCELLED') {
|
|
517
|
+
throw Errors.InvalidRequest(`Query execution ${QueryExecutionId} was cancelled`);
|
|
518
|
+
}
|
|
519
|
+
if (execution.Status.State === 'FAILED') {
|
|
520
|
+
throw Errors.InvalidRequest(`Query execution ${QueryExecutionId} failed: ${execution.Status.StateChangeReason}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const results = this.queryResults.get(QueryExecutionId) || {
|
|
524
|
+
ResultSet: { Rows: [], ResultSetMetadata: { ColumnInfo: [] } },
|
|
525
|
+
NextToken: null,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const rows = results.ResultSet.Rows;
|
|
529
|
+
const start = NextToken ? parseInt(NextToken, 10) : 0;
|
|
530
|
+
const page = rows.slice(start, start + MaxResults);
|
|
531
|
+
const next = start + MaxResults < rows.length ? String(start + MaxResults) : null;
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
ResultSet: { ...results.ResultSet, Rows: page },
|
|
535
|
+
NextToken: next,
|
|
536
|
+
UpdateCount: results.UpdateCount || 0,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
getQueryRuntimeStatistics(params) {
|
|
541
|
+
const { QueryExecutionId } = params;
|
|
542
|
+
const execution = this.queryExecutions.get(QueryExecutionId);
|
|
543
|
+
if (!execution) throw Errors.QueryNotFound(QueryExecutionId);
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
QueryRuntimeStatistics: {
|
|
547
|
+
Timeline: {
|
|
548
|
+
QueryQueueTimeInMillis: 50,
|
|
549
|
+
QueryPlanningTimeInMillis: 30,
|
|
550
|
+
EngineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis || 0,
|
|
551
|
+
ServiceProcessingTimeInMillis: 20,
|
|
552
|
+
TotalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis || 0,
|
|
553
|
+
},
|
|
554
|
+
Rows: {
|
|
555
|
+
InputRows: 100,
|
|
556
|
+
InputBytes: execution.Statistics?.DataScannedInBytes || 0,
|
|
557
|
+
OutputRows: 3,
|
|
558
|
+
OutputBytes: 1024,
|
|
559
|
+
},
|
|
560
|
+
OutputStage: null,
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Named Queries ─────────────────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
createNamedQuery(params) {
|
|
568
|
+
const { Name, Description, Database, QueryString, ClientRequestToken, WorkGroup } = params;
|
|
569
|
+
if (!Name) throw Errors.InvalidRequest('Name is required');
|
|
570
|
+
if (!QueryString) throw Errors.InvalidRequest('QueryString is required');
|
|
571
|
+
if (!Database) throw Errors.InvalidRequest('Database is required');
|
|
572
|
+
|
|
573
|
+
const queryId = ClientRequestToken || randomUUID();
|
|
574
|
+
|
|
575
|
+
const query = {
|
|
576
|
+
QueryId: queryId,
|
|
577
|
+
Name,
|
|
578
|
+
Description: Description || '',
|
|
579
|
+
Database,
|
|
580
|
+
QueryString,
|
|
581
|
+
WorkGroup: WorkGroup || 'primary',
|
|
582
|
+
NamedQueryId: queryId,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
this.namedQueries.set(queryId, query);
|
|
586
|
+
this.save();
|
|
587
|
+
|
|
588
|
+
this.logger.debug(`[Athena] CreateNamedQuery: ${queryId} (${Name})`);
|
|
589
|
+
return { NamedQueryId: queryId };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
deleteNamedQuery(params) {
|
|
593
|
+
const { NamedQueryId } = params;
|
|
594
|
+
if (!this.namedQueries.has(NamedQueryId)) throw Errors.NamedQueryNotFound(NamedQueryId);
|
|
595
|
+
this.namedQueries.delete(NamedQueryId);
|
|
596
|
+
this.save();
|
|
597
|
+
return {};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
getNamedQuery(params) {
|
|
601
|
+
const { NamedQueryId } = params;
|
|
602
|
+
const query = this.namedQueries.get(NamedQueryId);
|
|
603
|
+
if (!query) throw Errors.NamedQueryNotFound(NamedQueryId);
|
|
604
|
+
return { NamedQuery: query };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
listNamedQueries(params) {
|
|
608
|
+
const { WorkGroup, NextToken, MaxResults = 50 } = params;
|
|
609
|
+
let queries = [...this.namedQueries.values()];
|
|
610
|
+
|
|
611
|
+
if (WorkGroup) {
|
|
612
|
+
queries = queries.filter((q) => q.WorkGroup === WorkGroup);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const ids = queries.map((q) => q.QueryId);
|
|
616
|
+
const start = NextToken ? ids.indexOf(NextToken) + 1 : 0;
|
|
617
|
+
const page = ids.slice(start, start + MaxResults);
|
|
618
|
+
const next = start + MaxResults < ids.length ? ids[start + MaxResults] : null;
|
|
619
|
+
|
|
620
|
+
return { NamedQueryIds: page, NextToken: next };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
batchGetNamedQuery(params) {
|
|
624
|
+
const { NamedQueryIds } = params;
|
|
625
|
+
if (!Array.isArray(NamedQueryIds)) throw Errors.InvalidRequest('NamedQueryIds is required');
|
|
626
|
+
|
|
627
|
+
const found = [];
|
|
628
|
+
const unprocessed = [];
|
|
629
|
+
|
|
630
|
+
for (const id of NamedQueryIds) {
|
|
631
|
+
const q = this.namedQueries.get(id);
|
|
632
|
+
if (q) found.push(q);
|
|
633
|
+
else unprocessed.push({ NamedQueryId: id, ErrorCode: 'InvalidRequestException', ErrorMessage: `Named query not found: ${id}` });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return { NamedQueries: found, UnprocessedNamedQueryIds: unprocessed };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ── WorkGroups ────────────────────────────────────────────────────────────
|
|
640
|
+
|
|
641
|
+
createWorkGroup(params) {
|
|
642
|
+
const { Name, Description, Configuration, Tags } = params;
|
|
643
|
+
if (!Name) throw Errors.InvalidRequest('Name is required');
|
|
644
|
+
if (this.workgroups.has(Name)) throw Errors.WorkGroupAlreadyExists(Name);
|
|
645
|
+
|
|
646
|
+
const wg = {
|
|
647
|
+
Name,
|
|
648
|
+
State: 'ENABLED',
|
|
649
|
+
Description: Description || '',
|
|
650
|
+
Configuration: Configuration || {
|
|
651
|
+
ResultConfiguration: { OutputLocation: `s3://aws-athena-query-results-${Name}/` },
|
|
652
|
+
EnforceWorkGroupConfiguration: false,
|
|
653
|
+
PublishCloudWatchMetricsEnabled: false,
|
|
654
|
+
BytesScannedCutoffPerQuery: null,
|
|
655
|
+
RequesterPaysEnabled: false,
|
|
656
|
+
EngineVersion: { SelectedEngineVersion: 'AUTO', EffectiveEngineVersion: 'Athena engine version 3' },
|
|
657
|
+
},
|
|
658
|
+
CreationTime: new Date().toISOString(),
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
this.workgroups.set(Name, wg);
|
|
662
|
+
|
|
663
|
+
const arn = this._wgArn(Name);
|
|
664
|
+
if (Tags && Tags.length > 0) {
|
|
665
|
+
this.tags.set(arn, Tags);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
this.save();
|
|
669
|
+
this.logger.debug(`[Athena] CreateWorkGroup: ${Name}`);
|
|
670
|
+
return {};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
deleteWorkGroup(params) {
|
|
674
|
+
const { WorkGroup, RecursiveDeleteOption } = params;
|
|
675
|
+
if (!this.workgroups.has(WorkGroup)) throw Errors.WorkGroupNotFound(WorkGroup);
|
|
676
|
+
if (WorkGroup === 'primary') throw Errors.InvalidRequest('Cannot delete the primary workgroup');
|
|
677
|
+
|
|
678
|
+
if (RecursiveDeleteOption) {
|
|
679
|
+
for (const [id, ex] of this.queryExecutions) {
|
|
680
|
+
if (ex.WorkGroup === WorkGroup) {
|
|
681
|
+
this.queryExecutions.delete(id);
|
|
682
|
+
this.queryResults.delete(id);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const [id, q] of this.namedQueries) {
|
|
686
|
+
if (q.WorkGroup === WorkGroup) this.namedQueries.delete(id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
this.workgroups.delete(WorkGroup);
|
|
691
|
+
this.save();
|
|
692
|
+
return {};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
updateWorkGroup(params) {
|
|
696
|
+
const { WorkGroup, Description, Configuration, State } = params;
|
|
697
|
+
const wg = this.workgroups.get(WorkGroup);
|
|
698
|
+
if (!wg) throw Errors.WorkGroupNotFound(WorkGroup);
|
|
699
|
+
|
|
700
|
+
if (Description !== undefined) wg.Description = Description;
|
|
701
|
+
if (State !== undefined) wg.State = State;
|
|
702
|
+
if (Configuration) {
|
|
703
|
+
wg.Configuration = { ...wg.Configuration, ...Configuration };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
this.workgroups.set(WorkGroup, wg);
|
|
707
|
+
this.save();
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
getWorkGroup(params) {
|
|
712
|
+
const { WorkGroup } = params;
|
|
713
|
+
const wg = this.workgroups.get(WorkGroup);
|
|
714
|
+
if (!wg) throw Errors.WorkGroupNotFound(WorkGroup);
|
|
715
|
+
return { WorkGroup: wg };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
listWorkGroups(params) {
|
|
719
|
+
const { NextToken, MaxResults = 50 } = params;
|
|
720
|
+
const all = [...this.workgroups.values()].map((wg) => ({
|
|
721
|
+
Name: wg.Name,
|
|
722
|
+
State: wg.State,
|
|
723
|
+
Description: wg.Description,
|
|
724
|
+
CreationTime: wg.CreationTime,
|
|
725
|
+
EngineVersion: wg.Configuration?.EngineVersion || null,
|
|
726
|
+
}));
|
|
727
|
+
|
|
728
|
+
const start = NextToken ? all.findIndex((w) => w.Name === NextToken) + 1 : 0;
|
|
729
|
+
const page = all.slice(start, start + MaxResults);
|
|
730
|
+
const next = start + MaxResults < all.length ? all[start + MaxResults].Name : null;
|
|
731
|
+
|
|
732
|
+
return { WorkGroups: page, NextToken: next };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── Data Catalogs ─────────────────────────────────────────────────────────
|
|
736
|
+
|
|
737
|
+
createDataCatalog(params) {
|
|
738
|
+
const { Name, Type, Description, Parameters, Tags } = params;
|
|
739
|
+
if (!Name) throw Errors.InvalidRequest('Name is required');
|
|
740
|
+
if (!Type) throw Errors.InvalidRequest('Type is required');
|
|
741
|
+
if (this.dataCatalogs.has(Name)) throw Errors.DataCatalogAlreadyExists(Name);
|
|
742
|
+
|
|
743
|
+
const catalog = {
|
|
744
|
+
Name,
|
|
745
|
+
Description: Description || '',
|
|
746
|
+
Type,
|
|
747
|
+
Parameters: Parameters || {},
|
|
748
|
+
Tags: Tags || [],
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
this.dataCatalogs.set(Name, catalog);
|
|
752
|
+
const arn = this._catalogArn(Name);
|
|
753
|
+
if (Tags && Tags.length > 0) this.tags.set(arn, Tags);
|
|
754
|
+
|
|
755
|
+
this.save();
|
|
756
|
+
this.logger.debug(`[Athena] CreateDataCatalog: ${Name}`);
|
|
757
|
+
return {};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
deleteDataCatalog(params) {
|
|
761
|
+
const { Name } = params;
|
|
762
|
+
if (!this.dataCatalogs.has(Name)) throw Errors.DataCatalogNotFound(Name);
|
|
763
|
+
if (Name === 'AwsDataCatalog') throw Errors.InvalidRequest('Cannot delete the default AwsDataCatalog');
|
|
764
|
+
|
|
765
|
+
this.dataCatalogs.delete(Name);
|
|
766
|
+
this.save();
|
|
767
|
+
return {};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
updateDataCatalog(params) {
|
|
771
|
+
const { Name, Type, Description, Parameters } = params;
|
|
772
|
+
const catalog = this.dataCatalogs.get(Name);
|
|
773
|
+
if (!catalog) throw Errors.DataCatalogNotFound(Name);
|
|
774
|
+
|
|
775
|
+
if (Type !== undefined) catalog.Type = Type;
|
|
776
|
+
if (Description !== undefined) catalog.Description = Description;
|
|
777
|
+
if (Parameters !== undefined) catalog.Parameters = Parameters;
|
|
778
|
+
|
|
779
|
+
this.dataCatalogs.set(Name, catalog);
|
|
780
|
+
this.save();
|
|
781
|
+
return {};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
getDataCatalog(params) {
|
|
785
|
+
const { Name } = params;
|
|
786
|
+
const catalog = this.dataCatalogs.get(Name);
|
|
787
|
+
if (!catalog) throw Errors.DataCatalogNotFound(Name);
|
|
788
|
+
return { DataCatalog: catalog };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
listDataCatalogs(params) {
|
|
792
|
+
const { NextToken, MaxResults = 50 } = params;
|
|
793
|
+
const all = [...this.dataCatalogs.values()].map(({ Name, Description, Type }) => ({ CatalogName: Name, Description, Type }));
|
|
794
|
+
const start = NextToken ? all.findIndex((c) => c.CatalogName === NextToken) + 1 : 0;
|
|
795
|
+
const page = all.slice(start, start + MaxResults);
|
|
796
|
+
const next = start + MaxResults < all.length ? all[start + MaxResults].CatalogName : null;
|
|
797
|
+
return { DataCatalogsSummary: page, NextToken: next };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ── Databases ─────────────────────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
listDatabases(params) {
|
|
803
|
+
const { CatalogName, NextToken, MaxResults = 50 } = params;
|
|
804
|
+
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
805
|
+
if (!this.dataCatalogs.has(CatalogName)) throw Errors.DataCatalogNotFound(CatalogName);
|
|
806
|
+
|
|
807
|
+
const all = [...this.databases.entries()]
|
|
808
|
+
.filter(([k]) => k.startsWith(CatalogName + '.'))
|
|
809
|
+
.map(([, v]) => v);
|
|
810
|
+
|
|
811
|
+
const start = NextToken ? all.findIndex((d) => d.Name === NextToken) + 1 : 0;
|
|
812
|
+
const page = all.slice(start, start + MaxResults);
|
|
813
|
+
const next = start + MaxResults < all.length ? all[start + MaxResults].Name : null;
|
|
814
|
+
|
|
815
|
+
return { DatabaseList: page, NextToken: next };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
getDatabase(params) {
|
|
819
|
+
const { CatalogName, DatabaseName } = params;
|
|
820
|
+
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
821
|
+
if (!DatabaseName) throw Errors.InvalidRequest('DatabaseName is required');
|
|
822
|
+
|
|
823
|
+
const key = `${CatalogName}.${DatabaseName}`;
|
|
824
|
+
const db = this.databases.get(key);
|
|
825
|
+
if (!db) throw Errors.DatabaseNotFound(DatabaseName);
|
|
826
|
+
return { Database: db };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ── Table Metadata ────────────────────────────────────────────────────────
|
|
830
|
+
|
|
831
|
+
listTableMetadata(params) {
|
|
832
|
+
const { CatalogName, DatabaseName, Expression, NextToken, MaxResults = 50 } = params;
|
|
833
|
+
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
834
|
+
if (!DatabaseName) throw Errors.InvalidRequest('DatabaseName is required');
|
|
835
|
+
|
|
836
|
+
const prefix = `${CatalogName}.${DatabaseName}.`;
|
|
837
|
+
let tables = [...this.tables.entries()]
|
|
838
|
+
.filter(([k]) => k.startsWith(prefix))
|
|
839
|
+
.map(([, v]) => v);
|
|
840
|
+
|
|
841
|
+
if (Expression) {
|
|
842
|
+
const re = new RegExp(Expression.replace(/\*/g, '.*'), 'i');
|
|
843
|
+
tables = tables.filter((t) => re.test(t.Name));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const start = NextToken ? tables.findIndex((t) => t.Name === NextToken) + 1 : 0;
|
|
847
|
+
const page = tables.slice(start, start + MaxResults);
|
|
848
|
+
const next = start + MaxResults < tables.length ? tables[start + MaxResults].Name : null;
|
|
849
|
+
|
|
850
|
+
return { TableMetadataList: page, NextToken: next };
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
getTableMetadata(params) {
|
|
854
|
+
const { CatalogName, DatabaseName, TableName } = params;
|
|
855
|
+
if (!CatalogName) throw Errors.InvalidRequest('CatalogName is required');
|
|
856
|
+
if (!DatabaseName) throw Errors.InvalidRequest('DatabaseName is required');
|
|
857
|
+
if (!TableName) throw Errors.InvalidRequest('TableName is required');
|
|
858
|
+
|
|
859
|
+
const key = `${CatalogName}.${DatabaseName}.${TableName}`;
|
|
860
|
+
const table = this.tables.get(key);
|
|
861
|
+
if (!table) throw Errors.TableNotFound(TableName);
|
|
862
|
+
return { TableMetadata: table };
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── Prepared Statements ───────────────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
createPreparedStatement(params) {
|
|
868
|
+
const { StatementName, WorkGroup, QueryStatement, Description } = params;
|
|
869
|
+
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
870
|
+
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
871
|
+
if (!QueryStatement) throw Errors.InvalidRequest('QueryStatement is required');
|
|
872
|
+
|
|
873
|
+
this._resolveWorkgroup(WorkGroup);
|
|
874
|
+
|
|
875
|
+
const key = `${WorkGroup}.${StatementName}`;
|
|
876
|
+
const stmt = {
|
|
877
|
+
StatementName,
|
|
878
|
+
WorkGroupName: WorkGroup,
|
|
879
|
+
QueryStatement,
|
|
880
|
+
Description: Description || '',
|
|
881
|
+
LastModifiedTime: new Date().toISOString(),
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
this.preparedStatements.set(key, stmt);
|
|
885
|
+
this.save();
|
|
886
|
+
this.logger.debug(`[Athena] CreatePreparedStatement: ${key}`);
|
|
887
|
+
return {};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
updatePreparedStatement(params) {
|
|
891
|
+
const { StatementName, WorkGroup, QueryStatement, Description } = params;
|
|
892
|
+
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
893
|
+
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
894
|
+
|
|
895
|
+
const key = `${WorkGroup}.${StatementName}`;
|
|
896
|
+
const stmt = this.preparedStatements.get(key);
|
|
897
|
+
if (!stmt) throw Errors.PreparedStatementNotFound(StatementName);
|
|
898
|
+
|
|
899
|
+
if (QueryStatement) stmt.QueryStatement = QueryStatement;
|
|
900
|
+
if (Description !== undefined) stmt.Description = Description;
|
|
901
|
+
stmt.LastModifiedTime = new Date().toISOString();
|
|
902
|
+
|
|
903
|
+
this.preparedStatements.set(key, stmt);
|
|
904
|
+
this.save();
|
|
905
|
+
return {};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
deletePreparedStatement(params) {
|
|
909
|
+
const { StatementName, WorkGroup } = params;
|
|
910
|
+
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
911
|
+
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
912
|
+
|
|
913
|
+
const key = `${WorkGroup}.${StatementName}`;
|
|
914
|
+
if (!this.preparedStatements.has(key)) throw Errors.PreparedStatementNotFound(StatementName);
|
|
915
|
+
|
|
916
|
+
this.preparedStatements.delete(key);
|
|
917
|
+
this.save();
|
|
918
|
+
return {};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
getPreparedStatement(params) {
|
|
922
|
+
const { StatementName, WorkGroup } = params;
|
|
923
|
+
if (!StatementName) throw Errors.InvalidRequest('StatementName is required');
|
|
924
|
+
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
925
|
+
|
|
926
|
+
const key = `${WorkGroup}.${StatementName}`;
|
|
927
|
+
const stmt = this.preparedStatements.get(key);
|
|
928
|
+
if (!stmt) throw Errors.PreparedStatementNotFound(StatementName);
|
|
929
|
+
return { PreparedStatement: stmt };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
listPreparedStatements(params) {
|
|
933
|
+
const { WorkGroup, NextToken, MaxResults = 50 } = params;
|
|
934
|
+
if (!WorkGroup) throw Errors.InvalidRequest('WorkGroup is required');
|
|
935
|
+
this._resolveWorkgroup(WorkGroup);
|
|
936
|
+
|
|
937
|
+
const all = [...this.preparedStatements.entries()]
|
|
938
|
+
.filter(([k]) => k.startsWith(WorkGroup + '.'))
|
|
939
|
+
.map(([, v]) => ({ StatementName: v.StatementName, LastModifiedTime: v.LastModifiedTime }));
|
|
940
|
+
|
|
941
|
+
const start = NextToken ? all.findIndex((s) => s.StatementName === NextToken) + 1 : 0;
|
|
942
|
+
const page = all.slice(start, start + MaxResults);
|
|
943
|
+
const next = start + MaxResults < all.length ? all[start + MaxResults].StatementName : null;
|
|
944
|
+
|
|
945
|
+
return { PreparedStatements: page, NextToken: next };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// ── Tags ──────────────────────────────────────────────────────────────────
|
|
949
|
+
|
|
950
|
+
tagResource(params) {
|
|
951
|
+
const { ResourceARN, Tags } = params;
|
|
952
|
+
if (!ResourceARN) throw Errors.InvalidRequest('ResourceARN is required');
|
|
953
|
+
if (!Tags || !Array.isArray(Tags)) throw Errors.InvalidRequest('Tags is required');
|
|
954
|
+
|
|
955
|
+
const existing = this.tags.get(ResourceARN) || [];
|
|
956
|
+
const tagMap = {};
|
|
957
|
+
for (const t of existing) tagMap[t.Key] = t.Value;
|
|
958
|
+
for (const t of Tags) tagMap[t.Key] = t.Value;
|
|
959
|
+
|
|
960
|
+
this.tags.set(ResourceARN, Object.entries(tagMap).map(([Key, Value]) => ({ Key, Value })));
|
|
961
|
+
this.save();
|
|
962
|
+
return {};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
untagResource(params) {
|
|
966
|
+
const { ResourceARN, TagKeys } = params;
|
|
967
|
+
if (!ResourceARN) throw Errors.InvalidRequest('ResourceARN is required');
|
|
968
|
+
if (!TagKeys || !Array.isArray(TagKeys)) throw Errors.InvalidRequest('TagKeys is required');
|
|
969
|
+
|
|
970
|
+
const existing = this.tags.get(ResourceARN) || [];
|
|
971
|
+
this.tags.set(ResourceARN, existing.filter((t) => !TagKeys.includes(t.Key)));
|
|
972
|
+
this.save();
|
|
973
|
+
return {};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
listTagsForResource(params) {
|
|
977
|
+
const { ResourceARN } = params;
|
|
978
|
+
if (!ResourceARN) throw Errors.InvalidRequest('ResourceARN is required');
|
|
979
|
+
const tags = this.tags.get(ResourceARN) || [];
|
|
980
|
+
return { Tags: tags };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// ── Admin helpers ─────────────────────────────────────────────────────────
|
|
984
|
+
|
|
985
|
+
getAdminStatus() {
|
|
986
|
+
return {
|
|
987
|
+
queryExecutions: this.queryExecutions.size,
|
|
988
|
+
namedQueries: this.namedQueries.size,
|
|
989
|
+
workgroups: this.workgroups.size,
|
|
990
|
+
dataCatalogs: this.dataCatalogs.size,
|
|
991
|
+
databases: this.databases.size,
|
|
992
|
+
tables: this.tables.size,
|
|
993
|
+
preparedStatements: this.preparedStatements.size,
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
module.exports = { AthenaSimulator };
|