feat(pgsql): support vector and PostGIS indexes with extensions
* Add handling for pgvector and PostGIS extensions in migration scripts * Implement operator class and storage parameters for vector indexes * Update tests to validate new index behaviors and extension creation
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtensionRegistryConsistency(t *testing.T) {
|
||||
for name, ext := range postgresExtensions {
|
||||
if name != ext.Name {
|
||||
t.Errorf("extension registered as %q has Name %q", name, ext.Name)
|
||||
}
|
||||
if name != strings.ToLower(name) {
|
||||
t.Errorf("extension %q must be registered lowercase", name)
|
||||
}
|
||||
if ext.Description == "" || ext.Category == "" {
|
||||
t.Errorf("extension %q is missing a category or description", name)
|
||||
}
|
||||
for _, dependency := range ext.Requires {
|
||||
if !IsKnownExtension(dependency) {
|
||||
t.Errorf("extension %q requires unregistered extension %q", name, dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every extension named by a type in the type registry must itself be registered,
|
||||
// otherwise a column type would ask for a CREATE EXTENSION nothing knows how to order.
|
||||
func TestTypeExtensionsAreRegistered(t *testing.T) {
|
||||
for typeName, spec := range postgresBaseTypes {
|
||||
if spec.Extension == "" {
|
||||
continue
|
||||
}
|
||||
if !IsKnownExtension(spec.Extension) {
|
||||
t.Errorf("type %q declares unregistered extension %q", typeName, spec.Extension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexMethodExtension(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"hnsw": "vector",
|
||||
"ivfflat": "vector",
|
||||
"HNSW": "vector",
|
||||
"vchordrq": "vchord",
|
||||
"vchordg": "vchord",
|
||||
"bm25": "pg_search",
|
||||
"btree": "",
|
||||
"gin": "",
|
||||
"": "",
|
||||
}
|
||||
|
||||
for method, want := range tests {
|
||||
if got := IndexMethodExtension(method); got != want {
|
||||
t.Errorf("IndexMethodExtension(%q) = %q, want %q", method, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorClassExtension(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"gin_trgm_ops": "pg_trgm",
|
||||
"gist_trgm_ops": "pg_trgm",
|
||||
"vector_cosine_ops": "vector",
|
||||
"halfvec_l2_ops": "vector",
|
||||
"gist_ltree_ops": "ltree",
|
||||
"gist_geometry_ops_2d": "postgis",
|
||||
"jsonb_path_ops": "",
|
||||
"array_ops": "",
|
||||
"": "",
|
||||
}
|
||||
|
||||
for opClass, want := range tests {
|
||||
if got := OperatorClassExtension(opClass); got != want {
|
||||
t.Errorf("OperatorClassExtension(%q) = %q, want %q", opClass, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionsForExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
want []string
|
||||
}{
|
||||
{"empty", "", nil},
|
||||
{"no functions", "status = 'active'", nil},
|
||||
{"builtin only", "now()", nil},
|
||||
{"uuid-ossp default", "uuid_generate_v4()", []string{"uuid-ossp"}},
|
||||
{"gen_random_uuid is builtin", "gen_random_uuid()", nil},
|
||||
{"pgcrypto", "crypt(password, gen_salt('bf'))", []string{"pgcrypto"}},
|
||||
{"postgis prefix", "ST_Area(geom) > 0", []string{"postgis"}},
|
||||
{"paradedb prefix", "paradedb.snippet(body)", []string{"pg_search"}},
|
||||
{"jsonschema", "json_matches_schema('{}', payload)", []string{"pg_jsonschema"}},
|
||||
{"whitespace before paren", "unaccent ('crème')", []string{"unaccent"}},
|
||||
{"multiple sorted", "ST_X(geom) = levenshtein(a, b)::float", []string{"fuzzystrmatch", "postgis"}},
|
||||
{"column named like function", "similarity_score > 0.5", nil},
|
||||
{"numeric prefix ignored", "2(3)", nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ExtensionsForExpression(tt.expression); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ExtensionsForExpression(%q) = %v, want %v", tt.expression, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortExtensions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
want []string
|
||||
}{
|
||||
{"empty", nil, nil},
|
||||
{"alphabetical", []string{"pg_trgm", "citext"}, []string{"citext", "pg_trgm"}},
|
||||
{"deduplicated", []string{"vector", "vector", " VECTOR "}, []string{"vector"}},
|
||||
{"dependency first", []string{"vchord", "vector"}, []string{"vector", "vchord"}},
|
||||
{
|
||||
"postgis dependants",
|
||||
[]string{"postgis_topology", "pgrouting", "postgis"},
|
||||
[]string{"postgis", "pgrouting", "postgis_topology"},
|
||||
},
|
||||
{"dependency not requested", []string{"vchord"}, []string{"vchord"}},
|
||||
{"unknown names kept", []string{"zzz_custom", "citext"}, []string{"citext", "zzz_custom"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := SortExtensions(tt.input); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("SortExtensions(%v) = %v, want %v", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuoteExtensionName(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"vector": "vector",
|
||||
"pg_trgm": "pg_trgm",
|
||||
"uuid-ossp": `"uuid-ossp"`,
|
||||
"PostGIS": `"PostGIS"`,
|
||||
"": "",
|
||||
}
|
||||
|
||||
for name, want := range tests {
|
||||
if got := QuoteExtensionName(name); got != want {
|
||||
t.Errorf("QuoteExtensionName(%q) = %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionDependencies(t *testing.T) {
|
||||
if got := ExtensionDependencies("vchord"); !reflect.DeepEqual(got, []string{"vector"}) {
|
||||
t.Errorf("ExtensionDependencies(vchord) = %v, want [vector]", got)
|
||||
}
|
||||
if got := ExtensionDependencies("citext"); got != nil {
|
||||
t.Errorf("ExtensionDependencies(citext) = %v, want nil", got)
|
||||
}
|
||||
if got := ExtensionDependencies("not_an_extension"); got != nil {
|
||||
t.Errorf("ExtensionDependencies(not_an_extension) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user