From 4791a62531f89033e6469cc54c26ea9c7ef04891 Mon Sep 17 00:00:00 2001 From: dal Date: Mon, 5 May 2025 18:47:49 -0600 Subject: [PATCH] models created --- api/libs/semantic_layer/Cargo.toml | 2 + api/libs/semantic_layer/src/lib.rs | 1 + api/libs/semantic_layer/src/models.rs | 228 ++++++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 api/libs/semantic_layer/src/models.rs diff --git a/api/libs/semantic_layer/Cargo.toml b/api/libs/semantic_layer/Cargo.toml index 4e243b999..a0c315aba 100644 --- a/api/libs/semantic_layer/Cargo.toml +++ b/api/libs/semantic_layer/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +serde = { workspace = true } +serde_yaml = { workspace = true } # Dependencies will be inherited from the workspace diff --git a/api/libs/semantic_layer/src/lib.rs b/api/libs/semantic_layer/src/lib.rs index 914e79eb6..727a299a1 100644 --- a/api/libs/semantic_layer/src/lib.rs +++ b/api/libs/semantic_layer/src/lib.rs @@ -1 +1,2 @@ +pub mod models; // Placeholder for semantic_layer library code diff --git a/api/libs/semantic_layer/src/models.rs b/api/libs/semantic_layer/src/models.rs new file mode 100644 index 000000000..1f422f16e --- /dev/null +++ b/api/libs/semantic_layer/src/models.rs @@ -0,0 +1,228 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq)] +pub struct SemanticLayerSpec { + pub models: Vec, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Model { + pub name: String, + pub description: Option, + #[serde(default)] // Use default empty vec if missing + pub dimensions: Vec, + #[serde(default)] + pub measures: Vec, + #[serde(default)] + pub metrics: Vec, + #[serde(default)] + pub filters: Vec, + #[serde(default)] + pub entities: Vec, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Dimension { + pub name: String, + pub description: Option, + #[serde(rename = "type")] // Rename field 'type_' to avoid Rust keyword collision + pub type_: Option, // 'type' is optional according to spec + #[serde(default)] // Default to false if 'searchable' is missing + pub searchable: bool, + pub options: Option>, // Default to None if 'options' is missing +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Measure { + pub name: String, + pub description: Option, + #[serde(rename = "type")] + pub type_: Option, // 'type' is optional according to spec +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Metric { + pub name: String, + pub expr: String, + pub description: Option, + pub args: Option>, // 'args' is optional +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Filter { + pub name: String, + pub expr: String, + pub description: Option, + pub args: Option>, // 'args' is optional +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Argument { + pub name: String, + #[serde(rename = "type")] + pub type_: String, // 'type' is required for arguments + pub description: Option, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct Entity { + pub name: String, + pub primary_key: String, + pub foreign_key: String, + #[serde(rename = "type")] + pub type_: Option, // 'type' is optional according to spec + pub cardinality: Option, // 'cardinality' is optional + pub description: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_yaml; + + #[test] + fn test_deserialize_model_file() { + let yaml_content = r#" +models: + - name: culture + description: Core model for cultural groups + dimensions: + - name: cultureid + description: Unique identifier for the culture + - name: name + description: Culture name + options: ["Western", "Eastern"] + measures: + - name: revenue + description: Revenue generated by the culture + filters: + - name: active_subscribed_customer + expr: logins.login_count > {threshold} AND subscriptions.subscription_status = 'active' + args: + - name: threshold + type: integer + description: Minimum number of logins + description: Customers with logins above threshold and active subscription + metrics: + - name: popular_product_revenue + expr: SUM(revenue) WHERE culture_products.product_count > 5 + description: Revenue from cultures with popular products + entities: + - name: logins + primary_key: cultureid + foreign_key: cultureid + type: LEFT + cardinality: one-to-many + description: Links to login activity + - name: subscriptions + primary_key: cultureid + foreign_key: cultureid + cardinality: one-to-one + description: Links to subscription data (no type, LLM decides) + - name: culture_products + primary_key: cultureid + foreign_key: cultureid + cardinality: many-to-many + description: Links to product associations (many-to-many via junction) + - name: logins + description: Tracks user logins by culture + dimensions: + - name: cultureid + description: Foreign key to culture + measures: + - name: login_count + description: Number of logins + entities: + - name: culture + primary_key: cultureid + foreign_key: cultureid + cardinality: many-to-one + - name: subscriptions + description: Subscription status for cultures + dimensions: + - name: cultureid + description: Foreign key to culture + - name: subscription_status + description: Current subscription status + options: ["active", "inactive"] + entities: + - name: culture + primary_key: cultureid + foreign_key: cultureid + cardinality: one-to-one + - name: culture_products + description: Junction table linking cultures to products + dimensions: + - name: cultureid + description: Foreign key to culture + - name: productid + description: Foreign key to products + measures: + - name: product_count + description: Number of products in this association + entities: + - name: culture + primary_key: cultureid + foreign_key: cultureid + cardinality: many-to-many + - name: products + primary_key: productid + foreign_key: productid + cardinality: many-to-many + "#; + + let spec: Result = serde_yaml::from_str(yaml_content); + assert!(spec.is_ok(), "Failed to deserialize YAML: {:?}", spec.err()); + let spec = spec.unwrap(); + + assert_eq!(spec.models.len(), 4); + + // Basic checks on the first model ('culture') + let culture_model = &spec.models[0]; + assert_eq!(culture_model.name, "culture"); + assert_eq!(culture_model.description, Some("Core model for cultural groups".to_string())); + assert_eq!(culture_model.dimensions.len(), 2); + assert_eq!(culture_model.measures.len(), 1); + assert_eq!(culture_model.filters.len(), 1); + assert_eq!(culture_model.metrics.len(), 1); + assert_eq!(culture_model.entities.len(), 3); + + // Check dimension 'name' options + let name_dim = &culture_model.dimensions[1]; + assert_eq!(name_dim.name, "name"); + assert_eq!(name_dim.options, Some(vec!["Western".to_string(), "Eastern".to_string()])); + assert!(!name_dim.searchable); // Default false + + // Check filter 'active_subscribed_customer' args + let filter = &culture_model.filters[0]; + assert_eq!(filter.name, "active_subscribed_customer"); + assert!(filter.args.is_some()); + let filter_args = filter.args.as_ref().unwrap(); + assert_eq!(filter_args.len(), 1); + assert_eq!(filter_args[0].name, "threshold"); + assert_eq!(filter_args[0].type_, "integer"); + + + // Check entity 'logins' type and cardinality + let logins_entity = &culture_model.entities[0]; + assert_eq!(logins_entity.name, "logins"); + assert_eq!(logins_entity.type_, Some("LEFT".to_string())); + assert_eq!(logins_entity.cardinality, Some("one-to-many".to_string())); + + // Check entity 'subscriptions' type and cardinality (optional) + let subs_entity = &culture_model.entities[1]; + assert_eq!(subs_entity.name, "subscriptions"); + assert_eq!(subs_entity.type_, None); + assert_eq!(subs_entity.cardinality, Some("one-to-one".to_string())); + + // Check second model ('logins') + let logins_model = &spec.models[1]; + assert_eq!(logins_model.name, "logins"); + assert_eq!(logins_model.dimensions.len(), 1); + assert_eq!(logins_model.measures.len(), 1); + assert_eq!(logins_model.filters.len(), 0); // Default empty vec + assert_eq!(logins_model.metrics.len(), 0); // Default empty vec + assert_eq!(logins_model.entities.len(), 1); + + } +} \ No newline at end of file