11use std:: borrow:: Cow ;
22use std:: collections:: HashSet ;
33
4- use serde:: { Serialize , ser:: SerializeStruct } ;
4+ use serde:: { Serialize , Serializer , ser:: SerializeStruct } ;
55
66use crate :: error:: PowerSyncError ;
77
@@ -52,30 +52,19 @@ impl Schema {
5252///
5353/// When this is part of a schema, the PowerSync SDK will create and auto-migrate the table.
5454/// If you need direct control on a table, use [RawTable] instead.
55- #[ derive( Debug ) ]
55+ #[ derive( Debug , Serialize ) ]
5656pub struct Table {
5757 /// The synced table name, matching sync rules.
5858 pub name : SchemaString ,
5959 // Override the name for the view.
60+ #[ serde( rename = "view_name" ) ]
6061 pub view_name_override : Option < SchemaString > ,
6162 /// List of columns.
6263 pub columns : Vec < Column > ,
6364 /// List of indexes.
6465 pub indexes : Vec < Index > ,
65- /// Whether this is a local-only table.
66- pub local_only : bool ,
67- /// Whether this is an insert-only table.
68- pub insert_only : bool ,
69- /// Whether to add a hidden `_metadata` column that will be enabled for updates to attach custom
70- /// information about writes that will be reported through crud entries.
71- pub track_metadata : bool ,
72- /// When set, track old values of columns for CRUD entries.
73- ///
74- /// See [TrackPreviousValues] for details.
75- pub track_previous_values : Option < TrackPreviousValues > ,
76- /// Whether an `UPDATE` statement that doesn't change any values should be ignored when creating
77- /// CRUD entries.
78- pub ignore_empty_updates : bool ,
66+ #[ serde( flatten) ]
67+ pub options : TableOptions ,
7968}
8069
8170impl Table {
@@ -92,11 +81,7 @@ impl Table {
9281 view_name_override : None ,
9382 columns,
9483 indexes : vec ! [ ] ,
95- local_only : false ,
96- insert_only : false ,
97- track_metadata : false ,
98- track_previous_values : None ,
99- ignore_empty_updates : false ,
84+ options : TableOptions :: default ( ) ,
10085 } ;
10186 build ( & mut table) ;
10287 table
@@ -115,17 +100,7 @@ impl Table {
115100 Schema :: validate_name ( view_name_override, "table view" ) ?;
116101 }
117102
118- if self . local_only && self . track_metadata {
119- return Err ( PowerSyncError :: argument_error (
120- "Can't track metadata for local-only tables" ,
121- ) ) ;
122- }
123-
124- if self . local_only && self . track_previous_values . is_some ( ) {
125- return Err ( PowerSyncError :: argument_error (
126- "Can't track old values for local-only tables" ,
127- ) ) ;
128- }
103+ self . options . validate ( ) ?;
129104
130105 let mut column_names = HashSet :: new ( ) ;
131106 column_names. insert ( "id" ) ;
@@ -172,18 +147,52 @@ impl Table {
172147 const MAX_AMOUNT_OF_COLUMNS : usize = 1999 ;
173148}
174149
175- impl Serialize for Table {
150+ /// Options that apply to both view-based JSON tables and raw tables.
151+ #[ derive( Debug , Default ) ]
152+ pub struct TableOptions {
153+ /// Whether this is a local-only table.
154+ pub local_only : bool ,
155+ /// Whether this is an insert-only table.
156+ pub insert_only : bool ,
157+ /// Whether to add a hidden `_metadata` column that will be enabled for updates to attach custom
158+ /// information about writes that will be reported through crud entries.
159+ pub track_metadata : bool ,
160+ /// When set, track old values of columns for CRUD entries.
161+ ///
162+ /// See [TrackPreviousValues] for details.
163+ pub track_previous_values : Option < TrackPreviousValues > ,
164+ /// Whether an `UPDATE` statement that doesn't change any values should be ignored when creating
165+ /// CRUD entries.
166+ pub ignore_empty_updates : bool ,
167+ }
168+
169+ impl TableOptions {
170+ fn validate ( & self ) -> Result < ( ) , PowerSyncError > {
171+ if self . local_only && self . track_metadata {
172+ return Err ( PowerSyncError :: argument_error (
173+ "Can't track metadata for local-only tables" ,
174+ ) ) ;
175+ }
176+
177+ if self . local_only && self . track_previous_values . is_some ( ) {
178+ return Err ( PowerSyncError :: argument_error (
179+ "Can't track old values for local-only tables" ,
180+ ) ) ;
181+ }
182+
183+ Ok ( ( ) )
184+ }
185+ }
186+
187+ impl Serialize for TableOptions {
176188 fn serialize < S > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error >
177189 where
178- S : serde :: Serializer ,
190+ S : Serializer ,
179191 {
180- let mut serializer = serializer. serialize_struct ( "Table" , 10 ) ?;
181- serializer. serialize_field ( "name" , & self . name ) ?;
182- serializer. serialize_field ( "columns" , & self . columns ) ?;
183- serializer. serialize_field ( "indexes" , & self . indexes ) ?;
192+ let mut serializer = serializer. serialize_struct ( "TableOptions" , 5 ) ?;
193+
184194 serializer. serialize_field ( "local_only" , & self . local_only ) ?;
185195 serializer. serialize_field ( "insert_only" , & self . insert_only ) ?;
186- serializer. serialize_field ( "view_name" , & self . view_name_override ) ?;
187196 serializer. serialize_field ( "ignore_empty_update" , & self . ignore_empty_updates ) ?;
188197 serializer. serialize_field ( "include_metadata" , & self . track_metadata ) ?;
189198
@@ -199,7 +208,6 @@ impl Serialize for Table {
199208 & include_old. only_when_changed ,
200209 ) ?;
201210 } else {
202- serializer. serialize_field ( "include_old_include_oldonly_when_changed" , & false ) ?;
203211 serializer. serialize_field ( "include_old_only_when_changed" , & false ) ?;
204212 }
205213
@@ -261,24 +269,150 @@ pub struct IndexedColumn {
261269 pub type_name : SchemaString ,
262270}
263271
272+ /// A raw table, defined by the user instead of being managed by PowerSync.
273+ ///
274+ /// Any ordinary SQLite table can be defined as a raw table, which enables:
275+ ///
276+ /// - More performant queries, since data is stored in typed rows instead of the schemaless JSON
277+ /// view PowerSync uses by default.
278+ /// - More control over the table, since custom column constraints can be used in its definition.
279+ ///
280+ /// By default, the PowerSync client will infer the schema of raw tables and use that to generate
281+ /// `UPSERT` and `DELETE` statements to forward writes from the backend database to SQLite. This
282+ /// requires [Self::schema] to be set.
283+ /// These statements can be customized by providing [Self::put] and [Self::delete] statements.
284+ ///
285+ /// When using raw tables, you are responsible for creating and migrating them when they've changed.
286+ /// Further, triggers are necessary to collect local writes to those tables. For more information,
287+ /// see [the documentation](https://docs.powersync.com/client-sdks/advanced/raw-tables).
264288#[ derive( Serialize , Debug ) ]
265289pub struct RawTable {
290+ /// The name of the table as used by the sync service.
291+ ///
292+ /// This doesn't necessarily have to match the name of the SQLite table that [put] and [delete]
293+ /// write to. Instead, it's used by the sync client to identify which statements to use when it
294+ /// encounters sync operations for this table.
266295 pub name : SchemaString ,
267- pub put : PendingStatement ,
268- pub delete : PendingStatement ,
296+
297+ /// An optional schema containing the name of the raw table in the local schema.
298+ ///
299+ /// If this is set, [Self::put] and [Self::delete] can be omitted because these statements can
300+ /// be inferred from the schema.
301+ #[ serde( flatten) ]
302+ pub schema : Option < RawTableSchema > ,
303+
304+ /// A statement responsible for inserting or updating a row in this raw table based on data from
305+ /// the sync service.
306+ ///
307+ /// By default, the client generates an `INSERT` statement with an upsert clause for all columns
308+ /// in the table.
309+ ///
310+ /// See [PendingStatement] for details.
311+ pub put : Option < PendingStatement > ,
312+
313+ /// A statement responsible for deleting a row based on its PowerSync id.
314+ ///
315+ /// By default, the client generates the statement `DELETE FROM $local_table WHERE id = ?`.
316+ ///
317+ /// See [PendingStatement] for details. Note that [PendingStatementValue]s used here must all be
318+ /// [PendingStatementValue::Id].
319+ pub delete : Option < PendingStatement > ,
320+
321+ /// An optional statement to run when the `powersync_clear` SQL function is called.
322+ pub clear : Option < SchemaString > ,
323+ }
324+
325+ impl RawTable {
326+ /// Creates a [RawTable] where statements used to sync rows into the table are inferred from
327+ /// the columns of the table.
328+ pub fn with_schema ( name : impl Into < SchemaString > , schema : RawTableSchema ) -> Self {
329+ Self {
330+ name : name. into ( ) ,
331+ schema : Some ( schema) ,
332+ put : None ,
333+ delete : None ,
334+ clear : None ,
335+ }
336+ }
337+
338+ /// Creates a [RawTable] with explicit put and delete statements to use.
339+ pub fn with_statements (
340+ name : impl Into < SchemaString > ,
341+ put : PendingStatement ,
342+ delete : PendingStatement ,
343+ ) -> Self {
344+ Self {
345+ name : name. into ( ) ,
346+ schema : None ,
347+ put : Some ( put) ,
348+ delete : Some ( delete) ,
349+ clear : None ,
350+ }
351+ }
269352}
270353
354+ /// Information about the schema of a [RawTable] in the local database.
355+ ///
356+ /// This information is optional when declaring raw tables. However, providing it allows the sync
357+ /// client to infer [RawTable::put] and [RawTable::delete] statements automatically.
358+ #[ derive( Serialize , Debug ) ]
359+ pub struct RawTableSchema {
360+ /// The actual name of the raw table in the local schema.
361+ ///
362+ /// This is used to infer statements for the sync client. It can also be used to auto-generate
363+ /// triggers forwarding writes on raw tables into the CRUD upload queue.
364+ pub table_name : SchemaString ,
365+ /// An optional filter of columns that should be synced.
366+ ///
367+ /// By default, all columns in a raw table are considered to be synced. If a filter is
368+ /// specified, PowerSync treats unmatched columns as _local-only_ and will not attempt to sync
369+ /// them.
370+ pub synced_columns : Option < Vec < SchemaString > > ,
371+
372+ /// Common options affecting how the `powersync_create_raw_table_crud_trigger` SQL function
373+ /// generates triggers.
374+ #[ serde( flatten) ]
375+ pub options : TableOptions ,
376+ }
377+
378+ impl RawTableSchema {
379+ pub fn new ( table_name : impl Into < SchemaString > ) -> Self {
380+ Self {
381+ table_name : table_name. into ( ) ,
382+ synced_columns : None ,
383+ options : Default :: default ( ) ,
384+ }
385+ }
386+ }
387+
388+ /// An SQL statement to be run by the sync client against raw tables.
389+ ///
390+ /// Since raw tables are managed by the user, PowerSync can't know how to apply serverside changes
391+ /// to them. These statements bridge raw tables and PowerSync by providing upserts and delete
392+ /// statements.
393+ ///
394+ /// For more information, see [the documentation](https://docs.powersync.com/client-sdks/advanced/raw-tables).
271395#[ derive( Serialize , Debug ) ]
272396pub struct PendingStatement {
273397 pub sql : SchemaString ,
274398 /// This vec should contain an entry for each parameter in [sql].
275399 pub params : Vec < PendingStatementValue > ,
276400}
277401
402+ /// A description of a value that will be resolved in the sync client when running a
403+ /// [PendingStatement] for a [RawTable].
278404#[ derive( Serialize , Debug ) ]
279405pub enum PendingStatementValue {
406+ /// A value that is bound to the textual id used in the PowerSync protocol.
280407 Id ,
408+
409+ /// A value that is bound to the value of a column in a replace (`PUT`)
410+ /// operation of the PowerSync protocol.
281411 Column ( SchemaString ) ,
412+
413+ /// A value that is bound to a JSON object containing all columns from the synced row that
414+ /// haven't been matched by a [Self::Column] value.
415+ Rest ,
282416}
283417
284418/// Options to include old values in CRUD entries for update statements.
@@ -306,7 +440,7 @@ mod test {
306440 #[ test]
307441 fn handles_options_track_metadata ( ) {
308442 let value = serde_json:: to_value ( Table :: create ( "foo" , vec ! [ ] , |tbl| {
309- tbl. track_metadata = true
443+ tbl. options . track_metadata = true
310444 } ) )
311445 . unwrap ( ) ;
312446
@@ -324,7 +458,7 @@ mod test {
324458 #[ test]
325459 fn handles_options_ignore_empty_updates ( ) {
326460 let value = serde_json:: to_value ( Table :: create ( "foo" , vec ! [ ] , |tbl| {
327- tbl. ignore_empty_updates = true
461+ tbl. options . ignore_empty_updates = true
328462 } ) )
329463 . unwrap ( ) ;
330464
@@ -342,7 +476,7 @@ mod test {
342476 #[ test]
343477 fn handles_options_track_previous_all ( ) {
344478 let value = serde_json:: to_value ( Table :: create ( "foo" , vec ! [ ] , |tbl| {
345- tbl. track_previous_values = Some ( TrackPreviousValues :: all ( ) )
479+ tbl. options . track_previous_values = Some ( TrackPreviousValues :: all ( ) )
346480 } ) )
347481 . unwrap ( ) ;
348482 let value = value. as_object ( ) . unwrap ( ) ;
@@ -360,7 +494,7 @@ mod test {
360494 #[ test]
361495 fn handles_options_track_previous_column_filter ( ) {
362496 let value = serde_json:: to_value ( Table :: create ( "foo" , vec ! [ ] , |tbl| {
363- tbl. track_previous_values = Some ( TrackPreviousValues :: all ( ) )
497+ tbl. options . track_previous_values = Some ( TrackPreviousValues :: all ( ) )
364498 } ) )
365499 . unwrap ( ) ;
366500 let value = value. as_object ( ) . unwrap ( ) ;
0 commit comments