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 colr, err := ld.db.Query(`SELECT name FROM pragma_table_info(?)`, tableName) if err != nil { vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error()) return } defer colr.Close() f.contentBox.Columns().Clear() numColumns := 0 for colr.Next() { var columnName string err = colr.Scan(&columnName) if err != nil { vcl.ShowMessageFmt("Failed to read column names for table %q: %s", tableName, err.Error()) return } col := f.contentBox.Columns().Add() col.SetCaption(columnName) col.SetWidth(MY_WIDTH) col.SetAlignment(types.TaLeftJustify) numColumns++ } if colr.Err() != nil { vcl.ShowMessageFmt("Failed to load columns for table %q: %s", tableName, err.Error()) return } colr.Close() // will be double-closed // 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() for datar.Next() { fields := make([]interface{}, numColumns) pfields := make([]interface{}, numColumns) for i := 0; i < numColumns; i += 1 { pfields[i] = &fields[i] } err = datar.Scan(pfields...) if err != nil { vcl.ShowMessageFmt("Failed to load data for table %q: %s", tableName, err.Error()) return } dataEntry := f.contentBox.Items().Add() dataEntry.SetCaption(formatAny(fields[0])) for i := 1; i < len(fields); i += 1 { dataEntry.SubItems().Add(formatAny(fields[i])) } } // We successfully populated the data grid f.contentBox.SetEnabled(true) } else { // ??? unknown } } 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) }