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

Parameter binding in InfluxQL (influxdata/influxdb-java#274) #429

Merged
merged 11 commits into from
Mar 21, 2018

Conversation

hampelratte
Copy link
Contributor

This is a simple implementation for "prepared statements" (#274) using regular expressions.

I added a new query class BoundParameterQuery, which can be used for
"prepared statements". The constructor accepts an InfluxQL expression
with placeholders, a DB name and a varags list to bind the parameters to
the placeholders. I also extended the InfluxDBService, so that the HTTP
requests contain the "params" parameter. The InfluxDBImpl now
differentiates between a Query and a BoundParameterQuery. This is not
the cleanest solution, because this had to be done at a few locations,
but I didn't want to change too much of the code.

It has limitations, because it does not use a parser/grammar for InfluxQL. So you could for example have a placeholder in a string value of a field/tag and this implementation would fail, but at least it is a start and better than risking injections.

Feel free to delete this pull request, if you don't like it at all ;)

I added a new query class BoundParameterQuery, which can be used for
"prepared statements". The constructor accepts a InfluxQL expression
with placeholders, a DB name and a varags list to bind the parameters to
the placeholders. I also extended the InfluxDBService, so that the HTTP
requests contain the "params" parameter. The InfluxDBImpl now
differentiates between a Query and a BoundParameterQuery. This is not
the cleanest solution, because this has to be done at a few locations,
but I didn't want to change too much of the code.
@codecov-io
Copy link

codecov-io commented Mar 15, 2018

Codecov Report

Merging #429 into master will increase coverage by 0.79%.
The diff coverage is 89.33%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #429      +/-   ##
============================================
+ Coverage     84.98%   85.78%   +0.79%     
- Complexity      274      291      +17     
============================================
  Files            19       20       +1     
  Lines          1192     1259      +67     
  Branches        121      130       +9     
============================================
+ Hits           1013     1080      +67     
  Misses          118      118              
  Partials         61       61
Impacted Files Coverage Δ Complexity Δ
src/main/java/org/influxdb/impl/InfluxDBImpl.java 85.67% <79.31%> (+0.65%) 64 <3> (+3) ⬆️
...ain/java/org/influxdb/dto/BoundParameterQuery.java 95.65% <95.65%> (ø) 12 <12> (?)
src/main/java/org/influxdb/impl/TimeUtil.java 68% <0%> (+4%) 6% <0%> (+1%) ⬆️
src/main/java/org/influxdb/dto/Query.java 63.15% <0%> (+5.26%) 12% <0%> (+1%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 90f02ea...2fc928e. Read the comment docs.

@majst01
Copy link
Collaborator

majst01 commented Mar 16, 2018

Hi @hampelratte thanks for the effort, we always appreciate contribution.

I think this is a good starting Point, but i would like to have a fluent API without a very wide Constructor. I can think of something like:

query = BoundParameterQuery("SELECT * FROM abc WHERE a > $a AND b < $b")
                  .with("a",0)
                  .with("b",10)
                  .create()

This Builder avoid very long constructor calls and also makes it more
obvious, which value is bound to which placeholder.
Removed bind method from BoundParameterQuery, because this functionality
is now implemented in the QueryBuilder
Fixed checkstyle issues and formatted according to project standard
(hopefully)
Added a test which executes a BoundParameterQuery against an actual
database.
}

private void assurePlaceholdersAreBound(final List<String> placeholders, final Map<String, Object> params) {
if (placeholders.size() != params.size()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This check will cause a valid query to fail. For example:

BoundParameterQuery query = QueryBuilder.newQuery("SELECT * FROM cpu WHERE atag = $mytag ; SELECT * FROM cpu WHERE atag != $mytag")
   .forDatabase(dbName)
   .bind("mytag", "test")
   .create();

Is there any reason to have it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relevant code has been removed

for (Entry<String, Object> pair : parameterMap.entrySet()) {
String name = pair.getKey();
Object value = pair.getValue();
if (value instanceof Number) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The moshi library has a specific behavior when deserializing numbers: #153 (comment)

I hope casting to Number is enough to keep the data type on the JSON object being created here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a look at the moshi code and tested it. This works as intended.

} else if (value instanceof Boolean) {
writer.name(name).value((Boolean) value);
} else {
writer.name(name).value(value.toString());
Copy link
Contributor

Choose a reason for hiding this comment

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

This may cause a NullPointerException.

I would change the logic here to:

if (... Number) {
   //...
} else if (... Boolean) {
   //...
} else {
   writer.name(name).value(String.valueOf(value));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


private List<String> parsePlaceHolders(final String command) {
List<String> placeHolders = new ArrayList<>();
Pattern p = Pattern.compile("\\s+\\$(\\w+?)(?:\\s|$)");
Copy link
Contributor

Choose a reason for hiding this comment

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

Pattern is immutable and should be declared as class scoped static final.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relevant code has been removed

}

@Test
public void testEqualsAndHashCode() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this really necessary? Did you create your own hashcode/equals implementation or you are using the one provided by your IDE?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a test for hashcode and equals in Query. So I just did it for BoundParameterQuery, too.

.bind("b", 10)
.create();
Map<String, Object> params = readObject(decode(query.getParameterJsonWithUrlEncoded()));
Assert.assertEquals(2, params.size());
Copy link
Contributor

Choose a reason for hiding this comment

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

I really appreciate your detailed and extensive test but I think it's too much: you are testing multiple times the same logic over and over.

Could you please test query.getParameterJsonWithUrlEncoded() only once, covering all data types supported by InfluxDB?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relevant code has been removed


public String getParameterJsonWithUrlEncoded() {
try {
List<String> placeholders = parsePlaceHolders(getCommand());
Copy link
Contributor

Choose a reason for hiding this comment

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

parsePlaceHolders is only used to create a list for assurePlaceholdersAreBound.
Could you please move parsePlaceHolders to be called from inside assurePlaceholdersAreBound?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relevant code has been removed


for (String placeholder : placeholders) {
if (params.get(placeholder) == null) {
throw new RuntimeException("Placeholder $" + placeholder + " is not bound");
Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I don't like to throw RuntimeExceptions (check why here https://wiki.sei.cmu.edu/confluence/display/java/ERR07-J.+Do+not+throw+RuntimeException%2C+Exception%2C+or+Throwable ).

Also, this exception will be thrown only when the query is about to be executed instead of the moment when I'm executing the .create() from the Builder. Would be possible to move it to the builder?

Another question: what about throwing IllegalStateException from the .create() method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relevant code has been removed

@fmachado
Copy link
Contributor

@hampelratte and before I forget, thanks for your contribution. :)

@hampelratte
Copy link
Contributor Author

@fmachado Good point. I was thinking, that a check for the placeholders might be a good thing, but obviously InfluxDB does that already and returns reasonable error messages. So there is no reason to do that in the connector. I'm going to remove that and as a result the majority of your annotations will get resolved, too.

The checks in BoundParameterQuery have been removed, because InfluxDB
does that already and returns appropriate error messages in case of an
invalid request.
@fmachado
Copy link
Contributor

@majst01 do you want to take a look?

@fmachado
Copy link
Contributor

@hampelratte could you please update README.md with an explanation and example of this new feature? Also, would be nice to have a link to the this PR on CHANGELOG.md under a new section "2.10 [unreleased]".

@majst01
Copy link
Collaborator

majst01 commented Mar 20, 2018

Hi,
yes, once we have a bit documentation and a changelogentry i will merge this.
Again, thanks @hampelratte for contribution !

Added short paragraph in README, which describes the parameter binding
for queries. Also added an entry for the parameter binding pull request
to the changelog.
@majst01 majst01 merged commit aca3701 into influxdata:master Mar 21, 2018
@tekbot
Copy link

tekbot commented Apr 20, 2018

Hi @majst01 !

We're eagerly awaiting the 2.10 release for this very feature. Do you have an estimated release date by chance?

@tekbot
Copy link

tekbot commented May 11, 2018

I just added this to our project -- Thanks! It did require that I include OKIO (https://github.com/square/okio) as a dependency. Trying to query against an influxDB instance threw a Buffer exception without it. Easy enough to resolve but it might be worth adding it on your side or updating your documentation. On the documentation topic, you still list 2.9 as your latest version for the maven dependency configuration in your docs.

@majst01
Copy link
Collaborator

majst01 commented May 13, 2018

Hi,
Readme.md ist fixed.
If you are using maven or gradle to build and package your application, then okio is part of the transitive dependencies, if not you must of course care by yourself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants