|
|
@@ -4,6 +4,7 @@ import (
|
|
|
"bytes"
|
|
|
"encoding/binary"
|
|
|
"net"
|
|
|
+ "strconv"
|
|
|
"testing"
|
|
|
|
|
|
"github.com/danfragoso/pizzasql-next/pkg/executor"
|
|
|
@@ -16,27 +17,30 @@ func TestGetOIDForTypeDatetime(t *testing.T) {
|
|
|
"DATETIME": 1184,
|
|
|
"TIMESTAMP": 1184,
|
|
|
"DATE": 1082,
|
|
|
- "SMALLINT": 21,
|
|
|
- "INT2": 21,
|
|
|
- "TINYINT": 21,
|
|
|
- "MEDIUMINT": 23,
|
|
|
+ // Integer types narrower than the int64 carrier report int8 because the
|
|
|
+ // engine does not range-check stored values.
|
|
|
+ "SMALLINT": 20,
|
|
|
+ "INT2": 20,
|
|
|
+ "TINYINT": 20,
|
|
|
+ "MEDIUMINT": 20,
|
|
|
"INTEGER": 23,
|
|
|
"INT": 23,
|
|
|
"INT4": 23,
|
|
|
"BIGINT": 20,
|
|
|
"INT8": 20,
|
|
|
- "NUMERIC": 1700,
|
|
|
- "DECIMAL": 1700,
|
|
|
- "REAL": 700,
|
|
|
- "FLOAT": 700,
|
|
|
- "DOUBLE": 701,
|
|
|
- "TEXT": 25,
|
|
|
- "VARCHAR": 25,
|
|
|
- "CHAR": 25,
|
|
|
- "UUID": 25,
|
|
|
- "BOOLEAN": 16,
|
|
|
- "BOOL": 16,
|
|
|
- "BLOB": 17,
|
|
|
+ // NUMERIC/DECIMAL carry float64 values, so they report float8.
|
|
|
+ "NUMERIC": 701,
|
|
|
+ "DECIMAL": 701,
|
|
|
+ "REAL": 700,
|
|
|
+ "FLOAT": 700,
|
|
|
+ "DOUBLE": 701,
|
|
|
+ "TEXT": 25,
|
|
|
+ "VARCHAR": 25,
|
|
|
+ "CHAR": 25,
|
|
|
+ "UUID": 25,
|
|
|
+ "BOOLEAN": 16,
|
|
|
+ "BOOL": 16,
|
|
|
+ "BLOB": 17,
|
|
|
}
|
|
|
for name, want := range cases {
|
|
|
if got := c.getOIDForType(name); got != want {
|
|
|
@@ -128,15 +132,18 @@ func rowDescriptionOIDs(t *testing.T, data []byte) []int32 {
|
|
|
func TestGetTypeSizeForType(t *testing.T) {
|
|
|
c := &Connection{}
|
|
|
cases := map[string]int16{
|
|
|
- "SMALLINT": 2,
|
|
|
+ "SMALLINT": 8,
|
|
|
+ "TINYINT": 8,
|
|
|
+ "MEDIUMINT": 8,
|
|
|
"INTEGER": 4,
|
|
|
"BIGINT": 8,
|
|
|
"REAL": 4,
|
|
|
"DOUBLE": 8,
|
|
|
+ "NUMERIC": 8,
|
|
|
+ "DECIMAL": 8,
|
|
|
"BOOLEAN": 1,
|
|
|
"DATE": 4,
|
|
|
"TIMESTAMP": 8,
|
|
|
- "NUMERIC": -1,
|
|
|
"TEXT": -1,
|
|
|
"BLOB": -1,
|
|
|
}
|
|
|
@@ -181,17 +188,19 @@ func TestRowDescriptionProjectionOIDs(t *testing.T) {
|
|
|
{
|
|
|
name: "columns",
|
|
|
sql: "SELECT i, big, small, price, flag, name, data, created_at, day FROM typed",
|
|
|
- want: []int32{23, 20, 21, 1700, 16, 25, 17, 1184, 1082},
|
|
|
+ want: []int32{23, 20, 20, 701, 16, 25, 17, 1184, 1082},
|
|
|
},
|
|
|
{name: "count star", sql: "SELECT count(*) FROM typed", want: []int32{20}},
|
|
|
{name: "sum integer", sql: "SELECT sum(i) FROM typed", want: []int32{20}},
|
|
|
- {name: "avg integer", sql: "SELECT avg(i) FROM typed", want: []int32{1700}},
|
|
|
+ {name: "sum numeric", sql: "SELECT sum(price) FROM typed", want: []int32{701}},
|
|
|
+ {name: "avg integer", sql: "SELECT avg(i) FROM typed", want: []int32{701}},
|
|
|
{name: "comparison", sql: "SELECT i = 1 FROM typed", want: []int32{16}},
|
|
|
{name: "string literal", sql: "SELECT 'text'", want: []int32{25}},
|
|
|
{name: "integer literal", sql: "SELECT 42", want: []int32{23}},
|
|
|
{name: "wide integer literal", sql: "SELECT 4294967296", want: []int32{20}},
|
|
|
- {name: "numeric literal", sql: "SELECT 1.5", want: []int32{1700}},
|
|
|
+ {name: "numeric literal", sql: "SELECT 1.5", want: []int32{701}},
|
|
|
{name: "cast", sql: "SELECT CAST(i AS BIGINT) FROM typed", want: []int32{20}},
|
|
|
+ {name: "narrow integer cast", sql: "SELECT CAST(i AS SMALLINT) FROM typed", want: []int32{20}},
|
|
|
{
|
|
|
name: "empty result",
|
|
|
sql: "SELECT i, created_at, i + 1 FROM typed WHERE i = 999",
|
|
|
@@ -218,3 +227,204 @@ func TestRowDescriptionProjectionOIDs(t *testing.T) {
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// rowDescriptionColumns decodes the column names from a RowDescription message.
|
|
|
+func rowDescriptionColumns(t *testing.T, data []byte) []string {
|
|
|
+ t.Helper()
|
|
|
+ if len(data) < 2 {
|
|
|
+ t.Fatalf("row description too short")
|
|
|
+ }
|
|
|
+ count := int(binary.BigEndian.Uint16(data[:2]))
|
|
|
+ pos := 2
|
|
|
+ names := make([]string, 0, count)
|
|
|
+ for i := 0; i < count; i++ {
|
|
|
+ end := bytes.IndexByte(data[pos:], 0)
|
|
|
+ if end < 0 {
|
|
|
+ t.Fatalf("unterminated column name")
|
|
|
+ }
|
|
|
+ names = append(names, string(data[pos:pos+end]))
|
|
|
+ pos += end + 1 // name
|
|
|
+ pos += 4 // table OID
|
|
|
+ pos += 2 // column attribute number
|
|
|
+ pos += 4 // type OID
|
|
|
+ pos += 2 // type size
|
|
|
+ pos += 4 // type modifier
|
|
|
+ pos += 2 // format code
|
|
|
+ }
|
|
|
+ return names
|
|
|
+}
|
|
|
+
|
|
|
+// countMessageType counts messages of a type in a query response.
|
|
|
+func countMessageType(msgs []*Message, msgType byte) int {
|
|
|
+ count := 0
|
|
|
+ for _, msg := range msgs {
|
|
|
+ if msg.Type == msgType {
|
|
|
+ count++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return count
|
|
|
+}
|
|
|
+
|
|
|
+// TestFloatCarrierOIDsMatchValues asserts that projections carried as float64
|
|
|
+// are advertised as float8 and that their text values parse as float8. NUMERIC
|
|
|
+// (1700) is never advertised because the engine has no exact decimal carrier.
|
|
|
+func TestFloatCarrierOIDsMatchValues(t *testing.T) {
|
|
|
+ c, client := newDataConnection(t)
|
|
|
+ runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(
|
|
|
+ `CREATE TABLE floats (i INTEGER PRIMARY KEY, price NUMERIC, amount DECIMAL, score DOUBLE)`), 0)})
|
|
|
+ runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(
|
|
|
+ `INSERT INTO floats VALUES (1, 19.5, 3.25, 0.1)`), 0)})
|
|
|
+
|
|
|
+ cases := []struct {
|
|
|
+ name string
|
|
|
+ sql string
|
|
|
+ }{
|
|
|
+ {name: "numeric column", sql: "SELECT price FROM floats"},
|
|
|
+ {name: "decimal column", sql: "SELECT amount FROM floats"},
|
|
|
+ {name: "double column", sql: "SELECT score FROM floats"},
|
|
|
+ {name: "avg integer", sql: "SELECT avg(i) FROM floats"},
|
|
|
+ {name: "avg numeric", sql: "SELECT avg(price) FROM floats"},
|
|
|
+ {name: "sum numeric", sql: "SELECT sum(price) FROM floats"},
|
|
|
+ {name: "decimal literal", sql: "SELECT 1.5"},
|
|
|
+ {name: "float arithmetic", sql: "SELECT price + 0.5 FROM floats"},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range cases {
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
+ msgs := resultMessages(runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(tc.sql), 0)}))
|
|
|
+ oids := rowDescriptionOIDs(t, findRowDescription(t, msgs).Data)
|
|
|
+ if len(oids) != 1 {
|
|
|
+ t.Fatalf("%s: OIDs = %v, want one column", tc.sql, oids)
|
|
|
+ }
|
|
|
+ if oids[0] == 1700 {
|
|
|
+ t.Fatalf("%s: OID = 1700 (numeric) for a float64 carrier", tc.sql)
|
|
|
+ }
|
|
|
+ if oids[0] != 701 {
|
|
|
+ t.Fatalf("%s: OID = %d, want 701 (float8)", tc.sql, oids[0])
|
|
|
+ }
|
|
|
+ values := dataRowValues(t, msgs[1])
|
|
|
+ if len(values) != 1 {
|
|
|
+ t.Fatalf("%s: values = %v, want one value", tc.sql, values)
|
|
|
+ }
|
|
|
+ if _, err := strconv.ParseFloat(values[0], 64); err != nil {
|
|
|
+ t.Fatalf("%s: float8 value %q does not parse: %v", tc.sql, values[0], err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestNarrowIntegerOIDsUseCarrierType pins the chosen narrow-integer behavior:
|
|
|
+// the engine carries int64 without range checks, so SMALLINT/TINYINT/MEDIUMINT
|
|
|
+// report int8 (20) rather than a width a stored value may not fit.
|
|
|
+func TestNarrowIntegerOIDsUseCarrierType(t *testing.T) {
|
|
|
+ c, client := newDataConnection(t)
|
|
|
+ runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(
|
|
|
+ `CREATE TABLE narrow (id INTEGER PRIMARY KEY, small SMALLINT, tiny TINYINT, medium MEDIUMINT)`), 0)})
|
|
|
+ runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(
|
|
|
+ `INSERT INTO narrow VALUES (1, 40000, 200, 9000000)`), 0)})
|
|
|
+
|
|
|
+ cases := []struct {
|
|
|
+ name string
|
|
|
+ sql string
|
|
|
+ value string
|
|
|
+ }{
|
|
|
+ {name: "smallint column", sql: "SELECT small FROM narrow", value: "40000"},
|
|
|
+ {name: "tinyint column", sql: "SELECT tiny FROM narrow", value: "200"},
|
|
|
+ {name: "mediumint column", sql: "SELECT medium FROM narrow", value: "9000000"},
|
|
|
+ {name: "cast smallint", sql: "SELECT CAST(small AS SMALLINT) FROM narrow", value: "40000"},
|
|
|
+ {name: "cast integer", sql: "SELECT CAST(small AS INTEGER) FROM narrow", value: "40000"},
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range cases {
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
+ msgs := resultMessages(runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(tc.sql), 0)}))
|
|
|
+ oids := rowDescriptionOIDs(t, findRowDescription(t, msgs).Data)
|
|
|
+ if len(oids) != 1 || oids[0] != 20 {
|
|
|
+ t.Fatalf("%s: OIDs = %v, want [20] (int8 carrier)", tc.sql, oids)
|
|
|
+ }
|
|
|
+ values := dataRowValues(t, msgs[1])
|
|
|
+ if len(values) != 1 || values[0] != tc.value {
|
|
|
+ t.Fatalf("%s: values = %v, want [%s]", tc.sql, values, tc.value)
|
|
|
+ }
|
|
|
+ if _, err := strconv.ParseInt(values[0], 10, 64); err != nil {
|
|
|
+ t.Fatalf("%s: int8 value %q does not parse: %v", tc.sql, values[0], err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestEmptyResultWildcardMatchesNames guards the constant-false WHERE path:
|
|
|
+// wildcard projections must expand names and types into the same columns.
|
|
|
+func TestEmptyResultWildcardMatchesNames(t *testing.T) {
|
|
|
+ c, client := newDataConnection(t)
|
|
|
+ runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(
|
|
|
+ `CREATE TABLE star_t (id INTEGER PRIMARY KEY, created_at TIMESTAMP, name TEXT)`), 0)})
|
|
|
+ runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(
|
|
|
+ `CREATE TABLE star_b (b_id BIGINT PRIMARY KEY, score DOUBLE)`), 0)})
|
|
|
+
|
|
|
+ cases := []struct {
|
|
|
+ name string
|
|
|
+ sql string
|
|
|
+ wantNames []string
|
|
|
+ wantOIDs []int32
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "qualified star",
|
|
|
+ sql: "SELECT star_t.* FROM star_t WHERE 1 = 0",
|
|
|
+ wantNames: []string{"id", "created_at", "name"},
|
|
|
+ wantOIDs: []int32{23, 1184, 25},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "aliased qualified star",
|
|
|
+ sql: "SELECT s.* FROM star_t AS s WHERE 1 = 0",
|
|
|
+ wantNames: []string{"id", "created_at", "name"},
|
|
|
+ wantOIDs: []int32{23, 1184, 25},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "star",
|
|
|
+ sql: "SELECT * FROM star_t WHERE 1 = 0",
|
|
|
+ wantNames: []string{"id", "created_at", "name"},
|
|
|
+ wantOIDs: []int32{23, 1184, 25},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "column and expression",
|
|
|
+ sql: "SELECT star_t.name, star_t.id + 1 FROM star_t WHERE 1 = 0",
|
|
|
+ wantNames: []string{"name", "column2"},
|
|
|
+ wantOIDs: []int32{25, 20},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "join star",
|
|
|
+ sql: "SELECT * FROM star_t JOIN star_b ON star_t.id = star_b.b_id WHERE 1 = 0",
|
|
|
+ wantNames: []string{"id", "created_at", "name", "b_id", "score"},
|
|
|
+ wantOIDs: []int32{23, 1184, 25, 20, 701},
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tc := range cases {
|
|
|
+ t.Run(tc.name, func(t *testing.T) {
|
|
|
+ msgs := resultMessages(runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(tc.sql), 0)}))
|
|
|
+ rd := findRowDescription(t, msgs)
|
|
|
+ names := rowDescriptionColumns(t, rd.Data)
|
|
|
+ oids := rowDescriptionOIDs(t, rd.Data)
|
|
|
+ if len(names) != len(tc.wantNames) {
|
|
|
+ t.Fatalf("%s: names = %v, want %v", tc.sql, names, tc.wantNames)
|
|
|
+ }
|
|
|
+ for i := range tc.wantNames {
|
|
|
+ if names[i] != tc.wantNames[i] {
|
|
|
+ t.Fatalf("%s: names = %v, want %v", tc.sql, names, tc.wantNames)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if len(oids) != len(tc.wantOIDs) {
|
|
|
+ t.Fatalf("%s: OIDs = %v, want %v", tc.sql, oids, tc.wantOIDs)
|
|
|
+ }
|
|
|
+ for i := range tc.wantOIDs {
|
|
|
+ if oids[i] != tc.wantOIDs[i] {
|
|
|
+ t.Fatalf("%s: OIDs = %v, want %v", tc.sql, oids, tc.wantOIDs)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if rows := countMessageType(msgs, MsgDataRow); rows != 0 {
|
|
|
+ t.Fatalf("%s: got %d data rows, want 0", tc.sql, rows)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|