-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2487 from nextcloud/feat/neon_framework/sotrage_l…
…ibrary
- Loading branch information
Showing
19 changed files
with
573 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../../../assets/AGPL-3.0.txt |
5 changes: 5 additions & 0 deletions
5
packages/neon_framework/packages/neon_storage/analysis_options.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
include: package:neon_lints/dart.yaml | ||
|
||
custom_lint: | ||
rules: | ||
- avoid_exports: false |
4 changes: 4 additions & 0 deletions
4
packages/neon_framework/packages/neon_storage/lib/neon_sqlite.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/// SQLite wrappers for working with multiple tables independently. | ||
library; | ||
|
||
export 'src/sqlite/sqlite.dart' hide setupDatabase; |
7 changes: 7 additions & 0 deletions
7
packages/neon_framework/packages/neon_storage/lib/src/sqlite/_browser_database_factory.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import 'package:sqflite_common/sqflite.dart'; | ||
import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart'; | ||
|
||
/// Initializes the sqlite database factory with the [databaseFactoryFfiWeb]. | ||
void setupDatabase() { | ||
databaseFactory = databaseFactoryFfiWeb; | ||
} |
8 changes: 8 additions & 0 deletions
8
packages/neon_framework/packages/neon_storage/lib/src/sqlite/_io_database_factory.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; | ||
|
||
/// Initializes the sqlite database factory with the [databaseFactoryFfi]. | ||
void setupDatabase() { | ||
sqfliteFfiInit(); | ||
databaseFactory = databaseFactoryFfi; | ||
} |
7 changes: 7 additions & 0 deletions
7
packages/neon_framework/packages/neon_storage/lib/src/sqlite/database_factory.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import 'package:meta/meta.dart'; | ||
|
||
/// Initializes the sqlite database factory. | ||
@internal | ||
void setupDatabase() { | ||
throw UnsupportedError('Cannot create a database factory without dart:js_interop or dart:io.'); | ||
} |
19 changes: 19 additions & 0 deletions
19
packages/neon_framework/packages/neon_storage/lib/src/sqlite/database_path_utils.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import 'package:path/path.dart' as p; | ||
|
||
/// The default database file extension. | ||
const String databaseExtension = '.db'; | ||
|
||
/// Builds the database storage path from a directory and file name. | ||
String buildDatabasePath(String? directory, String name) { | ||
if (p.basenameWithoutExtension(name) != name) { | ||
throw ArgumentError.value(name, 'name', 'MUST NOT contain any directory or file extension.'); | ||
} | ||
|
||
final nameWithExtension = p.setExtension(name, databaseExtension); | ||
|
||
if (directory == null) { | ||
return nameWithExtension; | ||
} | ||
|
||
return p.join(directory, nameWithExtension); | ||
} |
170 changes: 170 additions & 0 deletions
170
packages/neon_framework/packages/neon_storage/lib/src/sqlite/multi_table_database.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:built_collection/built_collection.dart'; | ||
import 'package:meta/meta.dart'; | ||
import 'package:neon_storage/src/sqlite/sqlite.dart'; | ||
import 'package:sqflite_common/sqflite.dart'; | ||
|
||
/// A wrapper for the SQLite [Database] managing multiple tables. | ||
abstract class MultiTableDatabase { | ||
/// Creates a new database for the given [tables]. | ||
/// | ||
/// The provided tables must have distinct names or an `ArgumentError` is thrown. | ||
MultiTableDatabase({ | ||
required Iterable<Table> tables, | ||
}) : _tables = BuiltList.from(tables) { | ||
final names = _tables.map((t) => t.name).toSet(); | ||
|
||
if (names.length != _tables.length || names.contains(_metaTable)) { | ||
throw ArgumentError.value(_tables, 'tables', 'contains conflicting table names'); | ||
} | ||
} | ||
|
||
/// The full storage path of the database. | ||
@protected | ||
FutureOr<String> get path; | ||
|
||
/// The name of the database without any file extension. | ||
@protected | ||
String get name; | ||
|
||
/// When `true` all open parameters are ignored and the database is opened as-is. | ||
@protected | ||
bool get readOnly => false; | ||
|
||
/// When `true` (the default), a single database instance is opened for a | ||
/// given path. Subsequent calls to [init] with the same path will | ||
/// return the same instance, and will discard all other parameters such as | ||
/// callbacks for that invocation. | ||
@protected | ||
bool get singleInstance => true; | ||
|
||
static bool _sqfliteInitialized = false; | ||
|
||
late final _metaTable = '_${name}_meta'; | ||
|
||
final BuiltList<Table> _tables; | ||
|
||
Database? _database; | ||
|
||
/// Throws a `StateError` if [init] has not been called or completed before. | ||
@visibleForTesting | ||
@protected | ||
void assertInitialized() { | ||
if (_database == null) { | ||
throw StateError( | ||
'The database "$name" has not been set up. Make sure init() has been called before and completed.', | ||
); | ||
} | ||
} | ||
|
||
/// The database instance. | ||
/// | ||
/// Throws a `StateError` if [init] has not been called or completed before. | ||
Database get database { | ||
assertInitialized(); | ||
|
||
return _database!; | ||
} | ||
|
||
/// Closes the database. | ||
/// | ||
/// A closed database must be initialized again by calling [init]. | ||
@visibleForTesting | ||
Future<void> close() async { | ||
await _database?.close(); | ||
_database = null; | ||
} | ||
|
||
/// Initializes the database and all tables. | ||
/// | ||
/// This must called and completed before accessing other methods. | ||
Future<void> init() async { | ||
if (_database != null) { | ||
return; | ||
} | ||
|
||
if (!_sqfliteInitialized) { | ||
setupDatabase(); | ||
_sqfliteInitialized = true; | ||
} | ||
|
||
final database = await openDatabase( | ||
await path, | ||
version: 1, | ||
onCreate: _createMetaTable, | ||
readOnly: readOnly, | ||
singleInstance: singleInstance, | ||
); | ||
|
||
await _createTables(database); | ||
|
||
for (final table in _tables) { | ||
table.controller = this; | ||
} | ||
await Future.wait( | ||
_tables.map((t) => t.onOpen()), | ||
); | ||
|
||
_database = database; | ||
} | ||
|
||
Future<void> _createMetaTable(Database db, int version) async { | ||
await db.execute(''' | ||
CREATE TABLE IF NOT EXISTS "$_metaTable" ( | ||
"name" TEXT NOT NULL, | ||
"version" INTEGER NOT NULL, | ||
PRIMARY KEY("name") | ||
); | ||
'''); | ||
} | ||
|
||
Future<void> _createTables(Database db) async { | ||
final rows = await db.query(_metaTable); | ||
|
||
final versions = <String, int>{}; | ||
for (final row in rows) { | ||
final name = row['name']! as String; | ||
final version = row['version']! as int; | ||
|
||
versions[name] = version; | ||
} | ||
|
||
final batch = db.batch(); | ||
|
||
for (final table in _tables) { | ||
final oldVersion = versions[table.name]; | ||
final newVersion = table.version; | ||
|
||
if (oldVersion == null) { | ||
table.onCreate(batch, newVersion); | ||
batch.insert( | ||
_metaTable, | ||
{ | ||
'name': table.name, | ||
'version': newVersion, | ||
}, | ||
); | ||
continue; | ||
} else if (oldVersion == newVersion) { | ||
continue; | ||
} else if (oldVersion > newVersion) { | ||
table.onDowngrade(batch, oldVersion, newVersion); | ||
} else if (oldVersion < newVersion) { | ||
table.onUpgrade(batch, oldVersion, newVersion); | ||
} | ||
|
||
batch.update( | ||
_metaTable, | ||
{ | ||
'name': table.name, | ||
'version': newVersion, | ||
}, | ||
where: 'name = ?', | ||
whereArgs: [table.name], | ||
); | ||
} | ||
|
||
await batch.commit(noResult: true); | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
packages/neon_framework/packages/neon_storage/lib/src/sqlite/sqlite.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export 'database_factory.dart' | ||
if (dart.library.js_interop) '_browser_database_factory.dart' | ||
if (dart.library.io) '_io_database_factory.dart'; | ||
|
||
export 'database_path_utils.dart'; | ||
export 'multi_table_database.dart'; | ||
export 'table.dart'; |
42 changes: 42 additions & 0 deletions
42
packages/neon_framework/packages/neon_storage/lib/src/sqlite/table.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:neon_storage/src/sqlite/sqlite.dart'; | ||
import 'package:sqflite_common/sqlite_api.dart'; | ||
|
||
/// A SQLite table abstraction that mimics the `openDatabase` function from the sqflite package. | ||
/// | ||
/// Depending on the current [version], [onCreate], [onUpgrade], and [onDowngrade] can | ||
/// be called. These functions are mutually exclusive — only one of them can be | ||
/// called depending on the context. | ||
abstract mixin class Table { | ||
/// The name of the table. | ||
String get name; | ||
|
||
/// The value must be a 32-bit integer greater than `0`. | ||
int get version; | ||
|
||
/// The owning multi table database. | ||
late MultiTableDatabase controller; | ||
|
||
/// A callback for creating the required schema for the table. | ||
/// | ||
/// It is only called called if the table did not exist prior. | ||
void onCreate(Batch db, int version) {} | ||
|
||
/// A callback for upgrading the schema of a table. | ||
/// | ||
/// It is only called called if the database already exists and [version] is | ||
/// higher than the last database version. | ||
void onUpgrade(Batch db, int oldVersion, int newVersion) {} | ||
|
||
/// A callback for downgrading the required schema for the table. | ||
/// | ||
/// It is only called called when [version] is lower than the last database | ||
/// version. This is a rare case and should only come up if a newer version of | ||
/// your code has created a database that is then interacted with by an older | ||
/// version of your code. You should try to avoid this scenario. | ||
void onDowngrade(Batch db, int oldVersion, int newVersion) {} | ||
|
||
/// A callback invoked after all the table versions are set. | ||
Future<void> onOpen() async {} | ||
} |
20 changes: 20 additions & 0 deletions
20
packages/neon_framework/packages/neon_storage/lib/src/testing/test_table_database.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import 'package:meta/meta.dart'; | ||
import 'package:neon_storage/neon_sqlite.dart'; | ||
import 'package:sqflite_common/sqlite_api.dart'; | ||
|
||
/// A test implementation for testing a [Table]. | ||
@visibleForTesting | ||
final class TestTableDatabase extends MultiTableDatabase { | ||
/// Creates a test database for the given [table]. | ||
TestTableDatabase(Table table) | ||
: _table = table, | ||
super(tables: [table]); | ||
|
||
final Table _table; | ||
|
||
@override | ||
String get name => '${_table.name}_database'; | ||
|
||
@override | ||
String get path => inMemoryDatabasePath; | ||
} |
1 change: 1 addition & 0 deletions
1
packages/neon_framework/packages/neon_storage/lib/src/testing/testing.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export 'test_table_database.dart'; |
7 changes: 7 additions & 0 deletions
7
packages/neon_framework/packages/neon_storage/lib/testing.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/// This library contains testing helpers for the neon storage. | ||
@visibleForTesting | ||
library; | ||
|
||
import 'package:meta/meta.dart'; | ||
|
||
export 'src/testing/testing.dart'; |
34 changes: 34 additions & 0 deletions
34
packages/neon_framework/packages/neon_storage/pubspec.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: neon_storage | ||
description: Storage management and abstractions for the neon framework. | ||
version: 0.1.0 | ||
publish_to: none | ||
|
||
environment: | ||
sdk: ^3.0.0 | ||
|
||
dependencies: | ||
built_collection: ^5.0.0 | ||
cookie_store: | ||
git: | ||
url: https://github.com/nextcloud/neon | ||
path: packages/cookie_store | ||
http: ^1.0.0 | ||
logging: ^1.0.0 | ||
meta: ^1.0.0 | ||
nextcloud: ^7.0.0 | ||
path: ^1.9.0 | ||
sqflite_common: ^2.0.0 | ||
sqflite_common_ffi: ^2.3.3 | ||
sqflite_common_ffi_web: ^0.4.3+1 | ||
|
||
dev_dependencies: | ||
cookie_store_conformance_tests: | ||
git: | ||
url: https://github.com/nextcloud/neon | ||
path: packages/cookie_store_conformance_tests | ||
mocktail: ^1.0.4 | ||
neon_lints: | ||
git: | ||
url: https://github.com/nextcloud/neon | ||
path: packages/neon_lints | ||
test: ^1.25.8 |
12 changes: 12 additions & 0 deletions
12
packages/neon_framework/packages/neon_storage/pubspec_overrides.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# melos_managed_dependency_overrides: neon_lints,cookie_store,cookie_store_conformance_tests,dynamite_runtime,nextcloud | ||
dependency_overrides: | ||
cookie_store: | ||
path: ../../../cookie_store | ||
cookie_store_conformance_tests: | ||
path: ../../../cookie_store/packages/cookie_store_conformance_tests | ||
dynamite_runtime: | ||
path: ../../../dynamite/packages/dynamite_runtime | ||
neon_lints: | ||
path: ../../../neon_lints | ||
nextcloud: | ||
path: ../../../nextcloud |
26 changes: 26 additions & 0 deletions
26
packages/neon_framework/packages/neon_storage/test/sqlite/database_path_utils_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import 'package:neon_storage/neon_sqlite.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
test('buildDatabasePath', () { | ||
expect( | ||
() => buildDatabasePath(null, 'database.db'), | ||
throwsArgumentError, | ||
); | ||
|
||
expect( | ||
() => buildDatabasePath(null, 'tmp/database.db'), | ||
throwsArgumentError, | ||
); | ||
|
||
expect( | ||
buildDatabasePath(null, 'database'), | ||
equals('database.db'), | ||
); | ||
|
||
expect( | ||
buildDatabasePath('/tmp', 'database'), | ||
equals('/tmp/database.db'), | ||
); | ||
}); | ||
} |
Oops, something went wrong.