@@ -4,6 +4,7 @@ import 'package:sqlite3/common.dart';
44import 'package:test/test.dart' ;
55
66import 'utils/native_test_utils.dart' ;
7+ import 'utils/test_utils.dart' ;
78
89void main () {
910 group ('crud tests' , () {
@@ -774,5 +775,176 @@ void main() {
774775 expect (db.select ('SELECT * FROM ps_crud' ), isEmpty);
775776 });
776777 });
778+
779+ group ('raw tables' , () {
780+ void createRawTableTriggers (Object table,
781+ {bool insert = true , bool update = true , bool delete = true }) {
782+ db.execute ('SELECT powersync_init()' );
783+
784+ if (insert) {
785+ db.execute ('SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)' ,
786+ [json.encode (table), 'test_trigger_insert' , 'INSERT' ]);
787+ }
788+ if (update) {
789+ db.execute ('SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)' ,
790+ [json.encode (table), 'test_trigger_update' , 'UPDATE' ]);
791+ }
792+ if (delete) {
793+ db.execute ('SELECT powersync_create_raw_table_crud_trigger(?, ?, ?)' ,
794+ [json.encode (table), 'test_trigger_delete' , 'DELETE' ]);
795+ }
796+ }
797+
798+ Object rawTableDescription (Map <String , Object ?> options) {
799+ return {
800+ 'name' : 'row_type' ,
801+ 'put' : {'sql' : '' , 'params' : []},
802+ 'delete' : {'sql' : '' , 'params' : []},
803+ ...options,
804+ };
805+ }
806+
807+ test ('missing id column' , () {
808+ db.execute ('CREATE TABLE users (name TEXT);' );
809+ expect (
810+ () => createRawTableTriggers (
811+ rawTableDescription ({'table_name' : 'users' })),
812+ throwsA (isSqliteException (
813+ 3091 , contains ('Table users has no id column' ))),
814+ );
815+ });
816+
817+ test ('missing local table name' , () {
818+ db.execute ('CREATE TABLE users (name TEXT);' );
819+ expect (
820+ () => createRawTableTriggers (rawTableDescription ({})),
821+ throwsA (isSqliteException (3091 , contains ('Table has no local name' ))),
822+ );
823+ });
824+
825+ test ('missing local table' , () {
826+ expect (
827+ () => createRawTableTriggers (
828+ rawTableDescription ({'table_name' : 'users' })),
829+ throwsA (isSqliteException (
830+ 3091 , contains ('Could not find users in local schema' ))),
831+ );
832+ });
833+
834+ test ('default options' , () {
835+ db.execute ('CREATE TABLE users (id TEXT, name TEXT) STRICT;' );
836+ createRawTableTriggers (rawTableDescription ({'table_name' : 'users' }));
837+
838+ db
839+ ..execute (
840+ 'INSERT INTO users (id, name) VALUES (?, ?)' , ['id' , 'name' ])
841+ ..execute ('UPDATE users SET name = ?' , ['new name' ])
842+ ..execute ('DELETE FROM users WHERE id = ?' , ['id' ]);
843+
844+ final psCrud = db.select ('SELECT * FROM ps_crud' );
845+ expect (psCrud, [
846+ {
847+ 'id' : 1 ,
848+ 'tx_id' : 1 ,
849+ 'data' : json.encode ({
850+ 'op' : 'PUT' ,
851+ 'id' : 'id' ,
852+ 'type' : 'row_type' ,
853+ 'data' : {'name' : 'name' }
854+ }),
855+ },
856+ {
857+ 'id' : 2 ,
858+ 'tx_id' : 2 ,
859+ 'data' : json.encode ({
860+ 'op' : 'PATCH' ,
861+ 'id' : 'id' ,
862+ 'type' : 'row_type' ,
863+ 'data' : {'name' : 'new name' }
864+ }),
865+ },
866+ {
867+ 'id' : 3 ,
868+ 'tx_id' : 3 ,
869+ 'data' :
870+ json.encode ({'op' : 'DELETE' , 'id' : 'id' , 'type' : 'row_type' }),
871+ },
872+ ]);
873+ });
874+
875+ test ('insert only' , () {
876+ db.execute ('CREATE TABLE users (id TEXT, name TEXT) STRICT;' );
877+ createRawTableTriggers (
878+ rawTableDescription ({'table_name' : 'users' , 'insert_only' : true }));
879+
880+ db.execute (
881+ 'INSERT INTO users (id, name) VALUES (?, ?)' , ['id' , 'name' ]);
882+ expect (db.select ('SELECT * FROM ps_crud' ), hasLength (1 ));
883+
884+ // Should not update the $local bucket
885+ expect (db.select ('SELECT * FROM ps_buckets' ), hasLength (0 ));
886+
887+ // The trigger should prevent other writes.
888+ expect (
889+ () => db.execute ('UPDATE users SET name = ?' , ['new name' ]),
890+ throwsA (isSqliteException (
891+ 1811 , contains ('Unexpected update on insert-only table' ))));
892+ expect (
893+ () => db.execute ('DELETE FROM users WHERE id = ?' , ['id' ]),
894+ throwsA (isSqliteException (
895+ 1811 , contains ('Unexpected update on insert-only table' ))));
896+ });
897+
898+ test ('tracking old values' , () {
899+ db.execute (
900+ 'CREATE TABLE users (id TEXT, name TEXT, email TEXT) STRICT;' );
901+ createRawTableTriggers (rawTableDescription ({
902+ 'table_name' : 'users' ,
903+ 'include_old' : ['name' ],
904+ 'include_old_only_when_changed' : true ,
905+ }));
906+
907+ db
908+ ..execute ('INSERT INTO users (id, name, email) VALUES (?, ?, ?)' ,
909+ ['id' , 'name' , 'test@example.org' ])
910+ ..execute ('UPDATE users SET name = ?, email = ?' ,
911+ ['new name' , 'newmail@example.org' ])
912+ ..execute ('DELETE FROM users WHERE id = ?' , ['id' ]);
913+
914+ final psCrud = db.select (
915+ r"SELECT id, data->>'$.op' AS op, data->>'$.old' as old FROM ps_crud" );
916+ expect (psCrud, [
917+ {
918+ 'id' : 1 ,
919+ 'op' : 'PUT' ,
920+ 'old' : null ,
921+ },
922+ {
923+ 'id' : 2 ,
924+ 'op' : 'PATCH' ,
925+ 'old' : json.encode ({'name' : 'name' }),
926+ },
927+ {
928+ 'id' : 3 ,
929+ 'op' : 'DELETE' ,
930+ 'old' : json.encode ({'name' : 'new name' }),
931+ },
932+ ]);
933+ });
934+
935+ test ('skipping empty updates' , () {
936+ db.execute ('CREATE TABLE users (id TEXT, name TEXT) STRICT;' );
937+ createRawTableTriggers (rawTableDescription (
938+ {'table_name' : 'users' , 'ignore_empty_update' : true }));
939+
940+ db.execute (
941+ 'INSERT INTO users (id, name) VALUES (?, ?)' , ['id' , 'name' ]);
942+ expect (db.select ('SELECT * FROM ps_crud' ), hasLength (1 ));
943+
944+ // Empty update should not be recorded
945+ db.execute ('UPDATE users SET name = ?' , ['name' ]);
946+ expect (db.select ('SELECT * FROM ps_crud' ), hasLength (1 ));
947+ });
948+ });
777949 });
778950}
0 commit comments