@@ -7,29 +7,6 @@ export type CVR = {
7
7
order : number ;
8
8
} ;
9
9
10
- const VERSION_TREE_SQL = /*sql*/ `WITH RECURSIVE "version_tree" AS (
11
- SELECT
12
- "client_group_id",
13
- "version",
14
- "parent_version"
15
- FROM
16
- "client_view"
17
- WHERE
18
- "client_group_id" = $1 AND
19
- "version" = $2
20
-
21
- UNION ALL
22
-
23
- SELECT
24
- cv."client_group_id",
25
- cv."version",
26
- cv."parent_version"
27
- FROM
28
- "client_view" cv
29
- JOIN "version_tree" vt ON vt."parent_version" = cv."version"
30
- WHERE cv."client_group_id" = vt."client_group_id"
31
- )` ;
32
-
33
10
export async function getCVR (
34
11
executor : Executor ,
35
12
clientGroupID : string ,
@@ -49,68 +26,61 @@ export async function getCVR(
49
26
} ;
50
27
}
51
28
52
- export function putCVR ( executor : Executor , cvr : CVR , parentVersion : number ) {
29
+ export function putCVR ( executor : Executor , cvr : CVR ) {
53
30
return executor (
54
31
/*sql*/ `INSERT INTO client_view (
55
32
"client_group_id",
56
33
"client_version",
57
- "version",
58
- "parent_version"
59
- ) VALUES ($1, $2, $3, $4) ON CONFLICT ("client_group_id", "version") DO UPDATE SET
34
+ "version"
35
+ ) VALUES ($1, $2, $3) ON CONFLICT ("client_group_id", "version") DO UPDATE SET
60
36
client_version = excluded.client_version
61
37
` ,
62
- [ cvr . clientGroupID , cvr . clientVersion , cvr . order , parentVersion ] ,
38
+ [ cvr . clientGroupID , cvr . clientVersion , cvr . order ] ,
63
39
) ;
64
40
}
65
41
66
42
/**
67
43
* Find all rows in a table that are not in our CVR table.
44
+ * - either the id is not present
45
+ * - or the version is different
68
46
*
69
- * Multiple tabs could be trying to sync at the same time with different CVRs.
70
- * We path back through our CVR tree to ensure we send the correct data to each tab.
71
- *
72
- * We can prune branches of the client view tree that are no longer needed.
73
- *
74
- * If we receive a request for a pruned branch we'd track back to find the
75
- * greatest common ancestor.
47
+ * We could do cursoring to speed this along.
48
+ * I.e., For a given pull sequence, we can keep track of the last id and version.
49
+ * A "reset pull" brings cursor back to 0 to deal with privacy or query changes.
76
50
*
77
- * We could & should also compact:
78
- * 1. Deletion entries in the client_view_entry table
79
- * 2. Old entity_versions in the client_view_entry table
51
+ * But also, if the frontend drives the queries then we'll never actually be fetching
52
+ * so much as the frontend queries will be reasonably sized. The frontend queries would also
53
+ * be cursored.
80
54
*
81
- * Compared to the old approach, the client_view_entry table now grows
82
- * without bound (and requires compaction) since we store a record for each
83
- * entity_version.
55
+ * E.g., `SELECT * FROM issue WHERE modified > x OR (modified = y AND id > z) ORDER BY modified, id LIMIT 100`
56
+ * Cursored on modified and id.
84
57
*
85
- * We wouldn't have to introduce all of this machinery if
86
- * tabs coordinated sync.
58
+ * This means our compare against the CVR would not be a full table scan of `issue`.
87
59
*/
88
60
export function findUnsentItems (
89
61
executor : Executor ,
90
62
table : keyof typeof TableOrdinal ,
91
63
clientGroupID : string ,
92
- version : number ,
64
+ order : number ,
93
65
limit : number ,
94
66
) {
95
67
// The query used below is the fastest.
96
68
// Other query forms that were tried:
97
69
// 1. 10x slower: SELECT * FROM table WHERE NOT EXISTS (SELECT 1 FROM client_view_entry ...)
98
70
// 2. 2x slower: SELECT id, version FROM table EXCEPT SELECT entity_id, entity_version FROM client_view_entry ...
99
71
// 3. SELECT * FROM table LEFT JOIN client_view_entry ...
100
- const sql = /*sql*/ `
101
- ${ VERSION_TREE_SQL }
102
- SELECT *
103
- FROM "${ table } " t
104
- WHERE (t."id", t."version") NOT IN (
105
- SELECT "entity_id", "entity_version"
106
- FROM "client_view_entry"
107
- WHERE "client_view_version" IN (SELECT "version" FROM "version_tree") AND
108
- "client_group_id" = $1 AND
109
- "entity" = $3
110
- )
111
- LIMIT $4;` ;
72
+ const sql = /*sql*/ `SELECT *
73
+ FROM "${ table } " t
74
+ WHERE (t."id", t."version") NOT IN (
75
+ SELECT "entity_id", "entity_version"
76
+ FROM "client_view_entry"
77
+ WHERE "client_group_id" = $1
78
+ AND "client_view_version" <= $2
79
+ AND "entity" = $3
80
+ )
81
+ LIMIT $4;` ;
112
82
113
- const params = [ clientGroupID , version , TableOrdinal [ table ] , limit ] ;
83
+ const params = [ clientGroupID , order , TableOrdinal [ table ] , limit ] ;
114
84
return executor ( sql , params ) ;
115
85
}
116
86
@@ -124,40 +94,45 @@ export function findDeletions(
124
94
executor : Executor ,
125
95
table : keyof typeof TableOrdinal ,
126
96
clientGroupID : string ,
127
- version : number ,
97
+ order : number ,
128
98
limit : number ,
129
99
) {
130
- const sql = /*sql*/ `${ VERSION_TREE_SQL }
131
- SELECT DISTINCT("entity_id") FROM "client_view_entry" as cve WHERE
132
- cve."client_group_id" = $1 AND
133
- cve."entity" = $3 AND
134
- cve."client_view_version" IN (SELECT "version" FROM "version_tree") AND
135
- -- check that the entity is missing from the base table
136
- NOT EXISTS (
137
- SELECT 1 FROM "${ table } " WHERE id = cve."entity_id"
100
+ // Find rows that are in the CVR table but not in the actual table.
101
+ // Exclude rows that have a delete entry already.
102
+ // TODO: Maybe we can remove the second `NOT EXISTS` if we prune the CVR table.
103
+ // This pruning would mean that we need to record the delete against the
104
+ // current CVR rather than next CVR. If a request comes in for that prior CVR,
105
+ // we return the stored delete records and do not compute deletes.
106
+ return executor (
107
+ /*sql*/ `SELECT "entity_id" FROM "client_view_entry"
108
+ WHERE "client_view_entry"."entity" = $1 AND NOT EXISTS (
109
+ SELECT 1 FROM "${ table } " WHERE id = "client_view_entry"."entity_id"
138
110
) AND
139
- -- check that we didn't already record this deletion
140
- -- and that the row wasn't later resurrected and sent if we did record a deletion
141
- NOT EXISTS (
142
- SELECT 1
143
- FROM client_view_entry as cve2 LEFT JOIN client_view_entry as cve3
144
- ON (
145
- cve2.entity_id = cve3.entity_id AND
146
- cve2.client_view_version < cve3.client_view_version AND
147
- cve2.client_group_id = cve3.client_group_id AND
148
- cve2.entity = cve3.entity
149
- )
150
- WHERE
151
- cve3.entity_id IS NULL AND
152
- cve2.entity_id = cve.entity_id AND
153
- cve2.entity_version IS NULL AND
154
- cve2.client_group_id = $1 AND
155
- cve2.entity = $3 AND
156
- cve2.client_view_version IN (SELECT "version" FROM "version_tree")
157
- )
158
- LIMIT $4` ;
159
- const vars = [ clientGroupID , version , TableOrdinal [ table ] , limit ] ;
160
- return executor ( sql , vars ) ;
111
+ "client_view_entry"."client_group_id" = $2 AND
112
+ "client_view_entry"."client_view_version" <= $3
113
+ AND NOT EXISTS (
114
+ SELECT 1 FROM "client_view_delete_entry" WHERE "client_view_delete_entry"."entity" = $1 AND "client_view_delete_entry"."entity_id" = "client_view_entry"."entity_id"
115
+ AND "client_view_delete_entry"."client_group_id" = $2 AND "client_view_delete_entry"."client_view_version" <= $3
116
+ ) LIMIT $4` ,
117
+ [ TableOrdinal [ table ] , clientGroupID , order , limit ] ,
118
+ ) ;
119
+ }
120
+
121
+ export async function dropCVREntries (
122
+ executor : Executor ,
123
+ clientGroupID : string ,
124
+ order : number ,
125
+ ) {
126
+ await Promise . all ( [
127
+ executor (
128
+ /*sql*/ `DELETE FROM "client_view_entry" WHERE "client_group_id" = $1 AND "client_view_version" > $2` ,
129
+ [ clientGroupID , order ] ,
130
+ ) ,
131
+ executor (
132
+ /*sql*/ `DELETE FROM "client_view_delete_entry" WHERE "client_group_id" = $1 AND "client_view_version" > $2` ,
133
+ [ clientGroupID , order ] ,
134
+ ) ,
135
+ ] ) ;
161
136
}
162
137
163
138
export async function recordUpdates (
@@ -196,7 +171,12 @@ export async function recordUpdates(
196
171
"entity",
197
172
"entity_id",
198
173
"entity_version"
199
- ) VALUES ${ placeholders . join ( ', ' ) } ` ,
174
+ ) VALUES ${ placeholders . join (
175
+ ', ' ,
176
+ ) } ON CONFLICT ("client_group_id", "entity", "entity_id") DO UPDATE SET
177
+ "entity_version" = excluded."entity_version",
178
+ "client_view_version" = excluded."client_view_version"
179
+ ` ,
200
180
values ,
201
181
) ;
202
182
}
@@ -225,22 +205,21 @@ export async function recordDeletes(
225
205
}
226
206
const placeholders = [ ] ;
227
207
const values = [ ] ;
228
- const stride = 5 ;
208
+ const stride = 4 ;
229
209
for ( let i = 0 ; i < ids . length ; i ++ ) {
230
210
placeholders . push (
231
211
`($${ i * stride + 1 } , $${ i * stride + 2 } , $${ i * stride + 3 } , $${
232
212
i * stride + 4
233
- } , $ ${ i * stride + 5 } )`,
213
+ } )`,
234
214
) ;
235
- values . push ( clientGroupID , order , TableOrdinal [ table ] , ids [ i ] , null ) ;
215
+ values . push ( clientGroupID , order , TableOrdinal [ table ] , ids [ i ] ) ;
236
216
}
237
217
await executor (
238
- /*sql*/ `INSERT INTO client_view_entry (
218
+ /*sql*/ `INSERT INTO client_view_delete_entry (
239
219
"client_group_id",
240
220
"client_view_version",
241
221
"entity",
242
- "entity_id",
243
- "entity_version"
222
+ "entity_id"
244
223
) VALUES ${ placeholders . join ( ', ' ) } ` ,
245
224
values ,
246
225
) ;
0 commit comments