Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions crates/data/src/source/duckdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ impl Database {
match self {
Database::File(p) => p.file_stem().and_then(|s| s.to_str()).unwrap_or("database").to_string(),
Database::Postgres(d) => {
let get = |k: &str| d.split_whitespace().find_map(|kv| kv.strip_prefix(&format!("{k}=")).map(str::to_string));
get("dbname")
// Quoted values (`dbname='my gis'`) as libpq reads them.
super::pgconn::keyword_pairs(d)
.into_iter()
.find_map(|(k, v)| (k == "dbname").then_some(v))
.unwrap_or_else(|| d.rsplit('/').next().unwrap_or("postgres").split('?').next().unwrap_or("postgres").to_string())
}
Database::Mysql(a) => super::myconn::database_name(a).unwrap_or_else(|| "mysql".into()),
Expand Down Expand Up @@ -940,6 +942,9 @@ mod tests {
assert_eq!(d, Database::Postgres("host=db.example dbname=gis user=me".into()));
assert_eq!(d.label(), "postgres:host=db.example dbname=gis user=me");
assert_eq!(d.short(), "gis");
// Quoted values (as `pg.connect` writes them) are named without their quotes, whole.
assert_eq!(Database::Postgres("host='h' dbname='my gis' user='o\\'neil'".into()).short(), "my gis");
assert_eq!(Database::Postgres("host='h' dbname='gis'".into()).short(), "gis");
assert_eq!(password(Some(&d)).as_deref(), Some("pw1"));
assert_eq!(Database::parse(&d.label()), d);
let u = Database::parse("postgresql://me:pw2@db.example:5433/osm?sslmode=require");
Expand Down
543 changes: 505 additions & 38 deletions crates/data/src/source/postgis.rs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions crates/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ tiny-skia = { workspace = true }
# Time slider animations as GIFs (exported by the desktop app only).
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
image = { workspace = true, features = ["gif"] }

# The live PostGIS tests make their own tables, and change rows as someone else would.
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
postgres = { workspace = true }
2 changes: 2 additions & 0 deletions crates/engine/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod layout;
pub mod navigate;
pub mod nldi;
pub mod notebook;
pub mod pg;
pub mod plugins;
pub mod portals;
pub mod presentation;
Expand Down Expand Up @@ -98,6 +99,7 @@ pub fn all() -> Vec<CommandSpec> {
v.extend(history::commands());
v.extend(share::commands());
v.extend(sql::commands());
v.extend(pg::commands());
v.extend(plugins::commands());
v.extend(sharing::commands());
v.extend(stac::commands());
Expand Down
465 changes: 465 additions & 0 deletions crates/engine/src/cmd/pg.rs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions crates/engine/src/cmd/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ const NEVER: &[&str] = &[
"s3.disconnect",
"earthdata.login",
"earthdata.logout",
"pg.connect",
"pg.disconnect",
// Writes to a database: the user's to do, asked each time.
"pg.saveEdits",
];

/// Commands that write files without naming one in their parameters.
Expand All @@ -309,6 +313,9 @@ const DATABASES: &[&str] = &[
"sql.schema",
"raster.zarrInfo",
"layer.addZarr",
"pg.tables",
"pg.addLayer",
"pg.discardEdits",
"zarr.series",
"zarr.reduce",
"cube.trend",
Expand Down
51 changes: 51 additions & 0 deletions crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,14 @@ pub struct Session {
/// Edits per layer id since the project was last saved (Edit › Status). A failed command's
/// are rolled back; undo doesn't subtract them, since they count edits made, not the net change.
pub edit_counts: std::collections::HashMap<u32, cmd::design::EditCounts>,
/// The PostGIS database that layers loaded for editing work with (`pg.connect`): its
/// connection, never with its password. Kept for the session, not in the project.
pub postgis: Option<String>,
/// The PostGIS layers' row versions that `pg.saveEdits` saved over: a layer back on one
/// (undone, or restored by a failed command, past a save) would write its edits again.
/// Outside the project, so undo keeps it.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) pg_saved_over: Vec<rustgis_map::source::Versions>,
/// The feature cache fills itself after projection changes (Data › Feature Cache).
pub auto_cache: bool,
/// Mensuration results (Analyze › Mensuration › Results).
Expand Down Expand Up @@ -687,6 +695,46 @@ fn edited_streamed(before: &Project, after: &Project) -> Option<String> {
None
}

/// New features of a layer loaded from PostGIS never keep the key of a row it loaded (one
/// deleted since, whose id an editing tool can hand out again): saved, they would be that row,
/// changed. They take ids past every key instead. Layers whose rows were loaded or saved again by
/// the command (their versions replaced) are left alone.
fn fresh_postgis_ids(before: &Project, after: &mut Project) {
for (mb, ma) in before.maps.iter().zip(after.maps.iter_mut()) {
for la in ma.layers.iter_mut() {
let Some(pg) = &la.postgis else { continue };
let Some(lb) = mb.layers.iter().find(|l| l.id == la.id) else { continue };
let same_rows = lb.postgis.as_ref().is_some_and(|b| b.versions.shares(&pg.versions));
if !same_rows || la.dataset.features.shares(&lb.dataset.features) {
continue;
}
let had: std::collections::HashSet<u64> = lb.dataset.features.iter().map(|f| f.id).collect();
let reused: Vec<usize> = (0..la.dataset.features.len())
.filter(|&i| la.dataset.features.get(i).is_some_and(|f| !had.contains(&f.id) && pg.versions.get(f.id).is_some()))
.collect();
if reused.is_empty() {
continue;
}
// Past the largest key that isn't negative (a negative key's bits are the largest).
let mut next = la
.dataset
.features
.iter()
.map(|f| f.id)
.chain(pg.versions.iter().map(|(k, _)| k))
.filter(|&k| k.cast_signed() >= 0)
.max()
.unwrap_or(0);
for i in reused {
next = next.saturating_add(1);
if let Some(f) = la.dataset.features.get_mut(i) {
f.id = next;
}
}
}
}
}

/// Why command `label` can't edit streamed layer `layer`, refused up front or undone after.
fn streamed_refusal(label: &str, layer: &str) -> String {
format!("{label}: {layer} is read in place from its file; Load All Features (layer.loadAll) to edit it")
Expand Down Expand Up @@ -752,6 +800,8 @@ impl Session {
topology_errors: Vec::new(),
measurements: Vec::new(),
auto_cache: false,
postgis: None,
pg_saved_over: Vec::new(),
undo: Vec::new(),
redo: Vec::new(),
merge: None,
Expand Down Expand Up @@ -998,6 +1048,7 @@ impl Session {
// session's own flag says so, not the answer, which a plugin's command could forge.
(Ok(_), Some(_)) if std::mem::take(&mut self.request_only) => {}
(Ok(_), Some(mut snap)) => {
fresh_postgis_ids(&snap, &mut self.project);
// An edit may change the dates the time slider's window filters on: bound layers
// whose features or fields the command changed (features are shared with the
// snapshot until written) take the window again.
Expand Down
Loading
Loading