diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 5de10c00..8cc0d588 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -73,6 +73,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface; import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates; import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; +import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates; /******************************************************************************* @@ -553,6 +554,12 @@ public class GenerateReportAction AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); fieldAggregates.add(record.getValueInteger(field.getName())); } + else if(field.getType().equals(QFieldType.LONG)) + { + @SuppressWarnings("unchecked") + AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates()); + fieldAggregates.add(record.getValueLong(field.getName())); + } else if(field.getType().equals(QFieldType.DECIMAL)) { @SuppressWarnings("unchecked") diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 7905e706..782f50d5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -270,6 +270,10 @@ public class QPossibleValueTranslator { value = ValueUtils.getValueAsInteger(value); } + if(field.getType().equals(QFieldType.LONG) && !(value instanceof Long)) + { + value = ValueUtils.getValueAsLong(value); + } } catch(QValueException e) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 4457b401..5c4f84ba 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -444,6 +444,16 @@ public class QRecord implements Serializable } + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public Long getValueLong(String fieldName) + { + return (ValueUtils.getValueAsLong(values.get(fieldName))); + } + + /******************************************************************************* ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 8971a846..b0e8f8e7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -37,6 +37,7 @@ public enum QFieldType { STRING, INTEGER, + LONG, DECIMAL, BOOLEAN, DATE, @@ -65,6 +66,10 @@ public enum QFieldType { return (INTEGER); } + if(c.equals(Long.class) || c.equals(long.class)) + { + return (LONG); + } if(c.equals(BigDecimal.class)) { return (DECIMAL); @@ -110,7 +115,7 @@ public enum QFieldType *******************************************************************************/ public boolean isNumeric() { - return this == QFieldType.INTEGER || this == QFieldType.DECIMAL; + return this == QFieldType.INTEGER || this == QFieldType.LONG || this == QFieldType.DECIMAL; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 4685b7e3..a092a520 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -366,7 +366,7 @@ public class MemoryRecordStore ///////////////////////////////////////////////// // set the next serial in the record if needed // ///////////////////////////////////////////////// - if(recordToInsert.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER)) + if(recordToInsert.getValue(primaryKeyField.getName()) == null && (primaryKeyField.getType().equals(QFieldType.INTEGER) || primaryKeyField.getType().equals(QFieldType.LONG))) { recordToInsert.setValue(primaryKeyField.getName(), nextSerial++); } @@ -378,6 +378,13 @@ public class MemoryRecordStore { nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1; } + else if(primaryKeyField.getType().equals(QFieldType.LONG) && recordToInsert.getValueLong(primaryKeyField.getName()) > nextSerial) + { + ////////////////////////////////////// + // todo - mmm, could overflow here? // + ////////////////////////////////////// + nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1; + } tableData.put(recordToInsert.getValue(primaryKeyField.getName()), recordToInsert); if(returnInsertedRecords) @@ -709,7 +716,7 @@ public class MemoryRecordStore { // todo - joins probably? QFieldMetaData field = table.getField(fieldName); - if(field.getType().equals(QFieldType.INTEGER) && (operator.equals(AggregateOperator.AVG))) + if((field.getType().equals(QFieldType.INTEGER) || field.getType().equals(QFieldType.LONG)) && (operator.equals(AggregateOperator.AVG))) { fieldType = QFieldType.DECIMAL; } @@ -745,6 +752,10 @@ public class MemoryRecordStore .filter(r -> r.getValue(fieldName) != null) .mapToInt(r -> r.getValueInteger(fieldName)) .sum(); + case LONG -> records.stream() + .filter(r -> r.getValue(fieldName) != null) + .mapToLong(r -> r.getValueLong(fieldName)) + .sum(); case DECIMAL -> records.stream() .filter(r -> r.getValue(fieldName) != null) .map(r -> r.getValueBigDecimal(fieldName)) @@ -759,6 +770,11 @@ public class MemoryRecordStore .mapToInt(r -> r.getValueInteger(fieldName)) .min() .stream().boxed().findFirst().orElse(null); + case LONG -> records.stream() + .filter(r -> r.getValue(fieldName) != null) + .mapToLong(r -> r.getValueLong(fieldName)) + .min() + .stream().boxed().findFirst().orElse(null); case DECIMAL, STRING, DATE, DATE_TIME -> { Optional serializable = records.stream() @@ -775,7 +791,12 @@ public class MemoryRecordStore { case INTEGER -> records.stream() .filter(r -> r.getValue(fieldName) != null) - .mapToInt(r -> r.getValueInteger(fieldName)) + .mapToLong(r -> r.getValueInteger(fieldName)) + .max() + .stream().boxed().findFirst().orElse(null); + case LONG -> records.stream() + .filter(r -> r.getValue(fieldName) != null) + .mapToLong(r -> r.getValueLong(fieldName)) .max() .stream().boxed().findFirst().orElse(null); case DECIMAL, STRING, DATE, DATE_TIME -> @@ -797,6 +818,11 @@ public class MemoryRecordStore .mapToInt(r -> r.getValueInteger(fieldName)) .average() .stream().boxed().findFirst().orElse(null); + case LONG -> records.stream() + .filter(r -> r.getValue(fieldName) != null) + .mapToLong(r -> r.getValueLong(fieldName)) + .average() + .stream().boxed().findFirst().orElse(null); case DECIMAL -> records.stream() .filter(r -> r.getValue(fieldName) != null) .mapToDouble(r -> r.getValueBigDecimal(fieldName).doubleValue()) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java index 9fc87831..970b69ea 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockQueryAction.java @@ -103,6 +103,7 @@ public class MockQueryAction implements QueryInterface { case STRING -> UUID.randomUUID().toString(); case INTEGER -> 42; + case LONG -> 42L; case DECIMAL -> new BigDecimal("3.14159"); case DATE -> LocalDate.of(1970, Month.JANUARY, 1); case DATE_TIME -> LocalDateTime.of(1970, Month.JANUARY, 1, 0, 0); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java index a2a36a37..e7d8e9d3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/JsonUtils.java @@ -362,6 +362,7 @@ public class JsonUtils switch(metaData.getType()) { case INTEGER -> record.setValue(fieldName, jsonObjectToUse.optInt(backendName)); + case LONG -> record.setValue(fieldName, jsonObjectToUse.optLong(backendName)); case DECIMAL -> record.setValue(fieldName, jsonObjectToUse.optBigDecimal(backendName, null)); case BOOLEAN -> record.setValue(fieldName, jsonObjectToUse.optBoolean(backendName)); case DATE_TIME -> diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index dbb1e075..b73fec7d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -114,6 +114,113 @@ public class ValueUtils + /******************************************************************************* + ** Type-safely make an Long from any Object. + ** null and empty-string inputs return null. + ** We try to strip away commas and decimals (as long as they are exactly equal to the int value) + ** We may throw if the input can't be converted to an integer. + *******************************************************************************/ + public static Long getValueAsLong(Object value) throws QValueException + { + try + { + if(value == null) + { + return (null); + } + else if(value instanceof Integer i) + { + return Long.valueOf((i)); + } + else if(value instanceof Long l) + { + return (l); + } + else if(value instanceof BigInteger b) + { + return (b.longValue()); + } + else if(value instanceof Float f) + { + if(f.longValue() != f) + { + throw (new QValueException(f + " does not have an exact integer representation.")); + } + return (f.longValue()); + } + else if(value instanceof Double d) + { + if(d.longValue() != d) + { + throw (new QValueException(d + " does not have an exact integer representation.")); + } + return (d.longValue()); + } + else if(value instanceof BigDecimal bd) + { + return bd.longValueExact(); + } + else if(value instanceof PossibleValueEnum pve) + { + return getValueAsLong(pve.getPossibleValueId()); + } + else if(value instanceof String s) + { + if(!StringUtils.hasContent(s)) + { + return (null); + } + + try + { + return (Long.parseLong(s)); + } + catch(NumberFormatException nfe) + { + if(s.contains(",")) + { + String sWithoutCommas = s.replaceAll(",", ""); + try + { + return (getValueAsLong(sWithoutCommas)); + } + catch(Exception ignore) + { + throw (nfe); + } + } + if(s.matches(".*\\.\\d+$")) + { + String sWithoutDecimal = s.replaceAll("\\.\\d+$", ""); + try + { + return (getValueAsLong(sWithoutDecimal)); + } + catch(Exception ignore) + { + throw (nfe); + } + } + throw (nfe); + } + } + else + { + throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to Long.")); + } + } + catch(QValueException qve) + { + throw (qve); + } + catch(Exception e) + { + throw (new QValueException("Value [" + value + "] could not be converted to a Long.", e)); + } + } + + + /******************************************************************************* ** Type-safely make an Integer from any Object. ** null and empty-string inputs return null. @@ -693,6 +800,7 @@ public class ValueUtils { case STRING, TEXT, HTML, PASSWORD -> getValueAsString(value); case INTEGER -> getValueAsInteger(value); + case LONG -> getValueAsLong(value); case DECIMAL -> getValueAsBigDecimal(value); case BOOLEAN -> getValueAsBoolean(value); case DATE -> getValueAsLocalDate(value); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java new file mode 100644 index 00000000..bcf1862b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java @@ -0,0 +1,135 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils.aggregates; + + +import java.math.BigDecimal; + + +/******************************************************************************* + ** Long version of data aggregator + *******************************************************************************/ +public class LongAggregates implements AggregatesInterface +{ + private int count = 0; + // private Long countDistinct; + private Long sum; + private Long min; + private Long max; + + + + /******************************************************************************* + ** Add a new value to this aggregate set + *******************************************************************************/ + public void add(Long input) + { + if(input == null) + { + return; + } + + count++; + + if(sum == null) + { + sum = input; + } + else + { + sum = sum + input; + } + + if(min == null || input < min) + { + min = input; + } + + if(max == null || input > max) + { + max = input; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int getCount() + { + return (count); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Long getSum() + { + return (sum); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Long getMin() + { + return (min); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Long getMax() + { + return (max); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public BigDecimal getAverage() + { + if(this.count > 0) + { + return (BigDecimal.valueOf(this.sum.doubleValue() / (double) this.count)); + } + else + { + return (null); + } + } + +} diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index 4aed4e14..ac33f347 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -154,7 +154,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface if("".equals(value)) { QFieldType type = field.getType(); - if(type.equals(QFieldType.INTEGER) || type.equals(QFieldType.DECIMAL) || type.equals(QFieldType.DATE) || type.equals(QFieldType.DATE_TIME) || type.equals(QFieldType.BOOLEAN)) + if(type.equals(QFieldType.INTEGER) || type.equals(QFieldType.LONG) || type.equals(QFieldType.DECIMAL) || type.equals(QFieldType.DATE) || type.equals(QFieldType.DATE_TIME) || type.equals(QFieldType.BOOLEAN)) { value = null; } @@ -875,6 +875,10 @@ public abstract class AbstractRDBMSAction implements QActionInterface { return (QueryManager.getInteger(resultSet, i)); } + case LONG: + { + return (QueryManager.getLong(resultSet, i)); + } case DECIMAL: { return (QueryManager.getBigDecimal(resultSet, i)); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java index ce720120..4e0d0fad 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java @@ -143,7 +143,7 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega QFieldType fieldType = aggregate.getFieldType(); if(fieldType == null) { - if(field.getType().equals(QFieldType.INTEGER) && (aggregate.getOperator().equals(AggregateOperator.AVG))) + if((field.getType().equals(QFieldType.INTEGER) || field.getType().equals(QFieldType.LONG)) && (aggregate.getOperator().equals(AggregateOperator.AVG))) { fieldType = QFieldType.DECIMAL; } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 53617063..2212b13e 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -1680,7 +1680,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction "string"; - case INTEGER -> "integer"; + case INTEGER, LONG -> "integer"; // todo - we could give 'format' w/ int32 & int64 to further specify case DECIMAL -> "number"; case BOOLEAN -> "boolean"; }; diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java index fe8955eb..2a52e8d7 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QCommandBuilder.java @@ -415,6 +415,7 @@ public class QCommandBuilder { case STRING, TEXT, HTML, PASSWORD -> String.class; case INTEGER -> Integer.class; + case LONG -> Long.class; case DECIMAL -> BigDecimal.class; case DATE -> LocalDate.class; case TIME -> LocalTime.class;