mirror of
https://github.com/valitydev/redash.git
synced 2024-11-06 09:05:17 +00:00
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:
parent
efcf22079f
commit
004bc7a2ac
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user