Working migration cmd
							parent
							
								
									fc397d83c2
								
							
						
					
					
						commit
						a03cf82681
					
				@ -1,3 +1,3 @@
 | 
				
			|||||||
FLUX_DATABASE_DRIVER=libsql
 | 
					FLUX_DATABASE_DRIVER=sqlite3
 | 
				
			||||||
FLUX_DATABASE_PATH=file:./flux-local.db
 | 
					FLUX_DATABASE_DSN=./flux-local.db
 | 
				
			||||||
FLUX_MIGRATIONS_PATH=./migrations
 | 
					FLUX_MIGRATIONS_PATH=./migrations
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,40 @@
 | 
				
			|||||||
 | 
					// Entry point for applying database migrations for flux-feed application
 | 
				
			||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"git.runcible.io/androiddrew/flux-feed/config"
 | 
						"git.runcible.io/androiddrew/flux-feed/config"
 | 
				
			||||||
	"git.runcible.io/androiddrew/flux-feed/database"
 | 
						"github.com/golang-migrate/migrate/v4"
 | 
				
			||||||
 | 
						"github.com/golang-migrate/migrate/v4/database/sqlite3"
 | 
				
			||||||
 | 
						_ "github.com/golang-migrate/migrate/v4/source/file"
 | 
				
			||||||
 | 
						_ "github.com/mattn/go-sqlite3"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	cfg := config.New()
 | 
						cfg := config.New()
 | 
				
			||||||
	dbHandle := database.NewSqlx(cfg.Database)
 | 
						db, err := sql.Open(cfg.DatabaseDriver, cfg.DatabaseDSN)
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = db.Ping()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Printf("Connected to Database: %s", cfg.DatabaseDSN)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Printf("Using migrations path: %s", cfg.MigrationsPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						m, err := migrate.NewWithDatabaseInstance(
 | 
				
			||||||
 | 
							cfg.MigrationsPath, cfg.DatabaseDriver, driver)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Printf("Migrating: %s", cfg.DatabaseDSN)
 | 
				
			||||||
 | 
						err = m.Up()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,269 +0,0 @@
 | 
				
			|||||||
package libsql
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"database/sql"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	nurl "net/url"
 | 
					 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"go.uber.org/atomic"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/golang-migrate/migrate/v4"
 | 
					 | 
				
			||||||
	"github.com/golang-migrate/migrate/v4/database"
 | 
					 | 
				
			||||||
	"github.com/hashicorp/go-multierror"
 | 
					 | 
				
			||||||
	_ "github.com/tursodatabase/go-libsql"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	database.Register("libsql", &Sqlite{})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var DefaultMigrationsTable = "schema_migrations"
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	ErrDatabaseDirty  = fmt.Errorf("database is dirty")
 | 
					 | 
				
			||||||
	ErrNilConfig      = fmt.Errorf("no config")
 | 
					 | 
				
			||||||
	ErrNoDatabaseName = fmt.Errorf("no database name")
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Config struct {
 | 
					 | 
				
			||||||
	MigrationsTable string
 | 
					 | 
				
			||||||
	DatabaseName    string
 | 
					 | 
				
			||||||
	NoTxWrap        bool
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Sqlite struct {
 | 
					 | 
				
			||||||
	db       *sql.DB
 | 
					 | 
				
			||||||
	isLocked atomic.Bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	config *Config
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
 | 
					 | 
				
			||||||
	if config == nil {
 | 
					 | 
				
			||||||
		return nil, ErrNilConfig
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := instance.Ping(); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(config.MigrationsTable) == 0 {
 | 
					 | 
				
			||||||
		config.MigrationsTable = DefaultMigrationsTable
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mx := &Sqlite{
 | 
					 | 
				
			||||||
		db:     instance,
 | 
					 | 
				
			||||||
		config: config,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := mx.ensureVersionTable(); err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return mx, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// ensureVersionTable checks if versions table exists and, if not, creates it.
 | 
					 | 
				
			||||||
// Note that this function locks the database, which deviates from the usual
 | 
					 | 
				
			||||||
// convention of "caller locks" in the Sqlite type.
 | 
					 | 
				
			||||||
func (m *Sqlite) ensureVersionTable() (err error) {
 | 
					 | 
				
			||||||
	if err = m.Lock(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	defer func() {
 | 
					 | 
				
			||||||
		if e := m.Unlock(); e != nil {
 | 
					 | 
				
			||||||
			if err == nil {
 | 
					 | 
				
			||||||
				err = e
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				err = multierror.Append(err, e)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	query := fmt.Sprintf(`
 | 
					 | 
				
			||||||
	CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool);
 | 
					 | 
				
			||||||
  CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version);
 | 
					 | 
				
			||||||
  `, m.config.MigrationsTable, m.config.MigrationsTable)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if _, err := m.db.Exec(query); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Open(url string) (database.Driver, error) {
 | 
					 | 
				
			||||||
	purl, err := nurl.Parse(url)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "sqlite://", "", 1)
 | 
					 | 
				
			||||||
	db, err := sql.Open("libsql", dbfile)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	qv := purl.Query()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	migrationsTable := qv.Get("x-migrations-table")
 | 
					 | 
				
			||||||
	if len(migrationsTable) == 0 {
 | 
					 | 
				
			||||||
		migrationsTable = DefaultMigrationsTable
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	noTxWrap := false
 | 
					 | 
				
			||||||
	if v := qv.Get("x-no-tx-wrap"); v != "" {
 | 
					 | 
				
			||||||
		noTxWrap, err = strconv.ParseBool(v)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("x-no-tx-wrap: %s", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mx, err := WithInstance(db, &Config{
 | 
					 | 
				
			||||||
		DatabaseName:    purl.Path,
 | 
					 | 
				
			||||||
		MigrationsTable: migrationsTable,
 | 
					 | 
				
			||||||
		NoTxWrap:        noTxWrap,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return mx, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Close() error {
 | 
					 | 
				
			||||||
	return m.db.Close()
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Drop() (err error) {
 | 
					 | 
				
			||||||
	query := `SELECT name FROM sqlite_master WHERE type = 'table';`
 | 
					 | 
				
			||||||
	tables, err := m.db.Query(query)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer func() {
 | 
					 | 
				
			||||||
		if errClose := tables.Close(); errClose != nil {
 | 
					 | 
				
			||||||
			err = multierror.Append(err, errClose)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	tableNames := make([]string, 0)
 | 
					 | 
				
			||||||
	for tables.Next() {
 | 
					 | 
				
			||||||
		var tableName string
 | 
					 | 
				
			||||||
		if err := tables.Scan(&tableName); err != nil {
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if len(tableName) > 0 {
 | 
					 | 
				
			||||||
			tableNames = append(tableNames, tableName)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := tables.Err(); err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(tableNames) > 0 {
 | 
					 | 
				
			||||||
		for _, t := range tableNames {
 | 
					 | 
				
			||||||
			query := "DROP TABLE " + t
 | 
					 | 
				
			||||||
			err = m.executeQuery(query)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		query := "VACUUM"
 | 
					 | 
				
			||||||
		_, err = m.db.Query(query)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Lock() error {
 | 
					 | 
				
			||||||
	if !m.isLocked.CAS(false, true) {
 | 
					 | 
				
			||||||
		return database.ErrLocked
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Unlock() error {
 | 
					 | 
				
			||||||
	if !m.isLocked.CAS(true, false) {
 | 
					 | 
				
			||||||
		return database.ErrNotLocked
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Run(migration io.Reader) error {
 | 
					 | 
				
			||||||
	migr, err := io.ReadAll(migration)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	query := string(migr[:])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if m.config.NoTxWrap {
 | 
					 | 
				
			||||||
		return m.executeQueryNoTx(query)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return m.executeQuery(query)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) executeQuery(query string) error {
 | 
					 | 
				
			||||||
	tx, err := m.db.Begin()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Err: "transaction start failed"}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if _, err := tx.Exec(query); err != nil {
 | 
					 | 
				
			||||||
		if errRollback := tx.Rollback(); errRollback != nil {
 | 
					 | 
				
			||||||
			err = multierror.Append(err, errRollback)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := tx.Commit(); err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Err: "transaction commit failed"}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) executeQueryNoTx(query string) error {
 | 
					 | 
				
			||||||
	if _, err := m.db.Exec(query); err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) SetVersion(version int, dirty bool) error {
 | 
					 | 
				
			||||||
	tx, err := m.db.Begin()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Err: "transaction start failed"}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	query := "DELETE FROM " + m.config.MigrationsTable
 | 
					 | 
				
			||||||
	if _, err := tx.Exec(query); err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Also re-write the schema version for nil dirty versions to prevent
 | 
					 | 
				
			||||||
	// empty schema version for failed down migration on the first migration
 | 
					 | 
				
			||||||
	// See: https://github.com/golang-migrate/migrate/issues/330
 | 
					 | 
				
			||||||
	if version >= 0 || (version == database.NilVersion && dirty) {
 | 
					 | 
				
			||||||
		query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (?, ?)`, m.config.MigrationsTable)
 | 
					 | 
				
			||||||
		if _, err := tx.Exec(query, version, dirty); err != nil {
 | 
					 | 
				
			||||||
			if errRollback := tx.Rollback(); errRollback != nil {
 | 
					 | 
				
			||||||
				err = multierror.Append(err, errRollback)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return &database.Error{OrigErr: err, Query: []byte(query)}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err := tx.Commit(); err != nil {
 | 
					 | 
				
			||||||
		return &database.Error{OrigErr: err, Err: "transaction commit failed"}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (m *Sqlite) Version() (version int, dirty bool, err error) {
 | 
					 | 
				
			||||||
	query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1"
 | 
					 | 
				
			||||||
	err = m.db.QueryRow(query).Scan(&version, &dirty)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return database.NilVersion, false, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return version, dirty, nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +0,0 @@
 | 
				
			|||||||
package database
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import "database/sql"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Migrator struct {
 | 
					 | 
				
			||||||
	DB *sql.DB
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,17 +0,0 @@
 | 
				
			|||||||
package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const FluxGreeting string = "Welcome to Flux Feed\n"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func HelloFluxFeed(out io.Writer) {
 | 
					 | 
				
			||||||
	fmt.Fprint(out, FluxGreeting)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func main() {
 | 
					 | 
				
			||||||
	HelloFluxFeed(os.Stdout)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
package main
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestCliGreeting(t *testing.T) {
 | 
					 | 
				
			||||||
	buffer := &bytes.Buffer{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	HelloFluxFeed(buffer)
 | 
					 | 
				
			||||||
	got := buffer.String()
 | 
					 | 
				
			||||||
	want := FluxGreeting
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if got != want {
 | 
					 | 
				
			||||||
		t.Errorf("Got %s but wanted %s", got, want)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					DROP TABLE IF EXISTS user;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS feed;
 | 
				
			||||||
 | 
					DROP TABLE IF EXISTS entry;
 | 
				
			||||||
@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					PRAGMA foreign_keys=1;
 | 
				
			||||||
 | 
					CREATE TABLE user (
 | 
				
			||||||
 | 
					    id INTEGER PRIMARY KEY AUTOINCREMENT, 
 | 
				
			||||||
 | 
					    email TEXT UNIQUE NOT NULL, 
 | 
				
			||||||
 | 
					    password TEXT NOT NULL, 
 | 
				
			||||||
 | 
					    kindle_email TEXT
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE feed (
 | 
				
			||||||
 | 
					    id INTEGER PRIMARY KEY AUTOINCREMENT, 
 | 
				
			||||||
 | 
					    user_id INTEGER UNIQUE NOT NULL, 
 | 
				
			||||||
 | 
					    url TEXT, 
 | 
				
			||||||
 | 
					    type TEXT NOT NULL, 
 | 
				
			||||||
 | 
					    name TEXT UNIQUE, 
 | 
				
			||||||
 | 
					    icon_url TEXT, 
 | 
				
			||||||
 | 
					    created TIMESTAMP NOT NULL, 
 | 
				
			||||||
 | 
					    updated TIMESTAMP NOT NULL, 
 | 
				
			||||||
 | 
					    last_fetch TIMESTAMP, 
 | 
				
			||||||
 | 
					    raw_data TEXT, 
 | 
				
			||||||
 | 
					    folder TEXT, 
 | 
				
			||||||
 | 
					    etag TEXT, 
 | 
				
			||||||
 | 
					    modified_header TEXT, 
 | 
				
			||||||
 | 
					    filters TEXT, 
 | 
				
			||||||
 | 
					    FOREIGN KEY(user_id) REFERENCES user(id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE entry (
 | 
				
			||||||
 | 
					    id INTEGER PRIMARY KEY AUTOINCREMENT, 
 | 
				
			||||||
 | 
					    feed_id INTEGER UNIQUE, 
 | 
				
			||||||
 | 
					    user_id INTEGER NOT NULL, 
 | 
				
			||||||
 | 
					    remote_id TEXT UNIQUE NOT NULL, 
 | 
				
			||||||
 | 
					    title TEXT, 
 | 
				
			||||||
 | 
					    username TEXT, 
 | 
				
			||||||
 | 
					    user_url TEXT, 
 | 
				
			||||||
 | 
					    display_name TEXT, 
 | 
				
			||||||
 | 
					    avatar_url TEXT, 
 | 
				
			||||||
 | 
					    content_short TEXT, 
 | 
				
			||||||
 | 
					    content_full TEXT, 
 | 
				
			||||||
 | 
					    target_url TEXT, 
 | 
				
			||||||
 | 
					    content_url TEXT, 
 | 
				
			||||||
 | 
					    comments_url TEXT, 
 | 
				
			||||||
 | 
					    media_url TEXT, 
 | 
				
			||||||
 | 
					    created TIMESTAMP NOT NULL, 
 | 
				
			||||||
 | 
					    updated TIMESTAMP NOT NULL, 
 | 
				
			||||||
 | 
					    display_date TIMESTAMP NOT NULL, 
 | 
				
			||||||
 | 
					    sort_date TIMESTAMP NOT NULL, 
 | 
				
			||||||
 | 
					    viewed TIMESTAMP, 
 | 
				
			||||||
 | 
					    favorited TIMESTAMP, 
 | 
				
			||||||
 | 
					    pinned TIMESTAMP, 
 | 
				
			||||||
 | 
					    sent_to_kindle TIMESTAMP, 
 | 
				
			||||||
 | 
					    raw_data TEXT, 
 | 
				
			||||||
 | 
					    header TEXT, 
 | 
				
			||||||
 | 
					    icon_url TEXT, 
 | 
				
			||||||
 | 
					    FOREIGN KEY(feed_id) REFERENCES feed(id), 
 | 
				
			||||||
 | 
					    FOREIGN KEY(user_id) REFERENCES user(id)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX ix_feed_folder ON feed(folder);
 | 
				
			||||||
 | 
					CREATE INDEX ix_feed_user_id ON feed(user_id);
 | 
				
			||||||
 | 
					CREATE INDEX ix_name_user ON feed(user_id, name);
 | 
				
			||||||
 | 
					CREATE INDEX ix_feed_created ON feed(created);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_favorited ON entry(favorited);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_created ON entry(created);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_sort_date ON entry(sort_date);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_sent_to_kindle ON entry(sent_to_kindle);
 | 
				
			||||||
 | 
					CREATE INDEX entry_sort_ts ON entry(sort_date DESC);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_viewed ON entry(viewed);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_pinned ON entry(pinned);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_user_id ON entry(user_id);
 | 
				
			||||||
 | 
					CREATE INDEX ix_entry_username ON entry(username);
 | 
				
			||||||
					Loading…
					
					
				
		Reference in New Issue