diff --git a/.gitignore b/.gitignore index d24777ab..675fec69 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ Gemfile.lock /docs/.jekyll-metadata /docs/djongocs/assets/* -*.map \ No newline at end of file +*.maptmp/ diff --git a/djongo/__init__.py b/djongo/__init__.py index f350239f..85a24b43 100644 --- a/djongo/__init__.py +++ b/djongo/__init__.py @@ -1,2 +1,2 @@ -__version__ = '1.3.7' +__version__ = '1.4.0' diff --git a/djongo/features.py b/djongo/features.py index acfee1e3..0a38f995 100644 --- a/djongo/features.py +++ b/djongo/features.py @@ -12,3 +12,17 @@ class DatabaseFeatures(BaseDatabaseFeatures): test_db_allows_multiple_connections = False supports_unspecified_pk = True + # Django 3.1+ features + supports_json_field = True + + # Django 4.0+ features + supports_expression_defaults = False + supports_table_check_constraints = False + supports_column_check_constraints = False + can_return_columns_from_insert = False + can_return_rows_from_bulk_insert = False + + # Django 4.1+ features + supports_comments = False + supports_comments_inline = False + diff --git a/djongo/operations.py b/djongo/operations.py index 3a9abfb3..96f1264a 100644 --- a/djongo/operations.py +++ b/djongo/operations.py @@ -96,8 +96,9 @@ def get_db_converters(self, expression): converters.append(self.convert_datetimefield_value) return converters - def sql_flush(self, style, tables, reset_sequences, allow_cascade=False): + def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False, **kwargs): # TODO: Need to implement this fully + # Note: **kwargs added for Django 4.1+ compatibility (sequences parameter) return [f'ALTER TABLE "{table}" FLUSH' for table in tables] diff --git a/djongo/sql2mongo/converters.py b/djongo/sql2mongo/converters.py index 08230a82..eff685dc 100644 --- a/djongo/sql2mongo/converters.py +++ b/djongo/sql2mongo/converters.py @@ -276,10 +276,10 @@ def __init__(self, *args): def parse(self): tok = self.statement.next() - if not tok.match(tokens.Keyword, 'BY'): - raise SQLDecodeError - - tok = self.statement.next() + # In sqlparse >= 0.5, 'ORDER BY' is a single token, so there's no separate 'BY' + # In sqlparse < 0.5, 'ORDER' and 'BY' are separate tokens + if tok.match(tokens.Keyword, 'BY'): + tok = self.statement.next() self.columns.extend(SQLToken.tokens2sql(tok, self.query)) def to_mongo(self): @@ -443,9 +443,10 @@ def __init__(self, *args): def parse(self): tok = self.statement.next() - if not tok.match(tokens.Keyword, 'BY'): - raise SQLDecodeError - tok = self.statement.next() + # In sqlparse >= 0.5, 'GROUP BY' is a single token, so there's no separate 'BY' + # In sqlparse < 0.5, 'GROUP' and 'BY' are separate tokens + if tok.match(tokens.Keyword, 'BY'): + tok = self.statement.next() self.sql_tokens.extend(SQLToken.tokens2sql(tok, self.query)) def to_mongo(self): diff --git a/djongo/sql2mongo/query.py b/djongo/sql2mongo/query.py index fefb52ca..76966e03 100644 --- a/djongo/sql2mongo/query.py +++ b/djongo/sql2mongo/query.py @@ -20,6 +20,12 @@ Where, Statement) +# Try to import Values for sqlparse >= 0.4.0 +try: + from sqlparse.sql import Values +except ImportError: + Values = None + from ..exceptions import SQLDecodeError, MigrationError, print_warn from .functions import SQLFunc from .sql_tokens import (SQLToken, SQLStatement, SQLIdentifier, @@ -128,7 +134,8 @@ def parse(self): elif tok.match(tokens.Keyword, 'LIMIT'): self.limit = LimitConverter(self, statement) - elif tok.match(tokens.Keyword, 'ORDER'): + elif tok.match(tokens.Keyword, 'ORDER') or tok.match(tokens.Keyword, 'ORDER BY'): + # Handle both 'ORDER' (sqlparse < 0.5) and 'ORDER BY' (sqlparse >= 0.5) self.order = OrderConverter(self, statement) elif tok.match(tokens.Keyword, 'OFFSET'): @@ -142,7 +149,8 @@ def parse(self): converter = OuterJoinConverter(self, statement) self.joins.append(converter) - elif tok.match(tokens.Keyword, 'GROUP'): + elif tok.match(tokens.Keyword, 'GROUP') or tok.match(tokens.Keyword, 'GROUP BY'): + # Handle both 'GROUP' (sqlparse < 0.5) and 'GROUP BY' (sqlparse >= 0.5) self.groupby = GroupbyConverter(self, statement) elif tok.match(tokens.Keyword, 'HAVING'): @@ -356,17 +364,26 @@ def _columns(self, statement: SQLStatement): def _fill_values(self, statement: SQLStatement): for tok in statement: if isinstance(tok, Parenthesis): - placeholder = SQLToken.token2sql(tok, self) - values = [] - for index in placeholder: - if isinstance(index, int): - values.append(self.params[index]) - else: - values.append(index) - self._values.append(values) + self._process_values_parenthesis(tok) + elif Values is not None and isinstance(tok, Values): + # sqlparse >= 0.4.0 wraps VALUES (...) in a Values token + for subtok in tok.tokens: + if isinstance(subtok, Parenthesis): + self._process_values_parenthesis(subtok) elif not tok.match(tokens.Keyword, 'VALUES'): raise SQLDecodeError + def _process_values_parenthesis(self, tok): + """Process a Parenthesis token containing values.""" + placeholder = SQLToken.token2sql(tok, self) + values = [] + for index in placeholder: + if isinstance(index, int): + values.append(self.params[index]) + else: + values.append(index) + self._values.append(values) + def execute(self): docs = [] num = len(self._values) diff --git a/djongo/storage.py b/djongo/storage.py index b8a5d59c..481bda3c 100644 --- a/djongo/storage.py +++ b/djongo/storage.py @@ -16,8 +16,8 @@ def _get_subcollections(collection): """ Returns all sub-collections of `collection`. """ - # XXX: Use the MongoDB API for this once it exists. - for name in collection.database.collection_names(): + # Use list_collection_names() which is available in PyMongo 3.7+ + for name in collection.database.list_collection_names(): cleaned = name[:name.rfind('.')] if cleaned != collection.name and cleaned.startswith(collection.name): yield cleaned diff --git a/pyproject.toml b/pyproject.toml index 2775db3f..f0770492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,9 @@ requires = ["setuptools"] dynamic = ["version", "optional-dependencies"] name = "djongo" dependencies = [ - 'sqlparse==0.2.4', - 'pymongo>=3.7.0,<=3.11.4', - 'django>=2.1,<=3.1.12', + 'sqlparse>=0.4.4', + 'pymongo>=3.7.0', + 'django>=2.1', 'pytz>=2018.5' ] authors = [ @@ -16,12 +16,22 @@ authors = [ ] license= {text = "AGPL"} keywords = ["Django", "Djongo", "MongoDB", "driver", "connector"] -requires-python = ">=3.6" +requires-python = ">=3.8" classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: 3.6', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.0', + 'Framework :: Django :: 4.1', + 'Framework :: Django :: 4.2', + 'Framework :: Django :: 5.0', + 'Framework :: Django :: 5.1', ] description = "Djongo: The Django MongoDB connector" readme = "README.md" @@ -30,5 +40,9 @@ Homepage = "https://www.djongomapper.com/" Documentation = "https://www.djongomapper.com/docs/" Repository = "https://github.com/doableware/djongo.git" +[tool.setuptools.packages.find] +include = ["djongo*"] +exclude = ["tmp*", "docs*", "tests*"] + [tool.setuptools.dynamic] version = {attr = "djongo.__version__"} \ No newline at end of file