-- Seed for the Sql driver's PostgreSQL live gate (PostgresReadTests), applied once by the -- compose `postgres-seed` one-shot as the superuser against the OWNED `otopcua-sql-pg` container. -- -- The table shapes mirror the SQL Server fixture (SqlPollServerFixture) row for row — a present -- value, a present row with a NULL cell, and a wide-row table whose NEWEST row is deliberately not -- its first — so the same assertions can be made against both backends and a divergence between -- the two provider paths shows up as a failing assertion rather than as two suites that quietly -- test different things. -- -- ── TWO THINGS HERE ARE LOAD-BEARING, NOT SCAFFOLDING ───────────────────────────────────────── -- -- 1. The schema is owned by `sqlpoll_owner` and the suite connects as `otopcua`, which holds -- USAGE + SELECT and nothing else. That is the exact provisioning `PostgresDialect.ListSchemasSql` -- was written for: `information_schema.schemata` lists only schemas the current role OWNS, so a -- granted-not-owning read-only poller would see an empty schema list, `SqlCatalogLoader` would -- throw its "the catalog returned no schemas at all" grant diagnosis, and every tag would be -- rejected. Sourcing the schema list from `information_schema.tables` instead is what makes this -- work — and a fixture where the poller owned its own schema would pass either way, proving -- nothing. Do not "simplify" this by creating the tables as `otopcua`. -- -- 2. Every identifier is created UNQUOTED, so Postgres folds it to lower case in the catalog. The -- suite's case-folding leg authors the same names in UPPER case and expects them to resolve -- through `SqlCatalog.TryMatch` (exact-ordinal first, then a UNIQUE case-insensitive match, which -- substitutes the catalog's own spelling). Quoting a name here would make the catalog spelling -- mixed case and that leg would stop meaning anything. \set ON_ERROR_STOP on -- ── roles ───────────────────────────────────────────────────────────────────────────────────── -- \gexec runs the row the SELECT produced, and produces no row when the role already exists — -- the idempotent CREATE ROLE idiom. A DO block cannot be used for the login role: psql does not -- interpolate :'app_password' inside a dollar-quoted body. SELECT 'CREATE ROLE sqlpoll_owner NOLOGIN' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'sqlpoll_owner') \gexec SELECT 'CREATE ROLE otopcua LOGIN' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'otopcua') \gexec -- Set (or re-set) the password every run, so re-seeding with a different SQL_PG_PASSWORD converges -- rather than leaving the suite unable to log in. ALTER ROLE otopcua LOGIN PASSWORD :'app_password'; -- ── schema ──────────────────────────────────────────────────────────────────────────────────── -- Drop-and-recreate rather than merge-or-insert: the seed is an assertion contract, and a leftover -- row from an older revision of this file would make a passing test meaningless. The blast radius -- is bounded to this fixture-owned schema. DROP SCHEMA IF EXISTS sqlpoll CASCADE; CREATE SCHEMA sqlpoll AUTHORIZATION sqlpoll_owner; -- Everything below is created AS THE OWNER, not as the superuser — see note 1 above. SET ROLE sqlpoll_owner; -- Key-value (EAV) source: tag_name → num_value, with a source timestamp. CREATE TABLE sqlpoll.tag_values ( tag_name varchar(128) NOT NULL, num_value double precision NULL, sample_ts timestamp(3) NOT NULL ); INSERT INTO sqlpoll.tag_values (tag_name, num_value, sample_ts) VALUES ('Line1.Speed', 42.0, TIMESTAMP '2026-07-24 10:00:00'), ('Line1.Temp', NULL, TIMESTAMP '2026-07-24 10:00:01'), ('Line1.Pressure', 3.5, TIMESTAMP '2026-07-24 10:00:02'); -- Wide-row source: one row per station, several value columns. Station 8 is inserted LAST and is -- also the newest sample, while station 7 is first — so a topByTimestamp plan that lost its -- ORDER BY … DESC (or its LIMIT 1) returns a different, wrong value rather than a coincidentally -- right one. CREATE TABLE sqlpoll.latest_status ( station_id integer NOT NULL, oven_temp double precision NULL, pressure double precision NULL, sample_ts timestamp(3) NOT NULL ); INSERT INTO sqlpoll.latest_status (station_id, oven_temp, pressure, sample_ts) VALUES (7, 180.5, 1.2, TIMESTAMP '2026-07-24 10:00:00'), (9, NULL, 1.6, TIMESTAMP '2026-07-24 10:00:01'), (8, 210.25, 1.4, TIMESTAMP '2026-07-24 10:00:02'); -- Integer-keyed EAV source (STAB-24): the key column is `integer`, so a key bound as the default -- `text` fails with "operator does not exist: integer = text". Pins the untyped-bind fix in -- PostgresDialect.ConfigureParameter for the key-value IN-list path (the wide-row `whereColumn` -- path is covered by latest_status.station_id above). CREATE TABLE sqlpoll.int_tag_values ( tag_id integer NOT NULL, num_value double precision NULL, sample_ts timestamp(3) NOT NULL ); INSERT INTO sqlpoll.int_tag_values (tag_id, num_value, sample_ts) VALUES (101, 77.5, TIMESTAMP '2026-07-24 10:00:00'), (102, 12.0, TIMESTAMP '2026-07-24 10:00:01'); RESET ROLE; -- ── grants: USAGE + SELECT and nothing else ─────────────────────────────────────────────────── GRANT USAGE ON SCHEMA sqlpoll TO otopcua; GRANT SELECT ON ALL TABLES IN SCHEMA sqlpoll TO otopcua;