Skip to content

Add built-in functions#781

Open
aannleax wants to merge 16 commits into
mainfrom
feature/add-builtins
Open

Add built-in functions#781
aannleax wants to merge 16 commits into
mainfrom
feature/add-builtins

Conversation

@aannleax

@aannleax aannleax commented Apr 8, 2026

Copy link
Copy Markdown
Member

This PR adds new built-in functions. Closes #642 (I moved aggregate functions to a separate issue #782) and closes #769.

String functions

  • REPLACE(str, pattern, replacement[, flags]) : regex-based string replacement
  • langMatches(tag, range): language tag range matching per SPARQL/RFC 4647
  • STRTRIM(str): remove leading and trailing whitespace
  • STRTRIMSTART(str): remove leading whitespace only
  • STRTRIMEND(str): remove trailing whitespace only
  • STRDT(lex, datatype): construct a typed literal from a lexical value and datatype IRI
  • Hashing functions: MD5, SHA1, SHA256, SHA384, SHA512

Date/time functions

  • YEAR, MONTH, DAY, HOURS, MINUTES, SECONDS: component extraction from xsd:dateTime, xsd:date, and xsd:time values
  • TIMEZONE: timezone as xsd:dayTimeDuration
  • TZ: timezone as a plain string

Nondeterministic functions

  • RAND(): pseudo-random double in [0, 1)
  • UUID() : fresh IRI-form UUID
  • STRUUID() : fresh string-form UUID
  • NOW() : current date/time as xsd:dateTime

Changes to existing functions

  • REGEX now accepts flags as an optional third parameter

@github-project-automation github-project-automation Bot moved this to Todo in nemo Apr 8, 2026
@aannleax aannleax added the builtins Issue related to built-in functions label Apr 8, 2026
@aannleax aannleax added this to the Release 0.10.0 milestone Apr 8, 2026
@aannleax aannleax requested a review from mmarx April 8, 2026 14:40

@mmarx mmarx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not yet reviewed everything, but I don't think this is ready to merge without a major overhaul. There's several issues both with respect to code quality and with respect to correctness:

  • we're pulling in chrono for handling timestamps, but then re-implement the timestamp parsing ourselves – why?
  • I'm not impressed with the code quality – there's functions that are defined identically in multiple files (and then inlined in other places as well), one-line functions that are only called once, redundant function calls like Some(value.to_plain_string_unchecked()) which could be a simple value.to_plain_string() instead, …
  • the hash functions are implemented for plain strings and language tagged strings, yet the spec says that they take plain strings and simple literals
  • the nondeterministic functions take an arbitrary number of arguments, even though they really should not take any arguments at all

Comment thread nemo-physical/src/function/definitions/datetime.rs Outdated
Comment thread nemo-physical/src/function/definitions/datetime.rs Outdated
Comment on lines +17 to +31
fn int_type_propagation() -> FunctionTypePropagation {
FunctionTypePropagation::KnownOutput(StorageTypeName::Int64.bitset())
}

fn double_type_propagation() -> FunctionTypePropagation {
FunctionTypePropagation::KnownOutput(StorageTypeName::Double.bitset())
}

fn string_type_propagation() -> FunctionTypePropagation {
FunctionTypePropagation::KnownOutput(
StorageTypeName::Id32
.bitset()
.union(StorageTypeName::Id64.bitset()),
)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these separate functions? double_type_propagation is even only called once.

Comment on lines +149 to +159
if parameter_first.value_domain() != ValueDomain::PlainString {
return None;
}
if parameter_second.value_domain() != ValueDomain::Iri {
return None;
}

let lexical = parameter_first.to_plain_string_unchecked();
let datatype = parameter_second.to_iri_unchecked();

AnyDataValue::new_from_typed_literal(lexical, datatype).ok()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if parameter_first.value_domain() != ValueDomain::PlainString {
return None;
}
if parameter_second.value_domain() != ValueDomain::Iri {
return None;
}
let lexical = parameter_first.to_plain_string_unchecked();
let datatype = parameter_second.to_iri_unchecked();
AnyDataValue::new_from_typed_literal(lexical, datatype).ok()
AnyDataValue::new_from_typed_literal(
parameter_first.to_plain_string()?,
parameter_second.to_iri()?
).ok()

Comment thread nemo-physical/src/function/definitions/generic.rs
Comment thread nemo-physical/src/function/definitions/hashing.rs Outdated
Comment thread nemo-physical/src/function/definitions/hashing.rs Outdated
Comment thread nemo-physical/src/function/definitions/nondeterministic.rs Outdated
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in nemo Apr 8, 2026
@mkroetzsch

Copy link
Copy Markdown
Member

Since "simple literals" came up in the discussion: These should not be implemented as a separate type but identified with xsd:string. This is a relic from the depth of RDF history that is vanishing as specs get updated.

@aannleax aannleax requested a review from mmarx April 9, 2026 08:50
Comment thread nemo-physical/src/function/definitions/datetime.rs
Comment thread nemo-physical/src/function/definitions/datetime.rs
Comment thread nemo-physical/src/function/definitions/datetime.rs
Comment thread nemo-physical/src/function/definitions/datetime.rs
Comment thread nemo-physical/src/function/definitions/datetime.rs

let regex_str = if parameters.len() == 3 {
let flags = LangTaggedString::try_from(parameters[2].clone()).ok()?;
format!("(?{}){}", flags.string, lang_pattern.string)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has, in principle, similar problems to the REPLACE implementation. It's somewhat mitigated by the regex crate not implementing backreferences – which SPARQL requires, so that's a different kind of problems. We probably want to use fancy-regex instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah so the regex crate doesnt implement those, because they are hard to implement efficiently. But I guess we don't mind that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on what we want – if we want to go for SPARQL compatibility, a different regex implementation is required (fancy-regex uses a hybrid approach, where the efficient NFA-based implementation is used if possible, so backreferences, lookaround, etc. become pay-as-you-go). In any case, we still shouldn't just inject flags into the pattern here, certainly not without validating them first. It's also better to use the RegexBuilder to set the options instead of splicing them into the pattern.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +450 to +451
/// An optional third parameter may provide regex flags (e.g. `"i"` for case-insensitive),
/// corresponding to the SPARQL `regex(string, pattern [, flags])` function.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also doesn't validate the flags, and furthermore doesn't support the q flag.

Comment on lines +549 to +550
/// Returns a language tagged string if the first parameter has a language tag.
/// Otherwise, return a plain string.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if we could keep the grammar consistent and not use returns and return in the same paragraph.

Comment thread nemo-physical/src/function/definitions/string.rs
Comment on lines +1595 to 1603
// With flags: case-insensitive match
let string_flags = AnyDataValue::new_plain_string("Hello".to_string());
let pattern_flags = AnyDataValue::new_plain_string("hello".to_string());
let flags = AnyDataValue::new_plain_string("i".to_string());
let result_flags = AnyDataValue::new_boolean(true);
let actual_result_flags = StringRegex.evaluate(&[string_flags, pattern_flags, flags]);
assert!(actual_result_flags.is_some());
assert_eq!(result_flags, actual_result_flags.unwrap());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i is the most boring flag to test here. What we should have test for are the q flag (since that requires special handling), though that should rather be an integration test, backreferences in pattern (again an integration test), and that invalid flags lead to None.

@mmarx mmarx modified the milestones: Release 0.10.0, Release 0.11.0 Apr 13, 2026
Comment on lines +123 to +127
if *parameter_count == 0 {
current_height += 1;
} else {
current_height -= parameter_count - 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now a more complicated way to write the same thing as before, why?

Suggested change
if *parameter_count == 0 {
current_height += 1;
} else {
current_height -= parameter_count - 1;
}
current_height -= parameter_count - 1;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parameter count is a usize

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. It's still the most(?) convoluted way to write

Suggested change
if *parameter_count == 0 {
current_height += 1;
} else {
current_height -= parameter_count - 1;
}
current_height -= parameter_count;
current_height += 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(alternatively, make those isize here)

@mmarx

mmarx commented Apr 14, 2026

Copy link
Copy Markdown
Member

We really need to push the nondeterministic functions into SPARQL queries. Consider the following program:

@import nemo :- sparql{ endpoint = <https://query.wikidata.org/sparql>, query = "SELECT ?x ?y WHERE { BIND(?x AS ?y) }"} .
@import sparql :- sparql{ endpoint = <https://query.wikidata.org/sparql>, query = "SELECT ?x WHERE { BIND(RAND() AS ?x) }"} .

foo(?x) :- nemo(RAND(), ?x) .
bar(?x) :- sparql(?x) .

bar correctly turns into a single SPARQL query, whereas foo will just produce an endless list of queries.

pub struct FuncUuid;
impl NullaryFunction for FuncUuid {
fn evaluate(&self) -> Option<AnyDataValue> {
let iri = format!("urn:uuid:{}", Uuid::new_v4());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to use v7 UUIDs here: a v7 UUID is always lexicographically greater than all the v7 UUIDs generated before it, since they include the current time in addition to randomness. SPARQL leaves the version implementation-defined: “The variant and version of the UUID is implementation dependent.”

pub struct FuncStruuid;
impl NullaryFunction for FuncStruuid {
fn evaluate(&self) -> Option<AnyDataValue> {
Some(AnyDataValue::new_plain_string(Uuid::new_v4().to_string()))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above, v7 might be more appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

builtins Issue related to built-in functions

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Add built-in to strip/trim strings Suport more functions

3 participants