diff --git a/api/src/org/labkey/api/action/ConfirmAction.java b/api/src/org/labkey/api/action/ConfirmAction.java index 791218d6d35..cfea3870f6b 100644 --- a/api/src/org/labkey/api/action/ConfirmAction.java +++ b/api/src/org/labkey/api/action/ConfirmAction.java @@ -77,7 +77,10 @@ public final ModelAndView handleRequest() throws Exception ModelAndView mv = getSuccessView(form); if (null != mv) return mv; - throw new RedirectException(getSuccessURL(form)); + URLHelper redirect = getSuccessURL(form); + if (null != redirect) + throw new RedirectException(redirect); + return null; } } else @@ -137,7 +140,6 @@ public void validate(@NotNull Object form, @NotNull Errors errors) /* Generic version of validate */ public abstract void validateCommand(FORM form, Errors errors); - @NotNull public abstract URLHelper getSuccessURL(FORM form); // not usually used but some actions return views that close the current window etc... diff --git a/api/src/org/labkey/api/admin/AdminUrls.java b/api/src/org/labkey/api/admin/AdminUrls.java index e3a9dd0f92f..8101dc012bd 100644 --- a/api/src/org/labkey/api/admin/AdminUrls.java +++ b/api/src/org/labkey/api/admin/AdminUrls.java @@ -69,6 +69,7 @@ public interface AdminUrls extends UrlProvider ActionURL getAllowedExternalRedirectHostsURL(); ActionURL getDeleteEncryptedContentURL(); + ActionURL getOptionalFeaturesURL(); /** * Simply adds an "Admin Console" link to nav trail if invoked in the root container. Otherwise, root is unchanged. diff --git a/api/src/org/labkey/api/data/TableSelectorTestCase.java b/api/src/org/labkey/api/data/TableSelectorTestCase.java index 281681ebc2e..68d5c2ce623 100644 --- a/api/src/org/labkey/api/data/TableSelectorTestCase.java +++ b/api/src/org/labkey/api/data/TableSelectorTestCase.java @@ -17,6 +17,7 @@ import org.apache.commons.lang3.mutable.MutableInt; import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; import org.junit.Test; import org.labkey.api.collections.CsvSet; import org.labkey.api.data.Selector.ForEachBlock; @@ -30,6 +31,7 @@ import org.labkey.api.util.ExceptionUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.TestContext; +import org.labkey.api.util.logging.LogHelper; import org.springframework.jdbc.UncategorizedSQLException; import java.sql.ResultSet; @@ -50,6 +52,8 @@ public class TableSelectorTestCase extends AbstractSelectorTestCase { + private static final Logger LOG = LogHelper.getLogger(TableSelectorTestCase.class, "Test progress"); + @Test public void testTableSelector() throws SQLException { @@ -59,13 +63,25 @@ public void testTableSelector() throws SQLException // testTableSelector(DbSchema.get("oracle.granite", DbSchemaType.Bare).getTable("account"), Account.class); // Test MySQL or MariaDB database, if present - List mySqlScopes = Stream.of("mySql", "mariadb") - .map(DbScope::getDbScope).filter(Objects::nonNull).toList(); + List mySqlScopes = DbScope.getDbScopesToTest().stream() + .filter(scope -> Set.of("MySQL", "MariaDB").contains(scope.getSqlDialect().getProductName())) + .toList(); + for (DbScope mySqlScope: mySqlScopes) { DbSchema sakila = mySqlScope.getSchema("sakila", DbSchemaType.Bare); if (sakila.existsInDatabase()) - testTableSelector(sakila.getTable("country"), Country.class); + { + testTableSelector(sakila.getTable("Country"), Country.class); + } + else + { + DbSchema sys = mySqlScope.getSchema("sys", DbSchemaType.Bare); + if (sys.existsInDatabase()) + { + testTableSelector(sys.getTable("sys_config"), Config.class); + } + } } testTableSelector(CoreSchema.getInstance().getTableInfoActiveUsers(), User.class); testTableSelector(CoreSchema.getInstance().getTableInfoModules(), ModuleContext.class); @@ -124,6 +140,8 @@ public int hashCode() } } + record Config(String Variable, String Value, Date Set_Time, String Set_By){} + // public static class Account // { // private int _account_id; @@ -430,6 +448,9 @@ private void testColumnList(TableSelector selector, boolean stable) throws SQLEx private void testTableSelector(TableInfo table, Class clazz) throws SQLException { + DbSchema schema = table.getSchema(); + LOG.info("Testing {}.{}.{}", schema.getScope().getDisplayName(), schema.getName(), table.getName()); + TableSelector selector = new TableSelector(table); test(selector, clazz); diff --git a/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java b/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java index 986b0e0fa97..9362ddcde91 100644 --- a/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java +++ b/api/src/org/labkey/api/data/dialect/StandardJdbcHelper.java @@ -44,7 +44,7 @@ protected String parseDatabase(String url) throws ServletException if (-1 == dbEnd) dbEnd = url.length(); - // Last '/' is the database delimiter, except for "jdbc:postgresql:database" + // Last '/' is the database delimiter, except for "jdbc:postgresql:database" and old Oracle formats char dbDelimiter = url.contains("/") ? '/' : ':'; int dbDelimiterIndex = url.lastIndexOf(dbDelimiter, dbEnd); diff --git a/api/src/org/labkey/api/mcp/DocumentationService.java b/api/src/org/labkey/api/mcp/DocumentationService.java new file mode 100644 index 00000000000..af8b3e4e927 --- /dev/null +++ b/api/src/org/labkey/api/mcp/DocumentationService.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.mcp; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.services.ServiceRegistry; + +/** + * Local implementation of documentation search/retrieval, backing the {@code searchDocumentation}/ + * {@code retrieveDocument} MCP tools in {@code CoreMcp}. Only registered on servers that host the LabKey + * documentation content and its vector store (currently www.labkey.org); every other server forwards those + * tool calls to www.labkey.org instead of calling this service. See {@link #isEnabled()}. + */ +public interface DocumentationService +{ + static @Nullable DocumentationService get() + { + return ServiceRegistry.get().getService(DocumentationService.class); + } + + static void setInstance(DocumentationService impl) + { + ServiceRegistry.get().registerService(DocumentationService.class, impl); + } + + /** + * True if this server is enabled as the documentation source. The backing optional feature flag is owned by + * whichever module registers an implementation (currently serviceTools), not this interface, so the flag + * only exists at all on servers that have that module installed. + */ + boolean isEnabled(); + + /** Returns a JSON string; see CoreMcp's searchDocumentation tool description for the response shape. */ + String searchDocumentation(String query, @Nullable Integer topK); + + /** Returns a JSON string; see CoreMcp's retrieveDocument tool description for the response shape. */ + String retrieveDocument(String id); +} \ No newline at end of file diff --git a/api/src/org/labkey/api/mcp/McpToolProxy.java b/api/src/org/labkey/api/mcp/McpToolProxy.java new file mode 100644 index 00000000000..0a49ccfc9bb --- /dev/null +++ b/api/src/org/labkey/api/mcp/McpToolProxy.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.api.mcp; + +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +import io.modelcontextprotocol.spec.McpSchema; +import org.apache.logging.log4j.Logger; +import org.labkey.api.util.logging.LogHelper; + +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Proxy for calling an MCP tool on a remote server. + * + * A single {@link McpSyncClient} is created lazily and reused for the lifetime of this instance; the MCP + * initialize handshake only happens once, on the first forwarded call. + */ +public class McpToolProxy +{ + private static final Logger LOG = LogHelper.getLogger(McpToolProxy.class, "MCP tool forwarding"); + + private final String remoteBaseUrl; + private volatile McpSyncClient client; + + public McpToolProxy(String remoteBaseUrl) + { + this.remoteBaseUrl = remoteBaseUrl; + } + + private McpSyncClient getClient() + { + McpSyncClient c = client; + if (c == null) + { + synchronized (this) + { + c = client; + if (c == null) + { + var transport = HttpClientStreamableHttpTransport.builder(remoteBaseUrl).build(); + c = McpClient.sync(transport) + .clientInfo(McpSchema.Implementation.builder("labkey-server-forwarder", "1.0").build()) + .build(); + c.initialize(); + client = c; + } + } + } + return c; + } + + // Drop the cached client after a failed call, so the next attempt reconnects instead of reusing a dead session + private synchronized void resetClient() + { + if (client != null) + { + try + { + client.closeGracefully(); + } + catch (RuntimeException ignore) + { + // already broken; nothing to do + } + client = null; + } + } + + /** + * Calls {@code remoteToolName} on the remote MCP server with {@code arguments} and returns its text content + * (joined, if the tool returned more than one text content block). Throws if the remote server can't be + * reached or the remote tool itself reports an error. + */ + public String forward(String remoteToolName, Map arguments) + { + McpSchema.CallToolResult result; + try + { + result = getClient().callTool(McpSchema.CallToolRequest.builder(remoteToolName).arguments(arguments).build()); + } + catch (RuntimeException e) + { + LOG.error("Failed to forward MCP tool call '{}' to {}", remoteToolName, remoteBaseUrl, e); + resetClient(); + throw new McpException("Unable to reach " + remoteBaseUrl + " to forward '" + remoteToolName + "': " + e.getMessage()); + } + + String text = result.content().stream() + .filter(content -> content instanceof McpSchema.TextContent) + .map(content -> ((McpSchema.TextContent) content).text()) + .collect(Collectors.joining("\n")); + + if (Boolean.TRUE.equals(result.isError())) + throw new McpException("Remote tool '" + remoteToolName + "' at " + remoteBaseUrl + " reported an error: " + text); + + return text; + } +} \ No newline at end of file diff --git a/api/src/org/labkey/api/settings/AppProps.java b/api/src/org/labkey/api/settings/AppProps.java index a182527d9b5..a80b866e1b9 100644 --- a/api/src/org/labkey/api/settings/AppProps.java +++ b/api/src/org/labkey/api/settings/AppProps.java @@ -279,4 +279,9 @@ static WriteableAppProps getWriteableInstance() @NotNull List getAllowedExtensions(); @NotNull String getAllowedExternalResourceHosts(); + + default @Nullable String getDocumentationServer() + { + return "https://www.labkey.org"; + } } diff --git a/api/src/org/labkey/api/wiki/WikiService.java b/api/src/org/labkey/api/wiki/WikiService.java index 3dfb4efadc1..4459e3f2655 100644 --- a/api/src/org/labkey/api/wiki/WikiService.java +++ b/api/src/org/labkey/api/wiki/WikiService.java @@ -124,9 +124,8 @@ default HtmlString getHtml(Container c, String name) *

Each {@link org.labkey.api.mcp.McpService.VectorDocument} is assigned an ID of the form * {@code "/"}, where both components are GUIDs * (as returned by {@link Container#getId()} and the wiki's own entity ID). - * Tools that consume vector store results (e.g. {@code listDocuments}, - * {@code retrieveDocument}) must use this same format when constructing or - * interpreting document IDs.

+ * Tools that consume vector store results (e.g. {@code retrieveDocument}) must use this same + * format when constructing or interpreting document IDs.

* * @return the number of documents added */ diff --git a/core/src/org/labkey/core/CoreMcp.java b/core/src/org/labkey/core/CoreMcp.java index cfd39dc0292..84734b1c225 100644 --- a/core/src/org/labkey/core/CoreMcp.java +++ b/core/src/org/labkey/core/CoreMcp.java @@ -24,6 +24,9 @@ import org.labkey.api.collections.LabKeyCollectors; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; +import org.labkey.api.mcp.DocumentationService; +import org.labkey.api.mcp.McpException; +import org.labkey.api.mcp.McpToolProxy; import org.labkey.api.mcp.McpService; import org.labkey.api.module.ModuleLoader; import org.labkey.api.security.RequiresNoPermission; @@ -35,6 +38,7 @@ import org.labkey.api.study.Study; import org.labkey.api.study.StudyService; import org.labkey.api.util.HtmlString; +import org.labkey.api.util.URLHelper; import org.labkey.api.view.ActionURL; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.mcp.annotation.McpResource; @@ -42,14 +46,107 @@ import org.springframework.ai.tool.annotation.ToolParam; import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class CoreMcp implements McpService.McpImpl { + // Lazily created, then reused for the lifetime of this instance -- see McpToolProxy's class javadoc. + private volatile McpToolProxy documentationProxy; + + public CoreMcp() + { + } + + private static boolean isDocumentationSourceRemote() + { + if (DocumentationService.get() instanceof DocumentationService s && s.isEnabled()) + return false; + var documentationServer = AppProps.getInstance().getDocumentationServer(); + if (isBlank(documentationServer)) + return false; + + var baseServerUrl = AppProps.getInstance().getBaseServerUrl(); + try + { + return !(new URLHelper(documentationServer).getHost().equalsIgnoreCase(new URLHelper(baseServerUrl).getHost())); + } + catch (URISyntaxException x) + { + return false; + } + } + + private McpToolProxy getDocumentationProxy() + { + McpToolProxy proxy = documentationProxy; + if (proxy == null) + { + synchronized (this) + { + proxy = documentationProxy; + if (proxy == null) + { + proxy = new McpToolProxy(AppProps.getInstance().getDocumentationServer()); + documentationProxy = proxy; + } + } + } + return proxy; + } + + private String forward(String remoteToolName, Map arguments) + { + return getDocumentationProxy().forward(remoteToolName, arguments); + } + + + @Tool(description = "Search the LabKey documentation for chunks of text semantically similar to a natural language query. " + + "Each result is an excerpt from a larger document, not the full document -- multiple results may come from the same " + + "source document. Returns each chunk's content, metadata (title, source URL, content type), a similarity score, and " + + "an id identifying the source document; pass that id to retrieveDocument to fetch the entire document.") + @RequiresNoPermission + String searchDocumentation( + @ToolParam(description = "Natural language search query describing what you're looking for") String query, + @ToolParam(required = false, description = "Maximum number of results to return, defaults to 5") Integer topK) + { + if (isDocumentationSourceRemote()) + { + Map arguments = new HashMap<>(); + arguments.put("query", query); + if (topK != null) + arguments.put("topK", topK); + return forward("searchDocumentation", arguments); + } + + DocumentationService svc = DocumentationService.get(); + if (svc == null || !svc.isEnabled()) + throw new McpException("Documentation search is not available on this server."); + return svc.searchDocumentation(query, topK); + } + + @Tool(description = "Return the entire document from the LabKey documentation using the `id` as returned by `searchDocumentation`.") + @RequiresNoPermission + String retrieveDocument( + @ToolParam(description = "Id of the document to return") String id) + { + if (isDocumentationSourceRemote()) + { + return forward("retrieveDocument", Map.of("id", id)); + } + + DocumentationService svc = DocumentationService.get(); + if (svc == null || !svc.isEnabled()) + throw new McpException("Documentation retrieval is not available on this server."); + return svc.retrieveDocument(id); + } + @Tool(description = "This tool provides useful context information about the current user (name, userid), webserver " + "(name, url, description), and current container/folder (name, path, url, description) once the container is set via setContainer.") @RequiresPermission(ReadPermission.class) diff --git a/core/src/org/labkey/core/DataAnalysis_Python.md b/core/src/org/labkey/core/DataAnalysis_Python.md index 56d82d58230..311a4287e11 100644 --- a/core/src/org/labkey/core/DataAnalysis_Python.md +++ b/core/src/org/labkey/core/DataAnalysis_Python.md @@ -23,7 +23,7 @@ When writing Python scripts, read the server URL from `.mcp.json` to pre-populat Confirm all of these settings with the analyst before writing any script. ## Online Reference Material -https://www.labkey.org/Documentation/wiki-page.view?name=python +For questions about the `labkey` Python package, LabKey SQL, or server concepts not covered here, call `searchDocumentation` (e.g. `searchDocumentation("labkey python APIWrapper select_rows filters")`), then `retrieveDocument` for the full page. ## MCP Tools Available @@ -291,7 +291,7 @@ All modification APIs accept `timeout=300`, `container_path=None`, `transacted=T 10. **select_rows sends a GET request** to `query-getQuery.api`. `execute_sql` sends a POST to `query-executeSql.api`. -11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. Refer to LabKey documentation for dialect-specific features (e.g., lookup column traversal via `/` or `.` notation). +11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. For dialect-specific features (e.g., lookup column traversal via `/` or `.` notation), read the LabKey SQL resource (`resource://org/labkey/query/controllers/prompts/LabKeySql.md`) or call `searchDocumentation`. ## Typical Analysis Workflow diff --git a/core/src/org/labkey/core/DataAnalysis_R.md b/core/src/org/labkey/core/DataAnalysis_R.md index 61d72321f99..225b656c106 100644 --- a/core/src/org/labkey/core/DataAnalysis_R.md +++ b/core/src/org/labkey/core/DataAnalysis_R.md @@ -23,7 +23,7 @@ When writing R scripts, read the server URL from `.mcp.json` (via `jsonlite::fro Confirm all of these settings with the analyst before writing any script. ## Online Reference Material -https://www.labkey.org/Documentation/wiki-page.view?name=rAPI +For questions about the Rlabkey package, LabKey SQL, or server concepts not covered here, call `searchDocumentation` (e.g. `searchDocumentation("Rlabkey selectRows executeSql filters")`), then `retrieveDocument` for the full page. ## MCP Tools Available @@ -420,7 +420,7 @@ Data frames passed to modification functions must be created with `stringsAsFact 10. **baseUrl must include the context path and trailing slash**: e.g. `"http://localhost:8080/labkey/"` if the server uses a context path, or `"http://localhost:8080/"` if it does not. -11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. Refer to LabKey documentation for dialect-specific features (e.g., lookup column traversal via `/` or `.` notation). +11. **LabKey SQL is not standard SQL.** It is a SQL dialect specific to LabKey. Use `mcp__labkey__validateSQL` to check syntax before executing. For dialect-specific features (e.g., lookup column traversal via `/` or `.` notation), read the LabKey SQL resource (`resource://org/labkey/query/controllers/prompts/LabKeySql.md`) or call `searchDocumentation`. 12. **SSL configuration**: For HTTPS servers on Windows, you may need to set the `RLABKEY_CAINFO_FILE` environment variable pointing to a CA bundle file. Use `labkey.acceptSelfSignedCerts()` for development servers with self-signed certificates. diff --git a/core/src/org/labkey/core/FileBasedModules.md b/core/src/org/labkey/core/FileBasedModules.md index 20fc8811fb2..c97cb1eaf66 100644 --- a/core/src/org/labkey/core/FileBasedModules.md +++ b/core/src/org/labkey/core/FileBasedModules.md @@ -423,12 +423,8 @@ Simply refresh your browser to see changes. ## Documentation Resources -For more information, see: -- Simple Modules Overview: https://www.labkey.org/Documentation/wiki-page.view?name=simpleModules -- File-Based Module Tutorial: https://www.labkey.org/Documentation/wiki-page.view?name=moduleqvr - JavaScript API Documentation: https://labkey.github.io/labkey-api-js/ -- Module Directory Structures: https://www.labkey.org/Documentation/wiki-page.view?name=moduleDirectoryStructures -- Query Development: https://www.labkey.org/Documentation/wiki-page.view?name=addSQLQuery +- For everything else (simple modules overview, the file-based module tutorial, module directory structures, query development), call `searchDocumentation` — e.g. `searchDocumentation("file-based module directory structure")` — then `retrieveDocument` for the full page. ## Quick Start Checklist diff --git a/core/src/org/labkey/core/Reports.md b/core/src/org/labkey/core/Reports.md index 6f3f1b1d5c8..1f8bea765d5 100644 --- a/core/src/org/labkey/core/Reports.md +++ b/core/src/org/labkey/core/Reports.md @@ -8,6 +8,8 @@ Jump to the track that matches your language: Everything else on this page (data-bound vs. not, authorization, UI vs. file-based module) applies to both languages. +For report topics not covered here (e.g. additional export formats, less common report types), call `searchDocumentation`. + ## Report Type Landscape LabKey has several built-in report types: Query Report (renders a query view, no script), Attachment Report (uploaded static document), Link Report (URL pointer), JavaScript Report (runs in the *viewer's browser*, not server-side), R Report, Jupyter Report, and Query Snapshot (a persisted table, not really a "report"). **R Reports** and **Jupyter Reports** are the two paths this guide covers for turning an analyst-authored script into a server-side report. (A generic `ExternalScriptEngineReport`/`InternalScriptEngineReport` mechanism also exists for other JSR223-compatible engines an admin configures — historically used for Perl — but there's no conversion track for it here.) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index aada2d04fd8..c466468e266 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -926,6 +926,12 @@ public static ActionURL getDeprecatedFeaturesURL() { return new ActionURL(OptionalFeaturesAction.class, ContainerManager.getRoot()).addParameter("type", FeatureType.Deprecated.name()); } + + @Override + public ActionURL getOptionalFeaturesURL() + { + return new ActionURL(OptionalFeaturesAction.class, ContainerManager.getRoot()).addParameter("type", FeatureType.Optional.name()); + } } public static class MaintenanceBean diff --git a/core/src/org/labkey/core/webdav/DavController.java b/core/src/org/labkey/core/webdav/DavController.java index f171e828460..a62f976dcb0 100644 --- a/core/src/org/labkey/core/webdav/DavController.java +++ b/core/src/org/labkey/core/webdav/DavController.java @@ -3930,8 +3930,10 @@ WebdavStatus doMethod() throws DavException, IOException, RedirectException return getResponse().sendError(WebdavStatus.SC_LOCKED); } - Path path1 = getResourcePath(); -// _log.info("LOCK " + path1.toString()); // TODO: TEMP logging + if (getUser().isGuest()) + { + return getResponse().sendError(WebdavStatus.SC_FORBIDDEN); + } LockInfo lock = new LockInfo(); diff --git a/query/src/org/labkey/query/controllers/prompts/LabKeySql.md b/query/src/org/labkey/query/controllers/prompts/LabKeySql.md index 2346ea35687..ce98cb23e71 100644 --- a/query/src/org/labkey/query/controllers/prompts/LabKeySql.md +++ b/query/src/org/labkey/query/controllers/prompts/LabKeySql.md @@ -364,3 +364,5 @@ Many parse errors now include an inline suggestion (e.g. `Syntax error near 'OFF | `Parameter type is not supported: DATE` | Use TIMESTAMP (§11) | | `The underlying database does not support nested ORDER BY unless LIMIT...` (warning) | ORDER BY is being dropped — add `LIMIT` or sort via the API | | `ExecutingSelector; bad SQL grammar []` | Runtime (database-level) failure with no detail — usually a type mismatch (add CASTs) or a dialect-specific function limit (e.g. `timestampdiff` YEAR on PostgreSQL, `sum` over text) | + +For any LabKey SQL topic not covered above, call `searchDocumentation`.