Skip to content

Commit 48d69d4

Browse files
committed
Refactor table admin view
1 parent 5c7421a commit 48d69d4

File tree

5 files changed

+109
-204
lines changed

5 files changed

+109
-204
lines changed

crates/core/src/fix_data.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use alloc::string::String;
55

66
use crate::create_sqlite_optional_text_fn;
77
use crate::error::{PSResult, PowerSyncError};
8+
use crate::schema::inspection::ExistingTable;
89
use powersync_sqlite_nostd::{self as sqlite, ColumnType, Value};
910
use powersync_sqlite_nostd::{Connection, Context, ResultCode};
1011

@@ -23,12 +24,15 @@ use crate::util::quote_identifier;
2324
pub fn apply_v035_fix(db: *mut sqlite::sqlite3) -> Result<i64, PowerSyncError> {
2425
// language=SQLite
2526
let statement = db
26-
.prepare_v2("SELECT name, powersync_external_table_name(name) FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data__*'")
27-
.into_db_result(db)?;
27+
.prepare_v2("SELECT name FROM sqlite_master WHERE type='table' AND name GLOB 'ps_data__*'")
28+
.into_db_result(db)?;
2829

2930
while statement.step()? == ResultCode::ROW {
3031
let full_name = statement.column_text(0)?;
31-
let short_name = statement.column_text(1)?;
32+
let Some((short_name, _)) = ExistingTable::external_name(full_name) else {
33+
continue;
34+
};
35+
3236
let quoted = quote_identifier(full_name);
3337

3438
// language=SQLite

crates/core/src/schema/inspection.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,53 @@ SELECT
7979
Ok(())
8080
}
8181
}
82+
83+
pub struct ExistingTable {
84+
pub name: String,
85+
pub internal_name: String,
86+
pub local_only: bool,
87+
}
88+
89+
impl ExistingTable {
90+
pub fn list(db: *mut sqlite::sqlite3) -> Result<Vec<Self>, PowerSyncError> {
91+
let mut results = vec![];
92+
let stmt = db
93+
.prepare_v2(
94+
"
95+
SELECT name FROM sqlite_master WHERE type = 'table' AND name GLOB 'ps_data_*';
96+
",
97+
)
98+
.into_db_result(db)?;
99+
100+
while stmt.step()? == ResultCode::ROW {
101+
let internal_name = stmt.column_text(0)?;
102+
let Some((name, local_only)) = Self::external_name(internal_name) else {
103+
continue;
104+
};
105+
106+
results.push(ExistingTable {
107+
internal_name: internal_name.to_owned(),
108+
name: name.to_owned(),
109+
local_only: local_only,
110+
});
111+
}
112+
113+
Ok(results)
114+
}
115+
116+
/// Extracts the public name from a `ps_data__` or a `ps_data_local__` table.
117+
///
118+
/// Also returns whether the name is from a local table.
119+
pub fn external_name(name: &str) -> Option<(&str, bool)> {
120+
const LOCAL_PREFIX: &str = "ps_data_local__";
121+
const NORMAL_PREFIX: &str = "ps_data__";
122+
123+
if name.starts_with(LOCAL_PREFIX) {
124+
Some((&name[LOCAL_PREFIX.len()..], true))
125+
} else if name.starts_with(NORMAL_PREFIX) {
126+
Some((&name[NORMAL_PREFIX.len()..], false))
127+
} else {
128+
None
129+
}
130+
}
131+
}

crates/core/src/schema/management.rs

Lines changed: 47 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use sqlite::{Connection, ResultCode, Value};
1414

1515
use crate::error::{PSResult, PowerSyncError};
1616
use crate::ext::ExtendedDatabase;
17-
use crate::schema::inspection::ExistingView;
17+
use crate::schema::inspection::{ExistingTable, ExistingView};
1818
use crate::state::DatabaseState;
1919
use crate::util::{quote_identifier, quote_json_path};
2020
use crate::views::{
@@ -25,112 +25,62 @@ use crate::{create_auto_tx_function, create_sqlite_text_fn};
2525

2626
use super::Schema;
2727

28-
fn update_tables(db: *mut sqlite::sqlite3, schema: &str) -> Result<(), PowerSyncError> {
29-
{
30-
// In a block so that the statement is finalized before dropping tables
31-
// language=SQLite
32-
let statement = db
33-
.prepare_v2(
34-
"\
35-
SELECT
36-
json_extract(json_each.value, '$.name') as name,
37-
powersync_internal_table_name(json_each.value) as internal_name,
38-
ifnull(json_extract(json_each.value, '$.local_only'), 0) as local_only
39-
FROM json_each(json_extract(?, '$.tables'))
40-
WHERE name NOT IN (SELECT name FROM powersync_tables)",
41-
)
42-
.into_db_result(db)?;
43-
statement.bind_text(1, schema, sqlite::Destructor::STATIC)?;
44-
45-
while statement.step().into_db_result(db)? == ResultCode::ROW {
46-
let name = statement.column_text(0)?;
47-
let internal_name = statement.column_text(1)?;
48-
let local_only = statement.column_int(2) != 0;
49-
50-
db.exec_safe(&format!(
51-
"CREATE TABLE {:}(id TEXT PRIMARY KEY NOT NULL, data TEXT)",
52-
quote_identifier(internal_name)
53-
))
54-
.into_db_result(db)?;
28+
fn update_tables(db: *mut sqlite::sqlite3, schema: &Schema) -> Result<(), PowerSyncError> {
29+
let existing_tables = ExistingTable::list(db)?;
30+
let mut existing_tables = {
31+
let mut map = BTreeMap::new();
32+
for table in &existing_tables {
33+
map.insert(&*table.name, table);
34+
}
35+
map
36+
};
5537

56-
if !local_only {
57-
// MOVE data if any
58-
db.exec_text(
59-
&format!(
60-
"INSERT INTO {:}(id, data)
61-
SELECT id, data
62-
FROM ps_untyped
63-
WHERE type = ?",
64-
quote_identifier(internal_name)
65-
),
66-
name,
67-
)
38+
{
39+
// In a block so that all statements are finalized before dropping tables.
40+
for table in &schema.tables {
41+
if let Some(_) = existing_tables.remove(&*table.name) {
42+
// This table exists already, nothing to do.
43+
// TODO: Handle switch between local only <-> regular tables?
44+
} else {
45+
// New table.
46+
let quoted_internal_name = quote_identifier(&table.internal_name());
47+
48+
db.exec_safe(&format!(
49+
"CREATE TABLE {:}(id TEXT PRIMARY KEY NOT NULL, data TEXT)",
50+
quoted_internal_name
51+
))
6852
.into_db_result(db)?;
6953

70-
// language=SQLite
71-
db.exec_text(
72-
"DELETE
73-
FROM ps_untyped
74-
WHERE type = ?",
75-
name,
76-
)?;
77-
}
78-
79-
if !local_only {
80-
// MOVE data if any
81-
db.exec_text(
82-
&format!(
83-
"INSERT INTO {:}(id, data)
54+
if !table.local_only() {
55+
// MOVE data if any
56+
db.exec_text(
57+
&format!(
58+
"INSERT INTO {:}(id, data)
8459
SELECT id, data
8560
FROM ps_untyped
8661
WHERE type = ?",
87-
quote_identifier(internal_name)
88-
),
89-
name,
90-
)
91-
.into_db_result(db)?;
92-
93-
// language=SQLite
94-
db.exec_text(
95-
"DELETE
96-
FROM ps_untyped
97-
WHERE type = ?",
98-
name,
99-
)?;
62+
quoted_internal_name
63+
),
64+
&table.name,
65+
)
66+
.into_db_result(db)?;
67+
68+
// language=SQLite
69+
db.exec_text("DELETE FROM ps_untyped WHERE type = ?", &table.name)?;
70+
}
10071
}
10172
}
102-
}
103-
104-
let mut tables_to_drop: Vec<String> = alloc::vec![];
105-
106-
{
107-
// In a block so that the statement is finalized before dropping tables
108-
// language=SQLite
109-
let statement = db
110-
.prepare_v2(
111-
"\
112-
SELECT name, internal_name, local_only FROM powersync_tables WHERE name NOT IN (
113-
SELECT json_extract(json_each.value, '$.name')
114-
FROM json_each(json_extract(?, '$.tables'))
115-
)",
116-
)
117-
.into_db_result(db)?;
118-
statement.bind_text(1, schema, sqlite::Destructor::STATIC)?;
119-
120-
while statement.step()? == ResultCode::ROW {
121-
let name = statement.column_text(0)?;
122-
let internal_name = statement.column_text(1)?;
123-
let local_only = statement.column_int(2) != 0;
124-
125-
tables_to_drop.push(String::from(internal_name));
12673

127-
if !local_only {
74+
// Remaining tables need to be dropped. But first, we want to move their contents to
75+
// ps_untyped.
76+
for remaining in existing_tables.values() {
77+
if !remaining.local_only {
12878
db.exec_text(
12979
&format!(
13080
"INSERT INTO ps_untyped(type, id, data) SELECT ?, id, data FROM {:}",
131-
quote_identifier(internal_name)
81+
quote_identifier(&remaining.internal_name)
13282
),
133-
name,
83+
&remaining.name,
13484
)
13585
.into_db_result(db)?;
13686
}
@@ -139,8 +89,8 @@ SELECT name, internal_name, local_only FROM powersync_tables WHERE name NOT IN (
13989

14090
// We cannot have any open queries on sqlite_master at the point that we drop tables, otherwise
14191
// we get "table is locked" errors.
142-
for internal_name in tables_to_drop {
143-
let q = format!("DROP TABLE {:}", quote_identifier(&internal_name));
92+
for remaining in existing_tables.values() {
93+
let q = format!("DROP TABLE {:}", quote_identifier(&remaining.internal_name));
14494
db.exec_safe(&q).into_db_result(db)?;
14595
}
14696

@@ -305,7 +255,7 @@ fn powersync_replace_schema_impl(
305255
// language=SQLite
306256
db.exec_safe("SELECT powersync_init()").into_db_result(db)?;
307257

308-
update_tables(db, schema)?;
258+
update_tables(db, &parsed_schema)?;
309259
update_indexes(db, &parsed_schema)?;
310260
update_views(db, &parsed_schema)?;
311261

crates/core/src/schema/table_info.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ impl Table {
3535
.unwrap_or(self.name.as_str())
3636
}
3737

38+
pub fn local_only(&self) -> bool {
39+
self.flags.local_only()
40+
}
41+
3842
pub fn internal_name(&self) -> String {
39-
if self.flags.local_only() {
43+
if self.local_only() {
4044
format!("ps_data_local__{:}", self.name)
4145
} else {
4246
format!("ps_data__{:}", self.name)

0 commit comments

Comments
 (0)