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..64989a5ddae 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,33 @@ 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.isSqlServer()) + { + return new SQLFragment("ISNUMERIC(").append(arg).append(")"); + } + if (dialect.isPostgreSQL()) + { + return new SQLFragment("(CASE WHEN CAST((").append(arg) + .append(") AS TEXT) ~ '^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$' THEN 1 ELSE 0 END)"); + } + throw new IllegalStateException("isnumeric() is not supported for this database dialect: " + dialect.getProductName()); + } + } + static class VersionMethodInfo extends AbstractMethodInfo { VersionMethodInfo() @@ -1874,14 +1910,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));