From 28a1a91cd2c20486276ee37dacce623ed8a75d6e Mon Sep 17 00:00:00 2001
From: Rob Mensching <rob@firegiant.com>
Date: Tue, 27 Sep 2022 01:44:33 -0700
Subject: Refactor patch filtering on the path towards making it work properly

---
 .../Bind/AttachPatchTransformsCommand.cs           | 1312 --------------------
 .../Bind/CreatePatchSubStoragesCommand.cs          |  791 ++++++++++++
 .../Bind/CreatePatchTransformsCommand.cs           |   17 +-
 .../Bind/GenerateSectionIdsCommand.cs              |  225 ++++
 .../Bind/ReduceTransformCommand.cs                 |  549 ++++++++
 .../WixToolset.Core.WindowsInstaller/MspBackend.cs |    8 +-
 .../Unbind/UnbindDatabaseCommand.cs                |  220 +---
 src/wix/WixToolset.Core/Compiler.cs                |    6 +-
 .../WixToolsetTest.CoreIntegration/PatchFixture.cs |   29 +
 .../TestData/PatchFamilyFilter/Patch.wxs           |    4 +-
 10 files changed, 1630 insertions(+), 1531 deletions(-)
 delete mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
 create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs
 create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs
 create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs

(limited to 'src')

diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
deleted file mode 100644
index 6d37fdc2..00000000
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs
+++ /dev/null
@@ -1,1312 +0,0 @@
-// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
-
-namespace WixToolset.Core.WindowsInstaller.Bind
-{
-    using System;
-    using System.Collections.Generic;
-    using System.Globalization;
-    using System.Linq;
-    using System.Text.RegularExpressions;
-    using WixToolset.Core.Native.Msi;
-    using WixToolset.Data;
-    using WixToolset.Data.Symbols;
-    using WixToolset.Data.WindowsInstaller;
-    using WixToolset.Data.WindowsInstaller.Rows;
-    using WixToolset.Extensibility.Services;
-
-    /// <summary>
-    /// Include transforms in a patch.
-    /// </summary>
-    internal class AttachPatchTransformsCommand
-    {
-        private static readonly string[] PatchUninstallBreakingTables = new[]
-        {
-            "AppId",
-            "BindImage",
-            "Class",
-            "Complus",
-            "CreateFolder",
-            "DuplicateFile",
-            "Environment",
-            "Extension",
-            "Font",
-            "IniFile",
-            "IsolatedComponent",
-            "LockPermissions",
-            "MIME",
-            "MoveFile",
-            "MsiLockPermissionsEx",
-            "MsiServiceConfig",
-            "MsiServiceConfigFailureActions",
-            "ODBCAttribute",
-            "ODBCDataSource",
-            "ODBCDriver",
-            "ODBCSourceAttribute",
-            "ODBCTranslator",
-            "ProgId",
-            "PublishComponent",
-            "RemoveIniFile",
-            "SelfReg",
-            "ServiceControl",
-            "ServiceInstall",
-            "TypeLib",
-            "Verb",
-        };
-
-        private readonly TableDefinitionCollection tableDefinitions;
-
-        public AttachPatchTransformsCommand(IMessaging messaging, IBackendHelper backendHelper, Intermediate intermediate, IEnumerable<PatchTransform> transforms)
-        {
-            this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
-            this.Messaging = messaging;
-            this.BackendHelper = backendHelper;
-            this.Intermediate = intermediate;
-            this.Transforms = transforms;
-        }
-
-        private IMessaging Messaging { get; }
-
-        private IBackendHelper BackendHelper { get; }
-
-        private Intermediate Intermediate { get; }
-
-        private IEnumerable<PatchTransform> Transforms { get; }
-
-        public IEnumerable<SubStorage> SubStorages { get; private set; }
-
-        public IEnumerable<SubStorage> Execute()
-        {
-            var subStorages = new List<SubStorage>();
-
-            if (this.Transforms == null || !this.Transforms.Any())
-            {
-                this.Messaging.Write(ErrorMessages.PatchWithoutTransforms());
-                return subStorages;
-            }
-
-            var summaryInfo = this.ExtractPatchSummaryInfo();
-
-            var section = this.Intermediate.Sections.First();
-
-            var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList();
-
-            // Get the patch id from the WixPatchId symbol.
-            var patchSymbol = symbols.OfType<WixPatchSymbol>().FirstOrDefault();
-
-            if (String.IsNullOrEmpty(patchSymbol.Id?.Id))
-            {
-                this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp());
-                return subStorages;
-            }
-
-            if (String.IsNullOrEmpty(patchSymbol.ClientPatchId))
-            {
-                this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp());
-                return subStorages;
-            }
-
-            // enumerate patch.Media to map diskId to Media row
-            var patchMediaByDiskId = symbols.OfType<MediaSymbol>().ToDictionary(t => t.DiskId);
-
-            if (patchMediaByDiskId.Count == 0)
-            {
-                this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp());
-                return subStorages;
-            }
-
-            // populate MSP summary information
-            var patchMetadata = this.PopulateSummaryInformation(summaryInfo, symbols, patchSymbol);
-
-            // enumerate transforms
-            var productCodes = new SortedSet<string>();
-            var transformNames = new List<string>();
-            var validTransform = new List<Tuple<string, WindowsInstallerData>>();
-
-            var baselineSymbolsById = symbols.OfType<WixPatchBaselineSymbol>().ToDictionary(t => t.Id.Id);
-
-            foreach (var mainTransform in this.Transforms)
-            {
-                var baselineSymbol = baselineSymbolsById[mainTransform.Baseline];
-
-                var patchRefSymbols = symbols.OfType<WixPatchRefSymbol>().ToList();
-                if (patchRefSymbols.Count > 0)
-                {
-                    if (!this.ReduceTransform(mainTransform.Transform, patchRefSymbols))
-                    {
-                        // transform has none of the content authored into this patch
-                        continue;
-                    }
-                }
-
-                // Validate the transform doesn't break any patch specific rules.
-                this.Validate(mainTransform);
-
-                // ensure consistent File.Sequence within each Media
-                var mediaSymbol = patchMediaByDiskId[baselineSymbol.DiskId];
-
-                // Ensure that files are sequenced after the last file in any transform.
-                if (mainTransform.Transform.Tables.TryGetTable("Media", out var transformMediaTable))
-                {
-                    foreach (MediaRow transformMediaRow in transformMediaTable.Rows)
-                    {
-                        if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < transformMediaRow.LastSequence)
-                        {
-                            // The Binder will pre-increment the sequence.
-                            mediaSymbol.LastSequence = transformMediaRow.LastSequence;
-                        }
-                    }
-                }
-
-                // Use the Media/@DiskId if greater than the last sequence for backward compatibility.
-                if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < mediaSymbol.DiskId)
-                {
-                    mediaSymbol.LastSequence = mediaSymbol.DiskId;
-                }
-
-                // Ignore media table in the transform.
-                mainTransform.Transform.Tables.Remove("Media");
-                mainTransform.Transform.Tables.Remove("MsiDigitalSignature");
-
-                var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchSymbol, mainTransform.Transform, mediaSymbol, baselineSymbol, out var productCode);
-
-                productCode = productCode.ToUpperInvariant();
-                productCodes.Add(productCode);
-                validTransform.Add(Tuple.Create(productCode, mainTransform.Transform));
-
-                // Attach the main and paired transforms to the patch object.
-                var baseTransformName = mainTransform.Baseline;
-                var countSuffix = "." + validTransform.Count.ToString(CultureInfo.InvariantCulture);
-
-                if (PatchConstants.PairedPatchTransformPrefix.Length + baseTransformName.Length + countSuffix.Length > PatchConstants.MaxPatchTransformName)
-                {
-                    var trimmedTransformName = baseTransformName.Substring(0, PatchConstants.MaxPatchTransformName - PatchConstants.PairedPatchTransformPrefix.Length - countSuffix.Length);
-
-                    this.Messaging.Write(WindowsInstallerBackendWarnings.LongPatchBaselineIdTrimmed(baselineSymbol.SourceLineNumbers, baseTransformName, trimmedTransformName));
-
-                    baseTransformName = trimmedTransformName;
-                }
-
-                var transformName = baseTransformName + countSuffix;
-                subStorages.Add(new SubStorage(transformName, mainTransform.Transform));
-                transformNames.Add(":" + transformName);
-
-                var pairedTransformName = PatchConstants.PairedPatchTransformPrefix + transformName;
-                subStorages.Add(new SubStorage(pairedTransformName, pairedTransform));
-                transformNames.Add(":" + pairedTransformName);
-            }
-
-            if (validTransform.Count == 0)
-            {
-                this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms());
-                return subStorages;
-            }
-
-            // Validate that a patch authored as removable is actually removable
-            if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1")
-            {
-                var uninstallable = true;
-
-                foreach (var entry in validTransform)
-                {
-                    uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2);
-                }
-
-                if (!uninstallable)
-                {
-                    this.Messaging.Write(ErrorMessages.PatchNotRemovable());
-                    return subStorages;
-                }
-            }
-
-            // Finish filling tables with transform-dependent data.
-            productCodes = FinalizePatchProductCodes(symbols, productCodes);
-
-            // Semicolon delimited list of the product codes that can accept the patch.
-            summaryInfo.Add(SummaryInformationType.PatchProductCodes, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers)
-            {
-                PropertyId = SummaryInformationType.PatchProductCodes,
-                Value = String.Join(";", productCodes)
-            });
-
-            // Semicolon delimited list of transform substorage names in the order they are applied.
-            summaryInfo.Add(SummaryInformationType.TransformNames, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers)
-            {
-                PropertyId = SummaryInformationType.TransformNames,
-                Value = String.Join(";", transformNames)
-            });
-
-            // Put the summary information that was extracted back in now that it is updated.
-            foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId))
-            {
-                section.AddSymbol(readSummaryInfo);
-            }
-
-            this.SubStorages = subStorages;
-
-            return subStorages;
-        }
-
-        private Dictionary<SummaryInformationType, SummaryInformationSymbol> ExtractPatchSummaryInfo()
-        {
-            var result = new Dictionary<SummaryInformationType, SummaryInformationSymbol>();
-
-            foreach (var section in this.Intermediate.Sections)
-            {
-                // Remove all summary information from the symbols and remember those that
-                // are not calculated or reserved.
-                foreach (var patchSummaryInfo in section.Symbols.OfType<SummaryInformationSymbol>().ToList())
-                {
-                    section.RemoveSymbol(patchSummaryInfo);
-
-                    if (patchSummaryInfo.PropertyId != SummaryInformationType.PatchProductCodes &&
-                        patchSummaryInfo.PropertyId != SummaryInformationType.PatchCode &&
-                        patchSummaryInfo.PropertyId != SummaryInformationType.PatchInstallerRequirement &&
-                        patchSummaryInfo.PropertyId != SummaryInformationType.Reserved11 &&
-                        patchSummaryInfo.PropertyId != SummaryInformationType.Reserved14 &&
-                        patchSummaryInfo.PropertyId != SummaryInformationType.Reserved16)
-                    {
-                        result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo);
-                    }
-                }
-            }
-
-            return result;
-        }
-
-        private Dictionary<string, MsiPatchMetadataSymbol> PopulateSummaryInformation(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, List<IntermediateSymbol> symbols, WixPatchSymbol patchSymbol)
-        {
-            // PID_CODEPAGE
-            if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage))
-            {
-                // Set the code page by default to the same code page for the
-                // string pool in the database.
-                AddSummaryInformation(SummaryInformationType.Codepage, patchSymbol.Codepage?.ToString(CultureInfo.InvariantCulture) ?? "0", patchSymbol.SourceLineNumbers);
-            }
-
-            // GUID patch code for the patch.
-            AddSummaryInformation(SummaryInformationType.PatchCode, patchSymbol.Id.Id, patchSymbol.SourceLineNumbers);
-
-            // Indicates the minimum Windows Installer version that is required to install the patch.
-            AddSummaryInformation(SummaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchSymbol.SourceLineNumbers);
-
-            if (!summaryInfo.ContainsKey(SummaryInformationType.Security))
-            {
-                AddSummaryInformation(SummaryInformationType.Security, "4", patchSymbol.SourceLineNumbers); // Read-only enforced;
-            }
-
-            // Use authored comments or default to display name.
-            MsiPatchMetadataSymbol commentsSymbol = null;
-
-            var metadataSymbols = symbols.OfType<MsiPatchMetadataSymbol>().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property);
-
-            if (!summaryInfo.ContainsKey(SummaryInformationType.Title) &&
-                metadataSymbols.TryGetValue("DisplayName", out var displayName))
-            {
-                AddSummaryInformation(SummaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers);
-
-                // Default comments to use display name as-is.
-                commentsSymbol = displayName;
-            }
-
-            // TODO: This code below seems unnecessary given the codepage is set at the top of this method.
-            //if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage) &&
-            //    metadataValues.TryGetValue("CodePage", out var codepage))
-            //{
-            //    AddSummaryInformation(SummaryInformationType.Codepage, codepage);
-            //}
-
-            if (!summaryInfo.ContainsKey(SummaryInformationType.PatchPackageName) &&
-                metadataSymbols.TryGetValue("Description", out var description))
-            {
-                AddSummaryInformation(SummaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers);
-            }
-
-            if (!summaryInfo.ContainsKey(SummaryInformationType.Author) &&
-                metadataSymbols.TryGetValue("ManufacturerName", out var manufacturer))
-            {
-                AddSummaryInformation(SummaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers);
-            }
-
-            // Special metadata marshalled through the build.
-            //var wixMetadataValues = symbols.OfType<WixPatchMetadataSymbol>().ToDictionary(t => t.Id.Id, t => t.Value);
-
-            //if (wixMetadataValues.TryGetValue("Comments", out var wixComments))
-            if (metadataSymbols.TryGetValue("Comments", out var wixComments))
-            {
-                commentsSymbol = wixComments;
-            }
-
-            // Write the package comments to summary info.
-            if (!summaryInfo.ContainsKey(SummaryInformationType.Comments) &&
-                commentsSymbol != null)
-            {
-                AddSummaryInformation(SummaryInformationType.Comments, commentsSymbol.Value, commentsSymbol.SourceLineNumbers);
-            }
-
-            return metadataSymbols;
-
-            void AddSummaryInformation(SummaryInformationType type, string value, SourceLineNumber sourceLineNumber)
-            {
-                summaryInfo.Add(type, new SummaryInformationSymbol(sourceLineNumber)
-                {
-                    PropertyId = type,
-                    Value = value
-                });
-            }
-        }
-
-        /// <summary>
-        /// Ensure transform is uninstallable.
-        /// </summary>
-        /// <param name="productCode">Product code in transform.</param>
-        /// <param name="transform">Transform generated by torch.</param>
-        /// <returns>True if the transform is uninstallable</returns>
-        private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform)
-        {
-            var success = true;
-
-            foreach (var tableName in PatchUninstallBreakingTables)
-            {
-                if (transform.TryGetTable(tableName, out var table))
-                {
-                    foreach (var row in table.Rows.Where(r => r.Operation == RowOperation.Add))
-                    {
-                        success = false;
-
-                        var primaryKey = row.GetPrimaryKey('/') ?? String.Empty;
-
-                        this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey));
-                    }
-                }
-            }
-
-            return success;
-        }
-
-        /// <summary>
-        /// Reduce the transform according to the patch references.
-        /// </summary>
-        /// <param name="transform">transform generated by torch.</param>
-        /// <param name="patchRefSymbols">Table contains patch family filter.</param>
-        /// <returns>true if the transform is not empty</returns>
-        private bool ReduceTransform(WindowsInstallerData transform, IEnumerable<WixPatchRefSymbol> patchRefSymbols)
-        {
-            // identify sections to keep
-            var oldSections = new Dictionary<string, Row>();
-            var newSections = new Dictionary<string, Row>();
-            var tableKeyRows = new Dictionary<string, Dictionary<string, Row>>();
-            var sequenceList = new List<Table>();
-            var componentFeatureAddsIndex = new Dictionary<string, List<string>>();
-            var customActionTable = new Dictionary<string, Row>();
-            var directoryTableAdds = new Dictionary<string, Row>();
-            var featureTableAdds = new Dictionary<string, Row>();
-            var keptComponents = new Dictionary<string, Row>();
-            var keptDirectories = new Dictionary<string, Row>();
-            var keptFeatures = new Dictionary<string, Row>();
-            var keptLockPermissions = new HashSet<string>();
-            var keptMsiLockPermissionExs = new HashSet<string>();
-
-            var componentCreateFolderIndex = new Dictionary<string, List<string>>();
-            var directoryLockPermissionsIndex = new Dictionary<string, List<Row>>();
-            var directoryMsiLockPermissionsExIndex = new Dictionary<string, List<Row>>();
-
-            foreach (var patchRefSymbol in patchRefSymbols)
-            {
-                var tableName = patchRefSymbol.Table;
-                var key = patchRefSymbol.PrimaryKeys;
-
-                // Short circuit filtering if all changes should be included.
-                if ("*" == tableName && "*" == key)
-                {
-                    RemoveProductCodeFromTransform(transform);
-                    return true;
-                }
-
-                if (!transform.Tables.TryGetTable(tableName, out var table))
-                {
-                    // Table not found.
-                    continue;
-                }
-
-                // Index the table.
-                if (!tableKeyRows.TryGetValue(tableName, out var keyRows))
-                {
-                    keyRows = new Dictionary<string, Row>();
-                    tableKeyRows.Add(tableName, keyRows);
-
-                    foreach (var newRow in table.Rows)
-                    {
-                        var primaryKey = newRow.GetPrimaryKey();
-                        keyRows.Add(primaryKey, newRow);
-                    }
-                }
-
-                if (!keyRows.TryGetValue(key, out var row))
-                {
-                    // Row not found.
-                    continue;
-                }
-
-                // Differ.sectionDelimiter
-                var sections = row.SectionId.Split('/');
-                oldSections[sections[0]] = row;
-                newSections[sections[1]] = row;
-            }
-
-            // throw away sections not referenced
-            var keptRows = 0;
-            Table directoryTable = null;
-            Table featureTable = null;
-            Table lockPermissionsTable = null;
-            Table msiLockPermissionsTable = null;
-
-            foreach (var table in transform.Tables)
-            {
-                if ("_SummaryInformation" == table.Name)
-                {
-                    continue;
-                }
-
-                if (table.Name == "AdminExecuteSequence"
-                    || table.Name == "AdminUISequence"
-                    || table.Name == "AdvtExecuteSequence"
-                    || table.Name == "InstallUISequence"
-                    || table.Name == "InstallExecuteSequence")
-                {
-                    sequenceList.Add(table);
-                    continue;
-                }
-
-                for (var i = 0; i < table.Rows.Count; i++)
-                {
-                    var row = table.Rows[i];
-
-                    if (table.Name == "CreateFolder")
-                    {
-                        var createFolderComponentId = row.FieldAsString(1);
-
-                        if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out var directoryList))
-                        {
-                            directoryList = new List<string>();
-                            componentCreateFolderIndex.Add(createFolderComponentId, directoryList);
-                        }
-
-                        directoryList.Add(row.FieldAsString(0));
-                    }
-
-                    if (table.Name == "CustomAction")
-                    {
-                        customActionTable.Add(row.FieldAsString(0), row);
-                    }
-
-                    if (table.Name == "Directory")
-                    {
-                        directoryTable = table;
-                        if (RowOperation.Add == row.Operation)
-                        {
-                            directoryTableAdds.Add(row.FieldAsString(0), row);
-                        }
-                    }
-
-                    if (table.Name == "Feature")
-                    {
-                        featureTable = table;
-                        if (RowOperation.Add == row.Operation)
-                        {
-                            featureTableAdds.Add(row.FieldAsString(0), row);
-                        }
-                    }
-
-                    if (table.Name == "FeatureComponents")
-                    {
-                        if (RowOperation.Add == row.Operation)
-                        {
-                            var featureId = row.FieldAsString(0);
-                            var componentId = row.FieldAsString(1);
-
-                            if (!componentFeatureAddsIndex.TryGetValue(componentId, out var featureList))
-                            {
-                                featureList = new List<string>();
-                                componentFeatureAddsIndex.Add(componentId, featureList);
-                            }
-
-                            featureList.Add(featureId);
-                        }
-                    }
-
-                    if (table.Name == "LockPermissions")
-                    {
-                        lockPermissionsTable = table;
-                        if ("CreateFolder" == row.FieldAsString(1))
-                        {
-                            var directoryId = row.FieldAsString(0);
-
-                            if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out var rowList))
-                            {
-                                rowList = new List<Row>();
-                                directoryLockPermissionsIndex.Add(directoryId, rowList);
-                            }
-
-                            rowList.Add(row);
-                        }
-                    }
-
-                    if (table.Name == "MsiLockPermissionsEx")
-                    {
-                        msiLockPermissionsTable = table;
-                        if ("CreateFolder" == row.FieldAsString(1))
-                        {
-                            var directoryId = row.FieldAsString(0);
-
-                            if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var rowList))
-                            {
-                                rowList = new List<Row>();
-                                directoryMsiLockPermissionsExIndex.Add(directoryId, rowList);
-                            }
-
-                            rowList.Add(row);
-                        }
-                    }
-
-                    if (null == row.SectionId)
-                    {
-                        table.Rows.RemoveAt(i);
-                        i--;
-                    }
-                    else
-                    {
-                        var sections = row.SectionId.Split('/');
-                        // ignore the row without section id.
-                        if (0 == sections[0].Length && 0 == sections[1].Length)
-                        {
-                            table.Rows.RemoveAt(i);
-                            i--;
-                        }
-                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
-                        {
-                            if ("Component" == table.Name)
-                            {
-                                keptComponents.Add(row.FieldAsString(0), row);
-                            }
-
-                            if ("Directory" == table.Name)
-                            {
-                                keptDirectories.Add(row.FieldAsString(0), row);
-                            }
-
-                            if ("Feature" == table.Name)
-                            {
-                                keptFeatures.Add(row.FieldAsString(0), row);
-                            }
-
-                            keptRows++;
-                        }
-                        else
-                        {
-                            table.Rows.RemoveAt(i);
-                            i--;
-                        }
-                    }
-                }
-            }
-
-            keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
-
-            if (null != directoryTable)
-            {
-                foreach (var componentRow in keptComponents.Values)
-                {
-                    var componentId = componentRow.FieldAsString(0);
-
-                    if (RowOperation.Add == componentRow.Operation)
-                    {
-                        // Make sure each added component has its required directory and feature heirarchy.
-                        var directoryId = componentRow.FieldAsString(2);
-                        while (null != directoryId && directoryTableAdds.TryGetValue(directoryId, out var directoryRow))
-                        {
-                            if (!keptDirectories.ContainsKey(directoryId))
-                            {
-                                directoryTable.Rows.Add(directoryRow);
-                                keptDirectories.Add(directoryId, directoryRow);
-                                keptRows++;
-                            }
-
-                            directoryId = directoryRow.FieldAsString(1);
-                        }
-
-                        if (componentFeatureAddsIndex.TryGetValue(componentId, out var componentFeatureIds))
-                        {
-                            foreach (var featureId in componentFeatureIds)
-                            {
-                                var currentFeatureId = featureId;
-                                while (null != currentFeatureId && featureTableAdds.TryGetValue(currentFeatureId, out var featureRow))
-                                {
-                                    if (!keptFeatures.ContainsKey(currentFeatureId))
-                                    {
-                                        featureTable.Rows.Add(featureRow);
-                                        keptFeatures.Add(currentFeatureId, featureRow);
-                                        keptRows++;
-                                    }
-
-                                    currentFeatureId = featureRow.FieldAsString(1);
-                                }
-                            }
-                        }
-                    }
-
-                    // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept.
-                    foreach (var keptComponentId in keptComponents.Keys)
-                    {
-                        if (componentCreateFolderIndex.TryGetValue(keptComponentId, out var directoryList))
-                        {
-                            foreach (var directoryId in directoryList)
-                            {
-                                if (directoryLockPermissionsIndex.TryGetValue(directoryId, out var lockPermissionsRowList))
-                                {
-                                    foreach (var lockPermissionsRow in lockPermissionsRowList)
-                                    {
-                                        var key = lockPermissionsRow.GetPrimaryKey('/');
-                                        if (keptLockPermissions.Add(key))
-                                        {
-                                            lockPermissionsTable.Rows.Add(lockPermissionsRow);
-                                            keptRows++;
-                                        }
-                                    }
-                                }
-
-                                if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var msiLockPermissionsExRowList))
-                                {
-                                    foreach (var msiLockPermissionsExRow in msiLockPermissionsExRowList)
-                                    {
-                                        var key = msiLockPermissionsExRow.GetPrimaryKey('/');
-                                        if (keptMsiLockPermissionExs.Add(key))
-                                        {
-                                            msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow);
-                                            keptRows++;
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
-
-            // Delete tables that are empty.
-            var tablesToDelete = transform.Tables.Where(t => t.Rows.Count == 0).Select(t => t.Name).ToList();
-
-            foreach (var tableName in tablesToDelete)
-            {
-                transform.Tables.Remove(tableName);
-            }
-
-            return keptRows > 0;
-        }
-
-        private void Validate(PatchTransform patchTransform)
-        {
-            var transformPath = patchTransform.Baseline; // TODO: this is used in error messages, how best to set it?
-            var transform = patchTransform.Transform;
-
-            // Changing the ProdocutCode in a patch transform is not recommended.
-            if (transform.TryGetTable("Property", out var propertyTable))
-            {
-                foreach (var row in propertyTable.Rows)
-                {
-                    // Only interested in modified rows; fast check.
-                    if (RowOperation.Modify == row.Operation &&
-                        "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal))
-                    {
-                        this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended());
-                    }
-                }
-            }
-
-            // If there is nothing in the component table we can return early because the remaining checks are component based.
-            if (!transform.TryGetTable("Component", out var componentTable))
-            {
-                return;
-            }
-
-            // Index Feature table row operations
-            var featureOps = new Dictionary<string, RowOperation>();
-            if (transform.TryGetTable("Feature", out var featureTable))
-            {
-                foreach (var row in featureTable.Rows)
-                {
-                    featureOps[row.FieldAsString(0)] = row.Operation;
-                }
-            }
-
-            // Index Component table and check for keypath modifications
-            var componentKeyPath = new Dictionary<string, string>();
-            var deletedComponent = new Dictionary<string, Row>();
-            foreach (var row in componentTable.Rows)
-            {
-                var id = row.FieldAsString(0);
-                var keypath = row.FieldAsString(5) ?? String.Empty;
-
-                componentKeyPath.Add(id, keypath);
-
-                if (RowOperation.Delete == row.Operation)
-                {
-                    deletedComponent.Add(id, row);
-                }
-                else if (RowOperation.Modify == row.Operation)
-                {
-                    if (row.Fields[1].Modified)
-                    {
-                        // Changing the guid of a component is equal to deleting the old one and adding a new one.
-                        deletedComponent.Add(id, row);
-                    }
-
-                    // If the keypath is modified its an error
-                    if (row.Fields[5].Modified)
-                    {
-                        this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath));
-                    }
-                }
-            }
-
-            // Verify changes in the file table
-            if (transform.TryGetTable("File", out var fileTable))
-            {
-                var componentWithChangedKeyPath = new Dictionary<string, string>();
-                foreach (FileRow row in fileTable.Rows)
-                {
-                    if (RowOperation.None == row.Operation)
-                    {
-                        continue;
-                    }
-
-                    var fileId = row.File;
-                    var componentId = row.Component;
-
-                    // If this file is the keypath of a component
-                    if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal))
-                    {
-                        if (row.Fields[2].Modified)
-                        {
-                            // You can't change the filename of a file that is the keypath of a component.
-                            this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath));
-                        }
-
-                        if (!componentWithChangedKeyPath.ContainsKey(componentId))
-                        {
-                            componentWithChangedKeyPath.Add(componentId, fileId);
-                        }
-                    }
-
-                    if (RowOperation.Delete == row.Operation)
-                    {
-                        // If the file is removed from a component that is not deleted.
-                        if (!deletedComponent.ContainsKey(componentId))
-                        {
-                            var foundRemoveFileEntry = false;
-                            var filename = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), false, true);
-
-                            if (transform.TryGetTable("RemoveFile", out var removeFileTable))
-                            {
-                                foreach (var removeFileRow in removeFileTable.Rows)
-                                {
-                                    if (RowOperation.Delete == removeFileRow.Operation)
-                                    {
-                                        continue;
-                                    }
-
-                                    if (componentId == removeFileRow.FieldAsString(1))
-                                    {
-                                        // Check if there is a RemoveFile entry for this file
-                                        if (null != removeFileRow[2])
-                                        {
-                                            var removeFileName = this.BackendHelper.GetMsiFileName(removeFileRow.FieldAsString(2), false, true);
-
-                                            // Convert the MSI format for a wildcard string to Regex format.
-                                            removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\.");
-
-                                            var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
-                                            if (regex.IsMatch(filename))
-                                            {
-                                                foundRemoveFileEntry = true;
-                                                break;
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-
-                            if (!foundRemoveFileEntry)
-                            {
-                                this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId));
-                            }
-                        }
-                    }
-                }
-            }
-
-            var featureComponentsTable = transform.Tables["FeatureComponents"];
-
-            if (0 < deletedComponent.Count)
-            {
-                // Index FeatureComponents table.
-                var featureComponents = new Dictionary<string, List<string>>();
-
-                if (null != featureComponentsTable)
-                {
-                    foreach (var row in featureComponentsTable.Rows)
-                    {
-                        var componentId = row.FieldAsString(1);
-
-                        if (!featureComponents.TryGetValue(componentId, out var features))
-                        {
-                            features = new List<string>();
-                            featureComponents.Add(componentId, features);
-                        }
-
-                        features.Add(row.FieldAsString(0));
-                    }
-                }
-
-                // Check to make sure if a component was deleted, the feature was too.
-                foreach (var entry in deletedComponent)
-                {
-                    if (featureComponents.TryGetValue(entry.Key, out var features))
-                    {
-                        foreach (var featureId in features)
-                        {
-                            if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete)
-                            {
-                                // The feature was not deleted.
-                                this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath));
-                            }
-                        }
-                    }
-                }
-            }
-
-            // Warn if new components are added to existing features
-            if (null != featureComponentsTable)
-            {
-                foreach (var row in featureComponentsTable.Rows)
-                {
-                    if (RowOperation.Add == row.Operation)
-                    {
-                        // Check if the feature is in the Feature table
-                        var feature_ = row.FieldAsString(0);
-                        var component_ = row.FieldAsString(1);
-
-                        // Features may not be present if not referenced
-                        if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_])
-                        {
-                            this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath));
-                        }
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Remove the ProductCode property from the transform.
-        /// </summary>
-        /// <param name="transform">The transform.</param>
-        /// <remarks>
-        /// Changing the ProductCode is not supported in a patch.
-        /// </remarks>
-        private static void RemoveProductCodeFromTransform(WindowsInstallerData transform)
-        {
-            if (transform.Tables.TryGetTable("Property", out var propertyTable))
-            {
-                for (var i = 0; i < propertyTable.Rows.Count; ++i)
-                {
-                    var propertyRow = propertyTable.Rows[i];
-                    var property = propertyRow.FieldAsString(0);
-
-                    if ("ProductCode" == property)
-                    {
-                        propertyTable.Rows.RemoveAt(i);
-                        break;
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Check if the section is in a PatchFamily.
-        /// </summary>
-        /// <param name="oldSection">Section id in target wixout</param>
-        /// <param name="newSection">Section id in upgrade wixout</param>
-        /// <param name="oldSections">Dictionary contains section id should be kept in the baseline wixout.</param>
-        /// <param name="newSections">Dictionary contains section id should be kept in the upgrade wixout.</param>
-        /// <returns>true if section in patch family</returns>
-        private static bool IsInPatchFamily(string oldSection, string newSection, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections)
-        {
-            var result = false;
-
-            if ((String.IsNullOrEmpty(oldSection) && newSections.ContainsKey(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.ContainsKey(oldSection)))
-            {
-                result = true;
-            }
-            else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.ContainsKey(oldSection) || newSections.ContainsKey(newSection)))
-            {
-                result = true;
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Reduce the transform sequence tables.
-        /// </summary>
-        /// <param name="sequenceList">ArrayList of tables to be reduced</param>
-        /// <param name="oldSections">Hashtable contains section id should be kept in the baseline wixout.</param>
-        /// <param name="newSections">Hashtable contains section id should be kept in the target wixout.</param>
-        /// <param name="customAction">Hashtable contains all the rows in the CustomAction table.</param>
-        /// <returns>Number of rows left</returns>
-        private static int ReduceTransformSequenceTable(List<Table> sequenceList, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections, Dictionary<string, Row> customAction)
-        {
-            var keptRows = 0;
-
-            foreach (var currentTable in sequenceList)
-            {
-                for (var i = 0; i < currentTable.Rows.Count; i++)
-                {
-                    var row = currentTable.Rows[i];
-                    var actionName = row.Fields[0].Data.ToString();
-                    var sections = row.SectionId.Split('/');
-                    var isSectionIdEmpty = (sections[0].Length == 0 && sections[1].Length == 0);
-
-                    if (row.Operation == RowOperation.None)
-                    {
-                        // Ignore the rows without section id.
-                        if (isSectionIdEmpty)
-                        {
-                            currentTable.Rows.RemoveAt(i);
-                            i--;
-                        }
-                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
-                        {
-                            keptRows++;
-                        }
-                        else
-                        {
-                            currentTable.Rows.RemoveAt(i);
-                            i--;
-                        }
-                    }
-                    else if (row.Operation == RowOperation.Modify)
-                    {
-                        var sequenceChanged = row.Fields[2].Modified;
-                        var conditionChanged = row.Fields[1].Modified;
-
-                        if (sequenceChanged && !conditionChanged)
-                        {
-                            keptRows++;
-                        }
-                        else if (!sequenceChanged && conditionChanged)
-                        {
-                            if (isSectionIdEmpty)
-                            {
-                                currentTable.Rows.RemoveAt(i);
-                                i--;
-                            }
-                            else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
-                            {
-                                keptRows++;
-                            }
-                            else
-                            {
-                                currentTable.Rows.RemoveAt(i);
-                                i--;
-                            }
-                        }
-                        else if (sequenceChanged && conditionChanged)
-                        {
-                            if (isSectionIdEmpty)
-                            {
-                                row.Fields[1].Modified = false;
-                                keptRows++;
-                            }
-                            else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
-                            {
-                                keptRows++;
-                            }
-                            else
-                            {
-                                row.Fields[1].Modified = false;
-                                keptRows++;
-                            }
-                        }
-                    }
-                    else if (row.Operation == RowOperation.Delete)
-                    {
-                        if (isSectionIdEmpty)
-                        {
-                            // it is a stardard action which is added by wix, we should keep this action.
-                            row.Operation = RowOperation.None;
-                            keptRows++;
-                        }
-                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
-                        {
-                            keptRows++;
-                        }
-                        else
-                        {
-                            if (customAction.ContainsKey(actionName))
-                            {
-                                currentTable.Rows.RemoveAt(i);
-                                i--;
-                            }
-                            else
-                            {
-                                // it is a stardard action, we should keep this action.
-                                row.Operation = RowOperation.None;
-                                keptRows++;
-                            }
-                        }
-                    }
-                    else if (row.Operation == RowOperation.Add)
-                    {
-                        if (isSectionIdEmpty)
-                        {
-                            keptRows++;
-                        }
-                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
-                        {
-                            keptRows++;
-                        }
-                        else
-                        {
-                            if (customAction.ContainsKey(actionName))
-                            {
-                                currentTable.Rows.RemoveAt(i);
-                                i--;
-                            }
-                            else
-                            {
-                                keptRows++;
-                            }
-                        }
-                    }
-                }
-            }
-
-            return keptRows;
-        }
-
-        /// <summary>
-        /// Create the #transform for the given main transform.
-        /// </summary>
-        private WindowsInstallerData BuildPairedTransform(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, Dictionary<string, MsiPatchMetadataSymbol> patchMetadata, WixPatchSymbol patchIdSymbol, WindowsInstallerData mainTransform, MediaSymbol mediaSymbol, WixPatchBaselineSymbol baselineSymbol, out string productCode)
-        {
-            productCode = null;
-
-            var pairedTransform = new WindowsInstallerData(null)
-            {
-                Type = OutputType.Transform,
-                Codepage = mainTransform.Codepage
-            };
-
-            // lookup productVersion property to correct summaryInformation
-            var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1);
-
-            var mainSummaryTable = mainTransform.Tables["_SummaryInformation"];
-            var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0));
-
-            var baselineValidationFlags = ((int)baselineSymbol.ValidationFlags).ToString(CultureInfo.InvariantCulture);
-
-            if (!mainSummaryRows.ContainsKey((int)SummaryInformationType.TransformValidationFlags))
-            {
-                var mainSummaryRow = mainSummaryTable.CreateRow(baselineSymbol.SourceLineNumbers);
-                mainSummaryRow[0] = (int)SummaryInformationType.TransformValidationFlags;
-                mainSummaryRow[1] = baselineValidationFlags;
-            }
-
-            // copy summary information from core transform
-            var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
-
-            foreach (var mainSummaryRow in mainSummaryTable.Rows)
-            {
-                var type = (SummaryInformationType)mainSummaryRow.FieldAsInteger(0);
-                var value = mainSummaryRow.FieldAsString(1);
-                switch (type)
-                {
-                    case SummaryInformationType.TransformProductCodes:
-                        var propertyData = value.Split(';');
-                        var oldProductVersion = propertyData[0].Substring(38);
-                        var upgradeCode = propertyData[2];
-                        productCode = propertyData[0].Substring(0, 38);
-
-                        if (newProductVersion == null)
-                        {
-                            newProductVersion = oldProductVersion;
-                        }
-
-                        // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade'
-                        mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
-                        value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
-                        break;
-                    case SummaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML.
-                        value = baselineValidationFlags;
-                        mainSummaryRow[1] = value;
-                        break;
-                }
-
-                var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers);
-                pairedSummaryRow[0] = mainSummaryRow[0];
-                pairedSummaryRow[1] = value;
-            }
-
-            if (productCode == null)
-            {
-                this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo());
-                return null;
-            }
-
-            // Copy File table
-            if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count)
-            {
-                var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition);
-
-                foreach (var mainFileRow in mainFileTable.Rows.Cast<FileRow>())
-                {
-                    // Set File.Sequence to non null to satisfy transform bind and suppress any
-                    // change to File.Sequence to avoid bloat.
-                    mainFileRow.Sequence = 1;
-                    mainFileRow.Fields[7].Modified = false;
-
-                    // Override authored media to the media provided in the patch.
-                    mainFileRow.DiskId = mediaSymbol.DiskId;
-
-                    // Delete's don't need rows in the paired transform.
-                    if (mainFileRow.Operation == RowOperation.Delete)
-                    {
-                        continue;
-                    }
-
-                    var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers);
-                    pairedFileRow.Operation = RowOperation.Modify;
-                    mainFileRow.CopyTo(pairedFileRow);
-
-                    // Force modified File rows to appear in the transform.
-                    switch (mainFileRow.Operation)
-                    {
-                        case RowOperation.Modify:
-                        case RowOperation.Add:
-                            pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
-                            pairedFileRow.Fields[6].Modified = true;
-                            pairedFileRow.Operation = mainFileRow.Operation;
-                            break;
-                        default:
-                            pairedFileRow.Fields[6].Modified = false;
-                            break;
-                    }
-                }
-            }
-
-            // Add Media row to pairedTransform
-            var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]);
-            var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaSymbol.SourceLineNumbers);
-            pairedMediaRow.Operation = RowOperation.Add;
-            pairedMediaRow.DiskId = mediaSymbol.DiskId;
-            pairedMediaRow.LastSequence = mediaSymbol.LastSequence ?? 0;
-            pairedMediaRow.DiskPrompt = mediaSymbol.DiskPrompt;
-            pairedMediaRow.Cabinet = mediaSymbol.Cabinet;
-            pairedMediaRow.VolumeLabel = mediaSymbol.VolumeLabel;
-            pairedMediaRow.Source = mediaSymbol.Source;
-
-            // Add PatchPackage for this Media
-            var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]);
-            pairedPackageTable.Operation = TableOperation.Add;
-            var pairedPackageRow = pairedPackageTable.CreateRow(mediaSymbol.SourceLineNumbers);
-            pairedPackageRow.Operation = RowOperation.Add;
-            pairedPackageRow[0] = patchIdSymbol.Id.Id;
-            pairedPackageRow[1] = mediaSymbol.DiskId;
-
-            // Add the property to the patch transform's Property table.
-            var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]);
-            pairedPropertyTable.Operation = TableOperation.Add;
-
-            // Add property to both identify client patches and whether those patches are removable or not
-            patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalSymbol);
-
-            var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalSymbol?.SourceLineNumbers);
-            pairedPropertyRow.Operation = RowOperation.Add;
-            pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".AllowRemoval");
-            pairedPropertyRow[1] = allowRemovalSymbol?.Value ?? "0";
-
-            // Add this patch code GUID to the patch transform to identify
-            // which patches are installed, including in multi-patch
-            // installations.
-            pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers);
-            pairedPropertyRow.Operation = RowOperation.Add;
-            pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".PatchCode");
-            pairedPropertyRow[1] = patchIdSymbol.Id.Id;
-
-            // Add PATCHNEWPACKAGECODE to apply to admin layouts.
-            pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers);
-            pairedPropertyRow.Operation = RowOperation.Add;
-            pairedPropertyRow[0] = "PATCHNEWPACKAGECODE";
-            pairedPropertyRow[1] = patchIdSymbol.Id.Id;
-
-            // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts.
-            if (summaryInfo.TryGetValue(SummaryInformationType.Subject, out var subjectSymbol))
-            {
-                pairedPropertyRow = pairedPropertyTable.CreateRow(subjectSymbol.SourceLineNumbers);
-                pairedPropertyRow.Operation = RowOperation.Add;
-                pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT";
-                pairedPropertyRow[1] = subjectSymbol.Value;
-            }
-
-            if (summaryInfo.TryGetValue(SummaryInformationType.Comments, out var commentsSymbol))
-            {
-                pairedPropertyRow = pairedPropertyTable.CreateRow(commentsSymbol.SourceLineNumbers);
-                pairedPropertyRow.Operation = RowOperation.Add;
-                pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS";
-                pairedPropertyRow[1] = commentsSymbol.Value;
-            }
-
-            return pairedTransform;
-        }
-
-        private static SortedSet<string> FinalizePatchProductCodes(List<IntermediateSymbol> symbols, SortedSet<string> productCodes)
-        {
-            var patchTargetSymbols = symbols.OfType<WixPatchTargetSymbol>().ToList();
-
-            if (patchTargetSymbols.Any())
-            {
-                var targets = new SortedSet<string>();
-                var replace = true;
-                foreach (var wixPatchTargetRow in patchTargetSymbols)
-                {
-                    var target = wixPatchTargetRow.ProductCode.ToUpperInvariant();
-                    if (target == "*")
-                    {
-                        replace = false;
-                    }
-                    else
-                    {
-                        targets.Add(target);
-                    }
-                }
-
-                // Replace the target ProductCodes with the authored list.
-                if (replace)
-                {
-                    productCodes = targets;
-                }
-                else
-                {
-                    // Copy the authored target ProductCodes into the list.
-                    foreach (var target in targets)
-                    {
-                        productCodes.Add(target);
-                    }
-                }
-            }
-
-            return productCodes;
-        }
-    }
-}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs
new file mode 100644
index 00000000..db121137
--- /dev/null
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs
@@ -0,0 +1,791 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Core.WindowsInstaller.Bind
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Globalization;
+    using System.Linq;
+    using System.Text.RegularExpressions;
+    using WixToolset.Core.Native.Msi;
+    using WixToolset.Data;
+    using WixToolset.Data.Symbols;
+    using WixToolset.Data.WindowsInstaller;
+    using WixToolset.Data.WindowsInstaller.Rows;
+    using WixToolset.Extensibility.Services;
+
+    /// <summary>
+    /// Include transforms in a patch.
+    /// </summary>
+    internal class CreatePatchSubStoragesCommand
+    {
+        private static readonly string[] PatchUninstallBreakingTables = new[]
+        {
+            "AppId",
+            "BindImage",
+            "Class",
+            "Complus",
+            "CreateFolder",
+            "DuplicateFile",
+            "Environment",
+            "Extension",
+            "Font",
+            "IniFile",
+            "IsolatedComponent",
+            "LockPermissions",
+            "MIME",
+            "MoveFile",
+            "MsiLockPermissionsEx",
+            "MsiServiceConfig",
+            "MsiServiceConfigFailureActions",
+            "ODBCAttribute",
+            "ODBCDataSource",
+            "ODBCDriver",
+            "ODBCSourceAttribute",
+            "ODBCTranslator",
+            "ProgId",
+            "PublishComponent",
+            "RemoveIniFile",
+            "SelfReg",
+            "ServiceControl",
+            "ServiceInstall",
+            "TypeLib",
+            "Verb",
+        };
+
+        private readonly TableDefinitionCollection tableDefinitions;
+
+        public CreatePatchSubStoragesCommand(IMessaging messaging, IBackendHelper backendHelper, Intermediate intermediate, IEnumerable<PatchTransform> transforms)
+        {
+            this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
+            this.Messaging = messaging;
+            this.BackendHelper = backendHelper;
+            this.Intermediate = intermediate;
+            this.Transforms = transforms;
+        }
+
+        private IMessaging Messaging { get; }
+
+        private IBackendHelper BackendHelper { get; }
+
+        private Intermediate Intermediate { get; }
+
+        private IEnumerable<PatchTransform> Transforms { get; }
+
+        public IEnumerable<SubStorage> SubStorages { get; private set; }
+
+        public IEnumerable<SubStorage> Execute()
+        {
+            var subStorages = new List<SubStorage>();
+
+            if (this.Transforms == null || !this.Transforms.Any())
+            {
+                this.Messaging.Write(ErrorMessages.PatchWithoutTransforms());
+                return subStorages;
+            }
+
+            var summaryInfo = this.ExtractPatchSummaryInfo();
+
+            var section = this.Intermediate.Sections.First();
+
+            var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList();
+
+            // Get the patch id from the WixPatchId symbol.
+            var patchSymbol = symbols.OfType<WixPatchSymbol>().FirstOrDefault();
+
+            if (String.IsNullOrEmpty(patchSymbol.Id?.Id))
+            {
+                this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp());
+                return subStorages;
+            }
+
+            if (String.IsNullOrEmpty(patchSymbol.ClientPatchId))
+            {
+                this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp());
+                return subStorages;
+            }
+
+            // enumerate patch.Media to map diskId to Media row
+            var patchMediaByDiskId = symbols.OfType<MediaSymbol>().ToDictionary(t => t.DiskId);
+
+            if (patchMediaByDiskId.Count == 0)
+            {
+                this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp());
+                return subStorages;
+            }
+
+            // populate MSP summary information
+            var patchMetadata = this.PopulateSummaryInformation(summaryInfo, symbols, patchSymbol);
+
+            // enumerate transforms
+            var productCodes = new SortedSet<string>();
+            var transformNames = new List<string>();
+            var validTransform = new List<Tuple<string, WindowsInstallerData>>();
+
+            var baselineSymbolsById = symbols.OfType<WixPatchBaselineSymbol>().ToDictionary(t => t.Id.Id);
+
+            foreach (var mainTransform in this.Transforms)
+            {
+                // Validate the transform doesn't break any patch specific rules.
+                this.Validate(mainTransform);
+
+                // ensure consistent File.Sequence within each Media
+                var baselineSymbol = baselineSymbolsById[mainTransform.Baseline];
+                var mediaSymbol = patchMediaByDiskId[baselineSymbol.DiskId];
+
+                // Ensure that files are sequenced after the last file in any transform.
+                if (mainTransform.Transform.Tables.TryGetTable("Media", out var transformMediaTable))
+                {
+                    foreach (MediaRow transformMediaRow in transformMediaTable.Rows)
+                    {
+                        if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < transformMediaRow.LastSequence)
+                        {
+                            // The Binder will pre-increment the sequence.
+                            mediaSymbol.LastSequence = transformMediaRow.LastSequence;
+                        }
+                    }
+                }
+
+                // Use the Media/@DiskId if greater than the last sequence for backward compatibility.
+                if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < mediaSymbol.DiskId)
+                {
+                    mediaSymbol.LastSequence = mediaSymbol.DiskId;
+                }
+
+                // Ignore media table in the transform.
+                mainTransform.Transform.Tables.Remove("Media");
+                mainTransform.Transform.Tables.Remove("MsiDigitalSignature");
+
+                var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchSymbol, mainTransform.Transform, mediaSymbol, baselineSymbol, out var productCode);
+
+                productCode = productCode.ToUpperInvariant();
+                productCodes.Add(productCode);
+                validTransform.Add(Tuple.Create(productCode, mainTransform.Transform));
+
+                // Attach the main and paired transforms to the patch object.
+                var baseTransformName = mainTransform.Baseline;
+                var countSuffix = "." + validTransform.Count.ToString(CultureInfo.InvariantCulture);
+
+                if (PatchConstants.PairedPatchTransformPrefix.Length + baseTransformName.Length + countSuffix.Length > PatchConstants.MaxPatchTransformName)
+                {
+                    var trimmedTransformName = baseTransformName.Substring(0, PatchConstants.MaxPatchTransformName - PatchConstants.PairedPatchTransformPrefix.Length - countSuffix.Length);
+
+                    this.Messaging.Write(WindowsInstallerBackendWarnings.LongPatchBaselineIdTrimmed(baselineSymbol.SourceLineNumbers, baseTransformName, trimmedTransformName));
+
+                    baseTransformName = trimmedTransformName;
+                }
+
+                var transformName = baseTransformName + countSuffix;
+                subStorages.Add(new SubStorage(transformName, mainTransform.Transform));
+                transformNames.Add(":" + transformName);
+
+                var pairedTransformName = PatchConstants.PairedPatchTransformPrefix + transformName;
+                subStorages.Add(new SubStorage(pairedTransformName, pairedTransform));
+                transformNames.Add(":" + pairedTransformName);
+            }
+
+            if (validTransform.Count == 0)
+            {
+                this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms());
+                return subStorages;
+            }
+
+            // Validate that a patch authored as removable is actually removable
+            if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1")
+            {
+                var uninstallable = true;
+
+                foreach (var entry in validTransform)
+                {
+                    uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2);
+                }
+
+                if (!uninstallable)
+                {
+                    this.Messaging.Write(ErrorMessages.PatchNotRemovable());
+                    return subStorages;
+                }
+            }
+
+            // Finish filling tables with transform-dependent data.
+            productCodes = FinalizePatchProductCodes(symbols, productCodes);
+
+            // Semicolon delimited list of the product codes that can accept the patch.
+            summaryInfo.Add(SummaryInformationType.PatchProductCodes, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers)
+            {
+                PropertyId = SummaryInformationType.PatchProductCodes,
+                Value = String.Join(";", productCodes)
+            });
+
+            // Semicolon delimited list of transform substorage names in the order they are applied.
+            summaryInfo.Add(SummaryInformationType.TransformNames, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers)
+            {
+                PropertyId = SummaryInformationType.TransformNames,
+                Value = String.Join(";", transformNames)
+            });
+
+            // Put the summary information that was extracted back in now that it is updated.
+            foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId))
+            {
+                section.AddSymbol(readSummaryInfo);
+            }
+
+            this.SubStorages = subStorages;
+
+            return subStorages;
+        }
+
+        private Dictionary<SummaryInformationType, SummaryInformationSymbol> ExtractPatchSummaryInfo()
+        {
+            var result = new Dictionary<SummaryInformationType, SummaryInformationSymbol>();
+
+            foreach (var section in this.Intermediate.Sections)
+            {
+                // Remove all summary information from the symbols and remember those that
+                // are not calculated or reserved.
+                foreach (var patchSummaryInfo in section.Symbols.OfType<SummaryInformationSymbol>().ToList())
+                {
+                    section.RemoveSymbol(patchSummaryInfo);
+
+                    if (patchSummaryInfo.PropertyId != SummaryInformationType.PatchProductCodes &&
+                        patchSummaryInfo.PropertyId != SummaryInformationType.PatchCode &&
+                        patchSummaryInfo.PropertyId != SummaryInformationType.PatchInstallerRequirement &&
+                        patchSummaryInfo.PropertyId != SummaryInformationType.Reserved11 &&
+                        patchSummaryInfo.PropertyId != SummaryInformationType.Reserved14 &&
+                        patchSummaryInfo.PropertyId != SummaryInformationType.Reserved16)
+                    {
+                        result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo);
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        private Dictionary<string, MsiPatchMetadataSymbol> PopulateSummaryInformation(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, List<IntermediateSymbol> symbols, WixPatchSymbol patchSymbol)
+        {
+            // PID_CODEPAGE
+            if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage))
+            {
+                // Set the code page by default to the same code page for the
+                // string pool in the database.
+                AddSummaryInformation(SummaryInformationType.Codepage, patchSymbol.Codepage?.ToString(CultureInfo.InvariantCulture) ?? "0", patchSymbol.SourceLineNumbers);
+            }
+
+            // GUID patch code for the patch.
+            AddSummaryInformation(SummaryInformationType.PatchCode, patchSymbol.Id.Id, patchSymbol.SourceLineNumbers);
+
+            // Indicates the minimum Windows Installer version that is required to install the patch.
+            AddSummaryInformation(SummaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchSymbol.SourceLineNumbers);
+
+            if (!summaryInfo.ContainsKey(SummaryInformationType.Security))
+            {
+                AddSummaryInformation(SummaryInformationType.Security, "4", patchSymbol.SourceLineNumbers); // Read-only enforced;
+            }
+
+            // Use authored comments or default to display name.
+            MsiPatchMetadataSymbol commentsSymbol = null;
+
+            var metadataSymbols = symbols.OfType<MsiPatchMetadataSymbol>().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property);
+
+            if (!summaryInfo.ContainsKey(SummaryInformationType.Title) &&
+                metadataSymbols.TryGetValue("DisplayName", out var displayName))
+            {
+                AddSummaryInformation(SummaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers);
+
+                // Default comments to use display name as-is.
+                commentsSymbol = displayName;
+            }
+
+            // TODO: This code below seems unnecessary given the codepage is set at the top of this method.
+            //if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage) &&
+            //    metadataValues.TryGetValue("CodePage", out var codepage))
+            //{
+            //    AddSummaryInformation(SummaryInformationType.Codepage, codepage);
+            //}
+
+            if (!summaryInfo.ContainsKey(SummaryInformationType.PatchPackageName) &&
+                metadataSymbols.TryGetValue("Description", out var description))
+            {
+                AddSummaryInformation(SummaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers);
+            }
+
+            if (!summaryInfo.ContainsKey(SummaryInformationType.Author) &&
+                metadataSymbols.TryGetValue("ManufacturerName", out var manufacturer))
+            {
+                AddSummaryInformation(SummaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers);
+            }
+
+            // Special metadata marshalled through the build.
+            //var wixMetadataValues = symbols.OfType<WixPatchMetadataSymbol>().ToDictionary(t => t.Id.Id, t => t.Value);
+
+            //if (wixMetadataValues.TryGetValue("Comments", out var wixComments))
+            if (metadataSymbols.TryGetValue("Comments", out var wixComments))
+            {
+                commentsSymbol = wixComments;
+            }
+
+            // Write the package comments to summary info.
+            if (!summaryInfo.ContainsKey(SummaryInformationType.Comments) &&
+                commentsSymbol != null)
+            {
+                AddSummaryInformation(SummaryInformationType.Comments, commentsSymbol.Value, commentsSymbol.SourceLineNumbers);
+            }
+
+            return metadataSymbols;
+
+            void AddSummaryInformation(SummaryInformationType type, string value, SourceLineNumber sourceLineNumber)
+            {
+                summaryInfo.Add(type, new SummaryInformationSymbol(sourceLineNumber)
+                {
+                    PropertyId = type,
+                    Value = value
+                });
+            }
+        }
+
+        /// <summary>
+        /// Ensure transform is uninstallable.
+        /// </summary>
+        /// <param name="productCode">Product code in transform.</param>
+        /// <param name="transform">Transform generated by torch.</param>
+        /// <returns>True if the transform is uninstallable</returns>
+        private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform)
+        {
+            var success = true;
+
+            foreach (var tableName in PatchUninstallBreakingTables)
+            {
+                if (transform.TryGetTable(tableName, out var table))
+                {
+                    foreach (var row in table.Rows.Where(r => r.Operation == RowOperation.Add))
+                    {
+                        success = false;
+
+                        var primaryKey = row.GetPrimaryKey('/') ?? String.Empty;
+
+                        this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey));
+                    }
+                }
+            }
+
+            return success;
+        }
+
+        private void Validate(PatchTransform patchTransform)
+        {
+            var transformPath = patchTransform.Baseline;
+            var transform = patchTransform.Transform;
+
+            // Changing the ProdocutCode in a patch transform is not recommended.
+            if (transform.TryGetTable("Property", out var propertyTable))
+            {
+                foreach (var row in propertyTable.Rows)
+                {
+                    // Only interested in modified rows; fast check.
+                    if (RowOperation.Modify == row.Operation &&
+                        "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal))
+                    {
+                        this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended());
+                    }
+                }
+            }
+
+            // If there is nothing in the component table we can return early because the remaining checks are component based.
+            if (!transform.TryGetTable("Component", out var componentTable))
+            {
+                return;
+            }
+
+            // Index Feature table row operations
+            var featureOps = new Dictionary<string, RowOperation>();
+            if (transform.TryGetTable("Feature", out var featureTable))
+            {
+                foreach (var row in featureTable.Rows)
+                {
+                    featureOps[row.FieldAsString(0)] = row.Operation;
+                }
+            }
+
+            // Index Component table and check for keypath modifications
+            var componentKeyPath = new Dictionary<string, string>();
+            var deletedComponent = new Dictionary<string, Row>();
+            foreach (var row in componentTable.Rows)
+            {
+                var id = row.FieldAsString(0);
+                var keypath = row.FieldAsString(5) ?? String.Empty;
+
+                componentKeyPath.Add(id, keypath);
+
+                if (RowOperation.Delete == row.Operation)
+                {
+                    deletedComponent.Add(id, row);
+                }
+                else if (RowOperation.Modify == row.Operation)
+                {
+                    if (row.Fields[1].Modified)
+                    {
+                        // Changing the guid of a component is equal to deleting the old one and adding a new one.
+                        deletedComponent.Add(id, row);
+                    }
+
+                    // If the keypath is modified its an error
+                    if (row.Fields[5].Modified)
+                    {
+                        this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath));
+                    }
+                }
+            }
+
+            // Verify changes in the file table
+            if (transform.TryGetTable("File", out var fileTable))
+            {
+                var componentWithChangedKeyPath = new Dictionary<string, string>();
+                foreach (FileRow row in fileTable.Rows)
+                {
+                    if (RowOperation.None == row.Operation)
+                    {
+                        continue;
+                    }
+
+                    var fileId = row.File;
+                    var componentId = row.Component;
+
+                    // If this file is the keypath of a component
+                    if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal))
+                    {
+                        if (row.Fields[2].Modified)
+                        {
+                            // You can't change the filename of a file that is the keypath of a component.
+                            this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath));
+                        }
+
+                        if (!componentWithChangedKeyPath.ContainsKey(componentId))
+                        {
+                            componentWithChangedKeyPath.Add(componentId, fileId);
+                        }
+                    }
+
+                    if (RowOperation.Delete == row.Operation)
+                    {
+                        // If the file is removed from a component that is not deleted.
+                        if (!deletedComponent.ContainsKey(componentId))
+                        {
+                            var foundRemoveFileEntry = false;
+                            var filename = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), false, true);
+
+                            if (transform.TryGetTable("RemoveFile", out var removeFileTable))
+                            {
+                                foreach (var removeFileRow in removeFileTable.Rows)
+                                {
+                                    if (RowOperation.Delete == removeFileRow.Operation)
+                                    {
+                                        continue;
+                                    }
+
+                                    if (componentId == removeFileRow.FieldAsString(1))
+                                    {
+                                        // Check if there is a RemoveFile entry for this file
+                                        if (null != removeFileRow[2])
+                                        {
+                                            var removeFileName = this.BackendHelper.GetMsiFileName(removeFileRow.FieldAsString(2), false, true);
+
+                                            // Convert the MSI format for a wildcard string to Regex format.
+                                            removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\.");
+
+                                            var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+                                            if (regex.IsMatch(filename))
+                                            {
+                                                foundRemoveFileEntry = true;
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+
+                            if (!foundRemoveFileEntry)
+                            {
+                                this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId));
+                            }
+                        }
+                    }
+                }
+            }
+
+            var featureComponentsTable = transform.Tables["FeatureComponents"];
+
+            if (0 < deletedComponent.Count)
+            {
+                // Index FeatureComponents table.
+                var featureComponents = new Dictionary<string, List<string>>();
+
+                if (null != featureComponentsTable)
+                {
+                    foreach (var row in featureComponentsTable.Rows)
+                    {
+                        var componentId = row.FieldAsString(1);
+
+                        if (!featureComponents.TryGetValue(componentId, out var features))
+                        {
+                            features = new List<string>();
+                            featureComponents.Add(componentId, features);
+                        }
+
+                        features.Add(row.FieldAsString(0));
+                    }
+                }
+
+                // Check to make sure if a component was deleted, the feature was too.
+                foreach (var entry in deletedComponent)
+                {
+                    if (featureComponents.TryGetValue(entry.Key, out var features))
+                    {
+                        foreach (var featureId in features)
+                        {
+                            if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete)
+                            {
+                                // The feature was not deleted.
+                                this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath));
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Warn if new components are added to existing features
+            if (null != featureComponentsTable)
+            {
+                foreach (var row in featureComponentsTable.Rows)
+                {
+                    if (RowOperation.Add == row.Operation)
+                    {
+                        // Check if the feature is in the Feature table
+                        var feature_ = row.FieldAsString(0);
+                        var component_ = row.FieldAsString(1);
+
+                        // Features may not be present if not referenced
+                        if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_])
+                        {
+                            this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath));
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Create the #transform for the given main transform.
+        /// </summary>
+        private WindowsInstallerData BuildPairedTransform(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, Dictionary<string, MsiPatchMetadataSymbol> patchMetadata, WixPatchSymbol patchIdSymbol, WindowsInstallerData mainTransform, MediaSymbol mediaSymbol, WixPatchBaselineSymbol baselineSymbol, out string productCode)
+        {
+            productCode = null;
+
+            var pairedTransform = new WindowsInstallerData(null)
+            {
+                Type = OutputType.Transform,
+                Codepage = mainTransform.Codepage
+            };
+
+            // lookup productVersion property to correct summaryInformation
+            var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1);
+
+            var mainSummaryTable = mainTransform.Tables["_SummaryInformation"];
+            var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0));
+
+            var baselineValidationFlags = ((int)baselineSymbol.ValidationFlags).ToString(CultureInfo.InvariantCulture);
+
+            if (!mainSummaryRows.ContainsKey((int)SummaryInformationType.TransformValidationFlags))
+            {
+                var mainSummaryRow = mainSummaryTable.CreateRow(baselineSymbol.SourceLineNumbers);
+                mainSummaryRow[0] = (int)SummaryInformationType.TransformValidationFlags;
+                mainSummaryRow[1] = baselineValidationFlags;
+            }
+
+            // copy summary information from core transform
+            var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
+
+            foreach (var mainSummaryRow in mainSummaryTable.Rows)
+            {
+                var type = (SummaryInformationType)mainSummaryRow.FieldAsInteger(0);
+                var value = mainSummaryRow.FieldAsString(1);
+                switch (type)
+                {
+                    case SummaryInformationType.TransformProductCodes:
+                        var propertyData = value.Split(';');
+                        var oldProductVersion = propertyData[0].Substring(38);
+                        var upgradeCode = propertyData[2];
+                        productCode = propertyData[0].Substring(0, 38);
+
+                        if (newProductVersion == null)
+                        {
+                            newProductVersion = oldProductVersion;
+                        }
+
+                        // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade'
+                        mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
+                        value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode);
+                        break;
+                    case SummaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML.
+                        value = baselineValidationFlags;
+                        mainSummaryRow[1] = value;
+                        break;
+                }
+
+                var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers);
+                pairedSummaryRow[0] = mainSummaryRow[0];
+                pairedSummaryRow[1] = value;
+            }
+
+            if (productCode == null)
+            {
+                this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo());
+                return null;
+            }
+
+            // Copy File table
+            if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count)
+            {
+                var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition);
+
+                foreach (var mainFileRow in mainFileTable.Rows.Cast<FileRow>())
+                {
+                    // Set File.Sequence to non null to satisfy transform bind and suppress any
+                    // change to File.Sequence to avoid bloat.
+                    mainFileRow.Sequence = 1;
+                    mainFileRow.Fields[7].Modified = false;
+
+                    // Override authored media to the media provided in the patch.
+                    mainFileRow.DiskId = mediaSymbol.DiskId;
+
+                    // Delete's don't need rows in the paired transform.
+                    if (mainFileRow.Operation == RowOperation.Delete)
+                    {
+                        continue;
+                    }
+
+                    var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers);
+                    pairedFileRow.Operation = RowOperation.Modify;
+                    mainFileRow.CopyTo(pairedFileRow);
+
+                    // Force modified File rows to appear in the transform.
+                    switch (mainFileRow.Operation)
+                    {
+                        case RowOperation.Modify:
+                        case RowOperation.Add:
+                            pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded;
+                            pairedFileRow.Fields[6].Modified = true;
+                            pairedFileRow.Operation = mainFileRow.Operation;
+                            break;
+                        default:
+                            pairedFileRow.Fields[6].Modified = false;
+                            break;
+                    }
+                }
+            }
+
+            // Add Media row to pairedTransform
+            var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]);
+            var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaSymbol.SourceLineNumbers);
+            pairedMediaRow.Operation = RowOperation.Add;
+            pairedMediaRow.DiskId = mediaSymbol.DiskId;
+            pairedMediaRow.LastSequence = mediaSymbol.LastSequence ?? 0;
+            pairedMediaRow.DiskPrompt = mediaSymbol.DiskPrompt;
+            pairedMediaRow.Cabinet = mediaSymbol.Cabinet;
+            pairedMediaRow.VolumeLabel = mediaSymbol.VolumeLabel;
+            pairedMediaRow.Source = mediaSymbol.Source;
+
+            // Add PatchPackage for this Media
+            var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]);
+            pairedPackageTable.Operation = TableOperation.Add;
+            var pairedPackageRow = pairedPackageTable.CreateRow(mediaSymbol.SourceLineNumbers);
+            pairedPackageRow.Operation = RowOperation.Add;
+            pairedPackageRow[0] = patchIdSymbol.Id.Id;
+            pairedPackageRow[1] = mediaSymbol.DiskId;
+
+            // Add the property to the patch transform's Property table.
+            var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]);
+            pairedPropertyTable.Operation = TableOperation.Add;
+
+            // Add property to both identify client patches and whether those patches are removable or not
+            patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalSymbol);
+
+            var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalSymbol?.SourceLineNumbers);
+            pairedPropertyRow.Operation = RowOperation.Add;
+            pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".AllowRemoval");
+            pairedPropertyRow[1] = allowRemovalSymbol?.Value ?? "0";
+
+            // Add this patch code GUID to the patch transform to identify
+            // which patches are installed, including in multi-patch
+            // installations.
+            pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers);
+            pairedPropertyRow.Operation = RowOperation.Add;
+            pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".PatchCode");
+            pairedPropertyRow[1] = patchIdSymbol.Id.Id;
+
+            // Add PATCHNEWPACKAGECODE to apply to admin layouts.
+            pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers);
+            pairedPropertyRow.Operation = RowOperation.Add;
+            pairedPropertyRow[0] = "PATCHNEWPACKAGECODE";
+            pairedPropertyRow[1] = patchIdSymbol.Id.Id;
+
+            // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts.
+            if (summaryInfo.TryGetValue(SummaryInformationType.Subject, out var subjectSymbol))
+            {
+                pairedPropertyRow = pairedPropertyTable.CreateRow(subjectSymbol.SourceLineNumbers);
+                pairedPropertyRow.Operation = RowOperation.Add;
+                pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT";
+                pairedPropertyRow[1] = subjectSymbol.Value;
+            }
+
+            if (summaryInfo.TryGetValue(SummaryInformationType.Comments, out var commentsSymbol))
+            {
+                pairedPropertyRow = pairedPropertyTable.CreateRow(commentsSymbol.SourceLineNumbers);
+                pairedPropertyRow.Operation = RowOperation.Add;
+                pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS";
+                pairedPropertyRow[1] = commentsSymbol.Value;
+            }
+
+            return pairedTransform;
+        }
+
+        private static SortedSet<string> FinalizePatchProductCodes(List<IntermediateSymbol> symbols, SortedSet<string> productCodes)
+        {
+            var patchTargetSymbols = symbols.OfType<WixPatchTargetSymbol>().ToList();
+
+            if (patchTargetSymbols.Any())
+            {
+                var targets = new SortedSet<string>();
+                var replace = true;
+                foreach (var wixPatchTargetRow in patchTargetSymbols)
+                {
+                    var target = wixPatchTargetRow.ProductCode.ToUpperInvariant();
+                    if (target == "*")
+                    {
+                        replace = false;
+                    }
+                    else
+                    {
+                        targets.Add(target);
+                    }
+                }
+
+                // Replace the target ProductCodes with the authored list.
+                if (replace)
+                {
+                    productCodes = targets;
+                }
+                else
+                {
+                    // Copy the authored target ProductCodes into the list.
+                    foreach (var target in targets)
+                    {
+                        productCodes.Add(target);
+                    }
+                }
+            }
+
+            return productCodes;
+        }
+    }
+}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
index 6d5bff69..17583e96 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
@@ -50,13 +50,26 @@ namespace WixToolset.Core.WindowsInstaller.Bind
         {
             var patchTransforms = new List<PatchTransform>();
 
-            var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).OfType<WixPatchBaselineSymbol>();
+            var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols);
 
-            foreach (var symbol in symbols)
+            var patchBaselineSymbols = symbols.OfType<WixPatchBaselineSymbol>();
+
+            var patchRefSymbols = symbols.OfType<WixPatchRefSymbol>().ToList();
+
+            foreach (var symbol in patchBaselineSymbols)
             {
                 var targetData = this.GetWindowsInstallerData(symbol.BaselineFile.Path, BindStage.Target);
                 var updatedData = this.GetWindowsInstallerData(symbol.UpdateFile.Path, BindStage.Updated);
 
+                if (patchRefSymbols.Count > 0)
+                {
+                    var targetCommand = new GenerateSectionIdsCommand(targetData);
+                    targetCommand.Execute();
+
+                    var updatedCommand = new GenerateSectionIdsCommand(updatedData);
+                    updatedCommand.Execute();
+                }
+
                 var command = new GenerateTransformCommand(this.Messaging, targetData, updatedData, preserveUnchangedRows: true, showPedanticMessages: false);
                 var transform = command.Execute();
 
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs
new file mode 100644
index 00000000..c7bebbed
--- /dev/null
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs
@@ -0,0 +1,225 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Core.WindowsInstaller.Bind
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Globalization;
+    using WixToolset.Data.WindowsInstaller;
+
+    /// <summary>
+    /// Creates section ids on rows which form logical groupings of resources.
+    /// </summary>
+    internal class GenerateSectionIdsCommand
+    {
+        private int sectionCount;
+
+        public GenerateSectionIdsCommand(WindowsInstallerData data)
+        {
+            this.Data = data;
+        }
+
+        private WindowsInstallerData Data { get; }
+
+        public void Execute()
+        {
+            var output = this.Data;
+
+            this.sectionCount = 0;
+
+            // First assign and index section ids for the tables that are in their own sections.
+            this.AssignSectionIdsToTable(output.Tables["Binary"], 0);
+            var componentSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Component"], 0);
+            var customActionSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["CustomAction"], 0);
+            this.AssignSectionIdsToTable(output.Tables["Directory"], 0);
+            var featureSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Feature"], 0);
+            this.AssignSectionIdsToTable(output.Tables["Icon"], 0);
+            var digitalCertificateSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0);
+            this.AssignSectionIdsToTable(output.Tables["Property"], 0);
+
+            // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here.
+            var fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0);
+            var appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5);
+            var odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0);
+            var odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0);
+            var registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0);
+            var serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0);
+
+            // Now handle all the tables which only rely on previous indexes and order does not matter.
+            foreach (var table in output.Tables)
+            {
+                switch (table.Name)
+                {
+                    case "MsiFileHash":
+                        ConnectTableToSection(table, fileSectionIdIndex, 0);
+                        break;
+                    case "MsiAssembly":
+                    case "MsiAssemblyName":
+                        ConnectTableToSection(table, componentSectionIdIndex, 0);
+                        break;
+                    case "MsiPackageCertificate":
+                    case "MsiPatchCertificate":
+                        ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1);
+                        break;
+                    case "CreateFolder":
+                    case "FeatureComponents":
+                    case "MoveFile":
+                    case "ReserveCost":
+                    case "ODBCTranslator":
+                        ConnectTableToSection(table, componentSectionIdIndex, 1);
+                        break;
+                    case "TypeLib":
+                        ConnectTableToSection(table, componentSectionIdIndex, 2);
+                        break;
+                    case "Shortcut":
+                    case "Environment":
+                        ConnectTableToSection(table, componentSectionIdIndex, 3);
+                        break;
+                    case "RemoveRegistry":
+                        ConnectTableToSection(table, componentSectionIdIndex, 4);
+                        break;
+                    case "ServiceControl":
+                        ConnectTableToSection(table, componentSectionIdIndex, 5);
+                        break;
+                    case "IniFile":
+                    case "RemoveIniFile":
+                        ConnectTableToSection(table, componentSectionIdIndex, 7);
+                        break;
+                    case "AppId":
+                        ConnectTableToSection(table, appIdSectionIdIndex, 0);
+                        break;
+                    case "Condition":
+                        ConnectTableToSection(table, featureSectionIdIndex, 0);
+                        break;
+                    case "ODBCSourceAttribute":
+                        ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0);
+                        break;
+                    case "ODBCAttribute":
+                        ConnectTableToSection(table, odbcDriverSectionIdIndex, 0);
+                        break;
+                    case "AdminExecuteSequence":
+                    case "AdminUISequence":
+                    case "AdvtExecuteSequence":
+                    case "AdvtUISequence":
+                    case "InstallExecuteSequence":
+                    case "InstallUISequence":
+                        ConnectTableToSection(table, customActionSectionIdIndex, 0);
+                        break;
+                    case "LockPermissions":
+                    case "MsiLockPermissions":
+                        foreach (var row in table.Rows)
+                        {
+                            var lockObject = row.FieldAsString(0);
+                            var tableName = row.FieldAsString(1);
+                            switch (tableName)
+                            {
+                                case "File":
+                                    row.SectionId = fileSectionIdIndex[lockObject];
+                                    break;
+                                case "Registry":
+                                    row.SectionId = registrySectionIdIndex[lockObject];
+                                    break;
+                                case "ServiceInstall":
+                                    row.SectionId = serviceInstallSectionIdIndex[lockObject];
+                                    break;
+                            }
+                        }
+                        break;
+                }
+            }
+
+            // Now pass the output to each unbinder extension to allow them to analyze the output and determine their proper section ids.
+            //foreach (IUnbinderExtension extension in this.unbinderExtensions)
+            //{
+            //    extension.GenerateSectionIds(output);
+            //}
+        }
+
+        /// <summary>
+        /// Creates new section ids on all the rows in a table.
+        /// </summary>
+        /// <param name="table">The table to add sections to.</param>
+        /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param>
+        /// <returns>A dictionary containing the tables key for each row paired with its assigned section id.</returns>
+        private Dictionary<string, string> AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex)
+        {
+            var primaryKeyToSectionId = new Dictionary<string, string>();
+
+            if (null != table)
+            {
+                foreach (var row in table.Rows)
+                {
+                    row.SectionId = this.GetNewSectionId();
+
+                    primaryKeyToSectionId.Add(row.FieldAsString(rowPrimaryKeyIndex), row.SectionId);
+                }
+            }
+
+            return primaryKeyToSectionId;
+        }
+
+        /// <summary>
+        /// Connects a table's rows to an already sectioned table.
+        /// </summary>
+        /// <param name="table">The table containing rows that need to be connected to sections.</param>
+        /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param>
+        /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param>
+        private static void ConnectTableToSection(Table table, Dictionary<string, string> sectionIdIndex, int rowIndex)
+        {
+            if (null != table)
+            {
+                foreach (var row in table.Rows)
+                {
+                    if (sectionIdIndex.TryGetValue(row.FieldAsString(rowIndex), out var sectionId))
+                    {
+                        row.SectionId = sectionId;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it.
+        /// </summary>
+        /// <param name="table">The table containing rows that need to be connected to sections.</param>
+        /// <param name="sectionIdIndex">A dictionary containing keys to map table to its section.</param>
+        /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param>
+        /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param>
+        /// <returns>A dictionary containing the tables key for each row paired with its assigned section id.</returns>
+        private static Dictionary<string, string> ConnectTableToSectionAndIndex(Table table, Dictionary<string, string> sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex)
+        {
+            var newPrimaryKeyToSectionId = new Dictionary<string, string>();
+
+            if (null != table)
+            {
+                foreach (var row in table.Rows)
+                {
+                    var foreignKey = row.FieldAsString(rowIndex);
+
+                    if (!sectionIdIndex.TryGetValue(foreignKey, out var sectionId))
+                    {
+                        continue;
+                    }
+
+                    row.SectionId = sectionId;
+
+                    var primaryKey = row.FieldAsString(rowPrimaryKeyIndex);
+
+                    if (!String.IsNullOrEmpty(primaryKey) && sectionIdIndex.ContainsKey(primaryKey))
+                    {
+                        newPrimaryKeyToSectionId.Add(primaryKey, row.SectionId);
+                    }
+                }
+            }
+
+            return newPrimaryKeyToSectionId;
+        }
+
+        private string GetNewSectionId()
+        {
+            this.sectionCount++;
+
+            return "wix.section." + this.sectionCount.ToString(CultureInfo.InvariantCulture);
+        }
+    }
+}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs
new file mode 100644
index 00000000..4966a0b4
--- /dev/null
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs
@@ -0,0 +1,549 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Core.WindowsInstaller.Bind
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using WixToolset.Data;
+    using WixToolset.Data.Symbols;
+    using WixToolset.Data.WindowsInstaller;
+
+    internal class ReduceTransformCommand
+    {
+        private const char SectionDelimiter = '/';
+
+        public ReduceTransformCommand(Intermediate intermediate, IEnumerable<PatchTransform> patchTransforms)
+        {
+            this.Intermediate = intermediate;
+            this.PatchTransforms = patchTransforms;
+        }
+
+        private Intermediate Intermediate { get; }
+
+        private IEnumerable<PatchTransform> PatchTransforms { get; }
+
+        public void Execute()
+        {
+            var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList();
+
+            var patchRefSymbols = symbols.OfType<WixPatchRefSymbol>().ToList();
+
+            if (patchRefSymbols.Count > 0)
+            {
+                foreach (var patchTransform in this.PatchTransforms)
+                {
+                    if (!this.ReduceTransform(patchTransform.Transform, patchRefSymbols))
+                    {
+                        // transform has none of the content authored into this patch
+                        continue;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Reduce the transform according to the patch references.
+        /// </summary>
+        /// <param name="transform">transform generated by torch.</param>
+        /// <param name="patchRefSymbols">Table contains patch family filter.</param>
+        /// <returns>true if the transform is not empty</returns>
+        private bool ReduceTransform(WindowsInstallerData transform, IEnumerable<WixPatchRefSymbol> patchRefSymbols)
+        {
+            // identify sections to keep
+            var oldSections = new Dictionary<string, Row>();
+            var newSections = new Dictionary<string, Row>();
+            var tableKeyRows = new Dictionary<string, Dictionary<string, Row>>();
+            var sequenceList = new List<Table>();
+            var componentFeatureAddsIndex = new Dictionary<string, List<string>>();
+            var customActionTable = new Dictionary<string, Row>();
+            var directoryTableAdds = new Dictionary<string, Row>();
+            var featureTableAdds = new Dictionary<string, Row>();
+            var keptComponents = new Dictionary<string, Row>();
+            var keptDirectories = new Dictionary<string, Row>();
+            var keptFeatures = new Dictionary<string, Row>();
+            var keptLockPermissions = new HashSet<string>();
+            var keptMsiLockPermissionExs = new HashSet<string>();
+
+            var componentCreateFolderIndex = new Dictionary<string, List<string>>();
+            var directoryLockPermissionsIndex = new Dictionary<string, List<Row>>();
+            var directoryMsiLockPermissionsExIndex = new Dictionary<string, List<Row>>();
+
+            foreach (var patchRefSymbol in patchRefSymbols)
+            {
+                var tableName = patchRefSymbol.Table;
+                var key = patchRefSymbol.PrimaryKeys;
+
+                // Short circuit filtering if all changes should be included.
+                if ("*" == tableName && "*" == key)
+                {
+                    RemoveProductCodeFromTransform(transform);
+                    return true;
+                }
+
+                if (!transform.Tables.TryGetTable(tableName, out var table))
+                {
+                    // Table not found.
+                    continue;
+                }
+
+                // Index the table.
+                if (!tableKeyRows.TryGetValue(tableName, out var keyRows))
+                {
+                    keyRows = table.Rows.ToDictionary(r => r.GetPrimaryKey());
+                    tableKeyRows.Add(tableName, keyRows);
+                }
+
+                if (!keyRows.TryGetValue(key, out var row))
+                {
+                    // Row not found.
+                    continue;
+                }
+
+                // Differ.sectionDelimiter
+                var sections = row.SectionId.Split(SectionDelimiter);
+                oldSections[sections[0]] = row;
+                newSections[sections[1]] = row;
+            }
+
+            // throw away sections not referenced
+            var keptRows = 0;
+            Table directoryTable = null;
+            Table featureTable = null;
+            Table lockPermissionsTable = null;
+            Table msiLockPermissionsTable = null;
+
+            foreach (var table in transform.Tables)
+            {
+                if ("_SummaryInformation" == table.Name)
+                {
+                    continue;
+                }
+
+                if (table.Name == "AdminExecuteSequence"
+                    || table.Name == "AdminUISequence"
+                    || table.Name == "AdvtExecuteSequence"
+                    || table.Name == "InstallUISequence"
+                    || table.Name == "InstallExecuteSequence")
+                {
+                    sequenceList.Add(table);
+                    continue;
+                }
+
+                for (var i = 0; i < table.Rows.Count; i++)
+                {
+                    var row = table.Rows[i];
+
+                    if (table.Name == "CreateFolder")
+                    {
+                        var createFolderComponentId = row.FieldAsString(1);
+
+                        if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out var directoryList))
+                        {
+                            directoryList = new List<string>();
+                            componentCreateFolderIndex.Add(createFolderComponentId, directoryList);
+                        }
+
+                        directoryList.Add(row.FieldAsString(0));
+                    }
+
+                    if (table.Name == "CustomAction")
+                    {
+                        customActionTable.Add(row.FieldAsString(0), row);
+                    }
+
+                    if (table.Name == "Directory")
+                    {
+                        directoryTable = table;
+                        if (RowOperation.Add == row.Operation)
+                        {
+                            directoryTableAdds.Add(row.FieldAsString(0), row);
+                        }
+                    }
+
+                    if (table.Name == "Feature")
+                    {
+                        featureTable = table;
+                        if (RowOperation.Add == row.Operation)
+                        {
+                            featureTableAdds.Add(row.FieldAsString(0), row);
+                        }
+                    }
+
+                    if (table.Name == "FeatureComponents")
+                    {
+                        if (RowOperation.Add == row.Operation)
+                        {
+                            var featureId = row.FieldAsString(0);
+                            var componentId = row.FieldAsString(1);
+
+                            if (!componentFeatureAddsIndex.TryGetValue(componentId, out var featureList))
+                            {
+                                featureList = new List<string>();
+                                componentFeatureAddsIndex.Add(componentId, featureList);
+                            }
+
+                            featureList.Add(featureId);
+                        }
+                    }
+
+                    if (table.Name == "LockPermissions")
+                    {
+                        lockPermissionsTable = table;
+                        if ("CreateFolder" == row.FieldAsString(1))
+                        {
+                            var directoryId = row.FieldAsString(0);
+
+                            if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out var rowList))
+                            {
+                                rowList = new List<Row>();
+                                directoryLockPermissionsIndex.Add(directoryId, rowList);
+                            }
+
+                            rowList.Add(row);
+                        }
+                    }
+
+                    if (table.Name == "MsiLockPermissionsEx")
+                    {
+                        msiLockPermissionsTable = table;
+                        if ("CreateFolder" == row.FieldAsString(1))
+                        {
+                            var directoryId = row.FieldAsString(0);
+
+                            if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var rowList))
+                            {
+                                rowList = new List<Row>();
+                                directoryMsiLockPermissionsExIndex.Add(directoryId, rowList);
+                            }
+
+                            rowList.Add(row);
+                        }
+                    }
+
+                    if (null == row.SectionId)
+                    {
+                        table.Rows.RemoveAt(i);
+                        i--;
+                    }
+                    else
+                    {
+                        var sections = row.SectionId.Split(SectionDelimiter);
+                        // ignore the row without section id.
+                        if (0 == sections[0].Length && 0 == sections[1].Length)
+                        {
+                            table.Rows.RemoveAt(i);
+                            i--;
+                        }
+                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
+                        {
+                            if ("Component" == table.Name)
+                            {
+                                keptComponents.Add(row.FieldAsString(0), row);
+                            }
+
+                            if ("Directory" == table.Name)
+                            {
+                                keptDirectories.Add(row.FieldAsString(0), row);
+                            }
+
+                            if ("Feature" == table.Name)
+                            {
+                                keptFeatures.Add(row.FieldAsString(0), row);
+                            }
+
+                            keptRows++;
+                        }
+                        else
+                        {
+                            table.Rows.RemoveAt(i);
+                            i--;
+                        }
+                    }
+                }
+            }
+
+            keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
+
+            if (null != directoryTable)
+            {
+                foreach (var componentRow in keptComponents.Values)
+                {
+                    var componentId = componentRow.FieldAsString(0);
+
+                    if (RowOperation.Add == componentRow.Operation)
+                    {
+                        // Make sure each added component has its required directory and feature heirarchy.
+                        var directoryId = componentRow.FieldAsString(2);
+                        while (null != directoryId && directoryTableAdds.TryGetValue(directoryId, out var directoryRow))
+                        {
+                            if (!keptDirectories.ContainsKey(directoryId))
+                            {
+                                directoryTable.Rows.Add(directoryRow);
+                                keptDirectories.Add(directoryId, directoryRow);
+                                keptRows++;
+                            }
+
+                            directoryId = directoryRow.FieldAsString(1);
+                        }
+
+                        if (componentFeatureAddsIndex.TryGetValue(componentId, out var componentFeatureIds))
+                        {
+                            foreach (var featureId in componentFeatureIds)
+                            {
+                                var currentFeatureId = featureId;
+                                while (null != currentFeatureId && featureTableAdds.TryGetValue(currentFeatureId, out var featureRow))
+                                {
+                                    if (!keptFeatures.ContainsKey(currentFeatureId))
+                                    {
+                                        featureTable.Rows.Add(featureRow);
+                                        keptFeatures.Add(currentFeatureId, featureRow);
+                                        keptRows++;
+                                    }
+
+                                    currentFeatureId = featureRow.FieldAsString(1);
+                                }
+                            }
+                        }
+                    }
+
+                    // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept.
+                    foreach (var keptComponentId in keptComponents.Keys)
+                    {
+                        if (componentCreateFolderIndex.TryGetValue(keptComponentId, out var directoryList))
+                        {
+                            foreach (var directoryId in directoryList)
+                            {
+                                if (directoryLockPermissionsIndex.TryGetValue(directoryId, out var lockPermissionsRowList))
+                                {
+                                    foreach (var lockPermissionsRow in lockPermissionsRowList)
+                                    {
+                                        var key = lockPermissionsRow.GetPrimaryKey('/');
+                                        if (keptLockPermissions.Add(key))
+                                        {
+                                            lockPermissionsTable.Rows.Add(lockPermissionsRow);
+                                            keptRows++;
+                                        }
+                                    }
+                                }
+
+                                if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var msiLockPermissionsExRowList))
+                                {
+                                    foreach (var msiLockPermissionsExRow in msiLockPermissionsExRowList)
+                                    {
+                                        var key = msiLockPermissionsExRow.GetPrimaryKey('/');
+                                        if (keptMsiLockPermissionExs.Add(key))
+                                        {
+                                            msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow);
+                                            keptRows++;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable);
+
+            // Delete tables that are empty.
+            var tablesToDelete = transform.Tables.Where(t => t.Rows.Count == 0).Select(t => t.Name).ToList();
+
+            foreach (var tableName in tablesToDelete)
+            {
+                transform.Tables.Remove(tableName);
+            }
+
+            return keptRows > 0;
+        }
+
+        /// <summary>
+        /// Check if the section is in a PatchFamily.
+        /// </summary>
+        /// <param name="oldSection">Section id in target wixout</param>
+        /// <param name="newSection">Section id in upgrade wixout</param>
+        /// <param name="oldSections">Dictionary contains section id should be kept in the baseline wixout.</param>
+        /// <param name="newSections">Dictionary contains section id should be kept in the upgrade wixout.</param>
+        /// <returns>true if section in patch family</returns>
+        private static bool IsInPatchFamily(string oldSection, string newSection, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections)
+        {
+            var result = false;
+
+            if ((String.IsNullOrEmpty(oldSection) && newSections.ContainsKey(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.ContainsKey(oldSection)))
+            {
+                result = true;
+            }
+            else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.ContainsKey(oldSection) || newSections.ContainsKey(newSection)))
+            {
+                result = true;
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Remove the ProductCode property from the transform.
+        /// </summary>
+        /// <param name="transform">The transform.</param>
+        /// <remarks>
+        /// Changing the ProductCode is not supported in a patch.
+        /// </remarks>
+        private static void RemoveProductCodeFromTransform(WindowsInstallerData transform)
+        {
+            if (transform.Tables.TryGetTable("Property", out var propertyTable))
+            {
+                for (var i = 0; i < propertyTable.Rows.Count; ++i)
+                {
+                    var propertyRow = propertyTable.Rows[i];
+                    var property = propertyRow.FieldAsString(0);
+
+                    if ("ProductCode" == property)
+                    {
+                        propertyTable.Rows.RemoveAt(i);
+                        break;
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Reduce the transform sequence tables.
+        /// </summary>
+        /// <param name="sequenceList">ArrayList of tables to be reduced</param>
+        /// <param name="oldSections">Hashtable contains section id should be kept in the baseline wixout.</param>
+        /// <param name="newSections">Hashtable contains section id should be kept in the target wixout.</param>
+        /// <param name="customAction">Hashtable contains all the rows in the CustomAction table.</param>
+        /// <returns>Number of rows left</returns>
+        private static int ReduceTransformSequenceTable(List<Table> sequenceList, Dictionary<string, Row> oldSections, Dictionary<string, Row> newSections, Dictionary<string, Row> customAction)
+        {
+            var keptRows = 0;
+
+            foreach (var currentTable in sequenceList)
+            {
+                for (var i = 0; i < currentTable.Rows.Count; i++)
+                {
+                    var row = currentTable.Rows[i];
+                    var actionName = row.Fields[0].Data.ToString();
+                    var sections = row.SectionId.Split(SectionDelimiter);
+                    var isSectionIdEmpty = (sections[0].Length == 0 && sections[1].Length == 0);
+
+                    if (row.Operation == RowOperation.None)
+                    {
+                        // Ignore the rows without section id.
+                        if (isSectionIdEmpty)
+                        {
+                            currentTable.Rows.RemoveAt(i);
+                            i--;
+                        }
+                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
+                        {
+                            keptRows++;
+                        }
+                        else
+                        {
+                            currentTable.Rows.RemoveAt(i);
+                            i--;
+                        }
+                    }
+                    else if (row.Operation == RowOperation.Modify)
+                    {
+                        var sequenceChanged = row.Fields[2].Modified;
+                        var conditionChanged = row.Fields[1].Modified;
+
+                        if (sequenceChanged && !conditionChanged)
+                        {
+                            keptRows++;
+                        }
+                        else if (!sequenceChanged && conditionChanged)
+                        {
+                            if (isSectionIdEmpty)
+                            {
+                                currentTable.Rows.RemoveAt(i);
+                                i--;
+                            }
+                            else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
+                            {
+                                keptRows++;
+                            }
+                            else
+                            {
+                                currentTable.Rows.RemoveAt(i);
+                                i--;
+                            }
+                        }
+                        else if (sequenceChanged && conditionChanged)
+                        {
+                            if (isSectionIdEmpty)
+                            {
+                                row.Fields[1].Modified = false;
+                                keptRows++;
+                            }
+                            else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
+                            {
+                                keptRows++;
+                            }
+                            else
+                            {
+                                row.Fields[1].Modified = false;
+                                keptRows++;
+                            }
+                        }
+                    }
+                    else if (row.Operation == RowOperation.Delete)
+                    {
+                        if (isSectionIdEmpty)
+                        {
+                            // it is a stardard action which is added by wix, we should keep this action.
+                            row.Operation = RowOperation.None;
+                            keptRows++;
+                        }
+                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
+                        {
+                            keptRows++;
+                        }
+                        else
+                        {
+                            if (customAction.ContainsKey(actionName))
+                            {
+                                currentTable.Rows.RemoveAt(i);
+                                i--;
+                            }
+                            else
+                            {
+                                // it is a stardard action, we should keep this action.
+                                row.Operation = RowOperation.None;
+                                keptRows++;
+                            }
+                        }
+                    }
+                    else if (row.Operation == RowOperation.Add)
+                    {
+                        if (isSectionIdEmpty)
+                        {
+                            keptRows++;
+                        }
+                        else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections))
+                        {
+                            keptRows++;
+                        }
+                        else
+                        {
+                            if (customAction.ContainsKey(actionName))
+                            {
+                                currentTable.Rows.RemoveAt(i);
+                                i--;
+                            }
+                            else
+                            {
+                                keptRows++;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return keptRows;
+        }
+    }
+}
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs b/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
index bccdd3d4..d1d7c19b 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs
@@ -39,10 +39,16 @@ namespace WixToolset.Core.WindowsInstaller
                 patchTransforms = command.Execute();
             }
 
+            // Reduce transforms.
+            {
+                var command = new ReduceTransformCommand(context.IntermediateRepresentation, patchTransforms);
+                command.Execute();
+            }
+
             // Enhance the intermediate by attaching the created patch transforms.
             IEnumerable<SubStorage> subStorages;
             {
-                var command = new AttachPatchTransformsCommand(messaging, backendHelper, context.IntermediateRepresentation, patchTransforms);
+                var command = new CreatePatchSubStoragesCommand(messaging, backendHelper, context.IntermediateRepresentation, patchTransforms);
                 subStorages = command.Execute();
             }
 
diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
index 7bbbbd76..5eeb67c8 100644
--- a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
+++ b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
@@ -21,8 +21,6 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
     {
         private static readonly Regex Modularization = new Regex(@"\.[0-9A-Fa-f]{8}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{12}");
 
-        private int sectionCount;
-
         public UnbindDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IPathResolver pathResolver, string databasePath, OutputType outputType, string exportBasePath, string extractFilesFolder, string intermediateFolder, bool enableDemodularization, bool skipSummaryInfo)
         {
             this.Messaging = messaging;
@@ -65,6 +63,8 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
 
         public bool AdminImage { get; private set; }
 
+        public WindowsInstallerData Data { get; private set; }
+
         public IEnumerable<string> ExportedFiles { get; private set; }
 
         public WindowsInstallerData Execute()
@@ -72,7 +72,7 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
             var adminImage = false;
             var exportedFiles = new List<string>();
 
-            var output = new WindowsInstallerData(new SourceLineNumber(this.DatabasePath))
+            var data = new WindowsInstallerData(new SourceLineNumber(this.DatabasePath))
             {
                 Type = this.OutputType
             };
@@ -85,15 +85,13 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
 
                     Directory.CreateDirectory(this.IntermediateFolder);
 
-                    output.Codepage = this.GetCodePage();
+                    data.Codepage = this.GetCodePage();
 
-                    var modularizationGuid = this.ProcessTables(output, exportedFiles);
+                    var modularizationGuid = this.ProcessTables(data, exportedFiles);
 
-                    var summaryInfo = this.ProcessSummaryInfo(output, modularizationGuid);
+                    var summaryInfo = this.ProcessSummaryInfo(data, modularizationGuid);
 
-                    this.UpdateUnrealFileColumns(this.DatabasePath, output, summaryInfo, exportedFiles);
-
-                    this.GenerateSectionIds(output);
+                    this.UpdateUnrealFileColumns(this.DatabasePath, data, summaryInfo, exportedFiles);
                 }
             }
             catch (Win32Exception e)
@@ -107,9 +105,10 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
             }
 
             this.AdminImage = adminImage;
+            this.Data = data;
             this.ExportedFiles = exportedFiles;
 
-            return output;
+            return data;
         }
 
         private int GetCodePage()
@@ -657,207 +656,6 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
             }
         }
 
-        /// <summary>
-        /// Creates section ids on rows which form logical groupings of resources.
-        /// </summary>
-        /// <param name="output">The Output that represents the msi database.</param>
-        private void GenerateSectionIds(WindowsInstallerData output)
-        {
-            // First assign and index section ids for the tables that are in their own sections.
-            this.AssignSectionIdsToTable(output.Tables["Binary"], 0);
-            var componentSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Component"], 0);
-            var customActionSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["CustomAction"], 0);
-            this.AssignSectionIdsToTable(output.Tables["Directory"], 0);
-            var featureSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Feature"], 0);
-            this.AssignSectionIdsToTable(output.Tables["Icon"], 0);
-            var digitalCertificateSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0);
-            this.AssignSectionIdsToTable(output.Tables["Property"], 0);
-
-            // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here.
-            var fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0);
-            var appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5);
-            var odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0);
-            var odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0);
-            var registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0);
-            var serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0);
-
-            // Now handle all the tables which only rely on previous indexes and order does not matter.
-            foreach (var table in output.Tables)
-            {
-                switch (table.Name)
-                {
-                    case "MsiFileHash":
-                        ConnectTableToSection(table, fileSectionIdIndex, 0);
-                        break;
-                    case "MsiAssembly":
-                    case "MsiAssemblyName":
-                        ConnectTableToSection(table, componentSectionIdIndex, 0);
-                        break;
-                    case "MsiPackageCertificate":
-                    case "MsiPatchCertificate":
-                        ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1);
-                        break;
-                    case "CreateFolder":
-                    case "FeatureComponents":
-                    case "MoveFile":
-                    case "ReserveCost":
-                    case "ODBCTranslator":
-                        ConnectTableToSection(table, componentSectionIdIndex, 1);
-                        break;
-                    case "TypeLib":
-                        ConnectTableToSection(table, componentSectionIdIndex, 2);
-                        break;
-                    case "Shortcut":
-                    case "Environment":
-                        ConnectTableToSection(table, componentSectionIdIndex, 3);
-                        break;
-                    case "RemoveRegistry":
-                        ConnectTableToSection(table, componentSectionIdIndex, 4);
-                        break;
-                    case "ServiceControl":
-                        ConnectTableToSection(table, componentSectionIdIndex, 5);
-                        break;
-                    case "IniFile":
-                    case "RemoveIniFile":
-                        ConnectTableToSection(table, componentSectionIdIndex, 7);
-                        break;
-                    case "AppId":
-                        ConnectTableToSection(table, appIdSectionIdIndex, 0);
-                        break;
-                    case "Condition":
-                        ConnectTableToSection(table, featureSectionIdIndex, 0);
-                        break;
-                    case "ODBCSourceAttribute":
-                        ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0);
-                        break;
-                    case "ODBCAttribute":
-                        ConnectTableToSection(table, odbcDriverSectionIdIndex, 0);
-                        break;
-                    case "AdminExecuteSequence":
-                    case "AdminUISequence":
-                    case "AdvtExecuteSequence":
-                    case "AdvtUISequence":
-                    case "InstallExecuteSequence":
-                    case "InstallUISequence":
-                        ConnectTableToSection(table, customActionSectionIdIndex, 0);
-                        break;
-                    case "LockPermissions":
-                    case "MsiLockPermissions":
-                        foreach (var row in table.Rows)
-                        {
-                            var lockObject = (string)row[0];
-                            var tableName = (string)row[1];
-                            switch (tableName)
-                            {
-                                case "File":
-                                    row.SectionId = (string)fileSectionIdIndex[lockObject];
-                                    break;
-                                case "Registry":
-                                    row.SectionId = (string)registrySectionIdIndex[lockObject];
-                                    break;
-                                case "ServiceInstall":
-                                    row.SectionId = (string)serviceInstallSectionIdIndex[lockObject];
-                                    break;
-                            }
-                        }
-                        break;
-                }
-            }
-
-            // Now pass the output to each unbinder extension to allow them to analyze the output and determine their proper section ids.
-            //foreach (IUnbinderExtension extension in this.unbinderExtensions)
-            //{
-            //    extension.GenerateSectionIds(output);
-            //}
-        }
-
-        /// <summary>
-        /// Creates new section ids on all the rows in a table.
-        /// </summary>
-        /// <param name="table">The table to add sections to.</param>
-        /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param>
-        /// <returns>A dictionary containing the tables key for each row paired with its assigned section id.</returns>
-        private Dictionary<string, string> AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex)
-        {
-            var primaryKeyToSectionId = new Dictionary<string, string>();
-
-            if (null != table)
-            {
-                foreach (var row in table.Rows)
-                {
-                    row.SectionId = this.GetNewSectionId();
-
-                    primaryKeyToSectionId.Add(row.FieldAsString(rowPrimaryKeyIndex), row.SectionId);
-                }
-            }
-
-            return primaryKeyToSectionId;
-        }
-
-        /// <summary>
-        /// Connects a table's rows to an already sectioned table.
-        /// </summary>
-        /// <param name="table">The table containing rows that need to be connected to sections.</param>
-        /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param>
-        /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param>
-        private static void ConnectTableToSection(Table table, Dictionary<string, string> sectionIdIndex, int rowIndex)
-        {
-            if (null != table)
-            {
-                foreach (var row in table.Rows)
-                {
-                    if (sectionIdIndex.TryGetValue(row.FieldAsString(rowIndex), out var sectionId))
-                    {
-                        row.SectionId = sectionId;
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it.
-        /// </summary>
-        /// <param name="table">The table containing rows that need to be connected to sections.</param>
-        /// <param name="sectionIdIndex">A dictionary containing keys to map table to its section.</param>
-        /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param>
-        /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param>
-        /// <returns>A dictionary containing the tables key for each row paired with its assigned section id.</returns>
-        private static Dictionary<string, string> ConnectTableToSectionAndIndex(Table table, Dictionary<string, string> sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex)
-        {
-            var newPrimaryKeyToSectionId = new Dictionary<string, string>();
-
-            if (null != table)
-            {
-                foreach (var row in table.Rows)
-                {
-                    var foreignKey = row.FieldAsString(rowIndex);
-
-                    if (!sectionIdIndex.TryGetValue(foreignKey, out var sectionId))
-                    {
-                        continue;
-                    }
-
-                    row.SectionId = sectionId;
-
-                    var primaryKey = row.FieldAsString(rowPrimaryKeyIndex);
-
-                    if (!String.IsNullOrEmpty(primaryKey) && sectionIdIndex.ContainsKey(primaryKey))
-                    {
-                        newPrimaryKeyToSectionId.Add(primaryKey, row.SectionId);
-                    }
-                }
-            }
-
-            return newPrimaryKeyToSectionId;
-        }
-
-        private string GetNewSectionId()
-        {
-            this.sectionCount++;
-
-            return "wix.section." + this.sectionCount.ToString(CultureInfo.InvariantCulture);
-        }
-
         private class SummaryInformationBits
         {
             public bool AdminImage { get; set; }
diff --git a/src/wix/WixToolset.Core/Compiler.cs b/src/wix/WixToolset.Core/Compiler.cs
index 8f1ae7fb..e100c5be 100644
--- a/src/wix/WixToolset.Core/Compiler.cs
+++ b/src/wix/WixToolset.Core/Compiler.cs
@@ -5793,13 +5793,13 @@ namespace WixToolset.Core
                         this.ParseCertificatesElement(child);
                         break;
                     case "PatchFamily":
-                        this.ParsePatchFamilyElement(child, ComplexReferenceParentType.Unknown, id.Id);
+                        this.ParsePatchFamilyElement(child, ComplexReferenceParentType.Unknown, id?.Id);
                         break;
                     case "PatchFamilyGroup":
-                        this.ParsePatchFamilyGroupElement(child, ComplexReferenceParentType.Unknown, id.Id);
+                        this.ParsePatchFamilyGroupElement(child, ComplexReferenceParentType.Unknown, id?.Id);
                         break;
                     case "PatchFamilyGroupRef":
-                        this.ParsePatchFamilyGroupRefElement(child, ComplexReferenceParentType.Unknown, id.Id);
+                        this.ParsePatchFamilyGroupRefElement(child, ComplexReferenceParentType.Unknown, id?.Id);
                         break;
                     case "PayloadGroup":
                         this.ParsePayloadGroupElement(child, ComplexReferenceParentType.Unknown, null);
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
index 945c346b..b0d49c05 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
@@ -394,6 +394,35 @@ namespace WixToolsetTest.CoreIntegration
             }
         }
 
+        [Fact]
+        public void CanBuildPatchWithFiltering()
+        {
+            var sourceFolder = TestData.Get(@"TestData", "PatchFamilyFilter");
+
+            using (var fs = new DisposableFileSystem())
+            {
+                var baseFolder = fs.GetFolder();
+                var tempFolderPatch = Path.Combine(baseFolder, "patch");
+
+                var patchPath = BuildMsp("Patch1.msp", sourceFolder, tempFolderPatch, "1.0.1", bindpaths: new[] { Path.GetDirectoryName(this.templateBaselinePdb), Path.GetDirectoryName(this.templateUpdatePdb) });
+
+                var doc = GetExtractPatchXml(patchPath);
+                WixAssert.StringEqual("{11111111-2222-3333-4444-555555555555}", doc.Root.Element(TargetProductCodeName).Value);
+
+                var names = Query.GetSubStorageNames(patchPath);
+                WixAssert.CompareLineByLine(new[] { "#RTM.1", "RTM.1" }, names);
+
+                var cab = Path.Combine(baseFolder, "foo.cab");
+                Query.ExtractStream(patchPath, "foo.cab", cab);
+
+                var files = Query.GetCabinetFiles(cab);
+                var file = files.Single();
+                WixAssert.StringEqual("a.txt", file.Name);
+                var contents = file.OpenText().ReadToEnd();
+                WixAssert.StringEqual("This is A v1.0.1 from the '.update-data' folder in 'PatchTemplatePackage'.\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.\r\n", contents);
+            }
+        }
+
         private static string BuildMsi(string outputName, string sourceFolder, string baseFolder, string defineV, string defineA, string defineB, IEnumerable<string> bindpaths = null)
         {
             var extensionPath = Path.GetFullPath(new Uri(typeof(ExampleExtensionFactory).Assembly.CodeBase).LocalPath);
diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs
index d39170c0..f48fd1ef 100644
--- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs
+++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs
@@ -1,8 +1,8 @@
-<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
   <Patch AllowRemoval="yes" Manufacturer="FireGiant" MoreInfoURL="http://www.example.com/" DisplayName="~Test Patch v$(var.V)" Description="~Test Small Update Patch v($var.V)" Classification="Update">
 
     <Media Id="1" Cabinet="foo.cab">
-      <PatchBaseline Id="RTM" />
+      <PatchBaseline Id="RTM" BaselineFile="Baseline.wixpdb" UpdateFile="Update.wixpdb" />
     </Media>
 
     <PatchFamilyRef Id="SamplePatchFamily" />
-- 
cgit v1.2.3-55-g6feb