Skip to content

Add SQLite database preview support to Peek#45846

Open
thomastv wants to merge 15 commits into
microsoft:mainfrom
thomastv:feature/sqlite-db-previewer
Open

Add SQLite database preview support to Peek#45846
thomastv wants to merge 15 commits into
microsoft:mainfrom
thomastv:feature/sqlite-db-previewer

Conversation

@thomastv

Copy link
Copy Markdown

Summary of the Pull Request

This PR adds SQLite database file preview support to the Peek utility, resolving issue #45757 . Three new model classes (SQLiteTableInfo, SQLiteColumnInfo, ISQLitePreviewer) and a core SQLitePreviewer class were added to read .db, .sqlite, and .sqlite3 files read-only using Microsoft.Data.Sqlite, fetching table schema via PRAGMA table_info and up to 200 rows per table. A new SQLiteControl XAML control presents the data in a two-pane layout — a tree view listing all tables on the left and a data grid showing columns and rows on the right, with intelligent column sizing that stretches the last column when space allows and falls back to horizontal scrolling for wide tables. The previewer was wired into the existing previewerFactory and FilePreview host, and three localizable strings were added to Resources.resw.

PR Checklist

  • Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
  • Tests: Added/updated and all pass
  • Localization: All end-user-facing strings can be localized
  • Dev docs: Added/updated
  • New binaries: Added on the required places
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

  • Added the new SQLiteControl user control (SQLiteControl.xaml and SQLiteControl.xaml.cs) for visualizing SQLite database tables, columns, and data in a split-pane UI with a DataGrid and tree view.
  • Implemented supporting model classes: SQLiteTableInfo for table metadata and rows, and SQLiteColumnInfo for column details and display formatting.
  • Defined the ISQLitePreviewer interface for previewer implementations that expose SQLite table data and metadata.
  • Added the SQLitePreviewer to the previewer factory logic, enabling automatic detection and previewing of SQLite files.

Validation Steps Performed

  1. Verified that the new feature works locally
  2. Verified that the existing features are also working correctly after adding new changes
  3. Created a dummy .db file and verified that the default fallback screen appears, since it isn't a valid sqlite file
image

Comment thread src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs Fixed
Comment thread src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs Fixed
@github-actions

This comment has been minimized.

@thomastv

Copy link
Copy Markdown
Author

@jiripolasek,
Hi, could you please review the changes in this PR when you have a moment?
I’d appreciate any guidance or suggestions for improvement.

@niels9001 niels9001 added the Product-Peek Refers to Peek Powertoys label Apr 29, 2026

@MuyuanMS MuyuanMS left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Suggestions

After thorough review (including iterative Copilot code review on a fork), I've identified several issues ranging from a high-severity SQL injection vulnerability to UX improvements. The suggestions below are ordered by severity and are designed to be self-contained — each can be committed independently via the "Commit suggestion" button.

Summary of Issues

# Issue Severity File
1 SQL identifier injection via bracket-quoting High SQLitePreviewer.cs
2 WinUI binding path fails for column names with special chars Medium SQLiteControl.xaml.cs
3 Misleading row count when table data is truncated Medium SQLiteControl.xaml.cs
4 Key collision when distinct column names sanitize to the same string Medium SQLiteColumnInfo.cs

Recommended approach: Add a new SQLiteHelpers.cs file (suggested in comment 1) that centralizes SQL quoting and binding key management, then update the existing files to use it.

All changes have been verified to compile and function correctly with the Peek module.

Comment thread src/modules/peek/Peek.FilePreviewer/Previewers/SQLitePreviewer/SQLitePreviewer.cs Outdated

using (var cmd = connection.CreateCommand())
{
cmd.CommandText = $"SELECT COUNT(*) FROM [{tableName}];";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same bracket-quoting vulnerability as above. Replace with double-quote escaping:

Suggested change
cmd.CommandText = $"SELECT COUNT(*) FROM [{tableName}];";
cmd.CommandText = $"SELECT COUNT(*) FROM \"{tableName.Replace("\"", "\"\"")}\";";


using (var cmd = connection.CreateCommand())
{
cmd.CommandText = $"SELECT * FROM [{tableName}] LIMIT 200;";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same bracket-quoting vulnerability. Replace with double-quote escaping:

Suggested change
cmd.CommandText = $"SELECT * FROM [{tableName}] LIMIT 200;";
cmd.CommandText = $"SELECT * FROM \"{tableName.Replace("\"", "\"\"")}\" LIMIT 200;";

Comment on lines +128 to +129
{
TableDataGrid.Columns.Add(new DataGridTextColumn

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WinUI binding path fails for column names with special characters (Medium Severity)

The binding path $"[{col.Name}]" uses WinUI's indexer syntax to access dictionary keys. If a column name contains characters special to PropertyPath (., [, ], /), the binding silently fails and the DataGrid shows empty cells for that column.

Similarly, the row dictionary (lines 119–124) uses raw column names as keys. If two distinct column names sanitize to the same key (e.g., a.b and a_b both become a_b), one value overwrites the other.

Recommended fix: Add a BindingKey property to SQLiteColumnInfo and a collision-safe key assignment step:

  1. In SQLiteColumnInfo.cs, add:
public string BindingKey { get; set; } = string.Empty;
  1. After loading columns, call a helper that sanitizes and deduplicates:
// Replaces .[]/  with _ and appends suffix on collision
SQLiteHelpers.AssignBindingKeys(tableInfo.Columns);
  1. Then here, use col.BindingKey instead of col.Name:
Suggested change
{
TableDataGrid.Columns.Add(new DataGridTextColumn
TableDataGrid.Columns.Add(new DataGridTextColumn
{
Header = col.Name,
Binding = new Binding { Path = new PropertyPath($"[{col.BindingKey}]") },
IsReadOnly = true,
});
  1. And in SQLitePreviewer.cs (line 122), use col.BindingKey for the row dictionary key too:
var col = tableInfo.Columns[i];
row[col.BindingKey] = reader.IsDBNull(i) ? null : reader.GetValue(i)?.ToString();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a BindingKey property to SQLiteColumnInfo. The new SQLiteHelpers.AssignBindingKeys() method safely sanitizes column names containing special characters (like ., [, ], /) and handles suffix collisions. Both the row dictionary and the DataGrid bindings now safely use BindingKey.

Comment on lines +140 to +143
// so ActualWidth values are valid when we decide whether to stretch the last column.
TableDataGrid.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, AdjustLastColumnWidth);

RecordCountText.Text = string.Format(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading row count when table data is truncated (Medium Severity)

The record count display shows the total row count from COUNT(*) (e.g., "Records: 5000"), but the DataGrid only shows up to 200 rows due to the LIMIT 200 in the query. Users see "Records: 5000" but can only scroll through 200 rows, with no indication the view is truncated.

Fix: Conditionally format the string to indicate truncation when table.RowCount > table.Rows.Count:

Suggested change
// so ActualWidth values are valid when we decide whether to stretch the last column.
TableDataGrid.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, AdjustLastColumnWidth);
RecordCountText.Text = string.Format(
RecordCountText.Text = table.RowCount > table.Rows.Count
? string.Format(
CultureInfo.CurrentCulture,
ResourceLoaderInstance.ResourceLoader.GetString("SQLite_Row_Count_Truncated"),
table.Rows.Count,
table.RowCount)
: string.Format(
CultureInfo.CurrentCulture,
ResourceLoaderInstance.ResourceLoader.GetString("SQLite_Row_Count"),
table.RowCount);

This requires a new resource string in Resources.resw:

<data name="SQLite_Row_Count_Truncated" xml:space="preserve">
  <value>Records: {0} of {1}</value>
  <comment>{0} is the number of rows shown (limited), {1} is the total row count</comment>
</data>

This way users see "Records: 200 of 5000" making it clear the view is limited.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the suggestion. Added conditional formatting in SQLiteControl.xaml.cs to explicitly show when the table view is truncated. When the total row count exceeds the preview limit, the UI now displays "Records: {shown} of {total}" using the newly added SQLite_Row_Count_Truncated resource string.


public bool IsPrimaryKey { get; set; }

public bool IsNotNull { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add BindingKey property for safe WinUI data binding

To support the binding path fix (see comment on SQLiteControl.xaml.cs), this model needs a BindingKey property. It stores the sanitized, collision-safe key used by both the row dictionary and the DataGrid binding path. The key is computed after all columns are loaded so that duplicate detection can append numeric suffixes (e.g., a_ba_b_2).

Suggested change
public bool IsNotNull { get; set; }
public bool IsNotNull { get; set; }
/// <summary>
/// Sanitized key used for WinUI binding paths and row dictionary keys.
/// Computed after all columns are loaded to handle potential collisions.
/// </summary>
public string BindingKey { get; set; } = string.Empty;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Comment on lines +119 to +123
var row = new Dictionary<string, string?>(reader.FieldCount, StringComparer.Ordinal);
for (int i = 0; i < reader.FieldCount; i++)
{
row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i)?.ToString();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use column's BindingKey for row dictionary keys

The row dictionary keys must match the PropertyPath used in the DataGrid binding. Currently both use the raw column name, which breaks for special characters. After adding BindingKey to SQLiteColumnInfo and computing it with collision detection (via AssignBindingKeys()), use it here:

Suggested change
var row = new Dictionary<string, string?>(reader.FieldCount, StringComparer.Ordinal);
for (int i = 0; i < reader.FieldCount; i++)
{
row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i)?.ToString();
}
var row = new Dictionary<string, string?>(reader.FieldCount, StringComparer.Ordinal);
for (int i = 0; i < reader.FieldCount; i++)
{
var col = tableInfo.Columns[i];
row[col.BindingKey] = reader.IsDBNull(i) ? null : reader.GetValue(i)?.ToString();
}

This ensures the DataGrid can always find the correct value regardless of what characters appear in the original column name.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, updated the relevant code

thomastv and others added 10 commits June 18, 2026 15:58
- Create SQLiteHelpers for safe quoting and WinUI data binding.
- Safely quote identifiers in SQLite queries to mitigate SQL injection.
- Add BindingKey property to map valid WinUI binding paths.
- Update UI to accurately reflect truncated table rows.
- Add SQLite_Row_Count_Truncated resource string.

Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
- Update dependency-review-action to v4.3.4
- Update check-spelling to v0.0.22

Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
Update check-spelling and dependency-review actions to fix CI warnings.

Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
Remove upload/download-artifact actions because they are using deprecated v3.

Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
- Fix upload-artifact and download-artifact v3 deprecation warnings
- Fix dependency-review Node.js deprecation warning
- Fix check-spelling credential leak advisory failure

Co-authored-by: thomastv <44024439+thomastv@users.noreply.github.com>
- Layout cleanup, removed the floating tables and updated the theming
- Refactored to follow modern C# syntax
…k-9659624763486596079

Add SQLiteHelpers and update SQLite previewer

### 🎨 UI & UX Improvements
* **Modernized Empty State**: Replaced the plain "Select a table..." text with a proper, centered Windows 11-style empty state that features a large database `FontIcon`.
* **TreeView Icons**: Added a `SQLiteTreeNode` wrapper to support contextual icons within the TreeView. Tables are now visually distinguished from columns via `FontIcon` glyphs.
* **Layout Cleanups**: Removed the floating "1 tables" footer pill and relocated the table count to a clean header at the top of the left pane.
* **Seamless Theming**: Updated the `DataGrid` (including the `ColumnHeaderStyle` and Row Backgrounds) to be completely `Transparent`. This removes the jarring white boxes and allows the tables to seamlessly blend with the application's native Mica/grey background.

### 🛠️ Code Quality & Build Fixes
* **Resolved StyleCop & IDE Warnings**: Cleaned up the codebase to ensure zero active warnings. This included fixing trailing commas (`SA1413`), using discard parameters for unused event arguments (`IDE0060`, `SA1313`), collapsing redundant `if-else` blocks (`IDE0045`), and simplifying indexers (`IDE0056`).
* **Modernized C# Syntax**: Refactored the `ObservableCollection` and `HashSet` initializations in `SQLitePreviewer.cs` to utilize modern C# 12 collection expressions (`[]`) (`IDE0028`).
* **Safe XAML Data Binding**: Ensured `SQLiteTreeNode` was properly structured as a `public sealed class` (`CA1852`, `SA1402`) so the WinUI 3 XAML `{Binding}` engine can reliably read and resolve properties via reflection without silently failing.

* **Fixed SQL Identifier Injection**: Replaced the bracket-quoting `[identifier]` syntax with standard double-quote escaping. I introduced a shared `SQLiteHelpers.QuoteIdentifier()` method and applied it to all SQL queries (`PRAGMA table_info`, `SELECT COUNT(*)`, and `SELECT *`) in `SQLitePreviewer.cs`.
* **Resolved WinUI Binding Path Crashes**: Added a `BindingKey` property to `SQLiteColumnInfo`. The new `SQLiteHelpers.AssignBindingKeys()` method safely sanitizes column names containing special characters (like `.`, `[`, `]`, `/`) and handles suffix collisions. Both the row dictionary and the DataGrid bindings now safely use `BindingKey`.
* **Clarified Truncated Row Counts**: Added conditional formatting in `SQLiteControl.xaml.cs` to explicitly show when the table view is truncated. When the total row count exceeds the preview limit, the UI now displays "Records: {shown} of {total}" using the newly added `SQLite_Row_Count_Truncated` resource string.
@thomastv thomastv requested a review from a team as a code owner June 18, 2026 16:36
@MuyuanMS

MuyuanMS commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Can you remind me why you changed .yml workflows in .github/workflows in your last change? If it's about spell checks, iirc all you need to do is to add the patterns into according files to exclude them.

@thomastv

thomastv commented Jul 8, 2026

Copy link
Copy Markdown
Author

Can you remind me why you changed .yml workflows in .github/workflows in your last change? If it's about spell checks, iirc all you need to do is to add the patterns into according files to exclude them.

Sorry those files were changed when I synced the main branch, I have reverted it back

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Peek Refers to Peek Powertoys

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add SQLite (.db) File Preview Support to Peek

4 participants