/// class used to store huge amount of data with fast retrieval
// - licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynBigTable;

(*
    Synopse Big Table. Copyright (c) Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse Big Table.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (c)
  the Initial Developer. All Rights Reserved.

  Contributor(s):

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

  What are these classes meant for?
  - add a simple storage and data persistence to your application, in a few KB of code
  - implement a logging or read/only audit trail mechanism into your application
  - access to compressed data on a CD-ROM or DVD-ROM (see our Open Source
    compression libraries in our web site) - you can even add items to the list,
    since they will remain in memory; but they will be lost when you close the file
  - have an easy way to share data between Delphi 7 and Delphi XE applications,
    without any Unicode / TStringList encoding headache
  - store external resource files (think about an hidden folder) in which you
    can have read and write access from your applications
  - store efficiently an huge number of records with fields, using
    TSynBigTableRecord: creating 1,000,000 records is less than 900 ms, with
    integrated indexes, low storage size and fast search possibilities
    (this class is much faster than any SQL engine)
  - store BLOB with some metadata fields associated, via TSynBigTableMetaData
    (can be used e.g. to store thunmbails of pictures, or HTML/XML pages,
    associated with some indexed data)
  - if you don't want to install a database engine and configure it
  - if you need speed and performance
  - if you don't want to reinvent the wheel
  - etc... etc...

  What are these classes NOT for?
  - interact with DBExpress or VCL database components
  - replacing NTFS - go to Linux and pick the right file system you need
  - storing hierarchical data (like directories) - use B-Tree and such instead
  - store huge data items (maximum is 1 GB for one item) - a few MB is good,
    but remember that data is stored into RAM before writting to disk
  - replace a SQL database engine - use our SQLite3 framework instead, see
    http://blog.synopse.info/category/Open-Source-Projects/SQLite3-Framework -
    but TSynBigTableMetaData and TSynBigTableRecord allow effective field
    handling, which could be sufficient as a simple database
  - save your soul or make me rich...

  DISCLOSER: this unit is not use on production since years, so is not
    really maintained

*)

interface

{$I Synopse.inc} // define HASINLINE CPU32 CPU64

{$ifndef LVCL}
{.$define THREADSAFE}
{ define this if you want to make the Synopse Big Table thread safe
  - we use internaly TMultiReadExclusiveWriteSynchronizer for better
   performance
  - not fully tested yet, just to ensure basic thread safety  }
{$endif}

{$R-}
{ Range Checking must be sent to OFF in the code below - code is safe anyway }
{$S-}
{ Stack Checking must be sent to OFF in the code below }

uses
  {$ifdef MSWINDOWS}
  Windows,
  {$else}
  {$ifdef FPC}
  SynFPCLinux,
  {$endif}
  {$endif}
  Classes,
  SysUtils,
  {$ifdef FPC}
  Variants,
  {$endif}
  SynCommons,
  SynTable, // for TSynTable/TSynTableStatement
  SynTests;

type
  TSynBigTable = class;

  /// possible actions for CustomHeader() protected virtual method
  TSynBigTableCustomHeader = (sbtRead, sbtBeforeWrite, sbtWrite, sbtAfterRead);

  /// the way the GetIterating() method will loop through all items
  // - ioInternalID and ioInternalPhysical are internal codes used by
  // GetID and GetAllPhysicalIndexes methods
  TSynBigTableIterationOrder = (ioNone, ioPhysical, ioID, ioFaster,
    ioInternalID, ioInternalPhysical);

  /// prototype of a callback function for iterating through all items
  // of a table
  // - will be called following the incremental ID order
  // - implementation can set the result to TRUE to break the iteration loop
  // - the Data pointer is either direct access to the direct mapped buffer,
  // or a global temporary buffer: use the data between two iterations,
  // but copy the content if you need some persistency
  // - the DataIndex parameter is the index of the data, physicaly on disk
  TSynBigTableIterateEvent = function(Sender: TObject; Opaque: pointer;
    ID, DataIndex: integer; Data: pointer; DataLen: integer): boolean of object;

  /// event called after a pack, just before the UpdateToFile()
  // - can be used to synchronized the field indexes, e.g.
  // - format of supplied parameter is NewIndexs[oldIndex] := newIndex
  TSynBigTableAfterPackEvent = procedure(var NewIndexs: TIntegerDynArray) of object;

  /// a class to store huge amount of data, just specified by an integer ID
  // - data is stored in an unique file
  // - retrieval is very fast (can't be faster IMHO)
  // - data is appended at the end of this file at adding (but use a caching
  // mechanism for immediate adding)
  // - use a temporary in memory adding, till the UpdateToFile method is called
  // - data items can be deleted
  // - file can be packed using the Pack method in order to retrieve free space
  // from deleted entries (sounds like a VACUUM command, but faster)
  // - total size of file has no limit (but your hard disk, of course)
  // - limit of one data block depends on RAM (RawByteString is used
  // as storage for data block)
  // - before Delphi 2007, much faster when using FastMM4 memory manager
  // - after profiling, most of the time is spent in the Windows kernel,
  // waiting from hard disk write of raw data; in all cases, this class is
  // much faster than any SQL engine storing BLOB, and than plain Win32 files
  // - ACID behavior can be enforced by calling UpdateToFile(true)
  TSynBigTable = class
  private
    function IterateGetAllID(Sender: TObject; Opaque: pointer; ID, Index: integer;
      Data: pointer; DataLen: integer): boolean;
    function IteratePack(Sender: TObject; Opaque: pointer; ID, Index: integer;
      Data: pointer; DataLen: integer): boolean;
    /// used in TestBigTable function
    function TestIterateEvent(Sender: TObject; Opaque: pointer; ID, Index: integer;
      Data: pointer; DataLen: integer): boolean;
    function TestIterateEventSpeed(Sender: TObject; Opaque: pointer; ID, Index: integer;
      Data: pointer; DataLen: integer): boolean;
    /// retrieve a genuine integer value to identify the file content
    class function InternalMagic: integer; virtual;
  protected
    fFileName: TFileName;
    fCount: integer;
    fCurrentID: integer;
    fCurrentInMemoryDataSize: Int64;
    fInMemoryCount: integer;
    fInMemoryData: TRawByteStringDynArray;
    fInMemoryID: TIntegerDynArray;
    fDeletedCount: integer;
    fAliasCount: integer;
    fModified: boolean;
    fFile: THandle;
    /// all IDs, in the same order as the storage
    // - may contain an ID from a deleted entry -> don't use indexed with 0..Count-1
    // - Offset[] are in another dynamic array, so the code is somewhat faster
    fID: TIntegerDynArray;
    /// all end offsets of the storage data in the file (up to 1 GB of data)
    // - may contain an offset from a deleted entry -> don't use indexed with 0..Count-1
    // - ID[] are in another dynamic array, so the code is somewhat faster
    fOffset32: TIntegerDynArray;
    /// all end offsets of the storage data in the file (used for huge amount of data)
    // - may contain an offset from a deleted entry -> don't use indexed with 0..Count-1
    // - ID[] are in another dynamic array, so the code is somewhat faster
    fOffset64: TInt64DynArray;
    /// array of deleted items IDs
    // - this array is stored in ascending order, to make search faster
    fDeleted: TIntegerDynArray;
    /// array of Aliases for updated records
    fAliasSource, fAliasReal, fAliasRealSorted: TIntegerDynArray;
{$ifdef THREADSAFE}
    fLock: TMultiReadExclusiveWriteSynchronizer;
{$endif}
    fFileOpenMode: Cardinal;
    fReadBuffer: TFileBufferReader;
    fOnAfterPack: TSynBigTableAfterPackEvent;
    function GetCount: integer; virtual;
    function GetFileSizeOnDisk: Int64; virtual;
    /// this default implementation can be slow, because it must loop through all
    // items in case of deleted or updated items
    function GetID(Index: integer): integer; virtual;
    /// retrieve an index from a supplied ID
    // - return -1 if this ID is not existing in the index
    // - warning: return an index, even if the ID is in the fDeleted[] list
    // - but handle aliases IDs (after Update() method call)
    function IDToIndex(aID: integer; handleAliases: boolean=true): integer;
    /// retrieve an ID from a supplied index
    // - can handle aliases IDs (after Update() method call), i.e. return the
    // original ID, from the index of the updated content
    function IndexToID(aIndex: Integer; handleAliases: boolean=true): integer; virtual;
    /// retrieve a content, from its raw index in the database
    function GetFromIndex(aIndex: integer; out aData: RawByteString): boolean;
    /// retrieve a pointer to a content, from its raw index in the database
    function GetPointerFromIndex(aIndex: Integer; var aTempData: RawByteString;
      DataLen: PInteger=nil): pointer;
    /// load index from file into fID[], fMemory[] and fDeleted[]
    procedure LoadFromFile;
    /// returns true if the supplied file matches the current TSynBigTable class
    // - will allow making a difference between all implementation classes,
    // i.e. you can call TSynBigTable.FileFormatCheck,
    // TSynBigTableString.FileFormatCheck, TSynBigTableRecord.FileFormatCheck
    // or TSynBigTableMetaData.FileFormatCheck to verify a file layout
    // - uses the protected virtual InternalMagic method
    class function FileFormatCheck(const aFileName: TFileName): boolean;
    /// virtual method which can be used to store additional data into the header
    // - do nothing by default in TSynBigTable (means no additional header data)
    // - if Action is sbtBeforeWrite, prepare the data before writing: at the
    // sbtWrite step, update data won't be available any more (fReadBuffer is
    // already closed so e.g. indexes should be recreated in this first step)
    // - if Action is sbtWrite, write the custom header data to WriteBuffer
    // - if Action is sbtRead, read the custom header data from fReadBuffer
    // - if Action is sbtAfterRead, all headers have been read from disk: the
    // class can therefore synchronize its content with the data (to recreate
    // any missing index, for instance)
    function CustomHeader(Action: TSynBigTableCustomHeader;
      WriteBuffer: TFileBufferWriter): integer; virtual;
    /// retrieve an offset for a specified physical ID
    // - read from either fOffset32[] either fOffset64[]
    function GetOffset(Index: integer): Int64;
    /// retrieve an index from a supplied ID, and the associated AliasReal[] index
    function InternalIDToIndex(aID: integer; var aAliasIndex: integer): integer;
    /// update the content, returing the updated physical ID
    function InternalUpdateFromIndex(const aData: RawByteString;
      aID, aIndex, aAliasIndex: integer; var aNewIndex: cardinal): integer;
    /// will recreate the file processing all deleted or updated data, following
    // the ID order
    // - the callback must write the content (after any possible content change)
    // to the Opaque.WR file writer
    procedure RecreateFileContent(aCallBack: TSynBigTableIterateEvent;
      const OpaqueFields: TSQLFieldBits=[]);
  public
    /// initialize the database engine with a supplied filename
    // - InternalCacheSize can be used to customize the internal cache count
    // (set to 0 to disable caching; 128 is a good value, bigger makes no difference)
    // - you can specify a custom file open mode attributes on request, like
    // fmShareDenyNone (not set by default, for safety reason)
    constructor Create(const aFileName: TFileName; FileOpenMode: Cardinal = 0); reintroduce;
    /// finalize memory, and save all content
    destructor Destroy; override;
    /// clear the whole table content and indexs
    procedure Clear; virtual;
    /// add a data to the database
    // - return the unique ID created to identify this data
    // - you can force a specified ID number, by setting a non null value to
    // ForcedID (in this case, it MUST be added in increasing order)
    // - return 0 on error, otherwise the created Physical Index can be stored
    // - OldPhysicalIndex is to be used in case of constraint check (for
    // TSynBigTableRecord and TSynBigTableMetaData)
    function Add(const aData: RawByteString; ForcedID: integer=0;
      PhysicalIndex: PInteger=nil; OldPhysicalIndex: integer=-1): integer; virtual;
    /// add a file content to the database
    // - return the unique ID created to identify this data
    // - return 0 on error (e.g. specified file doesn't exist)
    function AddFile(const aFileName: TFileName): integer;
    /// retrieve a content, from a supplied ID
    // - return TRUE if found, FALSE if ID was not existing (or deleted)
    // - return the data into aData, or '' if ID was not existing (or deleted)
    function Get(aID: integer; out aData: RawByteString): boolean; overload;
    /// retrieve a content, from a supplied ID, into a TStream
    // - this method is faster than Get() into a RawByteString, because the
    // data is not moved from memory but mapped into a TCustomMemoryStream
    // - if the ID is not correct, returns nil
    // - if the ID is correct, returns a TStream instance, able to access to
    // the associated content
    // - in most cases, this TStream is just a wrapper around the memory mapped
    // buffer in memory
    // - the TStream must be consumed immediately, before any Pack or
    // UpdateToFile method calls
    // - the caller must Free the returned TStream instance
    // - if the data is not already memory mapped (i.e. for files >= 2 GB)
    // a custom TSynMemoryStreamMapped is used to access the data from disk
    function GetAsStream(aID: integer): TStream;
    /// retrieve the length of a content, from a supplied ID
    // - return -1 if the ID was not found, or the length (in bytes) of this ID content
    function GetLength(aID: integer): integer;
    /// retrieve a content, from a supplied ID, into a pointer
    // - returns nil on error
    // - returns a pointer to the data on success, directly from the memory mapped
    // file on most cases; if the data is not in a memory mapped buffer (i.e.
    // for files >= 2 GB) the aTempData variable is used to read the data from disk
    // - in case of success, if DataLen is not nil, it will be filled with the
    // corresponding data length
    // - this method is therefore much faster than Get()
    function GetPointer(aID: Integer; var aTempData: RawByteString;
      DataLen: PInteger=nil): pointer;
    /// retrieve a content, from a supplied ID, into a pointer
    // - returns nil on error
    // - returns a pointer to the data on success, directly from the memory mapped
    // file on most cases; if the data is not in a memory mapped buffer (i.e.
    // for files >= 2 GB) the aTempData variable is used to read the data from disk
    // - this method is not thread-safe (but is therefore faster)
    function GetPointerFromPhysicalIndex(aPhysicalIndex: integer;
      var aTempData: RawByteString): pointer;
    /// call a supplied Event method by iterating through all table items
    // - Event will be called following the physical order of data in the disk
    // file (somewhat faster), or incremental ID depending of Order parameter
    // - Event can set the result to TRUE to break the iteration loop
    // - the Opaque parameter will be supplied to the callback Event
    // - set DontRetrieveData to TRUE if you don't need any data to be set
    // in the callback, but only the ID (faster)
    procedure GetIterating(aCallBack: TSynBigTableIterateEvent;
      Order: TSynBigTableIterationOrder=ioPhysical; Opaque: pointer=nil;
      DontRetrieveData: Boolean=false);
    /// fast retrieval of all IDs
    // - returned in physical or increasing ID value order
    // - returns the number of IDs stored in the integer array
    function GetAllIDs(var IDs: TIntegerDynArray;
      Order: TSynBigTableIterationOrder=ioPhysical): integer; virtual;
    /// fast retrieval of all used items physical indexes
    // - returned in physical order
    // - returns the number of Indexes stored in the integer array
    function GetAllPhysicalIndexes(var Indexes: TIntegerDynArray): integer;
    /// update a record content in the database
    // - in fact, a new invisible record is created, and an alias will map this
    // new record to the original ID
    // - the physical replacement will take place only during Pack method call
    // - returns the new content ID value on success
    // - returns 0 on error
    function Update(aID: Integer; const aData: RawByteString;
      PhysicalIndexOldNew: PInt64=nil): integer; virtual;
    /// retrieve a part of a file content
    // - faster than Open/Seek/Read methods, which loads the whole content in memory
    // before Seek and Read
    // - only the needed part of data is copied into aData
    function GetPart(aID: integer; Offset, Len: Integer; out aData: RawByteString): boolean;
    /// retrieve a list of content, from a supplied ID range (including boundaries)
    // - return TRUE if all were found, FALSE if some ID were not existing (or deleted)
    // - return the data into aData[], or '' if one particular ID was not
    // existing (or deleted); after call, length(aData)=aIDLast-aIDFirst+1
    function Get(aIDFirst, aIDLast: integer; out aData: TRawByteStringDynArray): boolean; overload;
    /// delete an ID
    function Delete(aID: integer; PhysicalIndex: PInteger=nil): boolean; virtual;
    /// pack the database, i.e. delete all formerly deleted ID from the disk
    // - if forceFlushOnDisk is TRUE, data is forced to be saved on the disk
    // (slower but allow ACID behavior of the database file)
    procedure Pack(forceFlushOnDisk: boolean=false);
    /// save last added entries into the files
    // - do nothing is nothing is to be written (if forceAlwaysWrite is false)
    // - can be called from time to time, after checking CurrentInMemoryDataSize
    // - if forceFlushOnDisk is TRUE, data is forced to be saved on the disk
    // (slower but allow ACID behavior of the database file)
    procedure UpdateToFile(forceFlushOnDisk: boolean=false;
      dontReopenReadBuffer: boolean=false);
    /// the entries count
    property Count: integer read GetCount;
    /// the associated filename storing the database
    property FileName: TFileName read fFileName;
    /// returns the current in memory data size (in bytes)
    // - i.e. the data size not written yet to the disk
    // - can be used to flush regularely the data to disk by calling UpdateToFile
    // method when this value reach a certain limit
    property CurrentInMemoryDataSize: Int64 read fCurrentInMemoryDataSize;
    /// returns the current data size stored on disk
    property FileSizeOnDisk: Int64 read GetFileSizeOnDisk;
    /// retrieve an offset for a specified physical ID
    // - read from either fOffset32[] either fOffset64[]
    property Offset[Index: integer]: Int64 read GetOffset;
    /// read-only access to a numerical ID, from its index
    // - index is from NumericalID[0] to NumericalID[Count-1]
    // - follows the numerical ID order for TSynBigTable, and the alphabetical
    // order of UTF-8 keys for TSynBigTableString
    // - return 0 in case of out of range index
    // - this method can be slow with TSynBigTable (if they are some deleted or
    // updated items - just call the Pack method to improve speed); but with
    // a TSynBigTableString instance, it will be always fast
    // - don't use it to loop through all items, but rather the dedicated
    // GetIterating() or GetAllIDs() fast methods
    property NumericalID[Index: integer]: integer read GetID;
    /// event called after a pack, just before the UpdateToFile() call
    // - can be used to synchronized the field indexes, e.g.
    property OnAfterPack: TSynBigTableAfterPackEvent read fOnAfterPack write fOnAfterPack;
  end;

  /// a class to store huge amount of data, just specified by a string ID
  // - string ID are case-sensitive (important warning)
  // - string ID are of RawUTF8 type, so you must make explicit conversion
  // in your program to the native generic string type - you can use our
  // Utf8ToString() and StringToUtf8() functions, which work for
  // all version of Delphi (from Delphi 6 up to XE)
  TSynBigTableString = class(TSynBigTable)
  private
    function GetStringID(Index: integer): RawUTF8;
  protected
    /// = real Count property = fCount-fDeletedCount
    fHeaderCount: integer;
    /// store String IDs (in alpha-sorted order)
    fHeaderString: TRawUTF8DynArray;
    /// store associated numerical IDs (in alpha-sorted order)
    fHeaderID: TIntegerDynArray;
    /// overridden method which handle String ID loading and writing
    function CustomHeader(Action: TSynBigTableCustomHeader;
      WriteBuffer: TFileBufferWriter): integer; override;
    class function InternalMagic: integer; override;
    /// faster method using fHeaderID[]
    function GetID(Index: integer): integer; override;
    /// faster method returning fHeaderCount
    function GetCount: integer; override;
  public
    /// clear the whole table content and indexs
    procedure Clear; override;
    /// add a data to the database, and its associated string ID
    // - return the unique numerical ID created to identify this data
    // - return 0 if the string ID is invalid (i.e. void or already used)
    function Add(const aData: RawByteString; const aID: RawUTF8; ForcedID: integer=0): integer; reintroduce;
    /// retrieve a content, from a supplied string ID
    // - return TRUE if found, FALSE if this ID was not existing (or deleted)
    // - return the data into aData, or '' if ID was not existing (or deleted)
    function Get(const aID: RawUTF8; out aData: RawByteString): boolean; overload;
    /// retrieve a content, from a supplied ID, into a pointer
    // - returns nil on error
    // - returns a pointer to the data on success, directly from the memory mapped
    // file on most cases; if the data is not in a memory mapped buffer (i.e.
    // for files >= 2 GB) the aTempData variable is used to read the data from disk
    // - in case of success, if DataLen is not nil, it will be filled with the
    // corresponding data length
    // - this method is therefore much faster than Get() for big size of data
    function GetPointer(const aID: RawUTF8; var aTempData: RawByteString;
      DataLen: PInteger=nil): pointer; overload;
    /// retrieve a content, from a supplied ID, into a TStream
    // - if the ID is not correct, returns nil
    // - if the ID is correct, returns a TStream instance, able to access to
    // the associated content
    // - in most cases, this TStream is just a wrapper around the memory mapped
    // buffer in memory
    // - the TStream must be consumed immediately, before any Pack or
    // UpdateToFile method calls
    // - the caller must Free the returned TStream instance
    // - if the data is not already memory mapped (i.e. for files >= 2 GB)
    // a custom TSynMemoryStreamMapped is used to access the data from disk
    function GetAsStream(const aID: RawUTF8): TStream; overload;
    /// fast retrieval of all IDs
    // - this overridden method handle ioFaster order, i.e; the fHeaderID[] content
    // - returns the number of IDs stored in the integer array
    function GetAllIDs(var IDs: TIntegerDynArray;
      Order: TSynBigTableIterationOrder=ioPhysical): integer; override;
    /// delete an entry from its numerical ID
    function Delete(aID: integer; PhysicalIndex: PInteger=nil): boolean; overload; override;
    /// delete an entry from its string ID
    // - return true if the record was successfully deleted
    function Delete(const aID: RawUTF8): boolean; reintroduce; overload;
    /// update a record content in the database
    // - return true if the record was successfully updated
    function Update(const aData: RawByteString; const aID: RawUTF8): boolean; reintroduce; overload;
    /// retrieve a numerical ID from a UTF-8 encoded string ID
    // - return 0 if this string ID was not found
    function StringToID(const aID: RawUTF8): integer;
    /// retrieve the UTF-8 encoded string ID of a given numerical ID
    // - return '' if this ID was not found
    function IDToString(aID: integer): RawUTF8;
    /// read-only access to a string ID, from its index
    // - index is from StringID[0] to StringID[Count-1]
    // - string IDs are alphabetically sorted
    // - return '' in case of out of range index
    property StringID[Index: integer]: RawUTF8 read GetStringID;
  end;

  TSynBigTableTableClass = class of TSynBigTableTable;

  /// an abstract class, associating a TSynTable to a Big Table
  // - use optimized TSynTable logic for handling field values, using
  // our SBF compact binary format (similar to BSON or Protocol Buffers)
  TSynBigTableTable = class(TSynBigTable)
  protected
    fTableName: RawUTF8;
    fTable: TSynTable;
    /// used by Search() method below
    function IterateSearch(Sender: TObject; Opaque: pointer; ID, Index: integer;
      Data: pointer; DataLen: integer): boolean;
    /// false e.g. if a tftUnique constraint failed
    // - RecordIndex=-1 in case of adding, or the physical index of the updated record
    function CheckConstraints(const aRecordData: RawByteString; RecordIndex: integer): boolean;
      {$ifdef HASINLINE}inline;{$endif}
    /// internal version used by Search() method
    procedure SearchIterating(aCallBack: TSynBigTableIterateEvent;
      Order: TSynBigTableIterationOrder; Opaque: pointer); virtual; abstract;
  public
    /// initialize the database engine with a supplied filename
    // - you can specify an internal Table Name, similar to SQL table name
    // - you should better call either TSynBigTableMetaData or TSynBigTableRecord
    // reintroduced constructor, which will set GetRecordData parameter as expected
    constructor Create(const aFileName: TFileName; const aTableName: RawUTF8;
       GetRecordData: TSynTableGetRecordData; FileOpenMode: Cardinal = 0); reintroduce;
    /// finalize memory, and save all content
    destructor Destroy; override;
    /// add a field description to the table
    // - just a wrapper to the Table.AddField method
    // - warning: caller must call the AddFieldUpdate method when all
    // AddField() methods have been called, in order to eventually process
    // all already existing data to the resulting new field order
    // - physical order does not necessary follow the AddField() call order:
    // for better performance, it will try to store fixed-sized record first,
    // multiple of 4 bytes first (access is faster if dat is 4 byte aligned),
    // then variable-length after fixed-sized fields; in all case, a field
    // indexed will be put first
    // - if tfoUnique is set in aOptions and there is already some data, this
    // method will raise an exception: it's not possible to a have multiple
    // void data unique, so it will always fail the constraint
    function AddField(const aName: RawUTF8; aType: TSynTableFieldType;
      aOptions: TSynTableFieldOptions=[]): boolean;
    /// this method must be called after calls to AddField/Table.AddField
    // methods
    // - this will launch the recreation of the database file content, if some
    // field were effectively added (to map the new field layout): in this case
    // some default void value is set for all newly added fields
    // - for TSynBigTableRecord, this method may recreate the field order then
    // reload all field instances: you must retrieve all TSynTableFieldProperties
    // instances after this method call via proper
    // ! aField := Table.Table['FieldName'];
    procedure AddFieldUpdate; virtual; abstract;
    /// search for a matching value in a given field
    // - add the matching IDs in ResultID[] (in sorted order, with no duplicate),
    // and update the number matching of elements in ResultIDCount (for performance
    // reasons, the ResultID[] array remains filled with 0 until
    // ResultID[ResultIDCount-1] itemk)
    // - will use any existing index, or will iterate through all data (slower)
    // if the ForceIterate parameter is either ioPhysical or ioID
    // - the Limit parameter is similar to the SQL LIMIT clause: if greater than 0,
    // an upper bound on the number of rows returned is placed (e.g. set Limit=1
    // to only retrieve the first match)
    function Search(Field: TSynTableFieldProperties; const WhereValue: TSBFString;
      var ResultID: TIntegerDynArray; var ResultIDCount: integer; Limit: Integer=0;
      ForceIterate: TSynBigTableIterationOrder=ioNone): boolean; overload;
{$ifndef LVCL}
    /// search for a matching value in a given field
    // - add the matching IDs in ResultID[] (in sorted order, with no duplicate)
    // - will use any existing index, or will iterate through all data (slower)
    // if the ForceIterate parameter is either ioPhysical or ioID
    // - the Limit parameter is similar to the SQL LIMIT clause: if greater than 0,
    // an upper bound on the number of rows returned is placed (e.g. set Limit=1
    // to only retrieve the first match)
    function Search(Field: TSynTableFieldProperties; const WhereValue: variant;
      var ResultID: TIntegerDynArray; var ResultIDCount: integer; Limit: Integer=0;
      ForceIterate: TSynBigTableIterationOrder=ioNone): boolean; overload;
      {$ifdef HASINLINE}inline;{$endif}
{$endif}
    /// clear the whole table content and indexs
    // - also delete the field layout
    procedure Clear; override;
    /// the associated field description
    property Table: TSynTable read fTable;
    /// the internal Table Name
    property TableName: RawUTF8 read fTableName;
  public
{$ifndef LVCL}
    /// retrieve a void TSynTableVariantType variant instance
    // - similar to a call to Table.Data call
    function VariantVoid: Variant;
    /// retrieve a TSynTableVariantType variant to access a record properties
    function VariantGet(aID: integer): Variant; virtual; abstract;
{$endif}
    /// retrieve a record as a TSynTableData to access its properties
    // - using TSynTableData is faster than a TSynTableVariantType variant
    function RecordGet(aID: integer): TSynTableData; overload;
      {$ifdef HASINLINE}inline;{$endif}
    /// retrieve a record as a TSynTableData to access its properties
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - this overloaded function doesn't use a function return, therefore
    // will avoid a Record copy content (faster)
    procedure RecordGet(aID: integer; var result: TSynTableData); overload; virtual; abstract;
    /// update a Record from a given TSynTableData content
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - aRecord.ID is used to identify the record for calling raw Update()
    // - returns TRUE on success, FALSE on error (e.g. tftUnique constraint failure)
    // - for TSynBigTableMetaData, only update the metadata content, not the
    // main record content
    function RecordUpdate(const aDataRecord: TSynTableData): boolean; virtual; abstract;
  end;

  /// a class to store huge data (like files content), with metadata fields
  // associated with every record
  // - this class will store the fields in memory, then uses TSynBigTable records
  // to store some huge data blocks (e.g. file content), whereas TSynBigTableRecord
  // will store the fields in the records: TSynBigTableRecord is prefered for
  // huge number of records, and TSynBigTableMetaData is designed for less number
  // of records, but will natively handle associated "blob-like" data. For instance,
  // TSynBigTableRecord would be the right class to implement a logging table,
  // whereas TSynBigTableMetaData would be ideal for storing pictures.
  // - use optimized TSynTable logic for handling metadata field values, using
  // our SBF compact binary format (similar to BSON or Protocol Buffers)
  // - you can access to any metadata fields by using a custom
  // TSynTableVariantType variant type, allowing late-binding in the code
  // (this method is slower than direct access to the data due to the Variant
  // overhead, but is perhaps more convenient)
  TSynBigTableMetaData = class(TSynBigTableTable)
  protected
    fMetaDataCount: Integer;
    fMetaDataRecords: TRawByteStringDynArray;
    fMetaDataID: TIntegerDynArray;
    /// retrieve pointer to the metadata, SBF-encoded
    function GetMetaData(aPhysicalIndex: integer; var aTempData: RawByteString): pointer;
    /// faster method using function[]
    function GetID(Index: integer): integer; override;
    /// retrieve an ID from a supplied index
    // - can handle aliases IDs (after Update() method call), i.e. return the
    // original ID, from the index of the updated content
    function IndexToID(aIndex: Integer; handleAliases: boolean=true): integer; override;
    /// faster method returning fHeaderCount
    function GetCount: integer; override;
    /// overridden method which handle field description loading and writing
    function CustomHeader(Action: TSynBigTableCustomHeader;
      WriteBuffer: TFileBufferWriter): integer; override;
    class function InternalMagic: integer; override;
    /// internal version used by Search() method
    // - only handle internal order by now (i.e. ioID)
    procedure SearchIterating(aCallBack: TSynBigTableIterateEvent;
      Order: TSynBigTableIterationOrder; Opaque: pointer); override;
  public
    /// initialize the database engine with a supplied filename
    // - you can specify an internal Table Name, similar to SQL table name
    constructor Create(const aFileName: TFileName; const aTableName: RawUTF8;
       FileOpenMode: Cardinal = 0); reintroduce;
    /// this method must be called after calls to AddField/Table.AddField
    // methods
    // - this will launch the recreation of the database file content, if some
    // field were effectively added (to map the new field layout): in this case
    // some default void value is set for all newly added fields
    // - for TSynBigTableMeta, this method may recreate the field order, but
    // won't change the TSynTableFieldProperties instances
    procedure AddFieldUpdate; override;
    /// retrieve the metadata record of a given ID, encoded in our SBF format
    // - it could be more convenient to use VariantGet() or even the faster
    // RecordGet() methods
    function GetMetaDataFromID(aID: integer): pointer;
    /// overridden method to delete an entry from its numerical ID
    // - this method will handle the metadata fields synchronization
    function Delete(aID: integer; PhysicalIndex: PInteger=nil): boolean; override;
    /// add a data item with its associated metadata record to the table
    // - the metadata record uses our SBF enconding, and is mandatory
    // - returns the unique ID created to identify this data
    // - returns 0 on adding error (e.g. if a tftUnique constraint failed, or
    // if the supplied aMetaData is void)
    function Add(const aData: RawByteString; const aMetaData: TSBFString): integer; reintroduce; overload;
    /// update a metadata record using our SBF enconding
    // - returns TRUE on success, FALSE on error (e.g. tftUnique constraint failure)
    // - this method will only update the meta data - the main record data must
    // be updated with the inherited Update() method
    function Update(aID: integer; const aMetaData: TSBFString): boolean; reintroduce; overload;
  public
{$ifndef LVCL}
    /// add a data item with its associated metadata record to the table
    // - the metadata record is a TSynTableVariantType variant
    // - returns the unique ID created to identify this data
    // - returns 0 on adding error (e.g. if a tftUnique constraint failed)
    function VariantAdd(const aData: RawByteString; const aMetaDataRecord: Variant): integer;
    /// retrieve a TSynTableVariantType variant to access a record metadata
    function VariantGet(aID: integer): Variant; override;
    /// update a metadata record as TSynTableVariantType variant
    // - aRecord.ID is used to identify the record for calling raw Update()
    // - returns TRUE on success, FALSE on error (e.g. tftUnique constraint
    // failure, or wrong variant type)
    // - this method will only update the meta data - the main record data must
    // be updated with the inherited Update() method
    function VariantUpdate(const aMetaDataRecord: Variant): boolean;
{$endif}
    /// add a record to the table, with associated meta data
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - return the unique ID created to identify this data
    // - returns 0 on adding error (e.g. if a tftUnique constraint failed)
    function RecordAdd(const aData: RawByteString;
      const aMetaDataRecord: TSynTableData): integer;
    /// retrieve a record as a TSynTableData to access its properties
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - this overloaded function doesn't use a function return, therefore
    // will avoid a Record copy content (faster)
    procedure RecordGet(aID: integer; var result: TSynTableData); overload; override;
    /// update a Record from a given TSynTableData content
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - aRecord.ID is used to identify the record for calling raw Update()
    // - returns TRUE on success, FALSE on error (e.g. tftUnique constraint failure)
    // - this method will only update the meta data - the main record data must
    // be updated with the inherited Update() method
    function RecordUpdate(const aMetaDataRecord: TSynTableData): boolean; override;
  end;

  /// a class to store huge amount of data, with fields in every record
  // - this class will store the fields in the TSynBigTable records, whereas
  // TSynBigTableMetaData will store the fields in memory, and will uses records
  // to store some huge data blocks (e.g. file content): TSynBigTableRecord is
  // prefered for huge number of records, and TSynBigTableMetaData is designed
  // for less records, but with associated "blob-like" data. For instance,
  // TSynBigTableRecord would be the right class to implement a logging table,
  // where TSynBigTableMetaData would be ideal for storing pictures.
  // - use optimized TSynTable logic for handling field values, using
  // our SBF compact binary format (similar to BSON or Protocol Buffers)
  // - you can access to any record content fields by using a custom
  // TSynTableVariantType variant type, allowing late-binding in the code
  // (this method is slower than direct access to the data due to the Variant
  // overhead, but is perhaps more convenient)
  TSynBigTableRecord = class(TSynBigTableTable)
  protected
    /// overridden method which handle field description loading and writing
    function CustomHeader(Action: TSynBigTableCustomHeader;
      WriteBuffer: TFileBufferWriter): integer; override;
    class function InternalMagic: integer; override;
    /// version used by Search() method, calling the default GetIterating method
    procedure SearchIterating(aCallBack: TSynBigTableIterateEvent;
      Order: TSynBigTableIterationOrder; Opaque: pointer); override;
    /// refresh all fields indexes
    procedure RefreshIndexes(var NewIndexs: TIntegerDynArray);
  public
    /// initialize the database engine with a supplied filename
    // - you can specify an internal Table Name, similar to SQL table name
    constructor Create(const aFileName: TFileName; const aTableName: RawUTF8;
       FileOpenMode: Cardinal = 0); reintroduce;
    /// this method must be called after calls to AddField/Table.AddField
    // methods
    // - this will launch the recreation of the database file content, if some
    // field were effectively added (to map the new field layout): in this case
    // some default void value is set for all newly added fields
    // - for TSynBigTableRecord, this method may recreate the field order then
    // reload all field instances: you must retrieve all TSynTableFieldProperties
    // instances after this method call via proper
    // ! aField := Table.Table['FieldName'];
    procedure AddFieldUpdate; override;
  public
    /// overridden method to add a record to the database
    // - this method will handle the field indexes synchronization
    // - returns 0 on adding error (e.g. if a tftUnique constraint failed)
    function Add(const aData: RawByteString; ForcedID: integer=0;
      PhysicalIndex: PInteger=nil; OldPhysicalIndex: integer=-1): integer; override;
    /// overridden method to delete an entry from its numerical ID
    // - this method will handle the field indexes synchronization
    function Delete(aID: integer; PhysicalIndex: PInteger=nil): boolean; override;
    /// overridden method to update a record content in the database
    // - returns 0 on updating error (e.g. if a tftUnique constraint failed)
    function Update(aID: Integer; const aData: RawByteString;
      PhysicalIndexOldNew: PInt64=nil): integer; override;
  public
{$ifndef LVCL}
    /// add a record to the table
    // - the record is a TSynTableVariantType variant
    // - returns the unique ID created to identify this data
    // - returns 0 on adding error (e.g. if a tftUnique constraint failed)
    function VariantAdd(const aRecord: Variant): integer;
    /// retrieve a TSynTableVariantType variant to access a record properties
    function VariantGet(aID: integer): Variant; override;
    /// update a TSynTableVariantType variant
    // - aRecord.ID is used to identify the record for calling raw Update()
    // - returns TRUE on success, FALSE on error (e.g. tftUnique constraint failure)
    function VariantUpdate(const aRecord: Variant): boolean;
{$endif}
    /// add a record to the table
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - return the unique ID created to identify this data
    // - you can specify an expected ID to be used in aForceID parameter
    // - returns 0 on adding error (e.g. if a tftUnique constraint failed)
    function RecordAdd(const aRecord: TSynTableData; aForcedID: integer=0): integer;
    /// retrieve a record as a TSynTableData to access its properties
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - this overloaded function doesn't use a function return, therefore
    // will avoid a Record copy content (faster)
    procedure RecordGet(aID: integer; var result: TSynTableData); overload; override;
    /// update a Record from a given TSynTableData content
    // - using TSynTableData is faster than a TSynTableVariantType variant
    // - aRecord.ID is used to identify the record for calling raw Update()
    // - returns TRUE on success, FALSE on error (e.g. tftUnique constraint failure)
    function RecordUpdate(const aRecord: TSynTableData): boolean; override;
  end;

  /// unitary testing of the SynBigTable unit
  TTestBigTable = class(TSynTestCase)
  protected
    FN: TFileName;
    Updated: TIntegerDynArray;
    UpdatedCount: integer;
    Deleted: TIntegerDynArray;
    class function CreateString(aID: integer; n: integer=0): RawByteString;
    function TestString(aID: integer; const Data: RawByteString): boolean;
    function TestStream(aID: integer; Data: TStream): boolean;
    function Delete(var T: TSynBigTable; n: integer; Pack,withString: boolean): boolean;
    function DoFieldTest(DoRecord: boolean; n: integer): boolean;
  published
    /// test TSynBigTable class
    procedure _TSynBigTable;
    /// test TSynBigTableString class
    procedure _TSynBigTableString;
    /// test TSynBigTableMetaData class
    procedure _TSynBigTableMetaData;
    /// test TSynBigTableRecord class
    procedure _TSynBigTableRecord;
  end;

{$ifndef FPC}
/// unitary test function of the TSynBigTable class
// - return TRUE if test was OK, FALSE on any error
function TestBigTable: boolean;
{$endif}

const
  /// will flush the in-memory data to disk when reached 256 MB of data in RAM
  BIGTABLE_AUTOFLUSH_SIZE = 1 shl 28;


implementation

{$ifndef FPC}
function TestBigTable: boolean;
procedure SetS(var s: RawUTF8; i: cardinal);
var p: array[0..3] of Word;
begin
  p[2] := TwoDigitLookupW[(i div 100)mod 100];
  p[3] := TwoDigitLookupW[i mod 100];
  i := i div 10000;
  p[0] := TwoDigitLookupW[i div 100];
  p[1] := TwoDigitLookupW[i mod 100];
  SetString(s,PAnsiChar(@p[0]),8);
end;
var
  Updated: TIntegerDynArray;
  UpdatedCount: integer;
function TestString(aID: integer; const Data: RawByteString): boolean;
var i: integer;
begin
  result := false;
  if FastFindIntegerSorted(pointer(Updated),UpdatedCount-1,aID)>=0 then
    inc(aID,$01010101); // an updated ID has had its content refreshed
  for i := 0 to (length(Data) shr 2)-1 do
    if PIntegerArray(Data)^[i]<>aID then
      exit;
  result := true;
end;
function TestStream(aID: integer; Data: TStream): boolean;
var i: integer;
begin
  result := false;
  if not Data.InheritsFrom(TCustomMemoryStream) then
    exit;
  if FastFindIntegerSorted(pointer(Updated),UpdatedCount-1,aID)>=0 then
    inc(aID,$01010101); // an updated ID has had its content refreshed
  with TCustomMemoryStream(Data) do
    for i := 0 to (Size shr 2)-1 do
      if PIntegerArray(Memory)^[i]<>aID then
        exit;
  result := true;
  Data.Free;
end;
var i,n,id: integer;
    T: TSynBigTable;
    TS: TSynBigTableString absolute T; // absolute T -> Delete() will work
    Data, Data2: RawByteString;
    FN: TFileName;
    Deleted: TIntegerDynArray;
    iStart,iStop,iFreq: Int64;
    filen: array[0..49] of RawUTF8;
    guid: TGUID;
procedure start(const Text: string);
begin
  if iStart<>0 then begin
    QueryPerformanceCounter(iStop);
    iStart := ((iStop-iStart)*(1000*10)) div iFreq;
    writeln('  ',iStart div 10,'.',iStart mod 10,' ms');
  end else
    writeln;
  if Text='' then begin
    iStart := 0;
    exit;
  end;
  write(Text);
  QueryPerformanceCounter(iStart);
end;
function Delete(Pack,withString: boolean): boolean;
var i,j,nu,id,tot: integer;
    St: TStream;
    tmp: TIntegerDynArray;
    ok: boolean;
begin
  result := false;
  SetLength(Deleted,Random(100)+10);
  fillchar(Deleted[0],length(Deleted)*4,0);
  start('Deleting '+IntToString(length(Deleted))+' elements');
  for i := 0 to high(Deleted) do
  repeat
    repeat
      id := Random(n)+1;
    until (T.IDToIndex(id)>=0) and
      (IntegerScanIndex(pointer(Deleted),length(Deleted),id)<0); // no dup
    Deleted[i] := id;
    ok := T.Delete(id);
  until ok;
  for i := 0 to high(Deleted) do
    if FastFindIntegerSorted(pointer(T.fDeleted),T.fDeletedCount-1,Deleted[i])<0 then
      exit;
  start('Iteration test');
  tot := 0;
  T.GetIterating(T.TestIterateEvent,ioPhysical,@tot);
   if tot<>T.Count then
    exit;
  if Pack then begin
    start('Packing');
    T.Pack; // delete data from disk
  end else begin
    start('Close');
    T.Free; // verify after read than fDelete[] are saved
    start('Open');
    if withString then
      TS := TSynBigTableString.Create(FN) else
      T := TSynBigTable.Create(FN);
  end;
  start('Verify');
  for i := 0 to high(Deleted) do
    if T.Get(Deleted[i],Data) then
      exit;
  if Pack then begin
    for i := 1 to n do
      if IntegerScanIndex(pointer(Deleted),length(Deleted),i)<0 then
      if not T.Get(i,Data) or not TestString(i,Data) then
        exit else
      if withString then
      if not TS.Get(Int32ToUTF8(i),Data) or not TestString(i,Data) then
        exit;
  end else
    for i := 1 to n do
      if T.Get(i,Data) and not TestString(i,Data) then
        exit else
      if withString then
      if TS.Get(Int32ToUTF8(i),Data) and not TestString(i,Data) then
        exit;
  nu := Random(100)+10;
  start('Updating '+IntToString(nu)+' elements');
  for i := 0 to nu-1 do
  repeat
    repeat
      id := Random(n)+1;
    until (T.IDToIndex(id)>=0) and
      (FastFindIntegerSorted(pointer(Updated),UpdatedCount-1,id)<0); // no dup
    AddSortedInteger(Updated,UpdatedCount,id);
    T.Get(id,Data);
    UniqueString(AnsiString(Data));
    for j := 0 to (length(Data) shr 2)-1 do
      inc(PIntegerArray(Data)^[j],$01010101);
    if not TestString(id,Data) then
      exit;
//    if withString then
//      ok := TS.Update(Data,TS.IDToString(id)) else
    ok := T.Update(id,Data)<>0;
    if ok then
      if not T.Get(id,Data2) or (Data<>Data2) then
        exit;
  until ok;
  start('Verify');
  for i := 1 to n do
    if T.Get(i,Data) and not TestString(i,Data) then
      exit;
  start('AsStream');
  for i := 1 to n do begin
    St := T.GetAsStream(i);
    if (St<>nil) and not TestStream(i,St) then
      exit;
  end;
  start('Iteration test ID order');
  tot := 0;
  T.GetIterating(T.TestIterateEvent,ioID,@tot);
  if tot<>T.Count then
    exit;
  start('Iteration test physical order');
  tot := 0;
  T.GetIterating(T.TestIterateEvent,ioPhysical,@tot);
  if tot<>T.Count then
    exit;
  start('Iteration speed');
  T.GetIterating(T.TestIterateEventSpeed);
  start('ID[] speed');
  for i := 0 to T.Count-1 do
    if T.NumericalID[i]=0 then
      exit;
  start('GetAllIDs physical order');
  if T.GetAllIDs(tmp,ioPhysical)<>T.Count then
    exit;
  start('GetAllIDs ID order');
  if T.GetAllIDs(tmp,ioID)<>T.Count then
    exit;
  start('GetAllPhysicalIndexes');
  if T.GetAllPhysicalIndexes(tmp)<>T.Count then
    exit;
  result := true;
end;
var s,s2: RawByteString;
    st: string;
    nstr: string;
    By8: array of RawUTF8;
    tot: integer;
    tmp: TIntegerDynArray;
    rec: TSynTableData;
    p: pointer;
function stats(T: TSynBigTable): string;
begin
  result := Format('Values: %s, file size: %s',
    [KB(T.Offset[T.fCount-1]),KB(T.fReadBuffer.FileSize)]);
end;
function DoFieldTest(DoRecord: boolean): boolean;
var T: TSynBigTableTable;
    TRec: TSynBigTableRecord absolute T;
    TMeta: TSynBigTableMetaData absolute T;
function TRTest(TR: TSynBigTableTable): boolean;
var i: integer;
    {$ifndef LVCL}
    vari: variant;
    rec: TSynTableData;
    {$endif}
    fText, fInt: TSynTableFieldProperties;
    ID: TIntegerDynArray;
    IDCount: integer;
begin
  result := false;
  fText := TR.Table['Text'];
  fInt := TR.Table['INT'];
  {$ifndef LVCL}
  Start('Read as variant');
  for i := 1 to n do begin
    vari := TR.VariantGet(i);
    if (vari.ID<>i) or
       (vari.text<>By8[n-i]) or
       (vari.Int<>i-1) then
      exit;
  end;
  Start('Read as TSynTableData');
  for i := 1 to n do begin
    TR.RecordGet(i,rec);
    if (rec.GetFieldValue(fText)<>string(By8[n-i])) or
       (integer(rec.GetFieldValue(fInt))<>i-1) then
      exit;
  end;
  {$endif}
  Start('Read direct');
  for i := 1 to n do begin
    if DoRecord then
      p := TRec.GetPointer(i,s) else
      p := TMeta.GetMetaDataFromID(i);
    if (p=nil) or
       (fInt.GetInteger(p)<>i-1) or
       (fText.GetRawUTF8(p)<>By8[n-i]) then
      exit;
  end;
  Start('Search 50 Text iterating');
  IDCount := 0; // not mandatory (if ID=nil, TR.Search will do it)
  for i := n-50 to n-1 do
    if not TR.Search(fText,fText.SBF(By8[n-i]),ID,IDCount,0,ioID) or
      (IDCount<>1) or (ID[0]<>i) then
        exit else
        IDCount := 0;
  Start('Search 200 Text using index');
  for i := n-200 to n-1 do
    if not TR.Search(fText,fText.SBF(By8[n-i]),ID,IDCount) or
      (IDCount<>1) or (ID[0]<>i) then
        exit else
        IDCount := 0;
  Start('Search 50 Int iterating');
  for i := n-50 to n-1 do
    if not TR.Search(fInt,fInt.SBF(i),ID,IDCount,0,ioID) or
      (IDCount<>1) or (ID[0]<>i+1) then
        exit else
        IDCount := 0;
  Start('Search 200  Int using index');
  for i := n-200 to n-1 do
    if not TR.Search(fInt,fInt.SBF(i),ID,IDCount) or
      (IDCount<>1) or (ID[0]<>i+1) then
        exit else
        IDCount := 0;
  Result := true;
end;
var fText, fInt, fBool: TSynTableFieldProperties;
    i, nint, value: integer;
    ClassName: string;
    IDs: TIntegerDynArray;
    IDCount: integer;
begin
  result := false;
  if DoRecord then
    ClassName := TSynBigTableRecord.ClassName else
    ClassName := TSynBigTableMetaData.ClassName;
  FN := ChangeFileExt(paramstr(0),'.'+copy(ClassName,13,100));
  DeleteFile(FN);
  start(format(#13#10'Create a %s database',[ClassName]));
  if DoRecord then
    T := TSynBigTableRecord.Create(FN,'test') else
    T := TSynBigTableMetaData.Create(FN,'test');
  try
    T.AddField('text',tftWinAnsi,[tfoIndex]);
    T.AddField('Int',tftInt32,[tfoIndex,tfoUnique]);
    T.AddFieldUpdate;
    fText := T.Table['text']; // need to get it after AddFieldUpdate
    fInt := T.Table['int'];
    rec.Init(T.Table);
    start(format('Add %s records',[nstr]));
    for i := 0 to n-1 do begin
      rec.SetFieldSBFValue(fText,fText.SBF(By8[n-i-1])); // same as rec.Field['text'] :=
      rec.SetFieldSBFValue(fInt,fInt.SBF(i));
      if DoRecord and (TRec.RecordAdd(rec)<>i+1) then
        exit else
      if not DoRecord and (TMeta.RecordAdd(By8[n-i-1],rec)<>i+1) then
        exit;
    end;
    if tfoUnique in fInt.Options then begin
      start(format('Try to add %d records with not unique field',[n shr 3]));
      for i := 0 to (n shr 3)-1 do begin
        rec.SetFieldSBFValue(fInt,fInt.SBF(i shl 3));
        if DoRecord and (TRec.RecordAdd(rec)<>0) then
          exit else
        if not DoRecord and (TMeta.RecordAdd(By8[n-i],rec)<>0) then
          exit;
      end;
    end;
    if not TRTest(T) then exit;
    start('UpdateToFile');
    T.UpdateToFile;
    start('Close');
    T.Free;
    start('Open');
    if DoRecord then
      T := TSynBigTableRecord.Create(FN,'test') else
      T := TSynBigTableMetaData.Create(FN,'test');
    if not TRTest(T) then exit;
    start('Add a field');
    T.AddField('bool',tftBoolean);
    start('Recreate file with new field layout');
    T.AddFieldUpdate;
    if not TRTest(T) then exit;
    start(format('%d record content update',[n shr 3]));
    fBool := T.Table['bool']; // need to get it after any AddFieldUpdate
    for i := 0 to (n shr 3)-1 do begin
      //write(i:10,'        '#13);
      rec := T.RecordGet(i shl 3+1);
      if rec.ID<>i shl 3+1 then exit;
      rec.SetFieldSBFValue(fBool,fBool.SBF(true));
      if not T.RecordUpdate(rec) then exit;
      if not fBool.GetBoolean(Pointer(rec.SBF)) then exit;
    end;
    start('Packing');
    T.Pack;
    fText := T.Table['text'];
    if (fText=nil) or not TRTest(T) then exit;
    start(format('%d record text field content update',[n shr 5]));
    for i := 0 to (n shr 5)-1 do begin
      id := i shl 5+1;
      rec := T.RecordGet(id);
      if rec.ID<>id then exit;
      By8[n-id] := Int32ToUTF8(id); // as expected by TRTest above
      rec.SetFieldSBFValue(fText,fText.SBF(By8[n-id]));
      if not T.RecordUpdate(rec) then exit;
      if fText.GetRawUTF8(Pointer(rec.SBF))<>By8[n-id] then exit;
    end;
    fInt := T.Table['int'];
    if (fInt=nil) or not TRTest(T) then exit;
    nInt := 500; // unique field update is slow by design, need to refresh index
    start(format('%d record unique Int field content update',[nint]));
    for i := 0 to nint-1 do begin
      id := i shl 3+1;
      rec := T.RecordGet(id);
      if rec.ID<>id then exit;
      rec.SetFieldSBFValue(fInt,fInt.SBF(n*2+id)); // > n -> so unique value
      if not T.RecordUpdate(rec) then exit;
      if fInt.GetInteger(pointer(rec.SBF))<>n*2+id then exit;
    end;
    start(format('Test unique Int index after update for %s records',[nstr]));
    for i := 0 to n-1 do begin
      if (i and 7=0) and (i<nint shl 3) then
        Value := n*2+i+1 else
        Value := i;
      IDCount := 0;
      if not T.Search(fInt,fInt.SBF(Value),IDs,IDCount) or
        (IDCount<>1) or (IDs[0]<>i+1) then
{      if not T.Search(fInt,fInt.SBF(Value),IDs,IDCount,0,ioPhysical) or
        (IDCount<>1) or (IDs[0]<>i+1) then }
          exit;
    end;
    start(stats(T));
    result := true;
  finally
    T.Free;
  end;
end;
begin
  result := false;
  iStart := 0;
  QueryPerformanceFrequency(iFreq);
  Randomize;
  //RandSeed := 51672226;
  //writeln('RandSeed=',RandSeed);
  {$ifndef LVCL}
    {$ifdef ISDELPHIXE2}FormatSettings.{$endif}
      ThousandSeparator := ','; // to avoid buggy characters on console
  {$endif}
  n := 100000;
  nstr := format('%.0n',[n*1.0]);
  start(format(#13#10'Create %s records data in memory',[nstr]));
  SetLength(By8,n);
  for i := 1 to n do
    SetS(By8[i-1],i);
  if true then
  if not DoFieldTest(false) then // TSynBigTableMetaData test
    exit else
  if not DoFieldTest(true) then  // TSynBigTableRecord test
    exit;
{  start(format('Create %s records data in memory',[nstr]));
  for i := 1 to n do SetS(By8[i-1],i);}
  FN := ChangeFileExt(paramstr(0),'.syn1');
  DeleteFile(FN);
  if true then begin
    TS := TSynBigTableString.Create(FN,0);
    try
      SetLength(s,8);
      Start(format(
        #13#10'Store %s records of 8 chars key / 8 chars values',[nstr]));
      for i := 0 to n-1 do begin
        if TS.Add(By8[i],By8[i])<>i+1 then
          exit;
      end;
      Start('Read');
      for i := 1 to n do
        if not TS.Get(i,s2) then
          exit;
      start('UpdateToFile');
      TS.UpdateToFile;
      start('Close');
      TS.Free;
      start('Open');
      TS := TSynBigTableString.Create(FN);
      start('Verify');
      for i := 1 to n do
        if not TS.Get(i,s2) then
          exit;
      start('Iteration test in physical order');
      tot := 0;
      TS.GetIterating(TS.TestIterateEvent,ioPhysical,@tot);
      if tot<>TS.Count then
        exit;
      start('Iteration test in ID order');
      tot := 0;
      TS.GetIterating(TS.TestIterateEvent,ioID,@tot);
      if tot<>TS.Count then
        exit;
      start('Iteration speed');
      TS.GetIterating(T.TestIterateEventSpeed);
      start('ID[] speed');
      for i := 0 to TS.Count-1 do
        if TS.NumericalID[i]=0 then
          exit;
      start('GetAllIDs physical order');
      if TS.GetAllIDs(tmp,ioPhysical)<>T.Count then
        exit;
      start('GetAllIDs ID order');
      if TS.GetAllIDs(tmp,ioID)<>T.Count then
        exit;
      start('GetAllIDs faster order');
      if TS.GetAllIDs(tmp,ioFaster)<>T.Count then
        exit;
      start('GetAllPhysicalIndexes');
      if TS.GetAllPhysicalIndexes(tmp)<>T.Count then
        exit;
      start(stats(TS));
    finally
      TS.Free;
    end;
  end;
//  RandSeed := 1000;
  start('');
  Finalize(By8);
  n := Random(50)*50+1500;
  UpdatedCount := 0;
  FN := ChangeFileExt(paramstr(0),'.syn');
  DeleteFile(FN);
  start('Creating a TSynBigTable with '+IntToString(n)+' elements');
  T := TSynBigTable.Create(FN);
  if true then
  try
    for i := 1 to n do
      if T.Add(TTestBigTable.CreateString(i))<>i then
        exit else
      if T.CurrentInMemoryDataSize>10 shl 20 then // write on disk every 10 MB
        T.UpdateToFile;
    if T.Count<>n then
      exit;
    start('Verify');
    for i := 1 to n do
      if not T.Get(i,Data) or not TestString(i,Data) then
        exit;
    start('Close and Open');
    T.Free;
    T := TSynBigTable.Create(FN); // verify after read
    start('Verify Random');
    for i := 1 to n do begin
      id := Random(n)+1; // test random access speed
      if not T.Get(id,Data) or not TestString(id,Data) then
        exit;
    end;
    start('Verify');
    for i := 1 to n do
      if not T.Get(i,Data) or not TestString(i,Data) then
        exit;
    start('Adding 1000 elements');
    for i := n+1 to n+1000 do // create some fInMemory[] data before Pack
      if T.Add(TTestBigTable.CreateString(i))<>i then
        exit;
    start('Updating 1000 elements');
    for i := n+1000 downto n+1 do // update fInMemory[] in-place data
      if T.Update(i,TTestBigTable.CreateString(i))<>i then
        exit;
    inc(n,1000);
    if not Delete(true,false) then
      exit;
    start('Close and Open');
    T.Free;
    T := TSynBigTable.Create(FN); // verify after read
    start('Verify');
    for i := 1 to n do
      if IntegerScanIndex(pointer(Deleted),length(Deleted),i)<0 then
      if not T.Get(i,Data) or not TestString(i,Data) then
      if not T.Get(i,Data) or not TestString(i,Data) then
        exit;
    if not Delete(false,false) then
      exit;
    start('Packing');
    T.Pack; // delete data from disk
    start('Iteration test physical order');
    tot := 0;
    T.GetIterating(T.TestIterateEvent,ioPhysical,@tot);
    if tot<>T.Count then
      exit;
    start('Iteration test ID order');
    tot := 0;
    T.GetIterating(T.TestIterateEvent,ioID,@tot);
    if tot<>T.Count then
      exit;
    start('Iteration speed');
    T.GetIterating(T.TestIterateEventSpeed);
    start('ID[] speed');
    for i := 0 to T.Count-1 do
      if T.NumericalID[i]=0 then
        exit;
    start(stats(T));
  finally
    T.Free;
  end;
  start('');
  dec(n,1000);
  FN := FN+'2';
  UpdatedCount := 0;
  DeleteFile(FN);
  TS := TSynBigTableString.Create(FN);
  try
    start('Creating a GUID-indexed TSynBigTableString with '+IntToString(Length(FileN))+' * 4MB elements');
    for i := 0 to high(FileN) do
      repeat // avoid duplicates (Add=0 if FileN[i] was already there)
        guid.D1 := Random(maxInt);
        guid.D2 := Random(65535);
        guid.D3 := Random(65535);
        Int64(guid.D4) := Int64(Random(maxInt))*Random(maxInt);
        FileN[i] := GUIDToRawUTF8(guid);
      until TS.Add(TTestBigTable.CreateString(i+1,1 shl 20),FileN[i])<>0; // 4 MB each entry
    start('Verify');
    for i := high(FileN) downto 0 do
      if not TS.Get(FileN[i],data) or not TestString(i+1,Data) then
        exit;
    start('Close');
    TS.Free;
    start('Open');
    TS := TSynBigTableString.Create(FN);
    start('Verify');
    for i := 0 to high(FileN) do
      if not TS.Get(FileN[i],data) or not TestString(i+1,Data) then
        exit;
    start('Delete');
    id := Random(length(FileN));
    TS.Delete(FileN[id]);
    start('Close and Open');
    TS.Free;
    TS := TSynBigTableString.Create(FN);
    start('Pack');
    TS.Pack;
    start('Add one & UpdateToFile');
    TS.Add(TTestBigTable.CreateString(777,2 shl 20),FileN[id]);
    TS.UpdateToFile;
    start('Verify');
    for i := 0 to high(FileN) do
      if i<>id then
        if not TS.Get(FileN[i],data) or not TestString(i+1,Data) then
          exit;
    if not TS.Get(FileN[id],data) or not TestString(777,Data) then
      exit;
    st := stats(T);
    start('Clear');
    TS.Clear;
    UpdatedCount := 0;
    start(st+#13#10#10'Creating a string-indexed TSynBigTableString with '+
      IntToString(n)+' elements');
    for i := 1 to n do
      if TS.Add(TTestBigTable.CreateString(i),Int32ToUTF8(i))<>i then
        exit else
      if TS.CurrentInMemoryDataSize>10 shl 20 then // write on disk every 10 MB
        TS.UpdateToFile;
    if TS.Count<>n then
      exit;
    start('Verify');
    for i := 1 to n do
      if not TS.Get(i,Data) or not TestString(i,Data) or
         not TS.Get(Int32ToUTF8(i),Data) or not TestString(i,Data) then
        exit;
    start('Close and Open');
    TS.Free;
    TS := TSynBigTableString.Create(FN); // verify after read
    start('Verify');
    for i := 1 to n do
      if not TS.Get(i,Data) or not TestString(i,Data) or
         not TS.Get(Int32ToUTF8(i),Data) or not TestString(i,Data) then
        exit;
    start('Verify Random');
    for i := 1 to n do begin
      id := Random(n)+1; // test random access
      if not TS.Get(id,Data) or not TestString(id,Data) or
         not TS.Get(Int32ToUTF8(id),Data) or not TestString(id,Data) then
        exit;
    end;
    start('Adding 1000 elements');
    for i := n+1 to n+1000 do // create some fInMemory[] data before Pack
      if TS.Add(TTestBigTable.CreateString(i),Int32ToUTF8(i))<>i then
        exit;
    inc(n,1000);
    start('Verify');
    for i := 1 to n do
      if not TS.Get(i,Data) or not TestString(i,Data) or
         not TS.Get(Int32ToUTF8(i),Data) or not TestString(i,Data) then
        exit;
    if not Delete(true,true) then
      exit;
    start('Close and Open');
    TS.Free;
    TS := TSynBigTableString.Create(FN); // verify after read
    if not Delete(false,true) then
      exit;
    start(stats(T));
    writeln;
    result := true;
  finally
    TS.Free;
  end;
end;
{$endif FPC}


{ TSynBigTable }

function TSynBigTable.Add(const aData: RawByteString; ForcedID: integer=0;
   PhysicalIndex: PInteger=nil; OldPhysicalIndex: integer=-1): integer;
var n: integer;
begin
  if (self=nil) or (fFile=0) then begin
    result := 0; // mark error (mostly file format mismatch)
    exit;
  end;
{$ifdef THREADSAFE}
  fLock.BeginWrite;
  try
{$endif}
    n := length(fInMemoryData);
    //assert(length(fInMemoryID)=n);
    if fInMemoryCount>=n then begin // reserve enough memory
      inc(n,128+n shr 3);
      SetLength(fInMemoryData,n);
      SetLength(fInMemoryID,n);
    end;
    if ForcedID<>0 then
      if ForcedID<fCurrentID then begin
        result := 0; // invalid decreased ID -> abort
        exit;
      end else
      fCurrentID := ForcedID else
      inc(fCurrentID); // create increasing IDs -> can use fast binary search
    result := fCurrentID;
    fModified := true; // mark need to be saved
    fInMemoryID[fInMemoryCount] := fCurrentID;
    fInMemoryData[fInMemoryCount] := aData;
    if PhysicalIndex<>nil then
      PhysicalIndex^ := fInMemoryCount+fCount;
    inc(fInMemoryCount);
    inc(fCurrentInMemoryDataSize,PtrUInt(length(aData)));
    if fCurrentInMemoryDataSize>BIGTABLE_AUTOFLUSH_SIZE then
      UpdateToFile; // force flush the in-memory data to disk if > 256 MB
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;

function TSynBigTable.AddFile(const aFileName: TFileName): integer;
var tmp: RawByteString;
begin
  tmp := StringFromFile(aFileName);
  if tmp<>'' then
    result := Add(tmp) else
    result := 0;
end;

procedure TSynBigTable.Clear;
begin
{$ifdef THREADSAFE}
   fLock.BeginWrite;
   try
{$endif}
    fReadBuffer.Close;
    if fFile<>0 then
      FileClose(fFile);
    DeleteFile(fFileName);
    LoadFromFile; // will reset all internal data
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;

constructor TSynBigTable.Create(const aFileName: TFileName; FileOpenMode: Cardinal=0);
var magic: Integer;
begin
{$ifdef THREADSAFE}
  fLock := TMultiReadExclusiveWriteSynchronizer.Create;
{$endif}
  fFileOpenMode := FileOpenMode;
  fFileName := aFileName;
  magic := InternalMagic;
  if ((magic=integer($ABAB0006)) and (PPointer(self)^<>pointer(TSynBigTable))) or
     ((magic=integer($ABAB0007)) and (PPointer(self)^<>pointer(TSynBigTableString))) or
     ((magic=integer($A5A50001)) and (PPointer(self)^<>pointer(TSynBigTableRecord))) or
     ((magic=integer($A5A50002)) and (PPointer(self)^<>pointer(TSynBigTableMetaData))) then
    raise Exception.CreateFmt('%s shall have its own InternalMagic',[ClassName]);
  LoadFromFile;
end;

function TSynBigTable.CustomHeader(Action: TSynBigTableCustomHeader;
  WriteBuffer: TFileBufferWriter): integer;
begin
  result := 0;
end;

function TSynBigTable.Delete(aID: integer; PhysicalIndex: PInteger): boolean;
var i,n,ndx: integer;
begin
  result := false;
  if (self=nil) or (aID<=0) or (fFile=0) then
    exit;
{$ifdef THREADSAFE}
  fLock.BeginWrite;
  try
{$endif}
    if IntegerScan(pointer(fAliasReal),fAliasCount-1,aID)<>nil then
      exit; // do NOT delete an updated alias, which is an hidden value
    ndx := IDToIndex(aID,true);
    if ndx<0 then // aID not found
      exit;
    if PhysicalIndex<>nil then
      PhysicalIndex^ := ndx;
    if AddSortedInteger(fDeleted,fDeletedCount,aID)<0 then
      exit; // already deleted
    result := true;
    fModified := true; // mark need to save file index
    i := FastFindIntegerSorted(pointer(fAliasSource),fAliasCount-1,aID);
    if i>=0 then begin
      AddSortedInteger(fDeleted,fDeletedCount,fAliasReal[i]);
      n := fAliasCount;
      DeleteInteger(fAliasSource,n,i);
      DeleteInteger(fAliasReal,fAliasCount,i);
      if fAliasRealSorted<>nil then
        SetLength(fAliasRealSorted,0);
    end;
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;

destructor TSynBigTable.Destroy;
begin
  UpdateToFile(false,true);
  fReadBuffer.Close;
  if fFile<>0 then
    FileClose(fFile);
{$ifdef THREADSAFE}
  fLock.Free;
{$endif}
  inherited;
end;

function TSynBigTable.Get(aID: integer; out aData: RawByteString): boolean;
begin
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
    // aData := ''; already done by caller (because of "out aData" definition)
    if (self=nil) or (fFile=0) or (aID<=0) or
       ((fDeletedCount>0) and (aID>=fDeleted[0]) and
        (FastFindIntegerSorted(pointer(fDeleted),fDeletedCount-1,aID)>=0)) then
      result := false else
      result := GetFromIndex(IDToIndex(aID),aData);
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

function TSynBigTable.Get(aIDFirst, aIDLast: integer;
  out aData: TRawByteStringDynArray): boolean;
var aID: integer;
begin
  result := true; // OK by default
  if aIDLast<aIDFirst then // avoid GPF
    Finalize(aData) else begin
    SetLength(aData,aIDLast-aIDFirst+1);
    for aID := aIDFirst to aIDLast do
      if not Get(aID,aData[aID-aIDFirst]) then
        result := false; // not found -> leave aData[]='' and return false
  end;
end;

type
  PIterateGetDynArrayIntegerOpaque = ^TIterateGetDynArrayIntegerOpaque;
  TIterateGetDynArrayIntegerOpaque = record
    Values: PIntegerDynArray;
    Count: Integer;
  end;

function TSynBigTable.GetAllIDs(var IDs: TIntegerDynArray;
  Order: TSynBigTableIterationOrder=ioPhysical): integer;
var tmp: TIterateGetDynArrayIntegerOpaque;
begin
  tmp.Values := @IDs;
  tmp.Count := 0;
  GetIterating(IterateGetAllID,Order,@tmp,true);
  result := tmp.Count;
end;

function TSynBigTable.GetAllPhysicalIndexes(var Indexes: TIntegerDynArray): integer;
var tmp: TIterateGetDynArrayIntegerOpaque;
begin
  tmp.Values := @Indexes;
  tmp.Count := 0;
  SetLength(Indexes,Count);
  GetIterating(nil,ioInternalPhysical,@tmp,true);
  result := tmp.Count;
  assert(result=length(Indexes));
end;

function TSynBigTable.GetAsStream(aID: integer): TStream;
var aIndex: integer;
    OffsetBegin: Int64;
begin
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
    if (self=nil) or (fFile=0) or (aID<=0) or
       (FastFindIntegerSorted(pointer(fDeleted),fDeletedCount-1,aID)>=0) then
      result := nil else begin
      aIndex := IDToIndex(aID);
      if aIndex<0 then
        result := nil else
      if aIndex<fCount then begin
        // retrieve from file
        OffsetBegin := Offset[aIndex-1];
        fReadBuffer.Seek(OffsetBegin);
        result := fReadBuffer.ReadStream(Offset[aIndex]-OffsetBegin);
      end else
        // retrieve from data not already written to disk
        result := TSynMemoryStream.Create(fInMemoryData[aIndex-fCount]);
    end;
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

function TSynBigTable.GetCount: integer;
begin
  if (self=nil) or (fFile=0) then // avoid GPF
    result := 0 else begin
{$ifdef THREADSAFE}
    fLock.BeginRead;
    try
{$endif}
      result := fCount+fInMemoryCount-fDeletedCount-fAliasCount;
{$ifdef THREADSAFE}
    finally
      fLock.EndRead;
    end;
{$endif}
  end;
end;

function TSynBigTable.GetFromIndex(aIndex: integer;
  out aData: RawByteString): boolean;
var Len: integer;
    OffsetBegin: Int64;
begin
  if aIndex<0 then
    result := false else begin
    if aIndex<fCount then begin
      // retrieve from file
      OffsetBegin := Offset[aIndex-1];
      fReadBuffer.Seek(OffsetBegin);
      Len := Offset[aIndex]-OffsetBegin;
      SetLength(aData,Len);
      fReadBuffer.Read(pointer(aData),Len);
    end else
      // retrieve from data not already written to disk
      aData := fInMemoryData[aIndex-fCount];
    result := true;
  end;
end;

function TSynBigTable.GetID(Index: integer): integer;
var tmp: integer;
begin
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
   if (self<>nil) and (cardinal(Index)<cardinal(fCount+fInMemoryCount-fDeletedCount-fAliasCount)) then begin
     if Index<fCount then
       result := fID[Index] else
       result := fInMemoryID[Index-fCount];
     if ((fDeletedCount=0) or (fDeleted[0]>result)) and
        ((fAliasCount=0) or (fAliasSource[0]>result)) then
       exit; // best case: no deleted nor updated item before this index
     tmp := Index;
     GetIterating(nil,ioInternalID,@tmp); // will loop through all IDs till reached tmp=0
     if tmp>index then begin
       result := tmp;
       exit;
     end;
   end;
   result := 0;
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

procedure TSynBigTable.GetIterating(aCallBack: TSynBigTableIterateEvent;
  Order: TSynBigTableIterationOrder=ioPhysical; Opaque: pointer=nil;
  DontRetrieveData: Boolean=false);
type TStopper = (none,deleted,updated,alias);
var i,j, n, aID, index: integer;
    stopID: Cardinal;
    DataLen: integer;
    aTempData: RawByteString;
    stopper, S: TStopper;
    next: array[deleted..alias] of integer;
    stop: array[deleted..alias] of cardinal;
    GetID: PInteger absolute Opaque;
    Physical: PIterateGetDynArrayIntegerOpaque absolute Opaque;
    p: pointer; // alf: to circumvent FPC issues
label CallBack;
begin
  if self=nil then
    exit;
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
    n := fCount+fInMemoryCount;
    FillChar(next,sizeof(next),0);
    stopID := 0;
    stopper := none;
    j := 0;
    if fAliasCount>0 then
      if fAliasRealSorted=nil then
        CopyAndSortInteger(Pointer(fAliasReal),fAliasCount,fAliasRealSorted);
    while j<n do begin
      // retrieve the next stopper item
      if stopID<=Cardinal(fCurrentID) then begin
        fillchar(stop,sizeof(stop),255); // set all stop[]>cardinal(maxInt);
        if next[deleted]<fDeletedCount then
          stop[deleted] := fDeleted[next[deleted]];
        if (Order<>ioInternalID) and (next[updated]<fAliasCount) then
          stop[updated] := fAliasSource[next[updated]];
        if (Order<>ioInternalPhysical) and (next[alias]<fAliasCount) then
          stop[alias] := fAliasRealSorted[next[alias]];
        stopID := maxInt; // no more stopper
        for S := deleted to alias do
          if stop[S]<stopID then begin
            stopID := stop[S];
            stopper := S;
          end;
      end;
      // process data until reached stopper
      case Order of
      ioInternalID:
      for i := j to n-1 do begin
        j := i+1;
        if i<fCount then
          aID := fID[i] else
          aID := fInMemoryID[i-fCount];
        if aID=integer(stopID) then begin
          inc(next[stopper]); // find next fDeleted[] or fAliasReal[]
          break; // we reached StopID -> get next udpated or deleted ID
        end else
        if GetID^>0 then
          dec(GetID^) else begin
          GetID^ := aID; // reached index -> return corresponding ID
          exit;
        end;
      end;
      ioInternalPhysical:
      if stopID=cardinal(maxInt) then begin
        // no more stopper -> fastest possible method
        dec(n,j);
        FillIncreasing(@Physical^.Values^[Physical^.Count],j,n);
        inc(Physical^.Count,n);
        exit;
      end else
      for i := j to n-1 do begin
        j := i+1;
        if i<fCount then
          aID := fID[i] else
          aID := fInMemoryID[i-fCount];
        if aID=integer(stopID) then begin
          inc(next[stopper]); // find next fDeleted[] or fAliasSource[]
          break; // we reached StopID -> get next udpated or deleted ID
        end else begin
          Physical^.Values^[Physical^.Count] := i; // SetLength(Values^,Count) was made
          inc(Physical^.Count);
        end;
      end;
      else
      if Assigned(aCallBack) then
      for i := j to n-1 do begin
        j := i+1;
        if i<fCount then
          aID := fID[i] else
          aID := fInMemoryID[i-fCount];
        if aID=integer(stopID) then begin
          case stopper of
          alias: // ID: ignore alias, physical: callback expects original ID
            if Order=ioPhysical then begin
              aID := fAliasSource[IntegerScanIndex(pointer(fAliasReal),fAliasCount,aID)];
              if DontRetrieveData then begin
                if not aCallBack(self,Opaque,aID,0,nil,0) then
                  exit; // forced iteration break
              end else begin
                index := i;
                goto CallBack;
              end;
            end;
          updated:
            // physical: ignore update, ID: callback expects updated data
            if Order=ioID then
            if DontRetrieveData then begin
              if not aCallBack(self,Opaque,aID,0,nil,0) then
                exit; // forced iteration break
            end else begin
              index := IDToIndex(fAliasReal[next[updated]],false);
              if index>=0 then begin
CallBack:       p := GetPointerFromIndex(index,aTempData,PInteger(@DataLen));
                if not aCallBack(self,Opaque,aID,index,p,DataLen) then
                  exit; // forced iteration break
              end;
              end;
            end;
          inc(next[stopper]); // find next fDeleted[] or fAlias*[]
          break; // we reached StopID -> get next udpated or deleted ID
        end else
        if DontRetrieveData then begin
          if not aCallBack(self,Opaque,aID,0,nil,0) then
            exit; // forced iteration break
        end else
        begin
          p := GetPointerFromIndex(i,aTempData,PInteger(@DataLen));
          if not aCallBack(self,Opaque,aID,i,p,DataLen) then
            exit; // forced iteration break
      end;
      end;
      end; // case Order of
    end;
    assert(j=n);
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

function TSynBigTable.GetLength(aID: integer): integer;
var i: integer;
begin
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
    if (self=nil) or (fFile=0) or (aID<=0) or
       (FastFindIntegerSorted(pointer(fDeleted),fDeletedCount-1,aID)>=0) then
      result := -1 else begin
      i := IDToIndex(aID);
      if i<0 then
        result := -1 else begin
        if i<fCount then
          // retrieve from file
          result := Offset[i]-Offset[i-1] else
          // retrieve from data not already written to disk
          result := length(fInMemoryData[i-fCount]);
      end;
    end;
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

function TSynBigTable.GetOffset(Index: integer): Int64;
begin
  if (Self=nil) or (cardinal(Index)>=cardinal(fCount)) then
    result := 0 else // avoid GPF
    if fOffset64<>nil then
      result := fOffset64[Index] else
      result := PtrUInt(fOffset32[Index]);
end;

function TSynBigTable.GetPart(aID, Offset, Len: Integer;
  out aData: RawByteString): boolean;
var i: integer;
    OffsetBegin: Int64;
    FileLen: integer;
begin // note: caching should not be used here
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
    result := false;
    if (self=nil) or (fFile=0) or (aID<=0) or
       (FastFindIntegerSorted(pointer(fDeleted),fDeletedCount-1,aID)>=0) then
      exit;
    i := IDToIndex(aID);
    if i<0 then
      exit;
    if i<fCount then begin
      // retrieve from file
      OffsetBegin := GetOffset(i-1);
      FileLen := Int64(GetOffset(i)-OffsetBegin);
      if Offset>=FileLen then
        exit;
      inc(OffsetBegin,PtrUInt(Offset));
      fReadBuffer.Seek(OffsetBegin);
      if Offset+Len>FileLen then
        Len := FileLen-Offset; // truncate to real data size
      SetLength(aData,Len);
      fReadBuffer.Read(pointer(aData),Len);
    end else begin
      // retrieve from data not already written to disk
      FileLen := length(fInMemoryData[i-fCount]);
      if Offset>=FileLen then
        exit;
      if Offset+Len>FileLen then
        Len := FileLen-Offset; // truncate to real data size
      aData := Copy(fInMemoryData[i-fCount],Offset+1,Len);
    end;
    result := true;
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

function TSynBigTable.GetPointer(aID: Integer;
  var aTempData: RawByteString; DataLen: PInteger): pointer;
begin
{$ifdef THREADSAFE}
  fLock.BeginRead;
  try
{$endif}
    if (self=nil) or (fFile=0) or (aID<=0) or
       (FastFindIntegerSorted(pointer(fDeleted),fDeletedCount-1,aID)>=0) then
      result := nil else
      result := GetPointerFromIndex(IDToIndex(aID),aTempData,DataLen);
{$ifdef THREADSAFE}
  finally
    fLock.EndRead;
  end;
{$endif}
end;

function TSynBigTable.GetPointerFromIndex(aIndex: Integer;
  var aTempData: RawByteString; DataLen: PInteger): pointer;
var OffsetBegin: Int64;
    len: PtrInt;
begin
  if cardinal(aIndex)<cardinal(fCount) then begin
    if fOffset64<>nil then begin
      if aIndex=0 then
        OffsetBegin := 0 else
        OffsetBegin := fOffset64[aIndex-1];
      fReadBuffer.Seek(OffsetBegin);
      len := Int64(fOffset64[aIndex]-OffsetBegin);
    end else begin
      if aIndex=0 then
        len := 0 else
        len := fOffset32[aIndex-1];
      fReadBuffer.Seek(len);
      len := fOffset32[aIndex]-len;
    end;
    result := fReadBuffer.ReadPointer(len,aTempData);
    if DataLen<>nil then
      DataLen^ := len;
  end else begin
    dec(aIndex,fCount);
    if cardinal(aIndex)<cardinal(fInMemoryCount) then begin
      result := pointer(fInMemoryData[aIndex]);
      if DataLen<>nil then
        DataLen^ := length(AnsiString(result));
    end else
      result := nil;
  end;
end;

function TSynBigTable.GetPointerFromPhysicalIndex(aPhysicalIndex: integer;
  var aTempData: RawByteString): pointer;
var OffsetBegin: Int64;
    Offs32: PtrInt;
begin
  if cardinal(aPhysicalIndex)<cardinal(fCount) then
  if fOffset64<>nil then begin
    if aPhysicalIndex=0 then
      OffsetBegin := 0 else
      OffsetBegin := fOffset64[aPhysicalIndex-1];
    fReadBuffer.Seek(OffsetBegin);
    result := fReadBuffer.ReadPointer(Int64(fOffset64[aPhysicalIndex]-OffsetBegin),aTempData);
  end else begin
    if aPhysicalIndex=0 then
      Offs32 := 0 else
      Offs32 := fOffset32[aPhysicalIndex-1];
    fReadBuffer.Seek(Offs32);
    result := fReadBuffer.ReadPointer(fOffset32[aPhysicalIndex]-Offs32,aTempData);
  end else begin
    dec(aPhysicalIndex,fCount);
    if cardinal(aPhysicalIndex)<cardinal(fInMemoryCount) then
      result := pointer(fInMemoryData[aPhysicalIndex]) else
      result := nil;
  end;
end;

function TSynBigTable.IDToIndex(aID: integer; handleAliases: boolean=true): integer;
begin
  if (Self=nil) or (aID<=0) or (aID>fCurrentID) then
    result := -1 else begin
    if handleAliases and (fAliasCount>0) then begin
      result := FastFindIntegerSorted(pointer(fAliasSource),fAliasCount-1,aID);
      if result>=0 then
        aID := fAliasReal[result]; // get updated ID
    end;
    result := FastFindIntegerSorted(pointer(fInMemoryID),fInMemoryCount-1,aID);
    if result<0 then
      result := FastFindIntegerSorted(pointer(fID),fCount-1,aID) else
      inc(result,fCount);
  end;
end;

function TSynBigTable.IndexToID(aIndex: Integer; handleAliases: boolean): integer;
begin
  result := 0;
  if (self=nil) or (cardinal(aIndex)>=cardinal(fCount+fInMemoryCount)) then
    exit;
  if aIndex<fCount then
    result := fID[aIndex] else
    result := fInMemoryID[aIndex-fCount];
  if not handleAliases then
    exit;
  aIndex := IntegerScanIndex(pointer(fAliasReal),fAliasCount,result);
  if aIndex>=0 then
    result := fAliasSource[aIndex];
end;

function TSynBigTable.InternalIDToIndex(aID: integer;
  var aAliasIndex: integer): integer;
begin
  if (self=nil) or (aID>fCurrentID) or (aID<=0) or
     (FastFindIntegerSorted(pointer(fDeleted),fDeletedCount-1,aID)>=0) then
    result := -1 else begin
    aAliasIndex := FastFindIntegerSorted(pointer(fAliasSource),fAliasCount-1,aID);
    if aAliasIndex>=0 then
      result := IDToIndex(fAliasReal[aAliasIndex],false) else
      result := IDToIndex(aID,false);
  end;
end;

class function TSynBigTable.InternalMagic: integer;
begin
  result := integer($ABAB0006);
end;

function TSynBigTable.InternalUpdateFromIndex(const aData: RawByteString;
  aID, aIndex, aAliasIndex: integer; var aNewIndex: cardinal): integer;
var i: integer;
begin
  assert(aIndex>=0);
  if aIndex>=fCount then begin
    // the data is still in-memory -> refresh in place (much faster)
    fInMemoryData[aIndex-fCount] := aData;
    fModified := true;
    aNewIndex := aIndex;
    result := aID;
  end else begin
    // data on disk -> create new invisible record with supplied data
    result := Add(aData,0,@aNewIndex,aIndex);
    if result>0 then
      if aAliasIndex>=0 then begin
        // already updated -> delete previous content, and assign new one
        AddSortedInteger(fDeleted,fDeletedCount,fAliasReal[aAliasIndex]);
        fAliasReal[aAliasIndex] := result;
        if fAliasRealSorted<>nil then
          SetLength(fAliasRealSorted,0);
      end else begin
        // add alias to this new content
        i := AddSortedInteger(fAliasSource,fAliasCount,aID,@fAliasReal);
        assert(i>=0); // we already checked not in fAliasSource[]
        fAliasReal[i] := result;
        if fAliasRealSorted<>nil then
          SetLength(fAliasRealSorted,0);
      end;
  end;
end;

class function TSynBigTable.FileFormatCheck(const aFileName: TFileName): boolean;
var magic: integer;
begin
  try
    with TFileStream.Create(aFileName,fmOpenRead or fmShareDenyNone) do
    try // same exact layout as in TSynBigTable.LoadFromFile
      result := (Seek(-4,soEnd)>0) and (Read(magic,4)=4) and
        (magic=InternalMagic);
    finally
      Free;
    end;
  except
    on Exception do
      result := false;
  end;
end;

function TSynBigTable.IterateGetAllID(Sender: TObject; Opaque: pointer;
  ID, Index: integer; Data: pointer; DataLen: integer): boolean;
var IDs: PIterateGetDynArrayIntegerOpaque absolute Opaque;
begin
  AddInteger(IDs.Values^,IDs.Count,ID);
  result := true;
end;

function TSynBigTable.IteratePack(Sender: TObject; Opaque: pointer; ID,
  Index: integer; Data: pointer; DataLen: integer): boolean;
var Added: PUpdateFieldEvent absolute Opaque;
begin
  with Added^ do begin
    result := Count<length(IDs);
    if result then begin
      WR.Write(Data,DataLen);
      Offsets64[Count] := WR.TotalWritten;
      IDs[Count] := ID;
      NewIndexs[Index] := Count;
      inc(Count);
    end;
  end;
end;

procedure TSynBigTable.LoadFromFile;
var magic: integer;
    CustomHeaderSize: integer;
    UseOffset64: boolean;
begin
  // reinitialize all internal structures
  if pointer(fInMemoryData)<>nil then
    Finalize(fInMemoryData);
  if fAliasRealSorted<>nil then
    SetLength(fAliasRealSorted,0);
  if fOffset64<>nil then
    SetLength(fOffset64,0);
  if fOffset32<>nil then
    SetLength(fOffset32,0);
  fCurrentID := 0;
  fInMemoryCount := 0;
  fCurrentInMemoryDataSize := 0;
  fCount := 0;
  fAliasCount := 0;
  fDeletedCount := 0;
  // open existing file
  fFile := FileOpen(fFileName,fmOpenReadWrite or fFileOpenMode);
  if (fFile<>INVALID_HANDLE_VALUE) and (FileSeek64(fFile,0,soFromEnd)<16) then begin
    FileClose(fFile); // size if not big enough -> force create new
    fFile := INVALID_HANDLE_VALUE;
  end;
  if fFile=INVALID_HANDLE_VALUE then begin
    fFile := FileCreate(fFileName);
    if (fFileOpenMode<>0) and (fFile<>INVALID_HANDLE_VALUE) then begin
      FileClose(fFile);
      fFile := FileOpen(fFileName,fmOpenReadWrite or fFileOpenMode);
    end;
    CustomHeader(sbtAfterRead,nil); // refresh any internal index e.g.
    exit; // no data yet
  end;
  // load headers from existing file
  fReadBuffer.Open(fFile);
  magic := 0;
  if fReadBuffer.Seek(fReadBuffer.FileSize-8) then begin
    fReadBuffer.Read(@CustomHeaderSize,4);
    fReadBuffer.Read(@magic,4);
  end;
  if (magic<>InternalMagic) or (CustomHeaderSize<=0) then
    CustomHeaderSize := 0 else begin
    if not fReadBuffer.Seek(fReadBuffer.FileSize-CustomHeaderSize) then
      CustomHeaderSize := 0 else begin
      fCount := fReadBuffer.ReadVarUInt32;
      fDeletedCount := fReadBuffer.ReadVarUInt32;
      fAliasCount := fReadBuffer.ReadVarUInt32;
      if (fCount<0) or (PtrUInt(fDeletedCount)>PtrUInt(fCount)) or
         (PtrUInt(fAliasCount)>PtrUInt(fCount)) then
        CustomHeaderSize := 0;
    end;
  end;
  if CustomHeaderSize>0 then begin // valid file content
    SetLength(fID,fCount);
    SetLength(fDeleted,fDeletedCount);
    CustomHeader(sbtRead,nil);
    if fCount=0 then begin
      CustomHeader(sbtAfterRead,nil); // refresh any internal index e.g.
      exit;
    end;
    with fReadBuffer do begin
      fCurrentID := ReadVarUInt32;
      if (fCurrentID<=0) or
         (ReadVarUInt32Array(fID)<>fCount) or
         (ReadVarUInt32Array(fDeleted)<>fDeletedCount) or
         (ReadVarUInt32Array(fAliasSource)<>fAliasCount) or
         (ReadVarUInt32Array(fAliasReal)<>fAliasCount) then
        CustomHeaderSize := 0 else begin
        Read(@UseOffset64,1);
        if (UseOffset64 and (ReadVarUInt64Array(fOffset64)<>fCount)) or
           (not UseOffset64 and (ReadVarUInt32Array(fOffset32)<>fCount)) then
          CustomHeaderSize := 0 else // invalid file content
          CustomHeader(sbtAfterRead,nil); // refresh any internal index e.g.
      end;
    end;
  end;
  if CustomHeaderSize>0 then
    exit;
  fReadBuffer.Close;
  FileClose(fFile);
  fFile := 0; // most class methods will check this value and fail
  fCount := 0;
  fDeletedCount := 0;
  fAliasCount := 0;
end;

procedure TSynBigTable.Pack(forceFlushOnDisk: boolean=false);
var i, ndx, Di,Si,Ni,nCount,oneBuf,firstDel,nNewIndexs: integer;
    S,D,Diff,L,N: Int64;
    buf: RawByteString;
    index, NewIndexs: TIntegerDynArray;
begin
  if (self=nil) or ((fDeletedCount=0) and (fAliasCount=0) and (fInMemoryCount=0)) or
     (fCount=0) or (fFile=0) then
    exit; // nothing to pack
{$ifdef THREADSAFE}
  fLock.BeginWrite;
  try
{$endif}
    if fAliasRealSorted<>nil then
      SetLength(fAliasRealSorted,0);
    if fAliasCount=0 then begin
      // 1. if no updated item, we can pack the file in-place (faster)
      if Assigned(OnAfterPack) then begin
        nNewIndexs := fCount+fInMemoryCount;
        SetLength(NewIndexs,nNewIndexs);
        FillIncreasing(pointer(NewIndexs),0,nNewIndexs);
      end;
      fReadBuffer.Close; // unmap all reading files
      // mark all deleted IDs as fID[]=0 - pack is faster if done first
      Setlength(index,fDeletedCount);
      Di := maxInt;
      firstDel := -1;
      for i := 0 to fDeletedCount-1 do
      if fDeleted[i]>0 then begin
        ndx := FastFindIntegerSorted(pointer(fID),fCount-1,fDeleted[i]);
        if ndx>=0 then begin // ignore fInMemory*[] blocks
          index[i] := ndx; // no direct fID[] := 0 because of fID is used above
          if ndx<Di then
            Di := ndx;
        end else begin
          firstDel := i; // first fDeleted[] index in fInMemory*[]
          break;
        end;
      end else
        assert(false);
      if firstDel<0 then // no deleted fInMemory*[] block -> no more delete
        fDeletedCount := 0 else begin
        dec(fDeletedCount,firstDel); // keep only fDeleted[] for fInMemory*[] blocks
        Move(fDeleted[firstDel],fDeleted[0],fDeletedCount*4);
      end;
      // update fID[] with deleted items
      for i := 0 to high(index) do
      if index[i]>0 then // index[i]=0 for fInMemory*[] deleted blocks
        fID[index[i]] := 0;
      // save packed file in-place (it's faster than with 2 files)
      if Di<maxInt then begin
        SetLength(buf,32*1024*1024); // 32 MB buffer should be big enough
        D := Offset[Di-1];
        nCount := Di;
        repeat
          Si := Di+1;
          while (Si<fCount) and (fID[Si]=0) do inc(Si); // Si = next not deleted entry
          if Si=fCount then
            break; // deleted till the end
          S := Offset[Si-1];
          Ni := Si+1;
          while (Ni<fCount) and (fID[Ni]<>0) do inc(Ni); // Ni= next deleted entry
          N := Offset[Ni-1]; // if no more deleted entry, Ni=fCount
          L := N-S;
          repeat // move whole not deleted data block
            OneBuf := length(buf);
            if L<OneBuf then
              OneBuf := L;
            FileSeek64(fFile,S,soFromBeginning);
            FileRead(fFile,pointer(buf)^,OneBuf);
            FileSeek64(fFile,D,soFromBeginning);
            FileWrite(fFile,pointer(buf)^,OneBuf);
            dec(L,OneBuf);
            inc(S,OneBuf);
            inc(D,OneBuf);
          until L=0;
          Diff := S-D; // whole deleted size
          for i := Si to Ni-1 do begin // add all not deleted blocks
            if fOffset64<>nil then
              fOffset64[nCount] := fOffset64[i]-Diff else
              fOffset32[nCount] := fOffset32[i]-PtrInt(Diff);
            if NewIndexs<>nil then
              NewIndexs[i] := nCount;
            fID[nCount] := fID[i];
            inc(nCount);
    //        assert(fID[i]<>0);
          end;
    //      assert(D=FileSeek64(fFile,0,soFromCurrent));
    //      assert(fOffset[nCount-1]=D);
          Di := Ni;
        until Di=fCount;
        fCount := nCount;
      end;
      if Assigned(OnAfterPack) then
        OnAfterPack(NewIndexs);
      fModified := true; // force save to file
      UpdateToFile(forceFlushOnDisk);
    end else begin
      // 2. some items were updated -> we'll need two files
      RecreateFileContent(IteratePack); // will do all the magic
    end;
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;

procedure TSynBigTable.RecreateFileContent(aCallBack: TSynBigTableIterateEvent;
  const OpaqueFields: TSQLFieldBits=[]);
var n: integer;
    aFileName: TFileName;
    Opaque: TUpdateFieldEvent;
begin
  if (self=nil) or not Assigned(aCallBack) then
    exit;
  n := Count;
  aFileName := fFileName+'.~new';
  DeleteFile(aFileName);
  // prepare opaque structure
  FillChar(Opaque,sizeof(Opaque),0);
  Opaque.AvailableFields := OpaqueFields;
  Opaque.WR := TFileBufferWriter.Create(aFileName,1 shl 24); // write per 16 MB blocks
  try
    // create new data
    SetLength(Opaque.IDs,n);
    SetLength(Opaque.Offsets64,n);
    SetLength(Opaque.NewIndexs,fCount+fInMemoryCount); // NewIndexs[old] := new
    GetIterating(aCallBack,ioID,@Opaque); // will do the work
    assert(Opaque.Count=n);
    Opaque.WR.Flush;
    // close previous file
    fReadBuffer.Close;
    FileClose(fFile);
    fFile := 0;
    // update internal structures with new file content
    if Assigned(OnAfterPack) then
      OnAfterPack(Opaque.NewIndexs); // not existing -> created by LoadFromFile
    fCount := n;
    fCurrentID := Opaque.IDs[n-1];
    fInMemoryCount := 0;
    fAliasCount := 0;
    fDeletedCount := 0;
    SetLength(fID,n);
    move(Opaque.IDs[0],fID[0],n*4);
    if (n>0) and (Opaque.Offsets64[n-1]>1 shl 30) then begin
      SetLength(fOffset64,n);
      SetLength(fOffset32,0);
      move(Opaque.Offsets64[0],fOffset64[0],n*8);
    end else begin
      SetLength(fOffset64,0);
      SetLength(fOffset32,n);
      Int64ToUInt32(pointer(Opaque.Offsets64),pointer(fOffset32),n);
    end;
    // add headers to the new file
    if not DeleteFile(fFileName) then begin
      Sleep(1000);
      DeleteFile(fFileName);
    end;
    fFile := THandleStream(Opaque.WR.Stream).Handle; // write on the new file
    fModified := true;
    UpdateToFile(true,true); // flush + don't reopen fReadBuffer now
  finally
    Opaque.WR.Free;
  end;
  // use the new file
  MoveFile(Pointer(aFileName),Pointer(fFileName));
  LoadFromFile;
end;

function TSynBigTable.TestIterateEvent(Sender: TObject; Opaque: pointer;
  ID, Index: integer; Data: pointer; DataLen: integer): boolean;
var len: integer;
    aTempData: RawByteString;
begin // in some cases, Data can be nil (some data string='')
  result := true;
  if (Sender<>self) or not Get(ID,aTempData) or (Opaque=nil) then
    exit; // use Get() for testing GetPointer() returned value
  len := length(aTempData);
  if (len<>DataLen) or ((len<>0) and not CompareMem(pointer(aTempData),Data,len)) then
  if (len<>DataLen) or not Get(ID,aTempData) or
    ((len<>0) and not CompareMem(pointer(aTempData),Data,len)) then
    exit; // slow but proven comparison
  inc(PInteger(Opaque)^); // will fail test if not reached this line
end;

function TSynBigTable.TestIterateEventSpeed(Sender: TObject; Opaque: pointer;
  ID, Index: integer; Data: pointer; DataLen: integer): boolean;
begin
  result := true; // void call-back to test GetIterating() speed
end;

function TSynBigTable.Update(aID: Integer; const aData: RawByteString;
  PhysicalIndexOldNew: PInt64=nil): integer;
var ndx, aliasNdx: integer;
    OldNew: Int64Rec; // Lo=Old Hi=New
begin
{$ifdef THREADSAFE}
  fLock.BeginWrite;
  try
{$endif}
    ndx := InternalIDToIndex(aID,aliasNdx);
    if ndx<0 then // invalid ID
      result := 0 else begin
      result := InternalUpdateFromIndex(aData,aID,ndx,aliasNdx,OldNew.Hi);
      if PhysicalIndexOldNew<>nil then begin
        OldNew.Lo := ndx; // Lo=Old
        PhysicalIndexOldNew^ := Int64(OldNew);
      end;
    end;
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;

procedure TSynBigTable.UpdateToFile(forceFlushOnDisk: boolean=false;
  dontReopenReadBuffer: boolean=false);
var i,delNdx: integer;
    magic: integer;
    Offs: Int64;
    UseOffset64: boolean;
    Len: PtrUInt;
    WriteBuffer: TFileBufferWriter;
begin
  if (fFile=0) or not fModified then
    exit; // nothing to write
{$ifdef THREADSAFE}
  fLock.BeginWrite;
  try
{$endif}
    // prepare the internal structures for writing
    CustomHeader(sbtBeforeWrite,nil); // refresh any internal index e.g.
    // now update file content
    fReadBuffer.Close;
    Offs := Offset[fCount-1];
    FileSeek64(fFile,Offs,soFromBeginning);
    WriteBuffer := TFileBufferWriter.Create(fFile);
    try
      // append new blocks of data, and update fOffset*[] and fID[]
      if fInMemoryCount>0 then begin // fInMemoryCount=-1 from Pack call
        if Offs+CurrentInMemoryDataSize>1 shl 30 then
          // global file size will be > 1 GB -> use fOffset64[]
          if fOffset64=nil then begin
            SetLength(fOffset64,fCount+fInMemoryCount);
            for i := 0 to fCount-1 do
              fOffset64[i] := PtrUInt(fOffset32[i]);
            SetLength(fOffset32,0);
          end else
            SetLength(fOffset64,fCount+fInMemoryCount) else
          // total size < 1 GB -> less memory and faster with fOffset32[]
          SetLength(fOffset32,fCount+fInMemoryCount);
        SetLength(fID,fCount+fInMemoryCount);
        for i := 0 to fInMemoryCount-1 do begin
          delNdx := FastFindIntegerSorted(
            pointer(fDeleted),fDeletedCount-1,fInMemoryID[i]);
          if delNdx>=0 then
            DeleteInteger(fDeleted,fDeletedCount,delNdx) else begin
            Len := length(fInMemoryData[i]);
            WriteBuffer.Write(Pointer(fInMemoryData[i]),Len);
            inc(Offs,Len);
            if fOffset64<>nil then
              fOffset64[fCount] := Offs else
              fOffset32[fCount] := Offs;
            fID[fCount] := fInMemoryID[i];
            inc(fCount);
          end;
        end;
      end;
      // reset fInMemory*[] content
      Finalize(fInMemoryData);
      Finalize(fInMemoryID);
      fInMemoryCount := 0;
      fCurrentInMemoryDataSize := 0;
      fModified := false;
      // rewrite header
      Offs := WriteBuffer.TotalWritten;
      magic := InternalMagic;
      WriteBuffer.WriteVarUInt32(fCount);
      WriteBuffer.WriteVarUInt32(fDeletedCount);
      WriteBuffer.WriteVarUInt32(fAliasCount);
      CustomHeader(sbtWrite,WriteBuffer);
      WriteBuffer.WriteVarUInt32(fCurrentID);
      WriteBuffer.WriteVarUInt32Array(fID,fCount,wkSorted);
      WriteBuffer.WriteVarUInt32Array(fDeleted,fDeletedCount,wkSorted);
      WriteBuffer.WriteVarUInt32Array(fAliasSource,fAliasCount,wkSorted);
      WriteBuffer.WriteVarUInt32Array(fAliasReal,fAliasCount,wkVarUInt32);
      UseOffset64 := pointer(fOffset64)<>nil;
      WriteBuffer.Write(@UseOffset64,1);
      if UseOffset64 then
        WriteBuffer.WriteVarUInt64DynArray(fOffset64,fCount,true) else
        WriteBuffer.WriteVarUInt32Array(fOffset32,fCount,wkOffsetU);
      Offs := WriteBuffer.TotalWritten-Offs+8;
      //assert(Offs<maxInt);
      WriteBuffer.Write(@Offs,4);
      WriteBuffer.Write(@magic,4);
      WriteBuffer.Flush;
    finally
      WriteBuffer.Free;
    end;
    SetEndOfFile(fFile); // always force truncate at end of header
    if forceFlushOnDisk then
      FlushFileBuffers(fFile);
    if not dontReopenReadBuffer then
      fReadBuffer.Open(fFile);
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;

function TSynBigTable.GetFileSizeOnDisk: Int64;
begin
  result := 0;
  if fFile=0 then
   exit;
{$ifdef THREADSAFE}
  fLock.BeginWrite;
  try
{$endif}
    PInt64Rec(@result)^.Lo := GetFileSize(fFile,@PInt64Rec(@result)^.Hi);
{$ifdef THREADSAFE}
  finally
    fLock.EndWrite;
  end;
{$endif}
end;


{ TSynBigTableString }

function TSynBigTableString.Add(const aData: RawByteString; const aID: RawUTF8;
  ForcedID: integer=0): integer;
var i: integer;
begin
  if (aID='') or (self=nil) then
    result := 0 else begin
    i := FastLocatePUTF8CharSorted(pointer(fHeaderString),fHeaderCount-1,pointer(aID));
    if i<0 then // aID already exists -> fails
      result := 0 else begin
      result := inherited Add(aData,ForcedID);
      if result>0 then begin
        AddSortedRawUTF8(fHeaderString,fHeaderCount,aID,@fHeaderID,i);
        fHeaderID[i] := result;
      end;
    end;
  end;
end;

procedure TSynBigTableString.Clear;
begin
  inherited Clear;
  Finalize(fHeaderString[0],fHeaderCount); // force set all String ID to ''
  fHeaderCount := 0;
end;

function TSynBigTableString.CustomHeader(Action: TSynBigTableCustomHeader;
  WriteBuffer: TFileBufferWriter): integer;
begin
  result := 0; // make compiler happy
  case Action of
    sbtWrite: begin
      WriteBuffer.WriteVarUInt32Array(fHeaderID,fHeaderCount,wkVarUInt32);
      WriteBuffer.WriteRawUTF8DynArray(fHeaderString,fHeaderCount);
    end;
    sbtRead: begin
      Finalize(fHeaderString); // force set all String ID to ''
      fHeaderCount := fReadBuffer.ReadVarUInt32Array(fHeaderID);
      if (fHeaderCount<>fCount-fDeletedCount-fAliasCount) or
         (fHeaderCount<>fReadBuffer.ReadVarRawUTF8DynArray(fHeaderString)) then
        fReadBuffer.ErrorInvalidContent;
    end;
  end;
end;

function TSynBigTableString.Delete(aID: integer; PhysicalIndex: PInteger): boolean;
var ndx: integer;
begin
  result := inherited Delete(aID);
  if not result then
    exit;
  ndx := IntegerScanIndex(pointer(fHeaderID),fHeaderCount,aID);
  if ndx>=0 then
    result := DeleteRawUTF8(fHeaderString,fHeaderCount,ndx,@fHeaderID);
end;

function TSynBigTableString.Delete(const aID: RawUTF8): boolean;
var ID: integer;
begin
  ID := StringToID(aID);
  if ID=0 then
    result := false else
    result := Delete(ID);
end;

function TSynBigTableString.Get(const aID: RawUTF8; out aData: RawByteString): boolean;
var ID: integer;
begin
  ID := StringToID(aID);
  if ID=0 then
    result := false else
    result := Get(ID,aData);
end;

function TSynBigTableString.GetAllIDs(var IDs: TIntegerDynArray;
  Order: TSynBigTableIterationOrder): integer;
begin
  case Order of
  ioPhysical:
    // physical order is achieved by GetIterating()
    inherited GetAllIDs(IDs,ioPhysical);
  ioID:
    // faster in-place quick sort than GetIterating() call
    CopyAndSortInteger(pointer(fHeaderID),fHeaderCount,IDs);
  ioFaster: begin
    // no quicksort of IDs (leave string key order)
    if fHeaderCount>length(IDs) then
      SetLength(IDs,fHeaderCount);
    Move(fHeaderID[0],IDs[0],fHeaderCount*sizeof(integer));
  end;
  end;
  result := fHeaderCount;
end;

function TSynBigTableString.GetAsStream(const aID: RawUTF8): TStream;
var ID: integer;
begin
  ID := StringToID(aID);
  if ID=0 then
    result := nil else
    result := GetAsStream(ID);
end;

function TSynBigTableString.GetCount: integer;
begin
  if self=nil then
    result := 0 else
    result := fHeaderCount;
end;

function TSynBigTableString.GetID(Index: integer): integer;
begin
  if (self=nil) or (cardinal(Index)>=cardinal(fHeaderCount)) then
    result := 0 else
    result := fHeaderID[Index];
end;

function TSynBigTableString.GetPointer(const aID: RawUTF8;
  var aTempData: RawByteString; DataLen: PInteger): pointer;
var ID: integer;
begin
  ID := StringToID(aID);
  if ID=0 then
    result := nil else
    result := GetPointer(ID,aTempData,DataLen);
end;

function TSynBigTableString.GetStringID(Index: integer): RawUTF8;
begin
  if (self=nil) or (cardinal(Index)>=cardinal(fHeaderCount)) then
    result := '' else
    result := fHeaderString[Index];
end;

function TSynBigTableString.IDToString(aID: integer): RawUTF8;
begin
  if (aID<=0) or (self=nil) or (fFile=0) or (fHeaderCount=0) then
    result := '' else begin
    aID := IntegerScanIndex(pointer(fHeaderID),fHeaderCount,aID);
    if aID<0 then
      result := '' else
      result := fHeaderString[aID];
  end;
end;

class function TSynBigTableString.InternalMagic: integer;
begin
  result := integer($ABAB0007); // differs from TSynBigTable
end;

function TSynBigTableString.StringToID(const aID: RawUTF8): integer;
begin // use fast binary search
  if (aID='') or (self=nil) or (fFile=0) then
    result := 0 else begin
    result := FastFindPUTF8CharSorted(pointer(fHeaderString),fHeaderCount-1,pointer(aID));
    if result<0 then
      result := 0 else
      result := fHeaderID[result];
  end;
end;

function TSynBigTableString.Update(const aData: RawByteString;
  const aID: RawUTF8): boolean;
var ID: integer;
begin
  ID := StringToID(aID);
  if ID=0 then
    result := false else
    result := Update(ID,aData)>0;
end;


{ TSynBigTableRecord }

function TSynBigTableRecord.Add(const aData: RawByteString; ForcedID: integer;
  PhysicalIndex: PInteger; OldPhysicalIndex: integer): integer;
var ndx: integer;
    aTemp: RawByteString;
    p: pointer;
begin
  if CheckConstraints(aData,OldPhysicalIndex) then begin
    Result := inherited Add(aData,ForcedID,@ndx,OldPhysicalIndex);
    if Result<>0 then begin
      if PhysicalIndex<>nil then
        PhysicalIndex^ := ndx;
      p := GetPointerFromIndex(OldPhysicalIndex,aTemp);
      Table.FieldIndexModify(OldPhysicalIndex,ndx,p,pointer(aData));
    end;
  end else
    Result := 0;
end;

procedure TSynBigTableRecord.AddFieldUpdate;
var F: integer;
    Fields: TSQLFieldBits;
begin
  if (self=nil) or (Table.AddedField=nil) then
    exit; // nothing new
  if Count=0 then begin
    // no data to refresh
    Table.AddedField.Free; // do it once
    Table.AddedField := nil;
  end else begin
    // some data to refresh: guess field added, and process
    fillchar(Fields,sizeof(Fields),0);
    for F := 0 to Table.FieldCount-1 do
      if Table.AddedField.IndexOf(Table.Field[F])<0 then
        Include(Fields,F);
    Table.AddedField.Free; // do it once
    Table.AddedField := nil;
    RecreateFileContent(Table.UpdateFieldEvent,Fields);
  end;
end;

constructor TSynBigTableRecord.Create(const aFileName: TFileName;
  const aTableName: RawUTF8; FileOpenMode: Cardinal);
begin
  inherited Create(aFileName,aTableName,GetPointerFromPhysicalIndex,FileOpenMode);
  OnAfterPack := RefreshIndexes;
end;

function TSynBigTableRecord.CustomHeader(Action: TSynBigTableCustomHeader;
  WriteBuffer: TFileBufferWriter): integer;
var F,n: integer;
    PhysicalIndexes: TIntegerDynArray;
begin
  result := 0; // make compiler happy
  case Action of
    sbtBeforeWrite:
      for F := 0 to Table.FieldCount-1 do
        Table.Field[F].OrderedIndexRefresh;
    sbtWrite:
      if WriteBuffer<>nil then
        Table.SaveTo(WriteBuffer);
    sbtRead: begin
      Table.LoadFrom(fReadBuffer);
      fTableName := Table.TableName;
    end;
    sbtAfterRead: begin
      n := Count;
      if n>0 then
      for F := 0 to Table.FieldCount-1 do
        with Table.Field[F] do
        if tfoIndex in Options then
          if (OrderedIndexCount<>n) or (length(OrderedIndex)<n) then begin
            // this index need to be recreated -> do it now
            OrderedIndexCount := n;
            if PhysicalIndexes=nil then
              GetAllPhysicalIndexes(PhysicalIndexes);
            SetLength(OrderedIndex,n);
            Move(PhysicalIndexes[0],OrderedIndex[0],n*4);
            OrderedIndexUpdate(-1,-1,nil,nil); // both indexes equal -1 -> just sort
            fModified := true;
          end;
    end;
  end;
end;

function TSynBigTableRecord.Delete(aID: integer; PhysicalIndex: PInteger): boolean;
var ndx: integer;
begin
  Result := inherited Delete(aID,@ndx);
  if Result then begin
    if PhysicalIndex<>nil then
      PhysicalIndex^ := ndx;
    Table.FieldIndexModify(ndx,-1,nil,nil);
  end;
end;

class function TSynBigTableRecord.InternalMagic: integer;
begin
  result := integer($A5A50001); // genuine ID which differs from others
end;

function TSynBigTableRecord.RecordAdd(const aRecord: TSynTableData; aForcedID: integer): integer;
begin
  if aRecord.SBF='' then
    result := 0 else
    result := Add(aRecord.SBF,aForcedID);
end;

procedure TSynBigTableRecord.RecordGet(aID: integer; var result: TSynTableData);
var Temp: RawByteString;
    RecordBufLen: integer;
    p: pointer;
begin
  p := GetPointer(aID,Temp,@RecordBufLen);
  result.Init(Table,aID,p,RecordBufLen);
end;

function TSynBigTableRecord.RecordUpdate(const aRecord: TSynTableData): boolean;
begin
  result := Update(aRecord.ID,aRecord.SBF)<>0;
end;

procedure TSynBigTableRecord.RefreshIndexes(var NewIndexs: TIntegerDynArray);
var n, i, F: integer;
begin
  for n := 0 to high(NewIndexs) do
    if NewIndexs[n]<>n then begin
      // at least one physical index changed -> update OrderedIndex[] array
      for F := 0 to Table.FieldCount-1 do
        with Table.Field[F] do
        if OrderedIndex<>nil then
          for i := 0 to OrderedIndexCount-1 do
            OrderedIndex[i] := NewIndexs[OrderedIndex[i]];
      exit;
    end;
end;

procedure TSynBigTableRecord.SearchIterating(aCallBack: TSynBigTableIterateEvent;
  Order: TSynBigTableIterationOrder; Opaque: pointer);
begin
  GetIterating(aCallBack,Order,Opaque);
end;

function TSynBigTableRecord.Update(aID: Integer;
  const aData: RawByteString; PhysicalIndexOldNew: PInt64): integer;
var newNdx: cardinal;
    ndx, aliasNdx: integer;
begin
  ndx := InternalIDToIndex(aID,aliasNdx);
  if (ndx<0) or not CheckConstraints(aData,ndx) then // invalid ID or data
    result := 0 else begin
    result := InternalUpdateFromIndex(aData,aID,ndx,aliasNdx,newNdx);
    if ndx>=fCount then // was refreshed in memory -> manual index refresh
      Table.FieldIndexModify(ndx,newNdx,nil,nil);
      // old data was overridden -> no GetPointerFromIndex(ndx,aTemp),pointer(aData));
     // aIndex<fCount -> TSynBigTableRecord.Add() did call FieldIndexModify
  end;
end;

{$ifndef LVCL}
function TSynBigTableRecord.VariantAdd(const aRecord: Variant): integer;
var Data: RawByteString;
begin
  Data := TSynTableVariantType.ToSBF(aRecord);
  if Data='' then
    result := 0 else
    result := Add(Data);
end;

function TSynBigTableRecord.VariantGet(aID: integer): Variant;
var Temp: RawByteString;
    RecordBuf: pointer;
begin
  RecordBuf := GetPointer(aID,Temp);
  if RecordBuf=nil then
    VarClear(result) else
    result := fTable.Data(aID,RecordBuf);
end;

function TSynBigTableRecord.VariantUpdate(const aRecord: Variant): boolean;
begin
  with TSynTableVariantType do
    result := Update(ToID(aRecord),ToSBF(aRecord))<>0;
end;
{$endif LVCL}



{ TTestBigTable }

class function TTestBigTable.CreateString(aID, n: integer): RawByteString;
var i: integer;
begin
  result := '';
  if n=0 then
    n := Random(1000);
  SetLength(result,n*4); // each data block size is up to 4 KB
  for i := 0 to n-1 do
    PIntegerArray(result)^[i] := aID;
end;

function TTestBigTable.Delete(var T: TSynBigTable; n: integer; Pack, withString: boolean): boolean;
var i,j,nu,id,tot: integer;
    St: TStream;
    tmp: TIntegerDynArray;
    ok: boolean;
    Data: RawByteString;
    TS: TSynBigTableString absolute T; // absolute T -> Delete() will work
begin
  result := false;
  SetLength(Deleted,Random(10)+10);
  fillchar(Deleted[0],length(Deleted)*4,0);
  for i := 0 to high(Deleted) do
  repeat
    repeat
      id := Random(n)+1;
    until (T.IDToIndex(id)>=0) and
      (IntegerScanIndex(pointer(Deleted),length(Deleted),id)<0); // no dup
    Deleted[i] := id;
    ok := T.Delete(id);
  until ok;
  for i := 0 to high(Deleted) do
    Check(FastFindIntegerSorted(pointer(T.fDeleted),T.fDeletedCount-1,Deleted[i])>=0);
  tot := 0;
  T.GetIterating(T.TestIterateEvent,ioPhysical,@tot);
  Check(tot=T.Count);
  if Pack then begin
    T.Pack; // delete data from disk
  end else begin
    T.Free; // verify after read than fDelete[] are saved
    if withString then
      TS := TSynBigTableString.Create(FN) else
      T := TSynBigTable.Create(FN);
  end;
  for i := 0 to high(Deleted) do
    if CheckFailed(not T.Get(Deleted[i],Data)) then
      exit;
  if Pack then begin
    for i := 1 to n do
      if IntegerScanIndex(pointer(Deleted),length(Deleted),i)<0 then
      if T.Get(i,Data) then
        if CheckFailed(TestString(i,Data)) then
          exit else
        if withString then
        if CheckFailed(TS.Get(Int32ToUTF8(i),Data)) or CheckFailed(TestString(i,Data)) then
          exit;
  end else
    for i := 1 to n do
      if T.Get(i,Data) THEN
        if CheckFailed(TestString(i,Data)) then
          exit else
        if withString then
        if CheckFailed(TS.Get(Int32ToUTF8(i),Data)) or CheckFailed(TestString(i,Data)) then
          exit;
  nu := Random(100)+10;
  for i := 0 to nu-1 do
  repeat
    repeat
      id := Random(n)+1;
    until (T.IDToIndex(id)>=0) and
      (FastFindIntegerSorted(pointer(Updated),UpdatedCount-1,id)<0); // no dup
    AddSortedInteger(Updated,UpdatedCount,id);
    T.Get(id,Data);
    UniqueString(AnsiString(Data));
    for j := 0 to (length(Data) shr 2)-1 do
      inc(PIntegerArray(Data)^[j],$01010101);
    ok := T.Update(id,Data)<>0;
  until ok;
  for i := 1 to n do
    if T.Get(i,Data) then
      if CheckFailed(TestString(i,Data)) then
        exit;
  for i := 1 to n do begin
    St := T.GetAsStream(i);
    if (St<>nil) and CheckFailed(TestStream(i,St)) then
      exit;
  end;
  tot := 0;
  T.GetIterating(T.TestIterateEvent,ioID,@tot);
  Check(tot=T.Count,'Iteration test ID order');
  tot := 0;
  T.GetIterating(T.TestIterateEvent,ioPhysical,@tot);
  Check(tot=T.Count,'Iteration test physical order');
  for i := 0 to T.Count-1 do
    Check(T.NumericalID[i]<>0);
  Check(T.GetAllIDs(tmp,ioPhysical)=T.Count,'GetAllIDs physical order');
  Check(T.GetAllIDs(tmp,ioID)=T.Count,'GetAllIDs ID order');
  Check(T.GetAllPhysicalIndexes(tmp)=T.Count,'GetAllPhysicalIndexes');
  result := true;
end;

function TTestBigTable.DoFieldTest(DoRecord: boolean; n: integer): boolean;
var T: TSynBigTableTable;
    TRec: TSynBigTableRecord absolute T;
    TMeta: TSynBigTableMetaData absolute T;
    By8: TRawUTF8DynArray;
function TRTest: boolean;
var i: integer;
{$ifndef LVCL}
    vari: variant;
    rec: TSynTableData;
{$endif}
    fText, fInt: TSynTableFieldProperties;
    ID: TIntegerDynArray;
    IDCount: integer;
    p: pointer;
    s: RawByteString;
begin
  result := false;
  fText := T.Table['Text'];
  fInt := T.Table['INT'];
{$ifndef LVCL}
  for i := 1 to n do begin
    vari := T.VariantGet(i);
    if CheckFailed(vari.text=By8[n-i]) or CheckFailed(vari.ID=i) or
       CheckFailed(vari.Int=i-1,'Read as Variant') then exit;
  end;
  for i := 1 to n do begin
    T.RecordGet(i,rec);
    if CheckFailed(rec.GetFieldValue(fText)=string(By8[n-i])) or
       CheckFailed(integer(rec.GetFieldValue(fInt))=i-1,'Read as TSynTableData') then
      exit;
  end;
{$endif}
  for i := 1 to n do begin
    if DoRecord then
      p := TRec.GetPointer(i,s) else
      p := TMeta.GetMetaDataFromID(i);
    if CheckFailed(fInt.GetInteger(p)=i-1) or
       CheckFailed(fText.GetRawUTF8(p)=By8[n-i],'Read direct') then
      exit;
  end;
  IDCount := 0; // not mandatory (if ID=nil, T.Search will do it)
  for i := n-50 to n-1 do
    if CheckFailed(T.Search(fText,fText.SBF(By8[n-i]),ID,IDCount,0,ioPhysical)) or
       CheckFailed(IDCount=1) or CheckFailed(ID[0]=i,'Search 50 Text iterating') then
        exit else
        IDCount := 0;
  for i := n-200 to n-1 do
    if CheckFailed(T.Search(fText,fText.SBF(By8[n-i]),ID,IDCount)) or
      CheckFailed(IDCount=1) or CheckFailed(ID[0]=i,'Search 200 Text using index') then
        exit else
        IDCount := 0;
  for i := n-50 to n-1 do
    if CheckFailed(T.Search(fInt,fInt.SBF(i),ID,IDCount,0,ioPhysical)) or
       CheckFailed(IDCount=1) or CheckFailed(ID[0]=i+1,'Search 50 Int iterating') then
        exit else
        IDCount := 0;
  for i := n-200 to n-1 do
    if CheckFailed(T.Search(fInt,fInt.SBF(i),ID,IDCount)) or
      CheckFailed(IDCount=1) or CheckFailed(ID[0]=i+1,'Search 200 Int using index') then
        exit else
        IDCount := 0;
  if not DoRecord then
    for i := 1 to n do
      if CheckFailed(TMeta.Get(i,s)) or CheckFailed(TestString(i,s)) then
        exit;
  Result := true;
end;
var fText, fInt, fBool: TSynTableFieldProperties;
    i, aID, Value: integer;
    IDs: TIntegerDynArray;
    IDCount: integer;
    ClassName: string;
    rec: TSynTableData;
procedure SetS(var s: RawUTF8; i: cardinal);
var p: array[0..3] of Word;
begin
  p[2] := TwoDigitLookupW[(i div 100)mod 100];
  p[3] := TwoDigitLookupW[i mod 100];
  i := i div 10000;
  p[0] := TwoDigitLookupW[i div 100];
  p[1] := TwoDigitLookupW[i mod 100];
  SetString(s,PAnsiChar(@p[0]),8);
end;
const nInt=300; // unique field update is time consumming
begin
  result := false;
  if DoRecord then
    ClassName := TSynBigTableRecord.ClassName else
    ClassName := TSynBigTableMetaData.ClassName;
  FN := ChangeFileExt(FN,'.'+copy(ClassName,13,100));
  DeleteFile(FN);
  SetLength(By8,n);
  UpdatedCount := 0;
  for i := 1 to n do
    SetS(By8[i-1],i);
  if DoRecord then
    T := TSynBigTableRecord.Create(FN,'test') else
    T := TSynBigTableMetaData.Create(FN,'test');
  try
    T.AddField('text',tftWinAnsi,[tfoIndex]);
    T.AddField('Int',tftInt32,[tfoIndex,tfoUnique]);
    T.AddFieldUpdate;
    fText := T.Table['text']; // need to get it after AddFieldUpdate
    fInt := T.Table['int'];
    rec.Init(T.Table);
    for i := 0 to n-1 do begin
      rec.SetFieldSBFValue(fText,fText.SBF(By8[n-i-1]));
      rec.SetFieldSBFValue(fInt,fInt.SBF(i));
      if DoRecord then
        Check(TRec.RecordAdd(rec)=i+1) else
        Check(TMeta.RecordAdd(CreateString(i+1),rec)=i+1);
    end;
    if CheckFailed(TRTest) then exit;
    if tfoUnique in fInt.Options then
      for i := 0 to (n shr 3)-1 do begin
        rec.SetFieldSBFValue(fInt,fInt.SBF(i shl 3));
        if DoRecord then
          Check(TRec.RecordAdd(rec)=0) else
          Check(TMeta.RecordAdd(By8[i],rec)=0);
      end;
    if CheckFailed(TRTest) then exit;
    T.UpdateToFile;
    T.Free;
    if DoRecord then
      T := TSynBigTableRecord.Create(FN,'test') else
      T := TSynBigTableMetaData.Create(FN,'test');
    rec := T.RecordGet(1);
    if checkFailed(rec.ID=1) then exit;
    fInt := T.Table['int'];
    rec.SetFieldSBFValue(fInt,fInt.SBF(0)); // fake update
    if CheckFailed(T.RecordUpdate(rec)) then exit;
    T.UpdateToFile;
    T.Free;
    if DoRecord then
      T := TSynBigTableRecord.Create(FN,'test') else
      T := TSynBigTableMetaData.Create(FN,'test');
    if CheckFailed(TRTest,'after update+reload') then exit;
    Check(T.AddField('bool',tftBoolean));
    T.AddFieldUpdate;
    if CheckFailed(TRTest) then exit;
    fBool := TRec.Table['bool']; // need to get it after any AddFieldUpdate
    Check(fBool<>nil);
    for i := 0 to (n shr 3)-1 do begin
      aID := i shl 3+1;
      rec := T.RecordGet(aID);
      if CheckFailed(rec.ID=aID) then exit;
      if CheckFailed(not fBool.GetBoolean(Pointer(rec.SBF))) then exit;
      rec.SetFieldSBFValue(fBool,fBool.SBF(true));
      if CheckFailed(T.RecordUpdate(rec)) then exit;
      rec := T.RecordGet(aID);
      if CheckFailed(fBool.GetBoolean(Pointer(rec.SBF))) then exit;
    end;
    T.Pack;
    if CheckFailed(TRTest) then exit;
    fInt := T.Table['int'];
    for i := 0 to nInt-1 do begin
      aID := i shl 3+1;
      rec := T.RecordGet(aID);
      if CheckFailed(rec.ID=aID) then exit;
      rec.SetFieldSBFValue(fInt,fInt.SBF(n*2+aID)); // > n -> so unique value
      if CheckFailed(T.RecordUpdate(rec)) then exit;
      if CheckFailed(fInt.GetInteger(pointer(rec.SBF))=n*2+aID) then exit;
    end;
    for i := 0 to nInt-1 do begin
      if (i and 7=0) and (i<(nInt shl 3)) then
        Value := n*2+i+1 else
        Value := i;
      IDCount := 0;
      if CheckFailed(T.Search(fInt,fInt.SBF(Value),IDs,IDCount)) or
         CheckFailed(IDCount=1) or CheckFailed(IDs[0]=i+1) then
          exit;
    end;
    result := true;
  finally
    T.Free;
  end;
  Check(not TSynBigTable.FileFormatCheck(FN));
  Check(not TSynBigTableString.FileFormatCheck(FN));
  if DoRecord then
    Check(TSynBigTableRecord.FileFormatCheck(FN)) else
    Check(TSynBigTableMetaData.FileFormatCheck(FN));
end;

function TTestBigTable.TestStream(aID: integer; Data: TStream): boolean;
var i: integer;
begin
  result := false;
  if not Data.InheritsFrom(TCustomMemoryStream) then
    exit;
  if FastFindIntegerSorted(pointer(Updated),UpdatedCount-1,aID)>=0 then
    inc(aID,$01010101); // an updated ID has had its content refreshed
  with TCustomMemoryStream(Data) do
    for i := 0 to (Size shr 2)-1 do
      if PIntegerArray(Memory)^[i]<>aID then
        exit;
  result := true;
  Data.Free;
end;

function TTestBigTable.TestString(aID: integer; const Data: RawByteString): boolean;
var i: integer;
begin
  result := false;
  if FastFindIntegerSorted(pointer(Updated),UpdatedCount-1,aID)>=0 then
    inc(aID,$01010101); // an updated ID has had its content refreshed
  for i := 0 to (length(Data) shr 2)-1 do
    if PIntegerArray(Data)^[i]<>aID then
      exit;
  result := true;
end;

procedure TTestBigTable._TSynBigTable;
var i,n,id,tot: integer;
    T: TSynBigTable;
    Data: RawByteString;
begin
  n := Random(10)+1000;
  UpdatedCount := 0;
  FN := ChangeFileExt(paramstr(0),'.syn1');
  DeleteFile(FN);
  T := TSynBigTable.Create(FN);
  try
    for i := 1 to n do
      if not CheckFailed(T.Add(CreateString(i))=i) then
      if T.CurrentInMemoryDataSize>10 shl 20 then // write on disk every 10 MB
        T.UpdateToFile;
    Check(T.Count=n);
    for i := 1 to n do
      if CheckFailed(T.Get(i,Data)) or CheckFailed(TestString(i,Data)) then
        exit;
    T.Free;
    T := TSynBigTable.Create(FN); // verify after read
    for i := 1 to n do begin
      id := Random(n)+1; // test random access speed
      if CheckFailed(T.Get(id,Data)) or CheckFailed(TestString(id,Data)) then
        exit;
    end;
    for i := 1 to n do
      if CheckFailed(T.Get(i,Data)) or CheckFailed(TestString(i,Data)) then
        exit;
    for i := n+1 to n+100 do // create some fInMemory[] data before Pack
      Check(T.Add(CreateString(i))=i,'Adding 100 elements');
    for i := n+100 downto n+1 do // update fInMemory[] in-place data
      Check(T.Update(i,CreateString(i))=i,'Updating 100 elements');
    inc(n,100);
    if not Delete(T,n,true,false) then
      exit;
    T.Free;
    T := TSynBigTable.Create(FN); // verify after read
    for i := 1 to n do
      if IntegerScanIndex(pointer(Deleted),length(Deleted),i)<0 then
      if CheckFailed(T.Get(i,Data)) or CheckFailed(TestString(i,Data)) then
        exit;
    Check(Delete(T,n,false,false));
    T.Pack; // delete data from disk
    tot := 0;
    T.GetIterating(T.TestIterateEvent,ioPhysical,@tot);
    Check(tot=T.Count,'Iteration test physical order');
    tot := 0;
    T.GetIterating(T.TestIterateEvent,ioID,@tot);
    Check(tot=T.Count,'Iteration test ID order');
    for i := 0 to T.Count-1 do
      Check(T.NumericalID[i]<>0);
  finally
    T.Free;
  end;
  Check(TSynBigTable.FileFormatCheck(FN));
  Check(not TSynBigTableString.FileFormatCheck(FN));
end;

procedure TTestBigTable._TSynBigTableMetaData;
begin
  Check(DoFieldTest(false,10000));
end;

procedure TTestBigTable._TSynBigTableRecord;
begin
  Check(DoFieldTest(true,10000));
end;

procedure TTestBigTable._TSynBigTableString;
var TS: TSynBigTableString;
    i, n, id: integer;
    Data: RawByteString;
begin
  FN := ChangeFileExt(FN,'.syn2');
  DeleteFile(FN);
  TS := TSynBigTableString.Create(FN);
  try
    UpdatedCount := 0;
    n := Random(10)+500;
    for i := 1 to n do
      if CheckFailed(TS.Add(TTestBigTable.CreateString(i),Int32ToUTF8(i))=i) then
        exit else
      if TS.CurrentInMemoryDataSize>10 shl 20 then // write on disk every 10 MB
        TS.UpdateToFile;
    Check(TS.Count=n);
    for i := 1 to n do
      if CheckFailed(TS.Get(i,Data)) or CheckFailed(TestString(i,Data)) or
         CheckFailed(TS.Get(Int32ToUTF8(i),Data)) or CheckFailed(TestString(i,Data)) then
        exit;
    TS.Free;
    TS := TSynBigTableString.Create(FN); // verify after read
    for i := 1 to n do
      if CheckFailed(TS.Get(i,Data)) or CheckFailed(TestString(i,Data)) or
         CheckFailed(TS.Get(Int32ToUTF8(i),Data)) or CheckFailed(TestString(i,Data)) then
        exit;
    for i := 1 to n do begin
      id := Random(n)+1; // test random access
      if CheckFailed(TS.Get(id,Data)) or CheckFailed(TestString(id,Data)) or
         CheckFailed(TS.Get(Int32ToUTF8(id),Data)) or CheckFailed(TestString(id,Data)) then
        exit;
    end;
    for i := n+1 to n+100 do // create some fInMemory[] data before Pack
      Check(TS.Add(TTestBigTable.CreateString(i),Int32ToUTF8(i))=i);
    inc(n,100);
    for i := 1 to n do
      if CheckFailed(TS.Get(i,Data)) or CheckFailed(TestString(i,Data)) or
         CheckFailed(TS.Get(Int32ToUTF8(i),Data)) or CheckFailed(TestString(i,Data)) then
        exit;
    if CheckFailed(Delete(TSynBigTable(TS),n,true,true)) then
      exit;
    TS.Free;
    TS := TSynBigTableString.Create(FN); // verify after read
    Check(Delete(TSynBigTable(TS),n,false,true));
  finally
    TS.Free;
  end;
  Check(not TSynBigTable.FileFormatCheck(FN));
  Check(TSynBigTableString.FileFormatCheck(FN));
end;


{ TSynBigTableTable }

function TSynBigTableTable.AddField(const aName: RawUTF8; aType: TSynTableFieldType;
  aOptions: TSynTableFieldOptions): boolean;
begin
  if (self=nil) or (Table=nil) then
    result := false else
    if (tfoUnique in aOptions) and (Count>0) then
      raise Exception.CreateFmt(
        'Impossible to create an UNIQUE %s field with existing data',[aName]) else
      result := Table.AddField(aName,aType,aOptions)<>nil;
end;

function TSynBigTableTable.CheckConstraints(const aRecordData: RawByteString;
  RecordIndex: integer): boolean;
begin
  if (self=nil) or (aRecordData='') or (Table=nil) then
    // invalid data (direct Add/Update call with no SBF format)
    result := false else
    result := Table.Validate(pointer(aRecordData),RecordIndex)='';
end;

procedure TSynBigTableTable.Clear;
begin
  if (self<>nil) and (Table<>nil) then
    Table.FieldList.Clear;
  inherited Clear;
end;

constructor TSynBigTableTable.Create(const aFileName: TFileName;
  const aTableName: RawUTF8; GetRecordData: TSynTableGetRecordData;
  FileOpenMode: Cardinal);
begin
  fTable := TSynTable.Create(aTableName);
  fTable.GetRecordData := GetRecordData;
  inherited Create(aFileName,FileOpenMode);
  if aTableName<>'' then begin
    fTableName := aTableName;
    Table.TableName := aTableName;
  end;
end;

destructor TSynBigTableTable.Destroy;
begin
  inherited;
  fTable.Free;
end;

type
  PIterateSearchOpaque = ^TIterateSearchOpaque;
  TIterateSearchOpaque = record
    Values: PIntegerDynArray;
    Count: PInteger;
    Search: pointer;
    Field: TSynTableFieldProperties;
    Limit: Integer;
  end;

function TSynBigTableTable.IterateSearch(Sender: TObject; Opaque: pointer;
  ID, Index: integer; Data: pointer; DataLen: integer): boolean;
begin
  with PIterateSearchOpaque(Opaque)^ do
    if Field.SortCompare(Table.GetData(Data,Field),Search)=0 then begin
      AddSortedInteger(Values^,Count^,ID);
      if Count^>=Limit then begin
        result := false; // we have enough results -> stop iteration
        exit;
      end;
    end;
  result := true;
end;

function TSynBigTableTable.RecordGet(aID: integer): TSynTableData;
begin
  RecordGet(aId,result);
end;

{$ifndef LVCL}
function TSynBigTableTable.Search(Field: TSynTableFieldProperties;
  const WhereValue: variant; var ResultID: TIntegerDynArray;
  var ResultIDCount: integer; Limit: Integer;
  ForceIterate: TSynBigTableIterationOrder): boolean;
begin
  result := Search(Field,Field.SBF(WhereValue),ResultID,ResultIDCount,Limit,ForceIterate);
end;
{$endif}

function TSynBigTableTable.Search(Field: TSynTableFieldProperties;
  const WhereValue: TSBFString; var ResultID: TIntegerDynArray;
  var ResultIDCount: integer; Limit: Integer;
  ForceIterate: TSynBigTableIterationOrder): boolean;
var MatchIndex: TIntegerDynArray;
    MatchCount, i, aID: integer;
    Opaque: TIterateSearchOpaque;
begin
  if ResultID=nil then
    ResultIDCount := 0; // if caller forgot to initialize the count
  if (self=nil) or (Field=nil) or (WhereValue='') then
    result := false else
  if (tfoIndex in Field.Options) and (ForceIterate in [ioNone,ioFaster]) then begin
    MatchCount := 0;
    result := Field.OrderedIndexMatch(pointer(WhereValue),MatchIndex,MatchCount,Limit);
    if not result then
      exit;
    for i := 0 to MatchCount-1 do begin
      aID := IndexToID(MatchIndex[i]);
      if aID>0 then
        AddSortedInteger(ResultID,ResultIDCount,aID);
    end;
  end else begin
    Opaque.Count := @ResultIDCount;
    Opaque.Values := @ResultID;
    Opaque.Field := Field;
    Opaque.Search := pointer(WhereValue);
    if Limit=0 then // no LIMIT set -> retrieve all rows
      Opaque.Limit := maxInt else
      Opaque.Limit := Limit;
    SearchIterating(IterateSearch,ForceIterate,@Opaque);
    result := true;
  end;
end;

{$ifndef LVCL}
function TSynBigTableTable.VariantVoid: Variant;
begin
  if (self=nil) or (fTable=nil) then
    VarClear(result) else
    result := fTable.Data;
end;
{$endif}


{ TSynBigTableMetaData }

function TSynBigTableMetaData.Add(const aData: RawByteString;
  const aMetaData: TSBFString): integer;
var ndx: integer;
begin
  if not CheckConstraints(aMetaData,-1) then
    result := 0 else begin
    result := inherited Add(aData);
    if result>0 then begin
      ndx := fMetaDataCount;
      if ndx>=length(fMetaDataRecords) then begin
        inc(ndx,ndx+256+ndx shr 3);
        SetLength(fMetaDataRecords,ndx);
        SetLength(fMetaDataID,ndx);
        ndx := fMetaDataCount;
      end;
      inc(fMetaDataCount);
      fMetaDataRecords[ndx] := aMetaData;
      fMetaDataID[ndx] := Result;
      Table.FieldIndexModify(-1,ndx,nil,nil);
    end;
  end;
end;

procedure TSynBigTableMetaData.AddFieldUpdate;
var i, F: Integer;
    AvailableFields: TSQLFieldBits;
begin
  if (self=nil) or (Table.AddedField=nil) then
    exit; // nothing new
  if fMetaDataCount=0 then begin // no data to refresh
    Table.AddedField.Free; // do it once
    Table.AddedField:= nil;
    exit;
  end;
  fillchar(AvailableFields,sizeof(AvailableFields),0);
  for F := 0 to Table.FieldCount-1 do
    if Table.AddedField.IndexOf(Table.Field[F])<0 then
      Include(AvailableFields,F);
  Table.AddedField.Free; // do it once
  Table.AddedField := nil;
  for i := 0 to Count-1 do
    fMetaDataRecords[i] := Table.UpdateFieldRecord(pointer(fMetaDataRecords[i]),AvailableFields);
  fModified := true;
end;

constructor TSynBigTableMetaData.Create(const aFileName: TFileName;
  const aTableName: RawUTF8; FileOpenMode: Cardinal);
begin
  inherited Create(aFileName,aTableName,GetMetaData,FileOpenMode);
end;

function TSynBigTableMetaData.CustomHeader(Action: TSynBigTableCustomHeader;
  WriteBuffer: TFileBufferWriter): integer;
var F,n: integer;
begin
  result := 0; // make compiler happy
  case Action of
    sbtBeforeWrite:
      for F := 0 to Table.FieldCount-1 do
        Table.Field[F].OrderedIndexRefresh;
    sbtWrite:
      if WriteBuffer<>nil then begin
        Table.SaveTo(WriteBuffer);
        WriteBuffer.WriteRawUTF8DynArray(TRawUTF8DynArray(fMetaDataRecords),fMetaDataCount);
        WriteBuffer.WriteVarUInt32Array(fMetaDataID,fMetaDataCount,wkSorted);
      end;
    sbtRead: begin
      Table.LoadFrom(fReadBuffer);
      fTableName := Table.TableName;
      fMetaDataCount := fReadBuffer.ReadVarRawUTF8DynArray(TRawUTF8DynArray(fMetaDataRecords));
      if (fMetaDataCount<>inherited GetCount) or
         (fReadBuffer.ReadVarUInt32Array(fMetaDataID)<>fMetaDataCount) then
        fReadBuffer.ErrorInvalidContent;
    end;
    sbtAfterRead: begin
      n := Count;
      if n>0 then
      for F := 0 to Table.FieldCount-1 do
        with Table.Field[F] do
        if tfoIndex in Options then
          if (OrderedIndexCount<>n) or (length(OrderedIndex)<n) then begin
            // this index need to be recreated -> do it now
            OrderedIndexCount := n;
            SetLength(OrderedIndex,n);
            FillIncreasing(pointer(OrderedIndex),0,n);
            OrderedIndexUpdate(-1,-1,nil,nil); // both indexes equal -1 -> just sort
            fModified := true;
          end;
    end;
  end;
end;

function TSynBigTableMetaData.Delete(aID: integer; PhysicalIndex: PInteger): boolean;
var ndx: integer;
begin
  ndx := FastFindIntegerSorted(pointer(fMetaDataID),fMetaDataCount-1,aID);
  if ndx<0 then
    Result := false else begin
    Result := inherited Delete(aID);
    if Result then begin
      DeleteInteger(fMetaDataID,fMetaDataCount,ndx);
      DeleteRawUTF8(TRawUTF8DynArray(fMetaDataRecords),fMetaDataCount,ndx);
      Table.FieldIndexModify(ndx,-1,nil,nil);
    end;
  end;
end;

function TSynBigTableMetaData.GetCount: integer;
begin
  result := fMetaDataCount;
end;

function TSynBigTableMetaData.GetID(Index: integer): integer;
begin
  if cardinal(Index)<Cardinal(fMetaDataCount) then
    result := fMetaDataID[Index] else
    result := 0;
end;

function TSynBigTableMetaData.GetMetaData(aPhysicalIndex: integer;
  var aTempData: RawByteString): pointer;
begin
  if cardinal(aPhysicalIndex)<Cardinal(fMetaDataCount) then
    result := Pointer(fMetaDataRecords[aPhysicalIndex]) else
    result := nil;
end;

function TSynBigTableMetaData.GetMetaDataFromID(aID: integer): pointer;
var ndx: integer;
begin
  ndx := FastFindIntegerSorted(pointer(fMetaDataID),fMetaDataCount-1,aID);
  if ndx<0 then
    result := nil else
    result := pointer(fMetaDataRecords[ndx]);
end;

function TSynBigTableMetaData.IndexToID(aIndex: Integer;
  handleAliases: boolean): integer;
begin
  if cardinal(aIndex)<cardinal(fMetaDataCount) then
    result := fMetaDataID[aIndex] else
    result := 0;
end;

class function TSynBigTableMetaData.InternalMagic: integer;
begin
  result := integer($A5A50002); // genuine ID which differs from others
end;

function TSynBigTableMetaData.RecordAdd(const aData: RawByteString;
  const aMetaDataRecord: TSynTableData): integer;
begin
  result := Add(aData,aMetaDataRecord.SBF);
end;

procedure TSynBigTableMetaData.RecordGet(aID: integer;
  var result: TSynTableData);
var RecordSBF: RawByteString;
    ndx: integer;
begin
  ndx := FastFindIntegerSorted(pointer(fMetaDataID),fMetaDataCount-1,aID);
  if ndx>=0 then
    RecordSBF := fMetaDataRecords[ndx];
  result.Init(Table,aID,pointer(RecordSBF),length(RecordSBF));
end;

function TSynBigTableMetaData.RecordUpdate(const aMetaDataRecord: TSynTableData): boolean;
begin
  result := Update(aMetaDataRecord.ID,aMetaDataRecord.SBF);
end;

procedure TSynBigTableMetaData.SearchIterating(aCallBack: TSynBigTableIterateEvent;
  Order: TSynBigTableIterationOrder; Opaque: pointer);
var i: integer;
begin // only handle internal order by now (i.e. ioID)
  for i := 0 to fMetaDataCount-1 do
    if not aCallBack(self,Opaque,fMetaDataID[i],i,
       pointer(fMetaDataRecords[i]),length(fMetaDataRecords[i])) then
      exit;
end;

function TSynBigTableMetaData.Update(aID: integer; const aMetaData: TSBFString): boolean;
var ndx: integer;
    aOldData: TSBFString;
begin
  ndx := FastFindIntegerSorted(pointer(fMetaDataID),fMetaDataCount-1,aID);
  if (ndx>=0) and CheckConstraints(aMetaData,ndx) then begin
    aOldData := fMetaDataRecords[ndx];
    fMetaDataRecords[ndx] := aMetaData;
    Table.FieldIndexModify(ndx,ndx,pointer(aOldData),pointer(aMetaData));
    result := true;
  end else
    result := false;
end;

{$ifndef LVCL}
function TSynBigTableMetaData.VariantAdd(const aData: RawByteString;
  const aMetaDataRecord: Variant): integer;
begin
  result := Add(aData,TSynTableVariantType.ToSBF(aMetaDataRecord));
end;

function TSynBigTableMetaData.VariantGet(aID: integer): Variant;
var ndx: Integer;
    RecordSBF: RawByteString;
begin
  ndx := FastFindIntegerSorted(pointer(fMetaDataID),fMetaDataCount-1,aID);
  if ndx>=0 then
    RecordSBF := fMetaDataRecords[ndx];
  if RecordSBF='' then
    VarClear(result) else
    result := fTable.Data(aID,pointer(RecordSBF),length(RecordSBF));
end;

function TSynBigTableMetaData.VariantUpdate(const aMetaDataRecord: Variant): boolean;
begin
  with TSynTableVariantType do
   result := Update(ToID(aMetaDataRecord),ToSBF(aMetaDataRecord));
end;
{$endif}

end.

