Skip to content

Commit 3ee274f

Browse files
authored
fix(glue): real TableVersion delete/lookup, parallel GetPartitions, observable ResumeWorkflowRun + job field persistence (bug-hunt) (#2314)
2 parents 23ba989 + a1c374d commit 3ee274f

12 files changed

Lines changed: 605 additions & 62 deletions

File tree

crates/fakecloud-athena/src/service/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,7 @@ mod tests {
11991199
table_type: Some("EXTERNAL_TABLE".to_string()),
12001200
parameters: std::collections::BTreeMap::new(),
12011201
partitions: std::collections::BTreeMap::new(),
1202+
catalog_id: "123456789012".to_string(),
12021203
}
12031204
}
12041205

crates/fakecloud-cloudformation/src/resource_provisioner/glue.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ impl ResourceProvisioner {
9797
fakecloud_glue::Table {
9898
name: name.clone(),
9999
database_name: db_name.clone(),
100+
catalog_id: self.account_id.clone(),
100101
description: input
101102
.get("Description")
102103
.and_then(|v| v.as_str())

crates/fakecloud-glue/src/catalog.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,12 @@ impl GlueService {
231231
.collect()
232232
})
233233
.unwrap_or_default();
234-
Ok(AwsResponse::ok_json(
235-
json!({ "UserDefinedFunctions": list }),
236-
))
234+
let (page, token) = crate::common::paginate_body(&body, list);
235+
let mut resp = json!({ "UserDefinedFunctions": page });
236+
if let Some(t) = token {
237+
resp["NextToken"] = json!(t);
238+
}
239+
Ok(AwsResponse::ok_json(resp))
237240
}
238241

239242
pub(crate) fn update_user_defined_function(

crates/fakecloud-glue/src/common.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ pub(crate) fn resource_arn(account: &str, region: &str, kind: &str, name: &str)
9393
format!("arn:aws:glue:{region}:{account}:{kind}/{name}")
9494
}
9595

96+
/// Paginate a Glue list op using the request's `MaxResults`/`NextToken`.
97+
///
98+
/// Uses the offset-token scheme in [`fakecloud_core::pagination::paginate`]:
99+
/// the token is the numeric offset of the next page. When `MaxResults` is
100+
/// absent the full remaining list (from any incoming offset) is returned in a
101+
/// single page with no continuation token — matching how AWS returns an
102+
/// unbounded page when the caller omits a page size.
103+
pub(crate) fn paginate_body(body: &Value, items: Vec<Value>) -> (Vec<Value>, Option<String>) {
104+
let token = body.get("NextToken").and_then(|v| v.as_str());
105+
let max = body
106+
.get("MaxResults")
107+
.and_then(|v| v.as_u64())
108+
.map(|n| n as usize);
109+
match max {
110+
Some(m) => fakecloud_core::pagination::paginate(&items, token, m),
111+
None => fakecloud_core::pagination::paginate(&items, token, usize::MAX),
112+
}
113+
}
114+
96115
/// A small UUID-ish identifier (32 hex chars). Glue uses these for run ids,
97116
/// transform ids, etc.
98117
pub(crate) fn new_id() -> String {

crates/fakecloud-glue/src/connections.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,27 @@ impl GlueService {
5858
pub(crate) fn get_connection(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
5959
let body = req.json_body();
6060
let name = req_str(&body, "Name")?;
61+
let hide_password = body
62+
.get("HidePassword")
63+
.and_then(|v| v.as_bool())
64+
.unwrap_or(false);
6165
let accounts = self.state.read();
62-
let c = accounts
66+
let mut c = accounts
6367
.get(&req.account_id)
6468
.and_then(|s| s.connections.get(name))
69+
.cloned()
6570
.ok_or_else(|| entity_not_found(format!("Connection {name} not found")))?;
71+
// With HidePassword=true AWS strips the PASSWORD from ConnectionProperties
72+
// (and any encrypted password) rather than returning it verbatim.
73+
if hide_password {
74+
if let Some(props) = c
75+
.get_mut("ConnectionProperties")
76+
.and_then(|v| v.as_object_mut())
77+
{
78+
props.remove("PASSWORD");
79+
props.remove("ENCRYPTED_PASSWORD");
80+
}
81+
}
6682
Ok(AwsResponse::ok_json(json!({ "Connection": c })))
6783
}
6884

crates/fakecloud-glue/src/crawlers.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,18 @@ impl GlueService {
8282
}
8383

8484
pub(crate) fn get_crawlers(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
85+
let body = req.json_body();
8586
let accounts = self.state.read();
8687
let crawlers: Vec<Value> = accounts
8788
.get(&req.account_id)
8889
.map(|s| s.crawlers.values().cloned().collect())
8990
.unwrap_or_default();
90-
Ok(AwsResponse::ok_json(json!({ "Crawlers": crawlers })))
91+
let (page, token) = crate::common::paginate_body(&body, crawlers);
92+
let mut resp = json!({ "Crawlers": page });
93+
if let Some(t) = token {
94+
resp["NextToken"] = json!(t);
95+
}
96+
Ok(AwsResponse::ok_json(resp))
9197
}
9298

9399
pub(crate) fn batch_get_crawlers(

crates/fakecloud-glue/src/jobs.rs

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ pub(crate) fn job_to_json(j: &Job) -> Value {
6868
"SecurityConfiguration": j.security_configuration,
6969
"NotificationProperty": j.notification_property,
7070
"NonOverridableArguments": j.non_overridable_arguments,
71+
"SourceControlDetails": j.source_control_details,
72+
"CodeGenConfigurationNodes": j.code_gen_configuration_nodes,
73+
"MaintenanceWindow": j.maintenance_window,
74+
"LogUri": j.log_uri,
75+
"AllocatedCapacity": j.allocated_capacity,
7176
})
7277
}
7378

@@ -138,6 +143,11 @@ impl GlueService {
138143
security_configuration: body["SecurityConfiguration"].as_str().map(String::from),
139144
notification_property: opt_value(&body["NotificationProperty"]),
140145
non_overridable_arguments: parse_string_map(&body["NonOverridableArguments"]),
146+
source_control_details: opt_value(&body["SourceControlDetails"]),
147+
code_gen_configuration_nodes: opt_value(&body["CodeGenConfigurationNodes"]),
148+
maintenance_window: body["MaintenanceWindow"].as_str().map(String::from),
149+
log_uri: body["LogUri"].as_str().map(String::from),
150+
allocated_capacity: body["AllocatedCapacity"].as_i64(),
141151
};
142152
state.jobs.insert(name.to_string(), job);
143153
Ok(AwsResponse::ok_json(json!({ "Name": name })))
@@ -155,21 +165,33 @@ impl GlueService {
155165
}
156166

157167
pub(crate) fn get_jobs(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
168+
let body = req.json_body();
158169
let accounts = self.state.read();
159170
let jobs: Vec<Value> = accounts
160171
.get(&req.account_id)
161172
.map(|s| s.jobs.values().map(job_to_json).collect())
162173
.unwrap_or_default();
163-
Ok(AwsResponse::ok_json(json!({ "Jobs": jobs })))
174+
let (page, token) = crate::common::paginate_body(&body, jobs);
175+
let mut resp = json!({ "Jobs": page });
176+
if let Some(t) = token {
177+
resp["NextToken"] = json!(t);
178+
}
179+
Ok(AwsResponse::ok_json(resp))
164180
}
165181

166182
pub(crate) fn list_jobs(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
183+
let body = req.json_body();
167184
let accounts = self.state.read();
168-
let names: Vec<String> = accounts
185+
let names: Vec<Value> = accounts
169186
.get(&req.account_id)
170-
.map(|s| s.jobs.keys().cloned().collect())
187+
.map(|s| s.jobs.keys().map(|k| json!(k)).collect())
171188
.unwrap_or_default();
172-
Ok(AwsResponse::ok_json(json!({ "JobNames": names })))
189+
let (page, token) = crate::common::paginate_body(&body, names);
190+
let mut resp = json!({ "JobNames": page });
191+
if let Some(t) = token {
192+
resp["NextToken"] = json!(t);
193+
}
194+
Ok(AwsResponse::ok_json(resp))
173195
}
174196

175197
pub(crate) fn update_job(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
@@ -230,6 +252,21 @@ impl GlueService {
230252
if update["NonOverridableArguments"].is_object() {
231253
job.non_overridable_arguments = parse_string_map(&update["NonOverridableArguments"]);
232254
}
255+
if !update["SourceControlDetails"].is_null() {
256+
job.source_control_details = opt_value(&update["SourceControlDetails"]);
257+
}
258+
if !update["CodeGenConfigurationNodes"].is_null() {
259+
job.code_gen_configuration_nodes = opt_value(&update["CodeGenConfigurationNodes"]);
260+
}
261+
if let Some(s) = update["MaintenanceWindow"].as_str() {
262+
job.maintenance_window = Some(s.to_string());
263+
}
264+
if let Some(s) = update["LogUri"].as_str() {
265+
job.log_uri = Some(s.to_string());
266+
}
267+
if let Some(n) = update["AllocatedCapacity"].as_i64() {
268+
job.allocated_capacity = Some(n);
269+
}
233270
job.last_modified_on = Utc::now();
234271
Ok(AwsResponse::ok_json(json!({ "JobName": name })))
235272
}

crates/fakecloud-glue/src/service.rs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,12 @@ pub(crate) fn table_json(t: &Table) -> Value {
920920
"CreateTime": t.create_time.timestamp() as f64,
921921
"UpdateTime": t.update_time.timestamp() as f64,
922922
});
923+
// Emit CatalogId only when populated: AWS omits absent members rather than
924+
// returning an empty string, and tables restored from a pre-`catalog_id`
925+
// snapshot carry an empty default.
926+
if !t.catalog_id.is_empty() {
927+
o["CatalogId"] = json!(t.catalog_id);
928+
}
923929
if let Some(ref d) = t.description {
924930
o["Description"] = json!(d);
925931
}
@@ -944,6 +950,15 @@ pub(crate) fn table_json(t: &Table) -> Value {
944950
o
945951
}
946952

953+
/// The current (latest) archived VersionId for a table, defaulting to "1" for
954+
/// tables predating the version archive. Glue reports this on GetTable(s).
955+
fn current_table_version(st: &crate::state::GlueState, db: &str, table: &str) -> String {
956+
st.table_version_ids(db, table)
957+
.last()
958+
.map(|n| n.to_string())
959+
.unwrap_or_else(|| "1".to_string())
960+
}
961+
947962
pub(crate) fn partition_json(p: &Partition) -> Value {
948963
let mut o = json!({
949964
"Values": p.values,
@@ -1038,13 +1053,19 @@ impl GlueService {
10381053
}
10391054

10401055
pub(crate) fn get_databases(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1056+
let body = req.json_body();
10411057
let accounts = self.state.read();
10421058
let dbs: Vec<Value> = accounts
10431059
.get(&req.account_id)
10441060
.and_then(|s| s.dbs_in(&req.region))
10451061
.map(|map| map.values().map(database_json).collect())
10461062
.unwrap_or_default();
1047-
Ok(AwsResponse::ok_json(json!({"DatabaseList": dbs})))
1063+
let (page, token) = crate::common::paginate_body(&body, dbs);
1064+
let mut resp = json!({ "DatabaseList": page });
1065+
if let Some(t) = token {
1066+
resp["NextToken"] = json!(t);
1067+
}
1068+
Ok(AwsResponse::ok_json(resp))
10481069
}
10491070

10501071
pub(crate) fn update_database(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
@@ -1104,6 +1125,7 @@ impl GlueService {
11041125
let table = Table {
11051126
name: name.clone(),
11061127
database_name: db_name.clone(),
1128+
catalog_id: req.account_id.clone(),
11071129
description: input["Description"].as_str().map(|s| s.to_string()),
11081130
owner: input["Owner"].as_str().map(|s| s.to_string()),
11091131
create_time: now,
@@ -1145,7 +1167,9 @@ impl GlueService {
11451167
.tables
11461168
.get(name)
11471169
.ok_or_else(|| entity_not_found(format!("Table {name} not found")))?;
1148-
Ok(AwsResponse::ok_json(json!({"Table": table_json(t)})))
1170+
let mut tj = table_json(t);
1171+
tj["VersionId"] = json!(current_table_version(state, db_name, name));
1172+
Ok(AwsResponse::ok_json(json!({"Table": tj})))
11491173
}
11501174

11511175
pub(crate) fn get_tables(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
@@ -1161,19 +1185,24 @@ impl GlueService {
11611185
.filter(|e| !e.is_empty())
11621186
.and_then(|e| regex::Regex::new(e).ok());
11631187
let accounts = self.state.read();
1164-
let tables: Vec<Value> = accounts
1165-
.get(&req.account_id)
1166-
.and_then(|s| s.dbs_in(&req.region))
1167-
.and_then(|dbs| dbs.get(db_name))
1168-
.map(|db| {
1169-
db.tables
1170-
.values()
1171-
.filter(|t| name_filter.as_ref().is_none_or(|re| re.is_match(&t.name)))
1172-
.map(table_json)
1173-
.collect()
1174-
})
1175-
.unwrap_or_default();
1176-
Ok(AwsResponse::ok_json(json!({"TableList": tables})))
1188+
let mut tables: Vec<Value> = Vec::new();
1189+
if let Some(s) = accounts.get(&req.account_id) {
1190+
if let Some(db) = s.dbs_in(&req.region).and_then(|dbs| dbs.get(db_name)) {
1191+
for t in db.tables.values() {
1192+
if name_filter.as_ref().is_none_or(|re| re.is_match(&t.name)) {
1193+
let mut tj = table_json(t);
1194+
tj["VersionId"] = json!(current_table_version(s, db_name, &t.name));
1195+
tables.push(tj);
1196+
}
1197+
}
1198+
}
1199+
}
1200+
let (page, token) = crate::common::paginate_body(&body, tables);
1201+
let mut resp = json!({ "TableList": page });
1202+
if let Some(t) = token {
1203+
resp["NextToken"] = json!(t);
1204+
}
1205+
Ok(AwsResponse::ok_json(resp))
11771206
}
11781207

11791208
pub(crate) fn update_table(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
@@ -1341,6 +1370,19 @@ impl GlueService {
13411370
.as_str()
13421371
.ok_or_else(|| missing("TableName"))?;
13431372
let expression = body["Expression"].as_str().unwrap_or("");
1373+
// Parallel-scan support: when a `Segment{SegmentNumber, TotalSegments}`
1374+
// is supplied, each worker must see a disjoint slice of the matching
1375+
// partitions so the union across all segments equals the full set with
1376+
// no duplicates. Partition the deterministic (BTreeMap-ordered) match
1377+
// list by index modulo TotalSegments.
1378+
let segment = &body["Segment"];
1379+
let (segment_number, total_segments) = if segment.is_object() {
1380+
let total = segment["TotalSegments"].as_u64().unwrap_or(1).max(1);
1381+
let number = segment["SegmentNumber"].as_u64().unwrap_or(0);
1382+
(number, total)
1383+
} else {
1384+
(0, 1)
1385+
};
13441386
let accounts = self.state.read();
13451387
let parts: Vec<Value> = accounts
13461388
.get(&req.account_id)
@@ -1358,11 +1400,20 @@ impl GlueService {
13581400
&p.values,
13591401
)
13601402
})
1361-
.map(partition_json)
1403+
.enumerate()
1404+
.filter(|(i, _)| {
1405+
total_segments == 1 || (*i as u64) % total_segments == segment_number
1406+
})
1407+
.map(|(_, p)| partition_json(p))
13621408
.collect()
13631409
})
13641410
.unwrap_or_default();
1365-
Ok(AwsResponse::ok_json(json!({"Partitions": parts})))
1411+
let (page, token) = crate::common::paginate_body(&body, parts);
1412+
let mut resp = json!({ "Partitions": page });
1413+
if let Some(t) = token {
1414+
resp["NextToken"] = json!(t);
1415+
}
1416+
Ok(AwsResponse::ok_json(resp))
13661417
}
13671418

13681419
pub(crate) fn update_partition(

crates/fakecloud-glue/src/state.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,19 @@ pub struct Job {
187187
pub notification_property: Option<serde_json::Value>,
188188
#[serde(default)]
189189
pub non_overridable_arguments: BTreeMap<String, String>,
190+
/// Standard Job fields that were previously accepted then dropped
191+
/// (bug-hunt 2026-07-16). Round-tripped verbatim. `CodeGenConfigurationNodes`
192+
/// is a large nested shape stored opaquely rather than dropped.
193+
#[serde(default)]
194+
pub source_control_details: Option<serde_json::Value>,
195+
#[serde(default)]
196+
pub code_gen_configuration_nodes: Option<serde_json::Value>,
197+
#[serde(default)]
198+
pub maintenance_window: Option<String>,
199+
#[serde(default)]
200+
pub log_uri: Option<String>,
201+
#[serde(default)]
202+
pub allocated_capacity: Option<i64>,
190203
}
191204

192205
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -283,6 +296,9 @@ pub struct Database {
283296
pub struct Table {
284297
pub name: String,
285298
pub database_name: String,
299+
/// Owning Data Catalog id (the account id). Reported on read as `CatalogId`.
300+
#[serde(default)]
301+
pub catalog_id: String,
286302
pub description: Option<String>,
287303
pub owner: Option<String>,
288304
pub create_time: DateTime<Utc>,

0 commit comments

Comments
 (0)