added iOS source code
[wl-app.git] / iOS / Pods / FirebaseMessaging / Firebase / Messaging / FIRMessagingRmq2PersistentStore.m
diff --git a/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m b/iOS/Pods/FirebaseMessaging/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m
new file mode 100644 (file)
index 0000000..dc6e6c9
--- /dev/null
@@ -0,0 +1,807 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "FIRMessagingRmq2PersistentStore.h"
+
+#import <sqlite3.h>
+
+#import "FIRMessagingConstants.h"
+#import "FIRMessagingDefines.h"
+#import "FIRMessagingLogger.h"
+#import "FIRMessagingPersistentSyncMessage.h"
+#import "FIRMessagingUtilities.h"
+#import "NSError+FIRMessaging.h"
+#import "Protos/GtalkCore.pbobjc.h"
+
+#ifndef _FIRMessagingRmqLogAndExit
+#define _FIRMessagingRmqLogAndExit(stmt, return_value)   \
+do {                              \
+[self logErrorAndFinalizeStatement:stmt];  \
+return return_value; \
+} while(0)
+#endif
+
+typedef enum : NSUInteger {
+  FIRMessagingRmqDirectoryUnknown,
+  FIRMessagingRmqDirectoryDocuments,
+  FIRMessagingRmqDirectoryApplicationSupport,
+} FIRMessagingRmqDirectory;
+
+static NSString *const kFCMRmqStoreTag = @"FIRMessagingRmqStore:";
+
+// table names
+NSString *const kTableOutgoingRmqMessages = @"outgoingRmqMessages";
+NSString *const kTableLastRmqId = @"lastrmqid";
+NSString *const kOldTableS2DRmqIds = @"s2dRmqIds";
+NSString *const kTableS2DRmqIds = @"s2dRmqIds_1";
+
+// Used to prevent de-duping of sync messages received both via APNS and MCS.
+NSString *const kTableSyncMessages = @"incomingSyncMessages";
+
+static NSString *const kTablePrefix = @"";
+
+// create tables
+static NSString *const kCreateTableOutgoingRmqMessages =
+    @"create TABLE IF NOT EXISTS %@%@ "
+    @"(_id INTEGER PRIMARY KEY, "
+    @"rmq_id INTEGER, "
+    @"type INTEGER, "
+    @"ts INTEGER, "
+    @"data BLOB)";
+
+static NSString *const kCreateTableLastRmqId =
+    @"create TABLE IF NOT EXISTS %@%@ "
+    @"(_id INTEGER PRIMARY KEY, "
+    @"rmq_id INTEGER)";
+
+static NSString *const kCreateTableS2DRmqIds =
+    @"create TABLE IF NOT EXISTS %@%@ "
+    @"(_id INTEGER PRIMARY KEY, "
+    @"rmq_id TEXT)";
+
+static NSString *const kCreateTableSyncMessages =
+    @"create TABLE IF NOT EXISTS %@%@ "
+    @"(_id INTEGER PRIMARY KEY, "
+    @"rmq_id TEXT, "
+    @"expiration_ts INTEGER, "
+    @"apns_recv INTEGER, "
+    @"mcs_recv INTEGER)";
+
+static NSString *const kDropTableCommand =
+    @"drop TABLE if exists %@%@";
+
+// table infos
+static NSString *const kRmqIdColumn = @"rmq_id";
+static NSString *const kDataColumn = @"data";
+static NSString *const kProtobufTagColumn = @"type";
+static NSString *const kIdColumn = @"_id";
+
+static NSString *const kOutgoingRmqMessagesColumns = @"rmq_id, type, data";
+
+// Sync message columns
+static NSString *const kSyncMessagesColumns = @"rmq_id, expiration_ts, apns_recv, mcs_recv";
+// Message time expiration in seconds since 1970
+static NSString *const kSyncMessageExpirationTimestampColumn = @"expiration_ts";
+static NSString *const kSyncMessageAPNSReceivedColumn = @"apns_recv";
+static NSString *const kSyncMessageMCSReceivedColumn = @"mcs_recv";
+
+// table data handlers
+typedef void(^FCMOutgoingRmqMessagesTableHandler)(int64_t rmqId, int8_t tag, NSData *data);
+
+// Utility to create an NSString from a sqlite3 result code
+NSString * _Nonnull FIRMessagingStringFromSQLiteResult(int result) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+  const char *errorStr = sqlite3_errstr(result);
+#pragma pop
+  NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr];
+  return errorString;
+}
+
+@interface FIRMessagingRmq2PersistentStore () {
+  sqlite3 *_database;
+}
+
+@property(nonatomic, readwrite, strong) NSString *databaseName;
+@property(nonatomic, readwrite, assign) FIRMessagingRmqDirectory currentDirectory;
+
+@end
+
+@implementation FIRMessagingRmq2PersistentStore
+
+- (instancetype)initWithDatabaseName:(NSString *)databaseName {
+  self = [super init];
+  if (self) {
+    _databaseName = [databaseName copy];
+    BOOL didMoveToApplicationSupport =
+        [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory];
+
+    _currentDirectory = didMoveToApplicationSupport
+                            ? FIRMessagingRmqDirectoryApplicationSupport
+                            : FIRMessagingRmqDirectoryDocuments;
+
+    [self openDatabase:_databaseName];
+  }
+  return self;
+}
+
+- (void)dealloc {
+  sqlite3_close(_database);
+}
+
+- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName {
+  NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+                                                                NSUserDomainMask, YES);
+  NSString *applicationSupportDirPath = directoryPaths.lastObject;
+  NSArray *components = @[applicationSupportDirPath, subDirectoryName];
+  NSString *subDirectoryPath = [NSString pathWithComponents:components];
+  BOOL hasSubDirectory;
+
+  if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath
+                                            isDirectory:&hasSubDirectory]) {
+    // Cannot move to non-existent directory
+    return NO;
+  }
+
+  if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryDocuments]) {
+    NSString *oldPlistPath = [[self class] pathForDatabase:self.databaseName
+                                               inDirectory:FIRMessagingRmqDirectoryDocuments];
+    NSString *newPlistPath = [[self class]
+        pathForDatabase:self.databaseName
+            inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+
+    if ([self doesFileExistInDirectory:FIRMessagingRmqDirectoryApplicationSupport]) {
+      // File exists in both Documents and ApplicationSupport, delete the one in Documents
+      NSError *deleteError;
+      if (![[NSFileManager defaultManager] removeItemAtPath:oldPlistPath error:&deleteError]) {
+        FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore000,
+                                @"Failed to delete old copy of %@.sqlite in Documents %@",
+                                self.databaseName, deleteError);
+      }
+      return NO;
+    }
+    NSError *moveError;
+    if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath
+                                                 toPath:newPlistPath
+                                                  error:&moveError]) {
+      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore001,
+                              @"Failed to move file %@ from %@ to %@. Error: %@", self.databaseName,
+                              oldPlistPath, newPlistPath, moveError);
+      return NO;
+    }
+  }
+  // We moved the file if it existed, otherwise we didn't need to do anything
+  return YES;
+}
+
+- (BOOL)doesFileExistInDirectory:(FIRMessagingRmqDirectory)directory {
+  NSString *path = [[self class] pathForDatabase:self.databaseName inDirectory:directory];
+  return [[NSFileManager defaultManager] fileExistsAtPath:path];
+}
+
++ (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDirectory)directory {
+  NSArray *paths;
+  NSArray *components;
+  NSString *dbNameWithExtension = [NSString stringWithFormat:@"%@.sqlite", dbName];
+  NSString *errorMessage;
+
+  switch (directory) {
+    case FIRMessagingRmqDirectoryDocuments:
+      paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+      components = @[paths.lastObject, dbNameWithExtension];
+      break;
+
+    case FIRMessagingRmqDirectoryApplicationSupport:
+      paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+                                                  NSUserDomainMask,
+                                                  YES);
+      components = @[
+                     paths.lastObject,
+                     kFIRMessagingApplicationSupportSubDirectory,
+                     dbNameWithExtension
+                     ];
+      break;
+
+    default:
+      errorMessage = [NSString stringWithFormat:@"Invalid directory type %lu",
+                      (unsigned long)directory];
+      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreInvalidRmqDirectory,
+                              @"%@",
+                              errorMessage);
+      NSAssert(NO, errorMessage);
+      break;
+  }
+
+  return [NSString pathWithComponents:components];
+}
+
+- (void)createTableWithName:(NSString *)tableName command:(NSString *)command {
+  char *error;
+  NSString *createDatabase = [NSString stringWithFormat:command, kTablePrefix, tableName];
+  if (sqlite3_exec(_database, [createDatabase UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+    // remove db before failing
+    [self removeDatabase];
+    NSString *errorMessage = [NSString stringWithFormat:@"Couldn't create table: %@ %@",
+                              kCreateTableOutgoingRmqMessages,
+                              [NSString stringWithCString:error encoding:NSUTF8StringEncoding]];
+    FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingTable,
+                            @"%@",
+                            errorMessage);
+    NSAssert(NO, errorMessage);
+  }
+}
+
+- (void)dropTableWithName:(NSString *)tableName {
+  char *error;
+  NSString *dropTableSQL = [NSString stringWithFormat:kDropTableCommand, kTablePrefix, tableName];
+  if (sqlite3_exec(_database, [dropTableSQL UTF8String], NULL, NULL, &error) != SQLITE_OK) {
+    FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore002,
+                            @"Failed to remove table %@", tableName);
+  }
+}
+
+- (void)removeDatabase {
+  NSString *path = [[self class] pathForDatabase:self.databaseName
+                                     inDirectory:self.currentDirectory];
+  [[NSFileManager defaultManager] removeItemAtPath:path error:nil];
+}
+
++ (void)removeDatabase:(NSString *)dbName {
+  NSString *documentsDirPath = [self pathForDatabase:dbName
+                                         inDirectory:FIRMessagingRmqDirectoryDocuments];
+  NSString *applicationSupportDirPath =
+      [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport];
+  [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil];
+  [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil];
+}
+
+- (void)openDatabase:(NSString *)dbName {
+  NSFileManager *fileManager = [NSFileManager defaultManager];
+  NSString *path = [[self class] pathForDatabase:dbName inDirectory:self.currentDirectory];
+
+  BOOL didOpenDatabase = YES;
+  if (![fileManager fileExistsAtPath:path]) {
+    // We've to separate between different versions here because of backwards compatbility issues.
+    int result = sqlite3_open([path UTF8String], &_database);
+    if (result != SQLITE_OK) {
+      NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
+      NSString *errorMessage =
+          [NSString stringWithFormat:@"Could not open existing RMQ database at path %@, error: %@",
+                                     path,
+                                     errorString];
+      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorOpeningDatabase,
+                              @"%@",
+                              errorMessage);
+      NSAssert(NO, errorMessage);
+      return;
+    }
+    [self createTableWithName:kTableOutgoingRmqMessages
+                      command:kCreateTableOutgoingRmqMessages];
+
+    [self createTableWithName:kTableLastRmqId command:kCreateTableLastRmqId];
+    [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+  } else {
+    // Calling sqlite3_open should create the database, since the file doesn't exist.
+    int result = sqlite3_open([path UTF8String], &_database);
+    if (result != SQLITE_OK) {
+      NSString *errorString = FIRMessagingStringFromSQLiteResult(result);
+      NSString *errorMessage =
+          [NSString stringWithFormat:@"Could not create RMQ database at path %@, error: %@",
+                                     path,
+                                     errorString];
+      FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStoreErrorCreatingDatabase,
+                              @"%@",
+                              errorMessage);
+      NSAssert(NO, errorMessage);
+      didOpenDatabase = NO;
+    } else {
+      [self updateDbWithStringRmqID];
+    }
+  }
+
+  if (didOpenDatabase) {
+    [self createTableWithName:kTableSyncMessages command:kCreateTableSyncMessages];
+  }
+}
+
+- (void)updateDbWithStringRmqID {
+  [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds];
+  [self dropTableWithName:kOldTableS2DRmqIds];
+}
+
+#pragma mark - Insert
+
+- (BOOL)saveUnackedS2dMessageWithRmqId:(NSString *)rmqId {
+  NSString *insertFormat = @"INSERT INTO %@ (%@) VALUES (?)";
+  NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+                         kTableS2DRmqIds,
+                         kRmqIdColumn];
+  sqlite3_stmt *insert_statement;
+  if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+      != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  if (sqlite3_bind_text(insert_statement,
+                        1,
+                        [rmqId UTF8String],
+                        (int)[rmqId length],
+                        SQLITE_STATIC) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  sqlite3_finalize(insert_statement);
+  return YES;
+}
+
+- (BOOL)saveMessageWithRmqId:(int64_t)rmqId
+                         tag:(int8_t)tag
+                        data:(NSData *)data
+                       error:(NSError **)error {
+  NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@) VALUES (?, ?, ?)";
+  NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+                         kTableOutgoingRmqMessages, // table
+                         kRmqIdColumn, kProtobufTagColumn, kDataColumn /* columns */];
+  sqlite3_stmt *insert_statement;
+  if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &insert_statement, NULL)
+      != SQLITE_OK) {
+    if (error) {
+      *error = [NSError errorWithDomain:[NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]
+                                   code:sqlite3_errcode(_database)
+                               userInfo:nil];
+    }
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  if (sqlite3_bind_int64(insert_statement, 1, rmqId) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  if (sqlite3_bind_int(insert_statement, 2, tag) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  if (sqlite3_bind_blob(insert_statement, 3, [data bytes], (int)[data length], NULL) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+  if (sqlite3_step(insert_statement) != SQLITE_DONE) {
+    _FIRMessagingRmqLogAndExit(insert_statement, NO);
+  }
+
+  sqlite3_finalize(insert_statement);
+  return YES;
+}
+
+- (int)deleteMessagesFromTable:(NSString *)tableName
+                    withRmqIds:(NSArray *)rmqIds {
+  _FIRMessagingDevAssert([tableName isEqualToString:kTableOutgoingRmqMessages] ||
+                [tableName isEqualToString:kTableLastRmqId] ||
+                [tableName isEqualToString:kTableS2DRmqIds] ||
+                [tableName isEqualToString:kTableSyncMessages],
+                @"%@: Invalid Table Name %@", kFCMRmqStoreTag, tableName);
+
+  BOOL isRmqIDString = NO;
+  // RmqID is a string only for outgoing messages
+  if ([tableName isEqualToString:kTableS2DRmqIds] ||
+      [tableName isEqualToString:kTableSyncMessages]) {
+    isRmqIDString = YES;
+  }
+
+  NSMutableString *delete = [NSMutableString stringWithFormat:@"DELETE FROM %@ WHERE ", tableName];
+
+  NSString *toDeleteArgument = [NSString stringWithFormat:@"%@ = ? OR ", kRmqIdColumn];
+
+  int toDelete = (int)[rmqIds count];
+  if (toDelete == 0) {
+    return 0;
+  }
+  int maxBatchSize = 100;
+  int start = 0;
+  int deleteCount = 0;
+  while (start < toDelete) {
+
+    // construct the WHERE argument
+    int end = MIN(start + maxBatchSize, toDelete);
+    NSMutableString *whereArgument = [NSMutableString string];
+    for (int i = start; i < end; i++) {
+      [whereArgument appendString:toDeleteArgument];
+    }
+    // remove the last * OR * from argument
+    NSRange range = NSMakeRange([whereArgument length] -4, 4);
+    [whereArgument deleteCharactersInRange:range];
+    NSString *deleteQuery = [NSString stringWithFormat:@"%@ %@", delete, whereArgument];
+
+
+    // sqlite update
+    sqlite3_stmt *delete_statement;
+    if (sqlite3_prepare_v2(_database, [deleteQuery UTF8String],
+                           -1, &delete_statement, NULL) != SQLITE_OK) {
+      _FIRMessagingRmqLogAndExit(delete_statement, 0);
+    }
+
+    // bind values
+    int rmqIndex = 0;
+    int placeholderIndex = 1; // placeholders in sqlite3 start with 1
+    for (NSString *rmqId in rmqIds) { // objectAtIndex: is O(n) -- would make it slow
+      if (rmqIndex < start) {
+        rmqIndex++;
+        continue;
+      } else if (rmqIndex >= end) {
+        break;
+      } else {
+        if (isRmqIDString) {
+          if (sqlite3_bind_text(delete_statement,
+                                placeholderIndex,
+                                [rmqId UTF8String],
+                                (int)[rmqId length],
+                                SQLITE_STATIC) != SQLITE_OK) {
+            FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore003,
+                                    @"Failed to bind rmqID %@", rmqId);
+            continue;
+          }
+        } else {
+          int64_t rmqIdValue = [rmqId longLongValue];
+          sqlite3_bind_int64(delete_statement, placeholderIndex, rmqIdValue);
+        }
+        placeholderIndex++;
+      }
+      rmqIndex++;
+    }
+    if (sqlite3_step(delete_statement) != SQLITE_DONE) {
+      _FIRMessagingRmqLogAndExit(delete_statement, deleteCount);
+    }
+    sqlite3_finalize(delete_statement);
+    deleteCount += sqlite3_changes(_database);
+    start = end;
+  }
+
+  // if we are here all of our sqlite queries should have succeeded
+  FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore004,
+                          @"%@ Trying to delete %d s2D ID's, successfully deleted %d",
+                          kFCMRmqStoreTag, toDelete, deleteCount);
+  return deleteCount;
+}
+
+#pragma mark - Query
+
+- (int64_t)queryHighestRmqId {
+  NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kRmqIdColumn, // column
+                     kTableOutgoingRmqMessages, // table
+                     kRmqIdColumn, // order by column
+                     1]; // limit
+
+  sqlite3_stmt *statement;
+  int64_t highestRmqId = 0;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(statement, highestRmqId);
+  }
+  if (sqlite3_step(statement) == SQLITE_ROW) {
+    highestRmqId = sqlite3_column_int64(statement, 0);
+  }
+  sqlite3_finalize(statement);
+  return highestRmqId;
+}
+
+- (int64_t)queryLastRmqId {
+  NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ DESC LIMIT %d";
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kRmqIdColumn, // column
+                     kTableLastRmqId, // table
+                     kRmqIdColumn, // order by column
+                     1]; // limit
+
+  sqlite3_stmt *statement;
+  int64_t lastRmqId = 0;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(statement, lastRmqId);
+  }
+  if (sqlite3_step(statement) == SQLITE_ROW) {
+    lastRmqId = sqlite3_column_int64(statement, 0);
+  }
+  sqlite3_finalize(statement);
+  return lastRmqId;
+}
+
+- (BOOL)updateLastOutgoingRmqId:(int64_t)rmqID {
+  NSString *queryFormat = @"INSERT OR REPLACE INTO %@ (%@, %@) VALUES (?, ?)";
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kTableLastRmqId, // table
+                     kIdColumn, kRmqIdColumn]; // columns
+  sqlite3_stmt *statement;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(statement, NO);
+  }
+  if (sqlite3_bind_int(statement, 1, 1) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(statement, NO);
+  }
+  if (sqlite3_bind_int64(statement, 2, rmqID) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(statement, NO);
+  }
+  if (sqlite3_step(statement) != SQLITE_DONE) {
+    _FIRMessagingRmqLogAndExit(statement, NO);
+  }
+  sqlite3_finalize(statement);
+  return YES;
+}
+
+- (NSArray *)unackedS2dRmqIds {
+  NSString *queryFormat = @"SELECT %@ FROM %@ ORDER BY %@ ASC";
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kRmqIdColumn,
+                     kTableS2DRmqIds,
+                     kRmqIdColumn];
+  sqlite3_stmt *statement;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+    FIRMessagingLoggerDebug(kFIRMessagingMessageCodeRmq2PersistentStore005,
+                            @"%@: Could not find s2d ids", kFCMRmqStoreTag);
+    _FIRMessagingRmqLogAndExit(statement, @[]);
+  }
+  NSMutableArray *rmqIDArray = [NSMutableArray array];
+  while (sqlite3_step(statement) == SQLITE_ROW) {
+    const char *rmqID = (char *)sqlite3_column_text(statement, 0);
+    [rmqIDArray addObject:[NSString stringWithUTF8String:rmqID]];
+  }
+  sqlite3_finalize(statement);
+  return rmqIDArray;
+}
+
+#pragma mark - Scan
+
+- (void)scanOutgoingRmqMessagesWithHandler:(FCMOutgoingRmqMessagesTableHandler)handler {
+  static NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ != 0 ORDER BY %@ ASC";
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kOutgoingRmqMessagesColumns, // select (rmq_id, type, data)
+                     kTableOutgoingRmqMessages, // from table
+                     kRmqIdColumn, // where
+                     kRmqIdColumn]; // order by
+  sqlite3_stmt *statement;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &statement, NULL) != SQLITE_OK) {
+    [self logError];
+    sqlite3_finalize(statement);
+    return;
+  }
+  // can query sqlite3 for this but this is fine
+  const int rmqIdColumnNumber = 0;
+  const int typeColumnNumber = 1;
+  const int dataColumnNumber = 2;
+  while (sqlite3_step(statement) == SQLITE_ROW) {
+    int64_t rmqId = sqlite3_column_int64(statement, rmqIdColumnNumber);
+    int8_t type = sqlite3_column_int(statement, typeColumnNumber);
+    const void *bytes = sqlite3_column_blob(statement, dataColumnNumber);
+    int length = sqlite3_column_bytes(statement, dataColumnNumber);
+    _FIRMessagingDevAssert(bytes != NULL,
+                           @"%@ Message with no data being stored in Rmq",
+                           kFCMRmqStoreTag);
+    NSData *data = [NSData dataWithBytes:bytes length:length];
+    handler(rmqId, type, data);
+  }
+  sqlite3_finalize(statement);
+}
+
+#pragma mark - Sync Messages
+
+- (FIRMessagingPersistentSyncMessage *)querySyncMessageWithRmqID:(NSString *)rmqID {
+  _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to search in SYNC_RMQ", rmqID);
+
+  NSString *queryFormat = @"SELECT %@ FROM %@ WHERE %@ = '%@'";
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kSyncMessagesColumns, // SELECT (rmq_id, expiration_ts, apns_recv, mcs_recv)
+                     kTableSyncMessages,   // FROM sync_rmq
+                     kRmqIdColumn,         // WHERE rmq_id
+                     rmqID];
+
+  sqlite3_stmt *stmt;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+    [self logError];
+    sqlite3_finalize(stmt);
+    return nil;
+  }
+
+  const int rmqIDColumn = 0;
+  const int expirationTimestampColumn = 1;
+  const int apnsReceivedColumn = 2;
+  const int mcsReceivedColumn = 3;
+
+  int count = 0;
+  FIRMessagingPersistentSyncMessage *persistentMessage;
+
+  while (sqlite3_step(stmt) == SQLITE_ROW) {
+    NSString *rmqID =
+        [NSString stringWithUTF8String:(char *)sqlite3_column_text(stmt, rmqIDColumn)];
+    int64_t expirationTimestamp = sqlite3_column_int64(stmt, expirationTimestampColumn);
+    BOOL apnsReceived = sqlite3_column_int(stmt, apnsReceivedColumn);
+    BOOL mcsReceived = sqlite3_column_int(stmt, mcsReceivedColumn);
+
+    // create a new persistent message
+    persistentMessage =
+        [[FIRMessagingPersistentSyncMessage alloc] initWithRMQID:rmqID expirationTime:expirationTimestamp];
+    persistentMessage.apnsReceived = apnsReceived;
+    persistentMessage.mcsReceived = mcsReceived;
+
+    count++;
+  }
+  sqlite3_finalize(stmt);
+
+  _FIRMessagingDevAssert(count <= 1, @"Found multiple messages in %@ with same RMQ ID", kTableSyncMessages);
+  return persistentMessage;
+}
+
+- (BOOL)deleteSyncMessageWithRmqID:(NSString *)rmqID {
+  _FIRMessagingDevAssert([rmqID length], @"Invalid rmqID key %@ to delete in SYNC_RMQ", rmqID);
+  return [self deleteMessagesFromTable:kTableSyncMessages withRmqIds:@[rmqID]] > 0;
+}
+
+- (int)deleteExpiredOrFinishedSyncMessages:(NSError *__autoreleasing *)error {
+  int64_t now = FIRMessagingCurrentTimestampInSeconds();
+  NSString *deleteSQL = @"DELETE FROM %@ "
+                        @"WHERE %@ < %lld OR "  // expirationTime < now
+                        @"(%@ = 1 AND %@ = 1)";  // apns_received = 1 AND mcs_received = 1
+  NSString *query = [NSString stringWithFormat:deleteSQL,
+                     kTableSyncMessages,
+                     kSyncMessageExpirationTimestampColumn,
+                     now,
+                     kSyncMessageAPNSReceivedColumn,
+                     kSyncMessageMCSReceivedColumn];
+
+  NSString *errorReason = @"Failed to save delete expired sync messages from store.";
+
+  sqlite3_stmt *stmt;
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+    if (error) {
+      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+                                 userInfo:@{ @"error" : errorReason }];
+    }
+    _FIRMessagingRmqLogAndExit(stmt, 0);
+  }
+
+  if (sqlite3_step(stmt) != SQLITE_DONE) {
+    if (error) {
+      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+                                 userInfo:@{ @"error" : errorReason }];
+    }
+    _FIRMessagingRmqLogAndExit(stmt, 0);
+  }
+
+  sqlite3_finalize(stmt);
+  int deleteCount = sqlite3_changes(_database);
+  return deleteCount;
+}
+
+- (BOOL)saveSyncMessageWithRmqID:(NSString *)rmqID
+                  expirationTime:(int64_t)expirationTime
+                    apnsReceived:(BOOL)apnsReceived
+                     mcsReceived:(BOOL)mcsReceived
+                           error:(NSError **)error {
+  _FIRMessagingDevAssert([rmqID length], @"Invalid nil message to persist to SYNC_RMQ");
+
+  NSString *insertFormat = @"INSERT INTO %@ (%@, %@, %@, %@) VALUES (?, ?, ?, ?)";
+  NSString *insertSQL = [NSString stringWithFormat:insertFormat,
+                         kTableSyncMessages, // Table name
+                         kRmqIdColumn, // rmq_id
+                         kSyncMessageExpirationTimestampColumn, // expiration_ts
+                         kSyncMessageAPNSReceivedColumn, // apns_recv
+                         kSyncMessageMCSReceivedColumn /* mcs_recv */];
+
+  sqlite3_stmt *stmt;
+
+  if (sqlite3_prepare_v2(_database, [insertSQL UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+    if (error) {
+      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+                                 userInfo:@{ @"error" : @"Failed to save sync message to store." }];
+    }
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_bind_int64(stmt, 2, expirationTime) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_bind_int(stmt, 3, apnsReceived ? 1 : 0) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_bind_int(stmt, 4, mcsReceived ? 1 : 0) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_step(stmt) != SQLITE_DONE) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  sqlite3_finalize(stmt);
+  return YES;
+}
+
+- (BOOL)updateSyncMessageViaAPNSWithRmqID:(NSString *)rmqID
+                                    error:(NSError **)error {
+  return [self updateSyncMessageWithRmqID:rmqID
+                                   column:kSyncMessageAPNSReceivedColumn
+                                    value:YES
+                                    error:error];
+}
+
+- (BOOL)updateSyncMessageViaMCSWithRmqID:(NSString *)rmqID
+                                   error:(NSError *__autoreleasing *)error {
+  return [self updateSyncMessageWithRmqID:rmqID
+                                   column:kSyncMessageMCSReceivedColumn
+                                    value:YES
+                                    error:error];
+}
+
+- (BOOL)updateSyncMessageWithRmqID:(NSString *)rmqID
+                            column:(NSString *)column
+                             value:(BOOL)value
+                             error:(NSError **)error {
+  _FIRMessagingDevAssert([column isEqualToString:kSyncMessageAPNSReceivedColumn] ||
+                [column isEqualToString:kSyncMessageMCSReceivedColumn],
+                @"Invalid column name %@ for SYNC_RMQ", column);
+  NSString *queryFormat = @"UPDATE %@ "  // Table name
+                          @"SET %@ = %d "  // column=value
+                          @"WHERE %@ = ?";  // condition
+  NSString *query = [NSString stringWithFormat:queryFormat,
+                     kTableSyncMessages,
+                     column,
+                     value ? 1 : 0,
+                     kRmqIdColumn];
+  sqlite3_stmt *stmt;
+
+  if (sqlite3_prepare_v2(_database, [query UTF8String], -1, &stmt, NULL) != SQLITE_OK) {
+    if (error) {
+      *error = [NSError fcm_errorWithCode:sqlite3_errcode(_database)
+                                 userInfo:@{ @"error" : @"Failed to update sync message"}];
+    }
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_bind_text(stmt, 1, [rmqID UTF8String], (int)[rmqID length], NULL) != SQLITE_OK) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  if (sqlite3_step(stmt) != SQLITE_DONE) {
+    _FIRMessagingRmqLogAndExit(stmt, NO);
+  }
+
+  sqlite3_finalize(stmt);
+  return YES;
+
+}
+
+#pragma mark - Private
+
+- (NSString *)lastErrorMessage {
+  return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)];
+}
+
+- (int)lastErrorCode {
+  return sqlite3_errcode(_database);
+}
+
+- (void)logError {
+  FIRMessagingLoggerError(kFIRMessagingMessageCodeRmq2PersistentStore006,
+                          @"%@ error: code (%d) message: %@", kFCMRmqStoreTag, [self lastErrorCode],
+                          [self lastErrorMessage]);
+}
+
+- (void)logErrorAndFinalizeStatement:(sqlite3_stmt *)stmt {
+  [self logError];
+  sqlite3_finalize(stmt);
+}
+
+@end