diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e880659a..8581a81f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -74,6 +74,7 @@ Rules: - Long, descriptive test method names: Start with the SMO object type being tested, then scenario, then expected outcome. - Always include assertion messages clarifying intent & expected result. - Logs should make failures diagnosable without rerunning with a debugger. +- For data-driven/parameterized tests, use MSTest attributes (`[DataRow]` or `[DynamicData]`) instead of NUnit attributes (`[TestCase]`, `[TestCaseSource]`, etc.). Pattern example (pseudo): ``` @@ -122,6 +123,7 @@ Avoid: - Placing secret values or tokens in source or tests. - Publishing internal-only folder content to public mirrors. - Exposing raw JSON strings in public SMO APIs; wrap them in typed classes. +- Referencing internal work items (e.g., "VSTS #12345", "work item 12345") in code comments, assertions, or documentation. This is externally shipping code; use descriptive text instead. ## 9. Security & Compliance - Never commit credentials, connection strings with auth info, or access tokens. @@ -166,6 +168,18 @@ Many subdirectories include a focused `README.md` describing domain specifics (e Agent Hint: Before adding or altering code in an unfamiliar area, read the local README to pick up naming, nullability, threading, and performance patterns to mirror. +## 16. Design Specifications + +Design specifications for significant features and architectural changes live in the [`/specs`](../specs/) folder. See the [specs README](../specs/README.md) for an index of all specifications. + +### Workflow for New Design Work +1. **Before starting any new feature or significant change**, check `/specs` for existing relevant specifications. Read any related specs to understand prior decisions and constraints. +2. **If a spec exists for the work you are doing**, follow its design decisions. If implementation reveals issues with the spec, note discrepancies and propose spec updates. +3. **If no spec exists and the work is significant** (new public API surface, cross-cutting architectural changes, new async patterns, etc.), initiate creation of a new spec before implementation. Use the next available sequence number (`NNNN-short-title.md`) and add it to the index in `/specs/README.md`. +4. **Modification of existing specs** requires updating the status field and preserving the change history in the document. + +Agent Hint: When asked to implement a feature, first check `/specs` for a relevant design specification. If one exists, use it as the authoritative source for design decisions. If the work warrants a spec and none exists, propose creating one before proceeding with implementation. + --- Concise Canonical Rules (TL;DR): 1. Always add a failing test for a bug fix. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2995897c..843a6422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Update this document for externally visible changes. Put most recent changes first. Once we push a new version to nuget.org add a double hash header for that version. +## 181.15.0 + +- Fix bug when scripting ALTER USER for Windows Group users + ## 181.12.0 - Remove SQL Server 2005 and prior version support: diff --git a/global.json b/global.json index d14fbe27..8dda7782 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.417", + "version": "10.0.103", "rollForward": "latestMinor" }, "msbuild-sdks": { diff --git a/specs/0001-async-interfaces.md b/specs/0001-async-interfaces.md new file mode 100644 index 00000000..2c05862e --- /dev/null +++ b/specs/0001-async-interfaces.md @@ -0,0 +1,379 @@ +# Spec 0001: Async Interfaces for SMO + +**Status:** Draft +**Created:** 2026-02-10 +**Authors:** SMO Team + +## 1. Summary + +Add asynchronous interfaces to SQL Management Objects (SMO) so that consumers can perform all database interactions — property initialization, collection population, and DDL execution — without blocking threads. An opt-in **async-only mode** on `Server` prevents accidental synchronous database round-trips by throwing `InvalidOperationException` when code accesses un-fetched properties or uninitialized collections. + +## 2. Motivation + +SMO's current programming model is entirely synchronous. Every property access, collection enumeration, and DDL operation can block the calling thread while waiting for a SQL Server round-trip. This is problematic for: + +- **UI applications** (e.g., SSMS, Azure Data Studio) where blocking the UI thread degrades responsiveness. +- **High-throughput services** that manage many connections and cannot afford to pin a thread per operation. +- **Modern .NET development** where `async/await` is the standard pattern for I/O-bound work. + +The underlying ADO.NET layer (`SqlCommand`) already provides `ExecuteReaderAsync`, `ExecuteNonQueryAsync`, and `ExecuteScalarAsync`. SMO should surface this capability to consumers. + +## 3. Design Principles + +1. **Do not break existing synchronous behavior.** All existing method signatures, semantics, and default-mode behavior remain unchanged. +2. **Async methods are purely additive.** They are new API surface alongside existing sync methods. +3. **Async methods are available in both modes.** Consumers can use `LoadAsync()` or `InitializeAsync()` without enabling async-only mode. Async-only mode only restricts the *sync* path. +4. **No sync-over-async.** Existing synchronous methods must never internally call async methods and block on the result. This is strictly prohibited to avoid deadlocks. +5. **Minimize async-over-sync.** New async methods should be truly async end-to-end, using `await` down to `SqlCommand` async methods. Wrapping synchronous calls in `Task.Run()` is strongly discouraged but permitted as a last resort with documented justification when unforeseen barriers arise. +6. **`ConfigureAwait(false)` everywhere.** All internal `await` calls within SMO library code must use `ConfigureAwait(false)` to avoid `SynchronizationContext` capture and potential deadlocks. +7. **Binary compatibility.** Existing compiled code that references current SMO assemblies must continue to work without recompilation. + +## 4. Async-Only Mode + +### 4.1 Scope + +Async-only mode is scoped to a `Server` instance: + +```csharp +var server = new Server(connection); +server.AsyncOnlyMode = true; +``` + +When enabled, any operation that would trigger a **synchronous database fetch** throws `InvalidOperationException` with a descriptive message. This includes: + +- Accessing a property on an `Existing` object that has not been pre-fetched. +- Accessing a collection that has not been explicitly loaded. +- Any implicit lazy initialization triggered by `Count`, indexer, `foreach`, or property getters. + +### 4.2 Exception Behavior + +The exception type is `InvalidOperationException`. Messages should clearly indicate the cause and the remedy: + +- Property access: *"Property '{name}' has not been initialized. In async-only mode, call InitializeAsync() before accessing properties."* +- Collection access: *"Collection has not been loaded. In async-only mode, call LoadAsync() before accessing the collection."* + +### 4.3 Mode Interaction Matrix + +| Mode | Sync access (triggers fetch) | Async methods | Un-fetched property access | +|------|------------------------------|---------------|---------------------------| +| **Default** | ✅ Works as today | ✅ Available | Triggers sync fetch (as today) | +| **Async-only** | ❌ Throws `InvalidOperationException` | ✅ Required path | Throws `InvalidOperationException` | + +### 4.4 Incremental Adoption + +A consumer can incrementally adopt async: + +1. Start using `LoadAsync()` and `InitializeAsync()` in default mode alongside existing sync code. +2. Once confident that all data paths are pre-fetched asynchronously, enable `server.AsyncOnlyMode = true` to catch any remaining sync fetches during development/testing. + +## 5. ServerConnection Async Methods + +### 5.1 Overview + +`ServerConnection` (and its base class `ConnectionManager`) is the single choke-point for all SQL execution. Today, all execution flows through `ConnectionManager.ExecuteTSql()`, which calls synchronous `SqlCommand` methods. + +New async methods are added to `ServerConnection`: + +```csharp +public Task ExecuteNonQueryAsync(string sqlCommand, CancellationToken cancellationToken = default); +public Task ExecuteNonQueryAsync(IEnumerable sqlCommands, CancellationToken cancellationToken = default); +public Task ExecuteReaderAsync(string sqlCommand, CancellationToken cancellationToken = default); +public Task ExecuteWithResultsAsync(string sqlCommand, CancellationToken cancellationToken = default); +public Task ExecuteWithResultsAsync(IEnumerable sqlCommands, CancellationToken cancellationToken = default); +public Task ExecuteScalarAsync(string sqlCommand, CancellationToken cancellationToken = default); +``` + +> **Note on `ExecuteWithResultsAsync` return type:** The sync `ExecuteWithResults` returns `DataSet`, but `DataAdapter.Fill()` is inherently synchronous. The async version returns `DataTable` instead, populated by reading from `SqlDataReader` asynchronously via `ExecuteReaderAsync`. If multiple result sets are needed, callers should use `ExecuteReaderAsync` directly and process result sets via `SqlDataReader.NextResultAsync()`. + +### 5.2 Batch Execution Semantics (`IEnumerable` Overloads) + +The `IEnumerable` overloads execute commands **sequentially**, matching the behavior of the existing synchronous batch methods. If `CancellationToken` is cancelled mid-batch: + +- The currently executing command is cancelled via `SqlCommand` cancellation. +- Remaining commands in the batch are **not** executed. +- `OperationCanceledException` is thrown. +- Commands that completed before cancellation are **not** rolled back (consistent with the existing sync behavior, which does not use implicit transactions for batches). Callers requiring atomicity should wrap batch calls in an explicit transaction. + +### 5.3 CancellationToken Propagation + +`CancellationToken` is an optional parameter (defaulting to `CancellationToken.None`) on all async methods. It propagates all the way down to `SqlCommand.ExecuteReaderAsync(CancellationToken)` / `ExecuteNonQueryAsync(CancellationToken)` / `ExecuteScalarAsync(CancellationToken)`, enabling true cancellation of in-flight SQL queries. + +### 5.4 Internal Async Execution Path + +A new internal method `ExecuteTSqlAsync()` parallels the existing `ExecuteTSql()`. It uses `SqlCommand` async methods at the leaf: + +- `SqlCommand.ExecuteNonQueryAsync(CancellationToken)` +- `SqlCommand.ExecuteReaderAsync(CancellationToken)` +- `SqlCommand.ExecuteScalarAsync(CancellationToken)` + +### 5.5 Dependencies + +- None — this is the foundational layer. All other async components depend on this. + +## 6. SFC Async Layer (Enumerator / Request / ExecuteSql) + +### 6.1 Overview + +The SFC (SQL Foundation Classes) layer orchestrates query construction and execution. The key types in the async path are: + +| Type | Role | +|------|------| +| `Enumerator` | Front-end entry point — `GetData()` processes a `Request` | +| `Request` | Describes what to fetch (URN, fields, result type) | +| `ExecuteSql` | Wraps `ServerConnection` — executes constructed SQL | +| `Environment` | Builds the object list from URN, chains `EnumObject.GetData()` calls | + +### 6.2 New Async Methods + +```csharp +// Enumerator (parameter order matches existing sync Enumerator.GetData) +public static Task GetDataAsync(object connectionInfo, Request request, CancellationToken cancellationToken = default); + +// ExecuteSql +internal Task GetDataTableAsync(CancellationToken cancellationToken = default); +internal Task GetDataReaderAsync(CancellationToken cancellationToken = default); + +// Environment (parameter order matches Enumerator for consistency) +internal Task GetDataAsync(object connectionInfo, Request request, CancellationToken cancellationToken = default); +``` + +### 6.3 Dependencies + +- Depends on: **ServerConnection async methods** (Section 5). + +## 7. Object Initialization and Property Access + +### 7.1 Overview + +Today, `SqlSmoObject` lazily fetches properties via `Initialize()` → `ImplInitialize()` → SFC `Enumerator.GetData()`. The property bag tracks state: + +- **Pending** — no fields fetched yet. +- **Existing (default fields)** — default field set fetched. +- **Existing (all fields)** — all fields fetched. + +### 7.2 New Async Methods on SqlSmoObject + +```csharp +public Task InitializeAsync(CancellationToken cancellationToken = default); +public Task InitializeAsync(string[] fields, CancellationToken cancellationToken = default); +public Task RefreshAsync(CancellationToken cancellationToken = default); +``` + +These mirror the sync granularity: + +- `InitializeAsync()` — fetches the default field set. +- `InitializeAsync(fields)` — fetches a specific set of fields. +- `RefreshAsync()` — fetches all fields (equivalent to `Refresh()`). + +After an async initialization call, properties in the fetched set are accessible synchronously from the in-memory cache. + +### 7.3 Behavior Summary + +| Object State | Property in fetched set | Property NOT in fetched set (default mode) | Property NOT in fetched set (async-only mode) | +|---|---|---|---| +| `Creating` | ✅ Returns value | ✅ Returns value (no DB fetch) | ✅ Returns value (no DB fetch) | +| `Existing` (initialized) | ✅ Returns cached value | Triggers sync fetch | ❌ Throws `InvalidOperationException` | +| `Existing` (not initialized) | N/A | Triggers sync fetch | ❌ Throws `InvalidOperationException` | + +### 7.4 Dependencies + +- Depends on: **SFC async layer** (Section 6). + +## 8. Collection Async Initialization + +### 8.1 Overview + +SMO collections (`server.Databases`, `database.Tables`, etc.) lazily populate on first access (`Count`, indexer, `foreach`). The base class hierarchy is: + +``` +AbstractCollectionBase + └─ SmoCollectionBase + └─ SortedListCollectionBase + └─ SimpleObjectCollectionBase + └─ RemovableCollectionBase +``` + +### 8.2 New Async Method on SmoCollectionBase + +```csharp +public Task LoadAsync(CancellationToken cancellationToken = default); +``` + +`LoadAsync()` eagerly and asynchronously populates the collection. After it completes: + +- `Count`, indexer, and `foreach` work synchronously from the in-memory cache. +- Objects in the collection are initialized with the default field set (same as today's lazy init). + +### 8.3 Cancellation Behavior + +If `CancellationToken` is cancelled during `LoadAsync()`: + +- `LoadAsync()` throws `OperationCanceledException`. +- The collection remains in its **pre-call state** (uninitialized). No partial population. +- The caller can retry with a new token or give up. + +The same rollback-on-cancellation behavior applies to `InitializeAsync()` on `SqlSmoObject` — if cancelled, the object stays in whatever state it was before the call. + +### 8.4 Dependencies + +- Depends on: **SFC async layer** (Section 6). + +## 9. DDL Async Operations (IScriptCreate / IScriptAlter / IScriptDrop) + +### 9.1 Overview + +DDL operations (`Create`, `Alter`, `Drop`) generate T-SQL scripts and execute them. Today, the script generation is handled by virtual methods on SMO object base classes, and execution goes through `ServerConnection.ExecuteNonQuery()`. + +Rather than adding async methods to every individual SMO class, we introduce **public interfaces** for the script-generation contract and **extension methods** that provide async execution. + +### 9.2 New Interfaces + +Defined in the `ConnectionInfo` assembly (alongside existing `ICreatable`, `IAlterable`, `IDroppable`): + +```csharp +/// +/// Represents an SMO object that can generate a CREATE script. +/// +public interface IScriptCreate +{ + /// + /// Generates the CREATE T-SQL script for this object. + /// + IEnumerable GenerateCreateScript(); +} + +/// +/// Represents an SMO object that can generate an ALTER script. +/// +public interface IScriptAlter +{ + IEnumerable GenerateAlterScript(); +} + +/// +/// Represents an SMO object that can generate a DROP script. +/// +public interface IScriptDrop +{ + IEnumerable GenerateDropScript(); +} +``` + +The interfaces are intentionally focused on **script generation only**. They do not expose connection or execution context — the extension methods resolve the `ServerConnection` through the object's existing parent-chain machinery (every `SqlSmoObject` can walk up to its `Server` to obtain the connection). This keeps the interfaces lean and avoids coupling them to `ServerConnection`. + +> **Note:** The exact return types above are directional. Implementation may refine them (e.g., `IReadOnlyList` vs `IEnumerable`) based on how the existing script-generation methods produce output. + +SMO base classes (e.g., `ScriptNameObjectBase`, `NamedSmoObject`) implement the appropriate interfaces. Objects outside the `Smo` assembly that support these operations can also implement the interfaces. + +### 9.3 Extension Methods + +```csharp +public static class SmoAsyncExtensions +{ + public static Task CreateAsync(this IScriptCreate obj, CancellationToken cancellationToken = default); + public static Task AlterAsync(this IScriptAlter obj, CancellationToken cancellationToken = default); + public static Task DropAsync(this IScriptDrop obj, CancellationToken cancellationToken = default); +} +``` + +Consumers call these naturally: + +```csharp +await table.CreateAsync(cancellationToken); +await table.AlterAsync(cancellationToken); +await table.DropAsync(cancellationToken); +``` + +### 9.4 Sync Refactoring Opportunity + +The existing synchronous `Create()`, `Alter()`, and `Drop()` methods could eventually be refactored to consume the same `IScriptCreate` / `IScriptAlter` / `IScriptDrop` interfaces, unifying the script-generation contract. This is not required for the initial async implementation but is a desirable long-term goal. + +### 9.5 Dependencies + +- Depends on: **ServerConnection async methods** (Section 5) for execution. +- Depends on: Existing script-generation machinery in SMO base classes. + +## 10. Component Dependency Graph + +``` +┌──────────────────────────────────────────────────┐ +│ DDL Async (IScriptCreate/Alter/Drop extensions) │ +│ (Section 9) │ +└──────────────────┬───────────────────────────────┘ + │ uses +┌──────────────────▼───────────────────────────────┐ +│ Object InitializeAsync / Collection LoadAsync │ +│ (Sections 7 & 8) │ +└──────────────────┬───────────────────────────────┘ + │ uses +┌──────────────────▼───────────────────────────────┐ +│ SFC Async Layer (Enumerator/ExecuteSql) │ +│ (Section 6) │ +└──────────────────┬───────────────────────────────┘ + │ uses +┌──────────────────▼───────────────────────────────┐ +│ ServerConnection Async Methods │ +│ (Section 5) │ +└──────────────────┬───────────────────────────────┘ + │ uses +┌──────────────────▼───────────────────────────────┐ +│ SqlCommand Async (ADO.NET — already exists) │ +└──────────────────────────────────────────────────┘ + +Async-Only Mode (Section 4) is an orthogonal enforcement +layer on the Server object — it gates sync access paths but +does not depend on or affect the async method implementations. +``` + +## 11. Backward Compatibility Guarantees + +1. **No existing method signatures change.** `Create()`, `Alter()`, `Drop()`, `Initialize()`, etc. remain exactly as they are. +2. **No behavioral change in default mode.** Unless `Server.AsyncOnlyMode` is explicitly set to `true`, everything behaves exactly as it does today. Lazy property fetches happen synchronously. Collections auto-populate on access. +3. **New interfaces on existing classes are not breaking.** Adding `IScriptCreate` to a class that already has `Create()` is additive. Existing code that doesn't reference the interface is unaffected. +4. **Binary compatibility.** Existing compiled code that references current SMO assemblies continues to work without recompilation against the new version. + +## 12. Assembly Location + +| Component | Assembly | +|-----------|----------| +| `IScriptCreate`, `IScriptAlter`, `IScriptDrop` interfaces | `Microsoft.SqlServer.ConnectionInfo` | +| `ServerConnection` async methods | `Microsoft.SqlServer.ConnectionInfo` | +| SFC async layer (`Enumerator`, `ExecuteSql`) | `Microsoft.SqlServer.SqlEnum` | +| `SqlSmoObject.InitializeAsync`, `SmoCollectionBase.LoadAsync` | `Microsoft.SqlServer.Smo` | +| `Server.AsyncOnlyMode` property | `Microsoft.SqlServer.Smo` | +| `SmoAsyncExtensions` (extension methods) | TBD — `Microsoft.SqlServer.Smo` or a new assembly depending on whether all implementors of the interfaces are reachable | + +## 13. Testing Requirements + +### 13.1 Test Organization + +- Async methods have **dedicated test classes** separate from sync tests (e.g., `TableAsyncTests` alongside `TableTests`). +- Shared validation assertions may be refactored into helper methods consumed by both sync and async test classes. +- Follow existing naming conventions: `ObjectType_ScenarioAsync_ExpectedResult`. + +### 13.2 Async-Only Mode Tests + +A dedicated test suite must verify: + +- Pre-fetched properties are accessible after `InitializeAsync()`. +- Un-fetched properties throw `InvalidOperationException` with a descriptive message. +- Uninitialized collections throw `InvalidOperationException` on `Count`, indexer, and `foreach`. +- Collections loaded via `LoadAsync()` are fully accessible synchronously. +- Cancelled `LoadAsync()` leaves the collection uninitialized (no partial state). +- Cancelled `InitializeAsync()` leaves the object in its pre-call state. +- Extension methods (`CreateAsync`, `AlterAsync`, `DropAsync`) work correctly through the `IScriptCreate` / `IScriptAlter` / `IScriptDrop` interfaces. + +### 13.3 Parity Tests + +Async and sync operations must produce **identical T-SQL output**. Tests should verify that `table.Create()` and `await table.CreateAsync()` generate the same script. + +## 14. Open Questions + +- Exact member signatures for `IScriptCreate`, `IScriptAlter`, `IScriptDrop` — a directional sketch is provided in Section 9.2 but return types and shared base interface design need to be finalized during implementation. +- Should `Transfer` (bulk copy/scripting) get async support in this spec or a separate one? +- `IAsyncEnumerable` support on collections — deferred from v1 but may be valuable for large collections in a future iteration. +- Detailed error messages and exception hierarchy — should `InvalidOperationException` subclasses be introduced for more granular `catch` handling? + +## 15. Related Specifications + +- [0002-async-scripter.md](0002-async-scripter.md) — Async support for `Scripter.ScriptAsync()` (placeholder). diff --git a/specs/0002-async-scripter.md b/specs/0002-async-scripter.md new file mode 100644 index 00000000..fe566bb1 --- /dev/null +++ b/specs/0002-async-scripter.md @@ -0,0 +1,33 @@ +# Spec 0002: Async Scripter (ScriptAsync) + +**Status:** Placeholder +**Created:** 2026-02-10 +**Authors:** SMO Team + +## 1. Summary + +Add `ScriptAsync()` to the `Scripter` class, enabling fully asynchronous script generation for SMO objects. The `Scripter` walks object trees, reads properties, and emits T-SQL — making it a high-value target for async since scripting large schemas involves many database round-trips. + +## 2. Prerequisites + +This spec depends on the foundational async infrastructure defined in [0001-async-interfaces.md](0001-async-interfaces.md): + +- `SqlSmoObject.InitializeAsync()` / `RefreshAsync()` for async property fetching. +- `SmoCollectionBase.LoadAsync()` for async collection population. +- SFC async layer (`Enumerator.GetDataAsync`). +- `ServerConnection` async execution methods. + +## 3. Scope + +*To be defined.* This spec will cover: + +- `Scripter.ScriptAsync()` method signature and behavior. +- How the scripter asynchronously pre-fetches properties and collections for the objects being scripted. +- Cancellation support via `CancellationToken`. +- Whether scripting can be streamed (`IAsyncEnumerable`) or is batch-only. +- Interaction with `ScriptingOptions` and dependency discovery. +- Testing requirements and parity with synchronous `Script()`. + +## 4. Design + +*To be completed after [0001-async-interfaces.md](0001-async-interfaces.md) is accepted and implementation is underway.* diff --git a/specs/0003-workload-group-tempdb-rg.md b/specs/0003-workload-group-tempdb-rg.md new file mode 100644 index 00000000..578b54b1 --- /dev/null +++ b/specs/0003-workload-group-tempdb-rg.md @@ -0,0 +1,115 @@ +# Spec 0003: Workload Group TempDB Resource Governance in CREATE Scripts + +**Status:** Implemented +**Created:** 2025-11-26 +**Implemented:** Commit ce35e55b01c6a51615d708fe3b3d8d8f80790a18 +**Authors:** Tong Wu + +## 1. Summary + +Include TempDB resource governance parameters (`group_max_tempdb_data_mb` and `group_max_tempdb_data_percent`) in `CREATE WORKLOAD GROUP` scripts when their values are explicitly set to `-1` (representing `NULL`). Previously, these parameters were only included in `ALTER` scripts; this change ensures parity between CREATE and ALTER script generation. + +## 2. Motivation + +TempDB resource governance parameters control the maximum amount of TempDB space a workload group can consume. When creating a workload group via SMO scripting: + +- **Before this change:** The `-1` value (representing `NULL`/unlimited) was excluded from CREATE scripts but included in ALTER scripts, leading to inconsistent scripting behavior. +- **After this change:** Both CREATE and ALTER scripts consistently include these parameters when set to `-1`, scripted as `group_max_tempdb_data_mb=null` and `group_max_tempdb_data_percent=null`. + +This consistency is important for script generation tools like SSMS Generate Scripts, where users expect CREATE scripts to fully represent the object state. + +## 3. Target SQL Server Versions + +| Version | Support | +|---------|---------| +| SQL Server 2025 (v17.x) | ✅ Fully supported | +| Azure SQL Managed Instance | ✅ Supported | +| SQL Server 2022 and earlier | ❌ Not applicable | + +**Catalog View:** `sys.resource_governor_workload_groups` +**Relevant Columns:** `group_max_tempdb_data_mb`, `group_max_tempdb_data_percent` + +## 4. DDL Syntax + +### 4.1 CREATE WORKLOAD GROUP + +```sql +CREATE WORKLOAD GROUP [group_name] WITH( + group_max_requests=0, + importance=Medium, + request_max_cpu_time_sec=0, + request_max_memory_grant_percent=25, + request_memory_grant_timeout_sec=0, + max_dop=0, + group_max_tempdb_data_mb=null, + group_max_tempdb_data_percent=null) USING [pool_name], EXTERNAL [default] +GO +``` + +### 4.2 ALTER WORKLOAD GROUP + +```sql +ALTER WORKLOAD GROUP [group_name] WITH( + group_max_requests=0, + importance=Medium, + request_max_cpu_time_sec=0, + request_max_memory_grant_percent=25, + request_memory_grant_timeout_sec=0, + max_dop=0, + group_max_tempdb_data_mb=null, + group_max_tempdb_data_percent=null) +GO +``` + +## 5. SMO Implementation + +### 5.1 Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `GroupMaximumTempdbDataMB` | `double` | `-1` | Maximum TempDB data in MB. `-1` means NULL/unlimited. | +| `GroupMaximumTempdbDataPercent` | `double` | `-1` | Maximum TempDB data as percentage. `-1` means NULL/unlimited. | + +### 5.2 Scripting Behavior + +| Value | CREATE Script | ALTER Script (when dirty) | +|-------|---------------|---------------------------| +| `-1` | `group_max_tempdb_data_mb=null`, `group_max_tempdb_data_percent=null` | `group_max_tempdb_data_mb=null`, `group_max_tempdb_data_percent=null` | +| `> 0` | `group_max_tempdb_data_mb=`, `group_max_tempdb_data_percent=` | `group_max_tempdb_data_mb=`, `group_max_tempdb_data_percent=` | +| Not set (property not dirty) | Not included | Not included | + +### 5.3 Code Location + +- **Scripter:** [WorkloadGroupBase.cs](../src/Microsoft/SqlServer/Management/Smo/WorkloadGroupBase.cs) +- **XML Metadata:** [WorkloadGroup.xml](../src/Microsoft/SqlServer/Management/SqlEnum/xml/WorkloadGroup.xml) + +## 6. Formatting + +The implementation ensures proper formatting with line breaks between tempdb parameters in the WITH clause for readability: + +```sql +WITH(group_max_requests=0, + importance=Medium, + request_max_cpu_time_sec=0, + request_max_memory_grant_percent=25, + request_memory_grant_timeout_sec=0, + max_dop=0, + group_max_tempdb_data_mb=null, + group_max_tempdb_data_percent=null) +``` + +## 7. Testing + +Functional tests verify: +1. `-1` values are scripted as `null` in CREATE statements +2. `-1` values are scripted as `null` in ALTER statements (when dirty) +3. Scripts include proper `USING` clause placement after WITH clause + +**Test Location:** [WorkloadSmoTests.cs](../src/FunctionalTest/Smo/GeneralFunctionality/WorkloadSmoTests.cs) + +## 8. Documentation References + +- [Resource Governor Workload Group](https://learn.microsoft.com/sql/relational-databases/resource-governor/resource-governor-workload-group) +- [CREATE WORKLOAD GROUP (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/statements/create-workload-group-transact-sql) +- [ALTER WORKLOAD GROUP (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/statements/alter-workload-group-transact-sql) +- [sys.resource_governor_workload_groups](https://learn.microsoft.com/sql/relational-databases/system-catalog-views/sys-resource-governor-workload-groups-transact-sql) diff --git a/specs/0004-xevent-max-duration.md b/specs/0004-xevent-max-duration.md new file mode 100644 index 00000000..a7f66917 --- /dev/null +++ b/specs/0004-xevent-max-duration.md @@ -0,0 +1,151 @@ +# Spec 0004: XEvent Session MAX_DURATION Property + +**Status:** Implemented +**Created:** 2025-11-15 +**Implemented:** Commit f06bad2f6ce804812991b04cdef3e627bfb75228 +**Authors:** Kapil Thacker + +## 1. Summary + +Add support for the `MAX_DURATION` attribute on Extended Events (XEvent) sessions. This property specifies the maximum duration an event session will run before automatically stopping. The feature was introduced in SQL Server 2025. + +## 2. Motivation + +Extended Events sessions can now have a maximum duration limit, which is useful for: + +- **Diagnostic captures:** Automatically stop tracing after a specified time to avoid collecting excessive data. +- **Scheduled monitoring:** Run event sessions for a defined window without manual intervention. +- **Resource management:** Prevent long-running sessions from consuming resources indefinitely. + +SMO needs to expose this property to enable SSMS and other tools to script and manage sessions with duration limits. + +## 3. Target SQL Server Versions + +| Version | Support | +|---------|---------| +| SQL Server 2025 (v17.x) | ✅ Fully supported | +| Azure SQL Database | ⏳ Not yet enabled (tracked by Bug:4806316) | +| Azure SQL Managed Instance | ⏳ Not yet enabled (tracked by Bug:4816977) | +| SQL Server 2022 and earlier | ❌ Not applicable | + +**Catalog View:** `sys.server_event_sessions` +**Relevant Column:** `max_duration` (bigint, nullable) + +**Note:** The catalog column may not exist on older SQL versions. The SMO implementation uses a conditional `if exists` pattern to check for column existence before executing the dynamic SQL query. + +## 4. DDL Syntax + +### 4.1 CREATE EVENT SESSION + +```sql +CREATE EVENT SESSION [session_name] ON SERVER +ADD EVENT sqlserver.rpc_starting +WITH (MAX_MEMORY=4096 KB, + EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS, + MAX_DISPATCH_LATENCY=30 SECONDS, + MAX_EVENT_SIZE=0 KB, + MEMORY_PARTITION_MODE=NONE, + TRACK_CAUSALITY=OFF, + STARTUP_STATE=OFF, + MAX_DURATION=UNLIMITED) +GO +``` + +### 4.2 MAX_DURATION Values + +| Value | T-SQL Syntax | Description | +|-------|--------------|-------------| +| `0` | `MAX_DURATION=UNLIMITED` | Session runs indefinitely (engine default when omitted) | +| `> 0` | `MAX_DURATION= SECONDS` | Session stops after `n` seconds | + +### 4.3 Example with Specific Duration + +```sql +CREATE EVENT SESSION [session_name] ON SERVER +ADD EVENT sqlserver.rpc_starting +WITH (MAX_DURATION=3600 SECONDS) -- 1 hour +GO +``` + +## 5. SMO Implementation + +### 5.1 Properties + +| Property | Type | SMO Default | Description | +|----------|------|-------------|-------------| +| `MaxDuration` | `long` | `DefaultMaxDuration` (-1) | Duration in seconds. `0` means unlimited. `-1` is the SMO sentinel for "not explicitly set". | + +### 5.2 Constants + +```csharp +public const long DefaultMaxDuration = -1; // Property not explicitly set +public const long UnlimitedDuration = 0; // Explicitly set to UNLIMITED +``` + +### 5.3 Scripting Behavior + +| Value | Script Output | +|-------|---------------| +| `-1` (default) | Property omitted from script | +| `0` (unlimited) | `MAX_DURATION=UNLIMITED` | +| `> 0` | `MAX_DURATION= SECONDS` | + +The property is only included in scripts when explicitly set (not default `-1`), ensuring backward compatibility with existing scripts. + +### 5.4 Code Locations + +- **Session Class:** [Session.cs](../src/Microsoft/SqlServer/Management/XEvent/core/Session.cs) +- **Session Provider:** [SessionProviderBase.cs](../src/Microsoft/SqlServer/Management/XEvent/core/SessionProviderBase.cs) +- **XML Metadata:** [Session.xml](../src/Microsoft/SqlServer/Management/XEventEnum/xml/Session.xml) + +### 5.5 XML Metadata Implementation + +The implementation uses a conditional query pattern to handle version differences: + +```xml + + maxdur.event_session_id = session.event_session_id + + + create table #md (event_session_id int not null, max_duration bigint null) + if exists (select 1 from sys.all_columns + where object_id = object_id('sys.server_event_sessions') + and name = 'max_duration') + begin + declare @sql nvarchar(max) = 'insert into #md select event_session_id, max_duration from sys.server_event_sessions' + exec sp_executesql @sql + end + +``` + +This pattern ensures queries don't fail on older SQL versions lacking the column. + +## 6. Testing + +### 6.1 Unit Tests + +- Verify default value is `DefaultMaxDuration` +- Verify property initialization on new sessions + +**Test Location:** [SessionUnitTest.cs](../src/FunctionalTest/Smo/XEvent/SessionUnitTest.cs) + +### 6.2 Functional Tests + +- `MaxDuration_CreateSession_WithUnlimitedValue_IncludesInScript`: Verify `MAX_DURATION=UNLIMITED` appears when set to `0` +- `MaxDuration_CreateSession_WithValidValue_IncludesInScript`: Verify `MAX_DURATION= SECONDS` appears for positive values +- Tests verify default value omits `MAX_DURATION` from script + +**Test Location:** [XEventSessionTests.cs](../src/FunctionalTest/Smo/XEvent/XEventSessionTests.cs) + +### 6.3 Version Restrictions + +Tests are restricted to: +- SQL Server 2025+ (`MinMajor = 17`) +- Standalone engine type only (not Managed Instance until Bug:4816977 is resolved) + +## 7. Documentation References + +- [CREATE EVENT SESSION (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/statements/create-event-session-transact-sql) +- [ALTER EVENT SESSION (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/statements/alter-event-session-transact-sql) +- [sys.server_event_sessions](https://learn.microsoft.com/sql/relational-databases/system-catalog-views/sys-server-event-sessions-transact-sql) +- [Extended Events Overview](https://learn.microsoft.com/sql/relational-databases/extended-events/extended-events) diff --git a/specs/0005-ag-cluster-connection-options.md b/specs/0005-ag-cluster-connection-options.md new file mode 100644 index 00000000..aea34e85 --- /dev/null +++ b/specs/0005-ag-cluster-connection-options.md @@ -0,0 +1,165 @@ +# Spec 0005: Availability Group ClusterConnectionOptions for TDS 8.0 + +**Status:** Implemented +**Created:** 2025-11-13 +**Implemented:** Commit 847f3027f633c4ba10423c666cf2ead419a9a946 +**Authors:** Dawei Wang + +## 1. Summary + +Add support for the `CLUSTER_CONNECTION_OPTIONS` property on Availability Groups. This property specifies ODBC connection string options used by Windows Server Failover Clustering (WSFC) to connect to SQL Server, enabling TDS 8.0 secure connections between the cluster and SQL Server instances. + +## 2. Motivation + +SQL Server 2025 introduces TDS 8.0, which provides enhanced security features including: + +- **Strict encryption:** TLS 1.3 support with mandatory certificate validation +- **Certificate-based authentication:** Modern authentication patterns for cluster connectivity + +For WSFC-based Availability Groups, the cluster nodes need to communicate with SQL Server using ODBC. The `ClusterConnectionOptions` property allows administrators to configure these connections with TDS 8.0 settings like `Encrypt=Strict` and `HostNameInCertificate`. + +## 3. Target SQL Server Versions + +| Version | Support | +|---------|---------| +| SQL Server 2025 (v17.x) | ✅ Fully supported | +| SQL Server 2022 and earlier | ❌ Not applicable | + +**Cluster Type Requirement:** WSFC only (not applicable to Linux clusters or NONE cluster type) + +**Catalog View:** `sys.availability_groups` +**Relevant Column:** `cluster_connection_options` (nvarchar) + +## 4. DDL Syntax + +### 4.1 CREATE AVAILABILITY GROUP + +```sql +CREATE AVAILABILITY GROUP [ag_name] +WITH ( + AUTOMATED_BACKUP_PREFERENCE = SECONDARY, + CLUSTER_TYPE = WSFC, + CLUSTER_CONNECTION_OPTIONS = N'Encrypt=Strict;HostNameInCertificate=server.domain.com;' +) +FOR DATABASE [db1], [db2] +REPLICA ON ... +GO +``` + +### 4.2 ALTER AVAILABILITY GROUP + +```sql +ALTER AVAILABILITY GROUP [ag_name] +SET (CLUSTER_CONNECTION_OPTIONS = N'Encrypt=Strict;HostNameInCertificate=server.domain.com;') +GO +``` + +### 4.3 Common Connection Options + +| Option | Description | Example | +|--------|-------------|---------| +| `Encrypt` | Encryption mode | `Strict`, `Mandatory`, `Optional` | +| `HostNameInCertificate` | Expected hostname in server certificate | `server.domain.com` | +| `TrustServerCertificate` | Whether to trust self-signed certificates (not recommended for TDS 8.0) | `Yes`, `No` | + +## 5. SMO Implementation + +### 5.1 Properties + +**AvailabilityGroup Class:** + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `ClusterConnectionOptions` | `string` | `""` (empty) | ODBC connection string options for WSFC connectivity | + +**AvailabilityGroupData Class (HadrData):** + +| Property | Type | Description | +|----------|------|-------------| +| `ClusterConnectionOptions` | `string` | Data transfer object property for wizard/task flows | + +### 5.2 Helper Method + +The `AvailabilityGroup` class includes a helper method for appending key/value pairs: + +```csharp +public void SetClusterConnectionOptions(string key, string value) +``` + +This appends options in the format `key=value;` to the existing string. + +### 5.3 Scripting Behavior + +| Value | Script Action | +|-------|---------------| +| Empty/null | Property omitted from script | +| Non-empty | `CLUSTER_CONNECTION_OPTIONS = N''` included | + +### 5.4 Code Locations + +- **SMO Object:** [AvailabilityGroup.cs](../src/Microsoft/SqlServer/Management/Smo/AvailabilityGroup.cs) +- **Data Object:** [AvailabilityGroupData.cs](../src/Microsoft/SqlServer/Management/HadrData/AvailabilityGroupData.cs) +- **Task:** [CreateAvailabilityGroupTask.cs](../src/Microsoft/SqlServer/Management/HadrModel/CreateAvailabilityGroupTask.cs) +- **XML Metadata:** [AvailabilityGroup.xml](../src/Microsoft/SqlServer/Management/SqlEnum/xml/AvailabilityGroup.xml) + +### 5.5 XML Metadata + +```xml + + + ISNULL(AG.cluster_connection_options, N'') + + +``` + +### 5.6 cfg.xml Entry + +```xml + +``` + +## 6. Version Guarding + +The implementation uses `IsSupportedProperty` to check for SQL Server 2025+: + +```csharp +if (availabilityGroup.IsSupportedProperty(nameof(availabilityGroup.ClusterConnectionOptions))) +{ + availabilityGroup.ClusterConnectionOptions = this.availabilityGroupData.ClusterConnectionOptions; +} +``` + +This ensures the property is only set when connected to a supported SQL Server version. + +## 7. Testing + +### 7.1 Functional Tests + +Test scenarios: +1. **Default (not set):** Verify `ClusterConnectionOptions` is empty when not specified +2. **Empty string:** Verify setting to empty string and ALTER works correctly +3. **Non-empty value:** Verify appended key/value pairs with trailing semicolon + +**Test Location:** [HadrTests.cs](../src/FunctionalTest/SmoInternal/HighAvailability/HadrTests.cs) (SmoInternal - requires multi-server WSFC environment) + +### 7.2 Test Restrictions + +- Windows only (`[UnsupportedHostPlatform(SqlHostPlatforms.Linux)]`) +- SQL Server 2025+ (`MinMajor = 17`) +- WSFC cluster type only + +## 8. Platform Considerations + +| Platform | Support | +|----------|---------| +| Windows + WSFC | ✅ Full support | +| Linux | ❌ Not applicable (no WSFC) | +| Azure SQL MI | ❌ Not applicable (managed clustering) | + +## 9. Documentation References + +- [CREATE AVAILABILITY GROUP (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/statements/create-availability-group-transact-sql) +- [ALTER AVAILABILITY GROUP (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/statements/alter-availability-group-transact-sql) +- [sys.availability_groups](https://learn.microsoft.com/sql/relational-databases/system-catalog-views/sys-availability-groups-transact-sql) +- [TDS 8.0 and TLS 1.3 support](https://learn.microsoft.com/sql/relational-databases/security/networking/tds-8) +- [Always On Availability Groups Overview](https://learn.microsoft.com/sql/database-engine/availability-groups/windows/overview-of-always-on-availability-groups-sql-server) diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 00000000..8a58a0a1 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,22 @@ +# SMO Design Specifications + +This folder contains design specifications for significant features and architectural changes to SQL Management Objects. + +Specifications document **what** and **why** — not implementation sequencing. Implementation plans are created separately after a spec is finalized. + +## Index + +| Spec | Title | Status | +|------|-------|--------| +| [0001-async-interfaces.md](0001-async-interfaces.md) | Async Interfaces for SMO | Draft | +| [0002-async-scripter.md](0002-async-scripter.md) | Async Scripter (`ScriptAsync`) | Placeholder | +| [0003-workload-group-tempdb-rg.md](0003-workload-group-tempdb-rg.md) | Workload Group TempDB Resource Governance in CREATE Scripts | Implemented | +| [0004-xevent-max-duration.md](0004-xevent-max-duration.md) | XEvent Session MAX_DURATION Property | Implemented | +| [0005-ag-cluster-connection-options.md](0005-ag-cluster-connection-options.md) | Availability Group ClusterConnectionOptions for TDS 8.0 | Implemented | + +## Conventions + +- Specs are numbered sequentially: `NNNN-short-title.md`. +- Status values: **Placeholder** → **Draft** → **Review** → **Accepted** → **Implemented** → **Superseded**. +- Each spec should be self-contained with enough context for a reader unfamiliar with the prior discussion. +- Specs should clearly document dependencies between components to support implementation planning. diff --git a/src/Codegen/CodeGen.cs b/src/Codegen/CodeGen.cs index 895c60f2..4de88f80 100644 --- a/src/Codegen/CodeGen.cs +++ b/src/Codegen/CodeGen.cs @@ -178,6 +178,7 @@ private enum SingletonSupportedVersionFlags v15_0 = 512, v16_0 = 1024, v17_0 = 2048, + v18_0 = 4096, } private static KeyValuePair[] m_SingletonSupportedVersion = @@ -200,6 +201,7 @@ private enum SingletonSupportedVersionFlags new KeyValuePair(new ServerVersion(15,0,ushort.MaxValue), (int)SingletonSupportedVersionFlags.v15_0), new KeyValuePair(new ServerVersion(16,0,ushort.MaxValue), (int)SingletonSupportedVersionFlags.v16_0), new KeyValuePair(new ServerVersion(17,0,ushort.MaxValue), (int)SingletonSupportedVersionFlags.v17_0), + new KeyValuePair(new ServerVersion(18,0,ushort.MaxValue), (int)SingletonSupportedVersionFlags.v18_0), //VBUMP }; @@ -1148,6 +1150,7 @@ static void GeneratePropNameToIDLookup(CodeWriter f, bool bWithVersion) int v15 = 0; int v16 = 0; int v17 = 0; + int v18 = 0; int cv10 = 0; int cv11 = 0; int cv12 = 0; @@ -1458,6 +1461,29 @@ static void GeneratePropNameToIDLookup(CodeWriter f, bool bWithVersion) } } v17 = id; + + foreach (ObjectPropertyEx op in listSingletonProperties.Values) + { + int i = (int)listSingletonPropertiesVersion[op.Name]; + if ((i & (int)SingletonSupportedVersionFlags.v7_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v8_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v9_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v10_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v10_50) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v11_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v12_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v13_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v14_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v15_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v16_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v17_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v18_0) == (int)SingletonSupportedVersionFlags.v18_0) + { + op.index = id; + f.WriteCodeLine($"case \"{op.Name}\": return {id++};"); + } + } + v18 = id; // VBUMP f.DecrementIndent(); @@ -1474,7 +1500,7 @@ static void GeneratePropNameToIDLookup(CodeWriter f, bool bWithVersion) if (bWithVersion) { // VBUMP - f.WriteCodeLine("static int [] versionCount = new int [] {{ {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11} }};", v7, v8, v9, v10, v10_50, v11, v12, v13, v14, v15, v16, v17); + f.WriteCodeLine("static int [] versionCount = new int [] {{ {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12} }};", v7, v8, v9, v10, v10_50, v11, v12, v13, v14, v15, v16, v17, v18); f.WriteCodeLine("static int [] cloudVersionCount = new int [] {{ {0}, {1}, {2} }};", cv10, cv11, cv12); f.WriteCodeLine("static int sqlDwPropertyCount = {0};", datawarehousePropertyCount); f.WriteCodeLine("public override int Count"); @@ -1554,7 +1580,7 @@ static void GeneratePropNameToIDLookup(CodeWriter f, bool bWithVersion) f.WriteCodeLine("public override int Count"); f.IncrementIndent(); // VBUMP - f.WriteCodeLine("get {{ return {0}; }}", v17); + f.WriteCodeLine("get {{ return {0}; }}", v18); f.DecrementIndent(); } @@ -1853,6 +1879,27 @@ static void GenerateMetadataTable(CodeWriter f, bool bWithVersion) { f.WriteCodeLine($"new StaticMetadata(\"{op.Name}\", {op.Expensive.ToString().ToLower(CultureInfo.InvariantCulture)}, {op.ReadOnly.ToString().ToLower(CultureInfo.InvariantCulture)}, typeof({op.Type})),"); } + } + + foreach (ObjectProperty op in listSingletonProperties.Values) + { + int i = (int)listSingletonPropertiesVersion[op.Name]; + if ((i & (int)SingletonSupportedVersionFlags.v7_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v8_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v9_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v10_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v10_50) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v11_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v12_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v13_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v14_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v15_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v16_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v17_0) == (int)SingletonSupportedVersionFlags.NOT_SET + && (i & (int)SingletonSupportedVersionFlags.v18_0) == (int)SingletonSupportedVersionFlags.v18_0) + { + f.WriteCodeLine($"new StaticMetadata(\"{op.Name}\", {op.Expensive.ToString().ToLower(CultureInfo.InvariantCulture)}, {op.ReadOnly.ToString().ToLower(CultureInfo.InvariantCulture)}, typeof({op.Type})),"); + } // VBUMP } diff --git a/src/Codegen/cfg.xml b/src/Codegen/cfg.xml index b1881f7d..71957a12 100644 --- a/src/Codegen/cfg.xml +++ b/src/Codegen/cfg.xml @@ -304,6 +304,7 @@ + diff --git a/src/FunctionalTest/Directory.Build.props b/src/FunctionalTest/Directory.Build.props index ad40efe0..7bf0db78 100644 --- a/src/FunctionalTest/Directory.Build.props +++ b/src/FunctionalTest/Directory.Build.props @@ -8,6 +8,8 @@ $(NetfxVersion);net8.0 $(NetfxVersion) + + Major false diff --git a/src/FunctionalTest/Framework/Helpers/ServerObjectHelpers.cs b/src/FunctionalTest/Framework/Helpers/ServerObjectHelpers.cs index bb4fb0ef..21fcf1f0 100644 --- a/src/FunctionalTest/Framework/Helpers/ServerObjectHelpers.cs +++ b/src/FunctionalTest/Framework/Helpers/ServerObjectHelpers.cs @@ -15,6 +15,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Test.Manageability.Utils.Helpers; @@ -125,7 +126,7 @@ private static Database RestoreDatabaseFromPackageFile(SMO.Server server, string // Construct the sqlpackage.exe command var action = dbBackupFile.EndsWith(".bacpac", StringComparison.OrdinalIgnoreCase) ? "Import" : "Publish"; var connStr = new SqlConnectionStringBuilder(server.ConnectionContext.ConnectionString) { InitialCatalog = dbName }; - var sqlPackagePath = "sqlpackage.exe"; // Ensure sqlpackage.exe is in the PATH + var sqlPackagePath = "sqlpackage"; var isUrl = dbBackupFile.StartsWith("http", StringComparison.OrdinalIgnoreCase); if (isUrl) { @@ -143,30 +144,73 @@ private static Database RestoreDatabaseFromPackageFile(SMO.Server server, string $"/TargetConnectionString:\"{connStr.ConnectionString}\"" }); - // Execute the command with the response file + // The sqlpackage native apphost shim (installed as a dotnet global tool) + // requires hostfxr.dll at the exact major version it was built for. + // On build agents that only have .NET 10 runtime, the shim fails with + // 0xC0000135 (STATUS_DLL_NOT_FOUND) because hostfxr 8.x isn't present. + // To work around this, resolve the actual sqlpackage.dll from the tool + // store (preferring the TFM matching the installed runtime) and launch + // it via "dotnet exec --roll-forward Major". + string fileName; + string arguments; + var toolDll = ResolveDotnetToolDll("microsoft.sqlpackage", "sqlpackage.dll"); + if (toolDll != null) + { + TraceHelper.TraceInformation($"Launching sqlpackage via dotnet exec: {toolDll}"); + fileName = GetDotnetPath(); + arguments = $"exec --roll-forward Major \"{toolDll}\" @\"{responseFilePath}\""; + } + else + { + TraceHelper.TraceInformation("Could not resolve sqlpackage.dll from tool store; falling back to shim."); + fileName = sqlPackagePath; + arguments = $"@\"{responseFilePath}\""; + } + var processStartInfo = new ProcessStartInfo { - FileName = sqlPackagePath, - Arguments = $"@\"{responseFilePath}\"", - RedirectStandardOutput = false, + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + // Diagnostic logging to help investigate launch failures. + var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); + LogSqlPackageDiagnostics(sqlPackagePath, dotnetRoot); + using (var process = Process.Start(processStartInfo)) { if (process == null) { - throw new InvalidOperationException("Failed to start sqlpackage.exe process."); + throw new InvalidOperationException("Failed to start sqlpackage process."); } - // For some reason, trying to read StandardOut from sqlpackage causes it to hang when there's an error. + // Collect stdout asynchronously via the event-based API while + // reading stderr synchronously. Reading both streams with + // ReadToEnd() sequentially can deadlock when one pipe buffer + // fills and the child process blocks waiting for it to drain. + var stdout = new StringBuilder(); + process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) + { + stdout.AppendLine(e.Data); + } + }; + process.BeginOutputReadLine(); var error = process.StandardError.ReadToEnd(); process.WaitForExit(); + if (stdout.Length > 0) + { + TraceHelper.TraceInformation($"sqlpackage stdout:{Environment.NewLine}{stdout}"); + } + if (process.ExitCode != 0) { - Trace.TraceError($"*** sqlpackage.exe failed with exit code {process.ExitCode}.{Environment.NewLine}\t{error}"); + Trace.TraceError($"*** sqlpackage failed with exit code {process.ExitCode}.{Environment.NewLine}\t{error}"); throw new InvalidOperationException($"Failed to restore database from package file '{dbBackupFile}': {error}"); } } @@ -184,6 +228,192 @@ private static Database RestoreDatabaseFromPackageFile(SMO.Server server, string return server.Databases[dbName]; } + /// + /// Finds the full path to the dotnet executable by searching PATH. + /// + private static string GetDotnetPath() + { + var dotnetExe = Environment.OSVersion.Platform == PlatformID.Win32NT ? "dotnet.exe" : "dotnet"; + var pathVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + foreach (var dir in pathVar.Split(Path.PathSeparator)) + { + var candidate = Path.Combine(dir, dotnetExe); + if (File.Exists(candidate)) + { + return candidate; + } + } + return dotnetExe; + } + + /// + /// Resolves the managed entry-point DLL for a dotnet global tool by + /// searching the ~/.dotnet/tools/.store directory. This allows + /// launching the tool via dotnet exec instead of the native apphost + /// shim, avoiding 0xC0000135 failures when the required hostfxr major + /// version is not installed. + /// + /// Lowercase NuGet package ID (e.g. "microsoft.sqlpackage"). + /// Entry-point DLL filename (e.g. "sqlpackage.dll"). + /// Full path to the DLL, or null if it cannot be found. + private static string ResolveDotnetToolDll(string toolPackageId, string dllName) + { + try + { + var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var storeRoot = Path.Combine(userProfile, ".dotnet", "tools", ".store", toolPackageId); + if (!Directory.Exists(storeRoot)) + { + TraceHelper.TraceInformation($"Tool store not found: {storeRoot}"); + return null; + } + + // Prefer the highest installed version of the tool. + var versionDirs = Directory.GetDirectories(storeRoot); + Array.Sort(versionDirs); + Array.Reverse(versionDirs); + + // Prefer TFMs in descending order so the runtime that's actually + // installed gets matched first (e.g. net10.0 before net8.0). + var preferredTfms = new[] { "net10.0", "net9.0", "net8.0", "net6.0" }; + + foreach (var versionDir in versionDirs) + { + // Layout: .store/////tools//any/ + var innerRoot = Path.Combine(versionDir, toolPackageId); + if (!Directory.Exists(innerRoot)) + { + continue; + } + foreach (var innerVerDir in Directory.GetDirectories(innerRoot)) + { + var toolsDir = Path.Combine(innerVerDir, "tools"); + if (!Directory.Exists(toolsDir)) + { + continue; + } + + // First pass: check preferred TFMs in order. + foreach (var tfm in preferredTfms) + { + var candidate = Path.Combine(toolsDir, tfm, "any", dllName); + if (File.Exists(candidate)) + { + return candidate; + } + } + + // Second pass: any TFM directory that contains the DLL. + foreach (var tfmDir in Directory.GetDirectories(toolsDir)) + { + var candidate = Path.Combine(tfmDir, "any", dllName); + if (File.Exists(candidate)) + { + return candidate; + } + candidate = Path.Combine(tfmDir, dllName); + if (File.Exists(candidate)) + { + return candidate; + } + } + } + } + + TraceHelper.TraceInformation($"Could not find {dllName} in tool store: {storeRoot}"); + return null; + } + catch (Exception ex) + { + Trace.TraceWarning($"Error searching for dotnet tool DLL: {ex.Message}"); + return null; + } + } + + /// + /// Logs diagnostic information about sqlpackage and its .NET runtime + /// environment to help investigate 0xC0000135 (STATUS_DLL_NOT_FOUND) failures. + /// + private static void LogSqlPackageDiagnostics(string sqlPackagePath, string dotnetRoot) + { + try + { + // Resolve the full path that the OS will use to launch the process. + var resolvedPath = ResolveExecutablePath(sqlPackagePath); + TraceHelper.TraceInformation($"sqlpackage resolved path: {resolvedPath ?? "(not found)"}"); + + if (resolvedPath != null) + { + var sqlPackageDir = Path.GetDirectoryName(resolvedPath); + // Check for runtimeconfig.json — it tells the apphost which framework to load. + var runtimeConfig = Path.Combine(sqlPackageDir, "sqlpackage.runtimeconfig.json"); + if (File.Exists(runtimeConfig)) + { + TraceHelper.TraceInformation($"sqlpackage.runtimeconfig.json:{Environment.NewLine}{File.ReadAllText(runtimeConfig)}"); + } + else + { + TraceHelper.TraceInformation("sqlpackage.runtimeconfig.json not found next to executable."); + } + + // List DLLs beside sqlpackage to check if it's self-contained vs framework-dependent. + var dllCount = Directory.GetFiles(sqlPackageDir, "*.dll").Length; + TraceHelper.TraceInformation($"sqlpackage directory contains {dllCount} DLL(s): {sqlPackageDir}"); + } + + // Log DOTNET_ROOT and check for hostfxr.dll in expected locations. + TraceHelper.TraceInformation($"DOTNET_ROOT={dotnetRoot ?? "(not set)"}"); + if (!string.IsNullOrEmpty(dotnetRoot)) + { + var hostFxrDir = Path.Combine(dotnetRoot, "host", "fxr"); + if (Directory.Exists(hostFxrDir)) + { + var versions = Directory.GetDirectories(hostFxrDir); + TraceHelper.TraceInformation($"hostfxr versions at {hostFxrDir}: {string.Join(", ", versions.Select(Path.GetFileName))}"); + } + else + { + TraceHelper.TraceInformation($"hostfxr directory not found: {hostFxrDir}"); + } + } + + // Log the dotnet executable location and PATH for completeness. + var dotnetPath = GetDotnetPath(); + TraceHelper.TraceInformation($"dotnet executable: {dotnetPath}"); + TraceHelper.TraceInformation($"PATH={Environment.GetEnvironmentVariable("PATH")}"); + } + catch (Exception ex) + { + Trace.TraceWarning($"Failed to collect sqlpackage diagnostics: {ex.Message}"); + } + } + + /// + /// Resolves the full path to an executable by checking if it exists as-is, + /// then searching PATH with common Windows extensions. + /// + private static string ResolveExecutablePath(string fileName) + { + if (Path.IsPathRooted(fileName) && File.Exists(fileName)) + { + return Path.GetFullPath(fileName); + } + var extensions = new[] { "", ".exe", ".cmd", ".bat" }; + var pathVar = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + foreach (var dir in pathVar.Split(Path.PathSeparator)) + { + foreach (var ext in extensions) + { + var candidate = Path.Combine(dir, fileName + ext); + if (File.Exists(candidate)) + { + return Path.GetFullPath(candidate); + } + } + } + return null; + } + /// /// Check if certain database specified by databaseName exists on the server /// diff --git a/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj b/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj index afe98bf4..a72e6960 100644 --- a/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj +++ b/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj @@ -25,6 +25,7 @@ + diff --git a/src/FunctionalTest/Smo/GeneralFunctionality/DatabaseSmoTests.cs b/src/FunctionalTest/Smo/GeneralFunctionality/DatabaseSmoTests.cs index c92dbe28..a87d56fd 100644 --- a/src/FunctionalTest/Smo/GeneralFunctionality/DatabaseSmoTests.cs +++ b/src/FunctionalTest/Smo/GeneralFunctionality/DatabaseSmoTests.cs @@ -1082,6 +1082,43 @@ public void Database_Alter_toggles_optimized_locking() }); } + [TestMethod] + [SupportedServerVersionRange(Edition = DatabaseEngineEdition.Enterprise, MinMajor = 17)] + public void Database_Alter_toggles_automatic_index_compaction_not_supported() + { + ExecuteFromDbPool(db => + { + db.AutomaticIndexCompactionEnabled = true; + db.Alter(); + db.Parent.Databases.ClearAndInitialize(string.Format("[@Name='{0}']", Urn.EscapeString(db.Name)), new string[] { nameof(Database.AutomaticIndexCompactionEnabled) }); + db = db.Parent.Databases[db.Name]; + db.Refresh(); + Assert.That(db.AutomaticIndexCompactionEnabled, Is.False, "AutomaticIndexCompactionEnabled not set to true by Alter since we do not generate script for AIC"); + }); + } + + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.SqlAzureDatabase, MinMajor = 12)] + [SupportedServerVersionRange(Edition = DatabaseEngineEdition.SqlManagedInstance)] + public void Database_Alter_toggles_automatic_index_compaction() + { + ExecuteFromDbPool(db => + { + db.AutomaticIndexCompactionEnabled = true; + db.Alter(); + db.Parent.Databases.ClearAndInitialize(string.Format("[@Name='{0}']", Urn.EscapeString(db.Name)), new string[] { nameof(Database.AutomaticIndexCompactionEnabled) }); + db = db.Parent.Databases[db.Name]; + db.Refresh(); + Assert.That(db.AutomaticIndexCompactionEnabled, Is.True, "AutomaticIndexCompactionEnabled set to true by Alter"); + db.AutomaticIndexCompactionEnabled = false; + db.Alter(); + db.Parent.Databases.ClearAndInitialize(string.Format("[@Name='{0}']", Urn.EscapeString(db.Name)), new string[] { nameof(Database.AutomaticIndexCompactionEnabled) }); + db = db.Parent.Databases[db.Name]; + db.Refresh(); + Assert.That(db.AutomaticIndexCompactionEnabled, Is.False, "AutomaticIndexCompactionEnabled set to false by Alter"); + }); + } + /// /// If at any point we attempt to run an alter command that would result in AcceleratedDatabaseRecovery (ADR) /// being disabled while OptimizedLocking (OL) is enabled, we will get an error, as OL cannot be enabled without ADR. diff --git a/src/FunctionalTest/Smo/ScriptingTests/Job_SmoTestSuite.cs b/src/FunctionalTest/Smo/ScriptingTests/Job_SmoTestSuite.cs index 90c6ca5c..1ae96d11 100644 --- a/src/FunctionalTest/Smo/ScriptingTests/Job_SmoTestSuite.cs +++ b/src/FunctionalTest/Smo/ScriptingTests/Job_SmoTestSuite.cs @@ -7,11 +7,14 @@ #endif using System; +using System.Collections.Generic; using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo.Agent; using Microsoft.SqlServer.Test.Manageability.Utils.TestFramework; using Microsoft.VisualStudio.TestTools.UnitTesting; using _SMO = Microsoft.SqlServer.Management.Smo; +using NUnit.Framework; using Assert = NUnit.Framework.Assert; namespace Microsoft.SqlServer.Test.SMO.ScriptingTests @@ -70,6 +73,79 @@ public void SmoDropIfExists_Job_Sql16AndAfterOnPrem() }); } + /// + /// Tests that scripting multiple jobs with AgentJobId=false uses @job_name in sp_delete_job + /// and @job_id in sp_add_jobstep. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + [UnsupportedDatabaseEngineEdition(DatabaseEngineEdition.Express, DatabaseEngineEdition.SqlOnDemand)] + public void SmoScripter_AgentJobIdFalse_EmitsJobNameInDeleteJob() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + var server = database.Parent; + var jobs = new System.Collections.Generic.List<_SMO.Agent.Job>(); + + try + { + // Create two test jobs, each with one step + for (int i = 0; i < 2; i++) + { + var job = new _SMO.Agent.Job(server.JobServer, + GenerateUniqueSmoObjectName("job")); + job.Create(); + + var step = new _SMO.Agent.JobStep(job, "Step1"); + step.Command = "SELECT 1"; + step.SubSystem = _SMO.Agent.AgentSubSystem.TransactSql; + step.Create(); + + jobs.Add(job); + } + + // Configure the scripter + var scripter = new _SMO.Scripter(server); + scripter.Options.ScriptDrops = false; + scripter.Options.WithDependencies = false; + scripter.Options.IncludeHeaders = true; + scripter.Options.AppendToFile = true; + scripter.Options.AgentJobId = false; + + // Test each job + foreach (var job in jobs) + { + // Script with ScriptDrops = true and verify sp_delete_job uses @job_name + scripter.Options.ScriptDrops = true; + var deleteScripts = scripter.Script(new Urn[] { job.Urn }); + + Assert.That(deleteScripts, Has.Some.Matches(line => + line.Contains("msdb.dbo.sp_delete_job") && line.Contains($"@job_name=N'{job.Name}'")), + $"Expected sp_delete_job with @job_name=N'{job.Name}' in delete scripts"); + + // Script with ScriptDrops = false and verify sp_add_jobstep uses @job_id + scripter.Options.ScriptDrops = false; + var createScripts = scripter.Script(new Urn[] { job.Urn }); + + Assert.That(createScripts, Has.Some.Matches(line => + line.Contains("msdb.dbo.sp_add_jobstep") && line.Contains("@job_id=@jobId")), + "Expected sp_add_jobstep with @job_id=@jobId in create scripts"); + } + } + finally + { + // Cleanup: drop all test jobs + foreach (var job in jobs) + { + if (job.State == _SMO.SqlSmoState.Existing) + { + job.Drop(); + } + } + } + }); + } + #endregion // Scripting Tests // TODO: Fix collection construction to ensure StringComparer initialization at the Parent and Server levels diff --git a/src/FunctionalTest/Smo/ScriptingTests/SmoTestFramework/SmoTestBase.cs b/src/FunctionalTest/Smo/ScriptingTests/SmoTestFramework/SmoTestBase.cs index 82e1579b..192465f8 100644 --- a/src/FunctionalTest/Smo/ScriptingTests/SmoTestFramework/SmoTestBase.cs +++ b/src/FunctionalTest/Smo/ScriptingTests/SmoTestFramework/SmoTestBase.cs @@ -117,6 +117,17 @@ public static IEnumerable> TestServerScriptingOp OptimizerData = true, Permissions = true }); + yield return new Tuple( + "Sqlv180", + new ScriptingOptions() + { + ExtendedProperties = true, + TargetDatabaseEngineType = DatabaseEngineType.Standalone, + TargetServerVersion = SqlServerVersion.Version180, + IncludeScriptingParametersHeader = true, + OptimizerData = true, + Permissions = true + }); // VBUMP : Add new server versions here yield return new Tuple( "AzureSterlingV12", diff --git a/src/FunctionalTest/Smo/ScriptingTests/StoredProcedure_SmoTestSuite.cs b/src/FunctionalTest/Smo/ScriptingTests/StoredProcedure_SmoTestSuite.cs index 32f0f8e3..54c3614e 100644 --- a/src/FunctionalTest/Smo/ScriptingTests/StoredProcedure_SmoTestSuite.cs +++ b/src/FunctionalTest/Smo/ScriptingTests/StoredProcedure_SmoTestSuite.cs @@ -315,6 +315,83 @@ private void VerifyStoredProcedureParameterCount(_SMO.Database database) } } } + + /// + /// Tests that scripting a stored procedure with CR newline in comment preserves the TextBody after round-trip. + /// Regression test for DDL text parser handling different newline characters. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + public void SmoStoredProcedure_ScriptWithCRNewlineInComment() + { + VerifyScriptStoredProcedureWithNewline("\r"); + } + + /// + /// Tests that scripting a stored procedure with LF newline in comment preserves the TextBody after round-trip. + /// Regression test for DDL text parser handling different newline characters. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + public void SmoStoredProcedure_ScriptWithLFNewlineInComment() + { + VerifyScriptStoredProcedureWithNewline("\n"); + } + + /// + /// Tests that scripting a stored procedure with CRLF newline in comment preserves the TextBody after round-trip. + /// Regression test for DDL text parser handling different newline characters. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + public void SmoStoredProcedure_ScriptWithCRLFNewlineInComment() + { + VerifyScriptStoredProcedureWithNewline("\r\n"); + } + + /// + /// Helper method to verify that scripting a stored procedure with a specific newline character + /// in a comment header preserves the TextBody after script round-trip. + /// + /// The newline character(s) to test (e.g., "\r", "\n", "\r\n") + private void VerifyScriptStoredProcedureWithNewline(string newline) + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + string procName = GenerateUniqueSmoObjectName("proc"); + string textBody = "BEGIN SELECT 1 END"; + string createSql = string.Format("--Comment{0}CREATE PROCEDURE {1} AS\n{2}", newline, _SMO.SqlSmoObject.MakeSqlBraket(procName), textBody); + + // Create the proc via T-SQL (not SMO) to preserve exact newlines + database.ExecuteNonQuery(createSql); + + // Retrieve via SMO + database.StoredProcedures.Refresh(); + var proc = database.StoredProcedures[procName]; + Assert.IsNotNull(proc, "Stored procedure should exist"); + + // Script it with options to avoid database context + StringCollection scripts = proc.Script(new _SMO.ScriptingOptions { IncludeDatabaseContext = false }); + + // Drop and re-create from script + proc.Drop(); + foreach (string script in scripts) + { + database.ExecuteNonQuery(script); + } + + // Verify round-trip + database.StoredProcedures.Refresh(); + proc = database.StoredProcedures[procName]; + Assert.IsNotNull(proc, "Stored procedure should be re-created from script"); + proc.Refresh(); + Assert.That(proc.TextBody.Trim(), Is.EqualTo(textBody), + "TextBody should be preserved after script round-trip"); + + // Cleanup + proc.Drop(); + }); + } } #endregion -} +} \ No newline at end of file diff --git a/src/FunctionalTest/Smo/ScriptingTests/Table_SmoTestSuite.cs b/src/FunctionalTest/Smo/ScriptingTests/Table_SmoTestSuite.cs index 393d3cea..7dcb5bd4 100644 --- a/src/FunctionalTest/Smo/ScriptingTests/Table_SmoTestSuite.cs +++ b/src/FunctionalTest/Smo/ScriptingTests/Table_SmoTestSuite.cs @@ -281,6 +281,206 @@ public void SmoTableAlter_Sql2016AndAfterOnPrem() }); } + /// + /// Tests disabling change tracking on a table through SMO on SQL2016 and after onprem. + /// Regression test ensuring that change tracking can be disabled after being enabled. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + [UnsupportedFeature(SqlFeature.Fabric)] + public void SmoTableAlter_DisableChangeTracking() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Enable database-level change tracking (only if not already enabled) + if (!database.ChangeTrackingEnabled) + { + database.ChangeTrackingEnabled = true; + database.ChangeTrackingRetentionPeriod = 1; + database.ChangeTrackingRetentionPeriodUnits = _SMO.RetentionPeriodUnits.Hours; + database.ChangeTrackingAutoCleanUp = true; + database.Alter(); + } + + // Create a table with a primary key + var table = database.CreateTable(this.TestContext.TestName, new ColumnProperties("c1") { Nullable = false }); + table.CreateIndex(this.TestContext.TestName, new IndexProperties() { KeyType = _SMO.IndexKeyType.DriPrimaryKey }); + + // Enable change tracking on the table + table.ChangeTrackingEnabled = true; + table.Alter(); + table.Refresh(); + Assert.That(table.ChangeTrackingEnabled, Is.True, "Change tracking should be enabled after Alter()"); + + // Disable change tracking + table.ChangeTrackingEnabled = false; + table.Alter(); + table.Refresh(); + Assert.That(table.ChangeTrackingEnabled, Is.False, "Change tracking should be disabled after setting to false and calling Alter()"); + + // Verify via T-SQL that change tracking is disabled + var query = $"SELECT COUNT(*) FROM sys.change_tracking_tables WHERE object_id = OBJECT_ID({SqlSmoObject.MakeSqlString(table.FullQualifiedName)})"; + var result = database.ExecutionManager.ExecuteScalar(query); + Assert.That(result, Is.EqualTo(0), "sys.change_tracking_tables should not contain the table after disabling change tracking"); + }); + } + + /// + /// Tests that enabling change tracking on a table that already has it enabled throws FailedOperationException. + /// Regression test for ensuring proper error handling when re-enabling change tracking. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + [UnsupportedFeature(SqlFeature.Fabric)] + public void SmoTableAlter_EnableChangeTrackingOnAlreadyEnabledTable_Throws() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Enable database-level change tracking (only if not already enabled) + if (!database.ChangeTrackingEnabled) + { + database.ChangeTrackingEnabled = true; + database.ChangeTrackingRetentionPeriod = 1; + database.ChangeTrackingRetentionPeriodUnits = _SMO.RetentionPeriodUnits.Hours; + database.ChangeTrackingAutoCleanUp = true; + database.Alter(); + } + + // Create a table with a primary key + var table = database.CreateTable(this.TestContext.TestName, new ColumnProperties("c1") { Nullable = false }); + table.CreateIndex(this.TestContext.TestName, new IndexProperties() { KeyType = _SMO.IndexKeyType.DriPrimaryKey }); + + // Enable change tracking on the table + table.ChangeTrackingEnabled = true; + table.Alter(); + table.Refresh(); + + // Attempt to enable change tracking again (should throw) + table.ChangeTrackingEnabled = true; + Assert.Throws<_SMO.FailedOperationException>(() => table.Alter(), + "Expected FailedOperationException when re-enabling change tracking on an already-enabled table"); + }); + } + + /// + /// Tests that scripting a table with change tracking enabled produces a script that can be used to recreate the table + /// with change tracking still enabled (round-trip test). + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + [UnsupportedFeature(SqlFeature.Fabric)] + public void SmoTableScript_ChangeTrackingRoundTrip() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Enable database-level change tracking (only if not already enabled) + if (!database.ChangeTrackingEnabled) + { + database.ChangeTrackingEnabled = true; + database.ChangeTrackingRetentionPeriod = 1; + database.ChangeTrackingRetentionPeriodUnits = _SMO.RetentionPeriodUnits.Hours; + database.ChangeTrackingAutoCleanUp = true; + database.Alter(); + } + + // Create a table with a primary key + var table = database.CreateTable(this.TestContext.TestName, new ColumnProperties("c1") { Nullable = false }); + table.CreateIndex(this.TestContext.TestName, new IndexProperties() { KeyType = _SMO.IndexKeyType.DriPrimaryKey }); + + // Enable change tracking on the table + table.ChangeTrackingEnabled = true; + table.Alter(); + table.Refresh(); + + // Script the table with change tracking + var options = new _SMO.ScriptingOptions + { + ChangeTracking = true, + Indexes = true, + DriPrimaryKey = true, + IncludeDatabaseContext = false, + ScriptSchema = true + }; + StringCollection scripts = table.Script(options); + + // Verify the script contains CHANGE_TRACKING = ON + var scriptText = string.Join("\n", scripts.Cast()); + bool hasChangeTracking = scriptText.Contains("CHANGE_TRACKING = ON") || + scriptText.Contains("ENABLE CHANGE_TRACKING"); + Assert.IsTrue(hasChangeTracking, "Script should contain CHANGE_TRACKING = ON or ENABLE CHANGE_TRACKING"); + + // Capture table name before dropping (can't access properties after Drop) + var tableName = table.Name; + + // Drop the table + table.Drop(); + database.Tables.Refresh(); + Assert.IsNull(database.Tables[tableName, "dbo"], "Table should be dropped"); + + // Re-execute the scripts to recreate the table + foreach (string script in scripts) + { + try + { + database.ExecuteNonQuery(script); + } + catch (Exception ex) + { + Assert.Fail($"Failed to execute script on {database.Parent.Name}: {ex.Message}\nScript:\n{script}"); + } + } + + // Verify the re-created table has change tracking enabled + database.Tables.Refresh(); + var recreatedTable = database.Tables[tableName, "dbo"]; + Assert.IsNotNull(recreatedTable, "Table should be re-created from script"); + recreatedTable.Refresh(); + + // Verify via sys.change_tracking_tables + var query = $"SELECT COUNT(*) FROM sys.change_tracking_tables WHERE object_id = OBJECT_ID({SqlSmoObject.MakeSqlString(recreatedTable.FullQualifiedName)})"; + var result = database.ExecutionManager.ExecuteScalar(query); + Assert.That(result, Is.EqualTo(1), "sys.change_tracking_tables should contain the re-created table with change tracking enabled"); + }); + } + + /// + /// Tests that setting TrackColumnsUpdatedEnabled = true after table creation (when change tracking is already enabled) + /// throws FailedOperationException, because column tracking can only be set at table creation time. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + [UnsupportedFeature(SqlFeature.Fabric)] + public void SmoTableAlter_TrackColumnsUpdatedEnabled_ThrowsAfterCreation() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Enable database-level change tracking (only if not already enabled) + if (!database.ChangeTrackingEnabled) + { + database.ChangeTrackingEnabled = true; + database.ChangeTrackingRetentionPeriod = 1; + database.ChangeTrackingRetentionPeriodUnits = _SMO.RetentionPeriodUnits.Hours; + database.ChangeTrackingAutoCleanUp = true; + database.Alter(); + } + + // Create a table with a primary key + var table = database.CreateTable(this.TestContext.TestName, new ColumnProperties("c1") { Nullable = false }); + table.CreateIndex(this.TestContext.TestName, new IndexProperties() { KeyType = _SMO.IndexKeyType.DriPrimaryKey }); + + // Enable change tracking without column tracking + table.ChangeTrackingEnabled = true; + table.TrackColumnsUpdatedEnabled = false; + table.Alter(); + table.Refresh(); + + // Attempt to toggle column tracking on (should throw) + table.TrackColumnsUpdatedEnabled = true; + Assert.Throws<_SMO.FailedOperationException>(() => table.Alter(), + "Expected FailedOperationException when toggling TrackColumnsUpdatedEnabled after creation"); + }); + } + /// /// Verify that SMO object is dropped. /// Smo object. diff --git a/src/FunctionalTest/Smo/ScriptingTests/Trigger_SmoTestSuite.cs b/src/FunctionalTest/Smo/ScriptingTests/Trigger_SmoTestSuite.cs index 1421a38e..3946162e 100644 --- a/src/FunctionalTest/Smo/ScriptingTests/Trigger_SmoTestSuite.cs +++ b/src/FunctionalTest/Smo/ScriptingTests/Trigger_SmoTestSuite.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.Collections.Specialized; +using System.Linq; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo.Agent; using Microsoft.SqlServer.Test.Manageability.Utils; @@ -144,6 +146,151 @@ public void SmoCreateOrAlter_Trigger_Sql14AndBeforeOnPrem() Assert.That(e.Message, Does.Contain(string.Format("CreateOrAlter failed for Trigger '{0}'.", trigger.Name)), "Unexpected error message."); }); } + + /// + /// Tests that creating a DML trigger via SMO results in an enabled trigger. + /// Verifies the IsEnabled property reflects the correct state. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + public void SmoTrigger_CreateDmlTrigger_IsEnabled() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Create a table + var table = database.CreateTable(this.TestContext.TestName, new ColumnProperties("c1")); + + // Create a trigger using SMO + var trigger = new _SMO.Trigger(table, GenerateSmoObjectName("trg")); + trigger.TextMode = false; + trigger.Insert = true; + trigger.Update = true; + trigger.TextBody = "RAISERROR('Trigger fired', 1, 1)"; + trigger.ImplementationType = _SMO.ImplementationType.TransactSql; + trigger.Create(); + + // Refresh and verify + table.Triggers.Refresh(); + var createdTrigger = table.Triggers[trigger.Name]; + Assert.IsNotNull(createdTrigger, "Trigger should exist"); + Assert.That(createdTrigger.IsEnabled, Is.True, "Trigger should be enabled after creation"); + }); + } + + /// + /// Tests that scripting a disabled DML trigger at the trigger level produces a script with DISABLE TRIGGER. + /// Verifies that dropping and re-creating the trigger from the script preserves the disabled state. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + public void SmoTrigger_ScriptDisabledTrigger_RoundTrip() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Create a table with a column + var tableName = GenerateSmoObjectName("tbl"); + var table = database.CreateTable(tableName, new ColumnProperties("c1")); + + // Create a DML trigger via T-SQL + var triggerName = GenerateSmoObjectName("trg"); + database.ExecuteNonQuery( + $"CREATE TRIGGER {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(triggerName)} ON {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(table.Name)} AFTER INSERT AS BEGIN SET NOCOUNT ON; END"); + + // Disable the trigger via T-SQL + database.ExecuteNonQuery($"DISABLE TRIGGER {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(triggerName)} ON {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(table.Name)}"); + + // Retrieve via SMO and script at the trigger level + table.Triggers.Refresh(); + var smoTrigger = table.Triggers[triggerName]; + Assert.IsNotNull(smoTrigger, "Trigger should exist"); + Assert.That(smoTrigger.IsEnabled, Is.False, "Trigger should be disabled"); + + var scripts = smoTrigger.Script(new _SMO.ScriptingOptions { IncludeDatabaseContext = false }); + + // Verify script contains DISABLE TRIGGER + var scriptText = string.Join("\n", scripts.Cast()); + Assert.That(scriptText, Does.Contain("DISABLE TRIGGER"), + "Script should contain DISABLE TRIGGER statement"); + + // Drop the trigger + smoTrigger.Drop(); + table.Triggers.Refresh(); + Assert.IsNull(table.Triggers[triggerName], "Trigger should be dropped"); + + // Re-create from scripts + foreach (string script in scripts) + { + database.ExecuteNonQuery(script); + } + + // Verify the re-created trigger is disabled + table.Triggers.Refresh(); + var recreated = table.Triggers[triggerName]; + Assert.IsNotNull(recreated, "Trigger should be re-created from script"); + Assert.That(recreated.IsEnabled, Is.False, + "Trigger should be disabled after round-trip from script"); + }); + } + + /// + /// Tests that scripting a table with a disabled DML trigger (at the table level with Triggers=true) + /// produces a script with DISABLE TRIGGER. Verifies that dropping and re-creating the table from + /// the script preserves the disabled trigger state. + /// + [TestMethod] + [SupportedServerVersionRange(DatabaseEngineType = DatabaseEngineType.Standalone, MinMajor = 13)] + public void SmoTrigger_ScriptTableWithDisabledTrigger_RoundTrip() + { + ExecuteFromDbPool(this.TestContext.FullyQualifiedTestClassName, database => + { + // Create a table with a column + var tableName = GenerateSmoObjectName("tbl"); + var table = database.CreateTable(tableName, new ColumnProperties("c1")); + + // Create a DML trigger via T-SQL + var triggerName = GenerateSmoObjectName("trg"); + database.ExecuteNonQuery( + $"CREATE TRIGGER {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(triggerName)} ON {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(table.Name)} AFTER INSERT AS BEGIN SET NOCOUNT ON; END"); + + // Disable the trigger via T-SQL + database.ExecuteNonQuery($"DISABLE TRIGGER {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(triggerName)} ON {_SMO.SqlSmoObject.MakeSqlBraket("dbo")}.{_SMO.SqlSmoObject.MakeSqlBraket(table.Name)}"); + + // Retrieve via SMO and script at the table level with Triggers=true + database.Tables.Refresh(); + var smoTable = database.Tables[table.Name]; + Assert.IsNotNull(smoTable, "Table should exist"); + + var scripts = smoTable.Script(new _SMO.ScriptingOptions { Triggers = true, IncludeDatabaseContext = false }); + + // Verify script contains DISABLE TRIGGER + var scriptText = string.Join("\n", scripts.Cast()); + Assert.That(scriptText, Does.Contain("DISABLE TRIGGER"), + "Script should contain DISABLE TRIGGER statement when scripting table with disabled trigger"); + + // Drop the entire table + smoTable.Drop(); + database.Tables.Refresh(); + Assert.IsNull(database.Tables[table.Name], "Table should be dropped"); + + // Re-execute all scripts + foreach (string script in scripts) + { + database.ExecuteNonQuery(script); + } + + // Verify the re-created table and trigger + database.Tables.Refresh(); + var recreatedTable = database.Tables[table.Name]; + Assert.IsNotNull(recreatedTable, "Table should be re-created"); + + recreatedTable.Triggers.Refresh(); + var recreatedTrigger = recreatedTable.Triggers[triggerName]; + Assert.IsNotNull(recreatedTrigger, "Trigger should be re-created"); + Assert.That(recreatedTrigger.IsEnabled, Is.False, + "Trigger should be disabled when scripted at table level"); + }); + } + #endregion } } diff --git a/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs b/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs index f5990b85..1868e6dc 100644 --- a/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs +++ b/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs @@ -173,7 +173,7 @@ static public ServerInformation GetServerInformation(IDbConnection sqlConnection } var isFabricServer = Convert.ToBoolean(dataSet.Tables[0].Rows[0]["IsFabricServer"]); var connectionProtocol = dataSet.Tables[2].Rows[0]["ConnectionProtocol"]; - var collation = (string)dataSet.Tables[0].Rows[0]["Collation"]; + var collation = Convert.ToString(dataSet.Tables[0].Rows[0]["Collation"]); return new ServerInformation(serverVersion, new Version((string)dataSet.Tables[0].Rows[0]["ProductVersion"]), diff --git a/src/Microsoft/SqlServer/Management/Smo.Notebook/Microsoft.SqlServer.Smo.Notebook.csproj b/src/Microsoft/SqlServer/Management/Smo.Notebook/Microsoft.SqlServer.Smo.Notebook.csproj index 843b5a38..ad4d69f9 100644 --- a/src/Microsoft/SqlServer/Management/Smo.Notebook/Microsoft.SqlServer.Smo.Notebook.csproj +++ b/src/Microsoft/SqlServer/Management/Smo.Notebook/Microsoft.SqlServer.Smo.Notebook.csproj @@ -6,8 +6,9 @@ + - + diff --git a/src/Microsoft/SqlServer/Management/Smo.Notebook/NotebookScriptWriterRegistration.cs b/src/Microsoft/SqlServer/Management/Smo.Notebook/NotebookScriptWriterRegistration.cs new file mode 100644 index 00000000..b3e17712 --- /dev/null +++ b/src/Microsoft/SqlServer/Management/Smo.Notebook/NotebookScriptWriterRegistration.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using Microsoft.SqlServer.Management.SqlScriptPublish; + +namespace Microsoft.SqlServer.Management.Smo.Notebook +{ + /// + /// Provides a static method to register the Notebook ISmoScriptWriter implementation + /// with SqlScriptGenerator. Call at application startup before + /// using . + /// + public static class NotebookScriptWriterRegistration + { + /// + /// Registers the factory with + /// so that + /// ScriptDestination.ToNotebook uses the Notebook assembly's writer implementation. + /// + public static void Register() + { + SqlScriptGenerator.NotebookWriterFactory = (filePath) => + { + return new NotebookFileWriter(filePath); + }; + } + } +} diff --git a/src/Microsoft/SqlServer/Management/Smo/BrokerLocalizableResources.strings b/src/Microsoft/SqlServer/Management/Smo/BrokerLocalizableResources.strings index c3fe003e..913a7949 100644 --- a/src/Microsoft/SqlServer/Management/Smo/BrokerLocalizableResources.strings +++ b/src/Microsoft/SqlServer/Management/Smo/BrokerLocalizableResources.strings @@ -27,6 +27,12 @@ MessageType_ValidationXmlSchemaCollectionSchemaDesc=Name of the database schema MessageType_IsSystemObjectName=Is System Object MessageType_IsSystemObjectDesc=Gets the Boolean property that specifies whether the message type is a system object. + +MessageType_ExtendedPropertiesName=Extended Properties +MessageType_ExtendedPropertiesDesc=Gets the collection of extended properties for the message type. + +MessageType_ParentName=Parent +MessageType_ParentDesc=Gets the parent ServiceBroker object. #######End MessageType ######Begin ServiceContract @@ -42,12 +48,21 @@ ServiceContract_IDDesc=Gets the ID value that uniquely identifies the service co ServiceContract_IsSystemObjectName=Is System Object ServiceContract_IsSystemObjectDesc=Gets the Boolean property that specifies whether the message type is a system object. +ServiceContract_MessageTypeMappingsName=Message Type Mappings +ServiceContract_MessageTypeMappingsDesc=Gets the collection of message type mappings for this service contract. + +ServiceContract_ExtendedPropertiesName=Extended Properties +ServiceContract_ExtendedPropertiesDesc=Gets the collection of extended properties for the service contract. + ######End ServiceContract ######Begin ServiceQueue ServiceQueue_Name = Service Queue ServiceQueue_Desc = Name of the queue, which receives Service Broker messages. +ServiceQueue_NameName=Name +ServiceQueue_NameDesc=Gets or sets the name of the service queue. + ServiceQueue_IDName=ID ServiceQueue_IDDesc=Gets the ID value that uniquely identifies the service queue. @@ -93,9 +108,15 @@ ServiceQueue_ProcedureSchemaDesc= Name of the schema that contains the stored pr ServiceQueue_RowCountName= Message count ServiceQueue_RowCountDesc= The number of messages in the queue. +ServiceQueue_RowCountAsDoubleName=Row Count (Double) +ServiceQueue_RowCountAsDoubleDesc=Gets the number of messages in the queue as a double-precision floating point number. + ServiceQueue_IsSystemObjectName=Is System Object ServiceQueue_IsSystemObjectDesc=Gets the Boolean property that specifies whether the message type is a system object. +ServiceQueue_ExtendedPropertiesName=Extended Properties +ServiceQueue_ExtendedPropertiesDesc=Gets the collection of extended properties for the service queue. + ServiceQueue_SchemaName = Schema ServiceQueue_SchemaDesc = Gets or sets the schema.(inherited from ScriptSchemaObjectBase) ######EndServiceQueue @@ -119,6 +140,12 @@ BrokerService_QueueSchemaDesc = Name of the schema to which the queue belongs BrokerService_IsSystemObjectName=Is System Object BrokerService_IsSystemObjectDesc=Gets the Boolean property that specifies whether the message type is a system object. +BrokerService_ServiceContractMappingsName=Service Contract Mappings +BrokerService_ServiceContractMappingsDesc=Gets the collection of service contract mappings for this broker service. + +BrokerService_ExtendedPropertiesName=Extended Properties +BrokerService_ExtendedPropertiesDesc=Gets the collection of extended properties for the broker service. + ######End BrokerService ######Begin ServiceRoute diff --git a/src/Microsoft/SqlServer/Management/Smo/Collections/DatabaseCollection.cs b/src/Microsoft/SqlServer/Management/Smo/Collections/DatabaseCollection.cs index 69a33733..7d760517 100644 --- a/src/Microsoft/SqlServer/Management/Smo/Collections/DatabaseCollection.cs +++ b/src/Microsoft/SqlServer/Management/Smo/Collections/DatabaseCollection.cs @@ -14,11 +14,11 @@ internal override SqlSmoObject GetObjectByName(string name) { try { - return base.GetObjectByName(name); + return base.GetObjectByName(name); } - catch (Microsoft.SqlServer.Management.Common.ConnectionFailureException cfe) + catch (Microsoft.SqlServer.Management.Common.ConnectionFailureException cfe) when (cfe.InnerException is SqlException ex && ex.Number == 4060) - { + { Microsoft.SqlServer.Management.Diagnostics.TraceHelper.LogExCatch(cfe); // this exception occurs if the user doesn't have access to // the database with the input name diff --git a/src/Microsoft/SqlServer/Management/Smo/DatabaseBase.cs b/src/Microsoft/SqlServer/Management/Smo/DatabaseBase.cs index 8e052e6a..4dbdf848 100644 --- a/src/Microsoft/SqlServer/Management/Smo/DatabaseBase.cs +++ b/src/Microsoft/SqlServer/Management/Smo/DatabaseBase.cs @@ -1020,6 +1020,10 @@ private void AddCompatibilityLevel(StringCollection query, ScriptingPreferences if (null != propCompat.Value && (propCompat.Dirty || !sp.ScriptForAlter)) { // VBUMP + var isVersion180WithCompatLevelLessThan180 = + (sp.TargetServerVersion == SqlServerVersion.Version180) && + ((int)(CompatibilityLevel)propCompat.Value <= 180); + var isVersion170WithCompatLevelLessThan170 = (sp.TargetServerVersion == SqlServerVersion.Version170) && ((int)(CompatibilityLevel)propCompat.Value <= 170); @@ -1070,6 +1074,7 @@ private void AddCompatibilityLevel(StringCollection query, ScriptingPreferences isVersion150WithCompatLevelLessThan150 || isVersion160WithCompatLevelLessThan160 || isVersion170WithCompatLevelLessThan170 || + isVersion180WithCompatLevelLessThan180 || isTargetSqlAzureOrMIOrMIAA; // VBUMP @@ -6262,10 +6267,10 @@ private void ScriptDbOptionsProps(StringCollection query, ScriptingPreferences s } /// If at any point we attempt to run an alter command that would result in AcceleratedDatabaseRecovery (ADR) - /// being disabled while OptimizedLocking (OL) is enabled, we will get an error, as OL cannot be enabled without ADR. + /// being disabled while OptimizedLocking (OL) or AutomaticIndexCompaction (AIC) is enabled, we will get an error, as OL/AIC cannot be enabled without ADR. /// This means that when both features get enabled, ADR must be enabled first, whereas when both features - /// get disabled, OL must be disabled first. - /// So we check if ADR is being disabled and in that case we process OL first, otherwise we process ADR first. + /// get disabled, OL/AIC must be disabled first. + /// So we check if ADR is being disabled and in that case we process OL/AIC first, otherwise we process ADR first. /// Of course there are more scenarios than just enabling/disabling them both simoultaneously, but in those cases /// the order is not going to make a difference. For instance, if ADR gets disabled while OL gets enabled, that will /// still result in an error no matter the order, but that behavior would be expected. @@ -6274,14 +6279,17 @@ private void ScriptDbOptionsProps(StringCollection query, ScriptingPreferences s && ! (bool)Properties.Get(nameof(AcceleratedRecoveryEnabled)).Value) { ScriptAlterOptimizedLocking(sp, query, isAzureDb, targetEditionIsManagedServer); + ScriptAlterAutomaticIndexCompaction(sp, query); ScriptAlterAcceleratedDatabaseRecovery(sp, query, isAzureDb, targetEditionIsManagedServer); } else { ScriptAlterAcceleratedDatabaseRecovery(sp, query, isAzureDb, targetEditionIsManagedServer); ScriptAlterOptimizedLocking(sp, query, isAzureDb, targetEditionIsManagedServer); + ScriptAlterAutomaticIndexCompaction(sp, query); } + ScriptAlterPropBool(nameof(DataRetentionEnabled), "DATA_RETENTION", sp, query, false); } @@ -6325,6 +6333,14 @@ private void ScriptAlterOptimizedLocking(ScriptingPreferences sp, StringCollecti } } + private void ScriptAlterAutomaticIndexCompaction(ScriptingPreferences sp, StringCollection query) + { + if (IsSupportedProperty(nameof(AutomaticIndexCompactionEnabled), sp)) + { + ScriptAlterPropBool(nameof(AutomaticIndexCompactionEnabled), "AUTOMATIC_INDEX_COMPACTION", sp, query, true); + } + } + private void ScriptAlterFileStreamProp(ScriptingPreferences sp,StringCollection query ) { //FileStream Properties diff --git a/src/Microsoft/SqlServer/Management/Smo/ExceptionTemplatesImpl.strings b/src/Microsoft/SqlServer/Management/Smo/ExceptionTemplatesImpl.strings index d8d10a98..8e7565a2 100644 --- a/src/Microsoft/SqlServer/Management/Smo/ExceptionTemplatesImpl.strings +++ b/src/Microsoft/SqlServer/Management/Smo/ExceptionTemplatesImpl.strings @@ -68,39 +68,13 @@ EncryptedViewsFunctionsDownlevel(string view, string targetVersion) = {0} is an ; message for Schemas not supported in prior versions SchemaDownlevel(string objectName, string targetVersion) = Error with Schema {0}. Schemas are not supported in {1}. -; message for User Defined Aggregates not supported in prior versions -UserDefinedAggregatesDownlevel(string objectName, string targetVersion) = Error with User Defined Aggregate {0}. User Defined Aggregates are not supported in {1}. - -; message for XmlSchemaCollections not supported in prior versions -XmlSchemaCollectionDownlevel(string objectName, string targetVersion) = Error with Xml Schema Collection {0}. Xml Schema Collections are not supported in {1}. - -; message for Synonyms not supported in prior versions -SynonymDownlevel(string objectName, string targetVersion) = Error with Synonym {0}. Synonyms are not supported in {1}. - -; message for Sequences not supported in prior versions -SequenceDownlevel(string objectName, string targetVersion) = Error with Sequence {0}. Sequences are not supported in {1}. - -; message for Security Policies not supported in prior versions -SecurityPolicyDownlevel(string objectName, string targetVersion) = Error with Security Policy {0}. Security Policies are not supported in {1}. - -; message for External Data Sources not supported in prior versions -ExternalDataSourceDownlevel(string objectName, string targetVersion) = Error with External Data Source {0}. External Data Sources are not supported in {1}. ; message for column encryption keys not supported in prior versions ColumnEncryptionKeyDownlevel(string objectName, string targetVersion) = Error with Column Encryption Key {0}. Column Encryption Keys are not supported in {1}. -; message for External File Formats not supported in prior versions -ExternalFileFormatDownlevel(string objectName, string targetVersion) = Error with External File Format {0}. External File Formats are not supported in {1}. - ; message for column master keys not supported in prior versions ColumnMasterKeyDownlevel(string objectName, string targetVersion) = Error with Column Master Key {0}. Column Master Keys are not supported in {1}. -; message for column master keys not supported in prior versions -DatabaseScopedCredentialDownlevel(string objectName, string targetVersion) = Error with Database Scoped Credential {0}. Database Scoped Credentials are not supported in {1}. - -; message for User Defined Table Type not supported in prior versions -UserDefinedTableDownlevel(string objectName, string targetVersion) = Error with User Defined Table Type {0}. User Defined Table Types are not supported in {1}. - ; message for DDL triggers not supported in prior versions DdlTriggerDownlevel(string objectName, string targetVersion) = Error with DDL Trigger {0}. DDL Triggers are not supported in {1}. @@ -110,9 +84,6 @@ ClrUserDefinedFunctionDownlevel(string objectName, string targetVersion) = Error ; message for CLR sprocs not supported in prior versions ClrStoredProcedureDownlevel(string objectName, string targetVersion) = Error with CLR Stored Procedure {0}. CLR Stored Procedure are not supported in {1}. -; message for Assemblies not supported in Shiloh -AssemblyDownlevel(string objectName, string targetVersion) = Error with Assembly {0}. Assemblies are not supported in {1}. - ; message for no values in the ColumnEncryptionKey ColumnEncryptionKeyNoValues(string objectName) = Error creating Column Encryption Key {0}. Must specify at least one Column Encryption Key value before creation. @@ -1181,9 +1152,6 @@ UserOptions=UserOptions BackupDevice=BackupDevice FullTextService=FullTextService ServerActiveDirectory=ServerActiveDirectory -HttpEndpoint=HttpEndpoint -SoapConfiguration=SoapConfiguration -SoapMethod=SoapMethod ServerAlias=ServerAlias PhysicalPartition=PhysicalPartition Audit=Audit diff --git a/src/Microsoft/SqlServer/Management/Smo/IDatabaseOptions.cs b/src/Microsoft/SqlServer/Management/Smo/IDatabaseOptions.cs index 7dc03c80..99e01d3e 100644 --- a/src/Microsoft/SqlServer/Management/Smo/IDatabaseOptions.cs +++ b/src/Microsoft/SqlServer/Management/Smo/IDatabaseOptions.cs @@ -219,7 +219,7 @@ public interface IDatabaseOptions : IDmfFacet [DisplayDescriptionKey("Database_RemoteDatabaseNameDesc")] String RemoteDatabaseName { get; } - [DisplayNameKey("Database_RemoteDataArchiveUseFederatedServiceAccount")] + [DisplayNameKey("Database_RemoteDataArchiveUseFederatedServiceAccountName")] [DisplayDescriptionKey("Database_RemoteDataArchiveUseFederatedServiceAccountDesc")] Boolean RemoteDataArchiveUseFederatedServiceAccount { get; } @@ -243,5 +243,9 @@ public interface IDatabaseOptions : IDmfFacet [DisplayDescriptionKey("Database_DelayedDurabilityDesc")] DelayedDurability DelayedDurability { get; set; } + [DisplayNameKey("Database_AutomaticIndexCompactionEnabledName")] + [DisplayDescriptionKey("Database_AutomaticIndexCompactionEnabledDesc")] + Boolean AutomaticIndexCompactionEnabled { get; set; } + } } diff --git a/src/Microsoft/SqlServer/Management/Smo/IServerInformation.cs b/src/Microsoft/SqlServer/Management/Smo/IServerInformation.cs index 83006d40..52b8d0b0 100644 --- a/src/Microsoft/SqlServer/Management/Smo/IServerInformation.cs +++ b/src/Microsoft/SqlServer/Management/Smo/IServerInformation.cs @@ -161,7 +161,7 @@ public interface IServerInformation : IDmfFacet [DisplayDescriptionKey("Server_IsHadrEnabledDesc")] Boolean IsHadrEnabled { get; } - [DisplayNameKey("Server_IsXTPSupported")] + [DisplayNameKey("Server_IsXTPSupportedName")] [DisplayDescriptionKey("Server_IsXTPSupportedDesc")] Boolean IsXTPSupported { get; } diff --git a/src/Microsoft/SqlServer/Management/Smo/LocalizableResources.strings b/src/Microsoft/SqlServer/Management/Smo/LocalizableResources.strings index c67fa9bb..2ec06850 100644 --- a/src/Microsoft/SqlServer/Management/Smo/LocalizableResources.strings +++ b/src/Microsoft/SqlServer/Management/Smo/LocalizableResources.strings @@ -24,6 +24,7 @@ ServerSQL2017 = SQL Server 2017 ServerSQLv150 = SQL Server 2019 ServerSQLv160 = SQL Server 2022 ServerSQLv170 = SQL Server 2025 +ServerSQLv180 = SQL Server vNext #VBUMP EngineCloud = Microsoft Azure SQL Database EngineCloudMI = Microsoft Azure SQL Managed Instance @@ -224,6 +225,9 @@ Database_AutoCreateStatisticsEnabledDesc=Specifies whether statistics are automa Database_AutoCreateIncrementalStatisticsEnabledName=Auto Create Incremental Statistics Enabled Database_AutoCreateIncrementalStatisticsEnabledDesc=Specifies whether automatically created statistics for the database are incremental by default. +Database_AutomaticIndexCompactionEnabledName=Automatic Index Compaction Enabled +Database_AutomaticIndexCompactionEnabledDesc=Indicates whether Automatic Index Compaction is enabled. + Database_AutoUpdateStatisticsEnabledName=Auto Update Statistics Enabled Database_AutoUpdateStatisticsEnabledDesc=Specifies whether statistics are automatically updated for the database. @@ -404,7 +408,7 @@ Database_MirroringWitnessStatusDesc=The status of the mirroring witness server. Database_NameName=Name Database_NameDesc=The name of the database. -Database_OptimizedLockingOn=Optimized Locking On +Database_OptimizedLockingOnName=Optimized Locking On Database_OptimizedLockingOnDesc=Indicates whether Optimized Locking (OL) is on. Database_OwnerName=Owner @@ -554,6 +558,7 @@ Database_RemoteDataArchiveLinkedServerDesc=Name of the linked server associated Database_RemoteDatabaseNameName=Remote Database Name Database_RemoteDatabaseNameDesc=Name of the Remote Database. + Database_RemoteDataArchiveCredentialName=Remote Data Archive Credential Database_RemoteDataArchiveCredentialDesc=Credential associated with the stretched database. @@ -578,7 +583,7 @@ Database_UserAccessDesc=The database user access. Database_UserDataName=User Data Database_UserDataDesc=User-defined data associated with the referenced object. -Datababase_DefaultFileStreamFileGroupName=DefaultFILESTREAMFileGroup +Database_DefaultFileStreamFileGroupName=DefaultFILESTREAMFileGroup Database_DefaultFileStreamFileGroupDesc=Gets the filegroup name for the default FILESTREAM filegroup. Database_ChangeTrackingAutoCleanUpName=ChangeTrackingAutoCleanUp @@ -980,9 +985,6 @@ Table_DurabilityName=Durability Table_DurabilityDesc=Gets or sets the value that specifies whether the data stored in the table is recoverable. ###### Begin Server Settings -ISettings_Name=Server Settings -ISettings_Desc=Exposes properties of the Server regarding settings for the server. - Settings_AuditLevelName=Audit Level Settings_AuditLevelDesc=Gets or sets the audit level for the instance of Microsoft SQL Server. @@ -995,9 +997,6 @@ Settings_DefaultFileDesc=Gets or sets the default data file directory for the in Settings_DefaultLogName=Default Log Settings_DefaultLogDesc=Gets or sets the default log file directory for the instance of Microsoft SQL Server. -Settings_ImpersonateClientName=Impersonate Client -Settings_ImpersonateClientDesc=Gets or sets the Boolean property value that specifies whether xp_cmdshell runs in the security context of the client connection or SQL Server Agent. - Settings_LoginModeName=Login Mode Settings_LoginModeDesc=Gets or sets the logon mode for Microsoft SQL Server. @@ -1012,190 +1011,6 @@ Settings_ParentDesc=Gets the Server object that is the parent of the Settings ob ########## End Server Settings -########## Begin Server Configuration -ServerConfiguration_Name=Server Configuration -ServerConfiguration_Desc=Exposes properties of the Server regarding the configuration settings of the server. - -ServerConfiguration_ContainmentEnabledName=Containment Enabled -ServerConfiguration_ContainmentEnabledDesc=Gets or sets whether contained databases and authentication is enabled on this instance of SQL Server. - -ServerConfiguration_AdHocDistributedQueriesEnabledName=AdHoc Distributed Queries Enabled -ServerConfiguration_AdHocDistributedQueriesEnabledDesc=Gets the ConfigProperty object that is used to set the ad hoc distributed queries configuration option. - -ServerConfiguration_Affinity64MaskName=Affinity 64 Mask -ServerConfiguration_Affinity64MaskDesc=Gets the ConfigProperty object that is used to set the affinity 64 mask configuration option. - -ServerConfiguration_AffinityIOMaskName=Affinity IO Mask -ServerConfiguration_AffinityIOMaskDesc=Gets the ConfigProperty object that is used to set the affinity IO mask configuration option. - -ServerConfiguration_AffinityMaskName=Affinity Mask -ServerConfiguration_AffinityMaskDesc=Gets the ConfigProperty object that is used to set the affinity mask configuration option. - -ServerConfiguration_AgentXPsEnabledName=Agent XPs Enabled -ServerConfiguration_AgentXPsEnabledDesc=Gets the ConfigProperty object that is used to set the agent XPs enabled configuration option. - -ServerConfiguration_AllowedHttpSessionsName=Allowed Http Sessions -ServerConfiguration_AllowedHttpSessionsDesc=Gets the ConfigProperty object that is used to set the allowed HTTP sessions configuration option. - -ServerConfiguration_AllowUpdatesName=Allow Updates -ServerConfiguration_AllowUpdatesDesc=Gets the ConfigProperty object that is used to set the allow updates configuration option. - -ServerConfiguration_AweEnabledName=Awe Enabled -ServerConfiguration_AweEnabledDesc=Gets the ConfigProperty object that is used to set the AWE enabled configuration option. - -ServerConfiguration_C2AuditModeName=C2 Audit Mode -ServerConfiguration_C2AuditModeDesc=Gets the ConfigProperty object that is used to set the C2 audit mode configuration option. - -ServerConfiguration_CostThresholdForParallelismName=Cost Threshold For Parallelism -ServerConfiguration_CostThresholdForParallelismDesc=Gets the ConfigProperty object that is used to set the cost threshold for parallelism configuration option. - -ServerConfiguration_CrossDBOwnershipChainingName=Cross DB Ownership Chaining -ServerConfiguration_CrossDBOwnershipChainingDesc=Gets the ConfigProperty object that is used to set the cross DB ownership chaining configuration option. - -ServerConfiguration_CursorThresholdName=Cursor Threshold -ServerConfiguration_CursorThresholdDesc=Gets the ConfigProperty object that is used to set the cursor threshold configuration option. - -ServerConfiguration_DatabaseMailEnabledName=DatabaseMail Enabled -ServerConfiguration_DatabaseMailEnabledDesc=Gets the ConfigProperty object that is used to set the database mail enabled configuration option. - -ServerConfiguration_DefaultFullTextLanguageName=Default FullText Language -ServerConfiguration_DefaultFullTextLanguageDesc=Gets the ConfigProperty object that is used to set default full text language configuration option. - -ServerConfiguration_DefaultLanguageName=Default Language -ServerConfiguration_DefaultLanguageDesc=Gets the ConfigProperty object that is used to set the default language configuration option. - -ServerConfiguration_FilestreamAccessLevelName= FILESTREAM Access Level -ServerConfiguration_FilestreamAccessLevelDesc=Gets the FILESTREAM access level. - -ServerConfiguration_FillFactorName=Fill Factor -ServerConfiguration_FillFactorDesc=Gets the ConfigProperty object that is used to set the fill factor configuration option. - -ServerConfiguration_HttpConnectionIdleMaximumTimeName=Http Connection Idle Maximum Time -ServerConfiguration_HttpConnectionIdleMaximumTimeDesc=Gets the ConfigProperty object that is used to set the HTTP connection idle maximum time configuration option. - -ServerConfiguration_HttpSessionIdleMaximumTimeName=Http Session Idle Maximum Time -ServerConfiguration_HttpSessionIdleMaximumTimeDesc=Gets the ConfigProperty object that is used to set the HTTP session idle maximum time configuration option. - -ServerConfiguration_IndexCreateMemoryName=Index Create Memory -ServerConfiguration_IndexCreateMemoryDesc=Gets the ConfigProperty object that is used to set the index create memory configuration option. - -ServerConfiguration_IsSqlClrEnabledName=Is SqlClr Enabled -ServerConfiguration_IsSqlClrEnabledDesc=Gets the ConfigProperty object that is used to set the is SQL CLR enabled configuration option. - -ServerConfiguration_LightweightPoolingName=Light weight Pooling -ServerConfiguration_LightweightPoolingDesc=Gets the ConfigProperty object that is used to set the lightweight pooling configuration option. - -ServerConfiguration_LocksName=Locks -ServerConfiguration_LocksDesc=Gets the ConfigProperty object that is used to set the locks configuration option. - -ServerConfiguration_MaxDegreeOfParallelismName=Max Degree Of Parallelism -ServerConfiguration_MaxDegreeOfParallelismDesc=Gets the ConfigProperty object that is used to set the max degree of parallelism configuration option. - -ServerConfiguration_MaxServerMemoryName=Max Server Memory -ServerConfiguration_MaxServerMemoryDesc=Gets the ConfigProperty object that is used to set the max server memory configuration option. - -ServerConfiguration_MaxWorkerThreadsName=Max Worker Threads -ServerConfiguration_MaxWorkerThreadsDesc=Gets the ConfigProperty object that is used to set the max worker threads configuration option. - -ServerConfiguration_MediaRetentionName=Media Retention -ServerConfiguration_MediaRetentionDesc=Gets the ConfigProperty object that is used to set the media retention configuration option. - -ServerConfiguration_MinMemoryPerQueryName=Min Memory Per Query -ServerConfiguration_MinMemoryPerQueryDesc=Gets the ConfigProperty object that is used to set the min memory per query configuration option. - -ServerConfiguration_OptimizeAdhocWorkloadsName= Optimize for Ad hoc Workloads -ServerConfiguration_OptimizeAdhocWorkloadsDesc=When this option is set, plan cache size is further reduced for single-use adhoc OLTP workload. - -ServerConfiguration_MinServerMemoryName=Min Server Memory -ServerConfiguration_MinServerMemoryDesc=Gets the ConfigProperty object that is used to set the min server memory configuration option. - -ServerConfiguration_NestedTriggersName=Nested Triggers -ServerConfiguration_NestedTriggersDesc=Gets the ConfigProperty object that is used to set the nested triggers configuration option. - -ServerConfiguration_NetworkPacketSizeName=Network Packet Size -ServerConfiguration_NetworkPacketSizeDesc=Gets the ConfigProperty object that is used to set the network packet size configuration option. - -ServerConfiguration_OleAutomationProceduresEnabledName=Ole Automation Procedures Enabled -ServerConfiguration_OleAutomationProceduresEnabledDesc=Gets the ConfigProperty object that is used to set the OLE automation procedures enabled configuration option. - -ServerConfiguration_OpenObjectsName=Open Objects -ServerConfiguration_OpenObjectsDesc=Gets the ConfigProperty object that is used to set the open objects configuration option. - -ServerConfiguration_PrecomputeRankName=Precompute Rank -ServerConfiguration_PrecomputeRankDesc=Gets the ConfigProperty object that is used to set the precompute rank configuration option. - -ServerConfiguration_PriorityBoostName=Priority Boost -ServerConfiguration_PriorityBoostDesc=Gets the ConfigProperty object that is used to set the priority boost configuration option. - -ServerConfiguration_ProtocolHandlerTimeoutName=Protocol Handler Timeout -ServerConfiguration_ProtocolHandlerTimeoutDesc=Gets the ConfigProperty object that is used to set the protocol handler timeoutconfiguration option. - -ServerConfiguration_QueryGovernorCostLimitName=Query Governor Cost Limit -ServerConfiguration_QueryGovernorCostLimitDesc=Gets the ConfigProperty object that is used to set the query governor cost limit option. - -ServerConfiguration_QueryWaitName=Query Wait -ServerConfiguration_QueryWaitDesc=Gets the ConfigProperty object that is used to set the query wait configuration option. - -ServerConfiguration_RecoveryIntervalName=Recovery Interval -ServerConfiguration_RecoveryIntervalDesc=Gets the ConfigProperty object that is used to set the recovery interval configuration option. - -ServerConfiguration_RemoteAccessName=Remote Access -ServerConfiguration_RemoteAccessDesc=Gets the ConfigProperty object that is used to set the remote access configuration option. - -ServerConfiguration_RemoteDacConnectionsEnabledName=Remote Dac Connections Enabled -ServerConfiguration_RemoteDacConnectionsEnabledDesc=Gets the ConfigProperty object that is used to set the remote DAC connections enabled configuration option. - -ServerConfiguration_RemoteLoginTimeoutName=Remote Login Timeout -ServerConfiguration_RemoteLoginTimeoutDesc=Gets the ConfigProperty object that is used to set the remote login timeout configuration option. - -ServerConfiguration_RemoteProcTransName=Remote Proc Trans -ServerConfiguration_RemoteProcTransDesc=Gets the ConfigProperty object that is used to set the remote proc trans configuration option. - -ServerConfiguration_RemoteQueryTimeoutName=Remote Query Timeout -ServerConfiguration_RemoteQueryTimeoutDesc=Gets the ConfigProperty object that is used to set the remote query timeout configuration option. - -ServerConfiguration_ReplicationMaxTextSizeName=Replication Max Text Size -ServerConfiguration_ReplicationMaxTextSizeDesc=Gets the ConfigProperty object that is used to set the replication max text size configuration option. - -ServerConfiguration_ReplicationXPsEnabledName=Replication XPs Enabled -ServerConfiguration_ReplicationXPsEnabledDesc=Gets the ConfigProperty object that is used to set the replication XPs enabled configuration option. - -ServerConfiguration_ScanForStartupProceduresName=Scan For Startup Procedures -ServerConfiguration_ScanForStartupProceduresDesc=Gets the ConfigProperty object that is used to set the scan for startup procedures configuration option. - -ServerConfiguration_SetWorkingSetSizeName=Set Working Set Size -ServerConfiguration_SetWorkingSetSizeDesc=Gets the ConfigProperty object that is used to set the set working set size configuration option. - -ServerConfiguration_ShowAdvancedOptionsName=Show Advanced Options -ServerConfiguration_ShowAdvancedOptionsDesc=Gets the ConfigProperty object that is used to set the show advanced options configuration option. - -ServerConfiguration_SmoAndDmoXPsEnabledName=Smo And Dmo XPs Enabled -ServerConfiguration_SmoAndDmoXPsEnabledDesc=Gets the ConfigProperty object that is used to set the SMO and DMO XPs enabled configuration option. - -ServerConfiguration_SqlMailXPsEnabledName=SqlMail XP sEnabled -ServerConfiguration_SqlMailXPsEnabledDesc=Gets the ConfigProperty object that is used to set the SQL mail XPs enabled configuration option. - -ServerConfiguration_TransformNoiseWordsName=Transform Noise Words -ServerConfiguration_TransformNoiseWordsDesc=Gets the ConfigProperty object that is used to set the transform noise words configuration option. - -ServerConfiguration_TwoDigitYearCutoffName=Two Digit Year Cut off -ServerConfiguration_TwoDigitYearCutoffDesc=Gets the ConfigProperty object that is used to set the two digit year cutoff configuration option. - -ServerConfiguration_UserConnectionsName=User Connections -ServerConfiguration_UserConnectionsDesc=Gets the ConfigProperty object that is used to set the user connections configuration option. - -ServerConfiguration_UserOptionsName=User Options -ServerConfiguration_UserOptionsDesc=Gets the ConfigProperty object that is used to set the user options configuration option. - -ServerConfiguration_WebXPsEnabledName=Web XPs Enabled -ServerConfiguration_WebXPsEnabledDesc=Gets the ConfigProperty object that is used to set the web XPs enabled configuration option. - -ServerConfiguration_XPCmdShellEnabledName=XPCmdShell Enabled -ServerConfiguration_XPCmdShellEnabledDesc=Gets the ConfigProperty object that is used to set the XP cmd shell enabled configuration option. - - -###### End ServerConfiguration - ###### Begin Login Login_Name=Login @@ -1455,12 +1270,6 @@ Index_OptimizeForArraySearchDesc=Gets or sets whether the JSON index is optimize IndexedJsonPath_PathName=Path IndexedJsonPath_PathDesc=Gets or sets the JSON path expression for the indexed path. -IndexedJsonPath_TableIDName=Table ID -IndexedJsonPath_TableIDDesc=Gets the ID of the table that contains the index. - -IndexedJsonPath_IndexIDName=Index ID -IndexedJsonPath_IndexIDDesc=Gets the ID of the index that contains this path. - ####### End IndexedJsonPath @@ -2135,7 +1944,7 @@ Table_HasXmlCompressedPartitionsDesc=Gets the Boolean value that indicates wheth Table_IsVarDecimalStorageFormatEnabledName=Is Var Decimal Storage Format Enabled Table_IsVarDecimalStorageFormatEnabledDesc=Gets the Boolean value that indicates whether var decimal storage is enabled. -Table_AnsiNulAnsiNullsStatusName=Ansi Nulls Status +Table_AnsiNullsStatusName=ANSI Nulls Status Table_AnsiNullsStatusDesc=Gets the Boolean property value that specifies whether SQL-92 NULL handling is enabled on the table. Table_ChangeTrackingEnabledName=ChangeTrackingEnabled @@ -2156,7 +1965,7 @@ Table_DataRetentionPeriodDesc =The length of time to retain data in the table. Table_DataRetentionPeriodUnitName =Data Retention Period Unit Table_DataRetentionPeriodUnitDesc =The unit of time to retain data in the table (DAY, WEEK, MONTH, YEAR, INFINITE). -Table_DataRetentionFilterColumnName =Data Retention Filter Column Name +Table_DataRetentionFilterColumnNameName =Data Retention Filter Column Name Table_DataRetentionFilterColumnNameDesc =The name of the column used to filter rows for data retention. Table_DateLastModifiedName=Date Last Modified @@ -2273,9 +2082,6 @@ Table_QuotedIdentifierStatusDesc=Gets or sets a Boolean property value that spec Table_RemoteDataArchiveEnabledName=Remote Data Archive Enabled Table_RemoteDataArchiveEnabledDesc=Whether Remote Data Archive is enabled for the table. -Table_RemoteDataArchiveMigrationEnabledName=Remote Data Archive Migration Enabled -Table_RemoteDataArchiveMigrationEnabledDesc=Whether Remote Data Archive Migration is enabled for the table. - Table_RemoteDataArchiveDataMigrationStateName=Remote Data Archive Migration State Table_RemoteDataArchiveDataMigrationStateDesc=Migration state of Remote Data Archive for the table. @@ -2288,15 +2094,9 @@ Table_RemoteObjectNameDesc=For external tables over a SHARD_MAP_MANAGER external Table_RemoteTableNameName=Remote Table Name Table_RemoteTableNameDesc=Remote table name for the stretched table. -Table_RemoteDataArchiveMigratedRowCountName=Remote Data Archive Migrated Row Count -Table_RemoteDataArchiveMigratedRowCountDesc=Number of rows of the table migrated to remote table. - Table_RemoteSchemaNameName=Remote Schema Name Table_RemoteSchemaNameDesc=For external tables over a SHARD_MAP_MANAGER external data source, this is the schema where the base table is located on the remote databases (if different from the schema where the external table is defined). -Table_RemoteTableSizeName=Remote Table Size -Table_RemoteTableSizeDesc=Size of the stretched table. - Table_RemoteTableProvisionedName=Remote Table Provisioned Table_RemoteTableProvisionedDesc=Whether Remote Table provisioning is complete for the table. @@ -2458,6 +2258,7 @@ Server_BrowserServiceAccountDesc=Gets the service account name for the SQL Serve Server_NamedPipesEnabledName=Named Pipes Enabled Server_NamedPipesEnabledDesc=Boolean value that specifies whether the Named Pipes protocol is enabled. +Server_TcpEnabledName=TCP/IP Protocol Enabled Server_TcpIpProtocolEnabledName=TCP/IP Protocol Enabled Server_TcpEnabledDesc=Boolean value that specifies whether the TCP/IP protocol is enabled. @@ -2473,7 +2274,7 @@ Server_ServiceStartModeDesc=Get the SQL Server service startup type. Server_InstanceIdName=Instance Identifier Server_InstanceIdDesc=Get the Instance ID that specifies the installation location for the server components. -SServer_FilestreamLevelName=FILESTREAM Level +Server_FilestreamLevelName=FILESTREAM Level Server_FilestreamLevelDesc=Gets the enabled level of FILESTREAM. Server_FilestreamShareNameName=FILESTREAM Share Name @@ -2591,9 +2392,6 @@ Server_DefaultFileDesc=Gets or sets the default data file directory for the inst Server_DefaultLogName=Default Log Server_DefaultLogDesc=Gets or sets the default log file directory for the instance of Microsoft SQL Server. -Server_ImpersonateClientName=Impersonate Client -Server_ImpersonateClientDesc=Gets or sets the Boolean property value that specifies whether xp_cmdshell runs in the security context of the client connection or SQL Server Agent. - Server_LoginModeName=Login Mode Server_LoginModeDesc=Gets or sets the logon mode for Microsoft SQL Server. diff --git a/src/Microsoft/SqlServer/Management/Smo/ScriptingOptions.cs b/src/Microsoft/SqlServer/Management/Smo/ScriptingOptions.cs index 1762db8c..a6d276ea 100644 --- a/src/Microsoft/SqlServer/Management/Smo/ScriptingOptions.cs +++ b/src/Microsoft/SqlServer/Management/Smo/ScriptingOptions.cs @@ -128,9 +128,11 @@ public enum SqlServerVersion Version160 = 10, [DisplayNameKey("ServerSQLv170")] Version170 = 11, + [DisplayNameKey("ServerSQLv180")] + Version180 = 12, // VBUMP - [DisplayNameKey("ServerSQLv170")] - VersionLatest = Version170, + [DisplayNameKey("ServerSQLv180")] + VersionLatest = Version180, // Set this to the oldest SQL Server version still under extended security support // https://learn.microsoft.com/lifecycle/faq/extended-security-updates#esu-availability-and-end-dates [DisplayNameKey("ServerSQL14")] @@ -896,10 +898,12 @@ internal static ServerVersion ConvertToServerVersion(SqlServerVersion ver) return new ServerVersion(16, 0, 0); case SqlServerVersion.Version170: return new ServerVersion(17, 0, 0); + case SqlServerVersion.Version180: + return new ServerVersion(18, 0, 0); //VBUMP default: Debug.Fail("unexpected server version"); - return new ServerVersion(17, 0, 0); + return new ServerVersion(18, 0, 0); } } /// @@ -2353,9 +2357,12 @@ public static SqlServerVersion ConvertToSqlServerVersion(int majorVersion, int m case 16: sqlSvrVersion = SqlServerVersion.Version160; break; - case int n when n >= 17: + case 17: sqlSvrVersion = SqlServerVersion.Version170; break; + case int n when n >= 18: + sqlSvrVersion = SqlServerVersion.Version180; + break; default: // VBUMP throw new SmoException(ExceptionTemplates.InvalidVersion(majorVersion.ToString())); diff --git a/src/Microsoft/SqlServer/Management/Smo/ScriptingPreferences.cs b/src/Microsoft/SqlServer/Management/Smo/ScriptingPreferences.cs index 544f078e..4db79303 100644 --- a/src/Microsoft/SqlServer/Management/Smo/ScriptingPreferences.cs +++ b/src/Microsoft/SqlServer/Management/Smo/ScriptingPreferences.cs @@ -113,7 +113,7 @@ internal bool TargetVersionAndDatabaseEngineTypeDirty } // VBUMP - use latest in-market - private SqlServerVersion m_eTargetServerVersion = SqlServerVersion.Version160; + private SqlServerVersion m_eTargetServerVersion = SqlServerVersion.Version170; /// /// The server version on which the scripts will run. @@ -208,10 +208,14 @@ internal void SetTargetServerVersion(ServerVersion ver) case 17: m_eTargetServerVersion = SqlServerVersion.Version170; break; + + case 18: + m_eTargetServerVersion = SqlServerVersion.Version180; + break; // VBUMP default: - Debug.Fail($"Unknown server version {ver.Major}. Treating it as 17."); - m_eTargetServerVersion = SqlServerVersion.Version170; + Debug.Fail($"Unknown server version {ver.Major}. Treating it as 18."); + m_eTargetServerVersion = SqlServerVersion.Version180; break; } } diff --git a/src/Microsoft/SqlServer/Management/Smo/SmoUtility.cs b/src/Microsoft/SqlServer/Management/Smo/SmoUtility.cs index 6bc0668c..6768bfaf 100644 --- a/src/Microsoft/SqlServer/Management/Smo/SmoUtility.cs +++ b/src/Microsoft/SqlServer/Management/Smo/SmoUtility.cs @@ -562,7 +562,8 @@ internal static ServerVersion GetMinimumSupportedVersion(Type type, DatabaseEngi (new ServerVersion(14, 0)), //2017 (new ServerVersion(15, 0)), //2019 (new ServerVersion(16, 0)), //2022 - (new ServerVersion(17, 0)) //2025 (VBUMP) + (new ServerVersion(17, 0)), //2025 + (new ServerVersion(18, 0)) //vNext (VBUMP) }; /// diff --git a/src/Microsoft/SqlServer/Management/Smo/SqlSmoObject.cs b/src/Microsoft/SqlServer/Management/Smo/SqlSmoObject.cs index ffc8b1a0..b67649f0 100644 --- a/src/Microsoft/SqlServer/Management/Smo/SqlSmoObject.cs +++ b/src/Microsoft/SqlServer/Management/Smo/SqlSmoObject.cs @@ -4282,6 +4282,11 @@ internal static IEnumerable GetDisabledProperties(Type type, DatabaseEng { yield return nameof(Database.DataRetentionEnabled); } + if (databaseEngineEdition != DatabaseEngineEdition.SqlManagedInstance + && databaseEngineEdition != DatabaseEngineEdition.SqlDatabase) + { + yield return nameof(Database.AutomaticIndexCompactionEnabled); + } if (databaseEngineEdition == DatabaseEngineEdition.SqlOnDemand) { yield return nameof(Database.AutoClose); @@ -6771,12 +6776,16 @@ internal static CompatibilityLevel GetCompatibilityLevel(ServerVersion ver) return CompatibilityLevel.Version140; case 15: return CompatibilityLevel.Version150; + case 16: + return CompatibilityLevel.Version160; + case 17: + return CompatibilityLevel.Version170; // VBUMP //Forward Compatibility: An older version SSMS/Smo connecting to a future version sql server database engine. //That is why if the ver(ServerVersion) is unknown, we need to set it according to the latest database engine available, //so that all Latest-Version-Supported-Features in the Tools work seamlessly for the unknown future version database engines too. default: - return CompatibilityLevel.Version160; + return CompatibilityLevel.Version180; } } @@ -6892,6 +6901,8 @@ internal static string GetSqlServerName(SqlSmoObject srv) return LocalizableResources.ServerSQLv160; case 17: return LocalizableResources.ServerSQLv170; + case 18: + return LocalizableResources.ServerSQLv180; // VBUMP default: return string.Empty; diff --git a/src/Microsoft/SqlServer/Management/Smo/UserBase.cs b/src/Microsoft/SqlServer/Management/Smo/UserBase.cs index 9759ed98..ba555cd9 100644 --- a/src/Microsoft/SqlServer/Management/Smo/UserBase.cs +++ b/src/Microsoft/SqlServer/Management/Smo/UserBase.cs @@ -1179,7 +1179,7 @@ internal override void ScriptAlter(StringCollection query, ScriptingPreferences if (lType == LoginType.WindowsGroup && this.ServerVersion.Major >= 11) { AddComma(sbOption, ref optionAdded); - sbOption.Append("DEFAULT_SCHEMA==NULL"); + sbOption.Append("DEFAULT_SCHEMA=NULL"); } } diff --git a/src/Microsoft/SqlServer/Management/Smo/UserPermission.cs b/src/Microsoft/SqlServer/Management/Smo/UserPermission.cs index a2c6c192..7eeb4a1f 100644 --- a/src/Microsoft/SqlServer/Management/Smo/UserPermission.cs +++ b/src/Microsoft/SqlServer/Management/Smo/UserPermission.cs @@ -60,7 +60,7 @@ public override int PropertyNameToIDLookup(string propertyName) /// /// This is the number of properties available for each version of the Standalone SQL engine /// - static int[] versionCount = new int[] { 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 }; //7.0, 8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0 + static int[] versionCount = new int[] { 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 }; //7.0, 8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0 /// /// This is the number of properties available for each version of the Cloud SQL engine diff --git a/src/Microsoft/SqlServer/Management/Smo/exception.cs b/src/Microsoft/SqlServer/Management/Smo/exception.cs index d64762fc..3e56a493 100644 --- a/src/Microsoft/SqlServer/Management/Smo/exception.cs +++ b/src/Microsoft/SqlServer/Management/Smo/exception.cs @@ -1146,6 +1146,8 @@ private string GetVersionName(ServerVersion version) return LocalizableResources.ServerSQLv160; case 17: return LocalizableResources.ServerSQLv170; + case 18: + return LocalizableResources.ServerSQLv180; default: // VBUMP return version.ToString(); diff --git a/src/Microsoft/SqlServer/Management/Smo/propertiesMetadata.cs b/src/Microsoft/SqlServer/Management/Smo/propertiesMetadata.cs index 0cce998a..9b95ce53 100644 --- a/src/Microsoft/SqlServer/Management/Smo/propertiesMetadata.cs +++ b/src/Microsoft/SqlServer/Management/Smo/propertiesMetadata.cs @@ -158,6 +158,7 @@ protected enum StandaloneVersionIndex v150 = 9, v160 = 10, v170 = 11, + v180 = 12, // VBUMP } @@ -315,12 +316,17 @@ internal static int GetCurrentVersionIndex(ServerVersion sv, DatabaseEngineType versionIndex = (int)StandaloneVersionIndex.v170; break; } + case 18: + { + versionIndex = (int)StandaloneVersionIndex.v180; + break; + } // VBUMP //Forward Compatibility: An older version SSMS/Smo connecting to a future version sql server database engine. //That is why if the server version is unknown, we need to set it according to the latest database engine available, //so that all Latest-Version-Supported-Features in the Tools work seamlessly for the unknown future version database engines too. default: - versionIndex = (int)StandaloneVersionIndex.v170; + versionIndex = (int)StandaloneVersionIndex.v180; break; } } @@ -587,6 +593,11 @@ private string GetServerNameFromVersionIndex(int index) versionName = LocalizableResources.ServerSQLv170; break; } + case (int)StandaloneVersionIndex.v180: + { + versionName = LocalizableResources.ServerSQLv180; + break; + } default: // VBUMP { //Index is unknown, leave as default value but log the error since it shouldn't happen diff --git a/src/Microsoft/SqlServer/Management/SqlEnum/enumstructs.cs b/src/Microsoft/SqlServer/Management/SqlEnum/enumstructs.cs index db375c70..f89b2b22 100644 --- a/src/Microsoft/SqlServer/Management/SqlEnum/enumstructs.cs +++ b/src/Microsoft/SqlServer/Management/SqlEnum/enumstructs.cs @@ -961,7 +961,11 @@ public enum CompatibilityLevel /// /// Compatibility level for SQL v170 /// - Version170 = 170 + Version170 = 170, + /// + /// Compatibility level for SQL v180 + /// + Version180 = 180, // VBUMP } diff --git a/src/Microsoft/SqlServer/Management/SqlEnum/xml/Database.xml b/src/Microsoft/SqlServer/Management/SqlEnum/xml/Database.xml index 2b376015..e517cbe0 100644 --- a/src/Microsoft/SqlServer/Management/SqlEnum/xml/Database.xml +++ b/src/Microsoft/SqlServer/Management/SqlEnum/xml/Database.xml @@ -61,7 +61,11 @@ pfg.database_id = dtb.database_id - + + aic.database_id = dtb.database_id + + + + create table #aictemp (database_id int primary key, is_automatic_index_compaction_on bit) + if (((SERVERPROPERTY('EngineEdition') = 8 AND SERVERPROPERTY('ProductUpdateType') = 'Continuous')) OR SERVERPROPERTY('EngineEdition') = 5) + BEGIN + exec sp_executesql N'insert into #aictemp (database_id, is_automatic_index_compaction_on) select d.database_id, d.is_automatic_index_compaction_on from sys.databases d' + END + + + @@ -145,6 +160,12 @@ + + + drop table #aictemp + + + drop table #pvsfilegroups @@ -615,6 +636,12 @@ 0 + + + + ISNULL(aic.is_automatic_index_compaction_on, 0) + + diff --git a/src/Microsoft/SqlServer/Management/SqlScriptPublish/Microsoft.SqlServer.Management.SqlScriptPublish.csproj b/src/Microsoft/SqlServer/Management/SqlScriptPublish/Microsoft.SqlServer.Management.SqlScriptPublish.csproj index b891a0c8..28548ce3 100644 --- a/src/Microsoft/SqlServer/Management/SqlScriptPublish/Microsoft.SqlServer.Management.SqlScriptPublish.csproj +++ b/src/Microsoft/SqlServer/Management/SqlScriptPublish/Microsoft.SqlServer.Management.SqlScriptPublish.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SR.strings b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SR.strings index c87a7b36..c88da1b0 100644 --- a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SR.strings +++ b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SR.strings @@ -70,6 +70,7 @@ ValueIsNullOrEmpty(string str) = '{0}' is null or empty. InvalidObjectType(string typeName) = Invalid object type '{0}'. InvalidObjectTypeForVersion(string name, string type) = Object '{0}' of type '{1}' is not valid for the selected database engine type. ERROR_ScriptingFailed = An error occurred while scripting the objects. +ERROR_NotebookWriterNotRegistered = No Notebook writer factory has been registered. Call SqlScriptGenerator.NotebookWriterFactory to register an ISmoScriptWriter implementation for Notebook output before using ScriptDestination.ToNotebook. # Unsupported Options InvalidScriptPreText = /* * * *\nThe following script is not valid for the specified SQL Server database engine type and must be updated.\n* * * */\n diff --git a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptGenerator.cs b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptGenerator.cs index b197dffe..a1a91b80 100644 --- a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptGenerator.cs +++ b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptGenerator.cs @@ -11,7 +11,6 @@ using Microsoft.SqlServer.Management.Diagnostics; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; -using Microsoft.SqlServer.Management.Smo.Notebook; namespace Microsoft.SqlServer.Management.SqlScriptPublish { @@ -20,6 +19,13 @@ namespace Microsoft.SqlServer.Management.SqlScriptPublish /// public class SqlScriptGenerator { + /// + /// Global factory delegate for creating an ISmoScriptWriter that generates Notebook output. + /// Set this before using ScriptDestination.ToNotebook. + /// The delegate receives the output file path and returns an ISmoScriptWriter implementation. + /// + public static Func NotebookWriterFactory { get; set; } + private SqlScriptPublishModel model; private SqlScriptOptions scriptOptions; private ScriptMaker scriptMaker; @@ -181,7 +187,10 @@ private void CloseWriter(ScriptOutputOptions outputOptions, ISmoScriptWriter wri break; case ScriptDestination.ToNotebook: - (writer as NotebookFileWriter).Close(); + if (writer is IDisposable disposableWriter) + { + disposableWriter.Dispose(); + } break; } @@ -221,12 +230,11 @@ private ISmoScriptWriter GetScriptWriter(ScriptOutputOptions outputOptions) break; case ScriptDestination.ToNotebook: - writer = new NotebookFileWriter(outputOptions.SaveFileName) + if (NotebookWriterFactory == null) { - BatchTerminator = BatchTerminator, - ScriptBatchTerminator = ScriptBatchTerminator, - Indented = outputOptions.Indented - }; + throw new InvalidOperationException(SR.ERROR_NotebookWriterNotRegistered); + } + writer = NotebookWriterFactory(outputOptions.SaveFileName); break; case ScriptDestination.ToCustomWriter: @@ -389,7 +397,11 @@ private void SetScriptingOptions(ScriptingOptions scriptingOptions) scriptingOptions.ScriptXmlCompression = SqlScriptOptions.ConvertBooleanTypeOptionToBoolean(this.scriptOptions.ScriptXmlCompressionOptions); // VBUMP - if (scriptOptions.ScriptCompatibilityOption == SqlScriptOptions.ScriptCompatibilityOptions.Script170Compat) + if (scriptOptions.ScriptCompatibilityOption == SqlScriptOptions.ScriptCompatibilityOptions.Script180Compat) + { + scriptingOptions.TargetServerVersion = SqlServerVersion.Version180; + } + else if (scriptOptions.ScriptCompatibilityOption == SqlScriptOptions.ScriptCompatibilityOptions.Script170Compat) { scriptingOptions.TargetServerVersion = SqlServerVersion.Version170; } diff --git a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptions.cs b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptions.cs index 5320d65d..584bf88b 100644 --- a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptions.cs +++ b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptions.cs @@ -52,10 +52,13 @@ public enum ScriptCompatibilityOptions [DisplayNameKey("OnlyScript160CompatibleFeatures")] [CompatibilityLevelSupportedVersion(16)] Script160Compat, - // VBUMP [DisplayNameKey("OnlyScript170CompatibleFeatures")] [CompatibilityLevelSupportedVersion(17)] Script170Compat, + // VBUMP + [DisplayNameKey("OnlyScript180CompatibleFeatures")] + [CompatibilityLevelSupportedVersion(18)] + Script180Compat, } /// @@ -225,8 +228,8 @@ public SqlScriptOptions(Version version) // VBUMP else { - Debug.Assert(false, "Unexpected server version. Setting Compatibility Mode to 17.0!"); - compatMode = ScriptCompatibilityOptions.Script170Compat; + Debug.Assert(false, "Unexpected server version. Setting Compatibility Mode to 18.0!"); + compatMode = ScriptCompatibilityOptions.Script180Compat; } // setup the SqlAzure read/only properites and their default values @@ -352,8 +355,8 @@ public ICollection ConfigureVisibleEnumFields(ITypeDescriptorContext context, Ar } else { - // Remove 170 since it's currently only for standalone instances - values.Remove(ScriptCompatibilityOptions.Script170Compat); + // VBUMP: Remove 180 since Azure SQL Database doesn't support compatibility level 180 yet + values.Remove(ScriptCompatibilityOptions.Script180Compat); } } return values; diff --git a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptionsSR.strings b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptionsSR.strings index a7f8bd25..c571557b 100644 --- a/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptionsSR.strings +++ b/src/Microsoft/SqlServer/Management/SqlScriptPublish/SqlScriptOptionsSR.strings @@ -216,6 +216,7 @@ OnlyScript140CompatibleFeatures = SQL Server 2017 OnlyScript150CompatibleFeatures = SQL Server 2019 OnlyScript160CompatibleFeatures = SQL Server 2022 OnlyScript170CompatibleFeatures = SQL Server 2025 +OnlyScript180CompatibleFeatures = SQL Server vNext # VBUMP TargetEngineType = Script for the database engine type TargetEngineTypeDescription = Script only features compatible with the specified SQL Server database engine type. diff --git a/src/UnitTest/Directory.Build.props b/src/UnitTest/Directory.Build.props index 66f68d40..adfe5287 100644 --- a/src/UnitTest/Directory.Build.props +++ b/src/UnitTest/Directory.Build.props @@ -5,6 +5,8 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) $(NetfxVersion);net8.0 false + + Major diff --git a/src/UnitTest/Smo/LocalizablePropertyResourcesTests.cs b/src/UnitTest/Smo/LocalizablePropertyResourcesTests.cs new file mode 100644 index 00000000..3421b49a --- /dev/null +++ b/src/UnitTest/Smo/LocalizablePropertyResourcesTests.cs @@ -0,0 +1,602 @@ +// Copyright (c) Microsoft. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Resources; +using System.Text; +using Microsoft.SqlServer.Management.Dmf; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NUnit.Framework; +using Assert = NUnit.Framework.Assert; +using SmoServer = Microsoft.SqlServer.Management.Smo.Server; + +namespace Microsoft.SqlServer.Test.SmoUnitTests +{ + /// + /// Tests to verify that all SMO and related objects with LocalizableTypeConverter have + /// properly named localization strings for their properties. + /// This helps catch typos in .strings files like: + /// - "Datababase_" instead of "Database_" + /// - "SServer_" instead of "Server_" + /// - Missing "Name" suffix (e.g., "Table_DataRetentionFilterColumnName" instead of "Table_DataRetentionFilterColumnNameName") + /// + [TestClass] + public class LocalizablePropertyResourcesTests + { + // Cache resource managers to avoid repeated lookups + private static readonly Dictionary ResourceManagerCache = new Dictionary(); + + /// + /// Gets or sets the test context which provides information about the current test run. + /// + public Microsoft.VisualStudio.TestTools.UnitTesting.TestContext TestContext { get; set; } + + /// + /// List of assemblies to scan for localization validation. + /// These are assemblies that the test project has references to and use LocalizedPropertyResources. + /// + private static readonly (Assembly Assembly, string Name)[] AssembliesToScan = new[] + { + (typeof(SmoServer).Assembly, "Smo"), + (typeof(PolicyStore).Assembly, "Dmf"), + (typeof(SfcInstance).Assembly, "Sdk.Sfc"), + }; + + /// + /// Verifies that all types with LocalizedPropertyResources attribute have valid + /// Name and Description strings for each property that uses the default key pattern. + /// + [TestMethod] + [TestCategory("Unit")] + public void AllAssemblies_WithLocalizableTypeConverter_HaveValidPropertyNameAndDescriptionStrings() + { + var allErrors = new List(); + var totalCheckedProperties = 0; + var testedClassesAndProperties = new Dictionary>(); + var totalTypesScanned = 0; + + foreach (var (assembly, assemblyName) in AssembliesToScan) + { + var (errors, checkedProperties, testedClasses, typesScanned) = ValidateAssemblyLocalization(assembly); + + // Prefix errors with assembly name for clarity + allErrors.AddRange(errors.Select(e => $"[{assemblyName}] {e}")); + totalCheckedProperties += checkedProperties; + totalTypesScanned += typesScanned; + + foreach (var kvp in testedClasses) + { + testedClassesAndProperties[kvp.Key] = kvp.Value; + } + } + + // Generate and attach the tested classes and properties file + AttachTestedClassesReport(testedClassesAndProperties, totalCheckedProperties, totalTypesScanned, allErrors); + + // Fail the test if there are missing localization strings + if (allErrors.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine($"Found {allErrors.Count} missing localization strings (checked {totalCheckedProperties} properties across {totalTypesScanned} types in {AssembliesToScan.Length} assemblies):"); + sb.AppendLine(); + foreach (var error in allErrors.Take(50)) + { + sb.AppendLine($" - {error}"); + } + if (allErrors.Count > 50) + { + sb.AppendLine($" ... and {allErrors.Count - 50} more errors"); + } + + Assert.Fail(sb.ToString()); + } + + // Ensure we actually checked something + Assert.That(totalCheckedProperties, Is.GreaterThan(0), + "Expected to check at least some properties, but none were found. This may indicate a test setup issue."); + } + + /// + /// Validates localization for a single assembly. + /// + private (List errors, int checkedProperties, Dictionary> testedClasses, int typesScanned) ValidateAssemblyLocalization(Assembly assembly) + { + var errors = new List(); + var checkedProperties = 0; + var testedClassesAndProperties = new Dictionary>(); + + // Find all types that have LocalizedPropertyResources attribute + var typesWithLocalization = assembly.GetTypes() + .Where(t => t.GetCustomAttribute() != null) + .Where(t => !t.IsAbstract || t.IsInterface) + .Where(t => !t.IsEnum) // Enums are handled separately + .OrderBy(t => t.FullName) + .ToList(); + + foreach (var type in typesWithLocalization) + { + var resourceAttr = type.GetCustomAttribute(); + if (resourceAttr == null) + { + continue; + } + + var resourceManager = GetResourceManager(resourceAttr.ResourcesName, assembly); + if (resourceManager == null) + { + errors.Add($"Could not load resource manager for {resourceAttr.ResourcesName} (used by {type.Name})"); + continue; + } + + // Get properties that would be localized + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); + var testedPropertiesForType = new List(); + + foreach (var property in properties) + { + // Skip common infrastructure properties that don't need localization + if (IsInfrastructureProperty(property.Name)) + { + continue; + } + + // Check for explicit display name/description key attributes + var explicitNameKeyAttr = property.GetCustomAttribute(); + var explicitDescKeyAttr = property.GetCustomAttribute(); + + // If type doesn't use default keys and has no explicit keys, skip this property + if (!resourceAttr.UseDefaultKeys && explicitNameKeyAttr == null && explicitDescKeyAttr == null) + { + continue; + } + + checkedProperties++; + testedPropertiesForType.Add(property.Name); + + // Default key pattern: {TypeName}_{PropertyName}Name and {TypeName}_{PropertyName}Desc + var typeName = type.Name; + + // Validate display name key + if (explicitNameKeyAttr != null) + { + var nameValue = resourceManager.GetString(explicitNameKeyAttr.Key); + if (string.IsNullOrEmpty(nameValue)) + { + errors.Add($"{type.Name}.{property.Name}: Missing display name string '{explicitNameKeyAttr.Key}' (explicit key) in {resourceAttr.ResourcesName}"); + } + } + else if (resourceAttr.UseDefaultKeys) + { + var nameKey = $"{typeName}_{property.Name}Name"; + var nameValue = resourceManager.GetString(nameKey); + if (string.IsNullOrEmpty(nameValue)) + { + errors.Add($"{type.Name}.{property.Name}: Missing display name string '{nameKey}' in {resourceAttr.ResourcesName}"); + } + } + + // Validate description key + if (explicitDescKeyAttr != null) + { + var descValue = resourceManager.GetString(explicitDescKeyAttr.Key); + if (string.IsNullOrEmpty(descValue)) + { + errors.Add($"{type.Name}.{property.Name}: Missing description string '{explicitDescKeyAttr.Key}' (explicit key) in {resourceAttr.ResourcesName}"); + } + } + else if (resourceAttr.UseDefaultKeys) + { + var descKey = $"{typeName}_{property.Name}Desc"; + var descValue = resourceManager.GetString(descKey); + if (string.IsNullOrEmpty(descValue)) + { + errors.Add($"{type.Name}.{property.Name}: Missing description string '{descKey}' in {resourceAttr.ResourcesName}"); + } + } + } + + if (testedPropertiesForType.Count > 0) + { + testedClassesAndProperties[type.FullName] = testedPropertiesForType; + } + } + + return (errors, checkedProperties, testedClassesAndProperties, typesWithLocalization.Count); + } + + /// + /// Verifies that all types with LocalizedPropertyResources attribute have valid + /// Name and Description strings for each property that uses the default key pattern. + /// + [TestMethod] + [TestCategory("Unit")] + public void SmoTypes_WithLocalizableTypeConverter_HaveValidPropertyNameAndDescriptionStrings() + { + var smoAssembly = typeof(SmoServer).Assembly; + var (errors, checkedProperties, testedClassesAndProperties, typesScanned) = ValidateAssemblyLocalization(smoAssembly); + + + // Generate and attach the tested classes and properties file + AttachTestedClassesReport(testedClassesAndProperties, checkedProperties, typesScanned, errors); + + // Fail the test if there are missing localization strings + if (errors.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine($"Found {errors.Count} missing localization strings (checked {checkedProperties} properties across {typesScanned} types):"); + sb.AppendLine(); + foreach (var error in errors.Take(50)) + { + sb.AppendLine($" - {error}"); + } + if (errors.Count > 50) + { + sb.AppendLine($" ... and {errors.Count - 50} more errors"); + } + + Assert.Fail(sb.ToString()); + } + + Assert.That(checkedProperties, Is.GreaterThan(0), + "Expected to check at least some properties, but none were found. This may indicate a test setup issue."); + } + + + /// + /// Generates a text file report of all tested classes and properties and attaches it to the test results. + /// + private void AttachTestedClassesReport(Dictionary> testedClassesAndProperties, int totalProperties, int totalTypes, List missingStrings = null) + { + var sb = new StringBuilder(); + sb.AppendLine("============================================================================="); + sb.AppendLine("SMO Localizable Property Resources Test - Tested Classes and Properties"); + sb.AppendLine("============================================================================="); + sb.AppendLine(); + sb.AppendLine($"Generated: {DateTime.UtcNow:o}"); + sb.AppendLine($"Total Types Scanned: {totalTypes}"); + sb.AppendLine($"Total Properties Tested: {totalProperties}"); + sb.AppendLine($"Types with Tested Properties: {testedClassesAndProperties.Count}"); + if (missingStrings != null && missingStrings.Count > 0) + { + sb.AppendLine($"Missing Localization Strings: {missingStrings.Count}"); + } + sb.AppendLine(); + sb.AppendLine("============================================================================="); + sb.AppendLine(); + + // Report missing strings first if any + if (missingStrings != null && missingStrings.Count > 0) + { + sb.AppendLine("MISSING LOCALIZATION STRINGS:"); + sb.AppendLine("-----------------------------"); + foreach (var missing in missingStrings.OrderBy(s => s)) + { + sb.AppendLine($" - {missing}"); + } + sb.AppendLine(); + sb.AppendLine("============================================================================="); + sb.AppendLine(); + } + + sb.AppendLine("TESTED CLASSES AND PROPERTIES:"); + sb.AppendLine("------------------------------"); + sb.AppendLine(); + + foreach (var kvp in testedClassesAndProperties.OrderBy(k => k.Key)) + { + var typeName = kvp.Key; + var properties = kvp.Value; + + sb.AppendLine($"Class: {typeName}"); + sb.AppendLine($" Properties ({properties.Count}):"); + foreach (var prop in properties.OrderBy(p => p)) + { + sb.AppendLine($" - {prop}"); + } + sb.AppendLine(); + } + + sb.AppendLine("============================================================================="); + sb.AppendLine("End of Report"); + sb.AppendLine("============================================================================="); + + // Write to a temporary file and attach to test results + var tempPath = Path.Combine(Path.GetTempPath(), $"LocalizablePropertyResourcesTest_TestedClasses_{DateTime.Now:yyyyMMdd_HHmmss}.txt"); + File.WriteAllText(tempPath, sb.ToString()); + + TestContext?.AddResultFile(tempPath); + } + + /// + /// Verifies that localization string keys follow the expected naming pattern. + /// This catches typos like "Datababase_" or "SServer_". + /// + [TestMethod] + [TestCategory("Unit")] + public void LocalizableResources_StringKeys_FollowNamingConvention() + { + var smoAssembly = typeof(SmoServer).Assembly; + var errors = new List(); + + // Get the main LocalizableResources + var resourceManager = GetResourceManager("Microsoft.SqlServer.Management.Smo.LocalizableResources", smoAssembly); + Assert.That(resourceManager, Is.Not.Null, "Could not load LocalizableResources"); + + // Get all types that could have localized properties + var knownTypeNames = smoAssembly.GetTypes() + .Where(t => !t.IsAbstract || t.IsInterface) + .Select(t => t.Name) + .Concat(new[] { "NamedSmoObject", "ScriptSchemaObjectBase" }) // Base types + .Distinct() + .ToHashSet(System.StringComparer.OrdinalIgnoreCase); + + // Use reflection to get the resource set + var resourceSet = resourceManager.GetResourceSet( + System.Globalization.CultureInfo.InvariantCulture, + createIfNotExists: true, + tryParents: false); + + if (resourceSet != null) + { + foreach (System.Collections.DictionaryEntry entry in resourceSet) + { + var key = entry.Key?.ToString(); + if (string.IsNullOrEmpty(key)) + { + continue; + } + + // Check if key follows pattern {TypeName}_{PropertyName}{Suffix} + var underscoreIndex = key.IndexOf('_'); + if (underscoreIndex > 0) + { + var typePart = key.Substring(0, underscoreIndex); + + // Check if the type part looks like a typo of a known type + if (!knownTypeNames.Contains(typePart)) + { + // Check for common typo patterns + var possibleCorrection = FindSimilarTypeName(typePart, knownTypeNames); + if (possibleCorrection != null) + { + errors.Add($"Possible typo in string key '{key}': '{typePart}' might be '{possibleCorrection}'"); + } + } + } + } + } + + if (errors.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine($"Found {errors.Count} possible typos in localization string keys:"); + sb.AppendLine(); + foreach (var error in errors) + { + sb.AppendLine($" - {error}"); + } + + Assert.Fail(sb.ToString()); + } + } + + /// + /// Verifies that enums with LocalizedPropertyResources attribute have valid DisplayNameKey strings + /// for each enum value. Scans all assemblies. + /// + [TestMethod] + [TestCategory("Unit")] + public void AllEnums_WithDisplayNameKey_HaveValidResourceStrings() + { + var allErrors = new List(); + var totalCheckedEnumValues = 0; + var totalEnumsScanned = 0; + + foreach (var (assembly, assemblyName) in AssembliesToScan) + { + var (errors, checkedEnumValues, enumsScanned) = ValidateEnumLocalization(assembly); + allErrors.AddRange(errors.Select(e => $"[{assemblyName}] {e}")); + totalCheckedEnumValues += checkedEnumValues; + totalEnumsScanned += enumsScanned; + } + + if (allErrors.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine($"Found {allErrors.Count} missing enum display name strings (checked {totalCheckedEnumValues} values across {totalEnumsScanned} enums in {AssembliesToScan.Length} assemblies):"); + sb.AppendLine(); + foreach (var error in allErrors) + { + sb.AppendLine($" - {error}"); + } + + Assert.Fail(sb.ToString()); + } + + TestContext?.WriteLine($"Checked {totalCheckedEnumValues} enum values across {totalEnumsScanned} enums in {AssembliesToScan.Length} assemblies. All display name strings found."); + } + + /// + /// Verifies that enums with LocalizedPropertyResources attribute have valid DisplayNameKey strings + /// for each enum value. SMO-only version for backwards compatibility. + /// + [TestMethod] + [TestCategory("Unit")] + public void SmoEnums_WithDisplayNameKey_HaveValidResourceStrings() + { + var smoAssembly = typeof(SmoServer).Assembly; + var (errors, checkedEnumValues, enumsScanned) = ValidateEnumLocalization(smoAssembly); + + if (errors.Count > 0) + { + var sb = new StringBuilder(); + sb.AppendLine($"Found {errors.Count} missing enum display name strings (checked {checkedEnumValues} values across {enumsScanned} enums):"); + sb.AppendLine(); + foreach (var error in errors) + { + sb.AppendLine($" - {error}"); + } + + Assert.Fail(sb.ToString()); + } + + TestContext?.WriteLine($"Checked {checkedEnumValues} enum values across {enumsScanned} enums. All display name strings found."); + } + + /// + /// Validates enum localization for a single assembly. + /// + private (List errors, int checkedEnumValues, int enumsScanned) ValidateEnumLocalization(Assembly assembly) + { + var errors = new List(); + var checkedEnumValues = 0; + + // Find all enum types that have LocalizedPropertyResources attribute + var enumsWithLocalization = assembly.GetTypes() + .Where(t => t.IsEnum) + .Where(t => t.GetCustomAttribute() != null) + .OrderBy(t => t.FullName) + .ToList(); + + foreach (var enumType in enumsWithLocalization) + { + var resourceAttr = enumType.GetCustomAttribute(); + if (resourceAttr == null) + { + continue; + } + + var resourceManager = GetResourceManager(resourceAttr.ResourcesName, assembly); + if (resourceManager == null) + { + errors.Add($"Could not load resource manager for {resourceAttr.ResourcesName} (used by {enumType.Name})"); + continue; + } + + // Check each enum value for DisplayNameKey attribute + foreach (var fieldName in Enum.GetNames(enumType)) + { + var fieldInfo = enumType.GetField(fieldName); + var displayNameAttr = fieldInfo?.GetCustomAttribute(); + + if (displayNameAttr != null) + { + checkedEnumValues++; + var resourceValue = resourceManager.GetString(displayNameAttr.Key); + if (string.IsNullOrEmpty(resourceValue)) + { + errors.Add($"{enumType.Name}.{fieldName}: Missing display name string '{displayNameAttr.Key}' in {resourceAttr.ResourcesName}"); + } + } + } + } + + return (errors, checkedEnumValues, enumsWithLocalization.Count); + } + + private static ResourceManager GetResourceManager(string resourcesName, Assembly assembly) + { + if (ResourceManagerCache.TryGetValue(resourcesName, out var cached)) + { + return cached; + } + + try + { + var rm = new ResourceManager(resourcesName, assembly); + // Force load to validate + _ = rm.GetResourceSet(System.Globalization.CultureInfo.InvariantCulture, true, false); + ResourceManagerCache[resourcesName] = rm; + return rm; + } + catch + { + return null; + } + } + + /// + /// Common infrastructure properties that don't need localization strings. + /// These are typically inherited from base classes or are internal infrastructure. + /// + private static readonly HashSet InfrastructurePropertyNames = new HashSet(System.StringComparer.OrdinalIgnoreCase) + { + "Parent", + "Events", + "ExtendedProperties", + "State", + "Urn", + "Properties", + "UserData", + "Metadata", + "AbstractIdentityKey", + "IdentityKey", + "ExecutionManager", + "ObjectInSpace", + "IsDesignMode" + }; + + private static bool IsInfrastructureProperty(string propertyName) + => InfrastructurePropertyNames.Contains(propertyName); + + private static string FindSimilarTypeName(string typePart, HashSet knownTypeNames) + { + // Check for common typo patterns + foreach (var knownName in knownTypeNames) + { + // Check for doubled letters (e.g., "Datababase" vs "Database") + if (Math.Abs(typePart.Length - knownName.Length) <= 2) + { + var distance = ComputeLevenshteinDistance(typePart, knownName); + if (distance > 0 && distance <= 2) + { + return knownName; + } + } + + // Check for prefix typos (e.g., "SServer" vs "Server") + if (typePart.Length > knownName.Length && + typePart.EndsWith(knownName, StringComparison.OrdinalIgnoreCase)) + { + return knownName; + } + } + + return null; + } + + private static int ComputeLevenshteinDistance(string s1, string s2) + { + // See https://en.wikipedia.org/wiki/Levenshtein_distance + var n = s1.Length; + var m = s2.Length; + var d = new int[n + 1, m + 1]; + + if (n == 0) return m; + if (m == 0) return n; + + for (var i = 0; i <= n; i++) d[i, 0] = i; + for (var j = 0; j <= m; j++) d[0, j] = j; + + for (var i = 1; i <= n; i++) + { + for (var j = 1; j <= m; j++) + { + var cost = (s2[j - 1] == s1[i - 1]) ? 0 : 1; + d[i, j] = Math.Min( + Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), + d[i - 1, j - 1] + cost); + } + } + + return d[n, m]; + } + } +} diff --git a/src/UnitTest/Smo/SqlObjectTests.cs b/src/UnitTest/Smo/SqlObjectTests.cs index b8321235..626b6ec4 100644 --- a/src/UnitTest/Smo/SqlObjectTests.cs +++ b/src/UnitTest/Smo/SqlObjectTests.cs @@ -154,7 +154,7 @@ public void When_isolation_registry_valid_BuildSql_returns_script_with_isolation public void SqlPropertMetadataProvider_implementations_have_correct_number_of_versions() { // VBUMP - int standaloneVersionCount = new[] { 7.0, 8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0 }.Length; + int standaloneVersionCount = new[] { 7.0, 8.0, 9.0, 10.0, 10.5, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0 }.Length; int cloudVersionCount = new[] { 10.0, 11.0, 12.0 }.Length; var metadataType = typeof(SqlPropertyMetadataProvider); var providers = metadataType.Assembly.GetTypes().Where(t => t != metadataType && metadataType.IsAssignableFrom(t)).ToArray(); diff --git a/src/UnitTest/Smo/SqlSmoObjectTests.cs b/src/UnitTest/Smo/SqlSmoObjectTests.cs index e44963c8..ed29c2b9 100644 --- a/src/UnitTest/Smo/SqlSmoObjectTests.cs +++ b/src/UnitTest/Smo/SqlSmoObjectTests.cs @@ -121,6 +121,7 @@ public class SqlSmoObjectTests : UnitTestBase "AutoClose", "AutoCreateIncrementalStatisticsEnabled", "AutoCreateStatisticsEnabled", + nameof(Database.AutomaticIndexCompactionEnabled), "AutoUpdateStatisticsAsync", "AutoUpdateStatisticsEnabled", "BrokerEnabled", diff --git a/src/UnitTest/Smo/UserTests.cs b/src/UnitTest/Smo/UserTests.cs new file mode 100644 index 00000000..888a2516 --- /dev/null +++ b/src/UnitTest/Smo/UserTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.Specialized; +using System.Linq; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NUnit.Framework; +using Assert = NUnit.Framework.Assert; + +namespace Microsoft.SqlServer.Test.SmoUnitTests +{ + /// + /// Tests for the User object + /// + [TestClass] + [TestCategory("Unit")] + public class UserTests : UnitTestBase + { + /// + /// Verifies that scripting a User with LoginType.WindowsGroup using ScriptAlter + /// produces the expected valid script output. + /// + [TestMethod] + [DataRow("dbo", "[dbo]")] + [DataRow("", "NULL")] + public void User_ScriptAlter_WindowsGroup_ScriptsCorrectly(string defaultSchema, string expectedDefaultSchema) + { + // Create a design-mode server (SQL Server 2012+ to support WindowsGroup with DefaultSchema) + var server = ServerTests.GetDesignModeServer(11); + + var database = new Database(server, "TestDatabase"); + var user = new User(database, "TestWindowsGroupUser"); + + // Set LoginType to WindowsGroup using the IAlienObject interface + // since LoginType is a read-only property + ((IAlienObject)user).SetPropertyValue( + nameof(User.LoginType), + typeof(LoginType), + LoginType.WindowsGroup); + + // Set a Login and DefaultSchema + user.Login = @"DOMAIN\TestGroup"; + user.DefaultSchema = defaultSchema; + + // Call ScriptAlter + var script = new StringCollection(); + var sp = database.GetScriptingPreferencesForCreate(); + user.ScriptAlter(script, sp); + var scriptText = string.Join("\n", script.Cast()); + + Assert.That(scriptText, Is.EqualTo($"ALTER USER [TestWindowsGroupUser] WITH DEFAULT_SCHEMA={expectedDefaultSchema}, LOGIN=[DOMAIN\\TestGroup]")); + } + } +} diff --git a/src/UnitTest/SqlScriptPublish/SqlScriptOptionsTests.cs b/src/UnitTest/SqlScriptPublish/SqlScriptOptionsTests.cs index b637ec7e..89a40d9f 100644 --- a/src/UnitTest/SqlScriptPublish/SqlScriptOptionsTests.cs +++ b/src/UnitTest/SqlScriptPublish/SqlScriptOptionsTests.cs @@ -89,8 +89,13 @@ public void SqlScriptOptions_Enums_have_valid_DisplayNameKey_attributes() [TestCategory("Unit")] public void Compat_verify_enum_attribute_versions() { + // Version 18 + var attr = CompatibilityLevelSupportedVersionAttribute.GetAttributeForOption(ScriptCompatibilityOptions.Script180Compat); + Assert.That(attr.MinimumMajorVersion, Is.EqualTo(18)); + Assert.That(attr.MinimumMinorVersion, Is.EqualTo(0)); + // Version 17 - var attr = CompatibilityLevelSupportedVersionAttribute.GetAttributeForOption(ScriptCompatibilityOptions.Script170Compat); + attr = CompatibilityLevelSupportedVersionAttribute.GetAttributeForOption(ScriptCompatibilityOptions.Script170Compat); Assert.That(attr.MinimumMajorVersion, Is.EqualTo(17)); Assert.That(attr.MinimumMinorVersion, Is.EqualTo(0)); @@ -144,8 +149,17 @@ public void Compat_verify_enum_attribute_versions() [TestCategory("Unit")] public void Compat_GetOptionForVersion_returns_correct_options() { + // VBUMP + // Version 18 + var option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(18); + Assert.That(option.Value, Is.EqualTo(ScriptCompatibilityOptions.Script180Compat)); + + // Version 19 - Non-existent version (update when VBUMP) + option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(19); + Assert.That(option, Is.Null); + // Version 17 - var option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(17); + option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(17); Assert.That(option.Value, Is.EqualTo(ScriptCompatibilityOptions.Script170Compat)); // Version 16 @@ -188,10 +202,6 @@ public void Compat_GetOptionForVersion_returns_correct_options() option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(8); Assert.That(option, Is.Null); - // Version 18 - Non-existent version - option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(18); - Assert.That(option, Is.Null); - // Edge case for minor version-handling option = CompatibilityLevelSupportedVersionAttribute.GetOptionForVersion(1, 50); Assert.That(option, Is.Null); @@ -206,9 +216,15 @@ public void Compat_FilterUnsupportedOptions_removes_unsupported_options() var initialCount = optionsList.Count; + // VBUMP + // Version 18 + optionsList = CompatibilityLevelSupportedVersionAttribute.FilterUnsupportedOptions(optionsList, 18, 0); + Assert.That(optionsList.Count, Is.EqualTo(initialCount)); + // Version 17 optionsList = CompatibilityLevelSupportedVersionAttribute.FilterUnsupportedOptions(optionsList, 17, 0); - Assert.That(optionsList.Count, Is.EqualTo(initialCount)); + Assert.That(optionsList.Count, Is.EqualTo(--initialCount)); + Assert.That(optionsList, Does.Not.Contain(ScriptCompatibilityOptions.Script180Compat)); // Version 16 optionsList = CompatibilityLevelSupportedVersionAttribute.FilterUnsupportedOptions(optionsList, 16, 0);