feat: add user_id index to profiles table for faster lookups

The profiles table previously lacked an index on the user_id column, causing full table scans on join queries. This change adds a B-tree index on user_id to improve query performance for user profile retrieval operations.
This commit is contained in:
2026-05-25 14:16:53 +00:00
parent 033b6906a2
commit 950f26a420
15 changed files with 309 additions and 47 deletions
+26
View File
@@ -385,6 +385,32 @@ def resolve_by_slug(table, slug):
return entry
def resolve_object_url(target_type: str, target_uid: str) -> str:
if target_type == "post":
post = resolve_by_slug(get_table("posts"), target_uid)
return f"/posts/{post['slug'] or post['uid']}" if post else "/feed"
if target_type == "project":
project = resolve_by_slug(get_table("projects"), target_uid)
return f"/projects/{project['slug'] or project['uid']}" if project else "/projects"
if target_type == "news":
article = resolve_by_slug(get_table("news"), target_uid)
if article:
return f"/news/{article.get('slug', '') or article['uid']}"
return "/news"
if target_type == "bug":
return f"/bugs?highlight={target_uid}"
if target_type == "gist":
gist = resolve_by_slug(get_table("gists"), target_uid)
return f"/gists/{gist['slug'] or gist['uid']}" if gist else "/gists"
if target_type == "comment":
comment = get_table("comments").find_one(uid=target_uid)
if not comment:
return "/feed"
parent_url = resolve_object_url(comment.get("target_type", "post"), comment.get("target_uid") or comment.get("post_uid", ""))
return f"{parent_url}#comment-{target_uid}"
return "/feed"
VOTABLE_TARGETS: dict[str, str] = {
"post": "posts",
"project": "projects",
+8 -26
View File
@@ -3,7 +3,7 @@ from typing import Annotated
from datetime import datetime, timezone
from fastapi import APIRouter, Request, Form
from fastapi.responses import RedirectResponse
from devplacepy.database import get_table, resolve_by_slug
from devplacepy.database import get_table, resolve_object_url
from devplacepy.attachments import link_attachments, delete_target_attachments
from devplacepy.utils import generate_uid, require_user, create_mention_notifications, create_notification, award_badge
from devplacepy.models import CommentForm
@@ -12,26 +12,6 @@ logger = logging.getLogger(__name__)
router = APIRouter()
def resolve_target_redirect(target_type, target_uid):
if target_type == "post":
post = resolve_by_slug(get_table("posts"), target_uid)
return f"/posts/{post['slug'] or post['uid']}" if post else "/feed"
if target_type == "project":
project = resolve_by_slug(get_table("projects"), target_uid)
return f"/projects/{project['slug'] or project['uid']}" if project else "/projects"
if target_type == "news":
article = resolve_by_slug(get_table("news"), target_uid)
if article:
return f"/news/{article.get('slug', '') or article['uid']}"
return "/news"
if target_type == "bug":
return f"/bugs?highlight={target_uid}"
if target_type == "gist":
gist = resolve_by_slug(get_table("gists"), target_uid)
return f"/gists/{gist['slug'] or gist['uid']}" if gist else "/gists"
return "/bugs"
@router.post("/create")
async def create_comment(request: Request, data: Annotated[CommentForm, Form()]):
user = require_user(request)
@@ -40,7 +20,7 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
target_type = data.target_type
parent_uid = data.parent_uid
redirect_url = resolve_target_redirect(target_type, target_uid)
redirect_url = resolve_object_url(target_type, target_uid)
comment_uid = generate_uid()
insert = {
@@ -61,20 +41,22 @@ async def create_comment(request: Request, data: Annotated[CommentForm, Form()])
award_badge(user["uid"], "First Comment")
comment_url = f"{redirect_url}#comment-{comment_uid}"
if target_type == "post":
if parent_uid:
parent = get_table("comments").find_one(uid=parent_uid)
if parent and parent["user_uid"] != user["uid"]:
create_notification(parent["user_uid"], "reply", f"{user['username']} replied to your comment", user["uid"])
create_notification(parent["user_uid"], "reply", f"{user['username']} replied to your comment", user["uid"], comment_url)
else:
posts = get_table("posts")
post = posts.find_one(uid=target_uid)
if not post:
post = posts.find_one(slug=target_uid)
if post and post["user_uid"] != user["uid"]:
create_notification(post["user_uid"], "comment", f"{user['username']} commented on your post", user["uid"])
create_notification(post["user_uid"], "comment", f"{user['username']} commented on your post", user["uid"], comment_url)
create_mention_notifications(content, user["uid"], redirect_url)
create_mention_notifications(content, user["uid"], comment_url)
logger.info(f"Comment by {user['username']} on {target_type} {target_uid}")
return RedirectResponse(url=redirect_url, status_code=302)
@@ -94,5 +76,5 @@ async def delete_comment(request: Request, comment_uid: str):
get_table("votes").delete(target_uid=comment_uid, target_type="comment")
comments.delete(id=comment["id"])
logger.info(f"Comment {comment_uid} deleted by {user['username']}")
redirect_url = resolve_target_redirect(target_type, target_uid)
redirect_url = resolve_object_url(target_type, target_uid)
return RedirectResponse(url=redirect_url, status_code=302)
+1 -1
View File
@@ -29,7 +29,7 @@ async def follow_user(request: Request, username: str):
"created_at": datetime.now(timezone.utc).isoformat(),
})
create_notification(target["uid"], "follow", f"{user['username']} started following you", user["uid"])
create_notification(target["uid"], "follow", f"{user['username']} started following you", user["uid"], f"/profile/{user['username']}")
logger.info(f"{user['username']} followed {username}")
return RedirectResponse(url=f"/profile/{username}", status_code=302)
+1
View File
@@ -174,6 +174,7 @@ async def send_message(request: Request, data: Annotated[MessageForm, Form()]):
"type": "message",
"message": f"{user['username']} sent you a message",
"related_uid": user["uid"],
"target_url": f"/messages?with_uid={user['uid']}",
"read": False,
"created_at": datetime.now(timezone.utc).isoformat(),
})
+13
View File
@@ -86,6 +86,19 @@ async def notifications_page(request: Request):
})
@router.get("/open/{notification_uid}")
async def open_notification(request: Request, notification_uid: str):
user = require_user(request)
notifications_table = get_table("notifications")
n = notifications_table.find_one(uid=notification_uid)
if not n or n["user_uid"] != user["uid"]:
return RedirectResponse(url="/notifications", status_code=302)
if not n["read"]:
notifications_table.update({"id": n["id"], "read": True}, ["id"])
clear_unread_cache(user["uid"])
return RedirectResponse(url=n.get("target_url") or "/notifications", status_code=302)
@router.post("/mark-read/{notification_uid}")
async def mark_read(request: Request, notification_uid: str):
user = require_user(request)
+3 -2
View File
@@ -3,7 +3,7 @@ from typing import Annotated
from datetime import datetime, timezone
from fastapi import APIRouter, Request, Form
from fastapi.responses import RedirectResponse
from devplacepy.database import get_table, update_target_stars, get_target_owner_uid
from devplacepy.database import get_table, update_target_stars, get_target_owner_uid, resolve_object_url
from devplacepy.utils import generate_uid, require_user, create_notification
from devplacepy.models import VoteForm
@@ -45,7 +45,8 @@ async def vote(request: Request, target_type: str, target_uid: str, data: Annota
if value == 1 and target_type in NOTIFY_ON_VOTE:
owner_uid = get_target_owner_uid(target_type, target_uid)
if owner_uid and owner_uid != user["uid"]:
create_notification(owner_uid, "vote", f"{user['username']} ++'d your {target_type}", user["uid"])
target_url = resolve_object_url(target_type, target_uid)
create_notification(owner_uid, "vote", f"{user['username']} ++'d your {target_type}", user["uid"], target_url)
referer = request.headers.get("Referer", "/feed")
return RedirectResponse(url=referer, status_code=302)
+1
View File
@@ -31,6 +31,7 @@
border-radius: var(--radius-lg);
padding: 1.25rem;
transition: all 0.2s;
cursor: pointer;
}
.notification-card.unread {
+16
View File
@@ -115,6 +115,22 @@
min-width: 0;
}
.comment-highlight {
animation: comment-highlight-fade 2s ease-out;
border-radius: var(--radius);
}
@keyframes comment-highlight-fade {
from {
background: var(--accent-light);
box-shadow: 0 0 0 4px var(--accent-light);
}
to {
background: transparent;
box-shadow: 0 0 0 4px transparent;
}
}
.comment-header {
display: flex;
align-items: center;
+2
View File
@@ -1,6 +1,7 @@
import { ModalManager } from "./ModalManager.js";
import { FormManager } from "./FormManager.js";
import { VoteManager } from "./VoteManager.js";
import { NotificationManager } from "./NotificationManager.js";
import { MessageSearch } from "./MessageSearch.js";
import { ProfileEditor } from "./ProfileEditor.js";
import { MobileNav } from "./MobileNav.js";
@@ -15,6 +16,7 @@ class Application {
this.modals = new ModalManager();
this.forms = new FormManager();
this.votes = new VoteManager();
this.notifications = new NotificationManager();
this.messageSearch = new MessageSearch();
this.profile = new ProfileEditor();
this.mobileNav = new MobileNav();
@@ -0,0 +1,51 @@
import { Http } from "./Http.js";
export class NotificationManager {
constructor() {
this.initCardNavigation();
this.initDismiss();
this.initHashScroll();
}
initCardNavigation() {
document.querySelectorAll(".notification-card[data-href]").forEach((card) => {
card.addEventListener("click", (e) => {
if (e.target.closest("a, button")) {
return;
}
window.location.href = card.dataset.href;
});
});
}
initDismiss() {
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const uid = btn.dataset.uid;
if (!uid) {
return;
}
Http.postForm(`/notifications/mark-read/${uid}`);
});
});
}
initHashScroll() {
const hash = window.location.hash;
if (!hash.startsWith("#comment-")) {
return;
}
window.addEventListener("load", () => {
const target = document.getElementById(hash.slice(1));
if (!target) {
return;
}
requestAnimationFrame(() => {
target.scrollIntoView({ behavior: "smooth", block: "center" });
target.classList.add("comment-highlight");
setTimeout(() => target.classList.remove("comment-highlight"), 2000);
});
});
}
}
-13
View File
@@ -3,7 +3,6 @@ import { Http } from "./Http.js";
export class VoteManager {
constructor() {
this.initVoteButtons();
this.initNotificationDismiss();
}
initVoteButtons() {
@@ -15,16 +14,4 @@ export class VoteManager {
});
});
}
initNotificationDismiss() {
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
btn.addEventListener("click", () => {
const uid = btn.dataset.uid;
if (!uid) {
return;
}
Http.postForm(`/notifications/mark-read/${uid}`);
});
});
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
</form>
</div>
<div class="comment-body" data-comment-uid="{{ item.comment['uid'] }}">
<div class="comment-body" id="comment-{{ item.comment['uid'] }}" data-comment-uid="{{ item.comment['uid'] }}">
<div class="comment-header">
<a href="/profile/{{ item.author['username'] if item.author else '#' }}">
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 32) }}" class="avatar-img avatar-sm" alt="{{ item.author['username'] if item.author else '?' }}" loading="lazy">
+2 -3
View File
@@ -17,14 +17,13 @@
<div class="notification-group">
<div class="notification-group-label">{{ group.label }}</div>
{% for item in group.entries %}
{% set target_url = item.notification.get('target_url', '') %}
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}">
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}" data-href="/notifications/open/{{ item.notification['uid'] }}">
{% set actor_username = item.actor['username'] if item.actor else '#' %}
<a href="/profile/{{ actor_username }}" style="flex-shrink:0">
<img src="{{ avatar_url('multiavatar', actor_username, 32) }}" class="avatar-img avatar-sm" alt="{{ actor_username }}" loading="lazy">
</a>
<div class="notification-body">
<div class="notification-text">{% if target_url %}<a href="{{ target_url }}" style="color:inherit;text-decoration:none">{% endif %}{{ item.notification['message'] }}{% if target_url %}</a>{% endif %}</div>
<div class="notification-text">{{ item.notification['message'] }}</div>
<div class="notification-time">{{ item.time_ago }}</div>
</div>
<form method="POST" action="/notifications/mark-read/{{ item.notification['uid'] }}" style="display:inline;">
+5 -1
View File
@@ -212,6 +212,10 @@ def create_mention_notifications(content: str, actor_uid: str, target_url: str)
if not usernames:
return
users = get_table("users")
actor = users.find_one(uid=actor_uid)
if not actor:
return
actor_username = actor["username"]
seen = set()
for username in usernames:
if username in seen:
@@ -219,7 +223,7 @@ def create_mention_notifications(content: str, actor_uid: str, target_url: str)
seen.add(username)
mentioned = users.find_one(username=username)
if mentioned and mentioned["uid"] != actor_uid:
create_notification(mentioned["uid"], "mention", f"@{username} mentioned you", actor_uid, target_url)
create_notification(mentioned["uid"], "mention", f"@{actor_username} mentioned you", actor_uid, target_url)
def format_date(dt_str: str, include_time: bool = False) -> str: