From bca8fbb139ce31bd0c321ca720a8c2a6ba2941dd Mon Sep 17 00:00:00 2001 From: Noy Date: Thu, 28 Nov 2024 16:48:30 +0800 Subject: [PATCH] feat: add mysql client compatibility tests (#111) --- .github/workflows/clients-compatibility.yml | 128 +++++++++++++ .github/workflows/postgres-compatibility.yml | 66 ------- compatibility/{pg => }/.gitignore | 0 compatibility/README.md | 9 + compatibility/mysql/c/mysql_test.c | 154 ++++++++++++++++ compatibility/mysql/clean.sh | 7 + compatibility/mysql/csharp/MySQLTest.cs | 179 +++++++++++++++++++ compatibility/mysql/csharp/MySQLTest.csproj | 12 ++ compatibility/mysql/go/mysql.go | 178 ++++++++++++++++++ compatibility/mysql/java/MySQLTest.java | 136 ++++++++++++++ compatibility/mysql/node/mysql_test.js | 141 +++++++++++++++ compatibility/mysql/perl/mysql_test.pl | 136 ++++++++++++++ compatibility/mysql/php/mysql_test.php | 123 +++++++++++++ compatibility/mysql/python/mysql_test.py | 108 +++++++++++ compatibility/mysql/r/MySQLTest.R | 113 ++++++++++++ compatibility/mysql/ruby/mysql_test.rb | 129 +++++++++++++ compatibility/mysql/rust/Cargo.toml | 9 + compatibility/mysql/rust/mysql_test.rs | 142 +++++++++++++++ compatibility/mysql/test.bats | 91 ++++++++++ compatibility/mysql/test.data | 29 +++ 20 files changed, 1824 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/clients-compatibility.yml delete mode 100644 .github/workflows/postgres-compatibility.yml rename compatibility/{pg => }/.gitignore (100%) create mode 100644 compatibility/README.md create mode 100644 compatibility/mysql/c/mysql_test.c create mode 100644 compatibility/mysql/clean.sh create mode 100644 compatibility/mysql/csharp/MySQLTest.cs create mode 100644 compatibility/mysql/csharp/MySQLTest.csproj create mode 100644 compatibility/mysql/go/mysql.go create mode 100644 compatibility/mysql/java/MySQLTest.java create mode 100644 compatibility/mysql/node/mysql_test.js create mode 100644 compatibility/mysql/perl/mysql_test.pl create mode 100644 compatibility/mysql/php/mysql_test.php create mode 100644 compatibility/mysql/python/mysql_test.py create mode 100644 compatibility/mysql/r/MySQLTest.R create mode 100644 compatibility/mysql/ruby/mysql_test.rb create mode 100644 compatibility/mysql/rust/Cargo.toml create mode 100644 compatibility/mysql/rust/mysql_test.rs create mode 100644 compatibility/mysql/test.bats create mode 100644 compatibility/mysql/test.data diff --git a/.github/workflows/clients-compatibility.yml b/.github/workflows/clients-compatibility.yml new file mode 100644 index 00000000..483303f0 --- /dev/null +++ b/.github/workflows/clients-compatibility.yml @@ -0,0 +1,128 @@ +name: Compatibility Test for Clients + +on: + push: + branches: + - main + - compatibility + - test + pull_request: + branches: [ "main" ] + +jobs: + test-mysql: + 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]" + + 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 bats cpanminus + + cd compatibility/mysql/ + sudo apt-get install --yes --no-install-recommends libmysqlclient-dev dotnet-sdk-8.0 dotnet-runtime-8.0 + curl -L -o ./java/mysql-connector-java-8.0.30.jar https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.30/mysql-connector-java-8.0.30.jar + npm install mysql + sudo apt-get install --yes --no-install-recommends php-mysql + sudo cpanm DBD::mysql + pip3 install mysql-connector-python + sudo apt-get install --yes --no-install-recommends r-base-core + sudo R -e "install.packages('RMySQL', repos='http://cran.r-project.org')" + sudo gem install mysql2 + + - name: Build + run: go build -v + + - name: Start MyDuck Server + run: | + ./myduckserver & + sleep 5 + + - name: Run the Compatibility Test for MySQL Client + run: | + bats ./compatibility/mysql/test.bats + + test-postgresql: + 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]" + + 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 bats cpanminus + + cd compatibility/pg/ + sudo apt-get install --yes --no-install-recommends postgresql-client dotnet-sdk-8.0 dotnet-runtime-8.0 + 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 + pip3 install psycopg2 + sudo apt-get install --yes --no-install-recommends r-base-core + sudo R -e "install.packages('RPostgres', repos='http://cran.r-project.org')" + sudo gem install pg + + - name: Build + run: go build -v + + - name: Start MyDuck Server + run: | + ./myduckserver & + sleep 5 + + - name: Run the Compatibility Test for PostgreSQL Client + run: | + bats ./compatibility/pg/test.bats diff --git a/.github/workflows/postgres-compatibility.yml b/.github/workflows/postgres-compatibility.yml deleted file mode 100644 index 2d604c11..00000000 --- a/.github/workflows/postgres-compatibility.yml +++ /dev/null @@ -1,66 +0,0 @@ -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 diff --git a/compatibility/pg/.gitignore b/compatibility/.gitignore similarity index 100% rename from compatibility/pg/.gitignore rename to compatibility/.gitignore diff --git a/compatibility/README.md b/compatibility/README.md new file mode 100644 index 00000000..bb16799d --- /dev/null +++ b/compatibility/README.md @@ -0,0 +1,9 @@ +# Client Compatibility Test + +This contains a set of tests to verify the compatibility of the client libraries for the different databases. + +You should not run `tests.bats` directly, instead use github actions (or act) to run the tests. + +## TODO + +Fixes tests that are commented out \ No newline at end of file diff --git a/compatibility/mysql/c/mysql_test.c b/compatibility/mysql/c/mysql_test.c new file mode 100644 index 00000000..a6c0d8a9 --- /dev/null +++ b/compatibility/mysql/c/mysql_test.c @@ -0,0 +1,154 @@ +#include +#include +#include +#include + +typedef struct { + char *query; + char **expectedResults; + int expectedRows; + int expectedCols; +} Test; + +typedef struct { + MYSQL *conn; + Test *tests; + int testCount; +} MySQLTest; + +void connectDB(MySQLTest *mysqlTest, const char *ip, int port, const char *user, const char *password); +void disconnectDB(MySQLTest *mysqlTest); +void addTest(MySQLTest *mysqlTest, const char *query, char **expectedResults, int expectedRows, int expectedCols); +int runTests(MySQLTest *mysqlTest); +void readTestsFromFile(MySQLTest *mysqlTest, const char *filename); +size_t removeNewline(char *line); + +void connectDB(MySQLTest *mysqlTest, const char *ip, int port, const char *user, const char *password) { + mysqlTest->conn = mysql_init(NULL); + if (mysqlTest->conn == NULL) { + printf("mysql_init() failed\n"); + exit(1); + } + + if (mysql_real_connect(mysqlTest->conn, ip, user, password, NULL, port, NULL, 0) == NULL) { + printf("mysql_real_connect() failed\n"); + mysql_close(mysqlTest->conn); + exit(1); + } +} + +void disconnectDB(MySQLTest *mysqlTest) { + mysql_close(mysqlTest->conn); +} + +void addTest(MySQLTest *mysqlTest, const char *query, char **expectedResults, int expectedRows, int expectedCols) { + mysqlTest->tests = realloc(mysqlTest->tests, sizeof(Test) * (mysqlTest->testCount + 1)); + mysqlTest->tests[mysqlTest->testCount].query = strdup(query); + mysqlTest->tests[mysqlTest->testCount].expectedResults = expectedResults; + mysqlTest->tests[mysqlTest->testCount].expectedRows = expectedRows; + mysqlTest->tests[mysqlTest->testCount].expectedCols = expectedCols; + mysqlTest->testCount++; +} + +int runTests(MySQLTest *mysqlTest) { + for (int i = 0; i < mysqlTest->testCount; i++) { + Test *test = &mysqlTest->tests[i]; + printf("Running test: %s\n", test->query); + if (mysql_query(mysqlTest->conn, test->query)) { + printf("Query failed: %s\n", mysql_error(mysqlTest->conn)); + return 0; + } + + MYSQL_RES *res = mysql_store_result(mysqlTest->conn); + if (res) { + int rows = mysql_num_rows(res); + int cols = mysql_num_fields(res); + if (cols != test->expectedCols) { + printf("Expected %d columns, got %d\n", test->expectedCols, cols); + mysql_free_result(res); + return 0; + } + MYSQL_ROW row; + int r = 0; + while ((row = mysql_fetch_row(res))) { + for (int c = 0; c < cols; c++) { + if (strcmp(row[c], test->expectedResults[r * cols + c]) != 0) { + printf("Expected: '%s', got: '%s'\n", test->expectedResults[r * cols + c], row[c]); + mysql_free_result(res); + return 0; + } + } + r++; + } + if (rows != test->expectedRows) { + printf("Expected %d rows, got %d\n", test->expectedRows, rows); + mysql_free_result(res); + return 0; + } + mysql_free_result(res); + } + } + return 1; +} + +size_t removeNewline(char *line) { + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } + return len; +} + +void readTestsFromFile(MySQLTest *mysqlTest, 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(mysqlTest, query, expectedResults, expectedRows, expectedCols); + } + + fclose(file); +} + +int main(int argc, char *argv[]) { + if (argc != 6) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + MySQLTest mysqlTest = {0}; + connectDB(&mysqlTest, argv[1], atoi(argv[2]), argv[3], argv[4]); + + readTestsFromFile(&mysqlTest, argv[5]); + + int result = runTests(&mysqlTest); + disconnectDB(&mysqlTest); + free(mysqlTest.tests); + return result ? 0 : 1; +} \ No newline at end of file diff --git a/compatibility/mysql/clean.sh b/compatibility/mysql/clean.sh new file mode 100644 index 00000000..578db075 --- /dev/null +++ b/compatibility/mysql/clean.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +rm -rf ./c/mysql_test \ + ./csharp/bin ./csharp/obj \ + ./go/mysql \ + ./java/*.class \ + ./rust/target ./rust/Cargo.lock \ No newline at end of file diff --git a/compatibility/mysql/csharp/MySQLTest.cs b/compatibility/mysql/csharp/MySQLTest.cs new file mode 100644 index 00000000..802f5474 --- /dev/null +++ b/compatibility/mysql/csharp/MySQLTest.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Data; +using MySql.Data.MySqlClient; +using System.IO; + +public class MySQLTest +{ + public class Tests + { + private MySqlConnection conn; + private MySqlCommand cmd; + private List tests = new List(); + + public void Connect(string ip, int port, string user, string password) + { + string connectionString = $"Server={ip};Port={port};User Id={user};Password={password};"; + try + { + conn = new MySqlConnection(connectionString); + conn.Open(); + cmd = conn.CreateCommand(); + cmd.CommandType = CommandType.Text; + } + catch (MySqlException e) + { + throw new Exception($"Error connecting to database: {e.Message}", e); + } + } + + public void Disconnect() + { + try + { + cmd.Dispose(); + conn.Close(); + } + catch (MySqlException e) + { + throw new Exception(e.Message); + } + } + + public void AddTest(string query, string[][] expectedResults) + { + tests.Add(new Test(query, expectedResults)); + } + + public bool RunTests() + { + foreach (var test in tests) + { + if (!test.Run(cmd)) + { + return false; + } + } + return true; + } + + public void ReadTestsFromFile(string filename) + { + try + { + using (var reader = new StreamReader(filename)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + if (string.IsNullOrWhiteSpace(line)) continue; + string query = line; + var results = new List(); + while ((line = reader.ReadLine()) != null && !string.IsNullOrWhiteSpace(line)) + { + results.Add(line.Split(',')); + } + string[][] expectedResults = results.ToArray(); + AddTest(query, expectedResults); + } + } + } + catch (IOException e) + { + Console.Error.WriteLine(e.Message); + Environment.Exit(1); + } + } + + public class Test + { + private string query; + private string[][] expectedResults; + + public Test(string query, string[][] expectedResults) + { + this.query = query; + this.expectedResults = expectedResults; + } + + public bool Run(MySqlCommand cmd) + { + try + { + Console.WriteLine("Running test: " + query); + cmd.CommandText = query; + using (var reader = cmd.ExecuteReader()) + { + if (!reader.HasRows) + { + if (expectedResults.Length != 0) + { + Console.Error.WriteLine($"Expected {expectedResults.Length} rows, got 0"); + return false; + } + Console.WriteLine("Returns 0 rows"); + return true; + } + if (reader.FieldCount != expectedResults[0].Length) + { + Console.Error.WriteLine($"Expected {expectedResults[0].Length} columns, got {reader.FieldCount}"); + return false; + } + int rows = 0; + while (reader.Read()) + { + for (int col = 0; col < expectedResults[rows].Length; col++) + { + string result = reader.GetString(col); + if (expectedResults[rows][col] != result) + { + Console.Error.WriteLine($"Expected:\n'{expectedResults[rows][col]}'"); + Console.Error.WriteLine($"Result:\n'{result}'\nRest of the results:"); + while (reader.Read()) + { + Console.Error.WriteLine(reader.GetString(0)); + } + return false; + } + } + rows++; + } + Console.WriteLine("Returns " + rows + " rows"); + if (rows != expectedResults.Length) + { + Console.Error.WriteLine($"Expected {expectedResults.Length} rows"); + return false; + } + return true; + } + } + catch (MySqlException e) + { + Console.Error.WriteLine(e.Message); + return false; + } + } + } + } + + public static void Main(string[] args) + { + if (args.Length < 5) + { + Console.Error.WriteLine("Usage: MySQLTest "); + Environment.Exit(1); + } + + var tests = new Tests(); + tests.Connect(args[0], int.Parse(args[1]), args[2], args[3]); + tests.ReadTestsFromFile(args[4]); + + if (!tests.RunTests()) + { + tests.Disconnect(); + Environment.Exit(1); + } + tests.Disconnect(); + } +} \ No newline at end of file diff --git a/compatibility/mysql/csharp/MySQLTest.csproj b/compatibility/mysql/csharp/MySQLTest.csproj new file mode 100644 index 00000000..1ed26eab --- /dev/null +++ b/compatibility/mysql/csharp/MySQLTest.csproj @@ -0,0 +1,12 @@ + + + + Exe + net8.0 + + + + + + + \ No newline at end of file diff --git a/compatibility/mysql/go/mysql.go b/compatibility/mysql/go/mysql.go new file mode 100644 index 00000000..a3e22325 --- /dev/null +++ b/compatibility/mysql/go/mysql.go @@ -0,0 +1,178 @@ +package main + +import ( + "bufio" + "database/sql" + "fmt" + "os" + "strconv" + "strings" + + _ "github.com/go-sql-driver/mysql" +) + +type Test struct { + query string + expectedResults [][]string +} + +type Tests struct { + conn *sql.DB + tests []Test +} + +func (t *Tests) connect(ip string, port int, user, password string) { + connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/", user, password, ip, port) + var err error + t.conn, err = sql.Open("mysql", connStr) + if err != nil { + panic(err) + } +} + +func (t *Tests) disconnect() { + err := t.conn.Close() + if err != nil { + panic(err) + } +} + +func (t *Tests) addTest(query string, expectedResults [][]string) { + t.tests = append(t.tests, Test{query, expectedResults}) +} + +func (t *Tests) readTestsFromFile(filename string) { + file, err := os.Open(filename) + if err != nil { + panic(err) + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + panic(err) + } + }(file) + + scanner := bufio.NewScanner(file) + var query string + var results [][]string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" { + if query != "" { + t.addTest(query, results) + query, results = "", nil + } + } else if query == "" { + query = line + } else { + results = append(results, strings.Split(line, ",")) + } + } + if query != "" { + t.addTest(query, results) + } + if err := scanner.Err(); err != nil { + panic(err) + } +} + +func (t *Tests) runTests() bool { + for _, test := range t.tests { + if !t.runTest(test) { + return false + } + } + return true +} + +func (t *Tests) runTest(test Test) bool { + fmt.Println("Running test:", test.query) + rows, err := t.conn.Query(test.query) + if err != nil { + fmt.Println("Error executing query:", err) + return false + } + defer func(rows *sql.Rows) { + err := rows.Close() + if err != nil { + panic(err) + } + }(rows) + + columns, err := rows.Columns() + if err != nil { + fmt.Println("Error getting columns:", err) + return false + } + + if len(test.expectedResults) == 0 { + if len(columns) != 0 { + fmt.Printf("Expected 0 columns, got %d\n", len(columns)) + return false + } + fmt.Println("Returns 0 rows") + return true + } + + if len(columns) != len(test.expectedResults[0]) { + fmt.Printf("Expected %d columns, got %d\n", len(test.expectedResults[0]), len(columns)) + return false + } + + var rowCount int + for rows.Next() { + row := make([]string, len(columns)) + rowPointers := make([]interface{}, len(columns)) + for i := range row { + rowPointers[i] = &row[i] + } + if err := rows.Scan(rowPointers...); err != nil { + fmt.Println("Error scanning row:", err) + return false + } + + for i, expected := range test.expectedResults[rowCount] { + if row[i] != expected { + fmt.Printf("Mismatch at row %d, column %d: expected '%s', got '%s'\n", rowCount+1, i+1, expected, row[i]) + return false + } + } + rowCount++ + } + + if rowCount != len(test.expectedResults) { + fmt.Printf("Expected %d rows, got %d\n", len(test.expectedResults), rowCount) + return false + } + + fmt.Printf("Returns %d rows\n", rowCount) + return true +} + +func main() { + if len(os.Args) < 6 { + fmt.Println("Usage: mysql_test ") + os.Exit(1) + } + + ip := os.Args[1] + port, ok := strconv.Atoi(os.Args[2]) + if ok != nil { + fmt.Println("Invalid port:", os.Args[2]) + os.Exit(1) + } + user := os.Args[3] + password := os.Args[4] + testFile := os.Args[5] + + tests := &Tests{} + tests.connect(ip, port, user, password) + defer tests.disconnect() + + tests.readTestsFromFile(testFile) + + if !tests.runTests() { + os.Exit(1) + } +} diff --git a/compatibility/mysql/java/MySQLTest.java b/compatibility/mysql/java/MySQLTest.java new file mode 100644 index 00000000..f6ff672a --- /dev/null +++ b/compatibility/mysql/java/MySQLTest.java @@ -0,0 +1,136 @@ +import java.io.*; +import java.sql.*; +import java.util.List; +import java.util.LinkedList; + +public class MySQLTest { + public static class Tests { + private Connection conn; + private Statement st; + private List tests = new LinkedList<>(); + + public void connect(String ip, int port, String user, String password) { + try { + String url = "jdbc:mysql://" + ip + ":" + port + "/"; + 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 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)) { + if (expectedResults.length != 0) { + System.err.println("Expected " + expectedResults.length + " rows, got 0"); + return false; + } + System.out.println("Returns 0 rows"); + return true; + } + 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 MySQLTest "); + 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(); + } +} \ No newline at end of file diff --git a/compatibility/mysql/node/mysql_test.js b/compatibility/mysql/node/mysql_test.js new file mode 100644 index 00000000..189afe5c --- /dev/null +++ b/compatibility/mysql/node/mysql_test.js @@ -0,0 +1,141 @@ +const mysql = require('mysql'); +const fs = require('fs'); + +class Test { + constructor(query, expectedResults) { + this.query = query; + this.expectedResults = expectedResults; + } + + async run(connection) { + return new Promise((resolve, reject) => { + console.log(`Running test: ${this.query}`); + connection.query(this.query, (error, results, fields) => { + if (error) { + console.error(error.message); + return resolve(false); + } + if (results.length === undefined || results.length === 0) { + if (this.expectedResults.length !== 0) { + console.error(`Expected ${this.expectedResults.length} rows, got 0`); + return resolve(false); + } + console.log("Returns 0 rows"); + return resolve(true); + } + if (fields.length !== this.expectedResults[0].length) { + console.error(`Expected ${this.expectedResults[0].length} columns, got ${fields.length}`); + return resolve(false); + } + for (let i = 0; i < results.length; i++) { + const row = results[i]; + const expectedRow = this.expectedResults[i]; + for (let j = 0; j < expectedRow.length; j++) { + const columnName = fields[j].name; + if (String(row[columnName]) !== String(expectedRow[j])) { + console.error(`Expected: ${String(expectedRow[j])}, got: ${String(row[columnName])}`); + return resolve(false); + } + } + } + console.log(`Returns ${results.length} rows`); + if (results.length !== this.expectedResults.length) { + console.error(`Expected ${this.expectedResults.length} rows`); + return resolve(false); + } + resolve(true); + }); + }); + } +} + +class Tests { + constructor() { + this.connection = null; + this.tests = []; + } + + connect(ip, port, user, password) { + this.connection = mysql.createConnection({ + host: ip, + port: port, + user: user, + password: password + }); + return new Promise((resolve, reject) => { + this.connection.connect((err) => { + if (err) { + console.error('Error connecting to the database:', err.message); + return reject(err); + } + resolve(); + }); + }); + } + + disconnect() { + return new Promise((resolve, reject) => { + this.connection.end((err) => { + if (err) { + console.error('Error disconnecting from the database:', err.message); + return reject(err); + } + resolve(); + }); + }); + } + + addTest(query, expectedResults) { + this.tests.push(new Test(query, expectedResults)); + } + + async runTests() { + for (const test of this.tests) { + if (!(await test.run(this.connection))) { + return false; + } + } + return true; + } + + readTestsFromFile(filename) { + const data = fs.readFileSync(filename, 'utf8'); + const lines = data.split('\n'); + let query = null; + let expectedResults = []; + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) { + if (query) { + this.addTest(query, expectedResults); + query = null; + expectedResults = []; + } + } else if (query === null) { + query = trimmedLine; + } else { + expectedResults.push(trimmedLine.split(',')); + } + } + if (query) { + this.addTest(query, expectedResults); + } + } +} + +(async () => { + if (process.argv.length < 6) { + console.error("Usage: node mysql_test.js "); + process.exit(1); + } + + const tests = new Tests(); + await tests.connect(process.argv[2], parseInt(process.argv[3]), process.argv[4], process.argv[5]); + tests.readTestsFromFile(process.argv[6]); + + if (!(await tests.runTests())) { + await tests.disconnect(); + process.exit(1); + } + await tests.disconnect(); +})(); \ No newline at end of file diff --git a/compatibility/mysql/perl/mysql_test.pl b/compatibility/mysql/perl/mysql_test.pl new file mode 100644 index 00000000..bfee7ca1 --- /dev/null +++ b/compatibility/mysql/perl/mysql_test.pl @@ -0,0 +1,136 @@ +use strict; +use warnings; +use DBI; + +package Tests; + +sub new { + my $class = shift; + my $self = { + conn => undef, + tests => [], + }; + bless $self, $class; + return $self; +} + +sub connect { + my ($self, $ip, $port, $user, $password) = @_; + my $dsn = "DBI:mysql:host=$ip;port=$port"; + $self->{conn} = DBI->connect($dsn, $user, $password, { RaiseError => 1, AutoCommit => 1 }); +} + +sub disconnect { + my $self = shift; + $self->{conn}->disconnect(); +} + +sub add_test { + my ($self, $query, $expected_results) = @_; + push @{$self->{tests}}, { query => $query, expected_results => $expected_results }; +} + +sub run_tests { + my $self = shift; + foreach my $test (@{$self->{tests}}) { + return 0 unless $self->run_test($test); + } + return 1; +} + +sub trim { + my $s = shift; + $s =~ s/^\s+|\s+$//g; + return $s; +} + +sub read_tests_from_file { + my ($self, $filename) = @_; + open my $fh, '<', $filename or die "Could not open file '$filename': $!"; + my $query; + my @results; + while (my $line = <$fh>) { + $line = trim($line); + if ($line eq '') { + if ($query) { + $self->add_test($query, [@results]); + $query = undef; + @results = (); + } + } elsif (!$query) { + $query = $line; + } else { + push @results, [split /,/, $line]; + } + } + $self->add_test($query, [@results]) if $query; + close $fh; +} + +sub run_test { + my ($self, $test) = @_; + my $query = $test->{query}; + my $expected_results = $test->{expected_results}; + print "Running test: $query\n"; + + my $st = $self->{conn}->prepare($query); + $st->execute(); + my $rows = $st->rows; + if ($rows == 0 || $rows == -1) { + if (defined $expected_results && @$expected_results == 0) { + print "No rows returned\n"; + return 1; + } + print "Expected " . @$expected_results . " rows, got 0\n"; + return 0; + } + my $cols = $st->{NUM_OF_FIELDS}; + if (!defined $cols) { + if (defined $expected_results && @$expected_results == 0) { + print "No columns returned\n"; + return 1; + } + print "Expected " . @{$expected_results->[0]} . " columns, got 0\n"; + return 0; + } + if ($cols != @{$expected_results->[0]}) { + print "Expected " . @{$expected_results->[0]} . " columns, got $cols\n"; + return 0; + } + my $row_num = 0; + while (my @row = $st->fetchrow_array) { + for my $col_num (0..$#row) { + if ($row[$col_num] ne $expected_results->[$row_num][$col_num]) { + print "Expected:\n'$expected_results->[$row_num][$col_num]'\n"; + print "Result:\n'$row[$col_num]'\nRest of the results:\n"; + while (my @rest = $st->fetchrow_array) { + print join(',', @rest) . "\n"; + } + return 0; + } + } + $row_num++; + } + print "Returns $row_num rows\n"; + if ($row_num != @$expected_results) { + print "Expected " . @$expected_results . " rows\n"; + return 0; + } + return 1; +} + +package main; + +if (@ARGV < 5) { + die "Usage: perl mysql_test.pl \n"; +} + +my $tests = Tests->new(); +$tests->connect($ARGV[0], $ARGV[1], $ARGV[2], $ARGV[3]); +$tests->read_tests_from_file($ARGV[4]); + +if (!$tests->run_tests()) { + $tests->disconnect(); + exit 1; +} +$tests->disconnect(); \ No newline at end of file diff --git a/compatibility/mysql/php/mysql_test.php b/compatibility/mysql/php/mysql_test.php new file mode 100644 index 00000000..18572b7f --- /dev/null +++ b/compatibility/mysql/php/mysql_test.php @@ -0,0 +1,123 @@ + \n"; + exit(1); + } + + $tests = new Tests(); + $tests->connect($args[0], intval($args[1]), $args[2], $args[3]); + $tests->readTestsFromFile($args[4]); + + if (!$tests->runTests()) { + $tests->disconnect(); + exit(1); + } + $tests->disconnect(); + } +} + +class Tests { + private $conn; + private $tests = []; + + public function connect($ip, $port, $user, $password) { + try { + $url = "mysql:host=$ip;port=$port"; + $this->conn = new PDO($url, $user, $password); + } catch (PDOException $e) { + throw new RuntimeException($e->getMessage()); + } + } + + public function disconnect() { + $this->conn = null; + } + + public function addTest($query, $expectedResults) { + $this->tests[] = new Test($query, $expectedResults); + } + + public function runTests() { + foreach ($this->tests as $test) { + if (!$test->run($this->conn)) { + return false; + } + } + return true; + } + + public function readTestsFromFile($filename) { + $file = fopen($filename, "r"); + if (!$file) { + throw new RuntimeException("Failed to open test data file"); + } + + while (($line = fgets($file)) !== false) { + $line = trim($line); + if (empty($line)) continue; // Skip empty lines + $query = $line; + $expectedResults = []; + while (($line = fgets($file)) !== false) { + $line = trim($line); + if (empty($line)) break; // End of expected results for this query + $expectedResults[] = explode(",", $line); + } + $this->addTest($query, $expectedResults); + } + + fclose($file); + } +} + +class Test { + private $query; + private $expectedResults; + + public function __construct($query, $expectedResults) { + $this->query = $query; + $this->expectedResults = $expectedResults; + } + + public function run($conn) { + try { + echo "Running test: " . $this->query . "\n"; + $st = $conn->prepare($this->query); + $st->execute(); + if ($st->columnCount() == 0) { + if (count($this->expectedResults) != 0) { + echo "Expected " . count($this->expectedResults) . " rows, got 0\n"; + return false; + } + echo "Returns 0 rows\n"; + return true; + } + $rows = $st->fetchAll(PDO::FETCH_NUM); + if (count($rows[0]) != count($this->expectedResults[0])) { + echo "Expected " . count($this->expectedResults[0]) . " columns, got " . count($rows[0]) . "\n"; + return false; + } + foreach ($rows as $i => $row) { + if ($row != $this->expectedResults[$i]) { + echo "Expected: " . implode(", ", $this->expectedResults[$i]) . ", got: " . implode(", ", $row) . "\n"; + return false; + } + } + echo "Returns " . count($rows) . " rows\n"; + if (count($rows) != count($this->expectedResults)) { + echo "Expected " . count($this->expectedResults) . " rows\n"; + return false; + } + return true; + } catch (PDOException $e) { + echo $e->getMessage() . "\n"; + return false; + } + } +} + +MySQLTest::main(array_slice($argv, 1)); + +?> \ No newline at end of file diff --git a/compatibility/mysql/python/mysql_test.py b/compatibility/mysql/python/mysql_test.py new file mode 100644 index 00000000..25873171 --- /dev/null +++ b/compatibility/mysql/python/mysql_test.py @@ -0,0 +1,108 @@ +import mysql.connector + +class MySQLTest: + class Test: + def __init__(self, query, expected_results): + self.query = query + self.expected_results = expected_results + + def run(self, cursor): + print(f"Running test: {self.query}") + cursor.execute(self.query) + if cursor.description is None: + if len(self.expected_results) != 0: + print(f"Expected {len(self.expected_results)} rows, got 0") + return False + print("Returns 0 rows") + return True + + rows = cursor.fetchall() + if len(rows[0]) != len(self.expected_results[0]): + print(f"Expected {len(self.expected_results[0])} columns, got {len(rows[0])}") + return False + + for row, expected_row in zip(rows, self.expected_results): + if list(map(str, row)) != list(map(str, expected_row)): + print(f"Expected: {list(map(str, expected_row))}, got: {list(map(str, row))}") + return False + + print(f"Returns {len(rows)} rows") + if len(rows) != len(self.expected_results): + print(f"Expected {len(self.expected_results)} rows") + return False + + return True + + def __init__(self): + self.conn = None + self.tests = [] + + def connect(self, ip, port, user, password): + try: + self.conn = mysql.connector.connect( + host=ip, + port=port, + user=user, + password=password + ) + except Exception as e: + raise RuntimeError(e) + + def disconnect(self): + self.conn.close() + + def add_test(self, query, expected_results): + self.tests.append(self.Test(query, expected_results)) + + def run_tests(self): + for test in self.tests: + cursor = None + try: + self.conn.autocommit = False + cursor = self.conn.cursor() + if not test.run(cursor): + return False + except Exception as e: + print(f"Error running test: {e}") + return False + finally: + if cursor: + cursor.close() + return True + + def read_tests_from_file(self, filename): + with open(filename, "r") as file: + lines = file.readlines() + + query = None + expected_results = [] + for line in lines: + line = line.strip() + if not line: + if query: + self.add_test(query, expected_results) + query = None + expected_results = [] + elif query is None: + query = line + else: + expected_results.append(line.split(",")) + + if query: + self.add_test(query, expected_results) + +if __name__ == "__main__": + import sys + if len(sys.argv) != 6: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + mysql_test = MySQLTest() + mysql_test.connect(sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4]) + mysql_test.read_tests_from_file(sys.argv[5]) + + if not mysql_test.run_tests(): + mysql_test.disconnect() + sys.exit(1) + + mysql_test.disconnect() \ No newline at end of file diff --git a/compatibility/mysql/r/MySQLTest.R b/compatibility/mysql/r/MySQLTest.R new file mode 100644 index 00000000..1c18154c --- /dev/null +++ b/compatibility/mysql/r/MySQLTest.R @@ -0,0 +1,113 @@ +library(RMySQL) + +Tests <- setRefClass( + "Tests", + fields = list( + conn = "ANY", + tests = "list" + ), + methods = list( + connect = function(ip, port, user, password) { + conn <<- dbConnect( + MySQL(), + host = ip, + port = port, + user = user, + password = password + ) + }, + disconnect = function() { + dbDisconnect(conn) + }, + addTest = function(query, expectedResults) { + tests <<- c(tests, list(Test$new(query, expectedResults))) + }, + runTests = function() { + for (test in tests) { + if (!test$run(conn)) { + return(FALSE) + } + } + return(TRUE) + }, + readTestsFromFile = function(filename) { + lines <- readLines(filename) + i <- 1 + while (i <= length(lines)) { + if (trimws(lines[i]) == "") { + i <- i + 1 + next + } + query <- lines[i] + i <- i + 1 + results <- list() + while (i <= length(lines) && trimws(lines[i]) != "") { + results <- c(results, list(strsplit(lines[i], ",")[[1]])) + i <- i + 1 + } + addTest(query, results) + } + } + ) +) + +Test <- setRefClass( + "Test", + fields = list( + query = "character", + expectedResults = "list" + ), + methods = list( + initialize = function(query, expectedResults) { + query <<- query + expectedResults <<- expectedResults + }, + run = function(conn) { + cat("Running test:", query, "\n") + res <- dbSendQuery(conn, query) + fetched <- dbFetch(res) + dbClearResult(res) + if (length(expectedResults) == 0) { + if (nrow(fetched) == 0 || ncol(fetched) == 0) { + return(TRUE) + } else { + cat("Expected empty result, got", nrow(fetched), "rows and", ncol(fetched), "columns\n") + return(FALSE) + } + } + if (ncol(fetched) != length(expectedResults[[1]])) { + cat("Expected", length(expectedResults[[1]]), "columns, got", ncol(fetched), "\n") + return(FALSE) + } + if (nrow(fetched) != length(expectedResults)) { + cat("Expected", length(expectedResults), "rows, got", nrow(fetched), "\n") + return(FALSE) + } + for (i in seq_len(nrow(fetched))) { + for (j in seq_len(ncol(fetched))) { + if (as.character(fetched[i, j]) != expectedResults[[i]][j]) { + cat("Expected:", expectedResults[[i]][j], "Got:", fetched[i, j], "\n") + return(FALSE) + } + } + } + return(TRUE) + } + ) +) + +args <- commandArgs(trailingOnly = TRUE) +if (length(args) < 5) { + cat("Usage: Rscript MySQLTest.R \n") + quit(status = 1) +} + +tests <- Tests$new() +tests$connect(args[1], as.integer(args[2]), args[3], args[4]) +tests$readTestsFromFile(args[5]) + +if (!tests$runTests()) { + tests$disconnect() + quit(status = 1) +} +tests$disconnect() \ No newline at end of file diff --git a/compatibility/mysql/ruby/mysql_test.rb b/compatibility/mysql/ruby/mysql_test.rb new file mode 100644 index 00000000..4a821950 --- /dev/null +++ b/compatibility/mysql/ruby/mysql_test.rb @@ -0,0 +1,129 @@ +require 'mysql2' + +class MySQLTest + class Tests + def initialize + @conn = nil + @tests = [] + end + + def connect(ip, port, user, password) + begin + @conn = Mysql2::Client.new(host: ip, port: port, username: user, password: password) + rescue Mysql2::Error => e + raise "Connection failed: #{e.message}" + end + end + + def disconnect + @conn.close if @conn + end + + def add_test(query, expected_results) + @tests << Test.new(query, expected_results) + end + + def run_tests + @tests.each do |test| + return false unless test.run(@conn) + end + true + end + + def read_tests_from_file(filename) + begin + File.open(filename, 'r') do |file| + query = nil + results = [] + file.each_line do |line| + line = line.strip + if line.empty? + if query + add_test(query, results) + query = nil + results = [] + end + else + if query.nil? + query = line + else + results << line.split(',') + end + end + end + add_test(query, results) if query + end + rescue IOError => e + puts "File read error: #{e.message}" + exit 1 + end + end + + class Test + def initialize(query, expected_results) + @query = query + @expected_results = expected_results + end + + def run(conn) + begin + puts "Running test: #{@query}" + result = conn.query(@query) + if result.nil? || result.count == 0 + if @expected_results.empty? + puts "Returns 0 rows" + return true + end + puts "Expected #{@expected_results.length} rows, got 0" + return false + end + if result.fields.size != @expected_results[0].length + puts "Expected #{@expected_results[0].length} columns, got #{result.fields.size}" + return false + end + result.each_with_index do |row, row_num| + row.each_with_index do |(col_name, value), col_num| + expected = @expected_results[row_num][col_num] + if value.to_s != expected.to_s + puts "Expected:\n'#{expected}'" + puts "Result:\n'#{value}'\nRest of the results:" + result.each { |r| puts r.values.join(',') } + return false + end + end + end + puts "Returns #{result.count} rows" + if result.count != @expected_results.length + puts "Expected #{@expected_results.length} rows" + return false + end + true + rescue Mysql2::Error => e + puts e.message + false + end + end + end + end + + def self.main(args) + if args.length < 5 + puts "Usage: ruby MySQLTest.rb " + exit 1 + end + + tests = Tests.new + tests.connect(args[0], args[1].to_i, args[2], args[3]) + tests.read_tests_from_file(args[4]) + + unless tests.run_tests + tests.disconnect + exit 1 + end + tests.disconnect + end +end + +if __FILE__ == $0 + MySQLTest.main(ARGV) +end \ No newline at end of file diff --git a/compatibility/mysql/rust/Cargo.toml b/compatibility/mysql/rust/Cargo.toml new file mode 100644 index 00000000..2dc6cf58 --- /dev/null +++ b/compatibility/mysql/rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mysql_test" + +[dependencies] +mysql = "*" + +[[bin]] +name = "mysql_test" +path = "./mysql_test.rs" \ No newline at end of file diff --git a/compatibility/mysql/rust/mysql_test.rs b/compatibility/mysql/rust/mysql_test.rs new file mode 100644 index 00000000..ab665653 --- /dev/null +++ b/compatibility/mysql/rust/mysql_test.rs @@ -0,0 +1,142 @@ +use std::fs::File; +use std::io::{self, BufRead, BufReader}; +use std::process::exit; + +extern crate mysql; +use mysql::{Pool, PooledConn, OptsBuilder, prelude::*}; + +struct Test { + query: String, + expected_results: Vec>, +} + +impl Test { + fn new(query: String, expected_results: Vec>) -> Self { + Test { query, expected_results } + } + + fn run(&self, conn: &mut PooledConn) -> bool { + println!("Running test: {}", self.query); + match conn.query_iter(&self.query) { + Ok(result) => { + let rows: Result, _> = result.collect(); + match rows { + Ok(rows) => { + if rows.is_empty() { + if self.expected_results.is_empty() { + println!("Returns 0 rows"); + return true; + } + eprintln!("Expected {} rows, got 0", self.expected_results.len()); + return false; + } + if rows[0].len() != self.expected_results[0].len() { + eprintln!("Expected {} columns, got {}", self.expected_results[0].len(), rows[0].len()); + return false; + } + for (i, row) in rows.iter().enumerate() { + for (j, expected) in self.expected_results[i].iter().enumerate() { + let result: String = row.get(j).unwrap_or_default(); + if expected != &result { + eprintln!("Expected:\n'{}'", expected); + eprintln!("Result:\n'{}'\nRest of the results:", result); + for row in rows.iter().skip(i + 1) { + eprintln!("{:?}", row); + } + return false; + } + } + } + println!("Returns {} rows", rows.len()); + if rows.len() != self.expected_results.len() { + eprintln!("Expected {} rows", self.expected_results.len()); + return false; + } + true + } + Err(err) => { + eprintln!("{}", err); + false + } + } + } + Err(err) => { + eprintln!("{}", err); + false + } + } + } +} + +struct Tests { + pool: Pool, + tests: Vec, +} + +impl Tests { + fn new(ip: &str, port: u16, user: &str, password: &str) -> Result { + let opts = OptsBuilder::new() + .ip_or_hostname(Some(ip)) + .tcp_port(port) + .user(Some(user)) + .pass(Some(password)); + let pool = Pool::new(opts)?; + Ok(Tests { pool, tests: Vec::new() }) + } + + fn add_test(&mut self, query: String, expected_results: Vec>) { + self.tests.push(Test::new(query, expected_results)); + } + + fn run_tests(&mut self) -> bool { + let mut conn = self.pool.get_conn().expect("Failed to get connection from pool"); + for test in &self.tests { + if !test.run(&mut conn) { + return false; + } + } + true + } + + fn read_tests_from_file(&mut self, filename: &str) -> io::Result<()> { + let file = File::open(filename)?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + while let Some(Ok(line)) = lines.next() { + if line.trim().is_empty() { + continue; + } + let query = line; + let mut results = Vec::new(); + while let Some(Ok(line)) = lines.next() { + if line.trim().is_empty() { + break; + } + results.push(line.split(',').map(String::from).collect()); + } + self.add_test(query, results); + } + Ok(()) + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 6 { + eprintln!("Usage: {} ", args[0]); + exit(1); + } + + let ip = &args[1]; + let port: u16 = args[2].parse().expect("Invalid port number"); + let user = &args[3]; + let password = &args[4]; + let test_file = &args[5]; + + let mut tests = Tests::new(ip, port, user, password).expect("Failed to connect to database"); + tests.read_tests_from_file(test_file).expect("Failed to read test file"); + + if !tests.run_tests() { + exit(1); + } +} \ No newline at end of file diff --git a/compatibility/mysql/test.bats b/compatibility/mysql/test.bats new file mode 100644 index 00000000..8eab04a3 --- /dev/null +++ b/compatibility/mysql/test.bats @@ -0,0 +1,91 @@ +#!/usr/bin/env bats + +setup() { + mysql -h 127.0.0.1 -P 3306 -u root -e "DROP DATABASE IF EXISTS test;" + touch /tmp/test_pids +} + +custom_teardown="" + +set_custom_teardown() { + custom_teardown="$1" +} + +teardown() { + if [ -n "$custom_teardown" ]; then + eval "$custom_teardown" + custom_teardown="" + fi + + while read -r pid; do + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" + wait "$pid" 2>/dev/null + fi + done < /tmp/test_pids + rm /tmp/test_pids +} + +start_process() { + run timeout 2m "$@" + echo $! >> /tmp/test_pids + check_status +} + +check_status() { + if [ "$status" -ne 0 ]; then + echo "$output" + echo "$stderr" + fi + [ "$status" -eq 0 ] +} + +@test "mysql-c" { + start_process gcc -o $BATS_TEST_DIRNAME/c/mysql_test $BATS_TEST_DIRNAME/c/mysql_test.c -I/usr/include/mysql -lmysqlclient + start_process $BATS_TEST_DIRNAME/c/mysql_test 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-csharp" { + set_custom_teardown "sudo pkill -f dotnet" + start_process dotnet build $BATS_TEST_DIRNAME/csharp/MySQLTest.csproj -o $BATS_TEST_DIRNAME/csharp/bin + start_process dotnet $BATS_TEST_DIRNAME/csharp/bin/MySQLTest.dll 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-go" { + start_process go build -o $BATS_TEST_DIRNAME/go/mysql $BATS_TEST_DIRNAME/go/mysql.go + start_process $BATS_TEST_DIRNAME/go/mysql 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-java" { + start_process javac $BATS_TEST_DIRNAME/java/MySQLTest.java + start_process java -cp $BATS_TEST_DIRNAME/java:$BATS_TEST_DIRNAME/java/mysql-connector-java-8.0.30.jar MySQLTest 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-node" { + start_process node $BATS_TEST_DIRNAME/node/mysql_test.js 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-perl" { + start_process perl $BATS_TEST_DIRNAME/perl/mysql_test.pl 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-php" { + start_process php $BATS_TEST_DIRNAME/php/mysql_test.php 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-python" { + start_process python3 $BATS_TEST_DIRNAME/python/mysql_test.py 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-r" { + start_process Rscript $BATS_TEST_DIRNAME/r/MySQLTest.R 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql_ruby" { + start_process ruby $BATS_TEST_DIRNAME/ruby/mysql_test.rb 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} + +@test "mysql-rust" { + start_process cargo build --release --manifest-path $BATS_TEST_DIRNAME/rust/Cargo.toml + start_process $BATS_TEST_DIRNAME/rust/target/release/mysql_test 127.0.0.1 3306 root "" $BATS_TEST_DIRNAME/test.data +} \ No newline at end of file diff --git a/compatibility/mysql/test.data b/compatibility/mysql/test.data new file mode 100644 index 00000000..8093ca70 --- /dev/null +++ b/compatibility/mysql/test.data @@ -0,0 +1,29 @@ +CREATE DATABASE test + +USE test + +CREATE TABLE tb1 (id int, value float, c1 char(10), primary key(id)) + +INSERT INTO tb1 VALUES (1, 1.1, 'a'), (2, 2.2, 'b') + +SELECT * FROM tb1 +1,1.1,a +2,2.2,b + +SELECT * FROM tb1 WHERE id=1 +1,1.1,a + +UPDATE tb1 SET value=3.3 WHERE id=1 + +SELECT * FROM tb1 WHERE id=1 +1,3.3,a + +SELECT COUNT(*) FROM tb1 +2 + +DELETE FROM tb1 WHERE id=1 + +SELECT * FROM tb1 +2,2.2,b + +DROP DATABASE test