@@ -1242,6 +1242,66 @@ <h3>Run in Browser with WebAssembly</h3>
12421242 return json.dumps({"errorMessage": f"{e.__class__.__name__}: {str(e)}"})
12431243`
12441244
1245+ // Topological sort using Kahn's algorithm
1246+ function topologicalSort ( dag ) {
1247+ if ( ! dag || ! dag . nodes . length ) return [ ]
1248+
1249+ // Build adjacency list and in-degree map
1250+ const adjacency = new Map ( ) // from -> [to]
1251+ const inDegree = new Map ( ) // node -> count of incoming edges
1252+ const nodeOrder = new Map ( ) // blockId -> order (for tie-breaking)
1253+
1254+ // Initialize all nodes
1255+ for ( const node of dag . nodes ) {
1256+ adjacency . set ( node . blockId , [ ] )
1257+ inDegree . set ( node . blockId , 0 )
1258+ nodeOrder . set ( node . blockId , node . order ?? 0 )
1259+ }
1260+
1261+ // Build edges (from defines to uses)
1262+ for ( const edge of dag . edges ) {
1263+ const fromId = edge . fromBlockId
1264+ const toId = edge . toBlockId
1265+ if ( adjacency . has ( fromId ) && inDegree . has ( toId ) ) {
1266+ adjacency . get ( fromId ) . push ( toId )
1267+ inDegree . set ( toId , inDegree . get ( toId ) + 1 )
1268+ }
1269+ }
1270+
1271+ // Find all nodes with no incoming edges (sorted by original order for consistency)
1272+ const queue = [ ...inDegree . entries ( ) ]
1273+ . filter ( ( [ _ , deg ] ) => deg === 0 )
1274+ . map ( ( [ id ] ) => id )
1275+ . sort ( ( a , b ) => ( nodeOrder . get ( a ) ?? 0 ) - ( nodeOrder . get ( b ) ?? 0 ) )
1276+
1277+ const result = [ ]
1278+
1279+ while ( queue . length > 0 ) {
1280+ // Take the node with lowest original order (for deterministic output)
1281+ queue . sort ( ( a , b ) => ( nodeOrder . get ( a ) ?? 0 ) - ( nodeOrder . get ( b ) ?? 0 ) )
1282+ const node = queue . shift ( )
1283+ result . push ( node )
1284+
1285+ // Remove edges from this node
1286+ for ( const neighbor of adjacency . get ( node ) || [ ] ) {
1287+ inDegree . set ( neighbor , inDegree . get ( neighbor ) - 1 )
1288+ if ( inDegree . get ( neighbor ) === 0 ) {
1289+ queue . push ( neighbor )
1290+ }
1291+ }
1292+ }
1293+
1294+ // If we didn't visit all nodes, there's a cycle - return original order
1295+ if ( result . length !== dag . nodes . length ) {
1296+ console . warn ( 'DAG has cycles, falling back to original order' )
1297+ return dag . nodes
1298+ . sort ( ( a , b ) => ( a . order ?? 0 ) - ( b . order ?? 0 ) )
1299+ . map ( n => n . blockId )
1300+ }
1301+
1302+ return result
1303+ }
1304+
12451305 // DAG Builder - builds dependency graph from AST analysis
12461306 function buildDAGFromBlocks ( blocks ) {
12471307 const edges = [ ]
@@ -1356,41 +1416,73 @@ <h3>Run in Browser with WebAssembly</h3>
13561416 } )
13571417 }
13581418
1419+ // Browser-mode auto-fix: Replace file reads with in-memory DataFrames
1420+ // This enables cross-notebook data flow without a file system
1421+ function applyBrowserModeTransforms ( code , logReplacements = false ) {
1422+ return code . replace (
1423+ / p d \. r e a d _ c s v \s * \( \s * [ " ' ] ( [ ^ " ' ] + ) \. c s v [ " ' ] \s * \) / g,
1424+ ( match , filename ) => {
1425+ // Convert filename to likely variable name (e.g., "houses" -> "df_csv")
1426+ const varName = `df_csv` // Common pattern in Deepnote notebooks
1427+ if ( logReplacements ) {
1428+ console . log ( `Browser mode: Replacing pd.read_csv("${ filename } .csv") with ${ varName } .copy()` )
1429+ }
1430+ return `${ varName } .copy() # Browser mode: using in-memory DataFrame instead of file`
1431+ }
1432+ )
1433+ }
1434+
13591435 // Analyze notebook blocks and build DAG
13601436 async function buildNotebookDAG ( ) {
13611437 if ( ! pyodideReady ) return null
13621438
1363- const notebook = notebookData . project . notebooks [ currentNotebookIndex ]
1364- const blocksForAnalyzer = notebook . blocks
1365- . filter ( b => b . type === 'code' || b . type === 'sql' || b . type ?. startsWith ( 'input-' ) )
1366- . map ( ( block , index ) => ( {
1367- type : block . type ,
1368- blockId : block . id ,
1369- code : editedCode . get ( block . id ) || block . content || '' ,
1370- metadata : block . metadata ,
1371- } ) )
1439+ // Analyze ALL notebooks together for cross-notebook dependencies
1440+ const allBlocksForAnalyzer = [ ]
1441+ let globalOrder = 0
1442+
1443+ notebookData . project . notebooks . forEach ( ( notebook , notebookIndex ) => {
1444+ notebook . blocks
1445+ . filter ( b => b . type === 'code' || b . type === 'sql' || b . type ?. startsWith ( 'input-' ) )
1446+ . forEach ( ( block ) => {
1447+ let code = editedCode . get ( block . id ) || block . content || ''
1448+ // Apply browser-mode auto-fix for DAG analysis too
1449+ code = applyBrowserModeTransforms ( code )
1450+ allBlocksForAnalyzer . push ( {
1451+ type : block . type ,
1452+ blockId : block . id ,
1453+ code : code ,
1454+ metadata : block . metadata ,
1455+ notebookIndex : notebookIndex ,
1456+ globalOrder : globalOrder ++ ,
1457+ } )
1458+ } )
1459+ } )
13721460
1373- if ( blocksForAnalyzer . length === 0 ) return null
1461+ if ( allBlocksForAnalyzer . length === 0 ) return null
13741462
13751463 try {
13761464 // Pass data via Pyodide globals to avoid escaping issues
1377- pyodide . globals . set ( '_deepnote_blocks_input' , JSON . stringify ( blocksForAnalyzer ) )
1465+ pyodide . globals . set ( '_deepnote_blocks_input' , JSON . stringify ( allBlocksForAnalyzer ) )
13781466 const resultJson = await pyodide . runPythonAsync ( `_deepnote_analyze_blocks(_deepnote_blocks_input)` )
13791467 const result = JSON . parse ( resultJson )
1380-
1468+
13811469 if ( result . errorMessage ) {
13821470 console . error ( 'AST analysis error:' , result . errorMessage )
13831471 return null
13841472 }
13851473
1386- // Add order based on original notebook block order
1387- const blocksWithOrder = result . map ( item => ( {
1388- ...item ,
1389- order : notebook . blocks . findIndex ( b => b . id === item . blockId )
1390- } ) )
1474+ // Add order based on global notebook order (preserving cross-notebook dependencies)
1475+ const blocksWithOrder = result . map ( item => {
1476+ const inputBlock = allBlocksForAnalyzer . find ( b => b . blockId === item . blockId )
1477+ return {
1478+ ...item ,
1479+ order : inputBlock ?. globalOrder ?? 0 ,
1480+ notebookIndex : inputBlock ?. notebookIndex ?? 0 ,
1481+ }
1482+ } )
13911483
13921484 currentDAG = buildDAGFromBlocks ( blocksWithOrder )
1393- console . log ( 'Built DAG:' , currentDAG )
1485+ console . log ( 'Built global DAG (all notebooks) :' , currentDAG )
13941486 return currentDAG
13951487 } catch ( error ) {
13961488 console . error ( 'Failed to build DAG:' , error )
@@ -1443,6 +1535,10 @@ <h3>Run in Browser with WebAssembly</h3>
14431535 await pyodide . loadPackage ( 'micropip' )
14441536 const micropip = pyodide . pyimport ( 'micropip' )
14451537 await micropip . install ( 'seaborn' )
1538+ progressFill . style . width = '90%'
1539+
1540+ messageEl . textContent = 'Installing DuckDB for SQL support...'
1541+ await micropip . install ( 'duckdb' )
14461542 progressFill . style . width = '95%'
14471543
14481544 // Setup matplotlib for inline display
@@ -1535,12 +1631,28 @@ <h3>Run in Browser with WebAssembly</h3>
15351631 try {
15361632 // Use edited code if available, otherwise original
15371633 let code = editedCode . get ( blockId ) || block . content || ''
1538-
1539- // Skip SQL blocks - they can't run in Pyodide without a database
1634+
1635+ // Apply browser-mode transformations
1636+ code = applyBrowserModeTransforms ( code , true ) // true = log replacements
1637+
1638+ // Run SQL blocks using DuckDB
15401639 if ( block . type === 'sql' ) {
1541- outputContainer . innerHTML = '<div style="color: var(--text-muted); font-size: 0.8125rem;">ℹ️ SQL blocks use pre-computed results (no database in browser)</div>'
1542- blockEl . classList . remove ( 'cell-running' )
1543- return
1640+ const sqlCode = code . trim ( )
1641+ // Get variable name from block metadata or use default
1642+ const resultVar = block . metadata ?. deepnote_variable_name || 'sql_result'
1643+
1644+ // Escape for triple-quoted string (only need to escape triple quotes)
1645+ const escapedSql = sqlCode . replace ( / \\ / g, '\\\\' ) . replace ( / " " " / g, '\\"\\"\\"' )
1646+
1647+ // Wrap SQL in DuckDB execution
1648+ const wrappedCode = `
1649+ import duckdb
1650+
1651+ # Execute SQL query using DuckDB (can query pandas DataFrames directly)
1652+ ${ resultVar } = duckdb.query("""${ escapedSql } """).df()
1653+ ${ resultVar }
1654+ `
1655+ code = wrappedCode
15441656 }
15451657
15461658 // Skip bash/shell commands
@@ -1676,9 +1788,11 @@ <h3>Run in Browser with WebAssembly</h3>
16761788
16771789 if ( currentDAG ) {
16781790 const edgeCount = currentDAG . edges . length
1791+ const notebookCount = new Set ( currentDAG . nodes . map ( n => n . notebookIndex ) ) . size
1792+ const notebookText = notebookCount > 1 ? ` across ${ notebookCount } notebooks` : ''
16791793 indicator . innerHTML = `
16801794 <span style="color: var(--success);">●</span>
1681- <span>${ edgeCount } dependencies tracked </span>
1795+ <span>${ edgeCount } dependencies${ notebookText } </span>
16821796 `
16831797 } else {
16841798 indicator . innerHTML = `
@@ -1730,30 +1844,48 @@ <h3>Run in Browser with WebAssembly</h3>
17301844 runAllBtn . disabled = true
17311845 runAllBtn . innerHTML = '<svg class="loading-spinner" style="width:16px;height:16px;" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="32" stroke-dashoffset="12"></circle></svg><span>Running...</span>'
17321846
1733- const notebook = notebookData . project . notebooks [ currentNotebookIndex ]
1847+ // Run ALL cells across ALL notebooks in topological order
1848+ let allCodeBlocks = [ ]
17341849
1735- // Use DAG order if available, otherwise fall back to sorting key
1736- let codeBlocks
17371850 if ( currentDAG && currentDAG . nodes . length > 0 ) {
1738- // Sort blocks by DAG order (topological order based on dependencies)
1851+ // Get all code blocks from all notebooks, sorted by DAG order
17391852 const dagOrder = new Map ( currentDAG . nodes . map ( n => [ n . blockId , n . order ] ) )
1740- codeBlocks = notebook . blocks
1741- . filter ( b => b . type === 'code' || b . type === 'sql' )
1742- . sort ( ( a , b ) => ( dagOrder . get ( a . id ) || 999 ) - ( dagOrder . get ( b . id ) || 999 ) )
1743- console . log ( 'Running cells in DAG order:' , codeBlocks . map ( b => b . id ) )
1853+
1854+ notebookData . project . notebooks . forEach ( ( notebook ) => {
1855+ notebook . blocks
1856+ . filter ( b => b . type === 'code' || b . type === 'sql' )
1857+ . forEach ( block => {
1858+ allCodeBlocks . push ( {
1859+ ...block ,
1860+ dagOrder : dagOrder . get ( block . id ) ?? 999
1861+ } )
1862+ } )
1863+ } )
1864+
1865+ // Perform topological sort based on DAG edges
1866+ const sortedBlockIds = topologicalSort ( currentDAG )
1867+ const idToOrder = new Map ( sortedBlockIds . map ( ( id , idx ) => [ id , idx ] ) )
1868+
1869+ // Sort by topological order
1870+ allCodeBlocks . sort ( ( a , b ) => ( idToOrder . get ( a . id ) ?? 999 ) - ( idToOrder . get ( b . id ) ?? 999 ) )
1871+ console . log ( 'Running ALL cells across all notebooks in topological order:' , allCodeBlocks . map ( b => b . id ) )
17441872 } else {
1745- // Fallback to notebook order
1746- codeBlocks = [ ...notebook . blocks ]
1747- . sort ( ( a , b ) => a . sortingKey . localeCompare ( b . sortingKey ) )
1748- . filter ( b => b . type === 'code' || b . type === 'sql' )
1873+ // Fallback: run all notebooks in order
1874+ notebookData . project . notebooks . forEach ( ( notebook ) => {
1875+ const blocks = [ ...notebook . blocks ]
1876+ . sort ( ( a , b ) => a . sortingKey . localeCompare ( b . sortingKey ) )
1877+ . filter ( b => b . type === 'code' || b . type === 'sql' )
1878+ allCodeBlocks . push ( ...blocks )
1879+ } )
1880+ console . log ( 'Running ALL cells across all notebooks in notebook order' )
17491881 }
17501882
1751- for ( const block of codeBlocks ) {
1883+ for ( const block of allCodeBlocks ) {
17521884 await runCell ( block . id )
17531885 }
17541886
17551887 notebookInitialized = true
1756- console . log ( 'Notebook initialized - all cells have been run' )
1888+ console . log ( 'All notebooks initialized - all cells have been run' )
17571889
17581890 runAllBtn . disabled = false
17591891 runAllBtn . innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg><span>Run All</span>'
0 commit comments