diff --git a/CodenameOne/src/com/codename1/annotations/db/Temporal.java b/CodenameOne/src/com/codename1/annotations/db/Temporal.java index cde32975604..a62831b5c7e 100644 --- a/CodenameOne/src/com/codename1/annotations/db/Temporal.java +++ b/CodenameOne/src/com/codename1/annotations/db/Temporal.java @@ -27,8 +27,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/// Stores a `java.util.Date` field in a native date or timestamp column instead -/// of the portable default, epoch milliseconds in an integer column. +/// Stores a `java.util.Date`, `java.time.Instant`, `LocalDate` or +/// `LocalDateTime` field in a native date or timestamp column instead of the +/// portable default, a number in an integer column: epoch milliseconds, or +/// epoch days for a `LocalDate`. A `LocalDate` takes `DATE` and the other +/// java.time types take `TIMESTAMP`; a `LocalDateTime` is written as its wall +/// clock, with no zone applied. /// /// Opt-in, because the two are different columns: an existing integer column /// keeps its milliseconds and an existing native one needs this annotation. diff --git a/CodenameOne/src/com/codename1/impl/orm/JavaTimeConverter.java b/CodenameOne/src/com/codename1/impl/orm/JavaTimeConverter.java new file mode 100644 index 00000000000..e30c079e63b --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/orm/JavaTimeConverter.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code 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 General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.orm; + +import com.codename1.orm.session.AttributeConverter; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/// The storage of `java.time.Instant`, `LocalDate` and `LocalDateTime` fields. +/// +/// The defaults follow `java.util.Date`'s: a number in an integer column, which +/// sorts, compares and indexes the same on every engine. An Instant or a +/// LocalDateTime is milliseconds since 1970-01-01T00:00 -- UTC for the Instant, +/// the wall clock itself for the LocalDateTime, which has no zone to convert +/// through -- and a LocalDate is the day count since 1970-01-01. So an Instant +/// reads a column a `long createdAt` or a `Date` wrote, unchanged. +/// +/// `@Temporal` opts into a native column instead, through the same UTC ISO text +/// `TemporalConverter` writes: `TIMESTAMP` for an Instant or a LocalDateTime, +/// `DATE` for a LocalDate. +/// +/// Precision is the millisecond, as for a Date: nanoseconds below it are dropped +/// toward the earlier instant on write. +/// +/// Internal ORM runtime; not an application API. +/// @hidden +@com.codename1.impl.SharedWithBackend +public final class JavaTimeConverter { + private static final long DAY = 86400000L; + + private JavaTimeConverter() { + } + + /// An Instant as epoch milliseconds, the default. + /// @hidden + public static final class InstantMillis implements AttributeConverter { + @Override + public Long toDatabase(Instant value) { + return value == null ? null : Long.valueOf(value.toEpochMilli()); + } + + @Override + public Instant fromDatabase(Long value) { + return value == null ? null : Instant.ofEpochMilli(value.longValue()); + } + } + + /// An Instant in a native timestamp column, `@Temporal(TemporalType.TIMESTAMP)`. + /// @hidden + public static final class InstantTimestamp implements AttributeConverter { + private final TemporalConverter text = new TemporalConverter.Timestamp(); + + @Override + public String toDatabase(Instant value) { + return value == null ? null : text.toDatabase(new java.util.Date(value.toEpochMilli())); + } + + @Override + public Instant fromDatabase(String value) { + return value == null ? null : Instant.ofEpochMilli(TemporalConverter.parse(value)); + } + } + + /// A LocalDate as the day count since 1970-01-01, the default. + /// @hidden + public static final class LocalDateEpochDay implements AttributeConverter { + @Override + public Long toDatabase(LocalDate value) { + return value == null ? null : Long.valueOf(value.toEpochDay()); + } + + @Override + public LocalDate fromDatabase(Long value) { + return value == null ? null : LocalDate.ofEpochDay(value.longValue()); + } + } + + /// A LocalDate in a native date column, `@Temporal(TemporalType.DATE)`. + /// @hidden + public static final class LocalDateDay implements AttributeConverter { + private final TemporalConverter text = new TemporalConverter.Day(); + + @Override + public String toDatabase(LocalDate value) { + return value == null ? null : text.toDatabase(new java.util.Date(value.toEpochDay() * DAY)); + } + + @Override + public LocalDate fromDatabase(String value) { + return value == null ? null : LocalDate.ofEpochDay(floorDiv(TemporalConverter.parse(value), DAY)); + } + } + + /// A LocalDateTime's wall clock as milliseconds since 1970-01-01T00:00, the + /// default. + /// @hidden + public static final class LocalDateTimeMillis implements AttributeConverter { + @Override + public Long toDatabase(LocalDateTime value) { + return value == null ? null : Long.valueOf(millis(value)); + } + + @Override + public LocalDateTime fromDatabase(Long value) { + return value == null ? null : wallClock(value.longValue()); + } + } + + /// A LocalDateTime in a native timestamp column, + /// `@Temporal(TemporalType.TIMESTAMP)`. The wall clock is written as is; the + /// backend's UTC session means no zone ever shifts it. + /// @hidden + public static final class LocalDateTimeTimestamp implements AttributeConverter { + private final TemporalConverter text = new TemporalConverter.Timestamp(); + + @Override + public String toDatabase(LocalDateTime value) { + return value == null ? null : text.toDatabase(new java.util.Date(millis(value))); + } + + @Override + public LocalDateTime fromDatabase(String value) { + return value == null ? null : wallClock(TemporalConverter.parse(value)); + } + } + + /// Milliseconds of a wall clock, refusing what a long cannot hold: a + /// LocalDateTime reaches the year 999999999, and `LocalDateTime.MAX` as a + /// "no bound" value would otherwise wrap to a plausible wrong time. An + /// Instant needs no such check; its toEpochMilli throws by itself. + static long millis(LocalDateTime value) { + long days = value.toLocalDate().toEpochDay(); + if (days > Long.MAX_VALUE / DAY - 1 || days < Long.MIN_VALUE / DAY + 1) { + throw new IllegalArgumentException("A LocalDateTime does not fit in epoch milliseconds: " + value); + } + return days * DAY + value.toLocalTime().toNanoOfDay() / 1000000L; + } + + static LocalDateTime wallClock(long millis) { + long days = floorDiv(millis, DAY); + return LocalDateTime.of(LocalDate.ofEpochDay(days), LocalTime.ofNanoOfDay((millis - days * DAY) * 1000000L)); + } + + private static long floorDiv(long value, long divisor) { + long quotient = value / divisor; + return value % divisor != 0 && (value < 0) != (divisor < 0) ? quotient - 1 : quotient; + } +} diff --git a/docs/developer-guide/Annotation-SQLite-ORM.asciidoc b/docs/developer-guide/Annotation-SQLite-ORM.asciidoc index 878ad66d5db..44657ec1876 100644 --- a/docs/developer-guide/Annotation-SQLite-ORM.asciidoc +++ b/docs/developer-guide/Annotation-SQLite-ORM.asciidoc @@ -52,8 +52,10 @@ write a loaded value into it. Java type (`String -> TEXT`, `int/long -> INTEGER`, `float/double -> REAL`, `boolean -> INTEGER`, `java.util.Date -> INTEGER` (epoch millis), + `Instant`, `LocalDateTime -> INTEGER` (epoch millis), + `LocalDate -> INTEGER` (epoch day), `byte[] -> BLOB`). -| `@Temporal` | Stores a `java.util.Date` in a native date or time +| `@Temporal` | Stores a date or time field in a native date or time column instead of epoch milliseconds. See <>. | `@Column(nullable=false)`| Adds `NOT NULL` to the column declaration. @@ -454,7 +456,28 @@ represent exactly are rejected rather than converted. A value whose text carries a time-zone offset, such as one read from a PostgreSQL `timestamptz` column, reads as the instant it names. -`@Temporal` applies to entity fields of type `java.util.Date`. It can't be +`java.time.Instant`, `LocalDate`, and `LocalDateTime` fields map without an +annotation, to the same portable integer columns: + +[cols="1,2,2", options="header"] +|=== +| Field type | Default column | With `@Temporal` +| `Instant` | epoch milliseconds, the same value a `Date` or a `long` of `System.currentTimeMillis()` holds | `TIMESTAMP` +| `LocalDateTime` | milliseconds since `1970-01-01T00:00` of the wall clock, with no zone applied | `TIMESTAMP`, the wall clock as written +| `LocalDate` | days since `1970-01-01` | `DATE` +|=== + +Because an `Instant` stores the same number, changing a `long` or `Date` field of +epoch milliseconds to an `Instant` reads the rows already in the table. Pass an +`Instant`, `LocalDate`, or `LocalDateTime` as the query parameter for one of +these fields, not a number. Precision is milliseconds, as for a `Date`; finer +digits are dropped on write. A `LocalDate` takes only `@Temporal(TemporalType.DATE)` +and the other two only `TIMESTAMP`. `LocalTime`, `OffsetDateTime`, and +`ZonedDateTime` aren't mapped; store an `Instant` and keep the zone in its own +field when it matters. + +`@Temporal` applies to entity fields of type `java.util.Date`, `Instant`, +`LocalDate`, and `LocalDateTime`. It can't be combined with `@Convert`, used on an identifier or version, or applied to element-collection values. A field with `@Temporal` requires `openSession()`. An explicit `@Column(type)` on one must name a matching date type, such as diff --git a/maven/build-engine/src/main/java/com/codename1/maven/processors/OrmAnnotationProcessor.java b/maven/build-engine/src/main/java/com/codename1/maven/processors/OrmAnnotationProcessor.java index aa6a9e07e84..056bf99f25f 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/processors/OrmAnnotationProcessor.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/processors/OrmAnnotationProcessor.java @@ -460,7 +460,7 @@ private void processClass(AnnotatedClass cls, ProcessorContext ctx, boolean from if (relationship != null) { // Element collections and relationships keep their own column // format; a @Temporal there would be silently ignored. - if(f.getAnnotation("Lcom/codename1/annotations/db/Temporal;")!=null) { ctx.error(cls,"@Temporal applies to a single java.util.Date field, not a relationship or collection: "+fieldPath.path);continue; } + if(f.getAnnotation("Lcom/codename1/annotations/db/Temporal;")!=null) { ctx.error(cls,"@Temporal applies to a single date or time field, not a relationship or collection: "+fieldPath.path);continue; } if(fieldPath.prefix.length()>0) { ctx.error(cls,"Relationships inside embeddables are not supported: "+fieldPath.path);continue; } relationship.declaringType=fieldPath.declaringType;relationship.index=ec.relations.size(); ec.relations.add(relationship); continue; } PersistedField pf = new PersistedField(); @@ -489,15 +489,41 @@ private void processClass(AnnotatedClass cls, ProcessorContext ctx, boolean from // parameters all take the path a @Convert field already takes. Object value=temporal.get("value"); boolean day=value instanceof String[] && "DATE".equals(((String[])value)[1]); + String javaTime=JAVA_TIME_TYPES.get(f.getDescriptor()); if(conversion!=null) ctx.error(cls,"@Temporal cannot be combined with @Convert: "+pf.fieldName); - else if(!"Ljava/util/Date;".equals(f.getDescriptor())) ctx.error(cls,"@Temporal requires a java.util.Date field: "+pf.fieldName); + else if(!"Ljava/util/Date;".equals(f.getDescriptor()) && javaTime==null) ctx.error(cls,"@Temporal requires a java.util.Date, Instant, LocalDate or LocalDateTime field: "+pf.fieldName); else if(f.getAnnotation(ID_DESC)!=null || pf.version || ec.embeddedId!=null && pf.fieldName.startsWith(ec.embeddedId+".")) ctx.error(cls,"Identifier and version fields cannot be @Temporal: "+pf.fieldName); + // A LocalDate has no time to keep and the other two have no + // single day to be: the type already says which column it is. + else if(javaTime!=null && day!="LocalDate".equals(javaTime)) ctx.error(cls,"@Temporal("+(day?"DATE":"TIMESTAMP")+") does not hold a java.time."+javaTime+": "+pf.fieldName); else { - pf.converter="com.codename1.impl.orm.TemporalConverter."+(day?"Day":"Timestamp"); - pf.domainType="java.util.Date"; + pf.converter=javaTime==null?"com.codename1.impl.orm.TemporalConverter."+(day?"Day":"Timestamp") + :"com.codename1.impl.orm.JavaTimeConverter."+javaTime+(day?"Day":"Timestamp"); + pf.domainType=javaTime==null?"java.util.Date":"java.time."+javaTime; pf.kind=PropertyTypeKind.scalar("java.lang.String"); pf.temporal=day?1:2; } + } else if(conversion==null && JAVA_TIME_TYPES.containsKey(f.getDescriptor())) { + // java.time takes Date's portable default, a number in an integer + // column, through a built-in converter: an Instant reads what a + // long or a Date of milliseconds wrote. + String javaTime=JAVA_TIME_TYPES.get(f.getDescriptor()); + if(f.getAnnotation(ID_DESC)!=null || ec.embeddedId!=null && pf.fieldName.startsWith(ec.embeddedId+".")) { ctx.error(cls,"Identifier fields cannot be java.time values: "+pf.fieldName);continue; } + else if(pf.version) { ctx.error(cls,"Version fields cannot be java.time values: "+pf.fieldName);continue; } + else { + pf.converter="com.codename1.impl.orm.JavaTimeConverter."+javaTime+("LocalDate".equals(javaTime)?"EpochDay":"Millis"); + pf.domainType="java.time."+javaTime; + pf.kind=PropertyTypeKind.scalar("java.lang.Long"); + } + } + // A java.time field with no converter by now was refused above; it + // must not go on to be reported again as a relationship. + if(pf.converter==null && JAVA_TIME_TYPES.containsKey(f.getDescriptor())) continue; + // A Property holds its value through generated accessors that know + // only the scalar types; a converter has no hook there. + if(pf.kind.kind==PropertyTypeKind.Kind.PROPERTY && pf.kind.elementBinaryName!=null && pf.kind.elementBinaryName.startsWith("java.time.")) { + ctx.error(cls,"A Property of "+pf.kind.elementBinaryName+" is not supported; declare a plain java.time field instead: "+pf.fieldName); + continue; } AnnotatedClass enumClass = pf.kind.kind == PropertyTypeKind.Kind.REFERENCE ? findType(pf.kind.binaryName,ctx) : null; if (pf.kind.kind == PropertyTypeKind.Kind.REFERENCE && enumClass != null && enumClass.isEnum()) @@ -1135,6 +1161,15 @@ && lowerOrDigit(name.charAt(i-1)) && lowerOrDigit(name.charAt(i+1))) return out.toString().toLowerCase(java.util.Locale.ROOT); } + /// The java.time field types the ORM maps, by descriptor, to the simple name + /// JavaTimeConverter's nested converters start with. + private static final java.util.Map JAVA_TIME_TYPES=new java.util.HashMap(); + static { + JAVA_TIME_TYPES.put("Ljava/time/Instant;","Instant"); + JAVA_TIME_TYPES.put("Ljava/time/LocalDate;","LocalDate"); + JAVA_TIME_TYPES.put("Ljava/time/LocalDateTime;","LocalDateTime"); + } + /// Whether a declared column type holds a @Temporal value of the given kind /// (1 DATE, 2 TIMESTAMP), at millisecond precision. TEXT holds either: SQLite has no date type and /// schemas written for it declare their dates as TEXT. Mirrors SessionImpl's diff --git a/maven/build-engine/src/test/java/com/codename1/maven/processors/OrmAnnotationProcessorTest.java b/maven/build-engine/src/test/java/com/codename1/maven/processors/OrmAnnotationProcessorTest.java index 38f0e3447f5..c30b32639c4 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/processors/OrmAnnotationProcessorTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/processors/OrmAnnotationProcessorTest.java @@ -1399,7 +1399,100 @@ public void temporalFieldsRejectAColumnTypeThatCannotHoldThem() throws Exception + "@Entity public class Diary { @Id public long id; @Temporal(TemporalType.DATE) @ElementCollection public List days=new ArrayList(); }"); JavaSourceCompiler.compile(sources,classes,Arrays.asList(testClassesDir())); String errors=runProcessor(classes,Collections.emptyList()).getErrors().toString(); - assertTrue(errors,errors.contains("@Temporal applies to a single java.util.Date field, not a relationship or collection: days")); + assertTrue(errors,errors.contains("@Temporal applies to a single date or time field, not a relationship or collection: days")); + } + + /// Instant, LocalDate and LocalDateTime map without an annotation, to the + /// numbers a Date's default writes, and to native columns under @Temporal. + /// Both models store, load, bind a query parameter and order by each one. + @Test + public void javaTimeFieldsRoundTripAndQuery() throws Exception { + File classes=tmp.newFolder("javatime");Map sources=new java.util.LinkedHashMap(); + sources.put("javatime.Event","package javatime; import com.codename1.annotations.*; import com.codename1.annotations.db.*; import java.time.*; " + + "@Entity public class Event { @Id public long id; public Instant at; public LocalDate day; public LocalDateTime local; " + + "@Temporal(TemporalType.TIMESTAMP) public Instant nativeAt; @Temporal(TemporalType.DATE) public LocalDate nativeDay; @Temporal(TemporalType.TIMESTAMP) public LocalDateTime nativeLocal; public long rawAt; @Embedded public Span span=new Span(); }"); + sources.put("javatime.Span","package javatime; import com.codename1.annotations.db.*; import java.time.*; @Embeddable public class Span { public Instant until; }"); + JavaSourceCompiler.compile(sources,classes,Arrays.asList(testClassesDir())); + ProcessorContext client=runProcessor(classes);assertFalse(client.getErrors().toString(),client.hasErrors()); + ProcessorContext backend=runProcessor(classes,backendClasspath());assertFalse(backend.getErrors().toString(),backend.hasErrors()); + java.time.Instant early=java.time.Instant.parse("1969-07-20T20:17:40.123456Z"),late=java.time.Instant.parse("2026-10-24T09:30:00.250Z"); + try(java.net.URLClassLoader loader=new java.net.URLClassLoader(new URL[]{classes.toURI().toURL()},getClass().getClassLoader())) { + Class type=loader.loadClass("javatime.Event"); + for(String suffix:Arrays.asList("Cn1Model","Cn1BackendModel")) { + Map> models=new java.util.LinkedHashMap>();models.put(type.getName(),(com.codename1.impl.orm.EntityModel)loader.loadClass(type.getName()+suffix).newInstance()); + com.codename1.backend.Database db=com.codename1.backend.Database.open(":memory:");com.codename1.orm.session.Session session=new com.codename1.impl.orm.SessionImpl(new com.codename1.impl.orm.BackendSqlAccess(null,db,db.dialect()),models); + try { + session.createTables();session.beginTransaction(); + for(java.time.Instant at:Arrays.asList(late,early)) { + Object e=type.newInstance();java.time.LocalDateTime local=java.time.LocalDateTime.ofInstant(at,java.time.ZoneOffset.UTC); + type.getField("at").set(e,at);type.getField("nativeAt").set(e,at);type.getField("day").set(e,local.toLocalDate());type.getField("nativeDay").set(e,local.toLocalDate()); + type.getField("local").set(e,local);type.getField("nativeLocal").set(e,local);type.getField("rawAt").setLong(e,at.toEpochMilli()); + Object span=type.getField("span").get(e);span.getClass().getField("until").set(span,at);session.persist(e); + } + Object empty=type.newInstance();session.persist(empty);session.commitTransaction();session.clear(); + for(String field:Arrays.asList("at","nativeAt","day","nativeDay","local","nativeLocal")) { + java.util.List rows=session.createQuery("select e from Event e where e."+field+" is not null order by e."+field).list(); + assertEquals(suffix+" "+field,2,rows.size()); + Object first=rows.get(0),second=rows.get(1); + // Milliseconds are kept, finer digits are dropped. + java.time.Instant truncated=java.time.Instant.ofEpochMilli(early.toEpochMilli()); + Object expected=field.toLowerCase().endsWith("day")?java.time.LocalDate.of(1969,7,20) + :field.toLowerCase().endsWith("local")?java.time.LocalDateTime.ofInstant(truncated,java.time.ZoneOffset.UTC):truncated; + assertEquals(suffix+" "+field,expected,type.getField(field).get(first)); + Object after=session.createQuery("select e from Event e where e."+field+" > :v").setParameter("v",expected).first(); + assertEquals(suffix+" "+field,type.getField(field).get(second),type.getField(field).get(after)); + } + java.util.List spans=session.createQuery("select e from Event e where e.span.until < :v").setParameter("v",late).list(); + assertEquals(suffix,1,spans.size()); + Object loadedSpan=type.getField("span").get(spans.get(0)); + assertEquals(suffix,java.time.Instant.ofEpochMilli(early.toEpochMilli()),loadedSpan.getClass().getField("until").get(loadedSpan)); + // An aggregate of the field answers the field's type, not its storage. + assertEquals(suffix,java.time.Instant.ofEpochMilli(late.toEpochMilli()),session.createQuery("select max(e.at) from Event e").first()); + assertEquals(suffix,java.time.LocalDate.of(1969,7,20),session.createQuery("select min(e.nativeDay) from Event e").first()); + // The default Instant column is the epoch milliseconds a long holds. + assertEquals(1,session.createQuery("select e from Event e where e.rawAt = :v").setParameter("v",late.toEpochMilli()).list().size()); + Object loadedEmpty=session.find(type,type.getField("id").get(empty)); + for(String field:Arrays.asList("at","nativeAt","day","nativeDay","local","nativeLocal")) org.junit.Assert.assertNull(suffix+" "+field,type.getField(field).get(loadedEmpty)); + } finally { session.close();db.close(); } + } + } + String[][] rejected={{"@Temporal(TemporalType.DATE) public java.time.Instant at;","@Temporal(DATE) does not hold a java.time.Instant: at"}, + {"@Temporal(TemporalType.TIMESTAMP) public java.time.LocalDate at;","@Temporal(TIMESTAMP) does not hold a java.time.LocalDate: at"}, + {"@Temporal(TemporalType.DATE) public java.time.LocalDateTime at;","@Temporal(DATE) does not hold a java.time.LocalDateTime: at"}, + {"@Temporal public java.time.LocalTime at;","@Temporal requires a java.util.Date, Instant, LocalDate or LocalDateTime field: at"}}; + for(String[] c:rejected) { + File bad=tmp.newFolder();Map badSources=new java.util.LinkedHashMap(); + badSources.put("javatimebad.Entry","package javatimebad; import com.codename1.annotations.*; import com.codename1.annotations.db.*; @Entity public class Entry { @Id public long id; "+c[0]+" }"); + JavaSourceCompiler.compile(badSources,bad,Arrays.asList(testClassesDir())); + String errors=runProcessor(bad,Collections.emptyList()).getErrors().toString(); + assertTrue(errors,errors.contains(c[1])); + } + } + + /// Where java.time cannot go, the build says so once, in its own words -- + /// not as a relationship, and not as generated code that fails to compile. + @Test + public void javaTimeFieldsRejectUnsupportedPlacements() throws Exception { + String imports="package javatimeplace; import com.codename1.annotations.*; import com.codename1.annotations.db.*; import java.util.*; import java.time.*; "; + String[][] cases={ + {"@Id(autoIncrement=false) public Instant at; public long other;","Identifier fields cannot be java.time values: at"}, + {"@Id public long id; @Version public Instant at;","Version fields cannot be java.time values: at"}, + {"@Id public long id; public final com.codename1.properties.Property at=new com.codename1.properties.Property(\"at\");","A Property of java.time.Instant is not supported"}, + {"@Id(autoIncrement=false) @Temporal public Instant at; public long other;","Identifier and version fields cannot be @Temporal: at"}, + {"@Id public long id; @Temporal(TemporalType.DATE) public Instant at;","@Temporal(DATE) does not hold a java.time.Instant: at"}, + {"@Id public long id; @ElementCollection public List at=new ArrayList();","ElementCollection requires a supported scalar value: java.time.Instant"}}; + for(String[] c:cases) { + File classes=tmp.newFolder();Map sources=new java.util.LinkedHashMap(); + sources.put("javatimeplace.Entry",imports+"@Entity public class Entry { "+c[0]+" }"); + JavaSourceCompiler.compile(sources,classes,Arrays.asList(testClassesDir())); + String errors=runProcessor(classes,Collections.emptyList()).getErrors().toString(); + assertTrue(errors,errors.contains(c[1])); + assertFalse(errors,errors.contains("relationships are not yet supported")); + } + // A wall clock past what a long of milliseconds holds is refused, never wrapped. + org.junit.Assert.assertThrows(IllegalArgumentException.class,()->new com.codename1.impl.orm.JavaTimeConverter.LocalDateTimeMillis().toDatabase(java.time.LocalDateTime.MAX)); + org.junit.Assert.assertThrows(IllegalArgumentException.class,()->new com.codename1.impl.orm.JavaTimeConverter.LocalDateTimeMillis().toDatabase(java.time.LocalDateTime.MIN)); + org.junit.Assert.assertThrows(ArithmeticException.class,()->new com.codename1.impl.orm.JavaTimeConverter.InstantMillis().toDatabase(java.time.Instant.MAX)); } @Test diff --git a/vm/selfhost/perf-baseline/pr/6094.json b/vm/selfhost/perf-baseline/pr/6094.json new file mode 100644 index 00000000000..71539ff8303 --- /dev/null +++ b/vm/selfhost/perf-baseline/pr/6094.json @@ -0,0 +1,62 @@ +{ + "pr": 6094, + "rebaseline": { + "macos-arm64": { + "translator": { + "all": { + "from": { + "memory": 0.576, + "time": 0.623, + "tolerance": { + "memory": 0.35 + } + }, + "memory": 0.782, + "reason": "Nightly fold ab3505beb1 set translator all-cores RAM to 0.576 on macos-arm64; PRs with no vm/ changes measure 0.74 (#6081) and 0.79 (#6094), so the row was below the runner's normal spread", + "runs": 2, + "time": 0.623, + "tolerance": { + "memory": 0.35 + } + } + } + }, + "windows-x64@amd64-family-25-model-1-authenticamd": { + "translator": { + "all": { + "from": { + "memory": 0.542, + "time": 0.605, + "tolerance": { + "time": 0.35 + } + }, + "memory": 0.542, + "reason": "Translator all-cores time on this runner measures 0.83x (#6094) and 0.90x (#6098, no vm/ changes either) against a 0.60x row; the row is below the runner's normal spread", + "runs": 1, + "time": 0.833, + "tolerance": { + "time": 0.35 + } + } + } + }, + "windows-x64@amd64-family-25-model-17-authenticamd": { + "translator": { + "all": { + "from": { + "memory": 0.551, + "time": 0.701, + "tolerance": { + "time": 0.2 + } + }, + "memory": 0.551, + "reason": "Translator all-cores time on Windows x64 runners measures 0.83-0.90x on PRs with no vm/ changes (#6094, #6098) against rows the nightly fold set at 0.60-0.70x", + "runs": 1, + "time": 0.889 + } + } + } + } +}