Skip to content

Commit 82b00d0

Browse files
committed
fixing parsing AS after map projection
1 parent 6f44ba4 commit 82b00d0

File tree

3 files changed

+71
-9
lines changed

3 files changed

+71
-9
lines changed

pkg/cypher/executor.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,6 +1852,8 @@ func (e *StorageExecutor) parseRemoveProperties(removePart string) []string {
18521852
// smartSplitReturnItems splits a RETURN clause by commas, but respects:
18531853
// - CASE/END boundaries
18541854
// - Parentheses (function calls)
1855+
// - Curly braces (map projections like n { .*, key: value })
1856+
// - Square brackets (list literals)
18551857
// - String literals
18561858
// smartSplitReturnItems splits RETURN items by comma, respecting strings, parentheses, and CASE/END.
18571859
// Properly handles UTF-8 encoded strings with multi-byte characters.
@@ -1861,6 +1863,8 @@ func (e *StorageExecutor) smartSplitReturnItems(returnPart string) []string {
18611863
var inString bool
18621864
var stringChar rune
18631865
var parenDepth int
1866+
var braceDepth int
1867+
var bracketDepth int
18641868
var caseDepth int
18651869

18661870
runes := []rune(returnPart)
@@ -1910,6 +1914,30 @@ func (e *StorageExecutor) smartSplitReturnItems(returnPart string) []string {
19101914
continue
19111915
}
19121916

1917+
// Track curly braces (map projections)
1918+
if ch == '{' {
1919+
braceDepth++
1920+
current.WriteRune(ch)
1921+
continue
1922+
}
1923+
if ch == '}' {
1924+
braceDepth--
1925+
current.WriteRune(ch)
1926+
continue
1927+
}
1928+
1929+
// Track square brackets (list literals)
1930+
if ch == '[' {
1931+
bracketDepth++
1932+
current.WriteRune(ch)
1933+
continue
1934+
}
1935+
if ch == ']' {
1936+
bracketDepth--
1937+
current.WriteRune(ch)
1938+
continue
1939+
}
1940+
19131941
// Track CASE/END keywords (using byte positions for substring comparison)
19141942
if bytePos+4 <= len(returnPart) && upper[bytePos:bytePos+4] == "CASE" {
19151943
// Check if CASE is a word boundary
@@ -1937,8 +1965,8 @@ func (e *StorageExecutor) smartSplitReturnItems(returnPart string) []string {
19371965
}
19381966
}
19391967

1940-
// Split on comma only if we're not inside parens, CASE, or strings
1941-
if ch == ',' && parenDepth == 0 && caseDepth == 0 {
1968+
// Split on comma only if we're not inside parens, braces, brackets, CASE, or strings
1969+
if ch == ',' && parenDepth == 0 && braceDepth == 0 && bracketDepth == 0 && caseDepth == 0 {
19421970
result = append(result, current.String())
19431971
current.Reset()
19441972
continue

pkg/cypher/executor_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,6 +2288,32 @@ func TestParseReturnItemsWithAlias(t *testing.T) {
22882288
assert.Equal(t, "val", result.Columns[0])
22892289
}
22902290

2291+
func TestParseReturnItemsMapProjectionWithAlias(t *testing.T) {
2292+
store := storage.NewMemoryEngine()
2293+
exec := NewStorageExecutor(store)
2294+
ctx := context.Background()
2295+
2296+
// Test that map projection syntax n { .*, key: value } AS n is parsed correctly
2297+
// The comma inside {} should NOT split the return items
2298+
params := map[string]interface{}{
2299+
"props": map[string]interface{}{
2300+
"name": "TestNode",
2301+
"value": float64(42),
2302+
"embedding": []float64{0.1, 0.2, 0.3},
2303+
},
2304+
}
2305+
2306+
result, err := exec.Execute(ctx, "CREATE (n:Node $props) RETURN n { .*, embedding: null } AS n", params)
2307+
require.NoError(t, err)
2308+
2309+
// Should have exactly 1 column named "n", not 2 columns
2310+
assert.Len(t, result.Columns, 1, "Should have exactly 1 column, not split on comma inside {}")
2311+
assert.Equal(t, "n", result.Columns[0], "Column should be named 'n' from AS alias")
2312+
2313+
// Should have 1 row
2314+
assert.Len(t, result.Rows, 1)
2315+
}
2316+
22912317
func TestParseReturnItemsOrderByLimit(t *testing.T) {
22922318
store := storage.NewMemoryEngine()
22932319
exec := NewStorageExecutor(store)

ui/src/pages/Browser.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -712,17 +712,25 @@ function JsonPreview({ data, expanded = false }: { data: unknown; expanded?: boo
712712
if (typeof data === 'number') return <span className="json-number">{data}</span>;
713713
if (typeof data === 'boolean') return <span className="json-boolean">{String(data)}</span>;
714714
if (Array.isArray(data)) {
715-
if (!expanded && data.length > 3) {
716-
return <span className="text-norse-silver">[{data.length} items]</span>;
715+
if (expanded) {
716+
return (
717+
<pre className="text-xs text-norse-silver bg-norse-shadow/50 rounded p-2 overflow-x-auto max-h-48 overflow-y-auto">
718+
{JSON.stringify(data, null, 2)}
719+
</pre>
720+
);
717721
}
718-
return <span className="text-norse-silver">[...]</span>;
722+
return <span className="text-norse-silver">[{data.length} items]</span>;
719723
}
720724
if (typeof data === 'object') {
721-
const keys = Object.keys(data);
722-
if (!expanded && keys.length > 3) {
723-
return <span className="text-norse-silver">{'{'}...{keys.length} props{'}'}</span>;
725+
if (expanded) {
726+
return (
727+
<pre className="text-xs text-norse-silver bg-norse-shadow/50 rounded p-2 overflow-x-auto max-h-48 overflow-y-auto">
728+
{JSON.stringify(data, null, 2)}
729+
</pre>
730+
);
724731
}
725-
return <span className="text-norse-silver">{'{'}...{'}'}</span>;
732+
const keys = Object.keys(data);
733+
return <span className="text-norse-silver">{'{'}...{keys.length} props{'}'}</span>;
726734
}
727735
return <span>{String(data)}</span>;
728736
}

0 commit comments

Comments
 (0)