flowscope_core/generated/function_rules.rs
1//! Function argument handling rules per dialect.
2//!
3//! Generated from dialect_behavior.toml
4//!
5//! This module provides dialect-aware rules for function argument handling,
6//! particularly for date/time functions where certain arguments are keywords
7//! (like `YEAR`, `MONTH`) rather than column references.
8
9use crate::Dialect;
10
11/// Returns argument indices to skip when extracting column references from a function call.
12///
13/// Certain SQL functions take keyword arguments (e.g., `DATEDIFF(YEAR, start, end)` in Snowflake)
14/// that should not be treated as column references during lineage analysis. This function
15/// returns the indices of such arguments for the given function and dialect.
16///
17/// # Arguments
18///
19/// * `dialect` - The SQL dialect being analyzed
20/// * `func_name` - The function name (case-insensitive, underscore-insensitive)
21///
22/// # Returns
23///
24/// A slice of argument indices (0-based) to skip. Returns an empty slice for
25/// unknown functions or functions without skip rules.
26///
27/// # Example
28///
29/// ```ignore
30/// // In Snowflake, DATEDIFF takes a unit as the first argument
31/// let skip = skip_args_for_function(Dialect::Snowflake, "DATEDIFF");
32/// assert_eq!(skip, &[0]); // Skip first argument (the unit)
33///
34/// // Both DATEADD and DATE_ADD match the same rules
35/// let skip1 = skip_args_for_function(Dialect::Snowflake, "DATEADD");
36/// let skip2 = skip_args_for_function(Dialect::Snowflake, "DATE_ADD");
37/// assert_eq!(skip1, skip2);
38/// ```
39pub fn skip_args_for_function(dialect: Dialect, func_name: &str) -> &'static [usize] {
40 // Normalize: lowercase and remove underscores to handle both DATEADD and DATE_ADD variants
41 let func_normalized: String = func_name
42 .chars()
43 .filter(|c| *c != '_')
44 .map(|c| c.to_ascii_lowercase())
45 .collect();
46 match func_normalized.as_str() {
47 "datediff" => match dialect {
48 Dialect::Bigquery => &[],
49 Dialect::Databricks => &[],
50 Dialect::Duckdb => &[],
51 Dialect::Hive => &[],
52 Dialect::Mssql => &[0],
53 Dialect::Mysql => &[],
54 Dialect::Redshift => &[0],
55 Dialect::Snowflake => &[0],
56 _ => &[],
57 },
58 "dateadd" => match dialect {
59 Dialect::Bigquery => &[],
60 Dialect::Hive => &[],
61 Dialect::Mssql => &[0],
62 Dialect::Mysql => &[],
63 Dialect::Postgres => &[],
64 Dialect::Snowflake => &[0],
65 _ => &[],
66 },
67 "datepart" => match dialect {
68 Dialect::Postgres => &[0],
69 Dialect::Redshift => &[0],
70 Dialect::Snowflake => &[0],
71 _ => &[],
72 },
73 "datetrunc" => match dialect {
74 Dialect::Bigquery => &[1],
75 Dialect::Databricks => &[0],
76 Dialect::Duckdb => &[0],
77 Dialect::Postgres => &[0],
78 Dialect::Redshift => &[0],
79 Dialect::Snowflake => &[0],
80 _ => &[],
81 },
82 "extract" => &[0],
83 "timestampadd" => match dialect {
84 Dialect::Bigquery => &[1],
85 Dialect::Snowflake => &[0],
86 _ => &[],
87 },
88 "timestampsub" => match dialect {
89 Dialect::Bigquery => &[1],
90 _ => &[],
91 },
92 _ => &[],
93 }
94}
95
96
97/// NULL ordering behavior in ORDER BY.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum NullOrdering {
100 /// NULLs sort as larger than all other values (NULLS LAST for ASC)
101 NullsAreLarge,
102 /// NULLs sort as smaller than all other values (NULLS FIRST for ASC)
103 NullsAreSmall,
104 /// NULLs always sort last regardless of ASC/DESC
105 NullsAreLast,
106}
107
108impl Dialect {
109 /// Get the default NULL ordering behavior for this dialect.
110 pub const fn null_ordering(&self) -> NullOrdering {
111 match self {
112 Dialect::Bigquery => NullOrdering::NullsAreSmall,
113 Dialect::Clickhouse => NullOrdering::NullsAreLast,
114 Dialect::Databricks => NullOrdering::NullsAreSmall,
115 Dialect::Duckdb => NullOrdering::NullsAreLast,
116 Dialect::Hive => NullOrdering::NullsAreSmall,
117 Dialect::Mssql => NullOrdering::NullsAreSmall,
118 Dialect::Mysql => NullOrdering::NullsAreSmall,
119 Dialect::Oracle => NullOrdering::NullsAreLarge,
120 Dialect::Postgres => NullOrdering::NullsAreLarge,
121 Dialect::Redshift => NullOrdering::NullsAreLarge,
122 Dialect::Snowflake => NullOrdering::NullsAreLarge,
123 Dialect::Sqlite => NullOrdering::NullsAreSmall,
124 _ => NullOrdering::NullsAreLast,
125 }
126 }
127
128 /// Whether this dialect supports implicit UNNEST (no CROSS JOIN needed).
129 pub const fn supports_implicit_unnest(&self) -> bool {
130 matches!(self, Dialect::Bigquery | Dialect::Redshift)
131 }
132}
133
134/// Checks if a function is a value table function (returns rows) for the given dialect.
135///
136/// Value table functions (like UNNEST, GENERATE_SERIES, FLATTEN) return rows/tables
137/// rather than scalar values. This classification is used during lineage analysis
138/// to determine how FROM clause function calls should be handled.
139///
140/// # Arguments
141///
142/// * `dialect` - The SQL dialect being analyzed
143/// * `func_name` - The function name (case-insensitive)
144///
145/// # Returns
146///
147/// `true` if the function is a value table function for the given dialect.
148///
149/// # Example
150///
151/// ```ignore
152/// use flowscope_core::generated::is_value_table_function;
153/// use flowscope_core::Dialect;
154///
155/// assert!(is_value_table_function(Dialect::Postgres, "UNNEST"));
156/// assert!(is_value_table_function(Dialect::Snowflake, "FLATTEN"));
157/// assert!(!is_value_table_function(Dialect::Postgres, "COUNT"));
158/// ```
159pub fn is_value_table_function(dialect: Dialect, func_name: &str) -> bool {
160 let name = func_name.to_ascii_uppercase();
161 // Check common functions
162 if matches!(name.as_str(), "UNNEST" | "GENERATE_SERIES" | "JSON_TABLE") {
163 return true;
164 }
165 // Check dialect-specific functions
166 match dialect {
167 Dialect::Postgres => matches!(name.as_str(), "GENERATE_SUBSCRIPTS" | "REGEXP_MATCHES"),
168 Dialect::Snowflake => matches!(name.as_str(), "FLATTEN" | "SPLIT_TO_TABLE" | "STRTOK_SPLIT_TO_TABLE"),
169 Dialect::Mssql => matches!(name.as_str(), "OPENJSON" | "STRING_SPLIT"),
170 Dialect::Duckdb => matches!(name.as_str(), "RANGE"),
171 Dialect::Clickhouse => matches!(name.as_str(), "ARRAY_JOIN"),
172 Dialect::Databricks => matches!(name.as_str(), "EXPLODE" | "EXPLODE_OUTER" | "POSEXPLODE" | "POSEXPLODE_OUTER" | "INLINE" | "INLINE_OUTER"),
173 Dialect::Hive => matches!(name.as_str(), "EXPLODE" | "POSEXPLODE" | "INLINE" | "JSON_TUPLE" | "PARSE_URL_TUPLE"),
174 _ => false,
175 }
176}