WDO MySQL - Pool Usage¶
Once the pool is configured in config.json, using it in handlers is straightforward.
Basic handler template¶
FUNCTION ProductList()
LOCAL oConn := WDO_Get( "mysql" ) // acquire a pool slot
LOCAL oStmt, aRows
IF oConn == NIL
RETURN USendError( 503, "DB unavailable" )
ENDIF
TRY
oStmt := oConn:Query( "SELECT id, name, price FROM products ORDER BY id" )
aRows := oStmt:FetchAll( .T. ) // .T. = array of hashes
oStmt:Free()
CATCH oError
oConn:Close()
RETURN USendError( 500, oError:description )
END
oConn:Close() // returns the slot to the pool (does NOT close the socket)
RETURN USendJson( aRows )
Always call Close()
oConn:Close() returns the slot to the pool so another thread can use it. If you forget, the slot stays locked until the HIX dispatcher reclaims it at the end of the request (with a WARN in the log).
Driver API¶
Read queries¶
// Simple query → resultset object
oStmt := oConn:Query( "SELECT id, name FROM users WHERE active = 1" )
// Get all rows as array of hashes (field => value)
aRows := oStmt:FetchAll( .T. )
// Or as array of arrays (faster, index-based access)
aRows := oStmt:FetchAll( .F. )
// Row by row
DO WHILE ( hRow := oStmt:Fetch_Assoc() ) != NIL
? hRow[ "name" ]
ENDDO
oStmt:Free() // always free the resultset
Write queries¶
// Exec - for INSERT / UPDATE / DELETE without a resultset
oConn:Exec( "UPDATE users SET active = 0 WHERE id = " + hb_NToS( nId ) )
// Affected rows
? oConn:Affected_Rows()
// Last auto-generated ID (after INSERT with AUTO_INCREMENT)
nNewId := oConn:Last_Insert_Id()
Manual escaping¶
Use Prepared Statements
For external values (form, JSON, query string), always use Prepared Statements. Manual escaping is only for SQL built entirely in code with no external input.
// For queries without prepared statements with external strings
cSafe := oConn:Escape( cExternalValue )
oConn:Exec( "INSERT INTO log (msg) VALUES ('" + cSafe + "')" )
Server information¶
? oConn:mysql_get_server_info() // "8.0.33"
? oConn:mysql_get_client_info() // "6.1.6" (client DLL version)
? oConn:VersionName() // "MySQL 8.0.33"
? oConn:DllPath() // "c:/myapp/libmysql64.dll"
? oConn:DllSource() // "exedir" / "override" / etc.
HIX ↔ Pool ↔ MySQL balancing¶
The three levels must be sized in cascade:
| Level | Parameter | Recommendation |
|---|---|---|
| HIX workers | hix.ini → server.pool_size |
2× the WDO pool |
| WDO pool | config.json → pool_size |
peak_QPS × avg_latency_sec |
| MySQL | my.cnf → max_connections |
WDO pool + 30 (admin margin) |
Little's Law: if your app handles 500 req/s and each query takes 10 ms on average, you need 500 × 0.010 = 5 simultaneous active connections. A pool of 10 gives a 2× margin.
Verify in production¶
oSrv:AddRouteGet( "health.db", "/health/db", {||
USendJson( WDO_PoolStats( "mysql" ) )
// returns: { "size" => 5, "busy" => 2, "free" => 3, "closed" => false }
} )
If free stays at 0 consistently → increase pool_size. If busy never exceeds 2 with a pool of 10 → you are over-provisioned.
Common patterns¶
Read a record by ID¶
FUNCTION UserGet()
LOCAL nId := Val( UParam( "id", "0" ) )
LOCAL oConn, oStmt, hRow
IF nId <= 0
RETURN USendError( 400, "invalid id" )
ENDIF
oConn := WDO_Get( "mysql" )
IF oConn == NIL ; RETURN USendError( 503, "DB unavailable" ) ; ENDIF
oStmt := oConn:Query( "SELECT * FROM users WHERE id = " + hb_NToS( nId ) )
hRow := oStmt:Fetch_Assoc()
oStmt:Free()
oConn:Close()
IF hRow == NIL
RETURN USendError( 404, "User not found" )
ENDIF
RETURN USendJson( hRow )
Pagination¶
FUNCTION UserList()
LOCAL nPage := Max( 1, Val( UGet( "page", "1" ) ) )
LOCAL nLimit := Min( 100, Val( UGet( "limit", "20" ) ) )
LOCAL nOffset := ( nPage - 1 ) * nLimit
LOCAL oConn, oStmt, aRows
oConn := WDO_Get( "mysql" )
IF oConn == NIL ; RETURN USendError( 503, "DB unavailable" ) ; ENDIF
// For external values, use Prepare (see Prepared Statements section)
oStmt := oConn:Query( "SELECT id, name FROM users ORDER BY id " + ;
"LIMIT " + hb_NToS( nLimit ) + ;
" OFFSET " + hb_NToS( nOffset ) )
aRows := oStmt:FetchAll( .T. )
oStmt:Free()
oConn:Close()
RETURN USendJson( { "page" => nPage, "limit" => nLimit, "data" => aRows } )
FINALLY - guaranteed release¶
If the handler is complex and can exit through multiple paths:
FUNCTION ComplexHandler()
LOCAL oConn := WDO_Get( "mysql" )
LOCAL oStmt := NIL
IF oConn == NIL ; RETURN USendError( 503, "DB unavailable" ) ; ENDIF
TRY
oStmt := oConn:Prepare( "SELECT id FROM users WHERE age > ?" )
oStmt:BindParam( 1, 18, "i" )
oStmt:Execute()
// ... processing ...
FINALLY
IF oStmt != NIL ; oStmt:Free() ; ENDIF
oConn:Close() // always executes, whether success or error
END
RETURN NIL