From 3fb192aeb34a2f30427290a81c11ac398e988d77 Mon Sep 17 00:00:00 2001 From: JeePee Date: Tue, 1 Sep 2026 15:58:04 +0200 Subject: [PATCH] docs: correct the bacpac claims, and document --schema-only Two commenters on the launch thread pushed back on the same passage, and both were right. A bacpac carries database users and database roles, not only contained users. What it does not carry is the server logins behind them, so they restore orphaned - which is the real limitation and the one worth stating. The claim came from reading one database's model.xml, which held no user or role elements at all, and generalising that into a fact about the format. One sample is not a specification. The corrected passage says what was measured and what it does and does not license. "All-or-nothing" was also too strong: sqlpackage takes /p:TableData= and selects which tables' data to include. The distinction that survives is narrower and still real - no row filter within a table, and the schema is always complete. Also corrected a sentence in "when not to use this" that said a bacpac does not "hold more", which was about artifact size but now reads as a claim about coverage. Separately, --schema-only gets a section of its own. It has existed since the first commit and skips the data path entirely, but it appeared only as a row in a flag table, so #19 was filed asking for a feature that was already there - by someone who had read the README. Comparable flags each have a section; this one now does too, with the nightly-schema-into-git job it exists for, and two caveats worth knowing in advance: output is not yet byte-identical between runs, and verify reports OK on an archive with no rows in it. Fixes #23. Fixes #24. --- README.md | 73 ++++++++++++++++++++++++++++++--------- docs/dbdumper-vs-dacfx.md | 3 +- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5395258..f3433a6 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,40 @@ is byte-identical to one produced with `--parallel 1`. Work is scheduled largest-first, since the run cannot end before its biggest table does and starting that one last would leave the other workers idle. +### Dumping the schema without the data + +`--schema-only` writes the manifest and the DDL and nothing else. It is the whole-database +counterpart to `--exclude-data`, which works a table at a time: + +```bash +dbdumper export --server "DEVBOX\SQLEXPRESS" --database AppDb --out AppDb.dbdump --schema-only +``` + +The data path is skipped rather than filtered: no table is read, and the archive contains no +`data/` entries at all. The cost of the run is the schema pass alone, which scales with the number +of objects rather than with how much data the database holds — a 4 TB database and a 4 MB one with +the same schema take about the same time. + +That makes it usable as a nightly job that keeps a database's shape under version control: + +```bash +for db in AppDb Billing Reporting; do + dbdumper export --server "$SQLHOST" --database "$db" --out "schema/$db.dbdump" --schema-only --force +done +git add schema && git commit -m "nightly schema snapshot" +``` + +Two things to know before relying on that: + +- Output is **not yet byte-identical between runs**, so a nightly commit can show changes even when + nothing in the database moved. See [#21](https://github.com/JeePeeTee/dbdumper/issues/21). +- A schema-only archive restores as **empty tables**, which is the point — but `verify` then + compares zero rows against zero rows and reports `OK`. That is correct and still worth expecting. + +`import` has its own `--schema-only`, which creates the objects from an archive and loads no rows +even when the archive has data in it. The two flags are independent: either end can be +schema-only. + ### Taking part of a table `--exclude-data` is all-or-nothing. `--where` keeps a slice instead: @@ -477,15 +511,18 @@ catalogs; extended properties; constraints declared *inside* a table type (its c reproduced, its primary key is not). Objects it cannot reproduce are reported as warnings at export time rather than silently dropped. -Do not read that list as "a `.bacpac` would have kept these". A bacpac carries contained database -users where they exist, but never server logins, and on the 403-table database used for the -comparisons below its `model.xml` contained no user, role or permission elements at all — the same -403 tables, 403 primary keys, 1,091 indexes, 708 foreign keys and 13 views this tool records, and -nothing about security. What it did carry that this does not is a database master key and two -database-scoped credentials. If you need permissions reproduced, neither format is the answer: -script them with [dbatools](https://dbatools.io) — `Export-DbaLogin` and `Export-DbaUser` — or -SSMS's Generate Scripts, both of which read `sys.database_principals` and -`sys.database_permissions` directly. +Do not read that list as "a `.bacpac` keeps none of these". It keeps more than this tool does: a +bacpac carries database users and database roles. What it does not carry is the **server logins** +behind them, so those users arrive orphaned in the target and someone has to remap them by hand. + +On the 403-table database used for the comparisons below, that bacpac's `model.xml` happened to +contain no user, role or permission elements at all — but that is a fact about one database, not +about the format, and an earlier version of this file wrongly generalised it into one. What it did +carry that this does not is a database master key and two database-scoped credentials. + +If you need permissions reproduced faithfully, neither format is the answer: script them with +[dbatools](https://dbatools.io) — `Export-DbaLogin` and `Export-DbaUser` — or SSMS's Generate +Scripts, both of which read `sys.database_principals` and `sys.database_permissions` directly. ## Archive format @@ -567,10 +604,10 @@ read by humans. That buys selectivity and transparency, and costs throughput and jobs are better served by something else. **Copying a large database out of Azure SQL wholesale.** Use a `.bacpac` produced *server-side*, -so the extract never touches your connection. Not because a bacpac holds more — on the database -these notes were written against the two artifacts came out within 2% of each other, 488 MB -against 498 MB, describing the same 403 tables — but because the extract runs inside Azure instead -of across your link: +so the extract never touches your connection. Not because the artifact is smaller — on the +database these notes were written against the two came out within 2% of each other, 488 MB against +498 MB, describing the same 403 tables — but because the extract runs inside Azure instead of +across your link: ```bash az sql db export --resource-group --server --name \ @@ -598,8 +635,8 @@ copy. **Users, roles and permissions.** Not captured — see [what gets captured](#what-gets-captured) for the full list of what is left out. Script them with [dbatools](https://dbatools.io) (`Export-DbaLogin`, `Export-DbaUser`) or SSMS's Generate Scripts, -which read the security catalogue directly. A `.bacpac` is not the alternative here: it carries -contained users at best and no server logins at all. +which read the security catalogue directly. A `.bacpac` is only a partial alternative: it carries +database users and roles, but not the server logins behind them, so they restore orphaned. Microsoft's `mssql-scripter` used to be the obvious cross-platform answer, and earlier versions of this README recommended it. It is @@ -615,7 +652,11 @@ DacFx are. ### When it is the right tool - Pulling *part* of a database — `--exclude-data` on a log table turned one 5 GB transfer into - 1.4 GB, where a `.bacpac` is all-or-nothing. + 1.4 GB. `sqlpackage` can do that much with `/p:TableData=`, but not a row filter within a table, + and never a subset of the schema. +- Keeping a database's shape in version control — see + [dumping the schema without the data](#dumping-the-schema-without-the-data), which costs the same + whether the database holds 4 MB or 4 TB. - Wanting to read, grep or diff what you dumped, or edit `manifest.json` before restoring. - Restoring somewhere with nothing installed: one static binary, no DacFx, no SSMS. - Restoring into a differently named database, or a subset of tables, without ceremony. diff --git a/docs/dbdumper-vs-dacfx.md b/docs/dbdumper-vs-dacfx.md index a78938a..dc4ac49 100644 --- a/docs/dbdumper-vs-dacfx.md +++ b/docs/dbdumper-vs-dacfx.md @@ -46,7 +46,8 @@ Advantages that follow from the design rather than from a measurement. ### Export only part of a database -A `.bacpac` is all-or-nothing: every table, every row. +`sqlpackage` is not all-or-nothing — `/p:TableData=` selects which tables' data to include — but +the subsetting stops at whole tables. There is no row filter, and the schema is always complete. ```bash dbdumper export --include 'sales.*' --exclude-data LogEvents \