diff --git a/pkg/mariadb/datatypes.go b/pkg/mariadb/datatypes.go new file mode 100644 index 0000000..aa5dbc9 --- /dev/null +++ b/pkg/mariadb/datatypes.go @@ -0,0 +1,156 @@ +package mariadb + +import "strings" + +// MariaDBToCanonicalTypes maps MariaDB/MySQL type names to canonical types. +var MariaDBToCanonicalTypes = map[string]string{ + // Integer types + "tinyint": "int8", + "smallint": "int16", + "mediumint": "int", + "int": "int", + "integer": "int", + "int2": "int16", + "int4": "int", + "int8": "int64", + "bigint": "int64", + // Boolean (TINYINT(1) alias) + "boolean": "bool", + "bool": "bool", + "bit": "bool", + // Float types + "float": "float32", + "double": "float64", + "real": "float64", + "double precision": "float64", + // Decimal types + "decimal": "decimal", + "numeric": "decimal", + "dec": "decimal", + "fixed": "decimal", + // String types + "char": "string", + "character": "string", + "varchar": "string", + "nchar": "string", + "nvarchar": "string", + "tinytext": "text", + "text": "text", + "mediumtext": "text", + "longtext": "text", + // Binary/blob types + "binary": "bytea", + "varbinary": "bytea", + "tinyblob": "bytea", + "blob": "bytea", + "mediumblob": "bytea", + "longblob": "bytea", + // Date/time types + "date": "date", + "time": "time", + "datetime": "timestamp", + "timestamp": "timestamp", + "year": "int", + // Other types + "json": "json", + "enum": "string", + "set": "string", + "uuid": "uuid", +} + +// CanonicalToMariaDBTypes maps canonical types to MariaDB/MySQL types. +var CanonicalToMariaDBTypes = map[string]string{ + "bool": "TINYINT(1)", + "int8": "TINYINT", + "int16": "SMALLINT", + "int": "INT", + "int32": "INT", + "int64": "BIGINT", + "uint": "INT UNSIGNED", + "uint8": "TINYINT UNSIGNED", + "uint16": "SMALLINT UNSIGNED", + "uint32": "INT UNSIGNED", + "uint64": "BIGINT UNSIGNED", + "float32": "FLOAT", + "float64": "DOUBLE", + "decimal": "DECIMAL", + "string": "VARCHAR(255)", + "text": "TEXT", + "date": "DATE", + "time": "TIME", + "timestamp": "DATETIME", + "timestamptz": "DATETIME", + "uuid": "CHAR(36)", + "json": "JSON", + "jsonb": "JSON", + "bytea": "BLOB", +} + +// MariaDBTypeSynonyms maps MariaDB/MySQL type aliases to their canonical MariaDB name. +var MariaDBTypeSynonyms = map[string]string{ + "integer": "int", + "int2": "smallint", + "int4": "int", + "int8": "bigint", + "double precision": "double", + "character": "char", + "dec": "decimal", + "fixed": "decimal", + "numeric": "decimal", + "boolean": "tinyint", + "bool": "tinyint", +} + +// NormalizeMariaDBType maps a MariaDB/MySQL base type (no dimension parameters) +// to its canonical MariaDB form. Unknown types are returned as-is (lowercased). +func NormalizeMariaDBType(baseType string) string { + lower := strings.ToLower(strings.TrimSpace(baseType)) + if canonical, ok := MariaDBTypeSynonyms[lower]; ok { + return canonical + } + return lower +} + +// ConvertMariaDBToCanonical converts a MariaDB/MySQL type name to the canonical type. +// Strips dimension parameters and normalizes aliases. Defaults to "string". +func ConvertMariaDBToCanonical(mariadbType string) string { + base := strings.ToLower(strings.TrimSpace(mariadbType)) + if idx := strings.Index(base, "("); idx >= 0 { + base = strings.TrimSpace(base[:idx]) + } + + if canonical, ok := MariaDBToCanonicalTypes[base]; ok { + return canonical + } + + // Prefix match for composite types (e.g., "unsigned bigint") + for key, canonical := range MariaDBToCanonicalTypes { + if strings.HasPrefix(base, key) { + return canonical + } + } + + return "string" +} + +// ConvertCanonicalToMariaDB converts a canonical type to a MariaDB/MySQL type. +// Defaults to VARCHAR(255) for unrecognised types. +func ConvertCanonicalToMariaDB(canonicalType string) string { + lower := strings.ToLower(strings.TrimSpace(canonicalType)) + if idx := strings.Index(lower, "("); idx >= 0 { + lower = strings.TrimSpace(lower[:idx]) + } + + if mariadbType, ok := CanonicalToMariaDBTypes[lower]; ok { + return mariadbType + } + + // Prefix fallback + for canonical, mariadb := range CanonicalToMariaDBTypes { + if strings.HasPrefix(lower, canonical) { + return mariadb + } + } + + return "VARCHAR(255)" +} diff --git a/pkg/mssql/datatypes.go b/pkg/mssql/datatypes.go index b9b9085..e8ad3e9 100644 --- a/pkg/mssql/datatypes.go +++ b/pkg/mssql/datatypes.go @@ -2,32 +2,73 @@ package mssql import "strings" -// CanonicalToMSSQLTypes maps canonical types to MSSQL types +// CanonicalToMSSQLTypes maps canonical types to MSSQL types. +// Accepts both Go canonical names ("int", "string") and SQL canonical names +// ("integer", "varchar") so the writer handles input from any reader. var CanonicalToMSSQLTypes = map[string]string{ - "bool": "BIT", - "int8": "TINYINT", - "int16": "SMALLINT", - "int": "INT", - "int32": "INT", - "int64": "BIGINT", - "uint": "BIGINT", - "uint8": "SMALLINT", - "uint16": "INT", - "uint32": "BIGINT", - "uint64": "BIGINT", - "float32": "REAL", - "float64": "FLOAT", - "decimal": "NUMERIC", - "string": "NVARCHAR(255)", - "text": "NVARCHAR(MAX)", + // Boolean — Go and SQL canonical + "bool": "BIT", + "boolean": "BIT", + // Integer — Go canonical + "int8": "TINYINT", + "int16": "SMALLINT", + "int": "INT", + "int32": "INT", + "int64": "BIGINT", + "uint": "BIGINT", + "uint8": "TINYINT", + "uint16": "SMALLINT", + "uint32": "BIGINT", + "uint64": "BIGINT", + // Integer — SQL canonical (serial types map to base integer; IDENTITY is set via AutoIncrement) + "integer": "INT", + "smallint": "SMALLINT", + "bigint": "BIGINT", + "tinyint": "TINYINT", + "serial": "INT", + "smallserial": "SMALLINT", + "bigserial": "BIGINT", + // Float — Go canonical + "float32": "REAL", + "float64": "FLOAT", + // Float — SQL canonical + "real": "REAL", + "double precision": "FLOAT", + "double": "FLOAT", + // Decimal/numeric + "decimal": "NUMERIC", + "numeric": "NUMERIC", + "money": "MONEY", + // String — Go canonical + "string": "NVARCHAR(255)", + "text": "NVARCHAR(MAX)", + // String — SQL canonical + "varchar": "NVARCHAR(255)", + "char": "NCHAR", + "nvarchar": "NVARCHAR(255)", + "nchar": "NCHAR", + "citext": "NVARCHAR(MAX)", + // Date/time "date": "DATE", "time": "TIME", + "timetz": "DATETIMEOFFSET", "timestamp": "DATETIME2", "timestamptz": "DATETIMEOFFSET", - "uuid": "UNIQUEIDENTIFIER", - "json": "NVARCHAR(MAX)", - "jsonb": "NVARCHAR(MAX)", - "bytea": "VARBINARY(MAX)", + "datetime": "DATETIME2", + "interval": "NVARCHAR(50)", + // UUID + "uuid": "UNIQUEIDENTIFIER", + // JSON — MSSQL has no native JSON type; stored as NVARCHAR(MAX) + "json": "NVARCHAR(MAX)", + "jsonb": "NVARCHAR(MAX)", + // Binary + "bytea": "VARBINARY(MAX)", + "blob": "VARBINARY(MAX)", + // Network/geo types — no MSSQL native equivalent + "xml": "XML", + "inet": "NVARCHAR(45)", + "cidr": "NVARCHAR(43)", + "macaddr": "NVARCHAR(17)", } // MSSQLToCanonicalTypes maps MSSQL types to canonical types @@ -68,47 +109,50 @@ var MSSQLToCanonicalTypes = map[string]string{ "geometry": "string", } -// ConvertCanonicalToMSSQL converts a canonical type to MSSQL type +// MSSQLTypeSynonyms maps MSSQL type aliases to their canonical MSSQL name. +var MSSQLTypeSynonyms = map[string]string{ + "integer": "int", + "dec": "decimal", + "float(n)": "float", +} + +// NormalizeMSSQLType maps an MSSQL base type (no dimension parameters) to its +// canonical MSSQL form. Unknown types are returned as-is (lowercased). +func NormalizeMSSQLType(baseType string) string { + lower := strings.ToLower(strings.TrimSpace(baseType)) + if canonical, ok := MSSQLTypeSynonyms[lower]; ok { + return canonical + } + return lower +} + +// ConvertCanonicalToMSSQL converts a canonical type (Go or SQL) to an MSSQL type. +// Strips dimension parameters before lookup. Defaults to NVARCHAR(255) for unknown types. func ConvertCanonicalToMSSQL(canonicalType string) string { - // Check direct mapping - if mssqlType, exists := CanonicalToMSSQLTypes[strings.ToLower(canonicalType)]; exists { + base := strings.ToLower(strings.TrimSpace(canonicalType)) + if idx := strings.Index(base, "("); idx >= 0 { + base = strings.TrimSpace(base[:idx]) + } + base = strings.TrimSuffix(base, "[]") + + if mssqlType, exists := CanonicalToMSSQLTypes[base]; exists { return mssqlType } - // Try to find by prefix - lowerType := strings.ToLower(canonicalType) - for canonical, mssql := range CanonicalToMSSQLTypes { - if strings.HasPrefix(lowerType, canonical) { - return mssql - } - } - - // Default to NVARCHAR return "NVARCHAR(255)" } -// ConvertMSSQLToCanonical converts an MSSQL type to canonical type +// ConvertMSSQLToCanonical converts an MSSQL type to the canonical type. +// Strips dimension parameters before lookup. Defaults to "string" for unknown types. func ConvertMSSQLToCanonical(mssqlType string) string { - // Extract base type (remove parentheses and parameters) - baseType := mssqlType - if idx := strings.Index(baseType, "("); idx != -1 { - baseType = baseType[:idx] + base := strings.ToLower(strings.TrimSpace(mssqlType)) + if idx := strings.Index(base, "("); idx >= 0 { + base = strings.TrimSpace(base[:idx]) } - baseType = strings.TrimSpace(baseType) - // Check direct mapping - if canonicalType, exists := MSSQLToCanonicalTypes[strings.ToLower(baseType)]; exists { + if canonicalType, exists := MSSQLToCanonicalTypes[base]; exists { return canonicalType } - // Try to find by prefix - lowerType := strings.ToLower(baseType) - for mssql, canonical := range MSSQLToCanonicalTypes { - if strings.HasPrefix(lowerType, mssql) { - return canonical - } - } - - // Default to string return "string" } diff --git a/pkg/readers/pgsql/queries.go b/pkg/readers/pgsql/queries.go index 5087e7a..e2a54d2 100644 --- a/pkg/readers/pgsql/queries.go +++ b/pkg/readers/pgsql/queries.go @@ -270,7 +270,15 @@ func (r *Reader) queryColumns(schemaName string) (map[string]map[string]*models. } if numPrecision != nil { - column.Precision = *numPrecision + // For integer and serial types, numeric_precision is a bit-width (32, 64, 16) + // not a user-visible column parameter. Only store precision for types where + // it represents actual decimal/scale precision (numeric, decimal, float). + switch column.Type { + case "integer", "bigint", "smallint", "serial", "bigserial", "smallserial": + // skip — bit-width, not a column parameter + default: + column.Precision = *numPrecision + } } if numScale != nil { diff --git a/pkg/readers/sqlite/reader.go b/pkg/readers/sqlite/reader.go index 5228185..beb19f4 100644 --- a/pkg/readers/sqlite/reader.go +++ b/pkg/readers/sqlite/reader.go @@ -10,6 +10,7 @@ import ( "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" + sqlitepkg "git.warky.dev/wdevs/relspecgo/pkg/sqlite" ) // Reader implements the readers.Reader interface for SQLite databases @@ -183,59 +184,9 @@ func (r *Reader) close() { } } -// mapDataType maps SQLite data types to canonical types +// 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" + return sqlitepkg.ConvertSQLiteToCanonical(sqliteType) } // deriveRelationship creates a relationship from a foreign key constraint diff --git a/pkg/sqlite/datatypes.go b/pkg/sqlite/datatypes.go new file mode 100644 index 0000000..dd3b861 --- /dev/null +++ b/pkg/sqlite/datatypes.go @@ -0,0 +1,152 @@ +package sqlite + +import "strings" + +// SQLiteToCanonicalTypes maps SQLite type names to canonical types. +// SQLite has type affinity rules; this maps common type names including +// MySQL/PostgreSQL types that users write in SQLite schemas. +var SQLiteToCanonicalTypes = map[string]string{ + // Integer affinity + "integer": "int", + "int": "int", + "tinyint": "int8", + "smallint": "int16", + "mediumint": "int", + "bigint": "int64", + "unsigned big int": "uint64", + "int2": "int16", + "int8": "int64", + // Real affinity + "real": "float64", + "double": "float64", + "double precision": "float64", + "float": "float32", + // Numeric affinity + "numeric": "decimal", + "decimal": "decimal", + // Boolean (stored as integer in SQLite) + "boolean": "bool", + "bool": "bool", + // Date/time (stored as text in SQLite) + "date": "date", + "datetime": "timestamp", + "timestamp": "timestamp", + // Text affinity + "text": "string", + "varchar": "string", + "char": "string", + "character": "string", + "varying character": "string", + "nchar": "string", + "nvarchar": "string", + "clob": "text", + // Blob affinity + "blob": "bytea", +} + +// CanonicalToSQLiteAffinity maps type names to SQLite type affinity names. +// Accepts both Go canonical names ("int", "string") and SQL canonical names +// ("integer", "varchar") so the writer handles input from any reader. +// The five SQLite type affinities are TEXT, INTEGER, REAL, NUMERIC, BLOB. +var CanonicalToSQLiteAffinity = map[string]string{ + // INTEGER affinity — Go canonical + "int": "INTEGER", + "int8": "INTEGER", + "int16": "INTEGER", + "int32": "INTEGER", + "int64": "INTEGER", + "uint": "INTEGER", + "uint8": "INTEGER", + "uint16": "INTEGER", + "uint32": "INTEGER", + "uint64": "INTEGER", + "bool": "INTEGER", + // INTEGER affinity — SQL canonical + "integer": "INTEGER", + "smallint": "INTEGER", + "bigint": "INTEGER", + "serial": "INTEGER", + "smallserial": "INTEGER", + "bigserial": "INTEGER", + "boolean": "INTEGER", + "tinyint": "INTEGER", + "mediumint": "INTEGER", + // REAL affinity — Go canonical + "float32": "REAL", + "float64": "REAL", + // REAL affinity — SQL canonical + "real": "REAL", + "float": "REAL", + "double": "REAL", + "double precision": "REAL", + // NUMERIC affinity + "decimal": "NUMERIC", + "numeric": "NUMERIC", + "money": "NUMERIC", + "smallmoney": "NUMERIC", + // BLOB affinity + "bytea": "BLOB", + "blob": "BLOB", + // TEXT affinity — Go canonical + "string": "TEXT", + "text": "TEXT", + // TEXT affinity — SQL canonical + "varchar": "TEXT", + "char": "TEXT", + "nvarchar": "TEXT", + "nchar": "TEXT", + "citext": "TEXT", + "date": "TEXT", + "time": "TEXT", + "timetz": "TEXT", + "timestamp": "TEXT", + "timestamptz": "TEXT", + "datetime": "TEXT", + "uuid": "TEXT", + "json": "TEXT", + "jsonb": "TEXT", + "xml": "TEXT", + "inet": "TEXT", + "cidr": "TEXT", + "macaddr": "TEXT", +} + +// ConvertSQLiteToCanonical converts a SQLite type name to the canonical type. +// Strips dimension parameters (e.g. VARCHAR(255) → string) and handles +// SQLite's flexible affinity rules. Defaults to "string" for unknown types. +func ConvertSQLiteToCanonical(sqliteType string) string { + base := strings.ToUpper(strings.TrimSpace(sqliteType)) + if idx := strings.Index(base, "("); idx >= 0 { + base = strings.TrimSpace(base[:idx]) + } + lower := strings.ToLower(base) + + if canonical, ok := SQLiteToCanonicalTypes[lower]; ok { + return canonical + } + + // Prefix match for types like "VARYING CHARACTER(255)" + for key, canonical := range SQLiteToCanonicalTypes { + if strings.HasPrefix(lower, key) { + return canonical + } + } + + return "string" +} + +// ConvertCanonicalToSQLite converts a canonical type (or any SQL type) to its +// SQLite type affinity. Defaults to TEXT for unrecognised types. +func ConvertCanonicalToSQLite(canonicalType string) string { + normalized := strings.ToLower(strings.TrimSpace(canonicalType)) + if idx := strings.Index(normalized, "("); idx >= 0 { + normalized = strings.TrimSpace(normalized[:idx]) + } + normalized = strings.TrimSuffix(normalized, "[]") + + if affinity, ok := CanonicalToSQLiteAffinity[normalized]; ok { + return affinity + } + + return "TEXT" +} diff --git a/pkg/writers/sqlite/datatypes.go b/pkg/writers/sqlite/datatypes.go index bed217d..64027ec 100644 --- a/pkg/writers/sqlite/datatypes.go +++ b/pkg/writers/sqlite/datatypes.go @@ -3,10 +3,10 @@ package sqlite import ( "strings" - "git.warky.dev/wdevs/relspecgo/pkg/pgsql" + sqlitepkg "git.warky.dev/wdevs/relspecgo/pkg/sqlite" ) -// SQLite type affinities +// SQLite type affinity constants const ( TypeText = "TEXT" TypeInteger = "INTEGER" @@ -15,55 +15,27 @@ const ( TypeBlob = "BLOB" ) -// MapPostgreSQLType maps PostgreSQL data types to SQLite type affinities +// MapTypeToSQLite maps any SQL or Go canonical type to a SQLite type affinity. +// Handles input from any reader (PostgreSQL, MSSQL, SQLite, Go canonical). +func MapTypeToSQLite(colType string) string { + return sqlitepkg.ConvertCanonicalToSQLite(colType) +} + +// MapPostgreSQLType is an alias for MapTypeToSQLite kept for compatibility. +// +// Deprecated: use MapTypeToSQLite. func MapPostgreSQLType(pgType string) string { - normalized := strings.ToLower(strings.TrimSpace(pgType)) - normalized = strings.TrimSuffix(normalized, "[]") - if idx := strings.Index(normalized, "("); idx != -1 { - normalized = normalized[:idx] - } - // Resolve synonyms to canonical form before mapping - normalized = pgsql.NormalizePGType(normalized) - - switch normalized { - case "varchar", "char", "text", "citext", "uuid", - "timestamp", "timestamptz", "date", "time", "timetz", - "json", "jsonb", "xml", "inet", "cidr", "macaddr": - return TypeText - case "integer", "smallint", "bigint", - "serial", "smallserial", "bigserial", "boolean": - return TypeInteger - case "real", "float", "double precision": - return TypeReal - case "numeric", "money": - return TypeNumeric - case "bytea", "blob": - return TypeBlob - default: - return TypeText - } + return MapTypeToSQLite(pgType) } -// IsIntegerType checks if a column type should be treated as integer +// IsIntegerType reports whether a column type maps to SQLite INTEGER affinity. func IsIntegerType(colType string) bool { - normalized := strings.ToLower(strings.TrimSpace(colType)) - normalized = strings.TrimSuffix(normalized, "[]") - if idx := strings.Index(normalized, "("); idx != -1 { - normalized = normalized[:idx] - } - normalized = pgsql.NormalizePGType(normalized) - switch normalized { - case "integer", "smallint", "bigint", "serial", "smallserial", "bigserial": - return true - default: - return false - } + return MapTypeToSQLite(colType) == TypeInteger } -// MapBooleanValue converts PostgreSQL boolean literals to SQLite (0/1) +// MapBooleanValue converts common boolean literals to SQLite integers (1/0). func MapBooleanValue(value string) string { - normalized := strings.ToLower(strings.TrimSpace(value)) - switch normalized { + switch strings.ToLower(strings.TrimSpace(value)) { case "true", "t", "yes", "y", "1": return "1" case "false", "f", "no", "n", "0":