Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Kept `@pondpilot/flowscope-react` as a private monorepo workspace and removed it from the npm release pipeline

### Fixed

#### Core Engine (flowscope-core)

- **MSSQL `GO` batch separators** - treat standalone SQL Server batch separators as statement boundaries during analysis and statement splitting, including trailing separators and CRLF input.

## [0.9.0] - 2026-08-12

### Added
Expand Down
47 changes: 44 additions & 3 deletions crates/flowscope-core/src/analyzer/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,13 +653,14 @@ fn split_ranges_on_mssql_go_separators(sql: &str, ranges: Vec<Range<usize>>) ->
for range in ranges {
let mut cursor = range.start;
for go_range in &go_line_ranges {
if go_range.start < range.start || go_range.end > range.end || go_range.start < cursor {
if go_range.end <= cursor || go_range.start >= range.end {
continue;
}
if let Some(chunk) = trim_statement_range(sql, cursor, go_range.start) {
let separator_start = go_range.start.max(cursor);
if let Some(chunk) = trim_statement_range(sql, cursor, separator_start) {
out.push(chunk);
}
cursor = go_range.end;
cursor = go_range.end.min(range.end);
}

if let Some(chunk) = trim_statement_range(sql, cursor, range.end) {
Expand Down Expand Up @@ -1212,6 +1213,31 @@ mod tests {
assert_eq!(&sql[ranges[1].clone()], "CREATE TABLE test (id INT)");
}

#[test]
fn mssql_statement_ranges_split_trailing_go_batch_separators() {
for sql in [
"SELECT 1;\nGO\nSELECT 2;\nGO\n",
"SELECT 1;\r\n go \r\nSELECT 2;\r\nGO\r\n",
"SELECT 1\nGO\nGO\nSELECT 2\nGO\n",
] {
let ranges = compute_statement_ranges_for_dialect(sql, Dialect::Mssql);
assert_eq!(ranges.len(), 2, "unexpected ranges for {sql:?}");
assert_eq!(&sql[ranges[0].clone()], "SELECT 1");
assert_eq!(&sql[ranges[1].clone()], "SELECT 2");
}
}

#[test]
fn mssql_statement_ranges_ignore_go_inside_strings_comments_and_identifiers() {
let sql = "SELECT 'GO' AS literal;\nSELECT [GO] FROM [source];\n-- GO\n/* GO */\nGO\nSELECT 'inside\nGO\nstring' AS literal;";
let ranges = compute_statement_ranges_for_dialect(sql, Dialect::Mssql);

assert_eq!(ranges.len(), 3);
assert_eq!(&sql[ranges[0].clone()], "SELECT 'GO' AS literal");
assert_eq!(&sql[ranges[1].clone()], "SELECT [GO] FROM [source]");
assert!(sql[ranges[2].clone()].contains("inside\nGO\nstring"));
}

#[test]
fn collect_statements_mssql_go_batch_without_final_semicolon_parses_statements() {
let mut request = base_request();
Expand All @@ -1226,6 +1252,21 @@ mod tests {
assert_eq!(statements.len(), 2);
}

#[test]
fn collect_statements_mssql_go_batch_with_trailing_separator_has_no_parse_error() {
let mut request = base_request();
request.dialect = Dialect::Mssql;
request.sql = "SELECT 1;\nGO\nSELECT 2;\nGO\n".to_string();

let (statements, issues) = collect_statements(&request);

assert_eq!(statements.len(), 2);
assert!(
issues.is_empty(),
"MSSQL trailing GO should not produce parse errors: {issues:?}"
);
}

#[test]
fn parses_procedure_with_inner_semicolons() {
let mut request = base_request();
Expand Down
48 changes: 48 additions & 0 deletions crates/flowscope-wasm/tests/analysis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use flowscope_wasm::{analyze_sql_json, split_statements_json};
use serde_json::Value;

#[test]
fn analyze_sql_json_handles_mssql_go_batch_separators() {
let request = serde_json::json!({
"sql": "SELECT 1;\nGO\nSELECT 2;\nGO\n",
"dialect": "mssql"
});

let result: Value = serde_json::from_str(&analyze_sql_json(&request.to_string()))
.expect("analysis result should be valid JSON");
let statements = result
.get("statements")
.and_then(Value::as_array)
.expect("analysis result should contain statements");
let issues = result
.get("issues")
.and_then(Value::as_array)
.expect("analysis result should contain issues");

assert_eq!(statements.len(), 2);
assert!(!issues
.iter()
.any(|issue| { issue.get("code") == Some(&Value::String("PARSE_ERROR".to_string())) }));
}

#[test]
fn split_statements_json_handles_mssql_go_batch_separators() {
let sql = "SELECT 1;\nGO\nSELECT 2;\nGO\n";
let request = serde_json::json!({
"sql": sql,
"dialect": "mssql"
});

let result: Value = serde_json::from_str(&split_statements_json(&request.to_string()))
.expect("statement split result should be valid JSON");
let statements = result
.get("statements")
.and_then(Value::as_array)
.expect("statement split result should contain statements");

assert_eq!(statements.len(), 2);
assert_eq!(statements[0]["start"], 0);
assert_eq!(statements[0]["end"], 8);
assert_eq!(statements[1]["start"], 13);
assert_eq!(statements[1]["end"], 21);
}
17 changes: 16 additions & 1 deletion scripts/test_wasm_browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ const harness = `<!doctype html>
.map((node) => node.label);
const errors = result.issues.filter((issue) => issue.severity === 'error');

const mssqlRequest = {
sql: 'SELECT 1;\nGO\nSELECT 2;\nGO\n',
dialect: 'mssql',
};
const mssqlResult = JSON.parse(analyze_sql_json(JSON.stringify(mssqlRequest)));
const mssqlParseErrors = mssqlResult.issues.filter(
(issue) => issue.code === 'PARSE_ERROR'
);

if (result.statements.length !== 1 || result.summary.statementCount !== 1) {
throw new Error('Expected one analyzed statement');
}
Expand All @@ -52,12 +61,18 @@ const harness = `<!doctype html>
if (errors.length > 0) {
throw new Error('Analysis returned errors: ' + JSON.stringify(errors));
}
if (mssqlResult.statements.length !== 2 || mssqlParseErrors.length > 0) {
throw new Error(
'MSSQL GO batch analysis failed: ' + JSON.stringify(mssqlResult.issues)
);
}

body.dataset.status = 'passed';
body.textContent = JSON.stringify({
version: get_version(),
statementCount: result.summary.statementCount,
tableLabels
tableLabels,
mssqlStatementCount: mssqlResult.summary.statementCount,
});
} catch (error) {
body.dataset.status = 'failed';
Expand Down