Skip to content

Commit

Permalink
support geo.{intersects, distance, length}
Browse files Browse the repository at this point in the history
updated query parsing for geo.* functions, and added django backend
implementation.

-add GEOGRAPHY token and Geography AST node
-account for identifier namespace in various places
-add django implementation using django.contrib.gis
-use '__' for namespace separator in backend function names
-fix geo.distance number of args
  • Loading branch information
srepmub committed Nov 29, 2023
1 parent 06d763e commit 52069fe
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 5 deletions.
11 changes: 11 additions & 0 deletions odata_query/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Identifier(_Node):
name: str
namespace: Tuple[str, ...] = field(default_factory=tuple)

def full_name(self):
return '.'.join(self.namespace + (self.name,))


@dataclass(frozen=True)
class Attribute(_Node):
Expand Down Expand Up @@ -81,6 +84,14 @@ def py_val(self) -> str:
return self.val


@dataclass(frozen=True)
class Geography(_Literal):
val: str

def wkt(self):
return self.val


@dataclass(frozen=True)
class Date(_Literal):
val: str
Expand Down
19 changes: 17 additions & 2 deletions odata_query/django/django_q.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
)
from django.db.models.expressions import Expression

from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.db.models import functions as gis_functions

from odata_query import ast, exceptions as ex, typing, utils, visitor

from .django_q_ext import NotEqual
Expand Down Expand Up @@ -254,10 +257,13 @@ def visit_UnaryOp(self, node: ast.UnaryOp) -> str:

def visit_Call(self, node: ast.Call) -> Union[Expression, Q]:
":meta private:"

func_name = node.func.full_name().replace('.', '__')

try:
q_gen = getattr(self, "djangofunc_" + node.func.name.lower())
q_gen = getattr(self, "djangofunc_" + func_name.lower())
except AttributeError:
raise ex.UnsupportedFunctionException(node.func.name)
raise ex.UnsupportedFunctionException(func_name)

res = q_gen(*node.args)
return res
Expand Down Expand Up @@ -303,6 +309,15 @@ def visit_CollectionLambda(self, node: ast.CollectionLambda) -> Q:
else:
raise NotImplementedError()

def djangofunc_geo__intersects(self, a, b):
return Q(**{a.name + '__' + 'intersects': GEOSGeometry(b.wkt())})

def djangofunc_geo__distance(self, a, b):
return gis_functions.Distance(a.name, GEOSGeometry(b.wkt()))

def djangofunc_geo__length(self, a):
return gis_functions.Length(a.name)

def djangofunc_contains(
self, field: ast._Node, substr: ast._Node
) -> lookups.Contains:
Expand Down
15 changes: 13 additions & 2 deletions odata_query/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"floor": 1,
"ceiling": 1,
# Geo functions
"geo.distance": 1,
"geo.distance": 2,
"geo.length": 1,
"geo.intersects": 2,
# Set functions
Expand All @@ -67,6 +67,7 @@ class ODataLexer(Lexer):
"ODATA_IDENTIFIER",
"NULL",
"STRING",
"GEOGRAPHY",
"GUID",
"DATETIME",
"DATE",
Expand Down Expand Up @@ -143,6 +144,13 @@ def STRING(self, t):
t.value = ast.String(val)
return t

@_(r"geography'(?:[^']|'')*'")
def GEOGRAPHY(self, t):
":meta private:"

t.value = ast.Geography(t.value[10:-1])
return t

@_(r"[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}")
def GUID(self, t):
":meta private:"
Expand Down Expand Up @@ -375,6 +383,7 @@ def common_expr(self, p):
"INTEGER",
"DECIMAL",
"STRING",
"GEOGRAPHY",
"BOOLEAN",
"GUID",
"DATE",
Expand Down Expand Up @@ -551,7 +560,9 @@ def common_expr(self, p):
####################################################################################
def _function_call(self, func: ast.Identifier, args: List[ast._Node]):
":meta private:"
func_name = func.name

func_name = func.full_name()

try:
n_args_exp = ODATA_FUNCTIONS[func_name]
except KeyError:
Expand Down
2 changes: 1 addition & 1 deletion odata_query/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def infer_return_type(node: ast.Call) -> Optional[Type[ast._Node]]:
Returns:
The inferred type or ``None`` if unable to infer.
"""
func = node.func.name
func = node.func.full_name()

if func in (
"contains",
Expand Down

0 comments on commit 52069fe

Please sign in to comment.