LCOV - code coverage report
Current view: top level - dashql-core/src - sql_literal.rs Coverage Total Hit
Test: DashQL d805a4b Lines: 98.5 % 271 267
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : //! SQL literals in the Hyper dialect, printed for the dialect of a connection.
       2              : //!
       3              : //! The values of parameters (`@param :min = 10`, see docs/design/session_inputs.md) are literals written in the Hyper
       4              : //! dialect, whatever the connection of a script speaks: a number, a string, `true`, `false` or `null`, or a typed
       5              : //! literal, e.g. `date '2024-01-01'`, `timestamptz '2024-01-01 10:00+02'` or `interval '3' day`.
       6              : //!
       7              : //! A literal is the expression of `select <literal>`, read with the parser of DashQL. Its type comes from the reader of
       8              : //! types of the analyzer (`analyzer::sql_types`), the type of `timestamp '...'` is the type `timestamp`. The formatter
       9              : //! prints it, in the Hyper dialect as the text of a tag and in the dialect of a connection where the planner binds it.
      10              : //! A literal that a dialect can't express is an error, it is never sent as written.
      11              : //!
      12              : //! An interval is a number and one unit, `interval '3' day`, which Hyper and Trino read alike. Interval text with
      13              : //! several units or without one, `interval '1 day 2 hours'` or `interval '3 days'`, is no literal: Trino has no form of
      14              : //! it, and DashQL doesn't read the text of intervals.
      15              : 
      16              : use crate::analyzer::sql_types::{generic_kind, interval_keyword, read_sql_type};
      17              : use crate::analyzer::clfn_calls::number;
      18              : use crate::analyzer::{SqlType, SqlTypeKind};
      19              : use crate::formatter::Formatter;
      20              : use crate::utils::{quote_string_literal, unquote_string_literal};
      21              : use dashql_buffers::formatting::{FormattingConfigT, FormattingDialect, FormattingMode, KeywordCase};
      22              : use dashql_buffers::parser::{AttributeKey as K, ExpressionOperator, IntervalType as IntervalTypeEnum, Node, NodeType};
      23              : use dashql_parser::attrs;
      24              : use dashql_parser::parser::{parse, ParsedScript};
      25              : use dashql_parser::scanner::scan;
      26              : use serde_json::Value;
      27              : use std::fmt;
      28              : use std::sync::Arc;
      29              : 
      30              : /// The prefix that makes a literal a statement
      31              : const SELECT: &str = "select ";
      32              : 
      33              : /// A literal, the expression of `select <literal>`
      34              : #[derive(Clone)]
      35              : pub struct SqlLiteral {
      36              :     /// The parsed `select <literal>`
      37              :     parsed: Arc<ParsedScript>,
      38              :     /// The node of the literal
      39              :     node: u32,
      40              :     /// The literal in the Hyper dialect, as the formatter prints it
      41              :     hyper: String,
      42              :     /// The type, None for null
      43              :     sql_type: Option<SqlType>,
      44              :     /// The value as a control shows it
      45              :     value: String,
      46              : }
      47              : 
      48              : impl PartialEq for SqlLiteral {
      49            2 :     fn eq(&self, other: &Self) -> bool {
      50            2 :         self.hyper == other.hyper
      51            2 :     }
      52              : }
      53              : 
      54              : impl Eq for SqlLiteral {}
      55              : 
      56              : impl fmt::Debug for SqlLiteral {
      57            1 :     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
      58            1 :         write!(f, "SqlLiteral({})", self.hyper)
      59            1 :     }
      60              : }
      61              : 
      62              : /// The content of the string of a typed literal, e.g. `2024-01-01` of `date '2024-01-01'`
      63          113 : fn string_content(parsed: &ParsedScript, node: Option<&Node>) -> Result<String, String> {
      64          113 :     let node = node.ok_or("a typed literal needs a string")?;
      65          113 :     let text = parsed.node_text(node);
      66          113 :     match node.node_type() {
      67          112 :         NodeType::LITERAL_STRING => unquote_string_literal(text).ok_or_else(|| format!("{text} is no plain string")),
      68            1 :         _ => Err(format!("{text} is no string")),
      69              :     }
      70          113 : }
      71              : 
      72              : /// The type and the value of a literal node, an error if the node is no literal
      73          246 : fn read_literal(parsed: &ParsedScript, node: &Node) -> Result<(Option<SqlType>, String), String> {
      74          246 :     let text = parsed.node_text(node);
      75          246 :     let scalar = |kind| Some(SqlType::scalar(kind));
      76          246 :     match node.node_type() {
      77           76 :         NodeType::LITERAL_INTEGER => Ok((scalar(SqlTypeKind::Int8), text.to_string())),
      78            2 :         NodeType::LITERAL_FLOAT => Ok((scalar(SqlTypeKind::Numeric { precision: None, scale: None }), text.to_string())),
      79            2 :         NodeType::LITERAL_NULL => Ok((None, String::new())),
      80           16 :         NodeType::BOOL => Ok((scalar(SqlTypeKind::Bool), (node.children_begin_or_value() != 0).to_string())),
      81           47 :         NodeType::LITERAL_STRING => Ok((scalar(SqlTypeKind::Text), string_content(parsed, Some(node))?)),
      82              :         // -2.5
      83              :         NodeType::OBJECT_SQL_NARY_EXPRESSION => {
      84           14 :             let [op, args] = attrs!(parsed.children(node), [K::SQL_EXPRESSION_OPERATOR, K::SQL_EXPRESSION_ARGS]);
      85           14 :             let negate = op.is_some_and(|o| ExpressionOperator(o.children_begin_or_value() as _) == ExpressionOperator::NEGATE);
      86           14 :             match (negate, args.map(|a| parsed.children(a)).unwrap_or(&[])) {
      87           11 :                 (true, [arg]) if matches!(arg.node_type(), NodeType::LITERAL_INTEGER | NodeType::LITERAL_FLOAT) => {
      88            9 :                     let (sql_type, value) = read_literal(parsed, arg)?;
      89            9 :                     Ok((sql_type, format!("-{value}")))
      90              :                 }
      91            5 :                 _ => Err(format!("{text} is no literal")),
      92              :             }
      93              :         }
      94              :         // timestamp '...', numeric(10,2) '1.5'
      95              :         NodeType::OBJECT_SQL_CONST_TYPE_CAST => {
      96            6 :             let [ty, value] = attrs!(parsed.children(node), [K::SQL_CONST_CAST_TYPE, K::SQL_CONST_CAST_VALUE]);
      97            6 :             let sql_type = ty.and_then(|t| read_sql_type(parsed, t)).ok_or_else(|| format!("{text} has no type"))?;
      98            6 :             Ok((Some(sql_type), string_content(parsed, value)?))
      99              :         }
     100              :         // date '...', timestamptz '...', json '...'
     101              :         NodeType::OBJECT_SQL_CONST_FUNCTION_CAST => {
     102           28 :             let [args, name, value] =
     103           28 :                 attrs!(parsed.children(node), [K::SQL_CONST_CAST_FUNC_ARGS_LIST, K::SQL_CONST_CAST_FUNC_NAME, K::SQL_CONST_CAST_VALUE]);
     104           28 :             if args.is_some() {
     105            1 :                 return Err(format!("{text} is no literal"));
     106           27 :             }
     107           27 :             let name = name.map(|n| parsed.node_text(n).to_ascii_lowercase()).unwrap_or_default();
     108           27 :             let kind = generic_kind(&name).unwrap_or(SqlTypeKind::Generic { name: name.into(), modifiers: Vec::new() });
     109           27 :             Ok((scalar(kind), string_content(parsed, value)?))
     110              :         }
     111              :         // interval '3' day
     112              :         NodeType::OBJECT_SQL_CONST_INTERVAL_CAST => {
     113           42 :             let [interval, value] = attrs!(parsed.children(node), [K::SQL_CONST_CAST_INTERVAL, K::SQL_CONST_CAST_VALUE]);
     114           42 :             let invalid = || format!("{text} is no interval literal, an interval is a number and a unit, e.g. interval '3' day");
     115           35 :             let [fields, precision] = match interval {
     116           35 :                 Some(i) if i.node_type() == NodeType::OBJECT_SQL_INTERVAL_TYPE => {
     117           34 :                     attrs!(parsed.children(i), [K::SQL_INTERVAL_TYPE, K::SQL_INTERVAL_PRECISION])
     118              :                 }
     119            8 :                 _ => return Err(invalid()),
     120              :             };
     121           34 :             let unit = fields.map(|f| IntervalTypeEnum(f.children_begin_or_value() as _));
     122           34 :             let Some(unit) = unit.filter(|u| precision.is_none() && interval_unit(*u).is_some()) else { return Err(invalid()) };
     123           33 :             let amount = string_content(parsed, value).map_err(|_| invalid())?;
     124           32 :             if !is_interval_amount(&amount, unit) {
     125            4 :                 return Err(invalid());
     126           28 :             }
     127           28 :             let sql_type = SqlType::scalar(SqlTypeKind::Interval { fields: Some(unit), precision: None });
     128              :             // The control shows the amount with the unit, `3 day`
     129           28 :             Ok((Some(sql_type), format!("{amount} {}", interval_unit(unit).unwrap_or_default())))
     130              :         }
     131           13 :         _ => Err(format!("{text} is no literal")),
     132              :     }
     133          246 : }
     134              : 
     135              : /// The units of an interval literal
     136              : const INTERVAL_UNITS: [IntervalTypeEnum; 6] =
     137              :     [IntervalTypeEnum::YEAR, IntervalTypeEnum::MONTH, IntervalTypeEnum::DAY, IntervalTypeEnum::HOUR, IntervalTypeEnum::MINUTE, IntervalTypeEnum::SECOND];
     138              : 
     139              : /// The keyword of the unit of an interval literal, None for ranges like `day to second`
     140           62 : fn interval_unit(unit: IntervalTypeEnum) -> Option<&'static str> {
     141           62 :     INTERVAL_UNITS.contains(&unit).then(|| interval_keyword(unit)).flatten()
     142           62 : }
     143              : 
     144              : /// Is the text the amount of an interval literal of a unit? An integer, seconds may have a fraction, `1.5`.
     145           32 : fn is_interval_amount(amount: &str, unit: IntervalTypeEnum) -> bool {
     146           37 :     let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
     147           32 :     let unsigned = amount.strip_prefix('-').unwrap_or(amount);
     148           32 :     match unsigned.split_once('.') {
     149           28 :         None => digits(unsigned),
     150            4 :         Some((whole, fraction)) => unit == IntervalTypeEnum::SECOND && digits(whole) && digits(fraction),
     151              :     }
     152           32 : }
     153              : 
     154              : /// The config of the formatter of a literal, one line in lower case
     155          367 : fn formatting_config(dialect: FormattingDialect) -> FormattingConfigT {
     156          367 :     let mut config = FormattingConfigT::default();
     157          367 :     config.dialect = dialect;
     158          367 :     config.mode = FormattingMode::INLINE;
     159          367 :     config.max_width = u32::MAX;
     160          367 :     config.keyword_case = KeywordCase::LOWER;
     161          367 :     config
     162          367 : }
     163              : 
     164              : impl SqlLiteral {
     165              :     /// Read a literal in the Hyper dialect
     166          244 :     pub fn parse(text: &str) -> Result<SqlLiteral, String> {
     167          244 :         let text = text.trim();
     168          244 :         let hint = "a literal is a number, a string, true, false, null, or a typed literal like date '2024-01-01' or interval '3' day";
     169          244 :         let invalid = || format!("{text} is no literal, {hint}");
     170          244 :         if text.is_empty() {
     171            1 :             return Err(format!("the literal is empty, {hint}"));
     172          243 :         }
     173          243 :         let parsed = Arc::new(parse(Arc::new(scan(&format!("{SELECT}{text}"), 0, 0))));
     174          243 :         if !parsed.errors.is_empty() || parsed.statements.len() != 1 {
     175            4 :             return Err(invalid());
     176          239 :         }
     177              :         // The value of the one result target of the select, which is the whole text, `1 from t` is no literal
     178         1255 :         let mut values = parsed.nodes.iter().enumerate().filter(|(_, n)| n.attribute_key() == K::SQL_RESULT_TARGET_VALUE);
     179          239 :         let (Some((node, value)), None) = (values.next(), values.next()) else { return Err(invalid()) };
     180          239 :         if parsed.node_text(value) != text {
     181            2 :             return Err(invalid());
     182          237 :         }
     183          237 :         let (sql_type, display) = read_literal(&parsed, value).map_err(|e| format!("{e}, {hint}"))?;
     184          204 :         let mut formatter = Formatter::new(&parsed);
     185          204 :         let hyper = formatter.format_node_at(node, &formatting_config(FormattingDialect::HYPER));
     186          204 :         if !formatter.is_fully_formatted() {
     187            0 :             return Err(invalid());
     188          204 :         }
     189          204 :         Ok(SqlLiteral { parsed, node: node as u32, hyper, sql_type, value: display })
     190          244 :     }
     191              : 
     192              :     /// The literal of the value of a control with a type. A number type reads a number, a boolean `true` or `false`,
     193              :     /// a date, a time and a timestamp read the text of their literal, an interval reads a number and a unit, `3 day`,
     194              :     /// and any other type a string.
     195              :     /// Without a type the value is a literal if it is one, e.g. `10`, `true` or `date '2024-01-01'`, and a string
     196              :     /// otherwise.
     197           30 :     pub fn from_value(value: &str, ty: Option<&SqlType>) -> Result<SqlLiteral, String> {
     198           30 :         let value = value.trim();
     199           30 :         let string = || SqlLiteral::parse(&quote_string_literal(value));
     200           30 :         let Some(ty) = ty.filter(|t| t.array_dimensions == 0) else {
     201            7 :             return SqlLiteral::parse(value).or_else(|_| string());
     202              :         };
     203           23 :         let typed = |keyword: &str| SqlLiteral::parse(&format!("{keyword} {}", quote_string_literal(value)));
     204           23 :         let is = |literal: &SqlLiteral, check: fn(&SqlType) -> bool| literal.sql_type.as_ref().is_some_and(check);
     205           20 :         match &ty.kind {
     206           23 :             _ if ty.is_numeric() => {
     207            3 :                 let literal = SqlLiteral::parse(value).ok().filter(|l| is(l, SqlType::is_numeric));
     208            3 :                 let literal = literal.ok_or_else(|| format!("{ty} needs a number, not \"{value}\""))?;
     209            2 :                 if ty.is_integer() && !is(&literal, SqlType::is_integer) {
     210            1 :                     return Err(format!("{ty} needs an integer, not \"{value}\""));
     211            1 :                 }
     212            1 :                 Ok(literal)
     213              :             }
     214            3 :             SqlTypeKind::Bool => SqlLiteral::parse(value)
     215            3 :                 .ok()
     216            3 :                 .filter(|l| is(l, SqlType::is_bool))
     217            3 :                 .ok_or_else(|| format!("{ty} needs true or false, not \"{value}\"")),
     218            1 :             SqlTypeKind::Date => typed("date"),
     219            2 :             SqlTypeKind::Time { with_time_zone, .. } => typed(if *with_time_zone { "timetz" } else { "time" }),
     220            2 :             SqlTypeKind::Timestamp { with_time_zone, .. } => typed(if *with_time_zone { "timestamptz" } else { "timestamp" }),
     221              :             SqlTypeKind::Interval { .. } => {
     222            9 :                 let invalid = || format!("{ty} needs a number and a unit, e.g. 3 day, not \"{value}\"");
     223            9 :                 let mut words = value.split_whitespace();
     224            9 :                 let (Some(amount), Some(unit), None) = (words.next(), words.next(), words.next()) else { return Err(invalid()) };
     225            5 :                 let unit = unit.to_ascii_lowercase();
     226           22 :                 let Some(keyword) = INTERVAL_UNITS.into_iter().filter_map(interval_keyword).find(|keyword| *keyword == unit) else { return Err(invalid()) };
     227            4 :                 SqlLiteral::parse(&format!("interval {} {keyword}", quote_string_literal(amount))).map_err(|_| invalid())
     228              :             }
     229            3 :             _ => string(),
     230              :         }
     231           30 :     }
     232              : 
     233              :     /// The type of the literal, None for null
     234           63 :     pub fn sql_type(&self) -> Option<SqlType> {
     235           63 :         self.sql_type.clone()
     236           63 :     }
     237              : 
     238              :     /// The value as a control shows it: the number, the content of a string, the text of a typed literal
     239            5 :     pub fn display_value(&self) -> String {
     240            5 :         self.value.clone()
     241            5 :     }
     242              : 
     243              :     /// The value as JSON, for a request: a number, a boolean, a null, or the text that a control shows
     244            9 :     pub fn to_json(&self) -> Value {
     245            9 :         match self.sql_type.as_ref().map(|t| &t.kind) {
     246            0 :             None => Value::Null,
     247            3 :             Some(SqlTypeKind::Bool) => Value::Bool(self.value == "true"),
     248            3 :             Some(SqlTypeKind::Int8 | SqlTypeKind::Numeric { .. }) => number(&self.value),
     249            3 :             Some(_) => Value::String(self.value.clone()),
     250              :         }
     251            9 :     }
     252              : 
     253              :     /// The literal in the Hyper dialect, the text of an `@param` tag
     254          191 :     pub fn to_hyper(&self) -> String {
     255          191 :         self.hyper.clone()
     256          191 :     }
     257              : 
     258              :     /// The literal in a dialect, as an expression that stands in the place of a parameter: a negative number is in
     259              :     /// parentheses, `x-:n` would become a comment otherwise
     260          163 :     pub fn print(&self, dialect: FormattingDialect) -> Result<String, String> {
     261          163 :         let mut formatter = Formatter::new(&self.parsed);
     262          163 :         let text = formatter.format_node_at(self.node as usize, &formatting_config(dialect));
     263          163 :         if !formatter.is_fully_formatted() {
     264            0 :             let name = crate::formatter::program::formatting_dialect_to_string(dialect);
     265            0 :             return Err(format!("{} can't be written in the {name} dialect", self.hyper));
     266          163 :         }
     267          163 :         Ok(if text.starts_with('-') { format!("({text})") } else { text })
     268          163 :     }
     269              : }
     270              : 
     271              : #[cfg(test)]
     272              : mod tests {
     273              :     use super::*;
     274              : 
     275           37 :     fn read(text: &str) -> SqlLiteral {
     276           37 :         SqlLiteral::parse(text).unwrap_or_else(|e| panic!("{text}: {e}"))
     277           37 :     }
     278              : 
     279            7 :     fn trino(text: &str) -> String {
     280            7 :         read(text).print(FormattingDialect::TRINO).unwrap_or_else(|e| panic!("{text}: {e}"))
     281            7 :     }
     282              : 
     283            6 :     fn hyper(text: &str) -> String {
     284            6 :         read(text).print(FormattingDialect::HYPER).unwrap()
     285            6 :     }
     286              : 
     287           13 :     fn kind(text: &str) -> Option<SqlTypeKind> {
     288           13 :         read(text).sql_type().map(|t| t.kind)
     289           13 :     }
     290              : 
     291              :     #[test]
     292            1 :     fn reads_literals_and_their_types() {
     293            1 :         assert_eq!(kind("10"), Some(SqlTypeKind::Int8));
     294            1 :         assert_eq!(kind("-2.5"), Some(SqlTypeKind::Numeric { precision: None, scale: None }));
     295            1 :         assert_eq!(kind("'it''s'"), Some(SqlTypeKind::Text));
     296            1 :         assert_eq!(kind("TRUE"), Some(SqlTypeKind::Bool));
     297            1 :         assert_eq!(kind("null"), None);
     298            1 :         assert_eq!(kind("date '2024-01-01'"), Some(SqlTypeKind::Date));
     299            1 :         assert_eq!(kind("timestamp '2024-01-01 10:00'"), Some(SqlTypeKind::Timestamp { precision: None, with_time_zone: false }));
     300            1 :         assert_eq!(kind("timestamp with time zone '2024-01-01'"), Some(SqlTypeKind::Timestamp { precision: None, with_time_zone: true }));
     301            1 :         assert_eq!(kind("timestamptz '2024-01-01'"), Some(SqlTypeKind::Timestamp { precision: None, with_time_zone: true }));
     302            1 :         assert_eq!(kind("interval '3' day"), Some(SqlTypeKind::Interval { fields: Some(IntervalTypeEnum::DAY), precision: None }));
     303            1 :         assert_eq!(kind("interval '-2' MONTH"), Some(SqlTypeKind::Interval { fields: Some(IntervalTypeEnum::MONTH), precision: None }));
     304            1 :         assert_eq!(read("'it''s'").display_value(), "it's");
     305            1 :         assert_eq!(read("interval '3' day").display_value(), "3 day");
     306            1 :         assert_eq!(read("interval '1.5' second").display_value(), "1.5 second");
     307            1 :         assert_eq!(read("-2").display_value(), "-2");
     308            1 :     }
     309              : 
     310              :     #[test]
     311            1 :     fn rejects_expressions() {
     312           10 :         for text in ["", "1 + 2", "now()", "x", "'a' || 'b'", "interval(3) '1 s'", "select 1", "1; select 2", "1 from t", "-'a'"] {
     313           10 :             assert!(SqlLiteral::parse(text).is_err(), "{text}");
     314              :         }
     315            1 :     }
     316              : 
     317              :     #[test]
     318            1 :     fn reads_intervals_of_a_number_and_a_unit() {
     319              :         // Interval text is no literal, Trino has no form of it and DashQL doesn't read it
     320            7 :         for text in [
     321            1 :             "interval '3 days'",
     322            1 :             "interval '1 day 2 hours'",
     323            1 :             "interval '01:30'",
     324            1 :             "interval 'soon' day",
     325            1 :             "interval '1.5' hour",
     326            1 :             "interval '1 02' day to hour",
     327            1 :             "interval 3 day",
     328            1 :         ] {
     329            7 :             let error = SqlLiteral::parse(text).err().unwrap_or_else(|| panic!("{text} is a literal"));
     330            7 :             assert!(error.contains("an interval is a number and a unit"), "{text}: {error}");
     331              :         }
     332            1 :     }
     333              : 
     334              :     #[test]
     335            1 :     fn prints_hyper() {
     336            1 :         assert_eq!(hyper("10"), "10");
     337            1 :         assert_eq!(hyper("-2"), "(-2)");
     338            1 :         assert_eq!(hyper("'EU'"), "'EU'");
     339            1 :         assert_eq!(hyper("TRUE"), "TRUE");
     340            1 :         assert_eq!(hyper("date '2024-01-01'"), "date '2024-01-01'");
     341            1 :         assert_eq!(hyper("interval '3' day"), "interval '3' day");
     342            1 :         assert_eq!(read("'it''s'").to_hyper(), "'it''s'");
     343            1 :     }
     344              : 
     345              :     #[test]
     346            1 :     fn prints_trino() {
     347            1 :         assert_eq!(trino("'it''s'"), "'it''s'");
     348            1 :         assert_eq!(trino("null"), "null");
     349            1 :         assert_eq!(trino("date '2024-01-01'"), "date '2024-01-01'");
     350            1 :         assert_eq!(trino("timestamptz '2024-01-01 10:00 UTC'"), "timestamp '2024-01-01 10:00 UTC'");
     351            1 :         assert_eq!(trino("timestamp with time zone '2024-01-01 10:00 UTC'"), "timestamp '2024-01-01 10:00 UTC'");
     352            1 :         assert_eq!(trino("interval '3' day"), "interval '3' day");
     353            1 :         assert_eq!(trino("interval '-14' month"), "interval '-14' month");
     354            1 :     }
     355              : 
     356              :     #[test]
     357            1 :     fn makes_literals_of_values() {
     358            1 :         let int = SqlType::scalar(SqlTypeKind::Int4);
     359            1 :         let interval = SqlType::scalar(SqlTypeKind::Interval { fields: None, precision: None });
     360            1 :         let date = SqlType::scalar(SqlTypeKind::Date);
     361            1 :         let text = SqlType::scalar(SqlTypeKind::Text);
     362           20 :         let value = |v: &str, t: Option<&SqlType>| SqlLiteral::from_value(v, t).map(|l| l.to_hyper());
     363            1 :         assert_eq!(value("10", Some(&int)), Ok("10".into()));
     364            1 :         assert!(value("1.5", Some(&int)).is_err());
     365            1 :         assert!(value("ten", Some(&int)).is_err());
     366            1 :         assert_eq!(value("3 day", Some(&interval)), Ok("interval '3' day".into()));
     367            1 :         assert_eq!(value(" 1.5  SECOND ", Some(&interval)), Ok("interval '1.5' second".into()));
     368            7 :         for text in ["3 days", "1 day 2 hour", "3", "day", "soon day", "1.5 hour", "3'; drop table t; -- day"] {
     369            7 :             assert!(value(text, Some(&interval)).is_err(), "{text}");
     370              :         }
     371            1 :         assert_eq!(value("2024-01-01", Some(&date)), Ok("date '2024-01-01'".into()));
     372            1 :         assert_eq!(value("it's", Some(&text)), Ok("'it''s'".into()));
     373            1 :         assert_eq!(value("10", Some(&text)), Ok("'10'".into()));
     374              :         // Without a type, a literal stays one and anything else is a string
     375            1 :         assert_eq!(value("10", None), Ok("10".into()));
     376            1 :         assert_eq!(value("true", None), Ok("true".into()));
     377            1 :         assert_eq!(value("interval '2' day", None), Ok("interval '2' day".into()));
     378            1 :         assert_eq!(value("EU", None), Ok("'EU'".into()));
     379            1 :         assert_eq!(value("it's", None), Ok("'it''s'".into()));
     380            1 :     }
     381              : 
     382              :     #[test]
     383            1 :     fn makes_literals_of_booleans_times_and_arrays() {
     384            6 :         let scalar = |kind| SqlType::scalar(kind);
     385           10 :         let value = |v: &str, t: &SqlType| SqlLiteral::from_value(v, Some(t)).map(|l| l.to_hyper());
     386            1 :         let boolean = scalar(SqlTypeKind::Bool);
     387            1 :         assert_eq!(value("TRUE", &boolean), Ok("TRUE".into()));
     388            1 :         let error = value("yes", &boolean).unwrap_err();
     389            1 :         assert!(error.contains("needs true or false"), "{error}");
     390            1 :         assert!(value("1", &boolean).is_err());
     391              : 
     392            2 :         let time = |with_time_zone| scalar(SqlTypeKind::Time { precision: None, with_time_zone });
     393            2 :         let timestamp = |with_time_zone| scalar(SqlTypeKind::Timestamp { precision: None, with_time_zone });
     394            1 :         assert_eq!(value("10:00", &time(false)), Ok("time '10:00'".into()));
     395            1 :         assert_eq!(value("10:00+01", &time(true)), Ok("timetz '10:00+01'".into()));
     396            1 :         assert_eq!(value("2024-01-01 10:00", &timestamp(false)), Ok("timestamp '2024-01-01 10:00'".into()));
     397            1 :         assert_eq!(value("2024-01-01 10:00 UTC", &timestamp(true)), Ok("timestamptz '2024-01-01 10:00 UTC'".into()));
     398              : 
     399              :         // Arrays have no controls, their values are read like values without a type
     400            1 :         let ints = SqlType { kind: SqlTypeKind::Int4, array_dimensions: 1 };
     401            1 :         assert_eq!(value("10", &ints), Ok("10".into()));
     402            1 :         assert_eq!(value("EU", &ints), Ok("'EU'".into()));
     403              :         // A generic type reads a string
     404            1 :         let json = scalar(SqlTypeKind::Generic { name: "json".into(), modifiers: vec![] });
     405            1 :         assert_eq!(value("{}", &json), Ok("'{}'".into()));
     406            1 :     }
     407              : 
     408              :     #[test]
     409            1 :     fn reads_typed_literals_of_any_type() {
     410            1 :         assert_eq!(kind("numeric(10,2) '1.5'"), Some(SqlTypeKind::Numeric { precision: Some(10), scale: Some(2) }));
     411            1 :         assert_eq!(kind("json '{}'"), Some(SqlTypeKind::Generic { name: "json".into(), modifiers: vec![] }));
     412            1 :         assert_eq!(read("json '{}'").display_value(), "{}");
     413            3 :         for text in ["date 1", "-true", "geography(4326) 'x'"] {
     414            3 :             assert!(SqlLiteral::parse(text).is_err(), "{text}");
     415              :         }
     416            1 :     }
     417              : 
     418              :     #[test]
     419            1 :     fn literals_compare_by_their_hyper_text() {
     420            1 :         assert_eq!(read("10"), read(" 10 "));
     421            1 :         assert_ne!(read("10"), read("'10'"));
     422            1 :         assert_eq!(format!("{:?}", read("'EU'")), "SqlLiteral('EU')");
     423            1 :     }
     424              : }
        

Generated by: LCOV version 2.0-1