Skip to content

Commit 270b083

Browse files
committed
update linked fields when reference value updated refs #109
1 parent d39de8d commit 270b083

File tree

2 files changed

+73
-17
lines changed

2 files changed

+73
-17
lines changed

src/main/java/com/conveyal/gtfs/loader/JdbcTableWriter.java

+71-16
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
import java.sql.SQLException;
2525
import java.sql.Statement;
2626
import java.util.ArrayList;
27+
import java.util.Arrays;
2728
import java.util.HashSet;
2829
import java.util.List;
2930
import java.util.Set;
3031
import java.util.UUID;
32+
import java.util.stream.Collectors;
3133

3234
import static com.conveyal.gtfs.loader.JdbcGtfsLoader.INSERT_BATCH_SIZE;
3335

@@ -150,6 +152,12 @@ public String update(Integer id, String json, boolean autoCommit) throws SQLExce
150152
updateChildTable(childEntitiesArray, entityId, isCreating, referencingTable, connection);
151153
}
152154
}
155+
// Iterate over table's fields and apply linked values to any tables
156+
if ("routes".equals(specTable.name)) {
157+
updateLinkedFields(specTable, jsonObject, "trips", "route_id", "wheelchair_accessible");
158+
} else if ("patterns".equals(specTable.name)) {
159+
updateLinkedFields(specTable, jsonObject, "trips", "pattern_id", "direction_id");
160+
}
153161
if (autoCommit) {
154162
// If nothing failed up to this point, it is safe to assume there were no problems updating/creating the
155163
// main entity and any of its children, so we commit the transaction.
@@ -173,6 +181,51 @@ public String update(Integer id, String json, boolean autoCommit) throws SQLExce
173181
}
174182
}
175183

184+
/**
185+
* Updates linked fields with values from entity being updated. This is used to update identical fields in related
186+
* tables (for now just fields in trips and stop_times) where the reference table's value should take precedence over
187+
* the related table (e.g., pattern_stop#timepoint should update all of its related stop_times).
188+
*/
189+
private void updateLinkedFields(Table referenceTable, ObjectNode jsonObject, String tableName, String keyField, String ...fieldNames) throws SQLException {
190+
// Collect fields, the JSON values for these fields, and the strings to add to the prepared statement into Lists.
191+
List<Field> fields = new ArrayList<>();
192+
List<JsonNode> values = new ArrayList<>();
193+
List<String> fieldStrings = new ArrayList<>();
194+
for (String field : fieldNames) {
195+
fields.add(referenceTable.getFieldForName(field));
196+
values.add(jsonObject.get(field));
197+
fieldStrings.add(String.format("%s = ?", field));
198+
}
199+
String setFields = String.join(", ", fieldStrings);
200+
// If updating stop_times, use a more complex query that joins trips to stop_times in order to match on pattern_id
201+
boolean updatingStopTimes = "stop_times".equals(tableName);
202+
Field orderField = updatingStopTimes ? referenceTable.getFieldForName(referenceTable.getOrderFieldName()) : null;
203+
String sql = updatingStopTimes
204+
? String.format("update %s.stop_times st set %s from %s.trips t " +
205+
"where st.trip_id = t.trip_id AND t.%s = ? AND st.%s = ?",
206+
tablePrefix, setFields, tablePrefix, keyField, orderField.name)
207+
: String.format("update %s.%s set %s where %s = ?", tablePrefix, tableName, setFields, keyField);
208+
// Prepare the statement and set statement parameters
209+
PreparedStatement statement = connection.prepareStatement(sql);
210+
int oneBasedIndex = 1;
211+
// Iterate over list of fields that need to be updated and set params.
212+
for (int i = 0; i < fields.size(); i++) {
213+
String newValue = values.get(i).isNull() ? null : values.get(i).asText();
214+
fields.get(i).setParameter(statement, oneBasedIndex++, newValue);
215+
}
216+
// Set "where clause" with value for key field (e.g., set values where pattern_id = '3')
217+
statement.setString(oneBasedIndex++, jsonObject.get(keyField).asText());
218+
if (updatingStopTimes) {
219+
// If updating stop times set the order field parameter (stop_sequence)
220+
String orderValue = jsonObject.get(orderField.name).asText();
221+
orderField.setParameter(statement, oneBasedIndex++, orderValue);
222+
}
223+
// Log query, execute statement, and log result.
224+
LOG.info(statement.toString());
225+
int entitiesUpdated = statement.executeUpdate();
226+
LOG.info("{} {} linked fields updated", entitiesUpdated, tableName);
227+
}
228+
176229
/**
177230
* Creates a prepared statement for an entity create or update operation. If not performing a batch operation, the
178231
* method will set parameters for the prepared statement with values found in the provided JSON ObjectNode. The Table
@@ -279,42 +332,41 @@ private void setStatementParameters(ObjectNode jsonObject, Table table, Prepared
279332

280333
/**
281334
* This updates those tables that depend on the table currently being updated. For example, if updating/creating a
282-
* pattern, this method handles updating its pattern stops and shape points. For trips, this would handle updating
335+
* pattern, this method handles deleting any pattern stops and shape points. For trips, this would handle updating
283336
* the trips' stop times.
337+
*
338+
* This method should only be used on tables that have a single foreign key reference to another table, i.e., they
339+
* have a hierarchical relationship.
284340
* FIXME develop a better way to update tables with foreign keys to the table being updated.
285341
*/
286-
private void updateChildTable(ArrayNode entityList, Integer id, boolean isCreating, Table subTable, Connection connection) throws SQLException {
342+
private void updateChildTable(ArrayNode entityList, Integer id, boolean isCreatingNewEntity, Table subTable, Connection connection) throws SQLException {
287343
// Get parent table's key field
288344
Field keyField;
289345
String keyValue;
290-
// FIXME: This is shapes specific code that should probably be made generic.
291-
boolean updatingShapes = subTable.name.equals("shapes");
292-
if (updatingShapes) {
293-
keyField = subTable.getFieldForName("shape_id");
294-
} else {
295-
keyField = specTable.getFieldForName(specTable.getKeyFieldName());
296-
}
346+
// Primary key fields are always referenced by foreign key fields with the same name.
347+
keyField = specTable.getFieldForName(subTable.getKeyFieldName());
297348
// Get parent entity's key value
298349
keyValue = getValueForId(id, keyField.name, tablePrefix, specTable, connection);
299350
String childTableName = String.join(".", tablePrefix, subTable.name);
300351
// FIXME: add check for pattern stop consistency.
301352
// FIXME: re-order stop times if pattern stop order changes.
302353
// FIXME: allow shapes to be updated on pattern geometry change.
303-
if (!isCreating) {
304-
String deleteSql;
305-
// Delete existing sub-entities for given entity ID if the parent entity is not being created
306-
deleteSql = getUpdateReferencesSql(SqlMethod.DELETE, childTableName, keyField, keyValue, null);
354+
if (!isCreatingNewEntity) {
355+
// Delete existing sub-entities for given entity ID if the parent entity is not being newly created.
356+
String deleteSql = getUpdateReferencesSql(SqlMethod.DELETE, childTableName, keyField, keyValue, null);
307357
LOG.info(deleteSql);
308358
Statement statement = connection.createStatement();
309-
// FIXME: Copy on update (instead of deleting here)
359+
// FIXME: Use copy on update for a pattern's shape instead of deleting the previous shape and replacing it.
360+
// This would better account for GTFS data loaded from a file where multiple patterns reference a single
361+
// shape.
310362
int result = statement.executeUpdate(deleteSql);
311363
LOG.info("Deleted {}", result);
312364
// FIXME: are there cases when an update should not return zero?
313365
// if (result == 0) throw new SQLException("No stop times found for trip ID");
314366
}
315367
int entityCount = 0;
316368
PreparedStatement insertStatement = null;
317-
// Iterate over the entities found in the array and
369+
// Iterate over the entities found in the array and add to batch for inserting into table.
318370
for (JsonNode entityNode : entityList) {
319371
// Cast entity node to ObjectNode to allow mutations (JsonNode is immutable).
320372
ObjectNode entity = (ObjectNode)entityNode;
@@ -325,7 +377,10 @@ private void updateChildTable(ArrayNode entityList, Integer id, boolean isCreati
325377
if (entityCount == 0) {
326378
insertStatement = createPreparedUpdate(id, true, entity, subTable, connection, true);
327379
}
328-
// LOG.info("{}", entityCount);
380+
// Update linked stop times fields for updated pattern stop (e.g., timepoint, pickup/drop off type).
381+
if ("pattern_stops".equals(subTable.name)) {
382+
updateLinkedFields(subTable, entity, "stop_times", "pattern_id", "timepoint", "drop_off_type", "pickup_type", "shape_dist_traveled");
383+
}
329384
setStatementParameters(entity, subTable, insertStatement, connection);
330385
if (entityCount == 0) LOG.info(insertStatement.toString());
331386
insertStatement.addBatch();

src/main/java/com/conveyal/gtfs/loader/Table.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public Table (String name, Class<? extends Entity> entityClass, Requirement requ
129129
new ColorField("route_text_color", OPTIONAL),
130130
// Editor fields below.
131131
new ShortField("publicly_visible", EDITOR, 1),
132+
new ShortField("wheelchair_accessible", EDITOR, 2).permitEmptyValue(),
132133
// Status values are In progress (0), Pending approval (1), and Approved (2).
133134
new ShortField("status", EDITOR, 2)
134135
).addPrimaryKey();
@@ -550,7 +551,7 @@ public Field getFieldForName(String name) {
550551
}
551552

552553
/**
553-
* Gets the key field for the table.
554+
* Gets the key field for the table. Calling this on a table that has no key field is meaningless.
554555
*
555556
* FIXME: Should this return null if hasUniqueKeyField is false? Not sure what might break if we change this...
556557
*/

0 commit comments

Comments
 (0)