I’ve avoided writing this post for a few years now, but no longer! Oracle Database REST APIs served up from ORDS have paging of the results built-in, IF you are using SQL based handlers.

So using SQL to define a REST API on a GET handler, for example, you only need to provide that SQL. This is handled, automatically.

Sweet, super sweet even.

But as awesome as SQL is, we sometimes have the need to instead use PL/SQL. And one of the things PL/SQL can do, is run SQL. In this case, we’re looking at a SYS_REFCURSOR.

Now, I’ve shown before how to get a SYS_RECURSOR back from a PL/SQL block of code in your REST API. BUT, there was no paging there, so if the refcursor has 5000 rows in it, so be it, that’s what you’re going to get.

However many songs the album has, that’s how big the response will be.

Give me paging, already!

You just need to built it into your PL/SQL program, and into your handler. Community member and hero, Oli, shows how to do this briefly in our ORDS forums.

What we’ll need to accommodate this is:

  1. PL/SQL stored procedure with at least one OUT SYS_REFCURSOR
  2. said procedure accommodates inputs to shape the SQL behind the cursor
  3. paging logic
  4. ability to pass values to specify where we want to start and how many rows to grab
  5. provide link(s) to move about the pages
  6. include the paging attributes like shown above (hasMore, limit, offset, count…)

My PL/SQL program

No worries, I’ll share ALL of the source below at the end of the post. I find it’s easier to ‘read’ the code via screenshots sometimes.

create or replace procedure plsql_beers (
    data out SYS_REFCURSOR,
    starting_with in integer default 0,
    how_many in integer default 25)  is
   
 data_out SYS_REFCURSOR;

-- feed inputs to cursor query for offset and pagesize 

BEGIN

    open data_out FOR
      select *
        from UNTAPPD
      offset starting_with rows fetch next how_many rows only;      

    data := data_out;   
end plsql_beers;
Very simple, and using my favorite table.

Warning: ordering of queries isn’t guaranteed

I don’t have an ORDER BY clause in my SELECT statement, but I SHOULD. Since I don’t, it’s quite possible that I could get records moving around different pages, so they could show up twice or more!

Luckily, this is an EASY FIX. I can do an ORDER BY either CREATED_AT or CHECKIN_ID, ASC.

Just running the stored procedure, no REST API

And if I run my new SP using my old friend, SQL Developer, we can see I can get a ‘window’ of the contents of my UNTAPPD table.

Skip first 420 rows, then give me the next 7, thanks!

Now let’s build our REST API!

First I need to sort my REST API Template URI and Handler.

I’ll go with a GET on paged/

Input Parameters

To run the stored procedure, we need to feed two values to it, and the easiest way to to that for a GET handler, is to include them as parameters on the URI.

But I should declare those, so it’s clear what’s happening.

I can now pull these values off the request, and refer to them as :read and :skip in my PL/SQL.

For example, my GET request could look like –

ords/beers/examples/paged/?skip=101&read=3

Handler Code

So obviously I need to build up the code to invoke the PL/SQL program, and then I also need to sort the output, and build out the JSON response that will be returned to the API caller. Let’s look paging portion of the code –

My code is, OK. You should check, implement your own paging logic.

So if someone calls my API, without paging parameters, I want them to default to the first 25 records.

We call the SP with those inputs, from the URI parameters, if included.

I calculate what the offset would be for the ‘next’ page, basically add the current pageize to the current offset.

I include a first link. I am NOT calculating the PREV link, it’s later afternoon and my IF THEN ELSIF brain started hurting. I got some help from AI to calculate the PREV link, and it seems to work pretty good. See below for the logic and examples in the Scenarios.

Knowing how big a REFCURSOR is or if there are more rows to fetch is a challenge, so I’m punting on those, but someone asked how to include those parameters on a PL/SQL handler response, and this is how to do it (declare REST Handler Parameters, set the values appropriately).

About those :bind variables

Most of those are sorting the output or response of the REST API. It’s one thing to have the handler code call my stored procedure, but I must tell ORDS what do to with the results, or how to obtain the results. This is the #1 benefit of our AUTO feature for PL/SQL – we sort that for you!

Our output parameters

We have a few ‘tricks’ employed here, but here’s what we have –

The ‘name’ determines the json object attribute label, the :bind is the var in the plsql code.

Things to remember, keep in mind:

  • The order of the OUT parameters determines the order they are delivered/presented in the JSON response payload
  • A name that starts with a ‘$’ will generate a LINK, assuming it’s an OUT RESPONSE
  • parameter type ‘RESULTSET’ takes the refcursor and uses our JSON logic to give you the items array, just like you see for a SQL query based handler

Ok, let’s do a few examples!

Scenario 1: Default, no paging requested

GET/ords/beers/examples/paged/
Ok, we got our first 25 records, and maybe there’s more, let’s try the ‘next’ resource link!

Scenario 2: The ‘next’ page

So jump ahead 25 records, and get 25 more.

GET /ords/beers/examples/paged/?skip=25&read=25
Ok, next page would start with record number 51, and then get 25 more.

Scenario 3: A random page offset and size

Ok, let’s skip 1775 records and grab 2 only.

GET /ords/beers/examples/paged/?skip=1775&read=2
We have records 1776 and 1777, our next link will get us records 1778 and 1779.

Previous Page!

We have the current page of results, we need to know what the next page is, right? But we might also want to know how to get the previous page.

And, if we’re sitting on page 1 or have a small offset but a big skip, we don’t want to send someone BACK to a negative skip count, like /paged?skip=-3 … instead we’ll just omit the prev link.

The logic for calculating the previous page is fairly straightforward.

First, we’ll do some maths.

prev_link := greatest(param1 - param2, 0);

I’m going to take the largest of two numbers, the difference of skip parameter of the current page and read count, OR ZERO.

That way if that difference is -27, we’ll go with 0 instead.

And then I’ll just check to make sure that’s not set to ZERO before generating a value of the prev link.

if param1 <> 0 THEN
  :prev   := '../paged?skip=' || prev_link || '&read=' || next_link_param2;
end if;

First vs Second vs Third Pages.

Page 1 has no prev link, page 2 points back to page 1, and page 3 points back to page 2.

All the code, already

Ok here you go. As always, don’t copy and paste into production. This is all shown as an example of what CAN BE DONE with your REST APIs. I assume you are a better PL/SQL programmer than me, but if you’re new, be sure to ask someone for code review!

My code review comes from the hater comments following this post, haha. Just kidding, it’s caring to share your feedback!

The original PL/SQL program

This assumes you have a table, with enough data for paging to make sense. So like, 3 rows. I just happen to have a couple a thousand in my UNTAPPD table.

create or replace procedure plsql_beers (
    data out SYS_REFCURSOR,
    starting_with in integer default 0,
    how_many in integer default 25)  is
   
 data_out SYS_REFCURSOR;

-- feed inputs to cursor query for offset and pagesize 

BEGIN

    open data_out FOR
      select *
        from UNTAPPD
      offset starting_with rows fetch next how_many rows only;      

    data := data_out;   
end plsql_beers;

The REST Module, template, handler, and parameters.

If you copy and paste this, you’ll need to change out the table name, and note I’m including REST Privs, so this will be LOCKED DOWN, by default.


set define off;


-- Generated by ORDS REST Data Services 23.3.0.r2891830
-- Schema: BEERS  Date: Wed Dec 06 08:58:25 2023 
--

DECLARE
  l_roles     OWA.VC_ARR;
  l_modules   OWA.VC_ARR;
  l_patterns  OWA.VC_ARR;

BEGIN
  ORDS.ENABLE_SCHEMA(
      p_enabled             => TRUE,
      p_schema              => 'BEERS',
      p_url_mapping_type    => 'BASE_PATH',
      p_url_mapping_pattern => 'beers',
      p_auto_rest_auth      => TRUE);
    
  ORDS.DEFINE_MODULE(
      p_module_name    => 'examples',
      p_base_path      => '/examples/',
      p_items_per_page => 25,
      p_status         => 'PUBLISHED',
      p_comments       => NULL);

  ORDS.DEFINE_TEMPLATE(
      p_module_name    => 'examples',
      p_pattern        => 'paged/',
      p_priority       => 0,
      p_etag_type      => 'HASH',
      p_etag_query     => NULL,
      p_comments       => NULL);

  ORDS.DEFINE_HANDLER(
      p_module_name    => 'examples',
      p_pattern        => 'paged/',
      p_method         => 'GET',
      p_source_type    => 'plsql/block',
      p_mimes_allowed  => NULL,
      p_comments       => NULL,
      p_source         => 
'DECLARE
 param1 integer := :skip;
 param2 integer := :read;
 next_link_param1 integer;
 next_link_param2 integer;

 prev_link integer;


-- dealing with :binds can be fun, so I''m declaring local variables to deal with those 

BEGIN

-- if request comes in w/o number of rows requested, we''ll default to the 

    if param1 is null THEN 
     param1 := 0;
    end if;

    if param2 is null THEN
     param2 := 25;
    end if;

    -- call the program!
    -- we have a parameter for the REST handler of type OUT RESULTSET to deal


    plsql_beers(data => :response1,
                starting_with => param1,
                how_many => param2);

    -- there is little to no paging logic, but 

    -- to follow link to ''next page'', take current offset and add to it the number of rows fetched
    -- brain hurts, skipping the PREV logic

    next_link_param1 := param1 + param2;
    next_link_param2 := param2;
    prev_link := greatest(param1 - param2, 0);

    :next   := ''../paged?skip='' || next' || '_link_param1 || ''&read='' || next_link_param2;
    if param1 <> 0 THEN
     :prev   := ''../paged?skip='' || prev_link || ''&read='' || next_link_param2;
    end if;
    :first  := ''../paged/'';
    :more   := ''Mayhaps'';

    -- going to lie with count, no way to know for sure, default to number of rows requested

    :count  := next_link_param2;
    :offset := param1;
    :limit  := param2;

end;');


  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'items',
      p_bind_variable_name => 'response1',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'RESULTSET',
      p_access_method      => 'OUT',
      p_comments           => NULL);


  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'hasMore',
      p_bind_variable_name => 'more',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'STRING',
      p_access_method      => 'OUT',
      p_comments           => NULL);

        ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'limit',
      p_bind_variable_name => 'limit',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'INT',
      p_access_method      => 'OUT',
      p_comments           => NULL);


  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'offset',
      p_bind_variable_name => 'offset',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'INT',
      p_access_method      => 'OUT',
      p_comments           => NULL);

  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'count',
      p_bind_variable_name => 'count',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'INT',
      p_access_method      => 'OUT',
      p_comments           => NULL);


  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => '$first',
      p_bind_variable_name => 'first',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'STRING',
      p_access_method      => 'OUT',
      p_comments           => NULL);

  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => '$next',
      p_bind_variable_name => 'next',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'STRING',
      p_access_method      => 'OUT',
      p_comments           => NULL);

  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => '$prev',
      p_bind_variable_name => 'prev',
      p_source_type        => 'RESPONSE',
      p_param_type         => 'STRING',
      p_access_method      => 'OUT',
      p_comments           => NULL);

  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'read',
      p_bind_variable_name => 'read',
      p_source_type        => 'URI',
      p_param_type         => 'INT',
      p_access_method      => 'IN',
      p_comments           => NULL);

  ORDS.DEFINE_PARAMETER(
      p_module_name        => 'examples',
      p_pattern            => 'paged/',
      p_method             => 'GET',
      p_name               => 'skip',
      p_bind_variable_name => 'skip',
      p_source_type        => 'URI',
      p_param_type         => 'INT',
      p_access_method      => 'IN',
      p_comments           => NULL);

  ORDS.CREATE_ROLE(p_role_name => 'beers_client');
    
  l_roles(1) := 'SQL Developer';
  l_roles(2) := 'beers_client';
  l_modules(1) := 'examples';

  ORDS.DEFINE_PRIVILEGE(
      p_privilege_name => 'untappd.module',
      p_roles          => l_roles,
      p_patterns       => l_patterns,
      p_modules        => l_modules,
      p_label          => 'untappd',
      p_description    => 'locking down our example module',
      p_comments       => NULL);

  l_roles.DELETE;
  l_modules.DELETE;
  l_patterns.DELETE;
        
COMMIT;

END;
Author

I'm a Distinguished Product Manager at Oracle. My mission is to help you and your company be more efficient with our database tools.

Write A Comment