v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成
- Docker bridge 网络隔离(8000 端口封死) - Gunicorn 4 Worker 多进程 - Alembic 数据库迁移基线 - 日志轮转 20m×3 - JWT 密钥 + DB 密码 + CORS 收紧 - 3-2-1 备份链路(NAS + R740-B 冷备) - 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
@@ -0,0 +1,70 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# You cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||
export VIRTUAL_ENV=$(cygpath /home/hankin/crm_project/server/venv)
|
||||
else
|
||||
# use the path as-is
|
||||
export VIRTUAL_ENV=/home/hankin/crm_project/server/venv
|
||||
fi
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1='(venv) '"${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT='(venv) '
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
@@ -0,0 +1,27 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
|
||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV /home/hankin/crm_project/server/venv
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = '(venv) '"$prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
@@ -0,0 +1,69 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/). You cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV /home/hankin/crm_project/server/venv
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||
end
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from alembic.config import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from dotenv.__main__ import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from fastapi.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from httpx import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from mako.cmd import cmdline
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cmdline())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import decrypt
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(decrypt())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import encrypt
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(encrypt())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import keygen
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(keygen())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from rsa.util import private_to_public
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(private_to_public())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import sign
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(sign())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from rsa.cli import verify
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(verify())
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
python3
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
python3
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from uvicorn.main import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from watchfiles.cli import cli
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(cli())
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/home/hankin/crm_project/server/venv/bin/python3
|
||||
import sys
|
||||
from websockets.cli import main
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = sys.argv[0].removesuffix('.exe')
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,164 @@
|
||||
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
|
||||
|
||||
/* Greenlet object interface */
|
||||
|
||||
#ifndef Py_GREENLETOBJECT_H
|
||||
#define Py_GREENLETOBJECT_H
|
||||
|
||||
|
||||
#include <Python.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is deprecated and undocumented. It does not change. */
|
||||
#define GREENLET_VERSION "1.0.0"
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
#define implementation_ptr_t void*
|
||||
#endif
|
||||
|
||||
typedef struct _greenlet {
|
||||
PyObject_HEAD
|
||||
PyObject* weakreflist;
|
||||
PyObject* dict;
|
||||
implementation_ptr_t pimpl;
|
||||
} PyGreenlet;
|
||||
|
||||
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
|
||||
|
||||
|
||||
/* C API functions */
|
||||
|
||||
/* Total number of symbols that are exported */
|
||||
#define PyGreenlet_API_pointers 12
|
||||
|
||||
#define PyGreenlet_Type_NUM 0
|
||||
#define PyExc_GreenletError_NUM 1
|
||||
#define PyExc_GreenletExit_NUM 2
|
||||
|
||||
#define PyGreenlet_New_NUM 3
|
||||
#define PyGreenlet_GetCurrent_NUM 4
|
||||
#define PyGreenlet_Throw_NUM 5
|
||||
#define PyGreenlet_Switch_NUM 6
|
||||
#define PyGreenlet_SetParent_NUM 7
|
||||
|
||||
#define PyGreenlet_MAIN_NUM 8
|
||||
#define PyGreenlet_STARTED_NUM 9
|
||||
#define PyGreenlet_ACTIVE_NUM 10
|
||||
#define PyGreenlet_GET_PARENT_NUM 11
|
||||
|
||||
#ifndef GREENLET_MODULE
|
||||
/* This section is used by modules that uses the greenlet C API */
|
||||
static void** _PyGreenlet_API = NULL;
|
||||
|
||||
# define PyGreenlet_Type \
|
||||
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
|
||||
|
||||
# define PyExc_GreenletError \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
|
||||
|
||||
# define PyExc_GreenletExit \
|
||||
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_New(PyObject *args)
|
||||
*
|
||||
* greenlet.greenlet(run, parent=None)
|
||||
*/
|
||||
# define PyGreenlet_New \
|
||||
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
|
||||
_PyGreenlet_API[PyGreenlet_New_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetCurrent(void)
|
||||
*
|
||||
* greenlet.getcurrent()
|
||||
*/
|
||||
# define PyGreenlet_GetCurrent \
|
||||
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Throw(
|
||||
* PyGreenlet *greenlet,
|
||||
* PyObject *typ,
|
||||
* PyObject *val,
|
||||
* PyObject *tb)
|
||||
*
|
||||
* g.throw(...)
|
||||
*/
|
||||
# define PyGreenlet_Throw \
|
||||
(*(PyObject * (*)(PyGreenlet * self, \
|
||||
PyObject * typ, \
|
||||
PyObject * val, \
|
||||
PyObject * tb)) \
|
||||
_PyGreenlet_API[PyGreenlet_Throw_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
|
||||
*
|
||||
* g.switch(*args, **kwargs)
|
||||
*/
|
||||
# define PyGreenlet_Switch \
|
||||
(*(PyObject * \
|
||||
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
|
||||
_PyGreenlet_API[PyGreenlet_Switch_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
|
||||
*
|
||||
* g.parent = new_parent
|
||||
*/
|
||||
# define PyGreenlet_SetParent \
|
||||
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
|
||||
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
|
||||
|
||||
/*
|
||||
* PyGreenlet_GetParent(PyObject* greenlet)
|
||||
*
|
||||
* return greenlet.parent;
|
||||
*
|
||||
* This could return NULL even if there is no exception active.
|
||||
* If it does not return NULL, you are responsible for decrementing the
|
||||
* reference count.
|
||||
*/
|
||||
# define PyGreenlet_GetParent \
|
||||
(*(PyGreenlet* (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
|
||||
|
||||
/*
|
||||
* deprecated, undocumented alias.
|
||||
*/
|
||||
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
|
||||
|
||||
# define PyGreenlet_MAIN \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
|
||||
|
||||
# define PyGreenlet_STARTED \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
|
||||
|
||||
# define PyGreenlet_ACTIVE \
|
||||
(*(int (*)(PyGreenlet*)) \
|
||||
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
|
||||
|
||||
|
||||
|
||||
|
||||
/* Macro that imports greenlet and initializes C API */
|
||||
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
|
||||
keep the older definition to be sure older code that might have a copy of
|
||||
the header still works. */
|
||||
# define PyGreenlet_Import() \
|
||||
{ \
|
||||
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
|
||||
}
|
||||
|
||||
#endif /* GREENLET_MODULE */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_GREENLETOBJECT_H */
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright 2005-2024 SQLAlchemy authors and contributors <see AUTHORS file>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,243 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: SQLAlchemy
|
||||
Version: 2.0.36
|
||||
Summary: Database Abstraction Library
|
||||
Home-page: https://www.sqlalchemy.org
|
||||
Author: Mike Bayer
|
||||
Author-email: mike_mp@zzzcomputing.com
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://docs.sqlalchemy.org
|
||||
Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Database :: Front-Ends
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: typing-extensions >=4.6.0
|
||||
Requires-Dist: greenlet !=0.4.17 ; python_version < "3.13" and (platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32"))))))
|
||||
Requires-Dist: importlib-metadata ; python_version < "3.8"
|
||||
Provides-Extra: aiomysql
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
|
||||
Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
|
||||
Provides-Extra: aioodbc
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc'
|
||||
Requires-Dist: aioodbc ; extra == 'aioodbc'
|
||||
Provides-Extra: aiosqlite
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
|
||||
Requires-Dist: aiosqlite ; extra == 'aiosqlite'
|
||||
Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
|
||||
Provides-Extra: asyncio
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
|
||||
Provides-Extra: asyncmy
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
|
||||
Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
|
||||
Provides-Extra: mariadb_connector
|
||||
Requires-Dist: mariadb !=1.1.10,!=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
|
||||
Provides-Extra: mssql
|
||||
Requires-Dist: pyodbc ; extra == 'mssql'
|
||||
Provides-Extra: mssql_pymssql
|
||||
Requires-Dist: pymssql ; extra == 'mssql_pymssql'
|
||||
Provides-Extra: mssql_pyodbc
|
||||
Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
|
||||
Provides-Extra: mypy
|
||||
Requires-Dist: mypy >=0.910 ; extra == 'mypy'
|
||||
Provides-Extra: mysql
|
||||
Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
|
||||
Provides-Extra: mysql_connector
|
||||
Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
|
||||
Provides-Extra: oracle
|
||||
Requires-Dist: cx-oracle >=8 ; extra == 'oracle'
|
||||
Provides-Extra: oracle_oracledb
|
||||
Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
|
||||
Provides-Extra: postgresql
|
||||
Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
|
||||
Provides-Extra: postgresql_asyncpg
|
||||
Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
|
||||
Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
|
||||
Provides-Extra: postgresql_pg8000
|
||||
Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
|
||||
Provides-Extra: postgresql_psycopg
|
||||
Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
|
||||
Provides-Extra: postgresql_psycopg2binary
|
||||
Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
|
||||
Provides-Extra: postgresql_psycopg2cffi
|
||||
Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
|
||||
Provides-Extra: postgresql_psycopgbinary
|
||||
Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
|
||||
Provides-Extra: pymysql
|
||||
Requires-Dist: pymysql ; extra == 'pymysql'
|
||||
Provides-Extra: sqlcipher
|
||||
Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
|
||||
|
||||
SQLAlchemy
|
||||
==========
|
||||
|
||||
|PyPI| |Python| |Downloads|
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI
|
||||
|
||||
.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
|
||||
:target: https://pypi.org/project/sqlalchemy
|
||||
:alt: PyPI - Python Version
|
||||
|
||||
.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
|
||||
:target: https://pepy.tech/project/sqlalchemy
|
||||
:alt: PyPI - Downloads
|
||||
|
||||
|
||||
The Python SQL Toolkit and Object Relational Mapper
|
||||
|
||||
Introduction
|
||||
-------------
|
||||
|
||||
SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
|
||||
that gives application developers the full power and
|
||||
flexibility of SQL. SQLAlchemy provides a full suite
|
||||
of well known enterprise-level persistence patterns,
|
||||
designed for efficient and high-performing database
|
||||
access, adapted into a simple and Pythonic domain
|
||||
language.
|
||||
|
||||
Major SQLAlchemy features include:
|
||||
|
||||
* An industrial strength ORM, built
|
||||
from the core on the identity map, unit of work,
|
||||
and data mapper patterns. These patterns
|
||||
allow transparent persistence of objects
|
||||
using a declarative configuration system.
|
||||
Domain models
|
||||
can be constructed and manipulated naturally,
|
||||
and changes are synchronized with the
|
||||
current transaction automatically.
|
||||
* A relationally-oriented query system, exposing
|
||||
the full range of SQL's capabilities
|
||||
explicitly, including joins, subqueries,
|
||||
correlation, and most everything else,
|
||||
in terms of the object model.
|
||||
Writing queries with the ORM uses the same
|
||||
techniques of relational composition you use
|
||||
when writing SQL. While you can drop into
|
||||
literal SQL at any time, it's virtually never
|
||||
needed.
|
||||
* A comprehensive and flexible system
|
||||
of eager loading for related collections and objects.
|
||||
Collections are cached within a session,
|
||||
and can be loaded on individual access, all
|
||||
at once using joins, or by query per collection
|
||||
across the full result set.
|
||||
* A Core SQL construction system and DBAPI
|
||||
interaction layer. The SQLAlchemy Core is
|
||||
separate from the ORM and is a full database
|
||||
abstraction layer in its own right, and includes
|
||||
an extensible Python-based SQL expression
|
||||
language, schema metadata, connection pooling,
|
||||
type coercion, and custom types.
|
||||
* All primary and foreign key constraints are
|
||||
assumed to be composite and natural. Surrogate
|
||||
integer primary keys are of course still the
|
||||
norm, but SQLAlchemy never assumes or hardcodes
|
||||
to this model.
|
||||
* Database introspection and generation. Database
|
||||
schemas can be "reflected" in one step into
|
||||
Python structures representing database metadata;
|
||||
those same structures can then generate
|
||||
CREATE statements right back out - all within
|
||||
the Core, independent of the ORM.
|
||||
|
||||
SQLAlchemy's philosophy:
|
||||
|
||||
* SQL databases behave less and less like object
|
||||
collections the more size and performance start to
|
||||
matter; object collections behave less and less like
|
||||
tables and rows the more abstraction starts to matter.
|
||||
SQLAlchemy aims to accommodate both of these
|
||||
principles.
|
||||
* An ORM doesn't need to hide the "R". A relational
|
||||
database provides rich, set-based functionality
|
||||
that should be fully exposed. SQLAlchemy's
|
||||
ORM provides an open-ended set of patterns
|
||||
that allow a developer to construct a custom
|
||||
mediation layer between a domain model and
|
||||
a relational schema, turning the so-called
|
||||
"object relational impedance" issue into
|
||||
a distant memory.
|
||||
* The developer, in all cases, makes all decisions
|
||||
regarding the design, structure, and naming conventions
|
||||
of both the object model as well as the relational
|
||||
schema. SQLAlchemy only provides the means
|
||||
to automate the execution of these decisions.
|
||||
* With SQLAlchemy, there's no such thing as
|
||||
"the ORM generated a bad query" - you
|
||||
retain full control over the structure of
|
||||
queries, including how joins are organized,
|
||||
how subqueries and correlation is used, what
|
||||
columns are requested. Everything SQLAlchemy
|
||||
does is ultimately the result of a developer-initiated
|
||||
decision.
|
||||
* Don't use an ORM if the problem doesn't need one.
|
||||
SQLAlchemy consists of a Core and separate ORM
|
||||
component. The Core offers a full SQL expression
|
||||
language that allows Pythonic construction
|
||||
of SQL constructs that render directly to SQL
|
||||
strings for a target database, returning
|
||||
result sets that are essentially enhanced DBAPI
|
||||
cursors.
|
||||
* Transactions should be the norm. With SQLAlchemy's
|
||||
ORM, nothing goes to permanent storage until
|
||||
commit() is called. SQLAlchemy encourages applications
|
||||
to create a consistent means of delineating
|
||||
the start and end of a series of operations.
|
||||
* Never render a literal value in a SQL statement.
|
||||
Bound parameters are used to the greatest degree
|
||||
possible, allowing query optimizers to cache
|
||||
query plans effectively and making SQL injection
|
||||
attacks a non-issue.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Latest documentation is at:
|
||||
|
||||
https://www.sqlalchemy.org/docs/
|
||||
|
||||
Installation / Requirements
|
||||
---------------------------
|
||||
|
||||
Full documentation for installation is at
|
||||
`Installation <https://www.sqlalchemy.org/docs/intro.html#installation>`_.
|
||||
|
||||
Getting Help / Development / Bug reporting
|
||||
------------------------------------------
|
||||
|
||||
Please refer to the `SQLAlchemy Community Guide <https://www.sqlalchemy.org/support.html>`_.
|
||||
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
|
||||
constructive communication between users and developers.
|
||||
Please see our current Code of Conduct at
|
||||
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
SQLAlchemy is distributed under the `MIT license
|
||||
<https://www.opensource.org/licenses/mit-license.php>`_.
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
SQLAlchemy-2.0.36.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
SQLAlchemy-2.0.36.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100
|
||||
SQLAlchemy-2.0.36.dist-info/METADATA,sha256=EZH514FydYtyOhgoZk_OF1ZQEtI4eTAEddlnUlRjzac,9692
|
||||
SQLAlchemy-2.0.36.dist-info/RECORD,,
|
||||
SQLAlchemy-2.0.36.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
SQLAlchemy-2.0.36.dist-info/WHEEL,sha256=7B4nnId14TToQHuAKpxbDLCJbNciqBsV-mvXE2hVLJc,151
|
||||
SQLAlchemy-2.0.36.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
|
||||
sqlalchemy/__init__.py,sha256=J2PsdiJiNW93Etxk6YN8o_C3TcpR1_DckU71r4LBcGE,13033
|
||||
sqlalchemy/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/inspection.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/log.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__init__.py,sha256=PzXPqZqi3BzEnrs1eW0DcsR4lyknAzhhN9rWcQ97hb4,476
|
||||
sqlalchemy/connectors/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/aioodbc.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/asyncio.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/connectors/aioodbc.py,sha256=GSTiNMO9h0qjPxgqaxDwWZ8HvhWMFNVR6MJQnN1oc40,5288
|
||||
sqlalchemy/connectors/asyncio.py,sha256=Hq2bkXmG6-KO_RfCrwMqx4oGH-uH1Z1WWKqPWNjz8p4,6138
|
||||
sqlalchemy/connectors/pyodbc.py,sha256=t7AjyxIOnaWg3CrlUEpBs4Y5l0HFdNt3P_cSSKhbi0Y,8501
|
||||
sqlalchemy/cyextension/__init__.py,sha256=GzhhN8cjMnDTE0qerlUlpbrNmFPHQWCZ4Gk74OAxl04,244
|
||||
sqlalchemy/cyextension/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/cyextension/collections.cpython-312-x86_64-linux-gnu.so,sha256=ofziMIrcxCV-AGNBEvHL7QorRR2SPA9bkQj_k3WLD9E,1932256
|
||||
sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571
|
||||
sqlalchemy/cyextension/immutabledict.cpython-312-x86_64-linux-gnu.so,sha256=7SNuSRYPX4hzKqjAtbxJT2xLwa6Aegqtjfc9MYYZV0w,805632
|
||||
sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291
|
||||
sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535
|
||||
sqlalchemy/cyextension/processors.cpython-312-x86_64-linux-gnu.so,sha256=hBeVC8lWoCgLmD5det5fcoL4V8LYT9Cu8TRLaAzWeW0,530680
|
||||
sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792
|
||||
sqlalchemy/cyextension/resultproxy.cpython-312-x86_64-linux-gnu.so,sha256=n6E3F0rPZFVUnpAm1pr3T-Rc8ZMqUcLjdRoOetVvJ8M,621328
|
||||
sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725
|
||||
sqlalchemy/cyextension/util.cpython-312-x86_64-linux-gnu.so,sha256=IbH9FP4ihbuGMhZ95oiRo_6F2wkSAlE2YDzegCVqErg,950928
|
||||
sqlalchemy/cyextension/util.pyx,sha256=B85orxa9LddLuQEaDoVSq1XmAXIbLKxrxpvuB8ogV_o,2530
|
||||
sqlalchemy/dialects/__init__.py,sha256=Kos9Gf5JZg1Vg6GWaCqEbD6e0r1jCwCmcnJIfcxDdcY,1770
|
||||
sqlalchemy/dialects/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/_typing.py,sha256=hyv0nKucX2gI8ispB1IsvaUgrEPn9zEcq9hS7kfstEw,888
|
||||
sqlalchemy/dialects/mssql/__init__.py,sha256=r5t8wFRNtBQoiUWh0WfIEWzXZW6f3D0uDt6NZTW_7Cc,1880
|
||||
sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/aioodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mssql/aioodbc.py,sha256=UQd9ecSMIML713TDnLAviuBVJle7P7i1FtqGZZePk2Y,2022
|
||||
sqlalchemy/dialects/mssql/base.py,sha256=msl_N_a_z8ali7Nthx55AGoV7b5wakCWvWu560BvH9o,132423
|
||||
sqlalchemy/dialects/mssql/information_schema.py,sha256=HswjDc6y0mPXCf_x6VyylHlBdBa4PSY6Evxmmlch700,8084
|
||||
sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816
|
||||
sqlalchemy/dialects/mssql/provision.py,sha256=ZAtt6Div9NLIngMs8kyloxfphw0KDNMsnRCAVd7-esE,5593
|
||||
sqlalchemy/dialects/mssql/pymssql.py,sha256=LAv43q4vBCB85OsAwHQItaQUYTYIO0QJ-jvzaBrswmY,4097
|
||||
sqlalchemy/dialects/mssql/pyodbc.py,sha256=vwM-vBlmRwrqxOc73P0sFOrBSwn24wzc5IkEOpalbXQ,27056
|
||||
sqlalchemy/dialects/mysql/__init__.py,sha256=bxbi4hkysUK2OOVvr1F49akUj1cky27kKb07tgFzI9U,2153
|
||||
sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/expression.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/mysql/aiomysql.py,sha256=-oMZnCqNsSki8mlQRTWIwiQPT1OVdZIuANkb90q8LAs,9999
|
||||
sqlalchemy/dialects/mysql/asyncmy.py,sha256=YpuuOh8VknEeqHqUXQGfQ3jhfO3Xb-vZv78Jq5cscJ0,10067
|
||||
sqlalchemy/dialects/mysql/base.py,sha256=giGlZNGrKsNMoSkbzY0PGgfamKjA9rOkSq1o5vKvno4,122755
|
||||
sqlalchemy/dialects/mysql/cymysql.py,sha256=eXT1ry0w_qRxjiO24M980c-8PZ9qSsbhqBHntjEiKB0,2300
|
||||
sqlalchemy/dialects/mysql/dml.py,sha256=HXJMAvimJsqvhj3UZO4vW_6LkF5RqaKbHvklAjor7yU,7645
|
||||
sqlalchemy/dialects/mysql/enumerated.py,sha256=ipEPPQqoXfFwcywNdcLlZCEzHBtnitHRah1Gn6nItcg,8448
|
||||
sqlalchemy/dialects/mysql/expression.py,sha256=lsmQCHKwfPezUnt27d2kR6ohk4IRFCA64KBS16kx5dc,4097
|
||||
sqlalchemy/dialects/mysql/json.py,sha256=l6MEZ0qp8FgiRrIQvOMhyEJq0q6OqiEnvDTx5Cbt9uQ,2269
|
||||
sqlalchemy/dialects/mysql/mariadb.py,sha256=kTfBLioLKk4JFFst4TY_iWqPtnvvQXFHknLfm89H2N8,853
|
||||
sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=_S1aV93kyP52Nvj7HR9weThML4oUvSLsLqiVFdoLR2o,8623
|
||||
sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=oq3mtsNOMldUjs32JbJG2u3Hy3DObyVzUUMYfOkwkHg,5729
|
||||
sqlalchemy/dialects/mysql/mysqldb.py,sha256=qUBbA6STeYGozutyTxHCo5p1W3p59QFFS2FwCgPrjBA,9503
|
||||
sqlalchemy/dialects/mysql/provision.py,sha256=Jnk8UO9_Apd2odR2IQFLrscCfAmYxuBKcB8giS3bBog,3575
|
||||
sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083
|
||||
sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297
|
||||
sqlalchemy/dialects/mysql/reflection.py,sha256=3u34YwT1JJh3uThGZJZ3FKdnUcT7v08QB-tAl1r7VRk,22834
|
||||
sqlalchemy/dialects/mysql/reserved_words.py,sha256=ucKX2p2c3UnMq2ayZuOHuf73eXhu7SKsOsTlIN1Q83I,9258
|
||||
sqlalchemy/dialects/mysql/types.py,sha256=L5cTCsMT1pTedszNEM3jSxFNZEMcHQLprYCZ0vmfsnA,24343
|
||||
sqlalchemy/dialects/oracle/__init__.py,sha256=p4-2gw7TT0bX_MoJXTGD4i8WHctYsK9kCRbkpzykBrc,1493
|
||||
sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/dictionary.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/oracledb.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/oracle/base.py,sha256=zLMZedrr6j1LvJz4qYnoSjikI5RZY92YFeQHiZ_YvW0,119676
|
||||
sqlalchemy/dialects/oracle/cx_oracle.py,sha256=q8Nyj15UZCE2TWOmxuWp5ZsxiCiGMzqfd_9UkmjIja0,55235
|
||||
sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519
|
||||
sqlalchemy/dialects/oracle/oracledb.py,sha256=fZRKGqNIwW9LG4i8yDOXABrucbfzn_yC86Od-BJ3PcM,13619
|
||||
sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304
|
||||
sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231
|
||||
sqlalchemy/dialects/postgresql/__init__.py,sha256=wwnNAq4wDQzrlPRzDNB06ayuq3L2HNO99nzeEvq-YcU,3892
|
||||
sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/_psycopg_common.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/array.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/named_types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/operators.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/pg_catalog.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/__pycache__/types.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=7TudtgsPiSB8O5kX8W8KxcNYR8t5h_UHb86b_ChL0P8,5696
|
||||
sqlalchemy/dialects/postgresql/array.py,sha256=bWcame7ntmI_Kx6gmBX0-chwADFdLHeCvaDQ4iX8id8,13734
|
||||
sqlalchemy/dialects/postgresql/asyncpg.py,sha256=9P0Itn9eeSBu67kGSsHuzx8xd4YYwRKdiZ5m7bF5onU,41074
|
||||
sqlalchemy/dialects/postgresql/base.py,sha256=dGPsaV3Esw6-AwE3QcgHF0Fray3Yw5-gLLgCvgdxvS0,179083
|
||||
sqlalchemy/dialects/postgresql/dml.py,sha256=Pc69Le6qzmUHHb1FT5zeUSD31dWm6SBgdCAGW89cs3s,11212
|
||||
sqlalchemy/dialects/postgresql/ext.py,sha256=1bZ--iNh2O9ym7l2gXZX48yP3yMO4dqb9RpYro2Mj2Q,16262
|
||||
sqlalchemy/dialects/postgresql/hstore.py,sha256=otAx-RTDfpi_tcXkMuQV0JOIXtYgevgnsikLKKOkI6U,11541
|
||||
sqlalchemy/dialects/postgresql/json.py,sha256=53rQWon9cUXd1yCjIvUpJjWwNyRSy3U7Kz0HV70ftrc,11618
|
||||
sqlalchemy/dialects/postgresql/named_types.py,sha256=3IV1ufo7zJjKmX4VtGDEnoXE6xEqLJAtGG82IiqHXwY,17594
|
||||
sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808
|
||||
sqlalchemy/dialects/postgresql/pg8000.py,sha256=3yoekiWSF-xnaWMqG76XrYPMqerg-42TdmfsW_ivK9E,18640
|
||||
sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=hY3NXEUHxTWD4umhd2aowNu3laC-61Q_qQ_pReyXTUM,9254
|
||||
sqlalchemy/dialects/postgresql/provision.py,sha256=t6TZj0XaWG9zrpCjNr0oJRjAC_WQzaNdp3kaKJIbS8I,5770
|
||||
sqlalchemy/dialects/postgresql/psycopg.py,sha256=Uwf45f9fInOtaExiEdwiP9xzRo7hw0XyZTkRtgdom44,23168
|
||||
sqlalchemy/dialects/postgresql/psycopg2.py,sha256=kwEnflz5bAqJcuO_20eYiCtha_a4m_tg5_lppdDnaeU,31998
|
||||
sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756
|
||||
sqlalchemy/dialects/postgresql/ranges.py,sha256=6CgV7qkxEMJ9AQsiibo_XBLJYzGh-2ZxpG83sRaesVY,32949
|
||||
sqlalchemy/dialects/postgresql/types.py,sha256=Jfxqw9JaKNOq29JRWBublywgb3lLMyzx8YZI7CXpS2s,7300
|
||||
sqlalchemy/dialects/sqlite/__init__.py,sha256=lp9DIggNn349M-7IYhUA8et8--e8FRExWD2V_r1LJk4,1182
|
||||
sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/json.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-312.pyc,,
|
||||
sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=g3qGV6jmiXabWyb3282g_Nmxtj1jThxGSe9C9yalb-U,12345
|
||||
sqlalchemy/dialects/sqlite/base.py,sha256=LcnW6hzxqTtPlDBOInHumvuDt8a31THA5Jnm4vFvdFI,97811
|
||||
sqlalchemy/dialects/sqlite/dml.py,sha256=9GE55WvwoktKy2fHeT-Wbc9xPHgsbh5oBfd_fckMH5Q,8443
|
||||
sqlalchemy/dialects/sqlite/json.py,sha256=Eoplbb_4dYlfrtmQaI8Xddd2suAIHA-IdbDQYM-LIhs,2777
|
||||
sqlalchemy/dialects/sqlite/provision.py,sha256=UCpmwxf4IWlrpb2eLHGbPTpCFVbdI_KAh2mKtjiLYao,5632
|
||||
sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=OL2S_05DK9kllZj6DOz7QtEl7jI7syxjW6woS725ii4,5356
|
||||
sqlalchemy/dialects/sqlite/pysqlite.py,sha256=aDp47n0J509kl2hDchoaBKXEQVZtkux54DwfKytUAe4,28068
|
||||
sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239
|
||||
sqlalchemy/engine/__init__.py,sha256=Stb2oV6l8w65JvqEo6J4qtKoApcmOpXy3AAxQud4C1o,2818
|
||||
sqlalchemy/engine/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_processors.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_row.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/_py_util.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/characteristics.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/create.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/cursor.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/default.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/interfaces.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/mock.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/processors.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/reflection.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/result.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/row.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/strategies.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/url.cpython-312.pyc,,
|
||||
sqlalchemy/engine/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/engine/_py_processors.py,sha256=j9i_lcYYQOYJMcsDerPxI0sVFBIlX5sqoYMdMJlgWPI,3744
|
||||
sqlalchemy/engine/_py_row.py,sha256=wSqoUFzLOJ1f89kgDb6sJm9LUrF5LMFpXPcK1vUsKcs,3787
|
||||
sqlalchemy/engine/_py_util.py,sha256=f2DI3AN1kv6EplelowesCVpwS8hSXNufRkZoQmJtSH8,2484
|
||||
sqlalchemy/engine/base.py,sha256=frWSMmt3dlentYH4QNN3cijdGzp8NbunColUZwWsWgI,122958
|
||||
sqlalchemy/engine/characteristics.py,sha256=N3kbvw_ApMh86wb5yAGnxtPYD4YRhYMWion1H_aVZBI,4765
|
||||
sqlalchemy/engine/create.py,sha256=mYJtOG2ZKM8sgyfjpGpamW15RDU7JXi5s6iibbJHMIs,33206
|
||||
sqlalchemy/engine/cursor.py,sha256=cFq61yrw76k-QR_xNUBWuL-Zeyb14ltG-6jo2Q2iuuw,76392
|
||||
sqlalchemy/engine/default.py,sha256=2wwKKdsagb3QTajRSEw8Hl-EnQ-LmRxy822xOGyenHc,84648
|
||||
sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381
|
||||
sqlalchemy/engine/interfaces.py,sha256=fcVHOmnMo7JZLHzgSKoK3QsdVHH7kJ_AmrDvwW9Ka3k,112936
|
||||
sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179
|
||||
sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379
|
||||
sqlalchemy/engine/reflection.py,sha256=gwGs8y7x6py5z-ZWx3hQqQrwpHepMCTJyQcFwWJjPlw,75364
|
||||
sqlalchemy/engine/result.py,sha256=NZEskTMAcDzK-vjE96Fw8VvBL58s5Y6rt9vXcmZdM4w,77651
|
||||
sqlalchemy/engine/row.py,sha256=9AAQo9zYDL88GcZ3bjcQTwMT-YIcuGTSMAyTfmBJ_yM,12032
|
||||
sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442
|
||||
sqlalchemy/engine/url.py,sha256=8eWkUaIUyDExOcJ2D4xJXRcn4OY1GQJ3Q2duSX6UGAg,30784
|
||||
sqlalchemy/engine/util.py,sha256=bNirO8k1S8yOW61uNH-a9QrWtAJ9VGFgbiR0lk1lUQU,5682
|
||||
sqlalchemy/event/__init__.py,sha256=KBrp622xojnC3FFquxa2JsMamwAbfkvzfv6Op0NKiYc,997
|
||||
sqlalchemy/event/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/api.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/attr.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/legacy.cpython-312.pyc,,
|
||||
sqlalchemy/event/__pycache__/registry.cpython-312.pyc,,
|
||||
sqlalchemy/event/api.py,sha256=DtDVgjKSorOfp9MGJ7fgMWrj4seC_hkwF4D8CW1RFZU,8226
|
||||
sqlalchemy/event/attr.py,sha256=X8QeHGK4ioSYht1vkhc11f606_mq_t91jMNIT314ubs,20751
|
||||
sqlalchemy/event/base.py,sha256=270OShTD17-bSFUFnPtKdVnB0NFJZ2AouYPo1wT0aJw,15127
|
||||
sqlalchemy/event/legacy.py,sha256=teMPs00fO-4g8a_z2omcVKkYce5wj_1uvJO2n2MIeuo,8227
|
||||
sqlalchemy/event/registry.py,sha256=nfTSSyhjZZXc5wseWB4sXn-YibSc0LKX8mg17XlWmAo,10835
|
||||
sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525
|
||||
sqlalchemy/exc.py,sha256=M_8-O1hd8i6gbyx-TapV400p_Lxq2QqTGMXUAO-YgCc,23976
|
||||
sqlalchemy/ext/__init__.py,sha256=S1fGKAbycnQDV01gs-JWGaFQ9GCD4QHwKcU2wnugg_o,322
|
||||
sqlalchemy/ext/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/associationproxy.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/automap.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/baked.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/compiler.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/horizontal_shard.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/hybrid.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/indexable.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/instrumentation.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/mutable.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/orderinglist.cpython-312.pyc,,
|
||||
sqlalchemy/ext/__pycache__/serializer.cpython-312.pyc,,
|
||||
sqlalchemy/ext/associationproxy.py,sha256=ZGc_ssGf7FC6eKrja1iTvnWEKLkFZQA8CiVAjR8iVRw,66062
|
||||
sqlalchemy/ext/asyncio/__init__.py,sha256=1OqSxEyIUn7RWLGyO12F-jAUIvk1I6DXlVy80-Gvkds,1317
|
||||
sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/engine.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/result.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/__pycache__/session.cpython-312.pyc,,
|
||||
sqlalchemy/ext/asyncio/base.py,sha256=fl7wxZD9KjgFiCtG3WXrYjHEvanamcsodCqq9pH9lOk,8905
|
||||
sqlalchemy/ext/asyncio/engine.py,sha256=S_IRWX4QAjj2veLSu4Y3gKBIXkKQt7_2StJAK2_KUDY,48190
|
||||
sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639
|
||||
sqlalchemy/ext/asyncio/result.py,sha256=3rbVIY_wySi50JwaK3Kf2qa3c5Fc8W84FtUpt-9i9Vk,30477
|
||||
sqlalchemy/ext/asyncio/scoping.py,sha256=UxHAFxtWKqA7TEozyN2h7MJyzSspTCrS-1SlgQLTExo,52608
|
||||
sqlalchemy/ext/asyncio/session.py,sha256=QpXnqspwYnT28znD1EdpUIaVjQOO1BirtS0BJeBxeZk,63087
|
||||
sqlalchemy/ext/automap.py,sha256=r0mUSyogNyqdBL4m9AA1NXbLiTLQmtvyQymsssNEipo,61581
|
||||
sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807
|
||||
sqlalchemy/ext/compiler.py,sha256=6X6sZCAo9v-PQfLbwBSYQUK0-XH2xTE5Jm0Zg6Ka6eM,20877
|
||||
sqlalchemy/ext/declarative/__init__.py,sha256=20psLdFQbbOWfpdXHZ0CTY6I1k4UqXvKemNVu1LvPOI,1818
|
||||
sqlalchemy/ext/declarative/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/declarative/__pycache__/extensions.cpython-312.pyc,,
|
||||
sqlalchemy/ext/declarative/extensions.py,sha256=uCjN1GisQt54AjqYnKYzJdUjnGd2pZBW47WWdPlS7FE,19547
|
||||
sqlalchemy/ext/horizontal_shard.py,sha256=wuwAPnHymln0unSBnyx-cpX0AfESKSsypaSQTYCvzDk,16750
|
||||
sqlalchemy/ext/hybrid.py,sha256=IYkCaPZ29gm2cPKPg0cWMkLCEqMykD8-JJTvgacGbmc,52458
|
||||
sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032
|
||||
sqlalchemy/ext/instrumentation.py,sha256=sg8ghDjdHSODFXh_jAmpgemnNX1rxCeeXEG3-PMdrNk,15707
|
||||
sqlalchemy/ext/mutable.py,sha256=L5ZkHBGYhMaqO75Xtyrk2DBR44RDk0g6Rz2HzHH0F8Q,37355
|
||||
sqlalchemy/ext/mypy/__init__.py,sha256=0WebDIZmqBD0OTq5JLtd_PmfF9JGxe4d4Qv3Ml3PKUg,241
|
||||
sqlalchemy/ext/mypy/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/apply.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/infer.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/names.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/plugin.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/ext/mypy/apply.py,sha256=Aek_-XA1eXihT4attxhfE43yBKtCgsxBSb--qgZKUqc,10550
|
||||
sqlalchemy/ext/mypy/decl_class.py,sha256=1vVJRII2apnLTUbc5HkJS6Z2GueaUv_eKvhbqh7Wik4,17384
|
||||
sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369
|
||||
sqlalchemy/ext/mypy/names.py,sha256=Q3ef8XQBgVm9WUwlItqlYCXDNi_kbV5DdLEgbtEMEI8,10479
|
||||
sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750
|
||||
sqlalchemy/ext/mypy/util.py,sha256=DKRaurkXHI2lAMAAcEO5GLXbX_m2Xqy7l_juh8Byf5U,9960
|
||||
sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384
|
||||
sqlalchemy/ext/serializer.py,sha256=D0g4jMZkRk0Gjr0L-FZe81SR63h0Zs-9JzuWtT_SD7k,6140
|
||||
sqlalchemy/future/__init__.py,sha256=q2mw-gxk_xoxJLEvRoyMha3vO1xSRHrslcExOHZwmPA,512
|
||||
sqlalchemy/future/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/future/__pycache__/engine.cpython-312.pyc,,
|
||||
sqlalchemy/future/engine.py,sha256=AgIw6vMsef8W6tynOTkxsjd6o_OQDwGjLdbpoMD8ue8,495
|
||||
sqlalchemy/inspection.py,sha256=MF-LE358wZDUEl1IH8-Uwt2HI65EsQpQW5o5udHkZwA,5063
|
||||
sqlalchemy/log.py,sha256=8x9UR3nj0uFm6or6bQF-JWb4fYv2zOeQjG_w-0wOJFA,8607
|
||||
sqlalchemy/orm/__init__.py,sha256=ZYys5nL3RFUDCMOLFDBrRI52F6er3S1U1OY9TeORuKs,8463
|
||||
sqlalchemy/orm/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_orm_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/attributes.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/bulk_persistence.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/clsregistry.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/collections.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/context.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_api.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/decl_base.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dependency.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/descriptor_props.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/dynamic.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/evaluator.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/exc.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/identity.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/instrumentation.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/interfaces.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/loading.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapped_collection.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/mapper.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/path_registry.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/persistence.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/properties.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/query.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/relationships.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/scoping.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/session.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/state_changes.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategies.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/strategy_options.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/sync.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/unitofwork.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/orm/__pycache__/writeonly.cpython-312.pyc,,
|
||||
sqlalchemy/orm/_orm_constructors.py,sha256=8EQfYsDL2k_ev0eK-wxMl3algouczN38Gu43CrRlAlo,103434
|
||||
sqlalchemy/orm/_typing.py,sha256=DVBfpHmDVK4x1zxaGJPY2GoTrAsyR6uexv20Lzf1afc,4973
|
||||
sqlalchemy/orm/attributes.py,sha256=lorOHBJvJJYndOuafWJhHBbQ1pR6FAyimhqz-mErBRQ,92534
|
||||
sqlalchemy/orm/base.py,sha256=FXkYTSCDUJFQSB5pcyPt2wG-dRctf5P6ySjyjVxQsX0,27502
|
||||
sqlalchemy/orm/bulk_persistence.py,sha256=1FC23bRJKjpfbp2D5hYuV1qOVIKGSswu9XPXbbSJ5Mo,72663
|
||||
sqlalchemy/orm/clsregistry.py,sha256=IjoDZwWpjG42ji59L4M1EZvjBEoXPZykzENDtKWxU8A,17974
|
||||
sqlalchemy/orm/collections.py,sha256=WEKuUCRgLhDhJEIBhZ21UrE0pBOyRm2zxD20GvbgA9g,52243
|
||||
sqlalchemy/orm/context.py,sha256=FMPyw07OA9OXWQ32RQx52AEa2xTLSkqdYgx9R_yN1x0,112955
|
||||
sqlalchemy/orm/decl_api.py,sha256=_WPKQ_vSE5k2TLtNmkaxxYmvbhZvkRMrrvCeDxdqDQE,63998
|
||||
sqlalchemy/orm/decl_base.py,sha256=8R7go5sULTYNRlhYiEjXIJkQ34oPp7DY_fC2nS5D5is,83343
|
||||
sqlalchemy/orm/dependency.py,sha256=hgjksUWhgbmgHK5GdJdiDCBgDAIGQXIrY-Tj79tbL2k,47631
|
||||
sqlalchemy/orm/descriptor_props.py,sha256=dR_h4Gvdtpcdp4sj_ZOR4P5Nng2J2vhsvFHouRLlntc,37244
|
||||
sqlalchemy/orm/dynamic.py,sha256=rWAZ-nfAkREuNjt8e_FRdqYrvHDdbODn1CcfyP8Y18k,9816
|
||||
sqlalchemy/orm/evaluator.py,sha256=tRETz4dNZ71VsEA8nG0hpefByB-W0zBt02IxcSR5H2g,12353
|
||||
sqlalchemy/orm/events.py,sha256=1PiGT7JMUWTDAb3X1T79P02BMVDmcWEpatz1FwpLqoA,127777
|
||||
sqlalchemy/orm/exc.py,sha256=IP40P-wOeXhkYk0YizuTC3wqm6W9cPTaQU08f5MMaQ0,7413
|
||||
sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249
|
||||
sqlalchemy/orm/instrumentation.py,sha256=M-kZmkUvHUxtf-0mCA8RIM5QmMH1hWlYR_pKMwaidjA,24321
|
||||
sqlalchemy/orm/interfaces.py,sha256=7Lni4Cue41b1CsmN4VbeUyWwzuNMcKtkrpihc9U-WIw,48690
|
||||
sqlalchemy/orm/loading.py,sha256=9RacpzFOWbuKgPRWHFmyIvD4fYCLAnkpwBFASyQ2CoI,58277
|
||||
sqlalchemy/orm/mapped_collection.py,sha256=zK3d3iozORzDruBUrAmkVC0RR3Orj5szk-TSQ24xzIU,19682
|
||||
sqlalchemy/orm/mapper.py,sha256=W-srpoEc3UIYv_6qTXTd_dG_TVeQcToG77VGrXt85PM,171738
|
||||
sqlalchemy/orm/path_registry.py,sha256=sJZMv_WPqUpHfQtKWaX3WYFeKBcNJ8C3wOM2mkBGkTE,25920
|
||||
sqlalchemy/orm/persistence.py,sha256=dzyB2JOXNwQgaCbN8kh0sEz00WFePr48qf8NWVCUZH8,61701
|
||||
sqlalchemy/orm/properties.py,sha256=eDPFzxYUgdM3uWjHywnb1XW-i0tVKKyx7A2MCD31GQU,29306
|
||||
sqlalchemy/orm/query.py,sha256=Cf0e94-u1XyoXJoOAmr4iFvtCwNY98kxUYyMPenaWTE,117708
|
||||
sqlalchemy/orm/relationships.py,sha256=dS5SY0v1MiD7iCNnAQlHaI6prUQhL5EkXT7ijc8FR8E,128644
|
||||
sqlalchemy/orm/scoping.py,sha256=rJVc7_Lic4V00HZ-UvYFWkVpXqdrMayRmIs4fIwH1UA,78688
|
||||
sqlalchemy/orm/session.py,sha256=CZJTQ-wPwIy0c3AMFxgJnBgaft6eEf4JzcCLcaaCSjg,195979
|
||||
sqlalchemy/orm/state.py,sha256=327-F4TG29s6mLC8oWRiO2PuvYIUZzY1MqUPjtUy7M4,37670
|
||||
sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815
|
||||
sqlalchemy/orm/strategies.py,sha256=-tsBRsmEqkaxAAIn4t2F-U5SrRIPoPCyzpqFYGTAwNs,119866
|
||||
sqlalchemy/orm/strategy_options.py,sha256=oeDl_rMDNAC_90N7ytsni-psXWAeQMhABQFyKBSmai0,85353
|
||||
sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779
|
||||
sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033
|
||||
sqlalchemy/orm/util.py,sha256=5SC4MOVU0cPObexDjpMvXvetueiU5pze42raL94gj24,81021
|
||||
sqlalchemy/orm/writeonly.py,sha256=SYu2sAaHZONk2pW4PmtE871LG-O0P_bjidvKzY1H_zI,22305
|
||||
sqlalchemy/pool/__init__.py,sha256=qiDdq4r4FFAoDrK6ncugF_i6usi_X1LeJt-CuBHey0s,1804
|
||||
sqlalchemy/pool/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/pool/__pycache__/impl.cpython-312.pyc,,
|
||||
sqlalchemy/pool/base.py,sha256=WF4az4ZKuzQGuKeSJeyexaYjmWZUvYdC6KIi8zTGodw,52236
|
||||
sqlalchemy/pool/events.py,sha256=xGjkIUZl490ZDtCHqnQF9ZCwe2Jv93eGXmnQxftB11E,13147
|
||||
sqlalchemy/pool/impl.py,sha256=JwpALSkH-pCoO_6oENbkHYY00Jx9nlttyoI61LivRNc,18944
|
||||
sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
sqlalchemy/schema.py,sha256=dKiWmgHYjcKQ4TiiD6vD0UMmIsD8u0Fsor1M9AAeGUs,3194
|
||||
sqlalchemy/sql/__init__.py,sha256=UNa9EUiYWoPayf-FzNcwVgQvpsBdInPZfpJesAStN9o,5820
|
||||
sqlalchemy/sql/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_dml_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_elements_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_orm_types.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_py_util.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_selectable_constructors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/_typing.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/annotation.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/cache_key.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/coercions.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/compiler.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/crud.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/ddl.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/default_comparator.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/dml.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/elements.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/events.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/expression.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/functions.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/lambdas.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/naming.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/operators.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/roles.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/selectable.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/sqltypes.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/traversals.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/type_api.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/sql/__pycache__/visitors.cpython-312.pyc,,
|
||||
sqlalchemy/sql/_dml_constructors.py,sha256=YdBJex0MCVACv4q2nl_ii3uhxzwU6aDB8zAsratX5UQ,3867
|
||||
sqlalchemy/sql/_elements_constructors.py,sha256=833Flez92odZkE2Vy6SXK8LcoO1AwkfVzOnATJLWFsA,63168
|
||||
sqlalchemy/sql/_orm_types.py,sha256=T-vjcry4C1y0GToFKVxQCnmly_-Zsq4IO4SHN6bvUF4,625
|
||||
sqlalchemy/sql/_py_util.py,sha256=hiM9ePbRSGs60bAMxPFuJCIC_p9SQ1VzqXGiPchiYwE,2173
|
||||
sqlalchemy/sql/_selectable_constructors.py,sha256=wjE6HrLm9cR7bxvZXT8sFLUqT6t_J9G1XyQCnYmBDl0,18780
|
||||
sqlalchemy/sql/_typing.py,sha256=oqwrYHVMtK-AuKGH9c4SgfiOEJUt5vjkzSEzzscMHkM,12771
|
||||
sqlalchemy/sql/annotation.py,sha256=aqbbVz9kfbCT3_66CZ9GEirVN197Cukoqt8rq48FgkQ,18245
|
||||
sqlalchemy/sql/base.py,sha256=M1b-Tg49ikUW2mnZv0aI38oASG6dgeo4jBNWDgJgAg8,73925
|
||||
sqlalchemy/sql/cache_key.py,sha256=0Db8mR8IrpBgdzXs4TGTt98LOpL3c7KABd72MAPKUQQ,33668
|
||||
sqlalchemy/sql/coercions.py,sha256=hAEou9Ycyswzu8yz_Q7QkwL2_c3nctzBJQS2oDEr4iE,40664
|
||||
sqlalchemy/sql/compiler.py,sha256=hrTptbOKIgVIHapywj4Lk5OMwpXvHS-KGg3odFwlo-I,274687
|
||||
sqlalchemy/sql/crud.py,sha256=HBX4QPtW_PYYJmIKfNr-wE8IdEr963N24WXzFBUZOo0,56514
|
||||
sqlalchemy/sql/ddl.py,sha256=lKqvOigbcYrDG0euxd5F4tu9HbBi1kmp3eFPc45HH-8,45636
|
||||
sqlalchemy/sql/default_comparator.py,sha256=utXWsZVGEjflhFfCT4ywa6RnhORc1Rryo87Hga71Rps,16707
|
||||
sqlalchemy/sql/dml.py,sha256=pn0Lm1ofC5qVZzwGWFW73lPCiNba8OsTeemurJgwRyg,65614
|
||||
sqlalchemy/sql/elements.py,sha256=YfccXzQc9DlgF8q15kDf-zKBUY_vpIe0FGaVDBPoic4,176544
|
||||
sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290
|
||||
sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586
|
||||
sqlalchemy/sql/functions.py,sha256=kMMYplvuIHFAPwxBI03SizwaLcYEHzysecWk-R1V-JM,63762
|
||||
sqlalchemy/sql/lambdas.py,sha256=DP0Qz7Ypo8QhzMwygGHYgRhwJMx-rNezO1euouH3iYU,49292
|
||||
sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858
|
||||
sqlalchemy/sql/operators.py,sha256=himArRqBzrljob3Zfhi_ZS-Jleg1u6YFp0g3d7Co6IM,76106
|
||||
sqlalchemy/sql/roles.py,sha256=pOsVn_OZD7mF2gJByHf24Rjopt0_Hu3dUCEOK5t4KS8,7662
|
||||
sqlalchemy/sql/schema.py,sha256=iFleWHkxi-3mKGiK_N1TzUqxnNwOpypB4bWDuAVQe8c,229717
|
||||
sqlalchemy/sql/selectable.py,sha256=cgyV0AsPy4CXAFdhMiTCkbgaHiFilW9sclzxlHJKH3o,236460
|
||||
sqlalchemy/sql/sqltypes.py,sha256=5_N9MhprQFWYc3yjcXgFC_DmvkQU-Jz-Ok9nIMYp2Q4,127469
|
||||
sqlalchemy/sql/traversals.py,sha256=3ScTC1fh1-y8Y478h_2Azmd2xdQdWPWkDve4YgrwMf8,33664
|
||||
sqlalchemy/sql/type_api.py,sha256=SN16_oNZG6G65cvG6ABPcptz_YV5vfB2fknwJZxrkOs,84464
|
||||
sqlalchemy/sql/util.py,sha256=qGHQF-tPCj-m1FBerzT7weCanGcXU7dK5m-W7NHio-4,48077
|
||||
sqlalchemy/sql/visitors.py,sha256=71wdVvhhZL4nJvVwFAs6ssaW-qZgNRSmKjpAcOzF_TA,36317
|
||||
sqlalchemy/testing/__init__.py,sha256=zgitAYzsCWT_U48ZiifXHHLJFo8nZBYmI-5TueA4_lE,3160
|
||||
sqlalchemy/testing/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertions.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/assertsql.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/asyncio.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/config.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/engines.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/entities.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/exclusions.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/pickleable.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/profiling.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/provision.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/requirements.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/schema.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/util.cpython-312.pyc,,
|
||||
sqlalchemy/testing/__pycache__/warnings.cpython-312.pyc,,
|
||||
sqlalchemy/testing/assertions.py,sha256=gL0rA7CCZJbcVgvWOPV91tTZTRwQc1_Ta0-ykBn83Ew,31439
|
||||
sqlalchemy/testing/assertsql.py,sha256=IgQG7l94WaiRP8nTbilJh1ZHZl125g7GPq-S5kmQZN0,16817
|
||||
sqlalchemy/testing/asyncio.py,sha256=kM8uuOqDBagZF0r9xvGmsiirUVLUQ_KBzjUFU67W-b8,3830
|
||||
sqlalchemy/testing/config.py,sha256=AqyH1qub_gDqX0BvlL-JBQe7N-t2wo8655FtwblUNOY,12090
|
||||
sqlalchemy/testing/engines.py,sha256=HFJceEBD3Q_TTFQMTtIV5wGWO_a7oUgoKtUF_z636SM,13481
|
||||
sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354
|
||||
sqlalchemy/testing/exclusions.py,sha256=T8B01hmm8WVs-EKcUOQRzabahPqblWJfOidi6bHJ6GA,12460
|
||||
sqlalchemy/testing/fixtures/__init__.py,sha256=dMClrIoxqlYIFpk2ia4RZpkbfxsS_3EBigr9QsPJ66g,1198
|
||||
sqlalchemy/testing/fixtures/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/base.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/mypy.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/orm.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/__pycache__/sql.cpython-312.pyc,,
|
||||
sqlalchemy/testing/fixtures/base.py,sha256=9r_J2ksiTzClpUxW0TczICHrWR7Ny8PV8IsBz6TsGFI,12256
|
||||
sqlalchemy/testing/fixtures/mypy.py,sha256=gdxiwNFIzDlNGSOdvM3gbwDceVCC9t8oM5kKbwyhGBk,11973
|
||||
sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095
|
||||
sqlalchemy/testing/fixtures/sql.py,sha256=KZMjco9_3dsuspmkew5Ejp88Wlr9PsSBB1qeJGFxQAk,15900
|
||||
sqlalchemy/testing/pickleable.py,sha256=U9mIqk-zaxq9Xfy7HErP7UrKgTov-A3QFnhZh-NiOjI,2833
|
||||
sqlalchemy/testing/plugin/__init__.py,sha256=79F--BIY_NTBzVRIlJGgAY5LNJJ3cD19XvrAo4X0W9A,247
|
||||
sqlalchemy/testing/plugin/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-312.pyc,,
|
||||
sqlalchemy/testing/plugin/bootstrap.py,sha256=oYScMbEW4pCnWlPEAq1insFruCXFQeEVBwo__i4McpU,1685
|
||||
sqlalchemy/testing/plugin/plugin_base.py,sha256=BgNzWNEmgpK4CwhyblQQKnH-7FDKVi_Uul5vw8fFjBU,21578
|
||||
sqlalchemy/testing/plugin/pytestplugin.py,sha256=6jkQHH2VQMD75k2As9CuWXmEy9jrscoFRhCNg6-PaTw,27656
|
||||
sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148
|
||||
sqlalchemy/testing/provision.py,sha256=3qFor_sN1FFlS7odUGkKqLUxGmQZC9XM67I9vQ_zeXo,14626
|
||||
sqlalchemy/testing/requirements.py,sha256=Z__o-1Rj9B7dI8E_l3qsKTvsg0rK198vB0A1p7A5dcM,52832
|
||||
sqlalchemy/testing/schema.py,sha256=lr4GkGrGwagaHMuSGzWdzkMaj3HnS7dgfLLWfxt__-U,6513
|
||||
sqlalchemy/testing/suite/__init__.py,sha256=Y5DRNG0Yl1u3ypt9zVF0Z9suPZeuO_UQGLl-wRgvTjU,722
|
||||
sqlalchemy/testing/suite/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_cte.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_insert.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_results.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_select.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_types.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-312.pyc,,
|
||||
sqlalchemy/testing/suite/test_cte.py,sha256=6zBC3W2OwX1Xs-HedzchcKN2S7EaLNkgkvV_JSZ_Pq0,6451
|
||||
sqlalchemy/testing/suite/test_ddl.py,sha256=1Npkf0C_4UNxphthAGjG078n0vPEgnSIHpDu5MfokxQ,12031
|
||||
sqlalchemy/testing/suite/test_deprecations.py,sha256=BcJxZTcjYqeOAENVElCg3hVvU6fkGEW3KGBMfnW8bng,5337
|
||||
sqlalchemy/testing/suite/test_dialect.py,sha256=EH4ZQWbnGdtjmx5amZtTyhYmrkXJCvW1SQoLahoE7uk,22923
|
||||
sqlalchemy/testing/suite/test_insert.py,sha256=9azifj6-OCD7s8h_tAO1uPw100ibQv8YoKc_VA3hn3c,18824
|
||||
sqlalchemy/testing/suite/test_reflection.py,sha256=7sML8-owubSQeEM7Ve6LbnB8uIVlNV00WWepKwII2a8,109648
|
||||
sqlalchemy/testing/suite/test_results.py,sha256=X720GafdA4p75SOGS93j-dXkt6QDEnnJbU2bh18VCcg,16914
|
||||
sqlalchemy/testing/suite/test_rowcount.py,sha256=3KDTlRgjpQ1OVfp__1cv8Hvq4CsDKzmrhJQ_WIJWoJg,7900
|
||||
sqlalchemy/testing/suite/test_select.py,sha256=ulRZQJlzkwwcewEyisuBEXVWFR0Wshz9MEDxYYiYLwQ,61732
|
||||
sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923
|
||||
sqlalchemy/testing/suite/test_types.py,sha256=K4MGHvnTtgqeksoQOBCZRVQYC7HoYO6Z6rVt5vj2t9o,67805
|
||||
sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141
|
||||
sqlalchemy/testing/suite/test_update_delete.py,sha256=yTiM2unnfOK9rK8ZkqeTTU_MkT-RsKFLmdYliniZfAY,3994
|
||||
sqlalchemy/testing/util.py,sha256=qldXKw8gRJ4I2x3uXsBssYMqwatmcMFMTOveRQCmfDU,14469
|
||||
sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546
|
||||
sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168
|
||||
sqlalchemy/util/__init__.py,sha256=5D5Mquvx3SOmud0QErKzzGvBTkqMdhrrd_sXijOILeo,8312
|
||||
sqlalchemy/util/__pycache__/__init__.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_collections.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_has_cy.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/_py_collections.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/compat.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/concurrency.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/deprecations.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/langhelpers.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/preloaded.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/queue.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/tool_support.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/topological.cpython-312.pyc,,
|
||||
sqlalchemy/util/__pycache__/typing.cpython-312.pyc,,
|
||||
sqlalchemy/util/_collections.py,sha256=aZoSAVOXnHBoYEsxDOi0O9odg9wqLbGb7PGjaWQKiyY,20078
|
||||
sqlalchemy/util/_concurrency_py3k.py,sha256=zb0Bow2Y_QjTdaACEviBEEaFvqDuVvpJfmwCjaw8xNE,9170
|
||||
sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247
|
||||
sqlalchemy/util/_py_collections.py,sha256=U6L5AoyLdgSv7cdqB4xxQbw1rpeJjyOZVXffgxgga8I,16714
|
||||
sqlalchemy/util/compat.py,sha256=cnucBQOKspo58vjRpQXUBrHGguHOSFvftpD-I8vfUy0,8760
|
||||
sqlalchemy/util/concurrency.py,sha256=9lT_cMoO1fZNdY8QTUZ22oeSf-L5I-79Ke7chcBNPA0,3304
|
||||
sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971
|
||||
sqlalchemy/util/langhelpers.py,sha256=uIK3szZuq9aMnO-vEpSlNekNWv4I-E391e56bkTnUm0,65090
|
||||
sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904
|
||||
sqlalchemy/util/queue.py,sha256=CaeSEaYZ57YwtmLdNdOIjT5PK_LCuwMFiO0mpp39ybM,10185
|
||||
sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135
|
||||
sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458
|
||||
sqlalchemy/util/typing.py,sha256=lFcGo1dJbZIZ9drAnvef-PzP0cX4LMxMSwgk3lJBb0g,18182
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (75.1.0)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-manylinux_2_17_x86_64
|
||||
Tag: cp312-cp312-manylinux2014_x86_64
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
sqlalchemy
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
# This is a stub package designed to roughly emulate the _yaml
|
||||
# extension module, which previously existed as a standalone module
|
||||
# and has been moved into the `yaml` package namespace.
|
||||
# It does not perfectly mimic its old counterpart, but should get
|
||||
# close enough for anyone who's relying on it even when they shouldn't.
|
||||
import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
raise exc("No module named '_yaml'")
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,139 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: alembic
|
||||
Version: 1.18.4
|
||||
Summary: A database migration tool for SQLAlchemy.
|
||||
Author-email: Mike Bayer <mike_mp@zzzcomputing.com>
|
||||
License-Expression: MIT
|
||||
Project-URL: Homepage, https://alembic.sqlalchemy.org
|
||||
Project-URL: Documentation, https://alembic.sqlalchemy.org/en/latest/
|
||||
Project-URL: Changelog, https://alembic.sqlalchemy.org/en/latest/changelog.html
|
||||
Project-URL: Source, https://github.com/sqlalchemy/alembic/
|
||||
Project-URL: Issue Tracker, https://github.com/sqlalchemy/alembic/issues/
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Operating System :: OS Independent
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Database :: Front-Ends
|
||||
Requires-Python: >=3.10
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: SQLAlchemy>=1.4.23
|
||||
Requires-Dist: Mako
|
||||
Requires-Dist: typing-extensions>=4.12
|
||||
Requires-Dist: tomli; python_version < "3.11"
|
||||
Provides-Extra: tz
|
||||
Requires-Dist: tzdata; extra == "tz"
|
||||
Dynamic: license-file
|
||||
|
||||
Alembic is a database migrations tool written by the author
|
||||
of `SQLAlchemy <http://www.sqlalchemy.org>`_. A migrations tool
|
||||
offers the following functionality:
|
||||
|
||||
* Can emit ALTER statements to a database in order to change
|
||||
the structure of tables and other constructs
|
||||
* Provides a system whereby "migration scripts" may be constructed;
|
||||
each script indicates a particular series of steps that can "upgrade" a
|
||||
target database to a new version, and optionally a series of steps that can
|
||||
"downgrade" similarly, doing the same steps in reverse.
|
||||
* Allows the scripts to execute in some sequential manner.
|
||||
|
||||
The goals of Alembic are:
|
||||
|
||||
* Very open ended and transparent configuration and operation. A new
|
||||
Alembic environment is generated from a set of templates which is selected
|
||||
among a set of options when setup first occurs. The templates then deposit a
|
||||
series of scripts that define fully how database connectivity is established
|
||||
and how migration scripts are invoked; the migration scripts themselves are
|
||||
generated from a template within that series of scripts. The scripts can
|
||||
then be further customized to define exactly how databases will be
|
||||
interacted with and what structure new migration files should take.
|
||||
* Full support for transactional DDL. The default scripts ensure that all
|
||||
migrations occur within a transaction - for those databases which support
|
||||
this (Postgresql, Microsoft SQL Server), migrations can be tested with no
|
||||
need to manually undo changes upon failure.
|
||||
* Minimalist script construction. Basic operations like renaming
|
||||
tables/columns, adding/removing columns, changing column attributes can be
|
||||
performed through one line commands like alter_column(), rename_table(),
|
||||
add_constraint(). There is no need to recreate full SQLAlchemy Table
|
||||
structures for simple operations like these - the functions themselves
|
||||
generate minimalist schema structures behind the scenes to achieve the given
|
||||
DDL sequence.
|
||||
* "auto generation" of migrations. While real world migrations are far more
|
||||
complex than what can be automatically determined, Alembic can still
|
||||
eliminate the initial grunt work in generating new migration directives
|
||||
from an altered schema. The ``--autogenerate`` feature will inspect the
|
||||
current status of a database using SQLAlchemy's schema inspection
|
||||
capabilities, compare it to the current state of the database model as
|
||||
specified in Python, and generate a series of "candidate" migrations,
|
||||
rendering them into a new migration script as Python directives. The
|
||||
developer then edits the new file, adding additional directives and data
|
||||
migrations as needed, to produce a finished migration. Table and column
|
||||
level changes can be detected, with constraints and indexes to follow as
|
||||
well.
|
||||
* Full support for migrations generated as SQL scripts. Those of us who
|
||||
work in corporate environments know that direct access to DDL commands on a
|
||||
production database is a rare privilege, and DBAs want textual SQL scripts.
|
||||
Alembic's usage model and commands are oriented towards being able to run a
|
||||
series of migrations into a textual output file as easily as it runs them
|
||||
directly to a database. Care must be taken in this mode to not invoke other
|
||||
operations that rely upon in-memory SELECTs of rows - Alembic tries to
|
||||
provide helper constructs like bulk_insert() to help with data-oriented
|
||||
operations that are compatible with script-based DDL.
|
||||
* Non-linear, dependency-graph versioning. Scripts are given UUID
|
||||
identifiers similarly to a DVCS, and the linkage of one script to the next
|
||||
is achieved via human-editable markers within the scripts themselves.
|
||||
The structure of a set of migration files is considered as a
|
||||
directed-acyclic graph, meaning any migration file can be dependent
|
||||
on any other arbitrary set of migration files, or none at
|
||||
all. Through this open-ended system, migration files can be organized
|
||||
into branches, multiple roots, and mergepoints, without restriction.
|
||||
Commands are provided to produce new branches, roots, and merges of
|
||||
branches automatically.
|
||||
* Provide a library of ALTER constructs that can be used by any SQLAlchemy
|
||||
application. The DDL constructs build upon SQLAlchemy's own DDLElement base
|
||||
and can be used standalone by any application or script.
|
||||
* At long last, bring SQLite and its inability to ALTER things into the fold,
|
||||
but in such a way that SQLite's very special workflow needs are accommodated
|
||||
in an explicit way that makes the most of a bad situation, through the
|
||||
concept of a "batch" migration, where multiple changes to a table can
|
||||
be batched together to form a series of instructions for a single, subsequent
|
||||
"move-and-copy" workflow. You can even use "move-and-copy" workflow for
|
||||
other databases, if you want to recreate a table in the background
|
||||
on a busy system.
|
||||
|
||||
Documentation and status of Alembic is at https://alembic.sqlalchemy.org/
|
||||
|
||||
The SQLAlchemy Project
|
||||
======================
|
||||
|
||||
Alembic is part of the `SQLAlchemy Project <https://www.sqlalchemy.org>`_ and
|
||||
adheres to the same standards and conventions as the core project.
|
||||
|
||||
Development / Bug reporting / Pull requests
|
||||
___________________________________________
|
||||
|
||||
Please refer to the
|
||||
`SQLAlchemy Community Guide <https://www.sqlalchemy.org/develop.html>`_ for
|
||||
guidelines on coding and participating in this project.
|
||||
|
||||
Code of Conduct
|
||||
_______________
|
||||
|
||||
Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
|
||||
constructive communication between users and developers.
|
||||
Please see our current Code of Conduct at
|
||||
`Code of Conduct <https://www.sqlalchemy.org/codeofconduct.html>`_.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Alembic is distributed under the `MIT license
|
||||
<https://opensource.org/licenses/MIT>`_.
|
||||
@@ -0,0 +1,179 @@
|
||||
../../../bin/alembic,sha256=5SKrl4YQZ1pUXtp-qoGIz4Zb0oTIMGmajQ-ULm2AEzM,193
|
||||
alembic-1.18.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
alembic-1.18.4.dist-info/METADATA,sha256=sPH3Zq5eEaNtbnI1os9Rvk7eBbFJSMPq13poNNaxvfs,7217
|
||||
alembic-1.18.4.dist-info/RECORD,,
|
||||
alembic-1.18.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic-1.18.4.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
||||
alembic-1.18.4.dist-info/entry_points.txt,sha256=aykM30soxwGN0pB7etLc1q0cHJbL9dy46RnK9VX4LLw,48
|
||||
alembic-1.18.4.dist-info/licenses/LICENSE,sha256=bmjZSgOg4-Mn3fPobR6-3BTuzjkiAiYY_CRqNilv0Mw,1059
|
||||
alembic-1.18.4.dist-info/top_level.txt,sha256=FwKWd5VsPFC8iQjpu1u9Cn-JnK3-V1RhUCmWqz1cl-s,8
|
||||
alembic/__init__.py,sha256=6ppwNUS6dfdFIm5uwZaaZ9lDZ7pIwkTNyQcbjY47V3I,93
|
||||
alembic/__main__.py,sha256=373m7-TBh72JqrSMYviGrxCHZo-cnweM8AGF8A22PmY,78
|
||||
alembic/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/__pycache__/__main__.cpython-312.pyc,,
|
||||
alembic/__pycache__/command.cpython-312.pyc,,
|
||||
alembic/__pycache__/config.cpython-312.pyc,,
|
||||
alembic/__pycache__/context.cpython-312.pyc,,
|
||||
alembic/__pycache__/environment.cpython-312.pyc,,
|
||||
alembic/__pycache__/migration.cpython-312.pyc,,
|
||||
alembic/__pycache__/op.cpython-312.pyc,,
|
||||
alembic/autogenerate/__init__.py,sha256=ntmUTXhjLm4_zmqIwyVaECdpPDn6_u1yM9vYk6-553E,543
|
||||
alembic/autogenerate/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/autogenerate/__pycache__/api.cpython-312.pyc,,
|
||||
alembic/autogenerate/__pycache__/render.cpython-312.pyc,,
|
||||
alembic/autogenerate/__pycache__/rewriter.cpython-312.pyc,,
|
||||
alembic/autogenerate/api.py,sha256=8tVNDSHlqsBgj1IVLdqvZr_jlvz9kp3O5EKIL9biaZg,22781
|
||||
alembic/autogenerate/compare/__init__.py,sha256=kCvA0ZK0rTahNv9wlgyIB5DH2lFEhTRO4PFmoqcL9JE,1809
|
||||
alembic/autogenerate/compare/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/comments.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/constraints.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/schema.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/server_defaults.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/tables.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/types.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/__pycache__/util.cpython-312.pyc,,
|
||||
alembic/autogenerate/compare/comments.py,sha256=agSrWsZhJ47i-E-EqiP3id2CXTTbP0muOKk1-9in9lg,3234
|
||||
alembic/autogenerate/compare/constraints.py,sha256=7sLSvUK9M2CbMRRQy5pveIXbjDLRDnfPx0Dvi_KXOf8,27906
|
||||
alembic/autogenerate/compare/schema.py,sha256=plQ7JJ1zJGlnajweSV8lAD9tDYPks5G40sliocTuXJA,1695
|
||||
alembic/autogenerate/compare/server_defaults.py,sha256=D--5EvEfyX0fSVkK6iLtRoer5sYK6xeNC2TIdu7klUk,10792
|
||||
alembic/autogenerate/compare/tables.py,sha256=47pAgVhbmXGLrm3dMK6hrNABxOAe_cGSQmPtCBwORVc,10611
|
||||
alembic/autogenerate/compare/types.py,sha256=75bOduz-dOiyLI065XD5sEP_JF9GPLkDAQ_y5B8lXF0,4005
|
||||
alembic/autogenerate/compare/util.py,sha256=K_GArJ2xQXZi6ftb8gkgZuIdVqvyep3E2ZXq8F3-jIU,9521
|
||||
alembic/autogenerate/render.py,sha256=ceQL8nk8m2kBtQq5gtxtDLR9iR0Sck8xG_61Oez-Sqs,37270
|
||||
alembic/autogenerate/rewriter.py,sha256=NIASSS-KaNKPmbm1k4pE45aawwjSh1Acf6eZrOwnUGM,7814
|
||||
alembic/command.py,sha256=7RzAwwXR31sOl0oVItyZl9B0j3TeR5dRyx9634lVsLM,25297
|
||||
alembic/config.py,sha256=VoCZV2cFZoF0Xa1OxHqsA-MKzuwBRaJSC7hxZ3-uWN4,34983
|
||||
alembic/context.py,sha256=hK1AJOQXJ29Bhn276GYcosxeG7pC5aZRT5E8c4bMJ4Q,195
|
||||
alembic/context.pyi,sha256=b_naI_W8dyiZRsL_n299a-LbqLZxKTAgDIXubRLVKlY,32531
|
||||
alembic/ddl/__init__.py,sha256=Df8fy4Vn_abP8B7q3x8gyFwEwnLw6hs2Ljt_bV3EZWE,152
|
||||
alembic/ddl/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/_autogen.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/base.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/impl.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/mssql.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/mysql.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/oracle.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/postgresql.cpython-312.pyc,,
|
||||
alembic/ddl/__pycache__/sqlite.cpython-312.pyc,,
|
||||
alembic/ddl/_autogen.py,sha256=Blv2RrHNyF4cE6znCQXNXG5T9aO-YmiwD4Fz-qfoaWA,9275
|
||||
alembic/ddl/base.py,sha256=dNhLIZnFMP7Cr8rE_e2Zb5skGgCMBOdca1PajXqZYhs,11977
|
||||
alembic/ddl/impl.py,sha256=IU3yHFVI3v0QHEwNL_LSN1PRpPF0n09NFFqRZkW86wE,31376
|
||||
alembic/ddl/mssql.py,sha256=dee0acwnxmTZXuYPqqlYaDiSbKS46zVH0WRULjX5Blg,17398
|
||||
alembic/ddl/mysql.py,sha256=2fvzGcdg4qqCJogGnzvQN636vUi9mF6IoQWLGevvF_A,18456
|
||||
alembic/ddl/oracle.py,sha256=669YlkcZihlXFbnXhH2krdrvDry8q5pcUGfoqkg_R6Y,6243
|
||||
alembic/ddl/postgresql.py,sha256=04M4OpZOCJJ3ipuHoVwlR1gI1sgRwOguRRVx_mFg8Uc,30417
|
||||
alembic/ddl/sqlite.py,sha256=TmzU3YaR3aw_0spSrA6kcUY8fyDfwsu4GkH5deYPEK8,8017
|
||||
alembic/environment.py,sha256=MM5lPayGT04H3aeng1H7GQ8HEAs3VGX5yy6mDLCPLT4,43
|
||||
alembic/migration.py,sha256=MV6Fju6rZtn2fTREKzXrCZM6aIBGII4OMZFix0X-GLs,41
|
||||
alembic/op.py,sha256=flHtcsVqOD-ZgZKK2pv-CJ5Cwh-KJ7puMUNXzishxLw,167
|
||||
alembic/op.pyi,sha256=ABBlNk4Eg7DR17knSKIjmvHQBNAmKh3aHQNHU8Oyw08,53347
|
||||
alembic/operations/__init__.py,sha256=e0KQSZAgLpTWvyvreB7DWg7RJV_MWSOPVDgCqsd2FzY,318
|
||||
alembic/operations/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/operations/__pycache__/base.cpython-312.pyc,,
|
||||
alembic/operations/__pycache__/batch.cpython-312.pyc,,
|
||||
alembic/operations/__pycache__/ops.cpython-312.pyc,,
|
||||
alembic/operations/__pycache__/schemaobj.cpython-312.pyc,,
|
||||
alembic/operations/__pycache__/toimpl.cpython-312.pyc,,
|
||||
alembic/operations/base.py,sha256=ubpv1HDol0g0nuLi0b8-uN7-HEVRZ6mq8arvK9EGo0g,78432
|
||||
alembic/operations/batch.py,sha256=hYOpzG2FK_8hk-rHNuLuFAA3-VXRSOnsTrpz2YlA61Q,26947
|
||||
alembic/operations/ops.py,sha256=ofbHkReZkZX2n9lXDaIPlrKe2U1mwgQpZNhEbuC4QrM,99325
|
||||
alembic/operations/schemaobj.py,sha256=Wp-bBe4a8lXPTvIHJttBY0ejtpVR5Jvtb2kI-U2PztQ,9468
|
||||
alembic/operations/toimpl.py,sha256=f8rH3jdob9XvEJr6CoWEkX6X1zgNB5qxdcEQugyhBvU,8466
|
||||
alembic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic/runtime/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/runtime/__pycache__/environment.cpython-312.pyc,,
|
||||
alembic/runtime/__pycache__/migration.cpython-312.pyc,,
|
||||
alembic/runtime/__pycache__/plugins.cpython-312.pyc,,
|
||||
alembic/runtime/environment.py,sha256=1cR1v18sIKvOPZMlc4fHGU4J8r6Dec9h4o3WXkMmFKQ,42400
|
||||
alembic/runtime/migration.py,sha256=mR2Ee1h9Yy6OMFeDL4LOYorLYby2l2f899WGK_boECw,48427
|
||||
alembic/runtime/plugins.py,sha256=pWCDhMX8MvR8scXhiGSRNYNW7-ckEbOW2qK58xRFy1Q,5707
|
||||
alembic/script/__init__.py,sha256=lSj06O391Iy5avWAiq8SPs6N8RBgxkSPjP8wpXcNDGg,100
|
||||
alembic/script/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/script/__pycache__/base.cpython-312.pyc,,
|
||||
alembic/script/__pycache__/revision.cpython-312.pyc,,
|
||||
alembic/script/__pycache__/write_hooks.cpython-312.pyc,,
|
||||
alembic/script/base.py,sha256=OInSjbfcnUSjVCc5vVYY33UJ1Uo5xE5Huicp8P9VM1I,36698
|
||||
alembic/script/revision.py,sha256=SEePZPTMIyfjF73QAD0VIax9jc1dALkiLQZwTzwiyPw,62312
|
||||
alembic/script/write_hooks.py,sha256=KWH12250h_JcdBkGsLVo9JKYKpNcJxBUjwZ9r_r88Bc,5369
|
||||
alembic/templates/async/README,sha256=ISVtAOvqvKk_5ThM5ioJE-lMkvf9IbknFUFVU_vPma4,58
|
||||
alembic/templates/async/__pycache__/env.cpython-312.pyc,,
|
||||
alembic/templates/async/alembic.ini.mako,sha256=esbuCnpkyjntJC7k9NnYcCAzhrRQ8NVC4pWineiRk_w,5010
|
||||
alembic/templates/async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389
|
||||
alembic/templates/async/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/templates/generic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
|
||||
alembic/templates/generic/__pycache__/env.cpython-312.pyc,,
|
||||
alembic/templates/generic/alembic.ini.mako,sha256=2i2vPsGQSmE9XMiLz8tSBF_UIA8PJl0-fAvbRVmiK_w,5010
|
||||
alembic/templates/generic/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103
|
||||
alembic/templates/generic/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/templates/multidb/README,sha256=dWLDhnBgphA4Nzb7sNlMfCS3_06YqVbHhz-9O5JNqyI,606
|
||||
alembic/templates/multidb/__pycache__/env.cpython-312.pyc,,
|
||||
alembic/templates/multidb/alembic.ini.mako,sha256=asVt3aJVwjuuw9bopfMofVvonO31coXBbV5DeMRN6cM,5336
|
||||
alembic/templates/multidb/env.py,sha256=6zNjnW8mXGUk7erTsAvrfhvqoczJ-gagjVq1Ypg2YIQ,4230
|
||||
alembic/templates/multidb/script.py.mako,sha256=ZbCXMkI5Wj2dwNKcxuVGkKZ7Iav93BNx_bM4zbGi3c8,1235
|
||||
alembic/templates/pyproject/README,sha256=dMhIiFoeM7EdeaOXBs3mVQ6zXACMyGXDb_UBB6sGRA0,60
|
||||
alembic/templates/pyproject/__pycache__/env.cpython-312.pyc,,
|
||||
alembic/templates/pyproject/alembic.ini.mako,sha256=bQnEoydnLOUgg9vNbTOys4r5MaW8lmwYFXSrlfdEEkw,782
|
||||
alembic/templates/pyproject/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103
|
||||
alembic/templates/pyproject/pyproject.toml.mako,sha256=W6x_K-xLfEvyM8D4B3Fg0l20P1h6SPK33188pqRFroQ,3000
|
||||
alembic/templates/pyproject/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/templates/pyproject_async/README,sha256=2Q5XcEouiqQ-TJssO9805LROkVUd0F6d74rTnuLrifA,45
|
||||
alembic/templates/pyproject_async/__pycache__/env.cpython-312.pyc,,
|
||||
alembic/templates/pyproject_async/alembic.ini.mako,sha256=bQnEoydnLOUgg9vNbTOys4r5MaW8lmwYFXSrlfdEEkw,782
|
||||
alembic/templates/pyproject_async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389
|
||||
alembic/templates/pyproject_async/pyproject.toml.mako,sha256=W6x_K-xLfEvyM8D4B3Fg0l20P1h6SPK33188pqRFroQ,3000
|
||||
alembic/templates/pyproject_async/script.py.mako,sha256=04kgeBtNMa4cCnG8CfQcKt6P6rnloIfj8wy0u_DBydM,704
|
||||
alembic/testing/__init__.py,sha256=PTMhi_2PZ1T_3atQS2CIr0V4YRZzx_doKI-DxKdQS44,1297
|
||||
alembic/testing/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/assertions.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/env.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/fixtures.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/requirements.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/schemacompare.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/util.cpython-312.pyc,,
|
||||
alembic/testing/__pycache__/warnings.cpython-312.pyc,,
|
||||
alembic/testing/assertions.py,sha256=VKXMEVWjuPAsYnNxP3WnUpXaFN3ytNFf9LI72OEJ074,5344
|
||||
alembic/testing/env.py,sha256=oQN56xXHtHfK8RD-8pH8yZ-uWcjpuNL1Mt5HNrzZyc0,12151
|
||||
alembic/testing/fixtures.py,sha256=meqm10rd1ynppW6tw1wcpDJJLyQezZ7FwKyqcrwIOok,11931
|
||||
alembic/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
alembic/testing/plugin/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/testing/plugin/__pycache__/bootstrap.cpython-312.pyc,,
|
||||
alembic/testing/plugin/bootstrap.py,sha256=9C6wtjGrIVztZ928w27hsQE0KcjDLIUtUN3dvZKsMVk,50
|
||||
alembic/testing/requirements.py,sha256=OZSHd8I3zOb7288cZxUTebqxx8j0T6I8MekH15TyPvY,4566
|
||||
alembic/testing/schemacompare.py,sha256=N5UqSNCOJetIKC4vKhpYzQEpj08XkdgIoqBmEPQ3tlc,4838
|
||||
alembic/testing/suite/__init__.py,sha256=MvE7-hwbaVN1q3NM-ztGxORU9dnIelUCINKqNxewn7Y,288
|
||||
alembic/testing/suite/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/_autogen_fixtures.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_comments.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_computed.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_diffs.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_fks.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_autogen_identity.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_environment.cpython-312.pyc,,
|
||||
alembic/testing/suite/__pycache__/test_op.cpython-312.pyc,,
|
||||
alembic/testing/suite/_autogen_fixtures.py,sha256=3nNTd8iDeVeSgpPIj8KAraNbU-PkJtxDb4X_TVsZ528,14200
|
||||
alembic/testing/suite/test_autogen_comments.py,sha256=aEGqKUDw4kHjnDk298aoGcQvXJWmZXcIX_2FxH4cJK8,6283
|
||||
alembic/testing/suite/test_autogen_computed.py,sha256=puJ0hBtLzNz8LiPGqDPS8vse6dUS9VCBpUdw-cOksZo,4554
|
||||
alembic/testing/suite/test_autogen_diffs.py,sha256=T4SR1n_kmcOKYhR4W1-dA0e5sddJ69DSVL2HW96kAkE,8394
|
||||
alembic/testing/suite/test_autogen_fks.py,sha256=wHKjD4Egf7IZlH0HYw-c8uti0jhJpOm5K42QMXf5tIw,32930
|
||||
alembic/testing/suite/test_autogen_identity.py,sha256=kcuqngG7qXAKPJDX4U8sRzPKHEJECHuZ0DtuaS6tVkk,5824
|
||||
alembic/testing/suite/test_environment.py,sha256=OwD-kpESdLoc4byBrGrXbZHvqtPbzhFCG4W9hJOJXPQ,11877
|
||||
alembic/testing/suite/test_op.py,sha256=2XQCdm_NmnPxHGuGj7hmxMzIhKxXNotUsKdACXzE1mM,1343
|
||||
alembic/testing/util.py,sha256=CQrcQDA8fs_7ME85z5ydb-Bt70soIIID-qNY1vbR2dg,3350
|
||||
alembic/testing/warnings.py,sha256=cDDWzvxNZE6x9dME2ACTXSv01G81JcIbE1GIE_s1kvg,831
|
||||
alembic/util/__init__.py,sha256=xNpZtajyTF4eVEbLj0Pcm2FbNkIZD_pCvKGKSPucTEs,1777
|
||||
alembic/util/__pycache__/__init__.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/compat.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/editor.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/exc.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/langhelpers.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/messaging.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/pyfiles.cpython-312.pyc,,
|
||||
alembic/util/__pycache__/sqla_compat.cpython-312.pyc,,
|
||||
alembic/util/compat.py,sha256=NytmcsMtK8WEEVwWc-ZWYlSOi55BtRlmJXjxnF3nsh8,3810
|
||||
alembic/util/editor.py,sha256=JIz6_BdgV8_oKtnheR6DZoB7qnrHrlRgWjx09AsTsUw,2546
|
||||
alembic/util/exc.py,sha256=SublpLmAeAW8JeEml-1YyhIjkSORTkZbvHVVJeoPymg,993
|
||||
alembic/util/langhelpers.py,sha256=GBbR01xNi1kmz8W37h0NzXl3hBC1SY7k7Bj-h5jVgps,13164
|
||||
alembic/util/messaging.py,sha256=3bEBoDy4EAXETXAvArlYjeMITXDTgPTu6ZoE3ytnzSw,3294
|
||||
alembic/util/pyfiles.py,sha256=QUZYc5kE3Z7nV64PblcRffzA7VfVaiFB2x3vtcG0_AE,4707
|
||||
alembic/util/sqla_compat.py,sha256=llgJVtOsO1c3euS9_peORZkM9QeSvQWa-1LNHqrzEM4,15246
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (82.0.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
alembic = alembic.config:main
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright 2009-2026 Michael Bayer.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
alembic
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import context
|
||||
from . import op
|
||||
from .runtime import plugins
|
||||
|
||||
|
||||
__version__ = "1.18.4"
|
||||
@@ -0,0 +1,4 @@
|
||||
from .config import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(prog="alembic")
|
||||
@@ -0,0 +1,10 @@
|
||||
from .api import _render_migration_diffs as _render_migration_diffs
|
||||
from .api import compare_metadata as compare_metadata
|
||||
from .api import produce_migrations as produce_migrations
|
||||
from .api import render_python_code as render_python_code
|
||||
from .api import RevisionContext as RevisionContext
|
||||
from .compare import _produce_net_changes as _produce_net_changes
|
||||
from .compare import comparators as comparators
|
||||
from .render import render_op_text as render_op_text
|
||||
from .render import renderers as renderers
|
||||
from .rewriter import Rewriter as Rewriter
|
||||
@@ -0,0 +1,667 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from . import compare
|
||||
from . import render
|
||||
from .. import util
|
||||
from ..operations import ops
|
||||
from ..runtime.plugins import Plugin
|
||||
from ..util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.engine import Dialect
|
||||
from sqlalchemy.engine import Inspector
|
||||
from sqlalchemy.sql.schema import MetaData
|
||||
from sqlalchemy.sql.schema import SchemaItem
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
from ..config import Config
|
||||
from ..operations.ops import DowngradeOps
|
||||
from ..operations.ops import MigrationScript
|
||||
from ..operations.ops import UpgradeOps
|
||||
from ..runtime.environment import NameFilterParentNames
|
||||
from ..runtime.environment import NameFilterType
|
||||
from ..runtime.environment import ProcessRevisionDirectiveFn
|
||||
from ..runtime.environment import RenderItemFn
|
||||
from ..runtime.migration import MigrationContext
|
||||
from ..script.base import Script
|
||||
from ..script.base import ScriptDirectory
|
||||
from ..script.revision import _GetRevArg
|
||||
from ..util import PriorityDispatcher
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any:
|
||||
"""Compare a database schema to that given in a
|
||||
:class:`~sqlalchemy.schema.MetaData` instance.
|
||||
|
||||
The database connection is presented in the context
|
||||
of a :class:`.MigrationContext` object, which
|
||||
provides database connectivity as well as optional
|
||||
comparison functions to use for datatypes and
|
||||
server defaults - see the "autogenerate" arguments
|
||||
at :meth:`.EnvironmentContext.configure`
|
||||
for details on these.
|
||||
|
||||
The return format is a list of "diff" directives,
|
||||
each representing individual differences::
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.autogenerate import compare_metadata
|
||||
from sqlalchemy import (
|
||||
create_engine,
|
||||
MetaData,
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
Table,
|
||||
text,
|
||||
)
|
||||
import pprint
|
||||
|
||||
engine = create_engine("sqlite://")
|
||||
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
'''
|
||||
create table foo (
|
||||
id integer not null primary key,
|
||||
old_data varchar,
|
||||
x integer
|
||||
)
|
||||
'''
|
||||
)
|
||||
)
|
||||
conn.execute(text("create table bar (data varchar)"))
|
||||
|
||||
metadata = MetaData()
|
||||
Table(
|
||||
"foo",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("data", Integer),
|
||||
Column("x", Integer, nullable=False),
|
||||
)
|
||||
Table("bat", metadata, Column("info", String))
|
||||
|
||||
mc = MigrationContext.configure(engine.connect())
|
||||
|
||||
diff = compare_metadata(mc, metadata)
|
||||
pprint.pprint(diff, indent=2, width=20)
|
||||
|
||||
Output::
|
||||
|
||||
[
|
||||
(
|
||||
"add_table",
|
||||
Table(
|
||||
"bat",
|
||||
MetaData(),
|
||||
Column("info", String(), table=<bat>),
|
||||
schema=None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"remove_table",
|
||||
Table(
|
||||
"bar",
|
||||
MetaData(),
|
||||
Column("data", VARCHAR(), table=<bar>),
|
||||
schema=None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"add_column",
|
||||
None,
|
||||
"foo",
|
||||
Column("data", Integer(), table=<foo>),
|
||||
),
|
||||
[
|
||||
(
|
||||
"modify_nullable",
|
||||
None,
|
||||
"foo",
|
||||
"x",
|
||||
{
|
||||
"existing_comment": None,
|
||||
"existing_server_default": False,
|
||||
"existing_type": INTEGER(),
|
||||
},
|
||||
True,
|
||||
False,
|
||||
)
|
||||
],
|
||||
(
|
||||
"remove_column",
|
||||
None,
|
||||
"foo",
|
||||
Column("old_data", VARCHAR(), table=<foo>),
|
||||
),
|
||||
]
|
||||
|
||||
:param context: a :class:`.MigrationContext`
|
||||
instance.
|
||||
:param metadata: a :class:`~sqlalchemy.schema.MetaData`
|
||||
instance.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`.produce_migrations` - produces a :class:`.MigrationScript`
|
||||
structure based on metadata comparison.
|
||||
|
||||
"""
|
||||
|
||||
migration_script = produce_migrations(context, metadata)
|
||||
assert migration_script.upgrade_ops is not None
|
||||
return migration_script.upgrade_ops.as_diffs()
|
||||
|
||||
|
||||
def produce_migrations(
|
||||
context: MigrationContext, metadata: MetaData
|
||||
) -> MigrationScript:
|
||||
"""Produce a :class:`.MigrationScript` structure based on schema
|
||||
comparison.
|
||||
|
||||
This function does essentially what :func:`.compare_metadata` does,
|
||||
but then runs the resulting list of diffs to produce the full
|
||||
:class:`.MigrationScript` object. For an example of what this looks like,
|
||||
see the example in :ref:`customizing_revision`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`.compare_metadata` - returns more fundamental "diff"
|
||||
data from comparing a schema.
|
||||
|
||||
"""
|
||||
|
||||
autogen_context = AutogenContext(context, metadata=metadata)
|
||||
|
||||
migration_script = ops.MigrationScript(
|
||||
rev_id=None,
|
||||
upgrade_ops=ops.UpgradeOps([]),
|
||||
downgrade_ops=ops.DowngradeOps([]),
|
||||
)
|
||||
|
||||
compare._populate_migration_script(autogen_context, migration_script)
|
||||
|
||||
return migration_script
|
||||
|
||||
|
||||
def render_python_code(
|
||||
up_or_down_op: Union[UpgradeOps, DowngradeOps],
|
||||
sqlalchemy_module_prefix: str = "sa.",
|
||||
alembic_module_prefix: str = "op.",
|
||||
render_as_batch: bool = False,
|
||||
imports: Sequence[str] = (),
|
||||
render_item: Optional[RenderItemFn] = None,
|
||||
migration_context: Optional[MigrationContext] = None,
|
||||
user_module_prefix: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Render Python code given an :class:`.UpgradeOps` or
|
||||
:class:`.DowngradeOps` object.
|
||||
|
||||
This is a convenience function that can be used to test the
|
||||
autogenerate output of a user-defined :class:`.MigrationScript` structure.
|
||||
|
||||
:param up_or_down_op: :class:`.UpgradeOps` or :class:`.DowngradeOps` object
|
||||
:param sqlalchemy_module_prefix: module prefix for SQLAlchemy objects
|
||||
:param alembic_module_prefix: module prefix for Alembic constructs
|
||||
:param render_as_batch: use "batch operations" style for rendering
|
||||
:param imports: sequence of import symbols to add
|
||||
:param render_item: callable to render items
|
||||
:param migration_context: optional :class:`.MigrationContext`
|
||||
:param user_module_prefix: optional string prefix for user-defined types
|
||||
|
||||
.. versionadded:: 1.11.0
|
||||
|
||||
"""
|
||||
opts = {
|
||||
"sqlalchemy_module_prefix": sqlalchemy_module_prefix,
|
||||
"alembic_module_prefix": alembic_module_prefix,
|
||||
"render_item": render_item,
|
||||
"render_as_batch": render_as_batch,
|
||||
"user_module_prefix": user_module_prefix,
|
||||
}
|
||||
|
||||
if migration_context is None:
|
||||
from ..runtime.migration import MigrationContext
|
||||
from sqlalchemy.engine.default import DefaultDialect
|
||||
|
||||
migration_context = MigrationContext.configure(
|
||||
dialect=DefaultDialect()
|
||||
)
|
||||
|
||||
autogen_context = AutogenContext(migration_context, opts=opts)
|
||||
autogen_context.imports = set(imports)
|
||||
return render._indent(
|
||||
render._render_cmd_body(up_or_down_op, autogen_context)
|
||||
)
|
||||
|
||||
|
||||
def _render_migration_diffs(
|
||||
context: MigrationContext, template_args: Dict[Any, Any]
|
||||
) -> None:
|
||||
"""legacy, used by test_autogen_composition at the moment"""
|
||||
|
||||
autogen_context = AutogenContext(context)
|
||||
|
||||
upgrade_ops = ops.UpgradeOps([])
|
||||
compare._produce_net_changes(autogen_context, upgrade_ops)
|
||||
|
||||
migration_script = ops.MigrationScript(
|
||||
rev_id=None,
|
||||
upgrade_ops=upgrade_ops,
|
||||
downgrade_ops=upgrade_ops.reverse(),
|
||||
)
|
||||
|
||||
render._render_python_into_templatevars(
|
||||
autogen_context, migration_script, template_args
|
||||
)
|
||||
|
||||
|
||||
class AutogenContext:
|
||||
"""Maintains configuration and state that's specific to an
|
||||
autogenerate operation."""
|
||||
|
||||
metadata: Union[MetaData, Sequence[MetaData], None] = None
|
||||
"""The :class:`~sqlalchemy.schema.MetaData` object
|
||||
representing the destination.
|
||||
|
||||
This object is the one that is passed within ``env.py``
|
||||
to the :paramref:`.EnvironmentContext.configure.target_metadata`
|
||||
parameter. It represents the structure of :class:`.Table` and other
|
||||
objects as stated in the current database model, and represents the
|
||||
destination structure for the database being examined.
|
||||
|
||||
While the :class:`~sqlalchemy.schema.MetaData` object is primarily
|
||||
known as a collection of :class:`~sqlalchemy.schema.Table` objects,
|
||||
it also has an :attr:`~sqlalchemy.schema.MetaData.info` dictionary
|
||||
that may be used by end-user schemes to store additional schema-level
|
||||
objects that are to be compared in custom autogeneration schemes.
|
||||
|
||||
"""
|
||||
|
||||
connection: Optional[Connection] = None
|
||||
"""The :class:`~sqlalchemy.engine.base.Connection` object currently
|
||||
connected to the database backend being compared.
|
||||
|
||||
This is obtained from the :attr:`.MigrationContext.bind` and is
|
||||
ultimately set up in the ``env.py`` script.
|
||||
|
||||
"""
|
||||
|
||||
dialect: Dialect
|
||||
"""The :class:`~sqlalchemy.engine.Dialect` object currently in use.
|
||||
|
||||
This is normally obtained from the
|
||||
:attr:`~sqlalchemy.engine.base.Connection.dialect` attribute.
|
||||
|
||||
"""
|
||||
|
||||
imports: Set[str] = None # type: ignore[assignment]
|
||||
"""A ``set()`` which contains string Python import directives.
|
||||
|
||||
The directives are to be rendered into the ``${imports}`` section
|
||||
of a script template. The set is normally empty and can be modified
|
||||
within hooks such as the
|
||||
:paramref:`.EnvironmentContext.configure.render_item` hook.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogen_render_types`
|
||||
|
||||
"""
|
||||
|
||||
migration_context: MigrationContext
|
||||
"""The :class:`.MigrationContext` established by the ``env.py`` script."""
|
||||
|
||||
comparators: PriorityDispatcher
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
migration_context: MigrationContext,
|
||||
metadata: Union[MetaData, Sequence[MetaData], None] = None,
|
||||
opts: Optional[Dict[str, Any]] = None,
|
||||
autogenerate: bool = True,
|
||||
) -> None:
|
||||
if (
|
||||
autogenerate
|
||||
and migration_context is not None
|
||||
and migration_context.as_sql
|
||||
):
|
||||
raise util.CommandError(
|
||||
"autogenerate can't use as_sql=True as it prevents querying "
|
||||
"the database for schema information"
|
||||
)
|
||||
|
||||
# branch off from the "global" comparators. This collection
|
||||
# is empty in Alembic except that it is populated by third party
|
||||
# extensions that don't use the plugin system. so we will build
|
||||
# off of whatever is in there.
|
||||
if autogenerate:
|
||||
self.comparators = compare.comparators.branch()
|
||||
Plugin.populate_autogenerate_priority_dispatch(
|
||||
self.comparators,
|
||||
include_plugins=migration_context.opts.get(
|
||||
"autogenerate_plugins", ["alembic.autogenerate.*"]
|
||||
),
|
||||
)
|
||||
|
||||
if opts is None:
|
||||
opts = migration_context.opts
|
||||
|
||||
self.metadata = metadata = (
|
||||
opts.get("target_metadata", None) if metadata is None else metadata
|
||||
)
|
||||
|
||||
if (
|
||||
autogenerate
|
||||
and metadata is None
|
||||
and migration_context is not None
|
||||
and migration_context.script is not None
|
||||
):
|
||||
raise util.CommandError(
|
||||
"Can't proceed with --autogenerate option; environment "
|
||||
"script %s does not provide "
|
||||
"a MetaData object or sequence of objects to the context."
|
||||
% (migration_context.script.env_py_location)
|
||||
)
|
||||
|
||||
include_object = opts.get("include_object", None)
|
||||
include_name = opts.get("include_name", None)
|
||||
|
||||
object_filters = []
|
||||
name_filters = []
|
||||
if include_object:
|
||||
object_filters.append(include_object)
|
||||
if include_name:
|
||||
name_filters.append(include_name)
|
||||
|
||||
self._object_filters = object_filters
|
||||
self._name_filters = name_filters
|
||||
|
||||
self.migration_context = migration_context
|
||||
self.connection = self.migration_context.bind
|
||||
self.dialect = self.migration_context.dialect
|
||||
|
||||
self.imports = set()
|
||||
self.opts: Dict[str, Any] = opts
|
||||
self._has_batch: bool = False
|
||||
|
||||
@util.memoized_property
|
||||
def inspector(self) -> Inspector:
|
||||
if self.connection is None:
|
||||
raise TypeError(
|
||||
"can't return inspector as this "
|
||||
"AutogenContext has no database connection"
|
||||
)
|
||||
return inspect(self.connection)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _within_batch(self) -> Iterator[None]:
|
||||
self._has_batch = True
|
||||
yield
|
||||
self._has_batch = False
|
||||
|
||||
def run_name_filters(
|
||||
self,
|
||||
name: Optional[str],
|
||||
type_: NameFilterType,
|
||||
parent_names: NameFilterParentNames,
|
||||
) -> bool:
|
||||
"""Run the context's name filters and return True if the targets
|
||||
should be part of the autogenerate operation.
|
||||
|
||||
This method should be run for every kind of name encountered within the
|
||||
reflection side of an autogenerate operation, giving the environment
|
||||
the chance to filter what names should be reflected as database
|
||||
objects. The filters here are produced directly via the
|
||||
:paramref:`.EnvironmentContext.configure.include_name` parameter.
|
||||
|
||||
"""
|
||||
if "schema_name" in parent_names:
|
||||
if type_ == "table":
|
||||
table_name = name
|
||||
else:
|
||||
table_name = parent_names.get("table_name", None)
|
||||
if table_name:
|
||||
schema_name = parent_names["schema_name"]
|
||||
if schema_name:
|
||||
parent_names["schema_qualified_table_name"] = "%s.%s" % (
|
||||
schema_name,
|
||||
table_name,
|
||||
)
|
||||
else:
|
||||
parent_names["schema_qualified_table_name"] = table_name
|
||||
|
||||
for fn in self._name_filters:
|
||||
if not fn(name, type_, parent_names):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def run_object_filters(
|
||||
self,
|
||||
object_: SchemaItem,
|
||||
name: sqla_compat._ConstraintName,
|
||||
type_: NameFilterType,
|
||||
reflected: bool,
|
||||
compare_to: Optional[SchemaItem],
|
||||
) -> bool:
|
||||
"""Run the context's object filters and return True if the targets
|
||||
should be part of the autogenerate operation.
|
||||
|
||||
This method should be run for every kind of object encountered within
|
||||
an autogenerate operation, giving the environment the chance
|
||||
to filter what objects should be included in the comparison.
|
||||
The filters here are produced directly via the
|
||||
:paramref:`.EnvironmentContext.configure.include_object` parameter.
|
||||
|
||||
"""
|
||||
for fn in self._object_filters:
|
||||
if not fn(object_, name, type_, reflected, compare_to):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
run_filters = run_object_filters
|
||||
|
||||
@util.memoized_property
|
||||
def sorted_tables(self) -> List[Table]:
|
||||
"""Return an aggregate of the :attr:`.MetaData.sorted_tables`
|
||||
collection(s).
|
||||
|
||||
For a sequence of :class:`.MetaData` objects, this
|
||||
concatenates the :attr:`.MetaData.sorted_tables` collection
|
||||
for each individual :class:`.MetaData` in the order of the
|
||||
sequence. It does **not** collate the sorted tables collections.
|
||||
|
||||
"""
|
||||
result = []
|
||||
for m in util.to_list(self.metadata):
|
||||
result.extend(m.sorted_tables)
|
||||
return result
|
||||
|
||||
@util.memoized_property
|
||||
def table_key_to_table(self) -> Dict[str, Table]:
|
||||
"""Return an aggregate of the :attr:`.MetaData.tables` dictionaries.
|
||||
|
||||
The :attr:`.MetaData.tables` collection is a dictionary of table key
|
||||
to :class:`.Table`; this method aggregates the dictionary across
|
||||
multiple :class:`.MetaData` objects into one dictionary.
|
||||
|
||||
Duplicate table keys are **not** supported; if two :class:`.MetaData`
|
||||
objects contain the same table key, an exception is raised.
|
||||
|
||||
"""
|
||||
result: Dict[str, Table] = {}
|
||||
for m in util.to_list(self.metadata):
|
||||
intersect = set(result).intersection(set(m.tables))
|
||||
if intersect:
|
||||
raise ValueError(
|
||||
"Duplicate table keys across multiple "
|
||||
"MetaData objects: %s"
|
||||
% (", ".join('"%s"' % key for key in sorted(intersect)))
|
||||
)
|
||||
|
||||
result.update(m.tables)
|
||||
return result
|
||||
|
||||
|
||||
class RevisionContext:
|
||||
"""Maintains configuration and state that's specific to a revision
|
||||
file generation operation."""
|
||||
|
||||
generated_revisions: List[MigrationScript]
|
||||
process_revision_directives: Optional[ProcessRevisionDirectiveFn]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
script_directory: ScriptDirectory,
|
||||
command_args: Dict[str, Any],
|
||||
process_revision_directives: Optional[
|
||||
ProcessRevisionDirectiveFn
|
||||
] = None,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.script_directory = script_directory
|
||||
self.command_args = command_args
|
||||
self.process_revision_directives = process_revision_directives
|
||||
self.template_args = {
|
||||
"config": config # Let templates use config for
|
||||
# e.g. multiple databases
|
||||
}
|
||||
self.generated_revisions = [self._default_revision()]
|
||||
|
||||
def _to_script(
|
||||
self, migration_script: MigrationScript
|
||||
) -> Optional[Script]:
|
||||
template_args: Dict[str, Any] = self.template_args.copy()
|
||||
|
||||
if getattr(migration_script, "_needs_render", False):
|
||||
autogen_context = self._last_autogen_context
|
||||
|
||||
# clear out existing imports if we are doing multiple
|
||||
# renders
|
||||
autogen_context.imports = set()
|
||||
if migration_script.imports:
|
||||
autogen_context.imports.update(migration_script.imports)
|
||||
render._render_python_into_templatevars(
|
||||
autogen_context, migration_script, template_args
|
||||
)
|
||||
|
||||
assert migration_script.rev_id is not None
|
||||
return self.script_directory.generate_revision(
|
||||
migration_script.rev_id,
|
||||
migration_script.message,
|
||||
refresh=True,
|
||||
head=migration_script.head,
|
||||
splice=migration_script.splice,
|
||||
branch_labels=migration_script.branch_label,
|
||||
version_path=migration_script.version_path,
|
||||
depends_on=migration_script.depends_on,
|
||||
**template_args,
|
||||
)
|
||||
|
||||
def run_autogenerate(
|
||||
self, rev: _GetRevArg, migration_context: MigrationContext
|
||||
) -> None:
|
||||
self._run_environment(rev, migration_context, True)
|
||||
|
||||
def run_no_autogenerate(
|
||||
self, rev: _GetRevArg, migration_context: MigrationContext
|
||||
) -> None:
|
||||
self._run_environment(rev, migration_context, False)
|
||||
|
||||
def _run_environment(
|
||||
self,
|
||||
rev: _GetRevArg,
|
||||
migration_context: MigrationContext,
|
||||
autogenerate: bool,
|
||||
) -> None:
|
||||
if autogenerate:
|
||||
if self.command_args["sql"]:
|
||||
raise util.CommandError(
|
||||
"Using --sql with --autogenerate does not make any sense"
|
||||
)
|
||||
if set(self.script_directory.get_revisions(rev)) != set(
|
||||
self.script_directory.get_revisions("heads")
|
||||
):
|
||||
raise util.CommandError("Target database is not up to date.")
|
||||
|
||||
upgrade_token = migration_context.opts["upgrade_token"]
|
||||
downgrade_token = migration_context.opts["downgrade_token"]
|
||||
|
||||
migration_script = self.generated_revisions[-1]
|
||||
if not getattr(migration_script, "_needs_render", False):
|
||||
migration_script.upgrade_ops_list[-1].upgrade_token = upgrade_token
|
||||
migration_script.downgrade_ops_list[-1].downgrade_token = (
|
||||
downgrade_token
|
||||
)
|
||||
migration_script._needs_render = True
|
||||
else:
|
||||
migration_script._upgrade_ops.append(
|
||||
ops.UpgradeOps([], upgrade_token=upgrade_token)
|
||||
)
|
||||
migration_script._downgrade_ops.append(
|
||||
ops.DowngradeOps([], downgrade_token=downgrade_token)
|
||||
)
|
||||
|
||||
autogen_context = AutogenContext(
|
||||
migration_context, autogenerate=autogenerate
|
||||
)
|
||||
self._last_autogen_context: AutogenContext = autogen_context
|
||||
|
||||
if autogenerate:
|
||||
compare._populate_migration_script(
|
||||
autogen_context, migration_script
|
||||
)
|
||||
|
||||
if self.process_revision_directives:
|
||||
self.process_revision_directives(
|
||||
migration_context, rev, self.generated_revisions
|
||||
)
|
||||
|
||||
hook = migration_context.opts["process_revision_directives"]
|
||||
if hook:
|
||||
hook(migration_context, rev, self.generated_revisions)
|
||||
|
||||
for migration_script in self.generated_revisions:
|
||||
migration_script._needs_render = True
|
||||
|
||||
def _default_revision(self) -> MigrationScript:
|
||||
command_args: Dict[str, Any] = self.command_args
|
||||
op = ops.MigrationScript(
|
||||
rev_id=command_args["rev_id"] or util.rev_id(),
|
||||
message=command_args["message"],
|
||||
upgrade_ops=ops.UpgradeOps([]),
|
||||
downgrade_ops=ops.DowngradeOps([]),
|
||||
head=command_args["head"],
|
||||
splice=command_args["splice"],
|
||||
branch_label=command_args["branch_label"],
|
||||
version_path=command_args["version_path"],
|
||||
depends_on=command_args["depends_on"],
|
||||
)
|
||||
return op
|
||||
|
||||
def generate_scripts(self) -> Iterator[Optional[Script]]:
|
||||
for generated_revision in self.generated_revisions:
|
||||
yield self._to_script(generated_revision)
|
||||
@@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import comments
|
||||
from . import constraints
|
||||
from . import schema
|
||||
from . import server_defaults
|
||||
from . import tables
|
||||
from . import types
|
||||
from ... import util
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..api import AutogenContext
|
||||
from ...operations.ops import MigrationScript
|
||||
from ...operations.ops import UpgradeOps
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
comparators = util.PriorityDispatcher()
|
||||
"""global registry which alembic keeps empty, but copies when creating
|
||||
a new AutogenContext.
|
||||
|
||||
This is to support a variety of third party plugins that hook their autogen
|
||||
functionality onto this collection.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def _populate_migration_script(
|
||||
autogen_context: AutogenContext, migration_script: MigrationScript
|
||||
) -> None:
|
||||
upgrade_ops = migration_script.upgrade_ops_list[-1]
|
||||
downgrade_ops = migration_script.downgrade_ops_list[-1]
|
||||
|
||||
_produce_net_changes(autogen_context, upgrade_ops)
|
||||
upgrade_ops.reverse_into(downgrade_ops)
|
||||
|
||||
|
||||
def _produce_net_changes(
|
||||
autogen_context: AutogenContext, upgrade_ops: UpgradeOps
|
||||
) -> None:
|
||||
assert autogen_context.dialect is not None
|
||||
|
||||
autogen_context.comparators.dispatch(
|
||||
"autogenerate", qualifier=autogen_context.dialect.name
|
||||
)(autogen_context, upgrade_ops)
|
||||
|
||||
|
||||
Plugin.setup_plugin_from_module(schema, "alembic.autogenerate.schemas")
|
||||
Plugin.setup_plugin_from_module(tables, "alembic.autogenerate.tables")
|
||||
Plugin.setup_plugin_from_module(types, "alembic.autogenerate.types")
|
||||
Plugin.setup_plugin_from_module(
|
||||
constraints, "alembic.autogenerate.constraints"
|
||||
)
|
||||
Plugin.setup_plugin_from_module(
|
||||
server_defaults, "alembic.autogenerate.defaults"
|
||||
)
|
||||
Plugin.setup_plugin_from_module(comments, "alembic.autogenerate.comments")
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from ...operations import ops
|
||||
from ...util import PriorityDispatchResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import Column
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
from ..api import AutogenContext
|
||||
from ...operations.ops import AlterColumnOp
|
||||
from ...operations.ops import ModifyTableOps
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _compare_column_comment(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: quoted_name,
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
assert autogen_context.dialect is not None
|
||||
if not autogen_context.dialect.supports_comments:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
metadata_comment = metadata_col.comment
|
||||
conn_col_comment = conn_col.comment
|
||||
if conn_col_comment is None and metadata_comment is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
alter_column_op.existing_comment = conn_col_comment
|
||||
|
||||
if conn_col_comment != metadata_comment:
|
||||
alter_column_op.modify_comment = metadata_comment
|
||||
log.info("Detected column comment '%s.%s'", tname, cname)
|
||||
|
||||
return PriorityDispatchResult.STOP
|
||||
else:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _compare_table_comment(
|
||||
autogen_context: AutogenContext,
|
||||
modify_table_ops: ModifyTableOps,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
conn_table: Optional[Table],
|
||||
metadata_table: Optional[Table],
|
||||
) -> PriorityDispatchResult:
|
||||
assert autogen_context.dialect is not None
|
||||
if not autogen_context.dialect.supports_comments:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
# if we're doing CREATE TABLE, comments will be created inline
|
||||
# with the create_table op.
|
||||
if conn_table is None or metadata_table is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
if conn_table.comment is None and metadata_table.comment is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
if metadata_table.comment is None and conn_table.comment is not None:
|
||||
modify_table_ops.ops.append(
|
||||
ops.DropTableCommentOp(
|
||||
tname, existing_comment=conn_table.comment, schema=schema
|
||||
)
|
||||
)
|
||||
return PriorityDispatchResult.STOP
|
||||
elif metadata_table.comment != conn_table.comment:
|
||||
modify_table_ops.ops.append(
|
||||
ops.CreateTableCommentOp(
|
||||
tname,
|
||||
metadata_table.comment,
|
||||
existing_comment=conn_table.comment,
|
||||
schema=schema,
|
||||
)
|
||||
)
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def setup(plugin: Plugin) -> None:
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_column_comment,
|
||||
"column",
|
||||
"comments",
|
||||
)
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_table_comment,
|
||||
"table",
|
||||
"comments",
|
||||
)
|
||||
@@ -0,0 +1,812 @@
|
||||
# mypy: allow-untyped-defs, allow-untyped-calls, allow-incomplete-defs
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Collection
|
||||
from typing import Dict
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy.sql.schema import ForeignKeyConstraint
|
||||
from sqlalchemy.sql.schema import Index
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
|
||||
from .util import _InspectorConv
|
||||
from ... import util
|
||||
from ...ddl._autogen import is_index_sig
|
||||
from ...ddl._autogen import is_uq_sig
|
||||
from ...operations import ops
|
||||
from ...util import PriorityDispatchResult
|
||||
from ...util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine.interfaces import ReflectedForeignKeyConstraint
|
||||
from sqlalchemy.engine.interfaces import ReflectedIndex
|
||||
from sqlalchemy.engine.interfaces import ReflectedUniqueConstraint
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.elements import TextClause
|
||||
from sqlalchemy.sql.schema import Column
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
from ...autogenerate.api import AutogenContext
|
||||
from ...ddl._autogen import _constraint_sig
|
||||
from ...ddl.impl import DefaultImpl
|
||||
from ...operations.ops import AlterColumnOp
|
||||
from ...operations.ops import ModifyTableOps
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
_C = TypeVar("_C", bound=Union[UniqueConstraint, ForeignKeyConstraint, Index])
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _compare_indexes_and_uniques(
|
||||
autogen_context: AutogenContext,
|
||||
modify_ops: ModifyTableOps,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
conn_table: Optional[Table],
|
||||
metadata_table: Optional[Table],
|
||||
) -> PriorityDispatchResult:
|
||||
inspector = autogen_context.inspector
|
||||
is_create_table = conn_table is None
|
||||
is_drop_table = metadata_table is None
|
||||
impl = autogen_context.migration_context.impl
|
||||
|
||||
# 1a. get raw indexes and unique constraints from metadata ...
|
||||
if metadata_table is not None:
|
||||
metadata_unique_constraints = {
|
||||
uq
|
||||
for uq in metadata_table.constraints
|
||||
if isinstance(uq, sa_schema.UniqueConstraint)
|
||||
}
|
||||
metadata_indexes = set(metadata_table.indexes)
|
||||
else:
|
||||
metadata_unique_constraints = set()
|
||||
metadata_indexes = set()
|
||||
|
||||
conn_uniques: Collection[UniqueConstraint] = frozenset()
|
||||
conn_indexes: Collection[Index] = frozenset()
|
||||
|
||||
supports_unique_constraints = False
|
||||
|
||||
unique_constraints_duplicate_unique_indexes = False
|
||||
|
||||
if conn_table is not None:
|
||||
conn_uniques_reflected: Collection[ReflectedUniqueConstraint] = (
|
||||
frozenset()
|
||||
)
|
||||
conn_indexes_reflected: Collection[ReflectedIndex] = frozenset()
|
||||
|
||||
# 1b. ... and from connection, if the table exists
|
||||
try:
|
||||
conn_uniques_reflected = _InspectorConv(
|
||||
inspector
|
||||
).get_unique_constraints(tname, schema=schema)
|
||||
|
||||
supports_unique_constraints = True
|
||||
except NotImplementedError:
|
||||
pass
|
||||
except TypeError:
|
||||
# number of arguments is off for the base
|
||||
# method in SQLAlchemy due to the cache decorator
|
||||
# not being present
|
||||
pass
|
||||
else:
|
||||
conn_uniques_reflected = [
|
||||
uq
|
||||
for uq in conn_uniques_reflected
|
||||
if autogen_context.run_name_filters(
|
||||
uq["name"],
|
||||
"unique_constraint",
|
||||
{"table_name": tname, "schema_name": schema},
|
||||
)
|
||||
]
|
||||
for uq in conn_uniques_reflected:
|
||||
if uq.get("duplicates_index"):
|
||||
unique_constraints_duplicate_unique_indexes = True
|
||||
try:
|
||||
conn_indexes_reflected = _InspectorConv(inspector).get_indexes(
|
||||
tname, schema=schema
|
||||
)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
else:
|
||||
conn_indexes_reflected = [
|
||||
ix
|
||||
for ix in conn_indexes_reflected
|
||||
if autogen_context.run_name_filters(
|
||||
ix["name"],
|
||||
"index",
|
||||
{"table_name": tname, "schema_name": schema},
|
||||
)
|
||||
]
|
||||
|
||||
# 2. convert conn-level objects from raw inspector records
|
||||
# into schema objects
|
||||
if is_drop_table:
|
||||
# for DROP TABLE uniques are inline, don't need them
|
||||
conn_uniques = set()
|
||||
else:
|
||||
conn_uniques = {
|
||||
_make_unique_constraint(impl, uq_def, conn_table)
|
||||
for uq_def in conn_uniques_reflected
|
||||
}
|
||||
|
||||
conn_indexes = {
|
||||
index
|
||||
for index in (
|
||||
_make_index(impl, ix, conn_table)
|
||||
for ix in conn_indexes_reflected
|
||||
)
|
||||
if index is not None
|
||||
}
|
||||
|
||||
# 2a. if the dialect dupes unique indexes as unique constraints
|
||||
# (mysql and oracle), correct for that
|
||||
|
||||
if unique_constraints_duplicate_unique_indexes:
|
||||
_correct_for_uq_duplicates_uix(
|
||||
conn_uniques,
|
||||
conn_indexes,
|
||||
metadata_unique_constraints,
|
||||
metadata_indexes,
|
||||
autogen_context.dialect,
|
||||
impl,
|
||||
)
|
||||
|
||||
# 3. give the dialect a chance to omit indexes and constraints that
|
||||
# we know are either added implicitly by the DB or that the DB
|
||||
# can't accurately report on
|
||||
impl.correct_for_autogen_constraints(
|
||||
conn_uniques, # type: ignore[arg-type]
|
||||
conn_indexes, # type: ignore[arg-type]
|
||||
metadata_unique_constraints,
|
||||
metadata_indexes,
|
||||
)
|
||||
|
||||
# 4. organize the constraints into "signature" collections, the
|
||||
# _constraint_sig() objects provide a consistent facade over both
|
||||
# Index and UniqueConstraint so we can easily work with them
|
||||
# interchangeably
|
||||
metadata_unique_constraints_sig = {
|
||||
impl._create_metadata_constraint_sig(uq)
|
||||
for uq in metadata_unique_constraints
|
||||
}
|
||||
|
||||
metadata_indexes_sig = {
|
||||
impl._create_metadata_constraint_sig(ix) for ix in metadata_indexes
|
||||
}
|
||||
|
||||
conn_unique_constraints = {
|
||||
impl._create_reflected_constraint_sig(uq) for uq in conn_uniques
|
||||
}
|
||||
|
||||
conn_indexes_sig = {
|
||||
impl._create_reflected_constraint_sig(ix) for ix in conn_indexes
|
||||
}
|
||||
|
||||
# 5. index things by name, for those objects that have names
|
||||
metadata_names = {
|
||||
cast(str, c.md_name_to_sql_name(autogen_context)): c
|
||||
for c in metadata_unique_constraints_sig.union(metadata_indexes_sig)
|
||||
if c.is_named
|
||||
}
|
||||
|
||||
conn_uniques_by_name: Dict[
|
||||
sqla_compat._ConstraintName,
|
||||
_constraint_sig[sa_schema.UniqueConstraint],
|
||||
]
|
||||
conn_indexes_by_name: Dict[
|
||||
sqla_compat._ConstraintName, _constraint_sig[sa_schema.Index]
|
||||
]
|
||||
|
||||
conn_uniques_by_name = {c.name: c for c in conn_unique_constraints}
|
||||
conn_indexes_by_name = {c.name: c for c in conn_indexes_sig}
|
||||
conn_names = {
|
||||
c.name: c
|
||||
for c in conn_unique_constraints.union(conn_indexes_sig)
|
||||
if sqla_compat.constraint_name_string(c.name)
|
||||
}
|
||||
|
||||
doubled_constraints = {
|
||||
name: (conn_uniques_by_name[name], conn_indexes_by_name[name])
|
||||
for name in set(conn_uniques_by_name).intersection(
|
||||
conn_indexes_by_name
|
||||
)
|
||||
}
|
||||
|
||||
# 6. index things by "column signature", to help with unnamed unique
|
||||
# constraints.
|
||||
conn_uniques_by_sig = {uq.unnamed: uq for uq in conn_unique_constraints}
|
||||
metadata_uniques_by_sig = {
|
||||
uq.unnamed: uq for uq in metadata_unique_constraints_sig
|
||||
}
|
||||
unnamed_metadata_uniques = {
|
||||
uq.unnamed: uq
|
||||
for uq in metadata_unique_constraints_sig
|
||||
if not sqla_compat._constraint_is_named(
|
||||
uq.const, autogen_context.dialect
|
||||
)
|
||||
}
|
||||
|
||||
# assumptions:
|
||||
# 1. a unique constraint or an index from the connection *always*
|
||||
# has a name.
|
||||
# 2. an index on the metadata side *always* has a name.
|
||||
# 3. a unique constraint on the metadata side *might* have a name.
|
||||
# 4. The backend may double up indexes as unique constraints and
|
||||
# vice versa (e.g. MySQL, Postgresql)
|
||||
|
||||
def obj_added(
|
||||
obj: (
|
||||
_constraint_sig[sa_schema.UniqueConstraint]
|
||||
| _constraint_sig[sa_schema.Index]
|
||||
),
|
||||
):
|
||||
if is_index_sig(obj):
|
||||
if autogen_context.run_object_filters(
|
||||
obj.const, obj.name, "index", False, None
|
||||
):
|
||||
modify_ops.ops.append(ops.CreateIndexOp.from_index(obj.const))
|
||||
log.info(
|
||||
"Detected added index %r on '%s'",
|
||||
obj.name,
|
||||
obj.column_names,
|
||||
)
|
||||
elif is_uq_sig(obj):
|
||||
if not supports_unique_constraints:
|
||||
# can't report unique indexes as added if we don't
|
||||
# detect them
|
||||
return
|
||||
if is_create_table or is_drop_table:
|
||||
# unique constraints are created inline with table defs
|
||||
return
|
||||
if autogen_context.run_object_filters(
|
||||
obj.const, obj.name, "unique_constraint", False, None
|
||||
):
|
||||
modify_ops.ops.append(
|
||||
ops.AddConstraintOp.from_constraint(obj.const)
|
||||
)
|
||||
log.info(
|
||||
"Detected added unique constraint %r on '%s'",
|
||||
obj.name,
|
||||
obj.column_names,
|
||||
)
|
||||
else:
|
||||
assert False
|
||||
|
||||
def obj_removed(
|
||||
obj: (
|
||||
_constraint_sig[sa_schema.UniqueConstraint]
|
||||
| _constraint_sig[sa_schema.Index]
|
||||
),
|
||||
):
|
||||
if is_index_sig(obj):
|
||||
if obj.is_unique and not supports_unique_constraints:
|
||||
# many databases double up unique constraints
|
||||
# as unique indexes. without that list we can't
|
||||
# be sure what we're doing here
|
||||
return
|
||||
|
||||
if autogen_context.run_object_filters(
|
||||
obj.const, obj.name, "index", True, None
|
||||
):
|
||||
modify_ops.ops.append(ops.DropIndexOp.from_index(obj.const))
|
||||
log.info("Detected removed index %r on %r", obj.name, tname)
|
||||
elif is_uq_sig(obj):
|
||||
if is_create_table or is_drop_table:
|
||||
# if the whole table is being dropped, we don't need to
|
||||
# consider unique constraint separately
|
||||
return
|
||||
if autogen_context.run_object_filters(
|
||||
obj.const, obj.name, "unique_constraint", True, None
|
||||
):
|
||||
modify_ops.ops.append(
|
||||
ops.DropConstraintOp.from_constraint(obj.const)
|
||||
)
|
||||
log.info(
|
||||
"Detected removed unique constraint %r on %r",
|
||||
obj.name,
|
||||
tname,
|
||||
)
|
||||
else:
|
||||
assert False
|
||||
|
||||
def obj_changed(
|
||||
old: (
|
||||
_constraint_sig[sa_schema.UniqueConstraint]
|
||||
| _constraint_sig[sa_schema.Index]
|
||||
),
|
||||
new: (
|
||||
_constraint_sig[sa_schema.UniqueConstraint]
|
||||
| _constraint_sig[sa_schema.Index]
|
||||
),
|
||||
msg: str,
|
||||
):
|
||||
if is_index_sig(old):
|
||||
assert is_index_sig(new)
|
||||
|
||||
if autogen_context.run_object_filters(
|
||||
new.const, new.name, "index", False, old.const
|
||||
):
|
||||
log.info(
|
||||
"Detected changed index %r on %r: %s", old.name, tname, msg
|
||||
)
|
||||
modify_ops.ops.append(ops.DropIndexOp.from_index(old.const))
|
||||
modify_ops.ops.append(ops.CreateIndexOp.from_index(new.const))
|
||||
elif is_uq_sig(old):
|
||||
assert is_uq_sig(new)
|
||||
|
||||
if autogen_context.run_object_filters(
|
||||
new.const, new.name, "unique_constraint", False, old.const
|
||||
):
|
||||
log.info(
|
||||
"Detected changed unique constraint %r on %r: %s",
|
||||
old.name,
|
||||
tname,
|
||||
msg,
|
||||
)
|
||||
modify_ops.ops.append(
|
||||
ops.DropConstraintOp.from_constraint(old.const)
|
||||
)
|
||||
modify_ops.ops.append(
|
||||
ops.AddConstraintOp.from_constraint(new.const)
|
||||
)
|
||||
else:
|
||||
assert False
|
||||
|
||||
for removed_name in sorted(set(conn_names).difference(metadata_names)):
|
||||
conn_obj = conn_names[removed_name]
|
||||
if (
|
||||
is_uq_sig(conn_obj)
|
||||
and conn_obj.unnamed in unnamed_metadata_uniques
|
||||
):
|
||||
continue
|
||||
elif removed_name in doubled_constraints:
|
||||
conn_uq, conn_idx = doubled_constraints[removed_name]
|
||||
if (
|
||||
all(
|
||||
conn_idx.unnamed != meta_idx.unnamed
|
||||
for meta_idx in metadata_indexes_sig
|
||||
)
|
||||
and conn_uq.unnamed not in metadata_uniques_by_sig
|
||||
):
|
||||
obj_removed(conn_uq)
|
||||
obj_removed(conn_idx)
|
||||
else:
|
||||
obj_removed(conn_obj)
|
||||
|
||||
for existing_name in sorted(set(metadata_names).intersection(conn_names)):
|
||||
metadata_obj = metadata_names[existing_name]
|
||||
|
||||
if existing_name in doubled_constraints:
|
||||
conn_uq, conn_idx = doubled_constraints[existing_name]
|
||||
if is_index_sig(metadata_obj):
|
||||
conn_obj = conn_idx
|
||||
else:
|
||||
conn_obj = conn_uq
|
||||
else:
|
||||
conn_obj = conn_names[existing_name]
|
||||
|
||||
if type(conn_obj) != type(metadata_obj):
|
||||
obj_removed(conn_obj)
|
||||
obj_added(metadata_obj)
|
||||
else:
|
||||
# TODO: for plugins, let's do is_index_sig / is_uq_sig
|
||||
# here so we know index or unique, then
|
||||
# do a sub-dispatch,
|
||||
# autogen_context.comparators.dispatch("index")
|
||||
# or
|
||||
# autogen_context.comparators.dispatch("unique_constraint")
|
||||
#
|
||||
comparison = metadata_obj.compare_to_reflected(conn_obj)
|
||||
|
||||
if comparison.is_different:
|
||||
# constraint are different
|
||||
obj_changed(conn_obj, metadata_obj, comparison.message)
|
||||
elif comparison.is_skip:
|
||||
# constraint cannot be compared, skip them
|
||||
thing = (
|
||||
"index" if is_index_sig(conn_obj) else "unique constraint"
|
||||
)
|
||||
log.info(
|
||||
"Cannot compare %s %r, assuming equal and skipping. %s",
|
||||
thing,
|
||||
conn_obj.name,
|
||||
comparison.message,
|
||||
)
|
||||
else:
|
||||
# constraint are equal
|
||||
assert comparison.is_equal
|
||||
|
||||
for added_name in sorted(set(metadata_names).difference(conn_names)):
|
||||
obj = metadata_names[added_name]
|
||||
obj_added(obj)
|
||||
|
||||
for uq_sig in unnamed_metadata_uniques:
|
||||
if uq_sig not in conn_uniques_by_sig:
|
||||
obj_added(unnamed_metadata_uniques[uq_sig])
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _correct_for_uq_duplicates_uix(
|
||||
conn_unique_constraints,
|
||||
conn_indexes,
|
||||
metadata_unique_constraints,
|
||||
metadata_indexes,
|
||||
dialect,
|
||||
impl,
|
||||
):
|
||||
# dedupe unique indexes vs. constraints, since MySQL / Oracle
|
||||
# doesn't really have unique constraints as a separate construct.
|
||||
# but look in the metadata and try to maintain constructs
|
||||
# that already seem to be defined one way or the other
|
||||
# on that side. This logic was formerly local to MySQL dialect,
|
||||
# generalized to Oracle and others. See #276
|
||||
|
||||
# resolve final rendered name for unique constraints defined in the
|
||||
# metadata. this includes truncation of long names. naming convention
|
||||
# names currently should already be set as cons.name, however leave this
|
||||
# to the sqla_compat to decide.
|
||||
metadata_cons_names = [
|
||||
(sqla_compat._get_constraint_final_name(cons, dialect), cons)
|
||||
for cons in metadata_unique_constraints
|
||||
]
|
||||
|
||||
metadata_uq_names = {
|
||||
name for name, cons in metadata_cons_names if name is not None
|
||||
}
|
||||
|
||||
unnamed_metadata_uqs = {
|
||||
impl._create_metadata_constraint_sig(cons).unnamed
|
||||
for name, cons in metadata_cons_names
|
||||
if name is None
|
||||
}
|
||||
|
||||
metadata_ix_names = {
|
||||
sqla_compat._get_constraint_final_name(cons, dialect)
|
||||
for cons in metadata_indexes
|
||||
if cons.unique
|
||||
}
|
||||
|
||||
# for reflection side, names are in their final database form
|
||||
# already since they're from the database
|
||||
conn_ix_names = {cons.name: cons for cons in conn_indexes if cons.unique}
|
||||
|
||||
uqs_dupe_indexes = {
|
||||
cons.name: cons
|
||||
for cons in conn_unique_constraints
|
||||
if cons.info["duplicates_index"]
|
||||
}
|
||||
|
||||
for overlap in uqs_dupe_indexes:
|
||||
if overlap not in metadata_uq_names:
|
||||
if (
|
||||
impl._create_reflected_constraint_sig(
|
||||
uqs_dupe_indexes[overlap]
|
||||
).unnamed
|
||||
not in unnamed_metadata_uqs
|
||||
):
|
||||
conn_unique_constraints.discard(uqs_dupe_indexes[overlap])
|
||||
elif overlap not in metadata_ix_names:
|
||||
conn_indexes.discard(conn_ix_names[overlap])
|
||||
|
||||
|
||||
_IndexColumnSortingOps: Mapping[str, Any] = util.immutabledict(
|
||||
{
|
||||
"asc": expression.asc,
|
||||
"desc": expression.desc,
|
||||
"nulls_first": expression.nullsfirst,
|
||||
"nulls_last": expression.nullslast,
|
||||
"nullsfirst": expression.nullsfirst, # 1_3 name
|
||||
"nullslast": expression.nullslast, # 1_3 name
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _make_index(
|
||||
impl: DefaultImpl, params: ReflectedIndex, conn_table: Table
|
||||
) -> Optional[Index]:
|
||||
exprs: list[Union[Column[Any], TextClause]] = []
|
||||
sorting = params.get("column_sorting")
|
||||
|
||||
for num, col_name in enumerate(params["column_names"]):
|
||||
item: Union[Column[Any], TextClause]
|
||||
if col_name is None:
|
||||
assert "expressions" in params
|
||||
name = params["expressions"][num]
|
||||
item = text(name)
|
||||
else:
|
||||
name = col_name
|
||||
item = conn_table.c[col_name]
|
||||
if sorting and name in sorting:
|
||||
for operator in sorting[name]:
|
||||
if operator in _IndexColumnSortingOps:
|
||||
item = _IndexColumnSortingOps[operator](item)
|
||||
exprs.append(item)
|
||||
ix = sa_schema.Index(
|
||||
params["name"],
|
||||
*exprs,
|
||||
unique=params["unique"],
|
||||
_table=conn_table,
|
||||
**impl.adjust_reflected_dialect_options(params, "index"),
|
||||
)
|
||||
if "duplicates_constraint" in params:
|
||||
ix.info["duplicates_constraint"] = params["duplicates_constraint"]
|
||||
return ix
|
||||
|
||||
|
||||
def _make_unique_constraint(
|
||||
impl: DefaultImpl, params: ReflectedUniqueConstraint, conn_table: Table
|
||||
) -> UniqueConstraint:
|
||||
uq = sa_schema.UniqueConstraint(
|
||||
*[conn_table.c[cname] for cname in params["column_names"]],
|
||||
name=params["name"],
|
||||
**impl.adjust_reflected_dialect_options(params, "unique_constraint"),
|
||||
)
|
||||
if "duplicates_index" in params:
|
||||
uq.info["duplicates_index"] = params["duplicates_index"]
|
||||
|
||||
return uq
|
||||
|
||||
|
||||
def _make_foreign_key(
|
||||
params: ReflectedForeignKeyConstraint, conn_table: Table
|
||||
) -> ForeignKeyConstraint:
|
||||
tname = params["referred_table"]
|
||||
if params["referred_schema"]:
|
||||
tname = "%s.%s" % (params["referred_schema"], tname)
|
||||
|
||||
options = params.get("options", {})
|
||||
|
||||
const = sa_schema.ForeignKeyConstraint(
|
||||
[conn_table.c[cname] for cname in params["constrained_columns"]],
|
||||
["%s.%s" % (tname, n) for n in params["referred_columns"]],
|
||||
onupdate=options.get("onupdate"),
|
||||
ondelete=options.get("ondelete"),
|
||||
deferrable=options.get("deferrable"),
|
||||
initially=options.get("initially"),
|
||||
name=params["name"],
|
||||
)
|
||||
|
||||
referred_schema = params["referred_schema"]
|
||||
referred_table = params["referred_table"]
|
||||
|
||||
remote_table_key = sqla_compat._get_table_key(
|
||||
referred_table, referred_schema
|
||||
)
|
||||
if remote_table_key not in conn_table.metadata:
|
||||
# create a placeholder table
|
||||
sa_schema.Table(
|
||||
referred_table,
|
||||
conn_table.metadata,
|
||||
schema=(
|
||||
referred_schema
|
||||
if referred_schema is not None
|
||||
else sa_schema.BLANK_SCHEMA
|
||||
),
|
||||
*[
|
||||
sa_schema.Column(remote, conn_table.c[local].type)
|
||||
for local, remote in zip(
|
||||
params["constrained_columns"], params["referred_columns"]
|
||||
)
|
||||
],
|
||||
info={"alembic_placeholder": True},
|
||||
)
|
||||
elif conn_table.metadata.tables[remote_table_key].info.get(
|
||||
"alembic_placeholder"
|
||||
):
|
||||
# table exists and is a placeholder; ensure needed columns are present
|
||||
placeholder_table = conn_table.metadata.tables[remote_table_key]
|
||||
for local, remote in zip(
|
||||
params["constrained_columns"], params["referred_columns"]
|
||||
):
|
||||
if remote not in placeholder_table.c:
|
||||
placeholder_table.append_column(
|
||||
sa_schema.Column(remote, conn_table.c[local].type)
|
||||
)
|
||||
|
||||
# needed by 0.7
|
||||
conn_table.append_constraint(const)
|
||||
return const
|
||||
|
||||
|
||||
def _compare_foreign_keys(
|
||||
autogen_context: AutogenContext,
|
||||
modify_table_ops: ModifyTableOps,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
conn_table: Table,
|
||||
metadata_table: Table,
|
||||
) -> PriorityDispatchResult:
|
||||
# if we're doing CREATE TABLE, all FKs are created
|
||||
# inline within the table def
|
||||
|
||||
if conn_table is None or metadata_table is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
inspector = autogen_context.inspector
|
||||
metadata_fks = {
|
||||
fk
|
||||
for fk in metadata_table.constraints
|
||||
if isinstance(fk, sa_schema.ForeignKeyConstraint)
|
||||
}
|
||||
|
||||
conn_fks_list = [
|
||||
fk
|
||||
for fk in _InspectorConv(inspector).get_foreign_keys(
|
||||
tname, schema=schema
|
||||
)
|
||||
if autogen_context.run_name_filters(
|
||||
fk["name"],
|
||||
"foreign_key_constraint",
|
||||
{"table_name": tname, "schema_name": schema},
|
||||
)
|
||||
]
|
||||
|
||||
conn_fks = {
|
||||
_make_foreign_key(const, conn_table) for const in conn_fks_list
|
||||
}
|
||||
|
||||
impl = autogen_context.migration_context.impl
|
||||
|
||||
# give the dialect a chance to correct the FKs to match more
|
||||
# closely
|
||||
autogen_context.migration_context.impl.correct_for_autogen_foreignkeys(
|
||||
conn_fks, metadata_fks
|
||||
)
|
||||
|
||||
metadata_fks_sig = {
|
||||
impl._create_metadata_constraint_sig(fk) for fk in metadata_fks
|
||||
}
|
||||
|
||||
conn_fks_sig = {
|
||||
impl._create_reflected_constraint_sig(fk) for fk in conn_fks
|
||||
}
|
||||
|
||||
# check if reflected FKs include options, indicating the backend
|
||||
# can reflect FK options
|
||||
if conn_fks_list and "options" in conn_fks_list[0]:
|
||||
conn_fks_by_sig = {c.unnamed: c for c in conn_fks_sig}
|
||||
metadata_fks_by_sig = {c.unnamed: c for c in metadata_fks_sig}
|
||||
else:
|
||||
# otherwise compare by sig without options added
|
||||
conn_fks_by_sig = {c.unnamed_no_options: c for c in conn_fks_sig}
|
||||
metadata_fks_by_sig = {
|
||||
c.unnamed_no_options: c for c in metadata_fks_sig
|
||||
}
|
||||
|
||||
metadata_fks_by_name = {
|
||||
c.name: c for c in metadata_fks_sig if c.name is not None
|
||||
}
|
||||
conn_fks_by_name = {c.name: c for c in conn_fks_sig if c.name is not None}
|
||||
|
||||
def _add_fk(obj, compare_to):
|
||||
if autogen_context.run_object_filters(
|
||||
obj.const, obj.name, "foreign_key_constraint", False, compare_to
|
||||
):
|
||||
modify_table_ops.ops.append(
|
||||
ops.CreateForeignKeyOp.from_constraint(const.const)
|
||||
)
|
||||
|
||||
log.info(
|
||||
"Detected added foreign key (%s)(%s) on table %s%s",
|
||||
", ".join(obj.source_columns),
|
||||
", ".join(obj.target_columns),
|
||||
"%s." % obj.source_schema if obj.source_schema else "",
|
||||
obj.source_table,
|
||||
)
|
||||
|
||||
def _remove_fk(obj, compare_to):
|
||||
if autogen_context.run_object_filters(
|
||||
obj.const, obj.name, "foreign_key_constraint", True, compare_to
|
||||
):
|
||||
modify_table_ops.ops.append(
|
||||
ops.DropConstraintOp.from_constraint(obj.const)
|
||||
)
|
||||
log.info(
|
||||
"Detected removed foreign key (%s)(%s) on table %s%s",
|
||||
", ".join(obj.source_columns),
|
||||
", ".join(obj.target_columns),
|
||||
"%s." % obj.source_schema if obj.source_schema else "",
|
||||
obj.source_table,
|
||||
)
|
||||
|
||||
# so far it appears we don't need to do this by name at all.
|
||||
# SQLite doesn't preserve constraint names anyway
|
||||
|
||||
for removed_sig in set(conn_fks_by_sig).difference(metadata_fks_by_sig):
|
||||
const = conn_fks_by_sig[removed_sig]
|
||||
if removed_sig not in metadata_fks_by_sig:
|
||||
compare_to = (
|
||||
metadata_fks_by_name[const.name].const
|
||||
if const.name and const.name in metadata_fks_by_name
|
||||
else None
|
||||
)
|
||||
_remove_fk(const, compare_to)
|
||||
|
||||
for added_sig in set(metadata_fks_by_sig).difference(conn_fks_by_sig):
|
||||
const = metadata_fks_by_sig[added_sig]
|
||||
if added_sig not in conn_fks_by_sig:
|
||||
compare_to = (
|
||||
conn_fks_by_name[const.name].const
|
||||
if const.name and const.name in conn_fks_by_name
|
||||
else None
|
||||
)
|
||||
_add_fk(const, compare_to)
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _compare_nullable(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
metadata_col_nullable = metadata_col.nullable
|
||||
conn_col_nullable = conn_col.nullable
|
||||
alter_column_op.existing_nullable = conn_col_nullable
|
||||
|
||||
if conn_col_nullable is not metadata_col_nullable:
|
||||
if (
|
||||
sqla_compat._server_default_is_computed(
|
||||
metadata_col.server_default, conn_col.server_default
|
||||
)
|
||||
and sqla_compat._nullability_might_be_unset(metadata_col)
|
||||
or (
|
||||
sqla_compat._server_default_is_identity(
|
||||
metadata_col.server_default, conn_col.server_default
|
||||
)
|
||||
)
|
||||
):
|
||||
log.info(
|
||||
"Ignoring nullable change on identity column '%s.%s'",
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
else:
|
||||
alter_column_op.modify_nullable = metadata_col_nullable
|
||||
log.info(
|
||||
"Detected %s on column '%s.%s'",
|
||||
"NULL" if metadata_col_nullable else "NOT NULL",
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
# column nullablity changed, no further nullable checks needed
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def setup(plugin: Plugin) -> None:
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_indexes_and_uniques,
|
||||
"table",
|
||||
"indexes",
|
||||
)
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_foreign_keys,
|
||||
"table",
|
||||
"foreignkeys",
|
||||
)
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_nullable,
|
||||
"column",
|
||||
"nullable",
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
# mypy: allow-untyped-calls
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from ...util import PriorityDispatchResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
from ...autogenerate.api import AutogenContext
|
||||
from ...operations.ops import UpgradeOps
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _produce_net_changes(
|
||||
autogen_context: AutogenContext, upgrade_ops: UpgradeOps
|
||||
) -> PriorityDispatchResult:
|
||||
connection = autogen_context.connection
|
||||
assert connection is not None
|
||||
include_schemas = autogen_context.opts.get("include_schemas", False)
|
||||
|
||||
inspector: Inspector = inspect(connection)
|
||||
|
||||
default_schema = connection.dialect.default_schema_name
|
||||
schemas: Set[Optional[str]]
|
||||
if include_schemas:
|
||||
schemas = set(inspector.get_schema_names())
|
||||
# replace default schema name with None
|
||||
schemas.discard("information_schema")
|
||||
# replace the "default" schema with None
|
||||
schemas.discard(default_schema)
|
||||
schemas.add(None)
|
||||
else:
|
||||
schemas = {None}
|
||||
|
||||
schemas = {
|
||||
s for s in schemas if autogen_context.run_name_filters(s, "schema", {})
|
||||
}
|
||||
|
||||
assert autogen_context.dialect is not None
|
||||
autogen_context.comparators.dispatch(
|
||||
"schema", qualifier=autogen_context.dialect.name
|
||||
)(autogen_context, upgrade_ops, schemas)
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def setup(plugin: Plugin) -> None:
|
||||
plugin.add_autogenerate_comparator(
|
||||
_produce_net_changes,
|
||||
"autogenerate",
|
||||
)
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from types import NoneType
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy.sql.schema import DefaultClause
|
||||
|
||||
from ... import util
|
||||
from ...util import DispatchPriority
|
||||
from ...util import PriorityDispatchResult
|
||||
from ...util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import Column
|
||||
|
||||
from ...autogenerate.api import AutogenContext
|
||||
from ...operations.ops import AlterColumnOp
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _render_server_default_for_compare(
|
||||
metadata_default: Optional[Any], autogen_context: AutogenContext
|
||||
) -> Optional[str]:
|
||||
if isinstance(metadata_default, sa_schema.DefaultClause):
|
||||
if isinstance(metadata_default.arg, str):
|
||||
metadata_default = metadata_default.arg
|
||||
else:
|
||||
metadata_default = str(
|
||||
metadata_default.arg.compile(
|
||||
dialect=autogen_context.dialect,
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
)
|
||||
if isinstance(metadata_default, str):
|
||||
return metadata_default
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_computed_default(sqltext: str) -> str:
|
||||
"""we want to warn if a computed sql expression has changed. however
|
||||
we don't want false positives and the warning is not that critical.
|
||||
so filter out most forms of variability from the SQL text.
|
||||
|
||||
"""
|
||||
|
||||
return re.sub(r"[ \(\)'\"`\[\]\t\r\n]", "", sqltext).lower()
|
||||
|
||||
|
||||
def _compare_computed_default(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: str,
|
||||
cname: str,
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
|
||||
metadata_default = metadata_col.server_default
|
||||
conn_col_default = conn_col.server_default
|
||||
if conn_col_default is None and metadata_default is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
if sqla_compat._server_default_is_computed(
|
||||
conn_col_default
|
||||
) and not sqla_compat._server_default_is_computed(metadata_default):
|
||||
_warn_computed_not_supported(tname, cname)
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
if not sqla_compat._server_default_is_computed(metadata_default):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
rendered_metadata_default = str(
|
||||
cast(sa_schema.Computed, metadata_col.server_default).sqltext.compile(
|
||||
dialect=autogen_context.dialect,
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
)
|
||||
|
||||
# since we cannot change computed columns, we do only a crude comparison
|
||||
# here where we try to eliminate syntactical differences in order to
|
||||
# get a minimal comparison just to emit a warning.
|
||||
|
||||
rendered_metadata_default = _normalize_computed_default(
|
||||
rendered_metadata_default
|
||||
)
|
||||
|
||||
if isinstance(conn_col.server_default, sa_schema.Computed):
|
||||
rendered_conn_default = str(
|
||||
conn_col.server_default.sqltext.compile(
|
||||
dialect=autogen_context.dialect,
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
)
|
||||
rendered_conn_default = _normalize_computed_default(
|
||||
rendered_conn_default
|
||||
)
|
||||
else:
|
||||
rendered_conn_default = ""
|
||||
|
||||
if rendered_metadata_default != rendered_conn_default:
|
||||
_warn_computed_not_supported(tname, cname)
|
||||
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
|
||||
def _warn_computed_not_supported(tname: str, cname: str) -> None:
|
||||
util.warn("Computed default on %s.%s cannot be modified" % (tname, cname))
|
||||
|
||||
|
||||
def _compare_identity_default(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
skip: Sequence[str] = (
|
||||
"order",
|
||||
"on_null",
|
||||
"oracle_order",
|
||||
"oracle_on_null",
|
||||
),
|
||||
) -> PriorityDispatchResult:
|
||||
|
||||
metadata_default = metadata_col.server_default
|
||||
conn_col_default = conn_col.server_default
|
||||
if (
|
||||
conn_col_default is None
|
||||
and metadata_default is None
|
||||
or not sqla_compat._server_default_is_identity(
|
||||
metadata_default, conn_col_default
|
||||
)
|
||||
):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
assert isinstance(
|
||||
metadata_col.server_default,
|
||||
(sa_schema.Identity, sa_schema.Sequence, NoneType),
|
||||
)
|
||||
assert isinstance(
|
||||
conn_col.server_default,
|
||||
(sa_schema.Identity, sa_schema.Sequence, NoneType),
|
||||
)
|
||||
|
||||
impl = autogen_context.migration_context.impl
|
||||
diff, _, is_alter = impl._compare_identity_default( # type: ignore[no-untyped-call] # noqa: E501
|
||||
metadata_col.server_default, conn_col.server_default
|
||||
)
|
||||
|
||||
if is_alter:
|
||||
alter_column_op.modify_server_default = metadata_default
|
||||
if diff:
|
||||
log.info(
|
||||
"Detected server default on column '%s.%s': "
|
||||
"identity options attributes %s",
|
||||
tname,
|
||||
cname,
|
||||
sorted(diff),
|
||||
)
|
||||
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _user_compare_server_default(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
|
||||
metadata_default = metadata_col.server_default
|
||||
conn_col_default = conn_col.server_default
|
||||
if conn_col_default is None and metadata_default is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
alter_column_op.existing_server_default = conn_col_default
|
||||
|
||||
migration_context = autogen_context.migration_context
|
||||
|
||||
if migration_context._user_compare_server_default is False:
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
if not callable(migration_context._user_compare_server_default):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
rendered_metadata_default = _render_server_default_for_compare(
|
||||
metadata_default, autogen_context
|
||||
)
|
||||
rendered_conn_default = (
|
||||
cast(Any, conn_col_default).arg.text if conn_col_default else None
|
||||
)
|
||||
|
||||
is_diff = migration_context._user_compare_server_default(
|
||||
migration_context,
|
||||
conn_col,
|
||||
metadata_col,
|
||||
rendered_conn_default,
|
||||
metadata_col.server_default,
|
||||
rendered_metadata_default,
|
||||
)
|
||||
if is_diff:
|
||||
alter_column_op.modify_server_default = metadata_default
|
||||
log.info(
|
||||
"User defined function %s detected "
|
||||
"server default on column '%s.%s'",
|
||||
migration_context._user_compare_server_default,
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
return PriorityDispatchResult.STOP
|
||||
elif is_diff is False:
|
||||
# if user compare server_default returns False and not None,
|
||||
# it means "dont do any more server_default comparison"
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _dialect_impl_compare_server_default(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
"""use dialect.impl.compare_server_default.
|
||||
|
||||
This would in theory not be needed. however we dont know if any
|
||||
third party libraries haven't made their own alembic dialect and
|
||||
implemented this method.
|
||||
|
||||
"""
|
||||
metadata_default = metadata_col.server_default
|
||||
conn_col_default = conn_col.server_default
|
||||
if conn_col_default is None and metadata_default is None:
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
# this is already done by _user_compare_server_default,
|
||||
# but doing it here also for unit tests that want to call
|
||||
# _dialect_impl_compare_server_default directly
|
||||
alter_column_op.existing_server_default = conn_col_default
|
||||
|
||||
if not isinstance(
|
||||
metadata_default, (DefaultClause, NoneType)
|
||||
) or not isinstance(conn_col_default, (DefaultClause, NoneType)):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
migration_context = autogen_context.migration_context
|
||||
|
||||
rendered_metadata_default = _render_server_default_for_compare(
|
||||
metadata_default, autogen_context
|
||||
)
|
||||
rendered_conn_default = (
|
||||
cast(Any, conn_col_default).arg.text if conn_col_default else None
|
||||
)
|
||||
|
||||
is_diff = migration_context.impl.compare_server_default( # type: ignore[no-untyped-call] # noqa: E501
|
||||
conn_col,
|
||||
metadata_col,
|
||||
rendered_metadata_default,
|
||||
rendered_conn_default,
|
||||
)
|
||||
if is_diff:
|
||||
alter_column_op.modify_server_default = metadata_default
|
||||
log.info(
|
||||
"Dialect impl %s detected server default on column '%s.%s'",
|
||||
migration_context.impl,
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
return PriorityDispatchResult.STOP
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _setup_autoincrement(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: quoted_name,
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
if metadata_col.table._autoincrement_column is metadata_col:
|
||||
alter_column_op.kw["autoincrement"] = True
|
||||
elif metadata_col.autoincrement is True:
|
||||
alter_column_op.kw["autoincrement"] = True
|
||||
elif metadata_col.autoincrement is False:
|
||||
alter_column_op.kw["autoincrement"] = False
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def setup(plugin: Plugin) -> None:
|
||||
plugin.add_autogenerate_comparator(
|
||||
_user_compare_server_default,
|
||||
"column",
|
||||
"server_default",
|
||||
priority=DispatchPriority.FIRST,
|
||||
)
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_computed_default,
|
||||
"column",
|
||||
"server_default",
|
||||
)
|
||||
|
||||
plugin.add_autogenerate_comparator(
|
||||
_compare_identity_default,
|
||||
"column",
|
||||
"server_default",
|
||||
)
|
||||
|
||||
plugin.add_autogenerate_comparator(
|
||||
_setup_autoincrement,
|
||||
"column",
|
||||
"server_default",
|
||||
)
|
||||
plugin.add_autogenerate_comparator(
|
||||
_dialect_impl_compare_server_default,
|
||||
"column",
|
||||
"server_default",
|
||||
priority=DispatchPriority.LAST,
|
||||
)
|
||||
@@ -0,0 +1,316 @@
|
||||
# mypy: allow-untyped-calls
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Iterator
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy.util import OrderedSet
|
||||
|
||||
from .util import _InspectorConv
|
||||
from ...operations import ops
|
||||
from ...util import PriorityDispatchResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
from ...autogenerate.api import AutogenContext
|
||||
from ...operations.ops import ModifyTableOps
|
||||
from ...operations.ops import UpgradeOps
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _autogen_for_tables(
|
||||
autogen_context: AutogenContext,
|
||||
upgrade_ops: UpgradeOps,
|
||||
schemas: Set[Optional[str]],
|
||||
) -> PriorityDispatchResult:
|
||||
inspector = autogen_context.inspector
|
||||
|
||||
conn_table_names: Set[Tuple[Optional[str], str]] = set()
|
||||
|
||||
version_table_schema = (
|
||||
autogen_context.migration_context.version_table_schema
|
||||
)
|
||||
version_table = autogen_context.migration_context.version_table
|
||||
|
||||
for schema_name in schemas:
|
||||
tables = available = set(inspector.get_table_names(schema=schema_name))
|
||||
if schema_name == version_table_schema:
|
||||
tables = tables.difference(
|
||||
[autogen_context.migration_context.version_table]
|
||||
)
|
||||
|
||||
tablenames = [
|
||||
tname
|
||||
for tname in tables
|
||||
if autogen_context.run_name_filters(
|
||||
tname, "table", {"schema_name": schema_name}
|
||||
)
|
||||
]
|
||||
|
||||
conn_table_names.update((schema_name, tname) for tname in tablenames)
|
||||
|
||||
inspector = autogen_context.inspector
|
||||
insp = _InspectorConv(inspector)
|
||||
insp.pre_cache_tables(schema_name, tablenames, available)
|
||||
|
||||
metadata_table_names = OrderedSet(
|
||||
[(table.schema, table.name) for table in autogen_context.sorted_tables]
|
||||
).difference([(version_table_schema, version_table)])
|
||||
|
||||
_compare_tables(
|
||||
conn_table_names,
|
||||
metadata_table_names,
|
||||
inspector,
|
||||
upgrade_ops,
|
||||
autogen_context,
|
||||
)
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _compare_tables(
|
||||
conn_table_names: set[tuple[str | None, str]],
|
||||
metadata_table_names: set[tuple[str | None, str]],
|
||||
inspector: Inspector,
|
||||
upgrade_ops: UpgradeOps,
|
||||
autogen_context: AutogenContext,
|
||||
) -> None:
|
||||
default_schema = inspector.bind.dialect.default_schema_name
|
||||
|
||||
# tables coming from the connection will not have "schema"
|
||||
# set if it matches default_schema_name; so we need a list
|
||||
# of table names from local metadata that also have "None" if schema
|
||||
# == default_schema_name. Most setups will be like this anyway but
|
||||
# some are not (see #170)
|
||||
metadata_table_names_no_dflt_schema = OrderedSet(
|
||||
[
|
||||
(schema if schema != default_schema else None, tname)
|
||||
for schema, tname in metadata_table_names
|
||||
]
|
||||
)
|
||||
|
||||
# to adjust for the MetaData collection storing the tables either
|
||||
# as "schemaname.tablename" or just "tablename", create a new lookup
|
||||
# which will match the "non-default-schema" keys to the Table object.
|
||||
tname_to_table = {
|
||||
no_dflt_schema: autogen_context.table_key_to_table[
|
||||
sa_schema._get_table_key(tname, schema)
|
||||
]
|
||||
for no_dflt_schema, (schema, tname) in zip(
|
||||
metadata_table_names_no_dflt_schema, metadata_table_names
|
||||
)
|
||||
}
|
||||
metadata_table_names = metadata_table_names_no_dflt_schema
|
||||
|
||||
for s, tname in metadata_table_names.difference(conn_table_names):
|
||||
name = "%s.%s" % (s, tname) if s else tname
|
||||
metadata_table = tname_to_table[(s, tname)]
|
||||
if autogen_context.run_object_filters(
|
||||
metadata_table, tname, "table", False, None
|
||||
):
|
||||
upgrade_ops.ops.append(
|
||||
ops.CreateTableOp.from_table(metadata_table)
|
||||
)
|
||||
log.info("Detected added table %r", name)
|
||||
modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
|
||||
|
||||
autogen_context.comparators.dispatch(
|
||||
"table", qualifier=autogen_context.dialect.name
|
||||
)(
|
||||
autogen_context,
|
||||
modify_table_ops,
|
||||
s,
|
||||
tname,
|
||||
None,
|
||||
metadata_table,
|
||||
)
|
||||
if not modify_table_ops.is_empty():
|
||||
upgrade_ops.ops.append(modify_table_ops)
|
||||
|
||||
removal_metadata = sa_schema.MetaData()
|
||||
for s, tname in conn_table_names.difference(metadata_table_names):
|
||||
name = sa_schema._get_table_key(tname, s)
|
||||
|
||||
# a name might be present already if a previous reflection pulled
|
||||
# this table in via foreign key constraint
|
||||
exists = name in removal_metadata.tables
|
||||
t = sa_schema.Table(tname, removal_metadata, schema=s)
|
||||
|
||||
if not exists:
|
||||
event.listen(
|
||||
t,
|
||||
"column_reflect",
|
||||
# fmt: off
|
||||
autogen_context.migration_context.impl.
|
||||
_compat_autogen_column_reflect
|
||||
(inspector),
|
||||
# fmt: on
|
||||
)
|
||||
_InspectorConv(inspector).reflect_table(t)
|
||||
if autogen_context.run_object_filters(t, tname, "table", True, None):
|
||||
modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
|
||||
|
||||
autogen_context.comparators.dispatch(
|
||||
"table", qualifier=autogen_context.dialect.name
|
||||
)(autogen_context, modify_table_ops, s, tname, t, None)
|
||||
if not modify_table_ops.is_empty():
|
||||
upgrade_ops.ops.append(modify_table_ops)
|
||||
|
||||
upgrade_ops.ops.append(ops.DropTableOp.from_table(t))
|
||||
log.info("Detected removed table %r", name)
|
||||
|
||||
existing_tables = conn_table_names.intersection(metadata_table_names)
|
||||
|
||||
existing_metadata = sa_schema.MetaData()
|
||||
conn_column_info = {}
|
||||
for s, tname in existing_tables:
|
||||
name = sa_schema._get_table_key(tname, s)
|
||||
exists = name in existing_metadata.tables
|
||||
|
||||
# a name might be present already if a previous reflection pulled
|
||||
# this table in via foreign key constraint
|
||||
t = sa_schema.Table(tname, existing_metadata, schema=s)
|
||||
if not exists:
|
||||
event.listen(
|
||||
t,
|
||||
"column_reflect",
|
||||
# fmt: off
|
||||
autogen_context.migration_context.impl.
|
||||
_compat_autogen_column_reflect(inspector),
|
||||
# fmt: on
|
||||
)
|
||||
_InspectorConv(inspector).reflect_table(t)
|
||||
|
||||
conn_column_info[(s, tname)] = t
|
||||
|
||||
for s, tname in sorted(existing_tables, key=lambda x: (x[0] or "", x[1])):
|
||||
s = s or None
|
||||
name = "%s.%s" % (s, tname) if s else tname
|
||||
metadata_table = tname_to_table[(s, tname)]
|
||||
conn_table = existing_metadata.tables[name]
|
||||
|
||||
if autogen_context.run_object_filters(
|
||||
metadata_table, tname, "table", False, conn_table
|
||||
):
|
||||
modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
|
||||
with _compare_columns(
|
||||
s,
|
||||
tname,
|
||||
conn_table,
|
||||
metadata_table,
|
||||
modify_table_ops,
|
||||
autogen_context,
|
||||
inspector,
|
||||
):
|
||||
autogen_context.comparators.dispatch(
|
||||
"table", qualifier=autogen_context.dialect.name
|
||||
)(
|
||||
autogen_context,
|
||||
modify_table_ops,
|
||||
s,
|
||||
tname,
|
||||
conn_table,
|
||||
metadata_table,
|
||||
)
|
||||
|
||||
if not modify_table_ops.is_empty():
|
||||
upgrade_ops.ops.append(modify_table_ops)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _compare_columns(
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
conn_table: Table,
|
||||
metadata_table: Table,
|
||||
modify_table_ops: ModifyTableOps,
|
||||
autogen_context: AutogenContext,
|
||||
inspector: Inspector,
|
||||
) -> Iterator[None]:
|
||||
name = "%s.%s" % (schema, tname) if schema else tname
|
||||
metadata_col_names = OrderedSet(
|
||||
c.name for c in metadata_table.c if not c.system
|
||||
)
|
||||
metadata_cols_by_name = {
|
||||
c.name: c for c in metadata_table.c if not c.system
|
||||
}
|
||||
|
||||
conn_col_names = {
|
||||
c.name: c
|
||||
for c in conn_table.c
|
||||
if autogen_context.run_name_filters(
|
||||
c.name, "column", {"table_name": tname, "schema_name": schema}
|
||||
)
|
||||
}
|
||||
|
||||
for cname in metadata_col_names.difference(conn_col_names):
|
||||
if autogen_context.run_object_filters(
|
||||
metadata_cols_by_name[cname], cname, "column", False, None
|
||||
):
|
||||
modify_table_ops.ops.append(
|
||||
ops.AddColumnOp.from_column_and_tablename(
|
||||
schema, tname, metadata_cols_by_name[cname]
|
||||
)
|
||||
)
|
||||
log.info("Detected added column '%s.%s'", name, cname)
|
||||
|
||||
for colname in metadata_col_names.intersection(conn_col_names):
|
||||
metadata_col = metadata_cols_by_name[colname]
|
||||
conn_col = conn_table.c[colname]
|
||||
if not autogen_context.run_object_filters(
|
||||
metadata_col, colname, "column", False, conn_col
|
||||
):
|
||||
continue
|
||||
alter_column_op = ops.AlterColumnOp(tname, colname, schema=schema)
|
||||
|
||||
autogen_context.comparators.dispatch(
|
||||
"column", qualifier=autogen_context.dialect.name
|
||||
)(
|
||||
autogen_context,
|
||||
alter_column_op,
|
||||
schema,
|
||||
tname,
|
||||
colname,
|
||||
conn_col,
|
||||
metadata_col,
|
||||
)
|
||||
|
||||
if alter_column_op.has_changes():
|
||||
modify_table_ops.ops.append(alter_column_op)
|
||||
|
||||
yield
|
||||
|
||||
for cname in set(conn_col_names).difference(metadata_col_names):
|
||||
if autogen_context.run_object_filters(
|
||||
conn_table.c[cname], cname, "column", True, None
|
||||
):
|
||||
modify_table_ops.ops.append(
|
||||
ops.DropColumnOp.from_column_and_tablename(
|
||||
schema, tname, conn_table.c[cname]
|
||||
)
|
||||
)
|
||||
log.info("Detected removed column '%s.%s'", name, cname)
|
||||
|
||||
|
||||
def setup(plugin: Plugin) -> None:
|
||||
|
||||
plugin.add_autogenerate_comparator(
|
||||
_autogen_for_tables,
|
||||
"schema",
|
||||
"tables",
|
||||
)
|
||||
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import types as sqltypes
|
||||
|
||||
from ...util import DispatchPriority
|
||||
from ...util import PriorityDispatchResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import Column
|
||||
|
||||
from ...autogenerate.api import AutogenContext
|
||||
from ...operations.ops import AlterColumnOp
|
||||
from ...runtime.plugins import Plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _compare_type_setup(
|
||||
alter_column_op: AlterColumnOp,
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> bool:
|
||||
|
||||
conn_type = conn_col.type
|
||||
alter_column_op.existing_type = conn_type
|
||||
metadata_type = metadata_col.type
|
||||
if conn_type._type_affinity is sqltypes.NullType:
|
||||
log.info(
|
||||
"Couldn't determine database type for column '%s.%s'",
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
return False
|
||||
if metadata_type._type_affinity is sqltypes.NullType:
|
||||
log.info(
|
||||
"Column '%s.%s' has no type within the model; can't compare",
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _user_compare_type(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
|
||||
migration_context = autogen_context.migration_context
|
||||
|
||||
if migration_context._user_compare_type is False:
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
if not _compare_type_setup(
|
||||
alter_column_op, tname, cname, conn_col, metadata_col
|
||||
):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
if not callable(migration_context._user_compare_type):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
is_diff = migration_context._user_compare_type(
|
||||
migration_context,
|
||||
conn_col,
|
||||
metadata_col,
|
||||
conn_col.type,
|
||||
metadata_col.type,
|
||||
)
|
||||
if is_diff:
|
||||
alter_column_op.modify_type = metadata_col.type
|
||||
log.info(
|
||||
"Detected type change from %r to %r on '%s.%s'",
|
||||
conn_col.type,
|
||||
metadata_col.type,
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
return PriorityDispatchResult.STOP
|
||||
elif is_diff is False:
|
||||
# if user compare type returns False and not None,
|
||||
# it means "dont do any more type comparison"
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def _dialect_impl_compare_type(
|
||||
autogen_context: AutogenContext,
|
||||
alter_column_op: AlterColumnOp,
|
||||
schema: Optional[str],
|
||||
tname: Union[quoted_name, str],
|
||||
cname: Union[quoted_name, str],
|
||||
conn_col: Column[Any],
|
||||
metadata_col: Column[Any],
|
||||
) -> PriorityDispatchResult:
|
||||
|
||||
if not _compare_type_setup(
|
||||
alter_column_op, tname, cname, conn_col, metadata_col
|
||||
):
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
migration_context = autogen_context.migration_context
|
||||
is_diff = migration_context.impl.compare_type(conn_col, metadata_col)
|
||||
|
||||
if is_diff:
|
||||
alter_column_op.modify_type = metadata_col.type
|
||||
log.info(
|
||||
"Detected type change from %r to %r on '%s.%s'",
|
||||
conn_col.type,
|
||||
metadata_col.type,
|
||||
tname,
|
||||
cname,
|
||||
)
|
||||
return PriorityDispatchResult.STOP
|
||||
|
||||
return PriorityDispatchResult.CONTINUE
|
||||
|
||||
|
||||
def setup(plugin: Plugin) -> None:
|
||||
plugin.add_autogenerate_comparator(
|
||||
_user_compare_type,
|
||||
"column",
|
||||
"types",
|
||||
priority=DispatchPriority.FIRST,
|
||||
)
|
||||
plugin.add_autogenerate_comparator(
|
||||
_dialect_impl_compare_type,
|
||||
"column",
|
||||
"types",
|
||||
priority=DispatchPriority.LAST,
|
||||
)
|
||||
@@ -0,0 +1,314 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Collection
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.sql.elements import conv
|
||||
from typing_extensions import Self
|
||||
|
||||
from ...util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy.engine import Inspector
|
||||
from sqlalchemy.engine.interfaces import ReflectedForeignKeyConstraint
|
||||
from sqlalchemy.engine.interfaces import ReflectedIndex
|
||||
from sqlalchemy.engine.interfaces import ReflectedUniqueConstraint
|
||||
from sqlalchemy.engine.reflection import _ReflectionInfo
|
||||
|
||||
_INSP_KEYS = (
|
||||
"columns",
|
||||
"pk_constraint",
|
||||
"foreign_keys",
|
||||
"indexes",
|
||||
"unique_constraints",
|
||||
"table_comment",
|
||||
"check_constraints",
|
||||
"table_options",
|
||||
)
|
||||
_CONSTRAINT_INSP_KEYS = (
|
||||
"pk_constraint",
|
||||
"foreign_keys",
|
||||
"indexes",
|
||||
"unique_constraints",
|
||||
"check_constraints",
|
||||
)
|
||||
|
||||
|
||||
class _InspectorConv:
|
||||
__slots__ = ("inspector",)
|
||||
|
||||
def __new__(cls, inspector: Inspector) -> Self:
|
||||
obj: Any
|
||||
if sqla_compat.sqla_2:
|
||||
obj = object.__new__(_SQLA2InspectorConv)
|
||||
_SQLA2InspectorConv.__init__(obj, inspector)
|
||||
else:
|
||||
obj = object.__new__(_LegacyInspectorConv)
|
||||
_LegacyInspectorConv.__init__(obj, inspector)
|
||||
return cast(Self, obj)
|
||||
|
||||
def __init__(self, inspector: Inspector):
|
||||
self.inspector = inspector
|
||||
|
||||
def pre_cache_tables(
|
||||
self,
|
||||
schema: str | None,
|
||||
tablenames: list[str],
|
||||
all_available_tablenames: Collection[str],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def get_unique_constraints(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedUniqueConstraint]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_indexes(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedIndex]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_foreign_keys(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedForeignKeyConstraint]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def reflect_table(self, table: Table) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class _LegacyInspectorConv(_InspectorConv):
|
||||
|
||||
def _apply_reflectinfo_conv(self, consts):
|
||||
if not consts:
|
||||
return consts
|
||||
for const in consts:
|
||||
if const["name"] is not None and not isinstance(
|
||||
const["name"], conv
|
||||
):
|
||||
const["name"] = conv(const["name"])
|
||||
return consts
|
||||
|
||||
def _apply_constraint_conv(self, consts):
|
||||
if not consts:
|
||||
return consts
|
||||
for const in consts:
|
||||
if const.name is not None and not isinstance(const.name, conv):
|
||||
const.name = conv(const.name)
|
||||
return consts
|
||||
|
||||
def get_indexes(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedIndex]:
|
||||
return self._apply_reflectinfo_conv(
|
||||
self.inspector.get_indexes(tname, schema=schema)
|
||||
)
|
||||
|
||||
def get_unique_constraints(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedUniqueConstraint]:
|
||||
return self._apply_reflectinfo_conv(
|
||||
self.inspector.get_unique_constraints(tname, schema=schema)
|
||||
)
|
||||
|
||||
def get_foreign_keys(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedForeignKeyConstraint]:
|
||||
return self._apply_reflectinfo_conv(
|
||||
self.inspector.get_foreign_keys(tname, schema=schema)
|
||||
)
|
||||
|
||||
def reflect_table(self, table: Table) -> None:
|
||||
self.inspector.reflect_table(table, include_columns=None)
|
||||
|
||||
self._apply_constraint_conv(table.constraints)
|
||||
self._apply_constraint_conv(table.indexes)
|
||||
|
||||
|
||||
class _SQLA2InspectorConv(_InspectorConv):
|
||||
|
||||
def _pre_cache(
|
||||
self,
|
||||
schema: str | None,
|
||||
tablenames: list[str],
|
||||
all_available_tablenames: Collection[str],
|
||||
info_key: str,
|
||||
inspector_method: Any,
|
||||
) -> None:
|
||||
|
||||
if info_key in self.inspector.info_cache:
|
||||
return
|
||||
|
||||
# heuristic vendored from SQLAlchemy 2.0
|
||||
# if more than 50% of the tables in the db are in filter_names load all
|
||||
# the tables, since it's most likely faster to avoid a filter on that
|
||||
# many tables. also if a dialect doesnt have a "multi" method then
|
||||
# return the filter names
|
||||
if tablenames and all_available_tablenames and len(tablenames) > 100:
|
||||
fraction = len(tablenames) / len(all_available_tablenames)
|
||||
else:
|
||||
fraction = None
|
||||
|
||||
if (
|
||||
fraction is None
|
||||
or fraction <= 0.5
|
||||
or not self.inspector.dialect._overrides_default(
|
||||
inspector_method.__name__
|
||||
)
|
||||
):
|
||||
optimized_filter_names = tablenames
|
||||
else:
|
||||
optimized_filter_names = None
|
||||
|
||||
try:
|
||||
elements = inspector_method(
|
||||
schema=schema, filter_names=optimized_filter_names
|
||||
)
|
||||
except NotImplementedError:
|
||||
self.inspector.info_cache[info_key] = NotImplementedError
|
||||
else:
|
||||
self.inspector.info_cache[info_key] = elements
|
||||
|
||||
def _return_from_cache(
|
||||
self,
|
||||
tname: str,
|
||||
schema: str | None,
|
||||
info_key: str,
|
||||
inspector_method: Any,
|
||||
apply_constraint_conv: bool = False,
|
||||
optional=True,
|
||||
) -> Any:
|
||||
not_in_cache = object()
|
||||
|
||||
if info_key in self.inspector.info_cache:
|
||||
cache = self.inspector.info_cache[info_key]
|
||||
if cache is NotImplementedError:
|
||||
if optional:
|
||||
return {}
|
||||
else:
|
||||
# maintain NotImplementedError as alembic compare
|
||||
# uses these to determine classes of construct that it
|
||||
# should not compare to DB elements
|
||||
raise NotImplementedError()
|
||||
|
||||
individual = cache.get((schema, tname), not_in_cache)
|
||||
|
||||
if individual is not not_in_cache:
|
||||
if apply_constraint_conv and individual is not None:
|
||||
return self._apply_reflectinfo_conv(individual)
|
||||
else:
|
||||
return individual
|
||||
|
||||
try:
|
||||
data = inspector_method(tname, schema=schema)
|
||||
except NotImplementedError:
|
||||
if optional:
|
||||
return {}
|
||||
else:
|
||||
raise
|
||||
|
||||
if apply_constraint_conv:
|
||||
return self._apply_reflectinfo_conv(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
def get_unique_constraints(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedUniqueConstraint]:
|
||||
return self._return_from_cache(
|
||||
tname,
|
||||
schema,
|
||||
"alembic_unique_constraints",
|
||||
self.inspector.get_unique_constraints,
|
||||
apply_constraint_conv=True,
|
||||
optional=False,
|
||||
)
|
||||
|
||||
def get_indexes(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedIndex]:
|
||||
return self._return_from_cache(
|
||||
tname,
|
||||
schema,
|
||||
"alembic_indexes",
|
||||
self.inspector.get_indexes,
|
||||
apply_constraint_conv=True,
|
||||
optional=False,
|
||||
)
|
||||
|
||||
def get_foreign_keys(
|
||||
self, tname: str, schema: str | None
|
||||
) -> list[ReflectedForeignKeyConstraint]:
|
||||
return self._return_from_cache(
|
||||
tname,
|
||||
schema,
|
||||
"alembic_foreign_keys",
|
||||
self.inspector.get_foreign_keys,
|
||||
apply_constraint_conv=True,
|
||||
)
|
||||
|
||||
def _apply_reflectinfo_conv(self, consts):
|
||||
if not consts:
|
||||
return consts
|
||||
for const in consts if not isinstance(consts, dict) else [consts]:
|
||||
if const["name"] is not None and not isinstance(
|
||||
const["name"], conv
|
||||
):
|
||||
const["name"] = conv(const["name"])
|
||||
return consts
|
||||
|
||||
def pre_cache_tables(
|
||||
self,
|
||||
schema: str | None,
|
||||
tablenames: list[str],
|
||||
all_available_tablenames: Collection[str],
|
||||
) -> None:
|
||||
for key in _INSP_KEYS:
|
||||
keyname = f"alembic_{key}"
|
||||
meth = getattr(self.inspector, f"get_multi_{key}")
|
||||
|
||||
self._pre_cache(
|
||||
schema,
|
||||
tablenames,
|
||||
all_available_tablenames,
|
||||
keyname,
|
||||
meth,
|
||||
)
|
||||
|
||||
def _make_reflection_info(
|
||||
self, tname: str, schema: str | None
|
||||
) -> _ReflectionInfo:
|
||||
from sqlalchemy.engine.reflection import _ReflectionInfo
|
||||
|
||||
table_key = (schema, tname)
|
||||
|
||||
return _ReflectionInfo(
|
||||
unreflectable={},
|
||||
**{
|
||||
key: {
|
||||
table_key: self._return_from_cache(
|
||||
tname,
|
||||
schema,
|
||||
f"alembic_{key}",
|
||||
getattr(self.inspector, f"get_{key}"),
|
||||
apply_constraint_conv=(key in _CONSTRAINT_INSP_KEYS),
|
||||
)
|
||||
}
|
||||
for key in _INSP_KEYS
|
||||
},
|
||||
)
|
||||
|
||||
def reflect_table(self, table: Table) -> None:
|
||||
ri = self._make_reflection_info(table.name, table.schema)
|
||||
|
||||
self.inspector.reflect_table(
|
||||
table,
|
||||
include_columns=None,
|
||||
resolve_fks=False,
|
||||
_reflect_info=ri,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from .. import util
|
||||
from ..operations import ops
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..operations.ops import AddColumnOp
|
||||
from ..operations.ops import AlterColumnOp
|
||||
from ..operations.ops import CreateTableOp
|
||||
from ..operations.ops import DowngradeOps
|
||||
from ..operations.ops import MigrateOperation
|
||||
from ..operations.ops import MigrationScript
|
||||
from ..operations.ops import ModifyTableOps
|
||||
from ..operations.ops import OpContainer
|
||||
from ..operations.ops import UpgradeOps
|
||||
from ..runtime.migration import MigrationContext
|
||||
from ..script.revision import _GetRevArg
|
||||
|
||||
ProcessRevisionDirectiveFn = Callable[
|
||||
["MigrationContext", "_GetRevArg", List["MigrationScript"]], None
|
||||
]
|
||||
|
||||
|
||||
class Rewriter:
|
||||
"""A helper object that allows easy 'rewriting' of ops streams.
|
||||
|
||||
The :class:`.Rewriter` object is intended to be passed along
|
||||
to the
|
||||
:paramref:`.EnvironmentContext.configure.process_revision_directives`
|
||||
parameter in an ``env.py`` script. Once constructed, any number
|
||||
of "rewrites" functions can be associated with it, which will be given
|
||||
the opportunity to modify the structure without having to have explicit
|
||||
knowledge of the overall structure.
|
||||
|
||||
The function is passed the :class:`.MigrationContext` object and
|
||||
``revision`` tuple that are passed to the :paramref:`.Environment
|
||||
Context.configure.process_revision_directives` function normally,
|
||||
and the third argument is an individual directive of the type
|
||||
noted in the decorator. The function has the choice of returning
|
||||
a single op directive, which normally can be the directive that
|
||||
was actually passed, or a new directive to replace it, or a list
|
||||
of zero or more directives to replace it.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogen_rewriter` - usage example
|
||||
|
||||
"""
|
||||
|
||||
_traverse = util.Dispatcher()
|
||||
|
||||
_chained: Tuple[Union[ProcessRevisionDirectiveFn, Rewriter], ...] = ()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.dispatch = util.Dispatcher()
|
||||
|
||||
def chain(
|
||||
self,
|
||||
other: Union[
|
||||
ProcessRevisionDirectiveFn,
|
||||
Rewriter,
|
||||
],
|
||||
) -> Rewriter:
|
||||
"""Produce a "chain" of this :class:`.Rewriter` to another.
|
||||
|
||||
This allows two or more rewriters to operate serially on a stream,
|
||||
e.g.::
|
||||
|
||||
writer1 = autogenerate.Rewriter()
|
||||
writer2 = autogenerate.Rewriter()
|
||||
|
||||
|
||||
@writer1.rewrites(ops.AddColumnOp)
|
||||
def add_column_nullable(context, revision, op):
|
||||
op.column.nullable = True
|
||||
return op
|
||||
|
||||
|
||||
@writer2.rewrites(ops.AddColumnOp)
|
||||
def add_column_idx(context, revision, op):
|
||||
idx_op = ops.CreateIndexOp(
|
||||
"ixc", op.table_name, [op.column.name]
|
||||
)
|
||||
return [op, idx_op]
|
||||
|
||||
writer = writer1.chain(writer2)
|
||||
|
||||
:param other: a :class:`.Rewriter` instance
|
||||
:return: a new :class:`.Rewriter` that will run the operations
|
||||
of this writer, then the "other" writer, in succession.
|
||||
|
||||
"""
|
||||
wr = self.__class__.__new__(self.__class__)
|
||||
wr.__dict__.update(self.__dict__)
|
||||
wr._chained += (other,)
|
||||
return wr
|
||||
|
||||
def rewrites(
|
||||
self,
|
||||
operator: Union[
|
||||
Type[AddColumnOp],
|
||||
Type[MigrateOperation],
|
||||
Type[AlterColumnOp],
|
||||
Type[CreateTableOp],
|
||||
Type[ModifyTableOps],
|
||||
],
|
||||
) -> Callable[..., Any]:
|
||||
"""Register a function as rewriter for a given type.
|
||||
|
||||
The function should receive three arguments, which are
|
||||
the :class:`.MigrationContext`, a ``revision`` tuple, and
|
||||
an op directive of the type indicated. E.g.::
|
||||
|
||||
@writer1.rewrites(ops.AddColumnOp)
|
||||
def add_column_nullable(context, revision, op):
|
||||
op.column.nullable = True
|
||||
return op
|
||||
|
||||
"""
|
||||
return self.dispatch.dispatch_for(operator)
|
||||
|
||||
def _rewrite(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directive: MigrateOperation,
|
||||
) -> Iterator[MigrateOperation]:
|
||||
try:
|
||||
_rewriter = self.dispatch.dispatch(directive)
|
||||
except ValueError:
|
||||
_rewriter = None
|
||||
yield directive
|
||||
else:
|
||||
if self in directive._mutations:
|
||||
yield directive
|
||||
else:
|
||||
for r_directive in util.to_list(
|
||||
_rewriter(context, revision, directive), []
|
||||
):
|
||||
r_directive._mutations = r_directive._mutations.union(
|
||||
[self]
|
||||
)
|
||||
yield r_directive
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directives: List[MigrationScript],
|
||||
) -> None:
|
||||
self.process_revision_directives(context, revision, directives)
|
||||
for process_revision_directives in self._chained:
|
||||
process_revision_directives(context, revision, directives)
|
||||
|
||||
@_traverse.dispatch_for(ops.MigrationScript)
|
||||
def _traverse_script(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directive: MigrationScript,
|
||||
) -> None:
|
||||
upgrade_ops_list: List[UpgradeOps] = []
|
||||
for upgrade_ops in directive.upgrade_ops_list:
|
||||
ret = self._traverse_for(context, revision, upgrade_ops)
|
||||
if len(ret) != 1:
|
||||
raise ValueError(
|
||||
"Can only return single object for UpgradeOps traverse"
|
||||
)
|
||||
upgrade_ops_list.append(ret[0])
|
||||
|
||||
directive.upgrade_ops = upgrade_ops_list
|
||||
|
||||
downgrade_ops_list: List[DowngradeOps] = []
|
||||
for downgrade_ops in directive.downgrade_ops_list:
|
||||
ret = self._traverse_for(context, revision, downgrade_ops)
|
||||
if len(ret) != 1:
|
||||
raise ValueError(
|
||||
"Can only return single object for DowngradeOps traverse"
|
||||
)
|
||||
downgrade_ops_list.append(ret[0])
|
||||
directive.downgrade_ops = downgrade_ops_list
|
||||
|
||||
@_traverse.dispatch_for(ops.OpContainer)
|
||||
def _traverse_op_container(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directive: OpContainer,
|
||||
) -> None:
|
||||
self._traverse_list(context, revision, directive.ops)
|
||||
|
||||
@_traverse.dispatch_for(ops.MigrateOperation)
|
||||
def _traverse_any_directive(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directive: MigrateOperation,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def _traverse_for(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directive: MigrateOperation,
|
||||
) -> Any:
|
||||
directives = list(self._rewrite(context, revision, directive))
|
||||
for directive in directives:
|
||||
traverser = self._traverse.dispatch(directive)
|
||||
traverser(self, context, revision, directive)
|
||||
return directives
|
||||
|
||||
def _traverse_list(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directives: Any,
|
||||
) -> None:
|
||||
dest = []
|
||||
for directive in directives:
|
||||
dest.extend(self._traverse_for(context, revision, directive))
|
||||
|
||||
directives[:] = dest
|
||||
|
||||
def process_revision_directives(
|
||||
self,
|
||||
context: MigrationContext,
|
||||
revision: _GetRevArg,
|
||||
directives: List[MigrationScript],
|
||||
) -> None:
|
||||
self._traverse_list(context, revision, directives)
|
||||
@@ -0,0 +1,848 @@
|
||||
# mypy: allow-untyped-defs, allow-untyped-calls
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from . import autogenerate as autogen
|
||||
from . import util
|
||||
from .runtime.environment import EnvironmentContext
|
||||
from .script import ScriptDirectory
|
||||
from .util import compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from alembic.config import Config
|
||||
from alembic.script.base import Script
|
||||
from alembic.script.revision import _RevIdType
|
||||
from .runtime.environment import ProcessRevisionDirectiveFn
|
||||
|
||||
|
||||
def list_templates(config: Config) -> None:
|
||||
"""List available templates.
|
||||
|
||||
:param config: a :class:`.Config` object.
|
||||
|
||||
"""
|
||||
|
||||
config.print_stdout("Available templates:\n")
|
||||
for tempname in config._get_template_path().iterdir():
|
||||
with (tempname / "README").open() as readme:
|
||||
synopsis = next(readme).rstrip()
|
||||
config.print_stdout("%s - %s", tempname.name, synopsis)
|
||||
|
||||
config.print_stdout("\nTemplates are used via the 'init' command, e.g.:")
|
||||
config.print_stdout("\n alembic init --template generic ./scripts")
|
||||
|
||||
|
||||
def init(
|
||||
config: Config,
|
||||
directory: str,
|
||||
template: str = "generic",
|
||||
package: bool = False,
|
||||
) -> None:
|
||||
"""Initialize a new scripts directory.
|
||||
|
||||
:param config: a :class:`.Config` object.
|
||||
|
||||
:param directory: string path of the target directory.
|
||||
|
||||
:param template: string name of the migration environment template to
|
||||
use.
|
||||
|
||||
:param package: when True, write ``__init__.py`` files into the
|
||||
environment location as well as the versions/ location.
|
||||
|
||||
"""
|
||||
|
||||
directory_path = pathlib.Path(directory)
|
||||
if directory_path.exists() and list(directory_path.iterdir()):
|
||||
raise util.CommandError(
|
||||
"Directory %s already exists and is not empty" % directory_path
|
||||
)
|
||||
|
||||
template_path = config._get_template_path() / template
|
||||
|
||||
if not template_path.exists():
|
||||
raise util.CommandError(f"No such template {template_path}")
|
||||
|
||||
# left as os.access() to suit unit test mocking
|
||||
if not os.access(directory_path, os.F_OK):
|
||||
with util.status(
|
||||
f"Creating directory {directory_path.absolute()}",
|
||||
**config.messaging_opts,
|
||||
):
|
||||
os.makedirs(directory_path)
|
||||
|
||||
versions = directory_path / "versions"
|
||||
with util.status(
|
||||
f"Creating directory {versions.absolute()}",
|
||||
**config.messaging_opts,
|
||||
):
|
||||
os.makedirs(versions)
|
||||
|
||||
if not directory_path.is_absolute():
|
||||
# for non-absolute path, state config file in .ini / pyproject
|
||||
# as relative to the %(here)s token, which is where the config
|
||||
# file itself would be
|
||||
|
||||
if config._config_file_path is not None:
|
||||
rel_dir = compat.path_relative_to(
|
||||
directory_path.absolute(),
|
||||
config._config_file_path.absolute().parent,
|
||||
walk_up=True,
|
||||
)
|
||||
ini_script_location_directory = ("%(here)s" / rel_dir).as_posix()
|
||||
if config._toml_file_path is not None:
|
||||
rel_dir = compat.path_relative_to(
|
||||
directory_path.absolute(),
|
||||
config._toml_file_path.absolute().parent,
|
||||
walk_up=True,
|
||||
)
|
||||
toml_script_location_directory = ("%(here)s" / rel_dir).as_posix()
|
||||
|
||||
else:
|
||||
ini_script_location_directory = directory_path.as_posix()
|
||||
toml_script_location_directory = directory_path.as_posix()
|
||||
|
||||
script = ScriptDirectory(directory_path)
|
||||
|
||||
has_toml = False
|
||||
|
||||
config_file: pathlib.Path | None = None
|
||||
|
||||
for file_path in template_path.iterdir():
|
||||
file_ = file_path.name
|
||||
if file_ == "alembic.ini.mako":
|
||||
assert config.config_file_name is not None
|
||||
config_file = pathlib.Path(config.config_file_name).absolute()
|
||||
if config_file.exists():
|
||||
util.msg(
|
||||
f"File {config_file} already exists, skipping",
|
||||
**config.messaging_opts,
|
||||
)
|
||||
else:
|
||||
script._generate_template(
|
||||
file_path,
|
||||
config_file,
|
||||
script_location=ini_script_location_directory,
|
||||
)
|
||||
elif file_ == "pyproject.toml.mako":
|
||||
has_toml = True
|
||||
assert config._toml_file_path is not None
|
||||
toml_path = config._toml_file_path.absolute()
|
||||
|
||||
if toml_path.exists():
|
||||
# left as open() to suit unit test mocking
|
||||
with open(toml_path, "rb") as f:
|
||||
toml_data = compat.tomllib.load(f)
|
||||
if "tool" in toml_data and "alembic" in toml_data["tool"]:
|
||||
|
||||
util.msg(
|
||||
f"File {toml_path} already exists "
|
||||
"and already has a [tool.alembic] section, "
|
||||
"skipping",
|
||||
)
|
||||
continue
|
||||
script._append_template(
|
||||
file_path,
|
||||
toml_path,
|
||||
script_location=toml_script_location_directory,
|
||||
)
|
||||
else:
|
||||
script._generate_template(
|
||||
file_path,
|
||||
toml_path,
|
||||
script_location=toml_script_location_directory,
|
||||
)
|
||||
|
||||
elif file_path.is_file():
|
||||
output_file = directory_path / file_
|
||||
script._copy_file(file_path, output_file)
|
||||
|
||||
if package:
|
||||
for path in [
|
||||
directory_path.absolute() / "__init__.py",
|
||||
versions.absolute() / "__init__.py",
|
||||
]:
|
||||
with util.status(f"Adding {path!s}", **config.messaging_opts):
|
||||
# left as open() to suit unit test mocking
|
||||
with open(path, "w"):
|
||||
pass
|
||||
|
||||
assert config_file is not None
|
||||
|
||||
if has_toml:
|
||||
util.msg(
|
||||
f"Please edit configuration settings in {toml_path} and "
|
||||
"configuration/connection/logging "
|
||||
f"settings in {config_file} before proceeding.",
|
||||
**config.messaging_opts,
|
||||
)
|
||||
else:
|
||||
util.msg(
|
||||
"Please edit configuration/connection/logging "
|
||||
f"settings in {config_file} before proceeding.",
|
||||
**config.messaging_opts,
|
||||
)
|
||||
|
||||
|
||||
def revision(
|
||||
config: Config,
|
||||
message: Optional[str] = None,
|
||||
autogenerate: bool = False,
|
||||
sql: bool = False,
|
||||
head: str = "head",
|
||||
splice: bool = False,
|
||||
branch_label: Optional[_RevIdType] = None,
|
||||
version_path: Union[str, os.PathLike[str], None] = None,
|
||||
rev_id: Optional[str] = None,
|
||||
depends_on: Optional[str] = None,
|
||||
process_revision_directives: Optional[ProcessRevisionDirectiveFn] = None,
|
||||
) -> Union[Optional[Script], List[Optional[Script]]]:
|
||||
"""Create a new revision file.
|
||||
|
||||
:param config: a :class:`.Config` object.
|
||||
|
||||
:param message: string message to apply to the revision; this is the
|
||||
``-m`` option to ``alembic revision``.
|
||||
|
||||
:param autogenerate: whether or not to autogenerate the script from
|
||||
the database; this is the ``--autogenerate`` option to
|
||||
``alembic revision``.
|
||||
|
||||
:param sql: whether to dump the script out as a SQL string; when specified,
|
||||
the script is dumped to stdout. This is the ``--sql`` option to
|
||||
``alembic revision``.
|
||||
|
||||
:param head: head revision to build the new revision upon as a parent;
|
||||
this is the ``--head`` option to ``alembic revision``.
|
||||
|
||||
:param splice: whether or not the new revision should be made into a
|
||||
new head of its own; is required when the given ``head`` is not itself
|
||||
a head. This is the ``--splice`` option to ``alembic revision``.
|
||||
|
||||
:param branch_label: string label to apply to the branch; this is the
|
||||
``--branch-label`` option to ``alembic revision``.
|
||||
|
||||
:param version_path: string symbol identifying a specific version path
|
||||
from the configuration; this is the ``--version-path`` option to
|
||||
``alembic revision``.
|
||||
|
||||
:param rev_id: optional revision identifier to use instead of having
|
||||
one generated; this is the ``--rev-id`` option to ``alembic revision``.
|
||||
|
||||
:param depends_on: optional list of "depends on" identifiers; this is the
|
||||
``--depends-on`` option to ``alembic revision``.
|
||||
|
||||
:param process_revision_directives: this is a callable that takes the
|
||||
same form as the callable described at
|
||||
:paramref:`.EnvironmentContext.configure.process_revision_directives`;
|
||||
will be applied to the structure generated by the revision process
|
||||
where it can be altered programmatically. Note that unlike all
|
||||
the other parameters, this option is only available via programmatic
|
||||
use of :func:`.command.revision`.
|
||||
|
||||
"""
|
||||
|
||||
script_directory = ScriptDirectory.from_config(config)
|
||||
|
||||
command_args = dict(
|
||||
message=message,
|
||||
autogenerate=autogenerate,
|
||||
sql=sql,
|
||||
head=head,
|
||||
splice=splice,
|
||||
branch_label=branch_label,
|
||||
version_path=version_path,
|
||||
rev_id=rev_id,
|
||||
depends_on=depends_on,
|
||||
)
|
||||
revision_context = autogen.RevisionContext(
|
||||
config,
|
||||
script_directory,
|
||||
command_args,
|
||||
process_revision_directives=process_revision_directives,
|
||||
)
|
||||
|
||||
environment = util.asbool(
|
||||
config.get_alembic_option("revision_environment")
|
||||
)
|
||||
|
||||
if autogenerate:
|
||||
environment = True
|
||||
|
||||
if sql:
|
||||
raise util.CommandError(
|
||||
"Using --sql with --autogenerate does not make any sense"
|
||||
)
|
||||
|
||||
def retrieve_migrations(rev, context):
|
||||
revision_context.run_autogenerate(rev, context)
|
||||
return []
|
||||
|
||||
elif environment:
|
||||
|
||||
def retrieve_migrations(rev, context):
|
||||
revision_context.run_no_autogenerate(rev, context)
|
||||
return []
|
||||
|
||||
elif sql:
|
||||
raise util.CommandError(
|
||||
"Using --sql with the revision command when "
|
||||
"revision_environment is not configured does not make any sense"
|
||||
)
|
||||
|
||||
if environment:
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script_directory,
|
||||
fn=retrieve_migrations,
|
||||
as_sql=sql,
|
||||
template_args=revision_context.template_args,
|
||||
revision_context=revision_context,
|
||||
):
|
||||
script_directory.run_env()
|
||||
|
||||
# the revision_context now has MigrationScript structure(s) present.
|
||||
# these could theoretically be further processed / rewritten *here*,
|
||||
# in addition to the hooks present within each run_migrations() call,
|
||||
# or at the end of env.py run_migrations_online().
|
||||
|
||||
scripts = [script for script in revision_context.generate_scripts()]
|
||||
if len(scripts) == 1:
|
||||
return scripts[0]
|
||||
else:
|
||||
return scripts
|
||||
|
||||
|
||||
def check(config: "Config") -> None:
|
||||
"""Check if revision command with autogenerate has pending upgrade ops.
|
||||
|
||||
:param config: a :class:`.Config` object.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
"""
|
||||
|
||||
script_directory = ScriptDirectory.from_config(config)
|
||||
|
||||
command_args = dict(
|
||||
message=None,
|
||||
autogenerate=True,
|
||||
sql=False,
|
||||
head="head",
|
||||
splice=False,
|
||||
branch_label=None,
|
||||
version_path=None,
|
||||
rev_id=None,
|
||||
depends_on=None,
|
||||
)
|
||||
revision_context = autogen.RevisionContext(
|
||||
config,
|
||||
script_directory,
|
||||
command_args,
|
||||
)
|
||||
|
||||
def retrieve_migrations(rev, context):
|
||||
revision_context.run_autogenerate(rev, context)
|
||||
return []
|
||||
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script_directory,
|
||||
fn=retrieve_migrations,
|
||||
as_sql=False,
|
||||
template_args=revision_context.template_args,
|
||||
revision_context=revision_context,
|
||||
):
|
||||
script_directory.run_env()
|
||||
|
||||
# the revision_context now has MigrationScript structure(s) present.
|
||||
|
||||
migration_script = revision_context.generated_revisions[-1]
|
||||
diffs = []
|
||||
for upgrade_ops in migration_script.upgrade_ops_list:
|
||||
diffs.extend(upgrade_ops.as_diffs())
|
||||
|
||||
if diffs:
|
||||
raise util.AutogenerateDiffsDetected(
|
||||
f"New upgrade operations detected: {diffs}",
|
||||
revision_context=revision_context,
|
||||
diffs=diffs,
|
||||
)
|
||||
else:
|
||||
config.print_stdout("No new upgrade operations detected.")
|
||||
|
||||
|
||||
def merge(
|
||||
config: Config,
|
||||
revisions: _RevIdType,
|
||||
message: Optional[str] = None,
|
||||
branch_label: Optional[_RevIdType] = None,
|
||||
rev_id: Optional[str] = None,
|
||||
) -> Optional[Script]:
|
||||
"""Merge two revisions together. Creates a new migration file.
|
||||
|
||||
:param config: a :class:`.Config` instance
|
||||
|
||||
:param revisions: The revisions to merge.
|
||||
|
||||
:param message: string message to apply to the revision.
|
||||
|
||||
:param branch_label: string label name to apply to the new revision.
|
||||
|
||||
:param rev_id: hardcoded revision identifier instead of generating a new
|
||||
one.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`branches`
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
template_args = {
|
||||
"config": config # Let templates use config for
|
||||
# e.g. multiple databases
|
||||
}
|
||||
|
||||
environment = util.asbool(
|
||||
config.get_alembic_option("revision_environment")
|
||||
)
|
||||
|
||||
if environment:
|
||||
|
||||
def nothing(rev, context):
|
||||
return []
|
||||
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script,
|
||||
fn=nothing,
|
||||
as_sql=False,
|
||||
template_args=template_args,
|
||||
):
|
||||
script.run_env()
|
||||
|
||||
return script.generate_revision(
|
||||
rev_id or util.rev_id(),
|
||||
message,
|
||||
refresh=True,
|
||||
head=revisions,
|
||||
branch_labels=branch_label,
|
||||
**template_args, # type:ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def upgrade(
|
||||
config: Config,
|
||||
revision: str,
|
||||
sql: bool = False,
|
||||
tag: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Upgrade to a later version.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param revision: string revision target or range for --sql mode. May be
|
||||
``"heads"`` to target the most recent revision(s).
|
||||
|
||||
:param sql: if True, use ``--sql`` mode.
|
||||
|
||||
:param tag: an arbitrary "tag" that can be intercepted by custom
|
||||
``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
|
||||
method.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
|
||||
starting_rev = None
|
||||
if ":" in revision:
|
||||
if not sql:
|
||||
raise util.CommandError("Range revision not allowed")
|
||||
starting_rev, revision = revision.split(":", 2)
|
||||
|
||||
def upgrade(rev, context):
|
||||
return script._upgrade_revs(revision, rev)
|
||||
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script,
|
||||
fn=upgrade,
|
||||
as_sql=sql,
|
||||
starting_rev=starting_rev,
|
||||
destination_rev=revision,
|
||||
tag=tag,
|
||||
):
|
||||
script.run_env()
|
||||
|
||||
|
||||
def downgrade(
|
||||
config: Config,
|
||||
revision: str,
|
||||
sql: bool = False,
|
||||
tag: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Revert to a previous version.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param revision: string revision target or range for --sql mode. May
|
||||
be ``"base"`` to target the first revision.
|
||||
|
||||
:param sql: if True, use ``--sql`` mode.
|
||||
|
||||
:param tag: an arbitrary "tag" that can be intercepted by custom
|
||||
``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
|
||||
method.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
starting_rev = None
|
||||
if ":" in revision:
|
||||
if not sql:
|
||||
raise util.CommandError("Range revision not allowed")
|
||||
starting_rev, revision = revision.split(":", 2)
|
||||
elif sql:
|
||||
raise util.CommandError(
|
||||
"downgrade with --sql requires <fromrev>:<torev>"
|
||||
)
|
||||
|
||||
def downgrade(rev, context):
|
||||
return script._downgrade_revs(revision, rev)
|
||||
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script,
|
||||
fn=downgrade,
|
||||
as_sql=sql,
|
||||
starting_rev=starting_rev,
|
||||
destination_rev=revision,
|
||||
tag=tag,
|
||||
):
|
||||
script.run_env()
|
||||
|
||||
|
||||
def show(config: Config, rev: str) -> None:
|
||||
"""Show the revision(s) denoted by the given symbol.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param rev: string revision target. May be ``"current"`` to show the
|
||||
revision(s) currently applied in the database.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
|
||||
if rev == "current":
|
||||
|
||||
def show_current(rev, context):
|
||||
for sc in script.get_revisions(rev):
|
||||
config.print_stdout(sc.log_entry)
|
||||
return []
|
||||
|
||||
with EnvironmentContext(config, script, fn=show_current):
|
||||
script.run_env()
|
||||
else:
|
||||
for sc in script.get_revisions(rev):
|
||||
config.print_stdout(sc.log_entry)
|
||||
|
||||
|
||||
def history(
|
||||
config: Config,
|
||||
rev_range: Optional[str] = None,
|
||||
verbose: bool = False,
|
||||
indicate_current: bool = False,
|
||||
) -> None:
|
||||
"""List changeset scripts in chronological order.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param rev_range: string revision range.
|
||||
|
||||
:param verbose: output in verbose mode.
|
||||
|
||||
:param indicate_current: indicate current revision.
|
||||
|
||||
"""
|
||||
base: Optional[str]
|
||||
head: Optional[str]
|
||||
script = ScriptDirectory.from_config(config)
|
||||
if rev_range is not None:
|
||||
if ":" not in rev_range:
|
||||
raise util.CommandError(
|
||||
"History range requires [start]:[end], " "[start]:, or :[end]"
|
||||
)
|
||||
base, head = rev_range.strip().split(":")
|
||||
else:
|
||||
base = head = None
|
||||
|
||||
environment = (
|
||||
util.asbool(config.get_alembic_option("revision_environment"))
|
||||
or indicate_current
|
||||
)
|
||||
|
||||
def _display_history(config, script, base, head, currents=()):
|
||||
for sc in script.walk_revisions(
|
||||
base=base or "base", head=head or "heads"
|
||||
):
|
||||
if indicate_current:
|
||||
sc._db_current_indicator = sc.revision in currents
|
||||
|
||||
config.print_stdout(
|
||||
sc.cmd_format(
|
||||
verbose=verbose,
|
||||
include_branches=True,
|
||||
include_doc=True,
|
||||
include_parents=True,
|
||||
)
|
||||
)
|
||||
|
||||
def _display_history_w_current(config, script, base, head):
|
||||
def _display_current_history(rev, context):
|
||||
if head == "current":
|
||||
_display_history(config, script, base, rev, rev)
|
||||
elif base == "current":
|
||||
_display_history(config, script, rev, head, rev)
|
||||
else:
|
||||
_display_history(config, script, base, head, rev)
|
||||
return []
|
||||
|
||||
with EnvironmentContext(config, script, fn=_display_current_history):
|
||||
script.run_env()
|
||||
|
||||
if base == "current" or head == "current" or environment:
|
||||
_display_history_w_current(config, script, base, head)
|
||||
else:
|
||||
_display_history(config, script, base, head)
|
||||
|
||||
|
||||
def heads(
|
||||
config: Config, verbose: bool = False, resolve_dependencies: bool = False
|
||||
) -> None:
|
||||
"""Show current available heads in the script directory.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param verbose: output in verbose mode.
|
||||
|
||||
:param resolve_dependencies: treat dependency version as down revisions.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
if resolve_dependencies:
|
||||
heads = script.get_revisions("heads")
|
||||
else:
|
||||
heads = script.get_revisions(script.get_heads())
|
||||
|
||||
for rev in heads:
|
||||
config.print_stdout(
|
||||
rev.cmd_format(
|
||||
verbose, include_branches=True, tree_indicators=False
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def branches(config: Config, verbose: bool = False) -> None:
|
||||
"""Show current branch points.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param verbose: output in verbose mode.
|
||||
|
||||
"""
|
||||
script = ScriptDirectory.from_config(config)
|
||||
for sc in script.walk_revisions():
|
||||
if sc.is_branch_point:
|
||||
config.print_stdout(
|
||||
"%s\n%s\n",
|
||||
sc.cmd_format(verbose, include_branches=True),
|
||||
"\n".join(
|
||||
"%s -> %s"
|
||||
% (
|
||||
" " * len(str(sc.revision)),
|
||||
rev_obj.cmd_format(
|
||||
False, include_branches=True, include_doc=verbose
|
||||
),
|
||||
)
|
||||
for rev_obj in (
|
||||
script.get_revision(rev) for rev in sc.nextrev
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def current(
|
||||
config: Config, check_heads: bool = False, verbose: bool = False
|
||||
) -> None:
|
||||
"""Display the current revision for a database.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param check_heads: Check if all head revisions are applied to the
|
||||
database. Raises :class:`.DatabaseNotAtHead` if this is not the case.
|
||||
|
||||
.. versionadded:: 1.17.1
|
||||
|
||||
:param verbose: output in verbose mode.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
|
||||
def display_version(rev, context):
|
||||
if verbose:
|
||||
config.print_stdout(
|
||||
"Current revision(s) for %s:",
|
||||
util.obfuscate_url_pw(context.connection.engine.url),
|
||||
)
|
||||
if check_heads and (
|
||||
set(context.get_current_heads()) != set(script.get_heads())
|
||||
):
|
||||
raise util.DatabaseNotAtHead(
|
||||
"Database is not on all head revisions"
|
||||
)
|
||||
for rev in script.get_all_current(rev):
|
||||
config.print_stdout(rev.cmd_format(verbose))
|
||||
|
||||
return []
|
||||
|
||||
with EnvironmentContext(
|
||||
config, script, fn=display_version, dont_mutate=True
|
||||
):
|
||||
script.run_env()
|
||||
|
||||
|
||||
def stamp(
|
||||
config: Config,
|
||||
revision: _RevIdType,
|
||||
sql: bool = False,
|
||||
tag: Optional[str] = None,
|
||||
purge: bool = False,
|
||||
) -> None:
|
||||
"""'stamp' the revision table with the given revision; don't
|
||||
run any migrations.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param revision: target revision or list of revisions. May be a list
|
||||
to indicate stamping of multiple branch heads; may be ``"base"``
|
||||
to remove all revisions from the table or ``"heads"`` to stamp the
|
||||
most recent revision(s).
|
||||
|
||||
.. note:: this parameter is called "revisions" in the command line
|
||||
interface.
|
||||
|
||||
:param sql: use ``--sql`` mode
|
||||
|
||||
:param tag: an arbitrary "tag" that can be intercepted by custom
|
||||
``env.py`` scripts via the :class:`.EnvironmentContext.get_tag_argument`
|
||||
method.
|
||||
|
||||
:param purge: delete all entries in the version table before stamping.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
|
||||
if sql:
|
||||
destination_revs = []
|
||||
starting_rev = None
|
||||
for _revision in util.to_list(revision):
|
||||
if ":" in _revision:
|
||||
srev, _revision = _revision.split(":", 2)
|
||||
|
||||
if starting_rev != srev:
|
||||
if starting_rev is None:
|
||||
starting_rev = srev
|
||||
else:
|
||||
raise util.CommandError(
|
||||
"Stamp operation with --sql only supports a "
|
||||
"single starting revision at a time"
|
||||
)
|
||||
destination_revs.append(_revision)
|
||||
else:
|
||||
destination_revs = util.to_list(revision)
|
||||
|
||||
def do_stamp(rev, context):
|
||||
return script._stamp_revs(util.to_tuple(destination_revs), rev)
|
||||
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script,
|
||||
fn=do_stamp,
|
||||
as_sql=sql,
|
||||
starting_rev=starting_rev if sql else None,
|
||||
destination_rev=util.to_tuple(destination_revs),
|
||||
tag=tag,
|
||||
purge=purge,
|
||||
):
|
||||
script.run_env()
|
||||
|
||||
|
||||
def edit(config: Config, rev: str) -> None:
|
||||
"""Edit revision script(s) using $EDITOR.
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param rev: target revision.
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
|
||||
if rev == "current":
|
||||
|
||||
def edit_current(rev, context):
|
||||
if not rev:
|
||||
raise util.CommandError("No current revisions")
|
||||
for sc in script.get_revisions(rev):
|
||||
util.open_in_editor(sc.path)
|
||||
return []
|
||||
|
||||
with EnvironmentContext(config, script, fn=edit_current):
|
||||
script.run_env()
|
||||
else:
|
||||
revs = script.get_revisions(rev)
|
||||
if not revs:
|
||||
raise util.CommandError(
|
||||
"No revision files indicated by symbol '%s'" % rev
|
||||
)
|
||||
for sc in revs:
|
||||
assert sc
|
||||
util.open_in_editor(sc.path)
|
||||
|
||||
|
||||
def ensure_version(config: Config, sql: bool = False) -> None:
|
||||
"""Create the alembic version table if it doesn't exist already .
|
||||
|
||||
:param config: a :class:`.Config` instance.
|
||||
|
||||
:param sql: use ``--sql`` mode.
|
||||
|
||||
.. versionadded:: 1.7.6
|
||||
|
||||
"""
|
||||
|
||||
script = ScriptDirectory.from_config(config)
|
||||
|
||||
def do_ensure_version(rev, context):
|
||||
context._ensure_version_table()
|
||||
return []
|
||||
|
||||
with EnvironmentContext(
|
||||
config,
|
||||
script,
|
||||
fn=do_ensure_version,
|
||||
as_sql=sql,
|
||||
):
|
||||
script.run_env()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
from .runtime.environment import EnvironmentContext
|
||||
|
||||
# create proxy functions for
|
||||
# each method on the EnvironmentContext class.
|
||||
EnvironmentContext.create_module_class_proxy(globals(), locals())
|
||||
@@ -0,0 +1,876 @@
|
||||
# ### this file stubs are generated by tools/write_pyi.py - do not edit ###
|
||||
# ### imports are manually managed
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Collection
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import MutableMapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from typing_extensions import ContextManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from sqlalchemy.engine.url import URL
|
||||
from sqlalchemy.sql import Executable
|
||||
from sqlalchemy.sql.schema import Column
|
||||
from sqlalchemy.sql.schema import FetchedValue
|
||||
from sqlalchemy.sql.schema import MetaData
|
||||
from sqlalchemy.sql.schema import SchemaItem
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from .autogenerate.api import AutogenContext
|
||||
from .config import Config
|
||||
from .operations.ops import MigrationScript
|
||||
from .runtime.migration import _ProxyTransaction
|
||||
from .runtime.migration import MigrationContext
|
||||
from .runtime.migration import MigrationInfo
|
||||
from .script import ScriptDirectory
|
||||
|
||||
### end imports ###
|
||||
|
||||
def begin_transaction() -> (
|
||||
Union[_ProxyTransaction, ContextManager[None, Optional[bool]]]
|
||||
):
|
||||
"""Return a context manager that will
|
||||
enclose an operation within a "transaction",
|
||||
as defined by the environment's offline
|
||||
and transactional DDL settings.
|
||||
|
||||
e.g.::
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
:meth:`.begin_transaction` is intended to
|
||||
"do the right thing" regardless of
|
||||
calling context:
|
||||
|
||||
* If :meth:`.is_transactional_ddl` is ``False``,
|
||||
returns a "do nothing" context manager
|
||||
which otherwise produces no transactional
|
||||
state or directives.
|
||||
* If :meth:`.is_offline_mode` is ``True``,
|
||||
returns a context manager that will
|
||||
invoke the :meth:`.DefaultImpl.emit_begin`
|
||||
and :meth:`.DefaultImpl.emit_commit`
|
||||
methods, which will produce the string
|
||||
directives ``BEGIN`` and ``COMMIT`` on
|
||||
the output stream, as rendered by the
|
||||
target backend (e.g. SQL Server would
|
||||
emit ``BEGIN TRANSACTION``).
|
||||
* Otherwise, calls :meth:`sqlalchemy.engine.Connection.begin`
|
||||
on the current online connection, which
|
||||
returns a :class:`sqlalchemy.engine.Transaction`
|
||||
object. This object demarcates a real
|
||||
transaction and is itself a context manager,
|
||||
which will roll back if an exception
|
||||
is raised.
|
||||
|
||||
Note that a custom ``env.py`` script which
|
||||
has more specific transactional needs can of course
|
||||
manipulate the :class:`~sqlalchemy.engine.Connection`
|
||||
directly to produce transactional state in "online"
|
||||
mode.
|
||||
|
||||
"""
|
||||
|
||||
config: Config
|
||||
|
||||
def configure(
|
||||
connection: Optional[Connection] = None,
|
||||
url: Union[str, URL, None] = None,
|
||||
dialect_name: Optional[str] = None,
|
||||
dialect_opts: Optional[Dict[str, Any]] = None,
|
||||
transactional_ddl: Optional[bool] = None,
|
||||
transaction_per_migration: bool = False,
|
||||
output_buffer: Optional[TextIO] = None,
|
||||
starting_rev: Optional[str] = None,
|
||||
tag: Optional[str] = None,
|
||||
template_args: Optional[Dict[str, Any]] = None,
|
||||
render_as_batch: bool = False,
|
||||
target_metadata: Union[MetaData, Sequence[MetaData], None] = None,
|
||||
include_name: Optional[
|
||||
Callable[
|
||||
[
|
||||
Optional[str],
|
||||
Literal[
|
||||
"schema",
|
||||
"table",
|
||||
"column",
|
||||
"index",
|
||||
"unique_constraint",
|
||||
"foreign_key_constraint",
|
||||
],
|
||||
MutableMapping[
|
||||
Literal[
|
||||
"schema_name",
|
||||
"table_name",
|
||||
"schema_qualified_table_name",
|
||||
],
|
||||
Optional[str],
|
||||
],
|
||||
],
|
||||
bool,
|
||||
]
|
||||
] = None,
|
||||
include_object: Optional[
|
||||
Callable[
|
||||
[
|
||||
SchemaItem,
|
||||
Optional[str],
|
||||
Literal[
|
||||
"schema",
|
||||
"table",
|
||||
"column",
|
||||
"index",
|
||||
"unique_constraint",
|
||||
"foreign_key_constraint",
|
||||
],
|
||||
bool,
|
||||
Optional[SchemaItem],
|
||||
],
|
||||
bool,
|
||||
]
|
||||
] = None,
|
||||
include_schemas: bool = False,
|
||||
process_revision_directives: Optional[
|
||||
Callable[
|
||||
[
|
||||
MigrationContext,
|
||||
Union[str, Iterable[Optional[str]], Iterable[str]],
|
||||
List[MigrationScript],
|
||||
],
|
||||
None,
|
||||
]
|
||||
] = None,
|
||||
compare_type: Union[
|
||||
bool,
|
||||
Callable[
|
||||
[
|
||||
MigrationContext,
|
||||
Column[Any],
|
||||
Column[Any],
|
||||
TypeEngine[Any],
|
||||
TypeEngine[Any],
|
||||
],
|
||||
Optional[bool],
|
||||
],
|
||||
] = True,
|
||||
compare_server_default: Union[
|
||||
bool,
|
||||
Callable[
|
||||
[
|
||||
MigrationContext,
|
||||
Column[Any],
|
||||
Column[Any],
|
||||
Optional[str],
|
||||
Optional[FetchedValue],
|
||||
Optional[str],
|
||||
],
|
||||
Optional[bool],
|
||||
],
|
||||
] = False,
|
||||
render_item: Optional[
|
||||
Callable[[str, Any, AutogenContext], Union[str, Literal[False]]]
|
||||
] = None,
|
||||
literal_binds: bool = False,
|
||||
upgrade_token: str = "upgrades",
|
||||
downgrade_token: str = "downgrades",
|
||||
alembic_module_prefix: str = "op.",
|
||||
sqlalchemy_module_prefix: str = "sa.",
|
||||
user_module_prefix: Optional[str] = None,
|
||||
on_version_apply: Optional[
|
||||
Callable[
|
||||
[
|
||||
MigrationContext,
|
||||
MigrationInfo,
|
||||
Collection[Any],
|
||||
Mapping[str, Any],
|
||||
],
|
||||
None,
|
||||
]
|
||||
] = None,
|
||||
autogenerate_plugins: Optional[Sequence[str]] = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
"""Configure a :class:`.MigrationContext` within this
|
||||
:class:`.EnvironmentContext` which will provide database
|
||||
connectivity and other configuration to a series of
|
||||
migration scripts.
|
||||
|
||||
Many methods on :class:`.EnvironmentContext` require that
|
||||
this method has been called in order to function, as they
|
||||
ultimately need to have database access or at least access
|
||||
to the dialect in use. Those which do are documented as such.
|
||||
|
||||
The important thing needed by :meth:`.configure` is a
|
||||
means to determine what kind of database dialect is in use.
|
||||
An actual connection to that database is needed only if
|
||||
the :class:`.MigrationContext` is to be used in
|
||||
"online" mode.
|
||||
|
||||
If the :meth:`.is_offline_mode` function returns ``True``,
|
||||
then no connection is needed here. Otherwise, the
|
||||
``connection`` parameter should be present as an
|
||||
instance of :class:`sqlalchemy.engine.Connection`.
|
||||
|
||||
This function is typically called from the ``env.py``
|
||||
script within a migration environment. It can be called
|
||||
multiple times for an invocation. The most recent
|
||||
:class:`~sqlalchemy.engine.Connection`
|
||||
for which it was called is the one that will be operated upon
|
||||
by the next call to :meth:`.run_migrations`.
|
||||
|
||||
General parameters:
|
||||
|
||||
:param connection: a :class:`~sqlalchemy.engine.Connection`
|
||||
to use
|
||||
for SQL execution in "online" mode. When present, is also
|
||||
used to determine the type of dialect in use.
|
||||
:param url: a string database url, or a
|
||||
:class:`sqlalchemy.engine.url.URL` object.
|
||||
The type of dialect to be used will be derived from this if
|
||||
``connection`` is not passed.
|
||||
:param dialect_name: string name of a dialect, such as
|
||||
"postgresql", "mssql", etc.
|
||||
The type of dialect to be used will be derived from this if
|
||||
``connection`` and ``url`` are not passed.
|
||||
:param dialect_opts: dictionary of options to be passed to dialect
|
||||
constructor.
|
||||
:param transactional_ddl: Force the usage of "transactional"
|
||||
DDL on or off;
|
||||
this otherwise defaults to whether or not the dialect in
|
||||
use supports it.
|
||||
:param transaction_per_migration: if True, nest each migration script
|
||||
in a transaction rather than the full series of migrations to
|
||||
run.
|
||||
:param output_buffer: a file-like object that will be used
|
||||
for textual output
|
||||
when the ``--sql`` option is used to generate SQL scripts.
|
||||
Defaults to
|
||||
``sys.stdout`` if not passed here and also not present on
|
||||
the :class:`.Config`
|
||||
object. The value here overrides that of the :class:`.Config`
|
||||
object.
|
||||
:param output_encoding: when using ``--sql`` to generate SQL
|
||||
scripts, apply this encoding to the string output.
|
||||
:param literal_binds: when using ``--sql`` to generate SQL
|
||||
scripts, pass through the ``literal_binds`` flag to the compiler
|
||||
so that any literal values that would ordinarily be bound
|
||||
parameters are converted to plain strings.
|
||||
|
||||
.. warning:: Dialects can typically only handle simple datatypes
|
||||
like strings and numbers for auto-literal generation. Datatypes
|
||||
like dates, intervals, and others may still require manual
|
||||
formatting, typically using :meth:`.Operations.inline_literal`.
|
||||
|
||||
.. note:: the ``literal_binds`` flag is ignored on SQLAlchemy
|
||||
versions prior to 0.8 where this feature is not supported.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.Operations.inline_literal`
|
||||
|
||||
:param starting_rev: Override the "starting revision" argument
|
||||
when using ``--sql`` mode.
|
||||
:param tag: a string tag for usage by custom ``env.py`` scripts.
|
||||
Set via the ``--tag`` option, can be overridden here.
|
||||
:param template_args: dictionary of template arguments which
|
||||
will be added to the template argument environment when
|
||||
running the "revision" command. Note that the script environment
|
||||
is only run within the "revision" command if the --autogenerate
|
||||
option is used, or if the option "revision_environment=true"
|
||||
is present in the alembic.ini file.
|
||||
|
||||
:param version_table: The name of the Alembic version table.
|
||||
The default is ``'alembic_version'``.
|
||||
:param version_table_schema: Optional schema to place version
|
||||
table within.
|
||||
:param version_table_pk: boolean, whether the Alembic version table
|
||||
should use a primary key constraint for the "value" column; this
|
||||
only takes effect when the table is first created.
|
||||
Defaults to True; setting to False should not be necessary and is
|
||||
here for backwards compatibility reasons.
|
||||
:param on_version_apply: a callable or collection of callables to be
|
||||
run for each migration step.
|
||||
The callables will be run in the order they are given, once for
|
||||
each migration step, after the respective operation has been
|
||||
applied but before its transaction is finalized.
|
||||
Each callable accepts no positional arguments and the following
|
||||
keyword arguments:
|
||||
|
||||
* ``ctx``: the :class:`.MigrationContext` running the migration,
|
||||
* ``step``: a :class:`.MigrationInfo` representing the
|
||||
step currently being applied,
|
||||
* ``heads``: a collection of version strings representing the
|
||||
current heads,
|
||||
* ``run_args``: the ``**kwargs`` passed to :meth:`.run_migrations`.
|
||||
|
||||
Parameters specific to the autogenerate feature, when
|
||||
``alembic revision`` is run with the ``--autogenerate`` feature:
|
||||
|
||||
:param target_metadata: a :class:`sqlalchemy.schema.MetaData`
|
||||
object, or a sequence of :class:`~sqlalchemy.schema.MetaData`
|
||||
objects, that will be consulted during autogeneration.
|
||||
The tables present in each :class:`~sqlalchemy.schema.MetaData`
|
||||
will be compared against
|
||||
what is locally available on the target
|
||||
:class:`~sqlalchemy.engine.Connection`
|
||||
to produce candidate upgrade/downgrade operations.
|
||||
:param compare_type: Indicates type comparison behavior during
|
||||
an autogenerate
|
||||
operation. Defaults to ``True`` turning on type comparison, which
|
||||
has good accuracy on most backends. See :ref:`compare_types`
|
||||
for an example as well as information on other type
|
||||
comparison options. Set to ``False`` which disables type
|
||||
comparison. A callable can also be passed to provide custom type
|
||||
comparison, see :ref:`compare_types` for additional details.
|
||||
|
||||
.. versionchanged:: 1.12.0 The default value of
|
||||
:paramref:`.EnvironmentContext.configure.compare_type` has been
|
||||
changed to ``True``.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`compare_types`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.compare_server_default`
|
||||
|
||||
:param compare_server_default: Indicates server default comparison
|
||||
behavior during
|
||||
an autogenerate operation. Defaults to ``False`` which disables
|
||||
server default
|
||||
comparison. Set to ``True`` to turn on server default comparison,
|
||||
which has
|
||||
varied accuracy depending on backend.
|
||||
|
||||
To customize server default comparison behavior, a callable may
|
||||
be specified
|
||||
which can filter server default comparisons during an
|
||||
autogenerate operation.
|
||||
defaults during an autogenerate operation. The format of this
|
||||
callable is::
|
||||
|
||||
def my_compare_server_default(context, inspected_column,
|
||||
metadata_column, inspected_default, metadata_default,
|
||||
rendered_metadata_default):
|
||||
# return True if the defaults are different,
|
||||
# False if not, or None to allow the default implementation
|
||||
# to compare these defaults
|
||||
return None
|
||||
|
||||
context.configure(
|
||||
# ...
|
||||
compare_server_default = my_compare_server_default
|
||||
)
|
||||
|
||||
``inspected_column`` is a dictionary structure as returned by
|
||||
:meth:`sqlalchemy.engine.reflection.Inspector.get_columns`, whereas
|
||||
``metadata_column`` is a :class:`sqlalchemy.schema.Column` from
|
||||
the local model environment.
|
||||
|
||||
A return value of ``None`` indicates to allow default server default
|
||||
comparison
|
||||
to proceed. Note that some backends such as Postgresql actually
|
||||
execute
|
||||
the two defaults on the database side to compare for equivalence.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.compare_type`
|
||||
|
||||
:param include_name: A callable function which is given
|
||||
the chance to return ``True`` or ``False`` for any database reflected
|
||||
object based on its name, including database schema names when
|
||||
the :paramref:`.EnvironmentContext.configure.include_schemas` flag
|
||||
is set to ``True``.
|
||||
|
||||
The function accepts the following positional arguments:
|
||||
|
||||
* ``name``: the name of the object, such as schema name or table name.
|
||||
Will be ``None`` when indicating the default schema name of the
|
||||
database connection.
|
||||
* ``type``: a string describing the type of object; currently
|
||||
``"schema"``, ``"table"``, ``"column"``, ``"index"``,
|
||||
``"unique_constraint"``, or ``"foreign_key_constraint"``
|
||||
* ``parent_names``: a dictionary of "parent" object names, that are
|
||||
relative to the name being given. Keys in this dictionary may
|
||||
include: ``"schema_name"``, ``"table_name"`` or
|
||||
``"schema_qualified_table_name"``.
|
||||
|
||||
E.g.::
|
||||
|
||||
def include_name(name, type_, parent_names):
|
||||
if type_ == "schema":
|
||||
return name in ["schema_one", "schema_two"]
|
||||
else:
|
||||
return True
|
||||
|
||||
context.configure(
|
||||
# ...
|
||||
include_schemas = True,
|
||||
include_name = include_name
|
||||
)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogenerate_include_hooks`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.include_object`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.include_schemas`
|
||||
|
||||
|
||||
:param include_object: A callable function which is given
|
||||
the chance to return ``True`` or ``False`` for any object,
|
||||
indicating if the given object should be considered in the
|
||||
autogenerate sweep.
|
||||
|
||||
The function accepts the following positional arguments:
|
||||
|
||||
* ``object``: a :class:`~sqlalchemy.schema.SchemaItem` object such
|
||||
as a :class:`~sqlalchemy.schema.Table`,
|
||||
:class:`~sqlalchemy.schema.Column`,
|
||||
:class:`~sqlalchemy.schema.Index`
|
||||
:class:`~sqlalchemy.schema.UniqueConstraint`,
|
||||
or :class:`~sqlalchemy.schema.ForeignKeyConstraint` object
|
||||
* ``name``: the name of the object. This is typically available
|
||||
via ``object.name``.
|
||||
* ``type``: a string describing the type of object; currently
|
||||
``"table"``, ``"column"``, ``"index"``, ``"unique_constraint"``,
|
||||
or ``"foreign_key_constraint"``
|
||||
* ``reflected``: ``True`` if the given object was produced based on
|
||||
table reflection, ``False`` if it's from a local :class:`.MetaData`
|
||||
object.
|
||||
* ``compare_to``: the object being compared against, if available,
|
||||
else ``None``.
|
||||
|
||||
E.g.::
|
||||
|
||||
def include_object(object, name, type_, reflected, compare_to):
|
||||
if (type_ == "column" and
|
||||
not reflected and
|
||||
object.info.get("skip_autogenerate", False)):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
context.configure(
|
||||
# ...
|
||||
include_object = include_object
|
||||
)
|
||||
|
||||
For the use case of omitting specific schemas from a target database
|
||||
when :paramref:`.EnvironmentContext.configure.include_schemas` is
|
||||
set to ``True``, the :attr:`~sqlalchemy.schema.Table.schema`
|
||||
attribute can be checked for each :class:`~sqlalchemy.schema.Table`
|
||||
object passed to the hook, however it is much more efficient
|
||||
to filter on schemas before reflection of objects takes place
|
||||
using the :paramref:`.EnvironmentContext.configure.include_name`
|
||||
hook.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogenerate_include_hooks`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.include_name`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.include_schemas`
|
||||
|
||||
:param render_as_batch: if True, commands which alter elements
|
||||
within a table will be placed under a ``with batch_alter_table():``
|
||||
directive, so that batch migrations will take place.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`batch_migrations`
|
||||
|
||||
:param include_schemas: If True, autogenerate will scan across
|
||||
all schemas located by the SQLAlchemy
|
||||
:meth:`~sqlalchemy.engine.reflection.Inspector.get_schema_names`
|
||||
method, and include all differences in tables found across all
|
||||
those schemas. When using this option, you may want to also
|
||||
use the :paramref:`.EnvironmentContext.configure.include_name`
|
||||
parameter to specify a callable which
|
||||
can filter the tables/schemas that get included.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogenerate_include_hooks`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.include_name`
|
||||
|
||||
:paramref:`.EnvironmentContext.configure.include_object`
|
||||
|
||||
:param render_item: Callable that can be used to override how
|
||||
any schema item, i.e. column, constraint, type,
|
||||
etc., is rendered for autogenerate. The callable receives a
|
||||
string describing the type of object, the object, and
|
||||
the autogen context. If it returns False, the
|
||||
default rendering method will be used. If it returns None,
|
||||
the item will not be rendered in the context of a Table
|
||||
construct, that is, can be used to skip columns or constraints
|
||||
within op.create_table()::
|
||||
|
||||
def my_render_column(type_, col, autogen_context):
|
||||
if type_ == "column" and isinstance(col, MySpecialCol):
|
||||
return repr(col)
|
||||
else:
|
||||
return False
|
||||
|
||||
context.configure(
|
||||
# ...
|
||||
render_item = my_render_column
|
||||
)
|
||||
|
||||
Available values for the type string include: ``"column"``,
|
||||
``"primary_key"``, ``"foreign_key"``, ``"unique"``, ``"check"``,
|
||||
``"type"``, ``"server_default"``.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogen_render_types`
|
||||
|
||||
:param upgrade_token: When autogenerate completes, the text of the
|
||||
candidate upgrade operations will be present in this template
|
||||
variable when ``script.py.mako`` is rendered. Defaults to
|
||||
``upgrades``.
|
||||
:param downgrade_token: When autogenerate completes, the text of the
|
||||
candidate downgrade operations will be present in this
|
||||
template variable when ``script.py.mako`` is rendered. Defaults to
|
||||
``downgrades``.
|
||||
|
||||
:param alembic_module_prefix: When autogenerate refers to Alembic
|
||||
:mod:`alembic.operations` constructs, this prefix will be used
|
||||
(i.e. ``op.create_table``) Defaults to "``op.``".
|
||||
Can be ``None`` to indicate no prefix.
|
||||
|
||||
:param sqlalchemy_module_prefix: When autogenerate refers to
|
||||
SQLAlchemy
|
||||
:class:`~sqlalchemy.schema.Column` or type classes, this prefix
|
||||
will be used
|
||||
(i.e. ``sa.Column("somename", sa.Integer)``) Defaults to "``sa.``".
|
||||
Can be ``None`` to indicate no prefix.
|
||||
Note that when dialect-specific types are rendered, autogenerate
|
||||
will render them using the dialect module name, i.e. ``mssql.BIT()``,
|
||||
``postgresql.UUID()``.
|
||||
|
||||
:param user_module_prefix: When autogenerate refers to a SQLAlchemy
|
||||
type (e.g. :class:`.TypeEngine`) where the module name is not
|
||||
under the ``sqlalchemy`` namespace, this prefix will be used
|
||||
within autogenerate. If left at its default of
|
||||
``None``, the ``__module__`` attribute of the type is used to
|
||||
render the import module. It's a good practice to set this
|
||||
and to have all custom types be available from a fixed module space,
|
||||
in order to future-proof migration files against reorganizations
|
||||
in modules.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`autogen_module_prefix`
|
||||
|
||||
:param process_revision_directives: a callable function that will
|
||||
be passed a structure representing the end result of an autogenerate
|
||||
or plain "revision" operation, which can be manipulated to affect
|
||||
how the ``alembic revision`` command ultimately outputs new
|
||||
revision scripts. The structure of the callable is::
|
||||
|
||||
def process_revision_directives(context, revision, directives):
|
||||
pass
|
||||
|
||||
The ``directives`` parameter is a Python list containing
|
||||
a single :class:`.MigrationScript` directive, which represents
|
||||
the revision file to be generated. This list as well as its
|
||||
contents may be freely modified to produce any set of commands.
|
||||
The section :ref:`customizing_revision` shows an example of
|
||||
doing this. The ``context`` parameter is the
|
||||
:class:`.MigrationContext` in use,
|
||||
and ``revision`` is a tuple of revision identifiers representing the
|
||||
current revision of the database.
|
||||
|
||||
The callable is invoked at all times when the ``--autogenerate``
|
||||
option is passed to ``alembic revision``. If ``--autogenerate``
|
||||
is not passed, the callable is invoked only if the
|
||||
``revision_environment`` variable is set to True in the Alembic
|
||||
configuration, in which case the given ``directives`` collection
|
||||
will contain empty :class:`.UpgradeOps` and :class:`.DowngradeOps`
|
||||
collections for ``.upgrade_ops`` and ``.downgrade_ops``. The
|
||||
``--autogenerate`` option itself can be inferred by inspecting
|
||||
``context.config.cmd_opts.autogenerate``.
|
||||
|
||||
The callable function may optionally be an instance of
|
||||
a :class:`.Rewriter` object. This is a helper object that
|
||||
assists in the production of autogenerate-stream rewriter functions.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`customizing_revision`
|
||||
|
||||
:ref:`autogen_rewriter`
|
||||
|
||||
:paramref:`.command.revision.process_revision_directives`
|
||||
|
||||
:param autogenerate_plugins: A list of string names of "plugins" that
|
||||
should participate in this autogenerate run. Defaults to the list
|
||||
``["alembic.autogenerate.*"]``, which indicates that Alembic's default
|
||||
autogeneration plugins will be used.
|
||||
|
||||
See the section :ref:`plugins_autogenerate` for complete background
|
||||
on how to use this parameter.
|
||||
|
||||
.. versionadded:: 1.18.0 Added a new plugin system for autogenerate
|
||||
compare directives.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`plugins_autogenerate` - background on enabling/disabling
|
||||
autogenerate plugins
|
||||
|
||||
:ref:`alembic.plugins.toplevel` - Introduction and documentation
|
||||
to the plugin system
|
||||
|
||||
Parameters specific to individual backends:
|
||||
|
||||
:param mssql_batch_separator: The "batch separator" which will
|
||||
be placed between each statement when generating offline SQL Server
|
||||
migrations. Defaults to ``GO``. Note this is in addition to the
|
||||
customary semicolon ``;`` at the end of each statement; SQL Server
|
||||
considers the "batch separator" to denote the end of an
|
||||
individual statement execution, and cannot group certain
|
||||
dependent operations in one step.
|
||||
:param oracle_batch_separator: The "batch separator" which will
|
||||
be placed between each statement when generating offline
|
||||
Oracle migrations. Defaults to ``/``. Oracle doesn't add a
|
||||
semicolon between statements like most other backends.
|
||||
|
||||
"""
|
||||
|
||||
def execute(
|
||||
sql: Union[Executable, str],
|
||||
execution_options: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Execute the given SQL using the current change context.
|
||||
|
||||
The behavior of :meth:`.execute` is the same
|
||||
as that of :meth:`.Operations.execute`. Please see that
|
||||
function's documentation for full detail including
|
||||
caveats and limitations.
|
||||
|
||||
This function requires that a :class:`.MigrationContext` has
|
||||
first been made available via :meth:`.configure`.
|
||||
|
||||
"""
|
||||
|
||||
def get_bind() -> Connection:
|
||||
"""Return the current 'bind'.
|
||||
|
||||
In "online" mode, this is the
|
||||
:class:`sqlalchemy.engine.Connection` currently being used
|
||||
to emit SQL to the database.
|
||||
|
||||
This function requires that a :class:`.MigrationContext`
|
||||
has first been made available via :meth:`.configure`.
|
||||
|
||||
"""
|
||||
|
||||
def get_context() -> MigrationContext:
|
||||
"""Return the current :class:`.MigrationContext` object.
|
||||
|
||||
If :meth:`.EnvironmentContext.configure` has not been
|
||||
called yet, raises an exception.
|
||||
|
||||
"""
|
||||
|
||||
def get_head_revision() -> Union[str, Tuple[str, ...], None]:
|
||||
"""Return the hex identifier of the 'head' script revision.
|
||||
|
||||
If the script directory has multiple heads, this
|
||||
method raises a :class:`.CommandError`;
|
||||
:meth:`.EnvironmentContext.get_head_revisions` should be preferred.
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
.. seealso:: :meth:`.EnvironmentContext.get_head_revisions`
|
||||
|
||||
"""
|
||||
|
||||
def get_head_revisions() -> Union[str, Tuple[str, ...], None]:
|
||||
"""Return the hex identifier of the 'heads' script revision(s).
|
||||
|
||||
This returns a tuple containing the version number of all
|
||||
heads in the script directory.
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
"""
|
||||
|
||||
def get_revision_argument() -> Union[str, Tuple[str, ...], None]:
|
||||
"""Get the 'destination' revision argument.
|
||||
|
||||
This is typically the argument passed to the
|
||||
``upgrade`` or ``downgrade`` command.
|
||||
|
||||
If it was specified as ``head``, the actual
|
||||
version number is returned; if specified
|
||||
as ``base``, ``None`` is returned.
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
"""
|
||||
|
||||
def get_starting_revision_argument() -> Union[str, Tuple[str, ...], None]:
|
||||
"""Return the 'starting revision' argument,
|
||||
if the revision was passed using ``start:end``.
|
||||
|
||||
This is only meaningful in "offline" mode.
|
||||
Returns ``None`` if no value is available
|
||||
or was configured.
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
"""
|
||||
|
||||
def get_tag_argument() -> Optional[str]:
|
||||
"""Return the value passed for the ``--tag`` argument, if any.
|
||||
|
||||
The ``--tag`` argument is not used directly by Alembic,
|
||||
but is available for custom ``env.py`` configurations that
|
||||
wish to use it; particularly for offline generation scripts
|
||||
that wish to generate tagged filenames.
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.EnvironmentContext.get_x_argument` - a newer and more
|
||||
open ended system of extending ``env.py`` scripts via the command
|
||||
line.
|
||||
|
||||
"""
|
||||
|
||||
@overload
|
||||
def get_x_argument(as_dictionary: Literal[False]) -> List[str]: ...
|
||||
@overload
|
||||
def get_x_argument(as_dictionary: Literal[True]) -> Dict[str, str]: ...
|
||||
@overload
|
||||
def get_x_argument(
|
||||
as_dictionary: bool = ...,
|
||||
) -> Union[List[str], Dict[str, str]]:
|
||||
"""Return the value(s) passed for the ``-x`` argument, if any.
|
||||
|
||||
The ``-x`` argument is an open ended flag that allows any user-defined
|
||||
value or values to be passed on the command line, then available
|
||||
here for consumption by a custom ``env.py`` script.
|
||||
|
||||
The return value is a list, returned directly from the ``argparse``
|
||||
structure. If ``as_dictionary=True`` is passed, the ``x`` arguments
|
||||
are parsed using ``key=value`` format into a dictionary that is
|
||||
then returned. If there is no ``=`` in the argument, value is an empty
|
||||
string.
|
||||
|
||||
.. versionchanged:: 1.13.1 Support ``as_dictionary=True`` when
|
||||
arguments are passed without the ``=`` symbol.
|
||||
|
||||
For example, to support passing a database URL on the command line,
|
||||
the standard ``env.py`` script can be modified like this::
|
||||
|
||||
cmd_line_url = context.get_x_argument(
|
||||
as_dictionary=True).get('dbname')
|
||||
if cmd_line_url:
|
||||
engine = create_engine(cmd_line_url)
|
||||
else:
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
This then takes effect by running the ``alembic`` script as::
|
||||
|
||||
alembic -x dbname=postgresql://user:pass@host/dbname upgrade head
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.EnvironmentContext.get_tag_argument`
|
||||
|
||||
:attr:`.Config.cmd_opts`
|
||||
|
||||
"""
|
||||
|
||||
def is_offline_mode() -> bool:
|
||||
"""Return True if the current migrations environment
|
||||
is running in "offline mode".
|
||||
|
||||
This is ``True`` or ``False`` depending
|
||||
on the ``--sql`` flag passed.
|
||||
|
||||
This function does not require that the :class:`.MigrationContext`
|
||||
has been configured.
|
||||
|
||||
"""
|
||||
|
||||
def is_transactional_ddl() -> bool:
|
||||
"""Return True if the context is configured to expect a
|
||||
transactional DDL capable backend.
|
||||
|
||||
This defaults to the type of database in use, and
|
||||
can be overridden by the ``transactional_ddl`` argument
|
||||
to :meth:`.configure`
|
||||
|
||||
This function requires that a :class:`.MigrationContext`
|
||||
has first been made available via :meth:`.configure`.
|
||||
|
||||
"""
|
||||
|
||||
def run_migrations(**kw: Any) -> None:
|
||||
"""Run migrations as determined by the current command line
|
||||
configuration
|
||||
as well as versioning information present (or not) in the current
|
||||
database connection (if one is present).
|
||||
|
||||
The function accepts optional ``**kw`` arguments. If these are
|
||||
passed, they are sent directly to the ``upgrade()`` and
|
||||
``downgrade()``
|
||||
functions within each target revision file. By modifying the
|
||||
``script.py.mako`` file so that the ``upgrade()`` and ``downgrade()``
|
||||
functions accept arguments, parameters can be passed here so that
|
||||
contextual information, usually information to identify a particular
|
||||
database in use, can be passed from a custom ``env.py`` script
|
||||
to the migration functions.
|
||||
|
||||
This function requires that a :class:`.MigrationContext` has
|
||||
first been made available via :meth:`.configure`.
|
||||
|
||||
"""
|
||||
|
||||
script: ScriptDirectory
|
||||
|
||||
def static_output(text: str) -> None:
|
||||
"""Emit text directly to the "offline" SQL stream.
|
||||
|
||||
Typically this is for emitting comments that
|
||||
start with --. The statement is not treated
|
||||
as a SQL execution, no ; or batch separator
|
||||
is added, etc.
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import mssql
|
||||
from . import mysql
|
||||
from . import oracle
|
||||
from . import postgresql
|
||||
from . import sqlite
|
||||
from .impl import DefaultImpl as DefaultImpl
|
||||
@@ -0,0 +1,329 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.schema import ForeignKeyConstraint
|
||||
from sqlalchemy.sql.schema import Index
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from .. import util
|
||||
from ..util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
from alembic.autogenerate.api import AutogenContext
|
||||
from alembic.ddl.impl import DefaultImpl
|
||||
|
||||
CompareConstraintType = Union[Constraint, Index]
|
||||
|
||||
_C = TypeVar("_C", bound=CompareConstraintType)
|
||||
|
||||
_clsreg: Dict[str, Type[_constraint_sig]] = {}
|
||||
|
||||
|
||||
class ComparisonResult(NamedTuple):
|
||||
status: Literal["equal", "different", "skip"]
|
||||
message: str
|
||||
|
||||
@property
|
||||
def is_equal(self) -> bool:
|
||||
return self.status == "equal"
|
||||
|
||||
@property
|
||||
def is_different(self) -> bool:
|
||||
return self.status == "different"
|
||||
|
||||
@property
|
||||
def is_skip(self) -> bool:
|
||||
return self.status == "skip"
|
||||
|
||||
@classmethod
|
||||
def Equal(cls) -> ComparisonResult:
|
||||
"""the constraints are equal."""
|
||||
return cls("equal", "The two constraints are equal")
|
||||
|
||||
@classmethod
|
||||
def Different(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
|
||||
"""the constraints are different for the provided reason(s)."""
|
||||
return cls("different", ", ".join(util.to_list(reason)))
|
||||
|
||||
@classmethod
|
||||
def Skip(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
|
||||
"""the constraint cannot be compared for the provided reason(s).
|
||||
|
||||
The message is logged, but the constraints will be otherwise
|
||||
considered equal, meaning that no migration command will be
|
||||
generated.
|
||||
"""
|
||||
return cls("skip", ", ".join(util.to_list(reason)))
|
||||
|
||||
|
||||
class _constraint_sig(Generic[_C]):
|
||||
const: _C
|
||||
|
||||
_sig: Tuple[Any, ...]
|
||||
name: Optional[sqla_compat._ConstraintNameDefined]
|
||||
|
||||
impl: DefaultImpl
|
||||
|
||||
_is_index: ClassVar[bool] = False
|
||||
_is_fk: ClassVar[bool] = False
|
||||
_is_uq: ClassVar[bool] = False
|
||||
|
||||
_is_metadata: bool
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
cls._register()
|
||||
|
||||
@classmethod
|
||||
def _register(cls):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __init__(
|
||||
self, is_metadata: bool, impl: DefaultImpl, const: _C
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def compare_to_reflected(
|
||||
self, other: _constraint_sig[Any]
|
||||
) -> ComparisonResult:
|
||||
assert self.impl is other.impl
|
||||
assert self._is_metadata
|
||||
assert not other._is_metadata
|
||||
|
||||
return self._compare_to_reflected(other)
|
||||
|
||||
def _compare_to_reflected(
|
||||
self, other: _constraint_sig[_C]
|
||||
) -> ComparisonResult:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def from_constraint(
|
||||
cls, is_metadata: bool, impl: DefaultImpl, constraint: _C
|
||||
) -> _constraint_sig[_C]:
|
||||
# these could be cached by constraint/impl, however, if the
|
||||
# constraint is modified in place, then the sig is wrong. the mysql
|
||||
# impl currently does this, and if we fixed that we can't be sure
|
||||
# someone else might do it too, so play it safe.
|
||||
sig = _clsreg[constraint.__visit_name__](is_metadata, impl, constraint)
|
||||
return sig
|
||||
|
||||
def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]:
|
||||
return sqla_compat._get_constraint_final_name(
|
||||
self.const, context.dialect
|
||||
)
|
||||
|
||||
@util.memoized_property
|
||||
def is_named(self):
|
||||
return sqla_compat._constraint_is_named(self.const, self.impl.dialect)
|
||||
|
||||
@util.memoized_property
|
||||
def unnamed(self) -> Tuple[Any, ...]:
|
||||
return self._sig
|
||||
|
||||
@util.memoized_property
|
||||
def unnamed_no_options(self) -> Tuple[Any, ...]:
|
||||
raise NotImplementedError()
|
||||
|
||||
@util.memoized_property
|
||||
def _full_sig(self) -> Tuple[Any, ...]:
|
||||
return (self.name,) + self.unnamed
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return self._full_sig == other._full_sig
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
return self._full_sig != other._full_sig
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._full_sig)
|
||||
|
||||
|
||||
class _uq_constraint_sig(_constraint_sig[UniqueConstraint]):
|
||||
_is_uq = True
|
||||
|
||||
@classmethod
|
||||
def _register(cls) -> None:
|
||||
_clsreg["unique_constraint"] = cls
|
||||
|
||||
is_unique = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_metadata: bool,
|
||||
impl: DefaultImpl,
|
||||
const: UniqueConstraint,
|
||||
) -> None:
|
||||
self.impl = impl
|
||||
self.const = const
|
||||
self.name = sqla_compat.constraint_name_or_none(const.name)
|
||||
self._sig = tuple(sorted([col.name for col in const.columns]))
|
||||
self._is_metadata = is_metadata
|
||||
|
||||
@property
|
||||
def column_names(self) -> Tuple[str, ...]:
|
||||
return tuple([col.name for col in self.const.columns])
|
||||
|
||||
def _compare_to_reflected(
|
||||
self, other: _constraint_sig[_C]
|
||||
) -> ComparisonResult:
|
||||
assert self._is_metadata
|
||||
metadata_obj = self
|
||||
conn_obj = other
|
||||
|
||||
assert is_uq_sig(conn_obj)
|
||||
return self.impl.compare_unique_constraint(
|
||||
metadata_obj.const, conn_obj.const
|
||||
)
|
||||
|
||||
|
||||
class _ix_constraint_sig(_constraint_sig[Index]):
|
||||
_is_index = True
|
||||
|
||||
name: sqla_compat._ConstraintName
|
||||
|
||||
@classmethod
|
||||
def _register(cls) -> None:
|
||||
_clsreg["index"] = cls
|
||||
|
||||
def __init__(
|
||||
self, is_metadata: bool, impl: DefaultImpl, const: Index
|
||||
) -> None:
|
||||
self.impl = impl
|
||||
self.const = const
|
||||
self.name = const.name
|
||||
self.is_unique = bool(const.unique)
|
||||
self._is_metadata = is_metadata
|
||||
|
||||
def _compare_to_reflected(
|
||||
self, other: _constraint_sig[_C]
|
||||
) -> ComparisonResult:
|
||||
assert self._is_metadata
|
||||
metadata_obj = self
|
||||
conn_obj = other
|
||||
|
||||
assert is_index_sig(conn_obj)
|
||||
return self.impl.compare_indexes(metadata_obj.const, conn_obj.const)
|
||||
|
||||
@util.memoized_property
|
||||
def has_expressions(self):
|
||||
return sqla_compat.is_expression_index(self.const)
|
||||
|
||||
@util.memoized_property
|
||||
def column_names(self) -> Tuple[str, ...]:
|
||||
return tuple([col.name for col in self.const.columns])
|
||||
|
||||
@util.memoized_property
|
||||
def column_names_optional(self) -> Tuple[Optional[str], ...]:
|
||||
return tuple(
|
||||
[getattr(col, "name", None) for col in self.const.expressions]
|
||||
)
|
||||
|
||||
@util.memoized_property
|
||||
def is_named(self):
|
||||
return True
|
||||
|
||||
@util.memoized_property
|
||||
def unnamed(self):
|
||||
return (self.is_unique,) + self.column_names_optional
|
||||
|
||||
|
||||
class _fk_constraint_sig(_constraint_sig[ForeignKeyConstraint]):
|
||||
_is_fk = True
|
||||
|
||||
@classmethod
|
||||
def _register(cls) -> None:
|
||||
_clsreg["foreign_key_constraint"] = cls
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
is_metadata: bool,
|
||||
impl: DefaultImpl,
|
||||
const: ForeignKeyConstraint,
|
||||
) -> None:
|
||||
self._is_metadata = is_metadata
|
||||
|
||||
self.impl = impl
|
||||
self.const = const
|
||||
|
||||
self.name = sqla_compat.constraint_name_or_none(const.name)
|
||||
|
||||
(
|
||||
self.source_schema,
|
||||
self.source_table,
|
||||
self.source_columns,
|
||||
self.target_schema,
|
||||
self.target_table,
|
||||
self.target_columns,
|
||||
onupdate,
|
||||
ondelete,
|
||||
deferrable,
|
||||
initially,
|
||||
) = sqla_compat._fk_spec(const)
|
||||
|
||||
self._sig: Tuple[Any, ...] = (
|
||||
self.source_schema,
|
||||
self.source_table,
|
||||
tuple(self.source_columns),
|
||||
self.target_schema,
|
||||
self.target_table,
|
||||
tuple(self.target_columns),
|
||||
) + (
|
||||
(
|
||||
(None if onupdate.lower() == "no action" else onupdate.lower())
|
||||
if onupdate
|
||||
else None
|
||||
),
|
||||
(
|
||||
(None if ondelete.lower() == "no action" else ondelete.lower())
|
||||
if ondelete
|
||||
else None
|
||||
),
|
||||
# convert initially + deferrable into one three-state value
|
||||
(
|
||||
"initially_deferrable"
|
||||
if initially and initially.lower() == "deferred"
|
||||
else "deferrable" if deferrable else "not deferrable"
|
||||
),
|
||||
)
|
||||
|
||||
@util.memoized_property
|
||||
def unnamed_no_options(self):
|
||||
return (
|
||||
self.source_schema,
|
||||
self.source_table,
|
||||
tuple(self.source_columns),
|
||||
self.target_schema,
|
||||
self.target_table,
|
||||
tuple(self.target_columns),
|
||||
)
|
||||
|
||||
|
||||
def is_index_sig(sig: _constraint_sig) -> TypeGuard[_ix_constraint_sig]:
|
||||
return sig._is_index
|
||||
|
||||
|
||||
def is_uq_sig(sig: _constraint_sig) -> TypeGuard[_uq_constraint_sig]:
|
||||
return sig._is_uq
|
||||
|
||||
|
||||
def is_fk_sig(sig: _constraint_sig) -> TypeGuard[_fk_constraint_sig]:
|
||||
return sig._is_fk
|
||||
@@ -0,0 +1,406 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import exc
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.schema import Column
|
||||
from sqlalchemy.schema import DDLElement
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.elements import TextClause
|
||||
from sqlalchemy.sql.schema import FetchedValue
|
||||
|
||||
from ..util.sqla_compat import _columns_for_constraint # noqa
|
||||
from ..util.sqla_compat import _find_columns # noqa
|
||||
from ..util.sqla_compat import _fk_spec # noqa
|
||||
from ..util.sqla_compat import _is_type_bound # noqa
|
||||
from ..util.sqla_compat import _table_for_constraint # noqa
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from sqlalchemy import Computed
|
||||
from sqlalchemy import Identity
|
||||
from sqlalchemy.sql.compiler import Compiled
|
||||
from sqlalchemy.sql.compiler import DDLCompiler
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from .impl import DefaultImpl
|
||||
|
||||
_ServerDefaultType = Union[FetchedValue, str, TextClause, ColumnElement[Any]]
|
||||
|
||||
|
||||
class AlterTable(DDLElement):
|
||||
"""Represent an ALTER TABLE statement.
|
||||
|
||||
Only the string name and optional schema name of the table
|
||||
is required, not a full Table object.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
table_name: str,
|
||||
schema: Optional[Union[quoted_name, str]] = None,
|
||||
) -> None:
|
||||
self.table_name = table_name
|
||||
self.schema = schema
|
||||
|
||||
|
||||
class RenameTable(AlterTable):
|
||||
def __init__(
|
||||
self,
|
||||
old_table_name: str,
|
||||
new_table_name: Union[quoted_name, str],
|
||||
schema: Optional[Union[quoted_name, str]] = None,
|
||||
) -> None:
|
||||
super().__init__(old_table_name, schema=schema)
|
||||
self.new_table_name = new_table_name
|
||||
|
||||
|
||||
class AlterColumn(AlterTable):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column_name: str,
|
||||
schema: Optional[str] = None,
|
||||
existing_type: Optional[TypeEngine] = None,
|
||||
existing_nullable: Optional[bool] = None,
|
||||
existing_server_default: Optional[_ServerDefaultType] = None,
|
||||
existing_comment: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(name, schema=schema)
|
||||
self.column_name = column_name
|
||||
self.existing_type = (
|
||||
sqltypes.to_instance(existing_type)
|
||||
if existing_type is not None
|
||||
else None
|
||||
)
|
||||
self.existing_nullable = existing_nullable
|
||||
self.existing_server_default = existing_server_default
|
||||
self.existing_comment = existing_comment
|
||||
|
||||
|
||||
class ColumnNullable(AlterColumn):
|
||||
def __init__(
|
||||
self, name: str, column_name: str, nullable: bool, **kw
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.nullable = nullable
|
||||
|
||||
|
||||
class ColumnType(AlterColumn):
|
||||
def __init__(
|
||||
self, name: str, column_name: str, type_: TypeEngine, **kw
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.type_ = sqltypes.to_instance(type_)
|
||||
|
||||
|
||||
class ColumnName(AlterColumn):
|
||||
def __init__(
|
||||
self, name: str, column_name: str, newname: str, **kw
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.newname = newname
|
||||
|
||||
|
||||
class ColumnDefault(AlterColumn):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column_name: str,
|
||||
default: Optional[_ServerDefaultType],
|
||||
**kw,
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.default = default
|
||||
|
||||
|
||||
class ComputedColumnDefault(AlterColumn):
|
||||
def __init__(
|
||||
self, name: str, column_name: str, default: Optional[Computed], **kw
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.default = default
|
||||
|
||||
|
||||
class IdentityColumnDefault(AlterColumn):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column_name: str,
|
||||
default: Optional[Identity],
|
||||
impl: DefaultImpl,
|
||||
**kw,
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.default = default
|
||||
self.impl = impl
|
||||
|
||||
|
||||
class AddColumn(AlterTable):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column: Column[Any],
|
||||
schema: Optional[Union[quoted_name, str]] = None,
|
||||
if_not_exists: Optional[bool] = None,
|
||||
inline_references: Optional[bool] = None,
|
||||
inline_primary_key: Optional[bool] = None,
|
||||
) -> None:
|
||||
super().__init__(name, schema=schema)
|
||||
self.column = column
|
||||
self.if_not_exists = if_not_exists
|
||||
self.inline_references = inline_references
|
||||
self.inline_primary_key = inline_primary_key
|
||||
|
||||
|
||||
class DropColumn(AlterTable):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column: Column[Any],
|
||||
schema: Optional[str] = None,
|
||||
if_exists: Optional[bool] = None,
|
||||
) -> None:
|
||||
super().__init__(name, schema=schema)
|
||||
self.column = column
|
||||
self.if_exists = if_exists
|
||||
|
||||
|
||||
class ColumnComment(AlterColumn):
|
||||
def __init__(
|
||||
self, name: str, column_name: str, comment: Optional[str], **kw
|
||||
) -> None:
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.comment = comment
|
||||
|
||||
|
||||
@compiles(RenameTable)
|
||||
def visit_rename_table(
|
||||
element: RenameTable, compiler: DDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s RENAME TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_table_name(compiler, element.new_table_name, element.schema),
|
||||
)
|
||||
|
||||
|
||||
@compiles(AddColumn)
|
||||
def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
|
||||
return "%s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
add_column(
|
||||
compiler,
|
||||
element.column,
|
||||
if_not_exists=element.if_not_exists,
|
||||
inline_references=element.inline_references,
|
||||
inline_primary_key=element.inline_primary_key,
|
||||
**kw,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@compiles(DropColumn)
|
||||
def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str:
|
||||
return "%s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
drop_column(
|
||||
compiler, element.column.name, if_exists=element.if_exists, **kw
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnNullable)
|
||||
def visit_column_nullable(
|
||||
element: ColumnNullable, compiler: DDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
"DROP NOT NULL" if element.nullable else "SET NOT NULL",
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnType)
|
||||
def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
"TYPE %s" % format_type(compiler, element.type_),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnName)
|
||||
def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str:
|
||||
return "%s RENAME %s TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
format_column_name(compiler, element.newname),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnDefault)
|
||||
def visit_column_default(
|
||||
element: ColumnDefault, compiler: DDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
(
|
||||
"SET DEFAULT %s" % format_server_default(compiler, element.default)
|
||||
if element.default is not None
|
||||
else "DROP DEFAULT"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ComputedColumnDefault)
|
||||
def visit_computed_column(
|
||||
element: ComputedColumnDefault, compiler: DDLCompiler, **kw
|
||||
):
|
||||
raise exc.CompileError(
|
||||
'Adding or removing a "computed" construct, e.g. GENERATED '
|
||||
"ALWAYS AS, to or from an existing column is not supported."
|
||||
)
|
||||
|
||||
|
||||
@compiles(IdentityColumnDefault)
|
||||
def visit_identity_column(
|
||||
element: IdentityColumnDefault, compiler: DDLCompiler, **kw
|
||||
):
|
||||
raise exc.CompileError(
|
||||
'Adding, removing or modifying an "identity" construct, '
|
||||
"e.g. GENERATED AS IDENTITY, to or from an existing "
|
||||
"column is not supported in this dialect."
|
||||
)
|
||||
|
||||
|
||||
def quote_dotted(
|
||||
name: Union[quoted_name, str], quote: functools.partial
|
||||
) -> Union[quoted_name, str]:
|
||||
"""quote the elements of a dotted name"""
|
||||
|
||||
if isinstance(name, quoted_name):
|
||||
return quote(name)
|
||||
result = ".".join([quote(x) for x in name.split(".")])
|
||||
return result
|
||||
|
||||
|
||||
def format_table_name(
|
||||
compiler: Compiled,
|
||||
name: Union[quoted_name, str],
|
||||
schema: Optional[Union[quoted_name, str]],
|
||||
) -> Union[quoted_name, str]:
|
||||
quote = functools.partial(compiler.preparer.quote)
|
||||
if schema:
|
||||
return quote_dotted(schema, quote) + "." + quote(name)
|
||||
else:
|
||||
return quote(name)
|
||||
|
||||
|
||||
def format_column_name(
|
||||
compiler: DDLCompiler, name: Optional[Union[quoted_name, str]]
|
||||
) -> Union[quoted_name, str]:
|
||||
return compiler.preparer.quote(name) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def format_server_default(
|
||||
compiler: DDLCompiler,
|
||||
default: Optional[_ServerDefaultType],
|
||||
) -> str:
|
||||
# this can be updated to use compiler.render_default_string
|
||||
# for SQLAlchemy 2.0 and above; not in 1.4
|
||||
default_str = compiler.get_column_default_string(
|
||||
Column("x", Integer, server_default=default)
|
||||
)
|
||||
assert default_str is not None
|
||||
return default_str
|
||||
|
||||
|
||||
def format_type(compiler: DDLCompiler, type_: TypeEngine) -> str:
|
||||
return compiler.dialect.type_compiler.process(type_)
|
||||
|
||||
|
||||
def alter_table(
|
||||
compiler: DDLCompiler,
|
||||
name: str,
|
||||
schema: Optional[str],
|
||||
) -> str:
|
||||
return "ALTER TABLE %s" % format_table_name(compiler, name, schema)
|
||||
|
||||
|
||||
def drop_column(
|
||||
compiler: DDLCompiler, name: str, if_exists: Optional[bool] = None, **kw
|
||||
) -> str:
|
||||
return "DROP COLUMN %s%s" % (
|
||||
"IF EXISTS " if if_exists else "",
|
||||
format_column_name(compiler, name),
|
||||
)
|
||||
|
||||
|
||||
def alter_column(compiler: DDLCompiler, name: str) -> str:
|
||||
return "ALTER COLUMN %s" % format_column_name(compiler, name)
|
||||
|
||||
|
||||
def add_column(
|
||||
compiler: DDLCompiler,
|
||||
column: Column[Any],
|
||||
if_not_exists: Optional[bool] = None,
|
||||
inline_references: Optional[bool] = None,
|
||||
inline_primary_key: Optional[bool] = None,
|
||||
**kw,
|
||||
) -> str:
|
||||
text = "ADD COLUMN %s%s" % (
|
||||
"IF NOT EXISTS " if if_not_exists else "",
|
||||
compiler.get_column_specification(column, **kw),
|
||||
)
|
||||
|
||||
if inline_primary_key and column.primary_key:
|
||||
text += " PRIMARY KEY"
|
||||
|
||||
# Handle inline REFERENCES if requested
|
||||
# Only render inline if there's exactly one foreign key AND the
|
||||
# ForeignKeyConstraint is single-column, to avoid non-deterministic
|
||||
# behavior with sets and to ensure proper syntax
|
||||
if (
|
||||
inline_references
|
||||
and len(column.foreign_keys) == 1
|
||||
and (fk := list(column.foreign_keys)[0])
|
||||
and fk.constraint is not None
|
||||
and len(fk.constraint.columns) == 1
|
||||
):
|
||||
ref_col = fk.column
|
||||
ref_table = ref_col.table
|
||||
|
||||
# Format with proper quoting
|
||||
if ref_table.schema:
|
||||
table_name = "%s.%s" % (
|
||||
compiler.preparer.quote_schema(ref_table.schema),
|
||||
compiler.preparer.quote(ref_table.name),
|
||||
)
|
||||
else:
|
||||
table_name = compiler.preparer.quote(ref_table.name)
|
||||
|
||||
text += " REFERENCES %s (%s)" % (
|
||||
table_name,
|
||||
compiler.preparer.quote(ref_col.name),
|
||||
)
|
||||
|
||||
const = " ".join(
|
||||
compiler.process(constraint) for constraint in column.constraints
|
||||
)
|
||||
if const:
|
||||
text += " " + const
|
||||
|
||||
return text
|
||||
@@ -0,0 +1,921 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import cast
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy import PrimaryKeyConstraint
|
||||
from sqlalchemy import schema
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import text
|
||||
|
||||
from . import _autogen
|
||||
from . import base
|
||||
from ._autogen import _constraint_sig as _constraint_sig
|
||||
from ._autogen import ComparisonResult as ComparisonResult
|
||||
from .. import util
|
||||
from ..util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
from typing import TextIO
|
||||
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.engine import Dialect
|
||||
from sqlalchemy.engine.cursor import CursorResult
|
||||
from sqlalchemy.engine.interfaces import ReflectedForeignKeyConstraint
|
||||
from sqlalchemy.engine.interfaces import ReflectedIndex
|
||||
from sqlalchemy.engine.interfaces import ReflectedPrimaryKeyConstraint
|
||||
from sqlalchemy.engine.interfaces import ReflectedUniqueConstraint
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.sql import ClauseElement
|
||||
from sqlalchemy.sql import Executable
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.schema import ForeignKeyConstraint
|
||||
from sqlalchemy.sql.schema import Index
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
from sqlalchemy.sql.selectable import TableClause
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from .base import _ServerDefaultType
|
||||
from ..autogenerate.api import AutogenContext
|
||||
from ..operations.batch import ApplyBatchImpl
|
||||
from ..operations.batch import BatchOperationsImpl
|
||||
|
||||
_ReflectedConstraint = (
|
||||
ReflectedForeignKeyConstraint
|
||||
| ReflectedPrimaryKeyConstraint
|
||||
| ReflectedIndex
|
||||
| ReflectedUniqueConstraint
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImplMeta(type):
|
||||
def __init__(
|
||||
cls,
|
||||
classname: str,
|
||||
bases: Tuple[Type[DefaultImpl]],
|
||||
dict_: Dict[str, Any],
|
||||
):
|
||||
newtype = type.__init__(cls, classname, bases, dict_)
|
||||
if "__dialect__" in dict_:
|
||||
_impls[dict_["__dialect__"]] = cls # type: ignore[assignment]
|
||||
return newtype
|
||||
|
||||
|
||||
_impls: Dict[str, Type[DefaultImpl]] = {}
|
||||
|
||||
|
||||
class DefaultImpl(metaclass=ImplMeta):
|
||||
"""Provide the entrypoint for major migration operations,
|
||||
including database-specific behavioral variances.
|
||||
|
||||
While individual SQL/DDL constructs already provide
|
||||
for database-specific implementations, variances here
|
||||
allow for entirely different sequences of operations
|
||||
to take place for a particular migration, such as
|
||||
SQL Server's special 'IDENTITY INSERT' step for
|
||||
bulk inserts.
|
||||
|
||||
"""
|
||||
|
||||
__dialect__ = "default"
|
||||
|
||||
transactional_ddl = False
|
||||
command_terminator = ";"
|
||||
type_synonyms: Tuple[Set[str], ...] = ({"NUMERIC", "DECIMAL"},)
|
||||
type_arg_extract: Sequence[str] = ()
|
||||
# These attributes are deprecated in SQLAlchemy via #10247. They need to
|
||||
# be ignored to support older version that did not use dialect kwargs.
|
||||
# They only apply to Oracle and are replaced by oracle_order,
|
||||
# oracle_on_null
|
||||
identity_attrs_ignore: Tuple[str, ...] = ("order", "on_null")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dialect: Dialect,
|
||||
connection: Optional[Connection],
|
||||
as_sql: bool,
|
||||
transactional_ddl: Optional[bool],
|
||||
output_buffer: Optional[TextIO],
|
||||
context_opts: Dict[str, Any],
|
||||
) -> None:
|
||||
self.dialect = dialect
|
||||
self.connection = connection
|
||||
self.as_sql = as_sql
|
||||
self.literal_binds = context_opts.get("literal_binds", False)
|
||||
|
||||
self.output_buffer = output_buffer
|
||||
self.memo: dict = {}
|
||||
self.context_opts = context_opts
|
||||
if transactional_ddl is not None:
|
||||
self.transactional_ddl = transactional_ddl
|
||||
|
||||
if self.literal_binds:
|
||||
if not self.as_sql:
|
||||
raise util.CommandError(
|
||||
"Can't use literal_binds setting without as_sql mode"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_by_dialect(cls, dialect: Dialect) -> Type[DefaultImpl]:
|
||||
return _impls[dialect.name]
|
||||
|
||||
def static_output(self, text: str) -> None:
|
||||
assert self.output_buffer is not None
|
||||
self.output_buffer.write(text + "\n\n")
|
||||
self.output_buffer.flush()
|
||||
|
||||
def version_table_impl(
|
||||
self,
|
||||
*,
|
||||
version_table: str,
|
||||
version_table_schema: Optional[str],
|
||||
version_table_pk: bool,
|
||||
**kw: Any,
|
||||
) -> Table:
|
||||
"""Generate a :class:`.Table` object which will be used as the
|
||||
structure for the Alembic version table.
|
||||
|
||||
Third party dialects may override this hook to provide an alternate
|
||||
structure for this :class:`.Table`; requirements are only that it
|
||||
be named based on the ``version_table`` parameter and contains
|
||||
at least a single string-holding column named ``version_num``.
|
||||
|
||||
.. versionadded:: 1.14
|
||||
|
||||
"""
|
||||
vt = Table(
|
||||
version_table,
|
||||
MetaData(),
|
||||
Column("version_num", String(32), nullable=False),
|
||||
schema=version_table_schema,
|
||||
)
|
||||
if version_table_pk:
|
||||
vt.append_constraint(
|
||||
PrimaryKeyConstraint(
|
||||
"version_num", name=f"{version_table}_pkc"
|
||||
)
|
||||
)
|
||||
|
||||
return vt
|
||||
|
||||
def requires_recreate_in_batch(
|
||||
self, batch_op: BatchOperationsImpl
|
||||
) -> bool:
|
||||
"""Return True if the given :class:`.BatchOperationsImpl`
|
||||
would need the table to be recreated and copied in order to
|
||||
proceed.
|
||||
|
||||
Normally, only returns True on SQLite when operations other
|
||||
than add_column are present.
|
||||
|
||||
"""
|
||||
return False
|
||||
|
||||
def prep_table_for_batch(
|
||||
self, batch_impl: ApplyBatchImpl, table: Table
|
||||
) -> None:
|
||||
"""perform any operations needed on a table before a new
|
||||
one is created to replace it in batch mode.
|
||||
|
||||
the PG dialect uses this to drop constraints on the table
|
||||
before the new one uses those same names.
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def bind(self) -> Optional[Connection]:
|
||||
return self.connection
|
||||
|
||||
def _exec(
|
||||
self,
|
||||
construct: Union[Executable, str],
|
||||
execution_options: Optional[Mapping[str, Any]] = None,
|
||||
multiparams: Optional[Sequence[Mapping[str, Any]]] = None,
|
||||
params: Mapping[str, Any] = util.immutabledict(),
|
||||
) -> Optional[CursorResult]:
|
||||
if isinstance(construct, str):
|
||||
construct = text(construct)
|
||||
if self.as_sql:
|
||||
if multiparams is not None or params:
|
||||
raise TypeError("SQL parameters not allowed with as_sql")
|
||||
|
||||
compile_kw: dict[str, Any]
|
||||
if self.literal_binds and not isinstance(
|
||||
construct, schema.DDLElement
|
||||
):
|
||||
compile_kw = dict(compile_kwargs={"literal_binds": True})
|
||||
else:
|
||||
compile_kw = {}
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(construct, ClauseElement)
|
||||
compiled = construct.compile(dialect=self.dialect, **compile_kw)
|
||||
self.static_output(
|
||||
str(compiled).replace("\t", " ").strip()
|
||||
+ self.command_terminator
|
||||
)
|
||||
return None
|
||||
else:
|
||||
conn = self.connection
|
||||
assert conn is not None
|
||||
if execution_options:
|
||||
conn = conn.execution_options(**execution_options)
|
||||
|
||||
if params and multiparams is not None:
|
||||
raise TypeError(
|
||||
"Can't send params and multiparams at the same time"
|
||||
)
|
||||
|
||||
if multiparams:
|
||||
return conn.execute(construct, multiparams)
|
||||
else:
|
||||
return conn.execute(construct, params)
|
||||
|
||||
def execute(
|
||||
self,
|
||||
sql: Union[Executable, str],
|
||||
execution_options: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
self._exec(sql, execution_options)
|
||||
|
||||
def alter_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column_name: str,
|
||||
*,
|
||||
nullable: Optional[bool] = None,
|
||||
server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = False,
|
||||
name: Optional[str] = None,
|
||||
type_: Optional[TypeEngine] = None,
|
||||
schema: Optional[str] = None,
|
||||
autoincrement: Optional[bool] = None,
|
||||
comment: Optional[Union[str, Literal[False]]] = False,
|
||||
existing_comment: Optional[str] = None,
|
||||
existing_type: Optional[TypeEngine] = None,
|
||||
existing_server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = None,
|
||||
existing_nullable: Optional[bool] = None,
|
||||
existing_autoincrement: Optional[bool] = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
if autoincrement is not None or existing_autoincrement is not None:
|
||||
util.warn(
|
||||
"autoincrement and existing_autoincrement "
|
||||
"only make sense for MySQL",
|
||||
stacklevel=3,
|
||||
)
|
||||
if nullable is not None:
|
||||
self._exec(
|
||||
base.ColumnNullable(
|
||||
table_name,
|
||||
column_name,
|
||||
nullable,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
existing_comment=existing_comment,
|
||||
)
|
||||
)
|
||||
if server_default is not False:
|
||||
kw = {}
|
||||
cls_: Type[
|
||||
Union[
|
||||
base.ComputedColumnDefault,
|
||||
base.IdentityColumnDefault,
|
||||
base.ColumnDefault,
|
||||
]
|
||||
]
|
||||
if sqla_compat._server_default_is_computed(
|
||||
server_default, existing_server_default
|
||||
):
|
||||
cls_ = base.ComputedColumnDefault
|
||||
elif sqla_compat._server_default_is_identity(
|
||||
server_default, existing_server_default
|
||||
):
|
||||
cls_ = base.IdentityColumnDefault
|
||||
kw["impl"] = self
|
||||
else:
|
||||
cls_ = base.ColumnDefault
|
||||
self._exec(
|
||||
cls_(
|
||||
table_name,
|
||||
column_name,
|
||||
server_default, # type:ignore[arg-type]
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
existing_comment=existing_comment,
|
||||
**kw,
|
||||
)
|
||||
)
|
||||
if type_ is not None:
|
||||
self._exec(
|
||||
base.ColumnType(
|
||||
table_name,
|
||||
column_name,
|
||||
type_,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
existing_comment=existing_comment,
|
||||
)
|
||||
)
|
||||
|
||||
if comment is not False:
|
||||
self._exec(
|
||||
base.ColumnComment(
|
||||
table_name,
|
||||
column_name,
|
||||
comment,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
existing_comment=existing_comment,
|
||||
)
|
||||
)
|
||||
|
||||
# do the new name last ;)
|
||||
if name is not None:
|
||||
self._exec(
|
||||
base.ColumnName(
|
||||
table_name,
|
||||
column_name,
|
||||
name,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
)
|
||||
)
|
||||
|
||||
def add_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column: Column[Any],
|
||||
*,
|
||||
schema: Optional[Union[str, quoted_name]] = None,
|
||||
if_not_exists: Optional[bool] = None,
|
||||
inline_references: Optional[bool] = None,
|
||||
inline_primary_key: Optional[bool] = None,
|
||||
) -> None:
|
||||
self._exec(
|
||||
base.AddColumn(
|
||||
table_name,
|
||||
column,
|
||||
schema=schema,
|
||||
if_not_exists=if_not_exists,
|
||||
inline_references=inline_references,
|
||||
inline_primary_key=inline_primary_key,
|
||||
)
|
||||
)
|
||||
|
||||
def drop_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column: Column[Any],
|
||||
*,
|
||||
schema: Optional[str] = None,
|
||||
if_exists: Optional[bool] = None,
|
||||
**kw,
|
||||
) -> None:
|
||||
self._exec(
|
||||
base.DropColumn(
|
||||
table_name, column, schema=schema, if_exists=if_exists
|
||||
)
|
||||
)
|
||||
|
||||
def add_constraint(self, const: Any, **kw: Any) -> None:
|
||||
if const._create_rule is None or const._create_rule(self):
|
||||
if sqla_compat.sqla_2_1:
|
||||
# this should be the default already
|
||||
kw.setdefault("isolate_from_table", True)
|
||||
self._exec(schema.AddConstraint(const, **kw))
|
||||
|
||||
def drop_constraint(self, const: Constraint, **kw: Any) -> None:
|
||||
self._exec(schema.DropConstraint(const, **kw))
|
||||
|
||||
def rename_table(
|
||||
self,
|
||||
old_table_name: str,
|
||||
new_table_name: Union[str, quoted_name],
|
||||
schema: Optional[Union[str, quoted_name]] = None,
|
||||
) -> None:
|
||||
self._exec(
|
||||
base.RenameTable(old_table_name, new_table_name, schema=schema)
|
||||
)
|
||||
|
||||
def create_table(self, table: Table, **kw: Any) -> None:
|
||||
table.dispatch.before_create(
|
||||
table, self.connection, checkfirst=False, _ddl_runner=self
|
||||
)
|
||||
self._exec(schema.CreateTable(table, **kw))
|
||||
table.dispatch.after_create(
|
||||
table, self.connection, checkfirst=False, _ddl_runner=self
|
||||
)
|
||||
for index in table.indexes:
|
||||
self._exec(schema.CreateIndex(index))
|
||||
|
||||
with_comment = (
|
||||
self.dialect.supports_comments and not self.dialect.inline_comments
|
||||
)
|
||||
comment = table.comment
|
||||
if comment and with_comment:
|
||||
self.create_table_comment(table)
|
||||
|
||||
for column in table.columns:
|
||||
comment = column.comment
|
||||
if comment and with_comment:
|
||||
self.create_column_comment(column)
|
||||
|
||||
def drop_table(self, table: Table, **kw: Any) -> None:
|
||||
table.dispatch.before_drop(
|
||||
table, self.connection, checkfirst=False, _ddl_runner=self
|
||||
)
|
||||
self._exec(schema.DropTable(table, **kw))
|
||||
table.dispatch.after_drop(
|
||||
table, self.connection, checkfirst=False, _ddl_runner=self
|
||||
)
|
||||
|
||||
def create_index(self, index: Index, **kw: Any) -> None:
|
||||
self._exec(schema.CreateIndex(index, **kw))
|
||||
|
||||
def create_table_comment(self, table: Table) -> None:
|
||||
self._exec(schema.SetTableComment(table))
|
||||
|
||||
def drop_table_comment(self, table: Table) -> None:
|
||||
self._exec(schema.DropTableComment(table))
|
||||
|
||||
def create_column_comment(self, column: Column[Any]) -> None:
|
||||
self._exec(schema.SetColumnComment(column))
|
||||
|
||||
def drop_index(self, index: Index, **kw: Any) -> None:
|
||||
self._exec(schema.DropIndex(index, **kw))
|
||||
|
||||
def bulk_insert(
|
||||
self,
|
||||
table: Union[TableClause, Table],
|
||||
rows: List[dict],
|
||||
multiinsert: bool = True,
|
||||
) -> None:
|
||||
if not isinstance(rows, list):
|
||||
raise TypeError("List expected")
|
||||
elif rows and not isinstance(rows[0], dict):
|
||||
raise TypeError("List of dictionaries expected")
|
||||
if self.as_sql:
|
||||
for row in rows:
|
||||
self._exec(
|
||||
table.insert()
|
||||
.inline()
|
||||
.values(
|
||||
**{
|
||||
k: (
|
||||
sqla_compat._literal_bindparam(
|
||||
k, v, type_=table.c[k].type
|
||||
)
|
||||
if not isinstance(
|
||||
v, sqla_compat._literal_bindparam
|
||||
)
|
||||
else v
|
||||
)
|
||||
for k, v in row.items()
|
||||
}
|
||||
)
|
||||
)
|
||||
else:
|
||||
if rows:
|
||||
if multiinsert:
|
||||
self._exec(table.insert().inline(), multiparams=rows)
|
||||
else:
|
||||
for row in rows:
|
||||
self._exec(table.insert().inline().values(**row))
|
||||
|
||||
def _tokenize_column_type(self, column: Column) -> Params:
|
||||
definition: str
|
||||
definition = self.dialect.type_compiler.process(column.type).lower()
|
||||
|
||||
# tokenize the SQLAlchemy-generated version of a type, so that
|
||||
# the two can be compared.
|
||||
#
|
||||
# examples:
|
||||
# NUMERIC(10, 5)
|
||||
# TIMESTAMP WITH TIMEZONE
|
||||
# INTEGER UNSIGNED
|
||||
# INTEGER (10) UNSIGNED
|
||||
# INTEGER(10) UNSIGNED
|
||||
# varchar character set utf8
|
||||
#
|
||||
|
||||
tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition)
|
||||
|
||||
term_tokens: List[str] = []
|
||||
paren_term = None
|
||||
|
||||
for token in tokens:
|
||||
if re.match(r"^\(.*\)$", token):
|
||||
paren_term = token
|
||||
else:
|
||||
term_tokens.append(token)
|
||||
|
||||
params = Params(term_tokens[0], term_tokens[1:], [], {})
|
||||
|
||||
if paren_term:
|
||||
term: str
|
||||
for term in re.findall("[^(),]+", paren_term):
|
||||
if "=" in term:
|
||||
key, val = term.split("=")
|
||||
params.kwargs[key.strip()] = val.strip()
|
||||
else:
|
||||
params.args.append(term.strip())
|
||||
|
||||
return params
|
||||
|
||||
def _column_types_match(
|
||||
self, inspector_params: Params, metadata_params: Params
|
||||
) -> bool:
|
||||
if inspector_params.token0 == metadata_params.token0:
|
||||
return True
|
||||
|
||||
synonyms = [{t.lower() for t in batch} for batch in self.type_synonyms]
|
||||
inspector_all_terms = " ".join(
|
||||
[inspector_params.token0] + inspector_params.tokens
|
||||
)
|
||||
metadata_all_terms = " ".join(
|
||||
[metadata_params.token0] + metadata_params.tokens
|
||||
)
|
||||
|
||||
for batch in synonyms:
|
||||
if {inspector_all_terms, metadata_all_terms}.issubset(batch) or {
|
||||
inspector_params.token0,
|
||||
metadata_params.token0,
|
||||
}.issubset(batch):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _column_args_match(
|
||||
self, inspected_params: Params, meta_params: Params
|
||||
) -> bool:
|
||||
"""We want to compare column parameters. However, we only want
|
||||
to compare parameters that are set. If they both have `collation`,
|
||||
we want to make sure they are the same. However, if only one
|
||||
specifies it, dont flag it for being less specific
|
||||
"""
|
||||
|
||||
if (
|
||||
len(meta_params.tokens) == len(inspected_params.tokens)
|
||||
and meta_params.tokens != inspected_params.tokens
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
len(meta_params.args) == len(inspected_params.args)
|
||||
and meta_params.args != inspected_params.args
|
||||
):
|
||||
return False
|
||||
|
||||
insp = " ".join(inspected_params.tokens).lower()
|
||||
meta = " ".join(meta_params.tokens).lower()
|
||||
|
||||
for reg in self.type_arg_extract:
|
||||
mi = re.search(reg, insp)
|
||||
mm = re.search(reg, meta)
|
||||
|
||||
if mi and mm and mi.group(1) != mm.group(1):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def compare_type(
|
||||
self, inspector_column: Column[Any], metadata_column: Column
|
||||
) -> bool:
|
||||
"""Returns True if there ARE differences between the types of the two
|
||||
columns. Takes impl.type_synonyms into account between retrospected
|
||||
and metadata types
|
||||
"""
|
||||
inspector_params = self._tokenize_column_type(inspector_column)
|
||||
metadata_params = self._tokenize_column_type(metadata_column)
|
||||
|
||||
if not self._column_types_match(inspector_params, metadata_params):
|
||||
return True
|
||||
if not self._column_args_match(inspector_params, metadata_params):
|
||||
return True
|
||||
return False
|
||||
|
||||
def compare_server_default(
|
||||
self,
|
||||
inspector_column,
|
||||
metadata_column,
|
||||
rendered_metadata_default,
|
||||
rendered_inspector_default,
|
||||
):
|
||||
return rendered_inspector_default != rendered_metadata_default
|
||||
|
||||
def correct_for_autogen_constraints(
|
||||
self,
|
||||
conn_uniques: Set[UniqueConstraint],
|
||||
conn_indexes: Set[Index],
|
||||
metadata_unique_constraints: Set[UniqueConstraint],
|
||||
metadata_indexes: Set[Index],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def cast_for_batch_migrate(self, existing, existing_transfer, new_type):
|
||||
if existing.type._type_affinity is not new_type._type_affinity:
|
||||
existing_transfer["expr"] = cast(
|
||||
existing_transfer["expr"], new_type
|
||||
)
|
||||
|
||||
def render_ddl_sql_expr(
|
||||
self, expr: ClauseElement, is_server_default: bool = False, **kw: Any
|
||||
) -> str:
|
||||
"""Render a SQL expression that is typically a server default,
|
||||
index expression, etc.
|
||||
|
||||
"""
|
||||
|
||||
compile_kw = {"literal_binds": True, "include_table": False}
|
||||
|
||||
return str(
|
||||
expr.compile(dialect=self.dialect, compile_kwargs=compile_kw)
|
||||
)
|
||||
|
||||
def _compat_autogen_column_reflect(self, inspector: Inspector) -> Callable:
|
||||
return self.autogen_column_reflect
|
||||
|
||||
def correct_for_autogen_foreignkeys(
|
||||
self,
|
||||
conn_fks: Set[ForeignKeyConstraint],
|
||||
metadata_fks: Set[ForeignKeyConstraint],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
def autogen_column_reflect(self, inspector, table, column_info):
|
||||
"""A hook that is attached to the 'column_reflect' event for when
|
||||
a Table is reflected from the database during the autogenerate
|
||||
process.
|
||||
|
||||
Dialects can elect to modify the information gathered here.
|
||||
|
||||
"""
|
||||
|
||||
def start_migrations(self) -> None:
|
||||
"""A hook called when :meth:`.EnvironmentContext.run_migrations`
|
||||
is called.
|
||||
|
||||
Implementations can set up per-migration-run state here.
|
||||
|
||||
"""
|
||||
|
||||
def emit_begin(self) -> None:
|
||||
"""Emit the string ``BEGIN``, or the backend-specific
|
||||
equivalent, on the current connection context.
|
||||
|
||||
This is used in offline mode and typically
|
||||
via :meth:`.EnvironmentContext.begin_transaction`.
|
||||
|
||||
"""
|
||||
self.static_output("BEGIN" + self.command_terminator)
|
||||
|
||||
def emit_commit(self) -> None:
|
||||
"""Emit the string ``COMMIT``, or the backend-specific
|
||||
equivalent, on the current connection context.
|
||||
|
||||
This is used in offline mode and typically
|
||||
via :meth:`.EnvironmentContext.begin_transaction`.
|
||||
|
||||
"""
|
||||
self.static_output("COMMIT" + self.command_terminator)
|
||||
|
||||
def render_type(
|
||||
self, type_obj: TypeEngine, autogen_context: AutogenContext
|
||||
) -> Union[str, Literal[False]]:
|
||||
return False
|
||||
|
||||
def _compare_identity_default(self, metadata_identity, inspector_identity):
|
||||
# ignored contains the attributes that were not considered
|
||||
# because assumed to their default values in the db.
|
||||
diff, ignored = _compare_identity_options(
|
||||
metadata_identity,
|
||||
inspector_identity,
|
||||
schema.Identity(),
|
||||
skip={"always"},
|
||||
)
|
||||
|
||||
meta_always = getattr(metadata_identity, "always", None)
|
||||
inspector_always = getattr(inspector_identity, "always", None)
|
||||
# None and False are the same in this comparison
|
||||
if bool(meta_always) != bool(inspector_always):
|
||||
diff.add("always")
|
||||
|
||||
diff.difference_update(self.identity_attrs_ignore)
|
||||
|
||||
# returns 3 values:
|
||||
return (
|
||||
# different identity attributes
|
||||
diff,
|
||||
# ignored identity attributes
|
||||
ignored,
|
||||
# if the two identity should be considered different
|
||||
bool(diff) or bool(metadata_identity) != bool(inspector_identity),
|
||||
)
|
||||
|
||||
def _compare_index_unique(
|
||||
self, metadata_index: Index, reflected_index: Index
|
||||
) -> Optional[str]:
|
||||
conn_unique = bool(reflected_index.unique)
|
||||
meta_unique = bool(metadata_index.unique)
|
||||
if conn_unique != meta_unique:
|
||||
return f"unique={conn_unique} to unique={meta_unique}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def _create_metadata_constraint_sig(
|
||||
self, constraint: _autogen._C, **opts: Any
|
||||
) -> _constraint_sig[_autogen._C]:
|
||||
return _constraint_sig.from_constraint(True, self, constraint, **opts)
|
||||
|
||||
def _create_reflected_constraint_sig(
|
||||
self, constraint: _autogen._C, **opts: Any
|
||||
) -> _constraint_sig[_autogen._C]:
|
||||
return _constraint_sig.from_constraint(False, self, constraint, **opts)
|
||||
|
||||
def compare_indexes(
|
||||
self,
|
||||
metadata_index: Index,
|
||||
reflected_index: Index,
|
||||
) -> ComparisonResult:
|
||||
"""Compare two indexes by comparing the signature generated by
|
||||
``create_index_sig``.
|
||||
|
||||
This method returns a ``ComparisonResult``.
|
||||
"""
|
||||
msg: List[str] = []
|
||||
unique_msg = self._compare_index_unique(
|
||||
metadata_index, reflected_index
|
||||
)
|
||||
if unique_msg:
|
||||
msg.append(unique_msg)
|
||||
m_sig = self._create_metadata_constraint_sig(metadata_index)
|
||||
r_sig = self._create_reflected_constraint_sig(reflected_index)
|
||||
|
||||
assert _autogen.is_index_sig(m_sig)
|
||||
assert _autogen.is_index_sig(r_sig)
|
||||
|
||||
# The assumption is that the index have no expression
|
||||
for sig in m_sig, r_sig:
|
||||
if sig.has_expressions:
|
||||
log.warning(
|
||||
"Generating approximate signature for index %s. "
|
||||
"The dialect "
|
||||
"implementation should either skip expression indexes "
|
||||
"or provide a custom implementation.",
|
||||
sig.const,
|
||||
)
|
||||
|
||||
if m_sig.column_names != r_sig.column_names:
|
||||
msg.append(
|
||||
f"expression {r_sig.column_names} to {m_sig.column_names}"
|
||||
)
|
||||
|
||||
if msg:
|
||||
return ComparisonResult.Different(msg)
|
||||
else:
|
||||
return ComparisonResult.Equal()
|
||||
|
||||
def compare_unique_constraint(
|
||||
self,
|
||||
metadata_constraint: UniqueConstraint,
|
||||
reflected_constraint: UniqueConstraint,
|
||||
) -> ComparisonResult:
|
||||
"""Compare two unique constraints by comparing the two signatures.
|
||||
|
||||
The arguments are two tuples that contain the unique constraint and
|
||||
the signatures generated by ``create_unique_constraint_sig``.
|
||||
|
||||
This method returns a ``ComparisonResult``.
|
||||
"""
|
||||
metadata_tup = self._create_metadata_constraint_sig(
|
||||
metadata_constraint
|
||||
)
|
||||
reflected_tup = self._create_reflected_constraint_sig(
|
||||
reflected_constraint
|
||||
)
|
||||
|
||||
meta_sig = metadata_tup.unnamed
|
||||
conn_sig = reflected_tup.unnamed
|
||||
if conn_sig != meta_sig:
|
||||
return ComparisonResult.Different(
|
||||
f"expression {conn_sig} to {meta_sig}"
|
||||
)
|
||||
else:
|
||||
return ComparisonResult.Equal()
|
||||
|
||||
def _skip_functional_indexes(self, metadata_indexes, conn_indexes):
|
||||
conn_indexes_by_name = {c.name: c for c in conn_indexes}
|
||||
|
||||
for idx in list(metadata_indexes):
|
||||
if idx.name in conn_indexes_by_name:
|
||||
continue
|
||||
iex = sqla_compat.is_expression_index(idx)
|
||||
if iex:
|
||||
util.warn(
|
||||
"autogenerate skipping metadata-specified "
|
||||
"expression-based index "
|
||||
f"{idx.name!r}; dialect {self.__dialect__!r} under "
|
||||
f"SQLAlchemy {sqla_compat.sqlalchemy_version} can't "
|
||||
"reflect these indexes so they can't be compared"
|
||||
)
|
||||
metadata_indexes.discard(idx)
|
||||
|
||||
def adjust_reflected_dialect_options(
|
||||
self, reflected_object: _ReflectedConstraint, kind: str
|
||||
) -> Dict[str, Any]:
|
||||
return reflected_object.get("dialect_options", {}) # type: ignore[return-value] # noqa: E501
|
||||
|
||||
|
||||
class Params(NamedTuple):
|
||||
token0: str
|
||||
tokens: List[str]
|
||||
args: List[str]
|
||||
kwargs: Dict[str, str]
|
||||
|
||||
|
||||
def _compare_identity_options(
|
||||
metadata_io: Union[schema.Identity, schema.Sequence, None],
|
||||
inspector_io: Union[schema.Identity, schema.Sequence, None],
|
||||
default_io: Union[schema.Identity, schema.Sequence],
|
||||
skip: Set[str],
|
||||
):
|
||||
# this can be used for identity or sequence compare.
|
||||
# default_io is an instance of IdentityOption with all attributes to the
|
||||
# default value.
|
||||
meta_d = sqla_compat._get_identity_options_dict(metadata_io)
|
||||
insp_d = sqla_compat._get_identity_options_dict(inspector_io)
|
||||
|
||||
diff = set()
|
||||
ignored_attr = set()
|
||||
|
||||
def check_dicts(
|
||||
meta_dict: Mapping[str, Any],
|
||||
insp_dict: Mapping[str, Any],
|
||||
default_dict: Mapping[str, Any],
|
||||
attrs: Iterable[str],
|
||||
):
|
||||
for attr in set(attrs).difference(skip):
|
||||
meta_value = meta_dict.get(attr)
|
||||
insp_value = insp_dict.get(attr)
|
||||
if insp_value != meta_value:
|
||||
default_value = default_dict.get(attr)
|
||||
if meta_value == default_value:
|
||||
ignored_attr.add(attr)
|
||||
else:
|
||||
diff.add(attr)
|
||||
|
||||
check_dicts(
|
||||
meta_d,
|
||||
insp_d,
|
||||
sqla_compat._get_identity_options_dict(default_io),
|
||||
set(meta_d).union(insp_d),
|
||||
)
|
||||
if sqla_compat.identity_has_dialect_kwargs:
|
||||
assert hasattr(default_io, "dialect_kwargs")
|
||||
# use only the dialect kwargs in inspector_io since metadata_io
|
||||
# can have options for many backends
|
||||
check_dicts(
|
||||
getattr(metadata_io, "dialect_kwargs", {}),
|
||||
getattr(inspector_io, "dialect_kwargs", {}),
|
||||
default_io.dialect_kwargs,
|
||||
getattr(inspector_io, "dialect_kwargs", {}),
|
||||
)
|
||||
|
||||
return diff, ignored_attr
|
||||
@@ -0,0 +1,523 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.schema import Column
|
||||
from sqlalchemy.schema import CreateIndex
|
||||
from sqlalchemy.sql.base import Executable
|
||||
from sqlalchemy.sql.elements import ClauseElement
|
||||
|
||||
from .base import AddColumn
|
||||
from .base import alter_column
|
||||
from .base import alter_table
|
||||
from .base import ColumnComment
|
||||
from .base import ColumnDefault
|
||||
from .base import ColumnName
|
||||
from .base import ColumnNullable
|
||||
from .base import ColumnType
|
||||
from .base import format_column_name
|
||||
from .base import format_server_default
|
||||
from .base import format_table_name
|
||||
from .base import format_type
|
||||
from .base import RenameTable
|
||||
from .impl import DefaultImpl
|
||||
from .. import util
|
||||
from ..util import sqla_compat
|
||||
from ..util.sqla_compat import compiles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
from sqlalchemy.dialects.mssql.base import MSDDLCompiler
|
||||
from sqlalchemy.dialects.mssql.base import MSSQLCompiler
|
||||
from sqlalchemy.engine.cursor import CursorResult
|
||||
from sqlalchemy.sql.schema import Index
|
||||
from sqlalchemy.sql.schema import Table
|
||||
from sqlalchemy.sql.selectable import TableClause
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from .base import _ServerDefaultType
|
||||
from .impl import _ReflectedConstraint
|
||||
|
||||
|
||||
class MSSQLImpl(DefaultImpl):
|
||||
__dialect__ = "mssql"
|
||||
transactional_ddl = True
|
||||
batch_separator = "GO"
|
||||
|
||||
type_synonyms = DefaultImpl.type_synonyms + ({"VARCHAR", "NVARCHAR"},)
|
||||
identity_attrs_ignore = DefaultImpl.identity_attrs_ignore + (
|
||||
"minvalue",
|
||||
"maxvalue",
|
||||
"nominvalue",
|
||||
"nomaxvalue",
|
||||
"cycle",
|
||||
"cache",
|
||||
)
|
||||
|
||||
def __init__(self, *arg, **kw) -> None:
|
||||
super().__init__(*arg, **kw)
|
||||
self.batch_separator = self.context_opts.get(
|
||||
"mssql_batch_separator", self.batch_separator
|
||||
)
|
||||
|
||||
def _exec(self, construct: Any, *args, **kw) -> Optional[CursorResult]:
|
||||
result = super()._exec(construct, *args, **kw)
|
||||
if self.as_sql and self.batch_separator:
|
||||
self.static_output(self.batch_separator)
|
||||
return result
|
||||
|
||||
def emit_begin(self) -> None:
|
||||
self.static_output("BEGIN TRANSACTION" + self.command_terminator)
|
||||
|
||||
def emit_commit(self) -> None:
|
||||
super().emit_commit()
|
||||
if self.as_sql and self.batch_separator:
|
||||
self.static_output(self.batch_separator)
|
||||
|
||||
def alter_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column_name: str,
|
||||
*,
|
||||
nullable: Optional[bool] = None,
|
||||
server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = False,
|
||||
name: Optional[str] = None,
|
||||
type_: Optional[TypeEngine] = None,
|
||||
schema: Optional[str] = None,
|
||||
existing_type: Optional[TypeEngine] = None,
|
||||
existing_server_default: Union[
|
||||
_ServerDefaultType, Literal[False], None
|
||||
] = None,
|
||||
existing_nullable: Optional[bool] = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
if nullable is not None:
|
||||
if type_ is not None:
|
||||
# the NULL/NOT NULL alter will handle
|
||||
# the type alteration
|
||||
existing_type = type_
|
||||
type_ = None
|
||||
elif existing_type is None:
|
||||
raise util.CommandError(
|
||||
"MS-SQL ALTER COLUMN operations "
|
||||
"with NULL or NOT NULL require the "
|
||||
"existing_type or a new type_ be passed."
|
||||
)
|
||||
elif existing_nullable is not None and type_ is not None:
|
||||
nullable = existing_nullable
|
||||
|
||||
# the NULL/NOT NULL alter will handle
|
||||
# the type alteration
|
||||
existing_type = type_
|
||||
type_ = None
|
||||
|
||||
elif type_ is not None:
|
||||
util.warn(
|
||||
"MS-SQL ALTER COLUMN operations that specify type_= "
|
||||
"should also specify a nullable= or "
|
||||
"existing_nullable= argument to avoid implicit conversion "
|
||||
"of NOT NULL columns to NULL."
|
||||
)
|
||||
|
||||
used_default = False
|
||||
if sqla_compat._server_default_is_identity(
|
||||
server_default, existing_server_default
|
||||
) or sqla_compat._server_default_is_computed(
|
||||
server_default, existing_server_default
|
||||
):
|
||||
used_default = True
|
||||
kw["server_default"] = server_default
|
||||
kw["existing_server_default"] = existing_server_default
|
||||
|
||||
# drop existing default constraints before changing type
|
||||
# or default, see issue #1744
|
||||
if (
|
||||
server_default is not False
|
||||
and used_default is False
|
||||
and (
|
||||
existing_server_default is not False or server_default is None
|
||||
)
|
||||
):
|
||||
self._exec(
|
||||
_ExecDropConstraint(
|
||||
table_name,
|
||||
column_name,
|
||||
"sys.default_constraints",
|
||||
schema,
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: see why these two alter_columns can't be called
|
||||
# at once. joining them works but some of the mssql tests
|
||||
# seem to expect something different
|
||||
super().alter_column(
|
||||
table_name,
|
||||
column_name,
|
||||
nullable=nullable,
|
||||
type_=type_,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_nullable=existing_nullable,
|
||||
**kw,
|
||||
)
|
||||
|
||||
if server_default is not False and used_default is False:
|
||||
if server_default is not None:
|
||||
super().alter_column(
|
||||
table_name,
|
||||
column_name,
|
||||
schema=schema,
|
||||
server_default=server_default,
|
||||
)
|
||||
|
||||
if name is not None:
|
||||
super().alter_column(
|
||||
table_name, column_name, schema=schema, name=name
|
||||
)
|
||||
|
||||
def create_index(self, index: Index, **kw: Any) -> None:
|
||||
# this likely defaults to None if not present, so get()
|
||||
# should normally not return the default value. being
|
||||
# defensive in any case
|
||||
mssql_include = index.kwargs.get("mssql_include", None) or ()
|
||||
assert index.table is not None
|
||||
for col in mssql_include:
|
||||
if col not in index.table.c:
|
||||
index.table.append_column(Column(col, sqltypes.NullType))
|
||||
self._exec(CreateIndex(index, **kw))
|
||||
|
||||
def bulk_insert( # type:ignore[override]
|
||||
self, table: Union[TableClause, Table], rows: List[dict], **kw: Any
|
||||
) -> None:
|
||||
if self.as_sql:
|
||||
self._exec(
|
||||
"SET IDENTITY_INSERT %s ON"
|
||||
% self.dialect.identifier_preparer.format_table(table)
|
||||
)
|
||||
super().bulk_insert(table, rows, **kw)
|
||||
self._exec(
|
||||
"SET IDENTITY_INSERT %s OFF"
|
||||
% self.dialect.identifier_preparer.format_table(table)
|
||||
)
|
||||
else:
|
||||
super().bulk_insert(table, rows, **kw)
|
||||
|
||||
def drop_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column: Column[Any],
|
||||
*,
|
||||
schema: Optional[str] = None,
|
||||
**kw,
|
||||
) -> None:
|
||||
drop_default = kw.pop("mssql_drop_default", False)
|
||||
if drop_default:
|
||||
self._exec(
|
||||
_ExecDropConstraint(
|
||||
table_name, column, "sys.default_constraints", schema
|
||||
)
|
||||
)
|
||||
drop_check = kw.pop("mssql_drop_check", False)
|
||||
if drop_check:
|
||||
self._exec(
|
||||
_ExecDropConstraint(
|
||||
table_name, column, "sys.check_constraints", schema
|
||||
)
|
||||
)
|
||||
drop_fks = kw.pop("mssql_drop_foreign_key", False)
|
||||
if drop_fks:
|
||||
self._exec(_ExecDropFKConstraint(table_name, column, schema))
|
||||
super().drop_column(table_name, column, schema=schema, **kw)
|
||||
|
||||
def compare_server_default(
|
||||
self,
|
||||
inspector_column,
|
||||
metadata_column,
|
||||
rendered_metadata_default,
|
||||
rendered_inspector_default,
|
||||
):
|
||||
if rendered_metadata_default is not None:
|
||||
rendered_metadata_default = re.sub(
|
||||
r"[\(\) \"\']", "", rendered_metadata_default
|
||||
)
|
||||
|
||||
if rendered_inspector_default is not None:
|
||||
# SQL Server collapses whitespace and adds arbitrary parenthesis
|
||||
# within expressions. our only option is collapse all of it
|
||||
|
||||
rendered_inspector_default = re.sub(
|
||||
r"[\(\) \"\']", "", rendered_inspector_default
|
||||
)
|
||||
|
||||
return rendered_inspector_default != rendered_metadata_default
|
||||
|
||||
def _compare_identity_default(self, metadata_identity, inspector_identity):
|
||||
diff, ignored, is_alter = super()._compare_identity_default(
|
||||
metadata_identity, inspector_identity
|
||||
)
|
||||
|
||||
if (
|
||||
metadata_identity is None
|
||||
and inspector_identity is not None
|
||||
and not diff
|
||||
and inspector_identity.column is not None
|
||||
and inspector_identity.column.primary_key
|
||||
):
|
||||
# mssql reflect primary keys with autoincrement as identity
|
||||
# columns. if no different attributes are present ignore them
|
||||
is_alter = False
|
||||
|
||||
return diff, ignored, is_alter
|
||||
|
||||
def adjust_reflected_dialect_options(
|
||||
self, reflected_object: _ReflectedConstraint, kind: str
|
||||
) -> Dict[str, Any]:
|
||||
options: Dict[str, Any]
|
||||
options = reflected_object.get("dialect_options", {}).copy() # type: ignore[attr-defined] # noqa: E501
|
||||
if not options.get("mssql_include"):
|
||||
options.pop("mssql_include", None)
|
||||
if not options.get("mssql_clustered"):
|
||||
options.pop("mssql_clustered", None)
|
||||
return options
|
||||
|
||||
|
||||
class _ExecDropConstraint(Executable, ClauseElement):
|
||||
inherit_cache = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tname: str,
|
||||
colname: Union[Column[Any], str],
|
||||
type_: str,
|
||||
schema: Optional[str],
|
||||
) -> None:
|
||||
self.tname = tname
|
||||
self.colname = colname
|
||||
self.type_ = type_
|
||||
self.schema = schema
|
||||
|
||||
|
||||
class _ExecDropFKConstraint(Executable, ClauseElement):
|
||||
inherit_cache = False
|
||||
|
||||
def __init__(
|
||||
self, tname: str, colname: Column[Any], schema: Optional[str]
|
||||
) -> None:
|
||||
self.tname = tname
|
||||
self.colname = colname
|
||||
self.schema = schema
|
||||
|
||||
|
||||
@compiles(_ExecDropConstraint, "mssql")
|
||||
def _exec_drop_col_constraint(
|
||||
element: _ExecDropConstraint, compiler: MSSQLCompiler, **kw
|
||||
) -> str:
|
||||
schema, tname, colname, type_ = (
|
||||
element.schema,
|
||||
element.tname,
|
||||
element.colname,
|
||||
element.type_,
|
||||
)
|
||||
# from http://www.mssqltips.com/sqlservertip/1425/\
|
||||
# working-with-default-constraints-in-sql-server/
|
||||
return """declare @const_name varchar(256)
|
||||
select @const_name = QUOTENAME([name]) from %(type)s
|
||||
where parent_object_id = object_id('%(schema_dot)s%(tname)s')
|
||||
and col_name(parent_object_id, parent_column_id) = '%(colname)s'
|
||||
exec('alter table %(tname_quoted)s drop constraint ' + @const_name)""" % {
|
||||
"type": type_,
|
||||
"tname": tname,
|
||||
"colname": colname,
|
||||
"tname_quoted": format_table_name(compiler, tname, schema),
|
||||
"schema_dot": schema + "." if schema else "",
|
||||
}
|
||||
|
||||
|
||||
@compiles(_ExecDropFKConstraint, "mssql")
|
||||
def _exec_drop_col_fk_constraint(
|
||||
element: _ExecDropFKConstraint, compiler: MSSQLCompiler, **kw
|
||||
) -> str:
|
||||
schema, tname, colname = element.schema, element.tname, element.colname
|
||||
|
||||
return """declare @const_name varchar(256)
|
||||
select @const_name = QUOTENAME([name]) from
|
||||
sys.foreign_keys fk join sys.foreign_key_columns fkc
|
||||
on fk.object_id=fkc.constraint_object_id
|
||||
where fkc.parent_object_id = object_id('%(schema_dot)s%(tname)s')
|
||||
and col_name(fkc.parent_object_id, fkc.parent_column_id) = '%(colname)s'
|
||||
exec('alter table %(tname_quoted)s drop constraint ' + @const_name)""" % {
|
||||
"tname": tname,
|
||||
"colname": colname,
|
||||
"tname_quoted": format_table_name(compiler, tname, schema),
|
||||
"schema_dot": schema + "." if schema else "",
|
||||
}
|
||||
|
||||
|
||||
@compiles(AddColumn, "mssql")
|
||||
def visit_add_column(element: AddColumn, compiler: MSDDLCompiler, **kw) -> str:
|
||||
return "%s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
mssql_add_column(compiler, element.column, **kw),
|
||||
)
|
||||
|
||||
|
||||
def mssql_add_column(
|
||||
compiler: MSDDLCompiler, column: Column[Any], **kw
|
||||
) -> str:
|
||||
return "ADD %s" % compiler.get_column_specification(column, **kw)
|
||||
|
||||
|
||||
@compiles(ColumnNullable, "mssql")
|
||||
def visit_column_nullable(
|
||||
element: ColumnNullable, compiler: MSDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
format_type(compiler, element.existing_type), # type: ignore[arg-type]
|
||||
"NULL" if element.nullable else "NOT NULL",
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnDefault, "mssql")
|
||||
def visit_column_default(
|
||||
element: ColumnDefault, compiler: MSDDLCompiler, **kw
|
||||
) -> str:
|
||||
# TODO: there can also be a named constraint
|
||||
# with ADD CONSTRAINT here
|
||||
return "%s ADD DEFAULT %s FOR %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_server_default(compiler, element.default),
|
||||
format_column_name(compiler, element.column_name),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnName, "mssql")
|
||||
def visit_rename_column(
|
||||
element: ColumnName, compiler: MSDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "EXEC sp_rename '%s.%s', %s, 'COLUMN'" % (
|
||||
format_table_name(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
format_column_name(compiler, element.newname),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnType, "mssql")
|
||||
def visit_column_type(
|
||||
element: ColumnType, compiler: MSDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
format_type(compiler, element.type_),
|
||||
)
|
||||
|
||||
|
||||
@compiles(RenameTable, "mssql")
|
||||
def visit_rename_table(
|
||||
element: RenameTable, compiler: MSDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "EXEC sp_rename '%s', %s" % (
|
||||
format_table_name(compiler, element.table_name, element.schema),
|
||||
format_table_name(compiler, element.new_table_name, None),
|
||||
)
|
||||
|
||||
|
||||
def _add_column_comment(
|
||||
compiler: MSDDLCompiler,
|
||||
schema: Optional[str],
|
||||
tname: str,
|
||||
cname: str,
|
||||
comment: str,
|
||||
) -> str:
|
||||
schema_name = schema if schema else compiler.dialect.default_schema_name
|
||||
assert schema_name
|
||||
return (
|
||||
"exec sp_addextendedproperty 'MS_Description', {}, "
|
||||
"'schema', {}, 'table', {}, 'column', {}".format(
|
||||
compiler.sql_compiler.render_literal_value(
|
||||
comment, sqltypes.NVARCHAR()
|
||||
),
|
||||
compiler.preparer.quote_schema(schema_name),
|
||||
compiler.preparer.quote(tname),
|
||||
compiler.preparer.quote(cname),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _update_column_comment(
|
||||
compiler: MSDDLCompiler,
|
||||
schema: Optional[str],
|
||||
tname: str,
|
||||
cname: str,
|
||||
comment: str,
|
||||
) -> str:
|
||||
schema_name = schema if schema else compiler.dialect.default_schema_name
|
||||
assert schema_name
|
||||
return (
|
||||
"exec sp_updateextendedproperty 'MS_Description', {}, "
|
||||
"'schema', {}, 'table', {}, 'column', {}".format(
|
||||
compiler.sql_compiler.render_literal_value(
|
||||
comment, sqltypes.NVARCHAR()
|
||||
),
|
||||
compiler.preparer.quote_schema(schema_name),
|
||||
compiler.preparer.quote(tname),
|
||||
compiler.preparer.quote(cname),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _drop_column_comment(
|
||||
compiler: MSDDLCompiler, schema: Optional[str], tname: str, cname: str
|
||||
) -> str:
|
||||
schema_name = schema if schema else compiler.dialect.default_schema_name
|
||||
assert schema_name
|
||||
return (
|
||||
"exec sp_dropextendedproperty 'MS_Description', "
|
||||
"'schema', {}, 'table', {}, 'column', {}".format(
|
||||
compiler.preparer.quote_schema(schema_name),
|
||||
compiler.preparer.quote(tname),
|
||||
compiler.preparer.quote(cname),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnComment, "mssql")
|
||||
def visit_column_comment(
|
||||
element: ColumnComment, compiler: MSDDLCompiler, **kw: Any
|
||||
) -> str:
|
||||
if element.comment is not None:
|
||||
if element.existing_comment is not None:
|
||||
return _update_column_comment(
|
||||
compiler,
|
||||
element.schema,
|
||||
element.table_name,
|
||||
element.column_name,
|
||||
element.comment,
|
||||
)
|
||||
else:
|
||||
return _add_column_comment(
|
||||
compiler,
|
||||
element.schema,
|
||||
element.table_name,
|
||||
element.column_name,
|
||||
element.comment,
|
||||
)
|
||||
else:
|
||||
return _drop_column_comment(
|
||||
compiler, element.schema, element.table_name, element.column_name
|
||||
)
|
||||
@@ -0,0 +1,526 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import schema
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.sql import elements
|
||||
from sqlalchemy.sql import functions
|
||||
from sqlalchemy.sql import operators
|
||||
|
||||
from .base import alter_table
|
||||
from .base import AlterColumn
|
||||
from .base import ColumnDefault
|
||||
from .base import ColumnName
|
||||
from .base import ColumnNullable
|
||||
from .base import ColumnType
|
||||
from .base import format_column_name
|
||||
from .base import format_server_default
|
||||
from .impl import DefaultImpl
|
||||
from .. import util
|
||||
from ..util import sqla_compat
|
||||
from ..util.sqla_compat import _is_type_bound
|
||||
from ..util.sqla_compat import compiles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
from sqlalchemy.dialects.mysql.base import MySQLDDLCompiler
|
||||
from sqlalchemy.sql.ddl import DropConstraint
|
||||
from sqlalchemy.sql.elements import ClauseElement
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from .base import _ServerDefaultType
|
||||
|
||||
|
||||
class MySQLImpl(DefaultImpl):
|
||||
__dialect__ = "mysql"
|
||||
|
||||
transactional_ddl = False
|
||||
type_synonyms = DefaultImpl.type_synonyms + (
|
||||
{"BOOL", "TINYINT"},
|
||||
{"JSON", "LONGTEXT"},
|
||||
)
|
||||
type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
|
||||
|
||||
def render_ddl_sql_expr(
|
||||
self,
|
||||
expr: ClauseElement,
|
||||
is_server_default: bool = False,
|
||||
is_index: bool = False,
|
||||
**kw: Any,
|
||||
) -> str:
|
||||
# apply Grouping to index expressions;
|
||||
# see https://github.com/sqlalchemy/sqlalchemy/blob/
|
||||
# 36da2eaf3e23269f2cf28420ae73674beafd0661/
|
||||
# lib/sqlalchemy/dialects/mysql/base.py#L2191
|
||||
if is_index and (
|
||||
isinstance(expr, elements.BinaryExpression)
|
||||
or (
|
||||
isinstance(expr, elements.UnaryExpression)
|
||||
and expr.modifier not in (operators.desc_op, operators.asc_op)
|
||||
)
|
||||
or isinstance(expr, functions.FunctionElement)
|
||||
):
|
||||
expr = elements.Grouping(expr)
|
||||
|
||||
return super().render_ddl_sql_expr(
|
||||
expr, is_server_default=is_server_default, is_index=is_index, **kw
|
||||
)
|
||||
|
||||
def alter_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column_name: str,
|
||||
*,
|
||||
nullable: Optional[bool] = None,
|
||||
server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = False,
|
||||
name: Optional[str] = None,
|
||||
type_: Optional[TypeEngine] = None,
|
||||
schema: Optional[str] = None,
|
||||
existing_type: Optional[TypeEngine] = None,
|
||||
existing_server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = None,
|
||||
existing_nullable: Optional[bool] = None,
|
||||
autoincrement: Optional[bool] = None,
|
||||
existing_autoincrement: Optional[bool] = None,
|
||||
comment: Optional[Union[str, Literal[False]]] = False,
|
||||
existing_comment: Optional[str] = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
if sqla_compat._server_default_is_identity(
|
||||
server_default, existing_server_default
|
||||
) or sqla_compat._server_default_is_computed(
|
||||
server_default, existing_server_default
|
||||
):
|
||||
# modifying computed or identity columns is not supported
|
||||
# the default will raise
|
||||
super().alter_column(
|
||||
table_name,
|
||||
column_name,
|
||||
nullable=nullable,
|
||||
type_=type_,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_nullable=existing_nullable,
|
||||
server_default=server_default,
|
||||
existing_server_default=existing_server_default,
|
||||
**kw,
|
||||
)
|
||||
if name is not None or self._is_mysql_allowed_functional_default(
|
||||
type_ if type_ is not None else existing_type, server_default
|
||||
):
|
||||
self._exec(
|
||||
MySQLChangeColumn(
|
||||
table_name,
|
||||
column_name,
|
||||
schema=schema,
|
||||
newname=name if name is not None else column_name,
|
||||
nullable=(
|
||||
nullable
|
||||
if nullable is not None
|
||||
else (
|
||||
existing_nullable
|
||||
if existing_nullable is not None
|
||||
else True
|
||||
)
|
||||
),
|
||||
type_=type_ if type_ is not None else existing_type,
|
||||
default=(
|
||||
server_default
|
||||
if server_default is not False
|
||||
else existing_server_default
|
||||
),
|
||||
autoincrement=(
|
||||
autoincrement
|
||||
if autoincrement is not None
|
||||
else existing_autoincrement
|
||||
),
|
||||
comment=(
|
||||
comment if comment is not False else existing_comment
|
||||
),
|
||||
)
|
||||
)
|
||||
elif (
|
||||
nullable is not None
|
||||
or type_ is not None
|
||||
or autoincrement is not None
|
||||
or comment is not False
|
||||
):
|
||||
self._exec(
|
||||
MySQLModifyColumn(
|
||||
table_name,
|
||||
column_name,
|
||||
schema=schema,
|
||||
newname=name if name is not None else column_name,
|
||||
nullable=(
|
||||
nullable
|
||||
if nullable is not None
|
||||
else (
|
||||
existing_nullable
|
||||
if existing_nullable is not None
|
||||
else True
|
||||
)
|
||||
),
|
||||
type_=type_ if type_ is not None else existing_type,
|
||||
default=(
|
||||
server_default
|
||||
if server_default is not False
|
||||
else existing_server_default
|
||||
),
|
||||
autoincrement=(
|
||||
autoincrement
|
||||
if autoincrement is not None
|
||||
else existing_autoincrement
|
||||
),
|
||||
comment=(
|
||||
comment if comment is not False else existing_comment
|
||||
),
|
||||
)
|
||||
)
|
||||
elif server_default is not False:
|
||||
self._exec(
|
||||
MySQLAlterDefault(
|
||||
table_name, column_name, server_default, schema=schema
|
||||
)
|
||||
)
|
||||
|
||||
def drop_constraint(
|
||||
self,
|
||||
const: Constraint,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
if isinstance(const, schema.CheckConstraint) and _is_type_bound(const):
|
||||
return
|
||||
|
||||
super().drop_constraint(const)
|
||||
|
||||
def _is_mysql_allowed_functional_default(
|
||||
self,
|
||||
type_: Optional[TypeEngine],
|
||||
server_default: Optional[Union[_ServerDefaultType, Literal[False]]],
|
||||
) -> bool:
|
||||
return (
|
||||
type_ is not None
|
||||
and type_._type_affinity is sqltypes.DateTime
|
||||
and server_default is not None
|
||||
)
|
||||
|
||||
def compare_server_default(
|
||||
self,
|
||||
inspector_column,
|
||||
metadata_column,
|
||||
rendered_metadata_default,
|
||||
rendered_inspector_default,
|
||||
):
|
||||
# partially a workaround for SQLAlchemy issue #3023; if the
|
||||
# column were created without "NOT NULL", MySQL may have added
|
||||
# an implicit default of '0' which we need to skip
|
||||
# TODO: this is not really covered anymore ?
|
||||
if (
|
||||
metadata_column.type._type_affinity is sqltypes.Integer
|
||||
and inspector_column.primary_key
|
||||
and not inspector_column.autoincrement
|
||||
and not rendered_metadata_default
|
||||
and rendered_inspector_default == "'0'"
|
||||
):
|
||||
return False
|
||||
elif (
|
||||
rendered_inspector_default
|
||||
and inspector_column.type._type_affinity is sqltypes.Integer
|
||||
):
|
||||
rendered_inspector_default = (
|
||||
re.sub(r"^'|'$", "", rendered_inspector_default)
|
||||
if rendered_inspector_default is not None
|
||||
else None
|
||||
)
|
||||
return rendered_inspector_default != rendered_metadata_default
|
||||
elif (
|
||||
rendered_metadata_default
|
||||
and metadata_column.type._type_affinity is sqltypes.String
|
||||
):
|
||||
metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default)
|
||||
return rendered_inspector_default != f"'{metadata_default}'"
|
||||
elif rendered_inspector_default and rendered_metadata_default:
|
||||
# adjust for "function()" vs. "FUNCTION" as can occur particularly
|
||||
# for the CURRENT_TIMESTAMP function on newer MariaDB versions
|
||||
|
||||
# SQLAlchemy MySQL dialect bundles ON UPDATE into the server
|
||||
# default; adjust for this possibly being present.
|
||||
onupdate_ins = re.match(
|
||||
r"(.*) (on update.*?)(?:\(\))?$",
|
||||
rendered_inspector_default.lower(),
|
||||
)
|
||||
onupdate_met = re.match(
|
||||
r"(.*) (on update.*?)(?:\(\))?$",
|
||||
rendered_metadata_default.lower(),
|
||||
)
|
||||
|
||||
if onupdate_ins:
|
||||
if not onupdate_met:
|
||||
return True
|
||||
elif onupdate_ins.group(2) != onupdate_met.group(2):
|
||||
return True
|
||||
|
||||
rendered_inspector_default = onupdate_ins.group(1)
|
||||
rendered_metadata_default = onupdate_met.group(1)
|
||||
|
||||
return re.sub(
|
||||
r"(.*?)(?:\(\))?$", r"\1", rendered_inspector_default.lower()
|
||||
) != re.sub(
|
||||
r"(.*?)(?:\(\))?$", r"\1", rendered_metadata_default.lower()
|
||||
)
|
||||
else:
|
||||
return rendered_inspector_default != rendered_metadata_default
|
||||
|
||||
def correct_for_autogen_constraints(
|
||||
self,
|
||||
conn_unique_constraints,
|
||||
conn_indexes,
|
||||
metadata_unique_constraints,
|
||||
metadata_indexes,
|
||||
):
|
||||
# TODO: if SQLA 1.0, make use of "duplicates_index"
|
||||
# metadata
|
||||
removed = set()
|
||||
for idx in list(conn_indexes):
|
||||
if idx.unique:
|
||||
continue
|
||||
# MySQL puts implicit indexes on FK columns, even if
|
||||
# composite and even if MyISAM, so can't check this too easily.
|
||||
# the name of the index may be the column name or it may
|
||||
# be the name of the FK constraint.
|
||||
for col in idx.columns:
|
||||
if idx.name == col.name:
|
||||
conn_indexes.remove(idx)
|
||||
removed.add(idx.name)
|
||||
break
|
||||
for fk in col.foreign_keys:
|
||||
if fk.name == idx.name:
|
||||
conn_indexes.remove(idx)
|
||||
removed.add(idx.name)
|
||||
break
|
||||
if idx.name in removed:
|
||||
break
|
||||
|
||||
# then remove indexes from the "metadata_indexes"
|
||||
# that we've removed from reflected, otherwise they come out
|
||||
# as adds (see #202)
|
||||
for idx in list(metadata_indexes):
|
||||
if idx.name in removed:
|
||||
metadata_indexes.remove(idx)
|
||||
|
||||
def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks):
|
||||
conn_fk_by_sig = {
|
||||
self._create_reflected_constraint_sig(fk).unnamed_no_options: fk
|
||||
for fk in conn_fks
|
||||
}
|
||||
metadata_fk_by_sig = {
|
||||
self._create_metadata_constraint_sig(fk).unnamed_no_options: fk
|
||||
for fk in metadata_fks
|
||||
}
|
||||
|
||||
for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig):
|
||||
mdfk = metadata_fk_by_sig[sig]
|
||||
cnfk = conn_fk_by_sig[sig]
|
||||
# MySQL considers RESTRICT to be the default and doesn't
|
||||
# report on it. if the model has explicit RESTRICT and
|
||||
# the conn FK has None, set it to RESTRICT
|
||||
if (
|
||||
mdfk.ondelete is not None
|
||||
and mdfk.ondelete.lower() == "restrict"
|
||||
and cnfk.ondelete is None
|
||||
):
|
||||
cnfk.ondelete = "RESTRICT"
|
||||
if (
|
||||
mdfk.onupdate is not None
|
||||
and mdfk.onupdate.lower() == "restrict"
|
||||
and cnfk.onupdate is None
|
||||
):
|
||||
cnfk.onupdate = "RESTRICT"
|
||||
|
||||
|
||||
class MariaDBImpl(MySQLImpl):
|
||||
__dialect__ = "mariadb"
|
||||
|
||||
|
||||
class MySQLAlterDefault(AlterColumn):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column_name: str,
|
||||
default: Optional[_ServerDefaultType],
|
||||
schema: Optional[str] = None,
|
||||
) -> None:
|
||||
super(AlterColumn, self).__init__(name, schema=schema)
|
||||
self.column_name = column_name
|
||||
self.default = default
|
||||
|
||||
|
||||
class MySQLChangeColumn(AlterColumn):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
column_name: str,
|
||||
schema: Optional[str] = None,
|
||||
newname: Optional[str] = None,
|
||||
type_: Optional[TypeEngine] = None,
|
||||
nullable: Optional[bool] = None,
|
||||
default: Optional[Union[_ServerDefaultType, Literal[False]]] = False,
|
||||
autoincrement: Optional[bool] = None,
|
||||
comment: Optional[Union[str, Literal[False]]] = False,
|
||||
) -> None:
|
||||
super(AlterColumn, self).__init__(name, schema=schema)
|
||||
self.column_name = column_name
|
||||
self.nullable = nullable
|
||||
self.newname = newname
|
||||
self.default = default
|
||||
self.autoincrement = autoincrement
|
||||
self.comment = comment
|
||||
if type_ is None:
|
||||
raise util.CommandError(
|
||||
"All MySQL CHANGE/MODIFY COLUMN operations "
|
||||
"require the existing type."
|
||||
)
|
||||
|
||||
self.type_ = sqltypes.to_instance(type_)
|
||||
|
||||
|
||||
class MySQLModifyColumn(MySQLChangeColumn):
|
||||
pass
|
||||
|
||||
|
||||
@compiles(ColumnNullable, "mysql", "mariadb")
|
||||
@compiles(ColumnName, "mysql", "mariadb")
|
||||
@compiles(ColumnDefault, "mysql", "mariadb")
|
||||
@compiles(ColumnType, "mysql", "mariadb")
|
||||
def _mysql_doesnt_support_individual(element, compiler, **kw):
|
||||
raise NotImplementedError(
|
||||
"Individual alter column constructs not supported by MySQL"
|
||||
)
|
||||
|
||||
|
||||
@compiles(MySQLAlterDefault, "mysql", "mariadb")
|
||||
def _mysql_alter_default(
|
||||
element: MySQLAlterDefault, compiler: MySQLDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s ALTER COLUMN %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
(
|
||||
"SET DEFAULT %s" % format_server_default(compiler, element.default)
|
||||
if element.default is not None
|
||||
else "DROP DEFAULT"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@compiles(MySQLModifyColumn, "mysql", "mariadb")
|
||||
def _mysql_modify_column(
|
||||
element: MySQLModifyColumn, compiler: MySQLDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s MODIFY %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
_mysql_colspec(
|
||||
compiler,
|
||||
nullable=element.nullable,
|
||||
server_default=element.default,
|
||||
type_=element.type_,
|
||||
autoincrement=element.autoincrement,
|
||||
comment=element.comment,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@compiles(MySQLChangeColumn, "mysql", "mariadb")
|
||||
def _mysql_change_column(
|
||||
element: MySQLChangeColumn, compiler: MySQLDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s CHANGE %s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
format_column_name(compiler, element.newname),
|
||||
_mysql_colspec(
|
||||
compiler,
|
||||
nullable=element.nullable,
|
||||
server_default=element.default,
|
||||
type_=element.type_,
|
||||
autoincrement=element.autoincrement,
|
||||
comment=element.comment,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _mysql_colspec(
|
||||
compiler: MySQLDDLCompiler,
|
||||
nullable: Optional[bool],
|
||||
server_default: Optional[Union[_ServerDefaultType, Literal[False]]],
|
||||
type_: TypeEngine,
|
||||
autoincrement: Optional[bool],
|
||||
comment: Optional[Union[str, Literal[False]]],
|
||||
) -> str:
|
||||
spec = "%s %s" % (
|
||||
compiler.dialect.type_compiler.process(type_),
|
||||
"NULL" if nullable else "NOT NULL",
|
||||
)
|
||||
if autoincrement:
|
||||
spec += " AUTO_INCREMENT"
|
||||
if server_default is not False and server_default is not None:
|
||||
spec += " DEFAULT %s" % format_server_default(compiler, server_default)
|
||||
if comment:
|
||||
spec += " COMMENT %s" % compiler.sql_compiler.render_literal_value(
|
||||
comment, sqltypes.String()
|
||||
)
|
||||
|
||||
return spec
|
||||
|
||||
|
||||
@compiles(schema.DropConstraint, "mysql", "mariadb")
|
||||
def _mysql_drop_constraint(
|
||||
element: DropConstraint, compiler: MySQLDDLCompiler, **kw
|
||||
) -> str:
|
||||
"""Redefine SQLAlchemy's drop constraint to
|
||||
raise errors for invalid constraint type."""
|
||||
|
||||
constraint = element.element
|
||||
if isinstance(
|
||||
constraint,
|
||||
(
|
||||
schema.ForeignKeyConstraint,
|
||||
schema.PrimaryKeyConstraint,
|
||||
schema.UniqueConstraint,
|
||||
),
|
||||
):
|
||||
assert not kw
|
||||
return compiler.visit_drop_constraint(element)
|
||||
elif isinstance(constraint, schema.CheckConstraint):
|
||||
# note that SQLAlchemy as of 1.2 does not yet support
|
||||
# DROP CONSTRAINT for MySQL/MariaDB, so we implement fully
|
||||
# here.
|
||||
if compiler.dialect.is_mariadb:
|
||||
return "ALTER TABLE %s DROP CONSTRAINT %s" % (
|
||||
compiler.preparer.format_table(constraint.table),
|
||||
compiler.preparer.format_constraint(constraint),
|
||||
)
|
||||
else:
|
||||
return "ALTER TABLE %s DROP CHECK %s" % (
|
||||
compiler.preparer.format_table(constraint.table),
|
||||
compiler.preparer.format_constraint(constraint),
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"No generic 'DROP CONSTRAINT' in MySQL - "
|
||||
"please specify constraint type"
|
||||
)
|
||||
@@ -0,0 +1,202 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.sql import sqltypes
|
||||
|
||||
from .base import AddColumn
|
||||
from .base import alter_table
|
||||
from .base import ColumnComment
|
||||
from .base import ColumnDefault
|
||||
from .base import ColumnName
|
||||
from .base import ColumnNullable
|
||||
from .base import ColumnType
|
||||
from .base import format_column_name
|
||||
from .base import format_server_default
|
||||
from .base import format_table_name
|
||||
from .base import format_type
|
||||
from .base import IdentityColumnDefault
|
||||
from .base import RenameTable
|
||||
from .impl import DefaultImpl
|
||||
from ..util.sqla_compat import compiles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.dialects.oracle.base import OracleDDLCompiler
|
||||
from sqlalchemy.engine.cursor import CursorResult
|
||||
from sqlalchemy.sql.schema import Column
|
||||
|
||||
|
||||
class OracleImpl(DefaultImpl):
|
||||
__dialect__ = "oracle"
|
||||
transactional_ddl = False
|
||||
batch_separator = "/"
|
||||
command_terminator = ""
|
||||
type_synonyms = DefaultImpl.type_synonyms + (
|
||||
{"VARCHAR", "VARCHAR2"},
|
||||
{"BIGINT", "INTEGER", "SMALLINT", "DECIMAL", "NUMERIC", "NUMBER"},
|
||||
{"DOUBLE", "FLOAT", "DOUBLE_PRECISION"},
|
||||
)
|
||||
identity_attrs_ignore = ()
|
||||
|
||||
def __init__(self, *arg, **kw) -> None:
|
||||
super().__init__(*arg, **kw)
|
||||
self.batch_separator = self.context_opts.get(
|
||||
"oracle_batch_separator", self.batch_separator
|
||||
)
|
||||
|
||||
def _exec(self, construct: Any, *args, **kw) -> Optional[CursorResult]:
|
||||
result = super()._exec(construct, *args, **kw)
|
||||
if self.as_sql and self.batch_separator:
|
||||
self.static_output(self.batch_separator)
|
||||
return result
|
||||
|
||||
def compare_server_default(
|
||||
self,
|
||||
inspector_column,
|
||||
metadata_column,
|
||||
rendered_metadata_default,
|
||||
rendered_inspector_default,
|
||||
):
|
||||
if rendered_metadata_default is not None:
|
||||
rendered_metadata_default = re.sub(
|
||||
r"^\((.+)\)$", r"\1", rendered_metadata_default
|
||||
)
|
||||
|
||||
rendered_metadata_default = re.sub(
|
||||
r"^\"?'(.+)'\"?$", r"\1", rendered_metadata_default
|
||||
)
|
||||
|
||||
if rendered_inspector_default is not None:
|
||||
rendered_inspector_default = re.sub(
|
||||
r"^\((.+)\)$", r"\1", rendered_inspector_default
|
||||
)
|
||||
|
||||
rendered_inspector_default = re.sub(
|
||||
r"^\"?'(.+)'\"?$", r"\1", rendered_inspector_default
|
||||
)
|
||||
|
||||
rendered_inspector_default = rendered_inspector_default.strip()
|
||||
return rendered_inspector_default != rendered_metadata_default
|
||||
|
||||
def emit_begin(self) -> None:
|
||||
self._exec("SET TRANSACTION READ WRITE")
|
||||
|
||||
def emit_commit(self) -> None:
|
||||
self._exec("COMMIT")
|
||||
|
||||
|
||||
@compiles(AddColumn, "oracle")
|
||||
def visit_add_column(
|
||||
element: AddColumn, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
add_column(compiler, element.column, **kw),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnNullable, "oracle")
|
||||
def visit_column_nullable(
|
||||
element: ColumnNullable, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
"NULL" if element.nullable else "NOT NULL",
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnType, "oracle")
|
||||
def visit_column_type(
|
||||
element: ColumnType, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
"%s" % format_type(compiler, element.type_),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnName, "oracle")
|
||||
def visit_column_name(
|
||||
element: ColumnName, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s RENAME COLUMN %s TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
format_column_name(compiler, element.newname),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnDefault, "oracle")
|
||||
def visit_column_default(
|
||||
element: ColumnDefault, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
(
|
||||
"DEFAULT %s" % format_server_default(compiler, element.default)
|
||||
if element.default is not None
|
||||
else "DEFAULT NULL"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnComment, "oracle")
|
||||
def visit_column_comment(
|
||||
element: ColumnComment, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}"
|
||||
|
||||
comment = compiler.sql_compiler.render_literal_value(
|
||||
(element.comment if element.comment is not None else ""),
|
||||
sqltypes.String(),
|
||||
)
|
||||
|
||||
return ddl.format(
|
||||
table_name=element.table_name,
|
||||
column_name=element.column_name,
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
|
||||
@compiles(RenameTable, "oracle")
|
||||
def visit_rename_table(
|
||||
element: RenameTable, compiler: OracleDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s RENAME TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_table_name(compiler, element.new_table_name, None),
|
||||
)
|
||||
|
||||
|
||||
def alter_column(compiler: OracleDDLCompiler, name: str) -> str:
|
||||
return "MODIFY %s" % format_column_name(compiler, name)
|
||||
|
||||
|
||||
def add_column(compiler: OracleDDLCompiler, column: Column[Any], **kw) -> str:
|
||||
return "ADD %s" % compiler.get_column_specification(column, **kw)
|
||||
|
||||
|
||||
@compiles(IdentityColumnDefault, "oracle")
|
||||
def visit_identity_column(
|
||||
element: IdentityColumnDefault, compiler: OracleDDLCompiler, **kw
|
||||
):
|
||||
text = "%s %s " % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
)
|
||||
if element.default is None:
|
||||
# drop identity
|
||||
text += "DROP IDENTITY"
|
||||
return text
|
||||
else:
|
||||
text += compiler.visit_identity_column(element.default)
|
||||
return text
|
||||
@@ -0,0 +1,864 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Float
|
||||
from sqlalchemy import Identity
|
||||
from sqlalchemy import literal_column
|
||||
from sqlalchemy import Numeric
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.dialects.postgresql import BIGINT
|
||||
from sqlalchemy.dialects.postgresql import ExcludeConstraint
|
||||
from sqlalchemy.dialects.postgresql import INTEGER
|
||||
from sqlalchemy.schema import CreateIndex
|
||||
from sqlalchemy.sql.elements import ColumnClause
|
||||
from sqlalchemy.sql.elements import TextClause
|
||||
from sqlalchemy.sql.functions import FunctionElement
|
||||
from sqlalchemy.types import NULLTYPE
|
||||
|
||||
from .base import alter_column
|
||||
from .base import alter_table
|
||||
from .base import AlterColumn
|
||||
from .base import ColumnComment
|
||||
from .base import format_column_name
|
||||
from .base import format_table_name
|
||||
from .base import format_type
|
||||
from .base import IdentityColumnDefault
|
||||
from .base import RenameTable
|
||||
from .impl import ComparisonResult
|
||||
from .impl import DefaultImpl
|
||||
from .. import util
|
||||
from ..autogenerate import render
|
||||
from ..operations import ops
|
||||
from ..operations import schemaobj
|
||||
from ..operations.base import BatchOperations
|
||||
from ..operations.base import Operations
|
||||
from ..util import sqla_compat
|
||||
from ..util.sqla_compat import compiles
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
from sqlalchemy import Index
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql.array import ARRAY
|
||||
from sqlalchemy.dialects.postgresql.base import PGDDLCompiler
|
||||
from sqlalchemy.dialects.postgresql.hstore import HSTORE
|
||||
from sqlalchemy.dialects.postgresql.json import JSON
|
||||
from sqlalchemy.dialects.postgresql.json import JSONB
|
||||
from sqlalchemy.sql.elements import ClauseElement
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import MetaData
|
||||
from sqlalchemy.sql.schema import Table
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from .base import _ServerDefaultType
|
||||
from .impl import _ReflectedConstraint
|
||||
from ..autogenerate.api import AutogenContext
|
||||
from ..autogenerate.render import _f_name
|
||||
from ..runtime.migration import MigrationContext
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PostgresqlImpl(DefaultImpl):
|
||||
__dialect__ = "postgresql"
|
||||
transactional_ddl = True
|
||||
type_synonyms = DefaultImpl.type_synonyms + (
|
||||
{"FLOAT", "DOUBLE PRECISION"},
|
||||
)
|
||||
|
||||
def create_index(self, index: Index, **kw: Any) -> None:
|
||||
# this likely defaults to None if not present, so get()
|
||||
# should normally not return the default value. being
|
||||
# defensive in any case
|
||||
postgresql_include = index.kwargs.get("postgresql_include", None) or ()
|
||||
for col in postgresql_include:
|
||||
if col not in index.table.c: # type: ignore[union-attr]
|
||||
index.table.append_column( # type: ignore[union-attr]
|
||||
Column(col, sqltypes.NullType)
|
||||
)
|
||||
self._exec(CreateIndex(index, **kw))
|
||||
|
||||
def prep_table_for_batch(self, batch_impl, table):
|
||||
for constraint in table.constraints:
|
||||
if (
|
||||
constraint.name is not None
|
||||
and constraint.name in batch_impl.named_constraints
|
||||
):
|
||||
self.drop_constraint(constraint)
|
||||
|
||||
def compare_server_default(
|
||||
self,
|
||||
inspector_column,
|
||||
metadata_column,
|
||||
rendered_metadata_default,
|
||||
rendered_inspector_default,
|
||||
):
|
||||
|
||||
# don't do defaults for SERIAL columns
|
||||
if (
|
||||
metadata_column.primary_key
|
||||
and metadata_column is metadata_column.table._autoincrement_column
|
||||
):
|
||||
return False
|
||||
|
||||
conn_col_default = rendered_inspector_default
|
||||
|
||||
if conn_col_default and re.match(
|
||||
r"nextval\('(.+?)'::regclass\)", conn_col_default
|
||||
):
|
||||
conn_col_default = conn_col_default.replace("::regclass", "")
|
||||
|
||||
defaults_equal = conn_col_default == rendered_metadata_default
|
||||
if defaults_equal:
|
||||
return False
|
||||
|
||||
if None in (
|
||||
conn_col_default,
|
||||
rendered_metadata_default,
|
||||
metadata_column.server_default,
|
||||
):
|
||||
return not defaults_equal
|
||||
|
||||
metadata_default = metadata_column.server_default.arg
|
||||
|
||||
if isinstance(metadata_default, str):
|
||||
if not isinstance(inspector_column.type, (Numeric, Float)):
|
||||
metadata_default = re.sub(r"^'|'$", "", metadata_default)
|
||||
metadata_default = f"'{metadata_default}'"
|
||||
|
||||
metadata_default = literal_column(metadata_default)
|
||||
|
||||
# run a real compare against the server
|
||||
# TODO: this seems quite a bad idea for a default that's a SQL
|
||||
# function! SQL functions are not deterministic!
|
||||
conn = self.connection
|
||||
assert conn is not None
|
||||
return not conn.scalar(
|
||||
select(literal_column(conn_col_default) == metadata_default)
|
||||
)
|
||||
|
||||
def alter_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column_name: str,
|
||||
*,
|
||||
nullable: Optional[bool] = None,
|
||||
server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = False,
|
||||
name: Optional[str] = None,
|
||||
type_: Optional[TypeEngine] = None,
|
||||
schema: Optional[str] = None,
|
||||
autoincrement: Optional[bool] = None,
|
||||
existing_type: Optional[TypeEngine] = None,
|
||||
existing_server_default: Optional[
|
||||
Union[_ServerDefaultType, Literal[False]]
|
||||
] = None,
|
||||
existing_nullable: Optional[bool] = None,
|
||||
existing_autoincrement: Optional[bool] = None,
|
||||
**kw: Any,
|
||||
) -> None:
|
||||
using = kw.pop("postgresql_using", None)
|
||||
|
||||
if using is not None and type_ is None:
|
||||
raise util.CommandError(
|
||||
"postgresql_using must be used with the type_ parameter"
|
||||
)
|
||||
|
||||
if type_ is not None:
|
||||
self._exec(
|
||||
PostgresqlColumnType(
|
||||
table_name,
|
||||
column_name,
|
||||
type_,
|
||||
schema=schema,
|
||||
using=using,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
)
|
||||
)
|
||||
|
||||
super().alter_column(
|
||||
table_name,
|
||||
column_name,
|
||||
nullable=nullable,
|
||||
server_default=server_default,
|
||||
name=name,
|
||||
schema=schema,
|
||||
autoincrement=autoincrement,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
existing_autoincrement=existing_autoincrement,
|
||||
**kw,
|
||||
)
|
||||
|
||||
def autogen_column_reflect(self, inspector, table, column_info):
|
||||
if column_info.get("default") and isinstance(
|
||||
column_info["type"], (INTEGER, BIGINT)
|
||||
):
|
||||
seq_match = re.match(
|
||||
r"nextval\('(.+?)'::regclass\)", column_info["default"]
|
||||
)
|
||||
if seq_match:
|
||||
info = sqla_compat._exec_on_inspector(
|
||||
inspector,
|
||||
text(
|
||||
"select c.relname, a.attname "
|
||||
"from pg_class as c join "
|
||||
"pg_depend d on d.objid=c.oid and "
|
||||
"d.classid='pg_class'::regclass and "
|
||||
"d.refclassid='pg_class'::regclass "
|
||||
"join pg_class t on t.oid=d.refobjid "
|
||||
"join pg_attribute a on a.attrelid=t.oid and "
|
||||
"a.attnum=d.refobjsubid "
|
||||
"where c.relkind='S' and "
|
||||
"c.oid=cast(:seqname as regclass)"
|
||||
),
|
||||
seqname=seq_match.group(1),
|
||||
).first()
|
||||
if info:
|
||||
seqname, colname = info
|
||||
if colname == column_info["name"]:
|
||||
log.info(
|
||||
"Detected sequence named '%s' as "
|
||||
"owned by integer column '%s(%s)', "
|
||||
"assuming SERIAL and omitting",
|
||||
seqname,
|
||||
table.name,
|
||||
colname,
|
||||
)
|
||||
# sequence, and the owner is this column,
|
||||
# its a SERIAL - whack it!
|
||||
del column_info["default"]
|
||||
|
||||
def correct_for_autogen_constraints(
|
||||
self,
|
||||
conn_unique_constraints,
|
||||
conn_indexes,
|
||||
metadata_unique_constraints,
|
||||
metadata_indexes,
|
||||
):
|
||||
doubled_constraints = {
|
||||
index
|
||||
for index in conn_indexes
|
||||
if index.info.get("duplicates_constraint")
|
||||
}
|
||||
|
||||
for ix in doubled_constraints:
|
||||
conn_indexes.remove(ix)
|
||||
|
||||
if not sqla_compat.sqla_2:
|
||||
self._skip_functional_indexes(metadata_indexes, conn_indexes)
|
||||
|
||||
# pg behavior regarding modifiers
|
||||
# | # | compiled sql | returned sql | regexp. group is removed |
|
||||
# | - | ---------------- | -----------------| ------------------------ |
|
||||
# | 1 | nulls first | nulls first | - |
|
||||
# | 2 | nulls last | | (?<! desc)( nulls last)$ |
|
||||
# | 3 | asc | | ( asc)$ |
|
||||
# | 4 | asc nulls first | nulls first | ( asc) nulls first$ |
|
||||
# | 5 | asc nulls last | | ( asc nulls last)$ |
|
||||
# | 6 | desc | desc | - |
|
||||
# | 7 | desc nulls first | desc | desc( nulls first)$ |
|
||||
# | 8 | desc nulls last | desc nulls last | - |
|
||||
_default_modifiers_re = ( # order of case 2 and 5 matters
|
||||
re.compile("( asc nulls last)$"), # case 5
|
||||
re.compile("(?<! desc)( nulls last)$"), # case 2
|
||||
re.compile("( asc)$"), # case 3
|
||||
re.compile("( asc) nulls first$"), # case 4
|
||||
re.compile(" desc( nulls first)$"), # case 7
|
||||
)
|
||||
|
||||
def _cleanup_index_expr(self, index: Index, expr: str) -> str:
|
||||
expr = expr.lower().replace('"', "").replace("'", "")
|
||||
if index.table is not None:
|
||||
# should not be needed, since include_table=False is in compile
|
||||
expr = expr.replace(f"{index.table.name.lower()}.", "")
|
||||
|
||||
if "::" in expr:
|
||||
# strip :: cast. types can have spaces in them
|
||||
expr = re.sub(r"(::[\w ]+\w)", "", expr)
|
||||
|
||||
while expr and expr[0] == "(" and expr[-1] == ")":
|
||||
expr = expr[1:-1]
|
||||
|
||||
# NOTE: when parsing the connection expression this cleanup could
|
||||
# be skipped
|
||||
for rs in self._default_modifiers_re:
|
||||
if match := rs.search(expr):
|
||||
start, end = match.span(1)
|
||||
expr = expr[:start] + expr[end:]
|
||||
break
|
||||
|
||||
while expr and expr[0] == "(" and expr[-1] == ")":
|
||||
expr = expr[1:-1]
|
||||
|
||||
# strip casts
|
||||
cast_re = re.compile(r"cast\s*\(")
|
||||
if cast_re.match(expr):
|
||||
expr = cast_re.sub("", expr)
|
||||
# remove the as type
|
||||
expr = re.sub(r"as\s+[^)]+\)", "", expr)
|
||||
# remove spaces
|
||||
expr = expr.replace(" ", "")
|
||||
return expr
|
||||
|
||||
def _dialect_options(
|
||||
self, item: Union[Index, UniqueConstraint]
|
||||
) -> Tuple[Any, ...]:
|
||||
# only the positive case is returned by sqlalchemy reflection so
|
||||
# None and False are treated the same
|
||||
if item.dialect_kwargs.get("postgresql_nulls_not_distinct"):
|
||||
return ("nulls_not_distinct",)
|
||||
return ()
|
||||
|
||||
def compare_indexes(
|
||||
self,
|
||||
metadata_index: Index,
|
||||
reflected_index: Index,
|
||||
) -> ComparisonResult:
|
||||
msg = []
|
||||
unique_msg = self._compare_index_unique(
|
||||
metadata_index, reflected_index
|
||||
)
|
||||
if unique_msg:
|
||||
msg.append(unique_msg)
|
||||
m_exprs = metadata_index.expressions
|
||||
r_exprs = reflected_index.expressions
|
||||
if len(m_exprs) != len(r_exprs):
|
||||
msg.append(f"expression number {len(r_exprs)} to {len(m_exprs)}")
|
||||
if msg:
|
||||
# no point going further, return early
|
||||
return ComparisonResult.Different(msg)
|
||||
skip = []
|
||||
for pos, (m_e, r_e) in enumerate(zip(m_exprs, r_exprs), 1):
|
||||
m_compile = self._compile_element(m_e)
|
||||
m_text = self._cleanup_index_expr(metadata_index, m_compile)
|
||||
# print(f"META ORIG: {m_compile!r} CLEANUP: {m_text!r}")
|
||||
r_compile = self._compile_element(r_e)
|
||||
r_text = self._cleanup_index_expr(metadata_index, r_compile)
|
||||
# print(f"CONN ORIG: {r_compile!r} CLEANUP: {r_text!r}")
|
||||
if m_text == r_text:
|
||||
continue # expressions these are equal
|
||||
elif m_compile.strip().endswith("_ops") and (
|
||||
" " in m_compile or ")" in m_compile # is an expression
|
||||
):
|
||||
skip.append(
|
||||
f"expression #{pos} {m_compile!r} detected "
|
||||
"as including operator clause."
|
||||
)
|
||||
util.warn(
|
||||
f"Expression #{pos} {m_compile!r} in index "
|
||||
f"{reflected_index.name!r} detected to include "
|
||||
"an operator clause. Expression compare cannot proceed. "
|
||||
"Please move the operator clause to the "
|
||||
"``postgresql_ops`` dict to enable proper compare "
|
||||
"of the index expressions: "
|
||||
"https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#operator-classes", # noqa: E501
|
||||
)
|
||||
else:
|
||||
msg.append(f"expression #{pos} {r_compile!r} to {m_compile!r}")
|
||||
|
||||
m_options = self._dialect_options(metadata_index)
|
||||
r_options = self._dialect_options(reflected_index)
|
||||
if m_options != r_options:
|
||||
msg.extend(f"options {r_options} to {m_options}")
|
||||
|
||||
if msg:
|
||||
return ComparisonResult.Different(msg)
|
||||
elif skip:
|
||||
# if there are other changes detected don't skip the index
|
||||
return ComparisonResult.Skip(skip)
|
||||
else:
|
||||
return ComparisonResult.Equal()
|
||||
|
||||
def compare_unique_constraint(
|
||||
self,
|
||||
metadata_constraint: UniqueConstraint,
|
||||
reflected_constraint: UniqueConstraint,
|
||||
) -> ComparisonResult:
|
||||
metadata_tup = self._create_metadata_constraint_sig(
|
||||
metadata_constraint
|
||||
)
|
||||
reflected_tup = self._create_reflected_constraint_sig(
|
||||
reflected_constraint
|
||||
)
|
||||
|
||||
meta_sig = metadata_tup.unnamed
|
||||
conn_sig = reflected_tup.unnamed
|
||||
if conn_sig != meta_sig:
|
||||
return ComparisonResult.Different(
|
||||
f"expression {conn_sig} to {meta_sig}"
|
||||
)
|
||||
|
||||
metadata_do = self._dialect_options(metadata_tup.const)
|
||||
conn_do = self._dialect_options(reflected_tup.const)
|
||||
if metadata_do != conn_do:
|
||||
return ComparisonResult.Different(
|
||||
f"expression {conn_do} to {metadata_do}"
|
||||
)
|
||||
|
||||
return ComparisonResult.Equal()
|
||||
|
||||
def adjust_reflected_dialect_options(
|
||||
self, reflected_object: _ReflectedConstraint, kind: str
|
||||
) -> Dict[str, Any]:
|
||||
options: Dict[str, Any]
|
||||
options = reflected_object.get("dialect_options", {}).copy() # type: ignore[attr-defined] # noqa: E501
|
||||
if not options.get("postgresql_include"):
|
||||
options.pop("postgresql_include", None)
|
||||
return options
|
||||
|
||||
def _compile_element(self, element: Union[ClauseElement, str]) -> str:
|
||||
if isinstance(element, str):
|
||||
return element
|
||||
return element.compile(
|
||||
dialect=self.dialect,
|
||||
compile_kwargs={"literal_binds": True, "include_table": False},
|
||||
).string
|
||||
|
||||
def render_ddl_sql_expr(
|
||||
self,
|
||||
expr: ClauseElement,
|
||||
is_server_default: bool = False,
|
||||
is_index: bool = False,
|
||||
**kw: Any,
|
||||
) -> str:
|
||||
"""Render a SQL expression that is typically a server default,
|
||||
index expression, etc.
|
||||
|
||||
"""
|
||||
|
||||
# apply self_group to index expressions;
|
||||
# see https://github.com/sqlalchemy/sqlalchemy/blob/
|
||||
# 82fa95cfce070fab401d020c6e6e4a6a96cc2578/
|
||||
# lib/sqlalchemy/dialects/postgresql/base.py#L2261
|
||||
if is_index and not isinstance(expr, ColumnClause):
|
||||
expr = expr.self_group()
|
||||
|
||||
return super().render_ddl_sql_expr(
|
||||
expr, is_server_default=is_server_default, is_index=is_index, **kw
|
||||
)
|
||||
|
||||
def render_type(
|
||||
self, type_: TypeEngine, autogen_context: AutogenContext
|
||||
) -> Union[str, Literal[False]]:
|
||||
mod = type(type_).__module__
|
||||
if not mod.startswith("sqlalchemy.dialects.postgresql"):
|
||||
return False
|
||||
|
||||
if hasattr(self, "_render_%s_type" % type_.__visit_name__):
|
||||
meth = getattr(self, "_render_%s_type" % type_.__visit_name__)
|
||||
return meth(type_, autogen_context)
|
||||
|
||||
return False
|
||||
|
||||
def _render_HSTORE_type(
|
||||
self, type_: HSTORE, autogen_context: AutogenContext
|
||||
) -> str:
|
||||
return cast(
|
||||
str,
|
||||
render._render_type_w_subtype(
|
||||
type_, autogen_context, "text_type", r"(.+?\(.*text_type=)"
|
||||
),
|
||||
)
|
||||
|
||||
def _render_ARRAY_type(
|
||||
self, type_: ARRAY, autogen_context: AutogenContext
|
||||
) -> str:
|
||||
return cast(
|
||||
str,
|
||||
render._render_type_w_subtype(
|
||||
type_, autogen_context, "item_type", r"(.+?\()"
|
||||
),
|
||||
)
|
||||
|
||||
def _render_JSON_type(
|
||||
self, type_: JSON, autogen_context: AutogenContext
|
||||
) -> str:
|
||||
return cast(
|
||||
str,
|
||||
render._render_type_w_subtype(
|
||||
type_, autogen_context, "astext_type", r"(.+?\(.*astext_type=)"
|
||||
),
|
||||
)
|
||||
|
||||
def _render_JSONB_type(
|
||||
self, type_: JSONB, autogen_context: AutogenContext
|
||||
) -> str:
|
||||
return cast(
|
||||
str,
|
||||
render._render_type_w_subtype(
|
||||
type_, autogen_context, "astext_type", r"(.+?\(.*astext_type=)"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PostgresqlColumnType(AlterColumn):
|
||||
def __init__(
|
||||
self, name: str, column_name: str, type_: TypeEngine, **kw
|
||||
) -> None:
|
||||
using = kw.pop("using", None)
|
||||
super().__init__(name, column_name, **kw)
|
||||
self.type_ = sqltypes.to_instance(type_)
|
||||
self.using = using
|
||||
|
||||
|
||||
@compiles(RenameTable, "postgresql")
|
||||
def visit_rename_table(
|
||||
element: RenameTable, compiler: PGDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s RENAME TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_table_name(compiler, element.new_table_name, None),
|
||||
)
|
||||
|
||||
|
||||
@compiles(PostgresqlColumnType, "postgresql")
|
||||
def visit_column_type(
|
||||
element: PostgresqlColumnType, compiler: PGDDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s %s %s %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
"TYPE %s" % format_type(compiler, element.type_),
|
||||
"USING %s" % element.using if element.using else "",
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnComment, "postgresql")
|
||||
def visit_column_comment(
|
||||
element: ColumnComment, compiler: PGDDLCompiler, **kw
|
||||
) -> str:
|
||||
ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}"
|
||||
comment = (
|
||||
compiler.sql_compiler.render_literal_value(
|
||||
element.comment, sqltypes.String()
|
||||
)
|
||||
if element.comment is not None
|
||||
else "NULL"
|
||||
)
|
||||
|
||||
return ddl.format(
|
||||
table_name=format_table_name(
|
||||
compiler, element.table_name, element.schema
|
||||
),
|
||||
column_name=format_column_name(compiler, element.column_name),
|
||||
comment=comment,
|
||||
)
|
||||
|
||||
|
||||
@compiles(IdentityColumnDefault, "postgresql")
|
||||
def visit_identity_column(
|
||||
element: IdentityColumnDefault, compiler: PGDDLCompiler, **kw
|
||||
):
|
||||
text = "%s %s " % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
alter_column(compiler, element.column_name),
|
||||
)
|
||||
if element.default is None:
|
||||
# drop identity
|
||||
text += "DROP IDENTITY"
|
||||
return text
|
||||
elif element.existing_server_default is None:
|
||||
# add identity options
|
||||
text += "ADD "
|
||||
text += compiler.visit_identity_column(element.default)
|
||||
return text
|
||||
else:
|
||||
# alter identity
|
||||
diff, _, _ = element.impl._compare_identity_default(
|
||||
element.default, element.existing_server_default
|
||||
)
|
||||
identity = element.default
|
||||
for attr in sorted(diff):
|
||||
if attr == "always":
|
||||
text += "SET GENERATED %s " % (
|
||||
"ALWAYS" if identity.always else "BY DEFAULT"
|
||||
)
|
||||
else:
|
||||
text += "SET %s " % compiler.get_identity_options(
|
||||
Identity(**{attr: getattr(identity, attr)})
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
@Operations.register_operation("create_exclude_constraint")
|
||||
@BatchOperations.register_operation(
|
||||
"create_exclude_constraint", "batch_create_exclude_constraint"
|
||||
)
|
||||
@ops.AddConstraintOp.register_add_constraint("exclude_constraint")
|
||||
class CreateExcludeConstraintOp(ops.AddConstraintOp):
|
||||
"""Represent a create exclude constraint operation."""
|
||||
|
||||
constraint_type = "exclude"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
constraint_name: sqla_compat._ConstraintName,
|
||||
table_name: Union[str, quoted_name],
|
||||
elements: Union[
|
||||
Sequence[Tuple[str, str]],
|
||||
Sequence[Tuple[ColumnClause[Any], str]],
|
||||
],
|
||||
where: Optional[Union[ColumnElement[bool], str]] = None,
|
||||
schema: Optional[str] = None,
|
||||
_orig_constraint: Optional[ExcludeConstraint] = None,
|
||||
**kw,
|
||||
) -> None:
|
||||
self.constraint_name = constraint_name
|
||||
self.table_name = table_name
|
||||
self.elements = elements
|
||||
self.where = where
|
||||
self.schema = schema
|
||||
self._orig_constraint = _orig_constraint
|
||||
self.kw = kw
|
||||
|
||||
@classmethod
|
||||
def from_constraint( # type:ignore[override]
|
||||
cls, constraint: ExcludeConstraint
|
||||
) -> CreateExcludeConstraintOp:
|
||||
constraint_table = sqla_compat._table_for_constraint(constraint)
|
||||
return cls(
|
||||
constraint.name,
|
||||
constraint_table.name,
|
||||
[ # type: ignore
|
||||
(expr, op) for expr, name, op in constraint._render_exprs
|
||||
],
|
||||
where=cast("ColumnElement[bool] | None", constraint.where),
|
||||
schema=constraint_table.schema,
|
||||
_orig_constraint=constraint,
|
||||
deferrable=constraint.deferrable,
|
||||
initially=constraint.initially,
|
||||
using=constraint.using,
|
||||
)
|
||||
|
||||
def to_constraint(
|
||||
self, migration_context: Optional[MigrationContext] = None
|
||||
) -> ExcludeConstraint:
|
||||
if self._orig_constraint is not None:
|
||||
return self._orig_constraint
|
||||
schema_obj = schemaobj.SchemaObjects(migration_context)
|
||||
t = schema_obj.table(self.table_name, schema=self.schema)
|
||||
excl = ExcludeConstraint(
|
||||
*self.elements,
|
||||
name=self.constraint_name,
|
||||
where=self.where,
|
||||
**self.kw,
|
||||
)
|
||||
for (
|
||||
expr,
|
||||
name,
|
||||
oper,
|
||||
) in excl._render_exprs:
|
||||
t.append_column(Column(name, NULLTYPE))
|
||||
t.append_constraint(excl)
|
||||
return excl
|
||||
|
||||
@classmethod
|
||||
def create_exclude_constraint(
|
||||
cls,
|
||||
operations: Operations,
|
||||
constraint_name: str,
|
||||
table_name: str,
|
||||
*elements: Any,
|
||||
**kw: Any,
|
||||
) -> Optional[Table]:
|
||||
"""Issue an alter to create an EXCLUDE constraint using the
|
||||
current migration context.
|
||||
|
||||
.. note:: This method is Postgresql specific, and additionally
|
||||
requires at least SQLAlchemy 1.0.
|
||||
|
||||
e.g.::
|
||||
|
||||
from alembic import op
|
||||
|
||||
op.create_exclude_constraint(
|
||||
"user_excl",
|
||||
"user",
|
||||
("period", "&&"),
|
||||
("group", "="),
|
||||
where=("group != 'some group'"),
|
||||
)
|
||||
|
||||
Note that the expressions work the same way as that of
|
||||
the ``ExcludeConstraint`` object itself; if plain strings are
|
||||
passed, quoting rules must be applied manually.
|
||||
|
||||
:param name: Name of the constraint.
|
||||
:param table_name: String name of the source table.
|
||||
:param elements: exclude conditions.
|
||||
:param where: SQL expression or SQL string with optional WHERE
|
||||
clause.
|
||||
:param deferrable: optional bool. If set, emit DEFERRABLE or
|
||||
NOT DEFERRABLE when issuing DDL for this constraint.
|
||||
:param initially: optional string. If set, emit INITIALLY <value>
|
||||
when issuing DDL for this constraint.
|
||||
:param schema: Optional schema name to operate within.
|
||||
|
||||
"""
|
||||
op = cls(constraint_name, table_name, elements, **kw)
|
||||
return operations.invoke(op)
|
||||
|
||||
@classmethod
|
||||
def batch_create_exclude_constraint(
|
||||
cls,
|
||||
operations: BatchOperations,
|
||||
constraint_name: str,
|
||||
*elements: Any,
|
||||
**kw: Any,
|
||||
) -> Optional[Table]:
|
||||
"""Issue a "create exclude constraint" instruction using the
|
||||
current batch migration context.
|
||||
|
||||
.. note:: This method is Postgresql specific, and additionally
|
||||
requires at least SQLAlchemy 1.0.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.Operations.create_exclude_constraint`
|
||||
|
||||
"""
|
||||
kw["schema"] = operations.impl.schema
|
||||
op = cls(constraint_name, operations.impl.table_name, elements, **kw)
|
||||
return operations.invoke(op)
|
||||
|
||||
|
||||
@render.renderers.dispatch_for(CreateExcludeConstraintOp)
|
||||
def _add_exclude_constraint(
|
||||
autogen_context: AutogenContext, op: CreateExcludeConstraintOp
|
||||
) -> str:
|
||||
return _exclude_constraint(op.to_constraint(), autogen_context, alter=True)
|
||||
|
||||
|
||||
@render._constraint_renderers.dispatch_for(ExcludeConstraint)
|
||||
def _render_inline_exclude_constraint(
|
||||
constraint: ExcludeConstraint,
|
||||
autogen_context: AutogenContext,
|
||||
namespace_metadata: MetaData,
|
||||
) -> str:
|
||||
rendered = render._user_defined_render(
|
||||
"exclude", constraint, autogen_context
|
||||
)
|
||||
if rendered is not False:
|
||||
return rendered
|
||||
|
||||
return _exclude_constraint(constraint, autogen_context, False)
|
||||
|
||||
|
||||
def _postgresql_autogenerate_prefix(autogen_context: AutogenContext) -> str:
|
||||
imports = autogen_context.imports
|
||||
if imports is not None:
|
||||
imports.add("from sqlalchemy.dialects import postgresql")
|
||||
return "postgresql."
|
||||
|
||||
|
||||
def _exclude_constraint(
|
||||
constraint: ExcludeConstraint,
|
||||
autogen_context: AutogenContext,
|
||||
alter: bool,
|
||||
) -> str:
|
||||
opts: List[Tuple[str, Union[quoted_name, str, _f_name, None]]] = []
|
||||
|
||||
has_batch = autogen_context._has_batch
|
||||
|
||||
if constraint.deferrable:
|
||||
opts.append(("deferrable", str(constraint.deferrable)))
|
||||
if constraint.initially:
|
||||
opts.append(("initially", str(constraint.initially)))
|
||||
if constraint.using:
|
||||
opts.append(("using", str(constraint.using)))
|
||||
if not has_batch and alter and constraint.table.schema:
|
||||
opts.append(("schema", render._ident(constraint.table.schema)))
|
||||
if not alter and constraint.name:
|
||||
opts.append(
|
||||
("name", render._render_gen_name(autogen_context, constraint.name))
|
||||
)
|
||||
|
||||
def do_expr_where_opts():
|
||||
args = [
|
||||
"(%s, %r)"
|
||||
% (
|
||||
_render_potential_column(
|
||||
sqltext, # type:ignore[arg-type]
|
||||
autogen_context,
|
||||
),
|
||||
opstring,
|
||||
)
|
||||
for sqltext, name, opstring in constraint._render_exprs
|
||||
]
|
||||
if constraint.where is not None:
|
||||
args.append(
|
||||
"where=%s"
|
||||
% render._render_potential_expr(
|
||||
constraint.where, autogen_context
|
||||
)
|
||||
)
|
||||
args.extend(["%s=%r" % (k, v) for k, v in opts])
|
||||
return args
|
||||
|
||||
if alter:
|
||||
args = [
|
||||
repr(render._render_gen_name(autogen_context, constraint.name))
|
||||
]
|
||||
if not has_batch:
|
||||
args += [repr(render._ident(constraint.table.name))]
|
||||
args.extend(do_expr_where_opts())
|
||||
return "%(prefix)screate_exclude_constraint(%(args)s)" % {
|
||||
"prefix": render._alembic_autogenerate_prefix(autogen_context),
|
||||
"args": ", ".join(args),
|
||||
}
|
||||
else:
|
||||
args = do_expr_where_opts()
|
||||
return "%(prefix)sExcludeConstraint(%(args)s)" % {
|
||||
"prefix": _postgresql_autogenerate_prefix(autogen_context),
|
||||
"args": ", ".join(args),
|
||||
}
|
||||
|
||||
|
||||
def _render_potential_column(
|
||||
value: Union[
|
||||
ColumnClause[Any], Column[Any], TextClause, FunctionElement[Any]
|
||||
],
|
||||
autogen_context: AutogenContext,
|
||||
) -> str:
|
||||
if isinstance(value, ColumnClause):
|
||||
if value.is_literal:
|
||||
# like literal_column("int8range(from, to)") in ExcludeConstraint
|
||||
template = "%(prefix)sliteral_column(%(name)r)"
|
||||
else:
|
||||
template = "%(prefix)scolumn(%(name)r)"
|
||||
|
||||
return template % {
|
||||
"prefix": render._sqlalchemy_autogenerate_prefix(autogen_context),
|
||||
"name": value.name,
|
||||
}
|
||||
else:
|
||||
return render._render_potential_expr(
|
||||
value,
|
||||
autogen_context,
|
||||
wrap_in_element=isinstance(value, (TextClause, FunctionElement)),
|
||||
)
|
||||
@@ -0,0 +1,237 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import cast
|
||||
from sqlalchemy import Computed
|
||||
from sqlalchemy import JSON
|
||||
from sqlalchemy import schema
|
||||
from sqlalchemy import sql
|
||||
|
||||
from .base import alter_table
|
||||
from .base import ColumnName
|
||||
from .base import format_column_name
|
||||
from .base import format_table_name
|
||||
from .base import RenameTable
|
||||
from .impl import DefaultImpl
|
||||
from .. import util
|
||||
from ..util.sqla_compat import compiles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.sql.compiler import DDLCompiler
|
||||
from sqlalchemy.sql.elements import Cast
|
||||
from sqlalchemy.sql.elements import ClauseElement
|
||||
from sqlalchemy.sql.schema import Column
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.schema import Table
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from ..operations.batch import BatchOperationsImpl
|
||||
|
||||
|
||||
class SQLiteImpl(DefaultImpl):
|
||||
__dialect__ = "sqlite"
|
||||
|
||||
transactional_ddl = False
|
||||
"""SQLite supports transactional DDL, but pysqlite does not:
|
||||
see: http://bugs.python.org/issue10740
|
||||
"""
|
||||
|
||||
def requires_recreate_in_batch(
|
||||
self, batch_op: BatchOperationsImpl
|
||||
) -> bool:
|
||||
"""Return True if the given :class:`.BatchOperationsImpl`
|
||||
would need the table to be recreated and copied in order to
|
||||
proceed.
|
||||
|
||||
Normally, only returns True on SQLite when operations other
|
||||
than add_column are present.
|
||||
|
||||
"""
|
||||
for op in batch_op.batch:
|
||||
if op[0] == "add_column":
|
||||
col = op[1][1]
|
||||
if isinstance(
|
||||
col.server_default, schema.DefaultClause
|
||||
) and isinstance(col.server_default.arg, sql.ClauseElement):
|
||||
return True
|
||||
elif (
|
||||
isinstance(col.server_default, Computed)
|
||||
and col.server_default.persisted
|
||||
):
|
||||
return True
|
||||
elif op[0] not in ("create_index", "drop_index"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def add_constraint(self, const: Constraint, **kw: Any):
|
||||
# attempt to distinguish between an
|
||||
# auto-gen constraint and an explicit one
|
||||
if const._create_rule is None:
|
||||
raise NotImplementedError(
|
||||
"No support for ALTER of constraints in SQLite dialect. "
|
||||
"Please refer to the batch mode feature which allows for "
|
||||
"SQLite migrations using a copy-and-move strategy."
|
||||
)
|
||||
elif const._create_rule(self):
|
||||
util.warn(
|
||||
"Skipping unsupported ALTER for "
|
||||
"creation of implicit constraint. "
|
||||
"Please refer to the batch mode feature which allows for "
|
||||
"SQLite migrations using a copy-and-move strategy."
|
||||
)
|
||||
|
||||
def drop_constraint(self, const: Constraint, **kw: Any):
|
||||
if const._create_rule is None:
|
||||
raise NotImplementedError(
|
||||
"No support for ALTER of constraints in SQLite dialect. "
|
||||
"Please refer to the batch mode feature which allows for "
|
||||
"SQLite migrations using a copy-and-move strategy."
|
||||
)
|
||||
|
||||
def compare_server_default(
|
||||
self,
|
||||
inspector_column: Column[Any],
|
||||
metadata_column: Column[Any],
|
||||
rendered_metadata_default: Optional[str],
|
||||
rendered_inspector_default: Optional[str],
|
||||
) -> bool:
|
||||
if rendered_metadata_default is not None:
|
||||
rendered_metadata_default = re.sub(
|
||||
r"^\((.+)\)$", r"\1", rendered_metadata_default
|
||||
)
|
||||
|
||||
rendered_metadata_default = re.sub(
|
||||
r"^\"?'(.+)'\"?$", r"\1", rendered_metadata_default
|
||||
)
|
||||
|
||||
if rendered_inspector_default is not None:
|
||||
rendered_inspector_default = re.sub(
|
||||
r"^\((.+)\)$", r"\1", rendered_inspector_default
|
||||
)
|
||||
|
||||
rendered_inspector_default = re.sub(
|
||||
r"^\"?'(.+)'\"?$", r"\1", rendered_inspector_default
|
||||
)
|
||||
|
||||
return rendered_inspector_default != rendered_metadata_default
|
||||
|
||||
def _guess_if_default_is_unparenthesized_sql_expr(
|
||||
self, expr: Optional[str]
|
||||
) -> bool:
|
||||
"""Determine if a server default is a SQL expression or a constant.
|
||||
|
||||
There are too many assertions that expect server defaults to round-trip
|
||||
identically without parenthesis added so we will add parens only in
|
||||
very specific cases.
|
||||
|
||||
"""
|
||||
if not expr:
|
||||
return False
|
||||
elif re.match(r"^[0-9\.]$", expr):
|
||||
return False
|
||||
elif re.match(r"^'.+'$", expr):
|
||||
return False
|
||||
elif re.match(r"^\(.+\)$", expr):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def autogen_column_reflect(
|
||||
self,
|
||||
inspector: Inspector,
|
||||
table: Table,
|
||||
column_info: Dict[str, Any],
|
||||
) -> None:
|
||||
# SQLite expression defaults require parenthesis when sent
|
||||
# as DDL
|
||||
if self._guess_if_default_is_unparenthesized_sql_expr(
|
||||
column_info.get("default", None)
|
||||
):
|
||||
column_info["default"] = "(%s)" % (column_info["default"],)
|
||||
|
||||
def render_ddl_sql_expr(
|
||||
self, expr: ClauseElement, is_server_default: bool = False, **kw
|
||||
) -> str:
|
||||
# SQLite expression defaults require parenthesis when sent
|
||||
# as DDL
|
||||
str_expr = super().render_ddl_sql_expr(
|
||||
expr, is_server_default=is_server_default, **kw
|
||||
)
|
||||
|
||||
if (
|
||||
is_server_default
|
||||
and self._guess_if_default_is_unparenthesized_sql_expr(str_expr)
|
||||
):
|
||||
str_expr = "(%s)" % (str_expr,)
|
||||
return str_expr
|
||||
|
||||
def cast_for_batch_migrate(
|
||||
self,
|
||||
existing: Column[Any],
|
||||
existing_transfer: Dict[str, Union[TypeEngine, Cast]],
|
||||
new_type: TypeEngine,
|
||||
) -> None:
|
||||
if (
|
||||
existing.type._type_affinity is not new_type._type_affinity
|
||||
and not isinstance(new_type, JSON)
|
||||
):
|
||||
existing_transfer["expr"] = cast(
|
||||
existing_transfer["expr"], new_type
|
||||
)
|
||||
|
||||
def correct_for_autogen_constraints(
|
||||
self,
|
||||
conn_unique_constraints,
|
||||
conn_indexes,
|
||||
metadata_unique_constraints,
|
||||
metadata_indexes,
|
||||
):
|
||||
self._skip_functional_indexes(metadata_indexes, conn_indexes)
|
||||
|
||||
|
||||
@compiles(RenameTable, "sqlite")
|
||||
def visit_rename_table(
|
||||
element: RenameTable, compiler: DDLCompiler, **kw
|
||||
) -> str:
|
||||
return "%s RENAME TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_table_name(compiler, element.new_table_name, None),
|
||||
)
|
||||
|
||||
|
||||
@compiles(ColumnName, "sqlite")
|
||||
def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str:
|
||||
return "%s RENAME COLUMN %s TO %s" % (
|
||||
alter_table(compiler, element.table_name, element.schema),
|
||||
format_column_name(compiler, element.column_name),
|
||||
format_column_name(compiler, element.newname),
|
||||
)
|
||||
|
||||
|
||||
# @compiles(AddColumn, 'sqlite')
|
||||
# def visit_add_column(element, compiler, **kw):
|
||||
# return "%s %s" % (
|
||||
# alter_table(compiler, element.table_name, element.schema),
|
||||
# add_column(compiler, element.column, **kw)
|
||||
# )
|
||||
|
||||
|
||||
# def add_column(compiler, column, **kw):
|
||||
# text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
|
||||
# need to modify SQLAlchemy so that the CHECK associated with a Boolean
|
||||
# or Enum gets placed as part of the column constraints, not the Table
|
||||
# see ticket 98
|
||||
# for const in column.constraints:
|
||||
# text += compiler.process(AddConstraint(const))
|
||||
# return text
|
||||
@@ -0,0 +1 @@
|
||||
from .runtime.environment import * # noqa
|
||||
@@ -0,0 +1 @@
|
||||
from .runtime.migration import * # noqa
|
||||
@@ -0,0 +1,5 @@
|
||||
from .operations.base import Operations
|
||||
|
||||
# create proxy functions for
|
||||
# each method on the Operations class.
|
||||
Operations.create_module_class_proxy(globals(), locals())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
from . import toimpl
|
||||
from .base import AbstractOperations
|
||||
from .base import BatchOperations
|
||||
from .base import Operations
|
||||
from .ops import MigrateOperation
|
||||
from .ops import MigrationScript
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AbstractOperations",
|
||||
"Operations",
|
||||
"BatchOperations",
|
||||
"MigrateOperation",
|
||||
"MigrationScript",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,720 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import CheckConstraint
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import ForeignKeyConstraint
|
||||
from sqlalchemy import Index
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy import PrimaryKeyConstraint
|
||||
from sqlalchemy import schema as sql_schema
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import Table
|
||||
from sqlalchemy import types as sqltypes
|
||||
from sqlalchemy.sql.schema import SchemaEventTarget
|
||||
from sqlalchemy.util import OrderedDict
|
||||
from sqlalchemy.util import topological
|
||||
|
||||
from ..util import exc
|
||||
from ..util.sqla_compat import _columns_for_constraint
|
||||
from ..util.sqla_compat import _copy
|
||||
from ..util.sqla_compat import _copy_expression
|
||||
from ..util.sqla_compat import _ensure_scope_for_ddl
|
||||
from ..util.sqla_compat import _fk_is_self_referential
|
||||
from ..util.sqla_compat import _idx_table_bound_expressions
|
||||
from ..util.sqla_compat import _is_type_bound
|
||||
from ..util.sqla_compat import _remove_column_from_collection
|
||||
from ..util.sqla_compat import _resolve_for_variant
|
||||
from ..util.sqla_compat import constraint_name_defined
|
||||
from ..util.sqla_compat import constraint_name_string
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
from sqlalchemy.engine import Dialect
|
||||
from sqlalchemy.sql.elements import ColumnClause
|
||||
from sqlalchemy.sql.elements import quoted_name
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from ..ddl.base import _ServerDefaultType
|
||||
from ..ddl.impl import DefaultImpl
|
||||
|
||||
|
||||
class BatchOperationsImpl:
|
||||
def __init__(
|
||||
self,
|
||||
operations,
|
||||
table_name,
|
||||
schema,
|
||||
recreate,
|
||||
copy_from,
|
||||
table_args,
|
||||
table_kwargs,
|
||||
reflect_args,
|
||||
reflect_kwargs,
|
||||
naming_convention,
|
||||
partial_reordering,
|
||||
):
|
||||
self.operations = operations
|
||||
self.table_name = table_name
|
||||
self.schema = schema
|
||||
if recreate not in ("auto", "always", "never"):
|
||||
raise ValueError(
|
||||
"recreate may be one of 'auto', 'always', or 'never'."
|
||||
)
|
||||
self.recreate = recreate
|
||||
self.copy_from = copy_from
|
||||
self.table_args = table_args
|
||||
self.table_kwargs = dict(table_kwargs)
|
||||
self.reflect_args = reflect_args
|
||||
self.reflect_kwargs = dict(reflect_kwargs)
|
||||
self.reflect_kwargs.setdefault(
|
||||
"listeners", list(self.reflect_kwargs.get("listeners", ()))
|
||||
)
|
||||
self.reflect_kwargs["listeners"].append(
|
||||
("column_reflect", operations.impl.autogen_column_reflect)
|
||||
)
|
||||
self.naming_convention = naming_convention
|
||||
self.partial_reordering = partial_reordering
|
||||
self.batch = []
|
||||
|
||||
@property
|
||||
def dialect(self) -> Dialect:
|
||||
return self.operations.impl.dialect
|
||||
|
||||
@property
|
||||
def impl(self) -> DefaultImpl:
|
||||
return self.operations.impl
|
||||
|
||||
def _should_recreate(self) -> bool:
|
||||
if self.recreate == "auto":
|
||||
return self.operations.impl.requires_recreate_in_batch(self)
|
||||
elif self.recreate == "always":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def flush(self) -> None:
|
||||
should_recreate = self._should_recreate()
|
||||
|
||||
with _ensure_scope_for_ddl(self.impl.connection):
|
||||
if not should_recreate:
|
||||
for opname, arg, kw in self.batch:
|
||||
fn = getattr(self.operations.impl, opname)
|
||||
fn(*arg, **kw)
|
||||
else:
|
||||
if self.naming_convention:
|
||||
m1 = MetaData(naming_convention=self.naming_convention)
|
||||
else:
|
||||
m1 = MetaData()
|
||||
|
||||
if self.copy_from is not None:
|
||||
existing_table = self.copy_from
|
||||
reflected = False
|
||||
else:
|
||||
if self.operations.migration_context.as_sql:
|
||||
raise exc.CommandError(
|
||||
f"This operation cannot proceed in --sql mode; "
|
||||
f"batch mode with dialect "
|
||||
f"{self.operations.migration_context.dialect.name} " # noqa: E501
|
||||
f"requires a live database connection with which "
|
||||
f'to reflect the table "{self.table_name}". '
|
||||
f"To generate a batch SQL migration script using "
|
||||
"table "
|
||||
'"move and copy", a complete Table object '
|
||||
f'should be passed to the "copy_from" argument '
|
||||
"of the batch_alter_table() method so that table "
|
||||
"reflection can be skipped."
|
||||
)
|
||||
|
||||
existing_table = Table(
|
||||
self.table_name,
|
||||
m1,
|
||||
schema=self.schema,
|
||||
autoload_with=self.operations.get_bind(),
|
||||
*self.reflect_args,
|
||||
**self.reflect_kwargs,
|
||||
)
|
||||
reflected = True
|
||||
|
||||
batch_impl = ApplyBatchImpl(
|
||||
self.impl,
|
||||
existing_table,
|
||||
self.table_args,
|
||||
self.table_kwargs,
|
||||
reflected,
|
||||
partial_reordering=self.partial_reordering,
|
||||
)
|
||||
for opname, arg, kw in self.batch:
|
||||
fn = getattr(batch_impl, opname)
|
||||
fn(*arg, **kw)
|
||||
|
||||
batch_impl._create(self.impl)
|
||||
|
||||
def alter_column(self, *arg, **kw) -> None:
|
||||
self.batch.append(("alter_column", arg, kw))
|
||||
|
||||
def add_column(self, *arg, **kw) -> None:
|
||||
if (
|
||||
"insert_before" in kw or "insert_after" in kw
|
||||
) and not self._should_recreate():
|
||||
raise exc.CommandError(
|
||||
"Can't specify insert_before or insert_after when using "
|
||||
"ALTER; please specify recreate='always'"
|
||||
)
|
||||
self.batch.append(("add_column", arg, kw))
|
||||
|
||||
def drop_column(self, *arg, **kw) -> None:
|
||||
self.batch.append(("drop_column", arg, kw))
|
||||
|
||||
def add_constraint(self, const: Constraint) -> None:
|
||||
self.batch.append(("add_constraint", (const,), {}))
|
||||
|
||||
def drop_constraint(self, const: Constraint) -> None:
|
||||
self.batch.append(("drop_constraint", (const,), {}))
|
||||
|
||||
def rename_table(self, *arg, **kw):
|
||||
self.batch.append(("rename_table", arg, kw))
|
||||
|
||||
def create_index(self, idx: Index, **kw: Any) -> None:
|
||||
self.batch.append(("create_index", (idx,), kw))
|
||||
|
||||
def drop_index(self, idx: Index, **kw: Any) -> None:
|
||||
self.batch.append(("drop_index", (idx,), kw))
|
||||
|
||||
def create_table_comment(self, table):
|
||||
self.batch.append(("create_table_comment", (table,), {}))
|
||||
|
||||
def drop_table_comment(self, table):
|
||||
self.batch.append(("drop_table_comment", (table,), {}))
|
||||
|
||||
def create_table(self, table):
|
||||
raise NotImplementedError("Can't create table in batch mode")
|
||||
|
||||
def drop_table(self, table):
|
||||
raise NotImplementedError("Can't drop table in batch mode")
|
||||
|
||||
def create_column_comment(self, column):
|
||||
self.batch.append(("create_column_comment", (column,), {}))
|
||||
|
||||
|
||||
class ApplyBatchImpl:
|
||||
def __init__(
|
||||
self,
|
||||
impl: DefaultImpl,
|
||||
table: Table,
|
||||
table_args: tuple,
|
||||
table_kwargs: Dict[str, Any],
|
||||
reflected: bool,
|
||||
partial_reordering: tuple = (),
|
||||
) -> None:
|
||||
self.impl = impl
|
||||
self.table = table # this is a Table object
|
||||
self.table_args = table_args
|
||||
self.table_kwargs = table_kwargs
|
||||
self.temp_table_name = self._calc_temp_name(table.name)
|
||||
self.new_table: Optional[Table] = None
|
||||
|
||||
self.partial_reordering = partial_reordering # tuple of tuples
|
||||
self.add_col_ordering: Tuple[
|
||||
Tuple[str, str], ...
|
||||
] = () # tuple of tuples
|
||||
|
||||
self.column_transfers = OrderedDict(
|
||||
(c.name, {"expr": c}) for c in self.table.c
|
||||
)
|
||||
self.existing_ordering = list(self.column_transfers)
|
||||
|
||||
self.reflected = reflected
|
||||
self._grab_table_elements()
|
||||
|
||||
@classmethod
|
||||
def _calc_temp_name(cls, tablename: Union[quoted_name, str]) -> str:
|
||||
return ("_alembic_tmp_%s" % tablename)[0:50]
|
||||
|
||||
def _grab_table_elements(self) -> None:
|
||||
schema = self.table.schema
|
||||
self.columns: Dict[str, Column[Any]] = OrderedDict()
|
||||
for c in self.table.c:
|
||||
c_copy = _copy(c, schema=schema)
|
||||
c_copy.unique = c_copy.index = False
|
||||
# ensure that the type object was copied,
|
||||
# as we may need to modify it in-place
|
||||
if isinstance(c.type, SchemaEventTarget):
|
||||
assert c_copy.type is not c.type
|
||||
self.columns[c.name] = c_copy
|
||||
self.named_constraints: Dict[str, Constraint] = {}
|
||||
self.unnamed_constraints = []
|
||||
self.col_named_constraints = {}
|
||||
self.indexes: Dict[str, Index] = {}
|
||||
self.new_indexes: Dict[str, Index] = {}
|
||||
|
||||
for const in self.table.constraints:
|
||||
if _is_type_bound(const):
|
||||
continue
|
||||
elif (
|
||||
self.reflected
|
||||
and isinstance(const, CheckConstraint)
|
||||
and not const.name
|
||||
):
|
||||
# TODO: we are skipping unnamed reflected CheckConstraint
|
||||
# because
|
||||
# we have no way to determine _is_type_bound() for these.
|
||||
pass
|
||||
elif constraint_name_string(const.name):
|
||||
self.named_constraints[const.name] = const
|
||||
else:
|
||||
self.unnamed_constraints.append(const)
|
||||
|
||||
if not self.reflected:
|
||||
for col in self.table.c:
|
||||
for const in col.constraints:
|
||||
if const.name:
|
||||
self.col_named_constraints[const.name] = (col, const)
|
||||
|
||||
for idx in self.table.indexes:
|
||||
self.indexes[idx.name] = idx # type: ignore[index]
|
||||
|
||||
for k in self.table.kwargs:
|
||||
self.table_kwargs.setdefault(k, self.table.kwargs[k])
|
||||
|
||||
def _adjust_self_columns_for_partial_reordering(self) -> None:
|
||||
pairs = set()
|
||||
|
||||
col_by_idx = list(self.columns)
|
||||
|
||||
if self.partial_reordering:
|
||||
for tuple_ in self.partial_reordering:
|
||||
for index, elem in enumerate(tuple_):
|
||||
if index > 0:
|
||||
pairs.add((tuple_[index - 1], elem))
|
||||
else:
|
||||
for index, elem in enumerate(self.existing_ordering):
|
||||
if index > 0:
|
||||
pairs.add((col_by_idx[index - 1], elem))
|
||||
|
||||
pairs.update(self.add_col_ordering)
|
||||
|
||||
# this can happen if some columns were dropped and not removed
|
||||
# from existing_ordering. this should be prevented already, but
|
||||
# conservatively making sure this didn't happen
|
||||
pairs_list = [p for p in pairs if p[0] != p[1]]
|
||||
|
||||
sorted_ = list(
|
||||
topological.sort(pairs_list, col_by_idx, deterministic_order=True)
|
||||
)
|
||||
self.columns = OrderedDict((k, self.columns[k]) for k in sorted_)
|
||||
self.column_transfers = OrderedDict(
|
||||
(k, self.column_transfers[k]) for k in sorted_
|
||||
)
|
||||
|
||||
def _transfer_elements_to_new_table(self) -> None:
|
||||
assert self.new_table is None, "Can only create new table once"
|
||||
|
||||
m = MetaData()
|
||||
schema = self.table.schema
|
||||
|
||||
if self.partial_reordering or self.add_col_ordering:
|
||||
self._adjust_self_columns_for_partial_reordering()
|
||||
|
||||
self.new_table = new_table = Table(
|
||||
self.temp_table_name,
|
||||
m,
|
||||
*(list(self.columns.values()) + list(self.table_args)),
|
||||
schema=schema,
|
||||
**self.table_kwargs,
|
||||
)
|
||||
|
||||
for const in (
|
||||
list(self.named_constraints.values()) + self.unnamed_constraints
|
||||
):
|
||||
const_columns = {c.key for c in _columns_for_constraint(const)}
|
||||
|
||||
if not const_columns.issubset(self.column_transfers):
|
||||
continue
|
||||
|
||||
const_copy: Constraint
|
||||
if isinstance(const, ForeignKeyConstraint):
|
||||
if _fk_is_self_referential(const):
|
||||
# for self-referential constraint, refer to the
|
||||
# *original* table name, and not _alembic_batch_temp.
|
||||
# This is consistent with how we're handling
|
||||
# FK constraints from other tables; we assume SQLite
|
||||
# no foreign keys just keeps the names unchanged, so
|
||||
# when we rename back, they match again.
|
||||
const_copy = _copy(
|
||||
const, schema=schema, target_table=self.table
|
||||
)
|
||||
else:
|
||||
# "target_table" for ForeignKeyConstraint.copy() is
|
||||
# only used if the FK is detected as being
|
||||
# self-referential, which we are handling above.
|
||||
const_copy = _copy(const, schema=schema)
|
||||
else:
|
||||
const_copy = _copy(
|
||||
const, schema=schema, target_table=new_table
|
||||
)
|
||||
if isinstance(const, ForeignKeyConstraint):
|
||||
self._setup_referent(m, const)
|
||||
new_table.append_constraint(const_copy)
|
||||
|
||||
def _gather_indexes_from_both_tables(self) -> List[Index]:
|
||||
assert self.new_table is not None
|
||||
idx: List[Index] = []
|
||||
|
||||
for idx_existing in self.indexes.values():
|
||||
# this is a lift-and-move from Table.to_metadata
|
||||
|
||||
if idx_existing._column_flag:
|
||||
continue
|
||||
|
||||
idx_copy = Index(
|
||||
idx_existing.name,
|
||||
unique=idx_existing.unique,
|
||||
*[
|
||||
_copy_expression(expr, self.new_table)
|
||||
for expr in _idx_table_bound_expressions(idx_existing)
|
||||
],
|
||||
_table=self.new_table,
|
||||
**idx_existing.kwargs,
|
||||
)
|
||||
idx.append(idx_copy)
|
||||
|
||||
for index in self.new_indexes.values():
|
||||
idx.append(
|
||||
Index(
|
||||
index.name,
|
||||
unique=index.unique,
|
||||
*[self.new_table.c[col] for col in index.columns.keys()],
|
||||
**index.kwargs,
|
||||
)
|
||||
)
|
||||
return idx
|
||||
|
||||
def _setup_referent(
|
||||
self, metadata: MetaData, constraint: ForeignKeyConstraint
|
||||
) -> None:
|
||||
spec = constraint.elements[0]._get_colspec()
|
||||
parts = spec.split(".")
|
||||
tname = parts[-2]
|
||||
if len(parts) == 3:
|
||||
referent_schema = parts[0]
|
||||
else:
|
||||
referent_schema = None
|
||||
|
||||
if tname != self.temp_table_name:
|
||||
key = sql_schema._get_table_key(tname, referent_schema)
|
||||
|
||||
def colspec(elem: Any):
|
||||
return elem._get_colspec()
|
||||
|
||||
if key in metadata.tables:
|
||||
t = metadata.tables[key]
|
||||
for elem in constraint.elements:
|
||||
colname = colspec(elem).split(".")[-1]
|
||||
if colname not in t.c:
|
||||
t.append_column(Column(colname, sqltypes.NULLTYPE))
|
||||
else:
|
||||
Table(
|
||||
tname,
|
||||
metadata,
|
||||
*[
|
||||
Column(n, sqltypes.NULLTYPE)
|
||||
for n in [
|
||||
colspec(elem).split(".")[-1]
|
||||
for elem in constraint.elements
|
||||
]
|
||||
],
|
||||
schema=referent_schema,
|
||||
)
|
||||
|
||||
def _create(self, op_impl: DefaultImpl) -> None:
|
||||
self._transfer_elements_to_new_table()
|
||||
|
||||
op_impl.prep_table_for_batch(self, self.table)
|
||||
assert self.new_table is not None
|
||||
op_impl.create_table(self.new_table)
|
||||
|
||||
try:
|
||||
op_impl._exec(
|
||||
self.new_table.insert()
|
||||
.inline()
|
||||
.from_select(
|
||||
list(
|
||||
k
|
||||
for k, transfer in self.column_transfers.items()
|
||||
if "expr" in transfer
|
||||
),
|
||||
select(
|
||||
*[
|
||||
transfer["expr"]
|
||||
for transfer in self.column_transfers.values()
|
||||
if "expr" in transfer
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
op_impl.drop_table(self.table)
|
||||
except:
|
||||
op_impl.drop_table(self.new_table)
|
||||
raise
|
||||
else:
|
||||
op_impl.rename_table(
|
||||
self.temp_table_name, self.table.name, schema=self.table.schema
|
||||
)
|
||||
self.new_table.name = self.table.name
|
||||
try:
|
||||
for idx in self._gather_indexes_from_both_tables():
|
||||
op_impl.create_index(idx)
|
||||
finally:
|
||||
self.new_table.name = self.temp_table_name
|
||||
|
||||
def alter_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column_name: str,
|
||||
nullable: Optional[bool] = None,
|
||||
server_default: Union[
|
||||
_ServerDefaultType, None, Literal[False]
|
||||
] = False,
|
||||
name: Optional[str] = None,
|
||||
type_: Optional[TypeEngine] = None,
|
||||
autoincrement: Optional[Union[bool, Literal["auto"]]] = None,
|
||||
comment: Union[str, Literal[False]] = False,
|
||||
**kw,
|
||||
) -> None:
|
||||
existing = self.columns[column_name]
|
||||
existing_transfer: Dict[str, Any] = self.column_transfers[column_name]
|
||||
if name is not None and name != column_name:
|
||||
# note that we don't change '.key' - we keep referring
|
||||
# to the renamed column by its old key in _create(). neat!
|
||||
existing.name = name
|
||||
existing_transfer["name"] = name
|
||||
|
||||
existing_type = kw.get("existing_type", None)
|
||||
if existing_type:
|
||||
resolved_existing_type = _resolve_for_variant(
|
||||
kw["existing_type"], self.impl.dialect
|
||||
)
|
||||
|
||||
# pop named constraints for Boolean/Enum for rename
|
||||
if (
|
||||
isinstance(resolved_existing_type, SchemaEventTarget)
|
||||
and resolved_existing_type.name # type:ignore[attr-defined] # noqa E501
|
||||
):
|
||||
self.named_constraints.pop(
|
||||
resolved_existing_type.name, # type:ignore[attr-defined] # noqa E501
|
||||
None,
|
||||
)
|
||||
|
||||
if type_ is not None:
|
||||
type_ = sqltypes.to_instance(type_)
|
||||
# old type is being discarded so turn off eventing
|
||||
# rules. Alternatively we can
|
||||
# erase the events set up by this type, but this is simpler.
|
||||
# we also ignore the drop_constraint that will come here from
|
||||
# Operations.implementation_for(alter_column)
|
||||
|
||||
if isinstance(existing.type, SchemaEventTarget):
|
||||
existing.type._create_events = ( # type:ignore[attr-defined]
|
||||
existing.type.create_constraint # type:ignore[attr-defined] # noqa
|
||||
) = False
|
||||
|
||||
self.impl.cast_for_batch_migrate(
|
||||
existing, existing_transfer, type_
|
||||
)
|
||||
|
||||
existing.type = type_
|
||||
|
||||
# we *dont* however set events for the new type, because
|
||||
# alter_column is invoked from
|
||||
# Operations.implementation_for(alter_column) which already
|
||||
# will emit an add_constraint()
|
||||
|
||||
if nullable is not None:
|
||||
existing.nullable = nullable
|
||||
if server_default is not False:
|
||||
if server_default is None:
|
||||
existing.server_default = None
|
||||
else:
|
||||
sql_schema.DefaultClause(
|
||||
server_default # type: ignore[arg-type]
|
||||
)._set_parent(existing)
|
||||
if autoincrement is not None:
|
||||
existing.autoincrement = bool(autoincrement)
|
||||
|
||||
if comment is not False:
|
||||
existing.comment = comment
|
||||
|
||||
def _setup_dependencies_for_add_column(
|
||||
self,
|
||||
colname: str,
|
||||
insert_before: Optional[str],
|
||||
insert_after: Optional[str],
|
||||
) -> None:
|
||||
index_cols = self.existing_ordering
|
||||
col_indexes = {name: i for i, name in enumerate(index_cols)}
|
||||
|
||||
if not self.partial_reordering:
|
||||
if insert_after:
|
||||
if not insert_before:
|
||||
if insert_after in col_indexes:
|
||||
# insert after an existing column
|
||||
idx = col_indexes[insert_after] + 1
|
||||
if idx < len(index_cols):
|
||||
insert_before = index_cols[idx]
|
||||
else:
|
||||
# insert after a column that is also new
|
||||
insert_before = dict(self.add_col_ordering)[
|
||||
insert_after
|
||||
]
|
||||
if insert_before:
|
||||
if not insert_after:
|
||||
if insert_before in col_indexes:
|
||||
# insert before an existing column
|
||||
idx = col_indexes[insert_before] - 1
|
||||
if idx >= 0:
|
||||
insert_after = index_cols[idx]
|
||||
else:
|
||||
# insert before a column that is also new
|
||||
insert_after = {
|
||||
b: a for a, b in self.add_col_ordering
|
||||
}[insert_before]
|
||||
|
||||
if insert_before:
|
||||
self.add_col_ordering += ((colname, insert_before),)
|
||||
if insert_after:
|
||||
self.add_col_ordering += ((insert_after, colname),)
|
||||
|
||||
if (
|
||||
not self.partial_reordering
|
||||
and not insert_before
|
||||
and not insert_after
|
||||
and col_indexes
|
||||
):
|
||||
self.add_col_ordering += ((index_cols[-1], colname),)
|
||||
|
||||
def add_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column: Column[Any],
|
||||
insert_before: Optional[str] = None,
|
||||
insert_after: Optional[str] = None,
|
||||
**kw,
|
||||
) -> None:
|
||||
self._setup_dependencies_for_add_column(
|
||||
column.name, insert_before, insert_after
|
||||
)
|
||||
# we copy the column because operations.add_column()
|
||||
# gives us a Column that is part of a Table already.
|
||||
self.columns[column.name] = _copy(column, schema=self.table.schema)
|
||||
self.column_transfers[column.name] = {}
|
||||
|
||||
def drop_column(
|
||||
self,
|
||||
table_name: str,
|
||||
column: Union[ColumnClause[Any], Column[Any]],
|
||||
**kw,
|
||||
) -> None:
|
||||
if column.name in self.table.primary_key.columns:
|
||||
_remove_column_from_collection(
|
||||
self.table.primary_key.columns, column
|
||||
)
|
||||
del self.columns[column.name]
|
||||
del self.column_transfers[column.name]
|
||||
self.existing_ordering.remove(column.name)
|
||||
|
||||
# pop named constraints for Boolean/Enum for rename
|
||||
if (
|
||||
"existing_type" in kw
|
||||
and isinstance(kw["existing_type"], SchemaEventTarget)
|
||||
and kw["existing_type"].name # type:ignore[attr-defined]
|
||||
):
|
||||
self.named_constraints.pop(
|
||||
kw["existing_type"].name, None # type:ignore[attr-defined]
|
||||
)
|
||||
|
||||
def create_column_comment(self, column):
|
||||
"""the batch table creation function will issue create_column_comment
|
||||
on the real "impl" as part of the create table process.
|
||||
|
||||
That is, the Column object will have the comment on it already,
|
||||
so when it is received by add_column() it will be a normal part of
|
||||
the CREATE TABLE and doesn't need an extra step here.
|
||||
|
||||
"""
|
||||
|
||||
def create_table_comment(self, table):
|
||||
"""the batch table creation function will issue create_table_comment
|
||||
on the real "impl" as part of the create table process.
|
||||
|
||||
"""
|
||||
|
||||
def drop_table_comment(self, table):
|
||||
"""the batch table creation function will issue drop_table_comment
|
||||
on the real "impl" as part of the create table process.
|
||||
|
||||
"""
|
||||
|
||||
def add_constraint(self, const: Constraint) -> None:
|
||||
if not constraint_name_defined(const.name):
|
||||
raise ValueError("Constraint must have a name")
|
||||
if isinstance(const, sql_schema.PrimaryKeyConstraint):
|
||||
if self.table.primary_key in self.unnamed_constraints:
|
||||
self.unnamed_constraints.remove(self.table.primary_key)
|
||||
|
||||
if constraint_name_string(const.name):
|
||||
self.named_constraints[const.name] = const
|
||||
else:
|
||||
self.unnamed_constraints.append(const)
|
||||
|
||||
def drop_constraint(self, const: Constraint) -> None:
|
||||
if not const.name:
|
||||
raise ValueError("Constraint must have a name")
|
||||
try:
|
||||
if const.name in self.col_named_constraints:
|
||||
col, const = self.col_named_constraints.pop(const.name)
|
||||
|
||||
for col_const in list(self.columns[col.name].constraints):
|
||||
if col_const.name == const.name:
|
||||
self.columns[col.name].constraints.remove(col_const)
|
||||
elif constraint_name_string(const.name):
|
||||
const = self.named_constraints.pop(const.name)
|
||||
elif const in self.unnamed_constraints:
|
||||
self.unnamed_constraints.remove(const)
|
||||
|
||||
except KeyError:
|
||||
if _is_type_bound(const):
|
||||
# type-bound constraints are only included in the new
|
||||
# table via their type object in any case, so ignore the
|
||||
# drop_constraint() that comes here via the
|
||||
# Operations.implementation_for(alter_column)
|
||||
return
|
||||
raise ValueError("No such constraint: '%s'" % const.name)
|
||||
else:
|
||||
if isinstance(const, PrimaryKeyConstraint):
|
||||
for col in const.columns:
|
||||
self.columns[col.name].primary_key = False
|
||||
|
||||
def create_index(self, idx: Index) -> None:
|
||||
self.new_indexes[idx.name] = idx # type: ignore[index]
|
||||
|
||||
def drop_index(self, idx: Index) -> None:
|
||||
try:
|
||||
del self.indexes[idx.name] # type: ignore[arg-type]
|
||||
except KeyError:
|
||||
raise ValueError("No such index: '%s'" % idx.name)
|
||||
|
||||
def rename_table(self, *arg, **kw):
|
||||
raise NotImplementedError("TODO")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,290 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
from sqlalchemy.sql.schema import Column
|
||||
from sqlalchemy.sql.schema import Constraint
|
||||
from sqlalchemy.sql.schema import Index
|
||||
from sqlalchemy.types import Integer
|
||||
from sqlalchemy.types import NULLTYPE
|
||||
|
||||
from .. import util
|
||||
from ..util import sqla_compat
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlalchemy.sql.elements import TextClause
|
||||
from sqlalchemy.sql.schema import CheckConstraint
|
||||
from sqlalchemy.sql.schema import ForeignKey
|
||||
from sqlalchemy.sql.schema import ForeignKeyConstraint
|
||||
from sqlalchemy.sql.schema import MetaData
|
||||
from sqlalchemy.sql.schema import PrimaryKeyConstraint
|
||||
from sqlalchemy.sql.schema import Table
|
||||
from sqlalchemy.sql.schema import UniqueConstraint
|
||||
from sqlalchemy.sql.type_api import TypeEngine
|
||||
|
||||
from ..runtime.migration import MigrationContext
|
||||
|
||||
|
||||
class SchemaObjects:
|
||||
def __init__(
|
||||
self, migration_context: Optional[MigrationContext] = None
|
||||
) -> None:
|
||||
self.migration_context = migration_context
|
||||
|
||||
def primary_key_constraint(
|
||||
self,
|
||||
name: Optional[sqla_compat._ConstraintNameDefined],
|
||||
table_name: str,
|
||||
cols: Sequence[str],
|
||||
schema: Optional[str] = None,
|
||||
**dialect_kw,
|
||||
) -> PrimaryKeyConstraint:
|
||||
m = self.metadata()
|
||||
columns = [sa_schema.Column(n, NULLTYPE) for n in cols]
|
||||
t = sa_schema.Table(table_name, m, *columns, schema=schema)
|
||||
# SQLAlchemy primary key constraint name arg is wrongly typed on
|
||||
# the SQLAlchemy side through 2.0.5 at least
|
||||
p = sa_schema.PrimaryKeyConstraint(
|
||||
*[t.c[n] for n in cols], name=name, **dialect_kw # type: ignore
|
||||
)
|
||||
return p
|
||||
|
||||
def foreign_key_constraint(
|
||||
self,
|
||||
name: Optional[sqla_compat._ConstraintNameDefined],
|
||||
source: str,
|
||||
referent: str,
|
||||
local_cols: List[str],
|
||||
remote_cols: List[str],
|
||||
onupdate: Optional[str] = None,
|
||||
ondelete: Optional[str] = None,
|
||||
deferrable: Optional[bool] = None,
|
||||
source_schema: Optional[str] = None,
|
||||
referent_schema: Optional[str] = None,
|
||||
initially: Optional[str] = None,
|
||||
match: Optional[str] = None,
|
||||
**dialect_kw,
|
||||
) -> ForeignKeyConstraint:
|
||||
m = self.metadata()
|
||||
if source == referent and source_schema == referent_schema:
|
||||
t1_cols = local_cols + remote_cols
|
||||
else:
|
||||
t1_cols = local_cols
|
||||
sa_schema.Table(
|
||||
referent,
|
||||
m,
|
||||
*[sa_schema.Column(n, NULLTYPE) for n in remote_cols],
|
||||
schema=referent_schema,
|
||||
)
|
||||
|
||||
t1 = sa_schema.Table(
|
||||
source,
|
||||
m,
|
||||
*[
|
||||
sa_schema.Column(n, NULLTYPE)
|
||||
for n in util.unique_list(t1_cols)
|
||||
],
|
||||
schema=source_schema,
|
||||
)
|
||||
|
||||
tname = (
|
||||
"%s.%s" % (referent_schema, referent)
|
||||
if referent_schema
|
||||
else referent
|
||||
)
|
||||
|
||||
dialect_kw["match"] = match
|
||||
|
||||
f = sa_schema.ForeignKeyConstraint(
|
||||
local_cols,
|
||||
["%s.%s" % (tname, n) for n in remote_cols],
|
||||
name=name,
|
||||
onupdate=onupdate,
|
||||
ondelete=ondelete,
|
||||
deferrable=deferrable,
|
||||
initially=initially,
|
||||
**dialect_kw,
|
||||
)
|
||||
t1.append_constraint(f)
|
||||
|
||||
return f
|
||||
|
||||
def unique_constraint(
|
||||
self,
|
||||
name: Optional[sqla_compat._ConstraintNameDefined],
|
||||
source: str,
|
||||
local_cols: Sequence[str],
|
||||
schema: Optional[str] = None,
|
||||
**kw,
|
||||
) -> UniqueConstraint:
|
||||
t = sa_schema.Table(
|
||||
source,
|
||||
self.metadata(),
|
||||
*[sa_schema.Column(n, NULLTYPE) for n in local_cols],
|
||||
schema=schema,
|
||||
)
|
||||
kw["name"] = name
|
||||
uq = sa_schema.UniqueConstraint(*[t.c[n] for n in local_cols], **kw)
|
||||
# TODO: need event tests to ensure the event
|
||||
# is fired off here
|
||||
t.append_constraint(uq)
|
||||
return uq
|
||||
|
||||
def check_constraint(
|
||||
self,
|
||||
name: Optional[sqla_compat._ConstraintNameDefined],
|
||||
source: str,
|
||||
condition: Union[str, TextClause, ColumnElement[Any]],
|
||||
schema: Optional[str] = None,
|
||||
**kw,
|
||||
) -> Union[CheckConstraint]:
|
||||
t = sa_schema.Table(
|
||||
source,
|
||||
self.metadata(),
|
||||
sa_schema.Column("x", Integer),
|
||||
schema=schema,
|
||||
)
|
||||
ck = sa_schema.CheckConstraint(condition, name=name, **kw)
|
||||
t.append_constraint(ck)
|
||||
return ck
|
||||
|
||||
def generic_constraint(
|
||||
self,
|
||||
name: Optional[sqla_compat._ConstraintNameDefined],
|
||||
table_name: str,
|
||||
type_: Optional[str],
|
||||
schema: Optional[str] = None,
|
||||
**kw,
|
||||
) -> Any:
|
||||
t = self.table(table_name, schema=schema)
|
||||
types: Dict[Optional[str], Any] = {
|
||||
"foreignkey": lambda name: sa_schema.ForeignKeyConstraint(
|
||||
[], [], name=name
|
||||
),
|
||||
"primary": sa_schema.PrimaryKeyConstraint,
|
||||
"unique": sa_schema.UniqueConstraint,
|
||||
"check": lambda name: sa_schema.CheckConstraint("", name=name),
|
||||
None: sa_schema.Constraint,
|
||||
}
|
||||
try:
|
||||
const = types[type_]
|
||||
except KeyError as ke:
|
||||
raise TypeError(
|
||||
"'type' can be one of %s"
|
||||
% ", ".join(sorted(repr(x) for x in types))
|
||||
) from ke
|
||||
else:
|
||||
const = const(name=name)
|
||||
t.append_constraint(const)
|
||||
return const
|
||||
|
||||
def metadata(self) -> MetaData:
|
||||
kw = {}
|
||||
if (
|
||||
self.migration_context is not None
|
||||
and "target_metadata" in self.migration_context.opts
|
||||
):
|
||||
mt = self.migration_context.opts["target_metadata"]
|
||||
if hasattr(mt, "naming_convention"):
|
||||
kw["naming_convention"] = mt.naming_convention
|
||||
return sa_schema.MetaData(**kw)
|
||||
|
||||
def table(self, name: str, *columns, **kw) -> Table:
|
||||
m = self.metadata()
|
||||
|
||||
cols = [
|
||||
sqla_compat._copy(c) if c.table is not None else c
|
||||
for c in columns
|
||||
if isinstance(c, Column)
|
||||
]
|
||||
# these flags have already added their UniqueConstraint /
|
||||
# Index objects to the table, so flip them off here.
|
||||
# SQLAlchemy tometadata() avoids this instead by preserving the
|
||||
# flags and skipping the constraints that have _type_bound on them,
|
||||
# but for a migration we'd rather list out the constraints
|
||||
# explicitly.
|
||||
_constraints_included = kw.pop("_constraints_included", False)
|
||||
if _constraints_included:
|
||||
for c in cols:
|
||||
c.unique = c.index = False
|
||||
|
||||
t = sa_schema.Table(name, m, *cols, **kw)
|
||||
|
||||
constraints = [
|
||||
(
|
||||
sqla_compat._copy(elem, target_table=t)
|
||||
if getattr(elem, "parent", None) is not t
|
||||
and getattr(elem, "parent", None) is not None
|
||||
else elem
|
||||
)
|
||||
for elem in columns
|
||||
if isinstance(elem, (Constraint, Index))
|
||||
]
|
||||
|
||||
for const in constraints:
|
||||
t.append_constraint(const)
|
||||
|
||||
for f in t.foreign_keys:
|
||||
self._ensure_table_for_fk(m, f)
|
||||
return t
|
||||
|
||||
def column(self, name: str, type_: TypeEngine, **kw) -> Column:
|
||||
return sa_schema.Column(name, type_, **kw)
|
||||
|
||||
def index(
|
||||
self,
|
||||
name: Optional[str],
|
||||
tablename: Optional[str],
|
||||
columns: Sequence[Union[str, TextClause, ColumnElement[Any]]],
|
||||
schema: Optional[str] = None,
|
||||
**kw,
|
||||
) -> Index:
|
||||
t = sa_schema.Table(
|
||||
tablename or "no_table",
|
||||
self.metadata(),
|
||||
schema=schema,
|
||||
)
|
||||
kw["_table"] = t
|
||||
idx = sa_schema.Index(
|
||||
name,
|
||||
*[util.sqla_compat._textual_index_column(t, n) for n in columns],
|
||||
**kw,
|
||||
)
|
||||
return idx
|
||||
|
||||
def _parse_table_key(self, table_key: str) -> Tuple[Optional[str], str]:
|
||||
if "." in table_key:
|
||||
tokens = table_key.split(".")
|
||||
sname: Optional[str] = ".".join(tokens[0:-1])
|
||||
tname = tokens[-1]
|
||||
else:
|
||||
tname = table_key
|
||||
sname = None
|
||||
return (sname, tname)
|
||||
|
||||
def _ensure_table_for_fk(self, metadata: MetaData, fk: ForeignKey) -> None:
|
||||
"""create a placeholder Table object for the referent of a
|
||||
ForeignKey.
|
||||
|
||||
"""
|
||||
if isinstance(fk._colspec, str):
|
||||
table_key, cname = fk._colspec.rsplit(".", 1)
|
||||
sname, tname = self._parse_table_key(table_key)
|
||||
if table_key not in metadata.tables:
|
||||
rel_t = sa_schema.Table(tname, metadata, schema=sname)
|
||||
else:
|
||||
rel_t = metadata.tables[table_key]
|
||||
if cname not in rel_t.c:
|
||||
rel_t.append_column(sa_schema.Column(cname, NULLTYPE))
|
||||
@@ -0,0 +1,261 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import schema as sa_schema
|
||||
|
||||
from . import ops
|
||||
from .base import Operations
|
||||
from ..util.sqla_compat import _copy
|
||||
from ..util.sqla_compat import sqla_2
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.AlterColumnOp)
|
||||
def alter_column(
|
||||
operations: "Operations", operation: "ops.AlterColumnOp"
|
||||
) -> None:
|
||||
compiler = operations.impl.dialect.statement_compiler(
|
||||
operations.impl.dialect, None
|
||||
)
|
||||
|
||||
existing_type = operation.existing_type
|
||||
existing_nullable = operation.existing_nullable
|
||||
existing_server_default = operation.existing_server_default
|
||||
type_ = operation.modify_type
|
||||
column_name = operation.column_name
|
||||
table_name = operation.table_name
|
||||
schema = operation.schema
|
||||
server_default = operation.modify_server_default
|
||||
new_column_name = operation.modify_name
|
||||
nullable = operation.modify_nullable
|
||||
comment = operation.modify_comment
|
||||
existing_comment = operation.existing_comment
|
||||
|
||||
def _count_constraint(constraint):
|
||||
return not isinstance(constraint, sa_schema.PrimaryKeyConstraint) and (
|
||||
not constraint._create_rule or constraint._create_rule(compiler)
|
||||
)
|
||||
|
||||
if existing_type and type_:
|
||||
t = operations.schema_obj.table(
|
||||
table_name,
|
||||
sa_schema.Column(column_name, existing_type),
|
||||
schema=schema,
|
||||
)
|
||||
for constraint in t.constraints:
|
||||
if _count_constraint(constraint):
|
||||
operations.impl.drop_constraint(constraint)
|
||||
|
||||
# some weird pyright quirk here, these have Literal[False]
|
||||
# in their types, not sure why pyright thinks they could be True
|
||||
assert existing_server_default is not True # type: ignore[comparison-overlap] # noqa: E501
|
||||
assert comment is not True # type: ignore[comparison-overlap]
|
||||
|
||||
operations.impl.alter_column(
|
||||
table_name,
|
||||
column_name,
|
||||
nullable=nullable,
|
||||
server_default=server_default,
|
||||
name=new_column_name,
|
||||
type_=type_,
|
||||
schema=schema,
|
||||
existing_type=existing_type,
|
||||
existing_server_default=existing_server_default,
|
||||
existing_nullable=existing_nullable,
|
||||
comment=comment,
|
||||
existing_comment=existing_comment,
|
||||
**operation.kw,
|
||||
)
|
||||
|
||||
if type_:
|
||||
t = operations.schema_obj.table(
|
||||
table_name,
|
||||
operations.schema_obj.column(column_name, type_),
|
||||
schema=schema,
|
||||
)
|
||||
for constraint in t.constraints:
|
||||
if _count_constraint(constraint):
|
||||
operations.impl.add_constraint(constraint)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.DropTableOp)
|
||||
def drop_table(operations: "Operations", operation: "ops.DropTableOp") -> None:
|
||||
kw = {}
|
||||
if operation.if_exists is not None:
|
||||
kw["if_exists"] = operation.if_exists
|
||||
operations.impl.drop_table(
|
||||
operation.to_table(operations.migration_context), **kw
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.DropColumnOp)
|
||||
def drop_column(
|
||||
operations: "Operations", operation: "ops.DropColumnOp"
|
||||
) -> None:
|
||||
column = operation.to_column(operations.migration_context)
|
||||
operations.impl.drop_column(
|
||||
operation.table_name,
|
||||
column,
|
||||
schema=operation.schema,
|
||||
if_exists=operation.if_exists,
|
||||
**operation.kw,
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.CreateIndexOp)
|
||||
def create_index(
|
||||
operations: "Operations", operation: "ops.CreateIndexOp"
|
||||
) -> None:
|
||||
idx = operation.to_index(operations.migration_context)
|
||||
kw = {}
|
||||
if operation.if_not_exists is not None:
|
||||
kw["if_not_exists"] = operation.if_not_exists
|
||||
operations.impl.create_index(idx, **kw)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.DropIndexOp)
|
||||
def drop_index(operations: "Operations", operation: "ops.DropIndexOp") -> None:
|
||||
kw = {}
|
||||
if operation.if_exists is not None:
|
||||
kw["if_exists"] = operation.if_exists
|
||||
|
||||
operations.impl.drop_index(
|
||||
operation.to_index(operations.migration_context),
|
||||
**kw,
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.CreateTableOp)
|
||||
def create_table(
|
||||
operations: "Operations", operation: "ops.CreateTableOp"
|
||||
) -> "Table":
|
||||
kw = {}
|
||||
if operation.if_not_exists is not None:
|
||||
kw["if_not_exists"] = operation.if_not_exists
|
||||
table = operation.to_table(operations.migration_context)
|
||||
operations.impl.create_table(table, **kw)
|
||||
return table
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.RenameTableOp)
|
||||
def rename_table(
|
||||
operations: "Operations", operation: "ops.RenameTableOp"
|
||||
) -> None:
|
||||
operations.impl.rename_table(
|
||||
operation.table_name, operation.new_table_name, schema=operation.schema
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.CreateTableCommentOp)
|
||||
def create_table_comment(
|
||||
operations: "Operations", operation: "ops.CreateTableCommentOp"
|
||||
) -> None:
|
||||
table = operation.to_table(operations.migration_context)
|
||||
operations.impl.create_table_comment(table)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.DropTableCommentOp)
|
||||
def drop_table_comment(
|
||||
operations: "Operations", operation: "ops.DropTableCommentOp"
|
||||
) -> None:
|
||||
table = operation.to_table(operations.migration_context)
|
||||
operations.impl.drop_table_comment(table)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.AddColumnOp)
|
||||
def add_column(operations: "Operations", operation: "ops.AddColumnOp") -> None:
|
||||
table_name = operation.table_name
|
||||
column = operation.column
|
||||
schema = operation.schema
|
||||
kw = operation.kw
|
||||
inline_references = operation.inline_references
|
||||
inline_primary_key = operation.inline_primary_key
|
||||
|
||||
if column.table is not None:
|
||||
column = _copy(column)
|
||||
|
||||
t = operations.schema_obj.table(table_name, column, schema=schema)
|
||||
operations.impl.add_column(
|
||||
table_name,
|
||||
column,
|
||||
schema=schema,
|
||||
if_not_exists=operation.if_not_exists,
|
||||
inline_references=inline_references,
|
||||
inline_primary_key=inline_primary_key,
|
||||
**kw,
|
||||
)
|
||||
|
||||
for constraint in t.constraints:
|
||||
if not isinstance(constraint, sa_schema.PrimaryKeyConstraint):
|
||||
# Skip ForeignKeyConstraint if it was rendered inline
|
||||
# This only happens when inline_references=True AND there's exactly
|
||||
# one FK AND the constraint is single-column
|
||||
if (
|
||||
inline_references
|
||||
and isinstance(constraint, sa_schema.ForeignKeyConstraint)
|
||||
and len(column.foreign_keys) == 1
|
||||
and len(constraint.columns) == 1
|
||||
):
|
||||
continue
|
||||
operations.impl.add_constraint(constraint)
|
||||
for index in t.indexes:
|
||||
operations.impl.create_index(index)
|
||||
|
||||
with_comment = (
|
||||
operations.impl.dialect.supports_comments
|
||||
and not operations.impl.dialect.inline_comments
|
||||
)
|
||||
comment = column.comment
|
||||
if comment and with_comment:
|
||||
operations.impl.create_column_comment(column)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.AddConstraintOp)
|
||||
def create_constraint(
|
||||
operations: "Operations", operation: "ops.AddConstraintOp"
|
||||
) -> None:
|
||||
operations.impl.add_constraint(
|
||||
operation.to_constraint(operations.migration_context)
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.DropConstraintOp)
|
||||
def drop_constraint(
|
||||
operations: "Operations", operation: "ops.DropConstraintOp"
|
||||
) -> None:
|
||||
kw = {}
|
||||
if operation.if_exists is not None:
|
||||
if not sqla_2:
|
||||
raise NotImplementedError("SQLAlchemy 2.0 required")
|
||||
kw["if_exists"] = operation.if_exists
|
||||
operations.impl.drop_constraint(
|
||||
operations.schema_obj.generic_constraint(
|
||||
operation.constraint_name,
|
||||
operation.table_name,
|
||||
operation.constraint_type,
|
||||
schema=operation.schema,
|
||||
),
|
||||
**kw,
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.BulkInsertOp)
|
||||
def bulk_insert(
|
||||
operations: "Operations", operation: "ops.BulkInsertOp"
|
||||
) -> None:
|
||||
operations.impl.bulk_insert( # type: ignore[union-attr]
|
||||
operation.table, operation.rows, multiinsert=operation.multiinsert
|
||||
)
|
||||
|
||||
|
||||
@Operations.implementation_for(ops.ExecuteSQLOp)
|
||||
def execute_sql(
|
||||
operations: "Operations", operation: "ops.ExecuteSQLOp"
|
||||
) -> None:
|
||||
operations.migration_context.impl.execute(
|
||||
operation.sqltext, execution_options=operation.execution_options
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import metadata
|
||||
import logging
|
||||
import re
|
||||
from types import ModuleType
|
||||
from typing import Callable
|
||||
from typing import Pattern
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .. import util
|
||||
from ..util import DispatchPriority
|
||||
from ..util import PriorityDispatcher
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..util import PriorityDispatchResult
|
||||
|
||||
_all_plugins = {}
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Plugin:
|
||||
"""Describe a series of functions that are pulled in as a plugin.
|
||||
|
||||
This is initially to provide for portable lists of autogenerate
|
||||
comparison functions, however the setup for a plugin can run any
|
||||
other kinds of global registration as well.
|
||||
|
||||
.. versionadded:: 1.18.0
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
log.info("setup plugin %s", name)
|
||||
if name in _all_plugins:
|
||||
raise ValueError(f"A plugin named {name} is already registered")
|
||||
_all_plugins[name] = self
|
||||
self.autogenerate_comparators = PriorityDispatcher()
|
||||
|
||||
def remove(self) -> None:
|
||||
"""remove this plugin"""
|
||||
|
||||
del _all_plugins[self.name]
|
||||
|
||||
def add_autogenerate_comparator(
|
||||
self,
|
||||
fn: Callable[..., PriorityDispatchResult],
|
||||
compare_target: str,
|
||||
compare_element: str | None = None,
|
||||
*,
|
||||
qualifier: str = "default",
|
||||
priority: DispatchPriority = DispatchPriority.MEDIUM,
|
||||
) -> None:
|
||||
"""Register an autogenerate comparison function.
|
||||
|
||||
See the section :ref:`plugins_registering_autogenerate` for detailed
|
||||
examples on how to use this method.
|
||||
|
||||
:param fn: The comparison function to register. The function receives
|
||||
arguments specific to the type of comparison being performed and
|
||||
should return a :class:`.PriorityDispatchResult` value.
|
||||
|
||||
:param compare_target: The type of comparison being performed
|
||||
(e.g., ``"table"``, ``"column"``, ``"type"``).
|
||||
|
||||
:param compare_element: Optional sub-element being compared within
|
||||
the target type.
|
||||
|
||||
:param qualifier: Database dialect qualifier. Use ``"default"`` for
|
||||
all dialects, or specify a dialect name like ``"postgresql"`` to
|
||||
register a dialect-specific handler. Defaults to ``"default"``.
|
||||
|
||||
:param priority: Execution priority for this comparison function.
|
||||
Functions are executed in priority order from
|
||||
:attr:`.DispatchPriority.FIRST` to :attr:`.DispatchPriority.LAST`.
|
||||
Defaults to :attr:`.DispatchPriority.MEDIUM`.
|
||||
|
||||
"""
|
||||
self.autogenerate_comparators.dispatch_for(
|
||||
compare_target,
|
||||
subgroup=compare_element,
|
||||
priority=priority,
|
||||
qualifier=qualifier,
|
||||
)(fn)
|
||||
|
||||
@classmethod
|
||||
def populate_autogenerate_priority_dispatch(
|
||||
cls, comparators: PriorityDispatcher, include_plugins: list[str]
|
||||
) -> None:
|
||||
"""Populate all current autogenerate comparison functions into
|
||||
a given PriorityDispatcher."""
|
||||
|
||||
exclude: set[Pattern[str]] = set()
|
||||
include: dict[str, Pattern[str]] = {}
|
||||
|
||||
matched_expressions: set[str] = set()
|
||||
|
||||
for name in include_plugins:
|
||||
if name.startswith("~"):
|
||||
exclude.add(_make_re(name[1:]))
|
||||
else:
|
||||
include[name] = _make_re(name)
|
||||
|
||||
for plugin in _all_plugins.values():
|
||||
if any(excl.match(plugin.name) for excl in exclude):
|
||||
continue
|
||||
|
||||
include_matches = [
|
||||
incl for incl in include if include[incl].match(plugin.name)
|
||||
]
|
||||
if not include_matches:
|
||||
continue
|
||||
else:
|
||||
matched_expressions.update(include_matches)
|
||||
|
||||
log.info("setting up autogenerate plugin %s", plugin.name)
|
||||
comparators.populate_with(plugin.autogenerate_comparators)
|
||||
|
||||
never_matched = set(include).difference(matched_expressions)
|
||||
if never_matched:
|
||||
raise util.CommandError(
|
||||
f"Did not locate plugins: {', '.join(never_matched)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setup_plugin_from_module(cls, module: ModuleType, name: str) -> None:
|
||||
"""Call the ``setup()`` function of a plugin module, identified by
|
||||
passing the module object itself.
|
||||
|
||||
E.g.::
|
||||
|
||||
from alembic.runtime.plugins import Plugin
|
||||
import myproject.alembic_plugin
|
||||
|
||||
# Register the plugin manually
|
||||
Plugin.setup_plugin_from_module(
|
||||
myproject.alembic_plugin,
|
||||
"myproject.custom_operations"
|
||||
)
|
||||
|
||||
This will generate a new :class:`.Plugin` object with the given
|
||||
name, which will register itself in the global list of plugins.
|
||||
Then the module's ``setup()`` function is invoked, passing that
|
||||
:class:`.Plugin` object.
|
||||
|
||||
This exact process is invoked automatically at import time for any
|
||||
plugin module that is published via the ``alembic.plugins`` entrypoint.
|
||||
|
||||
"""
|
||||
module.setup(Plugin(name))
|
||||
|
||||
|
||||
def _make_re(name: str) -> Pattern[str]:
|
||||
tokens = name.split(".")
|
||||
|
||||
reg = r""
|
||||
for token in tokens:
|
||||
if token == "*":
|
||||
reg += r"\..+?"
|
||||
elif token.isidentifier():
|
||||
reg += r"\." + token
|
||||
else:
|
||||
raise ValueError(f"Invalid plugin expression {name!r}")
|
||||
|
||||
# omit leading r'\.'
|
||||
return re.compile(f"^{reg[2:]}$")
|
||||
|
||||
|
||||
def _setup() -> None:
|
||||
# setup third party plugins
|
||||
for entrypoint in metadata.entry_points(group="alembic.plugins"):
|
||||
for mod in entrypoint.load():
|
||||
Plugin.setup_plugin_from_module(mod, entrypoint.name)
|
||||
|
||||
|
||||
_setup()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .base import Script
|
||||
from .base import ScriptDirectory
|
||||
|
||||
__all__ = ["ScriptDirectory", "Script"]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
|
||||
# mypy: no-warn-return-any, allow-any-generics
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .. import util
|
||||
from ..util import compat
|
||||
from ..util.pyfiles import _preserving_path_as_str
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..config import PostWriteHookConfig
|
||||
|
||||
REVISION_SCRIPT_TOKEN = "REVISION_SCRIPT_FILENAME"
|
||||
|
||||
_registry: dict = {}
|
||||
|
||||
|
||||
def register(name: str) -> Callable:
|
||||
"""A function decorator that will register that function as a write hook.
|
||||
|
||||
See the documentation linked below for an example.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`post_write_hooks_custom`
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def decorate(fn):
|
||||
_registry[name] = fn
|
||||
return fn
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def _invoke(
|
||||
name: str,
|
||||
revision_path: str | os.PathLike[str],
|
||||
options: PostWriteHookConfig,
|
||||
) -> Any:
|
||||
"""Invokes the formatter registered for the given name.
|
||||
|
||||
:param name: The name of a formatter in the registry
|
||||
:param revision: string path to the revision file
|
||||
:param options: A dict containing kwargs passed to the
|
||||
specified formatter.
|
||||
:raises: :class:`alembic.util.CommandError`
|
||||
"""
|
||||
revision_path = _preserving_path_as_str(revision_path)
|
||||
try:
|
||||
hook = _registry[name]
|
||||
except KeyError as ke:
|
||||
raise util.CommandError(
|
||||
f"No formatter with name '{name}' registered"
|
||||
) from ke
|
||||
else:
|
||||
return hook(revision_path, options)
|
||||
|
||||
|
||||
def _run_hooks(
|
||||
path: str | os.PathLike[str], hooks: list[PostWriteHookConfig]
|
||||
) -> None:
|
||||
"""Invoke hooks for a generated revision."""
|
||||
|
||||
for hook in hooks:
|
||||
name = hook["_hook_name"]
|
||||
try:
|
||||
type_ = hook["type"]
|
||||
except KeyError as ke:
|
||||
raise util.CommandError(
|
||||
f"Key '{name}.type' (or 'type' in toml) is required "
|
||||
f"for post write hook {name!r}"
|
||||
) from ke
|
||||
else:
|
||||
with util.status(
|
||||
f"Running post write hook {name!r}", newline=True
|
||||
):
|
||||
_invoke(type_, path, hook)
|
||||
|
||||
|
||||
def _parse_cmdline_options(cmdline_options_str: str, path: str) -> list[str]:
|
||||
"""Parse options from a string into a list.
|
||||
|
||||
Also substitutes the revision script token with the actual filename of
|
||||
the revision script.
|
||||
|
||||
If the revision script token doesn't occur in the options string, it is
|
||||
automatically prepended.
|
||||
"""
|
||||
if REVISION_SCRIPT_TOKEN not in cmdline_options_str:
|
||||
cmdline_options_str = REVISION_SCRIPT_TOKEN + " " + cmdline_options_str
|
||||
cmdline_options_list = shlex.split(
|
||||
cmdline_options_str, posix=compat.is_posix
|
||||
)
|
||||
cmdline_options_list = [
|
||||
option.replace(REVISION_SCRIPT_TOKEN, path)
|
||||
for option in cmdline_options_list
|
||||
]
|
||||
return cmdline_options_list
|
||||
|
||||
|
||||
def _get_required_option(options: dict, name: str) -> str:
|
||||
try:
|
||||
return options[name]
|
||||
except KeyError as ke:
|
||||
raise util.CommandError(
|
||||
f"Key {options['_hook_name']}.{name} is required for post "
|
||||
f"write hook {options['_hook_name']!r}"
|
||||
) from ke
|
||||
|
||||
|
||||
def _run_hook(
|
||||
path: str, options: dict, ignore_output: bool, command: list[str]
|
||||
) -> None:
|
||||
cwd: str | None = options.get("cwd", None)
|
||||
cmdline_options_str = options.get("options", "")
|
||||
cmdline_options_list = _parse_cmdline_options(cmdline_options_str, path)
|
||||
|
||||
kw: dict[str, Any] = {}
|
||||
if ignore_output:
|
||||
kw["stdout"] = kw["stderr"] = subprocess.DEVNULL
|
||||
|
||||
subprocess.run([*command, *cmdline_options_list], cwd=cwd, **kw)
|
||||
|
||||
|
||||
@register("console_scripts")
|
||||
def console_scripts(
|
||||
path: str,
|
||||
options: dict,
|
||||
ignore_output: bool = False,
|
||||
verify_version: tuple[int, ...] | None = None,
|
||||
) -> None:
|
||||
entrypoint_name = _get_required_option(options, "entrypoint")
|
||||
for entry in compat.importlib_metadata_get("console_scripts"):
|
||||
if entry.name == entrypoint_name:
|
||||
impl: Any = entry
|
||||
break
|
||||
else:
|
||||
raise util.CommandError(
|
||||
f"Could not find entrypoint console_scripts.{entrypoint_name}"
|
||||
)
|
||||
|
||||
if verify_version:
|
||||
pyscript = (
|
||||
f"import {impl.module}; "
|
||||
f"assert tuple(int(x) for x in {impl.module}.__version__.split('.')) >= {verify_version}, " # noqa: E501
|
||||
f"'need exactly version {verify_version} of {impl.name}'; "
|
||||
f"{impl.module}.{impl.attr}()"
|
||||
)
|
||||
else:
|
||||
pyscript = f"import {impl.module}; {impl.module}.{impl.attr}()"
|
||||
|
||||
command = [sys.executable, "-c", pyscript]
|
||||
_run_hook(path, options, ignore_output, command)
|
||||
|
||||
|
||||
@register("exec")
|
||||
def exec_(path: str, options: dict, ignore_output: bool = False) -> None:
|
||||
executable = _get_required_option(options, "executable")
|
||||
_run_hook(path, options, ignore_output, command=[executable])
|
||||
|
||||
|
||||
@register("module")
|
||||
def module(path: str, options: dict, ignore_output: bool = False) -> None:
|
||||
module_name = _get_required_option(options, "module")
|
||||
|
||||
if importlib.util.find_spec(module_name) is None:
|
||||
raise util.CommandError(f"Could not find module {module_name}")
|
||||
|
||||
command = [sys.executable, "-m", module_name]
|
||||
_run_hook(path, options, ignore_output, command)
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration with an async dbapi.
|
||||
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = ${script_location}
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1,89 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
"""In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode."""
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = ${script_location}
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1,78 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,12 @@
|
||||
Rudimentary multi-database configuration.
|
||||
|
||||
Multi-DB isn't vastly different from generic. The primary difference is that it
|
||||
will run the migrations N times (depending on how many databases you have
|
||||
configured), providing one engine name and associated context for each run.
|
||||
|
||||
That engine name will then allow the migration to restrict what runs within it to
|
||||
just the appropriate migrations for that engine. You can see this behavior within
|
||||
the mako template.
|
||||
|
||||
In the provided configuration, you'll need to have `databases` provided in
|
||||
alembic's config, and an `sqlalchemy.url` provided for each engine name.
|
||||
@@ -0,0 +1,157 @@
|
||||
# a multi-database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = ${script_location}
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# for multiple database configuration, new named sections are added
|
||||
# which each include a distinct ``sqlalchemy.url`` entry. A custom value
|
||||
# ``databases`` is added which indicates a listing of the per-database sections.
|
||||
# The ``databases`` entry as well as the URLs present in the ``[engine1]``
|
||||
# and ``[engine2]`` sections continue to be consumed by the user-maintained env.py
|
||||
# script only.
|
||||
|
||||
databases = engine1, engine2
|
||||
|
||||
[engine1]
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
[engine2]
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname2
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1,140 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
import re
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
USE_TWOPHASE = False
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger("alembic.env")
|
||||
|
||||
# gather section names referring to different
|
||||
# databases. These are named "engine1", "engine2"
|
||||
# in the sample .ini file.
|
||||
db_names = config.get_main_option("databases", "")
|
||||
|
||||
# add your model's MetaData objects here
|
||||
# for 'autogenerate' support. These must be set
|
||||
# up to hold just those tables targeting a
|
||||
# particular database. table.tometadata() may be
|
||||
# helpful here in case a "copy" of
|
||||
# a MetaData is needed.
|
||||
# from myapp import mymodel
|
||||
# target_metadata = {
|
||||
# 'engine1':mymodel.metadata1,
|
||||
# 'engine2':mymodel.metadata2
|
||||
# }
|
||||
target_metadata = {}
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
# for the --sql use case, run migrations for each URL into
|
||||
# individual files.
|
||||
|
||||
engines = {}
|
||||
for name in re.split(r",\s*", db_names):
|
||||
engines[name] = rec = {}
|
||||
rec["url"] = context.config.get_section_option(name, "sqlalchemy.url")
|
||||
|
||||
for name, rec in engines.items():
|
||||
logger.info("Migrating database %s" % name)
|
||||
file_ = "%s.sql" % name
|
||||
logger.info("Writing output to %s" % file_)
|
||||
with open(file_, "w") as buffer:
|
||||
context.configure(
|
||||
url=rec["url"],
|
||||
output_buffer=buffer,
|
||||
target_metadata=target_metadata.get(name),
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations(engine_name=name)
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# for the direct-to-DB use case, start a transaction on all
|
||||
# engines, then run all migrations, then commit all transactions.
|
||||
|
||||
engines = {}
|
||||
for name in re.split(r",\s*", db_names):
|
||||
engines[name] = rec = {}
|
||||
rec["engine"] = engine_from_config(
|
||||
context.config.get_section(name, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
for name, rec in engines.items():
|
||||
engine = rec["engine"]
|
||||
rec["connection"] = conn = engine.connect()
|
||||
|
||||
if USE_TWOPHASE:
|
||||
rec["transaction"] = conn.begin_twophase()
|
||||
else:
|
||||
rec["transaction"] = conn.begin()
|
||||
|
||||
try:
|
||||
for name, rec in engines.items():
|
||||
logger.info("Migrating database %s" % name)
|
||||
context.configure(
|
||||
connection=rec["connection"],
|
||||
upgrade_token="%s_upgrades" % name,
|
||||
downgrade_token="%s_downgrades" % name,
|
||||
target_metadata=target_metadata.get(name),
|
||||
)
|
||||
context.run_migrations(engine_name=name)
|
||||
|
||||
if USE_TWOPHASE:
|
||||
for rec in engines.values():
|
||||
rec["transaction"].prepare()
|
||||
|
||||
for rec in engines.values():
|
||||
rec["transaction"].commit()
|
||||
except:
|
||||
for rec in engines.values():
|
||||
rec["transaction"].rollback()
|
||||
raise
|
||||
finally:
|
||||
for rec in engines.values():
|
||||
rec["connection"].close()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,51 @@
|
||||
<%!
|
||||
import re
|
||||
|
||||
%>"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade(engine_name: str) -> None:
|
||||
"""Upgrade schema."""
|
||||
globals()["upgrade_%s" % engine_name]()
|
||||
|
||||
|
||||
def downgrade(engine_name: str) -> None:
|
||||
"""Downgrade schema."""
|
||||
globals()["downgrade_%s" % engine_name]()
|
||||
|
||||
<%
|
||||
db_names = config.get_main_option("databases")
|
||||
%>
|
||||
|
||||
## generate an "upgrade_<xyz>() / downgrade_<xyz>()" function
|
||||
## for each database name in the ini file.
|
||||
|
||||
% for db_name in re.split(r',\s*', db_names):
|
||||
|
||||
def upgrade_${db_name}() -> None:
|
||||
"""Upgrade ${db_name} schema."""
|
||||
${context.get("%s_upgrades" % db_name, "pass")}
|
||||
|
||||
|
||||
def downgrade_${db_name}() -> None:
|
||||
"""Downgrade ${db_name} schema."""
|
||||
${context.get("%s_downgrades" % db_name, "pass")}
|
||||
|
||||
% endfor
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user