Как вы уже знаете, array_agg
принимает аргумент single и возвращает массив типа аргумента. Итак, если вы хотите, чтобы все столбцы строки были включены в элементы массива, вы можете просто передать ссылку на строку напрямую, например:
SELECT array_agg(b) FROM b
Если, однако, вы хотите включить только указание c столбцы в элементах массива, вы можете использовать конструктор ROW
, например:
SELECT array_agg(ROW(b.stuff, b.other)) FROM b
Go Стандартная библиотека предоставляет готовые решения поддержка сканирования только скалярных значений. Для сканирования более сложных значений, таких как произвольные объекты и массивы, нужно либо искать сторонние решения, либо реализовывать свои собственные sql.Scanner
.
Чтобы иметь возможность реализовать свои собственные sql.Scanner
и правильно проанализировать массив postgres строк, для начала вам нужно знать, какой формат postgres использует для вывода значения, вы можете узнать это с помощью psql
и некоторых запросов напрямую:
-- simple values
SELECT ARRAY[ROW(123,'foo'),ROW(456,'bar')];
-- output: {"(123,foo)","(456,bar)"}
-- not so simple values
SELECT ARRAY[ROW(1,'a b'),ROW(2,'a,b'),ROW(3,'a",b'),ROW(4,'(a,b)'),ROW(5,'"','""')];
-- output: {"(1,\"a b\")","(2,\"a,b\")","(3,\"a\"\",b\")","(4,\"(a,b)\")","(5,\"\"\"\",\"\"\"\"\"\")"}
Как вы можете видеть, это может показаться довольно странным, но, тем не менее, его можно разобрать, синтаксис выглядит примерно так:
{"(column_value[, ...])"[, ...]}
, где column_value
- это либо значение без кавычек, либо значение в кавычках с экранированными двойными кавычками и само такое значение в кавычках может содержать экранированные двойные кавычки, но только в двойках, то есть внутри экранированной column_value
не будет одиночной экранированной двойной кавычки. Таким образом, грубая и неполная реализация синтаксического анализатора может выглядеть примерно так:
ПРИМЕЧАНИЕ: могут существовать другие синтаксические правила, о которых я не знаю, которые необходимо учитывать при синтаксическом анализе. В дополнение к этому код ниже не обрабатывает NULL должным образом.
func parseRowArray(a []byte) (out [][]string) {
a = a[1 : len(a)-1] // drop surrounding curlies
for i := 0; i < len(a); i++ {
if a[i] == '"' { // start of row element
row := []string{}
i += 2 // skip over current '"' and the following '('
for j := i; j < len(a); j++ {
if a[j] == '\\' && a[j+1] == '"' { // start of quoted column value
var col string // column value
j += 2 // skip over current '\' and following '"'
for k := j; k < len(a); k++ {
if a[k] == '\\' && a[k+1] == '"' { // end of quoted column, maybe
if a[k+2] == '\\' && a[k+3] == '"' { // nope, just escaped quote
col += string(a[j:k]) + `"`
k += 3 // skip over `\"\` (the k++ in the for statement will skip over the `"`)
j = k + 1 // skip over `\"\"`
continue // go to k loop
} else { // yes, end of quoted column
col += string(a[j:k])
row = append(row, col)
j = k + 2 // skip over `\"`
break // go back to j loop
}
}
}
if a[j] == ')' { // row end
out = append(out, row)
i = j + 1 // advance i to j's position and skip the potential ','
break // go to back i loop
}
} else { // assume non quoted column value
for k := j; k < len(a); k++ {
if a[k] == ',' || a[k] == ')' { // column value end
col := string(a[j:k])
row = append(row, col)
j = k // advance j to k's position
break // go back to j loop
}
}
if a[j] == ')' { // row end
out = append(out, row)
i = j + 1 // advance i to j's position and skip the potential ','
break // go to back i loop
}
}
}
}
}
return out
}
Попробуйте его на playground
.
С чем-то подобным вам затем можно реализовать sql.Scanner
для вашего Go среза баров.
type BarList []*Bar
func (ls *BarList) Scan(src interface{}) error {
switch data := src.(type) {
case []byte:
a := praseRowArray(data)
res := make(BarList, len(a))
for i := 0; i < len(a); i++ {
bar := new(Bar)
// Here i'm assuming the parser produced a slice of at least two
// strings, if there are cases where this may not be the true you
// should add proper length checks to avoid unnecessary panics.
bar.Stuff = a[i][0]
bar.Other = a[i][1]
res[i] = bar
}
*ls = res
}
return nil
}
Теперь, если вы измените тип поля Bars
в типе Foo
с []*Bar
на BarList
вы сможете напрямую передать указатель поля на (*sql.Row|*sql.Rows).Scan
вызов:
rows.Scan(&f.Bars)
Если вы не хотите изменять тип поля, вы все равно можете заставить его работать, преобразовав указатель, когда он передается методу Scan
:
rows.Scan((*BarList)(&f.Bars))
JSON
Реализация sql.Scanner
для решения json, предложенного Генри Вуди будет выглядеть примерно так:
type BarList []*Bar
func (ls *BarList) Scan(src interface{}) error {
if b, ok := src.([]byte); ok {
return json.Unmarshal(b, ls)
}
return nil
}