Added code for backend glue
This commit is contained in:
47
backend/internal/db/db.go
Normal file
47
backend/internal/db/db.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"mind/internal/config"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFS embed.FS
|
||||
|
||||
func OpenAndMigrate(cfg config.Config) (*sql.DB, error) {
|
||||
dsn := cfg.DSN
|
||||
drv := strings.ToLower(cfg.Driver)
|
||||
if drv != "mysql" && drv != "sqlite" {
|
||||
drv = "sqlite"
|
||||
}
|
||||
db, err := sql.Open(drv, dsn)
|
||||
if err != nil { return nil, err }
|
||||
if err := db.Ping(); err != nil { return nil, err }
|
||||
if err := runMigrations(db, drv); err != nil { return nil, err }
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func runMigrations(db *sql.DB, driver string) error {
|
||||
files, err := migrationFS.ReadDir("migrations")
|
||||
if err != nil { return err }
|
||||
for _, f := range files {
|
||||
b, err := migrationFS.ReadFile("migrations/"+f.Name())
|
||||
if err != nil { return err }
|
||||
sqlText := string(b)
|
||||
if driver == "sqlite" {
|
||||
// very minor compatibility tweak: drop ENUM
|
||||
sqlText = strings.ReplaceAll(sqlText, "ENUM('user','assistant')", "TEXT")
|
||||
}
|
||||
if _, err := db.Exec(sqlText); err != nil {
|
||||
return fmt.Errorf("migration %s: %w", f.Name(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
40
backend/internal/db/migrations/0001_init.sql
Normal file
40
backend/internal/db/migrations/0001_init.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- users (minimal for v0)
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email VARCHAR(255) UNIQUE,
|
||||
pass_bcrypt BLOB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- conversations
|
||||
CREATE TABLE IF NOT EXISTS conversations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_id INTEGER NOT NULL,
|
||||
title VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- nodes
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL,
|
||||
author_kind ENUM('user','assistant') NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- edges (DAG)
|
||||
CREATE TABLE IF NOT EXISTS edges (
|
||||
parent_id INTEGER NOT NULL,
|
||||
child_id INTEGER NOT NULL,
|
||||
PRIMARY KEY(parent_id, child_id)
|
||||
);
|
||||
|
||||
-- branches (named pointers)
|
||||
CREATE TABLE IF NOT EXISTS branches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversation_id INTEGER NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
head_node_id INTEGER NOT NULL,
|
||||
UNIQUE (conversation_id, name)
|
||||
);
|
||||
88
backend/internal/db/repo.go
Normal file
88
backend/internal/db/repo.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"mind/internal/models"
|
||||
)
|
||||
|
||||
type Repo struct { db *sql.DB }
|
||||
|
||||
func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }
|
||||
|
||||
// Conversations
|
||||
func (r *Repo) CreateConversation(ctx context.Context, ownerID int64, title string) (int64, error) {
|
||||
res, err := r.db.ExecContext(ctx, `INSERT INTO conversations(owner_id,title) VALUES(?,?)`, ownerID, title)
|
||||
if err != nil { return 0, err }
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
func (r *Repo) ListConversations(ctx context.Context, ownerID int64) ([]models.Conversation, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `SELECT id, owner_id, title, created_at FROM conversations WHERE owner_id=? ORDER BY id DESC`, ownerID)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
var out []models.Conversation
|
||||
for rows.Next() {
|
||||
var c models.Conversation
|
||||
if err := rows.Scan(&c.ID, &c.OwnerID, &c.Title, &c.Created); err != nil { return nil, err }
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Nodes & Edges
|
||||
func (r *Repo) CreateNode(ctx context.Context, convID int64, authorKind, content string) (int64, error) {
|
||||
res, err := r.db.ExecContext(ctx, `INSERT INTO nodes(conversation_id,author_kind,content) VALUES(?,?,?)`, convID, authorKind, content)
|
||||
if err != nil { return 0, err }
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
func (r *Repo) Link(ctx context.Context, parentID, childID int64) error {
|
||||
_, err := r.db.ExecContext(ctx, `INSERT INTO edges(parent_id,child_id) VALUES(?,?)`, parentID, childID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) GetNode(ctx context.Context, id int64) (models.Node, error) {
|
||||
var n models.Node
|
||||
err := r.db.QueryRowContext(ctx, `SELECT id,conversation_id,author_kind,content,created_at FROM nodes WHERE id=?`, id).
|
||||
Scan(&n.ID, &n.ConversationID, &n.AuthorKind, &n.Content, &n.Created)
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Branches
|
||||
func (r *Repo) CreateOrGetBranch(ctx context.Context, convID int64, name string, headNodeID int64) (models.Branch, error) {
|
||||
// Try insert
|
||||
_, _ = r.db.ExecContext(ctx, `INSERT INTO branches(conversation_id,name,head_node_id) VALUES(?,?,?)`, convID, name, headNodeID)
|
||||
return r.GetBranch(ctx, convID, name)
|
||||
}
|
||||
|
||||
func (r *Repo) GetBranch(ctx context.Context, convID int64, name string) (models.Branch, error) {
|
||||
var b models.Branch
|
||||
err := r.db.QueryRowContext(ctx, `SELECT id,conversation_id,name,head_node_id FROM branches WHERE conversation_id=? AND name=?`, convID, name).
|
||||
Scan(&b.ID, &b.ConversationID, &b.Name, &b.HeadNodeID)
|
||||
return b, err
|
||||
}
|
||||
|
||||
func (r *Repo) MoveBranchHead(ctx context.Context, branchID, newHeadID int64) error {
|
||||
_, err := r.db.ExecContext(ctx, `UPDATE branches SET head_node_id=? WHERE id=?`, newHeadID, branchID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Latest-parent walk for linearize
|
||||
func (r *Repo) LatestParent(ctx context.Context, childID int64) (models.Node, bool, error) {
|
||||
row := r.db.QueryRowContext(ctx, `
|
||||
SELECT n.id,n.conversation_id,n.author_kind,n.content,n.created_at
|
||||
FROM edges e
|
||||
JOIN nodes n ON n.id=e.parent_id
|
||||
WHERE e.child_id=?
|
||||
ORDER BY n.created_at DESC
|
||||
LIMIT 1`, childID)
|
||||
var n models.Node
|
||||
if err := row.Scan(&n.ID, &n.ConversationID, &n.AuthorKind, &n.Content, &n.Created); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) { return models.Node{}, false, nil }
|
||||
return models.Node{}, false, err
|
||||
}
|
||||
return n, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user