package sqlite import ( "database/sql" "errors" "fmt" "sort" "strings" "time" sqlitedriver "modernc.org/sqlite" sqlitelib "modernc.org/sqlite/lib" ) // timeLayout — единственный вид времени в схеме: RFC 3339, UTC, суффикс `Z`, // секундная точность. // // Ширина такой записи постоянная, поэтому лексикографический порядок `TEXT` // совпадает с хронологией, и отбор по колонке времени работает без разбора // значения. Своего типа времени у SQLite нет: колонка хранит то, что в неё // положили, — колонка, заполненная то одним видом, то другим, обратила бы // условие срока протухания захвата в постоянную истину или ложь молча, и запись // не выдавалась бы ни одному воркеру никогда. const timeLayout = "2006-01-02T15:04:05Z" // formatTime приводит метку времени к виду колонки. func formatTime(v time.Time) string { return v.UTC().Format(timeLayout) } // timeValue кладёт время в колонку, допускающую пустое значение. Нулевое время // и отсутствующее — одно и то же: «времени нет». func timeValue(v *time.Time) any { if v == nil || v.IsZero() { return nil } return formatTime(*v) } // timeOf читает колонку времени. Нечитаемое значение отдаётся нулевым: колонка // пишется только нами, и разбор здесь — сторож, а не ветвь поведения. func timeOf(v sql.NullString) *time.Time { if !v.Valid || v.String == "" { return nil } parsed, err := time.Parse(timeLayout, v.String) if err != nil { return nil } parsed = parsed.UTC() return &parsed } // requiredTimeOf читает обязательную колонку времени. func requiredTimeOf(v string) time.Time { parsed, err := time.Parse(timeLayout, v) if err != nil { return time.Time{} } return parsed.UTC() } // stringValue кладёт необязательную строку: пустая и отсутствующая — одно и то // же. func stringValue(v *string) any { if v == nil || *v == "" { return nil } return *v } // stringOf читает необязательную строку. func stringOf(v sql.NullString) *string { if !v.Valid || v.String == "" { return nil } out := v.String return &out } // numberOf читает необязательное число. // // Указатель здесь не выражает «неизвестно»: обе величины записи ставит приём и // ставит всегда, а колонки объявлены обязательными. Форма осталась указателем // потому, что её несёт домен, а ответ приложению обязан различать поле и его // отсутствие. func numberOf(v int64) *int64 { out := v return &out } // numberValue кладёт необязательное число нулём: колонка обязательна. func numberValue(v *int64) int64 { if v == nil { return 0 } return *v } // insertSQL собирает вставку из карты «колонка → значение». // // Именованными параметрами, а не позиционным списком: у аудиозаписи поля одного // типа идут длинным непрерывным рядом, и позиционный сдвиг на одно поле // скомпилировался бы молча, положив идентификатор файла в колонку текста. По // имени такого сдвига не существует вовсе. // // Порядок колонок берётся сортировкой, а не порядком обхода карты: обход карты // в Go случаен, и текст запроса менялся бы от прогона к прогону — отладка по // журналу читала бы каждый раз новый запрос. func insertSQL(table string, values map[string]any) (string, []any) { names := sortedNames(values) placeholders := make([]string, 0, len(names)) args := make([]any, 0, len(names)) for _, name := range names { placeholders = append(placeholders, ":"+name) args = append(args, sql.Named(name, values[name])) } query := fmt.Sprintf( "INSERT INTO %s (%s) VALUES (%s)", table, strings.Join(names, ", "), strings.Join(placeholders, ", "), ) return query, args } // updateSQL собирает правку из карты «колонка → значение» и условия. func updateSQL(table string, values map[string]any, where string, whereArgs []any) (string, []any) { names := sortedNames(values) assignments := make([]string, 0, len(names)) args := make([]any, 0, len(names)+len(whereArgs)) for _, name := range names { assignments = append(assignments, name+" = :"+name) args = append(args, sql.Named(name, values[name])) } args = append(args, whereArgs...) query := fmt.Sprintf( "UPDATE %s SET %s WHERE %s", table, strings.Join(assignments, ", "), where, ) return query, args } func sortedNames(values map[string]any) []string { names := make([]string, 0, len(values)) for name := range values { names = append(names, name) } sort.Strings(names) return names } // selectList собирает перечень колонок для выборки из той же карты, по которой // потом идёт чтение. Один источник у обеих половин: колонка, забытая в перечне, // не имеет места назначения, и наоборот — расхождению взяться неоткуда. func selectList(targets map[string]any, prefix string) (string, []any) { names := sortedNames(targets) columns := make([]string, 0, len(names)) scan := make([]any, 0, len(names)) for _, name := range names { columns = append(columns, prefix+name) scan = append(scan, targets[name]) } return strings.Join(columns, ", "), scan } // isUniqueViolation говорит, отказала ли запись по уникальному индексу. // // Судится **код** отказа, а не его текст: текст у драйвера свой на каждую // версию, а узнавание ошибки по тексту запрещено правилом проекта. Какая именно // колонка не сошлась, код не называет — и это не мешает: заведение учётной // записи различает два отказа повторным поиском по ключу, а не разбором текста. func isUniqueViolation(err error) bool { var sqliteErr *sqlitedriver.Error if !errors.As(err, &sqliteErr) { return false } return sqliteErr.Code() == sqlitelib.SQLITE_CONSTRAINT_UNIQUE || sqliteErr.Code() == sqlitelib.SQLITE_CONSTRAINT_PRIMARYKEY }