Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ fn init_extension(db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> {
let state = Rc::new(DatabaseState::new());

crate::version::register(db)?;
crate::views::register(db)?;
crate::uuid::register(db)?;
crate::diff::register(db)?;
crate::fix_data::register(db)?;
Expand Down
15 changes: 10 additions & 5 deletions crates/core/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use sqlite::ResultCode;

use crate::error::{PSResult, PowerSyncError};
use crate::fix_data::apply_v035_fix;
use crate::schema::inspection::ExistingView;
use crate::sync::BucketPriority;

pub const LATEST_VERSION: i32 = 11;
Expand Down Expand Up @@ -188,15 +189,19 @@ VALUES(4,
// Down migrations are less common, so we're okay about that breaking
// in some cases.

for mut view in ExistingView::list(local_db)? {
view.delete_trigger_sql = String::default();
view.update_trigger_sql = String::default();
view.insert_trigger_sql = String::default();

// This drops everything, but immediately re-creates the CREATE VIEW statement.
view.create(local_db)?;
}

// language=SQLite
local_db
.exec_safe(
"\
UPDATE powersync_views SET
delete_trigger_sql = '',
update_trigger_sql = '',
insert_trigger_sql = '';

ALTER TABLE ps_buckets RENAME TO ps_buckets_old;
ALTER TABLE ps_oplog RENAME TO ps_oplog_old;

Expand Down
81 changes: 81 additions & 0 deletions crates/core/src/schema/inspection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use alloc::borrow::ToOwned;
use alloc::{format, vec};
use alloc::{string::String, vec::Vec};
use powersync_sqlite_nostd::Connection;
use powersync_sqlite_nostd::{self as sqlite, ResultCode};

use crate::error::{PSResult, PowerSyncError};
use crate::util::quote_identifier;

/// An existing PowerSync-managed view that was found in the schema.
#[derive(PartialEq)]
pub struct ExistingView {
/// The name of the view itself.
pub name: String,
/// SQL contents of the `CREATE VIEW` statement.
pub sql: String,
/// SQL contents of all triggers implementing deletes by forwarding to
/// `ps_data` and `ps_crud`.
pub delete_trigger_sql: String,
/// SQL contents of the trigger implementing inserts on this view.
pub insert_trigger_sql: String,
/// SQL contents of the trigger implementing updates on this view.
pub update_trigger_sql: String,
}

impl ExistingView {
pub fn list(db: *mut sqlite::sqlite3) -> Result<Vec<Self>, PowerSyncError> {
let mut results = vec![];
let stmt = db.prepare_v2("
SELECT
view.name,
view.sql,
ifnull(group_concat(trigger1.sql, ';\n' ORDER BY trigger1.name DESC), ''),
ifnull(trigger2.sql, ''),
ifnull(trigger3.sql, '')
FROM sqlite_master view
LEFT JOIN sqlite_master trigger1
ON trigger1.tbl_name = view.name AND trigger1.type = 'trigger' AND trigger1.name GLOB 'ps_view_delete*'
LEFT JOIN sqlite_master trigger2
ON trigger2.tbl_name = view.name AND trigger2.type = 'trigger' AND trigger2.name GLOB 'ps_view_insert*'
LEFT JOIN sqlite_master trigger3
ON trigger3.tbl_name = view.name AND trigger3.type = 'trigger' AND trigger3.name GLOB 'ps_view_update*'
WHERE view.type = 'view' AND view.sql GLOB '*-- powersync-auto-generated'
GROUP BY view.name;
").into_db_result(db)?;

while stmt.step()? == ResultCode::ROW {
let name = stmt.column_text(0)?.to_owned();
let sql = stmt.column_text(1)?.to_owned();
let delete = stmt.column_text(2)?.to_owned();
let insert = stmt.column_text(3)?.to_owned();
let update = stmt.column_text(4)?.to_owned();

results.push(ExistingView {
name,
sql,
delete_trigger_sql: delete,
insert_trigger_sql: insert,
update_trigger_sql: update,
});
}

Ok(results)
}

pub fn drop_by_name(db: *mut sqlite::sqlite3, name: &str) -> Result<(), PowerSyncError> {
let q = format!("DROP VIEW IF EXISTS {:}", quote_identifier(name));
db.exec_safe(&q)?;
Ok(())
}

pub fn create(&self, db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> {
Self::drop_by_name(db, &self.name)?;
db.exec_safe(&self.sql).into_db_result(db)?;
db.exec_safe(&self.delete_trigger_sql).into_db_result(db)?;
db.exec_safe(&self.insert_trigger_sql).into_db_result(db)?;
db.exec_safe(&self.update_trigger_sql).into_db_result(db)?;

Ok(())
}
}
98 changes: 48 additions & 50 deletions crates/core/src/schema/management.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
extern crate alloc;

use alloc::borrow::ToOwned;
use alloc::collections::btree_map::BTreeMap;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
Expand All @@ -12,8 +14,13 @@ use sqlite::{Connection, ResultCode, Value};

use crate::error::{PSResult, PowerSyncError};
use crate::ext::ExtendedDatabase;
use crate::schema::inspection::ExistingView;
use crate::state::DatabaseState;
use crate::util::{quote_identifier, quote_json_path};
use crate::views::{
powersync_trigger_delete_sql, powersync_trigger_insert_sql, powersync_trigger_update_sql,
powersync_view_sql,
};
use crate::{create_auto_tx_function, create_sqlite_text_fn};

use super::Schema;
Expand Down Expand Up @@ -236,55 +243,46 @@ SELECT
Ok(())
}

fn update_views(db: *mut sqlite::sqlite3, schema: &str) -> Result<(), PowerSyncError> {
// Update existing views if modified
// language=SQLite
db.exec_text("\
UPDATE powersync_views SET
sql = gen.sql,
delete_trigger_sql = gen.delete_trigger_sql,
insert_trigger_sql = gen.insert_trigger_sql,
update_trigger_sql = gen.update_trigger_sql
FROM (SELECT
ifnull(json_extract(json_each.value, '$.view_name'), json_extract(json_each.value, '$.name')) as name,
powersync_view_sql(json_each.value) as sql,
powersync_trigger_delete_sql(json_each.value) as delete_trigger_sql,
powersync_trigger_insert_sql(json_each.value) as insert_trigger_sql,
powersync_trigger_update_sql(json_each.value) as update_trigger_sql
FROM json_each(json_extract(?, '$.tables'))) as gen
WHERE powersync_views.name = gen.name AND
(powersync_views.sql IS NOT gen.sql OR
powersync_views.delete_trigger_sql IS NOT gen.delete_trigger_sql OR
powersync_views.insert_trigger_sql IS NOT gen.insert_trigger_sql OR
powersync_views.update_trigger_sql IS NOT gen.update_trigger_sql)
", schema).into_db_result(db)?;

// Create new views
// language=SQLite
db.exec_text("\
INSERT INTO powersync_views(
name,
sql,
delete_trigger_sql,
insert_trigger_sql,
update_trigger_sql
)
SELECT
ifnull(json_extract(json_each.value, '$.view_name'), json_extract(json_each.value, '$.name')) as name,
powersync_view_sql(json_each.value) as sql,
powersync_trigger_delete_sql(json_each.value) as delete_trigger_sql,
powersync_trigger_insert_sql(json_each.value) as insert_trigger_sql,
powersync_trigger_update_sql(json_each.value) as update_trigger_sql
FROM json_each(json_extract(?, '$.tables'))
WHERE name NOT IN (SELECT name FROM powersync_views)", schema).into_db_result(db)?;

// Delete old views
// language=SQLite
db.exec_text("\
DELETE FROM powersync_views WHERE name NOT IN (
SELECT ifnull(json_extract(json_each.value, '$.view_name'), json_extract(json_each.value, '$.name'))
FROM json_each(json_extract(?, '$.tables'))
)", schema).into_db_result(db)?;
fn update_views(db: *mut sqlite::sqlite3, schema: &Schema) -> Result<(), PowerSyncError> {
// First, find all existing views and index them by name.
let existing = ExistingView::list(db)?;
let mut existing = {
let mut map = BTreeMap::new();
for entry in &existing {
map.insert(&*entry.name, entry);
}
map
};

for table in &schema.tables {
let view_sql = powersync_view_sql(table);
let delete_trigger_sql = powersync_trigger_delete_sql(table)?;
let insert_trigger_sql = powersync_trigger_insert_sql(table)?;
let update_trigger_sql = powersync_trigger_update_sql(table)?;

let wanted_view = ExistingView {
name: table.view_name().to_owned(),
sql: view_sql,
delete_trigger_sql,
insert_trigger_sql,
update_trigger_sql,
};

if let Some(actual_view) = existing.remove(table.view_name()) {
if *actual_view == wanted_view {
// View exists with identical definition, don't re-create.
continue;
}
}

// View does not exist or has been defined differently, re-create.
wanted_view.create(db)?;
}

// Delete old views.
for remaining in existing.values() {
ExistingView::drop_by_name(db, &remaining.name)?;
}

Ok(())
}
Expand All @@ -309,7 +307,7 @@ fn powersync_replace_schema_impl(

update_tables(db, schema)?;
update_indexes(db, &parsed_schema)?;
update_views(db, schema)?;
update_views(db, &parsed_schema)?;

state.set_schema(parsed_schema);
Ok(String::from(""))
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/schema/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod inspection;
mod management;
mod table_info;

Expand Down
4 changes: 0 additions & 4 deletions crates/core/src/schema/table_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ pub struct RawTable {
}

impl Table {
pub fn from_json(text: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(text)
}

pub fn view_name(&self) -> &str {
self.view_name_override
.as_deref()
Expand Down
Loading