aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/changes/changes.xml3
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/DataType.java8
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java110
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java47
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java22
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 @@
<action dev="jahlborn" type="update">
Add basic support for access 2019+ dbs.
</action>
+ <action dev="jahlborn" type="update">
+ Add support for extended date/time type in access 2019+ dbs.
+ </action>
</release>
<release version="4.0.0" date="2021-01-20">
<action dev="jahlborn" type="update">
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
@@ -162,6 +162,14 @@ public enum DataType {
*/
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<ColumnImpl>, 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<ColumnImpl>, 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
@@ -967,6 +976,37 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, DateTimeConte
}
/**
+ * 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_
*/
@@ -1035,6 +1075,51 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, 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.
* @usage _advanced_method_
@@ -1059,6 +1144,15 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, 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<ColumnImpl>, 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<ColumnImpl>, 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<ColumnImpl>, 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<ColumnImpl>, 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<DataType> V16_UNSUPP_TYPES =
+ EnumSet.of(DataType.EXT_DATE_TIME);
+ private static final Set<DataType> V12_UNSUPP_TYPES =
+ EnumSet.of(DataType.BIG_INT);
+ private static final Set<DataType> 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