@@ -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+
419468fn 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