Skip to content

duckdb: col.field of a STRUCT column does not resolve, and alias.col.field is typed as the whole struct #4655

Open

Description

@xen0l

Version

main at commit 3ad8b20 (2026-10-23), built with CGO_ENABLED=0 go install github.com/sqlc-dev/sqlc/cmd/sqlc@3ad8b20fb02b5bea81591b673da0758b8685d951.
sqlc version prints v1.31.1 because internal/info/facts.go hard-codes it.
The DuckDB engine was added in #4621 and is not in a tagged release.

What happened?

Reading a field of a STRUCT column does not work like it does in DuckDB.

  1. The bare form info.label fails with unknown column "info"."label". DuckDB accepts it.
  2. The form i.info.label parses, but the result has the type of the struct column. The field name is ignored.
  3. A parameter that is compared with i.info.score gets the name info and the type any. The expected type is a float.
  4. The generated code compiles. It fails when it scans a row (see the output). sqlc generate gives no warning.

A workaround exists: CAST(i.info.label AS VARCHAR) gives sql.NullString.

What DuckDB v1.5.6 reports for the same queries (DESCRIBE): VARCHAR for info.label, i.info.label,
struct_extract(info, 'label') and info['label'].

Expected: the field resolves, and the type is the type of the field (nullable varchar for label).

Cause

typeColumnRef in internal/core/analyzer/expr.go (line 152 at this commit) reads only parts[0] and parts[1]
of the column reference. For i.info.label it resolves relation i and column info, and drops label.
For info.label it looks for a relation named info, finds none, and reports an unknown column.
sqlc analyze already shows the wrong type, so the fault is in the analyzer and not in code generation.
The struct's field types are available: the column's TypeExpr is struct(label: varchar, score: double).

Reproduction

Without Go code (only the sqlc binary):

repro1a.sh
#!/usr/bin/env bash
# No Go code needed: only the sqlc binary.
rm -rf repro1a && mkdir repro1a && cd repro1a
cat > schema.sql <<'EOF'
CREATE TABLE items (
  id   INTEGER NOT NULL,
  info STRUCT(label VARCHAR, score DOUBLE)
);
EOF
while IFS= read -r q; do
  printf -- '-- name: Q :many\n%s\n' "$q" > query.sql
  echo "== $q"
  sqlc analyze --dialect duckdb --schema schema.sql query.sql 2>&1 | grep -E '"(name|nullable)"|Error' | grep -v '"name": "Q"'
done <<'EOF'
SELECT info.label AS label FROM items;
SELECT i.info.label AS label FROM items AS i;
SELECT CAST(i.info.label AS VARCHAR) AS label FROM items AS i;
EOF

Output:

== SELECT info.label AS label FROM items;
Error: error parsing queries: query.sql:1:1: unknown column "info"."label"
== SELECT i.info.label AS label FROM items AS i;
        "name": "label",
          "name": "struct",
          "nullable": true,
                "name": "varchar"
                "name": "double"
== SELECT CAST(i.info.label AS VARCHAR) AS label FROM items AS i;
        "name": "label",
          "name": "varchar",
          "nullable": true

With the generated code, run on DuckDB:

repro1b.sh (needs Go 1.27+ and a C compiler)
#!/usr/bin/env bash
# Needs Go 1.27+ and a C compiler (duckdb-go v2.10506.0).
rm -rf repro1b && mkdir repro1b && cd repro1b
cat > sqlc.yaml <<'EOF'
version: "2"
sql:
  - engine: "duckdb"
    schema: "schema.sql"
    queries: "query.sql"
    gen:
      go:
        package: "repro"
        out: "out"
EOF
cat > schema.sql <<'EOF'
CREATE TABLE items (
  id   INTEGER NOT NULL,
  info STRUCT(label VARCHAR, score DOUBLE)
);
EOF
while IFS= read -r q; do
  printf -- '-- name: Q :many\n%s\n' "$q" > query.sql; rm -rf out
  echo "== $q"; sqlc generate 2>&1 | head -3; grep -hE '^func \(q \*Queries\) Q' out/query.sql.go 2>/dev/null
done <<'EOF'
SELECT i.info.label AS label FROM items AS i;
SELECT id FROM items AS i WHERE i.info.score > $1;
SELECT CAST(i.info.label AS VARCHAR) AS label FROM items AS i;
EOF

echo; echo "== run the code generated for the first query"
printf -- '-- name: Q :many\nSELECT i.info.label AS label FROM items AS i;\n' > query.sql
rm -rf out && sqlc generate
cat > main.go <<'EOF'
package main

import (
	"context"
	"database/sql"
	"fmt"

	_ "github.com/duckdb/duckdb-go/v2"

	"example.com/repro/out"
)

func main() {
	db, err := sql.Open("duckdb", "")
	if err != nil {
		panic(err)
	}
	defer db.Close()
	db.SetMaxOpenConns(1)
	if _, err := db.Exec(`CREATE TABLE items (id INTEGER NOT NULL, info STRUCT(label VARCHAR, score DOUBLE));
		INSERT INTO items VALUES (1, {'label': 'a', 'score': 1.5});`); err != nil {
		panic(err)
	}
	var s string
	_ = db.QueryRow("SELECT i.info.label FROM items AS i").Scan(&s)
	fmt.Printf("DuckDB returns a VARCHAR: %q\n", s)
	rows, err := repro.New(db).Q(context.Background())
	fmt.Println("generated Q():", rows, err)
}
EOF
printf 'module example.com/repro\n\ngo 1.27\n' > go.mod
go get github.com/duckdb/duckdb-go/v2@v2.10506.0 >/dev/null 2>&1
go mod tidy >/dev/null 2>&1
go run .

Fix

I have a patch with an end-to-end test case, and I will open a pull request.

Not covered

struct_extract(info, 'label') is still typed as struct, and info['label'] is still untyped.
They go through function and subscript typing, which is a different cause. I did not change them.

Relevant log output

== SELECT i.info.label AS label FROM items AS i;
func (q *Queries) Q(ctx context.Context) ([]*map[string]any, error) {
== SELECT id FROM items AS i WHERE i.info.score > $1;
func (q *Queries) Q(ctx context.Context, info any) ([]int32, error) {
== SELECT CAST(i.info.label AS VARCHAR) AS label FROM items AS i;
func (q *Queries) Q(ctx context.Context) ([]sql.NullString, error) {

== run the code generated for the first query
DuckDB returns a VARCHAR: "a"
generated Q(): [] sql: Scan error on column index 0, name "label": unsupported Scan, storing driver.Value type string into type *map[string]interface {}

Database schema

CREATE TABLE items (
  id   INTEGER NOT NULL,
  info STRUCT(label VARCHAR, score DOUBLE)
);

SQL queries

SELECT info.label AS label FROM items;
SELECT i.info.label AS label FROM items AS i;
SELECT id FROM items AS i WHERE i.info.score > $1;
SELECT CAST(i.info.label AS VARCHAR) AS label FROM items AS i;

Configuration

version: "2"
sql:
  - engine: "duckdb"
    schema: "schema.sql"
    queries: "query.sql"
    gen:
      go:
        package: "repro"
        out: "out"

Playground URL

Not available. play.sqlc.dev runs only v1.18.0–v1.25.0, and the DuckDB engine is not in a release (v1.25.0 answers unknown engine: duckdb; main, a commit and every later release return HTTP 500). The report has a script that needs only go install github.com/sqlc-dev/sqlc/cmd/sqlc@3ad8b20fb02b5bea81591b673da0758b8685d951.

What operating system are you using?

Linux

What database engines are you using?

(none of the listed options; the engine is duckdb)

What type of code are you generating?

Go

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions