Skip to main content

uqa_sql/semantics/
scalar_projection.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Argument validation and short-circuit semantics for score and highlight projections.
8
9use crate::{expr::RowLookup, semantics::expect_column_name, SQLError, ScalarExpr};
10use uqa_core::Value;
11
12pub fn validate_score_projection_args(
13    name: &str,
14    args: &[ScalarExpr],
15    evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
16) -> Result<(), SQLError> {
17    if !(1..=2).contains(&args.len()) {
18        return Err(SQLError::BadArity {
19            name: name.into(),
20            expected: "1..=2".into(),
21            actual: args.len(),
22        });
23    }
24    let query_idx = args.len() - 1;
25    if args.len() == 2 {
26        let _ = expect_column_name(&args[0], &format!("{name}.field"))?;
27    }
28    match evaluate(&args[query_idx])? {
29        Value::Str(_) => Ok(()),
30        other => Err(SQLError::TypeMismatch(format!(
31            "{name}.query must be a string, got {other:?}"
32        ))),
33    }
34}
35
36pub struct HighlightArguments {
37    pub text: String,
38    pub query: String,
39    pub start_tag: String,
40    pub end_tag: String,
41    pub max_fragments: usize,
42    pub fragment_size: usize,
43    pub analyzer: Option<String>,
44}
45
46pub enum HighlightInput {
47    Value(Value),
48    Arguments(HighlightArguments),
49}
50
51/// Evaluate fields and options in order, preserving NULL short circuits before later arguments.
52#[expect(
53    clippy::too_many_lines,
54    reason = "preserves ordered field and option evaluation"
55)]
56pub fn highlight_arguments(
57    row: &dyn RowLookup,
58    args: &[ScalarExpr],
59    evaluate: &mut dyn FnMut(&ScalarExpr) -> Result<Value, SQLError>,
60) -> Result<HighlightInput, SQLError> {
61    if args.len() < 2 || args.len() > 7 {
62        return Err(SQLError::BadArity {
63            name: "uqa_highlight".into(),
64            expected: "2..=7".into(),
65            actual: args.len(),
66        });
67    }
68    let text = match &args[0] {
69        ScalarExpr::Column(c) => match row.column(c) {
70            Some(Value::Str(s)) => s.clone(),
71            Some(Value::Null) => return Ok(HighlightInput::Value(Value::Null)),
72            Some(other) => format!("{other:?}"),
73            None => return Ok(HighlightInput::Value(Value::Null)),
74        },
75        ScalarExpr::QualifiedColumn { qualifier, column } => {
76            match row.qualified_column(qualifier, column) {
77                Some(Value::Str(s)) => s.clone(),
78                Some(Value::Null) => return Ok(HighlightInput::Value(Value::Null)),
79                Some(other) => format!("{other:?}"),
80                None => return Ok(HighlightInput::Value(Value::Null)),
81            }
82        }
83        other => match evaluate(other)? {
84            Value::Str(s) => s,
85            Value::Null => return Ok(HighlightInput::Value(Value::Null)),
86            v => format!("{v:?}"),
87        },
88    };
89    let query_str = match evaluate(&args[1])? {
90        Value::Str(s) => s,
91        Value::Null => return Ok(HighlightInput::Value(Value::Str(text))),
92        other => {
93            return Err(SQLError::TypeMismatch(format!(
94                "uqa_highlight query must be string, got {other:?}"
95            )));
96        }
97    };
98    let start_tag = match args.get(2) {
99        Some(e) => match evaluate(e)? {
100            Value::Str(s) => s,
101            Value::Null => "<b>".into(),
102            other => {
103                return Err(SQLError::TypeMismatch(format!(
104                    "uqa_highlight start_tag must be string, got {other:?}"
105                )));
106            }
107        },
108        None => "<b>".into(),
109    };
110    let end_tag = match args.get(3) {
111        Some(e) => match evaluate(e)? {
112            Value::Str(s) => s,
113            Value::Null => "</b>".into(),
114            other => {
115                return Err(SQLError::TypeMismatch(format!(
116                    "uqa_highlight end_tag must be string, got {other:?}"
117                )));
118            }
119        },
120        None => "</b>".into(),
121    };
122    let max_fragments = match args.get(4) {
123        Some(e) => match evaluate(e)? {
124            Value::Int(n) if n >= 0 => usize::try_from(n).map_err(|_| {
125                SQLError::TypeMismatch(format!(
126                    "uqa_highlight max_fragments {n} exceeds the platform usize range"
127                ))
128            })?,
129            Value::Null => 0,
130            other => {
131                return Err(SQLError::TypeMismatch(format!(
132                    "uqa_highlight max_fragments must be non-negative integer, got {other:?}"
133                )));
134            }
135        },
136        None => 0,
137    };
138    let fragment_size = match args.get(5) {
139        Some(e) => match evaluate(e)? {
140            Value::Int(n) if n > 0 => usize::try_from(n).map_err(|_| {
141                SQLError::TypeMismatch(format!(
142                    "uqa_highlight fragment_size {n} exceeds the platform usize range"
143                ))
144            })?,
145            Value::Null => 150,
146            other => {
147                return Err(SQLError::TypeMismatch(format!(
148                    "uqa_highlight fragment_size must be positive integer, got {other:?}"
149                )));
150            }
151        },
152        None => 150,
153    };
154    let analyzer = match args.get(6) {
155        Some(expression) => match evaluate(expression)? {
156            Value::Str(name) => Some(name),
157            Value::Null => None,
158            other => {
159                return Err(SQLError::TypeMismatch(format!(
160                    "uqa_highlight analyzer must be string, got {other:?}"
161                )))
162            }
163        },
164        None => None,
165    };
166    Ok(HighlightInput::Arguments(HighlightArguments {
167        text,
168        query: query_str,
169        start_tag,
170        end_tag,
171        max_fragments,
172        fragment_size,
173        analyzer,
174    }))
175}