From 692fac07b959ba6b154dbda392cc04c39e45bccc Mon Sep 17 00:00:00 2001 From: Zhengda Lu Date: Fri, 10 Nov 2023 18:41:57 -0500 Subject: [PATCH] Setup test suite to verify different query flavors per dbms (#25) --- dbms_test.go | 142 ++++++++++++++++++ normalizer.go | 41 +++-- obfuscator.go | 10 +- sqllexer.go | 2 +- sqllexer_utils.go | 2 + testdata/README.md | 54 +++++++ .../delete-complex-subqueries-joins.json | 19 +++ .../complex/insert-complex-select-joins.json | 24 +++ .../select-complex-aggregates-subqueries.json | 23 +++ ...select-complex-joins-window-functions.json | 21 +++ ...t-nested-subqueries-aggregates-limits.json | 19 +++ .../update-complex-subquery-conditional.json | 21 +++ .../postgresql/delete/delete-all-rows.json | 19 +++ .../postgresql/delete/delete-returning.json | 19 +++ testdata/postgresql/delete/delete-simple.json | 19 +++ .../postgresql/delete/delete-using-join.json | 19 +++ .../postgresql/delete/delete-with-cte.json | 21 +++ .../delete/delete-with-subquery.json | 21 +++ .../create-function-that-raises-notice.json | 14 ++ .../create-function-with-dynamic-query.json | 14 ++ .../create-function-with-parameters.json | 14 ++ .../create-function-with-table-return.json | 14 ++ .../create-simple-plpgsql-function.json | 14 ++ ...invoke-function-positional-parameters.json | 17 +++ .../invoke-function-returning-table.json | 8 + .../invoke-function-that-raises-notice.json | 8 + .../invoke-function-with-dynamic-query.json | 8 + .../invoke-function-with-parameter.json | 8 + .../function/invoke-simple-function.json | 8 + .../postgresql/insert/insert-array-data.json | 25 +++ .../postgresql/insert/insert-json-data.json | 19 +++ .../insert/insert-multiple-rows.json | 19 +++ .../insert/insert-positional-parameters.json | 14 ++ ...insert-returning-positional-parameter.json | 14 ++ .../postgresql/insert/insert-simple-row.json | 19 +++ .../insert-with-conflict-do-nothing.json | 19 +++ .../insert/insert-with-conflict-update.json | 20 +++ .../insert/insert-with-default.json | 19 +++ .../insert/insert-with-enum-type.json | 19 +++ .../insert/insert-with-geometric-data.json | 19 +++ .../insert/insert-with-hstore-data.json | 19 +++ .../insert/insert-with-range-data.json | 19 +++ .../insert/insert-with-returning.json | 19 +++ .../postgresql/insert/insert-with-select.json | 21 +++ .../insert-with-subquery-and-alias.json | 21 +++ .../select/aggregate-functions-count.json | 25 +++ .../select/basic_select_with_alias.json | 31 ++++ .../postgresql/select/case-statements.json | 19 +++ .../select/common-table-expressions-cte.json | 21 +++ testdata/postgresql/select/cross-joins.json | 21 +++ .../select/distinct-on-expressions.json | 19 +++ .../postgresql/select/fetch-first-clause.json | 19 +++ testdata/postgresql/select/for-update-of.json | 20 +++ .../postgresql/select/full-outer-joins.json | 21 +++ .../postgresql/select/group-by-having.json | 25 +++ .../postgresql/select/json-field-access.json | 19 +++ .../select/jsonb-array-elements-text.json | 25 +++ .../postgresql/select/jsonb-array-length.json | 25 +++ .../select/jsonb-contained-in-path.json | 19 +++ .../postgresql/select/jsonb-contains-key.json | 19 +++ .../jsonb-contains-object-at-top-level.json | 19 +++ .../select/jsonb-delete-array-element.json | 19 +++ .../postgresql/select/jsonb-delete-key.json | 19 +++ .../postgresql/select/jsonb-delete-path.json | 28 ++++ .../select/jsonb-extract-path-text.json | 25 +++ .../postgresql/select/jsonb-extract-path.json | 25 +++ .../postgresql/select/jsonb-pretty-print.json | 25 +++ .../select/jsonb-set-new-value.json | 25 +++ testdata/postgresql/select/lateral-joins.json | 26 ++++ .../postgresql/select/limit-and-offset.json | 19 +++ testdata/postgresql/select/natural-joins.json | 21 +++ ...elect-in-clause-positional-parameters.json | 14 ++ ...iple-conditions-positional-parameters.json | 14 ++ .../select-with-positional-parameter.json | 14 ++ testdata/postgresql/select/self-joins.json | 19 +++ .../postgresql/select/subquery-in-from.json | 25 +++ .../postgresql/select/subquery-in-select.json | 20 +++ .../postgresql/select/subquery-in-where.json | 20 +++ .../select/tablesample-bernoulli.json | 19 +++ .../update/update-array-append.json | 19 +++ .../update/update-increment-numeric.json | 19 +++ .../postgresql/update/update-json-data.json | 25 +++ ...multiple-fields-positional-parameters.json | 15 ++ .../update/update-positional-parameters.json | 14 ++ .../postgresql/update/update-returning.json | 25 +++ .../update/update-set-multiple-columns.json | 19 +++ .../update/update-set-single-column.json | 19 +++ .../postgresql/update/update-using-join.json | 20 +++ .../postgresql/update/update-with-case.json | 19 +++ .../postgresql/update/update-with-cte.json | 21 +++ .../update/update-with-subquery.json | 26 ++++ 91 files changed, 1871 insertions(+), 22 deletions(-) create mode 100644 dbms_test.go create mode 100644 testdata/README.md create mode 100644 testdata/postgresql/complex/delete-complex-subqueries-joins.json create mode 100644 testdata/postgresql/complex/insert-complex-select-joins.json create mode 100644 testdata/postgresql/complex/select-complex-aggregates-subqueries.json create mode 100644 testdata/postgresql/complex/select-complex-joins-window-functions.json create mode 100644 testdata/postgresql/complex/select-nested-subqueries-aggregates-limits.json create mode 100644 testdata/postgresql/complex/update-complex-subquery-conditional.json create mode 100644 testdata/postgresql/delete/delete-all-rows.json create mode 100644 testdata/postgresql/delete/delete-returning.json create mode 100644 testdata/postgresql/delete/delete-simple.json create mode 100644 testdata/postgresql/delete/delete-using-join.json create mode 100644 testdata/postgresql/delete/delete-with-cte.json create mode 100644 testdata/postgresql/delete/delete-with-subquery.json create mode 100644 testdata/postgresql/function/create-function-that-raises-notice.json create mode 100644 testdata/postgresql/function/create-function-with-dynamic-query.json create mode 100644 testdata/postgresql/function/create-function-with-parameters.json create mode 100644 testdata/postgresql/function/create-function-with-table-return.json create mode 100644 testdata/postgresql/function/create-simple-plpgsql-function.json create mode 100644 testdata/postgresql/function/invoke-function-positional-parameters.json create mode 100644 testdata/postgresql/function/invoke-function-returning-table.json create mode 100644 testdata/postgresql/function/invoke-function-that-raises-notice.json create mode 100644 testdata/postgresql/function/invoke-function-with-dynamic-query.json create mode 100644 testdata/postgresql/function/invoke-function-with-parameter.json create mode 100644 testdata/postgresql/function/invoke-simple-function.json create mode 100644 testdata/postgresql/insert/insert-array-data.json create mode 100644 testdata/postgresql/insert/insert-json-data.json create mode 100644 testdata/postgresql/insert/insert-multiple-rows.json create mode 100644 testdata/postgresql/insert/insert-positional-parameters.json create mode 100644 testdata/postgresql/insert/insert-returning-positional-parameter.json create mode 100644 testdata/postgresql/insert/insert-simple-row.json create mode 100644 testdata/postgresql/insert/insert-with-conflict-do-nothing.json create mode 100644 testdata/postgresql/insert/insert-with-conflict-update.json create mode 100644 testdata/postgresql/insert/insert-with-default.json create mode 100644 testdata/postgresql/insert/insert-with-enum-type.json create mode 100644 testdata/postgresql/insert/insert-with-geometric-data.json create mode 100644 testdata/postgresql/insert/insert-with-hstore-data.json create mode 100644 testdata/postgresql/insert/insert-with-range-data.json create mode 100644 testdata/postgresql/insert/insert-with-returning.json create mode 100644 testdata/postgresql/insert/insert-with-select.json create mode 100644 testdata/postgresql/insert/insert-with-subquery-and-alias.json create mode 100644 testdata/postgresql/select/aggregate-functions-count.json create mode 100644 testdata/postgresql/select/basic_select_with_alias.json create mode 100644 testdata/postgresql/select/case-statements.json create mode 100644 testdata/postgresql/select/common-table-expressions-cte.json create mode 100644 testdata/postgresql/select/cross-joins.json create mode 100644 testdata/postgresql/select/distinct-on-expressions.json create mode 100644 testdata/postgresql/select/fetch-first-clause.json create mode 100644 testdata/postgresql/select/for-update-of.json create mode 100644 testdata/postgresql/select/full-outer-joins.json create mode 100644 testdata/postgresql/select/group-by-having.json create mode 100644 testdata/postgresql/select/json-field-access.json create mode 100644 testdata/postgresql/select/jsonb-array-elements-text.json create mode 100644 testdata/postgresql/select/jsonb-array-length.json create mode 100644 testdata/postgresql/select/jsonb-contained-in-path.json create mode 100644 testdata/postgresql/select/jsonb-contains-key.json create mode 100644 testdata/postgresql/select/jsonb-contains-object-at-top-level.json create mode 100644 testdata/postgresql/select/jsonb-delete-array-element.json create mode 100644 testdata/postgresql/select/jsonb-delete-key.json create mode 100644 testdata/postgresql/select/jsonb-delete-path.json create mode 100644 testdata/postgresql/select/jsonb-extract-path-text.json create mode 100644 testdata/postgresql/select/jsonb-extract-path.json create mode 100644 testdata/postgresql/select/jsonb-pretty-print.json create mode 100644 testdata/postgresql/select/jsonb-set-new-value.json create mode 100644 testdata/postgresql/select/lateral-joins.json create mode 100644 testdata/postgresql/select/limit-and-offset.json create mode 100644 testdata/postgresql/select/natural-joins.json create mode 100644 testdata/postgresql/select/select-in-clause-positional-parameters.json create mode 100644 testdata/postgresql/select/select-multiple-conditions-positional-parameters.json create mode 100644 testdata/postgresql/select/select-with-positional-parameter.json create mode 100644 testdata/postgresql/select/self-joins.json create mode 100644 testdata/postgresql/select/subquery-in-from.json create mode 100644 testdata/postgresql/select/subquery-in-select.json create mode 100644 testdata/postgresql/select/subquery-in-where.json create mode 100644 testdata/postgresql/select/tablesample-bernoulli.json create mode 100644 testdata/postgresql/update/update-array-append.json create mode 100644 testdata/postgresql/update/update-increment-numeric.json create mode 100644 testdata/postgresql/update/update-json-data.json create mode 100644 testdata/postgresql/update/update-multiple-fields-positional-parameters.json create mode 100644 testdata/postgresql/update/update-positional-parameters.json create mode 100644 testdata/postgresql/update/update-returning.json create mode 100644 testdata/postgresql/update/update-set-multiple-columns.json create mode 100644 testdata/postgresql/update/update-set-single-column.json create mode 100644 testdata/postgresql/update/update-using-join.json create mode 100644 testdata/postgresql/update/update-with-case.json create mode 100644 testdata/postgresql/update/update-with-cte.json create mode 100644 testdata/postgresql/update/update-with-subquery.json diff --git a/dbms_test.go b/dbms_test.go new file mode 100644 index 0000000..735de09 --- /dev/null +++ b/dbms_test.go @@ -0,0 +1,142 @@ +package sqllexer + +import ( + "embed" + "encoding/json" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +//go:embed testdata/* +var testdata embed.FS + +type output struct { + Expected string `json:"expected"` + ObfuscatorConfig *obfuscatorConfig `json:"obfuscator_config,omitempty"` + NormalizerConfig *normalizerConfig `json:"normalizer_config,omitempty"` + StatementMetadata *StatementMetadata `json:"statement_metadata,omitempty"` +} + +type testcase struct { + Input string `json:"input"` + Outputs []output `json:"outputs"` +} + +// TestQueriesPerDBMS tests a preset of queries and expected output per DBMS +// Test folder structure: +// -- testdata +// +// -- dbms_type +// -- query_type +// -- query_name.json +func TestQueriesPerDBMS(t *testing.T) { + dbmsTypes := []DBMSType{ + DBMSPostgres, + } + + for _, dbms := range dbmsTypes { + // Get all subdirectories of the testdata folder + baseDir := filepath.Join("testdata", string(dbms)) + // Get all subdirectories of the testdata folder + queryTypes, err := testdata.ReadDir(baseDir) + if err != nil { + t.Fatal(err) + } + + for _, qt := range queryTypes { + dirPath := filepath.Join(baseDir, qt.Name()) + files, err := testdata.ReadDir(dirPath) + if err != nil { + t.Fatal(err) + } + + for _, file := range files { + testName := strings.TrimSuffix(file.Name(), ".json") + t.Run(testName, func(t *testing.T) { + queryPath := filepath.Join(dirPath, file.Name()) + + testfile, err := testdata.ReadFile(queryPath) + if err != nil { + t.Fatal(err) + } + + var tt testcase + + if err := json.Unmarshal(testfile, &tt); err != nil { + t.Fatal(err) + } + + var defaultObfuscatorConfig *obfuscatorConfig + var defaultNormalizerConfig *normalizerConfig + + for _, output := range tt.Outputs { + // If the test case has a custom obfuscator or normalizer config + // use it, otherwise use the default config + if output.ObfuscatorConfig != nil { + defaultObfuscatorConfig = output.ObfuscatorConfig + } else { + defaultObfuscatorConfig = &obfuscatorConfig{ + DollarQuotedFunc: true, + ReplaceDigits: true, + ReplacePositionalParameter: true, + ReplaceBoolean: true, + ReplaceNull: true, + } + } + + if output.NormalizerConfig != nil { + defaultNormalizerConfig = output.NormalizerConfig + } else { + defaultNormalizerConfig = &normalizerConfig{ + CollectComments: true, + CollectCommands: true, + CollectTables: true, + CollectProcedure: true, + KeepSQLAlias: false, + UppercaseKeywords: false, + RemoveSpaceBetweenParentheses: false, + KeepTrailingSemicolon: false, + } + } + + obfuscator := NewObfuscator( + WithDollarQuotedFunc(defaultObfuscatorConfig.DollarQuotedFunc), + WithReplaceDigits(defaultObfuscatorConfig.ReplaceDigits), + WithReplacePositionalParameter(defaultObfuscatorConfig.ReplacePositionalParameter), + WithReplaceBoolean(defaultObfuscatorConfig.ReplaceBoolean), + WithReplaceNull(defaultObfuscatorConfig.ReplaceNull), + ) + + normalizer := NewNormalizer( + WithCollectComments(defaultNormalizerConfig.CollectComments), + WithCollectCommands(defaultNormalizerConfig.CollectCommands), + WithCollectTables(defaultNormalizerConfig.CollectTables), + WithCollectProcedures(defaultNormalizerConfig.CollectProcedure), + WithKeepSQLAlias(defaultNormalizerConfig.KeepSQLAlias), + WithUppercaseKeywords(defaultNormalizerConfig.UppercaseKeywords), + WithRemoveSpaceBetweenParentheses(defaultNormalizerConfig.RemoveSpaceBetweenParentheses), + WithKeepTrailingSemicolon(defaultNormalizerConfig.KeepTrailingSemicolon), + ) + + got, statementMetadata, err := ObfuscateAndNormalize(string(tt.Input), obfuscator, normalizer, WithDBMS(dbms)) + + if err != nil { + t.Fatal(err) + } + + // Compare the expected output with the actual output + assert.Equal(t, output.Expected, got) + + // Compare the expected statement metadata with the actual statement metadata + if output.StatementMetadata != nil { + assert.Equal(t, output.StatementMetadata, statementMetadata) + } + } + }) + } + } + } +} diff --git a/normalizer.go b/normalizer.go index 4b2d7c1..fc1bafa 100644 --- a/normalizer.go +++ b/normalizer.go @@ -6,31 +6,31 @@ import ( type normalizerConfig struct { // CollectTables specifies whether the normalizer should also extract the table names that a query addresses - CollectTables bool + CollectTables bool `json:"collect_tables"` // CollectCommands specifies whether the normalizer should extract and return commands as SQL metadata - CollectCommands bool + CollectCommands bool `json:"collect_commands"` // CollectComments specifies whether the normalizer should extract and return comments as SQL metadata - CollectComments bool + CollectComments bool `json:"collect_comments"` // CollectProcedure specifies whether the normalizer should extract and return procedure name as SQL metadata - CollectProcedure bool + CollectProcedure bool `json:"collect_procedure"` // KeepSQLAlias specifies whether SQL aliases ("AS") should be truncated. - KeepSQLAlias bool + KeepSQLAlias bool `json:"keep_sql_alias"` // UppercaseKeywords specifies whether SQL keywords should be uppercased. - UppercaseKeywords bool + UppercaseKeywords bool `json:"uppercase_keywords"` // RemoveSpaceBetweenParentheses specifies whether spaces should be kept between parentheses. // Spaces are inserted between parentheses by default. but this can be disabled by setting this to true. - RemoveSpaceBetweenParentheses bool + RemoveSpaceBetweenParentheses bool `json:"remove_space_between_parentheses"` // KeepTrailingSemicolon specifies whether the normalizer should keep the trailing semicolon. // The trailing semicolon is removed by default, but this can be disabled by setting this to true. // PL/SQL requires a trailing semicolon, so this should be set to true when normalizing PL/SQL. - KeepTrailingSemicolon bool + KeepTrailingSemicolon bool `json:"keep_trailing_semicolon"` } type normalizerOption func(*normalizerConfig) @@ -84,11 +84,11 @@ func WithKeepTrailingSemicolon(keepTrailingSemicolon bool) normalizerOption { } type StatementMetadata struct { - Size int - Tables []string - Comments []string - Commands []string - Procedures []string + Size int `json:"size"` + Tables []string `json:"tables"` + Comments []string `json:"comments"` + Commands []string `json:"commands"` + Procedures []string `json:"procedures"` } type groupablePlaceholder struct { @@ -162,7 +162,7 @@ func (n *Normalizer) collectMetadata(token *Token, lastToken *Token, statementMe if n.config.CollectCommands && isCommand(strings.ToUpper(tokenVal)) { // Collect commands statementMetadata.Commands = append(statementMetadata.Commands, strings.ToUpper(tokenVal)) - } else if n.config.CollectTables && isTableIndicator(strings.ToUpper(lastToken.Value)) { + } else if n.config.CollectTables && isTableIndicator(strings.ToUpper(lastToken.Value)) && !isSQLKeyword(token) { // Collect table names statementMetadata.Tables = append(statementMetadata.Tables, tokenVal) } else if n.config.CollectProcedure && isProcedure(lastToken) { @@ -217,7 +217,7 @@ func (n *Normalizer) normalizeSQL(token *Token, lastToken *Token, normalizedSQLB } // group consecutive obfuscated values into single placeholder - if n.isObfuscatedValueGroupable(token, lastToken, groupablePlaceholder) { + if n.isObfuscatedValueGroupable(token, lastToken, groupablePlaceholder, normalizedSQLBuilder) { // return the token but not write it to the normalizedSQLBuilder *lastToken = *token return @@ -239,7 +239,7 @@ func (n *Normalizer) writeToken(token *Token, normalizedSQLBuilder *strings.Buil } } -func (n *Normalizer) isObfuscatedValueGroupable(token *Token, lastToken *Token, groupablePlaceholder *groupablePlaceholder) bool { +func (n *Normalizer) isObfuscatedValueGroupable(token *Token, lastToken *Token, groupablePlaceholder *groupablePlaceholder, normalizedSQLBuilder *strings.Builder) bool { if token.Value == NumberPlaceholder || token.Value == StringPlaceholder { if lastToken.Value == "(" || lastToken.Value == "[" { // if the last token is "(" or "[", and the current token is a placeholder, @@ -258,6 +258,15 @@ func (n *Normalizer) isObfuscatedValueGroupable(token *Token, lastToken *Token, if groupablePlaceholder.groupable && (token.Value == ")" || token.Value == "]") { // end of groupable placeholders groupablePlaceholder.groupable = false + return false + } + + if groupablePlaceholder.groupable && token.Value != NumberPlaceholder && token.Value != StringPlaceholder && lastToken.Value == "," { + // This is a tricky edge case. If we are inside a groupbale block, and the current token is not a placeholder, + // we not only want to write the current token to the normalizedSQLBuilder, but also write the last comma that we skipped. + // For example, (?, ARRAY[?, ?, ?]) should be normalized as (?, ARRAY[?]) + normalizedSQLBuilder.WriteString(lastToken.Value) + return false } return false diff --git a/obfuscator.go b/obfuscator.go index 269c75a..f7e105e 100644 --- a/obfuscator.go +++ b/obfuscator.go @@ -5,11 +5,11 @@ import ( ) type obfuscatorConfig struct { - DollarQuotedFunc bool - ReplaceDigits bool - ReplacePositionalParameter bool - ReplaceBoolean bool - ReplaceNull bool + DollarQuotedFunc bool `json:"dollar_quoted_func"` + ReplaceDigits bool `json:"replace_digits"` + ReplacePositionalParameter bool `json:"replace_positional_parameter"` + ReplaceBoolean bool `json:"replace_boolean"` + ReplaceNull bool `json:"replace_null"` } type obfuscatorOption func(*obfuscatorConfig) diff --git a/sqllexer.go b/sqllexer.go index 3154d2c..749e8e6 100644 --- a/sqllexer.go +++ b/sqllexer.go @@ -33,7 +33,7 @@ type Token struct { } type LexerConfig struct { - DBMS DBMSType + DBMS DBMSType `json:"dbms,omitempty"` } type lexerOption func(*LexerConfig) diff --git a/sqllexer_utils.go b/sqllexer_utils.go index e1dbebe..223ed48 100644 --- a/sqllexer_utils.go +++ b/sqllexer_utils.go @@ -135,6 +135,8 @@ var keywords = map[string]bool{ "UNLOGGED": true, "RECURSIVE": true, "RETURNING": true, + "OFFSET": true, + "OF": true, } func isWhitespace(ch rune) bool { diff --git a/testdata/README.md b/testdata/README.md new file mode 100644 index 0000000..1237e81 --- /dev/null +++ b/testdata/README.md @@ -0,0 +1,54 @@ +# Test Suite + +The test suite is a collection of test SQL statements that are organized per DBMS. The test suite is used to test the SQL obfuscator and normalizer for correctness and completeness. It is also intended to cover DBMS specific edge cases, that are not covered by the generic unit tests. + +## Test Suite Structure + +The test suite is organized in the following way: + +```text +testdata +├── README.md +├── dbms1 +│   ├── query_type1 +│   │   ├── test1.json +│   └── query_type2 +│   ├── test1.json +dbms_test.go +``` + +The test suite is organized per DBMS. Each DBMS has a number of query types. Each query type has a number of test cases. Each test case consists of a SQL statement and the expected output of the obfuscator/normalizer. + +## Test File Format + +The test files are simple json files where each test case comes with one input SQL statements and an array of expected outputs. +Each expected output can optionally come with a configuration for the obfuscator and normalizer. The configuration is optional, because the default configuration is used if no configuration is provided. + +testcase.json: + +```json +{ + "input": "SELECT * FROM table1", + "outputs": [ + { + // Test case 1 + "expected": "SELECT * FROM table1", + "obfuscator_config": {...}, // optional + "normalizer_config": {...} // optional + }, + { + // Test case 2 + "expected": "SELECT * FROM table1", + "obfuscator_config": {...}, // optional + "normalizer_config": {...} // optional + } + ] +} +``` + +## How to write a new test case + +1. Create a new directory for the DBMS, if it does not exist yet. (this step is often not necessary) +2. Create a new directory for the query type, if it does not exist yet. +3. Create a new test case `.json` file with the SQL statement and expected output. Refer to the [test file format](#test-file-format) or `testcase struct` in [dbms_test.go](../dbms_test.go) for more details. +4. Run the test suite to verify that the test case is working as expected. diff --git a/testdata/postgresql/complex/delete-complex-subqueries-joins.json b/testdata/postgresql/complex/delete-complex-subqueries-joins.json new file mode 100644 index 0000000..ef55833 --- /dev/null +++ b/testdata/postgresql/complex/delete-complex-subqueries-joins.json @@ -0,0 +1,19 @@ +{ + "input": "DELETE FROM \n users u\nUSING \n orders o,\n order_items oi,\n products p\nWHERE \n u.id = o.user_id\nAND o.id = oi.order_id\nAND oi.product_id = p.id\nAND p.category = 'obsolete'\nAND o.order_date < NOW() - INTERVAL '5 years';", + "outputs": [ + { + "expected": "DELETE FROM users u USING orders o, order_items oi, products p WHERE u.id = o.user_id AND o.id = oi.order_id AND oi.product_id = p.id AND p.category = ? AND o.order_date < NOW ( ) - INTERVAL ?", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "DELETE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/complex/insert-complex-select-joins.json b/testdata/postgresql/complex/insert-complex-select-joins.json new file mode 100644 index 0000000..841ce4b --- /dev/null +++ b/testdata/postgresql/complex/insert-complex-select-joins.json @@ -0,0 +1,24 @@ +{ + "input": "INSERT INTO order_summaries (order_id, product_count, total_amount, average_product_price)\nSELECT \n o.id,\n COUNT(p.id),\n SUM(oi.amount),\n AVG(p.price)\nFROM \n orders o\nJOIN order_items oi ON o.id = oi.order_id\nJOIN products p ON oi.product_id = p.id\nGROUP BY \n o.id\nHAVING \n SUM(oi.amount) > 1000;", + "outputs": [ + { + "expected": "INSERT INTO order_summaries ( order_id, product_count, total_amount, average_product_price ) SELECT o.id, COUNT ( p.id ), SUM ( oi.amount ), AVG ( p.price ) FROM orders o JOIN order_items oi ON o.id = oi.order_id JOIN products p ON oi.product_id = p.id GROUP BY o.id HAVING SUM ( oi.amount ) > ?", + "statement_metadata": { + "size": 56, + "tables": [ + "order_summaries", + "orders", + "order_items", + "products" + ], + "commands": [ + "INSERT", + "SELECT", + "JOIN" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/complex/select-complex-aggregates-subqueries.json b/testdata/postgresql/complex/select-complex-aggregates-subqueries.json new file mode 100644 index 0000000..1b2126a --- /dev/null +++ b/testdata/postgresql/complex/select-complex-aggregates-subqueries.json @@ -0,0 +1,23 @@ +{ + "input": "SELECT \n u.id,\n u.name,\n (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count,\n (SELECT SUM(amount) FROM payments p WHERE p.user_id = u.id) AS total_payments,\n (SELECT AVG(rating) FROM reviews r WHERE r.user_id = u.id) AS average_rating\nFROM \n users u\nWHERE \n EXISTS (\n SELECT 1 FROM logins l WHERE l.user_id = u.id AND l.time > NOW() - INTERVAL '1 month'\n )\nAND u.status = 'active'\nORDER BY \n total_payments DESC, average_rating DESC, order_count DESC\nLIMIT 10;", + "outputs": [ + { + "expected": "SELECT u.id, u.name, ( SELECT COUNT ( * ) FROM orders o WHERE o.user_id = u.id ), ( SELECT SUM ( amount ) FROM payments p WHERE p.user_id = u.id ), ( SELECT AVG ( rating ) FROM reviews r WHERE r.user_id = u.id ) FROM users u WHERE EXISTS ( SELECT ? FROM logins l WHERE l.user_id = u.id AND l.time > NOW ( ) - INTERVAL ? ) AND u.status = ? ORDER BY total_payments DESC, average_rating DESC, order_count DESC LIMIT ?", + "statement_metadata": { + "size": 38, + "tables": [ + "orders", + "payments", + "reviews", + "users", + "logins" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/complex/select-complex-joins-window-functions.json b/testdata/postgresql/complex/select-complex-joins-window-functions.json new file mode 100644 index 0000000..d53cff3 --- /dev/null +++ b/testdata/postgresql/complex/select-complex-joins-window-functions.json @@ -0,0 +1,21 @@ +{ + "input": "SELECT \n e1.name AS employee_name,\n e1.salary,\n e2.name AS manager_name,\n AVG(e2.salary) OVER (PARTITION BY e1.manager_id) AS avg_manager_salary,\n RANK() OVER (ORDER BY e1.salary DESC) AS salary_rank\nFROM \n employees e1\nLEFT JOIN employees e2 ON e1.manager_id = e2.id\nWHERE \n e1.department_id IN (SELECT id FROM departments WHERE name LIKE 'IT%')\nAND \n e1.hire_date > '2020-01-01'\nORDER BY \n salary_rank, avg_manager_salary DESC;", + "outputs": [ + { + "expected": "SELECT e?.name, e?.salary, e?.name, AVG ( e?.salary ) OVER ( PARTITION BY e?.manager_id ), RANK ( ) OVER ( ORDER BY e?.salary DESC ) FROM employees e? LEFT JOIN employees e? ON e?.manager_id = e?.id WHERE e?.department_id IN ( SELECT id FROM departments WHERE name LIKE ? ) AND e?.hire_date > ? ORDER BY salary_rank, avg_manager_salary DESC", + "statement_metadata": { + "size": 30, + "tables": [ + "employees", + "departments" + ], + "commands": [ + "SELECT", + "JOIN" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/complex/select-nested-subqueries-aggregates-limits.json b/testdata/postgresql/complex/select-nested-subqueries-aggregates-limits.json new file mode 100644 index 0000000..d864245 --- /dev/null +++ b/testdata/postgresql/complex/select-nested-subqueries-aggregates-limits.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT \n user_id,\n order_id,\n order_total,\n user_total\nFROM (\n SELECT \n o.user_id,\n o.id AS order_id,\n o.total AS order_total,\n (SELECT SUM(total) FROM orders WHERE user_id = o.user_id) AS user_total,\n RANK() OVER (PARTITION BY o.user_id ORDER BY o.total DESC) AS rnk\n FROM \n orders o\n) sub\nWHERE \n sub.rnk = 1\nAND user_total > (\n SELECT \n AVG(total) * 2 \n FROM orders\n);", + "outputs": [ + { + "expected": "SELECT user_id, order_id, order_total, user_total FROM ( SELECT o.user_id, o.id, o.total, ( SELECT SUM ( total ) FROM orders WHERE user_id = o.user_id ), RANK ( ) OVER ( PARTITION BY o.user_id ORDER BY o.total DESC ) FROM orders o ) sub WHERE sub.rnk = ? AND user_total > ( SELECT AVG ( total ) * ? FROM orders )", + "statement_metadata": { + "size": 12, + "tables": [ + "orders" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/complex/update-complex-subquery-conditional.json b/testdata/postgresql/complex/update-complex-subquery-conditional.json new file mode 100644 index 0000000..15d51e0 --- /dev/null +++ b/testdata/postgresql/complex/update-complex-subquery-conditional.json @@ -0,0 +1,21 @@ +{ + "input": "UPDATE \n products p\nSET \n price = CASE \n WHEN p.stock < 10 THEN p.price * 1.10\n WHEN p.stock BETWEEN 10 AND 50 THEN p.price\n ELSE p.price * 0.90\n END,\n last_updated = NOW()\nFROM (\n SELECT \n product_id, \n SUM(quantity) AS stock\n FROM \n inventory\n GROUP BY \n product_id\n) AS sub\nWHERE \n sub.product_id = p.id;", + "outputs": [ + { + "expected": "UPDATE products p SET price = CASE WHEN p.stock < ? THEN p.price * ? WHEN p.stock BETWEEN ? AND ? THEN p.price ELSE p.price * ? END, last_updated = NOW ( ) FROM ( SELECT product_id, SUM ( quantity ) FROM inventory GROUP BY product_id ) WHERE sub.product_id = p.id", + "statement_metadata": { + "size": 29, + "tables": [ + "products", + "inventory" + ], + "commands": [ + "UPDATE", + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/delete/delete-all-rows.json b/testdata/postgresql/delete/delete-all-rows.json new file mode 100644 index 0000000..96eb980 --- /dev/null +++ b/testdata/postgresql/delete/delete-all-rows.json @@ -0,0 +1,19 @@ +{ + "input": "DELETE FROM temp_table;", + "outputs": [ + { + "expected": "DELETE FROM temp_table", + "statement_metadata": { + "size": 16, + "tables": [ + "temp_table" + ], + "commands": [ + "DELETE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/delete/delete-returning.json b/testdata/postgresql/delete/delete-returning.json new file mode 100644 index 0000000..772ac10 --- /dev/null +++ b/testdata/postgresql/delete/delete-returning.json @@ -0,0 +1,19 @@ +{ + "input": "DELETE FROM orders WHERE id = 8 RETURNING *;", + "outputs": [ + { + "expected": "DELETE FROM orders WHERE id = ? RETURNING *", + "statement_metadata": { + "size": 12, + "tables": [ + "orders" + ], + "commands": [ + "DELETE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/delete/delete-simple.json b/testdata/postgresql/delete/delete-simple.json new file mode 100644 index 0000000..0ddcff7 --- /dev/null +++ b/testdata/postgresql/delete/delete-simple.json @@ -0,0 +1,19 @@ +{ + "input": "DELETE FROM users WHERE id = 7;", + "outputs": [ + { + "expected": "DELETE FROM users WHERE id = ?", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "DELETE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/delete/delete-using-join.json b/testdata/postgresql/delete/delete-using-join.json new file mode 100644 index 0000000..60d22f4 --- /dev/null +++ b/testdata/postgresql/delete/delete-using-join.json @@ -0,0 +1,19 @@ +{ + "input": "DELETE FROM user_logins USING users WHERE user_logins.user_id = users.id AND users.status = 'inactive';", + "outputs": [ + { + "expected": "DELETE FROM user_logins USING users WHERE user_logins.user_id = users.id AND users.status = ?", + "statement_metadata": { + "size": 17, + "tables": [ + "user_logins" + ], + "commands": [ + "DELETE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/delete/delete-with-cte.json b/testdata/postgresql/delete/delete-with-cte.json new file mode 100644 index 0000000..e721079 --- /dev/null +++ b/testdata/postgresql/delete/delete-with-cte.json @@ -0,0 +1,21 @@ +{ + "input": "WITH deleted AS (\n DELETE FROM users WHERE last_login < NOW() - INTERVAL '2 years' RETURNING *\n)\nSELECT * FROM deleted;", + "outputs": [ + { + "expected": "WITH deleted AS ( DELETE FROM users WHERE last_login < NOW ( ) - INTERVAL ? RETURNING * ) SELECT * FROM deleted", + "statement_metadata": { + "size": 24, + "tables": [ + "users", + "deleted" + ], + "commands": [ + "DELETE", + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/delete/delete-with-subquery.json b/testdata/postgresql/delete/delete-with-subquery.json new file mode 100644 index 0000000..8857f3a --- /dev/null +++ b/testdata/postgresql/delete/delete-with-subquery.json @@ -0,0 +1,21 @@ +{ + "input": "DELETE FROM comments WHERE user_id IN (SELECT id FROM users WHERE status = 'banned');", + "outputs": [ + { + "expected": "DELETE FROM comments WHERE user_id IN ( SELECT id FROM users WHERE status = ? )", + "statement_metadata": { + "size": 25, + "tables": [ + "comments", + "users" + ], + "commands": [ + "DELETE", + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/create-function-that-raises-notice.json b/testdata/postgresql/function/create-function-that-raises-notice.json new file mode 100644 index 0000000..3bb0c7c --- /dev/null +++ b/testdata/postgresql/function/create-function-that-raises-notice.json @@ -0,0 +1,14 @@ +{ + "input": "CREATE OR REPLACE FUNCTION log_activity(activity text) RETURNS void AS $func$\nBEGIN\n RAISE NOTICE 'Activity: %', activity;\nEND;\n$func$ LANGUAGE plpgsql;", + "outputs": [ + { + "expected": "CREATE OR REPLACE FUNCTION log_activity ( activity text ) RETURNS void AS $func$BEGIN RAISE NOTICE ?, activity; END$func$ LANGUAGE plpgsql" + }, + { + "obfuscator_config": { + "dollar_quoted_func": false + }, + "expected": "CREATE OR REPLACE FUNCTION log_activity ( activity text ) RETURNS void AS ? LANGUAGE plpgsql" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/create-function-with-dynamic-query.json b/testdata/postgresql/function/create-function-with-dynamic-query.json new file mode 100644 index 0000000..8804741 --- /dev/null +++ b/testdata/postgresql/function/create-function-with-dynamic-query.json @@ -0,0 +1,14 @@ +{ + "input": "CREATE OR REPLACE FUNCTION dynamic_query(sql_query text) RETURNS SETOF RECORD AS $func$\nBEGIN\n RETURN QUERY EXECUTE sql_query;\nEND;\n$func$ LANGUAGE plpgsql;", + "outputs": [ + { + "expected": "CREATE OR REPLACE FUNCTION dynamic_query ( sql_query text ) RETURNS SETOF RECORD AS $func$BEGIN RETURN QUERY EXECUTE sql_query; END$func$ LANGUAGE plpgsql" + }, + { + "obfuscator_config": { + "dollar_quoted_func": false + }, + "expected": "CREATE OR REPLACE FUNCTION dynamic_query ( sql_query text ) RETURNS SETOF RECORD AS ? LANGUAGE plpgsql" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/create-function-with-parameters.json b/testdata/postgresql/function/create-function-with-parameters.json new file mode 100644 index 0000000..18f8e7f --- /dev/null +++ b/testdata/postgresql/function/create-function-with-parameters.json @@ -0,0 +1,14 @@ +{ + "input": "CREATE OR REPLACE FUNCTION get_user_email(user_id integer) RETURNS text AS $func$\nBEGIN\n RETURN (SELECT email FROM users WHERE id = user_id);\nEND;\n$func$ LANGUAGE plpgsql;", + "outputs": [ + { + "expected": "CREATE OR REPLACE FUNCTION get_user_email ( user_id integer ) RETURNS text AS $func$BEGIN RETURN ( SELECT email FROM users WHERE id = user_id ); END$func$ LANGUAGE plpgsql" + }, + { + "obfuscator_config": { + "dollar_quoted_func": false + }, + "expected": "CREATE OR REPLACE FUNCTION get_user_email ( user_id integer ) RETURNS text AS ? LANGUAGE plpgsql" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/create-function-with-table-return.json b/testdata/postgresql/function/create-function-with-table-return.json new file mode 100644 index 0000000..dd9092d --- /dev/null +++ b/testdata/postgresql/function/create-function-with-table-return.json @@ -0,0 +1,14 @@ +{ + "input": "CREATE OR REPLACE FUNCTION get_users() RETURNS TABLE(user_id integer, user_name text) AS $func$\nBEGIN\n RETURN QUERY SELECT id, name FROM users;\nEND;\n$func$ LANGUAGE plpgsql;", + "outputs": [ + { + "expected": "CREATE OR REPLACE FUNCTION get_users ( ) RETURNS TABLE ( user_id integer, user_name text ) AS $func$BEGIN RETURN QUERY SELECT id, name FROM users; END$func$ LANGUAGE plpgsql" + }, + { + "obfuscator_config": { + "dollar_quoted_func": false + }, + "expected": "CREATE OR REPLACE FUNCTION get_users ( ) RETURNS TABLE ( user_id integer, user_name text ) AS ? LANGUAGE plpgsql" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/create-simple-plpgsql-function.json b/testdata/postgresql/function/create-simple-plpgsql-function.json new file mode 100644 index 0000000..0147b7d --- /dev/null +++ b/testdata/postgresql/function/create-simple-plpgsql-function.json @@ -0,0 +1,14 @@ +{ + "input": "CREATE OR REPLACE FUNCTION get_user_count() RETURNS integer AS $func$\nBEGIN\n RETURN (SELECT COUNT(*) FROM users);\nEND;\n$func$ LANGUAGE plpgsql;", + "outputs": [ + { + "expected": "CREATE OR REPLACE FUNCTION get_user_count ( ) RETURNS integer AS $func$BEGIN RETURN ( SELECT COUNT ( * ) FROM users ); END$func$ LANGUAGE plpgsql" + }, + { + "obfuscator_config": { + "dollar_quoted_func": false + }, + "expected": "CREATE OR REPLACE FUNCTION get_user_count ( ) RETURNS integer AS ? LANGUAGE plpgsql" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/invoke-function-positional-parameters.json b/testdata/postgresql/function/invoke-function-positional-parameters.json new file mode 100644 index 0000000..fe57a79 --- /dev/null +++ b/testdata/postgresql/function/invoke-function-positional-parameters.json @@ -0,0 +1,17 @@ +{ + "input": "SELECT calculate_discount($1, $2);", + "outputs": [ + { + "expected": "SELECT calculate_discount ( ? )" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "normalizer_config": { + "remove_space_between_parentheses": true + }, + "expected": "SELECT calculate_discount($1, $2)" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/invoke-function-returning-table.json b/testdata/postgresql/function/invoke-function-returning-table.json new file mode 100644 index 0000000..2b80ec4 --- /dev/null +++ b/testdata/postgresql/function/invoke-function-returning-table.json @@ -0,0 +1,8 @@ +{ + "input": "SELECT * FROM get_users();", + "outputs": [ + { + "expected": "SELECT * FROM get_users ( )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/invoke-function-that-raises-notice.json b/testdata/postgresql/function/invoke-function-that-raises-notice.json new file mode 100644 index 0000000..7e627ec --- /dev/null +++ b/testdata/postgresql/function/invoke-function-that-raises-notice.json @@ -0,0 +1,8 @@ +{ + "input": "SELECT log_activity('User logged in');", + "outputs": [ + { + "expected": "SELECT log_activity ( ? )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/invoke-function-with-dynamic-query.json b/testdata/postgresql/function/invoke-function-with-dynamic-query.json new file mode 100644 index 0000000..492c03b --- /dev/null +++ b/testdata/postgresql/function/invoke-function-with-dynamic-query.json @@ -0,0 +1,8 @@ +{ + "input": "SELECT * FROM dynamic_query('SELECT * FROM users WHERE id = 1') AS t(id integer, name text, email text);", + "outputs": [ + { + "expected": "SELECT * FROM dynamic_query ( ? ) AS t ( id integer, name text, email text )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/invoke-function-with-parameter.json b/testdata/postgresql/function/invoke-function-with-parameter.json new file mode 100644 index 0000000..0c4d1d5 --- /dev/null +++ b/testdata/postgresql/function/invoke-function-with-parameter.json @@ -0,0 +1,8 @@ +{ + "input": "SELECT get_user_email(1);", + "outputs": [ + { + "expected": "SELECT get_user_email ( ? )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/function/invoke-simple-function.json b/testdata/postgresql/function/invoke-simple-function.json new file mode 100644 index 0000000..3af547c --- /dev/null +++ b/testdata/postgresql/function/invoke-simple-function.json @@ -0,0 +1,8 @@ +{ + "input": "SELECT get_user_count();", + "outputs": [ + { + "expected": "SELECT get_user_count ( )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-array-data.json b/testdata/postgresql/insert/insert-array-data.json new file mode 100644 index 0000000..0887b14 --- /dev/null +++ b/testdata/postgresql/insert/insert-array-data.json @@ -0,0 +1,25 @@ +{ + "input": "INSERT INTO users (name, favorite_numbers) VALUES ('Array User', ARRAY[3, 6, 9]);", + "outputs": [ + { + "expected": "INSERT INTO users ( name, favorite_numbers ) VALUES ( ?, ARRAY [ ? ] )", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "INSERT INTO users (name, favorite_numbers) VALUES (?, ARRAY [?])", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-json-data.json b/testdata/postgresql/insert/insert-json-data.json new file mode 100644 index 0000000..7a9c655 --- /dev/null +++ b/testdata/postgresql/insert/insert-json-data.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO events (data) VALUES ('{\"type\": \"user_signup\", \"user_id\": 1}');", + "outputs": [ + { + "expected": "INSERT INTO events ( data ) VALUES ( ? )", + "statement_metadata": { + "size": 12, + "tables": [ + "events" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-multiple-rows.json b/testdata/postgresql/insert/insert-multiple-rows.json new file mode 100644 index 0000000..7101dc7 --- /dev/null +++ b/testdata/postgresql/insert/insert-multiple-rows.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO users (name, email) VALUES ('Jane Doe', 'jane@example.com'), ('Bob Smith', 'bob@example.com');", + "outputs": [ + { + "expected": "INSERT INTO users ( name, email ) VALUES ( ? ), ( ? )", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-positional-parameters.json b/testdata/postgresql/insert/insert-positional-parameters.json new file mode 100644 index 0000000..b85086c --- /dev/null +++ b/testdata/postgresql/insert/insert-positional-parameters.json @@ -0,0 +1,14 @@ +{ + "input": "INSERT INTO users (name, email, age) VALUES ($1, $2, $3);", + "outputs": [ + { + "expected": "INSERT INTO users ( name, email, age ) VALUES ( ? )" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "expected": "INSERT INTO users ( name, email, age ) VALUES ( $1, $2, $3 )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-returning-positional-parameter.json b/testdata/postgresql/insert/insert-returning-positional-parameter.json new file mode 100644 index 0000000..d9f4e22 --- /dev/null +++ b/testdata/postgresql/insert/insert-returning-positional-parameter.json @@ -0,0 +1,14 @@ +{ + "input": "INSERT INTO orders (product_id, quantity, total) VALUES ($1, $2, $3) RETURNING id;", + "outputs": [ + { + "expected": "INSERT INTO orders ( product_id, quantity, total ) VALUES ( ? ) RETURNING id" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "expected": "INSERT INTO orders ( product_id, quantity, total ) VALUES ( $1, $2, $3 ) RETURNING id" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-simple-row.json b/testdata/postgresql/insert/insert-simple-row.json new file mode 100644 index 0000000..88bbe70 --- /dev/null +++ b/testdata/postgresql/insert/insert-simple-row.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com');", + "outputs": [ + { + "expected": "INSERT INTO users ( name, email ) VALUES ( ? )", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-conflict-do-nothing.json b/testdata/postgresql/insert/insert-with-conflict-do-nothing.json new file mode 100644 index 0000000..0372dab --- /dev/null +++ b/testdata/postgresql/insert/insert-with-conflict-do-nothing.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO users (id, name, email) VALUES (1, 'Duplicate', 'duplicate@example.com') ON CONFLICT (id) DO NOTHING;", + "outputs": [ + { + "expected": "INSERT INTO users ( id, name, email ) VALUES ( ? ) ON CONFLICT ( id ) DO NOTHING", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-conflict-update.json b/testdata/postgresql/insert/insert-with-conflict-update.json new file mode 100644 index 0000000..428cebb --- /dev/null +++ b/testdata/postgresql/insert/insert-with-conflict-update.json @@ -0,0 +1,20 @@ +{ + "input": "INSERT INTO users (id, name, email) VALUES (1, 'Duplicate', 'duplicate@example.com') ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;", + "outputs": [ + { + "expected": "INSERT INTO users ( id, name, email ) VALUES ( ? ) ON CONFLICT ( id ) DO UPDATE SET email = EXCLUDED.email", + "statement_metadata": { + "size": 17, + "tables": [ + "users" + ], + "commands": [ + "INSERT", + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-default.json b/testdata/postgresql/insert/insert-with-default.json new file mode 100644 index 0000000..1f3a69f --- /dev/null +++ b/testdata/postgresql/insert/insert-with-default.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO products (name, price, description) VALUES ('New Product', 123, DEFAULT);", + "outputs": [ + { + "expected": "INSERT INTO products ( name, price, description ) VALUES ( ?, DEFAULT )", + "statement_metadata": { + "size": 14, + "tables": [ + "products" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-enum-type.json b/testdata/postgresql/insert/insert-with-enum-type.json new file mode 100644 index 0000000..0bb98b2 --- /dev/null +++ b/testdata/postgresql/insert/insert-with-enum-type.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO shipments (status) VALUES ('delivered'::shipment_status);", + "outputs": [ + { + "expected": "INSERT INTO shipments ( status ) VALUES ( ? :: shipment_status )", + "statement_metadata": { + "size": 15, + "tables": [ + "shipments" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-geometric-data.json b/testdata/postgresql/insert/insert-with-geometric-data.json new file mode 100644 index 0000000..bbae656 --- /dev/null +++ b/testdata/postgresql/insert/insert-with-geometric-data.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO places (name, location) VALUES ('Point Place', point '(10, 20)');", + "outputs": [ + { + "expected": "INSERT INTO places ( name, location ) VALUES ( ?, point ? )", + "statement_metadata": { + "size": 12, + "tables": [ + "places" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-hstore-data.json b/testdata/postgresql/insert/insert-with-hstore-data.json new file mode 100644 index 0000000..a71f5fb --- /dev/null +++ b/testdata/postgresql/insert/insert-with-hstore-data.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO user_profiles (profile) VALUES ('\"height\"=>\"2m\", \"weight\"=>\"70kg\"');", + "outputs": [ + { + "expected": "INSERT INTO user_profiles ( profile ) VALUES ( ? )", + "statement_metadata": { + "size": 19, + "tables": [ + "user_profiles" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-range-data.json b/testdata/postgresql/insert/insert-with-range-data.json new file mode 100644 index 0000000..c17593e --- /dev/null +++ b/testdata/postgresql/insert/insert-with-range-data.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO reservations (during) VALUES ('[2023-01-01 14:00, 2023-01-01 15:00)');", + "outputs": [ + { + "expected": "INSERT INTO reservations ( during ) VALUES ( ? )", + "statement_metadata": { + "size": 18, + "tables": [ + "reservations" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-returning.json b/testdata/postgresql/insert/insert-with-returning.json new file mode 100644 index 0000000..b75c50f --- /dev/null +++ b/testdata/postgresql/insert/insert-with-returning.json @@ -0,0 +1,19 @@ +{ + "input": "INSERT INTO users (name, email) VALUES ('Alice Jones', 'alice@example.com') RETURNING id;", + "outputs": [ + { + "expected": "INSERT INTO users ( name, email ) VALUES ( ? ) RETURNING id", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "INSERT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-select.json b/testdata/postgresql/insert/insert-with-select.json new file mode 100644 index 0000000..5325d6b --- /dev/null +++ b/testdata/postgresql/insert/insert-with-select.json @@ -0,0 +1,21 @@ +{ + "input": "INSERT INTO user_logins (user_id, login_time) SELECT id, NOW() FROM users WHERE active;", + "outputs": [ + { + "expected": "INSERT INTO user_logins ( user_id, login_time ) SELECT id, NOW ( ) FROM users WHERE active", + "statement_metadata": { + "size": 28, + "tables": [ + "user_logins", + "users" + ], + "commands": [ + "INSERT", + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/insert/insert-with-subquery-and-alias.json b/testdata/postgresql/insert/insert-with-subquery-and-alias.json new file mode 100644 index 0000000..dfdbfe7 --- /dev/null +++ b/testdata/postgresql/insert/insert-with-subquery-and-alias.json @@ -0,0 +1,21 @@ +{ + "input": "INSERT INTO user_logins (user_id, login_time) SELECT u.id, NOW() FROM users u WHERE u.active;", + "outputs": [ + { + "expected": "INSERT INTO user_logins ( user_id, login_time ) SELECT u.id, NOW ( ) FROM users u WHERE u.active", + "statement_metadata": { + "size": 28, + "tables": [ + "user_logins", + "users" + ], + "commands": [ + "INSERT", + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/aggregate-functions-count.json b/testdata/postgresql/select/aggregate-functions-count.json new file mode 100644 index 0000000..3119a2c --- /dev/null +++ b/testdata/postgresql/select/aggregate-functions-count.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT COUNT(*) AS total_users FROM users;", + "outputs": [ + { + "expected": "SELECT COUNT ( * ) FROM users", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT COUNT(*) FROM users", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/basic_select_with_alias.json b/testdata/postgresql/select/basic_select_with_alias.json new file mode 100644 index 0000000..5fd8a33 --- /dev/null +++ b/testdata/postgresql/select/basic_select_with_alias.json @@ -0,0 +1,31 @@ +{ + "input": "SELECT u.id AS user_id, u.name AS username FROM users u;", + "outputs": [ + { + "expected": "SELECT u.id, u.name FROM users u", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "normalizer_config": { + "keep_sql_alias": true + }, + "expected": "SELECT u.id AS user_id, u.name AS username FROM users u" + }, + { + "normalizer_config": { + "keep_trailing_semicolon": true + }, + "expected": "SELECT u.id, u.name FROM users u;" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/case-statements.json b/testdata/postgresql/select/case-statements.json new file mode 100644 index 0000000..6beb2c0 --- /dev/null +++ b/testdata/postgresql/select/case-statements.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT name, CASE WHEN age < 18 THEN 'minor' ELSE 'adult' END FROM users;", + "outputs": [ + { + "expected": "SELECT name, CASE WHEN age < ? THEN ? ELSE ? END FROM users", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/common-table-expressions-cte.json b/testdata/postgresql/select/common-table-expressions-cte.json new file mode 100644 index 0000000..34a1eb9 --- /dev/null +++ b/testdata/postgresql/select/common-table-expressions-cte.json @@ -0,0 +1,21 @@ +{ + "input": "WITH recursive_subordinates AS (\n SELECT id, manager_id FROM employees WHERE id = 1\n UNION ALL\n SELECT e.id, e.manager_id FROM employees e INNER JOIN recursive_subordinates rs ON rs.id = e.manager_id\n)\nSELECT * FROM recursive_subordinates;", + "outputs": [ + { + "expected": "WITH recursive_subordinates AS ( SELECT id, manager_id FROM employees WHERE id = ? UNION ALL SELECT e.id, e.manager_id FROM employees e INNER JOIN recursive_subordinates rs ON rs.id = e.manager_id ) SELECT * FROM recursive_subordinates", + "statement_metadata": { + "size": 41, + "tables": [ + "employees", + "recursive_subordinates" + ], + "commands": [ + "SELECT", + "JOIN" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/cross-joins.json b/testdata/postgresql/select/cross-joins.json new file mode 100644 index 0000000..aeaa4a1 --- /dev/null +++ b/testdata/postgresql/select/cross-joins.json @@ -0,0 +1,21 @@ +{ + "input": "SELECT * FROM users CROSS JOIN cities;", + "outputs": [ + { + "expected": "SELECT * FROM users CROSS JOIN cities", + "statement_metadata": { + "size": 21, + "tables": [ + "users", + "cities" + ], + "commands": [ + "SELECT", + "JOIN" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/distinct-on-expressions.json b/testdata/postgresql/select/distinct-on-expressions.json new file mode 100644 index 0000000..25d8e90 --- /dev/null +++ b/testdata/postgresql/select/distinct-on-expressions.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT DISTINCT ON (location) location, time FROM events ORDER BY location, time DESC;", + "outputs": [ + { + "expected": "SELECT DISTINCT ON ( location ) location, time FROM events ORDER BY location, time DESC", + "statement_metadata": { + "size": 12, + "tables": [ + "events" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/fetch-first-clause.json b/testdata/postgresql/select/fetch-first-clause.json new file mode 100644 index 0000000..a6b7f90 --- /dev/null +++ b/testdata/postgresql/select/fetch-first-clause.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT * FROM users ORDER BY created_at DESC FETCH FIRST 10 ROWS ONLY;", + "outputs": [ + { + "expected": "SELECT * FROM users ORDER BY created_at DESC FETCH FIRST ? ROWS ONLY", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/for-update-of.json b/testdata/postgresql/select/for-update-of.json new file mode 100644 index 0000000..b829904 --- /dev/null +++ b/testdata/postgresql/select/for-update-of.json @@ -0,0 +1,20 @@ +{ + "input": "SELECT * FROM users WHERE last_login < NOW() - INTERVAL '1 year' FOR UPDATE OF users;", + "outputs": [ + { + "expected": "SELECT * FROM users WHERE last_login < NOW ( ) - INTERVAL ? FOR UPDATE OF users", + "statement_metadata": { + "size": 17, + "tables": [ + "users" + ], + "commands": [ + "SELECT", + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/full-outer-joins.json b/testdata/postgresql/select/full-outer-joins.json new file mode 100644 index 0000000..f3329ff --- /dev/null +++ b/testdata/postgresql/select/full-outer-joins.json @@ -0,0 +1,21 @@ +{ + "input": "SELECT * FROM customers FULL OUTER JOIN orders ON customers.id = orders.customer_id;", + "outputs": [ + { + "expected": "SELECT * FROM customers FULL OUTER JOIN orders ON customers.id = orders.customer_id", + "statement_metadata": { + "size": 25, + "tables": [ + "customers", + "orders" + ], + "commands": [ + "SELECT", + "JOIN" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/group-by-having.json b/testdata/postgresql/select/group-by-having.json new file mode 100644 index 0000000..58b73c1 --- /dev/null +++ b/testdata/postgresql/select/group-by-having.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > 1;", + "outputs": [ + { + "expected": "SELECT status, COUNT ( * ) FROM orders GROUP BY status HAVING COUNT ( * ) > ?", + "statement_metadata": { + "size": 12, + "tables": [ + "orders" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT status, COUNT(*) FROM orders GROUP BY status HAVING COUNT(*) > ?", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/json-field-access.json b/testdata/postgresql/select/json-field-access.json new file mode 100644 index 0000000..c4cdfdc --- /dev/null +++ b/testdata/postgresql/select/json-field-access.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT data->'customer'->>'name' AS customer_name FROM orders;", + "outputs": [ + { + "expected": "SELECT data -> ? ->> ? FROM orders", + "statement_metadata": { + "size": 12, + "tables": [ + "orders" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-array-elements-text.json b/testdata/postgresql/select/jsonb-array-elements-text.json new file mode 100644 index 0000000..9cce21b --- /dev/null +++ b/testdata/postgresql/select/jsonb-array-elements-text.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT jsonb_array_elements_text(data->'tags') AS tag FROM products;", + "outputs": [ + { + "expected": "SELECT jsonb_array_elements_text ( data -> ? ) FROM products", + "statement_metadata": { + "size": 14, + "tables": [ + "products" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_array_elements_text(data -> ?) FROM products", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-array-length.json b/testdata/postgresql/select/jsonb-array-length.json new file mode 100644 index 0000000..f0113ee --- /dev/null +++ b/testdata/postgresql/select/jsonb-array-length.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT jsonb_array_length(data->'tags') AS num_tags FROM products;", + "outputs": [ + { + "expected": "SELECT jsonb_array_length ( data -> ? ) FROM products", + "statement_metadata": { + "size": 14, + "tables": [ + "products" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_array_length(data -> ?) FROM products", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-contained-in-path.json b/testdata/postgresql/select/jsonb-contained-in-path.json new file mode 100644 index 0000000..27ecb7d --- /dev/null +++ b/testdata/postgresql/select/jsonb-contained-in-path.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT * FROM events WHERE payload <@ '{\"events\": {\"type\": \"user_event\"}}';", + "outputs": [ + { + "expected": "SELECT * FROM events WHERE payload <@ ?", + "statement_metadata": { + "size": 12, + "tables": [ + "events" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-contains-key.json b/testdata/postgresql/select/jsonb-contains-key.json new file mode 100644 index 0000000..1485e09 --- /dev/null +++ b/testdata/postgresql/select/jsonb-contains-key.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT * FROM events WHERE payload ? 'user_id';", + "outputs": [ + { + "expected": "SELECT * FROM events WHERE payload ? ?", + "statement_metadata": { + "size": 12, + "tables": [ + "events" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-contains-object-at-top-level.json b/testdata/postgresql/select/jsonb-contains-object-at-top-level.json new file mode 100644 index 0000000..9fae952 --- /dev/null +++ b/testdata/postgresql/select/jsonb-contains-object-at-top-level.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT * FROM events WHERE payload @> '{\"type\": \"user_event\"}';", + "outputs": [ + { + "expected": "SELECT * FROM events WHERE payload @> ?", + "statement_metadata": { + "size": 12, + "tables": [ + "events" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-delete-array-element.json b/testdata/postgresql/select/jsonb-delete-array-element.json new file mode 100644 index 0000000..c224a8b --- /dev/null +++ b/testdata/postgresql/select/jsonb-delete-array-element.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT data #- '{tags,0}' AS tags_without_first FROM products;", + "outputs": [ + { + "expected": "SELECT data #- ? FROM products", + "statement_metadata": { + "size": 14, + "tables": [ + "products" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-delete-key.json b/testdata/postgresql/select/jsonb-delete-key.json new file mode 100644 index 0000000..ce40e28 --- /dev/null +++ b/testdata/postgresql/select/jsonb-delete-key.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT data - 'temporary_field' AS cleaned_data FROM user_profiles;", + "outputs": [ + { + "expected": "SELECT data - ? FROM user_profiles", + "statement_metadata": { + "size": 19, + "tables": [ + "user_profiles" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-delete-path.json b/testdata/postgresql/select/jsonb-delete-path.json new file mode 100644 index 0000000..f452fd1 --- /dev/null +++ b/testdata/postgresql/select/jsonb-delete-path.json @@ -0,0 +1,28 @@ +{ + "input": "SELECT jsonb_set(data, '{info,address}', NULL) AS removed_address FROM users;", + "outputs": [ + { + "expected": "SELECT jsonb_set ( data, ?, ? ) FROM users", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_set(data, ?, NULL) FROM users", + "obfuscator_config": { + "replace_null": false + }, + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-extract-path-text.json b/testdata/postgresql/select/jsonb-extract-path-text.json new file mode 100644 index 0000000..9f7b529 --- /dev/null +++ b/testdata/postgresql/select/jsonb-extract-path-text.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT jsonb_extract_path_text(data, 'user', 'name') AS user_name FROM user_profiles;", + "outputs": [ + { + "expected": "SELECT jsonb_extract_path_text ( data, ?, ? ) FROM user_profiles", + "statement_metadata": { + "size": 19, + "tables": [ + "user_profiles" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_extract_path_text(data, ?, ?) FROM user_profiles", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-extract-path.json b/testdata/postgresql/select/jsonb-extract-path.json new file mode 100644 index 0000000..79e00e1 --- /dev/null +++ b/testdata/postgresql/select/jsonb-extract-path.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT jsonb_extract_path(data, 'user', 'name') AS user_name FROM user_profiles;", + "outputs": [ + { + "expected": "SELECT jsonb_extract_path ( data, ?, ? ) FROM user_profiles", + "statement_metadata": { + "size": 19, + "tables": [ + "user_profiles" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_extract_path(data, ?, ?) FROM user_profiles", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-pretty-print.json b/testdata/postgresql/select/jsonb-pretty-print.json new file mode 100644 index 0000000..6b373c2 --- /dev/null +++ b/testdata/postgresql/select/jsonb-pretty-print.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT jsonb_pretty(data) AS pretty_data FROM logs;", + "outputs": [ + { + "expected": "SELECT jsonb_pretty ( data ) FROM logs", + "statement_metadata": { + "size": 10, + "tables": [ + "logs" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_pretty(data) FROM logs", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/jsonb-set-new-value.json b/testdata/postgresql/select/jsonb-set-new-value.json new file mode 100644 index 0000000..433bac4 --- /dev/null +++ b/testdata/postgresql/select/jsonb-set-new-value.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT jsonb_set(data, '{user,name}', '\"John Doe\"') AS updated_data FROM user_profiles;", + "outputs": [ + { + "expected": "SELECT jsonb_set ( data, ?, ? ) FROM user_profiles", + "statement_metadata": { + "size": 19, + "tables": [ + "user_profiles" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT jsonb_set(data, ?, ?) FROM user_profiles", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/lateral-joins.json b/testdata/postgresql/select/lateral-joins.json new file mode 100644 index 0000000..feb2213 --- /dev/null +++ b/testdata/postgresql/select/lateral-joins.json @@ -0,0 +1,26 @@ +{ + "input": "SELECT u.name, json_agg(l) FROM users u, LATERAL (SELECT id, text FROM logs WHERE logs.user_id = u.id) AS l GROUP BY u.name;", + "outputs": [ + { + "expected": "SELECT u.name, json_agg ( l ) FROM users u, LATERAL ( SELECT id, text FROM logs WHERE logs.user_id = u.id ) GROUP BY u.name", + "statement_metadata": { + "size": 15, + "tables": [ + "users", + "logs" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT u.name, json_agg(l) FROM users u, LATERAL (SELECT id, text FROM logs WHERE logs.user_id = u.id) GROUP BY u.name", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/limit-and-offset.json b/testdata/postgresql/select/limit-and-offset.json new file mode 100644 index 0000000..6bdafb4 --- /dev/null +++ b/testdata/postgresql/select/limit-and-offset.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT * FROM users ORDER BY created_at DESC LIMIT 10 OFFSET 20;", + "outputs": [ + { + "expected": "SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/natural-joins.json b/testdata/postgresql/select/natural-joins.json new file mode 100644 index 0000000..437be5d --- /dev/null +++ b/testdata/postgresql/select/natural-joins.json @@ -0,0 +1,21 @@ +{ + "input": "SELECT * FROM users NATURAL JOIN user_profiles;", + "outputs": [ + { + "expected": "SELECT * FROM users NATURAL JOIN user_profiles", + "statement_metadata": { + "size": 28, + "tables": [ + "users", + "user_profiles" + ], + "commands": [ + "SELECT", + "JOIN" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/select-in-clause-positional-parameters.json b/testdata/postgresql/select/select-in-clause-positional-parameters.json new file mode 100644 index 0000000..3c02bbc --- /dev/null +++ b/testdata/postgresql/select/select-in-clause-positional-parameters.json @@ -0,0 +1,14 @@ +{ + "input": "SELECT * FROM orders WHERE status IN ($1, $2, $3);", + "outputs": [ + { + "expected": "SELECT * FROM orders WHERE status IN ( ? )" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "expected": "SELECT * FROM orders WHERE status IN ( $1, $2, $3 )" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/select-multiple-conditions-positional-parameters.json b/testdata/postgresql/select/select-multiple-conditions-positional-parameters.json new file mode 100644 index 0000000..08e468b --- /dev/null +++ b/testdata/postgresql/select/select-multiple-conditions-positional-parameters.json @@ -0,0 +1,14 @@ +{ + "input": "SELECT * FROM products WHERE category = $1 AND price < $2;", + "outputs": [ + { + "expected": "SELECT * FROM products WHERE category = ? AND price < ?" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "expected": "SELECT * FROM products WHERE category = $1 AND price < $2" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/select-with-positional-parameter.json b/testdata/postgresql/select/select-with-positional-parameter.json new file mode 100644 index 0000000..1800787 --- /dev/null +++ b/testdata/postgresql/select/select-with-positional-parameter.json @@ -0,0 +1,14 @@ +{ + "input": "SELECT * FROM users WHERE id = $1;", + "outputs": [ + { + "expected": "SELECT * FROM users WHERE id = ?" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "expected": "SELECT * FROM users WHERE id = $1" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/self-joins.json b/testdata/postgresql/select/self-joins.json new file mode 100644 index 0000000..9a34cbd --- /dev/null +++ b/testdata/postgresql/select/self-joins.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT a.name, b.name FROM employees a, employees b WHERE a.manager_id = b.id;", + "outputs": [ + { + "expected": "SELECT a.name, b.name FROM employees a, employees b WHERE a.manager_id = b.id", + "statement_metadata": { + "size": 15, + "tables": [ + "employees" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/subquery-in-from.json b/testdata/postgresql/select/subquery-in-from.json new file mode 100644 index 0000000..af14f66 --- /dev/null +++ b/testdata/postgresql/select/subquery-in-from.json @@ -0,0 +1,25 @@ +{ + "input": "SELECT user_data.name FROM (SELECT name FROM users WHERE active = true) AS user_data;", + "outputs": [ + { + "expected": "SELECT user_data.name FROM ( SELECT name FROM users WHERE active = ? )", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "SELECT user_data.name FROM ( SELECT name FROM users WHERE active = true )", + "obfuscator_config": { + "replace_boolean": false + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/subquery-in-select.json b/testdata/postgresql/select/subquery-in-select.json new file mode 100644 index 0000000..a8f2cea --- /dev/null +++ b/testdata/postgresql/select/subquery-in-select.json @@ -0,0 +1,20 @@ +{ + "input": "SELECT name, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) AS order_count FROM users;", + "outputs": [ + { + "expected": "SELECT name, ( SELECT COUNT ( * ) FROM orders WHERE orders.user_id = users.id ) FROM users", + "statement_metadata": { + "size": 17, + "tables": [ + "orders", + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/subquery-in-where.json b/testdata/postgresql/select/subquery-in-where.json new file mode 100644 index 0000000..a8516ff --- /dev/null +++ b/testdata/postgresql/select/subquery-in-where.json @@ -0,0 +1,20 @@ +{ + "input": "SELECT name FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100);", + "outputs": [ + { + "expected": "SELECT name FROM users WHERE id IN ( SELECT user_id FROM orders WHERE total > ? )", + "statement_metadata": { + "size": 17, + "tables": [ + "users", + "orders" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/select/tablesample-bernoulli.json b/testdata/postgresql/select/tablesample-bernoulli.json new file mode 100644 index 0000000..c7a9c53 --- /dev/null +++ b/testdata/postgresql/select/tablesample-bernoulli.json @@ -0,0 +1,19 @@ +{ + "input": "SELECT * FROM users TABLESAMPLE BERNOULLI (10);", + "outputs": [ + { + "expected": "SELECT * FROM users TABLESAMPLE BERNOULLI ( ? )", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-array-append.json b/testdata/postgresql/update/update-array-append.json new file mode 100644 index 0000000..907d862 --- /dev/null +++ b/testdata/postgresql/update/update-array-append.json @@ -0,0 +1,19 @@ +{ + "input": "UPDATE users SET favorite_numbers = array_append(favorite_numbers, 42) WHERE id = 5;", + "outputs": [ + { + "expected": "UPDATE users SET favorite_numbers = array_append ( favorite_numbers, ? ) WHERE id = ?", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-increment-numeric.json b/testdata/postgresql/update/update-increment-numeric.json new file mode 100644 index 0000000..7feabe9 --- /dev/null +++ b/testdata/postgresql/update/update-increment-numeric.json @@ -0,0 +1,19 @@ +{ + "input": "UPDATE accounts SET balance = balance + 100.0 WHERE user_id = 4;", + "outputs": [ + { + "expected": "UPDATE accounts SET balance = balance + ? WHERE user_id = ?", + "statement_metadata": { + "size": 14, + "tables": [ + "accounts" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-json-data.json b/testdata/postgresql/update/update-json-data.json new file mode 100644 index 0000000..95ba892 --- /dev/null +++ b/testdata/postgresql/update/update-json-data.json @@ -0,0 +1,25 @@ +{ + "input": "UPDATE events SET data = jsonb_set(data, '{location}', '\"New Location\"') WHERE data->>'event_id' = '123';", + "outputs": [ + { + "expected": "UPDATE events SET data = jsonb_set ( data, ?, ? ) WHERE data ->> ? = ?", + "statement_metadata": { + "size": 12, + "tables": [ + "events" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "UPDATE events SET data = jsonb_set(data, ?, ?) WHERE data ->> ? = ?", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-multiple-fields-positional-parameters.json b/testdata/postgresql/update/update-multiple-fields-positional-parameters.json new file mode 100644 index 0000000..04a7d98 --- /dev/null +++ b/testdata/postgresql/update/update-multiple-fields-positional-parameters.json @@ -0,0 +1,15 @@ +{ + "input": "DELETE FROM sessions WHERE user_id = $1 AND expired = true;", + "outputs": [ + { + "expected": "DELETE FROM sessions WHERE user_id = ? AND expired = ?" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false, + "replace_boolean": false + }, + "expected": "DELETE FROM sessions WHERE user_id = $1 AND expired = true" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-positional-parameters.json b/testdata/postgresql/update/update-positional-parameters.json new file mode 100644 index 0000000..631b13c --- /dev/null +++ b/testdata/postgresql/update/update-positional-parameters.json @@ -0,0 +1,14 @@ +{ + "input": "UPDATE users SET email = $1 WHERE id = $2;", + "outputs": [ + { + "expected": "UPDATE users SET email = ? WHERE id = ?" + }, + { + "obfuscator_config": { + "replace_positional_parameter": false + }, + "expected": "UPDATE users SET email = $1 WHERE id = $2" + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-returning.json b/testdata/postgresql/update/update-returning.json new file mode 100644 index 0000000..e65fc2e --- /dev/null +++ b/testdata/postgresql/update/update-returning.json @@ -0,0 +1,25 @@ +{ + "input": "UPDATE users SET last_login = NOW() WHERE id = 3 RETURNING last_login;", + "outputs": [ + { + "expected": "UPDATE users SET last_login = NOW ( ) WHERE id = ? RETURNING last_login", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "UPDATE users SET last_login = NOW() WHERE id = ? RETURNING last_login", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-set-multiple-columns.json b/testdata/postgresql/update/update-set-multiple-columns.json new file mode 100644 index 0000000..62d037b --- /dev/null +++ b/testdata/postgresql/update/update-set-multiple-columns.json @@ -0,0 +1,19 @@ +{ + "input": "UPDATE users SET name = 'Jane Updated', email = 'jane.updated@example.com' WHERE id = 2;", + "outputs": [ + { + "expected": "UPDATE users SET name = ?, email = ? WHERE id = ?", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-set-single-column.json b/testdata/postgresql/update/update-set-single-column.json new file mode 100644 index 0000000..85972d0 --- /dev/null +++ b/testdata/postgresql/update/update-set-single-column.json @@ -0,0 +1,19 @@ +{ + "input": "UPDATE users SET name = 'John Updated' WHERE id = 1;", + "outputs": [ + { + "expected": "UPDATE users SET name = ? WHERE id = ?", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-using-join.json b/testdata/postgresql/update/update-using-join.json new file mode 100644 index 0000000..219899f --- /dev/null +++ b/testdata/postgresql/update/update-using-join.json @@ -0,0 +1,20 @@ +{ + "input": "UPDATE orders SET total = total * 0.9 FROM users WHERE users.id = orders.user_id AND users.status = 'VIP';", + "outputs": [ + { + "expected": "UPDATE orders SET total = total * ? FROM users WHERE users.id = orders.user_id AND users.status = ?", + "statement_metadata": { + "size": 17, + "tables": [ + "orders", + "users" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-with-case.json b/testdata/postgresql/update/update-with-case.json new file mode 100644 index 0000000..82cdf51 --- /dev/null +++ b/testdata/postgresql/update/update-with-case.json @@ -0,0 +1,19 @@ +{ + "input": "UPDATE users SET status = CASE WHEN last_login < NOW() - INTERVAL '1 year' THEN 'inactive' ELSE status END;", + "outputs": [ + { + "expected": "UPDATE users SET status = CASE WHEN last_login < NOW ( ) - INTERVAL ? THEN ? ELSE status END", + "statement_metadata": { + "size": 11, + "tables": [ + "users" + ], + "commands": [ + "UPDATE" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-with-cte.json b/testdata/postgresql/update/update-with-cte.json new file mode 100644 index 0000000..60fd0c4 --- /dev/null +++ b/testdata/postgresql/update/update-with-cte.json @@ -0,0 +1,21 @@ +{ + "input": "WITH updated AS (\n UPDATE users SET name = 'CTE Updated' WHERE id = 6 RETURNING *\n)\nSELECT * FROM updated;", + "outputs": [ + { + "expected": "WITH updated AS ( UPDATE users SET name = ? WHERE id = ? RETURNING * ) SELECT * FROM updated", + "statement_metadata": { + "size": 24, + "tables": [ + "users", + "updated" + ], + "commands": [ + "UPDATE", + "SELECT" + ], + "comments": [], + "procedures": [] + } + } + ] +} \ No newline at end of file diff --git a/testdata/postgresql/update/update-with-subquery.json b/testdata/postgresql/update/update-with-subquery.json new file mode 100644 index 0000000..a49aec3 --- /dev/null +++ b/testdata/postgresql/update/update-with-subquery.json @@ -0,0 +1,26 @@ +{ + "input": "UPDATE products SET price = (SELECT MAX(price) FROM products) * 0.9 WHERE name = 'Old Product';", + "outputs": [ + { + "expected": "UPDATE products SET price = ( SELECT MAX ( price ) FROM products ) * ? WHERE name = ?", + "statement_metadata": { + "size": 20, + "tables": [ + "products" + ], + "commands": [ + "UPDATE", + "SELECT" + ], + "comments": [], + "procedures": [] + } + }, + { + "expected": "UPDATE products SET price = (SELECT MAX(price) FROM products) * ? WHERE name = ?", + "normalizer_config": { + "remove_space_between_parentheses": true + } + } + ] +} \ No newline at end of file