From 89a943bc2cad9f3c4774265a0c29d1cfe9a9694b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 1 Aug 2022 19:43:59 -0500 Subject: [PATCH] more entity->QRecord abilities; add TIME & BOOLEAN types --- .../core/instances/QInstanceEnricher.java | 109 +++++++++++++++ .../actions/tables/query/QFilterCriteria.java | 26 +++- .../qqq/backend/core/model/data/QField.java | 54 ++++++++ .../core/model/data/QRecordEntity.java | 43 +++++- .../core/model/data/QRecordEntityField.java | 76 +++++++--- .../model/metadata/fields/QFieldMetaData.java | 45 +++++- .../model/metadata/fields/QFieldType.java | 22 ++- .../model/metadata/tables/QTableMetaData.java | 31 +++++ .../qqq/backend/core/utils/ListingHash.java | 16 ++- .../qqq/backend/core/utils/ValueUtils.java | 119 ++++++++++++++-- .../actions/metadata/MetaDataActionTest.java | 14 +- .../processes/RunBackendStepActionTest.java | 3 +- .../core/instances/QInstanceEnricherTest.java | 65 ++++++++- .../core/model/data/QRecordEntityTest.java | 63 ++++++++- .../core/model/data/testentities/Item.java | 17 ++- .../qqq/backend/core/utils/TestUtils.java | 48 ++++--- .../backend/core/utils/ValueUtilsTest.java | 86 ++++++++++++ .../rdbms/actions/AbstractRDBMSAction.java | 16 ++- .../rdbms/actions/RDBMSQueryAction.java | 5 + .../module/rdbms/jdbc/QueryManager.java | 80 ++++++++++- .../module/rdbms/jdbc/QueryManagerTest.java | 131 +++++++++++++----- .../qqq/frontend/picocli/QCommandBuilder.java | 4 +- 22 files changed, 956 insertions(+), 117 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 569ce603..b69a1da9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -47,7 +47,10 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEd import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileStep; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertStoreRecordsStep; import com.kingsrook.qqq.backend.core.processes.implementations.general.LoadInitialRecordsStep; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -57,6 +60,10 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; *******************************************************************************/ public class QInstanceEnricher { + private static final Logger LOG = LogManager.getLogger(QInstanceEnricher.class); + + + /******************************************************************************* ** *******************************************************************************/ @@ -182,6 +189,16 @@ public class QInstanceEnricher return (null); } + if(name.length() == 0) + { + return (""); + } + + if(name.length() == 1) + { + return (name.substring(0, 1).toUpperCase(Locale.ROOT)); + } + return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z])", " $1")); } @@ -403,4 +420,96 @@ public class QInstanceEnricher ))); } + + + /******************************************************************************* + ** for all fields in a table, set their backendName, using the default "inference" logic + ** see {@link #inferBackendName(String)} + *******************************************************************************/ + public static void setInferredFieldBackendNames(QTableMetaData tableMetaData) + { + if(tableMetaData == null) + { + LOG.warn("Requested to infer field backend names with a null table as input. Returning with noop."); + return; + } + + if(CollectionUtils.nullSafeIsEmpty(tableMetaData.getFields())) + { + LOG.warn("Requested to infer field backend names on a table [" + tableMetaData.getName() + "] with no fields. Returning with noop."); + return; + } + + for(QFieldMetaData field : tableMetaData.getFields().values()) + { + String fieldName = field.getName(); + String fieldBackendName = field.getBackendName(); + if(!StringUtils.hasContent(fieldBackendName)) + { + String backendName = inferBackendName(fieldName); + field.setBackendName(backendName); + } + } + } + + + + /******************************************************************************* + ** Do a default mapping from a camelCase field name to an underscore_style + ** name for a backend. + *******************************************************************************/ + static String inferBackendName(String fieldName) + { + //////////////////////////////////////////////////////////////////////////////////////// + // build a list of words in the name, then join them with _ and lower-case the result // + //////////////////////////////////////////////////////////////////////////////////////// + List words = new ArrayList<>(); + StringBuilder currentWord = new StringBuilder(); + for(int i = 0; i < fieldName.length(); i++) + { + Character thisChar = fieldName.charAt(i); + Character nextChar = i < (fieldName.length() - 1) ? fieldName.charAt(i + 1) : null; + + ///////////////////////////////////////////////////////////////////////////////////// + // if we're at the end of the whole string, then we're at the end of the last word // + ///////////////////////////////////////////////////////////////////////////////////// + if(nextChar == null) + { + currentWord.append(thisChar); + words.add(currentWord.toString()); + } + + /////////////////////////////////////////////////////////// + // transitioning from a lower to an upper starts a word. // + /////////////////////////////////////////////////////////// + else if(Character.isLowerCase(thisChar) && Character.isUpperCase(nextChar)) + { + currentWord.append(thisChar); + words.add(currentWord.toString()); + currentWord = new StringBuilder(); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // transitioning from an upper to a lower - it starts a word, as long as there were already letters in the current word // + // e.g., on wordThenTLAInMiddle, when thisChar=I and nextChar=n. currentWord will be "TLA". So finish that word, and start a new one with the 'I' // + // but the normal single-upper condition, e.g., firstName, when thisChar=N and nextChar=a, current word will be empty string, so just append the 'a' to it // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + else if(Character.isUpperCase(thisChar) && Character.isLowerCase(nextChar) && currentWord.length() > 0) + { + words.add(currentWord.toString()); + currentWord = new StringBuilder(); + currentWord.append(thisChar); + } + + ///////////////////////////////////////////////////////////// + // by default, just add this character to the current word // + ///////////////////////////////////////////////////////////// + else + { + currentWord.append(thisChar); + } + } + + return (String.join("_", words).toLowerCase(Locale.ROOT)); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java index d4c5959b..251a961b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java @@ -32,12 +32,33 @@ import java.util.List; *******************************************************************************/ public class QFilterCriteria implements Serializable { - private String fieldName; - private QCriteriaOperator operator; + private String fieldName; + private QCriteriaOperator operator; private List values; + /******************************************************************************* + ** + *******************************************************************************/ + public QFilterCriteria() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QFilterCriteria(String fieldName, QCriteriaOperator operator, List values) + { + this.fieldName = fieldName; + this.operator = operator; + this.values = values; + } + + + /******************************************************************************* ** Getter for fieldName ** @@ -127,6 +148,7 @@ public class QFilterCriteria implements Serializable } + /******************************************************************************* ** Setter for values ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java new file mode 100644 index 00000000..cbdb9616 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java @@ -0,0 +1,54 @@ +/* + * 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.model.data; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; + + +/******************************************************************************* + ** + *******************************************************************************/ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface QField +{ + /******************************************************************************* + ** + *******************************************************************************/ + String backendName() default ""; + + /******************************************************************************* + ** + *******************************************************************************/ + boolean isRequired() default false; + + /******************************************************************************* + ** + *******************************************************************************/ + boolean isEditable() default true; +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index df5c8ecc..2db368ba 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -104,7 +108,7 @@ public abstract class QRecordEntity /******************************************************************************* ** *******************************************************************************/ - private static List getFieldList(Class c) + public static List getFieldList(Class c) { if(!fieldMapping.containsKey(c)) { @@ -114,10 +118,12 @@ public abstract class QRecordEntity if(isGetter(possibleGetter)) { Optional setter = getSetterForGetter(c, possibleGetter); + if(setter.isPresent()) { - String name = getFieldNameFromGetter(possibleGetter); - fieldList.add(new QRecordEntityField(name, possibleGetter, setter.get(), possibleGetter.getReturnType())); + String fieldName = getFieldNameFromGetter(possibleGetter); + Optional fieldAnnotation = getQFieldAnnotation(c, fieldName); + fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null))); } else { @@ -132,6 +138,27 @@ public abstract class QRecordEntity + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional getQFieldAnnotation(Class c, String fieldName) + { + try + { + Field field = c.getDeclaredField(fieldName); + return (Optional.ofNullable(field.getAnnotation(QField.class))); + } + catch(NoSuchFieldException e) + { + ////////////////////////////////////////// + // ok, we just won't have an annotation // + ////////////////////////////////////////// + } + return (Optional.empty()); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -207,7 +234,15 @@ public abstract class QRecordEntity || returnType.equals(int.class) || returnType.equals(Boolean.class) || returnType.equals(boolean.class) - || returnType.equals(BigDecimal.class)); + || returnType.equals(BigDecimal.class) + || returnType.equals(Instant.class) + || returnType.equals(LocalDate.class) + || returnType.equals(LocalTime.class)); + ///////////////////////////////////////////// + // note - this list has implications upon: // + // - QFieldType.fromClass // + // - QRecordEntityField.convertValueType // + ///////////////////////////////////////////// } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java index b03dbea6..f977f244 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java @@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; import java.lang.reflect.Method; import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -38,18 +40,20 @@ public class QRecordEntityField private final Method getter; private final Method setter; private final Class type; + private final QField fieldAnnotation; /******************************************************************************* ** Constructor. *******************************************************************************/ - public QRecordEntityField(String fieldName, Method getter, Method setter, Class type) + public QRecordEntityField(String fieldName, Method getter, Method setter, Class type, QField fieldAnnotation) { this.fieldName = fieldName; this.getter = getter; this.setter = setter; this.type = type; + this.fieldAnnotation = fieldAnnotation; } @@ -98,39 +102,67 @@ public class QRecordEntityField + /******************************************************************************* + ** Getter for fieldAnnotation + ** + *******************************************************************************/ + public QField getFieldAnnotation() + { + return fieldAnnotation; + } + + + /******************************************************************************* ** *******************************************************************************/ public Object convertValueType(Serializable value) { - if(value == null) + try { - return (null); - } + if(value == null) + { + return (null); + } - if(value.getClass().equals(type)) - { - return (value); - } + if(value.getClass().equals(type)) + { + return (value); + } - if(type.equals(String.class)) - { - return (ValueUtils.getValueAsString(value)); - } + if(type.equals(String.class)) + { + return (ValueUtils.getValueAsString(value)); + } - if(type.equals(Integer.class) || type.equals(int.class)) - { - return (ValueUtils.getValueAsInteger(value)); - } + if(type.equals(Integer.class) || type.equals(int.class)) + { + return (ValueUtils.getValueAsInteger(value)); + } - if(type.equals(Boolean.class) || type.equals(boolean.class)) - { - return (ValueUtils.getValueAsBoolean(value)); - } + if(type.equals(Boolean.class) || type.equals(boolean.class)) + { + return (ValueUtils.getValueAsBoolean(value)); + } - if(type.equals(BigDecimal.class)) + if(type.equals(BigDecimal.class)) + { + return (ValueUtils.getValueAsBigDecimal(value)); + } + + if(type.equals(LocalDate.class)) + { + return (ValueUtils.getValueAsLocalDate(value)); + } + + if(type.equals(Instant.class)) + { + return (ValueUtils.getValueAsInstant(value)); + } + } + catch(Exception e) { - return (ValueUtils.getValueAsBigDecimal(value)); + throw (new QValueException("Exception converting value [" + value + "] for field [" + fieldName + "]", e)); } throw (new QValueException("Unhandled value type [" + type + "] for field [" + fieldName + "]")); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index c5fb9e72..99c0fbf3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -24,9 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Optional; import com.github.hervian.reflection.Fun; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -77,12 +81,49 @@ public class QFieldMetaData ** e.g., new QFieldMetaData(Order::getOrderNo). *******************************************************************************/ public QFieldMetaData(Fun.With1ParamAndVoid getterRef) throws QException + { + Method getter = Fun.toMethod(getterRef); + constructFromGetter(getter); + } + + + + /******************************************************************************* + ** Initialize a fieldMetaData from a getter method from an entity + ** + *******************************************************************************/ + public QFieldMetaData(Method getter) throws QException + { + constructFromGetter(getter); + } + + + + /******************************************************************************* + ** From a getter method, populate attributes in this field meta-data, including + ** those from the @QField annotation on the field in the class, if present. + *******************************************************************************/ + private void constructFromGetter(Method getter) throws QException { try { - Method getter = Fun.toMethod(getterRef); this.name = QRecordEntity.getFieldNameFromGetter(getter); this.type = QFieldType.fromClass(getter.getReturnType()); + + @SuppressWarnings("unchecked") + Optional optionalFieldAnnotation = QRecordEntity.getQFieldAnnotation((Class) getter.getDeclaringClass(), this.name); + + if(optionalFieldAnnotation.isPresent()) + { + QField fieldAnnotation = optionalFieldAnnotation.get(); + setIsRequired(fieldAnnotation.isRequired()); + setIsEditable(fieldAnnotation.isEditable()); + + if(StringUtils.hasContent(fieldAnnotation.backendName())) + { + setBackendName(fieldAnnotation.backendName()); + } + } } catch(QException qe) { @@ -90,7 +131,7 @@ public class QFieldMetaData } catch(Exception e) { - throw (new QException("Error constructing field from getterRef: " + getterRef, e)); + throw (new QException("Error constructing field from getter method: " + getter.getName(), e)); } } 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 324e8991..41ffdcc4 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 @@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -35,8 +38,9 @@ public enum QFieldType STRING, INTEGER, DECIMAL, + BOOLEAN, DATE, - // TIME, + TIME, DATE_TIME, TEXT, HTML, @@ -65,6 +69,22 @@ public enum QFieldType { return (DECIMAL); } + if(c.equals(Instant.class)) + { + return (DATE_TIME); + } + if(c.equals(LocalDate.class)) + { + return (DATE); + } + if(c.equals(LocalTime.class)) + { + return (TIME); + } + if(c.equals(Boolean.class)) + { + return (BOOLEAN); + } throw (new QException("Unrecognized class [" + c + "]")); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index aed8f51c..49695979 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -28,6 +28,10 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -91,6 +95,22 @@ public class QTableMetaData implements Serializable + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData withFieldsFromEntity(Class entityClass) throws QException + { + List recordEntityFieldList = QRecordEntity.getFieldList(entityClass); + for(QRecordEntityField recordEntityField : recordEntityFieldList) + { + QFieldMetaData field = new QFieldMetaData(recordEntityField.getGetter()); + addField(field); + } + return (this); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -434,4 +454,15 @@ public class QTableMetaData implements Serializable return (this); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData withInferredFieldBackendNames() + { + QInstanceEnricher.setInferredFieldBackendNames(this); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ListingHash.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ListingHash.java index 58eefe68..ebb35e2a 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ListingHash.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ListingHash.java @@ -41,7 +41,7 @@ public class ListingHash implements Map>, Serializable { public static final long serialVersionUID = 0L; - private HashMap> hashMap = null; + private Map> hashMap = null; @@ -51,7 +51,19 @@ public class ListingHash implements Map>, Serializable *******************************************************************************/ public ListingHash() { - this.hashMap = new HashMap>(); + this.hashMap = new HashMap<>(); + } + + + + /******************************************************************************* + ** Constructor where you can supply a source map (e.g., if you want a specific + ** Map type (like LinkedHashMap), or with pre-values + ** + *******************************************************************************/ + public ListingHash(Map> sourceMap) + { + this.hashMap = sourceMap; } 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 652273fa..d839a36b 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 @@ -23,11 +23,14 @@ package com.kingsrook.qqq.backend.core.utils; import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Calendar; +import java.util.List; import java.util.TimeZone; import com.kingsrook.qqq.backend.core.exceptions.QValueException; @@ -37,7 +40,8 @@ import com.kingsrook.qqq.backend.core.exceptions.QValueException; *******************************************************************************/ public class ValueUtils { - private static final DateTimeFormatter localDateDefaultFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter yyyyMMddWithDashesFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter MdyyyyWithSlashesFormatter = DateTimeFormatter.ofPattern("M/d/yyyy"); @@ -174,9 +178,13 @@ public class ValueUtils } else { - throw (new IllegalArgumentException("Unsupported class " + value.getClass().getName() + " for converting to Integer.")); + throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to Integer.")); } } + catch(QValueException qve) + { + throw (qve); + } catch(Exception e) { throw (new QValueException("Value [" + value + "] could not be converted to an Integer.", e)); @@ -212,8 +220,8 @@ public class ValueUtils } else if(value instanceof Calendar c) { - TimeZone tz = c.getTimeZone(); - ZoneId zid = (tz == null) ? ZoneId.systemDefault() : tz.toZoneId(); + TimeZone tz = c.getTimeZone(); + ZoneId zid = (tz == null) ? ZoneId.systemDefault() : tz.toZoneId(); return LocalDateTime.ofInstant(c.toInstant(), zid).toLocalDate(); } else if(value instanceof LocalDateTime ldt) @@ -227,21 +235,47 @@ public class ValueUtils return (null); } - return LocalDate.parse(s, localDateDefaultFormatter); + return tryLocalDateParsers(s); } else { - throw (new IllegalArgumentException("Unsupported class " + value.getClass().getName() + " for converting to LocalDate.")); + throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to LocalDate.")); } } + catch(QValueException qve) + { + throw (qve); + } catch(Exception e) { - throw (new QValueException("Value [" + value + "] could not be converted to an LocalDate.", e)); + throw (new QValueException("Value [" + value + "] could not be converted to a LocalDate.", e)); } } + /******************************************************************************* + ** + *******************************************************************************/ + private static LocalDate tryLocalDateParsers(String s) + { + DateTimeParseException lastException = null; + for(DateTimeFormatter dateTimeFormatter : List.of(yyyyMMddWithDashesFormatter, MdyyyyWithSlashesFormatter)) + { + try + { + return LocalDate.parse(s, dateTimeFormatter); + } + catch(DateTimeParseException dtpe) + { + lastException = dtpe; + } + } + throw (new QValueException("Could not parse value [" + s + "] to a local date", lastException)); + } + + + /******************************************************************************* ** Type-safely make a BigDecimal from any Object. ** null and empty-string inputs return null. @@ -305,13 +339,80 @@ public class ValueUtils } else { - throw (new IllegalArgumentException("Unsupported class " + value.getClass().getName() + " for converting to BigDecimal.")); + throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to BigDecimal.")); } } + catch(QValueException qve) + { + throw (qve); + } catch(Exception e) { - throw (new QValueException("Value [" + value + "] could not be converted to an BigDecimal.", e)); + throw (new QValueException("Value [" + value + "] could not be converted to a BigDecimal.", e)); } } + + + /******************************************************************************* + ** Type-safely make an Instant from any Object. + ** null and empty-string inputs return null. + ** We may throw if the input can't be converted to a Instant + *******************************************************************************/ + public static Instant getValueAsInstant(Object value) + { + try + { + if(value == null) + { + return (null); + } + else if(value instanceof Instant i) + { + return (i); + } + else if(value instanceof java.sql.Date d) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // note - in the jdk, this method throws UnsupportedOperationException (because of the lack of time in sql Dates) // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + return d.toInstant(); + } + else if(value instanceof java.util.Date d) + { + return d.toInstant(); + } + else if(value instanceof Calendar c) + { + TimeZone tz = c.getTimeZone(); + return (c.toInstant()); + } + else if(value instanceof LocalDateTime ldt) + { + ZoneId zoneId = ZoneId.systemDefault(); + return ldt.toInstant(zoneId.getRules().getOffset(ldt)); + } + else if(value instanceof String s) + { + if(!StringUtils.hasContent(s)) + { + return (null); + } + + return Instant.parse(s); + } + else + { + throw (new QValueException("Unsupported class " + value.getClass().getName() + " for converting to Instant.")); + } + } + catch(QValueException qve) + { + throw (qve); + } + catch(Exception e) + { + throw (new QValueException("Value [" + value + "] could not be converted to a Instant.", e)); + } + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java index f41b7012..f66f81ea 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java @@ -22,12 +22,15 @@ package com.kingsrook.qqq.backend.core.actions.metadata; +import java.util.Set; +import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; /******************************************************************************* @@ -47,9 +50,18 @@ class MetaDataActionTest request.setSession(TestUtils.getMockSession()); MetaDataOutput result = new MetaDataAction().execute(request); assertNotNull(result); + assertNotNull(result.getTables()); assertNotNull(result.getTables().get("person")); assertEquals("person", result.getTables().get("person").getName()); assertEquals("Person", result.getTables().get("person").getLabel()); + + assertNotNull(result.getProcesses().get("greet")); + assertNotNull(result.getProcesses().get("greetInteractive")); + assertNotNull(result.getProcesses().get("etl.basic")); + assertNotNull(result.getProcesses().get("person.bulkInsert")); + assertNotNull(result.getProcesses().get("person.bulkEdit")); + assertNotNull(result.getProcesses().get("person.bulkDelete")); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java index 3b88e502..c1d308dc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepActionTest.java @@ -103,7 +103,8 @@ public class RunBackendStepActionTest case STRING -> "ABC"; case INTEGER -> 42; case DECIMAL -> new BigDecimal("47"); - case DATE, DATE_TIME -> null; + case BOOLEAN -> true; + case DATE, TIME, DATE_TIME -> null; case TEXT -> """ ABC XYZ"""; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java index 0e6ef951..f9017298 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricherTest.java @@ -22,8 +22,11 @@ package com.kingsrook.qqq.backend.core.instances; +import java.util.Collections; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; @@ -45,7 +48,7 @@ class QInstanceEnricherTest @Test public void test_nullTableLabelComesFromName() { - QInstance qInstance = TestUtils.defineInstance(); + QInstance qInstance = TestUtils.defineInstance(); QTableMetaData personTable = qInstance.getTable("person"); personTable.setLabel(null); assertNull(personTable.getLabel()); @@ -54,6 +57,7 @@ class QInstanceEnricherTest } + /******************************************************************************* ** Test that a table missing a label and a name doesn't NPE, but just keeps ** the name & label both null. @@ -62,7 +66,7 @@ class QInstanceEnricherTest @Test public void test_nullNameGivesNullLabel() { - QInstance qInstance = TestUtils.defineInstance(); + QInstance qInstance = TestUtils.defineInstance(); QTableMetaData personTable = qInstance.getTable("person"); personTable.setLabel(null); personTable.setName(null); @@ -74,6 +78,7 @@ class QInstanceEnricherTest } + /******************************************************************************* ** Test that a field missing a label gets the default label applied (name w/ UC-first) ** @@ -81,12 +86,64 @@ class QInstanceEnricherTest @Test public void test_nullFieldLabelComesFromName() { - QInstance qInstance = TestUtils.defineInstance(); - QFieldMetaData idField = qInstance.getTable("person").getField("id"); + QInstance qInstance = TestUtils.defineInstance(); + QFieldMetaData idField = qInstance.getTable("person").getField("id"); idField.setLabel(null); assertNull(idField.getLabel()); new QInstanceEnricher().enrich(qInstance); assertEquals("Id", idField.getLabel()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetInferredFieldBackendNames() + { + QTableMetaData table = new QTableMetaData() + .withField(new QFieldMetaData("id", QFieldType.INTEGER)) + .withField(new QFieldMetaData("firstName", QFieldType.INTEGER)) + .withField(new QFieldMetaData("nonstandard", QFieldType.INTEGER).withBackendName("whateverImNon_standard")); + QInstanceEnricher.setInferredFieldBackendNames(table); + assertEquals("id", table.getField("id").getBackendName()); + assertEquals("first_name", table.getField("firstName").getBackendName()); + assertEquals("whateverImNon_standard", table.getField("nonstandard").getBackendName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetInferredFieldBackendNamesEdgeCases() + { + /////////////////////////////////////////////////////////////// + // make sure none of these cases throw (but all should warn) // + /////////////////////////////////////////////////////////////// + QInstanceEnricher.setInferredFieldBackendNames(null); + QInstanceEnricher.setInferredFieldBackendNames(new QTableMetaData()); + QInstanceEnricher.setInferredFieldBackendNames(new QTableMetaData().withFields(Collections.emptyMap())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInferBackendName() + { + assertEquals("id", QInstanceEnricher.inferBackendName("id")); + assertEquals("word_another_word_more_words", QInstanceEnricher.inferBackendName("wordAnotherWordMoreWords")); + assertEquals("l_ul_ul_ul", QInstanceEnricher.inferBackendName("lUlUlUl")); + assertEquals("starts_upper", QInstanceEnricher.inferBackendName("StartsUpper")); + assertEquals("tla_first", QInstanceEnricher.inferBackendName("TLAFirst")); + assertEquals("word_then_tla_in_middle", QInstanceEnricher.inferBackendName("wordThenTLAInMiddle")); + assertEquals("end_with_tla", QInstanceEnricher.inferBackendName("endWithTLA")); + assertEquals("tla_and_another_tla", QInstanceEnricher.inferBackendName("TLAAndAnotherTLA")); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java index e8f8e9e3..be3cd897 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java @@ -162,15 +162,74 @@ class QRecordEntityTest *******************************************************************************/ @SuppressWarnings("ResultOfMethodCallIgnored") @Test - void testQTableConstructionFromEntity() throws QException + void testQTableConstructionFromEntityGetterReferences() throws QException { QTableMetaData qTableMetaData = new QTableMetaData() .withField(new QFieldMetaData(Item::getSku)) .withField(new QFieldMetaData(Item::getDescription)) - .withField(new QFieldMetaData(Item::getQuantity)); + .withField(new QFieldMetaData(Item::getQuantity)) + .withField(new QFieldMetaData(Item::getFeatured)) + .withField(new QFieldMetaData(Item::getPrice)); assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType()); assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType()); + + /////////////////////////////////////////////////////////////// + // assert about attributes that came from @QField annotation // + /////////////////////////////////////////////////////////////// + assertTrue(qTableMetaData.getField("sku").getIsRequired()); + assertFalse(qTableMetaData.getField("quantity").getIsEditable()); + assertEquals("is_featured", qTableMetaData.getField("featured").getBackendName()); + + ////////////////////////////////////////////////////////////////////////// + // assert about attributes that weren't specified in @QField annotation // + ////////////////////////////////////////////////////////////////////////// + assertTrue(qTableMetaData.getField("sku").getIsEditable()); + assertFalse(qTableMetaData.getField("quantity").getIsRequired()); + assertNull(qTableMetaData.getField("sku").getBackendName()); + + ///////////////////////////////////////////////////////////////////// + // assert about attributes for fields without a @QField annotation // + ///////////////////////////////////////////////////////////////////// + assertTrue(qTableMetaData.getField("price").getIsEditable()); + assertFalse(qTableMetaData.getField("price").getIsRequired()); + assertNull(qTableMetaData.getField("price").getBackendName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQTableConstructionFromEntity() throws QException + { + QTableMetaData qTableMetaData = new QTableMetaData() + .withFieldsFromEntity(Item.class); + + assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType()); + assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType()); + + /////////////////////////////////////////////////////////////// + // assert about attributes that came from @QField annotation // + /////////////////////////////////////////////////////////////// + assertTrue(qTableMetaData.getField("sku").getIsRequired()); + assertFalse(qTableMetaData.getField("quantity").getIsEditable()); + assertEquals("is_featured", qTableMetaData.getField("featured").getBackendName()); + + ////////////////////////////////////////////////////////////////////////// + // assert about attributes that weren't specified in @QField annotation // + ////////////////////////////////////////////////////////////////////////// + assertTrue(qTableMetaData.getField("sku").getIsEditable()); + assertFalse(qTableMetaData.getField("quantity").getIsRequired()); + assertNull(qTableMetaData.getField("sku").getBackendName()); + + ///////////////////////////////////////////////////////////////////// + // assert about attributes for fields without a @QField annotation // + ///////////////////////////////////////////////////////////////////// + assertTrue(qTableMetaData.getField("price").getIsEditable()); + assertFalse(qTableMetaData.getField("price").getIsRequired()); + assertNull(qTableMetaData.getField("price").getBackendName()); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Item.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Item.java index 4412122b..86653880 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Item.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Item.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.data.testentities; import java.math.BigDecimal; +import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; @@ -31,11 +32,19 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; *******************************************************************************/ public class Item extends QRecordEntity { - private String sku; - private String description; - private Integer quantity; + @QField(isRequired = true) + private String sku; + + @QField() + private String description; + + @QField(isEditable = false) + private Integer quantity; + private BigDecimal price; - private Boolean featured; + + @QField(backendName = "is_featured") + private Boolean featured; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 6bae6380..d7f6b5d0 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -32,16 +32,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; -import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; @@ -50,10 +47,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; /******************************************************************************* @@ -62,9 +62,15 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicE *******************************************************************************/ public class TestUtils { - public static String DEFAULT_BACKEND_NAME = "default"; - public static String PROCESS_NAME_GREET_PEOPLE = "greet"; - public static String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; + public static final String DEFAULT_BACKEND_NAME = "default"; + + public static final String TABLE_NAME_PERSON = "person"; + + public static final String PROCESS_NAME_GREET_PEOPLE = "greet"; + public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; + public static final String PROCESS_NAME_ADD_TO_PEOPLES_AGE = "addToPeoplesAge"; + public static final String TABLE_NAME_PERSON_FILE = "personFile"; + public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; @@ -138,7 +144,7 @@ public class TestUtils public static QTableMetaData defineTablePerson() { return new QTableMetaData() - .withName("person") + .withName(TABLE_NAME_PERSON) .withLabel("Person") .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") @@ -160,7 +166,7 @@ public class TestUtils public static QTableMetaData definePersonFileTable() { return (new QTableMetaData() - .withName("personFile") + .withName(TABLE_NAME_PERSON_FILE) .withLabel("Person File") .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") @@ -175,7 +181,7 @@ public class TestUtils public static QTableMetaData defineTableIdAndNameOnly() { return new QTableMetaData() - .withName("idAndNameOnly") + .withName(TABLE_NAME_ID_AND_NAME_ONLY) .withLabel("Id and Name Only") .withBackendName(DEFAULT_BACKEND_NAME) .withPrimaryKeyField("id") @@ -192,7 +198,7 @@ public class TestUtils { return new QProcessMetaData() .withName(PROCESS_NAME_GREET_PEOPLE) - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addStep(new QBackendStepMetaData() .withName("prepare") .withCode(new QCodeReference() @@ -200,14 +206,14 @@ public class TestUtils .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() - .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON)) .withFieldList(List.of( new QFieldMetaData("greetingPrefix", QFieldType.STRING), new QFieldMetaData("greetingSuffix", QFieldType.STRING) ))) .withOutputMetaData(new QFunctionOutputMetaData() .withRecordListMetaData(new QRecordListMetaData() - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) @@ -223,7 +229,7 @@ public class TestUtils { return new QProcessMetaData() .withName(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE) - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addStep(new QFrontendStepMetaData() .withName("setup") @@ -238,14 +244,14 @@ public class TestUtils .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context? .withInputData(new QFunctionInputMetaData() - .withRecordListMetaData(new QRecordListMetaData().withTableName("person")) + .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON)) .withFieldList(List.of( new QFieldMetaData("greetingPrefix", QFieldType.STRING), new QFieldMetaData("greetingSuffix", QFieldType.STRING) ))) .withOutputMetaData(new QFunctionOutputMetaData() .withRecordListMetaData(new QRecordListMetaData() - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addField(new QFieldMetaData("fullGreeting", QFieldType.STRING)) ) .withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING)))) @@ -270,8 +276,8 @@ public class TestUtils private static QProcessMetaData defineProcessAddToPeoplesAge() { return new QProcessMetaData() - .withName("addToPeoplesAge") - .withTableName("person") + .withName(PROCESS_NAME_ADD_TO_PEOPLES_AGE) + .withTableName(TABLE_NAME_PERSON) .addStep(new QBackendStepMetaData() .withName("getAgeStatistics") .withCode(new QCodeReference() @@ -279,10 +285,10 @@ public class TestUtils .withCodeType(QCodeType.JAVA) .withCodeUsage(QCodeUsage.BACKEND_STEP)) .withInputData(new QFunctionInputMetaData() - .withRecordListMetaData(new QRecordListMetaData().withTableName("person"))) + .withRecordListMetaData(new QRecordListMetaData().withTableName(TABLE_NAME_PERSON))) .withOutputMetaData(new QFunctionOutputMetaData() .withRecordListMetaData(new QRecordListMetaData() - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addField(new QFieldMetaData("age", QFieldType.INTEGER))) .withFieldList(List.of( new QFieldMetaData("minAge", QFieldType.INTEGER), @@ -297,7 +303,7 @@ public class TestUtils .withFieldList(List.of(new QFieldMetaData("yearsToAdd", QFieldType.INTEGER)))) .withOutputMetaData(new QFunctionOutputMetaData() .withRecordListMetaData(new QRecordListMetaData() - .withTableName("person") + .withTableName(TABLE_NAME_PERSON) .addField(new QFieldMetaData("newAge", QFieldType.INTEGER))))); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java index a422338a..abb4afbc 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/ValueUtilsTest.java @@ -24,8 +24,16 @@ package com.kingsrook.qqq.backend.core.utils; import java.math.BigDecimal; import java.math.MathContext; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; +import java.util.Calendar; +import java.util.GregorianCalendar; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -41,6 +49,7 @@ class ValueUtilsTest @Test void testGetValueAsString() throws QValueException { + //noinspection ConstantConditions assertNull(ValueUtils.getValueAsString(null)); assertEquals("", ValueUtils.getValueAsString("")); assertEquals(" ", ValueUtils.getValueAsString(" ")); @@ -132,7 +141,9 @@ class ValueUtilsTest assertEquals(new BigDecimal("1"), ValueUtils.getValueAsBigDecimal(1.0F)); assertEquals(new BigDecimal("1"), ValueUtils.getValueAsBigDecimal(1.0D)); assertEquals(new BigDecimal("1000000000000"), ValueUtils.getValueAsBigDecimal(1_000_000_000_000L)); + //noinspection ConstantConditions assertEquals(0, new BigDecimal("1.1").compareTo(ValueUtils.getValueAsBigDecimal(1.1F).round(MathContext.DECIMAL32))); + //noinspection ConstantConditions assertEquals(0, new BigDecimal("1.1").compareTo(ValueUtils.getValueAsBigDecimal(1.1D).round(MathContext.DECIMAL64))); assertThrows(QValueException.class, () -> ValueUtils.getValueAsBigDecimal("a")); @@ -140,4 +151,79 @@ class ValueUtilsTest assertThrows(QValueException.class, () -> ValueUtils.getValueAsBigDecimal(new Object())); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("deprecation") + @Test + void testGetValueAsLocalDate() throws QValueException + { + assertNull(ValueUtils.getValueAsLocalDate(null)); + assertNull(ValueUtils.getValueAsLocalDate("")); + assertNull(ValueUtils.getValueAsLocalDate(" ")); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(LocalDate.of(1980, 5, 31))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new java.sql.Date(80, 4, 31))); + //noinspection MagicConstant + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new java.util.Date(80, 4, 31))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new java.util.Date(80, Calendar.MAY, 31))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new java.util.Date(80, Calendar.MAY, 31, 12, 0))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new java.util.Date(80, Calendar.MAY, 31, 4, 0))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new java.util.Date(80, Calendar.MAY, 31, 22, 0))); + //noinspection MagicConstant + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new GregorianCalendar(1980, 4, 31))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(new GregorianCalendar(1980, Calendar.MAY, 31))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(LocalDateTime.of(1980, 5, 31, 12, 0))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(LocalDateTime.of(1980, 5, 31, 4, 0))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(LocalDateTime.of(1980, 5, 31, 22, 0))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate(LocalDateTime.of(1980, Month.MAY, 31, 12, 0))); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate("1980-05-31")); + assertEquals(LocalDate.of(1980, Month.MAY, 31), ValueUtils.getValueAsLocalDate("05/31/1980")); + + assertThrows(QValueException.class, () -> ValueUtils.getValueAsLocalDate("a")); + assertThrows(QValueException.class, () -> ValueUtils.getValueAsLocalDate("a,b")); + assertThat(assertThrows(QValueException.class, () -> ValueUtils.getValueAsLocalDate("1980/05/31")).getMessage()).contains("parse"); + assertThat(assertThrows(QValueException.class, () -> ValueUtils.getValueAsLocalDate(new Object())).getMessage()).contains("Unsupported class"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("deprecation") + @Test + void testGetValueAsInstant() throws QValueException + { + Instant expected = Instant.parse("1980-05-31T12:30:00Z"); + + assertNull(ValueUtils.getValueAsInstant(null)); + assertNull(ValueUtils.getValueAsInstant("")); + assertNull(ValueUtils.getValueAsInstant(" ")); + assertEquals(expected, ValueUtils.getValueAsInstant(expected)); + assertEquals(expected, ValueUtils.getValueAsInstant("1980-05-31T12:30:00Z")); + + //////////////////////////// + // todo - time zone logic // + //////////////////////////// + // //noinspection MagicConstant + // assertEquals(expected, ValueUtils.getValueAsInstant(new java.util.Date(80, 4, 31, 7, 30))); + + // //noinspection MagicConstant + // assertEquals(expected, ValueUtils.getValueAsInstant(new GregorianCalendar(1980, 4, 31))); + // assertEquals(expected, ValueUtils.getValueAsInstant(new GregorianCalendar(1980, Calendar.MAY, 31))); + // // assertEquals(expected, ValueUtils.getValueAsInstant(InstantTime.of(1980, 5, 31, 12, 0))); + // // assertEquals(expected, ValueUtils.getValueAsInstant(InstantTime.of(1980, 5, 31, 4, 0))); + // // assertEquals(expected, ValueUtils.getValueAsInstant(InstantTime.of(1980, 5, 31, 22, 0))); + // // assertEquals(expected, ValueUtils.getValueAsInstant(InstantTime.of(1980, Month.MAY, 31, 12, 0))); + + assertThrows(QValueException.class, () -> ValueUtils.getValueAsInstant(new java.sql.Date(80, 4, 31))); + assertThrows(QValueException.class, () -> ValueUtils.getValueAsInstant("a")); + assertThrows(QValueException.class, () -> ValueUtils.getValueAsInstant("a,b")); + assertThrows(QValueException.class, () -> ValueUtils.getValueAsInstant("1980/05/31")); + assertThat(assertThrows(QValueException.class, () -> ValueUtils.getValueAsInstant(new Object())).getMessage()).contains("Unsupported class"); + } + + } \ No newline at end of file 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 9a9f1c1a..feafe6c9 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 @@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; @@ -94,7 +95,8 @@ public abstract class AbstractRDBMSAction /******************************************************************************* - ** Handle obvious problems with values - like empty string for integer should be null. + ** Handle obvious problems with values - like empty string for integer should be null, + ** and type conversions that we can do "better" than jdbc... ** *******************************************************************************/ protected Serializable scrubValue(QFieldMetaData field, Serializable value, boolean isInsert) @@ -108,6 +110,18 @@ public abstract class AbstractRDBMSAction } } + ////////////////////////////////////////////////////////////////////////////// + // value utils is good at making values from strings - jdbc, not as much... // + ////////////////////////////////////////////////////////////////////////////// + if(field.getType().equals(QFieldType.DATE) && value instanceof String) + { + value = ValueUtils.getValueAsLocalDate(value); + } + else if(field.getType().equals(QFieldType.DECIMAL) && value instanceof String) + { + value = ValueUtils.getValueAsBigDecimal(value); + } + return (value); } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 5c8361fb..02718222 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -189,8 +189,13 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf } case DATE: { + // todo - queryManager.getLocalDate? return (QueryManager.getDate(resultSet, i)); } + case TIME: + { + return (QueryManager.getLocalTime(resultSet, i)); + } case DATE_TIME: { return (QueryManager.getLocalDateTime(resultSet, i)); diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index fbd634e5..be854f39 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -35,6 +35,7 @@ import java.sql.Types; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; @@ -47,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.commons.lang.NotImplementedException; @@ -121,7 +123,10 @@ public class QueryManager statement.execute(); resultSet = statement.getResultSet(); - processor.processResultSet(resultSet); + if(processor != null) + { + processor.processResultSet(resultSet); + } } finally { @@ -743,10 +748,14 @@ public class QueryManager } else if(value instanceof LocalDate ld) { - ZoneOffset offset = OffsetDateTime.now().getOffset(); - long epochMillis = ld.atStartOfDay().toEpochSecond(offset) * MS_PER_SEC; - Timestamp timestamp = new Timestamp(epochMillis); - statement.setTimestamp(index, timestamp); + java.sql.Date date = new java.sql.Date(ld.getYear() - 1900, ld.getMonthValue() - 1, ld.getDayOfMonth()); + statement.setDate(index, date); + return (1); + } + else if(value instanceof LocalTime lt) + { + java.sql.Time time = new java.sql.Time(lt.getHour(), lt.getMinute(), lt.getSecond()); + statement.setTime(index, time); return (1); } else if(value instanceof OffsetDateTime odt) @@ -1199,6 +1208,67 @@ public class QueryManager + /******************************************************************************* + ** + *******************************************************************************/ + public static LocalTime getLocalTime(ResultSet resultSet, int column) throws SQLException + { + String timeString = resultSet.getString(column); + if(resultSet.wasNull()) + { + return (null); + } + return stringToLocalTime(timeString); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static LocalTime getLocalTime(ResultSet resultSet, String column) throws SQLException + { + String timeString = resultSet.getString(column); + if(resultSet.wasNull()) + { + return (null); + } + return stringToLocalTime(timeString); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static LocalTime stringToLocalTime(String timeString) throws SQLException + { + if(!StringUtils.hasContent(timeString)) + { + return (null); + } + + String[] parts = timeString.split(":"); + if(parts.length == 1) + { + return LocalTime.of(Integer.parseInt(parts[0]), 0); + } + if(parts.length == 2) + { + return LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])); + } + else if(parts.length == 3) + { + return LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); + } + else + { + throw (new SQLException("Unable to parse time value [" + timeString + "] to LocalTime")); + } + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index 94978f2a..e62ac2ae 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -32,6 +32,7 @@ import java.sql.SQLException; import java.sql.Timestamp; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.Month; import java.time.OffsetDateTime; import java.util.GregorianCalendar; @@ -59,7 +60,16 @@ class QueryManagerTest void beforeEach() throws SQLException { Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "CREATE TABLE t (i INTEGER, dt DATETIME, c CHAR(1), d DATE)"); + QueryManager.executeUpdate(connection, """ + CREATE TABLE test_table + ( + int_col INTEGER, + datetime_col DATETIME, + char_col CHAR(1), + date_col DATE, + time_col TIME + ) + """); } @@ -71,7 +81,7 @@ class QueryManagerTest void afterEach() throws SQLException { Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "DROP TABLE t"); + QueryManager.executeUpdate(connection, "DROP TABLE test_table"); } @@ -95,7 +105,7 @@ class QueryManagerTest { long ctMillis = System.currentTimeMillis(); Connection connection = getConnection(); - PreparedStatement ps = connection.prepareStatement("UPDATE t SET i = ? WHERE i > 0"); + PreparedStatement ps = connection.prepareStatement("UPDATE test_table SET int_col = ? WHERE int_col > 0"); /////////////////////////////////////////////////////////////////////////////// // these calls - we just want to assert that they don't throw any exceptions // @@ -149,37 +159,37 @@ class QueryManagerTest void testGetValueMethods() throws SQLException { Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO t (i, dt, c) VALUES (1, now(), 'A')"); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from t"); + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col) VALUES (1, now(), 'A')"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from test_table"); preparedStatement.execute(); ResultSet rs = preparedStatement.getResultSet(); rs.next(); - assertEquals(1, QueryManager.getInteger(rs, "i")); + assertEquals(1, QueryManager.getInteger(rs, "int_col")); assertEquals(1, QueryManager.getInteger(rs, 1)); - assertEquals(1L, QueryManager.getLong(rs, "i")); + assertEquals(1L, QueryManager.getLong(rs, "int_col")); assertEquals(1L, QueryManager.getLong(rs, 1)); - assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, "i")); + assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, "int_col")); assertArrayEquals(new byte[] { 0, 0, 0, 1 }, QueryManager.getByteArray(rs, 1)); - assertEquals(1, QueryManager.getObject(rs, "i")); + assertEquals(1, QueryManager.getObject(rs, "int_col")); assertEquals(1, QueryManager.getObject(rs, 1)); - assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, "i")); + assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, "int_col")); assertEquals(BigDecimal.ONE, QueryManager.getBigDecimal(rs, 1)); - assertEquals(true, QueryManager.getBoolean(rs, "i")); + assertEquals(true, QueryManager.getBoolean(rs, "int_col")); assertEquals(true, QueryManager.getBoolean(rs, 1)); - assertNotNull(QueryManager.getDate(rs, "dt")); + assertNotNull(QueryManager.getDate(rs, "datetime_col")); assertNotNull(QueryManager.getDate(rs, 2)); - assertNotNull(QueryManager.getCalendar(rs, "dt")); + assertNotNull(QueryManager.getCalendar(rs, "datetime_col")); assertNotNull(QueryManager.getCalendar(rs, 2)); - assertNotNull(QueryManager.getLocalDate(rs, "dt")); + assertNotNull(QueryManager.getLocalDate(rs, "datetime_col")); assertNotNull(QueryManager.getLocalDate(rs, 2)); - assertNotNull(QueryManager.getLocalDateTime(rs, "dt")); + assertNotNull(QueryManager.getLocalDateTime(rs, "datetime_col")); assertNotNull(QueryManager.getLocalDateTime(rs, 2)); - assertNotNull(QueryManager.getOffsetDateTime(rs, "dt")); + assertNotNull(QueryManager.getOffsetDateTime(rs, "datetime_col")); assertNotNull(QueryManager.getOffsetDateTime(rs, 2)); - assertNotNull(QueryManager.getTimestamp(rs, "dt")); + assertNotNull(QueryManager.getTimestamp(rs, "datetime_col")); assertNotNull(QueryManager.getTimestamp(rs, 2)); - assertEquals("A", QueryManager.getObject(rs, "c")); + assertEquals("A", QueryManager.getObject(rs, "char_col")); assertEquals("A", QueryManager.getObject(rs, 3)); } @@ -192,37 +202,37 @@ class QueryManagerTest void testGetValueMethodsReturningNull() throws SQLException { Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO t (i, dt, c) VALUES (null, null, null)"); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from t"); + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, datetime_col, char_col) VALUES (null, null, null)"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT * from test_table"); preparedStatement.execute(); ResultSet rs = preparedStatement.getResultSet(); rs.next(); - assertNull(QueryManager.getInteger(rs, "i")); + assertNull(QueryManager.getInteger(rs, "int_col")); assertNull(QueryManager.getInteger(rs, 1)); - assertNull(QueryManager.getLong(rs, "i")); + assertNull(QueryManager.getLong(rs, "int_col")); assertNull(QueryManager.getLong(rs, 1)); - assertNull(QueryManager.getByteArray(rs, "i")); + assertNull(QueryManager.getByteArray(rs, "int_col")); assertNull(QueryManager.getByteArray(rs, 1)); - assertNull(QueryManager.getObject(rs, "i")); + assertNull(QueryManager.getObject(rs, "int_col")); assertNull(QueryManager.getObject(rs, 1)); - assertNull(QueryManager.getBigDecimal(rs, "i")); + assertNull(QueryManager.getBigDecimal(rs, "int_col")); assertNull(QueryManager.getBigDecimal(rs, 1)); - assertNull(QueryManager.getBoolean(rs, "i")); + assertNull(QueryManager.getBoolean(rs, "int_col")); assertNull(QueryManager.getBoolean(rs, 1)); - assertNull(QueryManager.getDate(rs, "dt")); + assertNull(QueryManager.getDate(rs, "datetime_col")); assertNull(QueryManager.getDate(rs, 2)); - assertNull(QueryManager.getCalendar(rs, "dt")); + assertNull(QueryManager.getCalendar(rs, "datetime_col")); assertNull(QueryManager.getCalendar(rs, 2)); - assertNull(QueryManager.getLocalDate(rs, "dt")); + assertNull(QueryManager.getLocalDate(rs, "datetime_col")); assertNull(QueryManager.getLocalDate(rs, 2)); - assertNull(QueryManager.getLocalDateTime(rs, "dt")); + assertNull(QueryManager.getLocalDateTime(rs, "datetime_col")); assertNull(QueryManager.getLocalDateTime(rs, 2)); - assertNull(QueryManager.getOffsetDateTime(rs, "dt")); + assertNull(QueryManager.getOffsetDateTime(rs, "datetime_col")); assertNull(QueryManager.getOffsetDateTime(rs, 2)); - assertNull(QueryManager.getTimestamp(rs, "dt")); + assertNull(QueryManager.getTimestamp(rs, "datetime_col")); assertNull(QueryManager.getTimestamp(rs, 2)); - assertNull(QueryManager.getObject(rs, "c")); + assertNull(QueryManager.getObject(rs, "char_col")); assertNull(QueryManager.getObject(rs, 3)); } @@ -236,9 +246,9 @@ class QueryManagerTest void testLocalDate() throws SQLException { Connection connection = getConnection(); - QueryManager.executeUpdate(connection, "INSERT INTO t (d) VALUES (?)", LocalDate.of(2013, Month.OCTOBER, 1)); + QueryManager.executeUpdate(connection, "INSERT INTO test_table (date_col) VALUES (?)", LocalDate.of(2013, Month.OCTOBER, 1)); - PreparedStatement preparedStatement = connection.prepareStatement("SELECT d from t"); + PreparedStatement preparedStatement = connection.prepareStatement("SELECT date_col from test_table"); preparedStatement.execute(); ResultSet rs = preparedStatement.getResultSet(); rs.next(); @@ -268,4 +278,55 @@ class QueryManagerTest assertEquals(0, offsetDateTime.getMinute(), "Minute value"); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLocalTime() throws SQLException + { + Connection connection = getConnection(); + + //////////////////////////////////// + // insert one just hour & minutes // + //////////////////////////////////// + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, time_col) VALUES (?, ?)", 1, LocalTime.of(10, 42)); + + PreparedStatement preparedStatement = connection.prepareStatement("SELECT time_col from test_table where int_col=1"); + preparedStatement.execute(); + ResultSet rs = preparedStatement.getResultSet(); + rs.next(); + + LocalTime localTime = QueryManager.getLocalTime(rs, 1); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(0, localTime.getSecond(), "Second value"); + + localTime = QueryManager.getLocalTime(rs, "time_col"); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(0, localTime.getSecond(), "Second value"); + + ///////////////////////////////// + // now insert one with seconds // + ///////////////////////////////// + QueryManager.executeUpdate(connection, "INSERT INTO test_table (int_col, time_col) VALUES (?, ?)", 2, LocalTime.of(10, 42, 59)); + + preparedStatement = connection.prepareStatement("SELECT time_col from test_table where int_col=2"); + preparedStatement.execute(); + rs = preparedStatement.getResultSet(); + rs.next(); + + localTime = QueryManager.getLocalTime(rs, 1); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(59, localTime.getSecond(), "Second value"); + + localTime = QueryManager.getLocalTime(rs, "time_col"); + assertEquals(10, localTime.getHour(), "Hour value"); + assertEquals(42, localTime.getMinute(), "Minute value"); + assertEquals(59, localTime.getSecond(), "Second value"); + } + } \ No newline at end of file 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 505f1340..b1e18017 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 @@ -25,6 +25,7 @@ package com.kingsrook.qqq.frontend.picocli; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -407,7 +408,8 @@ public class QCommandBuilder case INTEGER -> Integer.class; case DECIMAL -> BigDecimal.class; case DATE -> LocalDate.class; - // case TIME -> LocalTime.class; + case TIME -> LocalTime.class; + case BOOLEAN -> Boolean.class; case DATE_TIME -> LocalDateTime.class; case BLOB -> byte[].class; };