Skip to content

Commit 5c7421a

Browse files
committed
Refactor view management
1 parent b5faf82 commit 5c7421a

File tree

8 files changed

+157
-280
lines changed

8 files changed

+157
-280
lines changed

crates/core/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ fn init_extension(db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> {
6464
let state = Rc::new(DatabaseState::new());
6565

6666
crate::version::register(db)?;
67-
crate::views::register(db)?;
6867
crate::uuid::register(db)?;
6968
crate::diff::register(db)?;
7069
crate::fix_data::register(db)?;

crates/core/src/migrations.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use sqlite::ResultCode;
1010

1111
use crate::error::{PSResult, PowerSyncError};
1212
use crate::fix_data::apply_v035_fix;
13+
use crate::schema::inspection::ExistingView;
1314
use crate::sync::BucketPriority;
1415

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

192+
for mut view in ExistingView::list(local_db)? {
193+
view.delete_trigger_sql = String::default();
194+
view.update_trigger_sql = String::default();
195+
view.insert_trigger_sql = String::default();
196+
197+
// This drops everything, but immediately re-creates the CREATE VIEW statement.
198+
view.create(local_db)?;
199+
}
200+
191201
// language=SQLite
192202
local_db
193203
.exec_safe(
194204
"\
195-
UPDATE powersync_views SET
196-
delete_trigger_sql = '',
197-
update_trigger_sql = '',
198-
insert_trigger_sql = '';
199-
200205
ALTER TABLE ps_buckets RENAME TO ps_buckets_old;
201206
ALTER TABLE ps_oplog RENAME TO ps_oplog_old;
202207
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use alloc::borrow::ToOwned;
2+
use alloc::{format, vec};
3+
use alloc::{string::String, vec::Vec};
4+
use powersync_sqlite_nostd::Connection;
5+
use powersync_sqlite_nostd::{self as sqlite, ResultCode};
6+
7+
use crate::error::{PSResult, PowerSyncError};
8+
use crate::util::quote_identifier;
9+
10+
/// An existing PowerSync-managed view that was found in the schema.
11+
#[derive(PartialEq)]
12+
pub struct ExistingView {
13+
/// The name of the view itself.
14+
pub name: String,
15+
/// SQL contents of the `CREATE VIEW` statement.
16+
pub sql: String,
17+
/// SQL contents of all triggers implementing deletes by forwarding to
18+
/// `ps_data` and `ps_crud`.
19+
pub delete_trigger_sql: String,
20+
/// SQL contents of the trigger implementing inserts on this view.
21+
pub insert_trigger_sql: String,
22+
/// SQL contents of the trigger implementing updates on this view.
23+
pub update_trigger_sql: String,
24+
}
25+
26+
impl ExistingView {
27+
pub fn list(db: *mut sqlite::sqlite3) -> Result<Vec<Self>, PowerSyncError> {
28+
let mut results = vec![];
29+
let stmt = db.prepare_v2("
30+
SELECT
31+
view.name,
32+
view.sql,
33+
ifnull(group_concat(trigger1.sql, ';\n' ORDER BY trigger1.name DESC), ''),
34+
ifnull(trigger2.sql, ''),
35+
ifnull(trigger3.sql, '')
36+
FROM sqlite_master view
37+
LEFT JOIN sqlite_master trigger1
38+
ON trigger1.tbl_name = view.name AND trigger1.type = 'trigger' AND trigger1.name GLOB 'ps_view_delete*'
39+
LEFT JOIN sqlite_master trigger2
40+
ON trigger2.tbl_name = view.name AND trigger2.type = 'trigger' AND trigger2.name GLOB 'ps_view_insert*'
41+
LEFT JOIN sqlite_master trigger3
42+
ON trigger3.tbl_name = view.name AND trigger3.type = 'trigger' AND trigger3.name GLOB 'ps_view_update*'
43+
WHERE view.type = 'view' AND view.sql GLOB '*-- powersync-auto-generated'
44+
GROUP BY view.name;
45+
").into_db_result(db)?;
46+
47+
while stmt.step()? == ResultCode::ROW {
48+
let name = stmt.column_text(0)?.to_owned();
49+
let sql = stmt.column_text(1)?.to_owned();
50+
let delete = stmt.column_text(2)?.to_owned();
51+
let insert = stmt.column_text(3)?.to_owned();
52+
let update = stmt.column_text(4)?.to_owned();
53+
54+
results.push(ExistingView {
55+
name,
56+
sql,
57+
delete_trigger_sql: delete,
58+
insert_trigger_sql: insert,
59+
update_trigger_sql: update,
60+
});
61+
}
62+
63+
Ok(results)
64+
}
65+
66+
pub fn drop_by_name(db: *mut sqlite::sqlite3, name: &str) -> Result<(), PowerSyncError> {
67+
let q = format!("DROP VIEW IF EXISTS {:}", quote_identifier(name));
68+
db.exec_safe(&q)?;
69+
Ok(())
70+
}
71+
72+
pub fn create(&self, db: *mut sqlite::sqlite3) -> Result<(), PowerSyncError> {
73+
Self::drop_by_name(db, &self.name)?;
74+
db.exec_safe(&self.sql).into_db_result(db)?;
75+
db.exec_safe(&self.delete_trigger_sql).into_db_result(db)?;
76+
db.exec_safe(&self.insert_trigger_sql).into_db_result(db)?;
77+
db.exec_safe(&self.update_trigger_sql).into_db_result(db)?;
78+
79+
Ok(())
80+
}
81+
}

crates/core/src/schema/management.rs

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
extern crate alloc;
22

3+
use alloc::borrow::ToOwned;
4+
use alloc::collections::btree_map::BTreeMap;
35
use alloc::rc::Rc;
46
use alloc::string::String;
57
use alloc::vec::Vec;
@@ -12,8 +14,13 @@ use sqlite::{Connection, ResultCode, Value};
1214

1315
use crate::error::{PSResult, PowerSyncError};
1416
use crate::ext::ExtendedDatabase;
17+
use crate::schema::inspection::ExistingView;
1518
use crate::state::DatabaseState;
1619
use crate::util::{quote_identifier, quote_json_path};
20+
use crate::views::{
21+
powersync_trigger_delete_sql, powersync_trigger_insert_sql, powersync_trigger_update_sql,
22+
powersync_view_sql,
23+
};
1724
use crate::{create_auto_tx_function, create_sqlite_text_fn};
1825

1926
use super::Schema;
@@ -236,55 +243,46 @@ SELECT
236243
Ok(())
237244
}
238245

239-
fn update_views(db: *mut sqlite::sqlite3, schema: &str) -> Result<(), PowerSyncError> {
240-
// Update existing views if modified
241-
// language=SQLite
242-
db.exec_text("\
243-
UPDATE powersync_views SET
244-
sql = gen.sql,
245-
delete_trigger_sql = gen.delete_trigger_sql,
246-
insert_trigger_sql = gen.insert_trigger_sql,
247-
update_trigger_sql = gen.update_trigger_sql
248-
FROM (SELECT
249-
ifnull(json_extract(json_each.value, '$.view_name'), json_extract(json_each.value, '$.name')) as name,
250-
powersync_view_sql(json_each.value) as sql,
251-
powersync_trigger_delete_sql(json_each.value) as delete_trigger_sql,
252-
powersync_trigger_insert_sql(json_each.value) as insert_trigger_sql,
253-
powersync_trigger_update_sql(json_each.value) as update_trigger_sql
254-
FROM json_each(json_extract(?, '$.tables'))) as gen
255-
WHERE powersync_views.name = gen.name AND
256-
(powersync_views.sql IS NOT gen.sql OR
257-
powersync_views.delete_trigger_sql IS NOT gen.delete_trigger_sql OR
258-
powersync_views.insert_trigger_sql IS NOT gen.insert_trigger_sql OR
259-
powersync_views.update_trigger_sql IS NOT gen.update_trigger_sql)
260-
", schema).into_db_result(db)?;
261-
262-
// Create new views
263-
// language=SQLite
264-
db.exec_text("\
265-
INSERT INTO powersync_views(
266-
name,
267-
sql,
268-
delete_trigger_sql,
269-
insert_trigger_sql,
270-
update_trigger_sql
271-
)
272-
SELECT
273-
ifnull(json_extract(json_each.value, '$.view_name'), json_extract(json_each.value, '$.name')) as name,
274-
powersync_view_sql(json_each.value) as sql,
275-
powersync_trigger_delete_sql(json_each.value) as delete_trigger_sql,
276-
powersync_trigger_insert_sql(json_each.value) as insert_trigger_sql,
277-
powersync_trigger_update_sql(json_each.value) as update_trigger_sql
278-
FROM json_each(json_extract(?, '$.tables'))
279-
WHERE name NOT IN (SELECT name FROM powersync_views)", schema).into_db_result(db)?;
280-
281-
// Delete old views
282-
// language=SQLite
283-
db.exec_text("\
284-
DELETE FROM powersync_views WHERE name NOT IN (
285-
SELECT ifnull(json_extract(json_each.value, '$.view_name'), json_extract(json_each.value, '$.name'))
286-
FROM json_each(json_extract(?, '$.tables'))
287-
)", schema).into_db_result(db)?;
246+
fn update_views(db: *mut sqlite::sqlite3, schema: &Schema) -> Result<(), PowerSyncError> {
247+
// First, find all existing views and index them by name.
248+
let existing = ExistingView::list(db)?;
249+
let mut existing = {
250+
let mut map = BTreeMap::new();
251+
for entry in &existing {
252+
map.insert(&*entry.name, entry);
253+
}
254+
map
255+
};
256+
257+
for table in &schema.tables {
258+
let view_sql = powersync_view_sql(table);
259+
let delete_trigger_sql = powersync_trigger_delete_sql(table)?;
260+
let insert_trigger_sql = powersync_trigger_insert_sql(table)?;
261+
let update_trigger_sql = powersync_trigger_update_sql(table)?;
262+
263+
let wanted_view = ExistingView {
264+
name: table.view_name().to_owned(),
265+
sql: view_sql,
266+
delete_trigger_sql,
267+
insert_trigger_sql,
268+
update_trigger_sql,
269+
};
270+
271+
if let Some(actual_view) = existing.remove(table.view_name()) {
272+
if *actual_view == wanted_view {
273+
// View exists with identical definition, don't re-create.
274+
continue;
275+
}
276+
}
277+
278+
// View does not exist or has been defined differently, re-create.
279+
wanted_view.create(db)?;
280+
}
281+
282+
// Delete old views.
283+
for remaining in existing.values() {
284+
ExistingView::drop_by_name(db, &remaining.name)?;
285+
}
288286

289287
Ok(())
290288
}
@@ -309,7 +307,7 @@ fn powersync_replace_schema_impl(
309307

310308
update_tables(db, schema)?;
311309
update_indexes(db, &parsed_schema)?;
312-
update_views(db, schema)?;
310+
update_views(db, &parsed_schema)?;
313311

314312
state.set_schema(parsed_schema);
315313
Ok(String::from(""))

crates/core/src/schema/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod inspection;
12
mod management;
23
mod table_info;
34

crates/core/src/schema/table_info.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ pub struct RawTable {
2929
}
3030

3131
impl Table {
32-
pub fn from_json(text: &str) -> Result<Self, serde_json::Error> {
33-
serde_json::from_str(text)
34-
}
35-
3632
pub fn view_name(&self) -> &str {
3733
self.view_name_override
3834
.as_deref()

0 commit comments

Comments
 (0)