Repository navigation
fix(data): keep GeoPackage rows without geometry #1673
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
Changes from all commits
8911ef7
2334e48
d7eb5ab
d0d1c3d
c587620
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -329,6 +329,7 @@ impl FeatureSource for GpkgMemSource { | |||||||||||||
| &self.info | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Keeps rows without geometry in whole reads; they have no location to match an extent. | ||||||||||||||
| fn read(&self, req: &Request) -> Result<Batch> { | ||||||||||||||
| let limit = clamp_limit(req.limit); | ||||||||||||||
| let native = req.extent.map(|e| self.reproject.extent_from_lonlat(&e)); | ||||||||||||||
|
|
@@ -337,16 +338,20 @@ impl FeatureSource for GpkgMemSource { | |||||||||||||
| self.db | ||||||||||||||
| .rows(&self.table, |rowid, r| { | ||||||||||||||
| let blob = match r.get(self.geom_col) { | ||||||||||||||
| Some(Val::Blob(b)) => b, | ||||||||||||||
| Some(Val::Null) | None => return ControlFlow::Continue(()), | ||||||||||||||
| Some(Val::Blob(b)) if !super::gpkg_blob::is_whole_empty_blob(b) => b, | ||||||||||||||
|
Contributor
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. Same bug in the browser reader's
Suggested change
Member
Author
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. Same as on |
||||||||||||||
| // A row without geometry (NULL, or an empty one) is kept in a whole read; it | ||||||||||||||
| // has no place to match an extent. | ||||||||||||||
| Some(Val::Null | Val::Blob(_)) | None => { | ||||||||||||||
| if native.is_none() && sample.wants() { | ||||||||||||||
| sample.keep(Feature { id: rowid as u64, geometry: None, attrs: self.attrs(r) }); | ||||||||||||||
| } | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| Some(_) => { | ||||||||||||||
| skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| }; | ||||||||||||||
| if super::gpkg_blob::is_empty_blob(blob) { | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| let Some((envelope, body)) = super::gpkg_blob::split_blob(blob) else { | ||||||||||||||
| skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
|
|
@@ -392,35 +397,36 @@ impl FeatureSource for GpkgMemSource { | |||||||||||||
| let mut failed = None; | ||||||||||||||
| self.db | ||||||||||||||
| .rows(&self.table, |rowid, r| { | ||||||||||||||
| let blob = match r.get(self.geom_col) { | ||||||||||||||
| Some(Val::Blob(b)) => b, | ||||||||||||||
| Some(Val::Null) | None => return ControlFlow::Continue(()), | ||||||||||||||
| let geometry = match r.get(self.geom_col) { | ||||||||||||||
| Some(Val::Blob(b)) if !super::gpkg_blob::is_whole_empty_blob(b) => { | ||||||||||||||
|
Contributor
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. Same bug in the browser reader's
Suggested change
Member
Author
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. Same here: a malformed empty-flagged blob takes this arm, |
||||||||||||||
| let Some((envelope, body)) = super::gpkg_blob::split_blob(b) else { | ||||||||||||||
| out.skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| }; | ||||||||||||||
| if let (Some(q), Some(e)) = (&native, envelope) | ||||||||||||||
| && !e.intersects(q) | ||||||||||||||
| { | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| let Some(geometry) = wkb::decode(body).and_then(|g| self.reproject.geometry(g)) else { | ||||||||||||||
| out.skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| }; | ||||||||||||||
| if !in_request(&geometry, &req) { | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| Some(geometry) | ||||||||||||||
| } | ||||||||||||||
| // Without geometry (NULL, or an empty one): kept unless an extent is asked for. | ||||||||||||||
| Some(Val::Null | Val::Blob(_)) | None if native.is_none() => None, | ||||||||||||||
| Some(Val::Null | Val::Blob(_)) | None => return ControlFlow::Continue(()), | ||||||||||||||
| Some(_) => { | ||||||||||||||
| out.skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| }; | ||||||||||||||
| if super::gpkg_blob::is_empty_blob(blob) { | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| let Some((envelope, body)) = super::gpkg_blob::split_blob(blob) else { | ||||||||||||||
| out.skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| }; | ||||||||||||||
| if let (Some(q), Some(e)) = (&native, envelope) | ||||||||||||||
| && !e.intersects(q) | ||||||||||||||
| { | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| let Some(geometry) = wkb::decode(body).and_then(|g| self.reproject.geometry(g)) else { | ||||||||||||||
| out.skipped += 1; | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| }; | ||||||||||||||
| if !in_request(&geometry, &req) { | ||||||||||||||
| return ControlFlow::Continue(()); | ||||||||||||||
| } | ||||||||||||||
| out.matched += 1; | ||||||||||||||
| match chunks.push(Feature { id: rowid as u64, geometry: Some(geometry), attrs: self.attrs(r) }) { | ||||||||||||||
| match chunks.push(Feature { id: rowid as u64, geometry, attrs: self.attrs(r) }) { | ||||||||||||||
| Ok(()) => ControlFlow::Continue(()), | ||||||||||||||
| Err(e) => { | ||||||||||||||
| failed = Some(e); | ||||||||||||||
|
|
||||||||||||||
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.
Bug (high confidence): a blob flagged empty whose header is cut off or has an undefined envelope code fails the guard on the first arm, so the next arm keeps it as a geometry-less feature in whole reads and visits. It is neither skipped nor counted, which contradicts the doc comment on
is_whole_empty_bloband the PR's claim that malformed geometry is still skipped and counted.geopackage_keeps_rows_without_geometryexpects rows 603–605 (47500011,4750001b0f000000,475000130f000000) to be skipped, giving(602, 3). With this code they are kept, giving 605 features andskipped == 0, so the test should fail. I have not run it. This also reverses the earlier thread on validating empty-flagged blobs before retaining rows (marked resolved in 2334e48, and claimed addressed in d7eb5ab). Unreadable empty blobs should fall through toRow::Bad: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 one doesn't hold. The first arm is
Blob(b) if !is_whole_empty_blob(b), negated. A cut-off or undefined-code blob failsis_whole_empty_blob, so it takes this first arm and goes tosplit_blob.split_blobreturnsNonefor a blob flagged empty, so the row isRow::Bad, skipped and counted. It never reaches the arm that keeps a row without geometry.geopackage_keeps_rows_without_geometryasserts(602, 3)for rows 603–605 in both readers, and it passes on this head.