All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.kernel.impl.index.schema.GenericKey Maven / Gradle / Ivy

/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.kernel.impl.index.schema;

import org.neo4j.gis.spatial.index.curves.SpaceFillingCurve;
import org.neo4j.io.pagecache.PageCursor;
import org.neo4j.kernel.impl.index.schema.config.IndexSpecificSpaceFillingCurveSettings;
import org.neo4j.string.UTF8;
import org.neo4j.values.storable.CoordinateReferenceSystem;
import org.neo4j.values.storable.PrimitiveArrayWriting;
import org.neo4j.values.storable.TextValue;
import org.neo4j.values.storable.TimeZones;
import org.neo4j.values.storable.Value;
import org.neo4j.values.storable.ValueGroup;

import static java.lang.String.format;
import static org.neo4j.io.pagecache.PageCache.PAGE_SIZE;
import static org.neo4j.kernel.impl.index.schema.NativeIndexKey.Inclusion.HIGH;
import static org.neo4j.kernel.impl.index.schema.NativeIndexKey.Inclusion.LOW;
import static org.neo4j.kernel.impl.index.schema.NativeIndexKey.Inclusion.NEUTRAL;
import static org.neo4j.kernel.impl.index.schema.Type.booleanOf;
import static org.neo4j.kernel.impl.index.schema.Types.HIGHEST_BY_VALUE_GROUP;
import static org.neo4j.kernel.impl.index.schema.Types.LOWEST_BY_VALUE_GROUP;

/**
 * A key instance which can handle all types of single values, i.e. not composite keys, but all value types.
 * See {@link CompositeGenericKey} for implementation which supports composite keys.
 *
 * Regarding why the "internal" versions of some methods which are overridden by the CompositeGenericKey sub-class. Example:
 * - Consider a method a() which is used by some part of the implementation of the generic index provider.
 * - Sometimes the instance the method is called on will be a CompositeGenericKey.
 * - CompositeGenericKey overrides a() to loop over multiple state slots. Each slot is a GenericKey too.
 * - Simply overriding a() and call slot[i].a() would result in StackOverflowError since it would be calling itself.
 * This is why aInternal() exists and GenericKey#a() is implemented by simply forwarding to aInternal().
 * CompositeGenericKey#a() is implemented by looping over multiple GenericKey instances, also calling aInternal() in each of those, instead of a().
 */
public class GenericKey extends NativeIndexKey
{
    /**
     * This is the biggest size a static (as in non-dynamic, like string), non-array value can have.
     */
    static final int BIGGEST_STATIC_SIZE = Long.BYTES * 4; // long0, long1, long2, long3

    // TODO copy-pasted from individual keys
    // TODO also put this in Type enum
    public static final int SIZE_GEOMETRY_HEADER = 3;              /* 2b tableId and 22b code */
    public static final int SIZE_GEOMETRY =        Long.BYTES;     /* rawValueBits */
    static final int SIZE_GEOMETRY_COORDINATE =    Long.BYTES;     /* one coordinate */
    public static final int SIZE_ZONED_DATE_TIME = Long.BYTES +    /* epochSecond */
                                                   Integer.BYTES + /* nanoOfSecond */
                                                   Integer.BYTES;  /* timeZone */
    public static final int SIZE_LOCAL_DATE_TIME = Long.BYTES +    /* epochSecond */
                                                   Integer.BYTES;  /* nanoOfSecond */
    public static final int SIZE_DATE =            Long.BYTES;     /* epochDay */
    public static final int SIZE_ZONED_TIME =      Long.BYTES +    /* nanosOfDayUTC */
                                                   Integer.BYTES;  /* zoneOffsetSeconds */
    public static final int SIZE_LOCAL_TIME =      Long.BYTES;     /* nanoOfDay */
    public static final int SIZE_DURATION =        Long.BYTES +    /* totalAvgSeconds */
                                                   Integer.BYTES + /* nanosOfSecond */
                                                   Long.BYTES +    /* months */
                                                   Long.BYTES;     /* days */
    public static final int SIZE_STRING_LENGTH =   Short.BYTES;    /* length of string byte array */
    public static final int SIZE_BOOLEAN =         Byte.BYTES;     /* byte for this boolean value */
    public static final int SIZE_NUMBER_TYPE =     Byte.BYTES;     /* type of value */
    public static final int SIZE_NUMBER_BYTE =     Byte.BYTES;     /* raw value bits */
    public static final int SIZE_NUMBER_SHORT =    Short.BYTES;    /* raw value bits */
    public static final int SIZE_NUMBER_INT =      Integer.BYTES;  /* raw value bits */
    public static final int SIZE_NUMBER_LONG =     Long.BYTES;     /* raw value bits */
    public static final int SIZE_NUMBER_FLOAT =    Integer.BYTES;  /* raw value bits */
    public static final int SIZE_NUMBER_DOUBLE =   Long.BYTES;     /* raw value bits */
    public static final int SIZE_ARRAY_LENGTH =    Short.BYTES;
    static final int BIGGEST_REASONABLE_ARRAY_LENGTH = PAGE_SIZE / SIZE_NUMBER_BYTE;

    static final long TRUE = 1;
    static final long FALSE = 0;
    static final int NO_ENTITY_ID = -1;
    public static final int TYPE_ID_SIZE = Byte.BYTES;
    /**
     * An average month is 30 days, 10 hours and 30 minutes.
     * In seconds this is (((30 * 24) + 10) * 60 + 30) * 60 = 2629800
     */
    static final long AVG_MONTH_SECONDS = 2_629_800;
    static final long AVG_DAY_SECONDS = 86_400;
    private static final double[] NO_COORDINATES = new double[0];

    // Immutable
    private final IndexSpecificSpaceFillingCurveSettings settings;

    // Mutable, meta-state
    Type type;
    NativeIndexKey.Inclusion inclusion;
    boolean isArray;

    // Mutable, non-array values
    long long0;
    long long1;
    long long2;
    long long3;
    byte[] byteArray;

    // Mutable, array values
    long[] long0Array;
    long[] long1Array;
    long[] long2Array;
    long[] long3Array;
    byte[][] byteArrayArray;
    boolean isHighestArray;
    int arrayLength;
    int currentArrayOffset;

    // Mutable, spatial values
    /*
     * Settings for a specific crs retrieved from #settings using #long1 and #long2.
     */
    SpaceFillingCurve spaceFillingCurve;

    GenericKey( IndexSpecificSpaceFillingCurveSettings settings )
    {
        this.settings = settings;
    }

    /*  */
    void clear()
    {
        if ( type == Types.TEXT && booleanOf( long1 ) )
        {
            // Clear byteArray if it has been dereferenced
            byteArray = null;
        }
        type = null;
        long0 = 0;
        long1 = 0;
        long2 = 0;
        long3 = 0;
        inclusion = NEUTRAL;
        isArray = false;
        arrayLength = 0;
        isHighestArray = false;
        currentArrayOffset = 0;
        spaceFillingCurve = null;
    }

    void initializeToDummyValue()
    {
        setEntityId( Long.MIN_VALUE );
        initializeToDummyValueInternal();
    }

    void initializeToDummyValueInternal()
    {
        clear();
        writeInteger( 0 );
        inclusion = NEUTRAL;
    }

    void initValueAsLowest( ValueGroup valueGroup )
    {
        clear();
        type = valueGroup == ValueGroup.UNKNOWN ? LOWEST_BY_VALUE_GROUP : Types.BY_GROUP[valueGroup.ordinal()];
        type.initializeAsLowest( this );
    }

    void initValueAsHighest( ValueGroup valueGroup )
    {
        clear();
        type = valueGroup == ValueGroup.UNKNOWN ? HIGHEST_BY_VALUE_GROUP : Types.BY_GROUP[valueGroup.ordinal()];
        type.initializeAsHighest( this );
    }

    void initAsPrefixLow( TextValue prefix )
    {
        prefix.writeTo( this );
        long2 = FALSE;
        inclusion = LOW;
        // Don't set ignoreLength = true here since the "low" a.k.a. left side of the range should care about length.
        // This will make the prefix lower than those that matches the prefix (their length is >= that of the prefix)
    }

    void initAsPrefixHigh( TextValue prefix )
    {
        prefix.writeTo( this );
        long2 = TRUE; // ignoreLength
        inclusion = HIGH;
    }

    /*  */
    void copyFrom( GenericKey key )
    {
        setEntityId( key.getEntityId() );
        setCompareId( key.getCompareId() );
        copyFromInternal( key );
    }

    void copyFromInternal( GenericKey key )
    {
        copyMetaFrom( key );
        type.copyValue( this, key );
    }

    void copyMetaFrom( GenericKey key )
    {
        this.type = key.type;
        this.inclusion = key.inclusion;
        this.isArray = key.isArray;
        if ( key.isArray )
        {
            this.arrayLength = key.arrayLength;
            this.currentArrayOffset = key.currentArrayOffset;
            this.isHighestArray = key.isHighestArray;
        }
    }

    void writeValue( Value value, NativeIndexKey.Inclusion inclusion )
    {
        isArray = false;
        value.writeTo( this );
        this.inclusion = inclusion;
    }

    @Override
    void writeValue( int stateSlot, Value value, Inclusion inclusion )
    {
        writeValue( value, inclusion );
    }

    @Override
    void assertValidValue( int stateSlot, Value value )
    {
        // No need, we can handle all values
    }

    @Override
    Value[] asValues()
    {
        return new Value[] {asValue()};
    }

    @Override
    void initValueAsLowest( int stateSlot, ValueGroup valueGroup )
    {
        initValueAsLowest( valueGroup );
    }

    @Override
    void initValueAsHighest( int stateSlot, ValueGroup valueGroup )
    {
        initValueAsHighest( valueGroup );
    }

    GenericKey stateSlot( int slot )
    {
        assert slot == 0;
        return this;
    }

    @Override
    int numberOfStateSlots()
    {
        return 1;
    }

    @Override
    int compareValueTo( GenericKey other )
    {
        return compareValueToInternal( other );
    }

    int compareValueToInternal( GenericKey other )
    {
        if ( type != other.type )
        {
            // These null checks guard for inconsistent reading where we're expecting a retry to occur
            // Unfortunately it's the case that SeekCursor calls these methods inside a shouldRetry.
            // Fortunately we only need to do these checks if the types aren't equal, and one of the two
            // are guaranteed to be a "real" state, i.e. not inside a shouldRetry.
            if ( type == null )
            {
                return -1;
            }
            if ( other.type == null )
            {
                return 1;
            }
            return Type.COMPARATOR.compare( type, other.type );
        }

        int valueComparison = type.compareValue( this, other );
        if ( valueComparison != 0 )
        {
            return valueComparison;
        }

        return inclusion.compareTo( other.inclusion );
    }

    void minimalSplitter( GenericKey left, GenericKey right, GenericKey into )
    {
        into.setCompareId( right.getCompareId() );
        if ( left.compareValueTo( right ) != 0 )
        {
            into.setEntityId( NO_ENTITY_ID );
        }
        else
        {
            // There was no minimal splitter to be found so entity id will serve as divider
            into.setEntityId( right.getEntityId() );
        }
        minimalSplitterInternal( left, right, into );
    }

    void minimalSplitterInternal( GenericKey left, GenericKey right, GenericKey into )
    {
        into.clear();
        into.copyMetaFrom( right );
        right.type.minimalSplitter( left, right, into );
    }

    int size()
    {
        return ENTITY_ID_SIZE + sizeInternal();
    }

    int sizeInternal()
    {
        return type.valueSize( this ) + TYPE_ID_SIZE;
    }

    Value asValue()
    {
        return type.asValue( this );
    }

    void put( PageCursor cursor )
    {
        cursor.putLong( getEntityId() );
        putInternal( cursor );
    }

    void putInternal( PageCursor cursor )
    {
        cursor.putByte( type.typeId );
        type.putValue( cursor, this );
    }

    boolean get( PageCursor cursor, int size )
    {
        if ( size < ENTITY_ID_SIZE )
        {
            initializeToDummyValue();
            cursor.setCursorException( format( "Failed to read " + getClass().getSimpleName() +
                    " due to keySize < ENTITY_ID_SIZE, more precisely %d", size ) );
            return false;
        }

        initialize( cursor.getLong() );
        if ( !getInternal( cursor, size ) )
        {
            initializeToDummyValue();
            return false;
        }
        return true;
    }

    boolean getInternal( PageCursor cursor, int size )
    {
        if ( size <= TYPE_ID_SIZE )
        {
            setCursorException( cursor, "slot size less than TYPE_ID_SIZE, " + size );
            return false;
        }

        byte typeId = cursor.getByte();
        if ( typeId < 0 || typeId >= Types.BY_ID.length )
        {
            setCursorException( cursor, "non-valid typeId, " + typeId );
            return false;
        }

        inclusion = NEUTRAL;
        return setType( Types.BY_ID[typeId] ).readValue( cursor, size - TYPE_ID_SIZE, this );
    }

    /*  (write to field state from Value or cursor) */

    private  T setType( T type )
    {
        if ( this.type != null && type != this.type )
        {
            clear();
        }
        this.type = type;
        return type;
    }

    @Override
    protected void writeDate( long epochDay )
    {
        if ( !isArray )
        {
            setType( Types.DATE );
            DateType.write( this, epochDay );
        }
        else
        {
            DateArrayType.write( this, currentArrayOffset++, epochDay );
        }
    }

    @Override
    protected void writeLocalTime( long nanoOfDay )
    {
        if ( !isArray )
        {
            setType( Types.LOCAL_TIME );
            LocalTimeType.write( this, nanoOfDay );
        }
        else
        {
            LocalTimeArrayType.write( this, currentArrayOffset++, nanoOfDay );
        }
    }

    @Override
    protected void writeTime( long nanosOfDayUTC, int offsetSeconds )
    {
        if ( !isArray )
        {
            setType( Types.ZONED_TIME );
            ZonedTimeType.write( this, nanosOfDayUTC, offsetSeconds );
        }
        else
        {
            ZonedTimeArrayType.write( this, currentArrayOffset++, nanosOfDayUTC, offsetSeconds );
        }
    }

    @Override
    protected void writeLocalDateTime( long epochSecond, int nano )
    {
        if ( !isArray )
        {
            setType( Types.LOCAL_DATE_TIME );
            LocalDateTimeType.write( this, epochSecond, nano );
        }
        else
        {
            LocalDateTimeArrayType.write( this, currentArrayOffset++, epochSecond, nano );
        }
    }

    @Override
    protected void writeDateTime( long epochSecondUTC, int nano, int offsetSeconds )
    {
        writeDateTime( epochSecondUTC, nano, (short) -1, offsetSeconds );
    }

    @Override
    protected void writeDateTime( long epochSecondUTC, int nano, String zoneId )
    {
        writeDateTime( epochSecondUTC, nano, TimeZones.map( zoneId ) );
    }

    protected void writeDateTime( long epochSecondUTC, int nano, short zoneId )
    {
        writeDateTime( epochSecondUTC, nano, zoneId, 0 );
    }

    private void writeDateTime( long epochSecondUTC, int nano, short zoneId, int offsetSeconds )
    {
        if ( !isArray )
        {
            setType( Types.ZONED_DATE_TIME );
            ZonedDateTimeType.write( this, epochSecondUTC, nano, zoneId, offsetSeconds );
        }
        else
        {
            ZonedDateTimeArrayType.write( this, currentArrayOffset++, epochSecondUTC, nano, zoneId, offsetSeconds );
        }
    }

    @Override
    public void writeBoolean( boolean value )
    {
        if ( !isArray )
        {
            setType( Types.BOOLEAN );
            BooleanType.write( this, value );
        }
        else
        {
            BooleanArrayType.write( this, currentArrayOffset++, value );
        }
    }

    private void writeNumber( long value, byte numberType )
    {
        if ( !isArray )
        {
            setType( Types.NUMBER );
            NumberType.write( this, value, numberType );
        }
        else
        {
            NumberArrayType.write( this, currentArrayOffset++, value );
        }
    }

    @Override
    public void writeInteger( byte value )
    {
        writeNumber( value, RawBits.BYTE );
    }

    @Override
    public void writeInteger( short value )
    {
        writeNumber( value, RawBits.SHORT );
    }

    @Override
    public void writeInteger( int value )
    {
        writeNumber( value, RawBits.INT );
    }

    static short toNonNegativeShortExact( long value )
    {
        if ( (value & ~0x7FFF) != 0 )
        {
            throw new IllegalArgumentException( value + " is bigger than maximum for a signed short (2B) " + 0x7FFF );
        }
        return (short) value;
    }

    @Override
    public void writeInteger( long value )
    {
        writeNumber( value, RawBits.LONG );
    }

    @Override
    public void writeFloatingPoint( float value )
    {
        writeNumber( Float.floatToIntBits( value ), RawBits.FLOAT );
    }

    @Override
    public void writeFloatingPoint( double value )
    {
        writeNumber( Double.doubleToLongBits( value ), RawBits.DOUBLE );
    }

    @Override
    public void writeString( String value )
    {
        writeStringBytes( UTF8.encode( value ), false );
    }

    @Override
    public void writeString( char value )
    {
        writeStringBytes( UTF8.encode( String.valueOf( value ) ), true );
    }

    @Override
    public void writeUTF8( byte[] bytes, int offset, int length )
    {
        byte[] dest = new byte[length];
        System.arraycopy( bytes, offset, dest, 0, length );
        writeStringBytes( dest, false );
    }

    private void writeStringBytes( byte[] bytes, boolean isCharType )
    {
        if ( !isArray )
        {
            setType( Types.TEXT );
            TextType.write( this, bytes, isCharType );
        }
        else
        {
            // in the array case we've already noted the char/string type in beginArray
            TextArrayType.write( this, currentArrayOffset++, bytes );
        }
        long1 = FALSE; // long1 is dereferenced true/false
    }

    @Override
    public void writeDuration( long months, long days, long seconds, int nanos )
    {
        long totalAvgSeconds = months * AVG_MONTH_SECONDS + days * AVG_DAY_SECONDS + seconds;
        writeDurationWithTotalAvgSeconds( months, days, totalAvgSeconds, nanos );
    }

    @Override
    public void writePoint( CoordinateReferenceSystem crs, double[] coordinate )
    {
        if ( !isArray )
        {
            setType( Types.GEOMETRY );
            updateCurve( crs.getTable().getTableId(), crs.getCode() );
            GeometryType.write( this, spaceFillingCurve.derivedValueFor( coordinate ), coordinate );
        }
        else
        {
            if ( currentArrayOffset == 0 )
            {
                updateCurve( crs.getTable().getTableId(), crs.getCode() );
            }
            else if ( this.long1 != crs.getTable().getTableId() || this.long2 != crs.getCode() )
            {
                throw new IllegalStateException( format(
                        "Tried to assign a geometry array containing different coordinate reference systems, first:%s, violating:%s at array position:%d",
                        CoordinateReferenceSystem.get( (int) long1, (int) long2 ), crs, currentArrayOffset ) );
            }
            GeometryArrayType.write( this, currentArrayOffset++, spaceFillingCurve.derivedValueFor( coordinate ), coordinate );
        }
    }

    void writePointDerived( CoordinateReferenceSystem crs, long derivedValue, NativeIndexKey.Inclusion inclusion )
    {
        if ( isArray )
        {
            throw new IllegalStateException( "This method is intended to be called when querying, where one or more sub-ranges are derived " +
                    "from a queried range and each sub-range written to separate keys. " +
                    "As such it's unexpected that this key state thinks that it's holds state for an array" );
        }
        setType( Types.GEOMETRY );
        updateCurve( crs.getTable().getTableId(), crs.getCode() );
        GeometryType.write( this, derivedValue, NO_COORDINATES );
        this.inclusion = inclusion;
    }

    private void updateCurve( int tableId, int code )
    {
        if ( this.long1 != tableId || this.long2 != code )
        {
            long1 = tableId;
            long2 = code;
            spaceFillingCurve = settings.forCrs( tableId, code );
        }
    }

    void writeDurationWithTotalAvgSeconds( long months, long days, long totalAvgSeconds, int nanos )
    {
        if ( !isArray )
        {
            setType( Types.DURATION );
            DurationType.write( this, months, days, totalAvgSeconds, nanos );
        }
        else
        {
            DurationArrayType.write( this, currentArrayOffset++, months, days, totalAvgSeconds, nanos );
        }
    }
    // Write byte array is a special case,

    // instead of calling beginArray and writing the bytes one-by-one
    // writeByteArray is called so that the bytes can be written in batches.
    // We don't care about that though so just delegate.
    @Override
    public void writeByteArray( byte[] value )
    {
        PrimitiveArrayWriting.writeTo( this, value );
    }

    @Override
    public void beginArray( int size, ArrayType arrayType )
    {
        AbstractArrayType arrayValueType = Types.BY_ARRAY_TYPE[arrayType.ordinal()];
        setType( arrayValueType );
        initializeArrayMeta( size );
        arrayValueType.initializeArray( this, size, arrayType );
    }

    void initializeArrayMeta( int size )
    {
        isArray = true;
        arrayLength = size;
        currentArrayOffset = 0;
    }

    @Override
    public void endArray()
    {   // no-op
    }
    /*  */

    /*  */

    @Override
    public String toString()
    {
        return "[" + toStringInternal() + "],entityId=" + getEntityId();
    }

    String toStringInternal()
    {
        return type.toString( this );
    }

    String toDetailedString()
    {
        return "[" + toDetailedStringInternal() + "],entityId=" + getEntityId();
    }

    String toDetailedStringInternal()
    {
        return type.toDetailedString( this );
    }

    static void setCursorException( PageCursor cursor, String reason )
    {
        cursor.setCursorException( format( "Unable to read generic key slot due to %s", reason ) );
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy