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.
- {sqlym-0.1.0 → sqlym-0.1.1}/Makefile +11 -1
- {sqlym-0.1.0 → sqlym-0.1.1}/PKG-INFO +22 -17
- {sqlym-0.1.0 → sqlym-0.1.1}/README.ja.md +21 -16
- {sqlym-0.1.0 → sqlym-0.1.1}/README.md +21 -16
- sqlym-0.1.1/examples/crud_example.py +382 -0
- sqlym-0.1.1/examples/sql/delete_user.sql +4 -0
- sqlym-0.1.1/examples/sql/find_users.sql +15 -0
- sqlym-0.1.1/examples/sql/insert_user.sql +8 -0
- sqlym-0.1.1/examples/sql/update_user.sql +8 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/pyproject.toml +1 -1
- {sqlym-0.1.0 → sqlym-0.1.1}/uv.lock +1 -1
- {sqlym-0.1.0 → sqlym-0.1.1}/.claude/settings.json +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/.devcontainer/devcontainer.json +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/.github/workflows/ci.yml +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/.gitignore +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/.pre-commit-config.yaml +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/.python-version +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/.vscode/settings.json +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/CLAUDE.md +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/LICENSE +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/SQL_SYNTAX.ja.md +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/SQL_SYNTAX.md +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/docker-compose.yml +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/docs/DESIGN.md +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/docs/SPEC.md +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/docs/TASK.M1.md +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/__init__.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/_parse.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/config.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/dialect.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/escape_utils.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/exceptions.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/loader.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/__init__.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/column.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/dataclass.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/factory.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/manual.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/protocol.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/mapper/pydantic.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/__init__.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/line_unit.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/tokenizer.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/src/sqlym/parser/twoway.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/__init__.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/conftest.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_mysql.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_oracle.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_postgresql.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/integration/test_sqlite.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_column_entity.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_create_mapper.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_dataclass_mapper.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_dialect.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_exceptions.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_line_unit.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_mapper_protocol.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_public_api.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_pydantic_mapper.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_sql_loader.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_tokenizer.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_clean_sql.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_in_clause.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_param_substitution.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_parse_lines.py +0 -0
- {sqlym-0.1.0 → sqlym-0.1.1}/tests/test_twoway_placeholder.py +0 -0
- {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.
|
|
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
|
|
91
|
+
from sqlym import SqlLoader, parse_sql, create_mapper
|
|
92
92
|
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
64
|
+
from sqlym import SqlLoader, parse_sql, create_mapper
|
|
65
65
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
64
|
+
from sqlym import SqlLoader, parse_sql, create_mapper
|
|
65
65
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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,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
|
|
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
|
|
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
|