package main import ( "database/sql" "errors" "fmt" "path/filepath" "unsafe" _ "yvbolt/sqliteclidriver" "github.com/ying32/govcl/vcl" ) 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) error { if len(ndata.bucketPath) == 0 { // Top-level f.propertiesBox.SetText("Please select...") return nil } else if len(ndata.bucketPath) == 1 { // Category (tables, ...) f.propertiesBox.SetText("Please select...") return nil } 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)) // 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 { return fmt.Errorf("Failed to load columns for table %q: %w", tableName) } 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 { return fmt.Errorf("Failed to load data for table %q: %w", tableName, err) } defer datar.Close() populateRows(datar, f.contentBox) // We successfully populated the data grid vcl_stringgrid_columnwidths(f.contentBox) f.contentBox.SetEnabled(true) return nil } else { // ??? unknown return errors.New("?") } } func (n *sqliteLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error { return ErrNotSupported } 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.TStringGrid) { for _, columnName := range names { col := dest.Columns().Add() col.Title().SetCaption(columnName) } } func populateRows(rr *sql.Rows, dest *vcl.TStringGrid) { 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 } rpos := dest.RowCount() dest.SetRowCount(rpos + 1) for i := 0; i < len(fields); i += 1 { dest.SetCells(int32(i), rpos, formatAny(fields[i])) } } if rr.Err() != nil { vcl.ShowMessageFmt("Failed to load data: %s", rr.Err().Error()) return } } func (ld *sqliteLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error { rr, err := ld.db.Query(query) if err != nil { return err } defer rr.Close() vcl_stringgrid_clear(resultArea) columns, err := rr.Columns() if err != nil { return err } populateColumns(columns, resultArea) populateRows(rr, resultArea) vcl_stringgrid_columnwidths(resultArea) resultArea.SetEnabled(true) return nil } 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) } func (ld *sqliteLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) { return nil, nil // No special actions are supported } func (ld *sqliteLoadedDatabase) Close() { _ = ld.db.Close() ld.arena = nil } var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion // func (f *TMainForm) sqliteAddDatabaseFromFile(path string, cliDriver bool) { driver := "sqlite3" if cliDriver { driver = "sqliteclidriver" } // TODO load in background thread to stop blocking the UI db, err := sql.Open(driver, 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) f.Buckets.SetSelected(ld.nav) // Select new element ld.Keepalive(navData) }