sqlc: sqlc.embed() generates code that cannot handle NULL items

Version

1.18.0

What happened?

I was testing to implement sqlc.embed() in order to simplify my conversions routines from sqlc to protobuf.

I have this query:

-- name: GetChannelLayers :many
SELECT sqlc.embed(t)
FROM unnest(sqlc.narg('channel_layer_ids')::TEXT[]) WITH ORDINALITY o(channel_layer_id, ord)
LEFT JOIN channel_layer AS t ON t.channel_layer_id = o.channel_layer_id::UUID AND t.tenant_id = $1;

The input is a list of IDs that i expect to get from the DB. If the ID is not found I expect to get a NULL row. Unfortunately it seems like the code for the new sqlc.embed() feature doesn’t handle this case very well since I get a scan error if I try to query for a non-existent ID.

pgx.ScanArgError: can't scan into dest[0]: cannot scan null into *string

The generated output code:


const getChannelLayers = `-- name: GetChannelLayers :many
SELECT t.tenant_id, t.channel_id, t.channel_layer_id, t.name, t.description, t.created, t.updated, t.removed, t.disabled, t.condition_ref, t.z_index, t.width, t.height, t.pos_x, t.pos_y, t.alignment, t.content_ref
FROM unnest($2::TEXT[]) WITH ORDINALITY o(channel_layer_id, ord)
LEFT JOIN channel_layer AS t ON t.channel_layer_id = o.channel_layer_id::UUID AND t.tenant_id = $1
`

type GetChannelLayersParams struct {
	TenantID        string
	ChannelLayerIds []string
}

type ChannelLayer struct {
	TenantID       string
	ChannelID      uuid.UUID
	ChannelLayerID uuid.UUID
	Name           string
	Description    sql.NullString
	Created        time.Time
	Updated        time.Time
	Removed        sql.NullTime
	Disabled       sql.NullTime
	ConditionRef   sql.NullString
	ZIndex         sql.NullInt32
	Width          sql.NullInt32
	Height         sql.NullInt32
	PosX           sql.NullInt32
	PosY           sql.NullInt32
	Alignment      ChannelLayerAlignment
	ContentRef     sql.NullString
}

type GetChannelLayersRow struct {
	ChannelLayer ChannelLayer
}

func (q *Queries) GetChannelLayers(ctx context.Context, arg GetChannelLayersParams) ([]*GetChannelLayersRow, error) {
	rows, err := q.db.Query(ctx, getChannelLayers, arg.TenantID, arg.ChannelLayerIds)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []*GetChannelLayersRow
	for rows.Next() {
		var i GetChannelLayersRow
		if err := rows.Scan(
			&i.ChannelLayer.TenantID,
			&i.ChannelLayer.ChannelID,
			&i.ChannelLayer.ChannelLayerID,
			&i.ChannelLayer.Name,
			&i.ChannelLayer.Description,
			&i.ChannelLayer.Created,
			&i.ChannelLayer.Updated,
			&i.ChannelLayer.Removed,
			&i.ChannelLayer.Disabled,
			&i.ChannelLayer.ConditionRef,
			&i.ChannelLayer.ZIndex,
			&i.ChannelLayer.Width,
			&i.ChannelLayer.Height,
			&i.ChannelLayer.PosX,
			&i.ChannelLayer.PosY,
			&i.ChannelLayer.Alignment,
			&i.ChannelLayer.ContentRef,
		); err != nil {
			return nil, err
		}
		items = append(items, &i)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

I’m actually not sure how to solve this. I need the LEFT JOIN because reasons… Perhaps it’s impossible to support this case?

Perhaps sqlc can create a temp/hidden type where all fields are NULL-able. Scan the fields using this type and then check if the PRIMARY KEY fields are Valid. If they are valid, convert the type with the NULL-able fields into the normal type. If the fields are not valid then return a nil object pointer for the embedded field.

@nickjackson, any input?

What operating system are you using?

Linux

What database engines are you using?

PostgreSQL

What type of code are you generating?

Go

About this issue

  • Original URL
  • State: open
  • Created a year ago
  • Reactions: 7
  • Comments: 15 (2 by maintainers)

Most upvoted comments

Workaround: if you add a view with just your LEFT JOIN, you can JOIN it and sqlc will generate the model with all nullable fields. See https://play.sqlc.dev/p/1f63b8bc9aac38f8be54ce6b4842b693ed259cf7a4807feaf55aebd1cc8c886a

CREATE VIEW authorimages AS (
  SELECT images.* FROM authors LEFT JOIN images ON authors.id = images.id
);

-- name: GetAuthor :one
SELECT sqlc.embed(authors), sqlc.embed(authorimages) FROM authors
JOIN authorimages on authors.id = authorimages.id
WHERE authors.id = $1 LIMIT 1;
type Authorimage struct {
	ID  sql.NullInt64
	Url sql.NullString
}

Yeah, sometimes it’s a question of “where do you stop?” with a feature like this I guess. 🤔

It’d be tempting to explicitly provide the support if the primary key is available, and bail out to the standard behaviour if not. This would surely increase its utility quite a bit. However, I do wonder if there’s an opportunity for the generator logic to be a bit more well-informed about which joins could result in a null embed even if we’re not joining on a primary key (for example, always using a nil pointer for a left join).

Alternatively, could there be room for a sqlc.embed(foo, sqlc.nullable) variant? Which would make it declarative, and put the onus on the developer to request it.

Workaround: if you add a view with just your LEFT JOIN, you can JOIN it and sqlc will generate the model with all nullable fields.

You don’t even need to join inside that view, do you? Ie. create view images_vw as (select * from images)?

Either way that’s a clever workaround that I’m probably going to rely on for now, I just wish it didn’t introduce a model with all nullable fields in our domain that only technically accommodates for only null or a model with all non-nullable fields. Dreaming of sqlc.nembed.

Yeah, you won’t be able to use embed for t and e, you need to handle 'em as columns and do some post-processing after the query.

I think, having done more poking around, that even if piggybacking on embed might be slightly easier, introducing nembed (nullable embed) is more in line with the existence of arg/narg. From the looks of it, it’ll require a bit of a refactor but maybe I can learn from @skabbes’ hard work in #1536 .

Good points nickjackson. I also think richchurcher has a point on the ability to configure the embed() command with parameters.

I think a good start could be to write the code such that it is able to detect the cases it cannot support. Right now the code generated doesn’t really work if you happen to do unsupported things. If it is possible to detect all the cases it doesn’t work, I think it would be easier to extend it to actually make it work, since then we know all about what type of join it is and so forth. I took a look at the code, and sqlc doesn’t seem to know very much about the join itself, just that there is some kind of relationship to another table.