Add CI/CD configuration and API documentation
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
import GRDB
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
// Test GRDB
|
||||
_ = try! DatabaseQueue()
|
||||
|
||||
// test SQLITE_ENABLE_FTS5
|
||||
_ = FTS5.self
|
||||
|
||||
// test_SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
_ = DatabasePreUpdateEvent.self
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,32 @@
|
||||
use_frameworks!
|
||||
target 'iOS'
|
||||
platform :ios, '11.0'
|
||||
pod 'GRDB.swift', :path => '../../..'
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# Enable extra GRDB APIs
|
||||
config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK"
|
||||
# Enable extra SQLite APIs
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: remove when https://github.com/CocoaPods/CocoaPods/pull/12009 is merged.
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/12012#issuecomment-1655191516
|
||||
installer.aggregate_targets.each do |target|
|
||||
target.xcconfigs.each do |variant, xcconfig|
|
||||
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
56606CF02353353600185962 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56606CEF2353353600185962 /* AppDelegate.swift */; };
|
||||
56606CF32353368800185962 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 56606CF22353368800185962 /* Default-568h@2x.png */; };
|
||||
905ECD161513DA7089DC0E4F /* Pods_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44319992BE6EA64810E8982A /* Pods_iOS.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
5644E4071C1072CA0090716E /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
44319992BE6EA64810E8982A /* Pods_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
56606CEF2353353600185962 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
56606CF12353357300185962 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
56606CF22353368800185962 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = "<group>"; };
|
||||
567374381B4D46600089DE44 /* iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
62C5B84AA50DF194B4DDDFD0 /* Pods-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iOS/Pods-iOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9573251A237B01D90415D0D7 /* Pods-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-iOS/Pods-iOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
567374351B4D46600089DE44 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
905ECD161513DA7089DC0E4F /* Pods_iOS.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
5673742F1B4D46600089DE44 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56606CEF2353353600185962 /* AppDelegate.swift */,
|
||||
56606CF22353368800185962 /* Default-568h@2x.png */,
|
||||
56606CF12353357300185962 /* Info.plist */,
|
||||
567374391B4D46600089DE44 /* Products */,
|
||||
9ABB37395508D35C98A292E7 /* Pods */,
|
||||
8D9BFC4007F760CC8E01ACC6 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567374391B4D46600089DE44 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567374381B4D46600089DE44 /* iOS.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8D9BFC4007F760CC8E01ACC6 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
44319992BE6EA64810E8982A /* Pods_iOS.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9ABB37395508D35C98A292E7 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
62C5B84AA50DF194B4DDDFD0 /* Pods-iOS.debug.xcconfig */,
|
||||
9573251A237B01D90415D0D7 /* Pods-iOS.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
567374371B4D46600089DE44 /* iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 567374621B4D46600089DE44 /* Build configuration list for PBXNativeTarget "iOS" */;
|
||||
buildPhases = (
|
||||
C5E9DF0928754FE22F481D3A /* [CP] Check Pods Manifest.lock */,
|
||||
567374341B4D46600089DE44 /* Sources */,
|
||||
567374351B4D46600089DE44 /* Frameworks */,
|
||||
567374361B4D46600089DE44 /* Resources */,
|
||||
5644E4071C1072CA0090716E /* Embed Frameworks */,
|
||||
846C75E920926E1E5C32A97F /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = iOS;
|
||||
productName = iOS;
|
||||
productReference = 567374381B4D46600089DE44 /* iOS.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
567374301B4D46600089DE44 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0730;
|
||||
LastUpgradeCheck = 1110;
|
||||
ORGANIZATIONNAME = "Gwendal Roué";
|
||||
TargetAttributes = {
|
||||
567374371B4D46600089DE44 = {
|
||||
CreatedOnToolsVersion = 7.0;
|
||||
DevelopmentTeam = AMD8W895CT;
|
||||
LastSwiftMigration = 1110;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 567374331B4D46600089DE44 /* Build configuration list for PBXProject "iOS" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 5673742F1B4D46600089DE44;
|
||||
productRefGroup = 567374391B4D46600089DE44 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
567374371B4D46600089DE44 /* iOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
567374361B4D46600089DE44 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56606CF32353368800185962 /* Default-568h@2x.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
846C75E920926E1E5C32A97F /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-iOS/Pods-iOS-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/GRDB.swift/GRDB.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDB.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS/Pods-iOS-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C5E9DF0928754FE22F481D3A /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-iOS-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
567374341B4D46600089DE44 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56606CF02353353600185962 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
567374601B4D46600089DE44 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
567374611B4D46600089DE44 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
567374631B4D46600089DE44 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 62C5B84AA50DF194B4DDDFD0 /* Pods-iOS.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
567374641B4D46600089DE44 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9573251A237B01D90415D0D7 /* Pods-iOS.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
567374331B4D46600089DE44 /* Build configuration list for PBXProject "iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
567374601B4D46600089DE44 /* Debug */,
|
||||
567374611B4D46600089DE44 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
567374621B4D46600089DE44 /* Build configuration list for PBXNativeTarget "iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
567374631B4D46600089DE44 /* Debug */,
|
||||
567374641B4D46600089DE44 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 567374301B4D46600089DE44 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:iOS.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:iOS.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,18 @@
|
||||
import GRDB
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
// Test GRDB
|
||||
_ = try! DatabaseQueue()
|
||||
|
||||
// test SQLITE_ENABLE_FTS5
|
||||
_ = FTS5.self
|
||||
|
||||
// test_SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
_ = DatabasePreUpdateEvent.self
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,31 @@
|
||||
target 'iOS'
|
||||
platform :ios, '11.0'
|
||||
pod 'GRDB.swift', :path => '../../..'
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# Enable extra GRDB APIs
|
||||
config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK"
|
||||
# Enable extra SQLite APIs
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: remove when https://github.com/CocoaPods/CocoaPods/pull/12009 is merged.
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/12012#issuecomment-1655191516
|
||||
installer.aggregate_targets.each do |target|
|
||||
target.xcconfigs.each do |variant, xcconfig|
|
||||
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
33D0B66EF0B896F9E8391C5D /* libPods-iOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D9232D362032D81EAFF2102C /* libPods-iOS.a */; };
|
||||
56606CF02353353600185962 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56606CEF2353353600185962 /* AppDelegate.swift */; };
|
||||
56606CF32353368800185962 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 56606CF22353368800185962 /* Default-568h@2x.png */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
5644E4071C1072CA0090716E /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
56606CEF2353353600185962 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
56606CF12353357300185962 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
56606CF22353368800185962 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = "<group>"; };
|
||||
567374381B4D46600089DE44 /* iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
62C5B84AA50DF194B4DDDFD0 /* Pods-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iOS/Pods-iOS.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9573251A237B01D90415D0D7 /* Pods-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-iOS/Pods-iOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D9232D362032D81EAFF2102C /* libPods-iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
567374351B4D46600089DE44 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33D0B66EF0B896F9E8391C5D /* libPods-iOS.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
5673742F1B4D46600089DE44 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56606CEF2353353600185962 /* AppDelegate.swift */,
|
||||
56606CF22353368800185962 /* Default-568h@2x.png */,
|
||||
56606CF12353357300185962 /* Info.plist */,
|
||||
567374391B4D46600089DE44 /* Products */,
|
||||
9ABB37395508D35C98A292E7 /* Pods */,
|
||||
8D9BFC4007F760CC8E01ACC6 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
567374391B4D46600089DE44 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
567374381B4D46600089DE44 /* iOS.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8D9BFC4007F760CC8E01ACC6 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9232D362032D81EAFF2102C /* libPods-iOS.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9ABB37395508D35C98A292E7 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
62C5B84AA50DF194B4DDDFD0 /* Pods-iOS.debug.xcconfig */,
|
||||
9573251A237B01D90415D0D7 /* Pods-iOS.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
567374371B4D46600089DE44 /* iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 567374621B4D46600089DE44 /* Build configuration list for PBXNativeTarget "iOS" */;
|
||||
buildPhases = (
|
||||
C5E9DF0928754FE22F481D3A /* [CP] Check Pods Manifest.lock */,
|
||||
567374341B4D46600089DE44 /* Sources */,
|
||||
567374351B4D46600089DE44 /* Frameworks */,
|
||||
567374361B4D46600089DE44 /* Resources */,
|
||||
5644E4071C1072CA0090716E /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = iOS;
|
||||
productName = iOS;
|
||||
productReference = 567374381B4D46600089DE44 /* iOS.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
567374301B4D46600089DE44 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0730;
|
||||
LastUpgradeCheck = 1110;
|
||||
ORGANIZATIONNAME = "Gwendal Roué";
|
||||
TargetAttributes = {
|
||||
567374371B4D46600089DE44 = {
|
||||
CreatedOnToolsVersion = 7.0;
|
||||
DevelopmentTeam = AMD8W895CT;
|
||||
LastSwiftMigration = 1110;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 567374331B4D46600089DE44 /* Build configuration list for PBXProject "iOS" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 5673742F1B4D46600089DE44;
|
||||
productRefGroup = 567374391B4D46600089DE44 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
567374371B4D46600089DE44 /* iOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
567374361B4D46600089DE44 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56606CF32353368800185962 /* Default-568h@2x.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
C5E9DF0928754FE22F481D3A /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-iOS-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
567374341B4D46600089DE44 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56606CF02353353600185962 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
567374601B4D46600089DE44 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
567374611B4D46600089DE44 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
567374631B4D46600089DE44 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 62C5B84AA50DF194B4DDDFD0 /* Pods-iOS.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
567374641B4D46600089DE44 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9573251A237B01D90415D0D7 /* Pods-iOS.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.GRDBTest;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
567374331B4D46600089DE44 /* Build configuration list for PBXProject "iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
567374601B4D46600089DE44 /* Debug */,
|
||||
567374611B4D46600089DE44 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
567374621B4D46600089DE44 /* Build configuration list for PBXNativeTarget "iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
567374631B4D46600089DE44 /* Debug */,
|
||||
567374641B4D46600089DE44 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 567374301B4D46600089DE44 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:iOS.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:iOS.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+2157
File diff suppressed because it is too large
Load Diff
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:SQLCipher4.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1340"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A2095226B8E18001F64F1"
|
||||
BuildableName = "GRDBTestsEncrypted.xctest"
|
||||
BlueprintName = "GRDBTestsEncrypted"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A2095226B8E18001F64F1"
|
||||
BuildableName = "GRDBTestsEncrypted.xctest"
|
||||
BlueprintName = "GRDBTestsEncrypted"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A1F1C226B876D001F64F1"
|
||||
BuildableName = "GRDBTests.xctest"
|
||||
BlueprintName = "GRDBTests"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A1F1C226B876D001F64F1"
|
||||
BuildableName = "GRDBTests.xctest"
|
||||
BlueprintName = "GRDBTests"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A1F1C226B876D001F64F1"
|
||||
BuildableName = "GRDBTests.xctest"
|
||||
BlueprintName = "GRDBTests"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:GRDBTests.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,43 @@
|
||||
platform :macos, '10.13'
|
||||
use_frameworks!
|
||||
|
||||
def common
|
||||
pod 'GRDB.swift/SQLCipher', path: '../../..'
|
||||
pod 'SQLCipher', '~> 3.0', inhibit_warnings: true
|
||||
end
|
||||
|
||||
target 'GRDBTests' do
|
||||
common
|
||||
end
|
||||
|
||||
target 'GRDBTestsEncrypted' do
|
||||
common
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# Workaround for Xcode 14.3+
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/11839
|
||||
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13'
|
||||
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '3'
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: remove when https://github.com/CocoaPods/CocoaPods/pull/12009 is merged.
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/12012#issuecomment-1655191516
|
||||
installer.aggregate_targets.each do |target|
|
||||
target.xcconfigs.each do |variant, xcconfig|
|
||||
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+2175
File diff suppressed because it is too large
Load Diff
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:SQLCipher4.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1340"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A2095226B8E18001F64F1"
|
||||
BuildableName = "GRDBTestsEncrypted.xctest"
|
||||
BlueprintName = "GRDBTestsEncrypted"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A2095226B8E18001F64F1"
|
||||
BuildableName = "GRDBTestsEncrypted.xctest"
|
||||
BlueprintName = "GRDBTestsEncrypted"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1100"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A1F1C226B876D001F64F1"
|
||||
BuildableName = "GRDBTests.xctest"
|
||||
BlueprintName = "GRDBTests"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A1F1C226B876D001F64F1"
|
||||
BuildableName = "GRDBTests.xctest"
|
||||
BlueprintName = "GRDBTests"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "564A1F1C226B876D001F64F1"
|
||||
BuildableName = "GRDBTests.xctest"
|
||||
BlueprintName = "GRDBTests"
|
||||
ReferencedContainer = "container:GRDBTests.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:GRDBTests.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string></string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,43 @@
|
||||
platform :macos, '10.13'
|
||||
use_frameworks!
|
||||
|
||||
def common
|
||||
pod 'GRDB.swift/SQLCipher', path: '../../..'
|
||||
pod 'SQLCipher', '~> 4.0', inhibit_warnings: true
|
||||
end
|
||||
|
||||
target 'GRDBTests' do
|
||||
common
|
||||
end
|
||||
|
||||
target 'GRDBTestsEncrypted' do
|
||||
common
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# Workaround for Xcode 14.3+
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/11839
|
||||
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13'
|
||||
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '3'
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: remove when https://github.com/CocoaPods/CocoaPods/pull/12009 is merged.
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/12012#issuecomment-1655191516
|
||||
installer.aggregate_targets.each do |target|
|
||||
target.xcconfigs.each do |variant, xcconfig|
|
||||
xcconfig_path = target.client_root + target.xcconfig_relative_path(variant)
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.base_configuration_reference.is_a? Xcodeproj::Project::Object::PBXFileReference
|
||||
xcconfig_path = config.base_configuration_reference.real_path
|
||||
IO.write(xcconfig_path, IO.read(xcconfig_path).gsub("DT_TOOLCHAIN_DIR", "TOOLCHAIN_DIR"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Binary file not shown.
+113
@@ -0,0 +1,113 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
/// A name space for publisher expectations
|
||||
public enum PublisherExpectations { }
|
||||
|
||||
/// The base protocol for PublisherExpectation. It is an implementation detail
|
||||
/// that you are not supposed to use, as shown by the underscore prefix.
|
||||
public protocol _PublisherExpectationBase {
|
||||
/// Sets up an XCTestExpectation. This method is an implementation detail
|
||||
/// that you are not supposed to use, as shown by the underscore prefix.
|
||||
func _setup(_ expectation: XCTestExpectation)
|
||||
|
||||
/// Returns an object that waits for the expectation. If nil, expectation
|
||||
/// is waited by the XCTestCase.
|
||||
func _makeWaiter() -> XCTWaiter?
|
||||
}
|
||||
|
||||
extension _PublisherExpectationBase {
|
||||
public func _makeWaiter() -> XCTWaiter? { nil }
|
||||
}
|
||||
|
||||
/// The protocol for publisher expectations.
|
||||
///
|
||||
/// You can build publisher expectations from Recorder returned by the
|
||||
/// `Publisher.record()` method.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // The expectation for all published elements until completion
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let expectation = recorder.elements
|
||||
///
|
||||
/// When a test grants some time for the expectation to fulfill, use the
|
||||
/// XCTest `wait(for:timeout:description)` method:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let expectation = recorder.elements
|
||||
/// let elements = try wait(for: expectation, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
///
|
||||
/// On the other hand, when the expectation is supposed to be immediately
|
||||
/// fulfilled, use the PublisherExpectation `get()` method in order to grab the
|
||||
/// expected value:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronouslyPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try recorder.elements.get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
public protocol PublisherExpectation: _PublisherExpectationBase {
|
||||
/// The type of the expected value.
|
||||
associatedtype Output
|
||||
|
||||
/// Returns the expected value, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronouslyPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try recorder.elements.get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
func get() throws -> Output
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
/// Waits for the publisher expectation to fulfill, and returns the
|
||||
/// expected value.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
///
|
||||
/// - parameter publisherExpectation: The publisher expectation.
|
||||
/// - parameter timeout: The number of seconds within which the expectation
|
||||
/// must be fulfilled.
|
||||
/// - parameter description: A string to display in the test log for the
|
||||
/// expectation, to help diagnose failures.
|
||||
/// - throws: An error if the expectation fails.
|
||||
public func wait<R: PublisherExpectation>(
|
||||
for publisherExpectation: R,
|
||||
timeout: TimeInterval,
|
||||
description: String = "")
|
||||
throws -> R.Output
|
||||
{
|
||||
let expectation = self.expectation(description: description)
|
||||
publisherExpectation._setup(expectation)
|
||||
if let waiter = publisherExpectation._makeWaiter() {
|
||||
waiter.wait(for: [expectation], timeout: timeout)
|
||||
} else {
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
return try publisherExpectation.get()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the timeout to expire, or
|
||||
/// the recorded publisher to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if
|
||||
/// the publisher fails before the expectation has expired.
|
||||
///
|
||||
/// Otherwise, an array of all elements published before the expectation
|
||||
/// has expired is returned.
|
||||
///
|
||||
/// Unlike other expectations, `AvailableElements` does not make a test fail
|
||||
/// on timeout expiration. It just returns the elements published so far.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testTimerPublishesIncreasingDates() throws {
|
||||
/// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||
/// let recorder = publisher.record()
|
||||
/// let dates = try wait(for: recorder.availableElements, timeout: ...)
|
||||
/// XCTAssertEqual(dates.sorted(), dates)
|
||||
/// }
|
||||
public struct AvailableElements<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _makeWaiter() -> XCTWaiter? { Waiter() }
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnCompletion(expectation)
|
||||
}
|
||||
|
||||
/// Returns all elements published so far, or throws an error if the
|
||||
/// publisher has failed.
|
||||
public func get() throws -> [Input] {
|
||||
try recorder.value { (elements, completion, remainingElements, consume) in
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
consume(remainingElements.count)
|
||||
return elements
|
||||
}
|
||||
}
|
||||
|
||||
/// A waiter that waits but never fails
|
||||
private class Waiter: XCTWaiter, XCTWaiterDelegate {
|
||||
init() {
|
||||
super.init(delegate: nil)
|
||||
delegate = self
|
||||
}
|
||||
|
||||
func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { }
|
||||
func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { }
|
||||
func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { }
|
||||
func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
// The Finished expectation waits for the publisher to complete, and throws an
|
||||
// error if and only if the publisher fails with an error.
|
||||
//
|
||||
// It is not derived from the Recording expectation, because Finished does not
|
||||
// throw RecordingError.notCompleted if the publisher does not complete on time.
|
||||
// It only triggers a timeout test failure.
|
||||
//
|
||||
// This allows to write tests for publishers that should not complete:
|
||||
//
|
||||
// // SUCCESS: no timeout, no error
|
||||
// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
// let publisher = PassthroughSubject<String, Never>()
|
||||
// let recorder = publisher.record()
|
||||
// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
// }
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherFinishesWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished, timeout: 1)
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public struct Finished<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnCompletion(expectation)
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronouslyFinishesWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// try recorder.finished.get()
|
||||
/// }
|
||||
public func get() throws {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
guard let completion else {
|
||||
consume(remainingElements.count)
|
||||
return
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an inverted publisher expectation which waits for a
|
||||
/// publisher to complete successfully.
|
||||
///
|
||||
/// When waiting for this expectation, an error is thrown if the
|
||||
/// publisher fails with an error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public var inverted: Inverted<Self> {
|
||||
return Inverted(base: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation that fails if the base expectation is fulfilled.
|
||||
///
|
||||
/// When waiting for this expectation, you receive the same result and
|
||||
/// eventual error as the base expectation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public struct Inverted<Base: PublisherExpectation>: PublisherExpectation {
|
||||
let base: Base
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
base._setup(expectation)
|
||||
expectation.isInverted.toggle()
|
||||
}
|
||||
|
||||
public func get() throws -> Base.Output {
|
||||
try base.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation that transforms the value of a base expectation.
|
||||
///
|
||||
/// This expectation has no public initializer.
|
||||
public struct Map<Base: PublisherExpectation, Output>: PublisherExpectation {
|
||||
let base: Base
|
||||
let transform: (Base.Output) throws -> Output
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
base._setup(expectation)
|
||||
}
|
||||
|
||||
public func get() throws -> Output {
|
||||
try transform(base.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectation {
|
||||
/// Returns a publisher expectation that transforms the value of the
|
||||
/// base expectation.
|
||||
func map<T>(_ transform: @escaping (Output) throws -> T) -> PublisherExpectations.Map<Self, T> {
|
||||
PublisherExpectations.Map(base: self, transform: transform)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher to emit
|
||||
/// `count` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a `RecordingError.notEnoughElements`
|
||||
/// is thrown if the publisher does not publish `count` elements after last
|
||||
/// waited expectation. The publisher error is thrown if the publisher fails
|
||||
/// before publishing the next `count` elements.
|
||||
///
|
||||
/// Otherwise, an array of exactly `count` elements is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var elements = try wait(for: recorder.next(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
///
|
||||
/// elements = try wait(for: recorder.next(1), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["baz"])
|
||||
/// }
|
||||
public struct Next<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
let count: Int
|
||||
|
||||
init(recorder: Recorder<Input, Failure>, count: Int) {
|
||||
precondition(count >= 0, "Can't take a prefix of negative length")
|
||||
self.recorder = recorder
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
if count == 0 {
|
||||
// Such an expectation is immediately fulfilled, by essence.
|
||||
expectation.expectedFulfillmentCount = 1
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
expectation.expectedFulfillmentCount = count
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var elements = try recorder.next(2).get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
///
|
||||
/// elements = try recorder.next(1).get()
|
||||
/// XCTAssertEqual(elements, ["baz"])
|
||||
/// }
|
||||
public func get() throws -> [Input] {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
if remainingElements.count >= count {
|
||||
consume(count)
|
||||
return Array(remainingElements.prefix(count))
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
} else {
|
||||
throw RecordingError.notEnoughElements
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher to emit
|
||||
/// one element, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a `RecordingError.notEnoughElements`
|
||||
/// is thrown if the publisher does not publish one element after last
|
||||
/// waited expectation. The publisher error is thrown if the publisher fails
|
||||
/// before publishing the next element.
|
||||
///
|
||||
/// Otherwise, the next published element is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfTwoElementsPublishesElementsInOrder() throws {
|
||||
/// let publisher = ["foo", "bar"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
///
|
||||
/// element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "bar")
|
||||
/// }
|
||||
public struct NextOne<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: false)
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws {
|
||||
/// let publisher = ["foo", "bar"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var element = try recorder.next().get()
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
///
|
||||
/// element = try recorder.next().get()
|
||||
/// XCTAssertEqual(element, "bar")
|
||||
/// }
|
||||
public func get() throws -> Input {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
if let next = remainingElements.first {
|
||||
consume(1)
|
||||
return next
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
} else {
|
||||
throw RecordingError.notEnoughElements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an inverted publisher expectation which waits for the
|
||||
/// recorded publisher to emit one element, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError is thrown if the
|
||||
/// publisher does not publish one element after last waited
|
||||
/// expectation. The publisher error is thrown if the publisher fails
|
||||
/// before publishing one element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotPublishAnyElement() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.next().inverted, timeout: 1)
|
||||
/// }
|
||||
public var inverted: NextOneInverted<Input, Failure> {
|
||||
return NextOneInverted(recorder: recorder)
|
||||
}
|
||||
}
|
||||
|
||||
/// An inverted publisher expectation which waits for the recorded publisher
|
||||
/// to emit one element, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError is thrown if the
|
||||
/// publisher does not publish one element after last waited expectation.
|
||||
/// The publisher error is thrown if the publisher fails before
|
||||
/// publishing one element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotPublishAnyElement() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.next().inverted, timeout: 1)
|
||||
/// }
|
||||
public struct NextOneInverted<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
expectation.isInverted = true
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: false)
|
||||
}
|
||||
|
||||
public func get() throws {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
if remainingElements.isEmpty == false {
|
||||
return
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher to emit
|
||||
/// `maxLength` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails before `maxLength` elements are published.
|
||||
///
|
||||
/// Otherwise, an array of received elements is returned, containing at
|
||||
/// most `maxLength` elements, or less if the publisher completes early.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.prefix(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// publisher.send("foo")
|
||||
/// publisher.send("bar")
|
||||
/// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
public struct Prefix<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
let maxLength: Int
|
||||
|
||||
init(recorder: Recorder<Input, Failure>, maxLength: Int) {
|
||||
precondition(maxLength >= 0, "Can't take a prefix of negative length")
|
||||
self.recorder = recorder
|
||||
self.maxLength = maxLength
|
||||
}
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
if maxLength == 0 {
|
||||
// Such an expectation is immediately fulfilled, by essence.
|
||||
expectation.expectedFulfillmentCount = 1
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
expectation.expectedFulfillmentCount = maxLength
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try recorder.prefix(2).get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
public func get() throws -> [Input] {
|
||||
try recorder.value { (elements, completion, remainingElements, consume) in
|
||||
if elements.count >= maxLength {
|
||||
let extraCount = max(maxLength + remainingElements.count - elements.count, 0)
|
||||
consume(extraCount)
|
||||
return Array(elements.prefix(maxLength))
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
consume(remainingElements.count)
|
||||
return elements
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an inverted publisher expectation which waits for a
|
||||
/// publisher to emit `maxLength` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown
|
||||
/// if the publisher fails before `maxLength` elements are published.
|
||||
///
|
||||
/// Otherwise, an array of received elements is returned, containing at
|
||||
/// most `maxLength` elements, or less if the publisher completes early.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// publisher.send("foo")
|
||||
/// publisher.send("bar")
|
||||
/// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
public var inverted: Inverted<Self> {
|
||||
return Inverted(base: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError.notCompleted is
|
||||
/// thrown if the publisher does not complete on time.
|
||||
///
|
||||
/// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording)
|
||||
/// is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherRecording() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let recording = try wait(for: recorder.recording, timeout: 1)
|
||||
/// XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
|
||||
/// if case let .failure(error) = recording.completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public struct Recording<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnCompletion(expectation)
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronousRecording() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let recording = try recorder.recording.get()
|
||||
/// XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
|
||||
/// if case let .failure(error) = recording.completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public func get() throws -> Record<Input, Failure>.Recording {
|
||||
try recorder.value { (elements, completion, remainingElements, consume) in
|
||||
if let completion {
|
||||
consume(remainingElements.count)
|
||||
return Record<Input, Failure>.Recording(output: elements, completion: completion)
|
||||
} else {
|
||||
throw RecordingError.notCompleted
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,617 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import XCTest
|
||||
|
||||
/// A Combine subscriber which records all events published by a publisher.
|
||||
///
|
||||
/// You create a Recorder with the `Publisher.record()` method:
|
||||
///
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// You can build publisher expectations from the Recorder. For example:
|
||||
///
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public class Recorder<Input, Failure: Error>: Subscriber {
|
||||
public typealias Input = Input
|
||||
public typealias Failure = Failure
|
||||
|
||||
private enum RecorderExpectation {
|
||||
case onInput(XCTestExpectation, remainingCount: Int)
|
||||
case onCompletion(XCTestExpectation)
|
||||
|
||||
var expectation: XCTestExpectation {
|
||||
switch self {
|
||||
case let .onCompletion(expectation):
|
||||
return expectation
|
||||
case let .onInput(expectation, remainingCount: _):
|
||||
return expectation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The recorder state
|
||||
private enum State {
|
||||
/// Publisher is not subscribed yet. The recorder may have an
|
||||
/// expectation to fulfill.
|
||||
case waitingForSubscription(RecorderExpectation?)
|
||||
|
||||
/// Publisher is subscribed. The recorder may have an expectation to
|
||||
/// fulfill. It keeps track of all published elements.
|
||||
case subscribed(Subscription, RecorderExpectation?, [Input])
|
||||
|
||||
/// Publisher is completed. The recorder keeps track of all published
|
||||
/// elements and completion.
|
||||
case completed([Input], Subscribers.Completion<Failure>)
|
||||
|
||||
var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion<Failure>?) {
|
||||
switch self {
|
||||
case .waitingForSubscription:
|
||||
return (elements: [], completion: nil)
|
||||
case let .subscribed(_, _, elements):
|
||||
return (elements: elements, completion: nil)
|
||||
case let .completed(elements, completion):
|
||||
return (elements: elements, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
var recorderExpectation: RecorderExpectation? {
|
||||
switch self {
|
||||
case let .waitingForSubscription(exp), let .subscribed(_, exp, _):
|
||||
return exp
|
||||
case .completed:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var state = State.waitingForSubscription(nil)
|
||||
private var consumedCount = 0
|
||||
|
||||
/// The elements and completion recorded so far.
|
||||
var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion<Failure>?) {
|
||||
synchronized {
|
||||
state.elementsAndCompletion
|
||||
}
|
||||
}
|
||||
|
||||
/// Use Publisher.record()
|
||||
fileprivate init() { }
|
||||
|
||||
deinit {
|
||||
if case let .subscribed(subscription, _, _) = state {
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronized<T>(_ execute: () throws -> T) rethrows -> T {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return try execute()
|
||||
}
|
||||
|
||||
// MARK: - PublisherExpectation API
|
||||
|
||||
/// Registers the expectation so that it gets fulfilled when publisher
|
||||
/// publishes elements or completes.
|
||||
///
|
||||
/// - parameter expectation: An XCTestExpectation.
|
||||
/// - parameter includingConsumed: This flag controls how elements that were
|
||||
/// already published at the time this method is called fulfill the
|
||||
/// expectation. If true, all published elements fulfill the expectation.
|
||||
/// If false, only published elements that are not consumed yet fulfill
|
||||
/// the expectation. For example, the Prefix expectation uses true, but
|
||||
/// the NextOne expectation uses false.
|
||||
func fulfillOnInput(_ expectation: XCTestExpectation, includingConsumed: Bool) {
|
||||
lock.lock()
|
||||
|
||||
preconditionCanFulfillExpectation()
|
||||
|
||||
let expectedFulfillmentCount = expectation.expectedFulfillmentCount
|
||||
|
||||
switch state {
|
||||
case .waitingForSubscription:
|
||||
let exp = RecorderExpectation.onInput(expectation, remainingCount: expectedFulfillmentCount)
|
||||
state = .waitingForSubscription(exp)
|
||||
lock.unlock()
|
||||
|
||||
case let .subscribed(subscription, _, elements):
|
||||
let maxFulfillmentCount = includingConsumed
|
||||
? elements.count
|
||||
: elements.count - consumedCount
|
||||
let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount)
|
||||
|
||||
let remainingCount = expectedFulfillmentCount - fulfillmentCount
|
||||
if remainingCount > 0 {
|
||||
let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount)
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
}
|
||||
lock.unlock()
|
||||
expectation.fulfill(count: fulfillmentCount)
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
expectation.fulfill(count: expectedFulfillmentCount)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the expectation so that it gets fulfilled when
|
||||
/// publisher completes.
|
||||
func fulfillOnCompletion(_ expectation: XCTestExpectation) {
|
||||
lock.lock()
|
||||
|
||||
preconditionCanFulfillExpectation()
|
||||
|
||||
switch state {
|
||||
case .waitingForSubscription:
|
||||
let exp = RecorderExpectation.onCompletion(expectation)
|
||||
state = .waitingForSubscription(exp)
|
||||
lock.unlock()
|
||||
|
||||
case let .subscribed(subscription, _, elements):
|
||||
let exp = RecorderExpectation.onCompletion(expectation)
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
lock.unlock()
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a value based on the recorded state of the publisher.
|
||||
///
|
||||
/// - parameter value: A function which returns the value, given the
|
||||
/// recorded state of the publisher.
|
||||
/// - parameter elements: All recorded elements.
|
||||
/// - parameter completion: The eventual publisher completion.
|
||||
/// - parameter remainingElements: The elements that were not consumed yet.
|
||||
/// - parameter consume: A function which consumes elements.
|
||||
/// - parameter count: The number of consumed elements.
|
||||
/// - returns: The value
|
||||
func value<T>(_ value: (
|
||||
_ elements: [Input],
|
||||
_ completion: Subscribers.Completion<Failure>?,
|
||||
_ remainingElements: ArraySlice<Input>,
|
||||
_ consume: (_ count: Int) -> ()) throws -> T)
|
||||
rethrows -> T
|
||||
{
|
||||
try synchronized {
|
||||
let (elements, completion) = state.elementsAndCompletion
|
||||
let remainingElements = elements[consumedCount...]
|
||||
return try value(elements, completion, remainingElements, { count in
|
||||
precondition(count >= 0)
|
||||
precondition(count <= remainingElements.count)
|
||||
consumedCount += count
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that recorder can fulfill an expectation.
|
||||
///
|
||||
/// The reason this method exists is that a recorder can fulfill a single
|
||||
/// expectation at a given time. It is a programmer error to wait for two
|
||||
/// expectations concurrently.
|
||||
///
|
||||
/// This method MUST be called within a synchronized block.
|
||||
private func preconditionCanFulfillExpectation() {
|
||||
if let exp = state.recorderExpectation {
|
||||
// We are already waiting for an expectation! Is it a programmer
|
||||
// error? Recorder drops references to non-inverted expectations
|
||||
// when they are fulfilled. But inverted expectations are not
|
||||
// fulfilled, and thus not dropped. We can't quite know if an
|
||||
// inverted expectations has expired yet, so just let it go.
|
||||
precondition(exp.expectation.isInverted, "Already waiting for an expectation")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscriber
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
synchronized {
|
||||
switch state {
|
||||
case let .waitingForSubscription(exp):
|
||||
state = .subscribed(subscription, exp, [])
|
||||
default:
|
||||
XCTFail("Publisher recorder is already subscribed")
|
||||
}
|
||||
}
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
public func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
|
||||
switch state {
|
||||
case let .subscribed(subscription, exp, elements):
|
||||
var elements = elements
|
||||
elements.append(input)
|
||||
|
||||
if case let .onInput(expectation, remainingCount: remainingCount) = exp {
|
||||
assert(remainingCount > 0)
|
||||
expectation.fulfill()
|
||||
if remainingCount > 1 {
|
||||
let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount - 1)
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
} else {
|
||||
state = .subscribed(subscription, nil, elements)
|
||||
}
|
||||
} else {
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
return .unlimited
|
||||
|
||||
case .waitingForSubscription:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected input before subscription: \(String(reflecting: input))")
|
||||
return .none
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected input after completion: \(String(reflecting: input))")
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
|
||||
switch state {
|
||||
case let .subscribed(_, exp, elements):
|
||||
if let exp {
|
||||
switch exp {
|
||||
case let .onCompletion(expectation):
|
||||
expectation.fulfill()
|
||||
case let .onInput(expectation, remainingCount: remainingCount):
|
||||
expectation.fulfill(count: remainingCount)
|
||||
}
|
||||
}
|
||||
state = .completed(elements, completion)
|
||||
lock.unlock()
|
||||
|
||||
case .waitingForSubscription:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected completion before subscription: \(String(describing: completion))")
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected completion after completion: \(String(describing: completion))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Publisher Expectations
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// The type of the publisher expectation returned by `Recorder.completion`.
|
||||
public typealias Completion<Input, Failure: Error> = Map<Recording<Input, Failure>, Subscribers.Completion<Failure>>
|
||||
|
||||
/// The type of the publisher expectation returned by `Recorder.elements`.
|
||||
public typealias Elements<Input, Failure: Error> = Map<Recording<Input, Failure>, [Input]>
|
||||
|
||||
/// The type of the publisher expectation returned by `Recorder.last`.
|
||||
public typealias Last<Input, Failure: Error> = Map<Elements<Input, Failure>, Input?>
|
||||
|
||||
/// The type of the publisher expectation returned by `Recorder.single`.
|
||||
public typealias Single<Input, Failure: Error> = Map<Elements<Input, Failure>, Input>
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension Recorder {
|
||||
/// Returns a publisher expectation which waits for the timeout to expire,
|
||||
/// or the recorded publisher to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if
|
||||
/// the publisher fails before the expectation has expired.
|
||||
///
|
||||
/// Otherwise, an array of all elements published before the expectation
|
||||
/// has expired is returned.
|
||||
///
|
||||
/// Unlike other expectations, `availableElements` does not make a test fail
|
||||
/// on timeout expiration. It just returns the elements published so far.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testTimerPublishesIncreasingDates() throws {
|
||||
/// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||
/// let recorder = publisher.record()
|
||||
/// let dates = try wait(for: recorder.availableElements, timeout: ...)
|
||||
/// XCTAssertEqual(dates.sorted(), dates)
|
||||
/// }
|
||||
public var availableElements: PublisherExpectations.AvailableElements<Input, Failure> {
|
||||
PublisherExpectations.AvailableElements(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError.notCompleted is
|
||||
/// thrown if the publisher does not complete on time.
|
||||
///
|
||||
/// Otherwise, a [Subscribers.Completion](https://developer.apple.com/documentation/combine/subscribers/completion)
|
||||
/// is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherCompletesWithSuccess() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let completion = try wait(for: recorder.completion, timeout: 1)
|
||||
/// if case let .failure(error) = completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public var completion: PublisherExpectations.Completion<Input, Failure> {
|
||||
recording.map { $0.completion }
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError.notCompleted is
|
||||
/// thrown if the publisher does not complete on time, and the publisher
|
||||
/// error is thrown if the publisher fails.
|
||||
///
|
||||
/// Otherwise, an array of published elements is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
public var elements: PublisherExpectations.Elements<Input, Failure> {
|
||||
recording.map { recording in
|
||||
if case let .failure(error) = recording.completion {
|
||||
throw error
|
||||
}
|
||||
return recording.output
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherFinishesWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished, timeout: 1)
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public var finished: PublisherExpectations.Finished<Input, Failure> {
|
||||
PublisherExpectations.Finished(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError.notCompleted is
|
||||
/// thrown if the publisher does not complete on time, and the publisher
|
||||
/// error is thrown if the publisher fails.
|
||||
///
|
||||
/// Otherwise, the last published element is returned, or nil if the publisher
|
||||
/// completes before it publishes any element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesLastElementLast() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// if let element = try wait(for: recorder.last, timeout: 1) {
|
||||
/// XCTAssertEqual(element, "baz")
|
||||
/// } else {
|
||||
/// XCTFail("Expected one element")
|
||||
/// }
|
||||
/// }
|
||||
public var last: PublisherExpectations.Last<Input, Failure> {
|
||||
elements.map { $0.last }
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to emit one element, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a `RecordingError.notEnoughElements`
|
||||
/// is thrown if the publisher does not publish one element after last
|
||||
/// waited expectation. The publisher error is thrown if the publisher fails
|
||||
/// before publishing the next element.
|
||||
///
|
||||
/// Otherwise, the next published element is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfTwoElementsPublishesElementsInOrder() throws {
|
||||
/// let publisher = ["foo", "bar"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
///
|
||||
/// element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "bar")
|
||||
/// }
|
||||
public func next() -> PublisherExpectations.NextOne<Input, Failure> {
|
||||
PublisherExpectations.NextOne(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to emit `count` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a `RecordingError.notEnoughElements`
|
||||
/// is thrown if the publisher does not publish `count` elements after last
|
||||
/// waited expectation. The publisher error is thrown if the publisher fails
|
||||
/// before publishing the next `count` elements.
|
||||
///
|
||||
/// Otherwise, an array of exactly `count` elements is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var elements = try wait(for: recorder.next(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
///
|
||||
/// elements = try wait(for: recorder.next(1), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["baz"])
|
||||
/// }
|
||||
///
|
||||
/// - parameter count: The number of elements.
|
||||
public func next(_ count: Int) -> PublisherExpectations.Next<Input, Failure> {
|
||||
PublisherExpectations.Next(recorder: self, count: count)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to emit `maxLength` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails before `maxLength` elements are published.
|
||||
///
|
||||
/// Otherwise, an array of received elements is returned, containing at
|
||||
/// most `maxLength` elements, or less if the publisher completes early.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.prefix(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// publisher.send("foo")
|
||||
/// publisher.send("bar")
|
||||
/// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
///
|
||||
/// - parameter maxLength: The maximum number of elements.
|
||||
public func prefix(_ maxLength: Int) -> PublisherExpectations.Prefix<Input, Failure> {
|
||||
PublisherExpectations.Prefix(recorder: self, maxLength: maxLength)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError.notCompleted is
|
||||
/// thrown if the publisher does not complete on time.
|
||||
///
|
||||
/// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording)
|
||||
/// is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherRecording() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let recording = try wait(for: recorder.recording, timeout: 1)
|
||||
/// XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
|
||||
/// if case let .failure(error) = recording.completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public var recording: PublisherExpectations.Recording<Input, Failure> {
|
||||
PublisherExpectations.Recording(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError is thrown if the
|
||||
/// publisher does not complete on time, or does not publish exactly one
|
||||
/// element before it completes. The publisher error is thrown if the
|
||||
/// publisher fails.
|
||||
///
|
||||
/// Otherwise, the single published element is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testJustPublishesExactlyOneElement() throws {
|
||||
/// let publisher = Just("foo")
|
||||
/// let recorder = publisher.record()
|
||||
/// let element = try wait(for: recorder.single, timeout: 1)
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
/// }
|
||||
public var single: PublisherExpectations.Single<Input, Failure> {
|
||||
elements.map { elements in
|
||||
guard let element = elements.first else {
|
||||
throw RecordingError.notEnoughElements
|
||||
}
|
||||
if elements.count > 1 {
|
||||
throw RecordingError.tooManyElements
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Publisher + Recorder
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension Publisher {
|
||||
/// Returns a subscribed Recorder.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// You can build publisher expectations from the Recorder. For example:
|
||||
///
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
public func record() -> Recorder<Output, Failure> {
|
||||
let recorder = Recorder<Output, Failure>()
|
||||
subscribe(recorder)
|
||||
return recorder
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
extension XCTestExpectation {
|
||||
fileprivate func fulfill(count: Int) {
|
||||
for _ in 0..<count {
|
||||
fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#if canImport(Combine)
|
||||
import Foundation
|
||||
|
||||
/// An error that may be thrown when waiting for publisher expectations.
|
||||
public enum RecordingError: Error {
|
||||
/// The publisher did not complete.
|
||||
case notCompleted
|
||||
|
||||
/// The publisher did not publish enough elements.
|
||||
/// For example, see `recorder.single`.
|
||||
case notEnoughElements
|
||||
|
||||
/// The publisher did publish too many elements.
|
||||
/// For example, see `recorder.single`.
|
||||
case tooManyElements
|
||||
}
|
||||
|
||||
extension RecordingError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .notCompleted:
|
||||
return "RecordingError.notCompleted"
|
||||
case .notEnoughElements:
|
||||
return "RecordingError.notEnoughElements"
|
||||
case .tooManyElements:
|
||||
return "RecordingError.tooManyElements"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
class DatabasePoolCrashTests: GRDBCrashTestCase {
|
||||
|
||||
func testReaderCanNotStartTransaction() {
|
||||
assertCrash("DatabasePool readers can not start transactions or savepoints.") {
|
||||
try dbPool.read { db in
|
||||
let statement = try db.makeStatement(sql: "BEGIN TRANSACTION")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReaderCanNotStartSavepoint() {
|
||||
assertCrash("DatabasePool readers can not start transactions or savepoints.") {
|
||||
try dbPool.read { db in
|
||||
let statement = try db.makeStatement(sql: "SAVEPOINT foo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
class DatabaseQueueCrashTests: GRDBCrashTestCase {
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - Reentrancy
|
||||
|
||||
func testInDatabaseIsNotReentrant() {
|
||||
assertCrash("Database methods are not reentrant.") {
|
||||
dbQueue.inDatabase { db in
|
||||
dbQueue.inDatabase { db in
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInTransactionInsideInDatabaseIsNotReentrant() {
|
||||
assertCrash("Database methods are not reentrant.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try dbQueue.inTransaction { db in
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInTransactionIsNotReentrant() {
|
||||
assertCrash("Database methods are not reentrant.") {
|
||||
try dbQueue.inTransaction { db in
|
||||
try dbQueue.inTransaction { db in
|
||||
return .commit
|
||||
}
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - Sequence iteration in wrong queue
|
||||
|
||||
func testRowSequenceCanNotBeGeneratedOutsideOfDatabaseQueue() {
|
||||
assertCrash("Database was not used on the correct thread: execute your statements inside DatabaseQueue.inDatabase() or DatabaseQueue.inTransaction(). If you get this error while iterating the result of a fetch() method, consider using the array returned by fetchAll() instead.") {
|
||||
var rows: DatabaseSequence<Row>?
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT)")
|
||||
rows = try Row.fetch(db, "SELECT * FROM persons")
|
||||
}
|
||||
_ = rows!.makeIterator()
|
||||
}
|
||||
}
|
||||
|
||||
func testRowSequenceCanNotBeIteratedOutsideOfDatabaseQueue() {
|
||||
assertCrash("Database was not used on the correct thread: execute your statements inside DatabaseQueue.inDatabase() or DatabaseQueue.inTransaction(). If you get this error while iterating the result of a fetch() method, consider using the array returned by fetchAll() instead.") {
|
||||
var iterator: DatabaseIterator<Row>?
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT)")
|
||||
iterator = try Row.fetch(db, "SELECT * FROM persons").makeIterator()
|
||||
}
|
||||
_ = iterator!.next()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - Concurrency
|
||||
|
||||
func testReaderCrashDuringExclusiveTransaction() {
|
||||
assertCrash("SQLite error 5 with statement `SELECT * FROM stuffs`: database is locked") {
|
||||
let dbQueue1 = try! makeDatabaseQueue(path: dbQueuepath, configuration: dbConfiguration)
|
||||
let dbQueue2 = try! makeDatabaseQueue(path: dbQueuepath, configuration: dbConfiguration)
|
||||
|
||||
try! dbQueue1.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY)")
|
||||
}
|
||||
|
||||
let queue = NSOperationQueue()
|
||||
queue.maxConcurrentOperationCount = 2
|
||||
queue.addOperation(NSBlockOperation {
|
||||
do {
|
||||
try dbQueue1.inTransaction(.exclusive) { db in
|
||||
sleep(2) // let other queue try to read.
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
catch is DatabaseError {
|
||||
}
|
||||
catch {
|
||||
XCTFail("\(error)")
|
||||
}
|
||||
})
|
||||
|
||||
queue.addOperation(NSBlockOperation {
|
||||
dbQueue2.inDatabase { db in
|
||||
sleep(1) // let other queue open transaction
|
||||
_ = try Row.fetch(db, "SELECT * FROM stuffs") // Crash expected
|
||||
}
|
||||
})
|
||||
|
||||
queue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// A type that adopts DatabaseValueConvertible but does not adopt StatementColumnConvertible
|
||||
private struct IntConvertible: DatabaseValueConvertible {
|
||||
let int: Int
|
||||
init(int: Int) {
|
||||
self.int = int
|
||||
}
|
||||
var databaseValue: DatabaseValue { int.databaseValue }
|
||||
static func fromDatabaseValue(_ dbValue: DatabaseValue) -> IntConvertible? {
|
||||
guard let int = Int.fromDatabaseValue(dbValue) else {
|
||||
return nil
|
||||
}
|
||||
return IntConvertible(int: int)
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseValueConvertibleCrashTests: GRDBCrashTestCase {
|
||||
|
||||
func testCrashFetchDatabaseValueConvertibleFromStatement() {
|
||||
assertCrash("could not convert NULL to IntConvertible.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
|
||||
let sequence = IntConvertible.fetch(statement)
|
||||
for _ in sequence { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashFetchAllDatabaseValueConvertibleFromStatement() {
|
||||
assertCrash("could not convert NULL to IntConvertible.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
|
||||
_ = IntConvertible.fetchAll(statement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashFetchDatabaseValueConvertibleFromDatabase() {
|
||||
assertCrash("could not convert NULL to IntConvertible.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
let sequence = IntConvertible.fetch(db, "SELECT int FROM ints ORDER BY int")
|
||||
for _ in sequence { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashFetchAllDatabaseValueConvertibleFromDatabase() {
|
||||
assertCrash("could not convert NULL to IntConvertible.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
_ = IntConvertible.fetchAll(db, sql: "SELECT int FROM ints ORDER BY int")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashDatabaseValueConvertibleInvalidConversionFromNULL() {
|
||||
assertCrash("could not convert NULL to IntConvertible.") {
|
||||
let row = Row(["int": nil])
|
||||
_ = row["int"] as IntConvertible
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashDatabaseValueConvertibleInvalidConversionFromInvalidType() {
|
||||
assertCrash("could not convert \"foo\" to IntConvertible") {
|
||||
let row = Row(["int": "foo"])
|
||||
_ = row["int"] as IntConvertible
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import XCTest
|
||||
|
||||
class GRDBCrashTestCase: GRDBTestCase {
|
||||
|
||||
// This method does not actually catch any crash.
|
||||
// But it expresses an intent :-)
|
||||
func assertCrash(message: String, @noescape block: () throws -> ()) {
|
||||
do {
|
||||
try block()
|
||||
XCTFail("Crash expected: \(message)")
|
||||
} catch {
|
||||
XCTFail("Unexpected error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import GRDB
|
||||
|
||||
class MigrationCrashTests: GRDBCrashTestCase {
|
||||
|
||||
func testMigrationNamesMustBeUnique() {
|
||||
assertCrash("already registered migration: \"foo\"") {
|
||||
var migrator = DatabaseMigrator()
|
||||
migrator.registerMigration("foo") { db in }
|
||||
migrator.registerMigration("foo") { db in }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private class RecordWithoutDatabaseTableName: Record { }
|
||||
|
||||
private class RecordWithInexistingDatabaseTable: Record {
|
||||
override class var databaseTableName: String {
|
||||
"foo"
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordWithEmptyPersistentDictionary : Record {
|
||||
override class var databaseTableName: String {
|
||||
"records"
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordWithNilPrimaryKey : Record {
|
||||
override class var databaseTableName: String {
|
||||
"records"
|
||||
}
|
||||
|
||||
override func encode(to container: inout PersistenceContainer) throws {
|
||||
container["id"] = nil
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordForTableWithoutPrimaryKey : Record {
|
||||
override class var databaseTableName: String {
|
||||
"records"
|
||||
}
|
||||
|
||||
override func encode(to container: inout PersistenceContainer) throws {
|
||||
container["name"] = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordForTableWithMultipleColumnsPrimaryKey : Record {
|
||||
override class var databaseTableName: String {
|
||||
"records"
|
||||
}
|
||||
|
||||
override func encode(to container: inout PersistenceContainer) throws {
|
||||
container["name"] = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
private class RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary : Record {
|
||||
override class var databaseTableName: String {
|
||||
"records"
|
||||
}
|
||||
|
||||
override func encode(to container: inout PersistenceContainer) throws {
|
||||
container["name"] = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
class RecordCrashTests: GRDBCrashTestCase {
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordWithoutDatabaseTableName
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeFetchedByID() {
|
||||
assertCrash("subclass must override") {
|
||||
dbQueue.inDatabase { db in
|
||||
_ = RecordWithoutDatabaseTableName.fetchOne(db, key: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeFetchedByKey() {
|
||||
assertCrash("subclass must override") {
|
||||
dbQueue.inDatabase { db in
|
||||
_ = RecordWithoutDatabaseTableName.fetchOne(db, key: ["foo": "bar"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeInserted() {
|
||||
assertCrash("subclass must override") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithoutDatabaseTableName().insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeUpdated() {
|
||||
assertCrash("subclass must override") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithoutDatabaseTableName().update(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeSaved() {
|
||||
assertCrash("subclass must override") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithoutDatabaseTableName().save(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeDeleted() {
|
||||
assertCrash("subclass must override") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithoutDatabaseTableName().delete(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithoutDatabaseTableNameCanNotBeTestedForExistence() {
|
||||
assertCrash("subclass must override") {
|
||||
dbQueue.inDatabase { db in
|
||||
RecordWithoutDatabaseTableName().exists(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordWithInexistingDatabaseTable
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeFetchedByID() {
|
||||
assertCrash("no such table: foo") {
|
||||
dbQueue.inDatabase { db in
|
||||
_ = RecordWithInexistingDatabaseTable.fetchOne(db, key: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeFetchedByKey() {
|
||||
assertCrash("SQLite error 1 with statement `SELECT * FROM \"foo\" WHERE (\"id\" = ?)`: no such table: foo") {
|
||||
dbQueue.inDatabase { db in
|
||||
_ = RecordWithInexistingDatabaseTable.fetchOne(db, key: ["id": 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeInserted() {
|
||||
assertCrash("no such table: foo") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithInexistingDatabaseTable().insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeUpdated() {
|
||||
assertCrash("no such table: foo") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithInexistingDatabaseTable().update(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeSaved() {
|
||||
assertCrash("no such table: foo") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithInexistingDatabaseTable().save(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeDeleted() {
|
||||
assertCrash("no such table: foo") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try RecordWithInexistingDatabaseTable().delete(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithInexistingDatabaseTableCanNotBeTestedForExistence() {
|
||||
assertCrash("no such table: foo") {
|
||||
dbQueue.inDatabase { db in
|
||||
RecordWithInexistingDatabaseTable().exists(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordWithEmptyPersistentDictionary
|
||||
|
||||
func testRecordWithEmptyPersistentDictionaryCanNotBeInserted() {
|
||||
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
try RecordWithEmptyPersistentDictionary().insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithEmptyPersistentDictionaryCanNotBeUpdated() {
|
||||
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
try RecordWithEmptyPersistentDictionary().update(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithEmptyPersistentDictionaryCanNotBeSaved() {
|
||||
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
try RecordWithEmptyPersistentDictionary().save(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithEmptyPersistentDictionaryCanNotBeDeleted() {
|
||||
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
try RecordWithEmptyPersistentDictionary().delete(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithEmptyPersistentDictionaryCanNotBeTestedForExistence() {
|
||||
assertCrash("RecordWithEmptyPersistentDictionary.persistentDictionary: invalid empty dictionary") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
RecordWithEmptyPersistentDictionary().exists(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordWithNilPrimaryKey
|
||||
|
||||
func testRecordWithNilPrimaryKeyCanNotBeUpdated() {
|
||||
assertCrash("invalid primary key in <RecordWithNilPrimaryKey id:nil>") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
try RecordWithNilPrimaryKey().update(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithNilPrimaryKeyCanNotBeDeleted() {
|
||||
assertCrash("invalid primary key in <RecordWithNilPrimaryKey id:nil>") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
try RecordWithNilPrimaryKey().delete(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordWithNilPrimaryKeyCanNotBeTestedForExistence() {
|
||||
assertCrash("invalid primary key in <RecordWithNilPrimaryKey id:nil>") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY)")
|
||||
RecordWithNilPrimaryKey().exists(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordForTableWithoutPrimaryKey
|
||||
|
||||
func testRecordForTableWithoutPrimaryKeyCanNotBeFetchedByID() {
|
||||
assertCrash("expected single column primary key in table: records") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (name TEXT)")
|
||||
_ = RecordForTableWithoutPrimaryKey.fetchOne(db, key: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordForTableWithoutPrimaryKeyCanNotBeUpdated() {
|
||||
assertCrash("invalid primary key in <RecordForTableWithoutPrimaryKey name:\"foo\">") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (name TEXT)")
|
||||
try RecordForTableWithoutPrimaryKey().update(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordForTableWithoutPrimaryKeyCanNotBeDeleted() {
|
||||
assertCrash("invalid primary key in <RecordForTableWithoutPrimaryKey name:\"foo\">") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (name TEXT)")
|
||||
try RecordForTableWithoutPrimaryKey().delete(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRecordForTableWithoutPrimaryKeyCanNotBeTestedForExistence() {
|
||||
assertCrash("invalid primary key in <RecordForTableWithoutPrimaryKey name:\"foo\">") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (name TEXT)")
|
||||
RecordForTableWithoutPrimaryKey().exists(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordForTableWithMultipleColumnsPrimaryKey
|
||||
|
||||
func testRecordForTableWithMultipleColumnsPrimaryKeyCanNotBeFetchedByID() {
|
||||
assertCrash("expected single column primary key in table: records") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (a TEXT, b TEXT, PRIMARY KEY(a,b))")
|
||||
_ = RecordForTableWithMultipleColumnsPrimaryKey.fetchOne(db, key: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary
|
||||
|
||||
func testRecordWithRowIDPrimaryKeyNotExposedInPersistentDictionaryCanNotBeInserted() {
|
||||
assertCrash("invalid primary key in <RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary name:\"foo\">") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE records (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
try RecordWithRowIDPrimaryKeyNotExposedInPersistentDictionary().update(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
class StatementColumnConvertibleCrashTests: GRDBCrashTestCase {
|
||||
|
||||
func testCrashFetchStatementColumnConvertibleFromStatement() {
|
||||
assertCrash("could not convert NULL to Int.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
|
||||
let sequence = try Int.fetch(statement)
|
||||
for _ in sequence { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashFetchAllStatementColumnConvertibleFromStatement() {
|
||||
assertCrash("could not convert NULL to Int.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
let statement = try db.makeStatement(sql: "SELECT int FROM ints ORDER BY int")
|
||||
_ = try Int.fetchAll(statement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashFetchStatementColumnConvertibleFromDatabase() {
|
||||
assertCrash("could not convert NULL to Int.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
let sequence = try Int.fetch(db, "SELECT int FROM ints ORDER BY int")
|
||||
for _ in sequence { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashFetchAllStatementColumnConvertibleFromDatabase() {
|
||||
assertCrash("could not convert NULL to Int.") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE ints (int Int)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (1)")
|
||||
try db.execute(sql: "INSERT INTO ints (int) VALUES (NULL)")
|
||||
|
||||
_ = try Int.fetchAll(db, sql: "SELECT int FROM ints ORDER BY int")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
class StatementCrashTests: GRDBCrashTestCase {
|
||||
|
||||
func testInvalidStatementArguments() {
|
||||
assertCrash("SQLite error 1 with statement `INSERT INTO persons (name, age) VALUES (:name, :age)`: missing statement argument(s): age") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try! db.execute(sql: "INSERT INTO persons (name, age) VALUES (:name, :age)", arguments: ["name": "Arthur"])
|
||||
}
|
||||
}
|
||||
|
||||
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Arthur', ?);`: wrong number of statement arguments: 0") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try db.execute(sql: """
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
|
||||
INSERT INTO persons (name, age) VALUES ('Barbara', ?);
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Barbara', ?);`: wrong number of statement arguments: 0") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try db.execute(sql: """
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
|
||||
INSERT INTO persons (name, age) VALUES ('Barbara', ?);
|
||||
""", arguments: [41])
|
||||
}
|
||||
}
|
||||
|
||||
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Arthur', :age1);`: missing statement argument(s): age1") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try db.execute(sql: """
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', :age1);
|
||||
INSERT INTO persons (name, age) VALUES ('Barbara', :age2);
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
assertCrash("SQLite error 21 with statement `INSERT INTO persons (name, age) VALUES ('Barbara', :age2);`: missing statement argument(s): age2") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try db.execute(sql: """
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', :age1);
|
||||
INSERT INTO persons (name, age) VALUES ('Barbara', :age2);
|
||||
""", arguments: ["age1": 41])
|
||||
}
|
||||
}
|
||||
|
||||
assertCrash("SQLite error 21: wrong number of statement arguments: 3") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try db.execute(sql: """
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', :age1);
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', :age2);
|
||||
""", arguments: [41, 32, 17])
|
||||
}
|
||||
}
|
||||
|
||||
assertCrash("SQLite error 21: wrong number of statement arguments: 3") {
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.execute(sql: "CREATE TABLE persons (name TEXT, age INT)")
|
||||
try db.execute(sql: """
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
|
||||
INSERT INTO persons (name, age) VALUES ('Arthur', ?);
|
||||
""", arguments: [41, 32, 17])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
56F5F6C62349F11E00C4D9F7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F5F6C52349F11E00C4D9F7 /* AppDelegate.swift */; };
|
||||
56F5F6C82349F11E00C4D9F7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F5F6C72349F11E00C4D9F7 /* ViewController.swift */; };
|
||||
56F5F6CA2349F11F00C4D9F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 56F5F6C92349F11F00C4D9F7 /* Assets.xcassets */; };
|
||||
56F5F6CD2349F11F00C4D9F7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 56F5F6CB2349F11F00C4D9F7 /* Main.storyboard */; };
|
||||
56F5F6E82349F26400C4D9F7 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56F5F6DE2349F22A00C4D9F7 /* GRDB.framework */; };
|
||||
56F5F6E92349F26500C4D9F7 /* GRDB.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 56F5F6DE2349F22A00C4D9F7 /* GRDB.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
56F5F6DD2349F22A00C4D9F7 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = F3BA805A1CFB2BB2003DC1BA;
|
||||
remoteInfo = GRDBCustomSQLiteOSX;
|
||||
};
|
||||
56F5F6DF2349F22A00C4D9F7 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = F3BA809B1CFB2F6F003DC1BA;
|
||||
remoteInfo = GRDBCustomSQLiteOSXTests;
|
||||
};
|
||||
56F5F6E12349F22A00C4D9F7 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = F3BA7FFE1CFB25E4003DC1BA;
|
||||
remoteInfo = GRDBCustomSQLiteiOS;
|
||||
};
|
||||
56F5F6E32349F22A00C4D9F7 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = F3BA80411CFB2AD7003DC1BA;
|
||||
remoteInfo = GRDBCustomSQLiteiOSTests;
|
||||
};
|
||||
56F5F6E52349F24D00C4D9F7 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = F3BA80591CFB2BB2003DC1BA;
|
||||
remoteInfo = GRDBCustomSQLiteOSX;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
56F5F6EA2349F26500C4D9F7 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
56F5F6E92349F26500C4D9F7 /* GRDB.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
56E987D9243EF667005CFD0D /* GRDBCustomSQLite-INSTALL.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "GRDBCustomSQLite-INSTALL.sh"; sourceTree = "<group>"; };
|
||||
56E987DA243EF667005CFD0D /* SQLiteLib-USER.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "SQLiteLib-USER.xcconfig"; sourceTree = "<group>"; };
|
||||
56E987DB243EF667005CFD0D /* GRDBCustomSQLite-USER.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "GRDBCustomSQLite-USER.xcconfig"; sourceTree = "<group>"; };
|
||||
56E987DC243EF667005CFD0D /* GRDBCustomSQLite-USER.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "GRDBCustomSQLite-USER.h"; sourceTree = "<group>"; };
|
||||
56F5F6C22349F11E00C4D9F7 /* CustomSQLite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CustomSQLite.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
56F5F6C52349F11E00C4D9F7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
56F5F6C72349F11E00C4D9F7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
56F5F6C92349F11F00C4D9F7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
56F5F6CC2349F11F00C4D9F7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
56F5F6CE2349F11F00C4D9F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
56F5F6CF2349F11F00C4D9F7 /* CustomSQLite.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CustomSQLite.entitlements; sourceTree = "<group>"; };
|
||||
56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GRDBCustom.xcodeproj; path = ../../GRDBCustom.xcodeproj; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
56F5F6BF2349F11E00C4D9F7 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56F5F6E82349F26400C4D9F7 /* GRDB.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
56E987D8243EF667005CFD0D /* GRDBCustomSQLite */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56E987D9243EF667005CFD0D /* GRDBCustomSQLite-INSTALL.sh */,
|
||||
56E987DA243EF667005CFD0D /* SQLiteLib-USER.xcconfig */,
|
||||
56E987DB243EF667005CFD0D /* GRDBCustomSQLite-USER.xcconfig */,
|
||||
56E987DC243EF667005CFD0D /* GRDBCustomSQLite-USER.h */,
|
||||
);
|
||||
path = GRDBCustomSQLite;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56F5F6B92349F11E00C4D9F7 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */,
|
||||
56E987D8243EF667005CFD0D /* GRDBCustomSQLite */,
|
||||
56F5F6C42349F11E00C4D9F7 /* CustomSQLite */,
|
||||
56F5F6C32349F11E00C4D9F7 /* Products */,
|
||||
56F5F6E72349F26400C4D9F7 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56F5F6C32349F11E00C4D9F7 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56F5F6C22349F11E00C4D9F7 /* CustomSQLite.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56F5F6C42349F11E00C4D9F7 /* CustomSQLite */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56F5F6C52349F11E00C4D9F7 /* AppDelegate.swift */,
|
||||
56F5F6C72349F11E00C4D9F7 /* ViewController.swift */,
|
||||
56F5F6C92349F11F00C4D9F7 /* Assets.xcassets */,
|
||||
56F5F6CB2349F11F00C4D9F7 /* Main.storyboard */,
|
||||
56F5F6CE2349F11F00C4D9F7 /* Info.plist */,
|
||||
56F5F6CF2349F11F00C4D9F7 /* CustomSQLite.entitlements */,
|
||||
);
|
||||
path = CustomSQLite;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56F5F6D62349F22A00C4D9F7 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
56F5F6DE2349F22A00C4D9F7 /* GRDB.framework */,
|
||||
56F5F6E02349F22A00C4D9F7 /* GRDBOSXTests.xctest */,
|
||||
56F5F6E22349F22A00C4D9F7 /* GRDB.framework */,
|
||||
56F5F6E42349F22A00C4D9F7 /* GRDBiOSTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56F5F6E72349F26400C4D9F7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
56F5F6C12349F11E00C4D9F7 /* CustomSQLite */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 56F5F6D22349F11F00C4D9F7 /* Build configuration list for PBXNativeTarget "CustomSQLite" */;
|
||||
buildPhases = (
|
||||
56F5F6BE2349F11E00C4D9F7 /* Sources */,
|
||||
56F5F6BF2349F11E00C4D9F7 /* Frameworks */,
|
||||
56F5F6C02349F11E00C4D9F7 /* Resources */,
|
||||
56F5F6EA2349F26500C4D9F7 /* Embed Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
56F5F6E62349F24D00C4D9F7 /* PBXTargetDependency */,
|
||||
);
|
||||
name = CustomSQLite;
|
||||
productName = CustomSQLite;
|
||||
productReference = 56F5F6C22349F11E00C4D9F7 /* CustomSQLite.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
56F5F6BA2349F11E00C4D9F7 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1110;
|
||||
LastUpgradeCheck = 1110;
|
||||
ORGANIZATIONNAME = "Gwendal Roué";
|
||||
TargetAttributes = {
|
||||
56F5F6C12349F11E00C4D9F7 = {
|
||||
CreatedOnToolsVersion = 11.1;
|
||||
LastSwiftMigration = 1110;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 56F5F6BD2349F11E00C4D9F7 /* Build configuration list for PBXProject "CustomSQLite" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 56F5F6B92349F11E00C4D9F7;
|
||||
productRefGroup = 56F5F6C32349F11E00C4D9F7 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectReferences = (
|
||||
{
|
||||
ProductGroup = 56F5F6D62349F22A00C4D9F7 /* Products */;
|
||||
ProjectRef = 56F5F6D52349F22A00C4D9F7 /* GRDBCustom.xcodeproj */;
|
||||
},
|
||||
);
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
56F5F6C12349F11E00C4D9F7 /* CustomSQLite */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXReferenceProxy section */
|
||||
56F5F6DE2349F22A00C4D9F7 /* GRDB.framework */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.framework;
|
||||
path = GRDB.framework;
|
||||
remoteRef = 56F5F6DD2349F22A00C4D9F7 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
56F5F6E02349F22A00C4D9F7 /* GRDBOSXTests.xctest */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.cfbundle;
|
||||
path = GRDBOSXTests.xctest;
|
||||
remoteRef = 56F5F6DF2349F22A00C4D9F7 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
56F5F6E22349F22A00C4D9F7 /* GRDB.framework */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.framework;
|
||||
path = GRDB.framework;
|
||||
remoteRef = 56F5F6E12349F22A00C4D9F7 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
56F5F6E42349F22A00C4D9F7 /* GRDBiOSTests.xctest */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.cfbundle;
|
||||
path = GRDBiOSTests.xctest;
|
||||
remoteRef = 56F5F6E32349F22A00C4D9F7 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
/* End PBXReferenceProxy section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
56F5F6C02349F11E00C4D9F7 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56F5F6CA2349F11F00C4D9F7 /* Assets.xcassets in Resources */,
|
||||
56F5F6CD2349F11F00C4D9F7 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
56F5F6BE2349F11E00C4D9F7 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56F5F6C82349F11E00C4D9F7 /* ViewController.swift in Sources */,
|
||||
56F5F6C62349F11E00C4D9F7 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
56F5F6E62349F24D00C4D9F7 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = GRDBCustomSQLiteOSX;
|
||||
targetProxy = 56F5F6E52349F24D00C4D9F7 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
56F5F6CB2349F11F00C4D9F7 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
56F5F6CC2349F11F00C4D9F7 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
56F5F6D02349F11F00C4D9F7 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
56F5F6D12349F11F00C4D9F7 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.13;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
56F5F6D32349F11F00C4D9F7 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = CustomSQLite/CustomSQLite.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = CustomSQLite/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.CustomSQLite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
56F5F6D42349F11F00C4D9F7 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = CustomSQLite/CustomSQLite.entitlements;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = CustomSQLite/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.github.groue.CustomSQLite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
56F5F6BD2349F11E00C4D9F7 /* Build configuration list for PBXProject "CustomSQLite" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
56F5F6D02349F11F00C4D9F7 /* Debug */,
|
||||
56F5F6D12349F11F00C4D9F7 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
56F5F6D22349F11F00C4D9F7 /* Build configuration list for PBXNativeTarget "CustomSQLite" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
56F5F6D32349F11F00C4D9F7 /* Debug */,
|
||||
56F5F6D42349F11F00C4D9F7 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 56F5F6BA2349F11E00C4D9F7 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:CustomSQLite.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1110"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Script"
|
||||
scriptText = "source "${PROJECT_DIR}/GRDBCustomSQLite/GRDBCustomSQLite-INSTALL.sh" ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "56F5F6C12349F11E00C4D9F7"
|
||||
BuildableName = "CustomSQLite.app"
|
||||
BlueprintName = "CustomSQLite"
|
||||
ReferencedContainer = "container:CustomSQLite.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "56F5F6C12349F11E00C4D9F7"
|
||||
BuildableName = "CustomSQLite.app"
|
||||
BlueprintName = "CustomSQLite"
|
||||
ReferencedContainer = "container:CustomSQLite.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "56F5F6C12349F11E00C4D9F7"
|
||||
BuildableName = "CustomSQLite.app"
|
||||
BlueprintName = "CustomSQLite"
|
||||
ReferencedContainer = "container:CustomSQLite.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "56F5F6C12349F11E00C4D9F7"
|
||||
BuildableName = "CustomSQLite.app"
|
||||
BlueprintName = "CustomSQLite"
|
||||
ReferencedContainer = "container:CustomSQLite.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,11 @@
|
||||
import Cocoa
|
||||
import GRDB
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
_ = try! DatabaseQueue()
|
||||
_ = FTS5()
|
||||
_ = sqlite3_preupdate_new(nil, 0, nil)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
+717
@@ -0,0 +1,717 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
<scene sceneID="JPo-4y-FX3">
|
||||
<objects>
|
||||
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="CustomSQLite" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="CustomSQLite" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About CustomSQLite" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide CustomSQLite" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit CustomSQLite" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="File" id="dMs-cI-mzQ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||
<items>
|
||||
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
|
||||
<connections>
|
||||
<action selector="newDocument:" target="Ady-hI-5gd" id="4Si-XN-c54"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
|
||||
<connections>
|
||||
<action selector="openDocument:" target="Ady-hI-5gd" id="bVn-NM-KNZ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Open Recent" id="tXI-mr-wws">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
|
||||
<items>
|
||||
<menuItem title="Clear Menu" id="vNY-rz-j42">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="clearRecentDocuments:" target="Ady-hI-5gd" id="Daa-9d-B3U"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||
<connections>
|
||||
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
|
||||
<connections>
|
||||
<action selector="saveDocument:" target="Ady-hI-5gd" id="teZ-XB-qJY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
|
||||
<connections>
|
||||
<action selector="saveDocumentAs:" target="Ady-hI-5gd" id="mDf-zr-I0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
|
||||
<connections>
|
||||
<action selector="revertDocumentToSaved:" target="Ady-hI-5gd" id="iJ3-Pv-kwq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
|
||||
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="runPageLayout:" target="Ady-hI-5gd" id="Din-rz-gC5"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
|
||||
<connections>
|
||||
<action selector="print:" target="Ady-hI-5gd" id="qaZ-4w-aoO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||
<items>
|
||||
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||
<connections>
|
||||
<action selector="undo:" target="Ady-hI-5gd" id="M6e-cu-g7V"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||
<connections>
|
||||
<action selector="redo:" target="Ady-hI-5gd" id="oIA-Rs-6OD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||
<connections>
|
||||
<action selector="cut:" target="Ady-hI-5gd" id="YJe-68-I9s"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||
<connections>
|
||||
<action selector="copy:" target="Ady-hI-5gd" id="G1f-GL-Joy"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||
<connections>
|
||||
<action selector="paste:" target="Ady-hI-5gd" id="UvS-8e-Qdg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteAsPlainText:" target="Ady-hI-5gd" id="cEh-KX-wJQ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="Ady-hI-5gd" id="0Mk-Ml-PaM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||
<connections>
|
||||
<action selector="selectAll:" target="Ady-hI-5gd" id="VNm-Mi-diN"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||
<menuItem title="Find" id="4EN-yA-p0u">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||
<items>
|
||||
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="cD7-Qs-BN4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="WD3-Gg-5AJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="NDo-RZ-v9R"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="HOh-sY-3ay"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||
<connections>
|
||||
<action selector="performFindPanelAction:" target="Ady-hI-5gd" id="U76-nv-p5D"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||
<connections>
|
||||
<action selector="centerSelectionInVisibleArea:" target="Ady-hI-5gd" id="IOG-6D-g5B"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||
<items>
|
||||
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||
<connections>
|
||||
<action selector="showGuessPanel:" target="Ady-hI-5gd" id="vFj-Ks-hy3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||
<connections>
|
||||
<action selector="checkSpelling:" target="Ady-hI-5gd" id="fz7-VC-reM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleContinuousSpellChecking:" target="Ady-hI-5gd" id="7w6-Qz-0kB"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleGrammarChecking:" target="Ady-hI-5gd" id="muD-Qn-j4w"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticSpellingCorrection:" target="Ady-hI-5gd" id="2lM-Qi-WAP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||
<items>
|
||||
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontSubstitutionsPanel:" target="Ady-hI-5gd" id="oku-mr-iSq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleSmartInsertDelete:" target="Ady-hI-5gd" id="3IJ-Se-DZD"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticQuoteSubstitution:" target="Ady-hI-5gd" id="ptq-xd-QOA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDashSubstitution:" target="Ady-hI-5gd" id="oCt-pO-9gS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticLinkDetection:" target="Ady-hI-5gd" id="Gip-E3-Fov"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticDataDetection:" target="Ady-hI-5gd" id="R1I-Nq-Kbl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleAutomaticTextReplacement:" target="Ady-hI-5gd" id="DvP-Fe-Py6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||
<items>
|
||||
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="uppercaseWord:" target="Ady-hI-5gd" id="sPh-Tk-edu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowercaseWord:" target="Ady-hI-5gd" id="iUZ-b5-hil"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="capitalizeWord:" target="Ady-hI-5gd" id="26H-TL-nsh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||
<items>
|
||||
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="startSpeaking:" target="Ady-hI-5gd" id="654-Ng-kyl"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="stopSpeaking:" target="Ady-hI-5gd" id="dX8-6p-jy9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Format" id="jxT-CU-nIS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
|
||||
<items>
|
||||
<menuItem title="Font" id="Gi5-1S-RQB">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
|
||||
<items>
|
||||
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
|
||||
<connections>
|
||||
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
|
||||
<connections>
|
||||
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
|
||||
<connections>
|
||||
<action selector="underline:" target="Ady-hI-5gd" id="FYS-2b-JAY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
|
||||
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
|
||||
<connections>
|
||||
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
|
||||
<menuItem title="Kern" id="jBQ-r6-VK2">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="GUa-eO-cwY">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardKerning:" target="Ady-hI-5gd" id="6dk-9l-Ckg"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="cDB-IK-hbR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffKerning:" target="Ady-hI-5gd" id="U8a-gz-Maa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Tighten" id="46P-cB-AYj">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="tightenKerning:" target="Ady-hI-5gd" id="hr7-Nz-8ro"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Loosen" id="ogc-rX-tC1">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="loosenKerning:" target="Ady-hI-5gd" id="8i4-f9-FKE"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Ligatures" id="o6e-r0-MWq">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="agt-UL-0e3">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useStandardLigatures:" target="Ady-hI-5gd" id="7uR-wd-Dx6"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use None" id="J7y-lM-qPV">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="turnOffLigatures:" target="Ady-hI-5gd" id="iX2-gA-Ilz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Use All" id="xQD-1f-W4t">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="useAllLigatures:" target="Ady-hI-5gd" id="KcB-kA-TuK"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Baseline" id="OaQ-X3-Vso">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
|
||||
<items>
|
||||
<menuItem title="Use Default" id="3Om-Ey-2VK">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unscript:" target="Ady-hI-5gd" id="0vZ-95-Ywn"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Superscript" id="Rqc-34-cIF">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="superscript:" target="Ady-hI-5gd" id="3qV-fo-wpU"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Subscript" id="I0S-gh-46l">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="subscript:" target="Ady-hI-5gd" id="Q6W-4W-IGz"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Raise" id="2h7-ER-AoG">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="raiseBaseline:" target="Ady-hI-5gd" id="4sk-31-7Q9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Lower" id="1tx-W0-xDw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="lowerBaseline:" target="Ady-hI-5gd" id="OF1-bc-KW4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
|
||||
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
|
||||
<connections>
|
||||
<action selector="orderFrontColorPanel:" target="Ady-hI-5gd" id="mSX-Xz-DV3"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
|
||||
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyFont:" target="Ady-hI-5gd" id="GJO-xA-L4q"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteFont:" target="Ady-hI-5gd" id="JfD-CL-leO"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Text" id="Fal-I4-PZk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Text" id="d9c-me-L2H">
|
||||
<items>
|
||||
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
|
||||
<connections>
|
||||
<action selector="alignLeft:" target="Ady-hI-5gd" id="zUv-R1-uAa"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
|
||||
<connections>
|
||||
<action selector="alignCenter:" target="Ady-hI-5gd" id="spX-mk-kcS"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Justify" id="J5U-5w-g23">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="alignJustified:" target="Ady-hI-5gd" id="ljL-7U-jND"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
|
||||
<connections>
|
||||
<action selector="alignRight:" target="Ady-hI-5gd" id="r48-bG-YeY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
|
||||
<menuItem title="Writing Direction" id="H1b-Si-o9J">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
|
||||
<items>
|
||||
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="YGs-j5-SAR">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionNatural:" target="Ady-hI-5gd" id="qtV-5e-UBP"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="Lbh-J2-qVU">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="S0X-9S-QSf"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="jFq-tB-4Kx">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeBaseWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="5fk-qB-AqJ"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
|
||||
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem id="Nop-cj-93Q">
|
||||
<string key="title"> Default</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionNatural:" target="Ady-hI-5gd" id="lPI-Se-ZHp"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="BgM-ve-c93">
|
||||
<string key="title"> Left to Right</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionLeftToRight:" target="Ady-hI-5gd" id="caW-Bv-w94"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem id="RB4-Sm-HuC">
|
||||
<string key="title"> Right to Left</string>
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="makeTextWritingDirectionRightToLeft:" target="Ady-hI-5gd" id="EXD-6r-ZUu"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
|
||||
<menuItem title="Show Ruler" id="vLm-3I-IUL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleRuler:" target="Ady-hI-5gd" id="FOx-HJ-KwY"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="copyRuler:" target="Ady-hI-5gd" id="71i-fW-3W2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="pasteRuler:" target="Ady-hI-5gd" id="cSh-wd-qM2"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<items>
|
||||
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleToolbarShown:" target="Ady-hI-5gd" id="BXY-wc-z0C"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="runToolbarCustomizationPalette:" target="Ady-hI-5gd" id="pQI-g3-MTW"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
|
||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="toggleFullScreen:" target="Ady-hI-5gd" id="dU3-MA-1Rq"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Window" id="aUF-d1-5bR">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||
<items>
|
||||
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||
<connections>
|
||||
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="CustomSQLite Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="0.0"/>
|
||||
</scene>
|
||||
<!--Window Controller-->
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
<connections>
|
||||
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
|
||||
</connections>
|
||||
</windowController>
|
||||
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="250"/>
|
||||
</scene>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2015-2020 Gwendal Roué. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
<true/>
|
||||
<key>NSSupportsSuddenTermination</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import Cocoa
|
||||
|
||||
class ViewController: NSViewController { }
|
||||
@@ -0,0 +1 @@
|
||||
../..
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# The path to the folder containing GRDBCustom.xcodeproj:
|
||||
GRDB_SOURCE_PATH="${PROJECT_DIR}/GRDB"
|
||||
|
||||
# The path to your custom "SQLiteLib-USER.xcconfig":
|
||||
SQLITELIB_XCCONFIG_USER_PATH="${PROJECT_DIR}/GRDBCustomSQLite/SQLiteLib-USER.xcconfig"
|
||||
|
||||
# The path to your custom "GRDBCustomSQLite-USER.xcconfig":
|
||||
CUSTOMSQLITE_XCCONFIG_USER_PATH="${PROJECT_DIR}/GRDBCustomSQLite/GRDBCustomSQLite-USER.xcconfig"
|
||||
|
||||
# The path to your custom "GRDBCustomSQLite-USER.h":
|
||||
CUSTOMSQLITE_H_USER_PATH="${PROJECT_DIR}/GRDBCustomSQLite/GRDBCustomSQLite-USER.h"
|
||||
|
||||
#######################################################
|
||||
#
|
||||
#######################################################
|
||||
|
||||
|
||||
if [ ! -d "$GRDB_SOURCE_PATH" ];
|
||||
then
|
||||
echo "error: Path to GRDB source (GRDB_SOURCE_PATH) missing/incorrect: $GRDB_SOURCE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SyncFileChanges () {
|
||||
SOURCE=$1
|
||||
DESTINATIONPATH=$2
|
||||
DESTINATIONFILENAME=$3
|
||||
DESTINATION="${DESTINATIONPATH}/${DESTINATIONFILENAME}"
|
||||
|
||||
if [ ! -f "$SOURCE" ];
|
||||
then
|
||||
echo "error: Source file missing: $SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rsync -a "$SOURCE" "$DESTINATION"
|
||||
}
|
||||
|
||||
SyncFileChanges $SQLITELIB_XCCONFIG_USER_PATH "${GRDB_SOURCE_PATH}/SQLiteCustom/src" "SQLiteLib-USER.xcconfig"
|
||||
SyncFileChanges $CUSTOMSQLITE_XCCONFIG_USER_PATH "${GRDB_SOURCE_PATH}/SQLiteCustom" "GRDBCustomSQLite-USER.xcconfig"
|
||||
SyncFileChanges $CUSTOMSQLITE_H_USER_PATH "${GRDB_SOURCE_PATH}/SQLiteCustom" "GRDBCustomSQLite-USER.h"
|
||||
|
||||
echo "Finished syncing"
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
#define SQLITE_ENABLE_FTS5
|
||||
#define SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
+1
@@ -0,0 +1 @@
|
||||
CUSTOM_OTHER_SWIFT_FLAGS = -D SQLITE_ENABLE_FTS5 -D SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
+1
@@ -0,0 +1 @@
|
||||
CUSTOM_SQLLIBRARY_CFLAGS = -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_PREUPDATE_HOOK
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import GRDB
|
||||
import XCTest
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
var score: Int?
|
||||
|
||||
static func createTable(_ db: Database) throws {
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseReaderReadPublisherTests : XCTestCase {
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testReadPublisher() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(reader: some DatabaseReader) throws {
|
||||
let publisher = reader.readPublisher(value: { db in
|
||||
try Player.fetchCount(db)
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
let value = try wait(for: recorder.single, timeout: 5)
|
||||
XCTAssertEqual(value, 0)
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// TODO: fix crasher
|
||||
//
|
||||
// * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x20)
|
||||
// frame #0: 0x00007fff7115c6e8 libobjc.A.dylib`objc_retain + 24
|
||||
// frame #1: 0x00007fff71c313a1 libswiftCore.dylib`swift::metadataimpl::ValueWitnesses<swift::metadataimpl::ObjCRetainableBox>::initializeWithCopy(swift::OpaqueValue*, swift::OpaqueValue*, swift::TargetMetadata<swift::InProcess> const*) + 17
|
||||
// frame #2: 0x000000010d926309 GRDB`outlined init with copy of Subscribers.Completion<A.Publisher.Failure> at <compiler-generated>:0
|
||||
// frame #3: 0x000000010d92474b GRDB`ReceiveValuesOnSubscription._receive(completion=failure, self=0x00000001064c13c0) at ReceiveValuesOn.swift:184:9
|
||||
// frame #4: 0x000000010d92392c GRDB`closure #1 in closure #1 in closure #1 in ReceiveValuesOnSubscription.receive(self=0x00000001064c13c0, completion=failure) at ReceiveValuesOn.swift:158:26
|
||||
// frame #5: 0x00007fff71d88f49 libswiftDispatch.dylib`reabstraction thunk helper from @escaping @callee_guaranteed () -> () to @escaping @callee_unowned @convention(block) () -> () + 25
|
||||
// frame #6: 0x00007fff722b76c4 libdispatch.dylib`_dispatch_call_block_and_release + 12
|
||||
// frame #7: 0x00007fff722b8658 libdispatch.dylib`_dispatch_client_callout + 8
|
||||
// frame #8: 0x00007fff722c3cab libdispatch.dylib`_dispatch_main_queue_callback_4CF + 936
|
||||
// frame #9: 0x00007fff38299e81 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
|
||||
// frame #10: 0x00007fff38259c87 CoreFoundation`__CFRunLoopRun + 2028
|
||||
// frame #11: 0x00007fff38258e3e CoreFoundation`CFRunLoopRunSpecific + 462
|
||||
// frame #12: 0x00000001003aa82b XCTest`-[XCTWaiter waitForExpectations:timeout:enforceOrder:] + 823
|
||||
// frame #13: 0x0000000100324a04 XCTest`-[XCTestCase(AsynchronousTesting) waitForExpectations:timeout:enforceOrder:] + 102
|
||||
// frame #14: 0x000000010961363d GRDBOSXTests`XCTestCase.wait<R>(publisherExpectation=GRDBOSXTests.PublisherExpectations.Recording<Swift.Array<GRDB.Row>, Swift.Error> @ 0x00007ffeefbfd2f0, timeout=1, description="", self=0x0000000101d8a9c0) at PublisherExpectation.swift:97:9
|
||||
// frame #15: 0x000000010915dbcf GRDBOSXTests`test #1 (reader=0x000000010672f210, self=0x0000000101d8a9c0) in DatabaseReaderReadPublisherTests.testReadPublisherError() at DatabaseReaderReadPublisherTests.swift:62:33
|
||||
// frame #16: 0x000000010915dfc0 GRDBOSXTests`partial apply for test #1 (reader:) in DatabaseReaderReadPublisherTests.testReadPublisherError() at <compiler-generated>:0
|
||||
// frame #17: 0x000000010915c334 GRDBOSXTests`thunk for @escaping @callee_guaranteed (@guaranteed DatabaseReader) -> (@error @owned Error) at <compiler-generated>:0
|
||||
// frame #18: 0x000000010915e014 GRDBOSXTests`thunk for @escaping @callee_guaranteed (@guaranteed DatabaseReader) -> (@error @owned Error)partial apply at <compiler-generated>:0
|
||||
// frame #19: 0x00000001095a85f8 GRDBOSXTests`closure #1 in Test.init(context=0x000000010672f210, _0=<unavailable>, test=0x000000010915e000 GRDBOSXTests`reabstraction thunk helper from @escaping @callee_guaranteed (@guaranteed GRDB.DatabaseReader) -> (@error @owned Swift.Error) to @escaping @callee_guaranteed (@in_guaranteed GRDB.DatabaseReader) -> (@error @owned Swift.Error)partial apply forwarder with unmangled suffix ".16" at <compiler-generated>) at Support.swift:13:41
|
||||
// frame #20: 0x00000001095a86af GRDBOSXTests`partial apply for closure #1 in Test.init(repeatCount:_:) at <compiler-generated>:0
|
||||
// frame #21: 0x00000001095a8a8f GRDBOSXTests`Test.run(context=0x000000010915e880 GRDBOSXTests`reabstraction thunk helper from @callee_guaranteed () -> (@owned GRDB.DatabaseReader, @error @owned Swift.Error) to @escaping @callee_guaranteed () -> (@out GRDB.DatabaseReader, @error @owned Swift.Error)partial apply forwarder with unmangled suffix ".17" at <compiler-generated>, self=(repeatCount = 1, test = 0x00000001095a8690 GRDBOSXTests`partial apply forwarder for closure #1 (A, Swift.Int) throws -> () in GRDBOSXTests.Test.init(repeatCount: Swift.Int, _: (A) throws -> ()) -> GRDBOSXTests.Test<A> at <compiler-generated>)) at Support.swift:24:17
|
||||
// * frame #22: 0x000000010915d4ec GRDBOSXTests`DatabaseReaderReadPublisherTests.testReadPublisherError(self=0x0000000101d8a9c0) at DatabaseReaderReadPublisherTests.swift:71:14
|
||||
// frame #23: 0x000000010915f26a GRDBOSXTests`@objc DatabaseReaderReadPublisherTests.testReadPublisherError() at <compiler-generated>:0
|
||||
// frame #24: 0x00007fff3823c8ac CoreFoundation`__invoking___ + 140
|
||||
// frame #25: 0x00007fff3823c751 CoreFoundation`-[NSInvocation invoke] + 303
|
||||
// frame #26: 0x0000000100339d3a XCTest`__24-[XCTestCase invokeTest]_block_invoke_3 + 52
|
||||
// frame #27: 0x0000000100402215 XCTest`+[XCTSwiftErrorObservation observeErrorsInBlock:] + 69
|
||||
// frame #28: 0x0000000100339c3c XCTest`__24-[XCTestCase invokeTest]_block_invoke_2 + 119
|
||||
// frame #29: 0x00000001003c959a XCTest`-[XCTMemoryChecker _assertInvalidObjectsDeallocatedAfterScope:] + 65
|
||||
// frame #30: 0x00000001003448ea XCTest`-[XCTestCase assertInvalidObjectsDeallocatedAfterScope:] + 61
|
||||
// frame #31: 0x0000000100339b82 XCTest`__24-[XCTestCase invokeTest]_block_invoke.231 + 199
|
||||
// frame #32: 0x00000001003ae6d8 XCTest`-[XCTestCase(XCTIssueHandling) _caughtUnhandledDeveloperExceptionPermittingControlFlowInterruptions:caughtInterruptionException:whileExecutingBlock:] + 179
|
||||
// frame #33: 0x0000000100339645 XCTest`-[XCTestCase invokeTest] + 1037
|
||||
// frame #34: 0x000000010033b023 XCTest`__26-[XCTestCase performTest:]_block_invoke_2 + 43
|
||||
// frame #35: 0x00000001003ae6d8 XCTest`-[XCTestCase(XCTIssueHandling) _caughtUnhandledDeveloperExceptionPermittingControlFlowInterruptions:caughtInterruptionException:whileExecutingBlock:] + 179
|
||||
// frame #36: 0x000000010033af5a XCTest`__26-[XCTestCase performTest:]_block_invoke.362 + 86
|
||||
// frame #37: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
|
||||
// frame #38: 0x000000010033a7c7 XCTest`-[XCTestCase performTest:] + 695
|
||||
// frame #39: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
|
||||
// frame #40: 0x0000000100334035 XCTest`__27-[XCTestSuite performTest:]_block_invoke + 329
|
||||
// frame #41: 0x0000000100333856 XCTest`__59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
|
||||
// frame #42: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
|
||||
// frame #43: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
|
||||
// frame #44: 0x000000010033380d XCTest`-[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
|
||||
// frame #45: 0x0000000100333b11 XCTest`-[XCTestSuite performTest:] + 290
|
||||
// frame #46: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
|
||||
// frame #47: 0x0000000100334035 XCTest`__27-[XCTestSuite performTest:]_block_invoke + 329
|
||||
// frame #48: 0x0000000100333856 XCTest`__59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
|
||||
// frame #49: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
|
||||
// frame #50: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
|
||||
// frame #51: 0x000000010033380d XCTest`-[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
|
||||
// frame #52: 0x0000000100333b11 XCTest`-[XCTestSuite performTest:] + 290
|
||||
// frame #53: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
|
||||
// frame #54: 0x0000000100334035 XCTest`__27-[XCTestSuite performTest:]_block_invoke + 329
|
||||
// frame #55: 0x0000000100333856 XCTest`__59-[XCTestSuite _performProtectedSectionForTest:testSection:]_block_invoke + 24
|
||||
// frame #56: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
|
||||
// frame #57: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
|
||||
// frame #58: 0x000000010033380d XCTest`-[XCTestSuite _performProtectedSectionForTest:testSection:] + 148
|
||||
// frame #59: 0x0000000100333b11 XCTest`-[XCTestSuite performTest:] + 290
|
||||
// frame #60: 0x000000010038c6da XCTest`-[XCTest runTest] + 57
|
||||
// frame #61: 0x00000001003dc8b5 XCTest`__44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke_2 + 148
|
||||
// frame #62: 0x00000001003bfb9f XCTest`+[XCTContext runInContextForTestCase:markAsReportingBase:block:] + 220
|
||||
// frame #63: 0x00000001003bfab0 XCTest`+[XCTContext runInContextForTestCase:block:] + 52
|
||||
// frame #64: 0x00000001003dc81a XCTest`__44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke + 111
|
||||
// frame #65: 0x00000001003dc99b XCTest`__44-[XCTTestRunSession runTestsAndReturnError:]_block_invoke.95 + 96
|
||||
// frame #66: 0x000000010035acb8 XCTest`-[XCTestObservationCenter _observeTestExecutionForBlock:] + 325
|
||||
// frame #67: 0x00000001003dc5e0 XCTest`-[XCTTestRunSession runTestsAndReturnError:] + 615
|
||||
// frame #68: 0x0000000100317a7e XCTest`-[XCTestDriver _runTests] + 466
|
||||
// frame #69: 0x00000001003bbb82 XCTest`_XCTestMain + 108
|
||||
// frame #70: 0x0000000100002f07 xctest`main + 210
|
||||
// frame #71: 0x00007fff72311cc9 libdyld.dylib`start + 1
|
||||
// frame #72: 0x00007fff72311cc9 libdyld.dylib`start + 1
|
||||
func testReadPublisherError() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(reader: some DatabaseReader) throws {
|
||||
let publisher = reader.readPublisher(value: { db in
|
||||
try Row.fetchAll(db, sql: "THIS IS NOT SQL")
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
||||
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() }
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() }
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testReadPublisherIsAsynchronous() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(reader: some DatabaseReader) throws {
|
||||
let expectation = self.expectation(description: "")
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let cancellable = reader
|
||||
.readPublisher(value: { db in
|
||||
try Player.fetchCount(db)
|
||||
})
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in
|
||||
semaphore.wait()
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
semaphore.signal()
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testReadPublisherDefaultScheduler() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(reader: some DatabaseReader) {
|
||||
let expectation = self.expectation(description: "")
|
||||
let cancellable = reader
|
||||
.readPublisher(value: { db in
|
||||
try Player.fetchCount(db)
|
||||
})
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
expectation.fulfill()
|
||||
},
|
||||
receiveValue: { _ in
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testReadPublisherCustomScheduler() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(reader: some DatabaseReader) {
|
||||
let queue = DispatchQueue(label: "test")
|
||||
let expectation = self.expectation(description: "")
|
||||
let cancellable = reader
|
||||
.readPublisher(receiveOn: queue, value: { db in
|
||||
try Player.fetchCount(db)
|
||||
})
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
dispatchPrecondition(condition: .onQueue(queue))
|
||||
expectation.fulfill()
|
||||
},
|
||||
receiveValue: { _ in
|
||||
dispatchPrecondition(condition: .onQueue(queue))
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshot() }
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)).makeSnapshotPool() }
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testReadPublisherIsReadonly() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(reader: some DatabaseReader) throws {
|
||||
let publisher = reader.readPublisher(value: { db in
|
||||
try Player.createTable(db)
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_READONLY)
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshot() }
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0).makeSnapshotPool() }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import GRDB
|
||||
import XCTest
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
var score: Int?
|
||||
|
||||
static func createTable(_ db: Database) throws {
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseRegionObservationPublisherTests : XCTestCase {
|
||||
|
||||
func testChangesNotifications() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = DatabaseRegionObservation(tracking: Player.all())
|
||||
.publisher(in: writer)
|
||||
.tryMap(Player.fetchCount)
|
||||
let recorder = publisher.record()
|
||||
|
||||
try writer.writeWithoutTransaction { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
|
||||
try db.inTransaction {
|
||||
try Player(id: 2, name: "Barbara", score: 750).insert(db)
|
||||
try Player(id: 3, name: "Craig", score: 500).insert(db)
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
|
||||
let elements = try wait(for: recorder.next(2), timeout: 1)
|
||||
XCTAssertEqual(elements, [1, 3])
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// This is an usage test. Do the available APIs allow to prepend a
|
||||
// database connection synchronously, with the guarantee that no race can
|
||||
// have the subscriber miss an impactful change?
|
||||
//
|
||||
// TODO: do the same, but asynchronously. If this is too hard, update the
|
||||
// public API so that users can easily do it.
|
||||
func testPrependInitialDatabaseSync() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let expectation = self.expectation(description: "")
|
||||
let testSubject = PassthroughSubject<Database, Error>()
|
||||
let testCancellable = testSubject
|
||||
.tryMap(Player.fetchCount)
|
||||
.collect(3)
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
assertNoFailure(completion)
|
||||
},
|
||||
receiveValue: { value in
|
||||
XCTAssertEqual(value, [0, 1, 3])
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
let observationCancellable = try writer.write { db in
|
||||
DatabaseRegionObservation(tracking: Player.all())
|
||||
.publisher(in: writer)
|
||||
.prepend(db)
|
||||
.subscribe(testSubject)
|
||||
}
|
||||
|
||||
try writer.writeWithoutTransaction { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
|
||||
try db.inTransaction {
|
||||
try Player(id: 2, name: "Barbara", score: 750).insert(db)
|
||||
try Player(id: 3, name: "Craig", score: 500).insert(db)
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
testCancellable.cancel()
|
||||
observationCancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import GRDB
|
||||
import XCTest
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
var score: Int?
|
||||
|
||||
static func createTable(_ db: Database) throws {
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseWriterWritePublisherTests : XCTestCase {
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWritePublisher() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
try XCTAssertEqual(writer.read(Player.fetchCount), 0)
|
||||
let publisher = writer.writePublisher(updates: { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
try wait(for: recorder.single, timeout: 5)
|
||||
try XCTAssertEqual(writer.read(Player.fetchCount), 1)
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWritePublisherValue() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer.writePublisher(updates: { db -> Int in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
let count = try wait(for: recorder.single, timeout: 5)
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWritePublisherError() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer.writePublisher(updates: { db in
|
||||
try db.execute(sql: "THIS IS NOT SQL")
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
||||
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
}
|
||||
|
||||
func testWritePublisherErrorRollbacksTransaction() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer.writePublisher(updates: { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
try db.execute(sql: "THIS IS NOT SQL")
|
||||
})
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
||||
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
|
||||
}
|
||||
let count = try writer.read(Player.fetchCount)
|
||||
XCTAssertEqual(count, 0)
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWritePublisherIsAsynchronous() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let expectation = self.expectation(description: "")
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let cancellable = writer
|
||||
.writePublisher(updates: { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
})
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in
|
||||
semaphore.wait()
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
semaphore.signal()
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWritePublisherDefaultScheduler() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test<Writer: DatabaseWriter>(writer: Writer) {
|
||||
let expectation = self.expectation(description: "")
|
||||
expectation.expectedFulfillmentCount = 2 // value + completion
|
||||
let cancellable = writer
|
||||
.writePublisher(updates: { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
})
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
expectation.fulfill()
|
||||
},
|
||||
receiveValue: { _ in
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWritePublisherCustomScheduler() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test<Writer: DatabaseWriter>(writer: Writer) {
|
||||
let queue = DispatchQueue(label: "test")
|
||||
let expectation = self.expectation(description: "")
|
||||
expectation.expectedFulfillmentCount = 2 // value + completion
|
||||
let cancellable = writer
|
||||
.writePublisher(receiveOn: queue, updates: { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
})
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
dispatchPrecondition(condition: .onQueue(queue))
|
||||
expectation.fulfill()
|
||||
},
|
||||
receiveValue: { _ in
|
||||
dispatchPrecondition(condition: .onQueue(queue))
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
waitForExpectations(timeout: 5, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// TODO: Fix flaky test with both pool and on-disk queue:
|
||||
// - Expectation timeout
|
||||
func testWriteThenReadPublisher() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer
|
||||
.writePublisher(
|
||||
updates: { db in try Player(id: 1, name: "Arthur", score: 1000).insert(db) },
|
||||
thenRead: { db, _ in try Player.fetchCount(db) })
|
||||
let recorder = publisher.record()
|
||||
let count = try wait(for: recorder.single, timeout: 5)
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWriteThenReadPublisherIsReadonly() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer
|
||||
.writePublisher(
|
||||
updates: { _ in },
|
||||
thenRead: { db, _ in try Player.createTable(db) })
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_READONLY)
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
func testWriteThenReadPublisherWriteError() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer.writePublisher(
|
||||
updates: { db in try db.execute(sql: "THIS IS NOT SQL") },
|
||||
thenRead: { _, _ in XCTFail("Should not read") })
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
||||
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
}
|
||||
|
||||
func testWriteThenReadPublisherWriteErrorRollbacksTransaction() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer.writePublisher(
|
||||
updates: { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
try db.execute(sql: "THIS IS NOT SQL")
|
||||
},
|
||||
thenRead: { _, _ in XCTFail("Should not read") })
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
||||
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
|
||||
}
|
||||
let count = try writer.read(Player.fetchCount)
|
||||
XCTAssertEqual(count, 0)
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// TODO: Fix flaky test with both pool and on-disk queue:
|
||||
// - Expectation timeout
|
||||
func testWriteThenReadPublisherReadError() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = writer.writePublisher(
|
||||
updates: { _ in },
|
||||
thenRead: { db, _ in try Row.fetchAll(db, sql: "THIS IS NOT SQL") })
|
||||
let recorder = publisher.record()
|
||||
let recording = try wait(for: recorder.recording, timeout: 5)
|
||||
XCTAssertTrue(recording.output.isEmpty)
|
||||
assertFailure(recording.completion) { (error: DatabaseError) in
|
||||
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
|
||||
XCTAssertEqual(error.sql, "THIS IS NOT SQL")
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Regression tests
|
||||
|
||||
// Regression test against deadlocks created by concurrent completion
|
||||
// and cancellations triggered by .switchToLatest().prefix(1)
|
||||
func testDeadlockPrevention() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter, iteration: Int) throws {
|
||||
// print(iteration)
|
||||
let scoreSubject = PassthroughSubject<Int, Error>()
|
||||
let publisher = scoreSubject
|
||||
.map { score in
|
||||
writer.writePublisher { db -> Int in
|
||||
try Player(id: 1, name: "Arthur", score: score).insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
}
|
||||
.switchToLatest()
|
||||
.prefix(1)
|
||||
let recorder = publisher.record()
|
||||
scoreSubject.send(0)
|
||||
let count = try wait(for: recorder.single, timeout: 5)
|
||||
XCTAssertEqual(count, 1)
|
||||
}
|
||||
|
||||
try Test(repeatCount: 100, test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(repeatCount: 100, test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(repeatCount: 100, test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class Test<Context> {
|
||||
// Raise the repeatCount in order to help spotting flaky tests.
|
||||
private let repeatCount: Int
|
||||
private let test: (Context, Int) throws -> ()
|
||||
|
||||
init(repeatCount: Int = 1, _ test: @escaping (Context) throws -> ()) {
|
||||
self.repeatCount = repeatCount
|
||||
self.test = { context, _ in try test(context) }
|
||||
}
|
||||
|
||||
init(repeatCount: Int, _ test: @escaping (Context, Int) throws -> ()) {
|
||||
self.repeatCount = repeatCount
|
||||
self.test = test
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func run(context: () throws -> Context) throws -> Self {
|
||||
for i in 1...repeatCount {
|
||||
try test(context(), i)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runInTemporaryDirectory(context: (_ directoryURL: URL) throws -> Context) throws -> Self {
|
||||
for i in 1...repeatCount {
|
||||
let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
.appendingPathComponent("GRDB", isDirectory: true)
|
||||
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true)
|
||||
|
||||
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: directoryURL)
|
||||
}
|
||||
|
||||
try test(context(directoryURL), i)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runAtTemporaryDatabasePath(context: (_ path: String) throws -> Context) throws -> Self {
|
||||
try runInTemporaryDirectory { url in
|
||||
try context(url.appendingPathComponent("db.sqlite").path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
final class AsyncTest<Context> {
|
||||
// Raise the repeatCount in order to help spotting flaky tests.
|
||||
private let repeatCount: Int
|
||||
private let test: (Context, Int) async throws -> ()
|
||||
|
||||
init(repeatCount: Int = 1, _ test: @escaping (Context) async throws -> ()) {
|
||||
self.repeatCount = repeatCount
|
||||
self.test = { context, _ in try await test(context) }
|
||||
}
|
||||
|
||||
init(repeatCount: Int, _ test: @escaping (Context, Int) async throws -> ()) {
|
||||
self.repeatCount = repeatCount
|
||||
self.test = test
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func run(context: () async throws -> Context) async throws -> Self {
|
||||
for i in 1...repeatCount {
|
||||
try await test(context(), i)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runInTemporaryDirectory(context: (_ directoryURL: URL) async throws -> Context) async throws -> Self {
|
||||
for i in 1...repeatCount {
|
||||
let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
.appendingPathComponent("GRDB", isDirectory: true)
|
||||
.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: true)
|
||||
|
||||
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
defer {
|
||||
try! FileManager.default.removeItem(at: directoryURL)
|
||||
}
|
||||
|
||||
try await test(context(directoryURL), i)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func runAtTemporaryDatabasePath(context: (_ path: String) async throws -> Context) async throws -> Self {
|
||||
try await runInTemporaryDirectory { url in
|
||||
try await context(url.appendingPathComponent("db.sqlite").path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public func assertNoFailure<Failure>(
|
||||
_ completion: Subscribers.Completion<Failure>,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
{
|
||||
if case let .failure(error) = completion {
|
||||
XCTFail("Unexpected completion failure: \(error)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public func assertFailure<Failure, ExpectedFailure>(
|
||||
_ completion: Subscribers.Completion<Failure>,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
test: (ExpectedFailure) -> Void)
|
||||
{
|
||||
if case let .failure(error) = completion, let expectedError = error as? ExpectedFailure {
|
||||
test(expectedError)
|
||||
} else {
|
||||
XCTFail("Expected \(ExpectedFailure.self), got \(completion)", file: file, line: line)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import GRDB
|
||||
import XCTest
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
var score: Int?
|
||||
|
||||
static func createTable(_ db: Database) throws {
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ValueObservationPublisherTests : XCTestCase {
|
||||
|
||||
// MARK: - Default Scheduler
|
||||
|
||||
func testDefaultSchedulerChangesNotifications() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer)
|
||||
let recorder = publisher.record()
|
||||
|
||||
try writer.writeWithoutTransaction { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
|
||||
try db.inTransaction {
|
||||
try Player(id: 2, name: "Barbara", score: 750).insert(db)
|
||||
try Player(id: 3, name: "Craig", score: 500).insert(db)
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
|
||||
let expectedElements = [0, 1, 3]
|
||||
if writer is DatabaseQueue {
|
||||
let elements = try wait(for: recorder.next(expectedElements.count), timeout: 1)
|
||||
XCTAssertEqual(elements, expectedElements)
|
||||
} else {
|
||||
// TODO: prefix(until:)
|
||||
let elements = try wait(for: recorder.prefix(expectedElements.count + 2).inverted, timeout: 1)
|
||||
assertValueObservationRecordingMatch(recorded: elements, expected: expectedElements)
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testDefaultSchedulerFirstValueIsEmittedAsynchronously() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let expectation = self.expectation(description: "")
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let cancellable = ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in
|
||||
semaphore.wait()
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
semaphore.signal()
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
cancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testDefaultSchedulerError() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = ValueObservation
|
||||
.trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") }
|
||||
.publisher(in: writer)
|
||||
let recorder = publisher.record()
|
||||
let completion = try wait(for: recorder.completion, timeout: 1)
|
||||
switch completion {
|
||||
case let .failure(error):
|
||||
XCTAssertNotNil(error as? DatabaseError)
|
||||
case .finished:
|
||||
XCTFail("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Immediate Scheduler
|
||||
|
||||
func testImmediateSchedulerChangesNotifications() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer, scheduling: .immediate)
|
||||
let recorder = publisher.record()
|
||||
|
||||
try writer.writeWithoutTransaction { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
|
||||
try db.inTransaction {
|
||||
try Player(id: 2, name: "Barbara", score: 750).insert(db)
|
||||
try Player(id: 3, name: "Craig", score: 500).insert(db)
|
||||
return .commit
|
||||
}
|
||||
}
|
||||
|
||||
let expectedElements = [0, 1, 3]
|
||||
if writer is DatabaseQueue {
|
||||
let elements = try wait(for: recorder.next(expectedElements.count), timeout: 1)
|
||||
XCTAssertEqual(elements, expectedElements)
|
||||
} else {
|
||||
// TODO: prefix(until:)
|
||||
let elements = try wait(for: recorder.prefix(expectedElements.count + 2).inverted, timeout: 1)
|
||||
assertValueObservationRecordingMatch(recorded: elements, expected: expectedElements)
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testImmediateSchedulerEmitsFirstValueSynchronously() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let testSubject = PassthroughSubject<Int, Error>()
|
||||
let testCancellable = testSubject
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in
|
||||
dispatchPrecondition(condition: .onQueue(.main))
|
||||
semaphore.signal()
|
||||
})
|
||||
|
||||
let observationCancellable = ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer, scheduling: .immediate)
|
||||
.subscribe(testSubject)
|
||||
|
||||
semaphore.wait()
|
||||
testCancellable.cancel()
|
||||
observationCancellable.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testImmediateSchedulerError() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let publisher = ValueObservation
|
||||
.trackingConstantRegion { try $0.execute(sql: "THIS IS NOT SQL") }
|
||||
.publisher(in: writer, scheduling: .immediate)
|
||||
let recorder = publisher.record()
|
||||
let completion = try recorder.completion.get()
|
||||
switch completion {
|
||||
case let .failure(error):
|
||||
XCTAssertNotNil(error as? DatabaseError)
|
||||
case .finished:
|
||||
XCTFail("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
try Test(test).run { try DatabaseQueue() }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
|
||||
}
|
||||
|
||||
// MARK: - Demand
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
private class DemandSubscriber<Input, Failure: Error>: Subscriber {
|
||||
private var subscription: Subscription?
|
||||
let subject = PassthroughSubject<Input, Failure>()
|
||||
deinit {
|
||||
subscription?.cancel()
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
subscription!.cancel()
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
subscription!.request(demand)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
subject.send(input)
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
subject.send(completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func testDemandNoneReceivesNoElement() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let subscriber = DemandSubscriber<Int, Error>()
|
||||
|
||||
let expectation = self.expectation(description: "")
|
||||
expectation.isInverted = true
|
||||
let testCancellable = subscriber.subject
|
||||
.sink(
|
||||
receiveCompletion: { _ in XCTFail("Unexpected completion") },
|
||||
receiveValue: { _ in expectation.fulfill() })
|
||||
|
||||
ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer)
|
||||
.subscribe(subscriber)
|
||||
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
testCancellable.cancel()
|
||||
subscriber.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testDemandOneReceivesOneElement() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let subscriber = DemandSubscriber<Int, Error>()
|
||||
let expectation = self.expectation(description: "")
|
||||
|
||||
let testCancellable = subscriber.subject.sink(
|
||||
receiveCompletion: { _ in XCTFail("Unexpected completion") },
|
||||
receiveValue: { value in
|
||||
XCTAssertEqual(value, 0)
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer)
|
||||
.subscribe(subscriber)
|
||||
|
||||
subscriber.request(.max(1))
|
||||
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
testCancellable.cancel()
|
||||
subscriber.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testDemandOneDoesNotReceiveTwoElements() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let subscriber = DemandSubscriber<Int, Error>()
|
||||
let expectation = self.expectation(description: "")
|
||||
expectation.isInverted = true
|
||||
|
||||
let testCancellable = subscriber.subject
|
||||
.collect(2)
|
||||
.sink(
|
||||
receiveCompletion: { _ in XCTFail("Unexpected completion") },
|
||||
receiveValue: { _ in expectation.fulfill() })
|
||||
|
||||
ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer, scheduling: .immediate /* make sure we get the initial db state */)
|
||||
.subscribe(subscriber)
|
||||
|
||||
subscriber.request(.max(1))
|
||||
|
||||
try writer.writeWithoutTransaction { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
testCancellable.cancel()
|
||||
subscriber.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
func testDemandTwoReceivesTwoElements() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
func setUp<Writer: DatabaseWriter>(_ writer: Writer) throws -> Writer {
|
||||
try writer.write(Player.createTable)
|
||||
return writer
|
||||
}
|
||||
|
||||
func test(writer: some DatabaseWriter) throws {
|
||||
let subscriber = DemandSubscriber<Int, Error>()
|
||||
let expectation = self.expectation(description: "")
|
||||
|
||||
let testCancellable = subscriber.subject
|
||||
.collect(2)
|
||||
.sink(
|
||||
receiveCompletion: { _ in XCTFail("Unexpected completion") },
|
||||
receiveValue: { values in
|
||||
expectation.fulfill()
|
||||
})
|
||||
|
||||
ValueObservation
|
||||
.trackingConstantRegion(Player.fetchCount)
|
||||
.publisher(in: writer, scheduling: .immediate /* make sure we get two db states */)
|
||||
.subscribe(subscriber)
|
||||
|
||||
subscriber.request(.max(2))
|
||||
|
||||
try writer.writeWithoutTransaction { db in
|
||||
try Player(id: 1, name: "Arthur", score: 1000).insert(db)
|
||||
}
|
||||
|
||||
waitForExpectations(timeout: 1, handler: nil)
|
||||
testCancellable.cancel()
|
||||
subscriber.cancel()
|
||||
}
|
||||
|
||||
try Test(test).run { try setUp(DatabaseQueue()) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabaseQueue(path: $0)) }
|
||||
try Test(test).runAtTemporaryDatabasePath { try setUp(DatabasePool(path: $0)) }
|
||||
}
|
||||
|
||||
// MARK: - Regression Tests
|
||||
|
||||
/// Regression test for https://github.com/groue/GRDB.swift/issues/1194
|
||||
func testIssue1194() throws {
|
||||
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
|
||||
throw XCTSkip("Combine is not available")
|
||||
}
|
||||
|
||||
struct Record: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
}
|
||||
|
||||
var configuration = Configuration()
|
||||
configuration.targetQueue = DispatchQueue(label: "crash.test", qos: .userInitiated)
|
||||
|
||||
let database = try DatabaseQueue(configuration: configuration)
|
||||
|
||||
var migrator = DatabaseMigrator()
|
||||
migrator.registerMigration("v1") { (db) in
|
||||
try db.create(table: Record.databaseTableName) { (t) in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
}
|
||||
|
||||
try migrator.migrate(database)
|
||||
|
||||
let observation = ValueObservation.tracking { (db) in
|
||||
try Record.fetchCount(db)
|
||||
}
|
||||
|
||||
let exp = expectation(description: "")
|
||||
let cancellable = observation.publisher(in: database, scheduling: .immediate)
|
||||
.map { _ in
|
||||
database.readPublisher { (db) in
|
||||
try Record.fetchCount(db)
|
||||
}
|
||||
}
|
||||
.switchToLatest()
|
||||
.sink(receiveCompletion: { _ in },
|
||||
receiveValue: { (value) in
|
||||
exp.fulfill()
|
||||
})
|
||||
|
||||
withExtendedLifetime(cancellable) {
|
||||
waitForExpectations(timeout: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct TestError : Error { }
|
||||
|
||||
class AnyCursorTests: GRDBTestCase {
|
||||
|
||||
func testAnyCursorFromClosure() {
|
||||
var i = 0
|
||||
let cursor: AnyCursor<Int> = AnyCursor {
|
||||
guard i < 2 else { return nil }
|
||||
defer { i += 1 }
|
||||
return i
|
||||
}
|
||||
XCTAssertEqual(try cursor.next()!, 0)
|
||||
XCTAssertEqual(try cursor.next()!, 1)
|
||||
XCTAssertTrue(try cursor.next() == nil) // end
|
||||
}
|
||||
|
||||
func testAnyCursorFromThrowingClosure() {
|
||||
var i = 0
|
||||
let cursor: AnyCursor<Int> = AnyCursor {
|
||||
guard i < 2 else { throw TestError() }
|
||||
defer { i += 1 }
|
||||
return i
|
||||
}
|
||||
XCTAssertEqual(try cursor.next()!, 0)
|
||||
XCTAssertEqual(try cursor.next()!, 1)
|
||||
do {
|
||||
_ = try cursor.next()
|
||||
XCTFail()
|
||||
} catch is TestError {
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testAnyCursorFromCursor() {
|
||||
let base = AnyCursor([0, 1])
|
||||
// This helper function makes sure AnyCursor initializer accepts any cursor,
|
||||
// and not only AnyCursor:
|
||||
func makeAnyCursor<C: Cursor>(_ cursor: C) -> AnyCursor<Int> where C.Element == Int {
|
||||
AnyCursor(cursor)
|
||||
}
|
||||
let cursor = AnyCursor(base)
|
||||
XCTAssertEqual(try cursor.next()!, 0)
|
||||
XCTAssertEqual(try cursor.next()!, 1)
|
||||
XCTAssertTrue(try cursor.next() == nil) // end
|
||||
}
|
||||
|
||||
func testAnyCursorFromThrowingCursor() {
|
||||
var i = 0
|
||||
let base: AnyCursor<Int> = AnyCursor {
|
||||
guard i < 2 else { throw TestError() }
|
||||
defer { i += 1 }
|
||||
return i
|
||||
}
|
||||
func makeAnyCursor<C: Cursor>(_ cursor: C) -> AnyCursor<Int> where C.Element == Int {
|
||||
AnyCursor(cursor)
|
||||
}
|
||||
let cursor = makeAnyCursor(base)
|
||||
XCTAssertEqual(try cursor.next()!, 0)
|
||||
XCTAssertEqual(try cursor.next()!, 1)
|
||||
do {
|
||||
_ = try cursor.next()
|
||||
XCTFail()
|
||||
} catch is TestError {
|
||||
} catch {
|
||||
XCTFail()
|
||||
}
|
||||
}
|
||||
|
||||
func testAnyCursorFromIterator() {
|
||||
let cursor = AnyCursor(iterator: [0, 1].makeIterator())
|
||||
XCTAssertEqual(try cursor.next()!, 0)
|
||||
XCTAssertEqual(try cursor.next()!, 1)
|
||||
XCTAssertTrue(try cursor.next() == nil) // end
|
||||
XCTAssertTrue(try cursor.next() == nil) // past the end
|
||||
}
|
||||
|
||||
func testAnyCursorFromSequence() {
|
||||
let cursor = AnyCursor([0, 1])
|
||||
XCTAssertEqual(try cursor.next()!, 0)
|
||||
XCTAssertEqual(try cursor.next()!, 1)
|
||||
XCTAssertTrue(try cursor.next() == nil) // end
|
||||
XCTAssertTrue(try cursor.next() == nil) // past the end
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+173
@@ -0,0 +1,173 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct Team: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "teams"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "players"
|
||||
static let team = Player.belongsTo(Team.self)
|
||||
var id: Int64
|
||||
var teamId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct PlayerWithRequiredTeam: Decodable, FetchableRecord {
|
||||
var player: Player
|
||||
var team: Team
|
||||
static let team = Player.team.forKey(CodingKeys.team)
|
||||
}
|
||||
|
||||
private struct PlayerWithOptionalTeam: Decodable, FetchableRecord {
|
||||
var player: Player
|
||||
var team: Team?
|
||||
static let team = Player.team.forKey(CodingKeys.team)
|
||||
}
|
||||
|
||||
private struct PlayerWithTeamName: Decodable, FetchableRecord {
|
||||
var player: Player
|
||||
var teamName: String?
|
||||
}
|
||||
|
||||
private extension QueryInterfaceRequest<Player> {
|
||||
func filter(teamName: String) -> QueryInterfaceRequest<Player> {
|
||||
joining(required: PlayerWithOptionalTeam.team.filter(Column("name") == teamName))
|
||||
}
|
||||
|
||||
func orderedByTeamName() -> QueryInterfaceRequest<Player> {
|
||||
let teamAlias = TableAlias()
|
||||
return joining(optional: PlayerWithOptionalTeam.team.aliased(teamAlias))
|
||||
.order(teamAlias[Column("name")], Column("name"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test support for Decodable records
|
||||
class AssociationBelongsToDecodableRecordTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "teams") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "players") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.belongsTo("team")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try Team(id: 1, name: "Reds").insert(db)
|
||||
try Player(id: 1, teamId: 1, name: "Arthur").insert(db)
|
||||
try Player(id: 2, teamId: nil, name: "Barbara").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(required: PlayerWithRequiredTeam.team)
|
||||
.asRequest(of: PlayerWithRequiredTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].team.id, 1)
|
||||
XCTAssertEqual(records[0].team.name, "Reds")
|
||||
}
|
||||
|
||||
func testAnnotatedWithRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.annotated(withRequired: Player.team.select(Column("name").forKey("teamName")))
|
||||
.asRequest(of: PlayerWithTeamName.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].teamName, "Reds")
|
||||
}
|
||||
|
||||
func testIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(optional: PlayerWithOptionalTeam.team)
|
||||
.asRequest(of: PlayerWithOptionalTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].team!.id, 1)
|
||||
XCTAssertEqual(records[0].team!.name, "Reds")
|
||||
XCTAssertEqual(records[1].player.id, 2)
|
||||
XCTAssertNil(records[1].player.teamId)
|
||||
XCTAssertEqual(records[1].player.name, "Barbara")
|
||||
XCTAssertNil(records[1].team)
|
||||
}
|
||||
|
||||
func testAnnotatedWithOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.annotated(withOptional: Player.team.select(Column("name").forKey("teamName")))
|
||||
.asRequest(of: PlayerWithTeamName.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].teamName, "Reds")
|
||||
XCTAssertEqual(records[1].player.id, 2)
|
||||
XCTAssertEqual(records[1].player.name, "Barbara")
|
||||
XCTAssertNil(records[1].teamName)
|
||||
}
|
||||
|
||||
func testJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(required: Player.team)
|
||||
let players = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(players.count, 1)
|
||||
XCTAssertEqual(players[0].id, 1)
|
||||
XCTAssertEqual(players[0].teamId, 1)
|
||||
XCTAssertEqual(players[0].name, "Arthur")
|
||||
}
|
||||
|
||||
func testJoiningsOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(optional: Player.team)
|
||||
let players = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(players.count, 2)
|
||||
XCTAssertEqual(players[0].id, 1)
|
||||
XCTAssertEqual(players[0].teamId, 1)
|
||||
XCTAssertEqual(players[0].name, "Arthur")
|
||||
XCTAssertEqual(players[1].id, 2)
|
||||
XCTAssertNil(players[1].teamId)
|
||||
XCTAssertEqual(players[1].name, "Barbara")
|
||||
}
|
||||
|
||||
func testRequestRefining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(required: PlayerWithRequiredTeam.team.select(Column("name"), Column("id")))
|
||||
.filter(teamName: "Reds")
|
||||
.orderedByTeamName()
|
||||
.asRequest(of: PlayerWithRequiredTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(lastSQLQuery, """
|
||||
SELECT "players".*, "teams"."name", "teams"."id" \
|
||||
FROM "players" \
|
||||
JOIN "teams" ON ("teams"."id" = "players"."teamId") AND ("teams"."name" = 'Reds') \
|
||||
ORDER BY "teams"."name", "players"."name"
|
||||
""")
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].team.id, 1)
|
||||
XCTAssertEqual(records[0].team.name, "Reds")
|
||||
}
|
||||
}
|
||||
+339
@@ -0,0 +1,339 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct Team: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "teams"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "players"
|
||||
static let teamScope = "team"
|
||||
static let team = Player.belongsTo(Team.self, key: teamScope)
|
||||
var id: Int64
|
||||
var teamId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct PlayerWithRequiredTeam: FetchableRecord {
|
||||
var player: Player
|
||||
var team: Team
|
||||
|
||||
init(row: Row) throws {
|
||||
player = try Player(row: row)
|
||||
team = row[Player.teamScope]
|
||||
}
|
||||
}
|
||||
|
||||
private struct PlayerWithOptionalTeam: FetchableRecord {
|
||||
var player: Player
|
||||
var team: Team?
|
||||
|
||||
init(row: Row) throws {
|
||||
player = try Player(row: row)
|
||||
team = row[Player.teamScope]
|
||||
}
|
||||
}
|
||||
|
||||
private struct PlayerWithTeamName: FetchableRecord {
|
||||
var player: Player
|
||||
var teamName: String?
|
||||
|
||||
init(row: Row) throws {
|
||||
player = try Player(row: row)
|
||||
teamName = row["teamName"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Test support for FetchableRecord records
|
||||
class AssociationBelongsToFetchableRecordTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "teams") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "players") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.belongsTo("team")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try Team(id: 1, name: "Reds").insert(db)
|
||||
try Player(id: 1, teamId: 1, name: "Arthur").insert(db)
|
||||
try Player(id: 2, teamId: nil, name: "Barbara").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(required: Player.team)
|
||||
.asRequest(of: PlayerWithRequiredTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].team.id, 1)
|
||||
XCTAssertEqual(records[0].team.name, "Reds")
|
||||
}
|
||||
|
||||
func testAnnotatedWithRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.annotated(withRequired: Player.team.select(Column("name").forKey("teamName")))
|
||||
.asRequest(of: PlayerWithTeamName.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].teamName, "Reds")
|
||||
}
|
||||
|
||||
func testIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(optional: Player.team)
|
||||
.asRequest(of: PlayerWithOptionalTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].team!.id, 1)
|
||||
XCTAssertEqual(records[0].team!.name, "Reds")
|
||||
XCTAssertEqual(records[1].player.id, 2)
|
||||
XCTAssertNil(records[1].player.teamId)
|
||||
XCTAssertEqual(records[1].player.name, "Barbara")
|
||||
XCTAssertNil(records[1].team)
|
||||
}
|
||||
|
||||
func testAnnotatedWithOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.annotated(withOptional: Player.team.select(Column("name").forKey("teamName")))
|
||||
.asRequest(of: PlayerWithTeamName.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].teamName, "Reds")
|
||||
XCTAssertEqual(records[1].player.id, 2)
|
||||
XCTAssertEqual(records[1].player.name, "Barbara")
|
||||
XCTAssertNil(records[1].teamName)
|
||||
}
|
||||
|
||||
func testJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(required: Player.team)
|
||||
let players = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(players.count, 1)
|
||||
XCTAssertEqual(players[0].id, 1)
|
||||
XCTAssertEqual(players[0].teamId, 1)
|
||||
XCTAssertEqual(players[0].name, "Arthur")
|
||||
}
|
||||
|
||||
func testJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(optional: Player.team)
|
||||
let players = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(players.count, 2)
|
||||
XCTAssertEqual(players[0].id, 1)
|
||||
XCTAssertEqual(players[0].teamId, 1)
|
||||
XCTAssertEqual(players[0].name, "Arthur")
|
||||
XCTAssertEqual(players[1].id, 2)
|
||||
XCTAssertNil(players[1].teamId)
|
||||
XCTAssertEqual(players[1].name, "Barbara")
|
||||
}
|
||||
|
||||
func testIncludingRequired_ColumnDecodingStrategy() throws {
|
||||
struct AnyKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int? { nil }
|
||||
init(stringValue: String) { self.stringValue = stringValue }
|
||||
init?(intValue: Int) { nil }
|
||||
}
|
||||
|
||||
struct XTeam: Decodable, FetchableRecord, TableRecord {
|
||||
static let databaseTableName = "teams"
|
||||
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.custom { column in
|
||||
AnyKey(stringValue: "x\(column)")
|
||||
}
|
||||
var xid: Int64
|
||||
var xname: String
|
||||
}
|
||||
|
||||
struct XPlayer: Decodable, FetchableRecord, TableRecord {
|
||||
static let databaseTableName = "players"
|
||||
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.custom { column in
|
||||
AnyKey(stringValue: "x\(column)")
|
||||
}
|
||||
static let team = belongsTo(Team.self, key: "team")
|
||||
static let xteam = belongsTo(XTeam.self, key: "xteam")
|
||||
var xid: Int64
|
||||
var xteamId: Int64?
|
||||
var xname: String
|
||||
}
|
||||
|
||||
do {
|
||||
struct XPlayerWithRequiredXTeam: Decodable, FetchableRecord {
|
||||
var xplayer: XPlayer
|
||||
var xteam: XTeam
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = XPlayer
|
||||
.including(required: XPlayer.xteam)
|
||||
.asRequest(of: XPlayerWithRequiredXTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xid, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xteamId, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xname, "Arthur")
|
||||
XCTAssertEqual(records[0].xteam.xid, 1)
|
||||
XCTAssertEqual(records[0].xteam.xname, "Reds")
|
||||
}
|
||||
|
||||
do {
|
||||
struct XPlayerWithRequiredTeam: Decodable, FetchableRecord {
|
||||
var xplayer: XPlayer
|
||||
var team: Team
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = XPlayer
|
||||
.including(required: XPlayer.team)
|
||||
.asRequest(of: XPlayerWithRequiredTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xid, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xteamId, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xname, "Arthur")
|
||||
XCTAssertEqual(records[0].team.id, 1)
|
||||
XCTAssertEqual(records[0].team.name, "Reds")
|
||||
}
|
||||
|
||||
do {
|
||||
struct PlayerWithRequiredXTeam: Decodable, FetchableRecord {
|
||||
var player: Player
|
||||
var xteam: XTeam
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(required: Player.belongsTo(XTeam.self, key: "xteam"))
|
||||
.asRequest(of: PlayerWithRequiredXTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 1)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].xteam.xid, 1)
|
||||
XCTAssertEqual(records[0].xteam.xname, "Reds")
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingOptional_ColumnDecodingStrategy() throws {
|
||||
struct AnyKey: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int? { nil }
|
||||
init(stringValue: String) { self.stringValue = stringValue }
|
||||
init?(intValue: Int) { nil }
|
||||
}
|
||||
|
||||
struct XTeam: Decodable, FetchableRecord, TableRecord {
|
||||
static let databaseTableName = "teams"
|
||||
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.custom { column in
|
||||
AnyKey(stringValue: "x\(column)")
|
||||
}
|
||||
var xid: Int64
|
||||
var xname: String
|
||||
}
|
||||
|
||||
struct XPlayer: Decodable, FetchableRecord, TableRecord {
|
||||
static let databaseTableName = "players"
|
||||
static let databaseColumnDecodingStrategy = DatabaseColumnDecodingStrategy.custom { column in
|
||||
AnyKey(stringValue: "x\(column)")
|
||||
}
|
||||
static let team = belongsTo(Team.self, key: "team")
|
||||
static let xteam = belongsTo(XTeam.self, key: "xteam")
|
||||
var xid: Int64
|
||||
var xteamId: Int64?
|
||||
var xname: String
|
||||
}
|
||||
|
||||
do {
|
||||
struct XPlayerWithOptionalXTeam: Decodable, FetchableRecord {
|
||||
var xplayer: XPlayer
|
||||
var xteam: XTeam?
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = XPlayer
|
||||
.including(optional: XPlayer.xteam)
|
||||
.asRequest(of: XPlayerWithOptionalXTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].xplayer.xid, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xteamId, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xname, "Arthur")
|
||||
XCTAssertEqual(records[0].xteam!.xid, 1)
|
||||
XCTAssertEqual(records[0].xteam!.xname, "Reds")
|
||||
XCTAssertEqual(records[1].xplayer.xid, 2)
|
||||
XCTAssertNil(records[1].xplayer.xteamId)
|
||||
XCTAssertEqual(records[1].xplayer.xname, "Barbara")
|
||||
XCTAssertNil(records[1].xteam)
|
||||
}
|
||||
|
||||
do {
|
||||
struct XPlayerWithOptionalTeam: Decodable, FetchableRecord {
|
||||
var xplayer: XPlayer
|
||||
var team: Team?
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = XPlayer
|
||||
.including(optional: XPlayer.team)
|
||||
.asRequest(of: XPlayerWithOptionalTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].xplayer.xid, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xteamId, 1)
|
||||
XCTAssertEqual(records[0].xplayer.xname, "Arthur")
|
||||
XCTAssertEqual(records[0].team!.id, 1)
|
||||
XCTAssertEqual(records[0].team!.name, "Reds")
|
||||
XCTAssertEqual(records[1].xplayer.xid, 2)
|
||||
XCTAssertNil(records[1].xplayer.xteamId)
|
||||
XCTAssertEqual(records[1].xplayer.xname, "Barbara")
|
||||
XCTAssertNil(records[1].team)
|
||||
}
|
||||
|
||||
do {
|
||||
struct PlayerWithOptionalXTeam: Decodable, FetchableRecord {
|
||||
var player: Player
|
||||
var xteam: XTeam?
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player
|
||||
.including(optional: Player.belongsTo(XTeam.self, key: "xteam"))
|
||||
.asRequest(of: PlayerWithOptionalXTeam.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
XCTAssertEqual(records.count, 2)
|
||||
XCTAssertEqual(records[0].player.id, 1)
|
||||
XCTAssertEqual(records[0].player.teamId, 1)
|
||||
XCTAssertEqual(records[0].player.name, "Arthur")
|
||||
XCTAssertEqual(records[0].xteam!.xid, 1)
|
||||
XCTAssertEqual(records[0].xteam!.xname, "Reds")
|
||||
XCTAssertEqual(records[1].player.id, 2)
|
||||
XCTAssertNil(records[1].player.teamId)
|
||||
XCTAssertEqual(records[1].player.name, "Barbara")
|
||||
XCTAssertNil(records[1].xteam)
|
||||
}
|
||||
}
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct Team: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "teams"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "players"
|
||||
static let defaultTeam = belongsTo(Team.self)
|
||||
static let customTeam = belongsTo(Team.self, key: "customTeam")
|
||||
var id: Int64
|
||||
var teamId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
/// Test row scopes
|
||||
class AssociationBelongsToRowScopeTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "teams") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "players") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.belongsTo("team")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try Team(id: 1, name: "Reds").insert(db)
|
||||
try Player(id: 1, teamId: 1, name: "Arthur").insert(db)
|
||||
try Player(id: 2, teamId: nil, name: "Barbara").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testJoiningDoesNotUseAnyRowAdapter() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let request = Player.joining(required: Player.defaultTeam)
|
||||
let adapter = try request.makePreparedRequest(db, forSingleResult: false).adapter
|
||||
XCTAssertNil(adapter)
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultScopeIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.including(required: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["team"])
|
||||
XCTAssertEqual(rows[0].scopes["team"]!, ["id":1, "name":"Reds"])
|
||||
}
|
||||
|
||||
func testDefaultScopeIncludingRequiredEmptySelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.select([]).including(required: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["team"])
|
||||
XCTAssertEqual(rows[0].scopes["team"]!, ["id":1, "name":"Reds"])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withRequired: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "id":1, "name":"Reds"])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithRequiredCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withRequired: Player.defaultTeam.select(Column("name").forKey("teamName")))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "teamName":"Reds"])
|
||||
}
|
||||
|
||||
func testDefaultScopeIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.including(optional: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["team"])
|
||||
XCTAssertEqual(rows[0].scopes["team"]!, ["id":1, "name":"Reds"])
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "teamId":nil, "name":"Barbara"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["team"])
|
||||
XCTAssertEqual(rows[1].scopes["team"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withOptional: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "id":1, "name":"Reds"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "teamId":nil, "name":"Barbara", "id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithOptionalCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withOptional: Player.defaultTeam.select(Column("name").forKey("teamName")))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "teamName":"Reds"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "teamId":nil, "name":"Barbara", "teamName":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(required: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testDefaultScopeJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(optional: Player.defaultTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "teamId":nil, "name":"Barbara"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testCustomScopeIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.including(required: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["customTeam"])
|
||||
XCTAssertEqual(rows[0].scopes["customTeam"]!, ["id":1, "name":"Reds"])
|
||||
}
|
||||
|
||||
func testCustomScopeIncludingRequiredEmptySelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.select([]).including(required: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["customTeam"])
|
||||
XCTAssertEqual(rows[0].scopes["customTeam"]!, ["id":1, "name":"Reds"])
|
||||
}
|
||||
|
||||
func testCustomScopeAnnotatedWithRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withRequired: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "id":1, "name":"Reds"])
|
||||
}
|
||||
|
||||
func testCustomScopeAnnotatedWithRequiredCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withRequired: Player.customTeam.select(Column("name").forKey("teamName")))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "teamName":"Reds"])
|
||||
}
|
||||
|
||||
func testCustomScopeIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.including(optional: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["customTeam"])
|
||||
XCTAssertEqual(rows[0].scopes["customTeam"]!, ["id":1, "name":"Reds"])
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "teamId":nil, "name":"Barbara"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["customTeam"])
|
||||
XCTAssertEqual(rows[1].scopes["customTeam"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testCustomScopeAnnotatedWithOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withOptional: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "id":1, "name":"Reds"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "teamId":nil, "name":"Barbara", "id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testCustomScopeAnnotatedWithOptionalCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.annotated(withOptional: Player.customTeam.select(Column("name").forKey("teamName")))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0], ["id":1, "teamId":1, "name":"Arthur", "teamName":"Reds"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "teamId":nil, "name":"Barbara", "teamName":nil])
|
||||
}
|
||||
|
||||
func testCustomPluralScopeIncludingRequired() throws {
|
||||
// Make sure explicit plural keys are preserved
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = Player.including(required: Player.belongsTo(Team.self, key: "teams"))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["teams"])
|
||||
XCTAssertEqual(rows[0].scopes["teams"]!, ["id":1, "name":"Reds"])
|
||||
}
|
||||
do {
|
||||
let request = Player.including(required: Player.defaultTeam.forKey("teams"))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["teams"])
|
||||
XCTAssertEqual(rows[0].scopes["teams"]!, ["id":1, "name":"Reds"])
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomPluralScopeIncludingOptional() throws {
|
||||
// Make sure explicit plural keys are preserved
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = Player.including(optional: Player.belongsTo(Team.self, key: "teams"))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["teams"])
|
||||
XCTAssertEqual(rows[0].scopes["teams"]!, ["id":1, "name":"Reds"])
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "teamId":nil, "name":"Barbara"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["teams"])
|
||||
XCTAssertEqual(rows[1].scopes["teams"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
let request = Player.including(optional: Player.defaultTeam.forKey("teams"))
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["teams"])
|
||||
XCTAssertEqual(rows[0].scopes["teams"]!, ["id":1, "name":"Reds"])
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "teamId":nil, "name":"Barbara"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["teams"])
|
||||
XCTAssertEqual(rows[1].scopes["teams"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomScopeJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(required: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testCustomScopeJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = Player.joining(optional: Player.customTeam)
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "teamId":1, "name":"Arthur"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "teamId":nil, "name":"Barbara"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
}
|
||||
+411
@@ -0,0 +1,411 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// A -> B
|
||||
private struct A : TableRecord {
|
||||
static let databaseTableName = "a"
|
||||
static let b = belongsTo(B.self)
|
||||
static let restrictedB = belongsTo(RestrictedB.self)
|
||||
static let extendedB = belongsTo(ExtendedB.self)
|
||||
}
|
||||
|
||||
private struct B : TableRecord {
|
||||
static let a = hasOne(A.self)
|
||||
static let databaseTableName = "b"
|
||||
}
|
||||
|
||||
private struct RestrictedB : TableRecord {
|
||||
static let databaseTableName = "b"
|
||||
static let databaseSelection: [any SQLSelectable] = [Column("name")]
|
||||
}
|
||||
|
||||
private struct ExtendedB : TableRecord {
|
||||
static let databaseTableName = "b"
|
||||
static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID]
|
||||
}
|
||||
|
||||
/// Test SQL generation
|
||||
class AssociationBelongsToSQLDerivationTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "b") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("bid", .integer).references("b")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.restrictedB), """
|
||||
SELECT "a".*, "b"."name" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.extendedB), """
|
||||
SELECT "a".*, "b".*, "b"."rowid" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A.including(required: A.b
|
||||
.select(Column("name")))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."name" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b
|
||||
.select(
|
||||
AllColumns(),
|
||||
Column.rowID))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".*, "b"."rowid" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let aAlias = TableAlias()
|
||||
let request = A
|
||||
.aliased(aAlias)
|
||||
.including(required: A.b
|
||||
.select(
|
||||
Column("name"),
|
||||
(Column("id") + aAlias[Column("id")]).forKey("foo")))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."name", "b"."id" + "a"."id" AS "foo" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFilteredAssociationImpactsJoinOnClause() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(Column("name") != nil))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."name" IS NOT NULL)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(key: 1))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" = 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(keys: [1, 2, 3]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" IN (1, 2, 3))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let alias = TableAlias()
|
||||
let request = A
|
||||
.aliased(alias)
|
||||
.including(required: A.b
|
||||
.filter([alias[Column("id")], Column("id"), 42].contains(Column("id"))))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" IN ("a"."id", "b"."id", 42))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(key: ["id": 1]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" = 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(keys: [["id": 1], ["id": 2]]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND (("b"."id" = 1) OR ("b"."id" = 2))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let bAlias = TableAlias(name: "customB")
|
||||
let request = A.including(required: A.b
|
||||
.aliased(bAlias)
|
||||
.filter(sql: "customB.name = ?", arguments: ["foo"]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "customB".* \
|
||||
FROM "a" \
|
||||
JOIN "b" "customB" ON ("customB"."id" = "a"."bid") AND (customB.name = 'foo')
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFilterAssociationInWhereClause() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let bAlias = TableAlias()
|
||||
let request = A
|
||||
.including(required: A.b.aliased(bAlias))
|
||||
.filter(bAlias[Column("name")] != nil)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
WHERE "b"."name" IS NOT NULL
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testAssociationOrderBubbleUp() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let aBase = A.all().aliased(TableAlias(name: "a"))
|
||||
let abBase = A.b.aliased(TableAlias(name: "ab"))
|
||||
let abaBase = B.a.aliased(TableAlias(name: "aba"))
|
||||
|
||||
let aTransforms = [
|
||||
{ (r: QueryInterfaceRequest<A>) in return r },
|
||||
{ (r: QueryInterfaceRequest<A>) in return r.order(Column("id")) },
|
||||
{ (r: QueryInterfaceRequest<A>) in return r.order(Column("id")).reversed() },
|
||||
]
|
||||
let abTransforms = [
|
||||
{ (r: BelongsToAssociation<A, B>) in return r },
|
||||
{ (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) },
|
||||
{ (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) },
|
||||
{ (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() },
|
||||
{ (r: BelongsToAssociation<A, B>) in return r.reversed() },
|
||||
]
|
||||
let abaTransforms = [
|
||||
{ (r: HasOneAssociation<B, A>) in return r },
|
||||
{ (r: HasOneAssociation<B, A>) in return r.order(Column("id")) },
|
||||
{ (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() },
|
||||
]
|
||||
|
||||
var sqls: [String] = []
|
||||
for aTransform in aTransforms {
|
||||
for abTransform in abTransforms {
|
||||
for abaTransform in abaTransforms {
|
||||
let request = aTransform(aBase)
|
||||
.including(required: abTransform(abBase)
|
||||
.including(required: abaTransform(abaBase)))
|
||||
try sqls.append(request.build(db).sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prefix = """
|
||||
SELECT "a".*, "ab".*, "aba".* \
|
||||
FROM "a" \
|
||||
JOIN "b" "ab" ON "ab"."id" = "a"."bid" \
|
||||
JOIN "a" "aba" ON "aba"."bid" = "ab"."id"
|
||||
"""
|
||||
let orderClauses = sqls.map { sql -> String in
|
||||
let prefixEndIndex = sql.index(sql.startIndex, offsetBy: prefix.count)
|
||||
return String(sql.suffix(from: prefixEndIndex))
|
||||
}
|
||||
XCTAssertEqual(orderClauses, [
|
||||
// a: { (r: QueryInterfaceRequest<A>) in return r },
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"ab\".\"name\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"ab\".\"name\" DESC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"ab\".\"name\" DESC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"aba\".\"id\"",
|
||||
|
||||
|
||||
// a: { (r: QueryInterfaceRequest<A>) in return r.order(Column("id")) },
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\"",
|
||||
|
||||
|
||||
// a: { (r: QueryInterfaceRequest<A>) in return r.order(Column("id")).reversed() }
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\", \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\", \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Test if and only if we really want to build an association from any request
|
||||
// func testAssociationFromRequest() throws {
|
||||
// let dbQueue = try makeDatabaseQueue()
|
||||
// try dbQueue.inDatabase { db in
|
||||
// do {
|
||||
// let bRequest = B
|
||||
// .filter(Column("name") != nil)
|
||||
// .order(Column("id"))
|
||||
// let association = A.belongsTo(bRequest)
|
||||
// let request = A.including(required: association)
|
||||
// try assertEqualSQL(db, request, """
|
||||
// SELECT "a".*, "b".* \
|
||||
// FROM "a" \
|
||||
// JOIN "b" ON (("b"."id" = "a"."bid") AND ("b"."name" IS NOT NULL)) \
|
||||
// ORDER BY "b"."id"
|
||||
// """)
|
||||
// }
|
||||
// do {
|
||||
// let bRequest = RestrictedB
|
||||
// .filter(Column("name") != nil)
|
||||
// .order(Column("id"))
|
||||
// let association = A.belongsTo(bRequest)
|
||||
// let request = A.including(required: association)
|
||||
// try assertEqualSQL(db, request, """
|
||||
// SELECT "a".*, "b"."name" \
|
||||
// FROM "a" \
|
||||
// JOIN "b" ON (("b"."id" = "a"."bid") AND ("b"."name" IS NOT NULL)) \
|
||||
// ORDER BY "b"."id"
|
||||
// """)
|
||||
// }
|
||||
// do {
|
||||
// let bRequest = ExtendedB
|
||||
// .select([Column("name")])
|
||||
// .filter(Column("name") != nil)
|
||||
// .order(Column("id"))
|
||||
// let association = A.belongsTo(bRequest)
|
||||
// let request = A.including(required: association)
|
||||
// try assertEqualSQL(db, request, """
|
||||
// SELECT "a".*, "b"."name" \
|
||||
// FROM "a" \
|
||||
// JOIN "b" ON (("b"."id" = "a"."bid") AND ("b"."name" IS NOT NULL)) \
|
||||
// ORDER BY "b"."id"
|
||||
// """)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
+2188
File diff suppressed because it is too large
Load Diff
+688
@@ -0,0 +1,688 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// A -> B <- C -> D
|
||||
private struct A: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "a"
|
||||
static let defaultB = belongsTo(B.self)
|
||||
var id: Int64
|
||||
var bid: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct B: Codable, FetchableRecord, PersistableRecord {
|
||||
static let defaultC = hasOne(C.self)
|
||||
static let databaseTableName = "b"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct C: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "c"
|
||||
static let defaultD = belongsTo(D.self)
|
||||
var id: Int64
|
||||
var bid: Int64?
|
||||
var did: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct D: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "d"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
/// Test row scopes
|
||||
class AssociationChainRowScopesTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "b") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "d") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("bid", .integer).references("b")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "c") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("bid", .integer).references("b")
|
||||
t.column("did", .integer).references("d")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try B(id: 1, name: "b1").insert(db)
|
||||
try B(id: 2, name: "b2").insert(db)
|
||||
try A(id: 1, bid: 1, name: "a1").insert(db)
|
||||
try A(id: 2, bid: 2, name: "a2").insert(db)
|
||||
try A(id: 3, bid: nil, name: "a3").insert(db)
|
||||
try D(id: 1, name: "d1").insert(db)
|
||||
try C(id: 1, bid: 1, did: 1, name: "c1").insert(db)
|
||||
try C(id: 2, bid: 1, did: nil, name: "c2").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testChainOfTwoIncludingIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = A.including(required: A.defaultB.including(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.defaultB.including(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.unscoped, ["id":2, "name":"b2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = A.including(optional: A.defaultB.including(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = A.including(optional: A.defaultB.including(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 4)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.unscoped, ["id":2, "name":"b2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
|
||||
XCTAssertEqual(rows[3].unscoped, ["id":3, "bid":nil, "name":"a3"])
|
||||
XCTAssertEqual(Set(rows[3].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[3].scopes["b"]!.unscoped, ["id":nil, "name":nil])
|
||||
XCTAssertEqual(Set(rows[3].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[3].scopes["b"]!.scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
let request = B.including(required: B.defaultC.including(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.unscoped, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
}
|
||||
do {
|
||||
let request = B.including(required: B.defaultC.including(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.unscoped, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.unscoped, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
XCTAssertEqual(Set(rows[1].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.scopes["d"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = B.including(optional: B.defaultC.including(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = B.including(optional: B.defaultC.including(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.unscoped, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.unscoped, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
XCTAssertEqual(Set(rows[1].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.scopes["d"]!, ["id":nil, "name":nil])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "name":"b2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["c"]!.unscoped, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
XCTAssertEqual(Set(rows[2].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[2].scopes["c"]!.scopes["d"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
}
|
||||
|
||||
func testChainOfTwoIncludingJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = A.including(required: A.defaultB.joining(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes["b"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[1].scopes["b"]!.scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.defaultB.joining(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes["b"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[1].scopes["b"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!, ["id":2, "name":"b2"])
|
||||
XCTAssertTrue(rows[2].scopes["b"]!.scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = A.including(optional: A.defaultB.joining(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = A.including(optional: A.defaultB.joining(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 4)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes["b"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[1].scopes["b"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!, ["id":2, "name":"b2"])
|
||||
XCTAssertTrue(rows[2].scopes["b"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[3].unscoped, ["id":3, "bid":nil, "name":"a3"])
|
||||
XCTAssertEqual(Set(rows[3].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[3].scopes["b"]!, ["id":nil, "name":nil])
|
||||
XCTAssertTrue(rows[3].scopes["b"]!.scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
let request = B.including(required: B.defaultC.joining(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertTrue(rows[0].scopes["c"]!.scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
let request = B.including(required: B.defaultC.joining(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertTrue(rows[0].scopes["c"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
XCTAssertTrue(rows[1].scopes["c"]!.scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = B.including(optional: B.defaultC.joining(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = B.including(optional: B.defaultC.joining(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertTrue(rows[0].scopes["c"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
XCTAssertTrue(rows[1].scopes["c"]!.scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "name":"b2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
XCTAssertTrue(rows[2].scopes["c"]!.scopes.names.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func testChainOfTwoJoiningIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = A.joining(required: A.defaultB.including(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
}
|
||||
do {
|
||||
let request = A.joining(required: A.defaultB.including(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[2].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = A.joining(optional: A.defaultB.including(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = A.joining(optional: A.defaultB.including(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 4)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!, ["id":2, "bid":1, "did":nil, "name":"c2"])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[2].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
|
||||
XCTAssertEqual(rows[3].unscoped, ["id":3, "bid":nil, "name":"a3"])
|
||||
XCTAssertEqual(Set(rows[3].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[3].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[3].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[3].scopes["b"]!.scopes["c"]!, ["id":nil, "bid":nil, "did":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
let request = B.joining(required: B.defaultC.including(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
}
|
||||
do {
|
||||
let request = B.joining(required: B.defaultC.including(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[1].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.scopes["d"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = B.joining(optional: B.defaultC.including(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = B.joining(optional: B.defaultC.including(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[1].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[1].scopes["c"]!.scopes["d"]!, ["id":nil, "name":nil])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":2, "name":"b2"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[2].scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[2].scopes["c"]!.scopes["d"]!, ["id":nil, "name":nil])
|
||||
}
|
||||
}
|
||||
|
||||
func testChainOfTwoJoiningJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = A.joining(required: A.defaultB.joining(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[1].scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
let request = A.joining(required: A.defaultB.joining(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[1].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2], ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertTrue(rows[2].scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = A.joining(optional: A.defaultB.joining(required: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = A.joining(optional: A.defaultB.joining(optional: B.defaultC)).order(sql: "a.id, b.id, c.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 4)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[1].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2], ["id":2, "bid":2, "name":"a2"])
|
||||
XCTAssertTrue(rows[2].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[3], ["id":3, "bid":nil, "name":"a3"])
|
||||
XCTAssertTrue(rows[3].scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
let request = B.joining(required: B.defaultC.joining(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
let request = B.joining(required: B.defaultC.joining(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1], ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[1].scopes.names.isEmpty)
|
||||
}
|
||||
do {
|
||||
// TODO: chainOptionalRequired
|
||||
// let request = B.joining(optional: B.defaultC.joining(required: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
// let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
}
|
||||
do {
|
||||
let request = B.joining(optional: B.defaultC.joining(optional: C.defaultD)).order(sql: "b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1], ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[1].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2], ["id":2, "name":"b2"])
|
||||
XCTAssertTrue(rows[2].scopes.names.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
func testChainOfThreeIncludingIncludingIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.defaultB.including(required: B.defaultC.including(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
}
|
||||
|
||||
func testChainOfThreeIncludingIncludingJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.defaultB.including(required: B.defaultC.joining(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"], ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertTrue(rows[0].scopes["b"]!.scopes["c"]!.scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testChainOfThreeIncludingJoiningIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.defaultB.joining(required: B.defaultC.including(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
}
|
||||
|
||||
func testChainOfThreeIncludingJoiningJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.defaultB.joining(required: B.defaultC.joining(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "name":"b1"])
|
||||
XCTAssertTrue(rows[0].scopes["b"]!.scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testChainOfThreeJoiningIncludingIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(required: A.defaultB.including(required: B.defaultC.including(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
}
|
||||
|
||||
func testChainOfThreeJoiningIncludingJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(required: A.defaultB.including(required: B.defaultC.joining(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"], ["id":1, "bid":1, "did":1, "name":"c1"])
|
||||
XCTAssertTrue(rows[0].scopes["b"]!.scopes["c"]!.scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testChainOfThreeJoiningJoiningIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(required: A.defaultB.joining(required: B.defaultC.including(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes["c"]!.scopes.names), ["d"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.scopes["d"]!, ["id":1, "name":"d1"])
|
||||
}
|
||||
|
||||
func testChainOfThreeJoiningJoiningJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(required: A.defaultB.joining(required: B.defaultC.joining(required: C.defaultD))).order(sql: "a.id, b.id, c.id, d.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bid":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+204
@@ -0,0 +1,204 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// Ordered hasManyThrough
|
||||
private struct Team: Codable, FetchableRecord, PersistableRecord, Equatable {
|
||||
static let players = hasMany(Player.self).order(Column("position"))
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord, Equatable {
|
||||
var id: Int64
|
||||
var teamId: Int64
|
||||
var name: String
|
||||
var position: Int
|
||||
}
|
||||
|
||||
private struct TeamInfo: Decodable, FetchableRecord, Equatable {
|
||||
var team: Team
|
||||
var players: [Player]
|
||||
}
|
||||
|
||||
private struct TeamWithOptionalPlayers: Decodable, FetchableRecord, Equatable {
|
||||
var team: Team
|
||||
var players: [Player]?
|
||||
}
|
||||
|
||||
/// A usage test for ordered hasMany association
|
||||
class AssociationHasManyOrderingTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "team") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
try db.create(table: "player") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.belongsTo("team").notNull()
|
||||
t.column("name", .text).notNull()
|
||||
t.column("position", .integer).notNull()
|
||||
}
|
||||
|
||||
try Team(id: 1, name: "Red").insert(db)
|
||||
try Team(id: 2, name: "Blue").insert(db)
|
||||
try Player(id: 1, teamId: 1, name: "Arthur", position: 1).insert(db)
|
||||
try Player(id: 2, teamId: 1, name: "Barbara", position: 2).insert(db)
|
||||
try Player(id: 3, teamId: 1, name: "Craig", position: 3).insert(db)
|
||||
try Player(id: 4, teamId: 2, name: "Diane", position: 3).insert(db)
|
||||
try Player(id: 5, teamId: 2, name: "Eugene", position: 2).insert(db)
|
||||
try Player(id: 6, teamId: 2, name: "Fiona", position: 1).insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestFor() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let team = try Team.fetchOne(db, key: 2)!
|
||||
let players = try team.request(for: Team.players).fetchAll(db)
|
||||
XCTAssertEqual(lastSQLQuery, """
|
||||
SELECT * FROM "player" WHERE "teamId" = 2 ORDER BY "position"
|
||||
""")
|
||||
XCTAssertEqual(players, [
|
||||
Player(id: 6, teamId: 2, name: "Fiona", position: 1),
|
||||
Player(id: 5, teamId: 2, name: "Eugene", position: 2),
|
||||
Player(id: 4, teamId: 2, name: "Diane", position: 3),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func testReorderedRequestFor() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let team = try Team.fetchOne(db, key: 2)!
|
||||
let players = try team.request(for: Team.players).order(Column("name")).fetchAll(db)
|
||||
XCTAssertEqual(lastSQLQuery, """
|
||||
SELECT * FROM "player" WHERE "teamId" = 2 ORDER BY "name"
|
||||
""")
|
||||
XCTAssertEqual(players, [
|
||||
Player(id: 4, teamId: 2, name: "Diane", position: 3),
|
||||
Player(id: 5, teamId: 2, name: "Eugene", position: 2),
|
||||
Player(id: 6, teamId: 2, name: "Fiona", position: 1),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingAll() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let teamInfos = try Team
|
||||
.orderByPrimaryKey()
|
||||
.including(all: Team.players)
|
||||
.asRequest(of: TeamInfo.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "team" ORDER BY "id"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT *, "teamId" AS "grdb_teamId" \
|
||||
FROM "player" \
|
||||
WHERE "teamId" IN (1, 2) \
|
||||
ORDER BY "position"
|
||||
"""))
|
||||
XCTAssertEqual(teamInfos, [
|
||||
TeamInfo(
|
||||
team: Team(id: 1, name: "Red"),
|
||||
players: [
|
||||
Player(id: 1, teamId: 1, name: "Arthur", position: 1),
|
||||
Player(id: 2, teamId: 1, name: "Barbara", position: 2),
|
||||
Player(id: 3, teamId: 1, name: "Craig", position: 3),
|
||||
]),
|
||||
TeamInfo(
|
||||
team: Team(id: 2, name: "Blue"),
|
||||
players: [
|
||||
Player(id: 6, teamId: 2, name: "Fiona", position: 1),
|
||||
Player(id: 5, teamId: 2, name: "Eugene", position: 2),
|
||||
Player(id: 4, teamId: 2, name: "Diane", position: 3),
|
||||
])])
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingAllDecodedAsOptional() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
do {
|
||||
let teamInfos = try Team
|
||||
.orderByPrimaryKey()
|
||||
.including(all: Team.players)
|
||||
.asRequest(of: TeamWithOptionalPlayers.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "team" ORDER BY "id"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT *, "teamId" AS "grdb_teamId" \
|
||||
FROM "player" \
|
||||
WHERE "teamId" IN (1, 2) \
|
||||
ORDER BY "position"
|
||||
"""))
|
||||
XCTAssertEqual(teamInfos, [
|
||||
TeamWithOptionalPlayers(
|
||||
team: Team(id: 1, name: "Red"),
|
||||
players: [
|
||||
Player(id: 1, teamId: 1, name: "Arthur", position: 1),
|
||||
Player(id: 2, teamId: 1, name: "Barbara", position: 2),
|
||||
Player(id: 3, teamId: 1, name: "Craig", position: 3),
|
||||
]),
|
||||
TeamWithOptionalPlayers(
|
||||
team: Team(id: 2, name: "Blue"),
|
||||
players: [
|
||||
Player(id: 6, teamId: 2, name: "Fiona", position: 1),
|
||||
Player(id: 5, teamId: 2, name: "Eugene", position: 2),
|
||||
Player(id: 4, teamId: 2, name: "Diane", position: 3),
|
||||
])])
|
||||
}
|
||||
do {
|
||||
let teamInfos = try Team
|
||||
.orderByPrimaryKey()
|
||||
.asRequest(of: TeamWithOptionalPlayers.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "team" ORDER BY "id"
|
||||
"""))
|
||||
XCTAssertEqual(teamInfos, [
|
||||
TeamWithOptionalPlayers(
|
||||
team: Team(id: 1, name: "Red"),
|
||||
players: nil),
|
||||
TeamWithOptionalPlayers(
|
||||
team: Team(id: 2, name: "Blue"),
|
||||
players: nil)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReorderedIncludingAll() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let teamInfos = try Team
|
||||
.orderByPrimaryKey()
|
||||
.including(all: Team.players.order(Column("name")))
|
||||
.asRequest(of: TeamInfo.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "team" ORDER BY "id"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT *, "teamId" AS "grdb_teamId" \
|
||||
FROM "player" \
|
||||
WHERE "teamId" IN (1, 2) \
|
||||
ORDER BY "name"
|
||||
"""))
|
||||
XCTAssertEqual(teamInfos, [
|
||||
TeamInfo(
|
||||
team: Team(id: 1, name: "Red"),
|
||||
players: [
|
||||
Player(id: 1, teamId: 1, name: "Arthur", position: 1),
|
||||
Player(id: 2, teamId: 1, name: "Barbara", position: 2),
|
||||
Player(id: 3, teamId: 1, name: "Craig", position: 3),
|
||||
]),
|
||||
TeamInfo(
|
||||
team: Team(id: 2, name: "Blue"),
|
||||
players: [
|
||||
Player(id: 4, teamId: 2, name: "Diane", position: 3),
|
||||
Player(id: 5, teamId: 2, name: "Eugene", position: 2),
|
||||
Player(id: 6, teamId: 2, name: "Fiona", position: 1),
|
||||
])])
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
class AssociationHasManyRowScopeTests: GRDBTestCase {
|
||||
|
||||
func testSingularTable() throws {
|
||||
struct Child : TableRecord {
|
||||
}
|
||||
|
||||
struct Parent : TableRecord {
|
||||
static let children = hasMany(Child.self)
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parent") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "child") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("parent")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO parent (id) VALUES (1);
|
||||
INSERT INTO child (id, parentId) VALUES (2, 1);
|
||||
""")
|
||||
|
||||
let request = Parent.including(required: Parent.children)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(row.scopes["child"], ["id": 2, "parentId": 1])
|
||||
}
|
||||
}
|
||||
|
||||
func testPluralTable() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
static let children = hasMany(Child.self)
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("parent")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO parents (id) VALUES (1);
|
||||
INSERT INTO children (id, parentId) VALUES (2, 1);
|
||||
""")
|
||||
|
||||
let request = Parent.including(required: Parent.children)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(row.scopes["child"], ["id": 2, "parentId": 1])
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomKey() throws {
|
||||
struct Child : TableRecord {
|
||||
}
|
||||
|
||||
struct Parent : TableRecord {
|
||||
static let littlePuppies = hasMany(Child.self, key: "littlePuppies")
|
||||
static let kittens = hasMany(Child.self).forKey("kittens")
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parent") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "child") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("parent")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO parent (id) VALUES (1);
|
||||
INSERT INTO child (id, parentId) VALUES (2, 1);
|
||||
""")
|
||||
|
||||
do {
|
||||
let request = Parent.including(required: Parent.littlePuppies)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(row.scopes["littlePuppy"], ["id": 2, "parentId": 1])
|
||||
}
|
||||
do {
|
||||
let request = Parent.including(required: Parent.kittens)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(row.scopes["kitten"], ["id": 2, "parentId": 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+167
@@ -0,0 +1,167 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// Ordered hasManyThrough
|
||||
private struct Team: Codable, FetchableRecord, PersistableRecord, Equatable {
|
||||
static let playerRoles = hasMany(PlayerRole.self).order(Column("position"))
|
||||
static let players = hasMany(Player.self, through: playerRoles, using: PlayerRole.player)
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct PlayerRole: Codable, FetchableRecord, PersistableRecord {
|
||||
static let player = belongsTo(Player.self)
|
||||
var teamId: Int64
|
||||
var playerId: Int64
|
||||
var position: Int
|
||||
}
|
||||
|
||||
private struct Player: Codable, FetchableRecord, PersistableRecord, Equatable {
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct TeamInfo: Decodable, FetchableRecord, Equatable {
|
||||
var team: Team
|
||||
var players: [Player]
|
||||
}
|
||||
|
||||
/// A usage test for ordered hasManyThrough association
|
||||
class AssociationHasManyThroughOrderingTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "team") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
try db.create(table: "player") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
try db.create(table: "playerRole") { t in
|
||||
t.belongsTo("team").notNull()
|
||||
t.belongsTo("player").notNull()
|
||||
t.column("position", .integer).notNull()
|
||||
t.primaryKey(["teamId", "playerId"])
|
||||
}
|
||||
|
||||
try Team(id: 1, name: "Red").insert(db)
|
||||
try Team(id: 2, name: "Blue").insert(db)
|
||||
try Player(id: 1, name: "Arthur").insert(db)
|
||||
try Player(id: 2, name: "Barbara").insert(db)
|
||||
try Player(id: 3, name: "Craig").insert(db)
|
||||
try Player(id: 4, name: "Diane").insert(db)
|
||||
try PlayerRole(teamId: 1, playerId: 1, position: 1).insert(db)
|
||||
try PlayerRole(teamId: 1, playerId: 2, position: 2).insert(db)
|
||||
try PlayerRole(teamId: 1, playerId: 3, position: 3).insert(db)
|
||||
try PlayerRole(teamId: 2, playerId: 2, position: 3).insert(db)
|
||||
try PlayerRole(teamId: 2, playerId: 3, position: 2).insert(db)
|
||||
try PlayerRole(teamId: 2, playerId: 4, position: 1).insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestFor() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let team = try Team.fetchOne(db, key: 2)!
|
||||
let players = try team.request(for: Team.players).fetchAll(db)
|
||||
XCTAssertEqual(lastSQLQuery, """
|
||||
SELECT "player".* \
|
||||
FROM "player" \
|
||||
JOIN "playerRole" ON ("playerRole"."playerId" = "player"."id") AND ("playerRole"."teamId" = 2) \
|
||||
ORDER BY "playerRole"."position"
|
||||
""")
|
||||
XCTAssertEqual(players, [
|
||||
Player(id: 4, name: "Diane"),
|
||||
Player(id: 3, name: "Craig"),
|
||||
Player(id: 2, name: "Barbara"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func testReorderedRequestFor() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let team = try Team.fetchOne(db, key: 2)!
|
||||
let players = try team.request(for: Team.players).order(Column("name")).fetchAll(db)
|
||||
XCTAssertEqual(lastSQLQuery, """
|
||||
SELECT "player".* \
|
||||
FROM "player" \
|
||||
JOIN "playerRole" ON ("playerRole"."playerId" = "player"."id") AND ("playerRole"."teamId" = 2) \
|
||||
ORDER BY "player"."name", "playerRole"."position"
|
||||
""")
|
||||
XCTAssertEqual(players, [
|
||||
Player(id: 2, name: "Barbara"),
|
||||
Player(id: 3, name: "Craig"),
|
||||
Player(id: 4, name: "Diane"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingAll() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let teamInfos = try Team
|
||||
.orderByPrimaryKey()
|
||||
.including(all: Team.players)
|
||||
.asRequest(of: TeamInfo.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "team" ORDER BY "id"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT "player".*, "playerRole"."teamId" AS "grdb_teamId" \
|
||||
FROM "player" \
|
||||
JOIN "playerRole" ON ("playerRole"."playerId" = "player"."id") AND ("playerRole"."teamId" IN (1, 2)) \
|
||||
ORDER BY "playerRole"."position"
|
||||
"""))
|
||||
XCTAssertEqual(teamInfos, [
|
||||
TeamInfo(
|
||||
team: Team(id: 1, name: "Red"),
|
||||
players: [
|
||||
Player(id: 1, name: "Arthur"),
|
||||
Player(id: 2, name: "Barbara"),
|
||||
Player(id: 3, name: "Craig"),
|
||||
]),
|
||||
TeamInfo(
|
||||
team: Team(id: 2, name: "Blue"),
|
||||
players: [
|
||||
Player(id: 4, name: "Diane"),
|
||||
Player(id: 3, name: "Craig"),
|
||||
Player(id: 2, name: "Barbara"),
|
||||
])])
|
||||
}
|
||||
}
|
||||
|
||||
func testReorderedIncludingAll() throws {
|
||||
try makeDatabaseQueue().read { db in
|
||||
let teamInfos = try Team
|
||||
.orderByPrimaryKey()
|
||||
.including(all: Team.players.order(Column("name")))
|
||||
.asRequest(of: TeamInfo.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT * FROM "team" ORDER BY "id"
|
||||
"""))
|
||||
XCTAssertTrue(sqlQueries.contains("""
|
||||
SELECT "player".*, "playerRole"."teamId" AS "grdb_teamId" \
|
||||
FROM "player" \
|
||||
JOIN "playerRole" ON ("playerRole"."playerId" = "player"."id") AND ("playerRole"."teamId" IN (1, 2)) \
|
||||
ORDER BY "player"."name", "playerRole"."position"
|
||||
"""))
|
||||
XCTAssertEqual(teamInfos, [
|
||||
TeamInfo(
|
||||
team: Team(id: 1, name: "Red"),
|
||||
players: [
|
||||
Player(id: 1, name: "Arthur"),
|
||||
Player(id: 2, name: "Barbara"),
|
||||
Player(id: 3, name: "Craig"),
|
||||
]),
|
||||
TeamInfo(
|
||||
team: Team(id: 2, name: "Blue"),
|
||||
players: [
|
||||
Player(id: 2, name: "Barbara"),
|
||||
Player(id: 3, name: "Craig"),
|
||||
Player(id: 4, name: "Diane"),
|
||||
])])
|
||||
}
|
||||
}
|
||||
}
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
/// Test SQL generation
|
||||
|
||||
class AssociationHasManyThroughRowScopeTests: GRDBTestCase {
|
||||
|
||||
func testBelongsToHasManySingularTable() throws {
|
||||
struct Parent: TableRecord {
|
||||
static let child = belongsTo(Child.self)
|
||||
static let grandChildren = hasMany(GrandChild.self, through: child, using: Child.grandChildren)
|
||||
}
|
||||
struct Child: TableRecord {
|
||||
static let grandChildren = hasMany(GrandChild.self)
|
||||
}
|
||||
struct GrandChild: TableRecord {
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "child") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "parent") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("child")
|
||||
}
|
||||
try db.create(table: "grandChild") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("child")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO child (id) VALUES (1);
|
||||
INSERT INTO parent (id, childId) VALUES (2, 1);
|
||||
INSERT INTO grandChild (id, childId) VALUES (3, 1);
|
||||
""")
|
||||
|
||||
let request = Parent.including(required: Parent.grandChildren)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 2, "childId": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["grandChild"], ["id": 3, "childId": 1])
|
||||
}
|
||||
}
|
||||
|
||||
func testBelongsToHasManyPluralTable() throws {
|
||||
struct Parent: TableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
static let child = belongsTo(Child.self)
|
||||
static let grandChildren = hasMany(GrandChild.self, through: child, using: Child.grandChildren)
|
||||
}
|
||||
struct Child: TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
static let grandChildren = hasMany(GrandChild.self)
|
||||
}
|
||||
struct GrandChild: TableRecord {
|
||||
static let databaseTableName = "grandChildren"
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "children") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "parents") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("child")
|
||||
}
|
||||
try db.create(table: "grandChildren") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("child")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO children (id) VALUES (1);
|
||||
INSERT INTO parents (id, childId) VALUES (2, 1);
|
||||
INSERT INTO grandChildren (id, childId) VALUES (3, 1);
|
||||
""")
|
||||
|
||||
let request = Parent.including(required: Parent.grandChildren)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 2, "childId": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["grandChild"], ["id": 3, "childId": 1])
|
||||
}
|
||||
}
|
||||
|
||||
func testBelongsToHasManyCustomKey() throws {
|
||||
struct Parent: TableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
static let child = belongsTo(Child.self)
|
||||
static let littlePuppies = hasMany(GrandChild.self, through: child, using: Child.grandChildren, key: "littlePuppies")
|
||||
static let kittens = hasMany(GrandChild.self, through: child, using: Child.grandChildren).forKey("kittens")
|
||||
}
|
||||
struct Child: TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
static let grandChildren = hasMany(GrandChild.self)
|
||||
}
|
||||
struct GrandChild: TableRecord {
|
||||
static let databaseTableName = "grandChildren"
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "children") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "parents") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("child")
|
||||
}
|
||||
try db.create(table: "grandChildren") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("child")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO children (id) VALUES (1);
|
||||
INSERT INTO parents (id, childId) VALUES (2, 1);
|
||||
INSERT INTO grandChildren (id, childId) VALUES (3, 1);
|
||||
""")
|
||||
|
||||
do {
|
||||
let request = Parent.including(required: Parent.littlePuppies)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 2, "childId": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["littlePuppy"], ["id": 3, "childId": 1])
|
||||
}
|
||||
|
||||
do {
|
||||
let request = Parent.including(required: Parent.kittens)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 2, "childId": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["kitten"], ["id": 3, "childId": 1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testHasManyBelongsToSingularTable() throws {
|
||||
struct Parent: TableRecord {
|
||||
static let children = hasMany(Child.self)
|
||||
static let grandChildren = hasMany(GrandChild.self, through: children, using: Child.grandChild)
|
||||
}
|
||||
struct Child: TableRecord {
|
||||
static let grandChild = belongsTo(GrandChild.self)
|
||||
}
|
||||
struct GrandChild: TableRecord {
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "parent") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "grandChild") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "child") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("parent")
|
||||
t.belongsTo("grandChild")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO parent (id) VALUES (1);
|
||||
INSERT INTO grandChild (id) VALUES (2);
|
||||
INSERT INTO child (id, parentId, grandChildId) VALUES (3, 1, 2);
|
||||
""")
|
||||
|
||||
let request = Parent.including(required: Parent.grandChildren)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["grandChild"], ["id": 2])
|
||||
}
|
||||
}
|
||||
|
||||
func testHasManyBelongsToPluralTable() throws {
|
||||
struct Parent: TableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
static let children = hasMany(Child.self)
|
||||
static let grandChildren = hasMany(GrandChild.self, through: children, using: Child.grandChild)
|
||||
}
|
||||
struct Child: TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
static let grandChild = belongsTo(GrandChild.self)
|
||||
}
|
||||
struct GrandChild: TableRecord {
|
||||
static let databaseTableName = "grandChildren"
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "grandChildren") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("parent")
|
||||
t.belongsTo("grandChild")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO parents (id) VALUES (1);
|
||||
INSERT INTO grandChildren (id) VALUES (2);
|
||||
INSERT INTO children (id, parentId, grandChildId) VALUES (3, 1, 2);
|
||||
""")
|
||||
|
||||
let request = Parent.including(required: Parent.grandChildren)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["grandChild"], ["id": 2])
|
||||
}
|
||||
}
|
||||
|
||||
func testHasManyBelongsToCustomKey() throws {
|
||||
struct Parent: TableRecord {
|
||||
static let children = hasMany(Child.self)
|
||||
static let littlePuppies = hasMany(GrandChild.self, through: children, using: Child.grandChild, key: "littlePuppies")
|
||||
static let kittens = hasMany(GrandChild.self, through: children, using: Child.grandChild).forKey("kittens")
|
||||
}
|
||||
struct Child: TableRecord {
|
||||
static let grandChild = belongsTo(GrandChild.self)
|
||||
}
|
||||
struct GrandChild: TableRecord {
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "parent") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "grandChild") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
}
|
||||
try db.create(table: "child") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("parent")
|
||||
t.belongsTo("grandChild")
|
||||
}
|
||||
try db.execute(sql: """
|
||||
INSERT INTO parent (id) VALUES (1);
|
||||
INSERT INTO grandChild (id) VALUES (2);
|
||||
INSERT INTO child (id, parentId, grandChildId) VALUES (3, 1, 2);
|
||||
""")
|
||||
|
||||
do {
|
||||
let request = Parent.including(required: Parent.littlePuppies)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["littlePuppy"], ["id": 2])
|
||||
}
|
||||
|
||||
do {
|
||||
let request = Parent.including(required: Parent.kittens)
|
||||
let row = try Row.fetchOne(db, request)!
|
||||
XCTAssertEqual(row.unscoped, ["id": 1])
|
||||
XCTAssertEqual(Set(row.scopes.names), ["child"])
|
||||
XCTAssertEqual(row.scopesTree["kitten"], ["id": 2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/groue/GRDB.swift/discussions/1274
|
||||
func testDiscussion1274() throws {
|
||||
struct MuscleGroup: Codable, Equatable, FetchableRecord, TableRecord {
|
||||
var id: String
|
||||
|
||||
static let primaryMuscleGroups = hasMany(PrimaryMuscleGroup.self)
|
||||
static let primaryExercises = hasMany(
|
||||
Exercise.self,
|
||||
through: primaryMuscleGroups,
|
||||
using: PrimaryMuscleGroup.exercise
|
||||
)
|
||||
}
|
||||
|
||||
struct Exercise: Codable, Equatable, FetchableRecord, TableRecord {
|
||||
var id: String
|
||||
|
||||
// Primary Muscle Groups
|
||||
static let primaryMuscleGroups = hasMany(
|
||||
MuscleGroup.self,
|
||||
// Tested: this has the "primaryMuscleGroups" key, just
|
||||
// as Exercise.primaryMuscleGroups
|
||||
through: Exercise.hasMany(PrimaryMuscleGroup.self),
|
||||
using: PrimaryMuscleGroup.muscleGroup
|
||||
)
|
||||
.forKey("primaryMuscleGroups")
|
||||
}
|
||||
|
||||
struct PrimaryMuscleGroup: Codable, FetchableRecord, TableRecord {
|
||||
var muscleGroupId: String
|
||||
var exerciseId: String
|
||||
|
||||
static let muscleGroup = belongsTo(MuscleGroup.self)
|
||||
static let exercise = belongsTo(Exercise.self)
|
||||
}
|
||||
|
||||
struct CompleteExercise: Decodable, Equatable, FetchableRecord {
|
||||
var exercise: Exercise
|
||||
var primaryMuscleGroups: [MuscleGroup]
|
||||
}
|
||||
|
||||
dbConfiguration.prepareDatabase { db in
|
||||
db.trace { print("SQL > \($0)") }
|
||||
}
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "muscleGroup") { table in
|
||||
table.primaryKey("id", .text)
|
||||
}
|
||||
try db.create(table: "exercise") { table in
|
||||
table.primaryKey("id", .text)
|
||||
}
|
||||
try db.create(table: "primaryMuscleGroup") { table in
|
||||
table.column("muscleGroupId", .text).notNull().references("muscleGroup", onDelete: .cascade)
|
||||
table.column("exerciseId", .text).notNull().references("exercise", onDelete: .cascade)
|
||||
table.primaryKey(["muscleGroupId", "exerciseId"])
|
||||
}
|
||||
|
||||
try db.execute(sql: """
|
||||
INSERT INTO muscleGroup (id) VALUES ('1');
|
||||
INSERT INTO exercise (id) VALUES ('2');
|
||||
INSERT INTO primaryMuscleGroup (muscleGroupId, exerciseId) VALUES ('1', '2');
|
||||
""")
|
||||
|
||||
let request = Exercise.including(all: Exercise.primaryMuscleGroups)
|
||||
|
||||
do {
|
||||
// Test records
|
||||
let results = try request
|
||||
.asRequest(of: CompleteExercise.self)
|
||||
.fetchAll(db)
|
||||
XCTAssertEqual(results, [
|
||||
CompleteExercise(
|
||||
exercise: Exercise(id: "2"),
|
||||
primaryMuscleGroups: [
|
||||
MuscleGroup(id: "1"),
|
||||
]),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1095
File diff suppressed because it is too large
Load Diff
+399
@@ -0,0 +1,399 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// A <- B
|
||||
private struct A : TableRecord {
|
||||
static let databaseTableName = "a"
|
||||
static let b = hasOne(B.self)
|
||||
static let restrictedB = hasOne(RestrictedB.self)
|
||||
static let extendedB = hasOne(ExtendedB.self)
|
||||
}
|
||||
|
||||
private struct B : TableRecord {
|
||||
static let a = belongsTo(A.self)
|
||||
static let databaseTableName = "b"
|
||||
}
|
||||
|
||||
private struct RestrictedB : TableRecord {
|
||||
static let databaseTableName = "b"
|
||||
static let databaseSelection: [any SQLSelectable] = [Column("name")]
|
||||
}
|
||||
|
||||
private struct ExtendedB : TableRecord {
|
||||
static let databaseTableName = "b"
|
||||
static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID]
|
||||
}
|
||||
|
||||
/// Test SQL generation
|
||||
class AssociationHasOneSQLDerivationTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "a") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
}
|
||||
try db.create(table: "b") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("aid", .integer).references("a")
|
||||
t.column("name", .text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.restrictedB), """
|
||||
SELECT "a".*, "b"."name" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.extendedB), """
|
||||
SELECT "a".*, "b".*, "b"."rowid" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A.including(required: A.b
|
||||
.select(Column("name")))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."name" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b
|
||||
.select(
|
||||
AllColumns(),
|
||||
Column.rowID))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".*, "b"."rowid" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let aAlias = TableAlias()
|
||||
let request = A
|
||||
.aliased(aAlias)
|
||||
.including(required: A.b
|
||||
.select(
|
||||
Column("name"),
|
||||
(Column("id") + aAlias[Column("id")]).forKey("foo")))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."name", "b"."id" + "a"."id" AS "foo" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFilteredAssociationImpactsJoinOnClause() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(Column("name") != nil))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(key: 1))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."id" = 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(keys: [1, 2, 3]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."id" IN (1, 2, 3))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(key: ["id": 1]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."id" = 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.b.filter(keys: [["id": 1], ["id": 2]]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."aid" = "a"."id") AND (("b"."id" = 1) OR ("b"."id" = 2))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let bAlias = TableAlias(name: "customB")
|
||||
let request = A.including(required: A.b
|
||||
.aliased(bAlias)
|
||||
.filter(sql: "customB.name = ?", arguments: ["foo"]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "customB".* \
|
||||
FROM "a" \
|
||||
JOIN "b" "customB" ON ("customB"."aid" = "a"."id") AND (customB.name = 'foo')
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFilterAssociationInWhereClause() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let bAlias = TableAlias()
|
||||
let request = A
|
||||
.including(required: A.b.aliased(bAlias))
|
||||
.filter(bAlias[Column("name")] != nil)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."aid" = "a"."id" \
|
||||
WHERE "b"."name" IS NOT NULL
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testAssociationOrderBubbleUp() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let aBase = A.all().aliased(TableAlias(name: "a"))
|
||||
let abBase = A.b.aliased(TableAlias(name: "ab"))
|
||||
let abaBase = B.a.aliased(TableAlias(name: "aba"))
|
||||
|
||||
let aTransforms = [
|
||||
{ (r: QueryInterfaceRequest<A>) in return r },
|
||||
{ (r: QueryInterfaceRequest<A>) in return r.order(Column("id")) },
|
||||
{ (r: QueryInterfaceRequest<A>) in return r.order(Column("id")).reversed() },
|
||||
]
|
||||
let abTransforms = [
|
||||
{ (r: HasOneAssociation<A, B>) in return r },
|
||||
{ (r: HasOneAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) },
|
||||
{ (r: HasOneAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) },
|
||||
{ (r: HasOneAssociation<A, B>) in return r.order(Column("name")).reversed() },
|
||||
{ (r: HasOneAssociation<A, B>) in return r.reversed() },
|
||||
]
|
||||
let abaTransforms = [
|
||||
{ (r: BelongsToAssociation<B, A>) in return r },
|
||||
{ (r: BelongsToAssociation<B, A>) in return r.order(Column("id")) },
|
||||
{ (r: BelongsToAssociation<B, A>) in return r.order(Column("id")).reversed() },
|
||||
]
|
||||
|
||||
var sqls: [String] = []
|
||||
for aTransform in aTransforms {
|
||||
for abTransform in abTransforms {
|
||||
for abaTransform in abaTransforms {
|
||||
let request = aTransform(aBase)
|
||||
.including(required: abTransform(abBase)
|
||||
.including(required: abaTransform(abaBase)))
|
||||
try sqls.append(request.build(db).sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prefix = """
|
||||
SELECT "a".*, "ab".*, "aba".* \
|
||||
FROM "a" \
|
||||
JOIN "b" "ab" ON "ab"."aid" = "a"."id" \
|
||||
JOIN "a" "aba" ON "aba"."id" = "ab"."aid"
|
||||
"""
|
||||
let orderClauses = sqls.map { sql -> String in
|
||||
let prefixEndIndex = sql.index(sql.startIndex, offsetBy: prefix.count)
|
||||
return String(sql.suffix(from: prefixEndIndex))
|
||||
}
|
||||
XCTAssertEqual(orderClauses, [
|
||||
// a: { (r: QueryInterfaceRequest<A>) in return r },
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"ab\".\"name\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"ab\".\"name\" DESC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"ab\".\"name\" DESC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"aba\".\"id\"",
|
||||
|
||||
|
||||
// a: { (r: QueryInterfaceRequest<A>) in return r.order(Column("id")) },
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"ab\".\"name\" DESC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\", \"aba\".\"id\"",
|
||||
|
||||
|
||||
// a: { (r: QueryInterfaceRequest<A>) in return r.order(Column("id")).reversed() }
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name"), Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).order(Column("id").desc) }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"id\" ASC, \"aba\".\"id\"",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.order(Column("name")).reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\", \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"ab\".\"name\", \"aba\".\"id\" DESC",
|
||||
|
||||
// ab: { (r: BelongsToAssociation<A, B>) in return r.reversed() }
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r }
|
||||
" ORDER BY \"a\".\"id\" DESC",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")) }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\"",
|
||||
// aba: { (r: HasOneAssociation<B, A>) in return r.order(Column("id")).reversed() }
|
||||
" ORDER BY \"a\".\"id\" DESC, \"aba\".\"id\" DESC",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Test if and only if we really want to build an association from any request
|
||||
// func testAssociationFromRequest() throws {
|
||||
// let dbQueue = try makeDatabaseQueue()
|
||||
// try dbQueue.inDatabase { db in
|
||||
// do {
|
||||
// let bRequest = B
|
||||
// .filter(Column("name") != nil)
|
||||
// .order(Column("id"))
|
||||
// let association = A.hasOne(bRequest)
|
||||
// let request = A.including(required: association)
|
||||
// try assertEqualSQL(db, request, """
|
||||
// SELECT "a".*, "b".* \
|
||||
// FROM "a" \
|
||||
// JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) \
|
||||
// ORDER BY "b"."id"
|
||||
// """)
|
||||
// }
|
||||
// do {
|
||||
// let bRequest = RestrictedB
|
||||
// .filter(Column("name") != nil)
|
||||
// .order(Column("id"))
|
||||
// let association = A.hasOne(bRequest)
|
||||
// let request = A.including(required: association)
|
||||
// try assertEqualSQL(db, request, """
|
||||
// SELECT "a".*, "b"."name" \
|
||||
// FROM "a" \
|
||||
// JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) \
|
||||
// ORDER BY "b"."id"
|
||||
// """)
|
||||
// }
|
||||
// do {
|
||||
// let bRequest = ExtendedB
|
||||
// .select([Column("name")])
|
||||
// .filter(Column("name") != nil)
|
||||
// .order(Column("id"))
|
||||
// let association = A.hasOne(bRequest)
|
||||
// let request = A.including(required: association)
|
||||
// try assertEqualSQL(db, request, """
|
||||
// SELECT "a".*, "b"."name" \
|
||||
// FROM "a" \
|
||||
// JOIN "b" ON ("b"."aid" = "a"."id") AND ("b"."name" IS NOT NULL) \
|
||||
// ORDER BY "b"."id"
|
||||
// """)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,856 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
/// Test SQL generation
|
||||
class AssociationHasOneSQLTests: GRDBTestCase {
|
||||
|
||||
func testSingleColumnNoForeignKeyNoPrimaryKey() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["id"] = 1
|
||||
container["rowid"] = 2
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.column("id", .integer)
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.column("parentId", .integer)
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentId")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentId")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."rowid"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."rowid"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."rowid"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."rowid"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 2")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentId")], to: [Column("id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentId")], to: [Column("id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleColumnNoForeignKey() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["id"] = 1
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.column("parentId", .integer)
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentId")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentId")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 1")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentId")], to: [Column("id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentId")], to: [Column("id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleColumnSingleForeignKey() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["id"] = 1
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.belongsTo("parent")
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self),
|
||||
Parent.hasOne(Table(Child.databaseTableName)),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 1")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentId")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentId")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 1")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentId")], to: [Column("id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentId")], to: [Column("id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parentId" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parentId\" = 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleColumnSeveralForeignKeys() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["id"] = 1
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.belongsTo("parent1", inTable: "parents")
|
||||
t.belongsTo("parent2", inTable: "parents")
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent1Id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent1Id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parent1Id\" = 1")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent1Id")], to: [Column("id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent1Id")], to: [Column("id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent1Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parent1Id\" = 1")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent2Id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent2Id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parent2Id\" = 1")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent2Id")], to: [Column("id")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent2Id")], to: [Column("id")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON "children"."parent2Id" = "parents"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE \"parent2Id\" = 1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCompoundColumnNoForeignKeyNoPrimaryKey() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["a"] = 1
|
||||
container["b"] = 2
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.column("a", .integer)
|
||||
t.column("b", .integer)
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.column("parentA", .integer)
|
||||
t.column("parentB", .integer)
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")], to: [Column("a"), Column("b")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")], to: [Column("a"), Column("b")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parentA\" = 1) AND (\"parentB\" = 2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCompoundColumnNoForeignKey() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["a"] = 1
|
||||
container["b"] = 2
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.column("a", .integer)
|
||||
t.column("b", .integer)
|
||||
t.primaryKey(["a", "b"])
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.column("parentA", .integer)
|
||||
t.column("parentB", .integer)
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parentA\" = 1) AND (\"parentB\" = 2)")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")], to: [Column("a"), Column("b")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")], to: [Column("a"), Column("b")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parentA\" = 1) AND (\"parentB\" = 2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCompoundColumnSingleForeignKey() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["a"] = 1
|
||||
container["b"] = 2
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.column("a", .integer)
|
||||
t.column("b", .integer)
|
||||
t.primaryKey(["a", "b"])
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.column("parentA", .integer)
|
||||
t.column("parentB", .integer)
|
||||
t.foreignKey(["parentA", "parentB"], references: "parents")
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self),
|
||||
Parent.hasOne(Table(Child.databaseTableName)),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parentA\" = 1) AND (\"parentB\" = 2)")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parentA\" = 1) AND (\"parentB\" = 2)")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")], to: [Column("a"), Column("b")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parentA"), Column("parentB")], to: [Column("a"), Column("b")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parentA" = "parents"."a") AND ("children"."parentB" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parentA\" = 1) AND (\"parentB\" = 2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testCompoundColumnSeveralForeignKeys() throws {
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
}
|
||||
|
||||
struct Parent : TableRecord, EncodableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
func encode(to container: inout PersistenceContainer) {
|
||||
container["a"] = 1
|
||||
container["b"] = 2
|
||||
}
|
||||
}
|
||||
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try db.create(table: "parents") { t in
|
||||
t.column("a", .integer)
|
||||
t.column("b", .integer)
|
||||
t.primaryKey(["a", "b"])
|
||||
}
|
||||
try db.create(table: "children") { t in
|
||||
t.column("parent1A", .integer)
|
||||
t.column("parent1B", .integer)
|
||||
t.column("parent2A", .integer)
|
||||
t.column("parent2B", .integer)
|
||||
t.foreignKey(["parent1A", "parent1B"], references: "parents")
|
||||
t.foreignKey(["parent2A", "parent2B"], references: "parents")
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.inDatabase { db in
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent1A"), Column("parent1B")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent1A"), Column("parent1B")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parent1A\" = 1) AND (\"parent1B\" = 2)")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent1A"), Column("parent1B")], to: [Column("a"), Column("b")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent1A"), Column("parent1B")], to: [Column("a"), Column("b")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent1A" = "parents"."a") AND ("children"."parent1B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parent1A\" = 1) AND (\"parent1B\" = 2)")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent2A"), Column("parent2B")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent2A"), Column("parent2B")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parent2A\" = 1) AND (\"parent2B\" = 2)")
|
||||
}
|
||||
|
||||
for association in [
|
||||
Parent.hasOne(Child.self,
|
||||
using: ForeignKey([Column("parent2A"), Column("parent2B")], to: [Column("a"), Column("b")])),
|
||||
Parent.hasOne(Table(Child.databaseTableName),
|
||||
using: ForeignKey([Column("parent2A"), Column("parent2B")], to: [Column("a"), Column("b")])),
|
||||
] {
|
||||
try assertEqualSQL(db, Parent.all().including(required: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().including(optional: association), """
|
||||
SELECT "parents".*, "children".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(required: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent.all().joining(optional: association), """
|
||||
SELECT "parents".* \
|
||||
FROM "parents" \
|
||||
LEFT JOIN "children" ON ("children"."parent2A" = "parents"."a") AND ("children"."parent2B" = "parents"."b")
|
||||
""")
|
||||
try assertEqualSQL(db, Parent().request(for: association), "SELECT * FROM \"children\" WHERE (\"parent2A\" = 1) AND (\"parent2B\" = 2)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testForeignKeyDefinitionFromColumn() {
|
||||
// This test pass if code compiles
|
||||
struct Parent : TableRecord {
|
||||
static let databaseTableName = "parents"
|
||||
enum Columns {
|
||||
static let id = Column("id")
|
||||
}
|
||||
static let child1 = hasOne(Child.self, using: Child.ForeignKeys.parent1)
|
||||
static let child2 = hasOne(Child.self, using: Child.ForeignKeys.parent2)
|
||||
}
|
||||
|
||||
struct Child : TableRecord {
|
||||
static let databaseTableName = "children"
|
||||
enum Columns {
|
||||
static let parentId = Column("parentId")
|
||||
}
|
||||
enum ForeignKeys {
|
||||
static let parent1 = ForeignKey([Columns.parentId])
|
||||
static let parent2 = ForeignKey([Columns.parentId], to: [Parent.Columns.id])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct A: Codable, FetchableRecord, PersistableRecord {
|
||||
static let b = belongsTo(B.self)
|
||||
static let c = hasOne(C.self, through: b, using: B.c)
|
||||
var id: Int64
|
||||
var bId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct B: Codable, FetchableRecord, PersistableRecord {
|
||||
static let c = belongsTo(C.self)
|
||||
var id: Int64
|
||||
var cId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct C: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct AWithRequiredC: Decodable, FetchableRecord {
|
||||
var a: A
|
||||
var c: C
|
||||
}
|
||||
|
||||
private struct AWithOptionalC: Decodable, FetchableRecord {
|
||||
var a: A
|
||||
var optionalC: C?
|
||||
static let c = A.c.forKey(CodingKeys.optionalC)
|
||||
}
|
||||
|
||||
private struct AWithRequiredBAndOptionalC: Decodable, FetchableRecord {
|
||||
var a: A
|
||||
var b: B
|
||||
var c: C?
|
||||
}
|
||||
|
||||
/// Test support for FetchableRecord records
|
||||
class AssociationHasOneThroughDecodableRecordTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "c") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "b") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("c")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("b")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try C(id: 1, name: "c1").insert(db)
|
||||
try B(id: 1, cId: 1, name: "b1").insert(db)
|
||||
try B(id: 2, cId: nil, name: "b2").insert(db)
|
||||
try A(id: 1, bId: 1, name: "a1").insert(db)
|
||||
try A(id: 2, bId: 2, name: "a2").insert(db)
|
||||
try A(id: 3, bId: nil, name: "a3").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(required: A.c)
|
||||
.order(sql: "a.id")
|
||||
.asRequest(of: AWithRequiredC.self)
|
||||
let records = try dbQueue.inDatabase(request.fetchAll)
|
||||
|
||||
XCTAssertEqual(records.count, 1)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].c.id, 1)
|
||||
XCTAssertEqual(records[0].c.name, "c1")
|
||||
}
|
||||
|
||||
func testIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(optional: AWithOptionalC.c)
|
||||
.order(sql: "a.id")
|
||||
.asRequest(of: AWithOptionalC.self)
|
||||
let records = try dbQueue.inDatabase(request.fetchAll)
|
||||
|
||||
XCTAssertEqual(records.count, 3)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].optionalC!.id, 1)
|
||||
XCTAssertEqual(records[0].optionalC!.name, "c1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bId, 2)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertNil(records[1].optionalC)
|
||||
|
||||
XCTAssertEqual(records[2].a.id, 3)
|
||||
XCTAssertNil(records[2].a.bId)
|
||||
XCTAssertEqual(records[2].a.name, "a3")
|
||||
XCTAssertNil(records[2].optionalC)
|
||||
}
|
||||
|
||||
func testJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.joining(required: A.c)
|
||||
.order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase(request.fetchAll)
|
||||
|
||||
XCTAssertEqual(records.count, 1)
|
||||
|
||||
XCTAssertEqual(records[0].id, 1)
|
||||
XCTAssertEqual(records[0].bId, 1)
|
||||
XCTAssertEqual(records[0].name, "a1")
|
||||
}
|
||||
|
||||
func testJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.joining(optional: A.c)
|
||||
.order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase(request.fetchAll)
|
||||
|
||||
XCTAssertEqual(records.count, 3)
|
||||
|
||||
XCTAssertEqual(records[0].id, 1)
|
||||
XCTAssertEqual(records[0].bId, 1)
|
||||
XCTAssertEqual(records[0].name, "a1")
|
||||
|
||||
XCTAssertEqual(records[1].id, 2)
|
||||
XCTAssertEqual(records[1].bId, 2)
|
||||
XCTAssertEqual(records[1].name, "a2")
|
||||
|
||||
XCTAssertEqual(records[2].id, 3)
|
||||
XCTAssertNil(records[2].bId)
|
||||
XCTAssertEqual(records[2].name, "a3")
|
||||
}
|
||||
|
||||
func testIncludingOptionalIncludingRequiredPivot() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(optional: A.c)
|
||||
.including(required: A.b)
|
||||
.order(sql: "a.id")
|
||||
.asRequest(of: AWithRequiredBAndOptionalC.self)
|
||||
let records = try dbQueue.inDatabase(request.fetchAll)
|
||||
|
||||
XCTAssertEqual(records.count, 2)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].b.id, 1)
|
||||
XCTAssertEqual(records[0].b.name, "b1")
|
||||
XCTAssertEqual(records[0].c!.id, 1)
|
||||
XCTAssertEqual(records[0].c!.name, "c1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bId, 2)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertEqual(records[1].b.id, 2)
|
||||
XCTAssertEqual(records[1].b.name, "b2")
|
||||
XCTAssertNil(records[1].c)
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct A: Codable, FetchableRecord, PersistableRecord {
|
||||
static let b = belongsTo(B.self)
|
||||
static let c = hasOne(C.self, through: b, using: B.c)
|
||||
var id: Int64
|
||||
var bId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct B: Codable, FetchableRecord, PersistableRecord {
|
||||
static let c = belongsTo(C.self)
|
||||
var id: Int64
|
||||
var cId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct C: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct AWithRequiredC: FetchableRecord {
|
||||
var a: A
|
||||
var c: C
|
||||
|
||||
init(row: Row) throws {
|
||||
a = try A(row: row)
|
||||
c = row["c"]
|
||||
}
|
||||
}
|
||||
|
||||
private struct AWithOptionalC: FetchableRecord {
|
||||
var a: A
|
||||
var c: C?
|
||||
|
||||
init(row: Row) throws {
|
||||
a = try A(row: row)
|
||||
c = row["c"]
|
||||
}
|
||||
}
|
||||
|
||||
private struct AWithRequiredBAndOptionalC: FetchableRecord {
|
||||
var a: A
|
||||
var b: B
|
||||
var c: C?
|
||||
|
||||
init(row: Row) throws {
|
||||
a = try A(row: row)
|
||||
b = row["b"]
|
||||
c = row["c"]
|
||||
}
|
||||
}
|
||||
|
||||
private struct AWithCName: FetchableRecord {
|
||||
var a: A
|
||||
var cName: String?
|
||||
|
||||
init(row: Row) throws {
|
||||
a = try A(row: row)
|
||||
cName = row["cName"]
|
||||
}
|
||||
}
|
||||
|
||||
private struct AWithRequiredBNameAndOptionalCName: FetchableRecord {
|
||||
var a: A
|
||||
var bName: String
|
||||
var cName: String?
|
||||
|
||||
init(row: Row) throws {
|
||||
a = try A(row: row)
|
||||
bName = row["bName"]
|
||||
cName = row["cName"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Test support for FetchableRecord records
|
||||
class AssociationHasOneThroughFetchableRecordTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "c") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "b") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("c")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("b")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try C(id: 1, name: "c1").insert(db)
|
||||
try B(id: 1, cId: 1, name: "b1").insert(db)
|
||||
try B(id: 2, cId: nil, name: "b2").insert(db)
|
||||
try A(id: 1, bId: 1, name: "a1").insert(db)
|
||||
try A(id: 2, bId: 2, name: "a2").insert(db)
|
||||
try A(id: 3, bId: nil, name: "a3").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.c).order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try AWithRequiredC.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 1)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].c.id, 1)
|
||||
XCTAssertEqual(records[0].c.name, "c1")
|
||||
}
|
||||
|
||||
func testAnnotatedWithRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.annotated(withRequired: A.c.select(Column("name").forKey("cName"))).order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try AWithCName.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 1)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].cName, "c1")
|
||||
}
|
||||
|
||||
func testIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(optional: A.c).order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try AWithOptionalC.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 3)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].c!.id, 1)
|
||||
XCTAssertEqual(records[0].c!.name, "c1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bId, 2)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertNil(records[1].c)
|
||||
|
||||
XCTAssertEqual(records[2].a.id, 3)
|
||||
XCTAssertNil(records[2].a.bId)
|
||||
XCTAssertEqual(records[2].a.name, "a3")
|
||||
XCTAssertNil(records[2].c)
|
||||
}
|
||||
|
||||
func testAnnotatedWithOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.annotated(withOptional: A.c.select(Column("name").forKey("cName"))).order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try AWithCName.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 3)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].cName, "c1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bId, 2)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertNil(records[1].cName)
|
||||
|
||||
XCTAssertEqual(records[2].a.id, 3)
|
||||
XCTAssertNil(records[2].a.bId)
|
||||
XCTAssertEqual(records[2].a.name, "a3")
|
||||
XCTAssertNil(records[2].cName)
|
||||
}
|
||||
|
||||
func testJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(required: A.c).order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try A.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 1)
|
||||
|
||||
XCTAssertEqual(records[0].id, 1)
|
||||
XCTAssertEqual(records[0].bId, 1)
|
||||
XCTAssertEqual(records[0].name, "a1")
|
||||
}
|
||||
|
||||
func testJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(optional: A.c).order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try A.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 3)
|
||||
|
||||
XCTAssertEqual(records[0].id, 1)
|
||||
XCTAssertEqual(records[0].bId, 1)
|
||||
XCTAssertEqual(records[0].name, "a1")
|
||||
|
||||
XCTAssertEqual(records[1].id, 2)
|
||||
XCTAssertEqual(records[1].bId, 2)
|
||||
XCTAssertEqual(records[1].name, "a2")
|
||||
|
||||
XCTAssertEqual(records[2].id, 3)
|
||||
XCTAssertNil(records[2].bId)
|
||||
XCTAssertEqual(records[2].name, "a3")
|
||||
}
|
||||
|
||||
func testIncludingOptionalIncludingRequiredPivot() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(optional: A.c)
|
||||
.including(required: A.b)
|
||||
.order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try AWithRequiredBAndOptionalC.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 2)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].b.id, 1)
|
||||
XCTAssertEqual(records[0].b.name, "b1")
|
||||
XCTAssertEqual(records[0].c!.id, 1)
|
||||
XCTAssertEqual(records[0].c!.name, "c1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bId, 2)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertEqual(records[1].b.id, 2)
|
||||
XCTAssertEqual(records[1].b.name, "b2")
|
||||
XCTAssertNil(records[1].c)
|
||||
}
|
||||
|
||||
func testAnnotatedWithOptionalAnnotatedWithRequiredPivot() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.annotated(withOptional: A.c.select(Column("name").forKey("cName")))
|
||||
.annotated(withRequired: A.b.select(Column("name").forKey("bName")))
|
||||
.order(sql: "a.id")
|
||||
let records = try dbQueue.inDatabase { try AWithRequiredBNameAndOptionalCName.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(records.count, 2)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bId, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].bName, "b1")
|
||||
XCTAssertEqual(records[0].cName, "c1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bId, 2)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertEqual(records[1].bName, "b2")
|
||||
XCTAssertNil(records[1].cName)
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct A: Codable, FetchableRecord, PersistableRecord {
|
||||
static let defaultB = belongsTo(B.self)
|
||||
static let defaultC = hasOne(C.self, through: defaultB, using: B.c)
|
||||
static let customC1 = hasOne(C.self, through: defaultB.forKey("customB"), using: B.c)
|
||||
static let customC2 = hasOne(C.self, through: defaultB, using: B.c).forKey("customC2")
|
||||
static let customC3 = hasOne(C.self, through: defaultB, using: B.c.forKey("customC3"))
|
||||
var id: Int64
|
||||
var bId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct B: Codable, FetchableRecord, PersistableRecord {
|
||||
static let c = belongsTo(C.self)
|
||||
var id: Int64
|
||||
var cId: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct C: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
/// Test row scopes
|
||||
class AssociationHasOneThroughRowscopeTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "c") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "b") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("c")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("b")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try C(id: 1, name: "c1").insert(db)
|
||||
try B(id: 1, cId: 1, name: "b1").insert(db)
|
||||
try B(id: 2, cId: nil, name: "b2").insert(db)
|
||||
try A(id: 1, bId: 1, name: "a1").insert(db)
|
||||
try A(id: 2, bId: 2, name: "a2").insert(db)
|
||||
try A(id: 3, bId: nil, name: "a3").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testJoiningDoesNotUseAnyRowAdapter() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let request = A.joining(required: A.defaultC)
|
||||
let adapter = try request.makePreparedRequest(db, forSingleResult: false).adapter
|
||||
XCTAssertNil(adapter)
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultScopeIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.defaultC).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, ["id":1, "name":"c1"])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.annotated(withRequired: A.defaultC).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "id":1, "name":"c1"])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithRequiredCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.annotated(withRequired: A.defaultC.select(Column("name").forKey("cName"))).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "cName":"c1"])
|
||||
}
|
||||
|
||||
func testDefaultScopeIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(optional: A.defaultC).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, ["id":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "bId":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!.unscoped, ["id":nil, "name":nil])
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":3, "bId":nil, "name":"a3"])
|
||||
XCTAssertEqual(Set(rows[2].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[2].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[2].scopes["b"]!.scopes["c"]!.unscoped, ["id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.annotated(withOptional: A.defaultC).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "id":1, "name":"c1"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "bId":2, "name":"a2", "id":nil, "name":nil])
|
||||
XCTAssertEqual(rows[2], ["id":3, "bId":nil, "name":"a3", "id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithOptionalCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.annotated(withOptional: A.defaultC.select(Column("name").forKey("cName"))).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "cName":"c1"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "bId":2, "name":"a2", "cName":nil])
|
||||
XCTAssertEqual(rows[2], ["id":3, "bId":nil, "name":"a3", "cName":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(required: A.defaultC).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testDefaultScopeJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.joining(optional: A.defaultC).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 3)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertTrue(rows[0].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "bId":2, "name":"a2"])
|
||||
XCTAssertTrue(rows[1].scopes.names.isEmpty)
|
||||
|
||||
XCTAssertEqual(rows[2].unscoped, ["id":3, "bId":nil, "name":"a3"])
|
||||
XCTAssertTrue(rows[2].scopes.names.isEmpty)
|
||||
}
|
||||
|
||||
func testCustomC1() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.customC1).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["customB"])
|
||||
XCTAssertEqual(rows[0].scopes["customB"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["customB"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["customB"]!.scopes["c"]!.unscoped, ["id":1, "name":"c1"])
|
||||
}
|
||||
|
||||
func testCustomC2() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.customC2).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["customC2"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["customC2"]!.unscoped, ["id":1, "name":"c1"])
|
||||
}
|
||||
|
||||
func testCustomC3() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A.including(required: A.customC3).order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
XCTAssertEqual(rows.count, 1)
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, [:])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["customC3"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["customC3"]!.unscoped, ["id":1, "name":"c1"])
|
||||
}
|
||||
|
||||
func testDefaultScopeIncludingOptionalIncludingRequiredPivot() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(optional: A.defaultC)
|
||||
.including(required: A.defaultB)
|
||||
.order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0].unscoped, ["id":1, "bId":1, "name":"a1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.unscoped, ["id":1, "cId":1, "name":"b1"])
|
||||
XCTAssertEqual(Set(rows[0].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[0].scopes["b"]!.scopes["c"]!.unscoped, ["id":1, "name":"c1"])
|
||||
|
||||
XCTAssertEqual(rows[1].unscoped, ["id":2, "bId":2, "name":"a2"])
|
||||
XCTAssertEqual(Set(rows[1].scopes.names), ["b"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.unscoped, ["id":2, "cId":nil, "name":"b2"])
|
||||
XCTAssertEqual(Set(rows[1].scopes["b"]!.scopes.names), ["c"])
|
||||
XCTAssertEqual(rows[1].scopes["b"]!.scopes["c"]!.unscoped, ["id":nil, "name":nil])
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithOptionalAnnotatedWithRequiredPivot() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = A
|
||||
.annotated(withOptional: A.defaultC)
|
||||
.annotated(withRequired: A.defaultB)
|
||||
.order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "id":1, "name":"c1", "id":1, "cId":1, "name":"b1"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "bId":2, "name":"a2", "id":nil, "name":nil, "id":2, "cId":nil, "name":"b2"])
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.annotated(withRequired: A.defaultB)
|
||||
.annotated(withOptional: A.defaultC)
|
||||
.order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "id":1, "cId":1, "name":"b1", "id":1, "name":"c1"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "bId":2, "name":"a2", "id":2, "cId":nil, "name":"b2", "id":nil, "name":nil])
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultScopeAnnotatedWithOptionalAnnotatedWithRequiredPivotCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
do {
|
||||
let request = A
|
||||
.annotated(withOptional: A.defaultC.select(Column("name").forKey("cName")))
|
||||
.annotated(withRequired: A.defaultB.select(Column("name").forKey("bName")))
|
||||
.order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "cName":"c1", "bName":"b1"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "bId":2, "name":"a2", "cName":nil, "bName":"b2"])
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.annotated(withRequired: A.defaultB.select(Column("name").forKey("bName")))
|
||||
.annotated(withOptional: A.defaultC.select(Column("name").forKey("cName")))
|
||||
.order(sql: "a.id")
|
||||
let rows = try dbQueue.inDatabase { try Row.fetchAll($0, request) }
|
||||
|
||||
XCTAssertEqual(rows.count, 2)
|
||||
|
||||
XCTAssertEqual(rows[0], ["id":1, "bId":1, "name":"a1", "bName":"b1", "cName":"c1"])
|
||||
XCTAssertEqual(rows[1], ["id":2, "bId":2, "name":"a2", "bName":"b2", "cName":nil])
|
||||
}
|
||||
}
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
private struct A: TableRecord {
|
||||
static let b = belongsTo(B.self)
|
||||
static let c = hasOne(C.self, through: b, using: B.c)
|
||||
static let restrictedC = hasOne(RestrictedC.self, through: b, using: B.restrictedC)
|
||||
static let extendedC = hasOne(ExtendedC.self, through: b, using: B.extendedC)
|
||||
}
|
||||
|
||||
private struct B: TableRecord {
|
||||
static let c = belongsTo(C.self)
|
||||
static let restrictedC = belongsTo(RestrictedC.self)
|
||||
static let extendedC = belongsTo(ExtendedC.self)
|
||||
}
|
||||
|
||||
private struct C: TableRecord {
|
||||
}
|
||||
|
||||
private struct RestrictedC : TableRecord {
|
||||
static let databaseTableName = "c"
|
||||
static let databaseSelection: [any SQLSelectable] = [Column("name")]
|
||||
}
|
||||
|
||||
private struct ExtendedC : TableRecord {
|
||||
static let databaseTableName = "c"
|
||||
static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID]
|
||||
}
|
||||
|
||||
/// Test SQL generation
|
||||
class AssociationHasOneThroughSQLDerivationTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "c") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "b") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("c")
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("b")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.c), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.restrictedC), """
|
||||
SELECT "a".*, "c"."name" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.extendedC), """
|
||||
SELECT "a".*, "c".*, "c"."rowid" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A.including(required: A.c
|
||||
.select(Column("name")))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c"."name" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.c
|
||||
.select(
|
||||
AllColumns(),
|
||||
Column.rowID))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".*, "c"."rowid" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let aAlias = TableAlias()
|
||||
let request = A
|
||||
.aliased(aAlias)
|
||||
.including(required: A.c
|
||||
.select(
|
||||
Column("name"),
|
||||
(Column("id") + aAlias[Column("id")]).forKey("foo")))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c"."name", "c"."id" + "a"."id" AS "foo" \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId"
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFilteredAssociationImpactsJoinOnClause() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A.including(required: A.c.filter(Column("name") != nil))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON ("c"."id" = "b"."cId") AND ("c"."name" IS NOT NULL)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.c.filter(key: 1))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON ("c"."id" = "b"."cId") AND ("c"."id" = 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.c.filter(keys: [1, 2, 3]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON ("c"."id" = "b"."cId") AND ("c"."id" IN (1, 2, 3))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.c.filter(key: ["id": 1]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON ("c"."id" = "b"."cId") AND ("c"."id" = 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A.including(required: A.c.filter(keys: [["id": 1], ["id": 2]]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON ("c"."id" = "b"."cId") AND (("c"."id" = 1) OR ("c"."id" = 2))
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let cAlias = TableAlias(name: "customC")
|
||||
let request = A.including(required: A.c
|
||||
.aliased(cAlias)
|
||||
.filter(sql: "customC.name = ?", arguments: ["foo"]))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "customC".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" "customC" ON ("customC"."id" = "b"."cId") AND (customC.name = 'foo')
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testFilterAssociationInWhereClause() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
let cAlias = TableAlias()
|
||||
let request = A
|
||||
.including(required: A.c.aliased(cAlias))
|
||||
.filter(cAlias[Column("name")] != nil)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
WHERE "c"."name" IS NOT NULL
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testOrderAssociation() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.c.order(Column("name"))), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "c"."name"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.orderByPrimaryKey()), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "c"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.orderByPrimaryKey()).orderByPrimaryKey(), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "a"."id", "c"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.orderByPrimaryKey().reversed()).orderByPrimaryKey(), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "a"."id", "c"."id" DESC
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.order(Column("name"))).reversed(), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "c"."name" DESC
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.orderByPrimaryKey()).reversed(), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "c"."id" DESC
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.orderByPrimaryKey()).orderByPrimaryKey().reversed(), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "a"."id" DESC, "c"."id" DESC
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.c.orderByPrimaryKey().reversed()).orderByPrimaryKey().reversed(), """
|
||||
SELECT "a".*, "c".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bId" \
|
||||
JOIN "c" ON "c"."id" = "b"."cId" \
|
||||
ORDER BY "a"."id" DESC, "c"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
+1792
File diff suppressed because it is too large
Load Diff
+231
@@ -0,0 +1,231 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// A -> B
|
||||
// A -> D
|
||||
private struct A: Codable, FetchableRecord, PersistableRecord {
|
||||
static let databaseTableName = "a"
|
||||
static let b = belongsTo(B.self)
|
||||
static let d = belongsTo(D.self)
|
||||
var id: Int64
|
||||
var bid: Int64?
|
||||
var did: Int64?
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct B: Codable, FetchableRecord, PersistableRecord {
|
||||
static let a = hasOne(A.self)
|
||||
static let databaseTableName = "b"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct D: Codable, FetchableRecord, PersistableRecord {
|
||||
static let a = hasOne(A.self)
|
||||
static let databaseTableName = "d"
|
||||
var id: Int64
|
||||
var name: String
|
||||
}
|
||||
|
||||
private struct AWithRequiredBD: Decodable, FetchableRecord {
|
||||
var a: A
|
||||
var b: B
|
||||
var d: D
|
||||
static let b = A.b.forKey(CodingKeys.b)
|
||||
static let d = A.d.forKey(CodingKeys.d)
|
||||
}
|
||||
|
||||
private struct AWithOptionalBD: Decodable, FetchableRecord {
|
||||
var a: A
|
||||
var b: B?
|
||||
var d: D?
|
||||
static let b = A.b.forKey(CodingKeys.b)
|
||||
static let d = A.d.forKey(CodingKeys.d)
|
||||
}
|
||||
|
||||
/// Test support for Decodable records
|
||||
class AssociationParallelDecodableRecordTests: GRDBTestCase {
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "b") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "d") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("name", .text)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("bid", .integer).references("b")
|
||||
t.column("did", .integer).references("d")
|
||||
t.column("name", .text)
|
||||
}
|
||||
|
||||
try B(id: 1, name: "b1").insert(db)
|
||||
try B(id: 2, name: "b2").insert(db)
|
||||
try B(id: 3, name: "b3").insert(db)
|
||||
try D(id: 1, name: "d1").insert(db)
|
||||
try A(id: 1, bid: 1, did: 1, name: "a1").insert(db)
|
||||
try A(id: 2, bid: 1, did: nil, name: "a2").insert(db)
|
||||
try A(id: 3, bid: 2, did: 1, name: "a3").insert(db)
|
||||
try A(id: 4, bid: 2, did: nil, name: "a4").insert(db)
|
||||
try A(id: 5, bid: nil, did: 1, name: "a5").insert(db)
|
||||
try A(id: 6, bid: nil, did: nil, name: "a6").insert(db)
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(required: AWithRequiredBD.b)
|
||||
.including(required: AWithRequiredBD.d)
|
||||
.order(sql: "a.id, b.id, d.id")
|
||||
.asRequest(of: AWithRequiredBD.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
|
||||
XCTAssertEqual(records.count, 2)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bid, 1)
|
||||
XCTAssertEqual(records[0].a.did, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].b.id, 1)
|
||||
XCTAssertEqual(records[0].b.name, "b1")
|
||||
XCTAssertEqual(records[0].d.id, 1)
|
||||
XCTAssertEqual(records[0].d.name, "d1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 3)
|
||||
XCTAssertEqual(records[1].a.bid, 2)
|
||||
XCTAssertEqual(records[1].a.did, 1)
|
||||
XCTAssertEqual(records[1].a.name, "a3")
|
||||
XCTAssertEqual(records[1].b.id, 2)
|
||||
XCTAssertEqual(records[1].b.name, "b2")
|
||||
XCTAssertEqual(records[1].d.id, 1)
|
||||
XCTAssertEqual(records[1].d.name, "d1")
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.including(optional: AWithOptionalBD.b)
|
||||
.including(optional: AWithOptionalBD.d)
|
||||
.order(sql: "a.id, b.id, d.id")
|
||||
.asRequest(of: AWithOptionalBD.self)
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
|
||||
XCTAssertEqual(records.count, 6)
|
||||
|
||||
XCTAssertEqual(records[0].a.id, 1)
|
||||
XCTAssertEqual(records[0].a.bid, 1)
|
||||
XCTAssertEqual(records[0].a.did, 1)
|
||||
XCTAssertEqual(records[0].a.name, "a1")
|
||||
XCTAssertEqual(records[0].b!.id, 1)
|
||||
XCTAssertEqual(records[0].b!.name, "b1")
|
||||
XCTAssertEqual(records[0].d!.id, 1)
|
||||
XCTAssertEqual(records[0].d!.name, "d1")
|
||||
|
||||
XCTAssertEqual(records[1].a.id, 2)
|
||||
XCTAssertEqual(records[1].a.bid, 1)
|
||||
XCTAssertEqual(records[1].a.did, nil)
|
||||
XCTAssertEqual(records[1].a.name, "a2")
|
||||
XCTAssertEqual(records[1].b!.id, 1)
|
||||
XCTAssertEqual(records[1].b!.name, "b1")
|
||||
XCTAssertNil(records[1].d)
|
||||
|
||||
XCTAssertEqual(records[2].a.id, 3)
|
||||
XCTAssertEqual(records[2].a.bid, 2)
|
||||
XCTAssertEqual(records[2].a.did, 1)
|
||||
XCTAssertEqual(records[2].a.name, "a3")
|
||||
XCTAssertEqual(records[2].b!.id, 2)
|
||||
XCTAssertEqual(records[2].b!.name, "b2")
|
||||
XCTAssertEqual(records[2].d!.id, 1)
|
||||
XCTAssertEqual(records[2].d!.name, "d1")
|
||||
|
||||
XCTAssertEqual(records[3].a.id, 4)
|
||||
XCTAssertEqual(records[3].a.bid, 2)
|
||||
XCTAssertEqual(records[3].a.did, nil)
|
||||
XCTAssertEqual(records[3].a.name, "a4")
|
||||
XCTAssertEqual(records[3].b!.id, 2)
|
||||
XCTAssertEqual(records[3].b!.name, "b2")
|
||||
XCTAssertNil(records[3].d)
|
||||
|
||||
XCTAssertEqual(records[4].a.id, 5)
|
||||
XCTAssertEqual(records[4].a.bid, nil)
|
||||
XCTAssertEqual(records[4].a.did, 1)
|
||||
XCTAssertEqual(records[4].a.name, "a5")
|
||||
XCTAssertNil(records[4].b)
|
||||
XCTAssertEqual(records[4].d!.id, 1)
|
||||
XCTAssertEqual(records[4].d!.name, "d1")
|
||||
|
||||
XCTAssertEqual(records[5].a.id, 6)
|
||||
XCTAssertEqual(records[5].a.bid, nil)
|
||||
XCTAssertEqual(records[5].a.did, nil)
|
||||
XCTAssertEqual(records[5].a.name, "a6")
|
||||
XCTAssertNil(records[5].b)
|
||||
XCTAssertNil(records[5].d)
|
||||
}
|
||||
|
||||
func testParallelTwoJoiningRequired() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.joining(required: A.b)
|
||||
.joining(required: A.d)
|
||||
.order(sql: "a.id, b.id, d.id")
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
|
||||
XCTAssertEqual(records.count, 2)
|
||||
|
||||
XCTAssertEqual(records[0].id, 1)
|
||||
XCTAssertEqual(records[0].bid, 1)
|
||||
XCTAssertEqual(records[0].did, 1)
|
||||
XCTAssertEqual(records[0].name, "a1")
|
||||
|
||||
XCTAssertEqual(records[1].id, 3)
|
||||
XCTAssertEqual(records[1].bid, 2)
|
||||
XCTAssertEqual(records[1].did, 1)
|
||||
XCTAssertEqual(records[1].name, "a3")
|
||||
}
|
||||
|
||||
func testParallelTwoJoiningOptional() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
let request = A
|
||||
.joining(optional: A.b)
|
||||
.joining(optional: A.d)
|
||||
.order(sql: "a.id, b.id, d.id")
|
||||
let records = try dbQueue.inDatabase { try request.fetchAll($0) }
|
||||
|
||||
XCTAssertEqual(records.count, 6)
|
||||
|
||||
XCTAssertEqual(records[0].id, 1)
|
||||
XCTAssertEqual(records[0].bid, 1)
|
||||
XCTAssertEqual(records[0].did, 1)
|
||||
XCTAssertEqual(records[0].name, "a1")
|
||||
|
||||
XCTAssertEqual(records[1].id, 2)
|
||||
XCTAssertEqual(records[1].bid, 1)
|
||||
XCTAssertEqual(records[1].did, nil)
|
||||
XCTAssertEqual(records[1].name, "a2")
|
||||
|
||||
XCTAssertEqual(records[2].id, 3)
|
||||
XCTAssertEqual(records[2].bid, 2)
|
||||
XCTAssertEqual(records[2].did, 1)
|
||||
XCTAssertEqual(records[2].name, "a3")
|
||||
|
||||
XCTAssertEqual(records[3].id, 4)
|
||||
XCTAssertEqual(records[3].bid, 2)
|
||||
XCTAssertEqual(records[3].did, nil)
|
||||
XCTAssertEqual(records[3].name, "a4")
|
||||
|
||||
XCTAssertEqual(records[4].id, 5)
|
||||
XCTAssertEqual(records[4].bid, nil)
|
||||
XCTAssertEqual(records[4].did, 1)
|
||||
XCTAssertEqual(records[4].name, "a5")
|
||||
|
||||
XCTAssertEqual(records[5].id, 6)
|
||||
XCTAssertEqual(records[5].bid, nil)
|
||||
XCTAssertEqual(records[5].did, nil)
|
||||
XCTAssertEqual(records[5].name, "a6")
|
||||
}
|
||||
}
|
||||
|
||||
+2860
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,712 @@
|
||||
import XCTest
|
||||
import GRDB
|
||||
|
||||
// A -> B <- C
|
||||
// A -> D
|
||||
private struct A: TableRecord {
|
||||
static let databaseTableName = "a"
|
||||
static let b = belongsTo(B.self)
|
||||
static let d = belongsTo(D.self)
|
||||
}
|
||||
|
||||
private struct B: TableRecord {
|
||||
static let a = hasOne(A.self)
|
||||
static let c = hasOne(C.self)
|
||||
static let databaseTableName = "b"
|
||||
}
|
||||
|
||||
private struct C: TableRecord {
|
||||
static let databaseTableName = "c"
|
||||
static let b = belongsTo(B.self)
|
||||
}
|
||||
|
||||
private struct D: TableRecord {
|
||||
static let a = hasOne(A.self)
|
||||
static let databaseTableName = "d"
|
||||
}
|
||||
|
||||
/// Test SQL generation
|
||||
class AssociationParallelSQLTests: GRDBTestCase {
|
||||
|
||||
override func setup(_ dbWriter: some DatabaseWriter) throws {
|
||||
try dbWriter.write { db in
|
||||
try db.create(table: "b") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
}
|
||||
try db.create(table: "d") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
}
|
||||
try db.create(table: "a") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("bid", .integer).references("b")
|
||||
t.column("did", .integer).references("d")
|
||||
}
|
||||
try db.create(table: "c") { t in
|
||||
t.primaryKey("id", .integer)
|
||||
t.column("bid", .integer).references("b")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b).including(required: A.d), """
|
||||
SELECT "a".*, "b".*, "d".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.b).including(optional: A.d), """
|
||||
SELECT "a".*, "b".*, "d".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).including(required: A.d), """
|
||||
SELECT "a".*, "b".*, "d".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).including(optional: A.d), """
|
||||
SELECT "a".*, "b".*, "d".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).including(required: B.c), """
|
||||
SELECT "b".*, "a".*, "c".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).including(optional: B.c), """
|
||||
SELECT "b".*, "a".*, "c".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).including(required: B.c), """
|
||||
SELECT "b".*, "a".*, "c".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).including(optional: B.c), """
|
||||
SELECT "b".*, "a".*, "c".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingIncludingOtherKey() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b).including(required: A.b.forKey("customB")), """
|
||||
SELECT "a".*, "b1".*, "b2".* \
|
||||
FROM "a" \
|
||||
JOIN "b" "b1" ON "b1"."id" = "a"."bid" \
|
||||
JOIN "b" "b2" ON "b2"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.b).including(optional: A.b.forKey("customB")), """
|
||||
SELECT "a".*, "b1".*, "b2".* \
|
||||
FROM "a" \
|
||||
JOIN "b" "b1" ON "b1"."id" = "a"."bid" \
|
||||
LEFT JOIN "b" "b2" ON "b2"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).including(required: A.b.forKey("customB")), """
|
||||
SELECT "a".*, "b1".*, "b2".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" "b1" ON "b1"."id" = "a"."bid" \
|
||||
JOIN "b" "b2" ON "b2"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).including(optional: A.b.forKey("customB")), """
|
||||
SELECT "a".*, "b1".*, "b2".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" "b1" ON "b1"."id" = "a"."bid" \
|
||||
LEFT JOIN "b" "b2" ON "b2"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).including(required: B.a.forKey("customA")), """
|
||||
SELECT "b".*, "a1".*, "a2".* \
|
||||
FROM "b" \
|
||||
JOIN "a" "a1" ON "a1"."bid" = "b"."id" \
|
||||
JOIN "a" "a2" ON "a2"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).including(optional: B.a.forKey("customA")), """
|
||||
SELECT "b".*, "a1".*, "a2".* \
|
||||
FROM "b" \
|
||||
JOIN "a" "a1" ON "a1"."bid" = "b"."id" \
|
||||
LEFT JOIN "a" "a2" ON "a2"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).including(required: B.a.forKey("customA")), """
|
||||
SELECT "b".*, "a1".*, "a2".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" "a1" ON "a1"."bid" = "b"."id" \
|
||||
JOIN "a" "a2" ON "a2"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).including(optional: B.a.forKey("customA")), """
|
||||
SELECT "b".*, "a1".*, "a2".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" "a1" ON "a1"."bid" = "b"."id" \
|
||||
LEFT JOIN "a" "a2" ON "a2"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingIncludingSameAssociation() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b).including(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.b).including(optional: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).including(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).including(optional: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).including(required: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).including(optional: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).including(required: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).including(optional: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b).joining(required: A.d), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.b).joining(optional: A.d), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).joining(required: A.d), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).joining(optional: A.d), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).joining(required: B.c), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).joining(optional: B.c), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).joining(required: B.c), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).joining(optional: B.c), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoIncludingJoiningSameAssociation() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.including(required: A.b).joining(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(required: A.b).joining(optional: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).joining(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.including(optional: A.b).joining(optional: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).joining(required: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(required: B.a).joining(optional: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).joining(required: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.including(optional: B.a).joining(optional: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoJoiningIncluding() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.joining(required: A.b).including(required: A.d), """
|
||||
SELECT "a".*, "d".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(required: A.b).including(optional: A.d), """
|
||||
SELECT "a".*, "d".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).including(required: A.d), """
|
||||
SELECT "a".*, "d".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).including(optional: A.d), """
|
||||
SELECT "a".*, "d".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).including(required: B.c), """
|
||||
SELECT "b".*, "c".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).including(optional: B.c), """
|
||||
SELECT "b".*, "c".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).including(required: B.c), """
|
||||
SELECT "b".*, "c".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).including(optional: B.c), """
|
||||
SELECT "b".*, "c".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoJoiningIncludingSameAssociation() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.joining(required: A.b).including(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(required: A.b).including(optional: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).including(required: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).including(optional: A.b), """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).including(required: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).including(optional: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).including(required: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).including(optional: B.a), """
|
||||
SELECT "b".*, "a".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoJoiningJoining() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.joining(required: A.b).joining(required: A.d), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(required: A.b).joining(optional: A.d), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).joining(required: A.d), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).joining(optional: A.d), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
LEFT JOIN "d" ON "d"."id" = "a"."did"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).joining(required: B.c), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).joining(optional: B.c), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).joining(required: B.c), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).joining(optional: B.c), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id" \
|
||||
LEFT JOIN "c" ON "c"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testParallelTwoJoiningJoiningSameAssociation() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
try assertEqualSQL(db, A.joining(required: A.b).joining(required: A.b), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(required: A.b).joining(optional: A.b), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).joining(required: A.b), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, A.joining(optional: A.b).joining(optional: A.b), """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
LEFT JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).joining(required: B.a), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(required: B.a).joining(optional: B.a), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).joining(required: B.a), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
try assertEqualSQL(db, B.joining(optional: B.a).joining(optional: B.a), """
|
||||
SELECT "b".* \
|
||||
FROM "b" \
|
||||
LEFT JOIN "a" ON "a"."bid" = "b"."id"
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestRefiningFilter() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.filter(Column("id") > 1))
|
||||
.joining(required: A.b)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" > 1)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b)
|
||||
.joining(required: A.b.filter(Column("id") < 3))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" < 3)
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.filter(Column("id") > 1))
|
||||
.joining(required: A.b.filter(Column("id") < 3))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON ("b"."id" = "a"."bid") AND ("b"."id" > 1) AND ("b"."id" < 3)
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestRefiningOrder() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.order(Column("id")))
|
||||
.joining(required: A.b)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
ORDER BY "b"."id"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b)
|
||||
.joining(required: A.b.order(Column("id").desc))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
ORDER BY "b"."id" DESC
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.order(Column("id")))
|
||||
.joining(required: A.b.order(Column("id").desc))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid" \
|
||||
ORDER BY "b"."id" DESC
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testRequestRefiningSelection() throws {
|
||||
let dbQueue = try makeDatabaseQueue()
|
||||
try dbQueue.inDatabase { db in
|
||||
// joining + joining
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.joining(required: A.b)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b)
|
||||
.joining(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.joining(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
|
||||
// joining + including
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.including(required: A.b)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b)
|
||||
.including(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."id", 2 \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.joining(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.including(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."id", 2 \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
|
||||
// including + joining
|
||||
do {
|
||||
let request = A
|
||||
.including(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.joining(required: A.b)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."id", 1 \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.including(required: A.b)
|
||||
.joining(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.including(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.joining(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."id", 1 \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
|
||||
// including + including
|
||||
do {
|
||||
let request = A
|
||||
.including(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.including(required: A.b)
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b".* \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.including(required: A.b)
|
||||
.including(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."id", 2 \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
do {
|
||||
let request = A
|
||||
.including(required: A.b.select(Column("id"), 1.databaseValue))
|
||||
.including(required: A.b.select(Column("id"), 2.databaseValue))
|
||||
try assertEqualSQL(db, request, """
|
||||
SELECT "a".*, "b"."id", 2 \
|
||||
FROM "a" \
|
||||
JOIN "b" ON "b"."id" = "a"."bid"
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user