add Xbase to Rails

It is very easy to add a Xbase application with desktop look and feel to an existing Rails web application. All common databases used by Rails's ActiveRecord can be accessed with Xbase++2.0 data engines out of the box. The new DataObject even allows a surprisingly simple implementation of ActiveRecord way of doing it. SQL-free full ORM with belongs_to association in 200 lines of code demonstrate the power of Xbase:

// record dataobject - rest read with sql crud
// needs s-pluralising and id primary key
// fields are vars in DataObject
// supports belongs_to association
// usage:
//  sXml := LoadFromUrl( "http://reposit/adsx/emps/1" )
//  empsalary := Record():new( sXml )        // create from xml
//  empsalary := Record():new( "empsalary" ) // create empty object
//  empsalary:find( 1 )                      // query by id
//  empsalary:find( "empno=14" )             // query by where
//  more := empsalary:salary + 100.00        // access field
//  empsalary:salary := more                 // update record
//  empsalary:update()                       // commit changes
//  empsalary:delete()                       // delete record
//  full := empsalary:employee:firstname + " " + ;
//          empsalary:employee:lastname      // even in belongs_to
#include "class.ch"

CLASS Record FROM DataObject
    VAR __model, __cid, __oAct, __has
    METHOD init, has_one, getNoIVar, set, fields, fieldn, get, geto, execute, readr
  EXPORTED:
    METHOD find, create, update, destroy
ENDCLASS

METHOD Record:init( sModel )
// build dataobject vars from xml record type or empty for model
  LOCAL oXmlHandle, oXmlRoot, aTags, aTags2, aTags3, oAttributes
  LOCAL i
  ::__oAct := DataObject():New()
  ::__has := {}
  oXmlHandle := XMLDocOpenString( sModel )
  oXmlRoot := XMLDocGetRootTag( oXmlHandle )
  XMLGetTag( oXmlRoot, @aTags )
  XMLGetTag( aTags[3][1], @aTags2 )
  IF aTags2[3] != NIL
  // found valid xml-record - handle as query
    ::__model := aTags2[1]
    FOR i := 1 TO len(aTags2[3])
      XMLGetTag( aTags2[3][i], @aTags3 )
      IF SubStr( aTags3[1], 1, 2 ) != "__"
        ::set( aTags3[1], aTags3[2] )
        IF aTags3[1] == "id"
          ::__cid := aTags3[2]
        ENDIF
      ENDIF
    NEXT i
  ELSE
    ::__model := sModel
  ENDIF
RETURN self

METHOD Record:has_one( sParent )
  AAdd( ::__has, sParent )
RETURN

METHOD Record:getNoIVar( sAssoc )
// query parent on first read access
  LOCAL oParent
  DO CASE
   CASE IsMemberVar( self, sAssoc + "_id" )
    // belongs_to if model_id field exists
    oParent := Record():new( sAssoc )
    ::setNoIVar( sAssoc, oParent )
    oParent:__cid := ::get( sAssoc + "_id" )
    oParent:readr()
   CASE AScan( ::__has, sAssoc )
    // has_one if __has entry
    oParent := Record():new( sAssoc )
    ::setNoIVar( sAssoc, oParent )
    oParent:readr( sAssoc + "_id = " + LTrim( STR(::__cid) ) )
   OTHERWISE
    oParent := NIL
  ENDCASE
RETURN oParent

METHOD Record:execute( sSQL )
// sql statement - use area for select
  LOCAL oSqlStmt
  oSqlStmt := DacSqlStatement(DbePgSess()):FromChar( sSQL )
  IF SubStr( sSQL, 1, 6 ) == 'SELECT'
    oSqlStmt:Build():Query(, "RFIND")
  ELSE
//   ::_Session:beginTransaction()
    oSQLStmt:build():execute()
//   ::_Session:commitTransaction()
  ENDIF
RETURN

METHOD Record:find( iId )
  IF Valtype( iId ) == "C"
    ::readr( iId )
  ELSE
    ::__cid := iId
    ::readr()
  ENDIF
RETURN self

METHOD Record:set( sKey, sVal )
  ::setNoIVar( sKey, sVal )
  ::__oAct:setNoIVar( sKey, sVal )
RETURN sVal

METHOD Record:fields()
  LOCAL aMembers
  aMembers := ::__oAct:classDescribe( CLASS_DESCR_MEMBERS )
RETURN aMembers

METHOD Record:fieldn()
  LOCAL aAll, aMembers, i
  aMembers := {}
  aAll := ::classDescribe( CLASS_DESCR_MEMBERS )
  FOR i := 1 TO len(aAll)
    IF SubStr( aAll[i][1], 1, 2 ) != "__"
      AAdd( aMembers, aAll[i] )
    ENDIF
  NEXT i
RETURN aMembers

METHOD Record:get( sField )
  LOCAL rVal
  rVal := ::&(sField)
RETURN rVal

METHOD Record:geto( sField )
  LOCAL rVal
  rVal := ::__oAct:&(sField)
RETURN rVal

// SQL create/insert read/select update destroy/delete

METHOD Record:readr( sQuery )
// select and build field vars
// prepare parent query with id
  LOCAL i, sWhere, sSelectSql, sField, sBelongsto, oParent
  IF PCount() > 0
    sWhere := sQuery
  ELSE
    sWhere := "id = " + LTrim( STR(::__cid) )
  ENDIF
  sSelectSql := "SELECT * FROM " + ::__model + "s WHERE " + sWhere
  ::execute( sSelectSql )
  FOR i := 1 TO RFIND->(FCount())
    sField := RFIND->(FieldName( i ))
    IF SubStr( sField, 1, 2 ) != "__"
      ::set( sField, RFIND->(FieldGet( i )) )
    ENDIF
    IF sField == "id"
      ::__cid := RFIND->(FieldGet( i ))
    ENDIF
  NEXT i
  CLOSE RFIND
RETURN sSelectSql

METHOD Record:update()
// update changed fields (oAct is old value)
  LOCAL aFields, sField, sUpdateSql
  LOCAL i, needsep := .F.
  aFields := ::fields()
  sUpdateSql := "UPDATE " + ::__model + "s SET "
  FOR i := 1 TO len(aFields)
    sField := aFields[i][1]
    IF ::get( sField ) != ::geto( sField ) .AND. sField != "id"
      IF needsep
        sUpdateSql := sUpdateSql + ", "
      ENDIF
      sUpdateSql := sUpdateSql + sField + " = " + SqlValue( ::get( sField ) )
      ::__oAct:&(sField) := ::get( sField )
      needsep := .T.
    ENDIF
  NEXT i
  sUpdateSql := sUpdateSql + " WHERE id = " + SqlValue( ::__cid )
  ::execute( sUpdateSql )
RETURN sUpdateSql

METHOD Record:create()
// insert record with all attrs (fields)
  LOCAL aFields, sField, sInsertSql
  LOCAL i, needsep := .F.
  aFields := ::fieldn()
  sInsertSql := "INSERT INTO " + ::__model + "s ("
  FOR i := 1 TO len(aFields)
    sField := aFields[i][1]
      IF needsep
        sInsertSql := sInsertSql + ", "
      ENDIF
      sInsertSql := sInsertSql + sField
      needsep := .T.
  NEXT i
  sInsertSql := sInsertSql + ") VALUES ("
  needsep := .F.
  FOR i := 1 TO len(aFields)
    sField := aFields[i][1]
      IF needsep
        sInsertSql := sInsertSql + ", "
      ENDIF
      sInsertSql := sInsertSql + SqlValue( ::get( sField ) )
      ::__oAct:&(sField) := ::get( sField )
      needsep := .T.
  NEXT i
  sInsertSql := sInsertSql + ")"
  ::execute( sInsertSql )
RETURN sInsertSql

METHOD Record:destroy()
// delete with id
  LOCAL sDeleteSql
  LOCAL i
  sDeleteSql := "DELETE FROM " + ::__model + "s WHERE id = " + SqlValue( ::id )
  ::execute( sDeleteSql )
RETURN sDeleteSql

If your Rails application is already connected to PostgreSQL you might want to use the DBF-emulation-mode of Xbase++2.0. Since your tables cannot be "upsized" you have to add the emulation layer to them. Use this script as an example to enable Xbase emulation layer to access the PostgreSQL tables ("upsizeify.sql"):

DELETE FROM "alaska-software.isam.tables" WHERE "table_name"='employees';
INSERT INTO "alaska-software.isam.tables"(table_name, unc_name, updated, created, record_count, update_count, lock_owner)
 VALUES ('employees', 'd:\\data\\employees.dbf', '2015-10-05 07:38:36', '2015-10-05 07:38:36', 0, 0, 0);
ALTER TABLE employees ADD COLUMN
 __deleted boolean NOT NULL DEFAULT false,
 __record serial PRIMARY KEY,
 __rowversion integer NOT NULL DEFAULT 0,
 __keyversion integer NOT NULL DEFAULT 0,
 __lock_owner integer NOT NULL DEFAULT 0;
CREATE INDEX "employees_record" ON employees USING btree (__record);
CREATE TRIGGER employees_isam_tablemeta AFTER INSERT OR DELETE OR UPDATE
 ON employees FOR EACH ROW EXECUTE PROCEDURE isam_tablemeta_update();
CREATE TRIGGER employees_isam_rowversion AFTER UPDATE
 ON employees FOR EACH ROW EXECUTE PROCEDURE isam_rowversion_update();
update "alaska-software.isam.tables" set record_count = (select count(*) from "employees") where table_name = 'employees';