// // Copyright 2014-2015 Amazon.com, // Inc. or its affiliates. All Rights Reserved. // // SPDX-License-Identifier: Apache-2.0 // using Mono.Data.Sqlite; using Amazon.Util.Internal; using System.IO; using System.Text.RegularExpressions; using Logger = Amazon.Runtime.Internal.Util.Logger; using System; using System.Globalization; using System.Collections.Generic; namespace Amazon.CognitoSync.SyncManager.Internal { public partial class SQLiteLocalStorage : ILocalStorage { internal readonly string directoryPath; internal readonly string filePath; /// /// An implementation for /// using SQLite /// public SQLiteLocalStorage() { _logger = Logger.GetLogger(this.GetType()); directoryPath = AmazonHookedPlatformInfo.Instance.PersistentDataPath; filePath = System.IO.Path.Combine(directoryPath, DB_FILE_NAME); this.SetupDatabase(); } /// /// Implements the Dispose pattern /// /// Whether this object is being disposed via a call to Dispose /// or garbage collected. protected virtual void Dispose(bool disposing) { if (disposing) { if (connection != null) { connection.Close(); connection.Dispose(); } } } private SqliteConnection connection; private void SetupDatabase() { //check if database already exists if (!Directory.Exists(directoryPath)) { DirectoryInfo di = Directory.CreateDirectory(directoryPath); } if (!File.Exists(filePath)) { SqliteConnection.CreateFile(filePath); } try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); string createDatasetTable = "CREATE TABLE IF NOT EXISTS " + TABLE_DATASETS + "(" + DatasetColumns.IDENTITY_ID + " TEXT NOT NULL," + DatasetColumns.DATASET_NAME + " TEXT NOT NULL," + DatasetColumns.CREATION_TIMESTAMP + " TEXT DEFAULT '0'," + DatasetColumns.LAST_MODIFIED_TIMESTAMP + " TEXT DEFAULT '0'," + DatasetColumns.LAST_MODIFIED_BY + " TEXT," + DatasetColumns.STORAGE_SIZE_BYTES + " INTEGER DEFAULT 0," + DatasetColumns.RECORD_COUNT + " INTEGER DEFAULT 0," + DatasetColumns.LAST_SYNC_COUNT + " INTEGER NOT NULL DEFAULT 0," + DatasetColumns.LAST_SYNC_TIMESTAMP + " TEXT DEFAULT '0'," + DatasetColumns.LAST_SYNC_RESULT + " TEXT," + "UNIQUE (" + DatasetColumns.IDENTITY_ID + ", " + DatasetColumns.DATASET_NAME + ")" + ")"; using (var command = new SqliteCommand(createDatasetTable, connection)) { command.ExecuteNonQuery(); } string createRecordsTable = "CREATE TABLE IF NOT EXISTS " + TABLE_RECORDS + "(" + RecordColumns.IDENTITY_ID + " TEXT NOT NULL," + RecordColumns.DATASET_NAME + " TEXT NOT NULL," + RecordColumns.KEY + " TEXT NOT NULL," + RecordColumns.VALUE + " TEXT," + RecordColumns.SYNC_COUNT + " INTEGER NOT NULL DEFAULT 0," + RecordColumns.LAST_MODIFIED_TIMESTAMP + " TEXT DEFAULT '0'," + RecordColumns.LAST_MODIFIED_BY + " TEXT," + RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP + " TEXT DEFAULT '0'," + RecordColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 1," + "UNIQUE (" + RecordColumns.IDENTITY_ID + ", " + RecordColumns.DATASET_NAME + ", " + RecordColumns.KEY + ")" + ")"; using (var command = new SqliteCommand(createRecordsTable, connection)) { command.ExecuteNonQuery(); } string createKvStore = "CREATE TABLE IF NOT EXISTS kvstore (key TEXT NOT NULL, value TEXT NOT NULL, UNIQUE (KEY))"; using (var command = new SqliteCommand(createKvStore, connection)) { command.ExecuteNonQuery(); } _logger.InfoFormat("{0}", @"Cognito Sync - SQLiteStorage - completed setupdatabase"); } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } internal void CreateDatasetHelper(string query, params object[] parameters) { try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, parameters); command.ExecuteNonQuery(); } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } internal DatasetMetadata GetMetadataHelper(string identityId, string datasetName) { string query = DatasetColumns.BuildQuery( DatasetColumns.IDENTITY_ID + " = @identityId AND " + DatasetColumns.DATASET_NAME + " = @datasetName " ); DatasetMetadata metadata = null; try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, identityId, datasetName); using (var reader = command.ExecuteReader()) { if (reader.HasRows && reader.Read()) { metadata = DatasetMetadataFromReader(reader); } } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } return metadata; } private static DatasetMetadata DatasetMetadataFromReader(SqliteDataReader reader) { return new DatasetMetadata( reader.GetString(DatasetColumns.DATASET_NAME_IDX) ?? string.Empty, new DateTime(long.Parse(reader.GetString(DatasetColumns.CREATION_TIMESTAMP_IDX), CultureInfo.InvariantCulture.NumberFormat), DateTimeKind.Utc), new DateTime(long.Parse(reader.GetString(DatasetColumns.LAST_MODIFIED_TIMESTAMP_IDX), CultureInfo.InvariantCulture.NumberFormat), DateTimeKind.Utc), reader.IsDBNull(DatasetColumns.LAST_MODIFIED_BY_IDX) ? string.Empty : reader.GetString(DatasetColumns.LAST_MODIFIED_BY_IDX), reader.GetInt32(DatasetColumns.STORAGE_SIZE_BYTES_IDX), reader.GetInt32(DatasetColumns.RECORD_COUNT_IDX)); } private static void BindData(SqliteCommand command, params object[] parameters) { string query = command.CommandText; int count = 0; foreach (Match match in Regex.Matches(query, "(\\@\\w+) ")) { var date = parameters[count] as DateTime?; if (date.HasValue) { command.Parameters.Add(new SqliteParameter(match.Groups[1].Value, date.Value.Ticks.ToString(CultureInfo.InvariantCulture.NumberFormat))); } else { command.Parameters.Add(new SqliteParameter(match.Groups[1].Value, parameters[count])); } count++; } } internal List GetDatasetMetadataHelper(string query, params string[] parameters) { List datasetMetadataList = new List(); try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, parameters); using (var reader = command.ExecuteReader()) { while (reader.HasRows && reader.Read()) { datasetMetadataList.Add(DatasetMetadataFromReader(reader)); } } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } return datasetMetadataList; } internal Record GetRecordHelper(string query, params string[] parameters) { Record record = null; try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, parameters); using (var reader = command.ExecuteReader()) { if (reader.Read()) { record = RecordFromReader(reader); } } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } return record; } internal List GetRecordsHelper(string query, params string[] parameters) { List records = new List(); try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, parameters); using (var reader = command.ExecuteReader()) { while (reader.HasRows && reader.Read()) { records.Add(RecordFromReader(reader)); } } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } return records; } internal long GetLastSyncCountHelper(string query, params string[] parameters) { long lastSyncCount = 0; try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, parameters); using (var reader = command.ExecuteReader()) { if (reader.HasRows && reader.Read()) { lastSyncCount = reader.GetInt64(DatasetColumns.LAST_SYNC_COUNT_IDX); } } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } return lastSyncCount; } internal List GetModifiedRecordsHelper(string query, params object[] parameters) { List records = new List(); try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = new SqliteCommand(connection)) { command.CommandText = query; BindData(command, parameters); using (var reader = command.ExecuteReader()) { while (reader.HasRows && reader.Read()) { records.Add(RecordFromReader(reader)); } } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } return records; } internal void UpdateOrInsertRecord(string identityId, string datasetName, Record record) { lock (sqlite_lock) { string checkRecordExistsQuery = "SELECT COUNT(*) FROM " + SQLiteLocalStorage.TABLE_RECORDS + " WHERE " + RecordColumns.IDENTITY_ID + " = @whereIdentityId AND " + RecordColumns.DATASET_NAME + " = @whereDatasetName AND " + RecordColumns.KEY + " = @whereKey "; bool recordsFound = false; try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = checkRecordExistsQuery; BindData(command, identityId, datasetName, record.Key); using (var reader = command.ExecuteReader()) { if (reader.Read()) recordsFound = reader.GetInt32(0) > 0; } } if (recordsFound) { string updateRecordQuery = RecordColumns.BuildUpdate( new string[] { RecordColumns.VALUE, RecordColumns.SYNC_COUNT, RecordColumns.MODIFIED, RecordColumns.LAST_MODIFIED_TIMESTAMP, RecordColumns.LAST_MODIFIED_BY, RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP }, RecordColumns.IDENTITY_ID + " = @whereIdentityId AND " + RecordColumns.DATASET_NAME + " = @whereDatasetName AND " + RecordColumns.KEY + " = @whereKey " ); using (var command = connection.CreateCommand()) { command.CommandText = updateRecordQuery; BindData(command, record.Value, record.SyncCount, record.IsModified ? 1 : 0, record.LastModifiedDate, record.LastModifiedBy, record.DeviceLastModifiedDate, identityId, datasetName, record.Key); command.ExecuteNonQuery(); } } else { string insertRecord = RecordColumns.BuildInsert(); using (var command = new SqliteCommand(insertRecord, connection)) { BindData(command, identityId, datasetName, record.Key, record.Value, record.SyncCount, record.LastModifiedDate, record.LastModifiedBy, record.DeviceLastModifiedDate, record.IsModified ? 1 : 0); command.ExecuteNonQuery(); } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } } private static Record RecordFromReader(SqliteDataReader reader) { return new Record( reader.GetString(RecordColumns.KEY_IDX), reader.IsDBNull(RecordColumns.VALUE_IDX) ? null : reader.GetString(RecordColumns.VALUE_IDX), reader.GetInt32(RecordColumns.SYNC_COUNT_IDX), new DateTime(long.Parse(reader.GetString(RecordColumns.LAST_MODIFIED_TIMESTAMP_IDX), CultureInfo.InvariantCulture.NumberFormat), DateTimeKind.Utc), reader.IsDBNull(RecordColumns.LAST_MODIFIED_BY_IDX) ? string.Empty : reader.GetString(RecordColumns.LAST_MODIFIED_BY_IDX), new DateTime(long.Parse(reader.GetString(RecordColumns.DEVICE_LAST_MODIFIED_TIMESTAMP_IDX), CultureInfo.InvariantCulture.NumberFormat), DateTimeKind.Utc), reader.GetInt32(RecordColumns.MODIFIED_IDX) == 1); } /// /// cache the identity /// /// /// public void CacheIdentity(string key, string identity) { string query = "INSERT INTO kvstore(key,value) values ( @key , @value )"; try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = query; BindData(command, key, identity); command.ExecuteNonQuery(); } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } /// /// Delete the cached identity id /// /// public void DeleteCachedIdentity(string key) { string query = "delete from kvstore where key = @key "; try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = query; BindData(command, key); command.ExecuteNonQuery(); } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } internal void ExecuteMultipleHelper(List statements) { try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); foreach (var stmt in statements) { using (var command = connection.CreateCommand()) { command.CommandText = stmt.Query; BindData(command, stmt.Parameters); command.ExecuteNonQuery(); } } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } internal void UpdateLastSyncCountHelper(string query, params object[] parameters) { try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = query; BindData(command, parameters); command.ExecuteNonQuery(); } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } internal void UpdateLastModifiedTimestampHelper(string query, params object[] parameters) { try { connection = new SqliteConnection("URI=file:" + filePath); connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = query; BindData(command, parameters); command.ExecuteNonQuery(); } } finally { if (connection != null) { connection.Close(); connection.Dispose(); } } } } }