Use Smart Files

A smart file is the same storage engine without the type system or variables: one untyped byte array in a file, with the same ACID guarantees. Reach for it when you want transactional inserts into the middle of a blob and don't need a schema.

Open and use a smart file

Open it with ns_smfile_open and address it with byte offsets rather than queries. size is the width of one element and stride is how many elements to step between touches, so a strided write spreads data out without a query language:

#include "numstore.h"

#include <inttypes.h>
#include <stdio.h>

static void
dump (nsdb_t *smf, const char *label)
{
  uint32_t out[8] = {0};
  sb_size  n = ns_smfile_read (smf, NULL, out, sizeof (uint32_t), 0, 1, 8);
  printf ("%-16s (%" PRId64 "):", label, n);
  for (sb_size i = 0; i < n; i++) {
    printf (" %u", out[i]);
  }
  printf ("\n");
}

int
main (void)
{
  ns_cleanup ("stream.smf");

  nsdb_t *smf = ns_smfile_open ("stream.smf");
  if (smf == NULL) {
    fprintf (stderr, "could not open stream.smf\n");
    return 1;
  }

  // A smart file is one untyped byte array. Offsets and lengths are bytes.
  const uint32_t src[] = {1, 2, 3, 4, 5, 6};
  sb_size        n = ns_smfile_insert (smf, NULL, src, 0, sizeof (src));
  printf (
      "inserted %" PRId64 " bytes, file is %" PRId64 " bytes\n",
      n,
      ns_smfile_size (smf, NULL)
  );
  dump (smf, "after insert");

  // Overwrite every other u32 starting at byte 0.
  const uint32_t patch[] = {70, 80, 90};
  ns_smfile_write (smf, NULL, patch, sizeof (uint32_t), 0, 2, 3);
  dump (smf, "after write");

  // Read every other u32.
  uint32_t strided[3] = {0};
  ns_smfile_read (smf, NULL, strided, sizeof (uint32_t), 0, 2, 3);
  printf ("strided read   : %u %u %u\n", strided[0], strided[1], strided[2]);

  // Remove the first two u32s and keep what came out.
  uint32_t taken[2] = {0};
  ns_smfile_remove (smf, NULL, taken, sizeof (uint32_t), 0, 1, 2);
  printf ("removed        : %u %u\n", taken[0], taken[1]);
  dump (smf, "after remove");

  return ns_close (smf);
}
inserted 24 bytes, file is 24 bytes
after insert     (6): 1 2 3 4 5 6
after write      (6): 70 2 80 4 90 6
strided read   : 70 80 90
removed        : 70 2
after remove     (4): 80 4 90 6

What smart files share with databases

Smart files share the rest of the API: ns_begin, ns_commit, ns_rollback, ns_close, ns_crash and ns_cleanup all work on an nsdb_t * from ns_smfile_open. What they don't share is the query language - a smart file has no variables, so ns_execute, ns_read, ns_write and plans don't apply.

Keep reads in the buffer

One sharp edge: unlike ns_read, the smart file functions take no destination length. nelem is the only thing bounding the write into dest, so it is on you to keep size * nelem within your buffer:

uint32_t out[8];

// Safe: nelem matches the buffer.
ns_smfile_read (smf, NULL, out, sizeof (uint32_t), 0, 1, 8);

// Overflows out if the file holds more than 8 elements - NS_END means
// "to the end of the file", and the call does not know how big out is.
ns_smfile_read (smf, NULL, out, sizeof (uint32_t), 0, 1, NS_END);

Use NS_END only when the buffer can hold the whole file, which you can check first with ns_smfile_size. Otherwise pass an explicit nelem and loop.