[ Home ]

The pgx Files 08: Handing Nulls

Welcome back to The pgx Files! Today we learn about handling nulls. As we know, Go's nil is the zero value for pointers, but the zero value for primitives like int and string are 0 and ''; there is no nil. So how do we transfer nulls from PostgreSQL data types that are allowed to have nulls, to Go types like int and string that are not allowed to have nil?

The short answer is, don't. SQL's coalesce allows us to transform nulls to more useful values anyway.

Let's say we have the following table with the following values.

# create table sales(
    salesperson text null,
    sales int null);

# insert into sales (salesperson) values ('Bob');
# insert into sales (sales) values (21);

# \pset null '[NULL]'

# select * from sales;
┌─────────────┬────────┐
│ salesperson │ sales  │
├─────────────┼────────┤
│ Bob         │ [NULL] │
│ [NULL]      │     21 │
└─────────────┴────────┘
(2 rows)

If we decide there are sane values for the nulls, we just use coalesce to provide those values, and our client can read them in to data types that don't support null/nil:

# select coalesce(salesperson, 'nobody') as salesperson,
         coalesce(sales,        0)       as sales
    from sales;
┌─────────────┬───────┐
│ salesperson │ sales │
├─────────────┼───────┤
│ Bob         │     0 │
│ nobody      │    21 │
└─────────────┴───────┘
(2 rows)

SQL fans know that many data problems can be solved in the database before even reaching the client.

For those times when you really do need to return nulls to the client, pgx provides data types that can be asked if they were null or not.

package main

import (
	"fmt"
	"log"

	"github.com/jackc/pgx"
	"github.com/manniwood/playground/pgxfiles"
)

func main() {
	conn := util.Connect("null selecter")
	defer conn.Close()

	var salesperson pgx.NullString
	var sales pgx.NullInt32
	rows, err := conn.Query(`
	select salesperson,
	       sales
	  from sales`)
	if err != nil {
		log.Fatalf("Unexpected error trying to read sales: %v\n", err)
	}
	defer rows.Close()

	for rows.Next() {
		rows.Scan(&salesperson, &sales)
		fmt.Printf("Salesperson %s, (null? %t); sales: %d (null? %t)\n",
			salesperson.String, !salesperson.Valid, sales.Int32, !sales.Valid)
	}
}

The output of the above program on the above table:

Salesperson Bob, (null? false); sales: 0 (null? true)
Salesperson , (null? true); sales: 21 (null? false)

The idea is that pgx.NullString has two fields, String and Valid. Valid is set to true when the String field is '' and was also '' in PostgreSQL. Conversely, Valid is set to false when the String field is '' but was actually null in PostgreSQL.

pgx.NullInt32 works the same way, except its value field is named Int32, and of course its zero value is 0 (not the empty string). The Valid field works the same.

The complete list of pgx.Null* data types can be found at https://godoc.org/github.com/jackc/pgx.

Of course, now we need to test inserting as well!

package main

import (
	"fmt"
	"log"

	"github.com/jackc/pgx"
	"github.com/manniwood/playground/pgxfiles"
)

func main() {
	conn := util.Connect("null inserter")
	defer conn.Close()

	result, err := conn.Exec(`
	insert into sales (salesperson, sales)
	           values ($1, $2),
	                  ($3, $4)`,
		pgx.NullString{String: "Chuck", Valid: true},
		pgx.NullInt32{Int32: 0, Valid: false},
		pgx.NullString{String: "", Valid: false},
		pgx.NullInt32{Int32: 23, Valid: true})
	if err != nil {
		log.Fatalf("Unexpected error trying to insert into sales: %v\n", err)
	}

	fmt.Printf("Inserted %d rows into sales\n", result.RowsAffected())
}

When we run this on the command line:

$ ./null_insert 
Inserted 2 rows into sales

And, when we look at the table from a psql session:

# select * from sales; rollback;
┌─────────────┬────────┐
│ salesperson │ sales  │
├─────────────┼────────┤
│ Bob         │ [NULL] │
│ [NULL]      │     21 │
│ Chuck       │ [NULL] │
│ [NULL]      │     23 │
└─────────────┴────────┘
(4 rows)

Nice.

For inserts, pgx also knows how to deal with nils directly, like so:

package main

import (
	"fmt"
	"log"

	"github.com/manniwood/playground/pgxfiles"
)

func main() {
	conn := util.Connect("null inserter")
	defer conn.Close()

	result, err := conn.Exec(`
	insert into sales (salesperson, sales)
	           values ($1, $2), ($3, $4)`,
		"Alice",
		nil,
		nil,
		42)
	if err != nil {
		log.Fatalf("Unexpected error trying to insert into sales: %v\n", err)
	}

	fmt.Printf("Inserted %d rows into sales\n", result.RowsAffected())
}
# select * from sales;
┌─────────────┬────────┐
│ salesperson │ sales  │
├─────────────┼────────┤
│ Bob         │ [NULL] │
│ [NULL]      │     21 │
│ Chuck       │ [NULL] │
│ [NULL]      │     23 │
│ Alice       │ [NULL] │
│ [NULL]      │     42 │
└─────────────┴────────┘
(6 rows)

And, for reading back out, pointers to types (rather than using the pgx.Null* types) also works:

package main

import (
	"fmt"
	"log"

	"github.com/manniwood/playground/pgxfiles"
)

func main() {
	conn := util.Connect("null selecter")
	defer conn.Close()

	var salesperson *string
	var sales *int
	rows, err := conn.Query(`
	select salesperson,
	       sales
	  from sales`)
	if err != nil {
		log.Fatalf("Unexpected error trying to read pg_stat_activity: %v\n", err)
	}
	defer rows.Close()

	salespersonValid := false
	salesValid := false
	displaySalesperson := ""
	displaySales := 0
	for rows.Next() {
		salespersonValid = false
		salesValid = false
		displaySalesperson = ""
		displaySales = 0
		rows.Scan(&salesperson, &sales)
		if salesperson != nil {
			salespersonValid = true
			displaySalesperson = *salesperson
		}
		if sales != nil {
			salesValid = true
			displaySales = *sales
		}
		fmt.Printf("Salesperson %s, (null? %t); sales: %d (null? %t)\n",
			displaySalesperson, !salespersonValid, displaySales, !salesValid)
	}
}

Next on the pgx files: Stored procedures!