Changeset 0.17.1 (#76)

This commit is contained in:
2026-02-28 01:40:31 +00:00
parent c9141a6896
commit 856dc9b0ac
64 changed files with 8037 additions and 1657 deletions

View File

@@ -3,11 +3,25 @@ package treepath
import (
"database/sql"
"github.com/google/uuid"
"git.gobha.me/xcaliber/chat-switchboard/database"
)
// UpdateCursor upserts the channel_cursors row for a user.
func UpdateCursor(channelID, userID, messageID string) error {
if database.IsSQLite() {
// SQLite: id TEXT PRIMARY KEY has no default generator; supply one.
// Use excluded.active_leaf_id so ON CONFLICT works cleanly.
_, err := database.DB.Exec(`
INSERT INTO channel_cursors (id, channel_id, user_id, active_leaf_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (channel_id, user_id) DO UPDATE SET
active_leaf_id = excluded.active_leaf_id, updated_at = datetime('now')
`, uuid.New().String(), channelID, userID, messageID)
return err
}
// Postgres: id has DEFAULT gen_random_uuid(); $3 is referenced twice.
_, err := database.DB.Exec(`
INSERT INTO channel_cursors (channel_id, user_id, active_leaf_id)
VALUES ($1, $2, $3)
@@ -22,15 +36,15 @@ func UpdateCursor(channelID, userID, messageID string) error {
func NextSiblingIndex(channelID string, parentID *string) int {
var maxIdx sql.NullInt64
if parentID == nil {
database.DB.QueryRow(`
database.DB.QueryRow(database.Q(`
SELECT MAX(sibling_index) FROM messages
WHERE channel_id = $1 AND parent_id IS NULL AND deleted_at IS NULL
`, channelID).Scan(&maxIdx)
`), channelID).Scan(&maxIdx)
} else {
database.DB.QueryRow(`
database.DB.QueryRow(database.Q(`
SELECT MAX(sibling_index) FROM messages
WHERE parent_id = $1 AND deleted_at IS NULL
`, *parentID).Scan(&maxIdx)
`), *parentID).Scan(&maxIdx)
}
if !maxIdx.Valid {
return 0

View File

@@ -46,17 +46,17 @@ func GetActiveLeaf(channelID, userID string) (*string, error) {
var leafID *string
// Try cursor first
err := database.DB.QueryRow(`
err := database.DB.QueryRow(database.Q(`
SELECT active_leaf_id FROM channel_cursors
WHERE channel_id = $1 AND user_id = $2
`, channelID, userID).Scan(&leafID)
`), channelID, userID).Scan(&leafID)
if err == nil && leafID != nil {
// Verify the leaf still exists and isn't deleted
var exists bool
database.DB.QueryRow(`
database.DB.QueryRow(database.Q(`
SELECT EXISTS(SELECT 1 FROM messages WHERE id = $1 AND deleted_at IS NULL)
`, *leafID).Scan(&exists)
`), *leafID).Scan(&exists)
if exists {
return leafID, nil
}
@@ -64,11 +64,11 @@ func GetActiveLeaf(channelID, userID string) (*string, error) {
// Fallback: latest live message in channel
var fallbackID string
err = database.DB.QueryRow(`
err = database.DB.QueryRow(database.Q(`
SELECT id FROM messages
WHERE channel_id = $1 AND deleted_at IS NULL
ORDER BY created_at DESC LIMIT 1
`, channelID).Scan(&fallbackID)
`), channelID).Scan(&fallbackID)
if err == sql.ErrNoRows {
return nil, nil // empty channel
}
@@ -96,7 +96,7 @@ func GetActivePath(channelID, userID string) ([]PathMessage, error) {
// GetPathToLeaf walks from a specific leaf up to root, returning root-first.
func GetPathToLeaf(channelID, leafID string) ([]PathMessage, error) {
rows, err := database.DB.Query(`
rows, err := database.DB.Query(database.Q(`
WITH RECURSIVE path AS (
-- Anchor: start at the leaf
SELECT id, parent_id, role, content, model, tokens_used, tool_calls, metadata,
@@ -119,7 +119,7 @@ func GetPathToLeaf(channelID, leafID string) ([]PathMessage, error) {
participant_type, participant_id, sibling_index, created_at
FROM path
ORDER BY depth DESC
`, leafID, channelID)
`), leafID, channelID)
if err != nil {
return nil, fmt.Errorf("GetPathToLeaf: %w", err)
}

View File

@@ -12,15 +12,15 @@ import (
func GetSiblingCount(channelID string, parentID *string) int {
var count int
if parentID == nil {
database.DB.QueryRow(`
database.DB.QueryRow(database.Q(`
SELECT COUNT(*) FROM messages
WHERE channel_id = $1 AND parent_id IS NULL AND deleted_at IS NULL
`, channelID).Scan(&count)
`), channelID).Scan(&count)
} else {
database.DB.QueryRow(`
database.DB.QueryRow(database.Q(`
SELECT COUNT(*) FROM messages
WHERE parent_id = $1 AND deleted_at IS NULL
`, *parentID).Scan(&count)
`), *parentID).Scan(&count)
}
if count == 0 {
count = 1
@@ -34,10 +34,10 @@ func GetSiblings(messageID string) ([]SiblingInfo, int, error) {
// First get the parent_id of the target message
var parentID *string
var channelID string
err := database.DB.QueryRow(`
err := database.DB.QueryRow(database.Q(`
SELECT parent_id, channel_id FROM messages
WHERE id = $1 AND deleted_at IS NULL
`, messageID).Scan(&parentID, &channelID)
`), messageID).Scan(&parentID, &channelID)
if err != nil {
return nil, 0, fmt.Errorf("message not found: %w", err)
}
@@ -45,19 +45,19 @@ func GetSiblings(messageID string) ([]SiblingInfo, int, error) {
var rows *sql.Rows
if parentID == nil {
// Root messages: siblings are other roots in the same channel
rows, err = database.DB.Query(`
SELECT id, role, model, sibling_index, LEFT(content, 80), created_at
rows, err = database.DB.Query(database.Q(`
SELECT id, role, model, sibling_index, SUBSTR(content, 1, 80), created_at
FROM messages
WHERE channel_id = $1 AND parent_id IS NULL AND deleted_at IS NULL
ORDER BY sibling_index, created_at
`, channelID)
`), channelID)
} else {
rows, err = database.DB.Query(`
SELECT id, role, model, sibling_index, LEFT(content, 80), created_at
rows, err = database.DB.Query(database.Q(`
SELECT id, role, model, sibling_index, SUBSTR(content, 1, 80), created_at
FROM messages
WHERE parent_id = $1 AND deleted_at IS NULL
ORDER BY sibling_index, created_at
`, *parentID)
`), *parentID)
}
if err != nil {
return nil, 0, err
@@ -85,7 +85,7 @@ func GetSiblings(messageID string) ([]SiblingInfo, int, error) {
// Used when switching branches: clicking a mid-tree sibling navigates to its leaf.
func FindLeafFromMessage(messageID string) (string, error) {
var leafID string
err := database.DB.QueryRow(`
err := database.DB.QueryRow(database.Q(`
WITH RECURSIVE descendants AS (
SELECT id, 0 AS depth
FROM messages
@@ -105,7 +105,7 @@ func FindLeafFromMessage(messageID string) (string, error) {
SELECT id FROM descendants
ORDER BY depth DESC
LIMIT 1
`, messageID).Scan(&leafID)
`), messageID).Scan(&leafID)
if err != nil {
return messageID, nil // if anything fails, just use the message itself