فهرست منبع

feat: add small PostgreSQL scalar and aggregate compatibility functions (#26)

Danilo Fragoso 1 روز پیش
والد
کامیت
bdf53e0abe
5فایلهای تغییر یافته به همراه193 افزوده شده و 34 حذف شده
  1. 36 32
      pkg/analyzer/types.go
  2. 1 1
      pkg/capabilities/capabilities.go
  3. 59 1
      pkg/executor/executor.go
  4. 32 0
      pkg/executor/json.go
  5. 65 0
      pkg/pgserver/compat_functions_test.go

+ 36 - 32
pkg/analyzer/types.go

@@ -194,19 +194,21 @@ var builtinFunctions = map[string]FunctionSignature{
 	"MAX":          {Name: "MAX", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeAny, IsAggregate: true},
 	"TOTAL":        {Name: "TOTAL", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeNumeric}, ReturnType: TypeReal, IsAggregate: true},
 	"GROUP_CONCAT": {Name: "GROUP_CONCAT", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: true},
+	"STRING_AGG":   {Name: "STRING_AGG", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: true},
 
 	// String functions
-	"LENGTH":  {Name: "LENGTH", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeInteger, IsAggregate: false},
-	"UPPER":   {Name: "UPPER", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"LOWER":   {Name: "LOWER", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"TRIM":    {Name: "TRIM", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"LTRIM":   {Name: "LTRIM", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"RTRIM":   {Name: "RTRIM", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"SUBSTR":  {Name: "SUBSTR", MinArgs: 2, MaxArgs: 3, ArgTypes: []Type{TypeText, TypeInteger, TypeInteger}, ReturnType: TypeText, IsAggregate: false},
-	"REPLACE": {Name: "REPLACE", MinArgs: 3, MaxArgs: 3, ArgTypes: []Type{TypeText, TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"INSTR":   {Name: "INSTR", MinArgs: 2, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeInteger, IsAggregate: false},
-	"PRINTF":  {Name: "PRINTF", MinArgs: 1, MaxArgs: -1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"CONCAT":  {Name: "CONCAT", MinArgs: 1, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"LENGTH":    {Name: "LENGTH", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeInteger, IsAggregate: false},
+	"UPPER":     {Name: "UPPER", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"LOWER":     {Name: "LOWER", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"TRIM":      {Name: "TRIM", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"LTRIM":     {Name: "LTRIM", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"RTRIM":     {Name: "RTRIM", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"SUBSTR":    {Name: "SUBSTR", MinArgs: 2, MaxArgs: 3, ArgTypes: []Type{TypeText, TypeInteger, TypeInteger}, ReturnType: TypeText, IsAggregate: false},
+	"REPLACE":   {Name: "REPLACE", MinArgs: 3, MaxArgs: 3, ArgTypes: []Type{TypeText, TypeText, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"INSTR":     {Name: "INSTR", MinArgs: 2, MaxArgs: 2, ArgTypes: []Type{TypeText, TypeText}, ReturnType: TypeInteger, IsAggregate: false},
+	"PRINTF":    {Name: "PRINTF", MinArgs: 1, MaxArgs: -1, ArgTypes: []Type{TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"CONCAT":    {Name: "CONCAT", MinArgs: 1, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"CONCAT_WS": {Name: "CONCAT_WS", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
 
 	// Numeric functions
 	"ABS":    {Name: "ABS", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeNumeric}, ReturnType: TypeNumeric, IsAggregate: false},
@@ -238,6 +240,7 @@ var builtinFunctions = map[string]FunctionSignature{
 	// SQLite specific
 	"SQLITE_VERSION":    {Name: "SQLITE_VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
 	"PIZZASQL_VERSION":  {Name: "PIZZASQL_VERSION", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeText, IsAggregate: false},
+	"PG_BACKEND_PID":    {Name: "PG_BACKEND_PID", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
 	"LAST_INSERT_ROWID": {Name: "LAST_INSERT_ROWID", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
 	"CHANGES":           {Name: "CHANGES", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
 	"TOTAL_CHANGES":     {Name: "TOTAL_CHANGES", MinArgs: 0, MaxArgs: 0, ArgTypes: []Type{}, ReturnType: TypeInteger, IsAggregate: false},
@@ -252,27 +255,28 @@ var builtinFunctions = map[string]FunctionSignature{
 	"PERCENT_DIFF": {Name: "PERCENT_DIFF", MinArgs: 2, MaxArgs: 2, ArgTypes: []Type{TypeNumeric, TypeNumeric}, ReturnType: TypeReal, IsAggregate: false},
 
 	// JSON1 scalar functions.
-	"JSON":          {Name: "JSON", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB":         {Name: "JSONB", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_VALID":    {Name: "JSON_VALID", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeInteger, IsAggregate: false},
-	"JSONB_VALID":   {Name: "JSONB_VALID", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeInteger, IsAggregate: false},
-	"JSON_TYPE":     {Name: "JSON_TYPE", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_EXTRACT":  {Name: "JSON_EXTRACT", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeAny, IsAggregate: false},
-	"JSONB_EXTRACT": {Name: "JSONB_EXTRACT", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeAny, IsAggregate: false},
-	"JSON_SET":      {Name: "JSON_SET", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_SET":     {Name: "JSONB_SET", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_INSERT":   {Name: "JSON_INSERT", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_INSERT":  {Name: "JSONB_INSERT", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_REPLACE":  {Name: "JSON_REPLACE", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_REPLACE": {Name: "JSONB_REPLACE", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_REMOVE":   {Name: "JSON_REMOVE", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_REMOVE":  {Name: "JSONB_REMOVE", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_ARRAY":    {Name: "JSON_ARRAY", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_ARRAY":   {Name: "JSONB_ARRAY", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_OBJECT":   {Name: "JSON_OBJECT", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_OBJECT":  {Name: "JSONB_OBJECT", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSON_QUOTE":    {Name: "JSON_QUOTE", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
-	"JSONB_QUOTE":   {Name: "JSONB_QUOTE", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON":              {Name: "JSON", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB":             {Name: "JSONB", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_VALID":        {Name: "JSON_VALID", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeInteger, IsAggregate: false},
+	"JSONB_VALID":       {Name: "JSONB_VALID", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeInteger, IsAggregate: false},
+	"JSON_TYPE":         {Name: "JSON_TYPE", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_EXTRACT":      {Name: "JSON_EXTRACT", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeAny, IsAggregate: false},
+	"JSONB_EXTRACT":     {Name: "JSONB_EXTRACT", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeAny, IsAggregate: false},
+	"JSON_SET":          {Name: "JSON_SET", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB_SET":         {Name: "JSONB_SET", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_INSERT":       {Name: "JSON_INSERT", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB_INSERT":      {Name: "JSONB_INSERT", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_REPLACE":      {Name: "JSON_REPLACE", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB_REPLACE":     {Name: "JSONB_REPLACE", MinArgs: 3, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText, TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_REMOVE":       {Name: "JSON_REMOVE", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB_REMOVE":      {Name: "JSONB_REMOVE", MinArgs: 2, MaxArgs: -1, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_ARRAY":        {Name: "JSON_ARRAY", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_ARRAY_LENGTH": {Name: "JSON_ARRAY_LENGTH", MinArgs: 1, MaxArgs: 2, ArgTypes: []Type{TypeAny, TypeText}, ReturnType: TypeInteger, IsAggregate: false},
+	"JSONB_ARRAY":       {Name: "JSONB_ARRAY", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_OBJECT":       {Name: "JSON_OBJECT", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB_OBJECT":      {Name: "JSONB_OBJECT", MinArgs: 0, MaxArgs: -1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSON_QUOTE":        {Name: "JSON_QUOTE", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
+	"JSONB_QUOTE":       {Name: "JSONB_QUOTE", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: false},
 
 	// JSON1 aggregate.
 	"JSON_GROUP_ARRAY":  {Name: "JSON_GROUP_ARRAY", MinArgs: 1, MaxArgs: 1, ArgTypes: []Type{TypeAny}, ReturnType: TypeText, IsAggregate: true},

+ 1 - 1
pkg/capabilities/capabilities.go

@@ -216,7 +216,7 @@ func Current() *Document {
 
 		JSON: JSON{
 			Storage:        "TEXT (JSON and JSONB type names are accepted without native semantics; insert JSON as text)",
-			Functions:      "json, json_extract, json_set, json_insert, json_replace, json_remove, json_valid, json_type, json_array, json_object, json_quote, json_group_array",
+			Functions:      "json, json_extract, json_set, json_insert, json_replace, json_remove, json_valid, json_type, json_array, json_array_length, json_object, json_quote, json_group_array; PostgreSQL text compatibility: concat_ws, string_agg, pg_backend_pid",
 			ArrowOperators: Feature{Supported: false, Status: "rejected", SQLState: "42601", Notes: "-> and ->> are not implemented; use json_extract"},
 			InvalidInput:   "json_extract/json_set throw 'malformed JSON'; json_valid returns 0",
 		},

+ 59 - 1
pkg/executor/executor.go

@@ -5578,6 +5578,20 @@ func (e *Executor) evalFunctionCall(fn *parser.FunctionCall, row storage.Row) (i
 			result.WriteString(toString(arg))
 		}
 		return result.String(), nil
+	case "CONCAT_WS":
+		// PostgreSQL semantics: a NULL separator yields NULL; NULL arguments are
+		// skipped rather than converted to empty strings.
+		if len(args) == 0 || args[0] == nil {
+			return nil, nil
+		}
+		separator := toString(args[0])
+		parts := make([]string, 0, len(args)-1)
+		for _, arg := range args[1:] {
+			if arg != nil {
+				parts = append(parts, toString(arg))
+			}
+		}
+		return strings.Join(parts, separator), nil
 
 	// Date/Time functions
 	case "DATE":
@@ -5602,6 +5616,8 @@ func (e *Executor) evalFunctionCall(fn *parser.FunctionCall, row storage.Row) (i
 		return SQLiteCompatVersion, nil
 	case "LAST_INSERT_ROWID":
 		return e.lastInsertRowID, nil
+	case "PG_BACKEND_PID":
+		return int64(e.backendPID), nil
 	case "CHANGES":
 		return e.changes, nil
 	case "TOTAL_CHANGES":
@@ -6238,6 +6254,48 @@ func (e *Executor) evalAggregateExpr(expr parser.Expr, rows []storage.Row) (inte
 		}
 		return sum, nil
 
+	case "STRING_AGG":
+		if len(fn.Args) == 0 {
+			return nil, fmt.Errorf("STRING_AGG requires at least one argument")
+		}
+		separator := ""
+		if len(fn.Args) > 1 && len(rows) > 0 {
+			sep, err := e.evalExpr(fn.Args[1], rows[0])
+			if err != nil {
+				return nil, err
+			}
+			if sep == nil {
+				return nil, nil
+			}
+			separator = toString(sep)
+		}
+		var seen map[string]struct{}
+		if fn.Distinct {
+			seen = make(map[string]struct{})
+		}
+		var parts []string
+		for _, row := range rows {
+			val, err := e.evalExpr(fn.Args[0], row)
+			if err != nil {
+				return nil, err
+			}
+			if val == nil {
+				continue
+			}
+			text := toString(val)
+			if fn.Distinct {
+				if _, exists := seen[text]; exists {
+					continue
+				}
+				seen[text] = struct{}{}
+			}
+			parts = append(parts, text)
+		}
+		if len(parts) == 0 {
+			return nil, nil
+		}
+		return strings.Join(parts, separator), nil
+
 	case "GROUP_CONCAT":
 		if len(fn.Args) == 0 {
 			return nil, fmt.Errorf("GROUP_CONCAT requires at least one argument")
@@ -6324,7 +6382,7 @@ func (e *Executor) evalAggregateExpr(expr parser.Expr, rows []storage.Row) (inte
 // evaluators.
 func isAggregateCallName(name string) bool {
 	switch strings.ToUpper(name) {
-	case "COUNT", "SUM", "AVG", "MIN", "MAX", "TOTAL", "GROUP_CONCAT",
+	case "COUNT", "SUM", "AVG", "MIN", "MAX", "TOTAL", "GROUP_CONCAT", "STRING_AGG",
 		"JSON_GROUP_ARRAY", "JSONB_GROUP_ARRAY":
 		return true
 	default:

+ 32 - 0
pkg/executor/json.go

@@ -518,6 +518,38 @@ func evalJSONFunction(name string, args []interface{}) (interface{}, bool, error
 		}
 		return jsonText(s), true, nil
 
+	case "JSON_ARRAY_LENGTH":
+		if len(args) < 1 || len(args) > 2 {
+			return nil, true, fmt.Errorf("wrong number of arguments to json_array_length()")
+		}
+		if args[0] == nil {
+			return nil, true, nil
+		}
+		node, err := jsonInput(args[0])
+		if err != nil {
+			return nil, true, err
+		}
+		if len(args) == 2 {
+			path, ok := jsonPathArg(args[1])
+			if !ok {
+				return nil, true, fmt.Errorf("JSON path error")
+			}
+			segs, err := parseJSONPath(path)
+			if err != nil {
+				return nil, true, err
+			}
+			found, ok := jsonLookup(node, segs)
+			if !ok {
+				return nil, true, nil
+			}
+			node = found
+		}
+		if array, ok := node.([]interface{}); ok {
+			return int64(len(array)), true, nil
+		}
+		// SQLite's JSON1 reports zero for a valid non-array document.
+		return int64(0), true, nil
+
 	case "JSON_ARRAY", "JSONB_ARRAY":
 		arr := make([]interface{}, len(args))
 		for i, a := range args {

+ 65 - 0
pkg/pgserver/compat_functions_test.go

@@ -0,0 +1,65 @@
+package pgserver
+
+import (
+	"bufio"
+	"testing"
+)
+
+func queryValue(t *testing.T, c *Connection, sql string) ([]string, string) {
+	t.Helper()
+	client := &countingConn{}
+	c.conn = client
+	c.writer = bufio.NewWriter(client)
+	if err := c.handleMessage(&Message{Type: MsgQuery, Data: append([]byte(sql), 0)}); err != nil {
+		t.Fatal(err)
+	}
+	messages := readAllMessages(t, client.bytes())
+	if len(messages) > 0 && messages[0].Type == MsgErrorResponse {
+		return nil, errorCode(messages[0])
+	}
+	for _, message := range messages {
+		if message.Type == MsgDataRow {
+			return dataRowValues(t, message), ""
+		}
+	}
+	return nil, ""
+}
+
+func TestSmallPostgreSQLCompatibilityFunctions(t *testing.T) {
+	c, _ := newDataConnection(t)
+	if _, code := queryValue(t, c, "CREATE TABLE agg (v TEXT)"); code != "" {
+		t.Fatal(code)
+	}
+	if _, code := queryValue(t, c, "INSERT INTO agg VALUES ('x'),('y'),('x')"); code != "" {
+		t.Fatal(code)
+	}
+	for _, tc := range []struct{ sql, want string }{
+		{"SELECT concat_ws('-', 'a', NULL, 'b')", "a-b"},
+		{"SELECT concat_ws(NULL, 'a', 'b')", "<null>"},
+		{"SELECT concat_ws('-', 'a')", "a"},
+		{"SELECT json_array_length('[1,2,3]')", "3"},
+		{"SELECT json_array_length('{\"a\":1}')", "0"},
+		{"SELECT json_array_length('[1,[2,3]]', '$[1]')", "2"},
+		{"SELECT json_array_length('{\"a\":[1,2]}', '$.a')", "2"},
+		{"SELECT pg_backend_pid() = pg_backend_pid()", "true"},
+		{"SELECT string_agg(v, ',') FROM agg", "x,y,x"},
+		{"SELECT string_agg(DISTINCT v, ',') FROM agg", "x,y"},
+	} {
+		values, code := queryValue(t, c, tc.sql)
+		if code != "" {
+			t.Fatalf("%s: SQLSTATE %s", tc.sql, code)
+		}
+		if len(values) != 1 || values[0] != tc.want {
+			t.Fatalf("%s: got %v want %s", tc.sql, values, tc.want)
+		}
+	}
+	if values, code := queryValue(t, c, "SELECT json_array_length('not json')"); code == "" || len(values) != 0 {
+		t.Fatalf("malformed JSON accepted: %v %s", values, code)
+	}
+	if values, code := queryValue(t, c, "SELECT string_agg('x', NULL)"); code != "" || len(values) != 1 || values[0] != "<null>" {
+		t.Fatalf("NULL delimiter: %v %s", values, code)
+	}
+	if values, code := queryValue(t, c, "SELECT string_agg(NULL, ',')"); code != "" || len(values) != 1 || values[0] != "<null>" {
+		t.Fatalf("NULL input: %v %s", values, code)
+	}
+}