Files
henlo_migration_wpf10/henlo_migration_wpf10/Services/DbServices.cs
T

641 lines
31 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using SqlSugar;
using Oracle.ManagedDataAccess.Client;
using Oracle.ManagedDataAccess.Types;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using henlo_migration.Common;
using henlo_migration.Enums;
using henlo_migration.Models;
namespace henlo_migration.Services
{
public class DbServices
{
/// <summary>获取表建表SQL - 包含建表语句、表注释、字段注释、主键、唯一键、外键、索引,不依赖存储过程</summary>
public static string GetTableSql(string tableName)
{
var db = SqlSugarHelper.DbOrig;
string t = tableName.Trim().ToUpperInvariant();
var sb = new StringBuilder();
// 0. 关闭不需要的 DDL 附加属性
db.Ado.ExecuteCommand(@"
BEGIN
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'STORAGE', FALSE);
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'TABLESPACE', FALSE);
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES', FALSE);
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'CONSTRAINTS', FALSE);
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'REF_CONSTRAINTS', FALSE);
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'CONSTRAINTS_AS_ALTER', TRUE);
END;");
// 1. 建表语句
var ddlResult = db.Ado.GetDataTable(
"SELECT DBMS_METADATA.GET_DDL('TABLE', :tbl) AS SQL_TEXT FROM DUAL",
new SugarParameter("tbl", t));
if (ddlResult.Rows.Count > 0)
{
sb.Append(ddlResult.Rows[0]["SQL_TEXT"]?.ToString() ?? "");
sb.AppendLine(";\n/");
}
// 2. 表注释(每个语句末尾加 ;/\n,便于 SplitStatements 分割)
var tabCmt = db.Ado.GetDataTable(
"SELECT COMMENTS FROM USER_TAB_COMMENTS WHERE TABLE_NAME = :tbl AND COMMENTS IS NOT NULL",
new SugarParameter("tbl", t));
foreach (DataRow r in tabCmt.Rows)
sb.AppendLine($"COMMENT ON TABLE \"{t}\" IS '{EscapeQuote(r["COMMENTS"].ToString())}';\n/");
// 3. 字段注释
var colCmt = db.Ado.GetDataTable(
"SELECT COLUMN_NAME, COMMENTS FROM USER_COL_COMMENTS WHERE TABLE_NAME = :tbl AND COMMENTS IS NOT NULL ORDER BY COLUMN_NAME",
new SugarParameter("tbl", t));
foreach (DataRow r in colCmt.Rows)
{
string cName = r["COLUMN_NAME"].ToString() ?? "";
sb.AppendLine($"COMMENT ON COLUMN \"{t}\".\"{cName}\" IS '{EscapeQuote(r["COMMENTS"].ToString())}';\n/");
}
// 4. 主键 & 唯一键
var pkUkCons = db.Ado.GetDataTable(
"SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM USER_CONSTRAINTS WHERE TABLE_NAME = :tbl AND CONSTRAINT_TYPE IN ('P','U') ORDER BY CONSTRAINT_TYPE, CONSTRAINT_NAME",
new SugarParameter("tbl", t));
foreach (DataRow cr in pkUkCons.Rows)
{
var cols = db.Ado.GetDataTable(
"SELECT COLUMN_NAME FROM USER_CONS_COLUMNS WHERE CONSTRAINT_NAME = :cn ORDER BY POSITION",
new SugarParameter("cn", cr["CONSTRAINT_NAME"].ToString()));
string colList = "";
for (int i = 0; i < cols.Rows.Count; i++)
colList += (i > 0 ? ", " : "") + $"\"{cols.Rows[i]["COLUMN_NAME"]}\"";
string kind = cr["CONSTRAINT_TYPE"].ToString() == "P" ? "PRIMARY KEY" : "UNIQUE";
sb.AppendLine($"ALTER TABLE \"{t}\" ADD CONSTRAINT \"{cr["CONSTRAINT_NAME"]}\" {kind} ({colList});\n/");
}
// 5. 外键
var fkCons = db.Ado.GetDataTable(
"SELECT CONSTRAINT_NAME, R_CONSTRAINT_NAME, DELETE_RULE FROM USER_CONSTRAINTS WHERE TABLE_NAME = :tbl AND CONSTRAINT_TYPE = 'R' ORDER BY CONSTRAINT_NAME",
new SugarParameter("tbl", t));
foreach (DataRow fr in fkCons.Rows)
{
var localCols = db.Ado.GetDataTable(
"SELECT COLUMN_NAME FROM USER_CONS_COLUMNS WHERE CONSTRAINT_NAME = :cn ORDER BY POSITION",
new SugarParameter("cn", fr["CONSTRAINT_NAME"].ToString()));
var refCons = db.Ado.GetDataTable(
"SELECT TABLE_NAME FROM USER_CONSTRAINTS WHERE CONSTRAINT_NAME = :cn",
new SugarParameter("cn", fr["R_CONSTRAINT_NAME"].ToString()));
if (refCons.Rows.Count > 0)
{
var refCols = db.Ado.GetDataTable(
"SELECT COLUMN_NAME FROM USER_CONS_COLUMNS WHERE CONSTRAINT_NAME = :cn ORDER BY POSITION",
new SugarParameter("cn", fr["R_CONSTRAINT_NAME"].ToString()));
string localList = "";
for (int i = 0; i < localCols.Rows.Count; i++)
localList += (i > 0 ? ", " : "") + $"\"{localCols.Rows[i]["COLUMN_NAME"]}\"";
string refList = "";
for (int i = 0; i < refCols.Rows.Count; i++)
refList += (i > 0 ? ", " : "") + $"\"{refCols.Rows[i]["COLUMN_NAME"]}\"";
string delRule = fr["DELETE_RULE"].ToString();
string onDel = delRule == "CASCADE" ? " ON DELETE CASCADE"
: delRule == "SET NULL" ? " ON DELETE SET NULL" : "";
sb.AppendLine($"ALTER TABLE \"{t}\" ADD CONSTRAINT \"{fr["CONSTRAINT_NAME"]}\" FOREIGN KEY ({localList}) REFERENCES \"{refCons.Rows[0]["TABLE_NAME"]}\" ({refList}){onDel};\n/");
}
}
// 6. 普通索引
var pkUkNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var pkUkRaw = db.Ado.GetDataTable(
"SELECT CONSTRAINT_NAME FROM USER_CONSTRAINTS WHERE TABLE_NAME = :tbl AND CONSTRAINT_TYPE IN ('P','U')",
new SugarParameter("tbl", t));
foreach (DataRow r in pkUkRaw.Rows) pkUkNames.Add(r["CONSTRAINT_NAME"].ToString());
var idxRaw = db.Ado.GetDataTable(
"SELECT INDEX_NAME, UNIQUENESS FROM USER_INDEXES WHERE TABLE_NAME = :tbl AND INDEX_TYPE IN ('NORMAL','FUNCTION-BASED NORMAL') ORDER BY INDEX_NAME",
new SugarParameter("tbl", t));
foreach (DataRow ir in idxRaw.Rows)
{
if (pkUkNames.Contains(ir["INDEX_NAME"].ToString())) continue;
var idxCols = db.Ado.GetDataTable(
"SELECT COLUMN_NAME FROM USER_IND_COLUMNS WHERE INDEX_NAME = :idx_nm ORDER BY COLUMN_POSITION",
new SugarParameter("idx_nm", ir["INDEX_NAME"].ToString()));
string colList = "";
for (int i = 0; i < idxCols.Rows.Count; i++)
colList += (i > 0 ? ", " : "") + $"\"{idxCols.Rows[i]["COLUMN_NAME"]}\"";
string unique = ir["UNIQUENESS"].ToString() == "UNIQUE" ? "UNIQUE " : "";
sb.AppendLine($"CREATE {unique}INDEX \"{ir["INDEX_NAME"]}\" ON \"{t}\" ({colList});\n/");
}
return sb.ToString();
}
private static string EscapeQuote(string s)
=> s?.Replace("'", "''") ?? "";
private const string BosReaderProcedureName = "HMIG_GET_TABLE_BOS_31";
private const string BosReaderProcedureFileName = "hmig_get_table_bos_31.prc";
private const string BosFieldReaderProcedureName = "HMIG_GET_TABLE_BOS_FIELD_31";
private const string BosFieldReaderProcedureFileName = "hmig_get_table_bos_field_31.prc";
/// <summary>确保源库存在且启用了指定辅助过程;缺失时从 pre 目录安装</summary>
private static void EnsureReaderProcedure(string procedureName, string procedureFileName)
{
var db = SqlSugarHelper.DbOrig;
var objectResult = db.Ado.GetDataTable(
"SELECT STATUS FROM USER_OBJECTS WHERE OBJECT_NAME = :name AND OBJECT_TYPE = 'PROCEDURE'",
new SugarParameter("name", procedureName));
if (objectResult.Rows.Count > 0)
{
if (string.Equals(objectResult.Rows[0]["STATUS"]?.ToString(), "VALID", StringComparison.OrdinalIgnoreCase))
return;
ThrowProcedureErrors(procedureName, $"源库过程 {procedureName} 状态无效");
throw new InvalidOperationException($"源库过程 {procedureName} 状态无效,请检查后重试。");
}
string scriptPath = Path.Combine(CommonHelper.PrePath, procedureFileName);
if (!File.Exists(scriptPath))
throw new FileNotFoundException($"缺少 BOS 配置读取过程脚本:{scriptPath}", scriptPath);
string script = File.ReadAllText(scriptPath).TrimEnd();
// 兼容带 SQL*Plus 结束符的脚本;ODP.NET 执行时不能包含末尾的独立斜杠。
if (script.EndsWith("/", StringComparison.Ordinal))
script = script.Substring(0, script.Length - 1).TrimEnd();
if (string.IsNullOrWhiteSpace(script))
throw new InvalidDataException($"BOS 配置读取过程脚本为空:{scriptPath}");
db.Ado.ExecuteCommand(script);
ThrowProcedureErrors(procedureName, $"自动安装 {procedureName} 后存在编译错误");
var installedResult = db.Ado.GetDataTable(
"SELECT STATUS FROM USER_OBJECTS WHERE OBJECT_NAME = :name AND OBJECT_TYPE = 'PROCEDURE'",
new SugarParameter("name", procedureName));
if (installedResult.Rows.Count == 0 ||
!string.Equals(installedResult.Rows[0]["STATUS"]?.ToString(), "VALID", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"自动安装 {procedureName} 失败,源库中未找到有效过程。");
}
}
private static void ThrowProcedureErrors(string procedureName, string title)
{
var errors = SqlSugarHelper.DbOrig.Ado.GetDataTable(
"SELECT LINE, POSITION, TEXT FROM USER_ERRORS WHERE NAME = :name AND TYPE = 'PROCEDURE' ORDER BY SEQUENCE",
new SugarParameter("name", procedureName));
if (errors.Rows.Count == 0)
return;
var message = new StringBuilder();
message.AppendLine(title + ":");
foreach (DataRow row in errors.Rows)
message.AppendLine($"Line {row["LINE"]}, Pos {row["POSITION"]}: {row["TEXT"]}");
throw new InvalidOperationException(message.ToString());
}
/// <summary>获取BOS表配置 - 确保过程存在后,调用过程获取 r_table,再查 tabletext</summary>
public static string GetTableBos(string tableName)
{
var db = SqlSugarHelper.DbOrig;
string t = tableName.Trim().ToUpperInvariant();
EnsureReaderProcedure(BosReaderProcedureName, BosReaderProcedureFileName);
// 1. 调用存储过程,获取 r_id 和 r_table
var pRId = new SugarParameter("r_id", null, true) { DbType = System.Data.DbType.Decimal };
var pRTable = new SugarParameter("r_table", null, true) { DbType = System.Data.DbType.String, Size = 200 };
db.Ado.ExecuteCommand(
"BEGIN hmig_get_table_bos_31(:p_name, :r_id, :r_table); END;",
new SugarParameter("p_name", t),
pRId,
pRTable);
// 解析 OUT 参数
string rTable = pRTable.Value?.ToString() ?? "";
string rId = pRId.Value?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(rTable) || string.IsNullOrWhiteSpace(rId)) return "";
// 2. 根据 r_table 确定字段名,用 r_id 查对应记录的文本
string colName = rTable.ToUpperInvariant() == "T_TABLETEXT"
? "tabletext"
: "table_text";
var textResult = db.Ado.GetDataTable(
$"SELECT {colName} FROM {rTable} WHERE id = :rid",
new SugarParameter("rid", rId));
return textResult.Rows.Count > 0 ? textResult.Rows[0][colName]?.ToString() ?? "" : "";
}
/// <summary>解析 TABLE.COLUMN 格式的 BOS 字段名称</summary>
public static bool TryParseBosFieldName(string qualifiedFieldName, out string tableName, out string columnName)
{
tableName = "";
columnName = "";
if (string.IsNullOrWhiteSpace(qualifiedFieldName))
return false;
string value = qualifiedFieldName.Trim();
int separator = value.IndexOf('.');
if (separator <= 0 || separator != value.LastIndexOf('.') || separator >= value.Length - 1)
return false;
tableName = value.Substring(0, separator).Trim().ToUpperInvariant();
columnName = value.Substring(separator + 1).Trim().ToUpperInvariant();
return tableName.Length > 0 && columnName.Length > 0;
}
/// <summary>获取单个 BOS 字段配置,返回可在目标库执行的完整 PL/SQL</summary>
public static string GetTableBosField(string qualifiedFieldName)
{
if (!TryParseBosFieldName(qualifiedFieldName, out string tableName, out string columnName))
throw new ArgumentException("BOS 字段配置必须使用 TABLE.COLUMN 格式。", nameof(qualifiedFieldName));
string physicalColumnSql = BuildPhysicalColumnEnsureSql(tableName, columnName);
EnsureReaderProcedure(BosFieldReaderProcedureName, BosFieldReaderProcedureFileName);
var connection = SqlSugarHelper.DbOrig.Ado.Connection as OracleConnection
?? throw new InvalidOperationException("源数据库连接不是 OracleConnection,无法读取 CLOB 输出参数。");
bool closeAfterRead = connection.State != ConnectionState.Open;
try
{
if (closeAfterRead)
connection.Open();
using var command = connection.CreateCommand();
command.BindByName = true;
command.CommandText =
"BEGIN hmig_get_table_bos_field_31(:p_table_name, :p_column_name, :r_sql); END;";
command.Parameters.Add(new OracleParameter("p_table_name", OracleDbType.Varchar2)
{
Direction = ParameterDirection.Input,
Value = tableName
});
command.Parameters.Add(new OracleParameter("p_column_name", OracleDbType.Varchar2)
{
Direction = ParameterDirection.Input,
Value = columnName
});
var output = new OracleParameter("r_sql", OracleDbType.Clob)
{
Direction = ParameterDirection.Output
};
command.Parameters.Add(output);
command.ExecuteNonQuery();
string bosSql;
if (output.Value == null || output.Value == DBNull.Value)
bosSql = "";
else if (output.Value is OracleClob clob)
bosSql = clob.IsNull ? "" : clob.Value;
else
bosSql = output.Value.ToString() ?? "";
// 旧版过程输出中的说明只针对其自身生成的 BOS 块;最终文件已在开头加入物理字段检查。
bosSql = bosSql.Replace(
"-- 注意:本文件不包含 Oracle 物理字段 DDL。",
"-- BOS 字段配置块;物理字段检查位于本文件开头。",
StringComparison.Ordinal);
return physicalColumnSql + bosSql;
}
finally
{
if (closeAfterRead && connection.State != ConnectionState.Closed)
connection.Close();
}
}
/// <summary>生成目标库物理字段存在性检查;源对象不是物理表或字段时只输出说明</summary>
private static string BuildPhysicalColumnEnsureSql(string tableName, string columnName)
{
var db = SqlSugarHelper.DbOrig;
int sourceTableCount = db.Ado.GetInt(
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = :table_name",
new SugarParameter("table_name", tableName));
if (sourceTableCount == 0)
return $"-- 源库对象 {tableName} 不是物理表,不生成物理字段 DDL。\n";
var columnResult = db.Ado.GetDataTable(@"
SELECT DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE,
CHAR_LENGTH, CHAR_USED
FROM USER_TAB_COLUMNS
WHERE TABLE_NAME = :table_name
AND COLUMN_NAME = :column_name",
new SugarParameter("table_name", tableName),
new SugarParameter("column_name", columnName));
if (columnResult.Rows.Count == 0)
return $"-- 源库物理表 {tableName} 不存在字段 {columnName},仅导出 BOS 字段配置。\n";
string columnType = BuildOracleColumnType(columnResult.Rows[0]);
string quotedTable = QuoteOracleIdentifier(tableName);
string quotedColumn = QuoteOracleIdentifier(columnName);
string alterSql = $"ALTER TABLE {quotedTable} ADD ({quotedColumn} {columnType})";
var sql = new StringBuilder();
sql.AppendLine($"-- 确保目标库物理字段存在:{tableName}.{columnName}");
sql.AppendLine("DECLARE");
sql.AppendLine(" v_table_count NUMBER;");
sql.AppendLine(" v_column_count NUMBER;");
sql.AppendLine("BEGIN");
sql.AppendLine($" SELECT COUNT(*) INTO v_table_count FROM user_tables WHERE table_name = '{EscapeQuote(tableName)}';");
sql.AppendLine(" IF v_table_count = 0 THEN");
sql.AppendLine($" RAISE_APPLICATION_ERROR(-20201, '目标库缺少物理表:{EscapeQuote(tableName)}');");
sql.AppendLine(" END IF;");
sql.AppendLine($" SELECT COUNT(*) INTO v_column_count FROM user_tab_columns WHERE table_name = '{EscapeQuote(tableName)}' AND column_name = '{EscapeQuote(columnName)}';");
sql.AppendLine(" IF v_column_count = 0 THEN");
sql.AppendLine($" EXECUTE IMMEDIATE '{EscapeQuote(alterSql)}';");
sql.AppendLine(" END IF;");
sql.AppendLine("END;");
sql.AppendLine("/");
return sql.ToString();
}
private static string BuildOracleColumnType(DataRow row)
{
string dataType = row["DATA_TYPE"]?.ToString()?.ToUpperInvariant() ?? "";
if (string.IsNullOrWhiteSpace(dataType))
throw new InvalidDataException("无法读取源库物理字段的数据类型。");
long dataLength = Convert.ToInt64(row["DATA_LENGTH"]);
long? precision = row["DATA_PRECISION"] == DBNull.Value
? null
: Convert.ToInt64(row["DATA_PRECISION"]);
long? scale = row["DATA_SCALE"] == DBNull.Value
? null
: Convert.ToInt64(row["DATA_SCALE"]);
long charLength = row["CHAR_LENGTH"] == DBNull.Value
? dataLength
: Convert.ToInt64(row["CHAR_LENGTH"]);
string charUsed = row["CHAR_USED"]?.ToString()?.ToUpperInvariant() ?? "";
return dataType switch
{
"CHAR" or "VARCHAR2" =>
$"{dataType}({(charUsed == "C" ? charLength : dataLength)} {(charUsed == "C" ? "CHAR" : "BYTE")})",
"NCHAR" or "NVARCHAR2" => $"{dataType}({charLength})",
"NUMBER" when precision.HasValue && scale.HasValue => $"NUMBER({precision.Value},{scale.Value})",
"NUMBER" when precision.HasValue => $"NUMBER({precision.Value})",
"FLOAT" when precision.HasValue => $"FLOAT({precision.Value})",
"RAW" => $"RAW({dataLength})",
"UROWID" => $"UROWID({dataLength})",
_ => dataType
};
}
private static string QuoteOracleIdentifier(string value)
=> $"\"{value.Replace("\"", "\"\"")}\"";
/// <summary>获取所有存储过程和函数列表 - 查 USER_OBJECTS,返回纯名称(不带前缀),不依赖存储过程</summary>
public static string GetAllPrc()
{
var db = SqlSugarHelper.DbOrig;
var result = db.Ado.GetDataTable(
"SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('PROCEDURE','FUNCTION') AND STATUS='VALID' ORDER BY OBJECT_NAME");
var sb = new StringBuilder();
foreach (DataRow row in result.Rows)
sb.AppendLine(row["OBJECT_NAME"].ToString());
return sb.ToString().TrimEnd();
}
/// <summary>获取单个存储过程/函数源码 - 查 USER_SOURCE,拼接完整 CREATE OR REPLACE 语句</summary>
public static string GetPrcSql(string prcName)
{
var db = SqlSugarHelper.DbOrig;
string name = prcName.Trim().ToUpperInvariant();
// 1. 查类型(PROCEDURE 或 FUNCTION)
var typeResult = db.Ado.GetDataTable(
"SELECT OBJECT_TYPE FROM USER_OBJECTS WHERE OBJECT_NAME = :n AND OBJECT_TYPE IN ('PROCEDURE','FUNCTION', 'TRIGGER')",
new SugarParameter("n", name));
if (typeResult.Rows.Count == 0) return "";
string objType = typeResult.Rows[0]["OBJECT_TYPE"].ToString(); // PROCEDURE 或 FUNCTION
// 2. 查源码
var result = db.Ado.GetDataTable(
"SELECT TEXT FROM USER_SOURCE WHERE NAME = :n ORDER BY LINE",
new SugarParameter("n", name));
var sb = new StringBuilder();
foreach (DataRow row in result.Rows)
sb.Append(row["TEXT"]);
// 3. 拼接完整 CREATE OR REPLACE 语句
string code = sb.ToString();
if (code.StartsWith(objType))
code = "CREATE OR REPLACE " + code;
// 4. 添加末尾的 /(Oracle PL/SQL 块结束符)
code = code.TrimEnd();
if (!code.EndsWith("/"))
code += "\n/";
return code;
}
/// <summary>获取表数据(INSERT语句)- 直接 SELECT + C# 拼接,不依赖存储过程</summary>
public static string GetTableData(string tableName)
{
var db = SqlSugarHelper.DbOrig;
string safeName = tableName.Trim().ToUpperInvariant();
// 1. 检查行数
var count = db.Ado.GetInt($"SELECT COUNT(*) FROM {safeName}");
if (count == 0)
return $"-- 表 {safeName} 无数据";
if (count > 200)
return $"-- 错误: 表 {safeName} 数据行数({count})超过200行限制";
// 2. 取列定义(列名 + 数据类型)
var cols = db.Ado.GetDataTable(@"
SELECT COLUMN_NAME, DATA_TYPE
FROM USER_TAB_COLUMNS
WHERE TABLE_NAME = :tbl
ORDER BY COLUMN_ID",
new SugarParameter("tbl", safeName));
// 3. 查所有数据
var rows = db.Ado.GetDataTable($"SELECT * FROM {safeName}");
// 4. C# 端拼接 INSERT
var sb = new System.Text.StringBuilder();
sb.AppendLine($"-- 表 {safeName} 共 {count} 行数据");
for (int ri = 0; ri < rows.Rows.Count; ri++)
{
var dr = rows.Rows[ri];
sb.Append("INSERT INTO " + safeName + " (");
// 列名
for (int ci = 0; ci < cols.Rows.Count; ci++)
sb.Append(ci > 0 ? ", " + cols.Rows[ci]["COLUMN_NAME"] : cols.Rows[ci]["COLUMN_NAME"].ToString());
sb.Append(") VALUES (");
// 值(按列名取,保证和列名对应)
for (int ci = 0; ci < cols.Rows.Count; ci++)
{
if (ci > 0) sb.Append(", ");
var colName = cols.Rows[ci]["COLUMN_NAME"].ToString();
var dataType = cols.Rows[ci]["DATA_TYPE"].ToString();
var val = dr[colName];
if (val == null || val == DBNull.Value)
sb.Append("NULL");
else if (dataType == "DATE" || dataType == "TIMESTAMP")
sb.Append($"TO_DATE('{val:yyyy-MM-dd HH:mm:ss}', 'YYYY-MM-DD HH24:MI:SS')");
else
sb.Append("'" + val.ToString()
.Replace("'", "''")
.Replace("\r", "").Replace("\n", " ") + "'");
}
sb.AppendLine(");");
}
sb.Append("COMMIT;");
return sb.ToString();
}
private static readonly string[] PlsqlKeywords = new[]
{
"PROCEDURE", "FUNCTION", "PACKAGE", "TRIGGER", "TYPE", "VIEW"
};
/// <summary>从 CREATE 语句中提取对象名(用于查 USER_ERRORS)</summary>
private static string? GetObjectNameFromCreate(string sql)
{
string upper = sql.ToUpperInvariant();
int createIdx = upper.IndexOf("CREATE ");
if (createIdx < 0) return null;
// 找到 PROCEDURE / FUNCTION / PACKAGE 等关键字的位置
int bestIdx = -1, bestLen = 0;
string? keyword = null;
foreach (var kw in PlsqlKeywords)
{
int idx = upper.IndexOf(kw, createIdx);
if (idx >= 0 && (bestIdx < 0 || idx < bestIdx))
{
bestIdx = idx;
bestLen = kw.Length;
keyword = kw;
}
}
if (bestIdx < 0) return null;
// 关键字后面的第一个非空 token 就是对象名(支持 schema.name 和 "引号名")
int pos = bestIdx + bestLen;
while (pos < sql.Length && char.IsWhiteSpace(sql[pos])) pos++;
if (pos >= sql.Length) return null;
StringBuilder sb = new();
// 处理带引号的标识符
if (sql[pos] == '"')
{
pos++; // 跳过 opening quote
while (pos < sql.Length && sql[pos] != '"') sb.Append(sql[pos++]);
}
else
{
while (pos < sql.Length && !char.IsWhiteSpace(sql[pos]) && sql[pos] != '(' && sql[pos] != '.')
sb.Append(sql[pos++]);
// 处理 schema.name(再往前一个 token)
// 实际上上面已经读到 . 为止,需要重来
}
// 简化:直接取 "keyword 之后的第一个完整 token(直到空格或括号)"
string token = GetNextToken(sql, bestIdx + bestLen);
if (string.IsNullOrEmpty(token)) return null;
// token 可能是 schema.name,只取 name 部分
if (token.Contains('.'))
token = token.Substring(token.LastIndexOf('.') + 1);
// 去掉引号
token = token.Trim('"');
return token.ToUpperInvariant();
}
private static string GetNextToken(string sql, int start)
{
while (start < sql.Length && char.IsWhiteSpace(sql[start])) start++;
if (start >= sql.Length) return "";
StringBuilder sb = new();
// 引号标识符
if (sql[start] == '"')
{
start++; // 跳过 "
while (start < sql.Length && sql[start] != '"') sb.Append(sql[start++]);
return sb.ToString();
}
while (start < sql.Length && !char.IsWhiteSpace(sql[start]) && sql[start] != '(')
sb.Append(sql[start++]);
return sb.ToString();
}
/// <summary>检查刚执行的 CREATE 语句是否有编译错误,有则抛异常</summary>
private static void CheckCompilationErrors(string sql)
{
string? objName = GetObjectNameFromCreate(sql);
if (objName == null) return;
try
{
string query = @"SELECT LINE, POSITION, TEXT FROM USER_ERRORS WHERE NAME = :name ORDER BY LINE, POSITION";
var dt = SqlSugarHelper.DbDest.Ado.GetDataTable(query, new { name = objName });
if (dt.Rows.Count == 0) return;
var sb = new StringBuilder();
sb.AppendLine($"对象 {objName} 编译错误:");
foreach (System.Data.DataRow row in dt.Rows)
{
sb.AppendLine($" Line {row["LINE"]}, Pos {row["POSITION"]}: {row["TEXT"]}");
}
throw new Exception(sb.ToString());
}
catch (Exception ex) when (!(ex is Exception && ex.Message.Contains("编译错误")))
{
// 查询 USER_ERRORS 失败(如表不存在),忽略,不阻断写入流程
}
}
/// <summary>在目标库执行建表语句</summary>
public static void ExecuteCreate(string sql)
{
foreach (var stmt in SplitStatements(sql))
{
// 彻底 Trim 去除首尾空白和换行
string clean = stmt.Trim();
if (string.IsNullOrWhiteSpace(clean)) continue;
// 写日志便于排查 ORA 错误(写执行前的清理后语句)
string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "execute_log.txt");
File.AppendAllText(logPath, $"=== {DateTime.Now:HH:mm:ss} [{clean.Length} chars] ===\n{clean}\n\n");
try
{
SqlSugarHelper.DbDest.Ado.ExecuteCommand(clean);
CheckCompilationErrors(clean);
}
catch (Exception ex)
{
// 追加错误信息到日志
File.AppendAllText(logPath, $"!!! ERROR: {ex.Message}\n");
throw; // 重新抛出,让调用方捕获
}
}
}
private static string[] SplitStatements(string sql)
{
// Oracle PL/SQL 块用 \n/ 分隔
var parts = sql.Split(new[] { "\n/" }, StringSplitOptions.RemoveEmptyEntries);
var result = new List<string>();
foreach (var part in parts)
{
// Trim 掉首尾空白,保留语句主体
var trimmed = part.Trim();
if (!string.IsNullOrWhiteSpace(trimmed))
result.Add(trimmed);
}
return result.ToArray();
}
}
}