diff --git a/src/Faithlife.Data/SqlFormatting/Sql.cs b/src/Faithlife.Data/SqlFormatting/Sql.cs index 98396c6..ed5d72c 100644 --- a/src/Faithlife.Data/SqlFormatting/Sql.cs +++ b/src/Faithlife.Data/SqlFormatting/Sql.cs @@ -335,7 +335,7 @@ internal override string Render(SqlContext context) if (m_filter is not null) filteredProperties = filteredProperties.Where(x => m_filter(x.Name)); - var text = string.Join(", ", filteredProperties.Select(x => context.RenderParam(x.GetValue(m_dto)))); + var text = string.Join(", ", filteredProperties.Select(x => context.RenderParam(key: null, value: x.GetValue(m_dto)))); if (text.Length == 0) throw new InvalidOperationException($"The specified type has no remaining columns: {type.FullName}"); return text; @@ -401,7 +401,7 @@ private sealed class JoinSql : Sql private sealed class LikePrefixParamSql : Sql { public LikePrefixParamSql(string prefix) => m_prefix = prefix; - internal override string Render(SqlContext context) => context.RenderParam(context.Syntax.EscapeLikeFragment(m_prefix) + "%"); + internal override string Render(SqlContext context) => context.RenderParam(key: this, value: context.Syntax.EscapeLikeFragment(m_prefix) + "%"); private readonly string m_prefix; } @@ -415,7 +415,7 @@ private sealed class NameSql : Sql private sealed class ParamSql : Sql { public ParamSql(object? value) => m_value = value; - internal override string Render(SqlContext context) => context.RenderParam(m_value); + internal override string Render(SqlContext context) => context.RenderParam(key: this, value: m_value); private readonly object? m_value; } @@ -436,7 +436,7 @@ public string Format(string? format, object? arg, IFormatProvider? formatProvide { if (format is not null) throw new FormatException($"Format specifier '{format}' is not supported."); - return arg is Sql sql ? sql.Render(m_context) : m_context.RenderParam(arg); + return arg is Sql sql ? sql.Render(m_context) : m_context.RenderParam(key: null, value: arg); } private readonly SqlContext m_context; diff --git a/src/Faithlife.Data/SqlFormatting/SqlContext.cs b/src/Faithlife.Data/SqlFormatting/SqlContext.cs index 1e2a6bf..7c8d927 100644 --- a/src/Faithlife.Data/SqlFormatting/SqlContext.cs +++ b/src/Faithlife.Data/SqlFormatting/SqlContext.cs @@ -13,13 +13,25 @@ public SqlContext(SqlSyntax syntax) public DbParameters Parameters => m_parameters is null ? DbParameters.Empty : DbParameters.Create(m_parameters); - public string RenderParam(object? value) + public string RenderParam(object? key, object? value) { - m_parameters ??= new List<(string Name, object? Value)>(); + if (key is not null && m_renderedParams is not null && m_renderedParams.TryGetValue(key, out var rendered)) + return rendered; + + m_parameters ??= []; var name = Invariant($"fdp{m_parameters.Count}"); m_parameters.Add((name, value)); - return Syntax.ParameterPrefix + name; + rendered = Syntax.ParameterPrefix + name; + + if (key is not null) + { + m_renderedParams ??= new(); + m_renderedParams.Add(key, rendered); + } + + return rendered; } private List<(string Name, object? Value)>? m_parameters; + private Dictionary? m_renderedParams; } diff --git a/tests/Faithlife.Data.Tests/SqlFormatting/SqlSyntaxTests.cs b/tests/Faithlife.Data.Tests/SqlFormatting/SqlSyntaxTests.cs index 423c41c..4b03a74 100644 --- a/tests/Faithlife.Data.Tests/SqlFormatting/SqlSyntaxTests.cs +++ b/tests/Faithlife.Data.Tests/SqlFormatting/SqlSyntaxTests.cs @@ -85,6 +85,18 @@ public void FormatSql(int? id) } } + [Test] + public void SameParamTwice() + { + var id = 42; + var name = "xyzzy"; + var desc = "long description"; + var descParam = Sql.Param(desc); + var (text, parameters) = Render(Sql.Format($"insert into widgets (Id, Name, Desc) values ({id}, {name}, {descParam}) on duplicate key update Name = {name}, Desc = {descParam}")); + text.Should().Be("insert into widgets (Id, Name, Desc) values (@fdp0, @fdp1, @fdp2) on duplicate key update Name = @fdp3, Desc = @fdp2"); + parameters.Should().Equal(("fdp0", id), ("fdp1", name), ("fdp2", desc), ("fdp3", name)); + } + [Test] public void FormatBadFormat() {