Skip to content

Commit e196c07

Browse files
committed
XML Nav Tree Improvements
1 parent f9f2a9d commit e196c07

13 files changed

Lines changed: 736 additions & 44 deletions

File tree

build-frontend.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/bin/bash
2+
#
3+
# Licensed to the Apache Software Foundation (ASF) under one
4+
# or more contributor license agreements. See the NOTICE file
5+
# distributed with this work for additional information
6+
# regarding copyright ownership. The ASF licenses this file
7+
# to you under the Apache License, Version 2.0 (the
8+
# "License"); you may not use this file except in compliance
9+
# with the License. You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
#
19+
20+
# Build the SQL Lab frontend and update the distribution.
21+
# Usage: ./build-frontend.sh
22+
23+
set -e
24+
25+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26+
WEBAPP_DIR="$SCRIPT_DIR/exec/java-exec/src/main/resources/webapp"
27+
DIST_BASE="$SCRIPT_DIR/distribution/target"
28+
29+
echo "=== Building SQL Lab frontend ==="
30+
cd "$WEBAPP_DIR"
31+
npm run build
32+
33+
echo ""
34+
echo "=== Building java-exec module ==="
35+
cd "$SCRIPT_DIR"
36+
mvn package -pl exec/java-exec -DskipTests -Dcheckstyle.skip=true -q
37+
38+
# Find the distribution directory and copy the jar
39+
DIST_DIR=$(find "$DIST_BASE" -name "jars" -type d 2>/dev/null | head -1)
40+
if [ -n "$DIST_DIR" ]; then
41+
echo ""
42+
echo "=== Copying jar to distribution ==="
43+
cp "$SCRIPT_DIR/exec/java-exec/target/drill-java-exec-"*"-SNAPSHOT.jar" "$DIST_DIR/"
44+
echo "Updated: $DIST_DIR/"
45+
else
46+
echo ""
47+
echo "WARNING: Distribution directory not found under $DIST_BASE"
48+
echo "Run a full 'mvn package' first to create the distribution, or copy the jar manually:"
49+
echo " cp exec/java-exec/target/drill-java-exec-*-SNAPSHOT.jar <drill-distribution>/jars/"
50+
fi
51+
52+
echo ""
53+
echo "=== Done! Restart Drill and hard-refresh your browser (Cmd+Shift+R) ==="

contrib/format-xml/src/main/java/org/apache/drill/exec/store/xml/XMLReader.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ public void close() {
161161

162162
/**
163163
* This function processes the XML elements. This function stops reading when the
164-
* limit (if any) which came from the query has been reached or the Iterator runs out of
165-
* elements.
164+
* limit (if any) which came from the query has been reached, a complete row has been
165+
* read, or the Iterator runs out of elements.
166166
* @return True if there are more elements to parse, false if not
167167
*/
168168
private boolean processElements() {
@@ -197,6 +197,13 @@ private boolean processElements() {
197197

198198
// Process the event
199199
processEvent(currentEvent, lastEvent, reader.peek());
200+
201+
// After completing a row, return to let next() check batch capacity.
202+
// This prevents batch overflow errors that occur when rows accumulate
203+
// beyond what the batch can hold without the isFull() check running.
204+
if (currentState == xmlState.ROW_ENDED) {
205+
return true;
206+
}
200207
} catch (XMLStreamException e) {
201208
throw UserException
202209
.dataReadError(e)

exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/MetadataResources.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -550,8 +550,8 @@ public TablePreviewResponse previewTable(
550550
int safeLimit = Math.min(Math.max(1, limit), 1000);
551551

552552
String sql = String.format(
553-
"SELECT * FROM `%s`.`%s` LIMIT %d",
554-
escapeBackticks(schema), escapeBackticks(table), safeLimit);
553+
"SELECT * FROM %s.`%s` LIMIT %d",
554+
formatSchemaPath(schema), escapeBackticks(table), safeLimit);
555555

556556
try {
557557
QueryResult result = executeQuery(sql);
@@ -577,9 +577,9 @@ public ColumnsResponse getFileColumns(
577577
}
578578

579579
// Build the fully qualified path
580-
// Handle paths that may contain special characters
581-
String fullPath = String.format("`%s`.`%s`",
582-
escapeBackticks(schema), escapeBackticks(filePath));
580+
// Plugin name stays unquoted, workspace parts are individually backtick-quoted,
581+
// and the file path is backtick-quoted. e.g. dfs.`test`.`file.xml`
582+
String fullPath = formatSchemaPath(schema) + ".`" + escapeBackticks(filePath) + "`";
583583

584584
String sql = String.format("SELECT * FROM %s LIMIT 1", fullPath);
585585

@@ -588,11 +588,15 @@ public ColumnsResponse getFileColumns(
588588
try {
589589
QueryResult result = executeQuery(sql);
590590

591-
// Get column names and infer types from the first row of results
592-
for (String columnName : result.columns) {
593-
// Try to infer type from first row value
591+
// Use result.metadata for column types when available (preferred),
592+
// fall back to value-based inference.
593+
List<String> columnNames = new ArrayList<>(result.columns);
594+
for (int i = 0; i < columnNames.size(); i++) {
595+
String columnName = columnNames.get(i);
594596
String dataType = "ANY";
595-
if (!result.rows.isEmpty()) {
597+
if (result.metadata != null && i < result.metadata.size()) {
598+
dataType = result.metadata.get(i);
599+
} else if (!result.rows.isEmpty()) {
596600
String value = result.rows.get(0).get(columnName);
597601
dataType = inferDataType(value);
598602
}
@@ -717,4 +721,22 @@ private String escapeBackticks(String value) {
717721
}
718722
return value.replace("`", "``");
719723
}
724+
725+
/**
726+
* Format a compound schema name for SQL queries.
727+
* Plugin name stays unquoted; workspace parts are individually backtick-quoted.
728+
* e.g. "dfs.test" → "dfs.`test`", "dfs" → "dfs"
729+
*/
730+
private String formatSchemaPath(String schema) {
731+
if (schema == null || !schema.contains(".")) {
732+
return schema;
733+
}
734+
String[] parts = schema.split("\\.", 2);
735+
String[] workspaceParts = parts[1].split("\\.");
736+
StringBuilder sb = new StringBuilder(parts[0]);
737+
for (String wp : workspaceParts) {
738+
sb.append(".`").append(escapeBackticks(wp)).append("`");
739+
}
740+
return sb.toString();
741+
}
720742
}

exec/java-exec/src/main/resources/webapp/src/api/metadata.ts

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,24 @@
1616
* limitations under the License.
1717
*/
1818
import apiClient from './client';
19-
import type { SchemaInfo, TableInfo, ColumnInfo, PluginInfo, NestedFieldInfo } from '../types';
19+
import type { SchemaInfo, TableInfo, ColumnInfo, PluginInfo, NestedFieldInfo, SubTableInfo } from '../types';
2020
import { executeQuery } from './queries';
2121

2222
const METADATA_BASE = '/api/v1/metadata';
2323

24+
/**
25+
* Format a compound schema name for use in SQL queries.
26+
* Plugin name stays unquoted; workspace parts are backtick-quoted.
27+
* e.g. "dfs.test" → "dfs.`test`", "dfs" → "dfs"
28+
*/
29+
function formatSchema(schema: string): string {
30+
const parts = schema.split('.');
31+
if (parts.length <= 1) {
32+
return schema;
33+
}
34+
return parts[0] + '.' + parts.slice(1).map((p) => `\`${p}\``).join('.');
35+
}
36+
2437
export interface PluginsResponse {
2538
plugins: PluginInfo[];
2639
}
@@ -147,14 +160,24 @@ export async function getFiles(schema: string, subPath?: string): Promise<FileIn
147160
}
148161

149162
/**
150-
* Fetch columns from a file by executing SELECT * LIMIT 1
163+
* Fetch columns from a file by executing SELECT * LIMIT 1 via the query API.
164+
* Uses the same code path as the SQL editor so format plugins work consistently.
151165
*/
152166
export async function getFileColumns(schema: string, filePath: string): Promise<ColumnInfo[]> {
153-
const response = await apiClient.get<ColumnsResponse>(
154-
`${METADATA_BASE}/schemas/${encodeURIComponent(schema)}/files/columns`,
155-
{ params: { path: filePath } }
156-
);
157-
return response.data.columns;
167+
const query = `SELECT * FROM ${formatSchema(schema)}.\`${filePath}\` LIMIT 1`;
168+
const result = await executeQuery({ query, queryType: 'SQL', autoLimitRowCount: 1 });
169+
170+
if (!result.columns || result.columns.length === 0) {
171+
return [];
172+
}
173+
174+
return result.columns.map((colName, idx) => ({
175+
name: colName,
176+
type: result.metadata?.[idx] || 'ANY',
177+
nullable: true,
178+
schema,
179+
table: filePath,
180+
}));
158181
}
159182

160183
/**
@@ -192,7 +215,7 @@ export async function getNestedColumns(
192215

193216
const query =
194217
`SELECT getMapSchema(${columnExpr}) AS \`schema\`` +
195-
` FROM \`${schema}\`.\`${tableOrFile}\` LIMIT 1`;
218+
` FROM ${formatSchema(schema)}.\`${tableOrFile}\` LIMIT 1`;
196219

197220
const result = await executeQuery({
198221
query,
@@ -221,3 +244,105 @@ export async function getNestedColumns(
221244

222245
return [];
223246
}
247+
248+
/**
249+
* Fetch sub-tables (sheets, datasets, tables) within a multi-table file.
250+
*
251+
* @param schema the schema name (e.g. "dfs.tmp")
252+
* @param filePath the file path (e.g. "data.xlsx")
253+
* @param formatType "excel" | "hdf5" | "msaccess"
254+
*/
255+
export async function getSubTables(
256+
schema: string,
257+
filePath: string,
258+
formatType: string,
259+
): Promise<SubTableInfo[]> {
260+
let query: string;
261+
262+
switch (formatType) {
263+
case 'excel':
264+
query = `SELECT _sheets FROM ${formatSchema(schema)}.\`${filePath}\` LIMIT 1`;
265+
break;
266+
case 'hdf5':
267+
query = `SELECT path, data_type FROM ${formatSchema(schema)}.\`${filePath}\``;
268+
break;
269+
case 'msaccess':
270+
query = `SELECT \`table\` FROM ${formatSchema(schema)}.\`${filePath}\``;
271+
break;
272+
default:
273+
return [];
274+
}
275+
276+
const result = await executeQuery({ query, queryType: 'SQL', autoLimitRowCount: 1000 });
277+
278+
if (!result.rows || result.rows.length === 0) {
279+
return [];
280+
}
281+
282+
if (formatType === 'excel') {
283+
const sheetsVal = result.rows[0]['_sheets'];
284+
if (Array.isArray(sheetsVal)) {
285+
return sheetsVal.map((s) => ({ name: String(s) }));
286+
}
287+
if (typeof sheetsVal === 'string') {
288+
// Could be JSON array string or comma-separated
289+
try {
290+
const parsed = JSON.parse(sheetsVal);
291+
if (Array.isArray(parsed)) {
292+
return parsed.map((s: unknown) => ({ name: String(s) }));
293+
}
294+
} catch {
295+
// Treat as comma-separated
296+
return sheetsVal.split(',').map((s) => ({ name: s.trim() })).filter((s) => s.name.length > 0);
297+
}
298+
}
299+
return [];
300+
}
301+
302+
if (formatType === 'hdf5') {
303+
return result.rows
304+
.filter((row) => String(row['data_type']).toUpperCase() === 'DATASET')
305+
.map((row) => ({ name: String(row['path']), dataType: String(row['data_type']) }));
306+
}
307+
308+
if (formatType === 'msaccess') {
309+
return result.rows.map((row) => ({ name: String(row['table']) }));
310+
}
311+
312+
return [];
313+
}
314+
315+
/**
316+
* Fetch columns for a sub-table within a multi-table file using table function syntax.
317+
*
318+
* @param schema the schema name (e.g. "dfs.tmp")
319+
* @param filePath the file path (e.g. "data.xlsx")
320+
* @param formatType "excel" | "hdf5" | "msaccess"
321+
* @param paramName the table-function parameter name (e.g. "sheetName")
322+
* @param subTableName the specific sub-table name (e.g. "Sheet1")
323+
*/
324+
export async function getSubTableColumns(
325+
schema: string,
326+
filePath: string,
327+
formatType: string,
328+
paramName: string,
329+
subTableName: string,
330+
): Promise<ColumnInfo[]> {
331+
const query =
332+
`SELECT * FROM table( ${formatSchema(schema)}.\`${filePath}\`` +
333+
` (type => '${formatType}', ${paramName} => '${subTableName}')) LIMIT 1`;
334+
335+
const result = await executeQuery({ query, queryType: 'SQL', autoLimitRowCount: 1 });
336+
337+
if (!result.columns || result.columns.length === 0) {
338+
return [];
339+
}
340+
341+
return result.columns.map((colName, idx) => ({
342+
name: colName,
343+
type: result.metadata?.[idx] || 'ANY',
344+
nullable: true,
345+
schema,
346+
table: `${filePath}/${subTableName}`,
347+
}));
348+
}

0 commit comments

Comments
 (0)