Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,313 changes: 2,667 additions & 2,646 deletions Dashboard/Controls/FinOpsContent.xaml

Large diffs are not rendered by default.

59 changes: 56 additions & 3 deletions Dashboard/Controls/FinOpsContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Controls.Primitives;

Check warning on line 19 in Dashboard/Controls/FinOpsContent.xaml.cs

View workflow job for this annotation

GitHub Actions / build

The using directive for 'System.Windows.Controls.Primitives' appeared previously in this namespace

Check warning on line 19 in Dashboard/Controls/FinOpsContent.xaml.cs

View workflow job for this annotation

GitHub Actions / build

The using directive for 'System.Windows.Controls.Primitives' appeared previously in this namespace
using System.Windows.Media;
using Microsoft.Win32;
using PerformanceMonitorDashboard.Helpers;
Expand All @@ -33,6 +34,10 @@
private DateTime _serverInventoryCacheTime;
private decimal _currentServerMonthlyCost;

private DataGridFilterManager<FinOpsDatabaseSizeStats>? _dbSizesFilterMgr;
private Popup? _dbSizeFilterPopup;
private ColumnFilterPopup? _dbSizeFilterPopupContent;

public FinOpsContent()
{
InitializeComponent();
Expand Down Expand Up @@ -70,6 +75,8 @@
TabHelpers.FreezeColumns(ExpensiveQueriesDataGrid, 1);
TabHelpers.FreezeColumns(IndexAnalysisDetailGrid, 1);
TabHelpers.FreezeColumns(HighImpactDataGrid, 1);

_dbSizesFilterMgr = new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid);
}
Comment on lines +79 to 80
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Initialize _dbSizesFilterMgr before first refresh path can use it.

ServerSelector.SelectedIndex = 0 (Line 93) can trigger data loading before OnLoaded runs. Then Line 634 dereferences _dbSizesFilterMgr!, which can be null on first load.

Proposed fix
 public FinOpsContent()
 {
     InitializeComponent();
+    _dbSizesFilterMgr = new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid);
     Loaded += OnLoaded;
 }
@@
-            _dbSizesFilterMgr = new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid);
+            _dbSizesFilterMgr ??= new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid);
@@
-                _dbSizesFilterMgr!.UpdateData(data);
+                _dbSizesFilterMgr ??= new DataGridFilterManager<FinOpsDatabaseSizeStats>(DatabaseSizesDataGrid);
+                _dbSizesFilterMgr.UpdateData(data);

As per coding guidelines, "Dashboard/**/*.cs: ... Watch for: null reference risks ...".

Also applies to: 634-635

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dashboard/Controls/FinOpsContent.xaml.cs` around lines 79 - 80, Initialize
_dbSizesFilterMgr before any code path that can trigger a refresh (e.g., before
ServerSelector.SelectedIndex = 0) to avoid null dereference in the refresh path;
specifically, create the DataGridFilterManager<FinOpsDatabaseSizeStats> instance
(assign to _dbSizesFilterMgr) in the constructor or as a field initializer so
that any call to the refresh/load routine (which dereferences
_dbSizesFilterMgr!) from ServerSelector.SelectedIndex or OnLoaded is safe, or
alternatively guard the refresh code that uses _dbSizesFilterMgr (the calls
around lines that dereference _dbSizesFilterMgr!) with a null-check until
initialization is guaranteed.


/// <summary>
Expand Down Expand Up @@ -624,16 +631,62 @@
}
}

DatabaseSizesDataGrid.ItemsSource = data;
DatabaseSizesNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
DbSizeCountIndicator.Text = data.Count > 0 ? $"{data.Count} file(s)" : "";
_dbSizesFilterMgr!.UpdateData(data);
UpdateDbSizeCountUI();
}
catch (Exception ex)
{
Logger.Error($"Error loading database sizes: {ex.Message}", ex);
}
}

private void UpdateDbSizeCountUI()
{
var list = DatabaseSizesDataGrid.ItemsSource as System.Collections.IList;
int count = list?.Count ?? 0;
DatabaseSizesNoDataMessage.Visibility = count == 0 ? Visibility.Visible : Visibility.Collapsed;
DbSizeCountIndicator.Text = count > 0 ? $"{count} file(s)" : "";
}

private void DatabaseSizesFilter_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button button || button.Tag is not string columnName) return;

if (_dbSizeFilterPopup == null)
{
_dbSizeFilterPopupContent = new ColumnFilterPopup();
_dbSizeFilterPopupContent.FilterApplied += FilterPopup_DbSizeFilterApplied;
_dbSizeFilterPopupContent.FilterCleared += FilterPopup_DbSizeFilterCleared;
_dbSizeFilterPopup = new Popup
{
Child = _dbSizeFilterPopupContent,
StaysOpen = false,
Placement = PlacementMode.Bottom,
AllowsTransparency = true
};
}

_dbSizesFilterMgr!.Filters.TryGetValue(columnName, out var existingFilter);
_dbSizeFilterPopupContent!.Initialize(columnName, existingFilter);
_dbSizeFilterPopup.PlacementTarget = button;
_dbSizeFilterPopup.IsOpen = true;
}

private void FilterPopup_DbSizeFilterApplied(object? sender, FilterAppliedEventArgs e)
{
if (_dbSizeFilterPopup != null)
_dbSizeFilterPopup.IsOpen = false;

_dbSizesFilterMgr!.SetFilter(e.FilterState);
UpdateDbSizeCountUI();
}

private void FilterPopup_DbSizeFilterCleared(object? sender, EventArgs e)
{
if (_dbSizeFilterPopup != null)
_dbSizeFilterPopup.IsOpen = false;
}

// ============================================
// Application Connections Tab
// ============================================
Expand Down
2 changes: 1 addition & 1 deletion Dashboard/Models/CollectionHealthItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class CollectionHealthItem
public decimal FailureRatePercent { get; set; }
public long TotalRuns7d { get; set; }
public long FailedRuns7d { get; set; }
public int AvgDurationMs { get; set; }
public long AvgDurationMs { get; set; }
public long TotalRowsCollected7d { get; set; }
}
}
136 changes: 136 additions & 0 deletions Dashboard/Services/DataGridFilterManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using PerformanceMonitorDashboard.Models;

namespace PerformanceMonitorDashboard.Services;

/// <summary>
/// Non-generic interface for looking up filter state from a shared dictionary.
/// </summary>
public interface IDataGridFilterManager
{
Dictionary<string, ColumnFilterState> Filters { get; }
void SetFilter(ColumnFilterState filterState);
void UpdateFilterButtonStyles();
}

/// <summary>
/// Manages column filter state, unfiltered data capture, and filter application
/// for a single DataGrid. Eliminates per-grid boilerplate code.
/// </summary>
public class DataGridFilterManager<T> : IDataGridFilterManager
{
private readonly DataGrid _dataGrid;
private readonly Dictionary<string, ColumnFilterState> _filters = new();
private List<T>? _unfilteredData;

public DataGridFilterManager(DataGrid dataGrid)
{
_dataGrid = dataGrid;
}

public Dictionary<string, ColumnFilterState> Filters => _filters;

/// <summary>
/// Called when new data arrives (refresh cycle). Captures unfiltered data,
/// then re-applies any active filters.
/// </summary>
public void UpdateData(List<T> newData)
{
_unfilteredData = newData;

if (!HasActiveFilters())
{
_dataGrid.ItemsSource = newData;
return;
}

ApplyFilters();
}

/// <summary>
/// Applies or removes a filter and re-filters the data.
/// </summary>
public void SetFilter(ColumnFilterState filterState)
{
if (filterState.IsActive)
_filters[filterState.ColumnName] = filterState;
else
_filters.Remove(filterState.ColumnName);

ApplyFilters();
UpdateFilterButtonStyles();
}

private bool HasActiveFilters()
{
return _filters.Count > 0 && _filters.Values.Any(f => f.IsActive);
}

private void ApplyFilters()
{
if (_unfilteredData == null) return;

if (!HasActiveFilters())
{
_dataGrid.ItemsSource = _unfilteredData;
return;
}

var filteredData = _unfilteredData.Where(item =>
{
foreach (var filter in _filters.Values)
{
if (filter.IsActive && !DataGridFilterService.MatchesFilter(item!, filter))
return false;
}
return true;
}).ToList();

_dataGrid.ItemsSource = filteredData;
}

/// <summary>
/// Updates filter icon colors (gold when active, dim when inactive).
/// </summary>
public void UpdateFilterButtonStyles()
{
foreach (var column in _dataGrid.Columns)
{
if (column.Header is StackPanel headerPanel)
{
var filterButton = headerPanel.Children.OfType<Button>().FirstOrDefault();
if (filterButton != null && filterButton.Tag is string columnName)
{
bool hasActive = _filters.TryGetValue(columnName, out var filter) && filter.IsActive;

var textBlock = new TextBlock
{
Text = hasActive ? "\uE16E" : "\uE71C",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
Foreground = hasActive
? new SolidColorBrush(Color.FromRgb(0xFF, 0xD7, 0x00))
: (Brush)Application.Current.FindResource("ForegroundDimBrush")
};
filterButton.Content = textBlock;

filterButton.ToolTip = hasActive && filter != null
? $"Filter: {filter.DisplayText}\n(Click to modify)"
: "Click to filter";
}
}
}
}
}
33 changes: 31 additions & 2 deletions Dashboard/Services/DatabaseService.FinOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,10 @@ public async Task<List<FinOpsDatabaseSizeStats>> GetFinOpsDatabaseSizeStatsAsync
state_desc,
volume_mount_point,
volume_total_mb,
volume_free_mb
volume_free_mb,
is_percent_growth,
growth_pct,
vlf_count
FROM collect.database_size_stats
WHERE collection_time =
(
Expand Down Expand Up @@ -364,7 +367,10 @@ ORDER BY
StateDesc = reader.IsDBNull(15) ? "" : reader.GetString(15),
VolumeMountPoint = reader.IsDBNull(16) ? "" : reader.GetString(16),
VolumeTotalMb = reader.IsDBNull(17) ? 0m : Convert.ToDecimal(reader.GetValue(17)),
VolumeFreeMb = reader.IsDBNull(18) ? 0m : Convert.ToDecimal(reader.GetValue(18))
VolumeFreeMb = reader.IsDBNull(18) ? 0m : Convert.ToDecimal(reader.GetValue(18)),
IsPercentGrowth = reader.IsDBNull(19) ? null : (bool?)(Convert.ToInt32(reader.GetValue(19)) == 1),
GrowthPct = reader.IsDBNull(20) ? null : Convert.ToInt32(reader.GetValue(20)),
VlfCount = reader.IsDBNull(21) ? null : Convert.ToInt32(reader.GetValue(21))
});
}
}
Expand Down Expand Up @@ -2540,9 +2546,32 @@ public class FinOpsDatabaseSizeStats
public string VolumeMountPoint { get; set; } = "";
public decimal VolumeTotalMb { get; set; }
public decimal VolumeFreeMb { get; set; }
public bool? IsPercentGrowth { get; set; }
public int? GrowthPct { get; set; }
public int? VlfCount { get; set; }

// FinOps cost — proportional share of server monthly budget
public decimal MonthlyCostShare { get; set; }

public string GrowthDisplay => IsPercentGrowth switch
{
null => "-",
true => GrowthPct.HasValue ? $"{GrowthPct}%" : "-",
false => AutoGrowthMb == 0 ? "Disabled" : $"{AutoGrowthMb:N0} MB"
};
Comment on lines +2556 to +2561
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "DatabaseService.FinOps.cs" | head -5

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 124


🏁 Script executed:

# Get file size first
wc -l ./Dashboard/Services/DatabaseService.FinOps.cs

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 129


🏁 Script executed:

# Read the GrowthDisplay property and surrounding context
sed -n '2540,2580p' ./Dashboard/Services/DatabaseService.FinOps.cs

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 1757


🏁 Script executed:

# Search for the model class definition and property declarations
rg "class.*Growth|GrowthPct|IsPercentGrowth|AutoGrowthMb" ./Dashboard/Services/DatabaseService.FinOps.cs -B 2 -A 2

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 2840


🏁 Script executed:

# Find the SQL query that fetches growth and is_percent_growth
rg "growth|is_percent_growth" ./Dashboard/Services/DatabaseService.FinOps.cs -B 3 -A 3 | head -100

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 1646


🏁 Script executed:

# Search for the query that selects from collect.database_size_stats or the underlying source
rg "FROM collect.database_size_stats|sys\.database_files|is_percent_growth" ./Dashboard/Services/DatabaseService.FinOps.cs -B 5 -A 20 | head -150

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 5024


🏁 Script executed:

# Look for SQL files in the repo that might contain the query definition
find . -type f \( -name "*.sql" -o -name "*.SQL" \) | head -20

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 863


🏁 Script executed:

# Search for any stored procedures or SQL files in Scripts or sql directories
find . -type d \( -name Scripts -o -name sql -o -name SQL \) 2>/dev/null | head -10

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 91


🏁 Script executed:

# Find the file that collects database size stats
grep -l "database_size_stats\|is_percent_growth\|growth_pct" ./install/*.sql

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 315


🏁 Script executed:

# Check the delta framework file which likely contains collection logic
grep -l "growth" ./install/*.sql

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 517


🏁 Script executed:

# Read the database size stats collection script
cat ./install/52_collect_database_size_stats.sql | head -300

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 11573


🏁 Script executed:

# Check if there's more content and look for the growth logic
wc -l ./install/52_collect_database_size_stats.sql

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 126


🏁 Script executed:

# Read more of the SQL to see the rest of the growth_pct logic
sed -n '200,250p' ./install/52_collect_database_size_stats.sql

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 2166


🏁 Script executed:

# Verify that GrowthPct can be 0 by checking the full property definitions one more time
sed -n '2550,2570p' ./Dashboard/Services/DatabaseService.FinOps.cs

Repository: erikdarlingdata/PerformanceMonitor

Length of output: 869


Render disabled percent-growth files as Disabled.

SQL Server allows growth = 0 with is_percent_growth = 1 for percent-growth files with no growth configured. The current display logic shows this as 0%, creating an inconsistency with the fixed-growth case where AutoGrowthMb = 0 displays as Disabled.

🔧 Suggested fix
 public string GrowthDisplay => IsPercentGrowth switch
 {
     null  => "-",
-    true  => GrowthPct.HasValue ? $"{GrowthPct}%" : "-",
+    true  => !GrowthPct.HasValue ? "-"
+            : GrowthPct.Value == 0 ? "Disabled"
+            : $"{GrowthPct.Value}%",
     false => AutoGrowthMb == 0 ? "Disabled" : $"{AutoGrowthMb:N0} MB"
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dashboard/Services/DatabaseService.FinOps.cs` around lines 2556 - 2561, The
GrowthDisplay getter treats percent-growth files with a configured growth of 0
as "0%" but should show "Disabled" to match fixed-growth behavior; update the
IsPercentGrowth true branch in GrowthDisplay to check for GrowthPct.HasValue &&
GrowthPct == 0 and return "Disabled" in that case, otherwise format the
percentage (e.g., $"{GrowthPct}%"); keep the null case returning "-" and the
false branch unchanged (AutoGrowthMb == 0 => "Disabled" else formatted MB).


public decimal AutoGrowthSort => IsPercentGrowth switch
{
null => -1m,
true => (decimal)(GrowthPct ?? -1),
false => AutoGrowthMb
};

public string VlfCountDisplay => string.Equals(FileTypeDesc, "LOG", StringComparison.OrdinalIgnoreCase)
? (VlfCount?.ToString() ?? "-") : "N/A";

public int VlfCountSort => string.Equals(FileTypeDesc, "LOG", StringComparison.OrdinalIgnoreCase)
? (VlfCount ?? 0) : -1;
}

public class FinOpsTopResourceConsumer
Expand Down
4 changes: 2 additions & 2 deletions Dashboard/Services/DatabaseService.QueryPerformance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,8 +1014,8 @@ USE HINT('ENABLE_PARALLEL_PLAN_PREFERENCE')
AvgRows = reader.IsDBNull(24) ? null : reader.GetInt64(24),
MinRows = reader.IsDBNull(25) ? null : reader.GetInt64(25),
MaxRows = reader.IsDBNull(26) ? null : reader.GetInt64(26),
MinDop = reader.IsDBNull(27) ? null : reader.GetInt16(27),
MaxDop = reader.IsDBNull(28) ? null : reader.GetInt16(28),
MinDop = reader.IsDBNull(27) ? null : Convert.ToInt16(reader.GetValue(27)),
MaxDop = reader.IsDBNull(28) ? null : Convert.ToInt16(reader.GetValue(28)),
MinGrantKb = reader.IsDBNull(29) ? null : reader.GetInt64(29),
MaxGrantKb = reader.IsDBNull(30) ? null : reader.GetInt64(30),
TotalSpills = reader.IsDBNull(31) ? 0 : reader.GetInt64(31),
Expand Down
2 changes: 1 addition & 1 deletion Dashboard/Services/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ CASE health_status
FailureRatePercent = reader.IsDBNull(4) ? 0m : Convert.ToDecimal(reader.GetValue(4), CultureInfo.InvariantCulture),
TotalRuns7d = reader.IsDBNull(5) ? 0L : Convert.ToInt64(reader.GetValue(5), CultureInfo.InvariantCulture),
FailedRuns7d = reader.IsDBNull(6) ? 0L : Convert.ToInt64(reader.GetValue(6), CultureInfo.InvariantCulture),
AvgDurationMs = reader.IsDBNull(7) ? 0 : Convert.ToInt32(reader.GetValue(7), CultureInfo.InvariantCulture),
AvgDurationMs = reader.IsDBNull(7) ? 0 : Convert.ToInt64(reader.GetValue(7), CultureInfo.InvariantCulture),
TotalRowsCollected7d = reader.IsDBNull(8) ? 0L : Convert.ToInt64(reader.GetValue(8), CultureInfo.InvariantCulture)
});
}
Expand Down
21 changes: 21 additions & 0 deletions Lite/Controls/FinOpsTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,27 @@
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding GrowthDisplay}" Width="110" SortMemberPath="AutoGrowthSort">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="GrowthDisplay" Click="FilterButton_Click" Margin="0,0,4,0"/>
<TextBlock Text="Auto Growth" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding VlfCountDisplay}" Width="80" SortMemberPath="VlfCountSort">
<DataGridTextColumn.Header>
<StackPanel Orientation="Horizontal">
<Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="VlfCountDisplay" Click="FilterButton_Click" Margin="0,0,4,0"/>
<TextBlock Text="VLF Count" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Monthly Cost ($)" Binding="{Binding MonthlyCostShare, StringFormat='{}{0:N2}'}" Width="110">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
Expand Down
18 changes: 17 additions & 1 deletion Lite/Database/DuckDbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void Dispose()
/// <summary>
/// Current schema version. Increment this when schema changes require table rebuilds.
/// </summary>
internal const int CurrentSchemaVersion = 21;
internal const int CurrentSchemaVersion = 22;

private readonly string _archivePath;

Expand Down Expand Up @@ -583,6 +583,22 @@ New tables only — no existing table changes needed. Tables created by
_logger?.LogWarning("Migration to v21 encountered an error (non-fatal): {Error}", ex.Message);
}
}

if (fromVersion < 22)
{
_logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats");
try
{
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Migration to v22 failed");
throw;
}
}
Comment on lines +587 to +601
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard v22 ALTERs when database_size_stats doesn’t exist yet.

On upgrades from older schemas, Line 592 can run before database_size_stats exists. Because Line 599 rethrows, initialization aborts instead of continuing to table creation.

💡 Suggested fix
         if (fromVersion < 22)
         {
             _logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats");
             try
             {
-                await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
-                await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
-                await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
+                using var existsCmd = connection.CreateCommand();
+                existsCmd.CommandText = @"
+SELECT COUNT(*)
+FROM information_schema.tables
+WHERE table_name = 'database_size_stats'";
+                var tableExists = Convert.ToInt32(await existsCmd.ExecuteScalarAsync()) > 0;
+
+                if (tableExists)
+                {
+                    await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
+                    await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
+                    await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
+                }
+                else
+                {
+                    _logger?.LogDebug("Skipping v22 ALTERs because database_size_stats does not exist yet; it will be created during table initialization.");
+                }
             }
             catch (Exception ex)
             {
                 _logger?.LogError(ex, "Migration to v22 failed");
                 throw;
             }
         }

As per coding guidelines, Lite/**/*.cs should watch for DuckDB access patterns and reliability in migration behavior.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fromVersion < 22)
{
_logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats");
try
{
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Migration to v22 failed");
throw;
}
}
if (fromVersion < 22)
{
_logger?.LogInformation("Running migration to v22: adding growth rate and VLF count columns to database_size_stats");
try
{
using var existsCmd = connection.CreateCommand();
existsCmd.CommandText = @"
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_name = 'database_size_stats'";
var tableExists = Convert.ToInt32(await existsCmd.ExecuteScalarAsync()) > 0;
if (tableExists)
{
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS is_percent_growth BOOLEAN");
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS growth_pct INTEGER");
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS vlf_count INTEGER");
}
else
{
_logger?.LogDebug("Skipping v22 ALTERs because database_size_stats does not exist yet; it will be created during table initialization.");
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Migration to v22 failed");
throw;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Lite/Database/DuckDbInitializer.cs` around lines 587 - 601, The migration
block in DuckDbInitializer.cs that runs ALTER TABLE on database_size_stats
should first verify the table exists to avoid aborting initialization; use a
lightweight existence check (e.g. query information_schema.tables or run a
scalar like SELECT COUNT(*) ...) via the same connection and ExecuteScalarAsync
before calling ExecuteNonQueryAsync for the three ALTER statements, and only run
them when the table exists; keep logging with _logger but do not rethrow when
the table is absent so subsequent table-creation logic can proceed.

}

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion Lite/Database/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,10 @@ max_size_mb DECIMAL(19,2),
state_desc VARCHAR,
volume_mount_point VARCHAR,
volume_total_mb DECIMAL(19,2),
volume_free_mb DECIMAL(19,2)
volume_free_mb DECIMAL(19,2),
is_percent_growth BOOLEAN,
growth_pct INTEGER,
vlf_count INTEGER
)";

public const string CreateDatabaseSizeStatsIndex = @"
Expand Down
Loading
Loading