Support for query placeholders in db.select

This commit is contained in:
Fabio Manganiello 2022-09-04 00:28:08 +02:00
parent e77d6a4ad4
commit 1ea53a6f50
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774

View file

@ -3,9 +3,11 @@
""" """
import time import time
from typing import Optional
from sqlalchemy import create_engine, Table, MetaData from sqlalchemy import create_engine, Table, MetaData
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.sql import text
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
@ -23,10 +25,17 @@ class DbPlugin(Plugin):
def __init__(self, engine=None, *args, **kwargs): def __init__(self, engine=None, *args, **kwargs):
""" """
:param engine: Default SQLAlchemy connection engine string (e.g. ``sqlite:///:memory:`` or ``mysql://user:pass@localhost/test``) that will be used. You can override the default engine in the db actions. :param engine: Default SQLAlchemy connection engine string (e.g.
``sqlite:///:memory:`` or ``mysql://user:pass@localhost/test``)
that will be used. You can override the default engine in the db
actions.
:type engine: str :type engine: str
:param args: Extra arguments that will be passed to ``sqlalchemy.create_engine`` (see https://docs.sqlalchemy.org/en/latest/core/engines.html) :param args: Extra arguments that will be passed to
:param kwargs: Extra kwargs that will be passed to ``sqlalchemy.create_engine`` (seehttps:///docs.sqlalchemy.org/en/latest/core/engines.html) ``sqlalchemy.create_engine`` (see
https://docs.sqlalchemy.org/en/latest/core/engines.html)
:param kwargs: Extra kwargs that will be passed to
``sqlalchemy.create_engine``
(see https:///docs.sqlalchemy.org/en/latest/core/engines.html)
""" """
super().__init__() super().__init__()
@ -41,11 +50,11 @@ class DbPlugin(Plugin):
return create_engine(engine, *args, **kwargs) return create_engine(engine, *args, **kwargs)
assert self.engine
return self.engine return self.engine
# noinspection PyUnusedLocal
@staticmethod @staticmethod
def _build_condition(table, column, value): def _build_condition(table, column, value): # type: ignore
if isinstance(value, str): if isinstance(value, str):
value = "'{}'".format(value) value = "'{}'".format(value)
elif not isinstance(value, int) and not isinstance(value, float): elif not isinstance(value, int) and not isinstance(value, float):
@ -69,8 +78,12 @@ class DbPlugin(Plugin):
:type statement: str :type statement: str
:param engine: Engine to be used (default: default class engine) :param engine: Engine to be used (default: default class engine)
:type engine: str :type engine: str
:param args: Extra arguments that will be passed to ``sqlalchemy.create_engine`` (see https://docs.sqlalchemy.org/en/latest/core/engines.html) :param args: Extra arguments that will be passed to
:param kwargs: Extra kwargs that will be passed to ``sqlalchemy.create_engine`` (seehttps:///docs.sqlalchemy.org/en/latest/core/engines.html) ``sqlalchemy.create_engine`` (see
https://docs.sqlalchemy.org/en/latest/core/engines.html)
:param kwargs: Extra kwargs that will be passed to
``sqlalchemy.create_engine``
(see https:///docs.sqlalchemy.org/en/latest/core/engines.html)
""" """
engine = self._get_engine(engine, *args, **kwargs) engine = self._get_engine(engine, *args, **kwargs)
@ -106,24 +119,42 @@ class DbPlugin(Plugin):
return table, engine return table, engine
@action @action
def select(self, query=None, table=None, filter=None, engine=None, *args, **kwargs): def select(
self,
query=None,
table=None,
filter=None,
engine=None,
data: Optional[dict] = None,
*args,
**kwargs
):
""" """
Returns rows (as a list of hashes) given a query. Returns rows (as a list of hashes) given a query.
:param query: SQL to be executed :param query: SQL to be executed
:type query: str :type query: str
:param filter: Query WHERE filter expressed as a dictionary. This approach is preferred over specifying raw SQL :param filter: Query WHERE filter expressed as a dictionary. This
in ``query`` as the latter approach may be prone to SQL injection, unless you need to build some complex approach is preferred over specifying raw SQL
SQL logic. in ``query`` as the latter approach may be prone to SQL injection,
unless you need to build some complex SQL logic.
:type filter: dict :type filter: dict
:param table: If you specified a filter instead of a raw query, you'll have to specify the target table :param table: If you specified a filter instead of a raw query, you'll
have to specify the target table
:type table: str :type table: str
:param engine: Engine to be used (default: default class engine) :param engine: Engine to be used (default: default class engine)
:type engine: str :type engine: str
:param args: Extra arguments that will be passed to ``sqlalchemy.create_engine`` :param data: If ``query`` is an SQL string, then you can use
(see https://docs.sqlalchemy.org/en/latest/core/engines.html) SQLAlchemy's *placeholders* mechanism. You can specify placeholders
:param kwargs: Extra kwargs that will be passed to ``sqlalchemy.create_engine`` in the query for values that you want to be safely serialized, and
(seehttps:///docs.sqlalchemy.org/en/latest/core/engines.html) their values can be specified on the ``data`` attribute in a
``name`` ``value`` mapping format.
:param args: Extra arguments that will be passed to
``sqlalchemy.create_engine`` (see
https://docs.sqlalchemy.org/en/latest/core/engines.html)
:param kwargs: Extra kwargs that will be passed to
``sqlalchemy.create_engine`` (see
https:///docs.sqlalchemy.org/en/latest/core/engines.html)
:returns: List of hashes representing the result rows. :returns: List of hashes representing the result rows.
Examples: Examples:
@ -136,7 +167,10 @@ class DbPlugin(Plugin):
"action": "db.select", "action": "db.select",
"args": { "args": {
"engine": "sqlite:///:memory:", "engine": "sqlite:///:memory:",
"query": "SELECT id, name FROM table" "query": "SELECT id, name FROM table WHERE name = :name",
"data": {
"name": "foobar"
}
} }
} }
@ -165,19 +199,24 @@ class DbPlugin(Plugin):
engine = self._get_engine(engine, *args, **kwargs) engine = self._get_engine(engine, *args, **kwargs)
if isinstance(query, str):
query = text(query)
if table: if table:
table, engine = self._get_table(table, engine=engine, *args, **kwargs) table, engine = self._get_table(table, engine=engine, *args, **kwargs)
query = table.select() query = table.select()
if filter: if filter:
for (k,v) in filter.items(): for (k, v) in filter.items():
query = query.where(self._build_condition(table, k, v)) query = query.where(self._build_condition(table, k, v))
if query is None: if query is None:
raise RuntimeError('You need to specify either "query", or "table" and "filter"') raise RuntimeError(
'You need to specify either "query", or "table" and "filter"'
)
with engine.connect() as connection: with engine.connect() as connection:
result = connection.execute(query) result = connection.execute(query, **(data or {}))
columns = result.keys() columns = result.keys()
rows = [ rows = [
{col: row[i] for i, col in enumerate(list(columns))} {col: row[i] for i, col in enumerate(list(columns))}
@ -187,8 +226,16 @@ class DbPlugin(Plugin):
return rows return rows
@action @action
def insert(self, table, records, engine=None, key_columns=None, def insert(
on_duplicate_update=False, *args, **kwargs): self,
table,
records,
engine=None,
key_columns=None,
on_duplicate_update=False,
*args,
**kwargs
):
""" """
Inserts records (as a list of hashes) into a table. Inserts records (as a list of hashes) into a table.
@ -198,12 +245,20 @@ class DbPlugin(Plugin):
:type records: list :type records: list
:param engine: Engine to be used (default: default class engine) :param engine: Engine to be used (default: default class engine)
:type engine: str :type engine: str
:param key_columns: Set it to specify the names of the key columns for ``table``. Set it if you want your statement to be executed with the ``on_duplicate_update`` flag. :param key_columns: Set it to specify the names of the key columns for
``table``. Set it if you want your statement to be executed with
the ``on_duplicate_update`` flag.
:type key_columns: list :type key_columns: list
:param on_duplicate_update: If set, update the records in case of duplicate rows (default: False). If set, you'll need to specify ``key_columns`` as well. :param on_duplicate_update: If set, update the records in case of
duplicate rows (default: False). If set, you'll need to specify
``key_columns`` as well.
:type on_duplicate_update: bool :type on_duplicate_update: bool
:param args: Extra arguments that will be passed to ``sqlalchemy.create_engine`` (see https://docs.sqlalchemy.org/en/latest/core/engines.html) :param args: Extra arguments that will be passed to
:param kwargs: Extra kwargs that will be passed to ``sqlalchemy.create_engine`` (seehttps:///docs.sqlalchemy.org/en/latest/core/engines.html) ``sqlalchemy.create_engine`` (see
https://docs.sqlalchemy.org/en/latest/core/engines.html)
:param kwargs: Extra kwargs that will be passed to
``sqlalchemy.create_engine``
(see https:///docs.sqlalchemy.org/en/latest/core/engines.html)
Example: Example:
@ -238,15 +293,21 @@ class DbPlugin(Plugin):
for record in records: for record in records:
table, engine = self._get_table(table, engine=engine, *args, **kwargs) table, engine = self._get_table(table, engine=engine, *args, **kwargs)
insert = table.insert().values(**record) insert = table.insert().values(**record)
try: try:
engine.execute(insert) engine.execute(insert)
except Exception as e: except Exception as e:
if on_duplicate_update and key_columns: if on_duplicate_update and key_columns:
self.update(table=table, records=records, self.update(
key_columns=key_columns, engine=engine, table=table,
*args, **kwargs) records=records,
key_columns=key_columns,
engine=engine,
*args,
**kwargs
)
else: else:
raise e raise e
@ -263,8 +324,12 @@ class DbPlugin(Plugin):
:type key_columns: list :type key_columns: list
:param engine: Engine to be used (default: default class engine) :param engine: Engine to be used (default: default class engine)
:type engine: str :type engine: str
:param args: Extra arguments that will be passed to ``sqlalchemy.create_engine`` (see https://docs.sqlalchemy.org/en/latest/core/engines.html) :param args: Extra arguments that will be passed to
:param kwargs: Extra kwargs that will be passed to ``sqlalchemy.create_engine`` (seehttps:///docs.sqlalchemy.org/en/latest/core/engines.html) ``sqlalchemy.create_engine`` (see
https://docs.sqlalchemy.org/en/latest/core/engines.html)
:param kwargs: Extra kwargs that will be passed to
``sqlalchemy.create_engine``
(see https:///docs.sqlalchemy.org/en/latest/core/engines.html)
Example: Example:
@ -297,12 +362,12 @@ class DbPlugin(Plugin):
for record in records: for record in records:
table, engine = self._get_table(table, engine=engine, *args, **kwargs) table, engine = self._get_table(table, engine=engine, *args, **kwargs)
key = { k:v for (k,v) in record.items() if k in key_columns } key = {k: v for (k, v) in record.items() if k in key_columns}
values = { k:v for (k,v) in record.items() if k not in key_columns } values = {k: v for (k, v) in record.items() if k not in key_columns}
update = table.update() update = table.update()
for (k,v) in key.items(): for (k, v) in key.items():
update = update.where(self._build_condition(table, k, v)) update = update.where(self._build_condition(table, k, v))
update = update.values(**values) update = update.values(**values)
@ -319,8 +384,12 @@ class DbPlugin(Plugin):
:type records: list :type records: list
:param engine: Engine to be used (default: default class engine) :param engine: Engine to be used (default: default class engine)
:type engine: str :type engine: str
:param args: Extra arguments that will be passed to ``sqlalchemy.create_engine`` (see https://docs.sqlalchemy.org/en/latest/core/engines.html) :param args: Extra arguments that will be passed to
:param kwargs: Extra kwargs that will be passed to ``sqlalchemy.create_engine`` (seehttps:///docs.sqlalchemy.org/en/latest/core/engines.html) ``sqlalchemy.create_engine`` (see
https://docs.sqlalchemy.org/en/latest/core/engines.html)
:param kwargs: Extra kwargs that will be passed to
``sqlalchemy.create_engine``
(see https:///docs.sqlalchemy.org/en/latest/core/engines.html)
Example: Example:
@ -347,7 +416,7 @@ class DbPlugin(Plugin):
table, engine = self._get_table(table, engine=engine, *args, **kwargs) table, engine = self._get_table(table, engine=engine, *args, **kwargs)
delete = table.delete() delete = table.delete()
for (k,v) in record.items(): for (k, v) in record.items():
delete = delete.where(self._build_condition(table, k, v)) delete = delete.where(self._build_condition(table, k, v))
engine.execute(delete) engine.execute(delete)