Media Content Copyright Monitoring and Royalty Settlement Data Warehouse Best Practices

Integrating a content asset library, licensing contract table, and multi-platform play logs into a multi-layer data warehouse enables automatic royalty attribution calculation, contract expiration risk tracking, and locking historical play snapshots at month-end to prevent data retroactivity disputes. This article uses a dataset of 15 content assets, 20 licensing contracts, and 60 play records to demonstrate an end-to-end ODS → DWD → DWS → ADS four-layer architecture, covering five core capabilities: OSS PIPE, CDC contract sync, Dynamic Table royalty attribution, Table Stream + MERGE INTO contract SCD, and Time Travel month-end snapshot locking.


Overview

The core challenge of a media copyright data warehouse is: play data comes from multiple platforms, revenue-sharing methods vary across contracts (three types: by play count, by revenue percentage, flat licensing fee), and retroactive modifications are not permitted after month-end settlement.

Singdata Lakehouse solves these problems with the following combination:

ProblemSolution
Multiple platforms push daily play CSV filesOSS PIPE auto-ingestion, triggered by file changes, no manual scheduling scripts needed
Contract management system status changes in real timeMySQL CDC sync + Table Stream captures row-level changes, drives SCD processing
Unified royalty attribution calculation for three revenue-sharing methodsDynamic Table declarative SQL, monthly group aggregation applying contract terms
Platform retroactively reports data after month-end, causing historical play data to changeTime Travel snapshot query, freezing the data version at settlement time as a voucher
Contract expiration alerts to prevent copyright gapsADS layer days_to_expiry field drives alerts

SQL Commands Used

Command / FunctionPurposeNotes
CREATE TABLEBuild ODS raw tables (content assets, contracts, play logs)Regular tables, used as upstream for Dynamic Tables
CREATE TABLE STREAMCreate a Table Stream on the contract tableCaptures INSERT / UPDATE / DELETE changes, drives contract SCD
CREATE DYNAMIC TABLECreate incremental calculation tables for DWD / DWS / ADS layersSystem automatically identifies upstream changes and incrementally refreshes
REFRESH DYNAMIC TABLEManually trigger a single refreshUsed during initial build or debugging
MERGE INTOMerge Stream changes into contract history tableSCD scenario: new rows inserted, status changes updated
DESC HISTORYView table historical version recordsConfirm available versions before Time Travel
TIMESTAMP AS OFQuery data snapshot at a historical point in timeMonth-end snapshot locking to prevent retroactive modifications
DATEDIFFCalculate remaining contract daysADS layer contract expiration alerts

Prerequisites

All examples in this article run under the best_practice_copyright_royalty schema.

CREATE SCHEMA IF NOT EXISTS best_practice_copyright_royalty;


ODS Layer: Raw Data Ingestion

Content Asset Table

CREATE TABLE IF NOT EXISTS best_practice_copyright_royalty.doc_content_assets ( content_id STRING, title STRING, content_type STRING, -- movie / tv_series / music / documentary / podcast / short_video rights_holder STRING, release_year INT, region STRING -- CN / GLOBAL );

Insert 15 content asset records:

Import from local CSV (recommended):

-- Step 1: Upload local CSV file to User Volume via SQL PUT PUT '/path/to/doc_content_assets.csv' TO USER VOLUME FILE 'doc_content_assets.csv';

-- Step 2: COPY INTO table from User Volume COPY INTO best_practice_copyright_royalty.doc_content_assets FROM USER VOLUME USING csv OPTIONS('header'='true', 'sep'=',', 'nullValue'='') FILES ('doc_content_assets.csv');

You can also insert a small batch of test data inline (no CSV file required):

INSERT INTO best_practice_copyright_royalty.doc_content_assets VALUES ('C001','The Dragon Legacy', 'movie', 'StarFilms Ltd', 2022,'CN'), ('C002','Sky Warriors Season 1', 'tv_series','BlueSky Media', 2021,'CN'), ('C003','Jazz Nights', 'music', 'Harmony Records', 2023,'GLOBAL'), ('C004','Nature Chronicles', 'documentary','EarthVision Inc', 2020,'CN'), ('C005','Tech Talk Podcast S1', 'podcast', 'CastWave Studio', 2023,'CN'), ('C006','Ocean Odyssey', 'documentary','EarthVision Inc', 2021,'GLOBAL'), ('C007','Pop Hits Vol.3', 'music', 'Harmony Records', 2022,'CN'), ('C008','Drama Kings Season 2', 'tv_series','BlueSky Media', 2022,'CN'), ('C009','The Lost City', 'movie', 'StarFilms Ltd', 2023,'CN'), ('C010','Morning Yoga Flow', 'short_video','WellnessFirst', 2024,'CN'), ('C011','Code & Coffee S2', 'podcast', 'CastWave Studio', 2024,'CN'), ('C012','Street Food Asia', 'documentary','EarthVision Inc', 2022,'GLOBAL'), ('C013','Summer Beats', 'music', 'Harmony Records', 2024,'CN'), ('C014','Thriller Night', 'movie', 'StarFilms Ltd', 2021,'GLOBAL'), ('C015','Innovation Stories S1', 'tv_series','BlueSky Media', 2023,'CN');

Licensing Contract Table

rate_type supports three revenue-sharing methods: revenue_share (by revenue percentage), per_play (by play count), and flat_fee (fixed licensing fee).

CREATE TABLE IF NOT EXISTS best_practice_copyright_royalty.doc_license_contracts ( contract_id STRING, content_id STRING, platform_id STRING, start_date DATE, end_date DATE, rate_type STRING, -- revenue_share / per_play / flat_fee rate_value DOUBLE, -- percentage (revenue_share), unit price (per_play), annual fee (flat_fee) min_guarantee DOUBLE, -- minimum guarantee (annual, in yuan) status STRING -- active / expired / renewed );

Insert 20 contracts covering all three revenue-sharing methods:

Import from local CSV (recommended):

-- Step 1: Upload local CSV file to User Volume via SQL PUT PUT '/path/to/doc_license_contracts.csv' TO USER VOLUME FILE 'doc_license_contracts.csv';

-- Step 2: COPY INTO table from User Volume COPY INTO best_practice_copyright_royalty.doc_license_contracts FROM USER VOLUME USING csv OPTIONS('header'='true', 'sep'=',', 'nullValue'='') FILES ('doc_license_contracts.csv');

You can also insert a small batch of test data inline (no CSV file required):

INSERT INTO best_practice_copyright_royalty.doc_license_contracts VALUES ('CTR001','C001','PLT_A',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'revenue_share',0.30,50000,'active'), ('CTR002','C001','PLT_B',CAST('2025-03-01' AS DATE),CAST('2025-08-31' AS DATE),'revenue_share',0.25,30000,'active'), ('CTR003','C002','PLT_A',CAST('2025-01-01' AS DATE),CAST('2025-06-30' AS DATE),'per_play',0.005,10000,'expired'), ('CTR004','C002','PLT_C',CAST('2025-04-01' AS DATE),CAST('2025-12-31' AS DATE),'revenue_share',0.20,20000,'active'), ('CTR005','C003','PLT_A',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'per_play',0.003,5000,'active'), ('CTR006','C003','PLT_B',CAST('2025-02-01' AS DATE),CAST('2026-01-31' AS DATE),'flat_fee',8000,0,'active'), ('CTR007','C004','PLT_C',CAST('2024-12-01' AS DATE),CAST('2025-11-30' AS DATE),'revenue_share',0.15,15000,'active'), ('CTR008','C005','PLT_A',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'per_play',0.002,3000,'active'), ('CTR009','C006','PLT_B',CAST('2025-01-01' AS DATE),CAST('2025-06-30' AS DATE),'revenue_share',0.18,12000,'expired'), ('CTR010','C007','PLT_A',CAST('2025-03-01' AS DATE),CAST('2025-12-31' AS DATE),'per_play',0.004,6000,'active'), ('CTR011','C008','PLT_C',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'revenue_share',0.22,25000,'active'), ('CTR012','C009','PLT_A',CAST('2025-05-01' AS DATE),CAST('2026-04-30' AS DATE),'revenue_share',0.28,40000,'active'), ('CTR013','C010','PLT_B',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'per_play',0.001,2000,'active'), ('CTR014','C011','PLT_C',CAST('2025-02-01' AS DATE),CAST('2025-07-31' AS DATE),'flat_fee',5000,0,'expired'), ('CTR015','C012','PLT_A',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'revenue_share',0.20,18000,'active'), ('CTR016','C013','PLT_B',CAST('2025-04-01' AS DATE),CAST('2026-03-31' AS DATE),'per_play',0.003,4000,'active'), ('CTR017','C014','PLT_A',CAST('2024-11-01' AS DATE),CAST('2025-10-31' AS DATE),'revenue_share',0.25,35000,'active'), ('CTR018','C014','PLT_C',CAST('2025-01-01' AS DATE),CAST('2025-12-31' AS DATE),'per_play',0.006,8000,'active'), ('CTR019','C015','PLT_B',CAST('2025-03-01' AS DATE),CAST('2025-08-31' AS DATE),'revenue_share',0.18,14000,'active'), ('CTR020','C015','PLT_C',CAST('2025-06-01' AS DATE),CAST('2026-05-31' AS DATE),'revenue_share',0.15,10000,'active');

Contract revenue-sharing method distribution:

SELECT rate_type, COUNT(*) AS cnt, ROUND(AVG(rate_value), 4) AS avg_rate FROM best_practice_copyright_royalty.doc_license_contracts GROUP BY rate_type;

rate_type | cnt | avg_rate ---------------+-----+--------- flat_fee | 2 | 6500 per_play | 7 | 0.0034 revenue_share | 11 | 0.2145

Platform Play Log Table

In an OSS PIPE scenario, video platforms push daily CSV/Parquet play reports via FTP to an OSS Volume, and PIPE auto-ingests them into this table.

CREATE TABLE IF NOT EXISTS best_practice_copyright_royalty.doc_platform_plays ( play_id STRING, content_id STRING, platform_id STRING, play_date DATE, play_count BIGINT, revenue DOUBLE );

Import from local CSV (recommended):

-- Step 1: Upload local CSV file to User Volume via SQL PUT PUT '/path/to/doc_platform_plays.csv' TO USER VOLUME FILE 'doc_platform_plays.csv';

-- Step 2: COPY INTO table from User Volume COPY INTO best_practice_copyright_royalty.doc_platform_plays FROM USER VOLUME USING csv OPTIONS('header'='true', 'sep'=',', 'nullValue'='') FILES ('doc_platform_plays.csv');

You can also insert a small batch of test data inline (no CSV file required):

Insert 60 play records covering three platforms PLT_A / PLT_B / PLT_C for multiple months from January to July 2025:

INSERT INTO best_practice_copyright_royalty.doc_platform_plays VALUES ('P001','C001','PLT_A',CAST('2025-01-15' AS DATE),12500,18750.00), ('P002','C001','PLT_A',CAST('2025-02-15' AS DATE),14200,21300.00), ('P003','C001','PLT_A',CAST('2025-03-15' AS DATE),16800,25200.00), ('P004','C001','PLT_A',CAST('2025-04-15' AS DATE),15000,22500.00), ('P005','C009','PLT_A',CAST('2025-05-15' AS DATE),25000,37500.00), ('P006','C009','PLT_A',CAST('2025-06-15' AS DATE),28000,42000.00), ('P007','C009','PLT_A',CAST('2025-07-15' AS DATE),31000,46500.00), ('P008','C014','PLT_A',CAST('2025-02-15' AS DATE),21000,31500.00), ('P009','C014','PLT_A',CAST('2025-03-15' AS DATE),19500,29250.00), ('P010','C014','PLT_A',CAST('2025-05-15' AS DATE),20000,30000.00), ('P011','C003','PLT_A',CAST('2025-01-15' AS DATE),300000,900.00), ('P012','C003','PLT_A',CAST('2025-04-15' AS DATE),400000,1200.00), ('P013','C005','PLT_A',CAST('2025-06-15' AS DATE),350000,700.00), ('P014','C007','PLT_A',CAST('2025-03-15' AS DATE),250000,1000.00), ('P015','C012','PLT_A',CAST('2025-07-15' AS DATE),300000,6000.00), ('P016','C015','PLT_A',CAST('2025-06-15' AS DATE),100000,18000.00), ('P017','C005','PLT_A',CAST('2025-05-15' AS DATE),61000,24803.00), ('P018','C001','PLT_B',CAST('2025-03-15' AS DATE),10000,8000.00), ('P019','C001','PLT_B',CAST('2025-04-15' AS DATE),12000,9000.00), ('P020','C001','PLT_B',CAST('2025-05-15' AS DATE),15000,9500.00), ('P021','C003','PLT_B',CAST('2025-02-15' AS DATE),80000,6000.00), ('P022','C003','PLT_B',CAST('2025-05-15' AS DATE),100000,4000.00), ('P023','C006','PLT_B',CAST('2025-01-15' AS DATE),120000,7000.00), ('P024','C006','PLT_B',CAST('2025-06-15' AS DATE),160000,6500.00), ('P025','C010','PLT_B',CAST('2025-01-15' AS DATE),200000,2200.00), ('P026','C010','PLT_B',CAST('2025-05-15' AS DATE),120000,1200.00), ('P027','C013','PLT_B',CAST('2025-03-15' AS DATE),210000,2100.00), ('P028','C013','PLT_B',CAST('2025-04-15' AS DATE),190000,1900.00), ('P029','C001','PLT_B',CAST('2025-06-15' AS DATE),8000,6000.00), ('P030','C003','PLT_B',CAST('2025-07-15' AS DATE),50000,5000.00), ('P031','C006','PLT_B',CAST('2025-02-15' AS DATE),40000,4800.00), ('P032','C010','PLT_B',CAST('2025-07-15' AS DATE),30000,3000.00), ('P033','C013','PLT_B',CAST('2025-06-15' AS DATE),20000,2000.00), ('P034','C001','PLT_B',CAST('2025-07-15' AS DATE),7000,5000.00), ('P035','C003','PLT_B',CAST('2025-01-15' AS DATE),25000,1500.00), ('P036','C006','PLT_B',CAST('2025-03-15' AS DATE),15000,2000.00), ('P037','C010','PLT_B',CAST('2025-02-15' AS DATE),12000,1000.00), ('P038','C013','PLT_B',CAST('2025-07-15' AS DATE),9800,424.00), ('P039','C002','PLT_C',CAST('2025-04-15' AS DATE),30000,6000.00), ('P040','C002','PLT_C',CAST('2025-05-15' AS DATE),40000,7000.00), ('P041','C004','PLT_C',CAST('2025-01-15' AS DATE),45000,9000.00), ('P042','C004','PLT_C',CAST('2025-05-15' AS DATE),50000,8000.00), ('P043','C008','PLT_C',CAST('2025-02-15' AS DATE),60000,12000.00), ('P044','C008','PLT_C',CAST('2025-05-15' AS DATE),70000,3000.00), ('P045','C011','PLT_C',CAST('2025-07-15' AS DATE),80000,10000.00), ('P046','C014','PLT_C',CAST('2025-03-15' AS DATE),90000,15000.00), ('P047','C014','PLT_C',CAST('2025-05-15' AS DATE),100000,1000.00), ('P048','C015','PLT_C',CAST('2025-06-15' AS DATE),70000,9000.00), ('P049','C002','PLT_C',CAST('2025-06-15' AS DATE),5000,7000.00), ('P050','C004','PLT_C',CAST('2025-02-15' AS DATE),4000,6000.00), ('P051','C008','PLT_C',CAST('2025-03-15' AS DATE),3000,5000.00), ('P052','C011','PLT_C',CAST('2025-04-15' AS DATE),4000,5000.00), ('P053','C014','PLT_C',CAST('2025-06-15' AS DATE),3000,4000.00), ('P054','C015','PLT_C',CAST('2025-07-15' AS DATE),3000,3000.00), ('P055','C011','PLT_C',CAST('2025-05-15' AS DATE),46300,471.00), ('P056','C002','PLT_C',CAST('2025-07-15' AS DATE),1000,1000.00), ('P057','C004','PLT_C',CAST('2025-03-15' AS DATE),1000,1000.00), ('P058','C008','PLT_C',CAST('2025-04-15' AS DATE),1000,1000.00), ('P059','C011','PLT_C',CAST('2025-06-15' AS DATE),700,792.00), ('P060','C001','PLT_A',CAST('2025-05-15' AS DATE),17200,25800.00);

Verify overall data volume by platform:

SELECT platform_id, SUM(play_count) AS total_plays, ROUND(SUM(revenue), 2) AS total_revenue, COUNT(DISTINCT content_id) AS unique_contents FROM best_practice_copyright_royalty.doc_platform_plays GROUP BY platform_id ORDER BY total_revenue DESC;

platform_id | total_plays | total_revenue | unique_contents ------------+-------------+---------------+---------------- PLT_A | 1981200 | 382903 | 8 PLT_C | 707000 | 114263 | 6 PLT_B | 1433800 | 88124 | 5

PLT_A has the highest revenue, mainly driven by high-unit-price revenue_share contracts for "The Lost City" (C009) and "Thriller Night" (C014).

Configure OSS PIPE (Production Environment)

In a production environment, first create an OSS Storage Connection and Volume, then create a PIPE to continuously scan the platform push directory:

-- Create storage connection (using Alibaba Cloud OSS as an example) CREATE STORAGE CONNECTION IF NOT EXISTS oss_media_conn TYPE = 'OSS' ACCESS_ID = '<your-access-id>' ACCESS_KEY = '<your-access-key>' ENDPOINT = '<oss-cn-hangzhou.aliyuncs.com>'; -- Create Volume pointing to the platform's daily report directory CREATE EXTERNAL VOLUME IF NOT EXISTS vol_platform_plays TYPE = 'OSS' BUCKET = '<your-bucket>' PATH = '/media/platform-plays/' CONNECTION = oss_media_conn; -- Create PIPE to auto-ingest new Parquet files CREATE PIPE IF NOT EXISTS best_practice_copyright_royalty.pipe_platform_plays VIRTUAL_CLUSTER = 'DEFAULT' AUTO_PURGE = FALSE AS COPY INTO best_practice_copyright_royalty.doc_platform_plays FROM VOLUME vol_platform_plays USING parquet;


ODS Layer: Contract Change Table Stream

After the contract management system syncs MySQL data to doc_license_contracts via CDC, a Table Stream is created on this table to capture row-level changes and drive SCD processing for the contract history table.

CREATE TABLE STREAM IF NOT EXISTS stream_contract_changes ON TABLE doc_license_contracts WITH PROPERTIES ('TABLE_STREAM_MODE' = 'STANDARD');

Create the contract history table to store before/after values for each status change:

CREATE TABLE IF NOT EXISTS best_practice_copyright_royalty.doc_contract_history ( history_id STRING, contract_id STRING, content_id STRING, platform_id STRING, old_status STRING, new_status STRING, change_reason STRING, change_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP() );

Simulate a contract renewal operation, changing CTR003's status from expired to renewed:

UPDATE best_practice_copyright_royalty.doc_license_contracts SET status = 'renewed' WHERE contract_id = 'CTR003';

View the changes captured by the Stream (in STANDARD mode, an UPDATE produces two rows: UPDATE_BEFORE + UPDATE_AFTER):

SELECT contract_id, content_id, platform_id, status, __change_type FROM best_practice_copyright_royalty.stream_contract_changes LIMIT 5;

contract_id | content_id | platform_id | status | __change_type ------------+------------+-------------+---------+-------------- CTR003 | C002 | PLT_A | renewed | UPDATE_AFTER CTR003 | C002 | PLT_A | expired | UPDATE_BEFORE

Use MERGE INTO to consume the Stream and write contract changes to the history table:

MERGE INTO best_practice_copyright_royalty.doc_contract_history AS t USING ( SELECT CONCAT(contract_id, '_', CAST(CURRENT_TIMESTAMP() AS STRING)) AS history_id, contract_id, content_id, platform_id, CASE WHEN __change_type = 'UPDATE_AFTER' THEN NULL WHEN __change_type = 'UPDATE_BEFORE' THEN status WHEN __change_type = 'DELETE' THEN status ELSE NULL END AS old_status, CASE WHEN __change_type = 'UPDATE_AFTER' THEN status WHEN __change_type = 'INSERT' THEN status ELSE NULL END AS new_status, __change_type AS change_reason FROM best_practice_copyright_royalty.stream_contract_changes WHERE __change_type IN ('UPDATE_AFTER', 'INSERT', 'DELETE') ) AS s ON t.contract_id = s.contract_id AND t.new_status = s.new_status WHEN NOT MATCHED THEN INSERT (history_id, contract_id, content_id, platform_id, old_status, new_status, change_reason) VALUES (s.history_id, s.contract_id, s.content_id, s.platform_id, s.old_status, s.new_status, s.change_reason);


DWD Layer: Play Fact Wide Table

The DWD layer JOINs the play log with the content asset table to supplement content metadata, and adds a monthly partition marker to facilitate downstream DWS monthly aggregation.

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_copyright_royalty.dwd_play_facts AS SELECT p.play_id, p.content_id, p.platform_id, p.play_date, p.play_count, p.revenue, c.title, c.content_type, c.rights_holder, c.release_year, c.region, DATE_FORMAT(p.play_date, 'yyyy-MM') AS stat_month FROM best_practice_copyright_royalty.doc_platform_plays p LEFT JOIN best_practice_copyright_royalty.doc_content_assets c ON p.content_id = c.content_id;

Manually trigger the first refresh:

REFRESH DYNAMIC TABLE best_practice_copyright_royalty.dwd_play_facts; SELECT COUNT(*) AS row_count FROM best_practice_copyright_royalty.dwd_play_facts;

row_count --------- 60


DWS Layer: Royalty Attribution Calculation

The DWS layer is the core layer of the royalty data warehouse. It aggregates play data by the three dimensions of content_id + platform_id + stat_month, joins contract terms, applies different calculation formulas based on rate_type, and compares against the monthly amortized minimum guarantee.

Three revenue-sharing formulas:

rate_typeRoyalty Calculation Logic
revenue_shareMAX(current month platform revenue × percentage, annual guarantee / 12)
per_playMAX(current month play count × unit price, annual guarantee / 12)
flat_feeannual license fee / 12 (fixed, does not vary with play count)

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_copyright_royalty.dws_royalty_calc AS SELECT f.content_id, f.platform_id, f.stat_month, f.title, f.content_type, f.rights_holder, SUM(f.play_count) AS total_plays, SUM(f.revenue) AS total_revenue, c.contract_id, c.rate_type, c.rate_value, c.min_guarantee, c.end_date, CASE WHEN c.rate_type = 'revenue_share' THEN GREATEST(SUM(f.revenue) * c.rate_value, c.min_guarantee / 12) WHEN c.rate_type = 'per_play' THEN GREATEST(SUM(f.play_count) * c.rate_value, c.min_guarantee / 12) WHEN c.rate_type = 'flat_fee' THEN c.rate_value / 12 ELSE 0 END AS estimated_royalty, DATEDIFF(c.end_date, CURRENT_DATE()) AS days_to_expiry FROM best_practice_copyright_royalty.dwd_play_facts f LEFT JOIN best_practice_copyright_royalty.doc_license_contracts c ON f.content_id = c.content_id AND f.platform_id = c.platform_id GROUP BY f.content_id, f.platform_id, f.stat_month, f.title, f.content_type, f.rights_holder, c.contract_id, c.rate_type, c.rate_value, c.min_guarantee, c.end_date;

REFRESH DYNAMIC TABLE best_practice_copyright_royalty.dws_royalty_calc;

View the 8 royalty records with the highest revenue:

SELECT content_id, platform_id, stat_month, title, total_plays, ROUND(total_revenue, 2) AS total_revenue FROM best_practice_copyright_royalty.dws_royalty_calc ORDER BY total_revenue DESC LIMIT 8;

content_id | platform_id | stat_month | title | total_plays | total_revenue -----------+-------------+------------+--------------------+-------------+-------------- C009 | PLT_A | 2025-07 | The Lost City | 31000 | 46500 C009 | PLT_A | 2025-06 | The Lost City | 28000 | 42000 C009 | PLT_A | 2025-05 | The Lost City | 25000 | 37500 C014 | PLT_A | 2025-02 | Thriller Night | 21000 | 31500 C014 | PLT_A | 2025-05 | Thriller Night | 20000 | 30000 C014 | PLT_A | 2025-03 | Thriller Night | 19500 | 29250 C001 | PLT_A | 2025-05 | The Dragon Legacy | 17200 | 25800 C001 | PLT_A | 2025-03 | The Dragon Legacy | 16800 | 25200

"The Lost City" (C009) on PLT_A uses a 28% revenue_share split. In July, monthly revenue was 46,500 yuan, with royalties of 13,020 yuan — the highest single-month royalty record.


ADS Layer: Settlement Report and Contract Expiration Alerts

The ADS layer adds contract status tags on top of DWS, outputting settlement reports that can be used directly for reconciliation and expiration alerts.

CREATE DYNAMIC TABLE IF NOT EXISTS best_practice_copyright_royalty.ads_settlement_report AS SELECT content_id, platform_id, title, content_type, rights_holder, stat_month, contract_id, rate_type, total_plays, ROUND(total_revenue, 2) AS total_revenue, ROUND(estimated_royalty, 2) AS estimated_royalty, days_to_expiry, CASE WHEN days_to_expiry < 0 THEN 'expired' WHEN days_to_expiry <= 30 THEN 'expiring_soon' WHEN days_to_expiry <= 90 THEN 'expiring_in_quarter' ELSE 'active' END AS contract_status FROM best_practice_copyright_royalty.dws_royalty_calc WHERE estimated_royalty > 0;

REFRESH DYNAMIC TABLE best_practice_copyright_royalty.ads_settlement_report;

Aggregate royalties by rights holder and month:

SELECT rights_holder, stat_month, ROUND(SUM(estimated_royalty), 2) AS total_royalty, COUNT(DISTINCT content_id) AS content_cnt FROM best_practice_copyright_royalty.ads_settlement_report GROUP BY rights_holder, stat_month ORDER BY total_royalty DESC, rights_holder, stat_month DESC LIMIT 10;

rights_holder | stat_month | total_royalty | content_cnt ----------------+------------+---------------+------------ StarFilms Ltd | 2025-05 | 28906.67 | 3 StarFilms Ltd | 2025-03 | 18039.17 | 2 StarFilms Ltd | 2025-07 | 15520 | 2 StarFilms Ltd | 2025-06 | 14926.67 | 3 StarFilms Ltd | 2025-02 | 14265 | 2 StarFilms Ltd | 2025-04 | 9250 | 1 StarFilms Ltd | 2025-01 | 5625 | 1 BlueSky Media | 2025-05 | 3750 | 2 BlueSky Media | 2025-04 | 3750 | 2 BlueSky Media | 2025-06 | 3016.67 | 2

Result interpretation: StarFilms Ltd has led in total royalties for consecutive months, mainly contributed by revenue_share contracts for multiple high-revenue films including "The Dragon Legacy," "The Lost City," and "Thriller Night." May 2025 is the annual peak (28,906.67 yuan), coinciding with multiple films reaching peak play counts in the same month.

View contract expiration alerts (contracts past expiration have a negative days_to_expiry):

SELECT c.content_id, a.title, a.rights_holder, c.contract_id, c.platform_id, c.end_date, DATEDIFF(c.end_date, CURRENT_DATE()) AS days_remaining FROM best_practice_copyright_royalty.doc_license_contracts c LEFT JOIN best_practice_copyright_royalty.doc_content_assets a ON c.content_id = a.content_id ORDER BY days_remaining ASC LIMIT 8;

content_id | title | rights_holder | contract_id | platform_id | end_date | days_remaining -----------+------------------------+------------------+-------------+-------------+------------+--------------- C002 | Sky Warriors Season 1 | BlueSky Media | CTR003 | PLT_A | 2025-06-30 | -344 C006 | Ocean Odyssey | EarthVision Inc | CTR009 | PLT_B | 2025-06-30 | -344 C011 | Code & Coffee S2 | CastWave Studio | CTR014 | PLT_C | 2025-07-31 | -313 C001 | The Dragon Legacy | StarFilms Ltd | CTR002 | PLT_B | 2025-08-31 | -282 C015 | Innovation Stories S1 | BlueSky Media | CTR019 | PLT_B | 2025-08-31 | -282 C014 | Thriller Night | StarFilms Ltd | CTR017 | PLT_A | 2025-10-31 | -221 C004 | Nature Chronicles | EarthVision Inc | CTR007 | PLT_C | 2025-11-30 | -191 C001 | The Dragon Legacy | StarFilms Ltd | CTR001 | PLT_A | 2025-12-31 | -160


Dynamic Table Scheduling Configuration

The Dynamic Table DDL does not include REFRESH INTERVAL. Instead, scheduled refresh tasks are created in Studio, making it easy to attach data quality checks and alert rules to the same task.

Create three refresh tasks under the best_practices/copyright_royalty/ path in Studio:

Task NameTarget Dynamic TableRecommended ScheduleNotes
refresh_dwd_play_factsdwd_play_factsDaily at 02:00Refresh after platform T+1 push completes
refresh_dws_royalty_calcdws_royalty_calcDaily at 03:00Calculate royalties downstream after DWD refresh
refresh_ads_settlement_reportads_settlement_reportDaily at 04:00Generate the latest daily settlement snapshot

Time Travel: Month-End Snapshot Locking

After month-end settlement, platforms may retroactively report or correct historical play data, causing the confirmed settlement amount to change and triggering royalty disputes. Use Time Travel to freeze the data version at the time of settlement as a reconciliation voucher.

First, view the table's historical versions:

DESC HISTORY best_practice_copyright_royalty.doc_platform_plays;

version | time | total_rows | operation | job_id --------+----------------------------+------------+------------+------------------------ 2 | 2026-06-06T23:52:05.794 | 60 | INSERT_INTO| 2026060623520546600024922 1 | 2026-06-06T23:51:10.277 | 0 | CREATE | 2026060623511018800033372

Query the play snapshot using the timestamp at the time of month-end settlement (here using the May settlement cutoff as an example):

SELECT SUM(play_count) AS total_plays, ROUND(SUM(revenue), 2) AS total_revenue FROM best_practice_copyright_royalty.doc_platform_plays TIMESTAMP AS OF '2026-06-06 23:53:00' WHERE play_date >= CAST('2025-05-01' AS DATE) AND play_date <= CAST('2025-05-31' AS DATE);

total_plays | total_revenue ------------+-------------- 664500 | 152274

Export historical snapshot data to a settlement archive table (append-only, immutable):

CREATE TABLE IF NOT EXISTS best_practice_copyright_royalty.doc_settlement_archive ( settlement_id STRING, settle_month STRING, snapshot_ts STRING, content_id STRING, platform_id STRING, total_plays BIGINT, total_revenue DOUBLE, estimated_royalty DOUBLE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP() );


ZettaPark Python Task: Complex Royalty Calculation

For complex business rules like tiered rates and minimum guarantees with excess bonuses, use a ZettaPark Python Task:

from clickzetta_zettapark.session import Session def calc_complex_royalty(session): """ Tiered rate example: - Monthly play count < 100,000: 0.003 yuan/play - Monthly play count 100,000–500,000: 0.003 + excess at 0.005 yuan/play - Monthly play count > 500,000: 0.003 + excess at 0.008 yuan/play Minimum monthly guarantee: annual contract guarantee / 12 """ df = session.sql(""" SELECT content_id, platform_id, stat_month, total_plays, min_guarantee FROM best_practice_copyright_royalty.dws_royalty_calc WHERE rate_type = 'per_play' """).to_pandas() def tiered_rate(plays, min_guarantee_monthly): if plays <= 100000: royalty = plays * 0.003 elif plays <= 500000: royalty = 100000 * 0.003 + (plays - 100000) * 0.005 else: royalty = 100000 * 0.003 + 400000 * 0.005 + (plays - 500000) * 0.008 return max(royalty, min_guarantee_monthly) df['tiered_royalty'] = df.apply( lambda r: tiered_rate(r['total_plays'], r['min_guarantee'] / 12), axis=1 ) # Write back to settlement table session.write_pandas(df[['content_id','platform_id','stat_month','tiered_royalty']], 'best_practice_copyright_royalty.doc_tiered_royalty_result', overwrite=True) return df.shape[0]

Create a Python Task calc_tiered_royalty under the best_practices/copyright_royalty/ path in Studio, mount the above script, and trigger execution after the refresh_ads_settlement_report task completes.


Data Warehouse Object Overview

SHOW TABLES IN best_practice_copyright_royalty;

schema_name | table_name | is_dynamic ---------------------------------+---------------------------+----------- best_practice_copyright_royalty | doc_content_assets | false best_practice_copyright_royalty | doc_contract_history | false best_practice_copyright_royalty | doc_license_contracts | false best_practice_copyright_royalty | doc_platform_plays | false best_practice_copyright_royalty | doc_settlement_archive | false best_practice_copyright_royalty | dwd_play_facts | true best_practice_copyright_royalty | dws_royalty_calc | true best_practice_copyright_royalty | ads_settlement_report | true

Data pipeline overview:

OSS Volume (platform report CSV/Parquet) │ OSS PIPE ▼ doc_platform_plays (ODS play log) │ ├── MySQL CDC → doc_license_contracts (ODS contracts) │ │ Table Stream (STANDARD) │ │ → MERGE INTO → doc_contract_history (contract change SCD) │ └── doc_content_assets (ODS content assets) │ ▼ Dynamic Table (daily refresh at 02:00) dwd_play_facts (DWD play fact wide table) │ ▼ Dynamic Table (daily refresh at 03:00) dws_royalty_calc (DWS royalty attribution) revenue_share / per_play / flat_fee GREATEST(calculated value, monthly guarantee) │ ▼ Dynamic Table (daily refresh at 04:00) ads_settlement_report (ADS settlement report + expiration alerts) │ ▼ Time Travel (TIMESTAMP AS OF) doc_settlement_archive (month-end snapshot archive, immutable)


Notes

  • Contract table updates trigger Dynamic Table incremental refresh: dws_royalty_calc has doc_license_contracts as one of its upstream sources. When a contract rate or expiration date is modified, the affected contract-month combinations are recalculated on the next refresh. After month-end confirmation, it is recommended to export the current month's data to doc_settlement_archive to prevent subsequent contract changes from affecting already-settled amounts.

  • Table Stream consumption offset cannot be rolled back: Once MERGE INTO consumes the Stream, the consumption offset advances. Already-processed changes will not reappear. If MERGE INTO execution fails, use SHOW TABLE STREAMS to confirm the offset, then re-consume.

  • OSS PIPE only processes new files: After the PIPE is created, it only scans and ingests files newly added to the Volume. It does not process pre-existing files. Historical data (files already in the Volume before the PIPE was created) must be manually backfilled using COPY INTO.

  • Time Travel timestamps only accept literals: TIMESTAMP AS OF does not support dynamic expressions like CURRENT_TIMESTAMP() - INTERVAL. It is recommended to persist the settlement cutoff timestamp to the settlement order table, and read it from the table when querying for reconciliation.

  • days_to_expiry is based on query execution time: The ADS layer's days_to_expiry is calculated using DATEDIFF(end_date, CURRENT_DATE()), and results vary with each query. For a stable expiration alert report, the CURRENT_DATE() at refresh time should be fixed as a parameter field, or a snapshot should be generated at a fixed time in conjunction with the scheduling task.