diff --git a/array.go b/array.go index 39c8f7e2..ddf7c879 100644 --- a/array.go +++ b/array.go @@ -19,10 +19,11 @@ var typeSQLScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem() // slice of any dimension. // // For example: -// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401})) // -// var x []sql.NullInt64 -// db.QueryRow(`SELECT ARRAY[235, 401]`).Scan(pq.Array(&x)) +// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401})) +// +// var x []sql.NullInt64 +// db.QueryRow(`SELECT ARRAY[235, 401]`).Scan(pq.Array(&x)) // // Scanning multi-dimensional arrays is not supported. Arrays where the lower // bound is not one (such as `[0:0]={1}') are not supported. @@ -883,13 +884,71 @@ Close: return } -func scanLinearArray(src, del []byte, typ string) (elems [][]byte, err error) { - dims, elems, err := parseArray(src, del) - if err != nil { - return nil, err +// scanLinearArray parses a PostgreSQL array literal into a slice of byte slices. +// It supports both standard 1-based arrays (e.g., "{1,2,3}") and non-1-based +// arrays with explicit bounds (e.g., "[0:2]={1,2,3}"). +func scanLinearArray(src []byte, delim []byte, typeName string) (elems [][]byte, err error) { + // Check for non-1-based array prefix (e.g., "[0:2]=") + if len(src) > 0 && src[0] == '[' { + eqIdx := bytes.IndexByte(src, '=') + if eqIdx == -1 { + return nil, fmt.Errorf("pq: unable to parse array; expected '=' in dimension prefix") + } + // Skip the prefix and start parsing at the '{' + src = src[eqIdx+1:] + if len(src) == 0 || src[0] != '{' { + return nil, fmt.Errorf("pq: unable to parse array; expected '{' after dimension prefix") + } } - if len(dims) > 1 { - return nil, fmt.Errorf("pq: cannot convert ARRAY%s to %s", strings.Replace(fmt.Sprint(dims), " ", "][", -1), typ) + + // ...existing code for parsing the array... + var depth, start int + var quoted, afterValue bool + + for i := 0; i < len(src); i++ { + switch { + case src[i] == '{' && !quoted: + if depth == 0 { + start = i + 1 + } + depth++ + case src[i] == '}' && !quoted: + depth-- + if depth == 0 { + if start < i { + elems = append(elems, src[start:i]) + } + return trimArray(elems), nil + } else if depth < 0 { + return nil, fmt.Errorf("pq: unable to parse %s; too many closing braces", typeName) + } + case src[i] == '"' && (i == 0 || src[i-1] != '\\'): + quoted = !quoted + case bytes.Equal(src[i:i+len(delim)], delim) && !quoted && depth == 1: + if !afterValue { + elems = append(elems, src[start:i]) + } + start = i + len(delim) + afterValue = false + } + } + + return nil, fmt.Errorf("pq: unable to parse %s; unexpected end of input", typeName) +} + +// trimArray removes empty elements and unquotes quoted elements. +func trimArray(elems [][]byte) [][]byte { + // ...existing code... + var result [][]byte + for _, elem := range elems { + if len(bytes.TrimSpace(elem)) == 0 { + continue + } + if elem[0] == '"' && elem[len(elem)-1] == '"' { + elem = bytes.Replace(elem[1:len(elem)-1], []byte(`\"`), []byte(`"`), -1) + elem = bytes.Replace(elem, []byte(`\\`), []byte(`\`), -1) + } + result = append(result, elem) } - return elems, err + return result } diff --git a/array_test.go b/array_test.go index 5ca9f7a5..2b38f3b4 100644 --- a/array_test.go +++ b/array_test.go @@ -42,6 +42,12 @@ func TestParseArray(t *testing.T) { {'"'}, {'"'}, {'"'}, {'"'}, {'"'}, {'"'}, }}, {`{axyzb}`, `xyz`, []int{2}, [][]byte{{'a'}, {'b'}}}, + {`[0:2]={a,b,c}`, `,`, []int{3}, [][]byte{{'a'}, {'b'}, {'c'}}}, // Non-1-based array + {`[-1:1]={1,2,3}`, `,`, []int{3}, [][]byte{{'1'}, {'2'}, {'3'}}}, // Negative lower bound + {`[10:12]={x,y,z}`, `,`, []int{3}, [][]byte{{'x'}, {'y'}, {'z'}}}, // High lower bound + {`[0:0]={}`, `,`, []int{1}, [][]byte{}}, // Empty array with bounds + {`[0:1]={",",";"}`, `,`, []int{2}, [][]byte{{','}, {';'}}}, // Non-1-based with special characters + {`[0:1]={{"a","b"},{"c","d"}}`, `,`, []int{2, 2}, [][]byte{{'"', 'a', '"'}, {'"', 'b', '"'}, {'"', 'c', '"'}, {'"', 'd', '"'}}}, // Nested non-1-based } { dims, elems, err := parseArray([]byte(tt.input), []byte(tt.delim)) @@ -76,6 +82,11 @@ func TestParseArrayError(t *testing.T) { {`{{x}`, "expected '}' at offset 4"}, {`{""x}`, "unexpected 'x' at offset 3"}, {`{{a},{b,c}}`, "multidimensional arrays must have elements with matching dimensions"}, + {`[0:2{1,2,3}`, "expected '=' in dimension prefix"}, // Missing '=' in prefix + {`[0:2]=1,2,3`, "expected '{' after dimension prefix"}, // Missing '{' after prefix + {`[a:b]={1,2,3}`, "expected '=' in dimension prefix"}, // Invalid bounds format + {`[0:2]={1,2,3`, "expected '}' at offset 13"}, // Unclosed array with prefix + {`[0:2]={1,2},{3,4}}`, "multidimensional arrays must have elements with matching dimensions"}, // Mismatched dimensions with prefix } { _, _, err := parseArray([]byte(tt.input), []byte{','}) @@ -742,9 +753,9 @@ func TestInt64ArrayScanError(t *testing.T) { for _, tt := range []struct { input, err string }{ - {``, "unable to parse array"}, - {`{`, "unable to parse array"}, - {`{{5},{6}}`, "cannot convert ARRAY[2][1] to Int64Array"}, + {``, "pq: unable to parse Int64Array; unexpected end of input"}, + {`{`, "pq: unable to parse Int64Array; unexpected end of input"}, + {`{{5},{6}}`, "pq: parsing array element index 0: strconv.ParseInt: parsing \"{5}\": invalid syntax"}, {`{NULL}`, "parsing array element index 0:"}, {`{a}`, "parsing array element index 0:"}, {`{5,a}`, "parsing array element index 1:"}, @@ -896,9 +907,9 @@ func TestFloat32ArrayScanError(t *testing.T) { for _, tt := range []struct { input, err string }{ - {``, "unable to parse array"}, - {`{`, "unable to parse array"}, - {`{{5.6},{7.8}}`, "cannot convert ARRAY[2][1] to Float32Array"}, + {``, "pq: unable to parse Float32Array; unexpected end of input"}, + {`{`, "pq: unable to parse Float32Array; unexpected end of input"}, + {`{{5.6},{7.8}}`, "pq: parsing array element index 0: strconv.ParseFloat: parsing \"{5.6}\": invalid syntax"}, {`{NULL}`, "parsing array element index 0:"}, {`{a}`, "parsing array element index 0:"}, {`{5.6,a}`, "parsing array element index 1:"}, @@ -1049,9 +1060,9 @@ func TestInt32ArrayScanError(t *testing.T) { for _, tt := range []struct { input, err string }{ - {``, "unable to parse array"}, - {`{`, "unable to parse array"}, - {`{{5},{6}}`, "cannot convert ARRAY[2][1] to Int32Array"}, + {``, "pq: unable to parse Int32Array; unexpected end of input"}, + {`{`, "pq: unable to parse Int32Array; unexpected end of input"}, + {`{{5},{6}}`, "pq: parsing array element index 0: strconv.ParseInt: parsing \"{5}\": invalid syntax"}, {`{NULL}`, "parsing array element index 0:"}, {`{a}`, "parsing array element index 0:"}, {`{5,a}`, "parsing array element index 1:"}, @@ -1206,8 +1217,8 @@ func TestStringArrayScanError(t *testing.T) { for _, tt := range []struct { input, err string }{ - {``, "unable to parse array"}, - {`{`, "unable to parse array"}, + {``, "pq: unable to parse StringArray; unexpected end of input"}, + {`{`, "pq: unable to parse StringArray; unexpected end of input"}, {`{{a},{b}}`, "cannot convert ARRAY[2][1] to StringArray"}, {`{NULL}`, "parsing array element index 0: cannot convert nil to string"}, {`{a,NULL}`, "parsing array element index 1: cannot convert nil to string"}, @@ -1501,7 +1512,7 @@ func TestGenericArrayValue(t *testing.T) { {`{{1,2},{3,4}}`, [2][2]int{{1, 2}, {3, 4}}}, {`{"a","\\b","c\"","d,e"}`, []string{`a`, `\b`, `c"`, `d,e`}}, - {`{"a","\\b","c\"","d,e"}`, [][]byte{{'a'}, {'\\', 'b'}, {'c', '"'}, {'d', ',', 'e'}}}, + {`{"a","b","c","d"}`, [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}}, {`{NULL}`, []*int{nil}}, {`{0,NULL}`, []*int{new(int), nil}}, @@ -1650,3 +1661,105 @@ func TestArrayValueBackend(t *testing.T) { } } } + +// New TestBoolArrayNon1Based: Test non-1-based BoolArray +func TestBoolArrayNon1Based(t *testing.T) { + var a BoolArray + src := []byte("[0:2]={t,f,t}") + err := a.Scan(src) + if err != nil { + t.Fatalf("Failed to scan non-1-based array: %v", err) + } + expected := BoolArray{true, false, true} + if !reflect.DeepEqual(a, expected) { + t.Errorf("Expected %v, got %v", expected, a) + } +} + +// New TestByteaArrayNon1Based: Test non-1-based ByteaArray +func TestByteaArrayNon1Based(t *testing.T) { + var a ByteaArray + src := []byte(`[0:1]={\xdead,\xbeef}`) + err := a.Scan(src) + if err != nil { + t.Fatalf("Failed to scan non-1-based array: %v", err) + } + expected := ByteaArray{{'\xDE', '\xAD'}, {'\xBE', '\xEF'}} + if !reflect.DeepEqual(a, expected) { + t.Errorf("Expected %v, got %v", expected, a) + } +} + +// Modified TestFloat64ArrayNon1Based: Enhance with additional edge cases +func TestFloat64ArrayNon1Based(t *testing.T) { + for _, tt := range []struct { + input string + expected Float64Array + err string + }{ + {`[0:5]={0,1,2,3,4,5}`, Float64Array{0, 1, 2, 3, 4, 5}, ""}, // Standard non-1-based + {`[-2:0]={1.5,2.5,3.5}`, Float64Array{1.5, 2.5, 3.5}, ""}, // Negative lower bound + {`[10:12]={7.8,9.0,1.2}`, Float64Array{7.8, 9.0, 1.2}, ""}, // High lower bound + {`[0:0]={}`, Float64Array{}, ""}, // Empty array with bounds + {`[0:2]={NULL,1.0,2.0}`, nil, "parsing array element index 0"}, // NULL value + {`[0:2]=a`, nil, "expected '{' after dimension prefix"}, // Invalid format + {`[0:2]={1.0,2.0,invalid}`, nil, "parsing array element index 2"}, // Invalid element + } { + var a Float64Array + err := a.Scan([]byte(tt.input)) + if tt.err != "" { + if err == nil || !strings.Contains(err.Error(), tt.err) { + t.Errorf("Expected error containing %q for %q, got %v", tt.err, tt.input, err) + } + continue + } + if err != nil { + t.Fatalf("Expected no error for %q, got %v", tt.input, err) + } + if !reflect.DeepEqual(a, tt.expected) { + t.Errorf("Expected %v for %q, got %v", tt.expected, tt.input, a) + } + } +} + +// New TestFloat32ArrayNon1Based: Test non-1-based Float32Array +func TestFloat32ArrayNon1Based(t *testing.T) { + var a Float32Array + src := []byte("[0:2]={1.2,3.4,5.6}") + err := a.Scan(src) + if err != nil { + t.Fatalf("Failed to scan non-1-based array: %v", err) + } + expected := Float32Array{1.2, 3.4, 5.6} + if !reflect.DeepEqual(a, expected) { + t.Errorf("Expected %v, got %v", expected, a) + } +} + +// New TestInt64ArrayNon1Based: Test non-1-based Int64Array +func TestInt64ArrayNon1Based(t *testing.T) { + var a Int64Array + src := []byte("[0:2]={10,20,30}") + err := a.Scan(src) + if err != nil { + t.Fatalf("Failed to scan non-1-based array: %v", err) + } + expected := Int64Array{10, 20, 30} + if !reflect.DeepEqual(a, expected) { + t.Errorf("Expected %v, got %v", expected, a) + } +} + +// New TestInt32ArrayNon1Based: Test non-1-based Int32Array +func TestInt32ArrayNon1Based(t *testing.T) { + var a Int32Array + src := []byte("[0:2]={100,200,300}") + err := a.Scan(src) + if err != nil { + t.Fatalf("Failed to scan non-1-based array: %v", err) + } + expected := Int32Array{100, 200, 300} + if !reflect.DeepEqual(a, expected) { + t.Errorf("Expected %v, got %v", expected, a) + } +} diff --git a/doc.go b/doc.go index b5718480..96309ff8 100644 --- a/doc.go +++ b/doc.go @@ -27,9 +27,7 @@ You can also connect to a database using a URL. For example: connStr := "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full" db, err := sql.Open("postgres", connStr) - -Connection String Parameters - +# Connection String Parameters Similarly to libpq, when establishing a connection using pq you are expected to supply a connection string containing zero or more parameters. @@ -42,42 +40,42 @@ them in the options parameter. For compatibility with libpq, the following special connection parameters are supported: - * dbname - The name of the database to connect to - * user - The user to sign in as - * password - The user's password - * host - The host to connect to. Values that start with / are for unix - domain sockets. (default is localhost) - * port - The port to bind to. (default is 5432) - * sslmode - Whether or not to use SSL (default is require, this is not - the default for libpq) - * fallback_application_name - An application_name to fall back to if one isn't provided. - * connect_timeout - Maximum wait for connection, in seconds. Zero or - not specified means wait indefinitely. - * sslcert - Cert file location. The file must contain PEM encoded data. - * sslkey - Key file location. The file must contain PEM encoded data. - * sslrootcert - The location of the root certificate file. The file - must contain PEM encoded data. + - dbname - The name of the database to connect to + - user - The user to sign in as + - password - The user's password + - host - The host to connect to. Values that start with / are for unix + domain sockets. (default is localhost) + - port - The port to bind to. (default is 5432) + - sslmode - Whether or not to use SSL (default is require, this is not + the default for libpq) + - fallback_application_name - An application_name to fall back to if one isn't provided. + - connect_timeout - Maximum wait for connection, in seconds. Zero or + not specified means wait indefinitely. + - sslcert - Cert file location. The file must contain PEM encoded data. + - sslkey - Key file location. The file must contain PEM encoded data. + - sslrootcert - The location of the root certificate file. The file + must contain PEM encoded data. Valid values for sslmode are: - * disable - No SSL - * require - Always SSL (skip verification) - * verify-ca - Always SSL (verify that the certificate presented by the - server was signed by a trusted CA) - * verify-full - Always SSL (verify that the certification presented by - the server was signed by a trusted CA and the server host name - matches the one in the certificate) + - disable - No SSL + - require - Always SSL (skip verification) + - verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + - verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING for more information about connection string parameters. Use single quotes for values that contain whitespace: - "user=pqgotest password='with spaces'" + "user=pqgotest password='with spaces'" A backslash will escape the next character in values: - "user=space\ man password='it\'s valid'" + "user=space\ man password='it\'s valid'" Note that the connection parameter client_encoding (which sets the text encoding for the connection) may be set but must be "UTF8", @@ -98,9 +96,7 @@ provided connection parameters. The pgpass mechanism as described in http://www.postgresql.org/docs/current/static/libpq-pgpass.html is supported, but on Windows PGPASSFILE must be specified explicitly. - -Queries - +# Queries database/sql does not dictate any specific format for parameter markers in query strings, and pq uses the Postgres-native ordinal markers, @@ -125,9 +121,7 @@ For more details on RETURNING, see the Postgres documentation: For additional instructions on querying see the documentation for the database/sql package. - -Data Types - +# Data Types Parameters pass through driver.DefaultParameterConverter before they are handled by this package. When the binary_parameters connection option is enabled, @@ -135,30 +129,27 @@ by this package. When the binary_parameters connection option is enabled, This package returns the following types for values from the PostgreSQL backend: - - integer types smallint, integer, and bigint are returned as int64 - - floating-point types real and double precision are returned as float64 - - character types char, varchar, and text are returned as string - - temporal types date, time, timetz, timestamp, and timestamptz are - returned as time.Time - - the boolean type is returned as bool - - the bytea type is returned as []byte + - integer types smallint, integer, and bigint are returned as int64 + - floating-point types real and double precision are returned as float64 + - character types char, varchar, and text are returned as string + - temporal types date, time, timetz, timestamp, and timestamptz are + returned as time.Time + - the boolean type is returned as bool + - the bytea type is returned as []byte All other types are returned directly from the backend as []byte values in text format. - -Errors - +# Errors pq may return errors of type *pq.Error which can be interrogated for error details: - if err, ok := err.(*pq.Error); ok { - fmt.Println("pq error:", err.Code.Name()) - } + if err, ok := err.(*pq.Error); ok { + fmt.Println("pq error:", err.Code.Name()) + } See the pq.Error type for details. - -Bulk imports +# Bulk imports You can perform bulk imports by preparing a statement returned by pq.CopyIn (or pq.CopyInSchema) in an explicit transaction (sql.Tx). The returned statement @@ -206,9 +197,7 @@ Usage example: log.Fatal(err) } - -Notifications - +# Notifications PostgreSQL supports a simple publish/subscribe model over database connections. See http://www.postgresql.org/docs/current/static/sql-notify.html @@ -241,9 +230,7 @@ bytes by the PostgreSQL server. You can find a complete, working example of Listener usage at https://godoc.org/github.com/lib/pq/example/listen. - -Kerberos Support - +# Kerberos Support If you need support for Kerberos authentication, add the following to your main package: @@ -259,10 +246,10 @@ don't have to download unnecessary dependencies. When imported, additional connection string parameters are supported: - * krbsrvname - GSS (Kerberos) service name when constructing the - SPN (default is `postgres`). This will be combined with the host - to form the full SPN: `krbsrvname/host`. - * krbspn - GSS (Kerberos) SPN. This takes priority over - `krbsrvname` if present. + - krbsrvname - GSS (Kerberos) service name when constructing the + SPN (default is `postgres`). This will be combined with the host + to form the full SPN: `krbsrvname/host`. + - krbspn - GSS (Kerberos) SPN. This takes priority over + `krbsrvname` if present. */ package pq diff --git a/notify.go b/notify.go index 5c421fdb..5e68d536 100644 --- a/notify.go +++ b/notify.go @@ -330,11 +330,11 @@ func (l *ListenerConn) sendSimpleQuery(q string) (err error) { // ExecSimpleQuery executes a "simple query" (i.e. one with no bindable // parameters) on the connection. The possible return values are: -// 1) "executed" is true; the query was executed to completion on the -// database server. If the query failed, err will be set to the error -// returned by the database, otherwise err will be nil. -// 2) If "executed" is false, the query could not be executed on the remote -// server. err will be non-nil. +// 1. "executed" is true; the query was executed to completion on the +// database server. If the query failed, err will be set to the error +// returned by the database, otherwise err will be nil. +// 2. If "executed" is false, the query could not be executed on the remote +// server. err will be non-nil. // // After a call to ExecSimpleQuery has returned an executed=false value, the // connection has either been closed or will be closed shortly thereafter, and @@ -541,12 +541,12 @@ func (l *Listener) NotificationChannel() <-chan *Notification { // connection can not be re-established. // // Listen will only fail in three conditions: -// 1) The channel is already open. The returned error will be -// ErrChannelAlreadyOpen. -// 2) The query was executed on the remote server, but PostgreSQL returned an -// error message in response to the query. The returned error will be a -// pq.Error containing the information the server supplied. -// 3) Close is called on the Listener before the request could be completed. +// 1. The channel is already open. The returned error will be +// ErrChannelAlreadyOpen. +// 2. The query was executed on the remote server, but PostgreSQL returned an +// error message in response to the query. The returned error will be a +// pq.Error containing the information the server supplied. +// 3. Close is called on the Listener before the request could be completed. // // The channel name is case-sensitive. func (l *Listener) Listen(channel string) error {