VMS Tech Demo 5 - BDB

Content:

  1. Introduction
  2. Hypothetical context
  3. Build
  4. BLOB in BLOB out
  5. Big records
  6. Multi key
  7. Transactions
  8. Conclusion

Introduction:

This article will show how to use BDB (BerkeleyDB) on VMS.

BDB is an excellent NoSQL Key Value Store database.

There is not actually any VMS specific Java code in this article. The VMS aspect is that it tries to relate BDB to VMS index-sequential files. And there are VMS scripts to build and run the code.

Many of the examples will be borrowed from the BDB section in my NoSQL KVS article.

The biggest downside of BDB is probably the licensing. BDB is available under either AGPL or a commercial license from Oracle.

Before using BDB for anything important please consult an expert in such licenses.

My understanding as a non-lawyer is:

your code AGPL commercial license
internal usage only OK OK (but no reason to pay unless you need support)
provide either software product or service based on open source externally
and willing to supply source code under AGPL license
OK OK (but no reason to pay unless you need support)
provide either software product or service based on open source externally
and *NOT* willing to supply source code under AGPL license
*not* OK OK

But if it is important then you need advice from an expert.

Hypothetical context:

Let us assume that we have an older VMS application written in native language (Pascal/Cobol/Basic) using index-sequential files and it has been decided to migrate to a newer language (C/C++/Java) but wanting to stay on VMS.

For persistence there are a few options:

Relational database is the obvious choice. I have already written about that choice numerous times:

But maybe there are reasons for not to go that route and sticking to the Key Value Store / ISAM model.

So if one decide to go for another NoSQL Key Value Store database, then BDB is a possibility.

Build:

First problem is to get BDB itself build on VMS.

Luckily JoukJ has already ported to VMS, so no big problem.

Process:

  1. download a standard db-18_1_40.zip
  2. download db-18_1_40_vmspatch.zip and vms_x_fix.h from JoukJ web site
  3. build
$ unzip [netsrc]db-18_1_40.zip
$ unzip -o [netsrc]db-18_1_40_vmspatch.zip
$ rename db-18^.1^.40.dir bdb.dir
$ set def [.bdb]
$ set def [.build_vms]
$ copy [netsrc]vms_x_fix.h *.*
$ define/nolog sys$library 'f$env("default")',sys$sysroot:[syslib]
$ mmk

The build takes a little time but proceeeds fine with only a single warning (tested on VMS Alpha).

Next is the client libraries.

C
included in above build
C++
I did not look at that
Perl, PHP, Tcl
I am not qualified to build those
Java
I did build

The native code for the Java client build with:

$ def/nolog dbinc disk2:[arne.bdb.db-18_1_40.src.dbinc]
$ def/nolog dbinc_auto disk2:[arne.bdb.db-18_1_40.src.dbinc_auto]
$ cc/def="vms"/name=(as_is,shortened)/reent=multi/float=ieee/ieee=denorm/include=(dbdir,disk0:[vms$common.java$150.include],disk0:[vms$common.java$150.include.vms]) db_java_wrap
$ @scan_globals_for_option db_java_wrap.obj wrap.opt
$ link/share=db_java_wrap_shr.exe db_java_wrap + wrap.opt/option + sys$input/option
java$java_shr/share
dbdir:libdb/lib
$

The Java code for the Java client build totally standard with ant:

<project name="db" default="build">
    <target name="build">
        <javac source="1.5" target="1.5" srcdir="src" destdir="bin"/>
        <jar destfile="db.jar" basedir="bin"/>
    </target>
</project>

If VMS Alpha Java 1.5 or VMS Itanium Java 1.6 then 32 bit pointers should be fine. If VMS Itanium Java 1.8 then 64 bit pointers will be necessary and that *may* require changes to the build.

The C client is very C centric and probably very difficult to call from other native VMS languages (of course with the exception of C++).

To make it easier I write a little VMS wrapper in C that expose an API with descriptors etc..

And that API got wrapped in a Pascal API.

That code including build script and test is available here (take the pbdb library).

BLOB in BLOB out:

BDB expose an API where both keys and values are just byte arrays. One can store a chunk of bytes (a BLOB) and later retrieve the same chunk of bytes (BLOB).

This is somewhat equivalent of the RMS API where one just pass address of data and length of data. For an example see the C code here.

Example showing various operations:

/* standard C headers */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* BDB headers */
#include "db.h"

/* data structure */
struct data
{
    int iv;
    double xv;
    char sv[50];
};

static void serialize(struct data *d, char *buf)
{
    memcpy(buf, d, sizeof(struct data));
}

static void deserialize(char *buf, struct data *d)
{
    memcpy(d, buf, sizeof(struct data));
}

/* error handling functions */
void db_exit(char *msg, int stat)
{
    char buf[256];
    printf("DB error %s: %s\n", msg, db_strerror(stat));
    exit(1);
}

static void dump(DB *db, char *keybuf)
{
    int stat;
    DBT key;
    DBT value;
    struct data d;
    printf("Key=%s\n", keybuf);
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = keybuf;
    key.size = strlen(keybuf);
    value.size = sizeof(struct data);
    stat = db->get(db, NULL, &key, &value, 0);
    if(stat == 0) 
    {
        deserialize(value.data, &d);
        printf("(%d,%f,%s)\n", d.iv, d.xv, d.sv);
    }
    else if(stat == DB_NOTFOUND)
    {
        printf("Not found\n");
    }
    else
    {
        db_exit("db->get", stat);
    }
}

static const char *DBNAME = "test2.db";
static const int NREC = 1000;
static const int C_OFFSET = 1000; // 1 * NREC;

int main()
{
    int stat;
    DB *db;
    DBC *it, *it2;
    DBT key, value, itkey;
    struct data d;
    char keybuf[32], termkeybuf[32], valuebuf[256];
    int n, n2;
    /* initialize */
    stat = db_create(&db, NULL, 0);
    if(stat != 0)
    {
        db_exit("db_create", stat);
    }
    /* get connection */
    stat = db->open(db, NULL, DBNAME, NULL, DB_BTREE, DB_CREATE, 0664); /* DB_HASH is alternative to DB_BTREE */
    if(stat != 0)
    {
        db_exit("db->open", stat);
    }
    /* put data */
    for(int i = 0; i < NREC; i++)
    {
        memset(&key, 0, sizeof(DBT));
        memset(&value, 0, sizeof(DBT));
        sprintf(keybuf, "Key#%d", C_OFFSET + i + 1);
        key.data = keybuf;
        key.size = strlen(keybuf);
        d.iv = i + 1;
        d.xv = i + 1.0;
        sprintf(d.sv, "This is value %d", i + 1);
        serialize(&d, valuebuf);
        value.data = valuebuf;
        value.size = sizeof(struct data);
        stat = db->put(db, NULL, &key, &value, 0);
        if(stat != 0)
        {
            db_exit("db->put", stat);
        }
    }
    /* */
    sprintf(keybuf, "Key#%d", C_OFFSET + 77);
    /* get */
    dump(db, keybuf);
    /* delete */
    memset(&key, 0, sizeof(DBT));
    key.data = keybuf;
    key.size = strlen(keybuf);
    stat = db->del(db, NULL, &key, 0);
    if(stat != 0)
    {
        db_exit("db->del", stat);
    }
    /* get non existing */
    dump(db, keybuf);
    /* */
    sprintf(keybuf, "Key#%d", C_OFFSET + 88);
    /* update and get */
    dump(db, keybuf);
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = keybuf;
    key.size = strlen(keybuf);
    value.size = sizeof(struct data);
    stat = db->get(db, NULL, &key, &value, 0);
    if(stat != 0)
    {
        db_exit("db->get", stat);
    }
    deserialize(value.data, &d);
    d.iv = d.iv + 1;
    d.xv = d.xv + 0.1;
    strcat(d.sv, " updated");
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    serialize(&d, valuebuf);
    key.data = keybuf;
    key.size = strlen(keybuf);
    value.data = valuebuf;
    value.size = sizeof(struct data);
    stat = db->put(db, NULL, &key, &value, 0);
    if(stat != 0)
    {
        db_exit("db->put", stat);
    }
    dump(db, keybuf);
    /* list all */
    n = 0;
    stat = db->cursor(db, NULL, &it, 0);
    if(stat != 0)
    {
        db_exit("db->cursor", stat);
    }
    for(;;)
    {
        memset(&key, 0, sizeof(DBT));
        memset(&value, 0, sizeof(DBT));
        stat = it->get(it, &key, &value, DB_NEXT);
        if(stat == 0) 
        {
            memcpy(keybuf, key.data, key.size);
            keybuf[key.size] = '\0';
            if(strstr(keybuf, "Key#") != keybuf) 
            {
                printf("Unexpected key: %s\n", keybuf);
            }
            deserialize(value.data, &d);
            if (d.iv < 1 || NREC < d.iv)
            {
                printf("Unexpected value: (%d,%f,%s)\n", d.iv, d.xv, d.sv);
            }
            n++;
        }
        else if(stat == DB_NOTFOUND)
        {
            /* done */
            break;
        }
        else
        {
            db_exit("dbc->get", stat);
        }
    }
    printf("%d\n", n);
    /* list keys where "Key#n075" <= key < "Key#n085" */
    n2 = 0;
    stat = db->cursor(db, NULL, &it2, 0);
    if(stat != 0)
    {
        db_exit("db->cursor", stat);
    }
    sprintf(keybuf, "Key#%d", C_OFFSET + 75);
    sprintf(termkeybuf, "Key#%d", C_OFFSET + 85);
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = keybuf;
    key.size = strlen(keybuf);
    value.size = sizeof(struct data);
    stat = it2->get(it2, &key, &value, DB_SET);
    for(;;)
    {
        if(stat == 0) 
        {
            memcpy(keybuf, key.data, key.size);
            keybuf[key.size] = '\0';
            if(strcmp(keybuf, termkeybuf) >= 0) break;
            n2++;
        }
        else if(stat == DB_NOTFOUND)
        {
            /* done */
            break;
        }
        else
        {
            db_exit("dbc->get", stat);
        }
        memset(&key, 0, sizeof(DBT));
        memset(&value, 0, sizeof(DBT));
        stat = it2->get(it2, &key, &value, DB_NEXT);
    }
    printf("%d\n", n2);
    /* close */
    db->close(db, 0);
    return 0;
}

Build and run:

$ cc/name=as_is/include=dbdir/float=ieee test2
$ link test2 + dbdir:libdb/lib
$ run test2
import java.io.Serializable;

public class Data implements Serializable {
    private static final long serialVersionUID = 1L;
    private int iv;
    private double xv;
    private String sv;
    public Data() {
        this(0, 0.0, "");
    }
    public Data(int iv, double xv, String sv) {
        this.iv = iv;
        this.xv = xv;
        this.sv = sv;
    }
    public int getIv() {
        return iv;
    }
    public void setIv(int iv) {
        this.iv = iv;
    }
    public double getXv() {
        return xv;
    }
    public void setXv(double xv) {
        this.xv = xv;
    }
    public String getSv() {
        return sv;
    }
    public void setSv(String sv) {
        this.sv = sv;
    }
    @Override
    public String toString() {
        return String.format("{iv: %d, xv: %f, sv: %s}", iv, xv, sv);
    }
}
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import com.sleepycat.db.Cursor;
import com.sleepycat.db.Database;
import com.sleepycat.db.DatabaseConfig;
import com.sleepycat.db.DatabaseEntry;
import com.sleepycat.db.DatabaseException;
import com.sleepycat.db.OperationStatus;

import dk.vajhoej.record.Alignment;
import dk.vajhoej.record.Endian;
import dk.vajhoej.record.FieldType;
import dk.vajhoej.record.RecordException;
import dk.vajhoej.record.Struct;
import dk.vajhoej.record.StructField;
import dk.vajhoej.record.StructReader;
import dk.vajhoej.record.StructWriter;

public class Test2 {
    @Struct(endianess=Endian.LITTLE, alignment=Alignment.ALIGN8, endpad=true)
    public static class CData {
        @StructField(n=0, type=FieldType.INT4)
        public int iv;
        @StructField(n=1, type=FieldType.FP8)
        public double xv;
        @StructField(n=2, type=FieldType.FIXSTRNULTERM, length=50)
        public String sv;
        //
        public CData() {
            iv = 0;
            xv = 0.0;
            sv = "";
        }
        public CData(Data d) {
            iv = d.getIv();
            xv = d.getXv();
            sv = d.getSv();
        }
        public Data getData() {
            return new Data(iv, xv, sv);
        }
    }
    private static DatabaseEntry prep(String key) throws UnsupportedEncodingException {
        return new DatabaseEntry(key.getBytes("UTF-8"));
    }
    private static DatabaseEntry serialize(Data o) throws IOException, RecordException {
        CData o2 = new CData(o);
        StructWriter sw = new StructWriter();
        sw.write(o2);
        return new DatabaseEntry(sw.getBytes());
    }
    private static Data deserialize(DatabaseEntry de) throws IOException, ClassNotFoundException, RecordException {
        StructReader sr = new StructReader(de.getData());
        CData o = sr.read(CData.class);
        return o.getData();
    }
    private static void dump(Database db, String key) throws ClassNotFoundException, IOException, DatabaseException, RecordException {
        System.out.printf("Key=%s\n", key);
        DatabaseEntry rawvalue = new DatabaseEntry();
        if(db.get(null, prep(key), rawvalue, null) == OperationStatus.SUCCESS) {
            Data value = deserialize(rawvalue);
            System.out.println(value);
        } else {
            System.out.println("Not found");
        }
    }
    private static final String DBNAME = "test2.db";
    private static final int NREC = 1000;
    private static final int JAVA_OFFSET = 2 * NREC;
    public static void main(String[] args) throws DatabaseException, UnsupportedEncodingException, IOException, ClassNotFoundException, RecordException {
         Database db = new Database(DBNAME, null, new DatabaseConfig());
         // put data
         for(int i = 0; i < NREC; i++) {
             String key = "Key#" + (JAVA_OFFSET + i + 1);
             Data value = new Data();
             value.setIv(i + 1);
             value.setXv(i + 1.0);
             value.setSv(String.format("This is value %d", i + 1));
             db.put(null, prep(key), serialize(value));
         }
         //
         String key;
         key = "Key#" + (JAVA_OFFSET + 77);
         // get
         dump(db, key);
         // delete
         db.delete(null, prep(key));
         // get non existing
         dump(db, key);
         //
         key = "Key#" + (JAVA_OFFSET + 88);
         // update and get
         dump(db, key);
         DatabaseEntry rawvalue = new DatabaseEntry();
         db.get(null, prep(key), rawvalue, null);
         Data value = (Data)deserialize(rawvalue);
         value.setIv(value.getIv() + 1);
         value.setXv(value.getXv() + 0.1);
         value.setSv(value.getSv() + " updated");
         db.put(null, prep(key), serialize(value));
         dump(db, key);
         // list all
         Cursor it = db.openCursor(null, null);
         DatabaseEntry itrawkey = new DatabaseEntry();
         DatabaseEntry itrawvalue = new DatabaseEntry();
         OperationStatus itstat = it.getFirst(itrawkey, itrawvalue, null);
         int n = 0;
         while(itstat == OperationStatus.SUCCESS) {
             String itkey = new String(itrawkey.getData(), "UTF-8");
             if(!itkey.startsWith("Key#")) {
                 System.out.println("Unexpected key: " + itkey);
             }
             Data itvalue = (Data)deserialize(itrawvalue);
             if(itvalue.getIv() < 1 || NREC < itvalue.getIv()) {
                 System.out.println("Unexpected value :" + itvalue);
             }
             n++;
             itstat = it.getNext(itrawkey, itrawvalue, null);
         }
         it.close();
         System.out.println(n);
         // list keys where "Key#1075" <= key < "Key#1085"
         Cursor it2 = db.openCursor(null, null);
         DatabaseEntry it2rawkey = prep("Key#" + 1075);
         DatabaseEntry it2rawvalue = new DatabaseEntry();
         OperationStatus it2stat = it2.getSearchKey(it2rawkey, it2rawvalue, null);
         int n2 = 0;
         while(it2stat == OperationStatus.SUCCESS) {
             String it2key = new String(it2rawkey.getData(), "UTF-8");
             if(it2key.compareTo("Key#" + 1085) >= 0) break;
             n2++;
             it2stat = it2.getNext(it2rawkey, it2rawvalue, null);
         }
         it2.close();
         System.out.println(n2);
         //
         db.close();
    }
}

Build and run:

$ javac -classpath db.jar:/isamdir/record.jar Test2.java Data.java
$ define/nolog db_java_wrap_shr disk2:[arne.bdb]db_java_wrap_shr.exe
$ java "-Xmx384m" "-Dsleepycat.db.libname=db_java_wrap_shr" -classpath .:db.jar:/isamdir/record.jar "Test2"
[inherit('pbdbdir:pbdb')]
program test2(input, output);

label
   finished;

const
   NREC = 1000;
   PAS_OFFSET = 3 * NREC;

type
   keystr = packed array [1..8] of char;
   strdata = packed array [1..50] of char;
   data = packed record
             iv : integer;
             xv : double;
             sv : strdata;
          end;

function trim(s : strdata) : pstr;

var
   temp : pstr;
   ix : integer;

begin
   temp := s;
   ix := temp.length;
   while ((temp[ix] = chr(0)) or (temp[ix] = ' ')) and (ix > 1) do  ix := ix - 1;
   trim := substr(temp, 1, ix);
end;

var
   db : db_ptr;

procedure dump(id : keystr);

var
   d : data;
   stat, dummy : integer;

begin
   writeln('Key=', id);
   stat := db_sk_get(db, id, iaddress(d), size(d), dummy);
   if stat = 0 then begin
      writeln('(', d.iv:1, ',', d.xv:3:1, ',', trim(d.sv), ')');
   end else begin
      writeln('Not found');
   end;
end;

var                                                          
   it, it2 : cursor_ptr;
   id : keystr;
   d : data;
   stat, i, dummy, n, n2 : integer;
   wdummy : word;

begin
   (* open database *)
   db := db_open('test2.db');
   (* put data *)
   for i := 1 to NREC do begin
      id := 'Key#' + dec(PAS_OFFSET + i, 4);
      d.iv := i;
      d.xv := i;
      d.sv := 'This is value ' + dec(i, 4);
      db_sk_put(db, id, iaddress(d), size(d));
   end;
   (* *)
   id := 'Key#' + dec(PAS_OFFSET + 77, 4);
   (* get *)
   dump(id);
   (* delete *)
   db_sk_delete(db, id);
   (* get non existing *)
   dump(id);
   (* *)
   id := 'Key#' + dec(PAS_OFFSET + 88, 4);
   (* update and get *)
   dump(id);
   db_sk_get(db, id, iaddress(d), size(d), dummy);
   d.iv := d.iv + 1;
   d.xv := d.xv + 0.1;
   d.sv := trim(d.sv) + ' updated';
   db_sk_put(db, id, iaddress(d), size(d));
   dump(id);
   (* list all *)
   it := db_cursor_open(db);
   n := 0;
   while db_cursor_sk_next(it, id, wdummy, iaddress(d), size(d), dummy) = 0 do begin
      if (d.iv < 1) or (NREC < d.iv) then begin
         writeln('Unepected value: ', '(', d.iv:1, ',', d.xv:3:1, ',', trim(d.sv), ')');
      end;
      n := n + 1;
   end;
   writeln(n:1);
   (* list keys where "Key#n075" <= key < "Key#n085" *)
   it2 := db_cursor_open(db);
   id := 'Key#' + dec(PAS_OFFSET + 75, 4);
   stat := db_cursor_sk_find(it2, id, iaddress(d), size(d), dummy);
   n2 := 0;
   while stat = 0 do begin
      if id >= ('Key#' + dec(PAS_OFFSET + 85, 4)) then goto finished;
      n2 := n2 + 1;
      stat := db_cursor_sk_next(it2, id, wdummy, iaddress(d), size(d), dummy);
   end;
finished:
   writeln(n2:1);
   (* close database *)
   db_close(db);
end.

Build and run:

$ define/nolog pbdbdir disk2:[arne.vmspascal.pbdb]
$ pas test2
$ link test2 + pbdbdir:pbdb/opt + pbdbdir:bdb/opt
$ run test2
      program test
      implicit none
      structure /mydata/
        integer*4 iv
        real*8 xv
        character*50 sv
      endstructure
      record /mydata/d
      character*8 id,stopid
      integer*4 NREC,FOR_OFFSET
      parameter (NREC=1000,FOR_OFFSET=4*NREC)
      integer*4 db,it,it2,i,dummy,slen,stat,n,n2
      integer*2 wdummy
cdec$ alias db_open, 'db_open'
cdec$ alias db_sk_put, 'db_sk_put'
cdec$ alias db_sk_get, 'db_sk_get'
cdec$ alias db_sk_delete, 'db_sk_delete'
cdec$ alias db_cursor_open, 'db_cursor_open'
cdec$ alias db_cursor_sk_find, 'db_cursor_sk_find'
cdec$ alias db_cursor_sk_next, 'db_cursor_sk_next'
cdec$ alias db_close, 'db_close'
      integer*4 db_open,db_cursor_open,db_cursor_sk_next,
     +          db_cursor_sk_find,real_len
c  open
      db=db_open('test2.db')
c  put data
      do 100 i=1,NREC
        write(id,'(A4,I4)') 'Key#',(FOR_OFFSET+i)
        d.iv=i
        d.xv=i
        write(d.sv,'(A,I4.4)') 'This is value ',i
        call db_sk_put(db,id,%val(%loc(d)),sizeof(d))
100   continue
c
      write(id,'(A4,I4)') 'Key#',(FOR_OFFSET + 77)
c  get
      call dump(db,id)
c  delete
      call db_sk_delete(db,id)
c  get non-existing
      call dump(db,id)
c  get and update
      write(id,'(A4,I4)') 'Key#',(FOR_OFFSET + 88)
      call dump(db,id)
      call db_sk_get(db,id,%val(%loc(d)),sizeof(d),dummy)
      d.iv=d.iv+1
      d.xv=d.xv+0.1
      slen=real_len(d.sv)
      d.sv(slen+1:slen+8)=' updated'
      call db_sk_put(db,id,%val(%loc(d)),sizeof(d))
      call dump(db,id)
c  list all
      n=0
      it=db_cursor_open(db)
200   stat=db_cursor_sk_next(it,id,wdummy,%val(%loc(d)),sizeof(d),dummy)
      if(stat.eq.0) then
        if(id(1:4).ne.'Key#') then
          write(*,*) 'Unexpected key: ',id
        end if
        if(d.iv.lt.1.or.NREC.lt.d.iv) then
          slen=real_len(d.sv)
          write(*,*) 'Unexpected value: ',
     +               '(',d.iv,',',d.xv,',',d.sv(1:slen),')'
        endif
        n=n+1
        goto 200
      end if
      write(*,*) n
c  list keys where "Key#n075" <= key < "Key#n085"
      write(id,'(A4,I4)') 'Key#',(FOR_OFFSET+75)
      write(stopid,'(A4,I4)') 'Key#',(FOR_OFFSET+85)
      n2=0
      it2=db_cursor_open(db)
      stat=db_cursor_sk_find(it2,id,%val(%loc(d)),sizeof(d),dummy)
300   if(stat.eq.0) then
        if(id.ge.stopid) goto 400
        n2=n2+1
        stat=db_cursor_sk_next(it2,id,wdummy,
     +                         %val(%loc(d)),sizeof(d),dummy)
        goto 300
      end if
400   write(*,*) n2
c  close
      call db_close(db)
      end
c
      subroutine dump(db,id)
      implicit none
      integer*4 db
      character*8 id
      structure /mydata/
        integer*4 iv
        real*8 xv
        character*50 sv
      endstructure
      record /mydata/d
      integer*4 stat,dummy,slen
cdec$ alias db_sk_get, 'db_sk_get'
      integer*4 db_sk_get,real_len
      write(*,*) 'Key=',id
      stat=db_sk_get(db,id,%val(%loc(d)),sizeof(d),dummy)
      if(stat.eq.0) then
        slen=real_len(d.sv)
        write(*,*) '(',d.iv,',',d.xv,',',d.sv(1:slen),')'
      else
        write(*,*) 'Not found'
      end if
      return
      end
c
      integer*4 function real_len(sv)
      implicit none
      character*50 sv
      integer*4 ix
      ix=50
900   if((sv(ix:ix).eq.char(0).or.sv(ix:ix).eq.' ').and.ix.gt.1) then
        ix=ix-1
        goto 900
      endif
      real_len=ix
      end

Build and run:

$ define/nolog pbdbdir disk2:[arne.vmspascal.pbdb]
$ for test2
$ link test2 + pbdbdir:pbdb/opt + pbdbdir:bdb/opt
$ run test2

Big records:

BDB does not have a 32 KB limit on records like RMS index-sequential files.

No problems with big records.

Here is an example working with 1.5 MB texts:

/* standard C headers */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* BDB headers */
#include "db.h"

/* error handling functions */
void db_exit(char *msg, int stat)
{
    char buf[256];
    printf("DB error %s: %s\n", msg, db_strerror(stat));
    exit(1);
}

/* IO function */
static char buf[10000000];

void load(char *fnm)
{
    FILE *fp;
    char *line;
    int ix;
    fp = fopen(fnm, "r");
    ix = 0;
    while((line = fgets(buf + ix, sizeof(buf) - ix, fp)) != NULL)
    {
        ix += strlen(line);
    }
    fclose(fp);
}

void save(char *fnm)
{
    FILE *fp;
    fp = fopen(fnm, "w");
    fwrite(buf, strlen(buf), 1, fp);
    fclose(fp);
}

static const char *DBNAME = "big.db";

int main()
{
    int stat;
    DB *db;
    DBC *it, *it2;
    DBT key, value;
    char *copy1 = "copy1";
    char *copy2 = "copy2";
    /* initialize */
    stat = db_create(&db, NULL, 0);
    if(stat != 0)
    {
        db_exit("db_create", stat);
    }
    /* get connection */
    stat = db->open(db, NULL, DBNAME, NULL, DB_BTREE, DB_CREATE, 0664); /* DB_HASH is alternative to DB_BTREE */
    if(stat != 0)
    {
        db_exit("db->open", stat);
    }
    /* put data */
    load("4300-0.txt");
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = copy1;
    key.size = strlen(copy1);
    value.data = buf;
    value.size = strlen(buf);
    stat = db->put(db, NULL, &key, &value, 0);
    if(stat != 0)
    {
        db_exit("db->put", stat);
    }
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = copy2;
    key.size = strlen(copy2);
    value.data = buf;
    value.size = strlen(buf);
    stat = db->put(db, NULL, &key, &value, 0);
    if(stat != 0)
    {
        db_exit("db->put", stat);
    }
    /* get data */
    memset(buf, 0, sizeof(buf));
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = copy1;
    key.size = strlen(copy1);
    value.size = sizeof(buf);
    stat = db->get(db, NULL, &key, &value, 0);
    if(stat != 0)
    {
        db_exit("db->get", stat);
    }
    strncpy(buf, value.data, value.size);
    save("copy1.txt");
    memset(&key, 0, sizeof(DBT));
    memset(&value, 0, sizeof(DBT));
    key.data = copy2;
    key.size = strlen(copy2);
    value.size = sizeof(buf);
    stat = db->get(db, NULL, &key, &value, 0);
    if(stat != 0)
    {
        db_exit("db->get", stat);
    }
    strncpy(buf, value.data, value.size);
    save("copy2.txt");
    /* close */
    db->close(db, 0);
    return 0;
}

Build and run:

$ cc/name=as_is/include=dbdir/float=ieee big
$ link big + dbdir:libdb/lib
$ run big
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

import com.sleepycat.db.Database;
import com.sleepycat.db.DatabaseConfig;
import com.sleepycat.db.DatabaseEntry;
import com.sleepycat.db.DatabaseException;
import com.sleepycat.db.DatabaseType;

public class Big {
    private static DatabaseEntry toEntry(String s) {
        return new DatabaseEntry(s.getBytes());
    }
    private static String fromEntry(DatabaseEntry dbe) {
        return new String(dbe.getData());
    }
    private static String load(String fnm) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new FileReader(fnm));
        String line;
        while((line = br.readLine()) != null) {
            sb.append(line + "\n");
        }
        br.close();
        return sb.toString();
    }
    private static void store(String txt, String fnm) throws IOException {
        PrintWriter pw = new PrintWriter(new FileWriter(fnm));
        pw.print(txt);
        pw.close();
    }
    public static void main(String[] args) throws IOException, DatabaseException {
        // open database
        DatabaseConfig dbcfg = new DatabaseConfig();
        dbcfg.setAllowCreate(true);
        dbcfg.setType(DatabaseType.BTREE);
        Database db = new Database("big.db", null, dbcfg);
        // put
        db.put(null, toEntry("copy1"), toEntry(load("4300-0.txt")));
        db.put(null, toEntry("copy2"), toEntry(load("4300-0.txt")));
        // get
        DatabaseEntry copy1 = new DatabaseEntry();
        db.get(null, toEntry("copy1"), copy1, null);
        store(fromEntry(copy1), "copy1.txt");
        DatabaseEntry copy2 = new DatabaseEntry();
        db.get(null, toEntry("copy2"), copy2, null);
        store(fromEntry(copy2), "copy2.txt");
        // close database
        db.close();
    }
}

Build and run:

$ javac -classpath db.jar Big.java
$ define/nolog db_java_wrap_shr disk2:[arne.bdb]db_java_wrap_shr.exe
$ java "-Xmx384m" "-Dsleepycat.db.libname=db_java_wrap_shr" -classpath .:db.jar "Big"
[inherit('pbdbdir:pbdb')]
program big(input, output);

var
   buf : packed array [1..10000000] of chr;
   buflen : integer;

(*
Note: these IO routines are neither typical Pascal nor typical VMS,
      but they are comptible with the way the C code works.
*)

procedure load(fnm : pstr);

var
   f : text;
   c : char;

begin
   open(f, fnm, old);
   reset(f);
   while not eof(f) do begin
      if eoln(f) then begin
         buflen := buflen + 1;
         buf[buflen] := chr(10);
         readln(f);
      end else begin
         read(f, c);
         bufen := buflen + 1;
         buf[buflen] := c;
      end;
   end;
   close(f);
end;

procedure store(fnm : pstr);

var
   f : text;
   i : integer;

begin
   open(f, fnm, new, record_format := 'stmlf');
   rewrite(f);
   for i := 1 to buflen do begin
      write(f, buf[i]);
   end;
   close(f);
end;

var
   db : db_ptr;
   stat, dummy : integer;
   wdummy : word;

begin
   (* open database *)
   db := db_open('big.db');
   (* put *)
   load('4300-0.txt');
   db_sk_put(db, 'copy1', buf, buflen);
   db_sk_put(db, 'copy2', buf, buflen);
   (* get *)
   buflen := 0;
   db_sk_get(db, 'copy1', buf, size(buf), buflen);
   store('copy1.txt');
   buflen := 0;
   db_sk_get(db, 'copy2', buf, size(buf), buflen);
   store('copy2.txt');
   (* close database *)
   db_close(db);
end.

Build and run:

$ define/nolog pbdbdir disk2:[arne.vmspascal.pbdb]
$ pas big
$ link big + pbdbdir:pbdb/opt + pbdbdir:bdb/opt
$ run big

Multi key:

An important capability of VMS index-sequential files is the support for multiple keys.

BDB has support for that even though the API is sligtly complex.

No examples yet.

Transactions:

BDB supports traditional transactions:

BEGIN
operation 1
operation 2
...
operation n
COMMIT or ROLLBACK

Two benefits:

The first is handled in VMS index-sequential files using explicit locking.

For more info about traditional transactions and transaction isolation level see here.

This can be very convenient.

No examples yet.

Conclusion:

BDB is a very capable NoSQL Key Value Store database. And it works fine on VMS.

But I doubt that it will be used much on VMS. Most likely a rewrite would move to more modern persistence paradigms.

Article history:

Version Date Description
1.0 February 8th 2023 Initial version

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj