]> source.dussan.org Git - jackcess.git/commitdiff
Add support for extended date/time type in access 2019+ dbs
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 8 Jun 2021 21:17:55 +0000 (21:17 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 8 Jun 2021 21:17:55 +0000 (21:17 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1365 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/DataType.java
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java
src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java

index e263ce48c8355e15370ec42929d1af8dc05b59dd..ba48501b15232fb931b5706a86072b45e7b337c0 100644 (file)
@@ -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">
index d191dc1612327ddd4884112a5265b8cf48704357..fc6a7921771c30a06ff79231a7e97776b0512bb9 100644 (file)
@@ -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}.
index ff221a7379722883b39951b7876db76a00627b70..8387258600596c143227ac0a6f513d8c78a0a030 100644 (file)
@@ -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
@@ -966,6 +975,37 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl>, 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<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.
@@ -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);
     }
   }
 
index bc2d111c9b3da8294edcd1d3c5897cf7d3521bcb..5afe8943b53d92c8113d61ff9603fa4fbd81854a 100644 (file)
@@ -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.
index ff3c00f36be848a99b39a128409404624427e51d..bd05eaf4bf11887a6b6c38f2e20a2688fdffd07b 100644 (file)
@@ -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