diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..2fccf82f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,36 @@ +ARG GO_VERSION=1.24 +ARG DEBIAN_CODENAME=trixie + +FROM mcr.microsoft.com/devcontainers/go:${GO_VERSION}-${DEBIAN_CODENAME} + +ARG PG_MAJOR=16 +ARG MARIADB_MAJOR=12.0.2 + +USER root + +# Add PostgreSQL and MariaDB official repositories (detect codename from /etc/os-release) +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + wget ca-certificates gnupg curl \ + && wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg \ + && echo "deb http://apt.postgresql.org/pub/repos/apt $(. /etc/os-release && echo $VERSION_CODENAME)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ + && curl -LsSO https://r.mariadb.com/downloads/mariadb_repo_setup \ + && chmod +x mariadb_repo_setup \ + && ./mariadb_repo_setup --mariadb-server-version="mariadb-${MARIADB_MAJOR}" --skip-maxscale --skip-tools \ + && rm -f mariadb_repo_setup \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + postgresql-${PG_MAJOR} postgresql-client-${PG_MAJOR} \ + mariadb-server mariadb-client \ + && rm -rf /var/lib/apt/lists/* + +# Prepare user-owned data dirs for rootless startup +RUN mkdir -p /home/vscode/.local/share/pg/pgdata \ + && mkdir -p /home/vscode/.local/share/mysql \ + && chown -R vscode:vscode /home/vscode/.local/share + +# Set environment variables based on build args +ENV PG_BIN_DIR=/usr/lib/postgresql/${PG_MAJOR}/bin + +USER vscode + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..f2c092ac --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +{ + "name": "singularity", + "build": { + "dockerfile": "Dockerfile", + "args": { + "GO_VERSION": "1.24", + "DEBIAN_CODENAME": "trixie", + "PG_MAJOR": "16", + "MARIADB_MAJOR": "12.0.2" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "golang.go", + "eamodio.gitlens" + ], + "settings": { + "go.useLanguageServer": true, + "go.toolsEnvVars": { + "GOPATH": "/home/vscode/go" + } + } + } + }, + "forwardPorts": [], + "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/singularity,type=bind,consistency=cached,relabel=private", + "workspaceFolder": "/workspaces/singularity", + "postCreateCommand": "/bin/bash -lc '${containerWorkspaceFolder}/.devcontainer/post-create.sh'", + "postStartCommand": "/bin/bash -lc '${containerWorkspaceFolder}/.devcontainer/start-postgres.sh && ${containerWorkspaceFolder}/.devcontainer/start-mysql.sh'", + "remoteUser": "vscode", + "containerEnv": { + "GOPATH": "/home/vscode/go", + "MYSQL_DATABASE": "singularity", + "MYSQL_USER": "singularity", + "MYSQL_PASSWORD": "singularity", + "MYSQL_SOCKET": "/home/vscode/.local/share/mysql/mysql.sock", + "PGDATA": "/home/vscode/.local/share/pg/pgdata", + "PGPORT": "55432", + "PGSOCK_DIR": "/home/vscode/.local/share/pg" + }, + "runArgs": ["--userns=keep-id"], + "containerUser": "vscode" +} + diff --git a/.devcontainer/init-mysql.sh b/.devcontainer/init-mysql.sh new file mode 100755 index 00000000..b9e3f1af --- /dev/null +++ b/.devcontainer/init-mysql.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +# MariaDB client is required for init + +# Resolve socket path; default to user-owned socket +SOCKET="${MYSQL_SOCKET:-${HOME}/.local/share/mysql/mysql.sock}" + +## Removed one-time init guard; operations below are idempotent + +# Determine root auth flags +MYSQL_ROOT_FLAGS=("-uroot") +if [ -n "${MYSQL_ROOT_PASSWORD:-}" ]; then + MYSQL_ROOT_FLAGS+=("-p${MYSQL_ROOT_PASSWORD}") +fi + +# Wait for server readiness (best effort) +echo "Waiting for MySQL server at socket: $SOCKET" +for i in {1..60}; do + if mariadb-admin --socket="$SOCKET" ping "${MYSQL_ROOT_FLAGS[@]}" >/dev/null 2>&1; then + echo "MySQL server is ready for init" + break + fi + sleep 1 +done + +# Bail if still unreachable +if ! mariadb-admin --socket="$SOCKET" ping "${MYSQL_ROOT_FLAGS[@]}" >/dev/null 2>&1; then + echo "MySQL server not reachable, init failed" + exit 1 +fi + +# Create database and user idempotently (MySQL 8+ supports IF NOT EXISTS for users) +DB=${MYSQL_DATABASE:-singularity} +USER=${MYSQL_USER:-singularity} +PASS=${MYSQL_PASSWORD:-singularity} + +echo "Creating database and user: ${USER}@localhost and ${USER}@%" +mariadb --socket="$SOCKET" "${MYSQL_ROOT_FLAGS[@]}" <> /home/vscode/.local/share/pg/pgdata/postgresql.conf + { + echo 'host all all 127.0.0.1/32 trust' + echo 'host all all ::1/128 trust' + echo 'local all all trust' + } >> /home/vscode/.local/share/pg/pgdata/pg_hba.conf +fi + +# Initialize MariaDB +if [ ! -d "/home/vscode/.local/share/mysql/data/mysql" ]; then + echo "Initializing MariaDB..." + mariadb-install-db --datadir=/home/vscode/.local/share/mysql/data --auth-root-authentication-method=normal --skip-test-db >/dev/null +fi + +# Start both servers +echo "Starting database servers..." +.devcontainer/start-postgres.sh +.devcontainer/start-mysql.sh + +# Create users (databases will be created during testing as needed) +echo "Creating database users..." + +# Postgres setup +psql -h localhost -p 55432 -d postgres -c "CREATE USER singularity WITH SUPERUSER CREATEDB CREATEROLE LOGIN;" + +# MySQL setup +mariadb --socket=/home/vscode/.local/share/mysql/mysql.sock -uroot </dev/null 2>&1; then + echo "MySQL already running" + exit 0 +fi + +# Start MariaDB server +echo "Starting MySQL server" +touch "${LOG_FILE}" +nohup mariadbd \ + --datadir="${DATA_DIR}" \ + --socket="${SOCKET}" \ + --pid-file="${PID_FILE}" \ + --bind-address=127.0.0.1 \ + --port="${PORT}" \ + --skip-name-resolve \ + --log-error="${LOG_FILE}" \ + >/dev/null 2>&1 & + +# Wait for MySQL to be ready +for i in {1..60}; do + if [ -S "${SOCKET}" ] && grep -q "ready for connections" "${LOG_FILE}" >/dev/null 2>&1; then + echo "MySQL server is ready" + exit 0 + fi + sleep 1 +done + +echo "MySQL server failed to start" +if [ -f "${LOG_FILE}" ]; then + echo "--- Begin MariaDB error log ---" + tail -n 200 "${LOG_FILE}" || true + echo "--- End MariaDB error log ---" +fi +exit 1 \ No newline at end of file diff --git a/.devcontainer/start-postgres.sh b/.devcontainer/start-postgres.sh new file mode 100755 index 00000000..b90cb1a8 --- /dev/null +++ b/.devcontainer/start-postgres.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Postgres server configuration +PGDATA_DIR="${PGDATA:-/home/vscode/.local/share/pg/pgdata}" +LOG_FILE="${PGDATA_DIR}/postgres.log" +PGSOCK_DIR="${PGSOCK_DIR:-/home/vscode/.local/share/pg}" +PGPORT="${PGPORT:-55432}" +PG_BIN_DIR="${PG_BIN_DIR:-/usr/lib/postgresql/16/bin}" + +# Check if already running +if "${PG_BIN_DIR}/pg_ctl" -D "$PGDATA_DIR" status >/dev/null 2>&1; then + echo "Postgres already running" + exit 0 +fi + +# Start Postgres server +echo "Starting Postgres server" +"${PG_BIN_DIR}/pg_ctl" -D "$PGDATA_DIR" -l "$LOG_FILE" -w -o "-p ${PGPORT} -k ${PGSOCK_DIR}" start + + diff --git a/.github/workflows/container-publish.yml b/.github/workflows/container-publish.yml index 8a35a737..e49bde56 100644 --- a/.github/workflows/container-publish.yml +++ b/.github/workflows/container-publish.yml @@ -4,6 +4,7 @@ on: push: branches: - 'main' + - 'develop' tags: - 'v*' workflow_run: diff --git a/.github/workflows/devcontainer-podman.yml b/.github/workflows/devcontainer-podman.yml new file mode 100644 index 00000000..84934b1e --- /dev/null +++ b/.github/workflows/devcontainer-podman.yml @@ -0,0 +1,94 @@ +name: CI (Podman Devcontainer) + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} + cancel-in-progress: true + +jobs: + devcontainer-checks: + name: Devcontainer CI + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Podman as Docker + uses: parkan/github-actions/setup-podman-docker@v2 + with: + disable-docker: true + cache-storage: false # Disabled until cache issues resolved + + - name: Build devcontainer + id: build + uses: parkan/github-actions/devcontainer-build@v2 + with: + workspace-folder: . + container-runtime: podman + container-id-label: ci=podman + + # Fast surface checks and codegen first + - name: Generate swagger code + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && mkdir -p client/swagger/client && go install github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5 && go generate ./client/swagger/...' + container-runtime: podman + + - name: Check formatting + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && gofmt -l .' + container-runtime: podman + + - name: Run go vet + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && go vet ./...' + container-runtime: podman + + - name: Run staticcheck + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && staticcheck ./...' + container-runtime: podman + + - name: Build binary + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && go build -o singularity .' + container-runtime: podman + + - name: Run tests + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && go test -v ./...' + container-runtime: podman + + - name: Run integration tests + uses: parkan/github-actions/devcontainer-exec@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + command: 'cd /workspaces/singularity && SINGULARITY_TEST_INTEGRATION=true go test -v -timeout 20m -run "Integration" ./cmd/...' + container-runtime: podman + + - name: Cleanup + if: always() + uses: parkan/github-actions/devcontainer-cleanup@v2 + with: + container-id: ${{ steps.build.outputs.container-id }} + container-runtime: podman diff --git a/.github/workflows/go-check-config.json b/.github/workflows/go-check-config.json deleted file mode 100644 index 4b37308d..00000000 --- a/.github/workflows/go-check-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "gogenerate": true -} diff --git a/.github/workflows/go-check.yml b/.github/workflows/go-check.yml deleted file mode 100644 index 6972415d..00000000 --- a/.github/workflows/go-check.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Go Checks - -on: - pull_request: - push: - branches: ["main"] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} - cancel-in-progress: true - -jobs: - go-check: - uses: ipdxco/unified-github-workflows/.github/workflows/go-check.yml@v1.0.22 diff --git a/.github/workflows/go-test-config.json b/.github/workflows/go-test-config.json deleted file mode 100644 index 209dca21..00000000 --- a/.github/workflows/go-test-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "skip32bit": true -} diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml deleted file mode 100644 index cb43a3ae..00000000 --- a/.github/workflows/go-test.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Go Test - -on: - pull_request: - push: - branches: ["main"] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }} - cancel-in-progress: true - -jobs: - go-test: - uses: ipdxco/unified-github-workflows/.github/workflows/go-test.yml@v1.0 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a67c1e00..74382123 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -11,7 +11,6 @@ builds: goarch: - amd64 - arm64 - - 386 mod_timestamp: '{{.CommitTimestamp}}' archives: diff --git a/Dockerfile b/Dockerfile index 1fbcc621..74456363 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.6-bullseye AS builder +FROM golang:1.24.0-bullseye AS builder WORKDIR /app COPY go.* ./ RUN go mod download diff --git a/cmd/functional_test.go b/cmd/functional_test.go index 14009ea6..d663950e 100644 --- a/cmd/functional_test.go +++ b/cmd/functional_test.go @@ -513,7 +513,7 @@ func TestNoDuplicatedOutput(t *testing.T) { // run the dataset worker. If multiple workers try to work on the same // job, then this will return fail because a previous worker will have // removed files. - _, _, err = runner.Run(ctx, "singularity run dataset-worker --exit-on-complete=true --exit-on-error=true --concurrency=8") + _, _, err = runner.Run(ctx, "singularity run dataset-worker --exit-on-complete=true --exit-on-error=true --concurrency=8 --enable-dag=false") require.NoError(t, err) // Check output to make sure is has some CAR files diff --git a/database/util.go b/database/util.go index 1df11f31..062b23b6 100644 --- a/database/util.go +++ b/database/util.go @@ -24,11 +24,6 @@ var ( ErrDatabaseNotSupported = errors.New("database not supported") ) -func retryOn(err error) bool { - emsg := err.Error() - return strings.Contains(emsg, sqlSerializationFailure) || strings.Contains(emsg, "database is locked") || strings.Contains(emsg, "database table is locked") -} - func DoRetry(ctx context.Context, f func() error) error { return retry.Do(f, retry.RetryIf(retryOn), retry.LastErrorOnly(true), retry.Context(ctx)) } @@ -66,7 +61,13 @@ func (d *databaseLogger) Trace(ctx context.Context, begin time.Time, fc func() ( sql = "[SLOW!] " + sql } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && !strings.Contains(err.Error(), sqlSerializationFailure) { - lvl = logging.LevelError + // Demote noisy missing-table errors during test setup/teardown + emsg := err.Error() + if strings.Contains(emsg, "no such table") || strings.Contains(emsg, "does not exist") || strings.Contains(emsg, "doesn't exist") { + lvl = logging.LevelDebug + } else { + lvl = logging.LevelError + } } // Uncomment for logging everything in testing @@ -95,3 +96,13 @@ func OpenFromCLI(c *cli.Context) (*gorm.DB, io.Closer, error) { connString := c.String("database-connection-string") return OpenWithLogger(connString) } + +func retryOn(err error) bool { + emsg := err.Error() + return strings.Contains(emsg, sqlSerializationFailure) || + strings.Contains(emsg, "database is locked") || + strings.Contains(emsg, "database table is locked") || + // MySQL/InnoDB serialization conflict + strings.Contains(emsg, "Record has changed since last read") || + strings.Contains(emsg, "Error 1020 (HY000)") +} diff --git a/service/datasetworker/find.go b/service/datasetworker/find.go index b8077a89..367c6915 100644 --- a/service/datasetworker/find.go +++ b/service/datasetworker/find.go @@ -30,11 +30,11 @@ func (w *Thread) findJob(ctx context.Context, typesOrdered []model.JobType) (*mo } var job model.Job for _, jobType := range typesOrdered { - err := database.DoRetry(ctx, func() error { - return db.Transaction(func(db *gorm.DB) error { - err := db.Preload("Attachment.Preparation.OutputStorages").Preload("Attachment.Storage"). - Where("type = ? AND state = ? OR (state = ? AND worker_id is null)", jobType, model.Ready, model.Processing). - First(&job).Error + err := database.DoRetry(ctx, func() error { + return db.Transaction(func(db *gorm.DB) error { + err := db.Preload("Attachment.Preparation.OutputStorages").Preload("Attachment.Storage"). + Where("type = ? AND (state = ? OR (state = ? AND worker_id IS NULL))", jobType, model.Ready, model.Processing). + First(&job).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { job.ID = 0 diff --git a/util/testutil/testutils.go b/util/testutil/testutils.go index 064fdbe5..7e544846 100644 --- a/util/testutil/testutils.go +++ b/util/testutil/testutils.go @@ -5,15 +5,15 @@ import ( "crypto/rand" "io" rand2 "math/rand" - "net" "os" + "os/exec" "strings" "testing" "time" - "github.com/cockroachdb/errors" "github.com/data-preservation-programs/singularity/database" "github.com/data-preservation-programs/singularity/model" + "github.com/google/uuid" "github.com/ipfs/boxo/util" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" @@ -49,6 +49,11 @@ func RandomLetterString(length int) string { return string(b) } +// GenerateUniqueName creates a unique name for testing by combining a prefix with a UUID suffix +func GenerateUniqueName(prefix string) string { + return prefix + "-" + strings.ReplaceAll(uuid.New().String(), "-", "") +} + func GetFileTimestamp(t *testing.T, path string) int64 { t.Helper() info, err := os.Stat(path) @@ -75,33 +80,82 @@ func getTestDB(t *testing.T, dialect string) (db *gorm.DB, closer io.Closer, con require.NoError(t, err) return } - dbName := RandomLetterString(6) - var opError *net.OpError + // Use UUID for database names to ensure uniqueness and avoid MySQL's 64-character limit + // Remove hyphens to make it a valid database identifier + dbName := "test_" + strings.ReplaceAll(uuid.New().String(), "-", "") switch dialect { case "mysql": - connStr = "mysql://singularity:singularity@tcp(localhost:3306)/singularity?parseTime=true" + socket := os.Getenv("MYSQL_SOCKET") + connStr = "mysql://singularity:singularity@unix(" + socket + ")/mysql?parseTime=true" case "postgres": - connStr = "postgres://singularity:singularity@localhost:5432/singularity?sslmode=disable" + pgPort := os.Getenv("PGPORT") + connStr = "postgres://singularity@localhost:" + pgPort + "/postgres?sslmode=disable" default: require.Fail(t, "Unsupported dialect: "+dialect) } - var db1 *gorm.DB - var closer1 io.Closer - db1, closer1, err = database.OpenWithLogger(connStr) - if errors.As(err, &opError) { - return + // Skip initial connection test - databases will be created during testing + // Create database using shell commands to avoid driver transaction issues + switch dialect { + case "postgres": + // Use createdb command for PostgreSQL + cmd := exec.Command("createdb", "-h", "localhost", "-p", os.Getenv("PGPORT"), "-U", "singularity", dbName) + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Failed to create PostgreSQL database %s: %v, output: %s", dbName, err, string(output)) + return nil, nil, "" + } + t.Logf("Created PostgreSQL database %s", dbName) + case "mysql": + // Use mysql command for MySQL + socket := os.Getenv("MYSQL_SOCKET") + cmd := exec.Command("mariadb", "--socket="+socket, "-usingularity", "-psingularity", "-e", "CREATE DATABASE "+dbName) + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Failed to create MySQL database %s: %v, output: %s", dbName, err, string(output)) + return nil, nil, "" + } + t.Logf("Created MySQL database %s", dbName) + default: + t.Logf("Unsupported dialect for shell database creation: %s", dialect) + return nil, nil, "" + } + // Replace database name in connection string + if strings.Contains(connStr, "postgres?") { + connStr = strings.ReplaceAll(connStr, "postgres?", dbName+"?") + } else if strings.Contains(connStr, "mysql?") { + connStr = strings.ReplaceAll(connStr, "mysql?", dbName+"?") } - require.NoError(t, err) - err = db1.Exec("CREATE DATABASE " + dbName + "").Error - require.NoError(t, err) - connStr = strings.ReplaceAll(connStr, "singularity?", dbName+"?") var closer2 io.Closer db, closer2, err = database.OpenWithLogger(connStr) - require.NoError(t, err) + if err != nil { + t.Logf("Failed to connect to test database %s: %v", dbName, err) + // Cleanup using shell commands + switch dialect { + case "postgres": + cmd := exec.Command("dropdb", "-h", "localhost", "-p", os.Getenv("PGPORT"), "-U", "singularity", dbName) + cmd.Run() // Ignore errors during cleanup + case "mysql": + socket := os.Getenv("MYSQL_SOCKET") + cmd := exec.Command("mariadb", "--socket="+socket, "-usingularity", "-psingularity", "-e", "DROP DATABASE "+dbName) + cmd.Run() // Ignore errors during cleanup + } + return nil, nil, "" + } closer = CloserFunc(func() error { - require.NoError(t, closer2.Close()) - require.NoError(t, db1.Exec("DROP DATABASE "+dbName+"").Error) - return closer1.Close() + if closer2 != nil { + _ = closer2.Close() + } + // Cleanup using shell commands + switch dialect { + case "postgres": + cmd := exec.Command("dropdb", "-h", "localhost", "-p", os.Getenv("PGPORT"), "-U", "singularity", dbName) + cmd.Run() // Ignore errors during cleanup + case "mysql": + socket := os.Getenv("MYSQL_SOCKET") + cmd := exec.Command("mariadb", "--socket="+socket, "-usingularity", "-psingularity", "-e", "DROP DATABASE "+dbName) + cmd.Run() // Ignore errors during cleanup + } + return nil }) return } @@ -147,6 +201,36 @@ func doOne(t *testing.T, backend string, testFunc func(ctx context.Context, t *t err := model.AutoMigrate(db) require.NoError(t, err) + // Clear any existing data from tables with unique constraints + tables := []string{ + "output_attachments", + "source_attachments", + "storages", + "wallets", + "deal_schedules", + "preparations", + } + + // Get DB type from connection string + isPostgres := strings.HasPrefix(connStr, "postgres:") + for _, table := range tables { + var err error + if isPostgres { + err = db.Exec("TRUNCATE TABLE " + table + " CASCADE").Error + } else { + err = db.Exec("DELETE FROM " + table).Error + } + if err != nil { + emsg := err.Error() + // Suppress noisy logs when tables don't exist yet across backends + if strings.Contains(emsg, "no such table") || strings.Contains(emsg, "does not exist") || strings.Contains(emsg, "doesn't exist") { + continue + } + t.Logf("Warning: Failed to clear table %s: %v", table, err) + // Don't fail the test for other errors either + } + } + t.Run(backend, func(t *testing.T) { testFunc(ctx, t, db) })