yvbolt/db_sqlite.go

267 lines
6.2 KiB
Go
Raw Normal View History

2024-06-08 01:44:18 +00:00
package main
import (
"database/sql"
"fmt"
"path/filepath"
"unsafe"
"github.com/ying32/govcl/vcl"
2024-06-08 02:49:57 +00:00
"github.com/ying32/govcl/vcl/types"
2024-06-08 01:44:18 +00:00
)
const (
sqliteTablesCaption = "Tables"
)
type sqliteLoadedDatabase struct {
displayName string
path string
db *sql.DB
nav *vcl.TTreeNode
arena []*navData // keepalive
}
func (ld *sqliteLoadedDatabase) DisplayName() string {
return ld.displayName
}
func (ld *sqliteLoadedDatabase) RootElement() *vcl.TTreeNode {
return ld.nav
}
func (ld *sqliteLoadedDatabase) Keepalive(ndata *navData) {
ld.arena = append(ld.arena, ndata)
}
func (ld *sqliteLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
if len(ndata.bucketPath) == 0 {
// Top-level
f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
} else if len(ndata.bucketPath) == 1 {
// Category (tables, ...)
f.propertiesBox.SetText("Please select...")
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
2024-06-08 02:23:38 +00:00
} else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption {
2024-06-08 01:44:18 +00:00
// Render for specific table
2024-06-08 02:23:38 +00:00
tableName := ndata.bucketPath[1]
// Get some basic properties
r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName)
var schemaStmt string
err := r.Scan(&schemaStmt)
if err != nil {
schemaStmt = fmt.Sprintf("* Failed to describe table %q: %s", tableName, err.Error())
}
// Display table properties
f.propertiesBox.SetText(fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt))
2024-06-08 02:49:57 +00:00
f.contentBox.SetEnabled(false)
f.contentBox.Clear()
2024-06-08 02:23:38 +00:00
// Load column details
2024-06-08 02:49:57 +00:00
// Use SELECT form instead of common PRAGMA table_info so we can just get names
2024-06-08 03:02:02 +00:00
// We could possibly get this from the main data select, but this will
// work even when there are 0 results
columnNames, err := ld.sqliteGetColumnNamesForTable(tableName)
2024-06-08 02:49:57 +00:00
if err != nil {
vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error())
return
}
populateColumns(columnNames, f.contentBox)
2024-06-08 02:49:57 +00:00
// Select count(*) so we know to display a warning if there are too many entries
// TODO
2024-06-08 02:23:38 +00:00
2024-06-08 01:44:18 +00:00
// Select * with small limit
2024-06-08 03:02:02 +00:00
datar, err := ld.db.Query(`SELECT * FROM ` + tableName + ` LIMIT 1000`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
if err != nil {
vcl.ShowMessageFmt("Failed to load data for table %q: %s", tableName, err.Error())
return
}
defer datar.Close()
populateRows(datar, f.contentBox)
2024-06-08 03:02:02 +00:00
// We successfully populated the data grid
f.contentBox.SetEnabled(true)
2024-06-08 03:02:02 +00:00
} else {
// ??? unknown
2024-06-08 03:02:02 +00:00
}
}
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info(?)`, tableName)
if err != nil {
return nil, fmt.Errorf("Query: %w", err)
}
defer colr.Close()
var ret []string
for colr.Next() {
2024-06-08 03:02:02 +00:00
var columnName string
err = colr.Scan(&columnName)
if err != nil {
return nil, fmt.Errorf("Scan: %w", colr.Err())
2024-06-08 03:02:02 +00:00
}
2024-06-08 02:49:57 +00:00
ret = append(ret, columnName)
}
if colr.Err() != nil {
return nil, colr.Err()
}
2024-06-08 01:44:18 +00:00
return ret, nil
}
2024-06-08 01:44:18 +00:00
func populateColumns(names []string, dest *vcl.TListView) {
dest.Columns().Clear()
for _, columnName := range names {
col := dest.Columns().Add()
col.SetCaption(columnName)
col.SetWidth(MY_WIDTH)
col.SetAlignment(types.TaLeftJustify)
2024-06-08 01:44:18 +00:00
}
}
func populateRows(rr *sql.Rows, dest *vcl.TListView) {
numColumns := int(dest.Columns().Count())
for rr.Next() {
fields := make([]interface{}, numColumns)
pfields := make([]interface{}, numColumns)
for i := 0; i < numColumns; i += 1 {
pfields[i] = &fields[i]
}
err := rr.Scan(pfields...)
if err != nil {
vcl.ShowMessageFmt("Failed to load data: %s", err.Error())
return
}
dataEntry := dest.Items().Add()
dataEntry.SetCaption(formatAny(fields[0]))
for i := 1; i < len(fields); i += 1 {
dataEntry.SubItems().Add(formatAny(fields[i]))
}
}
if rr.Err() != nil {
vcl.ShowMessageFmt("Failed to load data: %s", rr.Err().Error())
return
}
}
2024-06-08 01:44:18 +00:00
2024-06-15 00:14:11 +00:00
func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) {
rr, err := ld.db.Query(query)
if err != nil {
vcl.ShowMessage(err.Error())
return
}
defer rr.Close()
resultArea.SetEnabled(false)
resultArea.Clear()
columns, err := rr.Columns()
if err != nil {
vcl.ShowMessage(err.Error())
return
}
populateColumns(columns, resultArea)
populateRows(rr, resultArea)
resultArea.SetEnabled(true)
2024-06-08 01:44:18 +00:00
}
func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
if len(ndata.bucketPath) == 0 {
// The top-level children are always:
return []string{sqliteTablesCaption}, nil
}
2024-06-08 02:23:38 +00:00
if len(ndata.bucketPath) == 1 && ndata.bucketPath[0] == sqliteTablesCaption {
2024-06-08 01:44:18 +00:00
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name ASC;`)
if err != nil {
return nil, err
}
defer rr.Close()
var gather []string
for rr.Next() {
var tableName string
err = rr.Scan(&tableName)
if err != nil {
return nil, err
}
gather = append(gather, tableName)
}
if rr.Err() != nil {
return nil, rr.Err()
}
return gather, nil
}
2024-06-08 02:23:38 +00:00
if len(ndata.bucketPath) == 2 {
return nil, nil // Never any deeper children
}
2024-06-08 01:44:18 +00:00
return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath)
}
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
//
2024-06-08 02:23:38 +00:00
func (f *TMainForm) sqliteAddDatabaseFromFile(path string) {
2024-06-08 01:44:18 +00:00
// TODO load in background thread to stop blocking the UI
db, err := sql.Open("sqlite3", path)
if err != nil {
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
return
}
ld := &sqliteLoadedDatabase{
path: path,
displayName: filepath.Base(path),
db: db,
}
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
ld.nav.SetImageIndex(imgDatabase)
ld.nav.SetSelectedIndex(imgDatabase)
2024-06-08 01:44:18 +00:00
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
navData := &navData{
ld: ld,
childrenLoaded: false, // will be loaded dynamically
bucketPath: []string{}, // empty = root
}
ld.nav.SetData(unsafe.Pointer(navData))
f.dbs = append(f.dbs, ld)
ld.Keepalive(navData)
}