package sqlite import ( "context" "database/sql" "fmt" "path/filepath" _ "modernc.org/sqlite" // SQLite driver "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" ) // Reader implements the readers.Reader interface for SQLite databases type Reader struct { options *readers.ReaderOptions db *sql.DB ctx context.Context } // NewReader creates a new SQLite reader func NewReader(options *readers.ReaderOptions) *Reader { return &Reader{ options: options, ctx: context.Background(), } } // ReadDatabase reads the entire database schema from SQLite func (r *Reader) ReadDatabase() (*models.Database, error) { // Validate file path or connection string dbPath := r.options.FilePath if dbPath == "" && r.options.ConnectionString != "" { dbPath = r.options.ConnectionString } if dbPath == "" { return nil, fmt.Errorf("file path or connection string is required") } // Connect to the database if err := r.connect(dbPath); err != nil { return nil, fmt.Errorf("failed to connect: %w", err) } defer r.close() // Get database name from file path dbName := filepath.Base(dbPath) if dbName == "" { dbName = "sqlite" } // Initialize database model db := models.InitDatabase(dbName) db.DatabaseType = models.SqlLiteDatabaseType db.SourceFormat = "sqlite" // Get SQLite version var version string err := r.db.QueryRowContext(r.ctx, "SELECT sqlite_version()").Scan(&version) if err == nil { db.DatabaseVersion = version } // SQLite doesn't have schemas, so we create a single "main" schema schema := models.InitSchema("main") schema.RefDatabase = db // Query tables tables, err := r.queryTables() if err != nil { return nil, fmt.Errorf("failed to query tables: %w", err) } schema.Tables = tables // Query views views, err := r.queryViews() if err != nil { return nil, fmt.Errorf("failed to query views: %w", err) } schema.Views = views // Query columns for tables and views for _, table := range schema.Tables { columns, err := r.queryColumns(table.Name) if err != nil { return nil, fmt.Errorf("failed to query columns for table %s: %w", table.Name, err) } table.Columns = columns table.RefSchema = schema // Query primary key pk, err := r.queryPrimaryKey(table.Name) if err != nil { return nil, fmt.Errorf("failed to query primary key for table %s: %w", table.Name, err) } if pk != nil { table.Constraints[pk.Name] = pk // Mark columns as primary key and not null for _, colName := range pk.Columns { if col, exists := table.Columns[colName]; exists { col.IsPrimaryKey = true col.NotNull = true } } } // Query foreign keys foreignKeys, err := r.queryForeignKeys(table.Name) if err != nil { return nil, fmt.Errorf("failed to query foreign keys for table %s: %w", table.Name, err) } for _, fk := range foreignKeys { table.Constraints[fk.Name] = fk // Derive relationship from foreign key r.deriveRelationship(table, fk) } // Query indexes indexes, err := r.queryIndexes(table.Name) if err != nil { return nil, fmt.Errorf("failed to query indexes for table %s: %w", table.Name, err) } for _, idx := range indexes { table.Indexes[idx.Name] = idx } } // Query columns for views for _, view := range schema.Views { columns, err := r.queryColumns(view.Name) if err != nil { return nil, fmt.Errorf("failed to query columns for view %s: %w", view.Name, err) } view.Columns = columns view.RefSchema = schema } // Add schema to database db.Schemas = append(db.Schemas, schema) return db, nil } // ReadSchema reads a single schema (returns the main schema from the database) func (r *Reader) ReadSchema() (*models.Schema, error) { db, err := r.ReadDatabase() if err != nil { return nil, err } if len(db.Schemas) == 0 { return nil, fmt.Errorf("no schemas found in database") } return db.Schemas[0], nil } // ReadTable reads a single table (returns the first table from the schema) func (r *Reader) ReadTable() (*models.Table, error) { schema, err := r.ReadSchema() if err != nil { return nil, err } if len(schema.Tables) == 0 { return nil, fmt.Errorf("no tables found in schema") } return schema.Tables[0], nil } // connect establishes a connection to the SQLite database func (r *Reader) connect(dbPath string) error { db, err := sql.Open("sqlite", dbPath) if err != nil { return err } r.db = db return nil } // close closes the database connection func (r *Reader) close() { if r.db != nil { r.db.Close() } } // mapDataType maps SQLite data types to canonical types func (r *Reader) mapDataType(sqliteType string) string { // SQLite has a flexible type system, but we map common types typeMap := map[string]string{ "INTEGER": "int", "INT": "int", "TINYINT": "int8", "SMALLINT": "int16", "MEDIUMINT": "int", "BIGINT": "int64", "UNSIGNED BIG INT": "uint64", "INT2": "int16", "INT8": "int64", "REAL": "float64", "DOUBLE": "float64", "DOUBLE PRECISION": "float64", "FLOAT": "float32", "NUMERIC": "decimal", "DECIMAL": "decimal", "BOOLEAN": "bool", "BOOL": "bool", "DATE": "date", "DATETIME": "timestamp", "TIMESTAMP": "timestamp", "TEXT": "string", "VARCHAR": "string", "CHAR": "string", "CHARACTER": "string", "VARYING CHARACTER": "string", "NCHAR": "string", "NVARCHAR": "string", "CLOB": "text", "BLOB": "bytea", } // Try exact match first if mapped, exists := typeMap[sqliteType]; exists { return mapped } // Try case-insensitive match for common types sqliteTypeUpper := sqliteType if len(sqliteType) > 0 { // Extract base type (e.g., "VARCHAR(255)" -> "VARCHAR") for baseType := range typeMap { if len(sqliteTypeUpper) >= len(baseType) && sqliteTypeUpper[:len(baseType)] == baseType { return typeMap[baseType] } } } // Default to string for unknown types return "string" } // deriveRelationship creates a relationship from a foreign key constraint func (r *Reader) deriveRelationship(table *models.Table, fk *models.Constraint) { relationshipName := fmt.Sprintf("%s_to_%s", table.Name, fk.ReferencedTable) relationship := models.InitRelationship(relationshipName, models.OneToMany) relationship.FromTable = table.Name relationship.FromSchema = table.Schema relationship.ToTable = fk.ReferencedTable relationship.ToSchema = fk.ReferencedSchema relationship.ForeignKey = fk.Name // Store constraint actions in properties if fk.OnDelete != "" { relationship.Properties["on_delete"] = fk.OnDelete } if fk.OnUpdate != "" { relationship.Properties["on_update"] = fk.OnUpdate } table.Relationships[relationshipName] = relationship }