sqlpiston 0.1.0__py3-none-any.whl
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.
- sqlpiston/__init__.py +27 -0
- sqlpiston/_types.py +5 -0
- sqlpiston/builder/__init__.py +0 -0
- sqlpiston/builder/ddl.py +228 -0
- sqlpiston/builder/dml.py +124 -0
- sqlpiston/builder/nodes.py +248 -0
- sqlpiston/builder/selectable.py +153 -0
- sqlpiston/compiler/__init__.py +0 -0
- sqlpiston/compiler/base.py +581 -0
- sqlpiston/compiler/mysql.py +50 -0
- sqlpiston/compiler/sqlite.py +51 -0
- sqlpiston/core/__init__.py +0 -0
- sqlpiston/core/engine/__init__.py +3 -0
- sqlpiston/core/engine/base.py +99 -0
- sqlpiston/core/engine/mysql.py +80 -0
- sqlpiston/core/engine/sqlite.py +76 -0
- sqlpiston/core/pool.py +49 -0
- sqlpiston/core/session.py +61 -0
- sqlpiston/orm/__init__.py +0 -0
- sqlpiston/orm/mapper.py +68 -0
- sqlpiston-0.1.0.dist-info/METADATA +180 -0
- sqlpiston-0.1.0.dist-info/RECORD +25 -0
- sqlpiston-0.1.0.dist-info/WHEEL +5 -0
- sqlpiston-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlpiston-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from sqlpiston.builder.nodes import ExprValue
|
|
6
|
+
from sqlpiston._types import SQLValue
|
|
7
|
+
from sqlpiston.compiler.base import Dialect
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DBType(Enum):
|
|
11
|
+
MySQL = 1
|
|
12
|
+
SQLite = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Cursor(ABC):
|
|
16
|
+
"""Query result cursor. Wraps DB-API cursor."""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def fetchall(self) -> List[Tuple[SQLValue, ...]]:
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def fetchone(self) -> Optional[Tuple[SQLValue, ...]]:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def rowcount(self) -> int:
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def description(self) -> List[Tuple[str, int, Any, Any, Any, Any, Any]]:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Connection(ABC):
|
|
38
|
+
"""A single database connection."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def execute(self, sql: str, params: Tuple[ExprValue, ...]) -> Cursor:
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def begin(self) -> None:
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def commit(self) -> None:
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def rollback(self) -> None:
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def close(self) -> None:
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def __enter__(self) -> 'Connection': # pragma: no cover — abstract class, covered via subclasses
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, *args: Any) -> None: # pragma: no cover — abstract class, covered via subclasses
|
|
64
|
+
self.close()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Engine(ABC):
|
|
68
|
+
"""Abstract database engine."""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def dialect(self) -> Dialect:
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def connect(self) -> Connection:
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def close(self) -> None:
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def __enter__(self) -> 'Engine': # pragma: no cover — abstract class, covered via subclasses
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def __exit__(self, *args: Any) -> None: # pragma: no cover — abstract class, covered via subclasses
|
|
87
|
+
self.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def DBEngine(db_type: DBType) -> Engine:
|
|
91
|
+
"""Factory: returns typed engine by DBType."""
|
|
92
|
+
if db_type == DBType.MySQL:
|
|
93
|
+
from sqlpiston.core.engine.mysql import MySQLEngine
|
|
94
|
+
return MySQLEngine()
|
|
95
|
+
elif db_type == DBType.SQLite:
|
|
96
|
+
from sqlpiston.core.engine.sqlite import SQLiteEngine
|
|
97
|
+
return SQLiteEngine()
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"Unsupported DB type: {db_type}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Tuple, cast
|
|
2
|
+
|
|
3
|
+
from sqlpiston.builder.nodes import ExprValue
|
|
4
|
+
from sqlpiston._types import SQLValue
|
|
5
|
+
from sqlpiston.compiler.mysql import MySQLDialect
|
|
6
|
+
from sqlpiston.core.engine.base import Connection, Cursor, Engine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MySQLCursor(Cursor): # pragma: no cover — requires mysql.connector (optional dependency)
|
|
10
|
+
def __init__(self, raw_cursor: Any) -> None:
|
|
11
|
+
self._cursor = raw_cursor
|
|
12
|
+
|
|
13
|
+
def fetchall(self) -> List[Tuple[SQLValue, ...]]:
|
|
14
|
+
return [tuple(row) for row in self._cursor.fetchall()]
|
|
15
|
+
|
|
16
|
+
def fetchone(self) -> Optional[Tuple[SQLValue, ...]]:
|
|
17
|
+
row = self._cursor.fetchone()
|
|
18
|
+
return tuple(row) if row else None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def rowcount(self) -> int:
|
|
22
|
+
return int(self._cursor.rowcount)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def description(self) -> List[Tuple[str, int, Any, Any, Any, Any, Any]]: # pragma: no cover — requires mysql.connector
|
|
26
|
+
desc = self._cursor.description
|
|
27
|
+
if desc is None:
|
|
28
|
+
return []
|
|
29
|
+
return cast(List[Tuple[str, int, Any, Any, Any, Any, Any]], desc)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MySQLConnection(Connection): # pragma: no cover — requires mysql.connector (optional dependency)
|
|
33
|
+
def __init__(self, raw_conn: Any) -> None:
|
|
34
|
+
self._conn = raw_conn
|
|
35
|
+
|
|
36
|
+
def execute(self, sql: str, params: Tuple[ExprValue, ...]) -> MySQLCursor:
|
|
37
|
+
cursor = self._conn.cursor()
|
|
38
|
+
cursor.execute(sql, params)
|
|
39
|
+
return MySQLCursor(cursor)
|
|
40
|
+
|
|
41
|
+
def begin(self) -> None:
|
|
42
|
+
self._conn.begin()
|
|
43
|
+
|
|
44
|
+
def commit(self) -> None:
|
|
45
|
+
self._conn.commit()
|
|
46
|
+
|
|
47
|
+
def rollback(self) -> None:
|
|
48
|
+
self._conn.rollback()
|
|
49
|
+
|
|
50
|
+
def close(self) -> None:
|
|
51
|
+
self._conn.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MySQLEngine(Engine):
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._config: Optional[Dict[str, Any]] = None
|
|
57
|
+
self._conn: Any = None
|
|
58
|
+
|
|
59
|
+
def init_engine(self, host: str, port: int, user: str, password: str, database: str) -> None:
|
|
60
|
+
self._config = {
|
|
61
|
+
'host': host, 'port': port,
|
|
62
|
+
'user': user, 'password': password,
|
|
63
|
+
'database': database,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def dialect(self) -> MySQLDialect:
|
|
68
|
+
return MySQLDialect()
|
|
69
|
+
|
|
70
|
+
def connect(self) -> MySQLConnection: # pragma: no cover — requires mysql.connector (optional dependency)
|
|
71
|
+
if self._config is None:
|
|
72
|
+
raise RuntimeError("Call init_engine() before connect()")
|
|
73
|
+
import mysql.connector # type: ignore[import-not-found]
|
|
74
|
+
self._conn = mysql.connector.connect(**self._config)
|
|
75
|
+
return MySQLConnection(self._conn)
|
|
76
|
+
|
|
77
|
+
def close(self) -> None: # pragma: no cover — requires mysql.connector
|
|
78
|
+
if self._conn:
|
|
79
|
+
self._conn.close()
|
|
80
|
+
self._conn = None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from typing import Any, List, Optional, Tuple, cast
|
|
3
|
+
|
|
4
|
+
from sqlpiston.builder.nodes import ExprValue
|
|
5
|
+
from sqlpiston._types import SQLValue
|
|
6
|
+
from sqlpiston.compiler.sqlite import SQLiteDialect
|
|
7
|
+
from sqlpiston.core.engine.base import Connection, Cursor, Engine
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SQLiteCursor(Cursor):
|
|
11
|
+
def __init__(self, raw_cursor: sqlite3.Cursor) -> None:
|
|
12
|
+
self._cursor = raw_cursor
|
|
13
|
+
|
|
14
|
+
def fetchall(self) -> List[Tuple[SQLValue, ...]]:
|
|
15
|
+
return [tuple(row) for row in self._cursor.fetchall()]
|
|
16
|
+
|
|
17
|
+
def fetchone(self) -> Optional[Tuple[SQLValue, ...]]:
|
|
18
|
+
row = self._cursor.fetchone()
|
|
19
|
+
return tuple(row) if row else None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def rowcount(self) -> int:
|
|
23
|
+
return self._cursor.rowcount
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def description(self) -> List[Tuple[str, int, Any, Any, Any, Any, Any]]:
|
|
27
|
+
desc = self._cursor.description
|
|
28
|
+
if desc is None:
|
|
29
|
+
return []
|
|
30
|
+
return cast(List[Tuple[str, int, Any, Any, Any, Any, Any]], [tuple(d) for d in desc])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SQLiteConnection(Connection):
|
|
34
|
+
def __init__(self, raw_conn: sqlite3.Connection) -> None:
|
|
35
|
+
self._conn = raw_conn
|
|
36
|
+
|
|
37
|
+
def execute(self, sql: str, params: Tuple[ExprValue, ...]) -> SQLiteCursor:
|
|
38
|
+
cursor = self._conn.cursor()
|
|
39
|
+
cursor.execute(sql, params)
|
|
40
|
+
return SQLiteCursor(cursor)
|
|
41
|
+
|
|
42
|
+
def begin(self) -> None:
|
|
43
|
+
self._conn.execute("BEGIN")
|
|
44
|
+
|
|
45
|
+
def commit(self) -> None:
|
|
46
|
+
self._conn.commit()
|
|
47
|
+
|
|
48
|
+
def rollback(self) -> None:
|
|
49
|
+
self._conn.rollback()
|
|
50
|
+
|
|
51
|
+
def close(self) -> None:
|
|
52
|
+
self._conn.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SQLiteEngine(Engine):
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._file_path: Optional[str] = None
|
|
58
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
59
|
+
|
|
60
|
+
def init_engine(self, file_path: str) -> None:
|
|
61
|
+
self._file_path = file_path
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def dialect(self) -> SQLiteDialect:
|
|
65
|
+
return SQLiteDialect()
|
|
66
|
+
|
|
67
|
+
def connect(self) -> SQLiteConnection:
|
|
68
|
+
if self._file_path is None:
|
|
69
|
+
raise RuntimeError("Call init_engine() before connect()")
|
|
70
|
+
self._conn = sqlite3.connect(self._file_path)
|
|
71
|
+
return SQLiteConnection(self._conn)
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
if self._conn:
|
|
75
|
+
self._conn.close()
|
|
76
|
+
self._conn = None
|
sqlpiston/core/pool.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Any, List
|
|
3
|
+
|
|
4
|
+
from sqlpiston.core.engine.base import Connection, Engine
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConnectionPool:
|
|
8
|
+
"""Thread-safe connection pool."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, engine: Engine, min_size: int = 1, max_size: int = 10) -> None:
|
|
11
|
+
self._engine = engine
|
|
12
|
+
self._min_size = min_size
|
|
13
|
+
self._max_size = max_size
|
|
14
|
+
self._lock = threading.Lock()
|
|
15
|
+
self._available: List[Connection] = []
|
|
16
|
+
self._in_use: int = 0
|
|
17
|
+
|
|
18
|
+
for _ in range(min_size):
|
|
19
|
+
self._available.append(engine.connect())
|
|
20
|
+
|
|
21
|
+
def acquire(self) -> Connection:
|
|
22
|
+
with self._lock:
|
|
23
|
+
if self._available:
|
|
24
|
+
conn = self._available.pop()
|
|
25
|
+
self._in_use += 1
|
|
26
|
+
return conn
|
|
27
|
+
if self._in_use >= self._max_size:
|
|
28
|
+
raise RuntimeError("Connection pool exhausted")
|
|
29
|
+
conn = self._engine.connect()
|
|
30
|
+
self._in_use += 1
|
|
31
|
+
return conn
|
|
32
|
+
|
|
33
|
+
def release(self, conn: Connection) -> None:
|
|
34
|
+
with self._lock:
|
|
35
|
+
self._in_use -= 1
|
|
36
|
+
self._available.append(conn)
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
with self._lock:
|
|
40
|
+
for conn in self._available:
|
|
41
|
+
conn.close()
|
|
42
|
+
self._available.clear()
|
|
43
|
+
self._in_use = 0
|
|
44
|
+
|
|
45
|
+
def __enter__(self) -> 'ConnectionPool':
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def __exit__(self, *args: Any) -> None:
|
|
49
|
+
self.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from sqlpiston.builder.nodes import ASTNode
|
|
4
|
+
from sqlpiston.core.engine.base import Connection, Engine
|
|
5
|
+
from sqlpiston.core.pool import ConnectionPool
|
|
6
|
+
from sqlpiston.orm.mapper import ResultSet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Session:
|
|
10
|
+
"""Entry point for users. Binds engine, compiles AST, executes SQL.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
s = Session(engine)
|
|
15
|
+
result = s.execute(
|
|
16
|
+
Select().select('id', 'name').from_table('users').where(Field('age') >= 18)
|
|
17
|
+
)
|
|
18
|
+
s.commit()
|
|
19
|
+
s.close()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, engine: Engine, pool: Optional[ConnectionPool] = None) -> None:
|
|
23
|
+
self._engine = engine
|
|
24
|
+
self._pool = pool if pool is not None else ConnectionPool(engine)
|
|
25
|
+
self._current_conn: Optional[Connection] = None
|
|
26
|
+
|
|
27
|
+
def _get_conn(self) -> Connection:
|
|
28
|
+
if self._current_conn is None:
|
|
29
|
+
self._current_conn = self._pool.acquire()
|
|
30
|
+
return self._current_conn
|
|
31
|
+
|
|
32
|
+
def execute(self, stmt: ASTNode) -> ResultSet:
|
|
33
|
+
conn = self._get_conn()
|
|
34
|
+
sql, params = stmt.compile(self._engine.dialect)
|
|
35
|
+
cursor = conn.execute(sql, params)
|
|
36
|
+
column_names = [desc[0] for desc in cursor.description] if cursor.description else []
|
|
37
|
+
return ResultSet(cursor, column_names)
|
|
38
|
+
|
|
39
|
+
def begin(self) -> None:
|
|
40
|
+
conn = self._get_conn()
|
|
41
|
+
conn.begin()
|
|
42
|
+
|
|
43
|
+
def commit(self) -> None:
|
|
44
|
+
if self._current_conn:
|
|
45
|
+
self._current_conn.commit()
|
|
46
|
+
|
|
47
|
+
def rollback(self) -> None:
|
|
48
|
+
if self._current_conn:
|
|
49
|
+
self._current_conn.rollback()
|
|
50
|
+
|
|
51
|
+
def close(self) -> None:
|
|
52
|
+
if self._current_conn:
|
|
53
|
+
self._pool.release(self._current_conn)
|
|
54
|
+
self._current_conn = None
|
|
55
|
+
self._pool.close()
|
|
56
|
+
|
|
57
|
+
def __enter__(self) -> 'Session':
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __exit__(self, *args: Any) -> None:
|
|
61
|
+
self.close()
|
|
File without changes
|
sqlpiston/orm/mapper.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any, Dict, Iterator, List, Optional, Type, TypeVar,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from sqlpiston._types import SQLValue
|
|
6
|
+
from sqlpiston.core.engine.base import Cursor
|
|
7
|
+
|
|
8
|
+
T = TypeVar('T')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ResultSet:
|
|
12
|
+
"""Iterable set of mapped results from a query."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, cursor: Cursor, column_names: List[str]) -> None:
|
|
15
|
+
self._cursor = cursor
|
|
16
|
+
self._column_names = column_names
|
|
17
|
+
|
|
18
|
+
def all(self) -> List[Dict[str, SQLValue]]:
|
|
19
|
+
rows = self._cursor.fetchall()
|
|
20
|
+
return [dict(zip(self._column_names, row)) for row in rows]
|
|
21
|
+
|
|
22
|
+
def one(self) -> Dict[str, SQLValue]:
|
|
23
|
+
rows = self.all()
|
|
24
|
+
if len(rows) == 0:
|
|
25
|
+
raise ValueError("Expected exactly one row, got none")
|
|
26
|
+
if len(rows) > 1:
|
|
27
|
+
raise ValueError(f"Expected exactly one row, got {len(rows)}")
|
|
28
|
+
return rows[0]
|
|
29
|
+
|
|
30
|
+
def one_or_none(self) -> Optional[Dict[str, SQLValue]]:
|
|
31
|
+
rows = self.all()
|
|
32
|
+
if len(rows) == 0:
|
|
33
|
+
return None
|
|
34
|
+
if len(rows) > 1:
|
|
35
|
+
raise ValueError(f"Expected at most one row, got {len(rows)}")
|
|
36
|
+
return rows[0]
|
|
37
|
+
|
|
38
|
+
def first(self) -> Optional[Dict[str, SQLValue]]:
|
|
39
|
+
row = self._cursor.fetchone()
|
|
40
|
+
if row is None:
|
|
41
|
+
return None
|
|
42
|
+
return dict(zip(self._column_names, row))
|
|
43
|
+
|
|
44
|
+
def scalar(self) -> Any:
|
|
45
|
+
row = self._cursor.fetchone()
|
|
46
|
+
if row is None:
|
|
47
|
+
raise ValueError("Expected at least one row for scalar(), got none")
|
|
48
|
+
return row[0]
|
|
49
|
+
|
|
50
|
+
def map(self, target: Type[T]) -> List[T]:
|
|
51
|
+
rows = self.all()
|
|
52
|
+
results: List[T] = []
|
|
53
|
+
for row in rows:
|
|
54
|
+
obj = target(**row)
|
|
55
|
+
results.append(obj)
|
|
56
|
+
return results
|
|
57
|
+
|
|
58
|
+
def map_one(self, target: Type[T]) -> T:
|
|
59
|
+
row = self.one()
|
|
60
|
+
return target(**row)
|
|
61
|
+
|
|
62
|
+
def __iter__(self) -> Iterator[Dict[str, SQLValue]]:
|
|
63
|
+
all_rows = self.all()
|
|
64
|
+
return iter(all_rows)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def rowcount(self) -> int:
|
|
68
|
+
return self._cursor.rowcount
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlpiston
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A low-level SQL library that builds parameterized SQL queries through Python operator overloading
|
|
5
|
+
Author: MuliMuri
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: sql,query-builder,orm,mysql,sqlite
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
23
|
+
Requires-Dist: coverage>=7; extra == "dev"
|
|
24
|
+
Requires-Dist: flake8>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=1; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# SQLPiston
|
|
29
|
+
|
|
30
|
+
[](https://github.com/MuliMuri/sqlpiston/actions/workflows/test.yml)
|
|
31
|
+
[](https://github.com/MuliMuri/sqlpiston/actions/workflows/lint.yml)
|
|
32
|
+
[](https://codecov.io/gh/MuliMuri/sqlpiston)
|
|
33
|
+
[](https://pypi.org/project/sqlpiston/)
|
|
34
|
+
[](https://pypi.org/project/sqlpiston/)
|
|
35
|
+
[](https://github.com/MuliMuri/sqlpiston/blob/main/LICENSE)
|
|
36
|
+
|
|
37
|
+
*Write once, query everywhere — build SQL with Python operators.*
|
|
38
|
+
|
|
39
|
+
[中文版](README_ZH.md)
|
|
40
|
+
|
|
41
|
+
SQLPiston is a low-level SQL library that builds parameterized SQL queries
|
|
42
|
+
through Python operator overloading. AST nodes carry zero SQL knowledge;
|
|
43
|
+
dialect-specific compilers translate the same AST into the right SQL for each
|
|
44
|
+
database.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/MuliMuri/sqlpiston.git
|
|
50
|
+
cd sqlpiston
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Python 3.9+ on Linux / macOS / Windows.
|
|
55
|
+
|
|
56
|
+
## Basic Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from sqlpiston import Select, Field, Insert
|
|
60
|
+
from sqlpiston import DBEngine, DBType, Session
|
|
61
|
+
from dataclasses import dataclass
|
|
62
|
+
|
|
63
|
+
eng = DBEngine(DBType.SQLite)
|
|
64
|
+
eng.init_engine(":memory:")
|
|
65
|
+
session = Session(eng)
|
|
66
|
+
|
|
67
|
+
stmt = (
|
|
68
|
+
Select()
|
|
69
|
+
.select("id", "name", "age")
|
|
70
|
+
.from_table("users")
|
|
71
|
+
.where((Field("age") >= 18) & (Field("status") == "active"))
|
|
72
|
+
.order_by("id", "ASC")
|
|
73
|
+
.limit(10)
|
|
74
|
+
)
|
|
75
|
+
result = session.execute(stmt)
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class User:
|
|
79
|
+
id: int
|
|
80
|
+
name: str
|
|
81
|
+
age: int
|
|
82
|
+
|
|
83
|
+
users = result.map(User)
|
|
84
|
+
|
|
85
|
+
session.execute(
|
|
86
|
+
Insert().into("users").values({"name": "Alice", "age": 25})
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
session.commit()
|
|
90
|
+
session.close()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
<details>
|
|
94
|
+
<summary>More examples: JOIN, subquery, CTE, UPSERT, DDL</summary>
|
|
95
|
+
|
|
96
|
+
**JOIN**
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
Select() \
|
|
100
|
+
.select("users.name", "orders.total") \
|
|
101
|
+
.from_table("users") \
|
|
102
|
+
.join("orders",
|
|
103
|
+
Field("user_id", "orders") == Field("id", "users")) \
|
|
104
|
+
.where(Field("total", "orders") > 100)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Subquery (IN / EXISTS / Scalar)**
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# IN subquery
|
|
111
|
+
admin_ids = Select().select("id").from_table("admins")
|
|
112
|
+
stmt = Select().select("name").from_table("users") \
|
|
113
|
+
.where(Field("id").is_in(admin_ids))
|
|
114
|
+
|
|
115
|
+
# EXISTS
|
|
116
|
+
sub = Select().select("1").from_table("orders") \
|
|
117
|
+
.where(Field("user_id", "orders") == Field("id", "users"))
|
|
118
|
+
stmt = Select().select("name").from_table("users").where(sub.exists())
|
|
119
|
+
|
|
120
|
+
# Scalar
|
|
121
|
+
avg = Select().select(SQLFunction("avg", "salary")).from_table("employees")
|
|
122
|
+
stmt = Select().select("name").from_table("staff").where(Field("salary") > avg)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**CTE**
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
cte = Select().select("*").from_table("sales") \
|
|
129
|
+
.where(Field("amount") > 100).cte("big_sales")
|
|
130
|
+
stmt = Select().with_(cte).select("*").from_table("big_sales")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**UPSERT — same AST, different SQL per dialect**
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
Upsert() \
|
|
137
|
+
.into("users") \
|
|
138
|
+
.values({"id": 1, "name": "X"}) \
|
|
139
|
+
.on_conflict("id") \
|
|
140
|
+
.do_update({"name": "X"})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Dialect | Generated SQL |
|
|
144
|
+
|---------|--------------|
|
|
145
|
+
| MySQL | `INSERT INTO ... ON DUPLICATE KEY UPDATE` |
|
|
146
|
+
| SQLite | `INSERT INTO ... ON CONFLICT DO UPDATE` |
|
|
147
|
+
|
|
148
|
+
**DDL**
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
CreateTable().table("users").if_not_exists() \
|
|
152
|
+
.column("id", "INTEGER", primary_key=True, nullable=False) \
|
|
153
|
+
.column("name", "VARCHAR(255)")
|
|
154
|
+
|
|
155
|
+
AlterTable().table("users").add_column("age", "INTEGER", default=0)
|
|
156
|
+
|
|
157
|
+
DropTable().table("users").if_exists()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
</details>
|
|
161
|
+
|
|
162
|
+
## Highlights
|
|
163
|
+
|
|
164
|
+
- **Operator overloading** — `Field("age") >= 18` builds AST, not a boolean
|
|
165
|
+
- **Dialect-aware** — same AST compiles to MySQL (%s, backticks) or SQLite (?, double-quotes)
|
|
166
|
+
- **Full standard SQL** — SELECT, INSERT, UPDATE, DELETE, UPSERT, CTE, UNION, subqueries, DDL
|
|
167
|
+
- **Parameterized by design** — values never interpolated into SQL strings
|
|
168
|
+
- **Weak ORM** — result-to-dataclass mapping, no identity map or change tracking
|
|
169
|
+
|
|
170
|
+
## Documentation
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
cd docs
|
|
174
|
+
pip install -r requirements.txt
|
|
175
|
+
sphinx-build -b html source build/html
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
sqlpiston/__init__.py,sha256=3ghxUHksL7Hf7K7z32-qK-X6hK6GmjLo5uEhVetOMEg,1054
|
|
2
|
+
sqlpiston/_types.py,sha256=khQZ0rFi-wAxXF829o6Pj_GQEOfWqZt0uLTirIA0amA,125
|
|
3
|
+
sqlpiston/builder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
sqlpiston/builder/ddl.py,sha256=LwzUlJLhh163ndZoJ7yPSW2Wz8gSzErJ-jsqXnV7_QI,6414
|
|
5
|
+
sqlpiston/builder/dml.py,sha256=pkU5pINsQown4UY04I44mICPvgLxXUF4NRlZGiWu0Eo,3540
|
|
6
|
+
sqlpiston/builder/nodes.py,sha256=XP30lmbUlHWnPT0_qvXMuAkUkN2SnuxDEnf4oIAARDA,8072
|
|
7
|
+
sqlpiston/builder/selectable.py,sha256=B7c0hogEiFyyu3SRzdm85Ok4xU3fhy7VfMZwgKlQ638,5010
|
|
8
|
+
sqlpiston/compiler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
sqlpiston/compiler/base.py,sha256=iSVVLK8NEjnxEhE0vjI_iRdpOBjFv5nIk1kAVqerAAg,24502
|
|
10
|
+
sqlpiston/compiler/mysql.py,sha256=cbixJVUae699ht9Hx7zm5xqLcvSvhNPiRiPH9MkBJRI,2007
|
|
11
|
+
sqlpiston/compiler/sqlite.py,sha256=ccRxvrnaIXzwz6xt1jjdstbQfUaAuQN-BJKxj5UrlMk,2134
|
|
12
|
+
sqlpiston/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
sqlpiston/core/pool.py,sha256=g69X70znqPgKjk_XNQm8w6I61KyV8IDVrQtPXrW3aHA,1434
|
|
14
|
+
sqlpiston/core/session.py,sha256=1YMpdRE-kdaYkfgMTy-G2ZqGJMlmgP_feyyv96UBbHk,1861
|
|
15
|
+
sqlpiston/core/engine/__init__.py,sha256=BTeVrKTDJuE_EENZKLv-UEkIo4d2MynUBA9xwkTNzvk,90
|
|
16
|
+
sqlpiston/core/engine/base.py,sha256=Vc72j9B4dhwChnAoCN4K3ukNXb2E_ofUuafNIqCALFo,2347
|
|
17
|
+
sqlpiston/core/engine/mysql.py,sha256=Ofb_Lo_0bNhun3jD27lUnXwSL9V-I5gkpRXHJgM861c,2734
|
|
18
|
+
sqlpiston/core/engine/sqlite.py,sha256=s_QKIp2V7sBF4myvuNMrYlgUg-3jwyur2IFyDVD46A4,2273
|
|
19
|
+
sqlpiston/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
sqlpiston/orm/mapper.py,sha256=VzL9Hj-7jIS6eNKWhJvZ4RYSB7CjSnl8wsCZvU2xnBk,2010
|
|
21
|
+
sqlpiston-0.1.0.dist-info/licenses/LICENSE,sha256=FKq1ES92GFfgEWVFaOrUwjYp5Vw1Ax7erfJD-hf8-T0,1065
|
|
22
|
+
sqlpiston-0.1.0.dist-info/METADATA,sha256=hYCIoJfNuP0CCOMcxW2H6dj8YSrgyiRHMh6WPXt_S1w,5206
|
|
23
|
+
sqlpiston-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
24
|
+
sqlpiston-0.1.0.dist-info/top_level.txt,sha256=FrpiXMbeu-XF58dz1QZcE1FRBGK-GY2oA_m-E0uEtBw,10
|
|
25
|
+
sqlpiston-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MuliMuri
|
|
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 @@
|
|
|
1
|
+
sqlpiston
|