@@ -4,7 +4,9 @@ mod management;
44mod raw_table;
55mod table_info;
66
7- use alloc:: { rc:: Rc , vec:: Vec } ;
7+ use core:: { ffi:: c_void, fmt:: Write } ;
8+
9+ use alloc:: { format, rc:: Rc , vec:: Vec } ;
810pub use common:: { ColumnFilter , SchemaTable } ;
911use powersync_sqlite_nostd:: { self as sqlite, Connection , Context , Value , args} ;
1012pub use raw_table:: InferredSchemaCache ;
@@ -17,9 +19,12 @@ pub use table_info::{
1719
1820use crate :: {
1921 error:: { PSResult , PowerSyncError } ,
20- schema:: raw_table:: generate_raw_table_trigger,
22+ ext:: SafeManagedStmt ,
23+ schema:: raw_table:: { InferredTableStructure , generate_raw_table_trigger} ,
2124 state:: DatabaseState ,
22- utils:: WriteType ,
25+ sync:: RawTableWithCachedStatements ,
26+ utils:: { SqlBuffer , WriteType } ,
27+ views:: table_columns_to_json_object,
2328} ;
2429
2530#[ derive( Deserialize , Default ) ]
@@ -29,27 +34,157 @@ pub struct Schema {
2934 pub raw_tables : Vec < table_info:: RawTable > ,
3035}
3136
32- pub fn register ( db : * mut sqlite:: sqlite3 , state : Rc < DatabaseState > ) -> Result < ( ) , ResultCode > {
33- management:: register ( db, state) ?;
37+ impl Schema {
38+ pub fn find_raw_table ( & self , name : & str ) -> Option < & table_info:: RawTable > {
39+ self . raw_tables . iter ( ) . find ( |tbl| tbl. name == name)
40+ }
41+ }
3442
35- {
36- fn create_trigger (
37- context : * mut sqlite:: context ,
38- args : & [ * mut sqlite:: value ] ,
39- ) -> Result < ( ) , PowerSyncError > {
40- // Args: Table (JSON), trigger_name, write_type
41- let table: RawTable =
42- serde_json:: from_str ( args[ 0 ] . text ( ) ) . map_err ( PowerSyncError :: as_argument_error) ?;
43- let trigger_name = args[ 1 ] . text ( ) ;
44- let write_type: WriteType = args[ 2 ] . text ( ) . parse ( ) ?;
45-
46- let db = context. db_handle ( ) ;
47- let create_trigger_stmt =
48- generate_raw_table_trigger ( db, & table, trigger_name, write_type) ?;
49- db. exec_safe ( & create_trigger_stmt) . into_db_result ( db) ?;
50- Ok ( ( ) )
43+ fn create_trigger (
44+ context : * mut sqlite:: context ,
45+ args : & [ * mut sqlite:: value ] ,
46+ ) -> Result < ( ) , PowerSyncError > {
47+ // Args: Table (JSON), trigger_name, write_type
48+ let table: RawTable =
49+ serde_json:: from_str ( args[ 0 ] . text ( ) ) . map_err ( PowerSyncError :: as_argument_error) ?;
50+ let trigger_name = args[ 1 ] . text ( ) ;
51+ let write_type: WriteType = args[ 2 ] . text ( ) . parse ( ) ?;
52+
53+ let db = context. db_handle ( ) ;
54+ let create_trigger_stmt = generate_raw_table_trigger ( db, & table, trigger_name, write_type) ?;
55+ db. exec_safe ( & create_trigger_stmt) . into_db_result ( db) ?;
56+ Ok ( ( ) )
57+ }
58+
59+ enum RawTableMigration < ' a > {
60+ CreateFromUntyped { table_name : & ' a str } ,
61+ DropIntoUntyped { table : RawTable } ,
62+ }
63+
64+ impl < ' a > RawTableMigration < ' a > {
65+ fn from_args ( args : & ' a [ * mut sqlite:: value ] ) -> Result < Self , PowerSyncError > {
66+ match args[ 0 ] . text ( ) {
67+ "create" => Ok ( RawTableMigration :: CreateFromUntyped {
68+ table_name : args[ 1 ] . text ( ) ,
69+ } ) ,
70+ "drop" => {
71+ let table: RawTable = serde_json:: from_str ( args[ 1 ] . text ( ) )
72+ . map_err ( PowerSyncError :: as_argument_error) ?;
73+ Ok ( RawTableMigration :: DropIntoUntyped { table } )
74+ }
75+ _ => Err ( PowerSyncError :: argument_error (
76+ "Unknown action for powersync_raw_table_migrate" ,
77+ ) ) ,
78+ }
79+ }
80+ }
81+
82+ /// Utility to help with migrations involving raw tables.
83+ ///
84+ /// `powersync_raw_table_migrate('create', $name)` moves existing rows matching the table from
85+ /// `ps_untyped` into the raw table (to be called after adding the raw table to the schema).
86+ ///
87+ /// `powersync_raw_table_migrate('drop', $json)` moves rows from the raw table back into
88+ /// `ps_untyped` (call before removing the raw table from the schema), where `$json` is a JSON
89+ /// representation of the [`RawTable`] being dropped.
90+ fn raw_table_migration (
91+ context : * mut sqlite:: context ,
92+ args : & [ * mut sqlite:: value ] ,
93+ ) -> Result < ( ) , PowerSyncError > {
94+ let db = context. db_handle ( ) ;
95+ let context = unsafe { DatabaseState :: from_context ( & context) } ;
96+ let action = RawTableMigration :: from_args ( args) ?;
97+
98+ // Disable triggers forwarding writes to ps_crud.
99+ let _guard = context. sync_local_guard ( ) ;
100+
101+ let schema_version = InferredSchemaCache :: current_schema_version ( db) ?;
102+
103+ match action {
104+ RawTableMigration :: CreateFromUntyped { table_name } => {
105+ // Move data from ps_untyped into this raw table.
106+ let Some ( schema) = context. view_schema ( ) else {
107+ return Err ( PowerSyncError :: state_error ( "Schema not initialized" ) ) ;
108+ } ;
109+
110+ let Some ( table) = schema. find_raw_table ( table_name) else {
111+ return Err ( PowerSyncError :: argument_error ( format ! (
112+ "Raw table {table_name} not found"
113+ ) ) ) ;
114+ } ;
115+ let delete_untyped =
116+ db. prepare_v2 ( "DELETE FROM ps_untyped WHERE type = ? RETURNING id, data" ) ?;
117+ delete_untyped. bind_text ( 1 , table_name, powersync_sqlite_nostd:: Destructor :: STATIC ) ?;
118+ let mut table_statements = RawTableWithCachedStatements :: new ( table) ;
119+
120+ while let ResultCode :: ROW = delete_untyped. step ( ) ? {
121+ let put = table_statements. put_statement (
122+ db,
123+ schema_version,
124+ & context. inferred_schema_cache ,
125+ ) ?;
126+
127+ let id = delete_untyped. column_text ( 0 ) ?;
128+ let data_text = delete_untyped. column_text ( 1 ) ?;
129+ let parsed: serde_json:: Value =
130+ serde_json:: from_str ( data_text) . map_err ( PowerSyncError :: json_local_error) ?;
131+ let json_object = parsed. as_object ( ) . ok_or_else ( || {
132+ PowerSyncError :: argument_error ( "expected oplog data to be an object" )
133+ } ) ?;
134+
135+ let rest = put. render_rest_object ( & json_object) ?;
136+ put. bind_for_put ( id, & json_object, & rest) ?;
137+ put. exec ( db, table_name, id, Some ( & parsed) ) ?;
138+ }
139+ }
140+ RawTableMigration :: DropIntoUntyped { table } => {
141+ // Copy data from the raw table into ps_untyped, ignoring local-only columns.
142+ let local_table_name = table. require_table_name ( ) ?;
143+ let resolved_table = InferredTableStructure :: read_from_database (
144+ local_table_name,
145+ db,
146+ & table. schema . synced_columns ,
147+ ) ?;
148+ let as_schema_table = SchemaTable :: Raw {
149+ definition : & table,
150+ schema : & resolved_table,
151+ } ;
152+
153+ let sql = {
154+ // Generate an INSERT INTO ps_untyped with a SELECT source transforming existing
155+ // rows into JSON objects.
156+ let mut buffer = SqlBuffer :: new ( ) ;
157+ let fragment = table_columns_to_json_object ( & local_table_name, & as_schema_table) ?;
158+ buffer. push_str ( "INSERT INTO ps_untyped (type, id, data) SELECT ?, id, " ) ;
159+ buffer. push_str ( & fragment) ;
160+ buffer. push_str ( " FROM " ) ;
161+ let _ = buffer. identifier ( ) . write_str ( local_table_name) ;
162+ buffer. push_char ( ';' ) ;
163+ buffer. sql
164+ } ;
165+ {
166+ let stmt = db. prepare_v2 ( & sql) . into_db_result ( db) ?;
167+ stmt. bind_text ( 1 , & table. name , powersync_sqlite_nostd:: Destructor :: STATIC ) ?;
168+ stmt. exec ( ) . into_db_result ( db) ?;
169+ }
170+
171+ // Then, delete raw table contents.
172+ {
173+ let mut truncate_buf = SqlBuffer :: new ( ) ;
174+ truncate_buf. push_str ( "DELETE FROM " ) ;
175+ let _ = truncate_buf. identifier ( ) . write_str ( local_table_name) ;
176+ db. exec_safe ( & truncate_buf. sql ) . into_db_result ( db) ?;
177+ }
51178 }
179+ }
52180
181+ Ok ( ( ) )
182+ }
183+
184+ pub fn register ( db : * mut sqlite:: sqlite3 , state : Rc < DatabaseState > ) -> Result < ( ) , ResultCode > {
185+ management:: register ( db, state. clone ( ) ) ?;
186+
187+ {
53188 extern "C" fn create_raw_trigger_sqlite (
54189 context : * mut sqlite:: context ,
55190 argc : i32 ,
@@ -61,6 +196,17 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<()
61196 }
62197 }
63198
199+ extern "C" fn raw_table_migrate_sqlite (
200+ context : * mut sqlite:: context ,
201+ argc : i32 ,
202+ args : * mut * mut sqlite:: value ,
203+ ) {
204+ let args = args ! ( argc, args) ;
205+ if let Err ( e) = raw_table_migration ( context, args) {
206+ e. apply_to_ctx ( "powersync_raw_table_migrate" , context) ;
207+ }
208+ }
209+
64210 db. create_function_v2 (
65211 "powersync_create_raw_table_crud_trigger" ,
66212 3 ,
@@ -71,6 +217,18 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<()
71217 None ,
72218 None ,
73219 ) ?;
220+
221+ db. create_function_v2 (
222+ "powersync_raw_table_migrate" ,
223+ 2 ,
224+ sqlite:: UTF8 ,
225+ Some ( Rc :: into_raw ( state) as * mut c_void ) ,
226+ Some ( raw_table_migrate_sqlite) ,
227+ None ,
228+ None ,
229+ Some ( DatabaseState :: destroy_rc) ,
230+ ) ?;
74231 }
232+
75233 Ok ( ( ) )
76234}
0 commit comments