121 lines
7.1 KiB
C#
121 lines
7.1 KiB
C#
using System.Data;
|
|
using System.Reflection;
|
|
using henlo_migration.Services;
|
|
using SqlSugar;
|
|
|
|
// Only the injected metadata reader is called. No app startup, configuration or Oracle connection.
|
|
var builder = typeof(DbServices).GetMethod("BuildPhysicalColumnEnsureSql",
|
|
BindingFlags.NonPublic | BindingFlags.Static, null,
|
|
[typeof(string), typeof(string), typeof(Func<string, SugarParameter[], DataTable>)], null)!;
|
|
var splitter = typeof(DbServices).GetMethod("SplitStatements", BindingFlags.NonPublic | BindingFlags.Static)!;
|
|
var passed = 0;
|
|
|
|
void Check(bool condition, string message)
|
|
{
|
|
if (!condition) throw new Exception(message);
|
|
}
|
|
|
|
void Run(string label, TableInfo[] tables, string kind, string? expectedTable,
|
|
bool columnExists = true, string? expectedError = null)
|
|
{
|
|
var columnReads = 0;
|
|
string? queriedTable = null;
|
|
DataTable Read(string sql, SugarParameter[] parameters)
|
|
{
|
|
var result = new DataTable();
|
|
if (sql.Contains("FROM AD_TABLE"))
|
|
{
|
|
result.Columns.Add("ID", typeof(decimal));
|
|
result.Columns.Add("NAME", typeof(string));
|
|
result.Columns.Add("REALTABLE_ID", typeof(decimal));
|
|
var matches = sql.Contains("UPPER(NAME)")
|
|
? tables.Where(t => t.Name.Equals(parameters[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
|
|
: tables.Where(t => t.Id == Convert.ToDecimal(parameters[0].Value));
|
|
foreach (var t in matches)
|
|
result.Rows.Add(t.Id, t.Name, t.RealId.HasValue ? (object)t.RealId.Value : DBNull.Value);
|
|
}
|
|
else if (sql.Contains("FROM USER_TABLES"))
|
|
{
|
|
Check(sql.Contains("NOT EXISTS (SELECT 1 FROM USER_VIEWS"), "源库未排除视图");
|
|
Check(sql.Contains("NOT EXISTS (SELECT 1 FROM USER_MVIEWS"), "源库未排除物化视图");
|
|
queriedTable = parameters[0].Value.ToString();
|
|
result.Columns.Add("COUNT", typeof(int));
|
|
// Simulate the filtered dictionary result, including MVIEW's backing USER_TABLES row.
|
|
result.Rows.Add(kind == "TABLE" ? 1 : 0);
|
|
}
|
|
else if (sql.Contains("FROM USER_TAB_COLUMNS"))
|
|
{
|
|
columnReads++;
|
|
Check(parameters[0].Value.ToString() == queriedTable, "字段查询未使用实际表名");
|
|
Check(parameters[1].Value.ToString() == "TEST_COL", "字段名称改变");
|
|
foreach (var name in new[] { "DATA_TYPE", "DATA_LENGTH", "DATA_PRECISION", "DATA_SCALE", "CHAR_LENGTH", "CHAR_USED" })
|
|
result.Columns.Add(name, typeof(object));
|
|
if (columnExists) result.Rows.Add("VARCHAR2", 120, DBNull.Value, DBNull.Value, 30, "C");
|
|
}
|
|
else throw new Exception("意外查询:" + sql);
|
|
return result;
|
|
}
|
|
|
|
try
|
|
{
|
|
var sql = (string)builder.Invoke(null,
|
|
["BOS_TEST", "TEST_COL", (Func<string, SugarParameter[], DataTable>)Read])!;
|
|
Check(expectedError == null, label + " 应报错");
|
|
if (expectedTable != null)
|
|
{
|
|
Check(queriedTable == expectedTable, label + " 实际表解析错误");
|
|
Check(columnReads == 1, label + " 未读取实际字段");
|
|
Check(sql.Contains($"ALTER TABLE \"{expectedTable}\" ADD (\"TEST_COL\" VARCHAR2(30 CHAR))"), label + " DDL不正确");
|
|
Check(sql.Contains("NOT EXISTS (SELECT 1 FROM user_mviews"), "目标库未排除物化视图");
|
|
Check(sql.Contains($"table_name = '{expectedTable}' AND column_name = 'TEST_COL'"), "目标字段检查错误");
|
|
Check(sql.IndexOf("IF v_column_count = 0 THEN", StringComparison.Ordinal)
|
|
< sql.IndexOf("EXECUTE IMMEDIATE", StringComparison.Ordinal), "已有字段没有跳过保护");
|
|
Check(sql.Contains("IF v_table_count = 0 THEN") && sql.Contains("RAISE_APPLICATION_ERROR"), "目标表缺失未报错");
|
|
var blocks = (string[])splitter.Invoke(null, [sql + "BEGIN NULL; END;\n/\n"])!;
|
|
Check(blocks.Length == 2 && blocks[0].EndsWith("END;") && blocks[1] == "BEGIN NULL; END;", "PL/SQL分隔错误");
|
|
}
|
|
else
|
|
{
|
|
Check(!sql.Contains("ALTER TABLE") && !sql.Contains("EXECUTE IMMEDIATE"), label + " 不应生成物理DDL");
|
|
Check(columnReads == (kind == "TABLE" ? 1 : 0), label + " 发生非预期字段读取");
|
|
}
|
|
}
|
|
catch (TargetInvocationException ex) when (expectedError != null)
|
|
{
|
|
Check(ex.InnerException is InvalidOperationException
|
|
&& ex.InnerException.Message.Contains(expectedError), label + " 异常不正确:" + ex.InnerException?.Message);
|
|
Check(columnReads == 0, "无效映射仍读取字段");
|
|
}
|
|
Console.WriteLine("PASS " + label);
|
|
passed++;
|
|
}
|
|
|
|
Run("同名普通表", [new(1, "BOS_TEST", null)], "TABLE", "BOS_TEST");
|
|
Run("REALTABLE_ID映射", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "TABLE", "REAL_TEST");
|
|
Run("多级映射", [new(1, "BOS_TEST", 2), new(2, "MID_TEST", 3), new(3, "REAL_TEST", null)], "TABLE", "REAL_TEST");
|
|
Run("普通视图", [new(1, "BOS_TEST", null)], "VIEW", null);
|
|
Run("物化视图及其底层表", [new(1, "BOS_TEST", null)], "MVIEW", null);
|
|
Run("映射到视图", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "VIEW", null);
|
|
Run("映射到物化视图", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "MVIEW", null);
|
|
Run("源物理对象不存在", [new(1, "BOS_TEST", null)], "MISSING", null);
|
|
Run("BOS计算字段", [new(1, "BOS_TEST", null)], "TABLE", null, columnExists: false);
|
|
Run("映射实际表缺少字段", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", null)], "TABLE", null, columnExists: false);
|
|
Run("映射记录缺失", [new(1, "BOS_TEST", 2)], "TABLE", null, expectedError: "不存在或不唯一");
|
|
Run("映射循环", [new(1, "BOS_TEST", 2), new(2, "REAL_TEST", 1)], "TABLE", null, expectedError: "循环引用");
|
|
Run("自引用", [new(1, "BOS_TEST", 1)], "TABLE", null, expectedError: "循环引用");
|
|
Run("BOS表不存在", [], "TABLE", null, expectedError: "不存在");
|
|
Run("BOS表名重复", [new(1, "BOS_TEST", null), new(2, "BOS_TEST", null)], "TABLE", null, expectedError: "多条");
|
|
Run("映射ID不唯一", [new(1, "BOS_TEST", 2), new(2, "REAL_A", null), new(2, "REAL_B", null)], "TABLE", null, expectedError: "不存在或不唯一");
|
|
Run("实际表名称为空", [new(1, "BOS_TEST", 2), new(2, "", null)], "TABLE", null, expectedError: "名称为空");
|
|
Run("实际表名称规范化", [new(1, "BOS_TEST", 2), new(2, "real_test", null)], "TABLE", "REAL_TEST");
|
|
var representativeSql = "CREATE TABLE TEST_T (ID NUMBER);\n/\n"
|
|
+ "COMMENT ON COLUMN TEST_T.ID IS '编号';\n/\n"
|
|
+ "ALTER TABLE TEST_T ADD CONSTRAINT TEST_PK PRIMARY KEY (ID);\n/\n"
|
|
+ "BEGIN NULL; END;\n/\nINSERT INTO TEST_T (ID) VALUES (1);\n/\n";
|
|
var statements = (string[])splitter.Invoke(null, [representativeSql])!;
|
|
Check(statements.Length == 5 && statements[3] == "BEGIN NULL; END;", "代表性DDL/注释/约束/PLSQL/INSERT分隔回归失败");
|
|
Console.WriteLine("PASS DDL/注释/约束/PLSQL/INSERT分隔");
|
|
Console.WriteLine($"通过 {passed} 个离线回归场景(未连接数据库)。");
|
|
|
|
record TableInfo(decimal Id, string Name, decimal? RealId);
|