2024-01-15 04:45:34 +03:00
|
|
|
package sqlite3
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
|
|
|
"log/slog"
|
|
|
|
"os"
|
|
|
|
"sync"
|
2024-01-27 01:18:38 +03:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/neilotoole/sq/libsq/core/lg"
|
2024-01-15 04:45:34 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/lg/lgm"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/progress"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/sqlz"
|
|
|
|
"github.com/neilotoole/sq/libsq/driver"
|
|
|
|
"github.com/neilotoole/sq/libsq/source"
|
2024-01-25 09:29:55 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/source/drivertype"
|
2024-01-15 04:45:34 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/source/metadata"
|
|
|
|
)
|
|
|
|
|
|
|
|
// grip implements driver.Grip.
|
|
|
|
type grip struct {
|
2024-01-27 10:11:24 +03:00
|
|
|
closeErr error
|
|
|
|
log *slog.Logger
|
|
|
|
db *sql.DB
|
|
|
|
src *source.Source
|
|
|
|
drvr *driveri
|
2024-01-15 04:45:34 +03:00
|
|
|
|
|
|
|
// closeOnce and closeErr are used to ensure that Close is only called once.
|
|
|
|
// This is particularly relevant to sqlite, as calling Close multiple times
|
|
|
|
// can cause problems on Windows.
|
|
|
|
closeOnce sync.Once
|
|
|
|
}
|
|
|
|
|
|
|
|
// DB implements driver.Grip.
|
|
|
|
func (g *grip) DB(context.Context) (*sql.DB, error) {
|
|
|
|
return g.db, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SQLDriver implements driver.Grip.
|
|
|
|
func (g *grip) SQLDriver() driver.SQLDriver {
|
|
|
|
return g.drvr
|
|
|
|
}
|
|
|
|
|
|
|
|
// Source implements driver.Grip.
|
|
|
|
func (g *grip) Source() *source.Source {
|
|
|
|
return g.src
|
|
|
|
}
|
|
|
|
|
|
|
|
// TableMetadata implements driver.Grip.
|
|
|
|
func (g *grip) TableMetadata(ctx context.Context, tblName string) (*metadata.Table, error) {
|
|
|
|
db, err := g.DB(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
bar := progress.FromContext(ctx).NewUnitCounter(g.Source().Handle+"."+tblName+": read schema", "item")
|
|
|
|
defer bar.Stop()
|
|
|
|
ctx = progress.NewBarContext(ctx, bar)
|
|
|
|
|
|
|
|
return getTableMetadata(ctx, db, tblName)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SourceMetadata implements driver.Grip.
|
|
|
|
func (g *grip) SourceMetadata(ctx context.Context, noSchema bool) (*metadata.Source, error) {
|
|
|
|
// https://stackoverflow.com/questions/9646353/how-to-find-sqlite-database-file-version
|
|
|
|
|
|
|
|
bar := progress.FromContext(ctx).NewUnitCounter(g.Source().Handle+": read schema", "item")
|
|
|
|
defer bar.Stop()
|
|
|
|
ctx = progress.NewBarContext(ctx, bar)
|
|
|
|
|
2024-01-27 01:18:38 +03:00
|
|
|
start := time.Now()
|
|
|
|
md, err := g.getSourceMetadata(ctx, noSchema)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
lg.FromContext(ctx).Debug("Read source metadata", lga.Src, g.src, lga.Elapsed, time.Since(start))
|
|
|
|
return md, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SourceMetadata implements driver.Grip.
|
|
|
|
func (g *grip) getSourceMetadata(ctx context.Context, noSchema bool) (*metadata.Source, error) {
|
|
|
|
// https://stackoverflow.com/questions/9646353/how-to-find-sqlite-database-file-version
|
|
|
|
|
2024-01-25 09:29:55 +03:00
|
|
|
md := &metadata.Source{Handle: g.src.Handle, Driver: drivertype.SQLite, DBDriver: dbDrvr}
|
2024-01-15 04:45:34 +03:00
|
|
|
|
|
|
|
dsn, err := PathFromLocation(g.src)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
const q = "SELECT sqlite_version(), (SELECT name FROM pragma_database_list ORDER BY seq limit 1);"
|
|
|
|
|
|
|
|
err = g.db.QueryRowContext(ctx, q).Scan(&md.DBVersion, &md.Schema)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errw(err)
|
|
|
|
}
|
|
|
|
progress.Incr(ctx, 1)
|
|
|
|
|
|
|
|
md.DBProduct = "SQLite3 v" + md.DBVersion
|
|
|
|
|
|
|
|
fi, err := os.Stat(dsn)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errw(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
md.Size = fi.Size()
|
|
|
|
md.Name = fi.Name()
|
|
|
|
md.FQName = fi.Name() + "." + md.Schema
|
|
|
|
// SQLite doesn't support catalog, but we conventionally set it to "default"
|
|
|
|
md.Catalog = "default"
|
|
|
|
md.Location = g.src.Location
|
|
|
|
|
|
|
|
md.DBProperties, err = getDBProperties(ctx, g.db)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if noSchema {
|
|
|
|
return md, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
md.Tables, err = getAllTableMetadata(ctx, g.db, md.Schema)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tbl := range md.Tables {
|
|
|
|
if tbl.TableType == sqlz.TableTypeTable {
|
|
|
|
md.TableCount++
|
|
|
|
} else if tbl.TableType == sqlz.TableTypeView {
|
|
|
|
md.ViewCount++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return md, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close implements driver.Grip. Subsequent calls to Close are no-op and
|
|
|
|
// return the same error.
|
|
|
|
func (g *grip) Close() error {
|
|
|
|
g.closeOnce.Do(func() {
|
|
|
|
g.log.Debug(lgm.CloseDB, lga.Handle, g.src.Handle)
|
|
|
|
g.closeErr = errw(g.db.Close())
|
|
|
|
})
|
|
|
|
|
|
|
|
return g.closeErr
|
|
|
|
}
|