-
Notifications
You must be signed in to change notification settings - Fork 228
SQL over CSV with tests and docs #1146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2514715
changed return types and added datasource_query
stuioco edfe0f7
SQL select over csv
stuioco 302831d
Datasource SQL Support for UPDATE and DELETE
stuioco ba4ab8e
SQL over CSV with tests and docs
stuioco f646d99
Fixes based on initial review
stuioco 71d0bd9
Changes and fixes based on review
stuioco 33f692b
Fixed return types for sql commands
stuioco 8456ff4
Updated docs for sql update and delete
stuioco 084d5cf
Updated go.mod and ran go mod vendor for latest Spectolabs/raymond
stuioco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,375 @@ | ||
| package templating | ||
|
|
||
| import ( | ||
| "errors" | ||
| "regexp" | ||
| "strconv" | ||
| "strings" | ||
|
|
||
| log "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| // RowMap represents a single row in the result set | ||
| type RowMap map[string]string | ||
|
|
||
| // Condition represents a single condition in the WHERE clause | ||
| type Condition struct { | ||
| Column string | ||
| Operator string | ||
| Value string | ||
| } | ||
|
|
||
| // SQLStatement represents a simple SQL-like query | ||
| type SQLStatement struct { | ||
| Type string // "SELECT", "UPDATE", or "DELETE" | ||
| Columns []string | ||
| Conditions []Condition | ||
| SetClauses map[string]string // For UPDATE queries | ||
| DataSourceName string | ||
| } | ||
|
|
||
| func parseSqlCommand(query string, datasource *TemplateDataSource) (SQLStatement, error) { | ||
| query = strings.TrimSpace(query) | ||
| var commandType string | ||
| if strings.HasPrefix(strings.ToUpper(query), "SELECT") { | ||
| commandType = "SELECT" | ||
| } else if strings.HasPrefix(strings.ToUpper(query), "UPDATE") { | ||
| commandType = "UPDATE" | ||
| } else if strings.HasPrefix(strings.ToUpper(query), "DELETE") { | ||
| commandType = "DELETE" | ||
| } else { | ||
| return SQLStatement{}, errors.New("invalid query type") | ||
| } | ||
|
|
||
| var selectRegex *regexp.Regexp | ||
| var matches []string | ||
| var columnsPart, dataSourceName, wherePart string | ||
|
|
||
| switch commandType { | ||
| case "SELECT": | ||
| selectRegex = regexp.MustCompile(`(?i)^SELECT\s+(.+)\s+FROM\s+([\w-]+)\s*(?:WHERE\s+(.+))?$`) | ||
| matches = selectRegex.FindStringSubmatch(query) | ||
| if len(matches) < 3 { | ||
|
|
||
| return SQLStatement{}, errors.New("invalid query format") | ||
|
|
||
| } | ||
| columnsPart = matches[1] | ||
| dataSourceName = matches[2] | ||
| if !dataSourceExists(datasource, dataSourceName) { | ||
| return SQLStatement{}, errors.New("data source does not exist") | ||
| } | ||
|
|
||
| if len(matches) == 4 { | ||
| wherePart = matches[3] | ||
| } | ||
| headers := datasource.DataSources[dataSourceName].Data[0] | ||
| columns, err := parseColumns(columnsPart, headers) | ||
| if err != nil { | ||
| return SQLStatement{}, err | ||
| } | ||
|
|
||
| conditions, err := parseConditions(wherePart) | ||
| if err != nil { | ||
| return SQLStatement{}, err | ||
| } | ||
|
|
||
| return SQLStatement{ | ||
| Type: commandType, | ||
| Columns: columns, | ||
| Conditions: conditions, | ||
| DataSourceName: dataSourceName, | ||
| }, nil | ||
| case "UPDATE": | ||
| selectRegex = regexp.MustCompile(`(?i)^UPDATE\s+([\w-]+)\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$`) | ||
| matches = selectRegex.FindStringSubmatch(query) | ||
| if len(matches) < 3 { | ||
| return SQLStatement{}, errors.New("invalid UPDATE query format") | ||
| } | ||
| dataSourceName = matches[1] | ||
| if !dataSourceExists(datasource, dataSourceName) { | ||
| return SQLStatement{}, errors.New("data source does not exist") | ||
| } | ||
| setPart := matches[2] | ||
| if len(matches) == 4 { | ||
| wherePart = matches[3] | ||
| } | ||
|
|
||
| setClauses := parseSetClauses(setPart) | ||
| conditions, err := parseConditions(wherePart) | ||
| if err != nil { | ||
| return SQLStatement{}, err | ||
| } | ||
| return SQLStatement{ | ||
| Type: "UPDATE", | ||
| Conditions: conditions, | ||
| SetClauses: setClauses, | ||
| DataSourceName: dataSourceName, | ||
| }, nil | ||
|
|
||
| case "DELETE": | ||
| selectRegex = regexp.MustCompile(`(?i)^DELETE\s+FROM\s+([\w-]+)\s*(?:WHERE\s+(.+))?$`) | ||
| matches = selectRegex.FindStringSubmatch(query) | ||
| if len(matches) < 2 { | ||
| return SQLStatement{}, errors.New("invalid DELETE query format") | ||
| } | ||
| dataSourceName = matches[1] | ||
| if !dataSourceExists(datasource, dataSourceName) { | ||
| return SQLStatement{}, errors.New("data source does not exist") | ||
| } | ||
| if len(matches) == 3 { | ||
| wherePart = matches[2] | ||
| } | ||
| conditions, err := parseConditions(wherePart) | ||
| if err != nil { | ||
| return SQLStatement{}, err | ||
| } | ||
| return SQLStatement{ | ||
| Type: "DELETE", | ||
| Conditions: conditions, | ||
| DataSourceName: dataSourceName, | ||
| }, nil | ||
| } | ||
| return SQLStatement{}, errors.New("invalid query format") | ||
| } | ||
|
|
||
| // parseColumns determines the columns to select based on the query part and headers | ||
| func parseColumns(columnsPart string, headers []string) ([]string, error) { | ||
| columnsPart = strings.TrimSpace(columnsPart) | ||
| if columnsPart == "*" { | ||
| return headers, nil | ||
| } | ||
| columns := strings.Split(columnsPart, ",") | ||
| for i, column := range columns { | ||
| if !stringExists(headers, strings.TrimSpace(column)) { | ||
| return nil, errors.New("invalid column provided: " + strings.TrimSpace(column)) | ||
| } | ||
| columns[i] = strings.TrimSpace(column) | ||
| } | ||
| return columns, nil | ||
| } | ||
|
|
||
| func stringExists(slice []string, target string) bool { | ||
| for _, s := range slice { | ||
| if s == target { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| // TrimQuotes trims matching single or double quotes from the outer edges of a string. | ||
| func trimQuotes(s string) string { | ||
| // Check if the string length is less than 2, in which case there's nothing to trim. | ||
| if len(s) < 2 { | ||
| return s | ||
| } | ||
|
|
||
| // Get the first and last characters of the string. | ||
| firstChar := s[0] | ||
| lastChar := s[len(s)-1] | ||
|
|
||
| // Check if both the first and last characters are matching quotes. | ||
| if (firstChar == '\'' || firstChar == '"') && firstChar == lastChar { | ||
| return s[1 : len(s)-1] | ||
| } | ||
|
|
||
| // If no trimming is necessary, return the original string. | ||
| return s | ||
| } | ||
|
|
||
| // parseSetClauses parses the SET part of an UPDATE query | ||
| func parseSetClauses(setPart string) map[string]string { | ||
| setClauses := make(map[string]string) | ||
| parts := strings.Split(setPart, ",") | ||
| for _, part := range parts { | ||
| keyValue := strings.Split(strings.TrimSpace(part), "=") | ||
| if len(keyValue) == 2 { | ||
| setClauses[strings.TrimSpace(keyValue[0])] = trimQuotes(strings.TrimSpace(keyValue[1])) | ||
| } | ||
| } | ||
| return setClauses | ||
| } | ||
|
|
||
| // 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*'([^']*)'`) | ||
| conditionMatches := conditionRegex.FindAllStringSubmatch(wherePart, -1) | ||
|
|
||
| if len(conditionMatches) == 0 { | ||
| return conditions, nil | ||
| } | ||
|
|
||
| for _, match := range conditionMatches { | ||
| if len(match) != 4 { | ||
| return nil, errors.New("invalid condition format") | ||
| } | ||
| conditions = append(conditions, Condition{ | ||
| Column: match[1], | ||
| Operator: match[2], | ||
| Value: match[3], | ||
| }) | ||
| } | ||
|
|
||
| return conditions, nil | ||
| } | ||
|
|
||
| // ExecuteSelectQuery executes a SELECT query and returns the results as a slice of RowMaps | ||
| func executeSqlSelectQuery(data *[][]string, query SQLStatement) []RowMap { | ||
| headers := (*data)[0] // First row as header | ||
| results := []RowMap{} | ||
| for _, row := range (*data)[1:] { | ||
| rowMap := mapRow(headers, row) | ||
| if matchesConditions(rowMap, query.Conditions) { | ||
| results = append(results, projectRow(rowMap, query.Columns)) | ||
| } | ||
| } | ||
| return results | ||
| } | ||
|
|
||
| // ExecuteUpdateQuery executes an UPDATE query and modifies the data in-place | ||
| func executeSqlUpdateCommand(data *[][]string, query SQLStatement) []RowMap { | ||
| if len(*data) < 2 { | ||
| log.Error("no data available to update") | ||
| return []RowMap{ | ||
| { | ||
| "rowsAffected": "0", | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| headers := (*data)[0] | ||
| conditions := query.Conditions | ||
| setClauses := query.SetClauses | ||
| rowsAffected := 0 | ||
| for i, row := range (*data)[1:] { | ||
| rowMap := mapRow(headers, row) | ||
| if matchesConditions(rowMap, conditions) { | ||
| rowsAffected += 1 | ||
| for column, newValue := range setClauses { | ||
| colIndex := indexOf(headers, column) | ||
| if colIndex != -1 { | ||
| (*data)[i+1][colIndex] = newValue | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return []RowMap{ | ||
| { | ||
| "rowsAffected": strconv.Itoa(rowsAffected), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // executeSqlDeleteCommand executes a DELETE query and modifies the data in-place | ||
| func executeSqlDeleteCommand(data *[][]string, query SQLStatement) []RowMap { | ||
| if len(*data) < 2 { | ||
| log.Println("no data available to delete") | ||
| return []RowMap{ | ||
| { | ||
| "rowsAffected": "0", | ||
| }, | ||
| } | ||
| } | ||
| headers := (*data)[0] | ||
| conditions := query.Conditions | ||
| rowsAffected := 0 | ||
| // Iterate in reverse to avoid index shifting issues | ||
| for i := len(*data) - 1; i > 0; i-- { | ||
| row := (*data)[i] | ||
| rowMap := mapRow(headers, row) | ||
| if matchesConditions(rowMap, conditions) { | ||
| removeRow(data, i) | ||
| rowsAffected++ | ||
| } | ||
| } | ||
| return []RowMap{ | ||
| { | ||
| "rowsAffected": strconv.Itoa(rowsAffected), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // removeRow removes the row at index rowIndex from the data. | ||
| func removeRow(data *[][]string, rowIndex int) { | ||
| if rowIndex < 0 || rowIndex >= len(*data) { | ||
| // Return early if rowIndex is out of bounds | ||
| return | ||
| } | ||
| // Overwrite the row at rowIndex with the next rows | ||
| copy((*data)[rowIndex:], (*data)[rowIndex+1:]) | ||
| // Truncate the slice to remove the last row which is now duplicate | ||
| (*data) = (*data)[:len(*data)-1] | ||
| } | ||
|
|
||
| // Helper function to find the index of a column header | ||
| func indexOf(headers []string, column string) int { | ||
| for i, header := range headers { | ||
| if header == column { | ||
| return i | ||
| } | ||
| } | ||
| return -1 | ||
| } | ||
|
|
||
| // mapRow converts a CSV row into a map with column names as keys | ||
| func mapRow(headers, row []string) RowMap { | ||
| rowMap := make(RowMap) | ||
| for i, header := range headers { | ||
| rowMap[header] = row[i] | ||
| } | ||
| return rowMap | ||
| } | ||
|
|
||
| // projectRow filters the row based on the selected columns | ||
| func projectRow(row RowMap, columns []string) RowMap { | ||
| if len(columns) == 0 { | ||
| return row | ||
| } | ||
| projected := make(RowMap) | ||
| for _, col := range columns { | ||
| if val, ok := row[col]; ok { | ||
| projected[col] = val | ||
| } | ||
| } | ||
| return projected | ||
| } | ||
|
|
||
| // matchesConditions checks if a row matches all given conditions | ||
| func matchesConditions(row RowMap, conditions []Condition) bool { | ||
| for _, condition := range conditions { | ||
| val, ok := row[condition.Column] | ||
| if !ok { | ||
| return false | ||
| } | ||
| switch condition.Operator { | ||
| case "==": | ||
| if val != condition.Value { | ||
| return false | ||
| } | ||
| case "!=": | ||
| if val == condition.Value { | ||
| return false | ||
| } | ||
| case "<": | ||
| if isGreaterThanOrEqual(val, condition.Value) { | ||
| return false | ||
| } | ||
| case "<=": | ||
| if isGreaterThan(val, condition.Value) { | ||
| return false | ||
| } | ||
| case ">": | ||
| if isLessThanOrEqual(val, condition.Value) { | ||
| return false | ||
| } | ||
| case ">=": | ||
| if isLessThan(val, condition.Value) { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| return true | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.