/** * @file genai_embedding_rerank-t.cpp * @brief TAP test for the GenAI embedding and reranking functionality * * This test verifies the GenAI (Generative AI) module's core functionality: * - Embedding generation (single and batch) * - Reranking documents by relevance * - JSON query processing * - Error handling for malformed queries * - Timeout and error handling * * Note: These tests require a running GenAI service (llama-server or compatible) * at the configured endpoints. The tests use the GENAI: query syntax which * allows autonomous JSON query processing. * * @date 2025-01-09 */ #include #include #include #include #include #include #include #include "mysql.h" #include "mysqld_error.h" #include "tap.h" #include "command_line.h" #include "utils.h" using std::string; // ============================================================================ // Helper Functions // ============================================================================ /** * @brief Check if the GenAI module is initialized * * @param admin MySQL connection to admin interface * @return true if GenAI module is initialized, false otherwise */ bool check_genai_initialized(MYSQL* admin) { MYSQL_QUERY(admin, "SELECT @@genai-threads"); MYSQL_RES* res = mysql_store_result(admin); if (!res) { return false; } int num_rows = mysql_num_rows(res); mysql_free_result(res); // If we get a result, the GenAI module is loaded and initialized return num_rows == 1; } /** * @brief Execute a GENAI: query and check if it returns a result set * * @param client MySQL connection to client interface * @param json_query The JSON query to send (without GENAI: prefix) * @param expected_rows Expected number of rows (or -1 for any) * @return true if query succeeded, false otherwise */ bool execute_genai_query(MYSQL* client, const string& json_query, int expected_rows = -1) { string full_query = "GENAI: " + json_query; int rc = mysql_query(client, full_query.c_str()); if (rc != 0) { diag("Query failed: %s", mysql_error(client)); return false; } MYSQL_RES* res = mysql_store_result(client); if (!res) { diag("No result set returned"); return false; } int num_rows = mysql_num_rows(res); mysql_free_result(res); if (expected_rows >= 0 && num_rows != expected_rows) { diag("Expected %d rows, got %d", expected_rows, num_rows); return false; } return true; } /** * @brief Execute a GENAI: query and expect an error * * @param client MySQL connection to client interface * @param json_query The JSON query to send (without GENAI: prefix) * @return true if query returned an error as expected, false otherwise */ bool execute_genai_query_expect_error(MYSQL* client, const string& json_query) { string full_query = "GENAI: " + json_query; int rc = mysql_query(client, full_query.c_str()); // Query should either fail or return an error result set if (rc != 0) { // Query failed at MySQL level - this is expected for errors return true; } MYSQL_RES* res = mysql_store_result(client); if (!res) { // No result set - error condition return true; } // Check if result set contains an error message int num_fields = mysql_num_fields(res); bool has_error = false; if (num_fields >= 1) { MYSQL_ROW row = mysql_fetch_row(res); if (row && row[0]) { // Check if the first column contains "error" if (strstr(row[0], "\"error\"") || strstr(row[0], "error")) { has_error = true; } } } mysql_free_result(res); return has_error; } /** * @brief Test embedding a single document * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_single_embedding(MYSQL* client) { int test_count = 0; diag("Testing single document embedding"); // Test 1: Valid single document embedding string json = R"({"type": "embed", "documents": ["Hello, world!"]})"; ok(execute_genai_query(client, json, 1), "Single document embedding returns 1 row"); test_count++; // Test 2: Embedding with special characters json = R"({"type": "embed", "documents": ["Test with quotes \" and 'apostrophes'"]})"; ok(execute_genai_query(client, json, 1), "Embedding with special characters returns 1 row"); test_count++; // Test 3: Embedding with unicode json = R"({"type": "embed", "documents": ["Unicode test: 你好世界 🌍"]})"; ok(execute_genai_query(client, json, 1), "Embedding with unicode returns 1 row"); test_count++; return test_count; } /** * @brief Test embedding multiple documents (batch) * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_batch_embedding(MYSQL* client) { int test_count = 0; diag("Testing batch document embedding"); // Test 1: Batch embedding with 3 documents string json = R"({"type": "embed", "documents": ["First document", "Second document", "Third document"]})"; ok(execute_genai_query(client, json, 3), "Batch embedding with 3 documents returns 3 rows"); test_count++; // Test 2: Batch embedding with 5 documents json = R"({"type": "embed", "documents": ["doc1", "doc2", "doc3", "doc4", "doc5"]})"; ok(execute_genai_query(client, json, 5), "Batch embedding with 5 documents returns 5 rows"); test_count++; // Test 3: Batch embedding with empty document (edge case) json = R"({"type": "embed", "documents": [""]})"; ok(execute_genai_query(client, json, 1), "Batch embedding with empty document returns 1 row"); test_count++; return test_count; } /** * @brief Test embedding error handling * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_embedding_errors(MYSQL* client) { int test_count = 0; diag("Testing embedding error handling"); // Test 1: Missing documents array string json = R"({"type": "embed"})"; ok(execute_genai_query_expect_error(client, json), "Embedding without documents array returns error"); test_count++; // Test 2: Empty documents array json = R"({"type": "embed", "documents": []})"; ok(execute_genai_query_expect_error(client, json), "Embedding with empty documents array returns error"); test_count++; // Test 3: Invalid JSON json = R"({"type": "embed", "documents": [)"; ok(execute_genai_query_expect_error(client, json), "Embedding with invalid JSON returns error"); test_count++; // Test 4: Documents is not an array json = R"({"type": "embed", "documents": "not an array"})"; ok(execute_genai_query_expect_error(client, json), "Embedding with non-array documents returns error"); test_count++; return test_count; } /** * @brief Test basic reranking functionality * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_basic_rerank(MYSQL* client) { int test_count = 0; diag("Testing basic reranking"); // Test 1: Simple rerank with 3 documents string json = R"({ "type": "rerank", "query": "What is machine learning?", "documents": [ "Machine learning is a subset of artificial intelligence.", "The capital of France is Paris.", "Deep learning uses neural networks with multiple layers." ] })"; ok(execute_genai_query(client, json, 3), "Rerank with 3 documents returns 3 rows"); test_count++; // Test 2: Rerank with query containing quotes json = R"({ "type": "rerank", "query": "What is \"SQL\" injection?", "documents": [ "SQL injection is a code vulnerability.", "ProxySQL is a database proxy." ] })"; ok(execute_genai_query(client, json, 2), "Rerank with quoted query returns 2 rows"); test_count++; return test_count; } /** * @brief Test rerank with top_n parameter * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_rerank_top_n(MYSQL* client) { int test_count = 0; diag("Testing rerank with top_n parameter"); // Test 1: top_n = 2 with 5 documents string json = R"({ "type": "rerank", "query": "database systems", "documents": [ "ProxySQL is a proxy for MySQL.", "PostgreSQL is an object-relational database.", "Redis is an in-memory data store.", "Elasticsearch is a search engine.", "MongoDB is a NoSQL database." ], "top_n": 2 })"; ok(execute_genai_query(client, json, 2), "Rerank with top_n=2 returns exactly 2 rows"); test_count++; // Test 2: top_n = 1 with 3 documents json = R"({ "type": "rerank", "query": "best fruit", "documents": ["Apple", "Banana", "Orange"], "top_n": 1 })"; ok(execute_genai_query(client, json, 1), "Rerank with top_n=1 returns exactly 1 row"); test_count++; // Test 3: top_n = 0 should return all results json = R"({ "type": "rerank", "query": "test query", "documents": ["doc1", "doc2", "doc3"], "top_n": 0 })"; ok(execute_genai_query(client, json, 3), "Rerank with top_n=0 returns all 3 rows"); test_count++; return test_count; } /** * @brief Test rerank with columns parameter * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_rerank_columns(MYSQL* client) { int test_count = 0; diag("Testing rerank with columns parameter"); // Test 1: columns = 2 (index and score only) string json = R"({ "type": "rerank", "query": "test query", "documents": ["doc1", "doc2"], "columns": 2 })"; ok(execute_genai_query(client, json, 2), "Rerank with columns=2 returns 2 rows"); test_count++; // Test 2: columns = 3 (index, score, document) - default json = R"({ "type": "rerank", "query": "test query", "documents": ["doc1", "doc2"], "columns": 3 })"; ok(execute_genai_query(client, json, 2), "Rerank with columns=3 returns 2 rows"); test_count++; // Test 3: Invalid columns value should return error json = R"({ "type": "rerank", "query": "test query", "documents": ["doc1"], "columns": 5 })"; ok(execute_genai_query_expect_error(client, json), "Rerank with invalid columns=5 returns error"); test_count++; return test_count; } /** * @brief Test rerank error handling * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_rerank_errors(MYSQL* client) { int test_count = 0; diag("Testing rerank error handling"); // Test 1: Missing query string json = R"({"type": "rerank", "documents": ["doc1"]})"; ok(execute_genai_query_expect_error(client, json), "Rerank without query returns error"); test_count++; // Test 2: Empty query json = R"({"type": "rerank", "query": "", "documents": ["doc1"]})"; ok(execute_genai_query_expect_error(client, json), "Rerank with empty query returns error"); test_count++; // Test 3: Missing documents array json = R"({"type": "rerank", "query": "test"})"; ok(execute_genai_query_expect_error(client, json), "Rerank without documents returns error"); test_count++; // Test 4: Empty documents array json = R"({"type": "rerank", "query": "test", "documents": []})"; ok(execute_genai_query_expect_error(client, json), "Rerank with empty documents returns error"); test_count++; return test_count; } /** * @brief Test general JSON query error handling * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_json_query_errors(MYSQL* client) { int test_count = 0; diag("Testing JSON query error handling"); // Test 1: Missing type field string json = R"({"documents": ["doc1"]})"; ok(execute_genai_query_expect_error(client, json), "Query without type field returns error"); test_count++; // Test 2: Unknown operation type json = R"({"type": "unknown_op", "documents": ["doc1"]})"; ok(execute_genai_query_expect_error(client, json), "Query with unknown type returns error"); test_count++; // Test 3: Completely invalid JSON json = R"({invalid json})"; ok(execute_genai_query_expect_error(client, json), "Invalid JSON returns error"); test_count++; // Test 4: Empty JSON object json = R"({})"; ok(execute_genai_query_expect_error(client, json), "Empty JSON object returns error"); test_count++; // Test 5: Query is not an object json = R"(["array", "not", "object"])"; ok(execute_genai_query_expect_error(client, json), "JSON array (not object) returns error"); test_count++; return test_count; } /** * @brief Test GENAI: query syntax variations * * @param client MySQL connection to client interface * @return Number of tests performed */ int test_genai_syntax(MYSQL* client) { int test_count = 0; diag("Testing GENAI: query syntax variations"); // Test 1: GENAI: with leading space should FAIL (not recognized) int rc = mysql_query(client, " GENAI: {\"type\": \"embed\", \"documents\": [\"test\"]}"); ok(rc != 0, "GENAI: with leading space is rejected"); test_count++; // Test 2: Empty query after GENAI: rc = mysql_query(client, "GENAI: "); MYSQL_RES* res = mysql_store_result(client); // Should either fail or have no rows bool empty_query_ok = (rc != 0) || (res && mysql_num_rows(res) == 0); if (res) mysql_free_result(res); ok(empty_query_ok, "Empty GENAI: query handled correctly"); test_count++; // Test 3: Case sensitivity - lowercase should also work rc = mysql_query(client, "genai: {\"type\": \"embed\", \"documents\": [\"test\"]}"); ok(rc == 0, "Lowercase 'genai:' works"); test_count++; return test_count; } /** * @brief Test GenAI configuration variables * * @param admin MySQL connection to admin interface * @return Number of tests performed */ int test_genai_configuration(MYSQL* admin) { int test_count = 0; diag("Testing GenAI configuration variables"); // Test 1: Check genai-threads variable MYSQL_QUERY(admin, "SELECT @@genai-threads"); MYSQL_RES* res = mysql_store_result(admin); ok(res != NULL, "genai-threads variable is accessible"); if (res) { int num_rows = mysql_num_rows(res); ok(num_rows == 1, "genai-threads returns 1 row"); test_count++; mysql_free_result(res); } else { skip(1, "Cannot check row count"); test_count++; } test_count++; // Test 2: Check genai-embedding_uri variable MYSQL_QUERY(admin, "SELECT @@genai-embedding_uri"); res = mysql_store_result(admin); ok(res != NULL, "genai-embedding_uri variable is accessible"); if (res) { MYSQL_ROW row = mysql_fetch_row(res); ok(row && row[0] && strlen(row[0]) > 0, "genai-embedding_uri has a value"); test_count++; mysql_free_result(res); } else { skip(1, "Cannot check value"); test_count++; } test_count++; // Test 3: Check genai-rerank_uri variable MYSQL_QUERY(admin, "SELECT @@genai-rerank_uri"); res = mysql_store_result(admin); ok(res != NULL, "genai-rerank_uri variable is accessible"); if (res) { MYSQL_ROW row = mysql_fetch_row(res); ok(row && row[0] && strlen(row[0]) > 0, "genai-rerank_uri has a value"); test_count++; mysql_free_result(res); } else { skip(1, "Cannot check value"); test_count++; } test_count++; // Test 4: Check genai-embedding_timeout_ms variable MYSQL_QUERY(admin, "SELECT @@genai-embedding_timeout_ms"); res = mysql_store_result(admin); ok(res != NULL, "genai-embedding_timeout_ms variable is accessible"); if (res) { MYSQL_ROW row = mysql_fetch_row(res); ok(row && row[0] && atoi(row[0]) > 0, "genai-embedding_timeout_ms has a positive value"); test_count++; mysql_free_result(res); } else { skip(1, "Cannot check value"); test_count++; } test_count++; // Test 5: Check genai-rerank_timeout_ms variable MYSQL_QUERY(admin, "SELECT @@genai-rerank_timeout_ms"); res = mysql_store_result(admin); ok(res != NULL, "genai-rerank_timeout_ms variable is accessible"); if (res) { MYSQL_ROW row = mysql_fetch_row(res); ok(row && row[0] && atoi(row[0]) > 0, "genai-rerank_timeout_ms has a positive value"); test_count++; mysql_free_result(res); } else { skip(1, "Cannot check value"); test_count++; } test_count++; return test_count; } // ============================================================================ // Main Test Function // ============================================================================ int main() { CommandLine cl; if (cl.getEnv()) { diag("Failed to get the required environmental variables."); return EXIT_FAILURE; } // Initialize connections MYSQL* admin = mysql_init(NULL); if (!admin) { fprintf(stderr, "File %s, line %d, Error: mysql_init failed\n", __FILE__, __LINE__); return EXIT_FAILURE; } if (!mysql_real_connect(admin, cl.admin_host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); mysql_close(admin); return EXIT_FAILURE; } diag("Connected to ProxySQL admin interface at %s:%d", cl.admin_host, cl.admin_port); MYSQL* client = mysql_init(NULL); if (!client) { fprintf(stderr, "File %s, line %d, Error: mysql_init failed\n", __FILE__, __LINE__); mysql_close(admin); return EXIT_FAILURE; } if (!mysql_real_connect(client, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(client)); mysql_close(admin); mysql_close(client); return EXIT_FAILURE; } diag("Connected to ProxySQL client interface at %s:%d", cl.host, cl.port); // Check if GenAI module is initialized if (!check_genai_initialized(admin)) { diag("GenAI module is not initialized. Skipping all tests."); plan(1); skip(1, "GenAI module not initialized"); mysql_close(admin); mysql_close(client); return exit_status(); } diag("GenAI module is initialized. Proceeding with tests."); // Calculate total tests // Configuration tests: 10 tests (5 vars × 2 tests each) // Single embedding: 3 tests // Batch embedding: 3 tests // Embedding errors: 4 tests // Basic rerank: 2 tests // Rerank top_n: 3 tests // Rerank columns: 3 tests // Rerank errors: 4 tests // JSON query errors: 5 tests // GENAI syntax: 3 tests int total_tests = 10 + 3 + 3 + 4 + 2 + 3 + 3 + 4 + 5 + 3; plan(total_tests); int test_count = 0; // ============================================================================ // Part 1: Test GenAI configuration // ============================================================================ diag("=== Part 1: Testing GenAI configuration ==="); test_count += test_genai_configuration(admin); // ============================================================================ // Part 2: Test single document embedding // ============================================================================ diag("=== Part 2: Testing single document embedding ==="); test_count += test_single_embedding(client); // ============================================================================ // Part 3: Test batch embedding // ============================================================================ diag("=== Part 3: Testing batch embedding ==="); test_count += test_batch_embedding(client); // ============================================================================ // Part 4: Test embedding error handling // ============================================================================ diag("=== Part 4: Testing embedding error handling ==="); test_count += test_embedding_errors(client); // ============================================================================ // Part 5: Test basic reranking // ============================================================================ diag("=== Part 5: Testing basic reranking ==="); test_count += test_basic_rerank(client); // ============================================================================ // Part 6: Test rerank with top_n parameter // ============================================================================ diag("=== Part 6: Testing rerank with top_n parameter ==="); test_count += test_rerank_top_n(client); // ============================================================================ // Part 7: Test rerank with columns parameter // ============================================================================ diag("=== Part 7: Testing rerank with columns parameter ==="); test_count += test_rerank_columns(client); // ============================================================================ // Part 8: Test rerank error handling // ============================================================================ diag("=== Part 8: Testing rerank error handling ==="); test_count += test_rerank_errors(client); // ============================================================================ // Part 9: Test JSON query error handling // ============================================================================ diag("=== Part 9: Testing JSON query error handling ==="); test_count += test_json_query_errors(client); // ============================================================================ // Part 10: Test GENAI: query syntax variations // ============================================================================ diag("=== Part 10: Testing GENAI: query syntax variations ==="); test_count += test_genai_syntax(client); // ============================================================================ // Cleanup // ============================================================================ mysql_close(admin); mysql_close(client); diag("=== All GenAI embedding and reranking tests completed ==="); return exit_status(); }