Skip to content

Commit 17f7749

Browse files
committed
Split semicolonless persisted definitions
1 parent 2e0c57a commit 17f7749

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

sidemantic-rs/src/ffi.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,63 @@ fn starts_with_definition_keyword(sql: &str, keyword: &str) -> bool {
416416
.unwrap_or(true)
417417
}
418418

419+
const DEFINITION_KEYWORDS: &[&str] = &[
420+
"MODEL",
421+
"METRIC",
422+
"DIMENSION",
423+
"SEGMENT",
424+
"RELATIONSHIP",
425+
"PARAMETER",
426+
"PRE_AGGREGATION",
427+
];
428+
429+
fn definition_keyword_at_line_start(bytes: &[u8], idx: usize) -> bool {
430+
let Some(byte) = bytes.get(idx) else {
431+
return false;
432+
};
433+
if !byte.is_ascii_alphabetic() {
434+
return false;
435+
}
436+
437+
let line_start = bytes[..idx]
438+
.iter()
439+
.rposition(|byte| *byte == b'\n' || *byte == b'\r')
440+
.map(|pos| pos + 1)
441+
.unwrap_or(0);
442+
if !bytes[line_start..idx]
443+
.iter()
444+
.all(|byte| byte.is_ascii_whitespace())
445+
{
446+
return false;
447+
}
448+
449+
DEFINITION_KEYWORDS
450+
.iter()
451+
.any(|keyword| keyword_matches_at(bytes, idx, keyword.as_bytes()))
452+
}
453+
454+
fn keyword_matches_at(bytes: &[u8], idx: usize, keyword: &[u8]) -> bool {
455+
let Some(candidate) = bytes.get(idx..idx + keyword.len()) else {
456+
return false;
457+
};
458+
if !candidate.eq_ignore_ascii_case(keyword) {
459+
return false;
460+
}
461+
462+
bytes
463+
.get(idx + keyword.len())
464+
.map(|byte| byte.is_ascii_whitespace())
465+
.unwrap_or(true)
466+
}
467+
419468
fn statement_ranges(block: &str) -> Vec<(usize, usize)> {
420469
let mut ranges = Vec::new();
421470
let mut start = None;
422471
let mut in_single_quote = false;
423472
let mut in_double_quote = false;
424473
let mut in_line_comment = false;
425474
let mut in_block_comment = false;
475+
let mut paren_depth = 0usize;
426476
let bytes = block.as_bytes();
427477
let mut idx = 0;
428478

@@ -486,6 +536,16 @@ fn statement_ranges(block: &str) -> Vec<(usize, usize)> {
486536
start = Some(idx);
487537
}
488538

539+
if let Some(statement_start) = start {
540+
if paren_depth == 0
541+
&& idx != statement_start
542+
&& definition_keyword_at_line_start(bytes, idx)
543+
{
544+
ranges.push((statement_start, idx));
545+
start = Some(idx);
546+
}
547+
}
548+
489549
if byte == b'-' && bytes.get(idx + 1) == Some(&b'-') {
490550
in_line_comment = true;
491551
idx += 2;
@@ -500,10 +560,13 @@ fn statement_ranges(block: &str) -> Vec<(usize, usize)> {
500560
match byte {
501561
b'\'' => in_single_quote = true,
502562
b'"' => in_double_quote = true,
563+
b'(' => paren_depth = paren_depth.saturating_add(1),
564+
b')' => paren_depth = paren_depth.saturating_sub(1),
503565
b';' => {
504566
if let Some(statement_start) = start.take() {
505567
ranges.push((statement_start, idx + 1));
506568
}
569+
paren_depth = 0;
507570
}
508571
_ => {}
509572
}
@@ -1838,6 +1901,74 @@ models:
18381901
remove_definitions_file(&db_path);
18391902
}
18401903

1904+
#[test]
1905+
fn test_semicolonless_persisted_model_blocks_stay_separate_for_updates() {
1906+
let _guard = test_lock();
1907+
sidemantic_clear();
1908+
1909+
let db_path = unique_db_path("semicolonless_model_blocks");
1910+
let db_path = CString::new(db_path.to_string_lossy().to_string()).unwrap();
1911+
remove_definitions_file(&db_path);
1912+
let definitions_path = get_definitions_path(db_path.as_ptr()).unwrap();
1913+
1914+
let orders =
1915+
CString::new("MODEL (name orders, table orders, primary_key order_id)").unwrap();
1916+
let customers =
1917+
CString::new("MODEL (name customers, table customers, primary_key customer_id)")
1918+
.unwrap();
1919+
assert_success(sidemantic_define(orders.as_ptr(), db_path.as_ptr(), false));
1920+
assert_success(sidemantic_define(
1921+
customers.as_ptr(),
1922+
db_path.as_ptr(),
1923+
false,
1924+
));
1925+
1926+
let initial = fs::read_to_string(&definitions_path).unwrap();
1927+
assert_eq!(split_definitions(&initial).len(), 2, "{initial}");
1928+
1929+
let metric = CString::new("METRIC customers.customer_count AS COUNT(*)").unwrap();
1930+
assert_success(sidemantic_add_definition(
1931+
metric.as_ptr(),
1932+
db_path.as_ptr(),
1933+
false,
1934+
));
1935+
1936+
let replacement =
1937+
CString::new("MODEL (name orders, table orders_v2, primary_key order_id)").unwrap();
1938+
assert_success(sidemantic_define(
1939+
replacement.as_ptr(),
1940+
db_path.as_ptr(),
1941+
true,
1942+
));
1943+
1944+
let updated = fs::read_to_string(&definitions_path).unwrap();
1945+
let blocks = split_definitions(&updated);
1946+
assert_eq!(blocks.len(), 2, "{updated}");
1947+
assert!(updated.contains("orders_v2"), "{updated}");
1948+
assert!(updated.contains("customer_count"), "{updated}");
1949+
1950+
let orders_model = blocks
1951+
.iter()
1952+
.find_map(|block| {
1953+
let model = parse_sql_model(block).ok()?;
1954+
(model.name == "orders").then_some(model)
1955+
})
1956+
.unwrap();
1957+
let customers_model = blocks
1958+
.iter()
1959+
.find_map(|block| {
1960+
let model = parse_sql_model(block).ok()?;
1961+
(model.name == "customers").then_some(model)
1962+
})
1963+
.unwrap();
1964+
1965+
assert_eq!(orders_model.table.as_deref(), Some("orders_v2"));
1966+
assert_eq!(customers_model.metrics.len(), 1);
1967+
assert_eq!(customers_model.metrics[0].name, "customer_count");
1968+
1969+
remove_definitions_file(&db_path);
1970+
}
1971+
18411972
#[test]
18421973
fn test_clear_resets_active_model() {
18431974
let _guard = test_lock();

0 commit comments

Comments
 (0)