Repository: sqlc-dev/sqlc
Pull request #3728 (2024-11-28 08:04:36)

fix(postgresql): Support renaming and dropping materialized views
solving the issue here https://github.com/sqlc-dev/sqlc/issues/3371

basically if there are create mv - rename mv - drop mv, then on drop mv statement, the sqlc generate will fail because mv involved in rename statement is not registered to the list of statements, in short make them invisible to drop mv operations.

before
```
$ sqlc-dev generate -f internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/sqlc.yaml
# package querytest
schema.sql:1:1: relation "authors_mv_old" does not exist
```

after (no error)
```
$ sqlc-dev generate -f internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/sqlc.yaml

```

swallowstalker: hi @kyleconroy, as this is my first PR in this repo, let me know if i needed to add some additional testing for this (i'm unable to assign you as reviewer in this PR, maybe its maintainer only)

Patch:
diff --git a/docs/howto/vet.md b/docs/howto/vet.md
index ff6ff3ceb..3f6c6a025 100644
--- a/docs/howto/vet.md
+++ b/docs/howto/vet.md
@@ -259,7 +259,24 @@ rules:
 ### Opting-out of lint rules
 
 For any query, you can tell `sqlc vet` not to evaluate lint rules using the
-`@sqlc-vet-disable` query annotation.
+`@sqlc-vet-disable` query annotation. The annotation accepts a list of rules to ignore.
+
+```sql
+/* name: GetAuthor :one */
+/* @sqlc-vet-disable sqlc/db-prepare no-pg */
+SELECT * FROM authors
+WHERE id = ? LIMIT 1;
+```
+The rules can also be split across lines.
+```sql
+/* name: GetAuthor :one */
+/* @sqlc-vet-disable sqlc/db-prepare */
+/* @sqlc-vet-disable no-pg */
+SELECT * FROM authors
+WHERE id = ? LIMIT 1;
+```
+
+To skip all rules for a query, you can provide the `@sqlc-vet-disable` annotation without any parameters. 
 
 ```sql
 /* name: GetAuthor :one */
diff --git a/go.mod b/go.mod
index 9a9eadf6e..151071b32 100644
--- a/go.mod
+++ b/go.mod
@@ -19,14 +19,14 @@ require (
 	github.com/riza-io/grpc-go v0.2.0
 	github.com/spf13/cobra v1.8.1
 	github.com/spf13/pflag v1.0.5
-	github.com/tetratelabs/wazero v1.8.1
+	github.com/tetratelabs/wazero v1.8.2
 	github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc
 	github.com/xeipuuv/gojsonschema v1.2.0
 	golang.org/x/sync v0.9.0
 	google.golang.org/grpc v1.68.0
 	google.golang.org/protobuf v1.35.2
 	gopkg.in/yaml.v3 v3.0.1
-	modernc.org/sqlite v1.34.1
+	modernc.org/sqlite v1.34.2
 )
 
 require (
diff --git a/go.sum b/go.sum
index 867c1b049..a7b687b2c 100644
--- a/go.sum
+++ b/go.sum
@@ -178,8 +178,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
-github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
+github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4=
+github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
 github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc h1:Hgim1Xgk1+viV7p0aZh9OOrMRfG+E4mGA+JsI2uB0+k=
 github.com/wasilibs/go-pgquery v0.0.0-20240606042535-c0843d6592cc/go.mod h1:ah6UfXIl/oA0K3SbourB/UHggVJOBXwPZ2XudDmmFac=
 github.com/wasilibs/wazero-helpers v0.0.0-20240604052452-61d7981e9a38 h1:RBu75fhabyxyGJ2zhkoNuRyObBMhVeMoXqmeaPTg2CQ=
@@ -333,8 +333,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
 modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
 modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
 modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk=
-modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
+modernc.org/sqlite v1.34.2 h1:J9n76TPsfYYkFkZ9Uy1QphILYifiVEwwOT7yP5b++2Y=
+modernc.org/sqlite v1.34.2/go.mod h1:dnR723UrTtjKpoHCAMN0Q/gZ9MT4r+iRvIBb9umWFkU=
 modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
 modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
diff --git a/internal/cmd/vet.go b/internal/cmd/vet.go
index fe3f3b6bd..8a7f8077d 100644
--- a/internal/cmd/vet.go
+++ b/internal/cmd/vet.go
@@ -6,11 +6,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/sqlc-dev/sqlc/internal/constants"
 	"io"
 	"log"
 	"os"
 	"path/filepath"
 	"runtime/trace"
+	"slices"
 	"strings"
 	"time"
 
@@ -37,9 +39,6 @@ var ErrFailedChecks = errors.New("failed checks")
 
 var pjson = protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
 
-const RuleDbPrepare = "sqlc/db-prepare"
-const QueryFlagSqlcVetDisable = "@sqlc-vet-disable"
-
 func NewCmdVet() *cobra.Command {
 	return &cobra.Command{
 		Use:   "vet",
@@ -109,7 +108,7 @@ func Vet(ctx context.Context, dir, filename string, opts *Options) error {
 	}
 
 	rules := map[string]rule{
-		RuleDbPrepare: {NeedsPrepare: true},
+		constants.QueryRuleDbPrepare: {NeedsPrepare: true},
 	}
 
 	for _, c := range conf.Rules {
@@ -538,11 +537,23 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
 	req := codeGenRequest(result, combo)
 	cfg := vetConfig(req)
 	for i, query := range req.Queries {
-		if result.Queries[i].Metadata.Flags[QueryFlagSqlcVetDisable] {
-			if debug.Active {
-				log.Printf("Skipping vet rules for query: %s\n", query.Name)
+		md := result.Queries[i].Metadata
+		if md.Flags[constants.QueryFlagSqlcVetDisable] {
+			// If the vet disable flag is specified without any rules listed, all rules are ignored.
+			if len(md.RuleSkiplist) == 0 {
+				if debug.Active {
+					log.Printf("Skipping all vet rules for query: %s\n", query.Name)
+				}
+				continue
+			}
+
+			// Rules which are listed to be disabled but not declared in the config file are rejected.
+			for r := range md.RuleSkiplist {
+				if !slices.Contains(s.Rules, r) {
+					fmt.Fprintf(c.Stderr, "%s: %s: rule-check error: rule %q does not exist in the config file\n", query.Filename, query.Name, r)
+					errored = true
+				}
 			}
-			continue
 		}
 
 		evalMap := map[string]any{
@@ -551,74 +562,81 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
 		}
 
 		for _, name := range s.Rules {
-			rule, ok := c.Rules[name]
-			if !ok {
-				return fmt.Errorf("type-check error: a rule with the name '%s' does not exist", name)
-			}
-
-			if rule.NeedsPrepare {
-				if prep == nil {
-					fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: database connection required\n", query.Filename, query.Name, name)
-					errored = true
-					continue
+			if _, skip := md.RuleSkiplist[name]; skip {
+				if debug.Active {
+					log.Printf("Skipping vet rule %q for query: %s\n", name, query.Name)
 				}
-				prepName := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i)
-				if err := prep.Prepare(ctx, prepName, query.Text); err != nil {
-					fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: %s\n", query.Filename, query.Name, name, err)
-					errored = true
-					continue
+			} else {
+				rule, ok := c.Rules[name]
+				if !ok {
+					return fmt.Errorf("type-check error: a rule with the name '%s' does not exist", name)
 				}
-			}
-
-			// short-circuit for "sqlc/db-prepare" rule which doesn't have a CEL program
-			if rule.Program == nil {
-				continue
-			}
 
-			// Get explain output for this query if we need it
-			_, pgsqlOK := evalMap["postgresql"]
-			_, mysqlOK := evalMap["mysql"]
-			if rule.NeedsExplain && !(pgsqlOK || mysqlOK) {
-				if expl == nil {
-					fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: database connection required\n", query.Filename, query.Name, name)
-					errored = true
-					continue
+				if rule.NeedsPrepare {
+					if prep == nil {
+						fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: database connection required\n", query.Filename, query.Name, name)
+						errored = true
+						continue
+					}
+					prepName := fmt.Sprintf("sqlc_vet_%d_%d", time.Now().Unix(), i)
+					if err := prep.Prepare(ctx, prepName, query.Text); err != nil {
+						fmt.Fprintf(c.Stderr, "%s: %s: %s: error preparing query: %s\n", query.Filename, query.Name, name, err)
+						errored = true
+						continue
+					}
 				}
-				engineOutput, err := expl.Explain(ctx, query.Text, query.Params...)
-				if err != nil {
-					fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: %s\n", query.Filename, query.Name, name, err)
-					errored = true
+
+				// short-circuit for "sqlc/db-prepare" rule which doesn't have a CEL program
+				if rule.Program == nil {
 					continue
 				}
 
-				evalMap["postgresql"] = engineOutput.PostgreSQL
-				evalMap["mysql"] = engineOutput.MySQL
-			}
+				// Get explain output for this query if we need it
+				_, pgsqlOK := evalMap["postgresql"]
+				_, mysqlOK := evalMap["mysql"]
+				if rule.NeedsExplain && !(pgsqlOK || mysqlOK) {
+					if expl == nil {
+						fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: database connection required\n", query.Filename, query.Name, name)
+						errored = true
+						continue
+					}
+					engineOutput, err := expl.Explain(ctx, query.Text, query.Params...)
+					if err != nil {
+						fmt.Fprintf(c.Stderr, "%s: %s: %s: error explaining query: %s\n", query.Filename, query.Name, name, err)
+						errored = true
+						continue
+					}
+
+					evalMap["postgresql"] = engineOutput.PostgreSQL
+					evalMap["mysql"] = engineOutput.MySQL
+				}
 
-			if debug.Debug.DumpVetEnv {
-				fmt.Printf("vars for rule '%s' evaluating against query '%s':\n", name, query.Name)
-				debug.DumpAsJSON(evalMap)
-			}
+				if debug.Debug.DumpVetEnv {
+					fmt.Printf("vars for rule '%s' evaluating against query '%s':\n", name, query.Name)
+					debug.DumpAsJSON(evalMap)
+				}
 
-			out, _, err := (*rule.Program).Eval(evalMap)
-			if err != nil {
-				return err
-			}
-			tripped, ok := out.Value().(bool)
-			if !ok {
-				return fmt.Errorf("expression returned non-bool value: %v", out.Value())
-			}
-			if tripped {
-				// TODO: Get line numbers in the output
-				if rule.Message == "" {
-					fmt.Fprintf(c.Stderr, "%s: %s: %s\n", query.Filename, query.Name, name)
-				} else {
-					fmt.Fprintf(c.Stderr, "%s: %s: %s: %s\n", query.Filename, query.Name, name, rule.Message)
+				out, _, err := (*rule.Program).Eval(evalMap)
+				if err != nil {
+					return err
+				}
+				tripped, ok := out.Value().(bool)
+				if !ok {
+					return fmt.Errorf("expression returned non-bool value: %v", out.Value())
+				}
+				if tripped {
+					// TODO: Get line numbers in the output
+					if rule.Message == "" {
+						fmt.Fprintf(c.Stderr, "%s: %s: %s\n", query.Filename, query.Name, name)
+					} else {
+						fmt.Fprintf(c.Stderr, "%s: %s: %s: %s\n", query.Filename, query.Name, name, rule.Message)
+					}
+					errored = true
 				}
-				errored = true
 			}
 		}
 	}
+
 	if errored {
 		return ErrFailedChecks
 	}
diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go
index 022d23ea2..681d29112 100644
--- a/internal/compiler/parse.go
+++ b/internal/compiler/parse.go
@@ -65,7 +65,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
 		return nil, err
 	}
 
-	md.Params, md.Flags, err = metadata.ParseParamsAndFlags(cleanedComments)
+	md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/constants/query.go b/internal/constants/query.go
new file mode 100644
index 000000000..a572c56c6
--- /dev/null
+++ b/internal/constants/query.go
@@ -0,0 +1,12 @@
+package constants
+
+// Flags
+const (
+	QueryFlagParam          = "@param"
+	QueryFlagSqlcVetDisable = "@sqlc-vet-disable"
+)
+
+// Rules
+const (
+	QueryRuleDbPrepare = "sqlc/db-prepare"
+)
diff --git a/internal/engine/postgresql/parse.go b/internal/engine/postgresql/parse.go
index 659f1011d..d1082bb7f 100644
--- a/internal/engine/postgresql/parse.go
+++ b/internal/engine/postgresql/parse.go
@@ -617,7 +617,7 @@ func translate(node *nodes.Node) (ast.Node, error) {
 				MissingOk: n.MissingOk,
 			}, nil
 
-		case nodes.ObjectType_OBJECT_TABLE:
+		case nodes.ObjectType_OBJECT_TABLE, nodes.ObjectType_OBJECT_MATVIEW, nodes.ObjectType_OBJECT_VIEW:
 			rel := parseRelationFromRangeVar(n.Relation)
 			return &ast.RenameTableStmt{
 				Table:     rel.TableName(),
diff --git a/internal/metadata/meta.go b/internal/metadata/meta.go
index 97ff36dbd..8f63624d2 100644
--- a/internal/metadata/meta.go
+++ b/internal/metadata/meta.go
@@ -3,6 +3,7 @@ package metadata
 import (
 	"bufio"
 	"fmt"
+	"github.com/sqlc-dev/sqlc/internal/constants"
 	"strings"
 	"unicode"
 
@@ -18,6 +19,10 @@ type Metadata struct {
 	Params   map[string]string
 	Flags    map[string]bool
 
+	// RuleSkiplist contains the names of rules to disable vetting for.
+	// If the map is empty, but the disable vet flag is specified, then all rules are ignored.
+	RuleSkiplist map[string]struct{}
+
 	Filename string
 }
 
@@ -113,9 +118,12 @@ func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string
 	return "", "", nil
 }
 
-func ParseParamsAndFlags(comments []string) (map[string]string, map[string]bool, error) {
+// ParseCommentFlags processes the comments provided with queries to determine the metadata params, flags and rules to skip.
+// All flags in query comments are prefixed with `@`, e.g. @param, @@sqlc-vet-disable.
+func ParseCommentFlags(comments []string) (map[string]string, map[string]bool, map[string]struct{}, error) {
 	params := make(map[string]string)
 	flags := make(map[string]bool)
+	ruleSkiplist := make(map[string]struct{})
 
 	for _, line := range comments {
 		s := bufio.NewScanner(strings.NewReader(line))
@@ -129,7 +137,7 @@ func ParseParamsAndFlags(comments []string) (map[string]string, map[string]bool,
 		}
 
 		switch token {
-		case "@param":
+		case constants.QueryFlagParam:
 			s.Scan()
 			name := s.Text()
 			var rest []string
@@ -138,14 +146,27 @@ func ParseParamsAndFlags(comments []string) (map[string]string, map[string]bool,
 				rest = append(rest, paramToken)
 			}
 			params[name] = strings.Join(rest, " ")
+
+		case constants.QueryFlagSqlcVetDisable:
+			flags[token] = true
+
+			// Vet rules can all be disabled in the same line or split across lines .i.e.
+			// /* @sqlc-vet-disable sqlc/db-prepare delete-without-where */
+			// is equivalent to:
+			// /* @sqlc-vet-disable sqlc/db-prepare */
+			// /* @sqlc-vet-disable delete-without-where */
+			for s.Scan() {
+				ruleSkiplist[s.Text()] = struct{}{}
+			}
+
 		default:
 			flags[token] = true
 		}
 
 		if s.Err() != nil {
-			return params, flags, s.Err()
+			return params, flags, ruleSkiplist, s.Err()
 		}
 	}
 
-	return params, flags, nil
+	return params, flags, ruleSkiplist, nil
 }


Test patch:
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/issue.md b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/issue.md
new file mode 100644
index 000000000..6b8f89e93
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/issue.md
@@ -0,0 +1,1 @@
+https://github.com/sqlc-dev/sqlc/issues/3371
\ No newline at end of file
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/exec.json b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/exec.json
new file mode 100644
index 000000000..ee1b7ecd9
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/exec.json
@@ -0,0 +1,3 @@
+{
+  "contexts": ["managed-db"]
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/db.go b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/db.go
new file mode 100644
index 000000000..58779be81
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/db.go
@@ -0,0 +1,32 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.27.0
+
+package querytest
+
+import (
+	"context"
+
+	"github.com/jackc/pgconn"
+	"github.com/jackc/pgx/v4"
+)
+
+type DBTX interface {
+	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
+	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
+	QueryRow(context.Context, string, ...interface{}) pgx.Row
+}
+
+func New(db DBTX) *Queries {
+	return &Queries{db: db}
+}
+
+type Queries struct {
+	db DBTX
+}
+
+func (q *Queries) WithTx(tx pgx.Tx) *Queries {
+	return &Queries{
+		db: tx,
+	}
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/models.go b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/models.go
new file mode 100644
index 000000000..7a887cc21
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/models.go
@@ -0,0 +1,21 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.27.0
+
+package querytest
+
+import (
+	"database/sql"
+)
+
+type Author struct {
+	ID   int64
+	Name string
+	Bio  sql.NullString
+}
+
+type AuthorsMv struct {
+	ID   int64
+	Name string
+	Bio  sql.NullString
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/query.sql.go b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/query.sql.go
new file mode 100644
index 000000000..d7bb7ae43
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/go/query.sql.go
@@ -0,0 +1,22 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.27.0
+// source: query.sql
+
+package querytest
+
+import (
+	"context"
+)
+
+const getAuthorMv = `-- name: GetAuthorMv :one
+SELECT id, name, bio FROM authors_mv
+WHERE id = $1 LIMIT 1
+`
+
+func (q *Queries) GetAuthorMv(ctx context.Context, id int64) (AuthorsMv, error) {
+	row := q.db.QueryRow(ctx, getAuthorMv, id)
+	var i AuthorsMv
+	err := row.Scan(&i.ID, &i.Name, &i.Bio)
+	return i, err
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/query.sql b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/query.sql
new file mode 100644
index 000000000..752a6f4d4
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/query.sql
@@ -0,0 +1,3 @@
+-- name: GetAuthorMv :one
+SELECT * FROM authors_mv
+WHERE id = $1 LIMIT 1;
\ No newline at end of file
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/schema.sql b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/schema.sql
new file mode 100644
index 000000000..3f88871f9
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/schema.sql
@@ -0,0 +1,18 @@
+CREATE TABLE authors (
+    id   BIGSERIAL PRIMARY KEY,
+    name text      NOT NULL,
+    bio  text
+);
+
+CREATE MATERIALIZED VIEW authors_mv AS (
+  SELECT * FROM authors
+);
+
+CREATE MATERIALIZED VIEW authors_mv_new AS (
+  SELECT * FROM authors
+);
+
+ALTER MATERIALIZED VIEW authors_mv RENAME TO authors_mv_old;
+ALTER MATERIALIZED VIEW authors_mv_new RENAME TO authors_mv;
+
+DROP MATERIALIZED VIEW authors_mv_old;
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/sqlc.yaml b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/sqlc.yaml
new file mode 100644
index 000000000..3440c3f11
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v4/sqlc.yaml
@@ -0,0 +1,10 @@
+version: "2"
+sql:
+  - engine: "postgresql"
+    schema: "schema.sql"
+    queries: "query.sql"
+    gen:
+      go:
+        package: "querytest"
+        out: "go"
+        sql_package: "pgx/v4"
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/exec.json b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/exec.json
new file mode 100644
index 000000000..ee1b7ecd9
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/exec.json
@@ -0,0 +1,3 @@
+{
+  "contexts": ["managed-db"]
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/db.go b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/db.go
new file mode 100644
index 000000000..69ef00154
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/db.go
@@ -0,0 +1,32 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.27.0
+
+package querytest
+
+import (
+	"context"
+
+	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgconn"
+)
+
+type DBTX interface {
+	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
+	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
+	QueryRow(context.Context, string, ...interface{}) pgx.Row
+}
+
+func New(db DBTX) *Queries {
+	return &Queries{db: db}
+}
+
+type Queries struct {
+	db DBTX
+}
+
+func (q *Queries) WithTx(tx pgx.Tx) *Queries {
+	return &Queries{
+		db: tx,
+	}
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/models.go b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/models.go
new file mode 100644
index 000000000..84c6ec48e
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/models.go
@@ -0,0 +1,21 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.27.0
+
+package querytest
+
+import (
+	"github.com/jackc/pgx/v5/pgtype"
+)
+
+type Author struct {
+	ID   int64
+	Name string
+	Bio  pgtype.Text
+}
+
+type AuthorsMv struct {
+	ID   int64
+	Name string
+	Bio  pgtype.Text
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/query.sql.go b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/query.sql.go
new file mode 100644
index 000000000..d7bb7ae43
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/go/query.sql.go
@@ -0,0 +1,22 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.27.0
+// source: query.sql
+
+package querytest
+
+import (
+	"context"
+)
+
+const getAuthorMv = `-- name: GetAuthorMv :one
+SELECT id, name, bio FROM authors_mv
+WHERE id = $1 LIMIT 1
+`
+
+func (q *Queries) GetAuthorMv(ctx context.Context, id int64) (AuthorsMv, error) {
+	row := q.db.QueryRow(ctx, getAuthorMv, id)
+	var i AuthorsMv
+	err := row.Scan(&i.ID, &i.Name, &i.Bio)
+	return i, err
+}
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/query.sql b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/query.sql
new file mode 100644
index 000000000..752a6f4d4
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/query.sql
@@ -0,0 +1,3 @@
+-- name: GetAuthorMv :one
+SELECT * FROM authors_mv
+WHERE id = $1 LIMIT 1;
\ No newline at end of file
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/schema.sql b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/schema.sql
new file mode 100644
index 000000000..3f88871f9
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/schema.sql
@@ -0,0 +1,18 @@
+CREATE TABLE authors (
+    id   BIGSERIAL PRIMARY KEY,
+    name text      NOT NULL,
+    bio  text
+);
+
+CREATE MATERIALIZED VIEW authors_mv AS (
+  SELECT * FROM authors
+);
+
+CREATE MATERIALIZED VIEW authors_mv_new AS (
+  SELECT * FROM authors
+);
+
+ALTER MATERIALIZED VIEW authors_mv RENAME TO authors_mv_old;
+ALTER MATERIALIZED VIEW authors_mv_new RENAME TO authors_mv;
+
+DROP MATERIALIZED VIEW authors_mv_old;
diff --git a/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/sqlc.yaml b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/sqlc.yaml
new file mode 100644
index 000000000..5dc63e3f9
--- /dev/null
+++ b/internal/endtoend/testdata/ddl_rename_drop_materialized_views/postgresql/pgx/v5/sqlc.yaml
@@ -0,0 +1,10 @@
+version: "2"
+sql:
+  - engine: "postgresql"
+    schema: "schema.sql"
+    queries: "query.sql"
+    gen:
+      go:
+        package: "querytest"
+        out: "go"
+        sql_package: "pgx/v5"
diff --git a/internal/endtoend/testdata/vet_disable/query.sql b/internal/endtoend/testdata/vet_disable/query.sql
index c1eb8d221..50052de26 100644
--- a/internal/endtoend/testdata/vet_disable/query.sql
+++ b/internal/endtoend/testdata/vet_disable/query.sql
@@ -1,6 +1,29 @@
--- name: SkipVet :exec
+-- name: RunVetAll :exec
+SELECT true;
+
+-- name: SkipVetAll :exec
 -- @sqlc-vet-disable
 SELECT true;
 
--- name: RunVet :exec
+-- name: SkipVetSingleLine :exec
+-- @sqlc-vet-disable always-fail no-exec
+SELECT true;
+
+-- name: SkipVetMultiLine :exec
+-- @sqlc-vet-disable always-fail
+-- @sqlc-vet-disable no-exec
+SELECT true;
+
+-- name: SkipVet_always_fail :exec
+-- @sqlc-vet-disable always-fail
+SELECT true;
+
+-- name: SkipVet_no_exec :exec
+-- @sqlc-vet-disable no-exec
+SELECT true;
+
+-- name: SkipVetInvalidRule :exec
+-- @sqlc-vet-disable always-fail
+-- @sqlc-vet-disable block-delete
+-- @sqlc-vet-disable no-exec
 SELECT true;
\ No newline at end of file
diff --git a/internal/endtoend/testdata/vet_disable/sqlc.yaml b/internal/endtoend/testdata/vet_disable/sqlc.yaml
index ff7c1b520..1e53bd7f2 100644
--- a/internal/endtoend/testdata/vet_disable/sqlc.yaml
+++ b/internal/endtoend/testdata/vet_disable/sqlc.yaml
@@ -9,7 +9,13 @@ sql:
         out: "db"
     rules:
       - always-fail
+      - no-exec
 rules:
   - name: always-fail
     message: "Fail"
     rule: "true"
+
+  - name: no-exec
+    message: "don't use exec"
+    rule: |
+      query.cmd == "exec"
diff --git a/internal/endtoend/testdata/vet_disable/stderr.txt b/internal/endtoend/testdata/vet_disable/stderr.txt
index 9acbb8b34..3d9a6169f 100644
--- a/internal/endtoend/testdata/vet_disable/stderr.txt
+++ b/internal/endtoend/testdata/vet_disable/stderr.txt
@@ -1,1 +1,5 @@
-query.sql: RunVet: always-fail: Fail
+query.sql: RunVetAll: always-fail: Fail
+query.sql: RunVetAll: no-exec: don't use exec
+query.sql: SkipVet_always_fail: no-exec: don't use exec
+query.sql: SkipVet_no_exec: always-fail: Fail
+query.sql: SkipVetInvalidRule: rule-check error: rule "block-delete" does not exist in the config file
\ No newline at end of file
diff --git a/internal/metadata/meta_test.go b/internal/metadata/meta_test.go
index 3c2be6d6d..e9ef54586 100644
--- a/internal/metadata/meta_test.go
+++ b/internal/metadata/meta_test.go
@@ -1,6 +1,8 @@
 package metadata
 
-import "testing"
+import (
+	"testing"
+)
 
 func TestParseQueryNameAndType(t *testing.T) {
 
@@ -77,7 +79,7 @@ func TestParseQueryParams(t *testing.T) {
 			" @param @invalid UUID ",
 		},
 	} {
-		params, _, err := ParseParamsAndFlags(comments)
+		params, _, _, err := ParseCommentFlags(comments)
 		if err != nil {
 			t.Errorf("expected comments to parse, got err: %s", err)
 		}
@@ -123,7 +125,7 @@ func TestParseQueryFlags(t *testing.T) {
 			" @param @flag-bar UUID",
 		},
 	} {
-		_, flags, err := ParseParamsAndFlags(comments)
+		_, flags, _, err := ParseCommentFlags(comments)
 		if err != nil {
 			t.Errorf("expected comments to parse, got err: %s", err)
 		}
@@ -137,3 +139,44 @@ func TestParseQueryFlags(t *testing.T) {
 		}
 	}
 }
+
+func TestParseQueryRuleSkiplist(t *testing.T) {
+	for _, comments := range [][]string{
+		{
+			" name: CreateFoo :one",
+			" @sqlc-vet-disable sqlc/db-prepare delete-without-where ",
+		},
+		{
+			" name: CreateFoo :one ",
+			" @sqlc-vet-disable sqlc/db-prepare ",
+			" @sqlc-vet-disable delete-without-where ",
+		},
+		{
+			" name: CreateFoo :one",
+			" @sqlc-vet-disable sqlc/db-prepare ",
+			" update-without where",
+			" @sqlc-vet-disable delete-without-where ",
+		},
+	} {
+		_, flags, ruleSkiplist, err := ParseCommentFlags(comments)
+		if err != nil {
+			t.Errorf("expected comments to parse, got err: %s", err)
+		}
+
+		if !flags["@sqlc-vet-disable"] {
+			t.Errorf("expected @sqlc-vet-disable flag not found")
+		}
+
+		if _, ok := ruleSkiplist["sqlc/db-prepare"]; !ok {
+			t.Errorf("expected rule not found in skiplist")
+		}
+
+		if _, ok := ruleSkiplist["delete-without-where"]; !ok {
+			t.Errorf("expected rule not found in skiplist")
+		}
+
+		if _, ok := ruleSkiplist["update-without-where"]; ok {
+			t.Errorf("unexpected rule found in skiplist")
+		}
+	}
+}