diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/AssignmentConversion.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/AssignmentConversion.java new file mode 100644 index 0000000..2e8ca72 --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/AssignmentConversion.java @@ -0,0 +1,23 @@ +package org.ethelred.kiwiproc.processor; + +import org.jspecify.annotations.Nullable; + +/** + * It's not really a conversion. + */ +public record AssignmentConversion() implements Conversion { + @Override + public boolean isValid() { + return true; + } + + @Override + public boolean hasWarning() { + return false; + } + + @Override + public @Nullable String warning() { + return null; + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/Conversion.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/Conversion.java new file mode 100644 index 0000000..25f0358 --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/Conversion.java @@ -0,0 +1,17 @@ +package org.ethelred.kiwiproc.processor; + +import org.jspecify.annotations.Nullable; + +public sealed interface Conversion + permits AssignmentConversion, + FromSqlArrayConversion, + InvalidConversion, + NullableSourceConversion, + StringFormatConversion, + ToSqlArrayConversion { + boolean isValid(); + + boolean hasWarning(); + + @Nullable String warning(); +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java index 750764f..2319c09 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/CoreTypes.java @@ -18,9 +18,7 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.ethelred.kiwiproc.processor.types.BasicType; -import org.ethelred.kiwiproc.processor.types.KiwiType; -import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; +import org.ethelred.kiwiproc.processor.types.*; import org.jspecify.annotations.Nullable; public class CoreTypes { @@ -36,12 +34,6 @@ public class CoreTypes { LocalDateTime.class, OffsetDateTime.class); - public record Conversion(boolean isValid, @Nullable String warning, String conversionFormat) { - public boolean hasWarning() { - return warning != null; - } - } - public static final Map, Class> primitiveToBoxed = Map.ofEntries( entry(boolean.class, Boolean.class), entry(byte.class, Byte.class), @@ -74,19 +66,19 @@ private static boolean isAssignable(Class source, Class target) { return assignableFrom.getOrDefault(source, Set.of()).contains(target); } - private final Conversion invalid = new Conversion(false, null, "invalid"); + private final Conversion invalid = new InvalidConversion(); Map, KiwiType> coreTypes; - Map coreMappings; + Map simpleMappings; public CoreTypes() { coreTypes = defineTypes(); - coreMappings = defineMappings(); + simpleMappings = defineMappings(); // System.out.println( // coreMappings.entrySet().stream().map(Object::toString).collect(Collectors.joining("\n"))); } - private Map defineMappings() { - List> entries = new ArrayList<>(200); + private Map defineMappings() { + List> entries = new ArrayList<>(200); addPrimitiveMappings(entries); addPrimitiveParseMappings(entries); @@ -97,27 +89,26 @@ private Map defineMappings() { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)); } - private void addPrimitiveParseMappings(Collection> entries) { + private void addPrimitiveParseMappings(Collection> entries) { primitiveToBoxed.keySet().forEach(target -> { String warning = "possible NumberFormatException parsing String to %s".formatted(target.getName()); Class boxed = primitiveToBoxed.get(target); // String -> primitive TypeMapping t = new TypeMapping(STRING_TYPE, coreTypes.get(target)); - Conversion c = new Conversion( - true, + StringFormatConversion c = new StringFormatConversion( warning, "%s.parse%s(%%s)".formatted(boxed.getSimpleName(), Util.capitalizeFirst(target.getSimpleName()))); entries.add(entry(t, c)); // String -> boxed t = new TypeMapping(STRING_TYPE, coreTypes.get(boxed)); - c = new Conversion(true, warning, "%s.valueOf(%%s)".formatted(boxed.getSimpleName())); + c = new StringFormatConversion(warning, "%s.valueOf(%%s)".formatted(boxed.getSimpleName())); entries.add(entry(t, c)); }); } - private void addDateTimeMappings(Collection> entries) {} + private void addDateTimeMappings(Collection> entries) {} - private void addBigNumberMappings(Collection> entries) { + private void addBigNumberMappings(Collection> entries) { List.of(BigInteger.class, BigDecimal.class).forEach(big -> { // primitive -> Big Stream.of(byte.class, short.class, int.class, long.class, float.class, double.class) @@ -138,7 +129,7 @@ private void addBigNumberMappings(Collection> }); } - private void addPrimitiveMappings(Collection> entries) { + private void addPrimitiveMappings(Collection> entries) { // primitive safe assignments assignableFrom.forEach((source, targets) -> { targets.forEach(target -> { @@ -160,12 +151,12 @@ private void addPrimitiveMappings(Collection> }); } - private Map.Entry mappingEntry( + private Map.Entry mappingEntry( Class source, Class target, @Nullable String warning, String conversionFormat) { var fromType = Objects.requireNonNull(coreTypes.get(source)); var toType = Objects.requireNonNull(coreTypes.get(target)); var mapping = new TypeMapping(fromType, toType); - var lookup = new Conversion(true, warning, conversionFormat); + var lookup = new StringFormatConversion(warning, conversionFormat); return entry(mapping, lookup); } @@ -192,25 +183,47 @@ public Conversion lookup(TypeMapping mapper) { public Conversion lookup(KiwiType source, KiwiType target) { if (source.equals(target) || source.withIsNullable(true).equals(target)) { - return new Conversion(true, null, "%s"); + return new AssignmentConversion(); + } + if (source instanceof ContainerType ct && target instanceof SqlArrayType sat) { + return toSqlArray(ct, sat); + } + if (source instanceof SqlArrayType sat && target instanceof ContainerType ct) { + return fromSqlArray(sat, ct); } // special case String - Conversion stringConversion = null; + StringFormatConversion stringConversion = null; if (STRING_TYPE.equals(target) || STRING_TYPE.withIsNullable(true).equals(target)) { - stringConversion = new Conversion(true, null, "String.valueOf(%s)"); + stringConversion = new StringFormatConversion(null, "String.valueOf(%s)"); } var result = firstNonNull( stringConversion, - coreMappings.get(new TypeMapping(source, target)), - coreMappings.get(new TypeMapping(source, target.withIsNullable(false))), - coreMappings.get(new TypeMapping(source.withIsNullable(false), target.withIsNullable(false))), + simpleMappings.get(new TypeMapping(source, target)), + simpleMappings.get(new TypeMapping(source, target.withIsNullable(false))), + simpleMappings.get(new TypeMapping(source.withIsNullable(false), target.withIsNullable(false))), invalid); if (result.isValid() && source.isNullable()) { - result = new Conversion(true, result.warning(), nullWrap(result.conversionFormat())); + result = new NullableSourceConversion(result); } return result; } + private Conversion fromSqlArray(SqlArrayType sat, ContainerType ct) { + var elementConversion = lookup(sat.containedType(), ct.containedType()); + if (!elementConversion.isValid()) { + return elementConversion; + } + return new FromSqlArrayConversion(sat, ct, elementConversion); + } + + private Conversion toSqlArray(ContainerType ct, SqlArrayType sat) { + var elementConversion = lookup(ct.containedType().withIsNullable(false), sat.containedType()); + if (!elementConversion.isValid()) { + return elementConversion; + } + return new ToSqlArrayConversion(ct, sat, elementConversion); + } + private Conversion firstNonNull(@Nullable Conversion... conversions) { for (var c : conversions) { if (c != null) { @@ -219,9 +232,4 @@ private Conversion firstNonNull(@Nullable Conversion... conversions) { } throw new NullPointerException(); } - - private String nullWrap(String conversionFormat) { - conversionFormat = conversionFormat.replace("%s", "%(); + var componentTypes = new ArrayList(); for (var component : utils.recordComponents(t)) { - componentTypes.add(new RecordType.RecordTypeComponent( - component.getSimpleName().toString(), visit(component.asType()))); + componentTypes.add( + new RecordTypeComponent(component.getSimpleName().toString(), visit(component.asType()))); } return new RecordType(utils.packageName(t), utils.className(t), List.copyOf(componentTypes)); } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/NullableSourceConversion.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/NullableSourceConversion.java new file mode 100644 index 0000000..c720bff --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/NullableSourceConversion.java @@ -0,0 +1,20 @@ +package org.ethelred.kiwiproc.processor; + +import org.jspecify.annotations.Nullable; + +public record NullableSourceConversion(Conversion conversion) implements Conversion { + @Override + public boolean isValid() { + return conversion.isValid(); + } + + @Override + public boolean hasWarning() { + return conversion.hasWarning(); + } + + @Override + public @Nullable String warning() { + return conversion.warning(); + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java index 2fbd794..9d81702 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/SqlTypeMapping.java @@ -21,10 +21,11 @@ public record SqlTypeMapping( String accessorSuffix, boolean specialCase, boolean isNullable, - @Nullable SqlTypeMapping componentType) + @Nullable SqlTypeMapping componentType, + @Nullable String componentDbType) implements SqlTypeMappingBuilder.With { public SqlTypeMapping(JDBCType jdbcType, Class baseType, String accessorSuffix) { - this(jdbcType, baseType, accessorSuffix, false, false, null); + this(jdbcType, baseType, accessorSuffix, false, false, null, null); } private static final List types = List.of( @@ -45,7 +46,7 @@ public SqlTypeMapping(JDBCType jdbcType, Class baseType, String accessorSuffi new SqlTypeMapping(JDBCType.NCHAR, String.class, "String"), new SqlTypeMapping(JDBCType.NVARCHAR, String.class, "String"), new SqlTypeMapping(JDBCType.LONGNVARCHAR, String.class, "String"), - new SqlTypeMapping(JDBCType.ARRAY, java.sql.Array.class, "Array", true, false, null), + new SqlTypeMapping(JDBCType.ARRAY, java.sql.Array.class, "Array", true, false, null, null), // dates and times // Use java.time types - recommended for Postgres https://tada.github.io/pljava/use/datetime.html new SqlTypeMapping(JDBCType.DATE, LocalDate.class, ""), @@ -53,7 +54,7 @@ public SqlTypeMapping(JDBCType jdbcType, Class baseType, String accessorSuffi new SqlTypeMapping(JDBCType.TIME_WITH_TIMEZONE, OffsetTime.class, ""), new SqlTypeMapping(JDBCType.TIMESTAMP, LocalDateTime.class, ""), new SqlTypeMapping(JDBCType.TIMESTAMP_WITH_TIMEZONE, OffsetDateTime.class, ""), - new SqlTypeMapping(JDBCType.NULL, void.class, "", true, true, null) + new SqlTypeMapping(JDBCType.NULL, void.class, "", true, true, null, null) // TODO fill out types as necessary ); @@ -66,11 +67,16 @@ public static SqlTypeMapping get(ColumnMetaData columnMetaData) { throw new IllegalArgumentException("Unsupported JDBCType type " + columnMetaData.sqlType()); } if (r.jdbcType == JDBCType.ARRAY) { - var component = JDBC_TYPE_SQL_TYPE_MAPPING_MAP.get(columnMetaData.componentType()); + if (columnMetaData.componentType() == null) { + throw new IllegalArgumentException("No component type provided for SQL Array"); + } + var component = JDBC_TYPE_SQL_TYPE_MAPPING_MAP.get( + columnMetaData.componentType().jdbcType()); if (component == null) { throw new IllegalArgumentException("No component type found for SQL Array"); } - r = r.withComponentType(component); + r = r.withComponentType(component) + .withComponentDbType(columnMetaData.componentType().dbType()); } return r.withIsNullable(columnMetaData.nullable()); } @@ -78,7 +84,7 @@ public static SqlTypeMapping get(ColumnMetaData columnMetaData) { public KiwiType kiwiType() { if (jdbcType == JDBCType.ARRAY) { assert componentType != null; - return new SqlArrayType(componentType.kiwiType()); + return new SqlArrayType(componentType.kiwiType(), componentType.jdbcType, componentDbType); } if (CoreTypes.primitiveToBoxed.containsKey(baseType)) { return new PrimitiveKiwiType(baseType().getSimpleName(), isNullable); diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/StringFormatConversion.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/StringFormatConversion.java new file mode 100644 index 0000000..d3f9a6f --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/StringFormatConversion.java @@ -0,0 +1,14 @@ +package org.ethelred.kiwiproc.processor; + +import org.jspecify.annotations.Nullable; + +public record StringFormatConversion(@Nullable String warning, String conversionFormat) implements Conversion { + public boolean hasWarning() { + return warning != null; + } + + @Override + public boolean isValid() { + return true; + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/ToSqlArrayConversion.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/ToSqlArrayConversion.java new file mode 100644 index 0000000..5633bbe --- /dev/null +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/ToSqlArrayConversion.java @@ -0,0 +1,23 @@ +package org.ethelred.kiwiproc.processor; + +import org.ethelred.kiwiproc.processor.types.ContainerType; +import org.ethelred.kiwiproc.processor.types.SqlArrayType; +import org.jspecify.annotations.Nullable; + +public record ToSqlArrayConversion(ContainerType ct, SqlArrayType sat, Conversion elementConversion) + implements Conversion { + @Override + public boolean isValid() { + return elementConversion.isValid(); + } + + @Override + public boolean hasWarning() { + return elementConversion.hasWarning(); + } + + @Override + public @Nullable String warning() { + return elementConversion.warning(); + } +} diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java index f3ea92f..98d9fd3 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/TypeValidator.java @@ -11,10 +11,6 @@ public record TypeValidator(Logger logger, Element element, CoreTypes coreTypes) private static final KiwiType UPDATE_RETURN_TYPE = new PrimitiveKiwiType("int", false); private static final KiwiType BATCH_RETURN_TYPE = new ContainerType(ValidContainerType.ARRAY, UPDATE_RETURN_TYPE); - public TypeValidator { - element = Objects.requireNonNull(element); - } - public TypeValidator(Logger logger, Element methodElement) { this(logger, methodElement, new CoreTypes()); } @@ -82,13 +78,15 @@ private boolean validateCompatible(KiwiType source, KiwiType target) { return true; } debug("Comparing %s with %s".formatted(source, target)); - /* - We assume that a container may not contain a null. However, for adding to a target container, pretend that the - contained type is nullable. We will skip nulls. - */ - if (source instanceof ContainerType sourceContainer && target instanceof ContainerType targetContainer) { - // we can convert any container into a different one, since they are all effectively equivalent to Iterable. - return validateCompatible(sourceContainer.containedType(), targetContainer.containedType()); + + // check for a valid conversion + // don't fail on invalid conversion because there are other possible cases + Conversion c = coreTypes.lookup(source, target); + if (c.isValid()) { + if (c.hasWarning()) { + warn(Objects.requireNonNull(c.warning())); + } + return true; } if (target instanceof ContainerType targetContainer) { // we can convert a single value to a container by wrapping @@ -98,51 +96,12 @@ private boolean validateCompatible(KiwiType source, KiwiType target) { && containerType.type() == ValidContainerType.OPTIONAL && target.isSimple()) { // an Optional can be converted to a nullable simple type - // targetDO how to interact with Record? + // TODO how to interact with Record? return target.isNullable() && validateCompatible(containerType.containedType(), target); } - if (source instanceof RecordType sourceRecord && target instanceof RecordType targetRecord) { - // Component names must match, and types must be compatible. Order is not relevant in this context. - var targetComponents = targetRecord.components(); - return sourceRecord.components().stream().allMatch(e -> { - var targetComponentType = targetComponents.stream() - .filter(targetComponent -> e.name().equals(targetComponent.name())) - .findFirst() - .orElse(null); - return targetComponentType != null && validateCompatible(e.type(), targetComponentType.type()); - }); - } - if (source.isSimple() && !source.isNullable() && target.isSimple() && target.isNullable()) { - // non-null can be converted to nullable - return validateCompatible(source.withIsNullable(true), target); - } - if (source.isSimple() && target.isSimple()) { - return validateSimpleCompatible(source, target); - } return false; } - private boolean validateSimpleCompatible(KiwiType source, KiwiType target) { - if (source.equals(target)) { - // shortcut - return true; - } - if (source.isNullable() != target.isNullable()) { - // nullability must match. (See validateCompatible for non-null -> null) - return false; - } - if (target.equals(CoreTypes.STRING_TYPE.withIsNullable(target.isNullable()))) { - // anything can be converted to String - return true; - } - var typeLookup = coreTypes.lookup(source, target); - if (typeLookup.hasWarning()) { - logger.warn(element, typeLookup.warning()); - } - return typeLookup.isValid(); - // targetDO user defined mappings - } - /** * Common check for the expected hierarchy. * @param type @@ -157,6 +116,10 @@ private boolean validateGeneral(KiwiType type) { var contained = ct.containedType(); return ((contained instanceof RecordType) || (contained.isSimple())) && validateGeneral(contained); } + if (type instanceof SqlArrayType sqlArrayType) { + var contained = sqlArrayType.containedType(); + return contained.isSimple() && validateGeneral(contained); + } if (type instanceof RecordType rt) { var componentTypes = rt.components(); return componentTypes.stream().allMatch(t -> t.type().isSimple()); @@ -177,6 +140,10 @@ private void debug(String message) { info("DEBUG: " + message); } + private void warn(String message) { + logger.warn(element, message); + } + public boolean validateReturn(List columnMetaData, KiwiType returnType, QueryMethodKind kind) { if (returnType instanceof VoidType && kind != QueryMethodKind.QUERY) { return true; diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java index 1fac9c4..72b46ab 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/generator/InstanceGenerator.java @@ -8,17 +8,17 @@ import java.util.*; import javax.lang.model.element.Modifier; import org.ethelred.kiwiproc.processor.*; -import org.ethelred.kiwiproc.processor.types.BasicType; public class InstanceGenerator { private final Logger logger; private final KiwiTypeConverter kiwiTypeConverter; private final CoreTypes coreTypes; + private final Set parameterNames = new HashSet<>(); + private final Map patchedNames = new HashMap<>(); public InstanceGenerator(Logger logger, KiwiTypeConverter kiwiTypeConverter, CoreTypes coreTypes) { this.logger = logger; - this.kiwiTypeConverter = kiwiTypeConverter; this.coreTypes = coreTypes; } @@ -48,6 +48,8 @@ public JavaFile generate(DAOClassInfo classInfo) { } private MethodSpec buildMethod(DAOMethodInfo methodInfo) { + parameterNames.clear(); + patchedNames.clear(); var methodSpecBuilder = MethodSpec.overriding(methodInfo.methodElement()); methodSpecBuilder.addStatement("var connection = context.getConnection()"); methodSpecBuilder.beginControlFlow( @@ -78,19 +80,17 @@ private CodeBlock batchMethodBody(DAOMethodInfo methodInfo) { private CodeBlock queryMethodBody(DAOMethodInfo methodInfo) { var builder = CodeBlock.builder(); - Set parameterNames = new HashSet<>(); methodInfo.parameterMapping().forEach(parameterInfo -> { var name = "param" + parameterInfo.index(); var conversion = lookupConversion(parameterInfo::element, parameterInfo.mapper()); - builder.addStatement( - "var $L = $L", name, conversion.conversionFormat().formatted(parameterInfo.javaAccessor())); - var nullableSource = - parameterInfo.mapper().source() instanceof BasicType simpleType && simpleType.isNullable(); - if (nullableSource) { - builder.beginControlFlow("if ($L == null)", name) - .addStatement("statement.setNull($L, $L)", parameterInfo.index(), parameterInfo.sqlType()) - .nextControlFlow("else"); - } + buildConversion(builder, conversion, name, parameterInfo.javaAccessor(), true); + var nullableSource = parameterInfo.mapper().source().isNullable(); + // if (nullableSource) { + // builder.beginControlFlow("if ($L == null)", name) + // .addStatement("statement.setNull($L, $L)", parameterInfo.index(), + // parameterInfo.sqlType()) + // .nextControlFlow("else"); + // } builder.addStatement("statement.$L($L, $L)", parameterInfo.setter(), parameterInfo.index(), name); if (nullableSource) { builder.endControlFlow(); @@ -109,13 +109,9 @@ private CodeBlock queryMethodBody(DAOMethodInfo methodInfo) { TypeMapping mapping = singleColumn.asTypeMapping(); var conversion = lookupConversion(methodInfo::methodElement, mapping); builder.addStatement( - "var rawValue = rs.get$L($S)", - singleColumn.sqlTypeMapping().accessorSuffix(), - singleColumn.name()) - .addStatement( - "var value = $L", conversion.conversionFormat().formatted("rawValue")); + "var rawValue = rs.get$L($S)", singleColumn.sqlTypeMapping().accessorSuffix(), singleColumn.name()); + buildConversion(builder, conversion, "value", "rawValue", true); } else if (!multipleColumns.isEmpty()) { - Map patchedNames = new HashMap<>(); multipleColumns.forEach(daoResultColumn -> { var conversion = lookupConversion(methodInfo::methodElement, daoResultColumn.asTypeMapping()); String rawName = daoResultColumn.name() + "Raw"; @@ -132,9 +128,8 @@ private CodeBlock queryMethodBody(DAOMethodInfo methodInfo) { .addStatement("$L = null", rawName) .endControlFlow(); } - var varName = patchName(parameterNames, patchedNames, daoResultColumn.name()); - builder.addStatement( - "var $L = $L", varName, conversion.conversionFormat().formatted(rawName)); + var varName = patchName(daoResultColumn.name()); + buildConversion(builder, conversion, varName, rawName, true); }); var params = multipleColumns.stream() .map(p -> CodeBlock.of("$L", patchedNames.get(p.name()))) @@ -157,7 +152,42 @@ private CodeBlock queryMethodBody(DAOMethodInfo methodInfo) { return builder.build(); } - private String patchName(Set parameterNames, Map patchedNames, String name) { + private void buildConversion( + CodeBlock.Builder builder, Conversion conversion, String assignee, String accessor, boolean withVar) { + var insertVar = withVar ? "var " : ""; + if (conversion instanceof AssignmentConversion) { + /* e.g. + var param1 = id; + */ + builder.addStatement("$L$L = $L", insertVar, assignee, accessor); + } else if (conversion instanceof StringFormatConversion sfc) { + /* e.g. + var param1 = (int) id; + */ + builder.addStatement( + "$L$L = $L", insertVar, assignee, sfc.conversionFormat().formatted(accessor)); + } else if (conversion instanceof ToSqlArrayConversion sac) { + /* e.g. + Object[] elementObjects = listParam.toArray(); + var param1 = connection.createArrayOf("_int4", elementObjects); + */ + String elementObjects = patchName("elementObjects"); + builder.addStatement( + "Object[] $L = $L", + elementObjects, + String.format(sac.ct().type().toObjectArrayTemplate(), accessor)); + builder.addStatement( + "$L$L = connection.createArrayOf($S, $L)", + insertVar, + assignee, + sac.sat().dbType(), + elementObjects); + } else { + logger.error(null, "Unsupported Conversion %s".formatted(conversion)); // TODO add Element + } + } + + private String patchName(String name) { return patchedNames.computeIfAbsent(name, k -> { var newName = k; while (parameterNames.contains(newName)) { @@ -167,7 +197,7 @@ private String patchName(Set parameterNames, Map patched }); } - CoreTypes.Conversion lookupConversion(ElementSupplier elementSupplier, TypeMapping t) { + Conversion lookupConversion(ElementSupplier elementSupplier, TypeMapping t) { var element = elementSupplier.getElement(); var conversion = coreTypes.lookup(t); if (!conversion.isValid()) { diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java index 51a73ce..30eba6c 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/ContainerType.java @@ -16,4 +16,9 @@ public String className() { public boolean isSimple() { return false; } + + @Override + public String toString() { + return className() + "<" + containedType + ">"; + } } diff --git a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java index f747ae8..7ebce49 100644 --- a/processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java +++ b/processor/src/main/java/org/ethelred/kiwiproc/processor/types/RecordType.java @@ -8,6 +8,4 @@ public record RecordType(String packageName, String className, List(l) """), - OPTIONAL(Optional.class, """ + OPTIONAL( + Optional.class, + """ l.isEmpty() ? Optional.empty() : Optional.of(l.get(0)) - """); + """, + """ + %s.stream().toArray()"""); private final Class javaType; @@ -27,14 +36,20 @@ public String fromListTemplate() { } private final String fromListTemplate; + private final String toObjectArrayTemplate; - ValidContainerType(Class javaType, String fromListTemplate) { + ValidContainerType(Class javaType, String fromListTemplate, String toObjectArrayTemplate) { this.javaType = javaType; this.fromListTemplate = fromListTemplate; + this.toObjectArrayTemplate = toObjectArrayTemplate; } ValidContainerType(Class javaType) { - this(javaType, "List.copyOf(l)"); + this(javaType, "List.copyOf(l)", "%s.toArray()"); + } + + ValidContainerType(Class javaType, String fromListTemplate) { + this(javaType, fromListTemplate, "%s.toArray()"); } public boolean isMultiValued() { @@ -49,4 +64,8 @@ public Class javaType() { public String toString() { return javaType().getName(); } + + public String toObjectArrayTemplate() { + return toObjectArrayTemplate; + } } diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java index 760dbca..4f25ae8 100644 --- a/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/CoreTypesTest.java @@ -7,12 +7,10 @@ import java.io.OutputStream; import java.math.BigDecimal; +import java.sql.JDBCType; import java.time.LocalDate; import java.util.stream.Stream; -import org.ethelred.kiwiproc.processor.types.BasicType; -import org.ethelred.kiwiproc.processor.types.KiwiType; -import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; -import org.ethelred.kiwiproc.processor.types.UnsupportedType; +import org.ethelred.kiwiproc.processor.types.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -62,8 +60,10 @@ void testConversions( .that(conversion.hasWarning()) .isEqualTo(isWarning); if (conversion.isValid()) { - var formatted = conversion.conversionFormat().formatted("value"); - assertThat(formatted).isEqualTo(conversionFormatContains); + if (conversion instanceof StringFormatConversion sfc) { + var formatted = sfc.conversionFormat().formatted("value"); + assertThat(formatted).isEqualTo(conversionFormatContains); + } } } @@ -82,6 +82,12 @@ public static Stream testConversions() { arguments(ofClass(double.class), ofClass(Short.class, true), true, true, "(short) value"), arguments(ofClass(double.class), ofClass(BigDecimal.class), true, false, "BigDecimal.valueOf(value)"), arguments(ofClass(String.class), ofClass(int.class), true, true, "Integer.parseInt(value)"), - arguments(ofClass(String.class), ofClass(Integer.class, true), true, true, "Integer.valueOf(value)")); + arguments(ofClass(String.class), ofClass(Integer.class, true), true, true, "Integer.valueOf(value)"), + arguments( + new ContainerType(ValidContainerType.LIST, ofClass(Integer.class, true)), + new SqlArrayType(ofClass(int.class), JDBCType.INTEGER, "ignored"), + true, + false, + "fail")); } } diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java index e26af18..2e45559 100644 --- a/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/SqlTypeMappingTest.java @@ -2,15 +2,20 @@ import static com.google.common.truth.Truth.assertThat; import static org.ethelred.kiwiproc.processor.TestUtils.atLeastOne; +import static org.junit.jupiter.params.provider.Arguments.arguments; import java.sql.JDBCType; import java.util.Set; +import java.util.stream.Stream; +import org.ethelred.kiwiproc.meta.ArrayComponent; import org.ethelred.kiwiproc.meta.ColumnMetaData; import org.ethelred.kiwiproc.processor.types.BasicType; import org.ethelred.kiwiproc.processor.types.PrimitiveKiwiType; import org.ethelred.kiwiproc.processor.types.SqlArrayType; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; public class SqlTypeMappingTest { @@ -33,6 +38,11 @@ public class SqlTypeMappingTest { JDBCType.SQLXML, JDBCType.REF_CURSOR); + public static Stream sqlMappingIsPresentForArrayType() { + // arguments - JDBCType, String: db specific type name + return Stream.of(arguments(JDBCType.INTEGER, "fail")); + } + @ParameterizedTest @EnumSource(JDBCType.class) public void sqlMappingIsPresentForJDBCType(JDBCType jdbcType) { @@ -49,12 +59,13 @@ public void sqlMappingIsPresentForJDBCType(JDBCType jdbcType) { } @ParameterizedTest - @EnumSource(JDBCType.class) - public void sqlMappingIsPresentForArrayType(JDBCType componentType) { + @MethodSource + public void sqlMappingIsPresentForArrayType(JDBCType componentType, String dbType) { if (unsupportedTypes.contains(componentType)) { return; } - var columnMetaData = new ColumnMetaData(1, "columnName", false, JDBCType.ARRAY, componentType); + var columnMetaData = + new ColumnMetaData(1, "columnName", false, JDBCType.ARRAY, new ArrayComponent(componentType, dbType)); var mapping = SqlTypeMapping.get(columnMetaData); assertThat(mapping).isNotNull(); assertThat(mapping.kiwiType()).isInstanceOf(SqlArrayType.class); diff --git a/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java b/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java index a1c861d..baa9eaa 100644 --- a/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java +++ b/processor/src/test/java/org/ethelred/kiwiproc/processor/TypeValidatorTest.java @@ -17,6 +17,7 @@ import javax.lang.model.element.*; import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; +import org.ethelred.kiwiproc.meta.ArrayComponent; import org.ethelred.kiwiproc.meta.ColumnMetaData; import org.ethelred.kiwiproc.processor.types.*; import org.jspecify.annotations.Nullable; @@ -43,7 +44,7 @@ void testQueryParameter( boolean expectedResult, @Nullable String message) { var result = validator.validateParameters(Map.of(columnMetaData, parameterInfo), QueryMethodKind.QUERY); - assertWithMessage("testQueryParameter %s -> %s", logKiwiType(columnMetaData), parameterInfo.type()) + assertWithMessage("testQueryParameter %s -> %s", parameterInfo.type(), logKiwiType(columnMetaData)) .that(result) .isEqualTo(expectedResult); if (message == null) { @@ -74,6 +75,16 @@ public static Stream testQueryParameter() { col(true, JDBCType.INTEGER), new MethodParameterInfo(mockVariableElement(), "x", ofClass(Integer.class, true), false, null), true, + null), + arguments( + col(true, JDBCType.ARRAY, new ArrayComponent(JDBCType.INTEGER, "ignored")), + new MethodParameterInfo( + mockVariableElement(), + "x", + new ContainerType(ValidContainerType.LIST, ofClass(Integer.class, true)), + false, + null), + true, null)); } @@ -137,8 +148,7 @@ ValidContainerType.LIST, recordType("TestRecord", "test1", ofClass(int.class))), } private static KiwiType recordType(String className, String componentName, KiwiType componentType) { - return new RecordType( - "test", className, List.of(new RecordType.RecordTypeComponent(componentName, componentType))); + return new RecordType("test", className, List.of(new RecordTypeComponent(componentName, componentType))); } private static KiwiType recordType( @@ -151,8 +161,8 @@ private static KiwiType recordType( "test", className, List.of( - new RecordType.RecordTypeComponent(componentName, componentType), - new RecordType.RecordTypeComponent(componentName2, componentType2))); + new RecordTypeComponent(componentName, componentType), + new RecordTypeComponent(componentName2, componentType2))); } static Arguments testCase( @@ -162,7 +172,7 @@ static Arguments testCase( return result; } - static ColumnMetaData col(boolean nullable, JDBCType type, @Nullable JDBCType componentType) { + static ColumnMetaData col(boolean nullable, JDBCType type, @Nullable ArrayComponent componentType) { return new ColumnMetaData(colCount, "test" + colCount++, nullable, type, componentType); } diff --git a/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ArrayComponent.java b/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ArrayComponent.java new file mode 100644 index 0000000..4aa8225 --- /dev/null +++ b/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ArrayComponent.java @@ -0,0 +1,5 @@ +package org.ethelred.kiwiproc.meta; + +import java.sql.JDBCType; + +public record ArrayComponent(JDBCType jdbcType, String dbType) {} diff --git a/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ColumnMetaData.java b/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ColumnMetaData.java index ade8d13..f4e13e0 100644 --- a/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ColumnMetaData.java +++ b/querymeta/src/main/java/org/ethelred/kiwiproc/meta/ColumnMetaData.java @@ -1,13 +1,15 @@ package org.ethelred.kiwiproc.meta; import java.sql.*; +import java.util.Objects; import org.jspecify.annotations.Nullable; import org.postgresql.core.BaseConnection; public record ColumnMetaData( - int index, String name, boolean nullable, JDBCType sqlType, @Nullable JDBCType componentType + int index, String name, boolean nullable, JDBCType sqlType, @Nullable ArrayComponent componentType // TODO precision/scale? ) { + public static ColumnMetaData from(Connection connection, int index, ResultSetMetaData resultSetMetaData) throws SQLException { return new ColumnMetaData( @@ -24,7 +26,7 @@ public static ColumnMetaData from(Connection connection, int index, ResultSetMet resultSetMetaData.getColumnTypeName(index))); } - @Nullable private static JDBCType componentType(Connection connection, int columnType, String columnTypeName) { + @Nullable private static ArrayComponent componentType(Connection connection, int columnType, String columnTypeName) { if (columnType != Types.ARRAY) { return null; } @@ -33,7 +35,9 @@ public static ColumnMetaData from(Connection connection, int index, ResultSetMet var typeInfo = pgConnection.getTypeInfo(); var oid = typeInfo.getPGType(columnTypeName); var componentOid = typeInfo.getPGArrayElement(oid); - return JDBCType.valueOf(typeInfo.getSQLType(componentOid)); + return new ArrayComponent( + JDBCType.valueOf(typeInfo.getSQLType(componentOid)), + Objects.requireNonNull(typeInfo.getPGType(componentOid))); } catch (SQLException ignored) { } diff --git a/querymeta/src/test/java/org/ethelred/kiwiproc/meta/DatabaseWrapperTest.java b/querymeta/src/test/java/org/ethelred/kiwiproc/meta/DatabaseWrapperTest.java index 3602417..fb50540 100644 --- a/querymeta/src/test/java/org/ethelred/kiwiproc/meta/DatabaseWrapperTest.java +++ b/querymeta/src/test/java/org/ethelred/kiwiproc/meta/DatabaseWrapperTest.java @@ -74,7 +74,8 @@ public void simpleQueryArrayParameterMetaData() throws SQLException { var queryMetaData = getQueryMetaData("SELECT * FROM test_table where test_id = ANY(?)"); assertThat(queryMetaData.parameters()).hasSize(1); assertThat(queryMetaData.parameters().get(0)) - .isEqualTo(new ColumnMetaData(1, "parameter", false, JDBCType.ARRAY, JDBCType.INTEGER)); + .isEqualTo(new ColumnMetaData( + 1, "parameter", false, JDBCType.ARRAY, new ArrayComponent(JDBCType.INTEGER, "int4"))); assertThat(queryMetaData.resultColumns()).hasSize(4); assertThat(queryMetaData.resultColumns().get(0)) .isEqualTo(new ColumnMetaData(1, "test_id", false, JDBCType.INTEGER, null)); diff --git a/test-micronaut/src/main/java/org/ethelred/kiwiproc/test/PetClinicDAO.java b/test-micronaut/src/main/java/org/ethelred/kiwiproc/test/PetClinicDAO.java index 8b47780..8ab2825 100644 --- a/test-micronaut/src/main/java/org/ethelred/kiwiproc/test/PetClinicDAO.java +++ b/test-micronaut/src/main/java/org/ethelred/kiwiproc/test/PetClinicDAO.java @@ -38,7 +38,7 @@ default Map getPetTypesWithCount() { .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - // @SqlQuery(""" - // SELECT id, first_name, last_name FROM owners WHERE id = ANY(:ids)""") - // List findOwnersByIds(List ids); + @SqlQuery(""" + SELECT id, first_name, last_name FROM owners WHERE id = ANY(:ids)""") + List findOwnersByIds(List ids); } diff --git a/test-micronaut/src/test/java/org/ethelred/kiwiproc/test/PetClinicTest.java b/test-micronaut/src/test/java/org/ethelred/kiwiproc/test/PetClinicTest.java index 0cf082b..a6d646b 100644 --- a/test-micronaut/src/test/java/org/ethelred/kiwiproc/test/PetClinicTest.java +++ b/test-micronaut/src/test/java/org/ethelred/kiwiproc/test/PetClinicTest.java @@ -5,6 +5,7 @@ import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import java.sql.SQLException; +import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -43,11 +44,11 @@ void happyTestDefaultMethod() throws SQLException { assertThat(countsByType).containsEntry(new PetType(2, "dog"), 4L); } - // @Test - // void happyFindByArrayValues() throws SQLException { - // var owners = transactionalDao.call(dao -> dao.findOwnersByIds(List.of(2, 6, 99))); - // assertThat(owners).hasSize(2); - // var firstNames = owners.stream().map(Owner::first_name).toList(); - // assertThat(firstNames).containsExactly("bob", "joe"); - // } + @Test + void happyFindByArrayValues() throws SQLException { + var owners = dao.findOwnersByIds(List.of(2, 6, 99)); + assertThat(owners).hasSize(2); + var firstNames = owners.stream().map(Owner::first_name).toList(); + assertThat(firstNames).containsExactly("Betty", "Jean"); + } }