Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def __init__(self, connection: "Connection", grammar: Any, processor: Any):

self._columns: list[Any] = []
self._table = ""
self._alias: str | None = None
self._limit = False
self._offset = False
self._wheres = []
Expand Down Expand Up @@ -111,10 +112,11 @@ def with_(self, *eagers) -> "Self":
return self

def get_table_name(self) -> str:
return self._table
return self._alias or self._table

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: get_table_name() now returns the alias. That works for every relationship caller I traced. Note that a callback qualifying the real table name (e.g. q.where("members.name", ...)) now binds to the outer row instead of the inner one. Please document this and pin it with a test.


def table(self, table: str) -> "Self":
def table(self, table: str, alias: str | None = None) -> "Self":
self._table = table
self._alias = alias
return self

def where_in(self, column: str, values: Iterable[Any]) -> "Self":
Expand Down Expand Up @@ -256,6 +258,7 @@ def get_grammar(self):
return self.grammar(
columns=self._columns,
table=self._table,
alias=self._alias,
limit=self._limit,
offset=self._offset,
wheres=self._wheres,
Expand Down Expand Up @@ -550,7 +553,7 @@ def new(self):
# subgroup) prefixes its columns correctly instead of rendering a
# table-less ."column". Callers that want a different table override it
# with .table(...) as usual.
return self.connection.query().table(self._table)
return self.connection.query().table(self._table, self._alias)

def invalid_operator(self, operator: Any) -> bool:
"""Determine whether an operator is not supported by the builder."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ def __init__(
lock=False,
having=(),
connection_details=None,
alias=None,
):
self._columns = columns
self.table = table
self.alias = alias
self.database = database
self._wheres = wheres
self._limit = limit
Expand Down Expand Up @@ -161,7 +163,7 @@ def _compile_select(self, qmark=False):
.format(
columns=self.process_columns(separator=", ", qmark=qmark),
keyword="DISTINCT" if self._distinct else "",
table=self.process_table(self.table),
table=self.process_from_table(),
joins=self.process_joins(qmark=qmark),
wheres=self.process_wheres(qmark=qmark),
limit=self.process_limit(),
Expand Down Expand Up @@ -560,6 +562,11 @@ def process_table(self, table):
for t in table.name.split(".")
)

def process_from_table(self):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Only SELECT honours the alias, and insert/update/delete still use self.table. That's fine because only exists builders get aliased, but please state it in the ADR.

if not self.alias:
return self.process_table(self.table)
return f"{self.process_table(self.table)} AS {self.process_table(self.alias)}"

def process_limit(self):
"""Compiles the limit expression.

Expand Down Expand Up @@ -960,7 +967,7 @@ def process_column(self, column, separator=""):
table = None
if column and "." in column:
table, column = column.split(".")
return self.column_string().format(column=column, separator=separator, table=table or self.table)
return self.column_string().format(column=column, separator=separator, table=table or self.alias or self.table)

def _table_column_string(self, column, alias=None, separator=""):
"""Compiles a column into the column syntax.
Expand All @@ -982,15 +989,15 @@ def _table_column_string(self, column, alias=None, separator=""):
return self.column_strings["select_all"].format(
column=column,
separator=separator,
table=self.process_table(table or self.table),
table=self.process_table(table or self.alias or self.table),
)

alias_string = " " + self.subquery_alias_string().format(alias=alias) if alias else ""
return self.column_strings[self._action].format(
column=column,
separator=separator,
alias=alias_string,
table=self.process_table(table or self.table),
table=self.process_table(table or self.alias or self.table),
)

def _compile_value(self, value, separator=""):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ def model_reference(self) -> str | type:
def get_builder(self):
return self.fn()().get_builder()

def exists_alias(self, query, parent):

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker (process): this core relationship change needs an ADR in docs/adr/ plus an index entry in docs/adr/AGENTS.md. The ADR should cover why the alias is derived as <parent>_self rather than passed in by the caller, and it should mention that a real table named <table>_self would collide.

if query.get_table_name() == parent._table:
return f"{parent.get_table_name()}_self"
return None

def get_exists_builder(self, parent):
query = self.get_builder()
return query.table(query.get_table_name(), self.exists_alias(query, parent))

@overload
def __get__(self, instance: None, owner: Any) -> Self: ...
@overload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def apply_query(self, foreign, owner):
return foreign.where(self.foreign_key, owner.__attributes__[self.local_key]).first()

def query_has(self, current_query_builder, method="where_exists"):
related_builder = self.get_builder()
related_builder = self.get_exists_builder(current_query_builder)

getattr(current_query_builder, method)(
related_builder.where_column(
Expand All @@ -43,7 +43,7 @@ def query_has(self, current_query_builder, method="where_exists"):
return related_builder

def query_where_exists(self, builder, callback, method="where_exists"):
query = self.get_builder()
query = self.get_exists_builder(builder)
getattr(builder, method)(
callback(
query.where_column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .BaseRelationship import BaseRelationship, ManyResult, TModel
from ..collection import Collection
from ..models.builder import QueryBuilder
from ..models.pivot import Pivot


Expand Down Expand Up @@ -401,43 +402,32 @@ def joins(self, builder, clause=None):
return result

def query_where_exists(self, builder, callback, method="where_exists"):
query = self.get_builder()
pivot_table = self._table or self.get_pivot_table_name(query, builder)
table = self.get_builder().get_table_name()

getattr(builder, method)(
query.new()
.table(table)
.join(
f"{pivot_table}",
f"{table}.{self.other_owner_key}",
"=",
f"{pivot_table}.{self.foreign_key}",
)
.where_column(
f"{pivot_table}.{self.local_key}",
f"{builder.get_table_name()}.{self.local_owner_key}",
related = self.related_exists_query(builder)
constrained = callback(related)
if isinstance(constrained, QueryBuilder) and constrained is not related:
raise TypeError(
"where_has callback must constrain and return the query it receives, not a different QueryBuilder"
)
.where_in(self.other_owner_key, callback(query.select(self.other_owner_key)))
)
if constrained is not None and constrained is not related:
related.where_in(f"{related.get_table_name()}.{self.other_owner_key}", constrained)
return getattr(builder, method)(related)

def query_has(self, current_query_builder, method="where_exists"):
return getattr(current_query_builder, method)(self.related_exists_query(current_query_builder))

def related_exists_query(self, builder):
query = self.get_builder()
pivot_table = self._table or self.get_pivot_table_name(query, current_query_builder)
table = self.get_builder().get_table_name()
return getattr(current_query_builder, method)(
query.new()
.table(table)
.join(
f"{pivot_table}",
f"{table}.{self.other_owner_key}",
"=",
f"{pivot_table}.{self.foreign_key}",
)
.where_column(
f"{pivot_table}.{self.local_key}",
f"{current_query_builder.get_table_name()}.{self.local_owner_key}",
)
pivot_table = self._table or self.get_pivot_table_name(query, builder)
related = query.new().table(query.get_table_name(), self.exists_alias(query, builder))
table = related.get_table_name()
return related.join(
f"{pivot_table}",
f"{table}.{self.other_owner_key}",
"=",
f"{pivot_table}.{self.foreign_key}",
).where_column(
f"{pivot_table}.{self.local_key}",
f"{builder.get_table_name()}.{self.local_owner_key}",
)

def get_pivot_table_name(self, query, builder):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async def attach(self, current_model, related_record):
return await related_record.update({self.foreign_key: local_key_value})

def query_has(self, current_query_builder, method="where_exists"):
related_builder = self.get_builder()
related_builder = self.get_exists_builder(current_query_builder)

getattr(current_query_builder, method)(
related_builder.where_column(
Expand All @@ -52,7 +52,7 @@ def query_has(self, current_query_builder, method="where_exists"):
return related_builder

def query_where_exists(self, builder, callback, method="where_exists"):
query = self.get_builder()
query = self.get_exists_builder(builder)
getattr(builder, method)(
callback(
query.where_column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def map_related(self, related_result):
return related_result

def query_has(self, current_query_builder, method="where_exists"):
related_builder = self.get_builder()
related_builder = self.get_exists_builder(current_query_builder)

getattr(current_query_builder, method)(
related_builder.where_column(
Expand All @@ -58,7 +58,7 @@ def register_related(self, key, model, collection):
model.add_relation({key: related or None})

def query_where_exists(self, builder, callback, method="where_exists"):
query = self.get_builder()
query = self.get_exists_builder(builder)
getattr(builder, method)(
callback(
query.where_column(
Expand Down
Loading
Loading