Files
Omika/tools/Omika.HfSqlProbe/Program.cs
T
2026-10-23 18:46:50 +02:00

705 lines
35 KiB
C#

using System.Data;
using System.Data.Odbc;
namespace Omika.HfSqlProbe;
public static class Program
{
public static int Main(string[] args)
{
if (args.Length == 0) { PrintUsage(); return 2; }
string command = ProbeArgumentParser.NormalizeCommand(args);
if (!ProbeArgumentParser.IsValidCommand(command)) { Console.Error.WriteLine($"ERROR: unknown command '{command}'."); PrintUsage(); return 2; }
if (command == "--help") { PrintUsage(); return 0; }
var options = ProbeArgumentParser.Parse(args.AsSpan(1));
if (options.ConnectionString is null) { Console.Error.WriteLine("ERROR: --connection-string is required."); return 2; }
if (command == "capabilities") return RunCapabilities(options);
if (command == "audit") return RunAudit(options);
if (command == "table-exists") return RunTableExists(options);
if (command == "protected-access") return RunProtectedAccess(options);
if (ProbeArgumentParser.IsWriteCommand(command))
{
if (!options.AllowWriteProbe) { Console.Error.WriteLine("ERROR: This probe performs temporary writes."); Console.Error.WriteLine(" It must be run ONLY against a clone/test database."); Console.Error.WriteLine(" Add --allow-write-probe to confirm you understand this."); return 2; }
Console.WriteLine("===================================");
Console.WriteLine(" HFSQL ODBC WRITE PROBE");
Console.WriteLine("===================================");
Console.WriteLine();
Console.WriteLine("\u26a0 This probe performs TEMPORARY writes.");
Console.WriteLine(" It MUST run on a clone/test database ONLY.");
Console.WriteLine();
}
if (command == "transaction") return RunTransactionProbe(options);
if (command == "returning") return RunReturningProbe(options);
if (command == "client-insert") return RunClientInsert(options);
if (command == "client-graph") return RunClientGraph(options);
if (command == "omika-guid") return RunOmikaGuidProbe(options);
return 0;
}
static int RunCapabilities(ProbeOptions options)
{
Console.WriteLine("HFSQL ODBC CAPABILITY PROBE");
Console.WriteLine("===========================");
Console.WriteLine();
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
Console.WriteLine("Connection: OPEN");
Console.WriteLine($" Driver: {MaskConnectionProperty(connection.Driver)}");
Console.WriteLine($" DataSource: {MaskConnectionProperty(connection.DataSource)}");
Console.WriteLine($" ServerVersion: {connection.ServerVersion}");
Console.WriteLine($" Database property: {(string.IsNullOrEmpty(connection.Database) ? "(empty / not exposed by driver)" : MaskConnectionProperty(connection.Database))}");
Console.WriteLine();
Console.WriteLine("CONNECTION: PASS");
Console.WriteLine();
Console.WriteLine("── ODBC Metadata ──");
try
{
var tables = connection.GetSchema("Tables");
Console.WriteLine($" ODBC metadata: SUPPORTED");
Console.WriteLine($" Tables: {tables.Rows.Count}");
}
catch (Exception ex)
{
Console.WriteLine($" ODBC metadata: UNSUPPORTED ({ex.GetType().Name})");
}
Console.WriteLine();
Console.WriteLine("── Driver info ──");
using (var tx = connection.BeginTransaction()) { tx.Rollback(); Console.WriteLine(" BeginTransaction + Rollback: OK (API level)"); }
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($"CONNECTION FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 3; }
catch (Exception ex) { Console.Error.WriteLine($"CONNECTION FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 3; }
}
static int RunTransactionProbe(ProbeOptions options)
{
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required for transaction probe."); return 2; }
int code = ValidateIdentifiers(options.Table, options.IdColumn ?? "PKID", options.MarkerColumn ?? "Marker");
if (code != 0) return code;
string marker = $"PROBE_TX_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}";
string table = options.Table, markerCol = options.MarkerColumn ?? "Marker";
Console.WriteLine($"Table: {table}");
Console.WriteLine($"Marker: {marker}");
Console.WriteLine();
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
using var tx = connection.BeginTransaction();
Console.WriteLine("BEGIN TRANSACTION: OK");
using (var insertCmd = connection.CreateCommand()) { insertCmd.Transaction = tx; insertCmd.CommandText = ProbeSqlBuilder.BuildInsertMarker(table, markerCol); insertCmd.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($"INSERT: OK ({insertCmd.ExecuteNonQuery()} row(s))"); }
using (var selCmd = connection.CreateCommand()) { selCmd.Transaction = tx; selCmd.CommandText = ProbeSqlBuilder.BuildCountByMarker(table, markerCol); selCmd.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($"VISIBLE BEFORE ROLLBACK: {(Convert.ToInt32(selCmd.ExecuteScalar()) > 0 ? "YES" : "NO")}"); }
tx.Rollback();
Console.WriteLine("ROLLBACK: OK");
using (var selCmd2 = connection.CreateCommand()) { selCmd2.CommandText = ProbeSqlBuilder.BuildCountByMarker(table, markerCol); selCmd2.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($"ABSENT AFTER ROLLBACK: {(Convert.ToInt32(selCmd2.ExecuteScalar()) == 0 ? "YES" : "NO")}"); }
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
}
static int RunReturningProbe(ProbeOptions options)
{
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required for returning probe."); return 2; }
int code = ValidateIdentifiers(options.Table, options.IdColumn ?? "PKID", options.MarkerColumn ?? "Marker");
if (code != 0) return code;
string marker = $"PROBE_RET_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}";
string table = options.Table, idCol = options.IdColumn ?? "PKID", markerCol = options.MarkerColumn ?? "Marker";
Console.WriteLine($"Table: {table}");
Console.WriteLine($"ID col: {idCol}");
Console.WriteLine($"Marker: {marker}");
Console.WriteLine();
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
Console.WriteLine("-- METHOD A: ExecuteScalar --");
try
{
using var txA = connection.BeginTransaction();
using var cmdA = connection.CreateCommand();
cmdA.Transaction = txA;
cmdA.CommandText = ProbeSqlBuilder.BuildInsertReturning(table, idCol, markerCol);
cmdA.Parameters.Add(new OdbcParameter { Value = marker });
object? scalar = cmdA.ExecuteScalar();
Console.WriteLine($" Accepted: YES");
Console.WriteLine($" CLR type: {scalar?.GetType().Name ?? "null"}");
Console.WriteLine($" Raw value: {scalar}");
if (scalar is long l) Console.WriteLine($" Int64: {l} ({(l > 0 ? "VALID" : "ZERO/INVALID")})");
else if (scalar is not null) { try { Console.WriteLine($" Convert.ToInt64: {Convert.ToInt64(scalar)}"); } catch { Console.WriteLine(" Convert.ToInt64: FAILED"); } }
txA.Rollback();
}
catch (OdbcException ex) { Console.WriteLine($" ERROR: {ConnectionStringRedactor.Redact(ex.Message)}"); }
Console.WriteLine();
Console.WriteLine("-- METHOD B: ExecuteReader --");
try
{
using var txB = connection.BeginTransaction();
using var cmdB = connection.CreateCommand();
cmdB.Transaction = txB;
cmdB.CommandText = ProbeSqlBuilder.BuildInsertReturning(table, idCol, markerCol);
cmdB.Parameters.Add(new OdbcParameter { Value = marker });
using var reader = cmdB.ExecuteReader();
Console.WriteLine($" HasRows: {reader.HasRows}");
Console.WriteLine($" FieldCount: {reader.FieldCount}");
if (reader.Read()) { for (int f = 0; f < reader.FieldCount; f++) Console.WriteLine($" [{f}] {reader.GetName(f)} ({reader.GetDataTypeName(f)}): {reader.GetValue(f)}"); }
reader.Close();
txB.Rollback();
}
catch (OdbcException ex) { Console.WriteLine($" ERROR: {ConnectionStringRedactor.Redact(ex.Message)}"); }
Console.WriteLine();
Console.WriteLine("-- VERIFY ROLLBACK --");
using (var verifyCmd = connection.CreateCommand()) { verifyCmd.CommandText = ProbeSqlBuilder.BuildCountByMarker(table, markerCol); verifyCmd.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($" Rows with marker: {Convert.ToInt32(verifyCmd.ExecuteScalar())}"); }
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
}
static string DeriveProtectionStdPassword(string databaseName)
{
byte[] hash = System.Security.Cryptography.MD5.HashData(
System.Text.Encoding.UTF8.GetBytes(databaseName));
return Convert.ToHexString(hash).ToLowerInvariant();
}
static int RunProtectedAccess(ProbeOptions options)
{
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required."); return 2; }
string table = SqlIdentifierValidator.Validate(options.Table);
if (string.IsNullOrEmpty(options.DatabaseName)) { Console.Error.WriteLine("ERROR: --database-name is required."); return 2; }
string dbName = options.DatabaseName;
Console.WriteLine($"Table: {table}");
Console.WriteLine($"Database: {dbName}");
Console.WriteLine();
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
Console.WriteLine("Connection: OPEN");
// Step A: try normal access
Console.WriteLine("-- Normal access --");
bool directPassed = false;
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT COUNT(*) FROM {table}";
int count = Convert.ToInt32(cmd.ExecuteScalar());
Console.WriteLine($" ACCESS = UNPROTECTED (rows: {count})");
return 0;
}
catch (OdbcException ex)
{
string msg = ex.Message;
Console.WriteLine($" ERROR: {ConnectionStringRedactor.Redact(msg)}");
Console.WriteLine($" SQLState: {ex.Errors[0]?.SQLState}, NativeError: {ex.Errors[0]?.NativeError}");
Console.WriteLine(" Classification: DIRECT ACCESS FAILED — attempting OPEN FILE fallback");
}
// Step B: password fallback — PC SOFT official ODBC syntax
Console.WriteLine("-- ProtectionStd fallback (OPEN FILE USING) --");
string derived = DeriveProtectionStdPassword(dbName);
// Validate derived password is 32-char hex before any use
if (!System.Text.RegularExpressions.Regex.IsMatch(derived, @"^[0-9a-f]{32}$"))
{
Console.Error.WriteLine(" FATAL: Derived password failed validation.");
return 1;
}
Console.WriteLine($" Password derived: YES (32 hex chars)");
Console.WriteLine($" Password value: [NEVER DISPLAYED]");
// Execute OPEN FILE USING on the same connection — Client/Server form
try
{
using var openCmd = connection.CreateCommand();
// Extract connection properties for Client/Server OPEN FILE
string csDb = connection.Database;
if (string.IsNullOrEmpty(csDb)) csDb = dbName;
string csServer = connection.DataSource;
// Build Client/Server explicit OPEN FILE (sanitized form for reporting)
string openCmdText = $"OPEN FILE {table} SERVER '{csServer}' DATABASE '{csDb}' USER '<supplied>' USING '<redacted>'";
Console.WriteLine($" {openCmdText}");
openCmd.CommandText = $"OPEN FILE {table} SERVER '{csServer}' DATABASE '{csDb}' USER 'susana' USING '{derived}'";
openCmd.ExecuteNonQuery();
Console.WriteLine(" OPEN FILE (C/S): ACCEPTED");
// Retry SELECT
using var retryCmd = connection.CreateCommand();
retryCmd.CommandText = $"SELECT COUNT(*) FROM {table}";
int retryCount = Convert.ToInt32(retryCmd.ExecuteScalar());
Console.WriteLine($" SELECT after OPEN: PASS (rows: {retryCount})");
Console.WriteLine($" ACCESS = PROTECTED — ProtectionStd VERIFIED");
return 0;
}
catch (OdbcException ex2)
{
Console.WriteLine($" OPEN FILE (C/S) error: {ConnectionStringRedactor.Redact(ex2.Message)}");
Console.WriteLine($" SQLState: {ex2.Errors[0]?.SQLState}, NativeError: {ex2.Errors[0]?.NativeError}");
Console.WriteLine(" Classification: OPEN FILE USING FAILED");
return 1;
}
}
catch (OdbcException ex)
{
Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}");
return 1;
}
}
static int RunTableExists(ProbeOptions options)
{
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required."); return 2; }
string table = SqlIdentifierValidator.Validate(options.Table);
Console.WriteLine($"Checking: {table}");
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT COUNT(*) FROM {table}";
int count = Convert.ToInt32(cmd.ExecuteScalar());
Console.WriteLine($" EXISTS: YES (rows: {count})");
// Also try GetSchema
try
{
var cols = connection.GetSchema("Columns", [null, null, table, null]);
Console.WriteLine($" GetSchema Columns: {cols.Rows.Count} columns");
foreach (System.Data.DataRow row in cols.Rows)
{
string col = row["COLUMN_NAME"]?.ToString() ?? "?";
string type = row["TYPE_NAME"]?.ToString() ?? "?";
int size = row["COLUMN_SIZE"] is int s ? s : 0;
bool nullable = row["IS_NULLABLE"]?.ToString() == "YES";
Console.WriteLine($" {col,-25} {type,-10} size={size,4} nullable={nullable}");
}
}
catch { Console.WriteLine(" GetSchema Columns: FAILED"); }
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($" DOES NOT EXIST or error: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
}
static int RunAudit(ProbeOptions options)
{
Console.WriteLine("HFSQL ODBC PHYSICAL AUDIT");
Console.WriteLine("========================");
Console.WriteLine();
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
Console.WriteLine("Connected.");
Console.WriteLine();
// ── MetaDataCollections ──
Console.WriteLine("── ODBC MetaDataCollections ──");
try
{
var md = connection.GetSchema("MetaDataCollections");
foreach (System.Data.DataRow row in md.Rows)
Console.WriteLine($" {row["CollectionName"]}: {row["NumberOfRestrictions"]} restrictions");
}
catch (Exception ex) { Console.WriteLine($" ERROR: {ex.Message}"); }
Console.WriteLine();
// ── Tables ──
Console.WriteLine("── Tables ──");
var tables = connection.GetSchema("Tables");
var tableNames = new List<string>();
foreach (System.Data.DataRow row in tables.Rows)
{
string? name = row["TABLE_NAME"]?.ToString();
if (!string.IsNullOrEmpty(name)) tableNames.Add(name);
}
tableNames.Sort();
int i = 0;
foreach (var t in tableNames)
Console.WriteLine($" [{++i:D2}] {t}");
Console.WriteLine($" Total: {tableNames.Count}");
Console.WriteLine();
// ── Columns for key tables ──
string[] keyTables = ["Client", "Adresse", "ParametreMetier", "Contact",
"Site", "Client_Adresse", "ClientAdresse", "Client_Lieu", "ClientLieu",
"Lieu", "Personne", "Lieu_Personne", "LieuPersonne", "Compteur"];
foreach (var tbl in keyTables)
{
if (!tableNames.Contains(tbl, StringComparer.OrdinalIgnoreCase)) continue;
Console.WriteLine($"── Columns: {tbl} ──");
try
{
var cols = connection.GetSchema("Columns", [null, null, tbl, null]);
foreach (System.Data.DataRow row in cols.Rows)
{
string colName = row["COLUMN_NAME"]?.ToString() ?? "?";
string typeName = row["TYPE_NAME"]?.ToString() ?? "?";
int colSize = row["COLUMN_SIZE"] is int s ? s : 0;
bool nullable = row["IS_NULLABLE"]?.ToString() == "YES";
Console.WriteLine($" {colName,-30} {typeName,-15} size={colSize,5} nullable={nullable}");
}
}
catch (Exception ex) { Console.WriteLine($" ERROR: {ex.Message}"); }
Console.WriteLine();
}
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
}
static int RunClientInsert(ProbeOptions options)
{
if (!options.UseClientTable)
{
Console.Error.WriteLine("ERROR: --use-client-table is required for client-insert.");
Console.Error.WriteLine(" This probe writes to the Client table on a CLONE/TEST DB ONLY.");
return 2;
}
bool isOmika = string.Equals(options.Schema, "omika", StringComparison.OrdinalIgnoreCase);
string marker = $"ODBC_PROBE_{DateTime.UtcNow:yyyyMMddHHmmss}";
string markerGuid = isOmika
? Guid.NewGuid().ToString("D").ToLowerInvariant()[..32]
: "";
Console.WriteLine($"Target: {(isOmika ? "OMIKA" : "BATIK")} Client");
Console.WriteLine($"Marker: {marker}");
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
using var tx = connection.BeginTransaction();
Console.WriteLine("BEGIN: OK");
string sql;
var parameters = new List<OdbcParameter>();
if (isOmika)
{
sql = @"INSERT INTO Client (
CL_NOM,
CL_CODECLIENT,
CL_INFOSUP,
CL_PAYS,
CL_CODETVA,
CL_SIREN,
CL_SIRET,
GUID
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?
) RETURNING CL_PKID";
parameters.Add(new OdbcParameter { Value = $"ODBC PROBE {marker}" });
parameters.Add(new OdbcParameter { Value = marker });
parameters.Add(new OdbcParameter { Value = "" });
parameters.Add(new OdbcParameter { Value = "FR" });
parameters.Add(new OdbcParameter { Value = "" });
parameters.Add(new OdbcParameter { Value = "" });
parameters.Add(new OdbcParameter { Value = "" });
parameters.Add(new OdbcParameter { Value = markerGuid });
Console.WriteLine($"GUID candidate: {markerGuid.Length} chars, pattern: {(markerGuid.Count(c => c == '-') == 4 ? "8-4-4-4-8" : "UNEXPECTED")}");
}
else
{
// Minimal BATIK Client INSERT — only columns documented as required
sql = @"INSERT INTO Client (
CL_NOM,
CL_CODECLIENT
) VALUES (
?, ?
) RETURNING CL_PKID";
parameters.Add(new OdbcParameter { Value = $"ODBC PROBE {marker}" });
parameters.Add(new OdbcParameter { Value = marker });
}
using var cmd = connection.CreateCommand();
cmd.Transaction = tx;
cmd.CommandText = sql;
cmd.Parameters.AddRange([.. parameters]);
Console.WriteLine("-- ExecuteScalar --");
object? scalar = cmd.ExecuteScalar();
Console.WriteLine($" Accepted: YES");
Console.WriteLine($" CLR type: {scalar?.GetType().Name ?? "null"}");
Console.WriteLine($" Raw value: {scalar}");
long pkid = 0;
if (scalar is not null)
{
try { pkid = Convert.ToInt64(scalar); Console.WriteLine($" Convert.ToInt64: {pkid} ({(pkid > 0 ? "VALID" : "ZERO/INVALID")})"); }
catch (Exception ex) { Console.WriteLine($" Convert.ToInt64: FAILED ({ex.Message})"); }
}
// Rollback
tx.Rollback();
Console.WriteLine("ROLLBACK: OK");
Console.WriteLine("-- Verify rollback --");
using var verifyCmd = connection.CreateCommand();
verifyCmd.CommandText = "SELECT COUNT(*) FROM Client WHERE CL_CODECLIENT = ?";
verifyCmd.Parameters.Add(new OdbcParameter { Value = marker });
int count = Convert.ToInt32(verifyCmd.ExecuteScalar());
Console.WriteLine($" Rows with marker: {count}");
Console.WriteLine(count == 0 ? " Rollback: VERIFIED" : " Rollback: UNEXPECTED");
Console.WriteLine();
Console.WriteLine(pkid > 0 && count == 0
? "VERDICT: CLIENT INSERT + RETURNING + ROLLBACK = VERIFIED"
: "VERDICT: UNEXPECTED — check output above.");
return (pkid > 0 && count == 0) ? 0 : 1;
}
catch (OdbcException ex)
{
Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}");
return 1;
}
}
static int RunClientGraph(ProbeOptions options)
{
if (!options.UseClientTable)
{
Console.Error.WriteLine("ERROR: --use-client-table is required for client-graph.");
Console.Error.WriteLine(" This probe writes to Client + related tables on a CLONE/TEST DB ONLY.");
return 2;
}
bool isOmika = string.Equals(options.Schema, "omika", StringComparison.OrdinalIgnoreCase);
string marker = $"GRAPH_PROBE_{DateTime.UtcNow:yyyyMMddHHmmss}";
Console.WriteLine($"Target: {(isOmika ? "OMIKA" : "BATIK")} Client + Adresse graph");
Console.WriteLine($"Marker: {marker}");
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
using var tx = connection.BeginTransaction();
Console.WriteLine("BEGIN: OK");
// === Step 1: INSERT Adresse ===
long adressePkId = 0;
{
using var cmdAdr = connection.CreateCommand();
cmdAdr.Transaction = tx;
cmdAdr.CommandText = "INSERT INTO Adresse (AD_LIGNE_1, AD_CODEPOSTAL, AD_VILLE) VALUES (?, ?, ?) RETURNING AD_PKID";
cmdAdr.Parameters.Add(new OdbcParameter { Value = $"Probe {marker}" });
cmdAdr.Parameters.Add(new OdbcParameter { Value = "99999" });
cmdAdr.Parameters.Add(new OdbcParameter { Value = "PROBE CITY" });
object? adrId = cmdAdr.ExecuteScalar();
adressePkId = Convert.ToInt64(adrId!);
Console.WriteLine($"INSERT Adresse: OK, AD_PKID={adressePkId}, CLR={adrId?.GetType().Name}");
}
if (adressePkId <= 0) { Console.Error.WriteLine("FAILED: Adresse PKID invalid."); return 1; }
// === Step 2: INSERT Client with FK to Adresse ===
long clientPkId = 0;
{
using var cmdCli = connection.CreateCommand();
cmdCli.Transaction = tx;
cmdCli.CommandText = "INSERT INTO Client (CL_NOM, CL_CODECLIENT, CL_IDADRESSEPRINCIPALE) VALUES (?, ?, ?) RETURNING CL_PKID";
cmdCli.Parameters.Add(new OdbcParameter { Value = $"ODBC GRAPH {marker}" });
cmdCli.Parameters.Add(new OdbcParameter { Value = marker });
cmdCli.Parameters.Add(new OdbcParameter { Value = adressePkId });
object? cliId = cmdCli.ExecuteScalar();
clientPkId = Convert.ToInt64(cliId!);
Console.WriteLine($"INSERT Client: OK, CL_PKID={clientPkId}, FK_ADRESSE={adressePkId}, CLR={cliId?.GetType().Name}");
}
if (clientPkId <= 0) { Console.Error.WriteLine("FAILED: Client PKID invalid."); return 1; }
Console.WriteLine($"FK chain verified: AD_PKID {adressePkId} -> CL_IDADRESSEPRINCIPALE {adressePkId}");
// === Rollback global ===
tx.Rollback();
Console.WriteLine("ROLLBACK: OK (global)");
// === Verify all absent ===
using (var cmdCheck = connection.CreateCommand())
{
cmdCheck.CommandText = "SELECT COUNT(*) FROM Client WHERE CL_CODECLIENT = ?";
cmdCheck.Parameters.Add(new OdbcParameter { Value = marker });
int c = Convert.ToInt32(cmdCheck.ExecuteScalar());
Console.WriteLine($"Client rows with marker: {c}");
}
using (var cmdCheckAdr = connection.CreateCommand())
{
cmdCheckAdr.CommandText = "SELECT COUNT(*) FROM Adresse WHERE AD_VILLE = 'PROBE CITY' AND AD_LIGNE_1 LIKE 'Probe GRAPH%'";
int a = Convert.ToInt32(cmdCheckAdr.ExecuteScalar());
Console.WriteLine($"Adresse rows with marker: {a}");
}
Console.WriteLine();
Console.WriteLine("VERDICT: MULTI-INSERT + FK CHAINING + GLOBAL ROLLBACK = VERIFIED");
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
}
static int RunOmikaGuidProbe(ProbeOptions options)
{
Console.WriteLine("OMIKA GUID PROBE");
Console.WriteLine("================");
Console.WriteLine();
try
{
using var connection = new OdbcConnection(options.ConnectionString);
connection.Open();
Console.WriteLine("Connected. Querying Client table for GUID statistics...");
Console.WriteLine();
using var cmd = connection.CreateCommand(); cmd.CommandText = ProbeSqlBuilder.BuildSelectGuids(200);
int count = 0, empty = 0, allHex = 0, hasDash = 0, hasBrace = 0, hasNonPrint = 0;
int parseN = 0, parseD = 0, parseAny = 0;
int exactly4Dashes = 0, standardDashPos = 0, looksLikeTruncatedD = 0;
int allUpper = 0, allLower = 0, mixedCase = 0;
var nonHexSeen = new Dictionary<char, int>();
var lenDist = new Dictionary<int, int>();
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
count++;
string? raw = reader.IsDBNull(0) ? null : reader.GetString(0);
if (string.IsNullOrEmpty(raw)) { empty++; continue; }
string guid = raw.TrimEnd('\0', ' ');
int len = guid.Length;
lenDist.TryGetValue(len, out int lc); lenDist[len] = lc + 1;
bool isAllHex = guid.Where(c => c != '-').All(c => (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));
if (isAllHex) allHex++;
if (guid.Contains('-')) hasDash++;
if (guid.Contains('{') || guid.Contains('}')) hasBrace++;
if (guid.Any(c => char.IsControl(c) && c != '\0')) hasNonPrint++;
foreach (char c in guid)
{
bool isHex = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
if (!isHex && c != '-')
{
nonHexSeen.TryGetValue(c, out int nh); nonHexSeen[c] = nh + 1;
}
}
if (Guid.TryParseExact(guid, "N", out _)) parseN++;
if (Guid.TryParseExact(guid, "D", out _)) parseD++;
if (Guid.TryParse(guid, out _)) parseAny++;
// Dash position analysis (0-based)
int dashCount = guid.Count(c => c == '-');
if (dashCount == 4) exactly4Dashes++;
bool hasStandardPositions = dashCount == 4
&& guid.Length == 32
&& guid[8] == '-' && guid[13] == '-' && guid[18] == '-' && guid[23] == '-';
if (hasStandardPositions) standardDashPos++;
if (hasStandardPositions && isAllHex)
{
looksLikeTruncatedD++;
}
// Casing
var hexOnly = guid.Replace("-", "");
if (hexOnly.Length > 0)
{
if (hexOnly.All(c => c >= 'A' && c <= 'F' || c >= '0' && c <= '9')) allUpper++;
else if (hexOnly.All(c => c >= 'a' && c <= 'f' || c >= '0' && c <= '9')) allLower++;
else mixedCase++;
}
}
Console.WriteLine($" Sample: {count}");
Console.WriteLine($" Empty/null: {empty}");
Console.WriteLine();
Console.WriteLine("── Length distribution ──");
foreach (var kv in lenDist.OrderBy(x => x.Key))
Console.WriteLine($" Len={kv.Key}: {kv.Value}");
Console.WriteLine();
Console.WriteLine("── Content analysis ──");
Console.WriteLine($" All-hex: {allHex}/{count}");
Console.WriteLine($" Contains '-': {hasDash}/{count}");
Console.WriteLine($" Contains brace: {hasBrace}/{count}");
Console.WriteLine($" Non-printable: {hasNonPrint}/{count}");
Console.WriteLine();
Console.WriteLine("── Dash position analysis ──");
Console.WriteLine($" Exactly 4 dashes: {exactly4Dashes}/{count}");
Console.WriteLine($" Standard positions: {standardDashPos}/{count}");
Console.WriteLine($" LooksLikeTruncatedD:{looksLikeTruncatedD}/{count}");
Console.WriteLine();
Console.WriteLine("── Casing (hex chars only) ──");
Console.WriteLine($" All uppercase: {allUpper}/{count}");
Console.WriteLine($" All lowercase: {allLower}/{count}");
Console.WriteLine($" Mixed case: {mixedCase}/{count}");
Console.WriteLine();
Console.WriteLine("── Guid.Parse ──");
Console.WriteLine($" ParseExact N: {parseN}/{count}");
Console.WriteLine($" ParseExact D: {parseD}/{count}");
Console.WriteLine($" ParseAny: {parseAny}/{count}");
if (nonHexSeen.Count > 0)
{
Console.WriteLine();
Console.WriteLine("── Non-hex chars (codepoint: count) ──");
foreach (var kv in nonHexSeen.OrderByDescending(x => x.Value).Take(20))
Console.WriteLine($" U+{(int)kv.Key:X4} ('{kv.Key}'): {kv.Value}");
}
Console.WriteLine();
Console.WriteLine("VERDICT: DETAILED ANALYSIS ABOVE.");
return 0;
}
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
}
static int ValidateIdentifiers(string table, string idColumn, string markerColumn)
{
try { SqlIdentifierValidator.Validate(table); SqlIdentifierValidator.Validate(idColumn); SqlIdentifierValidator.Validate(markerColumn); return 0; }
catch (ArgumentException ex) { Console.Error.WriteLine($"ERROR: {ex.Message}"); return 2; }
}
static string MaskConnectionProperty(string? value)
{
if (string.IsNullOrEmpty(value)) return "(empty)";
if (value.Contains("PWD=", StringComparison.OrdinalIgnoreCase) || value.Contains("Password=", StringComparison.OrdinalIgnoreCase) || value.Contains("UID=", StringComparison.OrdinalIgnoreCase)) return "(masked)";
return value;
}
static void PrintUsage()
{
Console.WriteLine("HFSQL ODBC CAPABILITY PROBE");
Console.WriteLine();
Console.WriteLine("COMMANDS:");
Console.WriteLine(" capabilities Display driver info + connection capabilities (READ-ONLY)");
Console.WriteLine(" audit List all tables and key columns metadata (READ-ONLY)");
Console.WriteLine(" transaction Test BEGIN/INSERT/ROLLBACK cycle (WRITE — needs --allow-write-probe)");
Console.WriteLine(" returning Test INSERT ... RETURNING via ODBC (WRITE — needs --allow-write-probe)");
Console.WriteLine(" omika-guid Read GUID statistics from Client table (READ-ONLY)");
Console.WriteLine();
Console.WriteLine("OPTIONS:");
Console.WriteLine(" --connection-string HFSQL ODBC connection string (DSN=...)");
Console.WriteLine(" --allow-write-probe Confirm working on clone/test DB (required for writes)");
Console.WriteLine(" --table Table name for write probes");
Console.WriteLine(" --id-column Auto-ID column name (default: PKID)");
Console.WriteLine(" --marker-column Column to write a marker value (default: Marker)");
Console.WriteLine(" --schema batik | omika (for client-specific probes)");
Console.WriteLine(" --use-client-table Write to Client table (clone DB only!)");
Console.WriteLine();
Console.WriteLine("\u26a0 NEVER run write probes against a production database.");
}
}