Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit beea554

Browse files
committed
feat(lora-analyzer): add datetime/rand/range Cypher-compat aliases
1 parent 7977425 commit beea554

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

crates/lora-analyzer/src/analyzer/builtin_signatures.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,13 @@ pub const BUILTIN_ALIASES: &[BuiltinAlias] = &[
476476
alias("type.try_cast", "cast.try"),
477477
alias("type.can_cast", "cast.can"),
478478
alias("now", "temporal.now"),
479+
alias("datetime", "temporal.now"),
479480
alias("timestamp", "temporal.timestamp"),
480481
alias("timezone", "temporal.timezone"),
481482
alias("new", "uuid.new"),
482483
alias("random", "math.random"),
484+
alias("rand", "math.random"),
485+
alias("range", "list.range"),
483486
// Cypher / historical compatibility aliases.
484487
alias("head", "list.first"),
485488
alias("last", "list.last"),
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//! UNWIND-driven bulk-ingestion tests.
2+
//!
3+
//! Covers the playground's canonical "fill the graph from a range" pattern:
4+
//! `UNWIND range(...) AS id CREATE (n {id, computed properties...})`. The
5+
//! shapes here are exactly what a user pastes into the playground when
6+
//! seeding a graph for exploration — Cypher-style function names, a CASE
7+
//! expression in the property map, and a non-trivial per-row workload.
8+
9+
mod test_helpers;
10+
11+
use serde_json::Value as JsonValue;
12+
use test_helpers::TestDb;
13+
14+
// ============================================================
15+
// Cypher-compat aliases — these are the names a Neo4j user reaches for first
16+
// and must resolve to the canonical namespaced builtin without the analyzer
17+
// raising UnknownFunction.
18+
// ============================================================
19+
20+
#[test]
21+
fn range_alias_resolves_to_list_range() {
22+
let db = TestDb::new();
23+
let rows = db.run("UNWIND range(1, 5) AS k RETURN k");
24+
assert_eq!(rows.len(), 5);
25+
assert_eq!(
26+
rows.iter()
27+
.map(|r| r["k"].as_i64().unwrap())
28+
.collect::<Vec<_>>(),
29+
vec![1, 2, 3, 4, 5],
30+
);
31+
}
32+
33+
#[test]
34+
fn range_alias_supports_step_argument() {
35+
let db = TestDb::new();
36+
let rows = db.run("UNWIND range(0, 10, 2) AS k RETURN k");
37+
let values: Vec<i64> = rows.iter().map(|r| r["k"].as_i64().unwrap()).collect();
38+
assert_eq!(values, vec![0, 2, 4, 6, 8, 10]);
39+
}
40+
41+
#[test]
42+
fn rand_alias_resolves_to_math_random() {
43+
let db = TestDb::new();
44+
let rows = db.run("RETURN rand() AS r");
45+
let r = rows[0]["r"].as_f64().unwrap();
46+
assert!((0.0..1.0).contains(&r), "rand() outside [0,1): {r}");
47+
}
48+
49+
#[test]
50+
fn datetime_alias_resolves_to_temporal_now() {
51+
let db = TestDb::new();
52+
let rows = db.run("RETURN datetime() AS t");
53+
// temporal.now() is rendered as a typed datetime object; the shape
54+
// and exact representation belong to temporal.rs — here we just assert
55+
// the alias resolved (no error) and produced a non-null value.
56+
assert!(!rows[0]["t"].is_null(), "datetime() returned null");
57+
}
58+
59+
// ============================================================
60+
// The playground's canonical UNWIND-CREATE pattern.
61+
// ============================================================
62+
63+
#[test]
64+
fn unwind_range_creates_nodes_with_properties() {
65+
let db = TestDb::new();
66+
db.run(
67+
"WITH range(1, 100) AS ids \
68+
UNWIND ids AS id \
69+
CREATE (n:TestRecord { \
70+
id: id, \
71+
name: 'Record ' + toString(id), \
72+
createdAt: datetime(), \
73+
randomValue: rand(), \
74+
status: CASE \
75+
WHEN id % 3 = 0 THEN 'ACTIVE' \
76+
WHEN id % 3 = 1 THEN 'PENDING' \
77+
ELSE 'ARCHIVED' \
78+
END \
79+
})",
80+
);
81+
82+
db.assert_count("MATCH (n:TestRecord) RETURN n", 100);
83+
}
84+
85+
#[test]
86+
fn unwind_range_case_expression_distributes_across_buckets() {
87+
let db = TestDb::new();
88+
db.run(
89+
"UNWIND range(1, 30) AS id \
90+
CREATE (:Bucketed { \
91+
id: id, \
92+
status: CASE \
93+
WHEN id % 3 = 0 THEN 'ACTIVE' \
94+
WHEN id % 3 = 1 THEN 'PENDING' \
95+
ELSE 'ARCHIVED' \
96+
END \
97+
})",
98+
);
99+
100+
// 30 ids, divisible by 3 → 10 in each bucket.
101+
let active = db
102+
.exec_count("MATCH (n:Bucketed {status:'ACTIVE'}) RETURN n")
103+
.unwrap();
104+
let pending = db
105+
.exec_count("MATCH (n:Bucketed {status:'PENDING'}) RETURN n")
106+
.unwrap();
107+
let archived = db
108+
.exec_count("MATCH (n:Bucketed {status:'ARCHIVED'}) RETURN n")
109+
.unwrap();
110+
assert_eq!((active, pending, archived), (10, 10, 10));
111+
}
112+
113+
#[test]
114+
fn unwind_range_tostring_concatenation_produces_unique_names() {
115+
let db = TestDb::new();
116+
db.run(
117+
"UNWIND range(1, 50) AS id \
118+
CREATE (:Named { id: id, name: 'Record ' + toString(id) })",
119+
);
120+
121+
// Properties are unique per id, so name distinct count == row count.
122+
let names: Vec<JsonValue> = db.column("MATCH (n:Named) RETURN n.name AS name", "name");
123+
let mut set = std::collections::BTreeSet::new();
124+
for n in &names {
125+
set.insert(n.as_str().unwrap().to_string());
126+
}
127+
assert_eq!(set.len(), 50);
128+
assert!(set.contains("Record 1"));
129+
assert!(set.contains("Record 50"));
130+
}
131+
132+
#[test]
133+
fn unwind_range_returning_created_nodes_yields_one_row_per_id() {
134+
let db = TestDb::new();
135+
let rows = db.run(
136+
"UNWIND range(1, 25) AS id \
137+
CREATE (n:Returned { id: id }) \
138+
RETURN n.id AS id ORDER BY id",
139+
);
140+
assert_eq!(rows.len(), 25);
141+
for (i, row) in rows.iter().enumerate() {
142+
assert_eq!(row["id"].as_i64().unwrap() as usize, i + 1);
143+
}
144+
}
145+
146+
// ============================================================
147+
// Larger scale: the upper end of what the playground will plausibly accept
148+
// in a single statement. Kept modest (10k) because the parser/analyzer
149+
// recompile the entire UNWIND body per query — for true bulk loads the
150+
// playground should batch in ~2k chunks, see scale.rs.
151+
// ============================================================
152+
153+
#[test]
154+
fn unwind_range_ten_thousand_nodes_single_statement() {
155+
let db = TestDb::new();
156+
db.run(
157+
"UNWIND range(1, 10000) AS id \
158+
CREATE (:BulkNode { id: id, kind: id % 7 })",
159+
);
160+
161+
assert_eq!(db.service.node_count(), 10_000);
162+
163+
// Spot-check a value in the middle and at the boundaries.
164+
let middle = db.scalar("MATCH (n:BulkNode {id: 5000}) RETURN n.kind");
165+
assert_eq!(middle.as_i64().unwrap(), 5000 % 7);
166+
let last = db.scalar("MATCH (n:BulkNode {id: 10000}) RETURN n.kind");
167+
assert_eq!(last.as_i64().unwrap(), 10_000 % 7);
168+
}
169+
170+
#[test]
171+
#[ignore = "100k-in-one-statement is slow; run with --ignored. Real bulk loads should batch."]
172+
fn unwind_range_one_hundred_thousand_nodes_single_statement() {
173+
let db = TestDb::new();
174+
db.run(
175+
"WITH range(1, 100000) AS ids \
176+
UNWIND ids AS id \
177+
CREATE (n:TestRecord { \
178+
id: id, \
179+
name: 'Record ' + toString(id), \
180+
randomValue: rand(), \
181+
status: CASE \
182+
WHEN id % 3 = 0 THEN 'ACTIVE' \
183+
WHEN id % 3 = 1 THEN 'PENDING' \
184+
ELSE 'ARCHIVED' \
185+
END \
186+
})",
187+
);
188+
assert_eq!(db.service.node_count(), 100_000);
189+
}
190+
191+
#[test]
192+
fn batched_unwind_range_one_hundred_thousand_nodes() {
193+
// The pattern the playground should actually use for large ingest:
194+
// many small UNWIND statements rather than one huge one. Mirrors
195+
// scale.rs's approach but at a size that's reasonable for the
196+
// default `cargo test` run.
197+
let db = TestDb::new();
198+
const TOTAL: usize = 100_000;
199+
const BATCH: usize = 2_000;
200+
201+
let mut i = 0;
202+
while i < TOTAL {
203+
let end = (i + BATCH).min(TOTAL);
204+
db.run(&format!(
205+
"UNWIND range({i}, {}) AS id \
206+
CREATE (:Bulk {{ id: id, kind: id % 5 }})",
207+
end - 1,
208+
));
209+
i = end;
210+
}
211+
212+
assert_eq!(db.service.node_count(), TOTAL);
213+
214+
let buckets = db.run("MATCH (n:Bulk) RETURN n.kind AS k, count(n) AS c ORDER BY k");
215+
assert_eq!(buckets.len(), 5);
216+
for row in &buckets {
217+
assert_eq!(row["c"].as_i64().unwrap() as usize, TOTAL / 5);
218+
}
219+
}

0 commit comments

Comments
 (0)