Repository: cloudspannerecosystem/memefish
Pull request #76 (2023-10-21 10:16:11)

Add support for ALTER INDEX statement
This PR adds support for [ALTER INDEX](https://cloud.google.com/spanner/docs/reference/standard-sql/data-definition-language#alter-index) statement.


Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 39b6857..295b21b 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -58,6 +58,7 @@ func (CreateView) isStatement()         {}
 func (CreateIndex) isStatement()        {}
 func (CreateRole) isStatement()         {}
 func (AlterTable) isStatement()         {}
+func (AlterIndex) isStatement()         {}
 func (DropTable) isStatement()          {}
 func (DropIndex) isStatement()          {}
 func (DropRole) isStatement()           {}
@@ -225,6 +226,7 @@ func (CreateSequence) isDDL()     {}
 func (AlterTable) isDDL()         {}
 func (DropTable) isDDL()          {}
 func (CreateIndex) isDDL()        {}
+func (AlterIndex) isDDL()         {}
 func (DropIndex) isDDL()          {}
 func (CreateRole) isDDL()         {}
 func (DropRole) isDDL()           {}
@@ -292,6 +294,15 @@ func (ScalarSchemaType) isSchemaType() {}
 func (SizedSchemaType) isSchemaType()  {}
 func (ArraySchemaType) isSchemaType()  {}
 
+// IndexAlternation represents ALTER INDEX action.
+type IndexAlternation interface {
+	Node
+	isIndexAlternation()
+}
+
+func (AddStoredColumn) isIndexAlternation()  {}
+func (DropStoredColumn) isIndexAlternation() {}
+
 // DML represents data manipulation language in SQL.
 //
 // https://cloud.google.com/spanner/docs/data-definition-language
@@ -1599,7 +1610,7 @@ type RowDeletionPolicy struct {
 // CreateView is CREATE VIEW statement node.
 //
 //	CREATE {{if .OrReplace}}OR REPLACE{{end}} VIEW {{.Name | sql}}
-//	SQL SECURITY INVOKER AS
+//	SQL SECURITY {{.SecurityType}} AS
 //	{{.Query | sql}}
 type CreateView struct {
 	// pos = Create
@@ -1607,9 +1618,10 @@ type CreateView struct {
 
 	Create token.Pos
 
-	Name      *Ident
-	OrReplace bool
-	Query     QueryExpr
+	Name         *Ident
+	OrReplace    bool
+	SecurityType SecurityType
+	Query        QueryExpr
 }
 
 // AlterTable is ALTER TABLE statement node.
@@ -1625,6 +1637,19 @@ type AlterTable struct {
 	TableAlternation TableAlternation
 }
 
+// AlterIndex is ALTER INDEX statement node.
+//
+//	ALTER INDEX {{.Name | sql}} {{.IndexAlternation | sql}}
+type AlterIndex struct {
+	// pos = Alter
+	// end = IndexAlternation.end
+
+	Alter token.Pos // position of "ALTER" keyword
+
+	Name             *Ident
+	IndexAlternation IndexAlternation
+}
+
 // AlterChangeStream is ALTER CHANGE STREAM statement node.
 //
 //	ALTER CHANGE STREAM {{.Name | sql}} {{.ChangeStreamAlternation | sql}}
@@ -1938,6 +1963,30 @@ type InterleaveIn struct {
 	TableName *Ident
 }
 
+// AddStoredColumn is ADD STORED COLUMN clause in ALTER INDEX.
+//
+//	ADD STORED COLUMN {{.Name | sql}}
+type AddStoredColumn struct {
+	// pos = Add
+	// end = Name.end
+
+	Add token.Pos // position of "ADD" keyword
+
+	Name *Ident
+}
+
+// DropStoredColumn is DROP STORED COLUMN clause in ALTER INDEX.
+//
+//	DROP STORED COLUMN {{.Name | sql}}
+type DropStoredColumn struct {
+	// pos = Drop
+	// end = Name.end
+
+	Drop token.Pos // position of "DROP" keyword
+
+	Name *Ident
+}
+
 // DropIndex is DROP INDEX statement node.
 //
 //	DROP INDEX {{if .IfExists}}IF EXISTS{{end}} {{.Name | sql}}
diff --git a/ast/const.go b/ast/const.go
index 7031583..57840e2 100644
--- a/ast/const.go
+++ b/ast/const.go
@@ -115,3 +115,10 @@ const (
 	OnDeleteCascade  OnDeleteAction = "ON DELETE CASCADE"
 	OnDeleteNoAction OnDeleteAction = "ON DELETE NO ACTION"
 )
+
+type SecurityType string
+
+const (
+	SecurityTypeInvoker SecurityType = "INVOKER"
+	SecurityTypeDefiner SecurityType = "DEFINER"
+)
diff --git a/ast/pos.go b/ast/pos.go
index c0d14c8..a6aaa40 100644
--- a/ast/pos.go
+++ b/ast/pos.go
@@ -692,6 +692,15 @@ func (s *Storing) End() token.Pos { return s.Rparen + 1 }
 func (i *InterleaveIn) Pos() token.Pos { return i.Comma }
 func (i *InterleaveIn) End() token.Pos { return i.TableName.End() }
 
+func (a *AlterIndex) Pos() token.Pos { return a.Alter }
+func (a *AlterIndex) End() token.Pos { return a.IndexAlternation.End() }
+
+func (a *AddStoredColumn) Pos() token.Pos { return a.Add }
+func (a *AddStoredColumn) End() token.Pos { return a.Name.End() }
+
+func (a *DropStoredColumn) Pos() token.Pos { return a.Drop }
+func (a *DropStoredColumn) End() token.Pos { return a.Name.End() }
+
 func (d *DropIndex) Pos() token.Pos { return d.Drop }
 func (d *DropIndex) End() token.Pos { return d.Name.End() }
 
diff --git a/ast/sql.go b/ast/sql.go
index 1750522..c31d71b 100644
--- a/ast/sql.go
+++ b/ast/sql.go
@@ -771,7 +771,7 @@ func (c *CreateView) SQL() string {
 	if c.OrReplace {
 		sql += " OR REPLACE"
 	}
-	sql += " VIEW " + c.Name.SQL() + " SQL SECURITY INVOKER AS " + c.Query.SQL()
+	sql += " VIEW " + c.Name.SQL() + " SQL SECURITY " + string(c.SecurityType) + " AS " + c.Query.SQL()
 	return sql
 }
 
@@ -1057,6 +1057,18 @@ func (i *InterleaveIn) SQL() string {
 	return ", INTERLEAVE IN " + i.TableName.SQL()
 }
 
+func (a *AlterIndex) SQL() string {
+	return "ALTER INDEX " + a.Name.SQL() + " " + a.IndexAlternation.SQL()
+}
+
+func (a *AddStoredColumn) SQL() string {
+	return "ADD STORED COLUMN " + a.Name.SQL()
+}
+
+func (a *DropStoredColumn) SQL() string {
+	return "DROP STORED COLUMN " + a.Name.SQL()
+}
+
 func (d *DropIndex) SQL() string {
 	sql := "DROP INDEX "
 	if d.IfExists {
diff --git a/parser.go b/parser.go
index 62e19c8..7648d90 100644
--- a/parser.go
+++ b/parser.go
@@ -1997,6 +1997,8 @@ func (p *Parser) parseDDL() ast.DDL {
 		switch {
 		case p.Token.IsKeywordLike("TABLE"):
 			return p.parseAlterTable(pos)
+		case p.Token.IsKeywordLike("INDEX"):
+			return p.parseAlterIndex(pos)
 		case p.Token.IsKeywordLike("CHANGE"):
 			return p.parseAlterChangeStream(pos)
 		}
@@ -2164,16 +2166,28 @@ func (p *Parser) parseCreateView(pos token.Pos) *ast.CreateView {
 
 	p.expectKeywordLike("SQL")
 	p.expectKeywordLike("SECURITY")
-	p.expectKeywordLike("INVOKER")
+
+	id := p.expect(token.TokenIdent)
+	var securityType ast.SecurityType
+	switch {
+	case id.IsIdent("INVOKER"):
+		securityType = ast.SecurityTypeInvoker
+	case id.IsIdent("DEFINER"):
+		securityType = ast.SecurityTypeDefiner
+	default:
+		p.panicfAtToken(id, "expected identifier: INVOKER, DEFINER, but: %s", id.Raw)
+	}
+
 	p.expect("AS")
 
 	query := p.parseQueryExpr()
 
 	return &ast.CreateView{
-		Create:    pos,
-		Name:      name,
-		OrReplace: orReplace,
-		Query:     query,
+		Create:       pos,
+		Name:         name,
+		OrReplace:    orReplace,
+		SecurityType: securityType,
+		Query:        query,
 	}
 }
 
@@ -2839,6 +2853,53 @@ func (p *Parser) parseAlterColumn() ast.TableAlternation {
 	}
 }
 
+func (p *Parser) parseAlterIndex(pos token.Pos) *ast.AlterIndex {
+	p.expectKeywordLike("INDEX")
+
+	name := p.parseIdent()
+
+	var alternation ast.IndexAlternation
+	switch {
+	case p.Token.IsKeywordLike("ADD"):
+		alternation = p.parseAddStoredColumn()
+	case p.Token.IsKeywordLike("DROP"):
+		alternation = p.parseDropStoredColumn()
+	default:
+		p.panicfAtToken(&p.Token, "expected pseudo keyword: ADD, DROP, but: %s", p.Token.AsString)
+	}
+
+	return &ast.AlterIndex{
+		Alter:            pos,
+		Name:             name,
+		IndexAlternation: alternation,
+	}
+}
+
+func (p *Parser) parseAddStoredColumn() ast.IndexAlternation {
+	pos := p.expectKeywordLike("ADD").Pos
+	p.expectKeywordLike("STORED")
+	p.expectKeywordLike("COLUMN")
+
+	name := p.parseIdent()
+
+	return &ast.AddStoredColumn{
+		Add:  pos,
+		Name: name,
+	}
+}
+
+func (p *Parser) parseDropStoredColumn() ast.IndexAlternation {
+	pos := p.expectKeywordLike("DROP").Pos
+	p.expectKeywordLike("STORED")
+	p.expectKeywordLike("COLUMN")
+
+	name := p.parseIdent()
+
+	return &ast.DropStoredColumn{
+		Drop: pos,
+		Name: name,
+	}
+}
 func (p *Parser) parseDropTable(pos token.Pos) *ast.DropTable {
 	p.expectKeywordLike("TABLE")
 	ifExists := p.parseIfExists()


Test patch:
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 8b70fbb..c9c1f99 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -13,6 +13,7 @@ func TestStatement(t *testing.T) {
 	Statement(&CreateSequence{}).isStatement()
 	Statement(&CreateRole{}).isStatement()
 	Statement(&AlterTable{}).isStatement()
+	Statement(&AlterIndex{}).isStatement()
 	Statement(&DropTable{}).isStatement()
 	Statement(&DropIndex{}).isStatement()
 	Statement(&DropRole{}).isStatement()
@@ -128,6 +129,7 @@ func TestDDL(t *testing.T) {
 	DDL(&AlterTable{}).isDDL()
 	DDL(&DropTable{}).isDDL()
 	DDL(&CreateIndex{}).isDDL()
+	DDL(&AlterIndex{}).isDDL()
 	DDL(&DropIndex{}).isDDL()
 	DDL(&CreateRole{}).isDDL()
 	DDL(&DropRole{}).isDDL()
@@ -170,6 +172,11 @@ func TestSchemaType(t *testing.T) {
 	SchemaType(&ArraySchemaType{}).isSchemaType()
 }
 
+func TestIndexAlternation(t *testing.T) {
+	IndexAlternation(&AddStoredColumn{}).isIndexAlternation()
+	IndexAlternation(&DropStoredColumn{}).isIndexAlternation()
+}
+
 func TestDML(t *testing.T) {
 	DML(&Insert{}).isDML()
 	DML(&Delete{}).isDML()
diff --git a/testdata/input/ddl/alter_index_add_stored_column.sql b/testdata/input/ddl/alter_index_add_stored_column.sql
new file mode 100644
index 0000000..0e8bdc3
--- /dev/null
+++ b/testdata/input/ddl/alter_index_add_stored_column.sql
@@ -0,0 +1,1 @@
+alter index foo add stored column bar
diff --git a/testdata/input/ddl/alter_index_drop_stored_column.sql b/testdata/input/ddl/alter_index_drop_stored_column.sql
new file mode 100644
index 0000000..306aae0
--- /dev/null
+++ b/testdata/input/ddl/alter_index_drop_stored_column.sql
@@ -0,0 +1,1 @@
+alter index foo drop stored column bar
diff --git a/testdata/input/ddl/create_view_sql_security_definer.sql b/testdata/input/ddl/create_view_sql_security_definer.sql
new file mode 100644
index 0000000..b9b8923
--- /dev/null
+++ b/testdata/input/ddl/create_view_sql_security_definer.sql
@@ -0,0 +1,6 @@
+create view singernames
+sql security definer
+as select
+    singers.singerid as singerid,
+    singers.firstname || ' ' || singers.lastname as name
+from singers
diff --git a/testdata/result/ddl/alter_index_add_stored_column.sql.txt b/testdata/result/ddl/alter_index_add_stored_column.sql.txt
new file mode 100644
index 0000000..558262b
--- /dev/null
+++ b/testdata/result/ddl/alter_index_add_stored_column.sql.txt
@@ -0,0 +1,23 @@
+--- alter_index_add_stored_column.sql
+alter index foo add stored column bar
+
+--- AST
+&ast.AlterIndex{
+  Alter: 0,
+  Name:  &ast.Ident{
+    NamePos: 12,
+    NameEnd: 15,
+    Name:    "foo",
+  },
+  IndexAlternation: &ast.AddStoredColumn{
+    Add:  16,
+    Name: &ast.Ident{
+      NamePos: 34,
+      NameEnd: 37,
+      Name:    "bar",
+    },
+  },
+}
+
+--- SQL
+ALTER INDEX foo ADD STORED COLUMN bar
diff --git a/testdata/result/ddl/alter_index_drop_stored_column.sql.txt b/testdata/result/ddl/alter_index_drop_stored_column.sql.txt
new file mode 100644
index 0000000..cbad680
--- /dev/null
+++ b/testdata/result/ddl/alter_index_drop_stored_column.sql.txt
@@ -0,0 +1,23 @@
+--- alter_index_drop_stored_column.sql
+alter index foo drop stored column bar
+
+--- AST
+&ast.AlterIndex{
+  Alter: 0,
+  Name:  &ast.Ident{
+    NamePos: 12,
+    NameEnd: 15,
+    Name:    "foo",
+  },
+  IndexAlternation: &ast.DropStoredColumn{
+    Drop: 16,
+    Name: &ast.Ident{
+      NamePos: 35,
+      NameEnd: 38,
+      Name:    "bar",
+    },
+  },
+}
+
+--- SQL
+ALTER INDEX foo DROP STORED COLUMN bar
diff --git a/testdata/result/ddl/create_or_replace_view.sql.txt b/testdata/result/ddl/create_or_replace_view.sql.txt
index a23a1ad..e2ffed9 100644
--- a/testdata/result/ddl/create_or_replace_view.sql.txt
+++ b/testdata/result/ddl/create_or_replace_view.sql.txt
@@ -14,8 +14,9 @@ from singers
     NameEnd: 34,
     Name:    "singernames",
   },
-  OrReplace: true,
-  Query:     &ast.Select{
+  OrReplace:    true,
+  SecurityType: "INVOKER",
+  Query:        &ast.Select{
     Select:   59,
     Distinct: false,
     AsStruct: false,
diff --git a/testdata/result/ddl/create_view.sql.txt b/testdata/result/ddl/create_view.sql.txt
index 3e93fcd..ecd9b7c 100644
--- a/testdata/result/ddl/create_view.sql.txt
+++ b/testdata/result/ddl/create_view.sql.txt
@@ -14,8 +14,9 @@ from singers
     NameEnd: 23,
     Name:    "singernames",
   },
-  OrReplace: false,
-  Query:     &ast.Select{
+  OrReplace:    false,
+  SecurityType: "INVOKER",
+  Query:        &ast.Select{
     Select:   48,
     Distinct: false,
     AsStruct: false,
diff --git a/testdata/result/ddl/create_view_sql_security_definer.sql.txt b/testdata/result/ddl/create_view_sql_security_definer.sql.txt
new file mode 100644
index 0000000..11c5a8a
--- /dev/null
+++ b/testdata/result/ddl/create_view_sql_security_definer.sql.txt
@@ -0,0 +1,120 @@
+--- create_view_sql_security_definer.sql
+create view singernames
+sql security definer
+as select
+    singers.singerid as singerid,
+    singers.firstname || ' ' || singers.lastname as name
+from singers
+
+--- AST
+&ast.CreateView{
+  Create: 0,
+  Name:   &ast.Ident{
+    NamePos: 12,
+    NameEnd: 23,
+    Name:    "singernames",
+  },
+  OrReplace:    false,
+  SecurityType: "DEFINER",
+  Query:        &ast.Select{
+    Select:   48,
+    Distinct: false,
+    AsStruct: false,
+    Results:  []ast.SelectItem{
+      &ast.Alias{
+        Expr: &ast.Path{
+          Idents: []*ast.Ident{
+            &ast.Ident{
+              NamePos: 59,
+              NameEnd: 66,
+              Name:    "singers",
+            },
+            &ast.Ident{
+              NamePos: 67,
+              NameEnd: 75,
+              Name:    "singerid",
+            },
+          },
+        },
+        As: &ast.AsAlias{
+          As:    -1,
+          Alias: &ast.Ident{
+            NamePos: 79,
+            NameEnd: 87,
+            Name:    "singerid",
+          },
+        },
+      },
+      &ast.Alias{
+        Expr: &ast.BinaryExpr{
+          Op:   "||",
+          Left: &ast.BinaryExpr{
+            Op:   "||",
+            Left: &ast.Path{
+              Idents: []*ast.Ident{
+                &ast.Ident{
+                  NamePos: 93,
+                  NameEnd: 100,
+                  Name:    "singers",
+                },
+                &ast.Ident{
+                  NamePos: 101,
+                  NameEnd: 110,
+                  Name:    "firstname",
+                },
+              },
+            },
+            Right: &ast.StringLiteral{
+              ValuePos: 114,
+              ValueEnd: 117,
+              Value:    " ",
+            },
+          },
+          Right: &ast.Path{
+            Idents: []*ast.Ident{
+              &ast.Ident{
+                NamePos: 121,
+                NameEnd: 128,
+                Name:    "singers",
+              },
+              &ast.Ident{
+                NamePos: 129,
+                NameEnd: 137,
+                Name:    "lastname",
+              },
+            },
+          },
+        },
+        As: &ast.AsAlias{
+          As:    -1,
+          Alias: &ast.Ident{
+            NamePos: 141,
+            NameEnd: 145,
+            Name:    "name",
+          },
+        },
+      },
+    },
+    From: &ast.From{
+      From:   146,
+      Source: &ast.TableName{
+        Table: &ast.Ident{
+          NamePos: 151,
+          NameEnd: 158,
+          Name:    "singers",
+        },
+        Hint:   (*ast.Hint)(nil),
+        As:     (*ast.AsAlias)(nil),
+        Sample: (*ast.TableSample)(nil),
+      },
+    },
+    Where:   (*ast.Where)(nil),
+    GroupBy: (*ast.GroupBy)(nil),
+    Having:  (*ast.Having)(nil),
+    OrderBy: (*ast.OrderBy)(nil),
+    Limit:   (*ast.Limit)(nil),
+  },
+}
+
+--- SQL
+CREATE VIEW singernames SQL SECURITY DEFINER AS SELECT singers.singerid AS singerid, singers.firstname || " " || singers.lastname AS name FROM singers
diff --git a/testdata/result/statement/alter_index_add_stored_column.sql.txt b/testdata/result/statement/alter_index_add_stored_column.sql.txt
new file mode 100644
index 0000000..558262b
--- /dev/null
+++ b/testdata/result/statement/alter_index_add_stored_column.sql.txt
@@ -0,0 +1,23 @@
+--- alter_index_add_stored_column.sql
+alter index foo add stored column bar
+
+--- AST
+&ast.AlterIndex{
+  Alter: 0,
+  Name:  &ast.Ident{
+    NamePos: 12,
+    NameEnd: 15,
+    Name:    "foo",
+  },
+  IndexAlternation: &ast.AddStoredColumn{
+    Add:  16,
+    Name: &ast.Ident{
+      NamePos: 34,
+      NameEnd: 37,
+      Name:    "bar",
+    },
+  },
+}
+
+--- SQL
+ALTER INDEX foo ADD STORED COLUMN bar
diff --git a/testdata/result/statement/alter_index_drop_stored_column.sql.txt b/testdata/result/statement/alter_index_drop_stored_column.sql.txt
new file mode 100644
index 0000000..cbad680
--- /dev/null
+++ b/testdata/result/statement/alter_index_drop_stored_column.sql.txt
@@ -0,0 +1,23 @@
+--- alter_index_drop_stored_column.sql
+alter index foo drop stored column bar
+
+--- AST
+&ast.AlterIndex{
+  Alter: 0,
+  Name:  &ast.Ident{
+    NamePos: 12,
+    NameEnd: 15,
+    Name:    "foo",
+  },
+  IndexAlternation: &ast.DropStoredColumn{
+    Drop: 16,
+    Name: &ast.Ident{
+      NamePos: 35,
+      NameEnd: 38,
+      Name:    "bar",
+    },
+  },
+}
+
+--- SQL
+ALTER INDEX foo DROP STORED COLUMN bar
diff --git a/testdata/result/statement/create_or_replace_view.sql.txt b/testdata/result/statement/create_or_replace_view.sql.txt
index a23a1ad..e2ffed9 100644
--- a/testdata/result/statement/create_or_replace_view.sql.txt
+++ b/testdata/result/statement/create_or_replace_view.sql.txt
@@ -14,8 +14,9 @@ from singers
     NameEnd: 34,
     Name:    "singernames",
   },
-  OrReplace: true,
-  Query:     &ast.Select{
+  OrReplace:    true,
+  SecurityType: "INVOKER",
+  Query:        &ast.Select{
     Select:   59,
     Distinct: false,
     AsStruct: false,
diff --git a/testdata/result/statement/create_view.sql.txt b/testdata/result/statement/create_view.sql.txt
index 3e93fcd..ecd9b7c 100644
--- a/testdata/result/statement/create_view.sql.txt
+++ b/testdata/result/statement/create_view.sql.txt
@@ -14,8 +14,9 @@ from singers
     NameEnd: 23,
     Name:    "singernames",
   },
-  OrReplace: false,
-  Query:     &ast.Select{
+  OrReplace:    false,
+  SecurityType: "INVOKER",
+  Query:        &ast.Select{
     Select:   48,
     Distinct: false,
     AsStruct: false,
diff --git a/testdata/result/statement/create_view_sql_security_definer.sql.txt b/testdata/result/statement/create_view_sql_security_definer.sql.txt
new file mode 100644
index 0000000..11c5a8a
--- /dev/null
+++ b/testdata/result/statement/create_view_sql_security_definer.sql.txt
@@ -0,0 +1,120 @@
+--- create_view_sql_security_definer.sql
+create view singernames
+sql security definer
+as select
+    singers.singerid as singerid,
+    singers.firstname || ' ' || singers.lastname as name
+from singers
+
+--- AST
+&ast.CreateView{
+  Create: 0,
+  Name:   &ast.Ident{
+    NamePos: 12,
+    NameEnd: 23,
+    Name:    "singernames",
+  },
+  OrReplace:    false,
+  SecurityType: "DEFINER",
+  Query:        &ast.Select{
+    Select:   48,
+    Distinct: false,
+    AsStruct: false,
+    Results:  []ast.SelectItem{
+      &ast.Alias{
+        Expr: &ast.Path{
+          Idents: []*ast.Ident{
+            &ast.Ident{
+              NamePos: 59,
+              NameEnd: 66,
+              Name:    "singers",
+            },
+            &ast.Ident{
+              NamePos: 67,
+              NameEnd: 75,
+              Name:    "singerid",
+            },
+          },
+        },
+        As: &ast.AsAlias{
+          As:    -1,
+          Alias: &ast.Ident{
+            NamePos: 79,
+            NameEnd: 87,
+            Name:    "singerid",
+          },
+        },
+      },
+      &ast.Alias{
+        Expr: &ast.BinaryExpr{
+          Op:   "||",
+          Left: &ast.BinaryExpr{
+            Op:   "||",
+            Left: &ast.Path{
+              Idents: []*ast.Ident{
+                &ast.Ident{
+                  NamePos: 93,
+                  NameEnd: 100,
+                  Name:    "singers",
+                },
+                &ast.Ident{
+                  NamePos: 101,
+                  NameEnd: 110,
+                  Name:    "firstname",
+                },
+              },
+            },
+            Right: &ast.StringLiteral{
+              ValuePos: 114,
+              ValueEnd: 117,
+              Value:    " ",
+            },
+          },
+          Right: &ast.Path{
+            Idents: []*ast.Ident{
+              &ast.Ident{
+                NamePos: 121,
+                NameEnd: 128,
+                Name:    "singers",
+              },
+              &ast.Ident{
+                NamePos: 129,
+                NameEnd: 137,
+                Name:    "lastname",
+              },
+            },
+          },
+        },
+        As: &ast.AsAlias{
+          As:    -1,
+          Alias: &ast.Ident{
+            NamePos: 141,
+            NameEnd: 145,
+            Name:    "name",
+          },
+        },
+      },
+    },
+    From: &ast.From{
+      From:   146,
+      Source: &ast.TableName{
+        Table: &ast.Ident{
+          NamePos: 151,
+          NameEnd: 158,
+          Name:    "singers",
+        },
+        Hint:   (*ast.Hint)(nil),
+        As:     (*ast.AsAlias)(nil),
+        Sample: (*ast.TableSample)(nil),
+      },
+    },
+    Where:   (*ast.Where)(nil),
+    GroupBy: (*ast.GroupBy)(nil),
+    Having:  (*ast.Having)(nil),
+    OrderBy: (*ast.OrderBy)(nil),
+    Limit:   (*ast.Limit)(nil),
+  },
+}
+
+--- SQL
+CREATE VIEW singernames SQL SECURITY DEFINER AS SELECT singers.singerid AS singerid, singers.firstname || " " || singers.lastname AS name FROM singers