Skip to content

Instantly share code, notes, and snippets.

@dalberto
Forked from adharris/postgres_array.go
Last active August 29, 2015 14:26
Show Gist options
  • Save dalberto/fecee15cd14b36e27ff0 to your computer and use it in GitHub Desktop.
Save dalberto/fecee15cd14b36e27ff0 to your computer and use it in GitHub Desktop.

Revisions

  1. @invalid-email-address Anonymous created this gist Nov 28, 2012.
    260 changes: 260 additions & 0 deletions postgres_array.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,260 @@
    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")
    }