Skip to content

Commit

Permalink
Introducing SqlTimeSeriesSource and SqlTimeSeriesMapping and defining…
Browse files Browse the repository at this point in the history
… corresponding sql schemas
  • Loading branch information
sebastian-peter committed Jan 6, 2022
1 parent 04db918 commit 66e60aa
Show file tree
Hide file tree
Showing 19 changed files with 665 additions and 29 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased/Snapshot]

### Added
- SQL time series sources (`SqlTimeSeriesSource` and `SqlTimeSeriesMappingSource`)

### Fixed
- Reduced code smells [#492](https://github.com/ie3-institute/PowerSystemDataModel/issues/492)
- Protected constructors for abstract classes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public TimeBasedSimpleValueFactory(Class<? extends V> valueClasses, String timeP
timeUtil = new TimeUtil(ZoneId.of("UTC"), Locale.GERMANY, timePattern);
}

/**
* Return the field name for the date time
*
* @return the field name for the date time
*/
public String getTimeFieldString() {
return TIME;
}

@Override
protected TimeBasedValue<V> buildModel(SimpleTimeBasedValueData<V> data) {
UUID uuid = data.getUUID(UUID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

public class CsvTimeSeriesMappingSource extends CsvDataSource implements TimeSeriesMappingSource {
/* Available factories */
private final TimeSeriesMappingFactory mappingFactory = new TimeSeriesMappingFactory();
private static final TimeSeriesMappingFactory mappingFactory = new TimeSeriesMappingFactory();

private final Map<UUID, UUID> mapping;

Expand Down
39 changes: 36 additions & 3 deletions src/main/java/edu/ie3/datamodel/io/source/sql/SqlDataSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,28 @@ protected SqlDataSource(SqlConnector connector) {
this.connector = connector;
}

/**
* Creates a base query string without closing semicolon of the following pattern: <br>
* {@code SELECT * FROM <schema>.<table>}
*
* @param schemaName the name of the database schema
* @param tableName the name of the database table
* @return basic query string without semicolon
*/
protected static String createBaseQueryString(String schemaName, String tableName) {
return "SELECT * FROM " + schemaName + ".\"" + tableName + "\"";
}

/**
* Determine the corresponding database column name based on the provided factory field parameter
* name. Needed to support camel as well as snake case database column names.
*
* @param factoryColumnName the name of the field parameter set in the entity factory
* @param connector the sql connector of this source
* @param tableName the table name where the data is stored
* @return the column name that corresponds to the provided field parameter or an empty optional
* if no matching column can be found
*/
protected static String getDbColumnName(
String factoryColumnName, SqlConnector connector, String tableName) {
protected String getDbColumnName(String factoryColumnName, String tableName) {
try {
ResultSet rs =
connector.getConnection().getMetaData().getColumns(null, null, tableName, null);
Expand All @@ -65,6 +75,29 @@ protected static String getDbColumnName(
+ "Please ensure that the database connection is working and the column names are correct!");
}

/**
* Determine the corresponding table name based on the provided table name pattern.
*
* @param schemaPattern pattern of the schema to search in
* @param tableNamePattern pattern of the table name
* @return a matching table name, if one is found
*/
protected Optional<String> getDbTableName(String schemaPattern, String tableNamePattern) {
try {
ResultSet rs =
connector
.getConnection()
.getMetaData()
.getTables(null, schemaPattern, tableNamePattern, null);
if (rs.next()) {
return Optional.of(rs.getString("TABLE_NAME"));
}
} catch (SQLException ex) {
log.error("Cannot connect to database to retrieve tables meta information", ex);
}
return Optional.empty();
}

/**
* Interface for anonymous functions that are used as a parameter for {@link #executeQuery}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* © 2021. TU Dortmund University,
* Institute of Energy Systems, Energy Efficiency and Energy Economics,
* Research group Distribution grid planning and operation
*/
package edu.ie3.datamodel.io.source.sql;

import edu.ie3.datamodel.io.connectors.SqlConnector;
import edu.ie3.datamodel.io.csv.timeseries.IndividualTimeSeriesMetaInformation;
import edu.ie3.datamodel.io.factory.SimpleEntityData;
import edu.ie3.datamodel.io.factory.timeseries.TimeSeriesMappingFactory;
import edu.ie3.datamodel.io.naming.FileNamingStrategy;
import edu.ie3.datamodel.io.source.TimeSeriesMappingSource;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

public class SqlTimeSeriesMappingSource extends SqlDataSource<TimeSeriesMappingSource.MappingEntry>
implements TimeSeriesMappingSource {
private static final TimeSeriesMappingFactory mappingFactory = new TimeSeriesMappingFactory();

private final FileNamingStrategy fileNamingStrategy;
private final String queryFull;

public SqlTimeSeriesMappingSource(
SqlConnector connector, String schemaName, FileNamingStrategy fileNamingStrategy) {
super(connector);
this.fileNamingStrategy = fileNamingStrategy;

final String tableName =
fileNamingStrategy
.getEntityName(MappingEntry.class)
.orElseThrow(() -> new RuntimeException(""));
this.queryFull = createBaseQueryString(schemaName, tableName);
}

@Override
public Map<UUID, UUID> getMapping() {
return executeQuery(queryFull, (ps) -> {}).stream()
.collect(Collectors.toMap(MappingEntry::getParticipant, MappingEntry::getTimeSeries));
}

@Override
public Optional<IndividualTimeSeriesMetaInformation> getTimeSeriesMetaInformation(
UUID timeSeriesUuid) {
return getDbTableName(null, "%" + timeSeriesUuid.toString())
.map(
tableName ->
(IndividualTimeSeriesMetaInformation)
fileNamingStrategy.extractTimeSeriesMetaInformation(tableName));
}

@Override
protected Optional<MappingEntry> createEntity(Map<String, String> fieldToValues) {
SimpleEntityData entityData = new SimpleEntityData(fieldToValues, MappingEntry.class);
return mappingFactory.get(entityData);
}
}
182 changes: 182 additions & 0 deletions src/main/java/edu/ie3/datamodel/io/source/sql/SqlTimeSeriesSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* © 2021. TU Dortmund University,
* Institute of Energy Systems, Energy Efficiency and Energy Economics,
* Research group Distribution grid planning and operation
*/
package edu.ie3.datamodel.io.source.sql;

import edu.ie3.datamodel.exceptions.SourceException;
import edu.ie3.datamodel.io.connectors.SqlConnector;
import edu.ie3.datamodel.io.csv.timeseries.ColumnScheme;
import edu.ie3.datamodel.io.factory.timeseries.SimpleTimeBasedValueData;
import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory;
import edu.ie3.datamodel.io.source.TimeSeriesSource;
import edu.ie3.datamodel.models.timeseries.individual.IndividualTimeSeries;
import edu.ie3.datamodel.models.timeseries.individual.TimeBasedValue;
import edu.ie3.datamodel.models.value.Value;
import edu.ie3.util.interval.ClosedInterval;
import java.sql.Timestamp;
import java.time.ZonedDateTime;
import java.util.*;

public class SqlTimeSeriesSource<V extends Value> extends SqlDataSource<TimeBasedValue<V>>
implements TimeSeriesSource<V> {
private static final String WHERE = " WHERE ";

/**
* Factory method to build a source from given meta information
*
* @param connector the connector needed for database connection
* @param schemaName the database schema to use
* @param tableName the database table to use
* @param timeSeriesUuid the uuid of the time series
* @param columnScheme the column scheme of this time series
* @param timePattern the pattern of time values
* @return a SqlTimeSeriesSource for given time series table
* @throws SourceException if the column scheme is not supported
*/
public static SqlTimeSeriesSource<? extends Value> getSource(
SqlConnector connector,
String schemaName,
String tableName,
UUID timeSeriesUuid,
ColumnScheme columnScheme,
String timePattern)
throws SourceException {
if (!TimeSeriesSource.isSchemeAccepted(columnScheme))
throw new SourceException("Unsupported column scheme '" + columnScheme + "'.");

Class<? extends Value> valClass = columnScheme.getValueClass();

return create(connector, schemaName, tableName, timeSeriesUuid, valClass, timePattern);
}

private static <T extends Value> SqlTimeSeriesSource<T> create(
SqlConnector connector,
String schemaName,
String tableName,
UUID timeSeriesUuid,
Class<T> valClass,
String timePattern) {
TimeBasedSimpleValueFactory<T> valueFactory =
new TimeBasedSimpleValueFactory<>(valClass, timePattern);
return new SqlTimeSeriesSource<>(
connector, schemaName, tableName, timeSeriesUuid, valClass, valueFactory);
}

private final UUID timeSeriesUuid;
private final Class<V> valueClass;
private final TimeBasedSimpleValueFactory<V> valueFactory;

/**
* Queries that are available within this source. Motivation to have them as field value is to
* avoid creating a new string each time, bc they're always the same.
*/
private final String queryFull;

private final String queryTimeInterval;
private final String queryTime;

/**
* Initializes a new SqlTimeSeriesSource
*
* @param connector the connector needed for database connection
* @param schemaName the database schema to use
* @param tableName the database table to use
* @param timeSeriesUuid the uuid of the time series
* @param valueClass the class of returned time series values
* @param factory a factory that parses the input data
*/
public SqlTimeSeriesSource(
SqlConnector connector,
String schemaName,
String tableName,
UUID timeSeriesUuid,
Class<V> valueClass,
TimeBasedSimpleValueFactory<V> factory) {
super(connector);
this.timeSeriesUuid = timeSeriesUuid;
this.valueClass = valueClass;
this.valueFactory = factory;

String dbTimeColumnName = getDbColumnName(factory.getTimeFieldString(), tableName);

this.queryFull = createBaseQueryString(schemaName, tableName);
this.queryTimeInterval =
createQueryStringForTimeInterval(schemaName, tableName, dbTimeColumnName);
this.queryTime = createQueryStringForTime(schemaName, tableName, dbTimeColumnName);
}

@Override
public IndividualTimeSeries<V> getTimeSeries() {
List<TimeBasedValue<V>> timeBasedValues = executeQuery(queryFull, (ps) -> {});
return new IndividualTimeSeries<>(timeSeriesUuid, new HashSet<>(timeBasedValues));
}

@Override
public IndividualTimeSeries<V> getTimeSeries(ClosedInterval<ZonedDateTime> timeInterval) {
List<TimeBasedValue<V>> timeBasedValues =
executeQuery(
queryTimeInterval,
(ps) -> {
ps.setTimestamp(1, Timestamp.from(timeInterval.getLower().toInstant()));
ps.setTimestamp(2, Timestamp.from(timeInterval.getUpper().toInstant()));
});
return new IndividualTimeSeries<>(timeSeriesUuid, new HashSet<>(timeBasedValues));
}

@Override
public Optional<V> getValue(ZonedDateTime time) {
List<TimeBasedValue<V>> timeBasedValues =
executeQuery(queryTime, (ps) -> ps.setTimestamp(1, Timestamp.from(time.toInstant())));
if (timeBasedValues.isEmpty()) return Optional.empty();
if (timeBasedValues.size() > 1)
log.warn("Retrieved more than one result value, using the first");
return Optional.of(timeBasedValues.get(0).getValue());
}

/**
* Build a {@link TimeBasedValue} of type {@code V}, whereas the underlying {@link Value} does not
* need any additional information.
*
* @param fieldToValues Mapping from field id to values
* @return Optional simple time based value
*/
protected Optional<TimeBasedValue<V>> createEntity(Map<String, String> fieldToValues) {
SimpleTimeBasedValueData<V> factoryData =
new SimpleTimeBasedValueData<>(fieldToValues, valueClass);
return valueFactory.get(factoryData);
}

/**
* Creates a base query to retrieve all entities in the given time frame with the following
* pattern: <br>
* {@code <base query> WHERE <time column> BETWEEN ? AND ?;}
*
* @param schemaName the name of the database schema
* @param tableName the name of the database table
* @param timeColumnName the name of the column holding the timestamp info
* @return the query string
*/
private static String createQueryStringForTimeInterval(
String schemaName, String tableName, String timeColumnName) {
return createBaseQueryString(schemaName, tableName)
+ WHERE
+ timeColumnName
+ " BETWEEN ? AND ?;";
}

/**
* Creates a basic query to retrieve an entry for the given time with the following pattern: <br>
* {@code <base query> WHERE <time column>=?;}
*
* @param schemaName the name of the database schema
* @param weatherTableName the name of the database table
* @param timeColumnName the name of the column holding the timestamp info
* @return the query string
*/
private String createQueryStringForTime(
String schemaName, String weatherTableName, String timeColumnName) {
return createBaseQueryString(schemaName, weatherTableName) + WHERE + timeColumnName + "=?;";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ public SqlWeatherSource(
this.factoryCoordinateFieldName = weatherFactory.getCoordinateIdFieldString();

String dbTimeColumnName =
getDbColumnName(weatherFactory.getTimeFieldString(), connector, weatherTableName);
String dbCoordColumnName =
getDbColumnName(factoryCoordinateFieldName, connector, weatherTableName);
getDbColumnName(weatherFactory.getTimeFieldString(), weatherTableName);
String dbCoordColumnName = getDbColumnName(factoryCoordinateFieldName, weatherTableName);

// setup queries
this.queryTimeInterval =
Expand Down Expand Up @@ -136,18 +135,6 @@ public Optional<TimeBasedValue<WeatherValue>> getWeather(ZonedDateTime date, Poi
return Optional.of(timeBasedValues.get(0));
}

/**
* Creates a base query string without closing semicolon of the following pattern: <br>
* {@code SELECT * FROM <schema>.<table>}
*
* @param schemaName the name of the database schema
* @param weatherTableName the name of the database table
* @return basic query string without semicolon
*/
private static String createBaseQueryString(String schemaName, String weatherTableName) {
return "SELECT * FROM " + schemaName + "." + weatherTableName;
}

/**
* Creates a base query to retrieve all entities in the given time frame with the following
* pattern: <br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ import edu.ie3.datamodel.exceptions.SourceException
import edu.ie3.datamodel.io.connectors.CsvFileConnector
import edu.ie3.datamodel.io.csv.timeseries.ColumnScheme
import edu.ie3.datamodel.io.factory.timeseries.TimeBasedSimpleValueFactory
import edu.ie3.datamodel.io.source.IdCoordinateSource
import edu.ie3.datamodel.models.timeseries.individual.TimeBasedValue
import edu.ie3.datamodel.models.value.*
import edu.ie3.util.TimeUtil
import edu.ie3.util.geo.GeoUtils
import org.locationtech.jts.geom.Coordinate
import spock.lang.Specification
import tech.units.indriya.quantity.Quantities

Expand All @@ -28,9 +25,6 @@ class CsvTimeSeriesSourceTest extends Specification implements CsvTestDataMeta {

def "The csv time series source is able to build time based values from simple data"() {
given:
def defaultCoordinate = GeoUtils.DEFAULT_GEOMETRY_FACTORY.createPoint(new Coordinate(7.4116482, 51.4843281))
def coordinateSource = Mock(IdCoordinateSource)
coordinateSource.getCoordinate(5) >> defaultCoordinate
def factory = new TimeBasedSimpleValueFactory(EnergyPriceValue)
def source = new CsvTimeSeriesSource(";", timeSeriesFolderPath, new FileNamingStrategy(), UUID.fromString("2fcb3e53-b94a-4b96-bea4-c469e499f1a1"), "its_c_2fcb3e53-b94a-4b96-bea4-c469e499f1a1", EnergyPriceValue, factory)
def time = TimeUtil.withDefaults.toZonedDateTime("2019-01-01 00:00:00")
Expand Down

0 comments on commit 66e60aa

Please sign in to comment.