-
Notifications
You must be signed in to change notification settings - Fork 77
Description
Currently, the driver only supports query parameters of the following types:
- Primitives
- A handful of struct types which map to specific DB types
- Pointers to a supported parameter type
- Slices with elements of a supported parameter type
- Maps with string keys and values of a supported parameter type
- Interfaces, so long as the concrete values are a supported parameter type
It would be useful to be able to define a way to serialize a custom type, then pass values of that type in as part of the parameters set and have the serialization happen directly. (Likely, this would most-commonly be serializing custom struct types as maps, though other cases could exist.) Currently, the only alternative is to convert values of the custom types into supported types, then construct a parameter set using only these values. However, this process is relatively slow, memory inefficient, and causes a large number of allocations, especially when doing batch data insert operations.
I propose making the following changes to the API:
- Move
packstream.Packer
, or an interface with an appropriate subset of its methods, into the public API. - Do one of the following:
- Define an interface with a single method to serialize a type - if a value conforming to this interface is found during parameter serialization, serialize it using this method.
- Ex:
type Neo4jSerializer interface { SerializeNeo4j(*packstream.Packer) error }
- Provide a way to register a function to serialize values of a particular type - during serialization, if a value is encountered for which a function has been registered, serialize it using that function.
- Ex:
type SerializeFunc[T any] func(*packstream.Packer, T) func RegisterSerializer[T any](SerializeFunc[T])
- It may make sense for the registration to be global, or to be defined on the driver.
- Define an interface with a single method to serialize a type - if a value conforming to this interface is found during parameter serialization, serialize it using this method.
As far as I can tell, neither option should have backward compatibility concerns. The latter would not break anything because unless custom functions are registered it would behave the same. The former would only change the behavior if a conflicting method was already defined on a type in client code, but because the function uses a type that's not currently in the public API this is not possible.
The former option is simpler but more limiting in several ways, whereas the latter has a slightly more complicated API (and likely a slightly more complicated implementation) but is more flexible in the way that serialization could be set up. Either should be implementable with relatively low overhead on top of the existing serialization code - I'm not sure if one would be faster than the other.
One issue that can't be fixed without at least technically making a backward-incompatible change is the fact that the API always expects query parameters to be passed in as a map[string]any
. This is awkward, since it means the outermost map cannot be created through custom serialization. However, it's probably not worth trying to change this, at least until the next breaking API version. At least from a performance standpoint the impact is minimal - in most cases, most of the impact will come from lists of sub-maps rather than large outermost maps, and the problem can be worked around relatively easily. Additionally, leaving it as-is sidesteps the problem of dealing with a root parameter type that doesn't serialize to a map.
A tangential consideration is that it could be useful to allow clients to define serialization mappings for structs using tags. However, it would be hard to write efficient code to handle the reflective mapping, and this would only support specific ways of mapping structs, so I would argue that defining fully custom logic is useful regardless.