Skip to content
Open
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
10 changes: 7 additions & 3 deletions src/Service.GraphQLBuilder/GraphQLStoredProcedureBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,17 @@ public static FieldDefinitionNode GenerateStoredProcedureSchema(
parameterTypeNode = new NonNullTypeNode((INullableTypeNode)parameterTypeNode);
}

string parameterDescription = !string.IsNullOrWhiteSpace(paramMetadata?.Description)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This direct config lookup creates first-wins behavior for descriptions because FirstOrDefault() selects the first matching parameter. Metadata hydration has different behavior: it iterates the complete parameter list and repeatedly assigns to the same ParameterDefinition, so the last duplicate wins.

The parameter-array schema currently does not enforce uniqueness by name. For a config containing two title entries with different descriptions, GraphQL would now expose the first description while OpenAPI/MCP can expose the last merged description.

Could we either reject duplicate parameter names during semantic config validation or use the normalized ParameterDefinition consistently here? Rejecting duplicates would be preferable because it also removes ambiguity for required and default.

? paramMetadata.Description
: !string.IsNullOrWhiteSpace(definition.Description)
? definition.Description
: $"parameters for {name.Value} stored-procedure";

inputValues.Add(
new(
location: null,
name: new(param),
description: definition.Description != null
? new StringValueNode(definition.Description)
: new StringValueNode($"parameters for {name.Value} stored-procedure"),
description: new StringValueNode(parameterDescription),
type: parameterTypeNode,
defaultValue: defaultValueNode,
directives: new List<DirectiveNode>())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,104 @@ public void StoredProcedure_RequiredWithDefault_KeepsDefaultValue()
Assert.AreEqual("Demo Title", ((StringValueNode)arg.DefaultValue!).Value);
}

[TestMethod]
public void StoredProcedure_ParameterDescription_UsesConfigDescription()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't exercise the production path that appears responsible for the underlying issue. During normal initialization, both SQL metadata-provider implementations copy ParameterMetadata.Description onto ParameterDefinition.Description before GraphQL schema generation. Therefore, the old builder should also return the config description after normal metadata initialization.

Here the test bypasses that merge and manually creates a state where paramMetadata.Description == configDescription while definition.Description == dbDescription. That proves the new builder-level precedence, but it does not reproduce the reported production bug.

Could we add a regression test that starts from runtime config, initializes the metadata provider, builds the GraphQL schema, and asserts the argument description through introspection? Ideally, that test should fail on the PR's base commit. If it already passes on the base, we should identify and cover the specific path.

{
const string parameterName = "title";
const string configDescription = "Title from runtime config";
const string dbDescription = "Title from database metadata";

DatabaseObject spDbObj = new DatabaseStoredProcedure(schemaName: "dbo", tableName: "spParamDesc")
{
SourceType = EntitySourceType.StoredProcedure,
StoredProcedureDefinition = new()
{
Parameters = new()
{
{ parameterName, new() { SystemType = typeof(string), Description = dbDescription } }
}
}
};
spDbObj.SourceDefinition.Columns.TryAdd("col1", new() { SystemType = typeof(string) });

List<ParameterMetadata> configParameters = new()
{
new ParameterMetadata
{
Name = parameterName,
Description = configDescription
}
};

FieldDefinitionNode field = BuildSchemaAndGetExecuteField(
spDbObj: spDbObj,
configParameters: configParameters,
graphQLTypeName: "SpParamDescType",
entityName: "SpParamDesc");

InputValueDefinitionNode arg = field.Arguments.First(a => a.Name.Value == parameterName);
Assert.IsNotNull(arg.Description);
Assert.AreEqual(configDescription, arg.Description!.Value);
}

[TestMethod]
public void StoredProcedure_ParameterDescription_FallsBackToDatabaseDescription()
{
const string parameterName = "title";
const string dbDescription = "Title from database metadata";

DatabaseObject spDbObj = new DatabaseStoredProcedure(schemaName: "dbo", tableName: "spParamDescFallback")
{
SourceType = EntitySourceType.StoredProcedure,
StoredProcedureDefinition = new()
{
Parameters = new()
{
{ parameterName, new() { SystemType = typeof(string), Description = dbDescription } }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we really get description of a parameter from database metadata?

the definition here maybe showing the property Description, but are you sure it can get populated by querying the database? Did you test that?

}
}
};
spDbObj.SourceDefinition.Columns.TryAdd("col1", new() { SystemType = typeof(string) });

FieldDefinitionNode field = BuildSchemaAndGetExecuteField(
spDbObj: spDbObj,
configParameters: new List<ParameterMetadata>(),
graphQLTypeName: "SpParamDescFallbackType",
entityName: "SpParamDescFallback");

InputValueDefinitionNode arg = field.Arguments.First(a => a.Name.Value == parameterName);
Assert.IsNotNull(arg.Description);
Assert.AreEqual(dbDescription, arg.Description!.Value);
}

Comment thread
Copilot marked this conversation as resolved.
[TestMethod]
public void StoredProcedure_ParameterDescription_FallsBackToDefaultText()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
Could we add cases where the config description is "" or " " and the definition description is populated, plus a case where both values are empty/whitespace? That would verify both whitespace fallback branches and protect the behavior that this PR is introducing.

{
const string parameterName = "title";
const string graphQLTypeName = "SpParamDescDefaultTextType";
const string entityName = "SpParamDescDefaultText";

DatabaseObject spDbObj = new DatabaseStoredProcedure(schemaName: "dbo", tableName: "spParamDescDefaultText")
{
SourceType = EntitySourceType.StoredProcedure,
StoredProcedureDefinition = new()
{
Parameters = new() { { parameterName, new() { SystemType = typeof(string) } } }
}
};
spDbObj.SourceDefinition.Columns.TryAdd("col1", new() { SystemType = typeof(string) });

FieldDefinitionNode field = BuildSchemaAndGetExecuteField(
spDbObj: spDbObj,
configParameters: new List<ParameterMetadata>(),
graphQLTypeName: graphQLTypeName,
entityName: entityName);

InputValueDefinitionNode arg = field.Arguments.First(a => a.Name.Value == parameterName);
Assert.IsNotNull(arg.Description);
Assert.AreEqual($"parameters for {graphQLTypeName} stored-procedure", arg.Description!.Value);
}

/// <summary>
/// Helper that builds a query schema for a stored-procedure entity and returns
/// the generated execute* field so individual tests can assert on its argument
Expand Down
Loading