Source code for codegen_database.ext.comments.queries
r"""Threaded comment query helpers.
These helpers return SQLAlchemy :class:`~sqlalchemy.Select`\ s without
registering anything on metadata. Execute the result directly against
a connection. Pair with
:class:`fsh_lib.comments.CommentMixin` (the storage columns) and
:func:`fsh_lib.comments.build_comment_thread` (the pure-Python
nester) to serve a threaded comment list for any opted-in resource.
:func:`construct_comments_thread_query` returns the flat, parent-first
row stream :func:`fsh_lib.comments.build_comment_thread` nests into a
tree. The ordering is structural (every parent precedes its
children) so the nester never has to look ahead.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from sqlalchemy import Column, Table, and_, nulls_first, select
from codegen_database.errors import CodegenDatabaseValidationError
if TYPE_CHECKING:
from sqlalchemy import Select
def _resolve(
table: Table,
*,
resource_type_col: str,
resource_id_col: str,
parent_col: str,
created_at_col: str,
) -> tuple[Column[Any], Column[Any], Column[Any], Column[Any]]:
"""Validate and resolve the comment-table columns against *table*.
Args:
table: The comments table (a consumer model built on
:class:`fsh_lib.comments.CommentMixin`).
resource_type_col: Column holding the parent resource slug.
resource_id_col: Column holding the parent row's stringified id.
parent_col: Self-referential parent-comment column (nullable).
created_at_col: Timestamp column used for child ordering.
Returns:
The resolved ``(resource_type, resource_id, parent,
created_at)`` columns.
Raises:
CodegenDatabaseValidationError: If any column is missing.
"""
available = set(table.c.keys())
for name, role in (
(resource_type_col, "resource_type_col"),
(resource_id_col, "resource_id_col"),
(parent_col, "parent_col"),
(created_at_col, "created_at_col"),
):
if name not in available:
msg = f"{role} {name!r} not in table {table.name!r}"
raise CodegenDatabaseValidationError(msg)
return (
table.c[resource_type_col],
table.c[resource_id_col],
table.c[parent_col],
table.c[created_at_col],
)
[docs]
def construct_comments_thread_query( # noqa: PLR0913
table: Table,
*,
resource_type: str,
resource_id: str,
resource_type_col: str = "resource_type",
resource_id_col: str = "resource_id",
parent_col: str = "parent_comment_id",
created_at_col: str = "created_at",
) -> Select[Any]:
"""Flat, parent-first comment stream for one ``(resource, row)``.
Filters the comments table to *resource_type* / *resource_id* and
orders so every parent precedes its children: top-level comments
(``parent_comment_id IS NULL``) first by ``created_at``, then each
parent's replies by ``created_at``. Feed the result to
:func:`fsh_lib.comments.build_comment_thread` to nest it.
The ordering groups top-level comments (``parent_comment_id IS
NULL``) ahead of replies, and within each group sorts by
``created_at``. :func:`fsh_lib.comments.build_comment_thread`
nests the flat stream into a tree from an id-keyed dict, so the
only ordering that matters for the tree is the order of a
parent's replies -- which this sort keeps chronological. This
avoids a recursive CTE for the common one-level-deep thread
shape.
Generates (conceptually)::
SELECT *
FROM <table>
WHERE resource_type = :resource_type
AND resource_id = :resource_id
ORDER BY parent_comment_id NULLS FIRST, created_at
Args:
table: The comments table.
resource_type: Slug of the parent resource to filter on.
resource_id: Stringified id of the parent row to filter on.
resource_type_col: Column holding the resource slug.
resource_id_col: Column holding the parent row's id.
parent_col: Self-referential parent-comment column.
created_at_col: Timestamp column for child ordering.
Returns:
A SQLAlchemy :class:`~sqlalchemy.Select` over every column of
*table*, filtered and ordered for threading.
Raises:
CodegenDatabaseValidationError: If a referenced column is
missing.
"""
rtype, rid, parent, created = _resolve(
table,
resource_type_col=resource_type_col,
resource_id_col=resource_id_col,
parent_col=parent_col,
created_at_col=created_at_col,
)
return (
select(table)
.where(and_(rtype == resource_type, rid == resource_id))
.order_by(nulls_first(parent), created)
)
# ponytail: structural ordering instead of a recursive CTE -- correct
# for one-level-deep threads; add a ``WITH RECURSIVE`` variant if
# arbitrary-depth nesting is needed.
__all__ = ["construct_comments_thread_query"]