CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/sync/editor/db/test/doc.test.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { DBDocument, from_str } from "../doc";
7
import { fromJS } from "immutable";
8
9
describe("create a DBDocument with one primary key, and call methods on it", () => {
10
let doc: DBDocument;
11
const records = fromJS([
12
{ key: "cocalc", value: "sagemath" },
13
{ key: "cloud", value: "collaboration" },
14
]);
15
16
it("creates the db-doc", () => {
17
doc = new DBDocument(new Set(["key"]), new Set([]), records);
18
expect(`${doc}`).toBe("[object Object]");
19
});
20
21
it("convert to string", () => {
22
expect(doc.to_str()).toBe(
23
'{"key":"cloud","value":"collaboration"}\n{"key":"cocalc","value":"sagemath"}'
24
);
25
});
26
27
it("count gives number of records", () => {
28
expect(doc.count()).toBe(2);
29
});
30
31
it("checks equality", () => {
32
expect(doc.is_equal()).toBe(false);
33
expect(doc.is_equal(undefined)).toBe(false);
34
expect(doc.is_equal(doc)).toBe(true);
35
expect(
36
doc.is_equal(new DBDocument(new Set(["key"]), new Set([]), records))
37
).toBe(true);
38
expect(doc.is_equal(new DBDocument(new Set(["key"]), new Set([])))).toBe(
39
false
40
);
41
});
42
43
it("make and apply a patch", () => {
44
const doc2 = doc
45
.set([
46
{ key: "new", value: "value" },
47
{ key: "cloud", value: "computing" },
48
])
49
.delete({ key: "cocalc" });
50
const patch = doc.make_patch(doc2);
51
expect(patch).toEqual([
52
-1,
53
[{ key: "cocalc" }],
54
1,
55
[
56
{ key: "new", value: "value" },
57
{ key: "cloud", value: "computing" },
58
],
59
]);
60
expect(doc.apply_patch(patch).is_equal(doc2)).toBe(true);
61
62
// Apply to doc2 instead -- obviously shouldn't work.
63
expect(doc2.apply_patch(patch).is_equal(doc)).toBe(false);
64
65
// Let's also make a patch in the other direction from doc2 to doc:
66
const patch2 = doc2.make_patch(doc);
67
expect(patch2).toEqual([
68
-1,
69
[{ key: "new" }],
70
1,
71
[
72
{ key: "cocalc", value: "sagemath" },
73
{ key: "cloud", value: "collaboration" },
74
],
75
]);
76
expect(doc2.apply_patch(patch2).is_equal(doc)).toBe(true);
77
});
78
79
it("tests get_one", () => {
80
let x = doc.get_one({ key: "cloud" });
81
if (x == null) throw Error("bug");
82
expect(x.toJS()).toEqual({ key: "cloud", value: "collaboration" });
83
84
// can only search on primary keys
85
expect(() => doc.get_one({ value: "collaboration" })).toThrow(
86
"must be a primary key"
87
);
88
89
x = doc.get_one({});
90
if (x == null) throw Error("bug");
91
expect(x.toJS()).toEqual({ key: "cocalc", value: "sagemath" });
92
});
93
94
it("tests get", () => {
95
expect(doc.get({}).toJS()).toEqual([
96
{ key: "cocalc", value: "sagemath" },
97
{ key: "cloud", value: "collaboration" },
98
]);
99
expect(doc.get({ key: "cloud" }).toJS()).toEqual([
100
{ key: "cloud", value: "collaboration" },
101
]);
102
103
// can only search on primary keys
104
expect(() => doc.get({ value: "collaboration" })).toThrow(
105
"must be a primary key"
106
);
107
});
108
109
it("tests delete", () => {
110
const doc2 = doc.delete({ key: "cloud" });
111
expect(doc2.to_str()).toBe('{"key":"cocalc","value":"sagemath"}');
112
});
113
114
it("tests set changing a field", () => {
115
const doc2 = doc.set({ key: "cloud", value: "computing" });
116
expect(doc2.get({ key: "cloud" }).toJS()).toEqual([
117
{ key: "cloud", value: "computing" },
118
]);
119
});
120
121
it("tests set adding a new field", () => {
122
const doc2 = doc.set({ key: "cloud", other: [1, 2, 3] });
123
expect(doc2.get({ key: "cloud" }).toJS()).toEqual([
124
{ key: "cloud", other: [1, 2, 3], value: "collaboration" },
125
]);
126
});
127
});
128
129
describe("test various types of patches", () => {
130
it("string patch -- atomic", () => {
131
const doc = new DBDocument(
132
new Set(["key"]),
133
new Set([]),
134
fromJS([{ key: "cocalc", value: "a string" }])
135
);
136
const patch = doc.make_patch(
137
doc.set({ key: "cocalc", value: "a different string" })
138
);
139
expect(patch).toEqual([
140
1,
141
[{ key: "cocalc", value: "a different string" }],
142
]);
143
144
// And deleting that field:
145
const patch2 = doc.make_patch(doc.set({ key: "cocalc", value: null }));
146
expect(patch2).toEqual([1, [{ key: "cocalc", value: null }]]);
147
// And yes, it's really deleted:
148
expect(doc.apply_patch(patch2).to_str()).toBe('{"key":"cocalc"}');
149
});
150
151
it("string patch -- diff", () => {
152
const doc = new DBDocument(
153
new Set(["key"]),
154
new Set(["value"]) /* so uses diffs */,
155
fromJS([{ key: "cocalc", value: "a string" }])
156
);
157
const patch = doc.make_patch(
158
doc.set({ key: "cocalc", value: "a different string" })
159
);
160
expect(patch).toEqual([
161
1,
162
[
163
{
164
key: "cocalc",
165
value: [
166
[
167
[
168
[0, "a "],
169
[1, "different "],
170
[0, "string"],
171
],
172
0,
173
0,
174
8,
175
18,
176
],
177
],
178
},
179
],
180
]);
181
182
// And deleting that field, is the same still:
183
const patch2 = doc.make_patch(doc.set({ key: "cocalc", value: null }));
184
expect(patch2).toEqual([1, [{ key: "cocalc", value: null }]]);
185
});
186
187
it("map patch -- our efficient about diffs (NOT atomic)", () => {
188
const doc = new DBDocument(
189
new Set(["key"]),
190
new Set([]),
191
fromJS([{ key: "cocalc", value: { one: 1, two: 2, five: 5 } }])
192
);
193
194
// This really sets just the two part:
195
const doc2 = doc.set({ key: "cocalc", value: { two: "two" } });
196
expect(doc2.to_str()).toBe(
197
'{"key":"cocalc","value":{"five":5,"one":1,"two":"two"}}'
198
);
199
200
// That's why this patch can be compact:
201
const patch2 = doc.make_patch(doc2);
202
expect(patch2).toEqual([1, [{ key: "cocalc", value: { two: "two" } }]]);
203
204
// To completely change the value atomically of a map,
205
// you have to set to null (deleting that field),
206
// then set the new value:
207
let doc3 = doc.set({ key: "cocalc", value: null });
208
doc3 = doc3.set({ key: "cocalc", value: { two: "two" } });
209
expect(doc3.to_str()).toBe('{"key":"cocalc","value":{"two":"two"}}');
210
const patch3 = doc.make_patch(doc3);
211
// It's smart and just puts the deletes in as null setting, rather than
212
// using the two steps we did above:
213
expect(patch3).toEqual([
214
1,
215
[{ key: "cocalc", value: { five: null, one: null, two: "two" } }],
216
]);
217
// And confirm this patch "works":
218
expect(doc.apply_patch(patch3).is_equal(doc3)).toBe(true);
219
});
220
221
it("array patches are just stupidly atomic", () => {
222
const doc = new DBDocument(
223
new Set(["key"]),
224
new Set([]),
225
fromJS([{ key: "cocalc", value: [1, 2, 5] }])
226
);
227
228
// This really sets just the two part:
229
const doc2 = doc.set({ key: "cocalc", value: [1, "2", 5] });
230
expect(doc2.to_str()).toBe('{"key":"cocalc","value":[1,"2",5]}');
231
232
// The patch is NOT in any way **compact** -- it just
233
// sets the whole array.
234
const patch2 = doc.make_patch(doc2);
235
expect(patch2).toEqual([1, [{ key: "cocalc", value: [1, "2", 5] }]]);
236
});
237
238
it("tests that string column runtime type checking works", () => {
239
// This checks that https://github.com/sagemathinc/cocalc/issues/3625
240
// is fixed.
241
const doc = new DBDocument(
242
new Set(["key"]),
243
new Set(["value"]) /* so must always be a string */,
244
fromJS([{ key: "cocalc", value: "a string" }])
245
);
246
247
const doc2 = doc.set({ value: "foo" });
248
const x = doc2.get_one({})!;
249
expect(x.get("value")).toBe("foo");
250
251
expect(() => doc2.set({ value: 0 })).toThrow("must be a string");
252
});
253
});
254
255
describe("test conversion to and *from* strings", () => {
256
it("makes a doc with various data types", () => {
257
const doc = new DBDocument(
258
new Set(["key"]),
259
new Set([]),
260
fromJS([
261
{
262
key: "cocalc",
263
string: "cocalc",
264
number: 389,
265
map: { lat: 5, long: 7 },
266
list: ["milk", "cookies", { a: true }],
267
boolean: true,
268
},
269
{ key: [1, { a: 5 }], number: 37 },
270
])
271
);
272
273
const s = doc.to_str();
274
expect(s).toBe(
275
'{"boolean":true,"key":"cocalc","list":["milk","cookies",{"a":true}],"map":{"lat":5,"long":7},"number":389,"string":"cocalc"}\n{"key":[1,{"a":5}],"number":37}'
276
);
277
278
const doc2 = from_str(s, ["key"], []);
279
expect(doc2.is_equal(doc)).toBe(true);
280
281
// Equality testing ignores primary
282
// keys and string cols, since it's assumed
283
// that Documents are only compared when
284
// these are the same, since the application
285
// is to SyncDoc.
286
const doc3 = from_str(s, ["key"], ["string"]);
287
expect(doc3.is_equal(doc)).toBe(true);
288
const doc4 = from_str(s, ["key", "number"], []);
289
expect(doc4.is_equal(doc)).toBe(true);
290
});
291
});
292
293
describe("test using a compound primary key", () => {
294
// Using a compound primary key lets you have several
295
// separate database tables in the same db-doc.
296
297
const doc = new DBDocument(
298
new Set(["table", "id"]),
299
new Set([]),
300
fromJS([
301
{
302
table: "accounts",
303
id: 123,
304
name: "CoCalc User",
305
},
306
{ table: "projects", id: 123, title: "Test project" },
307
])
308
);
309
310
it("tests searches", () => {
311
expect(doc.get({ table: "accounts", id: 123 }).toJS()).toEqual([
312
{ id: 123, name: "CoCalc User", table: "accounts" },
313
]);
314
expect(doc.get({ table: "projects", id: 123 }).toJS()).toEqual([
315
{ id: 123, table: "projects", title: "Test project" },
316
]);
317
318
// type does matter
319
expect(doc.get({ table: "accounts", id: "123" }).toJS()).toEqual([]);
320
});
321
322
it("tests doing a set of two records (one change and one create)", () => {
323
const doc2 = doc.set([
324
{ table: "accounts", id: 123, name: "CoCalc Sage" },
325
{ table: "accounts", id: 124, name: "Sage Math" },
326
]);
327
expect(doc2.get({ table: "accounts" }).toJS()).toEqual([
328
{ id: 123, name: "CoCalc Sage", table: "accounts" },
329
{ id: 124, name: "Sage Math", table: "accounts" },
330
]);
331
});
332
});
333
334