Skip to content

Commit

Permalink
2.0: Clean up naming
Browse files Browse the repository at this point in the history
  • Loading branch information
tp committed Nov 7, 2024
1 parent af2d065 commit 0603dc9
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 273 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 2.0.0

* Update method names
* Most notably `insert` is now `write` to transmit the ambivalence between "insert" and "update"
* `get` is now `read` to be symmetric to write (but instead of entities it takes keys, of course)
* `query` now takes an optional `where` parameter, so instead of `getAll()` you can now just use `query()` to get the same result
* `delete` now bundles multiple ways to identify entities to delete, offering a simpler API surface for the overall store

## 1.4.7

* Add `contains` functions for `String` index columns
Expand Down
4 changes: 2 additions & 2 deletions example/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ final db = IndexedEntityDabase.open('/tmp/appdata.sqlite3'); // in practice put
final todos = db.entityStore(todoConnector);
// While using the String columns here is not super nice, this works without code gen and will throw if using a non-indexed column
final openTodos = todos.query((cols) => cols['done'].equals(false));
final openTodos = todos.query(where: (cols) => cols['done'].equals(false));
print(openTodos.value); // prints an empty list on first run as no TODOs are yet added to the database
todos.insert(
todos.write(
Todo(id: 1, text: 'Publish new version', done: false),
);
Expand Down
4 changes: 2 additions & 2 deletions example/lib/src/examples/async_value_group_and_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ class AsyncProductRepository {
DisposableValueListenable<AsyncValue<ProductDetail>> getProductDetail(
int productId,
) {
final productQuery = _detailStore.get(productId);
final productQuery = _detailStore.read(productId);

if (productQuery.value != null &&
// If data is older than 30s, run the fetch below (but show the most recent value while it's loading)
Expand All @@ -194,7 +194,7 @@ class AsyncProductRepository {
_productApi.getProductDetail(productId).then((product) {
// When we receive the product successfully, we put it into the database,
// from which point on the product query will deliver it to the outside
_detailStore.insert(product);
_detailStore.write(product);

return product;
}).asAsyncValue(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,13 @@ class AsyncProductRepository {
Future<ValueSource<ProductDetail>> getProductDetail(
int productId,
) async {
final product = _detailStore.get(productId);
final product = _detailStore.read(productId);

if (product.value == null) {
try {
final remoteEvent = await _productApi.getProductDetail(productId);

_detailStore.insert(remoteEvent);
_detailStore.write(remoteEvent);
} catch (e) {
product.dispose(); // failed to load the data, close view to database

Expand All @@ -180,7 +180,7 @@ class AsyncProductRepository {
'Local product is outdated, fetching new one in the background',
);

_productApi.getProductDetail(productId).then(_detailStore.insert);
_productApi.getProductDetail(productId).then(_detailStore.write);
}

// If we reached this, we now know that we have a value in the local database, and we don't expect it to ever be deleted in this case, and thus can "force unwrap" it.
Expand Down
10 changes: 5 additions & 5 deletions example/lib/src/examples/simple_synchronous.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,16 @@ class SimpleSynchronousRepository {
}) : _store = store;

void init() {
_store.insert(Todo(id: 1, text: 'Brew Coffe', done: false));
_store.insert(Todo(id: 2, text: 'Get milk', done: false));
_store.insert(Todo(id: 3, text: 'Read newspaper', done: false));
_store.write(Todo(id: 1, text: 'Brew Coffe', done: false));
_store.write(Todo(id: 2, text: 'Get milk', done: false));
_store.write(Todo(id: 3, text: 'Read newspaper', done: false));
}

QueryResult<List<Todo>> getOpenTodos() {
return _store.query((cols) => cols['done'].equals(false));
return _store.query(where: (cols) => cols['done'].equals(false));
}

void updateTodo(Todo todo) {
_store.insert(todo);
_store.write(todo);
}
}
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.4.7"
version: "2.0.0-dev2"
leak_tracker:
dependency: transitive
description:
Expand Down
183 changes: 80 additions & 103 deletions lib/src/index_entity_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class IndexedEntityStore<T, K> {
}

/// Returns a subscription to a single entity by its primary key
QueryResult<T?> get(K key) {
QueryResult<T?> read(K key) {
final QueryResultMapping<T?> mapping = (
() => _getOnce(key),
QueryResult._(
Expand All @@ -75,7 +75,7 @@ class IndexedEntityStore<T, K> {
}

/// Returns a single entity by its primary key
T? getOnce(K key) {
T? readOnce(K key) {
return _getOnce(key).result;
}

Expand All @@ -94,68 +94,18 @@ class IndexedEntityStore<T, K> {
return (dbValues: [dbValue], result: _connector.deserialize(dbValue));
}

/// Returns a subscription to all entities in this store
QueryResult<List<T>> getAll({
OrderByClause? orderBy,
}) {
final QueryResultMapping<List<T>> mapping = (
() => _getAllOnce(orderBy: orderBy),
QueryResult._(
initialValue: _getAllOnce(orderBy: orderBy),
onDispose: (r) {
_entityResults.removeWhere((m) => m.$2 == r);
},
)
);

_entityResults.add(mapping);

return mapping.$2;
}

/// Returns a list of all entities in this store
List<T> getAllOnce({
OrderByClause? orderBy,
}) {
return _getAllOnce(orderBy: orderBy).result;
}

MappedDBResult<List<T>> _getAllOnce({
OrderByClause? orderBy,
}) {
final res = _database.select(
[
'SELECT `entity`.`value` FROM `entity`',
if (orderBy != null)
' JOIN `index` ON `index`.`entity` = `entity`.`key` ',
' WHERE `entity`.`type` = ? ',
if (orderBy != null)
' AND `index`.`field` = ? ORDER BY `index`.`value` ${orderBy.$2 == SortOrder.asc ? 'ASC' : 'DESC'}',
].join(),
[
_entityKey,
if (orderBy != null) orderBy.$1,
],
);

final values = res.map((e) => e['value']).toList();

return (
dbValues: values,
result: values.map((v) => _connector.deserialize(v)).toList()
);
}

/// Returns the single entity (or null) for the given query
///
/// Throws an exception if the query returns 2 or more values.
/// If the caller expects more than 1 value but is only interested in one,
/// they can use [query] with a limit instead.
QueryResult<T?> single(QueryBuilder q) {
/// they can use [query] with a `limit: 1` instead.
QueryResult<T?> single({
required QueryBuilder where,
}) {
final QueryResultMapping<T?> mapping = (
() => _singleOnce(q),
() => _singleOnce(where: where),
QueryResult._(
initialValue: _singleOnce(q),
initialValue: _singleOnce(where: where),
onDispose: (r) {
_entityResults.removeWhere((m) => m.$2 == r);
},
Expand All @@ -172,32 +122,36 @@ class IndexedEntityStore<T, K> {
/// Throws an exception if the query returns 2 or more values.
/// If the caller expects more than 1 value but is only interested in one,
/// they can use [query] with a limit instead.
T? singleOnce(QueryBuilder q) {
return _singleOnce(q).result;
T? singleOnce({
required QueryBuilder where,
}) {
return _singleOnce(where: where).result;
}

MappedDBResult<T?> _singleOnce(QueryBuilder q) {
final result = _queryOnce(q, limit: 2);
MappedDBResult<T?> _singleOnce({
required QueryBuilder where,
}) {
final result = _queryOnce(where: where, limit: 2);

if (result.result.length > 1) {
throw Exception(
'singleOnce expected to find one element, but found at least 2 matching the query $q',
'singleOnce expected to find one element, but found at least 2 matching the query $where',
);
}

return (dbValues: result.dbValues, result: result.result.firstOrNull);
}

/// Returns a subscription to entities matching the given query
QueryResult<List<T>> query(
QueryBuilder q, {
QueryResult<List<T>> query({
QueryBuilder? where,
OrderByClause? orderBy,
int? limit,
}) {
final QueryResultMapping<List<T>> mapping = (
() => _queryOnce(q, limit: limit, orderBy: orderBy),
() => _queryOnce(where: where, limit: limit, orderBy: orderBy),
QueryResult._(
initialValue: _queryOnce(q, limit: limit, orderBy: orderBy),
initialValue: _queryOnce(where: where, limit: limit, orderBy: orderBy),
onDispose: (r) {
_entityResults.removeWhere((m) => m.$2 == r);
},
Expand All @@ -210,33 +164,34 @@ class IndexedEntityStore<T, K> {
}

/// Returns a list of entities matching the given query
List<T> queryOnce(
QueryBuilder q, {
List<T> queryOnce({
QueryBuilder? where,
OrderByClause? orderBy,
int? limit,
}) {
return _queryOnce(q, orderBy: orderBy, limit: limit).result;
return _queryOnce(where: where, orderBy: orderBy, limit: limit).result;
}

MappedDBResult<List<T>> _queryOnce(
QueryBuilder q, {
MappedDBResult<List<T>> _queryOnce({
QueryBuilder? where,
OrderByClause? orderBy,
int? limit,
}) {
final (w, s) = q(_indexColumns)._entityKeysQuery();
final whereClause = where?.call(_indexColumns)._entityKeysQuery();

final query = [
'SELECT `entity`.`value` FROM `entity` ',
if (orderBy != null)
' JOIN `index` ON `index`.`entity` = `entity`.`key` ',
' WHERE `entity`.`type` = ? AND `entity`.`key` IN ( $w ) ',
' WHERE `entity`.`type` = ? ',
if (whereClause != null) ' AND `entity`.`key` IN ( ${whereClause.$1} ) ',
if (orderBy != null)
'AND `index`.`field` = ? ORDER BY `index`.`value` ${orderBy.$2 == SortOrder.asc ? 'ASC' : 'DESC'}',
if (limit != null) ' LIMIT ?'
].join();
final values = [
_entityKey,
...s,
...?whereClause?.$2,
if (orderBy != null) orderBy.$1,
if (limit != null) limit,
];
Expand All @@ -260,7 +215,7 @@ class IndexedEntityStore<T, K> {
///
/// In case an entity with the same primary already exists in the database, it will be updated.
// TODO(tp): We might want to rename this to `upsert` going forward to make it clear that this will overwrite and not error when the entry already exits (alternatively maybe `persist`, `write`, or `set`).
void insert(T e) {
void write(T e) {
try {
_database.execute('BEGIN');
assert(_database.autocommit == false);
Expand All @@ -285,7 +240,7 @@ class IndexedEntityStore<T, K> {
/// Insert or update many entities in a single batch
///
/// Notification for changes will only fire after all changes have been written (meaning queries will get a single update after all writes are finished)
void insertMany(Iterable<T> entities) {
void writeMany(Iterable<T> entities) {
final keys = <K>{};

try {
Expand Down Expand Up @@ -330,8 +285,39 @@ class IndexedEntityStore<T, K> {
}
}

/// Delete the specified entries
void delete({
final T? entity,
final Iterable<T>? entities,
final K? key,
final Iterable<K>? keys,
// TODO(tp): QueryBuilder? where,
final bool? all,
}) {
assert(entity != null ||
entities != null ||
key != null ||
keys != null ||
all != null);
assert(
all == null ||
(entity == null && entities == null && key == null && keys == null),
);

if (all == true) {
_deleteAll();
} else {
_deleteManyByKey({
if (entity != null) _connector.getPrimaryKey(entity),
...?entities?.map(_connector.getPrimaryKey),
if (key != null) key,
...?keys,
});
}
}

/// Removes all entries from the store
void deleteAll() {
void _deleteAll() {
final result = _database.select(
'DELETE FROM `entity` WHERE `type` = ? RETURNING `key`',
[_entityKey],
Expand All @@ -344,34 +330,25 @@ class IndexedEntityStore<T, K> {
);
}

/// Deletes a single entity by its primary key
void delete(K key) {
deleteMany({key});
}
/// Deletes many entities by their primary key
void _deleteManyByKey(Set<K> keys) {
try {
_database.execute('BEGIN');

/// Deletes a single entity
void deleteEntity(T entity) {
delete(_connector.getPrimaryKey(entity));
}
for (final key in keys) {
_database.execute(
'DELETE FROM `entity` WHERE `type` = ? AND `key` = ?',
[_entityKey, key],
);

/// Deletes many entities
void deleteEntities(Iterable<T> entities) {
deleteMany(
{
for (final e in entities) _connector.getPrimaryKey(e),
},
);
}
_assertNoMoreIndexEntries(key);
}

/// Deletes many entities by their primary key
void deleteMany(Set<K> keys) {
for (final key in keys) {
_database.execute(
'DELETE FROM `entity` WHERE `type` = ? AND `key` = ?',
[_entityKey, key],
);
_database.execute('COMMIT');
} catch (e) {
_database.execute('ROLLBACK');

_assertNoMoreIndexEntries(key);
rethrow;
}

_handleUpdate(keys);
Expand Down Expand Up @@ -414,7 +391,7 @@ class IndexedEntityStore<T, K> {
[_entityKey],
);

final entities = getAllOnce();
final entities = queryOnce();

for (final e in entities) {
_updateIndexInternal(e);
Expand Down
Loading

0 comments on commit 0603dc9

Please sign in to comment.