1use serde::{Deserialize, Serialize};
13use serde_json::{Value, json};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct DatasetDescriptor {
18 pub name: String,
21 pub kind: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub schema: Option<Value>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub estimated_rows: Option<u64>,
36 pub config_patch: Value,
42}
43
44impl DatasetDescriptor {
45 pub fn new(name: impl Into<String>, kind: impl Into<String>, config_patch: Value) -> Self {
47 Self {
48 name: name.into(),
49 kind: kind.into(),
50 schema: None,
51 estimated_rows: None,
52 config_patch,
53 }
54 }
55
56 pub fn with_schema(mut self, schema: Value) -> Self {
58 self.schema = Some(schema);
59 self
60 }
61
62 pub fn with_estimated_rows(mut self, rows: u64) -> Self {
64 self.estimated_rows = Some(rows);
65 self
66 }
67}
68
69pub fn sql_type_to_json_schema(data_type: &str) -> Value {
79 let t = data_type.to_ascii_lowercase();
80 let ty = if t.contains("bool") || t == "bit" {
81 "boolean"
82 } else if t.contains("json") || t.contains("variant") || t == "object" || t.contains("struct") {
83 "object"
85 } else if t.contains("array") || t.starts_with('_') {
86 "array"
89 } else if (t.contains("int") && !t.contains("interval") && !t.contains("point"))
90 || t == "serial"
91 || t == "bigserial"
92 || t == "smallserial"
93 {
94 "integer"
97 } else if t.contains("double")
98 || t.contains("float")
99 || t.contains("real")
100 || t.contains("decimal")
101 || t.contains("numeric")
102 || t.contains("money")
103 || t == "number"
104 {
105 "number"
106 } else {
107 "string"
110 };
111 json!({ "type": ty })
112}
113
114pub fn nullable_type(fragment: Value) -> Value {
117 match fragment.get("type") {
118 Some(Value::String(t)) => json!({ "type": [t, "null"] }),
119 _ => fragment,
120 }
121}
122
123pub fn columns_to_schema(columns: impl IntoIterator<Item = (String, Value)>) -> Value {
127 let mut properties = serde_json::Map::new();
128 for (name, fragment) in columns {
129 properties.insert(name, fragment);
130 }
131 json!({ "type": "object", "properties": Value::Object(properties) })
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn descriptor_builder_round_trips() {
140 let d = DatasetDescriptor::new("public.orders", "table", json!({"query": "SELECT 1"}))
141 .with_schema(json!({"type": "object", "properties": {}}))
142 .with_estimated_rows(42);
143 assert_eq!(d.name, "public.orders");
144 assert_eq!(d.kind, "table");
145 assert_eq!(d.estimated_rows, Some(42));
146 let v = serde_json::to_value(&d).unwrap();
147 let back: DatasetDescriptor = serde_json::from_value(v).unwrap();
148 assert_eq!(back, d);
149 }
150
151 #[test]
152 fn descriptor_omits_absent_optionals_in_json() {
153 let d = DatasetDescriptor::new("t", "table", json!({}));
154 let v = serde_json::to_value(&d).unwrap();
155 assert!(v.get("schema").is_none());
156 assert!(v.get("estimated_rows").is_none());
157 }
158
159 #[test]
160 fn sql_types_map_to_json_types() {
161 for (sql, want) in [
162 ("integer", "integer"),
163 ("BIGINT", "integer"),
164 ("smallint", "integer"),
165 ("tinyint(1)", "integer"),
166 ("serial", "integer"),
167 ("double precision", "number"),
168 ("NUMERIC(10,2)", "number"),
169 ("decimal", "number"),
170 ("float8", "number"),
171 ("money", "number"),
172 ("boolean", "boolean"),
173 ("BOOL", "boolean"),
174 ("bit", "boolean"),
175 ("json", "object"),
176 ("JSONB", "object"),
177 ("VARIANT", "object"),
178 ("STRUCT<a INT>", "object"),
179 ("ARRAY", "array"),
180 ("_int4", "array"),
181 ("text", "string"),
182 ("character varying", "string"),
183 ("uuid", "string"),
184 ("timestamp with time zone", "string"),
185 ("date", "string"),
186 ("bytea", "string"),
187 ("some_exotic_type", "string"),
188 ] {
189 assert_eq!(
190 sql_type_to_json_schema(sql),
191 json!({ "type": want }),
192 "for SQL type {sql:?}"
193 );
194 }
195 }
196
197 #[test]
198 fn nullable_wraps_scalar_type() {
199 assert_eq!(
200 nullable_type(json!({"type": "integer"})),
201 json!({"type": ["integer", "null"]})
202 );
203 let complex = json!({"type": ["string", "null"]});
205 assert_eq!(nullable_type(complex.clone()), complex);
206 }
207
208 #[test]
209 fn columns_to_schema_shapes_like_infer_schema() {
210 let schema = columns_to_schema(vec![
211 ("id".to_string(), json!({"type": "integer"})),
212 ("name".to_string(), json!({"type": ["string", "null"]})),
213 ]);
214 assert_eq!(schema["type"], "object");
215 assert_eq!(schema["properties"]["id"]["type"], "integer");
216 assert_eq!(schema["properties"]["name"]["type"][1], "null");
217 }
218}