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()
{