From 34541d687fd02d0617f8772fbec0deb75d8a05fd Mon Sep 17 00:00:00 2001 From: Mason Payne Date: Mon, 19 Aug 2024 01:07:19 -0600 Subject: [PATCH] initial commit --- .idea/.gitignore | 8 +++ .idea/credit-based-payments.iml | 9 +++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ db/db.go | 89 +++++++++++++++++++++++++++++ db/embedded.go | 73 +++++++++++++++++++++++ db/migrations/001_create-tables.sql | 7 +++ db/remote-only.go | 41 +++++++++++++ go.mod | 17 ++++++ go.sum | 24 ++++++++ main.go | 26 +++++++++ readme.md | 16 ++++++ 12 files changed, 324 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/credit-based-payments.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 db/db.go create mode 100644 db/embedded.go create mode 100644 db/migrations/001_create-tables.sql create mode 100644 db/remote-only.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readme.md diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/credit-based-payments.iml b/.idea/credit-based-payments.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/credit-based-payments.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f157206 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..58f6de4 --- /dev/null +++ b/db/db.go @@ -0,0 +1,89 @@ +package db + +import ( + "database/sql" + "embed" + "fmt" + "io/fs" + "path/filepath" +) + +type Migrations struct { + name string + query string +} + +//go:embed migrations/*.sql +var migrationFiles embed.FS + +var migrations []Migrations + +// func NewLibSqlDB is defined in embedded.go and remote-only.go files +// these files are used to define the LibSqlDB struct and the NewLibSqlDB function +// they have different initializations based on the environment, embedded or remote-only +// Windows does not currently support the embedded database, so the remote-only file is used + +// setupMigrations initializes the filesystem and reads the migration files into the migrations variable +func setupMigrations() error { + // Walk through the embedded files and read their contents + err := fs.WalkDir(migrationFiles, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if !d.IsDir() { + content, err := migrationFiles.ReadFile(path) + if err != nil { + return err + } + + migration := Migrations{ + name: filepath.Base(path), + query: string(content), + } + migrations = append(migrations, migration) + } + return nil + }) + if err != nil { + return fmt.Errorf("error setting up migrations | %w", err) + } + return nil +} + +// Migrate updates the connected LibSqlDB to the latest schema based on the given migrations +func (t *LibSqlDB) Migrate() error { + // check if migration table exists + var migrationsCheck string + //goland:noinspection SqlResolve + err := t.db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&migrationsCheck) + if err != nil { + if err == sql.ErrNoRows { + _, err := t.db.Exec("CREATE TABLE migrations (name TEXT NOT NULL)") + if err != nil { + return fmt.Errorf("error creating migrations table | %w", err) + } + } else { + return fmt.Errorf("error checking if migrations table exists | %w", err) + } + } + + for _, migration := range migrations { + var migrationInHistory string + err = t.db.QueryRow("SELECT name FROM migrations WHERE name = ?", migration.name).Scan(&migrationInHistory) + if err != nil { + if err == sql.ErrNoRows { + _, err := t.db.Exec(migration.query) + if err != nil { + return fmt.Errorf("error running migration: %s | %w", migration.name, err) + } + _, err = t.db.Exec("INSERT INTO migrations (name) VALUES (?)", migration.name) + if err != nil { + return fmt.Errorf("error inserting migration: %s into migrations table | %w", migration.name, err) + } + } else { + return fmt.Errorf("error checking if migration: %s has been run | %w", migration.name, err) + } + } + } + return nil +} diff --git a/db/embedded.go b/db/embedded.go new file mode 100644 index 0000000..93d8f39 --- /dev/null +++ b/db/embedded.go @@ -0,0 +1,73 @@ +//go:build !windows + +package db + +import ( + "database/sql" + "fmt" + "github.com/tursodatabase/go-libsql" + "os" + "path/filepath" +) + +type LibSqlDB struct { + db *sql.DB + connector *libsql.Connector // only used for embedded replica + dir string // only used for embedded replica +} + +var syncInterval = 200 * time.Millisecond + +func NewLibSqlDB(primaryUrl, authToken, localDBName string) (*LibSqlDB, error) { + dir, err := os.MkdirTemp("", "libsql-*") + if err != nil { + fmt.Println("Error creating temporary directory:", err) + return nil, fmt.Errorf("error setting up temporary directory for local database | %w", err) + } + //defer os.RemoveAll(dir) + + dbPath := filepath.Join(dir, localDBName) + + connector, err := libsql.NewEmbeddedReplicaConnector(dbPath, primaryUrl, + libsql.WithAuthToken(authToken), + libsql.WithSyncInterval(syncInterval), + ) + if err != nil { + return nil, fmt.Errorf("error creating connector | %w", err) + } + + db := sql.OpenDB(connector) + + err = setupMigrations() + if err != nil { + return nil, fmt.Errorf("error setting up migrations | %w", err) + } + + return &LibSqlDB{ + db: db, + connector: connector, + dir: dir, + }, nil +} + +func (t *LibSqlDB) Close() error { + var resultError *multierror.Error + + if err := t.db.Close(); err != nil { + resultError = multierror.Append(resultError, fmt.Errorf("failed to close database: %w", err)) + } + + if t.connector != nil { + if err := t.connector.Close(); err != nil { + resultError = multierror.Append(resultError, fmt.Errorf("failed to close connector: %w", err)) + } + } + + if t.dir != "" { + if err := os.RemoveAll(t.dir); err != nil { + resultError = multierror.Append(resultError, fmt.Errorf("failed to remove directory %s: %w", t.dir, err)) + } + } + + return resultError.ErrorOrNil() +} diff --git a/db/migrations/001_create-tables.sql b/db/migrations/001_create-tables.sql new file mode 100644 index 0000000..f7df097 --- /dev/null +++ b/db/migrations/001_create-tables.sql @@ -0,0 +1,7 @@ +CREATE TABLE eventlog ( + eventid TEXT PRIMARY KEY, + name TEXT NOT NULL, + body BLOB, + metadata BLOB, + createdat TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/remote-only.go b/db/remote-only.go new file mode 100644 index 0000000..7648d38 --- /dev/null +++ b/db/remote-only.go @@ -0,0 +1,41 @@ +//go:build windows + +package db + +import ( + "database/sql" + "fmt" + "github.com/hashicorp/go-multierror" + _ "github.com/tursodatabase/libsql-client-go/libsql" +) + +type LibSqlDB struct { + db *sql.DB +} + +func NewLibSqlDB(primaryUrl, authToken, localDBName string) (*LibSqlDB, error) { + url := primaryUrl + "?authToken=" + authToken + db, err := sql.Open("libsql", url) + if err != nil { + return nil, fmt.Errorf("error setting up LibSQL db | %w", err) + } + + err = setupMigrations() + if err != nil { + return nil, fmt.Errorf("error setting up migrations | %w", err) + } + + return &LibSqlDB{ + db: db, + }, nil +} + +func (t *LibSqlDB) Close() error { + var resultError *multierror.Error + + if err := t.db.Close(); err != nil { + resultError = multierror.Append(resultError, fmt.Errorf("failed to close database: %w", err)) + } + + return resultError.ErrorOrNil() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d15f693 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module {{.ModulePath}} + +go 1.21 + +require ( + github.com/hashicorp/go-multierror v1.1.1 + github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b + github.com/tursodatabase/libsql-client-go v0.0.0-20240628122535-1c47b26184e8 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + nhooyr.io/websocket v1.8.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5a9dd45 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b h1:R7hev4b96zgXjKbS2ZNbHBnDvyFZhH+LlMqtKH6hIkU= +github.com/tursodatabase/go-libsql v0.0.0-20240429120401-651096bbee0b/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/tursodatabase/libsql-client-go v0.0.0-20240628122535-1c47b26184e8 h1:XM3aeBrpNrkvi48EiKCtMNAgsiaAaAOCHAW9SaIWouo= +github.com/tursodatabase/libsql-client-go v0.0.0-20240628122535-1c47b26184e8/go.mod h1:fblU7nZYWAROzJzkpln8teKFDtdRvAOmZHeIpahY4jk= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= +nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8afd520 --- /dev/null +++ b/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "os" + "{{.ModulePath}}/db" +) + +func main() { + primaryUrl := os.Getenv("LIBSQL_DATABASE_URL") + authToken := os.Getenv("LIBSQL_AUTH_TOKEN") + + tdb, err := db.NewLibSqlDB(primaryUrl, authToken, "local.db") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open db %s: %s", primaryUrl, err) + os.Exit(1) + } + + err = tdb.Migrate() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to migrate db %s: %s", primaryUrl, err) + os.Exit(1) + } + + defer tdb.Close() +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..58c95f6 --- /dev/null +++ b/readme.md @@ -0,0 +1,16 @@ +# LibSQL Boilerplate Golang + +Copy this repo to have a golang application that is already set up to work with libSQL. + +## How to use + +1. Copy this repo to your new project +2. Modify two files. + +go.mod needs a new package name. +main.go needs to use the package name to import from `{{packageName}}/db` + +3. Set up your environment variables + +LIBSQL_AUTH_TOKEN +LIBSQL_DATABASE_URL \ No newline at end of file