-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathview_admin.rs
377 lines (322 loc) · 10.6 KB
/
view_admin.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
extern crate alloc;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::ffi::c_int;
use core::slice;
use sqlite::{ResultCode, Value};
use sqlite_nostd as sqlite;
use sqlite_nostd::{Connection, Context};
use crate::error::SQLiteError;
use crate::migrations::{powersync_migrate, LATEST_VERSION};
use crate::util::quote_identifier;
use crate::{create_auto_tx_function, create_sqlite_text_fn};
fn powersync_drop_view_impl(
ctx: *mut sqlite::context,
args: &[*mut sqlite::value],
) -> Result<String, ResultCode> {
let name = args[0].text();
let local_db = ctx.db_handle();
let q = format!("DROP VIEW IF EXISTS {:}", quote_identifier(name));
let stmt2 = local_db.prepare_v2(&q)?;
if stmt2.step()? == ResultCode::ROW {
Ok(String::from(name))
} else {
Ok(String::from(""))
}
}
create_sqlite_text_fn!(
powersync_drop_view,
powersync_drop_view_impl,
"powersync_drop_view"
);
fn powersync_exec_impl(
ctx: *mut sqlite::context,
args: &[*mut sqlite::value],
) -> Result<String, ResultCode> {
let q = args[0].text();
if q != "" {
let local_db = ctx.db_handle();
local_db.exec_safe(q)?;
}
Ok(String::from(""))
}
create_sqlite_text_fn!(powersync_exec, powersync_exec_impl, "powersync_exec");
fn powersync_internal_table_name_impl(
ctx: *mut sqlite::context,
args: &[*mut sqlite::value],
) -> Result<String, ResultCode> {
// schema: JSON
let schema = args[0].text();
let local_db = ctx.db_handle();
// language=SQLite
let stmt1 = local_db.prepare_v2(
"SELECT json_extract(?1, '$.name') as name, ifnull(json_extract(?1, '$.local_only'), 0)",
)?;
stmt1.bind_text(1, schema, sqlite::Destructor::STATIC)?;
let step_result = stmt1.step()?;
if step_result != ResultCode::ROW {
return Err(ResultCode::SCHEMA);
}
let name = stmt1.column_text(0)?;
let local_only = stmt1.column_int(1)? != 0;
if local_only {
Ok(format!("ps_data_local__{:}", name))
} else {
Ok(format!("ps_data__{:}", name))
}
}
create_sqlite_text_fn!(
powersync_internal_table_name,
powersync_internal_table_name_impl,
"powersync_internal_table_name"
);
fn powersync_external_table_name_impl(
_ctx: *mut sqlite::context,
args: &[*mut sqlite::value],
) -> Result<String, SQLiteError> {
// name: full table name
let name = args[0].text();
if name.starts_with("ps_data_local__") {
Ok(String::from(&name[15..]))
} else if name.starts_with("ps_data__") {
Ok(String::from(&name[9..]))
} else {
Err(SQLiteError::from(ResultCode::CONSTRAINT_DATATYPE))
}
}
create_sqlite_text_fn!(
powersync_external_table_name,
powersync_external_table_name_impl,
"powersync_external_table_name"
);
fn powersync_init_impl(
ctx: *mut sqlite::context,
_args: &[*mut sqlite::value],
) -> Result<String, SQLiteError> {
let local_db = ctx.db_handle();
setup_internal_views(local_db)?;
powersync_migrate(ctx, LATEST_VERSION)?;
Ok(String::from(""))
}
create_auto_tx_function!(powersync_init_tx, powersync_init_impl);
create_sqlite_text_fn!(powersync_init, powersync_init_tx, "powersync_init");
fn powersync_test_migration_impl(
ctx: *mut sqlite::context,
args: &[*mut sqlite::value],
) -> Result<String, SQLiteError> {
let target_version = args[0].int();
powersync_migrate(ctx, target_version)?;
Ok(String::from(""))
}
create_auto_tx_function!(powersync_test_migration_tx, powersync_test_migration_impl);
create_sqlite_text_fn!(
powersync_test_migration,
powersync_test_migration_tx,
"powersync_test_migration"
);
fn powersync_clear_impl(
ctx: *mut sqlite::context,
args: &[*mut sqlite::value],
) -> Result<String, SQLiteError> {
let local_db = ctx.db_handle();
let clear_local = args[0].int();
// language=SQLite
local_db.exec_safe(
"\
DELETE FROM ps_oplog;
DELETE FROM ps_crud;
DELETE FROM ps_buckets;
DELETE FROM ps_untyped;
DELETE FROM ps_updated_rows;
DELETE FROM ps_kv WHERE key != 'client_id';
DELETE FROM ps_sync_state;
",
)?;
let table_glob = if clear_local != 0 {
"ps_data_*"
} else {
"ps_data__*"
};
let tables_stmt = local_db
.prepare_v2("SELECT name FROM sqlite_master WHERE type='table' AND name GLOB ?1")?;
tables_stmt.bind_text(1, table_glob, sqlite::Destructor::STATIC)?;
let mut tables: Vec<String> = alloc::vec![];
while tables_stmt.step()? == ResultCode::ROW {
let name = tables_stmt.column_text(0)?;
tables.push(name.to_string());
}
for name in tables {
let quoted = quote_identifier(&name);
// The first delete statement deletes a single row, to trigger an update notification for the table.
// The second delete statement uses the truncate optimization to delete the remainder of the data.
let delete_sql = format!(
"\
DELETE FROM {table} WHERE rowid IN (SELECT rowid FROM {table} LIMIT 1);
DELETE FROM {table};",
table = quoted
);
local_db.exec_safe(&delete_sql)?;
}
Ok(String::from(""))
}
create_auto_tx_function!(powersync_clear_tx, powersync_clear_impl);
create_sqlite_text_fn!(powersync_clear, powersync_clear_tx, "powersync_clear");
fn setup_internal_views(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
// powersync_views - just filters sqlite_master, and combines the view and related triggers
// into one row.
// These views are only usable while the extension is loaded, so use TEMP views.
// TODO: This should not be a public view - implement internally instead
// language=SQLite
db.exec_safe("\
CREATE TEMP VIEW IF NOT EXISTS powersync_views(name, sql, delete_trigger_sql, insert_trigger_sql, update_trigger_sql)
AS SELECT
view.name name,
view.sql sql,
ifnull(trigger1.sql, '') delete_trigger_sql,
ifnull(trigger2.sql, '') insert_trigger_sql,
ifnull(trigger3.sql, '') update_trigger_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';
CREATE TRIGGER IF NOT EXISTS powersync_views_insert
INSTEAD OF INSERT ON powersync_views
FOR EACH ROW
BEGIN
SELECT powersync_drop_view(NEW.name);
SELECT powersync_exec(NEW.sql);
SELECT powersync_exec(NEW.delete_trigger_sql);
SELECT powersync_exec(NEW.insert_trigger_sql);
SELECT powersync_exec(NEW.update_trigger_sql);
END;
CREATE TRIGGER IF NOT EXISTS powersync_views_update
INSTEAD OF UPDATE ON powersync_views
FOR EACH ROW
BEGIN
SELECT powersync_drop_view(OLD.name);
SELECT powersync_exec(NEW.sql);
SELECT powersync_exec(NEW.delete_trigger_sql);
SELECT powersync_exec(NEW.insert_trigger_sql);
SELECT powersync_exec(NEW.update_trigger_sql);
END;
CREATE TRIGGER IF NOT EXISTS powersync_views_delete
INSTEAD OF DELETE ON powersync_views
FOR EACH ROW
BEGIN
SELECT powersync_drop_view(OLD.name);
END;")?;
// language=SQLite
db.exec_safe(
"\
CREATE TEMP VIEW IF NOT EXISTS powersync_tables(name, internal_name, local_only)
AS SELECT
powersync_external_table_name(name) as name,
name as internal_name,
name GLOB 'ps_data_local__*' as local_only
FROM sqlite_master
WHERE type = 'table' AND name GLOB 'ps_data_*';",
)?;
Ok(())
}
pub fn register(db: *mut sqlite::sqlite3) -> Result<(), ResultCode> {
// This entire module is just making it easier to edit sqlite_master using queries.
// The primary interfaces exposed are:
// 1. Individual views:
//
// CREATE VIEW powersync_views(name TEXT, sql TEXT, delete_trigger_sql TEXT, insert_trigger_sql TEXT, update_trigger_sql TEXT)
//
// The views can be queried and updated using powersync_views.
// UPSERT is not supported on powersync_views (or any view or virtual table for that matter),
// but "INSERT OR REPLACE" is supported. However, it's a potentially expensive operation
// (drops and re-creates the view and trigger), so avoid where possible.
//
// 2. All-in-one schema updates:
//
// INSERT INTO powersync_replace_schema(schema) VALUES('{"tables": [...]}');
//
// This takes care of updating, inserting and deleting powersync_views to get it in sync
// with the schema.
//
// The same results could be achieved using virtual tables, but the interface would remain the same.
// A potential disadvantage of using views is that the JSON may be re-parsed multiple times.
// Internal function, used in triggers for powersync_views.
db.create_function_v2(
"powersync_drop_view",
1,
sqlite::UTF8,
None,
Some(powersync_drop_view),
None,
None,
None,
)?;
// Internal function, used in triggers for powersync_views.
db.create_function_v2(
"powersync_exec",
1,
sqlite::UTF8,
None,
Some(powersync_exec),
None,
None,
None,
)?;
// Initialize the extension internal tables.
db.create_function_v2(
"powersync_init",
0,
sqlite::UTF8,
None,
Some(powersync_init),
None,
None,
None,
)?;
db.create_function_v2(
"powersync_test_migration",
1,
sqlite::UTF8,
None,
Some(powersync_test_migration),
None,
None,
None,
)?;
// Initialize the extension internal tables.
db.create_function_v2(
"powersync_clear",
1,
sqlite::UTF8,
None,
Some(powersync_clear),
None,
None,
None,
)?;
db.create_function_v2(
"powersync_external_table_name",
1,
sqlite::UTF8 | sqlite::DETERMINISTIC,
None,
Some(powersync_external_table_name),
None,
None,
None,
)?;
db.create_function_v2(
"powersync_internal_table_name",
1,
sqlite::UTF8 | sqlite::DETERMINISTIC,
None,
Some(powersync_internal_table_name),
None,
None,
None,
)?;
Ok(())
}