Skip to content

v7.0.0

Choose a tag to compare

@jamesgpearce jamesgpearce released this 04 Dec 04:00
· 118 commits to main since this release

This important (and slightly breaking!) release adds support for null as a valid Cell and Value type, alongside string, number, and boolean.

Null Type Support

You can now set Cells and Values to null:

import {createStore} from 'tinybase';

const store = createStore();
store.setCell('pets', 'fido', 'species', 'dog');
store.setCell('pets', 'fido', 'color', null);

console.log(store.getCell('pets', 'fido', 'color'));
// -> null

console.log(store.hasCell('pets', 'fido', 'color'));
// -> true

To allow null values in your schema, use the new allowNull property:

store.setTablesSchema({
  pets: {
    species: {type: 'string'},
    color: {type: 'string', allowNull: true},
  },
});

store.setCell('pets', 'fido', 'color', null);
// Valid because allowNull is true

store.setCell('pets', 'fido', 'species', null);
// Invalid - species does not allow null

store.delSchema();

Important Distinction: null vs undefined

It's crucial to understand the difference between null and undefined in TinyBase:

  • null is an explicit value. A Cell set to null exists in the Store.
  • undefined means the Cell does not exist in the Store.

This means that the hasCell method will return true for a Cell with a null value:

store.setCell('pets', 'fido', 'color', null);
console.log(store.hasCell('pets', 'fido', 'color'));
// -> true

store.delCell('pets', 'fido', 'color');
console.log(store.hasCell('pets', 'fido', 'color'));
// -> false

store.delTables();

Breaking Change: Database Persistence

Important: This release includes a breaking change for applications using database persisters (the Sqlite3Persister, PostgresPersister, or PglitePersister interfaces, for example).

SQL NULL values are now loaded as TinyBase null values. Previously, SQL NULL would result in Cells being absent from the Store. Now, SQL NULL maps directly to TinyBase null, which means:

  • Tables loaded from SQL databases will be dense rather than sparse
  • Every Row will have every Cell Id present in the table schema
  • Cells that were SQL NULL will have the value null

Example of the roundtrip transformation via a SQLite database:

import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';

const sqlite3 = await sqlite3InitModule();
let db = new sqlite3.oo1.DB(':memory:', 'c');

store.setTable('pets', {
  fido: {species: 'dog'},
  felix: {species: 'cat', color: 'black'},
});

const tabularPersister = createSqliteWasmPersister(store, sqlite3, db, {
  mode: 'tabular',
  tables: {save: {pets: 'pets'}, load: {pets: 'pets'}},
});

await tabularPersister.save();
// After saving the the SQL database:
// SQL table: fido (species: 'dog', color: NULL)
//           felix (species: 'cat', color: 'black')

await tabularPersister.load();
// After loading again, the Store now has a dense table with an explicit null:

console.log(store.getRow('pets', 'fido'));
// -> {species: 'dog', color: null}

This is the correct semantic mapping since SQL databases have fixed schemas where every row must account for every column. See the Database Persistence guide for more details.

Migration Guide

If you are using database persisters, you should:

  1. Review your data access patterns: If you were checking hasCell(...) === false to detect missing data, you now need to check getCell(...) === null for null values.

  2. Update your schemas: Add allowNull: true to Cell definitions that should permit null values:

store.setTablesSchema({
  pets: {
    species: {type: 'string'},
    color: {type: 'string', allowNull: true},
    age: {type: 'number', allowNull: true},
  },
});
  1. Consider memory implications: Dense tables consume more memory than sparse tables. If you have large tables with many optional Cells, this could be significant.