I'm tinkering with an in-memory sqlite database, trying to wrap my head around concurrency. Here's an example using SQLAlchemy
from sqlalchemy import create_engine, text
# Make the engine
engine = create_engine("sqlite pysqlite:///:memory:", future=True, echo=True)
# Do stuff
with engine.connect() as conn1, engine.connect() as conn2:
conn1.execute(text("CREATE TABLE leagues (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL);"))
conn1.execute(text("INSERT INTO leagues (name) VALUES ('A')"))
conn1.execute(text("UPDATE leagues SET name = name || '1'")) # append 1 to the name
conn2.execute(text("UPDATE leagues SET name = name || '2'")) # append 2 to the name
conn1.execute(text("UPDATE leagues SET name = name || '1'")) # append 1 to the name
conn1.commit()
conn2.commit()
result = conn2.execute(text("SELECT name FROM leagues"))
print(result.all())
OUTPUT
2021-10-27 16:19:25,472 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2021-10-27 16:19:25,473 INFO sqlalchemy.engine.Engine CREATE TABLE leagues (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL);
2021-10-27 16:19:25,473 INFO sqlalchemy.engine.Engine [generated in 0.00058s] ()
2021-10-27 16:19:25,474 INFO sqlalchemy.engine.Engine INSERT INTO leagues (name) VALUES ('A')
2021-10-27 16:19:25,474 INFO sqlalchemy.engine.Engine [generated in 0.00026s] ()
2021-10-27 16:19:25,474 INFO sqlalchemy.engine.Engine UPDATE leagues SET name = name || '1'
2021-10-27 16:19:25,474 INFO sqlalchemy.engine.Engine [generated in 0.00008s] ()
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine UPDATE leagues SET name = name || '2'
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine [generated in 0.00013s] ()
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine UPDATE leagues SET name = name || '1'
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine [cached since 0.0005372s ago] ()
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine COMMIT
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine COMMIT
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine SELECT name FROM leagues
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine [generated in 0.00011s] ()
[('A121',)]
2021-10-27 16:19:25,475 INFO sqlalchemy.engine.Engine ROLLBACK
Here, I use two connections to write data to the same record. I would expect the output to be "A112", because I thought the first connection would have a lock on the record until it COMMITs, but to my surprise the output is "A121".
Does SQLite actually not lock the table or am I misunderstanding what's happening? (Would I get the same result with another database like PostgreSQL?)
CodePudding user response:
I'd actually expect the code to error out if you have multiple connections since you're creating a deadlock. You're running into an implementation quirk of SQLAlchemy.
From the documentation:
class sqlalchemy.pool.SingletonThreadPool(creator, pool_size=5, **kw)
A Pool that maintains one connection per thread.
[...]
SingletonThreadPool is used by the SQLite dialect automatically when a memory-based database is used.
In other words, when you open a :memory:
database, rather than using SQLite's shared cache mode that would enable sharing a memory database between different connections, SQLAlchemy creates a single connection and funnels all connections objects through it.
You can see the same behavior on a file connection by passing poolclass=SingletonThreadPool
to create_engine
, or you can see the error with a memory database using multiple connections to the same database outside of SQLAlchemy:
import sqlite3
db1 = sqlite3.connect("file::memory:?cache=shared", uri=True)
db2 = sqlite3.connect("file::memory:?cache=shared", uri=True)
conn1 = db1.cursor()
conn2 = db2.cursor()
conn1.execute("CREATE TABLE leagues (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL);")
conn1.execute("INSERT INTO leagues (name) VALUES ('A')")
conn1.execute("UPDATE leagues SET name = name || '1'")
# The next line will error out, since the db is locked by conn1
conn2.execute("UPDATE leagues SET name = name || '2'")