feat: ✨ Added Sqlite reader
This commit is contained in:
261
pkg/readers/sqlite/reader.go
Normal file
261
pkg/readers/sqlite/reader.go
Normal file
@@ -0,0 +1,261 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user