Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/swift-arm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
arch: ["armv6", "armv7"]
swift: ["6.1.2"]
swift: ["6.3.2"]
config: ["debug" , "release"]
linux: ["raspios"]
release: ["bookworm"]
Expand All @@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
arch: ["armv7"]
swift: ["6.1.2"]
swift: ["6.3.2"]
config: ["debug" , "release"]
linux: ["debian"]
release: ["bookworm", "bullseye"]
Expand Down
37 changes: 24 additions & 13 deletions .github/workflows/swift-wasm.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
name: Swift WASM
name: Swift WebAssembly
on: [push]
jobs:
build-wasm:
name: Build for WASM
wasm:
name: WebAssembly
runs-on: ubuntu-latest
container: swift:6.3.2
strategy:
fail-fast: false
matrix:
swift: ["6.0.3"]
config: ["debug", "release"]
container: swift:${{ matrix.swift }}
config: [debug, release]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- uses: swiftwasm/setup-swiftwasm@v2
- name: Build
run: swift build -c ${{ matrix.config }} --swift-sdk wasm32-unknown-wasi
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- name: Install dependencies
run: apt-get update -y && apt-get install -y curl
- name: Install WASM SDK
run: |
set -eux
url="https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz"
curl -fsSL "$url" -o wasm.artifactbundle.tar.gz
swift sdk install wasm.artifactbundle.tar.gz
swift sdk list
- name: Build
run: >-
swift build
-c ${{ matrix.config }}
--swift-sdk swift-6.3.2-RELEASE_wasm
24 changes: 12 additions & 12 deletions .github/workflows/swift-windows.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
name: Swift Windows
on: [push]
jobs:
windows-build:

windows:
name: Windows
runs-on: windows-latest
strategy:
matrix:
swift: ["6.1.2"]
config: ["debug", "release"]
steps:
- uses: compnerd/gha-setup-swift@main
with:
branch: swift-${{ matrix.swift }}-release
tag: ${{ matrix.swift }}-RELEASE
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- name: Build
run: swift build -c ${{ matrix.config }}
- uses: compnerd/gha-setup-swift@main
with:
branch: swift-6.3.2-release
tag: 6.3.2-RELEASE
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- name: Build
run: swift build -c ${{ matrix.config }}
19 changes: 10 additions & 9 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jobs:

macos:
name: macOS
runs-on: macos-15
runs-on: macos-26
strategy:
matrix:
config: ["debug", "release"]
Expand All @@ -18,16 +18,17 @@ jobs:
run: ${{ matrix.options }} swift build -c ${{ matrix.config }}
- name: Test
run: ${{ matrix.options }} swift test -c ${{ matrix.config }}

linux:
name: Linux
name: Linux (${{ matrix.container }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
container: ["swift:6.0.3", "swift:6.1.2"]
container: ["swift:latest", "swiftlang/swift:nightly-noble"]
config: ["debug", "release"]
options: ["", "SWIFT_BUILD_DYNAMIC_LIBRARY=1"]
runs-on: ubuntu-latest
container: ${{ matrix.container }}-jammy
container: ${{ matrix.container }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand All @@ -43,14 +44,14 @@ jobs:
strategy:
fail-fast: false
matrix:
swift: ['6.1', 'nightly-6.2']
swift: ['6.3.2']
arch: ["aarch64", "x86_64"]
runs-on: macos-15
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: "Build Swift Package for Android"
run: |
brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip)
skip android sdk install --version ${{ matrix.swift }}
ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }}
ANDROID_NDK_ROOT="" skip android build --arch ${{ matrix.arch }}
35 changes: 27 additions & 8 deletions Sources/CoreDataModel/NSManagedObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,27 @@ internal extension NSManagedObject {
for key: PropertyKey,
in context: NSManagedObjectContext
) throws {

var cache = NSManagedObjectContext.ManagedObjectCache()
try setRelationship(newValue, for: key, in: context, cache: &cache)
}

func setRelationship(
_ newValue: RelationshipValue,
for key: PropertyKey,
in context: NSManagedObjectContext,
cache: inout NSManagedObjectContext.ManagedObjectCache
) throws {

guard let relationship = self.entity.relationshipsByName[key.rawValue],
let destinationEntity = relationship.destinationEntity?.name.map({ EntityName(rawValue: $0) }) else {
assertionFailure("Invalid relationship for \"\(key)\"")
throw CocoaError(.coreData)
}

let model = self.entity.managedObjectModel

let objectValue: AnyObject?

switch newValue {
case .null:
objectValue = nil
Expand All @@ -207,7 +217,7 @@ internal extension NSManagedObject {
throw CocoaError(.coreData)
}
// find managed object
let managedObject = try context.find(destinationEntity, for: value)
let managedObject = try context.find(destinationEntity, for: value, cache: &cache)
objectValue = managedObject
case let .toMany(value):
guard relationship.isToMany else {
Expand All @@ -216,14 +226,14 @@ internal extension NSManagedObject {
}
// find or create
let managedObjects = try value
.map { try context.find(destinationEntity, for: $0) ?? context.create(destinationEntity, for: $0, in: model) }
.map { try context.find(destinationEntity, for: $0, cache: &cache) ?? context.create(destinationEntity, for: $0, in: model, cache: &cache) }
if relationship.isOrdered {
objectValue = NSOrderedSet(array: managedObjects)
} else {
objectValue = NSSet(array: managedObjects)
}
}

self.setValue(objectValue, forKey: key.rawValue)
}
}
Expand Down Expand Up @@ -274,13 +284,22 @@ internal extension NSManagedObject {
internal extension NSManagedObject {

func setValues(for value: ModelData, in context: NSManagedObjectContext) throws {
var cache = NSManagedObjectContext.ManagedObjectCache()
try setValues(for: value, in: context, cache: &cache)
}

func setValues(
for value: ModelData,
in context: NSManagedObjectContext,
cache: inout NSManagedObjectContext.ManagedObjectCache
) throws {
// apply attributes
for (key, value) in value.attributes {
setAttribute(value, for: key)
}
// apply relationships
for (key, value) in value.relationships {
try setRelationship(value, for: key, in: context)
try setRelationship(value, for: key, in: context, cache: &cache)
}
}
}
Expand Down
107 changes: 106 additions & 1 deletion Sources/CoreDataModel/NSManagedObjectContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,116 @@ internal extension NSManagedObjectContext {
_ values: [ModelData],
model: NSManagedObjectModel
) throws {
guard values.isEmpty == false else { return }
// Prefetch every object referenced by the batch (inserted values and their
// relationship targets) with one fetch request per entity. Per-object
// find-or-create degrades quadratically as pending inserts accumulate,
// because each fetch request evaluates its predicate against all unsaved
// objects in the context.
var cache = try prefetch(for: values, model: model)
// find or create the inserted objects first so relationships between
// values in the same batch resolve regardless of their order
var managedObjects = [NSManagedObject]()
managedObjects.reserveCapacity(values.count)
for value in values {
try insert(value, model: model, shouldSave: false)
let managedObject: NSManagedObject
if let cached = cache[value.entity]?[value.id] {
managedObject = cached
} else {
// the prefetch covered every value identifier, so a cache miss
// means the object does not exist yet
managedObject = try create(value.entity, for: value.id, in: model, cache: &cache)
}
managedObjects.append(managedObject)
}
// apply attributes and relationships
for (index, value) in values.enumerated() {
try managedObjects[index].setValues(for: value, in: self, cache: &cache)
}
try self.save()
}
}

internal extension NSManagedObjectContext {

/// Realized managed objects by entity and identifier, used to avoid per-object
/// fetch requests during batch inserts.
typealias ManagedObjectCache = [EntityName: [ObjectID: NSManagedObject]]

/// Fetch the existing managed objects referenced by the given values
/// (both inserted objects and their relationship targets) with a single
/// fetch request per entity.
func prefetch(
for values: [ModelData],
model: NSManagedObjectModel
) throws -> ManagedObjectCache {
// collect identifiers by entity
var idsByEntity = [EntityName: Set<ObjectID>]()
for value in values {
idsByEntity[value.entity, default: []].insert(value.id)
guard value.relationships.isEmpty == false else { continue }
let relationshipsByName = try model[value.entity].relationshipsByName
for (key, relationship) in value.relationships {
guard let destinationEntity = relationshipsByName[key.rawValue]?.destinationEntity?.name.map({ EntityName(rawValue: $0) }) else {
continue
}
switch relationship {
case .null:
continue
case let .toOne(id):
idsByEntity[destinationEntity, default: []].insert(id)
case let .toMany(ids):
idsByEntity[destinationEntity, default: []].formUnion(ids)
}
}
}
// fetch existing objects with a single request per entity
var cache = ManagedObjectCache(minimumCapacity: idsByEntity.count)
for (entityName, ids) in idsByEntity {
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName.rawValue)
fetchRequest.predicate = NSPredicate(
format: "%K IN %@",
NSManagedObject.BuiltInProperty.id.rawValue,
ids.map { $0.rawValue }
)
fetchRequest.returnsObjectsAsFaults = false
var entityCache = [ObjectID: NSManagedObject](minimumCapacity: ids.count)
for managedObject in try self.fetch(fetchRequest) {
entityCache[try managedObject.modelObjectID] = managedObject
}
cache[entityName] = entityCache
}
return cache
}

/// Find the managed object through the cache, falling back to a fetch request
/// and caching the result.
func find(
_ entityName: EntityName,
for id: ObjectID,
cache: inout ManagedObjectCache
) throws -> NSManagedObject? {
if let managedObject = cache[entityName]?[id] {
return managedObject
}
guard let managedObject = try find(entityName, for: id, includesPropertyValues: false) else {
return nil
}
cache[entityName, default: [:]][id] = managedObject
return managedObject
}

/// Create the managed object and add it to the cache.
func create(
_ entityName: EntityName,
for id: ObjectID,
in model: NSManagedObjectModel,
cache: inout ManagedObjectCache
) throws -> NSManagedObject {
let managedObject = try create(entityName, for: id, in: model)
cache[entityName, default: [:]][id] = managedObject
return managedObject
}
}

#endif
Loading
Loading