/* 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 java.lang.String.format;
import static org.locationtech.geogig.storage.sqlite.Xerial.log;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import javax.sql.DataSource;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Platform;
import org.locationtech.geogig.api.RevObject;
import org.locationtech.geogig.storage.BulkOpListener;
import org.locationtech.geogig.storage.ConfigDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterators;
import com.google.common.io.ByteStreams;
import com.google.inject.Inject;
/**
* Object database based on Xerial SQLite jdbc driver.
*
* @author Justin Deoliveira, Boundless
*/
public class XerialObjectDatabase extends SQLiteObjectDatabase<DataSource> {
static Logger LOG = LoggerFactory.getLogger(XerialObjectDatabase.class);
static final String OBJECTS = "objects";
final int partitionSize = 10 * 1000; // TODO make configurable
final String dbName;
@Inject
public XerialObjectDatabase(ConfigDatabase configdb, Platform platform) {
this(configdb, platform, "objects");
}
public XerialObjectDatabase(ConfigDatabase configdb, Platform platform, String dbName) {
super(configdb, platform);
this.dbName = dbName;
// File db = new File(new File(platform.pwd(), ".geogig"), name + ".db");
// dataSource = Xerial.newDataSource(db);
}
@Override
protected DataSource connect(File geogigDir) {
return Xerial.newDataSource(new File(geogigDir, dbName + ".db"));
}
@Override
protected void close(DataSource ds) {
}
@Override
public void init(DataSource ds) {
new DbOp<Void>() {
@Override
protected Void doRun(Connection cx) throws SQLException {
String sql = format(
"CREATE TABLE IF NOT EXISTS %s (id varchar PRIMARY KEY, object blob)",
OBJECTS);
open(cx.createStatement()).execute(log(sql, LOG));
return null;
}
}.run(ds);
}
@Override
public boolean has(final String id, DataSource ds) {
return new DbOp<Boolean>() {
@Override
protected Boolean doRun(Connection cx) throws SQLException {
String sql = format("SELECT count(*) FROM %s WHERE id = ?", OBJECTS);
PreparedStatement ps = open(cx.prepareStatement(log(sql, LOG, id)));
ps.setString(1, id);
ResultSet rs = open(ps.executeQuery());
rs.next();
return rs.getInt(1) > 0;
}
}.run(ds);
}
@Override
public Iterable<String> search(final String partialId, DataSource ds) {
Connection cx = Xerial.newConnection(ds);
final ResultSet rs = new DbOp<ResultSet>() {
@Override
protected ResultSet doRun(Connection cx) throws SQLException {
String sql = format("SELECT id FROM %s WHERE id LIKE '%%%s%%'", OBJECTS, partialId);
return cx.createStatement().executeQuery(log(sql, LOG));
}
}.run(cx);
return new StringResultSetIterable(rs, cx);
}
@Override
public InputStream get(final String id, DataSource ds) {
return new DbOp<InputStream>() {
@Override
protected InputStream doRun(Connection cx) throws SQLException {
String sql = format("SELECT object FROM %s WHERE id = ?", OBJECTS);
PreparedStatement ps = open(cx.prepareStatement(log(sql, LOG, id)));
ps.setString(1, id);
ResultSet rs = open(ps.executeQuery());
if (!rs.next()) {
return null;
}
byte[] bytes = rs.getBytes(1);
return new ByteArrayInputStream(bytes);
}
}.run(ds);
}
@Override
public void put(final String id, final InputStream obj, DataSource ds) {
new DbOp<Void>() {
@Override
protected Void doRun(Connection cx) throws SQLException, IOException {
String sql = format("INSERT OR IGNORE INTO %s (id,object) VALUES (?,?)", OBJECTS);
PreparedStatement ps = open(cx.prepareStatement(log(sql, LOG, id, obj)));
ps.setString(1, id);
ps.setBytes(2, ByteStreams.toByteArray(obj));
ps.executeUpdate();
return null;
}
}.run(ds);
}
@Override
public boolean delete(final String id, DataSource ds) {
return new DbOp<Boolean>() {
@Override
protected Boolean doRun(Connection cx) throws SQLException {
String sql = format("DELETE FROM %s WHERE id = ?", OBJECTS);
PreparedStatement ps = open(cx.prepareStatement(log(sql, LOG, id)));
ps.setString(1, id);
return ps.executeUpdate() > 0;
}
}.run(ds);
}
/**
* Override to optimize batch insert.
*/
@Override
public void putAll(final Iterator<? extends RevObject> objects, final BulkOpListener listener) {
Preconditions.checkState(isOpen(), "No open database connection");
new DbOp<Void>() {
@Override
protected boolean isAutoCommit() {
return false;
}
@Override
protected Void doRun(Connection cx) throws SQLException, IOException {
// use INSERT OR IGNORE to deal with duplicates cleanly
String sql = format("INSERT OR IGNORE INTO %s (object,id) VALUES (?,?)", OBJECTS);
PreparedStatement stmt = open(cx.prepareStatement(log(sql, LOG)));
// partition the objects into chunks for batch processing
@SuppressWarnings({ "unchecked", "rawtypes" })
Iterator<List<? extends RevObject>> it = (Iterator) Iterators.partition(objects,
partitionSize);
while (it.hasNext()) {
List<? extends RevObject> objs = it.next();
for (RevObject obj : objs) {
stmt.setBytes(1, ByteStreams.toByteArray(writeObject(obj)));
stmt.setString(2, obj.getId().toString());
stmt.addBatch();
}
notifyInserted(stmt.executeBatch(), objs, listener);
stmt.clearParameters();
}
cx.commit();
return null;
}
}.run(cx);
}
void notifyInserted(int[] inserted, List<? extends RevObject> objects, BulkOpListener listener) {
for (int i = 0; i < inserted.length; i++) {
if (inserted[i] > 0) {
listener.inserted(objects.get(i).getId(), null);
}
}
}
/**
* Override to optimize batch delete.
*/
@Override
public long deleteAll(final Iterator<ObjectId> ids, final BulkOpListener listener) {
Preconditions.checkState(isOpen(), "No open database connection");
return new DbOp<Long>() {
@Override
protected boolean isAutoCommit() {
return false;
}
@Override
protected Long doRun(Connection cx) throws SQLException, IOException {
String sql = format("DELETE FROM %s WHERE id = ?", OBJECTS);
PreparedStatement stmt = open(cx.prepareStatement(log(sql, LOG)));
long count = 0;
// partition the objects into chunks for batch processing
Iterator<List<ObjectId>> it = Iterators.partition(ids, partitionSize);
while (it.hasNext()) {
List<ObjectId> l = it.next();
for (ObjectId id : l) {
stmt.setString(1, id.toString());
stmt.addBatch();
}
count += notifyDeleted(stmt.executeBatch(), l, listener);
stmt.clearParameters();
}
cx.commit();
return count;
}
}.run(cx);
}
long notifyDeleted(int[] deleted, List<ObjectId> ids, BulkOpListener listener) {
long count = 0;
for (int i = 0; i < deleted.length; i++) {
if (deleted[i] > 0) {
count++;
listener.deleted(ids.get(i));
}
}
return count;
}
}