Skip to content
Open
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
13 changes: 13 additions & 0 deletions api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 19 additions & 9 deletions api/src/org/labkey/api/data/dialect/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions query/src/org/labkey/query/QueryServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<QueryException> 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"));
}
}
}
}
33 changes: 31 additions & 2 deletions query/src/org/labkey/query/sql/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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));
Expand Down