METAKIT C++ TUTORIAL
Riccardo Cohen (rcohen at articque dot com)
articque (www.articque.com)
Date : 19 Septembre 2003

1) Introduction

The metakit database has many interesting and innovative features that will not be described here. Please read Metakit web page : http://www.equi4.com/metakit.

Here I'll present my own vision of what one should understand to starting developping with the C++ api.

2) Think different

3) Create database and view

To create a table, just tell its name :

  c4_Storage database("metakit_tutorial.db", true);
  c4_View maps=database.GetAs("maps[mid:S,mwidth:I,mheight:I,mpath:S]");

This sample code creates a database in file metakit_tutorial.db and adds a view "maps" to store mapping information such as identifier, width, height and file path. But the information is not yet stored, until commit() function is called. Until then it remain in memory.

4) Add rows to the view 'maps'

To add information in the view, just call Add() function with the filled row argument :

  c4_StringProp mid("mid"),mpath("mpath");
  c4_IntProp mwidth("mwidth"),mheight("mheight");
  c4_Row maprow;
  mid(maprow)="A";
  mwidth(maprow)=10;
  mheight(maprow)=10;
  mpath(maprow)="/home/MapA.vxf";
  maps.Add(maprow);
  mid(maprow)="B";
  mwidth(maprow)=20;
  mheight(maprow)=20;
  mpath(maprow)="/home/MapB.vxf";
  maps.Add(maprow);
  mid(maprow)="C";
  mwidth(maprow)=30;
  mheight(maprow)=30;
  mpath(maprow)="/home/MapC.vxf";
  maps.Add(maprow);

Each property is set individually, using the same type as in declaration "GetAs()", then the row is added.

This creates the following View :

mid (string) mwidth (int) mheight (int) mpath (string)
A 10 10 /home/MapA.vxf
B 20 20 /home/MapB.vxf
C 30 30 /home/MapC.vxf

5) Save file

Just call :

database.Commit();

The Kitviewer provided for MSWindows will show you all information present in the database, to check if work is done all right.

6) Retreive with "SelectRange" (where 20<=width<=30)

To get information from that database, you may try this :

  c4_Row selectrow_start,selectrow_end;
  c4_View tableselect;
  mwidth(selectrow_start)=20;
  mwidth(selectrow_end)=30;
  tableselect=maps.SelectRange(selectrow_start,selectrow_end);
  for (idx=0;idx<tableselect.GetSize();idx++)
    printf("map %s has width=%d\n",
      (const char*)mid(tableselect[idx]),
      (long)mwidth(tableselect[idx]));
Which gives the following result :
map B has width=20
map C has width=30

7) Add another view and its data

Here we will add a view with statistical data related to our maps A, B and C:

  c4_View datas=database.GetAs("datas[did:S,dmapid:S,dpath:S]");
  c4_StringProp did("did"),dmapid("dmapid"),dpath("dpath");
  c4_Row  datarow;
  did(datarow)="1";
  dmapid(datarow)="A";
  dpath(datarow)="/home/A1.txt";
  datas.Add(datarow);
  did(datarow)="2";
  dmapid(datarow)="A";
  dpath(datarow)="/home/A2.txt";
  datas.Add(datarow);
  did(datarow)="3";
  dmapid(datarow)="B";
  dpath(datarow)="/home/B1.txt";
  datas.Add(datarow);
  did(datarow)="4";
  dmapid(datarow)="B";
  dpath(datarow)="/home/B2.txt";
  datas.Add(datarow);
  did(datarow)="5";
  dmapid(datarow)="C";
  dpath(datarow)="/home/C1.txt";
  datas.Add(datarow);
  database.Commit();

This creates the following View :

did (string) dmapid (string) dpath (string)
1 A /home/A1.txt
2 A /home/A2.txt
3 B /home/B1.txt
4 B /home/B2.txt
5 C /home/C1.txt

8) Retreive with "Find" (where mapid='B')

  long idxsearch=-1;
  c4_Row findrow;
  dmapid(findrow)="B";
  while(1)
  {
    idxsearch=datas.Find(findrow,idxsearch+1);
    if (idxsearch>=0)
      printf("DATA %s has dmapid=%s\n",
        (const char*)dpath(datas[idxsearch]),
        (const char*)dmapid(datas[idxsearch]));
    else
      break;
  }
Which gives the following result :
DATA /home/B1.txt has dmapid=B
DATA /home/B2.txt has dmapid=B

Find() is case sensitive

9) Retrieve information using a relation

As I used a lot SQL databases, I like to join tables with a simple relation, nothing more. Here is how to get all datas for the maps with width between 20 and 30:
(here view datas must rename its property dmapid, since the property name must be the same between the 2 tables)

  c4_View relation;
  relation=(datas.Rename(dmapid,mid)).Join(mid,tableselect);
  for (idx=0;idx<relation.GetSize();idx++)
    printf("DATA %s is applied to map %s (width=%d)\n",
      (const char*)dpath(relation[idx]),
      (const char*)mpath(relation[idx]),
      (long)mwidth(relation[idx]));
Which gives the following result :
DATA /home/B1.txt is applied to map /home/MapB.vxf (width=20)
DATA /home/B2.txt is applied to map /home/MapB.vxf (width=20)
DATA /home/C1.txt is applied to map /home/MapC.vxf (width=30)

10) Searching manually

You may do a specific search with your own function : case insensitive, removing diacritics, partial search etc.

  c4_Row strrow;
  const char*strdata;
  const char*expr="ome/B";
  for (idx=0;idx<datas.GetSize();idx++)
  {
    strdata=(const char*)dpath(datas[idx]);
    if (strstr(strdata,expr)!=NULL)
      printf("map %s has the string '%s' in the path\n",strdata,expr);
  }
Which gives the following result :
map /home/B1.txt has the string 'ome/B' in the path
map /home/B2.txt has the string 'ome/B' in the path

11) Removing rows

Removing change the indexes of course :

  c4_Row delrow;
  dmapid(delrow)="A";
  while(1)
  {
    idxsearch=datas.Find(delrow,0);
    if (idxsearch>=0)
    {
      printf("DATA %s at idx=%d is deleted\n",
        (const char*)dpath(datas[idxsearch]),idxsearch);
      datas.RemoveAt(idxsearch);
    }
    else
      break;
  }
Which gives the following result :
DATA /home/A1.txt at idx=0 is deleted
DATA /home/A2.txt at idx=0 is deleted

Notice that the indexes are all changed in a delete operation. You cannot rely on the index

12) Updating the view content

The function "SetAt()" will change a row :
  c4_Row updaterow;
  mid(updaterow)="E";
  mwidth(updaterow)=90;
  mheight(updaterow)=90;
  mpath(updaterow)="/home/MapE.vxf";
  maps.SetAt(2,updaterow);
Which gives the following new content :
A | 10 | 10 | /home/MapA.vxf
B | 20 | 20 | /home/MapB.vxf
E | 90 | 90 | /home/MapE.vxf

Full code for all above samples is available : HERE

++) Quick search

Select() and Find() are not optimized searches. Often a simple loop (like in chapter 10) Searching manually) will be much quicker. My performance tests on 60000 rows (each key is present 3 times, which makes 20000 different keys) and on random search have given the following results :
ADD = 0.014050 ms/add
COMMIT = 641 ms
FIND = avg=41.609500 ms/find
SELECT = avg=238.688000 ms/select
SIMPLE LOOP = avg=8.077500 ms/loop search
I can't show the code here, but the tests will be stored on my source code and I can send it to who wants it.

BUT: if your key is unique, you can use Find() on a Hashed view, which is very quick :
  c4_View maps=database.GetAs("maps[mid:S,mwidth:I,mheight:I,mpath:S]");
  c4_View viewsec=database.GetAs("sec[_H:I,_R:I]");
  view=view.Hash(viewsec,1); // 1=number of properties to use for hashing
All search done with Find() function will be done throught the hash table instead of the original view.
This gives the following result:
FIND = avg=0.031000 ms/find
On the other hand, if the key is not unique, you can still benefit from hashing, because one of the property may be a subview :
  c4_View maps=database.GetAs("maps[mid:S,subview[mwidth:I,mheight:I,mpath:S]]");
  c4_View viewsec=database.GetAs("sec[_H:I,_R:I]");
  view=view.Hash(viewsec,1);
in that case, for each mid (your key) you may have any number of rows in the subview. It is a bit more complicated to retreive information, but the result is a very quick access on multiple key hashed view.
(see also chapter Tips from equi4.com)

++) The "Search" function

This function implements a binary tree on sorted views. Look at regression test tbasic2 :
  {
    c4_IntProp p1 ("p1");
    c4_StringProp p2 ("p2");
    c4_View v1;
    v1.Add(p1 [111] + p2 ["one"]);
    v1.Add(p1 [222] + p2 ["two"]);
    v1.Add(p1 [333] + p2 ["three"]);
    v1.Add(p1 [345] + p2 ["four"]);
    v1.Add(p1 [234] + p2 ["five"]);
    v1.Add(p1 [123] + p2 ["six"]);
    c4_View v2 = v1.Sort();
    A(v2.GetSize() == 6);
    A(p1 (v2[0]) == 111);
    A(p1 (v2[1]) == 123);
    A(p1 (v2[2]) == 222);
    A(p1 (v2[3]) == 234);
    A(p1 (v2[4]) == 333);
    A(p1 (v2[5]) == 345);
    A(v2.Search(p1 [123]) == 1); // found at position 1
    A(v2.Search(p1 [100]) == 0); // to be inserted at pos 0
    A(v2.Search(p1 [200]) == 2); // to be inserted at pos 2
    A(v2.Search(p1 [400]) == 6); // to be inserted at the end
   }
The function works ONLY on sorted views, and returns the position where the row was found, or where it may be inserted (if not found). This position can also be just past the last row. To know if the index found is the row, or a new inserting position, just compare again the row's key with what you search for.
Multiple keys are not found here, since the function does not provide any way to scan all rows found.

Dont forget to read chapters:
General structure
Mapping Views
Blocked Views
Tips