Hitting the limits of property wrappers. Is this even possible?

I would like to write a Swift struct like this to describe a SQL table:

struct Person: Table {
  @Column var name: String
  @Column var color: String?
}

My goal is to write the Table protocol and Column property wrapper. The reason for using property wrappers is so that later on I can parameterize it like @Column(options: [.autoincrement]) (and more, like foreign keys, etc.).

The ultimate goal is for this call:

Person.createStatement

to return a string CREATE TABLE Person (name STRING NOT NULL, color STRING), and this call:

Person(name: "Bob", color: nil).insertStatement

to return a string INSERT INTO Person (name, color) values ("Bob", NULL)

I think I've gotten close a few times, but no luck.

I started by writing a property wrapper to hold values whose types can describe themselves for SQL:

protocol SQLType {
    static var sqlType: String { get }
}
@propertyWrapper
struct Column<T: SQLType> {
    var sqlColumnType: String {
        return T.sqlType
    }
  private var _value: T
  init(wrappedValue: T) {
    _value = wrappedValue;
  }
  var wrappedValue: T {
    return _value
  }
}

This works fine, except for optionals.

struct MyTable {
    @Column var name: String?
}

Gives the error: Property type 'String?' does not match that of the 'wrappedValue' property of its wrapper type 'Column'

I tried this:

extension Optional: SQLType where Wrapped: SQLType {
    static var sqlType: String {
        return Wrapped.sqlType
    }
}

But it has no effect.

I tried extending Optional without the type constraint but there doesn't seem to be any way to "cast" Wrapped to SQLType (e.g., static var sqlType: String { return (Wrapped as! SQLType).sqlType }). That's ugly anyway.

I tried creating an enum SQLOptional<T> that behaves like the real Optional but then initialization is ugly: MyTable(color: .some("red"), and worse the property wrapper forgets that it is a property wrapper:

MyTable(name: SQLOptional.some("George")) gives Cannot convert value of type 'SQLOptional<String>' to expected argument type 'Column<SQLOptional<String>>'.

Maybe property wrappers don't work when when their generic is itself a generic? If so I think I won't be able to use them and I should find another approach.

Am I overlooking a much better solution?

I think you’re on the right track, but you might be getting thrown off by an unhelpful error message. I get the same error message when I try the code above, but when I add = nil to the name declaration, the error message changes: Generic struct ‘Column’ requires that ‘String’ conform to ‘SQLType’. When I add SQLType conformance to String, everything compiles fine — do you have that conformance as well?

/facepalm
Thank you :)

Note that property wrappers currently need to store such parameters inside themselves, which means every instance will hold all that metadata, even though it doesn't change for other instances of the same field.

The one way I know of to hack around this is to move parameters into static vars on provider types that then are used as generic arguments for the wrapper (e.g. the way to provide fallback values for codable in CodableWrappers).

I vaguely recall some mention of lifting this constraint and allowing property wrappers to have per-declaration-they-are-used-on storage, but I can't find it now. I guess generic value parameters could be an approach as well.

1 Like

Thanks for pointing that out. I wished for C++-style value generics (e.g., Foo<2>) a few times while poking around here. I wish for Swift to not be as complicated as C++ a lot more than I wish for value parameters, though :)