0

I am try to use go-bindata and packr, but those packages do not show how to pack an SQLite database file in to a binary file.

I don't need to update the database in any way, I just want to read the data from it on startup.

How can I embed an SQLite database file in a Go binary file?

Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
Moon soon
  • 2,616
  • 2
  • 30
  • 51

1 Answers1

1

The SQLite driver can't read a database file from memory (e.g. from a byte slice). But you can write the data to a temporary file, and open that:

//go:generate go run gen.go

package main

import (
    "database/sql"
    "fmt"
    "io/ioutil"
    "log"
    "os"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // Create temporary file for database.
    tmpDB, err := ioutil.TempFile("", "db*.sqlite3")
    if err != nil {
        log.Fatal(err)
    }
    // Remove this file after on exit.
    defer func() {
        err := os.Remove(tmpDB.Name())
        if err != nil {
            log.Print(err)
        }
    }()

    // Write database to file.
    _, err = tmpDB.Write(sqlDB)
    if err != nil {
        log.Print(err)
    }
    err = tmpDB.Close()
    if err != nil {
        log.Print(err)
    }

    // Open DB.
    db, err := sql.Open("sqlite3", tmpDB.Name()+"?mode=ro")
    if err != nil {
        log.Fatal(err)
    }

    // Make sure it's loaded correct.
    rows, err := db.Query("select * from test")
    if err != nil {
        log.Fatal(err)
    }

    for rows.Next() {
        var c string
        err := rows.Scan(&c)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(c)
    }
}

And you can write the database to db.go with something like:

// +build generate

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "os"
    "strings"
)

func main() {
    // Read source database file.
    d, err := ioutil.ReadFile("source.sqlite3")
    if err != nil {
        log.Fatal(err)
    }

    fp, err := os.Create("db.go")
    if err != nil {
        log.Fatal(err)
    }

    _, err = fmt.Fprintf(fp, "// Code generated by gen.go; DO NOT EDIT.\n\n"+
        "package main\n\n"+
        "var sqlDB = %s\n", asbyte(d))
    if err != nil {
        log.Fatal(err)
    }
}

// Write any data as byte array.
func asbyte(s []byte) string {
    var b strings.Builder
    for i, c := range s {
        if i%19 == 0 {
            b.WriteString("\n\t\t")
        }
        b.WriteString(fmt.Sprintf("%#x, ", c))
    }
    return "[]byte{" + b.String() + "}"
}

You can also use go-bindata or packr for that if you prefer, but I don't really see an advantage.


An alternative way is to use a memory database, which may be faster depending on what you want to do.

  1. Embed the SQL schema and rows you want in your Go binary as strings.
  2. Open a new memory database when your program starts (sql.Open("sqlite3",:memory:`) and create the schema and insert the rows.

There is no disk access with this method, so querying it will probably be a bit faster at the expensive of slower startup times (benchmark to be sure!)

Martin Tournoij
  • 26,737
  • 24
  • 105
  • 146
  • I am trying to create a memory database, and init data from a `initSql` variable, this variable value is inject from `go build -ldflags "-X main.initSql=$(cat export.sql)"`, but the shell tell me: `bash: /usr/local/bin/go: Argument list too long` – Moon soon Aug 04 '19 at 01:18
  • Yeah, there's a limit on that @TangMonk; I believe it's about 2MB on modern Linux systems (see e.g. [this](https://unix.stackexchange.com/q/120642/33645)). You'll be better of using a `//go generate ...` directive to convert `export.sql` to a Go variable (like in my answer). – Martin Tournoij Aug 04 '19 at 01:48
  • I'm bringing this one back @MartinTournoij because I think I see a way now that we can embed starting in v1.16. I've written it up as a proposal on [mattn/go-sqlite3](https://github.com/mattn/go-sqlite3/issues/968) and would appreciate another set of eyes to see if I'm barking up the right tree. – Geek Stocks Aug 16 '21 at 08:11