1507 lines
48 KiB
Go
1507 lines
48 KiB
Go
// Copyright 2026 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
|
)
|
|
|
|
// setupTestServer creates a standard single-server test environment. The repo
|
|
// name is also used as the database name. Cleanup is registered with t.Cleanup.
|
|
func setupTestServer(t *testing.T, repoName string) *driver.SqlServer {
|
|
t.Helper()
|
|
ports := newPorts(t)
|
|
|
|
u, err := driver.NewDoltUser()
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { u.Cleanup() })
|
|
|
|
rs, err := u.MakeRepoStore()
|
|
require.NoError(t, err)
|
|
|
|
_, err = rs.MakeRepo(repoName)
|
|
require.NoError(t, err)
|
|
|
|
server := StartServer(t, rs, repoName, &driver.Server{
|
|
Args: []string{},
|
|
DynamicPort: "server_port",
|
|
}, ports)
|
|
return server
|
|
}
|
|
|
|
// makeTestText returns a deterministic ASCII string of exactly size bytes.
|
|
// The seed value differentiates rows so each row has unique content.
|
|
func makeTestText(seed, size int) string {
|
|
chunk := fmt.Sprintf("[row%07d-filler]", seed) // 18 bytes
|
|
n := (size + len(chunk) - 1) / len(chunk)
|
|
return strings.Repeat(chunk, n)[:size]
|
|
}
|
|
|
|
// makeTestBlobData returns a deterministic byte slice of exactly size bytes.
|
|
func makeTestBlobData(seed, size int) []byte {
|
|
data := make([]byte, size)
|
|
for i := range data {
|
|
data[i] = byte((seed*37 + i*17 + seed*i*3) & 0xFF)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// largeJSONDoc is the top-level structure used to generate large JSON values.
|
|
type largeJSONDoc struct {
|
|
ID int `json:"id"`
|
|
Description string `json:"description"`
|
|
Tags []string `json:"tags"`
|
|
Items []largeJSONItem `json:"items"`
|
|
}
|
|
|
|
type largeJSONItem struct {
|
|
Index int `json:"index"`
|
|
Name string `json:"name"`
|
|
Payload string `json:"payload"`
|
|
}
|
|
|
|
// makeTestJSONString returns a JSON string of at least targetSize bytes built
|
|
// from deterministic data keyed by seed.
|
|
func makeTestJSONString(seed, targetSize int) string {
|
|
doc := largeJSONDoc{
|
|
ID: seed,
|
|
Description: strings.Repeat(fmt.Sprintf("desc-seed%07d-", seed), 10),
|
|
}
|
|
for i := range 15 {
|
|
doc.Tags = append(doc.Tags, fmt.Sprintf("tag-%d-%d", seed, i))
|
|
}
|
|
for i := 0; ; i++ {
|
|
item := largeJSONItem{
|
|
Index: i,
|
|
Name: fmt.Sprintf("item-%d-%05d", seed, i),
|
|
Payload: strings.Repeat(fmt.Sprintf("pl-%d-%d-", seed, i), 12),
|
|
}
|
|
doc.Items = append(doc.Items, item)
|
|
bs, _ := json.Marshal(doc)
|
|
if len(bs) >= targetSize {
|
|
return string(bs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildWideCreateTable generates a CREATE TABLE statement with numCols columns
|
|
// of the given colType in addition to a BIGINT PRIMARY KEY column named "id".
|
|
func buildWideCreateTable(tableName string, numCols int, colType string) string {
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "CREATE TABLE %s (id BIGINT PRIMARY KEY", tableName)
|
|
for i := range numCols {
|
|
fmt.Fprintf(&sb, ", c%d %s", i, colType)
|
|
}
|
|
sb.WriteString(")")
|
|
return sb.String()
|
|
}
|
|
|
|
// buildWideInsert generates an INSERT statement for a wide table. makeVal is
|
|
// called for each column index and should return a SQL literal (not quoted for
|
|
// numeric types; caller wraps strings in single quotes).
|
|
func buildWideInsert(tableName string, rowID int, numCols int, makeVal func(col int) string) string {
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "INSERT INTO %s VALUES (%d", tableName, rowID)
|
|
for i := range numCols {
|
|
fmt.Fprintf(&sb, ", %s", makeVal(i))
|
|
}
|
|
sb.WriteString(")")
|
|
return sb.String()
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// TestLargeOutOfBandValues exercises rows whose individual column values
|
|
// exceed 10,000 bytes — the range where Dolt stores values out-of-band in
|
|
// TEXT, BLOB, and JSON columns. Each subtest commits the data, reads it back
|
|
// to verify integrity, runs GC, and then re-verifies.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestLargeOutOfBandValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("LargeText", func(t *testing.T) {
|
|
t.Parallel()
|
|
const textSize = 15_000
|
|
const numRows = 20
|
|
|
|
server := setupTestServer(t, "large_text_values")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
_, err = conn.ExecContext(ctx, "CREATE TABLE large_text (id BIGINT PRIMARY KEY, txt TEXT)")
|
|
require.NoError(t, err)
|
|
for i := range numRows {
|
|
_, err = conn.ExecContext(ctx, "INSERT INTO large_text VALUES ($1, $2)", i, makeTestText(i, textSize))
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'insert large text rows')")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
verifyLargeText := func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM large_text").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// All rows must have the correct byte length. Doltgres panics on
|
|
// octet_length() over large out-of-band text values, so the length
|
|
// is verified client-side.
|
|
count = countTextRowsOfLen(t, conn, ctx, "SELECT txt FROM large_text", textSize)
|
|
require.Equal(t, numRows, count, "every row should retain the full text length")
|
|
|
|
// Spot-check a few rows for exact content.
|
|
for _, id := range []int{0, numRows / 2, numRows - 1} {
|
|
var actual string
|
|
err = conn.QueryRowContext(ctx, "SELECT txt FROM large_text WHERE id = $1", id).Scan(&actual)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestText(id, textSize), actual,
|
|
"row %d: text content must survive storage and retrieval", id)
|
|
}
|
|
}
|
|
|
|
verifyLargeText()
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
// dolt_gc invalidates open connections; reconnect with a fresh pool.
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
|
|
verifyLargeText()
|
|
})
|
|
|
|
t.Run("LargeBlob", func(t *testing.T) {
|
|
t.Parallel()
|
|
const blobSize = 25_000
|
|
const numRows = 20
|
|
|
|
server := setupTestServer(t, "large_blob_values")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
_, err = conn.ExecContext(ctx, "CREATE TABLE large_blob (id BIGINT PRIMARY KEY, data BYTEA)")
|
|
require.NoError(t, err)
|
|
for i := range numRows {
|
|
_, err = conn.ExecContext(ctx, "INSERT INTO large_blob VALUES ($1, $2)", i, makeTestBlobData(i, blobSize))
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'insert large blob rows')")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
verifyLargeBlob := func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM large_blob").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Doltgres has no server-side byte-length function for bytea
|
|
// (octet_length/length/encode are unavailable), so verify every
|
|
// row's blob length on the client side instead.
|
|
lenRows, err := conn.QueryContext(ctx, "SELECT data FROM large_blob")
|
|
require.NoError(t, err)
|
|
count = 0
|
|
for lenRows.Next() {
|
|
var d []byte
|
|
require.NoError(t, lenRows.Scan(&d))
|
|
require.Equal(t, blobSize, len(d), "every row should retain the full blob length")
|
|
count++
|
|
}
|
|
require.NoError(t, lenRows.Err())
|
|
lenRows.Close()
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Spot-check exact binary content for a few rows.
|
|
for _, id := range []int{0, numRows / 2, numRows - 1} {
|
|
var actual []byte
|
|
err = conn.QueryRowContext(ctx, "SELECT data FROM large_blob WHERE id = $1", id).Scan(&actual)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestBlobData(id, blobSize), actual,
|
|
"row %d: blob content must survive storage and retrieval", id)
|
|
}
|
|
}
|
|
|
|
verifyLargeBlob()
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
|
|
verifyLargeBlob()
|
|
})
|
|
|
|
t.Run("LargeJSON", func(t *testing.T) {
|
|
t.Parallel()
|
|
const jsonTargetSize = 12_000
|
|
const numRows = 20
|
|
|
|
server := setupTestServer(t, "large_json_values")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
// Pre-generate JSON values so we know their exact sizes.
|
|
jsonVals := make([]string, numRows)
|
|
for i := range numRows {
|
|
jsonVals[i] = makeTestJSONString(i, jsonTargetSize)
|
|
require.GreaterOrEqual(t, len(jsonVals[i]), jsonTargetSize,
|
|
"generated JSON should meet the target size floor")
|
|
}
|
|
|
|
func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
_, err = conn.ExecContext(ctx, "CREATE TABLE large_json (id BIGINT PRIMARY KEY, doc JSON)")
|
|
require.NoError(t, err)
|
|
for i, v := range jsonVals {
|
|
_, err = conn.ExecContext(ctx, "INSERT INTO large_json VALUES ($1, $2)", i, v)
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'insert large json rows')")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
verifyLargeJSON := func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM large_json").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Verify that the JSON round-trip preserves the document ID field.
|
|
// Doltgres panics on JSON operators (e.g. ->>) over large
|
|
// out-of-band JSON values, so the whole document is retrieved and
|
|
// parsed client-side instead.
|
|
for _, id := range []int{0, numRows / 2, numRows - 1} {
|
|
var docStr string
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT doc FROM large_json WHERE id = $1", id).Scan(&docStr)
|
|
require.NoError(t, err)
|
|
var parsed largeJSONDoc
|
|
require.NoError(t, json.Unmarshal([]byte(docStr), &parsed))
|
|
require.Equal(t, id, parsed.ID,
|
|
"row %d: JSON id field must be preserved after storage", id)
|
|
}
|
|
}
|
|
|
|
verifyLargeJSON()
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
|
|
verifyLargeJSON()
|
|
})
|
|
|
|
t.Run("MixedLargeColumns", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Each row carries large values in three separate out-of-band column
|
|
// types simultaneously, exercising the case where a single logical row
|
|
// requires multiple large external chunks.
|
|
const textSize = 18_000
|
|
const blobSize = 22_000
|
|
const jsonTarget = 11_000
|
|
const numRows = 10
|
|
|
|
server := setupTestServer(t, "mixed_large_columns")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE mixed_large (
|
|
id BIGINT PRIMARY KEY,
|
|
txt TEXT,
|
|
bin_data BYTEA,
|
|
doc JSON,
|
|
note TEXT
|
|
)`)
|
|
require.NoError(t, err)
|
|
for i := range numRows {
|
|
note := makeTestText(i+1000, 5_000) // secondary TEXT column, ~5 KB
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO mixed_large VALUES ($1, $2, $3, $4, $5)",
|
|
i,
|
|
makeTestText(i, textSize),
|
|
makeTestBlobData(i, blobSize),
|
|
makeTestJSONString(i, jsonTarget),
|
|
note,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'insert mixed large column rows')")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
verifyMixed := func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM mixed_large").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Verify lengths of all large columns. Doltgres has no bytea length
|
|
// function and panics on octet_length() over large out-of-band text,
|
|
// so the lengths are checked client-side.
|
|
lenRows, err := conn.QueryContext(ctx, "SELECT txt, bin_data, note FROM mixed_large")
|
|
require.NoError(t, err)
|
|
count = 0
|
|
for lenRows.Next() {
|
|
var txtv, notev string
|
|
var binv []byte
|
|
require.NoError(t, lenRows.Scan(&txtv, &binv, ¬ev))
|
|
require.Equal(t, textSize, len(txtv))
|
|
require.Equal(t, blobSize, len(binv))
|
|
require.Equal(t, 5000, len(notev))
|
|
count++
|
|
}
|
|
require.NoError(t, lenRows.Err())
|
|
lenRows.Close()
|
|
require.Equal(t, numRows, count, "all large column lengths must be preserved")
|
|
|
|
// Spot-check content integrity.
|
|
for _, id := range []int{0, numRows - 1} {
|
|
var actualTxt string
|
|
var actualBlob []byte
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT txt, bin_data FROM mixed_large WHERE id = $1", id).
|
|
Scan(&actualTxt, &actualBlob)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestText(id, textSize), actualTxt)
|
|
require.Equal(t, makeTestBlobData(id, blobSize), actualBlob)
|
|
}
|
|
}
|
|
|
|
verifyMixed()
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
|
|
verifyMixed()
|
|
})
|
|
|
|
t.Run("ConcurrentLargeValueWrites", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Multiple goroutines writing large TEXT and BLOB rows concurrently,
|
|
// stress-testing the out-of-band storage path under writer contention.
|
|
const textSize = 12_000
|
|
const blobSize = 15_000
|
|
const numWorkers = 8
|
|
const rowsPerWorker = 10
|
|
|
|
server := setupTestServer(t, "concurrent_large_values")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
func() {
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE large_concurrent (
|
|
id BIGINT PRIMARY KEY,
|
|
txt TEXT,
|
|
data BYTEA
|
|
)`)
|
|
require.NoError(t, err)
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'create large_concurrent table')")
|
|
require.NoError(t, err)
|
|
}()
|
|
|
|
eg, egCtx := errgroup.WithContext(ctx)
|
|
startCh := make(chan struct{})
|
|
readyCh := make(chan struct{})
|
|
|
|
for w := range numWorkers {
|
|
eg.Go(func() error {
|
|
workerDB, err := server.DB(driver.Connection{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer workerDB.Close()
|
|
workerDB.SetMaxOpenConns(1)
|
|
conn, err := workerDB.Conn(egCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
|
|
select {
|
|
case readyCh <- struct{}{}:
|
|
case <-egCtx.Done():
|
|
return nil
|
|
}
|
|
select {
|
|
case <-startCh:
|
|
case <-egCtx.Done():
|
|
return nil
|
|
}
|
|
|
|
for j := range rowsPerWorker {
|
|
if egCtx.Err() != nil {
|
|
return nil
|
|
}
|
|
rowID := int64(w*rowsPerWorker + j)
|
|
seed := int(rowID)
|
|
_, err = conn.ExecContext(egCtx,
|
|
"INSERT INTO large_concurrent VALUES ($1, $2, $3)",
|
|
rowID,
|
|
makeTestText(seed, textSize),
|
|
makeTestBlobData(seed, blobSize),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("worker %d insert row %d: %w", w, j, err)
|
|
}
|
|
}
|
|
_, err = conn.ExecContext(egCtx,
|
|
fmt.Sprintf("SELECT dolt_commit('-Am', 'worker %d inserts')", w))
|
|
if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
|
|
return fmt.Errorf("worker %d commit: %w", w, err)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
for range numWorkers {
|
|
select {
|
|
case <-readyCh:
|
|
case <-ctx.Done():
|
|
require.NoError(t, eg.Wait())
|
|
t.FailNow()
|
|
}
|
|
}
|
|
close(startCh)
|
|
require.NoError(t, eg.Wait())
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM large_concurrent").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numWorkers*rowsPerWorker, count,
|
|
"all rows from all workers must be present")
|
|
|
|
// Doltgres has no bytea length function and panics on octet_length()
|
|
// over large out-of-band text, so verify sizes client-side.
|
|
lenRows, err := conn.QueryContext(ctx, "SELECT txt, data FROM large_concurrent")
|
|
require.NoError(t, err)
|
|
count = 0
|
|
for lenRows.Next() {
|
|
var txtv string
|
|
var datav []byte
|
|
require.NoError(t, lenRows.Scan(&txtv, &datav))
|
|
require.Equal(t, textSize, len(txtv))
|
|
require.Equal(t, blobSize, len(datav))
|
|
count++
|
|
}
|
|
require.NoError(t, lenRows.Err())
|
|
lenRows.Close()
|
|
require.Equal(t, numWorkers*rowsPerWorker, count,
|
|
"all large values must have the correct size")
|
|
})
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// TestTypeDiversity verifies that Doltgres correctly stores and retrieves a
|
|
// broad spectrum of column types, including boundary values, NULL, unicode
|
|
// strings, and values that cross the inline/out-of-band threshold. Each
|
|
// subtest operates on its own server so they can run in parallel.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestTypeDiversity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("IntegerTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
t.Skip("Doltgres panics (nil pointer dereference) handling a NUMERIC column holding the uint64 max value 18446744073709551615")
|
|
server := setupTestServer(t, "integer_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
// Postgres has no unsigned integer types; the "unsigned" columns are
|
|
// widened to the next-larger signed type so the max boundary values fit.
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE int_types (
|
|
id BIGINT PRIMARY KEY,
|
|
col_tinyint SMALLINT,
|
|
col_smallint SMALLINT,
|
|
col_mediumint INTEGER,
|
|
col_int INTEGER,
|
|
col_bigint BIGINT,
|
|
col_tinyint_u SMALLINT,
|
|
col_smallint_u INTEGER,
|
|
col_int_u BIGINT,
|
|
col_bigint_u NUMERIC
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
rows := []struct {
|
|
id, ti, si, mi, i int64
|
|
bi int64
|
|
tiu, siu uint64
|
|
iu uint64
|
|
biu uint64
|
|
}{
|
|
{0, -128, -32768, -8388608, -2147483648, -9223372036854775808, 0, 0, 0, 0},
|
|
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
{2, 127, 32767, 8388607, 2147483647, 9223372036854775807, 255, 65535, 4294967295, 18446744073709551615},
|
|
{3, 42, 1000, 100000, 1000000, 1000000000000, 200, 50000, 2000000000, 9000000000000000000},
|
|
}
|
|
for _, r := range rows {
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO int_types VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)",
|
|
r.id, r.ti, r.si, r.mi, r.i, r.bi, r.tiu, r.siu, r.iu, r.biu)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'integer types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM int_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count)
|
|
|
|
// Verify boundary values survive round-trip.
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM int_types WHERE col_tinyint = -128 AND col_bigint = -9223372036854775808").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "min boundary values must be stored exactly")
|
|
|
|
var bigU string
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT col_bigint_u::text FROM int_types WHERE col_tinyint = 127").Scan(&bigU)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "18446744073709551615", bigU, "max unsigned bigint boundary value must be stored exactly")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM int_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count, "integer rows must survive GC")
|
|
})
|
|
|
|
t.Run("FloatingPointAndDecimal", func(t *testing.T) {
|
|
t.Parallel()
|
|
server := setupTestServer(t, "float_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE float_types (
|
|
id BIGINT PRIMARY KEY,
|
|
col_float REAL,
|
|
col_double DOUBLE PRECISION,
|
|
col_dec DECIMAL(30,10)
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
type floatRow struct {
|
|
id int
|
|
f float32
|
|
d float64
|
|
dec string
|
|
}
|
|
rows := []floatRow{
|
|
{0, 0.0, 0.0, "0.0000000000"},
|
|
{1, 1.5, 1.5, "1.5000000000"},
|
|
{2, -1.5, -1.5, "-1.5000000000"},
|
|
{3, 3.14159, 3.14159265358979, "3.1415926536"},
|
|
{4, 1e10, 1e15, "99999999999999999999.9999999999"},
|
|
{5, -1e10, -1e15, "-99999999999999999999.9999999999"},
|
|
}
|
|
for _, r := range rows {
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO float_types VALUES ($1,$2,$3,$4)",
|
|
r.id, r.f, r.d, r.dec)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'float types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM float_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count)
|
|
|
|
// Decimal is exact — verify the stored value.
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM float_types WHERE col_dec = 3.1415926536").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "decimal value must be stored and retrieved exactly")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM float_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count, "float rows must survive GC")
|
|
})
|
|
|
|
t.Run("StringTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
server := setupTestServer(t, "string_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE string_types (
|
|
id BIGINT PRIMARY KEY,
|
|
col_char CHAR(100),
|
|
col_varchar VARCHAR(2000),
|
|
col_text TEXT,
|
|
col_medtext TEXT,
|
|
col_longtext TEXT
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
// Mix of empty, short, medium, large, unicode, and NULL values.
|
|
vals := []struct {
|
|
id int
|
|
ch, vc, tx, mtx, ltx interface{}
|
|
}{
|
|
{0, "", "", "", "", ""},
|
|
{1, "hello", "world", "short text", "medium text", "long text"},
|
|
// unicode and multi-byte characters
|
|
{2,
|
|
"日本語テスト",
|
|
"Ünïcödé strïng wïth vàrïöüs chàrs: αβγδεζηθ ℕ ℤ ℚ ℝ ℂ",
|
|
strings.Repeat("中文测试内容-", 100),
|
|
strings.Repeat("한국어 테스트 데이터-", 500),
|
|
strings.Repeat("العربية اختبار البيانات-", 1000),
|
|
},
|
|
// large values that cross the out-of-band threshold
|
|
{3,
|
|
strings.Repeat("x", 100),
|
|
strings.Repeat("v", 2000),
|
|
makeTestText(300, 12_000),
|
|
makeTestText(301, 50_000),
|
|
makeTestText(302, 200_000),
|
|
},
|
|
// NULL values
|
|
{4, nil, nil, nil, nil, nil},
|
|
}
|
|
|
|
for _, r := range vals {
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO string_types VALUES ($1,$2,$3,$4,$5,$6)",
|
|
r.id, r.ch, r.vc, r.tx, r.mtx, r.ltx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'string types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM string_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(vals), count)
|
|
|
|
// Verify the large TEXT value was preserved intact (200 KB of ASCII).
|
|
// Doltgres panics on octet_length() over large out-of-band text, so the
|
|
// length is checked client-side.
|
|
var longText string
|
|
err = conn.QueryRowContext(ctx, "SELECT col_longtext FROM string_types WHERE id = 3").Scan(&longText)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200000, len(longText), "200 KB TEXT value must be stored and retrieved intact")
|
|
|
|
// Verify the NULL row.
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM string_types WHERE col_char IS NULL AND col_varchar IS NULL").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "NULL string columns must be stored as NULL")
|
|
|
|
// Verify unicode content round-trip. CHAR(100) is blank-padded in
|
|
// Postgres, so trim trailing spaces before comparing.
|
|
var unicodeChar string
|
|
err = conn.QueryRowContext(ctx, "SELECT col_char FROM string_types WHERE id = 2").Scan(&unicodeChar)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "日本語テスト", strings.TrimRight(unicodeChar, " "), "unicode CHAR value must round-trip unchanged")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM string_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(vals), count, "string rows must survive GC")
|
|
})
|
|
|
|
t.Run("BinaryTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
server := setupTestServer(t, "binary_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
// Postgres has a single variable-length binary type (bytea); there is
|
|
// no fixed-width BINARY(n), so all binary columns map to bytea.
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE binary_types (
|
|
id BIGINT PRIMARY KEY,
|
|
col_binary BYTEA,
|
|
col_varbinary BYTEA,
|
|
col_blob BYTEA,
|
|
col_longblob BYTEA
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
vals := []struct {
|
|
id int
|
|
bi []byte
|
|
vb []byte
|
|
bl []byte
|
|
lb []byte
|
|
}{
|
|
{0, make([]byte, 32), []byte{}, []byte{}, []byte{}},
|
|
{1, makeTestBlobData(1, 32), makeTestBlobData(2, 500), makeTestBlobData(3, 8_000), makeTestBlobData(4, 30_000)},
|
|
{2, makeTestBlobData(5, 32), makeTestBlobData(6, 2000), makeTestBlobData(7, 65_000), makeTestBlobData(8, 500_000)},
|
|
{3, nil, nil, nil, nil},
|
|
}
|
|
|
|
for _, r := range vals {
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO binary_types VALUES ($1,$2,$3,$4,$5)",
|
|
r.id, r.bi, r.vb, r.bl, r.lb)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'binary types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM binary_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(vals), count)
|
|
|
|
// Verify the 500 KB bytea value survived. Doltgres has no server-side
|
|
// byte-length function for bytea, so check the length client-side.
|
|
var lb500 []byte
|
|
err = conn.QueryRowContext(ctx, "SELECT col_longblob FROM binary_types WHERE id = 2").Scan(&lb500)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 500_000, len(lb500), "500 KB bytea value must be stored and retrieved intact")
|
|
|
|
// Spot-check exact blob content.
|
|
var actualLB []byte
|
|
err = conn.QueryRowContext(ctx, "SELECT col_longblob FROM binary_types WHERE id = 2").Scan(&actualLB)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestBlobData(8, 500_000), actualLB,
|
|
"500 KB bytea round-trip must be byte-perfect")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM binary_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(vals), count, "binary rows must survive GC")
|
|
})
|
|
|
|
t.Run("DateTimeTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
server := setupTestServer(t, "datetime_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
// Postgres has no YEAR type and its DATE/TIME ranges differ from
|
|
// MySQL's, so the extreme MySQL-only boundary values are replaced with
|
|
// values that are valid in Postgres while still exercising round-trip,
|
|
// fractional seconds, and NULL handling. YEAR maps to a plain integer.
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE datetime_types (
|
|
id BIGINT PRIMARY KEY,
|
|
col_date DATE,
|
|
col_time TIME,
|
|
col_datetime TIMESTAMP(6),
|
|
col_ts TIMESTAMP(6),
|
|
col_year INTEGER
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
type dtRow struct {
|
|
id int
|
|
date interface{}
|
|
tm interface{}
|
|
dt interface{}
|
|
ts interface{}
|
|
yr interface{}
|
|
}
|
|
rows := []dtRow{
|
|
{0, "1000-01-01", "00:00:00", "1000-01-01 00:00:00.000000", nil, 1901},
|
|
{1, "2024-06-15", "00:00:00", "2024-06-15 12:30:45.123456", "2024-06-15 12:30:45.123456", 2024},
|
|
{2, "9999-12-31", "23:59:59", "9999-12-31 23:59:59.999999", "2038-01-19 03:14:07.000000", 2155},
|
|
{3, nil, nil, nil, nil, nil},
|
|
}
|
|
for _, r := range rows {
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO datetime_types VALUES ($1,$2,$3,$4,$5,$6)",
|
|
r.id, r.date, r.tm, r.dt, r.ts, r.yr)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'datetime types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM datetime_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count)
|
|
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM datetime_types WHERE col_date = '1000-01-01'").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "minimum DATE value must round-trip correctly")
|
|
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM datetime_types WHERE col_date IS NULL AND col_time IS NULL").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "NULL date/time values must be stored as NULL")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM datetime_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count, "datetime rows must survive GC")
|
|
})
|
|
|
|
t.Run("SpecialTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
server := setupTestServer(t, "special_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
// Postgres has no ENUM/SET literal types in this dialect; ENUM maps to
|
|
// varchar and SET (a multi-valued column) maps to varchar holding a
|
|
// comma-separated value. FIND_IN_SET is expressed with string matching.
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE special_types (
|
|
id BIGINT PRIMARY KEY,
|
|
col_bool BOOLEAN,
|
|
col_json JSON,
|
|
col_enum VARCHAR(32),
|
|
col_set VARCHAR(64)
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
type specialRow struct {
|
|
id int
|
|
b interface{}
|
|
j interface{} // JSON value (string or nil)
|
|
e interface{}
|
|
s interface{}
|
|
}
|
|
rows := []specialRow{
|
|
{0, false, `{}`, "alpha", "red"},
|
|
{1, true, `{"key": "value", "num": 42, "arr": [1,2,3]}`, "beta", "red,green"},
|
|
{2, nil, makeTestJSONString(200, 15_000), "gamma", "red,green,blue"},
|
|
{3, false, `null`, "delta", "red,green,blue,yellow"},
|
|
{4, nil, nil, nil, nil},
|
|
}
|
|
for _, r := range rows {
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO special_types VALUES ($1,$2,$3,$4,$5)",
|
|
r.id, r.b, r.j, r.e, r.s)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'special types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM special_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count)
|
|
|
|
// Verify the large JSON value is accessible as a non-empty object.
|
|
// Doltgres does not provide json_object_keys and panics on JSON
|
|
// operators over large out-of-band JSON, so the document is retrieved
|
|
// and parsed client-side and asserted to be a non-empty object.
|
|
var jsonDoc string
|
|
err = conn.QueryRowContext(ctx, "SELECT col_json FROM special_types WHERE id = 2").Scan(&jsonDoc)
|
|
require.NoError(t, err)
|
|
var jsonObj map[string]interface{}
|
|
require.NoError(t, json.Unmarshal([]byte(jsonDoc), &jsonObj))
|
|
require.Greater(t, len(jsonObj), 0, "large JSON value must be a non-empty object")
|
|
|
|
// Verify ENUM and SET values. Use a comma-padded LIKE pattern to test
|
|
// exact element membership in the comma-separated SET string without
|
|
// relying on string_to_array, which is not yet implemented in Doltgres.
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM special_types WHERE col_enum = 'gamma' AND (',' || col_set || ',') LIKE '%,blue,%'").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "ENUM and SET values must round-trip correctly")
|
|
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM special_types WHERE col_enum IS NULL AND col_set IS NULL").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count, "NULL ENUM and SET values must be stored as NULL")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM special_types").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(rows), count, "special-type rows must survive GC")
|
|
})
|
|
|
|
t.Run("NullValuesAcrossAllTypes", func(t *testing.T) {
|
|
t.Parallel()
|
|
// A single table with one nullable column of each major type.
|
|
// Row 0 has all NULLs, row 1 has all non-NULLs, to verify the
|
|
// storage engine handles both extremes in the same chunk.
|
|
server := setupTestServer(t, "null_types_test")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx, `CREATE TABLE nullable_types (
|
|
id BIGINT PRIMARY KEY,
|
|
n_int INTEGER,
|
|
n_dbl DOUBLE PRECISION,
|
|
n_dec DECIMAL(10,4),
|
|
n_str VARCHAR(255),
|
|
n_txt TEXT,
|
|
n_blob BYTEA,
|
|
n_date DATE,
|
|
n_ts TIMESTAMP(3),
|
|
n_json JSON
|
|
)`)
|
|
require.NoError(t, err)
|
|
|
|
// All NULLs.
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO nullable_types VALUES (0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)")
|
|
require.NoError(t, err)
|
|
|
|
// All non-NULLs.
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO nullable_types VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)",
|
|
1, 42, 3.14, "2.7183", "hello", makeTestText(99, 5000),
|
|
makeTestBlobData(99, 5000), "2024-03-15", "2024-03-15 10:00:00.000",
|
|
`{"x": 1}`)
|
|
require.NoError(t, err)
|
|
|
|
// Mix: alternate NULL and non-NULL.
|
|
for i := 2; i < 20; i++ {
|
|
var nInt, nStr, nTxt, nDate interface{}
|
|
if i%2 == 0 {
|
|
nInt = i * 100
|
|
nStr = fmt.Sprintf("value-%d", i)
|
|
nTxt = makeTestText(i, 2000)
|
|
nDate = "2024-01-01"
|
|
}
|
|
_, err = conn.ExecContext(ctx,
|
|
"INSERT INTO nullable_types (id, n_int, n_str, n_txt, n_date) VALUES ($1,$2,$3,$4,$5)",
|
|
i, nInt, nStr, nTxt, nDate)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'nullable types')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx,
|
|
"SELECT COUNT(*) FROM nullable_types WHERE n_int IS NULL AND n_str IS NULL AND n_blob IS NULL").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Greater(t, count, 0, "rows with all-NULL values must be stored and counted correctly")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Count should be stable after GC.
|
|
var total int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM nullable_types").Scan(&total)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 20, total, "all nullable rows must survive GC")
|
|
})
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// TestWideTable verifies that Doltgres handles tables with a very large number
|
|
// of columns, including cases where the per-row data volume greatly exceeds
|
|
// what would fit in an inline (64 KB) row representation. Subtests cover
|
|
// different shapes: many small integer columns, fewer but wider string columns,
|
|
// and many TEXT columns carrying large payloads.
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func TestWideTable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ManyIntColumns", func(t *testing.T) {
|
|
t.Parallel()
|
|
// 500 BIGINT columns: stresses column-count parsing and prolly-tree
|
|
// row serialisation without producing a large per-row payload.
|
|
const numCols = 500
|
|
const numRows = 30
|
|
|
|
server := setupTestServer(t, "wide_int_table")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx, buildWideCreateTable("wide_int", numCols, "BIGINT"))
|
|
require.NoError(t, err)
|
|
|
|
for row := range numRows {
|
|
stmt := buildWideInsert("wide_int", row, numCols, func(col int) string {
|
|
return fmt.Sprintf("%d", int64(row)*int64(col+1))
|
|
})
|
|
_, err = conn.ExecContext(ctx, stmt)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'wide int table')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM wide_int").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Verify a specific cell: row 5, column c250 should be 5*(250+1)=1255.
|
|
var val int64
|
|
err = conn.QueryRowContext(ctx, "SELECT c250 FROM wide_int WHERE id = 5").Scan(&val)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(5*251), val, "cell value at (row=5, col=c250) must be exact")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM wide_int").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count, "wide int table must survive GC intact")
|
|
|
|
// Re-verify the cell value after GC.
|
|
err = conn.QueryRowContext(ctx, "SELECT c250 FROM wide_int WHERE id = 5").Scan(&val)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(5*251), val, "cell value must be preserved after GC")
|
|
})
|
|
|
|
t.Run("ManyVarcharColumns", func(t *testing.T) {
|
|
t.Parallel()
|
|
// 8 VARCHAR(300) columns, stressing wide-row serialisation.
|
|
const numCols = 8
|
|
const colWidth = 300
|
|
const numRows = 20
|
|
|
|
server := setupTestServer(t, "wide_varchar_table")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx,
|
|
buildWideCreateTable("wide_varchar", numCols, fmt.Sprintf("VARCHAR(%d)", colWidth)))
|
|
require.NoError(t, err)
|
|
|
|
for row := range numRows {
|
|
stmt := buildWideInsert("wide_varchar", row, numCols, func(col int) string {
|
|
// Each cell is colWidth chars, content encodes (row, col) for verifiability.
|
|
prefix := fmt.Sprintf("r%03dc%03d-", row, col)
|
|
padded := strings.Repeat(prefix, (colWidth+len(prefix)-1)/len(prefix))[:colWidth]
|
|
return fmt.Sprintf("'%s'", padded)
|
|
})
|
|
_, err = conn.ExecContext(ctx, stmt)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'wide varchar table')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM wide_varchar").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Every cell should be exactly colWidth bytes.
|
|
err = conn.QueryRowContext(ctx,
|
|
fmt.Sprintf("SELECT COUNT(*) FROM wide_varchar WHERE octet_length(c0) = %d AND octet_length(c%d) = %d",
|
|
colWidth, numCols-1, colWidth)).Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count, "all VARCHAR cells must retain their full width")
|
|
|
|
// Verify that a specific cell starts with the right prefix.
|
|
var cell string
|
|
err = conn.QueryRowContext(ctx, "SELECT c4 FROM wide_varchar WHERE id = 10").Scan(&cell)
|
|
require.NoError(t, err)
|
|
require.True(t, strings.HasPrefix(cell, "r010c004-"),
|
|
"VARCHAR cell content must encode the correct (row, col) position")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM wide_varchar").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count, "wide varchar table must survive GC intact")
|
|
})
|
|
|
|
t.Run("ManyTextColumns", func(t *testing.T) {
|
|
t.Parallel()
|
|
// 100 TEXT columns each holding ~2 KB of data, giving roughly 200 KB
|
|
// of out-of-band content per logical row. This tests that Doltgres
|
|
// handles rows whose external chunk references far outnumber what would
|
|
// fit in any single inline storage format.
|
|
const numCols = 100
|
|
const colDataSize = 2_000
|
|
const numRows = 10
|
|
|
|
server := setupTestServer(t, "wide_text_table")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx, buildWideCreateTable("wide_text", numCols, "TEXT"))
|
|
require.NoError(t, err)
|
|
|
|
for row := range numRows {
|
|
// Build the VALUES tuple manually because each TEXT value contains
|
|
// content that must be passed as a query parameter to avoid quoting issues.
|
|
args := make([]interface{}, 0, numCols+1)
|
|
placeholders := make([]string, 0, numCols+1)
|
|
args = append(args, row)
|
|
placeholders = append(placeholders, "$1")
|
|
for col := range numCols {
|
|
// Unique seed per (row, col) so content is verifiable.
|
|
args = append(args, makeTestText(row*numCols+col, colDataSize))
|
|
placeholders = append(placeholders, fmt.Sprintf("$%d", col+2))
|
|
}
|
|
stmt := fmt.Sprintf("INSERT INTO wide_text VALUES (%s)", strings.Join(placeholders, ","))
|
|
_, err = conn.ExecContext(ctx, stmt, args...)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'wide text table')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM wide_text").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Verify that the first and last text columns have the expected length.
|
|
// Doltgres panics on octet_length() over large out-of-band text, so the
|
|
// lengths are checked client-side.
|
|
lenRows, err := conn.QueryContext(ctx, "SELECT c0, c99 FROM wide_text")
|
|
require.NoError(t, err)
|
|
count = 0
|
|
for lenRows.Next() {
|
|
var c0v, c99v string
|
|
require.NoError(t, lenRows.Scan(&c0v, &c99v))
|
|
require.Equal(t, colDataSize, len(c0v))
|
|
require.Equal(t, colDataSize, len(c99v))
|
|
count++
|
|
}
|
|
require.NoError(t, lenRows.Err())
|
|
lenRows.Close()
|
|
require.Equal(t, numRows, count, "all TEXT cells must retain their content length")
|
|
|
|
// Spot-check exact content of a cell.
|
|
var cell string
|
|
const checkRow = 3
|
|
const checkCol = 47
|
|
err = conn.QueryRowContext(ctx,
|
|
fmt.Sprintf("SELECT c%d FROM wide_text WHERE id = %d", checkCol, checkRow)).Scan(&cell)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestText(checkRow*numCols+checkCol, colDataSize), cell,
|
|
"TEXT cell content must be byte-perfect after storage and retrieval")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM wide_text").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count, "wide text table must survive GC intact")
|
|
|
|
// Re-check cell content after GC.
|
|
err = conn.QueryRowContext(ctx,
|
|
fmt.Sprintf("SELECT c%d FROM wide_text WHERE id = %d", checkCol, checkRow)).Scan(&cell)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestText(checkRow*numCols+checkCol, colDataSize), cell,
|
|
"TEXT cell content must be preserved through GC")
|
|
})
|
|
|
|
t.Run("ExtremelyWideRow", func(t *testing.T) {
|
|
t.Parallel()
|
|
// A single row with 200 TEXT columns each carrying 5 KB: 1 MB of
|
|
// out-of-band data in one logical row. Exercises the edge of what
|
|
// Doltgres's chunk graph must track for a single row pointer.
|
|
const numCols = 200
|
|
const colDataSize = 5_000
|
|
const numRows = 3
|
|
|
|
server := setupTestServer(t, "extreme_wide_row")
|
|
db, err := server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
defer db.Close()
|
|
ctx := t.Context()
|
|
|
|
conn, err := db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
_, err = conn.ExecContext(ctx, buildWideCreateTable("extreme_wide", numCols, "TEXT"))
|
|
require.NoError(t, err)
|
|
|
|
for row := range numRows {
|
|
args := make([]interface{}, 0, numCols+1)
|
|
placeholders := make([]string, 0, numCols+1)
|
|
args = append(args, row)
|
|
placeholders = append(placeholders, "$1")
|
|
for col := range numCols {
|
|
args = append(args, makeTestText(row*numCols+col, colDataSize))
|
|
placeholders = append(placeholders, fmt.Sprintf("$%d", col+2))
|
|
}
|
|
stmt := fmt.Sprintf("INSERT INTO extreme_wide VALUES (%s)", strings.Join(placeholders, ","))
|
|
_, err = conn.ExecContext(ctx, stmt, args...)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err = conn.ExecContext(ctx, "SELECT dolt_commit('-Am', 'extreme wide rows')")
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM extreme_wide").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count)
|
|
|
|
// Verify total data volume via the byte length of two boundary columns.
|
|
// Doltgres panics on octet_length() over large out-of-band text, so the
|
|
// lengths are checked client-side.
|
|
lenRows, err := conn.QueryContext(ctx, "SELECT c0, c199 FROM extreme_wide")
|
|
require.NoError(t, err)
|
|
count = 0
|
|
for lenRows.Next() {
|
|
var c0v, c199v string
|
|
require.NoError(t, lenRows.Scan(&c0v, &c199v))
|
|
require.Equal(t, colDataSize, len(c0v))
|
|
require.Equal(t, colDataSize, len(c199v))
|
|
count++
|
|
}
|
|
require.NoError(t, lenRows.Err())
|
|
lenRows.Close()
|
|
require.Equal(t, numRows, count,
|
|
"extremely wide rows must fully preserve all column data")
|
|
|
|
_, err = db.ExecContext(ctx, "SELECT dolt_gc()")
|
|
require.NoError(t, err)
|
|
|
|
db.Close()
|
|
db, err = server.DB(driver.Connection{})
|
|
require.NoError(t, err)
|
|
conn, err = db.Conn(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM extreme_wide").Scan(&count)
|
|
require.NoError(t, err)
|
|
require.Equal(t, numRows, count, "extremely wide rows must survive GC")
|
|
|
|
// Spot-check an interior column post-GC.
|
|
const checkRow = 1
|
|
const checkCol = 100
|
|
var cell string
|
|
err = conn.QueryRowContext(ctx,
|
|
fmt.Sprintf("SELECT c%d FROM extreme_wide WHERE id = %d", checkCol, checkRow)).Scan(&cell)
|
|
require.NoError(t, err)
|
|
require.Equal(t, makeTestText(checkRow*numCols+checkCol, colDataSize), cell,
|
|
"extreme wide row TEXT cell must be byte-perfect after GC")
|
|
})
|
|
}
|
|
|
|
// countTextRowsOfLen runs a single-column text query and returns the number of
|
|
// rows whose value has exactly want bytes, asserting each row matches. Doltgres
|
|
// currently panics on octet_length()/length() over large out-of-band text
|
|
// values, so callers verify text byte-lengths client-side instead of in SQL.
|
|
func countTextRowsOfLen(t *testing.T, conn *sql.Conn, ctx context.Context, query string, want int) int {
|
|
rows, err := conn.QueryContext(ctx, query)
|
|
require.NoError(t, err)
|
|
defer rows.Close()
|
|
n := 0
|
|
for rows.Next() {
|
|
var s string
|
|
require.NoError(t, rows.Scan(&s))
|
|
require.Equal(t, want, len(s))
|
|
n++
|
|
}
|
|
require.NoError(t, rows.Err())
|
|
return n
|
|
}
|