diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs
index b8d0c16..9e97a53 100644
--- a/Lite/Database/DuckDbInitializer.cs
+++ b/Lite/Database/DuckDbInitializer.cs
@@ -86,7 +86,7 @@ public void Dispose()
///
/// Current schema version. Increment this when schema changes require table rebuilds.
///
- internal const int CurrentSchemaVersion = 16;
+ internal const int CurrentSchemaVersion = 17;
private readonly string _archivePath;
@@ -505,6 +505,23 @@ New tables only — no existing table changes needed. Tables created by
GetAllTableStatements() during initialization. */
_logger?.LogInformation("Running migration to v16: adding FinOps tables (database_size_stats, server_properties)");
}
+
+ if (fromVersion < 17)
+ {
+ /* v17: Added volume-level drive space columns to database_size_stats.
+ Columns appended at end — safe for DuckDB appender positional writes. */
+ _logger?.LogInformation("Running migration to v17: adding volume stats columns to database_size_stats");
+ try
+ {
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS volume_mount_point VARCHAR");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS volume_total_mb DECIMAL(19,2)");
+ await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS volume_free_mb DECIMAL(19,2)");
+ }
+ catch
+ {
+ /* Table doesn't exist yet — will be created with correct schema below */
+ }
+ }
}
///
diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs
index 6472cb4..8d978dd 100644
--- a/Lite/Database/Schema.cs
+++ b/Lite/Database/Schema.cs
@@ -609,7 +609,10 @@ auto_growth_mb DECIMAL(19,2),
max_size_mb DECIMAL(19,2),
recovery_model_desc VARCHAR,
compatibility_level INTEGER,
- state_desc VARCHAR
+ state_desc VARCHAR,
+ volume_mount_point VARCHAR,
+ volume_total_mb DECIMAL(19,2),
+ volume_free_mb DECIMAL(19,2)
)";
public const string CreateDatabaseSizeStatsIndex = @"
diff --git a/Lite/Services/RemoteCollectorService.DatabaseSize.cs b/Lite/Services/RemoteCollectorService.DatabaseSize.cs
index cdaace3..6357ded 100644
--- a/Lite/Services/RemoteCollectorService.DatabaseSize.cs
+++ b/Lite/Services/RemoteCollectorService.DatabaseSize.cs
@@ -22,8 +22,8 @@ public partial class RemoteCollectorService
{
///
/// Collects per-file database sizes for growth trending and capacity planning.
- /// On-prem: queries sys.master_files + sys.databases for all online databases.
- /// Azure SQL DB: queries sys.database_files for the single database.
+ /// On-prem: queries sys.master_files + sys.databases + dm_os_volume_stats for file and drive context.
+ /// Azure SQL DB: queries sys.database_files for the single database (no volume stats available).
///
private async Task CollectDatabaseSizeStatsAsync(ServerConnection server, CancellationToken cancellationToken)
{
@@ -63,10 +63,17 @@ ELSE CONVERT(decimal(19,2), mf.max_size * 8.0 / 1024.0)
compatibility_level =
CONVERT(int, d.compatibility_level),
state_desc =
- d.state_desc
+ d.state_desc,
+ volume_mount_point =
+ RTRIM(vs.volume_mount_point),
+ volume_total_mb =
+ CONVERT(decimal(19,2), vs.total_bytes / 1048576.0),
+ volume_free_mb =
+ CONVERT(decimal(19,2), vs.available_bytes / 1048576.0)
FROM sys.master_files AS mf
JOIN sys.databases AS d
ON d.database_id = mf.database_id
+CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) AS vs
WHERE d.state_desc = N'ONLINE'
ORDER BY
d.name,
@@ -106,7 +113,13 @@ ELSE CONVERT(decimal(19,2), df.max_size * 8.0 / 1024.0)
compatibility_level =
CONVERT(int, NULL),
state_desc =
- N'ONLINE'
+ N'ONLINE',
+ volume_mount_point =
+ CONVERT(nvarchar(256), NULL),
+ volume_total_mb =
+ CONVERT(decimal(19,2), NULL),
+ volume_free_mb =
+ CONVERT(decimal(19,2), NULL)
FROM sys.database_files AS df
ORDER BY
df.file_id
@@ -123,7 +136,8 @@ ORDER BY
var rows = new List<(string DatabaseName, int DatabaseId, int FileId, string FileTypeDesc,
string FileName, string PhysicalName, decimal TotalSizeMb, decimal? UsedSizeMb,
decimal? AutoGrowthMb, decimal? MaxSizeMb, string? RecoveryModel,
- int? CompatibilityLevel, string? StateDesc)>();
+ int? CompatibilityLevel, string? StateDesc, string? VolumeMountPoint,
+ decimal? VolumeTotalMb, decimal? VolumeFreeMb)>();
var sqlSw = Stopwatch.StartNew();
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
@@ -146,7 +160,10 @@ ORDER BY
reader.IsDBNull(9) ? null : reader.GetDecimal(9),
reader.IsDBNull(10) ? null : reader.GetString(10),
reader.IsDBNull(11) ? null : reader.GetInt32(11),
- reader.IsDBNull(12) ? null : reader.GetString(12)));
+ reader.IsDBNull(12) ? null : reader.GetString(12),
+ reader.IsDBNull(13) ? null : reader.GetString(13),
+ reader.IsDBNull(14) ? null : reader.GetDecimal(14),
+ reader.IsDBNull(15) ? null : reader.GetDecimal(15)));
}
sqlSw.Stop();
@@ -178,6 +195,9 @@ ORDER BY
.AppendValue(r.RecoveryModel)
.AppendValue(r.CompatibilityLevel)
.AppendValue(r.StateDesc)
+ .AppendValue(r.VolumeMountPoint)
+ .AppendValue(r.VolumeTotalMb)
+ .AppendValue(r.VolumeFreeMb)
.EndRow();
rowsCollected++;
}
diff --git a/install/02_create_tables.sql b/install/02_create_tables.sql
index eb9a17a..d4e7562 100644
--- a/install/02_create_tables.sql
+++ b/install/02_create_tables.sql
@@ -1430,6 +1430,9 @@ BEGIN
recovery_model_desc nvarchar(12) NULL,
compatibility_level integer NULL,
state_desc nvarchar(60) NULL,
+ volume_mount_point nvarchar(256) NULL,
+ volume_total_mb decimal(19,2) NULL,
+ volume_free_mb decimal(19,2) NULL,
/*Analysis helpers - computed columns*/
free_space_mb AS
(
diff --git a/install/06_ensure_collection_table.sql b/install/06_ensure_collection_table.sql
index 71d6f0c..3fad7f2 100644
--- a/install/06_ensure_collection_table.sql
+++ b/install/06_ensure_collection_table.sql
@@ -1110,6 +1110,9 @@ BEGIN
recovery_model_desc nvarchar(12) NULL,
compatibility_level integer NULL,
state_desc nvarchar(60) NULL,
+ volume_mount_point nvarchar(256) NULL,
+ volume_total_mb decimal(19,2) NULL,
+ volume_free_mb decimal(19,2) NULL,
free_space_mb AS
(
total_size_mb - used_size_mb
diff --git a/install/52_collect_database_size_stats.sql b/install/52_collect_database_size_stats.sql
index 386bb72..10937e0 100644
--- a/install/52_collect_database_size_stats.sql
+++ b/install/52_collect_database_size_stats.sql
@@ -112,7 +112,10 @@ BEGIN
max_size_mb,
recovery_model_desc,
compatibility_level,
- state_desc
+ state_desc,
+ volume_mount_point,
+ volume_total_mb,
+ volume_free_mb
)
SELECT
collection_time = @start_time,
@@ -147,7 +150,10 @@ BEGIN
recovery_model_desc =
CONVERT(nvarchar(12), DATABASEPROPERTYEX(DB_NAME(), N'Recovery')),
compatibility_level = NULL,
- state_desc = N'ONLINE'
+ state_desc = N'ONLINE',
+ volume_mount_point = NULL,
+ volume_total_mb = NULL,
+ volume_free_mb = NULL
FROM sys.database_files AS df
OPTION(RECOMPILE);
@@ -200,7 +206,10 @@ BEGIN
max_size_mb,
recovery_model_desc,
compatibility_level,
- state_desc
+ state_desc,
+ volume_mount_point,
+ volume_total_mb,
+ volume_free_mb
)
SELECT
collection_time = @start_time,
@@ -234,9 +243,16 @@ BEGIN
END,
recovery_model_desc = d.recovery_model_desc,
compatibility_level = d.compatibility_level,
- state_desc = d.state_desc
+ state_desc = d.state_desc,
+ volume_mount_point =
+ RTRIM(vs.volume_mount_point),
+ volume_total_mb =
+ CONVERT(decimal(19,2), vs.total_bytes / 1048576.0),
+ volume_free_mb =
+ CONVERT(decimal(19,2), vs.available_bytes / 1048576.0)
FROM sys.database_files AS df
CROSS JOIN sys.databases AS d
+ CROSS APPLY sys.dm_os_volume_stats(DB_ID(), df.file_id) AS vs
WHERE d.database_id = DB_ID();';
EXECUTE sys.sp_executesql
@@ -277,7 +293,10 @@ BEGIN
dss.total_size_mb,
dss.used_size_mb,
dss.free_space_mb,
- dss.used_pct
+ dss.used_pct,
+ dss.volume_mount_point,
+ dss.volume_total_mb,
+ dss.volume_free_mb
FROM collect.database_size_stats AS dss
WHERE dss.collection_time = @start_time
ORDER BY
diff --git a/upgrades/2.1.0-to-2.2.0/06_add_volume_stats_columns.sql b/upgrades/2.1.0-to-2.2.0/06_add_volume_stats_columns.sql
new file mode 100644
index 0000000..a0cf2c2
--- /dev/null
+++ b/upgrades/2.1.0-to-2.2.0/06_add_volume_stats_columns.sql
@@ -0,0 +1,41 @@
+/*
+Copyright 2026 Darling Data, LLC
+https://www.erikdarling.com/
+
+Upgrade from 2.1.0 to 2.2.0
+Adds volume-level drive space columns to database_size_stats.
+*/
+
+SET ANSI_NULLS ON;
+SET ANSI_PADDING ON;
+SET ANSI_WARNINGS ON;
+SET ARITHABORT ON;
+SET CONCAT_NULL_YIELDS_NULL ON;
+SET QUOTED_IDENTIFIER ON;
+SET NUMERIC_ROUNDABORT OFF;
+SET IMPLICIT_TRANSACTIONS OFF;
+SET STATISTICS TIME, IO OFF;
+GO
+
+USE PerformanceMonitor;
+GO
+
+IF NOT EXISTS
+(
+ SELECT
+ 1/0
+ FROM sys.columns
+ WHERE object_id = OBJECT_ID(N'collect.database_size_stats', N'U')
+ AND name = N'volume_mount_point'
+)
+BEGIN
+ ALTER TABLE
+ collect.database_size_stats
+ ADD
+ volume_mount_point nvarchar(256) NULL,
+ volume_total_mb decimal(19,2) NULL,
+ volume_free_mb decimal(19,2) NULL;
+
+ PRINT 'Added volume_mount_point, volume_total_mb, volume_free_mb to collect.database_size_stats';
+END;
+GO
diff --git a/upgrades/2.1.0-to-2.2.0/upgrade.txt b/upgrades/2.1.0-to-2.2.0/upgrade.txt
index 8f2a8d5..1fe7ffd 100644
--- a/upgrades/2.1.0-to-2.2.0/upgrade.txt
+++ b/upgrades/2.1.0-to-2.2.0/upgrade.txt
@@ -3,3 +3,4 @@
03_compress_procedure_stats.sql
04_create_tracking_tables.sql
05_add_finops_collectors.sql
+06_add_volume_stats_columns.sql