From 865fde2667d219c5e1b9a91e60b976a8b8d9642b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:11:30 +0000 Subject: [PATCH 1/2] Initial plan From 5a1e5bd2460f4faf2c868240274ca33f1d300ccc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:17:38 +0000 Subject: [PATCH 2/2] Add TSqlParser facade and CLI coverage tests Co-authored-by: fupacat <595487+fupacat@users.noreply.github.com> --- .../java/io/github/fupacat/TSqlParser.java | 6 +- .../github/fupacat/TSqlParserFacadeTest.java | 133 +++++++++++++++++- 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/tsql-scriptdom-core/src/main/java/io/github/fupacat/TSqlParser.java b/tsql-scriptdom-core/src/main/java/io/github/fupacat/TSqlParser.java index e251fb3..b341737 100644 --- a/tsql-scriptdom-core/src/main/java/io/github/fupacat/TSqlParser.java +++ b/tsql-scriptdom-core/src/main/java/io/github/fupacat/TSqlParser.java @@ -78,7 +78,7 @@ public static ParseResult parse(Reader reader, SqlVersion version) { @SuppressWarnings("unchecked") public ParseResult parseScript(Reader reader) { List errors = new ArrayList<>(); - TSql180Parser underlying = new TSql180Parser(initialQuotedIdentifiersOn, targetVersion); + TSql180Parser underlying = createUnderlyingParser(); try { TSqlFragment fragment = underlying.parse(reader, errors); TSqlScript script = fragment instanceof TSqlScript s ? s : null; @@ -97,6 +97,10 @@ public ParseResult parseScript(String sql) { return parseScript(new StringReader(sql)); } + TSql180Parser createUnderlyingParser() { + return new TSql180Parser(initialQuotedIdentifiersOn, targetVersion); + } + public boolean isInitialQuotedIdentifiersOn() { return initialQuotedIdentifiersOn; } diff --git a/tsql-scriptdom-core/src/test/java/io/github/fupacat/TSqlParserFacadeTest.java b/tsql-scriptdom-core/src/test/java/io/github/fupacat/TSqlParserFacadeTest.java index 96e7bce..a4cce7a 100644 --- a/tsql-scriptdom-core/src/test/java/io/github/fupacat/TSqlParserFacadeTest.java +++ b/tsql-scriptdom-core/src/test/java/io/github/fupacat/TSqlParserFacadeTest.java @@ -2,16 +2,25 @@ import io.github.fupacat.ast.SelectStatement; import io.github.fupacat.ast.TSqlConcreteFragmentVisitor; +import io.github.fupacat.ast.TSqlFragment; import io.github.fupacat.ast.TSqlScript; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; public class TSqlParserFacadeTest { + // TRACEABILITY_MAPPING: REQ-028 @Test public void testParseSimpleValidScript() { String sql = "SELECT 1 AS num, 'hello' AS msg FROM dbo.TestTable WHERE id > 10;"; @@ -32,6 +41,7 @@ public void visit(SelectStatement statement) { assertThat(selectCount.get()).isEqualTo(1); } + // TRACEABILITY_MAPPING: REQ-028 @Test public void testParseInvalidScriptProducesErrors() { String sql = "SELECT FROM WHERE;"; @@ -42,6 +52,7 @@ public void testParseInvalidScriptProducesErrors() { assertThat(result.errors()).isNotEmpty(); } + // TRACEABILITY_MAPPING: REQ-028 @Test public void testParseWithVersionCompatibilityCheck() { // WINDOW clause was introduced in SQL Server 2022 (SQL160). @@ -58,8 +69,9 @@ public void testParseWithVersionCompatibilityCheck() { assertThat(legacyResult.errors().get(0).message).contains("WINDOW clause"); } + // TRACEABILITY_MAPPING: REQ-028 @Test - public void testBuilderAndReader() { + public void testBuilderAndReaderParseScriptString() { TSqlParser parser = TSqlParser.builder() .initialQuotedIdentifiers(true) .sqlVersion(SqlVersion.SQL160) @@ -71,4 +83,123 @@ public void testBuilderAndReader() { ParseResult result = parser.parseScript(new StringReader("PRINT 'OK';")); assertThat(result.isSuccess()).isTrue(); } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testReaderOverloadsUseDefaultAndTargetVersion() { + ParseResult defaultVersionResult = TSqlParser.parse(new StringReader("SELECT 1;")); + ParseResult explicitVersionResult = TSqlParser.parse(new StringReader("SELECT 1;"), SqlVersion.SQL150); + + assertThat(defaultVersionResult.isSuccess()).isTrue(); + assertThat(explicitVersionResult.isSuccess()).isTrue(); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testBuilderFluentMethodsAndNullVersionFallback() { + TSqlParser parser = TSqlParser.builder() + .initialQuotedIdentifiers(false) + .sqlVersion(null) + .build(); + + assertThat(parser.isInitialQuotedIdentifiersOn()).isFalse(); + assertThat(parser.getTargetVersion()).isEqualTo(SqlVersion.SQL180); + assertThat(parser.parseScript("SELECT 1;").isSuccess()).isTrue(); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testParseScriptHandlesReaderExceptions() { + TSqlParser parser = new TSqlParser() { + @Override + TSql180Parser createUnderlyingParser() { + return new TSql180Parser() { + @Override + public TSqlFragment parse(java.io.Reader reader, List errors) { + throw new RuntimeException("boom"); + } + }; + } + }; + + ParseResult result = parser.parseScript(new StringReader("SELECT 1;")); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.fragment()).isNull(); + assertThat(result.errors()).isNotEmpty(); + assertThat(result.errors().get(0).message).contains("Parsing exception: boom"); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testMainShowsHelp() throws Exception { + MainExecution execution = runMain("-h"); + + assertThat(execution.exitCode()).isZero(); + assertThat(execution.stdout()).contains("Usage:"); + assertThat(execution.stderr()).isBlank(); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testMainParsesRawSqlString() throws Exception { + MainExecution execution = runMain("SELECT", "1;"); + + assertThat(execution.exitCode()).isZero(); + assertThat(execution.stdout()).contains("SUCCESS: T-SQL parsed cleanly (0 errors)."); + assertThat(execution.stdout()).contains("Batches:"); + assertThat(execution.stderr()).isBlank(); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testMainReturnsFailureForInvalidSql() throws Exception { + MainExecution execution = runMain("SELECT", "FROM", "WHERE;"); + + assertThat(execution.exitCode()).isEqualTo(1); + assertThat(execution.stderr()).contains("FAILED: Found"); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testMainReadsSqlFromFile(@TempDir Path tempDir) throws Exception { + Path sqlFile = tempDir.resolve("valid.sql"); + Files.writeString(sqlFile, "SELECT 1;"); + + MainExecution execution = runMain("-f", sqlFile.toString()); + + assertThat(execution.exitCode()).isZero(); + assertThat(execution.stdout()).contains("SUCCESS: T-SQL parsed cleanly (0 errors)."); + } + + // TRACEABILITY_MAPPING: REQ-028 + @Test + public void testMainReturnsErrorForMissingFile(@TempDir Path tempDir) throws Exception { + Path missingSqlFile = tempDir.resolve("missing.sql"); + + MainExecution execution = runMain("-f", missingSqlFile.toString()); + + assertThat(execution.exitCode()).isEqualTo(1); + assertThat(execution.stderr()).contains("Error:"); + } + + // TRACEABILITY_MAPPING: REQ-028 + private MainExecution runMain(String... args) throws Exception { + String javaBinary = Path.of(System.getProperty("java.home"), "bin", "java").toString(); + String classPath = System.getProperty("java.class.path"); + List command = new ArrayList<>(); + command.add(javaBinary); + command.add("-cp"); + command.add(classPath); + command.add("io.github.fupacat.TSqlParser"); + command.addAll(Arrays.asList(args)); + + Process process = new ProcessBuilder(command).start(); + String stdout = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + String stderr = new String(process.getErrorStream().readAllBytes(), StandardCharsets.UTF_8); + int exitCode = process.waitFor(); + return new MainExecution(exitCode, stdout, stderr); + } + + private record MainExecution(int exitCode, String stdout, String stderr) { + } }