Skip to content

Installer: optional custom data/log file locations (#768)#1124

Merged
erikdarlingdata merged 1 commit into
devfrom
feature/768-custom-db-file-locations
Jun 15, 2026
Merged

Installer: optional custom data/log file locations (#768)#1124
erikdarlingdata merged 1 commit into
devfrom
feature/768-custom-db-file-locations

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Problem

Closes #768. The installer always created the PerformanceMonitor database in the instance default data/log directories (SERVERPROPERTY('InstanceDefaultDataPath') / InstanceDefaultLogPath). There was no way to put the .mdf/.ldf on a specific volume at install time.

New flags + usage

Two optional CLI flags, appended to the existing positional <server> [username] [password] contract (positional parsing is unchanged):

Flag Meaning
--data-path DIR Server-side directory for the data (.mdf) file
--log-path DIR Server-side directory for the log (.ldf) file

Both --data-path D:\SQLData and --data-path=D:\SQLData forms are accepted (mirrors --encrypt). Either flag is independent; an omitted one falls back to the instance default. Example:

PerformanceMonitorInstaller.exe MyServer --data-path D:\SQLData --log-path E:\SQLLogs

Help text (--help), the top-of-Main options comment, and the README options table + a usage example are updated.

How CREATE DATABASE now branches

install/01_install_database.sql (inside the existing IF DB_ID(N'PerformanceMonitor') IS NULL guard):

  • New @data_path_override / @log_path_override variables (declared empty) and an inert comment token /*__PM_FILE_PATH_OVERRIDES__*/ (file lines 40-51).
  • On the on-prem/RDS branch: IF LEN(@data_path_override) > 0 SET @data_path = @data_path_override + N'PerformanceMonitor.mdf' ELSE SET @data_path = CONVERT(..., SERVERPROPERTY('InstanceDefaultDataPath')) + ... (lines 82-106). Same for the log path.
  • The installer replaces the token with SET @..._override = N'...'; statements only when a path is supplied (InstallationService.ExecuteInstallationAsync, file 01_ handling). When run without the flags, or outside the installer, the token stays an inert comment and the SERVERPROPERTY defaults are used exactly as before.
  • Azure SQL Managed Instance (engine edition 8) is untouched — it uses a plain CREATE DATABASE and ignores the overrides (MI doesn't support file specs).

Injection safety

The path is user input flowing into a FILENAME = N'...' literal (FILENAME cannot be parameterized in CREATE DATABASE, so the value must be concatenated into dynamic SQL). Defense is layered:

  1. C# validation (Installer.Core/PathValidation.cs): rejects empty/relative paths, control characters (CR/LF/tab — so no extra statements/batches can be smuggled), the chars " < > | * ?, and caps length at 480. Requires an absolute Windows drive / UNC / Linux path. Single quote is intentionally allowed (legal in Windows paths) and handled by escaping.
  2. C# escaping: when building the injected SET @override = N'...' literal, single quotes are doubled (EscapeSqlStringLiteral). BuildFilePathOverrideSql re-validates each path so the Core API is self-defending even if a future caller bypasses the CLI.
  3. T-SQL escaping: the dynamic CREATE DATABASE builds the FILENAME literal via REPLACE(@data_path, N'''', N'''''') (both the primary and the model-resize retry blocks), doubling quotes again at runtime.

A security review traced quote-doubling, control characters, truncation desync, and the GO/:r post-processing and found no breakout.

Idempotency / already-exists behavior

The DB-creation block is guarded by IF DB_ID(N'PerformanceMonitor') IS NULL, so re-running the installer is a no-op for creation. The custom paths therefore apply only on first creation — if the database already exists they are ignored (documented in --help, the README, and the install echo). This is install-only behavior, so no upgrades/ script is needed.

Validation

  • dotnet build Installer/PerformanceMonitorInstaller.csproj -c Debug → Build succeeded, 0 errors, 0 warnings.
  • dotnet test Installer.Tests --filter "Category!=Integration" → 61 passed, 0 failed (34 new PathValidationTests). Integration tests (which require a live SQL Server) were not run.

Risk note

--data-path / --log-path are server-side directories. They must already exist on the SQL Server host and the SQL Server service account must have write permission, or CREATE DATABASE will fail (SQL Server does not create directories for data files). The installer validates only path shape on the client, not existence on the server.

🤖 Generated with Claude Code

#768)

Add optional --data-path / --log-path CLI flags so the installer can place
the PerformanceMonitor data (.mdf) and log (.ldf) files in caller-specified
server-side directories. When omitted, behavior is unchanged: the database
is created using SERVERPROPERTY('InstanceDefaultDataPath'/'InstanceDefaultLogPath').

- 01_install_database.sql declares @data_path_override/@log_path_override and an
  inert comment token the installer replaces with SET statements. The CREATE
  DATABASE step uses the override when present, else the instance default.
  Only applies on first creation (guarded by IF DB_ID(...) IS NULL); ignored if
  the database already exists. Managed Instance (engine edition 8) is unaffected.
- Injection safety is two-layer: PathValidation rejects relative paths, control
  characters (CR/LF/tab), and "<>|*? plus a length cap; the C# layer doubles
  single quotes when building the SET literal; and the SQL builds the FILENAME
  literal via REPLACE(@path, N'''', N''''''). BuildFilePathOverrideSql re-validates
  so the Core API is self-defending regardless of caller.
- New PathValidation class (Installer.Core) with 34 unit tests; help text, usage
  doc, and README options table updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit 4a86487 into dev Jun 15, 2026
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant