t-sql 2.0.0__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- t_sql-2.1.1/.beads/tsql.db +0 -0
- t_sql-2.1.1/.idea/.gitignore +8 -0
- t_sql-2.1.1/.idea/inspectionProfiles/Project_Default.xml +24 -0
- t_sql-2.1.1/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- t_sql-2.1.1/.idea/misc.xml +11 -0
- t_sql-2.1.1/.idea/tsql.iml +10 -0
- t_sql-2.1.1/.idea/vcs.xml +6 -0
- t_sql-2.1.1/.idea/workspace.xml +124 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/PKG-INFO +2 -1
- {t_sql-2.0.0 → t_sql-2.1.1}/context7.json +10 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/pyproject.toml +4 -2
- t_sql-2.1.1/tests/test_alembic_integration.py +363 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_helper_functions.py +2 -8
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_injection_edge_cases.py +153 -1
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_mysql_integration.py +5 -10
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_query_builder.py +331 -7
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_sqlite_integration.py +6 -12
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_styles.py +24 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tsql/__init__.py +29 -7
- {t_sql-2.0.0 → t_sql-2.1.1}/tsql/query_builder.py +102 -13
- {t_sql-2.0.0 → t_sql-2.1.1}/.dockerignore +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/.github/workflows/publish.yml +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/.github/workflows/test.yml +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/.gitignore +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/Dockerfile +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/LICENSE +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/README.md +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/compose.yaml +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/pytest.ini +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_different_object_types.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_escaped.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tests/test_tsql.py +0 -0
- {t_sql-2.0.0 → t_sql-2.1.1}/tsql/styles.py +0 -0
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<component name="InspectionProjectProfileManager">
|
|
2
|
+
<profile version="1.0">
|
|
3
|
+
<option name="myName" value="Project Default" />
|
|
4
|
+
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
5
|
+
<inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
|
6
|
+
<option name="ourVersions">
|
|
7
|
+
<value>
|
|
8
|
+
<list size="3">
|
|
9
|
+
<item index="0" class="java.lang.String" itemvalue="3.11" />
|
|
10
|
+
<item index="1" class="java.lang.String" itemvalue="3.12" />
|
|
11
|
+
<item index="2" class="java.lang.String" itemvalue="3.13" />
|
|
12
|
+
</list>
|
|
13
|
+
</value>
|
|
14
|
+
</option>
|
|
15
|
+
</inspection_tool>
|
|
16
|
+
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
|
17
|
+
<option name="ignoredIdentifiers">
|
|
18
|
+
<list>
|
|
19
|
+
<option value="base64.binascii" />
|
|
20
|
+
</list>
|
|
21
|
+
</option>
|
|
22
|
+
</inspection_tool>
|
|
23
|
+
</profile>
|
|
24
|
+
</component>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="Black">
|
|
4
|
+
<option name="sdkName" value="uv (tsql)" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="KubernetesApiPersistence">{}</component>
|
|
7
|
+
<component name="KubernetesApiProvider"><![CDATA[{
|
|
8
|
+
"isMigrated": true
|
|
9
|
+
}]]></component>
|
|
10
|
+
<component name="ProjectRootManager" version="2" project-jdk-name="uv (tsql)" project-jdk-type="Python SDK" />
|
|
11
|
+
</project>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module version="4">
|
|
3
|
+
<component name="PyDocumentationSettings">
|
|
4
|
+
<option name="format" value="PLAIN" />
|
|
5
|
+
<option name="myDocStringFormat" value="Plain" />
|
|
6
|
+
</component>
|
|
7
|
+
<component name="TestRunnerService">
|
|
8
|
+
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
|
9
|
+
</component>
|
|
10
|
+
</module>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="059146b3-62bd-4ad4-890d-4356cf237b89" name="Changes" comment="">
|
|
8
|
+
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
|
9
|
+
<change beforePath="$PROJECT_DIR$/tests/test_mysql_integration.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_mysql_integration.py" afterDir="false" />
|
|
10
|
+
<change beforePath="$PROJECT_DIR$/tests/test_query_builder.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_query_builder.py" afterDir="false" />
|
|
11
|
+
<change beforePath="$PROJECT_DIR$/tests/test_sqlalchemy_integration.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_sqlalchemy_integration.py" afterDir="false" />
|
|
12
|
+
<change beforePath="$PROJECT_DIR$/tests/test_sqlite_integration.py" beforeDir="false" afterPath="$PROJECT_DIR$/tests/test_sqlite_integration.py" afterDir="false" />
|
|
13
|
+
<change beforePath="$PROJECT_DIR$/tsql/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/tsql/__init__.py" afterDir="false" />
|
|
14
|
+
<change beforePath="$PROJECT_DIR$/tsql/query_builder.py" beforeDir="false" afterPath="$PROJECT_DIR$/tsql/query_builder.py" afterDir="false" />
|
|
15
|
+
</list>
|
|
16
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
17
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
18
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
19
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
20
|
+
</component>
|
|
21
|
+
<component name="Git.Settings">
|
|
22
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
23
|
+
</component>
|
|
24
|
+
<component name="GitHubPullRequestSearchHistory">{
|
|
25
|
+
"lastFilter": {
|
|
26
|
+
"state": "OPEN",
|
|
27
|
+
"assignee": "nhumrich"
|
|
28
|
+
}
|
|
29
|
+
}</component>
|
|
30
|
+
<component name="GithubPullRequestsUISettings">{
|
|
31
|
+
"selectedUrlAndAccountId": {
|
|
32
|
+
"url": "git@github.com:nhumrich/tsql.git",
|
|
33
|
+
"accountId": "f308fc0d-c429-47fb-8e52-74bdc95408d8"
|
|
34
|
+
}
|
|
35
|
+
}</component>
|
|
36
|
+
<component name="ProjectColorInfo">{
|
|
37
|
+
"associatedIndex": 2
|
|
38
|
+
}</component>
|
|
39
|
+
<component name="ProjectId" id="33DagtaPqCq8RWQhigWGQcgOtLh" />
|
|
40
|
+
<component name="ProjectViewState">
|
|
41
|
+
<option name="compactDirectories" value="true" />
|
|
42
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
43
|
+
<option name="showLibraryContents" value="true" />
|
|
44
|
+
</component>
|
|
45
|
+
<component name="PropertiesComponent">{
|
|
46
|
+
"keyToString": {
|
|
47
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
48
|
+
"Python tests.pytest in test_sqlalchemy_integration.py.executor": "Debug",
|
|
49
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
50
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
51
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
52
|
+
"git-widget-placeholder": "main",
|
|
53
|
+
"junie.onboarding.icon.badge.shown": "true",
|
|
54
|
+
"last_opened_file_path": "/home/nhumrich/personal/tsql",
|
|
55
|
+
"node.js.detected.package.eslint": "true",
|
|
56
|
+
"node.js.detected.package.tslint": "true",
|
|
57
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
58
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
59
|
+
"nodejs_package_manager_path": "npm",
|
|
60
|
+
"to.speed.mode.migration.done": "true",
|
|
61
|
+
"vue.rearranger.settings.migration": "true"
|
|
62
|
+
}
|
|
63
|
+
}</component>
|
|
64
|
+
<component name="RunManager">
|
|
65
|
+
<configuration name="pytest in test_sqlalchemy_integration.py" type="tests" factoryName="py.test" temporary="true" nameIsGenerated="true">
|
|
66
|
+
<module name="tsql" />
|
|
67
|
+
<option name="ENV_FILES" value="" />
|
|
68
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
69
|
+
<option name="PARENT_ENVS" value="true" />
|
|
70
|
+
<option name="SDK_HOME" value="" />
|
|
71
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
72
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
73
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
74
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
75
|
+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
|
76
|
+
<option name="_new_keywords" value="""" />
|
|
77
|
+
<option name="_new_parameters" value="""" />
|
|
78
|
+
<option name="_new_additionalArguments" value="""" />
|
|
79
|
+
<option name="_new_target" value=""$PROJECT_DIR$/tests/test_sqlalchemy_integration.py"" />
|
|
80
|
+
<option name="_new_targetType" value=""PATH"" />
|
|
81
|
+
<method v="2" />
|
|
82
|
+
</configuration>
|
|
83
|
+
<recent_temporary>
|
|
84
|
+
<list>
|
|
85
|
+
<item itemvalue="Python tests.pytest in test_sqlalchemy_integration.py" />
|
|
86
|
+
</list>
|
|
87
|
+
</recent_temporary>
|
|
88
|
+
</component>
|
|
89
|
+
<component name="SharedIndexes">
|
|
90
|
+
<attachedChunks>
|
|
91
|
+
<set>
|
|
92
|
+
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-PY-252.26830.99" />
|
|
93
|
+
<option value="bundled-python-sdk-164cda30dcd9-0af03a5fa574-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.26830.99" />
|
|
94
|
+
</set>
|
|
95
|
+
</attachedChunks>
|
|
96
|
+
</component>
|
|
97
|
+
<component name="TaskManager">
|
|
98
|
+
<task active="true" id="Default" summary="Default task">
|
|
99
|
+
<changelist id="059146b3-62bd-4ad4-890d-4356cf237b89" name="Changes" comment="" />
|
|
100
|
+
<created>1758854212149</created>
|
|
101
|
+
<option name="number" value="Default" />
|
|
102
|
+
<option name="presentableId" value="Default" />
|
|
103
|
+
<updated>1758854212149</updated>
|
|
104
|
+
</task>
|
|
105
|
+
<servers />
|
|
106
|
+
</component>
|
|
107
|
+
<component name="TypeScriptGeneratedFilesManager">
|
|
108
|
+
<option name="version" value="3" />
|
|
109
|
+
</component>
|
|
110
|
+
<component name="Vcs.Log.Tabs.Properties">
|
|
111
|
+
<option name="TAB_STATES">
|
|
112
|
+
<map>
|
|
113
|
+
<entry key="MAIN">
|
|
114
|
+
<value>
|
|
115
|
+
<State />
|
|
116
|
+
</value>
|
|
117
|
+
</entry>
|
|
118
|
+
</map>
|
|
119
|
+
</option>
|
|
120
|
+
</component>
|
|
121
|
+
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
|
122
|
+
<SUITE FILE_PATH="coverage/tsql$pytest_in_test_sqlalchemy_integration_py.coverage" NAME="pytest in test_sqlalchemy_integration.py Coverage Results" MODIFIED="1760138305802" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
|
123
|
+
</component>
|
|
124
|
+
</project>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: t-sql
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
4
4
|
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
5
|
Project-URL: Homepage, https://github.com/nhumrich/t-sql
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.14
|
|
8
|
+
Requires-Dist: alembic>=1.17.0
|
|
8
9
|
Description-Content-Type: text/markdown
|
|
9
10
|
|
|
10
11
|
# t-sql
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://context7.com/schema/context7.json",
|
|
3
|
+
"projectTitle": "t-sql",
|
|
4
|
+
"description": "t-sql is a lightweight SQL templating library for Python 3.14+ that leverages t-strings to safely build SQL queries, preventing injection attacks, and includes a query builder and SQLAlchemy integration.",
|
|
5
|
+
"branch": "main",
|
|
2
6
|
"excludeFolders": [
|
|
3
7
|
"tests",
|
|
4
8
|
".github",
|
|
@@ -22,5 +26,11 @@
|
|
|
22
26
|
"When mixing query builder with t-strings using .where(), t-string conditions are automatically wrapped in parentheses for proper operator precedence.",
|
|
23
27
|
"The @table decorator returns an instance - use the decorated class directly, don't instantiate it.",
|
|
24
28
|
"Default parameter style is QMARK (?). Use tsql.styles.NUMERIC_DOLLAR for PostgreSQL ($1, $2), or other styles as needed."
|
|
29
|
+
],
|
|
30
|
+
"previousVersions": [
|
|
31
|
+
{
|
|
32
|
+
"tag": "v1_2_1",
|
|
33
|
+
"title": "v1.2.1"
|
|
34
|
+
}
|
|
25
35
|
]
|
|
26
36
|
}
|
|
@@ -4,11 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "t-sql"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.1.1"
|
|
8
8
|
description = "Safe SQL. SQL queries for python t-strings (PEP 750)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
11
|
-
dependencies = [
|
|
11
|
+
dependencies = [
|
|
12
|
+
"alembic>=1.17.0",
|
|
13
|
+
]
|
|
12
14
|
|
|
13
15
|
[project.urls]
|
|
14
16
|
Homepage = "https://github.com/nhumrich/t-sql"
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from textwrap import dedent
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from sqlalchemy import MetaData, Column, String, Integer, Boolean, ForeignKey, TIMESTAMP, create_engine
|
|
9
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
10
|
+
from alembic.config import Config
|
|
11
|
+
from alembic.script import ScriptDirectory
|
|
12
|
+
from alembic.runtime.migration import MigrationContext
|
|
13
|
+
from alembic.autogenerate import compare_metadata
|
|
14
|
+
|
|
15
|
+
from tsql.query_builder import Table
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def temp_alembic_env():
|
|
20
|
+
temp_dir = tempfile.mkdtemp()
|
|
21
|
+
alembic_dir = Path(temp_dir) / "alembic"
|
|
22
|
+
alembic_dir.mkdir()
|
|
23
|
+
versions_dir = alembic_dir / "versions"
|
|
24
|
+
versions_dir.mkdir()
|
|
25
|
+
|
|
26
|
+
env_py = alembic_dir / "env.py"
|
|
27
|
+
env_py.write_text(dedent("""
|
|
28
|
+
from alembic import context
|
|
29
|
+
from sqlalchemy import engine_from_config, pool
|
|
30
|
+
|
|
31
|
+
config = context.config
|
|
32
|
+
target_metadata = config.attributes.get('target_metadata', None)
|
|
33
|
+
|
|
34
|
+
def run_migrations_offline():
|
|
35
|
+
context.configure(
|
|
36
|
+
url=config.get_main_option("sqlalchemy.url"),
|
|
37
|
+
target_metadata=target_metadata,
|
|
38
|
+
literal_binds=True,
|
|
39
|
+
dialect_opts={"paramstyle": "named"},
|
|
40
|
+
)
|
|
41
|
+
with context.begin_transaction():
|
|
42
|
+
context.run_migrations()
|
|
43
|
+
|
|
44
|
+
def run_migrations_online():
|
|
45
|
+
connectable = config.attributes.get('connection', None)
|
|
46
|
+
if connectable is None:
|
|
47
|
+
connectable = engine_from_config(
|
|
48
|
+
config.get_section(config.config_ini_section),
|
|
49
|
+
prefix="sqlalchemy.",
|
|
50
|
+
poolclass=pool.NullPool,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
with connectable.connect() as connection:
|
|
54
|
+
context.configure(
|
|
55
|
+
connection=connection,
|
|
56
|
+
target_metadata=target_metadata
|
|
57
|
+
)
|
|
58
|
+
with context.begin_transaction():
|
|
59
|
+
context.run_migrations()
|
|
60
|
+
|
|
61
|
+
if context.is_offline_mode():
|
|
62
|
+
run_migrations_offline()
|
|
63
|
+
else:
|
|
64
|
+
run_migrations_online()
|
|
65
|
+
"""))
|
|
66
|
+
|
|
67
|
+
script_py = alembic_dir / "script.py.mako"
|
|
68
|
+
script_py.write_text(dedent('''
|
|
69
|
+
"""${message}"""
|
|
70
|
+
from alembic import op
|
|
71
|
+
import sqlalchemy as sa
|
|
72
|
+
${imports if imports else ""}
|
|
73
|
+
|
|
74
|
+
revision = ${repr(up_revision)}
|
|
75
|
+
down_revision = ${repr(down_revision)}
|
|
76
|
+
branch_labels = ${repr(branch_labels)}
|
|
77
|
+
depends_on = ${repr(depends_on)}
|
|
78
|
+
|
|
79
|
+
def upgrade():
|
|
80
|
+
${upgrades if upgrades else "pass"}
|
|
81
|
+
|
|
82
|
+
def downgrade():
|
|
83
|
+
${downgrades if downgrades else "pass"}
|
|
84
|
+
'''))
|
|
85
|
+
|
|
86
|
+
alembic_ini = Path(temp_dir) / "alembic.ini"
|
|
87
|
+
alembic_ini.write_text(dedent(f"""
|
|
88
|
+
[alembic]
|
|
89
|
+
script_location = {alembic_dir}
|
|
90
|
+
sqlalchemy.url = sqlite:///:memory:
|
|
91
|
+
|
|
92
|
+
[loggers]
|
|
93
|
+
keys = root
|
|
94
|
+
|
|
95
|
+
[handlers]
|
|
96
|
+
keys = console
|
|
97
|
+
|
|
98
|
+
[formatters]
|
|
99
|
+
keys = generic
|
|
100
|
+
|
|
101
|
+
[logger_root]
|
|
102
|
+
level = WARN
|
|
103
|
+
handlers = console
|
|
104
|
+
|
|
105
|
+
[handler_console]
|
|
106
|
+
class = StreamHandler
|
|
107
|
+
args = (sys.stderr,)
|
|
108
|
+
level = NOTSET
|
|
109
|
+
formatter = generic
|
|
110
|
+
|
|
111
|
+
[formatter_generic]
|
|
112
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
113
|
+
"""))
|
|
114
|
+
|
|
115
|
+
yield temp_dir, alembic_ini
|
|
116
|
+
|
|
117
|
+
shutil.rmtree(temp_dir)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_alembic_detects_new_table_with_annotations(temp_alembic_env):
|
|
121
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
122
|
+
metadata = MetaData()
|
|
123
|
+
engine = create_engine("sqlite:///:memory:")
|
|
124
|
+
|
|
125
|
+
class Users(Table, table_name='users', metadata=metadata):
|
|
126
|
+
id: int
|
|
127
|
+
name: str
|
|
128
|
+
email: str
|
|
129
|
+
age: int
|
|
130
|
+
|
|
131
|
+
cfg = Config(str(alembic_ini))
|
|
132
|
+
cfg.attributes['target_metadata'] = metadata
|
|
133
|
+
cfg.attributes['connection'] = engine
|
|
134
|
+
|
|
135
|
+
with engine.begin() as connection:
|
|
136
|
+
mc = MigrationContext.configure(connection)
|
|
137
|
+
diff = compare_metadata(mc, metadata)
|
|
138
|
+
|
|
139
|
+
assert len(diff) > 0
|
|
140
|
+
|
|
141
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
142
|
+
assert len(add_table_ops) == 1
|
|
143
|
+
|
|
144
|
+
table = add_table_ops[0][1]
|
|
145
|
+
assert table.name == 'users'
|
|
146
|
+
assert 'id' in [c.name for c in table.columns]
|
|
147
|
+
assert 'name' in [c.name for c in table.columns]
|
|
148
|
+
assert 'email' in [c.name for c in table.columns]
|
|
149
|
+
assert 'age' in [c.name for c in table.columns]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_alembic_detects_new_table_with_sa_columns(temp_alembic_env):
|
|
153
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
154
|
+
metadata = MetaData()
|
|
155
|
+
engine = create_engine("sqlite:///:memory:")
|
|
156
|
+
|
|
157
|
+
class Posts(Table, table_name='posts', metadata=metadata):
|
|
158
|
+
id = Column(String, primary_key=True)
|
|
159
|
+
title = Column(String(255), nullable=False)
|
|
160
|
+
content = Column(String)
|
|
161
|
+
published = Column(Boolean, server_default='false')
|
|
162
|
+
|
|
163
|
+
cfg = Config(str(alembic_ini))
|
|
164
|
+
cfg.attributes['target_metadata'] = metadata
|
|
165
|
+
cfg.attributes['connection'] = engine
|
|
166
|
+
|
|
167
|
+
with engine.begin() as connection:
|
|
168
|
+
mc = MigrationContext.configure(connection)
|
|
169
|
+
diff = compare_metadata(mc, metadata)
|
|
170
|
+
|
|
171
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
172
|
+
assert len(add_table_ops) == 1
|
|
173
|
+
|
|
174
|
+
table = add_table_ops[0][1]
|
|
175
|
+
assert table.name == 'posts'
|
|
176
|
+
|
|
177
|
+
id_col = next(c for c in table.columns if c.name == 'id')
|
|
178
|
+
assert id_col.primary_key
|
|
179
|
+
|
|
180
|
+
title_col = next(c for c in table.columns if c.name == 'title')
|
|
181
|
+
assert not title_col.nullable
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_alembic_detects_mixed_table_definition(temp_alembic_env):
|
|
185
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
186
|
+
metadata = MetaData()
|
|
187
|
+
engine = create_engine("sqlite:///:memory:")
|
|
188
|
+
|
|
189
|
+
class Users(Table, table_name='users', metadata=metadata):
|
|
190
|
+
id = Column(String, primary_key=True)
|
|
191
|
+
|
|
192
|
+
class Comments(Table, table_name='comments', metadata=metadata):
|
|
193
|
+
id = Column(String, primary_key=True)
|
|
194
|
+
post_id: str
|
|
195
|
+
user_id = Column(String, ForeignKey('users.id'))
|
|
196
|
+
content: str
|
|
197
|
+
upvotes: int
|
|
198
|
+
created_at = Column(TIMESTAMP, nullable=False)
|
|
199
|
+
|
|
200
|
+
cfg = Config(str(alembic_ini))
|
|
201
|
+
cfg.attributes['target_metadata'] = metadata
|
|
202
|
+
cfg.attributes['connection'] = engine
|
|
203
|
+
|
|
204
|
+
with engine.begin() as connection:
|
|
205
|
+
mc = MigrationContext.configure(connection)
|
|
206
|
+
diff = compare_metadata(mc, metadata)
|
|
207
|
+
|
|
208
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
209
|
+
assert len(add_table_ops) == 2
|
|
210
|
+
|
|
211
|
+
table = next(op[1] for op in add_table_ops if op[1].name == 'comments')
|
|
212
|
+
assert table.name == 'comments'
|
|
213
|
+
|
|
214
|
+
column_names = [c.name for c in table.columns]
|
|
215
|
+
assert 'id' in column_names
|
|
216
|
+
assert 'post_id' in column_names
|
|
217
|
+
assert 'user_id' in column_names
|
|
218
|
+
assert 'content' in column_names
|
|
219
|
+
assert 'upvotes' in column_names
|
|
220
|
+
assert 'created_at' in column_names
|
|
221
|
+
|
|
222
|
+
user_id_col = next(c for c in table.columns if c.name == 'user_id')
|
|
223
|
+
assert len(list(user_id_col.foreign_keys)) == 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_alembic_detects_column_additions(temp_alembic_env):
|
|
227
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
228
|
+
metadata = MetaData()
|
|
229
|
+
engine = create_engine("sqlite:///:memory:")
|
|
230
|
+
|
|
231
|
+
with engine.begin() as connection:
|
|
232
|
+
connection.execute(sa_text("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"))
|
|
233
|
+
|
|
234
|
+
class Users(Table, table_name='users', metadata=metadata):
|
|
235
|
+
id: int
|
|
236
|
+
name: str
|
|
237
|
+
email: str
|
|
238
|
+
age: int
|
|
239
|
+
|
|
240
|
+
cfg = Config(str(alembic_ini))
|
|
241
|
+
cfg.attributes['target_metadata'] = metadata
|
|
242
|
+
cfg.attributes['connection'] = engine
|
|
243
|
+
|
|
244
|
+
with engine.begin() as connection:
|
|
245
|
+
mc = MigrationContext.configure(connection)
|
|
246
|
+
diff = compare_metadata(mc, metadata)
|
|
247
|
+
|
|
248
|
+
add_column_ops = [op for op in diff if op[0] == 'add_column']
|
|
249
|
+
assert len(add_column_ops) == 2
|
|
250
|
+
|
|
251
|
+
added_column_names = [op[3].name for op in add_column_ops]
|
|
252
|
+
assert 'email' in added_column_names
|
|
253
|
+
assert 'age' in added_column_names
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_alembic_detects_column_removals(temp_alembic_env):
|
|
257
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
258
|
+
metadata = MetaData()
|
|
259
|
+
engine = create_engine("sqlite:///:memory:")
|
|
260
|
+
|
|
261
|
+
with engine.begin() as connection:
|
|
262
|
+
connection.execute(sa_text("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, old_field TEXT, deprecated TEXT)"))
|
|
263
|
+
|
|
264
|
+
class Users(Table, table_name='users', metadata=metadata):
|
|
265
|
+
id: int
|
|
266
|
+
name: str
|
|
267
|
+
|
|
268
|
+
cfg = Config(str(alembic_ini))
|
|
269
|
+
cfg.attributes['target_metadata'] = metadata
|
|
270
|
+
cfg.attributes['connection'] = engine
|
|
271
|
+
|
|
272
|
+
with engine.begin() as connection:
|
|
273
|
+
mc = MigrationContext.configure(connection)
|
|
274
|
+
diff = compare_metadata(mc, metadata)
|
|
275
|
+
|
|
276
|
+
remove_column_ops = [op for op in diff if op[0] == 'remove_column']
|
|
277
|
+
assert len(remove_column_ops) == 2
|
|
278
|
+
|
|
279
|
+
removed_column_names = [op[3].name for op in remove_column_ops]
|
|
280
|
+
assert 'old_field' in removed_column_names
|
|
281
|
+
assert 'deprecated' in removed_column_names
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_alembic_no_spurious_changes_for_identical_schema(temp_alembic_env):
|
|
285
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
286
|
+
metadata = MetaData()
|
|
287
|
+
engine = create_engine("sqlite:///:memory:")
|
|
288
|
+
|
|
289
|
+
class Users(Table, table_name='users', metadata=metadata):
|
|
290
|
+
id = Column(Integer, primary_key=True)
|
|
291
|
+
name = Column(String)
|
|
292
|
+
email = Column(String)
|
|
293
|
+
|
|
294
|
+
metadata.create_all(engine)
|
|
295
|
+
|
|
296
|
+
cfg = Config(str(alembic_ini))
|
|
297
|
+
cfg.attributes['target_metadata'] = metadata
|
|
298
|
+
cfg.attributes['connection'] = engine
|
|
299
|
+
|
|
300
|
+
with engine.begin() as connection:
|
|
301
|
+
mc = MigrationContext.configure(connection)
|
|
302
|
+
diff = compare_metadata(mc, metadata)
|
|
303
|
+
|
|
304
|
+
assert len(diff) == 0
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_alembic_handles_foreign_keys_correctly(temp_alembic_env):
|
|
308
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
309
|
+
metadata = MetaData()
|
|
310
|
+
engine = create_engine("sqlite:///:memory:")
|
|
311
|
+
|
|
312
|
+
class Users(Table, table_name='users', metadata=metadata):
|
|
313
|
+
id = Column(String, primary_key=True)
|
|
314
|
+
|
|
315
|
+
class Posts(Table, table_name='posts', metadata=metadata):
|
|
316
|
+
id = Column(String, primary_key=True)
|
|
317
|
+
user_id = Column(String, ForeignKey('users.id', ondelete='CASCADE'))
|
|
318
|
+
title: str
|
|
319
|
+
|
|
320
|
+
cfg = Config(str(alembic_ini))
|
|
321
|
+
cfg.attributes['target_metadata'] = metadata
|
|
322
|
+
cfg.attributes['connection'] = engine
|
|
323
|
+
|
|
324
|
+
with engine.begin() as connection:
|
|
325
|
+
mc = MigrationContext.configure(connection)
|
|
326
|
+
diff = compare_metadata(mc, metadata)
|
|
327
|
+
|
|
328
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
329
|
+
assert len(add_table_ops) == 2
|
|
330
|
+
|
|
331
|
+
posts_table = next(op[1] for op in add_table_ops if op[1].name == 'posts')
|
|
332
|
+
user_id_col = next(c for c in posts_table.columns if c.name == 'user_id')
|
|
333
|
+
|
|
334
|
+
fks = list(user_id_col.foreign_keys)
|
|
335
|
+
assert len(fks) == 1
|
|
336
|
+
assert fks[0].column.table.name == 'users'
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_alembic_with_schema_parameter(temp_alembic_env):
|
|
340
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
341
|
+
metadata = MetaData()
|
|
342
|
+
engine = create_engine("sqlite:///:memory:")
|
|
343
|
+
|
|
344
|
+
class Users(Table, table_name='users', metadata=metadata, schema='public'):
|
|
345
|
+
id: int
|
|
346
|
+
name: str
|
|
347
|
+
|
|
348
|
+
cfg = Config(str(alembic_ini))
|
|
349
|
+
cfg.attributes['target_metadata'] = metadata
|
|
350
|
+
cfg.attributes['connection'] = engine
|
|
351
|
+
|
|
352
|
+
with engine.begin() as connection:
|
|
353
|
+
mc = MigrationContext.configure(connection)
|
|
354
|
+
diff = compare_metadata(mc, metadata)
|
|
355
|
+
|
|
356
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
357
|
+
assert len(add_table_ops) == 1
|
|
358
|
+
|
|
359
|
+
table = add_table_ops[0][1]
|
|
360
|
+
assert table.schema == 'public'
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
from sqlalchemy import text as sa_text
|
|
@@ -47,15 +47,9 @@ def test_as_set():
|
|
|
47
47
|
|
|
48
48
|
def test_insert():
|
|
49
49
|
"""Test the insert helper function"""
|
|
50
|
-
|
|
51
|
-
'name': 'Alice',
|
|
52
|
-
'age': 25,
|
|
53
|
-
'active': True
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
query = tsql.insert('users', values)
|
|
50
|
+
query = tsql.insert('users', name='Alice', age=25, active=True)
|
|
57
51
|
result = tsql.render(query)
|
|
58
|
-
|
|
52
|
+
|
|
59
53
|
assert "INSERT INTO users" in result[0]
|
|
60
54
|
assert "name" in result[0] and "age" in result[0] and "active" in result[0]
|
|
61
55
|
assert "VALUES" in result[0]
|