1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
6pub struct QueryPlan {
7 pub sources: Vec<Source>,
8 pub destination_schema: Vec<DestinationField>,
9 pub primary_keys: PrimaryKeySpec,
10 pub mappings: Vec<Mapping>,
11}
12
13#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
14pub struct Source {
15 pub id: String,
16 pub path: PathBuf,
17 pub format: String,
18}
19
20#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
21pub struct DestinationField {
22 pub name: String,
23 pub data_type: String,
24}
25
26#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
27#[serde(rename_all = "camelCase")]
28pub enum PrimaryKeyLogic {
29 Or,
30 And
31}
32
33#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
34pub struct PrimaryKeySpec {
35 pub logic: PrimaryKeyLogic,
36 pub keys: Vec<String>,
37}
38
39#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
40pub struct Mapping {
41 pub destination_field: String,
42 pub policy: MergePolicy,
43 pub source_fields: Vec<SourceFieldSpec>,
44}
45
46#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
47pub struct SourceFieldSpec {
48 pub id: String,
49 pub source_file_id: String,
50 pub column_name: String,
51}
52
53#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
54#[serde(tag = "type")]
55#[serde(rename_all = "camelCase")]
56pub enum MergePolicy {
57 FirstMatch { priority: Vec<String> },
58 Sum,
59 Count,
60 Average,
61 Min,
62 Max,
63}
64
65impl QueryPlan {
66 pub fn from_json(json: &str) -> anyhow::Result<Self> {
67 Ok(serde_json::from_str(json)?)
68 }
69
70 pub fn to_json(&self) -> anyhow::Result<String> {
71 Ok(serde_json::to_string_pretty(self)?)
72 }
73
74 pub fn json_schema() -> anyhow::Result<String> {
76 let schema = schemars::schema_for!(QueryPlan);
77 Ok(serde_json::to_string_pretty(&schema)?)
78 }
79
80 pub fn validate(&self) -> anyhow::Result<()> {
81 if self.sources.is_empty() {
82 return Err(anyhow::anyhow!("QueryPlan must have at least one data source"));
83 }
84
85 if self.mappings.is_empty() {
86 return Err(anyhow::anyhow!("QueryPlan must have at least one field mapping"));
87 }
88
89 if self.primary_keys.keys.is_empty() {
90 return Err(anyhow::anyhow!("QueryPlan must have at least one primary key"));
91 }
92
93 let source_ids: std::collections::HashSet<_> =
95 self.sources.iter().map(|s| &s.id).collect();
96
97 for mapping in &self.mappings {
98 for source_field in &mapping.source_fields {
99 if !source_ids.contains(&source_field.source_file_id) {
100 return Err(anyhow::anyhow!(
101 "Mapping references unknown source: {}",
102 source_field.source_file_id
103 ));
104 }
105 }
106 }
107
108 let dest_field_names: std::collections::HashSet<_> =
110 self.destination_schema.iter().map(|f| &f.name).collect();
111
112 for key in &self.primary_keys.keys {
113 if !dest_field_names.contains(key) {
114 return Err(anyhow::anyhow!(
115 "Primary key references unknown destination field: {}",
116 key
117 ));
118 }
119 }
120
121 Ok(())
122 }
123}