-
Notifications
You must be signed in to change notification settings - Fork 5.4k
sql: create common SQL parsing library to be used by mysql and postgres #11368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ba888f1
Create common SQL library to be used by SQL filters.
cpakulski c4f5a6a
Added unit tests to sqlutils library.
cpakulski 5dcc004
Small corrections to code comments.
cpakulski 6f09ee9
Updated CODEOWNERS file.
cpakulski 505d007
Clang-tidy changes.
cpakulski e83cdf4
Merge branch 'master' into issue/11320
cpakulski c1a3aa6
Merge branch 'master' into issue/11320
cpakulski 119c517
Changed unit tests to verify uppercase and lowercse versions of SQL
cpakulski 087508c
Merge branch 'master' into issue/11320
cpakulski 1bc2356
Corrected format in BUILD files.
cpakulski 9314508
Changed test routines to use abseil utilities for string manipulation.
cpakulski ff93e52
Moved sql-parser library to new revision.
cpakulski a80b087
Removed obsolete comment.
cpakulski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| load( | ||
| "//bazel:envoy_build_system.bzl", | ||
| "envoy_cc_library", | ||
| "envoy_package", | ||
| ) | ||
|
|
||
| licenses(["notice"]) # Apache 2 | ||
|
|
||
| envoy_package() | ||
|
|
||
| envoy_cc_library( | ||
| name = "sqlutils_lib", | ||
| srcs = ["sqlutils.cc"], | ||
| hdrs = ["sqlutils.h"], | ||
| external_deps = ["sqlparser"], | ||
| deps = [ | ||
| "//source/common/protobuf:utility_lib", | ||
| ], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| #include "extensions/common/sqlutils/sqlutils.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace Common { | ||
| namespace SQLUtils { | ||
|
|
||
| bool SQLUtils::setMetadata(const std::string& query, ProtobufWkt::Struct& metadata) { | ||
| hsql::SQLParserResult result; | ||
| hsql::SQLParser::parse(query, &result); | ||
|
|
||
| if (!result.isValid()) { | ||
| return false; | ||
| } | ||
|
|
||
| auto& fields = *metadata.mutable_fields(); | ||
|
|
||
| for (auto i = 0u; i < result.size(); ++i) { | ||
| if (result.getStatement(i)->type() == hsql::StatementType::kStmtShow) { | ||
| continue; | ||
| } | ||
| hsql::TableAccessMap table_access_map; | ||
| // Get names of accessed tables. | ||
| result.getStatement(i)->tablesAccessed(table_access_map); | ||
| for (auto& it : table_access_map) { | ||
| auto& operations = *fields[it.first].mutable_list_value(); | ||
| // For each table get names of operations performed on that table. | ||
| for (const auto& ot : it.second) { | ||
| operations.add_values()->set_string_value(ot); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| } // namespace SQLUtils | ||
| } // namespace Common | ||
| } // namespace Extensions | ||
| } // namespace Envoy |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #include "common/protobuf/utility.h" | ||
|
|
||
| #include "include/sqlparser/SQLParser.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace Common { | ||
| namespace SQLUtils { | ||
|
|
||
| class SQLUtils { | ||
| public: | ||
| /** | ||
| * Method parses SQL query string and writes output to metadata. | ||
| * @param query supplies SQL statement. | ||
| * @param metadata supplies placeholder where metadata should be written. | ||
| * @return True if parsing was successful and False if parsing failed. | ||
| * If True was returned the metadata contains result of parsing. The results are | ||
| * stored in metadata.mutable_fields. | ||
| **/ | ||
| static bool setMetadata(const std::string& query, ProtobufWkt::Struct& metadata); | ||
| }; | ||
|
|
||
| } // namespace SQLUtils | ||
| } // namespace Common | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| load( | ||
| "//bazel:envoy_build_system.bzl", | ||
| "envoy_cc_test", | ||
| "envoy_package", | ||
| ) | ||
|
|
||
| licenses(["notice"]) # Apache 2 | ||
|
|
||
| envoy_package() | ||
|
|
||
| envoy_cc_test( | ||
| name = "sqlutils_tests", | ||
| srcs = [ | ||
| "sqlutils_test.cc", | ||
| ], | ||
| external_deps = ["sqlparser"], | ||
| deps = [ | ||
| "//source/extensions/common/sqlutils:sqlutils_lib", | ||
| ], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| #include "extensions/common/sqlutils/sqlutils.h" | ||
|
|
||
| #include "gtest/gtest.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace Common { | ||
| namespace SQLUtils { | ||
|
|
||
| // MetadataFromSQLTest class is used for parameterized tests. | ||
| // The values in the tests are: | ||
| // std::string - SQL query | ||
| // bool - whether to expect SQL parsing to be successful | ||
| // std::map<std::string, std::list<std::string>> map of expected tables accessed based on the query. | ||
| // The map is checked only when parsing was successful. Map is indexed by table name and points to | ||
| // list of operations performed on the table. For example table1: "select", "insert" says that there | ||
| // was SELECT and INSERT operations on table1. | ||
| class MetadataFromSQLTest | ||
| : public ::testing::TestWithParam< | ||
| std::tuple<std::string, bool, std::map<std::string, std::list<std::string>>>> {}; | ||
|
|
||
| // Test takes SQL query as a parameter and checks if the parsing | ||
| // produces the correct metadata. | ||
| // Metadata is 2-level structure. First layer is list of resources | ||
| // over which the SQL query operates: in our case is list of tables. | ||
| // Under each table there is secondary list which contains operations performed | ||
| // on the table, like "select", "insert", etc. | ||
| TEST_P(MetadataFromSQLTest, ParsingAndMetadataTest) { | ||
| // Get the SQL query | ||
| const std::string& query = std::get<0>(GetParam()); | ||
| // vector of queries to check. | ||
| std::vector<std::string> test_queries; | ||
| test_queries.push_back(query); | ||
|
|
||
| // Create uppercase and lowercase versions of the queries and put | ||
| // them into vector of queries to check | ||
| test_queries.push_back(absl::AsciiStrToLower(query)); | ||
| test_queries.push_back(absl::AsciiStrToUpper(query)); | ||
|
|
||
| while (!test_queries.empty()) { | ||
| std::string test_query = test_queries.back(); | ||
| ProtobufWkt::Struct metadata; | ||
|
|
||
| // Check if the parsing result is what expected. | ||
| ASSERT_EQ(std::get<1>(GetParam()), SQLUtils::setMetadata(test_query, metadata)); | ||
|
|
||
| // If parsing was expected to fail do not check parsing values. | ||
| if (!std::get<1>(GetParam())) { | ||
| return; | ||
| } | ||
|
|
||
| // Access metadata fields, where parsing results are stored. | ||
| auto& fields = *metadata.mutable_fields(); | ||
|
|
||
| // Get the names of resources which SQL query operates on. | ||
| std::map<std::string, std::list<std::string>> expected_tables = std::get<2>(GetParam()); | ||
| // Check if query results return the same number of resources as expected. | ||
| ASSERT_EQ(expected_tables.size(), fields.size()); | ||
| for (const auto& i : fields) { | ||
| // Get from created metadata the list of operations on the resource | ||
| const auto& operations = i; | ||
| std::string table_name = operations.first; | ||
|
|
||
| std::transform(table_name.begin(), table_name.end(), table_name.begin(), | ||
| [](unsigned char c) { return std::tolower(c); }); | ||
| // Get the list of expected operations on the same resource from test param. | ||
| const auto& table_name_it = expected_tables.find(table_name); | ||
| // Make sure that a resource (table) found in metadata is expected. | ||
| ASSERT_NE(expected_tables.end(), table_name_it); | ||
| auto& operations_list = table_name_it->second; | ||
| // The number of expected operations and created in metadata must be the same. | ||
| ASSERT_EQ(operations_list.size(), operations.second.list_value().values().size()); | ||
| // Now iterate over the operations list found in metadata and check if the same operation | ||
| // is listed as expected in test param. | ||
| for (const auto& j : operations.second.list_value().values()) { | ||
| // Find that operation in test params. | ||
| const auto operation_it = | ||
| std::find(operations_list.begin(), operations_list.end(), j.string_value()); | ||
| ASSERT_NE(operations_list.end(), operation_it); | ||
| // Erase the operation. At the end of the test this list should be empty what means | ||
| // that we found all expected operations. | ||
| operations_list.erase(operation_it); | ||
| } | ||
| // Make sure that we went through all expected operations. | ||
| ASSERT_TRUE(operations_list.empty()); | ||
| // Remove the table from the list. At the end of the test this list must be empty. | ||
| expected_tables.erase(table_name_it); | ||
| } | ||
|
|
||
| ASSERT_TRUE(expected_tables.empty()); | ||
| test_queries.pop_back(); | ||
| } | ||
| } | ||
|
|
||
| // Note: This parameterized test's queries are converted to all lowercase and all uppercase | ||
| // to validate that parser is case-insensitive. The test routine converts to uppercase and | ||
| // lowercase entire query string, not only SQL keywords. This introduces a problem when comparing | ||
| // tables' names when verifying parsing result. Therefore the test converts table names to lowercase | ||
| // before comparing. It however requires that all table names in the queries below use lowercase | ||
| // only. | ||
| #define TEST_VALUE(...) \ | ||
| std::tuple<std::string, bool, std::map<std::string, std::list<std::string>>> { __VA_ARGS__ } | ||
| INSTANTIATE_TEST_SUITE_P( | ||
| SQLUtilsTestSuite, MetadataFromSQLTest, | ||
| ::testing::Values( | ||
| TEST_VALUE("blahblah;", false, {}), | ||
|
|
||
| TEST_VALUE("CREATE TABLE IF NOT EXISTS table1(Usr VARCHAR(40),Count INT);", true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we check for all lowercased queries?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed testing routine to convert queries to lowercase and uppercase, so all different variants are tested. |
||
| {{"table1", {"create"}}}), | ||
| TEST_VALUE("CREATE TABLE IF NOT EXISTS `table number 1`(Usr VARCHAR(40),Count INT);", true, | ||
| {{"table number 1", {"create"}}}), | ||
| TEST_VALUE( | ||
| "CREATE TABLE IF NOT EXISTS table1(Usr VARCHAR(40),Count INT); SELECT * from table1;", | ||
| true, {{"table1", {"select", "create"}}}), | ||
| TEST_VALUE( | ||
| "CREATE TABLE IF NOT EXISTS table1(Usr VARCHAR(40),Count INT); SELECT * from table2;", | ||
| true, {{"table1", {"create"}}, {"table2", {"select"}}}), | ||
|
|
||
| TEST_VALUE("CREATE TABLE table1(Usr VARCHAR(40),Count INT);", true, | ||
| {{"table1", {"create"}}}), | ||
| TEST_VALUE("CREATE TABLE;", false, {}), | ||
| TEST_VALUE("CREATE TEMPORARY table table1(Usr VARCHAR(40),Count INT);", true, | ||
| {{"table1", {"create"}}}), | ||
| TEST_VALUE("DROP TABLE IF EXISTS table1", true, {{"table1", {"drop"}}}), | ||
| TEST_VALUE("ALTER TABLE table1 add column Id varchar (20);", true, {{"table1", {"alter"}}}), | ||
| TEST_VALUE("INSERT INTO table1 (Usr, Count) VALUES ('allsp2', 3);", true, | ||
| {{"table1", {"insert"}}}), | ||
| TEST_VALUE("INSERT LOW_PRIORITY INTO table1 (Usr, Count) VALUES ('allsp2', 3);", true, | ||
| {{"table1", {"insert"}}}), | ||
| TEST_VALUE("INSERT IGNORE INTO table1 (Usr, Count) VALUES ('allsp2', 3);", true, | ||
| {{"table1", {"insert"}}}), | ||
| TEST_VALUE("INSERT INTO table1 (Usr, Count) VALUES ('allsp2', 3);SELECT * from table1", | ||
| true, {{"table1", {"insert", "select"}}}), | ||
| TEST_VALUE("DELETE FROM table1 WHERE Count > 3;", true, {{"table1", {"delete"}}}), | ||
| TEST_VALUE("DELETE LOW_PRIORITY FROM table1 WHERE Count > 3;", true, | ||
| {{"table1", {"delete"}}}), | ||
| TEST_VALUE("DELETE QUICK FROM table1 WHERE Count > 3;", true, {{"table1", {"delete"}}}), | ||
| TEST_VALUE("DELETE IGNORE FROM table1 WHERE Count > 3;", true, {{"table1", {"delete"}}}), | ||
|
|
||
| TEST_VALUE("SELECT * FROM table1 WHERE Count = 1;", true, {{"table1", {"select"}}}), | ||
| TEST_VALUE("SELECT * FROM table1 WHERE Count = 1;", true, {{"table1", {"select"}}}), | ||
| TEST_VALUE("SELECT product.category FROM table1 WHERE Count = 1;", true, | ||
| {{"table1", {"select"}}, {"product", {"unknown"}}}), | ||
| TEST_VALUE("SELECT DISTINCT Usr FROM table1;", true, {{"table1", {"select"}}}), | ||
| TEST_VALUE("SELECT Usr, Count FROM table1 ORDER BY Count DESC;", true, | ||
| {{"table1", {"select"}}}), | ||
| TEST_VALUE("SELECT 12 AS a, a FROM table1 GROUP BY a;", true, {{"table1", {"select"}}}), | ||
| TEST_VALUE("SELECT;", false, {}), TEST_VALUE("SELECT Usr, Count FROM;", false, {}), | ||
| TEST_VALUE("INSERT INTO table1 SELECT * FROM table2;", true, | ||
| {{"table1", {"insert"}}, {"table2", {"select"}}}), | ||
| TEST_VALUE("INSERT INTO table1 SELECT tbl_temp1.fld_order_id FROM table2;", true, | ||
| {{"tbl_temp1", {"unknown"}}, {"table2", {"select"}}, {"table1", {"insert"}}}), | ||
| TEST_VALUE("UPDATE table1 SET col1 = col1 + 1", true, {{"table1", {"update"}}}), | ||
| TEST_VALUE("UPDATE LOW_PRIORITY table1 SET col1 = col1 + 1", true, | ||
| {{"table1", {"update"}}}), | ||
| TEST_VALUE("UPDATE IGNORE table1 SET col1 = col1 + 1", true, {{"table1", {"update"}}}), | ||
| TEST_VALUE("UPDATE table1 SET column1=(SELECT * columnX from table2);", true, | ||
| {{"table1", {"update"}}, {"table2", {"select"}}}), | ||
|
|
||
| // operations on database should not create any metadata | ||
| TEST_VALUE("CREATE DATABASE testdb;", true, {}), | ||
| TEST_VALUE("CREATE DATABASE IF NOT EXISTS testdb;", true, {}), | ||
| TEST_VALUE("ALTER DATABASE testdb CHARACTER SET charset_name;", true, {}), | ||
| TEST_VALUE("ALTER DATABASE testdb default CHARACTER SET charset_name;", true, {}), | ||
| TEST_VALUE("ALTER DATABASE testdb default CHARACTER SET = charset_name;", true, {}), | ||
| TEST_VALUE("ALTER SCHEMA testdb default CHARACTER SET = charset_name;", true, {}), | ||
|
|
||
| // The following DROP DATABASE tests should not produce metadata. | ||
| TEST_VALUE("DROP DATABASE testdb;", true, {}), | ||
| TEST_VALUE("DROP DATABASE IF EXISTS testdb;", true, {}), | ||
|
|
||
| // Schema. Should be parsed fine, but should not produce any metadata | ||
| TEST_VALUE("SHOW databases;", true, {}), TEST_VALUE("SHOW tables;", true, {}), | ||
| TEST_VALUE("SELECT * FROM;", false, {}), | ||
| TEST_VALUE("SELECT 1 FROM tabletest1;", true, {{"tabletest1", {"select"}}}) | ||
|
|
||
| )); | ||
|
|
||
| } // namespace SQLUtils | ||
| } // namespace Common | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we need to add docs on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean. Actually at this moment nothing changed from metadata point of view. Documentation for mysql is still valid:https://www.envoyproxy.io/docs/envoy/v1.14.1/configuration/listeners/network_filters/mysql_proxy_filter.html?highlight=metadata#dynamic-metadata.
When I link this
setMetadatato Postgres filter I will create similar docs describing what metadata is created by Postgres filter, but this will happen once this PR is merged.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @dio meant doxygen style doc for this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.