alembic icon indicating copy to clipboard operation
alembic copied to clipboard

document that URLs need to be URL escaped, and if present in alembic.ini also need percent signs doubled

Open skairunner opened this issue 4 years ago • 14 comments

Describe the bug Alembic cannot consume database URIs that contain special characters.

Expected behavior Expected behavior is either a nice error message, or preferably consuming the database URI string without complaint.

To Reproduce

Create a new, minimal Alembic repository. In the env.py, add a line like config.set_main_option("sqlalchemy.url", "postgres://test:@#$%^&*()@localhost/test"). Then attempt to create a revision and upgrade the database.

Alternatively, I have an example repository.

Error

Traceback (most recent call last):
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/bin/alembic", line 8, in <module>
    sys.exit(main())
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/config.py", line 577, in main
    CommandLine(prog=prog).main(argv=argv)
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/config.py", line 571, in main
    self.run_cmd(cfg, options)
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/config.py", line 548, in run_cmd
    fn(
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/command.py", line 298, in upgrade
    script.run_env()
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/script/base.py", line 489, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/util/pyfiles.py", line 98, in load_python_file
    module = load_module_py(module_id, path)
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/util/compat.py", line 184, in load_module_py
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "alembic/env.py", line 12, in <module>
    config.set_main_option("sqlalchemy.url", "postgres://test:@#$%^&*()@localhost/test")
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/config.py", line 242, in set_main_option
    self.set_section_option(self.config_ini_section, name, value)
  File "/home/sky/.cache/pypoetry/virtualenvs/alembic-test-8N3_MNVT-py3.8/lib/python3.8/site-packages/alembic/config.py", line 269, in set_section_option
    self.file_config.set(section, name, value)
  File "/usr/lib/python3.8/configparser.py", line 1201, in set
    super().set(section, option, value)
  File "/usr/lib/python3.8/configparser.py", line 894, in set
    value = self._interpolation.before_set(self, section, option,
  File "/usr/lib/python3.8/configparser.py", line 402, in before_set
    raise ValueError("invalid interpolation syntax in %r at "
ValueError: invalid interpolation syntax in 'postgres://test:@#$%^&*()@localhost/test' at position 19

Versions.

  • OS: WSL 2 with Ubuntu 20.04
  • Python: 3.8.2
  • Alembic: 1.4.2
  • SQLAlchemy: 1.1.0
  • Database: Latest Postgres image on Docker
  • DBAPI:

Additional context

This is relevant because, if a DB URI's password is naively escaped (such as with urllib.parse.quote) it will contain many many % signs. E.g. @#$%^&* will turn into %40%23%24%25%5E%26%2A. I think that ideally the config interpolation should not use % interpolation?

Assuming this is a simple issue to fix, I intend to open a PR to try and address this.

skairunner avatar May 29 '20 13:05 skairunner

Hi, I think you need to quote the url in this case. See the documentation on sqlalchemy

CaselIT avatar May 29 '20 14:05 CaselIT

The problem is that the quoted version still contains naked %, but it seems like Alembic expects them to be %% somewhere within its code.

On Fri, May 29, 2020, 16:36 Federico Caselli [email protected] wrote:

Hi, I think you need to quote the url in this case. See the documentation https://docs.sqlalchemy.org/en/13/core/engines.html?highlight=create_engine#database-urls on sqlalchemy

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/sqlalchemy/alembic/issues/700#issuecomment-636008648, or unsubscribe https://github.com/notifications/unsubscribe-auth/AA22VOZBCAR32ZO4UIOZXELRT7B7LANCNFSM4NOBFVKA .

skairunner avatar May 29 '20 14:05 skairunner

If you quote '@#$%^&*'

>>> urllib.parse.quote_plus('@#$%^&*')
'%40%23%24%25%5E%26%2A'

so there is not naked %

CaselIT avatar May 29 '20 14:05 CaselIT

hi, are you using flask-migrate? you would be experiencing https://github.com/miguelgrinberg/Flask-Migrate/issues/328 if so which has an issue in that it rewrites the URL a second time. there are workarounds present there.

zzzeek avatar May 29 '20 15:05 zzzeek

also yes, alembic.ini uses Python configparser, which requires an additional escape of % signs.

zzzeek avatar May 29 '20 15:05 zzzeek

If you quote '@#$%^&*'

>>> urllib.parse.quote_plus('@#$%^&*')
'%40%23%24%25%5E%26%2A'

so there is not naked %

As zzzeek said, because alembic uses configparser the "%" has to be double escaped into %%s for it to be valid. This isn't obvious either in the documentation or in the error message. I think it would be helpful if they were updated to make this clear.

Also, as the example repository shows, I'm just using Alembic and sqlalchemy to reproduce this error.

skairunner avatar May 29 '20 15:05 skairunner

I created a SQLite database:

sqlite3 db%file.db

put it in alembic.ini as:

sqlalchemy.url = sqlite:///db%%file.db

and it works.

I took the URL from your sample file and escaped thusly:

>>> urllib.parse.quote_plus("@#$%^&*()").replace("%", "%%")
'%%40%%23%%24%%25%%5E%%26%%2A%%28%%29'

added to alembic.ini as:

sqlalchemy.url = postgresql://test:%%40%%23%%24%%25%%5E%%26%%2A%%28%%29@localhost/test

and while I dont have that username setup, I pdb'ed into the psycopg2 dialect and we can see the arguments restored correctly:

{'host': 'localhost', 'database': 'test', 'user': 'test', 'password': '@#$%^&*()'}

We should probably add a note to the docs and to the comments inside the sample alembic.ini that indicates the above recipe for escaping.

zzzeek avatar May 29 '20 15:05 zzzeek

If you quote '@#$%^&*'

>>> urllib.parse.quote_plus('@#$%^&*')
'%40%23%24%25%5E%26%2A'

so there is not naked %

As zzzeek said, because alembic uses configparser the "%" has to be double escaped into %%s for it to be valid. This isn't obvious either in the documentation or in the error message. I think it would be helpful if they were updated to make this clear.

Also, as the example repository shows, I'm just using Alembic and sqlalchemy to reproduce this error.

Indeed, I had not noticed that configparser was involved in this. Usually there is no need to double escape the %

CaselIT avatar May 29 '20 15:05 CaselIT

What is the status of this issue? Still does not seem to work for me. Is there a workaround until the proposed fix is rolled in?

bkrram avatar Mar 07 '24 06:03 bkrram

There is no fix propoed, just some additional documentation. Have you tried following this? https://github.com/sqlalchemy/alembic/issues/700#issuecomment-636037036

CaselIT avatar Mar 07 '24 06:03 CaselIT

I did. The problem is the @ in the password. Delimits there and treats the rest as the beginning of the db host part and bombs with this stack trace. @ramki88 has a fix that was submitted above which has not been approved yet so I was wondering what the status of that was..

Traceback (most recent call last): File "alembic/stamp.py", line 47, in stamp_revision() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/click/core.py", line 1157, in call return self.main(*args, **kwargs) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/click/core.py", line 1078, in main rv = self.invoke(ctx) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/click/core.py", line 1434, in invoke return ctx.invoke(self.callback, **ctx.params) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/click/core.py", line 783, in invoke return __callback(*args, **kwargs) File "alembic/stamp.py", line 43, in stamp_revision command.stamp(config, rev_id) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/alembic/command.py", line 690, in stamp script.run_env() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/alembic/script/base.py", line 583, in run_env util.load_python_file(self.dir, "env.py") File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/alembic/util/pyfiles.py", line 95, in load_python_file module = load_module_py(module_id, path) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/alembic/util/pyfiles.py", line 113, in load_module_py spec.loader.exec_module(module) # type: ignore File "", line 848, in exec_module File "", line 219, in _call_with_frames_removed File "/home/deploy/websites/altmo_python/altmo_rideschool/alembic/env.py", line 92, in run_migrations_online() File "/home/deploy/websites/altmo_python/altmo_rideschool/alembic/env.py", line 80, in run_migrations_online with connectable.connect() as connection: File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 3279, in connect return self._connection_cls(self) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 147, in init Connection._handle_dbapi_exception_noconnection( File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2443, in _handle_dbapi_exception_noconnection raise sqlalchemy_exception.with_traceback(exc_info[2]) from e File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 145, in init self._dbapi_connection = engine.raw_connection() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 3303, in raw_connection return self.pool.connect() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 449, in connect return _ConnectionFairy._checkout(self) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 1263, in _checkout fairy = _ConnectionRecord.checkout(pool) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 712, in checkout rec = pool._do_get() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/impl.py", line 308, in _do_get return self._create_connection() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 390, in _create_connection return _ConnectionRecord(self) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 674, in init self.__connect() File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 901, in __connect pool.logger.debug("Error on connect(): %s", e) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py", line 146, in exit raise exc_value.with_traceback(exc_tb) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 896, in __connect self.dbapi_connection = connection = pool._invoke_creator(self) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/create.py", line 643, in connect return dialect.connect(*cargs, **cparams) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 616, in connect return self.loaded_dbapi.connect(*cargs, **cparams) File "/home/deploy/websites/altmo_python/venv/lib/python3.8/site-packages/psycopg2/init.py", line 122, in connect conn = _connect(dsn, connection_factory=connection_factory, **kwasync) sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) could not translate host name "[email protected]" to address: Name or service not known

Where the XXX is the part of the password that follows the @ in the password string.

bkrram avatar Mar 07 '24 06:03 bkrram

Special characters need to be escaped. Like in the linked comment..

CaselIT avatar Mar 07 '24 07:03 CaselIT

This result was after escaping the whole password string and replacing the single %s with doubles.

On Thu, Mar 7, 2024 at 12:58 PM Federico Caselli @.***> wrote:

Special characters need to be escaped. Like in the linked comment..

— Reply to this email directly, view it on GitHub https://github.com/sqlalchemy/alembic/issues/700#issuecomment-1982739512, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACEC3QFQVTH2DFULRT6N6FLYXAJKXAVCNFSM4NOBFVKKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCOJYGI3TGOJVGEZA . You are receiving this because you commented.Message ID: @.***>

bkrram avatar Mar 07 '24 08:03 bkrram

Only username and passwords should be escaped.

This is OT for this issue though. How to use a password string is explained in the sqlalchemy docs https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls

CaselIT avatar Mar 07 '24 09:03 CaselIT