Skip to content

Commit

Permalink
add query-level insert/update methods to supplement ORM-level add/edit
Browse files Browse the repository at this point in the history
refs #171
  • Loading branch information
guruofgentoo committed Dec 2, 2022
1 parent aef1cf0 commit 60fac30
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 6 deletions.
16 changes: 10 additions & 6 deletions .circleci/config.yml
Expand Up @@ -36,12 +36,15 @@ jobs:
docker:
- image: level12/python-test-multi
environment:
SQLALCHEMY_DATABASE_URI: "postgresql://postgres:password@localhost/test"
SQLALCHEMY_DATABASE_URI: "postgresql://postgres@localhost:54321/postgres"
- image: postgres:latest
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: test
# Ok for local dev, potentially UNSAFE in other applications. Don't blindly copy & paste
# without considering implications.
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: postgres
PGPORT: 54321
steps:
- runtests
sqlite:
Expand All @@ -55,11 +58,12 @@ jobs:
docker:
- image: level12/python-test-multi
environment:
SQLALCHEMY_DATABASE_URI: "mssql+pyodbc_mssql://SA:Password12!@localhost:1433/tempdb?driver=ODBC+Driver+17+for+SQL+Server"
- image: mcr.microsoft.com/mssql/server:2017-latest
SQLALCHEMY_DATABASE_URI: "mssql+pyodbc_mssql://SA:Password12!@localhost:14331/tempdb?driver=ODBC+Driver+17+for+SQL+Server"
- image: mcr.microsoft.com/mssql/server
environment:
ACCEPT_EULA: Y
SA_PASSWORD: "Password12!"
MSSQL_SA_PASSWORD: "Password12!"
MSSQL_TCP_PORT: 14331
steps:
- runtests

Expand Down
35 changes: 35 additions & 0 deletions docker-compose.yaml
@@ -0,0 +1,35 @@
# Docker Notes
# ============
# - `docker-compose up`: bring up containers
# - `docker-compose up -d`: same, but run in background/daemon mode
# - `docker-compose down`: bring down containers started with `-d`
# - `docker ps`: show running containers
# - `docker ps -a`: show all containers
# - `docker-compose exec <container name> /bin/bash`: get shell in app container
# - `docker images`
# - `docker rmi <image name>`
# - `docker stop $(docker ps -aq)`: stop all running containers
# - `docker rm $(docker ps -a -q)`: remove all stopped containers

version: '2.1'
services:
keg-mssql:
image: mcr.microsoft.com/mssql/server
container_name: kegelements-mssql
ports:
- '${KEG_LIB_MSSQL_IP:-127.0.0.1}:${KEG_LIB_MSSQL_PORT:-14331}:1433'
environment:
ACCEPT_EULA: Y
MSSQL_SA_PASSWORD: "Password12!"
keg-pg:
image: postgres:13-alpine
container_name: kegelements-pg
ports:
- '${KEG_LIB_POSTGRES_IP:-127.0.0.1}:${KEG_LIB_POSTGRES_PORT:-54321}:5432'
environment:
# Ok for local dev, potentially UNSAFE in other applications. Don't blindly copy & paste
# without considering implications.
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_MULTIPLE_DATABASES: keg_tests
volumes:
- ./docker/pg-init-scripts:/docker-entrypoint-initdb.d
61 changes: 61 additions & 0 deletions keg_elements/db/mixins.py
Expand Up @@ -214,6 +214,67 @@ def edit(cls, oid=None, **kwargs):
obj.from_dict(kwargs)
return obj

@classmethod
def insert(cls, values=None, **kwargs):
"""Similar to ``add`` but without the ORM overhead. Useful for high data throughput
cases where having kwargs name validation and ORM ops/session on every iteration
would be inefficient.
Assumes the calling code is handling session flush/commit.
:param values: optional dict of values to insert
:param kwargs: values to insert, can be combined with the ``values`` dict
:return: primary key value(s). Note: SQLite does not support this
"""
if values is None:
values = kwargs
else:
values.update(kwargs)

stmt = sa.insert(cls.__table__).values(**values)
if db.engine.dialect.name == 'sqlite':
return db.session.execute(stmt)
primary_keys = cls.primary_keys()
stmt = stmt.returning(*primary_keys)
result = db.session.execute(stmt)
if len(primary_keys) > 1:
return result.fetchone()
return result.scalar()

@classmethod
def update(cls, ent_id, values=None, **kwargs):
"""Similar to ``edit`` but without the ORM overhead. Useful for high data throughput
cases where having kwargs name validation and ORM ops/session on every iteration
would be inefficient.
Assumes the calling code is handling session flush/commit.
Note: if the entitiy has multiple primary key columns, ``ent_id`` should be an iterable
with the values to match in the order of the column definitions as specified in the
SA model (i.e. the order of columns returned by ``cls.primary_keys()``).
:param ent_id: primary key value(s) to match for update
:param values: optional dict of values to insert
:param kwargs: values to insert, can be combined with the ``values`` dict
:return: db cursor result
"""
if values is None:
values = kwargs
else:
values.update(kwargs)

stmt = (
sa.update(cls.__table__)
.values(**values)
.where(
*map(
lambda pair: pair[0] == pair[1],
zip(cls.primary_keys(), tolist(ent_id))
)
)
)
return db.session.execute(stmt)

@classmethod
def get_by(cls, **kwargs):
"""Returns the instance of this class matching the given criteria or
Expand Down
59 changes: 59 additions & 0 deletions keg_elements/tests/test_db_mixins.py
Expand Up @@ -43,6 +43,7 @@ class TestMethodsMixin:

def setup_method(self, fn):
ents.Thing.delete_cascaded()
ents.MultiplePrimaryKeys.delete_cascaded()

def test_add(self):
ents.Thing.add(name='name', color='color', scale_check=1)
Expand All @@ -54,6 +55,36 @@ def test_add(self):
assert row.color == 'color'
assert row.scale_check == 1

def test_insert(self):
ret_id = ents.Thing.insert(name='name', color='color', scale_check=1)
assert ents.Thing.query.count() == 1

row = ents.Thing.query.first()

if db.engine.dialect.name != 'sqlite':
assert row.id == ret_id
assert row.name == 'name'
assert row.color == 'color'
assert row.scale_check == 1

def test_insert_values_dict(self):
ents.Thing.insert(values={'name': 'name'}, color='color', scale_check=1)
row = ents.Thing.query.first()

assert row.name == 'name'
assert row.color == 'color'
assert row.scale_check == 1

def test_insert_multiple_pk(self):
ret_id = ents.MultiplePrimaryKeys.insert(name='name', id=54, other_pk=5)
assert ents.MultiplePrimaryKeys.query.count() == 1

row = ents.MultiplePrimaryKeys.query.first()

if db.engine.dialect.name != 'sqlite':
assert (54, 5) == ret_id
assert row.name == 'name'

def test_delete(self):
thing = ents.Thing.testing_create()
assert ents.Thing.query.count() == 1
Expand All @@ -80,6 +111,34 @@ def test_edit(self):
with pytest.raises(AttributeError):
ents.Thing.edit(name='edited')

def test_update(self):
thing1 = ents.Thing.testing_create()

ents.Thing.update(thing1.id, name='edited', color='silver')
db.session.commit()
db.session.refresh(thing1)

assert thing1.name == 'edited'
assert thing1.color == 'silver'

def test_update_values_dict(self):
thing1 = ents.Thing.testing_create()

ents.Thing.update(thing1.id, values={'name': 'edited'}, color='silver')
db.session.commit()
db.session.refresh(thing1)

assert thing1.name == 'edited'
assert thing1.color == 'silver'

def test_update_multiple_pk(self):
row = ents.MultiplePrimaryKeys.testing_create(id=55, other_pk=6, name='foo')
ents.MultiplePrimaryKeys.update((55, 6), name='bar')
db.session.commit()
db.session.refresh(row)

assert row.name == 'bar'

def test_from_dict(self):
# Testing create uses `from_dict` so create objects by hand
related = ents.RelatedThing(name='something')
Expand Down

0 comments on commit 60fac30

Please sign in to comment.