diff --git a/bindings/java/jni/dataModel_SleuthkitJNI.cpp b/bindings/java/jni/dataModel_SleuthkitJNI.cpp
index 5ce1cfc3988c070464c5dc552f54242a00f46c23..b9160399769732c37d230ffeca93a1dc8bc428c9 100644
--- a/bindings/java/jni/dataModel_SleuthkitJNI.cpp
+++ b/bindings/java/jni/dataModel_SleuthkitJNI.cpp
@@ -2276,6 +2276,15 @@ JNIEXPORT jboolean JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_isImageSupp
     return (jboolean) result;
 }
 
+/*
+* Returns the current Sleuthkit version as a long
+* @return the current version
+*/
+JNIEXPORT jlong JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_getSleuthkitVersionNat
+(JNIEnv * env, jclass obj) {
+    return (jlong)TSK_VERSION_NUM;
+}
+
 
 /*
  * Finish the image being created by image writer.
diff --git a/bindings/java/jni/dataModel_SleuthkitJNI.h b/bindings/java/jni/dataModel_SleuthkitJNI.h
index e40dfbd2aef039f0bc0719d3e04495dcbe8ff404..52e06743f70d20a621e8eeef662966f37812c056 100644
--- a/bindings/java/jni/dataModel_SleuthkitJNI.h
+++ b/bindings/java/jni/dataModel_SleuthkitJNI.h
@@ -431,6 +431,14 @@ JNIEXPORT jstring JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_getCurDirNat
 JNIEXPORT jboolean JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_isImageSupportedNat
   (JNIEnv *, jclass, jstring);
 
+/*
+ * Class:     org_sleuthkit_datamodel_SleuthkitJNI
+ * Method:    getSleuthkitVersionNat
+ * Signature: ()J
+ */
+JNIEXPORT jlong JNICALL Java_org_sleuthkit_datamodel_SleuthkitJNI_getSleuthkitVersionNat
+  (JNIEnv *, jclass);
+
 /*
  * Class:     org_sleuthkit_datamodel_SleuthkitJNI
  * Method:    finishImageWriterNat
diff --git a/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..ecd94704b74cdf989590cee7be892fc220c45ad5
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/CaseDatabaseFactory.java
@@ -0,0 +1,614 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.
+ */
+package org.sleuthkit.datamodel;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Properties;
+import org.sleuthkit.datamodel.SQLHelper.PostgreSQLHelper;
+import org.sleuthkit.datamodel.SQLHelper.SQLiteHelper;
+
+/**
+ * Creates a SQLite or PostgreSQL case database.
+ */
+class CaseDatabaseFactory {
+	
+	private final SQLHelper dbQueryHelper;
+	private final DbCreationHelper dbCreationHelper;
+		
+	/**
+	 * Create a new SQLite case
+	 * 
+	 * @param dbPath Full path to the database
+	 */
+	CaseDatabaseFactory(String dbPath) {		
+		this.dbQueryHelper = new SQLiteHelper();
+		this.dbCreationHelper = new SQLiteDbCreationHelper(dbPath);
+	}
+	
+	/**
+	 * Create a new PostgreSQL case
+	 * 
+	 * @param caseName    The name of the case. It will be used to create a case
+	 *                    database name that can be safely used in SQL commands
+	 *                    and will not be subject to name collisions on the case
+	 *                    database server. Use getDatabaseName to get the
+	 *                    created name.
+	 * @param info        The information to connect to the database.
+	 */
+	CaseDatabaseFactory(String caseName, CaseDbConnectionInfo info) {
+		this.dbQueryHelper = new PostgreSQLHelper();
+		this.dbCreationHelper = new PostgreSQLDbCreationHelper(caseName, info);
+	}
+	
+	/**
+	 * Creates and initializes the case database.
+	 * Currently the case must be reopened after creation.
+	 * 
+	 * @throws TskCoreException 
+	 */
+	void createCaseDatabase() throws TskCoreException {
+		createDatabase();
+		initializeSchema();
+	}
+	
+	/**
+	 * Create the database itself (if necessary)
+	 * 
+	 * @throws TskCoreException 
+	 */
+	private void createDatabase() throws TskCoreException {
+		dbCreationHelper.createDatabase();
+	}
+	
+	/**
+	 * Initialize the database schema
+	 * 
+	 * @throws TskCoreException 
+	 */
+	private void initializeSchema() throws TskCoreException {
+		try (Connection conn = dbCreationHelper.getConnection()) {
+			// Perform any needed steps before creating the tables
+			dbCreationHelper.performPreInitialization(conn);
+
+			// Add schema version
+			addDbInfo(conn);
+
+			// Add tables
+			addTables(conn);
+			dbCreationHelper.performPostTableInitialization(conn);
+		
+			// Add indexes
+			addIndexes(conn);
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error initializing case database", ex);
+		}
+	}
+	
+	/**
+	 * Create and populate the db_info tables
+	 * 
+	 * @param conn the database connection
+	 * 
+	 * @throws TskCoreException 
+	 */
+	private void addDbInfo(Connection conn) throws TskCoreException {
+		CaseDbSchemaVersionNumber version = SleuthkitCase.CURRENT_DB_SCHEMA_VERSION;
+		long tskVersionNum = SleuthkitJNI.getSleuthkitVersion(); // This is the current version of TSK
+		
+		try (Statement stmt = conn.createStatement()) {
+			stmt.execute("CREATE TABLE tsk_db_info (schema_ver INTEGER, tsk_ver INTEGER, schema_minor_ver INTEGER)");
+			stmt.execute("INSERT INTO tsk_db_info (schema_ver, tsk_ver, schema_minor_ver) VALUES (" + 
+					version.getMajor() + ", " + tskVersionNum + ", " + version.getMinor() + ");");
+
+			stmt.execute("CREATE TABLE tsk_db_info_extended (name TEXT PRIMARY KEY, value TEXT NOT NULL);");
+			stmt.execute("INSERT INTO tsk_db_info_extended (name, value) VALUES ('TSK_VERSION', '" + tskVersionNum + "');");
+			stmt.execute("INSERT INTO tsk_db_info_extended (name, value) VALUES ('SCHEMA_MAJOR_VERSION', '" + version.getMajor() + "');");
+			stmt.execute("INSERT INTO tsk_db_info_extended (name, value) VALUES ('SCHEMA_MINOR_VERSION', '" + version.getMinor() + "');");
+			stmt.execute("INSERT INTO tsk_db_info_extended (name, value) VALUES ('CREATION_SCHEMA_MAJOR_VERSION', '" + version.getMajor() + "');");
+			stmt.execute("INSERT INTO tsk_db_info_extended (name, value) VALUES ('CREATION_SCHEMA_MINOR_VERSION', '" + version.getMinor() + "');");
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error initializing db_info tables", ex);
+		}
+	}
+	
+	/**
+	 * Add and initialize the database tables 
+	 * 
+	 * @param conn the database connection
+	 * 
+	 * @throws TskCoreException 
+	 */
+	private void addTables(Connection conn) throws TskCoreException {
+		try (Statement stmt = conn.createStatement()) {
+			createFileTables(stmt);
+			createArtifactTables(stmt);
+			createTagTables(stmt);
+			createIngestTables(stmt);
+			createAccountTables(stmt);
+			createEventTables(stmt);
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error initializing tables", ex);
+		}
+	}
+	
+	private void createFileTables(Statement stmt) throws SQLException {
+		// The UNIQUE here on the object ID is to create an index
+		stmt.execute("CREATE TABLE tsk_objects (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, par_obj_id " + dbQueryHelper.getBigIntType() 
+				+ ", type INTEGER NOT NULL, UNIQUE (obj_id), FOREIGN KEY (par_obj_id) REFERENCES tsk_objects (obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_image_info (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, type INTEGER, ssize INTEGER, " 
+				+ "tzone TEXT, size " + dbQueryHelper.getBigIntType() + ", md5 TEXT, sha1 TEXT, sha256 TEXT, display_name TEXT, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_image_names (obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, name TEXT NOT NULL, "
+				+ "sequence INTEGER NOT NULL, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_vs_info (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, vs_type INTEGER NOT NULL, "
+				+ "img_offset " + dbQueryHelper.getBigIntType() + " NOT NULL, block_size " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_vs_parts (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "addr " + dbQueryHelper.getBigIntType() + " NOT NULL, start " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "length " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ dbQueryHelper.getVSDescColName() + " TEXT, "
+				+ "flags INTEGER NOT NULL, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE);");		
+		
+		stmt.execute("CREATE TABLE tsk_pool_info (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "pool_type INTEGER NOT NULL, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE);");
+
+		stmt.execute("CREATE TABLE data_source_info (obj_id " + dbQueryHelper.getBigIntType() + " PRIMARY KEY, device_id TEXT NOT NULL, "
+				+ "time_zone TEXT NOT NULL, acquisition_details TEXT, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_fs_info (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "img_offset " + dbQueryHelper.getBigIntType() + " NOT NULL, fs_type INTEGER NOT NULL, "
+				+ "block_size " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "block_count " + dbQueryHelper.getBigIntType() + " NOT NULL, root_inum " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "first_inum " + dbQueryHelper.getBigIntType() + " NOT NULL, last_inum " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "display_name TEXT, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_files (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "fs_obj_id " + dbQueryHelper.getBigIntType() + ", data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "attr_type INTEGER, attr_id INTEGER, " 
+				+ "name TEXT NOT NULL, meta_addr " + dbQueryHelper.getBigIntType() + ", meta_seq " + dbQueryHelper.getBigIntType() + ", "
+				+ "type INTEGER, has_layout INTEGER, has_path INTEGER, "
+				+ "dir_type INTEGER, meta_type INTEGER, dir_flags INTEGER, meta_flags INTEGER, size " + dbQueryHelper.getBigIntType() + ", "
+				+ "ctime " + dbQueryHelper.getBigIntType() + ", "
+				+ "crtime " + dbQueryHelper.getBigIntType() + ", atime " + dbQueryHelper.getBigIntType() + ", "
+				+ "mtime " + dbQueryHelper.getBigIntType() + ", mode INTEGER, uid INTEGER, gid INTEGER, md5 TEXT, known INTEGER, "
+				+ "parent_path TEXT, mime_type TEXT, extension TEXT, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(fs_obj_id) REFERENCES tsk_fs_info(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(data_source_obj_id) REFERENCES data_source_info(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE file_encoding_types (encoding_type INTEGER PRIMARY KEY, name TEXT NOT NULL)");
+
+		stmt.execute("CREATE TABLE tsk_files_path (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, path TEXT NOT NULL, "
+				+ "encoding_type INTEGER NOT NULL, FOREIGN KEY(encoding_type) references file_encoding_types(encoding_type), "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_files_derived (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "derived_id " + dbQueryHelper.getBigIntType() + " NOT NULL, rederive TEXT, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE tsk_files_derived_method (derived_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "tool_name TEXT NOT NULL, tool_version TEXT NOT NULL, other TEXT)");		
+		
+		stmt.execute("CREATE TABLE tsk_file_layout (obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "byte_start " + dbQueryHelper.getBigIntType() + " NOT NULL, byte_len " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "sequence INTEGER NOT NULL, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE);");
+		
+		stmt.execute("CREATE TABLE reports (obj_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, path TEXT NOT NULL, "
+				+ "crtime INTEGER NOT NULL, src_module_name TEXT NOT NULL, report_name TEXT NOT NULL, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE);");		
+	}
+	
+	private void createArtifactTables(Statement stmt) throws SQLException {
+		stmt.execute("CREATE TABLE blackboard_artifact_types (artifact_type_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "type_name TEXT NOT NULL, display_name TEXT)");
+
+		stmt.execute("CREATE TABLE blackboard_attribute_types (attribute_type_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "type_name TEXT NOT NULL, display_name TEXT, value_type INTEGER NOT NULL)");
+
+		stmt.execute("CREATE TABLE review_statuses (review_status_id INTEGER PRIMARY KEY, "
+				+ "review_status_name TEXT NOT NULL, "
+				+ "display_name TEXT NOT NULL)");
+
+		stmt.execute("CREATE TABLE blackboard_artifacts (artifact_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "artifact_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "artifact_type_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "review_status_id INTEGER NOT NULL, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(artifact_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(data_source_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(artifact_type_id) REFERENCES blackboard_artifact_types(artifact_type_id), "
+				+ "FOREIGN KEY(review_status_id) REFERENCES review_statuses(review_status_id))");
+
+		/* Binary representation of BYTEA is a bunch of bytes, which could
+		* include embedded nulls so we have to pay attention to field length.
+		* http://www.postgresql.org/docs/9.4/static/libpq-example.html
+		*/
+		stmt.execute("CREATE TABLE blackboard_attributes (artifact_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "artifact_type_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "source TEXT, context TEXT, attribute_type_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "value_type INTEGER NOT NULL, value_byte " + dbQueryHelper.getBlobType() + ", "
+				+ "value_text TEXT, value_int32 INTEGER, value_int64 " + dbQueryHelper.getBigIntType() + ", value_double NUMERIC(20, 10), "
+				+ "FOREIGN KEY(artifact_id) REFERENCES blackboard_artifacts(artifact_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(artifact_type_id) REFERENCES blackboard_artifact_types(artifact_type_id), "
+				+ "FOREIGN KEY(attribute_type_id) REFERENCES blackboard_attribute_types(attribute_type_id))");		
+	}
+	
+	private void createTagTables(Statement stmt) throws SQLException {
+		stmt.execute("CREATE TABLE tag_names (tag_name_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, display_name TEXT UNIQUE, "
+				+ "description TEXT NOT NULL, color TEXT NOT NULL, knownStatus INTEGER NOT NULL)");
+		
+		stmt.execute("CREATE TABLE tsk_examiners (examiner_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "login_name TEXT NOT NULL, display_name TEXT, UNIQUE(login_name))");
+
+		stmt.execute("CREATE TABLE content_tags (tag_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, tag_name_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "comment TEXT NOT NULL, begin_byte_offset " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "end_byte_offset " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "examiner_id " + dbQueryHelper.getBigIntType() + ", "
+				+ "FOREIGN KEY(examiner_id) REFERENCES tsk_examiners(examiner_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(tag_name_id) REFERENCES tag_names(tag_name_id) ON DELETE CASCADE)");
+
+		stmt.execute("CREATE TABLE blackboard_artifact_tags (tag_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "artifact_id " + dbQueryHelper.getBigIntType() + " NOT NULL, tag_name_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "comment TEXT NOT NULL,  examiner_id " + dbQueryHelper.getBigIntType() + ", "
+				+ "FOREIGN KEY(examiner_id) REFERENCES tsk_examiners(examiner_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(artifact_id) REFERENCES blackboard_artifacts(artifact_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(tag_name_id) REFERENCES tag_names(tag_name_id) ON DELETE CASCADE)");
+	}
+	
+	/**
+	 * Add indexes
+	 * 
+	 * @param conn the database connection
+	 * @throws TskCoreException 
+	 */
+	private void addIndexes(Connection conn) throws TskCoreException {
+		try (Statement stmt = conn.createStatement()) {
+			// tsk_objects index
+			stmt.execute("CREATE INDEX parObjId ON tsk_objects(par_obj_id)");
+			
+			// file layout index
+			stmt.execute("CREATE INDEX layout_objID ON tsk_file_layout(obj_id)");
+			
+			// blackboard indexes
+			stmt.execute("CREATE INDEX artifact_objID ON blackboard_artifacts(obj_id)");
+			stmt.execute("CREATE INDEX artifact_artifact_objID ON blackboard_artifacts(artifact_obj_id)");
+			stmt.execute("CREATE INDEX artifact_typeID ON blackboard_artifacts(artifact_type_id)");
+			stmt.execute("CREATE INDEX attrsArtifactID ON blackboard_attributes(artifact_id)");
+			
+			//file type indexes
+			stmt.execute("CREATE INDEX mime_type ON tsk_files(dir_type,mime_type,type)");
+			stmt.execute("CREATE INDEX file_extension ON tsk_files(extension)");
+			
+			// account indexes
+			stmt.execute("CREATE INDEX relationships_account1 ON account_relationships(account1_id)");
+			stmt.execute("CREATE INDEX relationships_account2 ON account_relationships(account2_id)");
+			stmt.execute("CREATE INDEX relationships_relationship_source_obj_id ON account_relationships(relationship_source_obj_id)");
+			stmt.execute("CREATE INDEX relationships_date_time ON account_relationships(date_time)");
+			stmt.execute("CREATE INDEX relationships_relationship_type ON account_relationships(relationship_type)");
+			stmt.execute("CREATE INDEX relationships_data_source_obj_id ON account_relationships(data_source_obj_id)");
+			
+			//tsk_events indices
+			stmt.execute("CREATE INDEX events_data_source_obj_id ON tsk_event_descriptions(data_source_obj_id)");
+			stmt.execute("CREATE INDEX events_content_obj_id ON tsk_event_descriptions(content_obj_id)");
+			stmt.execute("CREATE INDEX events_artifact_id ON tsk_event_descriptions(artifact_id)");
+			stmt.execute("CREATE INDEX events_sub_type_time ON tsk_events(event_type_id,  time)");
+			stmt.execute("CREATE INDEX events_time ON tsk_events(time)");
+		} catch (SQLException ex) {
+			throw new TskCoreException("Error initializing db_info tables", ex);
+		}
+	}
+	
+	private void createIngestTables(Statement stmt) throws SQLException {
+		stmt.execute("CREATE TABLE ingest_module_types (type_id INTEGER PRIMARY KEY, type_name TEXT NOT NULL)");
+            
+		stmt.execute("CREATE TABLE ingest_job_status_types (type_id INTEGER PRIMARY KEY, type_name TEXT NOT NULL)");
+
+		stmt.execute("CREATE TABLE ingest_modules (ingest_module_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "display_name TEXT NOT NULL, unique_name TEXT UNIQUE NOT NULL, type_id INTEGER NOT NULL, "
+				+ "version TEXT NOT NULL, FOREIGN KEY(type_id) REFERENCES ingest_module_types(type_id) ON DELETE CASCADE);");
+
+		stmt.execute("CREATE TABLE ingest_jobs (ingest_job_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, host_name TEXT NOT NULL, "
+				+ "start_date_time " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "end_date_time " + dbQueryHelper.getBigIntType() + " NOT NULL, status_id INTEGER NOT NULL, "
+				+ "settings_dir TEXT, FOREIGN KEY(obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(status_id) REFERENCES ingest_job_status_types(type_id) ON DELETE CASCADE);");
+
+		stmt.execute("CREATE TABLE ingest_job_modules (ingest_job_id INTEGER, ingest_module_id INTEGER, "
+				+ "pipeline_position INTEGER, PRIMARY KEY(ingest_job_id, ingest_module_id), "
+				+ "FOREIGN KEY(ingest_job_id) REFERENCES ingest_jobs(ingest_job_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(ingest_module_id) REFERENCES ingest_modules(ingest_module_id) ON DELETE CASCADE);");
+	}
+	
+	private void createAccountTables(Statement stmt) throws SQLException {
+		stmt.execute("CREATE TABLE account_types (account_type_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "type_name TEXT UNIQUE NOT NULL, display_name TEXT NOT NULL)");
+
+		stmt.execute("CREATE TABLE accounts (account_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "account_type_id INTEGER NOT NULL, account_unique_identifier TEXT NOT NULL, "
+				+ "UNIQUE(account_type_id, account_unique_identifier), "
+				+ "FOREIGN KEY(account_type_id) REFERENCES account_types(account_type_id))");
+
+		stmt.execute("CREATE TABLE account_relationships (relationship_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+				+ "account1_id INTEGER NOT NULL, account2_id INTEGER NOT NULL, "
+				+ "relationship_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "date_time " + dbQueryHelper.getBigIntType() + ", relationship_type INTEGER NOT NULL, "
+				+ "data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+				+ "UNIQUE(account1_id, account2_id, relationship_source_obj_id), "
+				+ "FOREIGN KEY(account1_id) REFERENCES accounts(account_id), "
+				+ "FOREIGN KEY(account2_id) REFERENCES accounts(account_id), "
+				+ "FOREIGN KEY(relationship_source_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+				+ "FOREIGN KEY(data_source_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE)");
+	}
+	
+	private void createEventTables(Statement stmt) throws SQLException {
+		stmt.execute("CREATE TABLE tsk_event_types ("
+				+ " event_type_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY,"
+				+ " display_name TEXT UNIQUE NOT NULL , "
+				+ " super_type_id INTEGER REFERENCES tsk_event_types(event_type_id) )");
+
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(0, 'Event Types', null)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(1, 'File System', 0)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(2, 'Web Activity', 0)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(3, 'Misc Types', 0)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(4, 'Modified', 1)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(5, 'Accessed', 1)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(6, 'Created', 1)");
+		stmt.execute("INSERT INTO tsk_event_types(event_type_id, display_name, super_type_id) VALUES(7, 'Changed', 1)");
+		/*
+		* Regarding the timeline event tables schema, note that several columns
+		* in the tsk_event_descriptions table seem, at first glance, to be
+		* attributes of events rather than their descriptions and would appear
+		* to belong in tsk_events table instead. The rationale for putting the
+		* data source object ID, content object ID, artifact ID and the flags
+		* indicating whether or not the event source has a hash set hit or is
+		* tagged were motivated by the fact that these attributes are identical
+		* for each event in a set of file system file MAC time events. The
+		* decision was made to avoid duplication and save space by placing this
+		* data in the tsk_event-descriptions table.
+		*/			
+		stmt.execute(
+			"CREATE TABLE tsk_event_descriptions ( "
+			+ " event_description_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+			+ " full_description TEXT NOT NULL, "
+			+ " med_description TEXT, "
+			+ " short_description TEXT,"
+			+ " data_source_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+			+ " content_obj_id " + dbQueryHelper.getBigIntType() + " NOT NULL, "
+			+ " artifact_id " + dbQueryHelper.getBigIntType() + ", "
+			+ " hash_hit INTEGER NOT NULL, " //boolean 
+			+ " tagged INTEGER NOT NULL, " //boolean 
+			+ " FOREIGN KEY(data_source_obj_id) REFERENCES data_source_info(obj_id) ON DELETE CASCADE, "
+			+ " FOREIGN KEY(content_obj_id) REFERENCES tsk_objects(obj_id) ON DELETE CASCADE, "
+			+ " FOREIGN KEY(artifact_id) REFERENCES blackboard_artifacts(artifact_id) ON DELETE CASCADE,"
+			+ " UNIQUE (full_description, content_obj_id, artifact_id))");
+
+		stmt.execute(
+			"CREATE TABLE tsk_events ("
+			+ " event_id " + dbQueryHelper.getPrimaryKey() + " PRIMARY KEY, "
+			+ " event_type_id " + dbQueryHelper.getBigIntType() + " NOT NULL REFERENCES tsk_event_types(event_type_id) ,"
+			+ " event_description_id " + dbQueryHelper.getBigIntType() + " NOT NULL REFERENCES tsk_event_descriptions(event_description_id) ON DELETE CASCADE ,"
+			+ " time " + dbQueryHelper.getBigIntType() + " NOT NULL , "
+			+ " UNIQUE (event_type_id, event_description_id, time))");			
+	}
+	
+	/**
+	 * Helper class for holding code unique to each database type.
+	 */
+	private abstract class DbCreationHelper {
+		
+		/**
+		 * Create the database itself (if necessary)
+		 * 
+		 * @throws TskCoreException 
+		 */
+		abstract void createDatabase() throws TskCoreException;
+		
+		/**
+		 * Get an connection to the case database
+		 * 
+		 * @return the connection
+		 */
+		abstract Connection getConnection() throws TskCoreException;
+		
+		/**
+		 * Do any needed initialization before creating the tables.
+		 * This is where SQLite pragmas are set up.
+		 * 
+		 * @param conn The database connection
+		 * 
+		 * @throws TskCoreException 
+		 */
+		abstract void performPreInitialization(Connection conn) throws TskCoreException;
+		
+		/**
+		 * Do any additional steps after the tables are created.
+		 * 
+		 * @param conn The database connection
+		 * @throws TskCoreException 
+		 */
+		abstract void performPostTableInitialization(Connection conn) throws TskCoreException;
+	}
+	
+	/**
+	 * Implements the PostgreSQL-specific methods for creating the case
+	 */
+	private class PostgreSQLDbCreationHelper extends DbCreationHelper {
+		
+		private final static String JDBC_BASE_URI = "jdbc:postgresql://"; // NON-NLS
+		private final static String JDBC_DRIVER = "org.postgresql.Driver"; // NON-NLS
+		
+		final private String caseName;
+		final private CaseDbConnectionInfo info;
+		
+		PostgreSQLDbCreationHelper(String caseName, CaseDbConnectionInfo info) {
+			this.caseName = caseName;
+			this.info = info;
+		}
+		
+		@Override
+		void createDatabase() throws TskCoreException{
+			try(Connection conn = getPostgresConnection();
+					Statement stmt = conn.createStatement()) {
+				stmt.execute("CREATE DATABASE \"" + caseName + "\" WITH ENCODING='UTF8'");		
+			} catch (SQLException ex) {
+				throw new TskCoreException("Error creating PostgreSQL case " + caseName, ex);
+			}
+		}
+		
+		@Override
+		Connection getConnection() throws TskCoreException {
+			return getConnection(caseName);
+		}		
+		
+		/**
+		 * Connects to the "postgres" database for creating new databases.
+		 * 
+		 * @return the connection to the "postgres" database
+		 */
+		Connection getPostgresConnection() throws TskCoreException {
+			return getConnection("postgres");
+		}
+		
+		/**
+		 * Connects to an existing database with the given name.
+		 * 
+		 * @param databaseName the name of the database
+		 * 
+		 * @return the connection to the database
+		 */
+		Connection getConnection(String databaseName) throws TskCoreException {
+			
+			StringBuilder url = new StringBuilder();
+			url.append(JDBC_BASE_URI)
+				.append(info.getHost())
+				.append('/') // NON-NLS
+				.append(databaseName);
+			
+			Connection conn;
+			try {
+				Properties props = new Properties();
+				props.setProperty("user", info.getUserName());     // NON-NLS
+				props.setProperty("password", info.getPassword()); // NON-NLS
+
+				Class.forName(JDBC_DRIVER);
+				conn = DriverManager.getConnection(url.toString(), props);
+			} catch (ClassNotFoundException | SQLException ex) {
+				throw new TskCoreException("Failed to acquire ephemeral connection to PostgreSQL database " + databaseName, ex); // NON-NLS
+			}
+			return conn;
+		}	
+		
+		@Override
+		void performPreInitialization(Connection conn) throws TskCoreException {
+			// Nothing to do here for PostgreSQL
+		}
+		
+		@Override
+		void performPostTableInitialization(Connection conn) throws TskCoreException {
+			try (Statement stmt = conn.createStatement()) {
+				stmt.execute("ALTER SEQUENCE blackboard_artifacts_artifact_id_seq minvalue -9223372036854775808 restart with -9223372036854775808");
+			} catch (SQLException ex) {
+				throw new TskCoreException("Error altering artifact ID sequence", ex);
+			}
+		}
+	}
+	
+	/**
+	 * Implements the SQLite-specific methods for creating the case
+	 */
+	private class SQLiteDbCreationHelper extends DbCreationHelper {
+		
+		private final static String PRAGMA_SYNC_OFF = "PRAGMA synchronous = OFF"; // NON-NLS
+		private final static String PRAGMA_READ_UNCOMMITTED_TRUE = "PRAGMA read_uncommitted = True"; // NON-NLS
+		private final static String PRAGMA_ENCODING_UTF8 = "PRAGMA encoding = 'UTF-8'"; // NON-NLS
+		private final static String PRAGMA_PAGE_SIZE_4096 = "PRAGMA page_size = 4096"; // NON-NLS
+		private final static String PRAGMA_FOREIGN_KEYS_ON = "PRAGMA foreign_keys = ON"; // NON-NLS
+		
+		private final static String JDBC_DRIVER = "org.sqlite.JDBC"; // NON-NLS
+        private final static String JDBC_BASE_URI = "jdbc:sqlite:"; // NON-NLS
+		
+		String dbPath;
+		
+		SQLiteDbCreationHelper(String dbPath) {
+			this.dbPath = dbPath;
+		}
+		
+		@Override
+		void createDatabase() throws TskCoreException {
+			// SQLite doesn't need to explicitly create the case database but we will
+			// check that the folder exists and the database does not
+			File dbFile = new File(dbPath);
+			if (dbFile.exists()) {
+				throw new TskCoreException("Case database already exists : " + dbPath);
+			}
+
+			if (dbFile.getParentFile() != null && !dbFile.getParentFile().exists()) {
+				throw new TskCoreException("Case database folder does not exist : " + dbFile.getParent());
+			}
+		}
+		
+		@Override
+		Connection getConnection() throws TskCoreException {
+			
+			StringBuilder url = new StringBuilder();
+			url.append(JDBC_BASE_URI)
+				.append(dbPath);
+			
+			Connection conn;
+			try {
+				Class.forName(JDBC_DRIVER);
+				conn = DriverManager.getConnection(url.toString());
+			} catch (ClassNotFoundException | SQLException ex) {
+				throw new TskCoreException("Failed to acquire ephemeral connection SQLite database " + dbPath, ex); // NON-NLS
+			}
+			return conn;
+		}
+		
+		@Override
+		void performPreInitialization(Connection conn) throws TskCoreException {
+			try (Statement stmt = conn.createStatement()) {
+				stmt.execute(PRAGMA_SYNC_OFF);
+				stmt.execute(PRAGMA_READ_UNCOMMITTED_TRUE);
+				stmt.execute(PRAGMA_ENCODING_UTF8);
+				stmt.execute(PRAGMA_PAGE_SIZE_4096);
+				stmt.execute(PRAGMA_FOREIGN_KEYS_ON);
+			} catch (SQLException ex) {
+				throw new TskCoreException("Error setting pragmas", ex);
+			}
+		}	
+
+		@Override
+		void performPostTableInitialization(Connection conn) throws TskCoreException {
+			// Nothing to do here for SQLite
+		}
+	}
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SQLHelper.java b/bindings/java/src/org/sleuthkit/datamodel/SQLHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ae0f8f4c10e20974f10bec671ad849941b4e2a3
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/SQLHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Sleuth Kit Data Model
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.
+ */
+package org.sleuthkit.datamodel;
+
+/**
+ * Interface for classes to help create queries for SQLite or PostgreSQL
+ */
+interface SQLHelper {
+	
+	// Get the type for the primary key
+	String getPrimaryKey();
+	
+	// Get the type for big int-type data
+	String getBigIntType();
+	
+	// Get the type for blob-type data
+	String getBlobType();
+	
+	// Get the description column name for the tsk_vs_parts table.
+	// This varies between SQLite and PostgreSQL.
+	String getVSDescColName();
+
+
+	/**
+	 * PostgreSQL-specific implementation
+	 */
+	class PostgreSQLHelper implements SQLHelper {
+
+		@Override
+		public String getPrimaryKey() {
+			return "BIGSERIAL";
+		}
+
+		@Override
+		public String getBigIntType() {
+			return "BIGINT";
+		}
+
+		@Override
+		public String getBlobType() {
+			return "BYTEA";
+		}
+
+		@Override
+		public String getVSDescColName() {
+			return "descr";
+		}
+	}
+	
+	/**
+	 * SQLite-specific implementation
+	 */
+	class SQLiteHelper implements SQLHelper {
+
+		@Override
+		public String getPrimaryKey() {
+			return "INTEGER";
+		}
+
+		@Override
+		public String getBigIntType() {
+			return "INTEGER";
+		}
+
+		@Override
+		public String getBlobType() {
+			return "BLOB";
+		}
+
+		@Override
+		public String getVSDescColName() {
+			return "desc";
+		}
+	}
+}
\ No newline at end of file
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
index 6be1b4e196123753a5df80f2229ae7b9d1187775..40bfa848eb68c3ea8beb03a027470a640d16cae3 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java
@@ -96,7 +96,7 @@ public class SleuthkitCase {
 	 * This must be the same as TSK_SCHEMA_VER and TSK_SCHEMA_MINOR_VER in
 	 * tsk/auto/tsk_db.h.
 	 */
-	private static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
+	static final CaseDbSchemaVersionNumber CURRENT_DB_SCHEMA_VERSION
 			= new CaseDbSchemaVersionNumber(8, 4);
 
 	private static final long BASE_ARTIFACT_ID = Long.MIN_VALUE; // Artifact ids will start at the lowest negative value
@@ -2338,7 +2338,10 @@ public static SleuthkitCase openCase(String databaseName, CaseDbConnectionInfo i
 	 */
 	public static SleuthkitCase newCase(String dbPath) throws TskCoreException {
 		try {
-			SleuthkitJNI.CaseDbHandle caseHandle = SleuthkitJNI.newCaseDb(dbPath);
+			CaseDatabaseFactory factory = new CaseDatabaseFactory(dbPath);
+			factory.createCaseDatabase();
+
+			SleuthkitJNI.CaseDbHandle caseHandle = SleuthkitJNI.openCaseDb(dbPath);
 			return new SleuthkitCase(dbPath, caseHandle, DbType.SQLITE);
 		} catch (Exception ex) {
 			throw new TskCoreException("Failed to create case database at " + dbPath, ex);
@@ -2375,7 +2378,10 @@ public static SleuthkitCase newCase(String caseName, CaseDbConnectionInfo info,
 			 * the case. In this way, we obtain more detailed information if we
 			 * are able, but do not lose any information if unable.
 			 */
-			SleuthkitJNI.CaseDbHandle caseHandle = SleuthkitJNI.newCaseDb(databaseName, info);
+			CaseDatabaseFactory factory = new CaseDatabaseFactory(databaseName, info);
+			factory.createCaseDatabase();
+
+			final SleuthkitJNI.CaseDbHandle caseHandle = SleuthkitJNI.openCaseDb(databaseName, info);
 			return new SleuthkitCase(info.getHost(), Integer.parseInt(info.getPort()),
 					databaseName, info.getUserName(), info.getPassword(), caseHandle, caseDirPath, info.getDbType());
 		} catch (PropertyVetoException exp) {
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
index 5ab34ecfdce8b26555f460c990ac624e35b5b5b0..c1d398602ede2354f42559fb0db1feb5225ec661 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
@@ -1751,6 +1751,23 @@ public static long findDeviceSize(String devPath) throws TskCoreException {
 	public static boolean isImageSupported(String imagePath) {
 		return isImageSupportedNat(imagePath);
 	}
+	
+	/** Get the version of the Sleuthkit code in number form.
+	 * Upper byte is A, next is B, and next byte is C in version A.B.C.
+	 * Lowest byte is 0xff, except in beta releases, in which case it
+	 * increments from 1.  Nightly snapshots will have upper byte as
+	 * 0xff and next bytes with year, month, and date, respectively.
+	 * Note that you will not be able to differentiate between snapshots
+	 * from the trunk or branches with this method...
+	 * For example, 3.1.2 would be stored as 0x030102FF.
+	 * 3.1.2b1 would be 0x03010201.  Snapshot from Jan 2, 2003 would be
+	 * 0xFF030102.
+	 * 
+	 * @return the current Sleuthkit version
+     */
+	static long getSleuthkitVersion() {
+		return getSleuthkitVersionNat();
+	}
 
 	/**
 	 * Get a read lock for the C++ layer. Do not get this lock after obtaining
@@ -1997,6 +2014,8 @@ public static long openFile(long fsHandle, long fileId, TSK_FS_ATTR_TYPE_ENUM at
 	private static native String getCurDirNat(long process);
 
 	private static native boolean isImageSupportedNat(String imagePath);
+	
+	private static native long getSleuthkitVersionNat();
 
 	private static native int finishImageWriterNat(long a_img_info);