Systematic pass through PRG hot paths, promoting them to Go RTL while
preserving Harbour/FiveSql2 semantics. Full log in
docs/RTL-Go-Native-Migration.md.
Bench (bench_sql) vs 2026-04-08 baseline
- B1 SELECT * 2,192 → 114 µs (19x)
- B6 INNER JOIN 9,291 → 233 µs (40x)
- B7 CTE simple 8,037 → 129 µs (62x)
- B9 ROW_NUMBER 3,705 → 265 µs (14x)
- B10 RANK PARTITION 4,748 → 309 µs (15x)
- B12 INSERT (WA cache) 4,319 → 63 µs (69x)
- B13 UPDATE (WA cache) 6,144 → 68 µs (90x)
- B15 CTE+WIN+JOIN 18,395 → 1,873 µs (10x)
Infrastructure
- HbHash O(1) Index preserving insertion order (Harbour KEEPORDER)
- HbDeepClone Go RTL (scalar-sharing, immutable hash keys)
- MEMRDD auto-imported via gengo; all Five programs get mem:name driver
- SQL plan + pcode caches (s_hPlanCache, s_hDmlPcodeCache)
- Opt-in SqlWACacheEnable — dbUseArea/Close/Commit batched for DML
SQL engine
- FiveSql2 lexer ported to Go (byte FSM) with combined automatic
template parameterization (literals → ?, concat queries share plan)
- Go RTL: SqlDistinct, SqlGroupRows, SqlWindowPartitions,
SqlWindowSortPartition, SqlWindowAssignRank, SqlComputeAggSimple,
SqlBulkInsert, SqlBulkUpdate, SqlExprHasAgg, SqlEvalHaving
- CTE / subquery / driving-table materialize paths use MEMRDD
- SqlCoerce/SqlCmp/SqlIsTrue helpers moved from PRG to Go
- SqlBulkUpdate defers Flush when WA cache active (APFS fsync was
dominant B13 cost — 1.6ms/call → gone)
Correctness fixes uncovered during migration
- ASort default path now sorts dates/logicals/timestamps (was no-op)
- ORDER BY default NULL placement matches PRG SqlRowCompare across
Go fast path; explicit NULLS FIRST/LAST honored by both paths
- SqlBulkUpdate respects EXCLUSIVE vs SHARED mode record locks
- SqlCmp/SqlCmpEq normalize NumInt vs Double (caught by test 6b)
Verification
- go test ./... ALL PASS
- FiveSql2 test_sql1999 43/43
- tests/compat_harbour 56/56 (+5 new: ASort dates/logicals,
AScan int cross-type)
- Regression test test_null_order.prg for ORDER BY NULL ordering
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
473 lines
14 KiB
Plaintext
473 lines
14 KiB
Plaintext
/*
|
|
* 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
|