From 9706c1f1e579844f89bc39d979851ee0065096b0 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 8 Jun 2021 21:17:55 +0000 Subject: Add support for extended date/time type in access 2019+ dbs git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1365 f203690c-595d-4dc9-a70b-905162fa7fd2 --- src/changes/changes.xml | 3 + .../com/healthmarketscience/jackcess/DataType.java | 8 ++ .../jackcess/impl/ColumnImpl.java | 110 +++++++++++++++++++-- .../jackcess/impl/IndexData.java | 47 +++++++++ .../jackcess/impl/JetFormat.java | 22 +++-- 5 files changed, 177 insertions(+), 13 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index e263ce4..ba48501 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -11,6 +11,9 @@ Add basic support for access 2019+ dbs. + + Add support for extended date/time type in access 2019+ dbs. + diff --git a/src/main/java/com/healthmarketscience/jackcess/DataType.java b/src/main/java/com/healthmarketscience/jackcess/DataType.java index d191dc1..fc6a792 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DataType.java +++ b/src/main/java/com/healthmarketscience/jackcess/DataType.java @@ -161,6 +161,14 @@ public enum DataType { * {@link Types#BIGINT}. */ BIG_INT((byte) 0x13, Types.BIGINT, 8), + /** + * Corresponds to a java {@link LocalDateTime} (with 7 digits of nanosecond + * precision). Accepts a Date, LocalDateTime (or related types), any + * {@link Number} (using {@link Number#longValue}), or {@code null}. + * Equivalent to SQL {@link Types#TIMESTAMP}, {@link Types#DATE}, + * {@link Types#TIME}. + */ + EXT_DATE_TIME((byte) 0x14, null, 42), /** * Dummy type for a fixed length type which is not currently supported. * Handled like a fixed length {@link #BINARY}. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index ff221a7..8387258 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -37,6 +37,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQueries; import java.util.Calendar; @@ -109,6 +110,12 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte public static final LocalTime BASE_LT = LocalTime.of(0, 0); public static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT); + private static final LocalDate BASE_EXT_LD = LocalDate.of(1, 1, 1); + private static final LocalTime BASE_EXT_LT = LocalTime.of(0, 0); + private static final LocalDateTime BASE_EXT_LDT = + LocalDateTime.of(BASE_EXT_LD, BASE_EXT_LT); + private static final byte[] EXT_LDT_TRAILER = {':', '7', 0x00}; + private static final DateTimeFactory DEF_DATE_TIME_FACTORY = new DefaultDateTimeFactory(); @@ -808,6 +815,8 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte return readNumericValue(buffer); case GUID: return readGUIDValue(buffer, order); + case EXT_DATE_TIME: + return readExtendedDateValue(buffer); case UNKNOWN_0D: case UNKNOWN_11: // treat like "binary" data @@ -966,6 +975,37 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte return getDateTimeFactory().fromDateBits(this, dateBits); } + /** + * Decodes an "extended" date/time value. + */ + private static Object readExtendedDateValue(ByteBuffer buffer) { + // format: <19digits>:<19digits>:7 0x00 + long numDays = readExtDateLong(buffer, 19); + buffer.get(); + long seconds = readExtDateLong(buffer, 12); + // there are 7 fractional digits + long nanos = readExtDateLong(buffer, 7) * 100L; + ByteUtil.forward(buffer, EXT_LDT_TRAILER.length); + + return BASE_EXT_LDT + .plusDays(numDays) + .plusSeconds(seconds) + .plusNanos(nanos); + } + + /** + * Reads the given number of ascii encoded characters as a long value. + */ + private static long readExtDateLong(ByteBuffer buffer, int numChars) { + long val = 0L; + for(int i = 0; i < numChars; ++i) { + char digit = (char)buffer.get(); + long inc = digit - '0'; + val = (val * 10L) + inc; + } + return val; + } + /** * Returns a java long time value converted from an access date double. * @usage _advanced_method_ @@ -1034,6 +1074,51 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte } } + /** + * Writes an "extended" date/time value. + */ + private void writeExtendedDateValue(ByteBuffer buffer, Object value) + throws InvalidValueException + { + LocalDateTime ldt = BASE_EXT_LDT; + if(value != null) { + ldt = toLocalDateTime(value, this); + } + + LocalDate ld = ldt.toLocalDate(); + LocalTime lt = ldt.toLocalTime(); + + long numDays = BASE_EXT_LD.until(ld, ChronoUnit.DAYS); + long numSeconds = BASE_EXT_LT.until(lt, ChronoUnit.SECONDS); + long nanos = lt.getNano(); + + // format: <19digits>:<19digits>:7 0x00 + writeExtDateLong(buffer, numDays, 19); + buffer.put((byte)':'); + writeExtDateLong(buffer, numSeconds, 12); + // there are 7 fractional digits + writeExtDateLong(buffer, (nanos / 100L), 7); + + buffer.put(EXT_LDT_TRAILER); + } + + /** + * Writes the given long value as the given number of ascii encoded + * characters. + */ + private static void writeExtDateLong( + ByteBuffer buffer, long val, int numChars) { + // we write the desired number of digits in reverse order + int end = buffer.position(); + int start = end + numChars - 1; + for(int i = start; i >= end; --i) { + char digit = (char)('0' + (char)(val % 10L)); + buffer.put(i, (byte)digit); + val /= 10L; + } + ByteUtil.forward(buffer, numChars); + } + /** * Returns an access date double converted from a java Date/Calendar/Number * time value. @@ -1059,6 +1144,15 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte } private static LocalDateTime toLocalDateTime( + Object value, DateTimeContext dtc) { + if(value instanceof TemporalAccessor) { + return temporalToLocalDateTime((TemporalAccessor)value, dtc); + } + Instant inst = Instant.ofEpochMilli(toDateLong(value)); + return LocalDateTime.ofInstant(inst, dtc.getZoneId()); + } + + private static LocalDateTime temporalToLocalDateTime( TemporalAccessor value, DateTimeContext dtc) { // handle some common Temporal types @@ -1117,7 +1211,8 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte if(value instanceof Instant) { return (Instant)value; } - return toLocalDateTime(value, dtc).atZone(dtc.getZoneId()).toInstant(); + return temporalToLocalDateTime(value, dtc).atZone(dtc.getZoneId()) + .toInstant(); } static double toLocalDateDouble(long time) { @@ -1464,6 +1559,9 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte case BIG_INT: buffer.putLong(toNumber(obj).longValue()); break; + case EXT_DATE_TIME: + writeExtendedDateValue(buffer, obj); + break; case UNSUPPORTED_FIXEDLEN: byte[] bytes = toByteArray(obj); if(bytes.length != getLength()) { @@ -2188,6 +2286,8 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte case BIG_INT: return ((value instanceof Long) ? value : toNumber(value, db).longValue()); + case EXT_DATE_TIME: + return toLocalDateTime(value, db); default: // some variation of binary data return toByteArray(value); @@ -2756,16 +2856,12 @@ public class ColumnImpl implements Column, Comparable, DateTimeConte value = Instant.ofEpochMilli(toDateLong(value)); } return ColumnImpl.toDateDouble( - toLocalDateTime((TemporalAccessor)value, dtc)); + temporalToLocalDateTime((TemporalAccessor)value, dtc)); } @Override public Object toInternalValue(DatabaseImpl db, Object value) { - if(value instanceof TemporalAccessor) { - return toLocalDateTime((TemporalAccessor)value, db); - } - Instant inst = Instant.ofEpochMilli(toDateLong(value)); - return LocalDateTime.ofInstant(inst, db.getZoneId()); + return toLocalDateTime(value, db); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java index bc2d111..5afe894 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -73,6 +73,11 @@ public class IndexData { protected static final byte[] EMPTY_PREFIX = new byte[0]; + private static final byte[] ASC_EXT_DATE_TRAILER = + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}; + private static final byte[] DESC_EXT_DATE_TRAILER = + flipBytes(ByteUtil.copyOf(ASC_EXT_DATE_TRAILER, ASC_EXT_DATE_TRAILER.length)); + static final short COLUMN_UNUSED = -1; public static final byte ASCENDING_COLUMN_FLAG = (byte)0x01; @@ -1550,6 +1555,8 @@ public class IndexData { return new GuidColumnDescriptor(col, flags); case BINARY: return new BinaryColumnDescriptor(col, flags); + case EXT_DATE_TIME: + return new ExtDateColumnDescriptor(col, flags); default: // we can't modify this index at this point in time @@ -1980,6 +1987,46 @@ public class IndexData { } } + /** + * ColumnDescriptor for extended date/time based columns. + */ + private static final class ExtDateColumnDescriptor extends ColumnDescriptor + { + private ExtDateColumnDescriptor(ColumnImpl column, byte flags) + throws IOException + { + super(column, flags); + } + + @Override + protected void writeNonNullValue(Object value, ByteStream bout) + throws IOException + { + byte[] valueBytes = encodeNumberColumnValue(value, getColumn()); + + // entry (which is 42 bytes of data) is encoded in blocks of 8 bytes + // separated by '\t' char + + // note that for desc, all bytes are flipped _except_ separator char + byte[] trailer = ASC_EXT_DATE_TRAILER; + if(!isAscending()) { + flipBytes(valueBytes); + trailer = DESC_EXT_DATE_TRAILER; + } + + // first 5 blocks are all value data + int valIdx = 0; + for(int i = 0; i < 5; ++i) { + bout.write(valueBytes, valIdx, 8); + bout.write((byte)0x09); + valIdx += 8; + } + + // last two data bytes and then the trailer + bout.write(valueBytes, valIdx, 2); + bout.write(trailer); + } + } /** * ColumnDescriptor for columns which we cannot currently write. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java b/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java index ff3c00f..bd05eaf 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java @@ -155,6 +155,18 @@ public abstract class JetFormat { V16_CALC_TYPES.addAll(V14_CALC_TYPES); } + private static final Set V16_UNSUPP_TYPES = + EnumSet.of(DataType.EXT_DATE_TIME); + private static final Set V12_UNSUPP_TYPES = + EnumSet.of(DataType.BIG_INT); + private static final Set V3_UNSUPP_TYPES = + EnumSet.of(DataType.COMPLEX_TYPE); + + static { + V12_UNSUPP_TYPES.addAll(V16_UNSUPP_TYPES); + V3_UNSUPP_TYPES.addAll(V12_UNSUPP_TYPES); + } + /** the JetFormat constants for the Jet database version "3" */ public static final JetFormat VERSION_3 = new Jet3Format(); /** the JetFormat constants for the Jet database version "4" */ @@ -769,8 +781,7 @@ public abstract class JetFormat { @Override public boolean isSupportedDataType(DataType type) { - return ((type != DataType.COMPLEX_TYPE) && - (type != DataType.BIG_INT)); + return !V3_UNSUPP_TYPES.contains(type); } @Override @@ -1006,8 +1017,7 @@ public abstract class JetFormat { @Override public boolean isSupportedDataType(DataType type) { - return ((type != DataType.COMPLEX_TYPE) && - (type != DataType.BIG_INT)); + return !V3_UNSUPP_TYPES.contains(type); } @Override @@ -1063,7 +1073,7 @@ public abstract class JetFormat { @Override public boolean isSupportedDataType(DataType type) { - return (type != DataType.BIG_INT); + return !V12_UNSUPP_TYPES.contains(type); } @Override @@ -1114,7 +1124,7 @@ public abstract class JetFormat { @Override public boolean isSupportedDataType(DataType type) { - return true; + return !V16_UNSUPP_TYPES.contains(type); } @Override -- cgit v1.2.3