diff --git a/.gitignore b/.gitignore index 6c82503b..55cf710a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ tests/logs Gemfile.lock /docs/.jekyll-metadata -/docs/djongocs/assets/* \ No newline at end of file +/docs/djongocs/assets/* + +.vscode/ \ No newline at end of file diff --git a/djongo/sql2mongo/sql_tokens.py b/djongo/sql2mongo/sql_tokens.py index 81d92e9f..b8e0d0c6 100644 --- a/djongo/sql2mongo/sql_tokens.py +++ b/djongo/sql2mongo/sql_tokens.py @@ -8,6 +8,8 @@ from ..exceptions import SQLDecodeError, NotSupportedError all_token_types = U['SQLConstIdentifier', + 'SQLConstIntIdentifier', + 'SQLConstParameterizedIdentifier', 'djongo.sql2mongo.functions.CountFunc', 'djongo.sql2mongo.functions.SimpleFunc', 'SQLIdentifier', @@ -40,9 +42,14 @@ def tokens2sql(token: Token, except ValueError: yield SQLIdentifier(token[0][1], query) else: - yield SQLConstIdentifier(token, query) + # TMiles-2022.XI.18 - Specify int identifier to match original functionality. + yield SQLConstIntIdentifier(token, query) elif isinstance(token[0], Function): yield SQLFunc.token2sql(token, query) + elif token[0].ttype == tokens.Name.Placeholder: + # TMiles-2022.XI.18 + # Use a parameterized const identifier to handle newer django foreign key lookup queries. + yield SQLConstParameterizedIdentifier(token, query) else: yield SQLIdentifier(token, query) elif isinstance(token, Function): @@ -152,8 +159,19 @@ def column(self) -> str: return name +# TMiles-2022.XI.18 +# Break ConstIdentifier into Int and Parameterized subclasses to allow for django 4 +# foreign key lookups and for future expansion to other non-int constants. class SQLConstIdentifier(AliasableToken): + def __init__(self, *args): + super().__init__(*args) + + def to_mongo(self) -> dict: + return {'$literal': self.value} + +class SQLConstIntIdentifier(SQLConstIdentifier): + def __init__(self, *args): super().__init__(*args) @@ -161,8 +179,17 @@ def __init__(self, *args): def value(self) -> int: return int(self._token[0][1].value) - def to_mongo(self) -> dict: - return {'$literal': self.value} +class SQLConstParameterizedIdentifier(SQLConstIdentifier): + + def __init__(self, *args): + tok = args[0] + if (tok[0].ttype != tokens.Name.Placeholder): + raise Exception("Token is not a placeholder") + super().__init__(*args) + + @property + def value(self): + return self.query.params[self.placeholder_index(self._token[0])] class SQLComparison(SQLToken): diff --git a/tests/mock_tests/test_sqlparsing.py b/tests/mock_tests/test_sqlparsing.py index 79668778..3ffd23e8 100644 --- a/tests/mock_tests/test_sqlparsing.py +++ b/tests/mock_tests/test_sqlparsing.py @@ -1124,6 +1124,36 @@ def test_const_simple(self): ans = [(1,)] self.eval_aggregate(pipeline, return_value, ans) + # TMiles - 2022.XI.18 + # Test for the django 4.x foreign key lookups. + def test_parameterizedConst(self): + self.sql = 'SELECT %s AS "a" FROM "table1" WHERE "table1"."col2" = %s LIMIT 1' + self.params = [1,2] + pipeline = [ + { + '$match': { + 'col2': { + '$eq': 2 + } + } + }, + { + '$limit': 1 + }, + { + '$project': { + 'a': { + '$literal': 1 + } + } + }, + + ] + return_value = [{'a': 1}] + ans = [(1,)] + self.eval_aggregate(pipeline, return_value, ans) + + @skip class TestQueryUpdate(ResultQuery):