On this page

Read arrays and lists

Read an array column into a Go slice, preserving the difference between NULL, an empty array and a NULL element. This guide uses PostgreSQL; start with a configured project.

Declare the array and its elements

Add the array columns to a migration. Declare non_null_elements: true only for a column whose elements your application keeps non-null:

CREATE TABLE voyages (
    id        BIGINT   GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    boat      TEXT     NOT NULL,
    ports     TEXT[]   NOT NULL,
    catch_kg  BIGINT[] NOT NULL,
    sightings TEXT[]
);
-- sgl.column(voyages.ports, non_null_elements: true)

Put the write and read in db/queries/voyages.sql. Ports becomes []string; CatchKg and Sightings keep pointer elements because PostgreSQL can store NULL among them.

-- sgl.query(LogVoyage, exec)
INSERT INTO voyages (boat, ports, catch_kg, sightings) VALUES ($1, $2, $3, $4);
-- sgl.query(ListVoyages, many)
SELECT id, boat, ports, catch_kg, sightings FROM voyages ORDER BY id;

Pass slices and read the result

Run sgl generate, then construct store.New(database) from your open connection. The caller writes populated, NULL and empty arrays:

	voyages := []store.LogVoyageParams{
		{Boat: "Kittiwake", Ports: []string{"Newlyn", "St Ives", "Mousehole"}, CatchKg: values[int64](120, 85), Sightings: values("dolphin", "basking shark")},
		{Boat: "Salty Chip", Ports: []string{"Penzance", "Mousehole"}, CatchKg: values[int64](40), Sightings: nil},
		{Boat: "Kittiwake", Ports: []string{"Newlyn", "Lamorna"}, CatchKg: values[int64](60, 30, 15), Sightings: []*string{}},
	}
	for _, voyage := range voyages {
		if err := queries.LogVoyage(ctx, voyage); err != nil {
			return fmt.Errorf("logging %s's voyage: %w", voyage.Boat, err)
		}
	}

Read the generated rows without converting array text yourself:

	logged, err := queries.ListVoyages(ctx)
	if err != nil {
		return fmt.Errorf("listing voyages: %w", err)
	}
	for _, voyage := range logged {
		sightings := elements(voyage.Sightings)
		if voyage.Sightings == nil {
			sightings = "NULL"
		}
		fmt.Fprintf(output, "voyage %d  %-10s  ports %s, catch %s kg, sightings %s\n", voyage.ID, voyage.Boat, voyage.Ports, elements(voyage.CatchKg), sightings)
	}

Check NULL and empty arrays

From the Seagull repository root, run the array example:

cd examples/postgres/008_arrays
go test .

Set SGL_EXAMPLE_DSN to a disposable database before running this server-backed test; without it, the test skips. See running examples for connection setup.

The complete example produces:

voyage 1  Kittiwake   ports [Newlyn St Ives Mousehole], catch [120 85] kg, sightings [dolphin basking shark]
voyage 2  Salty Chip  ports [Penzance Mousehole], catch [40] kg, sightings NULL
voyage 3  Kittiwake   ports [Newlyn Lamorna], catch [60 30 15] kg, sightings []

calling at Mousehole:
  Kittiwake   from Newlyn, 3 stops
  Salty Chip  from Penzance, 2 stops

voyages per boat:
  Kittiwake   [1 3]
  Salty Chip  [2]

Keep NULL, empty and populated arrays distinct

Array columns differ from kind: slice, which expands an input list in an IN predicate. On PostgreSQL a nil slice represents a NULL array; an empty non-nil slice represents an empty array. A nil element in []*T represents SQL NULL.

DuckDB can read nullable list elements, but its driver cannot bind a NULL element. A parameter used as the array in id = ANY($1) also uses plain elements. See the reference for the full element rules.

See the type mapping reference for the complete contract. The example browser exposes other engine variants; use their actual SQL rather than changing only the dialect name.

Examples for this page