641 lines
31 KiB
C#
641 lines
31 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|