mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
Support PowerSync extension when analyzing SQL
This commit is contained in:
@@ -194,6 +194,8 @@ We currently support the following extensions:
|
||||
- `spellfix1`: Assumes that the [spellfix1](https://www.sqlite.org/spellfix1.html)
|
||||
module is available. Note that this is not the case for most sqlite3 builds,
|
||||
including the ones shipping with `sqlite3_flutter_libs`.
|
||||
- `powersync`: Assumes that the PowerSync SQLite extension is available, allowing references to e.g.
|
||||
`powersync_crud` in `CREATE TABLE` statements.
|
||||
|
||||
### Known custom functions
|
||||
|
||||
|
||||
@@ -86,6 +86,8 @@ class DriftAnalysisDriver {
|
||||
if (options.hasModule(SqlModule.spellfix1))
|
||||
const Spellfix1Extension(),
|
||||
if (options.hasModule(SqlModule.geopoly)) const GeopolyExtension(),
|
||||
if (options.hasModule(SqlModule.powersync))
|
||||
const PowerSyncSqliteExtension(),
|
||||
],
|
||||
version: options.sqliteVersion,
|
||||
),
|
||||
|
||||
@@ -451,6 +451,10 @@ enum SqlModule {
|
||||
/// Enables the dbstat table providing insights into the disk state occupied
|
||||
/// by certain tables.
|
||||
dbstat,
|
||||
|
||||
/// Enables static analysis support for PowerSync, providing the
|
||||
/// `powersync_crud` virtual table.
|
||||
powersync,
|
||||
}
|
||||
|
||||
/// The possible values for the case of the table and column names.
|
||||
|
||||
@@ -211,6 +211,7 @@ const _$SqlModuleEnumMap = {
|
||||
SqlModule.spellfix1: 'spellfix1',
|
||||
SqlModule.geopoly: 'geopoly',
|
||||
SqlModule.dbstat: 'dbstat',
|
||||
SqlModule.powersync: 'powersync',
|
||||
};
|
||||
|
||||
const _$CaseFromDartToSqlEnumMap = {
|
||||
|
||||
@@ -37,7 +37,7 @@ dependencies:
|
||||
# Drift-specific analysis and apis
|
||||
drift: ">=2.28.0 <2.29.0"
|
||||
sqlite3: ^2.4.6
|
||||
sqlparser: ^0.41.2
|
||||
sqlparser: ^0.42.0-0
|
||||
|
||||
# Dart analysis
|
||||
# Note: When changing this to a lower bound of 8.x, please re-enable
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:drift_dev/src/analysis/options.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
|
||||
void main() {
|
||||
test('can define triggers referencing powersync vtab', () async {
|
||||
final state = await TestBackend.inTest({
|
||||
'a|lib/main.drift': '''
|
||||
CREATE TABLE todo_lists (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_by TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE TRIGGER todo_lists_insert
|
||||
AFTER INSERT ON todo_lists
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PUT', NEW.id, 'todo_lists', json_object(
|
||||
'created_by', NEW.created_by,
|
||||
'title', NEW.title,
|
||||
'content', NEW.content
|
||||
));
|
||||
END;
|
||||
|
||||
CREATE TRIGGER todo_lists_update
|
||||
AFTER UPDATE ON todo_lists
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT CASE
|
||||
WHEN (OLD.id != NEW.id)
|
||||
THEN RAISE (FAIL, 'Cannot update id')
|
||||
END;
|
||||
|
||||
-- TODO: You may want to replace the json_object with a powersync_diff call of the old and new values, or
|
||||
-- use your own diff logic to avoid marking unchanged columns as updated.
|
||||
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PATCH', NEW.id, 'todo_lists', json_object(
|
||||
'created_by', NEW.created_by,
|
||||
'title', NEW.title,
|
||||
'content', NEW.content
|
||||
));
|
||||
END;
|
||||
|
||||
CREATE TRIGGER todo_lists_delete
|
||||
AFTER DELETE ON todo_lists
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'todo_lists');
|
||||
END;
|
||||
'''
|
||||
}, options: DriftOptions.defaults(modules: [SqlModule.powersync]));
|
||||
|
||||
await state.analyze('package:a/main.drift');
|
||||
state.expectNoErrors();
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
## 0.41.3-dev
|
||||
## 0.42.0-dev
|
||||
|
||||
- Fix unresolved references in CTEs resolving against the scope of the main query.
|
||||
- Static analysis support for the PowerSync SQLite extension.
|
||||
|
||||
## 0.41.2
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export 'src/engine/module/fts5.dart' show Fts5Extension, Fts5Table;
|
||||
export 'src/engine/module/geopoly.dart' show GeopolyExtension;
|
||||
export 'src/engine/module/json1.dart' show Json1Extension;
|
||||
export 'src/engine/module/math.dart' show BuiltInMathExtension;
|
||||
export 'src/engine/module/powersync.dart' show PowerSyncSqliteExtension;
|
||||
export 'src/engine/module/rtree.dart' show RTreeExtension;
|
||||
export 'src/engine/module/spellfix1.dart' show Spellfix1Extension;
|
||||
export 'src/engine/module/module.dart';
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:sqlparser/src/analysis/types/types.dart';
|
||||
import 'package:sqlparser/src/ast/ast.dart';
|
||||
import 'package:sqlparser/src/engine/sql_engine.dart';
|
||||
|
||||
import '../../analysis/analysis.dart';
|
||||
import 'module.dart';
|
||||
|
||||
/// Provides static analysis support for the [PowerSync SQLite core extension].
|
||||
///
|
||||
/// This provides the `powersync_crud` eponymous virtual table to store local
|
||||
/// mutation as well as some JSON / UUID helper functions.
|
||||
///
|
||||
/// [PowerSync SQLite core extension]: https://github.com/powersync-ja/powersync-sqlite-core/
|
||||
final class PowerSyncSqliteExtension implements Extension {
|
||||
const PowerSyncSqliteExtension();
|
||||
|
||||
@override
|
||||
void register(SqlEngine engine) {
|
||||
engine
|
||||
..registerTable(_powersyncCrudLegacy)
|
||||
..registerTable(_powersyncCrud)
|
||||
..registerFunctionHandler(const _PowerSyncFunctionHandler());
|
||||
}
|
||||
}
|
||||
|
||||
final class _PowerSyncFunctionHandler implements FunctionHandler {
|
||||
const _PowerSyncFunctionHandler();
|
||||
|
||||
@override
|
||||
Set<String> get functionNames => {
|
||||
'powersync_diff', // (TEXT, TEXT) -> TEXT
|
||||
'powersync_client_id', // () -> TEXT
|
||||
'powersync_in_sync_operation', // () -> BOOLEAN
|
||||
'gen_random_uuid', // () -> TEXT
|
||||
'uuid', // () -> TEXT
|
||||
};
|
||||
|
||||
@override
|
||||
ResolveResult inferArgumentType(
|
||||
TypeInferenceSession session, SqlInvocation call, Expression argument) {
|
||||
switch (call.name.toLowerCase()) {
|
||||
case 'powersync_diff':
|
||||
return ResolveResult(_text);
|
||||
default:
|
||||
return ResolveResult.unknown();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ResolveResult inferReturnType(TypeInferenceSession session,
|
||||
SqlInvocation call, List<Typeable> expandedArgs) {
|
||||
switch (call.name.toLowerCase()) {
|
||||
case 'powersync_diff':
|
||||
case 'powersync_client_id':
|
||||
case 'gen_random_uuid':
|
||||
case 'uuid':
|
||||
return const ResolveResult(_text);
|
||||
case 'powersync_in_sync_operation':
|
||||
return const ResolveResult(ResolvedType.bool());
|
||||
default:
|
||||
return ResolveResult.unknown();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void reportErrors(SqlInvocation call, AnalysisContext context) {}
|
||||
}
|
||||
|
||||
/// `CREATE TABLE powersync_crud_(data TEXT, options INT HIDDEN);`
|
||||
///
|
||||
/// Source: https://github.com/powersync-ja/powersync-sqlite-core/blob/637fda0f1d84c46964736a4f8c59fab5ae27e304/crates/core/src/crud_vtab.rs#L26
|
||||
Table get _powersyncCrudLegacy {
|
||||
return Table(name: 'powersync_crud', isVirtual: true, resolvedColumns: [
|
||||
TableColumn('data', _text),
|
||||
TableColumn('options', _int, isHidden: true),
|
||||
]);
|
||||
}
|
||||
|
||||
/// `CREATE TABLE powersync_crud(op TEXT, id TEXT, type TEXT, data TEXT, old_values TEXT, metadata TEXT, options INT HIDDEN);`
|
||||
///
|
||||
/// Source: https://github.com/powersync-ja/powersync-sqlite-core/blob/637fda0f1d84c46964736a4f8c59fab5ae27e304/crates/core/src/crud_vtab.rs#L27C6-L27C126
|
||||
Table get _powersyncCrud {
|
||||
return Table(name: 'powersync_crud', isVirtual: true, resolvedColumns: [
|
||||
TableColumn('op', _text),
|
||||
TableColumn('id', _text),
|
||||
TableColumn('type', _text),
|
||||
TableColumn('data', _text),
|
||||
TableColumn('old_values', _text),
|
||||
TableColumn('metadata', _text),
|
||||
TableColumn('options', _int, isHidden: true),
|
||||
]);
|
||||
}
|
||||
|
||||
const _text = ResolvedType(type: BasicType.text);
|
||||
const _int = ResolvedType(type: BasicType.int);
|
||||
@@ -1,6 +1,6 @@
|
||||
name: sqlparser
|
||||
description: Parses sqlite statements and performs static analysis on them
|
||||
version: 0.41.3-dev
|
||||
version: 0.42.0-dev
|
||||
homepage: https://github.com/simolus3/drift/tree/develop/sqlparser
|
||||
repository: https://github.com/simolus3/drift
|
||||
#homepage: https://drift.simonbinder.eu/
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../analysis/data.dart';
|
||||
import '../../analysis/errors/utils.dart';
|
||||
|
||||
void main() {
|
||||
final options = EngineOptions(
|
||||
version: SqliteVersion.current,
|
||||
enabledExtensions: [const PowerSyncSqliteExtension()]);
|
||||
late SqlEngine engine;
|
||||
|
||||
setUp(() {
|
||||
engine = SqlEngine(options)..registerTableFromSql('''
|
||||
CREATE TABLE todo_lists (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_by TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT
|
||||
) STRICT;
|
||||
''');
|
||||
});
|
||||
|
||||
group('crud vtab via triggers', () {
|
||||
// This captures suggestions from https://docs.powersync.com/usage/use-case-examples/raw-tables#capture-local-writes-with-triggers
|
||||
test('insert', () {
|
||||
final result = engine.analyze('''
|
||||
CREATE TRIGGER todo_lists_insert
|
||||
AFTER INSERT ON todo_lists
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PUT', NEW.id, 'todo_lists', json_object(
|
||||
'created_by', NEW.created_by,
|
||||
'title', NEW.title,
|
||||
'content', NEW.content
|
||||
));
|
||||
END;
|
||||
''');
|
||||
|
||||
result.expectNoError();
|
||||
});
|
||||
|
||||
test('update', () {
|
||||
final result = engine.analyze('''
|
||||
CREATE TRIGGER todo_lists_update
|
||||
AFTER UPDATE ON todo_lists
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SELECT CASE
|
||||
WHEN (OLD.id != NEW.id)
|
||||
THEN RAISE (FAIL, 'Cannot update id')
|
||||
END;
|
||||
|
||||
-- TODO: You may want to replace the json_object with a powersync_diff call of the old and new values, or
|
||||
-- use your own diff logic to avoid marking unchanged columns as updated.
|
||||
INSERT INTO powersync_crud (op, id, type, data) VALUES ('PATCH', NEW.id, 'todo_lists', json_object(
|
||||
'created_by', NEW.created_by,
|
||||
'title', NEW.title,
|
||||
'content', NEW.content
|
||||
));
|
||||
END;
|
||||
''');
|
||||
|
||||
result.expectNoError();
|
||||
});
|
||||
|
||||
test('delete', () {
|
||||
final result = engine.analyze('''
|
||||
CREATE TRIGGER todo_lists_delete
|
||||
AFTER DELETE ON todo_lists
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
INSERT INTO powersync_crud (op, id, type) VALUES ('DELETE', OLD.id, 'todo_lists');
|
||||
END;
|
||||
''');
|
||||
|
||||
result.expectNoError();
|
||||
});
|
||||
});
|
||||
|
||||
test('infers function return type', () {
|
||||
final functions = {
|
||||
"powersync_diff('{}', '{}')": BasicType.text,
|
||||
'powersync_client_id()': BasicType.text,
|
||||
'powersync_in_sync_operation()': BasicType.int,
|
||||
'gen_random_uuid()': BasicType.text,
|
||||
'uuid()': BasicType.text,
|
||||
};
|
||||
|
||||
functions.forEach((call, type) {
|
||||
final analyzed = engine.analyze('SELECT $call;');
|
||||
analyzed.expectNoError();
|
||||
final [column] = (analyzed.root as SelectStatement).resolvedColumns!;
|
||||
|
||||
expect(analyzed.typeOf(column).type?.type, type,
|
||||
reason: '$call should resolve to $type');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user