From 0ad35e84e180b5b42ae15efa9553854070da8024 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:00:14 +0000 Subject: [PATCH 1/4] Initial plan From f52cd520dc90afa58309696737c36e611fa9b86c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:07:31 +0000 Subject: [PATCH 2/4] feat: init scans Data/*.sql files and proposes trackedTables Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/4f486c6b-8ec2-405b-b0e5-d974c7798ac3 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- CHANGELOG.md | 1 + src/SqlChangeTracker/Commands/InitCommand.cs | 65 +++++- src/SqlChangeTracker/Config/Models.cs | 3 +- .../Output/OutputFormatter.cs | 12 +- .../Commands/InitAndConfigCommandTests.cs | 188 ++++++++++++++++++ 5 files changed, 266 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c3cf7d..8c61e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Trailing semicolon differences on `INSERT` statement lines in data scripts are now suppressed during comparison normalization; scripts emitted with and without statement terminators compare as compatible (#47). ### Added +- `sqlct init` now scans the target project directory for existing `Data/*.sql` files, extracts table names from the file names, and proposes a `trackedTables` list: in interactive mode the user is prompted to confirm before the tables are written to config; in non-interactive mode (with `--project-dir`) the tables are added automatically. - `sqlct init` now prompts interactively for connection details (server, database, auth, credentials, trust-server-certificate) when run without flags in a new project directory (#36). - Connection flags (`--server`, `--database`, `--auth`, `--user`, `--password`, `--trust-server-certificate`) for non-interactive/scripted `init` use (#36). - `--skip-connection-test` flag for `sqlct init` to bypass the connection test step (#36). diff --git a/src/SqlChangeTracker/Commands/InitCommand.cs b/src/SqlChangeTracker/Commands/InitCommand.cs index 175b942..215f95f 100644 --- a/src/SqlChangeTracker/Commands/InitCommand.cs +++ b/src/SqlChangeTracker/Commands/InitCommand.cs @@ -3,6 +3,7 @@ using SqlChangeTracker.Config; using SqlChangeTracker.Output; using SqlChangeTracker.Sql; +using SqlChangeTracker.Sync; namespace SqlChangeTracker.Commands; @@ -85,6 +86,29 @@ public override int Execute(CommandContext context, InitCommandSettings settings } var config = BuildConfig(connectionSetup); + + // Scan for existing Data/*.sql files and propose trackedTables. + var proposedTables = ScanDataFileTableNames(projectDir); + IReadOnlyList? addedTrackedTables = null; + if (proposedTables.Count > 0) + { + bool addTrackedTables; + if (isInteractive) + { + addTrackedTables = ConfirmTrackedTables(proposedTables); + } + else + { + addTrackedTables = true; + } + + if (addTrackedTables) + { + config.Data.TrackedTables.AddRange(proposedTables); + addedTrackedTables = proposedTables; + } + } + var configWriter = new SqlctConfigWriter(); var configPath = SqlctConfigWriter.GetDefaultPath(projectDir); var configResult = configWriter.Write(configPath, config); @@ -98,7 +122,7 @@ public override int Execute(CommandContext context, InitCommandSettings settings var created = projectSeedResult.Created.Concat(configResult.Created).ToList(); var skipped = projectSeedResult.Skipped.Concat(configResult.Skipped).ToList(); - var result = new InitResult("init", displayProjectDir, created, skipped, connectionTestResult, nextSteps); + var result = new InitResult("init", displayProjectDir, created, skipped, connectionTestResult, nextSteps, addedTrackedTables); output.WriteInit(result); return ExitCodes.Success; } @@ -286,6 +310,45 @@ private static string ReadPassword() return password.ToString(); } + private static IReadOnlyList ScanDataFileTableNames(string projectDir) + { + var dataDir = Path.Combine(projectDir, "Data"); + if (!Directory.Exists(dataDir)) + { + return []; + } + + var tables = new List(); + foreach (var file in Directory.GetFiles(dataDir, "*.sql", SearchOption.TopDirectoryOnly)) + { + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); + if (SyncCommandService.TryParseDataFileName(fileNameWithoutExtension, out var schema, out var name)) + { + tables.Add($"{schema}.{name}"); + } + } + + return tables + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(t => t, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static bool ConfirmTrackedTables(IReadOnlyList proposedTables) + { + Console.WriteLine(); + Console.WriteLine($"Found {proposedTables.Count} existing Data/*.sql file(s). Proposed trackedTables:"); + foreach (var table in proposedTables) + { + Console.WriteLine($" - {table}"); + } + Console.Write("Add these tables to trackedTables in config? [Y/n]: "); + var response = Console.ReadLine()?.Trim() ?? string.Empty; + return string.IsNullOrWhiteSpace(response) + || string.Equals(response, "y", StringComparison.OrdinalIgnoreCase) + || string.Equals(response, "yes", StringComparison.OrdinalIgnoreCase); + } + private static bool ConfirmCurrentDirectory(string displayProjectDir) { Console.Write($"Initialize project in current directory '{displayProjectDir}'? [y/N]: "); diff --git a/src/SqlChangeTracker/Config/Models.cs b/src/SqlChangeTracker/Config/Models.cs index d018be7..cce5b78 100644 --- a/src/SqlChangeTracker/Config/Models.cs +++ b/src/SqlChangeTracker/Config/Models.cs @@ -15,7 +15,8 @@ internal sealed record InitResult( IReadOnlyList Created, IReadOnlyList Skipped, InitConnectionTestResult? ConnectionTest = null, - IReadOnlyList? NextSteps = null); + IReadOnlyList? NextSteps = null, + IReadOnlyList? TrackedTables = null); internal sealed record InitConnectionTestResult( bool Success, diff --git a/src/SqlChangeTracker/Output/OutputFormatter.cs b/src/SqlChangeTracker/Output/OutputFormatter.cs index 467e0a5..25ef564 100644 --- a/src/SqlChangeTracker/Output/OutputFormatter.cs +++ b/src/SqlChangeTracker/Output/OutputFormatter.cs @@ -84,7 +84,8 @@ public void WriteInit(InitResult result) connectionTest = result.ConnectionTest != null ? new { result.ConnectionTest.Success, result.ConnectionTest.ErrorMessage } : (object?)null, - nextSteps = result.NextSteps + nextSteps = result.NextSteps, + trackedTables = result.TrackedTables }; WriteJson(payload); return; @@ -128,6 +129,15 @@ public void WriteInit(InitResult result) } } + if (result.TrackedTables != null && result.TrackedTables.Count > 0) + { + Console.WriteLine("Tracked tables:"); + foreach (var table in result.TrackedTables) + { + Console.WriteLine($" {table}"); + } + } + if (result.NextSteps != null && result.NextSteps.Count > 0) { Console.WriteLine("Next steps:"); diff --git a/tests/SqlChangeTracker.Tests/Commands/InitAndConfigCommandTests.cs b/tests/SqlChangeTracker.Tests/Commands/InitAndConfigCommandTests.cs index b782b5b..834033f 100644 --- a/tests/SqlChangeTracker.Tests/Commands/InitAndConfigCommandTests.cs +++ b/tests/SqlChangeTracker.Tests/Commands/InitAndConfigCommandTests.cs @@ -330,6 +330,194 @@ public void Config_WithConfigSwitch_ReturnsParsingFailure() } } + [Fact] + public void Init_WithProjectDir_WithExistingDataFiles_AutoIncludesTrackedTables() + { + var tempDir = CreateTempDir(); + + try + { + var projectDir = Path.Combine(tempDir, "project"); + var dataDir = Path.Combine(projectDir, "Data"); + Directory.CreateDirectory(dataDir); + File.WriteAllText(Path.Combine(dataDir, "dbo.Customer_Data.sql"), "-- data"); + File.WriteAllText(Path.Combine(dataDir, "Sales.Order_Data.sql"), "-- data"); + + var exitCode = Program.Main(["init", "--project-dir", projectDir]); + + Assert.Equal(ExitCodes.Success, exitCode); + + var configPath = Path.Combine(projectDir, ConfigFileNames.SqlctConfigFileName); + Assert.True(File.Exists(configPath)); + + var config = new SqlctConfigReader().Read(configPath); + Assert.True(config.Success); + Assert.Contains("dbo.Customer", config.Config!.Data.TrackedTables, StringComparer.OrdinalIgnoreCase); + Assert.Contains("Sales.Order", config.Config!.Data.TrackedTables, StringComparer.OrdinalIgnoreCase); + } + finally + { + CleanupTempDir(tempDir); + } + } + + [Fact] + public void Init_WithProjectDir_WithNoDataFiles_ProducesEmptyTrackedTables() + { + var tempDir = CreateTempDir(); + + try + { + var projectDir = Path.Combine(tempDir, "project"); + + var exitCode = Program.Main(["init", "--project-dir", projectDir]); + + Assert.Equal(ExitCodes.Success, exitCode); + + var configPath = Path.Combine(projectDir, ConfigFileNames.SqlctConfigFileName); + Assert.True(File.Exists(configPath)); + + var config = new SqlctConfigReader().Read(configPath); + Assert.True(config.Success); + Assert.Empty(config.Config!.Data.TrackedTables); + } + finally + { + CleanupTempDir(tempDir); + } + } + + [Fact] + public void Init_WithoutProjectDir_WithExistingDataFiles_WhenConfirmed_IncludesTrackedTables() + { + var tempDir = CreateTempDir(); + var originalCurrentDirectory = Environment.CurrentDirectory; + var originalInput = Console.In; + + try + { + Environment.CurrentDirectory = tempDir; + var dataDir = Path.Combine(tempDir, "Data"); + Directory.CreateDirectory(dataDir); + File.WriteAllText(Path.Combine(dataDir, "dbo.Customer_Data.sql"), "-- data"); + + InitCommand.ConnectionTesterOverride = new StubConnectionTester(true, null); + try + { + // "y" confirms directory; empty lines for connection prompts (defaults); "y" confirms tracked tables. + Console.SetIn(new StringReader( + "y" + Environment.NewLine + // confirm directory + Environment.NewLine + // server → localhost + Environment.NewLine + // database → empty + Environment.NewLine + // auth → integrated + Environment.NewLine + // trust cert → n + "y" + Environment.NewLine)); // confirm tracked tables + + var exitCode = Program.Main(["init"]); + + Assert.Equal(ExitCodes.Success, exitCode); + + var configPath = Path.Combine(tempDir, ConfigFileNames.SqlctConfigFileName); + Assert.True(File.Exists(configPath)); + + var config = new SqlctConfigReader().Read(configPath); + Assert.True(config.Success); + Assert.Contains("dbo.Customer", config.Config!.Data.TrackedTables, StringComparer.OrdinalIgnoreCase); + } + finally + { + InitCommand.ConnectionTesterOverride = null; + } + } + finally + { + Console.SetIn(originalInput); + Environment.CurrentDirectory = originalCurrentDirectory; + CleanupTempDir(tempDir); + } + } + + [Fact] + public void Init_WithoutProjectDir_WithExistingDataFiles_WhenDeclined_ExcludesTrackedTables() + { + var tempDir = CreateTempDir(); + var originalCurrentDirectory = Environment.CurrentDirectory; + var originalInput = Console.In; + + try + { + Environment.CurrentDirectory = tempDir; + var dataDir = Path.Combine(tempDir, "Data"); + Directory.CreateDirectory(dataDir); + File.WriteAllText(Path.Combine(dataDir, "dbo.Customer_Data.sql"), "-- data"); + + InitCommand.ConnectionTesterOverride = new StubConnectionTester(true, null); + try + { + // "y" confirms directory; empty lines for connection prompts (defaults); "n" declines tracked tables. + Console.SetIn(new StringReader( + "y" + Environment.NewLine + // confirm directory + Environment.NewLine + // server → localhost + Environment.NewLine + // database → empty + Environment.NewLine + // auth → integrated + Environment.NewLine + // trust cert → n + "n" + Environment.NewLine)); // decline tracked tables + + var exitCode = Program.Main(["init"]); + + Assert.Equal(ExitCodes.Success, exitCode); + + var configPath = Path.Combine(tempDir, ConfigFileNames.SqlctConfigFileName); + Assert.True(File.Exists(configPath)); + + var config = new SqlctConfigReader().Read(configPath); + Assert.True(config.Success); + Assert.Empty(config.Config!.Data.TrackedTables); + } + finally + { + InitCommand.ConnectionTesterOverride = null; + } + } + finally + { + Console.SetIn(originalInput); + Environment.CurrentDirectory = originalCurrentDirectory; + CleanupTempDir(tempDir); + } + } + + [Fact] + public void Init_WithProjectDir_WithMalformedDataFileNames_SkipsInvalidFiles() + { + var tempDir = CreateTempDir(); + + try + { + var projectDir = Path.Combine(tempDir, "project"); + var dataDir = Path.Combine(projectDir, "Data"); + Directory.CreateDirectory(dataDir); + // Valid file + File.WriteAllText(Path.Combine(dataDir, "dbo.Customer_Data.sql"), "-- data"); + // Malformed files (no _Data suffix or no schema separator) + File.WriteAllText(Path.Combine(dataDir, "NotADataFile.sql"), "-- not data"); + File.WriteAllText(Path.Combine(dataDir, "missingschema_Data.sql"), "-- no dot"); + + var exitCode = Program.Main(["init", "--project-dir", projectDir]); + + Assert.Equal(ExitCodes.Success, exitCode); + + var config = new SqlctConfigReader().Read(SqlctConfigWriter.GetDefaultPath(projectDir)); + Assert.True(config.Success); + Assert.Single(config.Config!.Data.TrackedTables); + Assert.Contains("dbo.Customer", config.Config!.Data.TrackedTables, StringComparer.OrdinalIgnoreCase); + } + finally + { + CleanupTempDir(tempDir); + } + } + private static string CreateTempDir() { var dir = Path.Combine(Path.GetTempPath(), "sqlct-tests", Guid.NewGuid().ToString("N")); From 5512e79b082b698a8836ee879b281ea89f9f68a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:09:56 +0000 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20rename=20ConfirmTrackedTables?= =?UTF-8?q?=20=E2=86=92=20ConfirmAddTrackedTables;=20simplify=20null-count?= =?UTF-8?q?=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/4f486c6b-8ec2-405b-b0e5-d974c7798ac3 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- src/SqlChangeTracker/Commands/InitCommand.cs | 4 ++-- src/SqlChangeTracker/Output/OutputFormatter.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SqlChangeTracker/Commands/InitCommand.cs b/src/SqlChangeTracker/Commands/InitCommand.cs index 215f95f..5e0a718 100644 --- a/src/SqlChangeTracker/Commands/InitCommand.cs +++ b/src/SqlChangeTracker/Commands/InitCommand.cs @@ -95,7 +95,7 @@ public override int Execute(CommandContext context, InitCommandSettings settings bool addTrackedTables; if (isInteractive) { - addTrackedTables = ConfirmTrackedTables(proposedTables); + addTrackedTables = ConfirmAddTrackedTables(proposedTables); } else { @@ -334,7 +334,7 @@ private static IReadOnlyList ScanDataFileTableNames(string projectDir) .ToList(); } - private static bool ConfirmTrackedTables(IReadOnlyList proposedTables) + private static bool ConfirmAddTrackedTables(IReadOnlyList proposedTables) { Console.WriteLine(); Console.WriteLine($"Found {proposedTables.Count} existing Data/*.sql file(s). Proposed trackedTables:"); diff --git a/src/SqlChangeTracker/Output/OutputFormatter.cs b/src/SqlChangeTracker/Output/OutputFormatter.cs index 25ef564..3b36e26 100644 --- a/src/SqlChangeTracker/Output/OutputFormatter.cs +++ b/src/SqlChangeTracker/Output/OutputFormatter.cs @@ -129,7 +129,7 @@ public void WriteInit(InitResult result) } } - if (result.TrackedTables != null && result.TrackedTables.Count > 0) + if (result.TrackedTables?.Count > 0) { Console.WriteLine("Tracked tables:"); foreach (var table in result.TrackedTables) From 31452a401dfb271b82ef3a66094034706adbd360 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:12:49 +0000 Subject: [PATCH 4/4] docs: update specs/01-cli.md, README.md, and PACKAGE_README.md for init Data/*.sql scan Agent-Logs-Url: https://github.com/ElegantCodeAtelier/sql-change-tracker/sessions/a362d428-0579-4862-a8c4-07ac399a1010 Co-authored-by: zacateras <1332231+zacateras@users.noreply.github.com> --- README.md | 1 + specs/01-cli.md | 5 +++++ src/SqlChangeTracker/PACKAGE_README.md | 1 + 3 files changed, 7 insertions(+) diff --git a/README.md b/README.md index afd5f98..4976d48 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ When `data.trackedTables` is configured, `status`, `diff`, and `pull` also proce ## Selective data scripting Tracked-table data scripting is configuration-driven. +- `sqlct init` scans the project directory for existing `Data/*.sql` files and proposes their table names as initial `trackedTables`; in interactive mode you are prompted to confirm, in non-interactive mode (`--project-dir`) they are added automatically. - `sqlct data track` matches user tables in the current database against a positional pattern (`schema.*`, `*.name`, `schema.name`), `--object `, or `--filter `, and asks for confirmation before updating `data.trackedTables`. Exactly one selector must be provided. - `sqlct data untrack` previews tracked matches (same selector forms) and asks for confirmation before removing them. - `sqlct data list` shows the currently tracked tables from config. diff --git a/specs/01-cli.md b/specs/01-cli.md index a7ad25e..5d6a411 100644 --- a/specs/01-cli.md +++ b/specs/01-cli.md @@ -104,6 +104,11 @@ Behavior: - Password input during interactive prompts is masked. - When connection details are collected, attempt a connection test **before** creating any project files (5-second timeout). - If the connection test fails, print troubleshooting hints and prompt `"Proceed anyway? [y/N]:"`. If declined, exit without creating any files. If confirmed, proceed to create the directory structure and write config. +- After seeding the project structure, scan `Data/*.sql` files in the project directory. Files whose names match the `Schema.Table_Data.sql` convention are parsed as `schema.table` candidates for `data.trackedTables`. + - In interactive mode: display the proposed table list and prompt `"Add these tables to trackedTables in config? [Y/n]:"` (default: yes). If confirmed, write the tables to `data.trackedTables` in the config. If declined, leave `data.trackedTables` empty. + - In non-interactive mode (with `--project-dir`): auto-include all discovered tables in `data.trackedTables` without a prompt. + - Files whose names do not match the naming convention are silently ignored. + - If no `Data/*.sql` files are found, `data.trackedTables` remains empty and no prompt is shown. - After init completes, print context-aware next-steps: `pull`, `status`, `diff` on success; edit config and run `sqlct config` on failure. - Exit codes: - `0` success. diff --git a/src/SqlChangeTracker/PACKAGE_README.md b/src/SqlChangeTracker/PACKAGE_README.md index 85f8f24..b15e617 100644 --- a/src/SqlChangeTracker/PACKAGE_README.md +++ b/src/SqlChangeTracker/PACKAGE_README.md @@ -68,6 +68,7 @@ Feature-backed object types are included when the target database exposes them, ## Selective Data Scripting Tracked-table data scripting is configuration-driven. +- `sqlct init` scans the project directory for existing `Data/*.sql` files and proposes their table names as initial `trackedTables`; in interactive mode you are prompted to confirm, in non-interactive mode (`--project-dir`) they are added automatically. - `sqlct data track` matches user tables in the current database against a positional pattern (`schema.*`, `*.name`, `schema.name`), `--object `, or `--filter `, and asks for confirmation before updating `data.trackedTables`. Exactly one selector must be provided. - `sqlct data untrack` previews tracked matches (same selector forms) and asks for confirmation before removing them. - `sqlct data list` shows the currently tracked tables from config.