Skip to content

Commit 4c04a95

Browse files
feat(ivfflat): add INCLUDE column support for IVF indexes (#24168)
Add INCLUDE column support across IVF DDL, entries-table maintenance, planner rewrites, and ivf_search so covered vector queries can push include predicates and avoid base-table joins when mode=include. Keep mode=post and mode=pre on their original single-round path, switch mode=include fallback from cumulative bucket expansion to non-overlapping bucket slices, reset per-input search round state, and collapse EXPLAIN ANALYZE background ivf_search plans by default while keeping capped verbose expansion. Add parser, planner, ALTER, explain, unit, and distributed SQL coverage for the new INCLUDE behavior. Approved by: @ouyuanning, @LeftHandCold, @heni02, @cpegeric, @aunjgr, @fengttt, @XuPeng-SH, @gouhongshen
1 parent f9c1419 commit 4c04a95

123 files changed

Lines changed: 13426 additions & 1815 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pkg/bootstrap/versions/v4_0_0/upgrade_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/golang/mock/gomock"
2323
"github.com/prashantv/gostub"
2424
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
2526

2627
"github.com/matrixorigin/matrixone/pkg/bootstrap/versions"
2728
"github.com/matrixorigin/matrixone/pkg/catalog"
@@ -48,6 +49,10 @@ func mockTenantUpgrade(t *testing.T) {
4849
}
4950

5051
func Test_Upgrade(t *testing.T) {
52+
originalTenantUpgEntries := append([]versions.UpgradeEntry(nil), tenantUpgEntries...)
53+
defer func() {
54+
tenantUpgEntries = originalTenantUpgEntries
55+
}()
5156
mockTenantUpgrade(t)
5257

5358
sid := ""
@@ -85,11 +90,11 @@ func Test_Upgrade(t *testing.T) {
8590
}
8691

8792
func Test_versionHandle_HandleClusterUpgrade(t *testing.T) {
88-
originalEntries := clusterUpgEntries
89-
clusterUpgEntries = nil
93+
originalClusterUpgEntries := append([]versions.UpgradeEntry(nil), clusterUpgEntries...)
9094
defer func() {
91-
clusterUpgEntries = originalEntries
95+
clusterUpgEntries = originalClusterUpgEntries
9296
}()
97+
clusterUpgEntries = []versions.UpgradeEntry{}
9398

9499
v := &versionHandle{
95100
metadata: versions.Version{
@@ -109,6 +114,16 @@ func Test_versionHandle_HandleClusterUpgrade(t *testing.T) {
109114
assert.Nil(t, err)
110115
}
111116

117+
func TestUpgradeDefersMoIndexesIncludeColumn(t *testing.T) {
118+
for _, entries := range [][]versions.UpgradeEntry{clusterUpgEntries, tenantUpgEntries} {
119+
for _, entry := range entries {
120+
require.False(t,
121+
entry.Schema == catalog.MO_CATALOG && entry.TableName == catalog.MO_INDEXES && entry.UpgType == versions.ADD_COLUMN,
122+
"mo_indexes schema changes require a staged release after all writers use explicit column lists")
123+
}
124+
}
125+
}
126+
112127
func Test_upg_create_mo_task_sql_task_check_exists(t *testing.T) {
113128
stubs := gostub.Stub(&versions.CheckTableDefinition, func(txn executor.TxnExecutor, accountId uint32, schema string, table string) (bool, error) {
114129
assert.Equal(t, uint32(0), accountId)

pkg/bootstrap/versions/v4_0_1/upgrade_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import (
2121
"github.com/golang/mock/gomock"
2222
"github.com/prashantv/gostub"
2323
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2425

2526
"github.com/matrixorigin/matrixone/pkg/bootstrap/versions"
27+
"github.com/matrixorigin/matrixone/pkg/catalog"
2628
"github.com/matrixorigin/matrixone/pkg/common/moerr"
2729
"github.com/matrixorigin/matrixone/pkg/common/runtime"
2830
mock_frontend "github.com/matrixorigin/matrixone/pkg/frontend/test"
@@ -94,6 +96,16 @@ func Test_versionHandle_HandleClusterUpgrade(t *testing.T) {
9496
assert.Nil(t, err)
9597
}
9698

99+
func TestUpgradeDefersMoIndexesIncludeColumn(t *testing.T) {
100+
for _, entries := range [][]versions.UpgradeEntry{clusterUpgEntries, tenantUpgEntries} {
101+
for _, entry := range entries {
102+
require.False(t,
103+
entry.Schema == catalog.MO_CATALOG && entry.TableName == catalog.MO_INDEXES && entry.UpgType == versions.ADD_COLUMN,
104+
"mo_indexes schema changes require a staged release after all writers use explicit column lists")
105+
}
106+
}
107+
}
108+
97109
func Test_upg_statistics_view_check_error(t *testing.T) {
98110
stubs := gostub.Stub(&versions.CheckViewDefinition, func(txn executor.TxnExecutor, accountId uint32, schema string, viewName string) (bool, string, error) {
99111
return true, "", moerr.NewInternalErrorNoCtx("return error")

pkg/catalog/secondary_index_utils.go

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,10 @@ const (
113113
IntermediateGraphDegree = "intermediate_graph_degree"
114114
GraphDegree = "graph_degree"
115115
ITopkSize = "itopk_size"
116-
IncludedColumns = "included_columns"
116+
// IncludedColumns persists INCLUDE metadata inside algo_params. Consumers
117+
// prefer plan.IndexDef.IncludedColumns when present and fall back to this key
118+
// for catalog-loaded definitions.
119+
IncludedColumns = "included_columns"
117120

118121
// Index-defining build params, settable as CREATE INDEX options (parsed by
119122
// each plugin's ParamsFromTree). Written into flat algo_params only when
@@ -127,6 +130,49 @@ const (
127130
IndexAlgoParamPrefixLengths = "prefix_lengths"
128131
)
129132

133+
func ParseIncludeColumnsValue(raw string) ([]string, error) {
134+
raw = strings.TrimSpace(raw)
135+
if raw == "" {
136+
return nil, nil
137+
}
138+
139+
if strings.HasPrefix(raw, "[") {
140+
var cols []string
141+
if err := json.Unmarshal([]byte(raw), &cols); err == nil {
142+
return cols, nil
143+
}
144+
}
145+
146+
parts := strings.Split(raw, ",")
147+
cols := make([]string, 0, len(parts))
148+
for _, part := range parts {
149+
part = strings.TrimSpace(part)
150+
if part == "" {
151+
continue
152+
}
153+
cols = append(cols, part)
154+
}
155+
if len(cols) == 0 {
156+
return nil, nil
157+
}
158+
return cols, nil
159+
}
160+
161+
// MarshalIncludeColumnsValue encodes INCLUDE column names without losing
162+
// commas or leading/trailing whitespace that are valid inside quoted SQL
163+
// identifiers. ParseIncludeColumnsValue retains support for the historical
164+
// comma-separated representation when catalog metadata is reloaded.
165+
func MarshalIncludeColumnsValue(cols []string) (string, error) {
166+
if len(cols) == 0 {
167+
return "", nil
168+
}
169+
encoded, err := json.Marshal(cols)
170+
if err != nil {
171+
return "", err
172+
}
173+
return string(encoded), nil
174+
}
175+
130176
/* 1. ToString Functions */
131177

132178
// IndexParamsToStringList used by buildShowCreateTable and restoreDDL
@@ -223,19 +269,6 @@ func IndexParamsToStringList(indexParams string) (string, error) {
223269
if val, ok := result[IndexAlgoParamQuantizerTrainLimit]; ok {
224270
res += fmt.Sprintf(" %s = %s ", IndexAlgoParamQuantizerTrainLimit, val)
225271
}
226-
227-
if val, ok := result[IncludedColumns]; ok && len(val) > 0 {
228-
raw := strings.Split(val, ",")
229-
parts := make([]string, 0, len(raw))
230-
for _, p := range raw {
231-
if p = strings.TrimSpace(p); p != "" {
232-
parts = append(parts, p)
233-
}
234-
}
235-
if len(parts) > 0 {
236-
res += " INCLUDE (" + strings.Join(parts, ", ") + ") "
237-
}
238-
}
239272
return res, nil
240273
}
241274

@@ -425,7 +458,7 @@ func indexParamsToMap(def interface{}) (map[string]string, error) {
425458
case tree.INDEX_TYPE_BTREE, tree.INDEX_TYPE_INVALID, tree.INDEX_TYPE_RTREE:
426459
// do nothing
427460
case tree.INDEX_TYPE_MASTER:
428-
// do nothing
461+
// do nothing
429462
default:
430463
// Vector algorithms (IVFFLAT / HNSW / CAGRA / IVFPQ) build their
431464
// algo_params via the per-plugin plan hook BuildIndexParams; they

pkg/catalog/secondary_index_utils_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ func newPrefixKeyPart(colName string, length int) *tree.KeyPart {
5959
}
6060

6161
func TestIsIndexAsync(t *testing.T) {
62-
6362
var (
6463
json string
6564
err error
@@ -252,3 +251,56 @@ func TestIndexPrefixLengthsFromParams(t *testing.T) {
252251
require.Nil(t, IndexPrefixLengthsFromParams("{bad json"))
253252
require.Nil(t, IndexPrefixLengthsFromParams(`{"prefix_lengths":"t:0"}`))
254253
}
254+
255+
func unresolvedName(name string) *tree.UnresolvedName {
256+
return tree.NewUnresolvedName(tree.NewCStr(name, 1))
257+
}
258+
259+
func TestIndexParamsToJsonString_RejectsPluginIvfFlatPath(t *testing.T) {
260+
idx := tree.NewIndex(
261+
false,
262+
[]*tree.KeyPart{tree.NewKeyPart(unresolvedName("embedding"), -1, tree.DefaultDirection, nil)},
263+
"idx1",
264+
tree.INDEX_TYPE_IVFFLAT,
265+
&tree.IndexOption{
266+
AlgoParamList: 10,
267+
AlgoParamVectorOpType: "vector_l2_ops",
268+
IncludeColumns: []*tree.UnresolvedName{
269+
unresolvedName("title"),
270+
unresolvedName("category"),
271+
},
272+
},
273+
)
274+
275+
params, err := IndexParamsToJsonString(idx)
276+
require.Error(t, err)
277+
require.Empty(t, params)
278+
require.Contains(t, err.Error(), "invalid index alogorithm type")
279+
}
280+
281+
func TestIndexParamsToStringList_DoesNotRenderIncludeColumns(t *testing.T) {
282+
paramList, err := IndexParamsToStringList(`{"lists":"10","op_type":"vector_l2_ops","included_columns":"title,category"}`)
283+
require.NoError(t, err)
284+
require.Contains(t, paramList, "lists = 10")
285+
require.Contains(t, paramList, "op_type 'vector_l2_ops'")
286+
require.NotContains(t, paramList, "INCLUDE")
287+
require.NotContains(t, paramList, "title")
288+
}
289+
290+
func TestParseIncludeColumnsValue_BackwardCompatible(t *testing.T) {
291+
encoded, err := MarshalIncludeColumnsValue([]string{"title, label", " rank "})
292+
require.NoError(t, err)
293+
require.Equal(t, `["title, label"," rank "]`, encoded)
294+
295+
cols, err := ParseIncludeColumnsValue(encoded)
296+
require.NoError(t, err)
297+
require.Equal(t, []string{"title, label", " rank "}, cols)
298+
299+
cols, err = ParseIncludeColumnsValue("title,category")
300+
require.NoError(t, err)
301+
require.Equal(t, []string{"title", "category"}, cols)
302+
303+
cols, err = ParseIncludeColumnsValue("[legacy,other")
304+
require.NoError(t, err)
305+
require.Equal(t, []string{"[legacy", "other"}, cols)
306+
}

pkg/catalog/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ const (
406406
SystemSI_IVFFLAT_TblCol_Entries_id = "__mo_index_centroid_fk_id"
407407
SystemSI_IVFFLAT_TblCol_Entries_pk = IndexTablePrimaryColName
408408
SystemSI_IVFFLAT_TblCol_Entries_entry = "__mo_index_centroid_fk_entry"
409+
SystemSI_IVFFLAT_IncludeColPrefix = "__mo_index_include_"
409410

410411
/************ 3. FULLTEXT Index **************/
411412

pkg/frontend/predefined.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ var (
457457
ordinal_position int unsigned not null,
458458
options text,
459459
index_table_name varchar(5000),
460+
included_columns text,
460461
primary key(id, column_name)
461462
)`, catalog.MO_CATALOG, catalog.MO_INDEXES)
462463

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2026 Matrix Origin
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package plan
16+
17+
import planpb "github.com/matrixorigin/matrixone/pkg/pb/plan"
18+
19+
// AlterColumnHooks is an optional plan-layer hook for algorithm-specific
20+
// metadata maintenance during ALTER TABLE column mutations.
21+
type AlterColumnHooks interface {
22+
HandleAlterDropColumn(tableDef *planpb.TableDef, indexDef *planpb.IndexDef, colName string) (bool, error)
23+
HandleAlterRenameColumn(tableDef *planpb.TableDef, oldColName, newColName string) ([]string, error)
24+
}
25+
26+
type RenameColumnRebuildHook interface {
27+
RenameColumnRequiresIndexRebuild(tableDef *planpb.TableDef, indexDef *planpb.IndexDef, oldColName string) (bool, error)
28+
}
29+
30+
type UpdateColumnRewriteHook interface {
31+
UpdateColumnRequiresIndexRewrite(tableDef *planpb.TableDef, indexDef *planpb.IndexDef, colName string) (bool, error)
32+
}
33+
34+
var (
35+
IncludedColumnAffected func(indexDef *planpb.IndexDef, colName string) (bool, error)
36+
RenameIncludedColumnsForAlgo func(tableDef *planpb.TableDef, algo, oldColName, newColName string) ([]string, error)
37+
)

pkg/iscp/index_sqlwriter.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type IvfflatSqlWriter struct {
8181
entries_tbl string
8282
meta_tbl string
8383
ivfparam vectorindex.IvfParam
84+
includeCols []string
8485
}
8586

8687
// Hnsw Sql Writer. Use the vectorindex.VectorIndeXCdc JSON format
@@ -589,17 +590,36 @@ func NewIvfflatSqlWriter(algo string, jobID JobID, info *ConsumerInfo, tabledef
589590
w.pkType = &types.Type{Oid: types.T(typ.Id), Width: typ.Width, Scale: typ.Scale}
590591

591592
nparts := len(w.indexdef[0].Parts)
592-
w.partsPos = make([]int32, nparts)
593-
w.partsType = make([]*types.Type, nparts)
593+
includeCols, err := ivfflatIncludeColumnsFromIndexDefs(w.indexdef)
594+
if err != nil {
595+
return nil, err
596+
}
597+
w.includeCols = append(w.includeCols[:0], includeCols...)
598+
w.partsPos = make([]int32, nparts+len(includeCols))
599+
w.partsType = make([]*types.Type, nparts+len(includeCols))
594600

595601
for i, part := range w.indexdef[0].Parts {
596602
w.partsPos[i] = tabledef.Name2ColIndex[part]
597603
typ = tabledef.Cols[w.partsPos[i]].Typ
598604
w.partsType[i] = &types.Type{Oid: types.T(typ.Id), Width: typ.Width, Scale: typ.Scale}
599605
}
606+
for i, includeCol := range includeCols {
607+
pos, ok := tabledef.Name2ColIndex[includeCol]
608+
if !ok {
609+
resolvedCol := catalog.ResolveAlias(includeCol)
610+
pos, ok = tabledef.Name2ColIndex[resolvedCol]
611+
}
612+
if !ok {
613+
return nil, moerr.NewInternalErrorNoCtxf("ivfflat include column %q not found in source table", includeCol)
614+
}
615+
dst := nparts + i
616+
w.partsPos[dst] = pos
617+
typ = tabledef.Cols[pos].Typ
618+
w.partsType[dst] = &types.Type{Oid: types.T(typ.Id), Width: typ.Width, Scale: typ.Scale}
619+
}
600620

601-
w.srcPos = make([]int32, nparts+1)
602-
w.srcType = make([]*types.Type, nparts+1)
621+
w.srcPos = make([]int32, len(w.partsPos)+1)
622+
w.srcType = make([]*types.Type, len(w.partsType)+1)
603623

604624
w.srcPos[0] = w.pkPos
605625
w.srcType[0] = w.pkType
@@ -615,6 +635,36 @@ func NewIvfflatSqlWriter(algo string, jobID JobID, info *ConsumerInfo, tabledef
615635
return w, nil
616636
}
617637

638+
func ivfflatIncludeColumnsFromIndexDefs(indexdefs []*plan.IndexDef) ([]string, error) {
639+
for _, idx := range indexdefs {
640+
if idx != nil && len(idx.IncludedColumns) > 0 {
641+
return idx.IncludedColumns, nil
642+
}
643+
}
644+
for _, idx := range indexdefs {
645+
if idx == nil || idx.IndexAlgoParams == "" {
646+
continue
647+
}
648+
params, err := catalog.IndexParamsStringToMap(idx.IndexAlgoParams)
649+
if err != nil {
650+
return nil, err
651+
}
652+
raw := params[catalog.IncludedColumns]
653+
if raw == "" {
654+
raw = params["include_columns"]
655+
}
656+
if raw == "" {
657+
continue
658+
}
659+
cols, err := catalog.ParseIncludeColumnsValue(raw)
660+
if err != nil {
661+
return nil, err
662+
}
663+
return cols, nil
664+
}
665+
return nil, nil
666+
}
667+
618668
// REPLACE INTO __mo_index_secondary_0197786c-285f-70cb-9337-e484a3ff92c4(__mo_index_centroid_fk_version, __mo_index_centroid_fk_id, __mo_index_pri_col, __mo_index_centroid_fk_entry)
619669
// with centroid as (select * from __mo_index_secondary_0197786c-285f-70bb-b277-2cef56da590a where __mo_index_centroid_version = 0),
620670
// src as (select column_0 as id, cast(column_1 as vecf32(3)) as embed from (values row(2005,'[0.4532634, 0.7297859, 0.48885703]'), row(2009, '[0.68150306, 0.6950923, 0.16590895] ')))
@@ -708,11 +758,20 @@ func (w *IvfflatSqlWriter) toIvfflatUpsert(upsert bool) ([]byte, error) {
708758
sql += fmt.Sprintf("REPLACE INTO %s ", sqlquote.QualifiedIdent(w.info.DBName, w.entries_tbl))
709759
}
710760

711-
sql += fmt.Sprintf("(`%s`, `%s`, `%s`, `%s`) ",
761+
targetCols := []string{
712762
catalog.SystemSI_IVFFLAT_TblCol_Entries_version,
713763
catalog.SystemSI_IVFFLAT_TblCol_Entries_id,
714764
catalog.SystemSI_IVFFLAT_TblCol_Entries_pk,
715-
catalog.SystemSI_IVFFLAT_TblCol_Entries_entry)
765+
catalog.SystemSI_IVFFLAT_TblCol_Entries_entry,
766+
}
767+
for _, includeCol := range w.includeCols {
768+
targetCols = append(targetCols, catalog.SystemSI_IVFFLAT_IncludeColPrefix+includeCol)
769+
}
770+
quotedTargetCols := make([]string, 0, len(targetCols))
771+
for _, col := range targetCols {
772+
quotedTargetCols = append(quotedTargetCols, sqlquote.Ident(col))
773+
}
774+
sql += fmt.Sprintf("(%s) ", strings.Join(quotedTargetCols, ", "))
716775

717776
versql := fmt.Sprintf("SELECT CAST(%s as BIGINT) FROM %s WHERE `%s` = 'version'", catalog.SystemSI_IVFFLAT_TblCol_Metadata_val,
718777
sqlquote.QualifiedIdent(w.info.DBName, w.meta_tbl), catalog.SystemSI_IVFFLAT_TblCol_Metadata_key)

0 commit comments

Comments
 (0)