-
-
Save adharris/4163702 to your computer and use it in GitHub Desktop.
package main | |
import ( | |
"database/sql" | |
"errors" | |
"fmt" | |
_ "github.com/bmizerany/pq" | |
"os" | |
"regexp" | |
"strings" | |
) | |
func main() { | |
db := dbConnect() | |
makeTestTables(db) | |
defer db.Close() | |
defer cleanup(db) | |
// Insert Some Data | |
db.Exec(`INSERT INTO array_test VALUES ('{"String1", "String2"}')`) | |
// arrays can be selected as strings... | |
dataString := selectAsString(db) | |
fmt.Println("SELECT as String:", dataString) | |
// Or by using array functions... | |
dataUnnest := selectUsingUnnest(db) | |
fmt.Println("SELECT using Unnest:", dataUnnest) | |
// Or by defining a scan type and parsing the return value | |
dataSlice := selectAsSlice(db) | |
fmt.Println("SELECT by parsing:", dataSlice) | |
// Arrays can be updated by replacing the entire array: | |
newArray := []interface{}{"String1", "String3", "String4", "String5"} | |
updateArray(db, newArray) | |
dataSlice = selectAsSlice(db) | |
fmt.Println("UPDATE entire array", dataSlice) | |
// or by appending / prepending value(s): | |
AppendToArray(db, "String6") | |
dataSlice = selectAsSlice(db) | |
fmt.Println("UPDATE with append:", dataSlice) | |
// or by replacing individual values: | |
ReplaceInArray(db, 2, "NULL") | |
dataSlice = selectAsSlice(db) | |
fmt.Println("UPDATE with replace:", dataSlice) | |
// Deleting by index requires slicing and is inefficient: | |
DeleteFromArray(db, 3) | |
dataSlice = selectAsSlice(db) | |
fmt.Println("UPDATE deleting index:", dataSlice) | |
} | |
// Arrays are serialized to strings {value, value...} by the database. | |
// these strings selected, updated and inserted like any string | |
func selectAsString(db *sql.DB) string { | |
row := db.QueryRow("SELECT data FROM array_test") | |
var asString string | |
err := row.Scan(&asString) | |
if err != nil { | |
panic(err) | |
} | |
return asString | |
} | |
// The UNNEST function expands an array into multiple rows. Each row | |
// can then be scanned individually. | |
func selectUsingUnnest(db *sql.DB) []string { | |
results := make([]string, 0) | |
rows, err := db.Query("SELECT UNNEST(data) FROM array_test") | |
if err != nil { | |
panic(err) | |
} | |
var scanString string | |
for rows.Next() { | |
rows.Scan(&scanString) | |
results = append(results, scanString) | |
} | |
return results | |
} | |
// By defining a wrapper type around a slice which implements | |
// sql.Scanner, we can scan the array directly into the type. | |
func selectAsSlice(db *sql.DB) StringSlice { | |
row := db.QueryRow("SELECT data FROM array_test") | |
var asSlice StringSlice | |
err := row.Scan(&asSlice) | |
if err != nil { | |
panic(err) | |
} | |
return asSlice | |
} | |
// Update an array by replacing the whole array with new values. | |
// This _could_ be done by serializing the StringSlice type using | |
// sql.driver.Valuer, but then we would have to validate the type | |
// of each value manually and format it for insert by hand. Instead, | |
// the ARRAY[...] format allows us to use query parameters to construct | |
// the array, ie ARRAY[$1, $2, $3], which then allows the database | |
// driver to coerce the variables into the right format for us. | |
func updateArray(db *sql.DB, array []interface{}) { | |
params := make([]string, 0, len(array)) | |
for i := range array { | |
params = append(params, fmt.Sprintf("$%v", i+1)) | |
} | |
query := fmt.Sprintf("UPDATE array_test SET data = ARRAY[%s]", strings.Join(params, ", ")) | |
db.Exec(query, array...) | |
} | |
// The ARRAY_APPEND and ARRAY_PREPEND functions can be used to add single | |
// values to arrays. ARRAY_CAT combines two arrays. The || operator can | |
// do the same thing: | |
// SET data = data || <value> | |
// SET data = data || ARRAY[<value1>, <value2>] | |
func AppendToArray(db *sql.DB, value string) { | |
_, err := db.Exec("UPDATE array_test SET data = ARRAY_APPEND(data, $1)", value) | |
if err != nil { | |
panic(err) | |
} | |
} | |
// Arrays are 1-indexed. Individual elements can be used in expressions, | |
// updated, or selected by indexing the array. | |
func ReplaceInArray(db *sql.DB, index int, newValue string) { | |
_, err := db.Exec("UPDATE array_test SET data[$1] = $2", index, newValue) | |
if err != nil { | |
panic(err) | |
} | |
} | |
// Arrays support slice indexing: | |
// ARRAY['a', 'b', 'c'][1:2] == ARRAY['a', 'b'] | |
// The ARRAY_UPPER function gets the length of an array for a specified dimension | |
// Deleting a value from an array amounts to slicing the array into two parts | |
// and combining them back together. | |
func DeleteFromArray(db *sql.DB, i int) { | |
_, err := db.Exec("UPDATE array_test SET data = array_cat(data[0:$1], data[$2:ARRAY_UPPER(data, 1) + 1])", i-1, i+1) | |
if err != nil { | |
panic(err) | |
} | |
} | |
type StringSlice []string | |
// Implements sql.Scanner for the String slice type | |
// Scanners take the database value (in this case as a byte slice) | |
// and sets the value of the type. Here we cast to a string and | |
// do a regexp based parse | |
func (s *StringSlice) Scan(src interface{}) error { | |
asBytes, ok := src.([]byte) | |
if !ok { | |
return error(errors.New("Scan source was not []bytes")) | |
} | |
asString := string(asBytes) | |
parsed := parseArray(asString) | |
(*s) = StringSlice(parsed) | |
return nil | |
} | |
// PARSING ARRAYS | |
// SEE http://www.postgresql.org/docs/9.1/static/arrays.html#ARRAYS-IO | |
// Arrays are output within {} and a delimiter, which is a comma for most | |
// postgres types (; for box) | |
// | |
// Individual values are surrounded by quotes: | |
// The array output routine will put double quotes around element values if | |
// they are empty strings, contain curly braces, delimiter characters, | |
// double quotes, backslashes, or white space, or match the word NULL. | |
// Double quotes and backslashes embedded in element values will be | |
// backslash-escaped. For numeric data types it is safe to assume that double | |
// quotes will never appear, but for textual data types one should be prepared | |
// to cope with either the presence or absence of quotes. | |
// construct a regexp to extract values: | |
var ( | |
// unquoted array values must not contain: (" , \ { } whitespace NULL) | |
// and must be at least one char | |
unquotedChar = `[^",\\{}\s(NULL)]` | |
unquotedValue = fmt.Sprintf("(%s)+", unquotedChar) | |
// quoted array values are surrounded by double quotes, can be any | |
// character except " or \, which must be backslash escaped: | |
quotedChar = `[^"\\]|\\"|\\\\` | |
quotedValue = fmt.Sprintf("\"(%s)*\"", quotedChar) | |
// an array value may be either quoted or unquoted: | |
arrayValue = fmt.Sprintf("(?P<value>(%s|%s))", unquotedValue, quotedValue) | |
// Array values are separated with a comma IF there is more than one value: | |
arrayExp = regexp.MustCompile(fmt.Sprintf("((%s)(,)?)", arrayValue)) | |
valueIndex int | |
) | |
// Find the index of the 'value' named expression | |
func init() { | |
for i, subexp := range arrayExp.SubexpNames() { | |
if subexp == "value" { | |
valueIndex = i | |
break | |
} | |
} | |
} | |
// Parse the output string from the array type. | |
// Regex used: (((?P<value>(([^",\\{}\s(NULL)])+|"([^"\\]|\\"|\\\\)*")))(,)?) | |
func parseArray(array string) []string { | |
results := make([]string, 0) | |
matches := arrayExp.FindAllStringSubmatch(array, -1) | |
for _, match := range matches { | |
s := match[valueIndex] | |
// the string _might_ be wrapped in quotes, so trim them: | |
s = strings.Trim(s, "\"") | |
results = append(results, s) | |
} | |
return results | |
} | |
// DB HELPERs | |
func dbConnect() *sql.DB { | |
datname := os.Getenv("PGDATABASE") | |
sslmode := os.Getenv("PGSSLMODE") | |
if datname == "" { | |
os.Setenv("PGDATABASE", "pqgotest") | |
} | |
if sslmode == "" { | |
os.Setenv("PGSSLMODE", "disable") | |
} | |
conn, err := sql.Open("postgres", "") | |
if err != nil { | |
panic(err) | |
} | |
return conn | |
} | |
// Create a table with an array type | |
// Can also use the syntax CREATE TABLE array_test (data varchar ARRAY) | |
func makeTestTables(db *sql.DB) { | |
_, err := db.Exec("CREATE TABLE array_test (data varchar[])") | |
if err != nil { | |
panic(err) | |
} | |
} | |
func cleanup(db *sql.DB) { | |
db.Exec("DROP TABLE array_test") | |
} |
Thank you for this!
Thank you, it's useful!
Oh men, you are my hero, i spent few days looking for something like this, i'm going to test this for calling postgres functions from golang, thanks
+1 Gold.
indeed, very nice!
+1
Line 186 probably needs a fix. It was breaking on characters N, U and L.
Works fine with
unquotedChar = `[^",\\{}\s]|NULL`
There is bad work any complex case for []text array.
What about from StringSlice
back to a string that can be inserted in SQL?
Here is an alternative approach that only supports single dimension string arrays. It uses the csv parser lib to do the parsing after a bit of preprocessing. And saving to postgres is as easy as creating a string of the proper format:
// for replacing escaped quotes except if it is preceded by a literal backslash
// eg "\\" should translate to a quoted element whose value is \
var quoteEscapeRegex = regexp.MustCompile(`([^\\]([\\]{2})*)\\"`)
// Scan convert to a slice of strings
// http://www.postgresql.org/docs/9.1/static/arrays.html#ARRAYS-IO
func (s *StringSlice) Scan(src interface{}) error {
asBytes, ok := src.([]byte)
if !ok {
return error(errors.New("Scan source was not []bytes"))
}
str := string(asBytes)
// change quote escapes for csv parser
str = quoteEscapeRegex.ReplaceAllString(str, `$1""`)
str = strings.Replace(str, `\\`, `\`, -1)
// remove braces
str = str[1 : len(str)-1]
csvReader := csv.NewReader(strings.NewReader(str))
slice, err := csvReader.Read()
if err != nil {
return err
}
(*s) = StringSlice(slice)
return nil
}
func (s StringSlice) Value() (driver.Value, error) {
// string escapes.
// \ => \\\
// " => \"
for i, elem := range s {
s[i] = `"` + strings.Replace(strings.Replace(elem, `\`, `\\\`, -1), `"`, `\"`, -1) + `"`
}
return "{" + strings.Join(s, ",") + "}", nil
}
Tests:
func TestStringSliceScan(t *testing.T) {
var slice StringSlice
err := slice.Scan([]byte(`{"12",45,"abc,\\\"d\\ef\\\\"}`))
if err != nil {
t.Errorf("Could not scan array, %v", err)
return
}
if slice[0] != "12" || slice[2] != `abc,\"d\ef\\` {
t.Errorf("Did not get expected slice contents")
}
}
func TestStringSliceDbValue(t *testing.T) {
slice := StringSlice([]string{`as"f\df`, "43", "}adsf"})
val, err := slice.Value()
if err != nil {
t.Errorf("Could not convert to db string")
}
if str, ok := val.(string); ok {
if `{"as\"f\\\df","43","}adsf"}` != str {
t.Errorf("db value expecting %s got %s", `{"as\"f\\\df","43","}adsf"}`, str)
}
} else {
t.Errorf("Could not convert %v to string for comparison", val)
}
}
Well this was an awesome find. Thanks for saving me a large amount of time!
An easier approach that works with all kind of nested arrays/composite types, if you only need to retrieve values from Postgres, is simply to use native Postgres functions to_json
/array_to_json
/row_to_json
in your SELECT
query and then json unmarshall the resulting string.
type StringSlice []string
func (s StringSlice) Value() (driver.Value, error) {
if len(s) == 0 {
return nil, nil
}
var buffer bytes.Buffer
buffer.WriteString("{")
last := len(s) - 1
for i, val := range s {
buffer.WriteString(strconv.Quote(val))
if i != last {
buffer.WriteString(",")
}
}
buffer.WriteString("}")
return string(buffer.Bytes()), nil
}
👍
@kyleboyle, your Scan needs a branch for a '{}' case.
Thanks! Quite helpful.
@kyleboyle @davidmz here's the Scan supporting the empty array case:
func (s *StringSlice) Scan(src interface{}) error {
var str string
if asBytes, ok := src.([]byte); ok {
str = string(asBytes)
} else if asStr, ok := src.(string); ok {
str = string(asStr)
} else {
return error(errors.New(
"StringSlice Scan source was not []bytes or string"))
}
// change quote escapes for csv parser
str = quoteEscapeRegex.ReplaceAllString(str, `$1""`)
str = strings.Replace(str, `\\`, `\`, -1)
// remove braces
str = str[1 : len(str)-1]
// if the result was empty, just return empty slice
if len(str) == 0 {
(*s) = StringSlice([]string{})
return nil
}
// otherwise, parse the list of values.
csvReader := csv.NewReader(strings.NewReader(str))
slice, err := csvReader.Read()
if err != nil {
return err
}
(*s) = StringSlice(slice)
return nil
}
Thanks a lot for sharing!! Let me show an example of working with arrays using pg.Array function, that leverages a little bit the mapping beteween go slices and psql arrays.
package main
import (
"database/sql"
"fmt"
"log"
"github.com/lib/pq"
)
const (
host = "localhost"
port = 5431
user = "user"
password = "secret :P" // actually would be difficult to guess
dbname = "testdb"
)
type SampleRow struct {
Id int
MultiplesIds []int64
}
func ensureSampleTableExists(db *sql.DB) {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS sample ( id serial NOT NULL,
multiples_ids bigint[],
CONSTRAINT sample_pk PRIMARY KEY (id)
)`)
if err != nil {
panic(err)
}
}
func main() {
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
"password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
db, err := sql.Open("postgres", psqlInfo)
if err != nil {
panic(err)
}
defer db.Close()
ensureSampleTableExists(db)
/*var x []int64 // must be int64 to work when reading... compatibility issues i guess...
db.QueryRow("SELECT ARRAY[235, 401]").Scan(pq.Array(&x))
log.Println(x)*/
insertRow(db, SampleRow{MultiplesIds: []int64{2, 3}})
samples, err := getRows(db)
if err != nil {
log.Panicln(err)
} else {
log.Println(samples)
}
}
func insertRow(Db *sql.DB, s SampleRow) (SampleRow, error) {
err := Db.QueryRow(`
INSERT INTO sample(multiples_ids)
VALUES ($1) RETURNING id`, pq.Array(s.MultiplesIds)).Scan(&s.Id)
return s, err
}
func getRows(Db *sql.DB) ([]SampleRow, error) {
rows, err := Db.Query(`
SELECT id, multiples_ids
FROM sample`)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
defer rows.Close()
samples := []SampleRow{}
for rows.Next() {
var sample SampleRow
err = rows.Scan(&sample.Id, pq.Array(&sample.MultiplesIds))
if err != nil {
log.Panicln("Error while scanning feched results", err)
return nil, err
}
samples = append(samples, sample)
}
return samples, nil
}
How about an integer slice? (not int64)
Man, nothing but thanks for this gist. It's going to save me almost an entire day.