diff --git a/docs/architecture/README.md b/docs/architecture/README.md index f9c2441..8435335 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -25,7 +25,7 @@ Declarative architecture docs (English, present tense). These describe the syste | Command surface | Two commands, `site` and `bench`, named for the situation rather than for the tool's internal steps; they differ in one thing, the memory tuning, which only `site` applies | | Archive read access | `ALTER DEFAULT PRIVILEGES FOR ROLE scada_writer` set **before** `public.trends` is created, and before the writer first runs. The `semiplot_*` tables are granted table by table instead, since the superuser creates them | | Objects we add | `semiplot_tags`, `semiplot_groups`, `semiplot_pen_groups`, `semiplot_meta`, the `semiplot_register_new_pens()` function the viewer calls, and the vendor-shaped `public.trends` — no triggers, scheduled jobs, extensions, or other functions; `messages` is not created, the SCADA makes it | -| SemiPlot schema version | `semiplot_meta.schema_version`, `1` in this release, written by `provision` alone. A floor, not an equality: a viewer refuses a database below the version it needs and accepts one above, because the versions this repository issues grow by addition. | +| SemiPlot schema version | `semiplot_meta.schema_version`, `1` in this release, written by `provision` alone. A floor, not an equality: a viewer refuses a database below the version it needs and accepts one above, because the versions this repository issues grow by addition. The rule starts with the first installation: until then a schema change, a rename included, edits the `CREATE TABLE` alone and keeps `1`. | | Archive schema | Owned by Simple-Scada 2 and documented in the SemiPlot repository. This tool creates `public.trends` once, with the vendor's shape, connected as `scada_writer`, and never alters it afterwards; day partitions and every later change to the schema remain the vendor's | | Distribution | Binaries as GitHub release assets; the Linux binary also as `ghcr.io/semiteq/semibase`, `FROM scratch`, one file, pushed after the release exists; benches track `:latest` and a prerelease never moves it | | Backup method | `UNDECIDED` — commissioning-time, needs the customer's archive size | diff --git a/docs/architecture/provisioning.md b/docs/architecture/provisioning.md index 0340d42..8af2298 100644 --- a/docs/architecture/provisioning.md +++ b/docs/architecture/provisioning.md @@ -183,7 +183,7 @@ saves a pen through the same connection it reads history on, and a bug in it can A `semiplot_tags` row is keyed by `id`, the SCADA variable number. SemiPlot owns the settings in the row and none of the keys: its editor changes a pen and never adds one, deletes one or moves one onto another variable. The grant on that table is therefore column-level, -`GRANT SELECT, UPDATE (name, unit, format, color, line_style, enabled_on_start, scale_min, scale_max)`, +`GRANT SELECT, UPDATE (name, unit, format, color, line_style, enabled_on_start, scale_min_on_start, scale_max_on_start)`, and a table-level `UPDATE` would not do: it would let `UPDATE semiplot_tags SET id = ...` re-key a pen. A column grant is not a table grant, so `has_table_privilege('semiplot', 'semiplot_tags', 'UPDATE')` answers false, and no check asks it. A pen row is added only by @@ -235,7 +235,7 @@ checks a `LANGUAGE sql` body against the relations it names when the function is | Table | Holds | | --- | --- | -| `semiplot_tags` | One pen: `id` matching `trends.id`, `name`, `unit`, `format` as a .NET numeric format string the viewer applies and the server does not validate, `color` as `#RRGGBB`, `line_style` (0 interpolated, 1 stepped), `enabled_on_start`, and the `scale_min`/`scale_max` pair that bounds the pen's own Y axis. Both bounds `NULL` means autoscale | +| `semiplot_tags` | One pen: `id` matching `trends.id`, `name`, `unit`, `format` as a .NET numeric format string the viewer applies and the server does not validate, `color` as `#RRGGBB`, `line_style` (0 interpolated, 1 stepped), `enabled_on_start`, and the `scale_min_on_start`/`scale_max_on_start` pair that bounds the pen's own Y axis. Both bounds `NULL` means autoscale. A column with the `_on_start` suffix holds a start value: what the pen opens with in a new window and what the viewer's "Restore initial scale" returns to. Editing the pen on the chart changes the current view and never a start value, and a changed start value never changes a chart already open. Every other settings column is a live setting, which a running chart applies at its next catalogue read | | `semiplot_groups` | A group name. `GENERATED ALWAYS AS IDENTITY` rather than `serial`, so `semiplot` needs no `USAGE` on a sequence to insert one | | `semiplot_pen_groups` | Membership, `(pen_id, group_id)`. A pen may sit in several groups and in none: a pen with no row here is legal and the viewer shows it ungrouped | | `semiplot_meta` | One row: `schema_version`. A `singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton)` admits no second row, and the value is written by one `INSERT ... ON CONFLICT (singleton) DO UPDATE`, so a failure cannot leave the table empty and a re-run cannot leave two rows | @@ -318,7 +318,9 @@ inside the `semiplot` role's 30 s `statement_timeout` either way. This release writes `1`. A viewer states the minimum version it needs and refuses a database storing a **lower** one; a **higher** one is accepted, because the versions this repository issues grow by addition and a database carrying more than a viewer needs still carries what it needs. -When `semiplot_markers` arrives it bumps the number to 2, and a viewer that reads only the pen +The rule starts with the first installation. Until a site runs a provisioned database, a schema +change, a column rename included, edits the `CREATE TABLE` alone, issues no `ALTER`, and leaves the +number at `1`. When `semiplot_markers` arrives it bumps the number to 2, and a viewer that reads only the pen tables has to keep working against it, which an equality check would break. A removal is not signalled by this number. An older viewer meeting a dropped column gets 42703, diff --git a/docs/deployment.md b/docs/deployment.md index 26a1e9e..1f278b6 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -70,7 +70,7 @@ SemiBase разворачивается одной утилитой — `semibas не может — эту проверку утилита выполняет на каждом запуске. Строка `semiplot_tags` привязана к номеру переменной SCADA. SemiPlot меняет в ней только -настройки пера: имя, единицы, формат, цвет, стиль линии, показ при старте и границы шкалы. +настройки пера: имя, единицы, формат, цвет, стиль линии, показ при старте и начальные границы шкалы. Добавить перо напрямую, удалить его или сменить ему `id` роль не может, и утилита на каждом запуске проверяет, что эти три команды ей запрещены. Новые перья добавляет только функция `semiplot_register_new_pens()`, и только для переменных, которые SCADA уже записала в `trends`. @@ -98,7 +98,7 @@ DDL. Права `CREATE` в схеме `public` у роли `semiplot` нет | Таблица | Что хранит | | --------------------- | ------------------------------------------------------------------------------------------- | -| `semiplot_tags` | Перо: `id` из `trends`, имя, единицы, формат числа, цвет `#RRGGBB`, стиль линии, показ при старте, границы шкалы | +| `semiplot_tags` | Перо: `id` из `trends`, имя, единицы, формат числа, цвет `#RRGGBB`, стиль линии, показ при старте, начальные границы шкалы | | `semiplot_groups` | Имена групп перьев | | `semiplot_pen_groups` | Принадлежность пера группам. Перо может входить в несколько групп и ни в одну | | `semiplot_meta` | Одну строку: версию схемы | diff --git a/internal/provision/check_test.go b/internal/provision/check_test.go index 9f0e903..b7cb376 100644 --- a/internal/provision/check_test.go +++ b/internal/provision/check_test.go @@ -36,8 +36,8 @@ func TestPlotWriteProbesCoverTheWritableTables(t *testing.T) { func TestPlotTagsUpdateProbeWritesEverySettingsColumn(t *testing.T) { want := "UPDATE semiplot_tags SET name = name, unit = unit, format = format, color = color, " + - "line_style = line_style, enabled_on_start = enabled_on_start, scale_min = scale_min, " + - "scale_max = scale_max WHERE id = -1" + "line_style = line_style, enabled_on_start = enabled_on_start, " + + "scale_min_on_start = scale_min_on_start, scale_max_on_start = scale_max_on_start WHERE id = -1" probes := plotWriteProbes() if got := probes[probePosition(t, probes, "semiplot_tags", "UPDATE")].statement; got != want { t.Errorf("the semiplot_tags probe is %q, want %q", got, want) @@ -92,8 +92,8 @@ func TestWriteProbeFailure(t *testing.T) { } tags := writeProbeFailure(plotWriteProbe{"semiplot_tags", "UPDATE", ""}, errors.New("permission denied")) - wantTags := "GRANT SELECT, UPDATE (name, unit, format, color, line_style, enabled_on_start, scale_min, " + - "scale_max) ON semiplot_tags TO semiplot" + wantTags := "GRANT SELECT, UPDATE (name, unit, format, color, line_style, enabled_on_start, " + + "scale_min_on_start, scale_max_on_start) ON semiplot_tags TO semiplot" if !strings.Contains(tags.Error(), wantTags) { t.Errorf("the semiplot_tags repair does not prescribe the column grant: %q", tags) } diff --git a/internal/provision/schema.go b/internal/provision/schema.go index 4dc957d..ce5717f 100644 --- a/internal/provision/schema.go +++ b/internal/provision/schema.go @@ -26,8 +26,8 @@ var plotEditableTagColumns = []string{ "color", "line_style", "enabled_on_start", - "scale_min", - "scale_max", + "scale_min_on_start", + "scale_max_on_start", } const ( diff --git a/internal/provision/schema_test.go b/internal/provision/schema_test.go index ea11569..7572d7d 100644 --- a/internal/provision/schema_test.go +++ b/internal/provision/schema_test.go @@ -69,8 +69,8 @@ func TestSemiplotTagsColumns(t *testing.T) { "color", "line_style", "enabled_on_start", - "scale_min", - "scale_max", + "scale_min_on_start", + "scale_max_on_start", } got := columnNamesOf(semibase.SemiplotTagsSQL) if len(got) != len(want) { @@ -150,7 +150,7 @@ func TestSemiplotGrantStatements(t *testing.T) { statement string }{ {"semiplot_tags", "GRANT SELECT, UPDATE (name, unit, format, color, line_style, enabled_on_start, " + - "scale_min, scale_max) ON semiplot_tags TO semiplot"}, + "scale_min_on_start, scale_max_on_start) ON semiplot_tags TO semiplot"}, {"semiplot_groups", "GRANT SELECT, INSERT, UPDATE, DELETE ON semiplot_groups TO semiplot"}, {"semiplot_pen_groups", "GRANT SELECT, INSERT, UPDATE, DELETE ON semiplot_pen_groups TO semiplot"}, {"semiplot_meta", "GRANT SELECT ON semiplot_meta TO semiplot"}, diff --git a/sql/semiplot_tags.sql b/sql/semiplot_tags.sql index 7df8c32..bf0b0e1 100644 --- a/sql/semiplot_tags.sql +++ b/sql/semiplot_tags.sql @@ -1,17 +1,17 @@ -- docs/architecture/provisioning.md#the-semiplot-configuration-schema CREATE TABLE IF NOT EXISTS semiplot_tags ( - id integer PRIMARY KEY, - name text NOT NULL, - unit text, - format text, - color text, - line_style smallint NOT NULL DEFAULT 0, - enabled_on_start boolean NOT NULL DEFAULT true, - scale_min double precision, - scale_max double precision, + id integer PRIMARY KEY, + name text NOT NULL, + unit text, + format text, + color text, + line_style smallint NOT NULL DEFAULT 0, + enabled_on_start boolean NOT NULL DEFAULT true, + scale_min_on_start double precision, + scale_max_on_start double precision, CONSTRAINT semiplot_tags_scale_paired CHECK ( - (scale_min IS NULL) = (scale_max IS NULL) - AND (scale_min IS NULL OR scale_min < scale_max)), + (scale_min_on_start IS NULL) = (scale_max_on_start IS NULL) + AND (scale_min_on_start IS NULL OR scale_min_on_start < scale_max_on_start)), CONSTRAINT semiplot_tags_color_hex CHECK (color IS NULL OR color ~ '^#[0-9A-Fa-f]{6}$') );