Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/vsix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
mkdir vsix
7z x src/GUI/lib/efreveng100.exe.zip -oefreveng100 -y
dir /a:-d /s /b "efreveng100" | find /c ":\" > filecount.txt
findstr "139" filecount.txt
findstr "141" filecount.txt

- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
Expand Down
5 changes: 5 additions & 0 deletions samples/efcpt-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
"default": false,
"title": "Use sp_describe_first_result_set instead of SET FMTONLY for result set discovery"
},
"generate-empty-result-type": {
"type": "boolean",
"default": false,
"title": "Generate empty result class for this stored procedure when result set cannot be discovered. When false (default), uses SqlQueryRaw directly."
},
"mapped-type": {
"type": "string",
"default": null,
Expand Down
2 changes: 2 additions & 0 deletions src/Core/NUnitTestCore/CliConfigMapperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ public void CanGetOptions(CliConfig config)
options.UseT4Split.Should().Be(config.CodeGeneration.UseT4Split);
options.UseTypedTvpParameters.Should().Be(config.CodeGeneration.UseTypedTvpParameters);

// Total should be 55 properties (was 56, removed GenerateEmptyResultType from global config)
options.GetType().GetProperties().Length.Should().Be(55);

config.GetType().GetProperties().Length.Should().Be(10);
config.Names.GetType().GetProperties().Length.Should().Be(4);
// Total should be 24 properties (was 25, removed GenerateEmptyResultType)
config.CodeGeneration.GetType().GetProperties().Length.Should().Be(24);
config.FileLayout.GetType().GetProperties().Length.Should().Be(5);
config.Replacements.GetType().GetProperties().Length.Should().Be(5);
Expand Down
17 changes: 4 additions & 13 deletions src/Core/RevEng.Core.80/DbContextExtensionsSqlQuery
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,14 @@ namespace #NAMESPACE#
{
#ACCESSMODIFIER# static class DbContextExtensions
{
public static async Task<List<T>#NULLABLE#> SqlQueryAsync<T>(this DbContext db, string sql, object[]#NULLABLE# parameters = null, CancellationToken? cancellationToken = default)
public static async Task<List<T>> SqlQueryAsync<T>(this DbContext db, string sql, object[]#NULLABLE# parameters = null, CancellationToken cancellationToken = default)
where T : class
{
parameters ??= Array.Empty<object>();
cancellationToken ??= CancellationToken.None;

if (typeof(T).GetProperties().Any())
{
return await db.Database
.SqlQueryRaw<T>(sql, parameters)
.ToListAsync(cancellationToken.Value);
}
else
{
await db.Database.ExecuteSqlRawAsync(sql, parameters, cancellationToken.Value);
return default;
}
return await db.Database
.SqlQueryRaw<T>(sql, parameters)
.ToListAsync(cancellationToken);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/Core/RevEng.Core.80/ReverseEngineerScaffolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ public SavedModelFiles GenerateStoredProcedures(
.Where(t => t.ObjectType == ObjectType.Procedure && !string.IsNullOrEmpty(t.MappedType))
.Select(m => new { m.Name, m.MappedType })
.ToDictionary(m => m.Name, m => m.MappedType),
ModulesGeneratingEmptyResultTypes = options.Tables
.Where(t => t.ObjectType == ObjectType.Procedure && t.GenerateEmptyResultType)
.Select(m => new { m.Name, m.GenerateEmptyResultType })
.ToDictionary(m => m.Name, m => m.GenerateEmptyResultType),
};

var procedureModel = procedureModelFactory.Create(options.Dacpac ?? options.ConnectionString, procedureModelFactoryOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,8 @@

if (procedure.HasValidResultSet && (procedure.Results.Count == 0 || procedure.Results[0].Count == 0))
{
var asyncExec = fullExec;

if (useNullableReferences)
{
asyncExec = asyncExec.Replace(" cancellationToken", " cancellationToken ?? CancellationToken.None", StringComparison.OrdinalIgnoreCase);
}

Sb.AppendLine(useAsyncCalls
? $"var _ = await _context.Database.ExecuteSqlRawAsync({asyncExec});"
? $"var _ = await _context.Database.ExecuteSqlRawAsync({fullExec});"
: $"var _ = _context.Database.ExecuteSqlRaw({fullExec});");
}
else
Expand Down Expand Up @@ -194,10 +187,8 @@
returnType = $"List<{returnClass}>";
}

if (useNullableReferences && !returnType.EndsWith('?'))
{
returnType += '?';
}
// Do not add nullable annotation to List<T> when nullable references are enabled
// The list itself is never null (empty list instead)

Check failure on line 191 in src/Core/RevEng.Core.80/Routines/Procedures/PostgresStoredProcedureScaffolder.cs

View workflow job for this annotation

GitHub Actions / build

Single-line comments should not be followed by blank line (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md)

Check failure on line 191 in src/Core/RevEng.Core.80/Routines/Procedures/PostgresStoredProcedureScaffolder.cs

View workflow job for this annotation

GitHub Actions / build

Single-line comments should not be followed by blank line (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md)

Check failure on line 191 in src/Core/RevEng.Core.80/Routines/Procedures/PostgresStoredProcedureScaffolder.cs

View workflow job for this annotation

GitHub Actions / build

Single-line comments should not be followed by blank line (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1512.md)

returnType = useAsyncCalls ? $"Task<{returnType}>" : returnType;

Expand All @@ -214,9 +205,9 @@
line += $"{string.Join(", ", outParamStrings)}";
}

var nullable = useNullableReferences ? "?" : string.Empty;

Check failure on line 208 in src/Core/RevEng.Core.80/Routines/Procedures/PostgresStoredProcedureScaffolder.cs

View workflow job for this annotation

GitHub Actions / build

Remove the unused local variable 'nullable'. (https://rules.sonarsource.com/csharp/RSPEC-1481)

Check failure on line 208 in src/Core/RevEng.Core.80/Routines/Procedures/PostgresStoredProcedureScaffolder.cs

View workflow job for this annotation

GitHub Actions / build

Remove the unused local variable 'nullable'. (https://rules.sonarsource.com/csharp/RSPEC-1481)

Check failure on line 208 in src/Core/RevEng.Core.80/Routines/Procedures/PostgresStoredProcedureScaffolder.cs

View workflow job for this annotation

GitHub Actions / build

Remove the unused local variable 'nullable'. (https://rules.sonarsource.com/csharp/RSPEC-1481)

line += useAsyncCalls ? $", CancellationToken{nullable} cancellationToken = default)" : ")";
line += useAsyncCalls ? $", CancellationToken cancellationToken = default)" : ")";

return line;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public ScaffoldedModel ScaffoldModel(RoutineModel model, ModuleScaffolderOptions
continue;
}

// Only generate empty result classes if GenerateEmptyResultType is enabled for this specific routine
if (resultSet.Count == 0 && !routine.GenerateEmptyResultType)
{
continue;
}

var suffix = string.Empty;
if (routine.Results.Count > 1)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,8 @@ protected override void GenerateProcedure(Routine procedure, RoutineModel model,

if (procedure.HasValidResultSet && (procedure.Results.Count == 0 || procedure.Results[0].Count == 0))
{
var asyncExec = fullExec;

if (useNullableReferences)
{
asyncExec = asyncExec.Replace(" cancellationToken", " cancellationToken ?? CancellationToken.None", StringComparison.OrdinalIgnoreCase);
}

Sb.AppendLine(useAsyncCalls
? $"var _ = await _context.Database.ExecuteSqlRawAsync({asyncExec});"
? $"var _ = await _context.Database.ExecuteSqlRawAsync({fullExec});"
: $"var _ = _context.Database.ExecuteSqlRaw({fullExec});");
}
else
Expand Down Expand Up @@ -275,10 +268,8 @@ private static string GenerateMethodSignature(
returnType = $"List<{returnClass}>";
}

if (useNullableReferences && !returnType.EndsWith('?'))
{
returnType += '?';
}
// Do not add nullable annotation to List<T> when nullable references are enabled
// The list itself is never null (empty list instead)
}

returnType = useAsyncCalls ? $"Task<{returnType}>" : returnType;
Expand All @@ -305,7 +296,7 @@ private static string GenerateMethodSignature(

line += $"OutputParameter<int>{nullable} {retValueIdentifier} = null";

line += useAsyncCalls ? $", CancellationToken{nullable} cancellationToken = default)" : ")";
line += useAsyncCalls ? $", CancellationToken cancellationToken = default)" : ")";

return line;
}
Expand Down
12 changes: 11 additions & 1 deletion src/Core/RevEng.Core.80/Routines/SqlServerRoutineModelFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ protected RoutineModel GetRoutines(string connectionString, ModuleModelFactoryOp
module.MappedType = options.MappedModules[key];
}

if (options.ModulesGeneratingEmptyResultTypes?.ContainsKey(key) ?? false)
{
module.GenerateEmptyResultType = options.ModulesGeneratingEmptyResultTypes[key];
}

if (allParameters.TryGetValue($"[{module.Schema}].[{module.Name}]", out var moduleParameters))
{
module.Parameters = moduleParameters;
Expand Down Expand Up @@ -114,7 +119,12 @@ protected RoutineModel GetRoutines(string connectionString, ModuleModelFactoryOp
new List<ModuleResultElement>(),
};
#pragma warning disable CA1308 // Normalize strings to uppercase
errors.Add($"Unable to get result set shape for {RoutineType.ToLower(CultureInfo.InvariantCulture)} '{module.Schema}.{module.Name}'. {ex.Message}.");
var errorMessage = $"Unable to get result set shape for {RoutineType.ToLower(CultureInfo.InvariantCulture)} '{module.Schema}.{module.Name}'. {ex.Message}.";
errorMessage += "\n Suggestions:";
errorMessage += "\n - Try alternate result set discovery method: add 'use-legacy-resultset-discovery: true' for this procedure in config";
errorMessage += "\n - Set 'generate-empty-result-type: true' in config to create an empty result class that you can customize";
errorMessage += "\n - Exclude this procedure from generation if it's not needed";
errors.Add(errorMessage);
#pragma warning restore CA1308 // Normalize strings to uppercase
}
#pragma warning restore CA1031 // Do not catch general exception types
Expand Down
2 changes: 2 additions & 0 deletions src/Core/RevEng.Core.Abstractions/Metadata/Routine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public bool NoResultSet

public bool IsScalar { get; set; }

public bool GenerateEmptyResultType { get; set; }

public List<ModuleParameter> Parameters { get; set; } = new List<ModuleParameter>();

public List<List<ModuleResultElement>> Results { get; set; } = new List<List<ModuleResultElement>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class ModuleModelFactoryOptions
#pragma warning disable CA2227 // Collection properties should be read only
public IDictionary<string, string> MappedModules { get; set; }

public IDictionary<string, bool> ModulesGeneratingEmptyResultTypes { get; set; }

#pragma warning restore CA2227 // Collection properties should be read only
public bool FullModel { get; set; }

Expand Down
15 changes: 14 additions & 1 deletion src/GUI/RevEng.Shared/Cli/CliConfigMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,20 @@ private static void ToSerializationModel<T>(IList<T> entities, Action<IEnumerabl

var serializationTableModels = entities.Where<T>(entity => ExclusionFilter(entity, excludeAll, filters)
&& !string.IsNullOrEmpty(entity.Name))
.Select(entity => new SerializationTableModel(entity.Name, objectType, entity.ExcludedColumns, entity.ExcludedIndexes))
.Select(entity =>
{
var model = new SerializationTableModel(entity.Name, objectType, entity.ExcludedColumns, entity.ExcludedIndexes);

// Map stored procedure specific properties
if (entity is StoredProcedure storedProcedure)
{
model.UseLegacyResultSetDiscovery = storedProcedure.UseLegacyResultsetDiscovery;
model.MappedType = storedProcedure.MappedType;
model.GenerateEmptyResultType = storedProcedure.GenerateEmptyResultType;
}

return model;
})
.ToList();

if (typeof(T) == typeof(Table)
Expand Down
5 changes: 5 additions & 0 deletions src/GUI/RevEng.Shared/Cli/Configuration/StoredProcedure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public class StoredProcedure : IEntity
[JsonPropertyName("use-legacy-resultset-discovery")]
public bool UseLegacyResultsetDiscovery { get; set; }

[JsonPropertyOrder(60)]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
[JsonPropertyName("generate-empty-result-type")]
public bool GenerateEmptyResultType { get; set; }

[JsonIgnore]
public List<string> ExcludedColumns { get; set; }

Expand Down
6 changes: 6 additions & 0 deletions src/GUI/RevEng.Shared/SerializationTableModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,11 @@ public SerializationTableModel(
/// </summary>
[DataMember(EmitDefaultValue = false)]
public string MappedType { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to generate an empty result type for stored procedures when result set cannot be discovered.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public bool GenerateEmptyResultType { get; set; }
}
}
Loading