Complete database schema and type definitions for toni’s SQLite database
toni uses SQLite for local-first data storage. The database contains three main tables: restaurants, visits, and want_to_visit, all connected through foreign key relationships.
Stores restaurant information, either manually entered or imported from Yelp.
CREATE TABLE IF NOT EXISTS restaurants ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, address TEXT, city TEXT, neighborhood TEXT, cuisine TEXT, price_range TEXT CHECK(price_range IN ('$','$$','$$$','$$$$') OR price_range IS NULL), latitude REAL, longitude REAL, place_id TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')));
Records individual restaurant visits with ratings and notes.
CREATE TABLE IF NOT EXISTS visits ( id INTEGER PRIMARY KEY, restaurant_id INTEGER NOT NULL REFERENCES restaurants(id), visited_on TEXT, rating REAL CHECK(rating BETWEEN 1 AND 10 OR rating IS NULL), notes TEXT, would_return INTEGER CHECK(would_return IN (0,1) OR would_return IS NULL), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')));
Tracks restaurants you want to visit with priority levels.
CREATE TABLE IF NOT EXISTS want_to_visit ( id INTEGER PRIMARY KEY, restaurant_id INTEGER NOT NULL REFERENCES restaurants(id), notes TEXT, priority INTEGER CHECK(priority BETWEEN 1 AND 5 OR priority IS NULL), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')));
CREATE INDEX IF NOT EXISTS idx_visits_restaurant_id ON visits(restaurant_id);CREATE INDEX IF NOT EXISTS idx_visits_visited_on ON visits(visited_on DESC);CREATE INDEX IF NOT EXISTS idx_want_to_visit_restaurant_id ON want_to_visit(restaurant_id);CREATE INDEX IF NOT EXISTS idx_want_to_visit_priority ON want_to_visit(priority DESC);
Index Details
idx_visits_restaurant_id: Optimizes lookups of all visits for a specific restaurant
idx_visits_visited_on: Enables fast sorting by visit date (descending)
idx_want_to_visit_restaurant_id: Optimizes want-to-visit lookups by restaurant
idx_want_to_visit_priority: Enables fast sorting by priority (descending)
// VisitRow represents a visit with joined restaurant data for list displaytype VisitRow struct { ID int64 VisitedOn string RestaurantName string City string Address string PriceRange string Rating *float64 WouldReturn *bool Notes string RestaurantID int64}// RestaurantRow represents a restaurant with aggregate statstype RestaurantRow struct { ID int64 Name string Address string City string Neighborhood string Cuisine string PriceRange string AvgRating *float64 // average of all visit ratings VisitCount int // total number of visits LastVisit string // date of most recent visit}// WantToVisitRow represents want_to_visit with joined restaurant datatype WantToVisitRow struct { ID int64 RestaurantName string Address string City string Neighborhood string Cuisine string PriceRange string Priority *int Notes string RestaurantID int64 CreatedAt time.Time}
Deleting a restaurant cascades to all associated visits and want-to-visit entries. This operation cannot be undone (though toni provides an undo feature during the session).
When you delete a restaurant, toni executes a transaction:
// From internal/db/restaurants.gofunc DeleteRestaurant(db *sql.DB, id int64) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete all visits for this restaurant if _, err := tx.Exec("DELETE FROM visits WHERE restaurant_id = ?", id); err != nil { return fmt.Errorf("failed to delete visits: %w", err) } // Delete all want_to_visit entries for this restaurant if _, err := tx.Exec("DELETE FROM want_to_visit WHERE restaurant_id = ?", id); err != nil { return fmt.Errorf("failed to delete want_to_visit entries: %w", err) } // Delete the restaurant itself if _, err := tx.Exec("DELETE FROM restaurants WHERE id = ?", id); err != nil { return fmt.Errorf("failed to delete restaurant: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil}
Go uses pointers to represent nullable SQL fields:
Reading Nullable Fields
// Reading a nullable rating fieldvar rating sql.NullFloat64err := db.QueryRow(query, id).Scan(&rating)var visit Visitif rating.Valid { r := rating.Float64 visit.Rating = &r // convert to *float64} else { visit.Rating = nil // NULL in database}
Writing Nullable Fields
var latitude interface{}if restaurant.Latitude != nil { latitude = *restaurant.Latitude}// latitude will be nil (SQL NULL) if the pointer was nil_, err := db.Exec(query, name, latitude, longitude)
The schema is created automatically when you first run toni:
// From internal/db/db.gofunc Open(dbPath string) (*sql.DB, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("failed to ping database: %w", err) } // Execute schema creation (CREATE TABLE IF NOT EXISTS) if _, err := db.Exec(schema); err != nil { db.Close() return nil, fmt.Errorf("failed to initialize schema: %w", err) } return db, nil}
The CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS statements make schema initialization idempotent. Running toni multiple times on the same database is safe.