Use Postgres 11; enhance search query with websearch; really use indexes, add hash index; fix partially patching lots
This commit is contained in:
parent
04358a5506
commit
6c4c89ac48
|
@ -23,11 +23,8 @@ Devicehub is built with [Teal](https://github.com/bustawin/teal) and
|
||||||
The requirements are:
|
The requirements are:
|
||||||
|
|
||||||
- Python 3.5.3 or higher. In debian 9 is `# apt install python3-pip`.
|
- Python 3.5.3 or higher. In debian 9 is `# apt install python3-pip`.
|
||||||
- PostgreSQL 9.6 or higher with pgcrypto and ltree.
|
- [PostgreSQL 11 or higher](https://www.postgresql.org/download/).
|
||||||
In debian 9 is `# apt install postgresql-contrib`
|
- Weasyprint [dependencies](http://weasyprint.readthedocs.io/en/stable/install.html).
|
||||||
- passlib. In debian 9 is `# apt install python3-passlib`.
|
|
||||||
- Weasyprint requires some system packages.
|
|
||||||
[Their docs explain which ones and how to install them](http://weasyprint.readthedocs.io/en/stable/install.html).
|
|
||||||
|
|
||||||
Install Devicehub with *pip*: `pip3 install ereuse-devicehub -U --pre`.
|
Install Devicehub with *pip*: `pip3 install ereuse-devicehub -U --pre`.
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class JoinedTableMixin:
|
||||||
|
|
||||||
class Agent(Thing):
|
class Agent(Thing):
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||||
type = Column(Unicode, nullable=False, index=True)
|
type = Column(Unicode, nullable=False)
|
||||||
name = Column(CIText())
|
name = Column(CIText())
|
||||||
name.comment = """
|
name.comment = """
|
||||||
The name of the organization or person.
|
The name of the organization or person.
|
||||||
|
@ -46,7 +46,8 @@ class Agent(Thing):
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
UniqueConstraint(tax_id, country, name='Registration Number per country.'),
|
||||||
UniqueConstraint(tax_id, name, name='One tax ID with one name.')
|
UniqueConstraint(tax_id, name, name='One tax ID with one name.'),
|
||||||
|
db.Index('agent_type', type, postgresql_using='hash')
|
||||||
)
|
)
|
||||||
|
|
||||||
@declared_attr
|
@declared_attr
|
||||||
|
|
|
@ -7,7 +7,7 @@ from typing import Dict, List, Set
|
||||||
|
|
||||||
from boltons import urlutils
|
from boltons import urlutils
|
||||||
from citext import CIText
|
from citext import CIText
|
||||||
from ereuse_utils.naming import Naming, HID_CONVERSION_DOC
|
from ereuse_utils.naming import HID_CONVERSION_DOC, Naming
|
||||||
from more_itertools import unique_everseen
|
from more_itertools import unique_everseen
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
from sqlalchemy import BigInteger, Boolean, Column, Enum as DBEnum, Float, ForeignKey, Integer, \
|
||||||
Sequence, SmallInteger, Unicode, inspect, text
|
Sequence, SmallInteger, Unicode, inspect, text
|
||||||
|
@ -49,7 +49,7 @@ class Device(Thing):
|
||||||
The identifier of the device for this database. Used only
|
The identifier of the device for this database. Used only
|
||||||
internally for software; users should not use this.
|
internally for software; users should not use this.
|
||||||
"""
|
"""
|
||||||
type = Column(Unicode(STR_SM_SIZE), nullable=False, index=True)
|
type = Column(Unicode(STR_SM_SIZE), nullable=False)
|
||||||
hid = Column(Unicode(), check_lower('hid'), unique=True)
|
hid = Column(Unicode(), check_lower('hid'), unique=True)
|
||||||
hid.comment = """
|
hid.comment = """
|
||||||
The Hardware ID (HID) is the unique ID traceability systems
|
The Hardware ID (HID) is the unique ID traceability systems
|
||||||
|
@ -110,6 +110,11 @@ class Device(Thing):
|
||||||
'weight'
|
'weight'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('device_id', id, postgresql_using='hash'),
|
||||||
|
db.Index('type_index', type, postgresql_using='hash')
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kw) -> None:
|
def __init__(self, **kw) -> None:
|
||||||
super().__init__(**kw)
|
super().__init__(**kw)
|
||||||
with suppress(TypeError):
|
with suppress(TypeError):
|
||||||
|
@ -476,7 +481,7 @@ class Component(Device):
|
||||||
"""A device that can be inside another device."""
|
"""A device that can be inside another device."""
|
||||||
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
id = Column(BigInteger, ForeignKey(Device.id), primary_key=True)
|
||||||
|
|
||||||
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||||
parent = relationship(Computer,
|
parent = relationship(Computer,
|
||||||
backref=backref('components',
|
backref=backref('components',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -485,6 +490,10 @@ class Component(Device):
|
||||||
collection_class=OrderedSet),
|
collection_class=OrderedSet),
|
||||||
primaryjoin=parent_id == Computer.id)
|
primaryjoin=parent_id == Computer.id)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('parent_index', parent_id, postgresql_using='hash'),
|
||||||
|
)
|
||||||
|
|
||||||
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
def similar_one(self, parent: Computer, blacklist: Set[int]) -> 'Component':
|
||||||
"""
|
"""
|
||||||
Gets a component that:
|
Gets a component that:
|
||||||
|
@ -720,19 +729,21 @@ class Manufacturer(db.Model):
|
||||||
Ideally users should use the names from this list when submitting
|
Ideally users should use the names from this list when submitting
|
||||||
devices.
|
devices.
|
||||||
"""
|
"""
|
||||||
__table_args__ = {'schema': 'common'}
|
|
||||||
CSV_DELIMITER = csv.get_dialect('excel').delimiter
|
CSV_DELIMITER = csv.get_dialect('excel').delimiter
|
||||||
|
|
||||||
name = db.Column(CIText(),
|
name = db.Column(CIText(), primary_key=True)
|
||||||
primary_key=True,
|
|
||||||
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
|
||||||
index=db.Index('name', text('name gin_trgm_ops'), postgresql_using='gin'))
|
|
||||||
name.comment = """The normalized name of the manufacturer."""
|
name.comment = """The normalized name of the manufacturer."""
|
||||||
url = db.Column(URL(), unique=True)
|
url = db.Column(URL(), unique=True)
|
||||||
url.comment = """An URL to a page describing the manufacturer."""
|
url.comment = """An URL to a page describing the manufacturer."""
|
||||||
logo = db.Column(URL())
|
logo = db.Column(URL())
|
||||||
logo.comment = """An URL pointing to the logo of the manufacturer."""
|
logo.comment = """An URL pointing to the logo of the manufacturer."""
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
# from https://niallburkley.com/blog/index-columns-for-like-in-postgres/
|
||||||
|
db.Index('name_index', text('name gin_trgm_ops'), postgresql_using='gin'),
|
||||||
|
{'schema': 'common'}
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_all_to_session(cls, session: db.Session):
|
def add_all_to_session(cls, session: db.Session):
|
||||||
"""Adds all manufacturers to session."""
|
"""Adds all manufacturers to session."""
|
||||||
|
|
|
@ -24,18 +24,20 @@ class DeviceSearch(db.Model):
|
||||||
primary_key=True)
|
primary_key=True)
|
||||||
device = db.relationship(Device, primaryjoin=Device.id == device_id)
|
device = db.relationship(Device, primaryjoin=Device.id == device_id)
|
||||||
|
|
||||||
properties = db.Column(TSVECTOR,
|
properties = db.Column(TSVECTOR, nullable=False)
|
||||||
nullable=False,
|
tags = db.Column(TSVECTOR)
|
||||||
index=db.Index('properties gist',
|
|
||||||
postgresql_using='gist',
|
|
||||||
postgresql_concurrently=True))
|
|
||||||
tags = db.Column(TSVECTOR, index=db.Index('tags gist',
|
|
||||||
postgresql_using='gist',
|
|
||||||
postgresql_concurrently=True))
|
|
||||||
|
|
||||||
__table_args__ = {
|
__table_args__ = (
|
||||||
'prefixes': ['UNLOGGED'] # Only for temporal tables, can cause table to empty on turn on
|
# todo to add concurrency this should be commited separately
|
||||||
}
|
# see https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#indexes-with-concurrently
|
||||||
|
db.Index('properties gist', properties, postgresql_using='gist'),
|
||||||
|
db.Index('tags gist', tags, postgresql_using='gist'),
|
||||||
|
{
|
||||||
|
'prefixes': ['UNLOGGED']
|
||||||
|
# Only for temporal tables, can cause table to empty on turn on
|
||||||
|
}
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_modified_devices(cls, session: db.Session):
|
def update_modified_devices(cls, session: db.Session):
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Event(Thing):
|
||||||
This class extends `Schema's Action <https://schema.org/Action>`_.
|
This class extends `Schema's Action <https://schema.org/Action>`_.
|
||||||
"""
|
"""
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
|
||||||
type = Column(Unicode, nullable=False, index=True)
|
type = Column(Unicode, nullable=False)
|
||||||
name = Column(CIText(), default='', nullable=False)
|
name = Column(CIText(), default='', nullable=False)
|
||||||
name.comment = """
|
name.comment = """
|
||||||
A name or title for the event. Used when searching for events.
|
A name or title for the event. Used when searching for events.
|
||||||
|
@ -146,7 +146,7 @@ class Event(Thing):
|
||||||
For Add and Remove though, this has another meaning: the components
|
For Add and Remove though, this has another meaning: the components
|
||||||
that are added or removed.
|
that are added or removed.
|
||||||
"""
|
"""
|
||||||
parent_id = Column(BigInteger, ForeignKey(Computer.id), index=True)
|
parent_id = Column(BigInteger, ForeignKey(Computer.id))
|
||||||
parent = relationship(Computer,
|
parent = relationship(Computer,
|
||||||
backref=backref('events_parent',
|
backref=backref('events_parent',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -161,6 +161,12 @@ class Event(Thing):
|
||||||
would point to the computer that contained this data storage, if any.
|
would point to the computer that contained this data storage, if any.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('ix_id', id, postgresql_using='hash'),
|
||||||
|
db.Index('ix_type', type, postgresql_using='hash'),
|
||||||
|
db.Index('ix_parent_id', parent_id, postgresql_using='hash')
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def elapsed(self):
|
def elapsed(self):
|
||||||
"""Returns the elapsed time with seconds precision."""
|
"""Returns the elapsed time with seconds precision."""
|
||||||
|
@ -230,7 +236,7 @@ class JoinedWithOneDeviceMixin:
|
||||||
|
|
||||||
|
|
||||||
class EventWithOneDevice(JoinedTableMixin, Event):
|
class EventWithOneDevice(JoinedTableMixin, Event):
|
||||||
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False, index=True)
|
device_id = Column(BigInteger, ForeignKey(Device.id), nullable=False)
|
||||||
device = relationship(Device,
|
device = relationship(Device,
|
||||||
backref=backref('events_one',
|
backref=backref('events_one',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -239,6 +245,10 @@ class EventWithOneDevice(JoinedTableMixin, Event):
|
||||||
collection_class=OrderedSet),
|
collection_class=OrderedSet),
|
||||||
primaryjoin=Device.id == device_id)
|
primaryjoin=Device.id == device_id)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('event_one_device_id_index', device_id, postgresql_using='hash'),
|
||||||
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<{0.t} {0.id} {0.severity} device={0.device!r}>'.format(self)
|
return '<{0.t} {0.id} {0.severity} device={0.device!r}>'.format(self)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ from ereuse_devicehub.resources.models import Thing
|
||||||
|
|
||||||
|
|
||||||
class Inventory(Thing):
|
class Inventory(Thing):
|
||||||
__table_args__ = {'schema': 'common'}
|
|
||||||
id = db.Column(db.Unicode(), primary_key=True)
|
id = db.Column(db.Unicode(), primary_key=True)
|
||||||
id.comment = """The name of the inventory as in the URL and schema."""
|
id.comment = """The name of the inventory as in the URL and schema."""
|
||||||
name = db.Column(db.CIText(), nullable=False, unique=True)
|
name = db.Column(db.CIText(), nullable=False, unique=True)
|
||||||
|
@ -16,6 +15,11 @@ class Inventory(Thing):
|
||||||
tag_token.comment = """The token to access a Tag service."""
|
tag_token.comment = """The token to access a Tag service."""
|
||||||
org_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey('organization.id'), nullable=False)
|
org_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey('organization.id'), nullable=False)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('id_hash', id, postgresql_using='hash'),
|
||||||
|
{'schema': 'common'}
|
||||||
|
)
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def current(cls) -> 'Inventory':
|
def current(cls) -> 'Inventory':
|
||||||
"""The inventory of the current_app."""
|
"""The inventory of the current_app."""
|
||||||
|
|
|
@ -182,7 +182,7 @@ class Path(db.Model):
|
||||||
id = db.Column(db.UUID(as_uuid=True),
|
id = db.Column(db.UUID(as_uuid=True),
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
server_default=db.text('gen_random_uuid()'))
|
server_default=db.text('gen_random_uuid()'))
|
||||||
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False, index=True)
|
lot_id = db.Column(db.UUID(as_uuid=True), db.ForeignKey(Lot.id), nullable=False)
|
||||||
lot = db.relationship(Lot,
|
lot = db.relationship(Lot,
|
||||||
backref=db.backref('paths',
|
backref=db.backref('paths',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
|
@ -199,7 +199,8 @@ class Path(db.Model):
|
||||||
# dag.delete_edge needs to disable internally/temporarily the unique constraint
|
# dag.delete_edge needs to disable internally/temporarily the unique constraint
|
||||||
db.UniqueConstraint(path, name='path_unique', deferrable=True, initially='immediate'),
|
db.UniqueConstraint(path, name='path_unique', deferrable=True, initially='immediate'),
|
||||||
db.Index('path_gist', path, postgresql_using='gist'),
|
db.Index('path_gist', path, postgresql_using='gist'),
|
||||||
db.Index('path_btree', path, postgresql_using='btree')
|
db.Index('path_btree', path, postgresql_using='btree'),
|
||||||
|
db.Index('lot_id_index', lot_id, postgresql_using='hash')
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, lot: Lot) -> None:
|
def __init__(self, lot: Lot) -> None:
|
||||||
|
|
|
@ -41,7 +41,8 @@ class LotView(View):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def patch(self, id):
|
def patch(self, id):
|
||||||
l = request.get_json()
|
patch_schema = self.resource_def.SCHEMA(only=('name', 'description'), partial=True)
|
||||||
|
l = request.get_json(schema=patch_schema)
|
||||||
lot = Lot.query.filter_by(id=id).one()
|
lot = Lot.query.filter_by(id=id).one()
|
||||||
for key, value in l.items():
|
for key, value in l.items():
|
||||||
setattr(lot, key, value)
|
setattr(lot, key, value)
|
||||||
|
|
|
@ -30,12 +30,12 @@ class Search:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def match(column: db.Column, search: str, lang=LANG):
|
def match(column: db.Column, search: str, lang=LANG):
|
||||||
"""Query that matches a TSVECTOR column with search words."""
|
"""Query that matches a TSVECTOR column with search words."""
|
||||||
return column.op('@@')(db.func.plainto_tsquery(lang, search))
|
return column.op('@@')(db.func.websearch_to_tsquery(lang, search))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rank(column: db.Column, search: str, lang=LANG):
|
def rank(column: db.Column, search: str, lang=LANG):
|
||||||
"""Query that ranks a TSVECTOR column with search words."""
|
"""Query that ranks a TSVECTOR column with search words."""
|
||||||
return db.func.ts_rank(column, db.func.plainto_tsquery(lang, search))
|
return db.func.ts_rank(column, db.func.websearch_to_tsquery(lang, search))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _vectorize(col: db.Column, weight: Weight = Weight.D, lang=LANG):
|
def _vectorize(col: db.Column, weight: Weight = Weight.D, lang=LANG):
|
||||||
|
|
|
@ -44,8 +44,7 @@ class Tag(Thing):
|
||||||
"""
|
"""
|
||||||
device_id = Column(BigInteger,
|
device_id = Column(BigInteger,
|
||||||
# We don't want to delete the tag on device deletion, only set to null
|
# We don't want to delete the tag on device deletion, only set to null
|
||||||
ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL),
|
ForeignKey(Device.id, ondelete=DB_CASCADE_SET_NULL))
|
||||||
index=True)
|
|
||||||
device = relationship(Device,
|
device = relationship(Device,
|
||||||
backref=backref('tags', lazy=True, collection_class=Tags),
|
backref=backref('tags', lazy=True, collection_class=Tags),
|
||||||
primaryjoin=Device.id == device_id)
|
primaryjoin=Device.id == device_id)
|
||||||
|
@ -56,6 +55,10 @@ class Tag(Thing):
|
||||||
constraints as the main one. Only needed in special cases.
|
constraints as the main one. Only needed in special cases.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index('device_id_index', device_id, postgresql_using='hash'),
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, id: str, **kwargs) -> None:
|
def __init__(self, id: str, **kwargs) -> None:
|
||||||
super().__init__(id=id, **kwargs)
|
super().__init__(id=id, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -11,3 +11,4 @@ psql -d $1 -c "GRANT ALL PRIVILEGES ON DATABASE $1 TO $2;" # Give access to the
|
||||||
psql -d $1 -c "CREATE EXTENSION pgcrypto SCHEMA public;" # Enable pgcrypto
|
psql -d $1 -c "CREATE EXTENSION pgcrypto SCHEMA public;" # Enable pgcrypto
|
||||||
psql -d $1 -c "CREATE EXTENSION ltree SCHEMA public;" # Enable ltree
|
psql -d $1 -c "CREATE EXTENSION ltree SCHEMA public;" # Enable ltree
|
||||||
psql -d $1 -c "CREATE EXTENSION citext SCHEMA public;" # Enable citext
|
psql -d $1 -c "CREATE EXTENSION citext SCHEMA public;" # Enable citext
|
||||||
|
psql -d $1 -c "CREATE EXTENSION pg_trgm SCHEMA public;" # Enable pg_trgm
|
||||||
|
|
|
@ -75,6 +75,7 @@ def test_lot_modify_patch_endpoint_and_delete(user: UserClient):
|
||||||
l_after, _ = user.get(res=Lot, item=l['id'])
|
l_after, _ = user.get(res=Lot, item=l['id'])
|
||||||
assert l_after['name'] == 'bar'
|
assert l_after['name'] == 'bar'
|
||||||
assert l_after['description'] == 'bax'
|
assert l_after['description'] == 'bax'
|
||||||
|
user.patch({'description': 'bax'}, res=Lot, item=l['id'], status=204)
|
||||||
user.delete(res=Lot, item=l['id'], status=204)
|
user.delete(res=Lot, item=l['id'], status=204)
|
||||||
user.get(res=Lot, item=l['id'], status=404)
|
user.get(res=Lot, item=l['id'], status=404)
|
||||||
|
|
||||||
|
|
Reference in New Issue