-
Notifications
You must be signed in to change notification settings - Fork 25
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
feat: add support of positional parameter in the queries #110
Conversation
e9426c3
to
4978ae3
Compare
.repo-metadata.json
Outdated
{ | ||
"name": "spanner-go-driver", | ||
"name_pretty": "Cloud Spanner driver for database/sql package", | ||
"product_documentation": "https://pkg.go.dev/github.com/googleapis/go-sql-spanner", | ||
"client_documentation": "https://pkg.go.dev/github.com/googleapis/go-sql-spanner", | ||
"release_level": "stable", | ||
"language": "go", | ||
"repo": "googleapis/go-sql-spanner", | ||
"distribution_name": "cloud.google.com/go-sql-spanner/spanner", | ||
"repo_short": "go-sql-spanner", | ||
"library_type": "OTHER", | ||
"codeowner_team": "@googleapis/go-sql-spanner" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this seems unrelated to this change and would be better moved to a separate PR
|
||
```go | ||
db.QueryContext(ctx, "SELECT id, text FROM tweets WHERE likes > @likes", 500) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we rather split this into two separate examples; one block that only uses named parameters and one that only uses positional parameters?
statement_parser.go
Outdated
@@ -45,6 +46,76 @@ func union(m1 map[string]bool, m2 map[string]bool) map[string]bool { | |||
return res | |||
} | |||
|
|||
// convertPositionalParametersToNamedParameters returns the sql string with all the positional | |||
// parameters converted to named parameter. The sql string must be a valid Cloud Spanner sql statement. | |||
// It may contain comments and (string) literals without any restrictions. That is, string literals |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't this function supports SQL strings with comments.
// It may contain comments and (string) literals without any restrictions. That is, string literals | |
// It may contain (string) literals without any restrictions. That is, string literals |
statement_parser.go
Outdated
startQuote = c | ||
// check whether it is a triple-quote | ||
if len(runes) > index+2 && runes[index+1] == startQuote && runes[index+2] == startQuote { | ||
isTripleQuoted = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can safely write a couple of more runes and increase the index more here, as you have already determined that they are all quotes.
statement_parser.go
Outdated
lastCharWasEscapeChar = false | ||
} else if isTripleQuoted { | ||
if len(runes) > index+2 && runes[index+1] == startQuote && runes[index+2] == startQuote { | ||
isInQuoted = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have already determined that this is the end of a triple-quoted string here, so you can safely write the three runes and update the position instead of executing the same loop for the next two characters.
statement_parser_test.go
Outdated
input: `?'?it\\'?s'?`, | ||
want: `@p1'?it\\'?s'@p2`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not seem correct. To me, the input string literal seems invalid. The double backslash in the string makes it a single backslash to Cloud Spanner. So the actual string literal here is ?it\
. The ?s'?
part that follows contains an unclosed literal.
statement_parser_test.go
Outdated
input: "?`?it\\\\`?s`?", | ||
want: "@p1`?it\\\\`?s`@p2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This test would be easier to read and understand if it used backticks to create the Golang string. Now you have to think about which part of the string is an escape to add a special character to the Golang string, and then after that, think about what the same escape character might mean to Cloud Spanner.
statement_parser_test.go
Outdated
err: spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "statement contains an unclosed literal: %s", "?'''?it\\'?s \n ?it\\'?s'?")), | ||
}, | ||
} | ||
for _, tc := range tests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test does not contain any tests for triple-quoted strings.
stmt.go
Outdated
@@ -65,6 +65,10 @@ func (s *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driv | |||
} | |||
|
|||
func prepareSpannerStmt(q string, args []driver.NamedValue) (spanner.Statement, error) { | |||
q, err := convertPositionalParametersToNamedParameters('?', q) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Has the statement already been cleared for comments at this point?
- It might be good to do a quick check whether the string contains any occurrences of
?
at all before sifting through the string searching for positional parameters. - It feels a little inefficient to first search for positional parameters and convert them to named parameters, only to call
parseNamedParameters
on the next line to loop through the same string once more to determine where those named parameters are.
I don't think we would want to support a mix of named and positional parameters in one SQL string. Would it be an option to only call one of the two functions here (so either convertPositionalParametersToNamedParameters
or parseNamedParameters
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with a couple of minor nits and test requests
statement_parser.go
Outdated
res = append(res, string(runes[startIndex:index])) | ||
hasNamedParameter = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: double assignment of hasNamedParameter
statement_parser.go
Outdated
@@ -199,14 +201,19 @@ func findParams(sql string) ([]string, error) { | |||
var startQuote rune | |||
lastCharWasEscapeChar := false | |||
isTripleQuoted := false | |||
hasNamedParameter := false | |||
hasPositionalParameter := false | |||
res := make([]string, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think it would make sense to rename this variable now that this method returns two results. So maybe something like namedParams
?
if hasPositionalParameter { | ||
return sql, nil, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "statement must not contain both named and positional parameter: %s", sql)) | ||
} | ||
parsedSQL.WriteRune(c) | ||
index++ | ||
startIndex := index | ||
for index < len(runes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... Looking at this code again I start to wonder whether there might be a bug here (that was already there before this change): What happens if the SQL string contains just a single @
? So for example select foo from bar where id=@ order by value
. Would you mind adding a test for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case the returned sql string will be exactly the same with no params, and we expect Cloud Spanner to handle the error similar to what Go client library will do, TL;DR parser won't handle this case.
return nil, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "statement contains an unclosed literal: %s", sql)) | ||
return sql, nil, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "statement contains an unclosed literal: %s", sql)) | ||
} | ||
sql = strings.TrimSpace(parsedSQL.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both for 'safety' and efficiency reasons, I think it would make sense here to only return the parsedSQL
value if hasPositionalParameters
is true. If hasPositionalParameters
is false, we can just return the input sql
value.
""" WHERE Name like @name AND Email='[email protected]'`, | ||
wantSQL: `SELECT * FROM """strange | ||
@table | ||
""" WHERE Name like @name AND Email='[email protected]'`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind adding a test that has a triple-quoted identifier as the last part of the SQL string. So for example
select * from foo where ?=```strange @table```
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the test, behaviour is similar to what JDBC spanner will return.
43828ca
to
3f20c19
Compare
3f20c19
to
4eb1eb9
Compare
Fixes: #18