/* Copyright (c) 2014 Boundless and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Distribution License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/edl-v10.html * * Contributors: * Justin Deoliveira (Boundless) - initial implementation */ package org.locationtech.geogig.storage.sqlite; import static org.locationtech.geogig.storage.sqlite.SQLiteStorage.FORMAT_NAME; import static org.locationtech.geogig.storage.sqlite.SQLiteStorage.VERSION; import java.io.File; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Queue; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.Platform; import org.locationtech.geogig.repository.RepositoryConnectionException; import org.locationtech.geogig.storage.ConfigDatabase; import org.locationtech.geogig.storage.GraphDatabase; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; /** * Base class for SQLite based graph database. * * @author Justin Deoliveira, Boundless * * @param <C> Connection type. */ public abstract class SQLiteGraphDatabase<T> implements GraphDatabase { final ConfigDatabase configdb; final Platform platform; private T cx; public SQLiteGraphDatabase(ConfigDatabase configdb, Platform platform) { this.configdb = configdb; this.platform = platform; } @Override public void open() { if (cx == null) { cx = connect(SQLiteStorage.geogigDir(platform)); init(cx); } } @Override public void configure() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.GRAPH.configure(configdb, FORMAT_NAME, VERSION); } @Override public void checkConfig() throws RepositoryConnectionException { RepositoryConnectionException.StorageType.GRAPH.verify(configdb, FORMAT_NAME, VERSION); } @Override public boolean isOpen() { return cx != null; } @Override public void close() { if (cx != null) { close(cx); cx = null; } } @Override public boolean exists(ObjectId commitId) { return has(commitId.toString(), cx); } @Override public ImmutableList<ObjectId> getParents(ObjectId commitId) throws IllegalArgumentException { return ImmutableList.copyOf(Iterables.transform(outgoing(commitId.toString(), cx), StringToObjectId.INSTANCE)); } @Override public ImmutableList<ObjectId> getChildren(ObjectId commitId) throws IllegalArgumentException { return ImmutableList.copyOf(Iterables.transform(incoming(commitId.toString(), cx), StringToObjectId.INSTANCE)); } @Override public boolean put(ObjectId commitId, ImmutableList<ObjectId> parentIds) { String node = commitId.toString(); boolean added = put(node, cx); // TODO: if node was node added should we severe existing parent relationships? for (ObjectId p : parentIds) { relate(node, p.toString(), cx); } return added; } @Override public void map(ObjectId mapped, ObjectId original) { map(mapped.toString(), original.toString(), cx); } @Override public ObjectId getMapping(ObjectId commitId) { String mapped = mapping(commitId.toString(), cx); return mapped != null ? ObjectId.valueOf(mapped) : null; } @Override public int getDepth(ObjectId commitId) { int depth = 0; Queue<String> q = Lists.newLinkedList(); Iterables.addAll(q, outgoing(commitId.toString(), cx)); List<String> next = Lists.newArrayList(); while (!q.isEmpty()) { depth++; while (!q.isEmpty()) { String n = q.poll(); List<String> parents = Lists.newArrayList(outgoing(n, cx)); if (parents.size() == 0) { return depth; } Iterables.addAll(next, parents); } q.addAll(next); next.clear(); } return depth; } @Override public void setProperty(ObjectId commitId, String name, String value) { property(commitId.toString(), name, value, cx); } @Override public void truncate() { } /** * Opens a database connection, returning the object representing connection state. */ protected abstract T connect(File geogigDir); /** * Closes a database connection. * * @param cx The connection object. */ protected abstract void close(T cx); /** * Creates the graph tables with the following schema: * * <pre> * nodes(id:varchar PRIMARY KEY) * edges(src:varchar, dst:varchar, PRIMARY KEY(src,dst)) * props(nid:varchar, key:varchar, val:varchar, PRIMARY KEY(nid,key)) * mappings(alias:varchar, nid:varchar) * </pre> * * Implementations of this method should be prepared to be called multiple times, so must check * if the tables already exist. * * @param cx The connection object. */ protected abstract void init(T cx); /** * Adds a new node to the graph. * <p> * This method must determine if the node already exists in the graph. * </p> * * @return True if the node did not previously exist in the graph, false if otherwise. */ protected abstract boolean put(String node, T cx); /** * Determines if a node exists in the graph. */ protected abstract boolean has(String node, T cx); /** * Relates two nodes in the graph. * * @param src The source (origin) node of the relationship. * @param dst The destination (origin) node of the relationship. */ protected abstract void relate(String src, String dst, T cx); /** * Creates a node mapping. * * @param from The node being mapped from. * @param to The node being mapped to. */ protected abstract void map(String from, String to, T cx); /** * Returns the mapping for a node. * <p> * This method should return <code>null</code> if no mapping exists. * </p> */ protected abstract String mapping(String node, T cx); /** * Assigns a property key/value pair to a node. * * @param node The node. * @param key The property key. * @param value The property value. */ protected abstract void property(String node, String key, String value, T cx); /** * Retrieves a property by key from a node. * * @param node The node. * @param key The property key. * * @return The property value, or <code>null</code> if the property is not set for the node. */ protected abstract String property(String node, String key, T cx); /** * Returns all nodes connected to the specified node through a relationship in which the * specified node is the "source" of the relationship. */ protected abstract Iterable<String> outgoing(String node, T cx); /** * Returns all nodes connected to the specified node through a relationship in which the * specified node is the "destination" of the relationship. */ protected abstract Iterable<String> incoming(String node, T cx); /** * Clears the contents of the graph. */ protected abstract void clear(T cx); private class SQLiteGraphNode extends GraphNode { private ObjectId id; public SQLiteGraphNode(ObjectId id) { this.id = id; } @Override public ObjectId getIdentifier() { return id; } @Override public Iterator<GraphEdge> getEdges(final Direction direction) { List<GraphEdge> edges = new LinkedList<GraphEdge>(); if (direction == Direction.IN || direction == Direction.BOTH) { Iterator<String> nodeEdges = incoming(id.toString(), cx).iterator(); while (nodeEdges.hasNext()) { String otherNode = nodeEdges.next(); edges.add(new GraphEdge(new SQLiteGraphNode(ObjectId.valueOf(otherNode)), this)); } } if (direction == Direction.OUT || direction == Direction.BOTH) { Iterator<String> nodeEdges = outgoing(id.toString(), cx).iterator(); while (nodeEdges.hasNext()) { String otherNode = nodeEdges.next(); edges.add(new GraphEdge(this, new SQLiteGraphNode(ObjectId.valueOf(otherNode)))); } } return edges.iterator(); } @Override public boolean isSparse() { String sparse = property(id.toString(), SPARSE_FLAG, cx); return sparse != null && Boolean.valueOf(sparse); } } @Override public GraphNode getNode(ObjectId id) { return new SQLiteGraphNode(id); } }