Repository: RDFLib/rdflib

Issue:
sparql builtin date functions only work with datetime, not with date
the functions `day`, `month` and `year` only work with datetime literals, not with date literals.

Example to reproduce:
```
import rdflib
import datetime

ns = rdflib.Namespace("http://example.com/")

graph = rdflib.Graph()

graph.add((ns["id:1"], ns["date"], rdflib.Literal(datetime.date(2016,8, 12))))

result = graph.query(
"""
SELECT ?a ?f
WHERE {
    ?a e:date ?c .
    BIND(DAY(?c) AS ?f).
}

""",
initNs={"e": ns}
)

for row in result:
    print(row)
```

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/33118429/badge)](https://coveralls.io/builds/33118429)

Coverage increased (+0.01%) to 75.595% when pulling **51a17fd407d901703d3e0dd063e0999ddcfab3c5 on FlorianLudwig:support_date_type** into **652d2e60eff064bfa803474919c88c0e84134cef on RDFLib:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/33118429/badge)](https://coveralls.io/builds/33118429)

Coverage increased (+0.01%) to 75.595% when pulling **51a17fd407d901703d3e0dd063e0999ddcfab3c5 on FlorianLudwig:support_date_type** into **652d2e60eff064bfa803474919c88c0e84134cef on RDFLib:master**.

white-gecko: According to the SPARQL 1.1 Specification the builtin functions are only defined on `xsd:dataTime` not on `xsd:date`. So I'm not sure if it is a good idea to extend to to `xsd:date` as well. If we want to extend it we might also want to extend it to `xsd:gDay`, `xsd:gMonth`, `xsd:gMonthDay`, `xsd:gYear`, and `xsd:gYearMonth`. While some of the builtins would result in an empty string in some cases.
FlorianLudwig: @white-gecko I am no expert on the specs, I searched through it and could not find any specification on what to do if the argument is of the wrong type (`date` vs `dateTime`) - except for the string-function it is explicitly said that it should result in an error if the argument is not a string. So this might be undefined behaviour?

If you would be interested in merging this I would look into supporting the other types as well and update this MR.

Note: `xsd:date` (and others) are not part of sparql 1.1 at all. Interestingly the first - and so far only - written [enhancement proposal for 1.2](https://github.com/w3c/sparql-12/blob/master/SEP/SEP-0002/sep-0002.md#specification). So I guess there seems to be interest in this from others as well :)
white-gecko: I have seen it in different implementations that only `dateTime` was supported for these builtins. So it is a tough decision to make weather to support all of the extra datatype or non.
Maybe we should introduce a new switch for this behavior like:
https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.plugins.sparql.html#module-rdflib.plugins.sparql

    rdflib.plugins.sparql.EXTRA_DATA_TYPES = true

or

    rdflib.plugins.sparql.EXTRA_DATE_DATA_TYPES = true
FlorianLudwig: @white-gecko to be honest i have developed a dislike for switches in software as they complicate code and testing as resulting combinations do increase exponentially. So **if** it does not clash with the specs I prefer to not have a switch.

I would really like to simplify the code base for sparql evaluation a bit - I am running with a few local changes which gave my use case 25% performance boost - and I would like to work on it a bit more, as it is still to slow for my use case. Those where really low hanging fruit and I am sure there is more. Introducing a switch means supporting more different combinations - and probably breaking those, making it harder to move the code forward. See #1155 for an example of this - my local refactoring broke the semantic of comparing a `Literal` with `None` - keeping those would have made the code more complex for a benefit I did see (or understand). 
white-gecko: In an informative section the standard says.

> 3.3 Other Term Constraints
> 
> In addition to numeric types, SPARQL supports types xsd:string, xsd:boolean and xsd:dateTime (see Operand Data Types). Section Operator Mapping describes the operators and section Function Definitions the functions that can be that can be applied to RDF terms.

Based on this I can not judge if we should support the additional types or not.

Your argument that it might be supported in SPARQL 1.2 sounds good to me.

Anyhow I ask @ashleysommer and @nicholascar for their opinion.
ashleysommer: >I am running with a few local changes which gave my use case 25% performance boost - and I would like to work on it a bit more, as it is still to slow for my use case

That sounds great! I've had a mental note for months to go in to see where SPARQL could be made faster in RDFLib.
It is the only thing that significantly slows down execution of [PySHACL](https://github.com/RDFLib/pySHACL). When not using any SPARQL features, the SHACL validation engine executes approx 10x faster than when SPARQL features are used.

Patch:
diff --git a/rdflib/plugins/sparql/operators.py b/rdflib/plugins/sparql/operators.py
index 29bdd5c0..3393f18a 100644
--- a/rdflib/plugins/sparql/operators.py
+++ b/rdflib/plugins/sparql/operators.py
@@ -12,6 +12,7 @@ import math
 import random
 import uuid
 import hashlib
+import datetime as py_datetime  # naming conflict with function within this module
 
 from functools import reduce
 
@@ -449,17 +450,17 @@ def Builtin_NOW(e, ctx):
 
 
 def Builtin_YEAR(e, ctx):
-    d = datetime(e.arg)
+    d = date(e.arg)
     return Literal(d.year)
 
 
 def Builtin_MONTH(e, ctx):
-    d = datetime(e.arg)
+    d = date(e.arg)
     return Literal(d.month)
 
 
 def Builtin_DAY(e, ctx):
-    d = datetime(e.arg)
+    d = date(e.arg)
     return Literal(d.day)
 
 
@@ -998,6 +999,17 @@ def datetime(e):
     return e.toPython()
 
 
+def date(e) -> py_datetime.date:
+    if not isinstance(e, Literal):
+        raise SPARQLError("Non-literal passed as date: %r" % e)
+    if e.datatype not in (XSD.date, XSD.dateTime):
+        raise SPARQLError("Literal with wrong datatype passed as date: %r" % e)
+    result = e.toPython()
+    if isinstance(result, py_datetime.datetime):
+        return result.date()
+    return result
+
+
 def string(s):
     """
     Make sure the passed thing is a string literal


Test patch:
diff --git a/test/test_sparql_operators.py b/test/test_sparql_operators.py
new file mode 100644
index 00000000..b4e4dacd
--- /dev/null
+++ b/test/test_sparql_operators.py
@@ -0,0 +1,36 @@
+import datetime
+
+import nose.tools
+
+import rdflib
+from rdflib.plugins.sparql import operators
+from rdflib.plugins.sparql import sparql
+
+
+def test_date_cast():
+    now = datetime.datetime.now()
+    today = now.date()
+
+    literal = rdflib.Literal(now)
+    result = operators.date(literal)
+    assert isinstance(result, datetime.date)
+    assert result == today
+
+    literal = rdflib.Literal(today)
+    result = operators.date(literal)
+    assert isinstance(result, datetime.date)
+    assert result == today
+
+
+def test_datetime_cast():
+    now = datetime.datetime.now()
+    literal = rdflib.Literal(now)
+    result = operators.datetime(literal)
+    assert isinstance(result, datetime.datetime)
+    assert result == now
+
+
+@nose.tools.raises(sparql.SPARQLError)
+def test_datetime_cast_type_error():
+    literal = rdflib.Literal("2020-01-02")
+    operators.date(literal)