package back import ( "context" "errors" "fmt" "log" "os" "crawshaw.io/sqlite" "crawshaw.io/sqlite/sqlitex" "github.com/rickb777/date" ) type GenString func(length int) (string, error) type Store struct { pool *sqlitex.Pool genString GenString } func NewStore(filename string, genString GenString) (*Store, error) { // TODO This stat needs to change, though I can't remember how, now that // SQLite has been updated. var needCreate bool if _, err := os.Stat(filename); os.IsNotExist(err) { needCreate = true } // If the file exists, then assume it was created properly. pool, err := sqlitex.Open(filename, 0, 10) if err != nil { return nil, err } store := &Store{ pool: pool, genString: genString, } if needCreate { log.Println("creating schema") err = store.createSchema() if err != nil { defer pool.Close() return nil, err } } return store, nil } func NewMemoryStore(genString GenString) (*Store, error) { pool, err := sqlitex.Open("file::memory:?mode=memory&cache=shared", 0, 10) if err != nil { return nil, err } store := &Store{ pool: pool, genString: genString, } err = store.createSchema() if err != nil { defer pool.Close() return nil, err } return store, nil } func (s *Store) Close() error { return s.pool.Close() } const schema = ` CREATE TABLE event ( id INTEGER PRIMARY KEY, alpha_id TEXT NOT NULL, admin_code TEXT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL, earliest_date DATE NOT NULL, latest_date DATE NOT NULL, UNIQUE (alpha_id) ); CREATE TABLE response ( id INTEGER PRIMARY KEY, alpha_id TEXT NOT NULL, event_id INTEGER NOT NULL, guest_name TEXT NOT NULL, UNIQUE (alpha_id), FOREIGN KEY (event_id) REFERENCES event(id) ); CREATE TABLE response_time ( response_id INTEGER NOT NULL, date DATE NOT NULL, time INTEGER NOT NULL, CHECK (0 <= time < 24), UNIQUE (response_id, date, time) ); CREATE TRIGGER enforce_valid_response_dates BEFORE INSERT ON response_time BEGIN SELECT CASE WHEN NEW.date NOT BETWEEN event.earliest_date AND event.latest_date THEN RAISE(ABORT, 'response date is out of range') END FROM response JOIN event ON response.event_id = event.id WHERE response.id = NEW.response_id; END;` func (s *Store) createSchema() error { conn := s.pool.Get(context.Background()) defer s.pool.Put(conn) return sqlitex.ExecScript(conn, schema) } type CreateEventCommand struct { Name string Description string Earliest, Latest date.Date } type CreateEventResult struct { AlphaID, AdminCode string } const dbDateLayout = "2006-01-02" func (s *Store) CreateEvent(ctx context.Context, cmd CreateEventCommand) (result CreateEventResult, err error) { const alphaIDLength = 10 const adminCodeLength = 10 conn := s.pool.Get(ctx) defer s.pool.Put(conn) alphaID, err := s.genString(alphaIDLength) if err != nil { return } adminCode, err := s.genString(adminCodeLength) if err != nil { return } const query = ` INSERT INTO event(alpha_id, admin_code, name, description, earliest_date, latest_date) VALUES (?, ?, ?, ?, ?, ?);` err = sqlitex.Exec(conn, query, nil, alphaID, adminCode, cmd.Name, cmd.Description, cmd.Earliest.Format(dbDateLayout), cmd.Latest.Format(dbDateLayout), ) if err != nil { return } result.AdminCode = adminCode result.AlphaID = alphaID return } type GetEventMetadataQuery struct { AlphaID string } type GetEventMetadataResult struct { Name string Description string Earliest, Latest date.Date } func (s *Store) GetEventMetadata(ctx context.Context, query GetEventMetadataQuery) (GetEventMetadataResult, error) { conn := s.pool.Get(ctx) defer s.pool.Put(conn) const dbQuery = ` SELECT name, description, earliest_date, latest_date FROM event WHERE alpha_id = ?;` var result GetEventMetadataResult var found bool err := sqlitex.Exec(conn, dbQuery, func(stmt *sqlite.Stmt) error { found = true result.Name = stmt.ColumnText(0) result.Description = stmt.ColumnText(1) earliestDateString := stmt.ColumnText(2) latestDateString := stmt.ColumnText(3) var err error result.Earliest, err = date.Parse(dbDateLayout, earliestDateString) if err != nil { return err } result.Latest, err = date.Parse(dbDateLayout, latestDateString) return err }, query.AlphaID) if err != nil { return GetEventMetadataResult{}, err } if !found { // TODO return a constant or a specific error type for Not Found return GetEventMetadataResult{}, errors.New("not found") } return result, nil } type GetEventResponseSummaryQuery struct { AlphaID string } type GetEventResponseSummaryResult struct { TotalResponses int } func (s *Store) GetEventResponseSummary(ctx context.Context, query GetEventResponseSummaryQuery) (GetEventResponseSummaryResult, error) { conn := s.pool.Get(ctx) defer s.pool.Put(conn) var found bool var eventID int const eventIDQuery = ` SELECT id FROM event WHERE alpha_id = ?` var err = sqlitex.Exec(conn, eventIDQuery, func(stmt *sqlite.Stmt) error { found = true eventID = stmt.ColumnInt(0) return nil }, query.AlphaID) if err != nil { return GetEventResponseSummaryResult{}, err } if !found { return GetEventResponseSummaryResult{}, errors.New("event not found") } var result GetEventResponseSummaryResult const responseCountQuery = ` SELECT COUNT(*) FROM response WHERE event_id = ?;` err = sqlitex.Exec(conn, responseCountQuery, func(stmt *sqlite.Stmt) error { result.TotalResponses = stmt.ColumnInt(0) return nil }, eventID) if err != nil { return GetEventResponseSummaryResult{}, err } return result, nil } type CreateEventResponseCommand struct { EventAlphaID string GuestName string DateHours map[date.Date]map[int]struct{} } type CreateEventResponseResult struct { ResponseAlphaID string } func (s *Store) CreateEventResponse(ctx context.Context, cmd CreateEventResponseCommand) (result CreateEventResponseResult, err error) { const responseAlphaIDLength = 10 fmt.Printf("creating response: %+v\n", cmd) conn := s.pool.Get(ctx) defer s.pool.Put(conn) defer sqlitex.Save(conn)(&err) responseAlphaID, err := s.genString(responseAlphaIDLength) if err != nil { return } const responseQuery = ` INSERT INTO response(event_id, alpha_id, guest_name) SELECT event.id AS event_id, ? AS alpha_id, ? as guest_name FROM event WHERE event.alpha_id = ?;` err = sqlitex.Exec(conn, responseQuery, nil, responseAlphaID, cmd.GuestName, cmd.EventAlphaID) if err != nil { return } const responseTimeQuery = ` INSERT INTO response_time(response_id, date, time) SELECT response.id AS response_id, ? AS date, ? AS time FROM response WHERE response.alpha_id = ?;` for d, hs := range cmd.DateHours { for h := range hs { err = sqlitex.Exec(conn, responseTimeQuery, nil, d.Format(dbDateLayout), h, responseAlphaID) if err != nil { return } } } return CreateEventResponseResult{ ResponseAlphaID: responseAlphaID, }, nil } type GetEventResponseQuery struct { EventAlphaID string ResponseAlphaID string } type GetEventResponseResult struct { GuestName string DateHours map[date.Date]map[int]struct{} } func (s *Store) GetEventResponse(ctx context.Context, query GetEventResponseQuery) (GetEventResponseResult, error) { conn := s.pool.Get(ctx) defer s.pool.Put(conn) var found bool var result GetEventResponseResult var responseID int const responseQuery = ` SELECT guest_name, response.id FROM response JOIN event ON response.event_id = event.id WHERE response.alpha_id = ? AND event.alpha_id = ?;` var err = sqlitex.Exec(conn, responseQuery, func(stmt *sqlite.Stmt) error { found = true result.GuestName = stmt.ColumnText(0) responseID = stmt.ColumnInt(1) return nil }, query.ResponseAlphaID, query.EventAlphaID) if err != nil { return GetEventResponseResult{}, err } if !found { // TODO return a constant or typed error return GetEventResponseResult{}, errors.New("not found") } result.DateHours = make(map[date.Date]map[int]struct{}) const responseTimeQuery = ` SELECT date, time FROM response_time WHERE response_id = ?` err = sqlitex.Exec(conn, responseTimeQuery, func(stmt *sqlite.Stmt) error { d, err := date.Parse(dbDateLayout, stmt.ColumnText(0)) if err != nil { return err } if result.DateHours[d] == nil { result.DateHours[d] = make(map[int]struct{}) } result.DateHours[d][stmt.ColumnInt(1)] = struct{}{} return nil }, responseID) if err != nil { return GetEventResponseResult{}, err } return result, nil }