unit dbf_fields;

interface

{$I dbf_common.inc}

uses
  Classes,
  SysUtils,
  db,
  dbf_common,
  dbf_str;

type
  PDbfFieldDef = ^TDbfFieldDef;

  { TDbfFieldDef }

  TDbfFieldDef = class(TCollectionItem)
  private
    FAutoIncStep: Integer;
    FFieldName: string;
    FFieldType: TFieldType;
    FIsSystemField: Boolean;
    FVarLengthPosition: integer;
    FNativeFieldType: TDbfFieldType;
    FDefaultBuf: PChar;
    FMinBuf: PChar;
    FMaxBuf: PChar;
    FSize: Integer;
    FPrecision: Integer;
    FHasDefault: Boolean;
    FHasMin: Boolean;
    FHasMax: Boolean;
    FAllocSize: Integer;
    FCopyFrom: Integer;
    FOffset: Integer;
    FAutoInc: Cardinal;
    FRequired: Boolean;
    FIsLockField: Boolean;
    FNullPosition: integer;

    function  GetDbfVersion: TXBaseVersion;
    procedure SetNativeFieldType(lFieldType: TDbfFieldType);
    procedure SetFieldType(lFieldType: TFieldType);
    procedure SetSize(lSize: Integer);
    procedure SetPrecision(lPrecision: Integer);
    // Converts VCL/LCL field types to dbf native field type markers ('C' etc)
    procedure VCLToNative;
    // Converts dbf native field type markers ('C' etc) to VCL/LCL field types
    procedure NativeToVCL;
    procedure FreeBuffers;
  protected
    function  GetDisplayName: string; override;
    procedure AssignTo(Dest: TPersistent); override;
    // File is compatible with this database product
    property DbfVersion: TXBaseVersion read GetDbfVersion;
  public
    constructor Create(ACollection: TCollection); override;
    destructor Destroy; override;

    procedure Assign(Source: TPersistent); override;
    procedure AssignDb(DbSource: TFieldDef);

    // Checks and adjusts field size & precision
    procedure CheckSizePrecision;
    procedure SetDefaultSize;
    procedure AllocBuffers;
    // Yes if field is a blob/memo type field (storage in external file)
    function  IsBlob: Boolean;

    property DefaultBuf: PChar read FDefaultBuf;
    property MinBuf: PChar read FMinBuf;
    property MaxBuf: PChar read FMaxBuf;
    property HasDefault: Boolean read FHasDefault write FHasDefault;
    property HasMin: Boolean read FHasMin write FHasMin;
    property HasMax: Boolean read FHasMax write FHasMax;
    // Distance of field from beginning of record
    property Offset: Integer read FOffset write FOffset;
    // Value for autoinc
    property AutoInc: Cardinal read FAutoInc write FAutoInc;
    // Step size for autoinc (Visual FoxPro only)
    property AutoIncStep: Integer read FAutoIncStep write FAutoIncStep;
    // Field contains lock data (not a normal field)
    property IsLockField: Boolean read FIsLockField write FIsLockField;
    // Field is a system, hidden field (Visual FoxPro supported only)
    property IsSystemField: Boolean read FIsSystemField write FIsSystemField;
    property CopyFrom: Integer read FCopyFrom write FCopyFrom;
  published
    property FieldName: string     read FFieldName write FFieldName;
    // VCL/LCL field type mapped to this field
    property FieldType: TFieldType read FFieldType write SetFieldType;
    // If using varchar/varbinary/var...:
    // VFP uses a varlength bit in _NullFields in physical order (bit number corresponds to physical order)
    // If flag=1, the actually used length/size is stored in the last data byte of the field
    // If the var* field is nullable, 2 bits are used:
    // lower bit number is varlength, next is null flag.
    // Note: VarLengthPosition property is 0 based
    // http://msdn.microsoft.com/en-us/library/st4a0s68%28v=VS.80%29.aspx
    property VarLengthPosition: integer read FVarLengthPosition write FVarLengthPosition;
    // Native dbf field type (C character etc)
    property NativeFieldType: TDbfFieldType read FNativeFieldType write SetNativeFieldType;
    // Size in physical dbase file.
    // Note: this often differs from the VCL field sizes
    property Size: Integer         read FSize write SetSize;
    // Visual FoxPro: position of field null flag in _NullFields field
    // Reflects the physical field order, except if varchar/varbinary/var* fields
    // are used (see VarLengthPosition property for details)
    // Note: NullPosition property is 0 based
    // http://msdn.microsoft.com/en-us/library/st4a0s68%28v=VS.80%29.aspx
    property NullPosition: integer read FNullPosition write FNullPosition;
    property Precision: Integer    read FPrecision write SetPrecision;
    property Required: Boolean     read FRequired  write FRequired;
  end;

  TDbfFieldDefs = class(TCollection)
  private
    FOwner: TPersistent;
    FDbfVersion: TXBaseVersion;
    function GetItem(Idx: Integer): TDbfFieldDef;
  protected
    function GetOwner: TPersistent; override;
  public
    constructor Create(Owner: TPersistent);

{$ifdef SUPPORT_DEFAULT_PARAMS}
    procedure Add(const Name: string; DataType: TFieldType; Size: Integer = 0; Required: Boolean = False);
{$else}
    procedure Add(const Name: string; DataType: TFieldType; Size: Integer; Required: Boolean);
{$endif}
    function AddFieldDef: TDbfFieldDef;

    property Items[Idx: Integer]: TDbfFieldDef read GetItem;
    property DbfVersion: TXBaseVersion read FDbfVersion write FDbfVersion;
  end;

implementation

uses
  dbf_dbffile;      // for dbf header structures

{$I dbf_struct.inc}

const
(*
The theory for Delphi/FPC is:
    ftSmallint  16 bits = -32768 to 32767
                          123456 = 6 digit max theorically
                          DIGITS_SMALLINT = 6;
    ftInteger  32 bits = -2147483648 to 2147483647
                         12345678901 = 11 digits max
                         DIGITS_INTEGER = 11;
    ftLargeInt 64 bits = -9223372036854775808 to 9223372036854775807
                         12345678901234567890 = 20 digits max
                         DIGITS_LARGEINT = 20;

But in fact if I accept 6 digits into a ftSmallInt then tDbf will not
be able to handles fields with 999999 (6 digits).

So I oversize the field type in order to accept anything coming from the
database.
    ftSmallint  16 bits = -32768 to 32767
    ... dbf supports:       -999 to  9999
                           4 digits max in practice
        therefore         DIGITS_SMALLINT = 4;
    ftWord 16 bits sign = 0 to     65535
    ... dbf supports:     0 to 999999999 (in an N field)
        therefore         DIGITS_WORD = 5;
    ftInteger  32 bits  = -2147483648 to 2147483647
    ... dbf supports:       -99999999 to  999999999                                        12345678901 = 11 digits max
        therefore         DIGITS_INTEGER = 9;
    ftLargeInt 64 bits  = -9223372036854775808 to 9223372036854775807
    ... dbf supports:       -99999999999999999 to  999999999999999999
        therefore         DIGITS_LARGEINT = 18;
 *)
  DIGITS_SMALLINT = 4;
  DIGITS_WORD = 5;
  DIGITS_INTEGER = 9;
  DIGITS_LARGEINT = 18;

//====================================================================
// DbfFieldDefs
//====================================================================
function TDbfFieldDefs.GetItem(Idx: Integer): TDbfFieldDef;
begin
  Result := TDbfFieldDef(inherited GetItem(Idx));
end;

constructor TDbfFieldDefs.Create(Owner: TPersistent);
begin
  inherited Create(TDbfFieldDef);
  FOwner := Owner;
end;

function TDbfFieldDefs.AddFieldDef: TDbfFieldDef;
begin
  Result := TDbfFieldDef(inherited Add);
end;

function TDbfFieldDefs.GetOwner: TPersistent; {override;}
begin
  Result := FOwner;
end;

procedure TDbfFieldDefs.Add(const Name: string; DataType: TFieldType; Size: Integer; Required: Boolean);
var
  FieldDef: TDbfFieldDef;
begin
  FieldDef := AddFieldDef;
  FieldDef.FieldName := Name;
  FieldDef.FieldType := DataType;
  if Size <> 0 then
    FieldDef.Size := Size;
  FieldDef.Required := Required;
end;

//====================================================================
// DbfFieldDef
//====================================================================
constructor TDbfFieldDef.Create(ACollection: TCollection); {virtual}
begin
  inherited;

  FDefaultBuf := nil;
  FMinBuf := nil;
  FMaxBuf := nil;
  FAllocSize := 0;
  FCopyFrom := -1;
  FPrecision := 0;
  FHasDefault := false;
  FHasMin := false;
  FHasMax := false;
  FNullPosition := -1;
  FVarLengthPosition := -1;
end;

destructor TDbfFieldDef.Destroy; {override}
begin
  FreeBuffers;
  inherited;
end;

procedure TDbfFieldDef.Assign(Source: TPersistent);
var
  DbfSource: TDbfFieldDef;
begin
  if Source is TDbfFieldDef then
  begin
    // copy from another TDbfFieldDef
    DbfSource := TDbfFieldDef(Source);
    FFieldName := DbfSource.FieldName;
    FFieldType := DbfSource.FieldType;
    FNativeFieldType := DbfSource.NativeFieldType;
    FSize := DbfSource.Size;
    FPrecision := DbfSource.Precision;
    FRequired := DbfSource.Required;
    FCopyFrom := DbfSource.Index;
    FIsLockField := DbfSource.IsLockField;
    FIsSystemField := DbfSource.IsSystemField;
    FNullPosition := DbfSource.NullPosition;
    FVarLengthPosition:=DbfSource.VarLengthPosition;
    // copy default,min,max
    AllocBuffers;
    if DbfSource.DefaultBuf <> nil then
      Move(DbfSource.DefaultBuf^, FDefaultBuf^, FAllocSize*3);
    FHasDefault := DbfSource.HasDefault;
    FHasMin := DbfSource.HasMin;
    FHasMax := DbfSource.HasMax;
    // do we need offsets?
    FOffset := DbfSource.Offset;
    FAutoInc := DbfSource.AutoInc;
    FAutoIncStep := DbfSource.AutoIncStep;
{$ifdef SUPPORT_FIELDDEF_TPERSISTENT}
  end else if Source is TFieldDef then begin
    AssignDb(TFieldDef(Source));
{$endif}
  end else
    inherited Assign(Source);
end;

procedure TDbfFieldDef.AssignDb(DbSource: TFieldDef);
begin
  // copy from Db.TFieldDef
  FFieldName := DbSource.Name;
  FFieldType := DbSource.DataType;
  // We do NOT copy over size if TFieldDef size is different from our native size
  if not(DBSource.DataType in [ftBCD,ftCurrency]) then
    FSize := DbSource.Size;
  FPrecision := DbSource.Precision;
  FRequired := DbSource.Required;
{$ifdef SUPPORT_FIELDDEF_INDEX}
  FCopyFrom := DbSource.Index;
{$endif}
  FIsLockField := false;
  FIsSystemField := false;
  // convert VCL fieldtypes to native DBF fieldtypes
  VCLToNative;
  // for integer / float fields try to fill in Size/precision
  if FSize = 0 then
    SetDefaultSize
  else
    CheckSizePrecision;
  // VCL does not have default value support
  AllocBuffers;
  FHasDefault := false;
  FHasMin := false;
  FHasMax := false;
  FOffset := 0;
  FAutoInc := 0;
  FAutoIncStep := 0;
end;

procedure TDbfFieldDef.AssignTo(Dest: TPersistent);
var
  DbDest: TFieldDef;
begin
{$ifdef SUPPORT_FIELDDEF_TPERSISTENT}
  // copy to VCL fielddef?
  if Dest is TFieldDef then
  begin
    DbDest := TFieldDef(Dest);
    // VCL TFieldDef does not know how to handle TDbfFieldDef!
    // what a shame :-)
{$ifdef SUPPORT_FIELDDEF_ATTRIBUTES}
    DbDest.Attributes := [];
    DbDest.ChildDefs.Clear;
    DbDest.DataType := FFieldType;
    DbDest.Required := FRequired;
    DbDest.Size := FSize;
    DbDest.Name := FFieldName;
{$endif}
  end else
{$endif}
    inherited AssignTo(Dest);
end;

function TDbfFieldDef.GetDbfVersion: TXBaseVersion;
begin
  Result := TDbfFieldDefs(Collection).DbfVersion;
end;

procedure TDbfFieldDef.SetFieldType(lFieldType: TFieldType);
begin
  FFieldType := lFieldType;
  VCLToNative;
  SetDefaultSize;
end;

procedure TDbfFieldDef.SetNativeFieldType(lFieldType: TDbfFieldType);
begin
  // convert lowercase to uppercase
  if (lFieldType >= 'a') and (lFieldType <= 'z') then
    lFieldType := Chr(Ord(lFieldType)-32);
  FNativeFieldType := lFieldType;
  NativeToVCL;
  CheckSizePrecision;
end;

procedure TDbfFieldDef.SetSize(lSize: Integer);
begin
  FSize := lSize;
  CheckSizePrecision;
end;

procedure TDbfFieldDef.SetPrecision(lPrecision: Integer);
begin
  FPrecision := lPrecision;
  CheckSizePrecision;
end;

procedure TDbfFieldDef.NativeToVCL;
begin
  case FNativeFieldType of
    '+' : //dbase7+ autoinc
      if DbfVersion = xBaseVII then
        FFieldType := ftAutoInc;
    'I' : //visual foxpro integer
      // todo: is this the right property to check for? Can't we check flags directly
      if FAutoIncStep=0 then
        FFieldType := ftInteger
      else
        FFieldType := ftAutoInc;
    'O' : //double, 8 bytes?
      FFieldType := ftFloat;
    '@', 'T' {Foxpro? datetime}:
      FFieldType := ftDateTime;
    'C', //character
    #$91  {Russian 'C'}:
      FFieldType := ftString;
    'L' : //logical
      FFieldType := ftBoolean;
    'F', 'N': //float/numeric
      begin
        if (FPrecision = 0) then
        begin
          if FSize <= DIGITS_SMALLINT then
            FFieldType := ftSmallInt
          else
          if FSize <= DIGITS_INTEGER then
            FFieldType := ftInteger
          else
{$ifdef SUPPORT_INT64}
            FFieldType := ftLargeInt;
{$else}
            FFieldType := ftFloat;
{$endif}
        end else begin
          FFieldType := ftFloat;
        end;
      end;
    'D' : //date
      FFieldType := ftDate;
    'M' : //memo
      FFieldType := ftMemo;
    'B' : //binary or float
      if (DbfVersion = xFoxPro) or (DbfVersion=xVisualFoxPro) then
        FFieldType := ftFloat
      else
        FFieldType := ftBlob;
    'G' : //general
      FFieldType := ftDBaseOle;
    'Y' : //currency
      if DbfGlobals.CurrencyAsBCD then
        FFieldType := ftBCD
      else
        FFieldType := ftCurrency;
    '0' : //zero, not the letter O
      FFieldType := ftBytes;
    'P' : //picture
      if (DBFversion in [xFoxPro,xVisualFoxPro]) then
        FFieldType := ftBlob; {Picture, but probably not compatible with ftGraphic storage}
    'V' : //VFP 9 Varchar; character with length indication
      if (DbfVersion = xVisualFoxPro) then
        FFieldType := ftString;
      //todo: verify if 'V' for other systems exists. DBF "Varifields"?
    'W' : //BLOB
      if (DBFVersion = xVisualFoxPro) then
        FFieldType := ftBlob;
    'Q' : //varbinary
      if (DBFVersion = xVisualFoxPro) then
        FFieldType := ftVarBytes;
  else
    FNativeFieldType := #0;
    FFieldType := ftUnknown;
  end; //case
end;

procedure TDbfFieldDef.VCLToNative;
begin
  FNativeFieldType := #0;
  // to do: look into ftBytes support; e.g. Visual FoxPro varbytes?
  case FFieldType of
    ftAutoInc  :
      if DbfVersion=xVisualFoxPro then
        FNativeFieldType  := 'I'
      else
        FNativeFieldType  := '+'; //Apparently xbaseV/7+ only; not (Visual) Foxpro
    ftDateTime :
      if DbfVersion = xBaseVII then
        FNativeFieldType := '@'
      else
      if (DbfVersion = xFoxPro) or (DbfVersion = xVisualFoxPro) then
        FNativeFieldType := 'T'
      else
        FNativeFieldType := 'D';
{$ifdef SUPPORT_FIELDTYPES_V4}
    ftFixedChar,
    ftWideString,
{$endif}
    ftString   :
      FNativeFieldType := 'C'; // VFP9: could have used V but this works, too.
    ftBoolean  :
      FNativeFieldType := 'L'; //logical
    ftFloat, ftSmallInt, ftWord
{$ifdef SUPPORT_INT64}
      , ftLargeInt
{$endif}
               :
      FNativeFieldType := 'N'; //numerical
    ftDate     :
      FNativeFieldType := 'D'; //date
    ftMemo     :
      FNativeFieldType := 'M'; //memo
    ftBlob     :
      case DBFVersion of
        xFoxPro:
          FNativeFieldType := 'P'; //picture; best we can do
        xVisualFoxPro:
          FNativeFieldType := 'W'; //blob
        xBaseIII,xBaseIV:
          FNativeFieldType := 'M'; //memo; best we can do
        xBaseV,xBaseVII:
          FNativeFieldType := 'B'; //binary
      else
        FNativeFieldType := 'M'; //fallback
      end;
    ftVarBytes :
      //todo: figure out if we can use the same fallbacks as ftBlob
      case DBFVersion of
        xVisualFoxPro:
          FNativeFieldType := 'Q'; //variant bytes
      end;
    ftDBaseOle :
      FNativeFieldType := 'G'; //general
      //todo: verify if this is dbaseV/7 specific
    ftGraphic  :
      // Let's store this as a BLOB even though FoxPro has P(icture).
      // P is apparently not recommended
      FNativeFieldType := 'B'; //BLOB
    ftInteger  :
      if (DbfVersion in [xBaseVII,xVisualFoxPro]) then
        FNativeFieldType := 'I' //integer
      else
        FNativeFieldType := 'N'; //numeric
    ftBCD, ftCurrency:
      if (DbfVersion = xFoxPro) or (DBFVersion = xVisualFoxPro) then
        FNativeFieldType := 'Y';
  end;
  if FNativeFieldType = #0 then
    raise EDbfError.CreateFmt(STRING_INVALID_VCL_FIELD_TYPE, [GetDisplayName, Ord(FFieldType)]);
end;

procedure TDbfFieldDef.SetDefaultSize;
begin
  // choose default values for variable size fields
  case FFieldType of
    ftFloat:
      begin
        FSize := 18;
        FPrecision := 8;
      end;
    ftCurrency, ftBCD:
      begin
        FSize := 8; // Stored in dbase as 8 bytes; up to 18 (or 20) characters including .-
        // FPC ftBCD/ftCurrency TFieldDef.Size has max 4 which is 4 bytes after decimal
        FPrecision := 4; //Total number of digits
      end;
    ftSmallInt:
      begin
        FSize := DIGITS_SMALLINT;
        FPrecision := 0;
      end;
    ftWord:
      begin
        FSize := DIGITS_WORD;
        FPrecision := 0;
      end;
    ftInteger, ftAutoInc:
      begin
        if DbfVersion in [xBaseVII,xVisualFoxPro] then
          FSize := 4 //I, @ field
        else
          FSize := DIGITS_INTEGER;
        FPrecision := 0;
      end;
{$ifdef SUPPORT_INT64}
    ftLargeInt:
      begin
        FSize := DIGITS_LARGEINT;
        FPrecision := 0;
      end;
{$endif}
    ftString {$ifdef SUPPORT_FIELDTYPES_V4}, ftFixedChar, ftWideString{$endif}:
      begin
        FSize := 30;
        FPrecision := 0;
      end;
  end; // case fieldtype

  // set sizes for fields that are restricted to single Size/precision
  CheckSizePrecision;
end;

procedure TDbfFieldDef.CheckSizePrecision;
// FSize means size in the database, not any VCL field size
begin
  case FNativeFieldType of
    'C','V','Q': // Character, Visual FoxPro varchar,Visual FoxPro varbinary
      begin
        if FSize < 0 then
          FSize := 0;
        if (DbfVersion = xFoxPro) or (DbfVersion = xVisualFoxPro) then
        begin
          if FSize >= $FFFF then
            FSize := $FFFF;
        end else begin
          if FSize >= $FF then
            FSize := $FF;
        end;
        FPrecision := 0;
      end;
    'L': // Logical/boolean
      begin
        FSize := 1;
        FPrecision := 0;
      end;
    'N','F': // Binary code decimal numeric, floating point binary numeric
      begin
        // ftBCD: precision=total number of digits; Delphi supports max 32
        // Note: this field can be stored as BCD or integer, depending on FPrecision;
        // that's why we allow 0 precision
        if FSize < 1   then FSize := 1;
        if FSize >= 20 then FSize := 20;
        if FPrecision > FSize-2 then FPrecision := FSize-2; //Leave space for . and -
        if FPrecision < 0       then FPrecision := 0;
      end;
    'D': // Date
      begin
        FSize := 8;
        FPrecision := 0;
      end;
    'B': // (Visual)Foxpro double, DBase binary
      begin
        if not(DbfVersion in [xFoxPro,xVisualFoxPro]) then
        begin
          FSize := 10;
          FPrecision := 0;
        end
        else
        begin
          FSize := 8; //Foxpro double
          FPrecision := 0;
        end;
      end;
    'M','G','P','W': // Memo, general, FoxPro picture, Visual FoxPro blob
      begin
        if (DbfVersion = xVisualFoxPro) then
        begin
          if (FSize <> 4) and (FSize <> 10) then
            FSize := 4;
        end else
          FSize := 10; //Dbase, includes FoxPro
        FPrecision := 0;
      end;
    '+','I': // Autoincrement, integer
      begin
        FSize := 4;
        FPrecision := 0;
      end;
    '@', 'O': //Timestamp, double (both DBase 7)
      begin
        FSize := 8;
        FPrecision := 0;
      end;
    'T': // DateTime
      begin
        if (DbfVersion = xFoxPro) or (DbfVersion = xVisualFoxPro) then
          FSize := 8
        else
          FSize := 14;
        FPrecision := 0;
      end;
    'Y': // Currency
      begin
        FSize := 8;
        FPrecision := 4;
      end;
  else
    // no idea/unimportant, let other code sort it out
  end;
end;

function TDbfFieldDef.GetDisplayName: string; {override;}
begin
  Result := FieldName;
end;

function TDbfFieldDef.IsBlob: Boolean; {override;}
begin
  // 'B' is float in (V)FP; W is Blob (VFP9)
  if (DbfVersion in [xFoxPro,xVisualFoxPro]) then
    Result := FNativeFieldType in ['M','G','W']
  else
    Result := FNativeFieldType in ['M','G','B'];
end;

procedure TDbfFieldDef.FreeBuffers;
begin
  if FDefaultBuf <> nil then
  begin
    // one buffer for all
    FreeMemAndNil(Pointer(FDefaultBuf));
    FMinBuf := nil;
    FMaxBuf := nil;
  end;
  FAllocSize := 0;
end;

procedure TDbfFieldDef.AllocBuffers;
begin
  // Size changed?
  if FAllocSize <> FSize then
  begin
    // free old buffers
    FreeBuffers;
    // alloc new
    GetMem(FDefaultBuf, FSize*3);
    FMinBuf := FDefaultBuf + FSize;
    FMaxBuf := FMinBuf + FSize;
    // store allocated Size
    FAllocSize := FSize;
  end;
end;

end.

