Azure / Azure/data-api-builder
⭐ [Enhancement]: Stored Procedure Parameters
- Dominant language
- C#
- Stars
- 1.5k
- Forks
- 370
- Avg merge
- 3d 17h
- Merged PRs (30d)
- 8
Description
## What is it?
* Support proc/param metadata.
* Support optional parameters.
* Support output parameters.
* Support error-provider parameters.
### Output scenario
This demonstrates an output parameter.
```tsql
CREATE PROCEDURE [dbo].[GetBooks]
@count INT OUTPUT
AS
SELECT *, @count = COUNT(*) OVER()
FROM dbo.Books;
```
> Note how the result set from the `SELECT` statement is combined with the output parameter itself.
### Current Configuration
Current configuration supports parameters and their default values - only.
```json
{
"entities": [
{
"": {
...
"source": {
...
"parameters": {
"": "",
"": "",
"": ""
}
...
```
### Future Configuration
New object type provides more flexibility. This is an additive/alternative change.
```json
{
"entities": [
{
"": {
...
"metadata": {
"description": "" (optional default: null)
},
"source": {
...
"parameters": [ // current hierarchy position
{
"name": "",
"alias": "" (optional default: ),
"description": "" (optional default: null),
"data-type": "" (optional default: ),
"required": "true | false" (optional default: true),
"default": "" (optional default: null)
"kind": "input | output | input-output | error-provider", (optional default: input)
}
...
```
## Description
Metadata `Description` is used to enhance both the `OpenAPI` and `GraphQL Schema`.
> Maybe this means views and tables get description. We will have to see.
## CLI impact
Though this may change to align with ongoing work, this is pretty close.
* `dab update --metadata.description "value"`
* `dab update --parameters "" --name "value"`
* `dab update --parameters "" --alias "value"`
* `dab update --parameters "" --description "value"`
* `dab update --parameters "" --data-type "value"`
* `dab update --parameters "" --required "value"`
* `dab update --parameters "" --default "value"`
* `dab update --parameters "" --kind "value"`
How do we delete a parameter from the CLI?
* `dab update --parameters "" --delete`
## Output parameter
This supports both SELECT and OUTPUT params at the same time.
### Example (page count)
```sql
CREATE PROCEDURE [dbo].[GetBooksPaginated]
@PageSize INT,
@PageNumber INT,
@PageCount INT OUTPUT
AS
BEGIN
DECLARE @TotalCount INT;
SELECT @TotalCount = COUNT(*) FROM dbo.Books;
SET @PageCount = CEILING(@TotalCount * 1.0 / @PageSize);
SELECT * FROM dbo.Books ORDER BY Id
OFFSET (@PageNumber - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;
END;
```
### Output payload (GraphQL)
TBD
### Output payload (REST)
```json
{
"value": [
{
"Id": 1,
"Name": "William Shatner",
"BirthYear": 1931
}
],
"output-params": [
{
"Name": "PageCount",
"Value": 123
}
],
"errors": [] // see below
}
```
### Database parity
| Database | Supports Output Params |
|-----------------|-------------------------------|
| SQL Server | Yes |
| MySQL | Yes |
| PostgreSQL | Yes |
| Cosmos DB (SQL) | No (Return values through JavaScript) |
## Error-Provider
Today, stored procedures cannot raise an error in a controlled way.
### Today's error format (REST)
```json
{
"error": {
"code": "DatabaseOperationFailed",
"message": "Invalid column name 'Id'.\r\nInvalid column name 'Id'.",
"status": 500
}
}
```
### Today's error format (GQL)
```json
{
"errors": [
{
"message": "Invalid column name 'Id'.\r\nInvalid column name 'Id'.",
"extensions": {
"code": "DatabaseOperationFailed"
}
}
]
}
```
### Future error format (REST)
The original `error` remains - it will return the `First()` from the new `errors` array.
```json
{
"value": [],
"output-params": [],
"errors": [
{
"code": "DatabaseOperationFailed", // optional
"message": "Invalid column name 'Id'.\r\nInvalid column name 'Id'.", // required
"status": 500 // optional
}
]
}
```
### Future error format (GQL)
No change.
## Raising errors versus returning errors
This section shows how returning errors is better than raising them. Reasons:
1. Allows multiple errors.
2. Keeps execution flow.
3. Consistent format for APIs.
4. Customizable messages.
### Raising errors
```tsql
CREATE PROCEDURE [dbo].[GetBooks]
@count INT OUTPUT
AS
BEGIN
SELECT *, @count = COUNT(*) OVER()
FROM dbo.Books;
IF @count < 10
RAISERROR('The number of books is less than 10. Operation aborted.', 16, 1);
END;
```
### (versus) Returning errors
Effectively, an `error-provider` is an output parameter with a JSON payload of errors.
```tsql
CREATE PROCEDURE [dbo].[GetBooks]
@count INT OUTPUT,
@ErrorMessage NVARCHAR(MAX) OUTPUT
AS
BEGIN
IF @count < 10
BEGIN
-- error
DECLARE @ErrorTable TABLE (code NVARCHAR(50), message NVARCHAR(255), status INT);
INSERT INTO @ErrorTable (code, message, status)
VALUES ('DatabaseOperationFailed', 'The number of books is less than 10. Operation aborted.', 500);
SELECT @ErrorMessage = (SELECT * FROM @ErrorTable FOR JSON PATH, WITHOUT_ARRAY_WRAPPER);
RETURN; -- Abort
END
SELECT *, @count = COUNT(*) OVER()
FROM dbo.Books;
END;
```
#### Error result
```json
{
"errors": [
{
"code": "DatabaseOperationFailed",
"message": "The number of books is less than 10. Operation aborted.",
"status": 500
}
]
}
```
### Questions
1. Can errors be returned WITH data? Yes.
2. Should `status` in an error impact the returned HTTP status? Return 200 (REST & GQL)
3. Let's make sure we log the non-generic error and return the generic error.
4. Custom errors are not censored.
## Related Issues to Close
* #1841
* #1826
* #1843
* #2346
* #2222
* #1748
* #1844
Contributor guide
Assessment
This issue has not been assessed yet.