Move project files from uzdb/ subdirectory to root directory for cleaner project structure.
Changes:
- Move frontend/ to root
- Move internal/ to root
- Move build/ to root
- Move all config files (go.mod, wails.json, etc.) to root
- Remove redundant uzdb/ subdirectory nesting
Project structure is now:
├── frontend/ # React application
├── internal/ # Go backend
├── build/ # Wails build assets
├── doc/ # Design documentation
├── main.go # Entry point
└── ...
🤖 Generated with Qoder
370 lines
8.7 KiB
Go
370 lines
8.7 KiB
Go
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
|
|
}
|