/* * TSqlFunc.prg — Scalar function library and type coercion helpers * * FiveSql — SQL Engine for Harbour DBF/NTX * * Copyright (c) 2025 Charles KWON (Charles KWON OhJun) * Email: charleskwonohjun@gmail.com * * All rights reserved. */ #include "FiveSqlDef.ch" STATIC s_cCollation := "" /* Evaluate a scalar or aggregate function by name */ FUNCTION SqlEvalFunc( cName, aArgs ) LOCAL i, xV, xV2, cS, nN, nN2, cRev LOCAL nNewM, nNewY, nNewD, nYr, nDy, dTmp /* Aggregate functions return placeholder during row-level fetch */ IF SqlIsAggName( cName ) IF Len( aArgs ) > 0 RETURN aArgs[ 1 ] ENDIF RETURN 0 ENDIF /* String functions */ DO CASE CASE cName == "UPPER" RETURN Upper( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "LOWER" RETURN Lower( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "TRIM" .OR. cName == "ALLTRIM" RETURN AllTrim( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "LTRIM" RETURN LTrim( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "RTRIM" RETURN RTrim( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "SUBSTR" .OR. cName == "SUBSTRING" cS := SqlCoerceStr( SqlArg(aArgs,1) ) nN := Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) IF Len( aArgs ) >= 3 RETURN SubStr( cS, nN, Int( SqlCoerceNum( SqlArg(aArgs,3) ) ) ) ENDIF RETURN SubStr( cS, nN ) CASE cName == "LEFT" RETURN Left( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) CASE cName == "RIGHT" RETURN Right( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) CASE cName == "LEN" .OR. cName == "LENGTH" RETURN Len( AllTrim( SqlCoerceStr( SqlArg(aArgs,1) ) ) ) CASE cName == "REPLACE" RETURN StrTran( SqlCoerceStr( SqlArg(aArgs,1) ), SqlCoerceStr( SqlArg(aArgs,2) ), SqlCoerceStr( SqlArg(aArgs,3) ) ) CASE cName == "SPACE" RETURN Space( Int( SqlCoerceNum( SqlArg(aArgs,1) ) ) ) CASE cName == "REPLICATE" RETURN Replicate( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) CASE cName == "STUFF" cS := SqlCoerceStr( SqlArg(aArgs,1) ) RETURN Left( cS, Int(SqlCoerceNum(SqlArg(aArgs,2))) - 1 ) + SqlCoerceStr( SqlArg(aArgs,4) ) + ; SubStr( cS, Int(SqlCoerceNum(SqlArg(aArgs,2))) + Int(SqlCoerceNum(SqlArg(aArgs,3))) ) CASE cName == "CHARINDEX" RETURN At( SqlCoerceStr( SqlArg(aArgs,1) ), SqlCoerceStr( SqlArg(aArgs,2) ) ) CASE cName == "CONCAT" cS := "" FOR i := 1 TO Len( aArgs ) cS += SqlCoerceStr( aArgs[ i ] ) NEXT RETURN cS /* Math functions */ CASE cName == "ABS" RETURN Abs( SqlCoerceNum( SqlArg(aArgs,1) ) ) CASE cName == "ROUND" RETURN Round( SqlCoerceNum( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) CASE cName == "INT" .OR. cName == "FLOOR" RETURN Int( SqlCoerceNum( SqlArg(aArgs,1) ) ) CASE cName == "CEILING" .OR. cName == "CEIL" nN := SqlCoerceNum( SqlArg(aArgs,1) ) IF nN == Int( nN ) RETURN nN ENDIF RETURN Int( nN ) + iif( nN > 0, 1, 0 ) CASE cName == "MOD" nN2 := SqlCoerceNum( SqlArg(aArgs,2) ) IF nN2 != 0 RETURN SqlCoerceNum( SqlArg(aArgs,1) ) % nN2 ENDIF RETURN 0 CASE cName == "POWER" RETURN SqlCoerceNum( SqlArg(aArgs,1) ) ^ SqlCoerceNum( SqlArg(aArgs,2) ) CASE cName == "SQRT" RETURN Sqrt( SqlCoerceNum( SqlArg(aArgs,1) ) ) CASE cName == "SIGN" nN := SqlCoerceNum( SqlArg(aArgs,1) ) RETURN iif( nN > 0, 1, iif( nN < 0, -1, 0 ) ) /* Date/time functions */ CASE cName == "YEAR" xV := SqlArg(aArgs,1) IF ValType( xV ) == "D" RETURN Year( xV ) ENDIF RETURN 0 CASE cName == "MONTH" xV := SqlArg(aArgs,1) IF ValType( xV ) == "D" RETURN Month( xV ) ENDIF RETURN 0 CASE cName == "DAY" xV := SqlArg(aArgs,1) IF ValType( xV ) == "D" RETURN Day( xV ) ENDIF RETURN 0 CASE cName == "NOW" RETURN hb_DateTime() CASE cName == "DATE" RETURN Date() CASE cName == "TIME" RETURN Time() CASE cName == "DTOS" xV := SqlArg(aArgs,1) IF ValType( xV ) == "D" RETURN DToS( xV ) ENDIF RETURN "" CASE cName == "DTOC" xV := SqlArg(aArgs,1) IF ValType( xV ) == "D" RETURN DToC( xV ) ENDIF RETURN "" CASE cName == "CTOD" RETURN CToD( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "STOD" RETURN SToD( SqlCoerceStr( SqlArg(aArgs,1) ) ) /* Type conversion functions */ CASE cName == "STR" IF Len( aArgs ) >= 2 RETURN Str( SqlCoerceNum( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) ENDIF RETURN AllTrim( Str( SqlCoerceNum( SqlArg(aArgs,1) ) ) ) CASE cName == "VAL" RETURN Val( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "CAST" RETURN SqlArg(aArgs,1) CASE cName == "CONVERT" RETURN SqlArg(aArgs,1) /* Conditional functions */ CASE cName == "IIF" IF SqlIsTrue( SqlArg(aArgs,1) ) RETURN SqlArg(aArgs,2) ENDIF RETURN SqlArg(aArgs,3) CASE cName == "COALESCE" /* SQL standard: empty string is NOT NULL, only NIL is NULL */ FOR i := 1 TO Len( aArgs ) IF aArgs[ i ] != NIL RETURN aArgs[ i ] ENDIF NEXT RETURN NIL CASE cName == "NULLIF" IF SqlCmpEq( SqlArg(aArgs,1), SqlArg(aArgs,2) ) RETURN NIL ENDIF RETURN SqlArg(aArgs,1) /* Date arithmetic */ CASE cName == "DATEADD" xV := SqlArg(aArgs,3) nN := Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) cS := Upper( SqlCoerceStr( SqlArg(aArgs,1) ) ) IF ValType( xV ) == "D" IF cS == "D" .OR. cS == "DAY" .OR. cS == "DD" RETURN xV + nN ELSEIF cS == "M" .OR. cS == "MONTH" .OR. cS == "MM" /* Normalize month overflow/underflow and clamp day to * end-of-month (Jan 31 + 1 month → Feb 28/29, not Feb 31) */ nNewM := Month(xV) + nN - 1 nNewY := Year(xV) + Int( nNewM / 12 ) nNewD := Day(xV) nNewM := ( nNewM % 12 ) + 1 IF nNewM <= 0 nNewM += 12 nNewY-- ENDIF /* Clamp day: find last day of target month */ dTmp := SToD( StrZero( nNewY, 4 ) + StrZero( nNewM, 2 ) + "01" ) dTmp := dTmp + 32 dTmp := SToD( StrZero( Year(dTmp), 4 ) + StrZero( Month(dTmp), 2 ) + "01" ) - 1 IF nNewD > Day(dTmp) nNewD := Day(dTmp) ENDIF RETURN SToD( StrZero( nNewY, 4 ) + StrZero( nNewM, 2 ) + StrZero( nNewD, 2 ) ) ELSEIF cS == "Y" .OR. cS == "YEAR" .OR. cS == "YY" .OR. cS == "YYYY" /* Clamp Feb 29 → Feb 28 on non-leap year */ nYr := Year(xV) + nN nDy := Day(xV) IF Month(xV) == 2 .AND. nDy == 29 dTmp := SToD( StrZero( nYr, 4 ) + "0301" ) - 1 nDy := Day(dTmp) ENDIF RETURN SToD( StrZero( nYr, 4 ) + StrZero( Month(xV), 2 ) + StrZero( nDy, 2 ) ) ENDIF ENDIF RETURN xV CASE cName == "DATEDIFF" xV := SqlArg(aArgs,2) xV2 := SqlArg(aArgs,3) cS := Upper( SqlCoerceStr( SqlArg(aArgs,1) ) ) IF ValType( xV ) == "D" .AND. ValType( xV2 ) == "D" IF cS == "D" .OR. cS == "DAY" RETURN xV2 - xV ELSEIF cS == "M" .OR. cS == "MONTH" RETURN ( Year(xV2) - Year(xV) ) * 12 + ( Month(xV2) - Month(xV) ) ELSEIF cS == "Y" .OR. cS == "YEAR" RETURN Year(xV2) - Year(xV) ENDIF ENDIF RETURN 0 CASE cName == "EOMONTH" xV := SqlArg(aArgs,1) nN := iif( Len(aArgs) >= 2, Int( SqlCoerceNum( SqlArg(aArgs,2) ) ), 0 ) IF ValType( xV ) == "D" xV := SToD( StrZero( Year(xV), 4 ) + StrZero( Month(xV) + nN + 1, 2 ) + "01" ) - 1 RETURN xV ENDIF RETURN NIL /* Extended string functions */ CASE cName == "INSTR" RETURN At( SqlCoerceStr( SqlArg(aArgs,1) ), SqlCoerceStr( SqlArg(aArgs,2) ) ) CASE cName == "REVERSE" cS := SqlCoerceStr( SqlArg(aArgs,1) ) cRev := "" FOR i := Len(cS) TO 1 STEP -1 cRev += SubStr( cS, i, 1 ) NEXT RETURN cRev CASE cName == "PADL" RETURN PadL( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) CASE cName == "PADR" RETURN PadR( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) CASE cName == "PADC" RETURN PadC( SqlCoerceStr( SqlArg(aArgs,1) ), Int( SqlCoerceNum( SqlArg(aArgs,2) ) ) ) /* Type check functions */ CASE cName == "ISNUMERIC" xV := SqlArg(aArgs,1) IF ValType( xV ) == "N" RETURN .T. ELSEIF ValType( xV ) == "C" RETURN Val( AllTrim( xV ) ) != 0 .OR. AllTrim( xV ) == "0" ENDIF RETURN .F. CASE cName == "ISDATE" xV := SqlArg(aArgs,1) RETURN ValType( xV ) == "D" CASE cName == "ISVALID" RETURN SqlArg(aArgs,1) != NIL CASE cName == "TYPEOF" .OR. cName == "TYPE" xV := SqlArg(aArgs,1) RETURN ValType( xV ) /* Timestamp functions */ CASE cName == "HB_HOUR" xV := SqlArg(aArgs,1) IF ValType( xV ) == "T" RETURN hb_Hour( xV ) ENDIF RETURN 0 CASE cName == "HB_MINUTE" xV := SqlArg(aArgs,1) IF ValType( xV ) == "T" RETURN hb_Minute( xV ) ENDIF RETURN 0 CASE cName == "HB_SECOND" xV := SqlArg(aArgs,1) IF ValType( xV ) == "T" RETURN hb_Sec( xV ) ENDIF RETURN 0 CASE cName == "HB_DATETIME" RETURN hb_DateTime() CASE cName == "HB_TTOC" xV := SqlArg(aArgs,1) IF ValType( xV ) == "T" RETURN hb_TToC( xV ) ENDIF RETURN "" CASE cName == "HB_CTOT" RETURN hb_CToT( SqlCoerceStr( SqlArg(aArgs,1) ) ) CASE cName == "TIMESTAMP" RETURN hb_CToT( SqlCoerceStr( SqlArg(aArgs,1) ) ) /* Banker's rounding */ CASE cName == "ROUND_BANKER" nN := SqlCoerceNum( SqlArg(aArgs,1) ) nN2 := iif( Len(aArgs) >= 2, Int( SqlCoerceNum( SqlArg(aArgs,2) ) ), 2 ) RETURN SqlBankerRound( nN, nN2 ) /* Format function */ CASE cName == "FORMAT" nN := SqlCoerceNum( SqlArg(aArgs,1) ) nN2 := iif( Len(aArgs) >= 2, Int( SqlCoerceNum( SqlArg(aArgs,2) ) ), 2 ) RETURN AllTrim( Str( nN, 20, nN2 ) ) ENDCASE RETURN NIL /* Safe argument accessor */ FUNCTION SqlArg( a, n ) IF n <= Len( a ) RETURN a[ n ] ENDIF RETURN NIL /* SqlCoerceStr/SqlCoerceNum/SqlCoerceForCmp/SqlIsTrue/SqlCmpEq/SqlCmpLt * are implemented in Go (hbrtl/sqlhelpers.go) — registered as * SQLCOERCESTR etc. The PRG bodies have been removed to avoid symbol * collision with the RTL symbols; behavior is byte-for-byte identical. * See docs/RTL-Go-Native-Migration.md (Tier 4). */ /* SQL LIKE pattern matching with optional escape character */ FUNCTION SqlLikeMatch( cStr, cPat, cEscape ) LOCAL nS, nP, nSLen, nPLen, chP, chS IF ValType( cStr ) != "C" .OR. ValType( cPat ) != "C" RETURN .F. ENDIF cStr := Upper( AllTrim( cStr ) ) cPat := Upper( AllTrim( cPat ) ) IF cEscape == NIL cEscape := "" ENDIF IF ValType( cEscape ) == "C" cEscape := Upper( AllTrim( cEscape ) ) ELSE cEscape := "" ENDIF nSLen := Len( cStr ) nPLen := Len( cPat ) nS := 1 nP := 1 RETURN SqlLikeRecurse( cStr, cPat, nS, nP, nSLen, nPLen, cEscape ) /* Recursive LIKE matcher supporting %, _ and ESCAPE */ FUNCTION SqlLikeRecurse( cStr, cPat, nS, nP, nSLen, nPLen, cEsc ) LOCAL chP, chS DO WHILE nP <= nPLen chP := SubStr( cPat, nP, 1 ) /* Escape character: next pattern char is literal */ IF ! Empty( cEsc ) .AND. chP == cEsc .AND. nP < nPLen nP++ chP := SubStr( cPat, nP, 1 ) IF nS > nSLen RETURN .F. ENDIF chS := SubStr( cStr, nS, 1 ) IF chS != chP RETURN .F. ENDIF nS++ nP++ LOOP ENDIF IF chP == "%" /* Skip consecutive % */ DO WHILE nP <= nPLen .AND. SubStr( cPat, nP, 1 ) == "%" nP++ ENDDO IF nP > nPLen RETURN .T. ENDIF DO WHILE nS <= nSLen IF SqlLikeRecurse( cStr, cPat, nS, nP, nSLen, nPLen, cEsc ) RETURN .T. ENDIF nS++ ENDDO RETURN .F. ENDIF IF chP == "_" IF nS > nSLen RETURN .F. ENDIF nS++ nP++ LOOP ENDIF /* Literal character match */ IF nS > nSLen RETURN .F. ENDIF chS := SubStr( cStr, nS, 1 ) IF chS != chP RETURN .F. ENDIF nS++ nP++ ENDDO RETURN nS > nSLen /* Banker's rounding (round half to even) */ FUNCTION SqlBankerRound( nVal, nDec ) LOCAL nFactor, nScaled, nInt, nFrac IF nDec == NIL nDec := 2 ENDIF nFactor := 10 ^ nDec nScaled := nVal * nFactor nInt := Int( nScaled ) nFrac := Abs( nScaled - nInt ) IF Abs( nFrac - 0.5 ) > 0.0000001 RETURN Round( nVal, nDec ) ENDIF IF nInt % 2 == 0 RETURN nInt / nFactor ENDIF IF nVal >= 0 RETURN ( nInt + 1 ) / nFactor ENDIF RETURN ( nInt - 1 ) / nFactor /* Collation support */ FUNCTION SqlSetCollation( cCollation ) s_cCollation := Upper( AllTrim( cCollation ) ) IF s_cCollation != "NOCASE" .AND. ! Empty( s_cCollation ) hb_cdpSelect( s_cCollation ) ENDIF RETURN NIL FUNCTION SqlGetCollation() RETURN s_cCollation