package main import ( "database/sql" "fmt" "path/filepath" "unsafe" sqlite3 "github.com/mattn/go-sqlite3" "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" ) 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) DriverName() string { ver1, _, _ := sqlite3.Version() return "SQLite " + ver1 } 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() } else if len(ndata.bucketPath) == 2 && ndata.bucketPath[0] == sqliteTablesCaption { // Render for specific table 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)) f.contentBox.SetEnabled(false) f.contentBox.Clear() // Load column details // Use SELECT form instead of common PRAGMA table_info so we can just get names // 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) if err != nil { vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error()) return } populateColumns(columnNames, f.contentBox) // Select count(*) so we know to display a warning if there are too many entries // TODO // Select * with small limit 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) // We successfully populated the data grid f.contentBox.SetEnabled(true) } else { // ??? unknown } } 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() { var columnName string err = colr.Scan(&columnName) if err != nil { return nil, fmt.Errorf("Scan: %w", colr.Err()) } ret = append(ret, columnName) } if colr.Err() != nil { return nil, colr.Err() } return ret, nil } 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) } } 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 } } } func (ld *sqliteLoadedDatabase) NavChildren(ndata *navData) ([]string, error) { if len(ndata.bucketPath) == 0 { // The top-level children are always: return []string{sqliteTablesCaption}, nil } if len(ndata.bucketPath) == 1 && ndata.bucketPath[0] == sqliteTablesCaption { 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 } if len(ndata.bucketPath) == 2 { return nil, nil // Never any deeper children } return nil, fmt.Errorf("unknown nav path %#v", ndata.bucketPath) } var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion // func (f *TMainForm) sqliteAddDatabaseFromFile(path string) { // 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) 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) }