hayate-sql 0.1.0__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.
- hayate_sql-0.1.0/LICENSE +21 -0
- hayate_sql-0.1.0/PKG-INFO +227 -0
- hayate_sql-0.1.0/README.md +204 -0
- hayate_sql-0.1.0/pyproject.toml +61 -0
- hayate_sql-0.1.0/src/hayate_sql/__init__.py +36 -0
- hayate_sql-0.1.0/src/hayate_sql/__main__.py +105 -0
- hayate_sql-0.1.0/src/hayate_sql/adapters/__init__.py +7 -0
- hayate_sql-0.1.0/src/hayate_sql/adapters/d1.py +72 -0
- hayate_sql-0.1.0/src/hayate_sql/adapters/postgres.py +36 -0
- hayate_sql-0.1.0/src/hayate_sql/adapters/sqlite.py +104 -0
- hayate_sql-0.1.0/src/hayate_sql/check.py +149 -0
- hayate_sql-0.1.0/src/hayate_sql/contract.py +111 -0
- hayate_sql-0.1.0/src/hayate_sql/database.py +135 -0
- hayate_sql-0.1.0/src/hayate_sql/generate.py +173 -0
- hayate_sql-0.1.0/src/hayate_sql/migrations.py +63 -0
- hayate_sql-0.1.0/src/hayate_sql/parser.py +153 -0
- hayate_sql-0.1.0/src/hayate_sql/py.typed +1 -0
hayate_sql-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuke Hayashi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hayate-sql
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQL-first query contracts for Python, PostgreSQL, SQLite, and Cloudflare D1
|
|
5
|
+
Keywords: hayate,sql,postgresql,sqlite,cloudflare-d1
|
|
6
|
+
Author: Yusuke Hayashi
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: asyncpg>=0.30 ; extra == 'postgres'
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Project-URL: Repository, https://github.com/hayatepy/hayate-sql
|
|
19
|
+
Project-URL: Documentation, https://github.com/hayatepy/hayate-sql#readme
|
|
20
|
+
Project-URL: Changelog, https://github.com/hayatepy/hayate-sql/blob/main/CHANGELOG.md
|
|
21
|
+
Provides-Extra: postgres
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# hayate-sql
|
|
25
|
+
|
|
26
|
+
Native SQL with a checked, typed execution boundary — not an ORM and not a
|
|
27
|
+
query builder.
|
|
28
|
+
|
|
29
|
+
`hayate-sql` keeps each database's SQL, placeholders, plans, and transaction
|
|
30
|
+
semantics intact. A small contract above each statement adds named application
|
|
31
|
+
arguments, result cardinality, result shape, timeout, and parameter-free
|
|
32
|
+
telemetry.
|
|
33
|
+
|
|
34
|
+
> **Status: alpha (0.1.x), typed.** SQLite, Cloudflare D1, and
|
|
35
|
+
> asyncpg-compatible PostgreSQL execution are implemented. SQLite/D1 contracts
|
|
36
|
+
> can be compiled against an in-memory schema; PostgreSQL contracts can be
|
|
37
|
+
> prepared against an ephemeral PostgreSQL transaction. The design memo
|
|
38
|
+
> (Japanese, per project convention) is in [DESIGN.md](DESIGN.md).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uv add hayate-sql
|
|
44
|
+
|
|
45
|
+
# Include asyncpg when PostgreSQL is the target.
|
|
46
|
+
uv add "hayate-sql[postgres]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Write SQL
|
|
50
|
+
|
|
51
|
+
One `.sql` file contains one database-native statement:
|
|
52
|
+
|
|
53
|
+
```sql
|
|
54
|
+
-- name: get_document :one?
|
|
55
|
+
-- param: workspace_id str
|
|
56
|
+
-- param: document_id UUID
|
|
57
|
+
-- column: id UUID
|
|
58
|
+
-- column: title str
|
|
59
|
+
-- column: body str
|
|
60
|
+
-- timeout: 50ms
|
|
61
|
+
SELECT id, title, body
|
|
62
|
+
FROM documents
|
|
63
|
+
WHERE workspace_id = ?1
|
|
64
|
+
AND id = ?2
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The placeholder syntax belongs to the target database. Use `?1` for
|
|
68
|
+
SQLite/D1 and `$1` for PostgreSQL. The declared parameter order maps named
|
|
69
|
+
Python arguments to those native positions.
|
|
70
|
+
|
|
71
|
+
Cardinality is explicit:
|
|
72
|
+
|
|
73
|
+
- `:one` — exactly one row
|
|
74
|
+
- `:one?` — zero or one row
|
|
75
|
+
- `:many` — zero or more rows
|
|
76
|
+
- `:exec` — no result rows; returns `CommandResult`
|
|
77
|
+
|
|
78
|
+
Supported contract types are `str`, `int`, `float`, `bool`, `bytes`, `object`,
|
|
79
|
+
`Any`, `datetime`, `date`, `Decimal`, and `UUID`. Append `?` for a nullable
|
|
80
|
+
value. Types generate Python annotations; runtime enforcement intentionally
|
|
81
|
+
checks the result's column shape rather than pretending that SQLite and
|
|
82
|
+
PostgreSQL use identical value representations. Declarations must therefore
|
|
83
|
+
describe the target driver's returned Python values (for example, a raw
|
|
84
|
+
SQLite boolean column is normally declared as `int` unless the SQL converts it).
|
|
85
|
+
|
|
86
|
+
## Compile and generate
|
|
87
|
+
|
|
88
|
+
Compile D1/SQLite SQL against the real schema without executing queries:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
hayate-sql check queries/ --dialect d1 --schema schema.sql
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For PostgreSQL, `prepare` every query inside a transaction that is always
|
|
95
|
+
rolled back:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
HAYATE_SQL_DATABASE_URL=postgresql://localhost/app_test \
|
|
99
|
+
hayate-sql check queries/postgres/ \
|
|
100
|
+
--dialect postgres \
|
|
101
|
+
--schema schema.sql
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Generate a typed facade:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
hayate-sql generate queries/ -o app/queries.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The generated module intentionally uses only Python 3.12+ syntax and does not
|
|
111
|
+
force postponed annotations. Run the project's formatter after generation
|
|
112
|
+
when its style policy is stricter than the standard output.
|
|
113
|
+
|
|
114
|
+
The generated function has named arguments and a `TypedDict` result:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
document = await queries.get_document(
|
|
118
|
+
database,
|
|
119
|
+
workspace_id=workspace_id,
|
|
120
|
+
document_id=document_id,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Generated code is a build artifact. SQL remains the source of truth.
|
|
125
|
+
|
|
126
|
+
### Check against migration history
|
|
127
|
+
|
|
128
|
+
When forward-only SQL migrations are the schema source of truth, replay them
|
|
129
|
+
in order before compiling the queries:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
hayate-sql check queries/ --dialect d1 --migrations migrations/
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Migration files use a fixed-width positive number followed by a lowercase
|
|
136
|
+
description, for example `0001_create_documents.sql`. Gaps are allowed, while
|
|
137
|
+
duplicate versions, mixed widths, malformed names, and empty migrations fail
|
|
138
|
+
the check.
|
|
139
|
+
|
|
140
|
+
The migrations run only in hayate-sql's disposable check database. Applying
|
|
141
|
+
them to development, staging, or production remains the responsibility of the
|
|
142
|
+
database-native tool, such as `wrangler d1 migrations apply` or Alembic.
|
|
143
|
+
|
|
144
|
+
## Execute
|
|
145
|
+
|
|
146
|
+
### SQLite
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from hayate_sql.adapters import SQLiteDatabase
|
|
150
|
+
|
|
151
|
+
async with SQLiteDatabase("app.db") as database:
|
|
152
|
+
async with database.transaction(mode="immediate"):
|
|
153
|
+
await queries.create_document(
|
|
154
|
+
database,
|
|
155
|
+
document_id=document_id,
|
|
156
|
+
title=title,
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Cloudflare D1
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from hayate_sql.adapters import D1Database
|
|
164
|
+
|
|
165
|
+
database = D1Database(context.env.DB)
|
|
166
|
+
|
|
167
|
+
# D1 consistency is explicit rather than hidden behind a generic transaction.
|
|
168
|
+
session = database.with_session("first-primary")
|
|
169
|
+
document = await queries.get_document(
|
|
170
|
+
session,
|
|
171
|
+
workspace_id=workspace_id,
|
|
172
|
+
document_id=document_id,
|
|
173
|
+
)
|
|
174
|
+
bookmark = session.bookmark()
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The repository's reproducible workerd probe builds the 0.1.0 wheel, injects
|
|
178
|
+
that wheel into the Python Workers bundle, applies a real local D1 migration,
|
|
179
|
+
and executes `:one`, `:one?`, `:many`, and `:exec`:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
bash scripts/check_workers_d1.sh
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The same probe asserts that telemetry contains neither SQL text nor a sentinel
|
|
186
|
+
bound value.
|
|
187
|
+
|
|
188
|
+
### PostgreSQL
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import asyncpg
|
|
192
|
+
|
|
193
|
+
from hayate_sql.adapters import AsyncpgDatabase
|
|
194
|
+
|
|
195
|
+
connection = await asyncpg.connect(dsn)
|
|
196
|
+
database = AsyncpgDatabase(connection)
|
|
197
|
+
|
|
198
|
+
# Delegates to asyncpg's native transaction context.
|
|
199
|
+
async with database.transaction(isolation="serializable"):
|
|
200
|
+
await queries.update_document(database, document_id=document_id, title=title)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Observe without leaking parameters
|
|
204
|
+
|
|
205
|
+
An observer receives only the query name, contract fingerprint, duration,
|
|
206
|
+
success, row count, and error type. SQL text and bound values are deliberately
|
|
207
|
+
absent.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
def observe(event):
|
|
211
|
+
metrics.histogram("db.query.duration", event.duration_ms, query=event.name)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
database = SQLiteDatabase("app.db", observer=observe)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Non-goals
|
|
218
|
+
|
|
219
|
+
- ORM models, identity maps, lazy relations, or repositories
|
|
220
|
+
- a database-independent SQL DSL or query builder
|
|
221
|
+
- SQL dialect translation
|
|
222
|
+
- a universal transaction abstraction
|
|
223
|
+
- production migration application, implicit schema migration, or write retries
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# hayate-sql
|
|
2
|
+
|
|
3
|
+
Native SQL with a checked, typed execution boundary — not an ORM and not a
|
|
4
|
+
query builder.
|
|
5
|
+
|
|
6
|
+
`hayate-sql` keeps each database's SQL, placeholders, plans, and transaction
|
|
7
|
+
semantics intact. A small contract above each statement adds named application
|
|
8
|
+
arguments, result cardinality, result shape, timeout, and parameter-free
|
|
9
|
+
telemetry.
|
|
10
|
+
|
|
11
|
+
> **Status: alpha (0.1.x), typed.** SQLite, Cloudflare D1, and
|
|
12
|
+
> asyncpg-compatible PostgreSQL execution are implemented. SQLite/D1 contracts
|
|
13
|
+
> can be compiled against an in-memory schema; PostgreSQL contracts can be
|
|
14
|
+
> prepared against an ephemeral PostgreSQL transaction. The design memo
|
|
15
|
+
> (Japanese, per project convention) is in [DESIGN.md](DESIGN.md).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
uv add hayate-sql
|
|
21
|
+
|
|
22
|
+
# Include asyncpg when PostgreSQL is the target.
|
|
23
|
+
uv add "hayate-sql[postgres]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Write SQL
|
|
27
|
+
|
|
28
|
+
One `.sql` file contains one database-native statement:
|
|
29
|
+
|
|
30
|
+
```sql
|
|
31
|
+
-- name: get_document :one?
|
|
32
|
+
-- param: workspace_id str
|
|
33
|
+
-- param: document_id UUID
|
|
34
|
+
-- column: id UUID
|
|
35
|
+
-- column: title str
|
|
36
|
+
-- column: body str
|
|
37
|
+
-- timeout: 50ms
|
|
38
|
+
SELECT id, title, body
|
|
39
|
+
FROM documents
|
|
40
|
+
WHERE workspace_id = ?1
|
|
41
|
+
AND id = ?2
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The placeholder syntax belongs to the target database. Use `?1` for
|
|
45
|
+
SQLite/D1 and `$1` for PostgreSQL. The declared parameter order maps named
|
|
46
|
+
Python arguments to those native positions.
|
|
47
|
+
|
|
48
|
+
Cardinality is explicit:
|
|
49
|
+
|
|
50
|
+
- `:one` — exactly one row
|
|
51
|
+
- `:one?` — zero or one row
|
|
52
|
+
- `:many` — zero or more rows
|
|
53
|
+
- `:exec` — no result rows; returns `CommandResult`
|
|
54
|
+
|
|
55
|
+
Supported contract types are `str`, `int`, `float`, `bool`, `bytes`, `object`,
|
|
56
|
+
`Any`, `datetime`, `date`, `Decimal`, and `UUID`. Append `?` for a nullable
|
|
57
|
+
value. Types generate Python annotations; runtime enforcement intentionally
|
|
58
|
+
checks the result's column shape rather than pretending that SQLite and
|
|
59
|
+
PostgreSQL use identical value representations. Declarations must therefore
|
|
60
|
+
describe the target driver's returned Python values (for example, a raw
|
|
61
|
+
SQLite boolean column is normally declared as `int` unless the SQL converts it).
|
|
62
|
+
|
|
63
|
+
## Compile and generate
|
|
64
|
+
|
|
65
|
+
Compile D1/SQLite SQL against the real schema without executing queries:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
hayate-sql check queries/ --dialect d1 --schema schema.sql
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For PostgreSQL, `prepare` every query inside a transaction that is always
|
|
72
|
+
rolled back:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
HAYATE_SQL_DATABASE_URL=postgresql://localhost/app_test \
|
|
76
|
+
hayate-sql check queries/postgres/ \
|
|
77
|
+
--dialect postgres \
|
|
78
|
+
--schema schema.sql
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Generate a typed facade:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
hayate-sql generate queries/ -o app/queries.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The generated module intentionally uses only Python 3.12+ syntax and does not
|
|
88
|
+
force postponed annotations. Run the project's formatter after generation
|
|
89
|
+
when its style policy is stricter than the standard output.
|
|
90
|
+
|
|
91
|
+
The generated function has named arguments and a `TypedDict` result:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
document = await queries.get_document(
|
|
95
|
+
database,
|
|
96
|
+
workspace_id=workspace_id,
|
|
97
|
+
document_id=document_id,
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Generated code is a build artifact. SQL remains the source of truth.
|
|
102
|
+
|
|
103
|
+
### Check against migration history
|
|
104
|
+
|
|
105
|
+
When forward-only SQL migrations are the schema source of truth, replay them
|
|
106
|
+
in order before compiling the queries:
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
hayate-sql check queries/ --dialect d1 --migrations migrations/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Migration files use a fixed-width positive number followed by a lowercase
|
|
113
|
+
description, for example `0001_create_documents.sql`. Gaps are allowed, while
|
|
114
|
+
duplicate versions, mixed widths, malformed names, and empty migrations fail
|
|
115
|
+
the check.
|
|
116
|
+
|
|
117
|
+
The migrations run only in hayate-sql's disposable check database. Applying
|
|
118
|
+
them to development, staging, or production remains the responsibility of the
|
|
119
|
+
database-native tool, such as `wrangler d1 migrations apply` or Alembic.
|
|
120
|
+
|
|
121
|
+
## Execute
|
|
122
|
+
|
|
123
|
+
### SQLite
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from hayate_sql.adapters import SQLiteDatabase
|
|
127
|
+
|
|
128
|
+
async with SQLiteDatabase("app.db") as database:
|
|
129
|
+
async with database.transaction(mode="immediate"):
|
|
130
|
+
await queries.create_document(
|
|
131
|
+
database,
|
|
132
|
+
document_id=document_id,
|
|
133
|
+
title=title,
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Cloudflare D1
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from hayate_sql.adapters import D1Database
|
|
141
|
+
|
|
142
|
+
database = D1Database(context.env.DB)
|
|
143
|
+
|
|
144
|
+
# D1 consistency is explicit rather than hidden behind a generic transaction.
|
|
145
|
+
session = database.with_session("first-primary")
|
|
146
|
+
document = await queries.get_document(
|
|
147
|
+
session,
|
|
148
|
+
workspace_id=workspace_id,
|
|
149
|
+
document_id=document_id,
|
|
150
|
+
)
|
|
151
|
+
bookmark = session.bookmark()
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The repository's reproducible workerd probe builds the 0.1.0 wheel, injects
|
|
155
|
+
that wheel into the Python Workers bundle, applies a real local D1 migration,
|
|
156
|
+
and executes `:one`, `:one?`, `:many`, and `:exec`:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
bash scripts/check_workers_d1.sh
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The same probe asserts that telemetry contains neither SQL text nor a sentinel
|
|
163
|
+
bound value.
|
|
164
|
+
|
|
165
|
+
### PostgreSQL
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import asyncpg
|
|
169
|
+
|
|
170
|
+
from hayate_sql.adapters import AsyncpgDatabase
|
|
171
|
+
|
|
172
|
+
connection = await asyncpg.connect(dsn)
|
|
173
|
+
database = AsyncpgDatabase(connection)
|
|
174
|
+
|
|
175
|
+
# Delegates to asyncpg's native transaction context.
|
|
176
|
+
async with database.transaction(isolation="serializable"):
|
|
177
|
+
await queries.update_document(database, document_id=document_id, title=title)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Observe without leaking parameters
|
|
181
|
+
|
|
182
|
+
An observer receives only the query name, contract fingerprint, duration,
|
|
183
|
+
success, row count, and error type. SQL text and bound values are deliberately
|
|
184
|
+
absent.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
def observe(event):
|
|
188
|
+
metrics.histogram("db.query.duration", event.duration_ms, query=event.name)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
database = SQLiteDatabase("app.db", observer=observe)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Non-goals
|
|
195
|
+
|
|
196
|
+
- ORM models, identity maps, lazy relations, or repositories
|
|
197
|
+
- a database-independent SQL DSL or query builder
|
|
198
|
+
- SQL dialect translation
|
|
199
|
+
- a universal transaction abstraction
|
|
200
|
+
- production migration application, implicit schema migration, or write retries
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.9,<1"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hayate-sql"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SQL-first query contracts for Python, PostgreSQL, SQLite, and Cloudflare D1"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Yusuke Hayashi" }]
|
|
14
|
+
keywords = ["hayate", "sql", "postgresql", "sqlite", "cloudflare-d1"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Database",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
postgres = ["asyncpg>=0.30"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
hayate-sql = "hayate_sql.__main__:entrypoint"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/hayatepy/hayate-sql"
|
|
34
|
+
Documentation = "https://github.com/hayatepy/hayate-sql#readme"
|
|
35
|
+
Changelog = "https://github.com/hayatepy/hayate-sql/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"mypy>=1.18",
|
|
40
|
+
"pytest>=8.3",
|
|
41
|
+
"pytest-asyncio>=0.25",
|
|
42
|
+
"ruff>=0.9",
|
|
43
|
+
"workers-py==1.15.0",
|
|
44
|
+
"workers-runtime-sdk==1.6.3",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
python_version = "3.12"
|
|
49
|
+
strict = true
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py312"
|
|
58
|
+
src = ["src", "tests"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""hayate-sql: SQL-first query contracts, not an ORM."""
|
|
2
|
+
|
|
3
|
+
from .contract import (
|
|
4
|
+
Cardinality,
|
|
5
|
+
CardinalityError,
|
|
6
|
+
Column,
|
|
7
|
+
Parameter,
|
|
8
|
+
Query,
|
|
9
|
+
QueryArgumentError,
|
|
10
|
+
QueryContractError,
|
|
11
|
+
RowShapeError,
|
|
12
|
+
)
|
|
13
|
+
from .database import CommandResult, Database, Observer, QueryEvent, Row
|
|
14
|
+
from .parser import QueryParseError, load_queries, parse_query
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Cardinality",
|
|
20
|
+
"CardinalityError",
|
|
21
|
+
"Column",
|
|
22
|
+
"CommandResult",
|
|
23
|
+
"Database",
|
|
24
|
+
"Observer",
|
|
25
|
+
"Parameter",
|
|
26
|
+
"Query",
|
|
27
|
+
"QueryArgumentError",
|
|
28
|
+
"QueryContractError",
|
|
29
|
+
"QueryEvent",
|
|
30
|
+
"QueryParseError",
|
|
31
|
+
"Row",
|
|
32
|
+
"RowShapeError",
|
|
33
|
+
"__version__",
|
|
34
|
+
"load_queries",
|
|
35
|
+
"parse_query",
|
|
36
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The ``hayate-sql`` command line."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import NoReturn
|
|
11
|
+
|
|
12
|
+
from .check import CheckFailure, check_postgres, check_sqlite
|
|
13
|
+
from .generate import generate_module
|
|
14
|
+
from .migrations import load_migrations
|
|
15
|
+
from .parser import load_queries
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: list[str] | None = None) -> int:
|
|
19
|
+
parser = _parser()
|
|
20
|
+
args = parser.parse_args(argv)
|
|
21
|
+
try:
|
|
22
|
+
queries = load_queries([Path(item) for item in args.paths])
|
|
23
|
+
if args.command == "generate":
|
|
24
|
+
output = generate_module([query for _, query in queries])
|
|
25
|
+
Path(args.output).write_text(output)
|
|
26
|
+
print(f"generated {len(queries)} queries in {args.output}")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
schemas = [Path(path).read_text() for path in args.schema]
|
|
30
|
+
migrations = load_migrations(Path(args.migrations)) if args.migrations else ()
|
|
31
|
+
if args.dialect in ("sqlite", "d1"):
|
|
32
|
+
failures = check_sqlite(
|
|
33
|
+
queries,
|
|
34
|
+
schemas=schemas,
|
|
35
|
+
migrations=migrations,
|
|
36
|
+
d1=args.dialect == "d1",
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
database_url = args.database_url or os.environ.get("HAYATE_SQL_DATABASE_URL")
|
|
40
|
+
if database_url is None:
|
|
41
|
+
parser.error("PostgreSQL checks require --database-url or HAYATE_SQL_DATABASE_URL")
|
|
42
|
+
failures = asyncio.run(
|
|
43
|
+
check_postgres(
|
|
44
|
+
queries,
|
|
45
|
+
database_url=database_url,
|
|
46
|
+
schemas=schemas,
|
|
47
|
+
migrations=migrations,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return _report(failures, len(queries))
|
|
51
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
52
|
+
print(f"hayate-sql: {error}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def entrypoint() -> NoReturn:
|
|
57
|
+
raise SystemExit(main())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parser() -> argparse.ArgumentParser:
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog="hayate-sql",
|
|
63
|
+
description="Compile SQL contracts and generate typed Python accessors.",
|
|
64
|
+
)
|
|
65
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
66
|
+
|
|
67
|
+
generate = commands.add_parser("generate", help="generate a typed Python module")
|
|
68
|
+
generate.add_argument("paths", nargs="+", help=".sql files or directories")
|
|
69
|
+
generate.add_argument("-o", "--output", required=True, help="generated .py path")
|
|
70
|
+
|
|
71
|
+
check = commands.add_parser("check", help="compile queries against a real schema")
|
|
72
|
+
check.add_argument("paths", nargs="+", help=".sql files or directories")
|
|
73
|
+
check.add_argument("--dialect", choices=("sqlite", "d1", "postgres"), default="sqlite")
|
|
74
|
+
schema_source = check.add_mutually_exclusive_group()
|
|
75
|
+
schema_source.add_argument(
|
|
76
|
+
"--schema",
|
|
77
|
+
action="append",
|
|
78
|
+
default=[],
|
|
79
|
+
metavar="FILE",
|
|
80
|
+
help="schema SQL to apply before checking; repeatable",
|
|
81
|
+
)
|
|
82
|
+
schema_source.add_argument(
|
|
83
|
+
"--migrations",
|
|
84
|
+
metavar="DIRECTORY",
|
|
85
|
+
help="ordered SQL migrations to replay in a disposable check database",
|
|
86
|
+
)
|
|
87
|
+
check.add_argument(
|
|
88
|
+
"--database-url",
|
|
89
|
+
help="ephemeral PostgreSQL URL; prefer HAYATE_SQL_DATABASE_URL to avoid shell history",
|
|
90
|
+
)
|
|
91
|
+
return parser
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _report(failures: list[CheckFailure], total: int) -> int:
|
|
95
|
+
if not failures:
|
|
96
|
+
print(f"checked {total} queries")
|
|
97
|
+
return 0
|
|
98
|
+
for failure in failures:
|
|
99
|
+
print(f"{failure.path}: {failure.query_name}: {failure.message}", file=sys.stderr)
|
|
100
|
+
print(f"{len(failures)} of {total} queries failed", file=sys.stderr)
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
entrypoint()
|