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"]