fix: preserve accepted Ticket query filters

This commit is contained in:
2026-08-18 08:06:21 +09:00
parent 4e738ac5eb
commit b723c64fa1
+66 -16
View File
@@ -354,11 +354,11 @@ impl SqliteWorkspaceAuthority {
}
if let Some(value) = &query.updated_after {
let value = bind(SqlValue::Text(value.clone()));
predicates.push(format!("COALESCE(t.updated_at,'')>={value}"));
predicates.push(format!("COALESCE(t.updated_at,'')>{value}"));
}
if let Some(value) = &query.updated_before {
let value = bind(SqlValue::Text(value.clone()));
predicates.push(format!("COALESCE(t.updated_at,'')<={value}"));
predicates.push(format!("COALESCE(t.updated_at,'')<{value}"));
}
if let Some(value) = &query.linked_objective_id {
let value = bind(SqlValue::Text(value.clone()));
@@ -373,7 +373,9 @@ impl SqliteWorkspaceAuthority {
.relation_kind
.as_ref()
.map(|value| bind(SqlValue::Text(value.clone())));
let related = related.map(|value| format!("AND ((r.ticket_id=t.ticket_id AND r.target={value}) OR (r.target=t.ticket_id AND r.ticket_id={value}))")).unwrap_or_default();
let related = related
.map(|value| format!("AND r.ticket_id=t.ticket_id AND r.target={value}"))
.unwrap_or_else(|| "AND r.ticket_id=t.ticket_id".to_string());
let kind = kind
.map(|value| format!("AND r.kind={value}"))
.unwrap_or_default();
@@ -427,14 +429,20 @@ impl SqliteWorkspaceAuthority {
"CASE WHEN {merge_request_id} IS NULL THEN 'none' WHEN {review_decision}='approve' THEN 'approved' WHEN {review_decision}='request_changes' THEN 'request_changes' ELSE 'pending' END"
);
let has_commit = format!(
"({merge_request_id} IS NOT NULL OR EXISTS (SELECT 1 FROM typed_ticket_event_references reference WHERE reference.workspace_id=t.workspace_id AND reference.ticket_id=t.ticket_id AND reference.kind='commit'))"
"({review_subject} IS NOT NULL OR EXISTS (SELECT 1 FROM typed_ticket_event_references reference WHERE reference.workspace_id=t.workspace_id AND reference.ticket_id=t.ticket_id AND reference.kind='commit'))"
);
for event_kind in &query.event_kinds {
let event_kind = bind(SqlValue::Text(event_kind.clone()));
predicates.push(format!("EXISTS (SELECT 1 FROM typed_ticket_events event WHERE event.workspace_id=t.workspace_id AND event.ticket_id=t.ticket_id AND event.kind={event_kind})"));
if !query.event_kinds.is_empty() {
let event_kinds = query
.event_kinds
.iter()
.map(|event_kind| bind(SqlValue::Text(event_kind.clone())))
.collect::<Vec<_>>();
predicates.push(format!("EXISTS (SELECT 1 FROM typed_ticket_events event WHERE event.workspace_id=t.workspace_id AND event.ticket_id=t.ticket_id AND event.kind IN ({}))", event_kinds.join(",")));
}
for evidence in &query.evidence {
predicates.push(match evidence.as_str() {
"implementation_report" => format!("{report_index} IS NOT NULL"),
"implementation_report_after_rescope" => current_report.clone(),
"merge_request" => format!("{merge_request_id} IS NOT NULL"),
"commit" => has_commit.clone(),
"approved_review" => format!("{review_status}='approved'"),
@@ -446,7 +454,7 @@ impl SqliteWorkspaceAuthority {
});
}
if let Some(status) = &query.review_status {
let status = if status == "unresolved_changes" {
let status = if matches!(status.as_str(), "unresolved_changes" | "changes_requested") {
"request_changes"
} else {
status.as_str()
@@ -457,15 +465,29 @@ impl SqliteWorkspaceAuthority {
for attention in &query.attention {
predicates.push(match attention.as_str() {
"done_not_closed" => "t.workflow_state='done'".to_string(),
"unresolved_review" | "unresolved_changes" => format!("{review_status}='request_changes'"),
"implementation_report_not_closed" => {
format!("{report_index} IS NOT NULL AND t.workflow_state!='closed'")
}
"report_after_rescope" => current_report.clone(),
"unresolved_review" | "unresolved_changes" => {
format!("{review_status}='request_changes'")
}
"missing_commit" => format!("NOT {has_commit}"),
"blocked" => blocker.to_string(),
"unblocked" => format!("NOT {blocker}"),
"ready" => format!("t.workflow_state='ready' AND NOT {active_blocker}"),
"awaiting_review" => format!("t.workflow_state='inprogress' AND {current_report} AND {review_status}='pending'"),
"stale_after_rescope" => format!("{report_index} IS NOT NULL AND {edit_index}>{report_index}"),
"missing_evidence" => format!("NOT ({current_report} AND {has_commit} AND {review_status}='approved')"),
other => return Err(Error::InvalidRecordId(format!("unsupported attention filter `{other}`"))),
"ready" => format!("t.workflow_state='ready' AND NOT {blocker}"),
"awaiting_review" => format!("{review_status}='pending'"),
"stale_after_rescope" => {
format!("{report_index} IS NOT NULL AND NOT {current_report}")
}
"missing_evidence" => format!(
"NOT ({current_report} AND {has_commit} AND {review_status}='approved')"
),
other => {
return Err(Error::InvalidRecordId(format!(
"unsupported attention filter `{other}`"
)));
}
});
}
let rank_expression = match sort {
@@ -561,11 +583,11 @@ impl SqliteWorkspaceAuthority {
}
if let Some(value) = &query.updated_after {
let value = bind(SqlValue::Text(value.clone()));
predicates.push(format!("o.updated_at>={value}"));
predicates.push(format!("o.updated_at>{value}"));
}
if let Some(value) = &query.updated_before {
let value = bind(SqlValue::Text(value.clone()));
predicates.push(format!("o.updated_at<={value}"));
predicates.push(format!("o.updated_at<{value}"));
}
if let Some(value) = &query.linked_ticket_id {
let value = bind(SqlValue::Text(value.clone()));
@@ -2576,6 +2598,34 @@ mod tests {
.any(|item| item.id == "00000000001J2"),
"review-status storage predicate must query the authoritative MR thread schema"
);
for evidence in [
"implementation_report",
"implementation_report_after_rescope",
] {
authority
.query_tickets(TicketQueryRequest {
evidence: vec![evidence.to_string()],
limit: Some(10),
..TicketQueryRequest::default()
})
.unwrap_or_else(|error| panic!("accepted evidence filter {evidence}: {error}"));
}
for attention in ["implementation_report_not_closed", "report_after_rescope"] {
authority
.query_tickets(TicketQueryRequest {
attention: vec![attention.to_string()],
limit: Some(10),
..TicketQueryRequest::default()
})
.unwrap_or_else(|error| panic!("accepted attention filter {attention}: {error}"));
}
authority
.query_tickets(TicketQueryRequest {
review_status: Some("changes_requested".to_string()),
limit: Some(10),
..TicketQueryRequest::default()
})
.expect("accepted review-status alias must execute");
let historical_event_query = authority
.query_tickets(TicketQueryRequest {
query: Some("Historical event marker".to_string()),