use anyhow::Result;
use arcgis::example_tracker::ExampleTracker;
use arcgis::{
ArcGISClient, FeatureQueryParams, FeatureServiceClient, LayerId, NoAuth, ObjectId,
RelatedRecordsParams, StatisticDefinition, StatisticType, TopFeaturesParams, TopFilter,
};
use tracing::instrument;
const SAMPLE_SERVICE: &str =
"https://sampleserver6.arcgisonline.com/arcgis/rest/services/SF311/FeatureServer";
#[tokio::main]
#[instrument]
async fn main() -> Result<()> {
tracing::debug!("Initializing tracing subscriber");
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let tracker = ExampleTracker::new("advanced_queries")
.service_type("ExampleClient")
.start();
tracing::info!("🔍 Advanced Feature Service Query Examples");
tracing::info!("Use Case: SF311 Service Request Analysis");
tracing::debug!(service_url = %SAMPLE_SERVICE, "Using ESRI sample SF311 service");
tracing::debug!("Creating ArcGIS client for public service");
let client = ArcGISClient::new(NoAuth);
let service = FeatureServiceClient::new(SAMPLE_SERVICE, &client);
let layer_id = LayerId::new(0);
tracing::info!("\n📊 Demonstrating Advanced Query Capabilities\n");
tracing::debug!("Testing basic connectivity with simple query");
let test_result = service
.query(layer_id)
.where_clause("1=1")
.limit(10)
.return_geometry(false)
.execute()
.await?;
let feature_count = test_result.features().len();
tracing::info!(
features = feature_count,
"Basic query successful - authentication working"
);
anyhow::ensure!(
feature_count > 0,
"Service returned no features - cannot demonstrate advanced queries. \
Service may be empty or query may have failed silently."
);
demonstrate_statistical_aggregations(&service, layer_id).await?;
demonstrate_pagination_strategies(&service, layer_id).await?;
demonstrate_feature_count(&service, layer_id).await?;
demonstrate_query_with_params(&service, layer_id).await?;
tracing::info!("\n⚠️ Note: Some advanced queries require newer service versions:");
tracing::info!(" - queryTopFeatures (ArcGIS Enterprise 10.3+, requires newer service)");
tracing::info!(" - queryRelatedRecords (requires relationship classes defined)");
tracing::info!(" - queryDomains (requires domain/subtype definitions)");
tracing::info!("\n This example demonstrates statistical aggregations and pagination,");
tracing::info!(" which work with all ArcGIS service versions.");
tracing::info!("\n✅ All advanced query examples completed successfully!");
print_best_practices();
tracker.success();
Ok(())
}
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_statistical_aggregations(
service: &FeatureServiceClient<'_>,
layer_id: LayerId,
) -> Result<()> {
tracing::info!("=== Example 1: Statistical Aggregations ===");
tracing::info!("Calculate service request statistics by type");
tracing::debug!("Creating statistic definitions for aggregation");
let stats = vec![StatisticDefinition::new(
StatisticType::Count,
"objectid".to_string(),
"total_requests".to_string(),
)];
tracing::debug!(stat_count = stats.len(), "Prepared statistic definitions");
tracing::debug!("Executing statistics query with GROUP BY");
let result = service
.query(layer_id)
.where_clause("1=1")
.statistics(stats)
.group_by(&["req_type"])
.order_by(&["total_requests DESC"])
.return_geometry(false)
.execute()
.await?;
let feature_count = result.features().len();
tracing::info!(
groups = feature_count,
"Statistics calculated by request type"
);
tracing::debug!(
exceeded_limit = result.exceeded_transfer_limit(),
"Query completion status"
);
anyhow::ensure!(
feature_count > 0,
"Statistics query returned no groups. Expected aggregated results by req_type field. \
Service may not have data or req_type field may not exist."
);
for (idx, feature) in result.features().iter().take(5).enumerate() {
let req_type = feature.attributes().get("req_type");
let count = feature.attributes().get("total_requests");
anyhow::ensure!(
count.is_some(),
"Statistics result missing 'total_requests' field. \
Aggregation may have failed or field name mismatch."
);
tracing::info!(
rank = idx + 1,
request_type = ?req_type,
request_count = ?count,
"Service request type statistics"
);
}
tracing::debug!("Statistical aggregations demonstration complete");
Ok(())
}
#[allow(dead_code)]
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_top_features(
service: &FeatureServiceClient<'_>,
layer_id: LayerId,
) -> Result<()> {
tracing::info!("\n=== Example 2: Top N Features Query ===");
tracing::info!("Find top 5 service requests by district");
tracing::debug!("Creating TopFilter specification");
let top_filter = TopFilter::new(
vec!["district".to_string()], 5, vec!["objectid DESC".to_string()], );
tracing::debug!(
group_by_fields = ?["district"],
top_count = 5,
"TopFilter configuration prepared"
);
tracing::debug!("Building TopFeaturesParams");
let params = TopFeaturesParams::builder()
.top_filter(top_filter)
.where_("req_type IS NOT NULL".to_string())
.out_fields(vec![
"req_type".to_string(),
"district".to_string(),
"address".to_string(),
])
.return_geometry(false)
.f("json".to_string())
.build()
.expect("Valid top features params");
tracing::debug!("Executing queryTopFeatures operation");
let result = service.query_top_features(layer_id, params).await?;
let feature_count = result.features().len();
tracing::info!(
total_features = feature_count,
expected_max = 5 * 10, "Top features query completed"
);
tracing::debug!(
exceeded_limit = result.exceeded_transfer_limit(),
"Query completion status"
);
anyhow::ensure!(
feature_count > 0,
"Top features query returned no results. Expected top 5 features per district. \
Service may not have data or district field may not exist."
);
tracing::debug!("Processing and displaying top features by district");
let mut by_district: std::collections::HashMap<String, Vec<_>> =
std::collections::HashMap::new();
for feature in result.features() {
let district_value = feature.attributes().get("district");
anyhow::ensure!(
district_value.is_some(),
"Feature missing 'district' field. \
Top features query requires this grouping field to exist."
);
if let Some(district) = district_value.and_then(|v| v.as_str()) {
by_district
.entry(district.to_string())
.or_default()
.push(feature);
}
}
let district_count = by_district.len();
tracing::info!(
districts_found = district_count,
"Grouping top features by district"
);
anyhow::ensure!(
district_count > 0,
"No districts found in results. district field may be null or invalid."
);
for (district, features) in by_district.iter().take(3) {
tracing::info!(
district = %district,
feature_count = features.len(),
"Top requests in district"
);
for (idx, feature) in features.iter().take(3).enumerate() {
let req_type = feature.attributes().get("req_type");
let address = feature.attributes().get("address");
tracing::debug!(
district = %district,
rank = idx + 1,
request_type = ?req_type,
address = ?address,
"Service request detail"
);
}
}
tracing::debug!("Top features demonstration complete");
Ok(())
}
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_pagination_strategies(
service: &FeatureServiceClient<'_>,
layer_id: LayerId,
) -> Result<()> {
tracing::info!("\n=== Example 3: Pagination Strategies ===");
tracing::info!("Efficiently paginate through large result sets");
let page_size = 100;
let max_pages = 3;
tracing::debug!(
page_size = page_size,
max_pages = max_pages,
"Starting manual pagination"
);
let mut total_fetched = 0;
let mut page_num = 0;
loop {
if page_num >= max_pages {
tracing::debug!(
pages_fetched = page_num,
"Reached maximum page limit for demonstration"
);
break;
}
let offset = page_num * page_size;
tracing::debug!(
page = page_num,
offset = offset,
limit = page_size,
"Fetching page"
);
let result = service
.query(layer_id)
.where_clause("1=1")
.out_fields(&["req_type", "address"])
.return_geometry(false)
.limit(page_size)
.offset(offset)
.execute()
.await?;
let feature_count = result.features().len();
total_fetched += feature_count;
tracing::info!(
page = page_num,
features_in_page = feature_count,
cumulative_total = total_fetched,
exceeded_limit = result.exceeded_transfer_limit(),
"Page retrieved"
);
if feature_count < page_size as usize || !*result.exceeded_transfer_limit() {
tracing::debug!(
feature_count = feature_count,
page_size = page_size,
exceeded_limit = result.exceeded_transfer_limit(),
"Reached end of available data"
);
break;
}
page_num += 1;
}
tracing::info!(
total_pages = page_num + 1,
total_features = total_fetched,
"Manual pagination complete"
);
anyhow::ensure!(
total_fetched > 0,
"Manual pagination fetched no features. Service may be empty or query failed."
);
tracing::debug!("Demonstrating automatic pagination with execute_all()");
let auto_result = service
.query(layer_id)
.where_clause("1=1")
.out_fields(&["req_type"])
.return_geometry(false)
.limit(page_size)
.execute_all()
.await?;
let auto_total = auto_result.features().len();
tracing::info!(
auto_pagination_total = auto_total,
manual_pagination_total = total_fetched,
"Automatic pagination completed for comparison"
);
tracing::debug!(
exceeded_limit = auto_result.exceeded_transfer_limit(),
"Automatic pagination status"
);
anyhow::ensure!(
auto_total > 0,
"Automatic pagination fetched no features. Service may be empty or query failed."
);
anyhow::ensure!(
auto_total >= total_fetched,
"Automatic pagination returned fewer features ({}) than manual ({}). This is unexpected.",
auto_total,
total_fetched
);
if auto_total > total_fetched {
tracing::info!(
additional_features = auto_total - total_fetched,
"Automatic pagination fetched more features (manual was limited to {} pages)",
max_pages
);
}
tracing::debug!("Pagination strategies demonstration complete");
Ok(())
}
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_feature_count(
service: &FeatureServiceClient<'_>,
layer_id: LayerId,
) -> Result<()> {
tracing::info!("\n=== Example 4: Feature Count Queries ===");
tracing::info!("Efficiently count features without transferring data");
tracing::debug!("Using query_feature_count for server-side counting");
tracing::debug!("Counting all features in layer");
let total_count = service.query_feature_count(layer_id, "1=1").await?;
tracing::info!(
total_features = total_count,
"Total features in layer (all records)"
);
anyhow::ensure!(
total_count > 0,
"Layer contains no features. Expected at least some records for counting demonstration."
);
tracing::debug!("Counting features with specific request type");
let graffiti_count = service
.query_feature_count(layer_id, "req_type LIKE 'Graffiti%'")
.await?;
tracing::info!(
graffiti_requests = graffiti_count,
"Features matching 'Graffiti' request type"
);
tracing::debug!("Counting features with complex criteria");
let complex_count = service
.query_feature_count(layer_id, "req_type IS NOT NULL AND district IS NOT NULL")
.await?;
tracing::info!(
features_with_type_and_district = complex_count,
"Features with both request type and district"
);
anyhow::ensure!(
complex_count <= total_count,
"Filtered count ({}) cannot exceed total count ({})",
complex_count,
total_count
);
anyhow::ensure!(
graffiti_count <= total_count,
"Graffiti count ({}) cannot exceed total count ({})",
graffiti_count,
total_count
);
tracing::info!(
"✅ Count queries verified: Graffiti={}, Complex={}, Total={}",
graffiti_count,
complex_count,
total_count
);
tracing::debug!("Feature count demonstration complete");
Ok(())
}
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_query_with_params(
service: &FeatureServiceClient<'_>,
layer_id: LayerId,
) -> Result<()> {
tracing::info!("\n=== Example 5: Advanced Query with Params ===");
tracing::info!("Use FeatureQueryParams for fine-grained query control");
tracing::debug!("Building FeatureQueryParams for selective field query");
let selective_params = FeatureQueryParams::builder()
.where_clause("req_type IS NOT NULL")
.out_fields(vec!["req_type".to_string(), "district".to_string()])
.return_geometry(false)
.result_record_count(10u32) .order_by_fields(vec!["req_type ASC".to_string()])
.build()
.expect("Valid query params");
tracing::debug!("Executing query_with_params with selective fields");
let selective_result = service
.query_with_params(layer_id, selective_params)
.await?;
let result_count = selective_result.features().len();
tracing::info!(
features_returned = result_count,
"Selective field query completed"
);
anyhow::ensure!(
result_count > 0,
"Expected results from selective field query. Service may have no data."
);
tracing::info!("Sample results with selected fields:");
for (idx, feature) in selective_result.features().iter().take(5).enumerate() {
let req_type = feature.attributes().get("req_type");
let district = feature.attributes().get("district");
tracing::info!(
item = idx + 1,
request_type = ?req_type,
district = ?district,
"Selective field result"
);
anyhow::ensure!(
req_type.is_some(),
"Selective query should return req_type field"
);
}
tracing::debug!("Building complex FeatureQueryParams with multiple filters");
let complex_params = FeatureQueryParams::builder()
.where_clause("district = 'Downtown' AND req_type IS NOT NULL")
.out_fields(vec![
"req_type".to_string(),
"address".to_string(),
"district".to_string(),
])
.return_geometry(false)
.result_record_count(10u32)
.order_by_fields(vec!["req_type ASC".to_string()])
.build()
.expect("Valid query params");
tracing::debug!("Executing complex query_with_params");
let complex_result = service.query_with_params(layer_id, complex_params).await?;
let result_count = complex_result.features().len();
tracing::info!(
downtown_requests = result_count,
exceeded_limit = complex_result.exceeded_transfer_limit(),
"Downtown district requests"
);
if result_count > 0 {
tracing::info!("Sample Downtown requests:");
for (idx, feature) in complex_result.features().iter().take(3).enumerate() {
let req_type = feature.attributes().get("req_type");
let address = feature.attributes().get("address");
tracing::info!(
item = idx + 1,
request_type = ?req_type,
address = ?address,
"Downtown request"
);
anyhow::ensure!(
req_type.is_some(),
"Result missing req_type field from out_fields"
);
}
tracing::info!(
"✅ query_with_params verified: {} results returned",
result_count
);
} else {
tracing::warn!(
"No results for Downtown district - this district may not exist in sample data. \
This is acceptable for demonstration purposes."
);
}
tracing::debug!("query_with_params demonstration complete");
Ok(())
}
#[allow(dead_code)]
#[instrument(skip(service), fields(layer_id = %layer_id))]
async fn demonstrate_related_records(
service: &FeatureServiceClient<'_>,
layer_id: LayerId,
) -> Result<()> {
tracing::info!("\n=== Example 4: Related Records Query ===");
tracing::info!("Find evidence records related to crime incidents");
tracing::debug!("Note: Requires service with relationship classes defined");
tracing::debug!("Fetching sample incident IDs");
let incidents = service
.query(layer_id)
.where_clause("1=1")
.ids_only(true)
.limit(5)
.execute()
.await?;
let object_ids: Vec<ObjectId> = incidents
.features()
.iter()
.filter_map(|f| {
f.attributes()
.get("objectid")
.and_then(|v| v.as_i64())
.and_then(|id| {
if id >= 0 {
Some(ObjectId::new(id as u32))
} else {
None
}
})
})
.collect();
tracing::debug!(
incident_count = object_ids.len(),
"Retrieved incident IDs for related records query"
);
anyhow::ensure!(
!object_ids.is_empty(),
"No object IDs found for related records query. Service may be empty or objectid field missing."
);
tracing::debug!("Building RelatedRecordsParams");
let params = RelatedRecordsParams::builder()
.object_ids(object_ids.clone())
.relationship_id(0u32) .out_fields(vec!["*".to_string()])
.return_geometry(false)
.build()
.expect("Valid related records params");
tracing::debug!(
object_id_count = object_ids.len(),
relationship_id = 0,
"Executing queryRelatedRecords operation"
);
let result = service.query_related_records(layer_id, params).await?;
let group_count = result.related_record_groups().len();
tracing::info!(groups = group_count, "Related records query completed");
for group in result.related_record_groups().iter().take(3) {
let related_count = group.related_records().len();
tracing::info!(
incident_id = ?group.object_id(),
related_records = related_count,
"Found related evidence"
);
for (idx, record) in group.related_records().iter().take(2).enumerate() {
tracing::debug!(
incident_id = ?group.object_id(),
evidence_num = idx + 1,
attributes = ?record.attributes(),
"Evidence record detail"
);
}
}
tracing::debug!("Related records demonstration complete");
Ok(())
}
#[allow(dead_code)]
#[instrument(skip(service))]
async fn demonstrate_domain_lookups(service: &FeatureServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 5: Domain Lookups ===");
tracing::info!("Query valid domain values for crime types");
tracing::debug!("Note: Requires service with domain/subtype definitions");
let layers = vec![LayerId::new(0)];
tracing::debug!(layer_count = layers.len(), "Querying domains for layers");
let result = service.query_domains(layers).await?;
let layer_count = result.layers().len();
tracing::info!(layers_with_domains = layer_count, "Domain query completed");
anyhow::ensure!(
layer_count > 0,
"Domain query returned no layers. Service may not support domains or query failed."
);
for layer_info in result.layers() {
let domain_count = layer_info.domains().len();
let subtype_count = layer_info.subtypes().as_ref().map(|s| s.len()).unwrap_or(0);
tracing::info!(
layer_id = ?layer_info.id(),
domains = domain_count,
subtypes = subtype_count,
"Layer domain information"
);
for (field_name, domain) in layer_info.domains().iter().take(3) {
tracing::debug!(
field = %field_name,
domain_name = %domain.name(),
domain_type = ?domain.domain_type(),
"Domain definition"
);
if let Some(coded_values) = domain.coded_values() {
let value_count = coded_values.len();
tracing::debug!(
field = %field_name,
coded_value_count = value_count,
"Coded values available"
);
for (idx, coded_value) in coded_values.iter().take(3).enumerate() {
tracing::debug!(
field = %field_name,
value_num = idx + 1,
code = ?coded_value.code(),
name = %coded_value.name(),
"Valid domain value"
);
}
}
}
}
tracing::debug!("Domain lookups demonstration complete");
Ok(())
}
fn print_best_practices() {
tracing::info!("\n💡 Advanced Query Best Practices:");
tracing::info!("");
tracing::info!("📊 Statistical Aggregations:");
tracing::info!(" - Use statistics queries instead of client-side aggregation");
tracing::info!(" - GROUP BY reduces data transfer and improves performance");
tracing::info!(" - HAVING filters aggregated results efficiently");
tracing::info!(" - Combine with ORDER BY to sort aggregated results");
tracing::info!("");
tracing::info!("🏆 Top Features:");
tracing::info!(" - Use queryTopFeatures for ranking within groups");
tracing::info!(" - Specify clear ORDER BY for deterministic results");
tracing::info!(" - Consider filtering with WHERE before grouping");
tracing::info!(" - Top queries reduce result size vs full query + sort");
tracing::info!("");
tracing::info!("📄 Pagination:");
tracing::info!(" - Manual pagination: Full control over page fetching");
tracing::info!(" - Automatic pagination: Convenient for smaller datasets");
tracing::info!(" - Use resultOffset/resultRecordCount for manual control");
tracing::info!(" - Monitor exceededTransferLimit to detect more data");
tracing::info!(" - Page size affects performance: 100-1000 is typical");
tracing::info!("");
tracing::info!("🔢 Feature Counting:");
tracing::info!(" - Use query_feature_count for server-side counting");
tracing::info!(" - Much faster than querying all features and counting");
tracing::info!(" - Reduces bandwidth - only count is transferred");
tracing::info!(" - Supports WHERE clauses for filtered counts");
tracing::info!(" - Perfect for pagination (get total before fetching pages)");
tracing::info!("");
tracing::info!("🔧 Advanced Parameters:");
tracing::info!(" - query_with_params: Lower-level control vs query builder");
tracing::info!(" - returnDistinctValues: Get unique field values efficiently");
tracing::info!(" - Use specific out_fields to reduce response size");
tracing::info!(" - Combine multiple parameters for complex queries");
tracing::info!(" - Distinct queries useful for populating dropdowns");
tracing::info!("");
tracing::info!("🔗 Relationships:");
tracing::info!(" - Query related records instead of joining tables");
tracing::info!(" - Results are grouped by source object ID");
tracing::info!(" - Filter related records with definitionExpression");
tracing::info!(" - Reduce network traffic with targeted out_fields");
tracing::info!("");
tracing::info!("🏷️ Domains:");
tracing::info!(" - Query domains once, cache for form validation");
tracing::info!(" - Use coded values for dropdown lists");
tracing::info!(" - Subtypes affect valid domain values per feature");
tracing::info!(" - Domain queries don't count against rate limits");
tracing::info!("");
tracing::info!("⚡ Performance:");
tracing::info!(" - Server-side aggregation >> client-side processing");
tracing::info!(" - Return only needed fields with out_fields");
tracing::info!(" - Skip geometry when not needed (return_geometry: false)");
tracing::info!(" - Use appropriate page sizes for your use case");
tracing::info!(" - Consider PBF format for large datasets");
}