Data Platform/Data Lake/Edits/Mediawiki history incremental v1
wmf_mediawiki.mediawiki_history_incremental_v1
is a dataset available in the
Data Lake
that provides the editing and account history of all Wikimedia wikis — revisions, page events (moves, deletes, restores) and user events (account creation, renames, group changes).
The schema of this table is a narrower version of
Mediawiki history
(
wmf.mediawiki_history
). However, this table's data comes primarily from event streams from the
Event Platform
, allowing us to update it on a
daily
basis, rather than the monthly cadence of
wmf.mediawiki_history
. The goal is freshness: most analyses that today wait weeks for the monthly rebuild can read this table within roughly a day of an edit.
Each row carries a
source
column describing its provenance and quality:
-
source = 'events'— recent rows landed daily from event streams. Best-effort: a small number of fields settle over the following days (see FAQ ). -
source = 'snapshot'— rows projected fromwmf.mediawiki_historyby a monthly reconcile. Authoritative, with semantics identical towmf.mediawiki_history.
In practice you query the table once and get the freshest available view: recent activity as
'events'
rows, older activity as authoritative
'snapshot'
rows once the monthly reconcile has run. If you need strict, fully-reconciled semantics only, filter on
source = 'snapshot'
.
Consuming this table is different from snapshot-based tables like Mediawiki history . See the FAQ below for details.
Schema
This table may expose data about users or revisions that has since been RevisionDeleted or suppressed . The
revision_deleted_partscolumn lists which components of a revision (text,comment,user) were hidden. If the results of a query are to be made public, please honor these visibility flags and do not expose components marked as hidden.
This is an
Iceberg
dataset, partitioned by
months(event_timestamp)
. It has no
snapshot
Hive partition; see the
FAQ
for how to consume it.
| col_name | data_type | comment |
|---|---|---|
| source | string | Row provenance: events (daily delta) or snapshot (monthly merge). |
| wiki_id | string | Wiki identifier, e.g. enwiki, dewiki, eswiktionary. |
| event_entity | string | Entity type: revision, user, or page. |
| event_type | string | Event sub-type: create, edit, move, delete, etc. |
| event_timestamp | timestamp | When this event occurred. |
| event_user_id | bigint | Local MediaWiki user ID of the actor; NULL for anonymous users. |
| event_user_central_id | bigint | Global CentralAuth user ID of the actor; NULL for anonymous users. |
| event_user_text_historical | string | Username or IP at the time of the event. |
| event_user_is_bot_by_historical | array<string> | Bot classification methods at event time: name and/or group. |
| event_user_is_anonymous | boolean | True if the actor had no local user account at event time. |
| event_user_is_temporary | boolean | True if the actor was a temporary (auto-created) account. |
| event_user_is_permanent | boolean | True if the actor was a permanent registered account. |
| event_user_registration_timestamp | timestamp | When the actor account was registered; NULL for anonymous users. |
| event_user_revision_count | bigint | Edit count of the actor at event time. |
| event_user_groups_historical | array<string> | Groups held by the actor at event time (before the change for altergroups events). NULL for revision and page events. |
| event_user_is_cross_wiki | boolean | True if the actor is a CentralAuth user editing while logged out (username format IP>GlobalUsername). NULL (not FALSE) when the actor username is absent from the event (e.g. system-created user accounts where performer text is not populated). |
| user_id | bigint | Local MediaWiki user ID of the user being created/renamed/altered. |
| user_central_id | bigint | Global CentralAuth user ID of the user being created/renamed/altered. NULL for non-user events. |
| user_text_historical | string | Username of the user after the event: new name for renames, current name otherwise. |
| user_is_anonymous | boolean | True if the user had no local account (always false for user events). |
| user_is_temporary | boolean | True if the user being acted upon was a temporary account. |
| user_is_permanent | boolean | True if the user being acted upon was a permanent registered account. |
| user_groups_historical | array<string> | Groups held by the user after the change (for altergroups), or at creation. |
| user_is_bot_by_historical | array<string> | Bot classification of the user based on user_groups_historical: name and/or group. |
| user_is_created_by_self | boolean | True if the user registered their own account (performer == user, not autocreate). NULL for non-create events. |
| user_is_created_by_system | boolean | True if the account was autocreated by the system (SSO/CentralAuth). NULL for non-create events. |
| user_is_created_by_peer | boolean | True if a distinct admin created the account (performer != user, not autocreate). NULL for non-create events. |
| page_id | bigint | Page ID at event time. |
| page_title_historical | string | Page title (without namespace prefix) at event time. |
| page_namespace_historical | int | Page namespace ID at event time. |
| page_namespace_is_content_historical | boolean | True if page_namespace_historical is a content namespace. |
| page_is_deleted | boolean | True if the page was in deleted state at this event. Set at event time for page events; back-patched for revision rows when a page delete/undelete arrives. NULL for user events. |
| revision_id | bigint | Revision ID; NULL for non-revision events. |
| revision_parent_id | bigint | Parent revision ID; NULL for page-creation revisions. |
| revision_minor_edit | boolean | True if the editor flagged this as a minor edit. |
| revision_text_bytes | bigint | Uncompressed byte size of the revision text. |
| revision_text_bytes_diff | bigint | Byte delta vs. parent revision; NULL for page-creation revisions. |
| revision_text_sha1 | string | SHA-1 of the concatenated slot content (all-slots hash). |
| revision_deleted_parts | array<string> | Visibility-suppressed components: text, comment, and/or user. NULL when nothing is suppressed. Reflects visibility at event time and is updated when later visibility-change events arrive. NULL for page and user events. |
| revision_is_identity_reverted | boolean | True if a later revision restored the page to the state before this one. source=events rows: FALSE for fresh revisions (rev_dt=today, non-null sha1) — a fresh revision cannot already be reverted; upgraded to TRUE when the reverter arrives. NULL for late-arriving or null-sha1 revisions until the monthly snapshot provides the authoritative value. |
| revision_first_identity_reverting_revision_id | bigint | ID of the first revision that identity-reverted this one; NULL if not reverted. |
| revision_seconds_to_identity_revert | bigint | Seconds between this revision and the first identity revert; NULL if not reverted. |
| revision_is_identity_revert | boolean | True if this revision itself is an identity revert (restores a prior page state). source=events rows: FALSE for fresh revisions (rev_dt=today, non-null sha1) where no revert was detected; NULL for late-arriving or null-sha1 revisions. |
| revision_is_deleted_by_page_deletion | boolean | True if this revision is in deleted state because its page was deleted (as opposed to an explicit RevisionDelete action). Back-patched when a page delete/undelete event arrives. NULL for page and user events. |
| revision_tags | array<string> | Change tags applied to this revision. Populated when the corresponding change-tag events are delivered, which may be after the revision was first written. |
| event_meta_id | string | Delivery UUID (meta.id) used as the MERGE key for page and user events. NULL for revision rows and all snapshot rows. Internal; not intended for analytics queries. |
| control_map | map<string,string> | Internal, do not use for queries. Per-stream rerun guard tracking the last event/backfill timestamp that updated this row, per stream. NULL for source=snapshot rows. |
| row_update_dt | timestamp | Incremental-read watermark of the run that last wrote this row. See the FAQ on reading the table incrementally. |
Changes and known problems
| Date | Phab Task | Details |
|---|---|---|
| 2026-06-12 | T424350 |
Phase I under development. The table is
not yet declared production quality
and its schema may still change. The exact semantics of the
row_update_dt
watermark are still being finalized and may change before production.
event_user_groups_historical
is currently NULL on
source='events'
revision and page rows; it should carry the actor's groups at event time. To be fixed before production.
|
FAQ
How do I get only fully-reconciled, authoritative data?
Filter on the
source
column:
SELECT *
FROM wmf_mediawiki.mediawiki_history_incremental_v1
WHERE source = 'snapshot'
AND wiki_id = 'simplewiki'
LIMIT 10
source = 'snapshot'
rows are projected from
wmf.mediawiki_history
by the monthly reconcile and have identical semantics to that table.
source = 'events'
rows are the fresh daily data and are best-effort (see below).
Which fields are "best-effort" on source='events' rows?
The best-effort aspect is across days : some signals about a revision can arrive after the revision itself, and are patched in on a later daily run.
-
revision_tagsare populated when the corresponding change-tag events are delivered, which may be after the revision was first written. -
revision_deleted_partsreflects visibility as of the last daily run, and is updated when later visibility-change events arrive. -
The revert fields (
revision_is_identity_reverted,revision_first_identity_reverting_revision_id,revision_seconds_to_identity_revert,revision_is_identity_revert) are filled in when a reverting revision is processed, which is typically a later day.
So a
source='events'
revision reflects everything known about it as of the last daily run, but may not yet reflect tags, visibility changes, or reverts that have not happened or not yet been delivered. Every field reaches its authoritative value at the next monthly reconcile, when the row is replaced by a
source='snapshot'
row.
This table doesn't have a 'snapshot' Hive partition. How do I consume it?
This table does not use a
snapshot
partition as with other tables such as
Mediawiki history
. We use a table format called
Iceberg
. Instead of rewriting all data each month, this technology lets us update the table in place, with the benefit of daily updates.
If you are building a data pipeline and need to define an
Airflow
sensor to wait on this table's updates, instead of waiting on Hive partitions you should use our
datasets.yaml
configuration and the appropriate helper functions to construct a sensor.
In your Airflow Python DAG code:
# here we use platform_eng, but you should use the config
# that applies to your Airflow instance
from platform_eng.config.dag_config import dataset
dataset_id = "iceberg_wmf_mediawiki_mediawiki_history_incremental_v1"
wait_for_sensor = dataset(dataset_id).get_sensor_for(dag)
If your sensor needs to cover more than one day's worth of target DAG executions, use the helper:
from platform_eng.config.dag_config import dataset
from wmf_airflow_common.sensors.helper import get_daily_execution_dates_fn
dataset_id = "iceberg_wmf_mediawiki_mediawiki_history_incremental_v1"
wait_for_sensor = dataset(dataset_id)
.get_sensor_for(
dag,
execution_date_fn=get_daily_execution_dates_fn(num_days=7),
)
How do I read only the rows that changed since my last run?
Use the
row_update_dt
watermark column. Every write stamps the row with the timestamp of the run that last touched it, so an incremental consumer can read only new work:
SELECT *
FROM wmf_mediawiki.mediawiki_history_incremental_v1
WHERE row_update_dt >= TIMESTAMP '{{ your_last_watermark }}'
You
may
additionally add an
event_timestamp
lower bound to get partition pruning, since the table is partitioned by
months(event_timestamp)
:
... AND event_timestamp >= TIMESTAMP '{{ lower_bound }}'
Caveat:
an
event_timestamp
bound is a performance optimization, not a correctness filter. Daily runs patch rows in place when visibility changes, page deletions, reverts, or change tags arrive for revisions that already exist — including rows with an old
event_timestamp
. An
event_timestamp
lower bound will silently miss those updates to historically-old rows. Only add it if your use case can tolerate missing such back-dated updates, or set the bound wide enough to cover them.
The monthly reconcile re-stamps its rows with a watermark at the very end of the snapshot month (one second before the next month boundary). This lets you choose whether your incremental read picks up the monthly re-baseline:
- A lower bound exactly at a month-boundary midnight returns the daily deltas only and skips the monthly re-baseline.
-
Lowering that same bound by one second (
... - INTERVAL 1 SECOND) also pulls the full monthly snapshot (billions of rows).
What does waiting on the sensor guarantee?
After the daily run for a given day succeeds, all of that day's events have been ingested as
source='events'
rows. Those rows are best-effort (see above), and are made fully authoritative at the next monthly reconcile. If your use case needs strict semantics, consume only
source='snapshot'
rows.
How do I migrate to MHW incremental?
In the event there is a pipeline that, for any reason, needs to be migrated to
wmf_mediawiki.mediawiki_history_incremental_v1
here is a list of changes to apply:
-
switch any reference from
wmf.mediawiki_historytowmf_mediawiki.mediawiki_history_incremental_v1. - switch the sensor reference, see above.
-
remove the
snapshotfilter and replace it with:-
source = 'snapshot', if only authoritative data is needed; -
source = 'events', if only stream and daily ingested data is needed; - no filter at all;
-
-
replace
wiki_dbwithwiki_id. -
replace columns such as
page_namespace,page_title,user_text,event_user_is_bot_by, etc. with the relative*_historicalcounterpart. If fresh, up to date data is necessary it is possible to self join the table and get the most recent information from the*_historicalcolumn. -
apply
COALESCEtorevision_is_identity_reverted. -
event_timestampis now atimestampfield, therefore apply the right semantic to the filters. Additionally rework all the time-window filters to the right granularity.
Pipeline
- EventBus produces the MediaWiki event streams this table is built from — mediawiki.page_change.v1 , mediawiki.revision_tags_change, mediawiki.revision_visibility_change and mediawiki.user_change — to eventgate and eventually to Kafka.
- These events are consumed and processed into Data Lake Hive event tables via Hadoop Event Ingestion .
-
Every day, a Spark job (
MWHistoryDeltaWriter
via Airflow DAG
merge_events_to_mw_incremental_history_daily
) reads one calendar day of these events, maps them to the table schema, runs identity-revert detection, applies namespace-content and bot classification, and MERGEs the result as
source='events'rows. The same run also patches existing rows in place — includingsource='snapshot'rows — when that day's visibility changes, page deletions, reverts, or change tags target revisions already in the table. -
Every month, after a new
wmf.mediawiki_historysnapshot is available, a Spark job ( MWHistorySnapshotMerger , via Airflow DAG merge_mw_history_to_mw_incremental_history_monthly ) lays down the authoritative baseline: it deletes the priorsource='snapshot'rows (and thesource='events'rows they now supersede) and inserts a fresh projection ofwmf.mediawiki_historyassource='snapshot'rows. - The daily job for a given month is gated on the monthly reconcile of the previous month — it blocks at the start of the month until that authoritative baseline has landed, so no back-dated events are lost. Although this gating helps data correctness, the compromise is that daily runs near the start of the months will likely come in 24h+ late.
-
Both jobs run on Spark 3.5.8 / Iceberg, write through a per-run Iceberg branch that is fast-forwarded to
mainon success (write-audit-publish, so readers never see a half-written state), and are serialized through a single-slot mutex pool because they write the same table.