packages feed

clickhouse-migrate-test-support-0.1.0.0: test/fixtures/kansoku/original/0017_replay_safe_sources.sql

-- EP-30: one immutable logical event before analytical joins/aggregations.
-- Administrative migration: keep the writer stopped throughout. A failed run
-- can be reapplied: replacement states are rebuilt from canonical retained rows,
-- rather than appended to partially populated aggregate tables.
CREATE OR REPLACE VIEW analytics.events_canonical
AS SELECT *, properties_json FROM analytics.events_raw FINAL;

CREATE OR REPLACE VIEW analytics.identity_bindings
AS
SELECT
    account_id,
    signal_kind,
    signal_value,
    user_id,
    observed_at,
    multiIf(
        signal_kind = 'email_sha256', 0.95,
        signal_kind = 'cookie',       0.90,
        signal_kind = 'device',       0.80,
        signal_kind = 'fingerprint',  0.50,
        0.0) AS confidence
FROM analytics.identity_signals FINAL;

-- Conflicting bindings stay visible rather than being collapsed by a
-- latest-wins table.
CREATE OR REPLACE VIEW analytics.identity_conflicts
AS
SELECT
    account_id,
    signal_kind,
    signal_value,
    groupUniqArray(user_id) AS user_ids,
    uniqExact(user_id) AS user_count,
    count() AS observation_count,
    max(observed_at) AS last_observed_at
FROM analytics.identity_signals FINAL
GROUP BY account_id, signal_kind, signal_value
HAVING user_count > 1;

CREATE OR REPLACE VIEW analytics.events_resolved_v3
AS
WITH event_signal_users AS
(
    SELECT
        s.account_id AS account_id,
        s.event_id AS event_id,
        argMax(b.user_id, tuple(b.confidence, b.observed_at)) AS stitched_user
    FROM
    (
        SELECT
            account_id,
            event_id,
            sig.1 AS signal_kind,
            sig.2 AS signal_value
        FROM analytics.events_canonical
        ARRAY JOIN arrayFilter(
            kv -> kv.2 != '',
            arrayConcat(
                [('cookie', anonymous_id)],
                JSONExtractKeysAndValues(identifiers, 'String'))) AS sig
        WHERE user_id = ''
    ) AS s
    INNER JOIN analytics.identity_bindings AS b
        ON  b.account_id = s.account_id
        AND b.signal_kind = s.signal_kind
        AND b.signal_value = s.signal_value
    WHERE b.confidence >= 0.50
    GROUP BY s.account_id, s.event_id
)
SELECT
    e.event_id AS event_id,
    e.event_name AS event_name,
    e.occurred_at AS occurred_at,
    e.url AS url,
    e.account_id AS account_id,
    multiIf(
        e.user_id != '', e.user_id,
        r.stitched_user != '', r.stitched_user,
        e.anonymous_id) AS canonical_user,
    e.properties_json AS properties_json
FROM analytics.events_canonical AS e
LEFT JOIN event_signal_users AS r
    ON r.account_id = e.account_id AND r.event_id = e.event_id;


DROP TABLE IF EXISTS analytics.mv_daily_event_counts;
DROP TABLE IF EXISTS analytics.mv_page_views_by_route;
DROP TABLE IF EXISTS analytics.daily_event_counts_rebuild;
DROP TABLE IF EXISTS analytics.page_views_by_route_rebuild;

CREATE TABLE analytics.daily_event_counts_rebuild
(
    account_id String,
    day Date,
    event_name String,
    events AggregateFunction(uniqExact, String)
)
ENGINE = AggregatingMergeTree
ORDER BY (account_id, day, event_name);

CREATE TABLE analytics.page_views_by_route_rebuild
(
    account_id String,
    day Date,
    route String,
    views AggregateFunction(uniqExact, String)
)
ENGINE = AggregatingMergeTree
ORDER BY (account_id, day, route);

INSERT INTO analytics.daily_event_counts_rebuild
SELECT account_id, toDate(occurred_at) AS day, event_name,
       uniqExactState(event_id) AS events
FROM analytics.events_canonical
GROUP BY account_id, day, event_name;

INSERT INTO analytics.page_views_by_route_rebuild
SELECT account_id, toDate(occurred_at) AS day, path(url) AS route,
       uniqExactState(event_id) AS views
FROM analytics.events_canonical
WHERE event_name = 'page_view'
GROUP BY account_id, day, route;

-- Verify complete replacement groups before publishing either table.
SELECT throwIf(count() > 0, 'canonical daily-event rebuild mismatch')
FROM
(
    SELECT account_id, toDate(occurred_at) AS day, event_name, count() AS n
    FROM analytics.events_canonical GROUP BY account_id, day, event_name
) AS canonical
FULL OUTER JOIN
(
    SELECT account_id, day, event_name, uniqExactMerge(events) AS n
    FROM analytics.daily_event_counts_rebuild GROUP BY account_id, day, event_name
) AS replacement USING (account_id, day, event_name)
WHERE ifNull(canonical.n, 0) != ifNull(replacement.n, 0);

SELECT throwIf(count() > 0, 'canonical page-view rebuild mismatch')
FROM
(
    SELECT account_id, toDate(occurred_at) AS day, path(url) AS route, count() AS n
    FROM analytics.events_canonical WHERE event_name = 'page_view'
    GROUP BY account_id, day, route
) AS canonical
FULL OUTER JOIN
(
    SELECT account_id, day, route, uniqExactMerge(views) AS n
    FROM analytics.page_views_by_route_rebuild GROUP BY account_id, day, route
) AS replacement USING (account_id, day, route)
WHERE ifNull(canonical.n, 0) != ifNull(replacement.n, 0);

-- Each swap is atomic. The writer stays stopped across both. On interruption,
-- discard/rebuild the temporary tables and swap again from canonical data;
-- this works whether the public name has its old or its replacement schema.
EXCHANGE TABLES analytics.daily_event_counts AND analytics.daily_event_counts_rebuild;
EXCHANGE TABLES analytics.page_views_by_route AND analytics.page_views_by_route_rebuild;
DROP TABLE analytics.daily_event_counts_rebuild;
DROP TABLE analytics.page_views_by_route_rebuild;

-- Trigger inputs are the current inserted block. Distinct-ID states compose
-- across arbitrary retry batches, parts, restarts and old archive replays.
CREATE MATERIALIZED VIEW analytics.mv_daily_event_counts
TO analytics.daily_event_counts AS
SELECT account_id, toDate(occurred_at) AS day, event_name,
       uniqExactState(event_id) AS events
FROM analytics.events_raw
GROUP BY account_id, day, event_name;

CREATE MATERIALIZED VIEW analytics.mv_page_views_by_route
TO analytics.page_views_by_route AS
SELECT account_id, toDate(occurred_at) AS day, path(url) AS route,
       uniqExactState(event_id) AS views
FROM analytics.events_raw
WHERE event_name = 'page_view'
GROUP BY account_id, day, route;