--- name: sql-database-projects description: >- Builds and publishes a SQL database project against Azure SQL Database: the SDK-style `.sqlproj` on `Microsoft.Build.Sql`, the target platform that decides what the build actually validates, pre and post deployment scripts, the refactorlog, and code analysis. Use when a user asks to "create a SQL database project", "build a dacpac", "publish a dacpac to Azure SQL", "add a post-deployment script", "rename a column without losing its data", or "turn on code analysis", and reports "the build passed but the publish failed", "the deploy said success and the data is gone", or "the post-deployment script failed and the table already changed". Covers what `dotnet build` does not check, what SqlPackage does with a mismatched target platform, and which publish options change data rather than schema. github-actions-for-sql owns the pipeline, schema-migrations-safely the change doctrine. --- # Build and publish a SQL database project **This owns the tooling: the project file, the build, the dacpac and the publish.** It does not own how to sequence a schema change (`schema-migrations-safely`), running the publish from a pipeline (`github-actions-for-sql`), or deploying the application beside it (`deploy-app-to-azure`). Open `sqlpackage-import-export` when the job is moving a whole database rather than building one, or when the artifact is a bacpac. Verified 2026-09-03 with the .NET SDK 8.0.421 on macOS, `Microsoft.Build.Sql` 2.2.0, `Microsoft.Build.Sql.Templates` 2.2.0 and SqlPackage 170.4.83.3, publishing into the Azure SQL Database container reporting `EngineEdition` 5, `SQL Azure`, `12.0.2000.8`. ## The build runs anywhere, and SqlPackage is a separate install ```bash dotnet new install Microsoft.Build.Sql.Templates dotnet new sqlproj -n ShopDb -o ShopDb --target-platform SqlAzureV12 dotnet build ShopDb # writes ShopDb/bin/Debug/ShopDb.dacpac dotnet tool install -g microsoft.sqlpackage ``` It is an SDK-style project, so `dotnet build` is the whole build story on Linux, macOS and Windows alike, with no IDE and no Windows-only component. SqlPackage is not installed by the SDK, is not on a hosted build image by default, and every publish below needs it. ## The template's default target is not Azure SQL Database `dotnet new sqlproj --help` lists ten platforms and defaults to `Sql170`, the box product. The choice lands in the project file as one property, and nothing in the build output announces it: ```xml Microsoft.Data.Tools.Schema.Sql.Sql170DatabaseSchemaProvider Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider ``` A project holding `CREATE QUEUE [dbo].[OrderQueue];`, which Azure SQL Database has never supported, built under both: | Target platform | `dotnet build` | Then `/Action:Publish` to Azure SQL Database | |---|---|---| | `Sql170`, the template default | **succeeds, 0 errors** | **refused before any change**, exit 1 | | `SqlAzureV12` | **fails, 1 error** | never reached | Under `SqlAzureV12` the build names the file and the line: ```text OrderQueue.sql(1,1,1,1): Build error SQL70015: Statement 'CREATE QUEUE' is not supported for the targeted platform. ``` Under the default the objection arrives from SqlPackage instead, during plan generation: ```text *** An error occurred during deployment plan generation. Deployment cannot continue. A project which specifies as the target platform cannot be published to Microsoft Azure SQL Database v12. ``` Nothing half-deploys from this cause: SqlPackage compares the dacpac's declared platform against the server and stops. It still costs a release, because the build went green and the artifact was tagged before anyone learned. Check an inherited project without editing it, by overriding the provider on the command line: ```bash dotnet build ShopDb -t:Rebuild /p:DSP=Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider ``` If that fails where the ordinary build passed, the project is not targeting Azure SQL Database. `/p:AllowIncompatiblePlatform=true` on the publish forces past the refusal instead of fixing it. It defaults to `False`. Leave it there. ## The right target validates statements, not references `SqlAzureV12` does not check that a referenced object exists. A procedure calling a mail or agent stored procedure Azure SQL Database does not have builds under the Azure target with only: ```text Build warning SQL71502: Procedure: [dbo].[DoWork] has an unresolved reference to object [dbo].[NoSuchTable]. ``` Warnings do not fail a build, so the procedure reaches the database and fails the first time it runs. Answer each one, or escalate the whole class: ```bash dotnet build ShopDb -t:Rebuild -p:TreatTSqlWarningsAsErrors=true ``` Measured: every `SQL71502` becomes `Build error SQL71502` and the build fails. Where one warning is genuinely fine, `SuppressTSqlWarnings` takes a comma-separated list of numbers. ## Deployment scripts ride in the dacpac and are read by nothing The SDK globs `**/*.sql` into the model, then removes whatever is declared as a deployment script or as `None`. There is a `RefactorLog` item type and no glob creates one. | Item type | Globbed | Parsed and validated | |---|---|---| | `Build`, ordinary object files | yes, `**/*.sql` | yes | | `PreDeploy`, `PostDeploy` | no, declare them | **no, not at all** | | `RefactorLog` | **no, declare it** | not applicable | ```xml ``` A post-deployment script whose entire body is `INSERT INTO dbo.NoSuchTable (id) VALUES (1);` builds with **0 errors and 0 warnings**. The same statement inside a stored procedure produces `SQL71502`. A project takes one pre-deployment and one post-deployment script; chain more with `:r .\scripts\script1.sql` inside them, and add `` so the chained files stay out of the model. Seed data belongs here, written so a second run is a no-op, never in a `Build` file. ## A failed publish does not roll back the schema Publishing a project that adds a column and carries a post-deployment script that fails: ```text Altering Table [dbo].[Product]... Updating database (Failed) Error SQL72014: ... Msg 208, Level 16, State 1, Line 2 Invalid object name 'dbo.NoSuchTable'. Error SQL72045: Script execution error. ``` SqlPackage exits 1, **and the new column is there and stays there.** `/p:IncludeTransactionalScripts=True` changes nothing here: deployment scripts run outside the transactional block, which is exactly the part that failed. So a red pipeline step is not a database that stayed still. Read the schema after a failed publish before deciding anything. `schema-migrations-safely` owns why idempotent scripts are a rule rather than a nicety. ## The rename, which is the one that costs data To a schema comparison a column rename is a drop and an add. The refactorlog turns it back into a rename, and it is the easiest thing here to leave half-wired, because the file existing is not the file being used. Measured on a `Product` table holding two rows: | What the project has | Publish result | |---|---| | the rename, no refactorlog | blocked: `Msg 50000 ... Rows were detected. The schema update is terminating because data loss might occur.` exit 1 | | the file on disk but **not declared** | **the same block.** The build succeeded and said nothing | | declared as a `RefactorLog` item | `Rename [dbo].[Product].[Name] to ProductName`, both rows keep their values, exit 0 | | no refactorlog, `/p:BlockOnPossibleDataLoss=False` | `Successfully published database`, **exit 0, both values now null** | The last row is the whole reason this skill exists: turning that option off to get a pipeline green converts a blocked deployment into a successful one that dropped a column. You do not need a database to find out which row you are in. DeployReport reads a dacpac against another dacpac, so the plan is readable before any server is involved: ```bash sqlpackage /Action:DeployReport /SourceFile:After/bin/Debug/After.dacpac \ /TargetFile:Before/bin/Debug/Before.dacpac /TargetDatabaseName:ShopDb /OutputPath:plan.xml ``` Measured 2026-09-03, the same rename twice. Without the refactorlog declared: ```xml ``` With it declared, the alert is gone and the operation changed: ```xml ``` The first publish carrying a refactorlog creates `dbo.__RefactorLog` in the target and records the operation key, so each rename applies once. That table is deployment state: copying a database without it makes an already-applied rename look outstanding. ## The database is not the project `/p:DropObjectsNotInSource` defaults to **False**. Measured: a table and a procedure created by hand in the target both survive a publish of a project that contains neither, and the publish reports success. Turning it on drops them and keeps `__RefactorLog`: ```text Dropping Table [dbo].[Legacy]... Dropping Procedure [dbo].[LegacyProc]... ``` Neither setting is the safe one. Decide whether this database is a mirror of the project or a superset holding things the project does not describe, and never turn the option on without reading a plan first. ## Code analysis is on request, and advisory until escalated ```xml true ``` Measured on a procedure using `SELECT *` and `@@IDENTITY`: `StaticCodeAnalysis warning SR0001`, `SR0008`, and `Build succeeded. 0 Error(s), 2 Warning(s)`. Warnings in a log nobody reads are not a gate. Escalate one rule with `+!`, which fails the build: ```bash dotnet build ShopDb -t:Rebuild -p:SqlCodeAnalysisRules='+!Microsoft.Rules.Data.SR0001' ``` Measured: `StaticCodeAnalysis error SR0001`, 1 error and 1 warning, `Build FAILED`. Two traps. The `sqlcodeanalysis` template does **not** turn analysis on: it scaffolds a C# project for writing a custom rule, which then has to be packaged and referenced. And `dotnet build` skips analysis entirely on a project that is up to date, so a rerun reporting zero warnings may have analysed nothing. Use `-t:Rebuild` whenever the warning count is the answer you want. ## Publishing ```bash sqlpackage /Action:Publish /SourceFile:ShopDb/bin/Debug/ShopDb.dacpac \ /TargetConnectionString:"Server=tcp:.database.windows.net,1433;Initial Catalog=;Authentication=Active Directory Default;Encrypt=True;" ``` Read the plan against the real target before applying it. Both of these write a file and change nothing: ```bash sqlpackage /Action:DeployReport /SourceFile:ShopDb/bin/Debug/ShopDb.dacpac \ /TargetConnectionString:"Server=tcp:.database.windows.net,1433;Initial Catalog=;Authentication=Active Directory Default;Encrypt=True;" \ /OutputPath:plan.xml sqlpackage /Action:Script /SourceFile:ShopDb/bin/Debug/ShopDb.dacpac \ /TargetConnectionString:"Server=tcp:.database.windows.net,1433;Initial Catalog=;Authentication=Active Directory Default;Encrypt=True;" \ /DeployScriptPath:publish.sql ``` `/OutputPath` is the DeployReport parameter and DeployReport does not accept `/DeployReportPath`; `github-actions-for-sql` owns that trap and the pipeline around it. `/Action:Publish` does not create the logical server, and in Azure that has to exist first. **It does create the database.** Measured against the container reporting `EngineEdition` 5: publishing a dacpac at a database name that did not exist printed `Creating database ...`, then created the table, and the row count in `sys.databases` went from 0 to 1. Do not add a create step before a publish on the assumption that it is required. ## Check it worked Three checks in the order the failures happen. The first two need no database at all. The dacpac declares the platform the publish will compare against the server, and that is the value that decides the refusal, not the text in the project file: ```bash unzip -p ShopDb/bin/Debug/ShopDb.dacpac model.xml | grep -o 'DspName="[^"]*"' ``` Expect exactly `DspName="Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider"`. Anything else and the publish to Azure SQL Database is refused during plan generation. Everything the build refuses to read sits in the package under a fixed name: ```bash unzip -l ShopDb/bin/Debug/ShopDb.dacpac ``` Expect `refactor.xml` if this release carries a rename, `postdeploy.sql` if it carries a post-deployment script, and `predeploy.sql` if it carries a pre-deployment one. Measured 2026-09-03: undeclared, the dacpac holds four entries and no `refactor.xml`; declared, five. A missing entry means the item was never declared, and the build did not complain. Then, after the publish, ask the target whether anything is left: ```bash sqlpackage /Action:DeployReport /SourceFile:ShopDb/bin/Debug/ShopDb.dacpac \ /TargetConnectionString:"" /OutputPath:after.xml grep -c "` with nothing inside it. A publish that landed leaves the next deploy report nothing to do, so any count above zero names an object the run did not apply. ## References - Open [target-platform-and-refactorlog-runs.md](references/target-platform-and-refactorlog-runs.md) when a table above needs reproducing, when a build or publish message you are seeing does not match one quoted here, or to look up a property or SqlPackage option this body only names in passing. - `sdk/Sdk.props` and `sdk/Sdk.targets` inside the `Microsoft.Build.Sql` package in the local NuGet cache: read them when a file is sitting in the project directory and is not in the build. - Microsoft Learn is the authority on whether a property or item type exists. Open [SQL projects properties](https://learn.microsoft.com/sql/tools/sql-database-projects/concepts/project-properties), [target platform](https://learn.microsoft.com/sql/tools/sql-database-projects/concepts/target-platform) or [refactoring](https://learn.microsoft.com/sql/tools/sql-database-projects/concepts/refactor-overview) before writing a property this body does not name.