diff --git a/djaosdb/models/fields.py b/djaosdb/models/fields.py index 789fd2de974122b06634e9a45194ebc386e51788..ba2938afcb977773beac7dd4434393758ea8a83a 100644 --- a/djaosdb/models/fields.py +++ b/djaosdb/models/fields.py @@ -27,7 +27,9 @@ from django.db.models import ( ForeignKey, BigAutoField, ManyToManyField) from django.utils import version from django.db.models.fields.related import ( - RelatedField, lazy_related_operation) + RelatedField, lazy_related_operation, resolve_relation) +from django.db.models.utils import make_model_tuple +from django.db.models.deletion import CASCADE from django.db.models.fields.related_descriptors import ( ManyToManyDescriptor) from django.forms import modelform_factory @@ -1194,7 +1196,7 @@ class ListOfReferencesField(ManyToManyField): # clashes between multiple m2m fields with related_name == '+'. self.remote_field.related_name = "_%s_%s_+" % (cls.__name__.lower(), name) - super().contribute_to_class(cls, name, **kwargs) + RelatedField.contribute_to_class(self, cls, name, **kwargs) # The intermediate m2m model is not auto created if: # 1) There is a manually specified intermediate, or diff --git a/djaosdb/sql2mongo/operators.py b/djaosdb/sql2mongo/operators.py index 02f84e76f77f43653c9ac3066b076f7fce9c0754..05a0340580145208f91fee9772c82f675b7f50a4 100644 --- a/djaosdb/sql2mongo/operators.py +++ b/djaosdb/sql2mongo/operators.py @@ -73,8 +73,11 @@ class _BinaryOp(_Op): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - identifier = SQLToken.token2sql(self.statement.prev_token, self.query) - self._field = identifier.field + self._identifier = SQLToken.token2sql(self.statement.prev_token, self.query) + + @property + def _field(self): + return self._identifier.field def negate(self): raise SQLDecodeError('Negating IN/NOT IN not supported') @@ -195,6 +198,7 @@ class IsOp(_BinaryOp): def __init__(self, *args, **kwargs): super().__init__(name='IS', *args, **kwargs) + token = self.statement key = token.next() if key.match(tokens.Keyword, 'Null'): @@ -208,11 +212,24 @@ class IsOp(_BinaryOp): self.is_negated = True def to_mongo(self): + ref = None + field = self._identifier.field + if self._identifier.query.left_table != self._identifier.table: + ref = self._identifier.table + field = self._identifier.column + is_null = not self._is_null if self.is_negated else self._is_null - return { - self._field: None if is_null else {'$ne': None} + result = { + "type": "pov", + "negation": False, + "p": field, + "o": " IS ", + "v": "NULL" if is_null else "NOT NULL", } + if ref: + result["ref"] = ref + return result class BetweenOp(_BinaryOp): @@ -269,20 +286,21 @@ class NotOp(_UnaryOp): self.lhs.rhs = self.rhs -class NotNullOp(_UnaryOp): +class IsTrueOp(_UnaryOp): def __init__(self, *args, **kwargs): - super().__init__(name="NOT", *args, **kwargs) + super().__init__(name="IS", *args, **kwargs) self._identifier = SQLToken.token2sql(self.statement, self.query) + self._negation = False def negate(self): - raise SQLDecodeError + self._negation = True def evaluate(self): pass def to_mongo(self): return {"type": "pov", "p": self._identifier.field, - "o": " IS ", "v": "NOT NULL", "negation": False} + "o": "=", "v": "TRUE", "negation": self._negation} def simplify_and_or(oper, elems): @@ -316,10 +334,6 @@ class _AndOrOp(_Op): def evaluate(self): if not (self.lhs and self.rhs): - print(self) - print(self.lhs) - print(self.rhs) - print(self.rhs.to_mongo()) raise SQLDecodeError if isinstance(self.lhs, _AndOrOp): @@ -458,7 +472,7 @@ class _StatementParser: pass elif isinstance(tok, Identifier): - op = NotNullOp(tok, self.query) + op = IsTrueOp(tok, self.query) elif tok.match(tokens.Whitespace, r"\s", True): pass else: @@ -468,8 +482,6 @@ class _StatementParser: def _statement2ops(self): def link_op(): - print("0", prev_op) - print("1", op) if prev_op is not None: prev_op.rhs = op op.lhs = prev_op @@ -480,10 +492,9 @@ class _StatementParser: op = None for tok in statement: op = self._token2op(prev_op, tok, statement) - print("###", tok, op) if not op: continue - if isinstance(op, _InNotInOp) and prev_op is not None and isinstance(prev_op, NotNullOp): + if isinstance(op, (_InNotInOp, LikeOp, IsOp, NotOp, CmpOp)) and prev_op is not None and isinstance(prev_op, IsTrueOp): self._ops.remove(prev_op) prev_op = prev_op.lhs link_op() @@ -560,21 +571,25 @@ class CmpOp(_Op): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._identifier = SQLToken.token2sql(self.statement.left, self.query) + self._field_ext = None + self._operator = None if isinstance(self.statement.right, Identifier): - raise SQLDecodeError('Join using WHERE not supported') - - self._operator = OPERATOR_MAP[self.statement.token_next(0)[1].value] - - index = re_index(self.statement.right.value) - - self._constant = self.params[index] if index is not None else None - if isinstance(self._constant, dict): - self._field_ext, self._constant = next(iter(self._constant.items())) + right = SQLToken.token2sql(self.statement.right, self.query) + if right.column.lower() == "id": + self._filter_type = "back_reference" + self._constant = self._identifier.table + ## TODO reference + else: + raise SQLDecodeError('Join using WHERE not supported') else: - self._field_ext = None - + self._filter_type = "pov" + self._operator = OPERATOR_MAP[self.statement.token_next(0)[1].value] + index = re_index(self.statement.right.value) + self._constant = self.params[index] if index is not None else None + if isinstance(self._constant, dict): + self._field_ext, self._constant = next(iter(self._constant.items())) def negate(self): self.is_negated = True @@ -583,7 +598,6 @@ class CmpOp(_Op): pass def to_mongo(self): - ref = None field = self._identifier.field if self._field_ext: @@ -592,11 +606,13 @@ class CmpOp(_Op): ref = self._identifier.table field = self._identifier.column - result = {"type": "pov", + result = {"type": self._filter_type, "negation": self.is_negated, "p": field, - "o": self._operator, "v": self._constant} + + if self._operator: + resule["o"] = self._operator if ref: result["ref"] = ref diff --git a/djaosdb/sql2mongo/query.py b/djaosdb/sql2mongo/query.py index 18841d628135abe72114473b4ba78c4ff9b5cbab..5c3d0408000a873af9b637f0b6579b78427d252f 100644 --- a/djaosdb/sql2mongo/query.py +++ b/djaosdb/sql2mongo/query.py @@ -67,14 +67,16 @@ def _merge_joins_and_where(joins, where): if not joins: return joins, where - merged_joins = [join for join in joins + # split into two sets, one containing the reference and back_reference + # filters, one the remainder + remaining_joins = [join for join in joins if join and not "type" in join] refs = [join for join in joins if "type" in join and join["type"] in ["reference", "back_reference"]] if not where: - return merged_joins, _conjunction_filters(*refs) + return remaining_joins, _conjunction_filters(*refs) for ref in refs: # look for subqueries @@ -86,19 +88,33 @@ def _merge_joins_and_where(joins, where): ref["sub"]["p"] = "ID" where = None break + ## TODO else: elif ref["type"] == "back_reference": v = ref["v"] - if ("ref" in where and where["ref"]): + if ("ref" in where and where["ref"] == v): # no conjunction or disjuction ref["sub"] = where where = None break + elif "elements" in where: + for i in range(len(where["elements"])): + elem = where["elements"][i] + if (elem is not None and "ref" in elem and elem["ref"] == v): + if "v" in elem and elem["v"] == "NOT NULL": + # this is unnecessary + pass + else: + ref["sub"] = elem + del elem["ref"] + where["elements"][i] = None + break + ## TODO deeper else: raise NotImplementedError("merging conjuction or disjuction " "filters with joins") merged_where = _conjunction_filters(where, *refs) - return merged_joins, merged_where + return remaining_joins, merged_where class ReturnDocument(enum.Enum): AFTER = 1 @@ -848,7 +864,6 @@ class Query: connection: 'caosdb_client.CaosDBClient', sql: str, params: Optional[Sequence]): - self._params = params self.connection = connection self._params_index_count = -1 diff --git a/tests/test_new.py b/tests/test_new.py index 2bf1d376d6d4ca7c8d82c62d6bc9bebc911a1f96..3310a0b7664bdd7b9ee03e7eb2d9f0f814d4faf6 100644 --- a/tests/test_new.py +++ b/tests/test_new.py @@ -198,7 +198,6 @@ def test_merge_joins_and_where_backref_sub_query_id(): merged_joins, merged_where = _merge_joins_and_where(joins, where) assert merged_joins == [] - print(merged_where) assert merged_where == { "type": "back_reference", "p": "p1", @@ -407,7 +406,6 @@ def test_inner_and_outer_join(): callback, _, pipeline = select_query._to_caosdb() assert callback == connection.aggregate assert "joins" not in pipeline - print(pipeline["filter"]) assert pipeline["filter"] == { "type": "reference", 'negation': False, @@ -489,7 +487,6 @@ def test_parse_select_join_with_reverse_on_clause(): callback, _, pipeline = select_query._to_caosdb() assert callback == connection.aggregate assert "joins" not in pipeline - print(pipeline["filter"]) assert pipeline["filter"] == { "type": "back_reference", 'negation': False, @@ -531,7 +528,6 @@ def test_inner_join_with_bad_names(): assert select_query.where is not None assert select_query.where.to_mongo() == { "type": "pov", 'negation': False, - # TODO 'o': '=', 'p': 'material_id', 'ref': "B", 'v': '227'} assert select_query._needs_aggregation() is True @@ -544,27 +540,21 @@ def test_inner_join_with_bad_names(): assert inner_join.to_mongo() == { "type": "back_reference", - # TODO 'negation': False, 'p': 'language_id', - # TODO 'v': 'B'} callback, _, pipeline = select_query._to_caosdb() assert callback == connection.aggregate assert "joins" not in pipeline - print(pipeline["filter"]) assert pipeline["filter"] == { "type": "back_reference", 'negation': False, - # TODO 'p': 'language_id', - # TODO 'v': 'B', "sub": { "type": "pov", 'negation': False, 'ref': "B", - # TODO 'p': 'material_id', 'o': '=', 'v': '227' @@ -715,27 +705,26 @@ def test_user(): connection = _MockConnection(cached_record_types=cached_record_types) q = Query(connection=connection, sql=sql, params=params) caosdb_params = q._query._to_caosdb()[2] - print(caosdb_params["filter"]) assert caosdb_params["filter"] == { 'type': 'and', 'elements': [{ 'type': 'pov', 'negation': False, 'p': 'is_active', - 'o': ' IS ', - 'v': 'NOT NULL', + 'o': "=", + 'v': "TRUE", },{ 'type': 'pov', 'negation': False, 'p': 'is_staff', - 'o': ' IS ', - 'v': 'NOT NULL', + 'o': "=", + 'v': "TRUE", }, { 'type': 'pov', 'negation': False, 'p': 'is_superuser', - 'o': ' IS ', - 'v': 'NOT NULL', + 'o': "=", + 'v': "TRUE", }, {'type': 'pov', 'p': 'username', 'o': '=', @@ -744,10 +733,32 @@ def test_user(): def test_join_where(): params = ('973',) - sql = ('SELECT "Language"."id", "Language"."name", "Language"."description" FROM ' - '"Language" , "Material" WHERE (("Material"."Language"="Language"."id") AND ' - '"Language"."id" IN (%s))') - assert False, "TODO" + sql = ('SELECT "A"."p1", "A"."p2", "A"."p3" FROM ' + '"A" , "B" WHERE (("B"."A_id"="A"."id") AND ' + '"A"."id" IN (%s))') + params = ('973',) + cached_record_types = [ + "A", "B", + ] + connection = _MockConnection(cached_record_types=cached_record_types) + q = Query(connection=connection, sql=sql, params=params) + caosdb_params = q._query._to_caosdb()[2] + assert caosdb_params["projection"] == ["p1", "p2", "p3"] + print(caosdb_params["filter"]["elements"]) + assert caosdb_params["filter"] == { + "type": "and", + "elements": [{ + 'type': 'back_reference', + 'negation': False, + 'p': 'A_id', + 'v': 'B', + 'ref': 'B' + }, { + 'type': 'in', + 'p': 'id', + 'v': ['973'] + }] + } def test_select_count_group_by(): sql = """ @@ -776,7 +787,6 @@ def test_select_count_having(): params = ('973', 0) assert False, "TODO" - def test_select_count_having_multi_join(): sql = """ SELECT "Language"."id", "Language"."name", "Language"."description", @@ -800,11 +810,79 @@ def test_select_count_having_multi_join(): def test_field_not_null(): sql = """ - SELECT "Language"."id", "Language"."name", "Language"."description" - FROM "Language" - INNER JOIN "Material" - ON ("Language"."id" = "Material"."language") - WHERE ("Material"."id" IS NOT NULL - AND "Language"."id" IN (%(0)s)) + SELECT "A"."p1", "A"."p2", "A"."p3" + FROM "A" + INNER JOIN "B" + ON ("A"."id" = "B"."a_id") + WHERE ("B"."a_id" IS NOT NULL + AND "A"."id" IN (%(0)s)) """ params = ('973',) + cached_record_types = [ + "A", "B", + ] + connection = _MockConnection(cached_record_types=cached_record_types) + q = Query(connection=connection, sql=sql, params=params) + caosdb_params = q._query._to_caosdb()[2] + assert caosdb_params["projection"] == ["p1", "p2", "p3"] + assert caosdb_params["filter"] == { + "type": "and", + "elements": [{ + "type": "in", + "p": "id", + "v": ["973"], + }, { + "type": "back_reference", + "p": "a_id", + "v": "B", + "negation": False, + }] + } + +def test_query(): + sql = """ + SELECT COUNT(*) AS "__count" + FROM "A" + WHERE ("A"."p1" = %(0)s + AND ("A"."p2" iLIKE %(1)s + OR "A"."p3" iLIKE %(2)s + OR "A"."p4" iLIKE %(3)s)) + """ + params = ('v1', 'v2', 'v3', 'v4') + cached_record_types = [ + "A", + ] + connection = _MockConnection(cached_record_types=cached_record_types) + q = Query(connection=connection, sql=sql, params=params) + caosdb_params = q._query._to_caosdb()[2] + assert caosdb_params["count"] == "__count" + assert caosdb_params["filter"] == { + "type": "and", + "elements": [{ + "type": "pov", + "negation": False, + "p": "p1", + "o": "=", + "v": "v1", + }, { + "type": "or", + "elements": [{ + "type": "pov", + "negation": False, + "p": "p2", + "o": " LIKE ", + "v": "v2", + }, { + "type": "pov", + "negation": False, + "p": "p3", + "o": " LIKE ", + "v": "v3", + }, { + "type": "pov", + "negation": False, + "p": "p4", + "o": " LIKE ", + "v": "v4", + }], + }]}