Skip to content

Keep the type arguments of generic column types and recognise their interfaces - #66

Open
dschreij wants to merge 3 commits into
go-gorm:masterfrom
dschreij:fix/generic-column-types
Open

dschreij wants to merge 3 commits into
go-gorm:masterfrom
dschreij:fix/generic-column-types

Conversation

@dschreij

@dschreij dschreij commented Sep 5, 2026

Copy link
Copy Markdown

Problem

A field of an instantiated generic type came out wrong twice over. datatypes.JSONType[map[string]string], for one, generated field.Struct[datatypes.JSONType[any]]:

  • The type arguments were lost. The type printer had no case for a map type, an interface type or a second argument (Pair[K, V]), so they rendered as any.
  • The type was not recognised as a column. The package and type names were split at the last dot of the whole expression, which for Box[example.com/sample.Plain] falls inside the argument, so the named type was never found and its driver.Valuer and sql.Scanner implementations were never checked. The field fell through to the association helper.

Fix

The package and type split happens on the part before the arguments and the lookup asks for the bare generic name. ImplementsAllowedInterfaces instantiates a generic type with any for each parameter before asking types.Implements, whose behaviour is unspecified for an uninstantiated generic type; constraints are not checked, since the question is which methods the type has and Value and Scan do not mention the parameters. The printer renders map types, multi-argument instantiations and the empty interface. The helper keeps the arguments as the field spelled them, shortened per component so a package path inside an argument does not break the type: field.Field[datatypes.JSONType[map[string]string]], field.Struct[sample.Pair[sample.Plain, int]].

Test

TestGenericColumnTypesKeepTheirArgumentsAndInterfaces generates over a generic Box[T] implementing Valuer and Scanner, instantiated with a map, a local struct and through a pointer, and over a generic Pair[K, V] that implements neither and stays an association with both arguments intact. TestShortTypeName gains the multi-argument cases.

This branch is stacked on #65, whose shortTypeName it extends; merging #65 first leaves this one with a single fix commit. The test helper commit is the one shared by the other pull requests from this fork.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 5, 2026 11:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There’s a confirmed incorrect type-shortening path for slice relation element types (generic args can be truncated), and the new shared test helper should create parent directories for nested relative paths.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves the generator’s type handling for generic Go types so that (1) instantiated type arguments are preserved when rendering helper types and (2) generic named types are correctly recognized as “column” types when they implement driver.Valuer / sql.Scanner (or other allowed interfaces), rather than being misclassified as associations.

Changes:

  • Instantiate uninstantiated generic named types with any before checking types.Implements, to reliably detect interface implementations.
  • Preserve and correctly shorten complex type expressions (maps, multi-arg generic instantiations, empty interface) so generated helpers keep the original type arguments.
  • Add focused regression tests using a temp-module generator harness for serializer-tagged fields and generic column/association classification.
File summaries
File Description
internal/gen/utils.go Adds generic instantiation for interface checks and introduces robust shortTypeName (maps, multi-arg generics) + serializer tag detection.
internal/gen/generator.go Fixes package/type splitting for generics, uses shortTypeName for generated helper types, and extends type printer for maps, multi-arg generics, and empty interface.
internal/gen/serializer_columns_test.go Adds regression tests ensuring serializer-tagged fields generate field.Field[...] and expands shortTypeName coverage.
internal/gen/generic_column_types_test.go Adds regression tests ensuring generic column types keep their type args and are classified by interface implementation.
internal/gen/generator_support_test.go Adds shared test helper to generate from ad-hoc sources in a temp module and assert generated helper field types.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/gen/generator.go Outdated
Comment on lines 473 to 475
@@ -464,7 +474,7 @@ func (f Field) Type() string {
elementType := filepath.Base(strings.TrimPrefix(goType, "[]"))
return fmt.Sprintf("field.Slice[%s]", elementType)
Comment on lines +19 to +23
for name, content := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
@dschreij
dschreij force-pushed the fix/generic-column-types branch from 873c987 to b085164 Compare September 5, 2026 15:38
@dschreij

dschreij commented Sep 5, 2026

Copy link
Copy Markdown
Author

Addressed the review. The shared test helper now creates parent directories before writing, so a nested key like models/model.go works as its doc comment promises.

The slice finding was a real defect and is fixed. field.Slice still took filepath.Base of the raw element while the struct branch had moved to shortTypeName, so an element carrying an import path inside its type arguments was truncated at the last slash: []Box[example.com/sample.Plain] rendered as field.Slice[sample.Plain]]. The element now goes through shortTypeName, and the leading * is still dropped, as you noted it should be.

TestGenericColumnTypesKeepTheirArgumentsAndInterfaces gained a []Box[Plain] field asserting field.Slice[sample.Box[sample.Plain]]; it fails on the previous code.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Field.Type() can unnecessarily invoke packages.Load for slice/map types due to the new strings.Cut base-type extraction, potentially causing significant performance regressions during generation.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread internal/gen/generator.go Outdated
Comment on lines 466 to 470
if typ := loadNamedType(f.file.goModDir, f.file.getFullImportPath(pkgName), typName); typ != nil {
if ImplementsAllowedInterfaces(typ) { // For interface-implementing types, use generic Field
return fmt.Sprintf("field.Field[%s]", filepath.Base(goType))
return fmt.Sprintf("field.Field[%s]", shortTypeName(goType))
}
}
@dschreij
dschreij force-pushed the fix/generic-column-types branch from b085164 to ae3038d Compare September 5, 2026 20:00
@dschreij

dschreij commented Sep 5, 2026

Copy link
Copy Markdown
Author

Addressed the second round, and this one was a real regression rather than a nit.

You are right that the strings.Cut base-type extraction made the lookup run for containers. []T cuts to an empty base name and map[K]V to map, and where the old code passed a nonsense package path that failed fast, the new one passed the file's own package, so packages.Load did real work before finding nothing. On a struct with many slice fields that is not free.

The lookup is now guarded by isNamedTypeCandidate, which skips an empty base name, map, and anything starting with [] or map[. A named slice or map still reaches the lookup, since its name is its own and a named byte slice has to keep resolving to field.Bytes.

The shared test helper also reads the whole output tree rather than its top level.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The interface-implementation lookup still resolves the package path incorrectly for fully-qualified types, so external interface-implementing types may remain misclassified despite the intended fix.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread internal/gen/generator.go
Comment on lines +466 to 476
// Only a plausible named type is worth loading a package for. A container has none to find:
// "[]T" leaves an empty base name and "map[K]V" leaves "map", and the lookup would shell out
// to the go command for a package it then finds nothing in. A named slice or map still
// reaches this, its name being its own.
if isNamedTypeCandidate(goType, typName) {
if typ := loadNamedType(f.file.goModDir, f.file.getFullImportPath(pkgName), typName); typ != nil {
if ImplementsAllowedInterfaces(typ) { // For interface-implementing types, use generic Field
return fmt.Sprintf("field.Field[%s]", shortTypeName(goType))
}
}
}
@dschreij
dschreij force-pushed the fix/generic-column-types branch from ae3038d to 4f1549f Compare September 5, 2026 20:45
@dschreij

dschreij commented Sep 5, 2026

Copy link
Copy Markdown
Author

I do not think this one holds, and I have added a test rather than just argue it.

getFullImportPath returns its argument unchanged when it matches no import, not "":

func (p *File) getFullImportPath(shortName string) string {
	for _, i := range p.Imports {
		if i.Name == shortName {
			return i.Path
		}
	}
	return shortName
}

So for gorm.io/datatypes.JSONType[map[string]string] the base type splits into gorm.io/datatypes and JSONType, no import is named gorm.io/datatypes, and the full path is handed to loadNamedType unchanged. That is the path the load needs, so the type resolves and the interface check runs.

TestGenericColumnTypesResolveThroughAFullImportPath now pins it: a generic Valuer declared in another package of the fixture module, referenced through its full import path, has to come out as field.Field[box.Box[map[string]string]]. It would be field.Struct if the lookup returned nothing.

Independently of the test, this is also what the change was written for. Running the generator over a real model package, a datatypes.JSONType[map[string]string] column went from field.Struct[datatypes.JSONType[any]] to field.Field[datatypes.JSONType[map[string]string]], which only happens when the external type resolves and ImplementsAllowedInterfaces sees its Valuer.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The updated map type rendering can cause map[...]... fields containing package-qualified element types to be misclassified as field.Struct[...] associations, which can generate incorrect helpers.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread internal/gen/generator.go
Comment on lines 484 to 486
} else if strings.Contains(goType, ".") {
return fmt.Sprintf("field.Struct[%s]", filepath.Base(goType))
return fmt.Sprintf("field.Struct[%s]", shortTypeName(goType))
}
…nterfaces

A field of an instantiated generic type, datatypes.JSONType[map[string]string]
for one, came out wrong twice over. Its type arguments were lost: the type
printer had no case for a map type, an interface type or a second argument, so
they rendered as any. And the package and type names were split at the last dot
of the whole expression, which for Box[example.com/sample.Plain] falls inside
the argument, so the named type was never found, its Valuer and Scanner
interfaces never checked, and the column became an association helper:
field.Struct[datatypes.JSONType[any]].

The split now happens on the part before the arguments, the lookup asks for the
bare generic name, and the type is instantiated with any for each parameter
before types.Implements is asked, which is unspecified for an uninstantiated
generic type. The printer renders map types, multi-argument instantiations and
the empty interface, and the helper keeps the arguments as the field spelled
them, shortened per component so a package path inside an argument does not
break the type.

TestGenericColumnTypesKeepTheirArgumentsAndInterfaces covers a generic Valuer
with a map, a local struct and a pointer instantiation, and a generic struct
that stays an association with its two arguments intact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@dschreij
dschreij force-pushed the fix/generic-column-types branch from 4f1549f to d4461ee Compare September 6, 2026 05:29
@dschreij

dschreij commented Sep 6, 2026

Copy link
Copy Markdown
Author

Right, and this one was mine. Fixed.

Rendering the map type is what exposed it: the association branch tests for a dot anywhere in the type, so map[string]sample.Attr matched and came out as field.Struct[map[string]sample.Attr]. Before this pull request an unnamed map rendered as any and fell through to field.Field[any], wrong in a different way but at least a column.

A map is now its own branch, ahead of the association test, and the default return uses shortTypeName too, as you suggested, so it does not truncate a generic either.

TestMapColumnsAreNotAssociations pins it over a plain map, one with a qualified value, and one whose value is a slice of a qualified type. It fails without the new branch.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The changes directly address the described generic-type and interface-detection issues and are covered by targeted new tests for both behavior and formatting.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants