PostgreSQL · Execute

Handle Execute errors

The errors an Execute call can raise, turned into messages, in two scenarios: what the database refuses (a routine that doesn't exist with these argument types, a function called as a procedure, a timeout) and what ARX refuses before it (a positional argument after a named one, an OUT parameter without a type, paging with nothing to page).

Set up for this page

The scenarios build on the sample application: SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleCatalog.cs (the tables' ids, keys, lookups and display names).

1 What the database refuses

Mapped from PostgreSQL's errors.

Already in scope: service (ArxPgDbService)

C#
await TryAsync("a procedure that doesn't exist", () => service.ExecuteAsync(new ArxProcedureExpression("sp_no_such_procedure")));

var wrongTypes = new ArxProcedureExpression("sp_rename_account");
wrongTypes.AddParameter("p_account_id", Guid.NewGuid());
wrongTypes.AddParameter("p_name", "x");
await TryAsync("the wrong argument types", () => service.ExecuteAsync(wrongTypes));

var function = new ArxProcedureExpression("fn_contact_count");
function.AddParameter(1L);
await TryAsync("a function called as a procedure", () => service.ExecuteAsync(function));

var slow = new ArxProcedureExpression("sp_slow");
slow.AddParameter("p_seconds", 3.0);
await TryAsync("a timeout of 1 second", () => service.ExecuteAsync(slow, new ArxExecuteOptions { CommandTimeout = 1 }));
Used above

Defined once in this sample.

private static async Task TryAsync(string label, Func<Task> call)
{
    try
    {
        await call();
        Console.WriteLine($"{label}: no error");
    }
    catch (ArxException error)
    {
        Console.WriteLine($"{label}: {error.Code} ({(int)error.Code}), {error.Category}");
    }
}

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT proname, prokind
FROM pg_proc
WHERE proname IN ('sp_no_such_procedure', 'sp_rename_account', 'fn_contact_count', 'sp_slow')
ORDER BY proname;

Expect: 3 rows: fn_contact_count f, sp_rename_account p, sp_slow p (no sp_no_such_procedure).

2 What ARX refuses first

Refused before any connection opens.

Already in scope: service (ArxPgDbService)

C#
var mixed = new ArxProcedureExpression("sp_rename_account");
mixed.AddParameter("p_account_id", 1L);
mixed.AddParameter("positional after a named one");
await TryAsync("a positional argument after a named one", () => service.ExecuteAsync(mixed));

var untyped = new ArxProcedureExpression("sp_account_totals");
untyped.Parameters.Add(new ArxProcedureParameter("p_orders", null, null, ArxProcedureParameterDirection.Out));
await TryAsync("an OUT parameter without a type", () => service.ExecuteAsync(untyped));

var nothingToPage = new ArxProcedureExpression("sp_rename_account") { PageInfo = new ArxPagingInfo { Count = 10 } };
await TryAsync("paging a procedure that returns no table", () => service.ExecuteAsync(nothingToPage));
Used above

Defined once in this sample.

private static async Task TryAsync(string label, Func<Task> call)
{
    try
    {
        await call();
        Console.WriteLine($"{label}: no error");
    }
    catch (ArxException error)
    {
        Console.WriteLine($"{label}: {error.Code} ({(int)error.Code}), {error.Category}");
    }
}

Check it in the database

SQL

Run it in any SQL tool connected to the database, after the code has run.

SELECT count(*)
FROM account
WHERE id = 1 AND name = 'positional after a named one';

Expect: 0: nothing reached the database.

Run the complete sample

The code above comes from one program, built in a fresh project and run against the database. To run it yourself, here is every file and step.

The program, its files and how to run it

Run it locally

  1. .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target net10.0).
  2. Create the project:
    Terminal
    dotnet new console -n ArxSample -f net10.0
    cd ArxSample
  3. Install the packages:
    Terminal
    dotnet add package ARX.SDK.PostgreSql --version 1.0.0-rc.1
  4. Database: a PostgreSQL 16 (tested with 16.15) database with the sample schema.
  5. Configuration: set ARX_SAMPLES_POSTGRESQL to its connection string. Nothing is hard-coded.
  6. Source code: add the files of the sample application this sample uses (SampleContext.cs, SampleOutput.cs, SampleContext.Application.cs, SampleCatalog.cs), then replace Program.cs and add the other file below, with the file names shown.
  7. Run it:
    Terminal
    dotnet run
  8. Expected output: below.

The files

Program.cs

Runs the sample. Generated for this page; it reads the connection string from the environment.

using ArxSamples.PostgreSql;
using ArxSamples.PostgreSql.Operations.Execute.RealCases;

// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_POSTGRESQL")
    ?? throw new InvalidOperationException("Set the ARX_SAMPLES_POSTGRESQL environment variable to your PostgreSQL connection string.");

await new RealCase7_HandleErrors().RunAsync(new SampleContext(connectionString));
RealCase7_HandleErrors.cs

The sample, as it is in the repository.

using ARX.SDK;
using ARX.SDK.Query;
using ARX.SDK.Services;
using static ArxSamples.PostgreSql.SampleOutput;

namespace ArxSamples.PostgreSql.Operations.Execute.RealCases;

/// <summary>
/// The errors an Execute call can raise, turned into messages, in two scenarios: what the database refuses (a routine
/// that doesn't exist with these argument types, a function called as a procedure, a timeout) and what ARX refuses
/// before it (a positional argument after a named one, an OUT parameter without a type, paging with nothing to page).
/// </summary>
public sealed class RealCase7_HandleErrors : ISample
{
    public string Name => "execute-real-7-handle-errors";

    public string Title => "Handle Execute errors";

    public async Task RunAsync(SampleContext dbContext)
    {
        await Scenario1_WhatTheDatabaseRefusesAsync(dbContext);

        await Scenario2_WhatARXRefusesFirstAsync(dbContext);
    }

    private static async Task TryAsync(string label, Func<Task> call)
    {
        try
        {
            await call();
            Show($"{label}: no error");
        }
        catch (ArxException error)
        {
            Show($"{label}: {error.Code} ({(int)error.Code}), {error.Category}");
        }
    }

    /// <summary>1. Mapped from PostgreSQL's errors.</summary>
    private static async Task Scenario1_WhatTheDatabaseRefusesAsync(SampleContext dbContext)
    {
        Step("1. What the database refuses");

        await TryAsync("a procedure that doesn't exist", () => dbContext.Service.ExecuteAsync(new ArxProcedureExpression("sp_no_such_procedure")));

        var wrongTypes = new ArxProcedureExpression("sp_rename_account");
        wrongTypes.AddParameter("p_account_id", Guid.NewGuid());
        wrongTypes.AddParameter("p_name", "x");
        await TryAsync("the wrong argument types", () => dbContext.Service.ExecuteAsync(wrongTypes));

        var function = new ArxProcedureExpression("fn_contact_count");
        function.AddParameter(1L);
        await TryAsync("a function called as a procedure", () => dbContext.Service.ExecuteAsync(function));

        var slow = new ArxProcedureExpression("sp_slow");
        slow.AddParameter("p_seconds", 3.0);
        await TryAsync("a timeout of 1 second", () => dbContext.Service.ExecuteAsync(slow, new ArxExecuteOptions { CommandTimeout = 1 }));

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT proname, prokind
         * FROM pg_proc
         * WHERE proname IN ('sp_no_such_procedure', 'sp_rename_account', 'fn_contact_count', 'sp_slow')
         * ORDER BY proname;
         * -- Expect: 3 rows: fn_contact_count f, sp_rename_account p, sp_slow p (no sp_no_such_procedure).
         * ===========================================================
         */
    }

    /// <summary>2. Refused before any connection opens.</summary>
    private static async Task Scenario2_WhatARXRefusesFirstAsync(SampleContext dbContext)
    {
        Step("2. What ARX refuses first");

        var mixed = new ArxProcedureExpression("sp_rename_account");
        mixed.AddParameter("p_account_id", 1L);
        mixed.AddParameter("positional after a named one");
        await TryAsync("a positional argument after a named one", () => dbContext.Service.ExecuteAsync(mixed));

        var untyped = new ArxProcedureExpression("sp_account_totals");
        untyped.Parameters.Add(new ArxProcedureParameter("p_orders", null, null, ArxProcedureParameterDirection.Out));
        await TryAsync("an OUT parameter without a type", () => dbContext.Service.ExecuteAsync(untyped));

        var nothingToPage = new ArxProcedureExpression("sp_rename_account") { PageInfo = new ArxPagingInfo { Count = 10 } };
        await TryAsync("paging a procedure that returns no table", () => dbContext.Service.ExecuteAsync(nothingToPage));

        /* ===========================================================
         * [DIRECT QUERY CHECK]
         * -----------------------------------------------------------
         * SELECT count(*)
         * FROM account
         * WHERE id = 1 AND name = 'positional after a named one';
         * -- Expect: 0: nothing reached the database.
         * ===========================================================
         */
    }
}

Expected output

What dotnet run printed when the sample was verified, on a freshly created sample schema. Ids differ when the tables already hold rows.

Output
  > 1. What the database refuses
      a procedure that doesn't exist: UndefinedEntity (5001), Schema
      the wrong argument types: UndefinedEntity (5001), Schema
      a function called as a procedure: NotSupported (8001), NotSupported
      a timeout of 1 second: Timeout (7003), Transient
  > 2. What ARX refuses first
      a positional argument after a named one: InvalidArgument (1001), Validation
      an OUT parameter without a type: InvalidArgument (1001), Validation
      paging a procedure that returns no table: InvalidArgument (1001), Validation