Skip to content

Commit

Permalink
Merge pull request #41 from edward3h/container_sqlarray
Browse files Browse the repository at this point in the history
Container sqlarray
  • Loading branch information
edward3h authored Sep 16, 2024
2 parents 59f1fd0 + 3ba28d3 commit 9fed712
Show file tree
Hide file tree
Showing 25 changed files with 379 additions and 158 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<?>, Class<?>> primitiveToBoxed = Map.ofEntries(
entry(boolean.class, Boolean.class),
entry(byte.class, Byte.class),
Expand Down Expand Up @@ -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<Class<?>, KiwiType> coreTypes;
Map<TypeMapping, Conversion> coreMappings;
Map<TypeMapping, StringFormatConversion> simpleMappings;

public CoreTypes() {
coreTypes = defineTypes();
coreMappings = defineMappings();
simpleMappings = defineMappings();
// System.out.println(
// coreMappings.entrySet().stream().map(Object::toString).collect(Collectors.joining("\n")));
}

private Map<TypeMapping, Conversion> defineMappings() {
List<Map.Entry<TypeMapping, Conversion>> entries = new ArrayList<>(200);
private Map<TypeMapping, StringFormatConversion> defineMappings() {
List<Map.Entry<TypeMapping, StringFormatConversion>> entries = new ArrayList<>(200);

addPrimitiveMappings(entries);
addPrimitiveParseMappings(entries);
Expand All @@ -97,27 +89,26 @@ private Map<TypeMapping, Conversion> defineMappings() {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
}

private void addPrimitiveParseMappings(Collection<Map.Entry<TypeMapping, Conversion>> entries) {
private void addPrimitiveParseMappings(Collection<Map.Entry<TypeMapping, StringFormatConversion>> 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<Map.Entry<TypeMapping, Conversion>> entries) {}
private void addDateTimeMappings(Collection<Map.Entry<TypeMapping, StringFormatConversion>> entries) {}

private void addBigNumberMappings(Collection<Map.Entry<TypeMapping, Conversion>> entries) {
private void addBigNumberMappings(Collection<Map.Entry<TypeMapping, StringFormatConversion>> 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)
Expand All @@ -138,7 +129,7 @@ private void addBigNumberMappings(Collection<Map.Entry<TypeMapping, Conversion>>
});
}

private void addPrimitiveMappings(Collection<Map.Entry<TypeMapping, Conversion>> entries) {
private void addPrimitiveMappings(Collection<Map.Entry<TypeMapping, StringFormatConversion>> entries) {
// primitive safe assignments
assignableFrom.forEach((source, targets) -> {
targets.forEach(target -> {
Expand All @@ -160,12 +151,12 @@ private void addPrimitiveMappings(Collection<Map.Entry<TypeMapping, Conversion>>
});
}

private Map.Entry<TypeMapping, Conversion> mappingEntry(
private Map.Entry<TypeMapping, StringFormatConversion> 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);
}

Expand All @@ -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) {
Expand All @@ -219,9 +232,4 @@ private Conversion firstNonNull(@Nullable Conversion... conversions) {
}
throw new NullPointerException();
}

private String nullWrap(String conversionFormat) {
conversionFormat = conversionFormat.replace("%s", "%<s");
return "%s == null ? null : " + conversionFormat;
}
}
Original file line number Diff line number Diff line change
@@ -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 FromSqlArrayConversion(SqlArrayType sat, ContainerType ct, 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.ethelred.kiwiproc.processor;

import org.jspecify.annotations.Nullable;

public record InvalidConversion() implements Conversion {
@Override
public boolean isValid() {
return false;
}

@Override
public boolean hasWarning() {
return false;
}

@Override
public @Nullable String warning() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ public KiwiType visitDeclared(DeclaredType t, Void ignore) {
}
}
if (utils.isRecord(t)) {
var componentTypes = new ArrayList<RecordType.RecordTypeComponent>();
var componentTypes = new ArrayList<RecordTypeComponent>();
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));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SqlTypeMapping> types = List.of(
Expand All @@ -45,15 +46,15 @@ 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, ""),
new SqlTypeMapping(JDBCType.TIME, LocalTime.class, ""),
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
);
Expand All @@ -66,19 +67,24 @@ 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());
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading

0 comments on commit 9fed712

Please sign in to comment.