From 6b26a9b1bb6e94c7e01da470a3bf52acd83d315f Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Mon, 1 Dec 2025 16:42:34 -0800 Subject: [PATCH 1/7] array methods checkpoint --- .../labkey/api/data/dialect/SqlDialect.java | 74 +++++++++++++++ .../core/dialect/PostgreSql92Dialect.java | 92 +++++++++++++++++++ query/src/org/labkey/query/QueryTestCase.jsp | 73 +++++++++++++++ 3 files changed, 239 insertions(+) diff --git a/api/src/org/labkey/api/data/dialect/SqlDialect.java b/api/src/org/labkey/api/data/dialect/SqlDialect.java index b11a782b34a..3833bdc61a4 100644 --- a/api/src/org/labkey/api/data/dialect/SqlDialect.java +++ b/api/src/org/labkey/api/data/dialect/SqlDialect.java @@ -2115,6 +2115,80 @@ public record Sequence(String schemaName, String tableName, String columnName, L return rs.getString("FILTER_CONDITION"); } + + + // + // ARRAY SUPPORT + // + + public boolean supportsArrays() + { + return false; + } + + // construct a sql array from SQLFragment elements + public SQLFragment array_construct(SQLFragment[] elements) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // element a is in array b + public SQLFragment element_in_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // element a is not in array b + public SQLFragment element_not_in_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // SET OPERATORS FOR ARRAY TYPE + + // all elements of array a are contained in array b + public SQLFragment array_all_in_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // some elements of array a are contained in array b + public SQLFragment array_some_in_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // no elements of array a are contained in array b + public SQLFragment array_none_in_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // arrays a and b contain the same elements equivalent to (A all in B) AND (B all in A) + public SQLFragment array_same_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + // array a and array b do not contain the same elements + public SQLFragment array_not_same_array(SQLFragment a, SQLFragment b) + { + assert !supportsArrays(); + throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement"); + } + + + // + // TESTS + // + public static class DialectTestCase { DbScope s; diff --git a/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java b/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java index 8e171ab8689..bdd82d20744 100644 --- a/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java +++ b/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java @@ -252,4 +252,96 @@ AND d.deptype IN ('a', 'i') -- Automatic dependency for DEFAULT or index-related ); return new SqlSelector(table.getSchema(), sql).getCollection(Sequence.class); } + + + // + // ARRAY and SET syntax + // + + + @Override + public boolean supportsArrays() + { + return true; + } + + @Override + public SQLFragment array_construct(SQLFragment[] elements) + { + SQLFragment ret = new SQLFragment(); + ret.append("ARRAY["); + String separator = ""; + for (SQLFragment element : elements) + { + ret.append(separator); + ret.append(element); + separator = ", "; + } + ret.append("]"); + return ret; + } + + @Override + public SQLFragment array_all_in_array(SQLFragment a, SQLFragment b) + { + SQLFragment ret = new SQLFragment(); + ret.append("(").append(a).append(") <@ (").append(b).append(")"); + return ret; + } + + @Override + public SQLFragment array_some_in_array(SQLFragment a, SQLFragment b) + { + SQLFragment ret = new SQLFragment(); + ret.append("(").append(a).append(") && (").append(b).append(")"); + return ret; + } + + @Override + public SQLFragment array_none_in_array(SQLFragment a, SQLFragment b) + { + return new SQLFragment(" NOT (").append(array_some_in_array(a, b)).append(")"); + } + + @Override + public SQLFragment array_same_array(SQLFragment a, SQLFragment b) + { + SQLFragment ret = new SQLFragment(); + ret.append(array_all_in_array(a, b)).append(" AND ").append(array_all_in_array(b, a)); + return ret; + } + + @Override + public SQLFragment array_not_same_array(SQLFragment a, SQLFragment b) + { + SQLFragment ret = new SQLFragment(); + ret.append("NOT (").append(array_all_in_array(a, b)).append(") OR NOT (").append(array_all_in_array(b, a)).append(")"); + return ret; + } + + @Override + public SQLFragment element_in_array(SQLFragment a, SQLFragment b) + { + SQLFragment ret = new SQLFragment(); + ret.append("(").append(a).append(")"); + // DOCs imply that IS NOT DISTINCT FROM ANY should work, but it doesn't??? + // ret.append(" IS NOT DISTINCT FROM ANY("); + ret.append(" = ANY("); + ret.append(b); + ret.append(")"); + return ret; + } + + @Override + public SQLFragment element_not_in_array(SQLFragment a, SQLFragment b) + { + SQLFragment ret = new SQLFragment(); + ret.append("(").append(a).append(")"); + // DOCs imply that IS NOT DISTINCT FROM ANY should work, but it doesn't??? + // ret.append(" IS DISTINCT FROM ALL("); + ret.append(" <> ALL("); + ret.append(b); + ret.append(")"); + return ret; + } } diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index 92224efd61e..df35936f006 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -79,6 +79,8 @@ <%@ page import="static java.util.Objects.requireNonNull" %> <%@ page import="static java.util.Objects.requireNonNull" %> <%@ page import="org.labkey.api.query.FieldKey" %> +<%@ page import="org.labkey.api.data.SqlSelector" %> +<%@ page import="org.labkey.api.data.DbScope" %> <%@ page extends="org.labkey.api.jsp.JspTest.DRT" %> <%! @@ -1134,6 +1136,7 @@ d,seven,twelve,day,month,date,duration,guid private final String hash = GUID.makeHash(); private QuerySchema lists; + private SqlDialect dialect; @Before public void setUp() @@ -1142,6 +1145,7 @@ d,seven,twelve,day,month,date,duration,guid assertNotNull(QueryService.get().getEnvironment(QueryService.Environment.USER)); assertNotNull(QueryService.get().getEnvironment(QueryService.Environment.CONTAINER)); Assume.assumeTrue(getClass().getSimpleName() + " requires list module", ListService.get() != null); + dialect = CoreSchema.getInstance().getSqlDialect(); } @@ -1860,4 +1864,73 @@ d,seven,twelve,day,month,date,duration,guid Assert.fail("Involved column '" + expectedColumn + "' not found for sql:\n" + sql); } } + + Boolean booleanSelect(SQLFragment s) + { + SQLFragment select = new SQLFragment("SELECT ").append(s); + return new SqlSelector(DbScope.getLabKeyScope(), select).getObject(Boolean.class); + } + + SQLFragment array(String... strings) + { + var args = Arrays.stream(strings).map(SQLFragment::unsafe).toList().toArray(new SQLFragment[0]); + return dialect.array_construct(args); + } + + @Test + public void testDialectArrayMethods() + { + if (!dialect.supportsArrays()) + return; + + var elNull = new SQLFragment("NULL"); + var elA = new SQLFragment("'A'"); + var elB = new SQLFragment("'B'"); + + assertTrue( booleanSelect(dialect.element_in_array( elA, array("'A'", "'B'") ))); + assertTrue( booleanSelect(dialect.element_in_array( elA, array("'A'", "NULL") ))); + assertFalse( booleanSelect(dialect.element_in_array( elA, array("'B'") ))); + // this returns NULL, unfortunately using "=" semantics rather than "IS NOT DISTINCT FROM" + assertNull( booleanSelect(dialect.element_in_array( elA, array("'B'", "NULL") ))); + assertNull( booleanSelect(dialect.element_in_array( elNull, array("'A'", "'B'") ))); + assertNull( booleanSelect(dialect.element_in_array( elNull, array("'A'", "NULL") ))); + assertNull( booleanSelect(dialect.element_in_array( elA, elNull))); + + assertFalse( booleanSelect(dialect.element_not_in_array( elA, array("'A'", "'B'") ))); + assertFalse( booleanSelect(dialect.element_not_in_array( elA, array("'A'", "NULL") ))); + assertTrue( booleanSelect(dialect.element_not_in_array( elA, array("'B'") ))); + assertNull( booleanSelect(dialect.element_not_in_array( elA, array("'B'", "NULL") ))); + assertNull( booleanSelect(dialect.element_not_in_array( elNull, array("'A'", "'B'") ))); + assertNull( booleanSelect(dialect.element_not_in_array( elNull, array("'A'", "NULL") ))); + assertNull( booleanSelect(dialect.element_not_in_array( elA, elNull))); + + assertTrue( booleanSelect(dialect.array_all_in_array( array("'A'"), array("'A'") ))); + assertTrue( booleanSelect(dialect.array_all_in_array( array("'A'","'A'"), array("'A'") ))); + assertTrue( booleanSelect(dialect.array_all_in_array( array("'A'", "'B'", "'B'"), array("'A'","'B'","'C'") ))); + assertFalse( booleanSelect(dialect.array_all_in_array( array("'A'"), array("NULL") ))); + assertFalse( booleanSelect(dialect.array_all_in_array( array("NULL"), array("'A'") ))); + assertFalse( booleanSelect(dialect.array_all_in_array( array("NULL"), array("NULL") ))); + assertNull( booleanSelect(dialect.array_all_in_array( elNull, array("NULL") ))); + assertNull( booleanSelect(dialect.array_all_in_array( array("NULL"), elNull ))); + + assertTrue( booleanSelect(dialect.array_some_in_array( array("'A'", "'B'", "'C'"), array("'A'") ))); + assertTrue( booleanSelect(dialect.array_some_in_array( array("'A'"), array("'A'", "'B'", "'C'") ))); + assertTrue( booleanSelect(dialect.array_some_in_array( array("'A'","'X'"), array("'A'", "'Y'") ))); + assertFalse( booleanSelect(dialect.array_some_in_array( array("'A'","'B'"), array("'X'", "'Y'") ))); + assertFalse( booleanSelect(dialect.array_some_in_array( array("'A'"), array("NULL") ))); + assertFalse( booleanSelect(dialect.array_some_in_array( array("NULL"), array("'A'") ))); + assertFalse( booleanSelect(dialect.array_some_in_array( array("NULL"), array("NULL") ))); + assertNull( booleanSelect(dialect.array_some_in_array( elNull, array("NULL") ))); + assertNull( booleanSelect(dialect.array_some_in_array( array("NULL"), elNull ))); + + assertFalse( booleanSelect(dialect.array_none_in_array( array("'A'", "'B'", "'C'"), array("'A'") ))); + assertFalse( booleanSelect(dialect.array_none_in_array( array("'A'"), array("'A'", "'B'", "'C'") ))); + assertFalse( booleanSelect(dialect.array_none_in_array( array("'A'","'X'"), array("'A'", "'Y'") ))); + assertTrue( booleanSelect(dialect.array_none_in_array( array("'A'","'B'"), array("'X'", "'Y'") ))); + assertTrue( booleanSelect(dialect.array_none_in_array( array("'A'"), array("NULL") ))); + assertTrue( booleanSelect(dialect.array_none_in_array( array("NULL"), array("'A'") ))); + assertTrue( booleanSelect(dialect.array_none_in_array( array("NULL"), array("NULL") ))); + assertNull( booleanSelect(dialect.array_none_in_array( elNull, array("NULL") ))); + assertNull( booleanSelect(dialect.array_none_in_array( array("NULL"), elNull ))); + } %> From 6775328546c551b5cc1787caccacd10bc088bb4c Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Tue, 2 Dec 2025 12:56:03 -0800 Subject: [PATCH 2/7] SQL methods SELECT 'a' as test, array_contains_all( ARRAY['A','X'], ARRAY['A','B'] ) as result UNION ALL SELECT 'b' as test, array_contains_any( ARRAY['A','X'], ARRAY['A','B'] ) as result UNION ALL SELECT 'c' as test, array_contains_none( ARRAY['A','X'], ARRAY['A','B'] ) as result UNION ALL SELECT 'd' as test, array_is_same( ARRAY['A','X'], ARRAY['A','B'] ) as result UNION ALL SELECT 'd' as test, array_is_same( ARRAY['A','B'], ARRAY['A','B'] ) as result --- query/src/org/labkey/query/sql/Method.java | 544 +++++++++++++-------- query/src/org/labkey/query/sql/SqlBase.g | 4 +- 2 files changed, 331 insertions(+), 217 deletions(-) diff --git a/query/src/org/labkey/query/sql/Method.java b/query/src/org/labkey/query/sql/Method.java index 0dee1f2c2b5..dbdb46ec86c 100644 --- a/query/src/org/labkey/query/sql/Method.java +++ b/query/src/org/labkey/query/sql/Method.java @@ -48,6 +48,7 @@ import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; import org.labkey.api.util.GUID; +import org.labkey.api.util.Pair; import org.labkey.query.QueryServiceImpl; import org.labkey.query.sql.antlr.SqlBaseLexer; @@ -60,6 +61,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import java.util.function.BinaryOperator; import java.util.stream.Collectors; import static org.labkey.query.sql.Method.TimestampDiffInterval.SQL_TSI_FRAC_SECOND; @@ -75,71 +77,72 @@ public abstract class Method labkeyMethod.put("abs", new JdbcMethod("abs", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("acos", new JdbcMethod("acos", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("age", new Method(JdbcType.INTEGER, 2, 3) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new AgeMethodInfo(); - } + return new AgeMethodInfo(); + } - @Override - public void validate(CommonTree fn, List args, List parseErrors, List parseWarnings) + @Override + public void validate(CommonTree fn, List args, List parseErrors, List parseWarnings) + { + super.validate(fn, args, parseErrors, parseWarnings); + // only YEAR, MONTH supported + if (args.size() == 3) { - super.validate(fn, args, parseErrors, parseWarnings); - // only YEAR, MONTH supported - if (args.size() == 3) + QNode nodeInterval = args.get(2); + String text = nodeInterval.getTokenText(); + if (text.length() >= 2 && text.startsWith("'") && text.endsWith("'")) + text = text.substring(1, text.length() - 1); + TimestampDiffInterval i = TimestampDiffInterval.parse(text); + if (!(i == TimestampDiffInterval.SQL_TSI_MONTH || i == TimestampDiffInterval.SQL_TSI_YEAR)) { - QNode nodeInterval = args.get(2); - String text = nodeInterval.getTokenText(); - if (text.length() >= 2 && text.startsWith("'") && text.endsWith("'")) - text = text.substring(1,text.length()-1); - TimestampDiffInterval i = TimestampDiffInterval.parse(text); - if (!(i == TimestampDiffInterval.SQL_TSI_MONTH || i == TimestampDiffInterval.SQL_TSI_YEAR)) - { - parseErrors.add(new QueryParseException("AGE function supports SQL_TSI_YEAR or SQL_TSI_MONTH", null, - nodeInterval.getLine(), nodeInterval.getColumn())); - } + parseErrors.add(new QueryParseException("AGE function supports SQL_TSI_YEAR or SQL_TSI_MONTH", null, + nodeInterval.getLine(), nodeInterval.getColumn())); } } - }); + } + }); labkeyMethod.put("age_in_months", new Method(JdbcType.INTEGER, 2, 2) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new AgeInMonthsMethodInfo(); - } - }); + return new AgeInMonthsMethodInfo(); + } + }); labkeyMethod.put("age_in_years", new Method(JdbcType.INTEGER, 2, 2) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new AgeInYearsMethodInfo(); - } - }); + return new AgeInYearsMethodInfo(); + } + }); labkeyMethod.put("asin", new JdbcMethod("asin", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("atan", new JdbcMethod("atan", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("atan2", new JdbcMethod("atan2", JdbcType.DOUBLE, 2, 2)); labkeyMethod.put("cast", new Method("convert", JdbcType.OTHER, 2, 3) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new ConvertInfo(); - } - }); + return new ConvertInfo(); + } + }); labkeyMethod.put("ceiling", new JdbcMethod("ceiling", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("coalesce", new Method("coalesce", JdbcType.OTHER, 0, Integer.MAX_VALUE) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new PassthroughInfo("coalesce", null, JdbcType.OTHER); - } - }); + return new PassthroughInfo("coalesce", null, JdbcType.OTHER); + } + }); labkeyMethod.put("concat", new JdbcMethod("concat", JdbcType.VARCHAR, 2, 2)); - labkeyMethod.put("contextpath", new Method("contextPath", JdbcType.VARCHAR, 0, 0) { + labkeyMethod.put("contextpath", new Method("contextPath", JdbcType.VARCHAR, 0, 0) + { @Override public MethodInfo getMethodInfo() { @@ -147,13 +150,13 @@ public MethodInfo getMethodInfo() } }); labkeyMethod.put("convert", new Method("convert", JdbcType.OTHER, 2, 2) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new ConvertInfo(); - } - }); + return new ConvertInfo(); + } + }); 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)); @@ -164,14 +167,16 @@ public MethodInfo getMethodInfo() labkeyMethod.put("degrees", new JdbcMethod("degrees", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("exp", new JdbcMethod("exp", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("floor", new JdbcMethod("floor", JdbcType.DOUBLE, 1, 1)); - labkeyMethod.put("foldername", new Method("folderName", JdbcType.VARCHAR, 0, 0) { + labkeyMethod.put("foldername", new Method("folderName", JdbcType.VARCHAR, 0, 0) + { @Override public MethodInfo getMethodInfo() { return new FolderInfo(false); } }); - labkeyMethod.put("folderpath", new Method("folderPath", JdbcType.VARCHAR, 0, 0) { + labkeyMethod.put("folderpath", new Method("folderPath", JdbcType.VARCHAR, 0, 0) + { @Override public MethodInfo getMethodInfo() { @@ -187,34 +192,39 @@ public MethodInfo getMethodInfo() } }); labkeyMethod.put("hour", new JdbcMethod("hour", JdbcType.INTEGER, 1, 1)); - labkeyMethod.put("ifnull", new JdbcMethod("ifnull", JdbcType.OTHER, 2, 2){ + labkeyMethod.put("ifnull", new JdbcMethod("ifnull", JdbcType.OTHER, 2, 2) + { @Override public MethodInfo getMethodInfo() { - return new JdbcMethodInfoImpl(_name, _jdbcType){ + return new JdbcMethodInfoImpl(_name, _jdbcType) + { @Override public JdbcType getJdbcType(JdbcType[] args) { - return JdbcType.promote(args[0],args[1]); + return JdbcType.promote(args[0], args[1]); } }; } - }) ; - labkeyMethod.put("isequal", new Method("isequal", JdbcType.BOOLEAN, 2, 2){ + }); + labkeyMethod.put("isequal", new Method("isequal", JdbcType.BOOLEAN, 2, 2) + { @Override public MethodInfo getMethodInfo() { return new IsEqualInfo(); } }); - labkeyMethod.put("ismemberof", new Method("ismemberof", JdbcType.BOOLEAN, 1, 2) { + labkeyMethod.put("ismemberof", new Method("ismemberof", JdbcType.BOOLEAN, 1, 2) + { @Override public MethodInfo getMethodInfo() { return new IsMemberInfo(); } }); - labkeyMethod.put("javaconstant", new Method("javaconstant", JdbcType.VARBINARY, 1, 1){ + labkeyMethod.put("javaconstant", new Method("javaconstant", JdbcType.VARBINARY, 1, 1) + { @Override public void validate(CommonTree fn, List args, List parseErrors, List parseWarnings) { @@ -240,7 +250,7 @@ public void validate(CommonTree fn, List args, List parseError parseErrors.add(new QueryParseException(_name.toUpperCase() + "() parameter should be valid class name '.' field: " + param, null, line, column)); return; } - className = param.substring(0,dot); + className = param.substring(0, dot); propertyName = param.substring(dot + 1); Class cls = Class.forName(className); Field f = cls.getField(propertyName); @@ -292,7 +302,7 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) { assert arguments.length == 2 || arguments.length == 3; if (arguments.length == 2) - return dialect.sqlLocate(arguments[0], arguments[1]); + return dialect.sqlLocate(arguments[0], arguments[1]); else return dialect.sqlLocate(arguments[0], arguments[1], arguments[2]); } @@ -303,11 +313,12 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) labkeyMethod.put("ltrim", new JdbcMethod("ltrim", JdbcType.VARCHAR, 1, 1)); labkeyMethod.put("minute", new JdbcMethod("minute", JdbcType.INTEGER, 1, 1)); labkeyMethod.put("mod", new JdbcMethod("mod", JdbcType.DOUBLE, 2, 2)); - labkeyMethod.put("moduleproperty", new Method("moduleproperty", JdbcType.VARCHAR, 2, 2){ + labkeyMethod.put("moduleproperty", new Method("moduleproperty", JdbcType.VARCHAR, 2, 2) + { @Override public void validate(CommonTree fn, List args, List parseErrors, List parseWarnings) { - super.validate(fn,args,parseErrors,parseWarnings); + super.validate(fn, args, parseErrors, parseWarnings); if (args.size() != 2) return; if (args.get(0).getTokenType() != SqlBaseLexer.QUOTED_STRING) @@ -358,27 +369,28 @@ public MethodInfo getMethodInfo() labkeyMethod.put("rand", new JdbcMethod("rand", JdbcType.DOUBLE, 0, 1)); labkeyMethod.put("repeat", new JdbcMethod("repeat", JdbcType.VARCHAR, 2, 2)); labkeyMethod.put("round", new Method("round", JdbcType.DOUBLE, 1, 2) - { - @Override - public MethodInfo getMethodInfo() - { - return new RoundInfo(); - } - }); + { + @Override + public MethodInfo getMethodInfo() + { + return new RoundInfo(); + } + }); labkeyMethod.put("rtrim", new JdbcMethod("rtrim", JdbcType.VARCHAR, 1, 1)); labkeyMethod.put("second", new JdbcMethod("second", JdbcType.INTEGER, 1, 1)); labkeyMethod.put("sign", new JdbcMethod("sign", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("sin", new JdbcMethod("sin", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("sqrt", new JdbcMethod("sqrt", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("startswith", new Method("startswith", JdbcType.BOOLEAN, 2, 2) - { + { @Override public MethodInfo getMethodInfo() { return new StartsWithInfo(); } }); - labkeyMethod.put("substring", new JdbcMethod("substring", JdbcType.VARCHAR, 2, 3){ + labkeyMethod.put("substring", new JdbcMethod("substring", JdbcType.VARCHAR, 2, 3) + { @Override public MethodInfo getMethodInfo() { @@ -393,7 +405,7 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) argumentsThree[0] = arguments[0]; argumentsThree[1] = arguments[1]; // 19187: Query error when using substring without 3rd parameter in LabKey SQL - argumentsThree[2] = new SQLFragment(String.valueOf(Integer.MAX_VALUE/2)); + argumentsThree[2] = new SQLFragment(String.valueOf(Integer.MAX_VALUE / 2)); arguments = argumentsThree; } return super.getSQL(dialect, arguments); @@ -403,21 +415,21 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) }); labkeyMethod.put("tan", new JdbcMethod("tan", JdbcType.DOUBLE, 1, 1)); labkeyMethod.put("timestampadd", new Method("timestampadd", JdbcType.TIMESTAMP, 3, 3) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new TimestampInfo(this); - } - }); + return new TimestampInfo(this); + } + }); labkeyMethod.put("timestampdiff", new Method("timestampdiff", JdbcType.INTEGER, 3, 3) + { + @Override + public MethodInfo getMethodInfo() { - @Override - public MethodInfo getMethodInfo() - { - return new TimestampInfo(this); - } - }); + return new TimestampInfo(this); + } + }); labkeyMethod.put("truncate", new JdbcMethod("truncate", JdbcType.DOUBLE, 2, 2)); labkeyMethod.put("ucase", new JdbcMethod("ucase", JdbcType.VARCHAR, 1, 1)); labkeyMethod.put("upper", new JdbcMethod("ucase", JdbcType.VARCHAR, 1, 1)); @@ -429,18 +441,22 @@ public MethodInfo getMethodInfo() return new UserIdInfo(); } }); - labkeyMethod.put("username", new Method("username", JdbcType.VARCHAR, 0, 0) { + labkeyMethod.put("username", new Method("username", JdbcType.VARCHAR, 0, 0) + { @Override public MethodInfo getMethodInfo() { return new UserNameInfo(); } }); - labkeyMethod.put("version", new Method("version", JdbcType.DECIMAL, 0, 0){ + labkeyMethod.put("version", new Method("version", JdbcType.DECIMAL, 0, 0) + { @Override public MethodInfo getMethodInfo() { - return new VersionMethodInfo(){}; + return new VersionMethodInfo() + { + }; } }); labkeyMethod.put("week", new JdbcMethod("week", JdbcType.INTEGER, 1, 1)); @@ -452,7 +468,8 @@ public MethodInfo getMethodInfo() // ========== Don't document these ========== - labkeyMethod.put("__cte_two__", new Method(JdbcType.INTEGER, 0, 0) { + labkeyMethod.put("__cte_two__", new Method(JdbcType.INTEGER, 0, 0) + { @Override public MethodInfo getMethodInfo() { @@ -470,7 +487,8 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) }; } }); - labkeyMethod.put("__cte_three__", new Method(JdbcType.INTEGER, 0, 0) { + labkeyMethod.put("__cte_three__", new Method(JdbcType.INTEGER, 0, 0) + { @Override public MethodInfo getMethodInfo() { @@ -488,7 +506,8 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) }; } }); - labkeyMethod.put("__cte_times__", new Method(JdbcType.INTEGER, 2, 2) { + labkeyMethod.put("__cte_times__", new Method(JdbcType.INTEGER, 2, 2) + { @Override public MethodInfo getMethodInfo() { @@ -537,7 +556,7 @@ public void validate(CommonTree fn, List args, List parseError if (count < _minArgs || count > _maxArgs) { if (_minArgs == _maxArgs) - parseErrors.add(new QueryParseException(_name.toUpperCase() + " function expects " + _minArgs + " argument" + (_minArgs==1?"":"s"), null, fn.getLine(), fn.getCharPositionInLine())); + parseErrors.add(new QueryParseException(_name.toUpperCase() + " function expects " + _minArgs + " argument" + (_minArgs == 1 ? "" : "s"), null, fn.getLine(), fn.getCharPositionInLine())); else parseErrors.add(new QueryParseException(_name.toUpperCase() + " function expects " + _minArgs + " to " + _maxArgs + " arguments", null, fn.getLine(), fn.getCharPositionInLine())); } @@ -545,7 +564,8 @@ public void validate(CommonTree fn, List args, List parseError public static void addMethod(String name, MethodInfo info, JdbcType returnType, int minArgs, int maxArgs) { - Method m = new Method(name, returnType, minArgs, maxArgs) { + Method m = new Method(name, returnType, minArgs, maxArgs) + { @Override public MethodInfo getMethodInfo() { @@ -679,16 +699,16 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] fragments) try { String dialectTypeName = getTypeArgument(fragments[1]); - JdbcType jdbcType = dialect.getJdbcType(dialect.sqlTypeIntFromSqlTypeName(dialectTypeName),dialectTypeName); + JdbcType jdbcType = dialect.getJdbcType(dialect.sqlTypeIntFromSqlTypeName(dialectTypeName), dialectTypeName); if ((jdbcType == JdbcType.DOUBLE || jdbcType == JdbcType.REAL) && isSimpleString(fragments[0])) { String s = toSimpleString(fragments[0]).toLowerCase(); if ("infinity".equals(s) || "+infinity".equals(s)) - fragments[0] = new SQLFragment("?", jdbcType==JdbcType.DOUBLE ? Double.POSITIVE_INFINITY : Float.POSITIVE_INFINITY); + fragments[0] = new SQLFragment("?", jdbcType == JdbcType.DOUBLE ? Double.POSITIVE_INFINITY : Float.POSITIVE_INFINITY); else if ("-infinity".equals(s)) - fragments[0] = new SQLFragment("?", jdbcType==JdbcType.DOUBLE ? Double.NEGATIVE_INFINITY : Float.NEGATIVE_INFINITY); + fragments[0] = new SQLFragment("?", jdbcType == JdbcType.DOUBLE ? Double.NEGATIVE_INFINITY : Float.NEGATIVE_INFINITY); else if ("nan".equals(s)) - fragments[0] = new SQLFragment("?", jdbcType==JdbcType.DOUBLE ? Double.NaN : Float.NaN); + fragments[0] = new SQLFragment("?", jdbcType == JdbcType.DOUBLE ? Double.NaN : Float.NaN); } } catch (IllegalArgumentException x) @@ -710,7 +730,9 @@ else if ("nan".equals(s)) return ret; } - /** This code could be avoided by making our parser a little smarter to handle the valid convert constants */ + /** + * This code could be avoided by making our parser a little smarter to handle the valid convert constants + */ String getTypeArgument(SQLFragment typeSqlFragment) throws IllegalArgumentException { String typeName = typeSqlFragment.getRawSQL(); @@ -733,7 +755,7 @@ public JdbcType getTypeFromArgs(QNode args) if (children.size() < 2) return JdbcType.VARCHAR; - QType type = (QType)children.get(1); + QType type = (QType) children.get(1); return type.getJdbcType(); } } @@ -804,13 +826,13 @@ static class RoundInfo extends JdbcMethodInfoImpl public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) { boolean supportsRoundDouble = dialect.supportsRoundDouble(); - boolean unitRound = arguments.length == 1 || (arguments.length==2 && arguments[1].getSQL().equals("0")); + boolean unitRound = arguments.length == 1 || (arguments.length == 2 && arguments[1].getSQL().equals("0")); if (unitRound) { if (supportsRoundDouble) - return super.getSQL(dialect, new SQLFragment[] {arguments[0], new SQLFragment("0")}); + return super.getSQL(dialect, new SQLFragment[]{arguments[0], new SQLFragment("0")}); else - return super.getSQL(dialect, new SQLFragment[] {arguments[0]}); + return super.getSQL(dialect, new SQLFragment[]{arguments[0]}); } if (supportsRoundDouble) @@ -824,7 +846,7 @@ else if (dialect.isPostgreSQL()) // This is not SQL standard behavior SQLFragment numeric = new SQLFragment(); numeric.append("CAST((").append(arguments[0]).append(") AS NUMERIC)"); - return super.getSQL(dialect, new SQLFragment[] {numeric, arguments[1]}); + return super.getSQL(dialect, new SQLFragment[]{numeric, arguments[1]}); } else { @@ -842,14 +864,14 @@ else if (dialect.isPostgreSQL()) // If 2nd argument isn't a constant, just do the default thing. This may work or it may cause a server parse error. if (n == Integer.MIN_VALUE) return super.getSQL(dialect, arguments); - var scale = Math.pow(10,n); + var scale = Math.pow(10, n); SQLFragment scaled = new SQLFragment().append("(").append(arguments[0]).append(")*").appendValue(scale); SQLFragment ret = super.getSQL(dialect, new SQLFragment[]{scaled}); ret.append("/").appendValue(scale); return ret; } - } - } + } + } static class AgeMethodInfo extends AbstractMethodInfo @@ -892,12 +914,12 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) MethodInfo dayofmonth = labkeyMethod.get("dayofmonth").getMethodInfo(); SQLFragment ret = new SQLFragment(); - SQLFragment yearA = year.getSQL(dialect, new SQLFragment[] {arguments[0]}); - SQLFragment monthA = month.getSQL(dialect, new SQLFragment[] {arguments[0]}); - SQLFragment dayA = dayofmonth.getSQL(dialect, new SQLFragment[] {arguments[0]}); - SQLFragment yearB = year.getSQL(dialect, new SQLFragment[] {arguments[1]}); - SQLFragment monthB = month.getSQL(dialect, new SQLFragment[] {arguments[1]}); - SQLFragment dayB = dayofmonth.getSQL(dialect, new SQLFragment[] {arguments[1]}); + SQLFragment yearA = year.getSQL(dialect, new SQLFragment[]{arguments[0]}); + SQLFragment monthA = month.getSQL(dialect, new SQLFragment[]{arguments[0]}); + SQLFragment dayA = dayofmonth.getSQL(dialect, new SQLFragment[]{arguments[0]}); + SQLFragment yearB = year.getSQL(dialect, new SQLFragment[]{arguments[1]}); + SQLFragment monthB = month.getSQL(dialect, new SQLFragment[]{arguments[1]}); + SQLFragment dayB = dayofmonth.getSQL(dialect, new SQLFragment[]{arguments[1]}); ret.append("(CASE WHEN (") .append(monthA).append(">").append(monthB).append(" OR ") @@ -929,12 +951,12 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) MethodInfo dayofmonth = labkeyMethod.get("dayofmonth").getMethodInfo(); SQLFragment ret = new SQLFragment(); - SQLFragment yearA = year.getSQL(dialect, new SQLFragment[] {arguments[0]}); - SQLFragment monthA = month.getSQL(dialect, new SQLFragment[] {arguments[0]}); - SQLFragment dayA = dayofmonth.getSQL(dialect, new SQLFragment[] {arguments[0]}); - SQLFragment yearB = year.getSQL(dialect, new SQLFragment[] {arguments[1]}); - SQLFragment monthB = month.getSQL(dialect, new SQLFragment[] {arguments[1]}); - SQLFragment dayB = dayofmonth.getSQL(dialect, new SQLFragment[] {arguments[1]}); + SQLFragment yearA = year.getSQL(dialect, new SQLFragment[]{arguments[0]}); + SQLFragment monthA = month.getSQL(dialect, new SQLFragment[]{arguments[0]}); + SQLFragment dayA = dayofmonth.getSQL(dialect, new SQLFragment[]{arguments[0]}); + SQLFragment yearB = year.getSQL(dialect, new SQLFragment[]{arguments[1]}); + SQLFragment monthB = month.getSQL(dialect, new SQLFragment[]{arguments[1]}); + SQLFragment dayB = dayofmonth.getSQL(dialect, new SQLFragment[]{arguments[1]}); ret.append("(CASE WHEN (") .append(dayA).append(">").append(dayB) @@ -989,8 +1011,6 @@ else if (dialect.isCaseSensitive()) } - - static class IsEqualInfo extends AbstractMethodInfo { IsEqualInfo() @@ -1041,7 +1061,7 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) { SQLFragment ret = new SQLFragment("?"); ret.add((Callable) () -> { - User user = (User)QueryServiceImpl.get().getEnvironment(QueryService.Environment.USER); + User user = (User) QueryServiceImpl.get().getEnvironment(QueryService.Environment.USER); return null == user ? null : user.getUserId(); }); return ret; @@ -1072,7 +1092,7 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) { SQLFragment ret = new SQLFragment("?"); ret.add((Callable) () -> { - User user = (User)QueryServiceImpl.get().getEnvironment(QueryService.Environment.USER); + User user = (User) QueryServiceImpl.get().getEnvironment(QueryService.Environment.USER); if (null == user) return null; return user.getDisplayName(user); @@ -1099,7 +1119,7 @@ public SQLFragment getSQL(Query query, SqlDialect dialect, SQLFragment[] argumen // NOTE we resolve CONTAINER at compile time because we don't have a good place to set this variable at runtime // use of SqlSelector and async complicate that Container cCompile = getCompileTimeContainer(query); - v = null==cCompile ? null : path ? cCompile.getPath() : cCompile.getName(); + v = null == cCompile ? null : path ? cCompile.getPath() : cCompile.getName(); if (null == v) return new SQLFragment("CAST(NULL AS VARCHAR)"); @@ -1145,6 +1165,7 @@ public SQLFragment getSQL(Query query, SqlDialect dialect, SQLFragment[] argumen return new SQLFragment("CAST(NULL AS VARCHAR)"); } } + static class JavaConstantInfo extends AbstractMethodInfo { JavaConstantInfo() @@ -1161,8 +1182,8 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) int dot = param.lastIndexOf('.'); if (dot < 0) break getProperty; - String className = param.substring(0,dot); - String propertyName = param.substring(dot+1); + String className = param.substring(0, dot); + String propertyName = param.substring(dot + 1); Class cls; try @@ -1249,12 +1270,12 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) //Current UserID gets put in QueryService.getEnvironment() by AuthFilter // NOTE: ideally this should be calculated at RUN time not compile time. (see UserIdInfo) // However, we are generating an IN () clause here, and it's easier to do this way - User user = (User)QueryServiceImpl.get().getEnvironment(QueryService.Environment.USER); + User user = (User) QueryServiceImpl.get().getEnvironment(QueryService.Environment.USER); if (null == user) throw new IllegalStateException("Query environment has not been set"); SQLFragment ret = new SQLFragment(); ret.append("(").append(groupArg).append(") IN (").append( - user.getGroups().stream().map(i -> Integer.toString(i)).collect(Collectors.joining(",")) + user.getGroups().stream().map(i -> Integer.toString(i)).collect(Collectors.joining(",")) ).append(")"); return ret; } @@ -1263,7 +1284,7 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) // NOTE: we are not verifying principals.container in (project,site) return CompareType.getMemberOfSQL(dialect, userArg, groupArg); - } + } } @@ -1313,7 +1334,7 @@ public static Method resolve(SqlDialect d, String name) { Method m = null; name = name.toLowerCase(); - if (null != d ) + if (null != d) { if (d.isPostgreSQL()) m = postgresMethods.get(name); @@ -1418,7 +1439,8 @@ public static SQLFragment escapeLikePattern(@Nullable SqlDialect d, SQLFragment } - /** NOTE: because of how our parser works, a bunch of random SQL keywords end up as strings. + /** + * NOTE: because of how our parser works, a bunch of random SQL keywords end up as strings. * isSimpleString() and toSimpleString() can be used to "extract" those sequences. * For instance "SQL_TSI_DAY" * It would be better to parse these into QSqlKeyword or something like that, but we'd still have the @@ -1428,44 +1450,46 @@ public static boolean isSimpleString(SQLFragment f) { String s = f.getSQL(); // am I a simple bound parameter? - if ("?".equals(s) && f.getParams().size()==1) + if ("?".equals(s) && f.getParams().size() == 1) return f.getParams().get(0) instanceof String; if (!f.getParams().isEmpty()) return false; if (s.endsWith("::VARCHAR")) - s = s.substring(0, s.length()-"::VARCHAR".length()); + s = s.substring(0, s.length() - "::VARCHAR".length()); // am I 'normal' SQL String with no embedded single-quotes? if (s.length() >= 2 && s.startsWith("'")) - return s.length()-1 == s.indexOf('\'',1); + return s.length() - 1 == s.indexOf('\'', 1); // am I a sqlserver N' string with no embedded single-quotes? if (s.length() >= 3 && s.startsWith("N'")) - return s.length()-1 == s.indexOf('\'',2); + return s.length() - 1 == s.indexOf('\'', 2); return false; } - /** see {@link #isSimpleString(SQLFragment)} */ + /** + * see {@link #isSimpleString(SQLFragment)} + */ public static String toSimpleString(SQLFragment f) { if (!isSimpleString(f)) throw new IllegalArgumentException(f.toDebugString()); String s = f.getSQL(); - if ("?".equals(s) && f.getParams().size()==1) - return (String)f.getParams().get(0); - assert(s.startsWith("'") || s.startsWith("N'")); - assert(s.endsWith("'") || s.endsWith("'::VARCHAR")); + if ("?".equals(s) && f.getParams().size() == 1) + return (String) f.getParams().get(0); + assert (s.startsWith("'") || s.startsWith("N'")); + assert (s.endsWith("'") || s.endsWith("'::VARCHAR")); if (s.endsWith("::VARCHAR")) - s = s.substring(0, s.length()-"::VARCHAR".length()); + s = s.substring(0, s.length() - "::VARCHAR".length()); if (s.startsWith("'")) - return s.substring(1,s.length()-1); + return s.substring(1, s.length() - 1); if (s.startsWith("N'")) - return s.substring(2,s.length()-1); + return s.substring(2, s.length() - 1); throw new IllegalArgumentException(f.toDebugString()); } static Container getCompileTimeContainer(Query query) { - Container cCompile = (Container)QueryServiceImpl.get().getEnvironment(QueryService.Environment.CONTAINER); + Container cCompile = (Container) QueryServiceImpl.get().getEnvironment(QueryService.Environment.CONTAINER); if (null == cCompile && null != query) { // Issue 53355: A column may be erroneously constructed (e.g. invalid calculated column) which can result in the schema being null @@ -1477,55 +1501,138 @@ static Container getCompileTimeContainer(Query query) } + // ARRAY methods + + private interface BinarySqlGenerator + { + SQLFragment apply(SqlDialect d, SQLFragment a, SQLFragment b); + } + + + static class ArrayOperatorMethod extends Method + { + final BinarySqlGenerator sqlGenerator; + ArrayOperatorMethod(String name, BinarySqlGenerator sqlGenerator) + { + super(name, JdbcType.BOOLEAN, 2, 2); + this.sqlGenerator = sqlGenerator; + } + + @Override + public MethodInfo getMethodInfo() + { + return new AbstractMethodInfo(JdbcType.BOOLEAN) + { + @Override + public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) + { + return sqlGenerator.apply(dialect, arguments[0], arguments[1]); + } + }; + } + } + + + static class ArrayConstructMethod extends Method + { + ArrayConstructMethod(String name) + { + super(name, JdbcType.ARRAY, 0, Integer.MAX_VALUE); + } + + @Override + public MethodInfo getMethodInfo() + { + return new AbstractMethodInfo(JdbcType.ARRAY) + { + @Override + public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) + { + return dialect.array_construct(arguments); + } + }; + } + } + + final static Map postgresMethods = Collections.synchronizedMap(new CaseInsensitiveHashMap<>()); + static { - postgresMethods.put("ascii",new PassthroughMethod("ascii",JdbcType.INTEGER,1,1)); - postgresMethods.put("btrim",new PassthroughMethod("btrim",JdbcType.VARCHAR,1,2)); - postgresMethods.put("char_length",new PassthroughMethod("char_length",JdbcType.INTEGER,1,1)); - postgresMethods.put("character_length",new PassthroughMethod("character_length",JdbcType.INTEGER,1,1)); - postgresMethods.put("chr",new PassthroughMethod("chr",JdbcType.VARCHAR,1,1)); + postgresMethods.put("ascii", new PassthroughMethod("ascii", JdbcType.INTEGER, 1, 1)); + postgresMethods.put("btrim", new PassthroughMethod("btrim", JdbcType.VARCHAR, 1, 2)); + postgresMethods.put("char_length", new PassthroughMethod("char_length", JdbcType.INTEGER, 1, 1)); + postgresMethods.put("character_length", new PassthroughMethod("character_length", JdbcType.INTEGER, 1, 1)); + postgresMethods.put("chr", new PassthroughMethod("chr", JdbcType.VARCHAR, 1, 1)); postgresMethods.put("concat_ws", new PassthroughMethod("concat_ws", JdbcType.VARCHAR, 1, Integer.MAX_VALUE)); - postgresMethods.put("decode",new PassthroughMethod("decode",JdbcType.VARCHAR,2,2)); - postgresMethods.put("encode",new PassthroughMethod("encode",JdbcType.VARCHAR,2,2)); - postgresMethods.put("initcap",new PassthroughMethod("initcap",JdbcType.VARCHAR,1,1)); - postgresMethods.put("lpad",new PassthroughMethod("lpad",JdbcType.VARCHAR,2,3)); - postgresMethods.put("md5",new PassthroughMethod("md5",JdbcType.VARCHAR,1,1)); - postgresMethods.put("octet_length",new PassthroughMethod("octet_length",JdbcType.INTEGER,1,1)); - postgresMethods.put("overlaps",new PassthroughMethod("overlaps",JdbcType.BOOLEAN,4,4) { + postgresMethods.put("decode", new PassthroughMethod("decode", JdbcType.VARCHAR, 2, 2)); + postgresMethods.put("encode", new PassthroughMethod("encode", JdbcType.VARCHAR, 2, 2)); + postgresMethods.put("initcap", new PassthroughMethod("initcap", JdbcType.VARCHAR, 1, 1)); + postgresMethods.put("lpad", new PassthroughMethod("lpad", JdbcType.VARCHAR, 2, 3)); + postgresMethods.put("md5", new PassthroughMethod("md5", JdbcType.VARCHAR, 1, 1)); + postgresMethods.put("octet_length", new PassthroughMethod("octet_length", JdbcType.INTEGER, 1, 1)); + postgresMethods.put("overlaps", new PassthroughMethod("overlaps", JdbcType.BOOLEAN, 4, 4) + { @Override public MethodInfo getMethodInfo() { return new OverlapsMethodInfo(); } }); - postgresMethods.put("quote_ident",new PassthroughMethod("quote_ident",JdbcType.VARCHAR,1,1)); - postgresMethods.put("quote_literal",new PassthroughMethod("quote_literal",JdbcType.VARCHAR,1,1)); - postgresMethods.put("regexp_replace",new PassthroughMethod("regexp_replace",JdbcType.VARCHAR,3,4)); - postgresMethods.put("repeat",new PassthroughMethod("repeat",JdbcType.VARCHAR,2,2)); - postgresMethods.put("replace",new PassthroughMethod("replace",JdbcType.VARCHAR,3,3)); - postgresMethods.put("rpad",new PassthroughMethod("rpad",JdbcType.VARCHAR,2,3)); - postgresMethods.put("similar_to", new PassthroughMethod("similar_to", JdbcType.BOOLEAN, 2, 3) { + postgresMethods.put("quote_ident", new PassthroughMethod("quote_ident", JdbcType.VARCHAR, 1, 1)); + postgresMethods.put("quote_literal", new PassthroughMethod("quote_literal", JdbcType.VARCHAR, 1, 1)); + postgresMethods.put("regexp_replace", new PassthroughMethod("regexp_replace", JdbcType.VARCHAR, 3, 4)); + postgresMethods.put("repeat", new PassthroughMethod("repeat", JdbcType.VARCHAR, 2, 2)); + postgresMethods.put("replace", new PassthroughMethod("replace", JdbcType.VARCHAR, 3, 3)); + postgresMethods.put("rpad", new PassthroughMethod("rpad", JdbcType.VARCHAR, 2, 3)); + postgresMethods.put("similar_to", new PassthroughMethod("similar_to", JdbcType.BOOLEAN, 2, 3) + { @Override - public MethodInfo getMethodInfo() { return new SimilarToMethodInfo(); } + public MethodInfo getMethodInfo() + { + return new SimilarToMethodInfo(); + } }); - postgresMethods.put("split_part",new PassthroughMethod("split_part",JdbcType.VARCHAR,3,3)); - postgresMethods.put("strpos",new PassthroughMethod("strpos",JdbcType.VARCHAR,2,2)); - postgresMethods.put("substr",new PassthroughMethod("substr",JdbcType.VARCHAR,2,3)); - postgresMethods.put("to_ascii",new PassthroughMethod("to_ascii",JdbcType.VARCHAR,1,2)); - postgresMethods.put("to_hex",new PassthroughMethod("to_hex",JdbcType.VARCHAR,1,1)); - postgresMethods.put("translate",new PassthroughMethod("translate",JdbcType.VARCHAR,3,3)); - postgresMethods.put("to_char",new PassthroughMethod("to_char",JdbcType.VARCHAR,2,2)); - postgresMethods.put("to_date",new PassthroughMethod("to_date",JdbcType.DATE,2,2)); - postgresMethods.put("to_timestamp",new PassthroughMethod("to_timestamp",JdbcType.TIMESTAMP,2,2)); - postgresMethods.put("to_number",new PassthroughMethod("to_number",JdbcType.DECIMAL,2,2)); - postgresMethods.put("string_to_array",new PassthroughMethod("string_to_array",JdbcType.VARCHAR,2,3)); - postgresMethods.put("unnest",new PassthroughMethod("unnest",JdbcType.VARCHAR,1,1)); - postgresMethods.put("row",new PassthroughMethod("row",JdbcType.VARCHAR,1, Integer.MAX_VALUE)); + postgresMethods.put("split_part", new PassthroughMethod("split_part", JdbcType.VARCHAR, 3, 3)); + postgresMethods.put("strpos", new PassthroughMethod("strpos", JdbcType.VARCHAR, 2, 2)); + postgresMethods.put("substr", new PassthroughMethod("substr", JdbcType.VARCHAR, 2, 3)); + postgresMethods.put("to_ascii", new PassthroughMethod("to_ascii", JdbcType.VARCHAR, 1, 2)); + postgresMethods.put("to_hex", new PassthroughMethod("to_hex", JdbcType.VARCHAR, 1, 1)); + postgresMethods.put("translate", new PassthroughMethod("translate", JdbcType.VARCHAR, 3, 3)); + postgresMethods.put("to_char", new PassthroughMethod("to_char", JdbcType.VARCHAR, 2, 2)); + postgresMethods.put("to_date", new PassthroughMethod("to_date", JdbcType.DATE, 2, 2)); + postgresMethods.put("to_timestamp", new PassthroughMethod("to_timestamp", JdbcType.TIMESTAMP, 2, 2)); + postgresMethods.put("to_number", new PassthroughMethod("to_number", JdbcType.DECIMAL, 2, 2)); + postgresMethods.put("string_to_array", new PassthroughMethod("string_to_array", JdbcType.VARCHAR, 2, 3)); + postgresMethods.put("unnest", new PassthroughMethod("unnest", JdbcType.VARCHAR, 1, 1)); + postgresMethods.put("row", new PassthroughMethod("row", JdbcType.VARCHAR, 1, Integer.MAX_VALUE)); + + addPostgresArrayMethods(); addPostgresJsonMethods(); } + + private static void addPostgresArrayMethods() + { + // (ELEMENT...) + postgresMethods.put("array_construct", new ArrayConstructMethod("array_construct")); + + // (ARRAY, ELEMENT) + postgresMethods.put("array_contains_element", new ArrayOperatorMethod("array_contains_element", (d,a,b) -> d.element_in_array(b,a))); + // Use "NOT array_contains_element()" instead of something clumsy like "array_does_not_contain()" + + // (ARRAY, ARRAY) + postgresMethods.put("array_contains_all", new ArrayOperatorMethod("array_contains_all", (d,a,b) -> d.array_all_in_array(b,a))); + postgresMethods.put("array_contains_any", new ArrayOperatorMethod("array_contains_any", SqlDialect::array_some_in_array)); + postgresMethods.put("array_contains_none", new ArrayOperatorMethod("array_contains_none", SqlDialect::array_none_in_array)); + + // not array_equals() because arrays are ordered, this is an unordered comparison + postgresMethods.put("array_is_same", new ArrayOperatorMethod("array_is_same", SqlDialect::array_same_array)); + // Use "NOT array_is_same()" instead of something clumsy like "array_is_not_same()" + } + + /** * Wire up JSON and JSONB data type support for Postgres, as described here: * https://www.postgresql.org/docs/9.5/functions-json.html @@ -1624,14 +1731,16 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) 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) { + 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) { + labkeyMethod.put("is_not_distinct_from", new Method(JdbcType.BOOLEAN, 2, 2) + { @Override public MethodInfo getMethodInfo() { @@ -1649,6 +1758,7 @@ private static class IsDistinctFromMethodInfo extends AbstractMethodInfo super(JdbcType.BOOLEAN); this.token = token; } + @Override public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) { @@ -1670,32 +1780,34 @@ private static void addJsonPassthroughMethod(String name, JdbcType type, int min } final static Map mssqlMethods = Collections.synchronizedMap(new CaseInsensitiveHashMap<>()); + static { - mssqlMethods.put("ascii",new PassthroughMethod("ascii",JdbcType.INTEGER,1,1)); - Method chr = new PassthroughMethod("char",JdbcType.VARCHAR,1,1); + mssqlMethods.put("ascii", new PassthroughMethod("ascii", JdbcType.INTEGER, 1, 1)); + Method chr = new PassthroughMethod("char", JdbcType.VARCHAR, 1, 1); mssqlMethods.put("char", chr); mssqlMethods.put("chr", chr); // postgres and oracle use 'chr' (see 15473) - mssqlMethods.put("charindex",new PassthroughMethod("charindex",JdbcType.INTEGER,2,3)); + 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)); - 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)); - mssqlMethods.put("stuff",new PassthroughMethod("stuff",JdbcType.VARCHAR,4,4)); + mssqlMethods.put("difference", new PassthroughMethod("difference", JdbcType.INTEGER, 2, 2)); + mssqlMethods.put("isnumeric", new PassthroughMethod("isnumeric", JdbcType.BOOLEAN, 1, 1)); + 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)); + mssqlMethods.put("stuff", new PassthroughMethod("stuff", JdbcType.VARCHAR, 4, 4)); mssqlMethods.put("ucase", new PassthroughMethod("upper", JdbcType.VARCHAR, 1, 1)); mssqlMethods.put("upper", new PassthroughMethod("upper", JdbcType.VARCHAR, 1, 1)); } final static Map oracleMethods = Collections.synchronizedMap(new CaseInsensitiveHashMap<>()); + static { /* Standard Oracle Functions @@ -1703,27 +1815,27 @@ private static void addJsonPassthroughMethod(String name, JdbcType type, int min // Numeric Functions - Haven't put advanced mathematical functions in. Can add in later if the demand is there. - oracleMethods.put("to_number", new PassthroughMethod("to_number", JdbcType.DECIMAL, 1,3)); + oracleMethods.put("to_number", new PassthroughMethod("to_number", JdbcType.DECIMAL, 1, 3)); // Character Functions returning Character Values - oracleMethods.put("to_char", new PassthroughMethod("to_char", JdbcType.VARCHAR, 1,3)); - oracleMethods.put("substr", new PassthroughMethod("substr", JdbcType.VARCHAR, 2,3)); - oracleMethods.put("trim", new PassthroughMethod("trim", JdbcType.VARCHAR, 1,1)); - oracleMethods.put("instr", new PassthroughMethod("instr", JdbcType.VARCHAR, 2,4)); - oracleMethods.put("replace", new PassthroughMethod("replace", JdbcType.VARCHAR, 2,3)); - oracleMethods.put("translate", new PassthroughMethod("translate", JdbcType.VARCHAR, 3,3)); - oracleMethods.put("rpad", new PassthroughMethod("rpad", JdbcType.VARCHAR, 2,3)); - oracleMethods.put("lpad", new PassthroughMethod("lpad", JdbcType.VARCHAR, 2,3)); - oracleMethods.put("ascii", new PassthroughMethod("ascii", JdbcType.INTEGER, 1,1)); - oracleMethods.put("initcap", new PassthroughMethod("initcap", JdbcType.VARCHAR, 1,1)); - oracleMethods.put("chr", new PassthroughMethod("chr", JdbcType.VARCHAR, 1,1)); - oracleMethods.put("regexp_like", new PassthroughMethod("regexp_like", JdbcType.VARCHAR, 2,2)); - - // Date Functions - - oracleMethods.put("to_date", new PassthroughMethod("to_date", JdbcType.DATE, 1,3)); - oracleMethods.put("sysdate", new PassthroughMethod("sysdate", JdbcType.DATE, 0,0)); + oracleMethods.put("to_char", new PassthroughMethod("to_char", JdbcType.VARCHAR, 1, 3)); + oracleMethods.put("substr", new PassthroughMethod("substr", JdbcType.VARCHAR, 2, 3)); + oracleMethods.put("trim", new PassthroughMethod("trim", JdbcType.VARCHAR, 1, 1)); + oracleMethods.put("instr", new PassthroughMethod("instr", JdbcType.VARCHAR, 2, 4)); + oracleMethods.put("replace", new PassthroughMethod("replace", JdbcType.VARCHAR, 2, 3)); + oracleMethods.put("translate", new PassthroughMethod("translate", JdbcType.VARCHAR, 3, 3)); + oracleMethods.put("rpad", new PassthroughMethod("rpad", JdbcType.VARCHAR, 2, 3)); + oracleMethods.put("lpad", new PassthroughMethod("lpad", JdbcType.VARCHAR, 2, 3)); + oracleMethods.put("ascii", new PassthroughMethod("ascii", JdbcType.INTEGER, 1, 1)); + oracleMethods.put("initcap", new PassthroughMethod("initcap", JdbcType.VARCHAR, 1, 1)); + oracleMethods.put("chr", new PassthroughMethod("chr", JdbcType.VARCHAR, 1, 1)); + oracleMethods.put("regexp_like", new PassthroughMethod("regexp_like", JdbcType.VARCHAR, 2, 2)); + + // Date Functions + + oracleMethods.put("to_date", new PassthroughMethod("to_date", JdbcType.DATE, 1, 3)); + oracleMethods.put("sysdate", new PassthroughMethod("sysdate", JdbcType.DATE, 0, 0)); } private static class ParseJSONMethod extends Method @@ -1750,7 +1862,6 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) } } - public static class TestCase extends Assert { void assertIsSimpleString(String expected, SQLFragment s) @@ -1758,6 +1869,7 @@ void assertIsSimpleString(String expected, SQLFragment s) assertTrue(expected + " should be a simple string", isSimpleString(s)); assertEquals(expected, toSimpleString(s)); } + void assertNotSimpleString(SQLFragment s) { assertFalse(s.toDebugString() + " should not be a simple string", isSimpleString(s)); @@ -1772,7 +1884,7 @@ public void testSimpleStringIntended() // e.g SQL keywords and oeprators for (var s : List.of("->", "->>", "#>", "#>>", "@>", "<@", "?", "?|", "?&", "||", "-", "#-", SQL_TSI_FRAC_SECOND.name())) { - assertIsSimpleString(s, new SQLFragment().appendStringLiteral(s,d)); + assertIsSimpleString(s, new SQLFragment().appendStringLiteral(s, d)); assertIsSimpleString(s, new SQLFragment("'" + s + "'")); assertIsSimpleString(s, new SQLFragment("N'" + s + "'")); assertIsSimpleString(s, new SQLFragment("'" + s + "'::VARCHAR")); diff --git a/query/src/org/labkey/query/sql/SqlBase.g b/query/src/org/labkey/query/sql/SqlBase.g index 908ce409aff..7896ace5d40 100644 --- a/query/src/org/labkey/query/sql/SqlBase.g +++ b/query/src/org/labkey/query/sql/SqlBase.g @@ -155,6 +155,7 @@ tokens ALL : 'all'; ANY : 'any'; AND : 'and'; +ARRAY : 'array['; // kinda a hack to avoid making "ARRAY" a new keyword ARRAY_AGG : 'array_agg'; AS : 'as'; AVG : 'avg'; @@ -756,7 +757,8 @@ starAtom // level 0 - the basic element of an expression primaryExpression - : id=identPrimary + : ARRAY exprList ']' -> ^(METHOD_CALL IDENT["ARRAY_CONSTRUCT"] exprList) + | id=identPrimary | constant | OPEN! ( expression | subQuery) CLOSE! | PARAM^ (NUM_INT)? From 2a0007c2ef6e34ec889f785ff70310bdf9aff32d Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Tue, 2 Dec 2025 13:18:44 -0800 Subject: [PATCH 3/7] junit test --- query/src/org/labkey/query/QueryTestCase.jsp | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index df35936f006..cbdbcbb9519 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -1933,4 +1933,38 @@ d,seven,twelve,day,month,date,duration,guid assertNull( booleanSelect(dialect.array_none_in_array( elNull, array("NULL") ))); assertNull( booleanSelect(dialect.array_none_in_array( array("NULL"), elNull ))); } + + @Test + public void testArraySql() throws SQLException + { + var testSql = """ + SELECT 'a' as test, false as expected, array_contains_all( ARRAY['A','X'], ARRAY['A','B'] ) as result + UNION ALL + SELECT 'b' as test, true as expected, array_contains_any( ARRAY['A','X'], ARRAY['A','B'] ) as result + UNION ALL + SELECT 'c' as test, false as expected, array_contains_none( ARRAY['A','X'], ARRAY['A','B'] ) as result + UNION ALL + SELECT 'd' as test, false as expected, array_is_same( ARRAY['A','X'], ARRAY['A','B'] ) as result + UNION ALL + SELECT 'd' as test, true as expected, array_is_same( ARRAY['A','B'], ARRAY['A','B'] ) as result + UNION ALL + SELECT 'e' as test, true as expected, array_contains_element( ARRAY['A','B'], 'B') as result + UNION ALL + SELECT 'f' as test, false as expected, array_contains_element( ARRAY['A','B'], 'X') as result + """; + + Container container = JunitUtil.getTestContainer(); + User user = TestContext.get().getUser(); + var schema = DefaultSchema.get(user, container).getSchema("core"); + try (var rs =QueryService.get().select(schema, testSql);) + { + while (rs.next()) + { + var test = rs.getString(1); + var expected = rs.getBoolean(2); + var result = rs.getBoolean(3); + assertEquals("test " + test + " failed", expected, result); + } + } + } %> From fd14ba18ca3dfcf733ab1c6a312bef44609459cd Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Tue, 2 Dec 2025 14:51:53 -0800 Subject: [PATCH 4/7] junit test --- query/src/org/labkey/query/QueryTestCase.jsp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index cbdbcbb9519..a6dd7838687 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -1937,6 +1937,9 @@ d,seven,twelve,day,month,date,duration,guid @Test public void testArraySql() throws SQLException { + if (!dialect.supportsArrays()) + return; + var testSql = """ SELECT 'a' as test, false as expected, array_contains_all( ARRAY['A','X'], ARRAY['A','B'] ) as result UNION ALL From 6994b54ccec95333291e2b75d88d9b7ebd7cd06a Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Mon, 8 Dec 2025 10:36:36 -0800 Subject: [PATCH 5/7] TEXTARRAY[] textarray_construct() --- .../core/dialect/PostgreSql92Dialect.java | 10 ++++++++ query/src/org/labkey/query/sql/Method.java | 24 +++++++++++++++++-- query/src/org/labkey/query/sql/SqlBase.g | 4 +++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java b/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java index 0f5146d5f85..67ce9924208 100644 --- a/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java +++ b/core/src/org/labkey/core/dialect/PostgreSql92Dialect.java @@ -287,6 +287,16 @@ AND d.deptype IN ('a', 'i') -- Automatic dependency for DEFAULT or index-related // ARRAY and SET syntax // + // NOTE LabKey currently does not support ARRAY[VARCHAR], use ARRAY[text] instead + // + // Postgres string literals can be auto-cast to both VARCHAR and TEXT. These all work + // 'color' = 'color'::varchar + // 'color' = 'color'::text + // ARRAY['color'] = ARRAY['color'::text]; + // However, ARRAY[text] cannot be auto cast to ARRAY[varchar] + // ARRAY['color'] = ARRAY['color'::varchar]; -- ERROR! + // + @Override public boolean supportsArrays() diff --git a/query/src/org/labkey/query/sql/Method.java b/query/src/org/labkey/query/sql/Method.java index dbdb46ec86c..77214c9d348 100644 --- a/query/src/org/labkey/query/sql/Method.java +++ b/query/src/org/labkey/query/sql/Method.java @@ -48,7 +48,6 @@ import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; import org.labkey.api.util.GUID; -import org.labkey.api.util.Pair; import org.labkey.query.QueryServiceImpl; import org.labkey.query.sql.antlr.SqlBaseLexer; @@ -61,7 +60,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; -import java.util.function.BinaryOperator; import java.util.stream.Collectors; import static org.labkey.query.sql.Method.TimestampDiffInterval.SQL_TSI_FRAC_SECOND; @@ -1554,6 +1552,27 @@ public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) } } + static class TextArrayConstructMethod extends Method + { + TextArrayConstructMethod(String name) + { + super(name, JdbcType.ARRAY, 0, Integer.MAX_VALUE); + } + + @Override + public MethodInfo getMethodInfo() + { + return new AbstractMethodInfo(JdbcType.ARRAY) + { + @Override + public SQLFragment getSQL(SqlDialect dialect, SQLFragment[] arguments) + { + return new SQLFragment("CAST(").append(dialect.array_construct(arguments)).append(" AS TEXT[])"); + } + }; + } + } + final static Map postgresMethods = Collections.synchronizedMap(new CaseInsensitiveHashMap<>()); @@ -1617,6 +1636,7 @@ private static void addPostgresArrayMethods() { // (ELEMENT...) postgresMethods.put("array_construct", new ArrayConstructMethod("array_construct")); + postgresMethods.put("textarray_construct", new TextArrayConstructMethod("textarray_construct")); // (ARRAY, ELEMENT) postgresMethods.put("array_contains_element", new ArrayOperatorMethod("array_contains_element", (d,a,b) -> d.element_in_array(b,a))); diff --git a/query/src/org/labkey/query/sql/SqlBase.g b/query/src/org/labkey/query/sql/SqlBase.g index 7896ace5d40..f3a9aa7f71c 100644 --- a/query/src/org/labkey/query/sql/SqlBase.g +++ b/query/src/org/labkey/query/sql/SqlBase.g @@ -155,7 +155,7 @@ tokens ALL : 'all'; ANY : 'any'; AND : 'and'; -ARRAY : 'array['; // kinda a hack to avoid making "ARRAY" a new keyword +ARRAY : 'array['; // kinda a hack to avoid making "ARRAY" a new keyword (alternate would be to use { identifier '[' } and sort it out in SqlParser.java ARRAY_AGG : 'array_agg'; AS : 'as'; AVG : 'avg'; @@ -228,6 +228,7 @@ STDDEV_POP : 'stddev_pop'; STDDEV_SAMP : 'stddev_samp'; STDERR : 'stderr'; SUM : 'sum'; +TEXTARRAY : 'textarray['; THEN : 'then'; TRUE : 'true'; UNION : 'union'; @@ -758,6 +759,7 @@ starAtom // level 0 - the basic element of an expression primaryExpression : ARRAY exprList ']' -> ^(METHOD_CALL IDENT["ARRAY_CONSTRUCT"] exprList) + | TEXTARRAY exprList ']' -> ^(METHOD_CALL IDENT["TEXTARRAY_CONSTRUCT"] exprList) | id=identPrimary | constant | OPEN! ( expression | subQuery) CLOSE! From 239b3d8bbaab1a2a824289398b39e78f92f44688 Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Tue, 9 Dec 2025 20:49:30 -0800 Subject: [PATCH 6/7] Update query/src/org/labkey/query/QueryTestCase.jsp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- query/src/org/labkey/query/QueryTestCase.jsp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index a6dd7838687..17373f20bb5 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -1949,11 +1949,11 @@ d,seven,twelve,day,month,date,duration,guid UNION ALL SELECT 'd' as test, false as expected, array_is_same( ARRAY['A','X'], ARRAY['A','B'] ) as result UNION ALL - SELECT 'd' as test, true as expected, array_is_same( ARRAY['A','B'], ARRAY['A','B'] ) as result + SELECT 'e' as test, true as expected, array_is_same( ARRAY['A','B'], ARRAY['A','B'] ) as result UNION ALL - SELECT 'e' as test, true as expected, array_contains_element( ARRAY['A','B'], 'B') as result + SELECT 'f' as test, true as expected, array_contains_element( ARRAY['A','B'], 'B') as result UNION ALL - SELECT 'f' as test, false as expected, array_contains_element( ARRAY['A','B'], 'X') as result + SELECT 'g' as test, false as expected, array_contains_element( ARRAY['A','B'], 'X') as result """; Container container = JunitUtil.getTestContainer(); From 98554f52d707e672b79a74f7340a27f2a1ed47f8 Mon Sep 17 00:00:00 2001 From: labkey-matthewb Date: Tue, 9 Dec 2025 20:50:08 -0800 Subject: [PATCH 7/7] Update query/src/org/labkey/query/QueryTestCase.jsp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- query/src/org/labkey/query/QueryTestCase.jsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/query/src/org/labkey/query/QueryTestCase.jsp b/query/src/org/labkey/query/QueryTestCase.jsp index 17373f20bb5..5cf3e7d3446 100644 --- a/query/src/org/labkey/query/QueryTestCase.jsp +++ b/query/src/org/labkey/query/QueryTestCase.jsp @@ -1959,7 +1959,7 @@ d,seven,twelve,day,month,date,duration,guid Container container = JunitUtil.getTestContainer(); User user = TestContext.get().getUser(); var schema = DefaultSchema.get(user, container).getSchema("core"); - try (var rs =QueryService.get().select(schema, testSql);) + try (var rs =QueryService.get().select(schema, testSql)) { while (rs.next()) {