<?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[Spendolini Blog]]></title><description><![CDATA[Blogging about Oracle Database technologies, tools & trends with a focus on Oracle APEX]]></description><link>https://spendolini.blog</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1733670264133/465ca27c-89a3-4209-9240-bb5e605a25a9.png</url><title>Spendolini Blog</title><link>https://spendolini.blog</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 14 Sep 2026 08:09:04 GMT</lastBuildDate><atom:link href="https://spendolini.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Surfacing Invisible Business Rules with SQL Assertions & AI]]></title><description><![CDATA[I saw Dimitri's post on SQL Assertions today and it piqued my curiosity. Putting more business rules closer to data is almost always a great idea, as you can now call DML from any technology and rest ]]></description><link>https://spendolini.blog/surfacing-invisible-business-rules-with-sql-assertions-ai</link><guid isPermaLink="true">https://spendolini.blog/surfacing-invisible-business-rules-with-sql-assertions-ai</guid><category><![CDATA[Oracle]]></category><category><![CDATA[AI]]></category><category><![CDATA[codex]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Fri, 20 Mar 2026 19:03:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/63555b7c31a9a1e09b693dda/302502a8-9871-4967-ac10-d9c2b1792af9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I saw Dimitri's <a href="https://dgielis.com/oracle-database-26ai-assertions-in-a-world-cup-betting-app">post on SQL Assertions</a> today and it piqued my curiosity. Putting more business rules closer to data is almost always a great idea, as you can now call DML from any technology and rest assured that your core business rules won't get violated.</p>
<div>
<div>💡</div>
<div>Learn more about <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/create-assertion.html" style="pointer-events:none">SQL Assertions</a> from the Oracle Documentation</div>
</div>

<p>Thus, I decided to test it out. This worked fine on my Always Free ADB, as it has been automatically upgraded to 23.26.1, the required version for this new feature.</p>
<p>Rather than create example assertions, I turned to Codex to do it for me based on data in the <code>EMP</code> table.</p>
<p>Using the following prompt:</p>
<blockquote>
<p>using data in emp, create a group of SQL assertions. SQL assertions are new in Oracle, have a look here for how to create them: <a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/create-assertion.html">https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/create-assertion.html</a></p>
</blockquote>
<p>Codex generated the following:</p>
<pre><code class="language-sql">create assertion if not exists emp_ename_not_blank check (not exists (select 1 from emp where ename is null or trim(ename) is null)) enable validate;

create assertion if not exists emp_job_not_blank check (not exists (select 1 from emp where job is null or trim(job) is null)) enable validate;

create assertion if not exists emp_empno_nn check (not exists (select 1 from emp where empno is null)) enable validate;

create assertion if not exists emp_mgr_not_self check (not exists (select 1 from emp where mgr = empno)) enable validate;

create assertion if not exists emp_mgr_exists check (not exists (select 1 from emp where mgr is not null and mgr not in (select empno from emp))) enable validate;

create assertion if not exists emp_dept_exists check (not exists (select 1 from emp where deptno is not null and deptno not in (select deptno from dept))) enable validate;

create assertion if not exists emp_deptno_nn check (not exists (select 1 from emp where deptno is null)) enable validate;

create assertion if not exists emp_sal_positive check (not exists (select 1 from emp where sal is not null and sal &lt;= 0)) enable validate;

create assertion if not exists emp_comm_nonnegative check (not exists (select 1 from emp where comm is not null and comm &lt; 0)) enable validate;

create assertion if not exists emp_sal_upper_bound check (not exists (select 1 from emp where sal is not null and sal &gt; 1000000)) enable validate;
</code></pre>
<p>While some of these are interesting (salary upper bound, commission not negative), most of them are duplicative of not null, PK and FK constraints already in place.</p>
<p>Not a problem. I just refined my prompt slightly:</p>
<blockquote>
<p>create some assertions that are not copies of things like PK, FK, etc. look at the business rules embedded in the data and create some based on those</p>
</blockquote>
<p>This time, I got a much better set of assertions:</p>
<pre><code class="language-sql">create assertion if not exists emp_br_president_rules check ( not exists ( select 1 from emp where job = 'PRESIDENT' and (mgr is not null or comm is not null or deptno &lt;&gt; 10) ) ) enable validate;

create assertion if not exists emp_br_non_sales_no_comm check ( not exists ( select 1 from emp where job &lt;&gt; 'SALESMAN' and comm is not null ) ) enable validate;

create assertion if not exists emp_br_sales_in_dept30 check ( not exists ( select 1 from emp where job = 'SALESMAN' and deptno &lt;&gt; 30 ) ) enable validate;

create assertion if not exists emp_br_salary_band_clerk check ( not exists ( select 1 from emp where job = 'CLERK' and sal is not null and (sal &lt; 800 or sal &gt; 1300) ) ) enable validate;

create assertion if not exists emp_br_salary_band_salesman check ( not exists ( select 1 from emp where job = 'SALESMAN' and sal is not null and (sal &lt; 1250 or sal &gt; 1600) ) ) enable validate;

create assertion if not exists emp_br_salary_band_analyst check ( not exists ( select 1 from emp where job = 'ANALYST' and sal is not null and sal &lt;&gt; 3000 ) ) enable validate;

create assertion if not exists emp_br_required_core_fields check ( not exists ( select 1 from emp where ename is null or job is null or hiredate is null or sal is null ) ) enable novalidate;
</code></pre>
<p>Some examples include:</p>
<ul>
<li><p><code>PRESIDENT</code> must have no manager, no commission, and belong to department 10</p>
</li>
<li><p>only <code>SALESMAN</code> can have commission</p>
</li>
<li><p><code>SALESMAN</code> must be in department 30</p>
</li>
<li><p>salary bands by job (<code>CLERK</code>, <code>SALESMAN</code>, <code>ANALYST</code>)</p>
</li>
</ul>
<p>All of these rules were not obvious to me, and I was doing literally nothing to enforce them otherwise. Thus, it was possible to have a <code>SALESMAN</code> in <code>OPERATIONS</code>, a <code>PRESIDENT</code> with a manager or an out-of-band salary.</p>
<p>Now, with these new assertions in place, I'm able to better enforce my business rules without having to make any changes to my applications - APEX or otherwise.</p>
<p>Even if you don't have Oracle 26ai or want to use SQL Assertions, it may be worth letting an LLM inspect your data and provide some insights as to which patterns exist. Then, you can pick and choose which ones can be converted to a business rule either the traditional way or via SQL Assertions.</p>
<p><strong>Notice</strong>: I should mention that in order to generate these insights, I had to share my data with the LLM - specifically, GPT-5.3-Codex. While this approach can surface valuable and unexpected business rules, it’s important to recognize the potential risks involved. To achieve similar results, you’d need to follow the same process, meaning that your organization's data would also have to be shared with an LLM. Many organizations have strict policies that prohibit sharing sensitive or proprietary data with public LLMs, so always exercise caution and ensure compliance with your company’s data-sharing guidelines.</p>
<hr />
<p><em>Title photo by</em> <a href="https://unsplash.com/@markusspiske"><em><strong>Markus Spiske</strong></em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Second Time Around]]></title><description><![CDATA[Almost 20 years ago to the date, I submitted my resignation to Oracle to go start my own company. It was a difficult decision to make, as APEX - or HTML DB as it was called then - was just starting to gain traction. However, I was presented with an o...]]></description><link>https://spendolini.blog/second-time-around</link><guid isPermaLink="true">https://spendolini.blog/second-time-around</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[Oracle Cloud]]></category><category><![CDATA[Oracle Database]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Mon, 15 Sep 2025 13:10:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757767634077/88c21183-166c-4a66-b3e2-9f2e3ed2d092.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Almost <a target="_blank" href="https://spendolini.blogspot.com/2005/09/leaving-oracle.html">20 years ago</a> to the date, I submitted my resignation to Oracle to go start my own company. It was a difficult decision to make, as APEX - or HTML DB as it was called then - was just starting to gain traction. However, I was presented with an opportunity that was too hard to pass by.</p>
<p>Fast forward 20 years: starting today, I am happy to announce that I am returning to where it all started for me and re-joining the Oracle Database Tools team as an Architect.</p>
<p>This move is bittersweet. For the past five years, I’ve had the privilege of building and leading an incredible team in the GIUs, collaborating daily with people I deeply respect and admire. Walking away from that isn’t easy. But, just like 20 years ago, the opportunity in front of me was one I couldn’t pass up.</p>
<p>In this new role, I’ll be working with all of the Database Tools teams to help advance things like APEXLang as well as enabling Oracle Database developers to embrace AI for both themselves and their users. I also hope to make it out to a few more conferences and engage with the community on a more regular basis via social media and in person.</p>
<p>As I get settled in over the next few days, I’ll be blogging about what I’ve been up to. If there’s a topic that you’d like to hear more about or just want to re-connect, please don’t hesitate to reach out to me in the comments or on my socials: <a target="_blank" href="https://bsky.app/profile/spendolini.blog">BlueSky</a>, <a target="_blank" href="https://x.com/sspendol">X</a> &amp; <a target="_blank" href="https://www.linkedin.com/in/spendolini/">LinkedIn</a>.</p>
<p>#LetsWreckThisTogether</p>
<p><em>Bonus points for anyone who can 1) identify the building in the heading and 2) identify its significance.</em></p>
]]></content:encoded></item><item><title><![CDATA[Using SQLcl Projects]]></title><description><![CDATA[One of the more difficult parts of the software development lifecycle is deployment. While it seems simple on the surface - just write some scripts and run them - it gets pretty complicated pretty quickly as the level of sophistication of your system...]]></description><link>https://spendolini.blog/using-sqlcl-projects</link><guid isPermaLink="true">https://spendolini.blog/using-sqlcl-projects</guid><category><![CDATA[#oracle-apex]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Cloud]]></category><category><![CDATA[Oracle Database]]></category><category><![CDATA[sqlcl]]></category><category><![CDATA[liquibase]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Mon, 01 Sep 2025 14:21:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755370649722/ca148bcc-ae85-440c-8ea5-de20f02d6aa8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the more difficult parts of the software development lifecycle is deployment. While it seems simple on the surface - just write some scripts and run them - it gets pretty complicated pretty quickly as the level of sophistication of your systems increase.</p>
<p>Assume that you’re developing an employee management application. At the core of this system is a table called <code>EMP</code>:</p>
<pre><code class="lang-sql">SQL&gt; desc emp;

Name        Null?       Type             
___________ ___________ ________________ 
EMPNO       NOT NULL    NUMBER(4)        
ENAME                   VARCHAR2(10)     
JOB                     VARCHAR2(9)      
MGR                     NUMBER(4)        
HIREDATE                DATE             
SAL                     NUMBER(7,2)      
COMM                    NUMBER(7,2)      
DEPTNO                  NUMBER(2)
</code></pre>
<p>To deploy your application, you can create a script called <code>emp.sql</code> and simply run it in production. And that’s exactly what you do.</p>
<p>Before you know it, your table has a bunch of data about the employees of the organization and people love using it. The next week, you get an enhancement request to add a column to track employee’s email addresses. Sounds simple enough, right?</p>
<p>Just run the following script:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> emp <span class="hljs-keyword">add</span> (email <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">100</span>));
</code></pre>
<p>Simple, right?</p>
<p>What happens if you scale up your development? Now, instead of one table and one developer, you have hundreds of tables, packages and views and ten or more developers.</p>
<p>And what happens if you also add multiple target systems to the mix? Or want to ensure that changes are properly tested? Or want to incorporate a CI/CD pipeline for automation?</p>
<p>Things will get really complicated really quickly if you’re going to rely on manual processes. It’s a matter of time before the wrong script is run on the wrong environment.</p>
<p>The good news is that there are modern tools &amp; methodologies available that were designed to help alleviate this complexity. This blog will dive into the details of one of them - SQLcl Projects &amp; Liquibase.</p>
<h2 id="heading-about-liquibase">About Liquibase</h2>
<p>Before we dive into SQLcl Projects, let’s talk Liquibase for a minute. Liquibase is an open source utility that helps promote database schema-level changes. SQLcl ships with a fork of Liquibase embedded in it, so if you have SQLcl, you have Liquibase and there’s no need to download anything else.</p>
<p>At a high level, Liquibase creates a “ledger” of sorts to track all DDL called changelogs. These changelogs can be pointed at any schema and “replayed” to install a set of database objects. The changelogs also prevent Liquibase from running the same script twice, ensuring database object integrity.</p>
<h3 id="heading-explain-it-like-im-5-years-old">Explain it Like I’m 5 Years Old</h3>
<p>Sure! Let’s walk through a simple example and how Liquibase helps manage your development lifecycle.</p>
<p>Let’s stick with our simple example that contains a single table called <code>EMP</code>. You can easily script out the DDL for these tables and even the <code>INSERT</code> statements to populate them. Traditionally, when you’re ready to deploy, you would hand those scripts to a DBA who would then run them in the next tier - QA, TEST, PROD, etc.</p>
<p>With Liquibase, things are similar - but different. Think of Liquibase as a ledger. This ledger - or in Liquibase terms, changelog - will keep track of all scripts that are run for a specific target. This way, it will only ever execute each script once as well as keep an running audit of which was run.</p>
<p>In our example, our script to create the <code>EMP</code> table will be deployed by Liquibase to the target. Given that we have a brand new schema with nothing in it, Liquibase runs the script, creates the table and adds an entry to it’s changelog. So far, so good.</p>
<p>Let’s say that we need to add a new column to <code>EMP</code>. Clearly, we can’t re-create the entire table, as we would lose all of our data. Instead, we create and deploy a script that only adds the new column. Liquibase will then run the single script, which adds the column to the table and adds another entry to the changelog.</p>
<p>So far, this is not unlike how you would do things without Liquibase.</p>
<p>So, what’s the difference?</p>
<p>It’s easy to mentally manage two changes to a single system. It’s not as easy to manage hundreds of changes to many systems, especially across many developers and many target systems.</p>
<p>If you have multiple target databases or have the need to frequently generate test databases automatically to confirm your changes as you develop, you’ll wonder how you lived without Liquibase. Using Liquibase, each target gets its own changelog of which releases have been applied. Releases are also inclusive, meaning that you can start with any release you like and not have to manually apply previous ones.</p>
<p>Or, if you’re building a packaged solution that multiple users will download and use, Liquibase makes it easy for them to keep up with changes. They can choose how often to upgrade and be assured that with no additional effort, they will always be able to patch to the latest version.</p>
<p>Liquibase is also completely scriptable and can easily fit into any CI/CD process. This enables developers to automate the deployment of their code to any number of targets across development and deployment pipelines.</p>
<p>And finally - and perhaps most importantly - the Liquibase changelog serves as an audit table as to who made what change when. In many organizations, auditing these type of actions are not just important, it’s the law.</p>
<h2 id="heading-sqlcl-projects">SQLcl Projects</h2>
<p>Last year, SQLcl introduced a new feature called <a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.2/sqcug/project-command.html">Projects</a>. From the SQlcl Projects documentation:</p>
<blockquote>
<p>The <strong>project</strong> command in Oracle SQLcl is a powerful tool designed to standardize database software versioning and create releasable artifacts, including APEX elements. This command supports a consistent model of development and operations, enabling repeatable builds that can be applied in a specific order.</p>
</blockquote>
<p>While SQLcl has had Liquibase support for some time now - this new Projects feature makes using Liquibase even easier and more intuitive than before. Think of it as a set of commands that act as a wrapper to Liquibase that make managing development easier. SQLcl Projects syntax is simple and easy to learn, and the tool itself automates a number of the requirements found when working with Liquibase.</p>
<h3 id="heading-sqlcl-projects-workflow">SQlcl Projects Workflow</h3>
<p>SQLcl Projects has a prescribed workflow when it comes to building deployable releases:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756494128998/18357ff4-151a-4036-bb77-07ca254098e5.png" alt class="image--center mx-auto" /></p>
<p>Here’s a breakdown of each phase.</p>
<h3 id="heading-build">Build</h3>
<p>Build is where you as a developer build your application. This involves creating database objects of all sorts, as well as APEX applications. In many cases, you will “over-create” and end up with some objects that just don’t need to get deployed.</p>
<h3 id="heading-stage">Stage</h3>
<p>As you complete your work, you “stage” it. Stage is where your release will be built from. SQLcl Projects can automatically generate and stage DDL scripts based on what you create in the database, or you can manually do this on your own. Thus, you can simply issue DDL commands from any tool and either stage the scripts or let SQLcl do it for you.</p>
<h3 id="heading-release">Release</h3>
<p>Once you have staged all of your scripts, it’s time to cut a release. A release is a collection of your scripts blended in with the required files for SQLcl to be able to properly deploy it. Again, this process is as simple as issuing a command; SQLcl again does the heavy lifting or you.</p>
<h3 id="heading-deploy">Deploy</h3>
<p>Once you have a release artifact, deploying it is also as simple as a single-line command. Release artifacts - which are really just ZIP files - can be deployed manually or as part of a CI/CD release process.</p>
<h1 id="heading-my-first-project">My First Project</h1>
<p>Let’s get to the fun part and build our first SQLcl Project. To keep things simple, we’re going to stick with the use case I just described. We’ll build an “application” that consists of just <code>EMP</code> &amp; <code>DEPT</code> and deploy it with SQLcl Projects.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>In order to follow along, you’ll need to have access to the following resources:</p>
<ul>
<li><p><a target="_blank" href="https://www.oracle.com/database/sqldeveloper/technologies/sqlcl/download/">SQLcl</a> version 25.2+ installed on your machine</p>
</li>
<li><p><a target="_blank" href="https://code.visualstudio.com/">VS Code</a></p>
</li>
<li><p><a target="_blank" href="https://www.oracle.com/database/sqldeveloper/vscode/">SQL Developer for VS Code</a></p>
</li>
<li><p>An instance of Git; <a target="_blank" href="http://github.com">github.com</a> will work just fine and is used in this example</p>
</li>
<li><p>Access to two Oracle database instances or two PDBs that are the same or at least similar versions</p>
<ul>
<li><p>You will need to create a new schema called <code>DEMO</code> in each database, so DBA-level access is required</p>
</li>
<li><p>In reality, these should be the exact same version; for our use case, something close should be fine</p>
</li>
</ul>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">A quick and free way to get a pair of Oracle Databases is to create a <a target="_self" href="https://www.oracle.com/cloud/free/">free Oracle Cloud account</a> and then create them there.</div>
</div>

<h2 id="heading-create-amp-seed-the-repository">Create &amp; Seed the Repository</h2>
<p>Let's start by creating a new repository and then using SQLcl Projects to seed it with your schema objects.</p>
<h3 id="heading-create-a-repository">Create a Repository</h3>
<p>First things first, create a new, clean repository. This can be anywhere; we'll use GitHub for this example.</p>
<ol>
<li><p>Navigate to <a target="_blank" href="https://github.com">github.com</a> and sign in.</p>
</li>
<li><p>Click <strong>New</strong>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756213179253/99fdd97f-687c-4eb5-9e20-7e2e263e7121.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Choose an <strong>Owner</strong> and enter a <strong>Repository name</strong>. We’ll use <code>sqlcl-projects-demo</code> for this demonstration.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756213735623/340a011a-47e4-4c87-a7a7-a5aaf604d6ee.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Click <strong>Create repository</strong>.</p>
</li>
</ol>
<p>You now have a new, blank repository that we can use with SQLcl Projects.</p>
<h3 id="heading-clone-the-repository">Clone the Repository</h3>
<p>Next, clone the repository to your local machine and change to the new directory. Be sure to update the username of the repo with yours!</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> git@github.com:[your-github-user]/sqlcl-projects-demo.git
<span class="hljs-built_in">cd</span> sqlcl-projects-demo
</code></pre>
<h3 id="heading-create-the-main-branch">Create the <code>main</code> Branch</h3>
<p>We'll need to create the main branch in the repository. These steps will create a basic "README" file and then commit and push it to a new branch called <code>main</code>.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"# sqlcl-projects-demo"</span> &gt;&gt; README.md
git init
git add README.md
git commit -m <span class="hljs-string">"first commit"</span>
git branch -M main
git push -u origin main
</code></pre>
<h2 id="heading-create-the-schema-amp-objects">Create the Schema &amp; Objects</h2>
<p>Next, we need to create our development schema and seed it with a couple tables.</p>
<h3 id="heading-create-the-demo-schema-on-the-source-database">Create the DEMO Schema on the Source Database</h3>
<p>For this example, we’ll create &amp; deploy our objects from and to a schema called <code>DEMO</code>. Thus, we will need to create this schema on both databases that we’re going to use.</p>
<ol>
<li><p>Connect to the source database as a DBA-level user, such as <code>SYSTEM</code> or <code>ADMIN</code> (ADB).</p>
</li>
<li><p>Run the following commands, ensuring to adjust the password and tablespace name, if needed.</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> demo <span class="hljs-keyword">identified</span> <span class="hljs-keyword">by</span> <span class="hljs-string">"StrongPassword1$"</span>;
<span class="hljs-keyword">alter</span> <span class="hljs-keyword">user</span> demo <span class="hljs-keyword">quota</span> <span class="hljs-keyword">unlimited</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">users</span>;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">connect</span>, <span class="hljs-keyword">resource</span>, <span class="hljs-keyword">create</span> <span class="hljs-keyword">view</span> <span class="hljs-keyword">to</span> demo;
</code></pre>
<ol start="3">
<li>Next, create a connection in SQL Developer for VS Code for this schema. Name that connection <code>demo</code>. If using ADB, please be sure to select the <code>_LOW</code> connection; using others can cause issues with Liquibase.</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Need help installing or creating connections in SQL Developer VS Code? Have a look at the <a target="_self" href="https://docs.oracle.com/en/database/oracle/sql-developer-vscode/24.2/sqdnx/getting-started.html">documentation</a>.</div>
</div>

<h3 id="heading-connect-as-the-demo-schema">Connect as the DEMO Schema</h3>
<p>Next, we’ll connect to the owner of the source code. We can re-used a SQL Developer for VS Code connection string here, making it a lot easier. In our case, we’ve pre-configured a connection called <code>demo</code> to point to the database schema where we have our source code and database objects.</p>
<p>Let’s connect to it with SQLcl:</p>
<pre><code class="lang-bash">sql -name demo
</code></pre>
<h3 id="heading-create-the-emp-amp-dept-tables">Create the EMP &amp; DEPT Tables</h3>
<p>For this example, let’s keep things simple and use the standard EMP &amp; DEPT tables.</p>
<p>As the <code>demo</code> user, run the following:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> dept(
  deptno <span class="hljs-built_in">number</span>(<span class="hljs-number">2</span>,<span class="hljs-number">0</span>),
  dname  <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">14</span>),
  loc    <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">13</span>),
  <span class="hljs-keyword">constraint</span> pk_dept primary <span class="hljs-keyword">key</span> (deptno)
);

<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> emp(
  empno    <span class="hljs-built_in">number</span>(<span class="hljs-number">4</span>,<span class="hljs-number">0</span>),
  ename    <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">10</span>),
  job      <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">9</span>),
  mgr      <span class="hljs-built_in">number</span>(<span class="hljs-number">4</span>,<span class="hljs-number">0</span>),
  hiredate <span class="hljs-built_in">date</span>,
  sal      <span class="hljs-built_in">number</span>(<span class="hljs-number">7</span>,<span class="hljs-number">2</span>),
  comm     <span class="hljs-built_in">number</span>(<span class="hljs-number">7</span>,<span class="hljs-number">2</span>),
  deptno   <span class="hljs-built_in">number</span>(<span class="hljs-number">2</span>,<span class="hljs-number">0</span>),
  <span class="hljs-keyword">constraint</span> pk_emp primary <span class="hljs-keyword">key</span> (empno),
  <span class="hljs-keyword">constraint</span> fk_deptno <span class="hljs-keyword">foreign</span> <span class="hljs-keyword">key</span> (deptno) <span class="hljs-keyword">references</span> demo.dept (deptno)
);
</code></pre>
<h3 id="heading-populate-the-emp-amp-dept-tables">Populate the EMP &amp; DEPT Tables</h3>
<p>Now that we have a pair of tables, let’s throw some data in them.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">10</span>, <span class="hljs-string">'ACCOUNTING'</span>, <span class="hljs-string">'NEW YORK'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">20</span>, <span class="hljs-string">'RESEARCH'</span>, <span class="hljs-string">'DALLAS'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">30</span>, <span class="hljs-string">'SALES'</span>, <span class="hljs-string">'CHICAGO'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">40</span>, <span class="hljs-string">'OPERATIONS'</span>, <span class="hljs-string">'BOSTON'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7839</span>, <span class="hljs-string">'KING'</span>, <span class="hljs-string">'PRESIDENT'</span>, <span class="hljs-literal">null</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'17-11-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">5000</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7698</span>, <span class="hljs-string">'BLAKE'</span>, <span class="hljs-string">'MANAGER'</span>, <span class="hljs-number">7839</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'1-5-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">2850</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7782</span>, <span class="hljs-string">'CLARK'</span>, <span class="hljs-string">'MANAGER'</span>, <span class="hljs-number">7839</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'9-6-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">2450</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7566</span>, <span class="hljs-string">'JONES'</span>, <span class="hljs-string">'MANAGER'</span>, <span class="hljs-number">7839</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'2-4-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">2975</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7788</span>, <span class="hljs-string">'SCOTT'</span>, <span class="hljs-string">'ANALYST'</span>, <span class="hljs-number">7566</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'13-JUL-87'</span>,<span class="hljs-string">'dd-mm-rr'</span>) - <span class="hljs-number">85</span>,<span class="hljs-number">3000</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7902</span>, <span class="hljs-string">'FORD'</span>, <span class="hljs-string">'ANALYST'</span>, <span class="hljs-number">7566</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'3-12-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">3000</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7369</span>, <span class="hljs-string">'SMITH'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7902</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'17-12-1980'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">800</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7499</span>, <span class="hljs-string">'ALLEN'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'20-2-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1600</span>, <span class="hljs-number">300</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7521</span>, <span class="hljs-string">'WARD'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'22-2-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1250</span>, <span class="hljs-number">500</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7654</span>, <span class="hljs-string">'MARTIN'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'28-9-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1250</span>, <span class="hljs-number">1400</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7844</span>, <span class="hljs-string">'TURNER'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'8-9-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1500</span>, <span class="hljs-number">0</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7876</span>, <span class="hljs-string">'ADAMS'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7788</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'13-JUL-87'</span>, <span class="hljs-string">'dd-mm-rr'</span>) - <span class="hljs-number">51</span>,<span class="hljs-number">1100</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7900</span>, <span class="hljs-string">'JAMES'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'3-12-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">950</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7934</span>, <span class="hljs-string">'MILLER'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7782</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'23-1-1982'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1300</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">commit</span>;
<span class="hljs-keyword">end</span>;
/
</code></pre>
<h2 id="heading-sqlcl-projects-1">SQLcl Projects</h2>
<p>Let’s turn our attention to configuring our first project with SQLcl. A project in SQLcl consists of a number of files:</p>
<ul>
<li><p><strong>Liquibase changelogs</strong></p>
</li>
<li><p><strong>Schema objects</strong></p>
</li>
<li><p><strong>Deployment scripts</strong></p>
</li>
<li><p><strong>Configuration files</strong></p>
</li>
</ul>
<p>These files act in concert to assist with the development and deployment of your code. Using SQLcl Project commands, developers can more easily manage their code without having to manually edit configuration files.</p>
<h3 id="heading-initialize-project">Initialize Project</h3>
<p>While still logged into the <code>DEMO</code> schema, let’s create a new project called <code>DEMO</code>. Run the following command from SQLcl:</p>
<pre><code class="lang-sql">project init -name demo -schemas demo
</code></pre>
<p>You should see the following, or something similar:</p>
<pre><code class="lang-plaintext">—-------------------
PROJECT DETAILS
—-------------------
Project name: demo 
Schema(s): DEMO 
Directory: /Users/scspend/Github/demo 
Connection name: faegans_demo 
Project root: demo 
Your project has been successfully created
</code></pre>
<p>You should also see some files &amp; folders appear in your local working copy directory:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756298969179/947c6314-e779-43de-87a6-7601fa8d2cd0.png" alt class="image--center mx-auto" /></p>
<p>Let’s take a second to run through the purpose of these files.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><code>.dbtools/filters/project.filters</code></td><td>This file controls which types of database objects are included when you run an <code>export</code> command from SQLcl.</td></tr>
</thead>
<tbody>
<tr>
<td><code>.dbtools/project.config.json</code></td><td>This is the project configuration file. You can make changes to the values here, should you need to change any of them. One option that you may want to change is <code>expSavedReports</code>; setting this to <code>true</code> will include any saved APEX reports that you may have.</td></tr>
<tr>
<td><code>.dbtools/project.sqlformat.xm</code>l</td><td>This file controls how the generated SQL will be formatted.</td></tr>
<tr>
<td><code>dist/install.sql</code></td><td>This is the main installation file that will be called when deploying a project. It will call Liquibase, which will handle the bulk of the deployment.</td></tr>
<tr>
<td><code>.gitignore</code></td><td>This file will tell Git which types of files to ignore and not include in the repository.</td></tr>
</tbody>
</table>
</div><h3 id="heading-create-the-initial-release-branch">Create the Initial Release Branch</h3>
<p>Next, we need to create our initial release, which we will call 1.0. This represents the initial set of objects that we will include in our application.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Some commands will need to be run from SQLcl, while others need to be run from the command line. Be mindful of which commands are run where.</div>
</div>

<p>From the command line, run the following:</p>
<pre><code class="lang-bash">git checkout -b release-1.0
</code></pre>
<p>We can validate that we’ve successfully switched to that branch by running the following command:</p>
<pre><code class="lang-bash">git branch --show-current
</code></pre>
<p>You should see <code>release-1.0</code> if successful.</p>
<h2 id="heading-export-commit-amp-stage-schema-objects">Export, Commit &amp; Stage Schema Objects</h2>
<p>Next, we’ll use SQLcl Project’s <code>export</code> command to create scripts for our database objects.</p>
<h3 id="heading-export-schema-objects">Export Schema Objects</h3>
<p>The <code>export</code> command is one of the coolest part of SQLcl Projects. With a single command, I can generate all of the scripts I need for my schema objects. I no longer need to maintain a separate of SQL scripts for my database objects, as I can simply generate them. I can also run <code>export</code> more discretely and export a single, specific object.</p>
<p>Of course, if you want to maintain scripts or already have them that’s also supported.</p>
<p>Let’s use the <code>export</code> command to generate ours from SQLcl:</p>
<pre><code class="lang-sql">project export
</code></pre>
<p>The results will look like this:</p>
<pre><code class="lang-plaintext">The current connection FAEGANS_TP DEMO will be used for all operations
*** TABLES ***
*** REF_CONSTRAINTS ***
-------------------------------
REF_CONSTRAINT                1
TABLE                         2
-------------------------------
Exported 3 objects
Elapsed 11 sec
</code></pre>
<p>Let’s take another look at the local working copy files now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756346315147/0508843d-46ab-407f-87f0-da8320b3056e.png" alt class="image--center mx-auto" /></p>
<p>Notice that there’s three new files in <code>src/database/demo</code>. These are the three schema objects that were in our source schema. If we take a look at one - for instance, <code>emp.sql</code> - we see mostly what we’d expect to see:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> demo.emp (
    empno    <span class="hljs-built_in">number</span>(<span class="hljs-number">4</span>, <span class="hljs-number">0</span>),
    ename    <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">10</span> <span class="hljs-keyword">byte</span>),
    job      <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">9</span> <span class="hljs-keyword">byte</span>),
    mgr      <span class="hljs-built_in">number</span>(<span class="hljs-number">4</span>, <span class="hljs-number">0</span>),
    hiredate <span class="hljs-built_in">date</span>,
    sal      <span class="hljs-built_in">number</span>(<span class="hljs-number">7</span>, <span class="hljs-number">2</span>),
    comm     <span class="hljs-built_in">number</span>(<span class="hljs-number">7</span>, <span class="hljs-number">2</span>),
    deptno   <span class="hljs-built_in">number</span>(<span class="hljs-number">2</span>, <span class="hljs-number">0</span>)
);

<span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> demo.emp
    <span class="hljs-keyword">add</span> <span class="hljs-keyword">constraint</span> pk_emp primary <span class="hljs-keyword">key</span> ( empno )
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">index</span> <span class="hljs-keyword">enable</span>;


<span class="hljs-comment">-- sqlcl_snapshot {"hash":"22c6c8ad205e23dd589af01b8cfdd5e06a2b6add","type":"TABLE","name":"EMP","schemaName":"DEMO","sxml":"\n  &lt;TABLE xmlns=\"http://xmlns.oracle.com/ku\" version=\"1.0\"&gt;\n   &lt;SCHEMA&gt;DEMO&lt;/SCHEMA&gt;\n   &lt;NAME&gt;EMP&lt;/NAME&gt;\n   &lt;RELATIONAL_TABLE&gt;\n      &lt;COL_LIST&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;EMPNO&lt;/NAME&gt;\n            &lt;DATATYPE&gt;NUMBER&lt;/DATATYPE&gt;\n            &lt;PRECISION&gt;4&lt;/PRECISION&gt;\n            &lt;SCALE&gt;0&lt;/SCALE&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;ENAME&lt;/NAME&gt;\n            &lt;DATATYPE&gt;VARCHAR2&lt;/DATATYPE&gt;\n            &lt;LENGTH&gt;10&lt;/LENGTH&gt;\n            &lt;COLLATE_NAME&gt;USING_NLS_COMP&lt;/COLLATE_NAME&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;JOB&lt;/NAME&gt;\n            &lt;DATATYPE&gt;VARCHAR2&lt;/DATATYPE&gt;\n            &lt;LENGTH&gt;9&lt;/LENGTH&gt;\n            &lt;COLLATE_NAME&gt;USING_NLS_COMP&lt;/COLLATE_NAME&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;MGR&lt;/NAME&gt;\n            &lt;DATATYPE&gt;NUMBER&lt;/DATATYPE&gt;\n            &lt;PRECISION&gt;4&lt;/PRECISION&gt;\n            &lt;SCALE&gt;0&lt;/SCALE&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;HIREDATE&lt;/NAME&gt;\n            &lt;DATATYPE&gt;DATE&lt;/DATATYPE&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;SAL&lt;/NAME&gt;\n            &lt;DATATYPE&gt;NUMBER&lt;/DATATYPE&gt;\n            &lt;PRECISION&gt;7&lt;/PRECISION&gt;\n            &lt;SCALE&gt;2&lt;/SCALE&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;COMM&lt;/NAME&gt;\n            &lt;DATATYPE&gt;NUMBER&lt;/DATATYPE&gt;\n            &lt;PRECISION&gt;7&lt;/PRECISION&gt;\n            &lt;SCALE&gt;2&lt;/SCALE&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n         &lt;COL_LIST_ITEM&gt;\n            &lt;NAME&gt;DEPTNO&lt;/NAME&gt;\n            &lt;DATATYPE&gt;NUMBER&lt;/DATATYPE&gt;\n            &lt;PRECISION&gt;2&lt;/PRECISION&gt;\n            &lt;SCALE&gt;0&lt;/SCALE&gt;\n            \n         &lt;/COL_LIST_ITEM&gt;\n      &lt;/COL_LIST&gt;\n      &lt;PRIMARY_KEY_CONSTRAINT_LIST&gt;\n         &lt;PRIMARY_KEY_CONSTRAINT_LIST_ITEM&gt;\n            &lt;NAME&gt;PK_EMP&lt;/NAME&gt;\n            &lt;COL_LIST&gt;\n               &lt;COL_LIST_ITEM&gt;\n                  &lt;NAME&gt;EMPNO&lt;/NAME&gt;\n               &lt;/COL_LIST_ITEM&gt;\n            &lt;/COL_LIST&gt;\n            &lt;USING_INDEX&gt;&lt;/USING_INDEX&gt;\n         &lt;/PRIMARY_KEY_CONSTRAINT_LIST_ITEM&gt;\n      &lt;/PRIMARY_KEY_CONSTRAINT_LIST&gt;\n      &lt;DEFAULT_COLLATION&gt;USING_NLS_COMP&lt;/DEFAULT_COLLATION&gt;\n      &lt;PHYSICAL_PROPERTIES&gt;\n         &lt;HEAP_TABLE&gt;&lt;/HEAP_TABLE&gt;\n      &lt;/PHYSICAL_PROPERTIES&gt;\n      \n   &lt;/RELATIONAL_TABLE&gt;\n&lt;/TABLE&gt;"}</span>
</code></pre>
<p>There is one notable difference - the last line that is commented out. This line was automatically added by SQLcl when we ran the <code>export</code> command. Essentially, this is a JSON document that contains metadata about the SQL script. If we format the XML portion of the JSON document, we get a much clearer picture of what it is:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">TABLE</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://xmlns.oracle.com/ku"</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"1.0"</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">SCHEMA</span>&gt;</span>DEMO<span class="hljs-tag">&lt;/<span class="hljs-name">SCHEMA</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>EMP<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
   <span class="hljs-tag">&lt;<span class="hljs-name">RELATIONAL_TABLE</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>EMPNO<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>NUMBER<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">PRECISION</span>&gt;</span>4<span class="hljs-tag">&lt;/<span class="hljs-name">PRECISION</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">SCALE</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">SCALE</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>ENAME<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>VARCHAR2<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">LENGTH</span>&gt;</span>10<span class="hljs-tag">&lt;/<span class="hljs-name">LENGTH</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">COLLATE_NAME</span>&gt;</span>USING_NLS_COMP<span class="hljs-tag">&lt;/<span class="hljs-name">COLLATE_NAME</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>JOB<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>VARCHAR2<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">LENGTH</span>&gt;</span>9<span class="hljs-tag">&lt;/<span class="hljs-name">LENGTH</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">COLLATE_NAME</span>&gt;</span>USING_NLS_COMP<span class="hljs-tag">&lt;/<span class="hljs-name">COLLATE_NAME</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>MGR<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>NUMBER<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">PRECISION</span>&gt;</span>4<span class="hljs-tag">&lt;/<span class="hljs-name">PRECISION</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">SCALE</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">SCALE</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>HIREDATE<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>DATE<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>SAL<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>NUMBER<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">PRECISION</span>&gt;</span>7<span class="hljs-tag">&lt;/<span class="hljs-name">PRECISION</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">SCALE</span>&gt;</span>2<span class="hljs-tag">&lt;/<span class="hljs-name">SCALE</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>COMM<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>NUMBER<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">PRECISION</span>&gt;</span>7<span class="hljs-tag">&lt;/<span class="hljs-name">PRECISION</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">SCALE</span>&gt;</span>2<span class="hljs-tag">&lt;/<span class="hljs-name">SCALE</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>DEPTNO<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">DATATYPE</span>&gt;</span>NUMBER<span class="hljs-tag">&lt;/<span class="hljs-name">DATATYPE</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">PRECISION</span>&gt;</span>2<span class="hljs-tag">&lt;/<span class="hljs-name">PRECISION</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">SCALE</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">SCALE</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">PRIMARY_KEY_CONSTRAINT_LIST</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">PRIMARY_KEY_CONSTRAINT_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>PK_EMP<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST</span>&gt;</span>
               <span class="hljs-tag">&lt;<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">NAME</span>&gt;</span>EMPNO<span class="hljs-tag">&lt;/<span class="hljs-name">NAME</span>&gt;</span>
               <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST_ITEM</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">COL_LIST</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">USING_INDEX</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">USING_INDEX</span>&gt;</span>
         <span class="hljs-tag">&lt;/<span class="hljs-name">PRIMARY_KEY_CONSTRAINT_LIST_ITEM</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">PRIMARY_KEY_CONSTRAINT_LIST</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">DEFAULT_COLLATION</span>&gt;</span>USING_NLS_COMP<span class="hljs-tag">&lt;/<span class="hljs-name">DEFAULT_COLLATION</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">PHYSICAL_PROPERTIES</span>&gt;</span>
         <span class="hljs-tag">&lt;<span class="hljs-name">HEAP_TABLE</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">HEAP_TABLE</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">PHYSICAL_PROPERTIES</span>&gt;</span>
   <span class="hljs-tag">&lt;/<span class="hljs-name">RELATIONAL_TABLE</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">TABLE</span>&gt;</span>
</code></pre>
<p>It’s a lot more obvious that the XML portion of this defines our table and associated constraint.</p>
<p>The point of this JSON document is a “snapshot” or sorts that SQLcl will use when managing changelogs. It’s best to leave it alone, as changing it can likely cause more harm than good.</p>
<h3 id="heading-commit-to-git">Commit to Git</h3>
<p>Now that we have our initial scripts, let’s commit those to Git. This is necessary because in the next step - staging - it will only stage files that are committed.</p>
<p>Let’s add and commit the new files that were created as part of the project creation and export. Run the following commands from the command line:</p>
<pre><code class="lang-bash">git add -A
git commit -m <span class="hljs-string">"Initial commit"</span>
</code></pre>
<p>After the <code>commit</code>, you will see something similar to this:</p>
<pre><code class="lang-bash">[release-1.0 0005da5] Initial commit
 9 files changed, 254 insertions(+)
 create mode 100644 .dbtools/filters/project.filters
 create mode 100644 .dbtools/project.config.json
 create mode 100644 .dbtools/project.sqlformat.xml
 create mode 100644 .gitignore
 create mode 100644 dist/install.sql
 create mode 100644 src/database/demo/ref_constraints/fk_deptno.sql
 create mode 100644 src/database/demo/tables/dept.sql
 create mode 100644 src/database/demo/tables/emp.sql
</code></pre>
<h3 id="heading-staging-the-project">Staging the Project</h3>
<p>Now that we’ve committed our changes, we can stage the project. Staging is a process where SQLcl will create a set of files that will eventually become a release. Think of staged files as a temporary place that we can add and remove files from during our development process before we create a release. Files that are staged will eventually become released.</p>
<p>To stage your project, enter the following from SQLcl:</p>
<pre><code class="lang-sql">project stage
</code></pre>
<p>If we look at the local working copy now, notice that there’s a whole new set of files &amp; folders, highlighted in green:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756497055305/2e857d10-05b2-49c5-b865-f9384b7c12f1.png" alt class="image--center mx-auto" /></p>
<p>Everything under the <code>dist/releases/next</code> folder is the staging area. It’s extremely similar to the <code>src/database/demo</code> folder, as it’s literally a copy of those files with one notable difference. Here’s the <code>emp.sql</code> file under <code>dist/releases/next/changes/release-1.0/demo/tables</code>:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- liquibase formatted sql</span>
<span class="hljs-comment">-- changeset DEMO:1756306177836 stripComments:false  logicalFilePath:release-1.0/demo/tables/emp.sql runAlways:false runOnChange:false replaceIfExists:true failOnError:true</span>
<span class="hljs-comment">-- sqlcl_snapshot src/database/demo/tables/emp.sql:null:3fb1c8da8567805e174f2feea15ffb986a37c120:create</span>

<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> demo.emp (
    empno    <span class="hljs-built_in">number</span>(<span class="hljs-number">4</span>, <span class="hljs-number">0</span>),
    ename    <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">10</span> <span class="hljs-keyword">byte</span>),
    job      <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">9</span> <span class="hljs-keyword">byte</span>),
    mgr      <span class="hljs-built_in">number</span>(<span class="hljs-number">4</span>, <span class="hljs-number">0</span>),
    hiredate <span class="hljs-built_in">date</span>,
    sal      <span class="hljs-built_in">number</span>(<span class="hljs-number">7</span>, <span class="hljs-number">2</span>),
    comm     <span class="hljs-built_in">number</span>(<span class="hljs-number">7</span>, <span class="hljs-number">2</span>),
    deptno   <span class="hljs-built_in">number</span>(<span class="hljs-number">2</span>, <span class="hljs-number">0</span>)
);

<span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> demo.emp
    <span class="hljs-keyword">add</span> <span class="hljs-keyword">constraint</span> pk_emp primary <span class="hljs-keyword">key</span> ( empno )
        <span class="hljs-keyword">using</span> <span class="hljs-keyword">index</span> <span class="hljs-keyword">enable</span>;
</code></pre>
<p>Notice that we now have Liquibase-related comments added to the top of the file and the SQLcl-generated JSON document at the bottoms of the file is gone. This is the standard way of marking up a file so that Liquibase can process it.</p>
<h3 id="heading-changelogs">Changelogs</h3>
<p>There’s now three “changelog” files that were added to the local working copy. Let’s dig into each one of them.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><code>main.changelog.xml</code></td><td>This file is referenced by the <code>install.sql</code> script when Liquibase runs. it simply has an include directive that points to <code>release.changelog.xml</code>, the next file in our sequence of changelog files.</td></tr>
</thead>
<tbody>
<tr>
<td><code>release.changelog.xml</code></td><td>This file is the changelog for the specific release. If additional directives needed to be added, you can make the changes here.</td></tr>
<tr>
<td><code>stage.changelog.xml</code></td><td>This is where the actual DLL changes are tracked. If you inspect this file, you will see references to the .sql files under the <code>changes/release-1.0</code> directory. Liquibase will run these files in the order that they are listed during a deployment. It is not recommended to change this file manually, as it will be managed automatically when you run <code>project stage</code>.</td></tr>
</tbody>
</table>
</div><h3 id="heading-utilities">Utilities</h3>
<p>There’s also a pair of files in the utilities - or <code>utils</code> - directory:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><code>utils/prechecks.sql</code></td><td>Ensures that the correct SQLcl version is used to deploy the changes</td></tr>
</thead>
<tbody>
<tr>
<td><code>utils/recompile.sql</code></td><td>Script to recompile invalid objects in your schema</td></tr>
</tbody>
</table>
</div><h3 id="heading-add-custom-files">Add Custom Files</h3>
<p>Before we can create our release, it would be nice to also automatically populate the <code>EMP</code> &amp; <code>DEPT</code> tables with their respective datasets. SQLcl Projects allows us to also add custom files - those that are not automatically generated based on database objects to our project. Let’s do that.</p>
<p>In a SQLcl session, enter the following:</p>
<pre><code class="lang-sql">project stage add-custom -file-name seed.sql
</code></pre>
<p>Once this command is run, have a close look at your local working copy. A new file - <code>seed.sql</code> - has been added at <code>releases/next/changes/release-1.0/_custom</code>. If we take a look at that file, it’s blank aside from the three lines of Liquibase control information:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- liquibase formatted sql</span>
<span class="hljs-comment">-- changeset  SqlCl:1756384780690 stripComments:false logicalFilePath:release-1.0/_custom/seed.sql</span>
<span class="hljs-comment">-- sqlcl_snapshot dist/releases/next/changes/release-1.0/_custom/seed.sql:null:null:custom</span>
</code></pre>
<p>We will need to add our SQL script to this file, starting at line 4. The end result of <code>seed.sql</code> should look like this:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- liquibase formatted sql</span>
<span class="hljs-comment">-- changeset  SqlCl:1756384780690 stripComments:false logicalFilePath:release-1.0/_custom/seed.sql</span>
<span class="hljs-comment">-- sqlcl_snapshot dist/releases/next/changes/release-1.0/_custom/seed.sql:null:null:custom</span>
<span class="hljs-keyword">begin</span>
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">10</span>, <span class="hljs-string">'ACCOUNTING'</span>, <span class="hljs-string">'NEW YORK'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">20</span>, <span class="hljs-string">'RESEARCH'</span>, <span class="hljs-string">'DALLAS'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">30</span>, <span class="hljs-string">'SALES'</span>, <span class="hljs-string">'CHICAGO'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.dept <span class="hljs-keyword">values</span>(<span class="hljs-number">40</span>, <span class="hljs-string">'OPERATIONS'</span>, <span class="hljs-string">'BOSTON'</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7839</span>, <span class="hljs-string">'KING'</span>, <span class="hljs-string">'PRESIDENT'</span>, <span class="hljs-literal">null</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'17-11-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">5000</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7698</span>, <span class="hljs-string">'BLAKE'</span>, <span class="hljs-string">'MANAGER'</span>, <span class="hljs-number">7839</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'1-5-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">2850</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7782</span>, <span class="hljs-string">'CLARK'</span>, <span class="hljs-string">'MANAGER'</span>, <span class="hljs-number">7839</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'9-6-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">2450</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7566</span>, <span class="hljs-string">'JONES'</span>, <span class="hljs-string">'MANAGER'</span>, <span class="hljs-number">7839</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'2-4-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">2975</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7788</span>, <span class="hljs-string">'SCOTT'</span>, <span class="hljs-string">'ANALYST'</span>, <span class="hljs-number">7566</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'13-JUL-87'</span>,<span class="hljs-string">'dd-mm-rr'</span>) - <span class="hljs-number">85</span>,<span class="hljs-number">3000</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7902</span>, <span class="hljs-string">'FORD'</span>, <span class="hljs-string">'ANALYST'</span>, <span class="hljs-number">7566</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'3-12-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">3000</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7369</span>, <span class="hljs-string">'SMITH'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7902</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'17-12-1980'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">800</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7499</span>, <span class="hljs-string">'ALLEN'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'20-2-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1600</span>, <span class="hljs-number">300</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7521</span>, <span class="hljs-string">'WARD'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'22-2-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1250</span>, <span class="hljs-number">500</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7654</span>, <span class="hljs-string">'MARTIN'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'28-9-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1250</span>, <span class="hljs-number">1400</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7844</span>, <span class="hljs-string">'TURNER'</span>, <span class="hljs-string">'SALESMAN'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'8-9-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1500</span>, <span class="hljs-number">0</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7876</span>, <span class="hljs-string">'ADAMS'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7788</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'13-JUL-87'</span>, <span class="hljs-string">'dd-mm-rr'</span>) - <span class="hljs-number">51</span>,<span class="hljs-number">1100</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">20</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7900</span>, <span class="hljs-string">'JAMES'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7698</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'3-12-1981'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">950</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">30</span>);
<span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> demo.emp <span class="hljs-keyword">values</span>(<span class="hljs-number">7934</span>, <span class="hljs-string">'MILLER'</span>, <span class="hljs-string">'CLERK'</span>, <span class="hljs-number">7782</span>, <span class="hljs-keyword">to_date</span>(<span class="hljs-string">'23-1-1982'</span>,<span class="hljs-string">'dd-mm-yyyy'</span>),<span class="hljs-number">1300</span>, <span class="hljs-literal">null</span>, <span class="hljs-number">10</span>);
<span class="hljs-keyword">commit</span>;
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>Make sure that you save <code>seed.sql</code> before continuing.</p>
<p>Before we generate a release, have a look at the <code>stage.changelog.xml</code> file one more time:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">databaseChangeLog</span> <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span>
                   <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.liquibase.org/xml/ns/dbchangelog"</span>
                   <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"demo/tables/dept.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"demo/tables/emp.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"demo/ref_constraints/fk_deptno.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"_custom/seed.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">databaseChangeLog</span>&gt;</span>
</code></pre>
<p>Notice that there is a new reference for our <code>seed.sql</code> file. This was automatically added when we staged the file a moment ago.</p>
<h2 id="heading-creating-amp-deploying-a-release">Creating &amp; Deploying a Release</h2>
<p>Once we have finished a phase of development and are ready to ship our code, we need to create a release and deploy it. SQLcl Projects makes this as easy as running a couple of commands. Let’s get started.</p>
<h3 id="heading-commit">Commit</h3>
<p>Before creating a release, we need to be sure that we commit our code into Git. SQLcl will ignore any code that is not committed when building releases, so while this step is small, it’s critical.</p>
<p>As we have done in the past, from a terminal session, issue the following two commands:</p>
<pre><code class="lang-bash">git add -A
git commit -m <span class="hljs-string">"Release 1.0"</span>
</code></pre>
<p>You should see something similar to the following:</p>
<pre><code class="lang-bash">[release-1.0 a78c91c] Release 1.0
 9 files changed, 402 insertions(+)
 create mode 100644 dist/releases/main.changelog.xml
 create mode 100644 dist/releases/next/changes/release-1.0/_custom/seed.sql
 create mode 100644 dist/releases/next/changes/release-1.0/demo/ref_constraints/fk_deptno.sql
 create mode 100644 dist/releases/next/changes/release-1.0/demo/tables/dept.sql
 create mode 100644 dist/releases/next/changes/release-1.0/demo/tables/emp.sql
 create mode 100644 dist/releases/next/changes/release-1.0/stage.changelog.xml
 create mode 100644 dist/releases/next/release.changelog.xml
 create mode 100644 dist/utils/prechecks.sql
 create mode 100644 dist/utils/recompile.sql
</code></pre>
<h3 id="heading-verify-the-project">Verify the Project</h3>
<p>It’s a good idea to verify that your project is ready to be released. The verify step looks at your changelog files, as well as other Liquibase-specific configurations to ensure that it can build a functional release. Adding the <code>-verbose</code> tag is not necessary, but when used, will highlight the specifics of any error or warning.</p>
<pre><code class="lang-plaintext">project verify -verbose
</code></pre>
<p>You should see something similar:</p>
<pre><code class="lang-plaintext">-------- Results Summary ----------
Errors:     0
Warnings:   0
Info:       6
---------------------------------

Level     Group Name     Test Name                     Message
-----     ----------     -----------                   -------------------------
INFO      settings       verifynonpublicsettings       No unsupported internal settings found
INFO      init           verifyprojectname             The final project name will be "demo"
INFO      examples       exampletest                   a message
INFO      stage          stagechangelogcomplete        Stage Changelog validation found no issues.
INFO      project        sqlclversion                  SQLcl Version check passed
INFO      snapshot       verifysnapshot                Snapshot validation found no issues.
</code></pre>
<h3 id="heading-create-the-release">Create the Release</h3>
<p>Now that we have exported our objects as scripts and added a custom one to populate them with data, staged our changes and verified that they will run, it’s time to create out first release.</p>
<pre><code class="lang-bash">project release -version 1.0 -verbose
</code></pre>
<p>Creating a release does a few of things to our files &amp; folders:</p>
<ul>
<li><p>Moves the contents of the folder <code>dist/releases/next</code> to <code>dist/releases/1.0</code></p>
</li>
<li><p>Creates a fresh <code>dist/releases/next</code> directory ready for the next release</p>
</li>
<li><p>Updates the <code>main.changelog.xml</code> file to include a reference to the new release</p>
</li>
<li><p>Updates the <code>project.config.json</code> file with the latest release number</p>
</li>
</ul>
<h3 id="heading-generate-the-artifact">Generate the Artifact</h3>
<p>While it’s possible to manually take the source and run it against our target environment, that’s not the best approach at all. SQLcl Projects can automatically generate an artifact that consists of all necessary files to deploy this release.</p>
<p>This artifact will be inclusive. In other words, if you have release 1.0 and the artifact contains release 1.1, running it will only apply changes in 1.1. With a clean installation, the artifact will install 1.0 as well as 1.1. We’ll demonstrate an “upgrade” flow next, but before we do that, let’s generate the artifact.</p>
<p>In a SQLcl session, run the following command:</p>
<pre><code class="lang-sql">project gen-artifact -version 1.0
</code></pre>
<p>This command will generate an artifact in the form of a ZIP file. There should also now be a new folder in your local directory called <code>artifact</code>. Inside that folder will be a file called <code>demo-1.0.zip</code> - a combination of the project name and release.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756391474381/2fbde092-3b80-4acf-b44e-1954e40e9eb4.png" alt class="image--center mx-auto" /></p>
<p>Feel free to unzip the file and explore the contents. It will be very similar to what the <code>dist/releases/1.0</code> directory in the working copy looks like.</p>
<p>One important thing to note - any file in the <code>artifact</code> directory will NOT be checked into your repository. This is due to the fact that that in the <code>.gitignore</code> file, there is a line that tells Git to ignore the artifact directory altogether.</p>
<p>You will need to manually move these artifacts to the system where they will be deployed.</p>
<h2 id="heading-deploying-a-release">Deploying a Release</h2>
<p>Now that we have an artifact, we’re ready to deploy. Before we do, let’s review how Liquibase deploys artifacts. When deployed, SQLcl Projects will issue a command to run the <code>install.sql</code> file that is found in the ZIP. The install.sql file will, in turn run the contents of the <code>main.changelog.xml</code> file. That file will, in turn, run <code>release.changelog.xml</code>. That file will, in turn, execute all of the discrete scripts to create table, constraints and populate the tables with data.</p>
<p>All of the discrete scripts that SQLcl Projects creates will refer to database objects in the <code>schema.object</code> format. Because of this, there’s two ways we can deploy our application:</p>
<ul>
<li><p>From the <code>DEMO</code> schema</p>
</li>
<li><p>From a more privileged schema</p>
</li>
</ul>
<p>Which one you use will depend on your needs. If all of your database objects will be in a single schema, and that schema has the required privileges to run the installation scripts, then you can connect directly to that schema and deploy. However, if your application is spread across multiple schemas, you may want to connect as a more privileged schema and deploy from there.</p>
<p>Keep in mind that when SQLcl Projects deploys an application, it will need to create a couple of tables to track which release is installed. If you change your mind, it gets tricky reconciling the data in these tables, so when in doubt, err on the side of the more privileged schema to give yourself room to grow.</p>
<p>To keep things simple, we will use the <code>DEMO</code> schema on the target database for our deployment.</p>
<h3 id="heading-create-a-privileged-installation-user">Create a Privileged Installation User</h3>
<p>Should you need to create a privileged user for installations, take a look at how we do this for APEX-SERT. We use a schema called ACDC to run all of our Liquibase deployments. This is a much safer way to do things vs. using something like SYSTEM or ADMIN (ADB), as we only grant the privileges needed to get the job done.</p>
<p>Details about how to create the ACDC schema can be found <a target="_blank" href="https://github.com/oracle-samples/apex-sert/blob/main/doc/install_guide.md">here</a> - specifically in section 2.2.1.</p>
<h3 id="heading-create-the-demo-schema-on-the-source-database-1">Create the DEMO Schema on the Source Database</h3>
<p>Since we’re going to deploy with the <code>DEMO</code> user, let’s create that on the target database.</p>
<ol>
<li><p>Connect to the target database as a DBA-level user, such as <code>SYSTEM</code> or <code>ADMIN</code> (Autonomous).</p>
</li>
<li><p>Run the following commands, ensuring to adjust the password and tablespace name, if needed.</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> demo <span class="hljs-keyword">identified</span> <span class="hljs-keyword">by</span> <span class="hljs-string">"StrongPassword1$"</span>;
<span class="hljs-keyword">alter</span> <span class="hljs-keyword">user</span> demo <span class="hljs-keyword">quota</span> <span class="hljs-keyword">unlimited</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">users</span>;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">connect</span>, <span class="hljs-keyword">resource</span>, <span class="hljs-keyword">create</span> <span class="hljs-keyword">view</span> <span class="hljs-keyword">to</span> demo;
</code></pre>
<ol start="3">
<li>Next, create a connection in SQL Developer for VS Code for this schema. Name that connection <code>demo_target</code>. If using ADB, please be sure to select the <code>_LOW</code> connection; using others can cause issues with Liquibase.</li>
</ol>
<h3 id="heading-deploy-the-artifact">Deploy the Artifact</h3>
<p>Now that we have a new schema ready to go on the target database, let’s connect to it and kick off the deployment.</p>
<ol>
<li>Quit SQLcl and then re-connect to the target database as the <code>DEMO</code> schema.</li>
</ol>
<pre><code class="lang-bash">sql -name demo_target
</code></pre>
<p>You should now be connected to a fresh schema with no objects. Let’s deploy our schema.</p>
<ol start="2">
<li>Enter the following command:</li>
</ol>
<pre><code class="lang-sql">project deploy -file artifact/demo-1.0.zip -verbose
</code></pre>
<ol start="3">
<li>After a few seconds, you will see something like the following:</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">Check</span> <span class="hljs-keyword">database</span> connection...
<span class="hljs-keyword">Extract</span> the <span class="hljs-keyword">file</span> <span class="hljs-keyword">name</span>: demo<span class="hljs-number">-1.0</span>
Artifact decompression <span class="hljs-keyword">in</span> progress...
Artifact decompressed: /<span class="hljs-keyword">var</span>/folders/<span class="hljs-number">0</span>_/nwtj13qx4bv2gzs667vs3h3w0000gn/T/<span class="hljs-number">23</span>c6cc92<span class="hljs-number">-35</span>ce<span class="hljs-number">-4</span>f9f<span class="hljs-number">-9e2</span>e<span class="hljs-number">-0</span>ffb00f6e2d712784159437265393156
<span class="hljs-keyword">Starting</span> the migration...
Running Changeset: <span class="hljs-keyword">release</span><span class="hljs-number">-1.0</span>/demo/<span class="hljs-keyword">tables</span>/dept.sql::<span class="hljs-number">1756346516693</span>::DEMO
<span class="hljs-keyword">Table</span> DEMO.DEPT created.


<span class="hljs-keyword">Table</span> DEMO.DEPT altered.
Running Changeset: <span class="hljs-keyword">release</span><span class="hljs-number">-1.0</span>/demo/<span class="hljs-keyword">tables</span>/emp.sql::<span class="hljs-number">1756346516709</span>::DEMO
<span class="hljs-keyword">Table</span> DEMO.EMP created.


<span class="hljs-keyword">Table</span> DEMO.EMP altered.
Running Changeset: <span class="hljs-keyword">release</span><span class="hljs-number">-1.0</span>/demo/ref_constraints/fk_deptno.sql::<span class="hljs-number">1756346516684</span>::DEMO
<span class="hljs-keyword">Table</span> DEMO.EMP altered.
Running Changeset: <span class="hljs-keyword">release</span><span class="hljs-number">-1.0</span>/_custom/seed.sql::<span class="hljs-number">1756384780690</span>::SqlCl

Liquibase: <span class="hljs-keyword">Update</span> has been successful. <span class="hljs-keyword">Rows</span> affected: <span class="hljs-number">4</span>
Installing/updating schemas
<span class="hljs-comment">--Starting Liquibase at 2025-08-29T06:41:26.162524 using Java 21.0.8 (version 4.30.0 #0 built at 2025-04-01 10:24+0000)</span>
<span class="hljs-keyword">Table</span> DEMO.DEPT created.


<span class="hljs-keyword">Table</span> DEMO.DEPT altered.
<span class="hljs-keyword">Table</span> DEMO.EMP created.


<span class="hljs-keyword">Table</span> DEMO.EMP altered.
<span class="hljs-keyword">Table</span> DEMO.EMP altered.


<span class="hljs-keyword">UPDATE</span> SUMMARY
Run:                          <span class="hljs-number">4</span>
Previously run:               <span class="hljs-number">0</span>
Filtered <span class="hljs-keyword">out</span>:                 <span class="hljs-number">0</span>
<span class="hljs-comment">-------------------------------</span>
Total <span class="hljs-keyword">change</span> <span class="hljs-keyword">sets</span>:            <span class="hljs-number">4</span>


Produced <span class="hljs-keyword">logfile</span>: sqlcl-lb<span class="hljs-number">-1756467685000.</span><span class="hljs-keyword">log</span>

Operation completed successfully.
<span class="hljs-keyword">Migration</span> has been completed
Removing the decompressed artifact: /<span class="hljs-keyword">var</span>/folders/<span class="hljs-number">0</span>_/nwtj13qx4bv2gzs667vs3h3w0000gn/T/<span class="hljs-number">23</span>c6cc92<span class="hljs-number">-35</span>ce<span class="hljs-number">-4</span>f9f<span class="hljs-number">-9e2</span>e<span class="hljs-number">-0</span>ffb00f6e2d712784159437265393156...
</code></pre>
<p>We can easily verify that the release was deployed by querying the <code>EMP</code> table, like so:</p>
<pre><code class="lang-sql">SQL&gt; <span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> emp;

   EMPNO ENAME     JOB              MGR HIREDATE         SAL    COMM    DEPTNO 
________ _________ ____________ _______ ____________ _______ _______ _________ 
    7839 KING      PRESIDENT            17-NOV-81       5000                10 
    7698 BLAKE     MANAGER         7839 01-MAY-81       2850                30 
    7782 CLARK     MANAGER         7839 09-JUN-81       2450                10 
    7566 JONES     MANAGER         7839 02-APR-81       2975                20 
    7788 SCOTT     ANALYST         7566 19-APR-87       3000                20 
    7902 FORD      ANALYST         7566 03-DEC-81       3000                20 
    7369 SMITH     CLERK           7902 17-DEC-80        800                20 
    7499 ALLEN     SALESMAN        7698 20-FEB-81       1600     300        30 
    7521 WARD      SALESMAN        7698 22-FEB-81       1250     500        30 
    7654 MARTIN    SALESMAN        7698 28-SEP-81       1250    1400        30 
    7844 TURNER    SALESMAN        7698 08-SEP-81       1500       0        30 
    7876 ADAMS     CLERK           7788 23-MAY-87       1100                20 
    7900 JAMES     CLERK           7698 03-DEC-81        950                30 
    7934 MILLER    CLERK           7782 23-JAN-82       1300                10 

14 rows selected.
</code></pre>
<p>If you see 14 rows - congratulations - your release was successfully deployed!</p>
<h3 id="heading-liquibase-tables">Liquibase Tables</h3>
<p>If you look closely at the DEMO schema, you will see an additional three tables were created. These are used by Liquibase to track deployments. Data in these tables should never be modified, as Liquibase will make changes as additional releases are deployed.</p>
<p>For the curious, here’s the names of them and their purpose:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><code>DATABASECHANGELOG</code></td><td>Tracks which Liquibase changesets have been applied and ensures that the same changeset is not run twice.</td></tr>
</thead>
<tbody>
<tr>
<td><code>DATABASECHANGELOGLOCK</code></td><td>Ensures that only one Liquibase session is run at a time.</td></tr>
<tr>
<td><code>DATABASECHANGELOG_ACTIONS</code></td><td>SQLcl’s version of which changelog has been run.</td></tr>
</tbody>
</table>
</div><p>I always feel a little uneasy mingling Liquibase tables with my application tables. Hence, I typically will opt to manage my deployments with a separate schema, as mentioned earlier.</p>
<h2 id="heading-merge-your-changes">Merge Your Changes</h2>
<p>Now is a good time to merge your changes to the repository. This way, the next branch that we pull will have the most up-to-date code. Let’s run through the steps.</p>
<h3 id="heading-another-commit">Another Commit</h3>
<p>First, let’s add &amp; commit all of the files as we’ve done before. From a terminal, run the following:</p>
<pre><code class="lang-bash">git add -A
git commit -m <span class="hljs-string">"Release 1.0"</span>
</code></pre>
<h3 id="heading-push-to-github">Push to Github</h3>
<p>Next, we need to push our committed changes back to the repository.</p>
<pre><code class="lang-bash">git push -u origin release-1.0
</code></pre>
<p>You should see something similar to this:</p>
<pre><code class="lang-bash">[release-1.0 d13db64] Release 1.0
 10 files changed, 35 insertions(+), 8 deletions(-)
 create mode 100644 dist/releases/1.0/changes/release-1.0/_custom/seed.sql
 rename dist/releases/{next =&gt; 1.0}/changes/release-1.0/demo/ref_constraints/fk_deptno.sql (100%)
 rename dist/releases/{next =&gt; 1.0}/changes/release-1.0/demo/tables/dept.sql (100%)
 rename dist/releases/{next =&gt; 1.0}/changes/release-1.0/demo/tables/emp.sql (100%)
 rename dist/releases/{next =&gt; 1.0}/changes/release-1.0/stage.changelog.xml (100%)
 create mode 100644 dist/releases/1.0/release.changelog.xml
 delete mode 100644 dist/releases/next/changes/release-1.0/_custom/seed.sql
sspendol@Scotts-MacBook-Air sqlcl-projects-demo % git push -u origin release-1.0
Enumerating objects: 56, <span class="hljs-keyword">done</span>.
Counting objects: 100% (56/56), <span class="hljs-keyword">done</span>.
Delta compression using up to 10 threads
Compressing objects: 100% (43/43), <span class="hljs-keyword">done</span>.
Writing objects: 100% (54/54), 11.20 KiB | 3.73 MiB/s, <span class="hljs-keyword">done</span>.
Total 54 (delta 10), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (10/10), <span class="hljs-keyword">done</span>.
remote: 
remote: Create a pull request <span class="hljs-keyword">for</span> <span class="hljs-string">'release-1.0'</span> on GitHub by visiting:
remote:      https://github.com/sspendol/sqlcl-projects-demo/pull/new/release-1.0
remote: 
To github.com:sspendol/sqlcl-projects-demo.git
 * [new branch]      release-1.0 -&gt; release-1.0
branch <span class="hljs-string">'release-1.0'</span> <span class="hljs-built_in">set</span> up to track <span class="hljs-string">'origin/release-1.0'</span>.
</code></pre>
<h3 id="heading-confirm-amp-merge-pull-request">Confirm &amp; Merge Pull Request</h3>
<p>Pushing the changes will trigger a pull request on Github. If we switch back to our browser, we should see that pull request:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756497904231/7d0e459f-6a22-4a76-a27a-e9ec3fa741df.png" alt class="image--center mx-auto" /></p>
<p>Go ahead and click the <strong>Compare &amp; pull request</strong> button. Enter any comments that you’d like to track and click <strong>Create pull request</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756497937930/38ce8cc6-23d0-4cf6-b484-7f0b2be1d252.png" alt class="image--center mx-auto" /></p>
<p>Github will check to ensure that there are no conflicts with your pull request, and if so, allow you to merge it into the <code>main</code> branch. Click <strong>Merge pull request</strong> to do so.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756497965315/aedf823e-3ee5-46ec-b253-af37b1821229.png" alt class="image--center mx-auto" /></p>
<p>One more step - enter a commit message, select a user and then click <strong>Confirm merge</strong> to complete the merge process.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756498040284/51054bbf-3bc5-4a58-b393-ec8bcf9b14b7.png" alt class="image--center mx-auto" /></p>
<p>Once your changes are merged, you can safely delete the <code>release-1.0</code> branch if you want to.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756498078609/746693a9-f9d1-474e-b109-ae7647486382.png" alt class="image--center mx-auto" /></p>
<p>If you click the <strong>Code</strong> tab, you should see your committed source code from the local working copy now in the <code>main</code> branch.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756470357119/d30b1aa2-3e7c-4dfe-a8ea-84613f948667.png" alt class="image--center mx-auto" /></p>
<p>Keep in mind that a merge will usually trigger a code review process, and developers don’t usually approve their own merges.</p>
<h1 id="heading-changing-the-project">Changing the Project</h1>
<p>If you’ve been a developer for more than 15 minutes, you’re well aware of the fact that end users sometimes don’t know what they want. For example, with our simple <code>EMP</code> &amp; <code>DEPT</code> tables, how would we add an additional column to <code>EMP</code> - say something like <code>EMAIL</code> - and deploy that change on top of release 1.0? This is where SQLcl Projects &amp; Liquibase shine.</p>
<h2 id="heading-modifying-emp">Modifying EMP</h2>
<p>We clearly can’t recreate <code>EMP</code> from scratch, as there is data in there that we don’t want to use. Thus, we will need to issue an <code>ALTER TABLE</code> command as part of our next release so that the new column is added without interfering with the data. Or do we…</p>
<h3 id="heading-create-a-new-branch">Create a New Branch</h3>
<p>Let’s start out by creating a new branch in our repository:</p>
<pre><code class="lang-bash">git checkout -b release-1.1
</code></pre>
<h3 id="heading-alter-the-table">Alter the Table</h3>
<p>One of the benefits of SQLcl Projects is that it doesn’t care how the DDL changes are made. While we could create a file that contains the <code>ALTER TABLE</code> statement, we can also just run the <code>ALTER TABLE</code> and let SQLcl Projects do all the work. Let’s opt for that route.</p>
<p>Connect to your source database as the <code>DEMO</code> user.</p>
<pre><code class="lang-bash">sql -name demo
</code></pre>
<p>Issue the <code>ALTER TABLE</code> command to add the email column. Don’t worry about saving this script anywhere, as Liquibase will automatically generate it later.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> emp <span class="hljs-keyword">add</span> (email <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">100</span>));
</code></pre>
<h3 id="heading-export-the-project">Export the Project</h3>
<p>Next, we need to export the project again. Again, you can do this discretely for a single database object, or just run the command naked to get any objects that have changed.</p>
<pre><code class="lang-sql">project export
</code></pre>
<p>Once you’ve exported the project, notice that the file <code>src/database/demo/tables/emp.sql</code> has changed. It now includes our new <code>EMAIL</code> column.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756471775226/6acf9b39-4224-4c18-8560-13efb738f627.png" alt class="image--center mx-auto" /></p>
<p>“We can’t run this script!” you might be thinking. And you’re right and wrong at the same time.</p>
<p>You’re right in that we can’t run this script, as it would fail. But this is not the script we’re going to run. SQLcl Projects will take care of that when we stage our changes.</p>
<h3 id="heading-add-a-custom-file">Add a Custom File</h3>
<p>Let’s add a custom file that when run, will populate the new <code>EMAIL</code> column.</p>
<pre><code class="lang-sql">project stage add-custom -file-name seed_email.sql
</code></pre>
<p>Notice that the new file should be visible in the <code>dist/releases/next/changes/release-1.1/_custom</code> directory of our local working copy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756499573290/82386d5d-b15c-45d5-aad6-98fb355d36dd.png" alt class="image--center mx-auto" /></p>
<p>This concept of “next” release is important. As you start to stage files, they will be placed under the <code>releases/next</code> directory. They will remain here until a new release is created. At that point, they will be moved to a folder named after the release version, and a new <code>releases/next</code> directory will be created.</p>
<p>Edit the <code>seed_email.sql</code> file and add the following after the comments:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- liquibase formatted sql</span>
<span class="hljs-comment">-- changeset  SqlCl:1756471732399 stripComments:false logicalFilePath:version-1.1/_custom/seed_email.sql</span>
<span class="hljs-comment">-- sqlcl_snapshot dist/releases/next/changes/version-1.1/_custom/seed_email.sql:null:null:custom</span>
<span class="hljs-keyword">begin</span>
<span class="hljs-keyword">update</span> demo.emp <span class="hljs-keyword">set</span> email = <span class="hljs-keyword">lower</span>(ename) || <span class="hljs-string">'@example.com'</span>;
<span class="hljs-keyword">commit</span>;
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>Don’t forget to save your changes.</p>
<h3 id="heading-add-amp-commit-your-files">Add &amp; Commit Your Files</h3>
<p>Remember - before we can stage the project, we will need to add &amp; commit our changes.</p>
<p>From a terminal, enter the following commands:</p>
<pre><code class="lang-bash">git add -A
git commit -m <span class="hljs-string">"Release 1.1"</span>
</code></pre>
<h3 id="heading-stage-the-project">Stage the Project</h3>
<p>Now, from SQLcl, stage the project with the following command:</p>
<pre><code class="lang-sql">project stage
</code></pre>
<p>You should now see a new version of <code>emp.sql</code> file in your local working copy under <code>dist/releases/next/changes/release-1.1</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756499805587/655513c4-e368-4db3-97fb-8b8db41d5bb6.png" alt class="image--center mx-auto" /></p>
<p>Inspecting this file will reveal that SQLcl Projects was able to compare the baseline version of <code>EMP</code> with the current version of <code>EMP</code> and generate the following DDL to synchronize them:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- liquibase formatted sql</span>
<span class="hljs-comment">-- changeset DEMO:1756472006274 stripComments:false  logicalFilePath:version-1.1/demo/tables/emp.sql runAlways:false runOnChange:false replaceIfExists:true failOnError:true</span>
<span class="hljs-comment">-- sqlcl_snapshot src/database/demo/tables/emp.sql:22c6c8ad205e23dd589af01b8cfdd5e06a2b6add:594f20e09b4dafdc75d54f0f6a617fda9fba7bdd:alter</span>

<span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> demo.emp <span class="hljs-keyword">add</span> (
    email <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">100</span>)
)
/
</code></pre>
<p>Here’s a curveball.</p>
<p>Have a look at the <code>stage.changelog.xml</code> file:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">databaseChangeLog</span> <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span>
                   <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.liquibase.org/xml/ns/dbchangelog"</span>
                   <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"_custom/seed_email.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"demo/tables/emp.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">databaseChangeLog</span>&gt;</span>
</code></pre>
<p>See it? Our <code>seed_email.sql</code> script will run BEFORE the new <code>EMAIL</code> column is added to <code>EMP</code>. That’s because we staged the custom file before we staged the changes to <code>EMP</code>. Sometimes this happens with Liquibase, and we need to modify the order of the files in the changelog.</p>
<p>Fortunately, there’s a quick fix - just change the order of when the scripts are called:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">databaseChangeLog</span> <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span>
                   <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.liquibase.org/xml/ns/dbchangelog"</span>
                   <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://www.liquibase.org/xml/ns/dbchangelog
                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"demo/tables/emp.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">include</span> <span class="hljs-attr">file</span>=<span class="hljs-string">"_custom/seed_email.sql"</span> <span class="hljs-attr">relativeToChangelogFile</span>=<span class="hljs-string">"true"</span>/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">databaseChangeLog</span>&gt;</span>
</code></pre>
<p>One thing that you’ll learn to appreciate when working with Liquibase is that order really matters, and it doesn’t always get it right. In this case, we had to swap two files. You will likely run into similar matters with your own projects.</p>
<h3 id="heading-create-a-release">Create a Release</h3>
<p>Now that we have that sorted, let’s create and deploy the release. From SQLcl, run the following:</p>
<pre><code class="lang-sql">project <span class="hljs-keyword">release</span> -<span class="hljs-keyword">version</span> <span class="hljs-number">1.1</span> -verbose
</code></pre>
<p>After running this command, have a look at your local working copy. You should see an additional folder called <code>1.1</code> under the <code>dist/releases</code> folder. If you expand <code>change/version-1.1/demo/tables</code> and inspect <code>emp.sql</code>, it should only contain the <code>ALTER</code> statement; not the full <code>CREATE TABLE</code> one.</p>
<p>This is the magic of Liquibase: if a user was applying 1.1 on top of 1.0, only the <code>ALTER</code> statement will be run. If a user was doing a clean installation of 1.1, it would run both the <code>CREATE TABLE</code> and <code>ALTER TABLE</code> statements sequentially.</p>
<h3 id="heading-generate-the-artifact-1">Generate the Artifact</h3>
<p>As we did before, let’s create a new artifact based on the new release we just created.</p>
<pre><code class="lang-sql">project gen-artifact -version 1.1
</code></pre>
<h3 id="heading-deploy-the-release">Deploy the Release</h3>
<p>Let’s switch to our target database and deploy the release. Connect to SQLcl as the <code>DEMO</code> user on the target database:</p>
<pre><code class="lang-bash">sql -name demo_target
</code></pre>
<p>Once connected, all that’s left is to run the deployment.</p>
<pre><code class="lang-sql">project deploy -file artifact/demo-1.1.zip -verbose
</code></pre>
<p>When complete, we should see something like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">Check</span> <span class="hljs-keyword">database</span> connection...
<span class="hljs-keyword">Extract</span> the <span class="hljs-keyword">file</span> <span class="hljs-keyword">name</span>: demo<span class="hljs-number">-1.1</span>
Artifact decompression <span class="hljs-keyword">in</span> progress...
Artifact decompressed: /<span class="hljs-keyword">var</span>/folders/<span class="hljs-number">0</span>_/nwtj13qx4bv2gzs667vs3h3w0000gn/T/<span class="hljs-number">39</span>ec72ef<span class="hljs-number">-8073</span><span class="hljs-number">-4483</span><span class="hljs-number">-81</span>c4<span class="hljs-number">-1</span>fbd34e9a10315832945422908414459
<span class="hljs-keyword">Starting</span> the migration...
Running Changeset: <span class="hljs-keyword">version</span><span class="hljs-number">-1.1</span>/demo/<span class="hljs-keyword">tables</span>/emp.sql::<span class="hljs-number">1756472006274</span>::DEMO
<span class="hljs-keyword">Table</span> DEMO.EMP altered.
Running Changeset: <span class="hljs-keyword">version</span><span class="hljs-number">-1.1</span>/_custom/seed_email.sql::<span class="hljs-number">1756471732399</span>::SqlCl
PL/<span class="hljs-keyword">SQL</span> <span class="hljs-keyword">procedure</span> successfully completed.
Liquibase: <span class="hljs-keyword">Update</span> has been successful. <span class="hljs-keyword">Rows</span> affected: <span class="hljs-number">2</span>
Installing/updating schemas
<span class="hljs-comment">--Starting Liquibase at 2025-08-29T08:31:46.608397 using Java 21.0.8 (version 4.30.0 #0 built at 2025-04-01 10:24+0000)</span>
<span class="hljs-keyword">Table</span> DEMO.EMP altered.
PL/<span class="hljs-keyword">SQL</span> <span class="hljs-keyword">procedure</span> successfully completed.

<span class="hljs-keyword">UPDATE</span> SUMMARY
Run:                          <span class="hljs-number">2</span>
Previously run:               <span class="hljs-number">4</span>
Filtered <span class="hljs-keyword">out</span>:                 <span class="hljs-number">0</span>
<span class="hljs-comment">-------------------------------</span>
Total <span class="hljs-keyword">change</span> <span class="hljs-keyword">sets</span>:            <span class="hljs-number">6</span>


Produced <span class="hljs-keyword">logfile</span>: sqlcl-lb<span class="hljs-number">-1756474305795.</span><span class="hljs-keyword">log</span>

Operation completed successfully.
<span class="hljs-keyword">Migration</span> has been completed
Removing the decompressed artifact: /<span class="hljs-keyword">var</span>/folders/<span class="hljs-number">0</span>_/nwtj13qx4bv2gzs667vs3h3w0000gn/T/<span class="hljs-number">39</span>ec72ef<span class="hljs-number">-8073</span><span class="hljs-number">-4483</span><span class="hljs-number">-81</span>c4<span class="hljs-number">-1</span>fbd34e9a10315832945422908414459...
</code></pre>
<p>To prove that it did, in fact, work, run this SQL:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> emp;

   EMPNO ENAME     JOB              MGR HIREDATE         SAL    COMM    DEPTNO EMAIL                 
________ _________ ____________ _______ ____________ _______ _______ _________ _____________________ 
    7839 KING      PRESIDENT            17-NOV-81       5000                10 king@example.com      
    7698 BLAKE     MANAGER         7839 01-MAY-81       2850                30 blake@example.com     
    7782 CLARK     MANAGER         7839 09-JUN-81       2450                10 clark@example.com     
    7566 JONES     MANAGER         7839 02-APR-81       2975                20 jones@example.com     
    7788 SCOTT     ANALYST         7566 19-APR-87       3000                20 scott@example.com     
    7902 FORD      ANALYST         7566 03-DEC-81       3000                20 ford@example.com      
    7369 SMITH     CLERK           7902 17-DEC-80        800                20 smith@example.com     
    7499 ALLEN     SALESMAN        7698 20-FEB-81       1600     300        30 allen@example.com     
    7521 WARD      SALESMAN        7698 22-FEB-81       1250     500        30 ward@example.com      
    7654 MARTIN    SALESMAN        7698 28-SEP-81       1250    1400        30 martin@example.com    
    7844 TURNER    SALESMAN        7698 08-SEP-81       1500       0        30 turner@example.com    
    7876 ADAMS     CLERK           7788 23-MAY-87       1100                20 adams@example.com     
    7900 JAMES     CLERK           7698 03-DEC-81        950                30 james@example.com     
    7934 MILLER    CLERK           7782 23-JAN-82       1300                10 miller@example.com    

14 rows selected.
</code></pre>
<p>There’s our 14 rows, complete with a new, populated column for email address.</p>
<h3 id="heading-add-commit-amp-push-to-github">Add, Commit &amp; Push to Github</h3>
<p>As we did with the initial release, we want to be sure to push our changes back to Github and merge them into the <code>main</code> branch.</p>
<pre><code class="lang-bash">git add -A
git commit -m <span class="hljs-string">"Release 1.1"</span>
git push -u origin release-1.1
</code></pre>
<p>Flip back over to your browser and complete the merge. Use the same sequence as you did to merge the <code>release-1.0</code> in the previous section. Here’s the high-level steps:</p>
<ul>
<li><p>Click the <strong>Compare &amp; pull request</strong> button</p>
</li>
<li><p>Enter any comments that you’d like to track and click <strong>Create pull request</strong></p>
</li>
<li><p>Click <strong>Merge pull request</strong></p>
</li>
<li><p>Enter a commit message, select a user and then click <strong>Confirm merge</strong></p>
</li>
<li><p>Delete the <code>version-1.1</code> branch</p>
</li>
<li><p>Click the <strong>Code</strong> tab to confirm your changes were merged</p>
</li>
</ul>
<p>Don’t forget to remove your working copy and/or switch back to the <code>main</code> branch, as the <code>release-1.1</code> branch was deleted from the repository.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Managing database changes might seem straightforward when you’re working alone on a single table, but as soon as your application and team grow, the cracks start to show. Ad-hoc scripts quickly become messy, inconsistent, and risky to deploy. That’s where structured approaches like SQLcl Projects and Liquibase come in. They provide a repeatable, controlled way to evolve your schema with confidence, while still keeping the flexibility developers need.</p>
<p>By treating database changes like source code, you not only reduce errors but also improve collaboration, traceability, and long-term maintainability. SQLcl Projects helps developers of all experience levels easily adapt into the world of automated deployments. It’s a great first step to get into the world of CI/CD pipelines, as the barrier to entry is low and benefit is high.</p>
<p>And yes, APEX is supported with SQLcl Projects. Look for another blog about how to do that in the coming days.</p>
<h2 id="heading-additional-reading">Additional Reading</h2>
<p>Still want to learn more about SQLcl Projects or want to hear about how others use it? Look no further than these articles - some of which I referred to while writing this post:</p>
<ul>
<li><p><a target="_blank" href="https://pretius.com/blog/sqlcl-project-reference">https://pretius.com/blog/sqlcl-project-reference</a></p>
</li>
<li><p><a target="_blank" href="https://rafal.hashnode.dev/part5-sqlcl-project-it-will-forever-change-your-database-apex-deployments">https://rafal.hashnode.dev/part5-sqlcl-project-it-will-forever-change-your-database-apex-deployments</a></p>
</li>
<li><p><a target="_blank" href="https://rafal.hashnode.dev/part-51-oracle-sqlcl-project-the-only-cicd-tool-for-apex-you-will-ever-need">https://rafal.hashnode.dev/part-51-oracle-sqlcl-project-the-only-cicd-tool-for-apex-you-will-ever-need</a></p>
</li>
</ul>
<hr />
<p><em>Title photo by</em> <a target="_blank" href="https://unsplash.com/@cdr6934?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><strong><em>Chris Ried</em></strong></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Using Claude Desktop to Create Dashboards for APEX]]></title><description><![CDATA[Another AI-centric tool that has raised a lot of interest is Claude Desktop. Like Cline, Claude Desktop uses the same reasoning engine at the core - Claude Code - to assist with development tasks. While they share the same engine, there are use cases...]]></description><link>https://spendolini.blog/using-claude-desktop-to-create-dashboards-for-apex</link><guid isPermaLink="true">https://spendolini.blog/using-claude-desktop-to-create-dashboards-for-apex</guid><category><![CDATA[orclapex]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Cloud]]></category><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[dashboard]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Mon, 18 Aug 2025 15:07:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755292121898/1bbef6d8-f356-45a4-a792-f9e907288cb6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Another AI-centric tool that has raised a lot of interest is <a target="_blank" href="https://claude.ai/download">Claude Desktop</a>. Like <a target="_blank" href="https://cline.bot/">Cline</a>, Claude Desktop uses the same reasoning engine at the core - <a target="_blank" href="https://www.anthropic.com/claude-code">Claude Code</a> - to assist with development tasks. While they share the same engine, there are use cases for each.</p>
<p>Claude Desktop is a self-contained, simple interface. You interact with it outside of the scope of your IDE and code in a more general manner. While it can still generate some powerful solutions, Claude Desktop seems to be closer to an alternative to ChatGPT vs. a true AI coding assistant.</p>
<p>Cline is the better choice if you’re looking for an AI assistant that lives in your IDE. It’s there to help you write, review, and comment on code. It was designed for that single purpose from the ground up.</p>
<p>Another difference between Cline and Claude Desktop is how LLMs are integrated. Cline allows you to pick almost any LLM and use that. Thus, as Oracle, we have access to a handful, and can use any of them with Cline at no additional cost. Claude Desktop takes a different approach. You can use it for free for a limited number of questions per day, or you can sign up for one of the paid tiers.</p>
<p>Put another way - If I wanted to give an AI tool to my non-technical staff so that they could query an Oracle Database via MCP Server, it would be Claude Desktop without hesitation. Its simple, easy to understand interface and robust rendering capabilities make it an easy choice for the non-technical crowd.</p>
<h1 id="heading-getting-started">Getting Started</h1>
<p>Before we can use Claude Desktop, we need to configure it with the MCP Server and then connect to it. These steps take just minutes each.</p>
<h2 id="heading-configuring-claude-desktop">Configuring Claude Desktop</h2>
<p>Configuring Claude Desktop is as simple as configuring Cline. Simply follow the instructions in the <a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.2/sqcug/starting-and-managing-sqlcl-mcp-server.html#GUID-E5F42EA3-8191-4328-8A95-6086D7E21FB3">SQLcl MCP Server documentation</a>.</p>
<p>Once configured, you can validate that the MCP Server is running by navigating to <strong>Settings</strong> &gt; <strong>Developer</strong>. You should see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755352579907/85592220-a9af-469b-89e6-263367a07692.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-connecting-to-the-mcp-server">Connecting to the MCP Server</h2>
<p>Once configured, connecting to Oracle is done the same way that <a target="_blank" href="https://spendolini.blog/exploring-mcp-server-for-oracle-database-and-apex#heading-connecting-with-cline">connecting to Cline</a> is: just ask.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755290791212/b88a9bfe-318e-4063-a910-6f020b768f0b.png" alt class="image--center mx-auto" /></p>
<p>The SQL Developer connection <code>apex_admin_user</code> was created as a part of my <a target="_blank" href="https://spendolini.blog/exploring-mcp-server-for-oracle-database-and-apex#heading-create-the-database-connection">last blog entry</a>. Please have a look there if you missed it.</p>
<h1 id="heading-creating-dashboards">Creating Dashboards</h1>
<p>Now that we’re connected, let’s skip the easy stuff and jump right into asking Claude to build us some dashboards.</p>
<h2 id="heading-page-views">Page Views</h2>
<p>For our first dashboard, let’s use the data found in <code>APEX_WORKSPACE_ACTIVITY_LOG</code> - or page views. APEX will automatically track all page views - full and partial - in this view. Since our user has the <code>APEX_ADMINISTRATOR_ROLE</code> role, it will be able to see data across all of the workspaces. Keep in mind APEX will automatically purge data from the underlying table, so at best, you’ll have roughly two weeks of data.</p>
<p>Let’s start with the following prompt:</p>
<blockquote>
<p>please create an analytics dashboard that summarizes page views for application 10000. include things like most popular page, avg render time, etc. make it look very slick and allow me to drill into details from the charts.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755291087110/fc4e655c-c7cb-4c23-b41d-37bcb6c9944a.png" alt class="image--center mx-auto" /></p>
<p>Like Cline, it required some back-and-forth to find the right view. Once it found it, it ran four queries to compute the data points that would ultimately end up in the dashboard. Remember - this dashboard is nothing more than static HTML with some Javascript.</p>
<p>Speaking of the dashboard it created, let’s have a look at it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755291703081/fd88bd49-b545-445e-b7d6-45e6a93c71ce.png" alt class="image--center mx-auto" /></p>
<p>It’s pretty amazing that with just a couple of sentences, Claude was able to generate a professional looking dashboard that contains a variety of insights as to the page view data, including a combination of metrics, charts and a searchable report.</p>
<p>You can also drill into most of the data points. Since that’s harder to illustrate, feel free to try the dashboard for yourself here: <a target="_blank" href="https://claude.ai/public/artifacts/c022f815-7e81-4a16-b6f3-3cae15e82fd2">https://claude.ai/public/artifacts/c022f815-7e81-4a16-b6f3-3cae15e82fd2</a></p>
<h2 id="heading-user-behaviors">User Behaviors</h2>
<p>Next, let’s create a dashboard that reports on data that APEX doesn’t: user behavior. It would be interesting to analyze the same data set to see how users actually use the applications - specifically, which page flows are the most popular, which pages get used the most, etc.</p>
<p>Thus, let’s use this prompt to create it:</p>
<blockquote>
<p>using the same page view data, come up with an assessment of common usage patterns based on session id. provide this data in a dashboard that provides insights as to what users do and what could be done to make the application better</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755265828435/a5bf5767-35c9-456a-9a95-2491cec79978.png" alt class="image--center mx-auto" /></p>
<p>This time, Claude didn’t need to think as much, as the source of the data was still in context from the previous dashboard. It got right to work and instantly created another dashboard that looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755350225837/f6e94d0c-b31f-49c0-9184-be7ff55e7fb6.png" alt class="image--center mx-auto" /></p>
<p>Wow! This one really impressed me, as not only did it provide a comprehensive dashboard with actionable data, but it also picked out some of the common user flows and trends in my applications and identified them.</p>
<p>Keep in mind that the dataset here is from an instance of APEX that I build and test things on. Thus, the usage patterns are more indicative of that versus actual usage across many users. Some of the recommendations reflect this.</p>
<p>Have a look at this one in action here: <a target="_blank" href="https://claude.ai/public/artifacts/5baab2fb-f0fe-4065-85d9-0d3844bae3bc">https://claude.ai/public/artifacts/5baab2fb-f0fe-4065-85d9-0d3844bae3bc</a></p>
<h2 id="heading-where-does-apex-fit-in">Where does APEX fit in?</h2>
<p>You may be thinking, “<em>I thought this was an APEX blog, why are we creating Javascript applications all of a sudden?</em></p>
<p>Short answer - we can’t yet have AI generate APEX applications since there is not a published API to do so. Yet.</p>
<p><a target="_blank" href="https://www.linkedin.com/posts/krisrice_today-at-apexworld-michael-hichwa-did-a-activity-7308547580576055296-88z_/">APEX Lang</a> - not the existing <code>APEX_LANG</code> API, but the upcoming domain specific language with a similar name - will change that once it’s released. APEX Lang exposes all facets of an APEX application via APIs, allowing either users or AIs to call APIs to build and modify applications. Once it’s released, it will fundamentally change how we build and maintain APEX applications.</p>
<h1 id="heading-summary">Summary</h1>
<p>Claude Desktop enables anyone to generate content-rich, interactive dashboards without writing a single line of code. With minimal guidance, it even added insights and metrics to the dashboards, making them more than a bunch of reports &amp; charts. Even if I simply harvested the SQL that Claude generated and used that in an APEX application, that’s a win.</p>
<p>I’m going to keep playing around with Claude Desktop and see what else I can come up with. Next, I’m going to dive into hooking up these dashboards to a RESTful endpoint hosted by ORDS so that they will always be accurate. Then I’ll look at how to secure the endpoint and perhaps allow for some updates to RESTful APIs.</p>
<hr />
<p><em>Title photo by</em> <a target="_blank" href="https://unsplash.com/@something_else?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><em>Mykhailo Amirdzhanian</em></a> <em>on Unsplash.</em></p>
]]></content:encoded></item><item><title><![CDATA[Exploring MCP Server for Oracle Database & APEX]]></title><description><![CDATA[When I first heard the term MCP used in the context of AI, I was immediately taken back to the 80s when the movie Tron was released. For those who have not seen it, Tron is a futuristic movie about a curious user who is literally sucked into a comput...]]></description><link>https://spendolini.blog/exploring-mcp-server-for-oracle-database-and-apex</link><guid isPermaLink="true">https://spendolini.blog/exploring-mcp-server-for-oracle-database-and-apex</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[AI]]></category><category><![CDATA[mcp]]></category><category><![CDATA[Oracle Cloud]]></category><category><![CDATA[Oracle Database]]></category><category><![CDATA[sqlcl]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Wed, 13 Aug 2025 18:12:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754915284986/db42a160-14b2-4273-aca7-1d674d3cc0be.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I first heard the term MCP used in the context of AI, I was immediately taken back to the 80s when the movie Tron was released. For those who have not seen it, <a target="_blank" href="https://en.wikipedia.org/wiki/Tron">Tron</a> is a futuristic movie about a curious user who is literally sucked into a computer and has to fight for his life. The main bad guy in the movie is not actually a guy, but rather the <a target="_blank" href="https://tron.fandom.com/wiki/Master_Control_Program">Master Control Program</a> - or MCP. The MCP was a pre-cursor to today’s AI, as it was self-aware, got smarter on its own and had plans for world domination.</p>
<p>Fast-forward a bunch of years, and that three-letter acronym - MCP - is making its way around the AI crowd again. But this time, it stands for <a target="_blank" href="https://www.anthropic.com/news/model-context-protocol">Model Context Protocol</a>. This modern version of MCP is not trying to kill you as much as it provides a critical link between your private data and any LLM. The MCP protocol was designed by Anthropic in November 2024 and was quickly adopted as an open source standard by all of the major players in AI shortly thereafter, making it one of the fastest growing areas of AI. It’s often compared to a USB hub, as it connects one type of service (AI) with another (data).</p>
<h2 id="heading-why-all-the-hype">Why All the Hype?</h2>
<p>MCP servers are getting a lot of hype these days, and rightly so. To understand why, let’s take a step back and think about how we interact with LLMs today. The LLMs that we use today - ChatGPT, Grok, Anthropic, etc. - are all hosted on the public internet. Anything we send to them is received, stored and processed on their hardware.</p>
<p>LLMs are great at answering questions based on the data that they were trained on. This includes almost all facets of programming in almost any modern language. Need a regular expression written? LLM’s got you. Want to validate a JSON document? Just throw it at ChatGPT. How about a quick check of my code for security issues? Done and done.</p>
<p>Where LLMs quickly fall apart is when you need to know about YOUR data. Since your data - or your organization’s data - is private, LLMs were never trained on it and don’t know anything about it. Sure, you can use things like RAG to give LLMs a small sample of your data, but in many cases, that’s just no where near enough.</p>
<p>Before MCP, you really only had a single choice if you wanted to use your data with an LLM: train it. Training an LLM is a significant investment, as it means that you’ll have to procure the processors, servers and disk to load all of your data onto and let the LLM go to town learning about it. Even training small datasets can easily rack up bills in the 6 or 7 digit range.</p>
<p>This is where MCP changes the game.</p>
<h1 id="heading-mcp-server-for-oracle-database">MCP Server for Oracle Database</h1>
<p>Oracle’s new MCP Server for the Oracle Database is an MCP server that serves a specific purpose - act as a broker between an LLM and your data in an Oracle Database. One “end” of the MCP server connects to your database while the other “end” plugs into the LLM of your choice.</p>
<p>The MCP server acts as an intermediary of sorts. In the case of MCP for Oracle Database, the MCP server has specific set of tasks that it can be asked to perform. From the <a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.2/sqcug/sqlcl-mcp-server-tools.html">Oracle SQLcl Documentation</a>:</p>
<ul>
<li><p><strong>list-connections:</strong> Discovers and lists all saved Oracle Database connections on your machine.</p>
</li>
<li><p><strong>connect:</strong> Establishes a connection to one of your specified named connections.</p>
</li>
<li><p><strong>disconnect:</strong> Terminates the current, active Oracle Database connection.</p>
</li>
<li><p><strong>run-sql:</strong> Executes standard SQL queries and PL/SQL code blocks against the connected database.</p>
</li>
<li><p><strong>run-sqlcl:</strong> Executes SQLcl-specific commands and extensions.</p>
</li>
</ul>
<p>The key task here is <strong>run-sql</strong>. This allows the MCP to interact with the LLM and then run the SQL that the LLM requests. Again, the LLM never has direct access to the database - it can only talk to and receive input from the MCP server directly. In many cases, the LLM will want to run SQL to get bits of data so that it can complete the task it was asked. An example of this is it may need to know which columns are in the EMP table so that it can write a query. Or, if you asked something like “provide a summary of all orders for product A”, the LLM will need to see your data in order to provide the summary. Tools like Cline will allow the end user to see what is about to be sent to the LLM and either approve or reject it.</p>
<h2 id="heading-get-more-information-on-mcp-server-for-oracle-database">Get More Information on MCP Server for Oracle Database</h2>
<p>Before diving into the rest of this article, it may be helpful to take a look at some other content about the MCP Server for Oracle Database. These are some great places to start:</p>
<ul>
<li><p><a target="_blank" href="https://blogs.oracle.com/database/post/introducing-mcp-server-for-oracle-database">Introducing MCP Server for Oracle Database</a> by Jeff Smith &amp; Kris Rice</p>
</li>
<li><p><a target="_blank" href="https://www.thatjeffsmith.com/archive/2025/07/getting-started-with-our-mcp-server-for-oracle-database/">Getting Started with our MCP Server for Oracle Database</a> by Jeff Smith</p>
</li>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.2/sqcug/using-oracle-sqlcl-mcp-server.html">Using the Oracle MCP Server</a> - Oracle SQLcl Documentation</p>
</li>
</ul>
<h1 id="heading-install-amp-configure-mcp-for-oracle-database">Install &amp; Configure MCP for Oracle Database</h1>
<p>OK, enough background. Let’s get MCP Server for Oracle Database installed &amp; configured so that we can play with it!</p>
<p>Before we go much further, I have to issue the following security warning:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>WARNING!</strong> Allowing AI to connect to your database and issue commands may have disastrous results. Please exercise extreme caution when connecting any MCP to your database. Strongly consider using a copy of production data or a test database.</div>
</div>

<h2 id="heading-create-the-database-user">Create the Database User</h2>
<p>The MCP server will be able to do anything that the schema which it connects can. Thus, one way to limit the blast area is to create a limited privilege schema and connect through that. That schema can be set to only see a limited set of views &amp; APIs, for example. If MCP tried to access another schema object, it will be blocked by the database security. This same principle can be applied to the parse-as schema in an APEX application; see Chapter 13 in a <a target="_blank" href="https://www.amazon.com/Expert-Application-Express-Security-Experts-ebook/dp/B00ACC6AO6?crid=141E76MI8ITZE&amp;dib=eyJ2IjoiMSJ9.46eY5bhXHsUmYPFgCRculbH_ecQlcNNQjb-konkjfTbeWV58gsLcRi9nnfTDTVGzTuFk-zIZ5tHLABBKKDMDh9XXN-164qwsXDCunaIup2MjyotZ2asqGAM5al1NGW9GnC9ohKyYUtQ3YHbUh37vlmlMPBKOx4qdum3e8mdBCSxWmqp8fYBFPJ5WlUW6woGBV2NQ-goynQnDthYKR68yrQrjwM4vroJgDbPP2d-COqU.m2xir1TRIcldYtCdNE23X6UkP5t_0tcvATQ1F9wxDL0&amp;dib_tag=se&amp;keywords=expert+oracle+application+express&amp;qid=1754961951&amp;sprefix=expert+oracle+application+expre%2Caps%2C297&amp;sr=8-3">book I wrote</a> many years ago for more details…</p>
<p>In order to both read APEX metadata and call APEX APIs, we’ll want to create a new schema and associate a role to it. It’s much safer this way vs. letting MCP connect to something like <code>SYS</code>, <code>SYSTEM</code> or <code>ADMIN</code>.</p>
<p>This new schema - called <code>APEX_ADMIN_USER</code> - will be granted the <code>APEX_ADMINISTRATOR_ROLE</code> role so that it can see any application in any workspace as well as call APEX administration APIs.</p>
<p>To create the <code>APEX_ADMIN_USER</code> schema:</p>
<ol>
<li><p>Connect to your database as <code>SYS</code>, <code>SYSTEM</code> or <code>ADMIN</code> - essentially a user that has DBA privileges.</p>
</li>
<li><p>Run the following commands to create the <code>APEX_ADMIN_USER</code>:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-comment">-- create the new user</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> apex_admin_user <span class="hljs-keyword">identified</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">oracle</span>;

<span class="hljs-comment">-- add quota to the new users tablespace</span>
<span class="hljs-keyword">alter</span> <span class="hljs-keyword">user</span> apex_admin_user <span class="hljs-keyword">quota</span> <span class="hljs-keyword">unlimited</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">users</span>;

<span class="hljs-comment">-- grant connect to the new user</span>
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">connect</span> <span class="hljs-keyword">to</span> apex_admin_user;

<span class="hljs-comment">-- grant the apex_administrator_read_role to the user</span>
<span class="hljs-keyword">grant</span> APEX_ADMINISTRATOR_ROLE <span class="hljs-keyword">to</span> apex_admin_user;

<span class="hljs-comment">-- allow the user to create a table &amp; sequence</span>
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span>, <span class="hljs-keyword">create</span> <span class="hljs-keyword">sequence</span> <span class="hljs-keyword">to</span> apex_admin_user;
</code></pre>
<h2 id="heading-create-the-database-connection">Create the Database Connection</h2>
<p>Next, we need to create a database connection for our new user in SQL Developer. There’s nothing special about this step at all; you just need to create a uniquely named connection so that the MCP server can use it to connect to the database. I’ve found that it’s easier to use simple words to name the connection - something like <code>apex_admin_user</code> works great.</p>
<ol>
<li><p>In <strong>SQL Developer for VS Code</strong>, click on the “<strong>+</strong>” in the Connections panel.</p>
</li>
<li><p>Complete the connection details with your specifics. For my example, here’s what I used:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754933803029/b7277b53-f91a-42d1-a871-00bd001c1930.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Optionally, click <strong>Test</strong> to be sure that the connection is valid.</p>
</li>
<li><p>Click <strong>Save</strong> to save the connection.</p>
</li>
</ol>
<h2 id="heading-installing-and-configuring-cline">Installing and Configuring Cline</h2>
<p>Cline is “<em>an AI development assistant which integrates with Microsoft Visual Studio Code. It provides an interface between your IDE and LLMs facilitating code development, increasing productivity and lowering the barrier to entry for new coders…</em>”</p>
<p>In other words, Cline can help analyze and generate code via VS Code. It integrates with practically any LLM and provides a chat-like interface accessible from VS Code.</p>
<p>The documentation for <strong>SQLcl MCP Server</strong> is quite good, and the steps to install &amp; configure Cline are quick &amp; easy.</p>
<ol>
<li><p>Navigate to the <a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.2/sqcug/starting-and-managing-sqlcl-mcp-server.html#GUID-42167832-B364-4A3E-8A17-9FAE1F6CCFD3">Oracle MCP Server Documentation</a>.</p>
</li>
<li><p>Follow the section labeled <strong>3.5.2 Configuring Cline in VS Code</strong>.</p>
</li>
</ol>
<h2 id="heading-connecting-with-cline">Connecting with Cline</h2>
<p>Now that Cline is installed and configured with an LLM, all we need to do is connect to our new schema.</p>
<p>To do this:</p>
<ol>
<li><p>Select <strong>Cline</strong> in the sidebar.</p>
</li>
<li><p>You should see a screen that looks like this:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754963072052/a7032e4c-2187-41a8-b4fe-cca5af7ca6f3.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Enter the following into the prompt and hit enter: <code>connect to apex_admin_user</code></p>
</li>
</ol>
<p>Cline will then attempt to connect to the database using the connection properties defined in <code>apex_admin_user</code>. As it attempts to connect, it may ask you for permission to execute certain commands. You can approve this each time, or set auto-approve to automatically approve any request to run the same portion of the MCP server - in this case, <strong>connect</strong>.</p>
<p>Here’s the output from my session - yours may vary depending on the LLM model you’re using as well as how many times you’ve connected in the past.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754963492366/ab8c0754-5d8b-4d5f-ac00-ce2e0b3741cf.png" alt class="image--center mx-auto" /></p>
<p>At this point, we’re ready to roll.</p>
<h2 id="heading-optional-cleanup">Optional Cleanup</h2>
<p>Before we start, there’s an optional step that you may want to perform. The first time Cline connects, the MCP server will create a table called <code>DBTOOLS$MCP_LOG</code> in your schema. This table will be used to log all of the actions that MCP performs. MCP will also add comments to other common log &amp; trace tables such as <code>v$sql</code>, <code>v$session</code>, <code>ASH</code>, <code>AWR</code>, etc. This makes it crystal clear which transactions were performed by the MCP server.</p>
<p>When we created the <code>APEX_ADMIN_USER</code> schema, we granted <code>create table</code> and <code>create sequence</code> so that MCP could create the table and corresponding sequence. If you want to revoke this pair of privileges, you can do so by running the following command:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- once we have connected via Cline, optionally revoke create table &amp; sequence</span>
<span class="hljs-keyword">revoke</span> <span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span>, <span class="hljs-keyword">create</span> <span class="hljs-keyword">sequence</span> <span class="hljs-keyword">from</span> apex_admin_user;
</code></pre>
<h1 id="heading-using-mcp-with-apex">Using MCP with APEX</h1>
<p>Now that we’re connected, we can literally use plain English to query the database about our APEX environment. For this section, I am using my Free Tier Cloud database, which has a decent amount of workspaces &amp; applications. Your results will vary.</p>
<h2 id="heading-number-of-workspaces">Number of Workspaces</h2>
<p>Let’s start off simple. Enter the following into the prompt and press enter: <em>how many workspaces are there?</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754964041223/bbc4e67d-c807-4713-9030-c7b49467cd86.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-what-just-happened">What Just Happened?</h3>
<p>Once we connected to the database, we asked Cline a simple question: <em>how many workspaces are there?</em> Workspaces are used in several software applications, so how did it know that I meant APEX workspaces?</p>
<p>That’s because my request was sent to the LLM with a prompt. That prompt provided additional context to the LLM so that it could choose the correct definition of workspace. Since the prompt likely stated that we’re connected to an Oracle Database, the LLM was able to return the correct SQL to run to answer the question.</p>
<p>It’s no different than if I were to open up a new ChatGPT window and ask the same question with a similar prompt:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754965040855/89372b93-7908-49c6-8600-a1db3c59e1a2.png" alt class="image--center mx-auto" /></p>
<p>Keep in mind that at no time at all does the LLM connect to the database or see any of the data. It’s simply taking the question that I sent as well as the prompt that’s embedded into the MCP Server and querying the LLM with that. The results are then returned to the MCP server, where it then turns around and runs them in the database connection that we established.</p>
<h3 id="heading-mechanics">Mechanics</h3>
<p>The LLM actually wraps the results in JSON and returns them to the MCP Server. If we take a look at the JSON, it’s pretty basic and very obvious which SQL is about to be run:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"sql"</span>: <span class="hljs-string">"SELECT /* LLM in use is claude-sonnet-4 */ COUNT(*) AS workspace_count FROM apex_workspaces;"</span>,
  <span class="hljs-attr">"mcp_client"</span>: <span class="hljs-string">"cline"</span>,
  <span class="hljs-attr">"model"</span>: <span class="hljs-string">"claude-sonnet-4"</span>
}
</code></pre>
<p>Notice the snippet <code>/* LLM in use is claude-sonnet-4 */</code> embedded in the SQL. This is how the MCP Server ensures that SQL in log tables are properly instrumented as having been run by an LLM.</p>
<p>Omitting that snippet, we’re left with a fairly simple SQL statement: <code>SELECT COUNT(*) AS workspace_count FROM apex_workspaces</code></p>
<p>The MCP server will then run that query and return the results, which in my case is 15. Running the same SQL via SQLcl confirms that the answer is correct.</p>
<h2 id="heading-number-of-applications">Number of Applications</h2>
<p>Sticking with simple, let’s ask this question: <em>how many applications are there?</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754963985941/9652c9f8-7433-4fe3-9a0a-ba7450c6379c.png" alt class="image--center mx-auto" /></p>
<p>In this example, it functioned exactly as the last one, with the only difference being the specifics of the question.</p>
<h2 id="heading-applications-per-workspace">Applications per Workspace</h2>
<p>So far, while this is cool, the complexity of the SQL and maybe even level of effort required is somewhat negligible. I could probably type the SQL in the same time it takes for me to type the question. Let’s take it up a notch.</p>
<p>This time, we’ll ask the following question: <em>list all applications by workspace; sort workspaces alphabetically and applications by the number of pages from most to least</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754964313633/b6221535-77d4-4dfd-9611-a70eb9fdc3a5.png" alt class="image--center mx-auto" /></p>
<p>This is where things get wild and the power of the MCP approach start to become clear. It’s clear that the results are correct; we have a dataset with all applications sorted by workspace name and then by page count high to low.</p>
<p>If we look at the full log, it looks like it stumbled a little bit. When it tried to run the following SQL:</p>
<pre><code class="lang-json">SELECT 
  a.workspace,
  a.application_id,
  a.application_name,
  COUNT(p.page_id) AS page_count
FROM 
  apex_applications a
  LEFT JOIN apex_application_pages p
    ON a.application_id = p.application_id 
    AND a.workspace_id = p.workspace_id
  GROUP BY 
    a.workspace, 
    a.application_id, 
    a.application_name
  ORDER BY 
    a.workspace ASC, 
    page_count DESC
</code></pre>
<p>It failed, as the column <code>WORKSPACE_ID</code> does not exist in the table <code>APEX_APPLICATION_PAGES</code>. Since Oracle returned an error code to the MCP Server, it took the entire error stack and sent that to the LLM and likely prompted that with something like “Help me fix this error”. The LLM was then able to realize that column did not exist - nor was it necessary - so it removed it from the join statement and sent another JSON document with the updated SQL statement.</p>
<p>Neat.</p>
<h2 id="heading-create-a-workspace">Create a Workspace</h2>
<p>Querying the APEX views is not the only thing we can do. Since we have the <code>APEX_ADMINISTRATOR_ROLE</code>, we can call any of the APEX APIs to do things like create and delete workspaces, users and alter instance settings.</p>
<p>Let’s try this: <em>create a new workspace called blog. that workspace should parse as a new schema called blog. the blog schema should have a strong password.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754966564606/c451fec7-ede2-4f36-88f2-61ba86e41b4d.png" alt class="image--center mx-auto" /></p>
<p>This time, Cline performed two, distinct actions:</p>
<ul>
<li><p>created the <strong>Database Schema</strong> <code>BLOG</code></p>
</li>
<li><p>created the <strong>APEX Workspace</strong> <code>BLOG</code> and associated the <code>BLOG</code> schema with it</p>
</li>
</ul>
<p>We can confirm the schema and workspace creation by logging into the APEX Instance Administration console:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754966780175/1e4804fc-52dd-4a38-b8aa-9926ee05d3db.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-create-an-apex-user">Create an APEX User</h2>
<p>Lastly, we’ll need a user created in that workspace so that we can login. Let’s create one now: <em>create a user in the blog workspace called admin. use a strong password and set the email address to</em> <a target="_blank" href="mailto:noreply@oracle.com"><em>noreply@oracle.com</em></a><em>.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754967055418/776b947a-cc61-438f-8170-54d5be241abb.png" alt class="image--center mx-auto" /></p>
<p>The first thing it did was try to call <code>APEX_UTIL.CREATE_USER</code>, but it used the wrong parameters. Here’s what it tried to call:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
  apex_util.create_user
    (
    p_user_name     =&gt; <span class="hljs-string">'admin'</span>,    
    p_web_password  =&gt; <span class="hljs-string">'Cg6^zpP4nWq2Xt#5'</span>,    
    p_email_address =&gt; <span class="hljs-string">'noreply@oracle.com'</span>,
    p_workspace     =&gt; <span class="hljs-string">'blog'</span>  
    );
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>Unfortunately, there is no <code>p_workspace</code> parameter in <code>APEX_UTIL.CREATE_USER</code>. Thus, it failed and sent the error stack back to the MCP Server.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">For the curious, you would need to set the workspace first via <code>APEX_UTIL.SET_WORKSPACE</code>. The correct API call would look like this:</div>
</div>

<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
  apex_util.set_workspace(p_workspace =&gt; <span class="hljs-string">'BLOG'</span>);
  apex_util.create_user
   (
    p_user_name     =&gt; 'admin',    
    p_web_password  =&gt; 'Cg6^zpP4nWq2Xt<span class="hljs-comment">#5',    </span>
    p_email_address =&gt; 'noreply@oracle.com'
    );
  <span class="hljs-keyword">commit</span>;
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>Here’s where things went wrong.</p>
<p>When it sent the error stack back to the LLM, the LLM replied with the following:</p>
<blockquote>
<p>The call to APEX_UTIL.CREATE_USER failed due to a wrong number or types of arguments. This strongly suggests either the API signature has changed or the arguments are not correct for the current APEX version.</p>
<p>In modern APEX (20.x+), workspace user creation is best performed via APEX_UTIL.CREATE_WORKSPACE_USER, which requires these parameters (with many as optional)…</p>
</blockquote>
<p>This is simply not true. There is no such API as <code>APEX_UTIL.CREATE_WORKSPACE_USER</code> whatsoever, as confirmed by this search in the APEX API Documentation:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754967867423/f11b2136-5ed7-4e9c-bfe2-111e8e921b48.png" alt class="image--center mx-auto" /></p>
<p>This is the infamous AI hallucination that you’ve heard about. The fact remains that sometimes, the AI just makes stuff up, and this is one of those times.</p>
<p>My experience is that AI for APEX administrative tasks works better with Lllama 4 or Grok 4 vs. GPT 4.1. Your results may vary, but if and when you hit one of these hallucinations, swapping out a different model - or even re-phrasing the question with more context - is all you need to do. It’s not a reason to give up on MCPs all together, as the quality of the LLMs gets better and better with each iteration.</p>
<h2 id="heading-create-an-apex-user-take-two">Create an APEX User: Take Two</h2>
<p>Lets switch models to Grok 4 and see if we can get it to work with the exact same prompt.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755000393090/b8db28fb-0f86-453c-8ab4-4dfa31f46b18.png" alt class="image--center mx-auto" /></p>
<p>This time, the LLM was aware that either the workspace or security group ID had to be set before creating a new APEX user. The SQL that it ran was this:</p>
<pre><code class="lang-sql">  <span class="hljs-keyword">BEGIN</span>
    APEX_UTIL.SET_SECURITY_GROUP_ID(<span class="hljs-number">48876018747798056</span>); 
    APEX_UTIL.CREATE_USER
      ( 
      p_user_name =&gt; 'ADMIN', 
      p_email_address =&gt; 'noreply@oracle.com', 
      p_web_password =&gt; 'Adm1n$ecure2025!', 
      p_developer_privs =&gt; 'ADMIN' 
      ); 
    <span class="hljs-keyword">COMMIT</span>;
  <span class="hljs-keyword">END</span>;
</code></pre>
<p>It even added a <code>COMMIT</code> - something that is also needed to complete any transaction.</p>
<p>Models matter. They are all trained on different data sets, and one may be missing critical details that cause it to send back false information, while another is perfectly aware on how to perform a task.</p>
<p>It would be interesting to have a “bake off” of sorts across the popular LLMs to see which performed the best in the context of MCP Server &amp; APEX…</p>
<h2 id="heading-analyze-plsql-processes">Analyze PL/SQL Processes</h2>
<p>Let’s kick things up a notch to demonstrate just how powerful the MCP Server really is.</p>
<p>This time, we’re going to ask the MCP to do a number of things at once:</p>
<ul>
<li><p>Search all applications for processes of type PL/SQL</p>
</li>
<li><p>Return all of the corresponding PL/SQL code</p>
</li>
<li><p>Provide an analysis of that code with advice as to how to optimize it</p>
</li>
</ul>
<p>To make things even more fun, let’s just post this list as a single, condensed prompt. Something like this: <em>show me any page process that uses plsql and provide suggestions as to how to optimize the code. return results in a JSON document</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755004020143/e29b2c21-35b1-4dbc-bdc4-17cd6313cbe2.png" alt class="image--center mx-auto" /></p>
<p>Wow, there’s a lot to unpack here. Let’s walk through all of the steps that the MCP Server took to get to the answer.</p>
<h2 id="heading-trust-the-process">Trust the Process</h2>
<p>First of all, the it struggled to get the right column name &amp; table name - failing first on <code>PROCESS_SOURCE_TYPE_CODE</code> (it should be <code>PROCESS_TYPE_CODE</code>) and then failing on <code>APEX_APPLICATION_PAGE_PROCESSES</code> (it should be <code>APEX_APPLICATION_PAGE_PROC</code>). Not wanting to admit it was wrong, it then looked for <code>APEX_APPLICATION_PAGE_PROCESSES</code> in <code>ALL_OBJECTS</code>. Again, it was nowhere to be found, since it simply doesn’t exist.</p>
<p>Next - and maybe as a sanity check - it wanted to verify which user it was connected to Oracle as. It then wanted to see which schema contained <code>APEX%</code>. Looks like it was trying to get its bearings to ensure that 1) APEX was installed and 2) it had privileges to see the views.</p>
<p>Still not 100% satisfied, it ran yet another query to see if <code>APEX_240200</code> owned a table named <code>APEX_APPLICATION_PAGE_PROCESSES</code>. To be fair, I’ve also struggled with this view over the years, as I would expect it to be called <code>APEX_APPLICATION_PAGE_PROCESSES</code> or even <code>APEX_APPLICATION_PAGE_PROCS</code>. Is it possible to feel bad for an LLM?</p>
<p>Next, it widened the search by querying all APEX views that start with <code>APEX_APPLICATION_PAGE</code>. And there it is - <code>APEX_APPLICATION_PAGE_PROC</code>.</p>
<p>Now that it has the correct view, it inspects all of the columns. Remember - the initial error was that the column <code>PROCESS_TYPE_SOURCE_CODE</code> was invalid. Having seen the full list, it is able to construct and run another query with the correct column names. While the query now runs successfully, we now have a data error. It’s looking for rows that have a <code>PROCESS_TYPE_CODE</code> of <code>NATIVE_PLSQL</code> and finds none, since APEX now uses just <code>PLSQL</code> to identify PL/SQL processes.</p>
<p>Persistent as ever, it now asks for a list of distinct values of <code>PROCESS_TYPE_CODE</code>, hoping that will shed some light on what the value really is. And once again, it finds what it’s looking for.</p>
<p>Now that all of the pieces are in place, the last query needed can be executed. Once it has the results, it sends them to the LLM with the initial ask (provide advice for optimization and return it in JSON) and returns the results back to us.</p>
<h2 id="heading-failing-forward">Failing Forward</h2>
<p>What amazed me most is the MCP Server &amp; LLM behaved strikingly similar to how I would have, had I been asked a similar question and did not have a deep understanding of how APEX works.</p>
<p>Basically, it employed the following workflow:</p>
<ul>
<li><p>Make an assumption and try something</p>
</li>
<li><p>If it fails, alter your assumption with another assumption or more facts</p>
</li>
<li><p>Try again</p>
</li>
<li><p>Repeat as needed</p>
</li>
</ul>
<p>In this case, the MCP Server &amp; LLM were eventually successful, as the results where precisely what I was asking for.</p>
<p>This last example is what I find to be the most fascinating thing about using MCP Servers with LLMs. It was given a plain English prompt and using the power of the LLM, was able to navigate through the database to find what it needed and deliver the correct result.</p>
<h1 id="heading-summary">Summary</h1>
<p>MCP Servers are evolving extremely fast, and there’s no sign of them slowing down. They represent a critical link that extends the power of AI to your internal data without the compromise of having to share the data with the LLM directly.</p>
<p>While MCP Servers can perform simple tasks, their true strength is when you challenge them with multi-step, complex tasks that would take a significant amount of time without automation. In the context of APEX, I can see multiple use cases for MCP Servers from both a developer’s point of view as well as the end user. People are already starting to kick the tires and sharing their experiences on social media, and the initial results are amazing.</p>
<p>MCP Servers aren’t magic - but paired with an LLM, they can feel like they are. Give them a simple request in plain English and they’ll reason, guess, retry, and adapt until they get it right with the tenacity of a person looking for a solution.</p>
<p>That’s the real shift that MCP servers represent: you’re not just running queries, you’re collaborating with a system that can navigate and interpret your data in all kinds of ways. And once you see that in action, it’s hard to go back to the old way.</p>
<p><strong><em>End of line.</em></strong></p>
]]></content:encoded></item><item><title><![CDATA[Adding AI to APEX-SERT]]></title><description><![CDATA[Recently, I’ve been asked several times: when will APEX-SERT have AI embedded in it? It’s a valid question, considering the unprecedented proliferation of AI across nearly every product. Heck, even my mouse has an AI button now, so why not put it int...]]></description><link>https://spendolini.blog/adding-ai-to-apex-sert</link><guid isPermaLink="true">https://spendolini.blog/adding-ai-to-apex-sert</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle Cloud]]></category><category><![CDATA[Oracle Database]]></category><category><![CDATA[Security]]></category><category><![CDATA[AI]]></category><category><![CDATA[orclapex]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Wed, 30 Jul 2025 12:38:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753800125744/5dda31fc-0e44-4fc3-b75b-a99e2bde1e75.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I’ve been asked several times: when will APEX-SERT have AI embedded in it? It’s a valid question, considering the unprecedented proliferation of AI across nearly every product. Heck, even my mouse has an <a target="_blank" href="https://www.theverge.com/2024/4/17/24132468/logitech-ai-prompt-builder-button">AI button</a> now, so why not put it into APEX-SERT?</p>
<p>APEX-SERT works well largely due to APEX’s declarative architecture. Rules can be configured to inspect a column in a view: if the value meets a specific criterion, the rule passes; otherwise, it fails. There’s literally no room for ambiguity when it comes to deriving the value of the rule and determining whether or not that value meets a pre-determined criteria.</p>
<p>Offloading this task to AI introduces the potential for less than 100% precision, which is unacceptable for our needs. And given that our current rules engine works 100% of the time, why would we make any changes to it and potentially decrease the accuracy?</p>
<p>Thus, given the following:</p>
<ul>
<li><p>APEX-SERT is a security tool</p>
</li>
<li><p>Security tools - by nature of their design and purpose - need to be precise</p>
</li>
<li><p>AI regularly hallucinates and makes stuff up</p>
</li>
</ul>
<p>At first glance, using AI with APEX-SERT might seem inadvisable.</p>
<p>But is it?</p>
<h1 id="heading-make-it-make-sense">Make it Make Sense</h1>
<p>AI is here to stay and is one of the most transformative events to occur in IT. Thus, blindly stiff-arming it - even with APEX-SERT - is not the right answer. In fact, it’s not even the right question. Put more succinctly, the right question is: how do we make AI work with APEX-SERT, not should we.</p>
<p>Using it to replace the rules engine still doesn’t make sense yet, as there’s still too many drawbacks. First, if we were to rewrite APEX-SERT to work exclusively with AI, we would likely price out a segment of our users. AI is not free, and adding that cost as mandatory is also a non-starter.</p>
<p>Second, many organizations have strict rules as to where and how AI can be used. Going “all in” on an AI-only version of APEX-SERT would also shrink its potential user base, as some organizations may not be allowed to use it on their applications anymore.</p>
<p>After some thought, it finally hit me where AI and APEX-SERT could meet: Evaluating the quality of exceptions.</p>
<h1 id="heading-exception-quality">Exception Quality</h1>
<p>Exceptions are, by and large, one of my favorite features of APEX-SERT. They enable developers to provide a rationale when they believe an attribute was incorrectly flagged - a fairly common occurrence. Exceptions also need to be approved by another developer, establishing segregation of duties and making APEX-SERT more compliant with regulations.</p>
<p>When creating an exception, the only requirement is to enter a value. Thus, developers can enter any value they wish, from something comprehensive and detailed to simply the letter “x”. APEX-SERT treats both of these examples as valid and advances the workflow to the next level.</p>
<p>It is not uncommon to have several exceptions per application, creating a significant amount of work for any approver to do, as they will need to evaluate and either approve or reject each of them. We’ve added the ability to bulk add and bulk approve/reject exceptions to make this task easier, but those features do not consider the quality of the exception itself.</p>
<p>This is where AI can and does make a difference.</p>
<p>The code to evaluate the quality of exceptions is amazingly simple. If you look at the procedure below, this is called each time an exception is created. To save cost, if an exception is created for multiple items at the same time, the score &amp; reason is only computed once.</p>
<pre><code class="lang-sql">procedure get_exception_score
  (
   p_rule_id                in number
  ,p_exception              in varchar2
  ,p_exception_score        out number
  ,p_exception_score_reason out varchar2
  )
is
  l_valid_exceptions        varchar2(4000);
  l_summary                 clob;
 <span class="hljs-keyword">begin</span>

<span class="hljs-comment">-- determine score of exception using AI</span>
<span class="hljs-keyword">if</span> reports_pkg.get_pref_value(p_pref_key =&gt; <span class="hljs-string">'AI_ENABLED'</span>) = <span class="hljs-string">'Y'</span> <span class="hljs-keyword">then</span>

  <span class="hljs-comment">-- get the list of valid exceptions</span>
  <span class="hljs-keyword">select</span> valid_exceptions <span class="hljs-keyword">into</span> l_valid_exceptions <span class="hljs-keyword">from</span> <span class="hljs-keyword">rules</span> <span class="hljs-keyword">where</span> rule_id = p_rule_id;

  <span class="hljs-comment">-- prepare the prompt and send to AI</span>
  if l_valid_exceptions is not null then

    l_summary := apex_ai.generate
      (
       p_prompt            =&gt; 'Evaluate the quality of the following exception: ' || p_exception
      ,p_system_prompt     =&gt; <span class="hljs-keyword">replace</span>(reports_pkg.get_pref_value(p_pref_key =&gt; <span class="hljs-string">'AI_EXCEPTION_PROMPT'</span>), <span class="hljs-string">'{VALID_EXCEPTIONS}'</span>, l_valid_exceptions)
      ,p_service_static_id =&gt; reports_pkg.get_pref_value(p_pref_key =&gt; <span class="hljs-string">'AI_STATIC_ID'</span>)
       );

    <span class="hljs-comment">-- log the results</span>
    apex_debug.message(l_summary);

    <span class="hljs-comment">-- parse the AI response to get the score and reason</span>
    <span class="hljs-keyword">select</span>
       json_value(l_summary, <span class="hljs-string">'$.score'</span>)  <span class="hljs-keyword">as</span> score
      ,json_value(l_summary, <span class="hljs-string">'$.reason'</span>) <span class="hljs-keyword">as</span> reason
    <span class="hljs-keyword">into</span>
       p_exception_score
      ,p_exception_score_reason
    <span class="hljs-keyword">from</span>
      dual;

  <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> get_exception_score;
</code></pre>
<p>Basically, the code will call the LLM using <strong>apex_ai.generate</strong>. The LLM will then return only a JSON document that is parsed out via SQL and returned to the <strong>OUT</strong> parameters. That’s it!</p>
<p>With AI, it’s less about the code and more about the prompt. Writing a clear, concise prompt will go a long way when working with LLMs. In fact, I typically ask AI to get me started by creating a prompt for me, and then I edit and modify it as needed. Here’s the one that is used to evaluate the exceptions in APEX-SERT:</p>
<pre><code class="lang-plaintext">You are an Oracle IT security expert reviewing an exception provided by a user in 
response to a flagged vulnerability from the APEX-SERT tool. The user believes 
the flag is a false positive.

You are provided with a list of acceptable exceptions for this rule:

{VALID_EXCEPTIONS}

Evaluate how well the user's exception aligns with the acceptable exceptions. 

Assign a score from 1 to 5, where:

 1 = Poorly written or irrelevant exception 
 3 = Partially acceptable, needs improvement or clarification 
 5 = Clearly aligns with acceptable exceptions and is well-justified

Return only a JSON document in the following format:

{ "score": &lt;integer from 1 to 5&gt;, "reason": "" }

Keep the explanation concise (1–2 sentences) and do not return any additional 
commentary outside the JSON.
</code></pre>
<p>This prompt tells the LLM to compare each exception to a set of probable exceptions for a given rule and produce a score from 1 to 5, with 1 indicating that the exception is of poor quality and no where near a match to the anticipated ones and a 5 indicating that the exception is of high quality and a very close match to the anticipated ones. The <strong>{VALID_EXCEPTIONS}</strong> token will be replaced with valid exceptions for the specific rule in question each time. Based on the instructions, the prompt will return a simple JSON document that can easily be parsed out.</p>
<p>Using this score, a reviewer can readily identify which exceptions likely need to be rejected as well as those that are likely valid. This reduces the amount of time it will take a reviewer to approve or reject a batch of exceptions considerably, as they can use the score and reason that AI provides as guidance.</p>
<p>The exception scoring has been added to the Exceptions Report, which has be relocated under the Exceptions button on the main evaluation page. Here’s a preview of what it will look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753878274788/746bf556-87c4-45d0-a757-80214a9d0113.png" alt class="image--center mx-auto" /></p>
<p>We stopped short of allowing AI to refuse poorly written exceptions, as we want to be sure that we gradually and pragmatically apply AI to APEX-SERT. I can see adding this feature in at some point, with the option to disable it if desired.</p>
<h1 id="heading-next-steps">Next Steps</h1>
<p>Once the AI-enabled version of APEX-SERT is released - which should be soon - enabling the AI portion will be a 100% optional and opt-in process. You will not be forced to use the AI feature at all and the rest of APEX-SERT will continue to function without it for the foreseeable future.</p>
<p>You will need to provide your own LLM to use the AI features. As you know, AI calls are not free, so each user will be responsible for the associated costs.</p>
<p>We will continue to explore other ways to augment APEX-SERT with additional AI capabilities. One area we are likely to explore is deeper analysis of PL/SQL code, including both code embedded in APEX and named packages, procedures, and functions. Using an LLM and a well-defined prompt should produce better results than writing PL/SQL code to evaluate other PL/SQL code. The AI is just better at detecting string patterns and can do a much more comprehensive job with tasks like this.</p>
<p>Let me know if there’s another area of APEX-SERT that you think AI would be a good fit for.</p>
<hr />
<p><em>Title photo by</em> <a target="_blank" href="https://unsplash.com/@lianhao"><em>Lianhao Qu</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[APEX-SERT: Now Available]]></title><description><![CDATA[After a long and thorough set of mandatory legal processes, I’m happy to announce that starting today, APEX-SERT is back and available for the community to download and use! It’s still open source, and can be used as much and as often as you like for...]]></description><link>https://spendolini.blog/apex-sert-now-available</link><guid isPermaLink="true">https://spendolini.blog/apex-sert-now-available</guid><category><![CDATA[orclapex]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Security]]></category><category><![CDATA[OCI]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 22 Apr 2025 12:11:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745322657037/23cab3a8-2ad9-4bfb-893a-c6fa88171f38.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After a long and thorough set of mandatory legal processes, I’m happy to announce that starting today, APEX-SERT is back and available for the community to download and use! It’s still open source, and can be used as much and as often as you like for free via the Universal Permissive License (UPL) 1.0.</p>
<p>The feature set has not changed much since my last post about it, so if you need a refresher, have a look here: <a target="_blank" href="https://spendolini.blog/apex-sert-the-sql">https://spendolini.blog/apex-sert-the-sql</a></p>
<p>I can’t thank <a target="_blank" href="https://bsky.app/profile/milobandit.bsky.social">Michael Potter</a> and his team in Australia enough for their help in getting this project released. He specifically helped navigate the several processes &amp; procedures required by our employer, as well as helped to tune and squash some bugs.</p>
<h2 id="heading-sqlcl-amp-liquibase">SQLcl &amp; Liquibase</h2>
<p>One difference that you may notice is that we used SQLcl &amp; Liquibase to build the installer. This gives us a lot more flexibility when releasing updates, as the installer will always know where to start, regardless of the version. There’s nothing different about the installation process - just run <code>install.sql</code> as SYSTEM or ADMIN and you’ll be up and running in no time at all!</p>
<h2 id="heading-printing">Printing</h2>
<p>APEX-SERT allows for either AOP or OCI DocGen to be integrated for printing of reports. In either case, you must procure your own instance &amp; license and configure APEX-SERT accordingly.</p>
<h2 id="heading-builder-extension">Builder Extension</h2>
<p>APEX-SERT makes use of one of the newer features of APEX: Builder Extensions. As a Builder Extension, you can run APEX-SERT as the same user you develop APEX applications with. This enhances the security posture of APEX-SERT itself, as previous releases needed to make a small adjustment to the APEX core schema - something that is no longer possible on ADB.</p>
<h2 id="heading-feedback">Feedback</h2>
<p>Find a bug? Have an idea you’d like to see added? Please use GitHub issues, which can be found here: <a target="_blank" href="https://github.com/oracle-samples/apex-sert/issues">https://github.com/oracle-samples/apex-sert/issues</a></p>
<h2 id="heading-download">Download</h2>
<p>Ready to get started? You can download it here: <a target="_blank" href="https://github.com/oracle-samples/apex-sert">https://github.com/oracle-samples/apex-sert</a></p>
<hr />
<p><em>Title photo by</em> <a target="_blank" href="https://unsplash.com/@tashakostyuk"><em>Tasha Kostyuk</em></a> <em>on Unsplash.</em></p>
]]></content:encoded></item><item><title><![CDATA[Exploring a SaaS API Platform]]></title><description><![CDATA[If you followed my last post, you should have integrated Jira Cloud & APEX so that you can see projects, issues and comments from the APEX side. As you may have inferred, building that application did take some time, most of which was spent on learni...]]></description><link>https://spendolini.blog/exploring-a-saas-api-platform</link><guid isPermaLink="true">https://spendolini.blog/exploring-a-saas-api-platform</guid><category><![CDATA[Postman]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[JIRA]]></category><category><![CDATA[REST API]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 28 Jan 2025 13:15:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736223793104/21cb760b-64fc-4ffa-8b54-69a08db70ff5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you followed <a target="_blank" href="https://spendolini.blog/configuring-the-apex-jira-environments">my last post</a>, you should have integrated Jira Cloud &amp; APEX so that you can see projects, issues and comments from the APEX side. As you may have inferred, building that application did take some time, most of which was spent on learning about the Jira API, how it works and which endpoints to call to get what I wanted.</p>
<p>As I’ve said before, if you’ve learned how to work with one platform’s API, you’ve learned just that - a single platform’s API. Unfortunately, they are all different, and there’s no real shortcut that you can take here. You simply have to put in the work.</p>
<h2 id="heading-api-documentation">API Documentation</h2>
<p>Finding the API documentation page should be your first step. In most cases, large SaaS platforms will have plenty of resources regarding their API, what it does, how to connect, etc. Jira Cloud is no exception. Their API page is loaded with good information and can be found here: <a target="_blank" href="https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about">https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about</a></p>
<p>Taking a look at their page, it’s broken out into a number of sections. Let’s walk through most of them, since regardless of the platform, these are the main things you’ll need to consider.</p>
<h3 id="heading-version">Version</h3>
<p>APIs, like most software, have versions. Each version will add or remove parameters, results or both. Typically, the version of the API is included in the URL. Jira takes this approach, and in their documentation note the difference between version 2 &amp; 3. For our purposes, either should work.</p>
<h3 id="heading-authentication-amp-authorization">Authentication &amp; Authorization</h3>
<p>This can be one of the trickier parts of calling an API, as there’s a number of ways to handle authentication (authN) and authorization (authZ). It is important to understand how your SaaS provider does this, and ensure that you adhere to their guidelines and best practices.</p>
<p>For our purposes, we’re going to stick with the Ad-hoc API calls option. This is the simplest of the batch, and uses Basic Authentication, or a username and password combination.</p>
<h3 id="heading-permissions">Permissions</h3>
<p>Jira manages permissions at the user level. Thus, if your user has a specific permission mapped to them, they will be able to call APIs that require that permission. In the example APEX application, the Jira user was an administrator, and thus could call pretty much any API. In a real-world scenario, different users would have different permissions, and you would need to account for which API they would be able to call at the APEX level - likely by querying an API to determine which permissions they had and using APEX authorization schemes to hide &amp; show corresponding APEX components.</p>
<h3 id="heading-expansion">Expansion</h3>
<p>For efficiency, Jira APIs use “resource expansion”. All this means is that if you call the plain API, you’ll get a small number of values returned in the JSON file. If you specify values in the “expand” option, you’ll get additional values in the JSON. Thus, if you don’t need lots of data, don’t specify values in the “expand” option and vice versa.</p>
<h3 id="heading-pagination">Pagination</h3>
<p>Perhaps the most important part of the Jira API is pagination, as it provides some of the core mechanical support for how APEX navigates data found in reports.</p>
<p>When you build a report on a local table or view, APEX can handle all of the pagination services natively, as it knows how to augment the local SQL query to determine how many rows there are, which set it’s looking at currently, and all of the other parameters required to allow users to paginate through the results. This is native, no-code functionality of APEX and has been present since essentially day one.</p>
<p>However, when you put the data sources behind a web service, APEX becomes “blind”, since it can no longer get pagination values by augmenting SQL calls. Additionally - and perhaps one of the most important concepts - is that web services will only send back a portion of the full dataset per request. Imagine asking for a million rows of data. It’s just not practical to send all million rows over in a JSON file, as it would take too long to transmit and then parse that file all at once.</p>
<p>Thus, web services typically send results in much smaller chunks, typically 25 or 50 rows of data. As users click “next set”, the web service is called again, and the next set of rows are sent back and displayed. But in order for APEX - or any client - to know where it is in the dataset, there needs to be a way to manage the pagination.</p>
<p>Thus, web services typically provide a mechanism as to how they manage pagination. They do this by providing bits of information as part of the JSON document that they send back. These results can be anywhere in the result set - it varies by web service.</p>
<p>For example, the Jira API gives the following example of how they handle pagination:</p>
<pre><code class="lang-plaintext">{
    "startAt" : 0,
    "maxResults" : 10,
    "total": 200,
    "isLast": false,
    "values": [
        { /* result 0 */ },
        { /* result 1 */ },
        { /* result 2 */ }
    ]
}
</code></pre>
<p>Each parameter is also defined on their site. Here’s those definitions:</p>
<ul>
<li><p><code>startAt</code> is the index of the first item returned in the page.</p>
</li>
<li><p><code>maxResults</code> is the maximum number of items that a page can return. Each operation can have a different limit for the number of items returned, and these limits may change without notice. To find the maximum number of items that an operation could return, set <code>maxResults</code> to a large number—for example, over 1000—and if the returned value of <code>maxResults</code> is less than the requested value, the returned value is the maximum.</p>
</li>
<li><p><code>total</code> is the total number of items contained in all pages. This number <strong><em>may change</em></strong> as the client requests the subsequent pages, therefore the client should always assume that the requested page can be empty. Note that this property is not returned for all operations.</p>
</li>
<li><p><code>isLast</code> indicates whether the page returned is the last one. Note that this property is not returned for all operations.</p>
</li>
</ul>
<p>By reading these values, developers can augment their code so that they can request data from a web service, keep track of where they are, and then request the next or even previous set of rows and display those to the user. Developers will also want to know when they reach the end of a result set; hence the <code>isLast</code> parameter. When that is true, developers will need to disable or hide the “next set” button, since there is no such thing.</p>
<p>If building this from scratch, there’s a fair amount of work required to consume, manage and react to the pagination metadata. Fortunately for us, when using APEX’s REST data sources, we don’t have to write any code at all to manage pagination. All we need to do is provide some guidance to APEX by way of pagination details and it will handle the rest. <strong>This benefit alone - APEX being able to automatically handle pagination of data received from a web service - cannot be understated.</strong></p>
<p>Now that we know how Jira handles pagination, we can adjust one of our REST data sources accordingly. Below is what the pagination section of the Projects REST data source looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737300405332/1ebf2fc9-c499-4fc5-b4e3-9af41b702341.png" alt class="image--center mx-auto" /></p>
<p>We’ll dive into the specifics of these setting in a future post. For now, you should be able to see the values specified in the Jira API mapped to their corresponding attributes in APEX.</p>
<h3 id="heading-ordering">Ordering</h3>
<p>Another important concept is ordering the results. Again, consider that million-row dataset. If we wanted to sort by Issue Number, and only have 50 rows, we simply can’t. Thus, most web services will do their sorting on the server and send back pre-sorted data.</p>
<p>Jira allows us to pass a column name to the URL parameter <code>orderBy</code> to perform sorting. It will then sort the data accordingly and return the dataset in a pre-sorted format. Adding a <code>+</code> or <code>-</code> before the column name will sort the data ascending or descending, respectively.</p>
<h3 id="heading-timestamps">Timestamps</h3>
<p>When working with APIs, dates are sent as strings, typically in a timestamp format. There is no “date” datatype, as it’s all just text. Many web services - including Jira - will use the <a target="_blank" href="https://www.w3.org/TR/NOTE-datetime">ISO 8601</a> format for timestamps. And in Jira’s case, the timezone used is the one mapped to the user’s account that the API key is associated with.</p>
<h1 id="heading-api-endpoints">API Endpoints</h1>
<p>Now that we have a good idea as to the basics of how Jira’s APIs work, let’s dig into some of the specifics of what it actually does. Most SaaS APIs will provide not only documentation on each endpoint, but also either a Postman Collection, an OpenAPI specification or both. Jira provides both, and they can be found at the top-right of this page: <a target="_blank" href="https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about">https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about</a></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Full Disclosure: I built out most of the REST Data Sources in the example Jira application by hand. I’d like to say there was some strategic reason for this decision, but there was not. I simply did not take the time to notice the Postman &amp; OpenAPI links and use them as a starting point. 🤷‍♂️</div>
</div>

<h2 id="heading-postman-collection">Postman Collection</h2>
<p><a target="_blank" href="https://www.postman.com/">Postman</a> is a popular API development tool that many organizations use. It basically allows you to set up a test harness and make RESTful calls to API endpoints. I’m not going to get into the specifics on how to use it here; have a look <a target="_blank" href="https://learning.postman.com/docs/getting-started/overview/">at their site</a> to get started if it’s new to you.</p>
<p>Using Postman to explore and become familiar with an API is almost always the best first step. Use the APIs documentation &amp; samples to get a jump on how to use it, paying specific attention to how to paginate, sort and pass parameters to it, as well as the requisite security attributes.</p>
<p>Using Postman to do this allows me to focus on the parameters and functionality of the APIs in an easy to use tool. I find this approach a lot easier and faster than wrapping these calls in SQL or PL/SQL. Postman also provides a fair bit of debug information if and when you get stuck.</p>
<p>Let’s fire up Postman and hit our first endpoint!</p>
<ol>
<li><p>Navigate to the Jira Cloud API page here: <a target="_blank" href="https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about">https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about</a></p>
</li>
<li><p>Click the icon labeled <strong>Postman Collection</strong> and save the file to your desktop.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738033872617/0f2dee12-7658-4774-b5d4-b4ab6dd2f5d0.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li><p>In Postman, click <strong>Import</strong>.</p>
</li>
<li><p>Locate the collection file that you just downloaded and drag it to the Postman window.</p>
</li>
<li><p>The collection should automatically load and you’ll see the following screen:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738032653456/183d00bf-ebc8-40ba-9a4b-091edf5dde33.png" alt class="image--center mx-auto" /></p>
<p>Now that we have a collection, let’s create an environment. An environment is a set of variables that are automatically passed to a collection. It’s used when you have different sets of APIs across different servers - such as dev, test, QA &amp; production. While not required, it’s a lot easier to use environments to set and store variables, as they will impact any endpoint that makes use of the variable syntax in Postman.</p>
<p>To create an environment:</p>
<ol>
<li><p>Click on the <strong>Environment</strong> tab in the left side of the page.</p>
</li>
<li><p>Click the “<strong>+</strong>” icon to crate a new environment.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738034026921/f82cb31d-27b4-4f97-bc10-e5bcbb73b81b.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>Rename your environment to something descriptive. I’ll use <code>spendoliniblog</code> for mine.</li>
</ol>
<p>Next, we need to add the three main variables that are required to call the Jira API: <code>host</code>, <code>username</code> and <code>apiToken</code>.</p>
<ol start="4">
<li><p>Enter <code>host</code> as the <strong>Variable</strong>, set <strong>Type</strong> to <code>default</code> and enter your Atlassian hostname for the <strong>Initial Value</strong>. This was the hostname that you specified when creating your Atlassian account in the <a target="_blank" href="https://spendolini.blog/configuring-the-apex-jira-environments">previous blog post</a>.</p>
</li>
<li><p>On the next line, enter <code>username</code> as the <strong>Variable</strong>, set <strong>Type</strong> to <code>default</code> and enter the email that you used to sign up for your Atlassian account as the <strong>Initial Value</strong>.</p>
</li>
<li><p>On the next line, enter <code>apiToken</code> as the <strong>Variable</strong>, set <strong>Type</strong> to <code>secret</code> and paste in the API Key that you generated when you signed up for your Atlassian account as the <strong>Initial Value</strong>.</p>
</li>
<li><p>Click <strong>Save</strong>.</p>
</li>
</ol>
<p>The screen should look similar to this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738034823594/2dcd17b9-7527-41d5-8e2d-3a9063f6bcd1.png" alt class="image--center mx-auto" /></p>
<p>Let’s use these freshly defined credentials to call one of the APIs to make sure that it all works.</p>
<ol>
<li><p>Click on the <strong>Collections</strong> icon on the left side of the screen.</p>
</li>
<li><p>Scroll down and locate and expand the <strong>Projects</strong> folder.</p>
</li>
<li><p>Select <strong>Get All Projects</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738035095119/96c952e8-2643-43ab-850a-5c301e3932d9.png" alt class="image--center mx-auto" /></p>
<p>Notice that many of the variables are highlighted in pink - indicating that they do not have a current value. You can hover over the blue ones to see what the value is set to.</p>
<p>To apply our environment to this endpoint:</p>
<ol start="4">
<li>Select the environment we created from the <strong>Environments</strong> menu in the upper right.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738035213889/5e16d5b5-9bec-406c-af24-7b653803f55b.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Click <strong>Send</strong> to call the endpoint using the variables defined in our environment.</li>
</ol>
<p>If configured correctly, you should see something similar to the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738035268202/e67c4bc8-6632-4223-94bb-885173d8d71b.png" alt class="image--center mx-auto" /></p>
<p>Feel free to play with any of the other endpoints - but understand that changes will be immediately made to your Jira projects and may be irrevocable. It’s a good idea to make sure that the Jira environment that you’re interacting is not production.</p>
<h2 id="heading-openapi">OpenAPI</h2>
<p>According to their website, <a target="_blank" href="https://www.openapis.org/what-is-openapi">OpenAPI</a> “<em>provides a consistent means to carry information through each stage of the API lifecycle. It is a specification language for HTTP APIs that defines structure and syntax in a way that is not wedded to the programming language the API is created in.</em>”</p>
<p>In other words, OpenAPI is a standard way to define RESTful endpoints. This allows developers to quickly discover what an API does, how it works and what operations are available. This standard is language agnostic - developers need to know nothing about the language that the API is written in, only about the language from which they will call it.</p>
<p>There’s a number of tools out there that can read OpenAPI specifications - Postman being one of them. Let’s stick with that and import the OpenAPI document.</p>
<ol>
<li><p>Navigate to the Jira Cloud API page here: <a target="_blank" href="https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about">https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about</a></p>
</li>
<li><p>Click the icon labeled <strong>OpenAPI</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738036594604/9c5305da-5a44-4257-b0ed-347f8bdba6d4.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>Your browser will open a new tab and you should see the following:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738036617966/4411bb15-7046-4b29-b095-8fdc9b84b0b9.png" alt class="image--center mx-auto" /></p>
<p>This is intentional; the browser is displaying the OpenAPI document, which is in YAML format. Let’s save the contents to our machine so that we can import it into Postman.</p>
<ol start="4">
<li><p>From the <strong>File</strong> menu, select <strong>Save Page As</strong> or a similar option depending on your browser.</p>
</li>
<li><p>Save the file to your desktop.</p>
</li>
<li><p>Switch to Postman, select <strong>Collections</strong> on the left side of the screen and then click <strong>Import</strong>.</p>
</li>
<li><p>Drag the file you just downloaded to the Postman window. You should see something similar to this:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738038475356/5ef698e3-40ef-44c9-9510-8c7e7a6374af.png" alt class="image--center mx-auto" /></p>
<ol start="8">
<li>Leave the default option selected and click <strong>Import</strong>. Once loaded, you should now have two collections:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738038593541/42ac0c6e-98b9-47ee-93f4-63e6823980a2.png" alt class="image--center mx-auto" /></p>
<p>Unfortunately, I could not get the OpenAPI to import properly. Many of the endpoints had redundant variables and some of the initial values caused calls to the endpoint to fail. To give you an example, here’s the <code>projects/search</code> endpoint from the Postman Collection:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738039170515/bf2e4ab3-5c44-468b-b6f0-a49054202e86.png" alt class="image--center mx-auto" /></p>
<p>And here’s the same endpoint from the OpenAPI import:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738039195933/076f1fe1-5d58-4d46-b0dd-77827ddf840f.png" alt class="image--center mx-auto" /></p>
<p>At least for Jira, I’ll stick with the Postman collection.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Using a tool like Postman to explore an API is almost a requirement. Vendor’s APIs will vary widely from API to API, so even if you have experience with a few APIs, the next one can throw you off. Get used to using the tool’s more advanced capabilities like environments, variables and the vault to make it even more impactful. Only once you’ve mastered the basics of any API are you ready to start calling it from APEX.</p>
<p>The key skill that you’ll want to take away from all of this is learning how to apply a change in Postman to an APEX REST Data Source. Once you’ve mastered this skill, there won’t be an API you can’t conquer.</p>
<p>Coming up next - we’ll dive into the implementation of the APIs inside APEX and cover the steps and through processes that I went through to get that working.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@rocinante_11?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><em>Mick Haupt</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Configuring the APEX & Jira Environments]]></title><description><![CDATA[One of the lesser-used features of APEX is using REST Data Sources to call non-Oracle web services to interact with data stored in another database. APEX has had web service integration for years now, but only in the last few releases has it been enh...]]></description><link>https://spendolini.blog/configuring-the-apex-jira-environments</link><guid isPermaLink="true">https://spendolini.blog/configuring-the-apex-jira-environments</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[JIRA]]></category><category><![CDATA[REST API]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 07 Jan 2025 13:52:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736223679075/8b556766-2594-42ab-a29a-d9ebadf91054.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the lesser-used features of APEX is using REST Data Sources to call non-Oracle web services to interact with data stored in another database. APEX has had web service integration for years now, but only in the last few releases has it been enhanced enough to be easy to use and extensible at the same time. Using REST Data Sources, developers can now assemble declarative components that can be easily referenced as the source of a report or in PL/SQL.</p>
<p>As cool as they are, REST Data Sources do take some getting used to. Some features are fairly obvious and easy to learn, while others are not.</p>
<p>Rather than walk through all of the specifics of REST Data Sources, I thought it would make more sense to do it in the context of a real integration - something that we can all relate to. Thus, I selected to use Jira for this for several reasons.</p>
<p>First of all, Jira has a massive API set that’s actually somewhat easy to understand and fairly well documented. This will make it easy for anyone who wants to expand on the work that I outline in my posts.</p>
<p>Second, Jira also has a “free forever” tier. You can create a cloud account with up to 10 users for free. This will make it easy for people to follow along, as there’s no cost at all.</p>
<p>Third - and perhaps most importantly - Jira has complex logic that sits behind the APIs that we don’t have to re-write in PL/SQL. If this were a real project, this would be absolutely critical, as all transactions from either APEX or Jira is fed to the same API layer, ensuring that we only have a single code base to maintain. And in the case of a SaaS application like Jira Cloud, we don’t even need to maintain it.</p>
<p>For example - if I want to assign a different project code to a Jira Project, all I need to do is submit the update. Jira’s APIs take care of updating the title of all of the issues. Same thing if I delete an Epic - all related issues are updated automatically. Even cooler - any automation that is configured will still fire if it’s triggered by the APIs.</p>
<p>Most SaaS platforms fall into this category - API-first platforms that work regardless where the transaction is originated. APEX can easily adapt and also fit into this architecture as you’ll see as soon as you have your application up &amp; running.</p>
<p>The APEX application that I built only does a fraction of what Jira is capable of. It can create bugs, tasks and epics, assign them to different workflows, add labels to any of them and capture notes. It also has a project-level summary dashboard with charts &amp; reports. Keep in mind here my goal was to create a real-world, practical example to highlight what’s possible with REST Data Services, not build a feature-for-feature implementation of Jira.</p>
<h1 id="heading-getting-started">Getting Started</h1>
<p>Rather than create a series of blogs that have all of the steps outlined to build the solution (there’s just too many), I’m going to take a different approach: I’ll provide the application for download and then blog about what I did, referencing what’s in the application. I believe that this approach will make it easier to follow along, as I probably spent too much time making incremental changes to the UI to get it just the way I wanted it.</p>
<p>You will, however, need your own Atlassian account if you want to test the integration. Fortunately, they are free and take just seconds to set up.</p>
<h2 id="heading-create-a-free-atlassian-account">Create a Free Atlassian Account</h2>
<p>Like Oracle, Atlassian has a free tier which limits what you can &amp; can’t do, how many users you can have, etc. Basically, you can have up to 10 users and 2GB of data across their platform. For testing and learning, that’s more than enough.</p>
<p>You can read about the specifics here: <a target="_blank" href="https://www.atlassian.com/software/free">https://www.atlassian.com/software/free</a></p>
<p>Let’s walk through the process of crating a new account:</p>
<ol>
<li><p>Navigate to the following URL: <a target="_blank" href="https://www.atlassian.com/try/cloud/signup?bundle=jira-software&amp;edition=free">https://www.atlassian.com/try/cloud/signup?bundle=jira-software&amp;edition=free</a></p>
</li>
<li><p>Enter your email address and click <strong>Sign Up</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736133392945/ff6cd9d2-5a31-4847-a3ce-28e679a22f19.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>A six-character code will be sent to the email that you used to sign up. Open that email and enter the code on the next screen and click <strong>Verify</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736133435556/a4ed4591-e400-47eb-b073-12be242e5a12.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li>Next, add your <strong>Full Name</strong> and a <strong>Password</strong> for your account; click <strong>Continue</strong> when done.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736133548158/9a1ff332-f60e-4614-bb16-72e0994c7732.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Enter the name of the site. You’ll want to remember this full URL for later when we add the Jira endpoint to APEX. In my case, it’s <code>spendoliniblog.atlassian.net</code>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736133309015/c78b6ea6-aee5-46d9-bc02-db750c6afe89.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li><p>Next, answer a couple of questions about how you’ll use Jira.</p>
</li>
<li><p>When you’re done, you’ll see the Welcome Page, where you can name your first project. Name it anything you like; I took the default “My Scrum Project”.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736133281101/b1efed63-faf2-4d61-9aac-6ca8faa709cb.png" alt class="image--center mx-auto" /></p>
<p>Take a few minutes to explore Jira Cloud. There’s plenty of documentation and a helpful community on Atlassian’s site that you can refer to, should you get stuck.</p>
<h2 id="heading-downgrade-to-free-edition">Downgrade to Free Edition</h2>
<p>You’ll need to make an adjustment to the account that you just created. Technically, you’re in a 30 day free trial for the Premium Tier, which at the time I’m writing this post, is $17/user/month. From what I remember, once the 30 days are up, you can also make this adjustment, but here’s the steps if you don’t want to wait.</p>
<p>To downgrade:</p>
<ol>
<li>Click the gear icon in the upper-right and select <strong>Atlassian account settings</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736170880003/280ee77f-16eb-420e-adf3-0039dc18164a.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li>Click on the <strong>Billing</strong> tab.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736170956064/0e5a8de3-4a4b-4baa-8ff7-89652c668e53.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li>Click <strong>Change Plan</strong>. On the next screen, click <strong>Select Free</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736170981781/d6221783-1baf-4a33-9eaa-8e6e921f9591.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Click <strong>Downgrade to free</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171007361/7858b724-0fa3-459a-835d-425c3cd9e7ce.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li>You’ll see the following confirmation message:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171022632/036706dd-b1d6-474a-92cc-fea55b58dbc6.png" alt class="image--center mx-auto" /></p>
<p>Of course, if you want to pay for Jira to unlock more features, have at it. This blog series makes use of only those available with the Free Edition.</p>
<h2 id="heading-create-an-atlassian-api-key">Create an Atlassian API Key</h2>
<p>In order to call the Jira APIs, you’ll need to create an API key. API keys in Jira are mapped to the user. Thus, if you have 5 users calling the APIs, they will each need their own API key. Makes sense when you have a platform that charges by the user and don’t want the API layer to become a back door that skirts around licensing!</p>
<p>To create an API Key:</p>
<ol>
<li><p>Navigate back to the home page of your Atlassian account with the following link: <a target="_blank" href="https://home.atlassian.com/"><strong>https://home.atlassian.com/</strong></a></p>
</li>
<li><p>Click the gear icon in the upper-right and select <strong>User management</strong>.</p>
</li>
<li><p>Click on <strong>Account Settings</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171348353/0e0f307e-ec85-43f7-a968-76db2ca6a62d.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li>Click on the <strong>Security</strong> tab.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171244167/326bf0c2-c9bf-4538-8a1f-b02358956296.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Click <strong>Create and manage API tokens</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171409461/0f7155bc-002c-4943-9eac-eabfd86cd87f.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li>Click <strong>Create API Token</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171431354/6c89be02-ea99-4928-a287-ac5872a44608.png" alt class="image--center mx-auto" /></p>
<ol start="7">
<li>Enter <code>APEX Integration</code> for the <strong>Name</strong> and set the <strong>Expires on</strong> to a far-out date. Atlassian lets you set the date up to a year out, so that’s what I chose.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171450724/134cce70-51ef-422b-b093-f882815cfa3c.png" alt class="image--center mx-auto" /></p>
<ol start="8">
<li>Click <strong>Create</strong>. When the token is displayed, be sure to copy it and store it somewhere safe. We will need this later when installing the APEX application, and this is the only time it will be displayed.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736171492056/e216ba67-03e5-482e-82fc-8bfdab1f128a.png" alt class="image--center mx-auto" /></p>
<p>You should now have three pieces of information that we will need later:</p>
<ol>
<li><p>Your Atlassian hostname</p>
</li>
<li><p>Your email address that you used to sign up for Atlassian with</p>
</li>
<li><p>Your API Key</p>
</li>
</ol>
<h2 id="heading-choose-where-to-host-the-apex-application">Choose Where to Host the APEX Application</h2>
<p>You can host the APEX application anywhere that has access to Jira Cloud. I’ve tested it on both <strong>apex.oracle.com</strong> and APEX on ADB in OCI. It will even work on a local container instance of APEX! If you’re running your own instance behind a VPN, you may run into issues where calls from the database to Atlassian are blocked.</p>
<p>If you’re going to install this application on either ADB-S or apex.oracle.com, you can skip the following step. If you’re installing this on a local container or DBaaS or on-prem APEX, you’ll likely need to execute the following as <code>ADMIN</code> (ADB-D) or <code>SYSTEM</code> (non-ADB) and run the following:</p>
<pre><code class="lang-plaintext">begin
  dbms_network_acl_admin.append_host_ace (
    host       =&gt; '[server name].atlassian.net',
    lower_port =&gt; 443,
    upper_port =&gt; 443,
    ace        =&gt; xs$ace_type(privilege_list =&gt; xs$name_list('connect'),
                              principal_name =&gt; 'APEX_240100',
                              principal_type =&gt; xs_acl.ptype_db));
end;
/
</code></pre>
<p>Make sure to adjust the value for <code>principal_name</code> if you’re not running APEX 24.1 and change <code>[server name]</code> to the name that you used.</p>
<h2 id="heading-download-and-install-the-apex-application">Download and Install the APEX Application</h2>
<p>Once you have a place to put the application and have opened up the respective URL for your Atlassian site, all that’s left is to download and install the APEX application.</p>
<p>You will need at least APEX 24.1 for this to work.</p>
<ol>
<li>Download the APEX application by <a target="_blank" href="https://objectstorage.us-ashburn-1.oraclecloud.com/n/idrmltuyzbbd/b/spendolini-blog-public/o/apex_and_jira.sql">clicking here</a>.</li>
</ol>
<ul>
<li><div data-node-type="callout">
  <div data-node-type="callout-emoji">💡</div>
  <div data-node-type="callout-text"><strong>Warning</strong>: This APEX application calls the Jira Cloud REST APIs and has not been thoroughly tested. There is a risk that it will make updates that cause issues with your Jira data. Use it at your own risk.</div>
  </div>


</li>
</ul>
<ol start="2">
<li><p>Login to your APEX workspace as at least a developer.</p>
</li>
<li><p>Click on the <strong>App Builder</strong> icon.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736172996610/b58aef6a-2492-4ade-86e3-f284bdac982e.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li>Click on <strong>Import</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736173013268/4ea78cc4-d573-493d-848d-28e10b6919cd.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Drag the application export file that you downloaded (<code>apex_and_jira.sql</code>) to the <strong>Drag and Drop</strong> region and click <strong>Next</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736173131274/f7226ed6-e3df-45fb-ba2a-ffe7b4ab7f11.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li>Click <strong>Install Application</strong>. No need to select any specific Application ID.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736173173642/50f51163-9e65-4a1f-8d50-9084c1d1f3b0.png" alt class="image--center mx-auto" /></p>
<p>On the next screen, we’ll provide the <strong>Remote Server</strong> information. <strong>Credentials</strong> also need to be provided, but we’ll do that after, since not all of the values we need to provide are available here.</p>
<ol start="7">
<li>In the <strong>Remote Servers</strong> section, change the hostname in the <strong>Base URL</strong> to what you specified when you created your Atlassian account. The full URL should look like this: <code>https://[servername].atlassian.net/rest/api/</code></li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736175062394/fe81bb2e-ac16-4fd2-a0a3-8a28bc199814.png" alt class="image--center mx-auto" /></p>
<ol start="7">
<li><p>Click <strong>Next</strong>.</p>
</li>
<li><p>Click <strong>Edit Application</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736175107296/40f0d285-908e-48e2-acae-95a81c0a8b31.png" alt class="image--center mx-auto" /></p>
<ol start="9">
<li>Click <strong>Shared Components</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736175127516/37ed5a12-555b-400b-85a5-00f73c8be00f.png" alt class="image--center mx-auto" /></p>
<ol start="10">
<li>Click <strong>Credentials</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736175165688/3fb4083b-5c6c-4743-b5c3-f44e28a16406.png" alt class="image--center mx-auto" /></p>
<ol start="11">
<li>Click <strong>Credentials for Jira Projects</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736175206614/ecbd3913-b2fe-4740-a31d-e70e57749522.png" alt class="image--center mx-auto" /></p>
<ol start="12">
<li>Set the values to the following and click <strong>Apply Changes</strong>.</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Client ID or Username</strong></td><td>[email used to sign up for Jira]</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Client Secret or Password</strong></td><td>[API Key]</td></tr>
<tr>
<td><strong>Verify Client Secret or Password</strong></td><td>[API Key]</td></tr>
<tr>
<td><strong>Valid for URLs</strong></td><td><code>https://[servername].atlassian.net/rest/api/</code></td></tr>
</tbody>
</table>
</div><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736178057769/146760d4-4b46-4289-af44-44401a63ffb1.png" alt class="image--center mx-auto" /></p>
<ol start="13">
<li>Navigate back to the application builder and run the application. If all was configured correctly, you should see an APEX application with a link to the starter Jira project that you created earlier.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736178431064/c4a22585-1e7d-4d48-b03d-4e76852dec52.png" alt class="image--center mx-auto" /></p>
<ol start="14">
<li>Click on the tile for <strong>My Scrum Project</strong> to see the details. It should be blank at this point unless you added an issue earlier.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736179700690/339acab9-f92a-4e22-834b-240151cd9cac.png" alt class="image--center mx-auto" /></p>
<p>At this point, the integration is up and running. You should be able to create issues in either APEX or Jira, reload, and see them on either side.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736180866404/1ea3fc7f-a6dc-4df4-b3d5-aac63230334f.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1736180873676/f446926d-0fc1-4fdc-8bb2-7ef829cde0ca.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-caveats">Caveats</h2>
<p>I tried to keep things as generic as possible so that it’s easy to get to a working application. In doing that, there’s a few items in the APEX application that are disabled by associating them with a Build Option and setting that to exclude.</p>
<h3 id="heading-backlog">Backlog</h3>
<p>While default Jira projects have a Backlog, the default workflow does not. Thus, the “swim lane” on page 10 for Backlog is disabled. If you add another step to your workflow and name it Backlog, you can re-enable this region.</p>
<h3 id="heading-sprints">Sprints</h3>
<p>Additionally, you won’t likely see your issue on the Kanban Board until you add it to a sprint and start it. Make sure to perform those actions in Jira so that both Jira and APEX display the same content.</p>
<p>Additional rules could be built into APEX to manage sprints as well, but that was out of scope for this blog series.</p>
<h3 id="heading-priorities">Priorities</h3>
<p>Default Jira projects do not enable the ability to manage the Priority of issues. Thus, the page item for Priority on page 10 has also been disabled. If you add Priority to your issues screen, you can enable it in APEX and it should work, as it is mapped in the REST Data Source.</p>
<h1 id="heading-next-steps">Next Steps</h1>
<p>This post is just the beginning. I’m going to create several more posts over the next few days that walk through how this integration works, tools that you’ll want to have to build similar things, and tips on how to debug issues when they occur. Stay tuned!</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@edenconstantin0?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><em>Eden Constantino</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Preserving the Oracle Database Container]]></title><description><![CDATA[In our last post, we added DBMS_CLOUD to our Oracle 23ai Container. Since that change was done via adding database objects, the version of our container is now different than what we had downloaded. Additionally, we’ve upgraded the database objects f...]]></description><link>https://spendolini.blog/preserving-the-oracle-database-container</link><guid isPermaLink="true">https://spendolini.blog/preserving-the-oracle-database-container</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[containers]]></category><category><![CDATA[podman]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 17 Dec 2024 00:39:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734368177871/4cc82130-62c3-43fc-84cc-3f04e18a37dd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In our last post, we added <code>DBMS_CLOUD</code> to our Oracle 23ai Container. Since that change was done via adding database objects, the version of our container is now different than what we had downloaded. Additionally, we’ve upgraded the database objects for APEX a couple of posts back, adding more delta from the container we downloaded.</p>
<p>Thus, if we wanted to quickly spin up a new, fresh database that looks like the one we have, we’d be out of luck. While upgrading APEX &amp; adding <code>DBMS_CLOUD</code> only take minutes, it could get repetitive if we need to do this regularly. And that’s just all that’s different today - things will change as we add more features &amp; applications over time.</p>
<p>To solve this issue, we can create a new image from our container. That image can then be used to create new containers - just like we’ve used the images provided by Oracle.</p>
<p>To wrap up this series, let’s walk through how we can create an image based on our container so that we can spin up new containers that have <code>DBMS_CLOUD</code>, APEX and anything else we want pre-installed.</p>
<h1 id="heading-creating-a-new-image">Creating a New Image</h1>
<p>We’ll start by creating a new image.</p>
<ol>
<li>From <strong>Podman Desktop</strong>, locate and stop the <code>oracle</code> container.</li>
</ol>
<p>It is important to stop the container before committing it, otherwise you risk saving a corrupt container.</p>
<ol start="2">
<li>Open a new terminal window and enter and run the following command:</li>
</ol>
<pre><code class="lang-plaintext">podman commit oracle
</code></pre>
<p>This will take a few minutes to complete. You should see something similar to the screenshot below as this is running:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734376114116/aa6a99f3-8127-4f53-93db-9d4d4c9810c8.png" alt class="image--center mx-auto" /></p>
<p>Next, let’s get a list of all images on our local repository.</p>
<ol start="3">
<li>Enter and run the following command:</li>
</ol>
<pre><code class="lang-plaintext">podman images
</code></pre>
<p>You should see something similar to the screenshot below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734376122391/2c78c53b-efd3-4e55-9eb6-be14c060bd4f.png" alt class="image--center mx-auto" /></p>
<ol start="4">
<li><p>Take note of the <code>IMAGE ID</code> of the <code>REPOSITORY</code> named <code>&lt;none&gt;</code>.</p>
</li>
<li><p>Run the following command, replacing <code>[IMAGE ID]</code> with the <code>IMAGE ID</code> from the previous step.</p>
</li>
</ol>
<pre><code class="lang-plaintext">podman tag [IMAGE ID] oracle-new
</code></pre>
<ol start="6">
<li>Switch back to <strong>Podman Desktop</strong> and select the Images tab. Notice that there should be a new entry called <code>localhost/oracle-new</code>. This is the image that was just created and tagged.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734387974469/6b41ba71-c89e-4ac7-ab9b-f9e9ee025029.png" alt class="image--center mx-auto" /></p>
<ol start="7">
<li><p>Create a new container from the new image by clicking on the <strong>Play</strong> button.</p>
</li>
<li><p>Optionally enter a <strong>Container Name</strong> and click <strong>Start Container</strong> and you should be up and running right where you left off!</p>
</li>
</ol>
<p>Ideally, you would destroy your original Oracle Database container first and then use the command line to create a new one based off of the new image. You can do that by running this code:</p>
<pre><code class="lang-plaintext">podman run --name oracle --ip 10.88.0.2 -p 1521:1521 localhost/oracle-new
</code></pre>
<h1 id="heading-summary">Summary</h1>
<p>Creating a new image based on a changed container is a helpful way to preserve your work, should you need to start over from a specific place. Keep in mind that if a new Oracle 23ai image was posted by Oracle, you would need to pull that down and re-apply all of the infrastructure changes there. Thus, it’s a good idea to try to keep any of these changes scripted so that they can be applied quickly and in a single script.</p>
<p>This post will likely wrap up the series <strong>Containerized, Local Oracle 23ai Environment</strong> - at least for now. I’m sure that I’ll find additional things that I want to add to my local development environment, and when I do, I’ll be sure to add them to this series.</p>
<p>If you haven’t already, be sure to start at the first post and go through all of them. I’ve tried to be as specific as possible and provided detailed steps, code snippets and screen shots. Having said that, there’s likely issues and/or typos that make things less than perfect. Please let me know if you find one of those, as I can quickly correct it if and when necessary.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><a target="_blank" href="https://www.techrepublic.com/article/create-custom-images-podman/">https://www.techrepublic.com/article/create-custom-images-podman/</a></li>
</ul>
<hr />
<p><em>Photo by</em> <a target="_blank" href="https://unsplash.com/@samsungmemory?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash"><em>Samsung Memory</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Adding DBMS_CLOUD to our Oracle Container Database - UPDATE]]></title><description><![CDATA[This morning, Steve Muench pointed out something in my recent post about Adding DBMS_CLOUD to your Oracle Container Database: It is no longer necessary to configure a database wallet in Oracle Database 23ai!
Connor McDonald provides the details here ...]]></description><link>https://spendolini.blog/adding-dbmscloud-to-our-oracle-container-database-update</link><guid isPermaLink="true">https://spendolini.blog/adding-dbmscloud-to-our-oracle-container-database-update</guid><category><![CDATA[Oracle]]></category><category><![CDATA[wallet]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[podman]]></category><category><![CDATA[containers]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Mon, 16 Dec 2024 16:16:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734363368478/31b446af-94e1-40e4-b780-abf01681221c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This morning, <a target="_blank" href="https://bsky.app/profile/stevemuench.bsky.social">Steve Muench</a> pointed out something in my recent post about <a target="_blank" href="https://spendolini.blog/adding-dbmscloud-to-our-oracle-container-database">Adding DBMS_CLOUD to your Oracle Container Database</a>: It is no longer necessary to configure a database wallet in Oracle Database 23ai!</p>
<p><a target="_blank" href="https://bsky.app/profile/connormcd.bsky.social">Connor McDonald</a> provides the details <a target="_blank" href="https://connor-mcdonald.com/2024/05/04/23ai-leave-your-wallet-at-home/">here</a> on his blog.</p>
<p>Thus, I’ve removed that entire section from that post, as it’s just not needed. If you already went through the exercise and added a wallet, that’s fine, too. Simply remove the last three lines of the <code>sqlnet.ora</code> file and everything should still work.</p>
<p>This is just another small but significant and time-saving feature of Oracle 23ai designed to make development faster and easier.</p>
]]></content:encoded></item><item><title><![CDATA[Adding DBMS_CLOUD to our Oracle Container Database]]></title><description><![CDATA[If you’re working with object storage on OCI at all, you’ll notice that the Oracle 23ai Free container database is missing a crucial component: DBMS_CLOUD.
DBMS_CLOUD is a package provides a layer so that the database can manage files and directories...]]></description><link>https://spendolini.blog/adding-dbmscloud-to-our-oracle-container-database</link><guid isPermaLink="true">https://spendolini.blog/adding-dbmscloud-to-our-oracle-container-database</guid><category><![CDATA[Oracle]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[podman]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[OCI]]></category><category><![CDATA[object storage]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Sat, 14 Dec 2024 14:52:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733840848883/52406149-feee-48bb-b57d-c62afa44c638.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you’re working with object storage on OCI at all, you’ll notice that the Oracle 23ai Free container database is missing a crucial component: <code>DBMS_CLOUD</code>.</p>
<p><code>DBMS_CLOUD</code> is a package provides a layer so that the database can manage files and directories in OCI’s object storage service. Typically found only in ADB environments, <code>DBMS_CLOUD</code> can also be installed on non-ADB instances. Here’s how:</p>
<h1 id="heading-installing-dbmscloud">Installing DBMS_CLOUD</h1>
<p>There’s a couple of existing resources that will step through installing <code>DBMS_CLOUD</code>:</p>
<ul>
<li><p><a target="_blank" href="https://oracle-base.com/articles/21c/dbms_cloud-installation">Oracle-Base’s DBMS_CLOUD Guide</a></p>
</li>
<li><p><a target="_blank" href="https://support.oracle.com/epmos/faces/DocContentDisplay?_afrLoop=312529336107510&amp;id=2748362.1&amp;_afrWindowMode=0&amp;_adf.ctrl-state=qrj7ek7cs_4">Oracle Support Note 2748362.1</a></p>
</li>
</ul>
<p>You’ll of course need a valid Oracle Support account to see the later one.</p>
<p>Either of these guides should provide the steps required, but for consistency’s sake - and to address some of the nuances of doing this in an Oracle 23ai Database container - I’ll walk through the steps here as well.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Notice</strong>: Since we’re about to modify the contents of the database container, it is important to understand that these changes will not persist if we were to destroy this container and create a new one.</div>
</div>

<h2 id="heading-podman-terminal">Podman Terminal</h2>
<p>Since we’re running the database in a container, and we can’t SSH to that container, we will need to use Podman’s terminal. This terminal give us a basic shell where we can run standard commands from. It’s a little feature-starved, but it will work for what we need.</p>
<p>To access Prodman’s terminal:</p>
<ol>
<li><p>Open up <strong>Podman</strong>.</p>
</li>
<li><p>Select the <strong>Containers</strong> icon (2nd from the top).</p>
</li>
<li><p>Click on the three dots and select <strong>Open Terminal</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733953553996/c995c9f9-d8a7-42b3-9d65-ca50716a8646.png" alt class="image--center mx-auto" /></p>
<p>From here, we’ll kick off the database installation.</p>
<h2 id="heading-database-installation">Database Installation</h2>
<p>First, let’s create a new directory.</p>
<ol>
<li>In the <strong>Terminal</strong>, enter and run the following command:</li>
</ol>
<pre><code class="lang-plaintext">mkdir -p /home/oracle/dbc/
</code></pre>
<p>Next, we will create a script that when run, will install <code>DBMS_CLOUD</code>.</p>
<ol start="2">
<li>To create the script, run the following command:</li>
</ol>
<pre><code class="lang-plaintext">vi /home/oracle/dbc/dbms_cloud_install.sql
</code></pre>
<p>If you’re not familiar with the <code>vi</code> editor, shame on you. Seriously. If that is the case, there’s tons of other resources that will familiarize you with basic commands. I’ll outline precisely what you need to do here, just in case.</p>
<ol start="3">
<li><p>Once the previous command is run, hit the <code>I</code> key to place vi into INSERT mode. You should see <code>—- INSERT -—</code> along the bottom of the window.</p>
</li>
<li><p>Copy the following script:</p>
</li>
</ol>
<pre><code class="lang-sql">@$ORACLE_HOME/rdbms/admin/sqlsessstart.sql

<span class="hljs-keyword">set</span> <span class="hljs-keyword">verify</span> <span class="hljs-keyword">off</span>
<span class="hljs-comment">-- you must not change the owner of the functionality to avoid future issues</span>
<span class="hljs-keyword">define</span> username=<span class="hljs-string">'C##CLOUD$SERVICE'</span>

<span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> &amp;username <span class="hljs-keyword">no</span> <span class="hljs-keyword">authentication</span> <span class="hljs-keyword">account</span> <span class="hljs-keyword">lock</span>;

REM <span class="hljs-keyword">Grant</span> Common <span class="hljs-keyword">User</span> <span class="hljs-keyword">Privileges</span>
<span class="hljs-keyword">grant</span> INHERIT <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">user</span> &amp;username <span class="hljs-keyword">to</span> <span class="hljs-keyword">sys</span>;
<span class="hljs-keyword">grant</span> INHERIT <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">user</span> <span class="hljs-keyword">sys</span> <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">RESOURCE</span>, <span class="hljs-keyword">UNLIMITED</span> <span class="hljs-keyword">TABLESPACE</span>, SELECT_CATALOG_ROLE <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">ANY</span> <span class="hljs-keyword">TABLE</span>, <span class="hljs-keyword">DROP</span> <span class="hljs-keyword">ANY</span> <span class="hljs-keyword">TABLE</span>, <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">ANY</span> <span class="hljs-keyword">TABLE</span>, <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">ANY</span> <span class="hljs-keyword">TABLE</span>,
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">ANY</span> CREDENTIAL, <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">PUBLIC</span> <span class="hljs-keyword">SYNONYM</span>, <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">PROCEDURE</span>, <span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">SESSION</span>, <span class="hljs-keyword">CREATE</span> JOB <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SESSION</span>, <span class="hljs-keyword">SET</span> <span class="hljs-keyword">CONTAINER</span> <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.V_$MYSTAT <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.SERVICE$ <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.V_$ENCRYPTION_WALLET <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">read</span>, write <span class="hljs-keyword">on</span> <span class="hljs-keyword">directory</span> DATA_PUMP_DIR <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_PRIV_CAPTURE <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_PDB_LIB <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_CRYPTO <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_SYS_ERROR <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> SYS.DBMS_ISCHED <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> SYS.DBMS_PDB_LIB <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_PDB <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_SERVICE <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_PDB <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.CONFIGURE_DV <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_SYS_ERROR <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_CREDENTIAL <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_RANDOM <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_SYS_SQL <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_LOCK <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_AQADM <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_AQ <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.DBMS_SYSTEM <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">on</span> SYS.SCHED$_LOG_ON_ERRORS_CLASS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_DATA_FILES <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_EXTENTS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_CREDENTIALS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.AUDIT_UNIFIED_ENABLED_POLICIES <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_ROLES <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.V_$ENCRYPTION_KEYS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_DIRECTORIES <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_USERS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_OBJECTS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.V_$PDBS <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.V_$<span class="hljs-keyword">SESSION</span> <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.GV_$<span class="hljs-keyword">SESSION</span> <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_REGISTRY <span class="hljs-keyword">to</span> &amp;username;
<span class="hljs-keyword">grant</span> <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">on</span> SYS.DBA_DV_STATUS <span class="hljs-keyword">to</span> &amp;username;

<span class="hljs-keyword">alter</span> <span class="hljs-keyword">session</span> <span class="hljs-keyword">set</span> current_schema=&amp;username;
REM <span class="hljs-keyword">Create</span> the <span class="hljs-keyword">Catalog</span> objects
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud_task_catalog.sql
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud_task_views.sql
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud_catalog.sql
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud_types.sql

<span class="hljs-keyword">REM</span> <span class="hljs-keyword">Create</span> the <span class="hljs-keyword">Package</span> Spec
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_core.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_task.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud_capability.sql
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_request.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_internal.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud.sql
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_admin_int.plb

<span class="hljs-keyword">REM</span> <span class="hljs-keyword">Create</span> the <span class="hljs-keyword">Package</span> <span class="hljs-keyword">Body</span>
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_core_body.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_task_body.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_capability_body.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_request_body.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_internal_body.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_body.plb
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/prvt_cloud_admin_int_body.plb

<span class="hljs-comment">-- Create the metadata</span>
@$ORACLE_HOME/rdbms/<span class="hljs-keyword">admin</span>/dbms_cloud_metadata.sql

<span class="hljs-keyword">alter</span> <span class="hljs-keyword">session</span> <span class="hljs-keyword">set</span> current_schema=<span class="hljs-keyword">sys</span>;

@$ORACLE_HOME/rdbms/admin/sqlsessend.sql
</code></pre>
<ol start="5">
<li><p>Select the Podman Terminal window.</p>
</li>
<li><p>Paste in the script with <code>CTRL-V</code> (PC) or <code>Command-V</code> (Mac).</p>
</li>
<li><p>Hit the <code>Esc</code> key.</p>
</li>
<li><p>Type the following: <code>:wq</code> and hit return.</p>
</li>
</ol>
<p>Congrats, you’re a <code>vi</code> expert now!</p>
<p>Next, we’re going to run the script via <code>catcon.pl</code>. This perl script will install <code>DBMS_CLOUD</code> into all PDBs as well as the CDB. We’ll pass our freshly minted script as a parameter and run it as <code>SYS</code>.</p>
<ol start="9">
<li>Copy the following code, changing <code>[SYS Password]</code> to the actual SYS password in the CDB before running.</li>
</ol>
<pre><code class="lang-plaintext">$ORACLE_HOME/perl/bin/perl $ORACLE_HOME/rdbms/admin/catcon.pl \
  -u sys/[SYS Password] \
  --force_pdb_mode 'READ WRITE' \
  -b dbms_cloud_install \
  -d /home/oracle/dbc \
  -l /home/oracle/dbc \
  dbms_cloud_install.sql
</code></pre>
<p>This script will run for a few seconds. If successful, you should see something similar to the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733970516490/0effe862-7e45-46a7-9a49-dd75ce8908ba.png" alt class="image--center mx-auto" /></p>
<p>Take a moment to browse the log file to see if there’s anything amiss.</p>
<p>Even better - simply connect to the CDB and check to see if <code>DBMS_CLOUD</code> is there:</p>
<ol start="10">
<li><p>From the Podman terminal, type the following: <code>sqlplus / as sysdba</code></p>
</li>
<li><p>Once connected, enter and run the following: <code>desc dbms_cloud</code></p>
</li>
</ol>
<p>You should see all of the procedures and functions associated with <code>DBMS_CLOUD</code>.</p>
<p>Let’s quickly check our PDB:</p>
<ol start="12">
<li><p>Enter and run the following command: <code>alter session set container = freepdb1;</code></p>
</li>
<li><p>Enter and run the following: <code>desc dbms_cloud</code></p>
</li>
</ol>
<p>Once again, you should see all of the procedures and functions associated with <code>DBMS_CLOUD</code>.</p>
<h1 id="heading-wallet-installation-amp-configuration">Wallet Installation &amp; Configuration</h1>
<p>In Oracle Database 23ai, there’s no need to install and configure a wallet, since all of the root CA certificates from the operating system can be shared. See <a target="_blank" href="https://connor-mcdonald.com/2024/05/04/23ai-leave-your-wallet-at-home/">this post</a> by Connor McDonald for details.</p>
<h1 id="heading-adding-acl-entries">Adding ACL Entries</h1>
<p>Now that we have <code>DBMS_CLOUD</code> installed, we need to create another entry in the database’s ACL so that it can call outbound web services - such as object storage. I’ve trimmed the steps required to do this to assume that we’re running on the Oracle 23ai free container. The steps found on Tim’s blog and the Oracle Support Note are much more comprehensive, and I recommend that you take a look there if your needs fall outside the scope of the container.</p>
<p>Adding ACL entries needs to be done in the CDB this time, not our PDB, so we need to run the following commands from the Podman Terminal again.</p>
<ol>
<li><p>Navigate to the Podman <strong>Terminal</strong>.</p>
</li>
<li><p>Start <strong>SQL*Plus</strong> and connect as <code>SYS</code> by entering the following: <code>sqlplus / as sysdba</code></p>
</li>
<li><p>Enter and run the following:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
dbms_network_acl_admin.append_host_ace
  (
  host =&gt;<span class="hljs-string">'*'</span>,
  lower_port =&gt; <span class="hljs-number">443</span>,
  upper_port =&gt; <span class="hljs-number">443</span>,
  ace =&gt; xs$ace_type
    (
    privilege_list =&gt; xs$name_list(<span class="hljs-string">'http'</span>, <span class="hljs-string">'http_proxy'</span>),
    principal_name =&gt; <span class="hljs-keyword">upper</span>(<span class="hljs-string">'C##CLOUD$SERVICE'</span>),
    principal_type =&gt; xs_acl.ptype_db)
    );

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

<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>We can verify that this worked with the following formatting &amp; query. Since we’re stuck with SQL*Plus, we’ll have to throw some good ol’ <code>COL</code> commands to make the data fit.</p>
<ol start="4">
<li>Enter and run the following:</li>
</ol>
<pre><code class="lang-sql">col host for a10
col lower_port for a5
col upper_port for a5
col principal for a20
col principal_type for a20
col privilege for a20
<span class="hljs-keyword">set</span> lin <span class="hljs-number">200</span>

<span class="hljs-keyword">SELECT</span>  
   host
  ,lower_port <span class="hljs-keyword">as</span> <span class="hljs-keyword">lower</span>
  ,upper_port <span class="hljs-keyword">as</span> <span class="hljs-keyword">upper</span>
  ,principal
  ,principal_type
  ,privilege
<span class="hljs-keyword">FROM</span>   
  dba_host_aces
<span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> 
  host
 ,ace_order
/
</code></pre>
<p>The output should look similar to this:</p>
<pre><code class="lang-sql">HOST            LOWER      UPPER PRINCIPAL            PRINCIPAL_TYPE       PRIVILEGE
<span class="hljs-comment">---------- ---------- ---------- -------------------- -------------------- --------------------</span>
*                                GSMADMIN_INTERNAL    DATABASE             RESOLVE
*                 443        443 C<span class="hljs-comment">##CLOUD$SERVICE     DATABASE             HTTP_PROXY</span>
*                 443        443 C<span class="hljs-comment">##CLOUD$SERVICE     DATABASE             HTTP</span>
</code></pre>
<h1 id="heading-creating-oci-components">Creating OCI Components</h1>
<p>Remember - what we’ve just spent time creating is the plumbing. It does not give us any specific access into OCI, but rather facilitates the calls when we make them. We still need to present the correct credentials, which need to be mapped to a user, which needs to be mapped to a group, which needs to be mapped to a policy that grants access to the object storage buckets in a specific compartment.</p>
<h2 id="heading-create-a-user-auth-token-amp-group">Create a User, Auth Token &amp; Group</h2>
<ol>
<li><p>Login to the OCI console at <a target="_blank" href="https://cloud.oracle.com">https://cloud.oracle.com</a> with an administrator account.</p>
</li>
<li><p>Using the main menu, navigate to <strong>Identity &amp; Security</strong> &gt; <strong>Domains</strong>.</p>
</li>
<li><p>Click <strong>Default</strong> (<em>current domain</em>).</p>
</li>
<li><p>Click on the <strong>Users</strong> tab. You should see a list of users in your domain.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734100456377/20d1a238-9aec-4ccc-9b98-cab5e4ccea75.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li><p>Click <strong>Create User</strong>.</p>
</li>
<li><p>Fill out the form, entering a <strong>First Name</strong>, <strong>Last Name</strong> and <strong>Username/Email</strong>. Ensure that <strong>Use the email address as the username</strong> is checked and click <strong>Create</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734100728660/7ed3b1de-c9b8-4aed-922a-f159d990c528.png" alt class="image--center mx-auto" /></p>
<p>We can dial down this user’s capabilities a bit so that all they are allowed to do is use Auth Tokens.</p>
<ol start="7">
<li><p>Click the <strong>Edit user capabilities</strong> button.</p>
</li>
<li><p>Uncheck all options except <strong>Auth Token</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734100821941/b75c8fa2-0710-43b1-9c83-2d07e32d2fea.png" alt class="image--center mx-auto" /></p>
<ol start="9">
<li>Click <strong>Save changes</strong>.</li>
</ol>
<p>Next, let’s create the <strong>Auth Token</strong>.</p>
<ol start="10">
<li><p>Under the <strong>Resources</strong> section, click <strong>Auth tokens</strong>.</p>
</li>
<li><p>Click the <strong>Generate token</strong> button.</p>
</li>
<li><p>Enter <code>Object Storage</code> for the <strong>Description</strong> and click <strong>Generate Token</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734104131504/32b7c36d-0204-49d0-ba68-b2b506bcc9dd.png" alt class="image--center mx-auto" /></p>
<ol start="10">
<li>On the next screen, be sure to <strong>copy</strong> the generated token. This will be the only opportunity to do so.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734104123035/db304f8c-191d-448d-84d9-b225192de6f8.png" alt class="image--center mx-auto" /></p>
<ol start="11">
<li>Click <strong>Close</strong> to dismiss the modal.</li>
</ol>
<p>While we’re here, let’s create a group. This group will he mapped to a policy that we’ll create in the next section that will allow our user &amp; auth token to access object storage buckets.</p>
<ol start="12">
<li><p>In the breadcrumb, click <strong>Default Domain</strong>.</p>
</li>
<li><p>Click the tab for <strong>Groups</strong>.</p>
</li>
<li><p>Click <strong>Create Group</strong>.</p>
</li>
<li><p>Enter <code>Read Object Storage</code> as the Name and be sure to select the user that you created in the previous steps.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734123258373/a6623763-0db9-4035-bf45-8906df1338dd.png" alt class="image--center mx-auto" /></p>
<ol start="16">
<li>Click <strong>Create</strong>.</li>
</ol>
<h2 id="heading-create-a-policy">Create a Policy</h2>
<p>Now that we have a user and auth token, we will need to create a policy that permits access to the object storage buckets, map that policy to a group, and then add our user to that group. That will allow us to use <code>dbms_credential</code> to make calls to Object Storage using the auth token associated with our user.</p>
<p>The steps here are similar for what was done to get OCI Email Services integrated with our portable development environment.</p>
<ol>
<li><p>From the OCI Console, navigate to <strong>Identity &amp; Security</strong> &gt; <strong>Policies</strong>.</p>
</li>
<li><p>Click <strong>Create Policy</strong>.</p>
</li>
<li><p>Enter <code>ReadObjectStorage</code> for the <strong>Name</strong> and <code>Read Object Storage Policy</code> for the <strong>Description</strong>.</p>
</li>
<li><p>Since we used the Default Domain, set the compartment to the <strong>Root</strong> compartment. That’s the first one on the list and should end with <code>(root)</code>.</p>
</li>
<li><p>In the <strong>Policy Builder</strong>, set <strong>Policy Use Cases</strong> to <strong>Storage Management</strong>.</p>
</li>
<li><p>Set <strong>Common policy templates</strong> to <strong>Let users download objects from Object Storage buckets</strong>.</p>
</li>
<li><p>In the middle select list labeled <strong>Select a group</strong>, set it to <strong>Read Object Storage</strong>.</p>
</li>
<li><p>In <strong>Location</strong> select list, set it to any compartment and make note of that. We will need to create an Object Storage bucket in that compartment in the next section.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734184174341/5bf79742-7d4f-4b06-af76-297b0faf78d1.png" alt class="image--center mx-auto" /></p>
<ol start="9">
<li>Click <strong>Create</strong>.</li>
</ol>
<p>At this point, we have configured our OCI security model - the policy includes a specific set of resources and a group and the group includes the user. Thus, the user that we created can call <code>DBMS_CLOUD</code> and present their auth token to read any bucket in the compartment we specified.</p>
<h2 id="heading-create-a-bucket">Create a Bucket</h2>
<p>Before we can test <code>DBMS_CLOUD</code>, let’s make sure that we have at least one bucket in the compartment we referenced in the policy.</p>
<ol>
<li><p>From the OCI Console, navigate to <strong>Storage</strong> &gt; <strong>Buckets</strong>.</p>
</li>
<li><p>Set the <strong>Compartment</strong> to the one that you selected in the previous steps.</p>
</li>
<li><p>Click <strong>Create Bucket</strong>.</p>
</li>
<li><p>Enter <code>bucket_test</code> for the <strong>Name</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734184509998/b9c2d791-ef96-457a-85ee-54ce6f42763b.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li><p>Click <strong>Create</strong>.</p>
</li>
<li><p>Next, click the name of the bucket that was just created - <code>bucket_test</code> - to edit the bucket.</p>
</li>
</ol>
<p>Let’s upload a couple images or files to the bucket so that we can properly test it out. Anything will work.</p>
<ol start="5">
<li><p>In the <strong>Objects</strong> region, click <strong>Upload</strong>.</p>
</li>
<li><p>Drag the file you wish to upload to the region on the page and click <strong>Upload</strong>.</p>
</li>
<li><p>Once the file uploads, click the <strong>Close</strong> button.</p>
</li>
</ol>
<p>Repeat steps 5 through 7 a couple of times so that you have a few files to test with.</p>
<p>Before we leave the Object Storage page, we need one more thing - the URL that we will use to access the button. The easiest way to get this it to edit one of the file and copy it from there.</p>
<ol start="8">
<li>In the <strong>Objects</strong> region, click the three dots next to any of your files and select <strong>View Object Details</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734185156335/d72055e0-b642-4fd5-9e8a-626791153764.png" alt class="image--center mx-auto" /></p>
<ol start="9">
<li><p>Copy the value of the <strong>URL Path (URI)</strong>, as we will need that in the next section.</p>
</li>
<li><p>Click <strong>Cancel</strong> to dismiss the modal window.</p>
</li>
</ol>
<h2 id="heading-create-a-dbmscredential">Create a DBMS_CREDENTIAL</h2>
<p>We’re almost there! The last thing we need to do before testing is create a <code>DBMS_CREDENTIAL</code>. This <code>DBMS_CREDENTIAL</code> will allow us to store our username and auth token in a secure place in the database. When we do call <code>DBMS_CLOUD</code>, we’ll pass in the <code>DBMS_CREDENTIAL</code> that we create here, so that <code>DBMS_CLOUD</code> can properly authenticate when making its calls.</p>
<p>The <code>DBMS_CREDENTIAL</code> should be created in the same schema that your APEX application will parse as. Thus, in our case, we’ll stick with the <code>DEMO</code> schema that we created a while back.</p>
<ol>
<li><p>Open a new terminal window.</p>
</li>
<li><p>Connect to the database as the <code>DEMO</code> user, using the following command, replacing <code>[demo password]</code> with the password of the <code>DEMO</code> schema:</p>
</li>
</ol>
<pre><code class="lang-plaintext">sql demo/[demo password]@localhost:1521/freepdb1
</code></pre>
<ol start="3">
<li>Once, connected, run the following command, replacing <code>[email address]</code> with the <strong>Username</strong> that you created previously and <code>[auth token]</code> with the value of the <strong>Auth Token</strong> that you copied earlier.</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
  dbms_credential.create_credential
    (
     credential_name =&gt; <span class="hljs-string">'obj_store_cred'</span>
    ,username        =&gt; <span class="hljs-string">'[email address]'</span>
    ,<span class="hljs-keyword">password</span>        =&gt; <span class="hljs-string">'[auth token]'</span>
  );
<span class="hljs-keyword">end</span>;
/
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: If you forgot the value of the <strong>Auth Token</strong>, you’ll have to go back and create a new one. There is no way to get that value once you dismiss the screen when it’s created.</div>
</div>

<h2 id="heading-test-dbmscloud">Test DBMS_CLOUD</h2>
<p>And now, the final step.</p>
<ol>
<li><p>Enter and run the following command, replacing <code>[object storage url]</code> with the value of the URL you copied in the last section without the filename. It should look something similar to this:</p>
<p> <code>https://objectstorage.us-ashburn-1.oraclecloud.com/n/fjaltjsfsdxw/b/bucket_test/o/</code></p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> 
  object_name
 ,<span class="hljs-keyword">bytes</span>
<span class="hljs-keyword">from</span>   
  dbms_cloud.list_objects
    (
     <span class="hljs-string">'obj_store_cred'</span>
     ,<span class="hljs-string">'[object storage url]'</span>
    )
/
</code></pre>
<p>If everything was done correctly, you should see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734186543421/a757fed7-e8b0-4478-b9c4-aaf7ab1bcaee.png" alt class="image--center mx-auto" /></p>
<p>The same SQL can also be used in APEX - simply copy it to a report region and run the page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734186675093/33dcbb56-bd0e-4fb8-9349-a291272dd4e2.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-summary">Summary</h1>
<p>Adding <code>DBMS_CLOUD</code> to your containerized development environment adds yet another capability that you can use. With it, you can easily and securely interact with files stored in object storage buckets. This post only covered adding the ability to read files from a bucket. It’s entirely possible to expand on the scope and build APEX applications that provide the ability to upload and even manage buckets themselves.</p>
<p>Keep in mind that you made changes to the database configuration within the container. Thus, it’s different that the image that you downloaded, and if you create a new container from that image, none of these changes will be there. You would have to re-install <code>DBMS_CLOUD</code> to get back to where we are now. Not a huge deal, but an extra step nonetheless.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@anikeevxo"><em>Vladimir Anikeev</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Adding a Local LLM to the Oracle Container]]></title><description><![CDATA[Unless you’ve been living under a rock, you’ve likely heard about AI & LLMs more than once.
LLM stands for Large Language Model. It's a type of artificial intelligence (AI) model that's designed to process and understand human language at a massive s...]]></description><link>https://spendolini.blog/adding-a-local-llm-to-the-oracle-container</link><guid isPermaLink="true">https://spendolini.blog/adding-a-local-llm-to-the-oracle-container</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[podman]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[AI]]></category><category><![CDATA[ollama]]></category><category><![CDATA[Llama3]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Thu, 05 Dec 2024 12:43:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494158096/98f0dfca-2e94-4d12-862e-c6a979011183.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Unless you’ve been living under a rock, you’ve likely heard about AI &amp; LLMs more than once.</p>
<p>LLM stands for Large Language Model. It's a type of artificial intelligence (AI) model that's designed to process and understand human language at a massive scale. Large Language Models are trained on vast amounts of text data, which enables them to learn patterns, relationships, and structures within language. This training allows the models to generate human-like text, respond to questions, and even engage in conversations.</p>
<p>And to be fully transparent, an LLM (Llama 3.2) wrote that last paragraph. In fact, AI generates the SEO summaries for all of my blog posts, as it’s a feature of the Hashnode blogging platform. Ya, it’s everywhere.</p>
<p>So why not integrate it with our local development platform?</p>
<p>In this post, we’ll walk through the steps to download &amp; install an LLM - Llama 3.2 - to our local machine, fire it up and integrate it with APEX in our Oracle 23ai container.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: The steps in this post are based on installing Ollama on MacOS. Other operating systems will vary slightly.</div>
</div>

<h1 id="heading-installing-ollama-amp-llm">Installing Ollama &amp; LLM</h1>
<p>The first thing that we need to do is download a program called Ollama. Simply put, Ollama is an application that enables users to install and run LLMs on their local machines. It obfuscates all of the hard parts, and in fact, the next few steps will only take a few minutes to complete.</p>
<h2 id="heading-download-amp-install-ollama">Download &amp; Install Ollama</h2>
<p>To download Ollama, follow the steps below:</p>
<ol>
<li><p>Navigate <a target="_blank" href="https://ollama.com/">https://ollama.com/</a></p>
</li>
<li><p>Click <strong>Download</strong>.</p>
</li>
<li><p>Select your operating system and the download will start.</p>
</li>
<li><p>Once it’s downloaded, unzip the file and drag the Ollama icon to your main Applications folder.</p>
</li>
<li><p>Double-click the Ollama icon to start the installation. You should see the following window:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733367936720/5a94d718-b1bf-4dca-a9b1-ff1db457461d.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li><p>Click <strong>Next</strong>.</p>
</li>
<li><p>Ollama will prompt to install the command portion. You will need to enter an Administrator password to pass this step after you click <strong>Install</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733367996057/debd7bd7-9b87-427e-a9f4-2c3ff5ca5b2c.png" alt class="image--center mx-auto" /></p>
<ol start="8">
<li>Next, copy the command <code>ollama run llama3.2</code> from the window by clicking on the copy icon.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733368018820/6619aedf-10b1-430c-871e-ed0d98495b7f.png" alt class="image--center mx-auto" /></p>
<ol start="9">
<li><p>Click <strong>Finish</strong> to dismiss the window.</p>
</li>
<li><p>Open a new terminal window.</p>
</li>
<li><p>Paste in the command - <code>ollama run llama3.2</code> - to install the <code>llama3.2</code> model and hit return. The model will begin to download. Depending on your network speed, this may take a few minutes, as the size of the download is roughly 2GB.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733368085608/82a2903e-9c02-4b81-9383-a0b6b3409948.png" alt class="image--center mx-auto" /></p>
<ol start="12">
<li>From the <code>&gt;&gt;&gt;</code> prompt, enter any question that you want to ask the model. For example: <code>what does llm stand for?</code></li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733368253966/d16534db-8379-4981-91bb-b1c71795825f.png" alt class="image--center mx-auto" /></p>
<p>Ollama is now up and running with the Llama 3.2 LLM. This is the same exact model that you may have interacted with online, but it’s running locally on your machine and is completely free!</p>
<p>Feel free to play around. When you want to exit the interactive mode, type <code>/bye</code> and hit return.</p>
<h2 id="heading-managing-ollama">Managing Ollama</h2>
<p>Since we “ran” a model, Ollama will check to see if it has the latest model, pull it down if not, and then run the model. You can check to see if it’s running with the following command: <code>ollama ps</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733368787314/d7c11421-bbbc-40fd-bb0d-bd4b867bf706.png" alt class="image--center mx-auto" /></p>
<p>If we wanted to stop this model, we can use the following command:<br /><code>ollama stop llama3.2</code></p>
<p>This will stop the model, but Ollama will technically still be running. To completely quit Ollama, select the Ollama icon in the menubar and select <strong>Quit Ollama</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733368899123/eff309d2-2a70-4740-bd73-af09c740f896.png" alt class="image--center mx-auto" /></p>
<p>This will stop all models, as well as any of the Ollama background processes.</p>
<h2 id="heading-ollama-as-a-service">Ollama as a Service</h2>
<p>While this is all well and good, it’s really not much different from interacting with a LLM via a web site. In fact, if we wanted to go that direction, we could install <a target="_blank" href="https://github.com/open-webui/open-webui">Open WebUI</a> and get a very familiar looking interface.</p>
<p>But that’s not what we’re after. We want to be able to use this LLM in APEX to drive the APEX Assistant as well as the APEX_AI API. To do that, it may make more sense to run the LLM as a service.</p>
<ol>
<li>Open a new terminal window and run the following command: <code>ollama serve</code></li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733371083672/a3ed2592-d352-44ea-988e-320d030585ed.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li>Open another new terminal window and copy &amp; paste the following command:</li>
</ol>
<pre><code class="lang-bash">curl --request POST \
  --url http://localhost:11434/api/chat \
  --header <span class="hljs-string">'content-type: application/json'</span> \
  --data <span class="hljs-string">'{
  "model": "llama3.2",
  "messages": [
    { "role": "user", "content": "who was the first president?" }
  ],
  "stream": false
}'</span>
</code></pre>
<p>The results should look similar to the following:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"model"</span>: <span class="hljs-string">"llama3.2"</span>,
  <span class="hljs-attr">"created_at"</span>: <span class="hljs-string">"2024-12-05T04:03:00.706654Z"</span>,
  <span class="hljs-attr">"message"</span>: {
    <span class="hljs-attr">"role"</span>: <span class="hljs-string">"assistant"</span>,
    <span class="hljs-attr">"content"</span>: <span class="hljs-string">"The first President of the United States was 
                George Washington. He served two terms in office 
                from April 30, 1789, to March 4, 1797."</span>
  },
  <span class="hljs-attr">"done_reason"</span>: <span class="hljs-string">"stop"</span>,
  <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"total_duration"</span>: <span class="hljs-number">1633274834</span>,
  <span class="hljs-attr">"load_duration"</span>: <span class="hljs-number">18144875</span>,
  <span class="hljs-attr">"prompt_eval_count"</span>: <span class="hljs-number">31</span>,
  <span class="hljs-attr">"prompt_eval_duration"</span>: <span class="hljs-number">667000000</span>,
  <span class="hljs-attr">"eval_count"</span>: <span class="hljs-number">36</span>,
  <span class="hljs-attr">"eval_duration"</span>: <span class="hljs-number">946000000</span>
}
</code></pre>
<p>Note that the <code>content</code> line contains the answer from the LLM. Thus, the Ollama service is up and running.</p>
<p>Given that we want to reach out to this service from a Podman container, we can’t use <code>localhost</code>, as that would translate to the container itself. Thus, we need to refer to the service on the IP address of the local machine - i.e. your local IP address.</p>
<p>To find out your local IP on Mac:</p>
<ol start="3">
<li><p>Open <strong>System Settings</strong> &gt; <strong>WiFi</strong> &gt; <strong>Details</strong>.</p>
</li>
<li><p>From there, either scroll down or select <strong>TCP/IP</strong> to see the <strong>IP address</strong> of your local machine. Note this address, as we will need it in the next step.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733371674483/6642f0d0-50e1-4f21-baac-fe4074d40321.png" alt class="image--center mx-auto" /></p>
<ol start="5">
<li>Next, copy the below command, replacing <code>[your IP address]</code> with you local machine’s IP address and run it:</li>
</ol>
<pre><code class="lang-bash">curl --request POST \
  --url http://[your IP address]:11434/api/chat \
  --header <span class="hljs-string">'content-type: application/json'</span> \
  --data <span class="hljs-string">'{
  "model": "llama3.2",
  "messages": [
    { "role": "user", "content": "who was the first president?" }
  ],
  "stream": false
}'</span>
</code></pre>
<p>You should get the same or very similar result as you did before.</p>
<p>If all is well, then we’re ready to hook up the LLM to APEX.</p>
<h3 id="heading-troubleshooting">Troubleshooting</h3>
<p>In some cases, you may need to run the following command on the Mac, and then stop &amp; start Ollama:</p>
<pre><code class="lang-bash">launchctl setenv OLLAMA_HOST <span class="hljs-string">"0.0.0.0"</span>
</code></pre>
<p>This essentially “unbinds” the Ollama service from <code>127.0.0.1</code>, allowing you to use your local IP address to access it.</p>
<h1 id="heading-configuring-apex">Configuring APEX</h1>
<p>Integrating Ollama with APEX is done pretty much the same way as a hosted LLM, with a couple of small differences. Let’s walk through the steps.</p>
<h2 id="heading-creating-an-ace-entry">Creating an ACE Entry</h2>
<p>First things first, we need to create an additional ACE entry to allow the database to talk to the Ollama service.</p>
<ol>
<li><p>Open a new terminal window.</p>
</li>
<li><p>Connect to the database via SQLcl as the <strong>SYS</strong> user.</p>
</li>
<li><p>Ensure that you’re connected to the correct PDB. In this case, it should be <code>freepdb1</code>.</p>
</li>
<li><p>Copy the below command, replacing <code>[your IP address]</code> with you local machine’s IP address and run it:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
  dbms_network_acl_admin.append_host_ace (
    host       =&gt; <span class="hljs-string">'[your IP address]'</span>,
    lower_port =&gt; <span class="hljs-number">11434</span>,
    upper_port =&gt; <span class="hljs-number">11434</span>,
    ace        =&gt; xs$ace_type(privilege_list =&gt; xs$name_list(<span class="hljs-string">'connect'</span>),
                              principal_name =&gt; <span class="hljs-string">'APEX_240100'</span>,
                              principal_type =&gt; xs_acl.ptype_db));
<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>If the IP address of your local machine changes, you will need to create another ACE entry for each new IP address. Thus, it may be easier to use <code>*</code> as the hostname, as that will apply to any and all IP address and/or hostname.</p>
<p>Again, this idea is fine for a local development server, but not for production.</p>
<p>We can quickly test this via SQL Workshop:</p>
<ol start="5">
<li><p>Login to any APEX workspace (except INTERNAL).</p>
</li>
<li><p>Click on <strong>SQL Workshop</strong>.</p>
</li>
<li><p>Click on <strong>SQL Commands</strong>.</p>
</li>
<li><p>Copy the below command, replacing <code>[your IP address]</code> with you local machine’s IP address and run it:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">declare</span>
  l_clob <span class="hljs-keyword">clob</span>;
<span class="hljs-keyword">begin</span>

l_clob := apex_web_service.make_rest_request
  (
    p_url         =&gt; <span class="hljs-string">'http://[your IP address]:11434/api/chat'</span>
   ,p_http_method =&gt; <span class="hljs-string">'POST'</span>
   ,p_body        =&gt; <span class="hljs-string">'{ "model": "llama3.2", "messages": [ { "role": "user", "content": "who was the first president" }], "stream": false}'</span>
  );

htp.prn(l_clob);

<span class="hljs-keyword">end</span>;
/
</code></pre>
<p>If all is working, you should see something similar to the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733372729177/93805a00-8c1b-45ae-864e-7df6e884fc94.png" alt class="image--center mx-auto" /></p>
<p>Again, the results may differ slightly, but as long as there’s a JSON document that looks similar to the one above, you’re good to go.</p>
<h3 id="heading-troubleshooting-1">Troubleshooting</h3>
<p>If there’s an error here, it almost always has to do with the ACE being improperly configured or the IP address being wrong. Verify that both of these components are correct and try again.</p>
<h2 id="heading-integrating-with-app-builder">Integrating with App Builder</h2>
<p>Integrating our model with App Builder is done pretty much the same way as you would any other hosted LLM. The only real difference is that we’re using our local machine’s IP and the value of the API Key really doesn’t matter, but can’t be null.</p>
<p>Let’s get this thing integrated!</p>
<h3 id="heading-workspace-integration">Workspace Integration</h3>
<p>Let’s start by adding our LLM to the workspace.</p>
<ol>
<li><p>Click the <strong>App Builder</strong> icon.</p>
</li>
<li><p>Click the <strong>Workspace Utilities</strong> icon.</p>
</li>
<li><p>Click <strong>Generative AI</strong>.</p>
</li>
<li><p>Click <strong>Create</strong>.</p>
</li>
<li><p>Set or enter the following values, replacing <code>[your IP address]</code> with you local machine’s IP address:</p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Attribue</strong></td><td><strong>Value</strong></td></tr>
</thead>
<tbody>
<tr>
<td>AI Provider</td><td><code>Open AI</code></td></tr>
<tr>
<td>Name</td><td><code>Llama 3.2</code></td></tr>
<tr>
<td>Static ID</td><td><code>llama_32</code></td></tr>
<tr>
<td>Used by App Builder</td><td>[selected]</td></tr>
<tr>
<td>Base URL</td><td><code>http://[your IP address]:11434/v1</code></td></tr>
<tr>
<td>Credential</td><td><code>- Create New -</code></td></tr>
<tr>
<td>API Key</td><td><code>xxxxxxxxx</code></td></tr>
<tr>
<td>AI Model</td><td><code>llama3.2:latest</code></td></tr>
</tbody>
</table>
</div><p>Note: The value of API Key can be anything, as it’s not needed when using local LLMs, but cannot be null.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733373469287/2ccffaa4-6f1d-4e8d-a43f-6c6144a2a2a3.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li>Click <strong>Create</strong>.</li>
</ol>
<p>Let’s ensure that it’s working.</p>
<ol start="7">
<li><p>Navigate to <strong>SQL Workshop</strong> &gt; <strong>SQL Commands</strong>.</p>
</li>
<li><p>Click on the <strong>APEX Assistant</strong> button.</p>
</li>
<li><p>Since we may not have any tables in our schema, simply type <code>hello</code> and hit enter. You should see a similar response:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733374160555/b8ab1993-b399-4859-8e6b-e1b228672808.png" alt class="image--center mx-auto" /></p>
<p>While this is less than helpful, it does prove that APEX is able to talk to Ollama.</p>
<h3 id="heading-application-integration-chat">Application Integration - Chat</h3>
<p>Rather than re-invent the wheel, there’s a decent amount of content on the internet about how to build applications with APEX &amp; AI - using chat, generation and the APEX_AI API. Now that we have our local LLM integrated, there’s nothing different that needs to happen when building APEX application that interact with AI - either via a dynamic action or API call.</p>
<p>Here’s a few links to get started:</p>
<ul>
<li><p><a target="_blank" href="https://docs.oracle.com/en/database/oracle/apex/24.1/htmdb/including-generative-ai-in-applications.html#GUID-1A53C3E5-658D-48D8-9C48-E9163B5D094F">Including Generative AI in Applications</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=9BLwdO6uYL4">Build AI Powered Apps with Oracle APEX</a></p>
</li>
<li><p><a target="_blank" href="https://blog.apexapplab.dev/how-the-new-apex-ai-features-work">How the new APEX AI features work</a></p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>AI offers an unparalleled set of features that can be used as a platform to build the next generation of application on. APEX provides an ideal place to start to quickly hash out, prototype and build these solutions. The ability to pack the power of AI onto your local machine can accelerate development and save cost at the same time.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@chris23?utm_source=Hashnode&amp;utm_medium=referral"><em>Chris</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Using Database Actions on the Oracle Container]]></title><description><![CDATA[The ORDS container that we are using not only exposes APEX, but it also provides a robust set of database-centric tools known as Database Actions (formerly SQL Developer Web). You can read a lot more about the specifics of Database Actions here or he...]]></description><link>https://spendolini.blog/using-database-actions-on-the-oracle-container</link><guid isPermaLink="true">https://spendolini.blog/using-database-actions-on-the-oracle-container</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[podman]]></category><category><![CDATA[containers]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 03 Dec 2024 16:42:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494300592/65434a24-7b7a-45db-bd40-03a91292ff1d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The ORDS container that we are using not only exposes APEX, but it also provides a robust set of database-centric tools known as Database Actions (formerly SQL Developer Web). You can read a lot more about the specifics of Database Actions <a target="_blank" href="https://www.thatjeffsmith.com/sql-developer-web/">here</a> or <a target="_blank" href="https://docs.oracle.com/en/database/oracle/sql-developer-web/index.html">here</a>.</p>
<p>This post won’t dive into what’s possible with Database Actions, but rather walk through how to enable them on our local containerized instance of Oracle 23ai.</p>
<h1 id="heading-enabling-database-actions">Enabling Database Actions</h1>
<p>There’s really only one thing to do before we can use Database Actions. It involves REST-enabling any schema that we want to use to connect to it. This can be done via command line or via APEX.</p>
<p>Let’s walk through both scenarios.</p>
<h2 id="heading-rest-enabling-a-schema-via-sqlcl">REST-Enabling a Schema via SQLcl</h2>
<p>It takes only a couple commands to REST-enable any schema in the database. But before we do that, let’s be sure we have a clean schema to walk through the steps with.</p>
<p>Let’s create a schema called HR.</p>
<ol>
<li><p>Open a new terminal window.</p>
</li>
<li><p>Connect to the <code>freepdb1</code> container as <strong>system</strong> with the following command:</p>
</li>
</ol>
<pre><code class="lang-sql">sql system/oracle@localhost:1521/freepdb1
</code></pre>
<ol start="3">
<li>Run the following commands to create the new HR schema:</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> <span class="hljs-keyword">user</span> hr <span class="hljs-keyword">identified</span> <span class="hljs-keyword">by</span> <span class="hljs-keyword">oracle</span> <span class="hljs-keyword">quota</span> <span class="hljs-keyword">unlimited</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">users</span>
/

<span class="hljs-keyword">grant</span> <span class="hljs-keyword">connect</span>, <span class="hljs-keyword">resource</span> <span class="hljs-keyword">to</span> hr
/
</code></pre>
<p>Now that we have a new schema, let’s try to connect to Database Actions.</p>
<ol start="4">
<li><p>Open a new browser and navigate to the following URL:<br /> <code>https://localhost/ords</code></p>
</li>
<li><p>Click the <strong>Go</strong> button under the card labeled <strong>SQL Developer Web</strong>.</p>
</li>
<li><p>Enter <code>HR</code> for the <strong>Username</strong> and <code>oracle</code> for the <strong>Password</strong> and click <strong>Sign In</strong>. You’ll see something like this:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733236926000/38ba4a86-9399-47a3-bb66-b37218fefe89.png" alt class="image--center mx-auto" /></p>
<p>This is due to the fact that the HR schema has not yet been REST-enabled, which is a requirement for using Database Actions. Thus, let’s get it enabled.</p>
<p>The first step is to allow <strong>SYSTEM</strong> to be able to call the ORDS schema objects. We need to issue a grant for that to happen.</p>
<ol start="7">
<li>From the same terminal window, enter and run the following:</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">grant</span> inherit <span class="hljs-keyword">privileges</span> <span class="hljs-keyword">on</span> <span class="hljs-keyword">user</span> <span class="hljs-keyword">SYSTEM</span> <span class="hljs-keyword">to</span> ORDS_METADATA
/
</code></pre>
<p>The next and final step is to actually issue the <code>ords.enable_schema</code> API call.</p>
<ol start="8">
<li>Next, enter and run the following:</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">BEGIN</span>
ORDS.ENABLE_SCHEMA
  (
   p_enabled =&gt; <span class="hljs-literal">TRUE</span>
  ,p_schema =&gt; <span class="hljs-string">'HR'</span>
  ,p_url_mapping_type =&gt; <span class="hljs-string">'BASE_PATH'</span>
  ,p_url_mapping_pattern =&gt; <span class="hljs-string">'hr'</span>
  ,p_auto_rest_auth =&gt; <span class="hljs-literal">FALSE</span>
  );
<span class="hljs-keyword">commit</span>;
<span class="hljs-keyword">END</span>;
/
</code></pre>
<p>One thing to note: in a production environment, it’s better to obfuscate the schema name in the <code>p_url_mappting_pattern</code> so as not to give away the name of the schema to a would-be hacker.</p>
<p>All that’s left now is to try to login to Database Actions again.</p>
<ol start="9">
<li><p>Switch back to the browser that was just used to try to login to Database Actions.</p>
</li>
<li><p>Enter <code>HR</code> for the <strong>Username</strong> and <code>oracle</code> for the <strong>Password</strong> and click <strong>Sign In</strong>. Now, you’ll see something like this:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733240720922/e08f55a7-8dbd-436e-b370-de54eedd4343.png" alt class="image--center mx-auto" /></p>
<p>You can now login to Database Actions as the <code>HR</code> user, and perform any tasks that schema has access to do.</p>
<h2 id="heading-rest-enabling-a-schema-via-apex">REST-Enabling a Schema via APEX</h2>
<p>If you already have an APEX workspace created, you can easily REST-enable your schema from APEX in just seconds. Here’s how:</p>
<ol>
<li><p>Open a new browser and login to your workspace.</p>
</li>
<li><p>Click the <strong>SQL Workshop</strong> icon.</p>
</li>
<li><p>Click the <strong>RESTful Services</strong> icon.</p>
</li>
<li><p>Click <strong>Register Schema with ORDS</strong>.</p>
</li>
<li><p>On the popup region, de-select <strong>Install Sample Service</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733241350789/0d5585fb-5d6e-4094-9982-dacad6b9ab8f.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li>Click <strong>Save Schema Attributes</strong>.</li>
</ol>
<p>That’s it! To confirm it worked, simply login to Database Actions with the schema username and password that’s mapped to the workspace.</p>
<p>Same warning about the <strong>Schema Alias</strong> applies here - using the schema name is fine for testing, but a different name should be used for production in order to better obfuscate the actual schema name.</p>
<h2 id="heading-integrating-database-actions-with-apex">Integrating Database Actions with APEX</h2>
<p>There’s one more integration that we can add to our environment - integrating Database Actions with APEX. This will add a link to Database Actions to the SQL Workshop tab in APEX, making it easy to jump from APEX to Database Actions without having to enter any credentials.</p>
<p>Note: On the APEX side, the older name “S<strong>QL Developer Web</strong>” is still referenced.</p>
<p>Let’s set that up.</p>
<ol>
<li><p>Login to the <code>INTERNAL</code> workspace of APEX using the <code>ADMIN</code> account.</p>
</li>
<li><p>Click on the <strong>Manage Instance</strong> icon.</p>
</li>
<li><p>Click <strong>Feature Configuration</strong>.</p>
</li>
<li><p>In the <strong>SQL Workshop</strong> section, set <strong>Enable SQL Developer Web</strong> to <code>Yes</code>.</p>
</li>
<li><p>Click <strong>Apply Changes</strong>.</p>
</li>
</ol>
<p>That’s all there is to it. To test, simply login to your workspace and from the <strong>SQL Workshop</strong> tab, select <strong>SQL Developer Web</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733242033360/8dd2b1c1-8a77-471e-968d-5e04edf2a44b.png" alt class="image--center mx-auto" /></p>
<p>There’s a near 100% chance that you’ll need to allow popup windows for localhost. Look for a small icon in the address bar that looks like this and click on it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733242115391/13cbd80e-d6f8-4cc7-a49a-86372cf5651f.png" alt class="image--center mx-auto" /></p>
<p>Then, allow this site to open popup windows by clicking <strong>Done</strong>. The above screenshot is from Brave; other browsers will differ slightly as to how they manage popup windows.</p>
<h1 id="heading-summary">Summary</h1>
<p>With just a command or click, we can now unlock the power of Database Actions in our local instance. This extends the capabilities of our local environment to those who don’t need or want to use APEX alone. It is also a great complement to what APEX offers, as there are a number of useful tools in Database Actions that are simply not part of APEX.</p>
<h1 id="heading-appendix">Appendix</h1>
<h2 id="heading-increasing-the-jdbcmaxlimit-parameter">Increasing the jdbc.MaxLimit Parameter</h2>
<p>In my initial testing, I found that the <code>jdbc.MaxLimit</code> value of 30 was not always sufficient. I kept getting errors like this one, which caused the ORDS container to occasionally crash:</p>
<blockquote>
<p>503 The database user for the connection pool named |default|lo|, is not able to proxy to the schema named HR. This could be a configured restriction on the maximum number of database sessions or an authorization failure.</p>
</blockquote>
<p>If you run into something similar, it’s simple to increase this value to fix the issue. I found that setting it to <code>50</code> made all my problems go away.</p>
<p>To change this parameter:</p>
<ol>
<li><p>Open up a new terminal window.</p>
</li>
<li><p>Way back in the <a target="_blank" href="https://spendolini.blog/container-yourself">first post in this series</a>, a new directory was created to store the ORDS configuration &amp; secrets. This directory should have three subdirectories - <code>ords_config</code>, <code>ords_secrets</code> &amp; <code>images</code>. Change to that directory in your terminal window.</p>
</li>
<li><p>Run the following command to edit the configuration file:<br /> <code>vi ords_config/databases/default/pool.xml</code></p>
</li>
<li><p>Locate the line that looks like this: <code>&lt;entry key="jdbc.MaxLimit"&gt;30&lt;/entry&gt;</code></p>
</li>
<li><p>Change the <code>30</code> to a <code>50</code> and save the file.</p>
</li>
</ol>
<p>Now that the change is made, we need to restart the ORDS container.</p>
<ol start="6">
<li><p>From the terminal window, enter the following:<br /> <code>podman stop ords</code></p>
</li>
<li><p>Next, create a new ORDS container with the following command:</p>
</li>
</ol>
<pre><code class="lang-plaintext"> podman run --rm --name ords -v `pwd`/ords_secrets/:/opt/oracle/variables -v `pwd`/ords_config/:/etc/ords/config/ -v `pwd`/images/:/opt/oracle/apex/24.1.0/images -p 443:8181 container-registry.oracle.com/database/ords-developer:latest &amp;
</code></pre>
<p>That’s it! The new container should be running with a higher limit of database sessions.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@charlesdeluvio?utm_source=Hashnode&amp;utm_medium=referral"><em>charlesdeluvio</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Sending Email from an Oracle Container]]></title><description><![CDATA[If you’ve been following this series, you now have:

A container running Oracle 23ai & APEX 24.1

A container running ORDS

A self-signed certificate allowing secure access via ORDS


While the goal is to have a truly portable development environment...]]></description><link>https://spendolini.blog/sending-email-from-an-oracle-container</link><guid isPermaLink="true">https://spendolini.blog/sending-email-from-an-oracle-container</guid><category><![CDATA[Oracle]]></category><category><![CDATA[OCI]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[podman]]></category><category><![CDATA[email]]></category><category><![CDATA[containers]]></category><category><![CDATA[Oracle 23ai]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Mon, 02 Dec 2024 12:45:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494400349/18fe049a-14ad-4ba6-9242-0e8b94fa8f05.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you’ve been following this series, you now have:</p>
<ul>
<li><p>A container running Oracle 23ai &amp; APEX 24.1</p>
</li>
<li><p>A container running ORDS</p>
</li>
<li><p>A self-signed certificate allowing secure access via ORDS</p>
</li>
</ul>
<p>While the goal is to have a truly portable development environment, there’s some things that do need a network. One of those things that most applications will need is the ability to send emails.</p>
<p>APEX has always had the ability to send emails - specifically using the <code>APEX_MAIL</code> API. Sure, you need to point to a configured mail server, but once you have one of those, it’s trivial to configure APEX to send emails.</p>
<p>While we could create another container and configure something like sendmail there, that would only work if it could talk to other mail servers, and also needs the internet to function. That sounds like a lot of work.</p>
<p>Instead, since we need the internet to send email, let’s just point to an existing internet-based email service instead. In this case, we’ll use the <a target="_blank" href="https://www.oracle.com/application-development/email-delivery/">Oracle Email Delivery</a> service as part of the “<a target="_blank" href="https://www.oracle.com/cloud/free/">Always Free”</a> tier on OCI.</p>
<p>If you haven’t created your own free tier by now, you ought to check it out. While it does ask for a credit card while signing up, as long as you stick to the “Always Free” resources, you should not be charged anything. I’ve been using it since 2019 and have paid $0.</p>
<p>Oracle’s Email Delivery service is included in the Always Free tier, meaning that you can use it anytime you like for as long as you like for free. There is a limitation of 100 emails per day, however. For most developers, this is likely an acceptable limit. If you need to send more than 100 per day, you can upgrade your free tier to a regular tenancy, where you will get your first 3,000 emails for free and then pay 8.5 cents per 1,000 additional messages.</p>
<h1 id="heading-email-configuration-overview">Email Configuration Overview</h1>
<p>There’s three parts to configuring the Email Delivery service. The configuration is split across OCI, the Oracle Database and Oracle APEX. Let’s walk through each of them.</p>
<h2 id="heading-steps-for-oci">Steps for OCI</h2>
<p>To configure the Email Delivery service, you’ll need administrator-level access to your OCI tenancy. If you’re using the Always Free tier, the account that you typically use to manage things should suffice.</p>
<h3 id="heading-login-to-the-oci-console">Login to the OCI Console</h3>
<p>To login to the OCI Console:</p>
<ol>
<li><p>Navigate to <a target="_blank" href="https://cloud.oracle.com">https://cloud.oracle.com</a></p>
</li>
<li><p>Enter your <strong>Cloud Account Name</strong> and click <strong>Next</strong>. This is also the name of your tenancy that you created when you signed up for the Free Tier.</p>
</li>
<li><p>Select an <strong>Identity Domain</strong> and click <strong>Next</strong>. Typically, this is set to <strong>Default</strong>.</p>
</li>
<li><p>Enter your <strong>Username</strong> and <strong>Password</strong> and click <strong>Sign In</strong>.</p>
</li>
<li><p>Allow the login event to occur using <strong>Oracle Mobile Authenticator</strong>.</p>
</li>
</ol>
<p>You should now see the main page of the OCI Console:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733107504932/4d0043b0-8250-4619-9c6e-f3f769e4d493.png" alt class="image--center mx-auto" /></p>
<p>Note: Some tenancies have an upgraded version of the OCI Console home page. Navigation will be slightly different if that is the case.</p>
<h3 id="heading-create-a-group">Create a Group</h3>
<p>To start, let’s create a Group. This should be in the same domain that you want to create the user in. Groups are similar to database roles. They are nothing more than a container used to associate users and privileges. Like database roles, Groups can be named anything at all, but won’t inherit any specific privileges until explicitly granted.</p>
<p>To create a Group:</p>
<ol>
<li>From the main menu, select <strong>Identity &amp; Security</strong>. When the next window appears, under the <strong>Identity</strong> section, select <strong>Domains</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733107810983/600b49ab-8212-4bf1-9f0e-2159e05a7d35.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li>On the next screen, choose the <strong>Compartment</strong> that you want to create all of these resources in. It doesn’t really matter which one you use, as long as you keep them all in the same one.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733144361467/f606bb8e-7558-499f-a630-f44d495b3fa9.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li><p>Select the <strong>Domain</strong> that you want to create the Group in. To keep things simple, we’ll simply use the <strong>Default</strong> domain, which is also the current one.</p>
</li>
<li><p>Click on the <strong>Groups</strong> tab to see all current Groups.</p>
</li>
<li><p>Click <strong>Create group</strong>.</p>
</li>
<li><p>Enter <code>approvedSenders</code> for the <strong>Name</strong> and <strong>Description</strong> and click <strong>Create</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733144543709/23d56460-2c06-414b-be28-fb83b3eb05ce.png" alt class="image--center mx-auto" /></p>
<p>We now have a new Group. We’ll map this group to the user we create in just a bit.</p>
<h3 id="heading-create-a-policy">Create a Policy</h3>
<p>Next, we need to create a Policy.</p>
<p>Policies are similar to database privileges. A user that has a policy granted to them will inherit the specific resources referred to in the policy.</p>
<p>Unlike database privileges, policies use their own vernacular when creating them. This post won’t get into the specifics, but you can read more about policy syntax <a target="_blank" href="https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/policysyntax.htm">here</a>.</p>
<p>Let’s create a policy to allow our group to send email.</p>
<ol>
<li><p>Use the Breadcrumbs at the top of the page and click on <strong>Domains</strong>.</p>
</li>
<li><p>Click the <strong>Policies</strong> tab and then click <strong>Create Policy</strong>.</p>
</li>
<li><p>Enter <code>approvedSenders</code> for the <strong>Name</strong> and <strong>Description</strong>, select your <strong>Compartment</strong>, and then click on <strong>Show manual editor</strong>.</p>
</li>
<li><p>When the text area appears, enter the following text:</p>
</li>
</ol>
<pre><code class="lang-sql">Allow group approvedSenders to <span class="hljs-keyword">use</span> approved-senders <span class="hljs-keyword">in</span> compartment &lt;Compartment <span class="hljs-keyword">Name</span>&gt;
</code></pre>
<p>Be sure to replace <code>&lt;Compartment Name&gt;</code> with the name of your compartment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733110367613/c40a012b-8213-44a0-9853-b30f1df91003.png" alt class="image--center mx-auto" /></p>
<ol start="6">
<li>Click <strong>Create</strong>.</li>
</ol>
<h3 id="heading-create-a-user">Create a User</h3>
<p>Next, we need to create a user that will be allowed to send emails. The email address of this user will need to be set in APEX as the designated sender, and only this email will be allowed to perform that role.</p>
<ol>
<li><p>Click on the <strong>Domains</strong> tab and select the domain that you created the Group in earlier.</p>
</li>
<li><p>Click on the <strong>Users</strong> tab and click <strong>Create user</strong>.</p>
</li>
<li><p>Enter <code>Email</code> for the <strong>First name</strong>, <code>Manager</code> for the <strong>Last name</strong>, and the email address that you want to send from for the <strong>Username / Email</strong>.<br /> Note: you can use any email address here. This will be the address in the <strong>From</strong> field of all emails sent, so choose wisely.</p>
</li>
<li><p>Make sure the <strong>Use the email address as the username</strong> is checked.</p>
</li>
<li><p>In the <strong>Groups</strong> section, be sure to check <strong>approvedSenders</strong>.</p>
</li>
<li><p>Click <strong>Create</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733144582382/bed355bc-4dfe-4b3f-aed3-17ed23c715fa.png" alt class="image--center mx-auto" /></p>
<p>Next, we want to remove unnecessary capabilities from this user, since all they will really need to do is store the SMTP credentials.</p>
<ol start="7">
<li><p>Edit the new user.</p>
</li>
<li><p>Click <strong>Edit user capabilities</strong>.</p>
</li>
<li><p>Uncheck all options except <strong>SMTP credentials</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733144617363/009279c9-7001-41e6-9758-cf8bf03c20d9.png" alt class="image--center mx-auto" /></p>
<ol start="10">
<li>Click <strong>Save changes</strong>.</li>
</ol>
<h3 id="heading-create-smtp-credentials">Create SMTP Credentials</h3>
<p>Next, we need to create a set of SMTP credentials and associate them with this user. These credentials will also need to be added to APEX and serve as the username and password to access the Email Delivery service with.</p>
<ol>
<li><p>In the <strong>Resources</strong> section, select <strong>SMTP credentials</strong>.</p>
</li>
<li><p>Click <strong>Generate credentials</strong>.</p>
</li>
<li><p>Enter <strong>SMTP credentials</strong> for the <strong>Description</strong> and click <strong>Generate credentials</strong>.</p>
</li>
</ol>
<p>On the next screen, it will display the SMTP <strong>Username</strong> and <strong>Password</strong>.</p>
<ol start="4">
<li>Copy both the <strong>Username</strong> and <strong>Password</strong> to somewhere that you can refer to them later.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733144649463/de33b942-43d6-4ab1-8eb1-956e3299989b.png" alt class="image--center mx-auto" /></p>
<p>Note: This will be the <strong>ONLY</strong> time you can copy the password, so be sure to copy it before closing this window.</p>
<h3 id="heading-add-approved-sender">Add Approved Sender</h3>
<p>Next, let’s add the email address of the user we just created to the list of Approved Senders. This is the final link that will allow us to send emails from APEX &amp; PL/SQL applications.</p>
<ol>
<li>From the main menu, select <strong>Developer Services</strong>. When the next window appears, under the <strong>Application Integration</strong> section, select <strong>Email Delivery</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733144681145/824a75ff-d008-4039-9aa2-be0f05d02d43.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li><p>Be sure that the correct <strong>Compartment</strong> is selected.</p>
</li>
<li><p>Click the <strong>Approved Senders</strong> tab.</p>
</li>
<li><p>Click <strong>Create Approved Sender</strong>.</p>
</li>
<li><p>Enter the email address of the user that was just created and click <strong>Create Approved Sender</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733111667887/4e86818b-38d0-4375-b852-94200166eb61.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-copy-email-delivery-endpoint">Copy Email Delivery Endpoint</h3>
<p>One last bit of information we’re going to need is the <strong>SMTP Public Endpoint</strong>. This is essentially the address of the Email Delivery service that we will need to add to APEX in just a bit.</p>
<p>To get the <strong>SMTP Public Endpoint</strong>:</p>
<ol>
<li><p>Click on the <strong>Configuration</strong> tab.</p>
</li>
<li><p>Under the <strong>SMTP Sending Information</strong> region, record the value of the <strong>Public Endpoint</strong> and <strong>SMTP Ports</strong>. We will need to add this to APEX in the next section.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733112050178/268a9e96-0785-4db2-939b-3357afcf9591.png" alt class="image--center mx-auto" /></p>
<p>That concludes the steps that need to be performed in the OCI Console.</p>
<h2 id="heading-steps-for-apex">Steps for APEX</h2>
<p>Now that we have the underlying components configured, all we need to do in APEX is enter them in the corresponding fields in the Instance Administration application.</p>
<ol>
<li><p>Login to the <code>INTERNAL</code> workspace as the <code>ADMIN</code> user in APEX.</p>
</li>
<li><p>Click on <strong>Manage Instance</strong>.</p>
</li>
<li><p>Click on <strong>Instance Settings</strong>.</p>
</li>
<li><p>In the <strong>Email</strong> section, enter the following values:</p>
</li>
</ol>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Field Name</strong></td><td><strong>Value</strong></td></tr>
</thead>
<tbody>
<tr>
<td>SMTP Host Address</td><td>[Value of SMTP Public Endpoint]</td></tr>
<tr>
<td>SMTP Host Port</td><td><code>587</code></td></tr>
<tr>
<td>SMTP Authentication Username</td><td>[Value of SMTP Credential Username]</td></tr>
<tr>
<td>SMTP Password</td><td>[Value of SMTP Credential Password]</td></tr>
<tr>
<td>Confirm SMTP Password</td><td>[Value of SMTP Credential Password]</td></tr>
<tr>
<td>Use SSL/TLS</td><td><code>After connection is established</code></td></tr>
<tr>
<td>Default Email From Address</td><td>[Email of user created earlier]</td></tr>
</tbody>
</table>
</div><p>Note that most values will need to be replaced with the ones derived in earlier steps.</p>
<ol start="5">
<li>Click <strong>Apply Changes</strong>.</li>
</ol>
<p>Since we’re configuring this at the instance level, these settings will apply to all workspaces in the instance of APEX. They can be overridden by passing in new values to the <code>APEX_MAIL</code> API.</p>
<h2 id="heading-steps-for-the-oracle-database">Steps for the Oracle Database</h2>
<p>There’s one more step at the database level that needs to be performed. We need to create an ACE (access control entry) so that APEX can call the Email Delivery endpoint.</p>
<ol>
<li><p>Connect to the database via SQLcl as the <strong>SYS</strong> user.</p>
</li>
<li><p>Ensure that you’re connected to the correct PDB. In this case, it should be <code>freepdb1</code>.</p>
</li>
<li><p>Run the following SQL statement:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>
  dbms_network_acl_admin.append_host_ace (
    host       =&gt; <span class="hljs-string">'smtp.email.us-ashburn-1.oci.oraclecloud.com'</span>,
    lower_port =&gt; <span class="hljs-number">587</span>,
    upper_port =&gt; <span class="hljs-number">587</span>,
    ace        =&gt; xs$ace_type(privilege_list =&gt; xs$name_list(<span class="hljs-string">'connect'</span>),
                              principal_name =&gt; <span class="hljs-string">'APEX_240100'</span>,
                              principal_type =&gt; xs_acl.ptype_db));
<span class="hljs-keyword">end</span>;
/
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Thanks to <a target="_self" href="https://bsky.app/profile/connormcd.bsky.social">Connor McDonald</a> for an updated way to open up access from the database using Access Control Entries vs. Access Control Lists.</div>
</div>

<p>Note: you may need to change the value of the <code>host</code> parameter, based on what your <strong>SMTP Public Endpoint</strong> is.</p>
<p>That’s it! All three parts - OCI, APEX &amp; Oracle Database - are all configured to send email from your local instance via the Email Delivery service!</p>
<h2 id="heading-testing-it-all-out">Testing It All Out</h2>
<p>Now that everything is configured, let’s test it out. To do this, all you will need is an APEX workspace on your local development environment. Any workspace aside from <code>INTERNAL</code> will do.</p>
<ol>
<li><p>Login to any workspace (aside from INTERNAL) on your local APEX environment.</p>
</li>
<li><p>Click on <strong>SQL Workshop</strong>.</p>
</li>
<li><p>Click on <strong>SQL Commands</strong>.</p>
</li>
<li><p>Enter the following SQL:</p>
</li>
</ol>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span>

<span class="hljs-comment">-- send a message</span>
apex_mail.send
  (
   p_from =&gt; <span class="hljs-string">'&lt;email address of user created&gt;'</span>  <span class="hljs-comment">-- replace this value</span>
  ,p_to   =&gt; <span class="hljs-string">'&lt;your email address&gt;'</span>             <span class="hljs-comment">-- replace this value</span>
  ,p_subj =&gt; <span class="hljs-string">'Test Email Message'</span>
  ,p_body =&gt; <span class="hljs-string">'This is a test email message sent via OCI Email Delivery'</span>
  );

<span class="hljs-comment">-- push the email queue now</span>
apex_mail.push_queue; 

<span class="hljs-keyword">end</span>; 
/
</code></pre>
<ol start="5">
<li><p>Be sure to replace the <code>p_from</code> &amp; <code>p_to</code> parameters with the email address that was created for the user and any email address that you have access to, respectively.</p>
</li>
<li><p>Run the SQL.</p>
</li>
</ol>
<p>If everything works, you will receive an email with the above subject &amp; body in just a few seconds.</p>
<p>In the case that you don’t receive an email, check the view <code>APEX_MAIL_QUEUE</code> - specifically the column <code>MAIL_SEND_ERROR</code> - for more information.</p>
<h1 id="heading-summary">Summary</h1>
<p>Our little development environment leveled up once again, and can now send email messages thanks to the OCI Email Delivery service. With just a few steps, we were able to create a user, group, policy, SMTP credential and then map all of that to our APEX instance. This is just another example of just how well you can extend the capabilities of APEX with a variety of OCI services.</p>
<p>Best of all - all of this can be done at no cost using the OCI Free Tier!</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@ethanchoover?utm_source=Hashnode&amp;utm_medium=referral"><em>Ethan Hoover</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Adding SSL to your ORDS Container]]></title><description><![CDATA[Something just doesn’t feel “right” about running a development environment on a non-standard port over just HTTP. Not only is it bothersome to see the “Not Secure” message, it’s not a best practice. Your development environment should be as close to...]]></description><link>https://spendolini.blog/adding-ssl-to-your-ords-container</link><guid isPermaLink="true">https://spendolini.blog/adding-ssl-to-your-ords-container</guid><category><![CDATA[Oracle]]></category><category><![CDATA[ords]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[SSL]]></category><category><![CDATA[podman]]></category><category><![CDATA[containers]]></category><category><![CDATA[Oracle 23ai]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Thu, 28 Nov 2024 02:40:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494545648/3570f882-06f4-43ab-aca7-8dfca0713ad2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Something just doesn’t feel “right” about running a development environment on a non-standard port over just HTTP. Not only is it bothersome to see the “Not Secure” message, it’s not a best practice. Your development environment should be as close to your production environment as possible.</p>
<p>While the main difference is that HTTPS will encrypt data in transit and HTTP will not, there’s another subtle one that’s also important. HTTPS pages will not allow Javascript to be called from HTTP-only URLs. Thus, if you don’t use HTTPS, this issue would not rear its ugly head until you deployed to production.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">NOTE: Do not use these steps for production instances. Creating a self-signed certificate and then manually trusting it is only OK for local development environments.</div>
</div>

<h1 id="heading-creating-amp-installing-the-certificate">Creating &amp; Installing the Certificate</h1>
<p>It only takes a few minutes to get up and running with your own, trusted, self-signed certificate. The initial first few steps work for both Mac &amp; Windows. Pay attention at the end, as adding the self-signed certificate as trusted varies across platforms. I’ve added a section for each OS.</p>
<h2 id="heading-creating-the-certificate">Creating the Certificate</h2>
<p>Let’s start by creating our self-signed certificate.</p>
<ol>
<li><p>Open a new terminal window</p>
</li>
<li><p>Navigate to the <code>ords_config</code> directory. This was set up as part of my previous post <a target="_blank" href="https://spendolini.blog/container-yourself">here</a>.</p>
</li>
<li><p>Create a new directory called <code>ssl</code>.</p>
</li>
<li><p>Navigate to the <code>ssl</code> directory.</p>
</li>
<li><p>Run the following command:</p>
<pre><code class="lang-plaintext"> openssl req -x509 -out cert.crt -keyout key.key -days 9999 \
   -newkey rsa:2048 -nodes -sha256 \
   -subj '/CN=localhost' -extensions EXT -config &lt;( \
    printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
</code></pre>
</li>
</ol>
<p>Some notes about this command:</p>
<ul>
<li><p>The bulk of this command was taken from this page: <a target="_blank" href="https://letsencrypt.org/docs/certificates-for-localhost/">https://letsencrypt.org/docs/certificates-for-localhost/</a></p>
</li>
<li><p>I set <code>days = 9999</code> because I don’t want to be bothered to re-create this certificate. Ever.</p>
</li>
<li><p>The name of the certificate pair - <code>cert.crt</code> &amp; <code>key.key</code> - are what the ORDS container will look for, so don’t change them.</p>
</li>
</ul>
<ol start="6">
<li><p>Upon running this command, you should have two files in the <code>ssl</code> directory:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732722322300/15dd3d6b-7400-4fb5-ae72-49ed11935b2f.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-installing-the-certificate">Installing the Certificate</h2>
<p>A simple restart of the ORDS container is all we need to switch over from HTTP to HTTPS.</p>
<p>Remember what I said about that non-standard port? Let’s change that to 443, so that we don’t need to put a port number in the URL at all.</p>
<ol>
<li><p>Terminate your existing ORDS container.</p>
</li>
<li><p>Using this new command, restart the ORDS container:</p>
<pre><code class="lang-plaintext"> podman run --rm --name ords -v `pwd`/ords_secrets/:/opt/oracle/variables -v `pwd`/ords_config/:/etc/ords/config/ -v `pwd`/images/:/opt/oracle/apex/24.1.0/images -p 443:8181 container-registry.oracle.com/database/ords-developer:latest &amp;
</code></pre>
</li>
</ol>
<p>This command has one additional argument in it: <code>-p 443:8181</code> That additional attribute will redirect port 8181 from the container to your local port 443.</p>
<p>As keen readers may have noticed, I’ve saved this command to a local file called <code>start_ords</code>. This way, I can just run that file to restart my container vs. sorting through my blog to find the command syntax.</p>
<h2 id="heading-testing-the-certificate">Testing the Certificate</h2>
<p>Let’s see what happens now what we try to access our ORDS container.</p>
<ol>
<li>Open up a new browser window and enter the following URL: <code>https://localhost</code></li>
</ol>
<p>As expected, you should see the “Your connection is not private” error. This is because the certificate is self-signed, and the browser cannot associate it with one of it’s CAs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732741276031/2824e532-6de1-4496-8bfa-8f9c06349b81.png" alt class="image--center mx-auto" /></p>
<p>You can click “<strong>Advanced</strong>” and then <strong>Proceed to localhost (unsafe)</strong>, but that gets annoying pretty quickly.</p>
<h2 id="heading-trusting-the-certificate">Trusting the Certificate</h2>
<p>While theres a number of ways to fix this issue, the quickest one that I have come by is to tell your local machine to simply “trust” this self-signed certificate as if it were signed by a legitimate CA. This takes just seconds, and agains, since this is a local development environment, is a perfectly secure approach to take.</p>
<p>Trusting the certificate will vary, depending on your local OS.</p>
<h3 id="heading-macos">MacOS</h3>
<p>To trust a self-signed certificate on Mac OS, we simply need to add the certificate to the MacOS Keychain.</p>
<ol>
<li><p>Launch the application called <strong>Keychain Access</strong>.</p>
</li>
<li><p>Enter your credentials.</p>
</li>
</ol>
<p>You should now see a window similar to this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732723757663/b1d258cd-7109-4332-bfd7-71065cb559e5.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li><p>Using the Finder, navigate to the folder where you placed the certificates that you created earlier. It should be in <code>/ords_config/ssl</code>.</p>
</li>
<li><p>Drag the file <code>cert.crt</code> to the “striped” part of the <strong>Keychain Access</strong> window (under the column headings). You should see the following popup window:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732740927398/29e4b75c-b80e-4233-9c8b-16e33e863ab8.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Select <strong>Add</strong>.</p>
</li>
<li><p>On the main <strong>Keychain Access</strong> window, search for <code>localhost</code>. You should see a single entry:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732740988348/955c389e-e44f-48d6-9c07-42df84d2d960.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Double-click the <code>localhost</code> entry.</p>
</li>
<li><p>Expand the <strong>Trust</strong> region.</p>
</li>
<li><p>Set <strong>When using this certificate</strong> to <strong>Always Trust</strong>. All other options should automatically change to <strong>Always Trust</strong> as well.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732741082655/63233933-b337-4282-82fd-ab6fae53fae2.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Close the <strong>localhost</strong> window. You will need to re-enter your credentials for the changes to take effect.</p>
</li>
</ol>
<p>At this point, your local OS will trust our self-signed <strong>localhost</strong> certificate as if it were valid. You will no longer see the “<strong>Not Secure</strong>” message, either. Again, fine for development, not really a great idea for production systems.</p>
<p>We can prove this by reloading our browser that was pointed to <code>https://localhost</code>.</p>
<p>The results should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732741284029/2b90030c-4599-471e-9d3e-8ecd3fd78284.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-windows">Windows</h3>
<p>On Windows, you need to take similar steps to allow the OS to trust the self-signed certificate.</p>
<p>I did some searching, and <a target="_blank" href="https://support.vertigis.com/hc/en-us/articles/11461054555410-Adding-a-self-signed-certificate-to-Trusted-Root-Certification-Authorities-Store">this post</a> seems to have a decent set of steps to do this. I did not have the chance to try this out, so please leave a comment if it doesn’t work or you have a better set of steps to use on Windows.</p>
<h1 id="heading-summary">Summary</h1>
<p>Even though it’s a local development environment, it’s important to try to mirror what’s on production as much as possible - including the protocol used to access APEX. It doesn’t take much work to create and install your own certificates into the ORDS container, ensuring that all web applications are accessed via HTTPS.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@fertroulik?utm_source=Hashnode&amp;utm_medium=referral"><em>Fer Troulik</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Upgrading APEX in a Container]]></title><description><![CDATA[💡
NOTE: this is a follow-up post from my last entry, Container Yourself. It’s best to start there if you’re going to follow along and perform the steps outlined here.


I didn’t really notice this, but the version of APEX that I downloaded was 24.1....]]></description><link>https://spendolini.blog/upgrading-apex-in-a-container</link><guid isPermaLink="true">https://spendolini.blog/upgrading-apex-in-a-container</guid><category><![CDATA[Oracle]]></category><category><![CDATA[containers]]></category><category><![CDATA[podman]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[AI]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[ords]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 26 Nov 2024 00:02:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494689933/9c71dde7-b0d5-4b0a-9f67-3357f71d0158.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">NOTE: this is a follow-up post from my last entry, <a target="_self" href="https://spendolini.blog/container-yourself">Container Yourself</a>. It’s best to start there if you’re going to follow along and perform the steps outlined here.</div>
</div>

<p>I didn’t really notice this, but the version of APEX that I downloaded was 24.1.0. Thus, I’m six patch sets behind what’s current as of the publish date of this post.</p>
<p>Remember: APEX patches typically contain two parts:</p>
<ul>
<li><p>Database objects</p>
</li>
<li><p>Images / Javascript files / CSS files</p>
</li>
</ul>
<h1 id="heading-container-differences">Container Differences</h1>
<p>In the world of containers, things are a little different - especially with the ORDS Developer container. Looking into how it’s setup, when ORDS starts, it points to a specific location in the container for the APEX images. And that path is somewhat hard-coded.</p>
<p>Here’s the the two commands that run when ORDS starts up:</p>
<p><code>APEXI=/opt/oracle/apex/$APEX_VER/images</code></p>
<p><code>ords --config $ORDS_CONF_DIR serve --port 8181 --apex-images $APEXI</code></p>
<p>$APEX_VER gets translated into 24.1.0 in this case, so the APEX images directory becomes <code>/opt/oracle/apex/24.1.0/images</code>. Since this is in the container’s filesystem, there’s no easy way to make updates to the images in there and have it persist.</p>
<h1 id="heading-verifying-an-apex-release">Verifying an APEX Release</h1>
<p>To figure out which release of APEX you’re running, you’re going to need to do one of two things, depending on whether you’re looking at the database objects or the “images” directory.</p>
<p>I use “images” lightly here, as most of what APEX relies on these days are Javascript and CSS files. Looking at some of the images that are included, they date back to HTML DB 1.5, where tabs were constructed by assigning images to <strong>&lt;td&gt;</strong> tags in an HTML table. Good times!</p>
<h2 id="heading-apex-database-objects-version">APEX Database Objects Version</h2>
<p>The name of the schema is an obvious tell here - <strong>APEX_240100</strong> is clearly APEX 24.1. But it gives you no clue as to the patch sets that have been applied. To get that, we have to run the following:</p>
<p><code>select version from dba_registry where comp_id = 'APEX';</code></p>
<p>This will return a more specific version of APEX, like below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732574541512/1fd4b794-e980-45a8-8700-4387e9daefe2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-apex-images-files-version">APEX “images” Files Version</h2>
<p>Hidden in the root directory of the APEX images directory is a file called <strong>apex_version.js</strong>. The contents of this file are simple:</p>
<p><code>var gApexVersion = “24.1.0”;</code></p>
<p>This will always have store the three-digit version of the APEX images directory.</p>
<p>If you don’t have access to the filesystem, simply enter the following URL to see this file’s contents in a browser:</p>
<p><code>https://servername.com/i/apex_version.js</code></p>
<h1 id="heading-upgrading-all-the-things">Upgrading All the Things</h1>
<p>So it’s obvious we’re running APEX 24.1.0, and need to upgrade to 24.1.6 - the most current release at the time this was written.</p>
<p>The database side is pretty straightforward - run the patch as SYS and you’re done.</p>
<p>The filesystem part is a little more involved. Since we can’t modify anything in the container, we need to host the files outside of the container. My first thought was to use the APEX CDN point to the proper version. That is a viable solution, so as long as you have network access to the CDN. Since one of the goals here was to have a portable development system that did not rely on the internet, I had to come up with something else.</p>
<p>We can host the APEX image directory locally, and when we run the ORDS container, we can add a parameter to map the directory that the container uses (<code>/opt/oracle/apex/24.1.0/images</code>) to our local directory - which we will then upgrade to 24.1.6.</p>
<p>Let’s walk through the steps do to this.</p>
<h2 id="heading-download-and-stage-the-apex-images-directory">Download and Stage the APEX Images Directory</h2>
<p>First of all, we need to download the APEX 24.1.0 images directory. This can be found in any distribution of APEX itself.</p>
<p>You can find and download all versions of APEX here:</p>
<p><a target="_blank" href="https://www.oracle.com/tools/downloads/apex-downloads/">https://www.oracle.com/tools/downloads/apex-downloads/</a></p>
<p>In this case, I simply downloaded and unzipped the English-language only version.</p>
<p>Next, we need a place to stage these files that is relatively permanent. I chose to move the whole <strong>images</strong> directory to the same top-level directory that I used to store my ORDS configuration and secrets. Here’s what it looks like in the filesystem:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732576592207/366d4e0d-4548-492a-b399-687b419be4c6.png" alt class="image--center mx-auto" /></p>
<p>I simply copied the <strong>images</strong> folder from the patch directory to my custom directory by dragging it via the Finder; you can move it any way you like.</p>
<h2 id="heading-restart-the-ords-container">Restart the ORDS Container</h2>
<p>Now that we have our custom images directory in place, let’s use a slightly different command to create a new ORDS container. This command contains an additional volume mapping that will use our images directory in place of the one in the container.</p>
<ol>
<li><p>Terminate your existing ORDS container via Podman</p>
</li>
<li><p>Open a new terminal window</p>
</li>
<li><p>Change directories to the directory that contains your <strong>images</strong>, <strong>ords_config</strong> and <strong>ords_secrets</strong> directories</p>
</li>
<li><p>Run the following command:</p>
<pre><code class="lang-plaintext"> podman run --rm --name ords -v `pwd`/ords_secrets/:/opt/oracle/variables -v `pwd`/ords_config/:/etc/ords/config/ -v `pwd`/images/:/opt/oracle/apex/24.1.0/images -p 8181:8181 container-registry.oracle.com/database/ords-developer:latest &amp;
</code></pre>
</li>
</ol>
<p>This additional volume mapping - <code>-v `pwd`/images/:/opt/oracle/apex/24.1.0/images</code> - will point the container images directory at our local directory - which right now should have identical contents.</p>
<p>From now on, this is the command that you want to use when re-launching ORDS containers.</p>
<h2 id="heading-confirm-the-directory-mapping">Confirm the Directory Mapping</h2>
<p>Let’s confirm that the change worked.</p>
<ol>
<li><p>Open a new browser.</p>
</li>
<li><p>Enter the following URL, replacing servername.com with your hostname:</p>
<p> <code>https://servername.com/i/apex_version.js</code></p>
</li>
</ol>
<p>If successful, you will still see this text:</p>
<p><code>var gApexVersion = “24.1.0”;</code></p>
<h2 id="heading-download-and-install-the-patch">Download and Install the Patch</h2>
<p>Now that we have the volume mapped, we will need to download and install the patch for APEX 24.1.6. This patch can only be downloaded from Oracle Support with a valid account. For reference, the patch number is <strong>36695709</strong>.</p>
<p>Once you have downloaded and unzipped the patch, follow the instructions in the <strong>README.txt</strong> file to install the database side of the patch. This should take no more than a few minutes.</p>
<p>When you get to Step 7 of the patch, follow the instructions with the assumption that the <strong>/images</strong> directory on the APEX server is the one that you’re hosting on your local machine.</p>
<p>In my case, this was the command that I ran from within the main patch directory:</p>
<p><code>cp -rp images ~/Podman/freepdb1/</code></p>
<h2 id="heading-verify-the-patch">Verify the Patch</h2>
<p>Now that the patch is applied, let’s check both places to ensure that it was successful.</p>
<p>First, the filesystem:</p>
<ol>
<li><p>Open a new browser.</p>
</li>
<li><p>Enter the following URL, replacing servername.com with your hostname:</p>
<p> <code>https://servername.com/i/apex_version.js</code></p>
</li>
</ol>
<p>If successful, you will still see this text:</p>
<p><code>var gApexVersion = “24.1.6”;</code></p>
<p>Next, the database:</p>
<ol start="3">
<li><p>Open a new terminal window and connect to your database a SYS</p>
</li>
<li><p>Run the following command:</p>
<p> <code>select version from dba_registry where comp_id = 'APEX';</code></p>
</li>
</ol>
<p>You should see the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732578132677/9f37832a-10e7-4cbf-ac99-316a60301aef.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-summary">Summary</h1>
<p>Congratulations! Your container database is now running APEX 24.1.6, and your ORDS container is using your local directory to provide the correct files for that release.</p>
<p>You should be able to stop &amp; start the ORDS container as often as you need using the command referenced in the <strong>Restart the ORDS Container</strong> section.</p>
<p>One thing to keep in mind is that you no longer need to create a <strong>conn_string.txt</strong> file to start to the ORDS container. In fact, if you do, you will get an APEX version mis-match error.</p>
<p>And lastly - just another reminder that the database running in your container is volatile. If you delete the container, you delete the database, too. You will then need to create a new container from the Oracle 23ai image, install APEX, upgrade to 24.1.6, etc. to restore your configuration.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@amiraaartistry?utm_source=Hashnode&amp;utm_medium=referral"><em>Amira El Pohail</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Container Yourself]]></title><description><![CDATA[Overview
Having a local development instance offers a lot of benefits. It’s fast, it works from anywhere - with or without the internet - and you can configure it any which way you want. It’s also typically free to use, since most vendors - including...]]></description><link>https://spendolini.blog/container-yourself</link><guid isPermaLink="true">https://spendolini.blog/container-yourself</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[podman]]></category><category><![CDATA[containers]]></category><category><![CDATA[AI]]></category><category><![CDATA[Oracle 23ai]]></category><category><![CDATA[ords]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Mon, 25 Nov 2024 14:50:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494766795/3d94583f-1a65-44cb-8d55-27cf99f47675.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-overview">Overview</h1>
<p>Having a local development instance offers a lot of benefits. It’s fast, it works from anywhere - with or without the internet - and you can configure it any which way you want. It’s also typically free to use, since most vendors - including Oracle - provide versions of their software at no cost for developers.</p>
<p>I find this very useful as a way to test out upgrades or ideas quickly, as well as a backup system that I can use for demonstrations in the case of an issue with my cloud databases.</p>
<p>In the old times, this would involve installing Oracle directly onto your laptop and configuring and managing it there. This was messy, and often conflicted with other software, creating a configuration nightmare. With today’s enterprise management tools, it may not even be possible to do it this way on your work machine.</p>
<p>Enter containers. Containers are portable, self-contained “units” that contain all of the things that an application needs to run. They make moving applications across environments a lot easier, as all of the dependencies are already included. There’s a ton of information on containers elsewhere; having some basic knowledge about containers may make this post a little easier to understand, but is not required.</p>
<h1 id="heading-building-the-environment">Building the Environment</h1>
<p>To build a containerized APEX environment, we will need two containers - one for the database and one for ORDS. We can visit the <a target="_blank" href="https://container-registry.oracle.com/">Oracle Container Registry</a> and grab pre-packaged containers for each.</p>
<p>Before we get started, we need to prepare our local machine. While Docker is the most popular container management software, it’s not the only one. There are several alternatives, such as Podman &amp; Rancher. This post will make use of Podman - mostly because it’s the standard at Oracle and installing Docker is typically not permitted.</p>
<h2 id="heading-be-persistent">Be Persistent</h2>
<p>A note about persistence and containers. A container will typically keep its filesystem intact when it’s stopped. However, if the container is deleted, then the filesystem usually goes with it.</p>
<p>Of course there are ways around this, but this post won’t get into that. Thus, if you end up deleting the Oracle Database container, your database gets deleted, too. It’s best to ensure that all of your code, seed scripts and anything else that lives in the database is stored in script files that are managed in a version control system.</p>
<p>In the case of the ORDS container, persisting the filesystem doesn’t really matter since we inject local configuration information into that container each time it starts.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: I used my MacBook Air M2 machine to build and test the containers in this post. Steps for other platforms may vary slightly and/or may not work the same.</div>
</div>

<h2 id="heading-download-amp-install-podman">Download &amp; Install Podman</h2>
<p>First, we’ll need to download and install Podman.</p>
<p>Podman is an open source alternative to Docker. Most Docker commands work just fine with Podman.</p>
<ol>
<li><p>Navigate to <a target="_blank" href="https://podman.io/">https://podman.io/</a></p>
</li>
<li><p>Click <strong>Download</strong></p>
</li>
<li><p>Select <strong>Podman Desktop</strong> for your OS</p>
</li>
<li><p>Once downloaded, install and start up Podman. You should see a screen that looks like this:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732464189182/51e82090-8a50-4221-b4f5-c3cdf3def567.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-download-the-oracle-23ai-free-container">Download the Oracle 23ai Free Container</h2>
<p>Next, we’ll need to download the container that contains the Oracle 23ai Free Edition. This can be done either via command line or from within Podman Desktop.</p>
<ol>
<li><p>Open <strong>Podman Desktop</strong></p>
</li>
<li><p>Click the <strong>Containers</strong> icon</p>
</li>
<li><p>Click <strong>Create</strong></p>
</li>
<li><p>Click <strong>Existing Image</strong></p>
</li>
<li><p>Click <strong>Pull</strong></p>
</li>
<li><p>Enter the following for <strong>Image to Pull</strong>:<br /> <code>container-registry.oracle.com/database/free:latest</code></p>
</li>
<li><p>Click <strong>Pull Image</strong></p>
</li>
</ol>
<p>The container will start to download. Depending on your network speed, it will take a few minutes for this to complete.</p>
<p>Once it’s completed, it should look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732464755708/33f0742e-23a3-41d3-9502-6e6c51d3482f.png" alt class="image--center mx-auto" /></p>
<p>Click <strong>Done</strong> to complete this section.</p>
<h2 id="heading-start-amp-configure-the-oracle-23ai-container">Start &amp; Configure the Oracle 23ai Container</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">NOTE: In order to survive reboots, it’s better to create the container from the command line and set a fixed IP address. This section has been updated to reflect that.</div>
</div>

<p>Now that we have the container downloaded, let’s start it up.</p>
<ol>
<li><p>Open a new terminal window</p>
</li>
<li><p>Enter the following command:</p>
<pre><code class="lang-plaintext"> podman run --name oracle --ip 10.88.0.2 -p 1521:1521 container-registry.oracle.com/database/free &amp;
</code></pre>
</li>
</ol>
<p>This will create a new container called oracle and set the IP address to 10.88.0.2. This is important, since the wallet will bind to the IP address, and having it change after each reboot will get annoying. It will also map port 1521 to local 1521, so that your muscle memory will work when typing connection strings.</p>
<ol start="3">
<li>Switch to Podman to verify that it worked. You should see a single entry in the Containers tab for <strong>oracle</strong>.</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732628930393/7704bd70-0253-4005-b324-e953f3247ba7.png" alt class="image--center mx-auto" /></p>
<p>Here’s the cool part - in just a few seconds - yes, seconds - we were able to spin up a fully-functional Oracle 23ai database.</p>
<p>Don’t believe me? Click on <strong>Actions &gt; Open Terminal</strong> tab and run the following SQL:</p>
<p><code>sqlplus / as sysdba</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732465350429/829421f1-dbbd-4cb3-b24d-8e728f8ae29c.png" alt class="image--center mx-auto" /></p>
<p>While we have the <strong>Terminal</strong> open, let’s change the password of SYS &amp; SYSTEM, so that we can connect from outside of Podman.</p>
<p>To do this</p>
<ol>
<li><p>Log out of SQL*Plus, if you’re still logged in</p>
</li>
<li><p>Run the following command in the <strong>Terminal</strong>:</p>
<p> <code>./setPassword oracle</code></p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732465712453/c38d2adf-0a9a-4ef2-b0e1-18b9aaadf159.png" alt class="image--center mx-auto" /></p>
<p>In the above example, the password was set to “oracle”, but you can use anything that you want.</p>
<p>Now, you should be able to connect to this container from outside of Podman. Let’s try to connect with SQLcl.</p>
<ol>
<li>Open a new terminal from your local machine and enter the following:</li>
</ol>
<p><code>sql sys@localhost:1521/freepdb1 as sysdba</code></p>
<ol start="2">
<li>Enter the password that you just set, and you should be connected to the <strong>FREEPDB1</strong> PDB, as shown below:</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732465971256/6ba5ea4c-5adc-4e77-9888-d69096ba947b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-download-amp-install-apex">Download &amp; Install APEX</h2>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: This step should not be necessary, as scripts to install APEX are included in the ords-developer container. Despite multiple attempts, I could not get it to work; thus APEX needs to be installed manually into the database container.</div>
</div>

<p>To install APEX manually, follow these steps.</p>
<ol>
<li><p>Download APEX from <a target="_blank" href="https://www.oracle.com/tools/downloads/apex-downloads/">https://www.oracle.com/tools/downloads/apex-downloads/</a></p>
</li>
<li><p>Unzip the downloaded file to your local machine.</p>
</li>
<li><p>Open up a new terminal and change directories to the <code>/apex</code> directory.</p>
</li>
<li><p>Connect to the container database as <strong>SYS</strong>. You can use the following command to do so: <code>sql sys/oracle@localhost:1521/freepdb1 as sysdba</code></p>
</li>
</ol>
<p>It’s a good idea to create a dedicated tablespace for APEX. This keeps it separate from other schemas.</p>
<ol start="5">
<li>Create a new tablespace with the following command:<br /> <code>create tablespace apex datafile 'apex_001.dbf' size 300m autoextend on;</code></li>
</ol>
<p>Now that we have our tablespace created, we’re ready to install APEX.</p>
<ol start="6">
<li>Install APEX with the following command:<br /> <code>@apexins apex apex temp /i/</code></li>
</ol>
<p>This step will likely take a few minutes to complete, depending on your local machine’s specifications.</p>
<p>Once completed, there’s two more administrative tasks we need to do. First, we need to unlock the <strong>APEX_PUBLIC_USER</strong> account. This is needed for ORDS to properly connect to the database.</p>
<ol start="7">
<li>To unlock <strong>APEX_PUBLIC_USER</strong>, run the following command:<br /> <code>alter user apex_public_user account unlock;</code></li>
</ol>
<p>Lastly, let’s reset the APEX administrator account so we will be able to login to the Internal workspace once ORDS is up and running.</p>
<ol start="8">
<li><p>Run the following command and follow the prompts.</p>
<p> <code>@apxchpwd</code></p>
</li>
</ol>
<p>Take the default values for the username and email. You will need to create a somewhat complex password that has at least 1 uppercase character, 1 number and 1 special character. This will be the password that you use to login to the ADMIN user in the INTERNAL workspace.</p>
<p>Here’s an example of what the output should look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732470910169/8a9d90bb-259c-4892-a25e-096b2a810241.png" alt class="image--center mx-auto" /></p>
<p>The APEX database schemas &amp; objects should now be successfully installed in your container database.</p>
<h2 id="heading-download-start-amp-configure-the-ords-container">Download, Start &amp; Configure the ORDS Container</h2>
<p>This next step is pretty cool, as it will do the following:</p>
<ul>
<li><p>Download the ORDS container</p>
</li>
<li><p>Configure the connection string in ORDS</p>
</li>
<li><p>Connect to your container database</p>
</li>
<li><p>Install and configure ORDS in the database</p>
</li>
<li><p><s>Install and configure APEX in the database</s></p>
</li>
<li><p>Start the ORDS Jetty listener so you can connect to APEX</p>
</li>
</ul>
<p>Before we do this, there’s a couple pre-requisites that need to be completed. We need to create a pair of local directories and add a configuration file so that ORDS knows where to connect to.</p>
<h3 id="heading-determine-the-ip-of-the-database-container">Determine the IP of the Database Container</h3>
<p>For ORDS to connect to the database, we’ll need to get the IP address of the container database. This is the IP address that containers use to talk to each other, not what we use to connect to it from our local machine.</p>
<ol>
<li><p>Open up a new local terminal</p>
</li>
<li><p>Enter the following command:<br /> <code>podman inspect oracle | grep IPAddress</code></p>
</li>
<li><p>Note the value of <strong>IPAddress</strong>, as that will be needed in the next step.</p>
</li>
</ol>
<h3 id="heading-create-the-directories-amp-files">Create the Directories &amp; Files</h3>
<p>Next, we need to create a pair of directories and a file on our local machine. This file will be used to seed ORDS with the connect information so that it can connect to the container database.</p>
<ol>
<li><p>Open up a new local terminal</p>
</li>
<li><p>Change directories to where you want to store the connect information for your container database. This should be somewhere relatively permanent on your local machine, as you will need to refer to this configuration data to restart the ORDS container from time to time.</p>
</li>
<li><p>Run the following commands:<br /> <code>mkdir ords_secrets ords_config</code><br /> <code>chmod 777 ords_config</code></p>
</li>
<li><p>Next, run the following command, replacing <code>[database container IP]</code> with the IP address of the database container from the previous section :<br /> <code>echo 'CONN_STRING=sys/oracle@[database container IP]:1521/freepdb1' &gt; ords_secrets/conn_string.txt</code></p>
</li>
</ol>
<p>This file will be deleted once the ORDS container is started, so there is little risk for the password being discovered.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">NOTE: If something goes wrong here and you had to re-created a new container database, be sure to remove all files from <code>/ords_config</code>. The files there will point to your old database IP, causing issues when trying to launch the ORDS container.</div>
</div>

<h3 id="heading-install-and-run-ords-container">Install and Run ORDS Container</h3>
<p>Now that everything is in place, let’s start the ORDS container. From the same terminal where you just ran the last commands, enter and run the following command:</p>
<pre><code class="lang-plaintext">podman run --rm --name ords -v `pwd`/ords_secrets/:/opt/oracle/variables -v `pwd`/ords_config/:/etc/ords/config/ -p 8181:8181 container-registry.oracle.com/database/ords-developer:latest &amp;
</code></pre>
<p>This will run for about a minute. Once the screen looks similar to this, it should be ready to test:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732478231803/c0b19e8b-789b-4bd2-bff8-959b56b09dda.png" alt class="image--center mx-auto" /></p>
<p>Looking at the Containers tab in Podman, we can see that our <strong>ords</strong> container is up and running:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732478434575/a02199df-50ca-42f4-a7de-117a4de8810f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-connecting-to-apex">Connecting to APEX</h2>
<p>If all went well, we should be able to connect to the Oracle REST Data Services landing page.</p>
<ol>
<li><p>Open up a new browser</p>
</li>
<li><p>Navigate to the following URL: <code>http://localhost:8181</code></p>
</li>
</ol>
<p>You should see the following page:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732478854996/8eb04f83-4dda-4c46-b612-9d9c3c9e072c.png" alt class="image--center mx-auto" /></p>
<ol start="3">
<li><p>Click on the “Go” button under the Oracle APEX region.</p>
</li>
<li><p>Enter <code>internal</code> for <strong>Workspace</strong>, <code>admin</code> for <strong>Username</strong> and the password that you set in the previous section via the <code>@apxchpwd</code> script for <strong>Password</strong> and click <strong>Sign In</strong>.</p>
</li>
</ol>
<p>Congratulations! You can now start to create workspaces and do anything else that’s possible with Oracle APEX.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732478980900/61395b1b-a2a0-4875-a88c-bde4b7a0915e.png" alt class="image--center mx-auto" /></p>
<p>Also - it’s OK to close the terminal that started the ORDS container at this point. The ORDS container will continue to run until it’s terminated manually.</p>
<h1 id="heading-summary">Summary</h1>
<p>Building a local instance offers many benefits to both casual and professional developers. It provides a place to learn new things, test existing ones and even build real software. With complete control and no cost, developers can spin these up and down all day long, and owe nothing to anyone.</p>
<p>Mac users have especially been waiting a while for a near-native Oracle Database, and with this new container and Podman, the wait is finally over.</p>
<h1 id="heading-references">References</h1>
<p>I referred to a few different sites for guidance when writing up this blog. They are:</p>
<ul>
<li><p>Oracle Database 23ai Free Edition Container</p>
<p>  <a target="_blank" href="https://container-registry.oracle.com/ords/ocr/ba/database/free">https://container-registry.oracle.com/ords/ocr/ba/database/free</a></p>
</li>
<li><p>Oracle RESTful Data Services Developer Container</p>
<p>  <a target="_blank" href="https://container-registry.oracle.com/ords/ocr/ba/database/ords-developer">https://container-registry.oracle.com/ords/ocr/ba/database/ords-developer</a></p>
</li>
<li><p>Launch your ORACLE APEX Instance in a Docker Container</p>
<p>  <a target="_blank" href="https://medium.com/@hamzaeraoui2000/lunch-your-oracle-apex-in-a-docker-container-22a16e27a6b3">https://medium.com/@hamzaeraoui2000/lunch-your-oracle-apex-in-a-docker-container-22a16e27a6b3</a></p>
</li>
</ul>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@osgcontainers?utm_source=Hashnode&amp;utm_medium=referral"><em>OSG Containers</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[Blog with a View: Update]]></title><description><![CDATA[Apparently, Hashnode has updated its GraphQL API - so much so that the view in my old post - Blog with a View - is no longer accurate. It will still run, but it won’t return any data.
Fortunately, the fix is pretty simple. Use this view instead of th...]]></description><link>https://spendolini.blog/blog-with-a-view-update</link><guid isPermaLink="true">https://spendolini.blog/blog-with-a-view-update</guid><category><![CDATA[orclapex]]></category><category><![CDATA[Oracle]]></category><category><![CDATA[Hashnode]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 19 Nov 2024 13:56:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733494849305/93188d27-61ef-4b4f-991f-2f843e259ec2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apparently, Hashnode has updated its GraphQL API - so much so that the view in my old post - <a target="_blank" href="https://spendolini.blog/blog-with-a-view">Blog with a View</a> - is no longer accurate. It will still run, but it won’t return any data.</p>
<p>Fortunately, the fix is pretty simple. Use this view instead of the one mentioned in the post, and it should still work:</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">view</span> blog_v
<span class="hljs-keyword">as</span>
<span class="hljs-keyword">with</span> <span class="hljs-keyword">json</span> <span class="hljs-keyword">as</span>
  (
  <span class="hljs-keyword">select</span>
    apex_web_service.make_rest_request
      (
       p_url=&gt; <span class="hljs-string">'https://gql.hashnode.com'</span>
      ,p_http_method =&gt; <span class="hljs-string">'POST'</span>
      ,p_body =&gt; <span class="hljs-string">'{ "query" : "query Publication '</span>
      || <span class="hljs-string">'          { publication(host: \"spendolini.blog\") '</span>
      || <span class="hljs-string">'            { id title posts(first: 10) '</span>
      || <span class="hljs-string">'              { edges '</span>
      || <span class="hljs-string">'                { node '</span>
      || <span class="hljs-string">'                  { id title subtitle url slug brief publishedAt reactionCount responseCount content { markdown } coverImage { url } '</span>
      || <span class="hljs-string">'                  } '</span>
      || <span class="hljs-string">'                } totalDocuments '</span>
      || <span class="hljs-string">'              } '</span>
      || <span class="hljs-string">'            } '</span>
      || <span class="hljs-string">'          }" '</span>
      || <span class="hljs-string">'        }'</span>
      ) <span class="hljs-keyword">as</span> val
  <span class="hljs-keyword">from</span>
    dual
  )
<span class="hljs-keyword">select</span>
   t.id
  ,to_timestamp(t.date_added, <span class="hljs-string">'YYYY-MM-DD"T"HH24:MI:SS.FF"Z"'</span>) <span class="hljs-keyword">as</span> date_added
  ,t.slug
  ,<span class="hljs-string">'https://spendolini.blog/'</span> || t.slug <span class="hljs-keyword">as</span> <span class="hljs-keyword">url</span>
  ,t.title
  ,t.brief
  ,t.cover_image
  ,t.popularity
  ,t.total_reactions
  ,t.content
<span class="hljs-keyword">from</span>
  <span class="hljs-keyword">json</span>,
  json_table
    (
      json.val
     ,<span class="hljs-string">'$.data.publication.posts.edges.node[*]'</span>
    <span class="hljs-keyword">columns</span>
      (
       <span class="hljs-keyword">id</span>              <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">1000</span>) <span class="hljs-keyword">path</span> <span class="hljs-string">'$.id'</span>
      ,slug            <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">1000</span>) <span class="hljs-keyword">path</span> <span class="hljs-string">'$.slug'</span>
      ,title           <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">1000</span>) <span class="hljs-keyword">path</span> <span class="hljs-string">'$.title'</span>
      ,brief           <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">1000</span>) <span class="hljs-keyword">path</span> <span class="hljs-string">'$.brief'</span>
      ,cover_image     <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">1000</span>) <span class="hljs-keyword">path</span> <span class="hljs-string">'$.coverImage.url'</span>
      ,date_added      <span class="hljs-built_in">varchar2</span>(<span class="hljs-number">1000</span>) <span class="hljs-keyword">path</span> <span class="hljs-string">'$.publishedAt'</span>
      ,popularity      <span class="hljs-built_in">number</span>         <span class="hljs-keyword">path</span> <span class="hljs-string">'$.popularity'</span>
      ,total_reactions <span class="hljs-built_in">number</span>         <span class="hljs-keyword">path</span> <span class="hljs-string">'$.reactionCount'</span>
      ,<span class="hljs-keyword">content</span>         <span class="hljs-keyword">clob</span>           <span class="hljs-keyword">path</span> <span class="hljs-string">'$.content.markdown'</span>
      )
    ) <span class="hljs-keyword">as</span> t;
</code></pre>
<p>There’s also a new <a target="_blank" href="https://gql.hashnode.com">GraphQL Playground</a> for Hashnode’s APIs that’s worth checking out.</p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@rocketcrocodileconsulting?utm_source=Hashnode&amp;utm_medium=referral"><em>David Kemptner-Rauscher</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item><item><title><![CDATA[A New Oracle Home]]></title><description><![CDATA[People are fleeing X in droves. There’s a couple reasons for this, none of which I’ll really get into here. Google it if you’re curious and not in the know…
So where are they going? It sure seems like Bluesky is the destination. In the last couple of...]]></description><link>https://spendolini.blog/a-new-oracle-home</link><guid isPermaLink="true">https://spendolini.blog/a-new-oracle-home</guid><category><![CDATA[Oracle]]></category><category><![CDATA[orclapex]]></category><category><![CDATA[Bluesky]]></category><dc:creator><![CDATA[Scott Spendolini]]></dc:creator><pubDate>Tue, 19 Nov 2024 00:06:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733495065770/9332fa81-6053-426c-a527-4ce3acd9651c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>People are fleeing X in droves. There’s a couple reasons for this, none of which I’ll really get into here. Google it if you’re curious and not in the know…</p>
<p>So where are they going? It sure seems like <a target="_blank" href="https://bsky.app">Bluesky</a> is the destination. In the last couple of weeks, there have been over 1 million new accounts created there per day. Check out <a target="_blank" href="https://bsky.jazco.dev/stats">this site</a> to see the specifics.</p>
<p>A portion of these millions of digital refugees are from the Oracle community. And form the discussions that have already happened on Bluesky, we are all overjoyed by the refreshing aura of our new home. There’s no ads, bots, trolls or anything else to get in the way of meaningful, spirited and even fun discussions. Just like Twitter used to be a long time ago.</p>
<h2 id="heading-starter-packs">Starter Packs</h2>
<p>If you’re ready to make the jump, check out some of these Starter Packs. A Starter Pack is a curated list of people that you can follow with a single click. This makes it easy to populate your feed in just seconds - something that typically takes time and effort on any social platform.</p>
<p>Here’s a list of a few to get you started:</p>
<ul>
<li><p><a target="_blank" href="https://go.bsky.app/6kaBDaf">Oracle APEX Starter Pack</a></p>
</li>
<li><p><a target="_blank" href="https://go.bsky.app/MZjZDjE">Oracle ACE Members</a></p>
</li>
<li><p><a target="_blank" href="https://go.bsky.app/E5TV5rS">Oracle Database Product Managers</a></p>
</li>
<li><p><a target="_blank" href="https://go.bsky.app/FvKfw4q">Oracle Folk</a></p>
</li>
</ul>
<p>You can use any or all of them once you create an account. In fact, if you don’t have an account, just click any of the links and it will walk you through the process of creating one and adding that Starter Pack!</p>
<h2 id="heading-a-new-hope">A New Hope</h2>
<p>With better blocking and moderation tools, Bluesky seems to be the new place to be. Whether it’s for Oracle or any other interest, the tone of the conversation is refreshingly decent, and those who try to disrupt this can quickly and easily be ignored. And yes, of course <a target="_blank" href="https://bsky.app/profile/markhamillofficial.bsky.social">Mark Hamill</a> already has an account there!</p>
<p><a target="_blank" href="https://bsky.app/profile/spendolini.blog"><em>Follow me on Bluesky</em></a></p>
<hr />
<p><em>Title Photo by</em> <a target="_blank" href="https://unsplash.com/@oskaycaglar?utm_source=Hashnode&amp;utm_medium=referral"><em>Çağlar Oskay</em></a> <em>on Unsplash</em></p>
]]></content:encoded></item></channel></rss>