Fixed bug/gorm indexes
Some checks are pending
CI / Build (push) Waiting to run
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run

This commit is contained in:
2025-12-18 19:15:22 +02:00
parent b7950057eb
commit d93a4b6f08
4 changed files with 622 additions and 142 deletions

View File

@@ -187,11 +187,43 @@ func (r *Reader) parseFile(filename string) ([]*models.Table, error) {
table.Schema = schemaName
}
// Update columns
// Update columns and indexes
for _, col := range table.Columns {
col.Table = tableName
col.Schema = table.Schema
}
for _, idx := range table.Indexes {
idx.Table = tableName
idx.Schema = table.Schema
}
}
}
// Third pass: parse relationship fields for constraints
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
table, ok := structMap[typeSpec.Name.Name]
if !ok {
continue
}
// Parse relationship fields
r.parseRelationshipConstraints(table, structType, structMap)
}
}
@@ -314,6 +346,10 @@ func (r *Reader) parseStruct(structName string, structType *ast.StructType) *mod
column.Table = tableName
column.Schema = schemaName
table.Columns[column.Name] = column
// Parse indexes from bun tags
r.parseIndexesFromTag(table, column, tag)
sequence++
}
}
@@ -346,6 +382,159 @@ func (r *Reader) isRelationship(tag string) bool {
return strings.Contains(tag, "bun:\"rel:") || strings.Contains(tag, ",rel:")
}
// parseRelationshipConstraints parses relationship fields to extract foreign key constraints
func (r *Reader) parseRelationshipConstraints(table *models.Table, structType *ast.StructType, structMap map[string]*models.Table) {
for _, field := range structType.Fields.List {
if field.Tag == nil {
continue
}
tag := field.Tag.Value
if !r.isRelationship(tag) {
continue
}
bunTag := r.extractBunTag(tag)
// Get the referenced type name from the field type
referencedType := r.getRelationshipType(field.Type)
if referencedType == "" {
continue
}
// Find the referenced table
referencedTable, ok := structMap[referencedType]
if !ok {
continue
}
// Parse the join information: join:user_id=id
// This means: referencedTable.user_id = thisTable.id
joinInfo := r.parseJoinInfo(bunTag)
if joinInfo == nil {
continue
}
// The FK is on the referenced table
constraint := &models.Constraint{
Name: fmt.Sprintf("fk_%s_%s", referencedTable.Name, table.Name),
Type: models.ForeignKeyConstraint,
Table: referencedTable.Name,
Schema: referencedTable.Schema,
Columns: []string{joinInfo.ForeignKey},
ReferencedTable: table.Name,
ReferencedSchema: table.Schema,
ReferencedColumns: []string{joinInfo.ReferencedKey},
OnDelete: "NO ACTION", // Bun doesn't specify this in tags
OnUpdate: "NO ACTION",
}
referencedTable.Constraints[constraint.Name] = constraint
}
}
// JoinInfo holds parsed join information
type JoinInfo struct {
ForeignKey string // Column in the related table
ReferencedKey string // Column in the current table
}
// parseJoinInfo parses join information from bun tag
// Example: join:user_id=id means foreign_key=referenced_key
func (r *Reader) parseJoinInfo(bunTag string) *JoinInfo {
// Look for join: in the tag
if !strings.Contains(bunTag, "join:") {
return nil
}
// Extract join clause
parts := strings.Split(bunTag, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "join:") {
joinStr := strings.TrimPrefix(part, "join:")
// Parse user_id=id
joinParts := strings.SplitN(joinStr, "=", 2)
if len(joinParts) == 2 {
return &JoinInfo{
ForeignKey: joinParts[0],
ReferencedKey: joinParts[1],
}
}
}
}
return nil
}
// getRelationshipType extracts the type name from a relationship field
func (r *Reader) getRelationshipType(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.ArrayType:
// []*ModelPost -> ModelPost
if starExpr, ok := t.Elt.(*ast.StarExpr); ok {
if ident, ok := starExpr.X.(*ast.Ident); ok {
return ident.Name
}
}
case *ast.StarExpr:
// *ModelPost -> ModelPost
if ident, ok := t.X.(*ast.Ident); ok {
return ident.Name
}
}
return ""
}
// parseIndexesFromTag extracts index definitions from Bun tags
func (r *Reader) parseIndexesFromTag(table *models.Table, column *models.Column, tag string) {
bunTag := r.extractBunTag(tag)
// Parse tag into parts
parts := strings.Split(bunTag, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
// Check for unique index
if part == "unique" {
// Auto-generate index name: idx_tablename_columnname
indexName := fmt.Sprintf("idx_%s_%s", table.Name, column.Name)
if _, exists := table.Indexes[indexName]; !exists {
index := &models.Index{
Name: indexName,
Table: table.Name,
Schema: table.Schema,
Columns: []string{column.Name},
Unique: true,
Type: "btree",
}
table.Indexes[indexName] = index
}
} else if strings.HasPrefix(part, "unique:") {
// Named unique index: unique:idx_name
indexName := strings.TrimPrefix(part, "unique:")
// Check if index already exists (for composite indexes)
if idx, exists := table.Indexes[indexName]; exists {
// Add this column to the existing index
idx.Columns = append(idx.Columns, column.Name)
} else {
// Create new index
index := &models.Index{
Name: indexName,
Table: table.Name,
Schema: table.Schema,
Columns: []string{column.Name},
Unique: true,
Type: "btree",
}
table.Indexes[indexName] = index
}
}
}
}
// extractTableNameFromTag extracts table and schema from bun tag
func (r *Reader) extractTableNameFromTag(tag string) (tableName string, schemaName string) {
// Extract bun tag value
@@ -422,6 +611,7 @@ func (r *Reader) parseColumn(fieldName string, fieldType ast.Expr, tag string, s
case "autoincrement":
column.AutoIncrement = true
case "default":
// Default value from Bun tag (e.g., default:gen_random_uuid())
column.Default = value
}
}
@@ -431,16 +621,24 @@ func (r *Reader) parseColumn(fieldName string, fieldType ast.Expr, tag string, s
column.Type = r.goTypeToSQL(fieldType)
}
// Determine if nullable based on Go type
if r.isNullableType(fieldType) {
// Determine if nullable based on Go type and bun tags
// In Bun:
// - nullzero tag means the field is nullable (can be NULL in DB)
// - absence of nullzero means the field is NOT NULL
// - primitive types (int64, bool, string) are NOT NULL by default
if strings.Contains(bunTag, "nullzero") {
column.NotNull = false
} else if !column.IsPrimaryKey && column.Type != "" {
// If it's not a nullable type and not a primary key, check the tag
if !strings.Contains(bunTag, "notnull") {
// If notnull is not explicitly set, it might still be nullable
// This is a heuristic - we default to nullable unless specified
return column
}
} else if r.isNullableGoType(fieldType) {
// SqlString, SqlInt, etc. without nullzero tag means NOT NULL
column.NotNull = true
} else {
// Primitive types are NOT NULL by default
column.NotNull = true
}
// Primary keys are always NOT NULL
if column.IsPrimaryKey {
column.NotNull = true
}
return column
@@ -529,11 +727,12 @@ func (r *Reader) sqlTypeToSQL(typeName string) string {
}
}
// isNullableType checks if a Go type represents a nullable field
func (r *Reader) isNullableType(expr ast.Expr) bool {
// isNullableGoType checks if a Go type represents a nullable field type
// (this is for types that CAN be nullable, not whether they ARE nullable)
func (r *Reader) isNullableGoType(expr ast.Expr) bool {
switch t := expr.(type) {
case *ast.StarExpr:
// Pointer type is nullable
// Pointer type can be nullable
return true
case *ast.SelectorExpr:
// Check for sql_types nullable types

View File

@@ -187,11 +187,44 @@ func (r *Reader) parseFile(filename string) ([]*models.Table, error) {
table.Schema = schemaName
}
// Update columns
// Update columns and indexes
for _, col := range table.Columns {
col.Table = tableName
col.Schema = table.Schema
}
for _, idx := range table.Indexes {
idx.Table = tableName
idx.Schema = table.Schema
}
}
}
// Third pass: parse relationship fields for constraints
// Re-parse the file to get relationship information
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
table, ok := structMap[typeSpec.Name.Name]
if !ok {
continue
}
// Parse relationship fields
r.parseRelationshipConstraints(table, structType, structMap)
}
}
@@ -280,8 +313,14 @@ func (r *Reader) parseStruct(structName string, structType *ast.StructType) *mod
continue
}
// Skip embedded GORM model and relationship fields
if r.isGORMModel(field) || r.isRelationship(tag) {
// Skip embedded GORM model
if r.isGORMModel(field) {
continue
}
// Parse relationship fields for foreign key constraints
if r.isRelationship(tag) {
// We'll parse constraints in a second pass after we know all table names
continue
}
@@ -310,6 +349,10 @@ func (r *Reader) parseStruct(structName string, structType *ast.StructType) *mod
table.Name = tableName
table.Schema = schemaName
table.Columns[column.Name] = column
// Parse indexes from GORM tags
r.parseIndexesFromTag(table, column, tag)
sequence++
}
}
@@ -345,6 +388,169 @@ func (r *Reader) isRelationship(tag string) bool {
strings.Contains(gormTag, "many2many:")
}
// parseRelationshipConstraints parses relationship fields to extract foreign key constraints
func (r *Reader) parseRelationshipConstraints(table *models.Table, structType *ast.StructType, structMap map[string]*models.Table) {
for _, field := range structType.Fields.List {
if field.Tag == nil {
continue
}
tag := field.Tag.Value
if !r.isRelationship(tag) {
continue
}
gormTag := r.extractGormTag(tag)
parts := r.parseGormTag(gormTag)
// Get the referenced type name from the field type
referencedType := r.getRelationshipType(field.Type)
if referencedType == "" {
continue
}
// Find the referenced table
referencedTable, ok := structMap[referencedType]
if !ok {
continue
}
// Extract foreign key information
foreignKey, hasForeignKey := parts["foreignKey"]
if !hasForeignKey {
continue
}
// Convert field name to column name
fkColumn := r.fieldNameToColumnName(foreignKey)
// Determine constraint behavior
onDelete := "NO ACTION"
onUpdate := "NO ACTION"
if constraintStr, hasConstraint := parts["constraint"]; hasConstraint {
// Parse constraint:OnDelete:CASCADE,OnUpdate:CASCADE
if strings.Contains(constraintStr, "OnDelete:CASCADE") {
onDelete = "CASCADE"
} else if strings.Contains(constraintStr, "OnDelete:SET NULL") {
onDelete = "SET NULL"
}
if strings.Contains(constraintStr, "OnUpdate:CASCADE") {
onUpdate = "CASCADE"
} else if strings.Contains(constraintStr, "OnUpdate:SET NULL") {
onUpdate = "SET NULL"
}
}
// The FK is on the referenced table, pointing back to this table
// For has-many, the FK is on the "many" side
constraint := &models.Constraint{
Name: fmt.Sprintf("fk_%s_%s", referencedTable.Name, table.Name),
Type: models.ForeignKeyConstraint,
Table: referencedTable.Name,
Schema: referencedTable.Schema,
Columns: []string{fkColumn},
ReferencedTable: table.Name,
ReferencedSchema: table.Schema,
ReferencedColumns: []string{"id"}, // Typically references the primary key
OnDelete: onDelete,
OnUpdate: onUpdate,
}
referencedTable.Constraints[constraint.Name] = constraint
}
}
// getRelationshipType extracts the type name from a relationship field
func (r *Reader) getRelationshipType(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.ArrayType:
// []*ModelPost -> ModelPost
if starExpr, ok := t.Elt.(*ast.StarExpr); ok {
if ident, ok := starExpr.X.(*ast.Ident); ok {
return ident.Name
}
}
case *ast.StarExpr:
// *ModelPost -> ModelPost
if ident, ok := t.X.(*ast.Ident); ok {
return ident.Name
}
}
return ""
}
// parseIndexesFromTag extracts index definitions from GORM tags
func (r *Reader) parseIndexesFromTag(table *models.Table, column *models.Column, tag string) {
gormTag := r.extractGormTag(tag)
parts := r.parseGormTag(gormTag)
// Check for regular index: index:idx_name or index
if indexName, ok := parts["index"]; ok {
if indexName == "" {
// Auto-generated index name
indexName = fmt.Sprintf("idx_%s_%s", table.Name, column.Name)
}
// Check if index already exists
if _, exists := table.Indexes[indexName]; !exists {
index := &models.Index{
Name: indexName,
Table: table.Name,
Schema: table.Schema,
Columns: []string{column.Name},
Unique: false,
Type: "btree",
}
table.Indexes[indexName] = index
} else {
// Add column to existing index
table.Indexes[indexName].Columns = append(table.Indexes[indexName].Columns, column.Name)
}
}
// Check for unique index: uniqueIndex:idx_name or uniqueIndex
if indexName, ok := parts["uniqueIndex"]; ok {
if indexName == "" {
// Auto-generated index name
indexName = fmt.Sprintf("idx_%s_%s", table.Name, column.Name)
}
// Check if index already exists
if _, exists := table.Indexes[indexName]; !exists {
index := &models.Index{
Name: indexName,
Table: table.Name,
Schema: table.Schema,
Columns: []string{column.Name},
Unique: true,
Type: "btree",
}
table.Indexes[indexName] = index
} else {
// Add column to existing index
table.Indexes[indexName].Columns = append(table.Indexes[indexName].Columns, column.Name)
}
}
// Check for simple unique flag (creates a unique index for this column)
if _, ok := parts["unique"]; ok {
// Auto-generated index name for unique constraint
indexName := fmt.Sprintf("idx_%s_%s", table.Name, column.Name)
if _, exists := table.Indexes[indexName]; !exists {
index := &models.Index{
Name: indexName,
Table: table.Name,
Schema: table.Schema,
Columns: []string{column.Name},
Unique: true,
Type: "btree",
}
table.Indexes[indexName] = index
}
}
}
// extractTableFromGormTag extracts table and schema from gorm tag
func (r *Reader) extractTableFromGormTag(tag string) (tablename string, schemaName string) {
// This is typically set via TableName() method, not in tags
@@ -406,6 +612,7 @@ func (r *Reader) parseColumn(fieldName string, fieldType ast.Expr, tag string, s
column.AutoIncrement = true
}
if def, ok := parts["default"]; ok {
// Default value from GORM tag (e.g., default:gen_random_uuid())
column.Default = def
}
if size, ok := parts["size"]; ok {
@@ -419,9 +626,27 @@ func (r *Reader) parseColumn(fieldName string, fieldType ast.Expr, tag string, s
column.Type = r.goTypeToSQL(fieldType)
}
// Determine if nullable based on Go type
if r.isNullableType(fieldType) {
column.NotNull = false
// Determine if nullable based on GORM tags and Go type
// In GORM:
// - explicit "not null" tag means NOT NULL
// - absence of "not null" tag with sql_types means nullable
// - primitive types (string, int64, bool) default to NOT NULL unless explicitly nullable
if _, hasNotNull := parts["not null"]; hasNotNull {
column.NotNull = true
} else {
// If no explicit "not null" tag, check the Go type
if r.isNullableGoType(fieldType) {
// sql_types.SqlString, etc. are nullable by default
column.NotNull = false
} else {
// Primitive types default to NOT NULL
column.NotNull = false // Default to nullable unless explicitly set
}
}
// Primary keys are always NOT NULL
if column.IsPrimaryKey {
column.NotNull = true
}
return column
@@ -557,11 +782,12 @@ func (r *Reader) sqlTypeToSQL(typeName string) string {
}
}
// isNullableType checks if a Go type represents a nullable field
func (r *Reader) isNullableType(expr ast.Expr) bool {
// isNullableGoType checks if a Go type represents a nullable field type
// (this is for types that CAN be nullable, not whether they ARE nullable)
func (r *Reader) isNullableGoType(expr ast.Expr) bool {
switch t := expr.(type) {
case *ast.StarExpr:
// Pointer type is nullable
// Pointer type can be nullable
return true
case *ast.SelectorExpr:
// Check for sql_types nullable types

View File

@@ -196,15 +196,31 @@ func (tm *TypeMapper) BuildBunTag(column *models.Column, table *models.Table) st
parts = append(parts, "nullzero")
}
// Check for unique constraint
// Check for indexes (unique indexes should be added to tag)
if table != nil {
for _, constraint := range table.Constraints {
if constraint.Type == models.UniqueConstraint {
for _, col := range constraint.Columns {
if col == column.Name {
parts = append(parts, "unique")
break
for _, index := range table.Indexes {
if !index.Unique {
continue
}
// Check if this column is in the index
for _, col := range index.Columns {
if col == column.Name {
// Add unique tag with index name for composite indexes
// or simple unique for single-column indexes
if len(index.Columns) > 1 {
// Composite index - use index name
parts = append(parts, fmt.Sprintf("unique:%s", index.Name))
} else {
// Single column - use index name if it's not auto-generated
// Auto-generated names typically follow pattern: idx_tablename_columnname
expectedAutoName := fmt.Sprintf("idx_%s_%s", table.Name, column.Name)
if index.Name == expectedAutoName {
parts = append(parts, "unique")
} else {
parts = append(parts, fmt.Sprintf("unique:%s", index.Name))
}
}
break
}
}
}