636 lines
26 KiB
C#
636 lines
26 KiB
C#
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.SqlServer.TransactSql.ScriptDom;
|
|
|
|
internal static class Program
|
|
{
|
|
private const string ParserPackage =
|
|
"Microsoft.SqlServer.TransactSql.ScriptDom";
|
|
private const string ParserPackageVersion = "180.59.2";
|
|
private const string ParserDialect = "TSql100";
|
|
private const long MaximumScriptBytes = 2L * 1024L * 1024L;
|
|
|
|
private static readonly string[] ExpectedScripts =
|
|
{
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/001_agent_business_idempotency.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/002_workflow_adapter_contract.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/003_record_workflow_acceptance.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/004_dynamic_module_adapter_contract.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/005_dynamic_module_update_contract.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/006_workflow_readiness_v3.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/customer-profiles/lserp-ai.workflow-read.compat100.draft.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/customer-profiles/lserp-ai.workflow-write.leave.compat100.draft.sql",
|
|
"插件库/Lskj.AgentBridge/Deployment/customer-profiles/lserp-ai.workflow-write.purchase.compat100.draft.sql"
|
|
};
|
|
|
|
private static int _passed;
|
|
private static int _failed;
|
|
|
|
private static int Main(string[] args)
|
|
{
|
|
if (args.Length != 1 || string.IsNullOrWhiteSpace(args[0]))
|
|
{
|
|
Console.Error.WriteLine("usage: Lskj.SqlContract.Tests <repo-root>");
|
|
return 2;
|
|
}
|
|
|
|
string repoRoot;
|
|
try
|
|
{
|
|
repoRoot = Path.GetFullPath(args[0]);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
Console.Error.WriteLine("invalid_repo_root: " + error.GetType().Name);
|
|
return 2;
|
|
}
|
|
|
|
Run("sql_contract_file_set_is_exact", () =>
|
|
VerifyExactScriptSet(repoRoot));
|
|
foreach (string relativePath in ExpectedScripts)
|
|
{
|
|
string testName = "compat100_parse_" +
|
|
Path.GetFileNameWithoutExtension(relativePath)
|
|
.Replace('.', '_')
|
|
.Replace('-', '_');
|
|
Run(testName, () => ParseCompatibility100File(
|
|
repoRoot,
|
|
relativePath));
|
|
}
|
|
Run("workflow_write_procedure_parameters_are_exact", () =>
|
|
VerifyWorkflowWriteProcedureParameters(repoRoot));
|
|
Run("workflow_readiness_v3_binds_signature_and_modify_date", () =>
|
|
VerifyWorkflowReadinessV3(repoRoot));
|
|
Run("purchase_requested_lines_projection_is_exact", () =>
|
|
VerifyPurchaseRequestedLinesProjection(repoRoot));
|
|
Run("explicit_insert_projection_arities_match", () =>
|
|
VerifyExplicitInsertProjectionArities(repoRoot));
|
|
Run("select_only_catalog_embedded_queries_are_tsql100_selects", () =>
|
|
VerifySelectOnlyCatalogQueries(repoRoot));
|
|
Run("tsql100_rejects_newer_create_or_alter_syntax",
|
|
RejectNewerSyntax);
|
|
|
|
Console.WriteLine(
|
|
$"parserPackage={ParserPackage} parserVersion={ParserPackageVersion} dialect={ParserDialect}");
|
|
Console.WriteLine($"passed={_passed} failed={_failed}");
|
|
return _failed == 0 ? 0 : 1;
|
|
}
|
|
|
|
private static void Run(string name, Action action)
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
_passed += 1;
|
|
Console.WriteLine("PASS " + name);
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
_failed += 1;
|
|
Console.WriteLine("FAIL " + name + " :: " + SafeMessage(error.Message));
|
|
}
|
|
}
|
|
|
|
private static void VerifyExactScriptSet(string repoRoot)
|
|
{
|
|
string deploymentRoot = Path.Combine(
|
|
repoRoot,
|
|
"插件库",
|
|
"Lskj.AgentBridge",
|
|
"Deployment");
|
|
string[] roots =
|
|
{
|
|
Path.Combine(deploymentRoot, "SqlServer"),
|
|
Path.Combine(deploymentRoot, "customer-profiles")
|
|
};
|
|
var actual = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (string root in roots)
|
|
{
|
|
if (!Directory.Exists(root))
|
|
throw new InvalidOperationException("sql_contract_directory_missing");
|
|
foreach (string path in Directory.EnumerateFiles(
|
|
root,
|
|
"*.sql",
|
|
SearchOption.TopDirectoryOnly))
|
|
{
|
|
actual.Add(NormalizeRelativePath(repoRoot, path));
|
|
}
|
|
}
|
|
|
|
var expected = new HashSet<string>(
|
|
ExpectedScripts,
|
|
StringComparer.Ordinal);
|
|
if (!actual.SetEquals(expected))
|
|
{
|
|
string missing = string.Join(",", expected.Except(actual).OrderBy(x => x));
|
|
string unexpected = string.Join(",", actual.Except(expected).OrderBy(x => x));
|
|
throw new InvalidOperationException(
|
|
$"sql_contract_file_set_mismatch missing=[{missing}] unexpected=[{unexpected}]");
|
|
}
|
|
}
|
|
|
|
private static void ParseCompatibility100File(
|
|
string repoRoot,
|
|
string relativePath)
|
|
{
|
|
_ = ParseCompatibility100Script(repoRoot, relativePath);
|
|
}
|
|
|
|
private static TSqlScript ParseCompatibility100Script(
|
|
string repoRoot,
|
|
string relativePath)
|
|
{
|
|
string path = Path.GetFullPath(Path.Combine(
|
|
repoRoot,
|
|
relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
string rootPrefix = repoRoot.TrimEnd(
|
|
Path.DirectorySeparatorChar,
|
|
Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
if (!path.StartsWith(rootPrefix, StringComparison.Ordinal))
|
|
throw new InvalidOperationException("sql_contract_path_outside_repo");
|
|
|
|
var file = new FileInfo(path);
|
|
if (!file.Exists
|
|
|| file.Length <= 0
|
|
|| file.Length > MaximumScriptBytes
|
|
|| (file.Attributes & FileAttributes.ReparsePoint) != 0)
|
|
{
|
|
throw new InvalidOperationException("sql_contract_file_invalid");
|
|
}
|
|
|
|
using var stream = new FileStream(
|
|
path,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read);
|
|
if (stream.Length != file.Length)
|
|
throw new InvalidOperationException("sql_contract_file_changed");
|
|
using var reader = new StreamReader(
|
|
stream,
|
|
new UTF8Encoding(
|
|
encoderShouldEmitUTF8Identifier: false,
|
|
throwOnInvalidBytes: true),
|
|
detectEncodingFromByteOrderMarks: true,
|
|
bufferSize: 4096,
|
|
leaveOpen: false);
|
|
var parser = new TSql100Parser(initialQuotedIdentifiers: true);
|
|
TSqlFragment fragment = parser.Parse(reader, out IList<ParseError> errors);
|
|
if (errors.Count != 0)
|
|
throw new InvalidOperationException(FormatErrors(errors));
|
|
if (fragment is not TSqlScript script || script.Batches.Count == 0)
|
|
throw new InvalidOperationException("sql_contract_ast_empty");
|
|
return script;
|
|
}
|
|
|
|
private static void VerifySelectOnlyCatalogQueries(string repoRoot)
|
|
{
|
|
string relativePath =
|
|
"插件库/Lskj.AgentBridge/Deployment/"
|
|
+ "Invoke-LserpSelectOnlyCatalogSnapshot.ps1";
|
|
string path = Path.GetFullPath(Path.Combine(
|
|
repoRoot,
|
|
relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
|
string rootPrefix = repoRoot.TrimEnd(
|
|
Path.DirectorySeparatorChar,
|
|
Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
|
if (!path.StartsWith(rootPrefix, StringComparison.Ordinal))
|
|
throw new InvalidOperationException("catalog_tool_path_outside_repo");
|
|
var file = new FileInfo(path);
|
|
if (!file.Exists
|
|
|| file.Length <= 0
|
|
|| file.Length > MaximumScriptBytes
|
|
|| (file.Attributes & FileAttributes.ReparsePoint) != 0)
|
|
{
|
|
throw new InvalidOperationException("catalog_tool_file_invalid");
|
|
}
|
|
|
|
string source;
|
|
using (var stream = new FileStream(
|
|
path,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read))
|
|
using (var reader = new StreamReader(
|
|
stream,
|
|
new UTF8Encoding(false, true),
|
|
detectEncodingFromByteOrderMarks: true))
|
|
{
|
|
source = reader.ReadToEnd();
|
|
}
|
|
foreach (string variable in new[] { "permissionQuery", "metadataQuery" })
|
|
{
|
|
MatchCollection matches = Regex.Matches(
|
|
source,
|
|
@"\$" + variable + @"\s*=\s*@'\r?\n(?<sql>.*?)\r?\n'@",
|
|
RegexOptions.Singleline | RegexOptions.CultureInvariant);
|
|
if (matches.Count != 1)
|
|
throw new InvalidOperationException(
|
|
"catalog_tool_query_literal_invalid:" + variable);
|
|
string sql = matches[0].Groups["sql"].Value;
|
|
var parser = new TSql100Parser(initialQuotedIdentifiers: true);
|
|
TSqlFragment fragment;
|
|
using (var verificationReader = new StringReader(sql))
|
|
{
|
|
fragment = parser.Parse(
|
|
verificationReader,
|
|
out IList<ParseError> errors);
|
|
if (errors.Count != 0)
|
|
throw new InvalidOperationException(FormatErrors(errors));
|
|
}
|
|
if (fragment is not TSqlScript script
|
|
|| script.Batches.Count == 0
|
|
|| script.Batches.Any(batch => batch.Statements.Count == 0)
|
|
|| script.Batches.SelectMany(batch => batch.Statements)
|
|
.Any(statement => statement is not SelectStatement))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"catalog_tool_query_not_select_only:" + variable);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void VerifyWorkflowWriteProcedureParameters(
|
|
string repoRoot)
|
|
{
|
|
VerifyProcedureParameters(
|
|
repoRoot,
|
|
"插件库/Lskj.AgentBridge/Deployment/customer-profiles/"
|
|
+ "lserp-ai.workflow-write.purchase.compat100.draft.sql",
|
|
"dbo.p_lserp_agent_workflow_write_purchase_compat100",
|
|
new[]
|
|
{
|
|
"@action:varchar(64):required",
|
|
"@module_code:nvarchar(64):required",
|
|
"@account_book:nvarchar(64):required",
|
|
"@subsystem_id:nvarchar(32):required",
|
|
"@user_id:nvarchar(64):required",
|
|
"@correlation_id:varchar(128):required",
|
|
"@idempotency_key:varchar(128):required",
|
|
"@input_fingerprint:char(64):required",
|
|
"@supplier_code:nvarchar(64):required",
|
|
"@currency_code:nvarchar(64):required",
|
|
"@invoice_number:nvarchar(128):required",
|
|
"@invoice_date:datetime:required",
|
|
"@total_without_tax:decimal(28,8):required",
|
|
"@tax_amount:decimal(28,8):required",
|
|
"@total_with_tax:decimal(28,8):required",
|
|
"@lines_xml:xml:required",
|
|
"@source_documents_xml:xml:required"
|
|
});
|
|
VerifyProcedureParameters(
|
|
repoRoot,
|
|
"插件库/Lskj.AgentBridge/Deployment/customer-profiles/"
|
|
+ "lserp-ai.workflow-write.leave.compat100.draft.sql",
|
|
"dbo.p_lserp_agent_workflow_write_leave_compat100",
|
|
new[]
|
|
{
|
|
"@action:varchar(64):required",
|
|
"@module_code:nvarchar(64):required",
|
|
"@account_book:nvarchar(64):required",
|
|
"@subsystem_id:nvarchar(32):required",
|
|
"@user_id:nvarchar(64):required",
|
|
"@correlation_id:varchar(128):required",
|
|
"@idempotency_key:varchar(128):required",
|
|
"@input_fingerprint:char(64):required",
|
|
"@employee_id:nvarchar(64):optional",
|
|
"@leave_type_code:nvarchar(64):optional",
|
|
"@flow_type_code:nvarchar(64):optional",
|
|
"@start_local:datetime:optional",
|
|
"@end_local:datetime:optional",
|
|
"@requested_hours:decimal(18,6):optional",
|
|
"@reason:nvarchar(500):optional",
|
|
"@submit_after_save_intent:bit:optional",
|
|
"@record_id:nvarchar(128):optional"
|
|
});
|
|
}
|
|
|
|
private static void VerifyWorkflowReadinessV3(string repoRoot)
|
|
{
|
|
const string relativePath =
|
|
"插件库/Lskj.AgentBridge/Deployment/SqlServer/"
|
|
+ "006_workflow_readiness_v3.sql";
|
|
TSqlScript script = ParseCompatibility100Script(repoRoot, relativePath);
|
|
var collector = new AlterProcedureCollector();
|
|
script.Accept(collector);
|
|
if (collector.Statements.Count != 1)
|
|
throw new InvalidOperationException(
|
|
"workflow_readiness_v3_alter_not_unique");
|
|
|
|
AlterProcedureStatement statement = collector.Statements[0];
|
|
var generator = new Sql100ScriptGenerator();
|
|
generator.GenerateScript(statement.ProcedureReference, out string procedureName);
|
|
if (NormalizeSqlFragment(procedureName)
|
|
!= "dbo.p_lserp_agent_workflow_readiness_v3")
|
|
throw new InvalidOperationException(
|
|
"workflow_readiness_v3_name_changed");
|
|
string[] expectedParameters =
|
|
{
|
|
"@workflow:varchar(32)",
|
|
"@module_code:nvarchar(64)",
|
|
"@account_book:nvarchar(128)",
|
|
"@subsystem_id:nvarchar(128)"
|
|
};
|
|
string[] actualParameters = statement.Parameters.Select(parameter =>
|
|
{
|
|
generator.GenerateScript(parameter.DataType, out string dataType);
|
|
return parameter.VariableName.Value.ToLowerInvariant()
|
|
+ ":" + NormalizeSqlFragment(dataType);
|
|
}).ToArray();
|
|
if (!actualParameters.SequenceEqual(
|
|
expectedParameters,
|
|
StringComparer.Ordinal))
|
|
throw new InvalidOperationException(
|
|
"workflow_readiness_v3_parameters_changed");
|
|
|
|
generator.GenerateScript(statement, out string generated);
|
|
string compact = NormalizeSqlFragment(generated);
|
|
foreach (string required in new[]
|
|
{
|
|
"sys.parameters",
|
|
"contract_parameter.parameter_id",
|
|
"@actual_read_signature",
|
|
"@actual_write_signature",
|
|
"isnull(@actual_read_signature,n'')",
|
|
"isnull(@actual_write_signature,n'')",
|
|
"latin1_general_100_bin2",
|
|
"sys.procedures",
|
|
"modify_date",
|
|
"validated_at_utc>=@latest_contract_modified_utc",
|
|
"validated_at_utc<=dateadd(minute,5,sysutcdatetime())",
|
|
"dbo.p_lserp_agent_workflow_read",
|
|
"dbo.p_lserp_agent_workflow_read_compat100",
|
|
"dbo.p_lserp_agent_workflow_write",
|
|
"dbo.p_lserp_agent_workflow_write_purchase_compat100",
|
|
"dbo.p_lserp_agent_workflow_write_leave_compat100",
|
|
"dbo.p_agent_workflow_adapter_evidence_v2"
|
|
})
|
|
{
|
|
if (!compact.Contains(required, StringComparison.Ordinal))
|
|
throw new InvalidOperationException(
|
|
"workflow_readiness_v3_guard_missing " + required);
|
|
}
|
|
}
|
|
|
|
private static void VerifyProcedureParameters(
|
|
string repoRoot,
|
|
string relativePath,
|
|
string expectedProcedureName,
|
|
IReadOnlyList<string> expectedParameters)
|
|
{
|
|
TSqlScript script = ParseCompatibility100Script(
|
|
repoRoot,
|
|
relativePath);
|
|
var collector = new CreateProcedureCollector();
|
|
script.Accept(collector);
|
|
if (collector.Statements.Count != 1)
|
|
throw new InvalidOperationException(
|
|
"workflow_write_procedure_not_unique "
|
|
+ Path.GetFileName(relativePath));
|
|
|
|
CreateProcedureStatement statement = collector.Statements[0];
|
|
var generator = new Sql100ScriptGenerator();
|
|
generator.GenerateScript(
|
|
statement.ProcedureReference,
|
|
out string procedureName);
|
|
if (!string.Equals(
|
|
NormalizeSqlFragment(procedureName),
|
|
NormalizeSqlFragment(expectedProcedureName),
|
|
StringComparison.Ordinal))
|
|
throw new InvalidOperationException(
|
|
"workflow_write_procedure_name_changed "
|
|
+ Path.GetFileName(relativePath));
|
|
|
|
string[] actual = statement.Parameters.Select(parameter =>
|
|
{
|
|
generator.GenerateScript(parameter.DataType, out string dataType);
|
|
generator.GenerateScript(parameter, out string declaration);
|
|
string requirement = declaration.Contains('=', StringComparison.Ordinal)
|
|
? "optional"
|
|
: "required";
|
|
return parameter.VariableName.Value.ToLowerInvariant()
|
|
+ ":" + NormalizeSqlFragment(dataType)
|
|
+ ":" + requirement;
|
|
}).ToArray();
|
|
if (!actual.SequenceEqual(expectedParameters, StringComparer.Ordinal))
|
|
throw new InvalidOperationException(
|
|
"workflow_write_procedure_parameters_changed "
|
|
+ Path.GetFileName(relativePath)
|
|
+ " expected=[" + string.Join(",", expectedParameters) + "]"
|
|
+ " actual=[" + string.Join(",", actual) + "]");
|
|
}
|
|
|
|
private static string NormalizeSqlFragment(string value)
|
|
{
|
|
return Regex.Replace(
|
|
value ?? string.Empty,
|
|
@"[\s\[\]]+",
|
|
string.Empty,
|
|
RegexOptions.CultureInvariant)
|
|
.ToLowerInvariant();
|
|
}
|
|
|
|
private static void VerifyPurchaseRequestedLinesProjection(
|
|
string repoRoot)
|
|
{
|
|
const string relativePath =
|
|
"插件库/Lskj.AgentBridge/Deployment/customer-profiles/"
|
|
+ "lserp-ai.workflow-write.purchase.compat100.draft.sql";
|
|
TSqlScript script = ParseCompatibility100Script(
|
|
repoRoot,
|
|
relativePath);
|
|
var collector = new InsertStatementCollector();
|
|
script.Accept(collector);
|
|
List<InsertSpecification> matches = collector.Statements
|
|
.Select(item => item.InsertSpecification)
|
|
.Where(item => item.Target is VariableTableReference target
|
|
&& string.Equals(
|
|
target.Variable.Name,
|
|
"@requested_lines",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
if (matches.Count != 1)
|
|
throw new InvalidOperationException(
|
|
"purchase_requested_lines_insert_not_unique");
|
|
|
|
InsertSpecification insert = matches[0];
|
|
string[] expectedColumns =
|
|
{
|
|
"line_id", "material_code", "unit_name", "quantity",
|
|
"unit_price", "tax_rate", "line_tax_amount", "line_amount",
|
|
"source_order_id", "source_order_number", "source_line_id",
|
|
"source_unit", "source_remaining_quantity",
|
|
"source_unit_price", "source_tax_rate", "source_exchange_rate"
|
|
};
|
|
string[] expectedAttributes =
|
|
{
|
|
"line_id", "material_code", "unit", "quantity", "unit_price",
|
|
"tax_rate", "tax_amount", "line_amount", "source_order_id",
|
|
"source_order_number", "source_line_id", "source_unit",
|
|
"source_remaining_quantity", "source_unit_price",
|
|
"source_tax_rate", "source_exchange_rate"
|
|
};
|
|
string[] actualColumns = insert.Columns
|
|
.Select(item => item.MultiPartIdentifier?.Identifiers
|
|
.LastOrDefault()?.Value ?? string.Empty)
|
|
.ToArray();
|
|
if (!actualColumns.SequenceEqual(
|
|
expectedColumns,
|
|
StringComparer.OrdinalIgnoreCase))
|
|
throw new InvalidOperationException(
|
|
"purchase_requested_lines_target_columns_changed");
|
|
if (insert.InsertSource is not SelectInsertSource source
|
|
|| source.Select is not QuerySpecification query
|
|
|| query.SelectElements.Count != expectedAttributes.Length
|
|
|| query.SelectElements.Any(item =>
|
|
item is not SelectScalarExpression))
|
|
throw new InvalidOperationException(
|
|
"purchase_requested_lines_select_arity_mismatch");
|
|
|
|
var generator = new Sql100ScriptGenerator();
|
|
for (int index = 0; index < expectedAttributes.Length; index++)
|
|
{
|
|
var scalar = (SelectScalarExpression)query.SelectElements[index];
|
|
generator.GenerateScript(scalar.Expression, out string text);
|
|
Match attribute = Regex.Match(
|
|
text,
|
|
@"\(@([a-z_]+)\)\[1\]",
|
|
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
if (!attribute.Success
|
|
|| !string.Equals(
|
|
attribute.Groups[1].Value,
|
|
expectedAttributes[index],
|
|
StringComparison.OrdinalIgnoreCase))
|
|
throw new InvalidOperationException(
|
|
"purchase_requested_lines_attribute_mapping_invalid_"
|
|
+ expectedColumns[index]);
|
|
}
|
|
}
|
|
|
|
private static void VerifyExplicitInsertProjectionArities(
|
|
string repoRoot)
|
|
{
|
|
var generator = new Sql100ScriptGenerator();
|
|
foreach (string relativePath in ExpectedScripts)
|
|
{
|
|
TSqlScript script = ParseCompatibility100Script(
|
|
repoRoot,
|
|
relativePath);
|
|
var collector = new InsertStatementCollector();
|
|
script.Accept(collector);
|
|
foreach (InsertSpecification insert in collector.Statements
|
|
.Select(item => item.InsertSpecification)
|
|
.Where(item => item.Columns.Count != 0))
|
|
{
|
|
int expected = insert.Columns.Count;
|
|
generator.GenerateScript(insert.Target, out string target);
|
|
string location = Path.GetFileName(relativePath) + ":"
|
|
+ insert.StartLine + ":" + SafeMessage(target);
|
|
if (insert.InsertSource is ValuesInsertSource values)
|
|
{
|
|
if (values.IsDefaultValues
|
|
|| values.RowValues.Count == 0
|
|
|| values.RowValues.Any(row =>
|
|
row.ColumnValues.Count != expected))
|
|
throw new InvalidOperationException(
|
|
"insert_values_arity_mismatch " + location);
|
|
continue;
|
|
}
|
|
if (insert.InsertSource is not SelectInsertSource select)
|
|
continue;
|
|
IReadOnlyList<int> widths = ProjectionWidths(select.Select);
|
|
if (widths.Count == 0 || widths.Any(width => width != expected))
|
|
throw new InvalidOperationException(
|
|
"insert_select_arity_mismatch " + location);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IReadOnlyList<int> ProjectionWidths(
|
|
QueryExpression expression)
|
|
{
|
|
if (expression is QuerySpecification specification)
|
|
{
|
|
if (specification.SelectElements.Count == 0
|
|
|| specification.SelectElements.Any(item =>
|
|
item is SelectStarExpression))
|
|
return Array.Empty<int>();
|
|
return new[] { specification.SelectElements.Count };
|
|
}
|
|
if (expression is BinaryQueryExpression binary)
|
|
return ProjectionWidths(binary.FirstQueryExpression)
|
|
.Concat(ProjectionWidths(binary.SecondQueryExpression))
|
|
.ToArray();
|
|
if (expression is QueryParenthesisExpression parenthesis)
|
|
return ProjectionWidths(parenthesis.QueryExpression);
|
|
return Array.Empty<int>();
|
|
}
|
|
|
|
private sealed class InsertStatementCollector : TSqlFragmentVisitor
|
|
{
|
|
public List<InsertStatement> Statements { get; } = new();
|
|
|
|
public override void ExplicitVisit(InsertStatement node)
|
|
{
|
|
Statements.Add(node);
|
|
}
|
|
}
|
|
|
|
private sealed class CreateProcedureCollector : TSqlFragmentVisitor
|
|
{
|
|
public List<CreateProcedureStatement> Statements { get; } = new();
|
|
|
|
public override void ExplicitVisit(CreateProcedureStatement node)
|
|
{
|
|
Statements.Add(node);
|
|
}
|
|
}
|
|
|
|
private sealed class AlterProcedureCollector : TSqlFragmentVisitor
|
|
{
|
|
public List<AlterProcedureStatement> Statements { get; } = new();
|
|
|
|
public override void ExplicitVisit(AlterProcedureStatement node)
|
|
{
|
|
Statements.Add(node);
|
|
}
|
|
}
|
|
|
|
private static void RejectNewerSyntax()
|
|
{
|
|
const string sql =
|
|
"CREATE OR ALTER PROCEDURE dbo.p_lserp_newer_syntax AS SELECT 1;";
|
|
var parser = new TSql100Parser(initialQuotedIdentifiers: true);
|
|
using var reader = new StringReader(sql);
|
|
_ = parser.Parse(reader, out IList<ParseError> errors);
|
|
if (errors.Count == 0)
|
|
throw new InvalidOperationException("tsql100_parser_accepted_newer_syntax");
|
|
}
|
|
|
|
private static string NormalizeRelativePath(string root, string path)
|
|
{
|
|
string value = Path.GetRelativePath(root, path)
|
|
.Replace(Path.DirectorySeparatorChar, '/');
|
|
if (Path.AltDirectorySeparatorChar != Path.DirectorySeparatorChar)
|
|
value = value.Replace(Path.AltDirectorySeparatorChar, '/');
|
|
return value;
|
|
}
|
|
|
|
private static string FormatErrors(IEnumerable<ParseError> errors)
|
|
{
|
|
return "tsql100_parse_error " + string.Join(
|
|
" | ",
|
|
errors.Take(10).Select(error =>
|
|
$"SQL{error.Number}@{error.Line}:{error.Column} {SafeMessage(error.Message)}"));
|
|
}
|
|
|
|
private static string SafeMessage(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) return "unknown_error";
|
|
string normalized = new string(value
|
|
.Where(character => !char.IsControl(character))
|
|
.Take(1000)
|
|
.ToArray());
|
|
return normalized.Length == 0 ? "unknown_error" : normalized;
|
|
}
|
|
}
|