sqlym 0.1.0__tar.gz → 0.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.
Files changed (67) hide show
  1. {sqlym-0.1.0 → sqlym-0.1.1}/Makefile +11 -1
  2. {sqlym-0.1.0 → sqlym-0.1.1}/PKG-INFO +22 -17
  3. {sqlym-0.1.0 → sqlym-0.1.1}/README.ja.md +21 -16
  4. {sqlym-0.1.0 → sqlym-0.1.1}/README.md +21 -16
  5. sqlym-0.1.1/examples/crud_example.py +382 -0
  6. sqlym-0.1.1/examples/sql/delete_user.sql +4 -0
  7. sqlym-0.1.1/examples/sql/find_users.sql +15 -0
  8. sqlym-0.1.1/examples/sql/insert_user.sql +8 -0
  9. sqlym-0.1.1/examples/sql/update_user.sql +8 -0
  10. {sqlym-0.1.0 → sqlym-0.1.1}/pyproject.toml +1 -1
  11. {sqlym-0.1.0 → sqlym-0.1.1}/uv.lock +1 -1
  12. {sqlym-0.1.0 → sqlym-0.1.1}/.claude/settings.json +0 -0
  13. {sqlym-0.1.0 → sqlym-0.1.1}/.devcontainer/devcontainer.json +0 -0
  14. {sqlym-0.1.0 → sqlym-0.1.1}/.github/workflows/ci.yml +0 -0
  15. {sqlym-0.1.0 → sqlym-0.1.1}/.gitignore +0 -0
  16. {sqlym-0.1.0 → sqlym-0.1.1}/.pre-commit-config.yaml +0 -0
  17. {sqlym-0.1.0 → sqlym-0.1.1}/.python-version +0 -0
  18. {sqlym-0.1.0 → sqlym-0.1.1}/.vscode/settings.json +0 -0
  19. {sqlym-0.1.0 → sqlym-0.1.1}/CLAUDE.md +0 -0
  20. {sqlym-0.1.0 → sqlym-0.1.1}/LICENSE +0 -0
  21. {sqlym-0.1.0 → sqlym-0.1.1}/SQL_SYNTAX.ja.md +0 -0
  22. {sqlym-0.1.0 → sqlym-0.1.1}/SQL_SYNTAX.md +0 -0
  23. {sqlym-0.1.0 → sqlym-0.1.1}/docker-compose.yml +0 -0
  24. {sqlym-0.1.0 → sqlym-0.1.1}/docs/DESIGN.md +0 -0
  25. {sqlym-0.1.0 → sqlym-0.1.1}/docs/SPEC.md +0 -0
  26. {sqlym-0.1.0 → sqlym-0.1.1}/docs/TASK.M1.md +0 -0
  27. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/__init__.py +0 -0
  28. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/_parse.py +0 -0
  29. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/config.py +0 -0
  30. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/dialect.py +0 -0
  31. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/escape_utils.py +0 -0
  32. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/exceptions.py +0 -0
  33. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/loader.py +0 -0
  34. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/__init__.py +0 -0
  35. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/column.py +0 -0
  36. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/dataclass.py +0 -0
  37. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/factory.py +0 -0
  38. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/manual.py +0 -0
  39. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/protocol.py +0 -0
  40. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/pydantic.py +0 -0
  41. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/__init__.py +0 -0
  42. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/line_unit.py +0 -0
  43. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/tokenizer.py +0 -0
  44. {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/twoway.py +0 -0
  45. {sqlym-0.1.0 → sqlym-0.1.1}/tests/__init__.py +0 -0
  46. {sqlym-0.1.0 → sqlym-0.1.1}/tests/conftest.py +0 -0
  47. {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_mysql.py +0 -0
  48. {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_oracle.py +0 -0
  49. {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_postgresql.py +0 -0
  50. {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_sqlite.py +0 -0
  51. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_column_entity.py +0 -0
  52. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_create_mapper.py +0 -0
  53. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_dataclass_mapper.py +0 -0
  54. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_dialect.py +0 -0
  55. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_exceptions.py +0 -0
  56. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_line_unit.py +0 -0
  57. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_mapper_protocol.py +0 -0
  58. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_public_api.py +0 -0
  59. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_pydantic_mapper.py +0 -0
  60. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_sql_loader.py +0 -0
  61. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_tokenizer.py +0 -0
  62. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_clean_sql.py +0 -0
  63. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_in_clause.py +0 -0
  64. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_param_substitution.py +0 -0
  65. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_parse_lines.py +0 -0
  66. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_placeholder.py +0 -0
  67. {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_removal.py +0 -0
@@ -1,4 +1,4 @@
1
- .PHONY: install test lint format lint-fix pre-commit clean db-up db-down test-postgresql test-mysql test-oracle test-db test-all
1
+ .PHONY: install test lint format lint-fix pre-commit clean db-up db-down test-postgresql test-mysql test-oracle test-db test-all build release-test release
2
2
 
3
3
  install:
4
4
  uv sync --dev
@@ -46,3 +46,13 @@ clean:
46
46
  rm -rf .venv .ruff_cache .pytest_cache .coverage htmlcov dist build
47
47
  find . -type d -name __pycache__ -exec rm -rf {} +
48
48
  find . -type d -name "*.egg-info" -exec rm -rf {} +
49
+
50
+ build:
51
+ rm -rf dist/
52
+ uv run python -m build
53
+
54
+ release-test: build
55
+ uv run twine upload --repository testpypi dist/*
56
+
57
+ release: build
58
+ uv run twine upload dist/*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlym
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: SQL-first database access library for Python
5
5
  Author: izuno4t
6
6
  License-Expression: MIT
@@ -85,26 +85,30 @@ WHERE
85
85
  AND status = /* $status */'active'
86
86
  ```
87
87
 
88
- ### 3. Execute
88
+ ### 3. Parse and Execute
89
89
 
90
90
  ```python
91
- from sqlym import SqlExecutor, create_mapper
91
+ from sqlym import SqlLoader, parse_sql, create_mapper
92
92
 
93
- executor = SqlExecutor(connection)
93
+ # Load SQL template
94
+ loader = SqlLoader("sql")
95
+ sql_template = loader.load("employee/find_by_dept.sql")
96
+
97
+ # Parse with parameters (lines with None are automatically removed)
98
+ result = parse_sql(sql_template, {
99
+ "id": 100,
100
+ "dept_id": None, # this line is removed
101
+ "status": "active",
102
+ })
103
+
104
+ # Execute with your database driver
105
+ cursor.execute(result.sql, result.params)
106
+
107
+ # Map results to entity
94
108
  mapper = create_mapper(Employee)
109
+ employees = [mapper.map(row) for row in cursor.fetchall()]
95
110
 
96
- # Lines with None parameters are automatically removed
97
- result = executor.query(
98
- "sql/employee/find_by_dept.sql",
99
- {
100
- "id": 100,
101
- "dept_id": None,
102
- "status": "active",
103
- }, # dept_id line is removed
104
- mapper=mapper
105
- )
106
-
107
- for emp in result:
111
+ for emp in employees:
108
112
  print(emp.name)
109
113
  ```
110
114
 
@@ -154,7 +158,8 @@ If you want to hide the SQL snippet from error messages, disable it via
154
158
  config:
155
159
 
156
160
  ```python
157
- from sqlym import config
161
+ from sqlym.config import ERROR_INCLUDE_SQL, ERROR_MESSAGE_LANGUAGE
162
+ import sqlym.config as config
158
163
 
159
164
  config.ERROR_INCLUDE_SQL = False
160
165
  config.ERROR_MESSAGE_LANGUAGE = "en"
@@ -58,26 +58,30 @@ WHERE
58
58
  AND status = /* $status */'active'
59
59
  ```
60
60
 
61
- ### 3. 実行
61
+ ### 3. パースと実行
62
62
 
63
63
  ```python
64
- from sqlym import SqlExecutor, create_mapper
64
+ from sqlym import SqlLoader, parse_sql, create_mapper
65
65
 
66
- executor = SqlExecutor(connection)
66
+ # SQL テンプレート読み込み
67
+ loader = SqlLoader("sql")
68
+ sql_template = loader.load("employee/find_by_dept.sql")
69
+
70
+ # パラメータでパース(None の行は自動削除)
71
+ result = parse_sql(sql_template, {
72
+ "id": 100,
73
+ "dept_id": None, # この行は削除される
74
+ "status": "active",
75
+ })
76
+
77
+ # データベースドライバーで実行
78
+ cursor.execute(result.sql, result.params)
79
+
80
+ # 結果をエンティティにマッピング
67
81
  mapper = create_mapper(Employee)
82
+ employees = [mapper.map(row) for row in cursor.fetchall()]
68
83
 
69
- # パラメータがNoneの行は自動削除される
70
- result = executor.query(
71
- "sql/employee/find_by_dept.sql",
72
- {
73
- "id": 100,
74
- "dept_id": None,
75
- "status": "active",
76
- }, # dept_idの行は消える
77
- mapper=mapper
78
- )
79
-
80
- for emp in result:
84
+ for emp in employees:
81
85
  print(emp.name)
82
86
  ```
83
87
 
@@ -127,7 +131,8 @@ SQL 断片を表示したくない場合は
127
131
  設定で無効化してください。
128
132
 
129
133
  ```python
130
- from sqlym import config
134
+ from sqlym.config import ERROR_INCLUDE_SQL, ERROR_MESSAGE_LANGUAGE
135
+ import sqlym.config as config
131
136
 
132
137
  config.ERROR_INCLUDE_SQL = False
133
138
  config.ERROR_MESSAGE_LANGUAGE = "en"
@@ -58,26 +58,30 @@ WHERE
58
58
  AND status = /* $status */'active'
59
59
  ```
60
60
 
61
- ### 3. Execute
61
+ ### 3. Parse and Execute
62
62
 
63
63
  ```python
64
- from sqlym import SqlExecutor, create_mapper
64
+ from sqlym import SqlLoader, parse_sql, create_mapper
65
65
 
66
- executor = SqlExecutor(connection)
66
+ # Load SQL template
67
+ loader = SqlLoader("sql")
68
+ sql_template = loader.load("employee/find_by_dept.sql")
69
+
70
+ # Parse with parameters (lines with None are automatically removed)
71
+ result = parse_sql(sql_template, {
72
+ "id": 100,
73
+ "dept_id": None, # this line is removed
74
+ "status": "active",
75
+ })
76
+
77
+ # Execute with your database driver
78
+ cursor.execute(result.sql, result.params)
79
+
80
+ # Map results to entity
67
81
  mapper = create_mapper(Employee)
82
+ employees = [mapper.map(row) for row in cursor.fetchall()]
68
83
 
69
- # Lines with None parameters are automatically removed
70
- result = executor.query(
71
- "sql/employee/find_by_dept.sql",
72
- {
73
- "id": 100,
74
- "dept_id": None,
75
- "status": "active",
76
- }, # dept_id line is removed
77
- mapper=mapper
78
- )
79
-
80
- for emp in result:
84
+ for emp in employees:
81
85
  print(emp.name)
82
86
  ```
83
87
 
@@ -127,7 +131,8 @@ If you want to hide the SQL snippet from error messages, disable it via
127
131
  config:
128
132
 
129
133
  ```python
130
- from sqlym import config
134
+ from sqlym.config import ERROR_INCLUDE_SQL, ERROR_MESSAGE_LANGUAGE
135
+ import sqlym.config as config
131
136
 
132
137
  config.ERROR_INCLUDE_SQL = False
133
138
  config.ERROR_MESSAGE_LANGUAGE = "en"
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env python3
2
+ """sqlym CRUD Example.
3
+
4
+ This example demonstrates the basic usage of sqlym:
5
+ - Entity definition (dataclass + Column mapping)
6
+ - SQL file loading with SqlLoader
7
+ - Dynamic conditional search (None parameters remove condition lines)
8
+ - IN clause expansion
9
+ - LIKE escape
10
+ - Row mapping with create_mapper
11
+
12
+ Usage:
13
+ uv run python examples/crud_example.py
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sqlite3
19
+ from dataclasses import dataclass
20
+ from datetime import datetime
21
+ from pathlib import Path
22
+ from typing import Annotated
23
+
24
+ from sqlym import Column, Dialect, SqlLoader, create_mapper, escape_like, parse_sql
25
+
26
+
27
+ # =============================================================================
28
+ # Entity Definition
29
+ # =============================================================================
30
+
31
+
32
+ @dataclass
33
+ class User:
34
+ """User entity.
35
+
36
+ Use Annotated[T, Column("DB_COLUMN_NAME")] to define
37
+ mapping between DB column names and field names.
38
+ """
39
+
40
+ id: int
41
+ name: str
42
+ email: str
43
+ department: str | None = None
44
+ created_at: Annotated[datetime | None, Column("created_at")] = None
45
+
46
+
47
+ # =============================================================================
48
+ # Database Setup
49
+ # =============================================================================
50
+
51
+
52
+ def setup_database() -> sqlite3.Connection:
53
+ """Set up SQLite database for testing."""
54
+ conn = sqlite3.connect(":memory:")
55
+ conn.row_factory = sqlite3.Row # Enable dict-like access
56
+
57
+ conn.execute("""
58
+ CREATE TABLE users (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ name TEXT NOT NULL,
61
+ email TEXT NOT NULL UNIQUE,
62
+ department TEXT,
63
+ created_at TEXT
64
+ )
65
+ """)
66
+
67
+ # Insert sample data
68
+ sample_data = [
69
+ ("Tanaka Taro", "tanaka@example.com", "Sales", "2024-01-15 09:00:00"),
70
+ ("Suzuki Hanako", "suzuki@example.com", "Development", "2024-02-01 10:30:00"),
71
+ ("Sato Ichiro", "sato@example.com", "Sales", "2024-02-15 14:00:00"),
72
+ ("Yamada Misaki", "yamada@example.com", "Development", "2024-03-01 11:00:00"),
73
+ ("Takahashi 100% Achieved", "takahashi@example.com", "Sales", "2024-03-15 16:00:00"),
74
+ ]
75
+ conn.executemany(
76
+ "INSERT INTO users (name, email, department, created_at) VALUES (?, ?, ?, ?)",
77
+ sample_data,
78
+ )
79
+ conn.commit()
80
+ return conn
81
+
82
+
83
+ # =============================================================================
84
+ # CRUD Operations
85
+ # =============================================================================
86
+
87
+
88
+ def demo_select_all(conn: sqlite3.Connection) -> None:
89
+ """Demo: Select all records."""
90
+ print("=" * 60)
91
+ print("[SELECT ALL]")
92
+ print("=" * 60)
93
+
94
+ sql = "SELECT * FROM users ORDER BY id"
95
+ result = parse_sql(sql, {})
96
+
97
+ print(f"SQL: {result.sql}")
98
+ print(f"Parameters: {result.params}")
99
+ print()
100
+
101
+ cursor = conn.execute(result.sql, result.params)
102
+ rows = [dict(row) for row in cursor.fetchall()]
103
+
104
+ mapper = create_mapper(User)
105
+ users = mapper.map_rows(rows)
106
+
107
+ for user in users:
108
+ print(f" {user}")
109
+ print()
110
+
111
+
112
+ def demo_dynamic_search(conn: sqlite3.Connection, loader: SqlLoader) -> None:
113
+ """Demo: Dynamic conditional search.
114
+
115
+ When a parameter is None, that condition line is automatically removed
116
+ (for parameters with $ prefix).
117
+ """
118
+ print("=" * 60)
119
+ print("[DYNAMIC SEARCH] Only department specified (other conditions removed)")
120
+ print("=" * 60)
121
+
122
+ sql_template = loader.load("find_users.sql")
123
+ print("SQL Template:")
124
+ print(sql_template)
125
+ print()
126
+
127
+ # Only department specified, others are None -> those condition lines are removed
128
+ result = parse_sql(
129
+ sql_template,
130
+ {
131
+ "id": None, # This line is removed
132
+ "name_pattern": None, # This line is removed
133
+ "department": "Sales", # This condition remains
134
+ "ids": None, # This line is removed
135
+ },
136
+ )
137
+
138
+ print(f"Generated SQL:\n{result.sql}")
139
+ print(f"Parameters: {result.params}")
140
+ print()
141
+
142
+ cursor = conn.execute(result.sql, result.params)
143
+ rows = [dict(row) for row in cursor.fetchall()]
144
+ mapper = create_mapper(User)
145
+ users = mapper.map_rows(rows)
146
+
147
+ print("Results:")
148
+ for user in users:
149
+ print(f" {user}")
150
+ print()
151
+
152
+
153
+ def demo_in_clause(conn: sqlite3.Connection, loader: SqlLoader) -> None:
154
+ """Demo: IN clause expansion.
155
+
156
+ List parameters are automatically expanded.
157
+ Example: IN /* $ids */(1) -> IN (?, ?, ?)
158
+ """
159
+ print("=" * 60)
160
+ print("[IN CLAUSE] Search by multiple IDs")
161
+ print("=" * 60)
162
+
163
+ sql_template = loader.load("find_users.sql")
164
+
165
+ # Only ids specified, others are None
166
+ result = parse_sql(
167
+ sql_template,
168
+ {
169
+ "id": None,
170
+ "name_pattern": None,
171
+ "department": None,
172
+ "ids": [1, 3, 5], # Expanded to IN clause
173
+ },
174
+ )
175
+
176
+ print(f"Generated SQL:\n{result.sql}")
177
+ print(f"Parameters: {result.params}")
178
+ print()
179
+
180
+ cursor = conn.execute(result.sql, result.params)
181
+ rows = [dict(row) for row in cursor.fetchall()]
182
+ mapper = create_mapper(User)
183
+ users = mapper.map_rows(rows)
184
+
185
+ print("Results:")
186
+ for user in users:
187
+ print(f" {user}")
188
+ print()
189
+
190
+
191
+ def demo_like_escape(conn: sqlite3.Connection, loader: SqlLoader) -> None:
192
+ """Demo: LIKE escape.
193
+
194
+ Use escape_like() to safely search strings containing
195
+ special characters (%, _, #).
196
+ """
197
+ print("=" * 60)
198
+ print("[LIKE ESCAPE] Search for names containing '100%'")
199
+ print("=" * 60)
200
+
201
+ sql_template = loader.load("find_users.sql")
202
+
203
+ # Search for users containing "100%" (% is a LIKE special char, needs escaping)
204
+ search_term = escape_like("100%", Dialect.SQLITE)
205
+ print(f"Search term: '100%' -> After escape: '{search_term}'")
206
+
207
+ result = parse_sql(
208
+ sql_template,
209
+ {
210
+ "id": None,
211
+ "name_pattern": f"%{search_term}%", # Add % for partial match
212
+ "department": None,
213
+ "ids": None,
214
+ },
215
+ dialect=Dialect.SQLITE,
216
+ )
217
+
218
+ print(f"Generated SQL:\n{result.sql}")
219
+ print(f"Parameters: {result.params}")
220
+ print()
221
+
222
+ cursor = conn.execute(result.sql, result.params)
223
+ rows = [dict(row) for row in cursor.fetchall()]
224
+ mapper = create_mapper(User)
225
+ users = mapper.map_rows(rows)
226
+
227
+ print("Results:")
228
+ for user in users:
229
+ print(f" {user}")
230
+ print()
231
+
232
+
233
+ def demo_insert(conn: sqlite3.Connection, loader: SqlLoader) -> None:
234
+ """Demo: INSERT."""
235
+ print("=" * 60)
236
+ print("[INSERT] Register new user")
237
+ print("=" * 60)
238
+
239
+ sql_template = loader.load("insert_user.sql")
240
+
241
+ result = parse_sql(
242
+ sql_template,
243
+ {
244
+ "name": "New User",
245
+ "email": "newuser@example.com",
246
+ "department": "General Affairs",
247
+ "created_at": "2024-04-01 09:00:00",
248
+ },
249
+ )
250
+
251
+ print(f"Generated SQL:\n{result.sql}")
252
+ print(f"Parameters: {result.params}")
253
+ print()
254
+
255
+ cursor = conn.execute(result.sql, result.params)
256
+ conn.commit()
257
+ print(f"Inserted ID: {cursor.lastrowid}")
258
+ print()
259
+
260
+
261
+ def demo_update(conn: sqlite3.Connection, loader: SqlLoader) -> None:
262
+ """Demo: UPDATE."""
263
+ print("=" * 60)
264
+ print("[UPDATE] Update user information")
265
+ print("=" * 60)
266
+
267
+ sql_template = loader.load("update_user.sql")
268
+
269
+ result = parse_sql(
270
+ sql_template,
271
+ {
272
+ "id": 1,
273
+ "name": "Tanaka Taro (Updated)",
274
+ "email": "tanaka-updated@example.com",
275
+ "department": "Sales (Transferred)",
276
+ },
277
+ )
278
+
279
+ print(f"Generated SQL:\n{result.sql}")
280
+ print(f"Parameters: {result.params}")
281
+ print()
282
+
283
+ conn.execute(result.sql, result.params)
284
+ conn.commit()
285
+
286
+ # Verify update result
287
+ cursor = conn.execute("SELECT * FROM users WHERE id = 1")
288
+ row = dict(cursor.fetchone())
289
+ mapper = create_mapper(User)
290
+ user = mapper.map_row(row)
291
+ print(f"After update: {user}")
292
+ print()
293
+
294
+
295
+ def demo_delete(conn: sqlite3.Connection, loader: SqlLoader) -> None:
296
+ """Demo: DELETE."""
297
+ print("=" * 60)
298
+ print("[DELETE] Delete user")
299
+ print("=" * 60)
300
+
301
+ sql_template = loader.load("delete_user.sql")
302
+
303
+ result = parse_sql(
304
+ sql_template,
305
+ {"id": 6}, # User inserted by demo_insert
306
+ )
307
+
308
+ print(f"Generated SQL:\n{result.sql}")
309
+ print(f"Parameters: {result.params}")
310
+ print()
311
+
312
+ cursor = conn.execute(result.sql, result.params)
313
+ conn.commit()
314
+ print(f"Deleted rows: {cursor.rowcount}")
315
+ print()
316
+
317
+
318
+ def demo_named_placeholder(conn: sqlite3.Connection) -> None:
319
+ """Demo: Named placeholder (:name format).
320
+
321
+ Supports :name format placeholders used by Oracle, etc.
322
+ """
323
+ print("=" * 60)
324
+ print("[NAMED PLACEHOLDER] :name format")
325
+ print("=" * 60)
326
+
327
+ sql = "SELECT * FROM users WHERE department = /* $department */'default'"
328
+ result = parse_sql(
329
+ sql,
330
+ {"department": "Development"},
331
+ placeholder=":name", # Specify named placeholder
332
+ )
333
+
334
+ print(f"Generated SQL: {result.sql}")
335
+ print(f"Named parameters: {result.named_params}")
336
+ print()
337
+
338
+ # sqlite3 supports :name format
339
+ cursor = conn.execute(result.sql, result.named_params)
340
+ rows = [dict(row) for row in cursor.fetchall()]
341
+ mapper = create_mapper(User)
342
+ users = mapper.map_rows(rows)
343
+
344
+ print("Results:")
345
+ for user in users:
346
+ print(f" {user}")
347
+ print()
348
+
349
+
350
+ # =============================================================================
351
+ # Main
352
+ # =============================================================================
353
+
354
+
355
+ def main() -> None:
356
+ """Run the examples."""
357
+ print("sqlym CRUD Example")
358
+ print("=" * 60)
359
+ print()
360
+
361
+ # Set up database and SqlLoader
362
+ conn = setup_database()
363
+ sql_dir = Path(__file__).parent / "sql"
364
+ loader = SqlLoader(sql_dir)
365
+
366
+ # Run each demo
367
+ demo_select_all(conn)
368
+ demo_dynamic_search(conn, loader)
369
+ demo_in_clause(conn, loader)
370
+ demo_like_escape(conn, loader)
371
+ demo_insert(conn, loader)
372
+ demo_update(conn, loader)
373
+ demo_delete(conn, loader)
374
+ demo_named_placeholder(conn)
375
+
376
+ print("=" * 60)
377
+ print("Example completed")
378
+ print("=" * 60)
379
+
380
+
381
+ if __name__ == "__main__":
382
+ main()
@@ -0,0 +1,4 @@
1
+ -- User deletion SQL
2
+ DELETE FROM users
3
+ WHERE
4
+ id = /* id */1
@@ -0,0 +1,15 @@
1
+ -- User search SQL (with dynamic conditions)
2
+ -- 2way SQL: When a parameter is None, that condition line is automatically removed
3
+ SELECT
4
+ id,
5
+ name,
6
+ email,
7
+ department,
8
+ created_at
9
+ FROM users
10
+ WHERE
11
+ id = /* $id */1
12
+ AND name LIKE /* $name_pattern */'%test%' ESCAPE '#'
13
+ AND department = /* $department */'Sales'
14
+ AND id IN /* $ids */(1, 2, 3)
15
+ ORDER BY id
@@ -0,0 +1,8 @@
1
+ -- User registration SQL
2
+ INSERT INTO users (name, email, department, created_at)
3
+ VALUES (
4
+ /* name */'default_name',
5
+ /* email */'default@example.com',
6
+ /* department */'General',
7
+ /* created_at */'2024-01-01 00:00:00'
8
+ )
@@ -0,0 +1,8 @@
1
+ -- User update SQL
2
+ UPDATE users
3
+ SET
4
+ name = /* name */'default_name',
5
+ email = /* email */'default@example.com',
6
+ department = /* department */'General'
7
+ WHERE
8
+ id = /* id */1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlym"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "SQL-first database access library for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -775,7 +775,7 @@ wheels = [
775
775
 
776
776
  [[package]]
777
777
  name = "sqlym"
778
- version = "0.1.0"
778
+ version = "0.1.1"
779
779
  source = { editable = "." }
780
780
 
781
781
  [package.optional-dependencies]
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
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