Passing String and Data sequences to sqlite_carray_bind

I'm trying to work with sqlite_carray_bind from the sqlite library.

Here is what I have working for binding a sequence of strings:

func bind<S: Sequence<String>>(strings: S, to statement: OpaquePointer?, at index: Int32) throws {
	var errmsg: UnsafePointer<CChar>? { sqlite3_errmsg(sqlite3_db_handle(statement)) }

	var array = strings.map { strdup($0) }
	array.append(nil)
	defer { array.forEach { free($0) } }

	if array.isEmpty {
		switch sqlite3_bind_null(statement, index) {
			case SQLITE_OK: return
			case let status: throw DataStore.Error(status, message: errmsg)
		}
	}
	let count = Int32(array.count)
	try array.withUnsafeBufferPointer { pointer in
		let mutableAddress = UnsafeMutableRawPointer(mutating: pointer.baseAddress)
		switch sqlite3_carray_bind(statement, index, mutableAddress, count, SQLITE_CARRAY_TEXT, SQLITE_TRANSIENT) {
			case SQLITE_OK: return
			case let status: throw DataStore.Error(status, message: errmsg)
		}
	}
}

My hangup here is specifically re: converting the sequence of strings I have to an array of pointers that the API needs. I'm adapting this gist I found through oleb.net. Is it the easiest/most efficient way to convert a sequence of strings to an array of pointers to their bytes? Can this be achieved without making a whole new copy of all the strings with strdup and needing to manage those allocations manually?

Secondly, here is what I have for binding a sequence of Data:

func bind<S: Sequence<Data>>(data: S, to statement: OpaquePointer?, at index: Int32) throws {
	var errmsg: UnsafePointer<CChar>? { sqlite3_errmsg(sqlite3_db_handle(statement)) }
	var array: [iovec?] = data.map {
		$0.withUnsafeBytes { pointer -> iovec? in
			guard let address = pointer.baseAddress else { return nil }
			return iovec(iov_base: .init(mutating: address), iov_len: pointer.count)
		}
	}
	if array.isEmpty {
		switch sqlite3_bind_null(statement, index) {
			case SQLITE_OK: return
			case let status: throw DataStore.Error(status, message: errmsg)
		}
	}
	array.append(nil)
	let count = Int32(array.count)
	try array.withUnsafeBufferPointer { pointer in
		let mutableAddress = UnsafeMutableRawPointer(mutating: pointer.baseAddress)
		switch sqlite3_carray_bind(statement, index, mutableAddress, count, SQLITE_CARRAY_BLOB, SQLITE_TRANSIENT) {
			case SQLITE_OK: return
			case let status: throw DataStore.Error(status, message: errmsg)
		}
	}
}

This seems to work only when there are zero or one Data elements in the sequence. What am I missing here?

1 Like

Have you checked if Optional<iovec> is layout compatible with iovec? I would try making array contain regular iovecs and either force-unwrapping baseAddress or using .compactMap(_:).

The issue with binding Data is fixed. Removing the pointer base address guard and mapping to a non-optional iovec array worked. Evidently sqlite handles pointers to data with zero length without a problem.

Still looking for an efficient and/or Swifty way to bind strings though. I hope copy allocation with manual memory management isn't my only option.

My issue is somewhat solved. I'm needing to depend on recursion within unsafe pointer methods so that the actual pointers aren't invalidated. Still not 100% satisfied with my solution but it's the best I can think of for now.

For anyone else facing similar issues in the future, this is what I'm going with to bind any sequence of strings:

func bind<S: Sequence<String>>(strings: S, to statement: OpaquePointer?, at index: Int32) throws {
	var errmsg: UnsafePointer<CChar>? { sqlite3_errmsg(sqlite3_db_handle(statement)) }
	var pointers: [UnsafePointer<CChar>] = []
	pointers.reserveCapacity(strings.underestimatedCount)
	var iterator = strings.makeIterator()

	func bind(string: String?) throws {
		switch string {
			case .some(let string): try string.withCString { pointer in
				pointers.append(pointer)
				try bind(string: iterator.next())
			}
			case .none where pointers.isEmpty: switch sqlite3_bind_null(statement, index) {
				case SQLITE_OK: return
				case let status: throw DataStore.Error(status, message: errmsg())
			}
			case .none: try pointers.withUnsafeBufferPointer { pointer in
				let address = UnsafeMutableRawPointer(mutating: pointer.baseAddress)
				let count = Int32(pointer.count)
				switch sqlite3_carray_bind(statement, index, address, count, SQLITE_CARRAY_TEXT, SQLITE_TRANSIENT) {
					case SQLITE_OK: return
					case let status: throw DataStore.Error(status, message: errmsg())
				}
			}
		}
	}
	try bind(string: iterator.next())
}

And this is for any sequence of Data:

func bind<S: Sequence<Data>>(data: S, to statement: OpaquePointer?, at index: Int32) throws {
	var errmsg: UnsafePointer<CChar>? { sqlite3_errmsg(sqlite3_db_handle(statement)) }
	var pointers: [iovec] = []
	pointers.reserveCapacity(data.underestimatedCount)
	var iterator = data.makeIterator()

	func bind(data: Data?) throws {
		switch data {
			case .some(let data): try data.withUnsafeBytes { pointer in
				pointers.append(iovec(iov_base: .init(mutating: pointer.baseAddress), iov_len: pointer.count))
				try bind(data: iterator.next())
			}
			case .none where pointers.isEmpty: switch sqlite3_bind_null(statement, index) {
				case SQLITE_OK: return
				case let status: throw DataStore.Error(status, message: errmsg)
			}
			case .none: try pointers.withUnsafeBufferPointer { pointer in
				let address = UnsafeMutableRawPointer(mutating: pointer.baseAddress)
				let count = Int32(pointer.count)
				switch sqlite3_carray_bind(statement, index, address, count, SQLITE_CARRAY_BLOB, SQLITE_TRANSIENT) {
					case SQLITE_OK: return
					case let status: throw DataStore.Error(status, message: errmsg)
				}
			}
		}
	}

	try bind(data: iterator.next())
}

Hoping someone out there has a better idea than this.

1 Like