diff --git a/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/exec.json b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/exec.json new file mode 100644 index 0000000000..56cb4b3fff --- /dev/null +++ b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "duckdb", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/query.sql b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/query.sql new file mode 100644 index 0000000000..476c2d55e3 --- /dev/null +++ b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/query.sql @@ -0,0 +1,2 @@ +-- name: ListItems :many +SELECT id, name FROM items; diff --git a/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/schema.sql b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/schema.sql new file mode 100644 index 0000000000..041f8b94f6 --- /dev/null +++ b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/schema.sql @@ -0,0 +1,5 @@ +INSTALL spatial; +LOAD spatial; +SET threads = 2; + +CREATE TABLE items (id INTEGER NOT NULL, name VARCHAR NOT NULL); diff --git a/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/stdout.json b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/stdout.json new file mode 100644 index 0000000000..6e63519ca9 --- /dev/null +++ b/internal/endtoend/testdata/analyze_skipped_schema_statements/duckdb/stdout.json @@ -0,0 +1,23 @@ +[ + { + "name": "ListItems", + "cmd": ":many", + "columns": [ + { + "name": "id", + "type": { + "name": "integer" + }, + "table": "items" + }, + { + "name": "name", + "type": { + "name": "varchar" + }, + "table": "items" + } + ], + "params": [] + } +] diff --git a/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/exec.json b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/exec.json new file mode 100644 index 0000000000..56cb4b3fff --- /dev/null +++ b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/exec.json @@ -0,0 +1,5 @@ +{ + "command": "analyze", + "args": ["--dialect", "duckdb", "--schema", "schema.sql", "query.sql"], + "contexts": ["base"] +} diff --git a/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/query.sql b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/query.sql new file mode 100644 index 0000000000..0e89f2608f --- /dev/null +++ b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/query.sql @@ -0,0 +1,5 @@ +-- name: ListItems :many +SELECT id, name FROM items; + +-- name: ExportItems :exec +COPY (SELECT id, name FROM items) TO 'items.parquet' (FORMAT PARQUET); diff --git a/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/schema.sql b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/schema.sql new file mode 100644 index 0000000000..3a1848aa23 --- /dev/null +++ b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/schema.sql @@ -0,0 +1 @@ +CREATE TABLE items (id INTEGER NOT NULL, name VARCHAR NOT NULL); diff --git a/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/stderr.txt b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/stderr.txt new file mode 100644 index 0000000000..9a5c53fef2 --- /dev/null +++ b/internal/endtoend/testdata/analyze_unsupported_statement/duckdb/stderr.txt @@ -0,0 +1 @@ +Error: error parsing queries: query.sql:5:1: query ExportItems: statement is not supported by the duckdb engine diff --git a/internal/engine/duckdb/parse.go b/internal/engine/duckdb/parse.go index 76a996e562..ada77784ea 100644 --- a/internal/engine/duckdb/parse.go +++ b/internal/engine/duckdb/parse.go @@ -1,12 +1,16 @@ package duckdb import ( + "bytes" "context" "errors" + "fmt" "io" + "strings" "github.com/sqlc-dev/darkwing/parser" + "github.com/sqlc-dev/sqlc/internal/metadata" "github.com/sqlc-dev/sqlc/internal/source" "github.com/sqlc-dev/sqlc/internal/sql/ast" "github.com/sqlc-dev/sqlc/internal/sql/sqlerr" @@ -23,7 +27,11 @@ type Parser struct{} // starts where the previous one ended — so a statement's span covers the // comments before it, which is where the sqlc query annotation lives. func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { - stmts, err := parser.Parse(context.Background(), r) + src, err := io.ReadAll(r) + if err != nil { + return nil, err + } + stmts, err := parser.Parse(context.Background(), bytes.NewReader(src)) if err != nil { var perr *parser.Error if errors.As(err, &perr) { @@ -45,6 +53,21 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { converter := &cc{} node := converter.convert(stmt) if _, ok := node.(*ast.TODO); ok { + // A statement with no query annotation (INSTALL, LOAD, SET in a + // schema file) is skipped. A statement with one is a query that + // the user expects sqlc to generate: dropping it would hide the + // missing method, so report it. + span := string(src[stmt.Pos():stmt.End()]) + name, _, merr := metadata.ParseQueryNameAndType(span, metadata.CommentSyntax(p.CommentSyntax())) + if merr != nil { + return nil, &sqlerr.Error{Message: merr.Error(), Location: stmt.Pos() + leadingSpace(span)} + } + if name != "" { + return nil, &sqlerr.Error{ + Message: fmt.Sprintf("query %s: statement is not supported by the duckdb engine", name), + Location: stmt.Pos() + leadingSpace(span), + } + } continue } out = append(out, ast.Statement{ @@ -58,6 +81,13 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return out, nil } +// leadingSpace returns the length of the whitespace before the first +// character of s, so an error points at the annotation and not at the blank +// lines that follow the previous statement. +func leadingSpace(s string) int { + return len(s) - len(strings.TrimLeft(s, " \t\r\n")) +} + // https://duckdb.org/docs/stable/sql/introduction#comments func (p *Parser) CommentSyntax() source.CommentSyntax { return source.CommentSyntax{