{"record":{"id":"c6f528c85e97bd89","repo":"vitessio/vitess","slug":"unsupported-multi-table-usage","errorCode":null,"errorMessage":"unsupported multi-table usage","messagePattern":"unsupported multi-table usage","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/vttablet/tabletmanager/vreplication/table_plan_builder.go","lineNumber":405,"sourceCode":"}\n\nfunc analyzeSelectFrom(query string, parser *sqlparser.Parser) (sel *sqlparser.Select, from string, err error) {\n\tstatement, err := parser.Parse(query)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tsel, ok := statement.(*sqlparser.Select)\n\tif !ok {\n\t\treturn nil, \"\", errors.New(\"unsupported non-select statement\")\n\t}\n\tif sel.Distinct {\n\t\treturn nil, \"\", errors.New(\"unsupported distinct clause\")\n\t}\n\tif len(sel.From) == 0 {\n\t\treturn nil, \"\", errors.New(\"unsupported select from dual\")\n\t}\n\tif len(sel.From) > 1 {\n\t\treturn nil, \"\", errors.New(\"unsupported multi-table usage\")\n\t}\n\tnode, ok := sel.From[0].(*sqlparser.AliasedTableExpr)\n\tif !ok {\n\t\treturn nil, \"\", fmt.Errorf(\"unsupported from expression (%T)\", sel.From[0])\n\t}\n\tfromTable := sqlparser.GetTableName(node.Expr)\n\tif fromTable.IsEmpty() {\n\t\treturn nil, \"\", fmt.Errorf(\"unsupported from source (%T)\", node.Expr)\n\t}\n\treturn sel, fromTable.String(), nil\n}\n\nfunc (tpb *tablePlanBuilder) analyzeExprs(selExprs []sqlparser.SelectExpr) error {\n\tfor _, selExpr := range selExprs {\n\t\tcexpr, err := tpb.analyzeExpr(selExpr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}","sourceCodeStart":387,"sourceCodeEnd":423,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/vttablet/tabletmanager/vreplication/table_plan_builder.go#L387-L423","documentation":"In vreplication's table plan builder, analyzeSelectFrom only supports copying from exactly one table per SELECT. This error is thrown when the source query's FROM clause contains more than one table expression, meaning a join or comma-separated multi-table select was used where a single-table select is required.","triggerScenarios":"analyzeSelectFrom (called by buildTablePlan) encounters sel.From with length > 1, i.e. a vreplication filter/rule was compiled against a query with multiple tables in FROM (joins or comma lists).","commonSituations":"A MoveTables/VReplication filter rule uses a select with a JOIN to filter or transform rows; hand-written filter rules that reference multiple tables; misconfigured keyrange/table filters.","solutions":["Rewrite the filter/rule SELECT to reference exactly one table in FROM; use WHERE clauses instead of JOINs to filter rows","If cross-table transformation is needed, replicate the base table and derive the target via a view or post-copy transformation","Use a table-level filter (one table per rule) instead of a join-based select"],"exampleFix":"// before\nfilter: { rules: [{ match: 't', filter: 'select * from t join u on t.id=u.id' }] }\n// after\nfilter: { rules: [{ match: 't', filter: 'select t.* from t where exists (select 1 from u where u.id=t.id)' }] }","handlingStrategy":"validation","validationCode":"// Before configuring a vreplication filter rule, ensure the select references one table:\nstmt, err := sqlparser.Parse(filterSQL)\nif err != nil { return err }\nsel, ok := stmt.(*sqlparser.Select)\nif !ok { return fmt.Errorf(\"expected SELECT\") }\nif len(sel.From) > 1 { return fmt.Errorf(\"filter must reference exactly one table, got %d\", len(sel.From)) }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["One table per filter rule — never JOIN in filter selects","Use WHERE/subquery filtering instead of joins","Test filter rules against a scratch workflow before production MoveTables"],"tags":["vreplication","unsupported-query","sql-plan"],"backgroundTag":"unsupported-sql-feature","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}