Skip to content
Merged
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 Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<PropertyGroup Label="Package metadata">
<Authors>Luís Amorim</Authors>
<PackageVersion>0.2.0-preview.1</PackageVersion>
<PackageVersion>0.2.0-preview.2</PackageVersion>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/lgamorim/sqlbound</PackageProjectUrl>
<RepositoryUrl>https://github.com/lgamorim/sqlbound</RepositoryUrl>
Expand Down
4 changes: 3 additions & 1 deletion src/SqlBound.Generators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ SQLB004 | SqlBound.Usage | Error | [SqlQuery] method must return Task&lt;IReadOn
SQLB005 | SqlBound.Usage | Error | Row type must have one public constructor with supported column types
SQLB006 | SqlBound.Usage | Error | Query parameter type is not supported
SQLB007 | SqlBound.Usage | Error | [SqlQuery] command text must not be empty
SQLB008 | SqlBound.Usage | Error | [SqlQuery] method must not be generic or nested in a generic type
SQLB008 | SqlBound.Usage | Error | [SqlQuery]/[SqlExecute] method must not be generic or nested in a generic type
SQLB009 | SqlBound.Usage | Error | [SqlExecute] method must return Task or Task&lt;int&gt;
SQLB010 | SqlBound.Usage | Error | A method cannot carry both [SqlQuery] and [SqlExecute]
190 changes: 170 additions & 20 deletions src/SqlBound.Generators/QueryMethodEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,20 @@ private static void EmitMethod(QueryMethodModel model, int depth, Action<int, st
var transaction = ParameterNameOrNull(model, ParameterKind.Transaction);
var cancellationToken = ParameterNameOrNull(model, ParameterKind.CancellationToken)
?? "global::System.Threading.CancellationToken.None";
var returnType = "global::System.Threading.Tasks.Task<"
+ $"global::System.Collections.Generic.IReadOnlyList<{model.RowTypeText}>>";

var signatureParameters = new List<string>();
foreach (var parameter in model.Parameters)
{
var prefix = model.IsExtensionMethod && signatureParameters.Count == 0 ? "this " : "";
if (model.Shape == ResultShape.Stream && parameter.Kind == ParameterKind.CancellationToken)
{
prefix += "[global::System.Runtime.CompilerServices.EnumeratorCancellation] ";
}

signatureParameters.Add($"{prefix}{parameter.TypeText} {parameter.Name}");
}

line(depth, $"{model.Accessibility} static async partial {returnType} "
line(depth, $"{model.Accessibility} static async partial {model.ReturnTypeText} "
+ $"{model.MethodName}({string.Join(", ", signatureParameters)})");
line(depth, "{");
line(depth + 1, $"if ({connection} is null)");
Expand Down Expand Up @@ -119,39 +122,186 @@ private static void EmitMethod(QueryMethodModel model, int depth, Action<int, st
scalarIndex++;
}

if (model.Shape is ResultShape.Execute or ResultShape.ExecuteDiscard)
{
var keyword = model.Shape == ResultShape.Execute ? "return await" : "await";
line(depth + 2, $"{keyword} __command.ExecuteNonQueryAsync({cancellationToken}).ConfigureAwait(false);");
}
else
{
EmitReaderBlock(model, depth, line, cancellationToken);
}

line(depth + 1, "}");
line(depth + 1, "finally");
line(depth + 1, "{");
line(depth + 2, "await __command.DisposeAsync().ConfigureAwait(false);");
line(depth + 1, "}");
line(depth, "}");
}

private static void EmitReaderBlock(
QueryMethodModel model, int depth, Action<int, string> line, string cancellationToken)
{
line(depth + 2, "global::System.Data.Common.DbDataReader __reader = "
+ $"await __command.ExecuteReaderAsync({cancellationToken}).ConfigureAwait(false);");
line(depth + 2, "try");
line(depth + 2, "{");
for (var i = 0; i < model.Columns.Count; i++)
if (model.ElementKind == ResultElementKind.Row)
{
line(depth + 3, $"int __ordinal{i} = __reader.GetOrdinal(\"{model.Columns[i].Name}\");");
for (var i = 0; i < model.Columns.Count; i++)
{
line(depth + 3, $"int __ordinal{i} = __reader.GetOrdinal(\"{model.Columns[i].Name}\");");
}
}

line(depth + 3, $"global::System.Collections.Generic.List<{model.RowTypeText}> __rows = new();");
line(depth + 3, $"while (await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth + 3, "{");
line(depth + 4, $"__rows.Add(new {model.RowTypeText}(");
for (var i = 0; i < model.Columns.Count; i++)
switch (model.Shape, model.ElementKind)
{
var terminator = i == model.Columns.Count - 1 ? "));" : ",";
line(depth + 5, ReadColumn(model.Columns[i], i) + terminator);
case (ResultShape.RowList, ResultElementKind.Row):
EmitRowListRead(model, depth + 3, line, cancellationToken);
break;
case (ResultShape.RowList, ResultElementKind.Scalar):
EmitScalarListRead(model, depth + 3, line, cancellationToken);
break;
case (ResultShape.SingleRow, ResultElementKind.Row):
EmitSingleRead(model, depth + 3, line, cancellationToken, optional: false);
break;
case (ResultShape.OptionalRow, ResultElementKind.Row):
EmitSingleRead(model, depth + 3, line, cancellationToken, optional: true);
break;
case (ResultShape.SingleRow, ResultElementKind.Scalar):
EmitScalarSingleRead(model, depth + 3, line, cancellationToken, optional: false);
break;
case (ResultShape.OptionalRow, ResultElementKind.Scalar):
EmitScalarSingleRead(model, depth + 3, line, cancellationToken, optional: true);
break;
case (ResultShape.Stream, _):
EmitStreamRead(model, depth + 3, line, cancellationToken);
break;
default:
throw new InvalidOperationException(
$"Unknown result shape '{model.Shape}'/'{model.ElementKind}'.");
}

line(depth + 3, "}");
line(depth + 3, "");
line(depth + 3, "return __rows;");
line(depth + 2, "}");
line(depth + 2, "finally");
line(depth + 2, "{");
line(depth + 3, "await __reader.DisposeAsync().ConfigureAwait(false);");
line(depth + 2, "}");
line(depth + 1, "}");
line(depth + 1, "finally");
line(depth + 1, "{");
line(depth + 2, "await __command.DisposeAsync().ConfigureAwait(false);");
line(depth + 1, "}");
}

private static void EmitRowListRead(
QueryMethodModel model, int depth, Action<int, string> line, string cancellationToken)
{
line(depth, $"global::System.Collections.Generic.List<{model.RowTypeText}> __rows = new();");
line(depth, $"while (await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth, "{");
EmitRowConstruction(model, depth + 1, line, "__rows.Add(", ");");
line(depth, "}");
line(depth, "");
line(depth, "return __rows;");
}

private static void EmitSingleRead(
QueryMethodModel model, int depth, Action<int, string> line, string cancellationToken, bool optional)
{
line(depth, $"if (!await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth, "{");
line(depth + 1, optional
? "return null;"
: "throw new global::System.InvalidOperationException("
+ "\"The query returned no rows but exactly one was expected.\");");
line(depth, "}");
line(depth, "");
EmitRowConstruction(model, depth, line, $"{model.RowTypeText} __row = ", ";");
line(depth, $"if (await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth, "{");
line(depth + 1, "throw new global::System.InvalidOperationException("
+ $"\"The query returned more than one row but {(optional ? "at most" : "exactly")} one was expected.\");");
line(depth, "}");
line(depth, "");
line(depth, "return __row;");
}

private static void EmitStreamRead(
QueryMethodModel model, int depth, Action<int, string> line, string cancellationToken)
{
line(depth, $"while (await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth, "{");
if (model.ElementKind == ResultElementKind.Scalar)
{
line(depth + 1, $"yield return {ReadScalar(model.Columns[0])};");
}
else
{
EmitRowConstruction(model, depth + 1, line, "yield return ", ";");
}

line(depth, "}");
}

private static void EmitScalarListRead(
QueryMethodModel model, int depth, Action<int, string> line, string cancellationToken)
{
line(depth, $"global::System.Collections.Generic.List<{model.RowTypeText}> __rows = new();");
line(depth, $"while (await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth, "{");
line(depth + 1, $"__rows.Add({ReadScalar(model.Columns[0])});");
line(depth, "}");
line(depth, "");
line(depth, "return __rows;");
}

private static void EmitScalarSingleRead(
QueryMethodModel model, int depth, Action<int, string> line, string cancellationToken, bool optional)
{
line(depth, $"if (!await __reader.ReadAsync({cancellationToken}).ConfigureAwait(false))");
line(depth, "{");
line(depth + 1, optional
? "return null;"
: "throw new global::System.InvalidOperationException("
+ "\"The query returned no rows but a scalar value was expected.\");");
line(depth, "}");
line(depth, "");
line(depth, $"return {ReadScalar(model.Columns[0])};");
}

private static string ReadScalar(ColumnModel column)
{
var read = $"__reader.{column.GetterInvocation}(0)";
if (column.IsNullable)
{
return $"__reader.IsDBNull(0) ? default({column.TypeText}) : {read}";
}

return "__reader.IsDBNull(0) "
+ "? throw new global::System.InvalidOperationException("
+ $"\"The scalar result is NULL but maps to non-nullable '{column.TypeText}'.\") "
+ $": {read}";
}

private static void EmitRowConstruction(
QueryMethodModel model, int depth, Action<int, string> line, string prefix, string suffix)
{
if (model.RowMapping == RowMappingKind.Properties)
{
line(depth, $"{prefix}new {model.RowTypeText}");
line(depth, "{");
for (var i = 0; i < model.Columns.Count; i++)
{
line(depth + 1, $"{model.Columns[i].Name} = {ReadColumn(model.Columns[i], i)},");
}

line(depth, "}" + suffix);
return;
}

line(depth, $"{prefix}new {model.RowTypeText}(");
for (var i = 0; i < model.Columns.Count; i++)
{
var terminator = i == model.Columns.Count - 1 ? ")" + suffix : ",";
line(depth + 1, ReadColumn(model.Columns[i], i) + terminator);
}
}

private static string ReadColumn(ColumnModel column, int index)
Expand Down
31 changes: 31 additions & 0 deletions src/SqlBound.Generators/QueryMethodModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,41 @@ internal sealed record QueryMethodModel(
string MethodName,
bool IsExtensionMethod,
string CommandText,
string ReturnTypeText,
ResultShape Shape,
ResultElementKind ElementKind,
RowMappingKind RowMapping,
string RowTypeText,
EquatableArray<ColumnModel> Columns,
EquatableArray<MethodParameterModel> Parameters);

/// <summary>The result shape a query method's return type declares, driving body emission.</summary>
internal enum ResultShape
{
RowList,
SingleRow,
OptionalRow,
Stream,
Execute,
ExecuteDiscard,
}

/// <summary>How row columns reach the row instance: through the single parameterized public
/// constructor, or through settable properties on a parameterless-constructible type.</summary>
internal enum RowMappingKind
{
Constructor,
Properties,
}

/// <summary>Whether the result element is a constructor-mapped row or a first-column scalar.
/// For scalars, <c>Columns</c> holds a single nameless entry describing the conversion.</summary>
internal enum ResultElementKind
{
Row,
Scalar,
}

/// <summary>One type in the (outermost-first) declaration chain wrapping the query method.</summary>
internal sealed record ContainingTypeModel(string Keyword, string Name);

Expand Down
Loading
Loading