From 36f9a6fdef33cc6ff5d2a9acbd883d32ce5f18e6 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Tue, 19 May 2026 20:13:50 -0700 Subject: [PATCH] feat(assets): preserve insertion order on tag retrieval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/assets response previously sorted tags alphabetically via .order_by(Tag.name.asc()). That breaks the structurally meaningful "root category first, then subpath" invariant the path-collapsing change relies on: alphabetical sort puts a custom user tag (or even the bare "models" root) at unpredictable positions, so positional access like tags[1] is not reliable on local. Cloud already preserves insertion order — its Ent WithTags() eager- load has no explicit ORDER BY, so Postgres returns rows in physical insertion order. Local's composite primary key on (asset_reference_id, tag_name) means SQLite walks the index in tag_name order even without an explicit ORDER BY, so just dropping the clause isn't enough. Switching to ORDER BY added_at ASC, tag_name ASC keeps the path tags inserted via set_reference_tags in their original order (microsecond-resolution timestamps disambiguate same-batch inserts; tag_name is a deterministic tiebreaker for the rare collision case). Custom tags added later via add_tags_to_reference land after the path tags in their own added_at bucket. Applies to both response-shaping queries: - list_references_page (GET /api/assets, tag_map join) - fetch_reference_asset_and_tags (GET /api/assets/{id}) Catalog/histogram queries in app/assets/database/queries/tags.py keep their alphabetical sort — those endpoints are listing all tags, not per-asset tags, and alphabetical is the right shape there. --- app/assets/database/queries/asset_reference.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/database/queries/asset_reference.py b/app/assets/database/queries/asset_reference.py index 8b90ae511..2ef5c210e 100644 --- a/app/assets/database/queries/asset_reference.py +++ b/app/assets/database/queries/asset_reference.py @@ -327,7 +327,12 @@ def list_references_page( select(AssetReferenceTag.asset_reference_id, Tag.name) .join(Tag, Tag.name == AssetReferenceTag.tag_name) .where(AssetReferenceTag.asset_reference_id.in_(id_list)) - .order_by(AssetReferenceTag.tag_name.asc()) + # Preserve insertion order so the structural first tag (the root + # category like "models") stays in position 0 and the path-derived + # sub-path tag stays in position 1, matching cloud's behavior. + # tag_name is a deterministic tiebreaker when multiple tags share + # an added_at (same-batch insert via set_reference_tags). + .order_by(AssetReferenceTag.added_at.asc(), AssetReferenceTag.tag_name.asc()) ) for ref_id, tag_name in rows.all(): tag_map[ref_id].append(tag_name) @@ -355,7 +360,8 @@ def fetch_reference_asset_and_tags( build_visible_owner_clause(owner_id), ) .options(noload(AssetReference.tags)) - .order_by(Tag.name.asc()) + # See list_references_page for the rationale behind ordering by added_at. + .order_by(AssetReferenceTag.added_at.asc(), Tag.name.asc()) ) rows = session.execute(stmt).all()