Top 5 Recent Articles
ARTICLES CATEGORIES
- Algorithms (22)
- All (399)
- Biography (1)
- Blog (44)
- Business Requirements (1)
- Commentary (1)
- Conversion (2)
- Customers (2)
- Data Models (1)
- Education (2)
- GeoRaptor (13)
- GPS (1)
- Image Processing (2)
- Import Export (8)
- Licensing (2)
- LiDAR (1)
- Linear Referencing (4)
- Manifold GIS (3)
- Mapping (1)
- MySQL Spatial (7)
- Networking and Routing (including Optimization) (5)
- Open Source (18)
- Oracle Spatial and Locator (194)
- Partitioning (1)
- PostGIS (36)
- Projections (1)
- Published Articles (1)
- qGIS (1)
- Recommendations (1)
- Services (1)
- Software Change Log (1)
- Source Code (37)
- Space Curves (9)
- Spatial Database Functions (109)
- Spatial DB comparison (1)
- Spatial XML Processing (11)
- SQL Server Spatial (92)
- Standards (3)
- Stored Procedure (17)
- Tessellation or Gridding (10)
- Tools (2)
- Topological Relationships (1)
- Training (2)
- Triangulation (2)
CASE Statements and SDO_GEOMETRY
Over the past week I have been having a conversation with a customer around problems with using CASE WHEN THEN ELSE END constructs in SQL for turning WKT into SDO_GEOMETRY objects on INSERT.
Now the customer was doing this sort of comparison for correctly processing WKT data values in an external file into SDO_GEOMETRY and was getting a wrong result forcing him to change the way he inserted the data.
The error he was generating was trying to construct a geometry with a NULL WKT value as follows:
SELECT sdo_util.from_wktgeometry(CAST(NULL AS VARCHAR2(100))) AS geom FROM dual; Error starting at line 260 IN command: SELECT sdo_util.from_wktgeometry(CAST(NULL AS VARCHAR2(100))) AS geom FROM dual Error report: SQL Error: ORA-29532: Java CALL TERMINATED BY uncaught Java exception: java.SQL.SQLException ORA-06512: at "MDSYS.SDO_UTIL", line 187 29532. 00000 - "Java call terminated by uncaught Java exception: %s" *Cause: A Java exception OR error was signaled AND could NOT be resolved BY the Java code. *Action: MODIFY Java code, IF this behavior IS NOT intended.
Now, he thought that he could catch this by doing this:
DECLARE v_wkt VARCHAR2(100) := NULL; v_geom sdo_geometry; BEGIN SELECT CASE v_wkt WHEN NULL THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt) END INTO v_geom FROM dual; END; / Error starting at line 253 IN command: DECLARE v_wkt VARCHAR2(100) := NULL; v_geom sdo_geometry; BEGIN SELECT CASE v_wkt WHEN NULL THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt) END INTO v_geom FROM dual; END; Error report: ORA-29532: Java CALL TERMINATED BY uncaught Java exception: java.SQL.SQLException ORA-06512: at "MDSYS.SDO_UTIL", line 187 ORA-06512: at line 5 29532. 00000 - "Java call terminated by uncaught Java exception: %s" *Cause: A Java exception OR error was signaled AND could NOT be resolved BY the Java code. *Action: MODIFY Java code, IF this behavior IS NOT intended.
That is, he thought the case statement would catch the null WKT error but it doesn’t: the same error is being generated. Why is this?
The reason is the way the CASE statement does the comparison when it hits a null value for the variable.
SELECT CASE NULL WHEN NULL THEN 'NULL' ELSE 'NOT NULL' END AS RESULT FROM DUAL; RESULT -------- NOT NULL -- The above fails because the CASE NULL WHEN NULL is executing a comparison on two NULLs as in the following "null = null" predicate. SELECT 1 FROM dual AS RESULT WHERE NULL = NULL UNION ALL SELECT 2 FROM dual AS RESULT WHERE NULL IS NULL; RESULT ------ 2
In other words, you simply can’t compare nulls using an equality operator. The CASE WHEN form actually executes a equals (*) and not an *IS NULL.
There are ways of making this form of CASE statement work and that is by using the NVL function as follows:
SET serveroutput ON DECLARE v_wkt1 VARCHAR2(100) := NULL; v_wkt2 VARCHAR2(100) := 'POINT(90 90)'; v_display SDO_GEOMETRY; BEGIN dbms_output.put_line('select case v_wkt2 when null ... works fine because a comparison with a null never happens....'); SELECT CASE v_wkt2 WHEN NULL THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt2) END INTO v_display FROM dual; dbms_output.put_line('RESULT: ' || SDO_UTIL.TO_WKTGEOMETRY(v_display)); dbms_output.put_line('case NVL(v_wkt1,''NULL'') when ''NULL'' .... works because, via NVL, we end up comparing two strings rather than two NULLs using incorrect NULL=NULL predicate....'); SELECT CASE NVL(v_wkt1,'NULL') WHEN 'NULL' THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt1) END INTO v_display FROM dual; dbms_output.put_line('RESULT: ' || CASE WHEN v_wkt1 IS NULL THEN 'NULL' ELSE SDO_UTIL.TO_WKTGEOMETRY(v_display) END); dbms_output.put_line('case when v_wkt1 is null then null.... works because this is the proper way to compare nulls ie using NULL IS NULL predicate.'); SELECT CASE WHEN v_wkt1 IS NULL THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt1) END INTO v_display FROM dual; dbms_output.put_line('RESULT: ' || CASE WHEN v_wkt1 IS NULL THEN 'NULL' ELSE SDO_UTIL.TO_WKTGEOMETRY(v_display) END); dbms_output.put_line('case v_wkt1 when null then null.... throws exception even because you can''t compare nulls this way.'); SELECT CASE v_wkt1 WHEN NULL THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt1) END INTO v_display FROM dual; EXCEPTION WHEN OTHERS THEN dbms_output.put_line('Exception: ' || SQLERRM); END; / anonymous block completed SELECT CASE v_wkt2 WHEN NULL ... works fine because a comparison WITH a NULL never happens.... RESULT: POINT (90.0 90.0) CASE NVL(v_wkt1,'NULL') WHEN 'NULL' .... works because, via NVL, we END up comparing two strings rather than two NULLS USING incorrect NULL=NULL predicate.... RESULT: NULL CASE WHEN v_wkt1 IS NULL THEN NULL.... works because this IS the proper way TO compare NULLS ie USING NULL IS NULL predicate. RESULT: NULL CASE v_wkt1 WHEN NULL THEN NULL.... throws exception even because you can't compare nulls this way. Exception: ORA-29532: Java call terminated by uncaught Java exception: java.sql.SQLException ORA-06512: at "MDSYS.SDO_UTIL", line 187
The CASE Statement The CASE statement is a mix of programming constructs. The first is when you use the following form:
CASE <value> WHEN <constant> THEN <result> WHEN <constant> THEN <result> ... ELSE <value> END
Here you are using the traditional form of a switch statement as in languages like Java etc.
In this, the switching value can only be an instance of a variable’s possible range of fixed values. So, numbers, strings, booleans and enumeration types are supported.
The second of a CASE statement is in the form of a set of nested IF statements…
IF ( clause ) THEN <result> ELSIF ( clause ) THEN <result> ELSE <result> END IF
That is:
CASE WHEN <variable> <operator> <value> THEN <result> WHEN <variable> <operator> <value> THEN <result> ... ELSE <result> END
This is used in the anonymous block above here:
SELECT CASE WHEN v_wkt1 IS NULL THEN NULL ELSE sdo_util.from_wktgeometry(v_wkt1) END INTO v_display FROM dual;
This is always safe for testing if a variable/column etc is NULL.
Comparing SDO_GEOMETRY Objects
This leads to a discussion about how to use Oracle OBJECTs like SDO_GEOMETRY in CASE statements. Can it be done? Well, yes it can if the TYPE declares a comparison operation called an ORDER MEMBER FUNCTION. SDO_GEOMETRY does not declare such a comparison operator as part of its type so one must use other methods (see later).
But, if it did have one how would it work?
As an exercise in creating a type I created the MBR type as part of my packages many years ago. As part of the type I declared and implemented an order member function as follows
CREATE OR REPLACE TYPE MBR AS OBJECT ( MinX NUMBER, MinY NUMBER, MaxX NUMBER, MaxY NUMBER, -- ================== Constructors -- Constructor FUNCTION MBR RETURN SELF AS RESULT, Constructor FUNCTION MBR( p_geometry IN MDSYS.SDO_GEOMETRY, p_tolerance IN NUMBER ) RETURN SELF AS RESULT, Constructor FUNCTION MBR( p_MBR IN MBR ) RETURN SELF AS RESULT, .... ORDER Member FUNCTION Evaluate(p_other IN MBR) RETURN PLS_Integer ); -- The Order Member Function Evaluate is declared in the Type body as.... -- CREATE OR REPLACE TYPE BODY MBR AS ..... -- @function : Evaluate -- @version : 1.0 -- @precis : Returns value that can be used to company two MBRs in an expression of type MBR < MBR -- @return : Computed number. -- @returntype: Number -- @history : SGG August 2006 - Original Coding -- ORDER Member FUNCTION Evaluate(p_other IN MBR) RETURN PLS_Integer IS BEGIN IF (MinX < p_other.MinX) THEN IF (MinY <= p_other.MinY) THEN RETURN -1; ElsIf (MinY = p_other.MinY) THEN RETURN -1; END IF; ElsIf (MinX = p_other.MinX) THEN IF (MinY < p_other.MinY) THEN RETURN -1; ElsIf (MinY = p_other.MinY) THEN RETURN 0; END IF; ELSE RETURN 1; END IF; END Evaluate; END MBR; /
Now you can implement any sort of processing in this method to compare the two MBRs – I have done it to return -1,0,1.
Now we can use the Evaluate method as follows:
SELECT MBR(337900, 5429000, 338900, 5430000).Evaluate(MBR(337950, 5429050, 338950, 5430500)) AS mbrvalue, CASE WHEN MBR(337900, 5429000, 338900, 5430000).Evaluate(MBR(337950, 5429050, 338950, 5430500)) = 0 THEN 'EQUALS' ELSE 'UNEQUAL' END AS testMBR FROM DUAL; MBRVALUE TESTMBR ---------------------- ------- -1 UNEQUAL . SELECT 'TRUE' AS RESULT FROM dual WHERE MBR(337900, 5429000, 338900, 5430000) = MBR(337950, 5429050, 338950, 5430500); RESULT ------ . SELECT MBR(337900, 5429000, 338900, 5430000).Evaluate(MBR(337900, 5429000, 338900, 5430000)) AS mbrvalue, CASE WHEN MBR(337900, 5429000, 338900, 5430000).Evaluate(MBR(337900, 5429000, 338900, 5430000)) = 0 THEN 'EQUALS' ELSE 'UNEQUAL' END AS testMBR FROM DUAL; MBRVALUE TESTMBR ---------------------- ------- 0 EQUALS . -- But you can even do this! SELECT CASE MBR(337900, 5429000, 338900, 5430000) WHEN MBR(337900, 5429000, 338900, 5430000) THEN 'EQUALS' ELSE 'UNEQUAL' END AS testMBR FROM DUAL; TESTMBR ------- EQUALS . SELECT 'TRUE' AS RESULT FROM dual WHERE MBR(337900, 5429000, 338900, 5430000) = MBR(337900, 5429000, 338900, 5430000); RESULT ------ TRUE
Now that is super cool!
However, Oracle did not implement any ORDER MEMBER function for SDO_GEOMETRY so you can’t do this:
SELECT CASE sdo_geometry('POINT(90 90)', 4326) WHEN sdo_geometry('POINT(90 90)',4326) THEN 'EQUALS' ELSE 'UNEQUAL' END AS geomCompare FROM dual; . Error starting at line 290 IN command: SELECT CASE sdo_geometry('POINT(90 90)', 4326) WHEN sdo_geometry('POINT(90 90)',4326) THEN 'EQUALS' ELSE 'UNEQUAL' END AS geomCompare FROM dual Error at Command Line:290 COLUMN:12 Error report: SQL Error: ORA-22901: cannot compare VARRAY OR LOB attributes OF an object TYPE 22901. 00000 - "cannot compare nested table or VARRAY or LOB attributes of an object type" *Cause: Comparison OF nested TABLE OR VARRAY OR LOB attributes OF an object TYPE was attempted IN the absence OF a MAP OR ORDER method. *Action: define a MAP OR ORDER method FOR the object TYPE. . -- Nor can you do this -- SELECT CASE WHEN sdo_geometry('POINT(90 90)', 4326) = sdo_geometry('POINT(90 90)',4326) THEN 'EQUALS' ELSE 'UNEQUAL' END AS geomCompare FROM dual; . Error starting at line 297 IN command: SELECT CASE WHEN sdo_geometry('POINT(90 90)', 4326) = sdo_geometry('POINT(90 90)',4326) THEN 'EQUALS' ELSE 'UNEQUAL' END AS geomCompare FROM dual Error at Command Line:297 COLUMN:17 Error report: SQL Error: ORA-22901: cannot compare VARRAY OR LOB attributes OF an object TYPE 22901. 00000 - "cannot compare nested table or VARRAY or LOB attributes of an object type" *Cause: Comparison OF nested TABLE OR VARRAY OR LOB attributes OF an object TYPE was attempted IN the absence OF a MAP OR ORDER method. *Action: define a MAP OR ORDER method FOR the object TYPE.
To compare two geometries one must convert them to WKT CLOBs or use the SDO_GEOM.RELATE function (Spatial only – or see my JTS replacement).
SELECT CASE sdo_geom.relate(sdo_geometry('POINT(90 90)',4326),'EQUAL',sdo_geometry('POINT(90 90)',4326),0.05) WHEN 'FALSE' THEN 'UNEQUAL' WHEN 'EQUAL' THEN 'EQUAL' ELSE sdo_geom.relate(sdo_geometry('POINT(90 90)',4326),'EQUAL',sdo_geometry('POINT(90 90)',4326),0.05) END AS geoCompare FROM dual; . GEOCOMPARE ---------- EQUAL
I hope this is of interest to someone out there.
Documentation
- MySQL Spatial General Functions
- Oracle LRS Objects
- Oracle Spatial Exporter (Java + pl/SQL)
- Oracle Spatial Object Functions
- Oracle Spatial Object Functions (Multi Page)
- PostGIS pl/pgSQL Functions
- SC4O Oracle Java Topology Suite (Java + pl/SQL)
- SQL Server Spatial General TSQL Functions
- SQL Server Spatial LRS TSQL Functions