Changeset 0.17.1 (#76)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user