async def test_finish_commit(raise_exception, tmp_path): """ Tests that the session is automatically committed if and only if the context was not exited with an exception. """ db_path = tmp_path / "test.db" engine = create_engine(f"sqlite:///{db_path}", poolclass=NullPool) with engine.begin() as connection: connection.execute(text("CREATE TABLE foo (id INTEGER PRIMARY KEY)")) component = SQLAlchemyComponent(url={ "drivername": "sqlite", "database": str(db_path) }, ) with ExitStack() as stack: async with Context() as ctx: await component.start(ctx) session = ctx.require_resource(Session) session.execute(text("INSERT INTO foo (id) VALUES(3)")) if raise_exception: stack.enter_context(pytest.raises(Exception, match="dummy")) raise Exception("dummy") rows = connection.execute(text("SELECT * FROM foo")).fetchall() assert len(rows) == (0 if raise_exception else 1)
async def test_multiple_engines(): component = SQLAlchemyComponent(engines={'db1': {}, 'db2': {}}, url='sqlite:///:memory:') async with Context() as ctx: await component.start(ctx) engine1 = ctx.require_resource(Engine, 'db1') engine2 = ctx.require_resource(Engine, 'db2') assert ctx.db1.bind is engine1 assert ctx.db2.bind is engine2
async def test_component_start_async(): """Test that the component creates all the expected (asynchronous) resources.""" url = URL.create("sqlite+aiosqlite", database=":memory:") component = SQLAlchemyComponent(url=url) async with Context() as ctx: await component.start(ctx) ctx.require_resource(AsyncEngine) ctx.require_resource(sessionmaker) ctx.require_resource(AsyncSession)
async def test_bind(): """Test that a Connection can be passed as "bind" in place of "url".""" engine = create_engine('sqlite:///:memory:') connection = engine.connect() component = SQLAlchemyComponent(bind=connection) async with Context() as ctx: await component.start(ctx) assert ctx.require_resource(Engine) is engine assert ctx.sql.bind is connection
async def test_component_start(poolclass): """Test that the component creates all the expected resources.""" component = SQLAlchemyComponent(url='sqlite:///:memory:', poolclass=poolclass) async with Context() as ctx: await component.start(ctx) engine = ctx.require_resource(Engine) ctx.require_resource(sessionmaker) assert ctx.sql is ctx.require_resource(Session) assert ctx.sql.bind is engine
async def test_memory_leak(): """Test that creating a session in a context does not leak memory.""" component = SQLAlchemyComponent(url='sqlite:///:memory:') async with Context() as ctx: await component.start(ctx) assert isinstance(ctx.sql, Session) del ctx gc.collect() # needed on PyPy assert next((x for x in gc.get_objects() if isinstance(x, Context)), None) is None
async def test_bind_async(): """Test that a Connection can be passed as "bind" in place of "url".""" engine = create_async_engine("sqlite+aiosqlite:///:memory:") connection = await engine.connect() component = SQLAlchemyComponent(bind=connection) async with Context() as ctx: await component.start(ctx) assert ctx.require_resource(AsyncEngine) is engine assert ctx.require_resource(AsyncSession).bind is connection await connection.close()
async def test_close_twice_async(asyncpg_url): """Test that closing a session releases connection resources, but remains usable.""" component = SQLAlchemyComponent(url=asyncpg_url) async with Context() as ctx: await component.start(ctx) session = ctx.require_resource(AsyncSession) pool = session.bind.pool await session.execute(text("SELECT 1")) assert pool.checkedout() == 1 await session.close() assert pool.checkedout() == 0 await session.execute(text("SELECT 1")) assert pool.checkedout() == 1 assert pool.checkedout() == 0
async def test_ready_callback(asynchronous): def ready_callback(engine, factory): nonlocal engine2, factory2 engine2 = engine factory2 = factory async def ready_callback_async(engine, factory): nonlocal engine2, factory2 engine2 = engine factory2 = factory engine2 = factory2 = None callback = ready_callback_async if asynchronous else ready_callback component = SQLAlchemyComponent(url='sqlite:///:memory:', ready_callback=callback) async with Context() as ctx: await component.start(ctx) engine = ctx.require_resource(Engine) factory = ctx.require_resource(sessionmaker) assert engine is engine2 assert factory is factory2
async def test_finish_commit(raise_exception, executor, commit_executor, tmpdir): """ Tests that the session is automatically committed if and only if the context was not exited with an exception. """ db_path = tmpdir.join('test.db') engine = create_engine('sqlite:///%s' % db_path, poolclass=NullPool) engine.execute('CREATE TABLE foo (id INTEGER PRIMARY KEY)') component = SQLAlchemyComponent( url={'drivername': 'sqlite', 'database': str(db_path)}, commit_executor=executor if commit_executor == 'instance' else commit_executor) ctx = Context() ctx.add_resource(executor, types=[Executor]) await component.start(ctx) ctx.sql.execute('INSERT INTO foo (id) VALUES(3)') await ctx.close(Exception('dummy') if raise_exception else None) rows = engine.execute('SELECT * FROM foo').fetchall() assert len(rows) == (0 if raise_exception else 1)
async def test_session_event_async(request, asyncpg_url, psycopg2_url): """Test that creating a session in a context does not leak memory.""" listener_session: Session listener_thread: Thread def listener(session: Session) -> None: nonlocal listener_session, listener_thread try: async_session = get_resource(AsyncSession) except NoCurrentContext: return if async_session and session is async_session.sync_session: listener_session = session listener_thread = current_thread() listen(Session, "before_commit", listener) request.addfinalizer(lambda: remove(Session, "before_commit", listener)) component = SQLAlchemyComponent(url=asyncpg_url) async with Context() as ctx: await component.start(ctx) dbsession = ctx.require_resource(AsyncSession) await dbsession.run_sync( lambda session: Person.metadata.create_all(session.bind)) dbsession.add(Person(name="Test person")) assert listener_session is dbsession.sync_session assert listener_thread is current_thread() engine = create_engine(psycopg2_url) with Session(engine) as sess: sess.add(Person(name="Test person 2")) sess.commit() engine.dispose() assert listener_session is dbsession.sync_session assert listener_thread is current_thread()
async def test_session_event_sync(psycopg2_url): """Test that creating a session in a context does not leak memory.""" listener_session: Session listener_thread: Thread def listener(session: Session) -> None: nonlocal listener_session, listener_thread current_context() listener_session = session listener_thread = current_thread() component = SQLAlchemyComponent(url=psycopg2_url) async with Context() as ctx: await component.start(ctx) Person.metadata.create_all(ctx.require_resource(Engine)) session_factory = ctx.require_resource(sessionmaker) listen(session_factory, "before_commit", listener) dbsession = ctx.require_resource(Session) dbsession.add(Person(name="Test person")) assert listener_session is dbsession assert listener_thread != current_thread()
def connect_test_database(url: Union[str, URL], **engine_kwargs) -> Connection: """ Connect to the given database and drop any existing tables in it. For SQLite URLs pointing to a file, the target database file will be deleted and a new one is created in its place. :param url: connection URL for the database :param engine_kwargs: additional keyword arguments passed to :meth:`asphalt.sqlalchemy.component.SQLAlchemyComponent.create_engine` :return: a connection object """ assert check_argument_types() _context_attr, engine = SQLAlchemyComponent.configure_engine(url=url, **engine_kwargs) if engine.dialect.name == 'sqlite': # SQLite does not support dropping constraints and it's faster to just delete the file if engine.url.database not in (None, ':memory:') and os.path.isfile(engine.url.database): os.remove(engine.url.database) connection = engine.connect() else: # Reflect the schema to get the list of the tables and constraints left over from the # previous run connection = engine.connect() metadata = MetaData(connection, reflect=True) # Drop all the foreign key constraints so we can drop the tables in any order for table in metadata.tables.values(): for fk in table.foreign_keys: connection.execute(DropConstraint(fk.constraint)) # Drop the tables metadata.drop_all() return connection