Skip to content

Commit 7c7bdf0

Browse files
[#72] Fix find-refs database open and null-ref crashes, add test coverage
find-refs had no automated tests and two bugs that broke it against any current analyze database: - Issue #72: the database was opened with a hand-written connection string using the legacy System.Data.SQLite "Version=3" keyword, which Microsoft.Data.Sqlite rejects. Build the connection string with SqliteConnectionStringBuilder instead (matching SQLiteWriter). Fixed the same broken string in ExpectedDataGenerator. - A ScriptableObject is a MonoBehaviour whose m_GameObject PPtr is 0, so its game_object column is a non-null id matching no row; the game_object/script subqueries then return NULL and GetString threw. Null-check those reads. Also add a --stdout option to find-refs (mutually exclusive with -o), mirroring dump, and update command-find-refs.md. Add FindRefsTests covering both bugs, name/type lookup and disambiguation, object-id lookup, --find-all, missing objects, empty refs table, the --stdout/-o validator, and direct refs-table queries. Tests run against the LeadingEdge AssetBundle reference build.
1 parent 4d0409b commit 7c7bdf0

5 files changed

Lines changed: 389 additions & 37 deletions

File tree

Documentation/command-find-refs.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# find-refs Command
22

3-
> ⚠️ **Experimental:** This command may not work as expected in all cases.
4-
53
The `find-refs` command traces reference chains leading to specific objects. Use it to understand why an asset was included (and potentially duplicated) in a build.
64

5+
It walks *up* the reference graph from the target object and **stops at the first asset it reaches**. The reported chains therefore end at the immediate containing asset, not at the ultimate root that transitively depends on the target. For example, if `RootAsset` references `LeafAsset` which references a texture, searching for the texture reports the chain ending at `LeafAsset`; to see that `RootAsset` pulls it in, search for `LeafAsset` instead.
6+
77
## Quick Reference
88

99
```
@@ -16,10 +16,13 @@ UnityDataTool find-refs <database> [options]
1616
| `-i, --object-id <id>` | ID of object to analyze (from `id` column) ||
1717
| `-n, --object-name <name>` | Name of objects to analyze ||
1818
| `-t, --object-type <type>` | Type filter when using `-n` ||
19-
| `-o, --output-file <file>` | Output filename ||
19+
| `-o, --output-file <file>` | Output filename | `references.txt` |
20+
| `--stdout` | Write the reference chains to stdout instead of a file | `false` |
2021
| `-a, --find-all` | Find all chains instead of stopping at first | `false` |
2122

2223
> **Note:** Either `--object-id` or `--object-name` must be provided.
24+
>
25+
> `--stdout` and `-o/--output-file` are mutually exclusive.
2326
2427
## Prerequisites
2528

@@ -29,14 +32,19 @@ This command requires a database created by the [`analyze`](command-analyze.md)
2932

3033
## Examples
3134

32-
Find references to an object by name and type:
35+
Find references to an object by name and type, printing directly to the console:
36+
```bash
37+
UnityDataTool find-refs my_database.db -n "MyTexture" -t "Texture2D" --stdout
38+
```
39+
40+
Write the reference chains to a file instead:
3341
```bash
3442
UnityDataTool find-refs my_database.db -n "MyTexture" -t "Texture2D" -o refs.txt
3543
```
3644

3745
Find references to a specific object by ID:
3846
```bash
39-
UnityDataTool find-refs my_database.db -i 12345 -o refs.txt
47+
UnityDataTool find-refs my_database.db -i 12345 --stdout
4048
```
4149

4250
Find all duplicate references (useful for finding why an asset is duplicated):

ReferenceFinder/ReferenceFinderTool.cs

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,14 @@ public class ReferenceFinderTool
2424
List<ReferenceTreeNode> m_Roots = new List<ReferenceTreeNode>();
2525
HashSet<(long, string)> m_ProcessedObjects = new HashSet<(long, string)>();
2626

27-
StreamWriter m_Writer;
27+
TextWriter m_Writer;
2828

29-
public int FindReferences(string objectName, string objectType, string databasePath, string outputFile, bool findAll)
29+
public int FindReferences(string objectName, string objectType, string databasePath, string outputFile, bool findAll, bool toStdout = false)
3030
{
3131
var objectIds = new List<long>();
32-
SqliteConnection db;
33-
34-
try
35-
{
36-
db = new SqliteConnection($"Data Source={databasePath};Version=3;Foreign Keys=False;");
37-
db.Open();
38-
}
39-
catch (Exception e)
32+
var db = OpenDatabase(databasePath);
33+
if (db == null)
4034
{
41-
Console.WriteLine($"Error opening database: {e.Message}");
4235
return 1;
4336
}
4437

@@ -81,33 +74,50 @@ public int FindReferences(string objectName, string objectType, string databaseP
8174
return 1;
8275
}
8376

84-
return FindReferences(db, outputFile, objectIds, findAll);
77+
return FindReferences(db, outputFile, objectIds, findAll, toStdout);
8578
}
8679

87-
public int FindReferences(long objectId, string databasePath, string outputFile, bool findAll)
80+
public int FindReferences(long objectId, string databasePath, string outputFile, bool findAll, bool toStdout = false)
8881
{
8982
var objectIds = new List<long>();
90-
SqliteConnection db;
83+
var db = OpenDatabase(databasePath);
84+
if (db == null)
85+
{
86+
return 1;
87+
}
9188

89+
objectIds.Add(objectId);
90+
91+
return FindReferences(db, outputFile, objectIds, findAll, toStdout);
92+
}
93+
94+
// Opens the analyze database for reading. Uses SqliteConnectionStringBuilder (matching SQLiteWriter) rather than a
95+
// hand-written connection string, which used a legacy System.Data.SQLite keyword that Microsoft.Data.Sqlite rejects.
96+
static SqliteConnection OpenDatabase(string databasePath)
97+
{
9298
try
9399
{
94-
db = new SqliteConnection($"Data Source={databasePath};Version=3;Foreign Keys=False;");
100+
var connectionString = new SqliteConnectionStringBuilder
101+
{
102+
DataSource = databasePath,
103+
Mode = SqliteOpenMode.ReadWriteCreate,
104+
Pooling = false,
105+
ForeignKeys = false,
106+
}.ConnectionString;
107+
var db = new SqliteConnection(connectionString);
95108
db.Open();
109+
return db;
96110
}
97111
catch (Exception e)
98112
{
99113
Console.WriteLine($"Error opening database: {e.Message}");
100-
return 1;
114+
return null;
101115
}
102-
103-
objectIds.Add(objectId);
104-
105-
return FindReferences(db, outputFile, objectIds, findAll);
106116
}
107117

108-
int FindReferences(SqliteConnection db, string outputFile, IList<long> objectIds, bool findAll)
118+
int FindReferences(SqliteConnection db, string outputFile, IList<long> objectIds, bool findAll, bool toStdout)
109119
{
110-
m_Writer = new StreamWriter(outputFile);
120+
m_Writer = toStdout ? Console.Out : new StreamWriter(outputFile);
111121

112122
m_GetRefsCommand = db.CreateCommand();
113123
m_GetRefsCommand.CommandText = @"SELECT object, property_path, EXISTS (SELECT * FROM assets a WHERE a.object = r.object) FROM refs r WHERE referenced_object = @id";
@@ -181,7 +191,11 @@ FROM object_view o
181191
}
182192
}
183193

184-
m_Writer.Close();
194+
// Don't close Console.Out when writing to stdout; just flush it.
195+
if (toStdout)
196+
m_Writer.Flush();
197+
else
198+
m_Writer.Close();
185199

186200
return 0;
187201
}
@@ -196,10 +210,13 @@ void OutputReferenceNode(ReferenceTreeNode node, string propertyPath, int indent
196210
{
197211
reader.Read();
198212

213+
// game_object and script come from correlated subqueries that yield NULL when there is no matching row
214+
// (e.g. a ScriptableObject is a MonoBehaviour whose m_GameObject PPtr is 0, or a MonoBehaviour with no
215+
// m_Script reference), so both must be null-checked.
199216
var objectType = reader.GetString(0);
200217
var objectName = reader.GetString(1);
201-
var gameObject = reader.GetString(2);
202-
var script = reader.GetString(3);
218+
var gameObject = reader.IsDBNull(2) ? "" : reader.GetString(2);
219+
var script = reader.IsDBNull(3) ? "" : reader.GetString(3);
203220

204221
if (propertyPath != "")
205222
{

UnityDataTool.Tests/ExpectedDataGenerator.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ public static void Generate(Context context)
3030

3131
Program.Main(new string[] { "analyze", Path.Combine(context.UnityDataFolder), "-r" });
3232

33-
using var db = new SqliteConnection($"Data Source={Path.Combine(Directory.GetCurrentDirectory(), "database.db")};Version=3;New=True;Foreign Keys=False;");
34-
db.Open();
33+
using var db = SQLTestHelper.OpenDatabase(Path.Combine(Directory.GetCurrentDirectory(), "database.db"));
3534

3635
using (var cmd = db.CreateCommand())
3736
{

0 commit comments

Comments
 (0)