diff --git a/src/Config/DatabasePrimitives/DatabaseObject.cs b/src/Config/DatabasePrimitives/DatabaseObject.cs
index 6e0db9d8b8..b33b314fc8 100644
--- a/src/Config/DatabasePrimitives/DatabaseObject.cs
+++ b/src/Config/DatabasePrimitives/DatabaseObject.cs
@@ -64,6 +64,7 @@ public SourceDefinition SourceDefinition
EntitySourceType.Table => ((DatabaseTable)this).TableDefinition,
EntitySourceType.View => ((DatabaseView)this).ViewDefinition,
EntitySourceType.StoredProcedure => ((DatabaseStoredProcedure)this).StoredProcedureDefinition,
+ EntitySourceType.Function => ((DatabaseStoredProcedure)this).StoredProcedureDefinition,
_ => throw new Exception(
message: $"Unsupported EntitySourceType. It can either be Table,View, or Stored Procedure.")
};
diff --git a/src/Config/ObjectModel/EntitySourceType.cs b/src/Config/ObjectModel/EntitySourceType.cs
index 863751da5c..2554068d2b 100644
--- a/src/Config/ObjectModel/EntitySourceType.cs
+++ b/src/Config/ObjectModel/EntitySourceType.cs
@@ -12,5 +12,6 @@ public enum EntitySourceType
{
Table,
View,
- [EnumMember(Value = "stored-procedure")] StoredProcedure
+ [EnumMember(Value = "stored-procedure")] StoredProcedure,
+ Function
}
diff --git a/src/Core/Resolvers/BaseQueryStructure.cs b/src/Core/Resolvers/BaseQueryStructure.cs
index cfe4a050ec..798713e1d1 100644
--- a/src/Core/Resolvers/BaseQueryStructure.cs
+++ b/src/Core/Resolvers/BaseQueryStructure.cs
@@ -120,7 +120,7 @@ public virtual string MakeDbConnectionParam(object? value, string? paramName = n
string encodedParamName = GetEncodedParamName(Counter.Next());
if (!string.IsNullOrEmpty(paramName))
{
- Parameters.Add(encodedParamName,
+ Parameters.Add(paramName,
new(value,
dbType: GetUnderlyingSourceDefinition().GetDbTypeForParam(paramName),
sqlDbType: GetUnderlyingSourceDefinition().GetSqlDbTypeForParam(paramName)));
@@ -130,7 +130,9 @@ public virtual string MakeDbConnectionParam(object? value, string? paramName = n
Parameters.Add(encodedParamName, new(value));
}
- return encodedParamName;
+ return paramName == null ? encodedParamName : $"{PARAM_NAME_PREFIX}{paramName}";
+
+
}
///
diff --git a/src/Core/Resolvers/PostgresQueryBuilder.cs b/src/Core/Resolvers/PostgresQueryBuilder.cs
index c779252cfe..6734381b28 100644
--- a/src/Core/Resolvers/PostgresQueryBuilder.cs
+++ b/src/Core/Resolvers/PostgresQueryBuilder.cs
@@ -101,7 +101,59 @@ public string Build(SqlDeleteStructure structure)
///
public string Build(SqlExecuteStructure structure)
{
- throw new NotImplementedException();
+ return $"SELECT * FROM {QuoteIdentifier(structure.DatabaseObject.SchemaName)}.{QuoteIdentifier(structure.DatabaseObject.Name)}({BuildProcedureParameterList(structure.ProcedureParameters)})";
+ }
+ private string BuildProcedureParameterList(Dictionary parameters)
+ {
+ if (parameters == null || parameters.Count == 0)
+ {
+ return "";
+ }
+
+ List parameterList = new();
+ foreach (KeyValuePair param in parameters)
+ {
+ parameterList.Add($"{QuoteIdentifier(param.Key)} := {FormatParameterValue(param.Value)}");
+ }
+
+ return string.Join(", ", parameterList);
+ }
+
+#pragma warning disable CA1822 // Mark members as static
+ private string? FormatParameterValue(object value)
+#pragma warning restore CA1822 // Mark members as static
+ {
+ if (value == null)
+ {
+ return "NULL";
+ }
+
+ if (value is string || value is char)
+ {
+ if (value == null)
+ {
+ value = string.Empty;
+ }
+ // Handle string values, escaping single quotes
+#pragma warning disable CS8602 // Dereference of a possibly null reference.
+ return $"{value.ToString().Replace("'", "''")}";
+#pragma warning restore CS8602 // Dereference of a possibly null reference.
+ }
+
+ if (value is bool)
+ {
+ // Handle boolean values
+ return (bool)value ? "TRUE" : "FALSE";
+ }
+
+ if (value is DateTime)
+ {
+ // Handle DateTime values
+ return $"'{((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss")}'";
+ }
+
+ // Handle numeric and other types
+ return value == null ? string.Empty : value.ToString();
}
public string Build(SqlUpsertQueryStructure structure)
@@ -218,13 +270,36 @@ private string MakeSelectColumns(SqlQueryStructure structure)
return string.Join(", ", builtColumns);
}
-
- ///
- public string BuildStoredProcedureResultDetailsQuery(string databaseObjectName)
+ public string BuildStoredProcedureResultDetailsQuery(string procedureName)
{
- throw new NotImplementedException();
+ // This query retrieves the details of the result set for a given stored procedure
+
+ string query = $@"
+ SELECT
+ p.parameter_name AS name,
+ p.data_type AS system_type_name,
+ CASE
+ WHEN p.parameter_mode = 'IN' THEN FALSE
+ ELSE TRUE
+ END AS is_nullable
+ FROM
+ information_schema.parameters p
+ JOIN
+ information_schema.routines r
+ ON p.specific_name = r.specific_name
+ WHERE
+ r.routine_schema = 'public'
+ and p.parameter_mode = 'OUT'
+ AND r.routine_name = '{procedureName}'
+ ORDER BY
+ p.ordinal_position";
+
+
+
+ return query;
}
+
///
public string BuildQueryToGetReadOnlyColumns(string schemaParamName, string tableParamName)
{
diff --git a/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs
index 99336180d5..0c3bb032d3 100644
--- a/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/MySqlMetadataProvider.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Collections;
using System.Data;
using Azure.DataApiBuilder.Config.DatabasePrimitives;
using Azure.DataApiBuilder.Core.Configurations;
@@ -128,12 +129,124 @@ protected override DatabaseTable GenerateDbTable(string schemaName, string table
}
///
- /// Takes a string version of a MySql data type and returns its .NET common language runtime (CLR) counterpart
- /// TODO: For MySql stored procedure support, this needs to be implemented.
+ /// Takes a string version of a PostgreSql data type and returns its .NET common language runtime (CLR) counterpart
+ /// TODO: For PostgreSql stored procedure support, this needs to be implemented.
///
public override Type SqlToCLRType(string sqlType)
{
- throw new NotImplementedException();
+ switch (sqlType.ToLower())
+ {
+ case "boolean":
+ case "bool":
+ return typeof(bool);
+
+ case "smallint":
+ return typeof(short);
+
+ case "integer":
+ case "int":
+ return typeof(int);
+
+ case "bigint":
+ return typeof(long);
+
+ case "real":
+ return typeof(float);
+
+ case "double precision":
+ return typeof(double);
+
+ case "numeric":
+ case "decimal":
+ return typeof(decimal);
+
+ case "money":
+ return typeof(decimal);
+
+ case "character varying":
+ case "varchar":
+ case "character":
+ case "char":
+ case "text":
+ return typeof(string);
+
+ case "bytea":
+ return typeof(byte[]);
+
+ case "date":
+ return typeof(DateTime);
+
+ case "timestamp":
+ case "timestamp without time zone":
+ return typeof(DateTime);
+
+ case "timestamp with time zone":
+ return typeof(DateTimeOffset);
+
+ case "time":
+ case "time without time zone":
+ return typeof(TimeSpan);
+
+ case "time with time zone":
+ return typeof(DateTimeOffset);
+
+ case "interval":
+ return typeof(TimeSpan);
+
+ case "uuid":
+ return typeof(Guid);
+
+ case "json":
+ case "jsonb":
+ return typeof(string);
+
+ case "xml":
+ return typeof(string);
+
+ case "inet":
+ return typeof(System.Net.IPAddress);
+
+ case "cidr":
+ return typeof(ValueTuple);
+
+ case "macaddr":
+ return typeof(System.Net.NetworkInformation.PhysicalAddress);
+
+ case "bit":
+ case "bit varying":
+ return typeof(BitArray);
+
+ case "point":
+ return typeof((double, double));
+
+ case "line":
+ return typeof(string); // Implement a custom type if needed
+
+ case "lseg":
+ return typeof((double, double)[]);
+
+ case "box":
+ return typeof((double, double)[]);
+
+ case "path":
+ return typeof((double, double)[]);
+
+ case "polygon":
+ return typeof((double, double)[]);
+
+ case "circle":
+ return typeof((double, double, double));
+
+ case "tsvector":
+ return typeof(string); // Implement a custom type if needed
+
+ case "tsquery":
+ return typeof(string); // Implement a custom type if needed
+
+ default:
+ throw new NotSupportedException($"The SQL type '{sqlType}' is not supported.");
+ }
}
+
}
}
diff --git a/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs
index ecd65b3d95..35cf0f7a84 100644
--- a/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/PostgreSqlMetadataProvider.cs
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System.Collections;
using System.Net;
+using Azure.DataApiBuilder.Config.DatabasePrimitives;
+using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Resolvers.Factories;
using Azure.DataApiBuilder.Service.Exceptions;
@@ -65,6 +68,16 @@ public override string GetDefaultSchemaName()
{
return "public";
}
+ ///
+ protected override Task FillSchemaForStoredProcedureAsync(
+ Entity procedureEntity,
+ string entityName,
+ string schemaName,
+ string storedProcedureSourceName,
+ StoredProcedureDefinition storedProcedureDefinition)
+ {
+ return Task.CompletedTask;
+ }
///
/// Takes a string version of a PostgreSql data type and returns its .NET common language runtime (CLR) counterpart
@@ -72,7 +85,119 @@ public override string GetDefaultSchemaName()
///
public override Type SqlToCLRType(string sqlType)
{
- throw new NotImplementedException();
+ switch (sqlType.ToLower())
+ {
+ case "boolean":
+ case "bool":
+ return typeof(bool);
+
+ case "smallint":
+ return typeof(short);
+
+ case "integer":
+ case "int":
+ return typeof(int);
+
+ case "bigint":
+ return typeof(long);
+
+ case "real":
+ return typeof(float);
+
+ case "double precision":
+ return typeof(double);
+
+ case "numeric":
+ case "decimal":
+ return typeof(decimal);
+
+ case "money":
+ return typeof(decimal);
+
+ case "character varying":
+ case "varchar":
+ case "character":
+ case "char":
+ case "text":
+ return typeof(string);
+
+ case "bytea":
+ return typeof(byte[]);
+
+ case "date":
+ return typeof(DateTime);
+
+ case "timestamp":
+ case "timestamp without time zone":
+ return typeof(DateTime);
+
+ case "timestamp with time zone":
+ return typeof(DateTimeOffset);
+
+ case "time":
+ case "time without time zone":
+ return typeof(TimeSpan);
+
+ case "time with time zone":
+ return typeof(DateTimeOffset);
+
+ case "interval":
+ return typeof(TimeSpan);
+
+ case "uuid":
+ return typeof(Guid);
+
+ case "json":
+ case "jsonb":
+ return typeof(string);
+
+ case "xml":
+ return typeof(string);
+
+ case "inet":
+ return typeof(System.Net.IPAddress);
+
+ case "cidr":
+ return typeof(ValueTuple);
+
+ case "macaddr":
+ return typeof(System.Net.NetworkInformation.PhysicalAddress);
+
+ case "bit":
+ case "bit varying":
+ return typeof(BitArray);
+
+ case "point":
+ return typeof((double, double));
+
+ case "line":
+ return typeof(string); // Implement a custom type if needed
+
+ case "lseg":
+ return typeof((double, double)[]);
+
+ case "box":
+ return typeof((double, double)[]);
+
+ case "path":
+ return typeof((double, double)[]);
+
+ case "polygon":
+ return typeof((double, double)[]);
+
+ case "circle":
+ return typeof((double, double, double));
+
+ case "tsvector":
+ return typeof(string); // Implement a custom type if needed
+
+ case "tsquery":
+ return typeof(string); // Implement a custom type if needed
+
+ default:
+ throw new NotSupportedException($"The SQL type '{sqlType}' is not supported.");
+ }
}
+
}
}
diff --git a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
index 839ac05785..73fd04582c 100644
--- a/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
+++ b/src/Core/Services/MetadataProviders/SqlMetadataProvider.cs
@@ -19,6 +19,7 @@
using Azure.DataApiBuilder.Service.Exceptions;
using HotChocolate.Language;
using Microsoft.Extensions.Logging;
+using Npgsql;
using static Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLNaming;
[assembly: InternalsVisibleTo("Azure.DataApiBuilder.Service.Tests")]
@@ -478,6 +479,102 @@ protected virtual async Task FillSchemaForStoredProcedureAsync(
GraphQLStoredProcedureExposedNameToEntityNameMap.TryAdd(GenerateStoredProcedureGraphQLFieldName(entityName, procedureEntity), entityName);
}
+ protected virtual async Task FillSchemaForFunctionAsync(
+ Entity procedureEntity,
+ string entityName,
+ string schemaName,
+ string storedProcedureSourceName,
+ StoredProcedureDefinition storedProcedureDefinition)
+ {
+ using NpgsqlConnection conn = new(ConnectionString);
+ DataTable procedureMetadata;
+ DataTable parameterMetadata;
+
+ try
+ {
+ await QueryExecutor.SetManagedIdentityAccessTokenIfAnyAsync(conn, _dataSourceName);
+ await conn.OpenAsync();
+
+ // Retrieve procedure metadata from PostgreSQL information schema
+ string procedureMetadataQuery = @"
+ SELECT routine_name
+ FROM information_schema.routines
+ WHERE routine_schema = @schemaName AND routine_name = @procedureName
+ ";
+
+ using (NpgsqlCommand procedureCommand = new(procedureMetadataQuery, conn))
+ {
+ procedureCommand.Parameters.AddWithValue("@schemaName", schemaName);
+ procedureCommand.Parameters.AddWithValue("@procedureName", storedProcedureSourceName);
+
+ using (NpgsqlDataReader reader = await procedureCommand.ExecuteReaderAsync())
+ {
+ procedureMetadata = new DataTable();
+ procedureMetadata.Load(reader);
+ }
+ }
+
+ // Throw exception if procedure does not exist
+ if (procedureMetadata.Rows.Count == 0)
+ {
+ throw new DataApiBuilderException(
+ message: $"No stored procedure definition found for the given database object {storedProcedureSourceName}",
+ statusCode: HttpStatusCode.ServiceUnavailable,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
+ }
+
+ // Retrieve parameter metadata from PostgreSQL information schema
+ string parameterMetadataQuery = @"
+ SELECT
+ p.parameter_name,
+ p.data_type
+ FROM
+ information_schema.parameters p
+ JOIN
+ information_schema.routines r ON p.specific_name = r.specific_name
+ WHERE
+ p.parameter_mode ='IN' AND
+ r.routine_schema = @schemaName AND r.routine_name = @functionName";
+
+ using (NpgsqlCommand parameterCommand = new(parameterMetadataQuery, conn))
+ {
+ parameterCommand.Parameters.AddWithValue("@schemaName", schemaName);
+ parameterCommand.Parameters.AddWithValue("@functionName", storedProcedureSourceName);
+
+ using (NpgsqlDataReader reader = await parameterCommand.ExecuteReaderAsync())
+ {
+ parameterMetadata = new DataTable();
+ parameterMetadata.Load(reader);
+ }
+ }
+
+ // For each row/parameter, add an entry to StoredProcedureDefinition.Parameters dictionary
+ foreach (DataRow row in parameterMetadata.Rows)
+ {
+ string sqlType = (string)row["data_type"];
+ Type systemType = SqlToCLRType(sqlType);
+ ParameterDefinition paramDefinition = new()
+ {
+ SystemType = systemType,
+ DbType = TypeHelper.GetDbTypeFromSystemType(systemType)
+ };
+
+ storedProcedureDefinition.Parameters.TryAdd((string)row["parameter_name"], paramDefinition);
+ }
+ }
+ catch (Exception ex)
+ {
+ string message = $"Cannot obtain Schema for entity {entityName} " +
+ $"with underlying database object source: {schemaName}.{storedProcedureSourceName} " +
+ $"due to: {ex.Message}";
+
+ throw new DataApiBuilderException(
+ message: message,
+ innerException: ex,
+ statusCode: HttpStatusCode.ServiceUnavailable,
+ subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
+ }
+ }
///
/// Takes a string version of a sql data type and returns its .NET common language runtime (CLR) counterpart
///
@@ -679,6 +776,14 @@ protected void PopulateDatabaseObjectForEntity(
StoredProcedureDefinition = new()
};
}
+ else if (sourceType is EntitySourceType.Function)
+ {
+ sourceObject = new DatabaseStoredProcedure(schemaName, dbObjectName)
+ {
+ SourceType = sourceType,
+ StoredProcedureDefinition = new()
+ };
+ }
else if (sourceType is EntitySourceType.Table)
{
sourceObject = new DatabaseTable()
@@ -1076,7 +1181,24 @@ private async Task PopulateObjectDefinitionForEntity(string entityName, Entity e
EntitySourceType entitySourceType = GetEntitySourceType(entityName, entity);
if (entitySourceType is EntitySourceType.StoredProcedure)
{
- await FillSchemaForStoredProcedureAsync(
+ await FillSchemaForFunctionAsync(
+ entity,
+ entityName,
+ GetSchemaName(entityName),
+ GetDatabaseObjectName(entityName),
+ GetStoredProcedureDefinition(entityName));
+
+ if (GetDatabaseType() == DatabaseType.MSSQL || GetDatabaseType() == DatabaseType.DWSQL || GetDatabaseType() == DatabaseType.PostgreSQL)
+ {
+ await PopulateResultSetDefinitionsForStoredProcedureAsync(
+ GetSchemaName(entityName),
+ GetDatabaseObjectName(entityName),
+ GetStoredProcedureDefinition(entityName));
+ }
+ }
+ else if (entitySourceType is EntitySourceType.Function)
+ {
+ await FillSchemaForFunctionAsync(
entity,
entityName,
GetSchemaName(entityName),
@@ -1131,7 +1253,7 @@ private async Task PopulateResultSetDefinitionsForStoredProcedureAsync(
// Generate query to get result set details
// of the stored procedure.
string queryForResultSetDetails = SqlQueryBuilder.BuildStoredProcedureResultDetailsQuery(
- dbStoredProcedureName);
+ storedProcedureName);
// Execute the query to get columns' details.
JsonArray? resultArray = await QueryExecutor.ExecuteQueryAsync(