start changes to make into an importable library

This commit is contained in:
2024-08-21 23:38:18 -06:00
parent 74c70e6723
commit cdc8fc4e5e
7 changed files with 235 additions and 121 deletions

140
libsqlDB/db.go Normal file
View File

@ -0,0 +1,140 @@
package libsqlDB
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"path/filepath"
"time"
)
type Migrations struct {
name string
query string
}
type Options func(*LibSqlDB) error
// use something like this in the user's code -> //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
}
// WithLocalDBName sets the local database name for the embedded database
func WithLocalDBName(localDBName string) Options {
return func(l *LibSqlDB) error {
l.localDBName = localDBName
return nil
}
}
// WithSyncInterval sets the sync interval for the embedded database
func WithSyncInterval(syncInterval time.Duration) Options {
return func(l *LibSqlDB) error {
l.syncInterval = syncInterval
return nil
}
}
// WithDir sets the directory for the embedded database
func WithDir(dir string) Options {
return func(l *LibSqlDB) error {
l.dir = dir
return nil
}
}
// WithAuthToken sets the auth token for the database
func WithAuthToken(authToken string) Options {
return func(l *LibSqlDB) error {
l.authToken = authToken
return nil
}
}
// WithEncryptionKey sets the encryption key for the embedded database
func WithEncryptionKey(key string) Options {
return func(l *LibSqlDB) error {
l.encryptionKey = key
return nil
}
}
// WithReadYourWrites sets the encryption key for the embedded database
func WithReadYourWrites(readYourWrites bool) Options {
return func(l *LibSqlDB) error {
l.readYourWrites = &readYourWrites
return nil
}
}

110
libsqlDB/embedded.go Normal file
View File

@ -0,0 +1,110 @@
//go:build !windows
package libsqlDB
import (
"database/sql"
"embed"
"fmt"
"os"
"path/filepath"
)
type LibSqlDB struct {
db *sql.DB
connector *libsql.Connector // only used for embedded replica
dir string // only used for embedded replica
localDBName string // only used for embedded replica
authToken string
syncInterval time.Duration // only used for embedded replica
encryptionKey string // only used for embedded replica
readYourWrites *bool // only used for embedded replica
}
var syncInterval = 200 * time.Millisecond
func NewLibSqlDB(primaryUrl string, migrationFiles embed.FS, opts ...Options) (*LibSqlDB, error) {
l := libSqlDefaults()
for _, option := range opts {
err := option(l)
if err != nil {
return nil, fmt.Errorf("error applying options | %w", err)
}
}
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, l.localDBName)
var lsOpts []libsql.Option
if l.authToken != "" {
lsOpts = append(lsOpts, libsql.WithAuthToken(l.authToken))
}
if l.syncInterval != 0 {
lsOpts = append(lsOpts, libsql.WithSyncInterval(l.syncInterval))
}
if l.encryptionKey != "" {
lsOpts = append(lsOpts, libsql.WithEncryption(l.encryptionKey))
}
if l.readYourWrites != nil {
lsOpts = append(lsOpts, libsql.WithReadYourWrites(l.readYourWrites))
}
connector, err := libsql.NewEmbeddedReplicaConnector(dbPath, primaryUrl,
lsOpts,
)
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)
}
l.db = db
l.connector = connector
l.dir = dir
return l, nil
}
func libSqlDefaults() *LibSqlDB {
return &LibSqlDB{
localDBName: "local.db",
}
}
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()
}

View File

@ -0,0 +1,7 @@
CREATE TABLE eventlog (
eventid TEXT PRIMARY KEY,
name TEXT NOT NULL,
body BLOB,
metadata BLOB,
createdat TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

64
libsqlDB/remote-only.go Normal file
View File

@ -0,0 +1,64 @@
//go:build windows
package libsqlDB
import (
"database/sql"
"embed"
"fmt"
"time"
multierror "github.com/hashicorp/go-multierror"
_ "github.com/tursodatabase/libsql-client-go/libsql"
)
type LibSqlDB struct {
db *sql.DB
dir string // only used for embedded replica
localDBName string // only used for embedded replica but needs to be here for consistency
authToken string
syncInterval time.Duration // only used for embedded replica
encryptionKey string // only used for embedded replica
readYourWrites *bool // only used for embedded replica
}
func NewLibSqlDB(primaryUrl string, migrationFiles embed.FS, opts ...Options) (*LibSqlDB, error) {
l := &LibSqlDB{}
for _, option := range opts {
err := option(l)
if err != nil {
return nil, fmt.Errorf("error applying options | %w", err)
}
}
url := primaryUrl
if l.authToken != "" {
url = primaryUrl + "?authToken=" + l.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()
}