2023-06-29

SOURCE_ROUTE in easy connect plus syntax

I regularly use Oracle connection manager (CMAN) to simplify connectivity to the database in complex IT environments. Normally the network infrastructure between the CMAN host and the target service is transparent for the client. 

but in some cases, the client can (or need) to define not only the connection to the CMAN, but also the 2nd leg to the DB service. This is called SOURCE_ROUTE in Oracles network naming. 

A common (quite simple) connection description is this tnsnames.ora entry: 


ALIAS=
 (DESCRIPTION= 
   (SOURCE_ROUTE=yes) 
   (ADDRESS=(PROTOCOL=tcp)(HOST=CMAN_HOST)(PORT=1521))  
   (ADDRESS=(PROTOCOL=tcp)(HOST=DB_INSTANCE_HOST)(PORT=1521))  
   (CONNECT_DATA=(SERVICE_NAME=DB_Service))
 )

By the definition SOURCE_ROUTE=yes the first entry defines the direct connection to the CMAN_HOST. In this connection the 2nd connection is described implicit to go to DB_INSTANCE_HOST

 

But sometimes, a tnsnames.ora entry isn't useful and a Easy Connect syntax is preferred. The simplest syntax does not show any way for a multi step connection. 



host:port/SERVICE_NAME


But this very simple syntax


the full possible syntax is

[[protocol:]//]host1{,host2}[:port1]{,host2:port2}[/[service_name][:server_type][/instance_name]][?parameter_name1=value1{&parameter_name2= value2}]
In a more specific case it looks like

//CMAN_HOST,DB_INSTANCE_HOST:1521/DB_Service?SOURCE_ROUTE=YES
And it works quite fine. I can do a simple test with sqlplus:

sqlplus x/x@CMAN_HOST,DB_INSTANCE_HOST:1521/DB_Service?SOURCE_ROUTE=YES

SQL*Plus: Release 21.0.0.0.0 - Production on Mon Jun 26 10:54:02 2023
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.

ERROR:
ORA-01017: invalid username/password; logon denied
And ORA-01017 is exactly what I expect: I could reach the instance, but of course authentication for user x with password x failed.

Another feature worth to mention is tnsping. This only tries to connect to the given listener but never checks for the service available. In addition it also prints the connection it tested in tnsnames.ora format. A simple connect string looks like: 

tnsping CMAN_HOST:1521/DB_Service

TNS Ping Utility for Linux: Version 21.0.0.0.0 - Production on 26-JUN-2023 10:53:44

Copyright (c) 1997, 2021, Oracle.  All rights reserved.

Used parameter files:
/tns/admin/sqlnet.ora

Used EZCONNECT adapter to resolve the alias
Attempting to contact (DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=DB_Service))(ADDRESS=(PROTOCOL=tcp)(HOST=10.0.0.2)(PORT=1521)))
OK (10 msec)
But even with a source route defined, tnsping is quite honest what's (not) doing: 


tnsping CMAN_HOST:1521,DB_INSTANCE_HOST:1521/DB_Service?SOURCE_ROUTE=YES

TNS Ping Utility for Linux: Version 21.0.0.0.0 - Production on 26-JUN-2023 10:53:51

Copyright (c) 1997, 2021, Oracle.  All rights reserved.

Used parameter files:
/tns/admin/sqlnet.ora

Used EZCONNECT adapter to resolve the alias
Attempting to contact (DESCRIPTION=(CONNECT_DATA=(SERVICE_NAME=DB_Service))(SOURCE_ROUTE=YES)(ADDRESS=(PROTOCOL=tcp)(HOST=10.0.0.2)(PORT=1521)))
OK (10 msec)
It adds the SOURCE_ROUTE=YES entries, but does not add the 2nd host (or IP) it doesn't even try to reach the DB_INSTANCE_HOST

The usage of tnsping is limited in any cman environment. 

2023-06-08

Coding with ChatGTP - a first try

 A lot is said already about the power and limitations of current AIs like ChatGTP
During a lazy afternoon I wanted to see it's capabilities and limitations in an easy (at least for me to describe) but real world situation. I try to explain my observations and findings while following my conversation with ChatGTP. The goal is to understand the whole process and learn about it. Any kind of blame or finger pointing isn't the intention.

My goal is to get a function like inet_aton in PL/SQL. This function converts an IPv4 address from any human readable form to it's true nature: a number. It might sound easy to convert something like 127.0.0.1 to it's number by some multiplies of 256, but there are more complicated cases like hex and octal notation, and even a compact notation: 0x7f.1 is also a valid representation. 


So my conversation starts: 

can you write an equivalent of inet_aton in Oracle PL/SQL language? 

Certainly! In Oracle PL/SQL, you can create a function that mimics the functionality of inet_aton, which converts an IPv4 address from its dotted-quad notation to a 32-bit integer. Here's an example implementation:





CREATE OR REPLACE FUNCTION inet_aton(ip_address VARCHAR2) RETURN NUMBER IS
  ip_parts SYS.ODCIVARCHAR2LIST;
  ip_number NUMBER := 0;
BEGIN
  -- Split the IP address into its four parts
  ip_parts := SYS.ODCIVARCHAR2LIST(SUBSTR(ip_address, 1, INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 1) + 1, INSTR(ip_address, '.', 1, 2) - INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 2) + 1, INSTR(ip_address, '.', 1, 3) - INSTR(ip_address, '.', 1, 2) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 3) + 1));

  -- Convert each part to a number and calculate the final IP number
  ip_number := (TO_NUMBER(ip_parts(1)) * 256 * 256 * 256) +
               (TO_NUMBER(ip_parts(2)) * 256 * 256) +
               (TO_NUMBER(ip_parts(3)) * 256) +
               TO_NUMBER(ip_parts(4));

  RETURN ip_number;
END;
/
This is a nice answer for "normal" cases. But I want to challenge the AI a little bit: 

in the manpage to inet_aton I can see it also handles octal and hex notation. Especially 0x7f.1 is mentioned there. Can you extend the function so it can handle those cases also? 

Certainly! I apologize for the oversight. Here's an extended version of the inet_aton function that can handle octal and hex notation as well:





CREATE OR REPLACE FUNCTION inet_aton(ip_address VARCHAR2) RETURN NUMBER IS
  ip_parts SYS.ODCIVARCHAR2LIST;
  ip_number NUMBER := 0;
  base NUMBER := 10; -- Default base is decimal

  FUNCTION convertToNumber(ip_part VARCHAR2) RETURN NUMBER IS
  BEGIN
    -- Determine the base of the IP part
    IF ip_part LIKE '0x%' THEN
      base := 16; -- Hexadecimal
      ip_part := SUBSTR(ip_part, 3); -- Remove the '0x' prefix
    ELSIF ip_part LIKE '0%' THEN
      base := 8; -- Octal
      ip_part := SUBSTR(ip_part, 2); -- Remove the leading '0'
    ELSE
      base := 10; -- Decimal
    END IF;

    -- Convert the IP part to a number based on the determined base
    RETURN TO_NUMBER(ip_part, 'FM' || CASE base WHEN 8 THEN '990' WHEN 16 THEN '990X' ELSE '999' END);
  END;

BEGIN
  -- Split the IP address into its four parts
  ip_parts := SYS.ODCIVARCHAR2LIST(SUBSTR(ip_address, 1, INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 1) + 1, INSTR(ip_address, '.', 1, 2) - INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 2) + 1, INSTR(ip_address, '.', 1, 3) - INSTR(ip_address, '.', 1, 2) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 3) + 1));

  -- Convert each part to a number and calculate the final IP number
  ip_number := (convertToNumber(ip_parts(1)) * 256 * 256 * 256) +
               (convertToNumber(ip_parts(2)) * 256 * 256) +
               (convertToNumber(ip_parts(3)) * 256) +
               convertToNumber(ip_parts(4));

  RETURN ip_number;
END;
/
This is a nice try, but the syntax isn't correct:  in the function convertToNumber ip_part isn't a variable and so it can't be assigned with a substr. It would also be a logical error if it would be possible. But the idea is clear and I can modify the code easily. 
Beside that, a more severe issue is the handling of octal numbers: TO_NUMBER( ..., 'FM990') just does NOT convert an octal string to a number. 
Here 2 issues are shown: simple syntax issues will be highlighted by the compiler. But the logical wrong, but syntactical correct usage of TO_NUMBER is much harder to spot and requires proper test-data or a deep understanding of the capabilities by the observing human. 

Can you please have a closer look at your sub-function convertToNumber: I face some issues there: first you can not assign to ip_part - a dedicated variable is required there. Second the to_number doesn't convert octal numbers properly. can you please fix this, or show me the link to a working example of to_number for octal? 

I apologize for the confusion and the errors in the previous response. Converting octal numbers directly using TO_NUMBER can indeed lead to incorrect results. Unfortunately, Oracle's TO_NUMBER function does not provide a format model for parsing octal numbers.

However, I can offer an alternative solution by implementing a custom function to convert octal numbers to decimal. Here's an updated version of the inet_aton function with the revised convertToNumber function that handles octal notation correctly:



CREATE OR REPLACE FUNCTION inet_aton(ip_address VARCHAR2) RETURN NUMBER IS
  ip_parts SYS.ODCIVARCHAR2LIST;
  ip_number NUMBER := 0;
  base NUMBER := 10; -- Default base is decimal

  FUNCTION convertToNumber(ip_part VARCHAR2) RETURN NUMBER IS
    hex_to_dec CONSTANT SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST('0', '1', '2', '3', '4', '5', '6', '7',
                                                                      '8', '9', '10', '11', '12', '13', '14', '15');
    dec_num NUMBER := 0;
    digit CHAR(1);
  BEGIN
    -- Determine the base of the IP part
    IF ip_part LIKE '0x%' THEN
      base := 16; -- Hexadecimal
      ip_part := SUBSTR(ip_part, 3); -- Remove the '0x' prefix
    ELSIF ip_part LIKE '0%' THEN
      base := 8; -- Octal
      ip_part := SUBSTR(ip_part, 2); -- Remove the leading '0'
    ELSE
      base := 10; -- Decimal
    END IF;

    -- Convert the IP part to a number based on the determined base
    IF base = 8 THEN
      FOR i IN 1..LENGTH(ip_part) LOOP
        digit := SUBSTR(ip_part, i, 1);
        dec_num := dec_num * 8 + TO_NUMBER(hex_to_dec(TO_NUMBER(digit)));
      END LOOP;
    ELSE
      dec_num := TO_NUMBER(ip_part);
    END IF;

    RETURN dec_num;
  END;

BEGIN
  -- Split the IP address into its four parts
  ip_parts := SYS.ODCIVARCHAR2LIST(SUBSTR(ip_address, 1, INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 1) + 1, INSTR(ip_address, '.', 1, 2) - INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 2) + 1, INSTR(ip_address, '.', 1, 3) - INSTR(ip_address, '.', 1, 2) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 3) + 1));

  -- Convert each part to a number and calculate the final IP number
  ip_number := (convertToNumber(ip_parts(1)) * 256 * 256 * 256) +
               (convertToNumber(ip_parts(2)) * 256 * 256) +
               (convertToNumber(ip_parts(3)) * 256) +
               convertToNumber(ip_parts(4));

  RETURN ip_number;
END;
/
Here ChatGTP ignores my first comment about the variable. Only my complaint about octal was taken into account. I would still need some manual checks after the compiler fails. Also the conversion of octal numbers seems to be over complicated and "borrowed" from an idea to convert hex numbers - as the name of the variable hex_to_dec indicates. As octal is within decimal range, this can be omitted. As I was more interested in concepts than a 100% working code, I asked for a last refinement:
in the manpage of inet_aton there is an example which accepts input 0x7f.1 - this is a compressed form where elements of zero are just ignored. Can you extend your code to these special cases also?

Certainly! I apologize for the oversight. Here's an updated version of the inet_aton function that can handle the compressed form of IP addresses where leading zeros are omitted:


 


CREATE OR REPLACE FUNCTION inet_aton(ip_address VARCHAR2) RETURN NUMBER IS
  ip_parts SYS.ODCIVARCHAR2LIST;
  ip_number NUMBER := 0;
  base NUMBER := 10; -- Default base is decimal

  FUNCTION convertToNumber(ip_part VARCHAR2) RETURN NUMBER IS
    hex_to_dec CONSTANT SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST('0', '1', '2', '3', '4', '5', '6', '7',
                                                                      '8', '9', '10', '11', '12', '13', '14', '15');
    dec_num NUMBER := 0;
    digit CHAR(1);
  BEGIN
    -- Determine the base of the IP part
    IF ip_part LIKE '0x%' THEN
      base := 16; -- Hexadecimal
      ip_part := SUBSTR(ip_part, 3); -- Remove the '0x' prefix
    ELSIF ip_part LIKE '0%' THEN
      base := 8; -- Octal
      ip_part := SUBSTR(ip_part, 2); -- Remove the leading '0'
    ELSE
      base := 10; -- Decimal
    END IF;

    -- Convert the IP part to a number based on the determined base
    IF base = 8 THEN
      FOR i IN 1..LENGTH(ip_part) LOOP
        digit := SUBSTR(ip_part, i, 1);
        dec_num := dec_num * 8 + TO_NUMBER(hex_to_dec(TO_NUMBER(digit)));
      END LOOP;
    ELSE
      dec_num := TO_NUMBER(ip_part);
    END IF;

    RETURN dec_num;
  END;

BEGIN
  -- Split the IP address into its four parts
  ip_parts := SYS.ODCIVARCHAR2LIST(SUBSTR(ip_address, 1, INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 1) + 1, INSTR(ip_address, '.', 1, 2) - INSTR(ip_address, '.', 1, 1) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 2) + 1, INSTR(ip_address, '.', 1, 3) - INSTR(ip_address, '.', 1, 2) - 1),
                                   SUBSTR(ip_address, INSTR(ip_address, '.', 1, 3) + 1));

  -- Handle the special case of compressed form (leading zeros omitted)
  IF LENGTH(ip_parts(1)) = 0 THEN
    ip_parts(1) := '0';
  END IF;
  IF LENGTH(ip_parts(2)) = 0 THEN
    ip_parts(2) := '0';
  END IF;
  IF LENGTH(ip_parts(3)) = 0 THEN
    ip_parts(3) := '0';
  END IF;
  IF LENGTH(ip_parts(4)) = 0 THEN
    ip_parts(4) := '0';
  END IF;

  -- Convert each part to a number and calculate the final IP number
  ip_number := (convertToNumber(ip_parts(1)) * 256 * 256 * 256) +
               (convertToNumber(ip_parts(2)) * 256 * 256) +
               (convertToNumber(ip_parts(3)) * 256) +
               convertToNumber(ip_parts(4));

  RETURN ip_number;
END;
/
Now the inet_aton function handles the compressed form of IP addresses correctly by checking for empty parts and assigning them the value '0'. This ensures that the leading zeros are considered in the conversion process. Apologies for the confusion earlier, and thank you for bringing this to my attention.

I like the confidence it shows! But here I stopped as it seems to get complicated and the answer doesn't improve the way I would expect.
My conclusion: 
It's ok to ask ChatGTP for some code, but it can not be trusted at all. Syntax errors might be simple to fix. Wrong implementations of algorithms are much more dangerous. 
I also totally miss any kind of error handling, instrumentation, defensive implementations and so on. 
For me it seems as if ChatGTP is acting like a newcomer to the coding with high skills in searching the internet for anything useful, but limited capacity to generate a valid result. 
It's not sure if this is true in all other areas where AI generates answers, but at least it's an indication for me how to deal with this kind of generated content.