/* This samples show how to use dbf to cache postgres records. */

#require "hbpgsql"

#define DB_ALIAS            1  // Table Name
#define DB_QUERY            2  // Object Query
#define DB_ROW              3  // Current Row
#define DB_FETCH            4  // Fetch Status

THREAD STATIC t_oServer
THREAD STATIC t_aTableTemp := {}

PROCEDURE Main( cHost, cDatabase, cUser, cPass )

   LOCAL i

   IF SQLConnect( cHost, hb_defaultValue( cDatabase, "postgres" ), cUser, cPass )

      QuickQuery( "DROP TABLE test" )

      SQLQuery( "CREATE TABLE test (" + ;
         "  codigo integer primary key," + ;
         "  descri char(50)," + ;
         "  email varchar(50) )" )

      SQLOpen( "nomes", "SELECT * FROM test" )

      FOR i := 1 TO 50
         dbAppend()
         nomes->codigo := i
         nomes->descri := "test " + hb_ntos( i )
      NEXT

      SQLApplyUpdates()

      SQLOpen( "nomes", SQLPrepare( "SELECT * FROM test WHERE codigo >= :1 ORDER BY codigo", 1 ) )

      DO WHILE ! Eof()
         ? RecNo(), nomes->Codigo, nomes->descri, nomes->email

         IF RecNo() == 10
            dbDelete()
         ENDIF

         IF RecNo() == 20
            nomes->email := "teste"
         ENDIF

         SQLFetch()
      ENDDO

      SQLApplyUpdates()
   ENDIF

   SQLGarbageCollector()

   RETURN

/* Put theses functions in a library */

FUNCTION SQLApplyUpdates()

   LOCAL cAlias := Upper( Alias() )
   LOCAL i, x
   LOCAL oQuery
   LOCAL oRow
   LOCAL lUpdate
   LOCAL lError := .F.
   LOCAL cError

   IF ( i := AScan( t_aTableTemp, {| aVal | aVal[ DB_ALIAS ] == cAlias } ) ) > 0

      oQuery := t_aTableTemp[ i ][ DB_QUERY ]

      FOR i := 1 TO LastRec()

         dbGoto( i )

         IF i > oQuery:LastRec()

            /* Check if it's a new record */
            IF ! Deleted()

               oRow := oQuery:GetBlankRow()

               FOR x := 1 TO FCount()
                  IF oRow:FieldPos( FieldName( x ) ) != 0
                     oRow:FieldPut( FieldName( x ), FieldGet( x ) )
                  ENDIF
               NEXT

               oQuery:Append( oRow )
               cError := oQuery:ErrorMsg()
               lError := oQuery:NetErr()
            ENDIF
         ELSE
            oRow := oQuery:GetRow( i )

            IF Deleted()
               oQuery:Delete( oRow )
               cError := oQuery:ErrorMsg()
               lError := oQuery:NetErr()
            ELSE
               /* Update if any of the fields have changed */

               lUpdate := .F.
               FOR x := 1 TO FCount()
                  IF oRow:FieldPos( FieldName( x ) ) != 0 .AND. ;
                     ! FieldGet( x ) == oRow:FieldGet( FieldName( x ) )
                     oRow:FieldPut( FieldName( x ), FieldGet( x ) )
                     lUpdate := .T.
                  ENDIF
               NEXT

               IF lUpdate
                  oQuery:Update( oRow )
                  cError := oQuery:ErrorMsg()
                  lError := oQuery:NetErr()
               ENDIF
            ENDIF
         ENDIF

         IF lError
            EXIT
         ENDIF
      NEXT
   ENDIF

   IF lError
      ? cError
   ENDIF

   RETURN ! lError

PROCEDURE SQLCloseTemp( cAlias )

   LOCAL x

   IF Select( cAlias ) != 0
      ( cAlias )->( dbCloseArea() )
   ENDIF

   IF ( x := AScan( t_aTableTemp, {| aVal | aVal[ DB_ALIAS ] == cAlias } ) ) > 0
      hb_ADel( t_aTableTemp, x, .F. /* .T. */ )
   ENDIF

   RETURN

PROCEDURE SQLGarbageCollector()

   LOCAL item
   LOCAL oQuery

   dbCloseAll()

   FOR EACH item IN t_aTableTemp
      IF ( oQuery := item[ DB_QUERY ] ) != NIL
         oQuery:Destroy()
      ENDIF
   NEXT

   ASize( t_aTableTemp, 0 )

   RETURN

FUNCTION SQLFetch( lFetchAll )

   LOCAL oQuery
   LOCAL oRow
   LOCAL cAlias := Upper( Alias() )
   LOCAL i, x, y
   LOCAL nPos
   LOCAL lEof := .F.

   hb_default( @lFetchAll, .F. )

   /* Search for table in array */
   IF ( i := AScan( t_aTableTemp, {| aVal | aVal[ DB_ALIAS ] == cAlias } ) ) > 0

      /* Get database records */
      oQuery := t_aTableTemp[ i ][ DB_QUERY ]
      nPos   := t_aTableTemp[ i ][ DB_ROW ] + 1

      IF lFetchAll
         t_aTableTemp[ i ][ DB_FETCH ] := .T.
      ENDIF

      IF oQuery:LastRec() >= nPos

         y := nPos

         DO WHILE nPos <= iif( lFetchAll, oQuery:LastRec(), y )
            oRow := oQuery:GetRow( nPos )
            dbAppend()

            FOR x := 1 TO oRow:FCount()
               FieldPut( FieldPos( oRow:FieldName( x ) ), oRow:FieldGet( x ) )
            NEXT

            t_aTableTemp[ i ][ DB_ROW ] := nPos
            nPos++
         ENDDO

      ELSE
         // Posiciona registro no eof
         dbSkip()
      ENDIF

      lEof := nPos > oQuery:LastRec()
   ENDIF

   RETURN lEof

PROCEDURE SQLFetchAll()

   SQLFetch( .T. )
   dbGoTop()

   RETURN

FUNCTION SQLOpen( cAlias, cQuery, xFetch, cOrder )

   LOCAL x
   LOCAL oServer
   LOCAL oQuery
   LOCAL lFetch

   oServer := SQLCurrentServer()
   cAlias := Upper( cAlias )

   /* Search by query in temporary area */
   IF ( x := AScan( t_aTableTemp, {| aVal | aVal[ DB_ALIAS ] == cAlias } ) ) > 0
      oQuery := t_aTableTemp[ x ][ DB_QUERY ]
      oQuery:Destroy()
   ENDIF

   IF cQuery == NIL
      cQuery := "SELECT * FROM " + cAlias
      IF ! Empty( cOrder )
         cQuery += " ORDER BY " + cOrder
      ENDIF
   ENDIF

   oQuery := oServer:Query( cQuery )

   IF oQuery:NetErr()
      ? oQuery:ErrorMsg()
      RETURN .F.
   ENDIF

   IF Select( cAlias ) == 0
      hb_dbCreateTemp( cAlias, oQuery:Struct() )
   ELSE
      dbSelectArea( cAlias )
      hb_dbZap()
   ENDIF

   IF xFetch != NIL
      lFetch := xFetch
   ELSE
      lFetch := .F.
   ENDIF

   /* If there is no query in the temporary area then add, otherwise just refresh. */
   IF x == 0
      AAdd( t_aTableTemp, { ;
         cAlias, ;  // DB_ALIAS
         oQuery, ;  // DB_QUERY
         0, ;       // DB_ROW
         lFetch } ) // DB_FETCH
   ELSE

      t_aTableTemp[ x ][ DB_QUERY ] := oQuery
      t_aTableTemp[ x ][ DB_ROW ]   := 0
      t_aTableTemp[ x ][ DB_FETCH ] := lFetch

   ENDIF

   /* Get database records */
   SQLFetch( lFetch )

   IF lFetch
      dbGoTop()
   ENDIF

   RETURN .T.

FUNCTION SQLConnect( cHost, cDatabase, cUser, cPassword, cSchema )

   LOCAL lRetval := .T.

   t_oServer := TPQServer():New( cHost, cDatabase, cUser, cPassword, 5432, cSchema )
   IF t_oServer:NetErr()
      ? t_oServer:ErrorMsg()
      lRetval := .F.
   ENDIF
   t_oServer:lAllCols := .F.

   RETURN lRetval

PROCEDURE SQLDestroy()

   IF t_oServer != NIL
      t_oServer:Destroy()
   ENDIF

   RETURN

FUNCTION SQLCurrentServer
   RETURN t_oServer

FUNCTION SQLQuery( cQuery )

   LOCAL oQuery := t_oServer:Query( cQuery )

   IF oQuery:NetErr()
      ? cQuery + ":" + oQuery:ErrorMsg()
   ENDIF

   RETURN oQuery

FUNCTION SQLExecQuery( cQuery )

   LOCAL oQuery := t_oServer:Query( cQuery )

   IF oQuery:NetErr()
      ? "Cannot execute", cQuery + ":" + oQuery:ErrorMsg()
      RETURN .F.
   ENDIF

   oQuery:Destroy()

   RETURN .T.

FUNCTION SQLPrepare( cQuery, ... )

   LOCAL i, x

   IF PCount() >= 2
      /* Remove unnecessary whitespace */
      DO WHILE Space( 2 ) $ cQuery
         cQuery := StrTran( cQuery, Space( 2 ), Space( 1 ) )
      ENDDO

      /* Place {} in the parameters */
      FOR i := 1 TO PCount() - 1
         IF ( x := At( ":" + hb_ntos( i ), cQuery ) ) > 0
            cQuery := Stuff( cQuery, x, 0, "{" )
            cQuery := Stuff( cQuery, x + Len( hb_ntos( i ) ) + 2, 0, "}" )
         ENDIF
      NEXT

      /* Replace parameters with values */
      FOR i := 2 TO PCount()
         x := hb_PValue( i )

         DO CASE
         CASE x != NIL .AND. Empty( x )
            x := "null"

         CASE HB_ISNUMERIC( x )
            x := hb_ntos( x )

         CASE HB_ISDATE( x )
            x := "'" + hb_DToC( x, "yyyy-mm-dd" ) + "'"

         CASE HB_ISLOGICAL( x )
            x := iif( x, "'t'", "'f'" )

         CASE HB_ISSTRING( x )
            x := SToQ( RTrim( x ) )

         OTHERWISE
            x := "null"
         ENDCASE

         cQuery := StrTran( cQuery, "{:" + hb_ntos( i - 1 ) + "}", x )
      NEXT
   ENDIF

   RETURN hb_StrReplace( cQuery, { ;
      "=="    => "="   , ;
      "!="    => "<>"  , ;
      ".and." => "and" , ;
      ".or."  => "or"  , ;
      ".not." => "not" } )

/* Get next result of a sequence */
FUNCTION SQLSequence( Sequence_name )
   RETURN Val( QuickQuery( "SELECT nextval(" + SToQ( sequence_name ) + ")" ) )

PROCEDURE SQLStartTrans()

   IF PQtransactionStatus( t_oServer:pDB ) != PQTRANS_INTRANS
      t_oServer:StartTransaction()
   ENDIF

   RETURN

FUNCTION SQLInTrans()
   RETURN PQtransactionStatus( t_oServer:pDB ) == PQTRANS_INTRANS

PROCEDURE SQLCommitTrans()

   t_oServer:Commit()

   RETURN

PROCEDURE SQLRollbackTrans()

   t_oServer:rollback()

   RETURN

/* Do query that returns only 1 column value */
FUNCTION QuickQuery( cQuery )

   LOCAL result := ""
   LOCAL temp, aTemp
   LOCAL x, y

   LOCAL pQuery := PQexec( t_oServer:pDB, cQuery )

   IF PQresultStatus( pQuery ) == PGRES_TUPLES_OK
      IF PQlastrec( pQuery ) != 0
         IF PQfcount( pQuery ) == 1 .AND. PQlastrec( pQuery ) == 1
            temp := PQgetvalue( pQuery, 1, 1 )
            result := iif( temp == NIL, "", temp )
         ELSE
            result := {}
            FOR x := 1 TO PQlastrec( pQuery )
               aTemp := {}
               FOR y := 1 TO PQfcount( pQuery )
                  temp := PQgetvalue( pQuery, x, y )
                  AAdd( aTemp, iif( temp == NIL, "", temp ) )
               NEXT
               AAdd( result, aTemp )
            NEXT
         ENDIF
      ENDIF
   ENDIF

   RETURN result

FUNCTION SToQ( cData )
   RETURN "'" + cData + "'"
