So the use case you are doing there was never something supported, and in particular the value you are getting on 1.3 is not really universally reliable, as it is relying upon cursor.lastrowid that is not defined for multiple-row inserts, even though for InnoDB specifically the behavior is likely deterministic.
now can you assume that if you inserted five rows and last_insert_id() gave you the number 1, the rows will be numbered 1, 2, 3, 4, 5? won't concurrent inserts possibly interfere with this? the consensus seems to be "the values probably are predictable, for a single server MySQL database, but it's not documented anywhere", see
https://stackoverflow.com/questions/6895679/mysqls-auto-increment-behavior-in-a-multiple-row-insert . that MySQL won't document this I would say is pretty telling that this is not a long term / generalized guarantee (like what happens if this is on Galera for example?) even though it might be the current behavior.
For SQLAlchemy, which gets cursor.lastrowid from other database engines also, it was definitely never assumed that "cursor.lastrowid" is useful for an INSERT statement that has multiple rows, and this has always been an undefined use case. The spec itself documents this:
https://www.python.org/dev/peps/pep-0249/#lastrowid "The semantics of
.lastrowid are undefined in case the last
executed statement modified more than one row, e.g. when using
INSERT with
.executemany()."
This is also not what we refer to as a "returned primary keys", which usually refers to the RETURNING SQL syntax. Only MariaDB as of 10.5 supports that syntax, which we have not yet added support for - other than that MySQL DBs only give us cursor.lastrowid.
1.4's architecture is more explicit about how things are handled so when you send an insert().values() with a list, that's automatically a multi-row insert and we don't collect cursor.lastrowid in this case. this caused no regressions because cursor.lastrowid was never considered to be defined in this case and no tests have covered it.
if you send just one row at a time, you get the inserted_primary_key back:
with engine.begin() as conn:
result = conn.execute(messages.insert().values({'message': 'Hello World'}))
first_id = result.inserted_primary_key[0]
to bypass the result and go straight to the pymysql cursor if you'd like to rely upon the driver-specfic behavior, you can get to it as result.context.cursor.lastrowid: