PostgreSQL

Execute

17 samples, from the least code that works to complete workflows.

What an Execute does

The call new ArxProcedureExpression("sp_place_order"), in the pattern of ArxQueryExpression: AddParameter, AddOutParameter<T>, AddInOutParameter<T>, AddCursorParameter, AddVariadicParameter; ProcedureType = ArxProcedureType.Function for a function. Never SQL text
Procedure or function The caller says which: a procedure runs as CALL, a function as SELECT * FROM. The wrong one is ARX8001, naming the right ProcedureType
Arguments Named or positional (positional first), each a typed parameter through the same value path as Create (dates, JSON, arrays, selection sets, lookups). A null for an overloaded routine needs its type: AddParameter(name, type, null)
What comes back ArxExecuteResult: Outputs (OUT and INOUT values, as their declared types), ResultSets (a procedure's tables by their cursor parameter's name, or a function's rows as ResultSets[0]), Messages (notices and warnings, in order). Value<T>() reads a function's single value
Tables from a procedure Each is an INOUT refcursor parameter, declared with AddCursorParameter. ARX reads every declared cursor in one transaction (two round trips)
Paging PageInfo, as on a query, three ways: the routine pages itself (PagingParameters: ARX fills its page arguments and reads its total), ARX pages a function's rows (LIMIT/OFFSET, the total in one round trip), ARX pages a procedure's table (MOVE, FETCH, MOVE ALL). The total is opt-in
Transactions ARX's own call runs outside any transaction block, so a procedure may commit; in an ArxTransaction or a batch scope a procedure that commits is ARX8001
Errors the routine raises RAISE EXCEPTION, ASSERT and a routine's own codes (USING ERRCODE = 'AH001') are ARX3005 RaisedByDatabase, with the routine's message, SqlState and Hint (Detail too when the connection string has Include Error Detail=true: Npgsql redacts it otherwise)
Security A routine or parameter name carrying SQL is ARX1017, refused before the database; values are parameters
Names in the samples Procedures sp_, functions fn_ (views vw_, materialized views mv_)
Without a catalog Needs no catalog and reads nothing first: a service created with only the connection string calls procedures and functions as any other

Reference

Error codes an Execute can raise

Code When
ARX1001 InvalidArgument A positional argument after a named one; OUT, INOUT or cursor parameters on a function; an OUT without a type; ReturnsCursors on a procedure; AddOrder on a procedure; PageInfo with nothing to page or below 1; PagingParameters without PageInfo; PagedResultSet naming no cursor
ARX1002, ARX1003, ARX1004 No routine name; an invalid name (three parts, a typo); a parameter given twice
ARX1007, ARX1013 A value type ARX can't write; default(DateTime)
ARX1017 SuspectedInjection A routine, parameter, cursor or paging name carrying SQL
ARX2003 MultipleRecordsMatched Value<T>() on several rows
ARX3001–3004 A constraint the routine broke (kept from the database)
ARX3005 RaisedByDatabase The routine refused: RAISE EXCEPTION (P0001), ASSERT (P0004), a routine's own code
ARX4004 DataTypeMismatch No routine with this name and these argument types (42883: PostgreSQL reports a missing routine the same way); an ambiguous overload (42725)
ARX5003 SchemaMismatch An OUT value of another type than declared, that can't be widened
ARX6001 PermissionDenied No EXECUTE privilege
ARX7003 Timeout CommandTimeout exceeded
ARX8001 NotSupported A function called as a procedure or the reverse (42809); a procedure that commits inside an ARX transaction or a scope (2D000); a type Npgsql can't read in an OUT value or a cursor row

The samples

Basics

The least code that works.

1Call a stored procedureProgressive

Calling a stored procedure, in three scenarios: named arguments; positional arguments; a parameter left out, so the procedure's default is used. The call is described with ArxProcedureExpression, the way a query is described with ArxQueryExpression; ARX sends CALL with every value as a parameter.

2Read OUT and INOUT valuesProgressive

OUT and INOUT values, in two scenarios: an INOUT goes in and comes back (here filled in by the procedure when it arrives as NULL), and OUT values come back typed in Outputs. Each OUT is declared with its .NET type; ARX passes it as a typed NULL, as PostgreSQL requires (PostgreSQL 14 or later).

3Call a function: a value, rows, an effectProgressive

Calling a function, in three scenarios: one value (Value<T>()); rows (ResultSets[0]); a function run for its effect (void: no rows). A function is the same ArxProcedureExpression with Function: ARX sends SELECT * FROM instead of CALL. To filter, order or page a function's rows like a table, Retrieve reads it as a row source (RetrieveMultiple's RealCase6, Advanced8).

4A procedure that returns tablesProgressive

A procedure that returns tables, in two scenarios. A PostgreSQL procedure returns rows through cursors: each table is an INOUT refcursor parameter, declared with AddCursorParameter. ARX runs the call and reads every declared cursor in one transaction (two round trips), and returns each table as a result set named after its parameter.

Real cases

What an application does: a small CRM and product catalogue.

1Place an order through a procedureProgressive

Placing an order through a procedure, in two scenarios: sp_place_order writes the order and its lines (from the products' current prices) in one call and returns the new id and the total; the id is then used like any other (Retrieve reads the order back). The application doesn't repeat the pricing rule the procedure owns.

2A report with several result setsProgressive

An account page built from one call: sp_account_report returns three tables (the contacts, the orders, a summary row), read together in one transaction, each by its cursor parameter's name. A function that returns its cursors itself (RETURNS SETOF refcursor) is read the same way with ReturnsCursors = true.

3Page through a procedure's result: the routine pages itself, ARX pages a function, ARX pages a cursorProgressive

Paging what a procedure or function returns, the three ways, in four scenarios. Each sets PageInfo, as a query does, and gets back a result set with CurrentPage, HasNextPage and, when asked, the total. The total is opt-in: ask for it once (page 1), as for RetrieveMultiple.

  1. The routine pages itself (best for large data: the database produces only the page). ARX fills its page arguments from PageInfo and reads its total from an OUT parameter.
  2. ARX pages a function's rows: LIMIT/OFFSET and the total in one round trip. Cheap for a SQL function (PostgreSQL inlines it, the limit reaches the index); a PL/pgSQL function runs whole first.
  3. ARX pages a table the procedure returns (a cursor): MOVE past the earlier rows, FETCH one page and one row more, MOVE ALL to count the rest. The skipped rows aren't sent, but the database still produces them.
  4. Which to choose.
4Business errors raised by a procedureProgressive

Business rules the database enforces, in three scenarios. sp_reserve_credit refuses an amount over the credit limit (RAISE EXCEPTION ... USING HINT) and an account on hold (its own code, AH001). Both arrive as ARX3005 RaisedByDatabase with the routine's message, its SqlState and its Hint, so the application can tell "the database said no" from "something broke", and branch on the routine's own code.

5A nightly job that commits in batchesProgressive

A nightly job that commits as it goes, in two scenarios. sp_recalculate_totals recalculates order totals in batches and commits after each, so a failure late in the run keeps the batches already done, and reports its progress. ARX's own call runs outside any transaction block, which PostgreSQL requires for a procedure that commits; inside an ARX transaction or a batch scope the same call is refused with ARX8001 and the reason.

6Progress messages from a procedureProgressive

The messages a routine sends while it runs, in two scenarios: RAISE NOTICE and WARNING come back in Messages, in order, for the application to show or log as it chooses (ARX never writes them to a log); CollectMessages = false leaves them out.

7Handle Execute errorsProgressive

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).

8Catch an SQL injection attempt on ExecuteProgressive

Catching an SQL injection attempt on Execute, in three scenarios. A routine name or a parameter name that carries SQL is refused with ARX1017 SuspectedInjection before the database. SQL in a value is a parameter: it's stored as text and runs nothing. Execute never takes SQL text.

Advanced cases

Dependency injection, transactions, batch scopes and setting the service up.

1Execute in an ARX transactionAdvanced

Execute inside an ARX transaction, in two scenarios: a Create and a procedure in one transaction, the procedure's writes read back inside it, and everything gone after a rollback; and a business error from the procedure aborting the transaction, so the next call is refused (ARX1015) until the application rolls back.

2Execute in a batch scope: a refused call undone aloneAdvanced

Execute in a batch scope, in one scenario: an import of three orders through the procedure, one transaction. Each call has its own savepoint (asked for with Savepoints = true), so the order the procedure refuses (a credit limit) is undone alone and the import continues; the others are committed together at the end.

3Export what a procedure returns, page by pageAdvanced

Exporting everything a procedure returns, page by page, in one scenario: sp_products_page pages itself (the first way of paging), the export asks for the total on the first page only, and reads pages until HasNextPage is false. Each page is one call, so the memory is one page's.

4A security check around Execute: probing, blocking, unblocking a false positiveAdvanced

A security check around Execute (SecurityGuard, in the project root), in three scenarios: a report run through an allowed list of procedures passes; an endpoint that runs whatever procedure the request names is probed, and the attacker is blocked after three attempts; and a false positive (an old client sending "account report (v1)") is triaged: support unblocks the user and the endpoint maps the old name.

5Execute on a preloaded, Strict schema: routines aren't covered, tables areAdvanced

Execute on a service whose schema is preloaded and Strict, in three scenarios: a procedure and a function called as usual, because Strict covers tables and views, not routines; a function read as a row source, with the routine's own columns; and the same service still refusing a table column that wasn't preloaded.