diff --git a/docs/rfcs/20260903_information_schema_views_definition_protocol.md b/docs/rfcs/20260903_information_schema_views_definition_protocol.md new file mode 100644 index 0000000000000..57a6710c4027c --- /dev/null +++ b/docs/rfcs/20260903_information_schema_views_definition_protocol.md @@ -0,0 +1,82 @@ +- Status: proposed — implementation complete; pending independent approval +- Start Date: 2026-09-03 +- Authors: MatrixOne maintainers +- Implementation PR: https://github.com/matrixorigin/matrixone/pull/27716 +- Issue for this RFC: https://github.com/matrixorigin/matrixone/issues/27655 + +# Parser-derived `information_schema.VIEWS` definitions + +## Summary + +`information_schema.VIEWS.VIEW_DEFINITION` must expose the defining SELECT, +not the original CREATE statement. New views persist a parser-derived definition +and legacy rows are read through parser-aware metadata functions. The functions +are new distributed plan functions (IDs 579 and 580), so the catalog contract is fenced by MORPC +v59. + +## Problem and invariant + +Schema-diff and migration clients replay `VIEW_DEFINITION`. A full CREATE +statement is not a standalone SELECT and falsely marks aggregate views as +updatable. The invariant is that every visible current or legacy view returns +its parser-derived frozen SELECT (or NULL only for a malformed catalog row), +and no CN that cannot resolve either function ID can receive a pipeline or catalog view +that references it. + +## Design + +The CREATE/ALTER owner derives `ViewData.Definition` from the stabilized view +AST, after wildcard expansion and separately persists `CheckOption`. The +catalog remains the single owner of that frozen metadata. +`mo_view_definition(viewdef)` and `mo_view_check_option(viewdef)` return the +stored fields without writes; for an older row that lacks them, they parse only +the stored statement using its persisted SQL mode and identifier-case settings. +This bounded, side-effect-free fallback avoids a second SQL regexp lexer and +does not depend on background recovery. + +MORPC v59 is allocated as `MORPCLatestVersion + 1` from official main v58, +which is already assigned to binary-string function semantics and runtime-domain metadata. It is specific to +this function and the persisted VIEWS definition. The v4.0.6 VIEWS upgrade +waits for common v59. New tenant initialization at v58 or below installs the +predecessor VIEWS DDL, which has no function reference; v59 installs the new +DDL. Pipeline preparation, remote +marshal, and remote unmarshal reject a pipeline containing either function ID +below v59. The receiver check protects stale prepared work as well as normal +sender dispatch. Before admitting any v58-or-earlier CN during rollback, +operators must restore `InformationSchemaViewsLegacyDDL` and wait for that +catalog change to converge; merely draining v59-dependent requests is not +sufficient because the new persisted view text references the function. The +new JSON fields are additive and old binaries keep treating them as unknown. + +## Alternatives + +Keeping raw SQL regexp extraction was rejected because it repeatedly diverged +from the SQL lexer for comments and quoted strings. Eagerly rewriting every +legacy row was rejected because the existing recovery lifecycle is deliberately +inactive and a metadata read must not perform unbounded catalog writes. Allowing +the DDL before v59 was rejected because an old CN cannot bind the metadata functions. + +## Bounds, security, and operations + +The compatibility parse is per visible legacy row and is linear in that row's +stored statement; current rows return their stored definition directly. It +creates no durable work, goroutine, queue, retry, or cache. Existing visibility +joins remain the authorization boundary, so parsing happens only after the view +row is selected. A mixed-version request fails before dispatch with a stable +NotSupported error rather than returning wrong metadata. + +## Validation + +Focused parser/function tests cover current and legacy definitions, quoted and +commented inputs, malformed rows, frozen wildcard expansion, and CHECK OPTION. +Protocol tests cover the v58 predecessor rejection and v59 acceptance at +prepare, sender, and receiver boundaries. System-view tests prove v58 tenant +initialization uses the predecessor DDL and v59 uses the parser-derived DDL; +upgrade tests prove the VIEWS entry requires v59. The predecessor-init test is +also the rollback guard: it proves that the restoration target has no function +reference before an older CN is admitted. + +## Unresolved questions + +None. This RFC is proposed pending independent design approval; it documents +the delivery contract and does not self-approve the design. diff --git a/pkg/bootstrap/versions/v4_0_6/tenant_upgrade_list.go b/pkg/bootstrap/versions/v4_0_6/tenant_upgrade_list.go index ff08cb0882a04..b78eab9f414a3 100644 --- a/pkg/bootstrap/versions/v4_0_6/tenant_upgrade_list.go +++ b/pkg/bootstrap/versions/v4_0_6/tenant_upgrade_list.go @@ -86,6 +86,10 @@ func upgradeInformationSchemaMetadataVisibilityView(viewName, viewDDL string) ve requiredProtocol := defines.MORPCVersion41 if viewName == "TABLES" || viewName == "COLUMNS" { requiredProtocol = defines.MORPCVersion46 + } else if viewName == "VIEWS" { + // The definition functions are encoded into remotely executed plans. Do not + // install this catalog contract until every CN can resolve function IDs 579 and 580. + requiredProtocol = defines.MORPCVersion59 } return versions.UpgradeEntry{ Schema: sysview.InformationDBConst, diff --git a/pkg/bootstrap/versions/v4_0_6/upgrade_test.go b/pkg/bootstrap/versions/v4_0_6/upgrade_test.go index 068c6944bf109..3bf608bc55b8c 100644 --- a/pkg/bootstrap/versions/v4_0_6/upgrade_test.go +++ b/pkg/bootstrap/versions/v4_0_6/upgrade_test.go @@ -45,7 +45,7 @@ func TestColumnsUpgradeProtocolGenerations(t *testing.T) { upgradeInformationSchemaColumnsBinaryStrings(), refreshInformationSchemaCharacterSetsUTF8Maxlen(), } { - for _, peer := range []int64{defines.MORPCVersion46, defines.MORPCVersion57, defines.MORPCVersion58} { + for _, peer := range []int64{defines.MORPCVersion46, defines.MORPCVersion57, defines.MORPCVersion58, defines.MORPCVersion59} { t.Run(fmt.Sprintf("%s-gate-%d-peer-%d", entry.TableName, entry.RequiredProtocolVersion, peer), func(t *testing.T) { mp := mpool.MustNewZero() defer mpool.DeleteMPool(mp) @@ -65,7 +65,8 @@ func TestColumnsUpgradeProtocolGenerations(t *testing.T) { entry.CheckFunc = func(executor.TxnExecutor, uint32) (bool, error) { return false, nil } err := entry.Upgrade(txn, 0) if peer < entry.RequiredProtocolVersion { - require.ErrorContains(t, err, "requires all CNs to support protocol version 58") + require.ErrorContains(t, err, fmt.Sprintf( + "requires all CNs to support protocol version %d", entry.RequiredProtocolVersion)) require.Empty(t, executed, "an old peer must block before DROP/DELETE or DDL") } else { require.NoError(t, err) @@ -216,7 +217,10 @@ func TestUpgradeEntries(t *testing.T) { "drop view if exists information_schema.statistics") for _, entry := range tenantUpgEntries { ddl := entry.UpgSql + entry.PostSql - if strings.Contains(ddl, "mo_subscription_tables()") || + if entry.TableName == "VIEWS" { + require.Equal(t, int64(defines.MORPCVersion59), entry.RequiredProtocolVersion, + "view upgrade %s must wait for mo_view_definition", entry.TableName) + } else if strings.Contains(ddl, "mo_subscription_tables()") || strings.Contains(ddl, "mo_subscription_columns()") { require.GreaterOrEqual(t, entry.RequiredProtocolVersion, int64(defines.MORPCVersion46), "view upgrade %s must wait for subscription metadata functions", entry.TableName) @@ -257,6 +261,8 @@ func TestUpgradeEntries(t *testing.T) { expectedProtocol := int64(defines.MORPCVersion41) if view.name == "TABLES" || view.name == "COLUMNS" { expectedProtocol = defines.MORPCVersion46 + } else if view.name == "VIEWS" { + expectedProtocol = defines.MORPCVersion59 } require.Equal(t, expectedProtocol, entry.RequiredProtocolVersion) require.Contains(t, strings.ToLower(entry.PreSql), @@ -471,6 +477,24 @@ func TestDaemonClaimPrecisionCheckUsesStoredType(t *testing.T) { } } +func TestInformationSchemaViewsUpgradeUsesLegacyDefinitionCompatibility(t *testing.T) { + // A pre-upgrade viewdef has Stmt but no parser-derived definition. The + // MODIFY_VIEW entry must keep the public metadata contract available through + // the parser-aware compatibility function instead of returning NULL while the + // separate lifecycle recovery remains inactive. + var viewsEntry *versions.UpgradeEntry + for i := range tenantUpgEntries { + if tenantUpgEntries[i].TableName == "VIEWS" { + viewsEntry = &tenantUpgEntries[i] + break + } + } + require.NotNil(t, viewsEntry) + require.Equal(t, versions.MODIFY_VIEW, viewsEntry.UpgType) + require.Contains(t, viewsEntry.UpgSql, "mo_view_definition(tbl.viewdef)") + require.NotContains(t, viewsEntry.UpgSql, "json_extract_string(tbl.viewdef, '$.definition')") +} + func TestInformationSchemaMetadataVisibilityUpgradeChecks(t *testing.T) { views := []struct { name string diff --git a/pkg/defines/const.go b/pkg/defines/const.go index 0fc77436f2321..b1ba7f469947e 100644 --- a/pkg/defines/const.go +++ b/pkg/defines/const.go @@ -94,7 +94,8 @@ const ( MORPCVersion56 int64 = 56 // session-scoped AUTO_INCREMENT increment/offset and provenance MORPCVersion57 int64 = 57 // Arrow LOAD external-scan pipeline payload MORPCVersion58 int64 = 58 // binary-string function semantics and runtime-domain metadata - MORPCLatestVersion = MORPCVersion58 + MORPCVersion59 int64 = 59 // parser-derived information_schema.VIEWS definition function + MORPCLatestVersion = MORPCVersion59 ) // DefaultLockWaitTimeoutSeconds is shared by the frontend default and by diff --git a/pkg/queryservice/client/query_client_test.go b/pkg/queryservice/client/query_client_test.go index 45d1cf4c530cf..9c5d74e52a272 100644 --- a/pkg/queryservice/client/query_client_test.go +++ b/pkg/queryservice/client/query_client_test.go @@ -35,7 +35,7 @@ func TestMongoDBClientRetireRequiresProtocolVersion5(t *testing.T) { } func TestRefreshSessionAuthRequiresCurrentProtocolVersion(t *testing.T) { - assert.Equal(t, defines.MORPCVersion58, defines.MORPCLatestVersion) + assert.Equal(t, defines.MORPCVersion59, defines.MORPCLatestVersion) assert.Equal(t, defines.MORPCVersion54, methodVersions[query.CmdMethod_RefreshSessionAuth]) assert.GreaterOrEqual(t, defines.MORPCLatestVersion, methodVersions[query.CmdMethod_RefreshSessionAuth]) } diff --git a/pkg/sql/compile/compile.go b/pkg/sql/compile/compile.go index 0212fbb3f2d2d..2677e840ed49e 100644 --- a/pkg/sql/compile/compile.go +++ b/pkg/sql/compile/compile.go @@ -7882,6 +7882,19 @@ func supportsRemotePadSpaceSemantics(service string) bool { return ok && protocolVersion >= defines.MORPCVersion40 } +func supportsRemoteViewDefinitionFunction(service string) bool { + rt := moruntime.ServiceRuntime(service) + if rt == nil { + return false + } + version, ok := rt.GetGlobalVariables(moruntime.MOProtocolVersion) + if !ok { + return false + } + protocolVersion, ok := version.(int64) + return ok && protocolVersion >= defines.MORPCVersion59 +} + func supportsRemoteParquetWholeFileFanout(service string) bool { rt := moruntime.ServiceRuntime(service) if rt == nil { diff --git a/pkg/sql/compile/remote_expr.go b/pkg/sql/compile/remote_expr.go index e543da8a5f98e..add9aa4ac86b4 100644 --- a/pkg/sql/compile/remote_expr.go +++ b/pkg/sql/compile/remote_expr.go @@ -548,6 +548,12 @@ func pipelineContainsFunction(p *pipeline.Pipeline, predicate remoteFunctionPred return containsFunctionInValue(reflect.ValueOf(p), nil, predicate) } +func pipelineContainsFunctionID(p *pipeline.Pipeline, functionID int32) bool { + return pipelineContainsFunction(p, func(id, _ int32) bool { + return id == functionID + }) +} + func containsFunctionInExpr( expr *plan.Expr, seen map[uintptr]struct{}, diff --git a/pkg/sql/compile/remote_expr_test.go b/pkg/sql/compile/remote_expr_test.go index e5548577fb742..fd44bd98a9bc1 100644 --- a/pkg/sql/compile/remote_expr_test.go +++ b/pkg/sql/compile/remote_expr_test.go @@ -767,6 +767,99 @@ func TestPadSpaceRemoteProtocolValidationV40FastPathIsAllocationFree(t *testing. require.Equal(t, float64(0), allocs) } +func TestViewDefinitionRemoteProtocolValidationAtPrepareSendAndReceiveBoundaries(t *testing.T) { + proc := testutil.NewProcess(t) + rt := runtime.ServiceRuntime(proc.GetService()) + previous, hadPrevious := rt.GetGlobalVariables(runtime.MOProtocolVersion) + t.Cleanup(func() { + if hadPrevious { + rt.SetGlobalVariables(runtime.MOProtocolVersion, previous) + } else { + rt.CompareAndDeleteGlobalVariables(runtime.MOProtocolVersion, defines.MORPCVersion59) + } + }) + + viewDefinitionType := types.T_text.ToType() + viewDefinition := &plan.Expr{ + Typ: plan2.MakePlan2Type(&viewDefinitionType), + Expr: &plan.Expr_F{F: &plan.Function{ + Func: &plan.ObjectRef{ + Obj: function.EncodeOverloadID(function.MO_VIEW_DEFINITION, 0), + ObjName: "mo_view_definition", + }, + Args: []*plan.Expr{plan2.MakePlan2StringConstExprWithType("{}", false)}, + }}, + } + pipelineWithFunction := &pipeline.Pipeline{InstructionList: []*pipeline.Instruction{{ + Op: int32(vm.Projection), + ProjectList: []*plan.Expr{viewDefinition}, + }}} + viewCheckOptionType := types.T_varchar.ToType() + viewCheckOption := &plan.Expr{ + Typ: plan2.MakePlan2Type(&viewCheckOptionType), + Expr: &plan.Expr_F{F: &plan.Function{ + Func: &plan.ObjectRef{ + Obj: function.EncodeOverloadID(function.MO_VIEW_CHECK_OPTION, 0), + ObjName: "mo_view_check_option", + }, + Args: []*plan.Expr{plan2.MakePlan2StringConstExprWithType("{}", false)}, + }}, + } + pipelineWithCheckOption := &pipeline.Pipeline{InstructionList: []*pipeline.Instruction{{ + Op: int32(vm.Projection), + ProjectList: []*plan.Expr{viewCheckOption}, + }}} + + rt.SetGlobalVariables(runtime.MOProtocolVersion, defines.MORPCVersion59) + require.NoError(t, validateRemoteViewDefinitionPipelineProtocol(proc, pipelineWithFunction)) + require.NoError(t, validateRemoteViewDefinitionPipelineProtocol(proc, pipelineWithCheckOption)) + + prepared := newScope(Remote) + prepared.Proc = proc + projection := projection.NewArgument() + projection.ProjectList = []*plan.Expr{viewDefinition} + prepared.setRootOperator(projection) + data, err := encodeRemoteScope(prepared, proc) + require.NoError(t, err) + _, err = encodeScope(prepared) + require.NoError(t, err) + + // v58 is the immediate predecessor after the main-branch rebase; it + // supports the binary-string protocol addition but not these new function IDs. + rt.SetGlobalVariables(runtime.MOProtocolVersion, defines.MORPCVersion58) + require.NoError(t, validateRemoteViewDefinitionPipelineProtocol(proc, &pipeline.Pipeline{})) + require.ErrorContains(t, validateRemoteViewDefinitionPipelineProtocol(proc, pipelineWithFunction), + "requires MORPC protocol version 59") + require.ErrorContains(t, validateRemoteViewDefinitionPipelineProtocol(proc, pipelineWithCheckOption), + "requires MORPC protocol version 59") + _, err = encodeRemoteScope(prepared, proc) + require.ErrorContains(t, err, "requires MORPC protocol version 59") + _, err = encodeScope(prepared) + require.ErrorContains(t, err, "requires MORPC protocol version 59") + _, err = decodeScope(data, proc, true, nil) + require.ErrorContains(t, err, "requires MORPC protocol version 59") +} + +func TestViewDefinitionRemoteProtocolValidationV59FastPathIsAllocationFree(t *testing.T) { + proc := testutil.NewProcess(t) + rt := runtime.ServiceRuntime(proc.GetService()) + defer rt.SetGlobalVariables(runtime.MOProtocolVersion, defines.MORPCLatestVersion) + rt.SetGlobalVariables(runtime.MOProtocolVersion, defines.MORPCVersion59) + + // A large ordinary pipeline makes an accidental reflective traversal visible. + ordinary := &pipeline.Pipeline{InstructionList: make([]*pipeline.Instruction, 1_000)} + for i := range ordinary.InstructionList { + ordinary.InstructionList[i] = &pipeline.Instruction{Op: int32(vm.Projection)} + } + require.NoError(t, validateRemoteViewDefinitionPipelineProtocol(proc, ordinary)) + allocs := testing.AllocsPerRun(100, func() { + if err := validateRemoteViewDefinitionPipelineProtocol(proc, ordinary); err != nil { + panic(err) + } + }) + require.Equal(t, float64(0), allocs) +} + func TestScopeContainsVarExprInAggArguments(t *testing.T) { scope := newScope(Normal) op := group.NewArgument() diff --git a/pkg/sql/compile/remoterun.go b/pkg/sql/compile/remoterun.go index 3b7ef685fcfe7..fd65570b11801 100644 --- a/pkg/sql/compile/remoterun.go +++ b/pkg/sql/compile/remoterun.go @@ -99,6 +99,9 @@ func encodeScope(s *Scope) ([]byte, error) { if err = validateRemoteBinaryStringPipelineProtocol(s.Proc, p); err != nil { return nil, err } + if err = validateRemoteViewDefinitionPipelineProtocol(s.Proc, p); err != nil { + return nil, err + } return p.Marshal() } @@ -119,6 +122,9 @@ func encodeRemoteScope(s *Scope, proc *process.Process) ([]byte, error) { if err = validateRemoteMongoUserQueryPipelineProtocol(proc, p); err != nil { return nil, err } + if err = validateRemoteViewDefinitionPipelineProtocol(proc, p); err != nil { + return nil, err + } if err = validateRemoteParquetWholeFileFanoutPipelineProtocol(proc, p); err != nil { return nil, err } @@ -225,6 +231,9 @@ func decodeScope(data []byte, proc *process.Process, isRemote bool, eng engine.E if err = validateRemotePadSpacePipelineProtocol(proc, p); err != nil { return nil, err } + if err = validateRemoteViewDefinitionPipelineProtocol(proc, p); err != nil { + return nil, err + } if err = validateRemoteParquetWholeFileFanoutPipelineProtocol(proc, p); err != nil { return nil, err } @@ -2471,6 +2480,31 @@ func validateRemoteArrowLoadPipelineProtocol(proc *process.Process, p *pipeline. return nil } +// validateRemoteViewDefinitionPipelineProtocol protects the function IDs that +// occur in the persisted VIEWS definition. It is used at both marshal and +// unmarshal boundaries, so a stale prepared or remote pipeline fails closed +// instead of being bound by a CN that predates the function registration. +func validateRemoteViewDefinitionPipelineProtocol( + proc *process.Process, + p *pipeline.Pipeline, +) error { + // A current peer cannot reject this function. Avoid a reflective traversal + // of every ordinary remote pipeline once the negotiated capability is known. + if proc != nil && supportsRemoteViewDefinitionFunction(proc.GetService()) { + return nil + } + if p == nil || (!pipelineContainsFunctionID(p, function.MO_VIEW_DEFINITION) && + !pipelineContainsFunctionID(p, function.MO_VIEW_CHECK_OPTION)) { + return nil + } + if proc == nil || !supportsRemoteViewDefinitionFunction(proc.GetService()) { + return moerr.NewNotSupportedNoCtx( + "view metadata remote execution requires MORPC protocol version 59", + ) + } + return nil +} + func aggregateUsesCollationAwareTextMinMax(agg aggexec.AggFuncExecExpression) bool { if agg.GetAggID() != aggexec.AggIdOfMin && agg.GetAggID() != aggexec.AggIdOfMax { return false diff --git a/pkg/sql/parsers/dialect/mysql/mysql_sql.go b/pkg/sql/parsers/dialect/mysql/mysql_sql.go index 794ed31bf2fd6..53a373337fe93 100644 --- a/pkg/sql/parsers/dialect/mysql/mysql_sql.go +++ b/pkg/sql/parsers/dialect/mysql/mysql_sql.go @@ -24578,6 +24578,7 @@ yydefault: var ColNames = yyDollar[6].identifierListUnion() var AsSource = yyDollar[8].selectUnion() var IfNotExists = yyDollar[4].ifNotExistsUnion() + var CheckOption = yyDollar[9].str if intoErr := tree.ValidateSelectIntoNotAllowed(AsSource); intoErr != "" { yylex.Error(intoErr) goto ret1 @@ -24587,7 +24588,7 @@ yydefault: Name, ColNames, AsSource, - IfNotExists, + IfNotExists, CheckOption, ) } yyVAL.union = yyLOCAL @@ -24601,6 +24602,7 @@ yydefault: var ColNames = yyDollar[6].identifierListUnion() var AsSource = yyDollar[8].selectUnion() var IfNotExists = yyDollar[4].ifNotExistsUnion() + var CheckOption = yyDollar[9].str if intoErr := tree.ValidateSelectIntoNotAllowed(AsSource); intoErr != "" { yylex.Error(intoErr) goto ret1 @@ -24610,7 +24612,7 @@ yydefault: Name, ColNames, AsSource, - IfNotExists, + IfNotExists, CheckOption, ) } yyVAL.union = yyLOCAL @@ -24688,13 +24690,16 @@ yydefault: yyDollar = yyS[yypt-0 : yypt+1] //line mysql_sql.y:8691 { - yyVAL.str = "" + yyVAL.str = "NONE" } case 1283: yyDollar = yyS[yypt-4 : yypt+1] //line mysql_sql.y:8695 { - yyVAL.str = "WITH " + yyDollar[2].str + " CHECK OPTION" + yyVAL.str = yyDollar[2].str + if yyVAL.str == "" { + yyVAL.str = "CASCADED" + } } case 1289: yyDollar = yyS[yypt-0 : yypt+1] diff --git a/pkg/sql/parsers/dialect/mysql/mysql_sql.y b/pkg/sql/parsers/dialect/mysql/mysql_sql.y index bc478f364ef60..ea9e9411d73b9 100644 --- a/pkg/sql/parsers/dialect/mysql/mysql_sql.y +++ b/pkg/sql/parsers/dialect/mysql/mysql_sql.y @@ -8596,7 +8596,7 @@ create_view_stmt: var Name = $5 var ColNames = $6 var AsSource = $8 - var IfNotExists = $4 + var IfNotExists = $4; var CheckOption = $9 if intoErr := tree.ValidateSelectIntoNotAllowed(AsSource); intoErr != "" { yylex.Error(intoErr) goto ret1 @@ -8606,7 +8606,7 @@ create_view_stmt: Name, ColNames, AsSource, - IfNotExists, + IfNotExists, CheckOption, ) } | CREATE replace_opt VIEW not_exists_opt table_name column_list_opt AS ctas_select_stmt view_tail @@ -8615,7 +8615,7 @@ create_view_stmt: var Name = $5 var ColNames = $6 var AsSource = $8 - var IfNotExists = $4 + var IfNotExists = $4; var CheckOption = $9 if intoErr := tree.ValidateSelectIntoNotAllowed(AsSource); intoErr != "" { yylex.Error(intoErr) goto ret1 @@ -8625,7 +8625,7 @@ create_view_stmt: Name, ColNames, AsSource, - IfNotExists, + IfNotExists, CheckOption, ) } @@ -8689,11 +8689,11 @@ view_opt: view_tail: { - $$ = "" + $$ = "NONE" } | WITH check_type CHECK OPTION { - $$ = "WITH " + $2 + " CHECK OPTION" + $$ = $2; if $$ == "" { $$ = "CASCADED" } } algorithm_type_2: diff --git a/pkg/sql/parsers/dialect/mysql/mysql_sql_test.go b/pkg/sql/parsers/dialect/mysql/mysql_sql_test.go index 91383e893a1ae..9dc262394ec97 100644 --- a/pkg/sql/parsers/dialect/mysql/mysql_sql_test.go +++ b/pkg/sql/parsers/dialect/mysql/mysql_sql_test.go @@ -4781,10 +4781,10 @@ var ( output: "create view t2 as select * from t1", }, { input: "create VIEW t2 as select * from t1 WITH CASCADED CHECK OPTION", - output: "create view t2 as select * from t1", + output: "create view t2 as select * from t1 with CASCADED check option", }, { input: "create VIEW t2 as select * from t1 WITH LOCAL CHECK OPTION", - output: "create view t2 as select * from t1", + output: "create view t2 as select * from t1 with LOCAL check option", }, { input: "insert into t1 values(_binary 0x123)", output: "insert into t1 values (0x123)", diff --git a/pkg/sql/parsers/tree/view.go b/pkg/sql/parsers/tree/view.go index 016d9e9c11d2a..7939b43da2bcd 100644 --- a/pkg/sql/parsers/tree/view.go +++ b/pkg/sql/parsers/tree/view.go @@ -31,15 +31,17 @@ type CreateView struct { ColNames IdentifierList AsSource *Select IfNotExists bool + CheckOption string } -func NewCreateView(replace bool, name *TableName, colNames IdentifierList, asSource *Select, ifNotExists bool) *CreateView { +func NewCreateView(replace bool, name *TableName, colNames IdentifierList, asSource *Select, ifNotExists bool, checkOption string) *CreateView { c := reuse.Alloc[CreateView](nil) c.Replace = replace c.Name = name c.ColNames = colNames c.AsSource = asSource c.IfNotExists = ifNotExists + c.CheckOption = checkOption return c } @@ -68,6 +70,11 @@ func (node *CreateView) Format(ctx *FmtCtx) { } ctx.WriteString(" as ") node.AsSource.Format(ctx) + if node.CheckOption != "" && node.CheckOption != "NONE" { + ctx.WriteString(" with ") + ctx.WriteString(node.CheckOption) + ctx.WriteString(" check option") + } } func (node *CreateView) reset() { diff --git a/pkg/sql/plan/build_ddl.go b/pkg/sql/plan/build_ddl.go index f3592126635c1..4e30b7ac53808 100644 --- a/pkg/sql/plan/build_ddl.go +++ b/pkg/sql/plan/build_ddl.go @@ -228,6 +228,7 @@ func genViewTableDef( colNames tree.IdentifierList, viewDatabase string, viewName string, + checkOption string, ) (*plan.TableDef, error) { var tableDef plan.TableDef dependencyCapture := newViewDependencyCaptureContext(ctx) @@ -337,14 +338,21 @@ func genViewTableDef( } } persistedCreateSQL := rootSQL - if stableViewSQL, rewritten := stableViewSQLWithExpandedStars(ctx, stmt, viewSql, expandedSelectLists); rewritten { + definitionStmt := stmt + if stableViewSQL, stableSelect, rewritten := stableViewSQLWithExpandedStarsAndSelect(ctx, stmt, viewSql, expandedSelectLists); rewritten { viewSql = stableViewSQL persistedCreateSQL = stableViewSQL + definitionStmt = stableSelect } lowerCaseTableNames := ctx.GetLowerCaseTableNames() viewData, err := json.Marshal(ViewData{ - Stmt: viewSql, + Stmt: viewSql, + // Definition must be generated from the same star-expanded SELECT that is + // persisted in Stmt. Formatting the original AST would let metadata replay + // a later schema's columns even though the View itself remains frozen. + Definition: tree.StringWithOpts(definitionStmt, dialect.MYSQL, tree.WithQuoteString(true), tree.WithQuoteIdentifier(), tree.WithModeIndependentStringLiterals()), + CheckOption: strings.ToUpper(checkOption), DefaultDatabase: ctx.DefaultDatabase(), SQLMode: parserSQLModeFromContext(ctx), SecurityType: getViewSecurityTypeFromContext(ctx), @@ -384,16 +392,26 @@ func stableViewSQLWithExpandedStars( viewSql string, expandedSelectLists map[*tree.SelectClause]tree.SelectExprs, ) (string, bool) { + stableSQL, _, rewritten := stableViewSQLWithExpandedStarsAndSelect(ctx, stmt, viewSql, expandedSelectLists) + return stableSQL, rewritten +} + +func stableViewSQLWithExpandedStarsAndSelect( + ctx CompilerContext, + stmt *tree.Select, + viewSql string, + expandedSelectLists map[*tree.SelectClause]tree.SelectExprs, +) (string, *tree.Select, bool) { // SAMPLE(*) expands to a sampling operator during binding. The rewriter // leaves that query block intact while still stabilizing ordinary stars in // unrelated query blocks. if viewSql == "" || len(expandedSelectLists) == 0 || !viewSelectHasStar(stmt) { - return viewSql, false + return viewSql, nil, false } stableSelect, ok := viewSelectWithExpandedStars(stmt, expandedSelectLists) if !ok { - return viewSql, false + return viewSql, nil, false } parserSQLMode := "" @@ -402,7 +420,7 @@ func stableViewSQLWithExpandedStars( } stmts, err := mysql.ParseWithSQLMode(ctx.GetContext(), viewSql, ctx.GetLowerCaseTableNames(), parserSQLMode) if err != nil { - return viewSql, false + return viewSql, nil, false } defer func() { for _, statement := range stmts { @@ -410,23 +428,23 @@ func stableViewSQLWithExpandedStars( } }() if len(stmts) != 1 { - return viewSql, false + return viewSql, nil, false } switch viewStmt := stmts[0].(type) { case *tree.CreateView: stableStmt := *viewStmt stableStmt.AsSource = stableSelect - return formatStableViewSQL(&stableStmt), true + return formatStableViewSQL(&stableStmt), stableSelect, true case *tree.AlterView: stableStmt := &tree.CreateView{ Name: viewStmt.Name, ColNames: viewStmt.ColNames, AsSource: stableSelect, } - return formatStableViewSQL(stableStmt), true + return formatStableViewSQL(stableStmt), stableSelect, true default: - return viewSql, false + return viewSql, nil, false } } @@ -1614,7 +1632,7 @@ func buildCreateView(stmt *tree.CreateView, ctx CompilerContext) (*Plan, error) } tableDef, err := genViewTableDef( - ctx, stmt.AsSource, stmt.ColNames, createView.Database, string(viewName)) + ctx, stmt.AsSource, stmt.ColNames, createView.Database, string(viewName), stmt.CheckOption) if err != nil { return nil, err } @@ -5562,7 +5580,7 @@ func buildAlterView(stmt *tree.AlterView, ctx CompilerContext) (*Plan, error) { defer func() { ctx.SetBuildingAlterView(false, "", "") }() - tableDef, err := genViewTableDef(ctx, stmt.AsSource, stmt.ColNames, alterView.Database, viewName) + tableDef, err := genViewTableDef(ctx, stmt.AsSource, stmt.ColNames, alterView.Database, viewName, "NONE") if err != nil { return nil, err } diff --git a/pkg/sql/plan/build_ddl_test.go b/pkg/sql/plan/build_ddl_test.go index d81f8a6ddc4cc..667472ff1d9ca 100644 --- a/pkg/sql/plan/build_ddl_test.go +++ b/pkg/sql/plan/build_ddl_test.go @@ -1016,6 +1016,80 @@ func (c *rootSQLCompilerContext) GetRootSql() string { return c.rootSQL } +func TestBuildCreateViewPersistsParserDerivedInformationSchemaMetadata(t *testing.T) { + tests := []struct { + name string + sql string + contains string + checkOption string + }{ + { + name: "dollar quoted definer cannot supply structural view tokens", + sql: "CREATE DEFINER=$q$ view fake as select 0$q$ VIEW v AS SELECT 1;", + contains: "select 1", + }, + { + name: "executable comment preserves dollar quoted terminator text", + sql: "/*!50001 CREATE VIEW v AS SELECT $q$x*/y$q$ AS s */;", + contains: "*/y", + }, + { + name: "executable comment preserves escaped double quoted terminator text", + sql: "/*!50001 CREATE VIEW v AS SELECT \"x\\\"*/y\" AS s */;", + contains: "*/y", + }, + { + name: "double minus remains an arithmetic operator", + sql: "/*!50001 CREATE VIEW v AS SELECT 1--2 AS x */;", + contains: "select", + }, + { + name: "check option is separate from the select definition", + sql: "CREATE VIEW v AS SELECT 1 WITH CASCADED CHECK OPTION;", + contains: "select 1", + checkOption: "CASCADED", + }, + { + name: "bound column references are normalized with the definition", + sql: "CREATE VIEW v AS SELECT n_name FROM nation;", + contains: "select `nation`.`n_name` from `nation`", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + stmt, err := parsers.ParseOne(t.Context(), dialect.MYSQL, test.sql, 1) + require.NoError(t, err) + defer stmt.Free() + + ctx := &rootSQLCompilerContext{ + MockCompilerContext: NewMockCompilerContext(false), + rootSQL: test.sql, + } + built, err := BuildPlan(ctx, stmt, false) + require.NoError(t, err) + var data ViewData + require.NoError(t, json.Unmarshal([]byte(built.GetDdl().GetCreateView().GetTableDef().GetViewSql().GetView()), &data)) + require.NotEmpty(t, data.Definition) + assert.Contains(t, strings.ToLower(data.Definition), test.contains) + assert.NotContains(t, strings.ToLower(data.Definition), "create") + assert.NotContains(t, strings.ToLower(data.Definition), "check option") + expectedCheckOption := test.checkOption + if expectedCheckOption == "" { + expectedCheckOption = "NONE" + } + assert.Equal(t, expectedCheckOption, data.CheckOption) + + definitions, err := parsers.Parse(t.Context(), dialect.MYSQL, data.Definition, 1) + require.NoError(t, err) + require.Len(t, definitions, 1) + _, ok := definitions[0].(*tree.Select) + assert.True(t, ok) + definitions[0].Free() + }) + } +} + func TestBuildCreateOrReplaceViewRejectsRecursiveDefinition(t *testing.T) { recentTimestamp := time.Now().UTC().Add(-time.Minute).Format("2006-01-02 15:04:05.999999999") aheadOfWallClock := time.Now().Add(time.Minute) @@ -1478,7 +1552,9 @@ func TestGenViewTableDefPersistsExpandedStarSelectList(t *testing.T) { var viewData ViewData require.NoError(t, json.Unmarshal([]byte(tableDef.GetViewSql().GetView()), &viewData)) require.NotContains(t, viewData.Stmt, "*") + require.NotContains(t, viewData.Definition, "*") require.Contains(t, viewData.Stmt, "`nation`.`n_nationkey`") + require.Contains(t, viewData.Definition, "`nation`.`n_nationkey`") require.Contains(t, viewData.Stmt, "`nation`.`n_name`") require.Contains(t, viewData.Stmt, "`nation`.`n_regionkey`") require.Contains(t, viewData.Stmt, "`nation`.`n_comment`") diff --git a/pkg/sql/plan/function/func_mo_view_definition.go b/pkg/sql/plan/function/func_mo_view_definition.go new file mode 100644 index 0000000000000..30fa57decd45b --- /dev/null +++ b/pkg/sql/plan/function/func_mo_view_definition.go @@ -0,0 +1,188 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "context" + "encoding/json" + + "github.com/matrixorigin/matrixone/pkg/container/types" + "github.com/matrixorigin/matrixone/pkg/container/vector" + "github.com/matrixorigin/matrixone/pkg/sql/parsers" + "github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect" + "github.com/matrixorigin/matrixone/pkg/sql/parsers/tree" + "github.com/matrixorigin/matrixone/pkg/vm/process" +) + +// legacyViewDefinitionSQLMode is the parser compatibility default used for +// persisted View definitions that predate recording SQLMode in ViewData. +const legacyViewDefinitionSQLMode = "PIPES_AS_CONCAT" + +type persistedViewDefinitionData struct { + Stmt string + Definition string `json:"definition,omitempty"` + CheckOption string `json:"check_option,omitempty"` + SQLMode *string `json:"sql_mode,omitempty"` + LowerCaseTableNames *int64 `json:"lower_case_table_names,omitempty"` +} + +type persistedViewMetadata struct { + definition string + checkOption string +} + +// builtInViewDefinition returns the frozen parser-derived definition for a +// current View and supplies a parser-aware compatibility read for legacy rows. +// It deliberately does not write catalog data: metadata reads must remain +// bounded, side-effect-free, and independent of the inactive refresh lifecycle. +func builtInViewDefinition( + parameters []*vector.Vector, + result vector.FunctionResultWrapper, + proc *process.Process, + length int, + selectList *FunctionSelectList, +) error { + definitions := vector.GenerateFunctionStrParameter(parameters[0]) + results := vector.MustFunctionResult[types.Varlena](result) + + for row := uint64(0); row < uint64(length); row++ { + if selectList != nil && !selectList.ShouldEvalAllRow() && selectList.Contains(row) { + if err := results.AppendBytes(nil, true); err != nil { + return err + } + continue + } + persisted, isNull := definitions.GetStrValue(row) + if isNull { + if err := results.AppendBytes(nil, true); err != nil { + return err + } + continue + } + metadata, ok := viewMetadataFromPersistedData(proc.Ctx, string(persisted)) + if !ok { + if err := results.AppendBytes(nil, true); err != nil { + return err + } + continue + } + if err := results.AppendBytes([]byte(metadata.definition), false); err != nil { + return err + } + } + return nil +} + +func builtInViewCheckOption( + parameters []*vector.Vector, + result vector.FunctionResultWrapper, + proc *process.Process, + length int, + selectList *FunctionSelectList, +) error { + definitions := vector.GenerateFunctionStrParameter(parameters[0]) + results := vector.MustFunctionResult[types.Varlena](result) + + for row := uint64(0); row < uint64(length); row++ { + if selectList != nil && !selectList.ShouldEvalAllRow() && selectList.Contains(row) { + if err := results.AppendBytes(nil, true); err != nil { + return err + } + continue + } + persisted, isNull := definitions.GetStrValue(row) + if isNull { + if err := results.AppendBytes(nil, true); err != nil { + return err + } + continue + } + metadata, ok := viewMetadataFromPersistedData(proc.Ctx, string(persisted)) + if !ok { + if err := results.AppendBytes(nil, true); err != nil { + return err + } + continue + } + if err := results.AppendBytes([]byte(metadata.checkOption), false); err != nil { + return err + } + } + return nil +} + +func viewDefinitionFromPersistedData(ctx context.Context, persisted string) (string, bool) { + metadata, ok := viewMetadataFromPersistedData(ctx, persisted) + return metadata.definition, ok +} + +func viewMetadataFromPersistedData(ctx context.Context, persisted string) (persistedViewMetadata, bool) { + var data persistedViewDefinitionData + if err := json.Unmarshal([]byte(persisted), &data); err != nil { + return persistedViewMetadata{}, false + } + if data.Definition != "" { + return persistedViewMetadata{definition: data.Definition, checkOption: checkOptionOrNone(data.CheckOption)}, true + } + if data.Stmt == "" { + return persistedViewMetadata{}, false + } + + lowerCaseTableNames := int64(0) + if data.LowerCaseTableNames != nil { + lowerCaseTableNames = *data.LowerCaseTableNames + } + parserSQLMode := legacyViewDefinitionSQLMode + if data.SQLMode != nil { + parserSQLMode = *data.SQLMode + } + statements, err := parsers.ParseWithSQLMode( + ctx, dialect.MYSQL, data.Stmt, lowerCaseTableNames, parserSQLMode) + defer func() { + for _, statement := range statements { + statement.Free() + } + }() + if err != nil || len(statements) == 0 { + return persistedViewMetadata{}, false + } + + // Legacy ViewData.Stmt can be the entire COM_QUERY text. View binding uses + // its first parsed statement, so metadata must retain that compatibility. + var selectStmt *tree.Select + checkOption := "NONE" + switch statement := statements[0].(type) { + case *tree.CreateView: + selectStmt = statement.AsSource + checkOption = checkOptionOrNone(statement.CheckOption) + case *tree.AlterView: + selectStmt = statement.AsSource + default: + return persistedViewMetadata{}, false + } + if selectStmt == nil { + return persistedViewMetadata{}, false + } + return persistedViewMetadata{definition: tree.StringWithOpts( + selectStmt, dialect.MYSQL, tree.WithQuoteString(true), + tree.WithQuoteIdentifier(), tree.WithModeIndependentStringLiterals()), checkOption: checkOption}, true +} + +func checkOptionOrNone(checkOption string) string { + if checkOption == "" { + return "NONE" + } + return checkOption +} diff --git a/pkg/sql/plan/function/func_mo_view_definition_test.go b/pkg/sql/plan/function/func_mo_view_definition_test.go new file mode 100644 index 0000000000000..890e1cc96e20e --- /dev/null +++ b/pkg/sql/plan/function/func_mo_view_definition_test.go @@ -0,0 +1,240 @@ +// Copyright 2026 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/matrixorigin/matrixone/pkg/container/types" + "github.com/matrixorigin/matrixone/pkg/container/vector" + "github.com/matrixorigin/matrixone/pkg/testutil" +) + +func TestViewDefinitionFunctionRegistration(t *testing.T) { + registered := allSupportedFunctions[MO_VIEW_DEFINITION] + require.Equal(t, MO_VIEW_DEFINITION, registered.functionId) + require.Len(t, registered.Overloads, 1) + require.Equal(t, types.T_text, registered.Overloads[0].retType(nil).Oid) + require.NotNil(t, registered.Overloads[0].newOp()) + + function, err := GetFunctionByName( + context.Background(), "mo_view_definition", []types.Type{types.T_varchar.ToType()}) + require.NoError(t, err) + functionID, _ := DecodeOverloadID(function.GetEncodedOverloadID()) + require.Equal(t, int32(MO_VIEW_DEFINITION), functionID) + require.Equal(t, types.T_text, function.GetReturnType().Oid) + + registered = allSupportedFunctions[MO_VIEW_CHECK_OPTION] + require.Equal(t, MO_VIEW_CHECK_OPTION, registered.functionId) + require.Len(t, registered.Overloads, 1) + require.Equal(t, types.T_varchar, registered.Overloads[0].retType(nil).Oid) + require.NotNil(t, registered.Overloads[0].newOp()) + function, err = GetFunctionByName( + context.Background(), "mo_view_check_option", []types.Type{types.T_varchar.ToType()}) + require.NoError(t, err) + functionID, _ = DecodeOverloadID(function.GetEncodedOverloadID()) + require.Equal(t, int32(MO_VIEW_CHECK_OPTION), functionID) + require.Equal(t, types.T_varchar, function.GetReturnType().Oid) +} + +func TestViewDefinitionFromPersistedData(t *testing.T) { + tests := []struct { + name string + persisted string + want string + ok bool + }{ + { + name: "current frozen definition is returned unchanged", + persisted: `{"Stmt":"create view v as select 0","definition":"select ` + "`frozen`" + ` from ` + "`t`" + `"}`, + want: "select `frozen` from `t`", + ok: true, + }, + { + name: "legacy block comment before view is structurally opaque", + persisted: `{"Stmt":"create /* migration view fake as */ view v as select 1"}`, + want: "select 1", + ok: true, + }, + { + name: "legacy executable wrapper preserves quoted terminator", + persisted: `{"Stmt":"/*!50001 CREATE VIEW v AS SELECT 'x*/y' AS s */;"}`, + want: "x*/y", + ok: true, + }, + { + name: "legacy quoted definer cannot supply view boundary", + persisted: `{"Stmt":"CREATE DEFINER=' view fake as select 0'@'%' VIEW v AS SELECT 1"}`, + want: "select 1", + ok: true, + }, + { + name: "legacy check option is outside definition", + persisted: `{"Stmt":"CREATE VIEW v AS SELECT 1 WITH CASCADED CHECK OPTION"}`, + want: "select 1", + ok: true, + }, + { + name: "legacy saved parser options are honored", + persisted: `{"Stmt":"CREATE VIEW v AS SELECT 1","sql_mode":"PIPES_AS_CONCAT","lower_case_table_names":1}`, + want: "select 1", + ok: true, + }, + { + name: "legacy COM_QUERY uses the first statement", + persisted: `{"Stmt":"CREATE VIEW v AS SELECT 1; SELECT 2"}`, + want: "select 1", + ok: true, + }, + { + name: "malformed JSON remains null", + persisted: `{`, + }, + { + name: "missing statement remains null", + persisted: `{}`, + }, + { + name: "malformed persisted row remains null", + persisted: `{"Stmt":"CREATE VIEW"}`, + }, + { + name: "non view statement remains null", + persisted: `{"Stmt":"SELECT 1"}`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + definition, ok := viewDefinitionFromPersistedData(context.Background(), test.persisted) + require.Equal(t, test.ok, ok) + if !ok { + require.Empty(t, definition) + return + } + require.Contains(t, strings.ToLower(definition), test.want) + require.NotContains(t, strings.ToLower(definition), "create view") + require.NotContains(t, strings.ToLower(definition), "check option") + }) + } +} + +func TestViewMetadataPreservesLegacyCheckOption(t *testing.T) { + tests := []struct { + name string + persisted string + definition string + checkOption string + }{ + { + name: "legacy cascaded check option", + persisted: `{"Stmt":"CREATE VIEW v AS SELECT 1 WITH CASCADED CHECK OPTION"}`, + definition: "select 1", + checkOption: "CASCADED", + }, + { + name: "current frozen metadata", + persisted: `{"Stmt":"CREATE VIEW v AS SELECT 1","definition":"select frozen","check_option":"LOCAL"}`, + definition: "select frozen", + checkOption: "LOCAL", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + metadata, ok := viewMetadataFromPersistedData(context.Background(), test.persisted) + require.True(t, ok) + require.Equal(t, test.definition, strings.ToLower(metadata.definition)) + require.Equal(t, test.checkOption, metadata.checkOption) + }) + } +} + +func TestBuiltInViewDefinition(t *testing.T) { + proc := testutil.NewProcess(t) + current := `{"Stmt":"create view v as select 0","definition":"select frozen from t"}` + legacy := `{"Stmt":"create /* migration view fake as */ view v as select 1"}` + + t.Run("evaluates valid and invalid persisted rows", func(t *testing.T) { + input := vector.NewVec(types.T_varchar.ToType()) + require.NoError(t, vector.AppendStringList(input, + []string{current, legacy, `{"Stmt":"CREATE VIEW"}`}, + []bool{false, false, false}, proc.Mp())) + result := vector.NewFunctionResultWrapper(types.T_text.ToType(), proc.Mp()) + require.NoError(t, result.PreExtendAndReset(input.Length())) + require.NoError(t, builtInViewDefinition( + []*vector.Vector{input}, result, proc, input.Length(), nil)) + + values := vector.GenerateFunctionStrParameter(result.GetResultVector()) + value, isNull := values.GetStrValue(0) + require.False(t, isNull) + require.Equal(t, "select frozen from t", string(value)) + value, isNull = values.GetStrValue(1) + require.False(t, isNull) + require.Equal(t, "select 1", strings.ToLower(string(value))) + _, isNull = values.GetStrValue(2) + require.True(t, isNull) + }) + + t.Run("preserves null inputs and selection mask", func(t *testing.T) { + input := vector.NewVec(types.T_varchar.ToType()) + require.NoError(t, vector.AppendStringList(input, + []string{current, legacy}, []bool{false, false}, proc.Mp())) + result := vector.NewFunctionResultWrapper(types.T_text.ToType(), proc.Mp()) + require.NoError(t, result.PreExtendAndReset(input.Length())) + require.NoError(t, builtInViewDefinition([]*vector.Vector{input}, result, + proc, input.Length(), &FunctionSelectList{ + AnyNull: true, + SelectList: []bool{true, false}, + })) + + values := vector.GenerateFunctionStrParameter(result.GetResultVector()) + value, isNull := values.GetStrValue(0) + require.False(t, isNull) + require.Equal(t, "select frozen from t", string(value)) + _, isNull = values.GetStrValue(1) + require.True(t, isNull) + + nullInput := vector.NewConstNull(types.T_varchar.ToType(), 1, proc.Mp()) + nullResult := vector.NewFunctionResultWrapper(types.T_text.ToType(), proc.Mp()) + require.NoError(t, nullResult.PreExtendAndReset(nullInput.Length())) + require.NoError(t, builtInViewDefinition( + []*vector.Vector{nullInput}, nullResult, proc, nullInput.Length(), nil)) + _, isNull = vector.GenerateFunctionStrParameter( + nullResult.GetResultVector()).GetStrValue(0) + require.True(t, isNull) + }) +} + +func TestBuiltInViewCheckOption(t *testing.T) { + proc := testutil.NewProcess(t) + input := vector.NewVec(types.T_varchar.ToType()) + require.NoError(t, vector.AppendStringList(input, + []string{`{"Stmt":"CREATE VIEW v AS SELECT 1 WITH CASCADED CHECK OPTION"}`, `{"Stmt":"CREATE VIEW"}`}, + []bool{false, false}, proc.Mp())) + result := vector.NewFunctionResultWrapper(types.T_varchar.ToType(), proc.Mp()) + require.NoError(t, result.PreExtendAndReset(input.Length())) + require.NoError(t, builtInViewCheckOption( + []*vector.Vector{input}, result, proc, input.Length(), nil)) + values := vector.GenerateFunctionStrParameter(result.GetResultVector()) + value, isNull := values.GetStrValue(0) + require.False(t, isNull) + require.Equal(t, "CASCADED", string(value)) + _, isNull = values.GetStrValue(1) + require.True(t, isNull) +} diff --git a/pkg/sql/plan/function/function_id.go b/pkg/sql/plan/function/function_id.go index f4f56815a706f..e6da06f6fd044 100644 --- a/pkg/sql/plan/function/function_id.go +++ b/pkg/sql/plan/function/function_id.go @@ -793,6 +793,10 @@ const ( APPROX_PERCENTILE = 557 // function `mo_is_legacy_temporary_table` MO_IS_LEGACY_TEMPORARY_TABLE = 558 + // function `mo_view_definition` + MO_VIEW_DEFINITION = 579 + // function `mo_view_check_option` + MO_VIEW_CHECK_OPTION = 580 // onnx_run: evaluate an ONNX model. Renumbered as main merges claim ids // (549->554->556); referenced by name only, so renumbering is safe. @@ -846,7 +850,7 @@ const ( // FUNCTION_END_NUMBER is not a function, just a flag to record the max number of function. // TODO: every one should put the new function id in front of this one if you want to make a new function. - FUNCTION_END_NUMBER = 579 + FUNCTION_END_NUMBER = 581 ) // functionIdRegister is what function we have registered already. @@ -945,6 +949,8 @@ var functionIdRegister = map[string]int32{ "median": MEDIAN, "approx_percentile": APPROX_PERCENTILE, "mo_is_legacy_temporary_table": MO_IS_LEGACY_TEMPORARY_TABLE, + "mo_view_definition": MO_VIEW_DEFINITION, + "mo_view_check_option": MO_VIEW_CHECK_OPTION, "max_by": MAX_BY, "max_by_non_null": MAX_BY_NON_NULL, "percentile_cont": PERCENTILE_CONT, diff --git a/pkg/sql/plan/function/function_id_test.go b/pkg/sql/plan/function/function_id_test.go index 4bd21052ea433..8133623beb70c 100644 --- a/pkg/sql/plan/function/function_id_test.go +++ b/pkg/sql/plan/function/function_id_test.go @@ -729,6 +729,8 @@ var predefinedFunids = map[int]int{ ONNX_RUN: 556, APPROX_PERCENTILE: 557, MO_IS_LEGACY_TEMPORARY_TABLE: 558, + MO_VIEW_DEFINITION: 579, + MO_VIEW_CHECK_OPTION: 580, MAX_BY: 559, MAX_BY_NON_NULL: 560, CHECK_CONSTRAINT_ASSERT: 561, @@ -751,7 +753,7 @@ var predefinedFunids = map[int]int{ INTERNAL_JSON_MEMBER_OF: 578, // FUNCTION_END_NUMBER is not a function, just a flag to record the max number of function. // TODO: every one should put the new function id in front of this one if you want to make a new function. - FUNCTION_END_NUMBER: 579, + FUNCTION_END_NUMBER: 581, } func Test_funids(t *testing.T) { diff --git a/pkg/sql/plan/function/list_builtIn.go b/pkg/sql/plan/function/list_builtIn.go index 16bbb2fdda158..4316d432262f8 100644 --- a/pkg/sql/plan/function/list_builtIn.go +++ b/pkg/sql/plan/function/list_builtIn.go @@ -13950,6 +13950,52 @@ var supportedOthersBuiltIns = []FuncNew{ }, }, + // function `mo_view_definition` + // Used only by information_schema.VIEWS to provide a parser-aware read path + // for legacy ViewData rows that predate the frozen definition field. + { + functionId: MO_VIEW_DEFINITION, + class: plan.Function_INTERNAL | plan.Function_STRICT, + layout: STANDARD_FUNCTION, + checkFn: fixedTypeMatch, + + Overloads: []overload{ + { + overloadId: 0, + args: []types.T{types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_text.ToType() + }, + newOp: func() executeLogicOfOverload { + return builtInViewDefinition + }, + }, + }, + }, + + // function `mo_view_check_option` + // Shares the parser-aware legacy read path with mo_view_definition so the + // separate information_schema column remains consistent with its SELECT. + { + functionId: MO_VIEW_CHECK_OPTION, + class: plan.Function_INTERNAL | plan.Function_STRICT, + layout: STANDARD_FUNCTION, + checkFn: fixedTypeMatch, + + Overloads: []overload{ + { + overloadId: 0, + args: []types.T{types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_varchar.ToType() + }, + newOp: func() executeLogicOfOverload { + return builtInViewCheckOption + }, + }, + }, + }, + // function `internal_char_length` { functionId: INTERNAL_CHAR_LENGTH, diff --git a/pkg/sql/plan/types.go b/pkg/sql/plan/types.go index fcbf56dd36fa3..b9579570b19a3 100644 --- a/pkg/sql/plan/types.go +++ b/pkg/sql/plan/types.go @@ -362,6 +362,8 @@ type BaseOptimizer struct { type ViewData struct { Stmt string + Definition string `json:"definition,omitempty"` + CheckOption string `json:"check_option,omitempty"` DefaultDatabase string SQLMode *string `json:"sql_mode,omitempty"` SecurityType string `json:"security_type,omitempty"` diff --git a/pkg/sql/plan/view_dependency_test.go b/pkg/sql/plan/view_dependency_test.go index d6674bb1efb30..e0d6e1a9b8708 100644 --- a/pkg/sql/plan/view_dependency_test.go +++ b/pkg/sql/plan/view_dependency_test.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "errors" + "strings" "testing" "github.com/matrixorigin/matrixone/pkg/catalog" @@ -26,6 +27,7 @@ import ( "github.com/matrixorigin/matrixone/pkg/pb/timestamp" "github.com/matrixorigin/matrixone/pkg/sql/parsers" "github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect" + "github.com/matrixorigin/matrixone/pkg/sql/parsers/tree" "github.com/stretchr/testify/require" ) @@ -187,7 +189,7 @@ func TestRegenerateViewDefinitionUsesAuthoritativeGeneratorAndPreservesJSON(t *t ctx.tables["nation"].TblId = 11 ctx.tables["nation"].LogicalId = 13 ctx.tables["nation"].Cols[1].Typ.Width = 60 - persisted := `{"Stmt":"create view v as select n_name from nation",` + + persisted := `{"Stmt":"create view v as select n_name from nation with cascaded check option",` + `"DefaultDatabase":"tpch","security_type":"DEFINER",` + `"future_field":{"keep":true}}` @@ -200,11 +202,80 @@ func TestRegenerateViewDefinitionUsesAuthoritativeGeneratorAndPreservesJSON(t *t var fields map[string]json.RawMessage require.NoError(t, json.Unmarshal([]byte(regenerated.TableDef.ViewSql.View), &fields)) require.JSONEq(t, `{"keep":true}`, string(fields["future_field"])) - require.JSONEq(t, `"create view v as select n_name from nation"`, string(fields["Stmt"])) + require.JSONEq(t, `"create view v as select n_name from nation with cascaded check option"`, string(fields["Stmt"])) + require.JSONEq(t, "\"select `nation`.`n_name` from `nation`\"", string(fields["definition"])) + require.JSONEq(t, `"CASCADED"`, string(fields["check_option"])) require.Contains(t, fields, "dependencies") require.Contains(t, fields, "lower_case_table_names") } +func TestRegenerateLegacyViewDefinitionUsesParserDerivedMetadata(t *testing.T) { + // Legacy catalog rows have only Stmt. Recovery must parse that statement and + // persist the SELECT AST rendering rather than attempting to tokenize the + // SQL in information_schema. + tests := []struct { + name string + stmt string + contains string + checkOption string + }{ + { + name: "dollar quoted definer cannot supply view boundary", + stmt: "CREATE DEFINER=$q$ view fake as select 0$q$ VIEW v AS SELECT 1", + contains: "select 1", + }, + { + name: "executable comment keeps quoted terminator text", + stmt: "/*!50001 CREATE VIEW v AS SELECT 'x*/y' AS s */;", + contains: "x*/y", + }, + { + name: "double quoted executable comment keeps escaped terminator text", + stmt: "/*!50001 CREATE VIEW v AS SELECT \"x\\\"*/y\" AS s */;", + contains: "*/y", + }, + { + name: "arithmetic double dash is not a line comment", + stmt: "/*!50001 CREATE VIEW v AS SELECT 1--2 AS s */;", + contains: "- -2", + }, + { + name: "check option is outside select definition", + stmt: "CREATE VIEW v AS SELECT 1 WITH CASCADED CHECK OPTION", + contains: "select 1", + checkOption: "CASCADED", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + persisted, err := json.Marshal(ViewData{Stmt: test.stmt, DefaultDatabase: "tpch"}) + require.NoError(t, err) + + regenerated, err := RegenerateViewDefinition(NewMockCompilerContext(false), string(persisted)) + require.NoError(t, err) + var data ViewData + require.NoError(t, json.Unmarshal([]byte(regenerated.TableDef.ViewSql.View), &data)) + require.NotEmpty(t, data.Definition) + require.Contains(t, strings.ToLower(data.Definition), test.contains) + require.NotContains(t, strings.ToLower(data.Definition), "create view") + require.NotContains(t, strings.ToLower(data.Definition), "check option") + if test.checkOption == "" { + require.Equal(t, "NONE", data.CheckOption) + } else { + require.Equal(t, test.checkOption, data.CheckOption) + } + + statements, err := parsers.Parse(t.Context(), dialect.MYSQL, data.Definition, 1) + require.NoError(t, err) + require.Len(t, statements, 1) + _, ok := statements[0].(*tree.Select) + require.True(t, ok) + statements[0].Free() + }) + } +} + func TestRegenerateViewDefinitionPersistsExpandedStar(t *testing.T) { for _, rootSQL := range []string{ "create view v as select * from nation", @@ -224,6 +295,7 @@ func TestRegenerateViewDefinitionPersistsExpandedStar(t *testing.T) { var firstData ViewData require.NoError(t, json.Unmarshal([]byte(first.TableDef.ViewSql.View), &firstData)) require.NotContains(t, firstData.Stmt, "*") + require.NotContains(t, firstData.Definition, "*") ctx.tables["nation"].Cols = append(ctx.tables["nation"].Cols, &planpb.ColDef{ Name: "n_extra", @@ -237,6 +309,9 @@ func TestRegenerateViewDefinitionPersistsExpandedStar(t *testing.T) { var fields map[string]json.RawMessage require.NoError(t, json.Unmarshal([]byte(second.TableDef.ViewSql.View), &fields)) require.JSONEq(t, `{"keep":true}`, string(fields["future_field"])) + var secondData ViewData + require.NoError(t, json.Unmarshal([]byte(second.TableDef.ViewSql.View), &secondData)) + require.Equal(t, firstData.Definition, secondData.Definition) }) } } diff --git a/pkg/sql/plan/view_regeneration.go b/pkg/sql/plan/view_regeneration.go index f405f9e724359..ef3489edc1be5 100644 --- a/pkg/sql/plan/view_regeneration.go +++ b/pkg/sql/plan/view_regeneration.go @@ -52,7 +52,7 @@ func ReplaceRegeneratedViewDependencies( lowerCaseTableNames = *data.LowerCaseTableNames } updated, err := patchPersistedViewMetadata( - regenerated.TableDef.ViewSql.View, nil, dependencies, lowerCaseTableNames) + regenerated.TableDef.ViewSql.View, nil, nil, nil, dependencies, lowerCaseTableNames) if err != nil { return err } @@ -130,10 +130,12 @@ func RegenerateViewDefinition( var selectStmt *tree.Select var columnNames tree.IdentifierList var viewDatabase, viewName string + checkOption := "NONE" switch statement := statements[0].(type) { case *tree.CreateView: selectStmt, columnNames = statement.AsSource, statement.ColNames viewDatabase, viewName = string(statement.Name.SchemaName), string(statement.Name.ObjectName) + checkOption = statement.CheckOption case *tree.AlterView: selectStmt, columnNames = statement.AsSource, statement.ColNames viewDatabase, viewName = string(statement.Name.SchemaName), string(statement.Name.ObjectName) @@ -151,7 +153,7 @@ func RegenerateViewDefinition( lowerCaseTableNames: lowerCaseTableNames, } tableDef, err := genViewTableDef( - regenerationCtx, selectStmt, columnNames, viewDatabase, viewName) + regenerationCtx, selectStmt, columnNames, viewDatabase, viewName, checkOption) if err != nil { return nil, err } @@ -161,7 +163,8 @@ func RegenerateViewDefinition( } updatedViewData, err := patchPersistedViewMetadata( - persistedViewData, &generatedData.Stmt, generatedData.Dependencies, lowerCaseTableNames) + persistedViewData, &generatedData.Stmt, &generatedData.Definition, &generatedData.CheckOption, + generatedData.Dependencies, lowerCaseTableNames) if err != nil { return nil, err } @@ -175,6 +178,8 @@ func RegenerateViewDefinition( func patchPersistedViewMetadata( persistedViewData string, stableStatement *string, + definition *string, + checkOption *string, dependencies []ViewDependency, lowerCaseTableNames int64, ) (string, error) { @@ -193,6 +198,20 @@ func patchPersistedViewMetadata( } fields["Stmt"] = encodedStatement } + if definition != nil { + encodedDefinition, marshalErr := json.Marshal(*definition) + if marshalErr != nil { + return "", marshalErr + } + fields["definition"] = encodedDefinition + } + if checkOption != nil { + encodedCheckOption, marshalErr := json.Marshal(*checkOption) + if marshalErr != nil { + return "", marshalErr + } + fields["check_option"] = encodedCheckOption + } fields["dependencies"] = encodedDependencies if _, ok := fields["lower_case_table_names"]; !ok { encodedLowerCaseTableNames, marshalErr := json.Marshal(lowerCaseTableNames) diff --git a/pkg/util/sysview/predefined.go b/pkg/util/sysview/predefined.go index 94afb5fc05b41..042ef1d1dabd0 100644 --- a/pkg/util/sysview/predefined.go +++ b/pkg/util/sysview/predefined.go @@ -21,6 +21,18 @@ import ( "github.com/matrixorigin/matrixone/pkg/catalog" ) +var ( + // VIEW_DEFINITION is parser-derived at CREATE/ALTER time. The internal + // function returns that frozen field directly and parses only old ViewData + // rows that predate it, avoiding a second SQL-level lexer or inactive + // lifecycle dependency in this public metadata contract. + informationSchemaViewDefinitionSQL = "mo_view_definition(tbl.viewdef)" + informationSchemaViewCheckOptionSQL = "mo_view_check_option(tbl.viewdef)" + informationSchemaViewsSourceSQL = "FROM mo_catalog.mo_tables tbl JOIN __mo_visible_tables visible_tbl ON " + + "tbl.account_id = visible_tbl.account_id AND tbl.rel_id = visible_tbl.rel_id LEFT JOIN mo_catalog.mo_user usr ON tbl.creator = usr.user_id WHERE tbl.account_id = current_account_id() " + + "and tbl.relkind = 'v' and tbl.reldatabase != 'information_schema'" +) + // `mysql` database system tables // They are all Tenant level system tables var ( @@ -577,20 +589,33 @@ var ( "WHERE `tbl`.`account_id` = current_account_id()" InformationSchemaViewsDDL = "CREATE VIEW information_schema.VIEWS AS " + + informationSchemaMetadataVisibilityCTE() + "SELECT 'def' AS `TABLE_CATALOG`," + + "tbl.reldatabase AS `TABLE_SCHEMA`," + + "tbl.relname AS `TABLE_NAME`," + + informationSchemaViewDefinitionSQL + " AS `VIEW_DEFINITION`," + + "cast(coalesce(" + informationSchemaViewCheckOptionSQL + ", 'NONE') as varchar(9)) AS `CHECK_OPTION`," + + "cast('NO' as varchar(3)) AS `IS_UPDATABLE`," + + "usr.user_name + '@' + usr.user_host AS `DEFINER`," + + "'DEFINER' AS `SECURITY_TYPE`," + + "'utf8mb4' AS `CHARACTER_SET_CLIENT`," + + "'" + DefaultCollationForCharset("utf8mb4") + "' AS `COLLATION_CONNECTION` " + + informationSchemaViewsSourceSQL + + // InformationSchemaViewsLegacyDDL is installed until every CN supports the + // parser-derived definition function. It must not reference a function ID an + // older remote receiver cannot resolve. + InformationSchemaViewsLegacyDDL = "CREATE VIEW information_schema.VIEWS AS " + informationSchemaMetadataVisibilityCTE() + "SELECT 'def' AS `TABLE_CATALOG`," + "tbl.reldatabase AS `TABLE_SCHEMA`," + "tbl.relname AS `TABLE_NAME`," + "tbl.rel_createsql AS `VIEW_DEFINITION`," + - "cast('NONE' as varchar(9)) AS `CHECK_OPTION`," + + "'NONE' AS `CHECK_OPTION`," + "'YES' AS `IS_UPDATABLE`," + "usr.user_name + '@' + usr.user_host AS `DEFINER`," + "'DEFINER' AS `SECURITY_TYPE`," + "'utf8mb4' AS `CHARACTER_SET_CLIENT`," + "'" + DefaultCollationForCharset("utf8mb4") + "' AS `COLLATION_CONNECTION` " + - "FROM mo_catalog.mo_tables tbl " + - "JOIN __mo_visible_tables visible_tbl ON tbl.account_id = visible_tbl.account_id AND tbl.rel_id = visible_tbl.rel_id " + - "LEFT JOIN mo_catalog.mo_user usr ON tbl.creator = usr.user_id " + - "WHERE tbl.account_id = current_account_id() and tbl.relkind = 'v' and tbl.reldatabase != 'information_schema'" + informationSchemaViewsSourceSQL InformationSchemaStatisticsDDL = fmt.Sprintf("CREATE VIEW information_schema.`STATISTICS` AS "+informationSchemaMetadataVisibilityCTE()+ "select 'def' AS `TABLE_CATALOG`,"+ diff --git a/pkg/util/sysview/predefined_test.go b/pkg/util/sysview/predefined_test.go index de680bd1bcfbf..fa05170ca0ca1 100644 --- a/pkg/util/sysview/predefined_test.go +++ b/pkg/util/sysview/predefined_test.go @@ -24,7 +24,9 @@ import ( "github.com/matrixorigin/matrixone/pkg/catalog" "github.com/matrixorigin/matrixone/pkg/defines" + "github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect" "github.com/matrixorigin/matrixone/pkg/sql/parsers/dialect/mysql" + "github.com/matrixorigin/matrixone/pkg/sql/parsers/tree" ) func TestInformationSchemaMetadataViewsHideTemporaryTables(t *testing.T) { @@ -116,7 +118,7 @@ func TestInformationSchemaMetadataViewsEnforceObjectPrivileges(t *testing.T) { assert.NotContains(t, InformationSchemaReferentialConstraintsDDL, "fk.table_id = fk_tbl.rel_id") assert.Contains(t, InformationSchemaCheckConstraintsDDL, "JOIN __mo_visible_tables check_tbl") assert.Contains(t, InformationSchemaViewsDDL, "JOIN __mo_visible_tables visible_tbl") - assert.Contains(t, InformationSchemaViewsDDL, "cast('NONE' as varchar(9)) AS `CHECK_OPTION`") + assert.Contains(t, InformationSchemaViewsDDL, "coalesce(mo_view_check_option(tbl.viewdef), 'NONE')") assert.Contains(t, InformationSchemaPartitionsDDL, "FROM `__mo_visible_tables` `tbl`") assert.Contains(t, InformationSchemaSchemataDDL, "FROM __mo_visible_databases") assert.Contains(t, InformationSchemaSchemataDDL, "db.owner IN (SELECT role_id FROM __mo_active_roles)") @@ -286,7 +288,17 @@ func TestInitInformationSchemaSysTablesForProtocol(t *testing.T) { }) } - latest := InitInformationSchemaSysTablesForProtocol(defines.MORPCVersion58) + predecessor := InitInformationSchemaSysTablesForProtocol(defines.MORPCVersion58) + assert.Contains(t, predecessor, InformationSchemaViewsLegacyDDL) + assert.NotContains(t, predecessor, InformationSchemaViewsDDL) + assert.NotContains(t, strings.Join(predecessor, "\n"), "mo_view_definition(") + assert.Contains(t, strings.Join(predecessor, "\n"), "mo_subscription_tables()") + assert.Contains(t, strings.Join(predecessor, "\n"), "mo_subscription_columns()") + for _, sql := range predecessor { + assertInformationSchemaInitSQLParses(t, sql) + } + + latest := InitInformationSchemaSysTablesForProtocol(defines.MORPCVersion59) assert.Equal(t, InitInformationSchemaSysTables, latest) assert.Contains(t, strings.Join(latest, "\n"), "WHEN 3 then 'utf8mb4'") } @@ -484,6 +496,31 @@ func TestInformationSchemaCharacterSetsData(t *testing.T) { assert.Equal(t, ddlIndex+1, dataIndex) } +func TestInformationSchemaViewsMetadata(t *testing.T) { + // VIEWS must not execute a second SQL-level regexp grammar for catalog rows. + assert.Contains(t, InformationSchemaViewsDDL, "mo_view_definition(tbl.viewdef)") + assert.Contains(t, InformationSchemaViewsDDL, "mo_view_check_option(tbl.viewdef)") + assert.Contains(t, InformationSchemaViewsDDL, "coalesce(mo_view_check_option(tbl.viewdef), 'NONE')") + // Installing the upgrade view must not hide a real pre-upgrade viewdef that + // lacks the frozen field. The internal parser compatibility function supplies + // its SELECT definition without depending on lifecycle activation. + assert.NotContains(t, InformationSchemaViewsDDL, "json_extract_string(tbl.viewdef, '$.definition')") + assert.NotContains(t, InformationSchemaViewsDDL, "json_extract_string(tbl.viewdef, '$.check_option')") + assert.NotContains(t, InformationSchemaViewsDDL, "regexp_substr") + assert.NotContains(t, InformationSchemaViewsDDL, "tbl.rel_createsql AS `VIEW_DEFINITION`") + statements, err := mysql.Parse(context.Background(), InformationSchemaViewsDDL, 1) + assert.NoError(t, err) + for _, statement := range statements { + persisted := tree.StringWithOpts(statement, dialect.MYSQL, tree.WithSingleQuoteString(), tree.WithQuoteIdentifier()) + roundTripped, err := mysql.Parse(context.Background(), persisted, 1) + assert.NoError(t, err, persisted) + for _, roundTrippedStatement := range roundTripped { + roundTrippedStatement.Free() + } + statement.Free() + } +} + func TestInformationSchemaDefaultCollationsMatchCanonicalDefinitions(t *testing.T) { assert.Empty(t, DefaultCollationForCharset("unknown_charset")) for _, charset := range []string{"binary", "utf8", "utf8mb4"} { diff --git a/pkg/util/sysview/sysview.go b/pkg/util/sysview/sysview.go index a03751191824e..9726905e3e401 100644 --- a/pkg/util/sysview/sysview.go +++ b/pkg/util/sysview/sysview.go @@ -76,7 +76,7 @@ var ( ) func InitInformationSchemaSysTablesForProtocol(protocol int64) []string { - if protocol >= defines.MORPCVersion58 { + if protocol >= defines.MORPCVersion59 { return InitInformationSchemaSysTables } @@ -95,6 +95,8 @@ func InitInformationSchemaSysTablesForProtocol(protocol int64) []string { } else { sql = InformationSchemaColumnsV41DDL } + case InformationSchemaViewsDDL: + sql = InformationSchemaViewsLegacyDDL } if !includeCheckConstraints { switch sql { diff --git a/test/distributed/cases/view/information_schema_views_metadata.result b/test/distributed/cases/view/information_schema_views_metadata.result new file mode 100644 index 0000000000000..ff210506d2bdc --- /dev/null +++ b/test/distributed/cases/view/information_schema_views_metadata.result @@ -0,0 +1,86 @@ +drop database if exists information_schema_views_metadata; +create database information_schema_views_metadata; +use information_schema_views_metadata; +create table t(a int, b int); +insert into t values (1, 10), (1, 20), (2, 30); +create view direct_v as select a, b from t; +create view agg_v as select a, count(*) cnt from t group by a; +/* migration */ create view leading_block_comment_v as select a from t; +/*!50001 CREATE DEFINER = `root`@`%` VIEW dump_v AS select a from t */; +/*!50001 CREATE ALGORITHM=UNDEFINED */ +/*!50013 DEFINER=`user view fake as select 0`@`%` SQL SECURITY DEFINER */ +/*!50001 VIEW split_dump_v AS select a from t */; +create view line_comment_v -- migration-generated view +as select a from t; +create view hash_comment_v # migration-generated view +as select a from t; +create view slash_comment_v // migration-generated view +as select a from t; +create view block_comment_v /* migration-generated view */ as select a from t; +create view adjacent_block_comment_v/* migration-generated view */as select a from t; +create /* migration view fake as */ view block_before_view_v as select a from t; +create view repeated_star_comment_v /***/ as select a from t; +create view long_repeated_star_comment_v /*****/ as select a from t; +/*! CREATE VIEW executable_without_version_v AS select a from t */; +/*!50001 CREATE VIEW executable_trailing_comment_v AS select a from t */ /* application */; +/*!50001 CREATE VIEW executable_string_terminator_v AS select 'x*/y' as s */; +CREATE DEFINER=' view fake as select 0'@'%' VIEW quoted_definer_v AS select a from t; +CREATE DEFINER=' view fake \' VIEW fake AS select 0'@'%' VIEW escaped_quoted_definer_v AS select a from t; +CREATE DEFINER=$q$ view fake as select 0$q$ VIEW dollar_quoted_definer_v AS select a from t; +/*!50001 CREATE VIEW executable_dollar_terminator_v AS select $q$x*/y$q$ as s */; +/*!50001 CREATE VIEW executable_double_quote_terminator_v AS select "x\"*/y" as s */; +/*!50001 CREATE VIEW executable_double_minus_v AS select 1--2 as x */; +CREATE VIEW check_option_v AS select a from t WITH CASCADED CHECK OPTION; +select table_name, view_definition, is_updatable +from information_schema.views +where table_schema = 'information_schema_views_metadata' +order by table_name; +➤ table_name[12,5000,0] ¦ view_definition[-1,16383,0] ¦ is_updatable[12,3,0] 𝄀 +adjacent_block_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +agg_v ¦ select `t`.`a`, count(*) as `cnt` from `t` group by `t`.`a` ¦ NO 𝄀 +block_before_view_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +block_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +check_option_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +direct_v ¦ select `t`.`a`, `t`.`b` from `t` ¦ NO 𝄀 +dollar_quoted_definer_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +dump_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +escaped_quoted_definer_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +executable_dollar_terminator_v ¦ select "x*/y" as `s` ¦ NO 𝄀 +executable_double_minus_v ¦ select 1 - -2 as `x` ¦ NO 𝄀 +executable_double_quote_terminator_v ¦ select "x\"*/y" as `s` ¦ NO 𝄀 +executable_string_terminator_v ¦ select "x*/y" as `s` ¦ NO 𝄀 +executable_trailing_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +executable_without_version_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +hash_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +leading_block_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +line_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +long_repeated_star_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +quoted_definer_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +repeated_star_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +slash_comment_v ¦ select `t`.`a` from `t` ¦ NO 𝄀 +split_dump_v ¦ select `t`.`a` from `t` ¦ NO +update agg_v set cnt = 1; +invalid input: cannot insert/update/delete from view +update direct_v set b = 1; +invalid input: cannot insert/update/delete from view +select table_name, view_definition, check_option +from information_schema.views +where table_schema = 'information_schema_views_metadata' and table_name = 'check_option_v'; +➤ table_name[12,5000,0] ¦ view_definition[-1,16383,0] ¦ check_option[12,9,0] 𝄀 +check_option_v ¦ select `t`.`a` from `t` ¦ CASCADED +create view stable_star_v as select * from t; +alter table t add column c int; +select table_name, view_definition +from information_schema.views +where table_schema = 'information_schema_views_metadata' and table_name = 'stable_star_v'; +➤ table_name[12,5000,0] ¦ view_definition[-1,16383,0] 𝄀 +stable_star_v ¦ select `t`.`a` as `a`, `t`.`b` as `b` from `t` +select * from stable_star_v order by a, b; +➤ a[4,32,0] ¦ b[4,32,0] 𝄀 +1 ¦ 10 𝄀 +1 ¦ 20 𝄀 +2 ¦ 30 +drop database information_schema_views_metadata; +drop database if exists information_schema_views_clone; +create database information_schema_views_clone clone information_schema; +drop database information_schema_views_clone; diff --git a/test/distributed/cases/view/information_schema_views_metadata.sql b/test/distributed/cases/view/information_schema_views_metadata.sql new file mode 100644 index 0000000000000..732bf33d3afb3 --- /dev/null +++ b/test/distributed/cases/view/information_schema_views_metadata.sql @@ -0,0 +1,64 @@ +-- @label:bvt +drop database if exists information_schema_views_metadata; +create database information_schema_views_metadata; +use information_schema_views_metadata; + +create table t(a int, b int); +insert into t values (1, 10), (1, 20), (2, 30); +create view direct_v as select a, b from t; +create view agg_v as select a, count(*) cnt from t group by a; +/* migration */ create view leading_block_comment_v as select a from t; +/*!50001 CREATE DEFINER = `root`@`%` VIEW dump_v AS select a from t */; +/*!50001 CREATE ALGORITHM=UNDEFINED */ +/*!50013 DEFINER=`user view fake as select 0`@`%` SQL SECURITY DEFINER */ +/*!50001 VIEW split_dump_v AS select a from t */; +create view line_comment_v -- migration-generated view +as select a from t; +create view hash_comment_v # migration-generated view +as select a from t; +create view slash_comment_v // migration-generated view +as select a from t; +create view block_comment_v /* migration-generated view */ as select a from t; +create view adjacent_block_comment_v/* migration-generated view */as select a from t; +create /* migration view fake as */ view block_before_view_v as select a from t; +create view repeated_star_comment_v /***/ as select a from t; +create view long_repeated_star_comment_v /*****/ as select a from t; +/*! CREATE VIEW executable_without_version_v AS select a from t */; +/*!50001 CREATE VIEW executable_trailing_comment_v AS select a from t */ /* application */; +/*!50001 CREATE VIEW executable_string_terminator_v AS select 'x*/y' as s */; +CREATE DEFINER=' view fake as select 0'@'%' VIEW quoted_definer_v AS select a from t; +CREATE DEFINER=' view fake \' VIEW fake AS select 0'@'%' VIEW escaped_quoted_definer_v AS select a from t; +CREATE DEFINER=$q$ view fake as select 0$q$ VIEW dollar_quoted_definer_v AS select a from t; +/*!50001 CREATE VIEW executable_dollar_terminator_v AS select $q$x*/y$q$ as s */; +/*!50001 CREATE VIEW executable_double_quote_terminator_v AS select "x\"*/y" as s */; +/*!50001 CREATE VIEW executable_double_minus_v AS select 1--2 as x */; +CREATE VIEW check_option_v AS select a from t WITH CASCADED CHECK OPTION; + +select table_name, view_definition, is_updatable +from information_schema.views +where table_schema = 'information_schema_views_metadata' +order by table_name; + +update agg_v set cnt = 1; +update direct_v set b = 1; + +select table_name, view_definition, check_option +from information_schema.views +where table_schema = 'information_schema_views_metadata' and table_name = 'check_option_v'; + +-- The public definition must match the creation-time frozen SELECT list, not +-- the later source-table shape. Replaying this metadata must recreate the +-- same two-column view after the source table gains a column. +create view stable_star_v as select * from t; +alter table t add column c int; +select table_name, view_definition +from information_schema.views +where table_schema = 'information_schema_views_metadata' and table_name = 'stable_star_v'; +select * from stable_star_v order by a, b; + +drop database information_schema_views_metadata; + +-- The stored VIEWS definition must remain executable when a system database is cloned. +drop database if exists information_schema_views_clone; +create database information_schema_views_clone clone information_schema; +drop database information_schema_views_clone; diff --git a/test/distributed/cases/zz_accesscontrol/inner_object.result b/test/distributed/cases/zz_accesscontrol/inner_object.result index 1967ddcaee67c..f3b5f0a8adf33 100644 --- a/test/distributed/cases/zz_accesscontrol/inner_object.result +++ b/test/distributed/cases/zz_accesscontrol/inner_object.result @@ -333,10 +333,10 @@ ac_db ¦ ac_t1 select count(*),table_name from information_schema.tables group by table_name having count(*) >1; ➤ count(*)[-5,64,0] ¦ table_name[12,-1,0] select * from information_schema.views where table_name='ac_v1'; -➤ TABLE_CATALOG[12,2,0] ¦ TABLE_SCHEMA[12,3750,0] ¦ TABLE_NAME[12,3750,0] ¦ VIEW_DEFINITION[12,0,0] ¦ CHECK_OPTION[12,9,0] ¦ IS_UPDATABLE[12,2,0] ¦ DEFINER[12,49151,0] ¦ SECURITY_TYPE[12,5,0] ¦ CHARACTER_SET_CLIENT[12,5,0] ¦ COLLATION_CONNECTION[12,13,0] 𝄀 -def ¦ ac_db ¦ ac_v1 ¦ create view `ac_db`.`ac_v1` as select `ac_t1`.`c1` as `c1` from `ac_db`.`ac_t1` ¦ NONE ¦ YES ¦ admin@localhost ¦ DEFINER ¦ utf8mb4 ¦ utf8mb4_general_ci +➤ TABLE_CATALOG[12,3,0] ¦ TABLE_SCHEMA[12,5000,0] ¦ TABLE_NAME[12,5000,0] ¦ VIEW_DEFINITION[-1,16383,0] ¦ CHECK_OPTION[12,9,0] ¦ IS_UPDATABLE[12,3,0] ¦ DEFINER[12,65535,0] ¦ SECURITY_TYPE[12,7,0] ¦ CHARACTER_SET_CLIENT[12,7,0] ¦ COLLATION_CONNECTION[12,18,0] 𝄀 +def ¦ ac_db ¦ ac_v1 ¦ select `ac_t1`.`c1` as `c1` from `ac_db`.`ac_t1` ¦ NONE ¦ NO ¦ admin@localhost ¦ DEFINER ¦ utf8mb4 ¦ utf8mb4_general_ci select * from information_schema.views where table_name='sys_v1'; -➤ TABLE_CATALOG[12,2,0] ¦ TABLE_SCHEMA[12,3750,0] ¦ TABLE_NAME[12,3750,0] ¦ VIEW_DEFINITION[12,0,0] ¦ CHECK_OPTION[12,9,0] ¦ IS_UPDATABLE[12,2,0] ¦ DEFINER[12,49151,0] ¦ SECURITY_TYPE[12,5,0] ¦ CHARACTER_SET_CLIENT[12,5,0] ¦ COLLATION_CONNECTION[12,13,0] +➤ TABLE_CATALOG[12,2,0] ¦ TABLE_SCHEMA[12,3750,0] ¦ TABLE_NAME[12,3750,0] ¦ VIEW_DEFINITION[12,0,0] ¦ CHECK_OPTION[12,3,0] ¦ IS_UPDATABLE[12,2,0] ¦ DEFINER[12,49151,0] ¦ SECURITY_TYPE[12,5,0] ¦ CHARACTER_SET_CLIENT[12,5,0] ¦ COLLATION_CONNECTION[12,13,0] select count(*),table_name from information_schema.views group by table_name having count(*)>1; ➤ count(*)[-5,64,0] ¦ table_name[12,-1,0] select count(*) from information_schema.partitions where table_schema='ac_db' and table_name='test02'; @@ -367,10 +367,10 @@ select table_schema,table_name from information_schema.tables where table_name=' select count(*),table_name from information_schema.tables group by table_name having count(*) >1; ➤ count(*)[-5,64,0] ¦ table_name[12,-1,0] select * from information_schema.views where table_name='sys_v1'; -➤ TABLE_CATALOG[12,2,0] ¦ TABLE_SCHEMA[12,3750,0] ¦ TABLE_NAME[12,3750,0] ¦ VIEW_DEFINITION[12,0,0] ¦ CHECK_OPTION[12,9,0] ¦ IS_UPDATABLE[12,2,0] ¦ DEFINER[12,49151,0] ¦ SECURITY_TYPE[12,5,0] ¦ CHARACTER_SET_CLIENT[12,5,0] ¦ COLLATION_CONNECTION[12,13,0] 𝄀 -def ¦ sys_db1 ¦ sys_v1 ¦ create view `sys_db1`.`sys_v1` as select `sys_t1`.`c1` as `c1` from `sys_db1`.`sys_t1` ¦ NONE ¦ YES ¦ dump@localhost ¦ DEFINER ¦ utf8mb4 ¦ utf8mb4_general_ci +➤ TABLE_CATALOG[12,3,0] ¦ TABLE_SCHEMA[12,5000,0] ¦ TABLE_NAME[12,5000,0] ¦ VIEW_DEFINITION[-1,16383,0] ¦ CHECK_OPTION[12,9,0] ¦ IS_UPDATABLE[12,3,0] ¦ DEFINER[12,65535,0] ¦ SECURITY_TYPE[12,7,0] ¦ CHARACTER_SET_CLIENT[12,7,0] ¦ COLLATION_CONNECTION[12,18,0] 𝄀 +def ¦ sys_db1 ¦ sys_v1 ¦ select `sys_t1`.`c1` as `c1` from `sys_db1`.`sys_t1` ¦ NONE ¦ NO ¦ dump@localhost ¦ DEFINER ¦ utf8mb4 ¦ utf8mb4_general_ci select * from information_schema.views where table_name='ac_v1'; -➤ TABLE_CATALOG[12,2,0] ¦ TABLE_SCHEMA[12,3750,0] ¦ TABLE_NAME[12,3750,0] ¦ VIEW_DEFINITION[12,0,0] ¦ CHECK_OPTION[12,9,0] ¦ IS_UPDATABLE[12,2,0] ¦ DEFINER[12,49151,0] ¦ SECURITY_TYPE[12,5,0] ¦ CHARACTER_SET_CLIENT[12,5,0] ¦ COLLATION_CONNECTION[12,13,0] +➤ TABLE_CATALOG[12,2,0] ¦ TABLE_SCHEMA[12,3750,0] ¦ TABLE_NAME[12,3750,0] ¦ VIEW_DEFINITION[12,0,0] ¦ CHECK_OPTION[12,3,0] ¦ IS_UPDATABLE[12,2,0] ¦ DEFINER[12,49151,0] ¦ SECURITY_TYPE[12,5,0] ¦ CHARACTER_SET_CLIENT[12,5,0] ¦ COLLATION_CONNECTION[12,13,0] select count(*),table_name from information_schema.views group by table_name having count(*)>1; ➤ count(*)[-5,64,0] ¦ table_name[12,-1,0] select count(*) from information_schema.partitions where table_schema='sys_db1' and table_name='test01';