Skip to content
Draft
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
162 changes: 162 additions & 0 deletions Musoq.Converter.Tests/CompilationExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.CodeAnalysis;
using Musoq.Converter.Exceptions;
using System;
using System.Linq;

namespace Musoq.Converter.Tests;

[TestClass]
public class CompilationExceptionTests
{
[TestMethod]
public void Constructor_WithAllParameters_ShouldSetProperties()
{
// Arrange
var message = "Compilation failed";
var generatedCode = "public class Test { }";
var diagnostics = new Diagnostic[0];
var queryContext = "SELECT * FROM test";

// Act
var exception = new CompilationException(message, generatedCode, diagnostics, queryContext);

// Assert
Assert.AreEqual(message, exception.Message);
Assert.AreEqual(generatedCode, exception.GeneratedCode);
Assert.AreEqual(queryContext, exception.QueryContext);
Assert.IsNotNull(exception.CompilationErrors);
}

[TestMethod]
public void Constructor_WithMinimalParameters_ShouldUseDefaults()
{
// Arrange
var message = "Simple error";

// Act
var exception = new CompilationException(message);

// Assert
Assert.AreEqual(message, exception.Message);
Assert.AreEqual(string.Empty, exception.GeneratedCode);
Assert.AreEqual(string.Empty, exception.QueryContext);
Assert.IsNotNull(exception.CompilationErrors);
Assert.AreEqual(0, exception.CompilationErrors.Count());
}

[TestMethod]
public void Constructor_WithInnerException_ShouldSetInnerException()
{
// Arrange
var message = "Compilation error";
var innerException = new InvalidOperationException("Inner error");

// Act
var exception = new CompilationException(message, innerException);

// Assert
Assert.AreEqual(message, exception.Message);
Assert.AreEqual(innerException, exception.InnerException);
}

[TestMethod]
public void ForAssemblyLoadFailure_ShouldCreateAppropriateException()
{
// Arrange
var innerException = new System.IO.FileLoadException("Could not load assembly");
var queryContext = "SELECT id FROM users";

// Act
var exception = CompilationException.ForAssemblyLoadFailure(innerException, queryContext);

// Assert
Assert.AreEqual(innerException, exception.InnerException);
Assert.AreEqual(queryContext, exception.QueryContext);
Assert.IsTrue(exception.Message.Contains("compiled query assembly could not be loaded"));
Assert.IsTrue(exception.Message.Contains("missing dependencies"));
Assert.IsTrue(exception.Message.Contains("incompatible assembly references"));
Assert.IsTrue(exception.Message.Contains("data source plugins are properly installed"));
}

[TestMethod]
public void ForTypeResolutionFailure_ShouldCreateAppropriateException()
{
// Arrange
var typeName = "Query_12345";
var queryContext = "SELECT name FROM products";

// Act
var exception = CompilationException.ForTypeResolutionFailure(typeName, queryContext);

// Assert
Assert.AreEqual(queryContext, exception.QueryContext);
Assert.IsTrue(exception.Message.Contains($"Could not resolve type '{typeName}'"));
Assert.IsTrue(exception.Message.Contains("problem with code generation"));
Assert.IsTrue(exception.Message.Contains("missing references"));
Assert.IsTrue(exception.Message.Contains("query syntax"));
Assert.IsTrue(exception.Message.Contains("schemas are registered"));
}

[TestMethod]
public void ForAssemblyLoadFailure_WithDefaultContext_ShouldUseUnknown()
{
// Arrange
var innerException = new Exception("Load error");

// Act
var exception = CompilationException.ForAssemblyLoadFailure(innerException);

// Assert
Assert.AreEqual("Unknown", exception.QueryContext);
}

[TestMethod]
public void ForTypeResolutionFailure_WithDefaultContext_ShouldUseUnknown()
{
// Arrange
var typeName = "TestType";

// Act
var exception = CompilationException.ForTypeResolutionFailure(typeName);

// Assert
Assert.AreEqual("Unknown", exception.QueryContext);
}

[TestMethod]
public void Constructor_WithNullParameters_ShouldHandleGracefully()
{
// Arrange
var message = "Test message";

// Act
var exception = new CompilationException(message, null, null, null);

// Assert
Assert.AreEqual(message, exception.Message);
Assert.AreEqual(string.Empty, exception.GeneratedCode);
Assert.AreEqual(string.Empty, exception.QueryContext);
Assert.IsNotNull(exception.CompilationErrors);
Assert.AreEqual(0, exception.CompilationErrors.Count());
}

[TestMethod]
public void Constructor_WithNullParametersAndInnerException_ShouldHandleGracefully()
{
// Arrange
var message = "Test message";
var innerException = new Exception("Inner");

// Act
var exception = new CompilationException(message, innerException, null, null, null);

// Assert
Assert.AreEqual(message, exception.Message);
Assert.AreEqual(innerException, exception.InnerException);
Assert.AreEqual(string.Empty, exception.GeneratedCode);
Assert.AreEqual(string.Empty, exception.QueryContext);
Assert.IsNotNull(exception.CompilationErrors);
Assert.AreEqual(0, exception.CompilationErrors.Count());
}
}
105 changes: 105 additions & 0 deletions Musoq.Converter.Tests/ErrorHandlingIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Musoq.Converter.Exceptions;
using Musoq.Parser.Exceptions;

namespace Musoq.Converter.Tests;

[TestClass]
public class ErrorHandlingIntegrationTests
{
[TestMethod]
public void WhenQueryContainsBackslash_ShouldProvideHelpfulError()
{
const string queryWithBackslash = "select 5 \\ 2 from #test.source()";

var exception = Assert.ThrowsException<AstValidationException>(() =>
{
InstanceCreator.CompileForExecution(queryWithBackslash, "TestAssembly", null, null);
});

// Debug output
Console.WriteLine($"Exception Message: {exception.Message}");
Console.WriteLine($"Inner Exception: {exception.InnerException?.GetType()?.Name}");
Console.WriteLine($"Inner Exception Message: {exception.InnerException?.Message}");

Assert.IsNotNull(exception.InnerException);
Assert.IsInstanceOfType(exception.InnerException, typeof(QueryValidationException));

var validationException = (QueryValidationException)exception.InnerException;
Assert.IsTrue(validationException.Message.Contains("problematic characters") && validationException.Message.Contains("\\"));
}

[TestMethod]
public void WhenQueryContainsQuestionMark_ShouldProvideHelpfulError()
{
const string queryWithQuestionMark = "select id from #test.source() where name = ?";

var exception = Assert.ThrowsException<AstValidationException>(() =>
{
InstanceCreator.CompileForExecution(queryWithQuestionMark, "TestAssembly", null, null);
});

// Debug output
Console.WriteLine($"Exception Message: {exception.Message}");
Console.WriteLine($"Inner Exception: {exception.InnerException?.GetType()?.Name}");
Console.WriteLine($"Inner Exception Message: {exception.InnerException?.Message}");

Assert.IsNotNull(exception.InnerException);
Assert.IsInstanceOfType(exception.InnerException, typeof(QueryValidationException));

var validationException = (QueryValidationException)exception.InnerException;
Assert.IsTrue(validationException.Message.Contains("problematic characters") && validationException.Message.Contains("?"));
}

[TestMethod]
public void WhenQueryIsEmpty_ShouldProvideHelpfulError()
{
const string emptyQuery = "";

var exception = Assert.ThrowsException<AstValidationException>(() =>
{
InstanceCreator.CompileForExecution(emptyQuery, "TestAssembly", null, null);
});

// Debug output
Console.WriteLine($"Exception Message: {exception.Message}");
Console.WriteLine($"Inner Exception: {exception.InnerException?.GetType()?.Name}");

// Should be caught by the early null check, not validation
Assert.IsTrue(exception.Message.Contains("RawQuery cannot be null or whitespace"));
}

[TestMethod]
public void WhenQueryHasUnbalancedParentheses_ShouldProvideHelpfulError()
{
const string unbalancedQuery = "select sum(column from #test.source()";

var exception = Assert.ThrowsException<AstValidationException>(() =>
{
InstanceCreator.CompileForExecution(unbalancedQuery, "TestAssembly", null, null);
});

Assert.IsNotNull(exception.InnerException);
Assert.IsInstanceOfType(exception.InnerException, typeof(QueryValidationException));

var validationException = (QueryValidationException)exception.InnerException;
Assert.IsTrue(validationException.Message.Contains("Missing") && validationException.Message.Contains("closing parenthesis"));
}

[TestMethod]
public void WhenQueryIsValid_ShouldPassValidation()
{
const string validQuery = "select 1 as Value";

// This should not throw an exception during validation
// (it might fail later due to missing schema, but validation should pass)
var exception = Assert.ThrowsException<AstValidationException>(() =>
{
InstanceCreator.CompileForExecution(validQuery, "TestAssembly", null, null);
});

// Should fail for a different reason (missing schema/compilation), not validation
Assert.IsFalse(exception.Message.Contains("Query validation failed"));
}
}
5 changes: 5 additions & 0 deletions Musoq.Converter/Build/CreateTree.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Musoq.Converter.Exceptions;
using Musoq.Parser.Lexing;
using Musoq.Parser.Validation;

namespace Musoq.Converter.Build;

Expand All @@ -16,6 +17,10 @@ public override void Build(BuildItems items)

try
{
// Early validation before expensive parsing
var validator = new QueryValidator();
validator.ValidateAndThrow(items.RawQuery);

var lexer = new Lexer(items.RawQuery, true);
var parser = new Parser.Parser(lexer);

Expand Down
57 changes: 56 additions & 1 deletion Musoq.Converter/Exceptions/CompilationException.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Musoq.Converter.Exceptions;

/// <summary>
/// Exception thrown when C# code compilation fails during query processing.
/// Provides detailed compilation diagnostics and helpful guidance for resolution.
/// </summary>
public class CompilationException : Exception
{
public CompilationException(string message)
public string GeneratedCode { get; }
public IEnumerable<Diagnostic> CompilationErrors { get; }
public string QueryContext { get; }

public CompilationException(string message, string generatedCode = null, IEnumerable<Diagnostic> compilationErrors = null, string queryContext = null)
: base(message)
{
GeneratedCode = generatedCode ?? string.Empty;
CompilationErrors = compilationErrors ?? Enumerable.Empty<Diagnostic>();
QueryContext = queryContext ?? string.Empty;
}

public CompilationException(string message, Exception innerException, string generatedCode = null, IEnumerable<Diagnostic> compilationErrors = null, string queryContext = null)
: base(message, innerException)
{
GeneratedCode = generatedCode ?? string.Empty;
CompilationErrors = compilationErrors ?? Enumerable.Empty<Diagnostic>();
QueryContext = queryContext ?? string.Empty;
}

public static CompilationException ForCompilationFailure(IEnumerable<Diagnostic> errors, string generatedCode, string queryContext = "Unknown")
{
var errorList = errors.ToList();
var errorDetails = string.Join("\n", errorList
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => $"- {d.GetMessage()} (Line: {d.Location.GetLineSpan().StartLinePosition.Line + 1})"));

var message = $"Failed to compile the generated C# code for your SQL query. " +
$"Compilation errors:\n{errorDetails}\n\n" +
"This usually indicates a problem with the SQL query syntax or unsupported operations. " +
"Please check your query for syntax errors, column references, and method calls.";

return new CompilationException(message, generatedCode, errorList, queryContext);
}

public static CompilationException ForAssemblyLoadFailure(Exception innerException, string queryContext = "Unknown")
{
var message = "The compiled query assembly could not be loaded. " +
"This may be due to missing dependencies or incompatible assembly references. " +
"Please ensure all required data source plugins are properly installed.";

return new CompilationException(message, innerException, queryContext: queryContext);
}

public static CompilationException ForTypeResolutionFailure(string typeName, string queryContext = "Unknown")
{
var message = $"Could not resolve type '{typeName}' in the compiled assembly. " +
"This indicates a problem with code generation or missing references. " +
"Please check your query syntax and ensure all required schemas are registered.";

return new CompilationException(message, queryContext: queryContext);
}
}
Loading