Thanks for the comprehensive answer, Mike, this is helpful.

Just to confirm, the per-instance version of your solution eagerly queries 
the value when assigning to foo.pk, not when foo is added/committed to the 
DB, right? And so this creates a race condition where multiple instances 
could be assigned the same computed PK?

But am I correct that with the per-model, ColumnDefault version, the query 
is performed at insert-time, meaning that race condition doesn't exist?

Thanks for the clarification!
-Gabriel


On Friday, 18 July 2014 12:22:47 UTC-4, Michael Bayer wrote:
>
> Hopefully I’ll be able to add this in 1.0 using the patch illustrated at 
> https://bitbucket.org/zzzeek/sqlalchemy/issue/3133/support-dbs-that-magically-get-sql
> .
>
>
> On Jul 18, 2014, at 11:54 AM, Michael Bayer <mik...@zzzcomputing.com 
> <javascript:>> wrote:
>
>
> On Jul 18, 2014, at 4:33 AM, Johannes Klug <klug.j...@gmail.com 
> <javascript:>> wrote:
>
>
> results in the stacktrace:
>
> InvalidRequestError: Instance <Foo at 0x224f710> cannot be refreshed - it's 
> not  persistent and does not contain a full primary key.
>
>
> so I hate to be a stickler here but that’s not a stack trace, that’s an 
> error message.   
>
> if we reproduce your full script we can see what is going on.
>
> First, always use echo=True to see what’s happening.  We can see that the 
> statement does in fact work as planned:
>
> INSERT INTO foo (pk, bar) VALUES ((SELECT coalesce(max(foo.pk) + ?, ?) AS 
> coalesce_1 
>
> however, the error then happens later on, with this partial trace:
>
>   File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/unitofwork.py", 
> line 389, in finalize_flush_changes
>     self.session._register_newly_persistent(other)
>   File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 
> 1408, in _register_newly_persistent
>     instance_key = mapper._identity_key_from_state(state)
>   File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/mapper.py", line 
> 2285, in _identity_key_from_state
>     for col in self.primary_key
>   File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/attributes.py", 
> line 580, in get
>     value = callable_(state, passive)
>   File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/state.py", line 
> 423, in __call__
>     self.manager.deferred_scalar_loader(self, toload)
>   File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/loading.py", line 
> 597, in load_scalar_attributes
>     "contain a full primary key." % state_str(state))
> sqlalchemy.exc.InvalidRequestError: Instance <Foo at 0x10165d510> cannot 
> be refreshed - it's not  persistent and does not contain a full primary key.
>
> we can see the SQLAlchemy ORM, like all ORMs, needs to know the primary 
> key of this newly inserted object - “identity key from state”.  SQLAlchemy 
> is failing pretty ungracefully here.  This could be improved.
>
> To understand the full picture here is pretty complicated because there 
> are a lot of nooks and crannies to the reality here so I will try to bullet 
> them out:
>
> 1. Typically, it’s not possible to avoid a failure here.  SQLite does not 
> support RETURNING, so the only way we can get at the value when it is done 
> inline like this is through cursor.lastrowid.  Cursor.lastrowid typically 
> only gives us the value that was generated using the database’s sequencing 
> mechanism.  On MySQL, we would have to use an AUTOINCREMENT column; we get 
> a zero back from lastrowid instead.
>
> 2. Apparently, based on a short test, recent pysqlite is giving us the 
> actual value here in cursor.lastrowid.    I’ve never seen a DBAPI do this 
> before and SQLAlchemy would need additional enhancements to distinguish 
> between databases where lastrowid only works for auto increment values, and 
> lastrowid that actually gives you the value no matter where it came from. 
>  That’s not available right now.
>
> 3. If we run this script as is on a database that supports RETURNING, the 
> whole thing works just fine.  Here’s Postgresql:
>
> INSERT INTO foo (pk, bar) VALUES ((SELECT coalesce(max(foo.pk) + 
> %(max_1)s, %(param_1)s) AS coalesce_1 
> FROM foo), %(bar)s) RETURNING foo.pk
>
>
> 4. So for now, for ORM use (which requires the PK, as opposed to Core 
> where this isn’t needed since there is no object to track), to use this 
> pattern on a lastrowid backend the statement needs to be invoked outside of 
> the INSERT.   We can achieve this using ColumnDefault:
>
> class Foo(Base):
>     __tablename__ = 'foo'
>     pk = Column(Integer, primary_key=True)
>     bar = Column(Integer)
>
> Foo.__table__.c.pk.default = 
> ColumnDefault(sql.select([sql.func.coalesce(sql.func.max(Foo.pk) + 1, 
> 1)]).as_scalar())
>
> Where we’ll see:
>
> SELECT (SELECT coalesce(max(foo.pk) + ?, ?) AS coalesce_1 
> FROM foo) AS anon_1
> (1, 1)
> INSERT INTO foo (pk, bar) VALUES (?, ?)
> (1, None)
>
>
> This will work on any backend.
>
> Or if the “per instance” system is needed, just run the statement:
>
> foo = Foo()
> foo.pk = 
> session.scalar(sql.select([sql.func.coalesce(sql.func.max(Foo.pk) + 1, 1)]))
> session.add(foo)
> session.commit()
>
>
>
> -- 
> You received this message because you are subscribed to the Google Groups 
> "sqlalchemy" group.
> To unsubscribe from this group and stop receiving emails from it, send an 
> email to sqlalchemy+...@googlegroups.com <javascript:>.
> To post to this group, send email to sqlal...@googlegroups.com 
> <javascript:>.
> Visit this group at http://groups.google.com/group/sqlalchemy.
> For more options, visit https://groups.google.com/d/optout.
>
>
>

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to