feat(spectypes): add support for PostGIS and pgvector types

* Implement custom types: SqlGeometry, SqlGeography, SqlHalfVector, SqlSparseVector, SqlBitVector
* Add spatial filter operators and vector similarity operators
* Include metadata and OpenAPI reporting for geometry/vector column types
* Create tests for EWKB and WKT conversions
This commit is contained in:
2026-08-29 21:27:42 +02:00
parent 798bb47e71
commit f259df1258
22 changed files with 2886 additions and 9 deletions
+56
View File
@@ -0,0 +1,56 @@
package reflection
import (
"testing"
"github.com/bitechdev/ResolveSpec/pkg/spectypes"
)
type geoModel struct {
ID int64 `json:"id"`
Location spectypes.SqlGeometry `json:"location"`
Area spectypes.SqlGeography `json:"area"`
Embedding spectypes.SqlVector `json:"embedding"`
HalfEmb spectypes.SqlHalfVector `json:"half_emb"`
Name spectypes.SqlString `json:"name"`
}
func TestGetColumnSQLTypeName(t *testing.T) {
m := geoModel{}
cases := map[string]string{
"location": "geometry",
"area": "geography",
"embedding": "vector",
"half_emb": "halfvec",
"name": "text",
}
for col, want := range cases {
got, ok := GetColumnSQLTypeName(m, col)
if !ok || got != want {
t.Errorf("GetColumnSQLTypeName(%q) = %q, %v; want %q", col, got, ok, want)
}
}
if _, ok := GetColumnSQLTypeName(m, "id"); ok {
t.Error("id is not a spectypes column")
}
}
func TestIsSpatialColumn(t *testing.T) {
m := geoModel{}
if !IsSpatialColumn(m, "location") || !IsSpatialColumn(m, "area") {
t.Error("location/area should be spatial")
}
if IsSpatialColumn(m, "embedding") || IsSpatialColumn(m, "name") {
t.Error("embedding/name should not be spatial")
}
}
func TestIsVectorColumn(t *testing.T) {
m := geoModel{}
if !IsVectorColumn(m, "embedding") || !IsVectorColumn(m, "half_emb") {
t.Error("embedding/half_emb should be vector")
}
if IsVectorColumn(m, "location") || IsVectorColumn(m, "name") {
t.Error("location/name should not be vector")
}
}
+94
View File
@@ -0,0 +1,94 @@
package reflection
import (
"reflect"
"github.com/bitechdev/ResolveSpec/pkg/spectypes"
)
// getColumnFieldType resolves the reflect.Type of the struct field that backs
// colName (matched by json tag, field name or snake_case), following the same
// rules as GetColumnTypeFromModel.
func getColumnFieldType(model interface{}, colName string) (reflect.Type, bool) {
if model == nil {
return nil, false
}
sourceColName := ExtractSourceColumn(colName)
modelType := reflect.TypeOf(model)
for modelType != nil && modelType.Kind() == reflect.Pointer {
modelType = modelType.Elem()
}
if modelType == nil || modelType.Kind() != reflect.Struct {
return nil, false
}
for i := 0; i < modelType.NumField(); i++ {
field := modelType.Field(i)
if jsonTag := field.Tag.Get("json"); jsonTag != "" {
if name := jsonTagName(jsonTag); name == sourceColName {
return field.Type, true
}
}
if equalFold(field.Name, sourceColName) {
return field.Type, true
}
if ToSnakeCase(field.Name) == sourceColName {
return field.Type, true
}
}
return nil, false
}
func jsonTagName(tag string) string {
for i := 0; i < len(tag); i++ {
if tag[i] == ',' {
return tag[:i]
}
}
return tag
}
func equalFold(a, b string) bool {
if len(a) != len(b) {
return false
}
for i := 0; i < len(a); i++ {
ca, cb := a[i], b[i]
if 'A' <= ca && ca <= 'Z' {
ca += 'a' - 'A'
}
if 'A' <= cb && cb <= 'Z' {
cb += 'a' - 'A'
}
if ca != cb {
return false
}
}
return true
}
// GetColumnSQLTypeName returns the canonical PostgreSQL type name for a column
// backed by a spectypes wrapper (e.g. "geometry", "vector", "jsonb"), or
// ("", false) if the column is not found or not a spectypes type.
func GetColumnSQLTypeName(model interface{}, colName string) (string, bool) {
t, ok := getColumnFieldType(model, colName)
if !ok {
return "", false
}
return spectypes.SQLTypeName(t)
}
// IsSpatialColumn reports whether colName is backed by a PostGIS
// geometry/geography wrapper.
func IsSpatialColumn(model interface{}, colName string) bool {
t, ok := getColumnFieldType(model, colName)
return ok && spectypes.IsSpatialType(t)
}
// IsVectorColumn reports whether colName is backed by a pgvector wrapper.
func IsVectorColumn(model interface{}, colName string) bool {
t, ok := getColumnFieldType(model, colName)
return ok && spectypes.IsVectorType(t)
}