/ keychain / ckks / CKKSSQLDatabaseObject.m
CKKSSQLDatabaseObject.m
  1  /*
  2   * Copyright (c) 2016 Apple Inc. All Rights Reserved.
  3   *
  4   * @APPLE_LICENSE_HEADER_START@
  5   *
  6   * This file contains Original Code and/or Modifications of Original Code
  7   * as defined in and that are subject to the Apple Public Source License
  8   * Version 2.0 (the 'License'). You may not use this file except in
  9   * compliance with the License. Please obtain a copy of the License at
 10   * http://www.opensource.apple.com/apsl/ and read it before using this
 11   * file.
 12   *
 13   * The Original Code and all software distributed under the License are
 14   * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 15   * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 16   * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 17   * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 18   * Please see the License for the specific language governing rights and
 19   * limitations under the License.
 20   *
 21   * @APPLE_LICENSE_HEADER_END@
 22   */
 23  
 24  #import <Foundation/Foundation.h>
 25  #import "CKKSSQLDatabaseObject.h"
 26  #include "keychain/securityd/SecItemServer.h"
 27  #include "keychain/securityd/SecItemDb.h"
 28  
 29  #import "keychain/ckks/CKKS.h"
 30  #import "CKKSKeychainView.h"
 31  
 32  @interface CKKSSQLResult ()
 33  @property (nullable) NSString* stringValue;
 34  @end
 35  
 36  @implementation CKKSSQLResult
 37  - (instancetype)init:(NSString* _Nullable)value
 38  {
 39      if((self = [super init])) {
 40          _stringValue = value;
 41      }
 42      return self;
 43  }
 44  
 45  - (BOOL)asBOOL
 46  {
 47      return [self.stringValue boolValue];
 48  }
 49  
 50  - (NSInteger)asNSInteger
 51  {
 52      return [self.stringValue integerValue];
 53  }
 54  
 55  - (NSString* _Nullable)asString
 56  {
 57      return self.stringValue;
 58  }
 59  
 60  - (NSNumber* _Nullable)asNSNumberInteger
 61  {
 62      if(self.stringValue == nil) {
 63          return nil;
 64      }
 65      return [NSNumber numberWithInteger: [self.stringValue integerValue]];
 66  }
 67  
 68  - (NSDate* _Nullable)asISO8601Date
 69  {
 70      if(self.stringValue == nil) {
 71          return nil;
 72      }
 73  
 74      NSISO8601DateFormatter* dateFormat = [[NSISO8601DateFormatter alloc] init];
 75      return [dateFormat dateFromString:self.stringValue];
 76  }
 77  
 78  - (NSData* _Nullable)asBase64DecodedData
 79  {
 80      if(self.stringValue == nil) {
 81          return nil;
 82      }
 83      return [[NSData alloc] initWithBase64EncodedString:self.stringValue options:0];
 84  }
 85  @end
 86  
 87  __thread bool CKKSSQLInTransaction = false;
 88  __thread bool CKKSSQLInWriteTransaction = false;
 89  
 90  @implementation CKKSSQLDatabaseObject
 91  
 92  + (bool) saveToDatabaseTable: (NSString*) table row: (NSDictionary*) row connection: (SecDbConnectionRef) dbconn error: (NSError * __autoreleasing *) error {
 93      __block CFErrorRef cferror = NULL;
 94  
 95  #if DEBUG
 96      NSAssert(CKKSSQLInTransaction, @"Must be in a transaction to perform database writes");
 97      NSAssert(CKKSSQLInWriteTransaction, @"Must be in a write transaction to perform database writes");
 98  #endif
 99  
100      bool (^doWithConnection)(SecDbConnectionRef) = ^bool (SecDbConnectionRef dbconn) {
101          NSString * columns = [row.allKeys componentsJoinedByString:@", "];
102          NSMutableString * values = [[NSMutableString alloc] init];
103          for(NSUInteger i = 0; i < [row.allKeys count]; i++) {
104              if(i != 0) {
105                  [values appendString: @",?"];
106              } else {
107                  [values appendString: @"?"];
108              }
109          }
110  
111          NSString *sql = [[NSString alloc] initWithFormat: @"INSERT OR REPLACE into %@ (%@) VALUES (%@);", table, columns, values];
112  
113          SecDbPrepare(dbconn, (__bridge CFStringRef) sql, &cferror, ^void (sqlite3_stmt *stmt) {
114              [row.allKeys enumerateObjectsUsingBlock:^(id  _Nonnull key, NSUInteger i, BOOL * _Nonnull stop) {
115                  SecDbBindObject(stmt, (int)(i+1), (__bridge CFStringRef) row[key], &cferror);
116              }];
117  
118              SecDbStep(dbconn, stmt, &cferror, ^(bool *stop) {
119                  // don't do anything, I guess?
120              });
121          });
122  
123          return true;
124      };
125  
126      if(dbconn) {
127          doWithConnection(dbconn);
128      } else {
129          kc_with_dbt(true, &cferror, doWithConnection);
130      }
131  
132      bool ret = cferror == NULL;
133  
134      SecTranslateError(error, cferror);
135  
136      return ret;
137  }
138  
139  + (NSString*) makeWhereClause: (NSDictionary*) whereDict {
140      if(!whereDict) {
141          return @"";
142      }
143      NSMutableString * whereClause = [[NSMutableString alloc] init];
144      __block bool conjunction = false;
145      [whereDict enumerateKeysAndObjectsUsingBlock: ^(NSString* key, NSNumber* value, BOOL* stop) {
146          if(!conjunction) {
147              [whereClause appendFormat: @" WHERE "];
148          } else {
149              [whereClause appendFormat: @" AND "];
150          }
151  
152          if([value class] == [CKKSSQLWhereValue class]) {
153              CKKSSQLWhereValue* obj = (CKKSSQLWhereValue*)value;
154              [whereClause appendFormat: @"%@%@(?)", key, CKKSSQLWhereComparatorAsString(obj.sqlOp)];
155  
156          } else if([value class] == [CKKSSQLWhereColumn class]) {
157              CKKSSQLWhereColumn* obj = (CKKSSQLWhereColumn*)value;
158              [whereClause appendFormat: @"%@%@%@",
159               key,
160               CKKSSQLWhereComparatorAsString(obj.sqlOp),
161               CKKSSQLWhereColumnNameAsString(obj.columnName)];
162  
163          } else if([value isMemberOfClass:[CKKSSQLWhereIn class]]) {
164              CKKSSQLWhereIn* obj = (CKKSSQLWhereIn*)value;
165  
166              NSMutableArray* q = [NSMutableArray arrayWithCapacity:obj.values.count];
167              for(NSString* value in obj.values) {
168                  [q addObject: @"?"];
169                  (void)value;
170              }
171  
172              NSString* binds = [q componentsJoinedByString:@", "];
173  
174              [whereClause appendFormat:@"%@ IN (%@)", key, binds];
175  
176          } else {
177              [whereClause appendFormat: @"%@=(?)", key];
178          }
179  
180          conjunction = true;
181      }];
182      return whereClause;
183  }
184  
185  + (NSString*) groupByClause: (NSArray*) columns {
186      if(!columns) {
187          return @"";
188      }
189      NSMutableString * groupByClause = [[NSMutableString alloc] init];
190      __block bool conjunction = false;
191      [columns enumerateObjectsUsingBlock: ^(NSString* column, NSUInteger i, BOOL* stop) {
192          if(!conjunction) {
193              [groupByClause appendFormat: @" GROUP BY "];
194          } else {
195              [groupByClause appendFormat: @", "];
196          }
197  
198          [groupByClause appendFormat: @"%@", column];
199  
200          conjunction = true;
201      }];
202      return groupByClause;
203  }
204  
205  + (NSString*)orderByClause: (NSArray*) columns {
206      if(!columns || columns.count == 0u) {
207          return @"";
208      }
209      NSMutableString * orderByClause = [[NSMutableString alloc] init];
210      __block bool conjunction = false;
211      [columns enumerateObjectsUsingBlock: ^(NSString* column, NSUInteger i, BOOL* stop) {
212          if(!conjunction) {
213              [orderByClause appendFormat: @" ORDER BY "];
214          } else {
215              [orderByClause appendFormat: @", "];
216          }
217  
218          [orderByClause appendFormat: @"%@", column];
219  
220          conjunction = true;
221      }];
222      return orderByClause;
223  }
224  
225  + (void)bindWhereClause:(sqlite3_stmt*)stmt whereDict:(NSDictionary*)whereDict cferror:(CFErrorRef*)cferror
226  {
227      __block int whereLocation = 1;
228  
229      [whereDict.allKeys enumerateObjectsUsingBlock:^(id  _Nonnull key, NSUInteger i, BOOL * _Nonnull stop) {
230          if([whereDict[key] class] == [CKKSSQLWhereValue class]) {
231              CKKSSQLWhereValue* obj = (CKKSSQLWhereValue*)whereDict[key];
232              SecDbBindObject(stmt, whereLocation, (__bridge CFStringRef)obj.value, cferror);
233              whereLocation++;
234  
235          } else if([whereDict[key] class] == [CKKSSQLWhereColumn class]) {
236              // skip
237          } else if([whereDict[key] isMemberOfClass:[CKKSSQLWhereIn class]]) {
238              CKKSSQLWhereIn* obj = (CKKSSQLWhereIn*)whereDict[key];
239  
240              for(NSString* value in obj.values) {
241                  SecDbBindObject(stmt, whereLocation, (__bridge CFStringRef)value, cferror);
242                  whereLocation++;
243              }
244  
245          } else {
246              SecDbBindObject(stmt, whereLocation, (__bridge CFStringRef) whereDict[key], cferror);
247              whereLocation++;
248          }
249      }];
250  }
251  
252  + (bool) deleteFromTable: (NSString*) table where: (NSDictionary*) whereDict connection:(SecDbConnectionRef) dbconn error: (NSError * __autoreleasing *) error {
253      __block CFErrorRef cferror = NULL;
254  
255  #if DEBUG
256      NSAssert(CKKSSQLInTransaction, @"Must be in a transaction to perform database writes");
257      NSAssert(CKKSSQLInWriteTransaction, @"Must be in a write transaction to perform database writes");
258  #endif
259  
260      bool (^doWithConnection)(SecDbConnectionRef) = ^bool (SecDbConnectionRef dbconn) {
261          NSString* whereClause = [CKKSSQLDatabaseObject makeWhereClause: whereDict];
262  
263          NSString * sql = [[NSString alloc] initWithFormat: @"DELETE FROM %@%@;", table, whereClause];
264          SecDbPrepare(dbconn, (__bridge CFStringRef) sql, &cferror, ^void (sqlite3_stmt *stmt) {
265              [self bindWhereClause:stmt whereDict:whereDict cferror:&cferror];
266  
267              SecDbStep(dbconn, stmt, &cferror, ^(bool *stop) {
268              });
269          });
270          return true;
271      };
272  
273      if(dbconn) {
274          doWithConnection(dbconn);
275      } else {
276          kc_with_dbt(true, &cferror, doWithConnection);
277      }
278  
279      // Deletes finish in a single step, so if we didn't get an error, the delete 'happened'
280      bool status = (cferror == nil);
281  
282      if(error) {
283          *error = (NSError*) CFBridgingRelease(cferror);
284      } else {
285          CFReleaseNull(cferror);
286      }
287  
288      return status;
289  }
290  
291  + (bool)queryDatabaseTable:(NSString*)table
292                       where:(NSDictionary*)whereDict
293                     columns:(NSArray*)names
294                     groupBy:(NSArray*)groupColumns
295                     orderBy:(NSArray*)orderColumns
296                       limit:(ssize_t)limit
297                  processRow:(void (^)(NSDictionary<NSString*, CKKSSQLResult*>*)) processRow
298                       error:(NSError * __autoreleasing *) error {
299      __block CFErrorRef cferror = NULL;
300  
301      kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbconn) {
302          NSString * columns = [names componentsJoinedByString:@", "];
303          NSString * whereClause = [CKKSSQLDatabaseObject makeWhereClause: whereDict];
304          NSString * groupByClause = [CKKSSQLDatabaseObject groupByClause: groupColumns];
305          NSString * orderByClause = [CKKSSQLDatabaseObject orderByClause: orderColumns];
306          NSString * limitClause = (limit > 0 ? [NSString stringWithFormat:@" LIMIT %lu", limit] : @"");
307  
308          NSString * sql = [[NSString alloc] initWithFormat: @"SELECT %@ FROM %@%@%@%@%@;", columns, table, whereClause, groupByClause, orderByClause, limitClause];
309          SecDbPrepare(dbconn, (__bridge CFStringRef) sql, &cferror, ^void (sqlite3_stmt *stmt) {
310              [self bindWhereClause:stmt whereDict:whereDict cferror:&cferror];
311  
312              SecDbStep(dbconn, stmt, &cferror, ^(bool *stop) {
313                  __block NSMutableDictionary<NSString*, CKKSSQLResult*>* row = [[NSMutableDictionary alloc] init];
314  
315                  [names enumerateObjectsUsingBlock:^(id  _Nonnull name, NSUInteger i, BOOL * _Nonnull stop) {
316                      const char * col = (const char *) sqlite3_column_text(stmt, (int)i);
317                      row[name] = [[CKKSSQLResult alloc] init:col ? [NSString stringWithUTF8String:col] : nil];
318                  }];
319  
320                  processRow(row);
321              });
322          });
323          return true;
324      });
325  
326      bool ret = (cferror == NULL);
327      SecTranslateError(error, cferror);
328      return ret;
329  }
330  
331  + (NSString *)quotedString:(NSString *)string
332  {
333      char *quotedMaxField = sqlite3_mprintf("%q", [string UTF8String]);
334      if (quotedMaxField == NULL) {
335          abort();
336      }
337      NSString *rstring = [NSString stringWithUTF8String:quotedMaxField];
338      sqlite3_free(quotedMaxField);
339      return rstring;
340  }
341  
342  + (bool)queryMaxValueForField:(NSString*)maxField
343                        inTable:(NSString*)table
344                          where:(NSDictionary*)whereDict
345                        columns:(NSArray*)names
346                     processRow:(void (^)(NSDictionary<NSString*, CKKSSQLResult*>*))processRow
347  {
348      __block CFErrorRef cferror = NULL;
349  
350      kc_with_dbt(false, &cferror, ^bool(SecDbConnectionRef dbconn) {
351          NSString *quotedMaxField = [self quotedString:maxField];
352          NSString *quotedTable = [self quotedString:table];
353  
354          NSMutableArray<NSString *>* quotedNames = [NSMutableArray array];
355          for (NSString *element in names) {
356              [quotedNames addObject:[self quotedString:element]];
357          }
358  
359          NSString* columns = [[quotedNames componentsJoinedByString:@", "] stringByAppendingFormat:@", %@", quotedMaxField];
360          NSString* whereClause = [CKKSSQLDatabaseObject makeWhereClause:whereDict];
361          
362          NSString* sql = [[NSString alloc] initWithFormat:@"SELECT %@ FROM %@%@", columns, quotedTable, whereClause];
363          SecDbPrepare(dbconn, (__bridge CFStringRef)sql, &cferror, ^(sqlite3_stmt* stmt) {
364              [self bindWhereClause:stmt whereDict:whereDict cferror:&cferror];
365              
366              SecDbStep(dbconn, stmt, &cferror, ^(bool*stop) {
367                  __block NSMutableDictionary<NSString*, CKKSSQLResult*>* row = [[NSMutableDictionary alloc] init];
368                  
369                  [names enumerateObjectsUsingBlock:^(id  _Nonnull name, NSUInteger i, BOOL * _Nonnull stop) {
370                      const char * col = (const char *) sqlite3_column_text(stmt, (int)i);
371                      row[name] = [[CKKSSQLResult alloc] init:col ? [NSString stringWithUTF8String:col] : nil];
372                  }];
373                  
374                  processRow(row);
375              });
376          });
377          
378          return true;
379      });
380      
381      bool ret = (cferror == NULL);
382      return ret;
383  }
384  
385  + (BOOL)performCKKSTransaction:(CKKSDatabaseTransactionResult (^)(void))block
386  {
387      CFErrorRef cferror = NULL;
388      bool ok = kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbconn) {
389          CFErrorRef cferrorInternal = NULL;
390          bool ret = kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferrorInternal, ^bool{
391              CKKSDatabaseTransactionResult result = CKKSDatabaseTransactionRollback;
392  
393              CKKSSQLInTransaction = true;
394              CKKSSQLInWriteTransaction = true;
395              result = block();
396              CKKSSQLInWriteTransaction = false;
397              CKKSSQLInTransaction = false;
398              return result == CKKSDatabaseTransactionCommit;
399          });
400          if(cferrorInternal) {
401              ckkserror_global("ckkssql",  "error performing database transaction, major problems ahead: %@", cferrorInternal);
402          }
403          CFReleaseNull(cferrorInternal);
404          return ret;
405      });
406  
407      if(cferror) {
408          ckkserror_global("ckkssql",  "error performing database operation, major problems ahead: %@", cferror);
409      }
410      CFReleaseNull(cferror);
411      return ok;
412  }
413  
414  + (BOOL)performCKKSReadonlyTransaction:(void(^)(void))block
415  {
416      CFErrorRef cferror = NULL;
417      bool ok = kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbconn) {
418          CFErrorRef cferrorInternal = NULL;
419          bool ret = kc_transaction_type(dbconn, kSecDbNormalTransactionType, &cferrorInternal, ^bool{
420              CKKSSQLInTransaction = true;
421              block();
422              CKKSSQLInTransaction = false;
423              return true;
424          });
425          if(cferrorInternal) {
426              ckkserror_global("ckkssql",  "error performing database transaction, major problems ahead: %@", cferrorInternal);
427          }
428          CFReleaseNull(cferrorInternal);
429          return ret;
430      });
431  
432      if(cferror) {
433          ckkserror_global("ckkssql",  "error performing database operation, major problems ahead: %@", cferror);
434      }
435      CFReleaseNull(cferror);
436      return ok;
437  }
438  
439  #pragma mark - Instance methods
440  
441  - (bool) saveToDatabase: (NSError * __autoreleasing *) error {
442      return [self saveToDatabaseWithConnection:nil error: error];
443  }
444  
445  - (bool) saveToDatabaseWithConnection: (SecDbConnectionRef) conn error: (NSError * __autoreleasing *) error {
446      // Todo: turn this into a transaction
447  
448      NSDictionary* currentWhereClause = [self whereClauseToFindSelf];
449  
450      // First, if we were loaded from the database and the where clause has changed, delete the old record.
451      if(self.originalSelfWhereClause && ![self.originalSelfWhereClause isEqualToDictionary: currentWhereClause]) {
452          secdebug("ckkssql", "Primary key changed; removing old row at %@", self.originalSelfWhereClause);
453          if(![CKKSSQLDatabaseObject deleteFromTable:[[self class] sqlTable] where: self.originalSelfWhereClause connection:conn error: error]) {
454              return false;
455          }
456      }
457  
458      bool ok = [CKKSSQLDatabaseObject saveToDatabaseTable: [[self class] sqlTable]
459                                                       row: [self sqlValues]
460                                                connection: conn
461                                                     error: error];
462  
463      if(ok) {
464          secdebug("ckkssql", "Saved %@", self);
465      } else {
466          secdebug("ckkssql", "Couldn't save %@: %@", self, error ? *error : @"unknown");
467      }
468      return ok;
469  }
470  
471  - (bool) deleteFromDatabase: (NSError * __autoreleasing *) error {
472      bool ok = [CKKSSQLDatabaseObject deleteFromTable:[[self class] sqlTable] where: [self whereClauseToFindSelf] connection:nil error: error];
473  
474      if(ok) {
475          secdebug("ckkssql", "Deleted %@", self);
476      } else {
477          secdebug("ckkssql", "Couldn't delete %@: %@", self, error ? *error : @"unknown");
478      }
479      return ok;
480  }
481  
482  + (bool) deleteAll: (NSError * __autoreleasing *) error {
483      bool ok = [CKKSSQLDatabaseObject deleteFromTable:[self sqlTable] where: nil connection:nil error: error];
484  
485      if(ok) {
486          secdebug("ckkssql", "Deleted all %@", self);
487      } else {
488          secdebug("ckkssql", "Couldn't delete all %@: %@", self, error ? *error : @"unknown");
489      }
490      return ok;
491  }
492  
493  + (instancetype) fromDatabaseWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error {
494      id ret = [self tryFromDatabaseWhere: whereDict error:error];
495  
496      if(!ret && error) {
497          *error = [NSError errorWithDomain:@"securityd"
498                                       code:errSecItemNotFound
499                                   userInfo:@{NSLocalizedDescriptionKey:
500                                                  [NSString stringWithFormat: @"%@ does not exist in database where %@", [self class], whereDict]}];
501      }
502  
503      return ret;
504  }
505  
506  + (instancetype _Nullable) tryFromDatabaseWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error {
507      __block id ret = nil;
508  
509      [CKKSSQLDatabaseObject queryDatabaseTable: [self sqlTable]
510                                          where: whereDict
511                                        columns: [self sqlColumns]
512                                        groupBy: nil
513                                        orderBy:nil
514                                          limit: -1
515                                     processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
516                                     ret = [[self fromDatabaseRow: row] memoizeOriginalSelfWhereClause];
517                                 }
518                                          error: error];
519  
520      return ret;
521  }
522  
523  + (NSArray*) all: (NSError * __autoreleasing *) error {
524      return [self allWhere: nil error:error];
525  }
526  
527  + (NSArray*) allWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error {
528      __block NSMutableArray* items = [[NSMutableArray alloc] init];
529  
530      [CKKSSQLDatabaseObject queryDatabaseTable: [self sqlTable]
531                                          where: whereDict
532                                        columns: [self sqlColumns]
533                                        groupBy: nil
534                                        orderBy:nil
535                                          limit: -1
536                                     processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
537                                         [items addObject: [[self fromDatabaseRow: row] memoizeOriginalSelfWhereClause]];
538                                     }
539                                          error: error];
540  
541      return items;
542  }
543  
544  + (NSArray*)fetch: (size_t)count error: (NSError * __autoreleasing *) error {
545      return [self fetch: count where:nil orderBy:nil error:error];
546  }
547  
548  + (NSArray*)fetch: (size_t)count where:(NSDictionary*)whereDict error: (NSError * __autoreleasing *) error {
549      return [self fetch: count where:whereDict orderBy:nil error:error];
550  }
551  
552  + (NSArray*)fetch:(size_t)count
553              where:(NSDictionary*)whereDict
554            orderBy:(NSArray*) orderColumns
555              error:(NSError * __autoreleasing *) error {
556      __block NSMutableArray* items = [[NSMutableArray alloc] init];
557  
558      [CKKSSQLDatabaseObject queryDatabaseTable: [self sqlTable]
559                                          where: whereDict
560                                        columns: [self sqlColumns]
561                                        groupBy:nil
562                                        orderBy:orderColumns
563                                          limit: (ssize_t) count
564                                     processRow: ^(NSDictionary<NSString*, CKKSSQLResult*>* row) {
565                                         [items addObject: [[self fromDatabaseRow: row] memoizeOriginalSelfWhereClause]];
566                                     }
567                                          error: error];
568  
569      return items;
570  }
571  
572  - (instancetype) memoizeOriginalSelfWhereClause {
573      _originalSelfWhereClause = [self whereClauseToFindSelf];
574      return self;
575  }
576  
577  #pragma mark - Subclass methods
578  
579  + (instancetype)fromDatabaseRow:(NSDictionary<NSString *, CKKSSQLResult*>*)row {
580      @throw [NSException exceptionWithName:NSInternalInconsistencyException
581                                     reason:[NSString stringWithFormat:@"A subclass must override %@", NSStringFromSelector(_cmd)]
582                                   userInfo:nil];
583  }
584  
585  + (NSString*) sqlTable {
586      @throw [NSException exceptionWithName:NSInternalInconsistencyException
587                                     reason:[NSString stringWithFormat:@"A subclass must override %@", NSStringFromSelector(_cmd)]
588                                   userInfo:nil];
589  }
590  
591  + (NSArray<NSString*>*) sqlColumns {
592      @throw [NSException exceptionWithName:NSInternalInconsistencyException
593                                     reason:[NSString stringWithFormat:@"A subclass must override %@", NSStringFromSelector(_cmd)]
594                                   userInfo:nil];
595  }
596  
597  - (NSDictionary<NSString*,NSString*>*) sqlValues {
598      @throw [NSException exceptionWithName:NSInternalInconsistencyException
599                                     reason:[NSString stringWithFormat:@"A subclass must override %@", NSStringFromSelector(_cmd)]
600                                   userInfo:nil];
601  }
602  
603  - (NSDictionary<NSString*,NSString*>*) whereClauseToFindSelf {
604      @throw [NSException exceptionWithName:NSInternalInconsistencyException
605                                     reason:[NSString stringWithFormat:@"A subclass must override %@", NSStringFromSelector(_cmd)]
606                                   userInfo:nil];
607  }
608  
609  - (instancetype)copyWithZone:(NSZone *)zone {
610      CKKSSQLDatabaseObject *dbCopy = [[[self class] allocWithZone:zone] init];
611      dbCopy->_originalSelfWhereClause = _originalSelfWhereClause;
612      return dbCopy;
613  }
614  @end
615  
616  NSString* CKKSSQLWhereComparatorAsString(CKKSSQLWhereComparator comparator)
617  {
618      switch(comparator) {
619          case CKKSSQLWhereComparatorEquals:
620              return @"=";
621          case CKKSSQLWhereComparatorNotEquals:
622              return @"<>";
623          case CKKSSQLWhereComparatorGreaterThan:
624              return @">";
625          case CKKSSQLWhereComparatorLessThan:
626              return @"<";
627      }
628  }
629  
630  NSString* CKKSSQLWhereColumnNameAsString(CKKSSQLWhereColumnName columnName)
631  {
632      switch(columnName) {
633          case CKKSSQLWhereColumnNameUUID:
634              return @"uuid";
635          case CKKSSQLWhereColumnNameParentKeyUUID:
636              return @"parentKeyUUID";
637      }
638  }
639  
640  #pragma mark - CKKSSQLWhereColumn
641  
642  @implementation CKKSSQLWhereColumn
643  - (instancetype)initWithOperation:(CKKSSQLWhereComparator)op columnName:(CKKSSQLWhereColumnName)column
644  {
645      if((self = [super init])) {
646          _sqlOp = op;
647          _columnName = column;
648      }
649      return self;
650  }
651  + (instancetype)op:(CKKSSQLWhereComparator)op column:(CKKSSQLWhereColumnName)columnName
652  {
653      return [[CKKSSQLWhereColumn alloc] initWithOperation:op columnName:columnName];
654  }
655  @end
656  
657  #pragma mark - CKKSSQLWhereObject
658  
659  @implementation CKKSSQLWhereValue
660  - (instancetype)initWithOperation:(CKKSSQLWhereComparator)op value:(NSString*)value
661  {
662      if((self = [super init])) {
663          _sqlOp = op;
664          _value = value;
665      }
666      return self;
667  }
668  + (instancetype)op:(CKKSSQLWhereComparator)op value:(NSString*)value
669  {
670      return [[CKKSSQLWhereValue alloc] initWithOperation:op value:value];
671  
672  }
673  @end
674  
675  #pragma mark - CKKSSQLWhereIn
676  
677  @implementation CKKSSQLWhereIn : NSObject
678  - (instancetype)initWithValues:(NSArray<NSString*>*)values
679  {
680      if((self = [super init])) {
681           _values = values;
682      }
683      return self;
684  }
685  @end