-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add postgres client compatibility tests (#111)
- Loading branch information
1 parent
34d91fd
commit c563289
Showing
10 changed files
with
1,038 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
name: Compatibility Test for Postgres | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
- compatibility | ||
pull_request: | ||
branches: [ "main" ] | ||
|
||
jobs: | ||
|
||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Set up Go | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version: '1.23' | ||
|
||
- name: Set up Node.js | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: '16' | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.10' | ||
|
||
- name: Install dependencies | ||
run: | | ||
go get . | ||
pip3 install "sqlglot[rs]" | ||
pip3 install psycopg2 | ||
curl -LJO https://github.com/duckdb/duckdb/releases/download/v1.1.3/duckdb_cli-linux-amd64.zip | ||
unzip duckdb_cli-linux-amd64.zip | ||
chmod +x duckdb | ||
sudo mv duckdb /usr/local/bin | ||
duckdb -c 'INSTALL json from core' | ||
duckdb -c 'SELECT extension_name, loaded, install_path FROM duckdb_extensions() where installed' | ||
sudo apt-get update | ||
sudo apt-get install --yes --no-install-recommends postgresql-client bats cpanminus | ||
cd compatibility/pg | ||
curl -L -o ./java/postgresql-42.7.4.jar https://jdbc.postgresql.org/download/postgresql-42.7.3.jar | ||
npm install pg | ||
sudo cpanm DBD::Pg | ||
sudo gem install pg | ||
- name: Build | ||
run: go build -v | ||
|
||
- name: Start MyDuck Server | ||
run: | | ||
./myduckserver & | ||
sleep 5 | ||
- name: Run the Compatibility Test | ||
run: | | ||
bats ./compatibility/pg/test.bats |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
#include <stdio.h> | ||
#include <stdlib.h> | ||
#include <libpq-fe.h> | ||
#include <string.h> | ||
|
||
typedef struct { | ||
char *query; | ||
char **expectedResults; | ||
int expectedRows; | ||
int expectedCols; | ||
} Test; | ||
|
||
typedef struct { | ||
PGconn *conn; | ||
Test *tests; | ||
int testCount; | ||
} PGTest; | ||
|
||
void connectDB(PGTest *pgTest, const char *ip, int port, const char *user, const char *password) { | ||
char conninfo[256]; | ||
snprintf(conninfo, sizeof(conninfo), "host=%s port=%d dbname=main user=%s password=%s", ip, port, user, password); | ||
pgTest->conn = PQconnectdb(conninfo); | ||
if (PQstatus(pgTest->conn) != CONNECTION_OK) { | ||
printf("Connection to database failed: %s", PQerrorMessage(pgTest->conn)); | ||
PQfinish(pgTest->conn); | ||
exit(1); | ||
} | ||
} | ||
|
||
void disconnectDB(PGTest *pgTest) { | ||
PQfinish(pgTest->conn); | ||
} | ||
|
||
void addTest(PGTest *pgTest, const char *query, char **expectedResults, int expectedRows, int expectedCols) { | ||
pgTest->tests = realloc(pgTest->tests, sizeof(Test) * (pgTest->testCount + 1)); | ||
pgTest->tests[pgTest->testCount].query = strdup(query); | ||
pgTest->tests[pgTest->testCount].expectedResults = expectedResults; | ||
pgTest->tests[pgTest->testCount].expectedRows = expectedRows; | ||
pgTest->tests[pgTest->testCount].expectedCols = expectedCols; | ||
pgTest->testCount++; | ||
} | ||
|
||
int runTests(PGTest *pgTest) { | ||
for (int i = 0; i < pgTest->testCount; i++) { | ||
Test *test = &pgTest->tests[i]; | ||
printf("Running test: %s\n", test->query); | ||
PGresult *res = PQexec(pgTest->conn, test->query); | ||
if (PQresultStatus(res) != PGRES_TUPLES_OK && PQresultStatus(res) != PGRES_COMMAND_OK) { | ||
printf("Query failed: %s\n", PQerrorMessage(pgTest->conn)); | ||
PQclear(res); | ||
return 0; | ||
} | ||
if (PQresultStatus(res) == PGRES_TUPLES_OK) { | ||
int rows = PQntuples(res); | ||
int cols = PQnfields(res); | ||
if (cols != test->expectedCols) { | ||
printf("Expected %d columns, got %d\n", test->expectedCols, cols); | ||
PQclear(res); | ||
return 0; | ||
} | ||
for (int r = 0; r < rows; r++) { | ||
for (int c = 0; c < cols; c++) { | ||
char *result = PQgetvalue(res, r, c); | ||
if (strcmp(result, test->expectedResults[r * cols + c]) != 0) { | ||
printf("Expected: '%s', got: '%s'\n", test->expectedResults[r * cols + c], result); | ||
PQclear(res); | ||
return 0; | ||
} | ||
} | ||
} | ||
if (rows != test->expectedRows) { | ||
printf("Expected %d rows, got %d\n", test->expectedRows, rows); | ||
PQclear(res); | ||
return 0; | ||
} | ||
} | ||
PQclear(res); | ||
} | ||
return 1; | ||
} | ||
|
||
size_t removeNewline(char *line) { | ||
size_t len = strlen(line); | ||
if (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { | ||
line[--len] = '\0'; | ||
} | ||
if (len > 0 && line[len - 1] == '\r') { | ||
line[--len] = '\0'; | ||
} | ||
return len; | ||
} | ||
|
||
void readTestsFromFile(PGTest *pgTest, const char *filename) { | ||
FILE *file = fopen(filename, "r"); | ||
if (!file) { | ||
perror("Failed to open test data file"); | ||
exit(1); | ||
} | ||
|
||
char line[1024]; | ||
while (fgets(line, sizeof(line), file)) { | ||
size_t len = removeNewline(line); | ||
if (len == 0) continue; | ||
|
||
char *query = strdup(line); | ||
char **expectedResults = NULL; | ||
int expectedRows = 0, expectedCols = 0; | ||
|
||
while (fgets(line, sizeof(line), file)) { | ||
len = removeNewline(line); | ||
if (len == 0) break; | ||
|
||
char *token = strtok(line, ","); | ||
int col = 0; | ||
while (token) { | ||
expectedResults = realloc(expectedResults, sizeof(char*) * (expectedRows * expectedCols + col + 1)); | ||
expectedResults[expectedRows * expectedCols + col] = strdup(token); | ||
token = strtok(NULL, ","); | ||
col++; | ||
} | ||
expectedRows++; | ||
if (expectedCols == 0) expectedCols = col; | ||
} | ||
addTest(pgTest, query, expectedResults, expectedRows, expectedCols); | ||
} | ||
|
||
fclose(file); | ||
} | ||
|
||
int main(int argc, char *argv[]) { | ||
if (argc != 6) { | ||
printf("Usage: %s <ip> <port> <user> <password> <testFile>\n", argv[0]); | ||
return 1; | ||
} | ||
|
||
PGTest pgTest = {0}; | ||
connectDB(&pgTest, argv[1], atoi(argv[2]), argv[3], argv[4]); | ||
|
||
readTestsFromFile(&pgTest, argv[5]); | ||
|
||
int result = runTests(&pgTest); | ||
disconnectDB(&pgTest); | ||
free(pgTest.tests); | ||
return result ? 0 : 1; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import java.io.*; | ||
import java.sql.*; | ||
import java.util.List; | ||
import java.util.LinkedList; | ||
|
||
public class PGTest { | ||
public static class Tests { | ||
private Connection conn; | ||
private Statement st; | ||
private List<Test> tests = new LinkedList<>(); | ||
|
||
public void connect(String ip, int port, String user, String password) { | ||
try { | ||
String url = "jdbc:postgresql://" + ip + ":" + port + "/main"; | ||
conn = DriverManager.getConnection(url, user, password); | ||
st = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); | ||
} catch (SQLException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public void disconnect() { | ||
try { | ||
st.close(); | ||
conn.close(); | ||
} catch (SQLException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public void addTest(String query, String[][] expectedResults) { | ||
tests.add(new Test(query, expectedResults)); | ||
} | ||
|
||
public boolean runTests() { | ||
for (Test test : tests) { | ||
if (!test.run()) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
public void readTestsFromFile(String filename) { | ||
try (BufferedReader br = new BufferedReader(new FileReader(filename))) { | ||
String line; | ||
while ((line = br.readLine()) != null) { | ||
if (line.trim().isEmpty()) continue; | ||
String query = line; | ||
List<String[]> results = new LinkedList<>(); | ||
while ((line = br.readLine()) != null && !line.trim().isEmpty()) { | ||
results.add(line.split(",")); | ||
} | ||
String[][] expectedResults = results.toArray(new String[0][]); | ||
addTest(query, expectedResults); | ||
} | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
System.exit(1); | ||
} | ||
} | ||
|
||
public class Test { | ||
private String query; | ||
private String[][] expectedResults; | ||
|
||
public Test(String query, String[][] expectedResults) { | ||
this.query = query; | ||
this.expectedResults = expectedResults; | ||
} | ||
|
||
public boolean run() { | ||
try { | ||
System.out.println("Running test: " + query); | ||
if (!st.execute(query)) { | ||
System.out.println("Returns 0 rows"); | ||
return expectedResults.length == 0; | ||
} | ||
ResultSet rs = st.getResultSet(); | ||
if (rs.getMetaData().getColumnCount() != expectedResults[0].length) { | ||
System.err.println("Expected " + expectedResults[0].length + " columns, got " + rs.getMetaData().getColumnCount()); | ||
return false; | ||
} | ||
int rows = 0; | ||
while (rs.next()) { | ||
int cols = 0; | ||
for (String expected : expectedResults[rows]) { | ||
String result = rs.getString(cols + 1); | ||
if (!expected.equals(result)) { | ||
System.err.println("Expected:\n'" + expected + "'"); | ||
System.err.println("Result:\n'" + result + "'\nRest of the results:"); | ||
while (rs.next()) { | ||
System.err.println(rs.getString(1)); | ||
} | ||
return false; | ||
} | ||
cols++; | ||
} | ||
rows++; | ||
} | ||
|
||
System.out.println("Returns " + rows + " rows"); | ||
if (rows != expectedResults.length) { | ||
System.err.println("Expected " + expectedResults.length + " rows"); | ||
return false; | ||
} | ||
return true; | ||
} catch (SQLException e) { | ||
System.err.println(e.getMessage()); | ||
return false; | ||
} | ||
} | ||
} | ||
} | ||
|
||
public static void main(String[] args) { | ||
if (args.length < 5) { | ||
System.err.println("Usage: java PGTest <ip> <port> <user> <password> <testFile>"); | ||
System.exit(1); | ||
} | ||
|
||
Tests tests = new Tests(); | ||
tests.connect(args[0], Integer.parseInt(args[1]), args[2], args[3]); | ||
tests.readTestsFromFile(args[4]); | ||
|
||
if (!tests.runTests()) { | ||
tests.disconnect(); | ||
System.exit(1); | ||
} | ||
tests.disconnect(); | ||
} | ||
} |
Oops, something went wrong.