all repos — homestead @ 52549c0ea610306039e1f3f91c89e05ba4f41e72

Code for my website

use sqlc for database queries

Alan Pearce
commit

52549c0ea610306039e1f3f91c89e05ba4f41e72

parent

3fc8094d18e9d0be354492e0b3a3aca11ae1a1d6

M flake.nixflake.nix
@@ -25,6 +25,7 @@ just
ko flyctl redis + sqlc ]; devPackages = with pkgs; [ wgo
M internal/builder/builder.gointernal/builder/builder.go
@@ -65,7 +65,7 @@ log *log.Logger,
) error { buf := new(buffer.Buffer) joinSource := joinSourcePath(options.Source) - storage := options.Storage + stor := options.Storage postDir := "post" siteSettings := templates.SiteSettings{ Title: config.Title,
@@ -98,7 +98,7 @@ if err := templates.PostPage(siteSettings, post).Render(buf); err != nil {
return fault.Wrap(err, fmsg.With("could not render post")) } - if err := storage.WritePost(post, buf); err != nil { + if err := stor.WritePost(post, buf); err != nil { return fault.Wrap(err) } }
@@ -111,7 +111,7 @@ Tags: mapset.Sorted(cc.Tags),
}).Render(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write("/tags/", "Tags", buf); err != nil { + if err := stor.Write("/tags/", "Tags", buf); err != nil { return fault.Wrap(err) } sitemap.AddPath("/tags/", lastMod)
@@ -132,7 +132,7 @@ Posts: matchingPosts,
}).Render(buf); err != nil { return fault.Wrap(err) } - if err = storage.Write(url, tag, buf); err != nil { + if err = stor.Write(url, tag, buf); err != nil { return fault.Wrap(err) } sitemap.AddPath(url, matchingPosts[0].Date)
@@ -152,7 +152,14 @@ buf.Reset()
if _, err := feed.WriteTo(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write(path.Join("/tags", tag, "atom.xml"), title, buf); err != nil { + file := &storage.File{ + Title: title, + LastModified: matchingPosts[0].Date, + ContentType: "application/xml", + Path: path.Join("/tags", tag, "atom.xml"), + FSPath: path.Join("/tags", tag, "atom.xml"), + } + if err := stor.WriteFile(file, buf); err != nil { return fault.Wrap(err) } }
@@ -164,7 +171,7 @@ Posts: cc.Posts,
}).Render(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write(path.Join("/", postDir)+"/", "Posts", buf); err != nil { + if err := stor.Write(path.Join("/", postDir)+"/", "Posts", buf); err != nil { return fault.Wrap(err) } sitemap.AddPath(path.Join("/", postDir)+"/", lastMod)
@@ -178,7 +185,14 @@ buf.Reset()
if _, err := feed.WriteTo(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write("/atom.xml", config.Title, buf); err != nil { + file := &storage.File{ + Title: config.Title, + LastModified: cc.Posts[0].Date, + ContentType: "application/xml", + Path: "/atom.xml", + FSPath: "/atom.xml", + } + if err := stor.WriteFile(file, buf); err != nil { return fault.Wrap(err) }
@@ -191,7 +205,7 @@ err,
fmsg.With(fmt.Sprintf("could not render template file %s", filename)), ) } - if err := storage.Write("/"+filename, "", buf); err != nil { + if err := stor.Write("/"+filename, "", buf); err != nil { return fault.Wrap(err) } }
@@ -212,9 +226,9 @@ if err := templates.Page(siteSettings, post).Render(buf); err != nil {
return fault.Wrap(err) } } - file := storage.NewFileFromPost(post) + file := stor.NewFileFromPost(post) file.ContentType = "text/html; charset=utf-8" - if err := storage.WriteFile(file, buf); err != nil { + if err := stor.WriteFile(file, buf); err != nil { return fault.Wrap(err) } }
@@ -232,7 +246,7 @@ buf.Reset()
if _, err := sitemap.WriteTo(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write("/sitemap.xml", "sitemap", buf); err != nil { + if err := stor.Write("/sitemap.xml", "sitemap", buf); err != nil { return fault.Wrap(err) }
@@ -242,14 +256,14 @@ err = template.RenderRobotsTXT(config.BaseURL, buf)
if err != nil { return fault.Wrap(err) } - if err := storage.Write("/robots.txt", "", buf); err != nil { + if err := stor.Write("/robots.txt", "", buf); err != nil { return fault.Wrap(err) } for _, sf := range cc.StaticFiles { src := joinSource(sf) log.Debug("copying static file", "sf", sf, "src", src) - err = copyFile(storage, src, sf) + err = copyFile(stor, src, sf) if err != nil { return fault.Wrap(err) }
@@ -261,7 +275,7 @@ buf.Reset()
if err := templates.GoPackagePage(siteSettings, &config.Go, p).Render(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write(fmt.Sprintf("/go/%s.html", p), siteSettings.Title, buf); err != nil { + if err := stor.Write(fmt.Sprintf("/go/%s.html", p), siteSettings.Title, buf); err != nil { return fault.Wrap(err) } }
@@ -270,7 +284,7 @@ buf.Reset()
if err := templates.GoPackageListPage(siteSettings, &config.Go).Render(buf); err != nil { return fault.Wrap(err) } - if err := storage.Write("/go/", siteSettings.Title, buf); err != nil { + if err := stor.Write("/go/", siteSettings.Title, buf); err != nil { return fault.Wrap(err) }
A internal/storage/sqlite/db/db.go
@@ -0,0 +1,128 @@
+// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.checkPathStmt, err = db.PrepareContext(ctx, checkPath); err != nil { + return nil, fmt.Errorf("error preparing query CheckPath: %w", err) + } + if q.getFileStmt, err = db.PrepareContext(ctx, getFile); err != nil { + return nil, fmt.Errorf("error preparing query GetFile: %w", err) + } + if q.insertContentStmt, err = db.PrepareContext(ctx, insertContent); err != nil { + return nil, fmt.Errorf("error preparing query InsertContent: %w", err) + } + if q.insertFileStmt, err = db.PrepareContext(ctx, insertFile); err != nil { + return nil, fmt.Errorf("error preparing query InsertFile: %w", err) + } + if q.insertURLStmt, err = db.PrepareContext(ctx, insertURL); err != nil { + return nil, fmt.Errorf("error preparing query InsertURL: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.checkPathStmt != nil { + if cerr := q.checkPathStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing checkPathStmt: %w", cerr) + } + } + if q.getFileStmt != nil { + if cerr := q.getFileStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getFileStmt: %w", cerr) + } + } + if q.insertContentStmt != nil { + if cerr := q.insertContentStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertContentStmt: %w", cerr) + } + } + if q.insertFileStmt != nil { + if cerr := q.insertFileStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertFileStmt: %w", cerr) + } + } + if q.insertURLStmt != nil { + if cerr := q.insertURLStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing insertURLStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + checkPathStmt *sql.Stmt + getFileStmt *sql.Stmt + insertContentStmt *sql.Stmt + insertFileStmt *sql.Stmt + insertURLStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + checkPathStmt: q.checkPathStmt, + getFileStmt: q.getFileStmt, + insertContentStmt: q.insertContentStmt, + insertFileStmt: q.insertFileStmt, + insertURLStmt: q.insertURLStmt, + } +}
A internal/storage/sqlite/db/models.go
@@ -0,0 +1,27 @@
+// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +type Content struct { + ContentID int64 + FileID int64 + Encoding string + Body []byte +} + +type File struct { + FileID int64 + UrlID int64 + ContentType string + LastModified int64 + Title string + Etag string + StyleHash string +} + +type Url struct { + UrlID int64 + Path string +}
A internal/storage/sqlite/db/query.sql.go
@@ -0,0 +1,143 @@
+// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: query.sql + +package db + +import ( + "context" +) + +const checkPath = `-- name: CheckPath :one +SELECT + EXISTS( + SELECT 1 + FROM url + WHERE path = ? + ) AS differs +` + +func (q *Queries) CheckPath(ctx context.Context, path string) (int64, error) { + row := q.queryRow(ctx, q.checkPathStmt, checkPath, path) + var differs int64 + err := row.Scan(&differs) + return differs, err +} + +const getFile = `-- name: GetFile :many +SELECT + file.file_id, file.url_id, file.content_type, file.last_modified, file.title, file.etag, file.style_hash, + content.content_id, content.file_id, content.encoding, content.body +FROM url +INNER JOIN file + USING (url_id) +INNER JOIN content + USING (file_id) +WHERE + url.path = ? +` + +type GetFileRow struct { + File File + Content Content +} + +func (q *Queries) GetFile(ctx context.Context, path string) ([]GetFileRow, error) { + rows, err := q.query(ctx, q.getFileStmt, getFile, path) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFileRow + for rows.Next() { + var i GetFileRow + if err := rows.Scan( + &i.File.FileID, + &i.File.UrlID, + &i.File.ContentType, + &i.File.LastModified, + &i.File.Title, + &i.File.Etag, + &i.File.StyleHash, + &i.Content.ContentID, + &i.Content.FileID, + &i.Content.Encoding, + &i.Content.Body, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertContent = `-- name: InsertContent :exec +INSERT INTO content (file_id, encoding, body) +VALUES (?1, ?2, ?3) +` + +type InsertContentParams struct { + Fileid int64 + Encoding string + Body []byte +} + +func (q *Queries) InsertContent(ctx context.Context, arg InsertContentParams) error { + _, err := q.exec(ctx, q.insertContentStmt, insertContent, arg.Fileid, arg.Encoding, arg.Body) + return err +} + +const insertFile = `-- name: InsertFile :execlastid +INSERT INTO file (url_id, content_type, last_modified, etag, style_hash, title) +VALUES ( + ?1, + ?2, + ?3, + ?4, + ?5, + ?6 +) +` + +type InsertFileParams struct { + UrlID int64 + ContentType string + LastModified int64 + Etag string + StyleHash string + Title string +} + +func (q *Queries) InsertFile(ctx context.Context, arg InsertFileParams) (int64, error) { + result, err := q.exec(ctx, q.insertFileStmt, insertFile, + arg.UrlID, + arg.ContentType, + arg.LastModified, + arg.Etag, + arg.StyleHash, + arg.Title, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const insertURL = `-- name: InsertURL :execlastid +INSERT INTO url (path) VALUES (?) +` + +func (q *Queries) InsertURL(ctx context.Context, path string) (int64, error) { + result, err := q.exec(ctx, q.insertURLStmt, insertURL, path) + if err != nil { + return 0, err + } + return result.LastInsertId() +}
M internal/storage/sqlite/reader.gointernal/storage/sqlite/reader.go
@@ -1,62 +1,28 @@
package sqlite import ( + "context" "database/sql" "strings" "time" - "github.com/Southclaws/fault" - "github.com/Southclaws/fault/fmsg" "alin.ovh/homestead/internal/buffer" "alin.ovh/homestead/internal/storage" + "alin.ovh/homestead/internal/storage/sqlite/db" "alin.ovh/x/log" + "github.com/Southclaws/fault" + "github.com/Southclaws/fault/fmsg" ) type Reader struct { - db *sql.DB log *log.Logger - queries struct { - getFile *sql.Stmt - checkPath *sql.Stmt - } + queries *db.Queries } -func NewReader(db *sql.DB, log *log.Logger) (*Reader, error) { +func NewReader(conn *sql.DB, log *log.Logger) (*Reader, error) { r := &Reader{ - log: log, - db: db, - } - var err error - r.queries.getFile, err = r.db.Prepare(` - SELECT - file.content_type, - file.last_modified, - file.etag, - file.title, - file.style_hash, - content.encoding, - content.body - FROM url - INNER JOIN file - USING (url_id) - INNER JOIN content - USING (file_id) - WHERE - url.path = ? - `) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("preparing select statement")) - } - - r.queries.checkPath, err = r.db.Prepare(` - SELECT EXISTS( - SELECT 1 - FROM url - WHERE path = ? - ) AS differs -`) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("preparing check path statement")) + log: log, + queries: db.New(conn), } return r, nil
@@ -66,41 +32,25 @@ func (r *Reader) GetFile(filename string) (*storage.File, error) {
file := &storage.File{ Encodings: make(map[string]*buffer.Buffer, 1), } - var unixTime int64 - var encoding string - var content []byte r.log.Debug("querying db for file", "filename", filename) - rows, err := r.queries.getFile.Query(filename) + rows, err := r.queries.GetFile(context.TODO(), filename) if err != nil { return nil, fault.Wrap(err, fmsg.With("querying database")) } - defer rows.Close() count := 0 - for rows.Next() { + for _, row := range rows { count++ - err = rows.Scan( - &file.ContentType, - &unixTime, - &file.Etag, - &file.Title, - &file.StyleHash, - &encoding, - &content, - ) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("querying database")) - } - - file.LastModified = time.Unix(unixTime, 0) - file.Encodings[encoding] = buffer.NewBuffer(content) + file.ContentType = row.File.ContentType + file.LastModified = time.Unix(row.File.LastModified, 0) + file.Etag = row.File.Etag + file.Title = row.File.Title + file.StyleHash = row.File.StyleHash + file.Encodings[row.Content.Encoding] = buffer.NewBuffer(row.Content.Body) } if count == 0 { return nil, nil - } - if err := rows.Err(); err != nil { - return nil, fault.Wrap(err, fmsg.With("error iterating rows")) } return file, nil
@@ -116,23 +66,25 @@ cPath, differs = strings.TrimSuffix(path, "index.xml")+"atom.xml", true
case strings.HasSuffix(path, "/index.html"): cPath, differs = strings.CutSuffix(path, "index.html") case !strings.HasSuffix(path, "/"): - err := r.queries.checkPath.QueryRow(cPath + "/").Scan(&differs) + d, err := r.queries.CheckPath(context.TODO(), cPath+"/") if err != nil { r.log.Warn("error canonicalising path", "path", path, "error", err) return } + differs = d == 1 if differs { cPath += "/" } case strings.HasSuffix(path, "/"): tmp := strings.TrimSuffix(path, "/") - err := r.queries.checkPath.QueryRow(tmp).Scan(&differs) + d, err := r.queries.CheckPath(context.TODO(), tmp) if err != nil { r.log.Warn("error canonicalising path", "path", path, "error", err) return } + differs = d == 1 if differs { cPath = tmp }
M internal/storage/sqlite/writer.gointernal/storage/sqlite/writer.go
@@ -1,18 +1,21 @@
package sqlite import ( + "context" "database/sql" "fmt" "hash/fnv" "io" "mime" "net/http" + "os" "path/filepath" "time" "alin.ovh/homestead/internal/buffer" "alin.ovh/homestead/internal/content" "alin.ovh/homestead/internal/storage" + "alin.ovh/homestead/internal/storage/sqlite/db" "alin.ovh/x/log" "github.com/andybalholm/brotli" "github.com/klauspost/compress/gzip"
@@ -26,15 +29,9 @@
var encodings = []string{"gzip", "br", "zstd"} type Writer struct { - db *sql.DB - options *Options log *log.Logger - queries struct { - insertURL *sql.Stmt - insertFile *sql.Stmt - insertContent *sql.Stmt - } + queries *db.Queries } type Options struct {
@@ -58,69 +55,23 @@
return db, nil } -func NewWriter(db *sql.DB, logger *log.Logger, opts *Options) (*Writer, error) { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS url ( - url_id INTEGER PRIMARY KEY, - path TEXT NOT NULL - ) STRICT; - CREATE UNIQUE INDEX IF NOT EXISTS url_path - ON url (path); +func NewWriter(conn *sql.DB, logger *log.Logger, opts *Options) (*Writer, error) { + q, err := os.ReadFile("schema.sql") + if err != nil { + return nil, fault.Wrap(err) + } - CREATE TABLE IF NOT EXISTS file ( - file_id INTEGER PRIMARY KEY, - url_id INTEGER NOT NULL, - content_type TEXT NOT NULL, - last_modified INTEGER NOT NULL, - title TEXT NOT NULL, - etag TEXT NOT NULL, - style_hash TEXT NOT NULL, - FOREIGN KEY (url_id) REFERENCES url (url_id) - ) STRICT; - CREATE UNIQUE INDEX IF NOT EXISTS file_url_content_type - ON file (url_id, content_type); - - CREATE TABLE IF NOT EXISTS content ( - content_id INTEGER PRIMARY KEY, - file_id INTEGER NOT NULL, - encoding TEXT NOT NULL, - body BLOB NOT NULL, - FOREIGN KEY (file_id) REFERENCES file (file_id) - ) STRICT; - CREATE UNIQUE INDEX IF NOT EXISTS file_content - ON content (file_id, encoding); - `) + _, err = conn.Exec(string(q)) if err != nil { return nil, fault.Wrap(err, fmsg.With("creating tables")) } w := &Writer{ - db: db, + queries: db.New(conn), log: logger, options: opts, } - w.queries.insertURL, err = db.Prepare(`INSERT INTO url (path) VALUES (?)`) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("preparing insert URL statement")) - } - - w.queries.insertFile, err = db.Prepare(` - INSERT INTO file (url_id, content_type, last_modified, etag, style_hash, title) - VALUES (:url_id, :content_type, :last_modified, :etag, :style_hash, :title) - `) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("preparing insert file statement")) - } - - w.queries.insertContent, err = db.Prepare(` - INSERT INTO content (file_id, encoding, body) - VALUES (:file_id, :encoding, :body) - `) - if err != nil { - return nil, fault.Wrap(err, fmsg.With("preparing insert content statement")) - } - return w, nil }
@@ -129,16 +80,11 @@ return nil
} func (s *Writer) storeURL(path string) (int64, error) { - r, err := s.queries.insertURL.Exec(path) + id, err := s.queries.InsertURL(context.TODO(), path) if err != nil { return 0, fault.Wrap(err, fmsg.With(fmt.Sprintf("inserting URL %s into database", path))) } - id, err := r.LastInsertId() - if err != nil { - return 0, fault.Wrap(err) - } - return id, nil }
@@ -153,32 +99,28 @@ "sniffed",
file.ContentType, ) } - r, err := s.queries.insertFile.Exec( - sql.Named("url_id", urlID), - sql.Named("content_type", file.ContentType), - sql.Named("last_modified", file.LastModified.Unix()), - sql.Named("etag", file.Etag), - sql.Named("style_hash", file.StyleHash), - sql.Named("title", file.Title), - ) + params := db.InsertFileParams{ + UrlID: urlID, + ContentType: file.ContentType, + LastModified: file.LastModified.Unix(), + Etag: file.Etag, + StyleHash: file.StyleHash, + Title: file.Title, + } + id, err := s.queries.InsertFile(context.TODO(), params) if err != nil { return 0, fault.Wrap(err, fmsg.With("inserting file into database")) } - id, err := r.LastInsertId() - if err != nil { - return 0, fault.Wrap(err) - } - return id, nil } func (s *Writer) storeEncoding(fileID int64, encoding string, data []byte) error { - _, err := s.queries.insertContent.Exec( - sql.Named("file_id", fileID), - sql.Named("encoding", encoding), - sql.Named("body", data), - ) + err := s.queries.InsertContent(context.TODO(), db.InsertContentParams{ + Fileid: fileID, + Encoding: encoding, + Body: data, + }) if err != nil { return fault.Wrap( err,
A query.sql
@@ -0,0 +1,37 @@
+-- name: InsertURL :execlastid +INSERT INTO url (path) VALUES (?); + +-- name: InsertFile :execlastid +INSERT INTO file (url_id, content_type, last_modified, etag, style_hash, title) +VALUES ( + @url_id, + @content_type, + @last_modified, + @etag, + @style_hash, + @title +); + +-- name: InsertContent :exec +INSERT INTO content (file_id, encoding, body) +VALUES (@fileid, @encoding, @body); + +-- name: GetFile :many +SELECT + sqlc.embed(file), + sqlc.embed(content) +FROM url +INNER JOIN file + USING (url_id) +INNER JOIN content + USING (file_id) +WHERE + url.path = ?; + +-- name: CheckPath :one +SELECT + EXISTS( + SELECT 1 + FROM url + WHERE path = ? + ) AS differs;
A schema.sql
@@ -0,0 +1,29 @@
+CREATE TABLE IF NOT EXISTS url ( + url_id INTEGER PRIMARY KEY, + path TEXT NOT NULL +) STRICT; +CREATE UNIQUE INDEX IF NOT EXISTS url_path + ON url (path); + +CREATE TABLE IF NOT EXISTS file ( + file_id INTEGER PRIMARY KEY, + url_id INTEGER NOT NULL, + content_type TEXT NOT NULL, + last_modified INTEGER NOT NULL, + title TEXT NOT NULL, + etag TEXT NOT NULL, + style_hash TEXT NOT NULL, + FOREIGN KEY (url_id) REFERENCES url (url_id) +) STRICT; +CREATE UNIQUE INDEX IF NOT EXISTS file_url_content_type + ON file (url_id, content_type); + +CREATE TABLE IF NOT EXISTS content ( + content_id INTEGER PRIMARY KEY, + file_id INTEGER NOT NULL, + encoding TEXT NOT NULL, + body BLOB NOT NULL, + FOREIGN KEY (file_id) REFERENCES file (file_id) +) STRICT; +CREATE UNIQUE INDEX IF NOT EXISTS file_content + ON content (file_id, encoding);
A sqlc.yaml
@@ -0,0 +1,11 @@
+version: "2" +sql: + - engine: "sqlite" + queries: "query.sql" + schema: "schema.sql" + database: + uri: "file:db.sqlite3?mode=rwc" + gen: + go: + out: "internal/storage/sqlite/db" + emit_prepared_queries: true