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

org.neo4j.kernel.builtinprocs.SchemaCalculator Maven / Gradle / Ivy

/*
 * Copyright (c) 2018-2020 "Graph Foundation,"
 * Graph Foundation, Inc. [https://graphfoundation.org]
 *
 * This file is part of ONgDB.
 *
 * ONgDB 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 .
 */
/*
 * Copyright (c) 2002-2020 "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.builtinprocs;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.neo4j.helpers.collection.Pair;
import org.neo4j.internal.kernel.api.CursorFactory;
import org.neo4j.internal.kernel.api.NamedToken;
import org.neo4j.internal.kernel.api.NodeCursor;
import org.neo4j.internal.kernel.api.PropertyCursor;
import org.neo4j.internal.kernel.api.Read;
import org.neo4j.internal.kernel.api.RelationshipScanCursor;
import org.neo4j.internal.kernel.api.TokenRead;
import org.neo4j.internal.kernel.api.Transaction;
import org.neo4j.values.storable.Value;

public class SchemaCalculator
{
    private Map propertyIdToPropertyNameMapping;

    private final Set emptyPropertyIdSet = Collections.unmodifiableSet( Collections.emptySet() );

    private final Read dataRead;
    private final TokenRead tokenRead;
    private final CursorFactory cursors;

    SchemaCalculator( Transaction ktx )
    {
        this.dataRead = ktx.dataRead();
        this.tokenRead = ktx.tokenRead();
        this.cursors = ktx.cursors();

        // the only one that is common for both nodes and rels so thats why we can do it here
        propertyIdToPropertyNameMapping = new HashMap<>( tokenRead.propertyKeyCount() );
        addNamesToCollection( tokenRead.propertyKeyGetAllTokens(), propertyIdToPropertyNameMapping );
    }

    private NodeMappings initializeMappingsForNodes()
    {
        int labelCount = tokenRead.labelCount();
        return new NodeMappings( labelCount );
    }

    private RelationshipMappings initializeMappingsForRels()
    {
        int relationshipTypeCount = tokenRead.relationshipTypeCount();
        return new RelationshipMappings( relationshipTypeCount );
    }

    // If we would have this schema information in the count store (or somewhere), this could be super fast
    public Stream calculateTabularResultStreamForNodes()
    {
        NodeMappings nodeMappings = initializeMappingsForNodes();
        scanEverythingBelongingToNodes(nodeMappings);

        // go through all labels to get actual names
        addNamesToCollection( tokenRead.labelsGetAllTokens(), nodeMappings.labelIdToLabelName );

        return produceResultsForNodes( nodeMappings ).stream();
    }

    public Stream calculateTabularResultStreamForRels()
    {
        RelationshipMappings relMappings = initializeMappingsForRels();
        scanEverythingBelongingToRelationships( relMappings );

        // go through all relationshipTypes to get actual names
        addNamesToCollection( tokenRead.relationshipTypesGetAllTokens(), relMappings.relationshipTypIdToRelationshipName );

        return produceResultsForRelationships( relMappings ).stream();
    }

    private List produceResultsForRelationships( RelationshipMappings relMappings )
    {
        List results = new ArrayList<>();
        for ( Integer typeId : relMappings.relationshipTypeIdToPropertyKeys.keySet() )
        {
            // lookup typ name
            String name = relMappings.relationshipTypIdToRelationshipName.get( typeId );
            name = ":`" + name + "`";  // escaping

            // lookup property value types
            Set propertyIds = relMappings.relationshipTypeIdToPropertyKeys.get( typeId );
            if ( propertyIds.size() == 0 )
            {
                results.add( new RelationshipPropertySchemaInfoResult( name, null, null, false ) );
            }
            else
            {
                String finalName = name;
                propertyIds.forEach( propId -> {
                    // lookup propId name and valueGroup
                    String propName = propertyIdToPropertyNameMapping.get( propId );
                    ValueTypeListHelper valueTypeListHelper = relMappings.relationshipTypeIdANDPropertyTypeIdToValueType.get( Pair.of( typeId, propId ) );
                    if ( relMappings.nullableRelationshipTypes.contains( typeId ) )
                    {
                        results.add( new RelationshipPropertySchemaInfoResult( finalName, propName, valueTypeListHelper.getCypherTypesList(),
                                false ) );
                    }
                    else
                    {
                        results.add( new RelationshipPropertySchemaInfoResult( finalName, propName, valueTypeListHelper.getCypherTypesList(),
                                valueTypeListHelper.isMandatory() ) );
                    }
                } );
            }
        }
        return results;
    }

    private List produceResultsForNodes( NodeMappings nodeMappings )
    {
        List results = new ArrayList<>();
        for ( SortedLabels labelSet : nodeMappings.labelSetToPropertyKeys.keySet() )
        {
            // lookup label names and produce list of names and produce String out of them
            List labelNames = new ArrayList<>();
            for ( int i = 0; i < labelSet.numberOfLabels(); i++ )
            {
                String name = nodeMappings.labelIdToLabelName.get( labelSet.label( i ) );
                labelNames.add( name );
            }
            Collections.sort( labelNames );  // this is optional but waaaaay nicer
            StringBuilder labelsConcatenator = new StringBuilder();
            for ( String item : labelNames )
            {
                labelsConcatenator.append( ":`" ).append( item ).append( "`" );
            }
            String labels = labelsConcatenator.toString();

            // lookup property value types
            Set propertyIds = nodeMappings.labelSetToPropertyKeys.get( labelSet );
            if ( propertyIds.size() == 0 )
            {
                results.add( new NodePropertySchemaInfoResult( labels, labelNames, null, null, false ) );
            }
            else
            {
                propertyIds.forEach( propId -> {
                    // lookup propId name and valueGroup
                    String propName = propertyIdToPropertyNameMapping.get( propId );
                    ValueTypeListHelper valueTypeListHelper = nodeMappings.labelSetANDNodePropertyKeyIdToValueType.get( Pair.of( labelSet, propId ) );
                    if ( nodeMappings.nullableLabelSets.contains( labelSet ) )
                    {
                        results.add( new NodePropertySchemaInfoResult( labels, labelNames, propName, valueTypeListHelper.getCypherTypesList(), false ) );
                    }
                    else
                    {
                        results.add( new NodePropertySchemaInfoResult( labels, labelNames, propName, valueTypeListHelper.getCypherTypesList(),
                                valueTypeListHelper.isMandatory() ) );
                    }
                } );
            }
        }
        return results;
    }

    private void scanEverythingBelongingToRelationships( RelationshipMappings relMappings )
    {
        try ( RelationshipScanCursor relationshipScanCursor = cursors.allocateRelationshipScanCursor();
                PropertyCursor propertyCursor = cursors.allocatePropertyCursor() )
        {
            dataRead.allRelationshipsScan( relationshipScanCursor );
            while ( relationshipScanCursor.next() )
            {
                int typeId = relationshipScanCursor.type();
                relationshipScanCursor.properties( propertyCursor );
                Set propertyIds = new HashSet<>();

                while ( propertyCursor.next() )
                {
                    int propertyKey = propertyCursor.propertyKey();

                    Value currentValue = propertyCursor.propertyValue();
                    Pair key = Pair.of( typeId, propertyKey );
                    updateValueTypeInMapping( currentValue, key, relMappings.relationshipTypeIdANDPropertyTypeIdToValueType );

                    propertyIds.add( propertyKey );
                }
                propertyCursor.close();

                Set oldPropertyKeySet = relMappings.relationshipTypeIdToPropertyKeys.getOrDefault( typeId, emptyPropertyIdSet );

                // find out which old properties we did not visited and mark them as nullable
                if ( oldPropertyKeySet == emptyPropertyIdSet )
                {
                    if ( propertyIds.size() == 0 )
                    {
                        // Even if we find property key on other rels with this type, set all of them nullable
                        relMappings.nullableRelationshipTypes.add( typeId );
                    }

                    propertyIds.addAll( oldPropertyKeySet );
                }
                else
                {
                    Set currentPropertyIdsHelperSet = new HashSet( propertyIds );
                    currentPropertyIdsHelperSet.addAll( propertyIds );
                    propertyIds.removeAll( oldPropertyKeySet );  // only the brand new ones in propIds now
                    oldPropertyKeySet.removeAll( currentPropertyIdsHelperSet );  // only the old ones that are not on the new rel

                    propertyIds.addAll( oldPropertyKeySet );
                    propertyIds.forEach( id -> {
                        Pair key = Pair.of( typeId, id );
                        relMappings.relationshipTypeIdANDPropertyTypeIdToValueType.get( key ).setNullable();
                    } );

                    propertyIds.addAll( currentPropertyIdsHelperSet );
                }

                relMappings.relationshipTypeIdToPropertyKeys.put( typeId, propertyIds );
            }
            relationshipScanCursor.close();
        }
    }

    private void scanEverythingBelongingToNodes( NodeMappings nodeMappings )
    {
        try ( NodeCursor nodeCursor = cursors.allocateNodeCursor();
                PropertyCursor propertyCursor = cursors.allocatePropertyCursor() )
        {
            dataRead.allNodesScan( nodeCursor );
            while ( nodeCursor.next() )
            {
                // each node
                SortedLabels labels = SortedLabels.from( nodeCursor.labels() );
                nodeCursor.properties( propertyCursor );
                Set propertyIds = new HashSet<>();

                while ( propertyCursor.next() )
                {
                    Value currentValue = propertyCursor.propertyValue();
                    int propertyKeyId = propertyCursor.propertyKey();
                    Pair key = Pair.of( labels, propertyKeyId );
                    updateValueTypeInMapping( currentValue, key, nodeMappings.labelSetANDNodePropertyKeyIdToValueType );

                    propertyIds.add( propertyKeyId );
                }
                propertyCursor.close();

                Set oldPropertyKeySet = nodeMappings.labelSetToPropertyKeys.getOrDefault( labels, emptyPropertyIdSet );

                // find out which old properties we did not visited and mark them as nullable
                if ( oldPropertyKeySet == emptyPropertyIdSet )
                {
                    if ( propertyIds.size() == 0 )
                    {
                        // Even if we find property key on other nodes with those labels, set all of them nullable
                        nodeMappings.nullableLabelSets.add( labels );
                    }

                    propertyIds.addAll( oldPropertyKeySet );
                }
                else
                {
                    Set currentPropertyIdsHelperSet = new HashSet( propertyIds );
                    currentPropertyIdsHelperSet.addAll( propertyIds );
                    propertyIds.removeAll( oldPropertyKeySet );  // only the brand new ones in propIds now
                    oldPropertyKeySet.removeAll( currentPropertyIdsHelperSet );  // only the old ones that are not on the new node

                    propertyIds.addAll( oldPropertyKeySet );
                    propertyIds.forEach( id -> {
                        Pair key = Pair.of( labels, id );
                        nodeMappings.labelSetANDNodePropertyKeyIdToValueType.get( key ).setNullable();
                    } );

                    propertyIds.addAll( currentPropertyIdsHelperSet );
                }

                nodeMappings.labelSetToPropertyKeys.put( labels, propertyIds );
            }
            nodeCursor.close();
        }
    }

    private  void updateValueTypeInMapping( Value currentValue, Pair key, Map,ValueTypeListHelper> mappingToUpdate )
    {
        ValueTypeListHelper helper = mappingToUpdate.get( key );
        if ( helper == null )
        {
            helper = new ValueTypeListHelper( currentValue );
            mappingToUpdate.put( key, helper );
        }
        else
        {
            helper.updateValueTypesWith( currentValue );
        }
    }

    private void addNamesToCollection( Iterator labelIterator, Map collection )
    {
        while ( labelIterator.hasNext() )
        {
            NamedToken label = labelIterator.next();
            collection.put( label.id(), label.name() );
        }
    }

    private class ValueTypeListHelper
    {
        private Set seenValueTypes;
        private boolean isMandatory = true;

        ValueTypeListHelper( Value v )
        {
            seenValueTypes = new HashSet<>();
            updateValueTypesWith( v );
        }

        private void setNullable()
        {
            isMandatory = false;
        }

        public boolean isMandatory()
        {
            return isMandatory;
        }

        List getCypherTypesList()
        {
            return new ArrayList<>( seenValueTypes );
        }

        void updateValueTypesWith( Value newValue )
        {
            if ( newValue == null )
            {
                throw new IllegalArgumentException();
            }
            seenValueTypes.add( newValue.getTypeName() );
        }
    }

    /*
      All mappings needed to describe Nodes except for property infos
     */
    private class NodeMappings
    {
        final Map> labelSetToPropertyKeys;
        final Map,ValueTypeListHelper> labelSetANDNodePropertyKeyIdToValueType;
        final Set nullableLabelSets; // used for label combinations without properties -> all properties are viewed as nullable
        final Map labelIdToLabelName;

        NodeMappings( int labelCount )
        {
            labelSetToPropertyKeys = new HashMap<>( labelCount );
            labelIdToLabelName = new HashMap<>( labelCount );
            labelSetANDNodePropertyKeyIdToValueType = new HashMap<>();
            nullableLabelSets = new HashSet<>();
        }
    }

    /*
      All mappings needed to describe Rels except for property infos
     */
    private class RelationshipMappings
    {
        final Map relationshipTypIdToRelationshipName;
        final Map> relationshipTypeIdToPropertyKeys;
        final Map,ValueTypeListHelper> relationshipTypeIdANDPropertyTypeIdToValueType;
        final Set nullableRelationshipTypes; // used for types without properties -> all properties are viewed as nullable

        RelationshipMappings( int relationshipTypeCount )
        {
            relationshipTypIdToRelationshipName = new HashMap<>( relationshipTypeCount );
            relationshipTypeIdToPropertyKeys = new HashMap<>( relationshipTypeCount );
            relationshipTypeIdANDPropertyTypeIdToValueType = new HashMap<>();
            nullableRelationshipTypes = new HashSet<>();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy