From 03b7256acd508df54d617f6f223297038ec4c216 Mon Sep 17 00:00:00 2001 From: Binal Patel Date: Tue, 7 Jul 2026 10:24:25 -0600 Subject: [PATCH 1/2] SQL Server Migration - update LabKey SQL helper for PostgreSQL compatibility (#7798) - Added portable LabKey SQL support for isnumeric() across SQL Server and PostgreSQL. - Moved right() into the shared LabKey SQL method registry so it can be used on PostgreSQL --- .../data/dialect/BasePostgreSqlDialect.java | 13 +++++++ .../labkey/api/data/dialect/SqlDialect.java | 28 ++++++++----- .../org/labkey/query/QueryServiceImpl.java | 39 +++++++++++++++++++ query/src/org/labkey/query/sql/Method.java | 33 +++++++++++++++- 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java index 0ecd40466c3..c2a43f9747a 100644 --- a/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java @@ -928,6 +928,19 @@ public boolean supportsNativeGreatestAndLeast() return true; } + @Override + public boolean supportsIsNumeric() + { + return true; + } + + @Override + public SQLFragment isNumericExpr(SQLFragment expression) + { + return new SQLFragment("(CASE WHEN CAST((").append(expression) + .append(") AS TEXT) ~ '^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$' THEN 1 ELSE 0 END)"); + } + private class PostgreSqlColumnMetaDataReader extends ColumnMetaDataReader { private final TableInfo _table; diff --git a/api/src/org/labkey/api/data/dialect/SqlDialect.java b/api/src/org/labkey/api/data/dialect/SqlDialect.java index 868702be8e2..a7dc67defef 100644 --- a/api/src/org/labkey/api/data/dialect/SqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/SqlDialect.java @@ -832,15 +832,25 @@ public SQLFragment getNumericCast(SQLFragment expression) * @param arguments Arguments passed from the LK SQL * @return the dialect equivalent SQLFragrment */ - public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments) - { - throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); - } - - public void handleCreateDatabaseException(SQLException e) throws ServletException - { - throw(new ServletException("Can't create database", e)); - } + public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments) + { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + public boolean supportsIsNumeric() + { + return false; + } + + public SQLFragment isNumericExpr(SQLFragment expression) + { + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + public void handleCreateDatabaseException(SQLException e) throws ServletException + { + throw(new ServletException("Can't create database", e)); + } /** * Wrap one or more INSERT statements to allow explicit specification diff --git a/query/src/org/labkey/query/QueryServiceImpl.java b/query/src/org/labkey/query/QueryServiceImpl.java index 749bbb656d8..c83c5b95972 100644 --- a/query/src/org/labkey/query/QueryServiceImpl.java +++ b/query/src/org/labkey/query/QueryServiceImpl.java @@ -3782,5 +3782,44 @@ public void testWhereClauseWithUnion() assertTrue(e.getMessage().contains("Syntax error near 'UNION'")); } } + + @Test + public void testRightAndIsnumeric() throws SQLException + { + // Portable LabKey-SQL functions: right() dispatches via the JDBC {fn right} escape; + // isnumeric() emits ISNUMERIC(x) on SQL Server and a regex-based CASE on PostgreSQL. + // This test exercises both against whichever dialect the test container is using. + String sql = + "SELECT " + + " right('hello', 2) AS r1, " + + " right('xy', 5) AS r2, " + + " isnumeric('5') AS n1, " + + " isnumeric('-3.14') AS n2, " + + " isnumeric('abc') AS n3, " + + " isnumeric(NULL) AS n4 " + + "FROM core.Containers"; + + QueryDef qd = new QueryDef(); + qd.setSchema("core"); + qd.setName("junit" + GUID.makeHash()); + qd.setContainer(JunitUtil.getTestContainer().getId()); + qd.setSql(sql); + QueryDefinition qdef = new CustomQueryDefinitionImpl(TestContext.get().getUser(), JunitUtil.getTestContainer(), qd); + List errors = new ArrayList<>(); + TableInfo t = qdef.getTable(errors, false); + String dialect = t == null ? "?" : t.getSqlDialect().getProductName(); + assertTrue("Query parse errors on " + dialect + ": " + errors, errors.isEmpty()); + + try (Results results = new TableSelector(t).getResults()) + { + assertTrue("Expected at least one row from core.Containers", results.next()); + assertEquals("right('hello', 2) on " + dialect, "lo", results.getString("r1")); + assertEquals("right('xy', 5) on " + dialect, "xy", results.getString("r2")); + assertEquals("isnumeric('5') on " + dialect, 1, results.getInt("n1")); + assertEquals("isnumeric('-3.14') on " + dialect, 1, results.getInt("n2")); + assertEquals("isnumeric('abc') on " + dialect, 0, results.getInt("n3")); + assertEquals("isnumeric(NULL) on " + dialect, 0, results.getInt("n4")); + } + } } } diff --git a/query/src/org/labkey/query/sql/Method.java b/query/src/org/labkey/query/sql/Method.java index 8789835eb94..f4829cc4a11 100644 --- a/query/src/org/labkey/query/sql/Method.java +++ b/query/src/org/labkey/query/sql/Method.java @@ -230,6 +230,14 @@ public MethodInfo getMethodInfo() return new IsMemberInfo(); } }); + labkeyMethod.put("isnumeric", new Method("isnumeric", JdbcType.BOOLEAN, 1, 1) + { + @Override + public MethodInfo getMethodInfo() + { + return new IsNumericInfo(); + } + }); labkeyMethod.put("javaconstant", new Method("javaconstant", JdbcType.VARBINARY, 1, 1) { @Override @@ -375,6 +383,7 @@ public MethodInfo getMethodInfo() labkeyMethod.put("radians", new JdbcMethod("radians", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("rand", new JdbcMethod("rand", JdbcType.DOUBLE, 0, 1)); labkeyMethod.put("repeat", new JdbcMethod("repeat", JdbcType.VARCHAR, 2, 2)); + labkeyMethod.put("right", new JdbcMethod("right", JdbcType.VARCHAR, 2, 2)); labkeyMethod.put("round", new Method("round", JdbcType.DOUBLE, 1, 2) { @Override @@ -1067,6 +1076,27 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) } } + // Portable isnumeric() emits ISNUMERIC(x) on SQL Server and a regex-based CASE on PostgreSQL. + // Returns 1 for digit strings with an optional sign/decimal point, 0 otherwise. + // This is stricter than SQL Server's ISNUMERIC(), which also accepts formats like scientific notation. + static class IsNumericInfo extends AbstractMethodInfo + { + IsNumericInfo() + { + super(JdbcType.BOOLEAN); + } + + @Override + public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) + { + SQLFragment arg = arguments[0]; + if (dialect.supportsIsNumeric()) + return dialect.isNumericExpr(arg); + + throw new IllegalStateException("isnumeric() is not supported for this database dialect: " + dialect.getProductName()); + } + } + static class VersionMethodInfo extends AbstractMethodInfo { VersionMethodInfo() @@ -1874,14 +1904,13 @@ private static void addJsonPassthroughMethod(String name, JdbcType type, int min mssqlMethods.put("charindex", new PassthroughMethod("charindex", JdbcType.INTEGER, 2, 3)); mssqlMethods.put("concat_ws", new PassthroughMethod("concat_ws", JdbcType.VARCHAR, 1, Integer.MAX_VALUE)); mssqlMethods.put("difference", new PassthroughMethod("difference", JdbcType.INTEGER, 2, 2)); - mssqlMethods.put("isnumeric", new PassthroughMethod("isnumeric", JdbcType.BOOLEAN, 1, 1)); + // isnumeric is registered in labkeyMethod (portable across PostgreSQL and SQL Server) mssqlMethods.put("len", new PassthroughMethod("len", JdbcType.INTEGER, 1, 1)); mssqlMethods.put("patindex", new PassthroughMethod("patindex", JdbcType.INTEGER, 2, 2)); mssqlMethods.put("quotename", new PassthroughMethod("quotename", JdbcType.VARCHAR, 1, 2)); mssqlMethods.put("replace", new PassthroughMethod("replace", JdbcType.VARCHAR, 3, 3)); mssqlMethods.put("replicate", new PassthroughMethod("replicate", JdbcType.VARCHAR, 2, 2)); mssqlMethods.put("reverse", new PassthroughMethod("reverse", JdbcType.VARCHAR, 1, 1)); - mssqlMethods.put("right", new PassthroughMethod("right", JdbcType.VARCHAR, 2, 2)); mssqlMethods.put("soundex", new PassthroughMethod("soundex", JdbcType.VARCHAR, 1, 1)); mssqlMethods.put("space", new PassthroughMethod("space", JdbcType.VARCHAR, 1, 1)); mssqlMethods.put("str", new PassthroughMethod("str", JdbcType.VARCHAR, 1, 3)); From 22d0e1aa71d728b0c506fb423fb3174d1820ab76 Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Tue, 7 Jul 2026 10:15:36 -0700 Subject: [PATCH 2/2] ModuleEditorService.getModuleRoot() (#7816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Rationale Make ModuleEditorService.getUpdatableResourcesRoot() return the path used to resolve module resources. getModuleRoot() returns the module source root (potentially parent of getUpdatableResourcesRoot()) ## Related Pull Requests - ## Changes - ## Related Pull Requests - ## Changes - --- .../labkey/api/moduleeditor/api/ModuleEditorService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java b/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java index 894e4b806be..40e2b438e78 100644 --- a/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java +++ b/api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java @@ -66,4 +66,11 @@ default File getFileForModuleResource(Module module, Path path) return null; return FileUtil.appendPath(resources, path); } + + // used by ModuleResourceProvider@AllModuleResourcesRoot() (_webdav/@modules) which wants to soo + // the whole module source directory (e.g. when using SourcePath) + default File getModuleRoot(Module module, @Nullable List messages) + { + return null; + } } \ No newline at end of file