Skip to content

Commit 2a5b310

Browse files
committed
[sqlite] Incremental saves
1 parent 3fe4fb6 commit 2a5b310

File tree

4 files changed

+614
-37
lines changed

4 files changed

+614
-37
lines changed

src/persisters/sqlite/commands.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {COMMA, EMPTY_STRING, strRepeat} from '../../common/strings';
2+
import {Cell, Table} from '../../types/store';
23
import {
34
IdMap2,
45
mapEnsure,
@@ -30,7 +31,6 @@ import {collDel, collHas, collValues} from '../../common/coll';
3031
import {isUndefined, promiseAll} from '../../common/other';
3132
import {setAdd, setNew} from '../../common/set';
3233
import {Id} from '../../types/common';
33-
import {Table} from '../../types/store';
3434
import {escapeId} from './common';
3535

3636
export type Cmd = (sql: string, args?: any[]) => Promise<IdObj<any>[]>;
@@ -49,7 +49,7 @@ export const getCommandFunctions = (
4949
saveTable: (
5050
tableName: string,
5151
rowIdColumnName: string,
52-
table: Table,
52+
table: Table | {[rowId: Id]: {[cellId: Id]: Cell | null} | null} | null,
5353
deleteEmptyColumns: boolean,
5454
deleteEmptyTable: boolean,
5555
partial?: boolean,
@@ -130,19 +130,20 @@ export const getCommandFunctions = (
130130
const saveTable = async (
131131
tableName: string,
132132
rowIdColumnName: string,
133-
table: Table,
133+
table: Table | {[rowId: Id]: {[cellId: Id]: Cell | null} | null} | null,
134134
deleteEmptyColumns: boolean,
135135
deleteEmptyTable: boolean,
136136
partial = false,
137137
): Promise<void> => {
138-
const cellIds = setNew<string>();
138+
const tableCellIds = setNew<string>();
139139
objMap(table ?? {}, (row) =>
140-
arrayMap(objIds(row), (cellId) => setAdd(cellIds, cellId)),
140+
arrayMap(objIds(row ?? {}), (cellId) => setAdd(tableCellIds, cellId)),
141141
);
142-
const tableColumnNames = collValues(cellIds);
142+
const tableColumnNames = collValues(tableCellIds);
143143

144144
// Delete the table
145145
if (
146+
!partial &&
146147
deleteEmptyTable &&
147148
arrayIsEmpty(tableColumnNames) &&
148149
collHas(schemaMap, tableName)
@@ -183,7 +184,7 @@ export const getCommandFunctions = (
183184
mapSet(tableSchemaMap, columnName, EMPTY_STRING);
184185
}
185186
}),
186-
...(deleteEmptyColumns
187+
...(!partial && deleteEmptyColumns
187188
? arrayMap(
188189
collValues(columnNamesAccountedFor),
189190
async (columnName) => {
@@ -202,30 +203,43 @@ export const getCommandFunctions = (
202203
}
203204

204205
// Insert or update or delete data
205-
if (!arrayIsEmpty(tableColumnNames)) {
206-
if (partial) {
206+
if (partial) {
207+
if (isUndefined(table)) {
208+
await cmd('DELETE FROM' + escapeId(tableName) + 'WHERE 1');
209+
} else {
207210
await promiseAll(
208-
objMap(
209-
table,
210-
async (row, rowId) =>
211+
objMap(table, async (row, rowId) => {
212+
if (isUndefined(row)) {
213+
await cmd(
214+
'DELETE FROM' +
215+
escapeId(tableName) +
216+
WHERE +
217+
escapeId(rowIdColumnName) +
218+
'=?',
219+
[rowId],
220+
);
221+
} else if (!arrayIsEmpty(tableColumnNames)) {
211222
await upsert(cmd, tableName, rowIdColumnName, objIds(row), [
212223
rowId,
213224
...objValues(row),
214-
]),
215-
),
225+
]);
226+
}
227+
}),
216228
);
217-
} else {
229+
}
230+
} else {
231+
if (!arrayIsEmpty(tableColumnNames)) {
218232
const changingColumnNames = arrayFilter(
219233
mapKeys(mapGet(schemaMap, tableName)),
220234
(columnName) => columnName != rowIdColumnName,
221235
);
222236
const args: any[] = [];
223237
const deleteRowIds: string[] = [];
224-
objMap(table, (row, rowId) => {
238+
objMap(table ?? {}, (row, rowId) => {
225239
arrayPush(
226240
args,
227241
rowId,
228-
...arrayMap(changingColumnNames, (cellId) => row[cellId]),
242+
...arrayMap(changingColumnNames, (cellId) => row?.[cellId]),
229243
);
230244
arrayPush(deleteRowIds, rowId);
231245
});
@@ -246,9 +260,9 @@ export const getCommandFunctions = (
246260
')',
247261
deleteRowIds,
248262
);
263+
} else if (collHas(schemaMap, tableName)) {
264+
await cmd('DELETE FROM' + escapeId(tableName) + 'WHERE 1');
249265
}
250-
} else if (!partial && collHas(schemaMap, tableName)) {
251-
await cmd('DELETE FROM' + escapeId(tableName));
252266
}
253267
};
254268

src/persisters/sqlite/tabular.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
import {
2+
Cell,
3+
GetTransactionChanges,
4+
Store,
5+
Tables,
6+
Value,
7+
Values,
8+
} from '../../types/store';
19
import {Cmd, getCommandFunctions} from './commands';
210
import {DEFAULT_ROW_ID_COLUMN_NAME, SINGLE_ROW_ID} from './common';
3-
import {GetTransactionChanges, Store, Tables, Values} from '../../types/store';
411
import {Persister, PersisterListener} from '../../types/persisters';
512
import {isUndefined, promiseAll} from '../../common/other';
613
import {objIsEmpty, objNew} from '../../common/obj';
714
import {DefaultedTabularConfig} from './config';
15+
import {Id} from '../../types/common';
816
import {arrayFilter} from '../../common/array';
917
import {createCustomPersister} from '../../persisters';
1018
import {mapMap} from '../../common/map';
@@ -26,32 +34,54 @@ export const createTabularSqlitePersister = <ListeningHandle>(
2634
managedTableNames,
2735
);
2836

29-
const saveTables = async (tables: Tables) =>
37+
const saveTables = async (
38+
tables:
39+
| Tables
40+
| {
41+
[tableId: Id]: {
42+
[rowId: Id]: {[cellId: Id]: Cell | null} | null;
43+
} | null;
44+
},
45+
partial?: boolean,
46+
) =>
3047
await promiseAll(
3148
mapMap(
3249
tablesSaveConfig,
3350
async (
3451
[tableName, rowIdColumnName, deleteEmptyColumns, deleteEmptyTable],
3552
tableId,
36-
) =>
37-
await saveTable(
38-
tableName,
39-
rowIdColumnName,
40-
tables[tableId],
41-
deleteEmptyColumns,
42-
deleteEmptyTable,
43-
),
53+
) => {
54+
const table = tables[tableId];
55+
if (!partial || table !== undefined) {
56+
await saveTable(
57+
tableName,
58+
rowIdColumnName,
59+
table,
60+
deleteEmptyColumns,
61+
deleteEmptyTable,
62+
partial,
63+
);
64+
}
65+
},
4466
),
4567
);
4668

47-
const saveValues = async (values: Values) =>
69+
const saveValues = async (
70+
values:
71+
| Values
72+
| {
73+
[valueId: Id]: Value | null;
74+
},
75+
partial?: boolean,
76+
) =>
4877
valuesSave
4978
? await saveTable(
5079
valuesTableName,
5180
DEFAULT_ROW_ID_COLUMN_NAME,
5281
{[SINGLE_ROW_ID]: values},
5382
true,
5483
true,
84+
partial,
5585
)
5686
: null;
5787

@@ -93,11 +123,14 @@ export const createTabularSqlitePersister = <ListeningHandle>(
93123
): Promise<void> => {
94124
await refreshSchema();
95125
if (!isUndefined(getTransactionChanges)) {
96-
// const [cellChanges, valueChanges] = getTransactionChanges();
126+
const [tableChanges, valueChanges] = getTransactionChanges();
127+
await saveTables(tableChanges, true);
128+
await saveValues(valueChanges, true);
129+
} else {
130+
const [tables, values] = getContent();
131+
await saveTables(tables);
132+
await saveValues(values);
97133
}
98-
const [tables, values] = getContent();
99-
await saveTables(tables);
100-
await saveValues(values);
101134
};
102135

103136
const persister: any = createCustomPersister(

src/types/docs/persisters.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,13 @@
385385
* enable the `deleteEmptyColumns` or `deleteEmptyTable` settings, you can
386386
* simply provide the name of the database table instead of the whole object.
387387
*
388+
* `deleteEmptyColumns` and `deleteEmptyTable` only have a guaranteed effect
389+
* when an explicit call is made to the Persister's save method. Columns and
390+
* tables will not necessarily be removed when the Persister is incrementally
391+
* 'autoSaving', due to performance reasons. If you want to be sure that your
392+
* database table matches a TinyBase Table without any extraneous columns,
393+
* simply call the save method at an idle moment.
394+
*
388395
* The 'Dpc' prefix indicates that this type is used within the
389396
* DatabasePersisterConfig type.
390397
* @example

0 commit comments

Comments
 (0)