Skip to content

Commit cc519e7

Browse files
committed
Raw table improvements
1 parent 81c3c79 commit cc519e7

File tree

3 files changed

+327
-53
lines changed

3 files changed

+327
-53
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
## 0.0.4 (unreleased)
1+
## 0.0.4
22

33
- Update PowerSync core extension to version 0.4.11.
4+
- Improvements for raw tables:
5+
- The `put` and `delete` statements are optional now.
6+
- The `RawTableSchema` struct represents a raw table in the local database, and can be used
7+
to create triggers forwarding writes to the CRUD upload queue and to infer statements used
8+
to sync data into raw tables.
49

510
## 0.0.3
611

powersync/src/db/schema.rs

Lines changed: 180 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::borrow::Cow;
22
use std::collections::HashSet;
33

4-
use serde::{Serialize, ser::SerializeStruct};
4+
use serde::{Serialize, Serializer, ser::SerializeStruct};
55

66
use 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)]
5656
pub 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

8170
impl 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)]
265289
pub 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)]
272396
pub 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)]
279405
pub 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

Comments
 (0)