diff --git a/.github/workflows/ci-postgres-client-tests.yaml b/.github/workflows/ci-postgres-client-tests.yaml index 907852c50a..b1e043c0a0 100644 --- a/.github/workflows/ci-postgres-client-tests.yaml +++ b/.github/workflows/ci-postgres-client-tests.yaml @@ -25,6 +25,6 @@ jobs: - name: Set up Docker uses: docker/setup-docker-action@v4 - name: Build Docker image - run: docker build -t postgres-client-tests --file testing/PostgresDockerfile . + run: docker build -t postgres-client-tests --file testing/postgres-client-tests/Dockerfile . - name: Run tests run: docker run --detach=false postgres-client-tests diff --git a/testing/PostgresDockerfile b/testing/PostgresDockerfile deleted file mode 100644 index 400206bed8..0000000000 --- a/testing/PostgresDockerfile +++ /dev/null @@ -1,112 +0,0 @@ -FROM --platform=${BUILDPLATFORM} ubuntu:22.04 - -# install python, java, bats, git ruby, perl, cpan -ENV DEBIAN_FRONTEND=noninteractive -RUN apt update -y && \ - apt install -y \ - curl \ - gnupg \ - software-properties-common && \ - curl -sL https://deb.nodesource.com/setup_22.x | bash - && \ - add-apt-repository ppa:deadsnakes/ppa -y && \ - sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \ - curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg -RUN apt update -y && \ - apt install -y \ - python3 \ - python3-pip \ - python3-psycopg2 \ - curl \ - wget \ - pkg-config \ - openjdk-17-jdk \ - ca-certificates-java \ - bats \ - perl \ - php \ - php-pgsql \ - cpanminus \ - cmake \ - g++ \ - libmysqlcppconn-dev \ - git \ - ruby \ - ruby-dev \ - gem \ - libc6 \ - libgcc1 \ - r-base \ - libpq-dev \ - nodejs \ - lsof \ - postgresql-server-dev-15 && \ - update-ca-certificates -f - -# install rust (cargo) -ENV RUSTUP_HOME=/root/.rustup \ - CARGO_HOME=/root/.cargo -ENV PATH="${CARGO_HOME}/bin:${PATH}" -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal && \ - rustc --version && cargo --version - -# install go -WORKDIR /root -ENV GO_VERSION=1.26.2 -ENV GOPATH=/go -ENV PATH=$PATH:$GOPATH/bin -ENV PATH=$PATH:$GOPATH/bin:/usr/local/go/bin -RUN curl -O "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz" && \ - sha256sum "go${GO_VERSION}.linux-amd64.tar.gz" && \ - tar -xvf "go${GO_VERSION}.linux-amd64.tar.gz" -C /usr/local && \ - chown -R root:root /usr/local/go && \ - mkdir -p $HOME/go/{bin,src} && \ - go version - -# Setup JAVA_HOME -- useful for docker commandline -ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/ - -# install java postgres JDBC driver -RUN mkdir -p /postgres-client-tests/java -RUN curl -L -o /postgres-client-tests/java/postgresql-42.7.3.jar \ - https://jdbc.postgresql.org/download/postgresql-42.7.3.jar - -# install node deps -COPY ./testing/postgres-client-tests/node/package.json /postgres-client-tests/node/ -COPY ./testing/postgres-client-tests/node/package-lock.json /postgres-client-tests/node/ -WORKDIR /postgres-client-tests/node -RUN npm install - -# install cpan dependencies -RUN cpanm --force DBD::Pg - -# install ruby dependencies -COPY ./testing/postgres-client-tests/ruby/Gemfile /postgres-client-tests/ruby/ -COPY ./testing/postgres-client-tests/ruby/Gemfile.lock /postgres-client-tests/ruby/ -WORKDIR /postgres-client-tests/ruby -RUN gem install bundler -v 2.1.4 && bundle install - -# install sqlalchemy -RUN pip3 install --no-cache-dir sqlalchemy==2.0.46 - -# install doltgres from source -WORKDIR /root/building -COPY go.mod doltgresql/ -# download dependencies first to persist Go dependencies download cache for doltgres despite other edits -WORKDIR doltgresql -RUN go mod download -# exclude clients directory to persist build-cache for doltgres when only editing client code -COPY --exclude=testing/postgres-client-tests/ . . - -# Build the parser -WORKDIR /root/building/doltgresql/postgres/parser -RUN bash ./build.sh - -# Build the doltgres binary, which we will need for bats, and put it on PATH -WORKDIR /root/building/doltgresql/cmd/doltgres -RUN go build -o /usr/local/bin/doltgres . - -COPY ./testing/postgres-client-tests /postgres-client-tests -COPY ./testing/postgres-client-tests/postgres-client-tests-entrypoint.sh /postgres-client-tests/entrypoint.sh - -WORKDIR /postgres-client-tests -ENTRYPOINT ["/postgres-client-tests/entrypoint.sh"] diff --git a/testing/postgres-client-tests/.gitignore b/testing/postgres-client-tests/.gitignore index e325046420..51cff1ca01 100644 --- a/testing/postgres-client-tests/.gitignore +++ b/testing/postgres-client-tests/.gitignore @@ -1,3 +1,9 @@ java/PostgresTest.class java/postgresql-42.7.3.jar node/node_modules/ +elixir/postgrex/_build +elixir/postgrex/mix.lock +elixir/postgrex/dep +dotnet/bin +dotnet/obj +dotnet/out diff --git a/testing/postgres-client-tests/Dockerfile b/testing/postgres-client-tests/Dockerfile new file mode 100644 index 0000000000..ce6c1414c5 --- /dev/null +++ b/testing/postgres-client-tests/Dockerfile @@ -0,0 +1,157 @@ +# syntax=docker/dockerfile:1 +FROM golang:1.26.2-alpine AS golang_cgo +ENV CGO_ENABLED=1 +ENV GO_LDFLAGS="-linkmode external -extldflags '-static'" +RUN apk add --no-cache build-base + +# --- Build doltgres binary --- +FROM golang_cgo AS doltgres_build +RUN apk add --no-cache icu-dev icu-static +RUN apk update && apk add --no-cache bash +WORKDIR /root/building +COPY go.mod doltgresql/ +WORKDIR doltgresql +RUN go mod download +COPY --exclude=testing/postgres-client-tests/ . . +WORKDIR /root/building/doltgresql/postgres/parser +RUN sh ./build.sh +WORKDIR /root/building/doltgresql/cmd/doltgres +RUN go build -ldflags "-linkmode external -extldflags '-static'" -o /build/bin/doltgres . + +# --- Build Go postgres clients (pgx, lib/pq) --- +FROM golang:1.26.4 AS go_clients_build +COPY testing/postgres-client-tests/go/pgx/ /build/go/pgx/ +WORKDIR /build/go/pgx +RUN go build -o /build/bin/pgx-test . +COPY testing/postgres-client-tests/go/libpq/ /build/go/libpq/ +WORKDIR /build/go/libpq +RUN go build -o /build/bin/libpq-test . + +# --- Build Rust sqlx client --- +FROM rust:1.96-slim-bookworm AS rust_clients_build +RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/* +COPY testing/postgres-client-tests/rust/ /build/rust/ +WORKDIR /build/rust +RUN cargo build --release + +# --- Build C libpq client --- +FROM debian:bookworm-slim AS c_clients_build +RUN apt-get update && apt-get install -y gcc make libpq-dev pkg-config && rm -rf /var/lib/apt/lists/* +COPY testing/postgres-client-tests/c/ /build/c/ +WORKDIR /build/c +RUN make + +# --- Install Node postgres client deps --- +FROM node:22-bookworm-slim AS node_clients_build +COPY testing/postgres-client-tests/node/package.json /build/node/ +COPY testing/postgres-client-tests/node/package-lock.json /build/node/ +WORKDIR /build/node +RUN npm install +COPY testing/postgres-client-tests/node/ /build/node/ + +# --- Install Ruby pg gem --- +FROM ruby:3.4-bookworm AS ruby_clients_build +RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* +COPY testing/postgres-client-tests/ruby/Gemfile /build/ruby/ +COPY testing/postgres-client-tests/ruby/Gemfile.lock /build/ruby/ +WORKDIR /build/ruby +RUN bundle install +COPY testing/postgres-client-tests/ruby/ /build/ruby/ + +# --- Install Python deps --- +FROM python:3.14-slim-bookworm AS python_clients_build +RUN pip3 install --no-cache-dir --target /build/python-deps sqlalchemy==2.0.46 +COPY testing/postgres-client-tests/python/ /build/python/ +WORKDIR /build/python/ + +# --- Build .NET Npgsql client --- +FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS dotnet_clients_build +COPY testing/postgres-client-tests/dotnet/ /build/dotnet/ +WORKDIR /build/dotnet +RUN dotnet publish -c Release -o /build/output/ + +# --- Runtime --- +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + lsb-release && \ + sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && \ + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + +# java JDBC dependencies +COPY testing/postgres-client-tests/java/ /postgres-client-tests/java/ +RUN apt-get install -y \ + openjdk-17-jdk && \ + curl -L -o /postgres-client-tests/java/postgresql-42.7.3.jar https://jdbc.postgresql.org/download/postgresql-42.7.3.jar + +# perl dependencies +RUN apt-get install -y \ + python3 \ + python3-pip \ + python3-psycopg2 \ + perl \ + cpanminus \ + php \ + php-pgsql \ + r-base \ + libpq-dev \ + ca-certificates-java \ + bats \ + lsof \ + postgresql-client-15 \ + nodejs \ + elixir \ + libicu-dev \ + git && \ + update-ca-certificates -f && \ + rm -rf /var/lib/apt/lists/* + +# install .NET +RUN curl -sSL https://dot.net/v1/dotnet-install.sh -o dotnet-install.sh \ + && chmod +x dotnet-install.sh \ + && ./dotnet-install.sh --channel 9.0 --runtime aspnetcore --install-dir /usr/share/dotnet \ + && ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet \ + && rm dotnet-install.sh + +# cpan dependencies +RUN cpanm --force DBD::Pg + +# r dependencies +COPY testing/postgres-client-tests/r/ /postgres-client-tests/r/ +RUN Rscript -e 'install.packages("RPostgres", repos="https://cloud.r-project.org")' + +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/ +ENV GEM_HOME="/usr/local/bundle" +ENV PYTHONPATH=/usr/local/lib/python-deps + +COPY --from=ruby_clients_build /usr/local/bin/ruby /usr/local/bin/ +COPY --from=ruby_clients_build /usr/local/lib/ /usr/local/lib/ +COPY --from=ruby_clients_build /usr/local/bundle/ /usr/local/bundle/ +RUN ldconfig + +COPY --from=doltgres_build /build/bin/doltgres /usr/local/bin/doltgres +COPY --from=go_clients_build /build/bin/pgx-test /build/bin/go/pgx-test +COPY --from=go_clients_build /build/bin/libpq-test /build/bin/go/libpq-test +COPY --from=rust_clients_build /build/rust/target/release/sqlx_exists_demo /build/bin/rust/sqlx_exists_demo +COPY --from=c_clients_build /build/c/postgres-c-connector-test /build/bin/c/postgres-c-connector-test +COPY --from=dotnet_clients_build /build/output /build/bin/dotnet +COPY --from=node_clients_build /build/node/ /postgres-client-tests/node/ +COPY --from=python_clients_build /build/python/ /postgres-client-tests/python/ +COPY --from=python_clients_build /build/python-deps/ /usr/local/lib/python-deps/ +COPY --from=ruby_clients_build /build/ruby/ /postgres-client-tests/ruby/ + +COPY testing/postgres-client-tests/c/ /postgres-client-tests/c/ +COPY testing/postgres-client-tests/drizzle/ /postgres-client-tests/drizzle/ +COPY testing/postgres-client-tests/helpers.bash /postgres-client-tests/ +COPY testing/postgres-client-tests/php/ /postgres-client-tests/php/ +COPY testing/postgres-client-tests/perl/ /postgres-client-tests/perl/ +COPY testing/postgres-client-tests/postgres-client-tests.bats /postgres-client-tests/ +COPY testing/postgres-client-tests/postgres-client-tests-entrypoint.sh /postgres-client-tests/entrypoint.sh + +WORKDIR /postgres-client-tests +ENTRYPOINT ["/postgres-client-tests/entrypoint.sh"] diff --git a/testing/postgres-client-tests/README.md b/testing/postgres-client-tests/README.md index 9c657b0724..1c322f97bf 100644 --- a/testing/postgres-client-tests/README.md +++ b/testing/postgres-client-tests/README.md @@ -5,7 +5,7 @@ on pull requests. These tests can be run locally using Docker. From the doltgresql directory of the repo, run: ```bash -$ docker build -t postgres-client-tests -f testing/PostgresDockerfile . +$ docker build -t postgres-client-tests -f testing/postgres-client-tests/Dockerfile . $ docker run postgres-client-tests:latest ``` diff --git a/testing/postgres-client-tests/dotnet/Program.cs b/testing/postgres-client-tests/dotnet/Program.cs new file mode 100644 index 0000000000..6ea151121e --- /dev/null +++ b/testing/postgres-client-tests/dotnet/Program.cs @@ -0,0 +1,103 @@ +using Npgsql; + +var user = args[0]; +var port = args[1]; + +var connStr = $"Host=localhost;Port={port};Username={user};Password=password;Database=postgres;SSL Mode=Disable"; +await using var conn = new NpgsqlConnection(connStr); +await conn.OpenAsync(); + +// Basic SELECT +await using (var cmd = new NpgsqlCommand("SELECT pk FROM test_table LIMIT 1", conn)) +{ + var pk = (int)(await cmd.ExecuteScalarAsync())!; + if (pk != 1) + throw new Exception($"expected pk=1, got {pk}"); +} + +// INSERT +await using (var cmd = new NpgsqlCommand("INSERT INTO test_table VALUES (2)", conn)) + await cmd.ExecuteNonQueryAsync(); + +// COUNT +await using (var cmd = new NpgsqlCommand("SELECT COUNT(*) FROM test_table", conn)) +{ + var count = (long)(await cmd.ExecuteScalarAsync())!; + if (count != 2) + throw new Exception($"expected count=2, got {count}"); +} + +// Prepared SELECT +await using (var cmd = new NpgsqlCommand("SELECT pk FROM test_table WHERE pk = $1", conn)) +{ + cmd.Parameters.AddWithValue(1); + await cmd.PrepareAsync(); + var pk = (int)(await cmd.ExecuteScalarAsync())!; + if (pk != 1) + throw new Exception($"expected pk=1 from prepared stmt, got {pk}"); +} + +// Dolt workflow: create table, insert, commit, branch, insert, commit, merge +foreach (var q in new[] +{ + "DROP TABLE IF EXISTS test", + "CREATE TABLE test (pk int, value int, PRIMARY KEY(pk))", + "INSERT INTO test (pk, value) VALUES (0, 0)", + "SELECT dolt_add('-A')", + "SELECT dolt_commit('-m', 'added table test')", + "SELECT dolt_checkout('-b', 'mybranch')", + "INSERT INTO test VALUES (1, 1)", + "SELECT dolt_commit('-a', '-m', 'updated test')", + "SELECT dolt_checkout('main')", + "SELECT dolt_merge('mybranch')", +}) +{ + await using var cmd = new NpgsqlCommand(q, conn); + await cmd.ExecuteNonQueryAsync(); +} + +await RunPreparedQuery( + "SELECT pk, value FROM test WHERE pk = $1", + [0], + async r => + { + if (!await r.ReadAsync()) throw new Exception("no rows"); + var pk = r.GetInt32(0); + var value = r.GetInt32(1); + if (pk != 0 || value != 0) + throw new Exception($"expected pk=0 value=0, got pk={pk} value={value}"); + }); + +await RunPreparedQuery( + "SELECT COUNT(*) FROM dolt_log", + [], + async r => + { + if (!await r.ReadAsync()) throw new Exception("no rows"); + var size = r.GetInt64(0); + if (size != 4) + throw new Exception($"expected 4 dolt_log entries, got {size}"); + }); + +await RunPreparedQuery( + "SELECT COUNT(*) FROM test", + [], + async r => + { + if (!await r.ReadAsync()) throw new Exception("no rows"); + var size = r.GetInt64(0); + if (size != 2) + throw new Exception($"expected 2 rows in test, got {size}"); + }); + +Console.WriteLine("Npgsql test passed"); + +async Task RunPreparedQuery(string query, object[] queryArgs, Func check) +{ + await using var cmd = new NpgsqlCommand(query, conn); + foreach (var arg in queryArgs) + cmd.Parameters.AddWithValue(arg); + await cmd.PrepareAsync(); + await using var reader = await cmd.ExecuteReaderAsync(); + await check(reader); +} diff --git a/testing/postgres-client-tests/dotnet/npgsql-test.csproj b/testing/postgres-client-tests/dotnet/npgsql-test.csproj new file mode 100644 index 0000000000..bb64e245f8 --- /dev/null +++ b/testing/postgres-client-tests/dotnet/npgsql-test.csproj @@ -0,0 +1,13 @@ + + + Exe + net9.0 + enable + enable + npgsql-test + true + + + + + diff --git a/testing/postgres-client-tests/go/libpq/go.mod b/testing/postgres-client-tests/go/libpq/go.mod new file mode 100644 index 0000000000..cc50e17f78 --- /dev/null +++ b/testing/postgres-client-tests/go/libpq/go.mod @@ -0,0 +1,5 @@ +module libpq-test + +go 1.26.3 + +require github.com/lib/pq v1.12.3 // indirect diff --git a/testing/postgres-client-tests/go/libpq/go.sum b/testing/postgres-client-tests/go/libpq/go.sum new file mode 100644 index 0000000000..c7cd147812 --- /dev/null +++ b/testing/postgres-client-tests/go/libpq/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= diff --git a/testing/postgres-client-tests/go/libpq/main.go b/testing/postgres-client-tests/go/libpq/main.go new file mode 100644 index 0000000000..b4ca2c845e --- /dev/null +++ b/testing/postgres-client-tests/go/libpq/main.go @@ -0,0 +1,191 @@ +// 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 ( + "database/sql" + "fmt" + "os" + + _ "github.com/lib/pq" +) + +type ResFunc func(rows *sql.Rows) error + +type StmtTest struct { + Query string + Args []interface{} + Res []ResFunc +} + +func main() { + user := os.Args[1] + port := os.Args[2] + + connStr := fmt.Sprintf("host=localhost port=%s user=%s password=password dbname=postgres sslmode=disable", port, user) + db, err := sql.Open("postgres", connStr) + if err != nil { + panic(err) + } + defer db.Close() + + if err = db.Ping(); err != nil { + panic(err) + } + + var pk int + err = db.QueryRow("SELECT pk FROM test_table LIMIT 1").Scan(&pk) + if err != nil { + panic(err) + } + if pk != 1 { + panic(fmt.Sprintf("expected pk=1, got %d", pk)) + } + + _, err = db.Exec("INSERT INTO test_table VALUES (2)") + if err != nil { + panic(err) + } + + var count int + err = db.QueryRow("SELECT COUNT(*) FROM test_table").Scan(&count) + if err != nil { + panic(err) + } + if count != 2 { + panic(fmt.Sprintf("expected count=2, got %d", count)) + } + + stmt, err := db.Prepare("SELECT pk FROM test_table WHERE pk = $1") + if err != nil { + panic(err) + } + defer stmt.Close() + + err = stmt.QueryRow(1).Scan(&pk) + if err != nil { + panic(err) + } + if pk != 1 { + panic(fmt.Sprintf("expected pk=1 from prepared stmt, got %d", pk)) + } + + // dolt workflow: create table, insert, commit, branch, insert, commit, merge + for _, q := range []string{ + "DROP TABLE IF EXISTS test", + "CREATE TABLE test (pk int, value int, PRIMARY KEY(pk))", + "INSERT INTO test (pk, value) VALUES (0, 0)", + "SELECT dolt_add('-A')", + "SELECT dolt_commit('-m', 'added table test')", + "SELECT dolt_checkout('-b', 'mybranch')", + "INSERT INTO test VALUES (1, 1)", + "SELECT dolt_commit('-a', '-m', 'updated test')", + "SELECT dolt_checkout('main')", + "SELECT dolt_merge('mybranch')", + } { + if _, err = db.Exec(q); err != nil { + panic(fmt.Sprintf("failed to execute %q: %v", q, err)) + } + } + + stmtTests := []StmtTest{ + { + Query: "SELECT pk, value FROM test WHERE pk = $1", + Args: []interface{}{int64(0)}, + Res: []ResFunc{ + func(rows *sql.Rows) error { + var pk, value int64 + if err := rows.Scan(&pk, &value); err != nil { + return err + } + if pk != 0 || value != 0 { + return fmt.Errorf("expected pk=0 value=0, got pk=%d value=%d", pk, value) + } + return nil + }, + }, + }, + { + Query: "SELECT COUNT(*) FROM dolt_log", + Res: []ResFunc{ + func(rows *sql.Rows) error { + var size int64 + if err := rows.Scan(&size); err != nil { + return err + } + if size != 4 { + return fmt.Errorf("expected 4 dolt_log entries, got %d", size) + } + return nil + }, + }, + }, + { + Query: "SELECT COUNT(*) FROM test", + Res: []ResFunc{ + func(rows *sql.Rows) error { + var size int64 + if err := rows.Scan(&size); err != nil { + return err + } + if size != 2 { + return fmt.Errorf("expected 2 rows in test, got %d", size) + } + return nil + }, + }, + }, + } + + for _, test := range stmtTests { + func() { + stmt, err := db.Prepare(test.Query) + if err != nil { + panic(fmt.Sprintf("prepare %q: %v", test.Query, err)) + } + defer func() { + if err := stmt.Close(); err != nil { + panic(fmt.Sprintf("stmt.Close() for %q: %v", test.Query, err)) + } + }() + + rows, err := stmt.Query(test.Args...) + if err != nil { + panic(fmt.Sprintf("query %q: %v", test.Query, err)) + } + defer func() { + if err := rows.Close(); err != nil { + panic(fmt.Sprintf("rows.Close() for %q: %v", test.Query, err)) + } + }() + + i := 0 + for rows.Next() { + if i >= len(test.Res) { + panic(fmt.Sprintf("too many rows for %q", test.Query)) + } + if err := test.Res[i](rows); err != nil { + panic(fmt.Sprintf("result %d of %q: %v", i, test.Query, err)) + } + i++ + } + if err := rows.Err(); err != nil { + panic(fmt.Sprintf("rows.Err() for %q: %v", test.Query, err)) + } + }() + } + + fmt.Println("lib/pq test passed") +} diff --git a/testing/postgres-client-tests/go/pgx/go.mod b/testing/postgres-client-tests/go/pgx/go.mod new file mode 100644 index 0000000000..518065c1ae --- /dev/null +++ b/testing/postgres-client-tests/go/pgx/go.mod @@ -0,0 +1,10 @@ +module pgx-test + +go 1.26.3 + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect + golang.org/x/text v0.29.0 // indirect +) diff --git a/testing/postgres-client-tests/go/pgx/go.sum b/testing/postgres-client-tests/go/pgx/go.sum new file mode 100644 index 0000000000..f97efec8b1 --- /dev/null +++ b/testing/postgres-client-tests/go/pgx/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testing/postgres-client-tests/go/pgx/main.go b/testing/postgres-client-tests/go/pgx/main.go new file mode 100644 index 0000000000..5f5f1eaa5a --- /dev/null +++ b/testing/postgres-client-tests/go/pgx/main.go @@ -0,0 +1,179 @@ +// 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" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +type ResFunc func(rows pgx.Rows) error + +type StmtTest struct { + Name string + Query string + Args []any + Res []ResFunc +} + +func main() { + user := os.Args[1] + port := os.Args[2] + + ctx := context.Background() + connStr := fmt.Sprintf("postgres://%s:password@localhost:%s/postgres", user, port) + conn, err := pgx.Connect(ctx, connStr) + if err != nil { + panic(err) + } + defer conn.Close(ctx) + + var pk int + err = conn.QueryRow(ctx, "SELECT pk FROM test_table LIMIT 1").Scan(&pk) + if err != nil { + panic(err) + } + if pk != 1 { + panic(fmt.Sprintf("expected pk=1, got %d", pk)) + } + + _, err = conn.Exec(ctx, "INSERT INTO test_table VALUES (2)") + if err != nil { + panic(err) + } + + var count int + err = conn.QueryRow(ctx, "SELECT COUNT(*) FROM test_table").Scan(&count) + if err != nil { + panic(err) + } + if count != 2 { + panic(fmt.Sprintf("expected count=2, got %d", count)) + } + + _, err = conn.Prepare(ctx, "select_pk", "SELECT pk FROM test_table WHERE pk = $1") + if err != nil { + panic(err) + } + err = conn.QueryRow(ctx, "select_pk", 1).Scan(&pk) + if err != nil { + panic(err) + } + if pk != 1 { + panic(fmt.Sprintf("expected pk=1 from prepared stmt, got %d", pk)) + } + + // dolt workflow: create table, insert, commit, branch, insert, commit, merge + for _, q := range []string{ + "DROP TABLE IF EXISTS test", + "CREATE TABLE test (pk int, value int, PRIMARY KEY(pk))", + "INSERT INTO test (pk, value) VALUES (0, 0)", + "SELECT dolt_add('-A')", + "SELECT dolt_commit('-m', 'added table test')", + "SELECT dolt_checkout('-b', 'mybranch')", + "INSERT INTO test VALUES (1, 1)", + "SELECT dolt_commit('-a', '-m', 'updated test')", + "SELECT dolt_checkout('main')", + "SELECT dolt_merge('mybranch')", + } { + if _, err = conn.Exec(ctx, q); err != nil { + panic(fmt.Sprintf("failed to execute %q: %v", q, err)) + } + } + + stmtTests := []StmtTest{ + { + Name: "select_test_by_pk", + Query: "SELECT pk, value FROM test WHERE pk = $1", + Args: []any{int64(0)}, + Res: []ResFunc{ + func(rows pgx.Rows) error { + var pk, value int64 + if err := rows.Scan(&pk, &value); err != nil { + return err + } + if pk != 0 || value != 0 { + return fmt.Errorf("expected pk=0 value=0, got pk=%d value=%d", pk, value) + } + return nil + }, + }, + }, + { + Name: "count_dolt_log", + Query: "SELECT COUNT(*) FROM dolt_log", + Res: []ResFunc{ + func(rows pgx.Rows) error { + var size int64 + if err := rows.Scan(&size); err != nil { + return err + } + if size != 4 { + return fmt.Errorf("expected 4 dolt_log entries, got %d", size) + } + return nil + }, + }, + }, + { + Name: "count_test", + Query: "SELECT COUNT(*) FROM test", + Res: []ResFunc{ + func(rows pgx.Rows) error { + var size int64 + if err := rows.Scan(&size); err != nil { + return err + } + if size != 2 { + return fmt.Errorf("expected 2 rows in test, got %d", size) + } + return nil + }, + }, + }, + } + + for _, test := range stmtTests { + func() { + if _, err := conn.Prepare(ctx, test.Name, test.Query); err != nil { + panic(fmt.Sprintf("prepare %q: %v", test.Query, err)) + } + rows, err := conn.Query(ctx, test.Name, test.Args...) + if err != nil { + panic(fmt.Sprintf("query %q: %v", test.Query, err)) + } + defer rows.Close() + + i := 0 + for rows.Next() { + if i >= len(test.Res) { + panic(fmt.Sprintf("too many rows for %q", test.Query)) + } + if err := test.Res[i](rows); err != nil { + panic(fmt.Sprintf("result %d of %q: %v", i, test.Query, err)) + } + i++ + } + if err := rows.Err(); err != nil { + panic(fmt.Sprintf("rows.Err() for %q: %v", test.Query, err)) + } + }() + } + + fmt.Println("pgx test passed") +} diff --git a/testing/postgres-client-tests/postgres-client-tests.bats b/testing/postgres-client-tests/postgres-client-tests.bats index c68632c7fb..4e1c4b3dba 100755 --- a/testing/postgres-client-tests/postgres-client-tests.bats +++ b/testing/postgres-client-tests/postgres-client-tests.bats @@ -15,12 +15,6 @@ setup() { teardown() { cd .. teardown_doltgres_repo - - # Check if postgresql is still running. If so stop it - active=$(service postgresql status) - if echo "$active" | grep "online"; then - service postgresql stop - fi } @test "postgres-connector-java client" { @@ -101,7 +95,22 @@ teardown() { npx tsx src/index.ts } +@test "R RPostgres client" { + Rscript $BATS_TEST_DIRNAME/r/rpostgres-test.r $USER $PORT +} + @test "rust sqlx" { - cd $BATS_TEST_DIRNAME/rust - RUSTFLAGS=-Awarnings cargo run -- $USER $PORT + /build/bin/rust/sqlx_exists_demo $USER $PORT +} + +@test "go pgx client" { + /build/bin/go/pgx-test $USER $PORT +} + +@test "go lib/pq client" { + /build/bin/go/libpq-test $USER $PORT +} + +@test "dotnet Npgsql client" { + /build/bin/dotnet/npgsql-test $USER $PORT } diff --git a/testing/postgres-client-tests/r/rpostgres-test.r b/testing/postgres-client-tests/r/rpostgres-test.r new file mode 100644 index 0000000000..6d6d22fa84 --- /dev/null +++ b/testing/postgres-client-tests/r/rpostgres-test.r @@ -0,0 +1,79 @@ +library(RPostgres) +library(DBI) + +args = commandArgs(trailingOnly=TRUE) + +user = args[1] +port = strtoi(args[2]) + +conn = dbConnect(RPostgres::Postgres(), + host="localhost", + port=port, + user=user, + password="password", + dbname="postgres") + +# check standard queries +queries = list( + "DROP TABLE IF EXISTS test", + "create table test (pk int, value int, primary key(pk))", + "select * from test", + "insert into test (pk, value) values (0,0)", + "select * from test") + +responses = list( + NULL, + NULL, + data.frame(pk = integer(0), value = integer(0), stringsAsFactors = FALSE), + NULL, + data.frame(pk = c(as.integer(0)), value = c(as.integer(0)), stringsAsFactors = FALSE)) + +for (i in 1:length(queries)) { + q = queries[[i]] + want = responses[[i]] + if (!is.null(want)) { + got <- dbGetQuery(conn, q) + if (length(want) == length(got)) { + for (j in 1:length(want)) { + if (!identical(want[[j]], got[[j]])) { + print(q) + print(c("want:", want[[j]], "type: ", typeof(want[[j]]))) + print(c("got:", got[[j]], "type: ", typeof(got[[j]]))) + quit("no", 1) + } + } + } + } else { + invisible(dbExecute(conn, q)) + } +} + +dolt_queries = list( + "select dolt_add('-A')", + "select dolt_commit('-m', 'my commit')", + "select dolt_checkout('-b', 'mybranch')", + "insert into test (pk, value) values (1,1)", + "select dolt_commit('-a', '-m', 'my commit2')", + "select dolt_checkout('main')", + "select dolt_merge('mybranch')") + +for (i in 1:length(dolt_queries)) { + q = dolt_queries[[i]] + if (startsWith(trimws(tolower(q)), "select")) { + dbGetQuery(conn, q) + } else { + invisible(dbExecute(conn, q)) + } +} + +count <- dbGetQuery(conn, "select COUNT(*) as c from dolt.log") +want <- data.frame(c = c(4)) +ret <- all.equal(count, want) +if (!isTRUE(ret)) { + print("Number of commits is incorrect") + print(count) + quit("no", 1) +} + +dbDisconnect(conn) +print("RPostgres test passed")