diff --git a/docs/queries/queries.md b/docs/queries/queries.md index ba790294..18b936cb 100644 --- a/docs/queries/queries.md +++ b/docs/queries/queries.md @@ -1097,7 +1097,7 @@ The pendant in a model are `identifying_clauses`. query = Model.query.filter(id=1) # ensures that the db connection doesn't drop during operation async with query.database as database: - # when using joins a exist subquery is generated + # when using joins a subquery is generated expression = query.table.select().where(await query.build_where_clause()) # as generic sql print(str(expression)) @@ -1124,6 +1124,10 @@ async with model.database as database: If you want raw sql see the print statements. You most probably want a dialect specific sql string for non-basic sql types because otherwise some features are not supported or cause warnings. +## Debugging + +QuerySet contains a cached debug property named `sql` which contains the QuerySet as query with inserted blanks. + [model]: ../models.md [managers]: ../managers.md [lambda statements](https://docs.sqlalchemy.org/en/20/core/sqlelement.html#sqlalchemy.sql.expression.lambda_stmt) diff --git a/docs/release-notes.md b/docs/release-notes.md index 5370f097..d44bc657 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ hide: - Breaking: Functions passed to filter functions reveive now a second positional parameter `tables_and_models`. - `build_where_clause` conditionally uses a subquery. - Rename QueryType to QuerySetType. The old name stays as an alias. +- The debug property of QuerySet named `sql` inserts now the blanks and uses the dialect. ### Fixed diff --git a/edgy/core/db/models/mixins/row.py b/edgy/core/db/models/mixins/row.py index ecaee888..400b182a 100644 --- a/edgy/core/db/models/mixins/row.py +++ b/edgy/core/db/models/mixins/row.py @@ -251,6 +251,9 @@ async def __set_prefetch( ) -> None: model_key = () if related._is_finished: + # when force_rollback + # we can only bake after all rows are retrieved + # this is why it is here await related.init_bake(type(model)) model_key = model.create_model_key() if model_key in related._baked_results: diff --git a/edgy/core/db/querysets/base.py b/edgy/core/db/querysets/base.py index df514290..48229f99 100644 --- a/edgy/core/db/querysets/base.py +++ b/edgy/core/db/querysets/base.py @@ -189,7 +189,7 @@ def _clone(self) -> "QuerySet": """ queryset = self.__class__( self.model_class, - database=self.database, + database=getattr(self, "_database", None), filter_clauses=self.filter_clauses, select_related=self._select_related, prefetch_related=self._prefetch_related, @@ -336,9 +336,8 @@ def _build_tables_join_from_relationship( """ # How does this work? - # First we build a transitions table with a dependency, so we find a path - # Secondly we check if a select_related path is joining a table from the set in an opposite direction - # If yes, we mark the transition for a full outer join (dangerous, there could be side-effects) + # First we build a transitions tree (maintable is root) by providing optionally a dependency. + # Resolving a dependency automatically let's us resolve the tree. # At last we iter through the transisitions and build their dependencies first # We pop out the transitions so a path is not taken 2 times @@ -362,7 +361,14 @@ def _build_tables_join_from_relationship( former_table = maintable former_transition = None prefix: str = "" + # prefix which expands m2m fields _select_prefix: str = "" + # False (default): add prefix regularly. + # True: skip adding existing prefix to tables_and_models and skip adding the next field to the + # public prefix. + # string: add a custom prefix instead of the calculated one and skip adding the next field to the + # public prefix. + injected_prefix: Union[bool, str] = False model_database: Optional[Database] = self.database while select_path: @@ -424,6 +430,8 @@ def _build_tables_join_from_relationship( table = model_class.table_schema(self.active_schema) table = table.alias(hash_tablekey(tablekey=table.key, prefix=prefix)) + # it is guranteed that former_table is either root and has a key or is an unique join node + # except there would be a hash collision which is very unlikely transition_key = (get_table_key_or_name(former_table), table.name, field_name) if transition_key in transitions: # can not provide new informations @@ -1041,14 +1049,16 @@ class QuerySet(BaseQuerySet): QuerySet object used for query retrieving. Public interface """ + async def _sql_helper(self) -> Any: + async with self.database: + return (await self.as_select()).compile( + self.database.engine, compile_kwargs={"literal_binds": True} + ) + @cached_property def sql(self) -> str: - """Get SQL select query as string.""" - return str( - run_sync(self.as_select()).compile( - compile_kwargs={"literal_binds": True}, - ) - ) + """Get SQL select query as string with inserted blanks. For debugging only!""" + return str(run_sync(self._sql_helper())) def filter( self,