|
| 1 | +// Copyright 2026 Dolthub, Inc. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package _go |
| 16 | + |
| 17 | +import ( |
| 18 | + "context" |
| 19 | + "testing" |
| 20 | + |
| 21 | + "github.com/dolthub/dolt/go/libraries/utils/svcs" |
| 22 | + "github.com/dolthub/go-mysql-server/sql" |
| 23 | + "github.com/jackc/pgx/v5" |
| 24 | + "github.com/jackc/pgx/v5/pgconn" |
| 25 | + "github.com/stretchr/testify/assert" |
| 26 | + "github.com/stretchr/testify/require" |
| 27 | +) |
| 28 | + |
| 29 | +// TestMultipleStatements is a test for: https://github.com/dolthub/doltgresql/issues/2175 |
| 30 | +func TestMultipleStatements(t *testing.T) { |
| 31 | + ctx := context.Background() |
| 32 | + var conn *Connection |
| 33 | + if runOnPostgres { |
| 34 | + pgxConn, err := pgx.Connect(ctx, "postgres://postgres:password@127.0.0.1:5432/postgres?sslmode=disable") |
| 35 | + require.NoError(t, err) |
| 36 | + conn = &Connection{ |
| 37 | + Default: pgxConn, |
| 38 | + Current: pgxConn, |
| 39 | + } |
| 40 | + require.NoError(t, pgxConn.Ping(ctx)) |
| 41 | + defer func() { |
| 42 | + conn.Close(ctx) |
| 43 | + }() |
| 44 | + } else { |
| 45 | + var controller *svcs.Controller |
| 46 | + ctx, conn, controller = CreateServer(t, "postgres") |
| 47 | + defer func() { |
| 48 | + conn.Close(ctx) |
| 49 | + controller.Stop() |
| 50 | + err := controller.WaitForStop() |
| 51 | + require.NoError(t, err) |
| 52 | + }() |
| 53 | + } |
| 54 | + queries := []string{ |
| 55 | + `BEGIN;`, |
| 56 | + `DROP TABLE IF EXISTS migrations;`, |
| 57 | + `DROP TABLE IF EXISTS animals;`, |
| 58 | + `CREATE TABLE IF NOT EXISTS migrations (file_name TEXT NOT NULL, file_hash TEXT NOT NULL);`, |
| 59 | + `CREATE TABLE IF NOT EXISTS animals (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL);`, |
| 60 | + `;`, // This should be ignored in the output |
| 61 | + `INSERT INTO migrations (file_name, file_hash) VALUES ('2021-09-07T154500-create-animals-table.sql', '42331f4277227d09e9bb32eeaf7e04d9c7fe320160e05372ed0ef010cfbf666b');`, |
| 62 | + `INSERT INTO animals(name) VALUES('Alpaca');`, |
| 63 | + `INSERT INTO animals(name) VALUES('Highland cow');`, |
| 64 | + `INSERT INTO animals(name) VALUES('Aardvark');`, |
| 65 | + `INSERT INTO migrations (file_name, file_hash) VALUES ('2021-09-07T154700-insert-animals.sql', '3223d0deb6fb7fb2accf6abffc0667ebe4503379987c472d10a585a553f9b3b6');`, |
| 66 | + `SELECT * FROM migrations ORDER BY file_name;`, |
| 67 | + `SELECT * FROM animals ORDER BY id;`, |
| 68 | + `COMMIT;`, |
| 69 | + } |
| 70 | + combinedQueries := "" |
| 71 | + for _, query := range queries { |
| 72 | + // We do this just to homogenize the queries, even though we're adding the delimiter right back |
| 73 | + query = sql.RemoveSpaceAndDelimiter(query, ';') |
| 74 | + combinedQueries += query + ";" |
| 75 | + } |
| 76 | + // First we'll check all invalid modes that fail immediately |
| 77 | + invalidModes := []pgx.QueryExecMode{ |
| 78 | + pgx.QueryExecModeCacheStatement, |
| 79 | + pgx.QueryExecModeCacheDescribe, |
| 80 | + pgx.QueryExecModeDescribeExec, |
| 81 | + pgx.QueryExecModeExec, |
| 82 | + } |
| 83 | + for _, mode := range invalidModes { |
| 84 | + rows, err := conn.Current.Query(ctx, combinedQueries, mode) |
| 85 | + if mode == pgx.QueryExecModeExec { |
| 86 | + // This mode requires reading from the returned rows to find the error, rather than erroring immediately |
| 87 | + require.NoError(t, err) |
| 88 | + _ = rows.Next() |
| 89 | + err = rows.Err() |
| 90 | + } else { |
| 91 | + require.Error(t, err) |
| 92 | + } |
| 93 | + require.Contains(t, err.Error(), "cannot insert multiple commands into a prepared statement") |
| 94 | + } |
| 95 | + // Then we'll check the singular valid mode |
| 96 | + rows, err := conn.Current.Query(ctx, combinedQueries, pgx.QueryExecModeSimpleProtocol) |
| 97 | + require.NoError(t, err) |
| 98 | + require.False(t, rows.Next()) // Simple mode doesn't return results with multiple statements |
| 99 | + rows.Close() |
| 100 | + // Now we'll use the underlying connection to verify all returned results |
| 101 | + mrr := conn.Current.PgConn().Exec(ctx, combinedQueries) |
| 102 | + results, err := mrr.ReadAll() |
| 103 | + require.NoError(t, err) |
| 104 | + if assert.Len(t, results, len(testMultipleStatementsResults)) { |
| 105 | + for resultIdx, expected := range testMultipleStatementsResults { |
| 106 | + result := results[resultIdx] |
| 107 | + if assert.Equal(t, len(expected.FieldDescriptions), len(result.FieldDescriptions)) { |
| 108 | + for fieldIdx, expectedField := range expected.FieldDescriptions { |
| 109 | + resultField := result.FieldDescriptions[fieldIdx] |
| 110 | + assert.Equal(t, expectedField.Name, resultField.Name) |
| 111 | + assert.Equal(t, expectedField.DataTypeOID, resultField.DataTypeOID) |
| 112 | + assert.Equal(t, expectedField.DataTypeSize, resultField.DataTypeSize) |
| 113 | + assert.Equal(t, expectedField.TypeModifier, resultField.TypeModifier) |
| 114 | + assert.Equal(t, expectedField.Format, resultField.Format) |
| 115 | + } |
| 116 | + } |
| 117 | + if assert.Equal(t, len(expected.Rows), len(result.Rows)) { |
| 118 | + for rowIdx, expectedRow := range expected.Rows { |
| 119 | + resultRow := result.Rows[rowIdx] |
| 120 | + for columnIdx, expectedCol := range expectedRow { |
| 121 | + assert.Equal(t, expectedCol, resultRow[columnIdx]) |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + assert.Equal(t, expected.CommandTag, result.CommandTag) |
| 126 | + } |
| 127 | + } |
| 128 | + require.NoError(t, mrr.Close()) |
| 129 | + |
| 130 | + // Now we'll ensure that errors are properly handled within multiple statements |
| 131 | + queries = []string{ |
| 132 | + `INSERT INTO animals(name) VALUES('Pigeon');`, |
| 133 | + `SELECT * FROM non_existent;`, |
| 134 | + `INSERT INTO animals(name) VALUES('Elephant');`, |
| 135 | + } |
| 136 | + combinedQueries = "" |
| 137 | + for _, query := range queries { |
| 138 | + query = sql.RemoveSpaceAndDelimiter(query, ';') |
| 139 | + combinedQueries += query + ";" |
| 140 | + } |
| 141 | + mrr = conn.Current.PgConn().Exec(ctx, combinedQueries) |
| 142 | + results, err = mrr.ReadAll() |
| 143 | + require.Error(t, err) |
| 144 | + require.Contains(t, err.Error(), "non_existent") |
| 145 | + if assert.Len(t, results, 1) { |
| 146 | + assert.Equal(t, results[0].CommandTag, pgconn.NewCommandTag("INSERT 0 1")) |
| 147 | + } |
| 148 | +} |
| 149 | + |
| 150 | +// testMultipleStatementsResults are used within TestMultipleStatements |
| 151 | +var testMultipleStatementsResults = []pgconn.Result{ |
| 152 | + {CommandTag: pgconn.NewCommandTag("BEGIN")}, |
| 153 | + {CommandTag: pgconn.NewCommandTag("DROP TABLE")}, |
| 154 | + {CommandTag: pgconn.NewCommandTag("DROP TABLE")}, |
| 155 | + {CommandTag: pgconn.NewCommandTag("CREATE TABLE")}, |
| 156 | + {CommandTag: pgconn.NewCommandTag("CREATE TABLE")}, |
| 157 | + {CommandTag: pgconn.NewCommandTag("INSERT 0 1")}, |
| 158 | + {CommandTag: pgconn.NewCommandTag("INSERT 0 1")}, |
| 159 | + {CommandTag: pgconn.NewCommandTag("INSERT 0 1")}, |
| 160 | + {CommandTag: pgconn.NewCommandTag("INSERT 0 1")}, |
| 161 | + {CommandTag: pgconn.NewCommandTag("INSERT 0 1")}, |
| 162 | + { |
| 163 | + FieldDescriptions: []pgconn.FieldDescription{ |
| 164 | + { |
| 165 | + Name: "file_name", |
| 166 | + DataTypeOID: 25, |
| 167 | + DataTypeSize: -1, |
| 168 | + TypeModifier: -1, |
| 169 | + Format: 0, |
| 170 | + }, |
| 171 | + { |
| 172 | + Name: "file_hash", |
| 173 | + DataTypeOID: 25, |
| 174 | + DataTypeSize: -1, |
| 175 | + TypeModifier: -1, |
| 176 | + Format: 0, |
| 177 | + }, |
| 178 | + }, |
| 179 | + Rows: [][][]byte{ |
| 180 | + { |
| 181 | + []byte("2021-09-07T154500-create-animals-table.sql"), |
| 182 | + []byte("42331f4277227d09e9bb32eeaf7e04d9c7fe320160e05372ed0ef010cfbf666b"), |
| 183 | + }, |
| 184 | + { |
| 185 | + []byte("2021-09-07T154700-insert-animals.sql"), |
| 186 | + []byte("3223d0deb6fb7fb2accf6abffc0667ebe4503379987c472d10a585a553f9b3b6"), |
| 187 | + }, |
| 188 | + }, |
| 189 | + CommandTag: pgconn.NewCommandTag("SELECT 2"), |
| 190 | + }, |
| 191 | + { |
| 192 | + FieldDescriptions: []pgconn.FieldDescription{ |
| 193 | + { |
| 194 | + Name: "id", |
| 195 | + DataTypeOID: 23, |
| 196 | + DataTypeSize: 4, |
| 197 | + TypeModifier: -1, |
| 198 | + Format: 0, |
| 199 | + }, |
| 200 | + { |
| 201 | + Name: "name", |
| 202 | + DataTypeOID: 25, |
| 203 | + DataTypeSize: -1, |
| 204 | + TypeModifier: -1, |
| 205 | + Format: 0, |
| 206 | + }, |
| 207 | + }, |
| 208 | + Rows: [][][]byte{ |
| 209 | + { |
| 210 | + []byte("1"), |
| 211 | + []byte("Alpaca"), |
| 212 | + }, |
| 213 | + { |
| 214 | + []byte("2"), |
| 215 | + []byte("Highland cow"), |
| 216 | + }, |
| 217 | + { |
| 218 | + []byte("3"), |
| 219 | + []byte("Aardvark"), |
| 220 | + }, |
| 221 | + }, |
| 222 | + CommandTag: pgconn.NewCommandTag("SELECT 3"), |
| 223 | + }, |
| 224 | + {CommandTag: pgconn.NewCommandTag("COMMIT")}, |
| 225 | +} |
0 commit comments