Repository navigation
Expand file tree
/
Copy pathdurable_engine.rs
More file actions
391 lines (356 loc) · 12.5 KB
/
Copy pathdurable_engine.rs
File metadata and controls
391 lines (356 loc) · 12.5 KB
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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
//! Durable objects against the real engine.
//!
//! `durable_conformance.rs` drives the state machine with a fake, which is what
//! lets it reach a second writer and a lost commit response. This file is the
//! other half: what ClickHouse's own parser decides, what a real `BACKUP` and
//! `RESTORE` carry, and what the one-data-path-per-process constraint means for
//! a caller.
//!
//! Every test here needs the process engine to itself, so each takes [`engine`]
//! first and closes its object before returning.
use std::time::Duration;
use chdb_rust::durable::{Category, DurableObject, Namespace, OpenOptions, Tuning};
use chdb_rust::format::OutputFormat;
use chdb_rust::{active_engine_path, active_engine_refs};
mod common;
/// Takes the process engine for one test.
///
/// chdb-core binds one data path per process, so these cannot overlap.
/// Serializing here rather than relying on `--test-threads=1` means a plain
/// `cargo test` reports real failures instead of collisions.
fn engine() -> std::sync::MutexGuard<'static, ()> {
static ENGINE: std::sync::Mutex<()> = std::sync::Mutex::new(());
ENGINE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn namespace(root: &std::path::Path) -> Namespace {
Namespace::new(root.to_str().expect("a UTF-8 root"))
.expect("a local namespace")
.with_owner("engine-test")
.with_tuning(Tuning {
lease_ttl: Duration::from_secs(10),
heartbeat_interval: Duration::from_secs(3),
..Tuning::default()
})
}
fn open(root: &std::path::Path) -> (DurableObject, bool) {
namespace(root)
.open(
"tenant",
OpenOptions {
database: Some("mem".to_string()),
..OpenOptions::default()
},
)
.expect("a writer")
}
fn text(bytes: Vec<u8>) -> String {
String::from_utf8(bytes)
.expect("a UTF-8 result")
.trim()
.to_string()
}
#[test]
fn a_database_survives_the_process_that_wrote_it() {
let _engine = engine();
let tmp = common::tempdir();
let (object, existed) = open(tmp.path());
assert!(!existed);
object
.execute("CREATE TABLE events (id UInt64, tag String) ENGINE = MergeTree ORDER BY id")
.expect("DDL is a mutation like any other");
object
.execute("INSERT INTO events VALUES (1, 'first')")
.expect("a write");
let ticket = object
.execute("INSERT INTO events VALUES (2, 'second')")
.expect("a write");
assert_eq!(
text(
object
.query("SELECT count() FROM events", OutputFormat::CSV)
.expect("a read")
),
"2",
"a write is visible locally the moment it returns"
);
object.flush_through(ticket).expect("a durability barrier");
object.close().expect("a clean close");
// A different handle, a different scratch directory, the same data.
let (object, existed) = open(tmp.path());
assert!(existed);
assert_eq!(
text(
object
.query("SELECT tag FROM events ORDER BY id", OutputFormat::CSV)
.expect("a read")
),
"\"first\"\n\"second\"",
"the WAL replayed onto an empty engine"
);
// And once folded into a base, the WAL is no longer what carries it.
object.checkpoint().expect("a checkpoint");
assert!(object.manifest().wal.is_empty());
object.close().expect("a clean close");
let (object, _) = open(tmp.path());
assert_eq!(
text(
object
.query("SELECT count() FROM events", OutputFormat::CSV)
.expect("a read")
),
"2",
"restored from the base alone"
);
object.close().expect("a clean close");
}
#[test]
fn the_parser_decides_what_a_durable_object_will_run() {
let _engine = engine();
let tmp = common::tempdir();
let (object, _) = open(tmp.path());
object
.execute("CREATE TABLE t (n UInt64) ENGINE = MergeTree ORDER BY n")
.expect("DDL");
let refused = [
// Two statements have no single replayable WAL record.
(
"a batch",
"INSERT INTO t VALUES (1); INSERT INTO t VALUES (2)",
),
// Session state is the object's to manage, not the caller's.
("a current-database change", "USE default"),
("a settings change", "SET max_threads = 1"),
(
"an async-insert relaxation",
"INSERT INTO t SETTINGS async_insert = 1 VALUES (1)",
),
// The object owns one database; its container is not a logged mutation.
("a database lifecycle change", "CREATE DATABASE other"),
// A checkpoint of one database cannot carry state that lives beside it.
("a global UDF", "CREATE FUNCTION plus_one AS (x) -> x + 1"),
// Writes a checkpoint would not capture.
(
"a write to another database",
"INSERT INTO other.t VALUES (1)",
),
(
"a write outside the engine",
"INSERT INTO FUNCTION file('out.csv', CSV) SELECT 1",
),
// Nothing was proven about text that did not parse.
("text that is not SQL", "this is not sql"),
// A read is not a write, and has its own entry point.
("a read", "SELECT 1"),
];
for (what, sql) in refused {
let error = object.execute(sql).expect_err("refused by the parser");
assert_eq!(
error.category(),
Category::ClassificationRefused,
"{what}: {error}"
);
}
// A semicolon inside inline data is data, not a statement boundary — which
// is the case no prefix list or regular expression can get right.
object
.execute("INSERT INTO t VALUES (1)")
.expect("one statement");
object
.execute("ALTER TABLE t ADD COLUMN tag String DEFAULT 'a;b'")
.expect("a semicolon in a literal is not a second statement");
// And query refuses anything that is not exactly one read. Note what is
// *not* in this list: `blah blah` parses, as an implicit SELECT of two
// identifiers, and is a perfectly good read that then fails on an unknown
// column. Only the parser can tell those apart, which is the argument for
// asking it.
for sql in [
"INSERT INTO t VALUES (2)",
"SELECT 1; SELECT 2",
"this is not sql",
] {
let error = object
.query(sql, OutputFormat::CSV)
.expect_err("refused by the parser");
assert_eq!(error.category(), Category::ClassificationRefused, "{sql}");
}
object.close().expect("a clean close");
}
#[test]
fn a_credential_may_be_read_with_but_never_logged() {
let _engine = engine();
let tmp = common::tempdir();
let (object, _) = open(tmp.path());
object
.execute("CREATE TABLE t (n UInt64) ENGINE = MergeTree ORDER BY n")
.expect("DDL");
object
.flush()
.expect("a flush, so what follows is measured from empty");
let secret_write = "INSERT INTO t SELECT 1 FROM \
s3('https://example.invalid/x.csv', 'AKIAEXAMPLE', 'hunter2', 'CSV', 'n UInt64')";
let error = object
.execute(secret_write)
.expect_err("a WAL segment is stored in plain text");
assert_eq!(error.category(), Category::SecretRefused);
assert!(
!error.to_string().contains("hunter2"),
"the refusal must not carry the credential: {error}"
);
// A read carrying the same credential is allowed through the gate: it never
// reaches the WAL. It then fails on the unreachable host, and that failure
// must not echo the statement either.
let error = object
.query(
"SELECT * FROM s3('https://example.invalid/x.csv', 'AKIAEXAMPLE', 'hunter2', 'CSV', 'n UInt64')",
OutputFormat::CSV,
)
.expect_err("example.invalid does not resolve");
assert_eq!(
error.category(),
Category::Engine,
"the gate let it through; the network did not"
);
assert_eq!(
object.stats().pending_statements,
0,
"neither the refused write nor the failed read reached the WAL"
);
object.close().expect("a clean close");
}
#[test]
fn a_database_name_that_needs_quoting_round_trips_through_backup_and_restore() {
let _engine = engine();
let tmp = common::tempdir();
let (object, _) = namespace(tmp.path())
.open(
"tenant",
OpenOptions {
database: Some("my-db".to_string()),
..OpenOptions::default()
},
)
.expect("a writer");
assert_eq!(object.database(), "my-db");
object
.execute("CREATE TABLE t (n UInt64) ENGINE = MergeTree ORDER BY n")
.expect("an unqualified name resolves to the object's database");
object
.execute("INSERT INTO t VALUES (42)")
.expect("a write");
object.checkpoint().expect("a checkpoint of a quoted name");
object.close().expect("a clean close");
let (object, _) = namespace(tmp.path())
.open("tenant", OpenOptions::default())
.expect("a writer");
assert_eq!(
object.database(),
"my-db",
"the object owns its database name, not the caller"
);
assert_eq!(
text(
object
.query("SELECT n FROM t", OutputFormat::CSV)
.expect("a read")
),
"42"
);
object.close().expect("a clean close");
}
#[test]
fn a_second_object_in_one_process_is_refused_with_both_paths_named() {
let _engine = engine();
let tmp = common::tempdir();
let (first, _) = open(tmp.path());
let error = namespace(tmp.path())
.open(
"other-tenant",
OpenOptions {
database: Some("mem".to_string()),
..OpenOptions::default()
},
)
.expect_err("chdb-core binds one data path per process");
assert_eq!(error.category(), Category::Engine);
let message = error.to_string();
assert!(
message.contains("one data path per process"),
"the registry's own message should reach the caller: {message}"
);
first.close().expect("a clean close");
assert_eq!(
active_engine_refs(),
0,
"closing the first object frees the engine"
);
assert_eq!(active_engine_path(), None);
// And now the second one opens, which is the half that proves the failed
// open left nothing behind.
let (second, _) = namespace(tmp.path())
.open(
"other-tenant",
OpenOptions {
database: Some("mem".to_string()),
..OpenOptions::default()
},
)
.expect("the engine is free");
second.close().expect("a clean close");
}
#[test]
fn a_read_only_open_serves_what_was_committed_and_takes_no_lease() {
let _engine = engine();
let tmp = common::tempdir();
let (object, _) = open(tmp.path());
object
.execute("CREATE TABLE t (n UInt64) ENGINE = MergeTree ORDER BY n")
.expect("DDL");
object.execute("INSERT INTO t VALUES (1)").expect("a write");
object.flush().expect("a flush");
object
.execute("INSERT INTO t VALUES (2)")
.expect("a write nobody else will see yet");
object
.close()
.expect("a clean close, which flushes the rest");
let (reader, _) = namespace(tmp.path())
.open(
"tenant",
OpenOptions {
read_only: true,
..OpenOptions::default()
},
)
.expect("a reader");
assert_eq!(
text(
reader
.query("SELECT count() FROM t", OutputFormat::CSV)
.expect("a read")
),
"2",
"close published what execute had only buffered"
);
assert!(reader
.execute("INSERT INTO t VALUES (3)")
.is_err_and(|e| e.category() == Category::ClassificationRefused));
reader.close().expect("a clean close");
}
#[test]
fn a_missing_object_is_not_found_rather_than_created() {
let _engine = engine();
let tmp = common::tempdir();
let error = namespace(tmp.path())
.open(
"never-written",
OpenOptions {
read_only: true,
..OpenOptions::default()
},
)
.expect_err("a reader never creates the object it came to read");
assert_eq!(error.category(), Category::NotFound);
assert_eq!(active_engine_refs(), 0, "and it started no engine");
}