feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<RestoreLockedMode>true</RestoreLockedMode>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom"
|
||||
Version="180.59.2" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,635 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"Microsoft.SqlServer.TransactSql.ScriptDom": {
|
||||
"type": "Direct",
|
||||
"requested": "[180.59.2, )",
|
||||
"resolved": "180.59.2",
|
||||
"contentHash": "ttecvWn7bpVE0LCpzDCypioJ5UFj7voWWdC6y1FDK3unFY4CAIfmCTIfFyjP4LdkMcpKs2C9KvljY7lth30Log=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user