From 93892f63e1e01470cc806ef73dc643d30630e981 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Thu, 9 Jul 2026 12:33:06 -0400 Subject: [PATCH 1/2] [#55] Show type name in build_report_packed_asset_contents_view Populate the types table from TypeIdRegistry when importing a BuildReport so the packed asset contents view can display human-readable type names instead of numeric class ids, even when the build output is not analyzed alongside the report. --- Analyzer/Resources/Init.sql | 14 +++++----- Analyzer/Resources/PackedAssets.sql | 5 +++- Analyzer/SQLite/Commands/AbstractCommand.cs | 8 +++++- .../SQLite/Commands/SerializedFile/AddType.cs | 6 +++++ .../SQLite/Handlers/PackedAssetsHandler.cs | 27 +++++++++++++++---- Documentation/buildreport.md | 15 +++++------ UnityDataTool.Tests/BuildReportTests.cs | 15 ++++++++--- UnityFileSystem/TypeIdRegistry.cs | 10 +++++++ 8 files changed, 76 insertions(+), 24 deletions(-) diff --git a/Analyzer/Resources/Init.sql b/Analyzer/Resources/Init.sql index 00c24d9..8e9480f 100644 --- a/Analyzer/Resources/Init.sql +++ b/Analyzer/Resources/Init.sql @@ -151,12 +151,14 @@ WHERE m.type = 'Material'; -- see SerializedFileSQLiteWriter for details INSERT INTO types (id, name) VALUES (-1, 'Scene'); --- Database schema version. Bump when the schema changes in a way that tools relying on it --- (e.g. find-refs) cannot read from an older database. 1 = normalized refs table (issue #44); --- 2 = renamed assets/asset_dependencies tables to assetbundle_assets/preload_dependencies --- (issue #82); 3 = renamed asset_bundles table to archives and the asset_bundle column/alias --- to archive (issue #68); databases produced before versioning report 0. -PRAGMA user_version = 3; +-- Database schema version. Bump on any schema change so the version records which schema a given +-- analyze.db was produced with. Commands that read an existing database (currently only find-refs) +-- can compare against it to give a clean error instead of failing on a missing table or column. +-- 1 = normalized refs table (issue #44); 2 = renamed assets/asset_dependencies tables to +-- assetbundle_assets/preload_dependencies (issue #82); 3 = renamed asset_bundles table to archives +-- and the asset_bundle column/alias to archive (issue #68); 4 = build_report_packed_asset_contents_view +-- type column changed from numeric id to type name (issue #55); databases produced before versioning report 0. +PRAGMA user_version = 4; PRAGMA synchronous = OFF; PRAGMA journal_mode = MEMORY; diff --git a/Analyzer/Resources/PackedAssets.sql b/Analyzer/Resources/PackedAssets.sql index b7f8acc..e717b0e 100644 --- a/Analyzer/Resources/PackedAssets.sql +++ b/Analyzer/Resources/PackedAssets.sql @@ -45,7 +45,9 @@ SELECT pa.path, pac.packed_assets_id, pac.object_id, - pac.type, + -- Show the type name when known (populated from TypeIdRegistry or TypeTree analysis), + -- otherwise fall back to the numeric class id as text. + COALESCE(t.name, CAST(pac.type AS TEXT)) as type, pac.size, pac.offset, sa.source_asset_guid, @@ -56,6 +58,7 @@ LEFT JOIN build_report_packed_assets pa ON pac.packed_assets_id = pa.id LEFT JOIN objects o ON o.id = pa.id INNER JOIN serialized_files sf ON o.serialized_file = sf.id LEFT JOIN build_report_source_assets sa ON pac.source_asset_id = sa.id +LEFT JOIN types t ON pac.type = t.id LEFT JOIN objects br_obj ON o.serialized_file = br_obj.serialized_file AND br_obj.type = 1125 LEFT JOIN build_report_archive_contents brac ON br_obj.id = brac.build_report_id AND pa.path = brac.archive_content; diff --git a/Analyzer/SQLite/Commands/AbstractCommand.cs b/Analyzer/SQLite/Commands/AbstractCommand.cs index 48011e5..9bee132 100644 --- a/Analyzer/SQLite/Commands/AbstractCommand.cs +++ b/Analyzer/SQLite/Commands/AbstractCommand.cs @@ -14,6 +14,11 @@ internal abstract class AbstractCommand protected virtual string DDLSource { get => null; } + // Conflict-resolution clause for the INSERT (e.g. "OR IGNORE"). Defaults to none, so a + // duplicate primary key surfaces as an error - some tables rely on that to detect problems + // such as the same SerializedFile being analyzed twice. + protected virtual string ConflictClause => ""; + // run data definition language commands to create // tables and views, run once at the beginning of creating // the database @@ -31,7 +36,8 @@ public void CreateCommand(SqliteConnection database) RunDDL(database); m_Command = database.CreateCommand(); - var commandText = new StringBuilder($"INSERT INTO {TableName} ("); + var insert = string.IsNullOrEmpty(ConflictClause) ? "INSERT" : $"INSERT {ConflictClause}"; + var commandText = new StringBuilder($"{insert} INTO {TableName} ("); commandText.Append(string.Join(", ", Fields.Keys)); commandText.Append(") VALUES (@"); commandText.Append(string.Join(", @", Fields.Keys)); diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddType.cs b/Analyzer/SQLite/Commands/SerializedFile/AddType.cs index 859b360..1e51ed5 100644 --- a/Analyzer/SQLite/Commands/SerializedFile/AddType.cs +++ b/Analyzer/SQLite/Commands/SerializedFile/AddType.cs @@ -18,6 +18,12 @@ internal class AddType : AbstractCommand protected override string DDLSource => null; + // The BuildReport PackedAssetsHandler may have already inserted a type by numeric id (with a + // name from TypeIdRegistry). The TypeTree name here is authoritative and identical for known + // ids, so ignore the conflict rather than crashing when both a report and its build output + // are analyzed together. + protected override string ConflictClause => "OR IGNORE"; + protected override Dictionary Fields => new() { { "id", SqliteType.Integer }, diff --git a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs index 78268b4..8ac8822 100644 --- a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs +++ b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Data.Sqlite; using UnityDataTools.Analyzer.SerializedObjects; +using UnityDataTools.FileSystem; using UnityDataTools.FileSystem.TypeTreeReaders; namespace UnityDataTools.Analyzer.SQLite.Handlers; @@ -12,6 +13,7 @@ public class PackedAssetsHandler : ISQLiteHandler private SqliteCommand m_InsertSourceAssetCommand; private SqliteCommand m_GetSourceAssetIdCommand; private SqliteCommand m_InsertContentsCommand; + private SqliteCommand m_InsertTypeCommand; private Dictionary<(string guid, string path), long> m_SourceAssetCache = new(); public void Init(SqliteConnection db) @@ -59,6 +61,15 @@ public void Init(SqliteConnection db) m_InsertContentsCommand.Parameters.Add("@size", SqliteType.Integer); m_InsertContentsCommand.Parameters.Add("@offset", SqliteType.Integer); m_InsertContentsCommand.Parameters.Add("@source_asset_id", SqliteType.Integer); + + // A BuildReport records object types by numeric id only. When we can map an id to a + // human-readable name via TypeIdRegistry, we add it to the shared types table so the + // build report views can show the name. INSERT OR IGNORE keeps any name already inserted + // by TypeTree analysis of the actual build output (which is authoritative). + m_InsertTypeCommand = db.CreateCommand(); + m_InsertTypeCommand.CommandText = "INSERT OR IGNORE INTO types(id, name) VALUES(@id, @name)"; + m_InsertTypeCommand.Parameters.Add("@id", SqliteType.Integer); + m_InsertTypeCommand.Parameters.Add("@name", SqliteType.Text); } public void Process(Context ctx, long objectId, RandomAccessReader reader, out string name, out long streamDataSize) @@ -93,14 +104,19 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s m_SourceAssetCache[cacheKey] = sourceAssetId; } + // Populate the types table so views can display the type name even when the build + // output (and its TypeTrees) is not analyzed alongside the report. + if (TypeIdRegistry.TryGetTypeName(content.Type, out var typeName)) + { + m_InsertTypeCommand.Transaction = ctx.Transaction; + m_InsertTypeCommand.Parameters["@id"].Value = content.Type; + m_InsertTypeCommand.Parameters["@name"].Value = typeName; + m_InsertTypeCommand.ExecuteNonQuery(); + } + m_InsertContentsCommand.Transaction = ctx.Transaction; m_InsertContentsCommand.Parameters["@packed_assets_id"].Value = objectId; m_InsertContentsCommand.Parameters["@object_id"].Value = content.ObjectID; - - // TODO: Ideally we would also populate the type table if the content.Type is - // not already in that table, and if we have a string value for it in TypeIdRegistry. That would - // make it possible to view object types as strings, for the most common types, when importing a BuildReport - // without the associated built content. m_InsertContentsCommand.Parameters["@type"].Value = content.Type; m_InsertContentsCommand.Parameters["@size"].Value = (long)content.Size; m_InsertContentsCommand.Parameters["@offset"].Value = (long)content.Offset; @@ -122,6 +138,7 @@ void IDisposable.Dispose() m_InsertSourceAssetCommand?.Dispose(); m_GetSourceAssetIdCommand?.Dispose(); m_InsertContentsCommand?.Dispose(); + m_InsertTypeCommand?.Dispose(); } } diff --git a/Documentation/buildreport.md b/Documentation/buildreport.md index 17f45f2..fd0f67d 100644 --- a/Documentation/buildreport.md +++ b/Documentation/buildreport.md @@ -258,21 +258,20 @@ For consistency and clarity, database columns use slightly different names than | `build_report_packed_assets.path` | `PackedAssets.ShortPath` | Filename of the SerializedFile, .resS, or .resource file ("short" was redundant since only one path is recorded) | | `build_report_packed_assets.file_header_size` | `PackedAssets.Overhead` | Size of the file header (zero for .resS and .resource files) | | `build_report_packed_asset_info.object_id` | `PackedAssetInfo.fileID` | Local file ID of the object (renamed for consistency with `objects.object_id`) | -| `build_report_packed_asset_info.type` | `PackedAssetInfo.classID` | Unity object type (renamed for consistency with `objects.type`) | +| `build_report_packed_asset_info.type` | `PackedAssetInfo.classID` | Unity object type as a numeric [Class ID](https://docs.unity3d.com/Manual/ClassIDReference.html) (renamed for consistency with `objects.type`). `build_report_packed_asset_contents_view` exposes this as the type name. | ## Limitations -**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. - -**Type names:** While `build_report_packed_asset_info.type` records valid [Class IDs](https://docs.unity3d.com/Manual/ClassIDReference.html), the string type name may not exist in the `types` table. The `types` table is only populated when processing object instances (during TypeTree analysis). Analyzing both build output **and** build report together ensures types are fully populated; otherwise only numeric IDs are available. +**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. The BuildReport files in the build history (introduced in Unity 6.6) intentionally have unique file names (incorporating the build session GUID), so analyze does not hit this issue when analyzing multiple entries from the BuildHistory folder. When analyzing multiple AssetBundle build reports, or build reports from older versions of unity, be sure to assign a unique filename to each. See also issue #36. ### Information Not Exported Currently, only the most useful BuildReport data is extracted to SQL. Additional data may be added as needed: -* [Code stripping](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-strippingInfo.html) appendix (IL2CPP Player builds) -* [ScenesUsingAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-scenesUsingAssets.html) (detailed build reports) -* `BuildReport.m_BuildSteps` array (SQL may not be ideal for this hierarchical data) -* `BuildAssetBundleInfoSet` appendix (undocumented object listing files in each AssetBundle; `build_report_archive_contents` currently derives this from the File list) +* [Code stripping](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-strippingInfo.html) appendix (available only for IL2CPP Player builds) +* [ScenesUsingAssets](https://docs.unity3d.com/ScriptReference/Build.Reporting.BuildReport-scenesUsingAssets.html) (available only in detailed build reports for Player builds) +* `BuildReport.m_BuildSteps` array. Supporting this is low priority - SQL is not an ideal representation for this hierarchical data. Starting in Unity 6.6 the build steps are also reported in the BuildLog.jsonl file and Trace Event Profile (TEP) files, which are easy to parse. +* `BuildAssetBundleInfoSet` appendix (undocumented object listing files in each AssetBundle; `build_report_archive_contents` currently derives this from the File list without relying on that data) * Analytics-only appendices (unlikely to be valuable for analysis) +Tip: All this additional BuildReport data can be viewed using the `dump` command. \ No newline at end of file diff --git a/UnityDataTool.Tests/BuildReportTests.cs b/UnityDataTool.Tests/BuildReportTests.cs index c9f7def..210955a 100644 --- a/UnityDataTool.Tests/BuildReportTests.cs +++ b/UnityDataTool.Tests/BuildReportTests.cs @@ -311,10 +311,10 @@ public async Task Analyze_BuildReport_AssetBundle_ContainsPackedAssetsData() // Verify the specific content row (data[3] from the dump) const long objectId = -1350043613627603771; var contentRow = SQLTestHelper.QueryInt(db, - $@"SELECT COUNT(*) FROM build_report_packed_asset_contents_view - WHERE packed_assets_id = {packedAssetId} + $@"SELECT COUNT(*) FROM build_report_packed_asset_contents_view + WHERE packed_assets_id = {packedAssetId} AND object_id = {objectId} - AND type = 28 + AND type = 'Texture2D' AND size = 204 AND offset = 11840 AND source_asset_guid = '8826f464101b93c4bb006e15a9aff317' @@ -323,6 +323,15 @@ public async Task Analyze_BuildReport_AssetBundle_ContainsPackedAssetsData() Assert.AreEqual(1, contentRow, "Expected exactly one packed_asset_contents row matching the specified criteria"); + // The type column shows the human-readable name (from TypeIdRegistry) even though the + // build report records only the numeric class id and no build output was analyzed here. + SQLTestHelper.AssertQueryString(db, + $@"SELECT type FROM build_report_packed_asset_contents_view + WHERE packed_assets_id = {packedAssetId} + AND object_id = {objectId}", + "Texture2D", + "Expected type name 'Texture2D' in build_report_packed_asset_contents_view"); + // Verify the view works correctly for this content row SQLTestHelper.AssertQueryString(db, $@"SELECT source_asset_guid FROM build_report_packed_asset_contents_view diff --git a/UnityFileSystem/TypeIdRegistry.cs b/UnityFileSystem/TypeIdRegistry.cs index f09a862..e8a5504 100644 --- a/UnityFileSystem/TypeIdRegistry.cs +++ b/UnityFileSystem/TypeIdRegistry.cs @@ -272,5 +272,15 @@ public static string GetTypeName(int typeId) ? name : typeId.ToString(); } + + /// + /// Looks up the type name for a TypeId, returning false when the id is not a known type. + /// Unlike GetTypeName, this does not fall back to the numeric id, so callers can tell + /// whether a real name is available. + /// + public static bool TryGetTypeName(int typeId, out string name) + { + return s_KnownTypes.TryGetValue(typeId, out name); + } } From 8ad05d2f944a9b8eceb080801622adeff0f91c74 Mon Sep 17 00:00:00 2001 From: Andrew Skowronski Date: Thu, 9 Jul 2026 12:54:57 -0400 Subject: [PATCH 2/2] [#55] Address review: dedup type inserts, clarify comments, doc fixes - PackedAssetsHandler: skip redundant type inserts via a HashSet (a report can list thousands of objects sharing a few types). - AddType: reword the OR IGNORE comment - first insert wins is fine since the name for a given id matches across TypeIdRegistry and TypeTree; the case that matters (id missing from the registry) still gets its name from TypeTree. - buildreport.md: capitalization and spacing. --- Analyzer/SQLite/Commands/SerializedFile/AddType.cs | 9 ++++++--- Analyzer/SQLite/Handlers/PackedAssetsHandler.cs | 8 +++++++- Documentation/buildreport.md | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Analyzer/SQLite/Commands/SerializedFile/AddType.cs b/Analyzer/SQLite/Commands/SerializedFile/AddType.cs index 1e51ed5..8f94c86 100644 --- a/Analyzer/SQLite/Commands/SerializedFile/AddType.cs +++ b/Analyzer/SQLite/Commands/SerializedFile/AddType.cs @@ -19,9 +19,12 @@ internal class AddType : AbstractCommand protected override string DDLSource => null; // The BuildReport PackedAssetsHandler may have already inserted a type by numeric id (with a - // name from TypeIdRegistry). The TypeTree name here is authoritative and identical for known - // ids, so ignore the conflict rather than crashing when both a report and its build output - // are analyzed together. + // name from TypeIdRegistry), so ignore the conflict rather than crashing when both a report + // and its build output are analyzed together. First insert wins: the name for a given id is + // the same whether it comes from TypeIdRegistry or the TypeTree, so it does not matter which + // path runs first (a mismatch would indicate a deeper problem). The case that matters is a + // type missing from TypeIdRegistry (e.g. an older UnityDataTool reading a newer file); the + // handler skips those, so the name still arrives here from TypeTree analysis. protected override string ConflictClause => "OR IGNORE"; protected override Dictionary Fields => new() diff --git a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs index 8ac8822..833cec4 100644 --- a/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs +++ b/Analyzer/SQLite/Handlers/PackedAssetsHandler.cs @@ -16,6 +16,11 @@ public class PackedAssetsHandler : ISQLiteHandler private SqliteCommand m_InsertTypeCommand; private Dictionary<(string guid, string path), long> m_SourceAssetCache = new(); + // Type ids we have already tried to add to the types table. A single BuildReport can list + // thousands of objects sharing a handful of types, so this skips the redundant INSERT calls. + // The handler instance lives for the whole analyze, so this dedups across all reports. + private HashSet m_InsertedTypes = new(); + public void Init(SqliteConnection db) { using var command = db.CreateCommand(); @@ -106,7 +111,8 @@ public void Process(Context ctx, long objectId, RandomAccessReader reader, out s // Populate the types table so views can display the type name even when the build // output (and its TypeTrees) is not analyzed alongside the report. - if (TypeIdRegistry.TryGetTypeName(content.Type, out var typeName)) + if (m_InsertedTypes.Add(content.Type) && + TypeIdRegistry.TryGetTypeName(content.Type, out var typeName)) { m_InsertTypeCommand.Transaction = ctx.Transaction; m_InsertTypeCommand.Parameters["@id"].Value = content.Type; diff --git a/Documentation/buildreport.md b/Documentation/buildreport.md index fd0f67d..5af322c 100644 --- a/Documentation/buildreport.md +++ b/Documentation/buildreport.md @@ -262,7 +262,7 @@ For consistency and clarity, database columns use slightly different names than ## Limitations -**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. The BuildReport files in the build history (introduced in Unity 6.6) intentionally have unique file names (incorporating the build session GUID), so analyze does not hit this issue when analyzing multiple entries from the BuildHistory folder. When analyzing multiple AssetBundle build reports, or build reports from older versions of unity, be sure to assign a unique filename to each. See also issue #36. +**Duplicate filenames:** Multiple build reports cannot be imported into the same database if they share the same filename. This is a general UnityDataTool limitation that assumes unique SerializedFile names. The BuildReport files in the build history (introduced in Unity 6.6) intentionally have unique file names (incorporating the build session GUID), so analyze does not hit this issue when analyzing multiple entries from the BuildHistory folder. When analyzing multiple AssetBundle build reports, or build reports from older versions of Unity, be sure to assign a unique filename to each. See also issue #36. ### Information Not Exported