From b58dbe1a9dce8b07ad8c8efbc1c61157094faf8e Mon Sep 17 00:00:00 2001 From: Oliver Brandford Date: Fri, 19 Dec 2025 11:38:07 +0100 Subject: [PATCH 1/3] Enhance schema export functionality: added support for SQL Server views, including extraction, export, and detailed metadata. Updated related classes and logging to accommodate views alongside tables. --- CHANGELOG.md | 13 ++++ README.md | 32 ++++++-- .../DTOs/SchemaExportResult.cs | 15 ++-- .../Formatters/CustomTextFormatter.cs | 73 +++++++++++++------ .../Services/SchemaExportService.cs | 7 +- src/DbDiff.Domain/DatabaseSchema.cs | 4 +- src/DbDiff.Domain/View.cs | 26 +++++++ .../MsSqlSchemaExtractor.cs | 46 +++++++++++- 8 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 src/DbDiff.Domain/View.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f780ee4..8273bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- View extraction and export functionality for SQL Server +- View SQL definitions included in exports (using sys.sql_modules for complete definitions) +- View column structures with full metadata (data types, nullability, precision, etc.) +- Alphabetically sorted views in output for deterministic comparison +- Support for encrypted views (gracefully handles NULL definitions) + +### Changed +- Updated `DatabaseSchema` domain entity to include `Views` collection +- Enhanced `CustomTextFormatter` to display view definitions and columns +- Improved logging to report both table and view counts +- Updated `SchemaExportResult` DTO with separate `TableCount` and `ViewCount` properties + ## [0.0.1] - 2025-12-16 ### Added diff --git a/README.md b/README.md index b94f940..3920b23 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,11 @@ Current version: **0.0.1** ## Features - Export MSSQL database schemas to text format +- **Complete schema export including:** + - Tables with full column definitions + - Views with SQL definitions and column structures - Deterministic, diff-friendly output format -- Alphabetically sorted tables and columns for easy comparison +- Alphabetically sorted tables, views, and columns for easy comparison - Configurable via CLI arguments, environment variables, or configuration files - Structured logging with Serilog - **Comprehensive security features:** @@ -157,12 +160,31 @@ TABLE: dbo.Users Nullable: Yes MaxLength: 100 -TABLE: dbo.Orders - ... +VIEW: dbo.ActiveUsers + DEFINITION: + CREATE VIEW dbo.ActiveUsers AS + SELECT Id, Name, Email + FROM dbo.Users + WHERE IsActive = 1 + COLUMN: Email + OrdinalPosition: 3 + Type: nvarchar + Nullable: Yes + MaxLength: 255 + COLUMN: Id + OrdinalPosition: 1 + Type: int + Nullable: No + COLUMN: Name + OrdinalPosition: 2 + Type: nvarchar + Nullable: Yes + MaxLength: 100 ``` Features: -- Tables and columns are alphabetically sorted +- Tables, views, and columns are alphabetically sorted +- Views include complete SQL definitions for comparison - Consistent formatting and indentation - One property per line - Deterministic output for reliable diffing @@ -285,7 +307,7 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and planned features. - PostgreSQL support - Schema comparison (diff) functionality -- Support for additional database objects (indexes, foreign keys, views, etc.) +- Support for additional database objects (indexes, foreign keys, stored procedures, functions, triggers, etc.) - GUI application using AvaloniaUI - Multiple output formats (JSON, YAML, SQL DDL) diff --git a/src/DbDiff.Application/DTOs/SchemaExportResult.cs b/src/DbDiff.Application/DTOs/SchemaExportResult.cs index 4bc5d26..f83f359 100644 --- a/src/DbDiff.Application/DTOs/SchemaExportResult.cs +++ b/src/DbDiff.Application/DTOs/SchemaExportResult.cs @@ -4,25 +4,28 @@ public class SchemaExportResult { public bool Success { get; init; } public string ExportedFilePath { get; init; } - public int ObjectCount { get; init; } + public int TableCount { get; init; } + public int ViewCount { get; init; } + public int ObjectCount => TableCount + ViewCount; public string? ErrorMessage { get; init; } - private SchemaExportResult(bool success, string exportedFilePath, int objectCount, string? errorMessage) + private SchemaExportResult(bool success, string exportedFilePath, int tableCount, int viewCount, string? errorMessage) { Success = success; ExportedFilePath = exportedFilePath; - ObjectCount = objectCount; + TableCount = tableCount; + ViewCount = viewCount; ErrorMessage = errorMessage; } - public static SchemaExportResult SuccessResult(string exportedFilePath, int objectCount) + public static SchemaExportResult SuccessResult(string exportedFilePath, int tableCount, int viewCount) { - return new SchemaExportResult(true, exportedFilePath, objectCount, null); + return new SchemaExportResult(true, exportedFilePath, tableCount, viewCount, null); } public static SchemaExportResult FailureResult(string errorMessage) { - return new SchemaExportResult(false, string.Empty, 0, errorMessage); + return new SchemaExportResult(false, string.Empty, 0, 0, errorMessage); } } diff --git a/src/DbDiff.Application/Formatters/CustomTextFormatter.cs b/src/DbDiff.Application/Formatters/CustomTextFormatter.cs index cf540d5..2fd66a5 100644 --- a/src/DbDiff.Application/Formatters/CustomTextFormatter.cs +++ b/src/DbDiff.Application/Formatters/CustomTextFormatter.cs @@ -27,36 +27,65 @@ public string Format(DatabaseSchema schema) foreach (var table in sortedTables) { sb.AppendLine($"TABLE: {table.FullName}"); + FormatColumns(sb, table.Columns); + sb.AppendLine(); + } - // Sort columns alphabetically by name - var sortedColumns = table.Columns - .OrderBy(c => c.Name) - .ToList(); + // Sort views alphabetically by full name for deterministic output + var sortedViews = schema.Views + .OrderBy(v => v.SchemaName) + .ThenBy(v => v.ViewName) + .ToList(); - foreach (var column in sortedColumns) + foreach (var view in sortedViews) + { + sb.AppendLine($"VIEW: {view.FullName}"); + + // Include view definition if available + if (!string.IsNullOrWhiteSpace(view.Definition)) { - sb.AppendLine($" COLUMN: {column.Name}"); - - if (IncludeOrdinalPosition) - sb.AppendLine($" OrdinalPosition: {column.OrdinalPosition}"); - - sb.AppendLine($" Type: {column.DataType}"); - sb.AppendLine($" Nullable: {(column.IsNullable ? "Yes" : "No")}"); - - if (column.MaxLength.HasValue) - sb.AppendLine($" MaxLength: {column.MaxLength.Value}"); - - if (column.Precision.HasValue) - sb.AppendLine($" Precision: {column.Precision.Value}"); - - if (column.Scale.HasValue) - sb.AppendLine($" Scale: {column.Scale.Value}"); + sb.AppendLine(" DEFINITION:"); + // Indent each line of the definition + var definitionLines = view.Definition.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + foreach (var line in definitionLines) + { + sb.AppendLine($" {line}"); + } } - + + FormatColumns(sb, view.Columns); sb.AppendLine(); } return sb.ToString(); } + + private void FormatColumns(StringBuilder sb, IReadOnlyList columns) + { + // Sort columns alphabetically by name + var sortedColumns = columns + .OrderBy(c => c.Name) + .ToList(); + + foreach (var column in sortedColumns) + { + sb.AppendLine($" COLUMN: {column.Name}"); + + if (IncludeOrdinalPosition) + sb.AppendLine($" OrdinalPosition: {column.OrdinalPosition}"); + + sb.AppendLine($" Type: {column.DataType}"); + sb.AppendLine($" Nullable: {(column.IsNullable ? "Yes" : "No")}"); + + if (column.MaxLength.HasValue) + sb.AppendLine($" MaxLength: {column.MaxLength.Value}"); + + if (column.Precision.HasValue) + sb.AppendLine($" Precision: {column.Precision.Value}"); + + if (column.Scale.HasValue) + sb.AppendLine($" Scale: {column.Scale.Value}"); + } + } } diff --git a/src/DbDiff.Application/Services/SchemaExportService.cs b/src/DbDiff.Application/Services/SchemaExportService.cs index 450ce78..d3392a8 100644 --- a/src/DbDiff.Application/Services/SchemaExportService.cs +++ b/src/DbDiff.Application/Services/SchemaExportService.cs @@ -36,9 +36,10 @@ public async Task ExportSchemaAsync( request.ConnectionString, cancellationToken); - _logger.LogInformation("Successfully extracted schema from database {DatabaseName} with {TableCount} tables", + _logger.LogInformation("Successfully extracted schema from database {DatabaseName} with {TableCount} tables and {ViewCount} views", schema.DatabaseName, - schema.Tables.Count); + schema.Tables.Count, + schema.Views.Count); // Format schema to text var formattedSchema = _schemaFormatter.Format(schema); @@ -56,7 +57,7 @@ public async Task ExportSchemaAsync( _logger.LogInformation("Schema exported successfully to {OutputPath}", request.OutputPath); - return SchemaExportResult.SuccessResult(request.OutputPath, schema.Tables.Count); + return SchemaExportResult.SuccessResult(request.OutputPath, schema.Tables.Count, schema.Views.Count); } catch (Exception ex) { diff --git a/src/DbDiff.Domain/DatabaseSchema.cs b/src/DbDiff.Domain/DatabaseSchema.cs index d12291f..c3ecd4f 100644 --- a/src/DbDiff.Domain/DatabaseSchema.cs +++ b/src/DbDiff.Domain/DatabaseSchema.cs @@ -5,8 +5,9 @@ public class DatabaseSchema public string DatabaseName { get; init; } public DateTime ExtractedAt { get; init; } public IReadOnlyList Tables { get; init; } + public IReadOnlyList Views { get; init; } - public DatabaseSchema(string databaseName, DateTime extractedAt, IEnumerable
tables) + public DatabaseSchema(string databaseName, DateTime extractedAt, IEnumerable
tables, IEnumerable views) { if (string.IsNullOrWhiteSpace(databaseName)) throw new ArgumentException("Database name cannot be null or empty.", nameof(databaseName)); @@ -14,6 +15,7 @@ public DatabaseSchema(string databaseName, DateTime extractedAt, IEnumerable Columns { get; init; } + + public View(string schemaName, string viewName, IEnumerable columns, string? definition = null) + { + if (string.IsNullOrWhiteSpace(schemaName)) + throw new ArgumentException("Schema name cannot be null or empty.", nameof(schemaName)); + + if (string.IsNullOrWhiteSpace(viewName)) + throw new ArgumentException("View name cannot be null or empty.", nameof(viewName)); + + SchemaName = schemaName; + ViewName = viewName; + Definition = definition; + Columns = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns)); + } + + public string FullName => $"{SchemaName}.{ViewName}"; +} + diff --git a/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs b/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs index 96f857b..0174a06 100644 --- a/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs +++ b/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs @@ -18,8 +18,9 @@ public async Task ExtractSchemaAsync( var extractedAt = DateTime.UtcNow; var tables = await ExtractTablesAsync(connection, cancellationToken); + var views = await ExtractViewsAsync(connection, cancellationToken); - return new DatabaseSchema(databaseName, extractedAt, tables); + return new DatabaseSchema(databaseName, extractedAt, tables, views); } private async Task> ExtractTablesAsync( @@ -60,6 +61,49 @@ FROM INFORMATION_SCHEMA.TABLES return tables; } + private async Task> ExtractViewsAsync( + SqlConnection connection, + CancellationToken cancellationToken) + { + var views = new List(); + + // Query to get all user views with their definitions (excluding system views) + // Using sys.views and sys.sql_modules to get the full definition (no 4000 char limit) + const string viewQuery = @" + SELECT + s.name AS SchemaName, + v.name AS ViewName, + m.definition AS Definition + FROM sys.views v + INNER JOIN sys.schemas s ON v.schema_id = s.schema_id + LEFT JOIN sys.sql_modules m ON v.object_id = m.object_id + WHERE s.name NOT IN ('sys', 'INFORMATION_SCHEMA') + ORDER BY s.name, v.name"; + + await using var viewCommand = new SqlCommand(viewQuery, connection); + await using var viewReader = await viewCommand.ExecuteReaderAsync(cancellationToken); + + var viewInfoList = new List<(string Schema, string Name, string? Definition)>(); + while (await viewReader.ReadAsync(cancellationToken)) + { + var schemaName = viewReader.GetString(0); + var viewName = viewReader.GetString(1); + var definition = viewReader.IsDBNull(2) ? null : viewReader.GetString(2); + viewInfoList.Add((schemaName, viewName, definition)); + } + + await viewReader.CloseAsync(); + + // Extract columns for each view + foreach (var (schemaName, viewName, definition) in viewInfoList) + { + var columns = await ExtractColumnsAsync(connection, schemaName, viewName, cancellationToken); + views.Add(new View(schemaName, viewName, columns, definition)); + } + + return views; + } + private async Task> ExtractColumnsAsync( SqlConnection connection, string schemaName, From cfcc721f687c6e1d9fd50ddbf5bc141bb21db6f7 Mon Sep 17 00:00:00 2001 From: Oliver Brandford Date: Fri, 19 Dec 2025 13:27:09 +0100 Subject: [PATCH 2/3] Enhance CLI and formatter: added `--exclude-view-definitions` flag to exclude view SQL definitions from output. Updated `CustomTextFormatter` to conditionally include view definitions based on new property. --- CHANGELOG.md | 2 ++ README.md | 1 + src/DbDiff.Application/Formatters/CustomTextFormatter.cs | 5 +++-- src/DbDiff.Cli/Program.cs | 6 ++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8273bec..0ea5a19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - View column structures with full metadata (data types, nullability, precision, etc.) - Alphabetically sorted views in output for deterministic comparison - Support for encrypted views (gracefully handles NULL definitions) +- `--exclude-view-definitions` CLI flag to optionally exclude SQL definitions from output ### Changed - Updated `DatabaseSchema` domain entity to include `Views` collection - Enhanced `CustomTextFormatter` to display view definitions and columns - Improved logging to report both table and view counts - Updated `SchemaExportResult` DTO with separate `TableCount` and `ViewCount` properties +- Made view definitions optional in export output via `IncludeViewDefinitions` formatter property ## [0.0.1] - 2025-12-16 diff --git a/README.md b/README.md index 3920b23..7e5c15e 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ dbdiff --connection "Server=localhost;Database=MyDb;Trusted_Connection=true;" -- - `-o, --output `: Output file path (default: schema.txt) - `--config `: Path to configuration file (default: appsettings.json) - `--ignore-position`: Exclude column ordinal positions from output +- `--exclude-view-definitions`: Exclude view SQL definitions from output (column structure still included) - `-h, --help`: Show help information ### Configuration diff --git a/src/DbDiff.Application/Formatters/CustomTextFormatter.cs b/src/DbDiff.Application/Formatters/CustomTextFormatter.cs index 2fd66a5..7c27f93 100644 --- a/src/DbDiff.Application/Formatters/CustomTextFormatter.cs +++ b/src/DbDiff.Application/Formatters/CustomTextFormatter.cs @@ -5,6 +5,7 @@ namespace DbDiff.Application.Formatters; public class CustomTextFormatter : ISchemaFormatter { public bool IncludeOrdinalPosition { get; set; } = true; + public bool IncludeViewDefinitions { get; set; } = true; public string Format(DatabaseSchema schema) { @@ -41,8 +42,8 @@ public string Format(DatabaseSchema schema) { sb.AppendLine($"VIEW: {view.FullName}"); - // Include view definition if available - if (!string.IsNullOrWhiteSpace(view.Definition)) + // Include view definition if available and enabled + if (IncludeViewDefinitions && !string.IsNullOrWhiteSpace(view.Definition)) { sb.AppendLine(" DEFINITION:"); // Indent each line of the definition diff --git a/src/DbDiff.Cli/Program.cs b/src/DbDiff.Cli/Program.cs index 582ed42..ce1e5a1 100644 --- a/src/DbDiff.Cli/Program.cs +++ b/src/DbDiff.Cli/Program.cs @@ -63,6 +63,7 @@ string? configFile = null; bool showHelp = false; bool ignorePosition = false; +bool excludeViewDefinitions = false; for (int i = 0; i < args.Length; i++) { @@ -83,6 +84,9 @@ case "--ignore-position": ignorePosition = true; break; + case "--exclude-view-definitions": + excludeViewDefinitions = true; + break; case "--help" or "-h" or "-?": showHelp = true; break; @@ -101,6 +105,7 @@ Console.WriteLine(" -o, --output Output file path (default: schema.txt)"); Console.WriteLine(" --config Configuration file path"); Console.WriteLine(" --ignore-position Exclude column ordinal positions from output"); + Console.WriteLine(" --exclude-view-definitions Exclude view SQL definitions from output"); Console.WriteLine(" -h, --help Show help information"); Console.WriteLine(); Console.WriteLine("Configuration:"); @@ -170,6 +175,7 @@ if (formatter is CustomTextFormatter customFormatter) { customFormatter.IncludeOrdinalPosition = !ignorePosition; + customFormatter.IncludeViewDefinitions = !excludeViewDefinitions; } var exportService = serviceProvider.GetRequiredService(); From fcd443f614f7d73fe7db429a2613ace8ad959b78 Mon Sep 17 00:00:00 2001 From: Oliver Brandford Date: Fri, 19 Dec 2025 14:44:33 +0100 Subject: [PATCH 3/3] Refactor code formatting: standardize whitespace across multiple files for improved readability and consistency. --- src/DbDiff.Application.Tests/PathValidatorTests.cs | 14 +++++++------- src/DbDiff.Application/DTOs/SchemaExportRequest.cs | 2 +- .../Formatters/CustomTextFormatter.cs | 8 ++++---- .../Services/SchemaExportService.cs | 10 +++++----- src/DbDiff.Application/Validation/PathValidator.cs | 6 +++--- src/DbDiff.Cli/Program.cs | 12 ++++++------ src/DbDiff.Domain/Column.cs | 10 +++++----- src/DbDiff.Domain/DataType.cs | 2 +- src/DbDiff.Domain/Table.cs | 2 +- src/DbDiff.Domain/View.cs | 2 +- src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs | 6 +++--- 11 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/DbDiff.Application.Tests/PathValidatorTests.cs b/src/DbDiff.Application.Tests/PathValidatorTests.cs index 3d318cd..33a42a5 100644 --- a/src/DbDiff.Application.Tests/PathValidatorTests.cs +++ b/src/DbDiff.Application.Tests/PathValidatorTests.cs @@ -80,7 +80,7 @@ public void ValidateOutputPath_WithAllowedBasePath_WhenPathIsOutsideBase_ThrowsU var outputPath = Path.Combine(Path.GetTempPath(), "outside", "output.txt"); // Act & Assert - Assert.Throws(() => + Assert.Throws(() => PathValidator.ValidateOutputPath(outputPath, basePath)); } @@ -104,7 +104,7 @@ public void ValidateOutputPath_WithSystemDirectory_ThrowsUnauthorizedAccessExcep } // Act & Assert - Assert.Throws(() => + Assert.Throws(() => PathValidator.ValidateOutputPath(systemPath)); } @@ -112,7 +112,7 @@ public void ValidateOutputPath_WithSystemDirectory_ThrowsUnauthorizedAccessExcep public void ValidateOutputPath_WithInvalidCharacters_ThrowsArgumentException() { // Arrange - var invalidPath = OperatingSystem.IsWindows() + var invalidPath = OperatingSystem.IsWindows() ? "output/file<>*.txt" // <, >, * are invalid on Windows : "output/file\0.txt"; // null character is invalid on Unix @@ -160,7 +160,7 @@ public void ValidateConfigPath_WithNonExistentFile_ThrowsFileNotFoundException() var nonExistentPath = Path.Combine(Path.GetTempPath(), "nonexistent-config.json"); // Act & Assert - Assert.Throws(() => + Assert.Throws(() => PathValidator.ValidateConfigPath(nonExistentPath)); } @@ -174,7 +174,7 @@ public void ValidateConfigPath_WithNonJsonFile_ThrowsArgumentException() try { // Act & Assert - Assert.Throws(() => + Assert.Throws(() => PathValidator.ValidateConfigPath(tempTextFile)); } finally @@ -196,7 +196,7 @@ public void ValidateConfigPath_WithAllowedBasePath_WhenFileIsOutsideBase_ThrowsU try { // Act & Assert - Assert.Throws(() => + Assert.Throws(() => PathValidator.ValidateConfigPath(configFile, basePath)); } finally @@ -251,7 +251,7 @@ public void ValidateLogPath_WithSystemDirectory_ThrowsUnauthorizedAccessExceptio } // Act & Assert - Assert.Throws(() => + Assert.Throws(() => PathValidator.ValidateLogPath(systemLogPath)); } diff --git a/src/DbDiff.Application/DTOs/SchemaExportRequest.cs b/src/DbDiff.Application/DTOs/SchemaExportRequest.cs index 9ceb9fa..d727f3a 100644 --- a/src/DbDiff.Application/DTOs/SchemaExportRequest.cs +++ b/src/DbDiff.Application/DTOs/SchemaExportRequest.cs @@ -11,7 +11,7 @@ public SchemaExportRequest(string connectionString, string outputPath) { if (string.IsNullOrWhiteSpace(connectionString)) throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString)); - + // Validate and sanitize the output path to prevent path traversal attacks OutputPath = PathValidator.ValidateOutputPath(outputPath); ConnectionString = connectionString; diff --git a/src/DbDiff.Application/Formatters/CustomTextFormatter.cs b/src/DbDiff.Application/Formatters/CustomTextFormatter.cs index 7c27f93..e3a082f 100644 --- a/src/DbDiff.Application/Formatters/CustomTextFormatter.cs +++ b/src/DbDiff.Application/Formatters/CustomTextFormatter.cs @@ -41,7 +41,7 @@ public string Format(DatabaseSchema schema) foreach (var view in sortedViews) { sb.AppendLine($"VIEW: {view.FullName}"); - + // Include view definition if available and enabled if (IncludeViewDefinitions && !string.IsNullOrWhiteSpace(view.Definition)) { @@ -53,7 +53,7 @@ public string Format(DatabaseSchema schema) sb.AppendLine($" {line}"); } } - + FormatColumns(sb, view.Columns); sb.AppendLine(); } @@ -71,10 +71,10 @@ private void FormatColumns(StringBuilder sb, IReadOnlyList columns) foreach (var column in sortedColumns) { sb.AppendLine($" COLUMN: {column.Name}"); - + if (IncludeOrdinalPosition) sb.AppendLine($" OrdinalPosition: {column.OrdinalPosition}"); - + sb.AppendLine($" Type: {column.DataType}"); sb.AppendLine($" Nullable: {(column.IsNullable ? "Yes" : "No")}"); diff --git a/src/DbDiff.Application/Services/SchemaExportService.cs b/src/DbDiff.Application/Services/SchemaExportService.cs index d3392a8..b64bc55 100644 --- a/src/DbDiff.Application/Services/SchemaExportService.cs +++ b/src/DbDiff.Application/Services/SchemaExportService.cs @@ -11,7 +11,7 @@ public class SchemaExportService private readonly ILogger _logger; public SchemaExportService( - ISchemaExtractor schemaExtractor, + ISchemaExtractor schemaExtractor, ISchemaFormatter schemaFormatter, ILogger logger) { @@ -21,7 +21,7 @@ public SchemaExportService( } public async Task ExportSchemaAsync( - SchemaExportRequest request, + SchemaExportRequest request, CancellationToken cancellationToken = default) { if (request == null) @@ -33,11 +33,11 @@ public async Task ExportSchemaAsync( // Extract schema from database var schema = await _schemaExtractor.ExtractSchemaAsync( - request.ConnectionString, + request.ConnectionString, cancellationToken); - _logger.LogInformation("Successfully extracted schema from database {DatabaseName} with {TableCount} tables and {ViewCount} views", - schema.DatabaseName, + _logger.LogInformation("Successfully extracted schema from database {DatabaseName} with {TableCount} tables and {ViewCount} views", + schema.DatabaseName, schema.Tables.Count, schema.Views.Count); diff --git a/src/DbDiff.Application/Validation/PathValidator.cs b/src/DbDiff.Application/Validation/PathValidator.cs index 29217bc..68f461c 100644 --- a/src/DbDiff.Application/Validation/PathValidator.cs +++ b/src/DbDiff.Application/Validation/PathValidator.cs @@ -178,8 +178,8 @@ private static bool IsPathWithinDirectory(string path, string directory) var normalizedPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var normalizedDirectory = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - return normalizedPath.StartsWith(normalizedDirectory + Path.DirectorySeparatorChar, - GetPathComparison()) || + return normalizedPath.StartsWith(normalizedDirectory + Path.DirectorySeparatorChar, + GetPathComparison()) || normalizedPath.Equals(normalizedDirectory, GetPathComparison()); } @@ -198,7 +198,7 @@ private static bool IsRestrictedSystemPath(string path) // Check system drive root var systemDrive = Path.GetPathRoot(Environment.SystemDirectory); - if (!string.IsNullOrEmpty(systemDrive) && + if (!string.IsNullOrEmpty(systemDrive) && normalizedPath.Equals(systemDrive.TrimEnd(Path.DirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)) return true; } diff --git a/src/DbDiff.Cli/Program.cs b/src/DbDiff.Cli/Program.cs index ce1e5a1..37a5f6f 100644 --- a/src/DbDiff.Cli/Program.cs +++ b/src/DbDiff.Cli/Program.cs @@ -128,9 +128,9 @@ { // Validate config file path - restrict to current directory and subdirectories for security var validatedConfigPath = PathValidator.ValidateConfigPath( - configFile, + configFile, allowedBasePath: Directory.GetCurrentDirectory()); - + configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(validatedConfigPath, optional: false, reloadOnChange: false) @@ -145,13 +145,13 @@ } // Priority: CLI args > Environment variables > Config file - var connectionString = connection + var connectionString = connection ?? Environment.GetEnvironmentVariable("DBDIFF_ConnectionStrings__Default") ?? configuration["ConnectionStrings:Default"]; - var outputPath = output + var outputPath = output ?? Environment.GetEnvironmentVariable("DBDIFF_Export__OutputPath") - ?? configuration["Export:OutputPath"] + ?? configuration["Export:OutputPath"] ?? "schema.txt"; if (string.IsNullOrWhiteSpace(connectionString)) @@ -179,7 +179,7 @@ } var exportService = serviceProvider.GetRequiredService(); - + // Note: Path validation happens in SchemaExportRequest constructor SchemaExportRequest request; try diff --git a/src/DbDiff.Domain/Column.cs b/src/DbDiff.Domain/Column.cs index f1c8239..f53fbc6 100644 --- a/src/DbDiff.Domain/Column.cs +++ b/src/DbDiff.Domain/Column.cs @@ -11,12 +11,12 @@ public class Column public int OrdinalPosition { get; init; } public Column( - string name, - DataType dataType, - bool isNullable, + string name, + DataType dataType, + bool isNullable, int ordinalPosition, - int? maxLength = null, - int? precision = null, + int? maxLength = null, + int? precision = null, int? scale = null) { if (string.IsNullOrWhiteSpace(name)) diff --git a/src/DbDiff.Domain/DataType.cs b/src/DbDiff.Domain/DataType.cs index 59d785c..b65be25 100644 --- a/src/DbDiff.Domain/DataType.cs +++ b/src/DbDiff.Domain/DataType.cs @@ -18,7 +18,7 @@ public override bool Equals(object? obj) { if (obj is DataType other) return TypeName.Equals(other.TypeName, StringComparison.OrdinalIgnoreCase); - + return false; } diff --git a/src/DbDiff.Domain/Table.cs b/src/DbDiff.Domain/Table.cs index aee55b6..326a87d 100644 --- a/src/DbDiff.Domain/Table.cs +++ b/src/DbDiff.Domain/Table.cs @@ -10,7 +10,7 @@ public Table(string schemaName, string tableName, IEnumerable columns) { if (string.IsNullOrWhiteSpace(schemaName)) throw new ArgumentException("Schema name cannot be null or empty.", nameof(schemaName)); - + if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentException("Table name cannot be null or empty.", nameof(tableName)); diff --git a/src/DbDiff.Domain/View.cs b/src/DbDiff.Domain/View.cs index 11cf8b6..6d6719a 100644 --- a/src/DbDiff.Domain/View.cs +++ b/src/DbDiff.Domain/View.cs @@ -11,7 +11,7 @@ public View(string schemaName, string viewName, IEnumerable columns, str { if (string.IsNullOrWhiteSpace(schemaName)) throw new ArgumentException("Schema name cannot be null or empty.", nameof(schemaName)); - + if (string.IsNullOrWhiteSpace(viewName)) throw new ArgumentException("View name cannot be null or empty.", nameof(viewName)); diff --git a/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs b/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs index 0174a06..186c702 100644 --- a/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs +++ b/src/DbDiff.Infrastructure/MsSqlSchemaExtractor.cs @@ -5,7 +5,7 @@ namespace DbDiff.Infrastructure; public class MsSqlSchemaExtractor : ISchemaExtractor { public async Task ExtractSchemaAsync( - string connectionString, + string connectionString, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(connectionString)) @@ -24,7 +24,7 @@ public async Task ExtractSchemaAsync( } private async Task> ExtractTablesAsync( - SqlConnection connection, + SqlConnection connection, CancellationToken cancellationToken) { var tables = new List
(); @@ -62,7 +62,7 @@ FROM INFORMATION_SCHEMA.TABLES } private async Task> ExtractViewsAsync( - SqlConnection connection, + SqlConnection connection, CancellationToken cancellationToken) { var views = new List();