Custom primary/foreign key types (#5008)

* allow overriding the type of key used for primary/foreign keys of the different models

* rename key_types to singular key_type

* add some documentation for `database_key_definitions`
This commit is contained in:
Omer Lachish 2020-06-30 15:08:28 +03:00 committed by GitHub
parent efcf22079f
commit 004bc7a2ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 59 deletions

View File

@ -42,7 +42,7 @@ from redash.utils import (
from redash.utils.configuration import ConfigurationContainer
from redash.models.parameterized_query import ParameterizedQuery
from .base import db, gfk_type, Column, GFKBase, SearchBaseQuery
from .base import db, gfk_type, Column, GFKBase, SearchBaseQuery, key_type, primary_key
from .changes import ChangeTrackingMixin, Change # noqa
from .mixins import BelongsToOrgMixin, TimestampMixin
from .organizations import Organization
@ -83,8 +83,8 @@ scheduled_queries_executions = ScheduledQueriesExecutions()
@generic_repr("id", "name", "type", "org_id", "created_at")
class DataSource(BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("DataSource")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization, backref="data_sources")
name = Column(db.String(255))
@ -281,10 +281,10 @@ class DataSource(BelongsToOrgMixin, db.Model):
@generic_repr("id", "data_source_id", "group_id", "view_only")
class DataSourceGroup(db.Model):
# XXX drop id, use datasource/group as PK
id = Column(db.Integer, primary_key=True)
data_source_id = Column(db.Integer, db.ForeignKey("data_sources.id"))
id = primary_key("DataSourceGroup")
data_source_id = Column(key_type("DataSource"), db.ForeignKey("data_sources.id"))
data_source = db.relationship(DataSource, back_populates="data_source_groups")
group_id = Column(db.Integer, db.ForeignKey("groups.id"))
group_id = Column(key_type("Group"), db.ForeignKey("groups.id"))
group = db.relationship(Group, back_populates="data_sources")
view_only = Column(db.Boolean, default=False)
@ -319,10 +319,10 @@ QueryResultPersistence = (
@generic_repr("id", "org_id", "data_source_id", "query_hash", "runtime", "retrieved_at")
class QueryResult(db.Model, QueryResultPersistence, BelongsToOrgMixin):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("QueryResult")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization)
data_source_id = Column(db.Integer, db.ForeignKey("data_sources.id"))
data_source_id = Column(key_type("DataSource"), db.ForeignKey("data_sources.id"))
data_source = db.relationship(DataSource, backref=backref("query_results"))
query_hash = Column(db.String(32), index=True)
query_text = Column("query", db.Text)
@ -464,14 +464,14 @@ def should_schedule_next(
"schedule_failures",
)
class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
id = primary_key("Query")
version = Column(db.Integer, default=1)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization, backref="queries")
data_source_id = Column(db.Integer, db.ForeignKey("data_sources.id"), nullable=True)
data_source_id = Column(key_type("DataSource"), db.ForeignKey("data_sources.id"), nullable=True)
data_source = db.relationship(DataSource, backref="queries")
latest_query_data_id = Column(
db.Integer, db.ForeignKey("query_results.id"), nullable=True
key_type("QueryResult"), db.ForeignKey("query_results.id"), nullable=True
)
latest_query_data = db.relationship(QueryResult)
name = Column(db.String(255))
@ -479,9 +479,9 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
query_text = Column("query", db.Text)
query_hash = Column(db.String(32))
api_key = Column(db.String(40), default=lambda: generate_token(40))
user_id = Column(db.Integer, db.ForeignKey("users.id"))
user_id = Column(key_type("User"), db.ForeignKey("users.id"))
user = db.relationship(User, foreign_keys=[user_id])
last_modified_by_id = Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
last_modified_by_id = Column(key_type("User"), db.ForeignKey("users.id"), nullable=True)
last_modified_by = db.relationship(
User, backref="modified_queries", foreign_keys=[last_modified_by_id]
)
@ -875,11 +875,11 @@ def query_last_modified_by(target, val, oldval, initiator):
@generic_repr("id", "object_type", "object_id", "user_id", "org_id")
class Favorite(TimestampMixin, db.Model):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("Favorite")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
object_type = Column(db.Unicode(255))
object_id = Column(db.Integer)
object_id = Column(key_type("Favorite"))
object = generic_relationship(object_type, object_id)
user_id = Column(db.Integer, db.ForeignKey("users.id"))
@ -962,11 +962,11 @@ class Alert(TimestampMixin, BelongsToOrgMixin, db.Model):
OK_STATE = "ok"
TRIGGERED_STATE = "triggered"
id = Column(db.Integer, primary_key=True)
id = primary_key("Alert")
name = Column(db.String(255))
query_id = Column(db.Integer, db.ForeignKey("queries.id"))
query_id = Column(key_type("Query"), db.ForeignKey("queries.id"))
query_rel = db.relationship(Query, backref=backref("alerts", cascade="all"))
user_id = Column(db.Integer, db.ForeignKey("users.id"))
user_id = Column(key_type("User"), db.ForeignKey("users.id"))
user = db.relationship(User, backref="alerts")
options = Column(MutableDict.as_mutable(PseudoJSON))
state = Column(db.String(255), default=UNKNOWN_STATE)
@ -1073,13 +1073,13 @@ def generate_slug(ctx):
"id", "name", "slug", "user_id", "org_id", "version", "is_archived", "is_draft"
)
class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
id = primary_key("Dashboard")
version = Column(db.Integer)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization, backref="dashboards")
slug = Column(db.String(140), index=True, default=generate_slug)
name = Column(db.String(100))
user_id = Column(db.Integer, db.ForeignKey("users.id"))
user_id = Column(key_type("User"), db.ForeignKey("users.id"))
user = db.relationship(User)
# layout is no longer used, but kept so we know how to render old dashboards.
layout = Column(db.Text)
@ -1181,9 +1181,9 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
@generic_repr("id", "name", "type", "query_id")
class Visualization(TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
id = primary_key("Visualization")
type = Column(db.String(100))
query_id = Column(db.Integer, db.ForeignKey("queries.id"))
query_id = Column(key_type("Query"), db.ForeignKey("queries.id"))
# query_rel and not query, because db.Model already has query defined.
query_rel = db.relationship(Query, back_populates="visualizations")
name = Column(db.String(255))
@ -1210,9 +1210,9 @@ class Visualization(TimestampMixin, BelongsToOrgMixin, db.Model):
@generic_repr("id", "visualization_id", "dashboard_id")
class Widget(TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
id = primary_key("Widget")
visualization_id = Column(
db.Integer, db.ForeignKey("visualizations.id"), nullable=True
key_type("Visualization"), db.ForeignKey("visualizations.id"), nullable=True
)
visualization = db.relationship(
Visualization, backref=backref("widgets", cascade="delete")
@ -1220,7 +1220,7 @@ class Widget(TimestampMixin, BelongsToOrgMixin, db.Model):
text = Column(db.Text, nullable=True)
width = Column(db.Integer)
options = Column(db.Text)
dashboard_id = Column(db.Integer, db.ForeignKey("dashboards.id"), index=True)
dashboard_id = Column(key_type("Dashboard"), db.ForeignKey("dashboards.id"), index=True)
__tablename__ = "widgets"
@ -1236,10 +1236,10 @@ class Widget(TimestampMixin, BelongsToOrgMixin, db.Model):
"id", "object_type", "object_id", "action", "user_id", "org_id", "created_at"
)
class Event(db.Model):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("Event")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization, back_populates="events")
user_id = Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
user_id = Column(key_type("User"), db.ForeignKey("users.id"), nullable=True)
user = db.relationship(User, backref="events")
action = Column(db.String(255))
object_type = Column(db.String(255))
@ -1295,13 +1295,13 @@ class Event(db.Model):
@generic_repr("id", "created_by_id", "org_id", "active")
class ApiKey(TimestampMixin, GFKBase, db.Model):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("ApiKey")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization)
api_key = Column(db.String(255), index=True, default=lambda: generate_token(40))
active = Column(db.Boolean, default=True)
# 'object' provided by GFKBase
created_by_id = Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
created_by_id = Column(key_type("User"), db.ForeignKey("users.id"), nullable=True)
created_by = db.relationship(User)
__tablename__ = "api_keys"
@ -1330,10 +1330,10 @@ class ApiKey(TimestampMixin, GFKBase, db.Model):
@generic_repr("id", "name", "type", "user_id", "org_id", "created_at")
class NotificationDestination(BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("NotificationDestination")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization, backref="notification_destinations")
user_id = Column(db.Integer, db.ForeignKey("users.id"))
user_id = Column(key_type("User"), db.ForeignKey("users.id"))
user = db.relationship(User, backref="notification_destinations")
name = Column(db.String(255))
type = Column(db.String(255))
@ -1387,14 +1387,14 @@ class NotificationDestination(BelongsToOrgMixin, db.Model):
@generic_repr("id", "user_id", "destination_id", "alert_id")
class AlertSubscription(TimestampMixin, db.Model):
id = Column(db.Integer, primary_key=True)
user_id = Column(db.Integer, db.ForeignKey("users.id"))
id = primary_key("AlertSubscription")
user_id = Column(key_type("User"), db.ForeignKey("users.id"))
user = db.relationship(User)
destination_id = Column(
db.Integer, db.ForeignKey("notification_destinations.id"), nullable=True
key_type("NotificationDestination"), db.ForeignKey("notification_destinations.id"), nullable=True
)
destination = db.relationship(NotificationDestination)
alert_id = Column(db.Integer, db.ForeignKey("alerts.id"))
alert_id = Column(key_type("Alert"), db.ForeignKey("alerts.id"))
alert = db.relationship(Alert, back_populates="subscriptions")
__tablename__ = "alert_subscriptions"
@ -1435,12 +1435,12 @@ class AlertSubscription(TimestampMixin, db.Model):
@generic_repr("id", "trigger", "user_id", "org_id")
class QuerySnippet(TimestampMixin, db.Model, BelongsToOrgMixin):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("QuerySnippet")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship(Organization, backref="query_snippets")
trigger = Column(db.String(255), unique=True)
description = Column(db.Text)
user_id = Column(db.Integer, db.ForeignKey("users.id"))
user_id = Column(key_type("User"), db.ForeignKey("users.id"))
user = db.relationship(User, backref="query_snippets")
snippet = Column(db.Text)

View File

@ -90,3 +90,15 @@ class GFKBase(object):
self._object = value
self.object_type = value.__class__.__tablename__
self.object_id = value.id
key_definitions = settings.dynamic_settings.database_key_definitions((db.Integer, {}))
def key_type(name):
return key_definitions[name][0]
def primary_key(name):
key_type, kwargs = key_definitions[name]
return Column(key_type, primary_key=True, **kwargs)

View File

@ -1,13 +1,13 @@
from sqlalchemy.inspection import inspect
from sqlalchemy_utils.models import generic_repr
from .base import GFKBase, db, Column
from .base import GFKBase, db, Column, primary_key
from .types import PseudoJSON
@generic_repr("id", "object_type", "object_id", "created_at")
class Change(GFKBase, db.Model):
id = Column(db.Integer, primary_key=True)
id = primary_key("Change")
# 'object' defined in GFKBase
object_version = Column(db.Integer, default=0)
user_id = Column(db.Integer, db.ForeignKey("users.id"))

View File

@ -3,7 +3,7 @@ from sqlalchemy_utils.models import generic_repr
from redash.settings.organization import settings as org_settings
from .base import db, Column
from .base import db, Column, primary_key
from .mixins import TimestampMixin
from .types import MutableDict, PseudoJSON
from .users import User, Group
@ -14,7 +14,7 @@ class Organization(TimestampMixin, db.Model):
SETTING_GOOGLE_APPS_DOMAINS = "google_apps_domains"
SETTING_IS_PUBLIC = "is_public"
id = Column(db.Integer, primary_key=True)
id = primary_key("Organization")
name = Column(db.String(255))
slug = Column(db.String(255), unique=True)
settings = Column(MutableDict.as_mutable(PseudoJSON))

View File

@ -17,7 +17,7 @@ from sqlalchemy_utils.models import generic_repr
from redash import redis_connection
from redash.utils import generate_token, utcnow, dt_from_timestamp
from .base import db, Column, GFKBase
from .base import db, Column, GFKBase, key_type, primary_key
from .mixins import TimestampMixin, BelongsToOrgMixin
from .types import json_cast_property, MutableDict, MutableList
@ -80,15 +80,15 @@ class PermissionsCheckMixin(object):
class User(
TimestampMixin, db.Model, BelongsToOrgMixin, UserMixin, PermissionsCheckMixin
):
id = Column(db.Integer, primary_key=True)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
id = primary_key("User")
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship("Organization", backref=db.backref("users", lazy="dynamic"))
name = Column(db.String(320))
email = Column(EmailType)
_profile_image_url = Column("profile_image_url", db.String(320), nullable=True)
password_hash = Column(db.String(128), nullable=True)
group_ids = Column(
"groups", MutableList.as_mutable(postgresql.ARRAY(db.Integer)), nullable=True
"groups", MutableList.as_mutable(postgresql.ARRAY(key_type("Group"))), nullable=True
)
api_key = Column(db.String(40), default=lambda: generate_token(40), unique=True)
@ -275,11 +275,11 @@ class Group(db.Model, BelongsToOrgMixin):
BUILTIN_GROUP = "builtin"
REGULAR_GROUP = "regular"
id = Column(db.Integer, primary_key=True)
id = primary_key("Group")
data_sources = db.relationship(
"DataSourceGroup", back_populates="group", cascade="all"
)
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
org_id = Column(key_type("Organization"), db.ForeignKey("organizations.id"))
org = db.relationship("Organization", back_populates="groups")
type = Column(db.String(255), default=REGULAR_GROUP)
name = Column(db.String(100))
@ -318,12 +318,12 @@ class Group(db.Model, BelongsToOrgMixin):
"id", "object_type", "object_id", "access_type", "grantor_id", "grantee_id"
)
class AccessPermission(GFKBase, db.Model):
id = Column(db.Integer, primary_key=True)
id = primary_key("AccessPermission")
# 'object' defined in GFKBase
access_type = Column(db.String(255))
grantor_id = Column(db.Integer, db.ForeignKey("users.id"))
grantor_id = Column(key_type("User"), db.ForeignKey("users.id"))
grantor = db.relationship(User, backref="grantor", foreign_keys=[grantor_id])
grantee_id = Column(db.Integer, db.ForeignKey("users.id"))
grantee_id = Column(key_type("User"), db.ForeignKey("users.id"))
grantee = db.relationship(User, backref="grantee", foreign_keys=[grantee_id])
__tablename__ = "access_permissions"

View File

@ -1,3 +1,5 @@
from collections import defaultdict
# Replace this method with your own implementation in case you want to limit the time limit on certain queries or users.
def query_time_limit(is_scheduled, user_id, org_id):
from redash import settings
@ -36,4 +38,22 @@ def ssh_tunnel_auth():
return {
# 'ssh_pkey': 'path_to_private_key', # or instance of `paramiko.pkey.PKey`
# 'ssh_private_key_password': 'optional_passphrase_of_private_key',
}
}
def database_key_definitions(default):
"""
All primary/foreign keys in Redash are of type `db.Integer` by default.
You may choose to use different column types for primary/foreign keys. To do so, add an entry below for each model you'd like to modify.
For each model, add a tuple with the database type as the first item, and a dict including any kwargs for the column definition as the second item.
"""
definitions = defaultdict(lambda: default)
definitions.update(
{
# "DataSource": (db.String(255), {
# "default": generate_key
# })
}
)
return definitions