MariaDB manual

Post Reply
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

MariaDB manual

Post by Antonio Linares »

MARIADB MANUAL

for FiveWin / Harbour

Practical Guide to Connection, Queries and Data Management

Based on the official FiveWin examples

Version 2025

Developed by Carlos Atúncar Hernandez

Contents
  1. Introduction
  2. Connecting to MariaDB
  3. Main Commands and Functions
  4. RowSet: Queries and Data Editing
  5. CRUD Operations
  6. Table Management
  7. Parent-Child Relationships
  8. Advanced Functions
  9. AES Encryption
  10. Spatial Data (GEO)
  11. Unicode and Encodings
  12. Importing DBF to MariaDB
  13. FWMariaRecord: Single Record Editing
  14. FWH vs ADO vs Dolphin Comparison
  15. Quick Method Reference
1. Introduction

FiveWin for Harbour includes native support for MariaDB/MySQL through
the maria_Connect() function and a set of classes that allow working
with relational databases easily, without requiring ODBC drivers or
external components.

This manual covers the practical use of MariaDB in FiveWin, based on the
official examples included in the distribution (maria01.prg to
maria16.prg and others).

Requirements
  • FiveWin for Harbour 25.x or higher
  • MariaDB 10.x / MySQL 5.7+ server accessible locally or over the
    network
  • Include fivewin.ch in the program
  • Enable Unicode: FW_SetUnicode( .t. ) if special characters are
    used
Connection Architecture

FiveWin connects natively to MariaDB without ODBC, offering greater
speed and simplicity. The connection object (oCn) is the central point
from which all database operations are performed.

2. Connecting to MariaDB 2.1 maria_Connect() Function

The main function to establish a connection is maria_Connect(). It
returns a connection object or NIL if it fails.

Syntax

Code: Select all | Expand

oCn := maria_Connect( cConnection, lShowError )
oCn := maria_Connect( cHost, cDatabase, cUser, cPassword [, nPort] )
Parameters -- Connection string format

Parameter Type Description

------------- ------ -------------------------------------------------

cConnection C String format: host:port,database,user,password
lShowError L .t. shows a message if the connection fails

Parameters -- Extended format (via FWCONNECT) -----------------------------------------------------------------------

Parameter Type Description

--------------------------- ------------- -----------------------------

cHost C Server name or IP. May
include :port (e.g.,
localhost:3306)

cDatabase C Database name

cUser C MariaDB user

cPassword C Password

nPort N Port (optional, default 3306)

----------------------------------------------------------------------- Connection Examples

Code: Select all | Expand

// Compact string format
oCn := maria_Connect( "208.91.198.197:3306,fwhdemo,user,password", .t. )

// Separate parameters (local)
oCn := maria_Connect( "localhost", "fwh", "fivetec1_antonio", "1234" )

// Via FWCONNECT command (fivewin.ch)
FWCONNECT oCn HOST cHost USER cUser PASSWORD cPassword DATABASE cDB

// Built-in demo function
oCn := FW_DemoDB()          // connects to FiveWin demo server
oCn := FW_DemoDB( 1 )       // alternate demo server
oCn := FW_DemoDB( "ADO" )   // connection via ADO
oCn := FW_DemoDB( "DLP" )   // connection via TDolphin
Verify Connection and Close

Code: Select all | Expand

if ( oCn := maria_Connect( ... ) ) == nil
   MsgStop( "Unable to connect to server" )
   return nil
endif

// ... database operations ...

oCn:Close()   // or oCn:End()
2.2 Connection Object Properties

Property Type Description

------------- ------ -----------------------------------------

lLogErr L Enables error logging to a .log file
lShowErrors L Automatically displays errors on screen
nError N Last error code (0 = no error)
LockTimeOut N Lock wait timeout (seconds)
lUnicode L Unicode mode active

Example:

Code: Select all | Expand

oCn:lLogErr     := .t.
oCn:lShowErrors := .t.
3. Main Commands and Functions 3.1 Connection Object Methods Summary -----------------------------------------------------------------------

Method Description

---------------------------- ------------------------------------------

oCn:RowSet( cSQL \[, Executes a query and returns an editable
aParams\] ) RowSet

oCn:RecSet( cTable, nRecs ) Reads n records from a table (efficient
for large tables)

oCn:Query( cSQL ) Executes query (TDolphin compatible)

oCn:Execute( cSQL ) Executes SQL statement without result
(INSERT, UPDATE, DELETE, CREATE...)

oCn:QueryResult( cSQL ) Returns value of first cell in result

oCn:ListTables() Returns array with database table names

oCn:TableExists( cTable ) Returns .t. if table exists

oCn:CreateTable( cTable, Creates a new table
aStructure )

oCn:DropTable( cTable ) Drops a table

oCn:Insert( cTable, cFields, Inserts one or multiple records
aData )

oCn:UpdateSummary( ... ) Updates master table totals from detail
table

oCn:ImportFromDbf( cFile ) Imports DBF file to MariaDB

oCn:PivotArray( ... ) Generates pivot table

oCn:Close() / oCn:End() Closes connection

oCn:ShowError() Displays last error

oCn:setAutoCommit( lVal ) Enables/disables auto commit

oCn:TableStructure( cTable ) Returns table structure

----------------------------------------------------------------------- 3.2 Recommended Initial Setup

Code: Select all | Expand

function Main()

   local oCn

   SET DATE ITALIAN
   SET CENTURY ON

   FW_SetUnicode( .t. )
   FWNumFormat( "A", .t. )

   oCn := maria_Connect( "localhost", "mydb", "user", "password" )

   if oCn == nil
      MsgStop( "Connection error" )
      return nil
   endif

   oCn:lShowErrors := .t.

   // ... application code ...

   oCn:Close()

return nil
4. RowSet: Queries and Data Editing

The RowSet is the central object for working with data in
FiveWin/MariaDB. It represents a set of records that can be displayed in
XBrowse and edited directly.

4.1 Create a RowSet

Code: Select all | Expand

// Full table
oRs := oCn:RowSet( "clientes" )

// Custom SQL
oRs := oCn:RowSet( "SELECT * FROM clientes WHERE activo = 1" )

// Parameterized SQL (avoids SQL injection)
oRs := oCn:RowSet( "SELECT * FROM clientes WHERE estado = ?", { cEstado } )
JOIN Example

Code: Select all | Expand

TEXT INTO cSql

   SELECT C.ID, C.FIRST, C.CITY, S.NAME AS STATENAME
   FROM customer C
   LEFT JOIN states S ON C.STATE = S.CODE

ENDTEXT

oRs := oCn:RowSet( cSql )
4.2 Display in XBrowse

Code: Select all | Expand

XBROWSER oRs FASTEDIT TITLE "Clients"
4.3 RowSet Methods

Method Description

-------------------------------------- -------------------------------

oRs:GoTop() Move to first record
oRs:GoBottom() Move to last record
oRs:Skip(n) Move n records
oRs:FieldGet(n) Get field value by index
oRs:FieldGet('name') Get field value by name
oRs:Fields('field'):lReadOnly := .t. Make field read-only
oRs:Delete() Delete current record
oRs:ReQuery() Reload data
oRs:ReSync() Synchronize current record
oRs:Refresh() Refresh data
oRs:SetDefault('field', value) Default value for new records
oRs:SetFilter('field = ?', {val}) Apply filter
oRs:ReFilter(aParams) Reapply filter
oRs:Close() Close RowSet
oRs:lAutoAppend := .t. Enable appending from browse
oRs:EditBaseRecord(...) Edit full record dialog

4.4 Field Access as Properties

Code: Select all | Expand

? oRs:nombre
oRs:estado := "A"
4.5 RecSet for Large Tables

Code: Select all | Expand

oRs := oCn:RecSet( "custbig", 1000 )
oRs:nLastRec := oCn:QueryResult( "SELECT COUNT(*) FROM custbig" )
5. CRUD Operations 5.1 INSERT

Code: Select all | Expand

oCn:Insert( "clientes", "nombre,ciudad,salario", ;
   { { "Juan Lopez", "Madrid", 3500 } } )
5.2 SELECT

Code: Select all | Expand

nTotal := oCn:QueryResult( "SELECT COUNT(*) FROM clientes" )
5.3 UPDATE

Code: Select all | Expand

oCn:Execute( "UPDATE clientes SET salario = salario * 1.10" )
5.4 DELETE

Code: Select all | Expand

oCn:Execute( "DELETE FROM clientes WHERE id = 10" )
5.5 Update Master Totals from Detail

Code: Select all | Expand

oCn:UpdateSummary( "facturas", "id", "total,cantidad", ;
                   "lineas", "fid", "importe,qty" )
6. Table Management 6.1 Create Table

Code: Select all | Expand

oCn:CreateTable( "clientes", { ;
   { "nombre", "C", 30, 0 }, ;
   { "ciudad", "C", 20, 0 }, ;
   { "salario", "N", 10, 2, "DEFAULT 0" }, ;
   { "activo", "L", 1, 0 }, ;
   { "fecha", "D", 8, 0 }, ;
   { "notas", "M", 0, 0 } } )
Supported Data Types

FiveWin MariaDB Description

--------- -------------------- -------------------

C VARCHAR Short text
N DECIMAL Numeric
D DATE Date
L TINYINT(1) Logical (boolean)
M TEXT/BLOB Memo/long text
\+ INT AUTO_INCREMENT Auto-increment
= TIMESTAMP Auto date/time
@ DATETIME Date and time
\^ BLOB Binary data

7. Parent-Child Relationships

Code: Select all | Expand

oStates := oCn:RowSet( "states" )
oStates:AddChild( "SELECT * FROM customer WHERE state = states.code" )
8. Advanced Functions AutoAppend

Code: Select all | Expand

oRs:lAutoAppend := .t.
oRs:SetDefault( "docid", nDocId )
Pivot Table

Code: Select all | Expand

aPivot := oCn:PivotArray( "ventas", "region", "producto", "importe", "SUM" )
9. AES Encryption

MariaDB includes native AES_ENCRYPT() and AES_DECRYPT() functions for
secure storage of sensitive data.

10. Spatial Data (GEO)

MariaDB 5.7.1+ supports the POINT data type and geometric functions like
ST_Distance_Sphere().

11. Unicode and Encodings

Code: Select all | Expand

FW_SetUnicode( .t. )
12. Importing DBF to MariaDB

Code: Select all | Expand

oCn:ImportFromDbf( "clientes.dbf" )
13. FWMariaRecord: Single Record Editing

Code: Select all | Expand

oRec := FWMariaRecord():New( oCn, "clientes", "id = 1" )
oRec:Save()
14. FWH vs ADO vs Dolphin Comparison

Native FWH connector (maria_Connect) is recommended for new
applications due to higher performance and no need for external drivers.

15. Quick Method Reference Connection
  • maria_Connect()
  • FWCONNECT
  • FW_DemoDB()
  • oCn:Close()
Queries
  • oCn:RowSet()
  • oCn:RecSet()
  • oCn:QueryResult()
  • oCn:Execute()
Tables
  • oCn:CreateTable()
  • oCn:DropTable()
  • oCn:Insert()
  • oCn:ImportFromDbf()
RowSet
  • oRs:Delete()
  • oRs:ReQuery()
  • oRs:SetFilter()
  • oRs:Close()
regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

All the documentation files so you can provide them to the AI:

https://github.com/FiveTechSoft/FWH_too ... ariadb.zip

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

MANUAL DE MARIADB

para FiveWin / Harbour

Guia Practica Completa: Conexion, Consultas, CRUD, Migracion y Funciones Avanzadas

Basado en los ejemplos oficiales de FiveWin (maria01.prg \- maria20.prg)

Version 2025

Contenido

1\. Introduccion

2\. Conexion a MariaDB

3\. Comandos y Funciones Principales

4\. RowSet: Consultas y Edicion de Datos

5\. Operaciones CRUD

6\. Manejo de Tablas

7\. Relaciones Padre-Hijo

8\. Funciones Avanzadas

9\. Encriptacion AES

10\. Datos Espaciales (GEO)

11\. Unicode y Codificaciones

12\. Importar DBF a MariaDB

13\. FWMariaRecord: Edicion de Registros Individuales

14\. Comparacion FWH vs ADO vs Dolphin

15\. RecSet: Tablas Grandes y Rendimiento

16\. Bloqueo de Registros (Locking)

17\. Replicacion entre Servidores

18\. Sincronizacion DBF \<-\> MariaDB

19\. Totales Acumulados (Running Totals)

20\. Query Browser Interactivo

21\. Portar una Aplicacion DBF a MariaDB

22\. Referencia Rapida y Constantes

23\. Clase TField: Metadatos de Campos

1\. Introduccion

FiveWin for Harbour (FWH) incluye soporte nativo para MariaDB/MySQL a traves de la funcion maria\_Connect() y un conjunto de clases que permiten trabajar con bases de datos relacionales de forma sencilla, sin necesidad de drivers ODBC ni componentes externos.

Caracteristicas principales
  • Conexion directa al servidor MariaDB/MySQL via protocolo nativo TCP/IP

  • RowSets editables con soporte completo CRUD (Create, Read, Update, Delete)

  • RecSets paginados para tablas con millones de registros

  • Integracion total con XBrowse para visualizacion y edicion interactiva

  • Soporte Unicode (UTF-8) nativo

  • Tablas Pivot, relaciones padre-hijo, replicacion, cifrado AES, datos geoespaciales

  • Importacion/Exportacion desde/hacia ficheros DBF

  • Compatibilidad con Harbour y xHarbour, 32 y 64 bits

Requisitos
  • FiveWin for Harbour 25.x o superior

  • Servidor MariaDB 10.x / MySQL 5.7+ accesible en red o local

  • Incluir fivewin.ch en el programa

  • Habilitar Unicode: FW\_SetUnicode( .t. ) si se usan caracteres especiales

Ficheros necesarios FicheroDescripcion libmariadb.dllDLL cliente MariaDB (32 bits) libmariadb64.dllDLL cliente MariaDB (64 bits) libmariadb.lib / libmariadb32.libLibreria de enlace (32 bits) libmariadb64.lib / libmariadb64.aLibreria de enlace (64 bits) fwmaria.prgCodigo fuente de las clases principales mariarec.prgCodigo fuente de la clase FWMariaRecord

La DLL libmariadb.dll debe estar en la misma carpeta que el .exe o en una carpeta del PATH del sistema.

2\. Conexion a MariaDB 2.1 Funcion maria\_Connect()

La funcion principal para establecer la conexion es maria\_Connect(). Devuelve un objeto de conexion o NIL si falla.

Sintaxis

oCn := maria\_Connect( cConexion, lMostrarError )

oCn := maria\_Connect( cHost, cDatabase, cUser, cPassword \[, nPort\] )

oCn := maria\_Connect( { cHost, cDB, cUser, cPassword } )

Ejemplos de conexion

// Forma 1: cadena compacta host:puerto,base,usuario,password

oCn := maria\_Connect( "208.91.198.197:3306,fwhdemo,usuario,pwd", .t. )

// Forma 2: parametros separados (servidor local)

oCn := maria\_Connect( "localhost", "fwh", "usuario", "1234" )

// Forma 3: via comando FWCONNECT (fivewin.ch)

FWCONNECT oCn HOST cHost USER cUser PASSWORD cPassword DATABASE cDB

// Forma 4: array de parametros

oCn := maria\_Connect( { "localhost", "mibase", "root", "clave" } )

// Forma 5: funcion de demo de FiveWin

oCn := FW\_DemoDB() // servidor de demo

oCn := FW\_DemoDB( "ADO" ) // via ADO

oCn := FW\_DemoDB( "DLP" ) // via TDolphin

// Forma 6: conexion embebida (sin servidor, solo 32 bits)

oCn := maria\_Embedded( cDataFolder, "mibase", cLangFolder )

Verificar conexion y cerrar

if ( oCn := maria\_Connect( "host,db,user,pwd", .t. ) ) \== nil

MsgStop( "No se pudo conectar" )

return nil

endif

// ... operaciones ...

oCn:Close() // o oCn:End()

2.2 Propiedades del objeto de conexion PropiedadTipoDescripcion oCn:lOpenL.t. si la conexion esta activa oCn:cDBCNombre de la base de datos actual oCn:lLogErrLActiva el registro de errores en archivo .log oCn:lShowErrorsLMuestra errores en pantalla automaticamente oCn:nErrorNCodigo del ultimo error (0 \= sin error) oCn:LockTimeOutNTiempo de espera para bloqueos (segundos) oCn:lUnicodeLModo Unicode activo oCn:cLastSQLCUltima sentencia SQL ejecutada oCn:pMySqlPPuntero interno a la conexion MySQL

oCn:lLogErr := .t. // activa log de errores

oCn:lShowErrors := .t. // muestra errores en pantalla

aErr := maria\_ConnectError() // { nErrorCode, cErrorMsg } del ultimo intento

3\. Comandos y Funciones Principales 3.1 Metodos del objeto de conexion MetodoDescripcion oCn:RowSet( cSQL \[, aParams\] )Ejecuta consulta y devuelve un RowSet editable en memoria oCn:RecSet( cTabla, nRecs )Lee n registros con paginacion (tablas grandes) oCn:Query( cSQL )Ejecuta consulta (compatible TDolphin) oCn:Execute( cSQL )Ejecuta sentencia SQL sin resultado (INSERT, UPDATE, DDL...) oCn:QueryResult( cSQL )Devuelve el valor de la primera celda del resultado oCn:ListTables()Devuelve array con nombres de tablas de la BD oCn:TableExists( cTabla )Devuelve .t. si la tabla existe oCn:TableStructure( cTabla )Devuelve estructura de la tabla oCn:CreateTable( cTabla, aStruct \[,lPK\] \[,cCharset\] )Crea una tabla nueva oCn:DropTable( cTabla )Elimina una tabla oCn:Insert( cTabla, cCampos, aData )Inserta uno o multiples registros oCn:Upsert( cTabla, nil, aVals )INSERT ... ON DUPLICATE KEY UPDATE oCn:UpdateSummary( ... )Actualiza totales de tabla maestra desde detalle oCn:ImportFromDbf( cArchivo \[,...\] )Importa un archivo DBF a MariaDB oCn:CopyTableToServer( cTabla, oOtroCn )Copia tabla a otro servidor oCn:PivotArray( cTabla, cFila, cCol, cVal, cAggr )Genera tabla pivot oCn:BeginTransaction()Inicia transaccion oCn:CommitTransaction()Confirma transaccion oCn:RollBack()Revierte transaccion oCn:SetAutoCommit( lVal )Activa/desactiva auto commit oCn:Close() / oCn:End()Cierra la conexion oCn:ShowError()Muestra el ultimo error oCn:ValToSql( uVal )Convierte valor Harbour a literal SQL oCn:ApplyParams( cSql, aParams )Sustituye ? por valores en SQL
3.2 Configuracion inicial recomendada

\#include "fivewin.ch"

function Main()

local oCn

SET DATE ITALIAN // formato dd/mm/yyyy

SET CENTURY ON

FW\_SetUnicode( .t. ) // habilitar Unicode

FWNumFormat( "A", .t. ) // formato numerico

oCn := maria\_Connect( "localhost", "mibd", "usuario", "clave" )

if oCn \== nil

Code: Select all | Expand

  MsgStop( "Error de conexion" )

  return nil

endif

oCn:lShowErrors := .t.

// ... codigo ...

oCn:Close()

return nil

3.3 Transacciones

// Metodo 1: desactivar auto-commit

oCn:SetAutoCommit( .f. )

// ... multiples INSERT/UPDATE ...

oCn:SetAutoCommit( .t. ) // commit implicito

// Metodo 2: transacciones explicitas

oCn:BeginTransaction()

oCn:Execute( "UPDATE cuentas SET saldo \= saldo \- 1000 WHERE id \= 1" )

oCn:Execute( "UPDATE cuentas SET saldo \= saldo \+ 1000 WHERE id \= 2" )

if oCn:nError \== 0

oCn:CommitTransaction()

else

oCn:RollBack()

endif

4\. RowSet: Consultas y Edicion de Datos

El RowSet es el objeto central para trabajar con datos en FiveWin/MariaDB. Representa un conjunto de registros cargados en memoria que puede mostrarse en un XBrowse y editarse directamente. Permite ordenacion y filtrado local sin consultas adicionales al servidor.

4.1 Crear un RowSet

// Tabla completa

oRs := oCn:RowSet( "clientes" )

// Acceso directo por nombre de tabla

oRs := oCn:clientes // equivale a oCn:RowSet('clientes')

// Con SQL personalizado

oRs := oCn:RowSet( "SELECT \* FROM clientes WHERE activo \= 1" )

// SQL con parametros (evita SQL injection)

oRs := oCn:RowSet( "SELECT \* FROM clientes WHERE estado \= ?", { cEstado } )

// JOIN entre tablas

TEXT INTO cSql

SELECT C.ID AS CustID, C.FIRST AS CustName,

Code: Select all | Expand

      C.STATE AS ST, S.NAME AS StateName

FROM customer C

LEFT OUTER JOIN states S ON C.STATE \= S.CODE

ORDER BY CUSTID

ENDTEXT

oRs := oCn:RowSet( cSql )

4.2 Mostrar en XBrowse

// Modo simple

XBROWSER oRs FASTEDIT TITLE "Clientes"

// Con opciones

XBROWSER oRs FASTEDIT AUTOSORT AUTOFIT SHOW RECID TITLE "Clientes" ;

SETUP ( oBrw:lFastEdit := .f., oBrw:aCols\[4\]:cEditPicture := "@\!" )

// Control total programatico

@ 50,20 XBROWSE oBrw SIZE \-20,-20 PIXEL OF oDlg ;

DATASOURCE oRs AUTOCOLS CELL LINES NOBORDER FOOTERS FASTEDIT

WITH OBJECT oBrw

:nEditTypes := EDIT\_GET

:Salary:nFooterType := AGGR\_SUM

:MakeTotals()

:CreateFromCode()

END

4.3 Navegacion y acceso a campos MetodoDescripcion oRs:GoTop() / GoBottom()Ir al primer/ultimo registro oRs:Skip(n)Saltar n registros oRs:GoTo(nRec)Ir al registro nRec oRs:Bof() / Eof()Inicio/fin de datos oRs:RecNo()Numero de registro actual (ID del servidor) oRs:KeyCount() / LastRec()Total de registros oRs:BookMarkLeer/asignar marcador de posicion oRs:campoLeer/escribir campo directamente (forma mas comoda) oRs:FieldGet(n) / FieldPut(n,v)Por posicion numerica oRs:FieldGet("NOMBRE")Por nombre de campo oRs:FCount()Numero total de campos oRs:FieldName(n) / FieldType(n)Nombre y tipo del campo n
4.4 Ordenacion, filtros y busqueda

// Ordenar

oRs:SetOrder( "FIRST" ) // ascendente

oRs:SetOrder( "SALARY", , .t. ) // descendente

// Filtrar (localmente sobre datos ya leidos)

oRs:SetFilter( "STATE \= ?", { "CA" } )

oRs:ReFilter( { "NY" } ) // re-filtrar con nuevo parametro

// Busqueda

oRs:Seek( "John" ) // exacto

oRs:Seek( "Jo", .t. ) // parcial (soft)

oRs:Locate( {|| oRs:Salary \> 50000 } )

4.5 Guardado, recarga y exportacion

oRs:Save() // genera UPDATE solo para columnas cambiadas

oRs:Cancel() // cancela cambios pendientes

oRs:Delete() // borra registro actual

oRs:AppendBlank() // agrega registro en blanco

oRs:ReSync() // re-leer registro actual del servidor

oRs:ReQuery() // re-leer todos los datos con mismo SQL

oRs:ReQuery( { 'NY' } ) // re-leer con nuevos parametros

oRs:ReQuery( cNewSql ) // re-leer con nuevo SQL

oRs:ToExcel() // exportar a Excel

cJson := oRs:ToJson() // exportar a JSON

aRows := oRs:GetRows() // obtener como array

4.6 Propiedades importantes del RowSet PropiedadTipoDescripcion oRs:SourceCSQL fuente del RowSet oRs:lReadOnlyL.t. si el RowSet es de solo lectura oRs:lAutoAppendLHabilitar auto-agregar desde browse oRs:lBatchModeLModo batch para ediciones masivas oRs:nReadSecsNSegundos que tardo la lectura oRs:aPrimaryAColumnas de clave primaria oRs:oChildORowSet hijo enlazado oRs:oReplServerOServidor de replicacion oRs:cLastSQLCUltima sentencia SQL ejecutada oRs:bTriggerBBloque trigger antes de guardar
5\. Operaciones CRUD 5.1 INSERT \- Insertar registros

// Un registro

"clientes", "nombre,ciudad,salario", { "Juan Lopez", "Madrid", 3500 }

oCn:Insert( "clientes", "nombre,ciudad,salario", ;

{ { "Juan Lopez", "Madrid", 3500 } } )

// Multiples registros de una vez (mucho mas eficiente)

local aData := {}

for i := 1 to 1000

AAdd( aData, { "Nombre" \+ Str(i), "Ciudad", HB\_RandomInt(1000,9999) } )

next

oCn:SetAutoCommit( .f. ) // inicio de transaccion

"clientes", "nombre,ciudad,salario", aData )

oCn:Insert( "clientes", "nombre,ciudad,salario", aData )

oCn:SetAutoCommit( .t. ) // commit

Upsert: INSERT o UPDATE automatico

// Inserta si no existe, actualiza si ya existe (basado en clave primaria)

oCn:Upsert( "clientes", nil, aValues )

5.2 SELECT \- Consultar datos

// Valor unico (escalar)

nTotal := oCn:QueryResult( "SELECT COUNT(\*) FROM clientes" )

nSuma := oCn:QueryResult( "SELECT SUM(salario) FROM clientes" )

// Resultado como array

aEstados := oCn:Execute( "SELECT CODE, NAME FROM states" )

// RowSet completo

oRs := oCn:RowSet( "SELECT \* FROM clientes WHERE ciudad \= ?", { cCiudad } )

5.3 UPDATE \- Actualizar datos

// Directo via SQL

oCn:Execute( "UPDATE clientes SET salario \= salario \* 1.10 WHERE ciudad \= 'Madrid'" )

// A traves del RowSet

oRs := oCn:RowSet( "SELECT \* FROM clientes WHERE id \= ?", { nId } )

oRs:salario := 4000

oRs:Save() // genera UPDATE solo para columnas cambiadas

5.4 DELETE \- Eliminar registros

// Via SQL

oCn:Execute( "DELETE FROM clientes WHERE id \= " \+ cValToChar( nId ) )

// Via RowSet

oRs:Delete()

oRs:ReQuery() // recargar datos

// Desde boton en browse

@ 20,20 BTNBMP PROMPT "Delete" SIZE 60,20 PIXEL FLAT OF oDlg ;

ACTION ( oRs:Delete(), oRs:ReQuery(), oBrw:MakeTotals(), oBrw:Refresh() )

5.5 Actualizar tabla maestra con totales del detalle

// Parametros: maestro, PK, campos\_maestro, detalle, FK, campos\_detalle

"facturas", "id", "total,cantidad", ;

oCn:UpdateSummary( "facturas", "id", "total,cantidad", ;

Code: Select all | Expand

               "lineas",   "fid", "importe,qty" )

// Ver el SQL generado

? MYSQL\_UpdateSummarySQL( "facturas", "id", "total,cantidad", ;

Code: Select all | Expand

                      "lineas",   "fid", "importe,qty" )

Util para mantener totales en cabeceras de facturas/pedidos actualizados automaticamente desde las lineas de detalle.

6\. Manejo de Tablas 6.1 Crear tabla

// Estructura: { nombre, tipo, longitud, decimales \[, opciones\] }

oCn:CreateTable( "clientes", { ;

{ "nombre", 'C', 30, 0 }, ;

{ "ciudad", 'C', 20, 0 }, ;

{ "salario", 'N', 10, 2, "DEFAULT 0" }, ;

{ "activo", 'L', 1, 0 }, ;

{ "fecha", 'D', 8, 0 }, ;

{ "notas", 'M', 0, 0 } } )

6.2 Tipos de datos Tipo FWHTipo MariaDBDescripcion CVARCHARCaracter / texto corto NDECIMALNumerico DDATEFecha LTINYINT(1)Logico (booleano) MTEXT / BLOBMemo / texto largo \+INT AUTO\_INCREMENTEntero autoincrementable (clave primaria) \=TIMESTAMPFecha/hora automatica (se actualiza al modificar) @DATETIMEFecha y hora ^BLOBDatos binarios
6.3 Opciones especiales en columnas

// Auto-increment (se agrega siempre como campo 'id')

{ "id", "+", 4, 0 }

// Con comentarios para comportamiento especial

{ "codigo", 'C', 10, 0, "comment 'case:upper'" }, // fuerza mayusculas

{ "nombre", 'C', 30, 0, "comment 'case:proper'" }, // primera letra mayuscula

// Columna computada (virtual)

{ "amount=qty\*rate", 'N', 12, 2 } // amount se calcula automaticamente

// Con charset especifico por campo

{ "texto\_utf8", 'C', 40, 0, "utf8" }

{ "texto\_latin", 'C', 40, 0, "latin1" }

// Clave foranea

{ "pid", "REFERENCES tabla\_padre( id )" }

6.4 Verificar, listar y eliminar tablas

if oCn:TableExists( "clientes" )

oCn:DropTable( "clientes" )

endif

// Listar todas las tablas

XBROWSER oCn:ListTables() TITLE "Tablas disponibles"

// Obtener estructura

aStruct := oCn:TableStructure( "clientes" )

6.5 Crear tabla con SQL directo y triggers

TEXT INTO cSql

CREATE TABLE usuarios (

id INT AUTO\_INCREMENT PRIMARY KEY,

nombre VARCHAR(30),

pass VARBINARY(255)

) ENGINE=InnoDB CHARSET=latin1

ENDTEXT

oCn:Execute( cSql )

// Trigger

TEXT INTO cSql

CREATE TRIGGER mitabla\_bi BEFORE INSERT ON mitabla

FOR EACH ROW

BEGIN

SET NEW.username \= SUBSTRING(CONCAT\_WS(', ', @os\_user, @pc\_name), 1, 30);

END

ENDTEXT

oCn:Execute( "DROP TRIGGER IF EXISTS mitabla\_bi" )

oCn:Execute( cSql )

7\. Relaciones Padre-Hijo 7.1 Un padre con un hijo

// Metodo 1: AddChild con SQL

oStates := oCn:RowSet( "states" )

oStates:AddChild( "SELECT \* FROM customer WHERE state \= states.code" )

// Sincronizar al cambiar de registro padre

oBrwParent:bChange := { || oStates:SyncChild(), ;

Code: Select all | Expand

                       oBrwChild:GoTop(), oBrwChild:Refresh() }

// Metodo 2: AddChild con RowSet existente

oStates := oCn:RowSet( "states" )

oCust := oCn:RowSet( "customer" )

oStates:AddChild( oCust, "state \= states.code" )

// Metodo 3: OrdSetRelation (alternativo)

oStates:OrdSetRelation( oChildRs, "state \= ?", { oStates:code } )

7.2 Un padre con dos hijos (SetFilter)

oRsState := oCn:RowSet( "SELECT \* FROM states ORDER BY name" )

oRsCity := oCn:RowSet( "SELECT id, state, city FROM customer" )

oRsCust := oCn:RowSet( "SELECT id, state, first FROM customer" )

// Filtros parametrizados vinculados al padre

oRsCity:SetFilter( "STATE \= ?", { oRsState:code } )

oRsCust:SetFilter( "STATE \= ?", { oRsState:code } )

// Al cambiar en el padre, re-filtrar ambos hijos

oBrwState:bChange := \<||

oRsCity:ReFilter( { oRsState:code } )

oRsCust:ReFilter( { oRsState:code } )

oBrwCity:Refresh()

oBrwCust:Refresh()

return nil \>

8\. Funciones Avanzadas 8.1 AutoAppend: agregar registros desde el browse

oRs := oCn:RowSet( "SELECT \* FROM detalle WHERE docid \= ?", { nDocId } )

oRs:lAutoAppend := .t. // habilita agregar con flecha abajo

oRs:SetDefault( "docid", nDocId ) // valor fijo para campo clave

// Para cambiar de documento mas tarde:

// oRs:ReQuery( { nOtherDocId } )

// oRs:SetDefault( "docid", nOtherDocId )

XBROWSER oRs FASTEDIT TITLE "Detalle del documento" SETUP SetUpBrowse( oBrw )

static function SetUpBrowse( oBrw )

WITH OBJECT oBrw

Code: Select all | Expand

  :bClrRowFocus := { || If( oBrw:oDbf:ID \== 0, ;

     { CLR\_BLACK, 0xAEEDFF }, { CLR\_BLACK, RGB(185,220,255) } ) }

  :lFooter := .t.

  :qty:nFooterType    := AGGR\_SUM

  :Amount:nFooterType := AGGR\_SUM

  :MakeTotals()

  :bPainted := { |dc,ps,brw| If( brw:nLen \== 0, brw:SayText( "Press Down Arrow" ), nil ) }

END

8.2 EditBaseRecord: editar registro completo

Cuando el RowSet contiene solo algunas columnas, EditBaseRecord() permite editar el registro completo de la tabla base con un formulario personalizado.

oRs := oCn:RowSet( "SELECT ID,FIRST,CITY,SALARY FROM customer" )

@ 20, 20 BTNBMP PROMPT "ADD" PIXEL FLAT OF oDlg ;

ACTION oRs:EditBaseRecord( nil, .t., { |oRec| MyEditDlg( oRec ) }, oBrw )

@ 20,130 BTNBMP PROMPT "EDIT" PIXEL FLAT OF oDlg ;

ACTION oRs:EditBaseRecord( nil, .f., { |oRec| MyEditDlg( oRec ) }, oBrw, .t. )

static function MyEditDlg( oRec )

local lNew := ( oRec:RecNo \== 0 )

@ 060,120 GET oRec:First SIZE 240,22 PIXEL OF oDlg VALID \!Empty(oRec:First)

@ 270,120 CHECKBOX oRec:Married PROMPT "" SIZE 22,22 PIXEL OF oDlg

@ 330,120 GET oRec:Salary SIZE 240,22 PIXEL OF oDlg PICTURE "999,999.99" RIGHT

// DBCOMBO para estado desde lista

@ 180,120 DBCOMBO oRec:State SIZE 240,300 PIXEL OF oDlg ALIAS aStates ITEMFIELD "1" LISTFIELD "2"

@ 420,020 BTNBMP PROMPT "Save" PIXEL FLAT OF oDlg ;

Code: Select all | Expand

  ACTION ( If( oRec:Modified(), oRec:Save(), nil ), oDlg:End() ) WHEN oRec:Modified()
8.3 Tabla Pivot

// Parametros: tabla, campo\_fila, campo\_columna, campo\_valor, funcion\_agregacion

aPivot := oCn:PivotArray( "ventas", "region", "producto", "importe", "SUM" )

// Funciones: SUM, COUNT, AVG

@ 30,10 XBROWSE oBrw SIZE \-10,-10 PIXEL OF oDlg ;

DATASOURCE aPivot AUTOCOLS CELL LINES FOOTERS NOBORDER

WITH OBJECT oBrw

:bRClicked := { || oBrw:InvertPivot() } // click derecho invierte ejes

:CreateFromCode()

END

8.4 Batch Mode: ediciones masivas

oRs:SetBatchMode( .t. ) // activar modo batch

// ... multiples ediciones ...

oRs:SaveBatch() // guardar todo de una vez

// o

oRs:CancelBatch() // cancelar todas las ediciones

? oRs:IsBatchEdited() // .t. si hay cambios pendientes

9\. Encriptacion AES

MariaDB incluye funciones nativas AES\_ENCRYPT() y AES\_DECRYPT() para almacenar datos sensibles como contrasenas y credenciales de forma segura.

9.1 Crear tabla con campos encriptados

TEXT INTO cSql

CREATE TABLE \`usuarios\_enc\` (

\`id\` int(11) NOT NULL AUTO\_INCREMENT,

\`name\` varchar(20) DEFAULT NULL,

\`login\` tinyblob,

\`pass\` varbinary(255),

PRIMARY KEY (\`id\`)

) ENGINE=InnoDB CHARSET=latin1

ENDTEXT

oCn:Execute( cSql )

Los campos encriptados deben ser de tipo TINYBLOB, BLOB o VARBINARY, ya que AES\_ENCRYPT() produce datos binarios.

9.2 Insertar y actualizar datos encriptados

cSql := "INSERT INTO usuarios\_enc ( name, login, pass ) VALUES ( " \+ ;

Code: Select all | Expand

    "'andrew', AES\_ENCRYPT( 'Andrew', 'miClave' ), " \+ ;

    "AES\_ENCRYPT( 'Pass123', 'miClave' ) )"

oCn:Execute( cSql )

cSql := "UPDATE usuarios\_enc SET pass \= " \+ ;

Code: Select all | Expand

    "AES\_ENCRYPT( 'NuevoClave', 'miClave' ) WHERE id \= 1"

oCn:Execute( cSql )

9.3 Consultar con desencriptacion

cSql := "SELECT id, name, " \+ ;

Code: Select all | Expand

    "AES\_DECRYPT(login,'miClave') AS login, " \+ ;

    "AES\_DECRYPT(pass, 'miClave') AS pass " \+ ;

    "FROM usuarios\_enc"

oRs := oCn:RowSet( cSql )

XBROWSER oRs

oRs:Close()

La clave de encriptacion nunca se almacena en la base de datos. Guardala de forma segura en tu aplicacion o en un archivo de configuracion externo.

10\. Datos Espaciales (GEO)

Requiere MariaDB/MySQL 5.7.1+. Soporta el tipo POINT y funciones geometricas para calcular distancias entre coordenadas geograficas.

10.1 Crear tabla e insertar coordenadas

oCn:Execute( "CREATE TABLE ciudades (" \+ ;

"id INT AUTO\_INCREMENT PRIMARY KEY, " \+ ;

"ciudad VARCHAR(20), pt POINT )" )

TEXT INTO cSql

INSERT INTO ciudades (ciudad, pt) VALUES

( 'Madrid', POINT( 3.7038, 40.4168 ) ),

( 'Paris', POINT( 2.3522, 48.8566 ) ),

( 'London', POINT( 0.1278, 51.5074 ) )

ENDTEXT

oCn:Execute( cSql )

10.2 Crear funcion de distancia en el servidor

oCn:Execute( "DROP FUNCTION IF EXISTS distancia" )

TEXT INTO cSql

CREATE FUNCTION distancia( ciudad1 VARCHAR(20), ciudad2 VARCHAR(20) )

RETURNS DOUBLE

BEGIN

Code: Select all | Expand

 DECLARE p1 POINT;

 DECLARE p2 POINT;

 SELECT pt INTO p1 FROM ciudades WHERE ciudad \= ciudad1;

 SELECT pt INTO p2 FROM ciudades WHERE ciudad \= ciudad2;

 RETURN ST\_Distance\_Sphere( p1, p2 );

END

ENDTEXT

oCn:Execute( cSql )

// Llamar desde FiveWin (resultado en metros)

? oCn:distancia( "Madrid", "Paris" )

11\. Unicode y Codificaciones 11.1 Activar Unicode

FW\_SetUnicode( .t. ) // activar ANTES de conectar

oCn := maria\_Connect( "localhost", "mibd", "user", "pass" )

11.2 Charset por tabla y por campo

oCn:CreateTable( "testunicode", { ;

{ "language", 'C', 15, 0, "latin1 comment 'case:upper'" }, ;

{ "unicodetext", 'C', 40, 0, "utf8" }, ;

{ "username", 'C', 30, 0, "utf8" }, ;

{ "writedt", '=', 8, 0 } }, nil, "utf8" )

11.3 Charset en ImportFromDbf

oCn:ImportFromDbf( "clientes.dbf",,,,,, "utf8\_spanish2\_ci" )

11.4 Triggers para registrar usuario automaticamente

TEXT INTO cSql

CREATE TRIGGER tabla\_bi BEFORE INSERT ON mitabla

FOR EACH ROW

BEGIN

SET NEW.username \= SUBSTRING(CONCAT\_WS(', ', @os\_user, @pc\_name), 1, 30);

END

ENDTEXT

oCn:Execute( "DROP TRIGGER IF EXISTS tabla\_bi" )

oCn:Execute( cSql )

12\. Importar DBF a MariaDB 12.1 Importacion basica

RDDSETDEFAULT( "DBFCDX" )

SET DELETED ON

FW\_SetUnicode( .f. )

oCn := maria\_Connect( "localhost", "fwh", "usuario", "clave" )

oCn:lLogErr := .t.

oCn:ImportFromDbf( "clientes.dbf",,,,,, "utf8\_spanish2\_ci" )

oRs := oCn:RowSet( "clientes" )

XBROWSER oRs FASTEDIT AUTOSORT

// Revisar log de errores si existe

if File( cFileSetExt( ExeName(), "log" ) )

WinExec( "notepad.exe " \+ cFileSetExt( ExeName(), "log" ) )

endif

12.2 Copiar tabla entre servidores

oMain := FW\_DemoDB()

oRepl := FW\_DemoDB( 6 )

oMain:CopyTableToServer( "states", oRepl )

12.3 Proceso recomendado de migracion
  • Conectar a MariaDB con oCn

  • Activar log: oCn:lLogErr := .t.

  • Llamar a oCn:ImportFromDbf() para cada tabla DBF

  • Revisar el archivo .log por si hubiera errores de conversion

  • Verificar los datos importados con XBROWSER oCn:RowSet(cTabla)

  • Ajustar charsets si hay problemas con caracteres especiales

13\. FWMariaRecord: Edicion de Registros Individuales

La clase FWMariaRecord (mariarec.prg) hereda de TDataRow y permite leer, editar y guardar un registro individual de MariaDB. Es ideal para formularios de alta/edicion.

13.1 Crear y usar un FWMariaRecord

// Leer un registro existente

oRec := FWMariaRecord():New( oCn, "clientes", "id \= " \+ cValToChar( nId ) )

// Registro en blanco (para alta)

oRec := FWMariaRecord():New( oCn, "clientes" )

oRec:Blank()

// Acceder y modificar campos

oRec:nombre := "Juan Garcia"

oRec:salario := 3500

// Guardar cambios

if oRec:Modified()

if oRec:Save()

Code: Select all | Expand

  ? "Guardado correctamente"

endif

endif

Save() usa internamente oCn:Upsert(): si el registro es nuevo hace INSERT, si existe hace UPDATE. Despues recarga el registro del servidor para obtener valores actualizados (auto-increment, timestamps, etc.).

13.2 Propiedades y metodos Propiedad / MetodoDescripcion oRec:lValidData.t. si el registro tiene datos validos oRec:RecNoNumero de registro (0 \= nuevo) oRec:oCnObjeto de conexion oRec:cTableNombre de la tabla oRec:aPrimaryIndices de campos de clave primaria oRec:nAutoIncFldIndice del campo auto-incremento oRec:New( oCn, cTabla, cWhere )Constructor oRec:Read( cWhere )Carga un registro segun condicion WHERE oRec:Blank()Carga un registro en blanco para insercion oRec:Load( \[lBlank\] )Recarga el registro del servidor oRec:Save()Guarda los cambios (INSERT o UPDATE segun corresponda) oRec:Modified()Devuelve .t. si hubo cambios sin guardar oRec:MakePrimaryWhere()Genera clausula WHERE desde clave primaria oRec:campoAcceso directo a cada campo como propiedad
14\. Comparacion FWH vs ADO vs Dolphin AspectoFWH NativoADOTDolphin Conexionmaria\_Connect()FW\_DemoDB('ADO')FW\_DemoDB('DLP') QueryoCn:RowSet(cSql)FW\_OpenRecordSet(oCn,cSql)oCn:Query(cSql) Cerrar RSoRs:Close()oRs:Close()oRs:End() Cerrar CNoCn:Close()oCn:Close()oCn:End() Driver extraNo (nativo)Necesita ODBCNecesita TDolphin Velocidad lecturaMuy altaMediaAlta Trafico de redMinimoAltoMedio EdicionCompleta \+ localVia servidorVia servidor RecomendadoSi (primera opcion)Solo si ya existeCompatible

Para nuevas aplicaciones se recomienda siempre el conector nativo FWH (maria\_Connect). Es el mas rapido, no requiere drivers adicionales y tiene mejor integracion con las clases de FiveWin.

Benchmark de rendimiento

cSql := "SELECT \* FROM custbig WHERE id \<= 51000"

nSecs := SECONDS()

MsgRun( "Reading", "FWH", { || oRsFWH := oCn:RowSet( cSql ) } )

nFWHSecs := SECONDS() \- nSecs

nSecs := SECONDS()

MsgRun( "Reading", "ADO", { || oRsADO := FW\_OpenRecordSet( oAdo, cSql ) } )

nAdoSecs := SECONDS() \- nSecs

15\. RecSet: Tablas Grandes y Rendimiento

El RecSet esta disenado para tablas muy grandes (millones de registros). A diferencia del RowSet que carga todo en memoria, el RecSet usa paginacion para cargar solo una porcion a la vez, cargando mas datos bajo demanda cuando el usuario navega.

15.1 Creacion

// RecSet con pagina de 1000 registros

oRs := oCn:RecSet( "custbig", 1000 )

// RecSet leyendo los ultimos 100 registros primero

oRs := oCn:RecSet( "custbig", \-100 )

// Establecer el total de registros (para barra de scroll correcta)

oRs:nLastRec := oCn:QueryResult( "SELECT COUNT(\*) FROM custbig" )

// Ejemplo completo con tabla de millon de registros

nSecs := SECONDS()

MsgRun( "Reading custbig", "Please wait", { || ;

oRs := oCn:RecSet( "custbig", \-100 ), ;

nSum := oCn:QueryResult( "SELECT SUM(SALARY) FROM custbig" ) } )

nSecs := SECONDS() \- nSecs

XBROWSER oRs TITLE "CUSTBIG (" \+ cValToChar( nSecs ) \+ " seconds)" ;

FASTEDIT SHOW RECID ;

SETUP ( ;

oBrw:id:cEditPicture := "99,999,999", ;

oBrw:nFreeze := 1, ;

oBrw:Salary:nFooterType := AGGR\_SUM, ;

oBrw:Salary:nTotal := nSum )

15.2 Propiedades del RecSet PropiedadTipoDescripcion nPageSizeNTamano de pagina (por defecto 1000\) nLastRecNUltimo registro total nAtNPosicion actual dentro de la pagina nOffsetNOffset de la pagina actual nReadSecsNTiempo de lectura aStructAEstructura de campos aDataADatos de la pagina actual
16\. Bloqueo de Registros (Locking)

Para aplicaciones multiusuario, el bloqueo pesimista con FOR UPDATE garantiza que solo un usuario pueda editar un registro a la vez.

Patron de bloqueo pesimista

static function LockedEdit( oBrw )

local oRsMain, oRsEdit, oRec, nWait, oCn

oRsMain := oBrw:oDbf

oCn := oRsMain:oCn

// Guardar timeout actual y poner uno corto

nWait := oCn:QueryResult( "SHOW SESSION VARIABLES LIKE 'innodb\_lock\_wait\_timeout'" )\[ 2 \]

oCn:Execute( "SET SESSION innodb\_lock\_wait\_timeout \= 1" )

// Iniciar transaccion y bloquear el registro

oCn:BeginTransaction()

MsgRun( "Reading Record", "WAIT", { || ;

Code: Select all | Expand

  oRsEdit := oCn:RowSet( ;

     "SELECT \* FROM customer WHERE ID \= ? FOR UPDATE", ;

     { oRsMain:ID } ) } )

if oCn:nError \== 0

Code: Select all | Expand

  // Registro bloqueado OK \- editar

  oCn:Execute( "SET SESSION innodb\_lock\_wait\_timeout \= " \+ cValToChar( nWait ) )

  oRec := TDataRow():New( oRsEdit )

  oRec:Edit()

  oCn:CommitTransaction()

  oRsMain:ReSync()

  oBrw:RefreshCurrent()

else

Code: Select all | Expand

  // No se pudo bloquear \- otro usuario lo tiene

  oCn:RollBack()

  ? "Registro bloqueado por otro usuario"

endif

17\. Replicacion entre Servidores

Con oRs:oReplServer, cada INSERT, UPDATE o DELETE ejecutado en el RowSet principal se replica automaticamente al servidor de replica.

oMain := FW\_DemoDB() // servidor principal

oRepl := FW\_DemoDB( 6 ) // servidor de replica

// Primera vez: copiar tabla al servidor replica

// oMain:CopyTableToServer( "states", oRepl )

oRsMain := oMain:RowSet( "states" )

oRsMain:oReplServer := oRepl // ACTIVAR REPLICACION

oRsRepl := oRepl:RowSet( "states" )

// Cada cambio en oRsMain se replica automaticamente a oRepl

oBrwMain:nEditTypes := EDIT\_GET

oBrwMain:bOnChanges := { || oRsRepl:ReSync(), oBrwRepl:RefreshCurrent() }

oBrwMain:lColChangeNotify := .t.

18\. Sincronizacion DBF \<-\> MariaDB

Permite mantener el DBF como fuente principal mientras todos los cambios se replican en tiempo real al servidor MariaDB, ideal para una migracion gradual.

oCn := FW\_DemoDB()

// Crear DBF e importarlo a MariaDB

oCn:DropTable( "statex" )

oCn:ImportFromDBF( "statex.dbf" )

// Abrir DBF y activar replicacion

oDbf := TDatabase():Open( nil, "statex", "DBFCDX", .t. )

oDbf:SetReplicationServer( oCn ) // \<- CLAVE

// Ahora cada modificacion en el DBF se replica a MariaDB

// El SQL generado se puede ver en:

? oCn:cLastSQL

Este patron es ideal para una migracion gradual: el DBF sigue funcionando y MariaDB recibe una copia en tiempo real. Ver ejemplo completo en mariasyn.prg.

19\. Totales Acumulados (Running Totals)

El patron @bal calcula saldos acumulados directamente en el servidor MariaDB, sin procesar en el cliente.

TEXT INTO cSql

SELECT id, fecha, descripcion, tipo, importe,

( @bal := IF( tipo \= '1', @bal \+ importe, @bal \- importe ) ) AS nsaldo

FROM ctacte,

( SELECT @bal := 0 ) AS t

WHERE ncli \= ?

ORDER BY fecha

ENDTEXT

oRs := oCn:RowSet( cSql, { 101 } ) // parametro: nCliente

oRs:GoBottom()

@ 50,20 XBROWSE oBrw SIZE \-20,-20 PIXEL OF oDlg ;

DATASOURCE oRs ;

COLUMNS 'id', 'Fecha', 'Descripcion', ;

Code: Select all | Expand

  'If( tipo \== ' \+ Chr(39) \+ '1' \+ Chr(39) \+ ', importe, 0 )', ;

  'If( tipo \== ' \+ Chr(39) \+ '1' \+ Chr(39) \+ ', 0, importe )', ;

  'nsaldo' ;

HEADERS 'DocID', nil, nil, 'DEBE', 'PAGO', 'SALDO' ;

CELL LINES NOBORDER FOOTERS FASTEDIT

WITH OBJECT oBrw

:Debe:nFooterType := AGGR\_SUM

:Pago:nFooterType := AGGR\_SUM

:Saldo:bFooter := { || oBrw:Debe:nTotal \- oBrw:Pago:nTotal }

:MakeTotals()

:CreateFromCode()

END

20\. Query Browser Interactivo

Permite escribir cualquier SQL y ver los resultados inmediatamente en un XBrowse. Usa oRs:Requery(cSql) para cambiar la consulta dinamicamente.

FW\_SetUnicode( .t. )

oCn := FW\_DemoDB()

cSql := "SELECT Now()"

oRs := oCn:RowSet( cSql )

// Area de edicion SQL \- al validar re-ejecuta

@ 20,20 GET oGet VAR cSql MEMO SIZE 840,150 PIXEL OF oDlg FONT oFixed

oGet:bValid := \<|o|

if Upper( cSql ) \== Upper( oRs:Source )

Code: Select all | Expand

  return .t.

endif

if oRs:Requery( cSql )

Code: Select all | Expand

  oRs:SetXbrColumns( oBrw )

  oBrw:nEditTypes := EDIT\_GET

  oBrw:GoTop()

  oBrw:Refresh()

  return .t.

endif

MsgAlert( "Invalid SQL" )

return .f. \>

// Browse con resultados

@ 190,20 XBROWSE oBrw SIZE \-20,-20 PIXEL OF oDlg ;

DATASOURCE oRs AUTOCOLS CELL LINES NOBORDER FOOTERS FASTEDIT

21\. Portar una Aplicacion DBF a MariaDB

Migrar una aplicacion FWH de DBF a MariaDB puede hacerse tabla por tabla, de forma gradual. Las principales ventajas son: acceso multiusuario concurrente, SQL para consultas complejas, rendimiento superior con grandes volumenes e integridad referencial.

21.1 Tabla de Equivalencias DBF \-\> MariaDB Operacion DBFEquivalente MariaDB/FWH USE "clientes"oRs := oCn:RowSet("clientes") APPEND BLANKoRs:AppendBlank() DELETEoRs:Delete() REPLACE field WITH valoRs:field := val; oRs:Save() GO TOP / GO BOTTOMoRs:GoTop() / oRs:GoBottom() SKIP noRs:Skip(n) RECNO()oRs:RecNo() RECCOUNT() / LASTREC()oRs:KeyCount() / oRs:LastRec() BOF() / EOF()oRs:Bof() / oRs:Eof() FIELD-\>nameoRs:name SET INDEX TO / INDEX ONoRs:SetOrder('campo') SEEK valoRs:Seek(val) SET FILTER TO exproRs:SetFilter('campo \= ?', {val}) LOCATE FOR condoRs:Locate({oRs:campo \== val}) SET RELATION TOoRs:AddChild(...) TDatabase()FWMariaRecord() \+ oCn:RowSet() CLOSE DATAoRs:Close(); oCn:Close() PACK / ZAPoRs:Zap() / oRs:Truncate
21.2 Migracion gradual con replicacion

// El DBF sigue siendo la fuente principal

oDbf := TDatabase():Open( nil, "clientes", "DBFCDX", .t. )

// Cada cambio en el DBF se replica al servidor MariaDB

oDbf:SetReplicationServer( oCn )

// Cuando estes listo, cambia directamente al RowSet:

// oRs := oCn:RowSet( "clientes" )

21.3 Errores comunes al migrar ErrorCausaSolucion Caracteres extranosCharset incorrectoFW\_SetUnicode(.t.) antes de conectar Datos truncadosCampo muy cortoVerificar longitudes al crear tabla No se puede editarSin clave primariaFWH agrega id AUTO\_INCREMENT con CreateTable Lentitud al abrirTabla muy grandeUsar RecSet con paginacion o limitar con WHERE Registro bloqueadoOtro usuario editandoImplementar locking con FOR UPDATE y timeout Error en Save()nError \!= 0Activar oCn:lShowErrors := .t. para ver detalles DBF con DELETEDRegistros marcadosEn MariaDB se borran de verdad; no hay SET DELETED
22\. Referencia Rapida y Constantes Conexion ComandoDescripcion maria\_Connect(cStr, lErr)Conectar al servidor FWCONNECT oCn HOST ... USER ... PASSWORD ... DATABASE ...Conectar via comando FW\_DemoDB(\[tipo\])Conectar al servidor de demo maria\_ConnectError(){ nErr, cErr } del ultimo intento oCn:Close() / oCn:End()Cerrar conexion
Consultas y Tablas ComandoDescripcion oCn:RowSet(cSql \[,aParams\])Recordset editable en memoria oCn:RecSet(cTabla, nRecs)Recordset paginado (tablas grandes) oCn:QueryResult(cSql)Valor unico (COUNT, SUM, etc.) oCn:Execute(cSql)Ejecutar SQL sin resultado oCn:TableExists(cTabla)Verificar si existe oCn:CreateTable(cTabla, aStruct \[,lPK\] \[,cCharset\])Crear tabla oCn:DropTable(cTabla)Eliminar tabla oCn:Insert(cTabla, cCampos, aData)Insertar registros oCn:ImportFromDbf(cArchivo,...)Importar desde DBF
RowSet MetodoDescripcion oRs:campoLeer/escribir campo directamente oRs:Fields('campo'):lReadOnlyPoner campo de solo lectura oRs:lAutoAppend := .t.Habilitar insercion desde browse oRs:SetDefault('campo', val)Valor por defecto en nuevos registros oRs:SetFilter(cExpr, aParams)Filtrar registros oRs:ReFilter(aParams)Re-aplicar filtro con nuevos parametros oRs:ReQuery(\[aParams\])Recargar datos del servidor oRs:ReSync()Re-leer registro actual oRs:Delete()Eliminar registro actual oRs:EditBaseRecord(...)Editar registro completo con dialogo oRs:AddChild(cSQL)Agregar RowSet hijo oRs:SyncChild()Sincronizar hijo con posicion actual oRs:SetOrder('campo')Ordenar oRs:Seek(val)Buscar oRs:Save() / Cancel()Guardar / cancelar cambios oRs:ToExcel() / ToJson()Exportar oRs:Close()Cerrar RowSet
Constantes \- Flags de Campo

\#define NOT\_NULL\_FLAG 1

\#define PRI\_KEY\_FLAG 2

\#define UNIQUE\_KEY\_FLAG 4

\#define BLOB\_FLAG 16

\#define UNSIGNED\_FLAG 32

\#define AUTO\_INCREMENT\_FLAG 512

\#define NO\_DEFAULT\_VALUE\_FLAG 4096

Constantes \- Indices de Estructura

\#define STRUCT\_FIELDNAME 1 // Nombre del campo

\#define STRUCT\_FIELDTYPE 2 // Tipo FWH (C, N, D, L, M, \+, \=, @)

\#define STRUCT\_FIELDLEN 3 // Longitud

\#define STRUCT\_FIELDDEC 4 // Decimales

\#define STRUCT\_MYSQLTYPE 5 // Tipo MySQL nativo

\#define STRUCT\_FLAGS 6 // Bit flags

\#define STRUCT\_ALIAS 7 // Alias

\#define STRUCT\_TABLE 9 // Tabla

\#define STRUCT\_DB 10 // Base de datos

\#define STRUCT\_CHARSETNR 11 // Numero de charset

\#define STRUCT\_RO 12 // Solo lectura

23\. Clase TField: Metadatos de Campos

Cada campo de un RowSet se representa como un objeto TField con metadatos completos del servidor.

Propiedades PropiedadTipoDescripcion nameCNombre del campo (alias en SQL) org\_nameCNombre original en la tabla typeCTipo FWH: C, N, D, L, T, M, \+, \=, @ len / decNLongitud / decimales sqltypeNTipo MySQL nativo flagsNFlags del campo (bit flags) table / org\_tableCAlias de tabla / nombre real lReadOnlyLSolo lectura lAutoIncLAuto-incremento lPrimaryLClave primaria lUniqueLUnico lNoNullLNo permite NULL Value\*Valor actual (lectura/escritura) OriginalValue\*Valor original sin modificar Default\*Valor por defecto cCaseCFormato: 'upper', 'proper'
Acceso

oField := oRs:Fields( "salary" ) // por nombre

oField := oRs:Fields( 3 ) // por posicion

? oField:Value // valor actual

oField:Value := 50000 // asignar

? oField:OriginalValue // valor antes de editar

oField:lReadOnly := .t. // marcar solo lectura

Tipos FWH \-\> MySQL Tipo FWHSignificadoMySQL CCaracterVARCHAR, CHAR NNumericoINT, DECIMAL, FLOAT, DOUBLE DFechaDATE TFecha+HoraDATETIME LLogicoTINYINT(1), BIT MMemo (texto)TEXT mMemo (binario)BLOB \+Auto-incrementoINT AUTO\_INCREMENT \=Timestamp autoTIMESTAMP @DateTime autoDATETIME (con default)

Documentacion adicional y foro de soporte: https://forums.fivetechsupport.com

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: MariaDB manual

Post by Otto »

Hello friends,

I am currently working with a legacy DBF application (FiveWin / Harbour) and experimenting with a modern architecture that exposes the data through a microservice and JSON API.

The current structure looks like this:

Browser / Web UI
→ PHP proxy
→ Harbour microservice
→ DBF database

This works well and allows us to build hybrid interfaces (desktop + web).

While reading the MariaDB manual and examples (RowSet, RecSet, etc.), I was wondering what would be the recommended architecture today if someone migrates from DBF to MariaDB but also wants to support web or hybrid interfaces.

Traditionally the examples show:

FiveWin application
→ maria_Connect()
→ MariaDB

But for web or hybrid systems it seems more common to use:

UI (FiveWin / Web / Mobile)
→ API / service layer
→ MariaDB

In other words, the application does not connect directly to the database but through a service.

Questions:

  1. When migrating from DBF to MariaDB today, would you still recommend the direct RowSet approach in the client application?
  2. Or would you recommend a service/API layer between the UI and MariaDB for hybrid applications?
  3. Are there examples of this architecture using FiveWin / Harbour?

Thank you for your guidance.

Best regards
Otto

User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

Dear Otto,

good morning,

See DeepWiki analysis:

Based on my analysis of the FiveTechSoft/fwh repository, here's what the codebase actually provides and how it relates to your architectural questions.

1. Direct RowSet Approach: Still Valid for Desktop

The direct FWMariaConnection + FWMariaRowSet approach is the most mature and feature-rich path in FiveWin for desktop applications. The FWMariaRowSet class provides full CRUD, parent-child relationships, batch mode, paging, filtering, sorting, replication, and deep integration with TXBrowse. 0-cite-0

A typical direct connection looks like this (from maria02.prg):

Code: Select all | Expand

oCn := maria_Connect( "host:3306,db,user,pass", .t. )
oRs := oCn:RowSet( cSql )
XBROWSER oRs FASTEDIT AUTOSORT
``` [0-cite-1](#0-cite-1) 

For **desktop-only** applications, this remains the recommended approach. You get buffered editing, transactions, savepoints, batch mode, stored procedure calls, and more — all tightly integrated. [0-cite-2](#0-cite-2) 

## 2. For Hybrid/Web: A Service Layer Is the Right Direction

Your current architecture (Browser → PHP proxy → Harbour microservice → DBF) is sound and aligns with modern practice. When migrating to MariaDB, the architecture naturally becomes:

```mermaid
graph TD
    A["FiveWin Desktop"] -->|"direct maria_Connect()"| D["MariaDB"]
    B["Web / Mobile UI"] --> C["API Service Layer"]
    C --> D

FiveWin does not include a production-ready REST API framework out of the box, but it provides several building blocks:

Built-in JSON serialization for MariaDB RowSets

FWMariaRowSet has native JSON export methods:

  • oRs:RecToJson( [aFields] ) — current record as JSON
  • oRs:ToJson( [anRows], [nStart], [aFields] ) — multiple records as JSON 0-cite-3

And for DBFs:

  • FW_RecToJson(), FW_DbfToJson(), ArrToJson() 0-cite-4
Web server infrastructure
  • TWebServer — basic HTTP server using TSocket 0-cite-5

  • HbWebServer — multithreaded HTTP server using Harbour sockets, used in the FiveDbu "WebApp" feature 0-cite-6

  • TWebSocketServer — HTML5 WebSocket server for real-time communication 0-cite-7

  • FiveDbu WebApp — an experimental feature that starts an HbWebServer in a background thread and serves the desktop UI as a web app 0-cite-8

MCP (Model Context Protocol) server

There's a recent example hbmcp.prg implementing a JSON-RPC service in Harbour that reads from stdin/stdout — this is the closest to a "Harbour microservice" pattern in the repo: 0-cite-9

3. DBF → MariaDB Migration Tools

FiveWin has specific support for incremental migration:

  • TDatabase:SetReplicationServer( oCn ) — automatically replicates all DBF changes (add/edit/delete) to a MariaDB table with the same structure 0-cite-10

  • oCn:ImportFromDBF() — one-time table migration 0-cite-11

  • oRs:oReplServer — MariaDB-to-MariaDB table replication for RowSets 0-cite-12

Sample dbf2sql.prg demonstrates this dual-view approach (DBF on left, MariaDB on right, changes replicated automatically): 0-cite-13

4. Recommended Architecture ScenarioApproach Desktop onlyDirect maria_Connect() + RowSet — full power, simplest path Hybrid (desktop + web)Desktop uses direct maria_Connect(); web clients go through a service layer (your PHP proxy pattern, or a Harbour HTTP service using HbWebServer) Web onlyService/API layer is mandatory — MariaDB should never be exposed directly to browsers

For your specific case, a pragmatic hybrid architecture is:

Code: Select all | Expand

graph LR
    A["FiveWin App"] -->|"maria_Connect() direct"| DB["MariaDB"]
    B["Web / Mobile"] --> C["PHP or Harbour API"]
    C -->|"maria_Connect() or native MySQL"| DB

The FiveWin desktop client can continue using RowSet directly for maximum functionality. The web tier accesses the same MariaDB through an API layer. The JSON methods (RecToJson, ToJson, ArrToJson) make it easy for the Harbour service to produce API responses.

5. Relevant Samples in the Repo SampleWhat it demonstrates samples/data/dbf2sql.prgDBF→MariaDB replication samples/misc/mariarpl.prgMariaDB→MariaDB replication samples/misc/mariasyn.prgDBF changes synced to MariaDB samples/misc/maria02.prgDirect RowSet with JOINs samples/misc/maria04.prgParent-child RowSets samples/misc/hbmcp.prgHarbour JSON-RPC microservice samples/misc/fivedbu.prgWebApp hybrid (desktop+web) samples/misc/wsserver.prgWebSocket server

There is no single "canonical" example of the full REST API + MariaDB + FiveWin stack in the repo today. Your approach of a Harbour microservice behind a proxy is the closest real-world pattern and is architecturally sound. The building blocks (JSON serialization, HTTP/WebSocket servers, MariaDB connection classes) are all available — they just need to be composed into a service layer for your specific needs.

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

Antigravity analysis:

DBF → MariaDB: Migration Architectures for Hybrid FiveWin Applications

📄 Relevant source files

source/internal/fwmaria.prg — MariaDB engine (13,411 lines)
source/classes/mariarec.prg — FWMariaRecord class
manual_mariadb/21_portar_dbf_a_mariadb.md — Porting guide
manual_mariadb/04_rowset.md — RowSet API
manual_mariadb/18_sincronizacion_dbf.md — DBF↔MariaDB sync
samples/web/webserv.prg — TWebServer class

This analysis examines the different architectures available for migrating a FiveWin/Harbour application from DBF to MariaDB, with special focus on hybrid scenarios (desktop + web + mobile). It covers existing FWH capabilities, built-in JSON support, and proposes a microservice architecture that enables web interfaces without leaving the Harbour platform.

--- The Three Architectures

Code: Select all | Expand

Architecture 1: DIRECT
  FiveWin App → RowSet/RecSet → MariaDB

Architecture 2: PHP PROXY (your current setup)
  Browser/Web UI → PHP Proxy → Harbour Service → DBF/MariaDB

Architecture 3: HARBOUR API (recommended for hybrid)
  FiveWin / Web / Mobile → Harbour API Server → RowSet + ToJson() → MariaDB
graph TD subgraph A["Architecture 1: Direct"] A1["FiveWin App"] --> A2["RowSet / RecSet"] A2 --> A3[("MariaDB")] end subgraph B["Architecture 2: PHP Proxy"] B1["Browser / Web UI"] --> B2["PHP Proxy"] B2 --> B3["Harbour Service"] B3 --> B4[("DBF / MariaDB")] end subgraph C["Architecture 3: Harbour API"] C1["FiveWin / Web / Mobile"] --> C2["Harbour API Server"] C2 --> C3["RowSet + ToJson()"] C3 --> C4[("MariaDB")] end
Quick Comparison FeatureDirect (RowSet)PHP ProxyHarbour API Complexity🟢 Low🟡 Medium🟡 Medium Performance🟢 Optimal🟡 HTTP overhead🟢 Good Web support🔴 No🟢 Yes🟢 Yes Mobile support🔴 No🟢 Yes🟢 Yes Multi-user🟢 MariaDB native🟢 Yes🟢 Yes Code reuse🟢 Maximum🟡 Duplication🟢 Shared logic Security🟡 DB exposed🟢 DB isolated🟢 DB isolated FWH examples🟢 25+ samples🔴 None🟡 TWebServer
--- Architecture 1: Direct RowSet / RecSet

📄 manual_mariadb/04_rowset.md — manual_mariadb/15_recset_rendimiento.md — source/internal/fwmaria.prg#L250-335

The direct architecture is the standard FWH pattern: the desktop application connects directly to MariaDB using maria_Connect() and works with RowSet and RecSet objects. It is the simplest approach and has the most examples in FWH.

RowSet Pattern — Normal Tables

RowSet loads all data into memory, enabling local sorting and filtering without additional server queries. Ideal for tables with up to 100K records.

Code: Select all | Expand

oCn := maria_Connect( "server,mydb,user,password" )

// Open table as RowSet (equivalent to USE table)
oRs := oCn:RowSet( "clientes" )

// SQL with safe parameters
oRs := oCn:RowSet( "SELECT * FROM clientes WHERE estado = ?", { cEstado } )

// Display in XBrowse
XBROWSER oRs FASTEDIT AUTOSORT TITLE "Clients"

// In-place editing
oRs:First  := "John"
oRs:Salary := 50000
oRs:Save()   // Generates UPDATE only for changed columns

// Export to JSON (key for APIs!)
cJson := oRs:ToJson()
RecSet Pattern — Large Tables (Millions of Rows)

For tables with millions of records, RecSet uses automatic LIMIT/OFFSET pagination, loading only a "window" of data at a time:

Code: Select all | Expand

// RecSet with 1000-record page
oRs := oCn:RecSet( "custbig", 1000 )
oRs:nLastRec := oCn:QueryResult( "SELECT COUNT(*) FROM custbig" )

XBROWSER oRs FASTEDIT SHOW RECID TITLE "1 million records"
DBF → RowSet Equivalence Table DBF OperationMariaDB/FWH Equivalent USE "clientes"oRs := oCn:RowSet("clientes") APPEND BLANKoRs:AppendBlank() DELETEoRs:Delete() REPLACE field WITH valoRs:field := val; oRs:Save() GO TOP / GO BOTTOMoRs:GoTop() / oRs:GoBottom() SKIP noRs:Skip(n) RECNO()oRs:RecNo() SET INDEX TO / INDEX ONoRs:SetOrder("field") SEEK valoRs:Seek(val) SET FILTER TO exproRs:SetFilter("field = ?", {val}) SET RELATION TOoRs:AddChild(...) —oRs:ToJson() — Export to JSON

💡 When to use this architecture: Pure desktop applications with a single client type (FiveWin), no web or mobile access needed. This is the fastest path to migrate from DBF.

sequenceDiagram participant App as FiveWin App participant RS as RowSet/RecSet participant DB as MariaDB
App->>RS: oCn:RowSet("clientes") RS->>DB: SELECT * FROM clientes DB-->>RS: Result set (all rows) RS-->>App: RowSet object in memory App->>App: XBROWSER oRs (UI) App->>RS: oRs:First := "John" App->>RS: oRs:Save() RS->>DB: UPDATE clientes SET first='John' WHERE id=? DB-->>RS: OK
--- Architecture 2: Hybrid — API/Service Layer

📄 samples/web/webserv.prg — source/internal/fwmaria.prg#L1492-1494 (ToJson/RecToJson) — source/function/dbffunc1.prg#L107 (FW_DbfToJson)

For applications that need to serve multiple client types (FiveWin desktop + web browser + mobile app), the recommended architecture interposes a service/API layer between the interfaces and the database. This is exactly what your current PHP proxy does, but with the advantage of using Harbour directly as an HTTP server.

Why an API Layer?
  • Security — The database is not directly exposed to clients; only the API has credentials
  • Reuse — Business logic is written once in Harbour and serves all clients
  • Scalability — Multiple API instances can run behind a load balancer
  • Decoupling — The frontend can change (React, Vue, Flutter) without touching the backend
  • Eliminate the PHP proxy — Harbour already has TWebServer, you don't need PHP in between
Your Current Architecture vs Proposed

Code: Select all | Expand

CURRENT:
  Browser → PHP Proxy → Harbour Service → DBF

PROPOSED:
  Browser / FiveWin / Mobile → Harbour API (TWebServer) → RowSet + ToJson() → MariaDB
graph TD subgraph Clients["Clients"] FW["FiveWin Desktop"] WEB["Browser / SPA"] MOB["Mobile App"] end
subgraph API["Harbour API Server"] WS["TWebServer"] BL["Business Logic<br/>(functions .prg)"] DAL["RowSet + ToJson()"] end DB[("MariaDB")] FW -->|"HTTP/JSON"| WS WEB -->|"HTTP/JSON"| WS MOB -->|"HTTP/JSON"| WS WS --> BL BL --> DAL DAL --> DB

💡 Key advantage: FWH already includes TWebServer and oRs:ToJson(). This means you can build your REST API entirely in Harbour, eliminating PHP from the stack.

--- Harbour Microservice + JSON API

📄 samples/web/webserv.prg — source/internal/fwmaria.prg#L1494 (RowSet:ToJson)

This is the recommended pattern for new hybrid applications. It combines the power of Harbour/FWH with a JSON API that can feed any type of client.

Example: REST API in Harbour with TWebServer

Code: Select all | Expand

#include "FiveWin.ch"

static oCn   // shared MariaDB connection

function Main()
   local oServer
   
   FW_SetUnicode( .t. )
   oCn := maria_Connect( "localhost,mydb,root,password" )
   oCn:lShowErrors := .t.
   
   oServer := TWebServer():New()
   
   // Define API routes
   oServer:Route( "GET",  "/api/clients",      {|req| ApiGetClients(req) } )
   oServer:Route( "GET",  "/api/clients/:id",   {|req| ApiGetClient(req) } )
   oServer:Route( "POST", "/api/clients",       {|req| ApiCreateClient(req) } )
   oServer:Route( "PUT",  "/api/clients/:id",   {|req| ApiUpdateClient(req) } )
   oServer:Route( "DELETE","/api/clients/:id",  {|req| ApiDeleteClient(req) } )
   
   oServer:Activate()  // Listen on port 8080
return nil

// GET /api/clients?state=CA&order=salary
static function ApiGetClients( oReq )
   local oRs, cWhere := "", aParams := {}
   
   if ! Empty( oReq:QueryParam("state") )
      cWhere := " WHERE state = ?"
      AAdd( aParams, oReq:QueryParam("state") )
   endif
   
   oRs := oCn:RowSet( "SELECT * FROM clients" + cWhere, aParams )
   
return oRs:ToJson()   // ← FWH already includes this!

// GET /api/clients/42
static function ApiGetClient( oReq )
   local oRs
   oRs := oCn:RowSet( "SELECT * FROM clients WHERE id = ?", ;
                       { Val(oReq:Param("id")) } )
return oRs:RecToJson()

// POST /api/clients (JSON body)
static function ApiCreateClient( oReq )
   local hData := hb_JsonDecode( oReq:Body() )
   oCn:Insert( "clients", ;
      "first,last,city,salary", ;
      { hData["first"], hData["last"], hData["city"], hData["salary"] } )
return '{"status":"created","id":' + cValToChar(oCn:InsertId()) + '}'

// PUT /api/clients/42
static function ApiUpdateClient( oReq )
   local hData := hb_JsonDecode( oReq:Body() )
   local oRec := FWMariaRecord():New( oCn, "clients", ;
                    "id = " + oReq:Param("id") )
   oRec:First  := hData["first"]
   oRec:Last   := hData["last"]
   oRec:Salary := hData["salary"]
   oRec:Save()
return '{"status":"updated"}'

// DELETE /api/clients/42
static function ApiDeleteClient( oReq )
   oCn:Execute( "DELETE FROM clients WHERE id = ?", ;
                { Val(oReq:Param("id")) } )
return '{"status":"deleted"}'
FiveWin as API Client

The desktop FiveWin app can also consume the API instead of connecting directly to MariaDB:

Code: Select all | Expand

// FiveWin consumes the API just like a browser
function GetClients()
   local cJson, hResp
   
   cJson := FW_HttpGet( "http://localhost:8080/api/clients" )
   hResp := hb_JsonDecode( cJson )
   
   // Display in XBrowse from array
   XBROWSER hResp TITLE "Clients from API"
return nil

⚠️ Note: The TWebServer routing example above is conceptual. FWH's TWebServer class is currently basic. For a robust microservice you may need to extend it or use hbhttpd as an alternative Harbour HTTP server.

sequenceDiagram participant Client as Browser/App participant WS as TWebServer participant BL as Business Logic participant RS as RowSet participant DB as MariaDB
Client->>WS: GET /api/clients?state=CA WS->>BL: Route dispatch BL->>RS: oCn:RowSet("SELECT...WHERE state=?", {"CA"}) RS->>DB: SELECT * FROM clients WHERE state='CA' DB-->>RS: Result set RS-->>BL: RowSet object BL->>BL: oRs:ToJson() BL-->>WS: JSON string WS-->>Client: HTTP 200 + JSON
--- Gradual Migration Strategy

📄 manual_mariadb/18_sincronizacion_dbf.md (mariasyn.prg) — manual_mariadb/21_portar_dbf_a_mariadb.md

FWH includes tools for gradual migration — no "big bang" needed. The recommended strategy has 4 phases:

graph LR F1["Phase 1: DBF + Replication"] --> F2["Phase 2: MariaDB Direct"] F2 --> F3["Phase 3: API Layer"] F3 --> F4["Phase 4: Multi-Client"]
style F1 fill:#1a3a1a,stroke:#3fb950 style F2 fill:#1a2a3a,stroke:#58a6ff style F3 fill:#2a1a3a,stroke:#bc8cff style F4 fill:#3a2a1a,stroke:#d29922
Phase 1: DBF with Real-Time Replication to MariaDB

Keep the DBF running but replicate every change to MariaDB in real time:

Code: Select all | Expand

// DBF remains the primary data source
oDbf := TDatabase():Open( nil, "clients", "DBFCDX", .t. )
oDbf:SetReplicationServer( oCn )  // ← every change is replicated

// The XBrowse stays the same for the user
@ 20,20 XBROWSE oBrw DATASOURCE oDbf AUTOCOLS

This lets you build and test web interfaces using MariaDB while the desktop app continues using DBF. See mariasyn.prg for a complete example.

Phase 2: Switch to MariaDB Direct

When confident, change the data source to RowSet:

Code: Select all | Expand

// BEFORE:  oDbf := TDatabase():Open( nil, "clients", "DBFCDX" )
// AFTER:
oRs := oCn:RowSet( "clients" )

// The XBrowse adapts automatically
@ 20,20 XBROWSE oBrw DATASOURCE oRs AUTOCOLS FASTEDIT AUTOSORT
Phase 3: Add the API Layer

Once on MariaDB, add the API layer using TWebServer + oRs:ToJson() as shown in the Harbour Microservice section above.

Phase 4: Connect Web/Mobile Clients

Build web and mobile frontends that consume your Harbour API. This eliminates the PHP proxy entirely.

graph TD subgraph F1["Phase 1"] A1["FiveWin"] --> A2["DBF"] A2 -.->|"Replication"| A3[("MariaDB")] end subgraph F2["Phase 2"] B1["FiveWin"] --> B2["RowSet"] B2 --> B3[("MariaDB")] end subgraph F34["Phases 3-4"] C1["FiveWin"] --> C4["Harbour API"] C2["Browser"] --> C4 C3["Mobile"] --> C4 C4 --> C5["RowSet/ToJson"] C5 --> C6[("MariaDB")] end
--- Final Recommendation Q1: Should you still use the direct RowSet approach?

Yes, for the initial migration. RowSet is the fastest path and has 25+ examples in FWH. The DBF→RowSet equivalence is nearly 1:1. Start the migration with direct RowSet.

Q2: Should you use a service/API layer for hybrid applications?

Yes, absolutely. If your end goal includes web or mobile interfaces, the API layer is essential. But you don't need to implement it from day 1. The recommended path is:

  1. First — migrate from DBF to MariaDB with direct RowSet (no API)
  2. Then — add the API layer using TWebServer + oRs:ToJson()
  3. Finally — connect web interfaces (you can eliminate the PHP proxy)
Q3: Are there examples of this architecture in FWH?

FWH includes the building blocks but not a complete REST API example:

Building BlockAvailable in FWH?File HTTP Server✅ TWebServersamples/web/webserv.prg RowSet → JSON✅ oRs:ToJson()fwmaria.prg#L1494 Record → JSON✅ oRs:RecToJson()fwmaria.prg#L1492 DBF → JSON✅ FW_DbfToJson()dbffunc1.prg#L107 Hash → JSON✅ ArrToJson()valtostr.prg#L1235 WebView embedded✅ TWebViewsamples/web/webview*.prg Complete REST API❌ Not yetProposed in this analysis DBF→MariaDB Replication✅ SetReplicationServermariasyn.prg
Decision Tree
graph TD Q1{"Desktop only?"} Q1 -->|"Yes"| R1["Direct RowSet<br/>= simplest path"] Q1 -->|"No, also web/mobile"| Q2{"Already have<br/>PHP proxy?"} Q2 -->|"Yes"| R2["Migrate DBF→MariaDB<br/>then replace PHP<br/>with Harbour API"] Q2 -->|"No"| R3["Harbour API<br/>from the start"]
style R1 fill:#1a3a1a,stroke:#3fb950 style R2 fill:#1a2a3a,stroke:#58a6ff style R3 fill:#2a1a3a,stroke:#bc8cff

💡 Summary: Migrate first to MariaDB with direct RowSet — it's nearly identical to working with DBF. When you need web/mobile, add the API layer using the tools FWH already includes (TWebServer + ToJson()).

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

Some mermaids diagrams are failing, I am emailing you the generated files.

We could ask Antigravity to use HIX instead of FWH webserver as it is much more robust and check what it answers :)

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

The updated deepwiki alike SKILL is already available from here:

https://github.com/FiveTechSoft/deepwiki_skill

It already includes a tool to generate HTML output :idea: explained in the README.md

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

Image

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

Image

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: MariaDB manual

Post by Otto »

Dear Antonio,

thank you very much for the detailed analysis.

In principle we are completely on the same line regarding the architecture.
The idea of moving towards a Harbour API layer using RowSet + ToJson() makes a lot of sense for hybrid applications (desktop + web + mobile).

At the moment our structure is:

Browser / Web UI
→ PHP proxy
→ Harbour microservice
→ DBF

and later it could easily become:

Browser / Web UI
→ Harbour service / API
→ MariaDB

So conceptually we are already very close to what you describe.

However, there is one point where I personally struggle a bit: TWebServer().

From my perspective it is currently the weakest part of the stack, mainly because there is no full source code available.

For us this becomes a real issue when selling software to customers.
We are a relatively small company, and especially in manager-driven businesses the question of long-term maintainability always comes up.

Customers often ask questions like:

What happens if the vendor disappears?

Can the system still be maintained?

Is the server layer fully under our control?

If we introduce a critical component (like the application server) without source code, this sometimes becomes difficult to justify during sales discussions.

That is why at the moment we prefer using components where we have complete source availability, even if the architecture becomes slightly more complex.

But conceptually we agree completely with the direction you describe.

Best regards
Otto

User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: MariaDB manual

Post by Otto »

Just as a small additional remark:

Another aspect we are seeing in practice is the long-term availability of components.

For example, some parts of the Harbour WebSocket implementation are currently no longer easily downloadable from the Harbour project pages. This makes it difficult for us to rely on them as a long-term infrastructure component.

When we build systems for customers, we have to be able to guarantee that the complete stack can still be rebuilt and maintained many years later.

User avatar
Antonio Linares
Site Admin
Posts: 44515
Joined: Thu Oct 06, 2005 5:47 pm
Location: Spain
Contact:

Re: MariaDB manual

Post by Antonio Linares »

Dear Otto,

Yes, I fully agree with you.

I have insisted Charly, many times, that HIX should be open source in order to succeed. Also it should be available for Linux and MacOS. I am positive about that he will do it. A closed server supported from just one person is very risky for a company.

Anyhow, I would advise you to ask Antigravity for its analysis using HIX + MariaDB, it may be very interesting.

These days AI works already so well that every desition must be consulted with it :wink:

regards, saludos

Antonio Linares
www.fivetechsoft.com
User avatar
Otto
Posts: 7020
Joined: Fri Oct 07, 2005 7:07 pm
Contact:

Re: MariaDB manual

Post by Otto »

Dear Antonio,
thank you for your reply.

Yes, that is exactly the direction I am currently following.
In fact the microservice I am using is based on your existing socketserver example. The parts that were no longer available in Harbour I replaced with small C blocks using pragma C (with some help from AI).

The server is working very well now. I already considered publishing it, but at the moment there are still many hard-coded paths and the style is very specific to my own "Harbourino" style.

For example I almost avoid functions completely and try to work with as few parameter transfers as possible. For me it makes debugging much easier, but I understand that programmers with a different coding style might not like it and quickly call it “spaghetti code”.

Regarding the coding style: I know that my approach is somewhat unusual. I generally prefer not to catch errors unless it is absolutely necessary. I like the program to stop exactly where the error occurs so that the full state is visible. Otherwise, as a programmer, you often never see the real problems.

In my experience, trying to write very generic and defensive code often makes the code exponentially more complex. For my own projects I try to keep things simple and transparent, even if that means the style looks unconventional to other developers.

However I regularly run the code through several AI reviews and so far the feedback has been very positive regarding structure and stability.

Another idea: extending your WebSocket server and making it fully open source could be a very strong strategy for FiveTech. If the infrastructure layer were open and easy to build, every developer could adapt it to their own needs.

Ideally it should be possible to say:
install Harbour, run the batch file to compile and link, and the server is ready.

That is actually the approach I followed. Everything is contained in a single source file with a simple build script. This makes it very easy to rebuild the server at any time and avoids external dependencies.

At the moment the complete flow works reliably:

authentication → web UI → web proxy → microservice → request → response → web UI.

Another advantage of this setup is that we added logging in every layer.
During development this helps a lot, and in production the logs can simply be disabled


Best regards
Otto

chiaiese
Posts: 117
Joined: Wed Feb 08, 2006 10:32 pm
Location: Roma, Italia
Contact:

Re: MariaDB manual

Post by chiaiese »

Antonio,
It seems like a very complete work, sooner or later it will be useful to me
Many thanks

Roberto

Roberto Chiaiese
R&C Informatica S.n.c.
https://www.recinformatica.it
[email protected]

Harbour 3.2 - FW2512

Post Reply