Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **PostgreSQL database support** - Full schema extraction for PostgreSQL databases
- `PostgreSqlSchemaExtractor` implementing `ISchemaExtractor` for PostgreSQL connectivity
- `SchemaExtractorFactory` to dynamically instantiate the correct extractor based on database type
- `DatabaseType` enum in Domain layer (`SqlServer`, `PostgreSql`)
- `--database-type` (`-d`) CLI parameter to explicitly specify database type
- Automatic database type detection from connection string patterns
- Cross-database schema comparison capability (compare SQL Server and PostgreSQL schemas)
- Npgsql package dependency for PostgreSQL connectivity

### Changed
- Updated `SchemaExportRequest` DTO to include `DatabaseType` property
- Refactored `SchemaExportService` to use factory pattern for extractor instantiation
- Updated CLI help text with PostgreSQL examples and database type parameter

## [0.0.1] - 2025-12-16

### Added
Expand Down
46 changes: 41 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ Current version: **0.0.1**

## Features

- Export MSSQL database schemas to text format
- Export database schemas to text format from SQL Server and PostgreSQL
- Deterministic, diff-friendly output format
- Alphabetically sorted tables and columns for easy comparison
- Cross-database schema comparison (compare SQL Server vs PostgreSQL schemas)
- Automatic database type detection from connection strings
- Configurable via CLI arguments, environment variables, or configuration files
- Structured logging with Serilog
- **Comprehensive security features:**
Expand Down Expand Up @@ -93,18 +95,33 @@ cd src/DbDiff.Cli/bin/Release/net10.0/publish

### Basic Usage

**SQL Server:**
```bash
dbdiff --connection "Server=localhost;Database=MyDb;Trusted_Connection=true;" --output schema.txt
```

**PostgreSQL:**
```bash
dbdiff --connection "Host=localhost;Database=mydb;Username=user;Password=pass" --output schema.txt
```

### Command-Line Options

- `-c, --connection <string>`: Database connection string (required)
- `-o, --output <path>`: Output file path (default: schema.txt)
- `-d, --database-type <type>`: Database type: `sqlserver`, `postgresql` (auto-detected if not specified)
- `--config <path>`: Path to configuration file (default: appsettings.json)
- `--ignore-position`: Exclude column ordinal positions from output
- `-h, --help`: Show help information

### Database Type Detection

DbDiff can automatically detect the database type from your connection string:
- **SQL Server** keywords: `Server=`, `Data Source=`, `Initial Catalog=`, `Integrated Security=`
- **PostgreSQL** keywords: `Host=`, `Username=`

You can override auto-detection using the `--database-type` parameter.

### Configuration

Connection strings and output paths can be configured in multiple ways (priority order):
Expand Down Expand Up @@ -196,18 +213,37 @@ The project follows **Hexagonal Architecture** (Ports & Adapters) with clear sep
## Supported Databases

- ✅ Microsoft SQL Server (MSSQL)
- 🔄 PostgreSQL (planned)
- ✅ PostgreSQL

### Cross-Database Schema Comparison

DbDiff uses a consistent output format for all database types, making it possible to compare schemas across different database platforms:

```bash
# Export SQL Server schema
dbdiff --connection "Server=localhost;Database=MyDb;Trusted_Connection=true;" --output sqlserver-schema.txt

# Export PostgreSQL schema
dbdiff --connection "Host=localhost;Database=mydb;Username=user;Password=pass" --output postgres-schema.txt

# Compare using your favorite diff tool
diff sqlserver-schema.txt postgres-schema.txt
```

**Note:** Data types will differ between platforms (e.g., SQL Server's `nvarchar` vs PostgreSQL's `character varying`), but the consistent format allows you to easily identify structural differences.

## Development

### Prerequisites

- .NET 10.0 SDK or later
- SQL Server (for MSSQL support)
- SQL Server (for SQL Server support)
- PostgreSQL (for PostgreSQL support)

### Dependencies

- **Microsoft.Data.SqlClient**: SQL Server connectivity
- **Npgsql**: PostgreSQL connectivity
- **Serilog**: Structured logging
- **Microsoft.Extensions.***: Configuration and dependency injection

Expand Down Expand Up @@ -283,11 +319,11 @@ See [CHANGELOG.md](CHANGELOG.md) for version history and planned features.

### Future Enhancements

- PostgreSQL support
- Schema comparison (diff) functionality
- Support for additional database objects (indexes, foreign keys, views, etc.)
- Support for additional database objects (indexes, foreign keys, views, stored procedures, etc.)
- GUI application using AvaloniaUI
- Multiple output formats (JSON, YAML, SQL DDL)
- Support for additional databases (MySQL, Oracle, etc.)

## Security

Expand Down
4 changes: 3 additions & 1 deletion src/DbDiff.Application/DTOs/SchemaExportRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ public class SchemaExportRequest
{
public string ConnectionString { get; init; }
public string OutputPath { get; init; }
public DatabaseType DatabaseType { get; init; }

public SchemaExportRequest(string connectionString, string outputPath)
public SchemaExportRequest(string connectionString, string outputPath, DatabaseType databaseType)
{
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;
DatabaseType = databaseType;
}
}

13 changes: 8 additions & 5 deletions src/DbDiff.Application/Services/SchemaExportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@

public class SchemaExportService
{
private readonly ISchemaExtractor _schemaExtractor;
private readonly ISchemaFormatter _schemaFormatter;
private readonly ILogger<SchemaExportService> _logger;
private readonly Func<DatabaseType, ISchemaExtractor> _extractorFactory;

public SchemaExportService(
ISchemaExtractor schemaExtractor,
Func<DatabaseType, ISchemaExtractor> extractorFactory,
ISchemaFormatter schemaFormatter,
ILogger<SchemaExportService> logger)
{
_schemaExtractor = schemaExtractor ?? throw new ArgumentNullException(nameof(schemaExtractor));
_extractorFactory = extractorFactory ?? throw new ArgumentNullException(nameof(extractorFactory));
_schemaFormatter = schemaFormatter ?? throw new ArgumentNullException(nameof(schemaFormatter));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
Expand All @@ -29,14 +29,17 @@

try
{
_logger.LogInformation("Starting schema extraction from database");
_logger.LogInformation("Starting schema extraction from {DatabaseType} database", request.DatabaseType);

Check warning on line 32 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (macos-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 32 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / build-and-test

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 32 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 32 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 32 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 32 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (ubuntu-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

// Get the appropriate extractor for the database type
var schemaExtractor = _extractorFactory(request.DatabaseType);

// Extract schema from database
var schema = await _schemaExtractor.ExtractSchemaAsync(
var schema = await schemaExtractor.ExtractSchemaAsync(
request.ConnectionString,
cancellationToken);

_logger.LogInformation("Successfully extracted schema from database {DatabaseName} with {TableCount} tables",

Check warning on line 42 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (macos-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 42 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 42 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 42 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 42 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (ubuntu-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
schema.DatabaseName,
schema.Tables.Count);

Expand All @@ -48,19 +51,19 @@
if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
_logger.LogDebug("Created output directory: {OutputDirectory}", outputDirectory);

Check warning on line 54 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 54 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogDebug(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 54 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 54 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (ubuntu-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)
}

// Write to file
await File.WriteAllTextAsync(request.OutputPath, formattedSchema, cancellationToken);

_logger.LogInformation("Schema exported successfully to {OutputPath}", request.OutputPath);

Check warning on line 60 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 60 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)

Check warning on line 60 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

Check warning on line 60 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (ubuntu-latest)

Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873)

return SchemaExportResult.SuccessResult(request.OutputPath, schema.Tables.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to export schema");

Check warning on line 66 in src/DbDiff.Application/Services/SchemaExportService.cs

View workflow job for this annotation

GitHub Actions / multi-platform-test (windows-latest)

For improved performance, use the LoggerMessage delegates instead of calling 'LoggerExtensions.LogError(ILogger, Exception?, string?, params object?[])' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1848)
return SchemaExportResult.FailureResult(ex.Message);
}
}
Expand Down
79 changes: 74 additions & 5 deletions src/DbDiff.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Reflection;

using DbDiff.Application.DTOs;
using DbDiff.Application.Formatters;
using DbDiff.Application.Services;
Expand All @@ -11,6 +13,11 @@

using Serilog;

// Get version from assembly
var version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "Unknown";

// Build configuration
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
Expand Down Expand Up @@ -52,8 +59,15 @@
});

// Register application services
services.AddSingleton<ISchemaExtractor, MsSqlSchemaExtractor>();
services.AddSingleton<ISchemaFormatter, CustomTextFormatter>();

// Register factory function for schema extractors
services.AddSingleton<Func<DatabaseType, ISchemaExtractor>>(serviceProvider =>
{
var factory = new SchemaExtractorFactory();
return (databaseType) => SchemaExtractorFactory.CreateExtractor(databaseType);
Copy link

Choose a reason for hiding this comment

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

Bug: Unused factory instance created but never referenced

The variable factory is instantiated with new SchemaExtractorFactory() on line 67 but is never used. The next line calls SchemaExtractorFactory.CreateExtractor(databaseType) as a static method, meaning the instance is completely unnecessary. This appears to be leftover code from when the method was possibly an instance method, and the instantiation was not removed when it was changed to static.

Fix in Cursor Fix in Web

});

services.AddSingleton<SchemaExportService>();

var serviceProvider = services.BuildServiceProvider();
Expand All @@ -62,6 +76,7 @@
string? connection = null;
string? output = null;
string? configFile = null;
string? databaseTypeArg = null;
bool showHelp = false;
bool ignorePosition = false;

Expand All @@ -81,6 +96,10 @@
if (i + 1 < args.Length)
configFile = args[++i];
break;
case "--database-type" or "-d":
if (i + 1 < args.Length)
databaseTypeArg = args[++i];
break;
case "--ignore-position":
ignorePosition = true;
break;
Expand All @@ -92,14 +111,15 @@

if (showHelp || args.Length == 0)
{
Console.WriteLine("DbDiff - Database Schema Comparison Tool v0.0.1");
Console.WriteLine($"DbDiff - Database Schema Comparison Tool v{version}");
Console.WriteLine();
Console.WriteLine("Usage:");
Console.WriteLine(" dbdiff --connection <connection-string> [options]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -c, --connection <string> Database connection string (required)");
Console.WriteLine(" -o, --output <path> Output file path (default: schema.txt)");
Console.WriteLine(" -d, --database-type <type> Database type: sqlserver, postgresql (auto-detected if not specified)");
Console.WriteLine(" --config <path> Configuration file path");
Console.WriteLine(" --ignore-position Exclude column ordinal positions from output");
Console.WriteLine(" -h, --help Show help information");
Expand All @@ -110,8 +130,14 @@
Console.WriteLine(" - appsettings.json file");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" # SQL Server");
Console.WriteLine(" dbdiff --connection \"Server=localhost;Database=MyDb;Trusted_Connection=true;\" --output schema.txt");
Console.WriteLine(" dbdiff --connection \"Server=localhost;Database=MyDb;Trusted_Connection=true;\" --ignore-position");
Console.WriteLine();
Console.WriteLine(" # PostgreSQL");
Console.WriteLine(" dbdiff --connection \"Host=localhost;Database=mydb;Username=user;Password=pass\" --database-type postgresql");
Console.WriteLine();
Console.WriteLine(" # Auto-detect database type and ignore column positions");
Console.WriteLine(" dbdiff --connection \"Host=localhost;Database=mydb;Username=user;Password=pass\" --ignore-position");
return 0;
}

Expand Down Expand Up @@ -162,7 +188,27 @@
return 1;
}

Console.WriteLine("DbDiff - Database Schema Export v0.0.1");
// Determine database type
DatabaseType databaseType;
if (!string.IsNullOrWhiteSpace(databaseTypeArg))
{
// Explicit database type specified
if (!Enum.TryParse<DatabaseType>(databaseTypeArg, ignoreCase: true, out databaseType))
{
Console.Error.WriteLine($"Error: Invalid database type '{databaseTypeArg}'.");
Console.Error.WriteLine("Valid values: sqlserver, postgresql");
return 1;
}
}
else
{
// Auto-detect database type from connection string
databaseType = DetectDatabaseType(connectionString);
Console.WriteLine($"Auto-detected database type: {databaseType}");
}

Console.WriteLine($"DbDiff - Database Schema Export v{version}");
Console.WriteLine($"Database Type: {databaseType}");
Console.WriteLine($"Output: {outputPath}");
Console.WriteLine();

Expand All @@ -179,7 +225,7 @@
SchemaExportRequest request;
try
{
request = new SchemaExportRequest(connectionString, outputPath);
request = new SchemaExportRequest(connectionString, outputPath, databaseType);
}
catch (UnauthorizedAccessException ex)
{
Expand Down Expand Up @@ -212,3 +258,26 @@
{
await Log.CloseAndFlushAsync();
}

static DatabaseType DetectDatabaseType(string connectionString)
{
// Check for PostgreSQL keywords
if (connectionString.Contains("Host=", StringComparison.OrdinalIgnoreCase) ||
connectionString.Contains("Username=", StringComparison.OrdinalIgnoreCase))
{
return DatabaseType.PostgreSql;
}

// Check for SQL Server keywords
if (connectionString.Contains("Server=", StringComparison.OrdinalIgnoreCase) ||
connectionString.Contains("Data Source=", StringComparison.OrdinalIgnoreCase) ||
connectionString.Contains("Initial Catalog=", StringComparison.OrdinalIgnoreCase) ||
connectionString.Contains("Integrated Security=", StringComparison.OrdinalIgnoreCase))
{
return DatabaseType.SqlServer;
}

// Default to SQL Server for backward compatibility
Console.WriteLine("Warning: Could not auto-detect database type. Defaulting to SQL Server.");
return DatabaseType.SqlServer;
}
8 changes: 8 additions & 0 deletions src/DbDiff.Domain/DatabaseType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DbDiff.Domain;

public enum DatabaseType
{
SqlServer,
PostgreSql
}

1 change: 1 addition & 0 deletions src/DbDiff.Infrastructure/DbDiff.Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageReference Include="Npgsql" Version="8.0.5" />
</ItemGroup>

<PropertyGroup>
Expand Down
Loading
Loading