Skip to content
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

Merged
merged 5 commits into from
Aug 18, 2022

Conversation

rahul2393
Copy link
Collaborator

Fixes: #18

@rahul2393 rahul2393 requested a review from a team as a code owner August 12, 2022 10:11
Comment on lines 1 to 13
{
"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"
}
Copy link
Collaborator

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)
Copy link
Collaborator

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?

@@ -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
Copy link
Collaborator

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.

Suggested change
// 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

startQuote = c
// check whether it is a triple-quote
if len(runes) > index+2 && runes[index+1] == startQuote && runes[index+2] == startQuote {
isTripleQuoted = true
Copy link
Collaborator

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.

lastCharWasEscapeChar = false
} else if isTripleQuoted {
if len(runes) > index+2 && runes[index+1] == startQuote && runes[index+2] == startQuote {
isInQuoted = false
Copy link
Collaborator

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.

Comment on lines 483 to 484
input: `?'?it\\'?s'?`,
want: `@p1'?it\\'?s'@p2`,
Copy link
Collaborator

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.

Comment on lines 491 to 492
input: "?`?it\\\\`?s`?",
want: "@p1`?it\\\\`?s`@p2",
Copy link
Collaborator

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.

err: spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "statement contains an unclosed literal: %s", "?'''?it\\'?s \n ?it\\'?s'?")),
},
}
for _, tc := range tests {
Copy link
Collaborator

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Has the statement already been cleared for comments at this point?
  2. 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.
  3. 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?

Copy link
Collaborator

@olavloite olavloite left a 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

res = append(res, string(runes[startIndex:index]))
hasNamedParameter = true
Copy link
Collaborator

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

@@ -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)
Copy link
Collaborator

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) {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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())
Copy link
Collaborator

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='test@test.com'`,
wantSQL: `SELECT * FROM """strange
@table
""" WHERE Name like @name AND Email='test@test.com'`,
Copy link
Collaborator

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```

Copy link
Collaborator Author

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.

@rahul2393 rahul2393 force-pushed the support-positional-parameters branch 3 times, most recently from 43828ca to 3f20c19 Compare August 18, 2022 05:26
@rahul2393 rahul2393 force-pushed the support-positional-parameters branch from 3f20c19 to 4eb1eb9 Compare August 18, 2022 09:32
@rahul2393 rahul2393 merged commit a71a457 into main Aug 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support positional parameters
2 participants