Skip to content

Commit fa7c007

Browse files
committed
feat(native): parse function headers and resolve function namespaces natively behind the preview engine
1 parent f5341dc commit fa7c007

25 files changed

Lines changed: 1300 additions & 86 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub(crate) mod namespace;
2+
pub(crate) mod values;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! One of database or schema of a function namespace.
2+
3+
pub(crate) type Part = (
4+
Option<String>,
5+
Option<String>,
6+
Option<String>,
7+
Option<String>,
8+
);
9+
10+
/// `(physical, logical, fingerprint, fingerprint logical)` for one of database or schema.
11+
pub(crate) fn part(
12+
header: Option<&String>,
13+
default: Option<&String>,
14+
target: Option<&String>,
15+
inherit: bool,
16+
) -> Part {
17+
let logical: Option<String> = match header {
18+
Some(value) => Some(value.clone()),
19+
None if inherit => default.cloned(),
20+
None => None,
21+
};
22+
let physical: Option<String> = match target {
23+
None => logical.clone(),
24+
Some(value) if header.is_some() || inherit => Some(value.clone()),
25+
Some(_) => None,
26+
};
27+
let fingerprint_logical: Option<String> = header.or(default).cloned();
28+
let fingerprint: Option<String> = target.cloned().or_else(|| fingerprint_logical.clone());
29+
(physical, logical, fingerprint, fingerprint_logical)
30+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! Python's header value checks: non-empty stripped strings, maps and string lists.
2+
3+
use sqlbuild_core::text::main::python_strip::python_strip;
4+
5+
use crate::functions::models::{HeaderValue, NamedType};
6+
7+
/// Python's `TABLE_FUNCTION_RETURN_KEYS`.
8+
pub(crate) const TABLE_RETURN_KEY: &str = "table";
9+
10+
/// `value.strip()` when `value` is a string that is not blank.
11+
pub(crate) fn stripped_text(value: Option<&HeaderValue>) -> Option<String> {
12+
match value? {
13+
HeaderValue::Text(text) if !python_strip(text).is_empty() => {
14+
Some(python_strip(text).to_owned())
15+
}
16+
_ => None,
17+
}
18+
}
19+
20+
/// Named types of a map of non-blank strings; None where Python raises.
21+
pub(crate) fn named_types(entries: &[(HeaderValue, HeaderValue)]) -> Option<Vec<NamedType>> {
22+
let mut named: Vec<NamedType> = Vec::with_capacity(entries.len());
23+
for (key, value) in entries {
24+
let HeaderValue::Text(raw_name) = key else {
25+
return None;
26+
};
27+
named.push(NamedType {
28+
raw_name: raw_name.clone(),
29+
name: stripped_text(Some(key))?,
30+
type_text: stripped_text(Some(value))?,
31+
});
32+
}
33+
Some(named)
34+
}
35+
36+
/// Stripped entries of an optional list or tuple of non-blank strings.
37+
pub(crate) fn stripped_list(value: Option<&HeaderValue>) -> Option<Vec<String>> {
38+
match value {
39+
None => Some(Vec::new()),
40+
Some(HeaderValue::Sequence(items)) => {
41+
let mut entries: Vec<String> = Vec::with_capacity(items.len());
42+
for item in items {
43+
entries.push(stripped_text(Some(item))?);
44+
}
45+
Some(entries)
46+
}
47+
Some(_) => None,
48+
}
49+
}
50+
51+
/// An optional string description, stripped.
52+
pub(crate) fn description(value: Option<&HeaderValue>) -> Option<Option<String>> {
53+
match value {
54+
None => Some(None),
55+
Some(HeaderValue::Text(text)) => Some(Some(python_strip(text).to_owned())),
56+
Some(_) => None,
57+
}
58+
}
59+
60+
/// The header value under `key`.
61+
pub(crate) fn lookup<'header>(
62+
header: &'header [(String, HeaderValue)],
63+
key: &str,
64+
) -> Option<&'header HeaderValue> {
65+
header
66+
.iter()
67+
.find(|(name, _)| name == key)
68+
.map(|(_, value)| value)
69+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod parse_function_header;
2+
pub mod resolve_function_namespace;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Python's SQL and Python function header parsing, before template expansion.
2+
3+
use crate::functions::_helpers::values::{
4+
TABLE_RETURN_KEY, description, lookup, named_types, stripped_list, stripped_text,
5+
};
6+
use crate::functions::models::{
7+
FunctionHeader, FunctionLanguage, FunctionReturns, HeaderValue, NamedType,
8+
};
9+
10+
/// The parsed header, or None wherever Python raises for its shape.
11+
#[must_use]
12+
pub fn parse_function_header(
13+
header: &[(String, HeaderValue)],
14+
language: FunctionLanguage,
15+
) -> Option<FunctionHeader> {
16+
let returns: FunctionReturns = match (language, lookup(header, "returns")?) {
17+
(_, HeaderValue::Text(_)) => {
18+
FunctionReturns::Type(stripped_text(lookup(header, "returns"))?)
19+
}
20+
(FunctionLanguage::Sql, HeaderValue::Map(entries)) => table_returns(entries)?,
21+
_ => return None,
22+
};
23+
let arguments: Vec<NamedType> = match lookup(header, "arguments") {
24+
None => Vec::new(),
25+
Some(HeaderValue::Map(entries)) => named_types(entries)?,
26+
Some(_) => return None,
27+
};
28+
let python: bool = language == FunctionLanguage::Python;
29+
Some(FunctionHeader {
30+
arguments,
31+
returns,
32+
runtime_version: if python {
33+
Some(stripped_text(lookup(header, "runtime_version"))?)
34+
} else {
35+
None
36+
},
37+
entry_point: if python {
38+
Some(stripped_text(lookup(header, "entry_point"))?)
39+
} else {
40+
None
41+
},
42+
packages: if python {
43+
stripped_list(lookup(header, "packages"))?
44+
} else {
45+
Vec::new()
46+
},
47+
tags: stripped_list(lookup(header, "tags"))?,
48+
description: description(lookup(header, "description"))?,
49+
})
50+
}
51+
52+
/// Python's `returns (table (...))`: exactly the `table` key with a non-empty column map.
53+
fn table_returns(entries: &[(HeaderValue, HeaderValue)]) -> Option<FunctionReturns> {
54+
let [(HeaderValue::Text(key), HeaderValue::Map(columns))] = entries else {
55+
return None;
56+
};
57+
if key != TABLE_RETURN_KEY || columns.is_empty() {
58+
return None;
59+
}
60+
Some(FunctionReturns::Table(named_types(columns)?))
61+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! Python's SQL and Python function namespace and fingerprint namespace resolution.
2+
3+
use crate::functions::_helpers::namespace::part;
4+
use crate::functions::models::{FunctionLanguage, FunctionNamespace, NamespaceInputs};
5+
6+
/// The physical, logical and fingerprint namespace Python's attachment assigns.
7+
#[must_use]
8+
pub fn resolve_function_namespace(inputs: &NamespaceInputs) -> FunctionNamespace {
9+
let inherit: bool =
10+
inputs.language == FunctionLanguage::Sql || inputs.inherit_default_namespace;
11+
let (database, logical_database, fingerprint_database, fingerprint_logical_database) = part(
12+
inputs.header_database.as_ref(),
13+
inputs.default_database.as_ref(),
14+
inputs.target_database.as_ref(),
15+
inherit,
16+
);
17+
let (schema, logical_schema, fingerprint_schema, fingerprint_logical_schema) = part(
18+
inputs.header_schema.as_ref(),
19+
inputs.default_schema.as_ref(),
20+
inputs.target_schema.as_ref(),
21+
inherit,
22+
);
23+
FunctionNamespace {
24+
database,
25+
schema,
26+
logical_database,
27+
logical_schema,
28+
fingerprint_database,
29+
fingerprint_schema,
30+
fingerprint_logical_database,
31+
fingerprint_logical_schema,
32+
}
33+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub(crate) mod _helpers;
2+
pub mod main;
3+
pub mod models;
4+
5+
#[cfg(test)]
6+
mod tests;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//! Function header values as Python holds them and what Python's attachment parses from them.
2+
3+
/// One authored header value: text, a mapping, a list or tuple, or anything else.
4+
#[derive(Clone, Debug, PartialEq, Eq)]
5+
pub enum HeaderValue {
6+
Text(String),
7+
Map(Vec<(HeaderValue, HeaderValue)>),
8+
Sequence(Vec<HeaderValue>),
9+
Other,
10+
}
11+
12+
/// Which function file kind's header rules apply.
13+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14+
pub enum FunctionLanguage {
15+
Sql,
16+
Python,
17+
}
18+
19+
/// One argument or return column: the authored name, the stripped name and the stripped type.
20+
#[derive(Clone, Debug, PartialEq, Eq)]
21+
pub struct NamedType {
22+
/// Python's error messages quote the name as authored.
23+
pub raw_name: String,
24+
pub name: String,
25+
pub type_text: String,
26+
}
27+
28+
/// A scalar return type or table function columns, with types before template expansion.
29+
#[derive(Clone, Debug, PartialEq, Eq)]
30+
pub enum FunctionReturns {
31+
Type(String),
32+
Table(Vec<NamedType>),
33+
}
34+
35+
/// Stripped header values; argument, return and column types are still unexpanded templates.
36+
#[derive(Clone, Debug, PartialEq, Eq)]
37+
pub struct FunctionHeader {
38+
pub arguments: Vec<NamedType>,
39+
pub returns: FunctionReturns,
40+
pub tags: Vec<String>,
41+
pub description: Option<String>,
42+
/// Python functions only: `runtime_version`, `entry_point` and `packages`.
43+
pub runtime_version: Option<String>,
44+
pub entry_point: Option<String>,
45+
pub packages: Vec<String>,
46+
}
47+
48+
/// The project and target namespace a function resolves against, already expanded.
49+
#[derive(Clone, Debug, PartialEq, Eq)]
50+
pub struct NamespaceInputs {
51+
/// The function header's own `database` and `schema`, when they are strings.
52+
pub header_database: Option<String>,
53+
pub header_schema: Option<String>,
54+
pub default_database: Option<String>,
55+
pub default_schema: Option<String>,
56+
pub target_database: Option<String>,
57+
pub target_schema: Option<String>,
58+
/// Python functions without their own namespace inherit the project defaults.
59+
pub language: FunctionLanguage,
60+
pub inherit_default_namespace: bool,
61+
}
62+
63+
/// Python's resolved physical, logical and fingerprint namespace of one function.
64+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
65+
pub struct FunctionNamespace {
66+
pub database: Option<String>,
67+
pub schema: Option<String>,
68+
pub logical_database: Option<String>,
69+
pub logical_schema: Option<String>,
70+
pub fingerprint_database: Option<String>,
71+
pub fingerprint_schema: Option<String>,
72+
pub fingerprint_logical_database: Option<String>,
73+
pub fingerprint_logical_schema: Option<String>,
74+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#[path = "tests/helpers.rs"]
2+
mod helpers;
3+
#[path = "tests/test_parse_function_header.rs"]
4+
mod parse_function_header;
5+
#[path = "tests/test_resolve_function_namespace.rs"]
6+
mod resolve_function_namespace;
7+
#[path = "tests/test_types.rs"]
8+
mod test_types;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use crate::functions::models::{FunctionLanguage, HeaderValue, NamedType, NamespaceInputs};
2+
3+
pub(super) fn text(value: &str) -> HeaderValue {
4+
HeaderValue::Text(value.to_owned())
5+
}
6+
7+
pub(super) fn map(entries: &[(&str, HeaderValue)]) -> HeaderValue {
8+
let mut pairs: Vec<(HeaderValue, HeaderValue)> = Vec::new();
9+
for (key, value) in entries {
10+
pairs.push((text(key), value.clone()));
11+
}
12+
HeaderValue::Map(pairs)
13+
}
14+
15+
pub(super) fn header(entries: &[(&str, HeaderValue)]) -> Vec<(String, HeaderValue)> {
16+
let mut pairs: Vec<(String, HeaderValue)> = Vec::new();
17+
for (key, value) in entries {
18+
pairs.push(((*key).to_owned(), value.clone()));
19+
}
20+
pairs
21+
}
22+
23+
pub(super) fn owned(value: Option<&str>) -> Option<String> {
24+
value.map(str::to_owned)
25+
}
26+
27+
/// Header `udfs`, defaults `analytics.shared`, target `prod_db` with no target schema.
28+
pub(super) fn namespace_inputs(language: FunctionLanguage, inherit: bool) -> NamespaceInputs {
29+
NamespaceInputs {
30+
header_database: None,
31+
header_schema: owned(Some("udfs")),
32+
default_database: owned(Some("analytics")),
33+
default_schema: owned(Some("shared")),
34+
target_database: owned(Some("prod_db")),
35+
target_schema: None,
36+
language,
37+
inherit_default_namespace: inherit,
38+
}
39+
}
40+
41+
pub(super) fn named(raw_name: &str, name: &str, type_text: &str) -> NamedType {
42+
NamedType {
43+
raw_name: raw_name.to_owned(),
44+
name: name.to_owned(),
45+
type_text: type_text.to_owned(),
46+
}
47+
}

0 commit comments

Comments
 (0)