package database import ( "database/sql" "fmt" "sync" "time" _ "modernc.org/sqlite" "go.uber.org/zap" "uzdb/internal/config" "uzdb/internal/models" ) // SQLiteConnection represents a SQLite connection type SQLiteConnection struct { db *sql.DB filePath string mu sync.RWMutex } // NewSQLiteConnection creates a new SQLite connection func NewSQLiteConnection(conn *models.UserConnection) (*SQLiteConnection, error) { filePath := conn.Database // Ensure file exists for new connections db, err := sql.Open("sqlite", filePath) if err != nil { return nil, fmt.Errorf("failed to open SQLite connection: %w", err) } // Configure connection pool db.SetMaxOpenConns(1) // SQLite only supports one writer db.SetMaxIdleConns(1) db.SetConnMaxLifetime(5 * time.Minute) // Enable WAL mode if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { config.GetLogger().Warn("failed to enable WAL mode", zap.Error(err)) } // Test connection if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("failed to ping SQLite: %w", err) } sqliteConn := &SQLiteConnection{ db: db, filePath: filePath, } config.GetLogger().Info("SQLite connection established", zap.String("path", filePath), ) return sqliteConn, nil } // GetDB returns the underlying sql.DB func (s *SQLiteConnection) GetDB() *sql.DB { s.mu.RLock() defer s.mu.RUnlock() return s.db } // Close closes the SQLite connection func (s *SQLiteConnection) Close() error { s.mu.Lock() defer s.mu.Unlock() if s.db != nil { if err := s.db.Close(); err != nil { return fmt.Errorf("failed to close SQLite connection: %w", err) } s.db = nil config.GetLogger().Info("SQLite connection closed", zap.String("path", s.filePath), ) } return nil } // IsConnected checks if the connection is alive func (s *SQLiteConnection) IsConnected() bool { s.mu.RLock() defer s.mu.RUnlock() if s.db == nil { return false } err := s.db.Ping() return err == nil } // ExecuteQuery executes a SQL query and returns results func (s *SQLiteConnection) ExecuteQuery(query string, args ...interface{}) (*models.QueryResult, error) { startTime := time.Now() db := s.GetDB() if db == nil { return nil, fmt.Errorf("connection is closed") } rows, err := db.Query(query, args...) if err != nil { return nil, fmt.Errorf("query execution failed: %w", err) } defer rows.Close() result := &models.QueryResult{ Success: true, Duration: time.Since(startTime).Milliseconds(), } // Get column names columns, err := rows.Columns() if err != nil { return nil, fmt.Errorf("failed to get columns: %w", err) } result.Columns = columns // Scan rows for rows.Next() { values := make([]interface{}, len(columns)) valuePtrs := make([]interface{}, len(columns)) for i := range values { valuePtrs[i] = &values[i] } if err := rows.Scan(valuePtrs...); err != nil { return nil, fmt.Errorf("failed to scan row: %w", err) } row := make([]interface{}, len(columns)) for i, v := range values { row[i] = convertValue(v) } result.Rows = append(result.Rows, row) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("row iteration error: %w", err) } result.RowCount = int64(len(result.Rows)) return result, nil } // ExecuteStatement executes a SQL statement (INSERT, UPDATE, DELETE, etc.) func (s *SQLiteConnection) ExecuteStatement(stmt string, args ...interface{}) (*models.QueryResult, error) { startTime := time.Now() db := s.GetDB() if db == nil { return nil, fmt.Errorf("connection is closed") } res, err := db.Exec(stmt, args...) if err != nil { return nil, fmt.Errorf("statement execution failed: %w", err) } rowsAffected, _ := res.RowsAffected() lastInsertID, _ := res.LastInsertId() result := &models.QueryResult{ Success: true, AffectedRows: rowsAffected, Duration: time.Since(startTime).Milliseconds(), Rows: [][]interface{}{{lastInsertID}}, Columns: []string{"last_insert_id"}, } return result, nil } // GetTables returns all tables in the database func (s *SQLiteConnection) GetTables(schema string) ([]models.Table, error) { db := s.GetDB() if db == nil { return nil, fmt.Errorf("connection is closed") } query := ` SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name ` rows, err := db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query tables: %w", err) } defer rows.Close() var tables []models.Table for rows.Next() { var t models.Table if err := rows.Scan(&t.Name, &t.Type); err != nil { return nil, fmt.Errorf("failed to scan table: %w", err) } // Get row count for tables if t.Type == "table" { countQuery := fmt.Sprintf("SELECT COUNT(*) FROM \"%s\"", t.Name) var rowCount int64 if err := db.QueryRow(countQuery).Scan(&rowCount); err == nil { t.RowCount = rowCount } } tables = append(tables, t) } return tables, nil } // GetTableStructure returns the structure of a table func (s *SQLiteConnection) GetTableStructure(tableName string) (*models.TableStructure, error) { db := s.GetDB() if db == nil { return nil, fmt.Errorf("connection is closed") } structure := &models.TableStructure{ TableName: tableName, } // Get table info using PRAGMA query := fmt.Sprintf("PRAGMA table_info(\"%s\")", tableName) rows, err := db.Query(query) if err != nil { return nil, fmt.Errorf("failed to query table info: %w", err) } defer rows.Close() for rows.Next() { var col models.TableColumn var pk int if err := rows.Scan(&col.Name, &col.DataType, &col.Default, &col.Nullable, &pk, &col.IsPrimary); err != nil { return nil, fmt.Errorf("failed to scan column: %w", err) } col.IsPrimary = pk > 0 col.Nullable = col.Nullable || !col.IsPrimary structure.Columns = append(structure.Columns, col) } // Get indexes idxQuery := fmt.Sprintf("PRAGMA index_list(\"%s\")", tableName) idxRows, err := db.Query(idxQuery) if err != nil { config.GetLogger().Warn("failed to query indexes", zap.Error(err)) } else { defer idxRows.Close() for idxRows.Next() { var idx models.TableIndex var origin string if err := idxRows.Scan(&idx.Name, &idx.IsUnique, &origin); err != nil { continue } idx.IsPrimary = idx.Name == "sqlite_autoindex_"+tableName+"_1" idx.Type = origin // Get index columns colQuery := fmt.Sprintf("PRAGMA index_info(\"%s\")", idx.Name) colRows, err := db.Query(colQuery) if err == nil { defer colRows.Close() for colRows.Next() { var seqno int var colName string if err := colRows.Scan(&seqno, &colName, &colName); err != nil { continue } idx.Columns = append(idx.Columns, colName) } } structure.Indexes = append(structure.Indexes, idx) } } // Get foreign keys fkQuery := fmt.Sprintf("PRAGMA foreign_key_list(\"%s\")", tableName) fkRows, err := db.Query(fkQuery) if err != nil { config.GetLogger().Warn("failed to query foreign keys", zap.Error(err)) } else { defer fkRows.Close() fkMap := make(map[string]*models.ForeignKey) for fkRows.Next() { var fk models.ForeignKey var id, seq int if err := fkRows.Scan(&id, &seq, &fk.Name, &fk.ReferencedTable, &fk.Columns, &fk.ReferencedColumns, &fk.OnUpdate, &fk.OnDelete, ""); err != nil { continue } // Handle array fields properly fk.Columns = []string{} fk.ReferencedColumns = []string{} var fromCol, toCol string if err := fkRows.Scan(&id, &seq, &fk.Name, &fk.ReferencedTable, &fromCol, &toCol, &fk.OnUpdate, &fk.OnDelete, ""); err != nil { continue } existingFk, exists := fkMap[fk.Name] if !exists { existingFk = &models.ForeignKey{ Name: fk.Name, ReferencedTable: fk.ReferencedTable, OnDelete: fk.OnDelete, OnUpdate: fk.OnUpdate, Columns: []string{}, ReferencedColumns: []string{}, } fkMap[fk.Name] = existingFk } existingFk.Columns = append(existingFk.Columns, fromCol) existingFk.ReferencedColumns = append(existingFk.ReferencedColumns, toCol) } for _, fk := range fkMap { structure.ForeignKeys = append(structure.ForeignKeys, *fk) } } return structure, nil } // GetMetadata returns database metadata func (s *SQLiteConnection) GetMetadata() (*models.DBMetadata, error) { db := s.GetDB() if db == nil { return nil, fmt.Errorf("connection is closed") } metadata := &models.DBMetadata{ Database: s.filePath, Version: "3", // SQLite version 3 } // Get SQLite version var version string err := db.QueryRow("SELECT sqlite_version()").Scan(&version) if err == nil { metadata.Version = version } // Get current time metadata.ServerTime = time.Now().Format(time.RFC3339) return metadata, nil }