Skip to content
导航栏

pg 数据库增加自定义类型

在 pg 数据库里可以增加自定义的类型。如果需要在 yao 是支持使用 pg 数据库的自定义类型,需要针对源代码作一些增强。

需求:给 pg 数据库增加一个自定义的类型 vector,对应 pg 数据库的插件 pgvector。

效果:比如下面的类型定义中的类类型 vector。

json
{
  "columns": [
    {
      "name": "id",
      "label": "ID",
      "type": "ID",
      "precision": 64,
      "primary": true
    },
    {
      "name": "filename",
      "label": "filename",
      "type": "text",
      "nullable": true
    },
    {
      "name": "content",
      "label": "CONTENT",
      "type": "text",
      "nullable": true
    },
    {
      "name": "embedding",
      "label": "EMBEDDING",
      "type": "vector",
      "length": 1024,
      // "length": 768,
      "nullable": true,
      "index": true
    }
  ],
  "option": {},
  "table": {
    "name": "documents"
  },
  "name": "documents",
  "relations": {}
}
// yao migrate -n documents --reset

GITHUB 提交

代码增强

在 yao 应用中,数据库的交互由组件 xun 完成,xun 的功能有点类似于 orm,适配不同的数据库。

首先需要调整 postgres 的语法器代码,给它增加自定义类型映射。在这里直接使用模型定义中的 vector 对应数据库中的 vector 类型。

go
// xun/grammar/postgres/postgres.go

// New Create a new mysql grammar inteface
func New() dbal.Grammar {
	pg := Postgres{
		SQL: sql.NewSQL(&Quoter{}),
	}
	pg.Driver = "postgres"
	pg.IndexTypes = map[string]string{
		"unique": "UNIQUE INDEX",
		"index":  "INDEX",
	}

	// overwrite types
	types := pg.SQL.Types
	types["tinyInteger"] = "SMALLINT"
	types["bigInteger"] = "BIGINT"
	types["string"] = "CHARACTER VARYING"
	types["integer"] = "INTEGER"
	types["decimal"] = "NUMERIC"
	types["float"] = "REAL"
	types["double"] = "DOUBLE PRECISION"
	types["char"] = "CHARACTER"
	types["mediumText"] = "TEXT"
	types["longText"] = "TEXT"
	types["dateTime"] = "TIMESTAMP(%d) WITHOUT TIME ZONE"
	types["dateTimeTz"] = "TIMESTAMP(%d) WITH TIME ZONE"
	types["time"] = "TIME(%d) WITHOUT TIME ZONE"
	types["timeTz"] = "TIME(%d) WITH TIME ZONE"
	types["timestamp"] = "TIMESTAMP(%d) WITHOUT TIME ZONE"
	types["timestampTz"] = "TIMESTAMP(%d) WITH TIME ZONE"
	types["binary"] = "BYTEA"
	types["macAddress"] = "MACADDR"
	types["vector"] = "VECTOR" //pgvector
	pg.Types = types

	// set fliptypes
    // 反向映射
	flipTypes, ok := utils.MapFilp(pg.Types)
	if ok {
		pg.FlipTypes = flipTypes.(map[string]string)
		pg.FlipTypes["TEXT"] = "text"
		pg.FlipTypes["TIMESTAMP WITHOUT TIME ZONE"] = "timestamp"
		pg.FlipTypes["TIMESTAMP WITH TIME ZONE"] = "timestampTz"
		pg.FlipTypes["TIME WITHOUT TIME ZONE"] = "time"
		pg.FlipTypes["TIME WITH TIME ZONE"] = "timeTz"
		pg.FlipTypes["SMALLINT"] = "smallInteger"

		pg.FlipTypes["VECTOR"] = "vector" //pgvector
	}

	return pg
}

然后给 Blueprint 的接口增加一个方法定义。这里增加的方法定义是为了给 gou 库暴露接口。

go
// xun/dbal/schema/interfaces.go
// pgvector
Vector(name string, args ...int) *Column

新增接口的实现,vector 类型对应数据库中的 vector 类型,并且可以设置 vector 的维度,这里直接使用 yao 模型定义中的长度来设置。

go
// xun/dbal/schema/blueprint.go

// pgvector
func (table *Table) Vector(name string, args ...int) *Column {
	column := table.newColumn(name).SetType("vector")
	column.MaxLength = 16000 //max dimensions
	column.DefaultLength = 1536
	length := column.DefaultLength
	if len(args) >= 1 {
		length = args[0]
	}
	column.SetLength(length)
	table.putColumn(column)
	return column
}

接下来改造 builder.go 文件,它的作用是针对于 pg 数据库,如何把 yao 模型的定义构造成对应 pg 数据库的 sql 语句。

go
// xun/grammar/postgres/builder.go

// SQLAddComment return the add comment sql for table create
func (grammarSQL Postgres) SQLAddComment(column *dbal.Column) string {

	mappingTypes := []string{"ipAddress", "year", "vector"}//增加新的vector,这里会把自定义类型写入字段的备注里。
	if utils.StringHave(mappingTypes, column.Type) {
		comment = fmt.Sprintf("COMMENT on column %s.%s is %s;",
			grammarSQL.ID(column.TableName),
			grammarSQL.ID(column.Name),
			grammarSQL.VAL(fmt.Sprintf("T:%s|%s", column.Type, utils.StringVal(column.Comment))),
		)
	}
	return comment
}



// SQLAddIndex  return the add index sql for table create
func (grammarSQL Postgres) SQLAddIndex(index *dbal.Index) string {
	quoter := grammarSQL.Quoter
	indexTypes := grammarSQL.IndexTypes
	typ, has := indexTypes[index.Type]
	if !has {
		typ = "KEY"
	}

	// UNIQUE KEY `unionid` (`unionid`) COMMENT 'xxxx'
	// IS JSON
	columns := []string{}
	isJSON := false
	isVector := false //增加一个判断是否vector类型
	for _, column := range index.Columns {
		columns = append(columns, quoter.ID(column.Name))
		if column.Type == "json" || column.Type == "jsonb" {
			isJSON = true
		} else if column.Type == "vector" {
			isVector = true //字段类型是vector
		}
	}
	if isJSON {
		return ""
	}

	comment := ""
	if index.Comment != nil {
		comment = fmt.Sprintf("COMMENT %s", quoter.VAL(index.Comment))
	}
	name := quoter.ID(fmt.Sprintf("%s_%s", index.TableName, index.Name))
	sql := ""
	if typ == "PRIMARY KEY" {
		sql = fmt.Sprintf(
			"%s (%s) %s",
			typ, strings.Join(columns, ","), comment)
	} else if isVector {
        // 自定义vector的索引构造sql,这里是使用了hnsw算法,自动的给新增数据也增加索引
		sql = fmt.Sprintf(
			"CREATE INDEX %s ON %s USING hnsw (%s vector_cosine_ops) WITH (m = 24, ef_construction = 200)",
			name, quoter.ID(index.TableName), strings.Join(columns, ","))
	} else {
		sql = fmt.Sprintf(
			"CREATE %s %s ON %s (%s)",
			typ, name, quoter.ID(index.TableName), strings.Join(columns, ","))
	}
	return sql
}

另外一个额外的优化是在根据数据库表结构生成对应 yao 模型定义时的处理。从备注中删除类型定义,避免下次更新表结构时重复赋值。

go
func (grammarSQL Postgres) GetColumnListing(dbName string, tableName string) ([]*dbal.Column, error) {


	// Cast the database data type to DBAL data type
	for _, column := range columns {
		typ, has := grammarSQL.FlipTypes[column.Type]
		if has {
			column.Type = typ
		}

		if column.Comment != nil {
			typ = grammarSQL.GetTypeFromComment(column.Comment)
			if typ != "" {
				column.Type = typ
                // 从备注中删除类型定义,避免下次更新表结构时重复赋值
				*column.Comment = strings.ReplaceAll(*column.Comment, fmt.Sprintf("T:%s|", typ), "")
			}
		}
    }

}

最后还需要对 GOU 库进行增强,在转换类型为 vector 的字段时调用 xun 库的 vector 方法。

go
// gou/schema/xun/column.go

func setColumnType(table schema.Blueprint, column types.Column) (*schema.Column, error) {

    // 在转换类型为vector的字段时调用xun库的vector方法。
	case "vector":
		return table.Vector(column.Name, column.Length), nil
	}

}