diff --git a/core/templating/datasource_sql_over_csv.go b/core/templating/datasource_sql_over_csv.go index 6d7fc1501..bc0c070a2 100644 --- a/core/templating/datasource_sql_over_csv.go +++ b/core/templating/datasource_sql_over_csv.go @@ -186,23 +186,65 @@ func trimQuotes(s string) string { // parseSetClauses parses the SET part of an UPDATE query func parseSetClauses(setPart string, headers []string) (map[string]string, error) { setClauses := make(map[string]string) - parts := strings.Split(setPart, ",") + parts, err := splitOnCommasOutsideQuotes(setPart) + if err != nil { + return nil, err + } + for _, part := range parts { - keyValue := strings.Split(strings.TrimSpace(part), "=") - if !stringExists(headers, strings.TrimSpace(keyValue[0])) { - return nil, errors.New("invalid column provided: " + strings.TrimSpace(keyValue[0])) - } else if len(keyValue) == 2 { - setClauses[strings.TrimSpace(keyValue[0])] = trimQuotes(strings.TrimSpace(keyValue[1])) + keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(keyValue) != 2 { + return nil, errors.New("invalid SET clause: " + part) + } + key := strings.TrimSpace(keyValue[0]) + value := strings.TrimSpace(keyValue[1]) + + if !stringExists(headers, key) { + return nil, errors.New("invalid column provided: " + key) } + setClauses[key] = trimQuotes(value) } return setClauses, nil } +// splitOnCommasOutsideQuotes splits a string by commas only if the commas are outside of quotes +func splitOnCommasOutsideQuotes(s string) ([]string, error) { + var parts []string + var sb strings.Builder + var inQuotes bool + var quoteChar rune + + for _, r := range s { + switch { + case r == '\'' || r == '"': + if inQuotes { + if r == quoteChar { + inQuotes = false + } + } else { + inQuotes = true + quoteChar = r + } + sb.WriteRune(r) + case r == ',' && !inQuotes: + parts = append(parts, sb.String()) + sb.Reset() + default: + sb.WriteRune(r) + } + } + + // Add the last part + parts = append(parts, sb.String()) + + return parts, nil +} + // parseConditions parses the WHERE part of the query into a slice of Conditions and returns an error if any issues are found. func parseConditions(wherePart string) ([]Condition, error) { conditions := []Condition{} - conditionRegex := regexp.MustCompile(`(\w+)\s*(==|!=|<=|>=|<|>)\s*'([^']*)'`) + conditionRegex := regexp.MustCompile(`(\w+)\s*(==|=|!=|<=|>=|<|>)\s*'([^']*)'`) conditionMatches := conditionRegex.FindAllStringSubmatch(wherePart, -1) if len(conditionMatches) == 0 { @@ -337,7 +379,7 @@ func matchesConditions(row RowMap, conditions []Condition) bool { return false } switch condition.Operator { - case "==": + case "==", "=": if val != condition.Value { return false } diff --git a/core/templating/datasource_sql_over_csv_test.go b/core/templating/datasource_sql_over_csv_test.go index e8f93b25c..6a6b1a616 100644 --- a/core/templating/datasource_sql_over_csv_test.go +++ b/core/templating/datasource_sql_over_csv_test.go @@ -43,11 +43,21 @@ func TestParseCommand(t *testing.T) { expected: SQLStatement{Type: "SELECT", Columns: []string{"id", "name", "age", "department"}, Conditions: []Condition{{Column: "department", Operator: "==", Value: "Engineering"}}, DataSourceName: "employees"}, expectError: false, }, + { + query: "SELECT * FROM employees WHERE department = 'Engineering'", + expected: SQLStatement{Type: "SELECT", Columns: []string{"id", "name", "age", "department"}, Conditions: []Condition{{Column: "department", Operator: "=", Value: "Engineering"}}, DataSourceName: "employees"}, + expectError: false, + }, { query: "UPDATE employees SET age = '35' WHERE name == 'John Doe'", expected: SQLStatement{Type: "UPDATE", Conditions: []Condition{{Column: "name", Operator: "==", Value: "John Doe"}}, SetClauses: map[string]string{"age": "35"}, DataSourceName: "employees"}, expectError: false, }, + { + query: "UPDATE employees SET department = 'Engineering, Marketing' WHERE name == 'John Doe'", + expected: SQLStatement{Type: "UPDATE", Conditions: []Condition{{Column: "name", Operator: "==", Value: "John Doe"}}, SetClauses: map[string]string{"department": "Engineering, Marketing"}, DataSourceName: "employees"}, + expectError: false, + }, { query: "DELETE FROM employees WHERE age < '30'", expected: SQLStatement{Type: "DELETE", Conditions: []Condition{{Column: "age", Operator: "<", Value: "30"}}, DataSourceName: "employees"}, diff --git a/core/templating/templating_test.go b/core/templating/templating_test.go index 14345e68a..cbbbb2bf1 100644 --- a/core/templating/templating_test.go +++ b/core/templating/templating_test.go @@ -196,7 +196,7 @@ func Test_ApplyTemplate_CsvCountRowsMissingDataset(t *testing.T) { Expect(template).To(Equal(``)) } -func Test_ApplyTemplate_CsvSQL_Select(t *testing.T) { +func Test_ApplyTemplate_CsvSQL_Select_Double_Equal(t *testing.T) { RegisterTestingT(t) template, err := ApplyTemplate(&models.RequestDetails{}, make(map[string]string), `{{#each (csvSqlCommand "SELECT name FROM test-csv1 WHERE id == '1'")}}{{this.name}}{{/each}}`) @@ -205,7 +205,16 @@ func Test_ApplyTemplate_CsvSQL_Select(t *testing.T) { Expect(template).To(Equal(`Test1`)) } -func Test_ApplyTemplate_CsvSQL_SelectAll(t *testing.T) { +func Test_ApplyTemplate_CsvSQL_Select_Single_Equal(t *testing.T) { + RegisterTestingT(t) + + template, err := ApplyTemplate(&models.RequestDetails{}, make(map[string]string), `{{#each (csvSqlCommand "SELECT name FROM test-csv1 WHERE id = '1'")}}{{this.name}}{{/each}}`) + + Expect(err).To(BeNil()) + Expect(template).To(Equal(`Test1`)) +} + +func Test_ApplyTemplate_CsvSQL_SelectAllWithNumbersNoQuotes(t *testing.T) { RegisterTestingT(t) template, err := ApplyTemplate(&models.RequestDetails{}, make(map[string]string), `{{#each (csvSqlCommand "SELECT * FROM test-csv1 WHERE 1==1")}}{{this.id}},{{this.name}},{{this.marks}};{{/each}}`) @@ -214,6 +223,15 @@ func Test_ApplyTemplate_CsvSQL_SelectAll(t *testing.T) { Expect(template).To(Equal(`1,Test1,55;2,Test2,56;*,Dummy,ABSENT;`)) } +func Test_ApplyTemplate_CsvSQL_SelectAllWithNumbersInQuotes(t *testing.T) { + RegisterTestingT(t) + + template, err := ApplyTemplate(&models.RequestDetails{}, make(map[string]string), `{{#each (csvSqlCommand "SELECT * FROM test-csv1 WHERE '1'=='1'")}}{{this.id}},{{this.name}},{{this.marks}};{{/each}}`) + + Expect(err).To(BeNil()) + Expect(template).To(Equal(`1,Test1,55;2,Test2,56;*,Dummy,ABSENT;`)) +} + func Test_ApplyTemplate_CsvSQL_Update(t *testing.T) { RegisterTestingT(t) diff --git a/docs/pages/keyconcepts/templating/templating.rst b/docs/pages/keyconcepts/templating/templating.rst index b18d345f0..ff17ef264 100644 --- a/docs/pages/keyconcepts/templating/templating.rst +++ b/docs/pages/keyconcepts/templating/templating.rst @@ -326,7 +326,7 @@ Example: Start Hoverfly with a CSV data source (pets.csv) provided below. | SELECT data using a SQL | { | { | | like syntax. | "Dogs-With-Big-Ids-Only": [ | "Dogs-With-Big-Ids-Only": [ | | | {{#each (csvSqlCommand "SELECT * FROM | { | -| | pets WHERE category == 'dogs' AND id >= | "id":2000, | +| | pets WHERE category = 'dogs' AND id >= | "id":2000, | | | '2000'")}} | "category":"dogs", | | | { | "name":"Violet", | | | "id":{{this.id}}, | "status":"sold" | @@ -431,7 +431,7 @@ Update the data in a CSV Data Source using SQL like syntax {{csvSqlCommand '(sql-update-statement)'}} Example: -``{{ csvSqlCommand "UPDATE pets SET status = 'sold' WHERE id > '20' AND category == 'cats'" }}`` +``{{ csvSqlCommand "UPDATE pets SET status = 'sold' WHERE id > '20' AND category = 'cats'" }}`` @@ -449,7 +449,9 @@ Only simple conditions are supported. You can chain conditions using AND. OR is not supported. The following comparison operators are supported in conditions: -== equals += equals +> greater than +< less than >= greater than or equal to <= less than or equal to != not equal to