-
Notifications
You must be signed in to change notification settings - Fork 145
Add row-level geo distance check and fix documentation examples #1510
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
Open
simplegaurav
wants to merge
3
commits into
databrickslabs:main
Choose a base branch
from
simplegaurav:feature/maritime-geofencing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from collections.abc import Callable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import operator as py_operator | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Literal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1340,3 +1341,104 @@ def is_geo_within( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return _has_topological_relationship_precise( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| column, reference_geometry, convert_column, convert_reference_geometry, "WITHIN" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @requires_dbr_version("17.1") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @register_rule("row") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def is_geo_within_distance( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| column: str | Column, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reference_geometry: str | bytes | Column, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance: int | float | str | Column, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| convert_column: bool = False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| convert_reference_geometry: bool = False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> Column: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Checks if the column geography is within a geodesic distance of the reference geography using `st_distance`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The distance is measured in meters along the WGS 84 ellipsoid, so the check is meaningful for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global data where planar `GEOMETRY` distances are not. A value is reported when the shortest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance between it and the reference geography is strictly greater than *distance*. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Both the target column and the reference geometry are always handled as `GEOGRAPHY`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| When conversion is requested (*convert_column* or *convert_reference_geometry* set to True), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *try_to_geography* is applied to parse the value from any supported format (WKT, WKB, EWKT, EWKB, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GeoJSON). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| See https://docs.databricks.com/aws/en/sql/language-manual/functions/try_to_geography for details. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| When conversion is not requested, the input is assumed to already hold a native `GEOGRAPHY` value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| column: Column to check. Null values are skipped for validation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reference_geometry: Reference geography as a literal WKT/EWKT/GeoJSON string or WKB/EWKB bytes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value, or a Column expression (e.g. *F.col('col_name')*) to reference another column. A | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| plain string is always treated as a literal, not a column name. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance: Maximum allowed distance in meters. Accepts a non-negative number, a Column | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expression (e.g. *F.col('radius_m')*), or a string SQL expression evaluated against the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input DataFrame. Rows where the distance expression evaluates to null are skipped. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| convert_column: When True, *try_to_geography* is applied to convert column values to GEOGRAPHY. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| When False (default), the column is assumed to already hold a native GEOGRAPHY value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| convert_reference_geometry: When True, *try_to_geography* is applied to convert the reference | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| geometry to GEOGRAPHY. When False (default), the reference geometry is assumed to already | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hold a native GEOGRAPHY value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Column object indicating whether values in the input column are farther than *distance* meters | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from the reference geography. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| InvalidParameterError: If *distance* is a boolean, or a numeric literal that is negative, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NaN or infinite. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Note: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This function requires Databricks serverless compute or runtime 17.1 or above. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # `bool` is a subclass of `int`, so it would otherwise slip through as a 0/1 metre radius. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(distance, bool) or ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isinstance(distance, (int, float)) and not (math.isfinite(distance) and distance >= 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise InvalidParameterError(f"'distance' must be a finite, non-negative number of meters, got {distance!r}.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| col_str_norm, col_expr_str, col_expr = get_normalized_column_and_expr(column) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref_col = reference_geometry if isinstance(reference_geometry, Column) else F.lit(reference_geometry) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| col_geog = F.call_function("try_to_geography", col_expr) if convert_column else col_expr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref_geog = F.call_function("try_to_geography", ref_col) if convert_reference_geometry else ref_col | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance_expr = get_limit_expr(distance) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # `try_to_geography` yields NULL for values that fail to parse. The column and the reference are | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # reported separately so the error message points at the value the user has to fix. Null input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # values are skipped before these are evaluated, so a NULL here always means "unparseable". | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| col_invalid = col_geog.isNull() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ref_invalid = ref_geog.isNull() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_too_far = F.call_function("st_distance", col_geog, ref_geog) > distance_expr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| condition = F.when(col_expr.isNull(), F.lit(None)).otherwise(col_invalid | ref_invalid | is_too_far) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
ghanse marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # How the offending value is rendered depends on the input contract. When the column is converted, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # it holds a WKT/WKB-style value that casts to string losslessly, and the raw text is what the user | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # needs to see - `st_astext` would be NULL on exactly the unparseable values the message is about. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # When conversion is off the column is already a native GEOGRAPHY, which has no string cast, so the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # value has to be rendered with `st_astext`; a NULL there is already excluded by the null guard. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_value_col = col_expr.cast("string") if convert_column else F.call_function("st_astext", col_geog) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalid_column_message = F.concat_ws( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| F.lit("value `"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_value_col, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| F.lit(f"` in column `{col_expr_str}` is not a valid geography"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| invalid_reference_message = F.lit(f"reference geometry for column `{col_expr_str}` is not a valid geography") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| too_far_message = F.concat_ws( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| F.lit("value `"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text_value_col, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| F.lit(f"` in column `{col_expr_str}` is farther than "), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance_expr.cast("string"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| F.lit(" meters from the reference geometry"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1420
to
+1436
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may not leave the most useful messages if the user passes WKB/EWKB with
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return make_condition( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| condition, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| F.when(col_invalid, invalid_column_message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .when(ref_invalid, invalid_reference_message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .otherwise(too_far_message), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alias=f"{col_str_norm}_is_not_within_distance_from_reference_geometry", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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
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.
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.
math.isfinitemay overflow on very large input distances. We might want to catch this and raiseInvalidParameterError?