[general]
name=Stratified Packager
description=Exports vector layers to multiple geopackages split by attributes or spatial intersection
category=
hasProcessingProvider=yes
about=Partitions the open project’s layers against a stratification layer — by attribute (following project relations) or spatially — and publishes one zipped GeoPackage per stratum, optionally with an embedded per-stratum QGIS project, layer styles, metadata and referenced style assets. Runs from the Processing Toolbox and from qgis_process.
icon=resources/images/icon.svg
tags=export,geopackage,gpkg,stratification,stratify,partition,split,divide,project
server=False

# credits and contact
author=Ivan Donisete Lonel
email=ivanlonel91@gmail.com
homepage=https://ivanlonel.github.io/stratified-packager/
repository=https://github.com/ivanlonel/stratified-packager
tracker=https://github.com/ivanlonel/stratified-packager/issues/

# QGIS context
deprecated=False
experimental=True
# plugin_dependencies=
qgisMinimumVersion=3.40
qgisMaximumVersion=4.99

# versioning
version=0.3.0
changelog=
 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.


