| /* |
| * wc-queries-test.c -- test the evaluation of the wc Sqlite queries |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| * ==================================================================== |
| */ |
| |
| #include "svn_pools.h" |
| #include "svn_hash.h" |
| #include "svn_ctype.h" |
| #include "private/svn_dep_compat.h" |
| |
| #include "svn_private_config.h" |
| |
| #include "../svn_test.h" |
| |
| #ifdef SVN_SQLITE_INLINE |
| /* Import the sqlite3 API vtable from sqlite3wrapper.c */ |
| # define SQLITE_OMIT_DEPRECATED |
| # include <sqlite3ext.h> |
| extern const sqlite3_api_routines *const svn_sqlite3__api_funcs; |
| extern int (*const svn_sqlite3__api_initialize)(void); |
| extern int (*const svn_sqlite3__api_config)(int, ...); |
| # define sqlite3_api svn_sqlite3__api_funcs |
| # define sqlite3_initialize svn_sqlite3__api_initialize |
| # define sqlite3_config svn_sqlite3__api_config |
| #else |
| # include <sqlite3.h> |
| #endif |
| |
| #include "../../libsvn_wc/wc-queries.h" |
| |
| WC_QUERIES_SQL_DECLARE_STATEMENTS(wc_queries); |
| WC_QUERIES_SQL_DECLARE_STATEMENT_INFO(wc_query_info); |
| |
| /* The first query after the normal wc queries */ |
| #define STMT_SCHEMA_FIRST STMT_CREATE_SCHEMA |
| |
| #define SQLITE_ERR(x) do \ |
| { \ |
| int sqlite_err__temp = (x); \ |
| if (sqlite_err__temp != SQLITE_OK) \ |
| return svn_error_createf(SVN_ERR_SQLITE_ERROR, \ |
| NULL, "sqlite: %s", \ |
| sqlite3_errmsg(sdb)); \ |
| } while (0) |
| |
| /* Schema creation statements fail during preparing when the table |
| already exists, and must be evaluated before testing the |
| queries. Statements above STMT_SCHEMA_FIRST only need to be |
| included here when they need to be evaluated before testing the |
| statements */ |
| static const int schema_statements[] = |
| { |
| /* Usual tables */ |
| STMT_CREATE_SCHEMA, |
| STMT_INSTALL_SCHEMA_STATISTICS, |
| /* Memory tables */ |
| STMT_CREATE_TARGETS_LIST, |
| STMT_CREATE_CHANGELIST_LIST, |
| STMT_CREATE_CHANGELIST_TRIGGER, |
| STMT_CREATE_TARGET_PROP_CACHE, |
| STMT_CREATE_REVERT_LIST, |
| STMT_CREATE_DELETE_LIST, |
| STMT_CREATE_UPDATE_MOVE_LIST, |
| -1 /* final marker */ |
| }; |
| |
| /* These statements currently trigger warnings. It would be nice if |
| we could annotate these in wc-queries.sql */ |
| static const int slow_statements[] = |
| { |
| /* Operate on the entire WC */ |
| STMT_SELECT_ALL_NODES, /* schema validation code */ |
| |
| /* Updates all records for a repository (designed slow) */ |
| STMT_UPDATE_LOCK_REPOS_ID, |
| |
| /* Full temporary table read */ |
| STMT_INSERT_ACTUAL_EMPTIES, |
| STMT_INSERT_ACTUAL_EMPTIES_FILES, |
| STMT_SELECT_REVERT_LIST_RECURSIVE, |
| STMT_SELECT_DELETE_LIST, |
| STMT_SELECT_UPDATE_MOVE_LIST, |
| STMT_FIND_REPOS_PATH_IN_WC, |
| STMT_SELECT_PRESENT_HIGHEST_WORKING_NODES_BY_BASENAME_AND_KIND, |
| STMT_SELECT_COPIES_OF_REPOS_RELPATH, |
| |
| /* Designed as slow to avoid penalty on other queries */ |
| STMT_SELECT_UNREFERENCED_PRISTINES, |
| |
| /* Slow, but just if foreign keys are enabled: |
| * STMT_DELETE_PRISTINE_IF_UNREFERENCED, |
| */ |
| STMT_HAVE_STAT1_TABLE, /* Queries sqlite_master which has no index */ |
| |
| -1 /* final marker */ |
| }; |
| |
| /* Statements that just read the first record from a table, |
| using the primary key. Specialized as different sqlite |
| versions produce different results */ |
| static const int primary_key_statements[] = |
| { |
| /* Is there a record? ### Can we somehow check for LIMIT 1, |
| and primary key instead of adding a list? */ |
| STMT_LOOK_FOR_WORK, |
| STMT_SELECT_WORK_ITEM, |
| |
| -1 /* final marker */ |
| }; |
| |
| /* Helper function to determine if a statement is in a list */ |
| static svn_boolean_t |
| in_list(const int list[], int stmt_idx) |
| { |
| int i; |
| |
| for (i = 0; list[i] != -1; i++) |
| { |
| if (list[i] == stmt_idx) |
| return TRUE; |
| } |
| return FALSE; |
| } |
| |
| /* Helpers to determine if a statement is in a common list */ |
| #define is_slow_statement(stmt_idx) in_list(slow_statements, stmt_idx) |
| #define is_schema_statement(stmt_idx) \ |
| ((stmt_idx >= STMT_SCHEMA_FIRST) || in_list(schema_statements, stmt_idx)) |
| |
| |
| /* Create an in-memory db for evaluating queries */ |
| static svn_error_t * |
| create_memory_db(sqlite3 **db, |
| apr_pool_t *pool) |
| { |
| sqlite3 *sdb; |
| int i; |
| |
| /* Create an in-memory raw database */ |
| SVN_TEST_ASSERT(sqlite3_initialize() == SQLITE_OK); |
| SQLITE_ERR(sqlite3_open_v2("", &sdb, |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, |
| NULL)); |
| |
| /* Create schema */ |
| for (i = 0; schema_statements[i] != -1; i++) |
| { |
| SQLITE_ERR(sqlite3_exec(sdb, wc_queries[schema_statements[i]], NULL, NULL, NULL)); |
| } |
| |
| *db = sdb; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Verify sqlite3 runtime version */ |
| static svn_error_t * |
| test_sqlite_version(apr_pool_t *scratch_pool) |
| { |
| printf("DBG: Using Sqlite %s\n", sqlite3_libversion()); |
| |
| if (sqlite3_libversion_number() != SQLITE_VERSION_NUMBER) |
| printf("DBG: Compiled against Sqlite %s\n", SQLITE_VERSION); |
| |
| if (sqlite3_libversion_number() < SQLITE_VERSION_NUMBER) |
| return svn_error_createf(SVN_ERR_TEST_FAILED, NULL, |
| "Compiled against Sqlite %s (at runtime we have Sqlite %s)", |
| SQLITE_VERSION, sqlite3_libversion()); |
| |
| #if !SQLITE_VERSION_AT_LEAST(3, 7, 9) |
| return svn_error_create(SVN_ERR_TEST_FAILED, NULL, |
| "Sqlite upgrade recommended:\n" |
| "****************************************************************\n" |
| "* Subversion needs at least SQLite 3.7.9 to work optimally *\n" |
| "* *\n" |
| "* With older versions, at least some queries that are expected *\n" |
| "* to be using an index are not. This makes some operations use *\n" |
| "* every node in the working copy instead of just one. *\n" |
| "* *\n" |
| "* While Subversion works correctly in this case, you may see *\n" |
| "* slowdowns of WELL MORE THAN 1000* in some cases! *\n" |
| "* *\n" |
| "* *\n" |
| "* SQLITE UPGRADE RECOMMENDED *\n" |
| "****************************************************************\n"); |
| #else |
| return SVN_NO_ERROR; |
| #endif |
| } |
| |
| /* Parse all normal queries */ |
| static svn_error_t * |
| test_parsable(apr_pool_t *scratch_pool) |
| { |
| sqlite3 *sdb; |
| int i; |
| |
| SVN_ERR(create_memory_db(&sdb, scratch_pool)); |
| |
| for (i=0; i < STMT_SCHEMA_FIRST; i++) |
| { |
| sqlite3_stmt *stmt; |
| const char *text = wc_queries[i]; |
| |
| if (is_schema_statement(i)) |
| continue; |
| |
| /* Some of our statement texts contain multiple queries. We prepare |
| them all. */ |
| while (*text != '\0') |
| { |
| const char *tail; |
| int r = sqlite3_prepare_v2(sdb, text, -1, &stmt, &tail); |
| |
| if (r != SQLITE_OK) |
| return svn_error_createf(SVN_ERR_SQLITE_ERROR, NULL, |
| "Preparing %s failed: %s\n%s", |
| wc_query_info[i][0], |
| sqlite3_errmsg(sdb), |
| text); |
| |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| |
| /* Continue after the current statement */ |
| text = tail; |
| } |
| } |
| |
| SQLITE_ERR(sqlite3_close(sdb)); /* Close the DB if ok; otherwise leaked */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* Contains a parsed record from EXPLAIN QUERY PLAN */ |
| struct explanation_item |
| { |
| const char *operation; |
| const char *table; |
| const char *alias; |
| svn_boolean_t scan; |
| svn_boolean_t search; |
| svn_boolean_t covered_by_index; |
| svn_boolean_t primary_key; |
| svn_boolean_t automatic_index; |
| const char *index; |
| const char *expressions; |
| const char *expected; |
| |
| const char *compound_left; |
| const char *compound_right; |
| svn_boolean_t create_btree; |
| |
| svn_boolean_t create_union; |
| svn_boolean_t union_all; |
| |
| svn_boolean_t merge; |
| |
| svn_boolean_t multi_index; |
| svn_boolean_t multi_index_or; |
| |
| int expression_vars; |
| int expected_rows; |
| }; |
| |
| #define MATCH_TOKEN(x, y) (x && (strcmp(x, y) == 0)) |
| |
| /* Simple parser for the Sqlite textual explanation into an explanation_item. |
| Writes "DBG:" lines when sqlite produces unexpected results. When no |
| valid explanation_item can be parsed sets *PARSED_ITEM to NULL, otherwise |
| to a valid result. */ |
| static svn_error_t * |
| parse_explanation_item(struct explanation_item **parsed_item, |
| const char *text, |
| apr_pool_t *result_pool) |
| { |
| struct explanation_item *item = apr_pcalloc(result_pool, sizeof(*item)); |
| char *token; |
| char *last; |
| char *tmp = apr_pstrdup(result_pool, text); |
| const char *tmp_end = &tmp[strlen(tmp)]; |
| |
| *parsed_item = NULL; |
| |
| item->operation = apr_strtok(tmp, " ", &last); |
| |
| if (!item->operation) |
| { |
| return SVN_NO_ERROR; |
| } |
| |
| item->scan = MATCH_TOKEN(item->operation, "SCAN"); |
| |
| if (item->scan || MATCH_TOKEN(item->operation, "SEARCH")) |
| { |
| item->search = TRUE; /* Search or scan */ |
| token = apr_strtok(NULL, " ", &last); |
| |
| if (MATCH_TOKEN(token, "TABLE")) |
| { |
| item->table = apr_strtok(NULL, " ", &last); |
| } |
| else if (MATCH_TOKEN(token, "SUBQUERY")) |
| { |
| item->table = apr_psprintf(result_pool, "SUBQUERY-%s", |
| apr_strtok(NULL, " ", &last)); |
| } |
| else if (MATCH_TOKEN(token, "CONSTANT")) |
| { |
| item->table = "sqlite_master"; /* not worth checking. |
| Just a lookup */ |
| token = apr_strtok(NULL, " ", &last); |
| if (!MATCH_TOKEN(token, "ROW")) |
| { |
| printf("DBG: Expected 'ROW', got '%s' in '%s'\n", |
| token, text); |
| return SVN_NO_ERROR; |
| } |
| } |
| else |
| { |
| printf("DBG: Expected 'TABLE', got '%s' in '%s'\n", token, text); |
| return SVN_NO_ERROR; /* Nothing to parse */ |
| } |
| |
| token = apr_strtok(NULL, " ", &last); |
| |
| /* Skip alias */ |
| if (MATCH_TOKEN(token, "AS")) |
| { |
| item->alias = apr_strtok(NULL, " ", &last); |
| token = apr_strtok(NULL, " ", &last); |
| } |
| |
| if (MATCH_TOKEN(token, "USING")) |
| { |
| token = apr_strtok(NULL, " ", &last); |
| |
| if (MATCH_TOKEN(token, "AUTOMATIC")) |
| { |
| /* Pain: A temporary index is created */ |
| item->automatic_index = TRUE; |
| token = apr_strtok(NULL, " ", &last); |
| } |
| |
| /* Handle COVERING */ |
| if (MATCH_TOKEN(token, "COVERING")) |
| { |
| /* Bonus: Query will be answered by just using the index */ |
| item->covered_by_index = TRUE; |
| token = apr_strtok(NULL, " ", &last); |
| } |
| |
| if (MATCH_TOKEN(token, "INDEX")) |
| { |
| item->index = apr_strtok(NULL, " ", &last); |
| } |
| else if (MATCH_TOKEN(token, "INTEGER")) |
| { |
| token = apr_strtok(NULL, " ", &last); |
| if (!MATCH_TOKEN(token, "PRIMARY")) |
| { |
| printf("DBG: Expected 'PRIMARY', got '%s' in '%s'\n", |
| token, text); |
| return SVN_NO_ERROR; |
| } |
| |
| token = apr_strtok(NULL, " ", &last); |
| if (!MATCH_TOKEN(token, "KEY")) |
| { |
| printf("DBG: Expected 'KEY', got '%s' in '%s'\n", |
| token, text); |
| return SVN_NO_ERROR; |
| } |
| |
| item->primary_key = TRUE; |
| } |
| else |
| { |
| printf("DBG: Expected 'INDEX' or 'PRIMARY', got '%s' in '%s'\n", |
| token, text); |
| return SVN_NO_ERROR; |
| } |
| |
| token = apr_strtok(NULL, " ", &last); |
| } |
| |
| if (token && token[0] == '(' && token[1] != '~') |
| { |
| /* Undo the tokenization to switch parser rules */ |
| size_t token_len = strlen(token); |
| |
| if (token + token_len < tmp_end) |
| token[token_len] = ' '; |
| |
| if (token[token_len] == '\0') |
| last[-1] = ' '; |
| |
| token++; /* Skip the '(' */ |
| |
| item->expressions = apr_strtok(token, ")", &last); |
| token = apr_strtok(NULL, " ", &last); |
| } |
| |
| if (token && *token == '(' && token[1] == '~') |
| { |
| /* Undo the tokenization to switch parser rules */ |
| size_t token_len = strlen(token); |
| |
| if (token + token_len < tmp_end) |
| token[token_len] = ' '; |
| |
| if (token[token_len] == '\0') |
| last[-1] = ' '; |
| |
| token += 2; /* Skip "(~" */ |
| |
| item->expected = apr_strtok(token, ")", &last); |
| token = apr_strtok(NULL, " ", &last); |
| } |
| |
| if (token) |
| { |
| printf("DBG: Unexpected token '%s' in '%s'\n", |
| token, text); |
| return SVN_NO_ERROR; |
| } |
| |
| /* Parsing successful */ |
| } |
| else if (MATCH_TOKEN(item->operation, "EXECUTE")) |
| { |
| /* Subquery handling */ |
| return SVN_NO_ERROR; |
| } |
| else if (MATCH_TOKEN(item->operation, "COMPOUND")) |
| { |
| /* Handling temporary table (E.g. UNION) */ |
| |
| token = apr_strtok(NULL, " ", &last); |
| if (MATCH_TOKEN(token, "SUBQUERIES")) |
| { |
| item->compound_left = apr_strtok(NULL, " ", &last); |
| token = apr_strtok(NULL, " ", &last); |
| |
| if (!MATCH_TOKEN(token, "AND")) |
| { |
| printf("DBG: Expected 'AND', got '%s' in '%s'\n", token, text); |
| return SVN_NO_ERROR; |
| } |
| |
| item->compound_right = apr_strtok(NULL, " ", &last); |
| |
| token = apr_strtok(NULL, " ", &last); |
| if (MATCH_TOKEN(token, "USING")) |
| { |
| token = apr_strtok(NULL, " ", &last); |
| if (!MATCH_TOKEN(token, "TEMP")) |
| { |
| printf("DBG: Expected 'TEMP', got '%s' in '%s'\n", token, text); |
| } |
| token = apr_strtok(NULL, " ", &last); |
| if (!MATCH_TOKEN(token, "B-TREE")) |
| { |
| printf("DBG: Expected 'B-TREE', got '%s' in '%s'\n", token, |
| text); |
| } |
| item->create_btree = TRUE; |
| } |
| } |
| else if (MATCH_TOKEN(token, "QUERY")) |
| { |
| } |
| else |
| { |
| printf("DBG: Expected 'SUBQUERIES', got '%s' in '%s'\n", token, |
| text); |
| return SVN_NO_ERROR; |
| } |
| } |
| else if (MATCH_TOKEN(item->operation, "USE")) |
| { |
| /* Using a temporary table for ordering results */ |
| /* ### Need parsing */ |
| item->create_btree = TRUE; |
| } |
| else if (MATCH_TOKEN(item->operation, "UNION")) |
| { |
| item->create_union = TRUE; |
| |
| token = apr_strtok(NULL, " ", &last); |
| if (MATCH_TOKEN(token, "ALL")) |
| item->union_all = TRUE; |
| else |
| item->union_all = FALSE; |
| } |
| else if (MATCH_TOKEN(item->operation, "INDEX")) |
| { |
| |
| } |
| else if (MATCH_TOKEN(item->operation, "MULTI-INDEX")) |
| { |
| item->multi_index = TRUE; |
| token = apr_strtok(NULL, " ", &last); |
| if (MATCH_TOKEN(token, "OR")) |
| item->multi_index_or = TRUE; |
| } |
| else if (MATCH_TOKEN(item->operation, "MERGE")) |
| { |
| item->merge = TRUE; |
| } |
| else if (MATCH_TOKEN(item->operation, "LEFT") |
| || MATCH_TOKEN(item->operation, "RIGHT")) |
| { |
| } |
| else if (MATCH_TOKEN(item->operation, "CORRELATED")) |
| { |
| item->merge = TRUE; |
| } |
| else if (MATCH_TOKEN(item->operation, "CO-ROUTINE")) |
| { |
| } |
| else if (MATCH_TOKEN(item->operation, "SCALAR")) |
| { |
| } |
| else if (MATCH_TOKEN(item->operation, "LEFT-MOST")) |
| { |
| } |
| else |
| { |
| printf("DBG: Unhandled sqlite operation '%s' in explanation\n", item->operation); |
| return SVN_NO_ERROR; |
| } |
| |
| if (item->expressions) |
| { |
| const char *p; |
| |
| for (p = item->expressions; *p; p++) |
| { |
| if (*p == '?') |
| item->expression_vars++; |
| } |
| } |
| if (item->expected) |
| { |
| item->expected_rows = atoi(item->expected); |
| } |
| |
| *parsed_item = item; |
| return SVN_NO_ERROR; |
| } |
| |
| /* Sqlite has an SQLITE_OMIT_EXPLAIN compilation flag, which may make |
| explain query just evaluate the query. Some older versions use a |
| different number of columns (and different texts) for |
| EXPLAIN query plan. |
| |
| If none of this is true set *SUPPORTED to TRUE, otherwise to FALSE */ |
| static svn_error_t * |
| supported_explain_query_plan(svn_boolean_t *supported, |
| sqlite3 *sdb, |
| apr_pool_t *scratch_pool) |
| { |
| sqlite3_stmt *stmt; |
| int r; |
| |
| *supported = TRUE; |
| |
| r = sqlite3_prepare(sdb, "EXPLAIN QUERY PLAN SELECT 1", |
| -1, &stmt, NULL); |
| |
| if (r != SQLITE_OK) |
| { |
| *supported = FALSE; |
| return SVN_NO_ERROR; |
| } |
| |
| if (sqlite3_step(stmt) == SQLITE_ROW) |
| { |
| if (sqlite3_column_count(stmt) < 4) |
| { |
| *supported = FALSE; |
| /* Fall through */ |
| } |
| } |
| |
| SQLITE_ERR(sqlite3_reset(stmt)); |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| return SVN_NO_ERROR; |
| } |
| |
| |
| /* Returns TRUE if TABLE_NAME specifies a nodes table, which should be indexed |
| by wc_id and either local_relpath or parent_relpath */ |
| static svn_boolean_t |
| is_node_table(const char *table_name) |
| { |
| return (apr_strnatcasecmp(table_name, "nodes") == 0 |
| || apr_strnatcasecmp(table_name, "actual_node") == 0 |
| || apr_strnatcasecmp(table_name, "externals") == 0 |
| || apr_strnatcasecmp(table_name, "lock") == 0 |
| || apr_strnatcasecmp(table_name, "wc_lock") == 0 |
| || FALSE); |
| } |
| |
| /* Returns TRUE if TABLE specifies an intermediate result table, which is |
| allowed to have table scans, etc. */ |
| static svn_boolean_t |
| is_result_table(const char *table_name) |
| { |
| return (apr_strnatcasecmp(table_name, "target_prop_cache") == 0 |
| || apr_strnatcasecmp(table_name, "changelist_list") == 0 |
| || FALSE); |
| } |
| |
| static svn_error_t * |
| test_query_expectations(apr_pool_t *scratch_pool) |
| { |
| sqlite3 *sdb; |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| svn_error_t *warnings = NULL; |
| svn_boolean_t supports_query_info; |
| |
| SVN_ERR(create_memory_db(&sdb, scratch_pool)); |
| |
| SVN_ERR(supported_explain_query_plan(&supports_query_info, sdb, |
| scratch_pool)); |
| if (!supports_query_info) |
| { |
| SQLITE_ERR(sqlite3_close(sdb)); |
| return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, |
| "Sqlite doesn't support EXPLAIN QUERY PLAN"); |
| } |
| |
| for (i=0; i < STMT_SCHEMA_FIRST; i++) |
| { |
| sqlite3_stmt *stmt; |
| const char *tail; |
| int r; |
| svn_boolean_t warned = FALSE; |
| apr_array_header_t *rows = NULL; |
| |
| if (is_schema_statement(i)) |
| continue; |
| |
| /* Prepare statement to find if it is a single statement. */ |
| r = sqlite3_prepare_v2(sdb, wc_queries[i], -1, &stmt, &tail); |
| |
| if (r != SQLITE_OK) |
| continue; /* Parse failure is already reported by 'test_parable' */ |
| |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| if (tail[0] != '\0') |
| continue; /* Multi-queries are currently not testable */ |
| |
| svn_pool_clear(iterpool); |
| |
| r = sqlite3_prepare_v2(sdb, |
| apr_pstrcat(iterpool, |
| "EXPLAIN QUERY PLAN ", |
| wc_queries[i], |
| SVN_VA_NULL), |
| -1, &stmt, &tail); |
| |
| if (r != SQLITE_OK) |
| continue; /* EXPLAIN not enabled or doesn't support this query */ |
| |
| while (SQLITE_ROW == (r = sqlite3_step(stmt))) |
| { |
| /*int iSelectid; |
| int iOrder; |
| int iFrom;*/ |
| const unsigned char *zDetail; |
| char *detail; |
| struct explanation_item *item; |
| |
| /* ### The following code is correct for current Sqlite versions |
| ### (tested with 3.7.x), but the EXPLAIN QUERY PLAN output |
| ### is not guaranteed to be stable for future versions. */ |
| |
| /* Names as in Sqlite documentation */ |
| /*iSelectid = sqlite3_column_int(stmt, 0); |
| iOrder = sqlite3_column_int(stmt, 1); |
| iFrom = sqlite3_column_int(stmt, 2);*/ |
| zDetail = sqlite3_column_text(stmt, 3); |
| |
| if (! zDetail) |
| continue; |
| |
| if (!rows) |
| rows = apr_array_make(iterpool, 10, sizeof(const char*)); |
| |
| detail = apr_pstrdup(iterpool, (const char*)zDetail); |
| |
| APR_ARRAY_PUSH(rows, const char *) = detail; |
| |
| SVN_ERR(parse_explanation_item(&item, detail, iterpool)); |
| |
| if (!item) |
| continue; /* Not parsable or not interesting */ |
| |
| if (item->search |
| && item->automatic_index) |
| { |
| warned = TRUE; |
| if (!is_slow_statement(i)) |
| { |
| warnings = svn_error_createf(SVN_ERR_TEST_FAILED, warnings, |
| "%s: " |
| "Creates a temporary index: %s\n", |
| wc_query_info[i][0], wc_queries[i]); |
| } |
| } |
| else if (item->search && item->primary_key) |
| { |
| /* Nice */ |
| } |
| else if (item->search |
| && ((item->expression_vars < 2 && is_node_table(item->table)) |
| || (item->expression_vars < 1)) |
| && !is_result_table(item->table)) |
| { |
| if (MATCH_TOKEN(item->table, "sqlite_master")) |
| { |
| /* The sqlite_master table does not have an index. |
| Query explanations that say 'SCAN TABLE sqlite_master' |
| will appear if SQLite was compiled with the option |
| SQLITE_ENABLE_STMT_SCANSTATUS, for queries such |
| as 'DROP TABLE foo', but the performance of such |
| statements is not our concern here. */ |
| |
| /* "Slow" statements do expect to see a warning, however. */ |
| if (is_slow_statement(i)) |
| warned = TRUE; |
| } |
| else if (in_list(primary_key_statements, i)) |
| { |
| /* Reported as primary key index usage in Sqlite 3.7, |
| as table scan in 3.8+, while the execution plan is |
| identical: read first record from table */ |
| } |
| else if (!is_slow_statement(i)) |
| { |
| warned = TRUE; |
| warnings = svn_error_createf(SVN_ERR_TEST_FAILED, warnings, |
| "%s: " |
| "Uses %s with only %d index component: (%s)\n%s", |
| wc_query_info[i][0], item->table, |
| item->expression_vars, item->expressions, |
| wc_queries[i]); |
| } |
| else |
| warned = TRUE; |
| } |
| else if (item->search && !item->index) |
| { |
| warned = TRUE; |
| if (!is_slow_statement(i)) |
| warnings = svn_error_createf(SVN_ERR_TEST_FAILED, warnings, |
| "%s: " |
| "Query on %s doesn't use an index:\n%s", |
| wc_query_info[i][0], item->table, wc_queries[i]); |
| } |
| else if (item->scan && !is_result_table(item->table)) |
| { |
| warned = TRUE; |
| if (!is_slow_statement(i)) |
| warnings = svn_error_createf(SVN_ERR_TEST_FAILED, warnings, |
| "Query %s: " |
| "Performs scan on %s:\n%s", |
| wc_query_info[i][0], item->table, wc_queries[i]); |
| } |
| else if (item->create_btree) |
| { |
| warned = TRUE; |
| if (!is_slow_statement(i)) |
| warnings = svn_error_createf(SVN_ERR_TEST_FAILED, warnings, |
| "Query %s: Creates a temporary B-TREE:\n%s", |
| wc_query_info[i][0], wc_queries[i]); |
| } |
| } |
| SQLITE_ERR(sqlite3_reset(stmt)); |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| |
| if (!warned && is_slow_statement(i)) |
| { |
| printf("DBG: Expected %s to be reported as slow, but it wasn't\n", |
| wc_query_info[i][0]); |
| } |
| |
| if (rows && warned != is_slow_statement(i)) |
| { |
| int w; |
| svn_error_t *info = NULL; |
| for (w = rows->nelts-1; w >= 0; w--) |
| { |
| if (warned) |
| info = svn_error_createf(SVN_ERR_SQLITE_CONSTRAINT, info, |
| "|%s", APR_ARRAY_IDX(rows, w, |
| const char*)); |
| else |
| printf("|%s\n", APR_ARRAY_IDX(rows, w, const char*)); |
| } |
| |
| warnings = svn_error_compose_create(warnings, info); |
| } |
| } |
| SQLITE_ERR(sqlite3_close(sdb)); /* Close the DB if ok; otherwise leaked */ |
| |
| return warnings; |
| } |
| |
| static svn_error_t * |
| test_query_duplicates(apr_pool_t *scratch_pool) |
| { |
| sqlite3 *sdb; |
| int i; |
| apr_pool_t *iterpool = svn_pool_create(scratch_pool); |
| svn_error_t *warnings = NULL; |
| svn_boolean_t supports_query_info; |
| apr_hash_t *sha_to_query = apr_hash_make(scratch_pool); |
| |
| SVN_ERR(create_memory_db(&sdb, scratch_pool)); |
| |
| SVN_ERR(supported_explain_query_plan(&supports_query_info, sdb, |
| scratch_pool)); |
| if (!supports_query_info) |
| { |
| SQLITE_ERR(sqlite3_close(sdb)); |
| return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, |
| "Sqlite doesn't support EXPLAIN QUERY PLAN"); |
| } |
| |
| for (i = 0; i < STMT_SCHEMA_FIRST; i++) |
| { |
| sqlite3_stmt *stmt; |
| const char *tail; |
| int r; |
| svn_stringbuf_t *result; |
| svn_checksum_t *checksum; |
| |
| if (is_schema_statement(i)) |
| continue; |
| |
| /* Prepare statement to find if it is a single statement. */ |
| r = sqlite3_prepare_v2(sdb, wc_queries[i], -1, &stmt, &tail); |
| |
| if (r != SQLITE_OK) |
| continue; /* Parse failure is already reported by 'test_parable' */ |
| |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| if (tail[0] != '\0') |
| continue; /* Multi-queries are currently not testable */ |
| |
| svn_pool_clear(iterpool); |
| |
| r = sqlite3_prepare_v2(sdb, |
| apr_pstrcat(iterpool, |
| "EXPLAIN ", |
| wc_queries[i], |
| SVN_VA_NULL), |
| -1, &stmt, &tail); |
| |
| if (r != SQLITE_OK) |
| continue; /* EXPLAIN not enabled or doesn't support this query */ |
| |
| result = svn_stringbuf_create_empty(iterpool); |
| |
| while (SQLITE_ROW == (r = sqlite3_step(stmt))) |
| { |
| int col; |
| |
| for (col = 0; col < sqlite3_column_count(stmt); col++) |
| { |
| const char *txt = (const char*)sqlite3_column_text(stmt, col); |
| if (txt) |
| svn_stringbuf_appendcstr(result, txt); |
| |
| svn_stringbuf_appendcstr(result, "|"); |
| } |
| |
| svn_stringbuf_appendcstr(result, "\n"); |
| } |
| |
| SQLITE_ERR(sqlite3_reset(stmt)); |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| |
| SVN_ERR(svn_checksum(&checksum, svn_checksum_sha1, |
| result->data, result->len, |
| iterpool)); |
| |
| { |
| const char *hex = svn_checksum_to_cstring(checksum, scratch_pool); |
| const char *other; |
| |
| other = svn_hash_gets(sha_to_query, hex); |
| if (other) |
| { |
| warnings = svn_error_createf(SVN_ERR_TEST_FAILED, warnings, |
| "Query %s has an identical execution plan as %s", |
| wc_query_info[i][0], other); |
| } |
| else |
| svn_hash_sets(sha_to_query, hex, wc_query_info[i][0]); |
| } |
| } |
| SQLITE_ERR(sqlite3_close(sdb)); /* Close the DB if ok; otherwise leaked */ |
| |
| return warnings; |
| } |
| |
| /* Helper to verify a bit of data in the sqlite3 statistics */ |
| static int |
| parse_stat_data(const char *stat) |
| { |
| int n = 0; |
| apr_int64_t last = APR_INT64_MAX; |
| while (*stat) |
| { |
| apr_int64_t v; |
| char *next; |
| |
| if (*stat < '0' || *stat > '9') |
| return -2; |
| |
| errno = 0; |
| v = apr_strtoi64(stat, &next, 10); |
| |
| /* All numbers specify the average number of rows |
| with the same values in all columns left of it, |
| so the value must be >= 1 and lower than or equal |
| to all previous seen numbers */ |
| if (v <= 0 || (v > last) || (errno != 0)) |
| return -1; |
| |
| last = v; |
| |
| n++; |
| stat = next; |
| |
| if (*stat == ' ') |
| stat++; |
| } |
| |
| return n; |
| } |
| |
| static svn_error_t * |
| test_schema_statistics(apr_pool_t *scratch_pool) |
| { |
| sqlite3 *sdb; |
| sqlite3_stmt *stmt; |
| |
| SVN_ERR(create_memory_db(&sdb, scratch_pool)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "CREATE TABLE shadow_stat1(tbl TEXT, idx TEXT, stat TEXT)", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO shadow_stat1 (tbl, idx, stat) " |
| "SELECT tbl, idx, stat FROM sqlite_stat1", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "DROP TABLE sqlite_stat1", |
| NULL, NULL, NULL)); |
| |
| /* Insert statement to give index at least 1 record */ |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO nodes (wc_id, local_relpath, op_depth," |
| " presence, kind) " |
| "VALUES (1, '', 0, 'normal', 'dir')", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO actual_node (wc_id, local_relpath) " |
| "VALUES (1, '')", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO lock (repos_id, repos_relpath, lock_token) " |
| "VALUES (1, '', '')", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO EXTERNALS (wc_id, local_relpath," |
| " parent_relpath, repos_id," |
| " presence, kind, def_local_relpath," |
| " def_repos_relpath) " |
| "VALUES (1, 'subdir', '', 1, 'normal', 'dir', '', '')", |
| NULL, NULL, NULL)); |
| |
| /* These are currently not necessary for query optimization, but it's better |
| to tell Sqlite how we intend to use this table anyway */ |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO wc_lock (wc_id, local_dir_relpath) " |
| "VALUES (1, '')", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "INSERT INTO WORK_QUEUE (work) " |
| "VALUES ('')", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_exec(sdb, |
| "ANALYZE", |
| NULL, NULL, NULL)); |
| |
| SQLITE_ERR( |
| sqlite3_prepare(sdb, "SELECT s.tbl, s.idx, s.stat, r.stat " |
| "FROM shadow_stat1 s " |
| "LEFT JOIN sqlite_stat1 r ON " |
| "s.tbl=r.tbl and s.idx=r.idx", |
| -1, &stmt, NULL)); |
| |
| while (sqlite3_step(stmt) == SQLITE_ROW) |
| { |
| const char *wc_stat = (const char*)sqlite3_column_text(stmt, 2); |
| const char *sqlite_stat = (const char*)sqlite3_column_text(stmt, 3); |
| |
| if (! sqlite_stat) |
| { |
| return svn_error_createf(SVN_ERR_TEST_FAILED, NULL, |
| "Schema statistic failure:" |
| " Refering to unknown index '%s' on '%s'", |
| sqlite3_column_text(stmt, 1), |
| sqlite3_column_text(stmt, 0)); |
| } |
| |
| if (parse_stat_data(wc_stat) != parse_stat_data(sqlite_stat)) |
| { |
| return svn_error_createf(SVN_ERR_TEST_FAILED, NULL, |
| "Schema statistic failure:" |
| " Column mismatch for '%s' on '%s'", |
| sqlite3_column_text(stmt, 1), |
| sqlite3_column_text(stmt, 0)); |
| } |
| } |
| |
| SQLITE_ERR(sqlite3_reset(stmt)); |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| |
| SQLITE_ERR(sqlite3_close(sdb)); /* Close the DB if ok; otherwise leaked */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| /* An SQLite application defined function that allows SQL queries to |
| use "relpath_depth(local_relpath)". */ |
| static void relpath_depth_sqlite(sqlite3_context* context, |
| int argc, |
| sqlite3_value* values[]) |
| { |
| SVN_ERR_MALFUNCTION_NO_RETURN(); /* STUB! */ |
| } |
| |
| /* Parse all verify/check queries */ |
| static svn_error_t * |
| test_verify_parsable(apr_pool_t *scratch_pool) |
| { |
| sqlite3 *sdb; |
| int i; |
| |
| SVN_ERR(create_memory_db(&sdb, scratch_pool)); |
| |
| SQLITE_ERR(sqlite3_create_function(sdb, "relpath_depth", 1, SQLITE_ANY, NULL, |
| relpath_depth_sqlite, NULL, NULL)); |
| |
| for (i=STMT_VERIFICATION_TRIGGERS; wc_queries[i]; i++) |
| { |
| sqlite3_stmt *stmt; |
| const char *text = wc_queries[i]; |
| |
| /* Some of our statement texts contain multiple queries. We prepare |
| them all. */ |
| while (*text != '\0') |
| { |
| const char *tail; |
| int r = sqlite3_prepare_v2(sdb, text, -1, &stmt, &tail); |
| |
| if (r != SQLITE_OK) |
| return svn_error_createf(SVN_ERR_SQLITE_ERROR, NULL, |
| "Preparing %s failed: %s\n%s", |
| wc_query_info[i][0], |
| sqlite3_errmsg(sdb), |
| text); |
| |
| SQLITE_ERR(sqlite3_finalize(stmt)); |
| |
| /* Continue after the current statement */ |
| text = tail; |
| } |
| } |
| |
| SQLITE_ERR(sqlite3_close(sdb)); /* Close the DB if ok; otherwise leaked */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| static int max_threads = 1; |
| |
| static struct svn_test_descriptor_t test_funcs[] = |
| { |
| SVN_TEST_NULL, |
| SVN_TEST_PASS2(test_sqlite_version, |
| "sqlite up-to-date"), |
| SVN_TEST_PASS2(test_parsable, |
| "queries are parsable"), |
| SVN_TEST_PASS2(test_query_expectations, |
| "test query expectations"), |
| SVN_TEST_PASS2(test_query_duplicates, |
| "test query duplicates"), |
| SVN_TEST_PASS2(test_schema_statistics, |
| "test schema statistics"), |
| SVN_TEST_PASS2(test_verify_parsable, |
| "verify queries are parsable"), |
| SVN_TEST_NULL |
| }; |
| |
| SVN_TEST_MAIN |