t-sql 1.2.0__tar.gz → 1.2.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-1.2.0 → t_sql-1.2.1}/.github/workflows/test.yml +17 -0
- t_sql-1.2.1/.idea/.gitignore +8 -0
- t_sql-1.2.1/.idea/inspectionProfiles/Project_Default.xml +24 -0
- t_sql-1.2.1/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- t_sql-1.2.1/.idea/misc.xml +11 -0
- t_sql-1.2.1/.idea/tsql.iml +10 -0
- t_sql-1.2.1/.idea/vcs.xml +6 -0
- t_sql-1.2.1/.idea/workspace.xml +118 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/PKG-INFO +68 -2
- {t_sql-1.2.0 → t_sql-1.2.1}/README.md +67 -1
- {t_sql-1.2.0 → t_sql-1.2.1}/pyproject.toml +1 -1
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_asyncpg_integration.py +2 -1
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_injection_protection_validation.py +2 -1
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_injections_for_escaped.py +2 -1
- {t_sql-1.2.0 → t_sql-1.2.1}/.dockerignore +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/.github/workflows/publish.yml +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/.gitignore +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/Dockerfile +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/LICENSE +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/compose.yaml +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/context7.json +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/pytest.ini +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_different_object_types.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_escaped.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_helper_functions.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_query_builder.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_sqlalchemy_integration.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_styles.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tests/test_tsql.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tsql/__init__.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tsql/query_builder.py +0 -0
- {t_sql-1.2.0 → t_sql-1.2.1}/tsql/styles.py +0 -0
|
@@ -13,6 +13,21 @@ jobs:
|
|
|
13
13
|
matrix:
|
|
14
14
|
python-version: ["3.14"]
|
|
15
15
|
|
|
16
|
+
services:
|
|
17
|
+
postgres:
|
|
18
|
+
image: postgres:18
|
|
19
|
+
env:
|
|
20
|
+
POSTGRES_PASSWORD: password
|
|
21
|
+
POSTGRES_USER: postgres
|
|
22
|
+
POSTGRES_DB: postgres
|
|
23
|
+
ports:
|
|
24
|
+
- 5432:5432
|
|
25
|
+
options: >-
|
|
26
|
+
--health-cmd pg_isready
|
|
27
|
+
--health-interval 10s
|
|
28
|
+
--health-timeout 5s
|
|
29
|
+
--health-retries 5
|
|
30
|
+
|
|
16
31
|
steps:
|
|
17
32
|
- uses: actions/checkout@v4
|
|
18
33
|
|
|
@@ -28,4 +43,6 @@ jobs:
|
|
|
28
43
|
run: uv sync
|
|
29
44
|
|
|
30
45
|
- name: Run tests
|
|
46
|
+
env:
|
|
47
|
+
DATABASE_URL: postgresql://postgres:password@localhost:5432/postgres
|
|
31
48
|
run: uv run pytest -v
|
|
@@ -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,118 @@
|
|
|
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$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
|
9
|
+
</list>
|
|
10
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
11
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
12
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
13
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
14
|
+
</component>
|
|
15
|
+
<component name="Git.Settings">
|
|
16
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
17
|
+
</component>
|
|
18
|
+
<component name="GitHubPullRequestSearchHistory">{
|
|
19
|
+
"lastFilter": {
|
|
20
|
+
"state": "OPEN",
|
|
21
|
+
"assignee": "nhumrich"
|
|
22
|
+
}
|
|
23
|
+
}</component>
|
|
24
|
+
<component name="GithubPullRequestsUISettings">{
|
|
25
|
+
"selectedUrlAndAccountId": {
|
|
26
|
+
"url": "git@github.com:nhumrich/tsql.git",
|
|
27
|
+
"accountId": "f308fc0d-c429-47fb-8e52-74bdc95408d8"
|
|
28
|
+
}
|
|
29
|
+
}</component>
|
|
30
|
+
<component name="ProjectColorInfo">{
|
|
31
|
+
"associatedIndex": 2
|
|
32
|
+
}</component>
|
|
33
|
+
<component name="ProjectId" id="33DagtaPqCq8RWQhigWGQcgOtLh" />
|
|
34
|
+
<component name="ProjectViewState">
|
|
35
|
+
<option name="compactDirectories" value="true" />
|
|
36
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
37
|
+
<option name="showLibraryContents" value="true" />
|
|
38
|
+
</component>
|
|
39
|
+
<component name="PropertiesComponent">{
|
|
40
|
+
"keyToString": {
|
|
41
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
42
|
+
"Python tests.pytest in test_sqlalchemy_integration.py.executor": "Debug",
|
|
43
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
44
|
+
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
|
45
|
+
"RunOnceActivity.git.unshallow": "true",
|
|
46
|
+
"git-widget-placeholder": "main",
|
|
47
|
+
"junie.onboarding.icon.badge.shown": "true",
|
|
48
|
+
"last_opened_file_path": "/home/nhumrich/personal/tsql",
|
|
49
|
+
"node.js.detected.package.eslint": "true",
|
|
50
|
+
"node.js.detected.package.tslint": "true",
|
|
51
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
52
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
53
|
+
"nodejs_package_manager_path": "npm",
|
|
54
|
+
"to.speed.mode.migration.done": "true",
|
|
55
|
+
"vue.rearranger.settings.migration": "true"
|
|
56
|
+
}
|
|
57
|
+
}</component>
|
|
58
|
+
<component name="RunManager">
|
|
59
|
+
<configuration name="pytest in test_sqlalchemy_integration.py" type="tests" factoryName="py.test" temporary="true" nameIsGenerated="true">
|
|
60
|
+
<module name="tsql" />
|
|
61
|
+
<option name="ENV_FILES" value="" />
|
|
62
|
+
<option name="INTERPRETER_OPTIONS" value="" />
|
|
63
|
+
<option name="PARENT_ENVS" value="true" />
|
|
64
|
+
<option name="SDK_HOME" value="" />
|
|
65
|
+
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
66
|
+
<option name="IS_MODULE_SDK" value="true" />
|
|
67
|
+
<option name="ADD_CONTENT_ROOTS" value="true" />
|
|
68
|
+
<option name="ADD_SOURCE_ROOTS" value="true" />
|
|
69
|
+
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
|
70
|
+
<option name="_new_keywords" value="""" />
|
|
71
|
+
<option name="_new_parameters" value="""" />
|
|
72
|
+
<option name="_new_additionalArguments" value="""" />
|
|
73
|
+
<option name="_new_target" value=""$PROJECT_DIR$/tests/test_sqlalchemy_integration.py"" />
|
|
74
|
+
<option name="_new_targetType" value=""PATH"" />
|
|
75
|
+
<method v="2" />
|
|
76
|
+
</configuration>
|
|
77
|
+
<recent_temporary>
|
|
78
|
+
<list>
|
|
79
|
+
<item itemvalue="Python tests.pytest in test_sqlalchemy_integration.py" />
|
|
80
|
+
</list>
|
|
81
|
+
</recent_temporary>
|
|
82
|
+
</component>
|
|
83
|
+
<component name="SharedIndexes">
|
|
84
|
+
<attachedChunks>
|
|
85
|
+
<set>
|
|
86
|
+
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-PY-252.26830.99" />
|
|
87
|
+
<option value="bundled-python-sdk-164cda30dcd9-0af03a5fa574-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.26830.99" />
|
|
88
|
+
</set>
|
|
89
|
+
</attachedChunks>
|
|
90
|
+
</component>
|
|
91
|
+
<component name="TaskManager">
|
|
92
|
+
<task active="true" id="Default" summary="Default task">
|
|
93
|
+
<changelist id="059146b3-62bd-4ad4-890d-4356cf237b89" name="Changes" comment="" />
|
|
94
|
+
<created>1758854212149</created>
|
|
95
|
+
<option name="number" value="Default" />
|
|
96
|
+
<option name="presentableId" value="Default" />
|
|
97
|
+
<updated>1758854212149</updated>
|
|
98
|
+
</task>
|
|
99
|
+
<servers />
|
|
100
|
+
</component>
|
|
101
|
+
<component name="TypeScriptGeneratedFilesManager">
|
|
102
|
+
<option name="version" value="3" />
|
|
103
|
+
</component>
|
|
104
|
+
<component name="Vcs.Log.Tabs.Properties">
|
|
105
|
+
<option name="TAB_STATES">
|
|
106
|
+
<map>
|
|
107
|
+
<entry key="MAIN">
|
|
108
|
+
<value>
|
|
109
|
+
<State />
|
|
110
|
+
</value>
|
|
111
|
+
</entry>
|
|
112
|
+
</map>
|
|
113
|
+
</option>
|
|
114
|
+
</component>
|
|
115
|
+
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
|
116
|
+
<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$" />
|
|
117
|
+
</component>
|
|
118
|
+
</project>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: t-sql
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
5
|
Project-URL: Homepage, https://github.com/nhumrich/tsql
|
|
6
6
|
License-File: LICENSE
|
|
@@ -154,11 +154,29 @@ values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
|
|
|
154
154
|
tsql.insert(table, values)
|
|
155
155
|
# INSERT INTO users (id, name, email) VALUES ('abc123', 'bob', 'bob@example.com')
|
|
156
156
|
|
|
157
|
+
# insert with ignore_conflict
|
|
158
|
+
tsql.insert(table, values, ignore_conflict=True)
|
|
159
|
+
# INSERT INTO users (id, name, email) VALUES ('abc123', 'bob', 'bob@example.com') ON CONFLICT DO NOTHING RETURNING *
|
|
160
|
+
|
|
161
|
+
# upsert (insert or update on conflict)
|
|
162
|
+
values = {'id': 'abc123', 'name': 'joe', 'email': 'joe@example.com'}
|
|
163
|
+
tsql.upsert(table, values, conflict_on='id')
|
|
164
|
+
# INSERT INTO users (id, name, email) VALUES ('abc123', 'joe', 'joe@example.com')
|
|
165
|
+
# ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email RETURNING *
|
|
166
|
+
|
|
167
|
+
# upsert with multiple conflict columns
|
|
168
|
+
tsql.upsert(table, values, conflict_on=['email', 'name'])
|
|
169
|
+
# ON CONFLICT (email, name) DO UPDATE SET ...
|
|
170
|
+
|
|
157
171
|
# update values on a single row
|
|
158
172
|
table = 'users'
|
|
159
173
|
values = {'name': 'joe', 'email': 'joe@example.com'}
|
|
160
174
|
tsql.update(table, values, id='abc123')
|
|
161
|
-
# UPDATE users SET name='joe', email='joe@example.com' WHERE id='abc123'
|
|
175
|
+
# UPDATE users SET name='joe', email='joe@example.com' WHERE id='abc123' RETURNING *
|
|
176
|
+
|
|
177
|
+
# delete a single row
|
|
178
|
+
tsql.delete(table, id='abc123')
|
|
179
|
+
# DELETE FROM users WHERE id = 'abc123'
|
|
162
180
|
```
|
|
163
181
|
|
|
164
182
|
# Query Builder
|
|
@@ -251,6 +269,54 @@ query = (Posts.select(Posts.title, Users.username)
|
|
|
251
269
|
.limit(20))
|
|
252
270
|
```
|
|
253
271
|
|
|
272
|
+
## Write Operations
|
|
273
|
+
|
|
274
|
+
The query builder supports INSERT, UPDATE, UPSERT, and DELETE operations:
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
# INSERT
|
|
278
|
+
values = {'id': 'abc123', 'username': 'john', 'email': 'john@example.com'}
|
|
279
|
+
query = Users.insert(values)
|
|
280
|
+
sql, params = query.render()
|
|
281
|
+
# INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *
|
|
282
|
+
|
|
283
|
+
# INSERT with conflict handling (ignore)
|
|
284
|
+
query = Users.insert(values, ignore_conflict=True)
|
|
285
|
+
sql, params = query.render()
|
|
286
|
+
# INSERT INTO users (id, username, email) VALUES (?, ?, ?) ON CONFLICT DO NOTHING RETURNING *
|
|
287
|
+
|
|
288
|
+
# UPSERT (INSERT ... ON CONFLICT DO UPDATE)
|
|
289
|
+
values = {'id': 'abc123', 'username': 'john_updated', 'email': 'john@example.com'}
|
|
290
|
+
query = Users.upsert(values, conflict_on='id')
|
|
291
|
+
sql, params = query.render()
|
|
292
|
+
# INSERT INTO users (id, username, email) VALUES (?, ?, ?)
|
|
293
|
+
# ON CONFLICT (id) DO UPDATE SET username=EXCLUDED.username, email=EXCLUDED.email RETURNING *
|
|
294
|
+
|
|
295
|
+
# UPSERT with multiple conflict columns
|
|
296
|
+
query = Users.upsert(values, conflict_on=['email', 'username'])
|
|
297
|
+
# Can also use Column objects: conflict_on=Users.id or conflict_on=[Users.email, Users.username]
|
|
298
|
+
|
|
299
|
+
# UPDATE with WHERE conditions
|
|
300
|
+
query = Users.update({'email': 'newemail@example.com'}).where(Users.id == 'abc123')
|
|
301
|
+
sql, params = query.render()
|
|
302
|
+
# UPDATE users SET email=? WHERE users.id = ? RETURNING *
|
|
303
|
+
|
|
304
|
+
# UPDATE with multiple conditions
|
|
305
|
+
query = (Users.update({'email': 'newemail@example.com'})
|
|
306
|
+
.where(Users.id == 'abc123')
|
|
307
|
+
.where(Users.username == 'john'))
|
|
308
|
+
|
|
309
|
+
# DELETE with WHERE conditions
|
|
310
|
+
query = Users.delete().where(Users.id == 'abc123')
|
|
311
|
+
sql, params = query.render()
|
|
312
|
+
# DELETE FROM users WHERE users.id = ? RETURNING *
|
|
313
|
+
|
|
314
|
+
# DELETE with multiple conditions
|
|
315
|
+
query = Users.delete().where(Users.id > 100).where(Users.email == None)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
All write operations return `RETURNING *` by default to retrieve the affected rows.
|
|
319
|
+
|
|
254
320
|
## Advanced Mixed Query (Query Builder + T-Strings)
|
|
255
321
|
|
|
256
322
|
You can combine the query builder's structured approach with raw t-string conditions for complex logic:
|
|
@@ -143,11 +143,29 @@ values = {'id': 'abc123', 'name': 'bob', 'email': 'bob@example.com'}
|
|
|
143
143
|
tsql.insert(table, values)
|
|
144
144
|
# INSERT INTO users (id, name, email) VALUES ('abc123', 'bob', 'bob@example.com')
|
|
145
145
|
|
|
146
|
+
# insert with ignore_conflict
|
|
147
|
+
tsql.insert(table, values, ignore_conflict=True)
|
|
148
|
+
# INSERT INTO users (id, name, email) VALUES ('abc123', 'bob', 'bob@example.com') ON CONFLICT DO NOTHING RETURNING *
|
|
149
|
+
|
|
150
|
+
# upsert (insert or update on conflict)
|
|
151
|
+
values = {'id': 'abc123', 'name': 'joe', 'email': 'joe@example.com'}
|
|
152
|
+
tsql.upsert(table, values, conflict_on='id')
|
|
153
|
+
# INSERT INTO users (id, name, email) VALUES ('abc123', 'joe', 'joe@example.com')
|
|
154
|
+
# ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email RETURNING *
|
|
155
|
+
|
|
156
|
+
# upsert with multiple conflict columns
|
|
157
|
+
tsql.upsert(table, values, conflict_on=['email', 'name'])
|
|
158
|
+
# ON CONFLICT (email, name) DO UPDATE SET ...
|
|
159
|
+
|
|
146
160
|
# update values on a single row
|
|
147
161
|
table = 'users'
|
|
148
162
|
values = {'name': 'joe', 'email': 'joe@example.com'}
|
|
149
163
|
tsql.update(table, values, id='abc123')
|
|
150
|
-
# UPDATE users SET name='joe', email='joe@example.com' WHERE id='abc123'
|
|
164
|
+
# UPDATE users SET name='joe', email='joe@example.com' WHERE id='abc123' RETURNING *
|
|
165
|
+
|
|
166
|
+
# delete a single row
|
|
167
|
+
tsql.delete(table, id='abc123')
|
|
168
|
+
# DELETE FROM users WHERE id = 'abc123'
|
|
151
169
|
```
|
|
152
170
|
|
|
153
171
|
# Query Builder
|
|
@@ -240,6 +258,54 @@ query = (Posts.select(Posts.title, Users.username)
|
|
|
240
258
|
.limit(20))
|
|
241
259
|
```
|
|
242
260
|
|
|
261
|
+
## Write Operations
|
|
262
|
+
|
|
263
|
+
The query builder supports INSERT, UPDATE, UPSERT, and DELETE operations:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
# INSERT
|
|
267
|
+
values = {'id': 'abc123', 'username': 'john', 'email': 'john@example.com'}
|
|
268
|
+
query = Users.insert(values)
|
|
269
|
+
sql, params = query.render()
|
|
270
|
+
# INSERT INTO users (id, username, email) VALUES (?, ?, ?) RETURNING *
|
|
271
|
+
|
|
272
|
+
# INSERT with conflict handling (ignore)
|
|
273
|
+
query = Users.insert(values, ignore_conflict=True)
|
|
274
|
+
sql, params = query.render()
|
|
275
|
+
# INSERT INTO users (id, username, email) VALUES (?, ?, ?) ON CONFLICT DO NOTHING RETURNING *
|
|
276
|
+
|
|
277
|
+
# UPSERT (INSERT ... ON CONFLICT DO UPDATE)
|
|
278
|
+
values = {'id': 'abc123', 'username': 'john_updated', 'email': 'john@example.com'}
|
|
279
|
+
query = Users.upsert(values, conflict_on='id')
|
|
280
|
+
sql, params = query.render()
|
|
281
|
+
# INSERT INTO users (id, username, email) VALUES (?, ?, ?)
|
|
282
|
+
# ON CONFLICT (id) DO UPDATE SET username=EXCLUDED.username, email=EXCLUDED.email RETURNING *
|
|
283
|
+
|
|
284
|
+
# UPSERT with multiple conflict columns
|
|
285
|
+
query = Users.upsert(values, conflict_on=['email', 'username'])
|
|
286
|
+
# Can also use Column objects: conflict_on=Users.id or conflict_on=[Users.email, Users.username]
|
|
287
|
+
|
|
288
|
+
# UPDATE with WHERE conditions
|
|
289
|
+
query = Users.update({'email': 'newemail@example.com'}).where(Users.id == 'abc123')
|
|
290
|
+
sql, params = query.render()
|
|
291
|
+
# UPDATE users SET email=? WHERE users.id = ? RETURNING *
|
|
292
|
+
|
|
293
|
+
# UPDATE with multiple conditions
|
|
294
|
+
query = (Users.update({'email': 'newemail@example.com'})
|
|
295
|
+
.where(Users.id == 'abc123')
|
|
296
|
+
.where(Users.username == 'john'))
|
|
297
|
+
|
|
298
|
+
# DELETE with WHERE conditions
|
|
299
|
+
query = Users.delete().where(Users.id == 'abc123')
|
|
300
|
+
sql, params = query.render()
|
|
301
|
+
# DELETE FROM users WHERE users.id = ? RETURNING *
|
|
302
|
+
|
|
303
|
+
# DELETE with multiple conditions
|
|
304
|
+
query = Users.delete().where(Users.id > 100).where(Users.email == None)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
All write operations return `RETURNING *` by default to retrieve the affected rows.
|
|
308
|
+
|
|
243
309
|
## Advanced Mixed Query (Query Builder + T-Strings)
|
|
244
310
|
|
|
245
311
|
You can combine the query builder's structured approach with raw t-string conditions for complex logic:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncpg
|
|
2
|
+
import os
|
|
2
3
|
import pytest
|
|
3
4
|
|
|
4
5
|
import tsql
|
|
@@ -6,7 +7,7 @@ import tsql.styles
|
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
# Test configuration
|
|
9
|
-
DATABASE_URL = "postgresql://postgres:password@localhost:5454/postgres"
|
|
10
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:password@localhost:5454/postgres")
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@pytest.fixture
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import asyncpg
|
|
2
|
+
import os
|
|
2
3
|
import pytest
|
|
3
4
|
|
|
4
5
|
import tsql
|
|
5
6
|
import tsql.styles
|
|
6
7
|
|
|
7
8
|
# Test configuration
|
|
8
|
-
DATABASE_URL = "postgresql://postgres:password@localhost:5454/postgres"
|
|
9
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:password@localhost:5454/postgres")
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@pytest.fixture
|
|
@@ -3,12 +3,13 @@ Proof-of-concept test demonstrating that tsql actually prevents SQL injection at
|
|
|
3
3
|
against a real PostgreSQL database.
|
|
4
4
|
"""
|
|
5
5
|
import asyncpg
|
|
6
|
+
import os
|
|
6
7
|
import pytest
|
|
7
8
|
|
|
8
9
|
import tsql
|
|
9
10
|
import tsql.styles
|
|
10
11
|
|
|
11
|
-
DATABASE_URL = "postgresql://postgres:password@localhost:5454/postgres"
|
|
12
|
+
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:password@localhost:5454/postgres")
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
@pytest.fixture
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|