From b3542ad1f7ae7513c0ac4bb5a06f7e0c364989b5 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 27 Jan 2024 14:54:00 +0100 Subject: [PATCH] [sqflite_darwin] Share iOS and MacOS implementation in a proper way (https://docs.flutter.dev/packages-and-plugins/developing-packages#shared-ios-and-macos-implementations) instead of copying implementation --- sqflite/{ios => darwin}/.gitignore | 0 .../darwin/Assets/.gitkeep | 0 .../{ios => darwin}/Classes/SqfliteCursor.h | 0 .../{ios => darwin}/Classes/SqfliteCursor.m | 0 .../{ios => darwin}/Classes/SqfliteDatabase.h | 0 .../{ios => darwin}/Classes/SqfliteDatabase.m | 0 .../Classes/SqfliteFmdbImport.m | 0 .../{ios => darwin}/Classes/SqfliteImport.h | 0 .../Classes/SqfliteOperation.h | 0 .../Classes/SqfliteOperation.m | 0 .../{ios => darwin}/Classes/SqflitePlugin.h | 0 .../{ios => darwin}/Classes/SqflitePlugin.m | 0 sqflite/darwin/FMDB_LICENSE.txt | 28 + .../darwin/Resources/PrivacyInfo.xcprivacy | 0 sqflite/{ios => darwin}/sqflite.podspec | 10 +- sqflite/ios/Assets/.gitkeep | 0 sqflite/ios/Resources/PrivacyInfo.xcprivacy | 14 - sqflite/macos/Classes/SqfliteCursor.h | 20 - sqflite/macos/Classes/SqfliteCursor.m | 10 - sqflite/macos/Classes/SqfliteDatabase.h | 41 - sqflite/macos/Classes/SqfliteDatabase.m | 428 --------- sqflite/macos/Classes/SqfliteFmdbImport.m | 18 - sqflite/macos/Classes/SqfliteImport.h | 16 - sqflite/macos/Classes/SqfliteOperation.h | 62 -- sqflite/macos/Classes/SqfliteOperation.m | 193 ---- sqflite/macos/Classes/SqflitePlugin.h | 51 - sqflite/macos/Classes/SqflitePlugin.m | 877 ------------------ sqflite/macos/Resources/PrivacyInfo.xcprivacy | 14 - sqflite/macos/sqflite.podspec | 24 - sqflite/pubspec.yaml | 6 +- 30 files changed, 38 insertions(+), 1774 deletions(-) rename sqflite/{ios => darwin}/.gitignore (100%) rename {packages_flutter/sqflite_darwin => sqflite}/darwin/Assets/.gitkeep (100%) rename sqflite/{ios => darwin}/Classes/SqfliteCursor.h (100%) rename sqflite/{ios => darwin}/Classes/SqfliteCursor.m (100%) rename sqflite/{ios => darwin}/Classes/SqfliteDatabase.h (100%) rename sqflite/{ios => darwin}/Classes/SqfliteDatabase.m (100%) rename sqflite/{ios => darwin}/Classes/SqfliteFmdbImport.m (100%) rename sqflite/{ios => darwin}/Classes/SqfliteImport.h (100%) rename sqflite/{ios => darwin}/Classes/SqfliteOperation.h (100%) rename sqflite/{ios => darwin}/Classes/SqfliteOperation.m (100%) rename sqflite/{ios => darwin}/Classes/SqflitePlugin.h (100%) rename sqflite/{ios => darwin}/Classes/SqflitePlugin.m (100%) create mode 100644 sqflite/darwin/FMDB_LICENSE.txt rename {packages_flutter/sqflite_darwin => sqflite}/darwin/Resources/PrivacyInfo.xcprivacy (100%) rename sqflite/{ios => darwin}/sqflite.podspec (76%) delete mode 100644 sqflite/ios/Assets/.gitkeep delete mode 100644 sqflite/ios/Resources/PrivacyInfo.xcprivacy delete mode 100644 sqflite/macos/Classes/SqfliteCursor.h delete mode 100644 sqflite/macos/Classes/SqfliteCursor.m delete mode 100644 sqflite/macos/Classes/SqfliteDatabase.h delete mode 100644 sqflite/macos/Classes/SqfliteDatabase.m delete mode 100644 sqflite/macos/Classes/SqfliteFmdbImport.m delete mode 100644 sqflite/macos/Classes/SqfliteImport.h delete mode 100644 sqflite/macos/Classes/SqfliteOperation.h delete mode 100644 sqflite/macos/Classes/SqfliteOperation.m delete mode 100644 sqflite/macos/Classes/SqflitePlugin.h delete mode 100644 sqflite/macos/Classes/SqflitePlugin.m delete mode 100644 sqflite/macos/Resources/PrivacyInfo.xcprivacy delete mode 100644 sqflite/macos/sqflite.podspec diff --git a/sqflite/ios/.gitignore b/sqflite/darwin/.gitignore similarity index 100% rename from sqflite/ios/.gitignore rename to sqflite/darwin/.gitignore diff --git a/packages_flutter/sqflite_darwin/darwin/Assets/.gitkeep b/sqflite/darwin/Assets/.gitkeep similarity index 100% rename from packages_flutter/sqflite_darwin/darwin/Assets/.gitkeep rename to sqflite/darwin/Assets/.gitkeep diff --git a/sqflite/ios/Classes/SqfliteCursor.h b/sqflite/darwin/Classes/SqfliteCursor.h similarity index 100% rename from sqflite/ios/Classes/SqfliteCursor.h rename to sqflite/darwin/Classes/SqfliteCursor.h diff --git a/sqflite/ios/Classes/SqfliteCursor.m b/sqflite/darwin/Classes/SqfliteCursor.m similarity index 100% rename from sqflite/ios/Classes/SqfliteCursor.m rename to sqflite/darwin/Classes/SqfliteCursor.m diff --git a/sqflite/ios/Classes/SqfliteDatabase.h b/sqflite/darwin/Classes/SqfliteDatabase.h similarity index 100% rename from sqflite/ios/Classes/SqfliteDatabase.h rename to sqflite/darwin/Classes/SqfliteDatabase.h diff --git a/sqflite/ios/Classes/SqfliteDatabase.m b/sqflite/darwin/Classes/SqfliteDatabase.m similarity index 100% rename from sqflite/ios/Classes/SqfliteDatabase.m rename to sqflite/darwin/Classes/SqfliteDatabase.m diff --git a/sqflite/ios/Classes/SqfliteFmdbImport.m b/sqflite/darwin/Classes/SqfliteFmdbImport.m similarity index 100% rename from sqflite/ios/Classes/SqfliteFmdbImport.m rename to sqflite/darwin/Classes/SqfliteFmdbImport.m diff --git a/sqflite/ios/Classes/SqfliteImport.h b/sqflite/darwin/Classes/SqfliteImport.h similarity index 100% rename from sqflite/ios/Classes/SqfliteImport.h rename to sqflite/darwin/Classes/SqfliteImport.h diff --git a/sqflite/ios/Classes/SqfliteOperation.h b/sqflite/darwin/Classes/SqfliteOperation.h similarity index 100% rename from sqflite/ios/Classes/SqfliteOperation.h rename to sqflite/darwin/Classes/SqfliteOperation.h diff --git a/sqflite/ios/Classes/SqfliteOperation.m b/sqflite/darwin/Classes/SqfliteOperation.m similarity index 100% rename from sqflite/ios/Classes/SqfliteOperation.m rename to sqflite/darwin/Classes/SqfliteOperation.m diff --git a/sqflite/ios/Classes/SqflitePlugin.h b/sqflite/darwin/Classes/SqflitePlugin.h similarity index 100% rename from sqflite/ios/Classes/SqflitePlugin.h rename to sqflite/darwin/Classes/SqflitePlugin.h diff --git a/sqflite/ios/Classes/SqflitePlugin.m b/sqflite/darwin/Classes/SqflitePlugin.m similarity index 100% rename from sqflite/ios/Classes/SqflitePlugin.m rename to sqflite/darwin/Classes/SqflitePlugin.m diff --git a/sqflite/darwin/FMDB_LICENSE.txt b/sqflite/darwin/FMDB_LICENSE.txt new file mode 100644 index 00000000..addfc1ad --- /dev/null +++ b/sqflite/darwin/FMDB_LICENSE.txt @@ -0,0 +1,28 @@ +If you are using FMDB in your project, I'd love to hear about it. Let Gus know +by sending an email to gus@flyingmeat.com. + +And if you happen to come across either Gus Mueller or Rob Ryan in a bar, you +might consider purchasing a drink of their choosing if FMDB has been useful to +you. + +Finally, and shortly, this is the MIT License. + +Copyright (c) 2008-2014 Flying Meat Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/packages_flutter/sqflite_darwin/darwin/Resources/PrivacyInfo.xcprivacy b/sqflite/darwin/Resources/PrivacyInfo.xcprivacy similarity index 100% rename from packages_flutter/sqflite_darwin/darwin/Resources/PrivacyInfo.xcprivacy rename to sqflite/darwin/Resources/PrivacyInfo.xcprivacy diff --git a/sqflite/ios/sqflite.podspec b/sqflite/darwin/sqflite.podspec similarity index 76% rename from sqflite/ios/sqflite.podspec rename to sqflite/darwin/sqflite.podspec index e976c8fe..0c903026 100644 --- a/sqflite/ios/sqflite.podspec +++ b/sqflite/darwin/sqflite.podspec @@ -14,12 +14,12 @@ Access SQLite database. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' s.dependency 'FMDB', '>= 2.7.5' - - s.platform = :ios, '11.0' - s.ios.deployment_target = '11.0' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '12.0' + s.osx.deployment_target = '10.14' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.resource_bundles = {'sqflite_ios_privacy' => ['Resources/PrivacyInfo.xcprivacy']} + s.resource_bundles = {'sqflite_darwin_privacy' => ['Resources/PrivacyInfo.xcprivacy']} end diff --git a/sqflite/ios/Assets/.gitkeep b/sqflite/ios/Assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/sqflite/ios/Resources/PrivacyInfo.xcprivacy b/sqflite/ios/Resources/PrivacyInfo.xcprivacy deleted file mode 100644 index 0eca193e..00000000 --- a/sqflite/ios/Resources/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - \ No newline at end of file diff --git a/sqflite/macos/Classes/SqfliteCursor.h b/sqflite/macos/Classes/SqfliteCursor.h deleted file mode 100644 index a824df92..00000000 --- a/sqflite/macos/Classes/SqfliteCursor.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// SqfliteCursor.h -// sqflite -// -// Created by Alexandre Roux on 24/10/2022. -// -#ifndef SqfliteCursor_h -#define SqfliteCursor_h - -// Cursor information -@class FMResultSet; -@interface SqfliteCursor : NSObject - -@property (atomic, retain) NSNumber* cursorId; -@property (atomic, retain) NSNumber* pageSize; -@property (atomic, retain) FMResultSet *resultSet; - -@end - -#endif // SqfliteCursor_h diff --git a/sqflite/macos/Classes/SqfliteCursor.m b/sqflite/macos/Classes/SqfliteCursor.m deleted file mode 100644 index 59a879dd..00000000 --- a/sqflite/macos/Classes/SqfliteCursor.m +++ /dev/null @@ -1,10 +0,0 @@ -#import "SqfliteCursor.h" -#import "SqfliteFmdbImport.m" - -@implementation SqfliteCursor - -@synthesize cursorId; -@synthesize pageSize; -@synthesize resultSet; - -@end diff --git a/sqflite/macos/Classes/SqfliteDatabase.h b/sqflite/macos/Classes/SqfliteDatabase.h deleted file mode 100644 index 9c0cdcd6..00000000 --- a/sqflite/macos/Classes/SqfliteDatabase.h +++ /dev/null @@ -1,41 +0,0 @@ -// -// SqfliteDatabase.h -// sqflite -// -// Created by Alexandre Roux on 24/10/2022. -// -#ifndef SqfliteDatabase_h -#define SqfliteDatabase_h - -#import "SqfliteCursor.h" -#import "SqfliteOperation.h" - -@class FMDatabaseQueue,FMDatabase; -@interface SqfliteDatabase : NSObject - -@property (atomic, retain) FMDatabaseQueue *fmDatabaseQueue; -@property (atomic, retain) NSNumber *databaseId; -@property (atomic, retain) NSString* path; -@property (nonatomic) bool singleInstance; -@property (nonatomic) bool inTransaction; -@property (nonatomic) int logLevel; -// Curosr support -@property (nonatomic) int lastCursorId; -@property (atomic, retain) NSMutableDictionary* cursorMap; -// Transaction v2 -@property (nonatomic) int lastTransactionId; -@property (atomic, retain) NSNumber *currentTransactionId; -@property (atomic, retain) NSMutableArray* noTransactionOperationQueue; - -- (void)closeCursorById:(NSNumber*)cursorId; -- (void)closeCursor:(SqfliteCursor*)cursor; -- (void)inDatabase:(void (^)(FMDatabase *db))block; -- (void)dbBatch:(FMDatabase*)db operation:(SqfliteMethodCallOperation*)mainOperation; -- (void)dbExecute:(FMDatabase*)db operation:(SqfliteOperation*)operation; -- (void)dbInsert:(FMDatabase*)db operation:(SqfliteOperation*)operation; -- (void)dbUpdate:(FMDatabase*)db operation:(SqfliteOperation*)operation; -- (void)dbQuery:(FMDatabase*)db operation:(SqfliteOperation*)operation; -- (void)dbQueryCursorNext:(FMDatabase*)db operation:(SqfliteOperation*)operation; -@end - -#endif // SqfliteDatabase_h diff --git a/sqflite/macos/Classes/SqfliteDatabase.m b/sqflite/macos/Classes/SqfliteDatabase.m deleted file mode 100644 index 772e9e6e..00000000 --- a/sqflite/macos/Classes/SqfliteDatabase.m +++ /dev/null @@ -1,428 +0,0 @@ -#import "SqfliteDatabase.h" -#import "SqflitePlugin.h" -#import "SqfliteFmdbImport.m" - -#import - -// iOS workaround bug #214 -static NSString *const SqfliteSqlPragmaSqliteDefensiveOff = @"PRAGMA sqflite -- db_config_defensive_off"; - -static NSString *const _paramCursorPageSize = @"cursorPageSize"; -static NSString *const _paramCursorId = @"cursorId"; -static NSString *const _paramCancel = @"cancel"; -// For batch -static NSString *const _paramOperations = @"operations"; - -static int transactionIdForce = -1; - -// Import hidden method -@interface FMDatabase () -- (void)resultSetDidClose:(FMResultSet *)resultSet; -@end - -@implementation SqfliteDatabase - -@synthesize databaseId, fmDatabaseQueue, cursorMap, logLevel, currentTransactionId, noTransactionOperationQueue, lastCursorId,lastTransactionId; - - -- (instancetype)init { - self = [super init]; - if (self) { - cursorMap = [NSMutableDictionary new]; - lastCursorId = 0; - lastTransactionId = 0; - noTransactionOperationQueue = [NSMutableArray new]; - } - return self; -} - - -- (void)inDatabase:(void (^)(FMDatabase *db))block { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self.fmDatabaseQueue inDatabase:block]; - }); -} - -- (void)dbHandleError:(FMDatabase*)db result:(FlutterResult)result { - // handle error - result([FlutterError errorWithCode:SqliteErrorCode - message:[NSString stringWithFormat:@"%@", [db lastError]] - details:nil]); -} - -- (void)dbHandleError:(FMDatabase*)db operation:(SqfliteOperation*)operation { - NSMutableDictionary* details = nil; - NSString* sql = [operation getSql]; - if (sql != nil) { - details = [NSMutableDictionary new]; - [details setObject:sql forKey:SqfliteParamSql]; - NSArray* sqlArguments = [operation getSqlArguments]; - if (sqlArguments != nil) { - [details setObject:sqlArguments forKey:SqfliteParamSqlArguments]; - } - } - - [operation error:([FlutterError errorWithCode:SqliteErrorCode - message:[NSString stringWithFormat:@"%@", [db lastError]] - details:details])]; - -} - -- (void)dbRunQueuedOperations:(FMDatabase*)db { - while (![SqflitePlugin arrayIsEmpy:noTransactionOperationQueue]) { - if (currentTransactionId != nil) { - break; - } - SqfliteQueuedOperation* queuedOperation = [noTransactionOperationQueue objectAtIndex:0]; - [noTransactionOperationQueue removeObjectAtIndex:0]; - queuedOperation.handler(db, queuedOperation.operation); - } -} - -- (void)wrapSqlOperationHandler:(FMDatabase*)db operation:(SqfliteOperation*)operation handler:(SqfliteOperationHandler)handler { - NSNumber* transactionId = [operation getTransactionId]; - if (currentTransactionId == nil) { - // ignore - handler(db, operation); - } else if (transactionId != nil && (transactionId.intValue == currentTransactionId.intValue || transactionId.intValue == transactionIdForce)) { - handler(db, operation); - if (currentTransactionId == nil && ![SqflitePlugin arrayIsEmpy:noTransactionOperationQueue]) { - [self dbRunQueuedOperations:db]; - } - } else { - // Queue for later - SqfliteQueuedOperation* queuedOperation = [SqfliteQueuedOperation new]; - queuedOperation.operation = operation; - queuedOperation.handler = handler; - [noTransactionOperationQueue addObject:queuedOperation]; - - } -} -- (bool)dbDoExecute:(FMDatabase*)db operation:(SqfliteOperation*)operation { - if (![self dbExecuteOrError:db operation:operation]) { - return false; - } - [operation success:[NSNull null]]; - return true; -} - -- (void)dbExecute:(FMDatabase*)db operation:(SqfliteOperation*)operation { - [self wrapSqlOperationHandler:db operation:operation handler:^(FMDatabase* db, SqfliteOperation* operation) { - NSNumber* inTransactionChange = [operation getInTransactionChange]; - bool hasNullTransactionId = [operation hasNullTransactionId]; - bool enteringTransaction = [inTransactionChange boolValue] == true && hasNullTransactionId; - - if (enteringTransaction) { - self.currentTransactionId = [NSNumber numberWithInt:++self.lastTransactionId]; - } - if ([self dbExecuteOrError:db operation:operation]) { - if (enteringTransaction) { - NSMutableDictionary* result = [NSMutableDictionary new]; - [result setObject:self.currentTransactionId forKey:SqfliteParamTransactionId]; - [operation success:result]; - } else { - bool leavingTransaction = inTransactionChange != nil && [inTransactionChange boolValue] == false; - if (leavingTransaction) { - self.currentTransactionId = nil; - } - [operation success:[NSNull null]]; - } - } else { - if (enteringTransaction) { - // On error revert change - self.currentTransactionId = nil; - } - } - }]; -} - -- (bool)dbExecuteOrError:(FMDatabase*)db operation:(SqfliteOperation*)operation { - NSString* sql = [operation getSql]; - NSArray* sqlArguments = [operation getSqlArguments]; - NSNumber* inTransaction = [operation getInTransactionChange]; - - // Handle Hardcoded workarounds - // Handle issue #525 - if ([SqfliteSqlPragmaSqliteDefensiveOff isEqualToString:sql]) { - sqlite3_db_config(db.sqliteHandle, SQLITE_DBCONFIG_DEFENSIVE, 0, 0); - } - - BOOL argumentsEmpty = [SqflitePlugin arrayIsEmpy:sqlArguments]; - if (sqfliteHasSqlLogLevel(logLevel)) { - NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments); - } - - BOOL success; - if (!argumentsEmpty) { - success = [db executeUpdate: sql withArgumentsInArray: sqlArguments]; - } else { - success = [db executeUpdate: sql]; - } - - // If wanted, we leave the transaction even if it fails - if (inTransaction != nil) { - if (![inTransaction boolValue]) { - self.inTransaction = false; - } - } - - // handle error - if (!success) { - [self dbHandleError:db operation:operation]; - return false; - } - - // We enter the transaction on success - if (inTransaction != nil) { - if ([inTransaction boolValue]) { - self.inTransaction = true; - } - } - - return true; -} - - -// -// insert -// -- (void)dbInsert:(FMDatabase*)db operation:(SqfliteOperation*)operation { - [self wrapSqlOperationHandler:db operation:operation handler:^(FMDatabase* db, SqfliteOperation* operation) { - [self dbDoInsert:db operation:operation]; - }]; -} -- (bool)dbDoInsert:(FMDatabase*)db operation:(SqfliteOperation*)operation { - if (![self dbExecuteOrError:db operation:operation]) { - return false; - } - if ([operation getNoResult]) { - [operation success:[NSNull null]]; - return true; - } - // handle ON CONFLICT IGNORE (issue #164) by checking the number of changes - // before - int changes = [db changes]; - if (changes == 0) { - if (sqfliteHasSqlLogLevel(self.logLevel)) { - NSLog(@"no changes"); - } - [operation success:[NSNull null]]; - return true; - } - sqlite_int64 insertedId = [db lastInsertRowId]; - if (sqfliteHasSqlLogLevel(self.logLevel)) { - NSLog(@"inserted %@", @(insertedId)); - } - [operation success:(@(insertedId))]; - return true; -} - -- (void)dbUpdate:(FMDatabase*)db operation:(SqfliteOperation*)operation { - [self wrapSqlOperationHandler:db operation:operation handler:^(FMDatabase* db, SqfliteOperation* operation) { - [self dbDoUpdate:db operation:operation]; - }]; -} -- (bool)dbDoUpdate:(FMDatabase*)db operation:(SqfliteOperation*)operation { - if (![self dbExecuteOrError:db operation:operation]) { - return false; - } - if ([operation getNoResult]) { - [operation success:[NSNull null]]; - return true; - } - int changes = [db changes]; - if (sqfliteHasSqlLogLevel(self.logLevel)) { - NSLog(@"changed %@", @(changes)); - } - [operation success:(@(changes))]; - return true; -} - -// -// query -// -- (void)dbQuery:(FMDatabase*)db operation:(SqfliteOperation*)operation { - [self wrapSqlOperationHandler:db operation:operation handler:^(FMDatabase* db, SqfliteOperation* operation) { - [self dbDoQuery:db operation:operation]; - }]; -} - -- (bool)dbDoQuery:(FMDatabase*)db operation:(SqfliteOperation*)operation { - NSString* sql = [operation getSql]; - NSArray* sqlArguments = [operation getSqlArguments]; - bool argumentsEmpty = [SqflitePlugin arrayIsEmpy:sqlArguments]; - // Non null means use a cursor - NSNumber* cursorPageSize = [operation getArgument:_paramCursorPageSize]; - - if (sqfliteHasSqlLogLevel(self.logLevel)) { - NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments); - } - - FMResultSet *resultSet; - if (!argumentsEmpty) { - resultSet = [db executeQuery:sql withArgumentsInArray:sqlArguments]; - } else { - // rs = [db executeQuery:sql]; - // This crashes on MacOS if there is any ? in the query - // Workaround using an empty array - resultSet = [db executeQuery:sql withArgumentsInArray:@[]]; - } - - // handle error - if ([db hadError]) { - [self dbHandleError:db operation:operation]; - return false; - } - - NSMutableDictionary* results = [SqflitePlugin resultSetToResults:resultSet cursorPageSize:cursorPageSize]; - - if (cursorPageSize != nil) { - bool cursorHasMoreData = [resultSet hasAnotherRow]; - if (cursorHasMoreData) { - NSNumber* cursorId = [NSNumber numberWithInt:++self.lastCursorId]; - SqfliteCursor* cursor = [SqfliteCursor new]; - cursor.cursorId = cursorId; - cursor.pageSize = cursorPageSize; - cursor.resultSet = resultSet; - self.cursorMap[cursorId] = cursor; - // Notify cursor support in the result - results[_paramCursorId] = cursorId; - // Prevent FMDB warning, we keep a result set open on purpose - [db resultSetDidClose:resultSet]; - } - } - [operation success:results]; - return true; -} - - - - -// -// query -// - -- (void)dbQueryCursorNext:(FMDatabase*)db operation:(SqfliteOperation*)operation { - - NSNumber* cursorId = [operation getArgument:_paramCursorId]; - NSNumber* cancelValue = [operation getArgument:_paramCancel]; - bool cancel = [cancelValue boolValue] == true; - if (sqfliteHasVerboseLogLevel(self.logLevel)) - { NSLog(@"queryCursorNext %@%s", cursorId, cancel ? " (cancel)" : ""); - } - - if (cancel) { - [self closeCursorById:cursorId]; - [operation success:[NSNull null]]; - return; - } else { - SqfliteCursor* cursor = self.cursorMap[cursorId]; - if (cursor == nil) { - NSLog(@"cursor %@ not found.", cursorId); - [operation success:[FlutterError errorWithCode:SqliteErrorCode - message: @"Cursor not found" - details:nil]]; - return; - } - FMResultSet* resultSet = cursor.resultSet; - NSMutableDictionary* results = [SqflitePlugin resultSetToResults:resultSet cursorPageSize:cursor.pageSize]; - - bool cursorHasMoreData = [resultSet hasAnotherRow]; - if (cursorHasMoreData) { - // Keep the cursorId to specify that we have more data. - results[_paramCursorId] = cursorId; - // Prevent FMDB warning, we keep a result set open on purpose - [db resultSetDidClose:resultSet]; - } else { - [self closeCursor:cursor]; - } - [operation success:results]; - - - } -} - - -- (void)dbBatch:(FMDatabase*)db operation:(SqfliteMethodCallOperation*)mainOperation { - - bool noResult = [mainOperation getNoResult]; - bool continueOnError = [mainOperation getContinueOnError]; - - NSArray* operations = [mainOperation getArgument:_paramOperations]; - NSMutableArray* operationResults = [NSMutableArray new]; - for (NSDictionary* dictionary in operations) { - // do something with object - - SqfliteBatchOperation* operation = [SqfliteBatchOperation new]; - operation.dictionary = dictionary; - operation.noResult = noResult; - - NSString* method = [operation getMethod]; - if ([SqfliteMethodInsert isEqualToString:method]) { - if ([self dbDoInsert:db operation:operation]) { - [operation handleSuccess:operationResults]; - } else if (continueOnError) { - [operation handleErrorContinue:operationResults]; - } else { - [operation handleError:mainOperation.flutterResult]; - return; - } - } else if ([SqfliteMethodUpdate isEqualToString:method]) { - if ([self dbDoUpdate:db operation:operation]) { - [operation handleSuccess:operationResults]; - } else if (continueOnError) { - [operation handleErrorContinue:operationResults]; - } else { - [operation handleError:mainOperation.flutterResult]; - return; - } - } else if ([SqfliteMethodExecute isEqualToString:method]) { - if ([self dbDoExecute:db operation:operation]) { - [operation handleSuccess:operationResults]; - } else if (continueOnError) { - [operation handleErrorContinue:operationResults]; - } else { - [operation handleError:mainOperation.flutterResult]; - return; - } - } else if ([SqfliteMethodQuery isEqualToString:method]) { - if ([self dbDoQuery:db operation:operation]) { - [operation handleSuccess:operationResults]; - } else if (continueOnError) { - [operation handleErrorContinue:operationResults]; - } else { - [operation handleError:mainOperation.flutterResult]; - return; - } - } else { - [mainOperation success:[FlutterError errorWithCode:SqfliteErrorBadParam - message:[NSString stringWithFormat:@"Batch method '%@' not supported", method] - details:nil]]; - return; - } - } - - if (noResult) { - [mainOperation success:[NSNull null]]; - } else { - [mainOperation success:operationResults]; - } - -} -- (void)closeCursorById:(NSNumber*)cursorId { - SqfliteCursor* cursor = cursorMap[cursorId]; - if (cursor != nil) { - [self closeCursor:cursor]; - } -} - -- (void)closeCursor:(SqfliteCursor*)cursor { - NSNumber* cursorId = cursor.cursorId; - if (sqfliteHasVerboseLogLevel(logLevel)) { - NSLog(@"closing cursor %@", cursorId); - } - [cursorMap removeObjectForKey:cursorId]; - [cursor.resultSet close]; -} - -@end diff --git a/sqflite/macos/Classes/SqfliteFmdbImport.m b/sqflite/macos/Classes/SqfliteFmdbImport.m deleted file mode 100644 index e2bab053..00000000 --- a/sqflite/macos/Classes/SqfliteFmdbImport.m +++ /dev/null @@ -1,18 +0,0 @@ -// -// SqfliteFmdbImport.m -// Shared import for FMDB -// -// Not a header file as XCode might complain. -// -// Created by Alexandre Roux on 03/12/2022. -// -#ifndef SqfliteFmdbImport_m -#define SqfliteFmdbImport_m - -#if __has_include() -#import -#else -@import FMDB; -#endif - -#endif /* SqfliteFmdbImport_m */ diff --git a/sqflite/macos/Classes/SqfliteImport.h b/sqflite/macos/Classes/SqfliteImport.h deleted file mode 100644 index 44f77576..00000000 --- a/sqflite/macos/Classes/SqfliteImport.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// SqfliteImport.h -// sqflite -// -// Created by Alexandre Roux on 24/10/2022. -// -#ifndef SqfliteImport_h -#define SqfliteImport_h - -#if TARGET_OS_IPHONE -#import -#else -#import -#endif - -#endif // SqfliteImport_h diff --git a/sqflite/macos/Classes/SqfliteOperation.h b/sqflite/macos/Classes/SqfliteOperation.h deleted file mode 100644 index 2ffd7193..00000000 --- a/sqflite/macos/Classes/SqfliteOperation.h +++ /dev/null @@ -1,62 +0,0 @@ -// -// Operation.h -// sqflite -// -// Created by Alexandre Roux on 09/01/2018. -// -#ifndef SqfliteOperation_h -#define SqfliteOperation_h - -#import "SqfliteImport.h" - -@class FMDatabase; -@interface SqfliteOperation : NSObject - -- (NSString*)getMethod; -- (NSString*)getSql; -- (NSArray*)getSqlArguments; -- (NSNumber*)getInTransactionChange; -- (void)success:(NSObject*)results; -- (void)error:(FlutterError*)error; -- (bool)getNoResult; -- (bool)getContinueOnError; -- (bool)hasNullTransactionId; -- (NSNumber*)getTransactionId; -// Generic way to get any argument -- (id)getArgument:(NSString*)key; -- (bool)hasArgument:(NSString*)key; - -@end - -@interface SqfliteBatchOperation : SqfliteOperation - -@property (atomic, retain) NSDictionary* dictionary; -@property (atomic, retain) NSObject* results; -@property (atomic, retain) FlutterError* error; -@property (atomic, assign) bool noResult; -@property (atomic, assign) bool continueOnError; - -- (void)handleSuccess:(NSMutableArray*)results; -- (void)handleErrorContinue:(NSMutableArray*)results; -- (void)handleError:(FlutterResult)result; - -@end - -@interface SqfliteMethodCallOperation : SqfliteOperation - -@property (atomic, retain) FlutterMethodCall* flutterMethodCall; -@property (atomic, copy) FlutterResult flutterResult; - -+ (SqfliteMethodCallOperation*)newWithCall:(FlutterMethodCall*)flutterMethodCall result:(FlutterResult)flutterResult; - -@end - -typedef void(^SqfliteOperationHandler)(FMDatabase* db, SqfliteOperation* operation); -@interface SqfliteQueuedOperation : NSObject - -@property (atomic, retain) SqfliteOperation* operation; -@property (atomic, copy) SqfliteOperationHandler handler; - -@end - -#endif // SqfliteOperation_h diff --git a/sqflite/macos/Classes/SqfliteOperation.m b/sqflite/macos/Classes/SqfliteOperation.m deleted file mode 100644 index 037d4ab1..00000000 --- a/sqflite/macos/Classes/SqfliteOperation.m +++ /dev/null @@ -1,193 +0,0 @@ -// -// Operation.m -// sqflite -// -// Created by Alexandre Roux on 09/01/2018. -// - -#import -#import "SqfliteOperation.h" -#import "SqflitePlugin.h" -#import "SqfliteFmdbImport.m" - -// Abstract -@implementation SqfliteOperation - -- (NSString*)getMethod { - return nil; -} -- (NSString*)getSql { - return nil; -} -- (NSArray*)getSqlArguments { - return nil; -} - -- (bool)getNoResult { - return false; -} -- (bool)getContinueOnError { - return false; -} -- (void)success:(NSObject*)results {} - -- (void)error:(NSObject*)error {} - -// To override -- (id)getArgument:(NSString*)key { - return nil; -} - -// To override -- (bool)hasArgument:(NSString*)key { - return false; -} - -// Either nil or NSNumber -- (NSNumber*)getTransactionId { - // It might be NSNull (for begin transaction) - id rawId = [self getArgument:SqfliteParamTransactionId]; - if ([rawId isKindOfClass:[NSNumber class]]) { - return rawId; - } - return nil; -} - -- (NSNumber*)getInTransactionChange { - return [self getArgument:SqfliteParamInTransactionChange]; -} - -- (bool)hasNullTransactionId { - return [self getArgument:SqfliteParamTransactionId] == [NSNull null]; -} - -@end - -@implementation SqfliteBatchOperation - -@synthesize dictionary, results, error, noResult, continueOnError; - -- (NSString*)getMethod { - return [dictionary objectForKey:SqfliteParamMethod]; -} - -- (NSString*)getSql { - return [dictionary objectForKey:SqfliteParamSql]; -} - -- (NSArray*)getSqlArguments { - NSArray* arguments = [dictionary objectForKey:SqfliteParamSqlArguments]; - return [SqflitePlugin toSqlArguments:arguments]; -} - -- (bool)getNoResult { - return noResult; -} - -- (bool)getContinueOnError { - return continueOnError; -} - -- (void)success:(NSObject*)results { - self.results = results; -} - -- (void)error:(FlutterError*)error { - self.error = error; -} - -- (void)handleSuccess:(NSMutableArray*)results { - if (![self getNoResult]) { - // We wrap the result in 'result' map - [results addObject:[NSDictionary dictionaryWithObject:((self.results == nil) ? [NSNull null] : self.results) - forKey:SqfliteParamResult]]; - } -} - -// Encore the flutter error in a map -- (void)handleErrorContinue:(NSMutableArray*)results { - if (![self getNoResult]) { - // We wrap the error in an 'error' map - NSMutableDictionary* error = [NSMutableDictionary new]; - error[SqfliteParamErrorCode] = self.error.code; - if (self.error.message != nil) { - error[SqfliteParamErrorMessage] = self.error.message; - } - if (self.error.details != nil) { - error[SqfliteParamErrorData] = self.error.details; - } - [results addObject:[NSDictionary dictionaryWithObject:error - forKey:SqfliteParamError]]; - } -} - -- (void)handleError:(FlutterResult)result { - result(error); -} - -- (id)getArgument:(NSString*)key { - return [dictionary objectForKey:key]; -} - -- (bool)hasArgument:(NSString*)key { - return [self getArgument:key] != nil; -} - -@end - -@implementation SqfliteMethodCallOperation - -@synthesize flutterMethodCall; -@synthesize flutterResult; - -+ (SqfliteMethodCallOperation*)newWithCall:(FlutterMethodCall*)flutterMethodCall result:(FlutterResult)flutterResult { - SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation new]; - operation.flutterMethodCall = flutterMethodCall; - operation.flutterResult = flutterResult; - return operation; -} - -- (NSString*)getMethod { - return flutterMethodCall.method; -} - -- (NSString*)getSql { - return flutterMethodCall.arguments[SqfliteParamSql]; -} - -- (bool)getNoResult { - NSNumber* noResult = flutterMethodCall.arguments[SqfliteParamNoResult]; - return [noResult boolValue]; -} - -- (bool)getContinueOnError { - NSNumber* noResult = flutterMethodCall.arguments[SqfliteParamContinueOnError]; - return [noResult boolValue]; -} - -- (NSArray*)getSqlArguments { - NSArray* arguments = flutterMethodCall.arguments[SqfliteParamSqlArguments]; - return [SqflitePlugin toSqlArguments:arguments]; -} - - - -- (void)success:(NSObject*)results { - flutterResult(results); -} - -- (void)error:(NSObject*)error { - flutterResult(error); -} - -- (id)getArgument:(NSString*)key { - return flutterMethodCall.arguments[key]; -} - -@end - -@implementation SqfliteQueuedOperation - -@synthesize operation, handler; - -@end diff --git a/sqflite/macos/Classes/SqflitePlugin.h b/sqflite/macos/Classes/SqflitePlugin.h deleted file mode 100644 index 26079c92..00000000 --- a/sqflite/macos/Classes/SqflitePlugin.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// SqflitePlugin.h -// sqflite -// -// Created by Alexandre Roux on 24/10/2022. -// -#ifndef SqflitePlugin_h -#define SqflitePlugin_h - -#import "SqfliteImport.h" - -@class FMResultSet; -@interface SqflitePlugin : NSObject - -+ (NSArray*)toSqlArguments:(NSArray*)rawArguments; -+ (bool)arrayIsEmpy:(NSArray*)array; -+ (NSMutableDictionary*)resultSetToResults:(FMResultSet*)resultSet cursorPageSize:(NSNumber*)cursorPageSize; - -@end - -extern NSString *const SqfliteMethodExecute;; -extern NSString *const SqfliteMethodInsert; -extern NSString *const SqfliteMethodUpdate; -extern NSString *const SqfliteMethodQuery; - -extern NSString *const SqfliteErrorBadParam; -extern NSString *const SqliteErrorCode; - -extern NSString *const SqfliteParamMethod; -extern NSString *const SqfliteParamSql; -extern NSString *const SqfliteParamSqlArguments; -extern NSString *const SqfliteParamInTransactionChange; -extern NSString *const SqfliteParamNoResult; -extern NSString *const SqfliteParamContinueOnError; -extern NSString *const SqfliteParamResult; -extern NSString *const SqfliteParamError; -extern NSString *const SqfliteParamErrorCode; -extern NSString *const SqfliteParamErrorMessage; -extern NSString *const SqfliteParamErrorData; -extern NSString *const SqfliteParamTransactionId; - -// Static helpers -static const int sqfliteLogLevelNone = 0; -static const int sqfliteLogLevelSql = 1; -static const int sqfliteLogLevelVerbose = 2; - -extern bool sqfliteHasSqlLogLevel(int logLevel); -// True for verbose debugging -extern bool sqfliteHasVerboseLogLevel(int logLevel); - -#endif // SqflitePlugin_h diff --git a/sqflite/macos/Classes/SqflitePlugin.m b/sqflite/macos/Classes/SqflitePlugin.m deleted file mode 100644 index 9dc79a44..00000000 --- a/sqflite/macos/Classes/SqflitePlugin.m +++ /dev/null @@ -1,877 +0,0 @@ -#import "SqflitePlugin.h" -#import "SqfliteDatabase.h" -#import "SqfliteOperation.h" -#import "SqfliteFmdbImport.m" - -#import - -static NSString *const _channelName = @"com.tekartik.sqflite"; -static NSString *const _inMemoryPath = @":memory:"; - -static NSString *const _methodGetPlatformVersion = @"getPlatformVersion"; -static NSString *const _methodGetDatabasesPath = @"getDatabasesPath"; -static NSString *const _methodDebugMode = @"debugMode"; -static NSString *const _methodDebug = @"debug"; -static NSString *const _methodOptions = @"options"; -static NSString *const _methodOpenDatabase = @"openDatabase"; -static NSString *const _methodCloseDatabase = @"closeDatabase"; -static NSString *const _methodDeleteDatabase = @"deleteDatabase"; -static NSString *const _methodDatabaseExists = @"databaseExists"; - -static NSString *const _methodQueryCursorNext = @"queryCursorNext"; -static NSString *const _methodBatch = @"batch"; - -// For open -static NSString *const _paramReadOnly = @"readOnly"; -static NSString *const _paramSingleInstance = @"singleInstance"; -// Open result -static NSString *const _paramRecovered = @"recovered"; -static NSString *const _paramRecoveredInTransaction = @"recoveredInTransaction"; - -// For batch -static NSString *const _paramOperations = @"operations"; -// For each batch operation -static NSString *const _paramPath = @"path"; -static NSString *const _paramId = @"id"; -static NSString *const _paramTable = @"table"; -static NSString *const _paramValues = @"values"; - - - -static NSString *const _errorOpenFailed = @"open_failed"; -static NSString *const _errorDatabaseClosed = @"database_closed"; - -// debug -static NSString *const _paramDatabases = @"databases"; -static NSString *const _paramLogLevel = @"logLevel"; -static NSString *const _paramCmd = @"cmd"; -static NSString *const _paramCmdGet = @"get"; - -// query -static NSString *const _paramCancel = @"cancel"; -static NSString *const _paramCursorId = @"cursorId"; -static NSString *const _paramCursorPageSize = @"cursorPageSize"; - -// Shared -NSString *const SqfliteMethodExecute = @"execute"; -NSString *const SqfliteMethodInsert = @"insert"; -NSString *const SqfliteMethodUpdate = @"update"; -NSString *const SqfliteMethodQuery = @"query"; - -NSString *const SqliteErrorCode = @"sqlite_error"; -NSString *const SqfliteErrorBadParam = @"bad_param"; // internal only - -NSString *const SqfliteParamSql = @"sql"; -NSString *const SqfliteParamSqlArguments = @"arguments"; -NSString *const SqfliteParamInTransactionChange = @"inTransaction"; -NSString *const SqfliteParamTransactionId = @"transactionId"; // int or null -NSString *const SqfliteParamNoResult = @"noResult"; -NSString *const SqfliteParamContinueOnError = @"continueOnError"; -NSString *const SqfliteParamMethod = @"method"; -// For each operation in a batch, we have either a result or an error -NSString *const SqfliteParamResult = @"result"; -NSString *const SqfliteParamError = @"error"; -NSString *const SqfliteParamErrorCode = @"code"; -NSString *const SqfliteParamErrorMessage = @"message"; -NSString *const SqfliteParamErrorData = @"data"; - -// iOS workaround bug #214 -NSString *const SqfliteSqlPragmaSqliteDefensiveOff = @"PRAGMA sqflite -- db_config_defensive_off"; - -// Import hidden method -@interface FMDatabase () -- (void)resultSetDidClose:(FMResultSet *)resultSet; -@end - -@interface SqflitePlugin () - -@property (atomic, retain) NSMutableDictionary* databaseMap; -@property (atomic, retain) NSMutableDictionary* singleInstanceDatabaseMap; -@property (atomic, retain) NSObject* mapLock; - -@end - - - -// True for basic debugging (open/close and sql) -bool sqfliteHasSqlLogLevel(int logLevel) { - return logLevel >= sqfliteLogLevelSql; -} - -// True for verbose debugging -bool sqfliteHasVerboseLogLevel(int logLevel) { - return logLevel >= sqfliteLogLevelVerbose; -} - -// -// Implementation -// - - -@implementation SqflitePlugin - -@synthesize databaseMap; -@synthesize mapLock; - -static int logLevel = sqfliteLogLevelNone; - -// static BOOL _log = false; -static BOOL _extra_log = false; - -static BOOL __extra_log = false; // to set to true for type debugging - -static NSInteger _lastDatabaseId = 0; -static NSInteger _databaseOpenCount = 0; - - -+ (void)registerWithRegistrar:(NSObject*)registrar { -#if TARGET_OS_IPHONE - FlutterMethodChannel* channel = - [[FlutterMethodChannel alloc] initWithName:_channelName - binaryMessenger:[registrar messenger] - codec:[FlutterStandardMethodCodec sharedInstance] - taskQueue:[registrar.messenger makeBackgroundTaskQueue]]; -#else - FlutterMethodChannel* channel = [FlutterMethodChannel - methodChannelWithName:_channelName - binaryMessenger:[registrar messenger]]; -#endif - SqflitePlugin* instance = [[SqflitePlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (instancetype)init { - self = [super init]; - if (self) { - self.databaseMap = [NSMutableDictionary new]; - self.singleInstanceDatabaseMap = [NSMutableDictionary new]; - self.mapLock = [NSObject new]; - } - return self; -} - -- (SqfliteDatabase *)getDatabaseOrError:(FlutterMethodCall*)call result:(FlutterResult)result { - NSNumber* databaseId = call.arguments[_paramId]; - SqfliteDatabase* database = self.databaseMap[databaseId]; - if (database == nil) { - NSLog(@"db not found."); - result([FlutterError errorWithCode:SqliteErrorCode - message: _errorDatabaseClosed - details:nil]); - - } - return database; -} - -- (void)handleError:(FMDatabase*)db result:(FlutterResult)result { - // handle error - result([FlutterError errorWithCode:SqliteErrorCode - message:[NSString stringWithFormat:@"%@", [db lastError]] - details:nil]); -} - -- (void)handleError:(FMDatabase*)db operation:(SqfliteOperation*)operation { - NSMutableDictionary* details = nil; - NSString* sql = [operation getSql]; - if (sql != nil) { - details = [NSMutableDictionary new]; - [details setObject:sql forKey:SqfliteParamSql]; - NSArray* sqlArguments = [operation getSqlArguments]; - if (sqlArguments != nil) { - [details setObject:sqlArguments forKey:SqfliteParamSqlArguments]; - } - } - - [operation error:([FlutterError errorWithCode:SqliteErrorCode - message:[NSString stringWithFormat:@"%@", [db lastError]] - details:details])]; - -} - -+ (NSObject*)toSqlValue:(NSObject*)value { - if (_extra_log) { - NSLog(@"value type %@ %@", [value class], value); - } - if (value == nil) { - return nil; - } else if ([value isKindOfClass:[FlutterStandardTypedData class]]) { - FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)value; - return typedData.data; - } else if ([value isKindOfClass:[NSArray class]]) { - // Assume array of number - // slow...to optimize - NSArray* array = (NSArray*)value; - NSMutableData* data = [NSMutableData new]; - for (int i = 0; i < [array count]; i++) { - uint8_t byte = [((NSNumber *)[array objectAtIndex:i]) intValue]; - [data appendBytes:&byte length:1]; - } - return data; - } else { - return value; - } -} - -+ (NSObject*)fromSqlValue:(NSObject*)sqlValue { - if (_extra_log) { - NSLog(@"sql value type %@ %@", [sqlValue class], sqlValue); - } - if (sqlValue == nil) { - return [NSNull null]; - } else if ([sqlValue isKindOfClass:[NSData class]]) { - return [FlutterStandardTypedData typedDataWithBytes:(NSData*)sqlValue]; - } else { - return sqlValue; - } -} - -+ (bool)arrayIsEmpy:(NSArray*)array { - return (array == nil || array == (id)[NSNull null] || [array count] == 0); -} - -+ (NSArray*)toSqlArguments:(NSArray*)rawArguments { - NSMutableArray* array = [NSMutableArray new]; - if (![SqflitePlugin arrayIsEmpy:rawArguments]) { - for (int i = 0; i < [rawArguments count]; i++) { - [array addObject:[SqflitePlugin toSqlValue:[rawArguments objectAtIndex:i]]]; - } - } - return array; -} - -+ (NSDictionary*)fromSqlDictionary:(NSDictionary*)sqlDictionary { - NSMutableDictionary* dictionary = [NSMutableDictionary new]; - [sqlDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) { - [dictionary setObject:[SqflitePlugin fromSqlValue:value] forKey:key]; - }]; - return dictionary; -} - -// TODO remove -- (bool)executeOrError:(SqfliteDatabase*)database fmdb:(FMDatabase*)db call:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* sql = call.arguments[SqfliteParamSql]; - NSArray* arguments = call.arguments[SqfliteParamSqlArguments]; - NSArray* sqlArguments = [SqflitePlugin toSqlArguments:arguments]; - BOOL argumentsEmpty = [SqflitePlugin arrayIsEmpy:arguments]; - if (sqfliteHasSqlLogLevel(database.logLevel)) { - NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments); - } - - BOOL success; - if (!argumentsEmpty) { - success = [db executeUpdate: sql withArgumentsInArray: sqlArguments]; - } else { - success = [db executeUpdate: sql]; - } - - // handle error - if (!success) { - [self handleError:db result:result]; - return false; - } - - return true; -} - -- (bool)executeOrError:(SqfliteDatabase*)database fmdb:(FMDatabase*)db operation:(SqfliteOperation*)operation { - NSString* sql = [operation getSql]; - NSArray* sqlArguments = [operation getSqlArguments]; - NSNumber* inTransaction = [operation getInTransactionChange]; - - // Handle Hardcoded workarounds - // Handle issue #525 - if ([SqfliteSqlPragmaSqliteDefensiveOff isEqualToString:sql]) { - sqlite3_db_config(db.sqliteHandle, SQLITE_DBCONFIG_DEFENSIVE, 0, 0); - } - - BOOL argumentsEmpty = [SqflitePlugin arrayIsEmpy:sqlArguments]; - if (sqfliteHasSqlLogLevel(database.logLevel)) { - NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments); - } - - BOOL success; - if (!argumentsEmpty) { - success = [db executeUpdate: sql withArgumentsInArray: sqlArguments]; - } else { - success = [db executeUpdate: sql]; - } - - // If wanted, we leave the transaction even if it fails - if (inTransaction != nil) { - if (![inTransaction boolValue]) { - database.inTransaction = false; - } - } - - // handle error - if (!success) { - [self handleError:db operation:operation]; - return false; - } - - // We enter the transaction on success - if (inTransaction != nil) { - if ([inTransaction boolValue]) { - database.inTransaction = true; - } - } - - return true; -} - -// Rewrite to handle empty bloc reported as null -// refer to original FMResultSet.objectForColumnIndex, removed -// when fixed in FMDB -// See https://github.com/ccgus/fmdb/issues/350 for information -+ (id)rsObjectForColumn:(FMResultSet*)rs index:(int)columnIdx { - FMStatement* _statement = [rs statement]; - if (columnIdx < 0 || columnIdx >= sqlite3_column_count([_statement statement])) { - return nil; - } - - int columnType = sqlite3_column_type([_statement statement], columnIdx); - - id returnValue = nil; - - if (columnType == SQLITE_INTEGER) { - returnValue = [NSNumber numberWithLongLong:[rs longLongIntForColumnIndex:columnIdx]]; - } - else if (columnType == SQLITE_FLOAT) { - returnValue = [NSNumber numberWithDouble:[rs doubleForColumnIndex:columnIdx]]; - } - else if (columnType == SQLITE_BLOB) { - returnValue = [rs dataForColumnIndex:columnIdx]; - // Workaround, empty blob are reported as nil - if (returnValue == nil) { - return [NSData new]; - } - } - else { - //default to a string for everything else - returnValue = [rs stringForColumnIndex:columnIdx]; - } - - if (returnValue == nil) { - returnValue = [NSNull null]; - } - - return returnValue; -} - -// if cursorPageSize is not null, we limit the result count -+ (NSMutableDictionary*)resultSetToResults:(FMResultSet*)resultSet cursorPageSize:(NSNumber*)cursorPageSize { - NSMutableDictionary* results = [NSMutableDictionary new]; - NSMutableArray* columns = nil; - NSMutableArray* rows; - int columnCount = 0; - - while ([resultSet next]) { - if (columns == nil) { - columnCount = [resultSet columnCount]; - columns = [NSMutableArray new]; - rows = [NSMutableArray new]; - for (int i = 0; i < columnCount; i++) { - [columns addObject:[resultSet columnNameForIndex:i]]; - } - [results setValue:columns forKey:@"columns"]; - [results setValue:rows forKey:@"rows"]; - - } - NSMutableArray* row = [NSMutableArray new]; - for (int i = 0; i < columnCount; i++) { - [row addObject:[SqflitePlugin fromSqlValue:[self rsObjectForColumn:resultSet index:i]]]; - } - [rows addObject:row]; - - if (cursorPageSize != nil) { - if ([rows count] >= [cursorPageSize intValue]) { - break; - } - } - } - return results; -} -// -// query -// -- (bool)query:(SqfliteDatabase*)database fmdb:(FMDatabase*)db operation:(SqfliteOperation*)operation { - NSString* sql = [operation getSql]; - NSArray* sqlArguments = [operation getSqlArguments]; - bool argumentsEmpty = [SqflitePlugin arrayIsEmpy:sqlArguments]; - // Non null means use a cursor - NSNumber* cursorPageSize = [operation getArgument:_paramCursorPageSize]; - - if (sqfliteHasSqlLogLevel(database.logLevel)) { - NSLog(@"%@ %@", sql, argumentsEmpty ? @"" : sqlArguments); - } - - FMResultSet *resultSet; - if (!argumentsEmpty) { - resultSet = [db executeQuery:sql withArgumentsInArray:sqlArguments]; - } else { - // rs = [db executeQuery:sql]; - // This crashes on MacOS if there is any ? in the query - // Workaround using an empty array - resultSet = [db executeQuery:sql withArgumentsInArray:@[]]; - } - - // handle error - if ([db hadError]) { - [self handleError:db operation:operation]; - return false; - } - - NSMutableDictionary* results = [SqflitePlugin resultSetToResults:resultSet cursorPageSize:cursorPageSize]; - - if (cursorPageSize != nil) { - bool cursorHasMoreData = [resultSet hasAnotherRow]; - if (cursorHasMoreData) { - NSNumber* cursorId = [NSNumber numberWithInt:++database.lastCursorId]; - SqfliteCursor* cursor = [SqfliteCursor new]; - cursor.cursorId = cursorId; - cursor.pageSize = cursorPageSize; - cursor.resultSet = resultSet; - database.cursorMap[cursorId] = cursor; - // Notify cursor support in the result - results[_paramCursorId] = cursorId; - // Prevent FMDB warning, we keep a result set open on purpose - [db resultSetDidClose:resultSet]; - } - } - [operation success:results]; - - return true; -} - -- (void)handleQueryCall:(FlutterMethodCall*)call result:(FlutterResult)result { - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - [database inDatabase:^(FMDatabase *db) { - SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result]; - [database dbQuery:db operation:operation]; - }]; - -} - - -- (void)handleQueryCursorNextCall:(FlutterMethodCall*)call result:(FlutterResult)result { - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - [database inDatabase:^(FMDatabase *db) { - SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result]; - [database dbQueryCursorNext:db operation:operation]; - }]; - -} - -- (void)handleInsertCall:(FlutterMethodCall*)call result:(FlutterResult)result { - - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - [database inDatabase:^(FMDatabase *db) { - SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result]; - [database dbInsert:db operation:operation]; - }]; - - -} - -- (void)handleUpdateCall:(FlutterMethodCall*)call result:(FlutterResult)result { - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - - [database inDatabase:^(FMDatabase *db) { - SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result]; - [database dbUpdate:db operation:operation]; - }]; - - -} - -- (void)handleExecuteCall:(FlutterMethodCall*)call result:(FlutterResult)result { - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - - [database inDatabase:^(FMDatabase *db) { - SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result]; - [database dbExecute:db operation:operation]; - }]; -} - -// -// batch -// -- (void)handleBatchCall:(FlutterMethodCall*)call result:(FlutterResult)result { - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - - [database inDatabase:^(FMDatabase *db) { - - SqfliteMethodCallOperation* mainOperation = [SqfliteMethodCallOperation newWithCall:call result:result]; - [database dbBatch:db operation:mainOperation]; - - }]; - - -} - -+ (bool)isInMemoryPath:(NSString*)path { - if ([path isEqualToString:_inMemoryPath]) { - return true; - } - return false; -} - -+ (NSDictionary*)makeOpenResult:(NSNumber*)databaseId recovered:(bool)recovered recoveredInTransaction:(bool)recoveredInTransaction { - NSMutableDictionary* result = [NSMutableDictionary new]; - [result setObject:databaseId forKey:_paramId]; - if (recovered) { - [result setObject:[NSNumber numberWithBool:recovered] forKey:_paramRecovered]; - } - if (recoveredInTransaction) { - [result setObject:[NSNumber numberWithBool:recoveredInTransaction] forKey:_paramRecoveredInTransaction]; - } - return result; -} - -// -// open -// -- (void)handleOpenDatabaseCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* path = call.arguments[_paramPath]; - NSNumber* readOnlyValue = call.arguments[_paramReadOnly]; - bool readOnly = [readOnlyValue boolValue] == true; - NSNumber* singleInstanceValue = call.arguments[_paramSingleInstance]; - bool inMemoryPath = [SqflitePlugin isInMemoryPath:path]; - // A single instance must be a regular database - bool singleInstance = [singleInstanceValue boolValue] != false && !inMemoryPath; - - bool _log = sqfliteHasSqlLogLevel(logLevel); - if (_log) { - NSLog(@"opening %@ %@ %@", path, readOnly ? @" read-only" : @"", singleInstance ? @"" : @" new instance"); - } - - // Handle hot-restart for single instance - // The dart code is killed but the native code remains - if (singleInstance) { - @synchronized (self.mapLock) { - SqfliteDatabase* database = self.singleInstanceDatabaseMap[path]; - if (database != nil) { - // Check if openedŸ - if (_log) { - NSLog(@"re-opened %@singleInstance %@ id %@", database.inTransaction ? @"(in transaction) ": @"", path, database.databaseId); - } - result([SqflitePlugin makeOpenResult:database.databaseId recovered:true recoveredInTransaction:database.inTransaction]); - return; - } - } - } - - // Make sure the directory exists - if (!inMemoryPath && !readOnly) { - NSError* error; - NSString* parentDir = [path stringByDeletingLastPathComponent]; - if (![[NSFileManager defaultManager] fileExistsAtPath:parentDir]) { - if (_log) { - NSLog(@"Creating parent dir %@", parentDir); - } - [[NSFileManager defaultManager] createDirectoryAtPath:parentDir withIntermediateDirectories:YES attributes:nil error:&error]; - // Ingore the error, it will break later during open - } - } - FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path flags:(readOnly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE))]; - bool success = queue != nil; - - if (!success) { - NSLog(@"Could not open db."); - result([FlutterError errorWithCode:SqliteErrorCode - message:[NSString stringWithFormat:@"%@ %@", _errorOpenFailed, path] - details:nil]); - return; - } - - // First call will be to prepare the database. - // We turn on extended result code, allowing failure - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [queue inDatabase:^(FMDatabase *db) { - sqlite3_extended_result_codes(db.sqliteHandle, 1); - }]; - }); - - NSNumber* databaseId; - @synchronized (self.mapLock) { - SqfliteDatabase* database = [SqfliteDatabase new]; - databaseId = [NSNumber numberWithInteger:++_lastDatabaseId]; - database.inTransaction = false; - database.fmDatabaseQueue = queue; - database.singleInstance = singleInstance; - database.databaseId = databaseId; - database.path = path; - database.logLevel = logLevel; - self.databaseMap[databaseId] = database; - // To handle hot-restart recovery - if (singleInstance) { - self.singleInstanceDatabaseMap[path] = database; - } - if (_databaseOpenCount++ == 0) { - if (sqfliteHasVerboseLogLevel(logLevel)) { - NSLog(@"Creating operation queue"); - } - } - } - - result([SqflitePlugin makeOpenResult: databaseId recovered:false recoveredInTransaction:false]); -} - -// -// close -// -- (void)handleCloseDatabaseCall:(FlutterMethodCall*)call result:(FlutterResult)result { - SqfliteDatabase* database = [self getDatabaseOrError:call result:result]; - if (database == nil) { - return; - } - - if (sqfliteHasSqlLogLevel(database.logLevel)) { - NSLog(@"closing %@", database.path); - } - [self closeDatabase:database callback:^(){ - // We are in a background thread here. - // resut itself is a wrapper posting on the main thread - result(nil); - }]; -} - -// -// close action -// -// The callback will be called from a background thread -// -- (void)closeDatabase:(SqfliteDatabase*)database callback:(void(^)(void))callback { - if (sqfliteHasSqlLogLevel(database.logLevel)) { - NSLog(@"closing %@", database.path); - } - @synchronized (self.mapLock) { - [self.databaseMap removeObjectForKey:database.databaseId]; - if (database.singleInstance) { - [self.singleInstanceDatabaseMap removeObjectForKey:database.path]; - } - if (--_databaseOpenCount == 0) { - if (sqfliteHasVerboseLogLevel(logLevel)) { - NSLog(@"No more databases open"); - } - } - } - FMDatabaseQueue* queue = database.fmDatabaseQueue; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // It is safe to call this from a background queue because the function - // dispatches immediately to its queue synchronously. - [queue close]; - // TODO(gaaclarke): Remove this dispatch once the minimum Flutter value is set to 3.0. - // See also: https://github.com/flutter/flutter/issues/91635 - callback(); - }); -} - -- (void)deleteDatabaseFile:(NSString*)path { - bool _log = sqfliteHasSqlLogLevel(logLevel); - if (_log) { - NSLog(@"Deleting %@", path); - } - if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { - [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; - } -} - -// -// delete -// -- (void)handleDeleteDatabaseCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* path = call.arguments[_paramPath]; - - bool _log = sqfliteHasSqlLogLevel(logLevel); - - // Handle hot-restart for single instance - // The dart code is killed but the native code remains - SqfliteDatabase* database = nil; - @synchronized (self.mapLock) { - database = self.singleInstanceDatabaseMap[path]; - if (database != nil) { - // Check if openedŸ - if (_log) { - NSLog(@"Deleting opened %@ id %@", path, database.databaseId); - } - } - } - - if (database != nil) { - [self closeDatabase:database callback:^() { - // We are in a background thread here. - // resut itself is a wrapper posting on the main thread - [self deleteDatabaseFile:path]; - result(nil); - }]; - } else { - [self deleteDatabaseFile:path]; - result(nil); - } -} - -- (bool)databaseExists:(NSString*)path { - bool _log = sqfliteHasSqlLogLevel(logLevel); - if (_log) { - NSLog(@"databaseExists %@", path); - } - return ([[NSFileManager defaultManager] fileExistsAtPath:path]); -} - -// -// exists -// -- (void)handleDatabaseExistsCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSString* path = call.arguments[_paramPath]; - NSNumber* existsResult =[NSNumber numberWithBool:[self databaseExists: path]]; - result(existsResult); -} - -// -// debug -// -- (void)handleDebugCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSMutableDictionary* info = [NSMutableDictionary new]; - - NSString* cmd = call.arguments[_paramCmd]; - // NSLog(@"cmd %@", cmd); - if ([_paramCmdGet isEqualToString:cmd]) { - @synchronized (self.mapLock) { - if ([self.databaseMap count] > 0) { - NSMutableDictionary* dbsInfo = [NSMutableDictionary new]; - [self.databaseMap enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, SqfliteDatabase * _Nonnull db, BOOL * _Nonnull stop) { - NSMutableDictionary* dbInfo = [NSMutableDictionary new]; - [dbInfo setObject:db.path forKey:_paramPath]; - [dbInfo setObject:[NSNumber numberWithBool:db.singleInstance] forKey:_paramSingleInstance]; - if (db.logLevel > sqfliteLogLevelNone) { - [dbInfo setObject:[NSNumber numberWithInteger:db.logLevel ] forKey:_paramLogLevel]; - } - [dbsInfo setObject:dbInfo forKey:[key stringValue]]; - [info setObject:dbsInfo forKey:_paramDatabases]; - }]; - } - } - if (logLevel > sqfliteLogLevelNone) { - [info setObject:[NSNumber numberWithInteger:logLevel] forKey:_paramLogLevel]; - } - - } - result(info); -} - -// -// debug mode - trying deprecation since 1.1.6 -// -- (void)handleDebugModeCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSNumber* on = (NSNumber*)call.arguments; - bool _log = [on boolValue]; - NSLog(@"Debug mode %d", _log); - _extra_log = __extra_log && _log; - - if (_log) { - if (_extra_log) { - logLevel = sqfliteLogLevelVerbose; - } else { - logLevel = sqfliteLogLevelSql; - } - } else { - logLevel = sqfliteLogLevelNone; - } - result(nil); -} - -// -// Options -// -- (void)handleOptionsCall:(FlutterMethodCall*)call result:(FlutterResult)result { - NSNumber* logLevelNumber = call.arguments[_paramLogLevel]; - - if (logLevelNumber) { - logLevel = [logLevelNumber intValue]; - NSLog(@"Sqflite: logLevel %d", logLevel); - } - result(nil); -} - -// -// getDatabasesPath -// returns the Documents directory on iOS -// -- (void)handleGetDatabasesPath:(FlutterMethodCall*)call result:(FlutterResult)result { - NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - result(paths.firstObject); -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { -#if !TARGET_OS_IPHONE - // result wrapper to post the result on the main thread - // until background threads are supported for plugin services - result = ^(id res) { - dispatch_async(dispatch_get_main_queue(), ^{ - result(res); - }); - }; -#endif - - if ([_methodGetPlatformVersion isEqualToString:call.method]) { -#if TARGET_OS_IPHONE - result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); - -#else - result([@"macOS " stringByAppendingString:[[NSProcessInfo processInfo] operatingSystemVersionString]]); -#endif - } else if ([_methodOpenDatabase isEqualToString:call.method]) { - [self handleOpenDatabaseCall:call result:result]; - } else if ([SqfliteMethodInsert isEqualToString:call.method]) { - [self handleInsertCall:call result:result]; - } else if ([SqfliteMethodQuery isEqualToString:call.method]) { - [self handleQueryCall:call result:result]; - } else if ([SqfliteMethodUpdate isEqualToString:call.method]) { - [self handleUpdateCall:call result:result]; - } else if ([SqfliteMethodExecute isEqualToString:call.method]) { - [self handleExecuteCall:call result:result]; - } else if ([_methodBatch isEqualToString:call.method]) { - [self handleBatchCall:call result:result]; - } else if ([_methodQueryCursorNext isEqualToString:call.method]) { - [self handleQueryCursorNextCall:call result:result]; - } else if ([_methodGetDatabasesPath isEqualToString:call.method]) { - [self handleGetDatabasesPath:call result:result]; - } else if ([_methodCloseDatabase isEqualToString:call.method]) { - [self handleCloseDatabaseCall:call result:result]; - } else if ([_methodDeleteDatabase isEqualToString:call.method]) { - [self handleDeleteDatabaseCall:call result:result]; - } else if ([_methodDatabaseExists isEqualToString:call.method]) { - [self handleDatabaseExistsCall:call result:result]; - } else if ([_methodOptions isEqualToString:call.method]) { - [self handleOptionsCall:call result:result]; - } else if ([_methodDebug isEqualToString:call.method]) { - [self handleDebugCall:call - result:result]; - } else if ([_methodDebugMode isEqualToString:call.method]) { - [self handleDebugModeCall:call - result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -@end diff --git a/sqflite/macos/Resources/PrivacyInfo.xcprivacy b/sqflite/macos/Resources/PrivacyInfo.xcprivacy deleted file mode 100644 index 0eca193e..00000000 --- a/sqflite/macos/Resources/PrivacyInfo.xcprivacy +++ /dev/null @@ -1,14 +0,0 @@ - - - - - NSPrivacyTrackingDomains - - NSPrivacyAccessedAPITypes - - NSPrivacyCollectedDataTypes - - NSPrivacyTracking - - - \ No newline at end of file diff --git a/sqflite/macos/sqflite.podspec b/sqflite/macos/sqflite.podspec deleted file mode 100644 index 1a84726a..00000000 --- a/sqflite/macos/sqflite.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint sqflite.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'sqflite' - s.version = '0.0.2' - s.summary = 'SQLite plugin.' - s.description = <<-DESC -Access SQLite database. - DESC - s.homepage = 'https://github.com/tekartik/sqflite' - s.license = { :file => '../LICENSE' } - s.author = { 'Tekartik' => 'alex@tekartik.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - s.dependency 'FMDB', '>= 2.7.5' - - s.platform = :osx, '10.14' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.swift_version = '5.0' - s.resource_bundles = {'sqflite_macos_privacy' => ['Resources/PrivacyInfo.xcprivacy']} -end diff --git a/sqflite/pubspec.yaml b/sqflite/pubspec.yaml index 08b3844d..6b363c1b 100644 --- a/sqflite/pubspec.yaml +++ b/sqflite/pubspec.yaml @@ -20,7 +20,9 @@ funding: environment: sdk: '>=3.0.0 <4.0.0' - flutter: ">=3.3.0" + # Flutter versions prior to 3.7 did not support the + # sharedDarwinSource option. + flutter: ">=3.7.0" flutter: plugin: @@ -32,9 +34,11 @@ flutter: ios: pluginClass: SqflitePlugin dartPluginClass: SqflitePlugin + sharedDarwinSource: true macos: pluginClass: SqflitePlugin dartPluginClass: SqflitePlugin + sharedDarwinSource: true dependencies: flutter: