01Where the 7,231 → 3,594 drop goes
Question 1
These figures are not from the stored report table. They come from the board’s V11 SQL run by hand on 21 September 2026 against the tables as they stand today.
The report itself is the materialised view public.signals_all_data_qmy_04092026, last refreshed around 15 April 2026 and holding 3,703 rows. Re-running the same logic today gives 3,594. Both are “the report”; they are five months apart.
The step-by-step breakdown below adds up exactly to the manual run: 7,231 in, 3,594 out. Each tactic is counted once, under the first rule that removes it.
The waterfall: 7,231 down to 3,594, step by step
| Step | Reason | Change | Running total | Valid? | Why I think so |
|---|---|---|---|---|---|
| Start | Every tactic in the system | 7,231 | |||
| 1 | Channel is not email or display media (sms 312, push 16, paid search 6, social 4) | − 338 | 6,893 | Valid scope choice | Board: “V1 Scope: SMS and Push can be ignored.” Deliberate. Confirm it still stands. |
| 2 | Tactic status is cancelled |
− 1,402 | 5,491 | Valid | Cancelled work never went to market. No board note gives the reason, but it makes sense. |
| 3 | Tactic flagged is_test |
− 666 | 4,825 | Valid | This is the official test flag. |
| 4 | Campaign flagged is_test |
− 519 | 4,306 | Mostly valid 302 need checking | Right rule, but the flag changed over the summer on in-market campaigns. 302 tactics that were in the April report are gone today for this reason alone. |
| 5 | Tactic or campaign name contains “delete” | − 155 | 4,151 | Valid | The team’s naming convention for records that are no longer needed. |
| 6 | Tactic name contains “test”, but the flag says it is not a test | − 209 | 3,942 | Not reliable review | Catches junk (test, GMTest 101) and real A/B creative variants like ACQ_Q2_US_0513 Boundless_Chase_TEST_Arrow_125K + 1FN_Non-Res_FAIC_DME. At least 58 follow production naming. |
| 7 | Campaign name contains “test”, but the flag says not | − 45 | 3,897 | Probably valid | Names like Jenn Test, BETA Testing JKH. The flag was just never set. This is better fixed in the source system. |
| 8 | Campaign name contains RETARGETING_MIGRATION |
− 140 | 3,757 | To confirm with Marcus/Amanda | Board says only “Exclude campaign name RETARGETING_MIGRATION (103)”. No reason given. It is probably a one-off data migration rather than marketing work. |
| 9 | Campaign name contains LPA |
− 55 | 3,702 | To confirm with Marcus/Amanda | No reason on the board. Names look like system records (PRD_LPA_LINKIDS, STG_PREARRIVAL_AKA_LPA). |
| 10 | Campaign name contains “decision” and “engine” | − 33 | 3,669 | To confirm with Marcus/Amanda | No reason on the board. Looks like a programme that runs all the time (Decision Engine 2.0 - Ongoing). |
| 11 | Campaign name contains global module |
− 4 | 3,665 | To confirm with Marcus/Amanda | No reason on the board. One campaign: Global Module Syndication. |
| 12 | Campaign name contains 100 days |
− 71 | 3,594 | To confirm with Marcus/Amanda | No reason on the board. Mostly 100 Days Content - Ongoing; one looks like a normal tactic (Q2_2025_F100 Days_DME_App). |
| End | What the logic returns today | 3,594 |
measured. One aggregate query over analytical_model.tactic_signals left-joined to analytical_model.campaign_signals, each tactic assigned to the first failing rule.
Totals by verdict
| Verdict | Tactics | Share of the 3,637 removed |
|---|---|---|
| Valid steps 1, 2, 3, 5, 7 | 2,606 | 72% |
| Valid rule but 302 of the 519 need checking (step 4) | 519 | 14% |
| Not reliable step 6 | 209 | 6% |
| To confirm with Marcus/Amanda steps 8–12 | 303 | 8% |
The only guidance the board gives for steps 8–12 as a group is a sticky about marking tactics “with custom workflow so we exclude them”. For step 6 the board itself asked for a change: “Add this to be the test check mark instead of just the name.”
So, is the drop legitimate? For about 85% of it, yes: 3,080 rows are removed in steps 1–5 for clear reasons we can defend. The other 557 rows are removed in steps 6–12 only because of words in their names.
02Stage definitions: agreed vs what the query does
Question 2
Every tactic is supposed to pass through four stages: Intake → Creative → Targeting → Fulfillment, each with a start and an end date. For every one of those columns the tables below give the source, what the team agreed, what the query actually does, and whether they match.
These are not repeated in each row, but they affect every date:
1. The row must survive the exclusion filters in section 1.
2. Only labelled Workfront tasks count. About 78% of the 361,190 rows in signals_workfront_tasks have no task_type and are invisible to the report. Caveat: Workfront carries all of Marriott’s project work, so much of that 78% is probably unrelated to these campaigns. Treat it as the most that could be missing, not the amount that is. We don’t yet know how much of the unlabelled work is actually tactic work.
3. The platform lookup routes the tactic. A WF tactic can never use DARF dates; a CP tactic can never use Workfront deployment tasks.
4. Some sources are frozen copies. Display placements come from a snapshot taken 6 April 2026; DARF files stop 16 May 2026. Anything after those dates cannot produce a real date.
5. A duration can only be worked out when both its start and end dates exist. If either date is missing, the duration is blank.
Words used below
| Word | Meaning |
|---|---|
| Campaign | A marketing programme, e.g. Boutiques_Shops. Contains many tactics. |
| Tactic | One deliverable a customer sees: one email or one display banner. One row in the report. |
| Workfront (WF) | The project-management tool. Work is tracked as tasks; a task has a date it was entered and a date it was completed. |
| Content Pantry (CP) | Marriott’s campaign-building tool. Holds the official list of campaigns and tactics. |
| DARF file | A delivery file showing an audience was sent. T = audience ready (targeting done). P = processed for sending (fulfillment done). Only for emails built in Content Pantry. |
| Display media (DME) | Banner/tile placements on Marriott’s site and app, tracked as placement tasks ending at “DME Launched”. |
| Platform | A lookup (signals_tactic_cp_wf.platform) labelling each tactic CP or WF. It decides which rule applies. Display is always WF. |
- schema.table_name database table
- column_name column in that table
- Value the value it is filtered to
- 1 tried in this order until one returns a date
Freshness marks below: fresh · stale or frozen · cannot tell. Numbers in the last column are for the downloaded April table (3,703 rows); the pattern is the same in the manual run and both were checked. Tables scroll sideways; the column name stays pinned.
Stage 1: Intake the request for the work comes in and is accepted
| Column | Source table | How fresh | In plain words | What the team agreed | What the report actually does | Exact conditions | Do they match? |
|---|---|---|---|---|---|---|---|
| intake_start_date |
|
Fresh. Newest campaign created 16 Sep 2026; last edit 18 Sep 2026. | The day the campaign was first set up. | “Campaign Creation” | Uses the day the campaign record was created. | Rule: cs.created_dt::dateIndirect: joined by cs.id = ts.campaign_id. It is a campaign date, so every tactic in the campaign gets the same one, even tactics added a year later. |
Matches the board But misleading per tactic see next row. |
| intake_end_date |
|
Table loads daily (latest task entered 20 Sep 2026). Only 244 rows carry this label: newest completed 10 Sep 2026. | The day the creative request for the campaign was signed off. | “Creative Intake Task Complete” | Takes the most recent sign-off of any “Creative Intake” task anywhere in the campaign. | Rule: max(actual_completion_date) WHERE task_type = 'Creative Intake' GROUP BY campaign_idIndirect: matched on campaign_id, not tactic_id. Rule 2 applies. The date moves later whenever a new intake task completes in that campaign. |
Same signal, wrong level Evidence: every tactic in a campaign shares one intake end (0 of 618 campaigns differ). 876 of 1,232 tactics were created after their own intake “ended”. |
| intake_duration |
|
Both fresh (see the two rows above). | How many days intake took. | End minus start. | End minus start. | intake_end_date − intake_start_date |
Campaign number on a tactic row Correct arithmetic on a campaign-level span. |
Stage 2: Creative the content is written and designed
| Column | Source table | How fresh | In plain words | What the team agreed | What the report actually does | Exact conditions | Do they match? |
|---|---|---|---|---|---|---|---|
| creative_start_date |
|
Fresh. Newest Content task created 18 Sep 2026.Routing: no date column. Holds tactic IDs up to 10,485 while the tactic list stops at 8,669, so it is still being loaded. |
The day work on the content began. | In general: “Content Task Entered”. For Workfront emails: “creative starts at the creative intake task being completed” |
For Workfront emails: shows the intake end date. For everything else: the day the first “Content” task was created. |
Rule (WF email): WHEN tactic_type = 'email' AND platform = 'WF' THEN intake end dateRule (others): min(entry_date) WHERE task_type = 'Content' GROUP BY tactic_idIndirect: rules 2 and 3. |
Displayed date follows the board, but see creative_duration. |
| creative_end_date |
|
Fresh. Newest Content task completed 18 Sep 2026. |
The day the last piece of content was finished. | “Content Task Completed” / “last content submissino task” | The latest completion date of any “Content” task for the tactic. | Rule: max(actual_completion_date) WHERE task_type = 'Content' GROUP BY tactic_idIndirect: rule 2. |
Matches |
| creative_duration |
|
Fresh (18 Sep 2026). | How many days creative took. | End minus start. | End minus the first Content task date, for every tactic, including Workfront emails. | creative_end_date − min(entry_date of Content tasks)Indirect: ignores the special Workfront-email start shown in creative_start_date. |
No For Workfront emails the duration is not the gap between the two dates shown on the row. Evidence: 214 of 220 Workfront-email rows with a creative duration. 0 of 589 other rows. |
Stage 3: Targeting the audience who will receive it is chosen and delivered
| Column | Source table | How fresh | In plain words | What the team agreed | What the report actually does | Exact conditions | Do they match? |
|---|---|---|---|---|---|---|---|
| targeting_start_date |
|
Frozen. No new or edited row since 19 May 2026. Labelling stopped: last Targeting Intake task created 10 Mar 2026 (678 rows).Routing: no date column. |
The day work on choosing the audience began. | Workfront: “beginning of targeting is essentially tactic creation.” Content Pantry: “For CP, start of targetting for now.” (tactic creation) Display: “Do we have this?” Other email: “will be hard.” |
Content Pantry tactics: the day the tactic was created. All others: the day the first “Targeting Intake” task was created. |
Source (CP): tactic_signals → min(created_dt) when platform = 'CP'Source (others): signals_workfront_tasks → min(entry_date) WHERE task_type = 'Targeting Intake'Indirect: rules 2 and 3. |
In practice, yes The task is created the same day as the tactic: 551 of 570 Workfront rows equal the tactic creation date; 186 of 186 CP rows do. But a stand-in the board says there is no real “targeting began” signal. |
| targeting_end_date |
|
1. Stale. Latest file 16 May 2026. 2. Frozen since 19 May 2026. 3. Labelling stopped: last Targeting Production File task created 6 Jun 2025 (396 rows).Routing: no date column. |
The day the audience was ready. | Content Pantry with DARF: first file marked T.Content Pantry without DARF: “In market status is end of targetting.” Wanted, not built: “ACC should send us an audience count after it is processed . This is the end of Targeting” |
Tried in this order: 1. If a T file exists → earliest DARF file date.2. Else, if CP and “in market” → the day the tactic record was last edited. 3. Else → the day the first “Targeting Production File” task was created. |
1. darf_tracking: WHEN count(*) FILTER (WHERE status = 'T') > 0 THEN min(file_name_date). Note that the date picked is not limited to T files2. tactic_signals: WHEN platform = 'CP' AND current_status = 'in_market' THEN min(update_dt)3. signals_workfront_tasks: min(entry_date) WHERE task_type = 'Targeting Production File'Indirect: rules 2, 3, 4. |
No Option 3 measures when the task was created, not when it was finished. Both targeting tasks are usually created the same day, so the stage looks instant. Evidence: targeting lasts 0 days on 362 of 454 rows (80%). Option 2 almost never applies: only 3 of 1,027 CP tactics are “in market”. |
| targeting_flow |
|
Every targeting source is stale. | A label saying which of the rules above was used. | not defined | CP DARF Enabled, CP Non-DARF Enabled, or WF Email. Blank if no complete pair of dates. |
Same conditions as above; the label only appears when both a start and an end exist. | Thin 756 rows have a start, 488 have an end, only 454 have both (12% of the report). CP Non-DARF Enabled never appears. |
Stage 4: Fulfillment the tactic is built, scheduled and goes live
| Column | Source table | How fresh | In plain words | What the team agreed | What the report actually does | Exact conditions | Do they match? |
|---|---|---|---|---|---|---|---|
| fulfillment_start_date |
|
Display: Frozen snapshot, loaded by hand. Ends 6 Apr 2026. WF email: Fresh (18 Sep 2026). CP email: Stale. Latest file 16 May 2026. |
The day building/scheduling the tactic began. | Display: “first status change of placement task per tactic” Workfront email: “Fulfillment start for WF email is first content task completed.” CP email: first DARF file for the tactic |
Display: first placement status change. Workfront email: the day the first Content task was created. CP email: first DARF file of any kind. |
Display: min(entry_date) WHERE tactic_type = 'display media'WF email: min(entry_date) WHERE task_type = 'Content' when platform = 'WF'CP email: min(file_name_date) when platform <> 'WF'Indirect: rules 2, 3, 4. Display join is spsu.tactic_id = ts.id through a decimal number. |
Display matches Workfront email does not it uses task created where the board says task completed, so fulfillment “starts” when creative starts, and the stages overlap. Evidence: fulfillment starts before creative ends on 201 of 533 rows with real dates. |
| fulfillment_end_date |
|
1. Frozen snapshot, by hand. Ends 6 Apr 2026. 2. Stale. Latest file 16 May 2026. 3. _2: one-off export, latest completion 14 Apr 2026; original: 21 Oct 2025.4. No load date. 5. Frozen since 19 May 2026. |
The launch date: the day the tactic went live. The most important date in the report. | Display: “Fulfillment End (DME) will be at DME Launched per tactic” Workfront email: “last completion of a schedule for deployment task.” CP email: last DARF file marked P |
Tried in this order. First three are real evidence. Last two are planned dates. 1. Display: last time a placement hit “DME Launch Ready” or “DME Launched”. 2. CP email: last P file.3. WF email: last “Schedule for Deployment” task completed. 4. Email only: the planned start date from a Content Pantry planning table. 5. Anything left: the tactic’s own planned date, if it is in the past. |
1. max(entry_date) WHERE task_status IN ('DME Launch Ready','DME Launched')2. max(file_name_date) WHERE status = 'P' AND platform <> 'WF'3. max("Actual Completion Date") WHERE platform = 'WF'4. max("Tactic_Start_Date")5. planned_end_dt if < now() (stored table) or planned_start_dt if < now() (board SQL)Indirect: rules 2, 3, 4. Option 5 depends on the date the report is refreshed, so it fills more rows at every refresh without any new evidence. |
Options 1–3 match the board Accepting “Launch Ready” changes nothing: all 589 launched display tactics reached “DME Launched”. Real launch dates can be overwritten by a planned date The report takes the latest date across all five options, not the first that applies. Evidence: for Content Pantry emails the reported launch date is later than the last P file on 14 of 35 rows (by 1–7 days). Display (546 of 547) and WF email (689 of 689) are unaffected.Options 4–5 are plans, not launches this is the borrowed 51% (April table) / 64% (manual run). The board itself asks: “Is this the correct fulfillment end? A lot of tactic skip over RDP and LRY it seems.” |
| fulfillment_flow |
|
Every real-evidence source is stale. | A label saying which of the five options gave the launch date. | not defined | DME, CP Email DARF Enabled, WF Email = real. TLP Metadata, Tactic Start Date (Fallback) = planned. |
Same conditions as above. In the stored table the label for option 5 checks planned_start_dt while the date uses planned_end_dt. |
The label is the only way to tell real from planned In the stored table 72 rows carry the fallback label with no date, and only 384 of 1,245 fallback dates are actually the planned start. |
| fulfillment_duration |
|
Same as those two rows. | How many days fulfillment took. | End minus start. | End minus start. | fulfillment_end_date − fulfillment_start_date |
Only meaningful when the end is real (options 1–3). |
Columns built on top of the stages
| Column | Source table | How fresh | In plain words | What the team agreed | What the report actually does | Exact conditions | Do they match? |
|---|---|---|---|---|---|---|---|
| tactic_launched |
|
Every real-evidence source is stale. | Did this tactic go live? Yes/no. | Nothing. The boards’ fifth stage (Deployment / In-Market) is an empty box. | “Yes” whenever a launch date exists, including planned dates. | fulfillment_end_date IS NOT NULL |
Not a launch status It reads “launched” on 92% of rows in the April table and 99.8% in the manual run, including tactics planned for the future. |
| tactic_creation_to_fulfillment_duration |
|
Frozen since 19 May 2026; launch-date sources stale. | Days from creating the tactic to going live, the headline “time to market”. | not defined | Launch date minus tactic creation date. | fulfillment_end_date − ts.created_dt::date |
Inherits the launch-date problem over half are “days until the planned date”. |
| quarter, month, year |
|
Frozen since 19 May 2026. | Which period the tactic belongs to. | Not defined. | The quarter/month/year of the planned start date. | EXTRACT(… FROM ts.planned_start_dt) |
A forecast, not an outcome Confirmed on 3,703 of 3,703 rows. It says when a tactic was scheduled, not when anything happened. |
| is_bad_fulfillment_duration |
|
Same as those two rows. | A warning flag for impossible numbers. | “If the sequence doesn’t make sense it is bad data and needs to be filtered out” | Flags a row only when the duration is negative. A blank duration is marked “fine”. | CASE WHEN (end − start) < 0 THEN true ELSE false END |
Misses the common failure 3,667 rows are marked “fine”; 2,071 of them have no duration at all. |
Stage by stage, in one line each
| Stage | Verdict |
|---|---|
| Intake | Right signal, wrong level. It is a campaign measurement copied onto every tactic. |
| Creative | Closest to agreed. One real defect: the Workfront-email duration does not match the dates shown. |
| Targeting | No agreed definition, and the numbers are not meaningful: 80% of measured rows show 0 days, because the report compares two task-creation dates. Filled on only 12% of rows. |
| Fulfillment | The three real launch signals match what was agreed. The two planned-date fallbacks do not, and they supply more than half the launch dates. Start dates overlap with Creative. |
inferred from the evidence above. What the board admits is not defined at all: targeting start for display and for non-Content-Pantry email, targeting end in general (“we don’t have them all yet”), fulfillment for Content Pantry tactics without DARF, and any deployment / in-market stage.
03Source tables and how fresh they are
Nine source tables feed the report. Row counts and latest dates were measured 2026-09-21 against the live database.
fresh · stale or frozen · cannot tell, meaning the table has no date to check.
Used by every stage
| Table | What it provides | Freshness |
|---|---|---|
| analytical_model.tactic_signals | The master list of tactics, one row each: name, type, status, test flag, created date, planned start and end. The report starts from this table: a tactic missing here cannot appear at all. | Frozen. 7,231 rows. No new or edited row since 19 May 2026, four months stale. |
| analytical_model.campaign_signals | The campaign list: name, test flag, created date. | Fresh. 4,525 rows, last updated 18 Sep 2026. |
By stage
| Stage | Table | What it provides | Freshness |
|---|---|---|---|
| Intake | analytical_model.campaign_signals | Intake start: the campaign creation date. | Fresh (18 Sep 2026). |
| Intake | public.signals_workfront_tasks rows where task_type = Creative Intake | Intake end: the latest intake task completed in the campaign. | The table loads daily: 361,190 rows, latest task entered 20 Sep 2026. Only 244 rows carry this label; the newest completed 10 Sep 2026. |
| Creative | public.signals_workfront_tasks rows where task_type = Content | Creative start (first Content task created) and end (last Content task completed). | Fresh. 9,392 rows with this label; the newest completed 18 Sep 2026. |
| Targeting | public.signals_tactic_cp_wf | Labels each tactic Content Pantry or Workfront. This decides which targeting and fulfillment rule the tactic follows. | Unknown. 8,912 rows, no date column. |
| Targeting | analytical_model.tactic_signals | Content Pantry tactics: targeting start (tactic created date) and the “in market” targeting end. | Frozen (19 May 2026). |
| Targeting | public.signals_workfront_tasks rows where task_type = Targeting Intake Targeting Production File | Workfront tactics: targeting start and end. | The labelling has stopped. Last Targeting Intake task entered 10 Mar 2026 (678 rows). Last Targeting Production File task entered 6 Jun 2025 (396 rows). |
| Targeting | public.darf_tracking where status = T | Content Pantry email: targeting end: the audience is ready. | Stale. About 206 million rows; latest file 16 May 2026. |
| Fulfillment | public.signals_placement_status_updates_04062026 | Display media: fulfillment start (first status change) and launch date (status reaches DME Launched). | A frozen snapshot loaded by hand. 85,657 rows, ending 6 Apr 2026. |
| Fulfillment | public.darf_tracking where status = P | Content Pantry email: fulfillment start (first file) and launch date (last P file). | Stale (16 May 2026). |
| Fulfillment | public.signals_wf_email_fulfillment_tasks_2 | Workfront email: launch date: the last “Schedule for Deployment” task completed. | A one-off April export. 1,482 rows, latest completion 14 Apr 2026. Dates stored as text; 401 rows have no tactic ID. |
| Fulfillment | public.signals_workfront_tasks rows where task_type = Content | Workfront email: fulfillment start, reused from the first Content task. | Fresh. |
| Fulfillment | analytical_model.campaign_tactic_metadata | The planned start date, used as a fallback launch date and labelled TLP Metadata. | Unknown. 4,652 rows, no load date. Planned dates run to 30 Sep 2026, so it is being updated. |
| Fulfillment | analytical_model.tactic_signals | The tactic’s planned start or end, used as the last-resort fallback launch date. | Frozen (19 May 2026). |
What to take from this
- Every table that records a real launch is stale (they stop on 6 April, 14 April and 16 May), and so is the tactic list itself. A recent tactic has nothing to match against, so it gets a planned date instead. Staleness is a direct cause of the borrowed dates.
- The manual run reads an even older table. The board SQL uses
signals_wf_email_fulfillment_tasks(1,105 rows, latest completion 21 Oct 2025) instead of the_2export. - Two up-to-date tables exist that the report does not read:
public.signals_placement_status_updates(live display status log, 128,585 rows, updated 21 Sep 2026) andpublic.v8_darf_files(live DARF file list, 4,038 rows, through 19 Sep 2026). They can’t simply be swapped in, though: the live display table uses status codes (LRY,LCD) in a column calledstatus, while the snapshot uses status names intask_status. - Workfront targeting labels stopped at the source. Even a perfect rebuild would show no Workfront targeting for anything in the last six months until that labelling is restarted.
- Intake and Creative are the only stages fed entirely by fresh data.
How measured: one aggregate query per table (count(*), max of the date column). darf_tracking was read only through its index, never scanned.
04What this means for the proposed design
The row count is a symptom. The real problem is that nobody can check the answer. Gaps in the underlying data get filled in without saying so, the rules that removed or kept a row leave no record, and a wrong value can’t be corrected. That makes the numbers hard to trust, and the problem builds up from three places.
| Where it starts | What actually happens |
|---|---|
| People cannot log everything the human gap |
About 78% of Workfront tasks carry no task_type, so the report never sees them. Workfront holds all of Marriott’s project work, though, so many of those tasks probably have nothing to do with these campaigns. The 78% is the most that could be missing, not a count of missing tactic work. What is certain: Targeting Intake labelling stopped in March 2026 and Targeting Production File in June 2025, and the display placement table is a snapshot someone loaded by hand on 6 April. None of this is anyone’s fault: it is what happens when logging is a side task. |
| The automated logic fills the gaps instead of reporting them the system gap |
Where there is no launch evidence, the query substitutes a planned date and still marks the tactic launched: 99.8% of rows read “launched” when the logic is run today. It takes max() across five sources, so a planned date that happens to be later overwrites a real one (14 of 35 Content Pantry email rows). Every refresh fills more rows without a single new piece of evidence. |
| Together they remove visibility and recourse the trust gap |
Exclusions, the choice of source and the gap-filling all happen inside one calculation in one stored table (a materialised view). You cannot see which rule applied to a tactic, cannot reliably tell a measured date from a borrowed one, and cannot correct a value you know is wrong, because the view is rebuilt from scratch at every refresh. So the numbers get argued about instead of used. |
The proposal below is about making each of these three problems visible and fixable, rather than keeping more rows. It uses dimension tables that record why a row is out, one fact table per phase where every date says where it came from, and a validation table where a person’s correction can be stored.
Dimension table (dim_): a master list of things, such as every campaign or every tactic, with their descriptive details.
Fact table (fct_): a table of events or measurements, here the start and end date of each phase for each tactic.
Materialised view: a query whose results are saved as a table and rebuilt from scratch each time it is refreshed.
dbt: an open-source tool for writing, testing and version-controlling the SQL that builds these tables.
COALESCE(a, b): SQL for “use a if it exists, otherwise use b”.
Today: one query, one table
The proposed design, wherever it ends up running
The design has three parts. Dimension tables keep every tactic and record why one is left out of the report. One fact table per phase stores each date along with where it came from. And a {table_name}_manual_validation table sits beside every dimension and fact table, so a person’s correction has a place to be stored. The three options that follow differ only in which database this runs in.
What an edit looks like
Amanda knows tactic 88412 went live on 12 June, and the report shows 18 June because it borrowed a planned date.
-
She edits in Retool/Other Web Apps
A row is written to fct_tactic_fulfillment_manual_validation:
ref_key 88412,end_date 2026-06-12,edited_by Amanda,edited_atnow. -
The fact table is untouched
fct_tactic_fulfillment still holds
2026-06-18,end_source 'TLP Metadata',is_measured false. The original value is kept, even after the correction. -
The view resolves the two
COALESCE(m.end_date, f.end_date)returns 12 June (Amanda’s value, because it exists). The source column says'confirmed by Amanda', so nobody mistakes it for a measured date. -
The report keeps both dates
It shows 12 June, labelled as confirmed by a person. The borrowed 18 June stays on record underneath, so the change can be checked and undone.
inferred: a worked example of the proposal, not something the current report can do.
Three ways to get there
The design above is the same in all three. What changes is which database it runs in, which in turn decides how fast it moves and who can change the logic.
- lives in Marriott’s Postgres
- lives in a Block & Tackle Postgres, which B&T builds and runs
Build at B&T, hand the automation back
Build and validate the pipelines on a B&T Postgres first, purely for development speed. Then split the work. Everything that follows a fixed rule is handed to Marriott’s existing pipeline build, and only the tables that actually need people to edit them are handled separately. Everything ends up inside Marriott’s system.
dim / fct tables at B&T, sources stay with Marriott
Most strategic and easiest to completeThe core source tables stay in Marriott’s system and are read from there. B&T owns the dimension and fact tables, which keeps the transformations clean and lets us change the logic ourselves, using dbt or a similar open-source tool that is version-controlled and testable. Retool/Other Web Apps sit on top, giving the team an easy-to-use screen, full customisation and a complete record of original values versus manual changes. The final report tables can be copied back to Marriott’s Postgres whenever Marriott wants them there.
Everything stays inside Marriott’s system
Work with Marriott’s engineers to automate what can be automated, and add corrections by hand where it can’t. Retool/Other Web Apps connect to Marriott’s Postgres with access limited to a few purpose-built tables. That allows some customisation without opening up the rest of the database.
The three side by side
| Option 1: build at B&T, hand back | Option 2: dim / fct at B&T | Option 3: Marriott only | |
|---|---|---|---|
| Source tables | Marriott | Marriott, read-only | Marriott |
| dim / fct tables | Marriott, after the handover | B&T | Marriott |
| Who can change the logic | Marriott’s pipeline, once handed over | B&T: dbt, version-controlled and tested | Marriott’s engineers |
| Manual edits | {table_name}_manual_validation in Marriott’s Postgres, Retool/Other Web Apps on top | Same tables at B&T, Retool/Other Web Apps on top, full record of original vs edited | A few purpose-built tables, Retool/Other Web Apps with restricted access |
| Overall fit | Moderate: the handover adds time and complexity | Most strategic and easiest to complete | Unknown: depends on Marriott’s engineering capacity |
| Complexity we carry | Decent amount: two systems during the build | Contained: one system, end to end | Needs a lot of coordination |
| Reporting inside Marriott | Native | Write the report tables back when they want them | Native |
| Main trade-off | Ends up in Marriott’s system, but the handover is the slowest part | Depends on a B&T-owned database | Least disruption, least control over the timeline |
inferred: options for discussion, not a decision. All three produce the same tables and the same record of changes. They differ in where it runs and how quickly it gets there.
Five questions, today and under the proposal
| Can you… | Today | Proposed |
|---|---|---|
| tell a measured date from a borrowed one? | Only the flow label hints at it, and it disagrees with the date: 72 rows carry the fallback label with no date, and only 384 of 1,245 fallback dates are actually the planned start. | is_measured and end_source sit on the same row as end_date. Planned dates keep their own named columns and never enter end_date. |
| see how a rule applied to a given tactic? | No. The twelve rules run inside one WHERE and the row is simply gone. There is nothing left to ask about. |
in_report and exclusion_reason on dim_tactic. One GROUP BY reproduces the waterfall in section 1, on demand. |
| correct a value you know is wrong? | No. The materialised view is rebuilt from scratch at every refresh, so any edit is erased. | {table_name}_manual_validation beside every dimension and fact table, coalesced in a view, with the person’s name carried through into the source column. |
| say “we do not measure this yet”? | No. A blank is filled with a plan. tactic_launched reads “launched” on 99.8% of rows when the logic runs today, including tactics planned for the future. | Yes. is_measured false with an empty end_date is a valid answer, and it is the accurate one for Targeting today. |
| explain what changed since the last refresh? | No. 302 tactics left the report over the summer because a campaign flag flipped, and nothing surfaced it. | When in_report changes, it shows up as a count moving on a dashboard instead of rows disappearing without notice. |
inferred from the measured evidence in sections 1–3. The 302 figure is measured: tactics present in the April table and absent today solely because their campaign’s is_test flag is now true, mostly Boutiques_Shops and Boutiques_Global, both in_market.
How this moves forward
Step by step, and nothing gets built until the step before it is settled. The first three are decisions, not engineering: they need one working session with the tables in sections 1 to 3 on screen.
-
Stage definitions
Sign off what each stage date means, column by column, against section 2. Targeting is the one to settle first, because the right answer there may be “not measured yet” rather than a definition.
-
Exclusions
A yes or no on the ~560 rows removed by name-matching in steps 6 to 12, and on the Boutiques test flag. This is a decision for Marcus and Amanda, not more SQL.
-
Data freshness
Agree what gets fixed at source: why tactic_signals stopped on 19 May, why targeting labelling stopped, and whether the live placement and DARF tables replace the frozen snapshots.
-
Workflow decisioning
Pick one of the three options above. It sets who owns the tables, who can change the logic, and how fast the rest of this moves.
Once a workflow is agreed, we come back with the next part of the proposal: the table specifications column by column, the validation rules, what sits in Retool/Other Web Apps and who can use it, and the order of work, in enough detail to estimate.
Build mode starts after that, not before. Every problem in sections 1 to 3 is a definition or a data-ownership question first, and building on top of definitions nobody has agreed would just give the same wrong answers in new tables.