Repositories
repositories
The repositories: stores addressed by identity, rather than documents read end to end.
A repository holds many plans and is asked for one -
DBRepository has get, save, delete,
patch and get_plan_by_id. It holds nothing but the session it was handed, so it is built
where it is used and thrown away afterwards, and it never commits: the caller owns the
transaction.
Connecting is separate, and happens once:
engine = create_engine("postgresql://user@host/db")
ensure_schema(engine, bootstrap=True)
with Session(engine) as session, session.begin():
DBRepository(session).save_all(collection)
AsyncDBRepository is the same over an
AsyncSession from create_async_engine;
the migrations are synchronous, so an async application builds both engines and calls
ensure_schema on the synchronous one.
The session must be bound to an engine from this package -
xmas_core.repositories.engine says why.
A document is not a store: it has no identity to address, it is read end to end and written
end to end. That is xmas_core.codec -
codec.read(path) and codec.write(collection, path) - and nothing here.
AsyncDBRepository(session)
The database repository over an AsyncSession.
Shares the statement builders and the ORM-to-pydantic mapping with
DBRepository as functions, not through a
base class: an async def overriding a def is not a substitute for it. The methods
differ in nothing but being awaited. It holds no engine of its own, so there is nothing
here to close.
The migrations are synchronous, so an async application builds both engines and calls
ensure_schema on the synchronous one
at startup:
engine = create_engine(url)
ensure_schema(engine, bootstrap=True)
engine.dispose()
SessionLocal = async_sessionmaker(create_async_engine(url))
async with SessionLocal() as session:
await AsyncDBRepository(session).save_all(collection)
await session.commit()
Binds the repository to an async session for one unit of work.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
AsyncSession
|
An |
required |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The session is unbound, its engine did not come from
|
Source code in xmas_core/repositories/db_async.py
delete(id)
async
Delete one feature and return it as it was.
Source code in xmas_core/repositories/db_async.py
delete_plan_by_id(id)
async
Delete a plan and everything it owns.
The delete holds its FOR UPDATE batch until the caller commits, so delete one
plan per transaction; see the synchronous method.
Source code in xmas_core/repositories/db_async.py
get(id)
async
get_plan_by_id(id)
async
Read a plan and every feature it owns as a collection.
Source code in xmas_core/repositories/db_async.py
patch(id, partial_update)
async
Apply a partial update to an existing feature and return the result.
Source code in xmas_core/repositories/db_async.py
save(feature)
async
save_all(features)
async
Insert new features and their references in bulk.
Source code in xmas_core/repositories/db_async.py
update(id, feature)
async
Replace an existing feature and its references.
Source code in xmas_core/repositories/db_async.py
update_all(features)
async
Upsert features and replace their references in bulk.
Source code in xmas_core/repositories/db_async.py
DBRepository(session)
Repository over the coretable database, addressed by identity.
Holds nothing but the session it is handed, so it is built where it is used and thrown away afterwards - one per request, per task, per CLI command. It never commits: the caller owns the transaction, and nothing written through a repository is persisted until the caller says so.
The session must be bound to an engine from
create_engine. That engine carries
the configured schema, and the repository's statements have no other way of finding it -
see xmas_core.repositories.engine. Bringing the
schema to head is ensure_schema,
called once at startup rather than every time a repository is built.
engine = create_engine("postgresql://user@host/db")
ensure_schema(engine, bootstrap=True)
with Session(engine) as session, session.begin():
DBRepository(session).save_all(collection)
Three consequences of the caller owning the session are worth knowing:
- A failure inside a method -
save's duplicate id, say - surfaces at the caller's flush or commit and poisons the whole transaction, rather than being confined to one call. - The methods autoflush, so pending work the caller has of its own is flushed from
inside them; an
IntegrityErrorthat has nothing to do with this repository can surface out ofget. - Every read goes to the database rather than to the session's identity map
(
populate_existing). The writes here are Core DML, which synchronizes nothing, so anupdatefollowed by apatchin one transaction would otherwise patch the pre-update state. It is also what a fresh session per call used to give for free.
Binds the repository to a session for the duration of one unit of work.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Session
|
A session bound to an engine from
|
required |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The session is unbound, its engine did not come from
|
Source code in xmas_core/repositories/db.py
delete(id)
Delete one feature and return it as it was.
Source code in xmas_core/repositories/db.py
delete_plan_by_id(id)
Delete a plan and everything it owns.
The delete holds its FOR UPDATE batch until the caller commits, so delete one
plan per transaction: several in one transaction hold several independently
ordered lock batches at once, which is the interleaving repositories.sql warns about.
Source code in xmas_core/repositories/db.py
get(id)
get_plan_by_id(id)
Read a plan and every feature it owns as a collection.
Source code in xmas_core/repositories/db.py
patch(id, partial_update)
Apply a partial update to an existing feature and return the result.
Source code in xmas_core/repositories/db.py
save(feature)
save_all(features)
Insert new features and their references in bulk.
Source code in xmas_core/repositories/db.py
update(id, feature)
Replace an existing feature and its references.
Source code in xmas_core/repositories/db.py
update_all(features)
Upsert features and replace their references in bulk.
Source code in xmas_core/repositories/db.py
create_async_engine(datasource)
Build the asyncpg engine an AsyncDBRepository's session is bound to.
Mirrors create_engine; the schema
migrations still need a synchronous engine, so an application creates both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
datasource
|
str | URL
|
A connection uri or a |
required |
Returns:
| Type | Description |
|---|---|
AsyncEngine
|
A PostgreSQL async engine bound to the configured schema. |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The URL could not be parsed, or names a database other than PostgreSQL. |
Source code in xmas_core/repositories/engine.py
create_engine(datasource)
Build the synchronous engine a repository's session is bound to.
The engine owns a connection pool for as long as it lives; the caller that built it
disposes it. CORETABLE_SCHEMA is bound to the configured schema, which
for_schema rebinds per tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
datasource
|
str | URL
|
A connection uri or a |
required |
Returns:
| Type | Description |
|---|---|
Engine
|
A PostgreSQL engine bound to the configured schema. |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The URL could not be parsed, or names a database other than PostgreSQL. |
Source code in xmas_core/repositories/engine.py
drop_schema(engine, schema=None)
Delete the coretable and its related tables and functions from one schema.
A schema with no alembic revision is left alone: there is nothing this created to take
back. A database created before 2.0 carries objects this revision knows nothing about and
is dropped at the database level (DROP SCHEMA <schema> CASCADE) instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A PostgreSQL engine, as built by
|
required |
schema
|
str | None
|
The schema to drop from. Defaults as in
|
None
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The schema is at a revision this release does not know. |
Source code in xmas_core/repositories/engine.py
ensure_schema(engine, schema=None, *, bootstrap=False)
Check that one schema behind engine is in the state the repository assumes.
Checks that the schema is accessible, at the current revision, and that the stored SRID
matches the configured one. Only with bootstrap=True does it change anything: it creates
a missing schema and runs the alembic migrations - creating the tables if the schema is
empty - which makes it the only call a tenant's provisioning has to make, and the only one
needing a role privileged enough to create a schema. Without it a missing, empty or
outdated schema is an error: a mistyped name, or an unrelated existing schema, must not
become a seeded tenant just because something tried to read from it. Call it once per
schema, at startup; it is not something a repository does on its own, so a short-lived
repository never pays for it. Several schemas are brought to head one after another, not from threads
of one process: alembic keeps its migration context in module globals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A PostgreSQL engine, as built by
|
required |
schema
|
str | None
|
The schema to bring to head. Defaults to the one |
None
|
bootstrap
|
bool
|
Create the schema if it does not exist, and bring it to head. |
False
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The schema is missing, empty or at an older revision and |
Source code in xmas_core/repositories/engine.py
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
for_schema(engine, schema)
Bind an engine to one schema, sharing its pool.
This is how one process serves several tenants: the returned engine is a shallow view over
the same pool, differing only in which schema CORETABLE_SCHEMA resolves to. The compiled
statement is identical for every tenant - SQLAlchemy substitutes the schema after
compilation - so the statement cache is shared and cannot leak between them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine | AsyncEngine
|
An engine from |
required |
schema
|
str
|
The schema to address. SQLAlchemy quotes it where it substitutes it. |
required |
Returns:
| Type | Description |
|---|---|
Engine | AsyncEngine
|
An engine of the same kind, bound to |
Source code in xmas_core/repositories/engine.py
engine
Connecting to the coretable database, and bringing its schema to head.
Everything here is about the connection, not about the store: building an engine the
repository can be handed a session from, and putting the schema behind it in the state the
repository assumes. A DBRepository holds a
caller-owned session and nothing else, so none of this belongs on it - constructing a
repository used to run alembic, which meant a per-request repository would have migrated
per request.
These are functions, not a class. SQLAlchemy's Session/sessionmaker already is the
context manager, and an engine is disposed by the caller that built it, so there is nothing
here to instantiate or to scope with with:
engine = create_engine(url)
try:
ensure_schema(engine, bootstrap=True)
with Session(engine) as session, session.begin():
DBRepository(session).save_all(collection)
finally:
engine.dispose()
create_engine is the one place a non-PostgreSQL database is refused, and the reason
everything downstream may assume PostgreSQL. make_url accepts any dialect, and a
sqlalchemy.URL handed in by a caller was never parsed from a string at all, so this is the
only check a sqlite:///x ever meets. The legacy postgres:// that SQLAlchemy dropped in 1.4
takes the same exit rather than a NoSuchModuleError.
One engine, many schemas
The ORM tables are declared in the symbolic schema
CORETABLE_SCHEMA, which is not a real schema and
has to be bound to one before a statement can run. create_engine binds it to the configured
schema - or, with none configured, to None, which SQLAlchemy renders as the connection's
current_schema() (the first existing schema on its search_path), read once when the engine
first connects - and for_schema rebinds it per tenant
over the same pool:
engine = create_engine(url) # one pool
ensure_schema(engine, "tenant_a", bootstrap=True) # once per tenant, at deployment
with Session(for_schema(engine, "tenant_a")) as session:
DBRepository(session).get(feature_id)
Binding happens through schema_translate_map, which SQLAlchemy substitutes after a
statement is compiled, so one compiled statement serves every tenant and the cache is neither
fragmented nor shared wrongly.
Both kinds of statement the repository emits carry the token: the tables, and the calls to
the plpgsql helpers, which xmas_core.repositories.sql qualifies with the same symbolic schema. Each
schema holds its own copy of the helpers, reading that schema's tables, so the map is the one
thing that decides where a statement lands; search_path plays no part beyond choosing the
default schema at first connect, and this module never sets it. A session bound to no schema addresses xmas_schema_placeholder.coretable and
xmas_schema_placeholder.coretable_role_graph_ids(...), neither of which exists - a loud
failure rather than a silent write into another tenant's copy.
Because none of this is visible in a statement, every engine built here is stamped with
ENGINE_SCHEMA_OPTION. DBRepository reads it back off its session's bind, so an engine built
any other way - or one carrying no schema binding - is refused at construction.
create_async_engine(datasource)
Build the asyncpg engine an AsyncDBRepository's session is bound to.
Mirrors create_engine; the schema
migrations still need a synchronous engine, so an application creates both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
datasource
|
str | URL
|
A connection uri or a |
required |
Returns:
| Type | Description |
|---|---|
AsyncEngine
|
A PostgreSQL async engine bound to the configured schema. |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The URL could not be parsed, or names a database other than PostgreSQL. |
Source code in xmas_core/repositories/engine.py
create_engine(datasource)
Build the synchronous engine a repository's session is bound to.
The engine owns a connection pool for as long as it lives; the caller that built it
disposes it. CORETABLE_SCHEMA is bound to the configured schema, which
for_schema rebinds per tenant.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
datasource
|
str | URL
|
A connection uri or a |
required |
Returns:
| Type | Description |
|---|---|
Engine
|
A PostgreSQL engine bound to the configured schema. |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The URL could not be parsed, or names a database other than PostgreSQL. |
Source code in xmas_core/repositories/engine.py
drop_schema(engine, schema=None)
Delete the coretable and its related tables and functions from one schema.
A schema with no alembic revision is left alone: there is nothing this created to take
back. A database created before 2.0 carries objects this revision knows nothing about and
is dropped at the database level (DROP SCHEMA <schema> CASCADE) instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A PostgreSQL engine, as built by
|
required |
schema
|
str | None
|
The schema to drop from. Defaults as in
|
None
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The schema is at a revision this release does not know. |
Source code in xmas_core/repositories/engine.py
ensure_schema(engine, schema=None, *, bootstrap=False)
Check that one schema behind engine is in the state the repository assumes.
Checks that the schema is accessible, at the current revision, and that the stored SRID
matches the configured one. Only with bootstrap=True does it change anything: it creates
a missing schema and runs the alembic migrations - creating the tables if the schema is
empty - which makes it the only call a tenant's provisioning has to make, and the only one
needing a role privileged enough to create a schema. Without it a missing, empty or
outdated schema is an error: a mistyped name, or an unrelated existing schema, must not
become a seeded tenant just because something tried to read from it. Call it once per
schema, at startup; it is not something a repository does on its own, so a short-lived
repository never pays for it. Several schemas are brought to head one after another, not from threads
of one process: alembic keeps its migration context in module globals.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
A PostgreSQL engine, as built by
|
required |
schema
|
str | None
|
The schema to bring to head. Defaults to the one |
None
|
bootstrap
|
bool
|
Create the schema if it does not exist, and bring it to head. |
False
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The schema is missing, empty or at an older revision and |
Source code in xmas_core/repositories/engine.py
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
for_schema(engine, schema)
Bind an engine to one schema, sharing its pool.
This is how one process serves several tenants: the returned engine is a shallow view over
the same pool, differing only in which schema CORETABLE_SCHEMA resolves to. The compiled
statement is identical for every tenant - SQLAlchemy substitutes the schema after
compilation - so the statement cache is shared and cannot leak between them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine | AsyncEngine
|
An engine from |
required |
schema
|
str
|
The schema to address. SQLAlchemy quotes it where it substitutes it. |
required |
Returns:
| Type | Description |
|---|---|
Engine | AsyncEngine
|
An engine of the same kind, bound to |
Source code in xmas_core/repositories/engine.py
CORETABLE_SCHEMA = 'xmas_schema_placeholder'
module-attribute
Symbolic schema the coretable tables are declared in - never a real schema name.
xmas_core.repositories.engine binds it to the real one per session through
schema_translate_map, which is what lets one engine address many tenant schemas over a
single pool. It is a constant rather than a settings read because a settings read here is
evaluated at import of this module, and a schema chosen later - as the CLI's --schema
does - would arrive too late to qualify anything.
A None key would do the same job for the three tables here, but it rewrites every
unqualified table in a statement, including tables the calling application owns. A named
token moves these three - and the calls to the SQL functions, which xmas_core.repositories.sql
qualifies with it too - and nothing else. A session with no binding fails on
xmas_schema_placeholder.coretable rather than landing in another schema's copy.
DBRepository(session)
Repository over the coretable database, addressed by identity.
Holds nothing but the session it is handed, so it is built where it is used and thrown away afterwards - one per request, per task, per CLI command. It never commits: the caller owns the transaction, and nothing written through a repository is persisted until the caller says so.
The session must be bound to an engine from
create_engine. That engine carries
the configured schema, and the repository's statements have no other way of finding it -
see xmas_core.repositories.engine. Bringing the
schema to head is ensure_schema,
called once at startup rather than every time a repository is built.
engine = create_engine("postgresql://user@host/db")
ensure_schema(engine, bootstrap=True)
with Session(engine) as session, session.begin():
DBRepository(session).save_all(collection)
Three consequences of the caller owning the session are worth knowing:
- A failure inside a method -
save's duplicate id, say - surfaces at the caller's flush or commit and poisons the whole transaction, rather than being confined to one call. - The methods autoflush, so pending work the caller has of its own is flushed from
inside them; an
IntegrityErrorthat has nothing to do with this repository can surface out ofget. - Every read goes to the database rather than to the session's identity map
(
populate_existing). The writes here are Core DML, which synchronizes nothing, so anupdatefollowed by apatchin one transaction would otherwise patch the pre-update state. It is also what a fresh session per call used to give for free.
Binds the repository to a session for the duration of one unit of work.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
Session
|
A session bound to an engine from
|
required |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The session is unbound, its engine did not come from
|
Source code in xmas_core/repositories/db.py
delete(id)
Delete one feature and return it as it was.
Source code in xmas_core/repositories/db.py
delete_plan_by_id(id)
Delete a plan and everything it owns.
The delete holds its FOR UPDATE batch until the caller commits, so delete one
plan per transaction: several in one transaction hold several independently
ordered lock batches at once, which is the interleaving repositories.sql warns about.
Source code in xmas_core/repositories/db.py
get(id)
get_plan_by_id(id)
Read a plan and every feature it owns as a collection.
Source code in xmas_core/repositories/db.py
patch(id, partial_update)
Apply a partial update to an existing feature and return the result.
Source code in xmas_core/repositories/db.py
save(feature)
save_all(features)
Insert new features and their references in bulk.
Source code in xmas_core/repositories/db.py
update(id, feature)
Replace an existing feature and its references.
Source code in xmas_core/repositories/db.py
update_all(features)
Upsert features and replace their references in bulk.
Source code in xmas_core/repositories/db.py
AsyncDBRepository(session)
The database repository over an AsyncSession.
Shares the statement builders and the ORM-to-pydantic mapping with
DBRepository as functions, not through a
base class: an async def overriding a def is not a substitute for it. The methods
differ in nothing but being awaited. It holds no engine of its own, so there is nothing
here to close.
The migrations are synchronous, so an async application builds both engines and calls
ensure_schema on the synchronous one
at startup:
engine = create_engine(url)
ensure_schema(engine, bootstrap=True)
engine.dispose()
SessionLocal = async_sessionmaker(create_async_engine(url))
async with SessionLocal() as session:
await AsyncDBRepository(session).save_all(collection)
await session.commit()
Binds the repository to an async session for one unit of work.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
AsyncSession
|
An |
required |
Raises:
| Type | Description |
|---|---|
DatasourceError
|
The session is unbound, its engine did not come from
|
Source code in xmas_core/repositories/db_async.py
delete(id)
async
Delete one feature and return it as it was.
Source code in xmas_core/repositories/db_async.py
delete_plan_by_id(id)
async
Delete a plan and everything it owns.
The delete holds its FOR UPDATE batch until the caller commits, so delete one
plan per transaction; see the synchronous method.
Source code in xmas_core/repositories/db_async.py
get(id)
async
get_plan_by_id(id)
async
Read a plan and every feature it owns as a collection.
Source code in xmas_core/repositories/db_async.py
patch(id, partial_update)
async
Apply a partial update to an existing feature and return the result.
Source code in xmas_core/repositories/db_async.py
save(feature)
async
save_all(features)
async
Insert new features and their references in bulk.
Source code in xmas_core/repositories/db_async.py
update(id, feature)
async
Replace an existing feature and its references.
Source code in xmas_core/repositories/db_async.py
update_all(features)
async
Upsert features and replace their references in bulk.
Source code in xmas_core/repositories/db_async.py
sql
SQLAlchemy expressions for the coretable PostgreSQL functions.
Every builder here returns an expression, never a statement: a set-returning function's image is
a from-item and a scalar function's is a column element, and the caller writes the select()
around it. That is what lets the caller choose the projection -- select(g) for whole rows,
select(g.id) for ids alone -- and what lets a traversal go inside a join or an exists().
The functions are schema-qualified the way the tables are. Every call is a
_CoretableFunction, whose schema renders as the symbolic orm.CORETABLE_SCHEMA; the
schema_translate_map of an engine from repositories.create_engine/for_schema then
substitutes and quotes the real schema at execution, exactly as it does for coretable. So
one compiled statement serves every tenant, search_path plays no part, and the binding is
the only thing that says which schema a statement addresses. The flip side: a builder run on
an engine without that binding addresses xmas_schema_placeholder.<function> and fails
with UndefinedFunction.
func.<schema>.<name> was declined. It needs the schema when the statement is built, which
makes it a second source for the schema beside the binding - and each schema's functions read
that schema's tables, so a disagreement deletes another tenant's rows without an error. It
would also put a schema parameter on every builder, cost a compiled statement per tenant,
and SQLAlchemy does not quote a package name that is mixed-case or a reserved word
(TenantA.f, user.f).
coretable_feature_graph is the one exception and returns a Select, for the reason given in its
docstring. coretable_delete_objects_recursive and coretable_delete_orphans_recursive delete when
the caller's statement runs; see their warnings, in particular why narrowing the delete's output
does not narrow the delete.
Every builder taking start accepts one root coretable id or a sequence of them, a bare id being
equivalent to a one-element sequence. Ids absent from coretable, None elements and repeats
are ignored; an empty sequence yields no rows.
MAX_BFS_DEPTH = 5
module-attribute
The maximum BFS traversal depth needed to reach everything belonging to a plan.
A literal rather than Appschema.max_containment_depth taken over the supported appschemas,
which is what it equals: deriving it here would import every appschema module (~1.3 s) on every
import of this module, and nothing else in it needs them. The test suite holds the two equal.
add_navigable_role(source_featuretype, navigable_role, target_featuretype, appschema, appschema_version, rel_direction, dependent_part=None)
Build the add_navigable_role call, which idempotently registers a navigable role.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source_featuretype
|
str
|
Owning/source feature type name. |
required |
navigable_role
|
str
|
Role (association) name linking source to target. |
required |
target_featuretype
|
str
|
Referenced/target feature type name. |
required |
appschema
|
str
|
Appschema prefix (e.g. |
required |
appschema_version
|
str
|
Appschema version (e.g. |
required |
rel_direction
|
Literal['forward', 'inverse']
|
Whether the refs edge runs |
required |
dependent_part
|
Literal['source', 'target'] | None
|
Ownership marker; |
None
|
Returns:
| Type | Description |
|---|---|
Function[bool]
|
The function's |
Function[bool]
|
in a select list rather than a |
Function[bool]
|
|
Source code in xmas_core/repositories/sql.py
coretable_delete_objects_recursive(start, dry_run=False)
Build the coretable_delete_objects_recursive call, which recursively deletes start.
Deletes each root together with its safe cascade closure (see coretable_role_graph_cascade).
Pass every root of a batch in one call rather than looping: the closure is computed against the
union of all roots, and the candidates are locked in a single ORDER BY id FOR UPDATE batch --
one ordered batch is what makes the delete deadlock-safe, and N calls in one transaction would
take N separately ordered batches that can interleave.
Warning
Executing any statement built over this expression performs the deletion unless dry_run
is True; run it inside a transaction. The affected rows are captured before removal, so
the statement still yields them after the delete.
Projecting is safe; restricting rows is not. select(g.id) deletes exactly what
select(g) deletes, and is the cheaper way to learn what was removed. But the function is
plpgsql and materializes its whole result set before any WHERE/LIMIT/OFFSET
of yours is applied, so a narrowed statement destroys the entire closure while reporting a
subset of it. Never paginate a preview -- use dry_run=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
UUID | Sequence[UUID]
|
One root coretable id to delete or a sequence of them; see the module docstring. |
required |
dry_run
|
bool
|
When |
False
|
Returns:
| Type | Description |
|---|---|
type[Feature]
|
|
type[Feature]
|
would-be-deleted) rows. Use |
Source code in xmas_core/repositories/sql.py
coretable_delete_orphans_recursive()
Build the coretable_delete_orphans_recursive call.
Deletes dependent-part objects no longer owned by any whole.
Warning
Executing the caller's statement performs the deletion; run it inside a transaction.
Returns:
| Type | Description |
|---|---|
Function[int]
|
The total number of deleted rows as an |
Function[int]
|
so it belongs in a select list rather than a |
Function[int]
|
|
Source code in xmas_core/repositories/sql.py
coretable_feature_graph(start_id, depth_limit=MAX_BFS_DEPTH, include_forward=True, include_backward=True)
Return a chainable Select of Features reachable from start_id via the role graph.
The role-graph traversal under the plan-graph defaults: one root and a bounded depth, with the ownership closure off.
This is the module's one exception to returning an expression, and the one traversal written over
coretable rather than over a function scan: get_plan_by_id appends
.where(Feature.id != plan_id) to the result, so it has to be chainable with a plain
Feature.… predicate -- which is what selecting from real coretable here buys. Use
coretable_role_graph instead where that constraint does not apply.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start_id
|
UUID
|
Root coretable id to traverse from. |
required |
depth_limit
|
int
|
Maximum BFS depth; defaults to the current appschemas' maximum depth. |
MAX_BFS_DEPTH
|
include_forward
|
bool
|
Traverse navigable roles in the source->target direction. |
True
|
include_backward
|
bool
|
Traverse navigable roles in the target->source direction. |
True
|
Returns:
| Type | Description |
|---|---|
Select[tuple[Feature]]
|
A chainable |
Source code in xmas_core/repositories/sql.py
coretable_role_graph(start, depth_limit=0, include_forward=True, include_backward=True, dependent_parts=False)
Build the coretable_role_graph call, the Feature rows reachable from start.
The SQL function is a thin SETOF wrapper joining coretable against
coretable_role_graph_ids, so this hydrates exactly the traversal whose ids
coretable_role_graph_ids returns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
UUID | Sequence[UUID]
|
One root coretable id or a sequence of them; see the module docstring. |
required |
depth_limit
|
int
|
Maximum BFS depth; |
0
|
include_forward
|
bool
|
Traverse navigable roles in the source->target direction. |
True
|
include_backward
|
bool
|
Traverse navigable roles in the target->source direction. |
True
|
dependent_parts
|
bool
|
Collect the transitive existential dependent-part (ownership) closure instead of a general traversal; both directions are followed, ignoring the include_* flags. |
False
|
Returns:
| Type | Description |
|---|---|
type[Feature]
|
|
type[Feature]
|
with |
type[Feature]
|
through the returned alias, since |
type[Feature]
|
|
type[Feature]
|
The annotation is |
type[Feature]
|
how SQLAlchemy declares |
type[Feature]
|
alias), which is what makes attribute access on the result check against |
type[Feature]
|
runtime object is an |
Source code in xmas_core/repositories/sql.py
coretable_role_graph_cascade(start, depth_limit=0)
Build the coretable_role_graph_cascade call, the rows safe to delete with start.
The result is the dependent-part closure of start minus any part whose owning whole lies
outside that closure (transitively), i.e. the objects that can be removed together with
start without orphaning a part still owned from elsewhere. Every root is exempt from that
exclusion, so a root that is itself an owned part is still returned.
Warning
The closure is not compositional, so a batch must be passed in one call. For a part owned
by both a and b, neither cascade(a) nor cascade(b) contains it -- it is
external to each closure taken alone -- and deleting both roots one at a time would leave
it ownerless. cascade([a, b]) computes the closure against the union, where the part is
external to neither.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
UUID | Sequence[UUID]
|
One root coretable id or a sequence of them; see the module docstring. |
required |
depth_limit
|
int
|
Maximum BFS depth; |
0
|
Returns:
| Type | Description |
|---|---|
type[Feature]
|
|
type[Feature]
|
filtering through the alias. |
Source code in xmas_core/repositories/sql.py
coretable_role_graph_ids(start, depth_limit=0, include_forward=True, include_backward=True, dependent_parts=False)
Build the coretable_role_graph_ids call, the ids reachable from start.
The cheap half of coretable_role_graph: it yields ids without joining coretable, so prefer
it wherever the rows are not needed. The edge set the traversal walks is bounded by |refs|
and independent of the roots, so one call over N roots builds it once where N calls build it N
times.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
UUID | Sequence[UUID]
|
One root coretable id or a sequence of them; see the module docstring. |
required |
depth_limit
|
int
|
Maximum BFS depth; |
0
|
include_forward
|
bool
|
Traverse navigable roles in the source->target direction. |
True
|
include_backward
|
bool
|
Traverse navigable roles in the target->source direction. |
True
|
dependent_parts
|
bool
|
Collect the transitive existential dependent-part (ownership) closure instead of a general traversal; both directions are followed, ignoring the include_* flags. |
False
|
Returns:
| Type | Description |
|---|---|
TableValuedAlias
|
A derived table with a single |
TableValuedAlias
|
yields the ids, and the same expression goes inside an |
Source code in xmas_core/repositories/sql.py
list_navigable_roles()
Build the list_navigable_roles call, which returns every configured navigable role.
Returns:
| Type | Description |
|---|---|
TableValuedAlias
|
A derived table with one row per configured role and columns |
TableValuedAlias
|
|
TableValuedAlias
|
|
TableValuedAlias
|
|
Source code in xmas_core/repositories/sql.py
list_top_level_featuretypes()
Build the list_top_level_featuretypes call.
Returns:
| Type | Description |
|---|---|
TableValuedAlias
|
A derived table with one row per appschema/version and columns |
TableValuedAlias
|
|
TableValuedAlias
|
|
Source code in xmas_core/repositories/sql.py
pg_array(values, type_)
Bind a Python sequence as a single PostgreSQL array parameter.
Keeps a statement's bind-parameter count independent of len(values): asyncpg refuses
more than 32767 parameters per statement, and a bind list whose length varies also defeats
the driver's prepared-statement cache, since it caches by SQL text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
Sequence[Any]
|
The values to bind; element bind processing runs through |
required |
type_
|
TypeEngine[Any]
|
The array's item type, e.g. |
required |
Returns:
| Type | Description |
|---|---|
Cast[Any]
|
A |
Cast[Any]
|
cast is explicit because PostgreSQL cannot infer a polymorphic function's argument |
Cast[Any]
|
type from an untyped parameter (see |
Cast[Any]
|
may render a second, redundant |
Source code in xmas_core/repositories/sql.py
unnest_rows(rows, columns, *, types=None)
Expose row dicts as a table-valued unnest over one array parameter per column.
The PostgreSQL counterpart to an inline VALUES list: instead of one bind parameter per
cell it binds one array per column, so the parameter count stays constant (see pg_array).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rows
|
Sequence[dict[str, Any]]
|
Row dicts keyed by column name; every column in |
required |
columns
|
Sequence[Column[Any]]
|
The columns to emit, in order. |
required |
types
|
Mapping[str, TypeEngine[Any]] | None
|
Array item types overriding a column's own type, keyed by column name. Needed
where the column type is not what the driver can encode an array of — geometry
columns travel as |
None
|
Returns:
| Type | Description |
|---|---|
TableValuedAlias
|
A derived table aliased with |