package main import ( "database/sql" "encoding/csv" "encoding/json" "fmt" "os" "strings" "inou/lib" _ "github.com/mattn/go-sqlite3" ) const dbPath = "/tank/inou/data/inou.db" func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, "Usage: dbquery [OPTIONS] ") fmt.Fprintln(os.Stderr, " Runs SQL against inou.db, decrypts fields, outputs JSON (default).") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Options:") fmt.Fprintln(os.Stderr, " -csv Output as CSV") fmt.Fprintln(os.Stderr, " -table Output as formatted table") fmt.Fprintln(os.Stderr, "") fmt.Fprintln(os.Stderr, "Example: dbquery \"SELECT * FROM entries LIMIT 5\"") os.Exit(1) } format := "json" args := os.Args[1:] if len(args) > 0 && (args[0] == "-csv" || args[0] == "--csv") { format = "csv" args = args[1:] } else if len(args) > 0 && (args[0] == "-table" || args[0] == "--table") { format = "table" args = args[1:] } if len(args) == 0 { fmt.Fprintln(os.Stderr, "Error: SQL query required") os.Exit(1) } query := strings.Join(args, " ") if err := lib.CryptoInit(lib.KeyPathDefault); err != nil { fmt.Fprintf(os.Stderr, "crypto init: %v\n", err) os.Exit(1) } db, err := sql.Open("sqlite3", dbPath) if err != nil { fmt.Fprintf(os.Stderr, "db open: %v\n", err) os.Exit(1) } defer db.Close() rows, err := db.Query(query) if err != nil { fmt.Fprintf(os.Stderr, "query: %v\n", err) os.Exit(1) } defer rows.Close() cols, _ := rows.Columns() colTypes, _ := rows.ColumnTypes() var results []map[string]interface{} for rows.Next() { // Scan as raw interface{} to preserve type info ([]byte for BLOBs, int64 for INTs, string for TEXT) vals := make([]interface{}, len(cols)) ptrs := make([]interface{}, len(cols)) for i := range vals { ptrs[i] = &vals[i] } if err := rows.Scan(ptrs...); err != nil { fmt.Fprintf(os.Stderr, "scan: %v\n", err) os.Exit(1) } row := make(map[string]interface{}) for i, col := range cols { v := vals[i] switch val := v.(type) { case []byte: // Try Unpack (new packed BLOBs) if unpacked := lib.Unpack(val); unpacked != nil { s := string(unpacked) // If it looks like JSON, parse it if strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[") { var parsed interface{} if json.Unmarshal(unpacked, &parsed) == nil { row[col] = parsed continue } } row[col] = s continue } // Try old CryptoDecrypt (legacy base64 strings) s := string(val) decrypted := s for j := 0; j < 10; j++ { next := lib.CryptoDecrypt(decrypted) if next == "" || next == decrypted { break } decrypted = next } if decrypted != s { if strings.HasPrefix(decrypted, "{") || strings.HasPrefix(decrypted, "[") { var parsed interface{} if json.Unmarshal([]byte(decrypted), &parsed) == nil { row[col] = parsed continue } } row[col] = decrypted } else { row[col] = s } case nil: row[col] = nil case string: // Try old CryptoDecrypt for legacy TEXT columns decrypted := val for j := 0; j < 10; j++ { next := lib.CryptoDecrypt(decrypted) if next == "" || next == decrypted { break } decrypted = next } if decrypted != val { if strings.HasPrefix(decrypted, "{") || strings.HasPrefix(decrypted, "[") { var parsed interface{} if json.Unmarshal([]byte(decrypted), &parsed) == nil { row[col] = parsed continue } } row[col] = decrypted } else { row[col] = val } default: row[col] = v } } results = append(results, row) } if err := rows.Err(); err != nil { fmt.Fprintf(os.Stderr, "rows: %v\n", err) os.Exit(1) } _ = colTypes // reserved for future use switch format { case "csv": outputCSV(cols, results) case "table": outputTable(cols, results) default: out, _ := json.MarshalIndent(results, "", " ") fmt.Println(string(out)) } } func formatValue(val interface{}) string { if val == nil { return "" } switch val.(type) { case map[string]interface{}, []interface{}: if b, err := json.Marshal(val); err == nil { return string(b) } } return fmt.Sprintf("%v", val) } func outputCSV(cols []string, results []map[string]interface{}) { w := csv.NewWriter(os.Stdout) defer w.Flush() w.Write(cols) for _, row := range results { record := make([]string, len(cols)) for i, col := range cols { record[i] = formatValue(row[col]) } w.Write(record) } } func outputTable(cols []string, results []map[string]interface{}) { if len(results) == 0 { fmt.Println("(no rows)") return } widths := make([]int, len(cols)) for i, col := range cols { widths[i] = len(col) } for _, row := range results { for i, col := range cols { n := len(formatValue(row[col])) if n > widths[i] { widths[i] = n } } } for i := range widths { if widths[i] > 50 { widths[i] = 50 } } for i, col := range cols { fmt.Printf("%-*s ", widths[i], col) } fmt.Println() for i := range cols { fmt.Printf("%s ", strings.Repeat("─", widths[i])) } fmt.Println() for _, row := range results { for i, col := range cols { val := formatValue(row[col]) if len(val) > widths[i] { val = val[:widths[i]-3] + "..." } fmt.Printf("%-*s ", widths[i], val) } fmt.Println() } }