Skip to content

Commit a444b0f

Browse files
committed
Helpers to move between raw tables and ps_untyped
1 parent dbf4cbb commit a444b0f

4 files changed

Lines changed: 327 additions & 29 deletions

File tree

crates/core/src/schema/mod.rs

Lines changed: 179 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ mod management;
44
mod raw_table;
55
mod 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};
810
pub use common::{ColumnFilter, SchemaTable};
911
use powersync_sqlite_nostd::{self as sqlite, Connection, Context, Value, args};
1012
pub use raw_table::InferredSchemaCache;
@@ -17,9 +19,12 @@ pub use table_info::{
1719

1820
use 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
}

crates/core/src/sync/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod sync_status;
1616

1717
pub use bucket_priority::BucketPriority;
1818
pub use checksum::Checksum;
19+
pub use sync_local::RawTableWithCachedStatements;
1920

2021
use crate::state::DatabaseState;
2122
pub use streaming_sync::SyncClient;

crates/core/src/sync/sync_local.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -433,13 +433,21 @@ struct ParsedSchemaTable<'a> {
433433
raw: Option<RawTableWithCachedStatements<'a>>,
434434
}
435435

436-
struct RawTableWithCachedStatements<'a> {
436+
pub struct RawTableWithCachedStatements<'a> {
437437
definition: &'a RawTable,
438438
cached_put: Option<PreparedPendingStatement>,
439439
cached_delete: Option<PreparedPendingStatement>,
440440
}
441441

442442
impl<'a> RawTableWithCachedStatements<'a> {
443+
pub fn new(definition: &'a RawTable) -> Self {
444+
Self {
445+
definition,
446+
cached_put: None,
447+
cached_delete: None,
448+
}
449+
}
450+
443451
fn prepare_lazily(
444452
db: *mut sqlite::sqlite3,
445453
slot: &mut Option<PreparedPendingStatement>,
@@ -454,7 +462,7 @@ impl<'a> RawTableWithCachedStatements<'a> {
454462
})
455463
}
456464

457-
fn put_statement(
465+
pub fn put_statement(
458466
&'_ mut self,
459467
db: *mut sqlite::sqlite3,
460468
schema_version: usize,
@@ -494,16 +502,12 @@ impl<'a> ParsedSchemaTable<'a> {
494502

495503
pub fn raw(definition: &'a RawTable) -> Self {
496504
Self {
497-
raw: Some(RawTableWithCachedStatements {
498-
definition,
499-
cached_put: None,
500-
cached_delete: None,
501-
}),
505+
raw: Some(RawTableWithCachedStatements::new(definition)),
502506
}
503507
}
504508
}
505509

506-
struct PreparedPendingStatement {
510+
pub struct PreparedPendingStatement {
507511
stmt: ManagedStmt,
508512
definition: Rc<PendingStatement>,
509513
}

0 commit comments

Comments
 (0)