Skip to content

SQLC does not infer nullability for subqueries in view SELECT #4262

Open

Description

@lookeey

Version

1.30.0

What happened?

I have a DB with a view like so:

CREATE VIEW schema.vw_myview AS
 SELECT
    p.id AS id_produto,
    (SELECT i.name
       FROM public.tb_ibpt i
       WHERE ((i.code)::text = (p.ncm)::text)
       LIMIT 1) AS description,
...

I would expect that selecting the description column in the resulting vw_myview would create a nullable type in the SQLC generated struct, but instead a string field is created. So when there are no matches in the subquery, there is a scan error.

Relevant log output

Database schema

SQL queries

Configuration

version: "2"

sql:
  - engine: postgresql
    schema: generated.sql
    queries:
      - queries
    gen:
      go:
        package: sqls_erp
        sql_package: "pgx/v5"
        out: ../internal/sqls/sql_erp
        emit_json_tags: true

Playground URL

No response

What operating system are you using?

Linux

What database engines are you using?

PostgreSQL

What type of code are you generating?

Go

Activity

  1. ken-talltree-io commented on Oct 24, 2026

    @ken-talltree-io

    This also happens outside views, and with CASE whose ELSE is a typed null (CASE WHEN … THEN s.label ELSE NULL::text END, which is how pg_dump writes every CASE without ELSE). On 1.31.1 and main (3ad8b20), the legacy analyzer marks both non-null. With SQLCEXPERIMENT=coreanalyzer both are nullable, as they should be, but left-joined columns become non-null there instead.

    Minimal repro:

    -- schema.sql
    create table authors (id int primary key, name text not null);
    create table series (id int primary key, name text not null);
    create table books (id int primary key, title text not null, author_id int not null references authors, series_id int);
    create table reviews (id int primary key, book_id int not null);
    create table shelves (id int primary key, label text not null, book_id int);
    create view book_view as
      select b.id, (select r.id from reviews r where r.book_id = b.id) review_id,
        case when b.id > 0 then s.label else null::text end shelf,
        (select count(*) from reviews r where r.book_id = b.id) reviews
      from books b left join shelves s on s.book_id = b.id;
    -- query.sql
    -- name: LeftJoin :many
    select b.title, s.name from books b left join series s on s.id = b.series_id;
    -- name: ThroughView :many
    select id, review_id, shelf, reviews from book_view;
    -- name: Inline :many
    select (select r.id from reviews r where r.book_id = b.id) review_id,
      case when b.id > 0 then b.title else null::text end maybe_title
    from books b;

    Nullability in the codegen request (? = nullable):

    legacy coreanalyzer
    LeftJoin.name text? ✓ text ✗
    ThroughView.review_id (scalar subquery) int4 ✗ integer? ✓
    ThroughView.shelf (ELSE NULL::text) text ✗ text? ✓
    ThroughView.reviews (count(*) subquery) bigint ✓ bigint? (always one row, so could be non-null)
    Inline.review_id int4 ✗ integer? ✓
    Inline.maybe_title text ✗ text? ✓

    I'd like to send a fix to internal/compiler/output_columns.go for the legacy path: scalar subqueries and ELSE NULL::type nullable, (SELECT count(*) …) kept non-null. Would you take that, or would you rather fixes go to the core analyzer?

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions