Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/src/org/labkey/api/data/DataColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ protected ColumnInfo getDisplayField(@NotNull ColumnInfo col, boolean withLookup
return null==display ? col : display;
}

@Override
public void setWithLookup(boolean withLookup)
{
_displayColumn = withLookup ? getDisplayField(_boundColumn, true) : _boundColumn;
}

@Override
public String toString()
{
Expand Down
5 changes: 5 additions & 0 deletions api/src/org/labkey/api/data/DisplayColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,11 @@ public void setRequiresHtmlFiltering(boolean requiresHtmlFiltering)
_requiresHtmlFiltering = requiresHtmlFiltering;
}

public void setWithLookup(boolean withLookup)
{
// subclasses override as needed
}

public void setLinkTarget(String linkTarget)
{
_linkTarget = linkTarget;
Expand Down
10 changes: 10 additions & 0 deletions api/src/org/labkey/api/util/PageFlowUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2697,6 +2697,16 @@ private static boolean shouldEscapeForExport(@NotNull String value)
return StringUtils.containsAny(value,",\"");
}

/// Generate one row of tab-delimited output using RFC 4180 quoting rules.
/// Fields containing tabs, newlines, or double quotes are enclosed in double quotes,
/// with embedded double quotes escaped by doubling.
public static String joinValuesWithTabs4180(@NotNull List<String> values)
{
return values.stream()
.map(value -> null == value ? "" : StringUtils.containsAny(value, "\t\n\r\"") ? "\"" + Strings.CS.replace(value, "\"", "\"\"") + "\"" : value)
.collect(Collectors.joining("\t"));
}


static final String FIELD_ENCODED_PREFIX = "%_";

Expand Down
3 changes: 2 additions & 1 deletion query/src/org/labkey/query/QueryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,8 @@ public Set<String> getSchemaNames()
RolapReader.RolapTest.class,
RolapTestCase.class,
SelectRowsStreamHack.TestCase.class,
ServerManager.TestCase.class
ServerManager.TestCase.class,
SqlController.TestCase.class
);
}

Expand Down
52 changes: 51 additions & 1 deletion query/src/org/labkey/query/controllers/QueryMcp.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,24 @@
import org.labkey.api.query.UserSchema;
import org.labkey.api.security.RequiresPermission;
import org.labkey.api.security.permissions.ReadPermission;
import org.labkey.query.QueryServiceImpl;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.writer.ContainerUser;
import org.labkey.query.QueryServiceImpl;
import org.labkey.query.sql.SqlParser;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.labkey.api.util.StringUtilsLabKey.pluralize;

public class QueryMcp implements McpService.McpImpl
{
Expand Down Expand Up @@ -210,6 +212,54 @@ String validateCalculatedColumnExpression(
}
}


@Tool(description =
"Execute a LabKey SQL query and return results as tab-separated values (RFC 4180 TSV). " +
"Use this to inspect actual query results while writing or debugging SQL. " +
"Prefer validateSQL when you only need to check syntax without running the query. " +
"Returns at most 100 rows; use offset and limit to page through larger result sets. " +
"Response format: a header row of column names, then one data row per newline, fields tab-separated. " +
"Fields containing tabs, newlines, or double-quotes are RFC 4180 quoted. " +
"On SQL error, the error message is returned as plain text rather than throwing. " +
"For data analysis or bulk retrieval, use the LabKey Python or R client APIs instead of this tool. " +
"**Important** This tool does not yet support queries with named parameters.")
@RequiresPermission(ReadPermission.class)
String executeSQL(
ToolContext toolContext,
@ToolParam(description = "Fully qualified schema name as it would appear in SQL e.g. Study or \"Study\".\"Datasets\"") String schemaName,
@ToolParam(description = "LabKey SQL to execute") String sql,
@ToolParam(description = "Rows to skip before returning results.", required=false) Integer offset,
@ToolParam(description = "Number of rows to return (limit <= 100)", required=false) Integer limit
)
{
var cu = getContext(toolContext);
var schema = DefaultSchema.get(cu.getUser(), cu.getContainer(), getSchemaKey(schemaName));
if (!(schema instanceof UserSchema userSchema))
return "Could not find schema " + schemaName;

offset = null==offset ? 0 : offset < 0 ? 0 : offset;
limit = (limit == null || limit < 0) ? 100 : Math.min(100, limit);
var execute = new SqlController.SqlExecute(cu, userSchema, sql)
.page(offset, limit)
.truncation(500, "…[truncated]");

try
{
StringWriter sw = new StringWriter(2000);
SqlController.SqlExecute.ExecuteResult result = execute.execute(sw);
String message = "\n-- " + pluralize(result.rows(), "row", "rows") + " returned";
if (result.complete())
message += ".";
else
message += ", more may be available (use offset and limit to page).";
return sw + message;
}
catch (Exception x)
{
return x.getMessage() != null ? x.getMessage() : x.getClass().getSimpleName();
}
}

/* For now, list all schemas. CONSIDER support incremental querying. */
public static Map<SchemaKey, UserSchema> _listAllSchemas(DefaultSchema root)
{
Expand Down
Loading