...
 
Commits (5)
......@@ -26,9 +26,9 @@ package de.sekmi.histream;
* Extensions allow additional information to be stored and retrieved
* for observations.
*
* @author Raphael
* @author R.W.Majeed
*
* @param <T> type class
* @param <T> slot type to be kept in memory with the observation
*/
public interface Extension<T>{
/**
......@@ -58,6 +58,23 @@ public interface Extension<T>{
* TODO change return type to array, to register all compatible classes
* @return instance type
*/
Iterable<Class<? super T>> getInstanceTypes();
Class<?>[] getInstanceTypes();
Class<T> getSlotType();
/**
* Extract subtype information from the slot type.
* E.g. a visit store can provide info about the patient
* @param slotInstance slot instance type
* @param subtype subtype to retrieve
* @return subtype instance
*/
<U> U extractSubtype(T slotInstance, Class<U> subtype);
public static <U,T> U extractSupertype(T slotInstance, Class<U> supertype){
if( supertype.isInstance(slotInstance) ) {
return supertype.cast(slotInstance);
}else {
throw new IllegalArgumentException("Unsupported supertype "+supertype);
}
}
}
package de.sekmi.histream.ext;
import java.util.List;
public interface PatientVisitStore {
Patient findPatient(String patientId);
Visit findVisit(String visitId);
void merge(Patient patient, String additionalId, ExternalSourceType source);
/**
* Get alias ids for the given patient (e.g. resulting from a merge)
* @param patient patient instance
* @return alias ids
*/
String[] getPatientAliasIds(Patient patient);
/**
* Deletes the patient identified by given id. This method does not remove any other associated
* data e.g. like visits, observations.
* @param id patient id
*/
void purgePatient(String patientId);
void purgeVisit(String visitId);
List<? extends Visit> allVisits(Patient patient);
}
......@@ -9,6 +9,7 @@ import java.util.Map;
* @author R.W.Majeed
*
*/
@Deprecated
public class CachedPatientExtension extends SimplePatientExtension {
private Map<String, PatientImpl> cache;
......
package de.sekmi.histream.impl;
import java.util.Arrays;
/*
* #%L
* histream
......@@ -35,11 +32,12 @@ import de.sekmi.histream.ext.Patient;
* @author R.W.Majeed
*
*/
@Deprecated
public class SimplePatientExtension implements Extension<PatientImpl>{
private final static Iterable<Class<? super PatientImpl>> TYPES = Arrays.asList(Patient.class, PatientImpl.class);
private final static Class<?>[] TYPES = new Class[] {Patient.class, PatientImpl.class};
@Override
public Iterable<Class<? super PatientImpl>> getInstanceTypes() {return TYPES;}
public Class<?>[] getInstanceTypes() {return TYPES;}
@Override
public PatientImpl createInstance(Object... args) {
......@@ -63,4 +61,14 @@ public class SimplePatientExtension implements Extension<PatientImpl>{
return patient;
}
@Override
public Class<PatientImpl> getSlotType() {
return PatientImpl.class;
}
@Override
public <U> U extractSubtype(PatientImpl slotInstance, Class<U> subtype) {
return extractSubtype(slotInstance, subtype);
}
}
package de.sekmi.histream.impl;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import de.sekmi.histream.Observation;
import de.sekmi.histream.Extension;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.Visit;
public class SimplePatientVisitExtension implements Extension<VisitPatientImpl>{
private final static Class<?>[] TYPES = new Class[] {Visit.class,VisitPatientImpl.class,Patient.class,PatientImpl.class};
@Override
public Class<?>[] getInstanceTypes() {return TYPES;}
@Override
public VisitPatientImpl createInstance(Object... args) {
if( args.length != 3
|| !(args[0] instanceof String)
|| !(args[1] instanceof String)
|| !(args[2] instanceof ExternalSourceType) )
{
throw new IllegalArgumentException("Need arguments Patient id, Visit id, ExternalSourceType");
}
ExternalSourceType source = (ExternalSourceType)args[2];
PatientImpl patient = new PatientImpl();
patient.setId((String)args[0]);
patient.setSourceId(source.getSourceId());
patient.setSourceTimestamp(source.getSourceTimestamp());
VisitPatientImpl visit = new VisitPatientImpl((String)args[1], patient, null);
visit.setSourceId(source.getSourceId());
visit.setSourceTimestamp(source.getSourceTimestamp());
return visit;
}
@Override
public VisitPatientImpl createInstance(Observation observation) {
VisitPatientImpl visit = createInstance(observation.getPatientId(), observation.getEncounterId(), observation.getSource());
//visit.setId();
//visit.setPatientId(observation.getPatientId());
//visit.setSourceId(observation.getSourceId());
//visit.setSourceTimestamp(observation.getSourceTimestamp());
return visit;
}
@Override
public Class<VisitPatientImpl> getSlotType() {
return VisitPatientImpl.class;
}
@Override
public <U> U extractSubtype(VisitPatientImpl slotInstance, Class<U> subtype) {
if( subtype.isAssignableFrom(PatientImpl.class) ){
return subtype.cast(slotInstance.getPatient());
}else if( subtype.isInstance(slotInstance) ) {
return subtype.cast(slotInstance);
}else {
throw new IllegalArgumentException("Unsupported subtype "+subtype);
}
}
}
......@@ -23,18 +23,18 @@ package de.sekmi.histream.impl;
import de.sekmi.histream.Observation;
import java.util.Arrays;
import de.sekmi.histream.Extension;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.Visit;
@Deprecated
public class SimpleVisitExtension implements Extension<VisitImpl>{
private final static Iterable<Class<? super VisitImpl>> TYPES = Arrays.asList(Visit.class, VisitImpl.class);
private final static Class<?>[] TYPES = new Class[] {Visit.class, VisitImpl.class};
@Override
public Iterable<Class<? super VisitImpl>> getInstanceTypes() {return TYPES;}
public Class<?>[] getInstanceTypes() {return TYPES;}
@Override
public VisitImpl createInstance(Object... args) {
......@@ -65,4 +65,13 @@ public class SimpleVisitExtension implements Extension<VisitImpl>{
return visit;
}
@Override
public Class<VisitImpl> getSlotType() {
return VisitImpl.class;
}
@Override
public <U> U extractSubtype(VisitImpl slotInstance, Class<U> subtype) {
return extractSubtype(slotInstance, subtype);
}
}
......@@ -30,6 +30,7 @@ import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.StoredExtensionType;
import de.sekmi.histream.ext.Visit;
@Deprecated
public class VisitImpl extends StoredExtensionType implements Visit {
private DateTimeAccuracy startTime;
private DateTimeAccuracy endTime;
......
package de.sekmi.histream.impl;
import java.util.Objects;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.ext.StoredExtensionType;
import de.sekmi.histream.ext.Visit;
public class VisitPatientImpl extends StoredExtensionType implements Visit {
private PatientImpl patient;
private DateTimeAccuracy startTime;
private DateTimeAccuracy endTime;
private Status status;
private String locationId;
private String providerId;
/**
* Empty constructor protected, only
* available to overriding classes.
*/
protected VisitPatientImpl() {
}
public VisitPatientImpl(String id, PatientImpl patient, DateTimeAccuracy startTime){
setId(id);
setPatient(patient);
this.startTime = startTime;
}
public String getPatientId(){return patient.getId();}
public void setPatient(PatientImpl patient){
Objects.requireNonNull(patient);
// patient id should not be changed normally.
this.patient = patient;
markDirty(true);
}
public PatientImpl getPatient() {
return this.patient;
}
@Override
public DateTimeAccuracy getStartTime() {
return startTime;
}
@Override
public DateTimeAccuracy getEndTime() {
return endTime;
}
@Override
public Status getStatus() {
return this.status;
}
@Override
public void setStatus(Status status) {
checkAndUpdateDirty(this.status, status);
this.status = status;
}
@Override
public String getLocationId() {
return locationId;
}
@Override
public void setLocationId(String locationId){
checkAndUpdateDirty(this.locationId, locationId);
this.locationId = locationId;
}
@Override
public void setEndTime(DateTimeAccuracy endTime) {
checkAndUpdateDirty(this.endTime, endTime);
this.endTime = endTime;
}
@Override
public void setStartTime(DateTimeAccuracy startTime) {
checkAndUpdateDirty(this.startTime, startTime);
this.startTime = startTime;
}
@Override
public String getProviderId() {
return this.providerId;
}
@Override
public void setProviderId(String providerId) {
checkAndUpdateDirty(this.providerId, providerId);
this.providerId = providerId;
}
}
......@@ -32,6 +32,15 @@ public class TestFileObservationSuppliers {
s.close();
p.close();
}
@Test
public void verifyMinimalXML() throws Exception{
GroupedXMLProvider p = new GroupedXMLProvider(null);
ObservationSupplier s = p.createSupplier(getClass().getResourceAsStream("/min.xml"), factory);
Assert.assertTrue( s.stream().count() > 0 );
s.close();
p.close();
}
@Test
public void verifyFlatSupplier() throws Exception{
FlatProviderFactory p = new FlatProviderFactory(null);
......
<eav-data xmlns="http://sekmi.de/histream/ns/eav-data"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- chronologisch impliziert, dass der zeitstempel eines nachfolgenden elementes
größer als alle vorangehenden elemente sein muss. Der Zeitstempel kann vor dem
Encounter-Start liegen -->
<meta>
<!-- Zeitpunkt, an dem der Export erstellt wurde bzw. Datenstand -->
<etl-strategy>replace-visit</etl-strategy>
<source timestamp="2015-04-21T06:58:00Z" id="test"/>
</meta>
<patient id="XX12345">
<given-name>A B</given-name>
<surname>Dampf</surname>
<!-- Gender allows for female, male, indeterminate -->
<!-- Gender element can also be removed or left empty -->
<gender>female</gender>
<encounter id="XXE12345">
<start>2014-01-01T10:30:00</start>
<fact concept="T:date:secs" start="2014-09-07T10:40:03"/>
</encounter>
</patient>
</eav-data>
......@@ -17,11 +17,6 @@
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
......@@ -33,5 +28,17 @@
<artifactId>histream-core</artifactId>
<version>0.16-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.4.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
\ No newline at end of file
package de.sekmi.histream.i2b2;
import java.time.temporal.ChronoUnit;
import de.sekmi.histream.ext.Visit;
import de.sekmi.histream.impl.PatientImpl;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import de.sekmi.histream.impl.VisitPatientImpl;
/**
* I2b2 visit. The active encounter_ide is returned by {@link #getId()}.
*
* @author Raphael
*
*/
public class I2b2PatientVisit extends VisitPatientImpl {
/**
* I2b2 internal encounter id (32bit integer)
*/
private int encounter_num;
private int patient_num;
/**
* String id aliases for the encounter
*/
String[] aliasIds;
/**
* Index in aliasIds for the primary alias
*/
int primaryAliasIndex;
int maxInstanceNum;
public I2b2PatientVisit(int encounter_num, int patient_num) {
super();
this.encounter_num = encounter_num;
this.patient_num = patient_num;
maxInstanceNum = 1;
// TODO set startDate, endDate
}
public int getNum(){return encounter_num;}
public int getPatientNum(){return patient_num;}
@Override
public void setPatient(PatientImpl patient) {
super.setPatient(patient);
if( patient instanceof I2b2Patient ) {
// also set the patient_num
int patient_num = ((I2b2Patient)patient).getNum();
this.patient_num = patient_num;
}else {
throw new IllegalArgumentException("Patient expected of instanceOf I2b2Patient");
}
}
@Override
public String toString(){
return "I2b2Visit(encounter_um="+encounter_num+")";
}
/**
* Get the i2b2 vital_status_cd for this visit.
* @return vital status code, see CRC_Design doc
*/
public String getActiveStatusCd(){
Visit visit = this;
char end_char=0, start_char=0;
if( visit.getEndTime() != null ){
switch( visit.getEndTime().getAccuracy() ){
case DAYS:
end_char = 0; // same meaning
end_char = 'Y';
break;
case MONTHS:
end_char = 'M';
break;
case YEARS:
end_char = 'X';
break;
case HOURS:
end_char = 'R';
break;
case MINUTES:
end_char = 'T';
break;
case SECONDS:
end_char = 'S';
break;
default:
}
}else{
// null end date
// U: unknown, O: ongoing
// default to unknown
end_char = 'U';
}
// start date
if( visit.getStartTime() != null ){
switch( visit.getStartTime().getAccuracy() ){
case DAYS:
start_char = 0; // same meaning
start_char = 'D';
break;
case MONTHS:
start_char = 'B';
break;
case YEARS:
start_char = 'F';
break;
case HOURS:
start_char = 'H';
break;
case MINUTES:
start_char = 'I';
break;
case SECONDS:
start_char = 'C';
break;
default:
}
}else{
// null start date
// L: unknown, A: active
// default to unknown
start_char = 'L';
}
if( end_char != 0 && start_char != 0 )
return new String(new char[]{end_char,start_char});
else if( end_char != 0 )
return new String(new char[]{end_char});
else if( start_char != 0 )
return new String(new char[]{start_char});
else return null; // should not happen
}
/**
* For decoding instructions, see the i2b2 documentation CRC_Design.pdf
* The vital cd can be one or two characters.
* This implementation is more failsafe by using the following
* algorithm:
* <ol>
* <li>For {@code null} or {@code ""} use both timestamps accurate to day
* <li>Try to decode first character as end indicator</li>
* <li>If {@code vital_cd.length > 1} use second character as start indicator, otherwise if unable to decode the end indicator, use the first character.</li>
* </ol>
* @param vital_cd code to indicate accuracy of start and end date
*/
public void setActiveStatusCd(String vital_cd){
Visit visit = this;
// load accuracy
char endIndicator = 0;
char startIndicator = 0;
if( vital_cd == null || vital_cd.length() == 0 ){
// start and end date accurate to day
// leave indicators at 0/null
}else{
// load first indicator character
endIndicator = vital_cd.charAt(0);
}
ChronoUnit accuracy = null;
// end date indicator
switch( endIndicator ){
case 'U': // unknown, no date
case 'O': // ongoing, no date
// set to null
visit.setEndTime(null);
break;
case 0:
case 'Y': // known, accurate to day
accuracy = ChronoUnit.DAYS;
break;
case 'M': // known, accurate to month
accuracy = ChronoUnit.MONTHS;
break;
case 'X': // known, accurate to year
accuracy = ChronoUnit.YEARS;
break;
case 'R': // known, accurate to hour
accuracy = ChronoUnit.HOURS;
break;
case 'T': // known, accurate to minute
accuracy = ChronoUnit.MINUTES;
break;
case 'S': // known, accurate to second
accuracy = ChronoUnit.SECONDS;
break;
default:
// no end indicator means accurate to day
accuracy = ChronoUnit.DAYS;
// no match for end date -> check for start status in first character
startIndicator = endIndicator;
}
// set accuracy for end time
if( visit.getEndTime() != null && accuracy != null ){
visit.getEndTime().setAccuracy(accuracy);
}
// load start indicator
if( vital_cd != null && vital_cd.length() > 1 ){
// use second character, if available
startIndicator = vital_cd.charAt(1);
}// otherwise, the first character is used if end indicator wasn't used. See default case above
accuracy = null;
// start date indicator
switch( startIndicator ){
case 'L': // unknown, no date
case 'A': // active, no date
setStartTime(null);
break;
case 0: // same as D
case 'D': // known, accurate to day
accuracy = ChronoUnit.DAYS;
break;
case 'B': // known, accurate to month
accuracy = ChronoUnit.MONTHS;
break;
case 'F': // known, accurate to year
accuracy = ChronoUnit.YEARS;
break;
case 'H': // known, accurate to hour
accuracy = ChronoUnit.HOURS;
break;
case 'I': // known, accurate to minute
accuracy = ChronoUnit.MINUTES;
break;
case 'C': // known, accurate to second
accuracy = ChronoUnit.SECONDS;
break;
default: // default to days if unable to parse
accuracy = ChronoUnit.DAYS;
}
if( visit.getStartTime() != null && accuracy != null ){
visit.getStartTime().setAccuracy(accuracy);
}
}
public String getInOutCd(){
Visit patient = this;
if( patient.getStatus() == null )return null;
else switch( patient.getStatus() ){
case Inpatient:
return "I";
case Outpatient:
case Emergency: // unsupported by i2b2, map to outpatient
return "O";
default:
// XXX should not happen, warning
return null;
}
}
}
......@@ -35,6 +35,7 @@ import de.sekmi.histream.impl.VisitImpl;
* @author Raphael
*
*/
@Deprecated
public class I2b2Visit extends VisitImpl {
/**
......@@ -73,6 +74,8 @@ public class I2b2Visit extends VisitImpl {
// also set the patient_num
int patient_num = ((I2b2Patient)patient).getNum();
this.patient_num = patient_num;
}else {
throw new IllegalArgumentException("Patient expected of instanceOf I2b2Patient");
}
}
......
package de.sekmi.histream.i2b2;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.Patient;
import de.sekmi.histream.ext.StoredExtensionType;
import de.sekmi.histream.ext.Patient.Sex;
/**
* Patient cache which synchronizes with i2b2 patient_dimension and patient_mapping tables.
* Required non-null columns are patient_num, update_date.
* <p>
* The method {@link #open(Connection, String, DataDialect)} will load all patients (relevant to the selected project)
* into a memory structure based on a hash table.
* <p>
* Some optional columns are used: vital_status_cd, birth_date, death_date, sex_cd, download_date, sourcesystem_cd
* <p>
* The store should be separate from the extension? E.g. I2b2PatientExtension which can operate
* on PostgresStore, OracleStore, etc.
* HistreamPatientExtension can also operate on I2b2PostgresStore?
* <p>
* The patient_mapping table is used to map actual (source) patient_ide to internal patient_num for facts.
* The patient_mapping table is also used to store patient merge events (eg. different patient_ide referring to the same patient_num
* The Observation stream will still use the actual source patient_ide and encounter_ide
* <p>
* In the patient_mapping table, patient_ide_status can assume values Active, Inactive, Deleted, Merged.
* The 'Active' patient_mapping for the selected project is used as primary patient id,
* all other rows are used as alias ids.
*
* @author marap1
*
*/
public class PostgresPatientCache implements Closeable{
private static final Logger log = Logger.getLogger(PostgresPatientCache.class.getName());
protected String projectId;
protected String idSourceDefault;
protected char idSourceSeparator;
protected Connection db;
protected int fetchSize;
// TODO read only flag!!!!!! XXX
// private String autoInsertSourceId;
// maximum patient number, used to generate new patient_num for new patients
private int maxPatientNum;
//private static ChronoUnit[] map_date_units = {ChronoUnit.DAYS, ChronoUnit.MONTHS, ChronoUnit.YEARS, ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS};
//private static char[] map_death_chars = {};
private Hashtable<Integer, I2b2Patient> patientCache;
private Hashtable<String, I2b2Patient> idCache;
private PreparedStatement insert;
private PreparedStatement insertIde;
private PreparedStatement update;
/*
private PreparedStatement select;
private PreparedStatement selectIde;
*/
private PreparedStatement selectAll;
private PreparedStatement selectAllIde;
private PreparedStatement deletePatientSource;
private PreparedStatement deleteMapSource;
protected DataDialect dialect;
// /**
// * Construct new postgres patient store. In addition to properties
// * needed by {@link PostgresExtension#PostgresExtension(Map)},
// * the following properties are needed:
// * <p>jdbc.{host|port|database} or data.jdbc.{host|port|database} to
// * construct the database URI.
// * Any other parameters under jdbc. or data.jdbc. are passed to the
// * JDBC connect method.
// *
// * <p>project,
// * <p>Optional properties:
// * <p>
// * idSourceDefault ('HIVE'), idSourceSeparator (single char, ':')
// * fetchSize (int, 10000)
// * @param configuration configuration
// * @throws SQLException if the preparation or initialisation fails
// * @throws ClassNotFoundException if postgresql driver not found
// */
// public PostgresPatientStore(Map<String,String> configuration) throws ClassNotFoundException, SQLException {
// super(configuration);
// this.projectId = config.get("project");
// openDatabase(new String[]{"jdbc.","data.jdbc."});
// initialize();
// }
//
//
// /**
// * Create a patient store using a {@link DataSource}.
// * The project id must be specified with the key {@code project}.
// * @param ds data source for the connection
// * @param configuration configuration settings
// * @throws SQLException SQL error
// */
// public PostgresPatientStore(DataSource ds, Map<String,String> configuration) throws SQLException{
// super(configuration);
// this.projectId = config.get("project");
// openDatabase(ds);
// initialize();
// }
public PostgresPatientCache(){
this.idSourceDefault = "HIVE";
this.idSourceSeparator = ':';
this.fetchSize = 1000;
}
public void open(Connection connection, String projectId, DataDialect dialect) throws SQLException{
this.db = connection;
this.projectId = projectId;
this.dialect = dialect;
// require project id
Objects.requireNonNull(this.projectId, "non-null projectId required");
// this.autoInsertSourceId = "HS.auto";
patientCache = new Hashtable<>(1000);
idCache = new Hashtable<>(1000);
prepareStatements();
loadMaxPatientNum();
batchLoad();
}
private I2b2Patient getCached(int patient_num){
return patientCache.get(patient_num);
}
public I2b2Patient lookupPatientNum(Integer patient_num){
return getCached(patient_num);
}
public I2b2Patient lookupPatientId(String patient_id) {
return getCached(patient_id);
}
private I2b2Patient getCached(String patient_id){
return idCache.get(patient_id);
}
private void loadMaxPatientNum() throws SQLException{
try( Statement s = db.createStatement() ){
String sql = "SELECT MAX(patient_num) FROM patient_dimension";
ResultSet rs = s.executeQuery(sql);
rs.next(); // statement will always return exactly one row
maxPatientNum = rs.getInt(1);
if( rs.wasNull() ){
// patient_dimension is empty
maxPatientNum = 0;
// numbering will start with 1
// this is a bit redundant, since getInt()
// will also return 0 on NULL values
}
rs.close();
}
log.info("MAX(patient_num) = "+maxPatientNum);
}
/**
* This method is called from {@link #open(Connection, String, DataDialect)}. Override to prepare additional statements.
* @throws SQLException SQL error
*/
protected void prepareStatements()throws SQLException{
db.setAutoCommit(true);
if( projectId == null ){
log.warning("property project is null, some things might fail");
}
// TODO: use prefix from configuration to specify tablespace
insert = db.prepareStatement("INSERT INTO patient_dimension(patient_num, import_date, sourcesystem_cd) VALUES(?,current_timestamp,?)");
insertIde = db.prepareStatement("INSERT INTO patient_mapping(patient_ide, patient_ide_source, patient_num, patient_ide_status, project_id, import_date, download_date, sourcesystem_cd) values (?,?,?,?,'"+projectId+"',current_timestamp,?,?)");
update = db.prepareStatement("UPDATE patient_dimension SET vital_status_cd=?, birth_date=?, death_date=?, sex_cd=?, update_date=current_timestamp, download_date=?, sourcesystem_cd=? WHERE patient_num=?");
/*
selectIde = db.prepareStatement("SELECT m.patient_num, m.patient_ide_status FROM patient_mapping m WHERE m.patient_ide=? AND patient_ide_source=? AND m.project_id='"+projectId+"'");
select = db.prepareStatement("SELECT p.patient_num, p.vital_status_cd, p.birth_date, p.death_date, p.sex_cd, p.download_date, p.sourcesystem_cd, m.patient_ide, m.patient_ide_source, m.patient_ide_status FROM patient_mapping m, patient_dimension p WHERE m.patient_num=p.patient_num AND m.patient_ide=? AND patient_ide_source=? AND m.project_id='"+projectId+"'");
*/
//selectAll = db.prepareStatement("SELECT p.patient_num, p.vital_status_cd, p.birth_date, p.death_date, p.sex_cd, p.download_date, p.sourcesystem_cd, m.patient_ide, m.patient_ide_source, m.patient_ide_status FROM patient_mapping m, patient_dimension p WHERE m.patient_num=p.patient_num AND m.project_id='"+projectId+"'");
// TODO select only patients relevant to the current project: eg. join patient_dimension with patient_mapping to get only relevant rows.
selectAll = db.prepareStatement("SELECT patient_num, vital_status_cd, birth_date, death_date, sex_cd, download_date, sourcesystem_cd FROM patient_dimension", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
selectAll.setFetchSize(this.fetchSize);
selectAllIde = db.prepareStatement("SELECT patient_num, patient_ide, patient_ide_source, patient_ide_status, project_id FROM patient_mapping WHERE project_id='"+projectId+"' ORDER BY patient_num", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
selectAllIde.setFetchSize(this.fetchSize);
deletePatientSource = db.prepareStatement("DELETE FROM patient_dimension WHERE sourcesystem_cd=?");
deleteMapSource = db.prepareStatement("DELETE FROM patient_mapping WHERE sourcesystem_cd=?");
}
/**
* Returns the number of patients which are currently available in memory.
* The number might grow as more patients are loaded from the database.
* @return number of patients.
*/
public int size(){
return patientCache.size();
}
private void batchLoad() throws SQLException{
try( ResultSet rs = selectAll.executeQuery() ){
int count = 0;
while( rs.next() ){
count ++;
I2b2Patient patient = loadFromResultSet(rs);
// put in numeric patient cache
patientCache.put(patient.getNum(), patient);
}
log.info("Loaded patient records: "+count);
}
try( ResultSet rs = selectAllIde.executeQuery() ){
I2b2Patient p;
ArrayList<String> ids = new ArrayList<>(16);
// count loaded IDs for logging
int total_count = 0, project_count=0;
int num = -1; // current patient number
while( rs.next() ){
total_count ++;
if( num == -1 ){ // first patient
num = rs.getInt(1);
}else if( num != rs.getInt(1) ){
// next patient
// cache ids for current patients
p = getCached(num);
if( p == null ){
// found row in patient_mapping which
// doesn't correspond to any row in patient_num
log.warning("No match for patient_num="+num+" in patient_dimension (see encounter_mapping.id='"+rs.getString(2)+"'");
}else if( ids.size() > 0 ){
p.mergedIds = new String[ids.size()];
p.mergedIds = ids.toArray(p.mergedIds);
}
// proceed with next patient
num = rs.getInt(1);
ids.clear();
}
String id;
// no prefix for ids with source idSourceDefault
if( rs.getString(3).equals(idSourceDefault) )
id = rs.getString(2);
else id = rs.getString(3)+idSourceSeparator+rs.getString(2);
// active id in current project is used for patient.getId
if( rs.getString(4).equals("A") && rs.getString(5).equals(projectId) ){
p = getCached(num);
if( p != null ){
project_count ++;
p.setId(id);
p.markDirty(false);
}
}else // all other ids are aliases
ids.add(id);
}
if( num != -1 ){
// don't forget to process last num
p = getCached(num);
if( p == null ){
// found row in patient_mapping which
// doesn't correspond to any row in patient_num
log.warning("No match for patient_num="+num+" in patient_dimension from encounter_mapping");
}else if( ids.size() > 0 ){
p.mergedIds = new String[ids.size()];
p.mergedIds = ids.toArray(p.mergedIds);
}
}
log.info("Loaded "+total_count+" aliases with "+project_count+" project specific IDs");
if( project_count == 0 && total_count > 0 ){
log.warning("No project specific patient IDs. Maybe wrong projectId?");
}
}
// fill idCache
Enumeration<I2b2Patient> all = patientCache.elements();
while( all.hasMoreElements() ){
I2b2Patient p = all.nextElement();
// XXX how make sure all patients have setId/getId set?
if( p.getId() != null )idCache.put(p.getId(), p);
if( p.mergedIds != null ){
for( int i=0; i<p.mergedIds.length; i++ ){
idCache.put(p.mergedIds[i], p);
}
}
}
}
/*
private I2b2Patient loadById(String id) throws IOException{
IdWithSource ide = new IdWithSource(id);
int patient_num;
// lookup identity string
synchronized( selectIde ){
try {
selectIde.setString(1, ide.ide);
selectIde.setString(2, ide.ids);
} catch (SQLException e) {
throw new IOException(e);
}
try( ResultSet rs = selectIde.executeQuery() ){
if( !rs.next() )
return null;// patient not found
patient_num = rs.getInt(1);
}catch( SQLException e ){
throw new IOException(e);
}
}
I2b2Patient pat = getCached(patient_num);
if( pat != null ){
// patient already cached, return reference
return pat;
}
// load patient with (internal) numeric id
synchronized( select ){
try {
select.setInt(1, patient_num);
} catch (NumberFormatException | SQLException e) {
throw new IOException(e);
}
try( ResultSet rs = select.executeQuery() ){
return loadFromResultSet(rs);
} catch (SQLException e) {
throw new IOException(e);
}
}
}
*/
private void updateStorage(I2b2Patient patient) throws SQLException {
synchronized( update ){
update.setString(1, patient.getVitalStatusCd());
update.setTimestamp(2, dialect.encodeInstantPartial(patient.getBirthDate()));
update.setTimestamp(3, dialect.encodeInstantPartial(patient.getDeathDate()));
update.setString(4, getSexCd(patient));
if( patient.getSourceTimestamp() != null ){
update.setTimestamp(5, dialect.encodeInstant(patient.getSourceTimestamp()));
}else{
update.setTimestamp(5, null);
}
update.setString(6, patient.getSourceId());
update.setInt(7, patient.getNum());
update.executeUpdate();
patient.markDirty(false);
}
}
/**
* Insert a new patient into the database. Only patient_num and sourcesystem_cd are filled.
* @param patient patient object
* @throws SQLException if INSERT failed
*/
private void insertPatient(I2b2Patient patient) throws SQLException{
synchronized( insert ){
insert.setInt(1, patient.getNum() );
insert.setString(2, patient.getSourceId());
insert.executeUpdate();
patient.markDirty(false);
}
}
/**
* Get the i2b2 sex_cd for a patient. Currently, only M and F are supported.
* @param patient patient object
* @return i2b2 sex_cd
*/
private static String getSexCd(Patient patient){
if( patient.getSex() == null )return null;
else switch( patient.getSex() ){
case female:
return "F";
case male:
return "M";
case indeterminate:
return "X";
default:
// XXX should not happen, warning
return null;
}
}
private I2b2Patient loadFromResultSet(ResultSet rs) throws SQLException{
int id = rs.getInt(1);
// load vital status code, which contains information about
// accuracy of birth and death dates.
String vital_cd = rs.getString(2);
// make sure that non-null vital code contains at least one character
if( vital_cd == null || vital_cd.length() == 0 )vital_cd = null;
// birth date
DateTimeAccuracy birthDate = dialect.decodeInstantPartial(rs.getTimestamp(3));
DateTimeAccuracy deathDate = dialect.decodeInstantPartial(rs.getTimestamp(4));
;
// load sex
String sex_cd = rs.getString(5);
Sex sex = null;
if( sex_cd != null ){
switch( sex_cd.charAt(0) ){
case 'F':
sex = Sex.female;
break;
case 'M':
sex = Sex.male;
break;
case 'X':
sex = Sex.indeterminate;
break;
default:
sex_cd = null; // unknown
}
}
I2b2Patient patient = new I2b2Patient(id, sex, birthDate, deathDate);
if( rs.getTimestamp(6) != null ){
patient.setSourceTimestamp(dialect.decodeInstant(rs.getTimestamp(6)));
}
patient.setSourceId(rs.getString(7));
patient.setVitalStatusCd(vital_cd);
patient.markDirty(false);
return patient;
}
/*
private void retrievalException(String id, IOException e) {
log.log(Level.SEVERE, "Unable to retrieve patient "+id, e);
}*/
private void insertionException(I2b2Patient patient, SQLException e) {
log.log(Level.SEVERE, "Unable to insert patient "+patient.getId(), e);
}
private void updateException(I2b2Patient patient, SQLException e) {
log.log(Level.SEVERE, "Unable to update patient "+patient.getId(), e);
}
private String[] splitId(String id){
String ide;
String ids;
int p = id.indexOf(idSourceSeparator);
if( p == -1 ){
// id does not contain source
ids = idSourceDefault;
ide = id;
}else{
// id contains source, separate from id
ids = id.substring(0, p);
ide = id.substring(p+1);
}
return new String[]{ids,ide};
}
private void insertIde(int patient_num, String id, String status, ExternalSourceType source)throws SQLException{
String[] ids = splitId(id);
insertIde.setString(1, ids[1]);
insertIde.setString(2, ids[0]);
insertIde.setInt(3, patient_num);
insertIde.setString(4, status);
insertIde.setTimestamp(5, dialect.encodeInstant(source.getSourceTimestamp()));
insertIde.setString(6, source.getSourceId());
insertIde.executeUpdate();
}
public I2b2Patient createPatient(String patientId, ExternalSourceType source){
I2b2Patient pat = getCached(patientId);
if( pat == null ){
// string id not known to cache
// create new patient
maxPatientNum ++;
int num = maxPatientNum;
pat = new I2b2Patient(num);
pat.setId(patientId);
// don't use source metadata, since we only know the patient id
pat.setSourceId(source.getSourceId());
pat.setSourceTimestamp(source.getSourceTimestamp());
// put in cache and insert into storage
patientCache.put(num, pat);
idCache.put(pat.getId(), pat);
try {
insertPatient(pat);
// insert ide into patient_mapping
insertIde(num, pat.getId(), "A", source);
} catch (SQLException e) {
insertionException(pat, e);
}
// commonly, the item is modified after a call to this method,
// but changes are written later via a call to update.
// (otherwise, the instance would need to know whether to perform INSERT or UPDATE)
}
return pat;
}
// @SuppressWarnings("unused")
// @Override
// public void merge(Patient patient, String additionalId, ExternalSourceType source) {
// I2b2Patient p = (I2b2Patient)patient;
// if( true )throw new UnsupportedOperationException();
// // TODO add additionalId to patient.mergedIds
//
// try {
// insertIde(p.getNum(), additionalId, "M", source);
// } catch (SQLException e) {
// log.log(Level.SEVERE, "Unable to insert patient merge", e);
// }
// }
// @Override
// public String[] getAliasIds(Patient patient) {
// I2b2Patient p = (I2b2Patient)patient;
// return p.mergedIds;
// }
//
public void deleteWhereSourceId(String sourceId) throws SQLException {
deletePatientSource.setString(1, sourceId);
int numRows = deletePatientSource.executeUpdate();
log.info("Deleted "+numRows+" rows with sourcesystem_cd = "+sourceId);
deleteMapSource.setString(1, sourceId);
deleteMapSource.executeUpdate();
// find matching patients in cache
Enumeration<I2b2Patient> all = patientCache.elements();
LinkedList<I2b2Patient>remove = new LinkedList<>();
while( all.hasMoreElements() ){
I2b2Patient p = all.nextElement();
if( p.getSourceId() != null && p.getSourceId().equals(sourceId) ){
remove.add(p); // remove later, otherwise the Enumeration might fail
}
// XXX does not work with sourceId == null
}
// remove patients from cache
for( I2b2Patient p : remove ){
patientCache.remove(p.getNum());
if( p.getId() != null )
idCache.remove(p.getId());
if( p.mergedIds != null )for( String id : p.mergedIds ){
idCache.remove(id);
}
}
// XXX some ids might remain in patient_mapping, because we don't store the patient_mapping sourcesystem_cd
// usually this should work, as we assume sourcesystem_cd to be equal for patients in both tables
// reload MAX(patient_num)
loadMaxPatientNum();
}
public void flush(){
int count = 0;
Iterator<I2b2Patient> dirty = StoredExtensionType.dirtyIterator(patientCache.elements());
while( dirty.hasNext() ){
I2b2Patient patient = dirty.next();
try {
updateStorage(patient);
count ++;
} catch (SQLException e) {
updateException(patient, e);
}
}
log.info("Updated "+count+" patients in database");
}
@Override
public synchronized void close() throws IOException {
if( db != null ){
flush();
try {
db.close();
} catch (SQLException e) {
throw new IOException(e);
}
db = null;
}
}
}
......@@ -30,7 +30,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
......@@ -41,6 +40,7 @@ import java.util.logging.Logger;
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.Extension;
import de.sekmi.histream.Observation;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.Patient;
......@@ -74,7 +74,7 @@ import de.sekmi.histream.ext.PatientStore;
*/
public class PostgresPatientStore extends PostgresExtension<I2b2Patient> implements PatientStore, Closeable{
private static final Logger log = Logger.getLogger(PostgresPatientStore.class.getName());
private static final Iterable<Class<? super I2b2Patient>> INSTANCE_TYPES = Arrays.asList(Patient.class, I2b2Patient.class);
private static final Class<?>[] INSTANCE_TYPES = new Class[] {Patient.class, I2b2Patient.class};
private String projectId;
private String idSourceDefault;
private char idSourceSeparator;
......@@ -541,7 +541,7 @@ public class PostgresPatientStore extends PostgresExtension<I2b2Patient> impleme
}
@Override
public Iterable<Class<? super I2b2Patient>> getInstanceTypes() {
public Class<?>[] getInstanceTypes() {
return INSTANCE_TYPES;
}
......@@ -649,5 +649,13 @@ public class PostgresPatientStore extends PostgresExtension<I2b2Patient> impleme
db = null;
}
}
@Override
public Class<I2b2Patient> getSlotType() {
return I2b2Patient.class;
}
@Override
public <U> U extractSubtype(I2b2Patient slotInstance, Class<U> subtype) {
return Extension.extractSupertype(slotInstance, subtype);
}
}
package de.sekmi.histream.i2b2;
import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
/*
* #%L
* histream
* %%
* Copyright (C) 2013 - 2015 R.W.Majeed
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.sekmi.histream.DateTimeAccuracy;
import de.sekmi.histream.ext.ExternalSourceType;
import de.sekmi.histream.ext.StoredExtensionType;
import de.sekmi.histream.ext.Visit.Status;
/**
* Visit cache which synchronizes with an i2b2 visit_dimension table.
* Required non-null columns are encounter_num, patient_num, update_date.
* <p>
* Some optional columns are used: active_status_cd, start_date, end_date, inout_cd, location_cd, sourcesystem_cd
* <p>
* XXX after loading encounters, the String patientId not set anymore and always null. To determine the patientId, the patientStore is required for lookup of the patientNum
* TODO use encounter_mapping table to map actual (source) patient_ide to internal patient_num for facts.
* <p>
* The variable argument list for {@link #createInstance(Object...)} requires the following arguments:
* {@link String}{@code visitId}, {@link I2b2Patient}{@code patient}, {@link ExternalSourceType}{@code source}.
*
* @author marap1
*
*/
public class PostgresPatientVisitCache extends PostgresPatientCache implements Closeable{
private static final Logger log = Logger.getLogger(PostgresPatientVisitCache.class.getName());
private int maxEncounterNum;
private Hashtable<Integer, I2b2PatientVisit> visitCache;
private Hashtable<String, I2b2PatientVisit> idCache;
/** if true, don't allow a change of patient for a given visit. */
private boolean rejectPatientChange;
private PreparedStatement insert;
private PreparedStatement insertMapping;
private PreparedStatement update;
// private PreparedStatement select;
private PreparedStatement selectAll;
private PreparedStatement selectMappingsAll;
private PreparedStatement deleteSource;
private PreparedStatement deleteMapSource;
// /**
// * Create a visit store using configuration settings.
// * The project id must be specified with the key {@code project}.
// * JDBC connection configuration is specified with the key
// * prefixes {@code jdbc.*} and {@code data.jdbc.*}
// * @param configuration key value pairs
// * @throws ClassNotFoundException database driver not found
// * @throws SQLException SQL exceptions
// */
// public PostgresVisitStore(Map<String,String> configuration) throws ClassNotFoundException, SQLException {
// super(configuration);
// this.projectId = config.get("project");
// openDatabase(new String[]{"jdbc.","data.jdbc."});
// initialize();
// }
//
// /**
// * Create a visit store using a {@link DataSource}.
// * The project id must be specified with the key {@code project}.
// * @param ds data source for the connection
// * @param configuration configuration settings
// * @throws SQLException SQL error
// */
// public PostgresVisitStore(DataSource ds, Map<String,String> configuration) throws SQLException{
// super(configuration);
// this.projectId = config.get("project");
// openDatabase(ds);
// initialize();
// }
public PostgresPatientVisitCache(){
this.rejectPatientChange = false;
}
@Override
public void open(Connection connection, String projectId, DataDialect dialect) throws SQLException{
// first load patients
super.open(connection, projectId, dialect);
visitCache = new Hashtable<>();
idCache = new Hashtable<>();
loadMaxEncounterNum();
batchLoad(); /// XXX loading visits does not set the String patientId, for that, the patientStore would be needed
}
@Override
protected void prepareStatements() throws SQLException {
super.prepareStatements();
// TODO: use prefix from configuration to specify tablespace
insert = db.prepareStatement("INSERT INTO visit_dimension(encounter_num, patient_num, import_date, download_date, sourcesystem_cd) VALUES(?,?,current_timestamp,?,?)");
insertMapping = db.prepareStatement("INSERT INTO encounter_mapping(encounter_num, encounter_ide, encounter_ide_source, patient_ide, patient_ide_source, encounter_ide_status, project_id, import_date, download_date, sourcesystem_cd) VALUES(?,?,?,?,?,'A','"+projectId+"',current_timestamp,?,?)");
update = db.prepareStatement("UPDATE visit_dimension SET patient_num=?, active_status_cd=?, start_date=?, end_date=?, inout_cd=?, location_cd=?, update_date=current_timestamp, download_date=?, sourcesystem_cd=? WHERE encounter_num=?");
//select = db.prepareStatement("SELECT encounter_num, patient_num, active_status_cd, start_date, end_date, inout_cd, location_cd, update_date, sourcesystem_cd FROM visit_dimension WHERE patient_num=?");
selectAll = db.prepareStatement("SELECT encounter_num, patient_num, active_status_cd, start_date, end_date, inout_cd, location_cd, download_date, sourcesystem_cd FROM visit_dimension", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
selectAll.setFetchSize(fetchSize);
selectMappingsAll = db.prepareStatement("SELECT encounter_num, encounter_ide, encounter_ide_source, patient_ide, patient_ide_source, encounter_ide_status, project_id FROM encounter_mapping ORDER BY encounter_num", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
selectMappingsAll.setFetchSize(fetchSize);
deleteSource = db.prepareStatement("DELETE FROM visit_dimension WHERE sourcesystem_cd=?");
deleteMapSource = db.prepareStatement("DELETE FROM encounter_mapping WHERE sourcesystem_cd=?");
}
public int size(){
return visitCache.size();
}
public void setRejectPatientChange(boolean rejectPatientChange){
this.rejectPatientChange = rejectPatientChange;
}
private void loadMaxEncounterNum() throws SQLException{
try( Statement s = db.createStatement() ){
String sql = "SELECT MAX(encounter_num) FROM visit_dimension";
ResultSet rs = s.executeQuery(sql);
if( rs.next() ){
maxEncounterNum = rs.getInt(1);
}else{
// patient_dimension is empty
// start numbering patients with 1
maxEncounterNum = 1;
}
rs.close();
}
log.info("MAX(encounter_num) = "+maxEncounterNum);
}
public I2b2PatientVisit lookupEncounterNum(Integer encounter_num){
return visitCache.get(encounter_num);
}
public void loadMaxInstanceNums() throws SQLException{
// TODO maybe better to load only encounters+max instance_num for current project -> join with encounter_mapping
log.info("Loading maximum instance_num for each encounter");
Statement stmt = db.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
final String sql = "SELECT patient_num, encounter_num, MAX(instance_num) FROM observation_fact GROUP BY patient_num, encounter_num";
int count = 0;
int noMatch = 0;
try( ResultSet rs = stmt.executeQuery(sql) ){
while( rs.next() ){
I2b2PatientVisit v = visitCache.get(rs.getInt(2));
if( v != null )v.maxInstanceNum = rs.getInt(3);
else noMatch ++;
count ++;
}
}
stmt.close();
log.info("Loaded MAX(instance_num) for "+count+" encounters");
if( noMatch != 0 ){
log.warning("Encountered "+noMatch+" encounter_num in observation_fact without matching visits");
}
}
private String pasteId(String source, String ide){