Evolution of a SQL Domain for Semantic Versioning

Evolution of a SQL Domain for Semantic Versioning

1. Introduction

In my current project, I use a SQL domain to implement the formatting and precedence rules for Semantic Versioning. I started with a simple implementation covering only the most basic rules. Getting the sorting right is key in my project. It allows me to identify the latest compatible version of an artefact. After adding some tests I started to evolve the functionality to a point where I can say that the implementation covers everything needed for my use case.

Domains make data models easier to understand and reduce the logic regarding data consistency and visualisation. In this blog post, I demonstrate this by evolving a domain for Semantic Versioning. This should give you an impression of whether keeping domains and table columns in sync is more complicated than traditional approaches.

2. What Are Domains?

A data use case domain (aka SQL domain, aka domain) is a new feature in the Oracle Database 23ai based on the SQL standard. Domains abstract column properties so that they can be used across multiple tables. They come in different flavours:

  • Single-column domain (constraints for a single column, enums, display and order expressions)
  • Multi-column domain (constraints across multiple columns, display and order expressions)
  • Flexible domain (abstract domain, delegating functionality to concrete domains via discriminator columns)

It’s important to notice, that a table column can have only one domain. It is therefore not possible to combine single-column domains with multi-column or flexible domains.

3. Starting Model

3.1 Domains

First, let’s create three domains.

1) create domains app_identifier, app_file_name and app_semantic_version
create domain if not exists app_identifier
   as raw(16) strict
   display lower(substr(rawtohex(app_identifier), 1, 8)
           || '-' || substr(rawtohex(app_identifier), 9, 4)
           || '-' || substr(rawtohex(app_identifier), 13, 4)
           || '-' || substr(rawtohex(app_identifier), 17, 4)
           || '-' || substr(rawtohex(app_identifier), 21, 12));

create domain if not exists app_file_name
   as varchar2(128 char) strict;

create domain if not exists app_semantic_version
   as varchar2(20 byte) strict
   -- naïve, incomplete implementation of semantic versioning
   constraint app_semantic_version_has_major_minor_patch_ck
      check (regexp_like(app_semantic_version, '\d{1,6}\.\d{1,6}\.\d{1,6}'))
   order to_char(to_number(regexp_substr(app_semantic_version, '\d+', 1, 1)), 'FM000000')
      || '.' || to_char(to_number(regexp_substr(app_semantic_version, '\d+', 1, 2)), 'FM000000')
      || '.' || to_char(to_number(regexp_substr(app_semantic_version, '\d+', 1, 3)), 'FM000000');
Domain APP_IDENTIFIER created.


Domain APP_FILE_NAME created.


Domain APP_SEMANTIC_VERSION created.

The domain app_identifier defines the data type for a GUID-based identifier. It also defines the display format.

The domain app_file_name defines only the data type used for file names.

The domain app_semantic_version is an incomplete implementation of Semantic Versioning 2.0.0 (we will fix that later). It defines the data type, a check constraint app_semantic_version_has_major_minor_patch_ck and an order expression.

3.2 Table

Let’s create a table using all these domains and insert a few rows.

2) create table with some data
create table if not exists app_files (
    file_id      app_identifier       default sys_guid() not null,
    file_name    app_file_name                           not null,
    file_version app_semantic_version                    not null,
    constraint app_files_pk primary key (file_id),
    constraint app_files_uk1 unique (file_name, file_version)
);

desc app_files

insert into app_files
   (file_name, file_version)
values
   ('file1.txt', '1.9.0'),
   ('file1.txt', '1.10.0'),
   ('file1.txt', '1.11.0'),
   ('file1.txt', '2.0.0'),
   ('file2.txt', '0.0.7'),
   ('file2.txt', '0.0.42');
Table APP_FILES created.

Name         Null?    Type                                     
------------ -------- ---------------------------------------- 
FILE_ID      NOT NULL RAW(16 BYTE) DOMAIN APP_IDENTIFIER       
FILE_NAME    NOT NULL VARCHAR2(128 CHAR) DOMAIN APP_FILE_NAME  
FILE_VERSION NOT NULL VARCHAR2(20) DOMAIN APP_SEMANTIC_VERSION 

6 rows inserted.

Look at the result of the desc app_files command. Each column has a concrete data type inherited from the domain, as well as the domain associated with it.

3.3 Demo

Now, let’s query the data to demonstrate the domain functionality.

3) demo domain functionality
column file_name format a9
select domain_display(file_id) as file_id,
       file_name,
       file_version,
       domain_order(file_version) as file_version_order
  from app_files
 order by file_name, file_version_order desc;
FILE_ID                              FILE_NAME FILE_VERSION         FILE_VERSION_ORDER     
------------------------------------ --------- -------------------- -----------------------
29ed8fd9-57c7-4b4d-e063-03e0a8c091f8 file1.txt 2.0.0                000002.000000.000000   
29ed8fd9-57c6-4b4d-e063-03e0a8c091f8 file1.txt 1.11.0               000001.000011.000000   
29ed8fd9-57c5-4b4d-e063-03e0a8c091f8 file1.txt 1.10.0               000001.000010.000000   
29ed8fd9-57c4-4b4d-e063-03e0a8c091f8 file1.txt 1.9.0                000001.000009.000000   
29ed8fd9-57c9-4b4d-e063-03e0a8c091f8 file2.txt 0.0.42               000000.000000.000042   
29ed8fd9-57c8-4b4d-e063-03e0a8c091f8 file2.txt 0.0.7                000000.000000.000007   

6 rows selected. 

You see the file version is sorted according to the specification in 11.2:

“Precedence is determined by the first difference when comparing each of these identifiers from left to right as follows: Major, minor, and patch versions are always compared numerically.”

4. Test

The semantic versioning specification covers a lot of ground that our simple implementation can barely cover. So let’s test it, to find out what is missing. We use utPLSQL, of course.

4.1 Define Test

Let’s create a utPLSQL test PL/SQL package.

4) utPLSQL test package
create or replace package test_app_domains is
   --%suite

   --%test
   procedure test_app_semantic_version;
end test_app_domains;
/

create or replace package body test_app_domains is
   procedure test_app_semantic_version is
      c_actual   sys_refcursor;
      c_expected sys_refcursor;
   begin
      open c_actual for
         select input_col,
                to_char(domain_check(app_semantic_version, input_col)) as test_check,
                case when domain_check(app_semantic_version, input_col) then domain_order(cast(input_col as app_semantic_version)) end as test_order
           from (values
                   -- https://semver.org/#spec-item-2
                   ('1.9.0'),
                   ('1.10.0'),
                   ('1.11.0'),
                   -- https://semver.org/#spec-item-9
                   ('1.0.0-alpha'),
                   ('1.0.0-alpha.1'),
                   ('1.0.0-0.3.7'),
                   ('1.0.0-x.7.z.92'),
                   ('1.0.0-x-y-z.--'),
                   -- https://semver.org/#spec-item-10
                   ('1.0.0-alpha+001'),
                   ('1.0.0+20130313144700'),
                   ('1.0.0-beta+exp.sha.5114f85'),
                   ('1.0.0+21AF26D3----117B344092BD'),
                   -- https://semver.org/#spec-item-11
                   ('1.0.0'),
                   ('2.0.0'),
                   ('2.1.0'),
                   ('2.1.1'),
                   ('1.0.0-alpha'),
                   ('1.0.0-alpha.1'),
                   ('1.0.0-alpha.beta'),
                   ('1.0.0-beta'),
                   ('1.0.0-beta.2'),
                   ('1.0.0-beta.11'),
                   ('1.0.0-rc.1'),
                   -- invalid
                   ('01.0.0'),
                   ('1.00.0'),
                   ('1.0.00'),
                   ('1.0.x')
                ) as t(input_col);
      open c_expected for
         select *
           from (values
                   -- https://semver.org/#spec-item-2
                   ('1.9.0',                          'TRUE',  '000001.000009.000000(1)'),
                   ('1.10.0',                         'TRUE',  '000001.000010.000000(1)'),
                   ('1.11.0',                         'TRUE',  '000001.000011.000000(1)'),
                   -- https://semver.org/#spec-item-9
                   ('1.0.0-alpha',                    'TRUE',  '000001.000000.000000(0)-alpha'),
                   ('1.0.0-alpha.1',                  'TRUE',  '000001.000000.000000(0)-alpha.000001'),
                   ('1.0.0-0.3.7',                    'TRUE',  '000001.000000.000000(0)-000000.000003.000007'),
                   ('1.0.0-x.7.z.92',                 'TRUE',  '000001.000000.000000(0)-x.000007.z.000092'),
                   ('1.0.0-x-y-z.--',                 'TRUE',  '000001.000000.000000(0)-x-y-z.--'),
                   -- https://semver.org/#spec-item-10
                   ('1.0.0-alpha+001',                'TRUE',  '000001.000000.000000(0)-alpha'),
                   ('1.0.0+20130313144700',           'TRUE',  '000001.000000.000000(1)'),
                   ('1.0.0-beta+exp.sha.5114f85',     'TRUE',  '000001.000000.000000(0)-beta'),
                   ('1.0.0+21AF26D3----117B344092BD', 'TRUE',  '000001.000000.000000(1)'),
                   -- https://semver.org/#spec-item-11
                   ('1.0.0',                          'TRUE',  '000001.000000.000000(1)'),
                   ('2.0.0',                          'TRUE',  '000002.000000.000000(1)'),
                   ('2.1.0',                          'TRUE',  '000002.000001.000000(1)'),
                   ('2.1.1',                          'TRUE',  '000002.000001.000001(1)'),
                   ('1.0.0-alpha',                    'TRUE',  '000001.000000.000000(0)-alpha'),
                   ('1.0.0-alpha.1',                  'TRUE',  '000001.000000.000000(0)-alpha.000001'),
                   ('1.0.0-alpha.beta',               'TRUE',  '000001.000000.000000(0)-alpha.beta'),
                   ('1.0.0-beta',                     'TRUE',  '000001.000000.000000(0)-beta'),
                   ('1.0.0-beta.2',                   'TRUE',  '000001.000000.000000(0)-beta.000002'),
                   ('1.0.0-beta.11',                  'TRUE',  '000001.000000.000000(0)-beta.000011'),
                   ('1.0.0-rc.1',                     'TRUE',  '000001.000000.000000(0)-rc.000001'),
                   --invalid
                   ('01.0.0',                         'FALSE', null),
                   ('1.00.0',                         'FALSE', null),
                   ('1.0.00',                         'FALSE', null),
                   ('1.0.x',                          'FALSE', null)
                ) as t(input_col, test_check, test_order);
      ut.expect(c_actual).to_equal(c_expected).join_by('INPUT_COL');
   end test_app_semantic_version;
end test_app_domains;
/
Package TEST_APP_DOMAINS compiled


Package Body TEST_APP_DOMAINS compiled 

The test compares the cursors for the actual and expected results.

You will get a compile error for the package body if you do not have the utPLSQL framework installed on your database instance. See the utPLSQL installation guide for information on how to install utPLSQL. It’s quite simple and works on an Oracle Database 23ai.

4.2 Run Test

Now let’s run the test.

5) run utPLSQL test
set serveroutput on size unlimited;
exec ut.run;
test_app_domains
  test_app_semantic_version [.087 sec] (FAILED - 1)
 
Failures:
 
  1) test_app_semantic_version
      Actual: refcursor [ count = 27 ] was expected to equal: refcursor [ count = 27 ]
      Diff:
      Rows: [ 30 differences, showing first 20 ]
        PK <INPUT_COL>1.9.0</INPUT_COL> - Actual:   <TEST_ORDER>000001.000009.000000</TEST_ORDER>
        PK <INPUT_COL>1.9.0</INPUT_COL> - Expected: <TEST_ORDER>000001.000009.000000(1)</TEST_ORDER>
        PK <INPUT_COL>1.10.0</INPUT_COL> - Actual:   <TEST_ORDER>000001.000010.000000</TEST_ORDER>
        PK <INPUT_COL>1.10.0</INPUT_COL> - Expected: <TEST_ORDER>000001.000010.000000(1)</TEST_ORDER>
        PK <INPUT_COL>1.11.0</INPUT_COL> - Actual:   <TEST_ORDER>000001.000011.000000</TEST_ORDER>
        PK <INPUT_COL>1.11.0</INPUT_COL> - Expected: <TEST_ORDER>000001.000011.000000(1)</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-alpha</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-alpha</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha.1</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha.1</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-alpha.000001</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha.1</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha.1</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-alpha.000001</TEST_ORDER>
        PK <INPUT_COL>1.0.0-0.3.7</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-0.3.7</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-000000.000003.000007</TEST_ORDER>
        PK <INPUT_COL>1.0.0-x.7.z.92</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-x.7.z.92</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-x.000007.z.000092</TEST_ORDER>
        PK <INPUT_COL>1.0.0-x-y-z.--</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-x-y-z.--</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-x-y-z.--</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha+001</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha+001</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0)-alpha</TEST_ORDER>
        PK <INPUT_COL>1.0.0+20130313144700</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0+20130313144700</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(1)</TEST_ORDER>
        PK <INPUT_COL>1.0.0-beta+exp.sha.5114f85</INPUT_COL> - Actual:   <TEST_CHECK>FALSE</TEST_CHECK><TEST_ORDER/>
        PK <INPUT_COL>1.0.0-beta+exp.sha.5114f85</INPUT_COL> - Expected: <TEST_CHECK>TRUE</TEST_CHECK><TEST_ORDER>000001.000000.000000(0)-beta</TEST_ORDER>
        PK <INPUT_COL>1.0.0+21AF26D3----117B344092BD</INPUT_COL> - Actual:   <TEST_CHECK>FALSE</TEST_CHECK><TEST_ORDER/>
        PK <INPUT_COL>1.0.0+21AF26D3----117B344092BD</INPUT_COL> - Expected: <TEST_CHECK>TRUE</TEST_CHECK><TEST_ORDER>000001.000000.000000(1)</TEST_ORDER>
        PK <INPUT_COL>1.0.0</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(1)</TEST_ORDER>
        PK <INPUT_COL>2.0.0</INPUT_COL> - Actual:   <TEST_ORDER>000002.000000.000000</TEST_ORDER>
        PK <INPUT_COL>2.0.0</INPUT_COL> - Expected: <TEST_ORDER>000002.000000.000000(1)</TEST_ORDER>
        PK <INPUT_COL>2.1.0</INPUT_COL> - Actual:   <TEST_ORDER>000002.000001.000000</TEST_ORDER>
        PK <INPUT_COL>2.1.0</INPUT_COL> - Expected: <TEST_ORDER>000002.000001.000000(1)</TEST_ORDER>
        PK <INPUT_COL>2.1.1</INPUT_COL> - Actual:   <TEST_ORDER>000002.000001.000001</TEST_ORDER>
        PK <INPUT_COL>2.1.1</INPUT_COL> - Expected: <TEST_ORDER>000002.000001.000001(1)</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha</INPUT_COL> - Actual:   <TEST_ORDER>000001.000000.000000</TEST_ORDER>
        PK <INPUT_COL>1.0.0-alpha</INPUT_COL> - Expected: <TEST_ORDER>000001.000000.000000(0[...]
      at "DEMO42.TEST_APP_DOMAINS.TEST_APP_SEMANTIC_VERSION", line 80 ut.expect(c_actual).to_equal(c_expected).join_by('INPUT_COL');
       
Finished in .091311 seconds
1 tests, 1 failed, 0 errored, 0 disabled, 0 warning(s)
 


PL/SQL procedure successfully completed.

The highlighted lines for 1.0.0-alpha+001 explain the problems with our current implementation, which are:

  1. Missing support for pre-release labels (-alpha)
  2. Missing support for build metadata (+001, leading zeros are allowed and preserved)
  3. Missing precedence handling between releases and pre-releases (releases (1) are newer than pre-releases (0))
  4. Missing precedence handling for pre-releases (numeric qualifiers to be compared numerically)
  5. Missing precedence handling for build metadata (to be ignored)

5. Fix

5.1 Drop domain

Before deploying a new version of the domain, we have to drop the existing one. alter domain is not applicable because we need to change the length of the data type and a create or replace syntax variant does not exist for domains.

6) drop domain
drop domain if exists app_semantic_version;
Error starting at line : 1 in command -
drop domain if exists app_semantic_version
Error report -
ORA-11502: The domain APP_SEMANTIC_VERSION to be dropped has dependent objects.

https://docs.oracle.com/error-help/db/ora-11502/11502. 0000 -  "The domain %s to be dropped has dependent objects."
*Cause:    An attempt is made to drop a domain with dependent objects.
*Action:   Drop the domain using the FORCE mode

Okay, that didn’t work, but the error message is good. It lets us know what to do.

So let’s try again with the force option.

7) drop domain with force option
drop domain if exists app_semantic_version force;
Domain APP_SEMANTIC_VERSION dropped.
5.2 Recreate domain

Now we can deploy the fixed variant of the domain.

8) recreate domain
create domain if not exists app_semantic_version
   as varchar2(60 byte) strict
   -- valid examples: '0.13.0', '23.5.0', '123456.789012.345678'
   -- valid pre-release examples: '1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-0.3.7', '1.0.0-x.7.z.92'
   -- valid build metadata examples: '1.0.0+20130313144700', '1.0.0-beta+exp.sha.5114f85'
   -- use suggested regex from https://semver.org/spec/v2.0.0.html#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string without non-capturing groups (?:)
   constraint app_semantic_version_has_major_minor_patch_ck
      check (regexp_like(app_semantic_version, '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$'))
   -- add leading zeroes to major, minor, patch and numeric qualifiers in pre-release for sorting (normalized semantic version)
   -- supports up to 6 digits for major, minor, patch and numeric qualifiers in pre-release
   order to_char(to_number(regexp_substr(app_semantic_version, '\d+', 1, 1)), 'FM000000')
      || '.' || to_char(to_number(regexp_substr(app_semantic_version, '\d+', 1, 2)), 'FM000000')
      || '.' || to_char(to_number(regexp_substr(app_semantic_version, '\d+', 1, 3)), 'FM000000')
      -- sort pre-release versions (0) before final versions (1)
      -- build metadata is ignored for sorting, they have the same precedence according to the specification
      || case
            when instr(app_semantic_version, '-') > 0
               and (instr(app_semantic_version, '+') = 0 or instr(app_semantic_version, '-') < instr(app_semantic_version, '+'))
            then
               -- sort pre-release according the qualifiers after the hyphen, ignoring build metadata, add leading zeroes to qualifiers starting with a number
               '(0)' || regexp_replace( --  workaround part 2: remove superfluous leading zeroes
                           regexp_replace( -- workaround part 1: add 6 leading zeroes to numeric qualifiers
                              regexp_replace( -- remove build metadata
                                 substr(app_semantic_version, instr(app_semantic_version, '-')),
                                 '\+.+$',
                                 null
                              ),
                              '(\.|\-|^)(\d{1,6})',
                              '\1@000000\2@' -- workaround since \2 in lpad, to_char is not evaluated before calling the function
                           ),
                           '@[0]+(\d{6})@',
                           '\1'
                        )
            else
               '(1)'
         end;
Domain APP_SEMANTIC_VERSION created.

The details of the fix are not so important, besides the fact that the data type changed from varchar2(20 bytes) to varchar2(60 bytes).

This variant should fully support Semantic Versioning 2.0 as long as numeric qualifiers do not require more than 6 digits. This is good enough for my use case.

5.3 Re-test

It’s now time to re-run the previously failed utPLSQL test.

9) re-run utPLSQL test
set serveroutput on size unlimited;
exec ut.run;
test_app_domains
  test_app_semantic_version [.02 sec]
 
Finished in .021919 seconds
1 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)
 


PL/SQL procedure successfully completed.

Looks good.

5.4 Check Table

Now let’s look at the structure of the app_files table.

10) describe app_files before change
desc app_files
Name         Null?    Type                                    
------------ -------- --------------------------------------- 
FILE_ID      NOT NULL RAW(16 BYTE) DOMAIN APP_IDENTIFIER      
FILE_NAME    NOT NULL VARCHAR2(128 CHAR) DOMAIN APP_FILE_NAME 
FILE_VERSION NOT NULL VARCHAR2(20)                            

The file_version column has a length of 20 instead of 60 bytes, and the association with the app_semantic_version domain is missing.

5.5 Modify Table

We can fix the column length and associate the column with a domain in one go.

11) modify table app_files and describe it
alter table app_files
   modify (file_version varchar2(60 byte) domain app_semantic_version);

desc app_files
Table APP_FILES altered.

Name         Null?    Type                                     
------------ -------- ---------------------------------------- 
FILE_ID      NOT NULL RAW(16 BYTE) DOMAIN APP_IDENTIFIER       
FILE_NAME    NOT NULL VARCHAR2(128 CHAR) DOMAIN APP_FILE_NAME  
FILE_VERSION NOT NULL VARCHAR2(60) DOMAIN APP_SEMANTIC_VERSION                        

This looks good and was quite easy.

Let’s query the data in this table using the new domain variant.

12) query app_files
column file_name format a9
column file_version format a12
column file_version_order format a25
select domain_display(file_id) as file_id,
       file_name,
       file_version,
       domain_order(file_version) as file_version_order
  from app_files
 order by file_name, file_version_order desc;
FILE_ID                              FILE_NAME FILE_VERSION FILE_VERSION_ORDER       
------------------------------------ --------- ------------ -------------------------
29ed8fd9-57c7-4b4d-e063-03e0a8c091f8 file1.txt 2.0.0        000002.000000.000000(1)  
29ed8fd9-57c6-4b4d-e063-03e0a8c091f8 file1.txt 1.11.0       000001.000011.000000(1)  
29ed8fd9-57c5-4b4d-e063-03e0a8c091f8 file1.txt 1.10.0       000001.000010.000000(1)  
29ed8fd9-57c4-4b4d-e063-03e0a8c091f8 file1.txt 1.9.0        000001.000009.000000(1)  
29ed8fd9-57c9-4b4d-e063-03e0a8c091f8 file2.txt 0.0.42       000000.000000.000042(1)  
29ed8fd9-57c8-4b4d-e063-03e0a8c091f8 file2.txt 0.0.7        000000.000000.000007(1)  

6 rows selected. 

The only visible change is the release version indicator –(1) – at the end of the file_version_order column. We need it when we have pre-release versions to determine the correct order.

Let’s add some pre-release versions and versions with build metadata and query the table again.

13) insert pre-release versions and re-query
insert into app_files
   (file_name, file_version)
values
   ('file3.txt', '1.0.0-beta'),
   ('file3.txt', '1.0.0-beta.2'),
   ('file3.txt', '1.0.0-beta.11.2.3.4'),
   ('file3.txt', '1.0.0-beta+exp.sha.5114f85'),
   ('file3.txt', '1.0.0+21AF26D3----117B344092BD');

column file_name format a9
column file_version format a30
column file_version_order format a57
select domain_display(file_id) as file_id,
       file_name,
       file_version,
       domain_order(file_version) as file_version_order
  from app_files
 order by file_name, file_version_order desc;
5 rows inserted.


FILE_ID                              FILE_NAME FILE_VERSION                   FILE_VERSION_ORDER                                       
------------------------------------ --------- ------------------------------ ---------------------------------------------------------
29ed8fd9-57c7-4b4d-e063-03e0a8c091f8 file1.txt 2.0.0                          000002.000000.000000(1)                                  
29ed8fd9-57c6-4b4d-e063-03e0a8c091f8 file1.txt 1.11.0                         000001.000011.000000(1)                                  
29ed8fd9-57c5-4b4d-e063-03e0a8c091f8 file1.txt 1.10.0                         000001.000010.000000(1)                                  
29ed8fd9-57c4-4b4d-e063-03e0a8c091f8 file1.txt 1.9.0                          000001.000009.000000(1)                                  
29ed8fd9-57c9-4b4d-e063-03e0a8c091f8 file2.txt 0.0.42                         000000.000000.000042(1)                                  
29ed8fd9-57c8-4b4d-e063-03e0a8c091f8 file2.txt 0.0.7                          000000.000000.000007(1)                                  
29fb5aeb-44d7-39e2-e063-03e0a8c01f4e file3.txt 1.0.0+21AF26D3----117B344092BD 000001.000000.000000(1)                                  
29fb5aeb-44d5-39e2-e063-03e0a8c01f4e file3.txt 1.0.0-beta.11.2.3.4            000001.000000.000000(0)-beta.000011.000002.000003.000004 
29fb5aeb-44d4-39e2-e063-03e0a8c01f4e file3.txt 1.0.0-beta.2                   000001.000000.000000(0)-beta.000002                      
29fb5aeb-44d3-39e2-e063-03e0a8c01f4e file3.txt 1.0.0-beta                     000001.000000.000000(0)-beta                             
29fb5aeb-44d6-39e2-e063-03e0a8c01f4e file3.txt 1.0.0-beta+exp.sha.5114f85     000001.000000.000000(0)-beta                             

11 rows selected. 

Look at the five highlighted rows. The pre-release labels are considered in the file_version_order column but the build metadata is ignored.

Now we can easily determine the latest version of a file.

14) query latest versions of all files
select file_Name, file_version as latest_file_version
  from (
          select file_name,
                 file_version,
                 domain_order(file_version) as current_version,
                 max(domain_order(file_version)) over (partition by file_name) as max_version
            from app_files
       )
 where current_version = max_version
 order by file_name;
FILE_NAME LATEST_FILE_VERSION                                         
--------- ------------------------------------------------------------
file1.txt 2.0.0                                                       
file2.txt 0.0.42                                                      
file3.txt 1.0.0+21AF26D3----117B344092BD                                     

The domain nicely hides the implementation details of the semantic versioning precedence rules.

6. Alternatives to Domains

The following table shows the domain features and their alternatives within the Oracle Database.

Domain FeatureAlternative
❌ Data type of a domain column❌ Data type of a table column
❌ Enums🤔 Lookup table
❌ Constraint on single-column domain❌ Table constraint
❌ Constraint on multi-column domain❌ Table constraint
❌ Flexible domain❌ Table constraint
❌ Validate JSON column against a schema❌ Table constraint
❌ Collation❌ Table column collation
✅ Annotation✅ Table column annotation
✅ Display expression✅ Function (standalone, package, type)
✅ Order expression✅ Function (standalone, package, type)

The emojis have the following meanings:

  • ✅ A change is possible without impacting the underlying table/data (e.g. alter domain is applicable)
  • 🤔 A change might have an impact on the underlying table/data (e.g. removing/adding enum item)
  • ❌ A change will have an impact on the underlying table/data (e.g. alter table or data migration)

The main difference between the domain features and their alternatives is that domains provide an abstraction in a standardised way. This should make the models easier to understand and therefore easier to maintain.

7. Conclusion

A tough part of evolving data structures is changing data types. Domains neither simplify nor complicate this. You only use an alternative series of statements for a change.

However, reassociating domains with columns can become tedious for domains used in many columns. It’s a good idea to save the usage before dropping a domain. This is certainly a useful area for annotations and helper scripts. Perhaps future versions of the Oracle Database will allow us to disable domain associations instead of dropping them. Similar to constraints. This would be very helpful in preventing the loss of important information and would relieve us of the burden of managing additional metadata.

Furthermore, the lack of support for domains in PL/SQL or virtual columns limits its usefulness. Hopefully, future versions will address these shortcomings.

Nevertheless, I like the idea of domains and will try to apply the following principles in new projects:

  • Use strict single-column domains instead of raw data types in tables whenever possible. This ensures the consistent use of data types (e.g., for identifiers, names, descriptions, etc.).
  • Do not use multi-column domains, as columns can only be associated with one domain. Also, do not use flexible domains (IMO we should avoid designs with discriminator columns).
  • Favour traditional lookup tables over enums (and provide the data as part of the application). Enums become appealing for small and static reference data once domains are supported in PL/SQL.
  • Define a display_expression for raw and string columns, if the data should not be presented “as is” (e.g. GUID).
  • Define an order_expression for columns that have a non-default precedence (e.g. semantic version).

Ask me in two or three years whether this was a good idea.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.