From 5f9d803a08b427c3b3faa79754bad66aaf944e6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:28:24 +0000 Subject: [PATCH 1/5] Add YamlDotNet 17.0.1 package reference Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/7cd0b300-31cf-4f75-8735-a88cbf873b48 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- src/SqlChangeTracker/SqlChangeTracker.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SqlChangeTracker/SqlChangeTracker.csproj b/src/SqlChangeTracker/SqlChangeTracker.csproj index 63349cd..5911284 100644 --- a/src/SqlChangeTracker/SqlChangeTracker.csproj +++ b/src/SqlChangeTracker/SqlChangeTracker.csproj @@ -20,6 +20,7 @@ + From 231368095b12865f74d197086ea93b342cfbce37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:33:09 +0000 Subject: [PATCH 2/5] Replace JSON config with YAML config (sqlct.config.yaml) Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/7cd0b300-31cf-4f75-8735-a88cbf873b48 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- CHANGELOG.md | 6 + specs/02-config.md | 80 ++++++------- specs/12-project-plan.md | 4 +- src/SqlChangeTracker/Commands/InitCommand.cs | 4 +- src/SqlChangeTracker/Config/Constants.cs | 3 +- .../Config/SqlctConfigReader.cs | 40 +++++-- .../Config/SqlctConfigWriter.cs | 29 +++-- .../Commands/DataCommandTests.cs | 20 ++-- .../Config/SqlctConfigWriterTests.cs | 113 +++++++++++------- .../Sync/SyncCommandServiceTests.cs | 37 +++--- 10 files changed, 204 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43de7e4..2a6067e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Changed +- Replace `sqlct.config.json` with `sqlct.config.yaml` as the project configuration file; config files are now written in YAML with a header comment block containing a tool introduction, installation instructions, usage instructions, and a link to the GitHub repository. + +### Removed +- `sqlct.config.json` is no longer created by `sqlct init`; existing users should rename the file to `sqlct.config.yaml` and run `sqlct config` to rewrite it in the new format. + ## [0.3.0] - 2026-04-12 ### Fixed diff --git a/specs/02-config.md b/specs/02-config.md index 0d86185..d4de040 100644 --- a/specs/02-config.md +++ b/specs/02-config.md @@ -1,53 +1,47 @@ # Config Status: draft -Last updated: 2026-04-06 +Last updated: 2026-04-13 ## Authoritative Configuration -`sqlct.config.json` is the authoritative configuration file for `sqlct` projects. +`sqlct.config.yaml` is the authoritative configuration file for `sqlct` projects. - Required for baseline `sqlct` workflows. - Stores database connection settings and tool options. - All command behavior is resolved from this file (with CLI overrides where supported). ## Schema -The `sqlct.config.json` structure defines the current contract. +The `sqlct.config.yaml` structure defines the current contract. Current shape: -```json -{ - "database": { - "server": "localhost", - "name": "MyDb", - "auth": "integrated", - "user": "", - "password": "", - "trustServerCertificate": false - }, - "options": { - "parallelism": 0 - }, - "data": { - "trackedTables": [ - "dbo.Customer", - "Sales.SalesOrderHeader" - ] - } -} +```yaml +database: + server: localhost + name: MyDb + auth: integrated + user: '' + password: '' + trustServerCertificate: false +options: + parallelism: 0 +data: + trackedTables: + - dbo.Customer + - Sales.SalesOrderHeader ``` ## Init Behavior -`sqlct init` initializes `sqlct.config.json` in the target project directory. -- Writes default config when missing. +`sqlct init` initializes `sqlct.config.yaml` in the target project directory. +- Writes default config when missing. The written file begins with a header comment block containing a short intro, installation instructions, usage instructions, and a link to the GitHub repository. - Preserves existing compatibility files if present. ## Config Command Behavior `sqlct config` parses, validates, and writes configuration from the project directory. -- Requires `sqlct.config.json` to already exist in the project directory. If the file is missing, `config` exits with code `2` (`invalid config`) and prints: `Error: project directory is not initialized.` with hint `run \`sqlct init\` first.` No file is created. -- Validates `sqlct.config.json` as the primary source. +- Requires `sqlct.config.yaml` to already exist in the project directory. If the file is missing, `config` exits with code `2` (`invalid config`) and prints: `Error: project directory is not initialized.` with hint `run \`sqlct init\` first.` No file is created. +- Validates `sqlct.config.yaml` as the primary source. - Detects optional compatibility file presence for summary output only. -- Writes normalized configuration back to `sqlct.config.json`. +- Writes normalized configuration back to `sqlct.config.yaml`. The header comment block is regenerated on every write; any user-added comments inside the file are not preserved. - Deprecated fields are tolerated on read and omitted on rewrite. Deprecated runtime fields removed from v1 contract: @@ -56,7 +50,7 @@ Deprecated runtime fields removed from v1 contract: - `options.comparison.*` ## File Handling Policy -- `sqlct.config.json`: required; read/write by `init` and `config`. +- `sqlct.config.yaml`: required; read/write by `init` and `config`. - External compatibility files: optional; presence may be scanned/reported; files are preserved as-is. - Do not fail baseline workflows solely because optional external compatibility files are missing. @@ -67,17 +61,14 @@ Deprecated runtime fields removed from v1 contract: - `sql`: Uses SQL Server Authentication. `user` is required; `password` is optional (empty string is accepted). ### SQL Authentication example -```json -{ - "database": { - "server": "my-server.example.com", - "name": "MyDb", - "auth": "sql", - "user": "my_login", - "password": "my_password", - "trustServerCertificate": false - } -} +```yaml +database: + server: my-server.example.com + name: MyDb + auth: sql + user: my_login + password: my_password + trustServerCertificate: false ``` Validation rules: @@ -96,6 +87,15 @@ Validation rules: - Entries in `data.trackedTables` MUST be unique case-insensitively and persisted in stable sorted order. - When omitted, `data.trackedTables` defaults to an empty array. +## Migration from JSON config +Projects initialized with `sqlct` v0.3.0 or earlier use `sqlct.config.json`. To migrate: + +1. Rename `sqlct.config.json` to `sqlct.config.yaml`. +2. Translate the JSON content to YAML (the field names are identical; the schema is the same). +3. Run `sqlct config` to validate and normalize the migrated file. + +If `sqlct.config.yaml` is absent but `sqlct.config.json` is present in the project directory, `sqlct` exits with a targeted error and migration hint instead of a generic "not initialized" message. + ## External interoperability - Compatibility-file presence may be detected for summary/reporting purposes. - Any future mapping from compatibility files into runtime config is a vNext item. diff --git a/specs/12-project-plan.md b/specs/12-project-plan.md index ec7801e..1312210 100644 --- a/specs/12-project-plan.md +++ b/specs/12-project-plan.md @@ -26,7 +26,7 @@ Status values: `not_started`, `in_progress`, `blocked`, `done`. | Stream | Scope | Status | Notes | | --- | --- | --- | --- | | S1 | CLI foundation and command wiring | done | Command registration and global settings are in place. | -| S2 | Config and init flows | done | `sqlct.config.json` read/write and project seeding are functional. | +| S2 | Config and init flows | done | `sqlct.config.yaml` read/write and project seeding are functional. | | S3 | SQL adapter and schema mapping | done | Active object-type services are integrated into command runtime. | | S3b | Additional object-type activation | done | Active scope covers broker, full-text, XML schema collection, search-property-list, and assembly support needed for current compatibility work. | | S4 | Status/diff engine | done | End-to-end behavior implemented for active object types. | @@ -164,7 +164,7 @@ Status values: `not_started`, `in_progress`, `blocked`, `done`. ## Cross-Cutting Rules - Specs are authoritative over inferred behavior. -- `sqlct.config.json` is the primary runtime configuration source. +- `sqlct.config.yaml` is the primary runtime configuration source. - Local artifacts under `local/` remain untracked. - Keep naming compatibility-neutral in code and docs. - Keep v1 runtime scope constrained to the active object types and simplified config contract. diff --git a/src/SqlChangeTracker/Commands/InitCommand.cs b/src/SqlChangeTracker/Commands/InitCommand.cs index 5e0a718..734353f 100644 --- a/src/SqlChangeTracker/Commands/InitCommand.cs +++ b/src/SqlChangeTracker/Commands/InitCommand.cs @@ -162,7 +162,7 @@ private static ConnectionSetup PromptForConnectionSetup() string? password = null; if (string.Equals(auth, "sql", StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine(" WARNING: password will be stored in plain text in sqlct.config.json."); + Console.WriteLine(" WARNING: password will be stored in plain text in sqlct.config.yaml."); Console.Write(" Username: "); user = Console.ReadLine()?.Trim(); Console.Write(" Password: "); @@ -225,7 +225,7 @@ private static IReadOnlyList GetNextSteps(InitConnectionTestResult? conn return [ - "Edit 'sqlct.config.json' to configure your database connection.", + "Edit 'sqlct.config.yaml' to configure your database connection.", "Run 'sqlct config' to validate your configuration.", "Run 'sqlct pull' to pull the current database schema into your folder.", ]; diff --git a/src/SqlChangeTracker/Config/Constants.cs b/src/SqlChangeTracker/Config/Constants.cs index 9a29c01..433c659 100644 --- a/src/SqlChangeTracker/Config/Constants.cs +++ b/src/SqlChangeTracker/Config/Constants.cs @@ -11,7 +11,8 @@ internal static class ExitCodes internal static class ConfigFileNames { - public const string SqlctConfigFileName = "sqlct.config.json"; + public const string SqlctConfigFileName = "sqlct.config.yaml"; + public const string SqlctConfigLegacyFileName = "sqlct.config.json"; } internal static class ErrorCodes diff --git a/src/SqlChangeTracker/Config/SqlctConfigReader.cs b/src/SqlChangeTracker/Config/SqlctConfigReader.cs index 0d4a07a..28c011a 100644 --- a/src/SqlChangeTracker/Config/SqlctConfigReader.cs +++ b/src/SqlChangeTracker/Config/SqlctConfigReader.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; namespace SqlChangeTracker.Config; @@ -8,6 +10,24 @@ public SqlctConfigReadResult Read(string configPath) { if (!File.Exists(configPath)) { + // Detect legacy JSON config to give a targeted migration hint. + var directory = Path.GetDirectoryName(configPath); + if (directory != null) + { + var legacyPath = Path.Combine(directory, ConfigFileNames.SqlctConfigLegacyFileName); + if (File.Exists(legacyPath)) + { + return SqlctConfigReadResult.Failure( + new ErrorInfo( + ErrorCodes.MissingLink, + "no linked schema folder found.", + File: configPath, + Detail: $"found '{ConfigFileNames.SqlctConfigLegacyFileName}' but '{ConfigFileNames.SqlctConfigFileName}' is required.", + Hint: $"rename '{ConfigFileNames.SqlctConfigLegacyFileName}' to '{ConfigFileNames.SqlctConfigFileName}' and run `sqlct config` to migrate."), + ExitCodes.InvalidConfig); + } + } + return SqlctConfigReadResult.Failure( new ErrorInfo( ErrorCodes.MissingLink, @@ -20,13 +40,13 @@ public SqlctConfigReadResult Read(string configPath) try { - var json = File.ReadAllText(configPath); - var config = JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true - }); + var yaml = File.ReadAllText(configPath); + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + var config = deserializer.Deserialize(yaml); if (config == null) { @@ -38,10 +58,10 @@ public SqlctConfigReadResult Read(string configPath) SqlctConfigNormalizer.Normalize(config); return SqlctConfigReadResult.Ok(config); } - catch (JsonException ex) + catch (YamlException ex) { return SqlctConfigReadResult.Failure( - new ErrorInfo(ErrorCodes.InvalidConfig, "invalid config file.", Detail: $"invalid JSON: {ex.Message}"), + new ErrorInfo(ErrorCodes.InvalidConfig, "invalid config file.", Detail: $"invalid YAML: {ex.Message}"), ExitCodes.InvalidConfig); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) diff --git a/src/SqlChangeTracker/Config/SqlctConfigWriter.cs b/src/SqlChangeTracker/Config/SqlctConfigWriter.cs index 009de39..215031c 100644 --- a/src/SqlChangeTracker/Config/SqlctConfigWriter.cs +++ b/src/SqlChangeTracker/Config/SqlctConfigWriter.cs @@ -1,9 +1,25 @@ -using System.Text.Json; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; namespace SqlChangeTracker.Config; internal sealed class SqlctConfigWriter { + private const string HeaderComment = + "# SQL Change Tracker (sqlct)\n" + + "# https://github.com/ElegantCodeAtelier/sql-change-tracker\n" + + "#\n" + + "# Installation:\n" + + "# dotnet tool install --global sqlct\n" + + "#\n" + + "# Usage:\n" + + "# sqlct init - initialize this project\n" + + "# sqlct config - validate and rewrite configuration\n" + + "# sqlct status - compare database against schema folder\n" + + "# sqlct diff - show textual schema differences\n" + + "# sqlct pull - pull database schema into folder\n" + + "#\n"; + public static string GetDefaultPath(string baseDirectory) => Path.Combine(baseDirectory, ConfigFileNames.SqlctConfigFileName); @@ -24,13 +40,12 @@ public ConfigWriteResult Write(string configPath, SqlctConfig config, bool overw return ConfigWriteResult.Ok(Array.Empty(), new[] { ConfigFileNames.SqlctConfigFileName }); } - var options = new JsonSerializerOptions - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; + var serializer = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); - var payload = JsonSerializer.Serialize(config, options); + var yaml = serializer.Serialize(config); + var payload = HeaderComment + yaml; File.WriteAllText(configPath, payload); return ConfigWriteResult.Ok(new[] { ConfigFileNames.SqlctConfigFileName }, Array.Empty()); diff --git a/tests/SqlChangeTracker.Tests/Commands/DataCommandTests.cs b/tests/SqlChangeTracker.Tests/Commands/DataCommandTests.cs index bfcadf4..3568b20 100644 --- a/tests/SqlChangeTracker.Tests/Commands/DataCommandTests.cs +++ b/tests/SqlChangeTracker.Tests/Commands/DataCommandTests.cs @@ -19,7 +19,7 @@ public void DataTrackCommand_WhenNoMatches_ReturnsSuccessWithoutConfirmation() new DataTrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", [], ["dbo.Customer"], @@ -44,7 +44,7 @@ public void DataTrackCommand_WhenDeclined_ReturnsSuccessWithoutApplying() new DataTrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", ["dbo.Customer", "dbo.Order"], ["dbo.Customer"], @@ -79,7 +79,7 @@ public void DataTrackCommand_WhenConfirmationUnavailable_ReturnsExecutionFailure new DataTrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", ["dbo.Customer"], [], @@ -114,7 +114,7 @@ public void DataUntrackCommand_WhenNoMatches_ReturnsSuccessWithoutConfirmation() new DataUntrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "*.Missing", [], ["dbo.Customer"], @@ -139,7 +139,7 @@ public void DataUntrackCommand_WhenDeclined_WritesPreviewAndDoesNotApply() new DataUntrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", ["dbo.Customer"], ["dbo.Customer", "dbo.Order"], @@ -181,7 +181,7 @@ public void DataTrackCommand_WithJsonFlag_WritesPromptToStdErrAndJsonToStdOut() new DataTrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", ["dbo.Customer"], [], @@ -344,7 +344,7 @@ public void DataTrackCommand_WithObjectOption_PassesObjectPatternToService() new DataTrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", [], [], @@ -370,7 +370,7 @@ public void DataTrackCommand_WithFilterOption_PassesFilterPatternToService() new DataTrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "^dbo\\.", [], [], @@ -435,7 +435,7 @@ public void DataUntrackCommand_WithObjectOption_PassesObjectPatternToService() new DataUntrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "dbo.*", [], [], @@ -461,7 +461,7 @@ public void DataUntrackCommand_WithFilterOption_PassesFilterPatternToService() new DataUntrackPlan( ".\\schema", ".\\schema", - ".\\schema\\sqlct.config.json", + ".\\schema\\sqlct.config.yaml", "^dbo\\.", [], [], diff --git a/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs b/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs index ce3e986..6fe11b5 100644 --- a/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs +++ b/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs @@ -1,5 +1,4 @@ using SqlChangeTracker.Config; -using System.Text.Json; using Xunit; namespace SqlChangeTracker.Tests.Config; @@ -34,6 +33,37 @@ public void Read_WhenConfigMissing_ReturnsMissingLinkWithConfigPath() } } + [Fact] + public void Read_WhenLegacyJsonConfigExists_ReturnsMigrationHint() + { + var tempDir = Path.Combine(Path.GetTempPath(), "sqlct-tests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + + try + { + // Write the old JSON config file (no YAML file present). + var legacyPath = Path.Combine(tempDir, ConfigFileNames.SqlctConfigLegacyFileName); + File.WriteAllText(legacyPath, "{}"); + + var configPath = Path.Combine(tempDir, ConfigFileNames.SqlctConfigFileName); + var reader = new SqlctConfigReader(); + var result = reader.Read(configPath); + + Assert.False(result.Success); + Assert.Equal(ErrorCodes.MissingLink, result.Error!.Code); + Assert.NotNull(result.Error.Hint); + Assert.Contains(ConfigFileNames.SqlctConfigLegacyFileName, result.Error.Hint); + Assert.Contains(ConfigFileNames.SqlctConfigFileName, result.Error.Hint); + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + [Fact] public void Write_CreatesConfigWithDefaults() { @@ -52,12 +82,16 @@ public void Write_CreatesConfigWithDefaults() Assert.Contains(ConfigFileNames.SqlctConfigFileName, result.Created); Assert.True(File.Exists(configPath)); - using var document = JsonDocument.Parse(File.ReadAllText(configPath)); - var root = document.RootElement; - Assert.Equal(string.Empty, root.GetProperty("database").GetProperty("server").GetString()); - Assert.Equal(string.Empty, root.GetProperty("database").GetProperty("name").GetString()); - Assert.Equal("integrated", root.GetProperty("database").GetProperty("auth").GetString()); - Assert.Equal(0, root.GetProperty("data").GetProperty("trackedTables").GetArrayLength()); + var yamlText = File.ReadAllText(configPath); + Assert.StartsWith("#", yamlText); + + // Deserialize to verify values via the reader. + var readResult = new SqlctConfigReader().Read(configPath); + Assert.True(readResult.Success); + Assert.Equal(string.Empty, readResult.Config!.Database.Server); + Assert.Equal(string.Empty, readResult.Config.Database.Name); + Assert.Equal("integrated", readResult.Config.Database.Auth); + Assert.Empty(readResult.Config.Data.TrackedTables); } finally { @@ -77,25 +111,23 @@ public void ReadThenWrite_LegacyConfig_RemovesDeprecatedFields() try { var configPath = SqlctConfigWriter.GetDefaultPath(tempDir); + // Write a YAML config that contains deprecated fields (IgnoreUnmatchedProperties drops them). File.WriteAllText(configPath, """ - { - "database": { - "server": "localhost", - "name": "MyDb", - "auth": "integrated", - "user": "", - "password": "", - "trustServerCertificate": false - }, - "options": { - "includeSchemas": ["dbo"], - "excludeObjects": ["sys.*"], - "orderByDependencies": false, - "comparison": { - "ignoreWhitespace": true - } - } - } + database: + server: localhost + name: MyDb + auth: integrated + user: '' + password: '' + trustServerCertificate: false + options: + includeSchemas: + - dbo + excludeObjects: + - sys.* + orderByDependencies: false + comparison: + ignoreWhitespace: true """); var reader = new SqlctConfigReader(); @@ -106,12 +138,17 @@ public void ReadThenWrite_LegacyConfig_RemovesDeprecatedFields() var write = writer.Write(configPath, read.Config!, overwriteExisting: true); Assert.True(write.Success); - using var document = JsonDocument.Parse(File.ReadAllText(configPath)); - var options = document.RootElement.GetProperty("options"); - Assert.False(options.TryGetProperty("includeSchemas", out _)); - Assert.False(options.TryGetProperty("excludeObjects", out _)); - Assert.False(options.TryGetProperty("comparison", out _)); - Assert.Equal(0, document.RootElement.GetProperty("data").GetProperty("trackedTables").GetArrayLength()); + var yamlText = File.ReadAllText(configPath); + // Deprecated keys must not appear in the rewritten output. + Assert.DoesNotContain("includeSchemas", yamlText); + Assert.DoesNotContain("excludeObjects", yamlText); + Assert.DoesNotContain("orderByDependencies", yamlText); + Assert.DoesNotContain("comparison", yamlText); + + // Re-read and verify the data section is empty. + var readAgain = new SqlctConfigReader().Read(configPath); + Assert.True(readAgain.Success); + Assert.Empty(readAgain.Config!.Data.TrackedTables); } finally { @@ -139,15 +176,10 @@ public void Write_NormalizesTrackedTables() Assert.True(result.Success); - using var document = JsonDocument.Parse(File.ReadAllText(configPath)); - var tracked = document.RootElement - .GetProperty("data") - .GetProperty("trackedTables") - .EnumerateArray() - .Select(item => item.GetString()) - .ToArray(); - - Assert.Equal(new[] { "dbo.Customer", "sales.Order" }, tracked); + // Read back via the reader and verify normalization. + var readResult = new SqlctConfigReader().Read(configPath); + Assert.True(readResult.Success); + Assert.Equal(new[] { "dbo.Customer", "sales.Order" }, readResult.Config!.Data.TrackedTables.ToArray()); } finally { @@ -158,3 +190,4 @@ public void Write_NormalizesTrackedTables() } } } + diff --git a/tests/SqlChangeTracker.Tests/Sync/SyncCommandServiceTests.cs b/tests/SqlChangeTracker.Tests/Sync/SyncCommandServiceTests.cs index be02817..8fa750f 100644 --- a/tests/SqlChangeTracker.Tests/Sync/SyncCommandServiceTests.cs +++ b/tests/SqlChangeTracker.Tests/Sync/SyncCommandServiceTests.cs @@ -745,7 +745,7 @@ public void CollectUnsupportedFolderWarnings_IgnoresSupportedFilesAndNonSqlArtif { var supportedTable = CreateFile(tempDir, Path.Combine("Tables", "dbo.Customer.sql"), "CREATE TABLE dbo.Customer;"); var supportedView = CreateFile(tempDir, Path.Combine("Views", "dbo.CustomerView.sql"), "CREATE VIEW dbo.CustomerView AS SELECT 1;"); - File.WriteAllText(Path.Combine(tempDir, "sqlct.config.json"), "{}"); + File.WriteAllText(Path.Combine(tempDir, "sqlct.config.yaml"), "{}"); File.WriteAllText(Path.Combine(tempDir, "notes.txt"), "ignored"); var warnings = SyncCommandService.CollectUnsupportedFolderWarnings(tempDir, [supportedTable, supportedView]); @@ -2786,25 +2786,22 @@ public void RunStatus_WithIntegratedAuth_PassesAuthValidation() private static void WriteConfigWithAuth(string projectDir, string auth, string? user, string? password) { Directory.CreateDirectory(projectDir); - var configPath = Path.Combine(projectDir, "sqlct.config.json"); - var userLine = user != null ? $""" - "user": "{user}", - """ : string.Empty; - var passwordLine = password != null ? $""" - "password": "{password}", - """ : string.Empty; - File.WriteAllText(configPath, $$""" - { - "database": { - "server": "non-existent-server-for-auth-test", - "name": "TestDb", - "auth": "{{auth}}", - {{userLine}} - {{passwordLine}} - "trustServerCertificate": true - } - } - """); + var configPath = Path.Combine(projectDir, "sqlct.config.yaml"); + var userLine = user != null ? $" user: '{user}'" : string.Empty; + var passwordLine = password != null ? $" password: '{password}'" : string.Empty; + var lines = new List + { + "database:", + " server: non-existent-server-for-auth-test", + " name: TestDb", + $" auth: {auth}", + }; + if (!string.IsNullOrEmpty(userLine)) + lines.Add(userLine); + if (!string.IsNullOrEmpty(passwordLine)) + lines.Add(passwordLine); + lines.Add(" trustServerCertificate: true"); + File.WriteAllText(configPath, string.Join("\n", lines) + "\n"); } private static string CreateFile(string root, string relativePath, string content) From afa2caea7f50ac41cd4140db708415f13ea263e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:51:02 +0000 Subject: [PATCH 3/5] Add Update section to config file header comment Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/f92c9b4d-8105-4480-b94d-da9a2ca01766 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- src/SqlChangeTracker/Config/SqlctConfigWriter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SqlChangeTracker/Config/SqlctConfigWriter.cs b/src/SqlChangeTracker/Config/SqlctConfigWriter.cs index 215031c..7f67940 100644 --- a/src/SqlChangeTracker/Config/SqlctConfigWriter.cs +++ b/src/SqlChangeTracker/Config/SqlctConfigWriter.cs @@ -12,6 +12,9 @@ internal sealed class SqlctConfigWriter "# Installation:\n" + "# dotnet tool install --global sqlct\n" + "#\n" + + "# Update:\n" + + "# dotnet tool update --global sqlct\n" + + "#\n" + "# Usage:\n" + "# sqlct init - initialize this project\n" + "# sqlct config - validate and rewrite configuration\n" + From 5f0b91d1323158ff484c789e889489e12c03c64f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:56:18 +0000 Subject: [PATCH 4/5] Add help hint to config file header comment Usage section Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/f0e2ad31-f73d-4c0a-b612-146713b3cdd7 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- src/SqlChangeTracker/Config/SqlctConfigWriter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SqlChangeTracker/Config/SqlctConfigWriter.cs b/src/SqlChangeTracker/Config/SqlctConfigWriter.cs index 7f67940..b140eec 100644 --- a/src/SqlChangeTracker/Config/SqlctConfigWriter.cs +++ b/src/SqlChangeTracker/Config/SqlctConfigWriter.cs @@ -16,6 +16,7 @@ internal sealed class SqlctConfigWriter "# dotnet tool update --global sqlct\n" + "#\n" + "# Usage:\n" + + "# sqlct --help - print help\n" + "# sqlct init - initialize this project\n" + "# sqlct config - validate and rewrite configuration\n" + "# sqlct status - compare database against schema folder\n" + From 420c28efb4b7f2f10438d06d3e588f6bd534c2f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 09:04:12 +0000 Subject: [PATCH 5/5] Remove legacy sqlct.config.json support entirely Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/8b343fb7-7e4b-498c-88f8-797195bafa4c Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- CHANGELOG.md | 2 +- specs/02-config.md | 9 ------ src/SqlChangeTracker/Config/Constants.cs | 1 - .../Config/SqlctConfigReader.cs | 18 ----------- .../Config/SqlctConfigWriterTests.cs | 31 ------------------- 5 files changed, 1 insertion(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a6067e..5f99530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Replace `sqlct.config.json` with `sqlct.config.yaml` as the project configuration file; config files are now written in YAML with a header comment block containing a tool introduction, installation instructions, usage instructions, and a link to the GitHub repository. ### Removed -- `sqlct.config.json` is no longer created by `sqlct init`; existing users should rename the file to `sqlct.config.yaml` and run `sqlct config` to rewrite it in the new format. +- `sqlct.config.json` is no longer supported; existing users should rename the file to `sqlct.config.yaml` and run `sqlct config` to rewrite it in the new format. ## [0.3.0] - 2026-04-12 diff --git a/specs/02-config.md b/specs/02-config.md index d4de040..efe1e84 100644 --- a/specs/02-config.md +++ b/specs/02-config.md @@ -87,15 +87,6 @@ Validation rules: - Entries in `data.trackedTables` MUST be unique case-insensitively and persisted in stable sorted order. - When omitted, `data.trackedTables` defaults to an empty array. -## Migration from JSON config -Projects initialized with `sqlct` v0.3.0 or earlier use `sqlct.config.json`. To migrate: - -1. Rename `sqlct.config.json` to `sqlct.config.yaml`. -2. Translate the JSON content to YAML (the field names are identical; the schema is the same). -3. Run `sqlct config` to validate and normalize the migrated file. - -If `sqlct.config.yaml` is absent but `sqlct.config.json` is present in the project directory, `sqlct` exits with a targeted error and migration hint instead of a generic "not initialized" message. - ## External interoperability - Compatibility-file presence may be detected for summary/reporting purposes. - Any future mapping from compatibility files into runtime config is a vNext item. diff --git a/src/SqlChangeTracker/Config/Constants.cs b/src/SqlChangeTracker/Config/Constants.cs index 433c659..aed0185 100644 --- a/src/SqlChangeTracker/Config/Constants.cs +++ b/src/SqlChangeTracker/Config/Constants.cs @@ -12,7 +12,6 @@ internal static class ExitCodes internal static class ConfigFileNames { public const string SqlctConfigFileName = "sqlct.config.yaml"; - public const string SqlctConfigLegacyFileName = "sqlct.config.json"; } internal static class ErrorCodes diff --git a/src/SqlChangeTracker/Config/SqlctConfigReader.cs b/src/SqlChangeTracker/Config/SqlctConfigReader.cs index 28c011a..7c46704 100644 --- a/src/SqlChangeTracker/Config/SqlctConfigReader.cs +++ b/src/SqlChangeTracker/Config/SqlctConfigReader.cs @@ -10,24 +10,6 @@ public SqlctConfigReadResult Read(string configPath) { if (!File.Exists(configPath)) { - // Detect legacy JSON config to give a targeted migration hint. - var directory = Path.GetDirectoryName(configPath); - if (directory != null) - { - var legacyPath = Path.Combine(directory, ConfigFileNames.SqlctConfigLegacyFileName); - if (File.Exists(legacyPath)) - { - return SqlctConfigReadResult.Failure( - new ErrorInfo( - ErrorCodes.MissingLink, - "no linked schema folder found.", - File: configPath, - Detail: $"found '{ConfigFileNames.SqlctConfigLegacyFileName}' but '{ConfigFileNames.SqlctConfigFileName}' is required.", - Hint: $"rename '{ConfigFileNames.SqlctConfigLegacyFileName}' to '{ConfigFileNames.SqlctConfigFileName}' and run `sqlct config` to migrate."), - ExitCodes.InvalidConfig); - } - } - return SqlctConfigReadResult.Failure( new ErrorInfo( ErrorCodes.MissingLink, diff --git a/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs b/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs index 6fe11b5..eb428ad 100644 --- a/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs +++ b/tests/SqlChangeTracker.Tests/Config/SqlctConfigWriterTests.cs @@ -33,37 +33,6 @@ public void Read_WhenConfigMissing_ReturnsMissingLinkWithConfigPath() } } - [Fact] - public void Read_WhenLegacyJsonConfigExists_ReturnsMigrationHint() - { - var tempDir = Path.Combine(Path.GetTempPath(), "sqlct-tests", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(tempDir); - - try - { - // Write the old JSON config file (no YAML file present). - var legacyPath = Path.Combine(tempDir, ConfigFileNames.SqlctConfigLegacyFileName); - File.WriteAllText(legacyPath, "{}"); - - var configPath = Path.Combine(tempDir, ConfigFileNames.SqlctConfigFileName); - var reader = new SqlctConfigReader(); - var result = reader.Read(configPath); - - Assert.False(result.Success); - Assert.Equal(ErrorCodes.MissingLink, result.Error!.Code); - Assert.NotNull(result.Error.Hint); - Assert.Contains(ConfigFileNames.SqlctConfigLegacyFileName, result.Error.Hint); - Assert.Contains(ConfigFileNames.SqlctConfigFileName, result.Error.Hint); - } - finally - { - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } - } - [Fact] public void Write_CreatesConfigWithDefaults() {