This commit is contained in:
Timothy Jaeryang Baek
2026-04-17 14:09:35 +09:00
parent f44b7a01f5
commit e7e752f8e7
+39 -34
View File
@@ -3,7 +3,7 @@ import time
import uuid import uuid
from typing import Optional from typing import Optional
from sqlalchemy import select, delete, update, or_, func, cast, String from sqlalchemy import select, delete, update, or_, func, text, cast, String
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from open_webui.internal.db import Base, JSONField, get_async_db_context from open_webui.internal.db import Base, JSONField, get_async_db_context
from open_webui.models.groups import Groups from open_webui.models.groups import Groups
@@ -260,12 +260,12 @@ class PromptsTable:
) -> PromptListResponse: ) -> PromptListResponse:
async with get_async_db_context(db) as db: async with get_async_db_context(db) as db:
# Join with User table for user filtering and sorting # Join with User table for user filtering and sorting
stmt = select(Prompt, User).outerjoin(User, User.id == Prompt.user_id) query = select(Prompt, User).outerjoin(User, User.id == Prompt.user_id)
if filter: if filter:
query_key = filter.get('query') query_key = filter.get('query')
if query_key: if query_key:
stmt = stmt.filter( query = query.filter(
or_( or_(
Prompt.name.ilike(f'%{query_key}%'), Prompt.name.ilike(f'%{query_key}%'),
Prompt.command.ilike(f'%{query_key}%'), Prompt.command.ilike(f'%{query_key}%'),
@@ -277,14 +277,14 @@ class PromptsTable:
view_option = filter.get('view_option') view_option = filter.get('view_option')
if view_option == 'created': if view_option == 'created':
stmt = stmt.filter(Prompt.user_id == user_id) query = query.filter(Prompt.user_id == user_id)
elif view_option == 'shared': elif view_option == 'shared':
stmt = stmt.filter(Prompt.user_id != user_id) query = query.filter(Prompt.user_id != user_id)
# Apply access grant filtering # Apply access grant filtering
stmt = AccessGrants.has_permission_filter( query = AccessGrants.has_permission_filter(
db=db, db=db,
query=stmt, query=query,
DocumentModel=Prompt, DocumentModel=Prompt,
filter=filter, filter=filter,
resource_type='prompt', resource_type='prompt',
@@ -293,56 +293,61 @@ class PromptsTable:
tag = filter.get('tag') tag = filter.get('tag')
if tag: if tag:
# SQLite stores JSON text via json.dumps(ensure_ascii=True), bind = await db.connection()
# so non-ASCII chars are \uXXXX-escaped. PostgreSQL native JSONB dialect_name = bind.dialect.name
# stores literal Unicode. Use the right pattern for each. tag_lower = tag.lower()
if db.bind.dialect.name == 'sqlite':
if tag.isascii(): if dialect_name == 'sqlite':
tags_text = func.lower(cast(Prompt.tags, String)) tag_clause = text(
pattern = f'%{json.dumps(tag.lower())}%' "EXISTS (SELECT 1 FROM json_each(prompt.tags) t WHERE LOWER(t.value) = :tag_val)"
else: )
# LOWER() is ASCII-only; non-ASCII codepoints would elif dialect_name == 'postgresql':
# produce different \uXXXX escapes when lowered. tag_clause = text(
tags_text = cast(Prompt.tags, String) "EXISTS (SELECT 1 FROM json_array_elements_text(prompt.tags) t WHERE LOWER(t) = :tag_val)"
pattern = f'%{json.dumps(tag)}%' )
else: else:
tags_text = func.lower(cast(Prompt.tags, String)) # Fallback: LIKE on serialised JSON text (ASCII-safe only)
pattern = f'%{json.dumps(tag.lower(), ensure_ascii=False)}%' tag_clause = func.lower(cast(Prompt.tags, String)).like(f'%{json.dumps(tag_lower, ensure_ascii=False)}%')
stmt = stmt.filter(tags_text.like(pattern)) tag_lower = None
if tag_lower is not None:
query = query.filter(tag_clause.params(tag_val=tag_lower))
else:
query = query.filter(tag_clause)
order_by = filter.get('order_by') order_by = filter.get('order_by')
direction = filter.get('direction') direction = filter.get('direction')
if order_by == 'name': if order_by == 'name':
if direction == 'asc': if direction == 'asc':
stmt = stmt.order_by(Prompt.name.asc()) query = query.order_by(Prompt.name.asc())
else: else:
stmt = stmt.order_by(Prompt.name.desc()) query = query.order_by(Prompt.name.desc())
elif order_by == 'created_at': elif order_by == 'created_at':
if direction == 'asc': if direction == 'asc':
stmt = stmt.order_by(Prompt.created_at.asc()) query = query.order_by(Prompt.created_at.asc())
else: else:
stmt = stmt.order_by(Prompt.created_at.desc()) query = query.order_by(Prompt.created_at.desc())
elif order_by == 'updated_at': elif order_by == 'updated_at':
if direction == 'asc': if direction == 'asc':
stmt = stmt.order_by(Prompt.updated_at.asc()) query = query.order_by(Prompt.updated_at.asc())
else: else:
stmt = stmt.order_by(Prompt.updated_at.desc()) query = query.order_by(Prompt.updated_at.desc())
else: else:
stmt = stmt.order_by(Prompt.updated_at.desc()) query = query.order_by(Prompt.updated_at.desc())
else: else:
stmt = stmt.order_by(Prompt.updated_at.desc()) query = query.order_by(Prompt.updated_at.desc())
# Count BEFORE pagination # Count BEFORE pagination
count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) count_result = await db.execute(select(func.count()).select_from(query.subquery()))
total = count_result.scalar() total = count_result.scalar()
if skip: if skip:
stmt = stmt.offset(skip) query = query.offset(skip)
if limit: if limit:
stmt = stmt.limit(limit) query = query.limit(limit)
result = await db.execute(stmt) result = await db.execute(query)
items = result.all() items = result.all()
prompt_ids = [prompt.id for prompt, _ in items] prompt_ids = [prompt.id for prompt, _ in items]