Skip to content

Commit 33c3bc8

Browse files
committed
Fixes and enhancements
1 parent 6858e37 commit 33c3bc8

File tree

16 files changed

+111
-29
lines changed

16 files changed

+111
-29
lines changed

docs/instrumentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ from sqlorm.opentelemetry import SQLORMInstrumentor
1010
SQLORMInstrumentor().instrument(engine=engine)
1111
```
1212

13-
You can optionally configure SQLAlchemy instrumentation to enable sqlcommenter which enriches the query with contextual information.
13+
You can optionally configure SQLORM instrumentation to enable sqlcommenter which enriches the query with contextual information.
1414

1515
```py
1616
SQLORMInstrumentor().instrument(enable_commenter=True, commenter_options={})

docs/models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ By default, unknown columns (ones which are not mapped), will be set as attribut
248248
Dirty attributes which are not mapped will also be saved in DML statements.
249249
250250
You can thus create models with no mapped columns. However, models require a primary key to function properly.
251-
One will be automatically added when non are defined (named "id").
251+
One will be automatically added when none are defined (named "id").
252252
253253
When no columns are mapped, `SELECT *` is used.
254254

sqlorm/doc.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import abc
2+
from .model import Model, Column
3+
from .types import JSON
4+
5+
6+
class DocModel(Model, abc.ABC):
7+
fields = Column(JSON)
8+
9+
def __getattr__(self, name):
10+
pass
11+
12+
def __setattr__(self, name, value):
13+
pass

sqlorm/drivers/sqlite.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,21 @@
1414
}
1515

1616

17+
PRIMARY_KEY_SCHEMA_DEFINITION = "PRIMARY KEY AUTOINCREMENT"
18+
19+
1720
def connect(*args, **kwargs):
1821
pragmas = kwargs.pop("pragma", {})
1922
extensions = kwargs.pop("ext", None)
2023
fine_tune = kwargs.pop("fine_tune", False)
24+
foreign_keys = kwargs.pop("foreign_keys", False)
2125

2226
kwargs.setdefault("check_same_thread", False) # better default to work with sqlorm pooling
2327
conn = sqlite3.connect(*args, **kwargs)
2428
conn.row_factory = sqlite3.Row
2529

30+
if foreign_keys:
31+
pragmas["foreign_keys"] = "ON"
2632
if fine_tune:
2733
pragmas.update(FINE_TUNED_PRAGMAS)
2834

sqlorm/engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ def fetchcomposite(
509509
map = HydrationMap.create([model, nested])
510510
elif nested:
511511
map = CompositionMap(
512-
lambda r: r.values(), {k: HydrationMap.create(v) for k, v in nested.items()}
512+
lambda r: list(r.values()), {k: HydrationMap.create(v) for k, v in nested.items()}
513513
)
514514
elif not map:
515515
map = CompositionMap.create([loader, nested])

sqlorm/mapper.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def from_class(cls, object_class, **kwargs):
3939
if value.__class__ is t._AnnotatedAlias:
4040
primary_key = PrimaryKeyColumn in getattr(value, "__metadata__", ())
4141
value = value.__args__[0]
42-
mapper.columns.append(
42+
mapper.map(key,
4343
Column(key, SQLType.from_pytype(value), primary_key=primary_key, attribute=key)
4444
)
4545
return mapper
@@ -58,6 +58,7 @@ def map(self, attr, col_or_rel=None):
5858
if isinstance(col_or_rel, MappedColumnMixin):
5959
col_or_rel.attribute = attr
6060
col_or_rel.mapper = self
61+
col_or_rel.table = self.table
6162
self.columns.append(col_or_rel)
6263
elif isinstance(col_or_rel, Relationship):
6364
col_or_rel.attribute = attr
@@ -296,13 +297,28 @@ def delete(self, obj):
296297

297298
def __repr__(self):
298299
return f"<Mapper({self.table} -> {self.object_class.__name__})>"
300+
301+
302+
class _Unknown:
303+
pass
304+
unknown = _Unknown()
299305

300306

301307
class MapperColumnList(ColumnList):
302308
def __init__(self, mapper):
303309
super().__init__([], wildcard=None)
304310
self.mapper = mapper
305311

312+
def aliased_table(self, alias=unknown):
313+
if alias is unknown:
314+
alias = self.mapper.table
315+
return super().aliased_table(alias)
316+
317+
def prefixed(self, prefix=unknown):
318+
if prefix is unknown:
319+
prefix = f"{self.mapper.object_class.__name__}__"
320+
return super().prefixed(prefix)
321+
306322
def get(self, name):
307323
for col in self:
308324
if col.name == name:
@@ -331,6 +347,7 @@ def __init__(
331347
unique=False,
332348
lazy=False,
333349
attribute=None,
350+
schema_def=None
334351
):
335352
self.name = name
336353
self.table = None
@@ -344,6 +361,7 @@ def __init__(
344361
self.unique = unique
345362
self.lazy = lazy
346363
self.attribute = attribute
364+
self.schema_def = schema_def
347365

348366
def get(self, obj):
349367
"""Returns the database value from the object, using the provided dump function if needed

sqlorm/model.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ def process_mapped_attributes(dct):
4141
mapped_attrs = {}
4242
for name, annotation in dct.get("__annotations__", {}).items():
4343
primary_key = False
44+
nullable = None
4445
if annotation.__class__ is t._AnnotatedAlias:
4546
primary_key = PrimaryKeyColumn in getattr(annotation, "__metadata__", ())
4647
annotation = annotation.__args__[0]
48+
if annotation.__class__ is t.Optional:
49+
annotation = annotation.__args__[0]
50+
nullable = True
4751
if name not in dct:
4852
# create column object for annotations with no default values
49-
dct[name] = mapped_attrs[name] = Column(name, annotation, primary_key=primary_key)
53+
dct[name] = mapped_attrs[name] = Column(name, annotation, primary_key=primary_key, nullable=nullable)
5054
elif isinstance(dct[name], Column):
5155
mapped_attrs[name] = dct[name]
5256
if dct[name].type is None:
@@ -112,6 +116,10 @@ def create_mapper(cls, mapped_attrs=None):
112116
if mapped_attrs:
113117
cls.__mapper__.map(mapped_attrs)
114118

119+
for col in cls.__mapper__.columns:
120+
if col.nullable is None:
121+
col.nullable = cls.Meta.columns_default_nullable
122+
115123
cls.c = cls.__mapper__.columns # handy shortcut
116124

117125
auto_primary_key = cls.Meta.auto_primary_key
@@ -120,7 +128,9 @@ def create_mapper(cls, mapped_attrs=None):
120128
# we force the usage of SELECT * as we auto add a primary key without any other mapped columns
121129
# without doing this, only the primary key would be selected
122130
cls.__mapper__.force_select_wildcard = True
123-
cls.__mapper__.map(auto_primary_key, Column(auto_primary_key, type=cls.Meta.auto_primary_key_type, primary_key=True))
131+
pk_column = Column(auto_primary_key, type=cls.Meta.auto_primary_key_type, primary_key=True, nullable=False)
132+
cls.__mapper__.map(auto_primary_key, pk_column)
133+
setattr(cls, auto_primary_key, pk_column)
124134

125135
@staticmethod
126136
def process_meta_inheritance(cls):
@@ -340,6 +350,7 @@ class Meta:
340350
)
341351
auto_primary_key_type: SQLType = Integer
342352
allow_unknown_columns: bool = True # hydrate() will set attributes for unknown columns
353+
columns_default_nullable : bool = True
343354

344355
@classmethod
345356
def bind(cls, engine: Engine):
@@ -349,6 +360,9 @@ def bind(cls, engine: Engine):
349360
(cls, abc.ABC),
350361
{"__engine__": engine, "__model_registry__": ModelRegistry()},
351362
)
363+
364+
def __sql__(self):
365+
return self.table.name
352366

353367

354368
class Model(BaseModel, abc.ABC):

sqlorm/schema.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from .model import BaseModel
22
from .sql import SQL, split_sql_statements
33
from .sqlfunc import execute
4-
from .engine import ensure_transaction
4+
from .engine import ensure_transaction, get_current_session
55
from .mapper import Mapper
66
import os
77
import re
@@ -39,17 +39,18 @@ def create_all(model_registry=None, engine=None, check_missing=False, logger=Non
3939
def create_table(mapper, default_type="varchar"):
4040
if not isinstance(mapper, Mapper):
4141
mapper = Mapper.from_class(mapper)
42+
dbapi = get_current_session().engine.dbapi
4243
return SQL(
4344
"CREATE TABLE",
4445
mapper.table,
4546
SQL.Tuple(
4647
[
47-
SQL(
48+
SQL(c.schema_def) if c.schema_def else SQL(
4849
c.name,
4950
c.type.sql_type if c.type else default_type,
5051
"UNIQUE" if c.unique else "",
5152
"NOT NULL" if not c.nullable else "",
52-
"PRIMARY KEY" if c.primary_key else "",
53+
getattr(dbapi, "PRIMARY_KEY_SCHEMA_DEFINITION", "PRIMARY KEY") if c.primary_key else "",
5354
SQL(
5455
"REFERENCES",
5556
f"{c.references.table} ({c.references.name})"
@@ -146,3 +147,16 @@ def set_schema_version(version, engine=None):
146147
SQL.insert("schema_version", {"version": version}),
147148
)
148149
)
150+
151+
152+
def create_initial_migration(model_registry=None, path="migrations", version="000", **kwargs):
153+
if not model_registry:
154+
model_registry = BaseModel.__model_registry__
155+
156+
stmts = []
157+
for model in model_registry.values():
158+
stmts.append(create_table.sql(model.__mapper__, **kwargs))
159+
160+
sql = "-- Initial creation of the database (auto-generated by sqlorm)\n\n" + ";\n\n".join(stmts)
161+
with open(os.path.join(path, f"{version}_initial.sql"), "w") as f:
162+
f.write(sql)

sqlorm/sql.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import typing as t
22
from collections import namedtuple
3+
from collections.abc import Iterable
34

45

56
def render(stmt, params=None, dbapi=None, paramstyle=None):
@@ -113,6 +114,8 @@ def _render(self, params):
113114
for part in self.parts:
114115
if isinstance(part, SQLStr):
115116
stmt.append(str(part._render(params)))
117+
elif hasattr(part, "__sql__"):
118+
stmt.append(str(part.__sql__()))
116119
elif part:
117120
stmt.append(str(part))
118121
return " ".join(filter(bool, stmt))
@@ -354,8 +357,9 @@ def as_dict(self):
354357
class List(SQLStr, list):
355358
"""A list of SQL items"""
356359

357-
def __init__(self, items=None, joinstr=",", startstr="", endstr=""):
358-
super().__init__(items)
360+
def __init__(self, *items, joinstr=",", startstr="", endstr=""):
361+
_items = items[0] if len(items) == 1 and isinstance(items[0], Iterable) else items
362+
super().__init__(_items)
359363
self.joinstr = joinstr
360364
self.startstr = startstr
361365
self.endstr = endstr
@@ -374,18 +378,18 @@ def _render(self, params):
374378

375379

376380
class And(List):
377-
def __init__(self, items):
378-
super().__init__(items, "AND", "(", ")")
381+
def __init__(self, *items):
382+
super().__init__(*items, joinstr="AND", startstr="(", endstr=")")
379383

380384

381385
class Or(List):
382-
def __init__(self, items):
383-
super().__init__(items, "OR", "(", ")")
386+
def __init__(self, *items):
387+
super().__init__(*items, joinstr="OR", startstr="(", endstr=")")
384388

385389

386390
class Tuple(List):
387-
def __init__(self, items):
388-
super().__init__(items, ",", "(", ")")
391+
def __init__(self, *items):
392+
super().__init__(*items, joinstr=",", startstr="(", endstr=")")
389393

390394

391395
class Function(SQLStr):

sqlorm/sqlfunc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def is_sqlfunc(func):
4444
"""Checks if func is an empty function with a python doc"""
4545
doc = inspect.getdoc(func)
4646
src = inspect.getsource(func).strip(' "\n\r')
47-
return doc and src.endswith(func.__doc__) and not getattr(func, "sqlfunc", False)
47+
return doc and src.endswith(func.__doc__.strip(' \n\r')) and not getattr(func, "sqlfunc", False)
4848

4949

5050
def _query_executor_builder(executor):

0 commit comments

Comments
 (0)