// Regression test for NULL ordering in ORDER BY. // Both the Go fast path (SqlOrderBy) and the PRG fallback // (SqlRowCompare) must produce the same order and honor explicit // NULLS FIRST/LAST from SQL:2003. // // Default (no NULLS spec): NULLs sort as the largest value — last in // ASC, first in DESC. Matches PostgreSQL default and the legacy PRG // SqlRowCompare behavior that predates the Go port. STATIC s_nPass := 0 STATIC s_nFail := 0 STATIC s_nTotal := 0 PROCEDURE Main() ErrorBlock( {|e| QOut( "TRAP: " + e:description + " " + e:operation ), Break(e) } ) ? "================================================================" ? " FiveSql NULL Ordering Test" ? "================================================================" ? SetupData() // Diagnostic: show what score column actually contains after UPDATE, // then what ORDER BY score ASC produces. LOCAL aR := five_SQL( "SELECT name, score FROM nullord" ) LOCAL i, aRows ? "Raw rows:" IF ValType( aR ) == "A" .AND. Len( aR ) >= 2 aRows := aR[ 2 ] FOR i := 1 TO Len( aRows ) ? " name=" + AllTrim( aRows[ i ][ 1 ] ), "score=" + ; iif( aRows[ i ][ 2 ] == NIL, "NIL", LTrim( Str( aRows[ i ][ 2 ] ) ) ) NEXT ENDIF ? aR := five_SQL( "SELECT name, score FROM nullord ORDER BY score" ) ? "ORDER BY score ASC:" IF ValType( aR ) == "A" .AND. Len( aR ) >= 2 aRows := aR[ 2 ] FOR i := 1 TO Len( aRows ) ? " name=" + AllTrim( aRows[ i ][ 1 ] ), "score=" + ; iif( aRows[ i ][ 2 ] == NIL, "NIL", LTrim( Str( aRows[ i ][ 2 ] ) ) ) NEXT ENDIF ? TestDefaultAsc() TestDefaultDesc() TestNullsFirstAsc() TestNullsLastDesc() ? ? "================================================================" ? " Results:", LTrim(Str(s_nPass)), "/", LTrim(Str(s_nTotal)), "passed" ? "================================================================" CleanupData() RETURN STATIC PROCEDURE SetupData() LOCAL aFields aFields := { ; {"NAME", "C", 20, 0}, ; {"SCORE", "N", 6, 0} } dbCreate( "nullord", aFields ) USE "nullord" NEW EXCLUSIVE APPEND BLANK ; REPLACE NAME WITH "A", SCORE WITH 10 APPEND BLANK ; REPLACE NAME WITH "B", SCORE WITH 30 APPEND BLANK ; REPLACE NAME WITH "C" APPEND BLANK ; REPLACE NAME WITH "D", SCORE WITH 20 APPEND BLANK ; REPLACE NAME WITH "E" CLOSE ALL // Null out C and E via UPDATE — SCORE column goes from numeric 0 to NIL. five_SQL( "UPDATE nullord SET score = NULL WHERE name = 'C'" ) five_SQL( "UPDATE nullord SET score = NULL WHERE name = 'E'" ) RETURN STATIC PROCEDURE CleanupData() CLOSE ALL FErase( "nullord.dbf" ) RETURN STATIC FUNCTION NameOrder( aResult ) LOCAL cOut := "", i, aRows IF ValType( aResult ) == "A" .AND. Len( aResult ) >= 2 aRows := aResult[ 2 ] FOR i := 1 TO Len( aRows ) cOut += AllTrim( aRows[ i ][ 1 ] ) NEXT ENDIF RETURN cOut STATIC PROCEDURE Check( cLabel, cGot, cWant ) s_nTotal++ IF cGot == cWant s_nPass++ ? " PASS:", cLabel, "→", cGot ELSE s_nFail++ ? " FAIL:", cLabel, "→ got", cGot, "want", cWant ENDIF RETURN // Induce NULL via NULLIF(score, 0) — DBF N fields can't store NULL, so // we turn the sentinel 0 into NULL at projection time. C and E have // score=0 → projected s = NULL. STATIC PROCEDURE TestDefaultAsc() LOCAL aR := five_SQL( "SELECT name, NULLIF(score, 0) AS s FROM nullord ORDER BY s" ) // Default ASC: NULLs sort as largest → non-NULLs first (A=10, D=20, B=30), then NULLs (C, E) Check( "default ASC (NULLs last)", NameOrder( aR ), "ADBCE" ) RETURN STATIC PROCEDURE TestDefaultDesc() LOCAL aR := five_SQL( "SELECT name, NULLIF(score, 0) AS s FROM nullord ORDER BY s DESC" ) // Default DESC: NULLs sort as largest → NULLs first (C, E), then descending (B=30, D=20, A=10) Check( "default DESC (NULLs first)", NameOrder( aR ), "CEBDA" ) RETURN STATIC PROCEDURE TestNullsFirstAsc() LOCAL aR := five_SQL( "SELECT name, NULLIF(score, 0) AS s FROM nullord ORDER BY s ASC NULLS FIRST" ) Check( "ASC NULLS FIRST", NameOrder( aR ), "CEADB" ) RETURN STATIC PROCEDURE TestNullsLastDesc() LOCAL aR := five_SQL( "SELECT name, NULLIF(score, 0) AS s FROM nullord ORDER BY s DESC NULLS LAST" ) Check( "DESC NULLS LAST", NameOrder( aR ), "BDACE" ) RETURN