2020-07-27 18:57:36 +00:00
|
|
|
"""
|
|
|
|
schema.py defines the schema of the tables describing Grist's own data structures. While users can
|
|
|
|
create tables, add and remove columns, etc, Grist stores various document metadata (about the
|
|
|
|
users' tables, views, etc.) also in tables.
|
|
|
|
|
|
|
|
Before changing this file, please review:
|
|
|
|
https://phab.getgrist.com/w/migrations/
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import itertools
|
|
|
|
from collections import OrderedDict, namedtuple
|
2021-06-22 15:12:25 +00:00
|
|
|
|
|
|
|
import six
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
import actions
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
SCHEMA_VERSION = 21
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def make_column(col_id, col_type, formula='', isFormula=False):
|
|
|
|
return {
|
|
|
|
"id": col_id,
|
|
|
|
"type": col_type,
|
|
|
|
"isFormula": isFormula,
|
|
|
|
"formula": formula
|
|
|
|
}
|
|
|
|
|
|
|
|
def schema_create_actions():
|
|
|
|
return [
|
|
|
|
# The document-wide metadata. It's all contained in a single record with id=1.
|
|
|
|
actions.AddTable("_grist_DocInfo", [
|
|
|
|
make_column("docId", "Text"), # DEPRECATED: docId is now stored in _gristsys_FileInfo
|
|
|
|
make_column("peers", "Text"), # DEPRECATED: now _grist_ACLPrincipals is used for this
|
|
|
|
|
|
|
|
# Basket id of the document for online storage, if a Basket has been created for it.
|
|
|
|
make_column("basketId", "Text"),
|
|
|
|
|
|
|
|
# Version number of the document. It tells us how to migrate it to reach SCHEMA_VERSION.
|
|
|
|
make_column("schemaVersion", "Int"),
|
|
|
|
|
|
|
|
# Document timezone.
|
|
|
|
make_column("timezone", "Text"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# The names of the user tables. This does NOT include built-in tables.
|
|
|
|
actions.AddTable("_grist_Tables", [
|
|
|
|
make_column("tableId", "Text"),
|
|
|
|
make_column("primaryViewId","Ref:_grist_Views"),
|
|
|
|
|
|
|
|
# For a summary table, this points to the corresponding source table.
|
|
|
|
make_column("summarySourceTable", "Ref:_grist_Tables"),
|
|
|
|
|
|
|
|
# A table may be marked as "onDemand", which will keep its data out of the data engine, and
|
|
|
|
# only available to the frontend when requested.
|
|
|
|
make_column("onDemand", "Bool")
|
|
|
|
]),
|
|
|
|
|
|
|
|
# All columns in all user tables.
|
|
|
|
actions.AddTable("_grist_Tables_column", [
|
|
|
|
make_column("parentId", "Ref:_grist_Tables"),
|
|
|
|
make_column("parentPos", "PositionNumber"),
|
|
|
|
make_column("colId", "Text"),
|
|
|
|
make_column("type", "Text"),
|
|
|
|
make_column("widgetOptions","Text"), # JSON extending column's widgetOptions
|
|
|
|
make_column("isFormula", "Bool"),
|
|
|
|
make_column("formula", "Text"),
|
|
|
|
make_column("label", "Text"),
|
|
|
|
|
|
|
|
# Normally a change to label changes colId as well, unless untieColIdFromLabel is True.
|
|
|
|
# (We intentionally pick a variable whose default value is false.)
|
|
|
|
make_column("untieColIdFromLabel", "Bool"),
|
|
|
|
|
|
|
|
# For a group-by column in a summary table, this points to the corresponding source column.
|
|
|
|
make_column("summarySourceCol", "Ref:_grist_Tables_column"),
|
|
|
|
# Points to a display column, if it exists, for this column.
|
|
|
|
make_column("displayCol", "Ref:_grist_Tables_column"),
|
|
|
|
# For Ref cols only, points to the column in the pointed-to table, which is to be displayed.
|
|
|
|
# E.g. Foo.person may have a visibleCol pointing to People.Name, with the displayCol
|
|
|
|
# pointing to Foo._gristHelper_DisplayX column with the formula "$person.Name".
|
|
|
|
make_column("visibleCol", "Ref:_grist_Tables_column"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# DEPRECATED: Previously used to keep import options, and allow the user to change them.
|
|
|
|
actions.AddTable("_grist_Imports", [
|
|
|
|
make_column("tableRef", "Ref:_grist_Tables"),
|
|
|
|
make_column("origFileName", "Text"),
|
|
|
|
make_column("parseFormula", "Text", isFormula=True,
|
|
|
|
formula="grist.parseImport(rec, table._engine)"),
|
|
|
|
|
|
|
|
# The following translate directly to csv module options. We can use csv.Sniffer to guess
|
|
|
|
# them based on a sample of the data (it also guesses hasHeaders option).
|
|
|
|
make_column("delimiter", "Text", formula="','"),
|
|
|
|
make_column("doublequote", "Bool", formula="True"),
|
|
|
|
make_column("escapechar", "Text"),
|
|
|
|
make_column("quotechar", "Text", formula="'\"'"),
|
|
|
|
make_column("skipinitialspace", "Bool"),
|
|
|
|
|
|
|
|
# Other parameters Grist understands.
|
|
|
|
make_column("encoding", "Text", formula="'utf8'"),
|
|
|
|
make_column("hasHeaders", "Bool"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# DEPRECATED: Previously - All external database credentials attached to the document
|
|
|
|
actions.AddTable("_grist_External_database", [
|
|
|
|
make_column("host", "Text"),
|
|
|
|
make_column("port", "Int"),
|
|
|
|
make_column("username", "Text"),
|
|
|
|
make_column("dialect", "Text"),
|
|
|
|
make_column("database", "Text"),
|
|
|
|
make_column("storage", "Text"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# DEPRECATED: Previously - Reference to a table from an external database
|
|
|
|
actions.AddTable("_grist_External_table", [
|
|
|
|
make_column("tableRef", "Ref:_grist_Tables"),
|
|
|
|
make_column("databaseRef", "Ref:_grist_External_database"),
|
|
|
|
make_column("tableName", "Text"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# Document tabs that represent a cross-reference between Tables and Views
|
|
|
|
actions.AddTable("_grist_TableViews", [
|
|
|
|
make_column("tableRef", "Ref:_grist_Tables"),
|
|
|
|
make_column("viewRef", "Ref:_grist_Views"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# DEPRECATED: Previously used to cross-reference between Tables and Views
|
|
|
|
actions.AddTable("_grist_TabItems", [
|
|
|
|
make_column("tableRef", "Ref:_grist_Tables"),
|
|
|
|
make_column("viewRef", "Ref:_grist_Views"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
actions.AddTable("_grist_TabBar", [
|
|
|
|
make_column("viewRef", "Ref:_grist_Views"),
|
|
|
|
make_column("tabPos", "PositionNumber"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# Table for storing the tree of pages. 'pagePos' and 'indentation' columns gives how a page is
|
|
|
|
# shown in the panel: 'pagePos' determines the page overall position when no pages are collapsed
|
|
|
|
# (ie: all pages are visible) and 'indentation' gives the level of nesting (depth). Note that
|
|
|
|
# the parent-child relationships between pages have to be inferred from the variation of
|
|
|
|
# `indentation` between consecutive pages. For instance a difference of +1 between two
|
|
|
|
# consecutive pages means that the second page is the child of the first page. A difference of 0
|
|
|
|
# means that both are siblings and a difference of -1 means that the second page is a sibling to
|
|
|
|
# the first page parent.
|
|
|
|
actions.AddTable("_grist_Pages", [
|
|
|
|
make_column("viewRef", "Ref:_grist_Views"),
|
|
|
|
make_column("indentation", "Int"),
|
|
|
|
make_column("pagePos", "PositionNumber"),
|
|
|
|
]),
|
|
|
|
|
|
|
|
# All user views.
|
|
|
|
actions.AddTable("_grist_Views", [
|
|
|
|
make_column("name", "Text"),
|
|
|
|
make_column("type", "Text"), # TODO: Should this be removed?
|
|
|
|
make_column("layoutSpec", "Text"), # JSON string describing the view layout
|
|
|
|
]),
|
|
|
|
|
|
|
|
# The sections of user views (e.g. a view may contain a list section and a detail section).
|
|
|
|
# Different sections may need different parameters, so this table includes columns for all
|
|
|
|
# possible parameters, and any given section will use some subset, depending on its type.
|
|
|
|
actions.AddTable("_grist_Views_section", [
|
|
|
|
make_column("tableRef", "Ref:_grist_Tables"),
|
|
|
|
make_column("parentId", "Ref:_grist_Views"),
|
|
|
|
# parentKey is the type of view section, such as 'list', 'detail', or 'single'.
|
|
|
|
# TODO: rename this (e.g. to "sectionType").
|
|
|
|
make_column("parentKey", "Text"),
|
|
|
|
make_column("title", "Text"),
|
|
|
|
make_column("defaultWidth", "Int", formula="100"),
|
|
|
|
make_column("borderWidth", "Int", formula="1"),
|
|
|
|
make_column("theme", "Text"),
|
|
|
|
make_column("options", "Text"),
|
|
|
|
make_column("chartType", "Text"),
|
|
|
|
make_column("layoutSpec", "Text"), # JSON string describing the record layout
|
|
|
|
# filterSpec is deprecated as of version 15. Do not remove or reuse.
|
|
|
|
make_column("filterSpec", "Text"),
|
|
|
|
make_column("sortColRefs", "Text"),
|
|
|
|
make_column("linkSrcSectionRef", "Ref:_grist_Views_section"),
|
|
|
|
make_column("linkSrcColRef", "Ref:_grist_Tables_column"),
|
|
|
|
make_column("linkTargetColRef", "Ref:_grist_Tables_column"),
|
|
|
|
# embedId is deprecated as of version 12. Do not remove or reuse.
|
|
|
|
make_column("embedId", "Text"),
|
|
|
|
]),
|
|
|
|
# The fields of a view section.
|
|
|
|
actions.AddTable("_grist_Views_section_field", [
|
|
|
|
make_column("parentId", "Ref:_grist_Views_section"),
|
|
|
|
make_column("parentPos", "PositionNumber"),
|
|
|
|
make_column("colRef", "Ref:_grist_Tables_column"),
|
|
|
|
make_column("width", "Int"),
|
|
|
|
make_column("widgetOptions","Text"), # JSON extending field's widgetOptions
|
|
|
|
# Points to a display column, if it exists, for this field.
|
|
|
|
make_column("displayCol", "Ref:_grist_Tables_column"),
|
|
|
|
# For Ref cols only, may override the column to be displayed fromin the pointed-to table.
|
|
|
|
make_column("visibleCol", "Ref:_grist_Tables_column"),
|
|
|
|
# JSON string describing the default filter as map from either an `included` or an
|
|
|
|
# `excluded` string to an array of column values:
|
|
|
|
# Ex1: { included: ['foo', 'bar'] }
|
|
|
|
# Ex2: { excluded: ['apple', 'orange'] }
|
|
|
|
make_column("filter", "Text")
|
|
|
|
]),
|
|
|
|
|
|
|
|
# The code for all of the validation rules available to a Grist document
|
|
|
|
actions.AddTable("_grist_Validations", [
|
|
|
|
make_column("formula", "Text"),
|
|
|
|
make_column("name", "Text"),
|
|
|
|
make_column("tableRef", "Int")
|
|
|
|
]),
|
|
|
|
|
|
|
|
# The input code and output text and compilation/runtime errors for usercode
|
|
|
|
actions.AddTable("_grist_REPL_Hist", [
|
|
|
|
make_column("code", "Text"),
|
|
|
|
make_column("outputText", "Text"),
|
|
|
|
make_column("errorText", "Text")
|
|
|
|
]),
|
|
|
|
|
|
|
|
# All of the attachments attached to this document.
|
|
|
|
actions.AddTable("_grist_Attachments", [
|
|
|
|
make_column("fileIdent", "Text"), # Checksum of the file contents. It identifies the file
|
|
|
|
# data in the _gristsys_Files table.
|
|
|
|
make_column("fileName", "Text"), # User defined file name
|
|
|
|
make_column("fileType", "Text"), # A string indicating the MIME type of the data
|
|
|
|
make_column("fileSize", "Int"), # The size in bytes
|
|
|
|
make_column("imageHeight", "Int"), # height in pixels
|
|
|
|
make_column("imageWidth", "Int"), # width in pixels
|
|
|
|
make_column("timeUploaded", "DateTime")
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
|
|
# All of the ACL rules.
|
|
|
|
actions.AddTable('_grist_ACLRules', [
|
|
|
|
make_column('resource', 'Ref:_grist_ACLResources'),
|
2020-11-17 21:49:32 +00:00
|
|
|
make_column('permissions', 'Int'), # DEPRECATED: permissionsText is used instead.
|
|
|
|
make_column('principals', 'Text'), # DEPRECATED
|
2020-07-27 18:57:36 +00:00
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
# Text of match formula, in restricted Python syntax; "" for default rule.
|
|
|
|
make_column('aclFormula', 'Text'),
|
|
|
|
|
|
|
|
make_column('aclColumn', 'Ref:_grist_Tables_column'), # DEPRECATED
|
|
|
|
|
|
|
|
# JSON representation of the parse tree of matchFunc; "" for default rule.
|
|
|
|
make_column('aclFormulaParsed', 'Text'),
|
|
|
|
|
|
|
|
# Permissions in the form '[+<bits>][-<bits>]' where <bits> is a string of
|
|
|
|
# C,R,U,D,S characters, each appearing at most once. Or the special values
|
|
|
|
# 'all' or 'none'. The empty string does not affect permissions.
|
|
|
|
make_column('permissionsText', 'Text'),
|
|
|
|
|
|
|
|
# Rules for one resource are ordered by increasing rulePos. The default rule
|
|
|
|
# should be at the end (later rules would have no effect).
|
|
|
|
make_column('rulePos', 'PositionNumber'),
|
|
|
|
|
|
|
|
# If non-empty, this rule adds extra user attributes. It should contain JSON
|
|
|
|
# of the form {name, tableId, lookupColId, charId}, and should be tied to the
|
|
|
|
# resource *:*. It acts by looking up user[charId] in the given tableId on the
|
|
|
|
# given lookupColId, and adds the full looked-up record as user[name], which
|
|
|
|
# becomes available to matchFunc. These rules are processed in order of rulePos,
|
|
|
|
# which should list them before regular rules.
|
|
|
|
make_column('userAttributes', 'Text'),
|
2020-07-27 18:57:36 +00:00
|
|
|
]),
|
|
|
|
|
2020-11-17 21:49:32 +00:00
|
|
|
# Note that the special resource with tableId of '' and colIds of '' should be ignored. It is
|
|
|
|
# present to satisfy older versions of Grist (before Nov 2020).
|
2020-07-27 18:57:36 +00:00
|
|
|
actions.AddTable('_grist_ACLResources', [
|
2020-11-17 21:49:32 +00:00
|
|
|
make_column('tableId', 'Text'), # Name of the table this rule applies to, or '*'
|
|
|
|
make_column('colIds', 'Text'), # Comma-separated list of colIds, or '*'
|
2020-07-27 18:57:36 +00:00
|
|
|
]),
|
|
|
|
|
2020-11-12 04:56:05 +00:00
|
|
|
# DEPRECATED: All of the principals used by ACL rules, including users, groups, and instances.
|
2020-07-27 18:57:36 +00:00
|
|
|
actions.AddTable('_grist_ACLPrincipals', [
|
|
|
|
make_column('type', 'Text'), # 'user', 'group', or 'instance'
|
|
|
|
make_column('userEmail', 'Text'), # For 'user' principals
|
|
|
|
make_column('userName', 'Text'), # For 'user' principals
|
|
|
|
make_column('groupName', 'Text'), # For 'group' principals
|
|
|
|
make_column('instanceId', 'Text'), # For 'instance' principals
|
|
|
|
|
|
|
|
# docmodel.py defines further `name` and `allInstances`, and members intended as helpers
|
|
|
|
# only: `memberships`, `children`, and `descendants`.
|
|
|
|
]),
|
|
|
|
|
2020-11-12 04:56:05 +00:00
|
|
|
# DEPRECATED: Table for containment relationships between Principals, e.g. user contains
|
|
|
|
# multiple instances, group contains multiple users, and groups may contain other groups.
|
2020-07-27 18:57:36 +00:00
|
|
|
actions.AddTable('_grist_ACLMemberships', [
|
|
|
|
make_column('parent', 'Ref:_grist_ACLPrincipals'),
|
|
|
|
make_column('child', 'Ref:_grist_ACLPrincipals'),
|
|
|
|
]),
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# These are little structs to represent the document schema that's used in code generation.
|
|
|
|
# Schema itself (as stored by Engine) is an OrderedDict(tableId -> SchemaTable), with
|
|
|
|
# SchemaTable.columns being an OrderedDict(colId -> SchemaColumn).
|
|
|
|
SchemaTable = namedtuple('SchemaTable', ('tableId', 'columns'))
|
|
|
|
SchemaColumn = namedtuple('SchemaColumn', ('colId', 'type', 'isFormula', 'formula'))
|
|
|
|
|
|
|
|
# Helpers to convert between schema structures and dicts used in schema actions.
|
|
|
|
def dict_to_col(col, col_id=None):
|
|
|
|
"""Convert dict as used in AddColumn/AddTable actions to a SchemaColumn object."""
|
|
|
|
return SchemaColumn(col_id or col["id"], col["type"], bool(col["isFormula"]), col["formula"])
|
|
|
|
|
|
|
|
def col_to_dict(col, include_id=True):
|
|
|
|
"""Convert SchemaColumn to dict to use in AddColumn/AddTable actions."""
|
|
|
|
ret = {"type": col.type, "isFormula": col.isFormula, "formula": col.formula}
|
|
|
|
if include_id:
|
|
|
|
ret["id"] = col.colId
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def dict_list_to_cols(dict_list):
|
|
|
|
"""Convert list of column dicts to an OrderedDict of SchemaColumns."""
|
|
|
|
return OrderedDict((c["id"], dict_to_col(c)) for c in dict_list)
|
|
|
|
|
|
|
|
def cols_to_dict_list(cols):
|
|
|
|
"""Convert OrderedDict of SchemaColumns to an array of column dicts."""
|
|
|
|
return [col_to_dict(c) for c in cols.values()]
|
|
|
|
|
|
|
|
def clone_schema(schema):
|
|
|
|
return OrderedDict((t, SchemaTable(s.tableId, s.columns.copy()))
|
2021-06-22 15:12:25 +00:00
|
|
|
for (t, s) in six.iteritems(schema))
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def build_schema(meta_tables, meta_columns, include_builtin=True):
|
|
|
|
"""
|
|
|
|
Arguments are TableData objects for the _grist_Tables and _grist_Tables_column tables.
|
|
|
|
Returns the schema object for engine.py, used in particular in gencode.py.
|
|
|
|
"""
|
|
|
|
assert meta_tables.table_id == '_grist_Tables'
|
|
|
|
assert meta_columns.table_id == '_grist_Tables_column'
|
|
|
|
|
|
|
|
# Schema is an OrderedDict.
|
|
|
|
schema = OrderedDict()
|
|
|
|
if include_builtin:
|
|
|
|
for t in schema_create_actions():
|
|
|
|
schema[t.table_id] = SchemaTable(t.table_id, dict_list_to_cols(t.columns))
|
|
|
|
|
|
|
|
# Construct a list of columns sorted by table and position.
|
|
|
|
collist = sorted(actions.transpose_bulk_action(meta_columns),
|
|
|
|
key=lambda c: (c.parentId, c.parentPos))
|
|
|
|
coldict = {t: list(cols) for t, cols in itertools.groupby(collist, lambda r: r.parentId)}
|
|
|
|
|
|
|
|
for t in actions.transpose_bulk_action(meta_tables):
|
2021-01-27 19:38:12 +00:00
|
|
|
columns = OrderedDict((c.colId, SchemaColumn(c.colId, c.type, bool(c.isFormula), c.formula))
|
2020-07-27 18:57:36 +00:00
|
|
|
for c in coldict[t.id])
|
|
|
|
schema[t.tableId] = SchemaTable(t.tableId, columns)
|
|
|
|
return schema
|