Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions flowquery-py/src/parsing/data_structures/lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def is_operand(self) -> bool:

def value(self) -> Any:
obj = self.variable.value()
if obj is None:
return None
key = self.index.value()
# Try dict-like access first, then fall back to attribute access for objects
try:
Expand Down
42 changes: 40 additions & 2 deletions flowquery-py/src/parsing/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
from .aggregate_function import AggregateFunction
from .async_function import AsyncFunction
from .avg import Avg
from .coalesce import Coalesce
from .collect import Collect
from .count import Count
from .date_ import DateFunction
from .datetime_ import Datetime
from .duration import Duration
from .element_id import ElementId
from .function import Function
from .function_factory import FunctionFactory
from .function_metadata import (
Expand All @@ -19,23 +24,36 @@
get_registered_function_metadata,
)
from .functions import Functions
from .head import Head
from .id_ import Id
from .join import Join
from .keys import Keys
from .last import Last
from .localdatetime import LocalDatetime
from .localtime import LocalTime
from .max_ import Max
from .min_ import Min
from .nodes import Nodes
from .predicate_function import PredicateFunction
from .predicate_sum import PredicateSum
from .properties import Properties
from .rand import Rand
from .range_ import Range
from .reducer_element import ReducerElement
from .relationships import Relationships
from .replace import Replace
from .round_ import Round
from .schema import Schema
from .size import Size
from .split import Split
from .string_distance import StringDistance
from .stringify import Stringify

# Built-in functions
from .sum import Sum
from .tail import Tail
from .time_ import Time
from .timestamp import Timestamp
from .to_float import ToFloat
from .to_integer import ToInteger
from .to_json import ToJson
from .to_lower import ToLower
from .to_string import ToString
Expand Down Expand Up @@ -64,10 +82,23 @@
# Built-in functions
"Sum",
"Avg",
"DateFunction",
"Datetime",
"Coalesce",
"Collect",
"Count",
"Duration",
"ElementId",
"Head",
"Id",
"Join",
"Last",
"Keys",
"Max",
"Min",
"Nodes",
"Properties",
"Relationships",
"Rand",
"Range",
"Replace",
Expand All @@ -76,11 +107,18 @@
"Split",
"StringDistance",
"Stringify",
"Tail",
"Time",
"Timestamp",
"ToFloat",
"ToInteger",
"ToJson",
"ToLower",
"ToString",
"Trim",
"Type",
"LocalDatetime",
"LocalTime",
"Functions",
"Schema",
"PredicateSum",
Expand Down
44 changes: 44 additions & 0 deletions flowquery-py/src/parsing/functions/coalesce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Coalesce function."""

from typing import Any

from .function import Function
from .function_metadata import FunctionDef


@FunctionDef({
"description": "Returns the first non-null value from a list of expressions",
"category": "scalar",
"parameters": [
{"name": "expressions", "description": "Two or more expressions to evaluate", "type": "any"}
],
"output": {"description": "The first non-null value, or null if all values are null", "type": "any"},
"examples": [
"RETURN coalesce(null, 'hello', 'world')",
"MATCH (n) RETURN coalesce(n.nickname, n.name) AS displayName"
]
})
class Coalesce(Function):
"""Coalesce function.

Returns the first non-null value from a list of expressions.
Equivalent to Neo4j's coalesce() function.
"""

def __init__(self) -> None:
super().__init__("coalesce")
self._expected_parameter_count = None # variable number of parameters

def value(self) -> Any:
children = self.get_children()
if len(children) == 0:
raise ValueError("coalesce() requires at least one argument")
for child in children:
try:
val = child.value()
except (KeyError, AttributeError):
# Treat missing properties/keys as null, matching Neo4j behavior
val = None
if val is not None:
return val
return None
63 changes: 63 additions & 0 deletions flowquery-py/src/parsing/functions/date_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Date function."""

from datetime import datetime
from typing import Any

from .function import Function
from .function_metadata import FunctionDef
from .temporal_utils import build_date_object, parse_temporal_arg


@FunctionDef({
"description": (
"Returns a date value. With no arguments returns the current date. "
"Accepts an ISO 8601 date string or a map of components (year, month, day)."
),
"category": "scalar",
"parameters": [
{
"name": "input",
"description": "Optional. An ISO 8601 date string (YYYY-MM-DD) or a map of components.",
"type": "string",
"required": False,
},
],
"output": {
"description": (
"A date object with properties: year, month, day, "
"epochMillis, dayOfWeek, dayOfYear, quarter, formatted"
),
"type": "object",
},
"examples": [
"RETURN date() AS today",
"RETURN date('2025-06-15') AS d",
"RETURN date({year: 2025, month: 6, day: 15}) AS d",
"WITH date() AS d RETURN d.year, d.month, d.dayOfWeek",
],
})
class DateFunction(Function):
"""Date function.

Returns a date value (no time component).
When called with no arguments, returns the current date.
When called with a string argument, parses it as an ISO 8601 date.

Equivalent to Neo4j's date() function.
"""

def __init__(self) -> None:
super().__init__("date")
self._expected_parameter_count = None

def value(self) -> Any:
children = self.get_children()
if len(children) > 1:
raise ValueError("date() accepts at most one argument")

if len(children) == 1:
d = parse_temporal_arg(children[0].value(), "date")
else:
d = datetime.now()

return build_date_object(d)
64 changes: 64 additions & 0 deletions flowquery-py/src/parsing/functions/datetime_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Datetime function."""

from datetime import datetime, timezone
from typing import Any

from .function import Function
from .function_metadata import FunctionDef
from .temporal_utils import build_datetime_object, parse_temporal_arg


@FunctionDef({
"description": (
"Returns a datetime value. With no arguments returns the current UTC datetime. "
"Accepts an ISO 8601 string or a map of components (year, month, day, hour, minute, second, millisecond)."
),
"category": "scalar",
"parameters": [
{
"name": "input",
"description": "Optional. An ISO 8601 datetime string or a map of components.",
"type": "string",
"required": False,
},
],
"output": {
"description": (
"A datetime object with properties: year, month, day, hour, minute, second, millisecond, "
"epochMillis, epochSeconds, dayOfWeek, dayOfYear, quarter, formatted"
),
"type": "object",
},
"examples": [
"RETURN datetime() AS now",
"RETURN datetime('2025-06-15T12:30:00Z') AS dt",
"RETURN datetime({year: 2025, month: 6, day: 15, hour: 12}) AS dt",
"WITH datetime() AS dt RETURN dt.year, dt.month, dt.day",
],
})
class Datetime(Function):
"""Datetime function.

Returns a datetime value (date + time + timezone offset).
When called with no arguments, returns the current UTC datetime.
When called with a string argument, parses it as an ISO 8601 datetime.
When called with a map argument, constructs a datetime from components.

Equivalent to Neo4j's datetime() function.
"""

def __init__(self) -> None:
super().__init__("datetime")
self._expected_parameter_count = None

def value(self) -> Any:
children = self.get_children()
if len(children) > 1:
raise ValueError("datetime() accepts at most one argument")

if len(children) == 1:
d = parse_temporal_arg(children[0].value(), "datetime")
else:
d = datetime.now(timezone.utc)

return build_datetime_object(d, utc=True)
Loading