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
Original file line number Diff line number Diff line change
@@ -1,6 +1,32 @@
import Foundation
import DashSDKFFI

/// Result of ``SDK/documentCount(dataContractId:documentType:whereJSON:orderByJSON:groupByJSON:limit:)``.
///
/// Mirrors the FFI's `{"counts": {"<hexKey>": <u64>}}` payload. Keys are the
/// hex-encoded group keys; for an ungrouped (aggregate) count there is a
/// single entry under the empty-string key, exposed via ``total``.
public struct DocumentCountResult: Sendable {
/// Hex-encoded group key β†’ count. The empty-string key holds the
/// aggregate total for an ungrouped count.
public let counts: [String: UInt64]

public init(counts: [String: UInt64]) {
self.counts = counts
}

/// The aggregate total for an ungrouped count: `counts[""]`. `nil` when
/// the request was grouped (no empty-string entry).
public var total: UInt64? {
counts[""]
}

/// `true` when the result carries per-group counts (any non-empty key).
public var isGrouped: Bool {
counts.keys.contains { !$0.isEmpty }
}
}

// MARK: - Platform Query Extensions for SDK
@MainActor
extension SDK {
Expand Down Expand Up @@ -555,6 +581,113 @@ extension SDK {
return json
}

/// Count documents of a given type, optionally filtered by `where` and
/// grouped by `group_by`.
///
/// Thin bridge over `dash_sdk_document_count`: it fetches the contract
/// handle (same precedent as `documentList`/`documentGet`), marshals the
/// document type + optional `where`/`order_by`/`group_by` JSON + the
/// `limit` sentinel in, calls the FFI, and marshals the
/// `{"counts": {"<hexKey>": <u64>}}` payload out. All aggregation is done
/// on the Rust side; nothing is decided here.
///
/// - Parameters:
/// - dataContractId: Base58 id of the data contract holding the type.
/// - documentType: The document type name to count.
/// - whereJSON: Optional `[{field, operator, value}]` filter JSON.
/// Pass `nil` for an unfiltered count.
/// - orderByJSON: Optional `[{field, direction}]` JSON. Pass `nil` for
/// none (server defaults to ascending).
/// - groupByJSON: Optional `["<field>", ...]` JSON. Pass `nil` for an
/// aggregate (ungrouped) count.
/// - limit: Sentinel-encoded `int64`. `-1` = use server default
/// (the default). `> 0` = explicit cap. `0` is rejected at the FFI
/// boundary, so the wrapper rejects it before calling.
/// - Returns: A ``DocumentCountResult`` mapping hex-encoded group key β†’
/// count. For an ungrouped count the total lives under the empty-string
/// key and is surfaced via ``DocumentCountResult/total``.
@MainActor
public func documentCount(
dataContractId: String,
documentType: String,
whereJSON: String? = nil,
orderByJSON: String? = nil,
groupByJSON: String? = nil,
limit: Int64 = -1
) async throws -> DocumentCountResult {
guard let handle = handle else {
throw SDKError.invalidState("SDK not initialized")
}
guard limit != 0 else {
// The FFI rejects limit == 0 (the v1 wire rejects Some(0)); fail
// fast with a clear message rather than relaying it.
throw SDKError.invalidParameter("limit must be -1 (server default) or a positive value, not 0")
}

// Fetch the contract handle (precedent: documentList / documentGet).
let contractResult = dash_sdk_data_contract_fetch(handle, dataContractId)
if let error = contractResult.error {
let sdkError = SDKError.fromDashSDKError(error.pointee)
dash_sdk_error_free(error)
throw sdkError
}
guard let contractHandle = contractResult.data else {
throw SDKError.notFound("Data contract not found")
}
defer {
dash_sdk_data_contract_destroy(contractHandle.assumingMemoryBound(to: DataContractHandle.self))
}

// Marshal the optional JSON strings in. nil β†’ null pointer = "none".
let result = documentType.withCString { typePtr in
withOptionalCString(whereJSON) { wherePtr in
withOptionalCString(orderByJSON) { orderPtr in
withOptionalCString(groupByJSON) { groupPtr in
dash_sdk_document_count(
handle,
contractHandle.assumingMemoryBound(to: DataContractHandle.self),
typePtr,
wherePtr,
orderPtr,
groupPtr,
limit
)
}
}
}
}

let json = try processJSONResult(result)

// Payload shape: {"counts": {"<hexKey>": <u64>}}.
guard let countsObject = json["counts"] as? [String: Any] else {
throw SDKError.serializationError("Expected 'counts' object in document count response")
}

var counts: [String: UInt64] = [:]
for (key, value) in countsObject {
if let num = value as? NSNumber {
counts[key] = num.uint64Value
}
}
Comment on lines +667 to +672

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’¬ Nitpick: u64 counts above Int64.max lose precision through JSONSerialization β†’ uint64Value

The Rust side serializes counts as raw JSON integers from BTreeMap<String, u64>. Foundation's JSONSerialization only guarantees fidelity for integers in the Int64 range β€” values in (Int64.max, UInt64.max] are typically returned as a Double-backed NSNumber, and num.uint64Value then silently truncates/rounds. Document counts will not realistically reach 2^63, so this is theoretical rather than a real bug. If you want full round-trip fidelity, decode with JSONDecoder into a Codable struct keyed [String: UInt64], which preserves UInt64 precision.

source: ['claude', 'codex']


return DocumentCountResult(counts: counts)
}

/// Call `body` with a `const char *` for an optional Swift string,
/// passing a null pointer when the string is `nil`. Mirrors the FFI's
/// "null/empty means none" contract for the where/order/group JSON args.
private func withOptionalCString<R>(
_ string: String?,
_ body: (UnsafePointer<CChar>?) -> R
) -> R {
if let string = string {
return string.withCString { body($0) }
} else {
return body(nil)
}
}

// MARK: - DPNS Queries

/// Get DPNS usernames for identity
Expand Down
Loading
Loading