diff --git a/go/vt/schemadiff/errors.go b/go/vt/schemadiff/errors.go index 42dd304e75a..d1c9adeec70 100644 --- a/go/vt/schemadiff/errors.go +++ b/go/vt/schemadiff/errors.go @@ -334,3 +334,32 @@ type ViewDependencyUnresolvedError struct { func (e *ViewDependencyUnresolvedError) Error() string { return fmt.Sprintf("view %s has unresolved/loop dependencies", sqlescape.EscapeID(e.View)) } + +type InvalidColumnReferencedInViewError struct { + View string + Column string + Ambiguous bool +} + +func (e *InvalidColumnReferencedInViewError) Error() string { + if e.Ambiguous { + return fmt.Sprintf("view %s references unqualified but non unique column %s", sqlescape.EscapeID(e.View), sqlescape.EscapeID(e.Column)) + } + return fmt.Sprintf("view %s references unqualified but non-existent column %s", sqlescape.EscapeID(e.View), sqlescape.EscapeID(e.Column)) +} + +type InvalidStarExprInViewError struct { + View string +} + +func (e *InvalidStarExprInViewError) Error() string { + return fmt.Sprintf("view %s has invalid star expression", sqlescape.EscapeID(e.View)) +} + +type EntityNotFoundError struct { + Name string +} + +func (e *EntityNotFoundError) Error() string { + return fmt.Sprintf("entity %s not found", sqlescape.EscapeID(e.Name)) +} diff --git a/go/vt/schemadiff/schema.go b/go/vt/schemadiff/schema.go index 0e9ae4c4df1..39ed20024cf 100644 --- a/go/vt/schemadiff/schema.go +++ b/go/vt/schemadiff/schema.go @@ -24,7 +24,11 @@ import ( "sort" "strings" + "vitess.io/vitess/go/vt/concurrency" + "vitess.io/vitess/go/vt/proto/vtrpc" "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vterrors" + "vitess.io/vitess/go/vt/vtgate/semantics" ) // Schema represents a database schema, which may contain entities such as tables and views. @@ -318,6 +322,11 @@ func (s *Schema) normalize() error { } } + // Validate views' referenced columns: do these columns actually exist in referenced tables/views? + if err := s.ValidateViewReferences(); err != nil { + return err + } + // Validate table definitions for _, t := range s.tables { if err := t.validate(); err != nil { @@ -761,3 +770,147 @@ func (s *Schema) Apply(diffs []EntityDiff) (*Schema, error) { } return dup, nil } + +func (s *Schema) ValidateViewReferences() error { + errs := &concurrency.AllErrorRecorder{} + schemaInformation := newDeclarativeSchemaInformation() + + // Remember that s.Entities() is already ordered by dependency. ie. tables first, then views + // that only depend on those tables (or on dual), then 2nd tier views, etc. + // Thus, the order of iteration below is valid and sufficient, to build + for _, e := range s.Entities() { + entityColumns, err := s.getEntityColumnNames(e.Name(), schemaInformation) + if err != nil { + errs.RecordError(err) + continue + } + schemaInformation.addTable(e.Name()) + for _, col := range entityColumns { + schemaInformation.addColumn(e.Name(), col.Lowered()) + } + } + + // Add dual table with no explicit columns for dual style expressions in views. + schemaInformation.addTable("dual") + + for _, view := range s.Views() { + sel := sqlparser.CloneSelectStatement(view.CreateView.Select) // Analyze(), below, rewrites the select; we don't want to actually modify the schema + _, err := semantics.Analyze(sel, semanticKS.Name, schemaInformation) + extractColName := func(arg any) string { + switch arg := arg.(type) { + case string: + return arg + case *sqlparser.ColName: + return arg.Name.String() + default: + return "" // unexpected + } + } + formalizeErr := func(err error) error { + if err == nil { + return nil + } + semErr, ok := err.(*semantics.Error) + if !ok { + return err + } + if len(semErr.Args()) == 0 { + return err + } + switch semErr.Code { + case semantics.AmbiguousColumn: + if colName := extractColName(semErr.Args()[0]); colName != "" { + return &InvalidColumnReferencedInViewError{ + View: view.Name(), + Column: colName, + Ambiguous: true, + } + } + case semantics.ColumnNotFound: + if colName := extractColName(semErr.Args()[0]); colName != "" { + return &InvalidColumnReferencedInViewError{ + View: view.Name(), + Column: colName, + } + } + } + return err + } + errs.RecordError(formalizeErr(err)) + } + return errs.AggrError(vterrors.Aggregate) +} + +// getEntityColumnNames returns the names of columns in given entity (either a table or a view) +func (s *Schema) getEntityColumnNames(entityName string, schemaInformation *declarativeSchemaInformation) ( + columnNames []*sqlparser.IdentifierCI, + err error, +) { + entity := s.Entity(entityName) + if entity == nil { + if strings.ToLower(entityName) == "dual" { + // this is fine. DUAL does not exist but is allowed + return nil, nil + } + return nil, &EntityNotFoundError{Name: entityName} + } + // The entity is either a table or a view + switch entity := entity.(type) { + case *CreateTableEntity: + return s.getTableColumnNames(entity), nil + case *CreateViewEntity: + return s.getViewColumnNames(entity, schemaInformation) + } + return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected entity type for %v", entityName) +} + +// getTableColumnNames returns the names of columns in given table. +func (s *Schema) getTableColumnNames(t *CreateTableEntity) (columnNames []*sqlparser.IdentifierCI) { + for _, c := range t.TableSpec.Columns { + columnNames = append(columnNames, &c.Name) + } + return columnNames +} + +// getViewColumnNames returns the names of aliased columns returned by a given view. +func (s *Schema) getViewColumnNames(v *CreateViewEntity, schemaInformation *declarativeSchemaInformation) ( + columnNames []*sqlparser.IdentifierCI, + err error, +) { + for _, node := range v.Select.GetColumns() { + switch node := node.(type) { + case *sqlparser.StarExpr: + if tableName := node.TableName.Name.String(); tableName != "" { + for _, col := range schemaInformation.Tables[tableName].Columns { + name := sqlparser.CloneRefOfIdentifierCI(&col.Name) + columnNames = append(columnNames, name) + } + } else { + dependentNames, err := getViewDependentTableNames(v.CreateView) + if err != nil { + return nil, err + } + // add all columns from all referenced tables and views + for _, entityName := range dependentNames { + if schemaInformation.Tables[entityName] != nil { // is nil for dual/DUAL + for _, col := range schemaInformation.Tables[entityName].Columns { + name := sqlparser.CloneRefOfIdentifierCI(&col.Name) + columnNames = append(columnNames, name) + } + } + } + } + if len(columnNames) == 0 { + return nil, &InvalidStarExprInViewError{View: v.Name()} + } + case *sqlparser.AliasedExpr: + ci := sqlparser.NewIdentifierCI(node.ColumnName()) + columnNames = append(columnNames, &ci) + } + } + + if err != nil { + return nil, err + } + return columnNames, nil +} diff --git a/go/vt/schemadiff/schema_test.go b/go/vt/schemadiff/schema_test.go index 1a24b862b1c..b32aca1a8f7 100644 --- a/go/vt/schemadiff/schema_test.go +++ b/go/vt/schemadiff/schema_test.go @@ -17,6 +17,7 @@ limitations under the License. package schemadiff import ( + "sort" "strings" "testing" @@ -36,7 +37,7 @@ var createQueries = []string{ "create table t5(id int)", "create view v2 as select * from v3, t2", "create view v1 as select * from v3", - "create view v3 as select * from t3 as t3", + "create view v3 as select *, id+1 as id_plus, id+2 from t3 as t3", "create view v0 as select 1 from DUAL", "create view v9 as select 1", } @@ -74,12 +75,12 @@ var expectSortedViewNames = []string{ "v6", // level 3 } -var toSQL = "CREATE TABLE `t1` (\n\t`id` int\n);\nCREATE TABLE `t2` (\n\t`id` int\n);\nCREATE TABLE `t3` (\n\t`id` int,\n\t`type` enum('foo', 'bar') NOT NULL DEFAULT 'foo'\n);\nCREATE TABLE `t5` (\n\t`id` int\n);\nCREATE VIEW `v0` AS SELECT 1 FROM `dual`;\nCREATE VIEW `v3` AS SELECT * FROM `t3` AS `t3`;\nCREATE VIEW `v9` AS SELECT 1 FROM `dual`;\nCREATE VIEW `v1` AS SELECT * FROM `v3`;\nCREATE VIEW `v2` AS SELECT * FROM `v3`, `t2`;\nCREATE VIEW `v4` AS SELECT * FROM `t2` AS `something_else`, `v3`;\nCREATE VIEW `v5` AS SELECT * FROM `t1`, (SELECT * FROM `v3`) AS `some_alias`;\nCREATE VIEW `v6` AS SELECT * FROM `v4`;\n" +var toSQL = "CREATE TABLE `t1` (\n\t`id` int\n);\nCREATE TABLE `t2` (\n\t`id` int\n);\nCREATE TABLE `t3` (\n\t`id` int,\n\t`type` enum('foo', 'bar') NOT NULL DEFAULT 'foo'\n);\nCREATE TABLE `t5` (\n\t`id` int\n);\nCREATE VIEW `v0` AS SELECT 1 FROM `dual`;\nCREATE VIEW `v3` AS SELECT *, `id` + 1 AS `id_plus`, `id` + 2 FROM `t3` AS `t3`;\nCREATE VIEW `v9` AS SELECT 1 FROM `dual`;\nCREATE VIEW `v1` AS SELECT * FROM `v3`;\nCREATE VIEW `v2` AS SELECT * FROM `v3`, `t2`;\nCREATE VIEW `v4` AS SELECT * FROM `t2` AS `something_else`, `v3`;\nCREATE VIEW `v5` AS SELECT * FROM `t1`, (SELECT * FROM `v3`) AS `some_alias`;\nCREATE VIEW `v6` AS SELECT * FROM `v4`;\n" func TestNewSchemaFromQueries(t *testing.T) { schema, err := NewSchemaFromQueries(createQueries) assert.NoError(t, err) - assert.NotNil(t, schema) + require.NotNil(t, schema) assert.Equal(t, expectSortedNames, schema.EntityNames()) assert.Equal(t, expectSortedTableNames, schema.TableNames()) @@ -89,7 +90,7 @@ func TestNewSchemaFromQueries(t *testing.T) { func TestNewSchemaFromSQL(t *testing.T) { schema, err := NewSchemaFromSQL(strings.Join(createQueries, ";")) assert.NoError(t, err) - assert.NotNil(t, schema) + require.NotNil(t, schema) assert.Equal(t, expectSortedNames, schema.EntityNames()) assert.Equal(t, expectSortedTableNames, schema.TableNames()) @@ -158,7 +159,7 @@ func TestNewSchemaFromQueriesLoop(t *testing.T) { func TestToSQL(t *testing.T) { schema, err := NewSchemaFromQueries(createQueries) assert.NoError(t, err) - assert.NotNil(t, schema) + require.NotNil(t, schema) sql := schema.ToSQL() assert.Equal(t, toSQL, sql) @@ -167,7 +168,7 @@ func TestToSQL(t *testing.T) { func TestCopy(t *testing.T) { schema, err := NewSchemaFromQueries(createQueries) assert.NoError(t, err) - assert.NotNil(t, schema) + require.NotNil(t, schema) schemaClone := schema.copy() assert.Equal(t, schema, schemaClone) @@ -398,3 +399,309 @@ func TestInvalidTableForeignKeyReference(t *testing.T) { assert.EqualError(t, err, (&ForeignKeyDependencyUnresolvedError{Table: "t11"}).Error()) } } + +func TestGetEntityColumnNames(t *testing.T) { + var queries = []string{ + "create table t1(id int, state int, some char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select id as id from t1", + "create view v2 as select 3+1 as `id`, state as state, some as thing from t1", + "create view v3 as select `id` as `id`, state as state, thing as another from v2", + "create view v4 as select 1 as `ok` from dual", + "create view v5 as select 1 as `ok` from DUAL", + "create view v6 as select ok as `ok` from v5", + "create view v7 as select * from t1", + "create view v8 as select * from v7", + "create view v9 as select * from v8, v6", + "create view va as select * from v6, v8", + "create view vb as select *, now() from v8", + } + + schema, err := NewSchemaFromQueries(queries) + require.NoError(t, err) + require.NotNil(t, schema) + + expectedColNames := map[string]([]string){ + "t1": []string{"id", "state", "some"}, + "t2": []string{"id", "c"}, + "v1": []string{"id"}, + "v2": []string{"id", "state", "thing"}, + "v3": []string{"id", "state", "another"}, + "v4": []string{"ok"}, + "v5": []string{"ok"}, + "v6": []string{"ok"}, + "v7": []string{"id", "state", "some"}, + "v8": []string{"id", "state", "some"}, + "v9": []string{"id", "state", "some", "ok"}, + "va": []string{"ok", "id", "state", "some"}, + "vb": []string{"id", "state", "some", "now()"}, + } + entities := schema.Entities() + require.Equal(t, len(entities), len(expectedColNames)) + + tcmap := newDeclarativeSchemaInformation() + // we test by order of dependency: + for _, e := range entities { + tbl := e.Name() + t.Run(tbl, func(t *testing.T) { + identifiers, err := schema.getEntityColumnNames(tbl, tcmap) + assert.NoError(t, err) + names := []string{} + for _, ident := range identifiers { + names = append(names, ident.String()) + } + // compare columns. We disregard order. + expectNames := expectedColNames[tbl][:] + sort.Strings(names) + sort.Strings(expectNames) + assert.Equal(t, expectNames, names) + // emulate the logic that fills known columns for known entities: + tcmap.addTable(tbl) + for _, name := range names { + tcmap.addColumn(tbl, name) + } + }) + } +} + +func TestViewReferences(t *testing.T) { + tt := []struct { + name string + queries []string + expectErr error + }{ + { + name: "valid", + queries: []string{ + "create table t1(id int, state int, some char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select id as id from t1", + "create view v2 as select 3+1 as `id`, state as state, some as thing from t1", + "create view v3 as select `id` as `id`, state as state, thing as another from v2", + "create view v4 as select 1 as `ok` from dual", + "create view v5 as select 1 as `ok` from DUAL", + "create view v6 as select ok as `ok` from v5", + }, + }, + { + name: "valid WHERE", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select c from t1 where id=3", + }, + }, + { + name: "invalid unqualified referenced column", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select unexpected from t1", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v1", Column: "unexpected"}, + }, + { + name: "invalid unqualified referenced column in where clause", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select 1 from t1 where unexpected=3", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v1", Column: "unexpected"}, + }, + { + name: "valid qualified", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select t1.c from t1 where t1.id=3", + }, + }, + { + name: "valid qualified, multiple tables", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select t1.c from t1, t2 where t2.id=3", + }, + }, + { + name: "invalid unqualified, multiple tables", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select c from t1, t2 where t2.id=3", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v1", Column: "c", Ambiguous: true}, + }, + { + name: "invalid unqualified in WHERE clause, multiple tables", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5))", + "create view v1 as select t2.c from t1, t2 where id=3", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v1", Column: "id", Ambiguous: true}, + }, + { + name: "valid unqualified, multiple tables", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5), only_in_t2 int)", + "create view v1 as select only_in_t2 from t1, t2 where t1.id=3", + }, + }, + { + name: "valid unqualified in WHERE clause, multiple tables", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, c char(5), only_in_t2 int)", + "create view v1 as select t1.id from t1, t2 where only_in_t2=3", + }, + }, + { + name: "valid cascaded views", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id, c from t1 where id > 0", + "create view v2 as select * from v1 where id > 0", + }, + }, + { + name: "valid cascaded views, column aliases", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id, c as ch from t1 where id > 0", + "create view v2 as select ch from v1 where id > 0", + }, + }, + { + name: "valid cascaded views, column aliases in WHERE", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id as counter, c as ch from t1 where id > 0", + "create view v2 as select ch from v1 where counter > 0", + }, + }, + { + name: "valid cascaded views, aliased expression", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id+1 as counter, c as ch from t1 where id > 0", + "create view v2 as select ch from v1 where counter > 0", + }, + }, + { + name: "valid cascaded views, non aliased expression", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id+1, c as ch from t1 where id > 0", + "create view v2 as select ch from v1 where `id + 1` > 0", + }, + }, + { + name: "cascaded views, invalid column aliases", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id, c as ch from t1 where id > 0", + "create view v2 as select c from v1 where id > 0", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v2", Column: "c"}, + }, + { + name: "cascaded views, column not in view", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id from t1 where c='x'", + "create view v2 as select c from v1 where id > 0", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v2", Column: "c"}, + }, + { + name: "complex cascade", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, n int, info int)", + "create view v1 as select id, c as ch from t1 where id > 0", + "create view v2 as select n as num, info from t2", + "create view v3 as select num, v1.id, ch from v1 join v2 using (id) where info > 5", + }, + }, + { + name: "valid dual", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select 1 from dual", + }, + }, + { + name: "invalid dual column", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select id from dual", + }, + expectErr: &InvalidColumnReferencedInViewError{View: "v1", Column: "id"}, + }, + { + name: "invalid dual star", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select * from dual", + }, + expectErr: &InvalidStarExprInViewError{View: "v1"}, + }, + { + name: "valid star", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select * from t1 where id > 0", + }, + }, + { + name: "valid star, cascaded", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create view v1 as select t1.* from t1 where id > 0", + "create view v2 as select * from v1 where id > 0", + }, + }, + { + name: "valid star, two tables, cascaded", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, ts timestamp)", + "create view v1 as select t1.* from t1, t2 where t1.id > 0", + "create view v2 as select * from v1 where c > 0", + }, + }, + { + name: "valid two star, two tables, cascaded", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, ts timestamp)", + "create view v1 as select t1.*, t2.* from t1, t2 where t1.id > 0", + "create view v2 as select * from v1 where c > 0 and ts is not null", + }, + }, + { + name: "valid unqualified star, cascaded", + queries: []string{ + "create table t1(id int primary key, c char(5))", + "create table t2(id int primary key, ts timestamp)", + "create view v1 as select * from t1, t2 where t1.id > 0", + "create view v2 as select * from v1 where c > 0 and ts is not null", + }, + }, + } + for _, ts := range tt { + t.Run(ts.name, func(t *testing.T) { + schema, err := NewSchemaFromQueries(ts.queries) + if ts.expectErr == nil { + require.NoError(t, err) + require.NotNil(t, schema) + } else { + require.Equal(t, ts.expectErr, err, "received error: %v", err) + } + }) + } +} diff --git a/go/vt/schemadiff/semantics.go b/go/vt/schemadiff/semantics.go new file mode 100644 index 00000000000..ef9017d3b25 --- /dev/null +++ b/go/vt/schemadiff/semantics.go @@ -0,0 +1,75 @@ +/* +Copyright 2023 The Vitess Authors. + +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 schemadiff + +import ( + "vitess.io/vitess/go/mysql/collations" + "vitess.io/vitess/go/vt/key" + topodatapb "vitess.io/vitess/go/vt/proto/topodata" + "vitess.io/vitess/go/vt/sqlparser" + "vitess.io/vitess/go/vt/vtgate/semantics" + "vitess.io/vitess/go/vt/vtgate/vindexes" +) + +// semanticKS is a bogus keyspace, used for consistency purposes. The name is not important +var semanticKS = &vindexes.Keyspace{ + Name: "ks", + Sharded: false, +} + +var _ semantics.SchemaInformation = (*declarativeSchemaInformation)(nil) + +// declarativeSchemaInformation is a utility wrapper arounf FakeSI, and adds a few utility functions +// to make it more simple and accessible to schemadiff's logic. +type declarativeSchemaInformation struct { + Tables map[string]*vindexes.Table +} + +func newDeclarativeSchemaInformation() *declarativeSchemaInformation { + return &declarativeSchemaInformation{ + Tables: make(map[string]*vindexes.Table), + } +} + +// FindTableOrVindex implements the SchemaInformation interface +func (si *declarativeSchemaInformation) FindTableOrVindex(tablename sqlparser.TableName) (*vindexes.Table, vindexes.Vindex, string, topodatapb.TabletType, key.Destination, error) { + table := si.Tables[sqlparser.String(tablename)] + return table, nil, "", 0, nil, nil +} + +func (si *declarativeSchemaInformation) ConnCollation() collations.ID { + return 45 +} + +// addTable adds a fake table with an empty column list +func (si *declarativeSchemaInformation) addTable(tableName string) { + tbl := &vindexes.Table{ + Name: sqlparser.NewIdentifierCS(tableName), + Columns: []vindexes.Column{}, + ColumnListAuthoritative: true, + Keyspace: semanticKS, + } + si.Tables[tableName] = tbl +} + +// addColumn adds a fake column with no type. It assumes the table already exists +func (si *declarativeSchemaInformation) addColumn(tableName string, columnName string) { + col := &vindexes.Column{ + Name: sqlparser.NewIdentifierCI(columnName), + } + si.Tables[tableName].Columns = append(si.Tables[tableName].Columns, *col) +} diff --git a/go/vt/vtgate/semantics/errors.go b/go/vt/vtgate/semantics/errors.go index 3088fe19430..72c40e22b0d 100644 --- a/go/vt/vtgate/semantics/errors.go +++ b/go/vt/vtgate/semantics/errors.go @@ -201,3 +201,7 @@ func (n *Error) ErrorCode() vtrpcpb.Code { return f.code } } + +func (n *Error) Args() []any { + return n.args +}