<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[apex_debug]]></title><description><![CDATA[apex_debug]]></description><link>https://apexdebug.com</link><generator>RSS for Node</generator><lastBuildDate>Tue, 19 May 2026 09:51:47 GMT</lastBuildDate><atom:link href="https://apexdebug.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Using APEX_STRING.SPLIT in SQL]]></title><description><![CDATA[First...have I mentioned how much I don't like the blogging platform that I am using (hashnode)? Well, I don't like it. I don't like it for many reasons, but today just highlights one more reason. I'm]]></description><link>https://apexdebug.com/using-apex-string-split-in-sql</link><guid isPermaLink="true">https://apexdebug.com/using-apex-string-split-in-sql</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[SQL]]></category><category><![CDATA[PL/SQL]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Sun, 15 Mar 2026 22:09:29 GMT</pubDate><content:encoded><![CDATA[<p>First...have I mentioned how much I don't like the blogging platform that I am using (hashnode)? Well, I don't like it. I don't like it for many reasons, but today just highlights one more reason. I'm trying to make use of the freesql embed that allows me to have a SQL editor right on the page. It's quite cool. You can <a href="https://www.thatjeffsmith.com/archive/2025/06/database-news-june-edition-23ai-and-some-livesql/">read about it here</a>. Unfortunately, after way too much time trying to get it to work today on hashnode I have given up. Instead I will just give you the code you <em>would</em> use if it worked. So, on to the blog post, which is hosted on hashnode...for now.</p>
<p>In APEX Instant Tips episode 170 Marwa and I showed how to <strong>find the matching elements of two delimited strings</strong>. A lot of people will automatically turn to PL/SQL to do this kind of thing, but it is really just an inner join in SQL. We can make use of APEX_STRING.SPLIT to do this in a single SQL statement. Try it out below:</p>
<pre><code class="language-sql">select column_value
  from apex_string.split('a,b,c,d,e,f,g,h',',') s1
  join apex_string.split('g,h,i,j',',') s2 using (column_value)
</code></pre>
<p>The code to embed a SQL editor:</p>
<pre><code class="language-html">&lt;iframe id="live-sql-embedded" src="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE23NQQrCMBRF0XlX8WZp4VXQLYhjl1Bi%252FE1%252FSZPSn4jLdyQ4cH4P1yRJqAgltS1PL5%252BadMB8lA1%252Bl%252Fdk9dAcT7Ynrb3zfDDwSeHMyMXR0Q2wcwesRfM%252FErlQuX7TC5ppjuh%252Fj8M43u7XD%252FaNIwWLAAAA&amp;&amp;code_language=PL_SQL&amp;code_format=false" height="460px" width="100%" frameborder="0" allowfullscreen="true" style="width:100%;border:1px solid #e0e0e0;border-radius:12px;overflow:hidden"&gt;FreeSQL Embedded Playground&lt;/iframe&gt;
</code></pre>
<p>Or just <a href="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE23NQQrCMBRF0XlX8WZp4VXQLYhjl1Bi%252FE1%252FSZPSn4jLdyQ4cH4P1yRJqAgltS1PL5%252BadMB8lA1%252Bl%252Fdk9dAcT7Ynrb3zfDDwSeHMyMXR0Q2wcwesRfM%252FErlQuX7TC5ppjuh%252Fj8M43u7XD%252FaNIwWLAAAA&amp;&amp;code_language=PL_SQL&amp;code_format=false">click here t</a>o try it out.</p>
<p><strong>Everything that does not match</strong></p>
<pre><code class="language-sql">select column_value
  from apex_string.split('a,b,c,d,e,f,g,h',',') s1
minus  
select column_value
  from apex_string.split('g,h,i,j',',') s2 
order by 1
</code></pre>
<p>The code to embed a SQL editor:</p>
<pre><code class="language-html">&lt;iframe id="live-sql-embedded2" src="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE5XMQQ7CIBAF0D2n%252BDs0%252BTWpVzCuPUJD6ZRigDYMGL29Kw%252FgO8BTSeIb%252FJ56LtPLpS4GWOue4Q55T9pqLOGiR4rtZB1nei4UrgzcLC3tGTqaHEtXwOhfXeDGyOevucLsdZGK%252BYNxGO6P2xdN70yOngAAAA%253D%253D&amp;code_language=PL_SQL&amp;code_format=false" height="460px" width="100%" frameborder="0" allowfullscreen="true" style="width:100%;border:1px solid #e0e0e0;border-radius:12px;overflow:hidden"&gt;FreeSQL Embedded Playground&lt;/iframe&gt;
</code></pre>
<p>Or <a href="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE5XMQQ7CIBAF0D2n%252BDs0%252BTWpVzCuPUJD6ZRigDYMGL29Kw%252FgO8BTSeIb%252FJ56LtPLpS4GWOue4Q55T9pqLOGiR4rtZB1nei4UrgzcLC3tGTqaHEtXwOhfXeDGyOevucLsdZGK%252BYNxGO6P2xdN70yOngAAAA%253D%253D&amp;code_language=PL_SQL&amp;code_format=false">click here</a> to try it out.</p>
<p><strong>Everything that is in either list, but not both lists</strong></p>
<pre><code class="language-sql">select s1.column_value s1_column_value, s2.column_value s2_column_value
  from apex_string.split('a,b,c,d,e,f,g,h',',') s1
  full outer join apex_string.split('g,h,i,j',',') s2 on s1.column_value = s2.column_value
  where s1.column_value is null
     or s2.column_value is null
  order by 1, 2
</code></pre>
<p>The code to embed a SQL editor:</p>
<pre><code class="language-html">&lt;iframe id="live-sql-embedded3" src="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE22POw6DMBBEe04xHYm0INl9qih1joD4LGBkbOS187l9RBElgUy32vekGWHLbYSosvU2za661TYxRFXfN0H0BtA%252FQAb0wc%252BoF35UEoNxQymLNfGQ19RQSx0x9TTQmFNO%252BRGiViVZC58iB0zeuH%252F2QCMZmt6Whne7sqdtuwy4jxx4RxqBS9ZmWOPDbtXn70PHAc0TiqCL4nI9vwAl7DC2KgEAAA%253D%253D&amp;code_language=PL_SQL&amp;code_format=false" height="460px" width="100%" frameborder="0" allowfullscreen="true" style="width:100%;border:1px solid #e0e0e0;border-radius:12px;overflow:hidden"&gt;FreeSQL Embedded Playground&lt;/iframe&gt;
</code></pre>
<p>Or <a href="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE22POw6DMBBEe04xHYm0INl9qih1joD4LGBkbOS187l9RBElgUy32vekGWHLbYSosvU2za661TYxRFXfN0H0BtA%252FQAb0wc%252BoF35UEoNxQymLNfGQ19RQSx0x9TTQmFNO%252BRGiViVZC58iB0zeuH%252F2QCMZmt6Whne7sqdtuwy4jxx4RxqBS9ZmWOPDbtXn70PHAc0TiqCL4nI9vwAl7DC2KgEAAA%253D%253D&amp;code_language=PL_SQL&amp;code_format=false">click here</a> to try it out.</p>
<p><strong>Everything that is in either list, but only show once</strong></p>
<pre><code class="language-sql">select column_value
  from apex_string.split('a,b,c,d,e,f,g,h',',') s1
union  
select column_value
  from apex_string.split('g,h,i,j',',') s2 
order by 1
</code></pre>
<p>The code to embed a SQL editor:</p>
<pre><code class="language-html">&lt;iframe id="live-sql-embedded5" src="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE5XMQQ7CIBAF0D2n%252BDs0%252BTWpVzCuPUJD6ZRiKDQMGL29Kw%252FgO8BTSeIbfEl9z9PLpS4GWGvZ4Q55T9pqzOGiR4rtZB1nei4UrgzcLC3tGTqanmPJgNG%252FusCNkc9fc4UpdZGK%252BYNxGO6P2xfzRoOdngAAAA%253D%253D&amp;code_language=PL_SQL&amp;code_format=false" height="460px" width="100%" frameborder="0" allowfullscreen="true" style="width:100%;border:1px solid #e0e0e0;border-radius:12px;overflow:hidden"&gt;FreeSQL Embedded Playground&lt;/iframe&gt;
</code></pre>
<p>Or <a href="https://freesql.com/embedded/?layout=vertical&amp;compressed_code=H4sIAAAAAAAAE5XMQQ7CIBAF0D2n%252BDs0%252BTWpVzCuPUJD6ZRiKDQMGL29Kw%252FgO8BTSeIbfEl9z9PLpS4GWGvZ4Q55T9pqzOGiR4rtZB1nei4UrgzcLC3tGTqanmPJgNG%252FusCNkc9fc4UpdZGK%252BYNxGO6P2xfzRoOdngAAAA%253D%253D&amp;code_language=PL_SQL&amp;code_format=false">click here</a> to try it out.</p>
<p>There we have it, yet another blog post about the magic of APEX_STRING. Truly one of the great hidden features of APEX.</p>
]]></content:encoded></item><item><title><![CDATA[How to Run Oracle APEX on your Local Machine]]></title><description><![CDATA[There are several great blog posts on this topic. In particular, this on from United Codes covers the details of each step. Marwa and I also did an APEX Instant Tip (episode 191) on this topic. This p]]></description><link>https://apexdebug.com/how-to-run-oracle-apex-on-your-local-machine</link><guid isPermaLink="true">https://apexdebug.com/how-to-run-oracle-apex-on-your-local-machine</guid><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Mon, 02 Mar 2026 17:52:42 GMT</pubDate><content:encoded><![CDATA[<p>There are several great blog posts on this topic. In particular, <a href="https://www.united-codes.com/products/uc-local-apex-dev/docs/">this on from United Codes</a> covers the details of each step. Marwa and I also did an <a href="https://www.youtube.com/watch?v=Yiid4ugLS7E&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=1">APEX Instant Tip (episode 191)</a> on this topic. This post expands upon that tip.</p>
<p>I have found that just asking an LLM (ChatGPT, Claude, etc.) to help you set up a local APEX environment does not go well. If you give it a little guidance, though, you can probably get there. The steps below are the high level steps. I will give more detail below them.</p>
<ol>
<li><p>Download and install a modern version of Java (if not already installed)</p>
</li>
<li><p>Download and install SQLcl (if not already installed)</p>
</li>
<li><p>Download, install, and run Docker Desktop (if not already installed)</p>
</li>
<li><p>Get the latest Docker image of the Oracle free database</p>
</li>
<li><p>Run that Docker image (port forwarding ports 1521) (this is FREEPDB1)</p>
</li>
<li><p>Download APEX on YOUR LOCAL MACHINE</p>
</li>
<li><p>Install APEX (from your local machine) into the FREEPDB1 (in your Docker container)</p>
</li>
<li><p>Download ORDS on YOUR LOCAL MACHINE</p>
</li>
<li><p>Install ORDS on YOUR LOCAL MACHINE pointed to FREEPDB1</p>
</li>
</ol>
<h2>Steps 1-3</h2>
<p>Steps 1-3 are up to you to do. They are a little different on Windows or Mac, but they are easy to do. All of these steps should be done on your local machine. That is to say, you should be able to run SQLcl on your local machine.</p>
<h2>Step 4</h2>
<p>You could skip step 4 and just go to step 5, but I like to do it in two steps. Step 4 takes a long time, so I like to know that I have accomplished it before moving to step 5. In order to do step 4, you need to have the Docker Desktop running. If you just installed Docker Desktop and have not used it yet, you will have something that looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62ea7bc192b7fd0a78c368b2/d9613a10-5ee4-423a-9281-66429d4832d5.png" alt="" style="display:block;margin:0 auto" />

<p>From the command line, issue the following command (or, for what is currently truly the latest, the command below this command):</p>
<pre><code class="language-plaintext">% docker pull container-registry.oracle.com/database/free:latest
</code></pre>
<p>This will download the latest version of the free database. If you want a specific version, and additional context and instructions, see the Oracle page related to this. For example, you may wish to explicitly get the 23.26.1 version (which, oddly, as of this writing, is more current than the "latest" version):</p>
<pre><code class="language-plaintext">docker pull container-registry.oracle.com/database/free:23.26.1.0
</code></pre>
<p>Note: The Oracle page referenced above gives instructions to use Podman. That is fine as well. I happen to already use Docker, so Docker was my choice.</p>
<h2>Step 5</h2>
<p>Run the docker image from the command line. <strong>CHANGE THE PASSWORD</strong> that you see below. Also <strong>CHANGE THE PATH</strong> specified in -v (or leave out -v altogether). Read this whole section to understand the parameters prior to blindly running the command ;)</p>
<pre><code class="language-plaintext">% docker run -d \
  --name apex-db \
  -p 1521:1521 \
  -e ORACLE_PWD=abc123ABCyadayadayada \
  -v ~/oware/docker/dbfree_apex_db/oradata:/opt/oracle/oradata \
  container-registry.oracle.com/database/free:latest
</code></pre>
<p>or, if like me, you want 23.26.1.0</p>
<pre><code class="language-plaintext">% docker run -d \
  --name apex-db \
  -p 1521:1521 \
  -e ORACLE_PWD=abc123ABCyadayadayada \
  -v ~/oware/docker/dbfree_apex_db/oradata:/opt/oracle/oradata \
  container-registry.oracle.com/database/free:23.26.1.0
</code></pre>
<p>Below is a quick description of that command:</p>
<pre><code class="language-plaintext">% docker run -d \  
^^^ this tells docker to create/run a container
  --name apex-db \ 
^^^ this is the container name
  -p 1521:1521 \   
^^^ tells Docker to map the local port 1521 to the container virtual port 1521 for SQLNet traffic
  -e ORACLE_PWD=abc123ABCyadayadayada \ 
^^^ this is the database SYS password
  -v ~/oware/docker/dbfree_apex_db/oradata:/opt/oracle/oradata \
  container-registry.oracle.com/database/free:latest
^^^ This tells docker that I want to map the container's filesystem directory /opt/oracle/oradata to my local OS directory ~/oware/docker/dbfree_apex_db/oradata. If I destroy my container, the local directory with my database files will not get deleted.
</code></pre>
<p>Once you run this command you will see something like this in your Docker Desktop:</p>
<img src="https://cdn.hashnode.com/uploads/covers/62ea7bc192b7fd0a78c368b2/9c4b2fc7-ecaf-4aaa-a734-5f7c1b9bb3c8.png" alt="" style="display:block;margin:0 auto" />

<p>Mine shows apex-db and apex-db2 because I've done this a couple of times. (Note the different port mapping. That allows me to run two databases at the same time. I changed --name, -p, and -v to run it multiple times.)</p>
<p>The first time you do this it will take some time for it to run. You can follow the output with the following commands:</p>
<pre><code class="language-plaintext">% docker logs apex-db

% docker logs -f apex-db
</code></pre>
<p>You interact with the Docker container by first opening a bash terminal. It will look something like this:</p>
<pre><code class="language-plaintext">% docker exec -it apex-db bash
bash-4.4$ whoami
oracle
bash-4.4$ pwd
/home/oracle
</code></pre>
<p>Once you execute "docker exec -it apex-db bash" the rest of the commands are within the Docker container.</p>
<h2>Download APEX -- ON YOUR LOCAL MACHINE</h2>
<p>It's important to do this step on your local machine. This is because the Docker image is really only good for the database. It's very hard to run the middle tier component on that Docker image, so we are going to run the middle tier on the local OS. Below is the link to download APEX.</p>
<p><a href="https://www.oracle.com/tools/downloads/apex-downloads/">https://www.oracle.com/tools/downloads/apex-downloads/</a></p>
<h2>Install APEX (from your local machine) into the FREEPDB1 (in your Docker container)</h2>
<p>You can follow the APEX install guide, but this is basically how you do it. You run the commands below ON YOUR LOCAL MACHINE:</p>
<pre><code class="language-plaintext">% sql sys/your_password_here@localhost:1521/FREEPDB1 as sysdba

SQL&gt; ALTER SESSION SET CONTAINER=FREEPDB1;
SQL&gt; @apexins.sql SYSAUX SYSAUX TEMP /i/
SQL&gt; @apxchpwd.sql
SQL&gt; alter user apex_public_user account unlock;
</code></pre>
<p>Note that your database appears to be running on "localhost" port 1521. This is because when you ran the Docker container you mapped port 1521 on localhost to port 1521 in the container.</p>
<h2>Download ORDS on YOUR LOCAL MACHINE</h2>
<p>It's important to do this step on your local machine. This is because the Docker image is really only good for the database. It's very hard to run the middle tier component on that Docker image, so we are going to run the middle tier on the local OS. Below is the link to download ORDS.</p>
<p><a href="https://www.oracle.com/database/sqldeveloper/technologies/db-actions/download/">https://www.oracle.com/database/sqldeveloper/technologies/db-actions/download/</a></p>
<h2>Install ORDS on YOUR LOCAL MACHINE pointed to FREEPDB1</h2>
<p>It's important to do this step on your local machine. This is because the Docker image is really only good for the database. It's very hard to run the middle tier component on that Docker image, so we are going to run the middle tier on the local OS. I've said that three times now ;) Below are instructions to install and configure ORDS. Be sure to copy the APEX images to whatever location you define for images for ORDS.</p>
<p><a href="https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/25.4/ordig/installing-and-configuring-oracle-rest-data-services.html">https://docs.oracle.com/en/database/oracle/oracle-rest-data-services/25.4/ordig/installing-and-configuring-oracle-rest-data-services.html</a></p>
<p>The main command to install ORDS after downloading it is</p>
<pre><code class="language-plaintext">% ords --config /etc/ords/config install
</code></pre>
<p>Notes</p>
<ol>
<li><p>You can change /etc/ords/config above to any directory you want. It will hold the ORDS config info (obviously).</p>
</li>
<li><p>I prefer to run in TLS/SSL/HTTPS mode. When you go through the interactive install (when you run the command above), it will give you the option to set this up. Be sure to read everything along the way during the interactive install. If you just accept the defaults, you will end up running http (not https). While this is "fine," you will likely want to test things as they will be in a "real" environment. You might as well set it up for https now.</p>
</li>
<li><p>Be sure to select the option to set your "APEX Static resources location" (currently option [9]. Enter the filesystem path to your APEX image directory (part of the APEX download you did above).</p>
</li>
</ol>
<h2>Run APEX</h2>
<p>That's it. If you have done everything above, APEX should be available at</p>
<p><a href="http://localhost:8080">http://localhost:8080</a></p>
<p>or, if you set up to run via SSL on port 8443</p>
<p><a href="https://localhost:8443">https://localhost:8443</a></p>
<p>Follow instructions for starting and stopping ORDS found in the ORDS documentation. If you shut things down, you first have to start your Docker container (using the Docker Desktop), then start ORDS. I use this command (from within my ORDS config directory) to start ORDS in the background:</p>
<p>nohup ords serve &gt; ~/ords.log 2&gt;&amp;1 &amp;</p>
]]></content:encoded></item><item><title><![CDATA[Using Faceted Search in Additional Region Types]]></title><description><![CDATA[Background
Faceted search only works for classic reports, maps, and card regions. But with just a little effort, you can extend this feature to any region type. I stumbled upon this method while working for a client. I’d love to give them credit, but...]]></description><link>https://apexdebug.com/using-faceted-search-in-additional-region-types</link><guid isPermaLink="true">https://apexdebug.com/using-faceted-search-in-additional-region-types</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 21 Nov 2025 17:10:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763733352501/98219cf4-981c-4178-9ef3-d47a97713ec8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-background">Background</h1>
<p>Faceted search only works for classic reports, maps, and card regions. But with just a little effort, you can extend this feature to any region type. I stumbled upon this method while working for a client. I’d love to give them credit, but I can’t always reveal my sources :). I do know that the client got the technique from a blog post that is no longer active:<br /><a target="_blank" href="https://blogs.oracle.com/apex/post/add-a-chart-to-your-faceted-search-page">https://blogs.oracle.com/apex/post/add-a-chart-to-your-faceted-search-page</a><br />I never had the pleasure to read the blog post, but I got a gist of what it must have said. I’ve written what I believe is a more generic utility to accomplish the task—perhaps an improvement on the original, but I don’t know for sure. (I tried using https://archive.org to look it up, but at the time of this writing the archive was temporarily offline.)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763646397612/c5e9636f-d300-4dba-b343-eddaef577532.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-overview">Overview</h1>
<p>The overall approach is quite straightforward, but I’ll add some nuance later.</p>
<ol>
<li><p>Create a faceted search based upon a classic report. Get it working exactly as you would like it to work. Get all the facets working as they should. Make sure the classic report returns exactly the data you are interested in. Give the report a static ID (e.g. EMP_R).</p>
</li>
<li><p>Create the kind of component you actually want, using the same query as the classic report. I’m going to call this a <strong>chart</strong> just to be able to refer to it, but it could be any region type that supports refresh.</p>
</li>
<li><p>Create a dynamic action that refreshes the chart after the classic report refreshes.</p>
<ol>
<li>At this point changing a facet will change the results of the classic report. That, in turn, will refresh the chart, but the results of the chart won’t change.</li>
</ol>
</li>
<li><p>This is the hardest step, but the solution is below…so just get the idea. Create a function that returns a table type. The function will return this table type containing only the rows returned by the classic report. This table type needs to include one or more attributes that allow you to identify the rows that are returned. Generally, any row returned should be able to be uniquely identified by one or more columns. Usually they can be identified by a single primary key column. For example, if your data is about employees (the classic EMP table), you may only need to return the EMPNO. Even if the query is very complicated, a single primary key column may likely be all you need. You can review the code below to see how this works. This function will use APEX_EXEC. I’m going to call this function “<strong>ait_util.get_faceted_search_number_data</strong>”.</p>
</li>
<li><p>To the chart region query you will either add a join on get_faceted_search_number_data or you will add this to the where clause of the query. It will look something like this:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> e.empno,
       e.ename,
       ...
       d.dname
       ...
  <span class="hljs-keyword">from</span> emp e
  <span class="hljs-keyword">join</span> dept d <span class="hljs-keyword">on</span> d.deptno = e.empno
  <span class="hljs-keyword">join</span> ait_util.get_faceted_search_number_data(...) facet_results <span class="hljs-keyword">on</span> facet_results.column_value = e.empno
  <span class="hljs-keyword">where</span> ...
</code></pre>
<p>or (this method can also be used if you define the region directly on a table or view, not a SQL query)</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> e.empno,
       e.ename,
       ...
       d.dname
       ...
  <span class="hljs-keyword">from</span> emp e
  <span class="hljs-keyword">join</span> dept d <span class="hljs-keyword">on</span> d.deptno = e.empno
  <span class="hljs-keyword">where</span> empno <span class="hljs-keyword">in</span> (<span class="hljs-keyword">select</span> <span class="hljs-keyword">column_value</span> <span class="hljs-keyword">from</span> ait_util.get_faceted_search_number_data(...) )
    <span class="hljs-keyword">and</span> ...
</code></pre>
<p>This will limit the chart query to just the rows being displayed in the classic report.</p>
<ol start="6">
<li><p>Now you can “hide” the classic report (assuming you don’t want it to show). The classic report still needs to exist on the page, though. You can hide it using an page onload dynamic action or by adding an attribute to the region: style=”display:none” .</p>
</li>
<li><p>Note: ultimately, your classic report query only needs to have the columns in the select portion of the query that are used for facets, plus the primary key column used to limit the rows of the chart. If you have some large calculation or other potentially slow part of the query that is not used to reduce the rows returned, it does not need to be in the classic report.</p>
</li>
</ol>
<p>The key to this, of course, is the function ait_util.get_faceted_search_number_data. I think the original post suggested creating an object type with multiple attributes. In the example above it would return a table type of an object, e.g. T_EMPLOYEE_TABLE returning object T_EMPLOYEE, with at least the EMPNO attribute. This is great and particularly important if you don’t have a way to identify the rows by a single column/attribute. In that case you will need to be able to return more than one attribute. But usually, it will be a primary key.</p>
<h1 id="heading-the-details">The Details</h1>
<p>Steps 4 &amp; 5 above are all about using the faceted search classic report to drive the results of the chart region (or any other region type). It’s funny because I thought we did an APEX Instant Tip on exactly this at some point in the past, but I don’t see one. In any case, I’ve made this super easy to do. The package below provides two functions, one for row identifiers that are varchar2 and the other for row identifiers that are numbers. With these functions you don’t need to create any object types. The package relies on APEX_T_VARCHAR2 and APEX_T_NUMBER. Just install the package spec and body below and you can follow the steps above to drive the results of any region type based upon a faceted search.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">package</span> <span class="hljs-string">"AIT_UTIL"</span> <span class="hljs-keyword">as</span>

<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-comment">-- comments about function</span>
<span class="hljs-comment">-- select column_value</span>
<span class="hljs-comment">--   from ait_util.get_faceted_search_vc2_data(</span>
<span class="hljs-comment">--                                    p_page_id =&gt; :APP_PAGE_ID,</span>
<span class="hljs-comment">--                                    p_region_static_id =&gt; 'EMP_R',</span>
<span class="hljs-comment">--                                    p_pk_column_name =&gt; 'EMPNO')</span>
<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-keyword">function</span> get_faceted_search_vc2_data( 
    p_page_id           <span class="hljs-keyword">in</span> <span class="hljs-built_in">number</span>,
    p_region_static_id  <span class="hljs-keyword">in</span> <span class="hljs-built_in">varchar2</span>,
    p_pk_column_name    <span class="hljs-keyword">in</span> <span class="hljs-built_in">varchar2</span> ) 
  <span class="hljs-keyword">return</span> apex_t_varchar2 <span class="hljs-keyword">pipelined</span> ;

<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-comment">-- comments about function</span>
<span class="hljs-comment">-- select column_value</span>
<span class="hljs-comment">--   from ait_util.get_faceted_search_number_data(</span>
<span class="hljs-comment">--                                    p_page_id =&gt; :APP_PAGE_ID,</span>
<span class="hljs-comment">--                                    p_region_static_id =&gt; 'EMP_R',</span>
<span class="hljs-comment">--                                    p_pk_column_name =&gt; 'EMPNO')</span>
<span class="hljs-comment">--==============================================================================</span>
function get_faceted_search_number_data( 
    p_page_id           in number,
    p_region_static_id  in varchar2,
    p_pk_column_name    in varchar2 ) 
  return apex_t_number pipelined ;  

<span class="hljs-keyword">end</span> <span class="hljs-string">"AIT_UTIL"</span>;
/
</code></pre>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">package</span> <span class="hljs-keyword">body</span> <span class="hljs-string">"AIT_UTIL"</span> <span class="hljs-keyword">as</span>

<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-comment">-- Public API, see specification</span>
<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-keyword">function</span> get_faceted_search_vc2_data( 
    p_page_id           <span class="hljs-keyword">in</span> <span class="hljs-built_in">number</span>,
    p_region_static_id  <span class="hljs-keyword">in</span> <span class="hljs-built_in">varchar2</span>,
    p_pk_column_name    <span class="hljs-keyword">in</span> <span class="hljs-built_in">varchar2</span> ) 
  <span class="hljs-keyword">return</span> apex_t_varchar2 <span class="hljs-keyword">pipelined</span> 
  <span class="hljs-keyword">is</span>

    l_region_id   <span class="hljs-built_in">number</span>;
    l_context     apex_exec.t_context;

    type t_col_index is table of pls_integer index by varchar2(255);
    l_col_index t_col_index;

    <span class="hljs-comment">---------------------------------------------------------------------------</span>
    procedure get_column_indexes( p_columns wwv_flow_t_varchar2 ) is
    <span class="hljs-keyword">begin</span>
      <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span> .. p_columns.count <span class="hljs-keyword">loop</span>
          l_col_index( p_columns( i ) ) := apex_exec.get_column_position( 
                                               p_context =&gt; l_context, 
                                               p_column_name =&gt; p_columns( i ) );
      <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;
    <span class="hljs-keyword">end</span> get_column_indexes;

  <span class="hljs-keyword">begin</span>
    <span class="hljs-comment">-- 1. get the region ID of the Faceted Search region</span>
    <span class="hljs-keyword">select</span> region_id
      <span class="hljs-keyword">into</span> l_region_id
      <span class="hljs-keyword">from</span> apex_application_page_regions
     <span class="hljs-keyword">where</span> application_id = v(<span class="hljs-string">'APP_ID'</span>)
       <span class="hljs-keyword">and</span> page_id        = p_page_id
       <span class="hljs-keyword">and</span> static_id      = p_region_static_id;

    <span class="hljs-comment">-- 2. Get a cursor (apex_exec.t_context) for the current region data</span>
    l_context := apex_region.open_query_context(
                     p_page_id      =&gt; p_page_id,
                     p_region_id    =&gt; l_region_id );

    get_column_indexes( wwv_flow_t_varchar2( p_pk_column_name ) );

    while apex_exec.next_row( p_context =&gt; l_context ) loop
        pipe row(apex_exec.get_varchar2( p_context =&gt; l_context, p_column_idx =&gt; l_col_index( p_pk_column_name ) ) );
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;

    apex_exec.close( l_context );

    return;
  exception
    when no_data_needed then
      apex_exec.close( l_context );
      return;
    when others then
      apex_exec.close( l_context );
      raise;
<span class="hljs-keyword">end</span> get_faceted_search_vc2_data;


<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-comment">-- Public API, see specification</span>
<span class="hljs-comment">--==============================================================================</span>
function get_faceted_search_number_data( 
    p_page_id           in number,
    p_region_static_id  in varchar2,
    p_pk_column_name    in varchar2 ) 
  return apex_t_number pipelined 
  is

    l_region_id   number;
    l_context     apex_exec.t_context;

    type t_col_index is table of pls_integer index by varchar2(255);
    l_col_index t_col_index;

    <span class="hljs-comment">---------------------------------------------------------------------------</span>
    procedure get_column_indexes( p_columns wwv_flow_t_varchar2 ) is
    <span class="hljs-keyword">begin</span>
      <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-number">1</span> .. p_columns.count <span class="hljs-keyword">loop</span>
          l_col_index( p_columns( i ) ) := apex_exec.get_column_position( 
                                               p_context =&gt; l_context, 
                                               p_column_name =&gt; p_columns( i ) );
      <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;
    <span class="hljs-keyword">end</span> get_column_indexes;

  <span class="hljs-keyword">begin</span>
    <span class="hljs-comment">-- 1. get the region ID of the Faceted Search region</span>
    <span class="hljs-keyword">select</span> region_id
      <span class="hljs-keyword">into</span> l_region_id
      <span class="hljs-keyword">from</span> apex_application_page_regions
     <span class="hljs-keyword">where</span> application_id = v(<span class="hljs-string">'APP_ID'</span>)
       <span class="hljs-keyword">and</span> page_id        = p_page_id
       <span class="hljs-keyword">and</span> static_id      = p_region_static_id;

    <span class="hljs-comment">-- 2. Get a cursor (apex_exec.t_context) for the current region data</span>
    l_context := apex_region.open_query_context(
                     p_page_id      =&gt; p_page_id,
                     p_region_id    =&gt; l_region_id );

    get_column_indexes( wwv_flow_t_varchar2( p_pk_column_name ) );

    while apex_exec.next_row( p_context =&gt; l_context ) loop
        pipe row(apex_exec.get_number( p_context =&gt; l_context, p_column_idx =&gt; l_col_index( p_pk_column_name ) ) );
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;

    apex_exec.close( l_context );

    return;
  exception
    when no_data_needed then
      apex_exec.close( l_context );
      return;
    when others then
      apex_exec.close( l_context );
      raise;
<span class="hljs-keyword">end</span> get_faceted_search_number_data;


<span class="hljs-keyword">end</span> <span class="hljs-string">"AIT_UTIL"</span>;
/
</code></pre>
<h1 id="heading-some-nuance">Some Nuance</h1>
<p>Keep in mind that your classic report is going to run—even though you may hide the results. Hence, you want it to run as quickly as possible. The classic report and the chart, interactive report, interactive grid, etc. don’t have to have the exact same query. The classic report must have the columns associated with the facets in the select clause, and it should only return the primary key values that you ultimately want in the the target region (e.g. the chart). That means it’s predicate (where clause) should have everything you want in it, but it’s possible you don’t need every join, especially outer joins. Additionally, you may not need every column in the select clause. Take this interactive report as an example:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> e.empno,
       e.ename,
       e.mgr,
       my_slow_function(p_empno =&gt; e.empno) credit_score, <span class="hljs-comment">-- scalar subquery not helpful here</span>
       (<span class="hljs-keyword">select</span> my_other_slow_function(p_dept_mgr_id =&gt; d.mgr_id) <span class="hljs-keyword">from</span> dual) dept_mgr_info,
       e.deptno
  <span class="hljs-keyword">from</span> emp e
  <span class="hljs-keyword">join</span> dept d <span class="hljs-keyword">on</span> d.deptno = e.deptno
</code></pre>
<p>you can use shared LOVs for e.mgr and e.deptno to display the content. You may not want to have facets on credit_score and dept_mgr_info. In this case, your classic report could simply be</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> empno,
       mgr,
       deptno
  <span class="hljs-keyword">from</span> emp
</code></pre>
<p>That will give you the ability to have facets on Manager and Department (using shared LOVs), and it won’t have the overhead of the join or the expensive slow functions. In the end, your IR query would be</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> e.empno,
       e.ename,
       e.mgr,
       my_slow_function(p_empno =&gt; e.empno) credit_score, <span class="hljs-comment">-- scalar subquery not helpful here</span>
       (<span class="hljs-keyword">select</span> my_other_slow_function(p_dept_mgr_id =&gt; d.mgr_id) <span class="hljs-keyword">from</span> dual) dept_mgr_info,
       e.deptno
  <span class="hljs-keyword">from</span> emp e
  <span class="hljs-keyword">join</span> dept d <span class="hljs-keyword">on</span> d.deptno = e.deptno
  <span class="hljs-keyword">join</span> ait_util.get_faceted_search_number_data(
                                    p_page_id =&gt; :APP_PAGE_ID,
                                    p_region_static_id =&gt; <span class="hljs-string">'EMP_R'</span>,
                                    p_pk_column_name =&gt; <span class="hljs-string">'EMPNO'</span>) fs_data
       <span class="hljs-keyword">on</span> fs_data.column_value = e.empno
</code></pre>
<p>If you are basing the region on a table or view, not a SQL query, you can just add this to the where clause of the region:</p>
<pre><code class="lang-sql">e.empno in (<span class="hljs-keyword">select</span> <span class="hljs-keyword">column_value</span> <span class="hljs-keyword">from</span> ait_util.get_faceted_search_number_data(
                                    p_page_id =&gt; :APP_PAGE_ID,
                                    p_region_static_id =&gt; <span class="hljs-string">'EMP_R'</span>,
                                    p_pk_column_name =&gt; <span class="hljs-string">'EMPNO'</span>) )
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Using Declarative APEX REST Data Sources via PL/SQL APIs #JoelKallmanDay]]></title><description><![CDATA[I have written and done several APEX Instant Tips about the topics of using the declarative features of APEX (instead of writing code) and about using APEX REST Data Sources:

APEX Instant Tips #180: Deploying APEX applications that include REST Data...]]></description><link>https://apexdebug.com/using-declarative-apex-rest-data-sources-via-plsql-apis-joelkallmanday</link><guid isPermaLink="true">https://apexdebug.com/using-declarative-apex-rest-data-sources-via-plsql-apis-joelkallmanday</guid><category><![CDATA[orclapex]]></category><category><![CDATA[JoelKallmanDay]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Wed, 15 Oct 2025 12:00:37 GMT</pubDate><content:encoded><![CDATA[<p>I have written and done several APEX Instant Tips about the topics of using the declarative features of APEX (instead of writing code) and about using APEX REST Data Sources:</p>
<ul>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=GH3pGXtKeaM&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=3">APEX Instant Tips #180</a>: Deploying APEX applications that include REST Data Sources</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=wDmngdO5lDw">APEX Instant Tips #99</a>: Connecting an APEX app to GPT-3</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=Oear6q9b4C8&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=84&amp;t=198s">APEX Instant Tips #98</a>: An easier way to give secure access to a REST enabled schema</p>
</li>
<li><p>APEX Instant Tips #92: Using RESTful services to synch data during deployment</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=5P3nx-OLxek&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=141">APEX Instant Tips #41</a> How to secure a RESTful API with ORDS and OAuth2</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=Wwwa90waqBo&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=155">APEX Instant Tips #27</a> REST Data Synchronization, and an homage to <a target="_blank" href="https://oracleapex.com/ords/r/apex_pm/joel/memories">Joel</a> ( <a target="_blank" href="https://oracle-base.com/blog/2025/09/24/joel-kallman-day-2025-announcement/">#JoelKallmanDay</a> )</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=KYdwsY7wSag&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=52">APEX Instant Tips #130</a>: Downloading files (mostly) declaratively</p>
</li>
</ul>
<p>The most recent tip on these topics (180) covers deploying APEX applications with REST Data Sources. It highlights how much the APEX team takes into account the lifecycle of an application. When using a declarative APEX REST Data Source, you get all of the benefits described in AIT episode 180.</p>
<p>What if you need to call a REST service from PL/SQL, though? This may be during an automation or for a host of other reasons. It may even be code that is unrelated to an APEX application. I still recommend you create an APEX application and a declarative REST Data Source. You can then reference the declarative REST Data Source in your PL/SQL code. In the end you will get all of the benefits of the declarative REST Data Source and have less custom code to maintain. Let’s take a look at what would be required to call the REST service defined in AIT 99—a call to ChatGPT—with and without APEX.</p>
<h2 id="heading-without-apex-components">Without APEX Components</h2>
<p>Skip to “With APEX Components” if you just want to get to the “right” way to do it.</p>
<p>Without a declarative REST DATA Source (or any APEX features) I would need the following:</p>
<ol>
<li><p>A way to <em>securely</em> store the web credential</p>
</li>
<li><p>A way to store the REST endpoint</p>
</li>
<li><p>A way to have both of those be different in DEV, TEST, and PROD</p>
</li>
<li><p>A way to call the REST endpoint</p>
</li>
</ol>
<p>Securely storing web credentials is something I never want to be responsible for. I want to rely on a credential store that has been created for this purpose. If I’m not using APEX to store the web credential, I could use the Oracle Wallet associated with the database. Most people, though, tend to store this in a table or even just hard-code it into a PL/SQL package. Entirely unacceptable. So, right away, we have some work to do here. This post is about how to do things the right way, so I’m not going to waste your time or mine describing this further.</p>
<p>Storing the REST endpoint is less problematic in that it doesn’t have the same security implications, but you do have to consider how to maintain it in different environments. So, probably, you will create a table for this and update the table in each environment. Again, totally taken care of by APEX when done declaratively.</p>
<p>Finally, a way to call the endpoint. In this case you could use the UTL_HTTP API to create your own calls to the endpoint. More likely, though, you would still use APEX by calling the APEX_WEB_SERVICE API. During this exercise, you might stumble upon the fact that you can use an APEX Web Credential in your APEX_WEB_SERVICE API calls. This would be great! You could get rid of the issues associate with storing a web credential.</p>
<p>I’ve done it in all of the variations listed above—starting before APEX existed. This method ranges from a few hundred lines of code to a few thousand, depending on how fully you want to implement the solution. Every line of code costs money to write and to maintain.</p>
<h2 id="heading-with-apex-components">With APEX Components</h2>
<p>Obviously, I recommend using APEX declarative components to the maximum extent possible. Using the ChatGPT example from AIT 99, you would simply create the APEX REST Data Source as shown in the episode. In this case, I have given the REST Data Source a static ID: OpenAI___GPT3. Th code below calls the REST Data Source from within the application:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">declare</span>
    l_params apex_exec.t_parameters;
<span class="hljs-keyword">begin</span>
    apex_exec.add_parameter( l_params, <span class="hljs-string">'PROMPT'</span>, apex_json.stringify(:P2_PROMPT));
    apex_exec.add_parameter( l_params, 'TEMP', :P2_TEMP );

    apex_exec.execute_rest_source(
        p_static_id        =&gt; 'OpenAI___GPT3',
        p_operation        =&gt; 'POST',
        p_parameters       =&gt; l_params );

    :P2_RESPONSE := apex_exec.get_parameter_clob(l_params,'RESPONSE');

<span class="hljs-keyword">end</span>;
</code></pre>
<p>If you are not calling the REST Data Source, the APEX documentation for APEX_EXEC gives the following advice:</p>
<blockquote>
<p>All <code>APEX_EXEC</code> procedures require an existing APEX session to function. In a pure SQL or PL/SQL context, use the <code>APEX_SESSION</code> package to initialize a new session.</p>
</blockquote>
<p>This just means you need to create an APEX session prior to to calling APEX_EXEC (and delete or detach after):</p>
<pre><code class="lang-sql"><span class="hljs-keyword">declare</span>
    l_params apex_exec.t_parameters;
<span class="hljs-keyword">begin</span>

    apex_session.create_session (
      p_app_id   =&gt; <span class="hljs-number">123456</span>, <span class="hljs-comment">-- possibly look this up based upon the APP Alias</span>
      p_page_id  =&gt; <span class="hljs-number">1</span>, <span class="hljs-comment">-- fine to hard code this</span>
      p_username =&gt; <span class="hljs-string">'EXAMPLE_USER'</span> ); <span class="hljs-comment">-- also fine to hard code this</span>

    apex_exec.add_parameter( l_params, 'PROMPT', apex_json.stringify(:P2_PROMPT));
    apex_exec.add_parameter( l_params, 'TEMP', :P2_TEMP );

    apex_exec.execute_rest_source(
        p_static_id        =&gt; 'OpenAI___GPT3',
        p_operation        =&gt; 'POST',
        p_parameters       =&gt; l_params );

    :P2_RESPONSE := apex_exec.get_parameter_clob(l_params,'RESPONSE');

    apex_session.delete_session;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>That’s less than 20 lines of code. It requires no custom database objects aside from the PL/SQL package this is stored in. You can easily use APEX_DEBUG messages. In my book declarative wins.</p>
]]></content:encoded></item><item><title><![CDATA[Using APEX_DEBUG Without an APEX Session]]></title><description><![CDATA[Check out APEX Instant Tips episode 178 for more information on this topic.
Code instrumentation is key to developing maintainable and testable applications. The APEX_DEBUG PL/SQL API has everything I need and a lot of features under the covers that ...]]></description><link>https://apexdebug.com/using-apexdebug-without-an-apex-session</link><guid isPermaLink="true">https://apexdebug.com/using-apexdebug-without-an-apex-session</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Sat, 23 Aug 2025 04:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Check out APEX Instant Tips episode 178 for more information on this topic.</p>
<p>Code instrumentation is key to developing maintainable and testable applications. The APEX_DEBUG PL/SQL API has everything I need and a lot of features under the covers that most home-grown systems lack (like flood control). Despite initial appearances, you can use APEX_DEBUG without being in the context of an APEX application, though it takes just a little extra. Below are a few steps that will give you all of the features of APEX_DEBUG without needing an APEX session.</p>
<h2 id="heading-create-the-following-table">Create the following table</h2>
<p>This will allow you to control the debug level when you don’t have an APEX context. You can add additional columns to support additional requirements. For example, you could add a column to indicate a particular database session for which you want to set the debug level. If you alter the table below you will need to change the corresponding package (further below) to meet your needs.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span>  <span class="hljs-string">"XXTLN_DEBUG"</span> 
   (    <span class="hljs-string">"DEBUG_LEVEL"</span> <span class="hljs-built_in">NUMBER</span>
   )
/
</code></pre>
<h3 id="heading-add-1-row-to-the-table">Add 1 row to the table</h3>
<p>In this case we are just going to set the debug level for ALL sessions that do not have an APEX context.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> xxtln_debug (debug_level) <span class="hljs-keyword">values</span> (<span class="hljs-number">1</span>);
</code></pre>
<h2 id="heading-create-the-following-package">Create the following package</h2>
<p>This package just handles setting the APEX security group id (aka workspace id). Setting that allows you to see the contents of the APEX debug message log.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">package</span> XXTLN_DEBUG_util <span class="hljs-keyword">as</span>

<span class="hljs-comment">-- Set how often you would like to check for a change to the debug level.</span>
<span class="hljs-comment">-- <span class="hljs-doctag">Note:</span> do NOT set this while things are running...you should set it to what you want it to be all the time and leave it alone.</span>
<span class="hljs-comment">-- It is reasonable to set it 1 so that it always check, but you may decide to check less fequently.</span>
gc_workspace_name               <span class="hljs-keyword">constant</span>    <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">256</span>) := <span class="hljs-string">'ANTON'</span>;

g_check_every_n_calls           number := 1;
g_count_since_debug_checked     number := g_check_every_n_calls + 1;
g_debug_level                   number;

procedure setup;

<span class="hljs-keyword">end</span>;
/

<span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">package</span> <span class="hljs-keyword">body</span> XXTLN_DEBUG_UTIL <span class="hljs-keyword">as</span>

<span class="hljs-keyword">procedure</span> setup <span class="hljs-keyword">is</span>

l_sg_id         <span class="hljs-built_in">number</span>;
l_debug_level   number;
<span class="hljs-keyword">begin</span>
    <span class="hljs-comment">-- Make sure we always have a workspace ID so that errors and "forced messages" are visible.</span>
    <span class="hljs-keyword">if</span> sys_context(<span class="hljs-string">'APEX$SESSION'</span>,<span class="hljs-string">'WORKSPACE_ID'</span>) <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">then</span>
        apex_util.set_security_group_id(
            p_security_group_id =&gt; apex_util.find_security_group_id(p_workspace =&gt; gc_workspace_name)
            );
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

    <span class="hljs-comment">-- If we are NOT in an APEX application session set the "no APEX session" debug level.</span>
    if sys_context('APEX$SESSION','APP_ID') is null then
        <span class="hljs-comment">-- only check every n calls</span>
        if g_count_since_debug_checked &gt;= g_check_every_n_calls then
            <span class="hljs-comment">-- Get the current "no APEX session" debug level</span>
            <span class="hljs-keyword">select</span> <span class="hljs-keyword">max</span>(debug_level) <span class="hljs-keyword">into</span> l_debug_level
              <span class="hljs-keyword">from</span> xxtln_debug;

            <span class="hljs-comment">-- Set the debug level if it has changed.</span>
            if nvl(g_debug_level, -1) != nvl(l_debug_level, -1) then

                if l_debug_level is not null then
                    apex_debug.enable(l_debug_level);
                else
                    apex_debug.disable;
                <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

                g_debug_level := l_debug_level;                
            <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

            g_count_since_debug_checked := 1;
        else
            g_count_since_debug_checked := g_count_since_debug_checked + 1;
        <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;
<span class="hljs-keyword">end</span> setup;

<span class="hljs-keyword">end</span> XXTLN_DEBUG_UTIL;
/
</code></pre>
<h3 id="heading-add-the-following-line-of-code-to-the-beginning-of-all-of-your-public-functions-and-procedures-within-your-plsql-code">Add the following line of code to the beginning of all of your public functions and procedures within your PL/SQL code</h3>
<pre><code class="lang-sql">xxtln_debug_util.setup;
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Oracle APEX - Set Modal Dialog Title Based Upon Substitution Strings]]></title><description><![CDATA[For more details, check out APEX Instant Tips episode 179.
In APEX Instant Tips episode 31 we discussed how to navigate the APEX DOM, specifically highlighting how to set a modal dialog title. That episode has one missed feature—with un-chained dialo...]]></description><link>https://apexdebug.com/oracle-apex-set-modal-dialog-title-based-upon-substitution-strings</link><guid isPermaLink="true">https://apexdebug.com/oracle-apex-set-modal-dialog-title-based-upon-substitution-strings</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 22 Aug 2025 16:11:12 GMT</pubDate><content:encoded><![CDATA[<p>For more details, check out <a target="_blank" href="https://www.youtube.com/watch?v=Wi8ta28A9oo&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=2">APEX Instant Tips episode 179</a>.</p>
<p>In <a target="_blank" href="https://www.youtube.com/watch?v=hv0mIJz5gZA&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP">APEX Instant Tips episode 31</a> we discussed how to navigate the APEX DOM, specifically highlighting how to set a modal dialog title. That episode has one missed feature—with un-chained dialogs the solution set the title of all of the dialogs, not just the new dialog. It also required that you put JavaScript code on every page that required this feature.</p>
<p>A much better solution would be to have the page function like just about everything else in APEX—allow the title to simply use substitution strings. If the title is defined as</p>
<p><strong>Details for &amp;P123_FIRST_NAME.</strong></p>
<p>Then when you open the modal dialog for Marwa Chouchene the page title should be</p>
<p><strong>Details for Marwa</strong></p>
<p>Of course, it does not behave this way by default. And Marwa didn’t like that at all. So what does an APEX expert do when APEX lacks a feature? She writes a plug-in!</p>
<p>Below is a link to Marwa’s plug-in.</p>
<p><a target="_blank" href="https://github.com/ainielse/set-modal-title/">https://github.com/ainielse/set-modal-title/</a></p>
<p>All you have to do is install the plug-in in your application and add it to Page 0 as an on-load dynamic action. Then, whenever you have a modal dialog title with a substitution string the plug-in will do take care of it.</p>
<p>The code for the plug-in is simple:</p>
<pre><code class="lang-sql">function f_render (
    p_dynamic_action   in   apex_plugin.t_dynamic_action,
    p_plugin           in   apex_plugin.t_plugin
) return apex_plugin.t_dynamic_action_render_result as

    l_result            apex_plugin.t_dynamic_action_render_result;
    l_title             varchar2(4000);
    l_title_after_subs  varchar2(4000);
    l_page_mode         varchar2(500);

<span class="hljs-keyword">begin</span>

    <span class="hljs-keyword">select</span> page_title, page_mode
      <span class="hljs-keyword">into</span> l_title, l_page_mode
      <span class="hljs-keyword">from</span> apex_application_pages aap
      <span class="hljs-keyword">where</span> application_id = :APP_ID
        <span class="hljs-keyword">and</span> page_id = :APP_PAGE_ID;

    if l_page_mode != 'Normal' then

        <span class="hljs-comment">-- Do substitutions. </span>
        <span class="hljs-comment">-- <span class="hljs-doctag">NOTE:</span> Modal dialog titles automatically escape HTML. Hence, we need to set p_escape to FALSE.</span>
        l_title_after_subs := apex_plugin_util.replace_substitutions(p_value =&gt; l_title, p_escape =&gt; false); 

        <span class="hljs-comment">-- see if page title has a substution string</span>
        if l_title != l_title_after_subs then

            l_result.javascript_function := 'function () { 
                                                      apex.util.getTopApex().jQuery(".ui-dialog-content").last().dialog("option", "title", ' ||
                                                        apex_javascript.add_value( p_value       =&gt; l_title_after_subs,
                                                                                   p_add_comma   =&gt; false
                                                        ) ||
                                                       '); }';
        <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

    if l_result.javascript_function is null then
        l_result.javascript_function := 'function () { null; }';
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">if</span>;

    return l_result;
exception
    when others then apex_debug.message('Unexpected error in SET_DIALOG_TITLE_SS: %s', sqlerrm);
        return l_result;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>The part that we missed in episode 31 related to un-chained dialogs is here:</p>
<pre><code class="lang-javascript">apex.util.getTopApex().jQuery(<span class="hljs-string">".ui-dialog-content"</span>).last().dialog
</code></pre>
<p>We added .last() to ensure that we only change the title for the last dialog that was opened.</p>
<p>Really, though, adding this as a plug-in on your global page (page 0) is all it takes.</p>
]]></content:encoded></item><item><title><![CDATA[Realign APEX Interactive Components]]></title><description><![CDATA[It’s fairly common to change the look of an apex component via javascript. This could be for a variety of reasons, some of which we discussed in APEX Instant Tips episode 75. The problem is that sometimes this causes page elements to shift out of ali...]]></description><link>https://apexdebug.com/realign-apex-interactive-components</link><guid isPermaLink="true">https://apexdebug.com/realign-apex-interactive-components</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Wed, 07 May 2025 22:56:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746657587214/ec21efb5-bdb6-4e9c-8e00-a61cc998bc8f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It’s fairly common to change the look of an apex component via javascript. This could be for a variety of reasons, some of which we discussed in <a target="_blank" href="https://www.youtube.com/watch?v=mXgWKMrDdaM&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=99&amp;t=9s">APEX Instant Tips episode 75</a>. The problem is that sometimes this causes page elements to shift out of alignment. This is particularly true for headers of reports (classic reports, interactive reports, interactive grids). (Note: I’m working on the page SEO right there. Next time I search “how do I align my APEX interactive report headings” I want to find this page and realize that I should already know the answer.)</p>
<p>You can get more details on this topic by watching <a target="_blank" href="https://www.youtube.com/watch?v=mXgWKMrDdaM&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=99&amp;t=9s">APEX Instant Tips episode 75</a>.</p>
<p>You can see what I’m talking about in the image below. I’ve made an exaggerated example; the Job column does not match the heading.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746658237269/10510a94-18ba-4bfe-9d3e-f29ba696c290.png" alt class="image--center mx-auto" /></p>
<p>To solve the problem, you need to trick APEX into thinking the page has been resized. Whenever you do something to dynamically format the page, typically via a dynamic action, you can add an additional line of javascript code (or an additional true action to run this line of code).</p>
<pre><code class="lang-javascript">$(<span class="hljs-built_in">window</span>).trigger(<span class="hljs-string">"apexwindowresized"</span>);
</code></pre>
<p>That will trigger the event that tells APEX to realign things…like Interactive Report columns.</p>
]]></content:encoded></item><item><title><![CDATA[Recentering Oracle APEX Maps]]></title><description><![CDATA[If you have worked with Oracle APEX Map regions you have likely found that under Map Attributes > Initial Position and Zoom > Type you can set “how” you want the map to initially render. By “how” I mean what location in the world you want it to go to...]]></description><link>https://apexdebug.com/recentering-oracle-apex-maps</link><guid isPermaLink="true">https://apexdebug.com/recentering-oracle-apex-maps</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Wed, 07 May 2025 22:29:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746656922930/ca837944-0722-4a6b-b72b-4c55ca6a92d2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have worked with Oracle APEX Map regions you have likely found that under Map Attributes &gt; Initial Position and Zoom &gt; Type you can set “how” you want the map to initially render. By “how” I mean what location in the world you want it to go to and what zoom level you want it to be at. If you have spent any time at all working with maps you may think this feature is broken. I have come to realize that APEX just <em>really</em> means <strong>initial</strong>. It doesn’t matter what you do (declaratively) on or to the page, once you have visited the page in an APEX session, that initial position is set. You can refresh the report, reset the page cache in the URL, reset everything you can think of, but after that first (initial) visit to the page, the map is going to keep recentering and zooming on that initial spot.</p>
<p>The sample maps application has a Search and Show page that manages to recenter the map. It uses about 25 lines of javascript (in an after refresh dynamic action) to make it happen:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> lMapRegion   = apex.region(<span class="hljs-string">"airport-map-region"</span>),
    <span class="hljs-comment">// important: Use the layer name exactly as specified in the "name" attribute in Page Designer</span>
    lLayerId     = lMapRegion.call(<span class="hljs-string">"getLayerIdByName"</span>, <span class="hljs-string">"Airport"</span>),
    lCurrentZoom = lMapRegion.call(<span class="hljs-string">"getMapCenterAndZoomLevel"</span>).zoom,
    lAirportId   = apex.item(<span class="hljs-string">"P121_ID"</span>).getValue(),
    lFeature     = lMapRegion.call(<span class="hljs-string">"getFeature"</span>, lLayerId, lAirportId ),
    lPosition;

<span class="hljs-keyword">if</span> ( lFeature.geometry ) {
    lPosition    = lFeature.geometry.coordinates;

    <span class="hljs-comment">// close all Info Windows, which might currently be open</span>
    lMapRegion.call( <span class="hljs-string">"closeAllInfoWindows"</span> );

    <span class="hljs-comment">// focus the map to the chosen feature</span>
    lMapRegion.call( <span class="hljs-string">"setCenter"</span>, lPosition );

    <span class="hljs-comment">// if the current zoom level is below 8, zoom in. Otherwise do nothing.</span>
    <span class="hljs-keyword">if</span> ( lCurrentZoom &lt; <span class="hljs-number">12</span> ) {
        lMapRegion.call( <span class="hljs-string">"setZoomLevel"</span>, <span class="hljs-number">12</span> );
    }
    <span class="hljs-built_in">setTimeout</span>( <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{lMapRegion.call( <span class="hljs-string">"displayPopup"</span>, <span class="hljs-string">"infoWindow"</span>, lLayerId, lAirportId.toString(), <span class="hljs-literal">false</span> )}, <span class="hljs-number">500</span> );
}
</code></pre>
<p>Unfortunately, those 25 lines of javascript don’t work for polygons (and perhaps other layer types). Fortunately, there is a single line of javascript that works for all types:</p>
<pre><code class="lang-javascript">apex.region(<span class="hljs-string">"your_region_static-ic"</span>).reset();
</code></pre>
<p>With that line of javascript you don’t even need to refresh the region. It will refresh and recenter the region. For more details, check out <a target="_blank" href="https://www.youtube.com/watch?v=J_ZLa5AxawA">APEX Instant Tips Episode 154</a>. Many thanks to Marwa for teaching me this tip!</p>
<p>I admit there is a downside to this method. The region will zoom out to a location and then zoom into the location you want. It looks cool the first few times it happens, but gets old after a while. The solution is simple enough that I’m willing to live with it…until I have someone that insists on a different solution.</p>
]]></content:encoded></item><item><title><![CDATA[Oracle APEX Chart Colors]]></title><description><![CDATA[I’ve been on a mission to make the applications we build look clean and professional. I’ve always been on a mission to make them maintainable. I also want them to be easily “branded” for different customers. In APEX, one way to help with all of this ...]]></description><link>https://apexdebug.com/oracle-apex-chart-colors</link><guid isPermaLink="true">https://apexdebug.com/oracle-apex-chart-colors</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Wed, 30 Apr 2025 18:29:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746037627298/cc09b4fb-0178-4c83-9304-68e4b33d8631.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been on a mission to make the applications we build look clean and professional. I’ve always been on a mission to make them maintainable. I also want them to be easily “branded” for different customers. In APEX, one way to help with all of this is to use the colors that are defined in the theme, that is, the colors that are in the color palette of Theme Roller. Importantly, you don’t want to copy the hex values, you want to reference the values either by using classes are by referencing the css variables. You can find these in the Universal Theme sample application or at this URL: <a target="_blank" href="https://apex.oracle.com/pls/apex/r/apex_pm/ut/css-variables">https://apex.oracle.com/pls/apex/r/apex_pm/ut/css-variables</a></p>
<p>Unfortunately, APEX charts do not do this by default. I found this <a target="_blank" href="https://pretius.com/blog/chart-patterns-oracle-apex/">great blog post</a> by Tomáš Kucharzyk that describes how to change the colors of APEX charts and how to do a lot of other very cool things. It does not, however, mention how to tie it to the APEX CSS variables. After reviewing the blog post, though, I came up with the following two blocks of code. Both go in the Initialization Javascript Function of the chart attributes.</p>
<p>This code works for some chart types (multi-series charts):</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">options</span>) </span>{
  <span class="hljs-keyword">let</span> listOfPatterns = [    
    <span class="hljs-string">"smallChecker"</span>, <span class="hljs-string">"smallTriangle"</span>, <span class="hljs-string">"smallCrosshatch"</span>, <span class="hljs-string">"smallDiagonalLeft"</span>,
    <span class="hljs-string">"smallDiamond"</span>, <span class="hljs-string">"smallDiagonalRight"</span>, <span class="hljs-string">"largeChecker"</span>, <span class="hljs-string">"largeCrosshatch"</span>, <span class="hljs-string">"largeDiagonalLeft"</span>, <span class="hljs-string">"largeDiagonalRight"</span>, <span class="hljs-string">"largeDiamond"</span>,
    <span class="hljs-string">"largeTriangle"</span>, <span class="hljs-string">"auto"</span>
  ];

  <span class="hljs-keyword">let</span> listOfColors = [
    <span class="hljs-string">"--u-color-1"</span>, <span class="hljs-string">"--u-color-2"</span>, <span class="hljs-string">"--u-color-3"</span>,<span class="hljs-string">"--u-color-4"</span>, <span class="hljs-string">"--u-color-5"</span>, <span class="hljs-string">"--u-color-6"</span>,<span class="hljs-string">"--u-color-7"</span>, <span class="hljs-string">"--u-color-8"</span>, <span class="hljs-string">"--u-color-9"</span>,
    <span class="hljs-string">"--u-color-10"</span>, <span class="hljs-string">"--u-color-11"</span>, <span class="hljs-string">"--u-color-12"</span>,<span class="hljs-string">"--u-color-13"</span>, <span class="hljs-string">"--u-color-14"</span>,<span class="hljs-string">"--u-color-1"</span>, <span class="hljs-string">"--u-color-2"</span>, <span class="hljs-string">"--u-color-3"</span>,<span class="hljs-string">"--u-color-4"</span>, <span class="hljs-string">"--u-color-5"</span>, <span class="hljs-string">"--u-color-6"</span>,<span class="hljs-string">"--u-color-7"</span>, <span class="hljs-string">"--u-color-8"</span>, <span class="hljs-string">"--u-color-9"</span>,
    <span class="hljs-string">"--u-color-10"</span>, <span class="hljs-string">"--u-color-11"</span>, <span class="hljs-string">"--u-color-12"</span>,<span class="hljs-string">"--u-color-13"</span>, <span class="hljs-string">"--u-color-14"</span>
    ];

  options.dataFilter = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">data</span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; data.series.length; i++) {
      <span class="hljs-keyword">let</span> color = getComputedStyle(<span class="hljs-built_in">document</span>.documentElement).getPropertyValue(listOfColors[i % listOfColors.length]).trim();  
      data.series[i].color = color;
      data.series[i].borderColor = color;
      <span class="hljs-comment">//data.series[i].pattern = listOfPatterns[i % listOfPatterns.length];</span>
    }
    <span class="hljs-keyword">return</span> data;
};
<span class="hljs-keyword">return</span> options;
}
</code></pre>
<p>This works for single series charts:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">options</span>) </span>{
  <span class="hljs-keyword">let</span> listOfPatterns = [    
    <span class="hljs-string">"smallChecker"</span>, <span class="hljs-string">"smallTriangle"</span>, <span class="hljs-string">"smallCrosshatch"</span>, <span class="hljs-string">"smallDiagonalLeft"</span>,
    <span class="hljs-string">"smallDiamond"</span>, <span class="hljs-string">"smallDiagonalRight"</span>, <span class="hljs-string">"largeChecker"</span>, <span class="hljs-string">"largeCrosshatch"</span>, <span class="hljs-string">"largeDiagonalLeft"</span>, <span class="hljs-string">"largeDiagonalRight"</span>, <span class="hljs-string">"largeDiamond"</span>,
    <span class="hljs-string">"largeTriangle"</span>, <span class="hljs-string">"auto"</span>
  ];

  <span class="hljs-keyword">let</span> listOfColors = [
    <span class="hljs-string">"--u-color-1"</span>, <span class="hljs-string">"--u-color-2"</span>, <span class="hljs-string">"--u-color-3"</span>,<span class="hljs-string">"--u-color-4"</span>, <span class="hljs-string">"--u-color-5"</span>, <span class="hljs-string">"--u-color-6"</span>,<span class="hljs-string">"--u-color-7"</span>, <span class="hljs-string">"--u-color-8"</span>, <span class="hljs-string">"--u-color-9"</span>,
    <span class="hljs-string">"--u-color-10"</span>, <span class="hljs-string">"--u-color-11"</span>, <span class="hljs-string">"--u-color-12"</span>,<span class="hljs-string">"--u-color-13"</span>, <span class="hljs-string">"--u-color-14"</span>,<span class="hljs-string">"--u-color-1"</span>, <span class="hljs-string">"--u-color-2"</span>, <span class="hljs-string">"--u-color-3"</span>,<span class="hljs-string">"--u-color-4"</span>, <span class="hljs-string">"--u-color-5"</span>, <span class="hljs-string">"--u-color-6"</span>,<span class="hljs-string">"--u-color-7"</span>, <span class="hljs-string">"--u-color-8"</span>, <span class="hljs-string">"--u-color-9"</span>,
    <span class="hljs-string">"--u-color-10"</span>, <span class="hljs-string">"--u-color-11"</span>, <span class="hljs-string">"--u-color-12"</span>,<span class="hljs-string">"--u-color-13"</span>, <span class="hljs-string">"--u-color-14"</span>
    ];

  options.dataFilter = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">data</span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; data.series.length; i++) {
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> j = <span class="hljs-number">0</span>; j &lt; data.series[i].items.length; j++) {
            <span class="hljs-keyword">let</span> color = getComputedStyle(<span class="hljs-built_in">document</span>.documentElement).getPropertyValue(listOfColors[j % listOfColors.length]).trim();  
            data.series[i].items[j].color = color;
            data.series[i].items[j].borderColor = color;
            <span class="hljs-comment">//data.series[i].pattern = listOfPatterns[i % listOfPatterns.length];</span>
        }
      }
    <span class="hljs-keyword">return</span> data;
    };
<span class="hljs-keyword">return</span> options;
}
</code></pre>
<p>In both cases I left in some code from Tomas’s blog to change the fill pattern. You can enable the pattern by uncommenting the commented line in the code above.</p>
<p>The nice thing about this approach is that if you ever change the theme, or the color palette, all of your charts that have this code will automatically have the new colors applied.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746037205525/60999e79-de41-49ac-b1dd-ca15392a4721.png" alt class="image--center mx-auto" /></p>
<p>By changing the color of “Color 1” in the color palette, my chart’s bottom color changes automatically.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746037250776/6d3facbf-11f3-4439-b91a-050f76550581.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-exercise">Exercise</h2>
<p>If you implement the custom patterns found in Tomas’s blog, you can do the same thing by referencing the theme colors via the variables instead of hard coding them in his javascript file. That’s an exercise left to the reader, but it is very straightforward.</p>
]]></content:encoded></item><item><title><![CDATA[Generational Knowledge Inversion]]></title><description><![CDATA[TLDR
If you are over the age of 50 and attempting to impress your viewpoint on someone between the age of 17 and 35 you should first stop talking and listen to their viewpoint. You’ll probably learn something. If you don’t know what TLDR means, this ...]]></description><link>https://apexdebug.com/generational-knowledge-inversion</link><guid isPermaLink="true">https://apexdebug.com/generational-knowledge-inversion</guid><category><![CDATA[knowledge]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Mon, 28 Apr 2025 13:16:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745846157367/5121b256-81a9-4708-bcc2-c7f2b24ecf16.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr">TLDR</h2>
<p>If you are over the age of 50 and attempting to impress your viewpoint on someone between the age of 17 and 35 you should first stop talking and listen to their viewpoint. You’ll probably learn something. If you don’t know what TLDR means, this article is definitely for you.</p>
<h2 id="heading-disclaimer">Disclaimer</h2>
<p>This blog is usually about a topic on which I have some expertise. The content is thoroughly researched. You can generally rely on it. Today’s post is based primarily upon observation and my own understanding of the human condition. There is no science here. Even my terms are at best loosely defined. I’d love to have this theory vetted or refuted by someone with actual expertise and a willingness to do the research required. I anticipate and welcome criticism.</p>
<h2 id="heading-background">Background</h2>
<p>On July 1, 2022 my “<a target="_blank" href="https://youtu.be/Fav1SR4CKuw?list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;t=517">Wisdom of the Week</a>” was about Generational Knowledge Inversion (GKI). (You can <a target="_blank" href="https://youtu.be/Fav1SR4CKuw?list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;t=517">watch the video</a> if you prefer.) GKI is my hypothesis that, for the first time in the existence of humans, parents should anticipate that their children will, at some point in the parents’ lifetime, have more “relevant knowledge” than the parent. I do not have a concrete definition of relevant knowledge. Notionally, though, it is knowledge that, at the current point in time, has some usefulness. For example, I was once very proficient at using a slide rule and I know how to drive a manual transmission car. Knowing the foundational mathematics that govern how a slide rule works (logarithms, exponents, etc.) is certainly relevant knowledge. Perhaps knowing how and why a slide rule works is valuable knowledge. Being good at using one, though, is no longer relevant. Right now, for most Americans, knowing how to drive a manual car is not relevant and in just a few years it will go the way of the slide rule. A little thought reveals a plethora of examples from the last century: using a phone book, the Dewey Decimal System, shorthand, rewinding a cassette tape that has had the magnetic strip get pulled out of it. You get the idea.</p>
<p>I came upon the theory of GKI through my interactions with my own children. Most parents experience a moment when their children exclaim, “You’re not listening to me!” I am no exception. From the outside, though, I witnessed my mother engaged in a disagreement with my daughter (probably 20 years old at the time), and this exclamation came ringing back into my ears. I later suggested to my mother that her grandchildren are, in fact, experts in many of the topics that they discuss…and that she should consider listening to their viewpoint (or expertise) before trying to impress her viewpoint upon them. She might learn something. A couple months later, my mother unexpectedly announced to me, “You know, after spending more time with [my granddaughter] I’ve realized that when we disagree I should just stop talking and listen. I always learn something new.” It was in that moment that I realized that if my 78 year old mother was learning from her grandchildren, I too should stop talking and listen to them.</p>
<h2 id="heading-generational-knowledge-inversion">Generational Knowledge Inversion</h2>
<p>Why is it so hard for us to come to that conclusion? Why don’t we all just instinctively stop talking and listen when our adult children speak? For about 300,000 years humans have roamed the earth. In nearly all of that time, parents, in general, could assume that they possessed more relevant knowledge of the world than their offspring. If we look at a completely unscientific graph of a person’s relevant knowledge it might look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745842575933/7d72a751-c417-4b7d-8bee-a4bb5b04d68f.png" alt class="image--center mx-auto" /></p>
<p>At birth everything we see is new and relevant. We rapidly gain relevant knowledge. As we age, though, most of what we see is something we have already learned. Throughout most of human existence the graph was ever increasing, but the rate of increase was continually lessoning.</p>
<p>A person’s offspring would have almost the identical graph. Perhaps over centuries the total accumulation of knowledge might increase ever so slightly. A parent’s knowledge, however, could reliably be considered to be greater than that the offspring throughout the lifetime of the parent.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745842994324/0e2f2641-f1e8-42ab-9d0c-4ebc1547c724.png" alt class="image--center mx-auto" /></p>
<p>There were exceptions, of course. Perhaps Isaac Newton had more relevant knowledge than his parents. In general, though, all 300,000 years of human evolution have prepared parents to assume they have more relevant knowledge than their children…and their children’s generation.</p>
<p>Three elements of the past contributed to this. The world changed very slowly. Once gained, relevant knowledge rarely became irrelevant during a lifetime. That contributed to a graph that was ever increasing. Secondly, communities were small and local. What was relevant was typically relevant throughout the community and learned from the community. Limited travel and intrinsic commonality within a community dictated that a parent’s experiences would be directly relevant to the life of their children. Thirdly, the information available to a child generation was essentially the same that was available to the parental generation. This began to change with the printing press, but the change was small. And, typically, the older generation had access to, and the facility to utilize, the additional information before, or at least, at the same time as the younger generation.</p>
<p>For those born after 1960 (myself included), all of these elements have radically changed. Relevant knowledge gained in our youth rapidly dissipates into anachronistic trivia. New knowledge is abundant. The boundaries of space are overcome by instant communications with virtually anyone on the planet. Our children grew up in the information age, giving them an immense edge in accessing and assimilating relevant knowledge. My children learned elements of physics, chemistry, and biology in high school that were unknown while I was in college. The graph has shifted:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745843846991/125788cc-0113-405f-95d9-014b63538823.png" alt class="image--center mx-auto" /></p>
<p>Interestingly, I postulate that members of my specific generation, born between 1960 and 1980, are in the unique position that we may never surpass our parents, but our children will surpass both their parents and their grandparents.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745844303382/214d549b-67da-49db-8085-274ded14ca2c.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-pseudoscience">Pseudoscience</h2>
<p>Here we have all the makings of a full-on pseudoscientific article: multiple graphs without any data to support them, corresponding text to support the pretty graphs but still lacking any data. Since proposing this theory nearly 3 years ago, I have had many people ask me, or more accurately, say to me, “But, you don’t really think your children know more than you, right?” My children are now ages 23, 26, and 30. On any topic that is not my area of expertise, I state emphatically: Yes, my children likely have more relevant knowledge than I do. That doesn’t mean they <em>always</em> have more relevant knowledge than I do. I go into most conversations, however, knowing that they may, or likely do have more relevant knowledge than I. Interestingly, I have found that even in areas where I have significant expertise, my children often contribute novel ideas and insights to the topic. I have been primarily discussing a parent-child relationship, but the same holds true simply across generations. I work with many people a generation or two younger than me. I consistently find that I should listen to their ideas and viewpoint before espousing my own. It occasionally saves me some embarrassment and I invariably learn something.</p>
<p>Whether this pseudoscience is accurate or not is somewhat irrelevant. My own experience is that I can shift my blue line (in the graph above) by listening more (and first). I’m not suggesting that I can keep my blue line above the red line, but I hope I can at least keep it rising instead of falling.</p>
<p>Finally, I admit that I am wildly imperfect at this. 300,000 years of evolution tug at me to think that I will always have more relevant knowledge than those in subsequent generations. Recognizing the fallacy, though, is a start.</p>
<p>TLDR = Too Long, Didn’t Read</p>
]]></content:encoded></item><item><title><![CDATA[Oracle APEX Region Template Best Practices]]></title><description><![CDATA[Others may understand this intuitively, but despite looking, I have not found a concise guide related to when to use each template type. And, in fact, this post doesn’t cover all of the possibilities. In order to keep things short, I’m going to conce...]]></description><link>https://apexdebug.com/oracle-apex-region-template-best-practices</link><guid isPermaLink="true">https://apexdebug.com/oracle-apex-region-template-best-practices</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 25 Apr 2025 15:08:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745592752571/3aa09715-a220-471a-a906-420154438277.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Others may understand this intuitively, but despite looking, I have not found a concise guide related to when to use each template type. And, in fact, this post doesn’t cover all of the possibilities. In order to keep things short, I’m going to concentrate on the just the three region templates listed in the subtitle. The same principals apply, though.</p>
<h2 id="heading-tldr">TLDR</h2>
<p>Use “Blank with Attributes” as a parent region to arrange subregions.</p>
<p>Use “Blank with Attributes (No Grid)” almost never :) If you wish to manually arrange everything in the region, this is the region type for you.</p>
<p>Use “Standard” for just about everything else (on a non-modal page)…with lots of exceptions. Most of the other templates have specific use cases and their names make it clear why you would use them.</p>
<p>“But wait!” you exclaim, “What if I don’t want the title bar on Standard region? Shouldn’t I use ‘Blank with Attributes’?” This is the TLDR, you’ll have to keep reading for answers.</p>
<p>But…no, you should use a variation of the Standard region. Again, you’ll have to keep reading.</p>
<h2 id="heading-the-apex-grid">The APEX Grid</h2>
<p>APEX uses a 12 column grid to arrange things on a page. 12 is divisible by 1, 2, 3, 4, and 6. This provides good symmetry for arranging up to 6 components (or all 12) across the page or within a region, with the notable exceptions of 5 and 7 components. For this reason I try to avoid 5 and 7 components on a single row. I’m specifically using the word “component” because this applies to regions, items, buttons, etc.</p>
<p>In a similar fashion, most regions templates support a grid. When the region template supports a grid it will also have 12 columns. Hence, if you have two regions on the same row, you will have 24 columns in which to arrange components within those 2 regions. If these two regions have no padding and are each defined as 6 columns wide, the 24 total columns will each represent ½ of the page columns and they will align with the page columns. If the two side-by-side regions have any padding or are not each 6 columns wide, each region’s 12 column grid will be slightly different (or potentially radically different) than the 12 page columns.</p>
<p>The notable exception to this is the “Blank with Attributes (No Grid)” template. This template just has one big open space. You will see that items, buttons, etc, do not show attributes to indicate column, column span, etc. Of course, you can create additional templates that don’t support a grid. In recent versions of APEX this is accomplished in the region template in the “Slots” attribute section.</p>
<p>You can view the grid layout on any page by selecting Show Layout Columns in the developer toolbar.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745588880520/ab1b70c7-5393-4e53-8a5d-f152855f3b8c.png" alt class="image--center mx-auto" /></p>
<p>If you enable this on a region with the Standard template (and at least one component in the region) you can see that the 12 region columns don’t align precisely with the 12 page columns.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745588991157/30a9f8dd-0dd2-4948-ad98-39e859dbbd49.png" alt class="image--center mx-auto" /></p>
<p>With two Standard regions you start to see real differences. (Note: I intentionally did not add an item to the second region to demonstrate that you must have a component in the region in order to see the layout columns.)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745589088692/02f5903f-bea8-499a-a0f8-1ac23bbff0b5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-blank-with-attributes">Blank with Attributes</h2>
<p>I recommend using the “Blank with Attributes” region template primarily for arranging other regions. I do not recommend it for containing content of its own.</p>
<h3 id="heading-arranging-content">Arranging Content</h3>
<p>“Blank with Attributes” does not have padding and adds very little to the page. It does not even send the region title to the page (not even as hidden or otherwise not shown text). Hence, this template is ideal as a parent region that is just used to establish the placement on the page for subregions.</p>
<h3 id="heading-not-for-content">Not for Content</h3>
<p>For these same reasons, I do not recommend “Blank with Attributes” for actual content. This region template doesn’t apply the same font (etc) conventions that are used in the “Standard” template. It also does not send the region title to the page, so screen readers and other accessibility features are not well-supported.</p>
<h2 id="heading-standard">Standard</h2>
<p>The “Standard” template should be used for most content. There are a lot of templates that have specific uses, and when it makes sense, you should, of course, use those templates. Regions using the "Standard” template have a variety of features that provide a unified look and feel, abide by theme colors, and work well with the adaptive and accessibility features of modern browsers.</p>
<h3 id="heading-i-dont-want-a-region-title">I Don’t Want a Region Title</h3>
<p>If you are using breadcrumbs, having a region title on the primary region of the page is often redundant and wastes valuable space. In these cases people often default to using “Blank with Attributes.” I prefer to use the “Standard” template with modifications to the template options.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745590833036/cfcaab21-430f-4b4f-89f5-e5f5815f6884.png" alt class="image--center mx-auto" /></p>
<p>The template options above mimic a “Blank with Attributes” template but maintain the “Standard” template’s font and other settings. This provides a unified look and feel to the application. It also continues to provide adaptive and accessibility features of modern browsers. You can play around with these settings to determine exactly how you want to define the template for your main content.</p>
<h3 id="heading-making-things-easy">Making Things Easy</h3>
<p>Once you have settled on the appropriate settings for your main content you want to make it easy for your development team to apply this standard. In order to do this, I recommend that you copy the “Standard” template and name it something explicit, e.g. “Stoked Standard”. Modify the new template and apply the template options you have determined.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745591420604/92470da9-26ad-49a9-a7a1-58835fd19c7f.png" alt class="image--center mx-auto" /></p>
<p>After creating the new template, edit the application theme and change the default templates:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745591535297/998dad88-e38a-4d4b-82c5-a47dd742a0dc.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745591562139/1e0f5891-2bbf-42d2-a980-3ded34ee420f.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745591590130/8a42fc9d-be39-4693-b061-a2d78399c3fb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-of-course">Of Course…</h3>
<p>You will likely still use the original “Standard” template. Make sure your developers know that it’s OK to use it. In fact, you may actually decide you want to leave “Standard” in most of the Region Defaults. The “Component Defaults” are what are used when you use the create page wizard or when you first add a region to the page in the component tree pain. The “Region Defaults” are used when you change to a specific region type. Often this is an additional region. Your application standards my drive slightly different default settings. The good news is that, by having defined templates, getting the right settings is as easy as selecting a template—just a click away.</p>
<h3 id="heading-one-downside">One Downside</h3>
<p>There is one downside. Because you have copied the "Standard” template, when you upgrade APEX and then you refresh the theme in your application, the new “Standard” template will not be applied to your copy, which is now your default template. Generally, all you have to do is copy each block of HTML from the newly refreshed “Standard” template to your copy.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By giving a little thought to your page layout and template choices early in the design process you can save time for the life of the project. The APEX team puts a lot of effort into providing a unified look and feel, implementing accessibility, and generally applying modern best practices. With just a little upfront work all of our applications can benefit from their efforts.</p>
]]></content:encoded></item><item><title><![CDATA[Improving Oracle SQL Query with Connect By Performance]]></title><description><![CDATA[I recently had a hierarchical query that was slow. I could run the same query, un-nested, quickly, but when I added in the “connect by” things slowed down. I had the same issue using a recursive common table expression (CTE or “with clause”). Either ...]]></description><link>https://apexdebug.com/improving-oracle-sql-query-with-connect-by-performance</link><guid isPermaLink="true">https://apexdebug.com/improving-oracle-sql-query-with-connect-by-performance</guid><category><![CDATA[Oracle]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Tue, 22 Apr 2025 14:48:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745333238755/2dcc137d-0323-4888-a199-9338da9b5a57.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently had a hierarchical query that was slow. I could run the same query, un-nested, quickly, but when I added in the “connect by” things slowed down. I had the same issue using a recursive common table expression (CTE or “with clause”). Either way, the query took 104 or more seconds to run. Below is an example of the query:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- this is slow</span>
<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span>,
       parent_id,
       sys_connect_by_path(<span class="hljs-keyword">id</span>,<span class="hljs-string">'.'</span>) cb_path,
       <span class="hljs-keyword">connect_by_isleaf</span> is_leaf,
       c1
       ...
       c200,
       (<span class="hljs-keyword">select</span> my_function(c1) <span class="hljs-keyword">from</span> dual) my_f,
       ...
  <span class="hljs-keyword">from</span> my_table
  <span class="hljs-keyword">start</span> <span class="hljs-keyword">with</span> parent_id <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>  <span class="hljs-comment">-- ** set parent id column</span>
  <span class="hljs-keyword">connect</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">nocycle</span> <span class="hljs-keyword">prior</span> <span class="hljs-keyword">id</span> = parent_id
  <span class="hljs-keyword">order</span> <span class="hljs-keyword">siblings</span> <span class="hljs-keyword">by</span> seq_no
</code></pre>
<p>Looking at the explain plan and a trace, I found that the database was reading the entire table many times. I guessed that I might be able to get the database to do a single read of the table if I could limit the “connect by” portion to use only an index, and then, once the hierarchy is established, join in the rest of the data in a single pass.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- this is much faster</span>
<span class="hljs-comment">-- assumes you have a single compound index on id,parent_id</span>
<span class="hljs-comment">-- and another unique key index on id</span>
<span class="hljs-keyword">with</span> cb_data <span class="hljs-keyword">as</span>
(<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span>,
       parent_id,
       sys_connect_by_path(<span class="hljs-keyword">id</span>,<span class="hljs-string">'.'</span>) cb_path,
       <span class="hljs-keyword">connect_by_isleaf</span> is_leaf
  <span class="hljs-keyword">from</span> my_table
  <span class="hljs-keyword">start</span> <span class="hljs-keyword">with</span> parent_id <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>  <span class="hljs-comment">-- ** set parent id column</span>
  <span class="hljs-keyword">connect</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">nocycle</span> <span class="hljs-keyword">prior</span> <span class="hljs-keyword">id</span> = parent_id
  <span class="hljs-keyword">order</span> <span class="hljs-keyword">siblings</span> <span class="hljs-keyword">by</span> seq_no
)
<span class="hljs-keyword">select</span> <span class="hljs-keyword">id</span>,
       parent_id,
       cb_path,
       is_leaf,
       c1
       ...
       c200,
       (<span class="hljs-keyword">select</span> my_function(c1) <span class="hljs-keyword">from</span> dual) my_f,
       ...
  <span class="hljs-keyword">from</span> cb_data
  <span class="hljs-keyword">join</span> my_table mt <span class="hljs-keyword">on</span> mt.id = cb_data.id
</code></pre>
<p>In the new query, the hierarchical portion, cb_data, contains only 3 columns: id, parent_id, seq_no. I created an index with just these three columns. If I run just the hierarchical portion of the query the database does not read the table at all. It only reads the index. When I run the entire query, the database reads only the index to establish the results of cb_data and then does a single pass of my_table to return the final results. The new query returns the same results but, in my case, runs in 4 seconds. That’s a solid improvement over the original 104 seconds.</p>
<p>The key is to have a single index that contains all of the columns used in the CTE (cb_data above). If you require any additional columns in the CTE to accomplish the connect by, ensure they are in the index. I have not done extensive testing related to the order of the columns. My initial impression is that id, parent_id, seq_no provides good performance.</p>
<p>I hope this helps others as well. Comment to let me know if you try it and have similar results.</p>
]]></content:encoded></item><item><title><![CDATA[Writing Fast Queries in APEX]]></title><description><![CDATA[Tip #9 of my Writing Fast Queries blog post states: Avoid writing one query when it really should be two (or more) queries. It’s worth checking out that blog post in its entirety (as a refresher if you have already seen it).
To briefly recap tip #9, ...]]></description><link>https://apexdebug.com/writing-fast-queries-in-apex</link><guid isPermaLink="true">https://apexdebug.com/writing-fast-queries-in-apex</guid><category><![CDATA[orclapex]]></category><category><![CDATA[oracleapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 21 Mar 2025 16:10:51 GMT</pubDate><content:encoded><![CDATA[<p>Tip #9 of my <a target="_blank" href="https://apexdebug.com/writing-fast-queries">Writing Fast Queries</a> blog post states: Avoid writing one query when it really should be two (or more) queries. It’s worth checking out that blog post in its entirety (as a refresher if you have already seen it).</p>
<p>To briefly recap tip #9, this is bad:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> EMPNO,
       ENAME,
       JOB,
       MGR,
       HIREDATE,
       SAL,
       COMM,
       DEPTNO
  <span class="hljs-keyword">from</span> EMP
  <span class="hljs-keyword">where</span> deptno = nvl(:P5_DEPTNO, deptno)
    <span class="hljs-keyword">and</span> <span class="hljs-keyword">instr</span>(<span class="hljs-keyword">upper</span>(ename), <span class="hljs-keyword">upper</span>(nvl(:P5_NAME, ename))) &gt; <span class="hljs-number">0</span>
</code></pre>
<p>It’s bad because the where clause is trying to do too much. It will be parsed once and the execution plan will be cached and re-used. This should be multiple queries depending on whether or not the bind values are null.</p>
<p>One way to accomplish this is to use the APEX report feature “Function Body returning SQL Query”. You can then write a function that builds the query based upon the bind variables.</p>
<pre><code class="lang-sql">return q'~
<span class="hljs-keyword">select</span> EMPNO,
       ENAME,
       JOB,
       MGR,
       HIREDATE,
       SAL,
       COMM,
       DEPTNO
  <span class="hljs-keyword">from</span> EMP
  <span class="hljs-keyword">where</span> <span class="hljs-number">1</span>=<span class="hljs-number">1</span> 
  ~<span class="hljs-string">' ||
  case when :P5_DEPTNO is not null then '</span> <span class="hljs-keyword">and</span> deptno = :P5_DEPTNO<span class="hljs-string">'
       else null
       end ||
  case when :P5_NAME is not null then '</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">instr</span>(<span class="hljs-keyword">upper</span>(ename), <span class="hljs-keyword">upper</span>(:P5_NAME)) &gt; <span class="hljs-number">0</span><span class="hljs-string">'
       else null
       end
       ;</span>
</code></pre>
<p>Of course, this means those extra lines of code over and over, remembering that exact syntax each time you do it. And, as has been pointed out to me, if you make a mistake the “Check if it compiles” button doesn’t catch it at design time because the case statement won’t return anything.</p>
<p>Wouldn’t it be great if you could do something like this?</p>
<pre><code class="lang-sql">return 
    get_query(
        p_base_query        =&gt; 
q'~<span class="hljs-keyword">select</span> EMPNO,
       ENAME,
       JOB,
       MGR,
       HIREDATE,
       SAL,
       COMM,
       DEPTNO
  <span class="hljs-keyword">from</span> EMP ~<span class="hljs-string">',
    p_columns_aliases   =&gt; apex_t_varchar2('</span>ENAME<span class="hljs-string">','</span>DEPTNO<span class="hljs-string">'),
    p_page_items        =&gt; apex_t_varchar2('</span>P5_NAME<span class="hljs-string">','</span>P5_DEPTNO<span class="hljs-string">'),
    p_comp_method       =&gt; apex_t_varchar2('</span>i<span class="hljs-string">','</span>e<span class="hljs-string">')
    );</span>
</code></pre>
<p>You just have to call “get_query” and pass in a list of columns, bind variable names, and if you want (i)nstring, (e)qual, (l)ike (not shown), or (in). You can do this for any number of columns and bind variables. The base query can be super complicated. The bind variables are handled by the get_query function. Oh, and it would be great if the “Check if it compiles” code would check the inputs of p_column_aliases, etc., as well.</p>
<p>Well, here it is, get_query below does just that. Note: Please put this into a package—don’t use it as a stand-alone function.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">function</span> get_query(
    p_base_query        <span class="hljs-keyword">in</span> <span class="hljs-keyword">clob</span>,
    p_columns_aliases   <span class="hljs-keyword">in</span> apex_t_varchar2,
    p_page_items        <span class="hljs-keyword">in</span> apex_t_varchar2,
    p_comp_method       <span class="hljs-keyword">in</span> apex_t_varchar2   <span class="hljs-comment">-- comparison method: i = instring, e = equal (=), l = like, in = in (select)</span>
    ) <span class="hljs-keyword">return</span> <span class="hljs-keyword">clob</span>
<span class="hljs-keyword">as</span>
l_final_query       <span class="hljs-keyword">clob</span>;
l_lf                varchar2(32) := chr(10);
l_comp_method       varchar2(32);
<span class="hljs-keyword">begin</span>

    l_final_query := <span class="hljs-string">'select * from ('</span> || l_lf || p_base_query || l_lf ||<span class="hljs-string">') where 1=1 '</span>;

    if p_columns_aliases.count != p_page_items.count or p_columns_aliases.count != p_comp_method.count then
        raise_application_error(-20001, 'p_columns_aliases, p_page_items, p_comp_method <span class="hljs-keyword">do</span> <span class="hljs-keyword">not</span> have the same <span class="hljs-built_in">number</span> <span class="hljs-keyword">of</span> elements.<span class="hljs-string">');
    end if;

    for i in 1..p_columns_aliases.count loop

        if v(p_page_items(i)) is not null or v('</span>APP_ID<span class="hljs-string">') = 4000 then
            l_comp_method := trim(lower(p_comp_method(i)));

            if l_comp_method not in ('</span>i<span class="hljs-string">','</span>e<span class="hljs-string">','</span>l<span class="hljs-string">','</span><span class="hljs-keyword">in</span><span class="hljs-string">') then
                raise_application_error(-20001, '</span>p_comp_method must be i, e, l, <span class="hljs-keyword">or</span> <span class="hljs-keyword">in</span><span class="hljs-string">');
            end if;

            l_final_query := l_final_query || l_lf || '</span> <span class="hljs-keyword">and</span> <span class="hljs-string">'
                             || case 
                                    when l_comp_method = '</span>e<span class="hljs-string">' then '</span><span class="hljs-keyword">upper</span>(<span class="hljs-string">' || p_columns_aliases(i) || '</span>) = <span class="hljs-keyword">upper</span>(:<span class="hljs-string">' || p_page_items(i) ||'</span>)<span class="hljs-string">'
                                    when l_comp_method = '</span>i<span class="hljs-string">' then '</span><span class="hljs-keyword">instr</span>(<span class="hljs-keyword">upper</span>(<span class="hljs-string">'||p_columns_aliases(i) || '</span>), <span class="hljs-keyword">upper</span>(:<span class="hljs-string">' || p_page_items(i) || '</span>)) &gt; <span class="hljs-number">0</span> <span class="hljs-string">'
                                    when l_comp_method = '</span>l<span class="hljs-string">' then '</span><span class="hljs-keyword">upper</span>(<span class="hljs-string">' || p_columns_aliases(i) || '</span>) <span class="hljs-keyword">like</span> <span class="hljs-keyword">upper</span>(<span class="hljs-string">''</span>%<span class="hljs-string">''</span> || :<span class="hljs-string">' || p_page_items(i) ||'</span>|| <span class="hljs-string">''</span>%<span class="hljs-string">''</span> )<span class="hljs-string">'
                                    when l_comp_method = '</span><span class="hljs-keyword">in</span><span class="hljs-string">' then p_columns_aliases(i) || '</span> <span class="hljs-keyword">in</span> (<span class="hljs-keyword">select</span> bvt.column_value <span class="hljs-keyword">from</span> apex_string.split(:<span class="hljs-string">' || p_page_items(i) || '</span>,<span class="hljs-string">''</span>:<span class="hljs-string">''</span>) bvt)<span class="hljs-string">'
                                end;
        end if;
    end loop;

    return l_final_query;
end get_query;
/</span>
</code></pre>
<p>Please let me know in the comments if you have any comments, suggestions, or just if you have used this.</p>
]]></content:encoded></item><item><title><![CDATA[Mark This Down]]></title><description><![CDATA[You may have heard me say it before, but I like writing documentation. That includes both help text within an application and the comments describing how to use APIs. I don’t, however, like writing documentation that never gets read. Or worse still, ...]]></description><link>https://apexdebug.com/mark-this-down</link><guid isPermaLink="true">https://apexdebug.com/mark-this-down</guid><category><![CDATA[Oracle]]></category><category><![CDATA[#oracle-apex]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[PL/SQL]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 31 Jan 2025 17:15:38 GMT</pubDate><content:encoded><![CDATA[<p>You may have heard me say it before, but I like writing documentation. That includes both help text within an application and the comments describing how to use APIs. I don’t, however, like writing documentation that never gets read. Or worse still, documentation that gets written once but is never updated as things change, so it’s wrong. And documentation that is redundant, I also have no time for that.</p>
<p>Given that, I recently decided to write all of my package specification (PKS) comments in a way that allows me to easily convert them to HTML in real time. The documentation is always up to date—taken directly from the PKS. It has a bonus that it makes it extremely easy to copy and paste example code from the documentation into a code editor.</p>
<p>Hat tip to the APEX development team. I noticed that the APEX documentation appears to match the APEX package specifications—except that the APEX package specifications appear to use Markdown syntax.</p>
<p>Below is my process.</p>
<h2 id="heading-write-your-comments-as-markdown">Write your comments as Markdown</h2>
<p>It’s really that easy, but defining a few standards will improve your results. An example will help. <strong>NOTE</strong>: In the example below I am using three standard ticks (‘) before and after example code. But in reality you should use <strong>three back ticks</strong> (`) or three tildas (~). If I use three back ticks, though, it messes up Hashnode’s formatting. So…just remember that those three ticks should be three back ticks.</p>
<pre><code class="lang-sql"><span class="hljs-comment">--==============================================================================</span>
<span class="hljs-comment">-- ### Globals</span>
<span class="hljs-comment">--==============================================================================</span>

<span class="hljs-comment">-- **gc_excluded_schemas**</span>
<span class="hljs-comment">-- provides a list of schemas that cannot be searched</span>
gc_excluded_schemas         apex_t_varchar2 := apex_t_varchar2( 'XDB', 'SYSAUX','CTXSYS','MDSYS','SYSTEM');

<span class="hljs-comment">-- **gc_excluded_tables_views**</span>
<span class="hljs-comment">-- provides a list of tables and views that cannot be searched (regardless of schema)</span>
<span class="hljs-comment">-- ~~~sql</span>
gc_excluded_tables_views    apex_t_varchar2 := apex_t_varchar2('XXIVS_SEARCH','XXIVS_SEARCH_APP', 'XXIVS_RESULT','XXIVS_RESULT_LINK', 'XXIVS_SEARCH_SESSION_V', 'XXIVS_RESULT_SESSION_V','XXIVS_RESULT_LINK_SESSION_V', 'XXIVS_LINK_V');
<span class="hljs-comment">-- ~~~</span>
<span class="hljs-comment">-- **gc_excluded_columns**</span>
<span class="hljs-comment">-- provides a list of columns that cannot be searched (regardless of schema or table)</span>
<span class="hljs-comment">-- ~~~sql</span>
gc_excluded_columns         apex_t_varchar2 := apex_t_varchar2('PASSWORD','PUBLIC_KEY','PRIVATE_KEY','P__1','P__2','P__3'); <span class="hljs-comment">-- 'P__1','P__2','P__3' are reserved for use inside this pkg.</span>
<span class="hljs-comment">-- ~~~</span>
<span class="hljs-comment">-- **gc_search_data_types**</span>
<span class="hljs-comment">-- indicates the data types available to be searched</span>
<span class="hljs-comment">-- ~~~sql</span>
gc_search_data_types        apex_t_varchar2 := apex_t_varchar2('VARCHAR2', 'CHAR','NVARCHAR2', 'CLOB', 'NCLOB', 'NUMBER'); <span class="hljs-comment">-- ***<span class="hljs-doctag">TODO:</span>  'BLOB'</span>
<span class="hljs-comment">-- ~~~</span>
<span class="hljs-comment">-- ### Statuses</span>
<span class="hljs-comment">-- ~~~sql</span>
gc_initiated                varchar2(200) := 'Initiated';
gc_in_progress              varchar2(200) := 'In Progress';
gc_complete                 varchar2(200) := 'Complete';
gc_error                    varchar2(200) := 'Error';
gc_killed                   varchar2(200) := 'Killed';
<span class="hljs-comment">-- ~~~</span>
<span class="hljs-comment">--==============================================================================</span>
<span class="hljs-comment">-- ### function excluded_schemas</span>
<span class="hljs-comment">--</span>
<span class="hljs-comment">-- This function returns the value of the constant gc_excluded_schemas so that it can be used in SQL.</span>
<span class="hljs-comment">-- </span>
<span class="hljs-comment">-- example:</span>
<span class="hljs-comment">-- ~~~sql</span>
<span class="hljs-comment">--  select distinct owner d, owner r</span>
<span class="hljs-comment">--    from all_tables</span>
<span class="hljs-comment">--    where owner not in (select column_value from table (xxivs_vast_search.excluded_schemas) )</span>
<span class="hljs-comment">--    order by owner</span>
<span class="hljs-comment">-- ~~~</span>
<span class="hljs-comment">--==============================================================================</span>
function excluded_schemas return apex_t_varchar2 deterministic;
</code></pre>
<p>I made several decisions on how to format my output. These decisions go hand in hand with the PL/SQL function that I will provide below. You may decide on different standards.</p>
<ol>
<li><p>In my output I only keep lines that start with "-- " (that’s dash dash space) or “gc” (for global constant). The space at the end of "-- " allows me to control formatting. A comment without the space allows me to add a blank line in the PKS but not in the HTML output. I also like to provide the values of global constants as part of the HTML output and my standard is to prefix these constants gc_.</p>
</li>
<li><p>I “throw away” the leading "-- " of every line.</p>
</li>
<li><p>I use ## and ### to indicate sections, e.g. constant sections, procedure names, etc.</p>
</li>
<li><p>I use ** around some things (within the comments) to make them “strong.”</p>
</li>
<li><p>I use surround examples with … well … take a like at the code block above. It’s super hard to give the example here because Hashnode wants to use convert the example into a code block!</p>
</li>
</ol>
<h2 id="heading-use-a-tiny-function-to-convert-your-comments-to-html">Use a tiny function to convert your comments to HTML</h2>
<p>Below is the function I use:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">function</span> get_pks_comments_html(  
                            p_package_name      <span class="hljs-keyword">in</span> <span class="hljs-built_in">varchar2</span>,
                            p_package_owner     <span class="hljs-keyword">in</span> <span class="hljs-built_in">varchar2</span> <span class="hljs-keyword">default</span> sys_context(<span class="hljs-string">'USERENV'</span>, <span class="hljs-string">'CURRENT_SCHEMA'</span>)) <span class="hljs-keyword">return</span> <span class="hljs-keyword">clob</span> <span class="hljs-keyword">is</span>
l_markdown_clob       <span class="hljs-keyword">clob</span>;
l_line                varchar2(32000);
<span class="hljs-keyword">begin</span>
    <span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> 
        (<span class="hljs-keyword">select</span> <span class="hljs-built_in">text</span>
           <span class="hljs-keyword">from</span> all_source
          <span class="hljs-keyword">where</span> <span class="hljs-keyword">type</span> = <span class="hljs-string">'PACKAGE'</span>
            <span class="hljs-keyword">and</span> <span class="hljs-keyword">name</span> = p_package_name
            <span class="hljs-keyword">and</span> owner = p_package_owner
            <span class="hljs-keyword">and</span> (<span class="hljs-built_in">text</span> <span class="hljs-keyword">like</span> <span class="hljs-string">'-- %'</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">length</span>(<span class="hljs-built_in">text</span>) = <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> <span class="hljs-built_in">text</span> <span class="hljs-keyword">like</span> <span class="hljs-string">'gc\_%'</span> escape <span class="hljs-string">'\'</span> )
           <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> line
        ) <span class="hljs-keyword">loop</span>

        l_line := regexp_replace(line.text, <span class="hljs-string">'^-- '</span>, <span class="hljs-string">''</span>);

        l_markdown_clob := l_markdown_clob || l_line;
    <span class="hljs-keyword">end</span> <span class="hljs-keyword">loop</span>;

    return apex_markdown.to_html(p_markdown =&gt; l_markdown_clob,
                                 p_softbreak =&gt; apex_application.LF,
                                 p_extra_link_attributes =&gt; apex_t_varchar2('target', '_blank'));

<span class="hljs-keyword">end</span> get_pks_comments_html;
/
</code></pre>
<p>This code block implements the standards I mentioned above. It’s worth noting that the value of TEXT in the ALL_SOURCE table always appears to end with a CHR(10)—which is equivalent to apex_application.LF.</p>
<h2 id="heading-add-a-dynamic-content-region-to-your-apex-documentation-page">Add a Dynamic Content region to your APEX documentation page</h2>
<p>In your APEX application you can simply add a region to your application that has the following code:</p>
<pre><code class="lang-sql">return get_pks_comments_html('XXIVS_VAST_SEARCH');
</code></pre>
<p>If you have more than one package, you can have a select list and use the following:</p>
<pre><code class="lang-sql">return xxivs_vast_search.get_pks_comments_html(:P10_PACKAGE_NAME);
</code></pre>
<h2 id="heading-the-results">The results!</h2>
<p>This is how it turns out without any additional formatting.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738331021085/4a1e3fb8-e196-4b68-b50f-bd675673f063.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Full Outer Join - How to Compare Tables]]></title><description><![CDATA[It’s not all that often than I need to do a full out join. Heck, the “Oracle join syntax” doesn’t even support it. I’ve changed to using the JOIN keyword (sometimes referred to as an ANSI join) which makes things so much more readable and understanda...]]></description><link>https://apexdebug.com/full-outer-join-how-to-compare-tables</link><guid isPermaLink="true">https://apexdebug.com/full-outer-join-how-to-compare-tables</guid><category><![CDATA[orclapex]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Database]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Wed, 11 Dec 2024 18:25:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733955093523/1ec9aad0-ff3d-45aa-881e-9004fe2abd5d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It’s not all that often than I need to do a full out join. Heck, the “Oracle join syntax” doesn’t even support it. I’ve changed to using the JOIN keyword (sometimes referred to as an ANSI join) which makes things so much more readable and understandable—and you can do a FULL OUTER JOIN.</p>
<h2 id="heading-tldr">TLDR</h2>
<p>Jump to the solution at the bottom and skip the why and how.</p>
<h2 id="heading-what-is-a-full-outer-join">What is a FULL OUTER JOIN?</h2>
<p>It’s easiest to describe the results. Let’s say we have two tables as shown below:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>CAR</strong></td><td><strong>TRUCK</strong></td></tr>
</thead>
<tbody>
<tr>
<td>ID</td><td>ID</td></tr>
<tr>
<td>MAKE</td><td>MAKE</td></tr>
<tr>
<td>MODEL</td><td>MODEL</td></tr>
<tr>
<td>YEAR</td><td>YEAR</td></tr>
<tr>
<td>CYLINDERS</td><td>CYLINDERS</td></tr>
<tr>
<td>PASSENGERS</td><td></td></tr>
<tr>
<td>TRUNK_SIZE</td><td></td></tr>
<tr>
<td></td><td>BED_LENGTH</td></tr>
<tr>
<td></td><td>TONNAGE</td></tr>
</tbody>
</table>
</div><p>If I want to query the database view USER_TAB_COLS to get the results above, A FULL OUTER JOIN will do it. A FULL OUTER JOIN will get all of the data from two data sets, aligning those that match and putting nulls where they do not…but it’s not super easy.</p>
<h2 id="heading-how-to-get-there">How to Get There</h2>
<p>If I run the following query:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> column_name
  <span class="hljs-keyword">from</span> user_tab_cols
  <span class="hljs-keyword">where</span> table_name = <span class="hljs-string">'CAR'</span>
</code></pre>
<p>I will get one row for each the columns for the CAR table: 7 rows. And, of course, if I change the table_name to TRUCK I will get 7 rows, but there will be 2 different rows.</p>
<p>If I want all of the columns that exist in both tables I would use an INNER JOIN:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> t1.column_name car_column, t2.column_name truck_column
  <span class="hljs-keyword">from</span> user_tab_cols t1
  <span class="hljs-keyword">inner</span> <span class="hljs-keyword">join</span> user_tab_cols t2
    <span class="hljs-keyword">on</span> t2.column_name = t1.column_name
    <span class="hljs-keyword">and</span> t2.table_name = <span class="hljs-string">'TRUCK'</span>
  <span class="hljs-keyword">where</span> t1.table_name = <span class="hljs-string">'CAR'</span>
</code></pre>
<p>I will get one row for each the columns that are in <em>both</em> tables: 5 rows.</p>
<p>But what if I want the results of the table above? In this case I need to have one row for every row that is in <em>either</em> table, but if they match, they should be on the same row. This is where the outer join comes in, but it’s not as simple as you might think. If I just change it to a LEFT OUTER JOIN I get the following:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> t1.column_name car_column, t2.column_name truck_column
  <span class="hljs-keyword">from</span> user_tab_cols t1
  <span class="hljs-keyword">left</span> <span class="hljs-keyword">outer</span> <span class="hljs-keyword">join</span> user_tab_cols t2
    <span class="hljs-keyword">on</span> t2.column_name = t1.column_name
    <span class="hljs-keyword">and</span> t2.table_name = <span class="hljs-string">'TRUCK'</span>
  <span class="hljs-keyword">where</span> t1.table_name = <span class="hljs-string">'CAR'</span>
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td>CAR_COLUMN</td><td>TRUCK_COLUMN</td></tr>
</thead>
<tbody>
<tr>
<td>ID</td><td>ID</td></tr>
<tr>
<td>MAKE</td><td>MAKE</td></tr>
<tr>
<td>MODEL</td><td>MODEL</td></tr>
<tr>
<td>YEAR</td><td>YEAR</td></tr>
<tr>
<td>CYLINDERS</td><td>CYLINDERS</td></tr>
<tr>
<td>PASSENGERS</td><td></td></tr>
<tr>
<td>TRUNK_SIZE</td></tr>
</tbody>
</table>
</div><p>That happens because the where clause will ONLY bring back the rows “where t1.table_name = ‘CAR’”. I still sometimes think that just using a FULL OUTER JOIN will fix this. But the following query brings back the same 7 rows.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> t1.column_name car_column, t2.column_name truck_column
  <span class="hljs-keyword">from</span> user_tab_cols t1
  <span class="hljs-keyword">full</span> <span class="hljs-keyword">outer</span> <span class="hljs-keyword">join</span> user_tab_cols t2
    <span class="hljs-keyword">on</span> t2.column_name = t1.column_name
    <span class="hljs-keyword">and</span> t2.table_name = <span class="hljs-string">'TRUCK'</span>
  <span class="hljs-keyword">where</span> t1.table_name = <span class="hljs-string">'CAR'</span>
</code></pre>
<p>For the same reason as the left outer join, the query above can only bring back 7 rows. This is where things get complicated with a FULL OUTER JOIN. I need a WHERE clause that will allow for any row in either table. And this is where things get a little strange. I would think that this would bring back everything I want:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> t1.column_name car_column, t2.column_name truck_column
  <span class="hljs-keyword">from</span> user_tab_cols t1
  <span class="hljs-keyword">full</span> <span class="hljs-keyword">outer</span> <span class="hljs-keyword">join</span> user_tab_cols t2
    <span class="hljs-keyword">on</span> t2.column_name = t1.column_name
    <span class="hljs-keyword">and</span> t2.table_name = <span class="hljs-string">'TRUCK'</span>
  <span class="hljs-keyword">where</span> (t1.table_name = <span class="hljs-string">'CAR'</span> <span class="hljs-keyword">or</span> t1.table_name <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">and</span> (t2.table_name = <span class="hljs-string">'TRUCK'</span> <span class="hljs-keyword">or</span> t2.table_name <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>)
</code></pre>
<p>But it doesn’t! I still only get 7 rows—the same rows returned with a LEFT OUTER JOIN. If I want to get all 9 rows, I need to <em>fully</em> understand the FULL OUTER JOIN. Well, I’ll admit, I don’t. I just know how to get it to work:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> t1.column_name car_column, t2.column_name truck_column
  <span class="hljs-keyword">from</span> user_tab_cols t1
  <span class="hljs-keyword">full</span> <span class="hljs-keyword">outer</span> <span class="hljs-keyword">join</span> user_tab_cols t2
    <span class="hljs-keyword">on</span> t2.column_name = t1.column_name
    <span class="hljs-keyword">and</span> t2.table_name = <span class="hljs-string">'TRUCK'</span>
    <span class="hljs-keyword">and</span> t1.table_name = <span class="hljs-string">'CAR'</span> <span class="hljs-comment">-- &lt;= this is key</span>
  <span class="hljs-keyword">where</span> (t1.table_name = <span class="hljs-string">'CAR'</span> <span class="hljs-keyword">or</span> t1.table_name <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">and</span> (t2.table_name = <span class="hljs-string">'TRUCK'</span> <span class="hljs-keyword">or</span> t2.table_name <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>)
</code></pre>
<p>That’s right. You need that extra AND condition in the FULL OUTER JOIN. It seems strange that an AND brings back <strong>more</strong> rows, but it does. The query above gives all 9 rows to match the very first table in this blog post.</p>
<p>This is great, but that query is slow. It takes over 2 seconds in my environment. That’s because the database has to look through the whole user_tab_cols view to do all of its work.</p>
<h2 id="heading-the-solution">The Solution</h2>
<p>I can speed things up by telling the database to only consider the rows I care about in each table/view (in this case I’m using the same view, USER_TAB_COLS, twice). I can do this with a CTE (a WITH clause):</p>
<pre><code class="lang-sql"><span class="hljs-keyword">with</span> 
t1 <span class="hljs-keyword">as</span> (
    <span class="hljs-keyword">select</span> table_name, column_name
      <span class="hljs-keyword">from</span> user_tab_cols
      <span class="hljs-keyword">where</span> table_name = <span class="hljs-string">'CAR'</span>
),
t2 <span class="hljs-keyword">as</span> (
    <span class="hljs-keyword">select</span> table_name, column_name
      <span class="hljs-keyword">from</span> user_tab_cols
      <span class="hljs-keyword">where</span> table_name = <span class="hljs-string">'TRUCK'</span>
)
  <span class="hljs-keyword">select</span> t1.column_name car_column, t2.column_name truck_column
  <span class="hljs-keyword">from</span> t1
  <span class="hljs-keyword">full</span> <span class="hljs-keyword">outer</span> <span class="hljs-keyword">join</span> t2
      <span class="hljs-keyword">on</span> t2.column_name = t1.column_name
<span class="hljs-comment">-- Now you don't need the following 4 lines</span>
<span class="hljs-comment">--      and t2.table_name = 'TRUCK'        </span>
<span class="hljs-comment">--      and t1.table_name = 'CAR' -- &lt;= this is key</span>
<span class="hljs-comment">--  where (t1.table_name = 'CAR' or t1.table_name is null)</span>
<span class="hljs-comment">--    and (t2.table_name = 'TRUCK' or t2.table_name is null)</span>
</code></pre>
<p>It runs in just 10% of the time—less than .2 seconds in my environment.</p>
<p>In my experience, this is how you should write a FULL OUTER JOIN.</p>
<ol>
<li><p>Use CTEs (with clauses) to define the data set of each table in the FULL OUTER JOIN.</p>
</li>
<li><p>Create the main query with just a FULL OUTER JOIN—no where clause required.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Unindexed Foreign Keys]]></title><description><![CDATA[Today’s assignment: subscribe to the “Unindexed Foreign Keys” exception report.
Really, that’s the gist of this blog post, but I will elaborate. Number 11 of my post on Writing Fast Queries is that joins (and where clauses) should have indexed column...]]></description><link>https://apexdebug.com/unindexed-foreign-keys</link><guid isPermaLink="true">https://apexdebug.com/unindexed-foreign-keys</guid><category><![CDATA[orclapex]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Tue, 03 Dec 2024 16:18:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733242599034/abea4e87-3b2d-4e71-abfd-b3cd7aa2a75d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today’s assignment: subscribe to the “Unindexed Foreign Keys” exception report.</p>
<p>Really, that’s the gist of this blog post, but I will elaborate. Number 11 of my post on <a target="_blank" href="https://apexdebug.com/writing-fast-queries">Writing Fast Queries</a> is that joins (and where clauses) should have indexed columns. Joins <em>almost always</em> involve a foreign key (FK). Every FK should have an associated index. Always. Sometimes we miss them, but there is an easy way to find them.</p>
<ol>
<li><p>Navigate to SQL Workshop &gt; Utilities &gt; Object Reports.</p>
</li>
<li><p>Under the heading “Exception Reports”, select “Unindexed Foreign Keys”.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733242343191/d957c36f-79fc-4bfa-94b7-f12174e6f460.png" alt /></p>
</li>
<li><p>If there are any results, create the recommended indexes.</p>
</li>
<li><p>This is an interactive report…so you can subscribe to it.<br /> Enable “Skip if No Data Found” so that you only receive the report if there is an issue.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733242224965/3f752681-c1ab-4c7b-ad3e-01e771a0596b.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>That’s it.</p>
<p>Bonus: There are several other exception reports. You may want to subscribe to some of those as well.</p>
]]></content:encoded></item><item><title><![CDATA[Oracle APEX : Invoke an Ajax Callback Process]]></title><description><![CDATA[When I want to invoke a server side process from a dynamic action (DA), I have two choices:

Create a DA Action of type "Execute Server-side Code"

Create a DA Action of type "Execute Javascript Code" and then write the javascript to invoke an Ajax C...]]></description><link>https://apexdebug.com/oracle-apex-invoke-an-ajax-callback-process</link><guid isPermaLink="true">https://apexdebug.com/oracle-apex-invoke-an-ajax-callback-process</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 05 Jul 2024 14:31:12 GMT</pubDate><content:encoded><![CDATA[<p>When I want to invoke a server side process from a dynamic action (DA), I have two choices:</p>
<ol>
<li><p>Create a DA Action of type "Execute Server-side Code"</p>
</li>
<li><p>Create a DA Action of type "Execute Javascript Code" and then write the javascript to invoke an Ajax Callback process.</p>
</li>
</ol>
<p>Option 1 seems like a lot less work. It has a drawback, though: the only way to pass data from the browser to the "Execute Server-side Code" action is by passing page items using the "Items to Submit" attribute. (OK, there is a corresponding drawback: the only way to get data back is via the "Items to Return" attribute). That often requires you to create hidden items (that are not session state protected and that do not warn on unsaved changes). Then you have to process subsequent actions based upon those items.</p>
<p>Option 2 allows you to pass data into the apex_application.g_f0x PL/SQL variables; you don't need to create those extra hidden items. But option 2 requires you to remember how to call the APEX Ajax Callback process via Javascript--and to write it and test it.</p>
<h1 id="heading-tldr"><strong>TLDR</strong></h1>
<p>APEX really should have a native DA type "Invoke Ajax Callback" that allows you to declaratively call the Ajax Callback process. <s>Please up-vote the APEX idea </s> <a target="_blank" href="https://apex.oracle.com/ideas/FR-3883"><s>FR-3883</s></a> <s> that describes this</s>. In the meantime, you can <a target="_blank" href="https://github.com/ainielse/invoke_ajax_callback">use the plug-in Marwa and I created</a>. (<strong>Update</strong>: FR-3883 was closed because the APEX team plans to re-work the way Ajax calls happen within the APEX engine. Look for a significant new feature in an upcoming release. Until then you can use our plug-in.)</p>
<p>The link to the plug-in is <a target="_blank" href="https://github.com/ainielse/invoke_ajax_callback">https://github.com/ainielse/invoke_ajax_callback</a></p>
<p>Also check out APEX Instant Tips <a target="_blank" href="https://www.youtube.com/watch?v=mpp6h07eZLE&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=1&amp;t=40s">episode #147</a>.</p>
<h1 id="heading-the-challenge">The Challenge</h1>
<p>You can see the results in action here:<br /><a target="_blank" href="https://apex.oracle.com/pls/apex/r/mchouchene/ait-147/employees">https://apex.oracle.com/pls/apex/r/mchouchene/ait-147/employees</a></p>
<p>We have a report column link that passes the EMPNO to a DA. The DA invokes an Ajax Callback process, passing both the EMPNO and the value of the P4_RAISE_PERCENT item. The javascript required to do this is</p>
<pre><code class="lang-sql">var da = this;
var processName = da.action.attribute01;
var itemsToSubmit = da.action.attribute02;

apex.server.process(
  processName, {
    x01: this.data.empno,
    pageItems: itemsToSubmit

  }, {
    success: function(data) {

      console.log('success', data.message);
      apex.message.showPageSuccess(data.message);
      apex.da.resume(da.resumeCallback, false);

    },
    error: function(error) {
      apex.message.showErrors(error.responseText)
    }

  }
);
}
</code></pre>
<p>Sure, this isn't hard, but it's also not super easy. And if you want to do something other than show a message you have to remember how to do it (or, in my case, look up how to do it each time). You may want to take different kinds of actions: raise a custom event, call a Javascript function, etc. Each of these requires you to recall exactly how to do it.</p>
<h1 id="heading-the-plug-in">The Plug-in</h1>
<p>To make our own lives easier, we created a <a target="_blank" href="https://github.com/ainielse/invoke_ajax_callback">plug-in</a> that provides a declarative way to invoke the Ajax Callback process. Because we are oh so creative, we named the plug-in "<a target="_blank" href="https://github.com/ainielse/invoke_ajax_callback">Invoke Ajax Callback</a>."</p>
<p>The plug-in contains significant help within it, so I won't rewrite that help here. This example assumes you have an Ajax Callback process named "GIVE_RAISE". The basic steps are as follows:</p>
<ol>
<li><p>Import the plug-in into your application (if you have not already).</p>
</li>
<li><p>The most common way to invoke the plugin will be to raise a custom event. Hence, you should create a DA that is based upon a custom event. In this example the custom event is "giveRaise":</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720189145790/400335ab-9ef7-4a39-ae0c-c87cafd0a181.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Add a True action of type "Invoke Ajax Callback":</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720189238071/d9f4998c-51d5-40fe-a2ee-7d7381728548.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>View the help for each attribute to understand how they are used.</p>
</li>
<li><p>You can then raise the event with code as show below:<br /> <code>javascript:apex.event.trigger(document, "giveRaise", {"empno":"#EMPNO#","empnoCS":"#EMPNO_CS#"});</code></p>
</li>
<li><p>That is all the Javascript you need to know. The rest is handled by the plug-in. If you want to do additional processing, though, the plug-in also supports that. Simple change the "With Result Action" to invoke additional Javascript or to raise an additional custom event.</p>
</li>
</ol>
<p>Returning to the Ajax Callback process, GIVE_RAISE...that process should provide a success message. This is also in the plug-in help, but I'll put it here for completeness:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>

<span class="hljs-comment">-- your code here</span>

<span class="hljs-comment">-- if successful...</span>
    apex_json.open_object;
    apex_json.write('message', l_name  || ' given ' || :P4_RAISE_PERCENT || '% Raise!');
    apex_json.close_object;

exception when others then
        htp.p('Invalid Employee');
        <span class="hljs-comment">-- *** also log the error</span>
<span class="hljs-keyword">end</span>;
</code></pre>
<h1 id="heading-if-you-like-this-post-or-plug-in">If you like this post or plug-in...</h1>
<p>let use know :). Leave a comment, hit the like button, send me a note written on a the rim of a <a target="_blank" href="https://boydcycling.com/collections/podium-carbon-gravel-wheels/products/jocassee-700c-wheelset">Boyd Cycles carbon fiber gravel wheel</a>. Whatever works for you.</p>
]]></content:encoded></item><item><title><![CDATA[ORA-29106: Cannot import PKCS #12 wallet.]]></title><description><![CDATA[As the name of this blog suggests, sometimes this blog will just be about debugging a little something that occurs in APEX. This is one of those little things. If you attempt to access a TLS/SSL web service in any way (the declarative REST services, ...]]></description><link>https://apexdebug.com/ora-29106-cannot-import-pkcs-12-wallet</link><guid isPermaLink="true">https://apexdebug.com/ora-29106-cannot-import-pkcs-12-wallet</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Thu, 23 May 2024 13:31:50 GMT</pubDate><content:encoded><![CDATA[<p>As the name of this blog suggests, sometimes this blog will just be about debugging a little something that occurs in APEX. This is one of those little things. If you attempt to access a TLS/SSL web service in any way (the declarative REST services, using the apex_web_service API, a social sign-in provider, etc.) and you get the error</p>
<p>ORA-29106: Cannot import PKCS #12 wallet.</p>
<p>your problem may be that you are on Oracle Database <strong>Standard Edition</strong> and your wallet password is "complex"--that is, it has special characters. This seems to happen when you use orapki, but it may not be an issue when you use Oracle Wallet Manager. So, according to <a target="_blank" href="https://support.oracle.com/epmos/faces/DocumentDisplay?_afrLoop=379538240644380&amp;id=2815506.1&amp;_adf.ctrl-state=1o582xe1z_109">Oracle Support Document 2815506.1</a>, there are two possible solutions:</p>
<ol>
<li><p>Simply change the password to something without special characters (but with lower case, upper case and numbers), things should work. You may also need to bounce ORDS to get new database sessions.</p>
</li>
<li><p>Use Oracle Wallet Manager to create the wallet (not orapki).</p>
</li>
</ol>
<p>In my experience you may have to play around with this a bit. Depending on versions of everything, you may not be able to use orapki without special chars, different lengths or required, etc. You may even need to create the Oracle Wallet with an earlier version of orapki or Oracle Wallet Manager. If you can't get it working, leave a comment with your specific database version and I may be able to point you in the right direction.</p>
<p>Yes, a small...but if you run into this...a (bitter)sweet blog post.</p>
]]></content:encoded></item><item><title><![CDATA[Oracle APEX AuthN vs AuthZ...and How to Change Messages]]></title><description><![CDATA[Authentication and authorization are closely related, so closely that they are often confused, combined or commingled (yes, I too thought the spelling was comingled). I argue, though, that in general we should maintain a strict separation:

Authentic...]]></description><link>https://apexdebug.com/oracle-apex-authn-vs-authzand-how-to-change-messages</link><guid isPermaLink="true">https://apexdebug.com/oracle-apex-authn-vs-authzand-how-to-change-messages</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Thu, 23 May 2024 12:08:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716466054520/fe1c534c-1ac9-4fd6-a521-ff60f6af3265.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Authentication and authorization are closely related, so closely that they are often confused, combined or commingled (yes, I too thought the spelling was comingled). I argue, though, that in general we should maintain a strict separation:</p>
<ul>
<li><p>Authentication (AuthN) uniquely identifies who the user is.</p>
</li>
<li><p>Authorization (AuthZ) determines what the user can do.</p>
</li>
</ul>
<h1 id="heading-tldr">TLDR</h1>
<p>If you use a verify function or other method within the Authentication Scheme with social sign-on (Google, Azure, Facebook, OIDC, etc.) to invalidate a user that passes the social sign-on (for example, the user's email address is not in your domain), the user will not get any feedback as to why they are not able to authenticate. They will just get redirected back to the login screen (which they successfully logged into already). Instead, use an application level Authorization Scheme and modify the error message as described below.</p>
<h1 id="heading-the-longer-story">The Longer Story</h1>
<p>It seems simple but people, myself included, often embed some authorization into our authentication process. In many cases you have no choice. If you do your own authN, for example via APEX workspace users, database users, a table of users and passwords, even a corporate LDAP server, those users are the only users you can positively identify. If you can't identify them, they can't authenticate to your application. Let's take the case of LDAP users, though. Your company may include many users in the LDAP server that are not allowed to use the application you have developed. Should your authN scheme reject those users, or should there be an authZ scheme that rejects them. If we follow the strict guidance above, it should be an authZ scheme (not the authN scheme). This is even more striking if you use a public authN mechanism, for example Google or Facebook authentication. You may authenticate a user that has nothing to do with your application at all. Should you reject the user at the point of authentication, or should the user be rejected via authorization...or both?</p>
<p>This is probably a good time to mention that if you are a more visual learner, you can watch <a target="_blank" href="https://www.youtube.com/watch?v=EvbGrKH6A4g&amp;list=PLCAYBJ7ynpQQQrdwKFBZu8Kx9VTFt-pRP&amp;index=12">APEX Instant Tips Episode #132</a> for much of the same info.</p>
<p>I admit, there are solid arguments to rejecting the authentication if the user is not a valid user of your system. Perhaps I use Azure or Google to authenticate my users...and my users all must have an email address in the @inielsen.net domain. One could argue that any user with an email address in a different domain is "not a user" and, therefor, should not be able to authenticate. As described in the TLDR, there is a practical reason, however, to allow the user to authenticate and then stop the user at the point of application authorization instead of at the point of authentication. If you use the Authentication Scheme the user will not get any feedback as to why they are not able to authenticate. The user simply gets redirected back to the login screen. By using an application level Authorization Scheme the user will get an error page. You can customize the error they receive, allowing you to tell the user why they can't use the application.</p>
<p>In short, instead of repeatedly seeing this (with no indication why)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716465197502/856dadb4-f411-4ddc-b647-178324ee464c.png" alt class="image--center mx-auto" /></p>
<p>your user will successfully login but then receive a customized message like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716465317352/c54ea8ba-82e4-4d03-82f7-49cec7b94fe8.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-how-to">How To...</h1>
<p>Step 1: Configure your AuthN Scheme without any additional checks--no extra session verify function or anything beyond the standard authentication.</p>
<p>Step 2: Create an AuthZ scheme that does the check you would like. It will look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716465493485/ceb0b8b0-dcc2-4454-9a4c-4f1006fcf803.png" alt class="image--center mx-auto" /></p>
<p>Step 3: Edit the Application Definition &gt; Security. Add you application level AuthZ scheme:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716465953607/b8e201b1-6add-4d35-be94-c8f43b2a995a.png" alt class="image--center mx-auto" /></p>
<p>Step 4: Configure the additional error message...the portion that will show below the error message in the image directly above. Navigate to Shared Components &gt; Globalization &gt; Text Messages. Create a new text message with the name APEX.AUTHORIZATION.ACCESS_DENIED.APPLICATION. (For more on this topic see <a target="_blank" href="https://apexdebug.com/apex-cataclysmic-failure-messages">APEX Cataclysmic Failure Messages</a>.)</p>
<p>It will look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716465611812/b0c830a7-a242-41c0-9eb7-f7a15e58e0da.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-the-end">The End</h1>
<p>That's all folks. Happy authenticating...or authorizing.</p>
]]></content:encoded></item><item><title><![CDATA[Monitor Page Performance in Production]]></title><description><![CDATA[As application developers we spend most of our time in the development environment. If we are just a little bit lucky, though, the applications we develop actually find their way to a production environment. While our day to day attention is rightful...]]></description><link>https://apexdebug.com/monitor-page-performance-in-production</link><guid isPermaLink="true">https://apexdebug.com/monitor-page-performance-in-production</guid><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Anton Nielsen]]></dc:creator><pubDate>Fri, 16 Feb 2024 17:12:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708101396287/5055c3f5-1e57-4f98-8345-b76bb69a498e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As application developers we spend most of our time in the development environment. If we are just a little bit lucky, though, the applications we develop actually find their way to a production environment. While our day to day attention is rightfully focused in the development environment, the production environment can tell us what our users are experiencing--often highlighting issues before users have a chance to complain. It's great to fix an issue before the issue is raised. Users are happier--and it sure cuts down on paperwork. If you are a visual learner, check out APEX Instant Tips episode 134.</p>
<p>One of my favorite ways to accomplish this is by using the APEX "Weighted Page Performance" report. You click the person with the wrench at the top right of the APEX builder, and then "Monitor Activity."</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708032612253/5ecfa5a2-eed3-435d-958b-eb7e021a57e5.png" alt class="image--center mx-auto" /></p>
<p>Then choose "By Weighted Page Performance."</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708032660508/4ae11339-cb74-4316-ac78-e38f9ccb8ae2.png" alt class="image--center mx-auto" /></p>
<p>I suggest setting "Since" to "1 Week."</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708032757638/f5a54ec5-2328-47db-9456-5b9cabfe9bb1.png" alt class="image--center mx-auto" /></p>
<p>If you are interested in a specific application you can set that as well. The key column here is "Weighted Average." It is the primary order by column, ordered descending for a reason. This column takes into account both how often the page is utilized and how long the page takes to build. You can see the first row above has a fast execution time (Average Elapsed), so there is not much to do there. But the second and third rows are quite slow.</p>
<p>If users rarely visit a page, and it is slow, it may not be that critical to look at. But if a page is just a little slow, but used a LOT, then you may want to look at it soon. A high weighted average indicates two things: the amount of time your users are "wasting" waiting, and the overall impact on the system (how much compute, read/write, etc. the database is using).</p>
<p>Those pages are definitely targets for improvement. Checking this report regularly will give you areas of concern. It's worth adding a performance improvement task to your next development sprint for any slow pages near the top of this list. I love including a message about a code promotion that include "Improved performance of the Assignment Creation page" and getting a response "Oh right! I've been meaning to open a bug about that but you’ve already fixed it!"</p>
<p>Of course, you want to find out about these regularly, so create a report subscription.</p>
<p>Choose Actions &gt; Subscription</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708032848164/116f02ea-6dcf-47e4-8d67-a8a97f4ccc06.png" alt class="image--center mx-auto" /></p>
<p>and set up a subscription to send the report to yourself once a week.</p>
<p>Notice I'm doing this in the production environment. If you don't have access to this report in production, ask someone who does to create a subscription with your email address.</p>
<p>And, if you can't do that, create an interactive report in an application you have access to in production. Then subscribe to that report. The report query should be something like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> 
       application_id, 
       page_id, 
       application_name,
       page_name,
       <span class="hljs-keyword">count</span> (*)                page_events,
       <span class="hljs-keyword">avg</span>(ELAPSED_TIME)                average_ELAPSED_TIMEsed_time, 
       <span class="hljs-keyword">avg</span>(ELAPSED_TIME)*<span class="hljs-keyword">count</span>(*)       weighted_average,
       <span class="hljs-keyword">median</span>(ELAPSED_TIME)             median_ELAPSED_TIMEsed_time,
       <span class="hljs-keyword">median</span>(ELAPSED_TIME)*<span class="hljs-keyword">count</span>(*)    weighted_median,
       (<span class="hljs-keyword">median</span>(ELAPSED_TIME)*<span class="hljs-keyword">count</span>(*)) - (<span class="hljs-keyword">avg</span>(ELAPSED_TIME)*<span class="hljs-keyword">count</span>(*)) median_to_avg_delta,
       <span class="hljs-keyword">max</span>(ELAPSED_TIME)                maximum_ELAPSED_TIMEsed_time,
       <span class="hljs-keyword">min</span>(ELAPSED_TIME)                minimum_ELAPSED_TIMEsed_time,
       <span class="hljs-keyword">max</span>(ELAPSED_TIME) - <span class="hljs-keyword">min</span>(ELAPSED_TIME)    maximum_minimum_delta,
       <span class="hljs-keyword">median</span>(content_length)  median_content_length
<span class="hljs-keyword">from</span> apex_workspace_activity_log l
<span class="hljs-keyword">where</span> 
    VIEW_DATE &gt;= (<span class="hljs-keyword">sysdate</span> - nvl(<span class="hljs-number">7</span>,<span class="hljs-number">1</span>)) 
<span class="hljs-keyword">group</span> <span class="hljs-keyword">by</span> application_id, page_id, application_name, page_name
</code></pre>
<p>Go ahead and modify that query to your liking.</p>
<p>If you've made it this far please do this today and let me know if you did. I'm always pleased to hear that someone actually implemented a suggestion.</p>
]]></content:encoded></item></channel></rss>