[Feature] Supporto Visual FoxPro / dBase come sorgente dati (managed, sola lettura)
Aggiunge un connettore completamente managed per database Visual FoxPro e dBase, utilizzabile come sorgente dati in tutti i flussi dell'applicazione (discovery manuale, esecuzione schedulata, hash/change-detection, backup/restore). ## Motivazione Il provider OLE DB ufficiale (VFPOLEDB.1) è solo a 32-bit, deprecato e richiede installazione separata. Il connettore managed usa DbfDataReader 1.0.0 (MIT), legge direttamente i file .dbf/.fpt/.dbc a 64-bit senza dipendenze esterne e con streaming efficiente (tabelle da centinaia di MB con uso di memoria contenuto). ## Nuovi file - DataConnection/DB/FoxPro/FoxProConnectionInfo.cs Classe sealed con le informazioni di connessione risolte (cartella, percorso .dbc, encoding, flag IncludeDeleted). Proprietà di convenienza IsContainer e DisplayName. - DataConnection/DB/FoxPro/FoxProReader.cs Helper statico che registra CodePagesEncodingProvider, risolve la connection string (path diretto, stile OLE DB con Provider=vfpoledb, chiave=valore), verifica l'accesso al filesystem, legge il catalogo .dbc (il file .dbc è esso stesso un DBF; filtro su OBJECTTYPE='Table' + cross-check esistenza fisica .dbf), enumera le tabelle free, mappa i tipi VFP (Character, Memo, Numeric, Float, Double, Integer, Currency, Date, DateTime, Logical, General) in descrizioni leggibili, esegue streaming dei record via DbfDataReader, auto-rileva l'encoding dall'header DBF (language driver byte) con fallback a code page 1252, e analizza query SELECT [TOP n] * FROM tabella. - DataConnection/DB/FoxProDatabaseManager.cs Implementa IDatabaseManager. Lettura: TestConnectionAsync, GetTableNamesAsync, GetTableSchemaAsync, GetDatabaseSchemaAsync, GetAllRecordsAsync, ExecuteRawQueryAsync, GetPrimaryKeyFieldAsync (ritorna null, selezione manuale chiave), GetAvailableDatabasesAsync, ChangeDatabaseAsync (no-op per sorgenti file-based). Scrittura: tutti i metodi di modifica (Insert/Update/Delete/Upsert/BulkInsert/ ExecuteCommand/ExecuteNonQuery) lanciano NotSupportedException con messaggio esplicito. - DataConnection/DB/EF/SchemaProviders/FoxProSchemaProvider.cs Implementa IDatabaseSchemaProvider delegando a FoxProReader. Gestione errori per tabella in GetDatabaseSchemaAsync (una tabella non apribile non blocca la discovery). - FOXPRO_CONNECTION.md Guida utente: passo-passo per creare la credenziale, formato connection string, tabella tipi VFP supportati, note su sola lettura e limitazioni query, tabella risoluzione problemi (caratteri accentati, tabelle mancanti, ecc.). ## File modificati - DataConnection/DataConnection.csproj Aggiunto DbfDataReader 1.0.0 (porta transitivamente System.Text.Encoding.CodePages >= 10.0.3; rimossa dipendenza esplicita alla versione 9.0.3 che causava NU1605). - DataConnection/DB/Enums/DatabaseType.cs Aggiunto valore Foxpro in fondo all'enum (preserva valori già persistiti). - CredentialManager/Models/CredentialModels.cs Aggiunto DatabaseType.Foxpro all'enum e metodo BuildFoxproConnectionString che genera "Data Source=percorso[;CodePage=n][;IncludeDeleted=true]". - DataConnection/CredentialManagement/Models/CredentialExtensions.cs Mappatura bidirezionale Foxpro tra enum CredentialManager e DataConnection. - DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs Aggiunto TestFoxproConnection: verifica percorso accessibile, legge nomi tabelle, restituisce messaggio con conteggio tabelle, encoding rilevato e nota sola lettura. - DataConnection/DB/EF/DatabaseSchemaProviderFactory.cs Caso Foxpro => new FoxProSchemaProvider(). - Data_Coupler/Services/DataConnectionFactory.cs Branch Foxpro => new FoxProDatabaseManager(connectionString). - Data_Coupler/Extensions/DataCoupler/DatabaseMethod.cs Aggiunto IsFoxproConnection() e caso Foxpro in CreateLimitedQuery (SELECT TOP n * FROM (query) AS subquery). - CredentialManager/Integration/DataConnectionHelper.cs ValidateDatabaseCredential: introdotto flag isFileBased (Sqlite || Foxpro) per esonerare host/utente/password/porta; messaggio di errore specifico per il campo percorso FoxPro. - Data_Coupler/Pages/CredentialManagement.razor Aggiunta opzione "Visual FoxPro (.dbc / .dbf)" nel selettore tipo database. Sezione di configurazione dedicata: banner sola lettura, campo percorso con esempi (.dbc e cartella), dropdown code page (Auto/1252/1250/1251/850/437/65001), checkbox record cancellati, pannello tipi supportati, anteprima connection string. Fix validazione form test-connessione: branch Foxpro che verifica solo DatabaseName (non Host/Username/Password), eliminando il falso errore "Il campo Host è obbligatorio". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,22 +82,28 @@ public static class DataConnectionHelper
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
// Sorgenti file-based (percorso, senza host/utente/password/porta)
|
||||
bool isFileBased = credential.DatabaseType == DatabaseType.Sqlite
|
||||
|| credential.DatabaseType == DatabaseType.Foxpro;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credential.Name))
|
||||
errors.Add("Il nome della credenziale è obbligatorio");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credential.Host) && credential.DatabaseType != DatabaseType.Sqlite)
|
||||
if (string.IsNullOrWhiteSpace(credential.Host) && !isFileBased)
|
||||
errors.Add("L'host è obbligatorio per questo tipo di database");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credential.DatabaseName))
|
||||
errors.Add("Il nome del database è obbligatorio");
|
||||
errors.Add(credential.DatabaseType == DatabaseType.Foxpro
|
||||
? "Il percorso del database FoxPro (.dbc o cartella .dbf) è obbligatorio"
|
||||
: "Il nome del database è obbligatorio");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credential.Username) && credential.DatabaseType != DatabaseType.Sqlite)
|
||||
if (string.IsNullOrWhiteSpace(credential.Username) && !isFileBased)
|
||||
errors.Add("Il nome utente è obbligatorio per questo tipo di database");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(credential.Password) && credential.DatabaseType != DatabaseType.Sqlite)
|
||||
if (string.IsNullOrWhiteSpace(credential.Password) && !isFileBased)
|
||||
errors.Add("La password è obbligatoria per questo tipo di database");
|
||||
|
||||
if (credential.Port <= 0 && credential.DatabaseType != DatabaseType.Sqlite)
|
||||
if (credential.Port <= 0 && !isFileBased)
|
||||
{
|
||||
// Assegna porta predefinita se non specificata
|
||||
credential.Port = GetDefaultPort(credential.DatabaseType);
|
||||
|
||||
@@ -58,7 +58,8 @@ public enum DatabaseType
|
||||
DB2,
|
||||
SapHana,
|
||||
Odbc,
|
||||
OleDb
|
||||
OleDb,
|
||||
Foxpro
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -200,6 +201,7 @@ public static class ConnectionStringBuilder
|
||||
DatabaseType.SapHana => BuildSapHanaConnectionString(credential),
|
||||
DatabaseType.Odbc => BuildOdbcConnectionString(credential),
|
||||
DatabaseType.OleDb => BuildOleDbConnectionString(credential),
|
||||
DatabaseType.Foxpro => BuildFoxproConnectionString(credential),
|
||||
_ => throw new NotSupportedException($"Database type {credential.DatabaseType} not supported")
|
||||
};
|
||||
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
|
||||
@@ -474,6 +476,37 @@ public static class ConnectionStringBuilder
|
||||
return string.Join(";", builder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce la "connection string" per una sorgente Visual FoxPro / dBase.
|
||||
/// FoxPro non usa un vero connection string: serve solo il percorso (file .dbc o cartella di .dbf),
|
||||
/// salvato in <see cref="DatabaseCredential.DatabaseName"/>. Le opzioni encoding/record cancellati
|
||||
/// vengono aggiunte come chiavi CodePage/IncludeDeleted, interpretate da FoxProReader.
|
||||
/// </summary>
|
||||
private static string BuildFoxproConnectionString(DatabaseCredential credential)
|
||||
{
|
||||
// Una connection string esplicita (es. incollata dall'utente) ha la precedenza.
|
||||
if (!string.IsNullOrEmpty(credential.ConnectionString))
|
||||
return credential.ConnectionString;
|
||||
|
||||
var path = !string.IsNullOrEmpty(credential.DatabaseName)
|
||||
? credential.DatabaseName
|
||||
: credential.Host;
|
||||
|
||||
var builder = new List<string> { $"Data Source={path}" };
|
||||
|
||||
if (credential.AdditionalParameters != null)
|
||||
{
|
||||
if (credential.AdditionalParameters.TryGetValue("Encoding", out var enc) && !string.IsNullOrWhiteSpace(enc))
|
||||
builder.Add($"CodePage={enc}");
|
||||
|
||||
if (credential.AdditionalParameters.TryGetValue("IncludeDeleted", out var del) &&
|
||||
del.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
builder.Add("IncludeDeleted=true");
|
||||
}
|
||||
|
||||
return string.Join(";", builder);
|
||||
}
|
||||
|
||||
private static void AddAdditionalParameters(List<string> builder, Dictionary<string, string>? additionalParams)
|
||||
{
|
||||
if (additionalParams != null)
|
||||
|
||||
@@ -23,6 +23,7 @@ public static class CredentialExtensions
|
||||
CredentialManager.Models.DatabaseType.SapHana => DataConnection.Enums.DatabaseType.SapHana,
|
||||
CredentialManager.Models.DatabaseType.Odbc => DataConnection.Enums.DatabaseType.Odbc,
|
||||
CredentialManager.Models.DatabaseType.OleDb => DataConnection.Enums.DatabaseType.OleDb,
|
||||
CredentialManager.Models.DatabaseType.Foxpro => DataConnection.Enums.DatabaseType.Foxpro,
|
||||
_ => throw new NotSupportedException($"Database type {credentialDbType} not supported")
|
||||
};
|
||||
}
|
||||
@@ -43,6 +44,7 @@ public static class CredentialExtensions
|
||||
DataConnection.Enums.DatabaseType.SapHana => CredentialManager.Models.DatabaseType.SapHana,
|
||||
DataConnection.Enums.DatabaseType.Odbc => CredentialManager.Models.DatabaseType.Odbc,
|
||||
DataConnection.Enums.DatabaseType.OleDb => CredentialManager.Models.DatabaseType.OleDb,
|
||||
DataConnection.Enums.DatabaseType.Foxpro => CredentialManager.Models.DatabaseType.Foxpro,
|
||||
_ => throw new NotSupportedException($"Database type {dataConnectionDbType} not supported")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -266,6 +266,7 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
CredentialManager.Models.DatabaseType.Sqlite => await TestSqliteConnection(connectionString, credential),
|
||||
CredentialManager.Models.DatabaseType.Odbc => await TestOdbcConnection(connectionString, credential),
|
||||
CredentialManager.Models.DatabaseType.OleDb => await TestOleDbConnection(connectionString, credential),
|
||||
CredentialManager.Models.DatabaseType.Foxpro => await TestFoxproConnection(connectionString, credential),
|
||||
_ => (false, $"Test di connessione non implementato per {credential.DatabaseType}")
|
||||
};
|
||||
}
|
||||
@@ -459,6 +460,40 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Message)> TestFoxproConnection(string connectionString, DatabaseCredential credential)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var info = DataConnection.DB.FoxPro.FoxProReader.Resolve(connectionString);
|
||||
DataConnection.DB.FoxPro.FoxProReader.EnsureReachable(info);
|
||||
|
||||
var tables = DataConnection.DB.FoxPro.FoxProReader.GetTableNames(info);
|
||||
|
||||
var details = new System.Text.StringBuilder();
|
||||
details.AppendLine("Connessione Visual FoxPro riuscita! (lettura managed, 64-bit)");
|
||||
details.AppendLine();
|
||||
details.AppendLine("Dettagli:");
|
||||
details.AppendLine(info.IsContainer
|
||||
? $"- Database container: {info.DbcPath}"
|
||||
: $"- Cartella tabelle libere: {info.Folder}");
|
||||
details.AppendLine($"- Tabelle rilevate: {tables.Count}");
|
||||
details.AppendLine($"- Encoding: code page {info.Encoding.CodePage}");
|
||||
details.AppendLine($"- Record cancellati: {(info.IncludeDeleted ? "inclusi" : "esclusi")}");
|
||||
details.AppendLine("- Modalità: sola lettura (sorgente dati)");
|
||||
|
||||
return tables.Count > 0
|
||||
? (true, details.ToString())
|
||||
: (false, $"Percorso raggiungibile ma nessuna tabella .dbf trovata in: {info.Folder}");
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"Errore Visual FoxPro: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> TestRestApiConnectionAsync(string credentialName)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -22,6 +22,7 @@ public class DatabaseSchemaProviderFactory
|
||||
DatabaseType.SqlServer => new SqlServerSchemaProvider(),
|
||||
DatabaseType.Odbc => new OdbcSchemaProvider(),
|
||||
DatabaseType.OleDb => new OleDbSchemaProvider(),
|
||||
DatabaseType.Foxpro => new FoxProSchemaProvider(),
|
||||
// Aggiungere qui altri provider quando implementati
|
||||
// DatabaseType.MySql => new MySqlSchemaProvider(),
|
||||
// DatabaseType.PostgreSql => new PostgreSqlSchemaProvider(),
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
using DataConnection.DB.FoxPro;
|
||||
using DataConnection.Interfaces;
|
||||
|
||||
namespace DataConnection.EF.SchemaProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Provider di schema per database Visual FoxPro / dBase, completamente managed
|
||||
/// (legge i file <c>.dbf</c>/<c>.fpt</c>/<c>.dbc</c> senza provider OLE DB/ODBC).
|
||||
/// La "connection string" è il percorso al <c>.dbc</c> o alla cartella di tabelle libere.
|
||||
/// </summary>
|
||||
public class FoxProSchemaProvider : IDatabaseSchemaProvider
|
||||
{
|
||||
public Task<IDictionary<string, IEnumerable<DbColumnInfo>>> GetDatabaseSchemaAsync(string connectionString)
|
||||
{
|
||||
var info = FoxProReader.Resolve(connectionString);
|
||||
var result = new Dictionary<string, IEnumerable<DbColumnInfo>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var table in FoxProReader.GetTableNames(info))
|
||||
{
|
||||
try
|
||||
{
|
||||
var columns = FoxProReader.GetTableColumns(info, table);
|
||||
if (columns.Count > 0)
|
||||
result[table] = columns;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Una tabella corrotta/illeggibile non deve bloccare l'intera discovery.
|
||||
Console.WriteLine($"[FoxPro] Impossibile leggere lo schema della tabella {table}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult<IDictionary<string, IEnumerable<DbColumnInfo>>>(result);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> GetTableNamesAsync(string connectionString)
|
||||
{
|
||||
var info = FoxProReader.Resolve(connectionString);
|
||||
return Task.FromResult<IEnumerable<string>>(FoxProReader.GetTableNames(info));
|
||||
}
|
||||
|
||||
public Task<IEnumerable<DbColumnInfo>> GetTableSchemaAsync(string connectionString, string tableName)
|
||||
{
|
||||
var info = FoxProReader.Resolve(connectionString);
|
||||
return Task.FromResult<IEnumerable<DbColumnInfo>>(FoxProReader.GetTableColumns(info, tableName));
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> GetAvailableDatabasesAsync(string connectionString)
|
||||
{
|
||||
// Sorgente file-based: un solo "database" (il container o la cartella).
|
||||
var info = FoxProReader.Resolve(connectionString);
|
||||
return Task.FromResult<IEnumerable<string>>(new[] { info.DisplayName });
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,6 @@ public enum DatabaseType
|
||||
DB2,
|
||||
SapHana,
|
||||
Odbc,
|
||||
OleDb
|
||||
OleDb,
|
||||
Foxpro
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Text;
|
||||
|
||||
namespace DataConnection.DB.FoxPro;
|
||||
|
||||
/// <summary>
|
||||
/// Informazioni di connessione risolte per una sorgente Visual FoxPro / dBase.
|
||||
/// Una "connessione" FoxPro è semplicemente un percorso sul filesystem:
|
||||
/// un file <c>.dbc</c> (database container) oppure una cartella di tabelle libere <c>.dbf</c>.
|
||||
/// </summary>
|
||||
public sealed class FoxProConnectionInfo
|
||||
{
|
||||
/// <summary>Cartella che contiene i file <c>.dbf</c> (e relativi <c>.fpt</c>/<c>.cdx</c>).</summary>
|
||||
public string Folder { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>Percorso completo del file <c>.dbc</c>, se la sorgente è un database container; altrimenti null.</summary>
|
||||
public string? DbcPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Encoding usato per decodificare i campi carattere/memo. Il valore reale è sempre
|
||||
/// impostato da <see cref="FoxProReader.Resolve"/> (default effettivo: code page 1252);
|
||||
/// qui usiamo Latin1 come placeholder built-in che non richiede il provider code-page.
|
||||
/// </summary>
|
||||
public Encoding Encoding { get; init; } = Encoding.Latin1;
|
||||
|
||||
/// <summary>Se true, include anche i record marcati come cancellati (default: false).</summary>
|
||||
public bool IncludeDeleted { get; init; }
|
||||
|
||||
/// <summary>True se la sorgente è un database container (.dbc).</summary>
|
||||
public bool IsContainer => !string.IsNullOrEmpty(DbcPath);
|
||||
|
||||
/// <summary>Nome leggibile della sorgente (nome del .dbc o della cartella), per messaggi/log.</summary>
|
||||
public string DisplayName => IsContainer
|
||||
? System.IO.Path.GetFileName(DbcPath!)
|
||||
: new System.IO.DirectoryInfo(Folder).Name;
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DbfDataReader;
|
||||
using DataConnection.Interfaces;
|
||||
|
||||
namespace DataConnection.DB.FoxPro;
|
||||
|
||||
/// <summary>
|
||||
/// Helper di basso livello per leggere database Visual FoxPro / dBase in modo
|
||||
/// completamente managed (nessun provider OLE DB/ODBC richiesto, funziona a 64-bit).
|
||||
///
|
||||
/// Legge direttamente i file <c>.dbf</c> (tabelle), <c>.fpt</c> (campi memo) e, quando
|
||||
/// la sorgente è un container <c>.dbc</c>, ne legge il catalogo per ricavare l'elenco
|
||||
/// ufficiale delle tabelle. La libreria sottostante è <c>DbfDataReader</c> (MIT).
|
||||
///
|
||||
/// Sola lettura: il coupler usa FoxPro come sorgente dati.
|
||||
/// </summary>
|
||||
public static class FoxProReader
|
||||
{
|
||||
static FoxProReader()
|
||||
{
|
||||
// Le code page non-Unicode (es. 1252, 1250, 850) non sono disponibili di default
|
||||
// su .NET Core/5+: serve registrare il provider.
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
}
|
||||
|
||||
/// <summary>Code page di default (Windows-1252, ANSI Europa occidentale).</summary>
|
||||
public const int DefaultCodePage = 1252;
|
||||
|
||||
// Mappa byte "language driver" dell'header DBF -> code page più comuni.
|
||||
private static readonly Dictionary<byte, int> LanguageDriverCodePages = new()
|
||||
{
|
||||
[0x01] = 437, // U.S. MS-DOS
|
||||
[0x02] = 850, // International MS-DOS
|
||||
[0x03] = 1252, // Windows ANSI (più comune nei dati VFP)
|
||||
[0x08] = 865, // Danish OEM
|
||||
[0x09] = 437, // Dutch OEM
|
||||
[0x0A] = 850, // Dutch OEM*
|
||||
[0x64] = 852, // Eastern European MS-DOS
|
||||
[0x65] = 866, // Russian MS-DOS
|
||||
[0x66] = 865, // Nordic MS-DOS
|
||||
[0x67] = 861, // Icelandic MS-DOS
|
||||
[0x6A] = 737, // Greek MS-DOS
|
||||
[0x6B] = 857, // Turkish MS-DOS
|
||||
[0x57] = 1252, // ANSI
|
||||
[0xC8] = 1250, // Eastern European Windows
|
||||
[0xC9] = 1251, // Russian Windows
|
||||
[0xCA] = 1254, // Turkish Windows
|
||||
[0xCB] = 1253, // Greek Windows
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Interpreta la "connection string" FoxPro (un percorso, eventualmente con opzioni)
|
||||
/// e risolve cartella, eventuale <c>.dbc</c>, encoding e gestione record cancellati.
|
||||
///
|
||||
/// Formati accettati:
|
||||
/// <list type="bullet">
|
||||
/// <item>percorso nudo a un file <c>.dbc</c>: <c>C:\dati\Data.dbc</c></item>
|
||||
/// <item>percorso nudo a una cartella di <c>.dbf</c>: <c>C:\dati</c></item>
|
||||
/// <item>stringa con chiavi: <c>Data Source=C:\dati\Data.dbc;CodePage=1252;IncludeDeleted=false</c></item>
|
||||
/// <item>stringa stile OLE DB VFP: <c>Provider=vfpoledb;Data Source=C:\dati\Data.dbc;...</c> (Data Source viene estratto)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static FoxProConnectionInfo Resolve(string connectionString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
throw new ArgumentException("Percorso del database FoxPro non specificato.", nameof(connectionString));
|
||||
|
||||
string? path = null;
|
||||
int? codePage = null;
|
||||
bool includeDeleted = false;
|
||||
|
||||
// Se contiene almeno una coppia chiave=valore, interpretiamo come connection string;
|
||||
// altrimenti l'intera stringa è il percorso.
|
||||
if (connectionString.Contains('='))
|
||||
{
|
||||
foreach (var segment in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var idx = segment.IndexOf('=');
|
||||
if (idx < 0)
|
||||
{
|
||||
// Segmento senza '=': potrebbe essere un percorso nudo mescolato — usalo se non abbiamo già un path
|
||||
path ??= segment.Trim();
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = segment[..idx].Trim();
|
||||
var value = segment[(idx + 1)..].Trim().Trim('"', '\'');
|
||||
|
||||
switch (key.Replace(" ", "").ToLowerInvariant())
|
||||
{
|
||||
case "datasource":
|
||||
case "source":
|
||||
case "data":
|
||||
path = value;
|
||||
break;
|
||||
case "codepage":
|
||||
case "encoding":
|
||||
if (int.TryParse(value, out var cp))
|
||||
codePage = cp;
|
||||
else
|
||||
codePage = TryGetCodePageByName(value);
|
||||
break;
|
||||
case "includedeleted":
|
||||
case "deleted":
|
||||
includeDeleted = value.Equals("true", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("on", StringComparison.OrdinalIgnoreCase)
|
||||
|| value == "1";
|
||||
break;
|
||||
// "Provider", "Collating Sequence", ecc. sono ignorati (compatibilità OLE DB)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
path = connectionString.Trim();
|
||||
}
|
||||
|
||||
path = path?.Trim().Trim('"', '\'');
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Impossibile determinare il percorso del database FoxPro dalla stringa fornita.", nameof(connectionString));
|
||||
|
||||
string folder;
|
||||
string? dbcPath = null;
|
||||
|
||||
if (path.EndsWith(".dbc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dbcPath = path;
|
||||
folder = Path.GetDirectoryName(path) ?? path;
|
||||
}
|
||||
else
|
||||
{
|
||||
folder = path.TrimEnd('\\', '/');
|
||||
// Se la cartella contiene un singolo .dbc e nessuna ambiguità, lo usiamo come container.
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
var dbcs = Directory.GetFiles(folder, "*.dbc");
|
||||
if (dbcs.Length == 1)
|
||||
dbcPath = dbcs[0];
|
||||
}
|
||||
}
|
||||
|
||||
var encoding = ResolveEncoding(codePage, dbcPath, folder);
|
||||
|
||||
return new FoxProConnectionInfo
|
||||
{
|
||||
Folder = folder,
|
||||
DbcPath = dbcPath,
|
||||
Encoding = encoding,
|
||||
IncludeDeleted = includeDeleted
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Verifica che la sorgente sia raggiungibile (cartella esistente, eventuale .dbc presente).</summary>
|
||||
public static void EnsureReachable(FoxProConnectionInfo info)
|
||||
{
|
||||
if (info.IsContainer && !File.Exists(info.DbcPath!))
|
||||
throw new FileNotFoundException($"File database container (.dbc) non trovato: {info.DbcPath}");
|
||||
|
||||
if (!Directory.Exists(info.Folder))
|
||||
throw new DirectoryNotFoundException($"Cartella del database FoxPro non trovata: {info.Folder}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Elenco delle tabelle: dal catalogo del <c>.dbc</c> (OBJECTTYPE='Table') se container,
|
||||
/// altrimenti enumerando i file <c>.dbf</c> nella cartella. Vengono restituite solo le
|
||||
/// tabelle effettivamente apribili (file <c>.dbf</c> presente).
|
||||
/// </summary>
|
||||
public static List<string> GetTableNames(FoxProConnectionInfo info)
|
||||
{
|
||||
EnsureReachable(info);
|
||||
|
||||
var names = new List<string>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (info.IsContainer)
|
||||
{
|
||||
foreach (var name in ReadTableNamesFromContainer(info))
|
||||
{
|
||||
if (seen.Add(name) && File.Exists(ResolveTableFilePath(info.Folder, name)))
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(info.Folder, "*.dbf", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(file);
|
||||
if (seen.Add(name))
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
names.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
return names;
|
||||
}
|
||||
|
||||
/// <summary>Schema (colonne) di una singola tabella.</summary>
|
||||
public static List<DbColumnInfo> GetTableColumns(FoxProConnectionInfo info, string tableName)
|
||||
{
|
||||
var filePath = ResolveTableFilePath(info.Folder, tableName);
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"Tabella FoxPro non trovata: {filePath}");
|
||||
|
||||
using var table = new DbfTable(filePath, info.Encoding);
|
||||
var columns = new List<DbColumnInfo>();
|
||||
foreach (var col in table.Columns)
|
||||
{
|
||||
columns.Add(new DbColumnInfo
|
||||
{
|
||||
Name = col.ColumnName.Trim(),
|
||||
DataType = MapColumnType(col),
|
||||
IsNullable = true, // VFP: nullability non esposta in modo affidabile dall'header
|
||||
IsPrimaryKey = false // le tabelle .dbf non hanno una PK affidabile; la chiave si sceglie nel coupler
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legge i record di una tabella come dizionari nome-colonna -> valore.
|
||||
/// I valori null sono rappresentati con <see cref="DBNull.Value"/> (coerente con gli altri manager).
|
||||
/// Streaming: non carica l'intera tabella in memoria.
|
||||
/// </summary>
|
||||
public static IEnumerable<Dictionary<string, object>> ReadRecords(
|
||||
FoxProConnectionInfo info, string tableName, int? maxRows = null)
|
||||
{
|
||||
var filePath = ResolveTableFilePath(info.Folder, tableName);
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"Tabella FoxPro non trovata: {filePath}");
|
||||
|
||||
var options = new DbfDataReaderOptions
|
||||
{
|
||||
Encoding = info.Encoding,
|
||||
SkipDeletedRecords = !info.IncludeDeleted
|
||||
};
|
||||
|
||||
using var reader = new DbfDataReader.DbfDataReader(filePath, options);
|
||||
|
||||
// Nomi colonna (trimmati) per ordinale.
|
||||
var names = reader.DbfTable.Columns.Select(c => c.ColumnName.Trim()).ToArray();
|
||||
|
||||
long emitted = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
var row = new Dictionary<string, object>(names.Length, StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
row[names[i]] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
|
||||
}
|
||||
yield return row;
|
||||
|
||||
emitted++;
|
||||
if (maxRows.HasValue && emitted >= maxRows.Value)
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Numero di record (header) di una tabella, senza scorrere i dati.</summary>
|
||||
public static long GetRecordCount(FoxProConnectionInfo info, string tableName)
|
||||
{
|
||||
var filePath = ResolveTableFilePath(info.Folder, tableName);
|
||||
if (!File.Exists(filePath))
|
||||
return 0;
|
||||
using var table = new DbfTable(filePath, info.Encoding);
|
||||
return table.Header.RecordCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae il nome tabella (e un eventuale <c>TOP n</c>) da una semplice query SELECT.
|
||||
/// Gestisce anche le query "wrappate" generate da CreateLimitedQuery
|
||||
/// (<c>SELECT TOP n * FROM (SELECT ... FROM tabella) AS subquery</c>): viene preso
|
||||
/// l'ultimo identificatore dopo FROM, cioè la tabella reale più interna.
|
||||
/// </summary>
|
||||
public static (string Table, int? Top)? ParseSimpleSelect(string sql)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
return null;
|
||||
|
||||
var fromMatches = Regex.Matches(sql, @"\bFROM\s+\[?([A-Za-z_][\w]*)\]?", RegexOptions.IgnoreCase);
|
||||
if (fromMatches.Count == 0)
|
||||
return null;
|
||||
|
||||
var table = fromMatches[^1].Groups[1].Value;
|
||||
|
||||
int? top = null;
|
||||
var topMatch = Regex.Match(sql, @"\bTOP\s+(\d+)", RegexOptions.IgnoreCase);
|
||||
if (topMatch.Success && int.TryParse(topMatch.Groups[1].Value, out var t))
|
||||
top = t;
|
||||
|
||||
return (table, top);
|
||||
}
|
||||
|
||||
/// <summary>Risolve il path del file .dbf per una tabella, gestendo il case-insensitive di Windows.</summary>
|
||||
public static string ResolveTableFilePath(string folder, string tableName)
|
||||
{
|
||||
var name = tableName.Trim().Trim('[', ']', '"', '\'');
|
||||
if (name.EndsWith(".dbf", StringComparison.OrdinalIgnoreCase))
|
||||
name = name[..^4];
|
||||
|
||||
var direct = Path.Combine(folder, name + ".dbf");
|
||||
if (File.Exists(direct))
|
||||
return direct;
|
||||
|
||||
// Ricerca case-insensitive (i cataloghi .dbc spesso usano un case diverso dai file).
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(folder, "*.dbf", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(file), name, StringComparison.OrdinalIgnoreCase))
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
return direct; // non trovato: il chiamante gestirà il FileNotFound
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ReadTableNamesFromContainer(FoxProConnectionInfo info)
|
||||
{
|
||||
// Il file .dbc è esso stesso una tabella DBF: il suo catalogo contiene righe con
|
||||
// OBJECTTYPE in {Database, Table, Index, Field, Relation, View}. Ci interessano i 'Table'.
|
||||
var options = new DbfDataReaderOptions
|
||||
{
|
||||
Encoding = info.Encoding,
|
||||
SkipDeletedRecords = true
|
||||
};
|
||||
|
||||
using var reader = new DbfDataReader.DbfDataReader(info.DbcPath!, options);
|
||||
|
||||
int typeIdx = -1, nameIdx = -1;
|
||||
for (int i = 0; i < reader.DbfTable.Columns.Count; i++)
|
||||
{
|
||||
var col = reader.DbfTable.Columns[i].ColumnName.Trim();
|
||||
if (col.Equals("OBJECTTYPE", StringComparison.OrdinalIgnoreCase)) typeIdx = i;
|
||||
else if (col.Equals("OBJECTNAME", StringComparison.OrdinalIgnoreCase)) nameIdx = i;
|
||||
}
|
||||
|
||||
if (typeIdx < 0 || nameIdx < 0)
|
||||
yield break; // .dbc non nel formato atteso
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
var type = reader.IsDBNull(typeIdx) ? null : reader.GetValue(typeIdx)?.ToString()?.Trim();
|
||||
if (!string.Equals(type, "Table", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var name = reader.IsDBNull(nameIdx) ? null : reader.GetValue(nameIdx)?.ToString()?.Trim();
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
yield return name;
|
||||
}
|
||||
}
|
||||
|
||||
private static string MapColumnType(DbfColumn col)
|
||||
{
|
||||
return col.ColumnType switch
|
||||
{
|
||||
DbfColumnType.Character => $"Character({col.Length})",
|
||||
DbfColumnType.WideCharacter => $"Varchar({col.Length})",
|
||||
DbfColumnType.Memo => "Memo",
|
||||
DbfColumnType.Number => col.DecimalCount > 0
|
||||
? $"Numeric({col.Length},{col.DecimalCount})"
|
||||
: $"Numeric({col.Length})",
|
||||
DbfColumnType.Float => $"Float({col.Length},{col.DecimalCount})",
|
||||
DbfColumnType.Double => "Double",
|
||||
DbfColumnType.SignedLong => "Integer",
|
||||
DbfColumnType.Currency => "Currency",
|
||||
DbfColumnType.Date => "Date",
|
||||
DbfColumnType.DateTime => "DateTime",
|
||||
DbfColumnType.Boolean => "Logical",
|
||||
DbfColumnType.General => "General",
|
||||
_ => col.ColumnType.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private static Encoding ResolveEncoding(int? overrideCodePage, string? dbcPath, string folder)
|
||||
{
|
||||
// 1) Override esplicito dell'utente
|
||||
if (overrideCodePage.HasValue)
|
||||
{
|
||||
var enc = TryGetEncoding(overrideCodePage.Value);
|
||||
if (enc != null) return enc;
|
||||
}
|
||||
|
||||
// 2) Auto-detect dal byte "language driver" dell'header (dbc o prima tabella della cartella)
|
||||
try
|
||||
{
|
||||
string? sampleDbf = dbcPath;
|
||||
if (sampleDbf == null && Directory.Exists(folder))
|
||||
sampleDbf = Directory.EnumerateFiles(folder, "*.dbf", SearchOption.TopDirectoryOnly).FirstOrDefault();
|
||||
|
||||
if (sampleDbf != null && File.Exists(sampleDbf))
|
||||
{
|
||||
using var fs = File.OpenRead(sampleDbf);
|
||||
var header = new DbfHeader(fs);
|
||||
if (LanguageDriverCodePages.TryGetValue(header.LanguageDriver, out var cp))
|
||||
{
|
||||
var enc = TryGetEncoding(cp);
|
||||
if (enc != null) return enc;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignora: si usa il default
|
||||
}
|
||||
|
||||
// 3) Default
|
||||
return Encoding.GetEncoding(DefaultCodePage);
|
||||
}
|
||||
|
||||
private static Encoding? TryGetEncoding(int codePage)
|
||||
{
|
||||
try { return Encoding.GetEncoding(codePage); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static int? TryGetCodePageByName(string name)
|
||||
{
|
||||
try { return Encoding.GetEncoding(name).CodePage; }
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Linq.Expressions;
|
||||
using DataConnection.DB.FoxPro;
|
||||
using DataConnection.Interfaces;
|
||||
|
||||
namespace DataConnection.DB;
|
||||
|
||||
/// <summary>
|
||||
/// Database manager per sorgenti <b>Visual FoxPro / dBase</b> in modalità completamente
|
||||
/// <b>managed</b>: legge direttamente i file <c>.dbf</c>/<c>.fpt</c>/<c>.dbc</c> tramite la
|
||||
/// libreria <c>DbfDataReader</c>, senza alcun provider OLE DB/ODBC e funzionando a 64-bit.
|
||||
///
|
||||
/// <para><b>Sola lettura</b>: FoxPro è supportato come <i>sorgente</i> dati. Le operazioni di
|
||||
/// scrittura lanciano <see cref="NotSupportedException"/> — scrivere nei <c>.dbf</c> rischierebbe
|
||||
/// di corrompere indici <c>.cdx</c> e memo <c>.fpt</c> dei gestionali legacy.</para>
|
||||
///
|
||||
/// La "connection string" è semplicemente un percorso: un file <c>.dbc</c> (container) oppure
|
||||
/// una cartella di tabelle libere <c>.dbf</c>. Vedi <see cref="FoxProReader.Resolve"/>.
|
||||
/// </summary>
|
||||
public class FoxProDatabaseManager : IDatabaseManager
|
||||
{
|
||||
private const string ReadOnlyMessage =
|
||||
"Visual FoxPro è supportato in sola lettura (sorgente dati). La scrittura sui file .dbf non è disponibile.";
|
||||
|
||||
private readonly FoxProConnectionInfo _info;
|
||||
|
||||
public FoxProDatabaseManager(string connectionString)
|
||||
{
|
||||
_info = FoxProReader.Resolve(connectionString);
|
||||
}
|
||||
|
||||
public async Task<bool> TestConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
FoxProReader.EnsureReachable(_info);
|
||||
// Forza la lettura dell'elenco tabelle: conferma che cartella/.dbc siano leggibili.
|
||||
_ = FoxProReader.GetTableNames(_info);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> GetTableNamesAsync()
|
||||
=> Task.Run(() => (IEnumerable<string>)FoxProReader.GetTableNames(_info));
|
||||
|
||||
public Task<IEnumerable<DbColumnInfo>> GetTableSchemaAsync(string tableName)
|
||||
=> Task.Run(() => (IEnumerable<DbColumnInfo>)FoxProReader.GetTableColumns(_info, tableName));
|
||||
|
||||
public Task<IDictionary<string, IEnumerable<DbColumnInfo>>> GetDatabaseSchemaAsync()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var result = new Dictionary<string, IEnumerable<DbColumnInfo>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var table in FoxProReader.GetTableNames(_info))
|
||||
{
|
||||
try
|
||||
{
|
||||
var columns = FoxProReader.GetTableColumns(_info, table);
|
||||
if (columns.Count > 0)
|
||||
result[table] = columns;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[FoxPro] Schema non leggibile per {table}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
return (IDictionary<string, IEnumerable<DbColumnInfo>>)result;
|
||||
});
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsAsync(string tableName)
|
||||
=> Task.Run(() => (IEnumerable<Dictionary<string, object>>)FoxProReader.ReadRecords(_info, tableName).ToList());
|
||||
|
||||
public Task<List<Dictionary<string, object>>> ExecuteRawQueryAsync(string sql, string databaseName = "", params object[] parameters)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var parsed = FoxProReader.ParseSimpleSelect(sql);
|
||||
if (parsed == null)
|
||||
throw new NotSupportedException(
|
||||
"Per le sorgenti FoxPro è supportato solo 'SELECT [TOP n] * FROM <tabella>'. " +
|
||||
"Impossibile interpretare la query: " + sql);
|
||||
|
||||
var (table, top) = parsed.Value;
|
||||
return FoxProReader.ReadRecords(_info, table, top).ToList();
|
||||
});
|
||||
}
|
||||
|
||||
public Task<string?> GetPrimaryKeyFieldAsync(string tableName)
|
||||
// Le tabelle .dbf non espongono una PK affidabile: la chiave si seleziona nel coupler.
|
||||
=> Task.FromResult<string?>(null);
|
||||
|
||||
public Task<List<string>> GetAvailableDatabasesAsync()
|
||||
=> Task.FromResult(new List<string> { _info.DisplayName });
|
||||
|
||||
public Task ChangeDatabaseAsync(string databaseName)
|
||||
{
|
||||
// Sorgente file-based: il database è definito dal percorso. Nessun cambio a runtime.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ===== Operazioni non supportate (sola lettura) =====
|
||||
|
||||
public Task<int> ExecuteCommandAsync(string sql, params object[] parameters)
|
||||
=> throw new NotSupportedException(ReadOnlyMessage);
|
||||
|
||||
public Task<bool> UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary<string, object?> record)
|
||||
=> throw new NotSupportedException(ReadOnlyMessage);
|
||||
|
||||
// ===== Operazioni LINQ/tipizzate non applicabili a una sorgente DBF =====
|
||||
|
||||
public Task<IEnumerable<T>> GetAsync<T>(
|
||||
Expression<Func<T, bool>>? filter = null,
|
||||
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null,
|
||||
string includeProperties = "",
|
||||
int? skip = null,
|
||||
int? take = null) where T : class
|
||||
=> throw new NotSupportedException("GetAsync<T> con LINQ non è supportato per FoxPro. Usare GetAllRecordsAsync.");
|
||||
|
||||
public Task<T?> GetByIdAsync<T>(object id) where T : class
|
||||
=> throw new NotSupportedException("GetByIdAsync<T> non è supportato per FoxPro.");
|
||||
|
||||
public Task<IEnumerable<T>> ExecuteQueryAsync<T>(string sql, params object[] parameters) where T : class
|
||||
=> throw new NotSupportedException("ExecuteQueryAsync<T> tipizzato non è supportato per FoxPro. Usare ExecuteRawQueryAsync.");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nessuna risorsa persistente: ogni operazione apre/chiude i file.
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
|
||||
<PackageReference Include="System.Data.Odbc" Version="9.0.3" />
|
||||
<PackageReference Include="System.Data.OleDb" Version="9.0.3" />
|
||||
<!-- Visual FoxPro / dBase: lettura managed di .dbf/.fpt/.dbc (64-bit, nessun provider OLE DB richiesto).
|
||||
DbfDataReader porta transitivamente System.Text.Encoding.CodePages (per le code page non-Unicode). -->
|
||||
<PackageReference Include="DbfDataReader" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -90,6 +90,19 @@ public partial class DataCoupler : ComponentBase
|
||||
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
||||
return credential?.DatabaseType == DatabaseType.OleDb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se la credenziale database selezionata è di tipo Visual FoxPro
|
||||
/// (sorgente file-based in sola lettura).
|
||||
/// </summary>
|
||||
protected bool IsFoxproConnection()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedDatabaseCredential))
|
||||
return false;
|
||||
|
||||
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
|
||||
return credential?.DatabaseType == DatabaseType.Foxpro;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce il cambio di credenziale database selezionata
|
||||
@@ -760,6 +773,7 @@ public partial class DataCoupler : ComponentBase
|
||||
{
|
||||
DatabaseType.SqlServer => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
|
||||
DatabaseType.OleDb => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
|
||||
DatabaseType.Foxpro => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
|
||||
DatabaseType.Oracle => $"SELECT * FROM ({baseQuery}) WHERE ROWNUM <= {limit}",
|
||||
DatabaseType.MySql => $"{baseQuery} LIMIT {limit}",
|
||||
DatabaseType.PostgreSql => $"{baseQuery} LIMIT {limit}",
|
||||
|
||||
@@ -244,6 +244,7 @@ else
|
||||
<option value="@CredentialManager.Models.DatabaseType.SapHana">SAP HANA</option>*@
|
||||
<option value="@CredentialManager.Models.DatabaseType.Odbc">ODBC</option>
|
||||
<option value="@CredentialManager.Models.DatabaseType.OleDb">OLE DB</option>
|
||||
<option value="@CredentialManager.Models.DatabaseType.Foxpro">Visual FoxPro (.dbc / .dbf)</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
@@ -609,6 +610,76 @@ else
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (currentDatabaseCredential.DatabaseType == CredentialManager.Models.DatabaseType.Foxpro)
|
||||
{
|
||||
<!-- Configurazione Visual FoxPro -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h6 class="mb-0"><i class="oi oi-hard-drive"></i> Configurazione Visual FoxPro</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info py-2">
|
||||
<i class="oi oi-info"></i> Lettura <strong>managed</strong> dei file <code>.dbf</code>/<code>.fpt</code>/<code>.dbc</code>:
|
||||
funziona a <strong>64-bit senza installare alcun provider</strong>. FoxPro è utilizzabile come
|
||||
<strong>sorgente</strong> (sola lettura).
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Percorso Database FoxPro *</label>
|
||||
<InputText class="form-control font-monospace"
|
||||
@bind-Value="currentDatabaseCredential.DatabaseName"
|
||||
placeholder="es. C:\dati\Data.dbc oppure C:\dati" />
|
||||
<small class="form-text text-muted">
|
||||
<strong>Database container</strong>: percorso completo al file <code>.dbc</code>
|
||||
(es. <code>C:\Users\aless\Desktop\data\Data.dbc</code>).<br />
|
||||
<strong>Tabelle libere</strong>: percorso della <strong>cartella</strong> che contiene i file <code>.dbf</code>
|
||||
(es. <code>C:\Users\aless\Desktop\data</code>).
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Code Page (encoding)</label>
|
||||
<select class="form-select" @bind="foxproCodePage">
|
||||
<option value="">-- Auto / Default (1252) --</option>
|
||||
<option value="1252">1252 — Europa occidentale (ANSI)</option>
|
||||
<option value="1250">1250 — Europa centrale</option>
|
||||
<option value="1251">1251 — Cirillico</option>
|
||||
<option value="850">850 — DOS Latin-1</option>
|
||||
<option value="437">437 — DOS US</option>
|
||||
<option value="65001">65001 — UTF-8</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Per dati italiani lasciare 1252.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Record cancellati</label>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="foxproIncludeDeleted" @bind="foxproIncludeDeleted" />
|
||||
<label class="form-check-label" for="foxproIncludeDeleted">
|
||||
Includi i record marcati come cancellati
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Di default i record cancellati vengono esclusi.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light border py-2 small mb-3">
|
||||
<i class="oi oi-list"></i> Tipi VFP supportati: Character, Memo, Numeric, Float, Double,
|
||||
Integer, Currency, Date, DateTime, Logical. I campi memo (<code>.fpt</code>) e gli indici
|
||||
(<code>.cdx</code>) vengono rilevati automaticamente accanto al <code>.dbf</code>.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Anteprima percorso/connessione</label>
|
||||
<textarea class="form-control font-monospace" rows="2" readonly>@GetFoxproConnectionStringPreview()</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Configurazione Standard Database -->
|
||||
@@ -1035,6 +1106,10 @@ else
|
||||
private string oleDbCollatingSequence = string.Empty;
|
||||
private string oleDbDeleted = string.Empty;
|
||||
|
||||
// Visual FoxPro specific state
|
||||
private string foxproCodePage = string.Empty;
|
||||
private bool foxproIncludeDeleted = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{ await RefreshCredentials();
|
||||
CheckForProblematicCredentials();
|
||||
@@ -1131,7 +1206,15 @@ else
|
||||
if (currentDatabaseCredential.AdditionalParameters?.ContainsKey("DELETED") == true)
|
||||
oleDbDeleted = currentDatabaseCredential.AdditionalParameters["DELETED"];
|
||||
}
|
||||
|
||||
|
||||
// Se è Visual FoxPro, ripristina encoding e gestione record cancellati
|
||||
if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
|
||||
{
|
||||
foxproCodePage = currentDatabaseCredential.AdditionalParameters?.GetValueOrDefault("Encoding") ?? string.Empty;
|
||||
var incDel = currentDatabaseCredential.AdditionalParameters?.GetValueOrDefault("IncludeDeleted", "") ?? "";
|
||||
foxproIncludeDeleted = string.Equals(incDel, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
showDatabaseModal = true;
|
||||
}
|
||||
|
||||
@@ -1155,6 +1238,26 @@ else
|
||||
currentDatabaseCredential.AdditionalParameters.Remove("DELETED");
|
||||
}
|
||||
|
||||
// Sincronizza i parametri Visual FoxPro negli AdditionalParameters
|
||||
if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
|
||||
{
|
||||
currentDatabaseCredential.AdditionalParameters ??= new Dictionary<string, string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(foxproCodePage))
|
||||
currentDatabaseCredential.AdditionalParameters["Encoding"] = foxproCodePage;
|
||||
else
|
||||
currentDatabaseCredential.AdditionalParameters.Remove("Encoding");
|
||||
|
||||
if (foxproIncludeDeleted)
|
||||
currentDatabaseCredential.AdditionalParameters["IncludeDeleted"] = "true";
|
||||
else
|
||||
currentDatabaseCredential.AdditionalParameters.Remove("IncludeDeleted");
|
||||
|
||||
// Sorgente file-based: nessun host/porta/credenziali
|
||||
currentDatabaseCredential.Host = string.Empty;
|
||||
currentDatabaseCredential.Port = 0;
|
||||
}
|
||||
|
||||
await CredentialService.SaveDatabaseCredentialAsync(currentDatabaseCredential);
|
||||
await JSRuntime.InvokeVoidAsync("alert", "Credenziale database salvata con successo!");
|
||||
CloseDatabaseModal();
|
||||
@@ -1227,6 +1330,15 @@ else
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
|
||||
{
|
||||
// Visual FoxPro: sorgente file-based, richiede solo il percorso (DatabaseName)
|
||||
if (string.IsNullOrWhiteSpace(currentDatabaseCredential.DatabaseName))
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", "Inserisci il percorso del database FoxPro (file .dbc o cartella con i .dbf).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Altri database: validazione standard (Host, Username, Password)
|
||||
@@ -1281,7 +1393,15 @@ else
|
||||
{
|
||||
await LoadOdbcData();
|
||||
}
|
||||
|
||||
|
||||
// Se è Visual FoxPro, reimposta i valori di default (sola lettura, file-based)
|
||||
if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
|
||||
{
|
||||
currentDatabaseCredential.AdditionalParameters ??= new Dictionary<string, string>();
|
||||
foxproCodePage = string.Empty;
|
||||
foxproIncludeDeleted = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -1531,6 +1651,36 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFoxproConnectionStringPreview()
|
||||
{
|
||||
if (currentDatabaseCredential.DatabaseType != DatabaseType.Foxpro)
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
currentDatabaseCredential.AdditionalParameters ??= new Dictionary<string, string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(foxproCodePage))
|
||||
currentDatabaseCredential.AdditionalParameters["Encoding"] = foxproCodePage;
|
||||
else
|
||||
currentDatabaseCredential.AdditionalParameters.Remove("Encoding");
|
||||
|
||||
if (foxproIncludeDeleted)
|
||||
currentDatabaseCredential.AdditionalParameters["IncludeDeleted"] = "true";
|
||||
else
|
||||
currentDatabaseCredential.AdditionalParameters.Remove("IncludeDeleted");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currentDatabaseCredential.DatabaseName))
|
||||
return "(inserire il percorso al file .dbc o alla cartella di .dbf)";
|
||||
|
||||
return ConnectionStringBuilder.BuildConnectionString(currentDatabaseCredential);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"Errore nella generazione: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -91,6 +91,14 @@ namespace Data_Coupler.Services
|
||||
return new DataConnection.DB.OleDbDatabaseManager(connectionString);
|
||||
}
|
||||
|
||||
// Per Visual FoxPro, usa FoxProDatabaseManager (lettura managed di .dbf/.fpt/.dbc, 64-bit)
|
||||
if (credential.DatabaseType == DatabaseType.Foxpro)
|
||||
{
|
||||
var connectionString = CredentialManager.Models.ConnectionStringBuilder.BuildConnectionString(credential);
|
||||
_logger.LogInformation("Creando FoxProDatabaseManager (sola lettura) per {CredentialName}", credentialName);
|
||||
return new DataConnection.DB.FoxProDatabaseManager(connectionString);
|
||||
}
|
||||
|
||||
// Per altri database, usa EFCoreDatabaseManager
|
||||
var dbManagerOptions = await _credentialService.GetDbManagerOptionsAsync(credential.Name);
|
||||
return new EFCoreDatabaseManager(dbManagerOptions);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Connessione a database Visual FoxPro
|
||||
|
||||
Data-Coupler supporta i database **Visual FoxPro / dBase** come **sorgente dati** (sola lettura)
|
||||
tramite un lettore **completamente managed**: legge direttamente i file `.dbf` (tabelle),
|
||||
`.fpt` (campi memo) e `.dbc` (database container) **senza installare alcun provider OLE DB/ODBC**
|
||||
e funzionando a **64-bit**.
|
||||
|
||||
> ℹ️ **Perché managed e non OLE DB?** Il provider `VFPOLEDB.1` di Microsoft è **solo a 32-bit**,
|
||||
> deprecato e va installato a parte. Il lettore managed non ha questi vincoli e funziona ovunque
|
||||
> giri l'applicazione (anche in container Linux/Windows a 64-bit).
|
||||
|
||||
---
|
||||
|
||||
## 1. Come connettersi (esempio)
|
||||
|
||||
La "connessione" FoxPro è semplicemente un **percorso**. Non serve host, utente o password.
|
||||
|
||||
### Passo passo nell'interfaccia
|
||||
|
||||
1. Vai su **Credenziali** → **Aggiungi credenziale database**.
|
||||
2. **Nome**: un nome a piacere (es. `Gestionale FoxPro`).
|
||||
3. **Tipo Database**: seleziona **Visual FoxPro (.dbc / .dbf)**.
|
||||
4. **Percorso Database FoxPro**: inserisci uno dei due:
|
||||
|
||||
| Caso | Cosa inserire | Esempio |
|
||||
|---|---|---|
|
||||
| **Database container** | percorso completo al file `.dbc` | `C:\Users\aless\Desktop\data\Data.dbc` |
|
||||
| **Tabelle libere** (free tables) | percorso della **cartella** con i `.dbf` | `C:\Users\aless\Desktop\data` |
|
||||
|
||||
5. (Opzionale) **Code Page**: lascia `1252` per dati italiani/europei occidentali.
|
||||
6. (Opzionale) **Record cancellati**: lascia deselezionato per escludere i record marcati come cancellati.
|
||||
7. Premi **Test connessione**: deve mostrare il numero di tabelle rilevate.
|
||||
8. **Salva**.
|
||||
|
||||
A questo punto la credenziale è utilizzabile come **sorgente** nella pagina **Data Coupler**:
|
||||
selezionala, attendi la discovery delle tabelle, scegli la tabella e procedi con il mapping.
|
||||
|
||||
---
|
||||
|
||||
## 2. Formato della connection string
|
||||
|
||||
Internamente la credenziale viene tradotta in una stringa interpretata dal lettore FoxPro.
|
||||
Sono accettate tutte queste forme (utile se incolli un percorso o una stringa esistente):
|
||||
|
||||
```
|
||||
C:\Users\aless\Desktop\data\Data.dbc
|
||||
C:\Users\aless\Desktop\data
|
||||
Data Source=C:\Users\aless\Desktop\data\Data.dbc;CodePage=1252;IncludeDeleted=false
|
||||
Provider=vfpoledb;Data Source=C:\Users\aless\Desktop\data\Data.dbc;Collating Sequence=machine;
|
||||
```
|
||||
|
||||
> Le stringhe stile **OLE DB** (`Provider=vfpoledb;Data Source=...`) sono accettate per comodità:
|
||||
> viene estratto automaticamente il valore di `Data Source`. `Provider` e `Collating Sequence`
|
||||
> vengono ignorati (il lettore è managed).
|
||||
|
||||
Opzioni riconosciute:
|
||||
|
||||
| Chiave | Significato | Default |
|
||||
|---|---|---|
|
||||
| `Data Source` | percorso al `.dbc` o alla cartella di `.dbf` | — (obbligatorio) |
|
||||
| `CodePage` / `Encoding` | code page per i campi carattere/memo | auto-rilevata dall'header, fallback **1252** |
|
||||
| `IncludeDeleted` | includere i record marcati come cancellati | `false` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Tipi di dato supportati
|
||||
|
||||
| Tipo VFP | Mappato come |
|
||||
|---|---|
|
||||
| Character (`C`) | testo |
|
||||
| Varchar (`V`) | testo |
|
||||
| Memo (`M`, file `.fpt`) | testo |
|
||||
| Numeric (`N`) | decimale |
|
||||
| Float (`F`) | decimale |
|
||||
| Double (`B`) | double |
|
||||
| Integer (`I`) | intero |
|
||||
| Currency (`Y`) | decimale |
|
||||
| Date (`D`) | data |
|
||||
| DateTime (`T`) | data/ora |
|
||||
| Logical (`L`) | booleano |
|
||||
|
||||
I file memo (`.fpt`) e gli indici (`.cdx`) vengono individuati **automaticamente** accanto al `.dbf`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Note e limitazioni
|
||||
|
||||
- **Sola lettura**: FoxPro è utilizzabile solo come **sorgente**. La scrittura sui `.dbf` non è
|
||||
supportata (eviterebbe il rischio di corrompere indici `.cdx` e memo `.fpt` di gestionali legacy in uso).
|
||||
- **Elenco tabelle**: con un `.dbc` viene letto il catalogo del container (solo le tabelle effettivamente
|
||||
presenti come file `.dbf`); con una cartella vengono elencati tutti i `.dbf` presenti.
|
||||
- **Query**: la sorgente lavora in modalità "tabella" (estrazione completa della tabella selezionata).
|
||||
Le eventuali query custom supportano solo `SELECT [TOP n] * FROM <tabella>` (niente JOIN/WHERE evoluti).
|
||||
- **Performance**: la lettura è in **streaming**; anche tabelle da centinaia di MB vengono elaborate
|
||||
con un uso di memoria contenuto.
|
||||
|
||||
---
|
||||
|
||||
## 5. Risoluzione problemi
|
||||
|
||||
| Sintomo | Causa probabile | Soluzione |
|
||||
|---|---|---|
|
||||
| "Cartella del database FoxPro non trovata" | percorso errato | verifica il percorso del `.dbc` o della cartella |
|
||||
| "nessuna tabella .dbf trovata" | cartella senza `.dbf` | indica la cartella giusta o il file `.dbc` |
|
||||
| Caratteri accentati errati | code page diversa | imposta la **Code Page** corretta (es. 1250, 65001) |
|
||||
| Una tabella non compare | `.dbf` referenziato nel `.dbc` ma file assente | ripristina il file `.dbf` mancante |
|
||||
Reference in New Issue
Block a user