ns-orm 0.0.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.
ns_orm/dialects.py ADDED
@@ -0,0 +1,290 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, Iterable, Mapping, Optional
6
+
7
+ from ns_orm.exceptions import ConfigurationError
8
+
9
+ _PARAM_RE = re.compile(r":([a-zA-Z_]\w*)")
10
+ _DIALECTS: Dict[str, Dialect] = {}
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PreparedSQL:
15
+ sql: str
16
+ params: Any
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Dialect:
21
+ name: str
22
+ paramstyle: str
23
+ quote_char: str = '"'
24
+
25
+ def quote_ident(self, ident: str) -> str:
26
+ q = self.quote_char
27
+ escaped = ident.replace(q, q + q)
28
+ return f"{q}{escaped}{q}"
29
+
30
+ def apply_limit_offset(
31
+ self, sql: str, limit: Optional[int], offset: Optional[int]
32
+ ) -> str:
33
+ if limit is None and offset is None:
34
+ return sql
35
+ if limit is None:
36
+ return f"{sql} LIMIT -1 OFFSET {offset}"
37
+ if offset is None:
38
+ return f"{sql} LIMIT {limit}"
39
+ return f"{sql} LIMIT {limit} OFFSET {offset}"
40
+
41
+ def prepare(self, sql: str, params: Mapping[str, Any]) -> PreparedSQL:
42
+ style = self.paramstyle
43
+ if not params:
44
+ return PreparedSQL(
45
+ sql=sql, params=() if style in {"qmark", "format"} else {}
46
+ )
47
+
48
+ keys_in_order = _PARAM_RE.findall(sql)
49
+ if not keys_in_order:
50
+ return PreparedSQL(sql=sql, params=params)
51
+
52
+ if style == "named":
53
+ return PreparedSQL(sql=sql, params=dict(params))
54
+
55
+ if style == "pyformat":
56
+ sql2 = _PARAM_RE.sub(lambda m: f"%({m.group(1)})s", sql)
57
+ return PreparedSQL(sql=sql2, params=dict(params))
58
+
59
+ if style == "qmark":
60
+ sql2 = _PARAM_RE.sub("?", sql)
61
+ return PreparedSQL(sql=sql2, params=tuple(params[k] for k in keys_in_order))
62
+
63
+ if style == "format":
64
+ sql2 = _PARAM_RE.sub("%s", sql)
65
+ return PreparedSQL(sql=sql2, params=tuple(params[k] for k in keys_in_order))
66
+
67
+ if style == "numeric":
68
+ mapping: dict[str, int] = {}
69
+ out_params: list[Any] = []
70
+
71
+ def _sub(m: re.Match[str]) -> str:
72
+ key = m.group(1)
73
+ if key not in mapping:
74
+ mapping[key] = len(out_params) + 1
75
+ out_params.append(params[key])
76
+ return f":{mapping[key]}"
77
+
78
+ sql2 = _PARAM_RE.sub(_sub, sql)
79
+ return PreparedSQL(sql=sql2, params=tuple(out_params))
80
+
81
+ raise ConfigurationError(f"Unsupported paramstyle: {style}")
82
+
83
+ def type_sql(self, field_def: Any) -> str:
84
+ render = getattr(field_def, "render_type", None)
85
+ if callable(render):
86
+ return str(render(self))
87
+
88
+ sa_type = getattr(field_def, "sa_type", None)
89
+ if sa_type is None:
90
+ raise ConfigurationError(f"Field has no type: {field_def}")
91
+
92
+ if sa_type == "VARCHAR":
93
+ max_length = getattr(field_def, "max_length", 255)
94
+ return f"VARCHAR({int(max_length)})"
95
+ if sa_type == "CHAR":
96
+ length = getattr(field_def, "length", 1)
97
+ return f"CHAR({int(length)})"
98
+ if sa_type == "BINARY":
99
+ length = getattr(field_def, "length", None)
100
+ return "BLOB" if length is None else f"BINARY({int(length)})"
101
+ if sa_type == "DECIMAL":
102
+ precision = getattr(field_def, "precision", 18)
103
+ scale = getattr(field_def, "scale", 6)
104
+ return f"DECIMAL({int(precision)},{int(scale)})"
105
+ if sa_type in {
106
+ "TINYINT",
107
+ "SMALLINT",
108
+ "INTEGER",
109
+ "BIGINT",
110
+ "REAL",
111
+ "DOUBLE",
112
+ "FLOAT",
113
+ "BOOLEAN",
114
+ "TEXT",
115
+ "DATE",
116
+ "JSON",
117
+ "UUID",
118
+ }:
119
+ return sa_type
120
+ if sa_type == "DATETIME":
121
+ return "TIMESTAMP"
122
+
123
+ return str(sa_type)
124
+
125
+ def ddl_create_table(
126
+ self,
127
+ *,
128
+ table_name: str,
129
+ columns: list[tuple[str, Any]],
130
+ pk_name: str,
131
+ fks: list[tuple[str, str, str, str]],
132
+ uniques: Iterable[str],
133
+ unique_together: Iterable[tuple[str, ...]] = (),
134
+ ) -> str:
135
+ qn = self.quote_ident(table_name)
136
+ col_sql: list[str] = []
137
+ for name, fdef in columns:
138
+ parts = [self.quote_ident(name), self.type_sql(fdef)]
139
+ if getattr(fdef, "primary_key", False):
140
+ parts.append("PRIMARY KEY")
141
+ nullable = getattr(fdef, "nullable", None)
142
+ if nullable is False:
143
+ parts.append("NOT NULL")
144
+ if getattr(fdef, "unique", False):
145
+ parts.append("UNIQUE")
146
+ col_sql.append(" ".join(parts))
147
+
148
+ for from_col, to_table, to_col, on_delete in fks:
149
+ col_sql.append(
150
+ " ".join(
151
+ [
152
+ "FOREIGN KEY",
153
+ f"({self.quote_ident(from_col)})",
154
+ "REFERENCES",
155
+ f"{self.quote_ident(to_table)}({self.quote_ident(to_col)})",
156
+ "ON DELETE",
157
+ on_delete,
158
+ ]
159
+ )
160
+ )
161
+
162
+ for col in uniques:
163
+ col_sql.append(f"UNIQUE ({self.quote_ident(col)})")
164
+
165
+ for cols in unique_together:
166
+ col_sql.append(
167
+ "UNIQUE (" + ", ".join(self.quote_ident(c) for c in cols) + ")"
168
+ )
169
+
170
+ return f"CREATE TABLE IF NOT EXISTS {qn} (\n " + ",\n ".join(col_sql) + "\n)"
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class ClickHouseDialect(Dialect):
175
+ def type_sql(self, field_def: Any) -> str:
176
+ render = getattr(field_def, "render_type", None)
177
+ if callable(render):
178
+ return str(render(self))
179
+
180
+ sa_type = getattr(field_def, "sa_type", None)
181
+ if sa_type is None:
182
+ raise ConfigurationError(f"Field has no type: {field_def}")
183
+
184
+ mapping = {
185
+ "TINYINT": "Int8",
186
+ "SMALLINT": "Int16",
187
+ "INTEGER": "Int32",
188
+ "BIGINT": "Int64",
189
+ "UTINYINT": "UInt8",
190
+ "USMALLINT": "UInt16",
191
+ "UINTEGER": "UInt32",
192
+ "UBIGINT": "UInt64",
193
+ "FLOAT": "Float32",
194
+ "REAL": "Float32",
195
+ "DOUBLE": "Float64",
196
+ "BOOLEAN": "Bool",
197
+ "TEXT": "String",
198
+ "VARCHAR": "String",
199
+ "CHAR": "String",
200
+ "DATE": "Date",
201
+ "DATETIME": "DateTime",
202
+ "UUID": "UUID",
203
+ "JSON": "JSON",
204
+ }
205
+ if sa_type == "DECIMAL":
206
+ precision = getattr(field_def, "precision", 18)
207
+ scale = getattr(field_def, "scale", 6)
208
+ return f"Decimal({int(precision)},{int(scale)})"
209
+ return mapping.get(str(sa_type), str(sa_type))
210
+
211
+ def ddl_create_table(
212
+ self,
213
+ *,
214
+ table_name: str,
215
+ columns: list[tuple[str, Any]],
216
+ pk_name: str,
217
+ fks: list[tuple[str, str, str, str]],
218
+ uniques: Iterable[str],
219
+ unique_together: Iterable[tuple[str, ...]] = (),
220
+ ) -> str:
221
+ qn = self.quote_ident(table_name)
222
+ col_sql: list[str] = []
223
+ for name, fdef in columns:
224
+ parts = [self.quote_ident(name), self.type_sql(fdef)]
225
+ nullable = getattr(fdef, "nullable", None)
226
+ if nullable is False:
227
+ parts.append("NOT NULL")
228
+ col_sql.append(" ".join(parts))
229
+
230
+ columns_ddl = ",\n ".join(col_sql)
231
+ return (
232
+ f"CREATE TABLE IF NOT EXISTS {qn} (\n {columns_ddl}\n)"
233
+ " ENGINE = MergeTree() ORDER BY tuple()"
234
+ )
235
+
236
+
237
+ SQLITE_DIALECT = Dialect(name="sqlite", paramstyle="qmark", quote_char='"')
238
+ POSTGRES_DIALECT = Dialect(name="postgres", paramstyle="pyformat", quote_char='"')
239
+ MYSQL_DIALECT = Dialect(name="mysql", paramstyle="pyformat", quote_char="`")
240
+ MSSQL_DIALECT = Dialect(name="mssql", paramstyle="qmark", quote_char='"')
241
+ ORACLE_DIALECT = Dialect(name="oracle", paramstyle="named", quote_char='"')
242
+ DUCKDB_DIALECT = Dialect(name="duckdb", paramstyle="qmark", quote_char='"')
243
+ CLICKHOUSE_DIALECT = ClickHouseDialect(
244
+ name="clickhouse", paramstyle="pyformat", quote_char="`"
245
+ )
246
+
247
+ _DIALECTS.update(
248
+ {
249
+ "sqlite": SQLITE_DIALECT,
250
+ "postgres": POSTGRES_DIALECT,
251
+ "postgresql": POSTGRES_DIALECT,
252
+ "mysql": MYSQL_DIALECT,
253
+ "mssql": MSSQL_DIALECT,
254
+ "sqlserver": MSSQL_DIALECT,
255
+ "oracle": ORACLE_DIALECT,
256
+ "duckdb": DUCKDB_DIALECT,
257
+ "clickhouse": CLICKHOUSE_DIALECT,
258
+ "ch": CLICKHOUSE_DIALECT,
259
+ }
260
+ )
261
+
262
+
263
+ def register_dialect(name: str, dialect: Dialect) -> None:
264
+ _DIALECTS[name.lower()] = dialect
265
+
266
+
267
+ def get_dialect(name: str) -> Dialect:
268
+ key = name.lower()
269
+ if key in _DIALECTS:
270
+ return _DIALECTS[key]
271
+ return Dialect(name=key, paramstyle="named", quote_char='"')
272
+
273
+
274
+ def _normalize_scheme(scheme: str) -> str:
275
+ scheme = scheme.strip().lower()
276
+ if "+" in scheme:
277
+ scheme = scheme.split("+", 1)[0]
278
+ return scheme
279
+
280
+
281
+ def dialect_from_url(scheme: str) -> Dialect:
282
+ scheme = _normalize_scheme(scheme)
283
+ if scheme.startswith(("mariadb", "tidb")):
284
+ scheme = "mysql"
285
+ if scheme.startswith(("cockroachdb", "redshift")):
286
+ scheme = "postgres"
287
+ for key, d in _DIALECTS.items():
288
+ if scheme.startswith(key):
289
+ return d
290
+ return get_dialect(scheme)
ns_orm/exceptions.py ADDED
@@ -0,0 +1,26 @@
1
+ class OrmError(Exception):
2
+ pass
3
+
4
+
5
+ class ConfigurationError(OrmError):
6
+ pass
7
+
8
+
9
+ class ModelDefinitionError(OrmError):
10
+ pass
11
+
12
+
13
+ class QueryError(OrmError):
14
+ pass
15
+
16
+
17
+ class DoesNotExist(OrmError):
18
+ pass
19
+
20
+
21
+ class MultipleObjectsReturned(OrmError):
22
+ pass
23
+
24
+
25
+ class IntegrityError(OrmError):
26
+ pass
ns_orm/expressions.py ADDED
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Iterable, List, Mapping, Tuple, Union
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Condition:
9
+ sql: str
10
+ params: Mapping[str, Any]
11
+
12
+
13
+ def _merge_params(*parts: Mapping[str, Any]) -> dict[str, Any]:
14
+ merged: dict[str, Any] = {}
15
+ for p in parts:
16
+ for k, v in p.items():
17
+ if k in merged:
18
+ raise ValueError(f"Duplicate param: {k}")
19
+ merged[k] = v
20
+ return merged
21
+
22
+
23
+ class Q:
24
+ __slots__ = ("_op", "_children", "_negated")
25
+
26
+ def __init__(self, **lookups: Any):
27
+ self._op: str = "AND"
28
+ self._children: List[Union[Q, Tuple[str, Any]]] = []
29
+ self._negated: bool = False
30
+ for k, v in lookups.items():
31
+ self._children.append((k, v))
32
+
33
+ def __and__(self, other: Q) -> Q:
34
+ return self._combine("AND", other)
35
+
36
+ def __or__(self, other: Q) -> Q:
37
+ return self._combine("OR", other)
38
+
39
+ def __invert__(self) -> Q:
40
+ q = Q()
41
+ q._op = self._op
42
+ q._children = list(self._children)
43
+ q._negated = not self._negated
44
+ return q
45
+
46
+ def _combine(self, op: str, other: Q) -> Q:
47
+ q = Q()
48
+ q._op = op
49
+ q._children = [self, other]
50
+ return q
51
+
52
+ def is_empty(self) -> bool:
53
+ return not self._children
54
+
55
+ def compile(
56
+ self,
57
+ *,
58
+ table_alias: str,
59
+ param_prefix: str,
60
+ start_index: int,
61
+ lookup_compiler: Any,
62
+ ) -> tuple[Condition, int]:
63
+ if self.is_empty():
64
+ return Condition(sql="1=1", params={}), start_index
65
+
66
+ parts: list[str] = []
67
+ params: dict[str, Any] = {}
68
+ idx = start_index
69
+
70
+ for child in self._children:
71
+ if isinstance(child, Q):
72
+ cond, idx = child.compile(
73
+ table_alias=table_alias,
74
+ param_prefix=param_prefix,
75
+ start_index=idx,
76
+ lookup_compiler=lookup_compiler,
77
+ )
78
+ parts.append(f"({cond.sql})")
79
+ params = _merge_params(params, cond.params)
80
+ continue
81
+
82
+ lookup, value = child
83
+ cond, idx = lookup_compiler(
84
+ lookup=lookup,
85
+ value=value,
86
+ table_alias=table_alias,
87
+ param_prefix=param_prefix,
88
+ start_index=idx,
89
+ )
90
+ parts.append(cond.sql)
91
+ params = _merge_params(params, cond.params)
92
+
93
+ combined = f" {self._op} ".join(parts)
94
+ if self._negated:
95
+ combined = f"NOT ({combined})"
96
+ return Condition(sql=combined, params=params), idx
97
+
98
+
99
+ def normalize_in_values(value: Any) -> list[Any]:
100
+ if value is None:
101
+ return []
102
+ if isinstance(value, (str, bytes)):
103
+ return [value]
104
+ if isinstance(value, Mapping):
105
+ return list(value.values())
106
+ if isinstance(value, Iterable):
107
+ return list(value)
108
+ return [value]