diff --git a/cmd/root.go b/cmd/root.go index c8146de..c62943d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,7 +25,7 @@ var rootCmd = &cobra.Command{ Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { dbPath = args[0] - + if _, err := os.Stat(dbPath); os.IsNotExist(err) { return fmt.Errorf("database file '%s' does not exist", dbPath) } @@ -56,4 +56,4 @@ func Execute() error { func init() { rootCmd.Flags().StringVarP(&dbPath, "database", "d", "", "Path to SQLite database file") -} \ No newline at end of file +} diff --git a/internal/app/app.go b/internal/app/app.go index 0034225..4d4d8de 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -603,4 +603,3 @@ func (m *Model) getSharedData() *SharedData { return NewSharedData(m.db) } } - diff --git a/internal/app/edit_cell.go b/internal/app/edit_cell.go index 8fe36a7..e55c970 100644 --- a/internal/app/edit_cell.go +++ b/internal/app/edit_cell.go @@ -82,10 +82,16 @@ func (m *EditCellModel) View() string { } content := TitleStyle.Render(fmt.Sprintf("Edit Cell: %s", columnName)) + "\n\n" - content += fmt.Sprintf("Value: %s\n", m.value) - content += fmt.Sprintf("Cursor: %d\n\n", m.cursor) + + // Display value with visible cursor + displayValue := m.value + if m.cursor <= len(displayValue) { + // Insert cursor character at cursor position + displayValue = displayValue[:m.cursor] + "_" + displayValue[m.cursor:] + } + + content += fmt.Sprintf("Value: %s\n\n", displayValue) content += HelpStyle.Render("enter: save • esc: cancel") return content } - diff --git a/internal/app/query.go b/internal/app/query.go index 61464ee..0d55407 100644 --- a/internal/app/query.go +++ b/internal/app/query.go @@ -105,9 +105,121 @@ func (m *QueryModel) handleResultsNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd return m, nil } +func (m *QueryModel) ensureIDColumns(query string) string { + // Convert to lowercase for easier parsing + lowerQuery := strings.ToLower(strings.TrimSpace(query)) + + // Only modify SELECT statements + if !strings.HasPrefix(lowerQuery, "select") { + return query + } + + // Extract table name from FROM clause + tableName := m.extractTableName(query) + if tableName == "" { + return query // Can't determine table, return original query + } + + // Get primary key columns for this table + primaryKeys := m.getTablePrimaryKeys(tableName) + if len(primaryKeys) == 0 { + return query // No primary keys found + } + + // Check if any primary key columns are already in the query + for _, pk := range primaryKeys { + if strings.Contains(lowerQuery, strings.ToLower(pk)) { + return query // Primary key already included + } + } + + // Check if it's a SELECT * query + if strings.Contains(lowerQuery, "select *") { + return query // SELECT * already includes all columns + } + + // Add primary key columns to the SELECT clause + selectIndex := strings.Index(lowerQuery, "select") + fromIndex := strings.Index(lowerQuery, "from") + + if selectIndex == -1 || fromIndex == -1 || fromIndex <= selectIndex { + return query // Malformed query + } + + // Extract the column list + selectClause := strings.TrimSpace(query[selectIndex+6 : fromIndex]) + + // Add primary keys to the beginning + var pkList []string + for _, pk := range primaryKeys { + pkList = append(pkList, pk) + } + + newSelectClause := strings.Join(pkList, ", ") + ", " + selectClause + + // Reconstruct the query + return "SELECT " + newSelectClause + " " + query[fromIndex:] +} + +func (m *QueryModel) extractTableName(query string) string { + lowerQuery := strings.ToLower(query) + + // Find FROM keyword + fromIndex := strings.Index(lowerQuery, "from") + if fromIndex == -1 { + return "" + } + + // Extract everything after FROM + afterFrom := strings.TrimSpace(query[fromIndex+4:]) + + // Split by whitespace and take the first word (table name) + parts := strings.Fields(afterFrom) + if len(parts) == 0 { + return "" + } + + // Remove any alias or additional clauses + tableName := parts[0] + + // Remove quotes if present + tableName = strings.Trim(tableName, "\"'`") + + return tableName +} + +func (m *QueryModel) getTablePrimaryKeys(tableName string) []string { + rows, err := m.Shared.DB.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName)) + if err != nil { + return nil + } + defer rows.Close() + + var primaryKeys []string + for rows.Next() { + var cid int + var name, dataType string + var notNull, pk int + var defaultValue any + + if err := rows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &pk); err != nil { + continue + } + + if pk == 1 { + primaryKeys = append(primaryKeys, name) + } + } + + return primaryKeys +} + func (m *QueryModel) executeQuery() tea.Cmd { return func() tea.Msg { - rows, err := m.Shared.DB.Query(m.query) + // Modify query to always include ID columns if it's a SELECT statement + modifiedQuery := m.ensureIDColumns(m.query) + + rows, err := m.Shared.DB.Query(modifiedQuery) if err != nil { m.err = err return nil @@ -194,11 +306,21 @@ func (m *QueryModel) View() string { content.WriteString(TitleStyle.Render(headerRow)) content.WriteString("\n") - // Data rows + // Data rows with scrolling visibleCount := Max(1, m.Shared.Height-10) - endIdx := Min(len(m.results), visibleCount) + startIdx := 0 - for i := 0; i < endIdx; i++ { + // Adjust start index if selected row is out of view + if m.selectedRow >= visibleCount { + startIdx = m.selectedRow - visibleCount + 1 + } + + endIdx := Min(len(m.results), startIdx+visibleCount) + + for i := range endIdx { + if i < startIdx { + continue + } row := m.results[i] rowStr := "" for j, cell := range row { @@ -228,4 +350,3 @@ func (m *QueryModel) View() string { return content.String() } - diff --git a/internal/app/row_detail.go b/internal/app/row_detail.go index ce738a3..d625387 100644 --- a/internal/app/row_detail.go +++ b/internal/app/row_detail.go @@ -8,15 +8,17 @@ import ( ) type RowDetailModel struct { - Shared *SharedData - rowIndex int - FromQuery bool + Shared *SharedData + rowIndex int + selectedCol int + FromQuery bool } func NewRowDetailModel(shared *SharedData, rowIndex int) *RowDetailModel { return &RowDetailModel{ - Shared: shared, - rowIndex: rowIndex, + Shared: shared, + rowIndex: rowIndex, + selectedCol: 0, } } @@ -35,11 +37,21 @@ func (m *RowDetailModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, func() tea.Msg { return SwitchToTableDataMsg{TableIndex: m.Shared.SelectedTable} } case "e": - if len(m.Shared.FilteredData) > m.rowIndex && len(m.Shared.Columns) > 0 { + if len(m.Shared.FilteredData) > m.rowIndex && len(m.Shared.Columns) > m.selectedCol { return m, func() tea.Msg { - return SwitchToEditCellMsg{RowIndex: m.rowIndex, ColIndex: 0} + return SwitchToEditCellMsg{RowIndex: m.rowIndex, ColIndex: m.selectedCol} } } + + case "up", "k": + if m.selectedCol > 0 { + m.selectedCol-- + } + + case "down", "j": + if m.selectedCol < len(m.Shared.Columns)-1 { + m.selectedCol++ + } } } return m, nil @@ -56,16 +68,23 @@ func (m *RowDetailModel) View() string { return content.String() } + // Show current row position + content.WriteString(fmt.Sprintf("Row %d of %d\n\n", m.rowIndex+1, len(m.Shared.FilteredData))) + row := m.Shared.FilteredData[m.rowIndex] for i, col := range m.Shared.Columns { if i < len(row) { - content.WriteString(fmt.Sprintf("%s: %s\n", col, row[i])) + if i == m.selectedCol { + content.WriteString(SelectedStyle.Render(fmt.Sprintf("> %s: %s", col, row[i]))) + } else { + content.WriteString(NormalStyle.Render(fmt.Sprintf(" %s: %s", col, row[i]))) + } + content.WriteString("\n") } } content.WriteString("\n") - content.WriteString(HelpStyle.Render("e: edit • q: back")) + content.WriteString(HelpStyle.Render("↑/↓: navigate columns • e: edit • q: back")) return content.String() } - diff --git a/internal/app/table_data.go b/internal/app/table_data.go index ab17a44..163a630 100644 --- a/internal/app/table_data.go +++ b/internal/app/table_data.go @@ -202,4 +202,3 @@ func (m *TableDataModel) View() string { return content.String() } - diff --git a/internal/app/table_list.go b/internal/app/table_list.go index 962f2ab..103d43e 100644 --- a/internal/app/table_list.go +++ b/internal/app/table_list.go @@ -195,4 +195,3 @@ func (m *TableListModel) View() string { return content.String() } - diff --git a/main.go b/main.go index 7e0d9da..469acd1 100644 --- a/main.go +++ b/main.go @@ -10,4 +10,4 @@ func main() { if err := cmd.Execute(); err != nil { log.Fatal(err) } -} \ No newline at end of file +}