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
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,12 @@ public boolean supportsNativeGreatestAndLeast()
return true;
}

@Override
public boolean supportsNativeIsDistinctFrom()
{
return true;
}

@Override
public boolean supportsIsNumeric()
{
Expand Down
11 changes: 11 additions & 0 deletions api/src/org/labkey/api/data/dialect/SqlDialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,17 @@ public SQLFragment isNumericExpr(SQLFragment expression)
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
}

/**
* Does the dialect natively support the standard "IS [NOT] DISTINCT FROM" predicate? PostgreSQL and Snowflake
* do; SQL Server, MySQL, and Oracle do not (SQL Server added GREATEST/LEAST in recent versions but has never
* added this predicate). Dialects that return false here get a portable CASE-based rewrite instead; see
* Method.IsDistinctFromMethodInfo.
*/
public boolean supportsNativeIsDistinctFrom()
{
return false;
}

public void handleCreateDatabaseException(SQLException e) throws ServletException
{
throw(new ServletException("Can't create database", e));
Expand Down
24 changes: 13 additions & 11 deletions query/src/org/labkey/query/QueryTestCase.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -914,18 +914,10 @@ d,seven,twelve,day,month,date,duration,guid
},

// test operators in THEN expression
new SqlTest("SELECT CASE WHEN 1=1 THEN 'a' || 'b' ELSE 'x' || 'y' END")
);

List<SqlTest> postgres = List.of(
// ORDER BY tests
new SqlTest("SELECT R.day, R.month, R.date FROM R ORDER BY R.date", 3, Rsize),
new SqlTest("SELECT R.day, R.month, R.date FROM R UNION SELECT R.day, R.month, R.date FROM R ORDER BY date"),
new SqlTest("SELECT R.guid FROM R WHERE overlaps(CAST('2001-01-01' AS DATE), CAST('2001-01-10' AS DATE), CAST('2001-01-05' AS DATE), CAST('2001-01-15' AS DATE))", 1, Rsize),

// regression test: field reference in sub-select (https://www.labkey.org/home/Developer/issues/issues-details.view?issueId=43580)
new SqlTest("SELECT (SELECT GROUP_CONCAT(b.displayname, ', ') FROM core.UsersAndGroups b WHERE b.email IN (SELECT UNNEST(STRING_TO_ARRAY(a.title, ',')))) AS procedurename, a.parent.rowid FROM core.containers a ", 2, 1),
new SqlTest("SELECT CASE WHEN 1=1 THEN 'a' || 'b' ELSE 'x' || 'y' END"),

// is_distinct_from()/is_not_distinct_from() are portable: native "IS [NOT] DISTINCT FROM" on PostgreSQL and
// Snowflake, a CASE-based rewrite elsewhere (e.g. SQL Server) -- see IsDistinctFromMethodInfo.getSQL()
new MethodSqlTest("SELECT is_distinct_from(NULL,NULL)", JdbcType.BOOLEAN, false),
new MethodSqlTest("SELECT is_not_distinct_from(NULL,NULL)", JdbcType.BOOLEAN, true),
new MethodSqlTest("SELECT is_distinct_from(1,NULL)", JdbcType.BOOLEAN, true),
Expand All @@ -936,6 +928,16 @@ d,seven,twelve,day,month,date,duration,guid
new MethodSqlTest("SELECT is_not_distinct_from(1,2)", JdbcType.BOOLEAN, false)
);

List<SqlTest> postgres = List.of(
// ORDER BY tests
new SqlTest("SELECT R.day, R.month, R.date FROM R ORDER BY R.date", 3, Rsize),
new SqlTest("SELECT R.day, R.month, R.date FROM R UNION SELECT R.day, R.month, R.date FROM R ORDER BY date"),
new SqlTest("SELECT R.guid FROM R WHERE overlaps(CAST('2001-01-01' AS DATE), CAST('2001-01-10' AS DATE), CAST('2001-01-05' AS DATE), CAST('2001-01-15' AS DATE))", 1, Rsize),

// regression test: field reference in sub-select (https://www.labkey.org/home/Developer/issues/issues-details.view?issueId=43580)
new SqlTest("SELECT (SELECT GROUP_CONCAT(b.displayname, ', ') FROM core.UsersAndGroups b WHERE b.email IN (SELECT UNNEST(STRING_TO_ARRAY(a.title, ',')))) AS procedurename, a.parent.rowid FROM core.containers a ", 2, 1)
);

List<SqlTest> postgresOnlyFunctions()
{
int majorVersion = ((BasePostgreSqlDialect) CoreSchema.getInstance().getSqlDialect()).getMajorVersion();
Expand Down
639 changes: 295 additions & 344 deletions query/src/org/labkey/query/controllers/prompts/LabKeySql.md

Large diffs are not rendered by default.

109 changes: 85 additions & 24 deletions query/src/org/labkey/query/sql/Method.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
import org.labkey.api.data.CompareType;
import org.labkey.api.data.Container;
import org.labkey.api.data.CoreSchema;
import org.labkey.api.data.DbScope;
import org.labkey.api.data.JdbcType;
import org.labkey.api.data.MethodInfo;
import org.labkey.api.data.MutableColumnInfo;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.SqlSelector;
import org.labkey.api.data.TableInfo;
import org.labkey.api.data.dialect.SqlDialect;
import org.labkey.api.module.Module;
Expand Down Expand Up @@ -167,7 +169,7 @@ public MethodInfo getMethodInfo()
labkeyMethod.put("cos", new JdbcMethod("cos", JdbcType.DOUBLE, 1, 1));
labkeyMethod.put("cot", new JdbcMethod("cot", JdbcType.DOUBLE, 1, 1));
labkeyMethod.put("curdate", new JdbcMethod("curdate", JdbcType.DATE, 0, 0));
labkeyMethod.put("curtime", new JdbcMethod("curtime", JdbcType.DATE, 0, 0));
labkeyMethod.put("curtime", new JdbcMethod("curtime", JdbcType.TIME, 0, 0));
labkeyMethod.put("dayofmonth", new JdbcMethod("dayofmonth", JdbcType.INTEGER, 1, 1));
labkeyMethod.put("dayofweek", new JdbcMethod("dayofweek", JdbcType.INTEGER, 1, 1));
labkeyMethod.put("dayofyear", new JdbcMethod("dayofyear", JdbcType.INTEGER, 1, 1));
Expand Down Expand Up @@ -1664,6 +1666,24 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
}
};
}

// "is distinct from" and "is not distinct from" operators in method form
labkeyMethod.put("is_distinct_from", new Method(JdbcType.BOOLEAN, 2, 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this applied from a patch? They should be added around line 546 instead.

{
@Override
public MethodInfo getMethodInfo()
{
return new IsDistinctFromMethodInfo(IS);
}
});
labkeyMethod.put("is_not_distinct_from", new Method(JdbcType.BOOLEAN, 2, 2)
{
@Override
public MethodInfo getMethodInfo()
{
return new IsDistinctFromMethodInfo(IS_NOT);
}
});
}

final static Map<String, Method> postgresMethods = Collections.synchronizedMap(new CaseInsensitiveHashMap<>());
Expand Down Expand Up @@ -1843,24 +1863,6 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
postgresMethods.put("jsonb_path_query_tz", new PassthroughMethod("jsonb_path_query_tz", JdbcType.VARCHAR, 2, 4));
postgresMethods.put("jsonb_path_query_array_tz", new PassthroughMethod("jsonb_path_query_array_tz", JdbcType.VARCHAR, 2, 4));
postgresMethods.put("jsonb_path_query_first_tz", new PassthroughMethod("jsonb_path_query_first_tz", JdbcType.VARCHAR, 2, 4));

// "is distinct from" and "is not distinct from" operators in method form
labkeyMethod.put("is_distinct_from", new Method(JdbcType.BOOLEAN, 2, 2)
{
@Override
public MethodInfo getMethodInfo()
{
return new IsDistinctFromMethodInfo(IS);
}
});
labkeyMethod.put("is_not_distinct_from", new Method(JdbcType.BOOLEAN, 2, 2)
{
@Override
public MethodInfo getMethodInfo()
{
return new IsDistinctFromMethodInfo(IS_NOT);
}
});
}

private static class IsDistinctFromMethodInfo extends AbstractMethodInfo
Expand All @@ -1876,13 +1878,35 @@ private static class IsDistinctFromMethodInfo extends AbstractMethodInfo
@Override
public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments)
{
SQLFragment a = arguments[0];
SQLFragment b = arguments[1];
SQLFragment ret = new SQLFragment();
ret.append(" ((").append(arguments[0]).append(")");
if (token == IS)
ret.append(" IS DISTINCT FROM ");

if (dialect.supportsNativeIsDistinctFrom())
{
ret.append(" ((").append(a).append(")");
if (token == IS)
ret.append(" IS DISTINCT FROM ");
else
ret.append(" IS NOT DISTINCT FROM ");
ret.append("(").append(b).append(")) ");
}
else
ret.append(" IS NOT DISTINCT FROM ");
ret.append("(").append(arguments[1]).append(")) ");
{
// "IS [NOT] DISTINCT FROM" isn't standard/portable SQL -- it's native only on PostgreSQL-family and
// Snowflake dialects. Elsewhere (e.g. SQL Server), rewrite as a CASE expression that always
// evaluates to a real TRUE/FALSE -- never NULL, even when exactly one side is null -- so it behaves
// the same as the native predicate would.
String notDistinctValue = token == IS ? dialect.getBooleanFALSE() : dialect.getBooleanTRUE();
String distinctValue = token == IS ? dialect.getBooleanTRUE() : dialect.getBooleanFALSE();

ret.append("(CASE WHEN (").append(a).append(") = (").append(b).append(")");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (CASE WHEN) expression works as SELECT, but would it work in WHERE?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I'll double check.

ret.append(" OR ((").append(a).append(") IS NULL AND (").append(b).append(") IS NULL)");
ret.append(" THEN ").append(notDistinctValue);
ret.append(" ELSE ").append(distinctValue);
ret.append(" END)");
}

return ret;
}
}
Expand Down Expand Up @@ -2013,5 +2037,42 @@ public void testSimpleString()
assertNotSimpleString(new SQLFragment("SELECT 'test'"));
assertNotSimpleString(new SQLFragment("'test''string'"));
}

// Exercises both the native and portable-fallback branches of IsDistinctFromMethodInfo.getSQL() against every
// dialect that's actually connected in this environment, not just whichever dialect the current CI leg happens
// to be running against. A FROM-less SELECT works everywhere except Oracle, which requires FROM DUAL.
@Test
public void testIsDistinctFrom()
{
record Case(String a, String b, boolean distinct) {}
List<Case> cases = List.of(
new Case("1", "2", true),
new Case("1", "1", false),
new Case("NULL", "NULL", false),
new Case("1", "NULL", true)
);

for (DbScope scope : DbScope.getDbScopesToTest())
{
SqlDialect d = scope.getSqlDialect();

for (Case c : cases)
{
assertIsDistinctFrom(scope, d, IS, c.a(), c.b(), c.distinct());
assertIsDistinctFrom(scope, d, IS_NOT, c.a(), c.b(), !c.distinct());
}
}
}

private void assertIsDistinctFrom(DbScope scope, SqlDialect d, int token, String a, String b, boolean expected)
{
SQLFragment expr = new IsDistinctFromMethodInfo(token).getSQL(d, new SQLFragment[]{new SQLFragment(a), new SQLFragment(b)});
SQLFragment sql = new SQLFragment("SELECT ").append(expr);
if (d.isOracle())
sql.append(" FROM DUAL");

Boolean result = new SqlSelector(scope, sql).getObject(Boolean.class);
assertEquals(d.getClass().getSimpleName() + ": " + sql.toDebugString(), expected, result);
}
}
}
4 changes: 3 additions & 1 deletion query/src/org/labkey/query/sql/QMethodCall.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ public QueryParseException fieldCheck(QNode parent, SqlDialect d)
{
if (getMethod(d) == null)
{
return new QueryParseException("Unknown method " + getField().getName(), null, getLine(), getColumn());
String name = getField().getName();
String hint = SqlParser.forUnknownMethod(name, d);
return new QueryParseException("Unknown method " + name + (null == hint ? "" : ". " + hint), null, getLine(), getColumn());
}
return null;
}
Expand Down
21 changes: 11 additions & 10 deletions query/src/org/labkey/query/sql/SqlBase.g
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ tokens
@lexer::header
{
package org.labkey.query.sql.antlr;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
}


Expand Down Expand Up @@ -134,17 +131,14 @@ tokens

@lexer::members
{
Logger _log = LogManager.getLogger(org.labkey.query.sql.SqlParser.class);

protected void setPossibleID(boolean possibleID)
{
}

@Override
public void emitErrorMessage(String msg)
{
_log.debug(msg);
}
// NOTE: lexer errors are reported via reportError(), which SqlParser._SqlLexer overrides to collect
// them as parse errors. Always use _SqlLexer rather than instantiating SqlBaseLexer directly -- the
// default ANTLR emitErrorMessage() prints unmatchable input to System.err and then drops it, letting
// the remaining characters re-lex into a different, valid-looking query.
}


Expand Down Expand Up @@ -180,6 +174,9 @@ REGR_SXY : 'regr_sxy';
REGR_SYY : 'regr_syy';
COUNT : 'count';
CROSS : 'cross';
CURRENT_DATE : 'current_date';
CURRENT_TIME : 'current_time';
CURRENT_TIMESTAMP : 'current_timestamp';
DELETE : 'delete';
DISTINCT : 'distinct';
DOT : '.';
Expand Down Expand Up @@ -761,6 +758,10 @@ starAtom
primaryExpression
: ARRAY exprList ']' -> ^(METHOD_CALL IDENT["ARRAY_CONSTRUCT"] exprList)
| TEXTARRAY exprList ']' -> ^(METHOD_CALL IDENT["TEXTARRAY_CONSTRUCT"] exprList)
// SQL-standard niladic datetime keywords -- no parens allowed; sugar for curdate()/curtime()/now()
| CURRENT_DATE -> ^(METHOD_CALL IDENT["CURDATE"] ^(EXPR_LIST))
| CURRENT_TIME -> ^(METHOD_CALL IDENT["CURTIME"] ^(EXPR_LIST))
| CURRENT_TIMESTAMP -> ^(METHOD_CALL IDENT["NOW"] ^(EXPR_LIST))
| id=identPrimary
| constant
| OPEN! ( expression | subQuery) CLOSE!
Expand Down
Loading