Skip to content

Repository files navigation

bac2dac

Turns a .bacpac into a .dacpac without going anywhere near a SQL Server.

Warning

The bacpac and dacpac layouts are Microsoft's internal packaging details. Everything here was worked out by unzipping packages and diffing them against dacpacs SqlPackage produced from the same database - there is no specification behind it, and a DacFx update can break it silently.

Install

Needs .NET 10.

dotnet tool install --global dotnet-bac2dac
dotnet bac2dac AdventureWorks.bacpac
Usage:
  dotnet-bac2dac <input.bacpac> [output.dacpac] [options]

Options:
  -o, --output <path>  Same as the positional output argument.
  -f, --force          Overwrite the output file if it already exists.
  -q, --quiet          Print nothing unless something goes wrong.
  -v, --version        Print the version and exit.
  -h, --help           Print this help and exit.

With no output given it writes next to the input with the extension swapped. It refuses to overwrite unless you pass --force, and removes the output it was writing if the conversion fails, so a failed run never leaves a dacpac that looks finished but isn't. Exits 0 on success, 1 on a failed conversion, 2 on a bad command line.

As a library

dotnet add package bac2dac
await using var input = File.OpenRead("AdventureWorks.bacpac");
await using var output = File.Create("AdventureWorks.dacpac");

await Bac2DacConverter.ConvertAsync(input, output);

The input has to be seekable - that's what reading a zip central directory needs. Neither stream is closed for you, and entries are streamed one at a time, so memory doesn't track the size of the bacpac. Nothing outside the BCL is pulled in - System.IO.Compression and System.Xml.Linq.

Tests

dotnet test

Two suites: the converter against real packages, and the command line parser.

The test data isn't in the repository - the samples are tens of megabytes each and artifacts/ is gitignored, so on a fresh clone the converter tests have nothing to run against. Generate it first:

./generate-test-data.sh

That needs Docker, ~240 MB of downloads and a few minutes on the first run. It fetches the Microsoft sample backups into a volume, restores them into a SQL Server 2025 container, verifies each database is online, then runs SqlPackage twice per database - Extract for the dacpac, Export for the bacpac - into bac2dac.tests/artifacts/<Database>/. Containers are torn down at the end and the volumes kept, so later runs are quick. --force regenerates everything.

On Apple Silicon the SQL Server image is linux/amd64 and runs emulated, which is slow but works.

Which databases

Database Size (bacpac) What it drags in
AdventureWorksLT2025 370 KB 12 tables. The one to iterate on - converts in ~50 ms.
AdventureWorks2025 17 MB 71 tables, XML schema collections, sequences, triggers, 538 extended properties.
AdventureWorksDW2025 14 MB Star schema, wide fact tables.
WideWorldImporters 59 MB The hard one: memory-optimized and temporal tables, columnstore, partition schemes.
WideWorldImportersDW 20 MB Columnstore warehouse, memory-optimized staging, partitioning.

Microsoft's own samples, which is the point - the closest thing to a neutral corpus of SQL Server features, and nobody has to trust a schema I wrote myself.

Each pair is converted and compared to the SqlPackage dacpac using DacFx's SchemaComparison. The test requires the comparison to be valid (DacFx could open the generated package at all - this is where a bad checksum or stray BOM fails), equal (zero differences either way), and the model to hold at least one user-defined object, since an empty package would otherwise compare equal to nothing and pass. Schema equivalence, not byte equality.

Against your own database

Test data is discovered from the filesystem, so there's no code to change - drop a matched pair in and dotnet test picks it up:

bac2dac.tests/artifacts/
└── MyDatabase/
    ├── MyDatabase.bacpac
    └── MyDatabase.dacpac

Both files must come from the same database in the same state. Export at lunchtime and extract after the afternoon's migration and you're testing schema drift, not the converter.

To produce the pair:

sqlpackage /Action:Extract \
  /SourceConnectionString:"Server=myserver;Database=MyDatabase;Integrated Security=true;TrustServerCertificate=true" \
  /TargetFile:bac2dac.tests/artifacts/MyDatabase/MyDatabase.dacpac \
  /p:ExtractAllTableData=false
sqlpackage /Action:Export \
  /SourceConnectionString:"Server=myserver;Database=MyDatabase;Integrated Security=true;TrustServerCertificate=true" \
  /TargetFile:bac2dac.tests/artifacts/MyDatabase/MyDatabase.bacpac \
  /p:VerifyFullTextDocumentTypesSupported=false

Both flags matter. ExtractAllTableData=false keeps data out of the dacpac you're comparing against - one with data in it will never match. VerifyFullTextDocumentTypesSupported=false skips an export-side check that fails where full-text filters aren't registered, which is most containers.

To put your database in the Docker generator instead, add it to DATABASE_SPECS in docker/shared/databases.sh and give docker/backups/download-backups.sh somewhere to fetch it from. Everything downstream is driven off that list.

License

MIT.

About

Turns a .bacpac into a .dacpac without going anywhere near a SQL Server

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages