ref(query): Isolate query formatting from the remainder of query processing#418
Conversation
| ) | ||
|
|
||
|
|
||
| def format_query(dataset, body, table, prewhere_conditions, final) -> str: |
There was a problem hiding this comment.
My goal is to eventually replace the body, table, prewhere_conditions, final part of this signature with a Query construct that is separate from the request body format. This will require (probably substantial) changes to column_expr and condition_expr.
There was a problem hiding this comment.
any chance we could have type hints in the signature (together with the result). It would make it easier to understand the method body.
There was a problem hiding this comment.
This seems so much more readable. One question at your plan for the Query construct. Are you going to create a class that takes body, dataset, table, pre_where and that has a method that returns the query string?
Second, how will this play with multiple dataset? Is every dataset going to provide its Query construct to allow for dataset specific logic in query parsing? Assuming we will allow datasets to provide their own logic.
There was a problem hiding this comment.
any chance we could have type hints in the signature (together with the result). It would make it easier to understand the method body.
I can add them for dataset, table, and final, but the ones for body are going to take more work. I experimented a bit with this last week — anything that ends up using column_expr or condition_expr is going to take a lot of work to nail down. We can start partially typing Query into a TypedDict if you think that's helpful. I think we can at least have a comprehensive set of valid keys, and types that are more specific than Any for a handful of them (but unfortunately not really any of those that would be useful.)
There was a problem hiding this comment.
yeah, even Any would be reasonable for body, so that it sets expectations.
There was a problem hiding this comment.
This seems so much more readable. One question at your plan for the Query construct. Are you going to create a class that takes body, dataset, table, pre_where and that has a method that returns the query string?
Second, how will this play with multiple dataset? Is every dataset going to provide its Query construct to allow for dataset specific logic in query parsing? Assuming we will allow datasets to provide their own logic.
I think this is probably a little bit clearer when considered in conjunction with #425, but my general thinking is:
- The API endpoint turns a request body into a
Requestobject (might be a better name for that) that includesquery(generic query schema, this will be the exact same for all datasets) data andextension(dataset-specific schema/schemata) data. - The
Requestobject is provided to the dataset, which can mutate the query or do additional validation based on the extensions. This preferably returns just theQuerybut might need to return the fullRequest, at least for now. (More on this below.) - The query is evaluated (probably the API responsibility, but not 100% sure about this yet), which does the following in this order (the order here is important, since it's not the exact same order as
raw_queryis today):- Rate limit accounting. This will probably require access to attributes in the
Request(specificallyproject.) Trying to extract them from theQueryseems like a lot of work for little benefit. - Caching, and (optionally) deduplication. These two processes are pretty tightly coupled in right now in practical usage but are implemented independently. I think it makes sense to make that coupling explicit. Right now you can have deduplication without caching which makes very little practical sense.
- Query splitting. This will only be enabled for some datasets. This may or may not require access to the
Requestdata. The current implementation does. Not quite sure how I see this evolving yet. I think this makes more sense to happen after caching and deduplication than the way it's implemented today, where we cache each intermediate execution step. - Query formatting. (I think there's an argument to push some of this down into the
Reader, I'm not set one way or another yet.) - Query execution. Pretty much just what the
Readerdoes now.
- Rate limit accounting. This will probably require access to attributes in the
- Serialize and return the response, etc.
| body['conditions'].extend([ | ||
| ('timestamp', '>=', from_date), | ||
| ('timestamp', '<', to_date), | ||
| ]) |
There was a problem hiding this comment.
This will eventually move to the time series dataset.
| select_clause = u'SELECT {}'.format(', '.join(select_exprs)) | ||
|
|
||
| from_clause = u'FROM {}'.format(table) | ||
| body['conditions'].append(('project_id', 'IN', project_ids)) |
There was a problem hiding this comment.
This will eventually move to the events dataset.
There was a problem hiding this comment.
I suspect you may run into problems if you update the body of the query during split queries since the body is passed down by the split_query wrapper to "parse_and_run_query".
Same above at line 251. Could you please verify these mutations are harmless. Though I think the processing would be more robust if we treated body as immutable.
There was a problem hiding this comment.
There's no concerns around mutating the body here since this is already deepcopied on line 233.
This was also already mutating the conditions list (specifically, the copy of the original conditions list from line 233) since the conditions list is created (as a default) during schema validation:
Lines 92 to 104 in 2d55ce8
The branch to set default or return a default value should never be set, other than potentially during tests.
I agree that we should make this immutable moving forward. I think that's part of a larger change, though. The design of the splitter will have to change, and even things like alias_expr mutate the body:
Line 187 in 2d55ce8
There was a problem hiding this comment.
ok, I did not see line 233. If we are not introducing a mutation here. then it is fine
There was a problem hiding this comment.
Yeah, I think the fact that it's not obvious is a smell in and of itself. I think there are two problems here:
- If splitter is the component that is concerned with the mutations to the query, it should be the one doing the copy before it yields control to other parts of the codebase, not relying on them to act in its best interests. The way the code is structured right now makes that really easy to get wrong, and I think your concern around it is totally justified.
- In general the splitter is implemented in a weird part of the query execution pipeline today. If splitting occurred after condition rewriting and things (as noted above), it wouldn't be an issue. The splitter would probably still have to make copies of the original payload, but it'd probably be more obvious why.
| else: | ||
| final = False | ||
| if 'sample' not in body: | ||
| body['sample'] = settings.TURBO_SAMPLE_RATE |
There was a problem hiding this comment.
This will eventually move to the events dataset.
| joins.append(u'ARRAY JOIN {}'.format(body['arrayjoin'])) | ||
| join_clause = ' '.join(joins) | ||
| if 'sample' not in body and config_sample is not None: | ||
| body['sample'] = config_sample |
There was a problem hiding this comment.
I'm not sure how helpful this configuration parameter is, since it changes the response so drastically. I'd like to remove it but this preserves the existing behavior for now.
| prewhere_clause = '' | ||
| if prewhere_conditions: | ||
| prewhere_clause = u'PREWHERE {}'.format(util.conditions_expr(dataset, prewhere_conditions, body)) | ||
| body['conditions'] = list(filter(lambda cond: cond not in prewhere_conditions, body['conditions'])) |
There was a problem hiding this comment.
This will probably stay shared but PREWHERE_KEYS will become a dataset configuration.
| 'num_projects': len(project_ids), | ||
| 'sample': sample, | ||
| 'sample': body.get('sample'), | ||
| } |
There was a problem hiding this comment.
This is really just a grab bag of random attributes at the moment, we should figure out a more reasonable thing to do here.
|
|
||
| limitby_clause = '' | ||
| if 'limitby' in body: | ||
| limitby_clause = 'LIMIT {} BY {}'.format(*body['limitby']) |
There was a problem hiding this comment.
This was moved, but I'm pretty sure this won't actually work because it's not using column_expr. I didn't test it.
There was a problem hiding this comment.
Lines 370 to 372 in 2d55ce8
Almost everything here should be a move out of parse_and_run_query. Sorry if this wasn't clear from the description.
fpacifici
left a comment
There was a problem hiding this comment.
Could you please verify we won't have problems with split queries if we make changes to the body of the query (or better keep body immutable - not providing this guarantee will be a little weak when we will allow the datasets to do their own optimizations and splitting).
| ) | ||
|
|
||
|
|
||
| def format_query(dataset, body, table, prewhere_conditions, final) -> str: |
There was a problem hiding this comment.
any chance we could have type hints in the signature (together with the result). It would make it easier to understand the method body.
| ) | ||
|
|
||
|
|
||
| def format_query(dataset, body, table, prewhere_conditions, final) -> str: |
There was a problem hiding this comment.
This seems so much more readable. One question at your plan for the Query construct. Are you going to create a class that takes body, dataset, table, pre_where and that has a method that returns the query string?
Second, how will this play with multiple dataset? Is every dataset going to provide its Query construct to allow for dataset specific logic in query parsing? Assuming we will allow datasets to provide their own logic.
|
|
||
| def format_query(dataset, body, table, prewhere_conditions, final) -> str: | ||
| """Generate a SQL string from the parameters.""" | ||
| aggregate_exprs = [util.column_expr(dataset, col, body, alias, agg) for (agg, col, alias) in body['aggregations']] |
There was a problem hiding this comment.
@markstory you have a PR that changes this. You probably want to keep an eye on this one.
|
|
||
| join_clause = '' | ||
| if 'arrayjoin' in body: | ||
| join_clause = u'ARRAY JOIN {}'.format(body['arrayjoin']) |
There was a problem hiding this comment.
do we have to "unicodify" this one ?
There was a problem hiding this comment.
I'm not sure what this question means. I don't think we need to do any specific encoding here or anything. This will be a unicode string. The u prefix is actually unnecessary for both of these strings due to Python 3. Behaviorally, this is really just the same as before, with the exception that this removes some unnecessary list operations that weren't valid per the JSON schema.
There was a problem hiding this comment.
I was talking about body['arrayjoin']. But it seems it is ok.
|
|
||
| limitby_clause = '' | ||
| if 'limitby' in body: | ||
| limitby_clause = 'LIMIT {} BY {}'.format(*body['limitby']) |
| select_clause = u'SELECT {}'.format(', '.join(select_exprs)) | ||
|
|
||
| from_clause = u'FROM {}'.format(table) | ||
| body['conditions'].append(('project_id', 'IN', project_ids)) |
There was a problem hiding this comment.
I suspect you may run into problems if you update the body of the query during split queries since the body is passed down by the split_query wrapper to "parse_and_run_query".
Same above at line 251. Could you please verify these mutations are harmless. Though I think the processing would be more robust if we treated body as immutable.
fpacifici
left a comment
There was a problem hiding this comment.
The typing on the format method are not a blocker. Still welcome if you want to add it before committing.
| ) | ||
|
|
||
|
|
||
| def format_query(dataset, body, table, prewhere_conditions, final) -> str: |
|
|
||
| join_clause = '' | ||
| if 'arrayjoin' in body: | ||
| join_clause = u'ARRAY JOIN {}'.format(body['arrayjoin']) |
There was a problem hiding this comment.
I was talking about body['arrayjoin']. But it seems it is ok.
| select_clause = u'SELECT {}'.format(', '.join(select_exprs)) | ||
|
|
||
| from_clause = u'FROM {}'.format(table) | ||
| body['conditions'].append(('project_id', 'IN', project_ids)) |
There was a problem hiding this comment.
ok, I did not see line 233. If we are not introducing a mutation here. then it is fine
tkaemming
left a comment
There was a problem hiding this comment.
The typing on the format method are not a blocker. Still welcome if you want to add it before committing.
I think it's a good idea and I had it partially started on another branch, I'll bring over what I can.
| select_clause = u'SELECT {}'.format(', '.join(select_exprs)) | ||
|
|
||
| from_clause = u'FROM {}'.format(table) | ||
| body['conditions'].append(('project_id', 'IN', project_ids)) |
There was a problem hiding this comment.
Yeah, I think the fact that it's not obvious is a smell in and of itself. I think there are two problems here:
- If splitter is the component that is concerned with the mutations to the query, it should be the one doing the copy before it yields control to other parts of the codebase, not relying on them to act in its best interests. The way the code is structured right now makes that really easy to get wrong, and I think your concern around it is totally justified.
- In general the splitter is implemented in a weird part of the query execution pipeline today. If splitting occurred after condition rewriting and things (as noted above), it wouldn't be an issue. The splitter would probably still have to make copies of the original payload, but it'd probably be more obvious why.
Actually, I'm going to do both of these (splitter changes and type addition) in two follow ups after this because I want to move |
## Bump sentry-conventions to 0.11.0 Updates `sentry-conventions` from 0.10.0 to [0.11.0](https://github.com/getsentry/sentry-conventions/releases/tag/0.11.0). ### Changes in 0.11.0 **New Features** - Add `session.id` attribute (#412) - Update several attributes to latest OTel representation (#418) - Add `db.operation.batch.size` (#407) - Add `app.vitals.start.prewarmed` attribute (#379) - Add and deprecate runtime context attributes (#383) - Add Android Runtime (ART) GC and memory attributes (#382) - Add `db.stored_procedure.name` and ensure span name attribute consistency (#398) - Add Cloudflare SDK attributes (#392) - Add missing GCP and FaaS attributes (#403) **Bug Fixes** - Set `pii: 'maybe'` on `faas` string attributes (#415) - Add missing Cloudflare visibility attributes (#408) **Internal** - Remove `sdks` field from attribute schema and definitions (#410)
This change isolates the query formatting concerns from
parse_and_run_query.