Version: [5673] Stratified Packager 0.3.0 Experimental

Version 0.3.0:
- Translations: **fixed the "Strata resolved" algorithm output staying
English in every language.** The label was authored with
`QT_TRANSLATE_NOOP`, which marks a string for extraction and hands
back the source text — correct for a table built at import time, wrong
here, because the outputs are declared from `initAlgorithm` and so are
already past the point where the plugin translator is installed. Its
three sibling outputs translate at that call and were unaffected. The
Portuguese and Spanish translations existed and were finished all
along; nothing ever looked them up, which is what makes this class of
bug read as a missing translation. The output now translates like its
siblings, and a test pins all four against a stubbed translator, since
English output cannot tell a translated label from an untranslated
one.
- Embedded project: **fixed a live virtual layer costing minutes to
hours per stratum.** Writing a project asks every layer for its
extent, and a virtual layer answers by running its query over the
stratum GeoPackage — where the packaged tables carry no index on the
columns the query joins, making that a nested-loop scan whose cost
grows faster than the stratum does. It was paid once per stratum, for
a value the written project does not even store. Adding one such layer
took a 93-stratum project from 35-55 seconds per stratum to 4 minutes
on its smallest and 2 hours on a large one, with under 11 seconds of
actual layer writing in each. The re-pointed layer is now left without
a computed extent; the recipient's QGIS derives one on demand,
locally, once. The delivered layer is otherwise unchanged — same
query, same styling, and it still opens with its fields and features.
Its subset string, until now silently dropped on the way into the
package, is carried over too.
- Processing: each stratum now also reports the **seconds spent
building its embedded project** — the phase total, plus a breakdown
naming every layer that build opens and the project write itself,
slowest first. Only the layer writes were timed before, which is why a
stall inside this phase survived two rounds of investigation. The two
lines are read as a pair: when their totals agree the cost is in a
layer the breakdown names, and when they diverge it is in the
tree/style/relation work between them.
- Embedded project: **remote layers are no longer re-fetched over the
network once per stratum.** Layers riding only in the project (WMS/XYZ
basemaps, annotations) were re-created for each stratum's project, and
re-creating a layer re-constructs its data provider from the URI —
which for a WMS source means a blocking GetCapabilities request. A
project with 21 remote basemaps therefore made ~2000 identical
requests across 93 strata, each one asking a server whose answer never
changes. Those layers are now serialized once per run and rebuilt from
that XML without ever opening their source, so the cost is independent
of the network: against an unreachable host, a layer measured at 42
seconds per stratum now takes milliseconds. The packaged projects are
unchanged — same sources, styles, names and tree positions — and still
resolve their remote layers on the recipient's machine.
- Processing: each stratum now closes with a **timing line** — total
seconds spent writing its layers, plus the slowest few by name.
Diagnosing a slow run from the log was not possible before:
`qgis_process` block-buffers stdout, so a harness that timestamps the
piped lines timestamps *flushes*, not work (dozens of per-layer lines
land in one millisecond-wide burst), and a multi-hour stall inside a
stratum could not be attributed to the layer that caused it. Carrying
the measured numbers in the message body makes the attribution survive
the buffering. One extra line per stratum; no behavior change.
- Shared sources: **fixed a §12 group member whose subset the
GeoPackage cannot evaluate still erroring once per stratum.** Grouping
keys on the data source *ignoring* the subset — a subset is a
per-member view over the same table, so it must not split the group —
and the embedded project re-applies each member's subset to the shared
table to tell the members apart. But a subset in the source provider's
dialect (a PostgreSQL `lpad()`, a schema-qualified table SQLite has no
schema for) can never run against a GeoPackage, so re-applying it
failed inside GDAL (`ERROR 1: Failed to prepare SQL … no such table
…`) one line per stratum, and the delivered layer showed no features.
A member whose subset does not compile as SQLite is now **excluded
from grouping**: it stages on its own, where the filter runs on the
source provider and is materialized into its table, so the packaged
project needs no runtime subset for it. Members whose subset the
GeoPackage *can* run still share one table, unchanged. The previous
entry gated re-apply to group members and fixed the *ungrouped* layer;
this fixes the *grouped* one it left erroring.
- Embedded project: **fixed a layer's subset string being re-applied
to the packaged GeoPackage, breaking the delivered project.** A subset
string is the *source* provider's SQL, but the embedded project
re-applies it to the layer's GeoPackage table. A filter written for
another provider — a PostgreSQL `::` cast, a schema-qualified table, a
function SQLite lacks — is accepted by the layer API and only fails
deeper, inside the data provider, where the failure surfaces as a bare
`ERROR 1: Failed to prepare SQL …` on stderr and nothing else. The
unusable filter was then stored in every stratum's project, so the
delivered packages opened with that layer showing no features (the
GeoPackage data was always correct). The subset is now re-applied only
where it does work: a shared-source group (§12), whose one table holds
every member's features and needs the subsets to tell them apart. An
ungrouped layer's table already *is* its subset view, so nothing is
filtered and nothing is demanded of the SQL. When a re-applied subset
does not compile as SQLite the run now says so in plain language,
naming the layer and the reason, instead of leaving a GDAL error as
the only clue.
- Embedded project: **fixed layers sharing a data source being left
out of the packaged project.** Only one member of a shared-source
group runs a write job, so the others have no result of their own; the
project builder looked their outcome up directly and silently skipped
every layer it did not find. Those layers were in the GeoPackage and
in the report, but absent from the `.qgz`/stored project — the run
gave no sign. They now inherit the group primary's outcome, as the
reporting side already did.
- Matching: **fixed the relation-chain memo never hitting.** It keyed
on the whole chain, but packaged layers rarely share a chain outright
— they share its leading hops and diverge at the final, layer-specific
relation — so no two lookups ever matched and every layer re-derived
hops another layer had just walked. On a 25-layer, 94-stratum project
that was 4092 hop queries where 279 distinct answers existed (`0
hit(s), 2325 miss(es)` in the run log). Hops are now memoized per
chain *prefix*, keyed also by the fields the next hop reads back,
since one relation traversed for two different onward keys yields two
different sets. Results are unchanged, and the run's `relation-chain
hop memo:` line reports the saving.
- Shared sources: a group whose members are **all** filtered no longer
loses the primary member's own view. Clearing the primary's subset is
what lets its table hold the union, but the plugin also forgot the
subset, so that one member showed the whole union in the packaged
project instead of its own features.
- Processing: **fixed every staging and template step being logged
twice.** `setProgressText` already writes its text to the algorithm's
log — the Processing dialog appends the progress text to its log panel
and `qgis_process` prints it to stdout — so the two steps that
additionally pushed the same line as an info message emitted each line
twice in the run log. The redundant push is gone; the progress label
and the log line itself are unchanged.

Version 0.2.0:
- Processing: **fixed a multi-part `FULL_PACKAGE_PATH` producing the
wrong output layout.** `FULL_PACKAGE_PATH` is the full package's zip
path, but the code used only its last component as the zip name
(flattened to the `OUTPUT_DIRECTORY` root) and the *whole* path as the
in-zip GeoPackage path, never consulting `GPKG_PATH_EXPRESSION` — so
`sub/dir/name` wrote `name.zip` at the root containing
`sub/dir/name.gpkg`. `FULL_PACKAGE_PATH` is now the (extensionless)
zip path in full, and the in-zip GeoPackage path follows
`GPKG_PATH_EXPRESSION` like every stratum (empty ⇒ the zip basename).
Single-part paths are unchanged.
- Reporting: **fixed the run report (`REPORT` output) being written in
the system locale encoding instead of UTF-8.** The per-zip
`report.csv` is stdlib-written UTF-8, but the run report rides a
Processing *feature sink* whose byte encoding the framework seeds from
the OS codepage (cp1252 on a pt-BR Windows), so accented
stratum/layer/detail text came out mojibake. The sink is now pinned to
UTF-8 (no BOM) for text destinations; GeoPackage and memory-layer
destinations are unaffected.
- Processing GUI: **fixed layers sharing a data source being dropped
from every run started from QGIS Desktop.** `LAYERS` was pre-filled
with layer ids so the dialog opened with the eligible layers
pre-checked, but the standard multiple-layers widget does not
round-trip identity — its wrapper rewrites the value into one *data
source* string per layer, and source-keyed resolution answers every
string of a shared source with the same first-matching layer. A
project with, say, eight layers over one table therefore packaged that
one layer eight times and silently lost the other seven, staging and
writing the survivor once per loss. `qgis_process` was never affected:
it omits `LAYERS` and resolves by id. `LAYERS` now carries no default,
so both paths resolve identically and even a partial selection of a
shared-source group is exact; the dialog shows an empty selection,
which already means "all eligible layers". A `LAYERS` value that still
arrives as source strings (a saved model or script) is reported rather
than silently collapsed.
- Matching: relation-chain resolution is now memoized per run (bounded
LRU). Every packaged layer sharing a chain used to re-derive the
identical key set for each stratum, and the SPEC §8.2 staging pass
re-derived what Phase B derived again — on a remote database that was
the dominant cost of a run. The memo is an optimization only: results
are unchanged, and a test asserts memoized and unmemoized resolutions
agree.
- Matching: relation-chain **intermediate** hop layers can now be
staged. They were the one read path staging never covered — read
straight from the project, once per `IN` chunk, per member, per
stratum — because staging only replaces a *packaged* layer's read
source. A hop layer whose provider is in `STAGE_PROVIDERS` is copied
once into a local GeoPackage holding just the fields the chains query,
with those fields indexed. Opt-in, and a staging failure falls back to
reading the project layer rather than failing the run.
- Virtual layers: materializing one whose sources are **not local**
now warns, naming the sources and the remedy. Such a layer's query is
re-executed per stratum, and because SQLite cannot push a correlated
subquery down to a database provider it pulls the source through row
by row — enough concurrent scans exhaust QGIS's 4-connection pool and
wedge the run. Detection only; routing is unchanged.
- SPEC: fixed the §4 `stage` row, which documented `auto` as staging
only `whole_export` layers and omitted the `STAGE_PROVIDERS` clause
that §8.2 states and the code has always implemented.
- Translations: **fixed the style-category options showing in English
in every locale.** The `STYLE_CATEGORIES` table resolved its 18 labels
at module import, and that module is loaded from the settings schema
while `classFactory` runs — before the plugin installs its translator
— so the labels froze to source English while every neighbouring label
translated normally. They are now authored with `QT_TRANSLATE_NOOP`
and translated on access, as the rest of the plugin already did. The
selectable tokens are unchanged.
- Development: `just deploy` now compiles the translation files first.
`.qm` files are git-ignored and removed by `just clean`, so a deploy
could mirror a plugin whose translator never installs, leaving the
entire plugin untranslated.
- Processing GUI: the progress bar now always reads **overall** run
progress — each GeoPackage write's internal 0–100 sweep is scaled into
that write's slice of the SPEC §8.4 progress bands (via
`QgsProcessingMultiStepFeedback` over a band-mapping proxy) instead of
clobbering the bar — and the text above the bar follows the step
actually in progress ("Staging layer …", "Writing template layer …",
per-stratum layer writes) instead of freezing at the last "Preparing
layer" line.

Version 0.1.1:
- plugins.qgis.org submission: the repository's Bandit scan flagged
the identifier-quoted `COUNT(*)` query in `toolbelt/gpkg.py` (B608).
The line now also carries a targeted `# nosec B608` — it already had
ruff's equivalent `# noqa: S608`, but Bandit does not honor `noqa`
comments. No behavior change.

yes

ivanlonel

2026-07-27T04:31:11.980615+00:00

3.40.0

4.99.0

None

yes

Version management

Plugin details