diff --git a/DataConnection/DB/EF/EFCoreDatabaseManager.cs b/DataConnection/DB/EF/EFCoreDatabaseManager.cs index be66f8b..de40674 100644 --- a/DataConnection/DB/EF/EFCoreDatabaseManager.cs +++ b/DataConnection/DB/EF/EFCoreDatabaseManager.cs @@ -19,7 +19,7 @@ namespace DataConnection.EF; public class EFCoreDatabaseManager : IDatabaseManager { private readonly DbManagerOptions _options; - private ExistingDatabaseContext _context; + private ExistingDatabaseContext _context = null!; public EFCoreDatabaseManager(DbManagerOptions options) { @@ -54,8 +54,8 @@ public class EFCoreDatabaseManager : IDatabaseManager } public async Task> GetAsync( - Expression> filter = null, - Func, IOrderedQueryable> orderBy = null, + Expression>? filter = null, + Func, IOrderedQueryable>? orderBy = null, string includeProperties = "", int? skip = null, int? take = null) where T : class @@ -91,7 +91,7 @@ public class EFCoreDatabaseManager : IDatabaseManager return await query.ToListAsync(); } - public async Task GetByIdAsync(object id) where T : class + public async Task GetByIdAsync(object id) where T : class { return await _context.Set().FindAsync(id); } @@ -101,6 +101,43 @@ public class EFCoreDatabaseManager : IDatabaseManager return await _context.Set().FromSqlRaw(sql, parameters).ToListAsync(); } + public async Task>> ExecuteRawQueryAsync(string sql, params object[] parameters) + { + using var command = _context.Database.GetDbConnection().CreateCommand(); + command.CommandText = sql; + + // Aggiungi i parametri + for (int i = 0; i < parameters.Length; i++) + { + var parameter = command.CreateParameter(); + parameter.ParameterName = $"@p{i}"; + parameter.Value = parameters[i] ?? DBNull.Value; + command.Parameters.Add(parameter); + } + + await _context.Database.OpenConnectionAsync(); + + var results = new List>(); + + using var reader = await command.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) + { + var row = new Dictionary(); + + for (int i = 0; i < reader.FieldCount; i++) + { + var columnName = reader.GetName(i); + var value = reader.IsDBNull(i) ? null : reader.GetValue(i); + row[columnName] = value ?? ""; // Usa stringa vuota invece di null + } + + results.Add(row); + } + + return results; + } + public async Task ExecuteCommandAsync(string sql, params object[] parameters) { return await _context.Database.ExecuteSqlRawAsync(sql, parameters); diff --git a/DataConnection/DB/Interfaces/IDatabaseManager.cs b/DataConnection/DB/Interfaces/IDatabaseManager.cs index de98d65..61215e4 100644 --- a/DataConnection/DB/Interfaces/IDatabaseManager.cs +++ b/DataConnection/DB/Interfaces/IDatabaseManager.cs @@ -25,8 +25,8 @@ public interface IDatabaseManager : IDisposable /// Numero di elementi da saltare /// Numero di elementi da prendere Task> GetAsync( - Expression> filter = null, - Func, IOrderedQueryable> orderBy = null, + Expression>? filter = null, + Func, IOrderedQueryable>? orderBy = null, string includeProperties = "", int? skip = null, int? take = null) where T : class; @@ -34,13 +34,18 @@ public interface IDatabaseManager : IDisposable /// /// Ottiene un'entità singola in base alla chiave primaria /// - Task GetByIdAsync(object id) where T : class; + Task GetByIdAsync(object id) where T : class; /// /// Esegue una query SQL raw /// Task> ExecuteQueryAsync(string sql, params object[] parameters) where T : class; + /// + /// Esegue una query SQL raw e restituisce i risultati come dictionary + /// + Task>> ExecuteRawQueryAsync(string sql, params object[] parameters); + /// /// Esegue un comando SQL che non restituisce risultati /// diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 31f0610..9cc3234 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -80,58 +80,183 @@ } - @if (databaseTables.Any()) + @if (isDatabaseConnected) { +
-
Tabelle Database (@databaseTables.Count disponibili):
- - -
-
- - - - - @if (!string.IsNullOrEmpty(databaseSearchTerm)) +
+ + +
+ + Scegli se selezionare una tabella o scrivere una query SQL personalizzata + +
+ + @if (useCustomQuery) + { + +
+
Query SQL Custom:
+ +
+ + + + + Scrivi una query SELECT personalizzata. La query verrà automaticamente ottimizzata per il trasferimento dati. + +
+ +
+ + + @if (isQueryValid) { - + + @if (showQueryPreview) + { + + } }
-
- - -
- @foreach (var table in GetFilteredDatabaseTables()) + + @if (!string.IsNullOrEmpty(queryValidationMessage) && !isQueryValid) { - -
@{ - var isSourceReady = (selectedSourceType == "database" && isDatabaseConnected && !string.IsNullOrEmpty(selectedTable)) || + var isSourceReady = (selectedSourceType == "database" && isDatabaseConnected && + ((useCustomQuery && isQueryValid) || (!useCustomQuery && !string.IsNullOrEmpty(selectedTable)))) || (selectedSourceType == "file" && !string.IsNullOrEmpty(selectedSheet)); } @if (isSourceReady && isRestConnected && selectedRestEntity != null) @@ -503,30 +629,60 @@
Campi @sourceTypeName (@sourceDisplayName)
+ @if (selectedSourceType == "database" && useCustomQuery && queryColumns.Any()) + { + + }
- @if (selectedSourceType == "database" && databaseTables.ContainsKey(selectedTable)) + @if (selectedSourceType == "database") { - @foreach (var column in databaseTables[selectedTable]) + @if (useCustomQuery && queryColumns.Any()) { - -
-
- @column.Name - @column.DataType + + @foreach (var column in queryColumns) + { + +
+
+ @column + Query Column +
+
+ @if (fieldMappings.ContainsKey(column)) + { + Mapped + } +
-
- + + } } } else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet)) @@ -694,11 +850,21 @@ { } - @if (selectedSourceType == "database" && databaseTables.ContainsKey(selectedTable)) + @if (selectedSourceType == "database") { - @foreach (var column in databaseTables[selectedTable].Where(c => c.Name != suggestedPrimaryKey)) + @if (useCustomQuery && queryColumns.Any()) { - + @foreach (var column in queryColumns) + { + + } + } + else if (!useCustomQuery && databaseTables.ContainsKey(selectedTable)) + { + @foreach (var column in databaseTables[selectedTable].Where(c => c.Name != suggestedPrimaryKey)) + { + + } } } else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet)) @@ -1010,7 +1176,18 @@ private string selectedDatabase = ""; private bool showDatabaseSelection = false; private bool showDatabaseSelectionModal = false; - private bool isLoadingDatabases = false; // File handling + private bool isLoadingDatabases = false; + + // Custom query functionality + private bool useCustomQuery = false; + private string customQuery = ""; + private bool isValidatingQuery = false; + private bool isQueryValid = false; + private string queryValidationMessage = ""; + private List> queryPreviewData = new(); + private List queryColumns = new(); + private bool showQueryPreview = false; + private bool isLoadingPreview = false; // File handling private string selectedFileName = ""; private bool isProcessingFile = false; private string fileErrorMessage = ""; @@ -1431,6 +1608,18 @@ selectedTable = ""; databaseSearchTerm = ""; databaseErrorMessage = ""; + + // Reset custom query state + useCustomQuery = false; + customQuery = ""; + isValidatingQuery = false; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + isLoadingPreview = false; + currentDatabaseManager?.Dispose(); currentDatabaseManager = null; @@ -1583,6 +1772,16 @@ } private async void SelectTable(string tableName) { selectedTable = tableName; + + // Clear custom query state when selecting a table + useCustomQuery = false; + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + // Clear mappings when changing table ClearAllMappings(); @@ -1753,29 +1952,52 @@ private void AutoMapFields() { - if (!databaseTables.ContainsKey(selectedTable) || restEntityDetails == null) + if (restEntityDetails == null) return; - var dbColumns = databaseTables[selectedTable]; - var restProperties = restEntityDetails.Properties; + IEnumerable sourceColumns = new List(); + + // Ottiene le colonne in base al tipo di sorgente + if (selectedSourceType == "database") + { + if (useCustomQuery && queryColumns.Any()) + { + sourceColumns = queryColumns; + } + else if (!useCustomQuery && databaseTables.ContainsKey(selectedTable)) + { + sourceColumns = databaseTables[selectedTable].Select(c => c.Name); + } + } + else if (selectedSourceType == "file" && fileSheets.ContainsKey(selectedSheet)) + { + sourceColumns = fileSheets[selectedSheet]; + } + if (!sourceColumns.Any()) + return; + + var restProperties = restEntityDetails.Properties; int mappingsCreated = 0; - foreach (var dbColumn in dbColumns) + foreach (var sourceColumn in sourceColumns) { // Trova una proprietà REST con nome simile var matchingProperty = restProperties.FirstOrDefault(p => - string.Equals(p.Name, dbColumn.Name, StringComparison.OrdinalIgnoreCase) || - string.Equals(p.Name.Replace("_", ""), dbColumn.Name.Replace("_", ""), StringComparison.OrdinalIgnoreCase) || - string.Equals(p.Name.Replace("Id", ""), dbColumn.Name.Replace("Id", ""), StringComparison.OrdinalIgnoreCase) + string.Equals(p.Name, sourceColumn, StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name.Replace("_", ""), sourceColumn.Replace("_", ""), StringComparison.OrdinalIgnoreCase) || + string.Equals(p.Name.Replace("Id", ""), sourceColumn.Replace("Id", ""), StringComparison.OrdinalIgnoreCase) ); - if (matchingProperty != null && !fieldMappings.ContainsKey(dbColumn.Name)) + if (matchingProperty != null && !fieldMappings.ContainsKey(sourceColumn)) { - fieldMappings[dbColumn.Name] = matchingProperty.Name; + fieldMappings[sourceColumn] = matchingProperty.Name; mappingsCreated++; } - } Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici", mappingsCreated); + } + + Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici da {SourceType}", + mappingsCreated, useCustomQuery ? "query custom" : selectedSourceType); } private async Task ShowMappingSummary() { var summary = "Riepilogo Configurazione:\n\n"; @@ -1803,11 +2025,30 @@ } // Check source-specific requirements - if (selectedSourceType == "database" && (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable))) + if (selectedSourceType == "database") { - transferMessage = "Database non connesso o tabella non selezionata."; - transferMessageType = "error"; - return; + if (currentDatabaseManager == null) + { + transferMessage = "Database non connesso."; + transferMessageType = "error"; + return; + } + + if (useCustomQuery) + { + if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) + { + transferMessage = "Query custom non valida. Validare la query prima di procedere."; + transferMessageType = "error"; + return; + } + } + else if (string.IsNullOrEmpty(selectedTable)) + { + transferMessage = "Tabella non selezionata."; + transferMessageType = "error"; + return; + } } if (selectedSourceType == "file" && string.IsNullOrEmpty(selectedSheet)) @@ -1832,7 +2073,9 @@ try { - var sourceName = selectedSourceType == "database" ? selectedTable : selectedSheet; + var sourceName = selectedSourceType == "database" + ? (useCustomQuery ? "custom_query" : selectedTable) + : selectedSheet; Logger.LogInformation("Iniziando trasferimento dati da {SourceType} {Source} a {Entity} con {MappingCount} mappature", selectedSourceType, sourceName, selectedRestEntity.Name, fieldMappings.Count); @@ -1883,7 +2126,9 @@ // Genera la chiave sorgente per questo record var sourceKey = GenerateSourceKey(record); - var currentSourceName = selectedSourceType == "database" ? selectedTable : selectedSheet; + var currentSourceName = selectedSourceType == "database" + ? (useCustomQuery ? "custom_query" : selectedTable) + : selectedSheet; // NUOVA LOGICA: Cerca associazione esistente if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey)) @@ -2085,18 +2330,39 @@ private async Task>> GetAllRecordsFromDatabase() { - if (currentDatabaseManager == null || string.IsNullOrEmpty(selectedTable)) + if (currentDatabaseManager == null) return new List>(); try { - // Usa il database manager per eseguire una query che ottiene tutti i record - // Questo è un esempio semplificato - potresti voler implementare paginazione per tabelle grandi - return await currentDatabaseManager.GetAllRecordsAsync(selectedTable); + if (useCustomQuery) + { + // Usa la query custom per ottenere tutti i record + if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery)) + { + throw new InvalidOperationException("Query custom non valida. Validare la query prima di procedere."); + } + + var cleanQuery = CleanQuery(customQuery); + Logger.LogInformation("Esecuzione query custom per trasferimento dati: {Query}", cleanQuery); + + return await currentDatabaseManager.ExecuteRawQueryAsync(cleanQuery); + } + else + { + // Usa il metodo standard per tabelle + if (string.IsNullOrEmpty(selectedTable)) + { + throw new InvalidOperationException("Nessuna tabella selezionata."); + } + + return await currentDatabaseManager.GetAllRecordsAsync(selectedTable); + } } catch (Exception ex) { - Logger.LogError(ex, "Errore nell'ottenere i record dalla tabella {Table}", selectedTable); + Logger.LogError(ex, "Errore nell'ottenere i record dal database. UseCustomQuery: {UseCustomQuery}, Table: {Table}, Query: {Query}", + useCustomQuery, selectedTable, useCustomQuery ? customQuery : "N/A"); throw; } } private async Task>> GetAllRecordsFromFile() @@ -2456,4 +2722,269 @@ // Per ora usa DocEntry come default per SAP B1 return "DocEntry"; } + + // Custom Query Methods + private void OnQueryModeChanged(ChangeEventArgs e) + { + useCustomQuery = bool.Parse(e.Value?.ToString() ?? "false"); + + if (useCustomQuery) + { + // Reset table selection when switching to custom query + selectedTable = ""; + ClearAllMappings(); + + // Reset query-specific state + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + + // For custom queries, require manual key selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + } + else + { + // Reset custom query when switching to table mode + customQuery = ""; + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + ClearAllMappings(); + + // Reset key field selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = false; + } + + StateHasChanged(); + } + + private async Task ValidateCustomQuery() + { + if (string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) + { + isQueryValid = false; + queryValidationMessage = "Query vuota o database non connesso"; + return; + } + + isValidatingQuery = true; + queryValidationMessage = ""; + queryColumns.Clear(); + + try + { + // Converte la query per testare solo 1 riga + var testQuery = ConvertQueryForValidation(customQuery); + + Logger.LogInformation("Validazione query: {TestQuery}", testQuery); + + // Esegue la query di test + var testResults = await currentDatabaseManager.ExecuteRawQueryAsync(testQuery); + + if (testResults != null && testResults.Any()) + { + isQueryValid = true; + + // Estrae i nomi delle colonne dal primo record + var firstRecord = testResults.First(); + queryColumns = firstRecord.Keys.ToList(); + + // Non mostra più messaggi di successo per ridurre l'ingombro visivo + queryValidationMessage = ""; + + Logger.LogInformation("Query validata con successo. Colonne: {Columns}", string.Join(", ", queryColumns)); + + // Clear existing mappings since we have new columns + fieldMappings.Clear(); + selectedDbColumn = ""; + selectedRestProperty = ""; + + // For custom queries, always require manual key selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + + StateHasChanged(); + } + else + { + isQueryValid = false; + queryValidationMessage = "Query valida ma non restituisce risultati"; + queryColumns.Clear(); + } + } + catch (Exception ex) + { + isQueryValid = false; + queryValidationMessage = $"Errore nella validazione: {ex.Message}"; + queryColumns.Clear(); + Logger.LogError(ex, "Errore nella validazione della query custom"); + } + finally + { + isValidatingQuery = false; + StateHasChanged(); + } + } + + private async Task LoadQueryPreview() + { + if (!isQueryValid || string.IsNullOrWhiteSpace(customQuery) || currentDatabaseManager == null) + { + return; + } + + isLoadingPreview = true; + + try + { + // Usa la query limitata per il preview (max 50 righe per performance) + var previewQuery = ConvertQueryForPreview(customQuery, 50); + + Logger.LogInformation("Caricamento preview query: {PreviewQuery}", previewQuery); + + queryPreviewData = await currentDatabaseManager.ExecuteRawQueryAsync(previewQuery); + showQueryPreview = true; + + Logger.LogInformation("Preview caricato: {RowCount} righe", queryPreviewData.Count); + } + catch (Exception ex) + { + queryValidationMessage = $"Errore nel caricamento preview: {ex.Message}"; + Logger.LogError(ex, "Errore nel caricamento del preview della query"); + } + finally + { + isLoadingPreview = false; + StateHasChanged(); + } + } + + private void HideQueryPreview() + { + showQueryPreview = false; + queryPreviewData.Clear(); + StateHasChanged(); + } + + private string ConvertQueryForValidation(string originalQuery) + { + // Rimuove commenti e spazi extra + var cleanQuery = CleanQuery(originalQuery); + + // Se la query ha già un LIMIT/TOP, la usa così com'è per il test + if (HasLimitClause(cleanQuery)) + { + return cleanQuery; + } + + // Aggiunge LIMIT/TOP in base al tipo di database + return AddLimitClause(cleanQuery, 1); + } + + private string ConvertQueryForPreview(string originalQuery, int maxRows = 50) + { + var cleanQuery = CleanQuery(originalQuery); + + // Se la query ha già un LIMIT/TOP con un valore minore, la mantiene + if (HasLimitClause(cleanQuery)) + { + return cleanQuery; + } + + return AddLimitClause(cleanQuery, maxRows); + } + + private string CleanQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + return ""; + + // Rimuove commenti SQL + var lines = query.Split('\n') + .Select(line => line.Contains("--") ? line.Substring(0, line.IndexOf("--")) : line) + .Where(line => !string.IsNullOrWhiteSpace(line)); + + var cleanQuery = string.Join(" ", lines).Trim(); + + // Rimuove il punto e virgola finale se presente + if (cleanQuery.EndsWith(";")) + { + cleanQuery = cleanQuery.Substring(0, cleanQuery.Length - 1); + } + + return cleanQuery; + } + + private bool HasLimitClause(string query) + { + var upperQuery = query.ToUpperInvariant(); + return upperQuery.Contains(" LIMIT ") || + upperQuery.Contains(" TOP ") || + upperQuery.Contains("ROWNUM") || + upperQuery.Contains("FETCH FIRST"); + } + + private string AddLimitClause(string query, int limit) + { + var upperQuery = query.ToUpperInvariant(); + + // Per SQL Server, Oracle, e altri che supportano TOP + if (upperQuery.Contains("SELECT ")) + { + var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); + if (credential != null) + { + var dbType = credential.DatabaseType.ToString().ToLowerInvariant(); + switch (dbType) + { + case "sqlserver": + case "oracle": + // Aggiunge TOP dopo SELECT + return query.Replace("SELECT ", $"SELECT TOP {limit} ", StringComparison.OrdinalIgnoreCase); + + case "mysql": + case "postgresql": + case "sqlite": + default: + // Aggiunge LIMIT alla fine + return $"{query} LIMIT {limit}"; + } + } + } + + // Fallback: aggiunge LIMIT + return $"{query} LIMIT {limit}"; + } + + private void OnCustomQueryChanged(ChangeEventArgs e) + { + customQuery = e.Value?.ToString() ?? ""; + + // Reset validation quando la query cambia + isQueryValid = false; + queryValidationMessage = ""; + queryPreviewData.Clear(); + queryColumns.Clear(); + showQueryPreview = false; + + // Clear mappings quando la query cambia + ClearAllMappings(); + + // Reset key field selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + + StateHasChanged(); + } } diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index 052349c..10621da 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index 26862dc..028f54c 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 06dd215..863b848 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ