@@ -101,6 +101,112 @@ function keyPathSqlLiteral(pathSegments: string[]): string {
101101 return `ARRAY[${ pathSegments . map ( ( segment ) => quoteSqlJsonbLiteral ( segment ) ) . join ( ", " ) } ]::jsonb[]` ;
102102}
103103
104+ function splitSqlStatements ( sqlScript : string ) : string [ ] {
105+ const statements : string [ ] = [ ] ;
106+ let statementStart = 0 ;
107+ let index = 0 ;
108+ let inSingleQuote = false ;
109+ let inDoubleQuote = false ;
110+ let inLineComment = false ;
111+ let blockCommentDepth = 0 ;
112+ let dollarQuoteTag : null | string = null ;
113+ while ( index < sqlScript . length ) {
114+ const current = sqlScript [ index ] ;
115+ const next = sqlScript [ index + 1 ] ;
116+
117+ if ( inLineComment ) {
118+ if ( current === "\n" ) inLineComment = false ;
119+ index ++ ;
120+ continue ;
121+ }
122+ if ( blockCommentDepth > 0 ) {
123+ if ( current === "/" && next === "*" ) {
124+ blockCommentDepth ++ ;
125+ index += 2 ;
126+ continue ;
127+ }
128+ if ( current === "*" && next === "/" ) {
129+ blockCommentDepth -- ;
130+ index += 2 ;
131+ continue ;
132+ }
133+ index ++ ;
134+ continue ;
135+ }
136+ if ( dollarQuoteTag !== null ) {
137+ if ( sqlScript . startsWith ( dollarQuoteTag , index ) ) {
138+ index += dollarQuoteTag . length ;
139+ dollarQuoteTag = null ;
140+ } else {
141+ index ++ ;
142+ }
143+ continue ;
144+ }
145+ if ( inSingleQuote ) {
146+ if ( current === "'" ) {
147+ if ( next === "'" ) {
148+ index += 2 ;
149+ continue ;
150+ }
151+ inSingleQuote = false ;
152+ }
153+ index ++ ;
154+ continue ;
155+ }
156+ if ( inDoubleQuote ) {
157+ if ( current === "\"" ) inDoubleQuote = false ;
158+ index ++ ;
159+ continue ;
160+ }
161+
162+ if ( current === "-" && next === "-" ) {
163+ inLineComment = true ;
164+ index += 2 ;
165+ continue ;
166+ }
167+ if ( current === "/" && next === "*" ) {
168+ blockCommentDepth = 1 ;
169+ index += 2 ;
170+ continue ;
171+ }
172+ if ( current === "'" ) {
173+ inSingleQuote = true ;
174+ index ++ ;
175+ continue ;
176+ }
177+ if ( current === "\"" ) {
178+ inDoubleQuote = true ;
179+ index ++ ;
180+ continue ;
181+ }
182+ if ( current === "$" ) {
183+ let tagEnd = index + 1 ;
184+ while ( tagEnd < sqlScript . length && / [ a - z A - Z 0 - 9 _ ] / . test ( sqlScript [ tagEnd ] ?? "" ) ) {
185+ tagEnd ++ ;
186+ }
187+ if ( sqlScript [ tagEnd ] === "$" ) {
188+ dollarQuoteTag = sqlScript . slice ( index , tagEnd + 1 ) ;
189+ index = tagEnd + 1 ;
190+ continue ;
191+ }
192+ }
193+ if ( current === ";" ) {
194+ const statement = sqlScript . slice ( statementStart , index ) . trim ( ) ;
195+ if ( statement . length > 0 ) {
196+ statements . push ( statement ) ;
197+ }
198+ statementStart = index + 1 ;
199+ }
200+ index ++ ;
201+ }
202+
203+ const trailingStatement = sqlScript . slice ( statementStart ) . trim ( ) ;
204+ if ( trailingStatement . length > 0 ) {
205+ statements . push ( trailingStatement ) ;
206+ }
207+ return statements ;
208+ }
209+
104210function tableIdToString ( tableId : unknown ) : string {
105211 if ( typeof tableId === "string" ) return tableId ;
106212 return JSON . stringify ( tableId ) ;
@@ -134,10 +240,14 @@ const schemaObject: Record<string, unknown> = exampleFungibleLedgerSchema;
134240const registry = createTableRegistry ( schemaObject ) ;
135241
136242async function executeStatements ( statements : SqlStatement [ ] ) : Promise < void > {
243+ const sqlScript = toExecutableSqlStatements ( statements ) ;
244+ const executableStatements = splitSqlStatements ( sqlScript ) ;
137245 await retryTransaction ( globalPrismaClient , async ( tx ) => {
138246 await tx . $executeRawUnsafe ( `SET LOCAL jit = off` ) ;
139247 await tx . $executeRawUnsafe ( `SELECT pg_advisory_xact_lock(${ BULLDOZER_LOCK_ID } )` ) ;
140- await tx . $executeRawUnsafe ( toExecutableSqlStatements ( statements ) ) ;
248+ for ( const statement of executableStatements ) {
249+ await tx . $executeRawUnsafe ( statement ) ;
250+ }
141251 } ) ;
142252}
143253
@@ -229,17 +339,17 @@ async function computeStudioLayout(tables: Array<Awaited<ReturnType<typeof getTa
229339 } ) ,
230340 } ) ;
231341
232- const positions : Record < string , { x : number , y : number } > = { } ;
342+ const positions = new Map < string , { x : number , y : number } > ( ) ;
233343 for ( const child of layout . children ?? [ ] ) {
234344 if ( typeof child . id !== "string" ) continue ;
235- positions [ child . id ] = {
345+ positions . set ( child . id , {
236346 x : Number ( child . x ?? 0 ) ,
237347 y : Number ( child . y ?? 0 ) ,
238- } ;
348+ } ) ;
239349 }
240350
241351 return {
242- positions,
352+ positions : Object . fromEntries ( positions ) ,
243353 sceneWidth : Number ( Reflect . get ( layout , "width" ) ?? 600 ) ,
244354 sceneHeight : Number ( Reflect . get ( layout , "height" ) ?? 600 ) ,
245355 } ;
@@ -1532,13 +1642,24 @@ function getStudioPageHtml(): string {
15321642 info.className = "detail-section";
15331643 const kv = document.createElement("div");
15341644 kv.className = "kv";
1535- kv.innerHTML = ""
1536- + "<div class='kv-key'>name</div><div class='mono'>" + table.name + "</div>"
1537- + "<div class='kv-key'>tableId</div><div class='mono'>" + table.tableId + "</div>"
1538- + "<div class='kv-key'>operator</div><div class='mono'>" + table.operator + "</div>"
1539- + "<div class='kv-key'>initialized</div><div>" + String(table.initialized) + "</div>"
1540- + "<div class='kv-key'>dependencies</div><div class='mono'>" + (table.dependencies.length === 0 ? "(none)" : table.dependencies.join(", ")) + "</div>"
1541- + "<div class='kv-key'>rows(all groups)</div><div class='mono'>" + String(details.totalRows) + "</div>";
1645+ const appendInfoRow = (label: string, value: string, isMonospace = false) => {
1646+ const keyCell = document.createElement("div");
1647+ keyCell.className = "kv-key";
1648+ keyCell.textContent = label;
1649+ const valueCell = document.createElement("div");
1650+ if (isMonospace) {
1651+ valueCell.className = "mono";
1652+ }
1653+ valueCell.textContent = value;
1654+ kv.appendChild(keyCell);
1655+ kv.appendChild(valueCell);
1656+ };
1657+ appendInfoRow("name", table.name, true);
1658+ appendInfoRow("tableId", table.tableId, true);
1659+ appendInfoRow("operator", table.operator, true);
1660+ appendInfoRow("initialized", String(table.initialized));
1661+ appendInfoRow("dependencies", table.dependencies.length === 0 ? "(none)" : table.dependencies.join(", "), true);
1662+ appendInfoRow("rows(all groups)", String(details.totalRows), true);
15421663 info.appendChild(kv);
15431664 detailsPane.appendChild(info);
15441665
@@ -2038,6 +2159,12 @@ async function handleRequest(request: http.IncomingMessage, response: http.Serve
20382159 if ( method === "POST" && pathname === "/api/raw/delete" ) {
20392160 const body = requireRecord ( await readJsonBody ( request ) , "raw delete body must be an object." ) ;
20402161 const pathSegments = requireStringArray ( Reflect . get ( body , "pathSegments" ) , "pathSegments must be a string[]" ) ;
2162+ if (
2163+ pathSegments . length === 0
2164+ || ( pathSegments . length === 1 && pathSegments [ 0 ] === "table" )
2165+ ) {
2166+ throw new StackAssertionError ( "Deleting reserved root paths is not allowed." ) ;
2167+ }
20412168 await retryTransaction ( globalPrismaClient , async ( tx ) => {
20422169 await tx . $executeRawUnsafe ( `
20432170 DELETE FROM "BulldozerStorageEngine"
0 commit comments