Skip to content

Commit dc19c1b

Browse files
committed
[null] Docs
1 parent b4c3709 commit dc19c1b

File tree

17 files changed

+349
-25
lines changed

17 files changed

+349
-25
lines changed

site/guides/03_schemas/1_using_schemas.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ As you can see, when a Values object is used that doesn't quite match those
3232
constraints, the data is corrected. The `website` Value is ignored, and the
3333
missing `open` Value gets defaulted to `false`.
3434

35+
TinyBase supports four primitive types: `string`, `number`, `boolean`, and
36+
`null`. You can also allow `null` values for a specific Cell or Value by adding
37+
the `allowNull` property:
38+
39+
```js
40+
const store2 = createStore().setValuesSchema({
41+
nickname: {type: 'string', allowNull: true, default: null},
42+
verified: {type: 'boolean'},
43+
});
44+
store2.setValues({nickname: null, verified: true});
45+
console.log(store2.getValues());
46+
// -> {nickname: null, verified: true}
47+
48+
store2.setValue('nickname', 'Buddy');
49+
console.log(store2.getValue('nickname'));
50+
// -> 'Buddy'
51+
```
52+
53+
When `allowNull` is `true`, the Cell or Value can be set to either its defined
54+
type or `null`. Without `allowNull`, attempting to set a `null` value will be
55+
rejected by the schema.
56+
3557
## Adding A TablesSchema
3658

3759
Tabular schemas are similar. Set a TablesSchema prior to loading data into your

site/guides/04_persistence/2_database_persistence.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,121 @@ console.log(db.exec('SELECT * FROM pets;', {rowMode: 'object'}));
368368
await subsetPersister.destroy();
369369
```
370370

371+
## Null Values and Dense Tables
372+
373+
When working with database persisters, it's important to understand how SQL
374+
`NULL` values map to TinyBase values. This has significant implications for the
375+
structure of your Store.
376+
377+
### SQL NULL is TinyBase null
378+
379+
In TinyBase v7.0, `null` became a valid Cell and Value type alongside `string`,
380+
`number`, and `boolean`. When a database persister loads data from a SQL table,
381+
any SQL `NULL` values are loaded as TinyBase `null` values.
382+
383+
This is the natural and correct behavior: SQL `NULL` represents an explicit null
384+
value, which maps directly to TinyBase's `null` type.
385+
386+
### Sparse vs Dense Tables
387+
388+
TinyBase tables can be either **sparse** or **dense**:
389+
390+
- A **sparse table** has rows where some Cells may be missing entirely. The Row
391+
object simply doesn't have that Cell Id as a property.
392+
- A **dense table** has rows where every Cell is present, though some may have
393+
`null` values.
394+
395+
Here's an example showing the difference:
396+
397+
```js
398+
// Sparse table - some Cells are missing
399+
store.setTable('pets', {
400+
fido: {species: 'dog'},
401+
felix: {species: 'cat', color: 'black'},
402+
});
403+
404+
console.log(store.getRow('pets', 'fido'));
405+
// -> {species: 'dog'}
406+
// Note: no 'color' property
407+
408+
console.log(store.hasCell('pets', 'fido', 'color'));
409+
// -> false
410+
411+
// Dense table - all Cells present, some are null
412+
store.setTable('pets', {
413+
fido: {species: 'dog', color: null},
414+
felix: {species: 'cat', color: 'black'},
415+
});
416+
417+
console.log(store.getRow('pets', 'fido'));
418+
// -> {species: 'dog', color: null}
419+
// Note: 'color' property exists with null value
420+
421+
console.log(store.hasCell('pets', 'fido', 'color'));
422+
// -> true
423+
```
424+
425+
### Why Database Tables Become Dense
426+
427+
SQL databases have a fixed schema: every table has a defined set of columns, and
428+
every row must account for all columns. When a Cell has no value, SQL represents
429+
this as `NULL`.
430+
431+
When a database persister loads data from SQL into TinyBase:
432+
433+
- Every SQL column becomes a Cell Id in the TinyBase table
434+
- Every SQL `NULL` becomes a TinyBase `null` value
435+
- The resulting TinyBase table is **dense** - every Row has every Cell Id
436+
437+
This means that if you save a sparse TinyBase table to a database and load it
438+
back, it will become dense. For example, the roundtrip transformation via a
439+
SQLite database looks something like:
440+
441+
```js
442+
store.setTables({
443+
pets: {
444+
fido: {species: 'dog'},
445+
felix: {species: 'cat', color: 'black'},
446+
},
447+
});
448+
449+
await tabularPersister.save();
450+
// After saving the the SQL database:
451+
// SQL table: fido (species: 'dog', color: NULL)
452+
// felix (species: 'cat', color: 'black')
453+
454+
await tabularPersister.load();
455+
// After loading again, the Store now has a dense table with an explicit null:
456+
console.log(store.getRow('pets', 'fido'));
457+
// -> {species: 'dog', color: null}
458+
```
459+
460+
### Important Implications
461+
462+
1. **Round-trip behavior**: A sparse table that goes through a save/load cycle
463+
with a database persister will become dense. This is a breaking change from
464+
TinyBase v6.x and earlier.
465+
466+
2. **The `hasCell` method**: After loading from a database, `hasCell` will
467+
return `true` for all Cells in the table schema, even those that were
468+
originally missing. Use `getCell(tableId, rowId, cellId) === null` to check
469+
if a Cell has a null value.
470+
471+
3. **Schemas**: When using database persisters, you may want to use the
472+
`allowNull` schema property to explicitly allow `null` values:
473+
474+
```js
475+
store.setTablesSchema({
476+
pets: {
477+
species: {type: 'string'},
478+
color: {type: 'string', allowNull: true},
479+
},
480+
});
481+
```
482+
483+
4. **Memory usage**: Dense tables consume more memory than sparse tables. If you
484+
have large tables with many optional Cells, this could be significant.
485+
371486
## Summary
372487

373488
With care, you can load and save Store data from and to a SQLite database in a

site/guides/16_releases.md

Lines changed: 149 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,169 @@ highlighted features.
55

66
---
77

8+
# v7.0
9+
10+
This important (and slightly breaking!) release adds support for `null` as a
11+
valid Cell and Value type, alongside `string`, `number`, and `boolean`.
12+
13+
## Null Type Support
14+
15+
You can now set Cells and Values to `null`:
16+
17+
```js
18+
import {createStore} from 'tinybase';
19+
20+
const store = createStore();
21+
store.setCell('pets', 'fido', 'species', 'dog');
22+
store.setCell('pets', 'fido', 'color', null);
23+
24+
console.log(store.getCell('pets', 'fido', 'color'));
25+
// -> null
26+
27+
console.log(store.hasCell('pets', 'fido', 'color'));
28+
// -> true
29+
```
30+
31+
To allow `null` values in your schema, use the new `allowNull` property:
32+
33+
```js
34+
store.setTablesSchema({
35+
pets: {
36+
species: {type: 'string'},
37+
color: {type: 'string', allowNull: true},
38+
},
39+
});
40+
41+
store.setCell('pets', 'fido', 'color', null);
42+
// Valid because allowNull is true
43+
44+
store.setCell('pets', 'fido', 'species', null);
45+
// Invalid - species does not allow null
46+
47+
store.delSchema();
48+
```
49+
50+
## Important Distinction: `null` vs `undefined`
51+
52+
It's crucial to understand the difference between `null` and `undefined` in
53+
TinyBase:
54+
55+
- `null` is an explicit value. A Cell set to `null` exists in the Store.
56+
- `undefined` means the Cell does not exist in the Store.
57+
58+
This means that the hasCell method will return `true` for a Cell with a `null`
59+
value:
60+
61+
```js
62+
store.setCell('pets', 'fido', 'color', null);
63+
console.log(store.hasCell('pets', 'fido', 'color'));
64+
// -> true
65+
66+
store.delCell('pets', 'fido', 'color');
67+
console.log(store.hasCell('pets', 'fido', 'color'));
68+
// -> false
69+
70+
store.delTables();
71+
```
72+
73+
## Breaking Change: Database Persistence
74+
75+
**Important:** This release includes a breaking change for applications using
76+
database persisters (the Sqlite3Persister, PostgresPersister, or PglitePersister
77+
interfaces, for example).
78+
79+
SQL `NULL` values are now loaded as TinyBase `null` values. Previously, SQL
80+
`NULL` would result in Cells being absent from the Store. Now, SQL `NULL` maps
81+
directly to TinyBase `null`, which means:
82+
83+
- Tables loaded from SQL databases will be **dense** rather than **sparse**
84+
- Every Row will have every Cell Id present in the table schema
85+
- Cells that were SQL `NULL` will have the value `null`
86+
87+
Example of the roundtrip transformation via a SQLite database:
88+
89+
```js
90+
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
91+
import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';
92+
93+
const sqlite3 = await sqlite3InitModule();
94+
let db = new sqlite3.oo1.DB(':memory:', 'c');
95+
96+
store.setTable('pets', {
97+
fido: {species: 'dog'},
98+
felix: {species: 'cat', color: 'black'},
99+
});
100+
101+
const tabularPersister = createSqliteWasmPersister(store, sqlite3, db, {
102+
mode: 'tabular',
103+
tables: {save: {pets: 'pets'}, load: {pets: 'pets'}},
104+
});
105+
106+
await tabularPersister.save();
107+
// After saving the the SQL database:
108+
// SQL table: fido (species: 'dog', color: NULL)
109+
// felix (species: 'cat', color: 'black')
110+
111+
await tabularPersister.load();
112+
// After loading again, the Store now has a dense table with an explicit null:
113+
114+
console.log(store.getRow('pets', 'fido'));
115+
// -> {species: 'dog', color: null}
116+
```
117+
118+
This is the correct semantic mapping since SQL databases have fixed schemas
119+
where every row must account for every column. See the Database Persistence
120+
guide for more details.
121+
122+
## Migration Guide
123+
124+
If you are using database persisters, you should:
125+
126+
1. **Review your data access patterns**: If you were checking `hasCell(...) ===
127+
false` to detect missing data, you now need to check `getCell(...) === null`
128+
for null values.
129+
130+
2. **Update your schemas**: Add `allowNull: true` to Cell definitions that
131+
should permit null values:
132+
133+
```js yolo
134+
store.setTablesSchema({
135+
pets: {
136+
species: {type: 'string'},
137+
color: {type: 'string', allowNull: true},
138+
age: {type: 'number', allowNull: true},
139+
},
140+
});
141+
```
142+
143+
3. **Consider memory implications**: Dense tables consume more memory than
144+
sparse tables. If you have large tables with many optional Cells, this could
145+
be significant.
146+
147+
---
148+
8149
# v6.7
9150

10151
This release includes support for the Origin Private File System (OPFS) in a
11152
browser. The createOpfsPersister function is the main entry point, and is
12153
available in the existing persister-browser module:
13154

14155
```js
15-
import {createStore} from 'tinybase';
16156
import {createOpfsPersister} from 'tinybase/persisters/persister-browser';
17157

18158
const opfs = await navigator.storage.getDirectory();
19159
const handle = await opfs.getFileHandle('tinybase.json', {create: true});
20160

21-
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
22-
const persister = createOpfsPersister(store, handle);
161+
store.delTables().setTables({pets: {fido: {species: 'dog'}}});
162+
const opfsPersister = createOpfsPersister(store, handle);
23163

24-
await persister.save();
164+
await opfsPersister.save();
25165
// Store JSON will be saved to the OPFS file.
26166

27-
await persister.load();
167+
await opfsPersister.load();
28168
// Store JSON will be loaded from the OPFS file.
29169

30-
await persister.destroy();
170+
await opfsPersister.destroy();
31171
```
32172

33173
That's it! If you've used other TinyBase persisters, this API should be easy and
@@ -68,7 +208,7 @@ import {createMMKV} from 'react-native-mmkv';
68208
import {createReactNativeMmkvPersister} from 'tinybase/persisters/persister-react-native-mmkv';
69209

70210
const storage = createMMKV();
71-
const store = createStore().setTables({pets: {fido: {species: 'dog'}}});
211+
store.setTables({pets: {fido: {species: 'dog'}}});
72212
const persister = createReactNativeMmkvPersister(store, storage);
73213

74214
await persister.save();
@@ -1222,12 +1362,9 @@ and from a local SQLite database. It uses an explicit tabular one-to-one mapping
12221362
for the 'pets' table:
12231363

12241364
```js
1225-
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
1226-
import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';
1227-
1228-
const sqlite3 = await sqlite3InitModule();
1229-
const db = new sqlite3.oo1.DB(':memory:', 'c');
12301365
store.setTables({pets: {fido: {species: 'dog'}}});
1366+
1367+
db = new sqlite3.oo1.DB(':memory:', 'c');
12311368
const sqlitePersister = createSqliteWasmPersister(store, sqlite3, db, {
12321369
mode: 'tabular',
12331370
tables: {load: {pets: 'pets'}, save: {pets: 'pets'}},

site/home/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
</h2>
77
</section>
88

9-
<a href='/guides/releases/#v6-7'><em>NEW!</em> v6.7 release</a>
9+
<a href='/guides/releases/#v7-0'><em>NEW!</em> v7.0 release</a>
1010

11-
<span id="one-with">"The one with OPFS!"</span>
11+
<span id="one-with">"The one with <code>NULL</code>!"</span>
1212

1313
<a class='start' href='/guides/the-basics/getting-started/'>Get started</a>
1414

src/@types/persisters/persister-cr-sqlite-wasm/docs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272
*
7373
* See the documentation for the DpcJson and DpcTabular types for more
7474
* information on how both of those modes can be configured.
75+
*
76+
* Note: When using tabular mode, SQL NULL values are loaded as TinyBase null
77+
* values, making tables dense (every Row has every Cell). See the Database
78+
* Persistence guide for details.
7579
* @param store The Store to persist.
7680
* @param db The database instance that was returned from `crSqlite3.open(...)`.
7781
* @param configOrStoreTableName A DatabasePersisterConfig to configure the

src/@types/persisters/persister-durable-object-sql-storage/docs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@
233233
* See the documentation for the DpcJson, DpcFragmented, and DpcTabular types
234234
* for more information on how all of those modes can be configured.
235235
*
236+
* Note: When using tabular mode, SQL NULL values are loaded as TinyBase null
237+
* values, making tables dense (every Row has every Cell). See the Database
238+
* Persistence guide for details.
239+
*
236240
* As well as providing a reference to the Store or MergeableStore to persist,
237241
* you must provide a `sqlStorage` parameter which identifies the Durable Object
238242
* SQLite storage to persist it to.

src/@types/persisters/persister-electric-sql/docs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
*
8181
* See the documentation for the DpcJson and DpcTabular types for more
8282
* information on how both of those modes can be configured.
83+
*
84+
* Note: When using tabular mode, SQL NULL values are loaded as TinyBase null
85+
* values, making tables dense (every Row has every Cell). See the Database
86+
* Persistence guide for details.
8387
* @param store The Store to persist.
8488
* @param electricClient The Electric client that was returned from `await
8589
* electrify(...)`.

0 commit comments

Comments
 (0)