2020-07-27 18:57:36 +00:00
|
|
|
import collections
|
2021-07-19 13:04:05 +00:00
|
|
|
import itertools
|
2023-07-18 15:20:02 +00:00
|
|
|
import logging
|
2020-07-27 18:57:36 +00:00
|
|
|
import types
|
|
|
|
|
2021-06-22 15:12:25 +00:00
|
|
|
import six
|
|
|
|
from six.moves import xrange
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
import column
|
|
|
|
import depend
|
|
|
|
import docmodel
|
2022-06-07 14:57:29 +00:00
|
|
|
import functions
|
2020-07-27 18:57:36 +00:00
|
|
|
import lookup
|
2023-02-04 16:20:13 +00:00
|
|
|
from records import adjust_record, Record as BaseRecord, RecordSet as BaseRecordSet
|
2020-07-27 18:57:36 +00:00
|
|
|
import relation as relation_module # "relation" is used too much as a variable name below.
|
|
|
|
import usertypes
|
|
|
|
|
2023-07-18 15:20:02 +00:00
|
|
|
log = logging.getLogger(__name__)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_default_func_name(col_id):
|
|
|
|
return "_default_" + col_id
|
|
|
|
|
|
|
|
def get_validation_func_name(index):
|
|
|
|
return "validation___%d" % index
|
|
|
|
|
|
|
|
class UserTable(object):
|
|
|
|
"""
|
|
|
|
Each data table in the document is represented in the code by an instance of `UserTable` class.
|
|
|
|
These names are always capitalized. A UserTable provides access to all the records in the table,
|
|
|
|
as well as methods to look up particular records.
|
|
|
|
|
|
|
|
Every table in the document is available to all formulas.
|
|
|
|
"""
|
|
|
|
# UserTables are only created in auto-generated code by using UserTable as decorator for a table
|
|
|
|
# model class. I.e.
|
|
|
|
#
|
|
|
|
# @grist.UserTable
|
|
|
|
# class Students:
|
|
|
|
# ...
|
|
|
|
#
|
|
|
|
# makes the "Students" identifier an actual UserTable instance, so that Students.lookupRecords
|
|
|
|
# and so on can be used.
|
|
|
|
|
|
|
|
def __init__(self, model_class):
|
|
|
|
docmodel.enhance_model(model_class)
|
|
|
|
self.Model = model_class
|
|
|
|
self.table = None
|
|
|
|
|
|
|
|
def _set_table_impl(self, table_impl):
|
|
|
|
self.table = table_impl
|
|
|
|
|
2021-07-16 18:15:04 +00:00
|
|
|
@property
|
|
|
|
def Record(self):
|
|
|
|
return self.table.Record
|
|
|
|
|
|
|
|
@property
|
|
|
|
def RecordSet(self):
|
|
|
|
return self.table.RecordSet
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
# Note these methods are named camelCase since they are a public interface exposed to formulas,
|
|
|
|
# and we decided camelCase was a more user-friendly choice for user-facing functions.
|
|
|
|
def lookupRecords(self, **field_value_pairs):
|
|
|
|
"""
|
2022-02-13 05:38:24 +00:00
|
|
|
Name: lookupRecords
|
|
|
|
Usage: UserTable.__lookupRecords__(Field_In_Lookup_Table=value, ...)
|
2023-02-04 16:20:13 +00:00
|
|
|
Returns a [RecordSet](#recordset) matching the given field=value arguments. The value may be
|
|
|
|
any expression,
|
2022-02-13 05:45:24 +00:00
|
|
|
most commonly a field in the current row (e.g. `$SomeField`) or a constant (e.g. a quoted string
|
|
|
|
like `"Some Value"`) (examples below).
|
2024-06-12 21:47:25 +00:00
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
For example:
|
|
|
|
```
|
2022-02-13 05:45:24 +00:00
|
|
|
People.lookupRecords(Email=$Work_Email)
|
2020-07-27 18:57:36 +00:00
|
|
|
People.lookupRecords(First_Name="George", Last_Name="Washington")
|
|
|
|
```
|
|
|
|
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
You may set the optional `order_by` parameter to the column ID by which to sort the results.
|
|
|
|
You can prefix the column ID with "-" to reverse the order. You can also specify multiple
|
|
|
|
column IDs as a tuple (e.g. `order_by=("Account", "-Date")`).
|
|
|
|
|
|
|
|
For example:
|
|
|
|
```
|
|
|
|
Transactions.lookupRecords(Account=$Account, order_by="Date")
|
|
|
|
Transactions.lookupRecords(Account=$Account, order_by="-Date")
|
|
|
|
Transactions.lookupRecords(Active=True, order_by=("Account", "-Date"))
|
|
|
|
```
|
|
|
|
|
|
|
|
For records with equal `order_by` fields, the results are sorted according to how they appear
|
|
|
|
in views (which is determined by the special `manualSort` column). You may set `order_by=None`
|
|
|
|
to match the order of records in unsorted views.
|
|
|
|
|
|
|
|
By default, with no `order_by`, records are sorted by row ID, as if with `order_by="id"`.
|
|
|
|
|
|
|
|
For backward compatibility, `sort_by` may be used instead of `order_by`, but only allows a
|
|
|
|
single field, and falls back to row ID (rather than `manualSort`).
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
See [RecordSet](#recordset) for useful properties offered by the returned object.
|
2022-01-06 23:58:54 +00:00
|
|
|
|
|
|
|
See [CONTAINS](#contains) for an example utilizing `UserTable.lookupRecords` to find records
|
|
|
|
where a field of a list type (such as `Choice List` or `Reference List`) contains the given
|
|
|
|
value.
|
2024-06-12 21:47:25 +00:00
|
|
|
|
|
|
|
Learn more about [lookupRecords](references-lookups.md#lookuprecords).
|
2020-07-27 18:57:36 +00:00
|
|
|
"""
|
|
|
|
return self.table.lookup_records(**field_value_pairs)
|
|
|
|
|
|
|
|
def lookupOne(self, **field_value_pairs):
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
# pylint: disable=line-too-long
|
2020-07-27 18:57:36 +00:00
|
|
|
"""
|
2022-02-13 05:38:24 +00:00
|
|
|
Name: lookupOne
|
|
|
|
Usage: UserTable.__lookupOne__(Field_In_Lookup_Table=value, ...)
|
2023-02-04 16:20:13 +00:00
|
|
|
Returns a [Record](#record) matching the given field=value arguments. The value may be any
|
|
|
|
expression,
|
2022-02-13 05:45:24 +00:00
|
|
|
most commonly a field in the current row (e.g. `$SomeField`) or a constant (e.g. a quoted string
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
like `"Some Value"`).
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
For example:
|
|
|
|
```
|
|
|
|
People.lookupOne(First_Name="Lewis", Last_Name="Carroll")
|
2022-02-13 05:45:24 +00:00
|
|
|
People.lookupOne(Email=$Work_Email)
|
2020-07-27 18:57:36 +00:00
|
|
|
```
|
2024-06-12 21:47:25 +00:00
|
|
|
|
|
|
|
Learn more about [lookupOne](references-lookups.md#lookupone).
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
|
|
|
|
If multiple records are found, the first match is returned. You may set the optional `order_by`
|
|
|
|
parameter to the column ID by which to sort the matches, to determine which of them is
|
|
|
|
returned as the first one. By default, the record with the lowest row ID is returned.
|
|
|
|
|
|
|
|
See [`lookupRecords`](#lookupRecords) for details of all available options and behavior of
|
|
|
|
`order_by` (and of its legacy alternative, `sort_by`).
|
|
|
|
|
|
|
|
For example:
|
|
|
|
```
|
|
|
|
Tasks.lookupOne(Project=$id, order_by="Priority") # Returns the Task with the smallest Priority.
|
|
|
|
Rates.lookupOne(Person=$id, order_by="-Date") # Returns the Rate with the latest Date.
|
|
|
|
```
|
2020-07-27 18:57:36 +00:00
|
|
|
"""
|
|
|
|
return self.table.lookup_one_record(**field_value_pairs)
|
|
|
|
|
|
|
|
def lookupOrAddDerived(self, **kwargs):
|
|
|
|
return self.table.lookupOrAddDerived(**kwargs)
|
|
|
|
|
|
|
|
def getSummarySourceGroup(self, rec):
|
|
|
|
return self.table.getSummarySourceGroup(rec)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def all(self):
|
|
|
|
"""
|
|
|
|
Name: all
|
|
|
|
Usage: UserTable.__all__
|
|
|
|
|
|
|
|
The list of all the records in this table.
|
|
|
|
|
|
|
|
For example, this evaluates to the number of records in the table `Students`.
|
|
|
|
```
|
|
|
|
len(Students.all)
|
|
|
|
```
|
|
|
|
|
|
|
|
This evaluates to the sum of the `Population` field for every record in the table `Countries`.
|
|
|
|
```
|
|
|
|
sum(r.Population for r in Countries.all)
|
|
|
|
```
|
|
|
|
"""
|
|
|
|
return self.lookupRecords()
|
|
|
|
|
|
|
|
def __dir__(self):
|
|
|
|
# Suppress member properties when listing dir(TableClass). This affects rlcompleter, with the
|
|
|
|
# result that auto-complete will only return class properties, not member properties added in
|
|
|
|
# the constructor.
|
|
|
|
return []
|
|
|
|
|
2022-08-20 18:02:45 +00:00
|
|
|
def __getattr__(self, item):
|
|
|
|
if self.table.has_column(item):
|
|
|
|
raise AttributeError(
|
|
|
|
"To retrieve all values in a column, use `{table_id}.all.{item}`. "
|
|
|
|
"Tables have no attribute '{item}'".format(table_id=self.table.table_id, item=item)
|
|
|
|
)
|
|
|
|
super(UserTable, self).__getattribute__(item)
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
raise TypeError(
|
|
|
|
"To iterate (loop) over all records in a table, use `{table_id}.all`. "
|
|
|
|
"Tables are not directly iterable.".format(table_id=self.table.table_id)
|
|
|
|
)
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
class Table(object):
|
|
|
|
"""
|
|
|
|
Table represents a table with all its columns and data.
|
|
|
|
"""
|
|
|
|
|
|
|
|
class RowIDs(object):
|
|
|
|
"""
|
|
|
|
Helper container that represents the set of valid row IDs in this table.
|
|
|
|
"""
|
|
|
|
def __init__(self, id_column):
|
|
|
|
self._id_column = id_column
|
|
|
|
|
|
|
|
def __contains__(self, row_id):
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
return 0 < row_id < self._id_column.size() and self._id_column.raw_get(row_id) > 0
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
for row_id in xrange(self._id_column.size()):
|
|
|
|
if self._id_column.raw_get(row_id) > 0:
|
|
|
|
yield row_id
|
|
|
|
|
|
|
|
def max(self):
|
|
|
|
last = self._id_column.size() - 1
|
|
|
|
while last > 0 and last not in self:
|
|
|
|
last -= 1
|
|
|
|
return last
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, table_id, engine):
|
|
|
|
# The id of the table is the name of its class.
|
|
|
|
self.table_id = table_id
|
|
|
|
|
|
|
|
# Each table maintains a reference to the engine that owns it.
|
|
|
|
self._engine = engine
|
|
|
|
|
|
|
|
# The UserTable object for this table, set in _rebuild_model
|
|
|
|
self.user_table = None
|
|
|
|
|
|
|
|
# Store the identity Relation for this table.
|
|
|
|
self._identity_relation = relation_module.IdentityRelation(table_id)
|
|
|
|
|
|
|
|
# Set of ReferenceColumn objects that refer to this table
|
|
|
|
self._back_references = set()
|
|
|
|
|
|
|
|
# Store the constant Node for "new columns". Accessing invalid columns creates a dependency
|
|
|
|
# on this node, and triggers recomputation when columns are added or renamed.
|
|
|
|
self._new_columns_node = depend.Node(self.table_id, None)
|
|
|
|
|
|
|
|
# Collection of special columns that this table maintains, which include LookupMapColumns
|
|
|
|
# and formula columns for maintaining summary tables. These persist across table rebuilds, and
|
|
|
|
# get cleaned up with delete_column().
|
|
|
|
self._special_cols = {}
|
|
|
|
|
|
|
|
# Maintain Column objects both as a mapping from col_id and as an ordered list.
|
|
|
|
self.all_columns = collections.OrderedDict()
|
|
|
|
|
|
|
|
# This column is always present.
|
|
|
|
self._id_column = column.create_column(self, 'id', column.get_col_info(usertypes.Id()))
|
|
|
|
|
|
|
|
# The `row_ids` member offers some useful interfaces:
|
|
|
|
# * if row_id in table.row_ids
|
|
|
|
# * for row_id in table.row_ids
|
|
|
|
self.row_ids = self.RowIDs(self._id_column)
|
|
|
|
|
|
|
|
# For a summary table, this is a reference to the Table object for the source table.
|
|
|
|
self._summary_source_table = None
|
|
|
|
|
|
|
|
# For a summary table, the name of the special helper column auto-added to the source table.
|
|
|
|
self._summary_helper_col_id = None
|
|
|
|
|
2021-07-19 13:04:05 +00:00
|
|
|
# For a summary table, True in the common case where every source record belongs
|
|
|
|
# to just one group in the summary table, False if grouping by list columns
|
|
|
|
# which are 'flattened' so source records may appear in multiple groups
|
|
|
|
self._summary_simple = None
|
|
|
|
|
2023-02-04 16:20:13 +00:00
|
|
|
# Add Record and RecordSet subclasses with correct `_table` attribute, which will also hold a
|
|
|
|
# field attribute for each column.
|
|
|
|
class Record(BaseRecord):
|
|
|
|
__slots__ = ()
|
|
|
|
_table = self
|
2022-02-21 14:19:11 +00:00
|
|
|
|
2023-02-04 16:20:13 +00:00
|
|
|
class RecordSet(BaseRecordSet):
|
|
|
|
__slots__ = ()
|
|
|
|
_table = self
|
2021-07-16 18:15:04 +00:00
|
|
|
|
|
|
|
self.Record = Record
|
|
|
|
self.RecordSet = RecordSet
|
|
|
|
|
2023-02-04 16:20:13 +00:00
|
|
|
# For use in _num_rows. The attribute isn't strictly needed,
|
|
|
|
# but it makes _num_rows slightly faster, and only creating the lookup map when _num_rows
|
|
|
|
# is called seems to be too late, at least for unit tests.
|
|
|
|
self._empty_lookup_column = self._get_lookup_map(())
|
|
|
|
|
2022-02-21 14:19:11 +00:00
|
|
|
def _num_rows(self):
|
|
|
|
"""
|
|
|
|
Similar to `len(self.lookup_records())` but faster and doesn't create dependencies.
|
|
|
|
"""
|
|
|
|
return len(self._empty_lookup_column._do_fast_empty_lookup())
|
|
|
|
|
2022-03-22 21:19:31 +00:00
|
|
|
@property
|
|
|
|
def sample_record(self):
|
|
|
|
"""
|
|
|
|
Used for auto-completion as a record with correct properties of correct types.
|
|
|
|
"""
|
2022-08-16 19:18:19 +00:00
|
|
|
# Create a type with a property for each column. We use property-methods rather than
|
|
|
|
# plain attributes because this sample record is created before all tables have initialized, so
|
|
|
|
# reference values (using .sample_record for other tables) are not yet available.
|
|
|
|
props = {}
|
|
|
|
for col in self.all_columns.values():
|
(core) Show example values in formula autocomplete
Summary:
This diff adds a preview of the value of certain autocomplete suggestions, especially of the form `$foo.bar` or `user.email`. The main initial motivation was to show the difference between `$Ref` and `$Ref.DisplayCol`, but the feature is more general.
The client now sends the row ID of the row being edited (along with the table and column IDs which were already sent) to the server to fetch autocomplete suggestions. The returned suggestions are now tuples `(suggestion, example_value)` where `example_value` is a string or null. The example value is simply obtained by evaluating (in a controlled way) the suggestion in the context of the given record and the current user. The string representation is similar to the standard `repr` but dates and datetimes are formatted, and the whole thing is truncated for efficiency.
The example values are shown in the autocomplete popup separated from the actual suggestion by a number of spaces calculated to:
1. Clearly separate the suggestion from the values
2. Left-align the example values in most cases
3. Avoid having so much space such that connecting suggestions and values becomes visually difficult.
The tokenization of the row is then tweaked to show the example in light grey to deemphasise it.
Main discussion where the above was decided: https://grist.slack.com/archives/CDHABLZJT/p1661795588100009
The diff also includes various other small improvements and fixes:
- The autocomplete popup is much wider to make room for long suggestions, particularly lookups, as pointed out in https://phab.getgrist.com/D3580#inline-41007. The wide popup is the reason a fancy solution was needed to position the example values. I didn't see a way to dynamically resize the popup based on suggestions, and it didn't seem like a good idea to try.
- The `grist` and `python` labels previously shown on the right are removed. They were not helpful (https://grist.slack.com/archives/CDHABLZJT/p1659697086155179) and would get in the way of the example values.
- Fixed a bug in our custom tokenization that caused function arguments to be weirdly truncated in the middle: https://grist.slack.com/archives/CDHABLZJT/p1661956353699169?thread_ts=1661953258.342739&cid=CDHABLZJT and https://grist.slack.com/archives/C069RUP71/p1659696778991339
- Hide suggestions involving helper columns like `$gristHelper_Display` or `Table.lookupRecords(gristHelper_Display=` (https://grist.slack.com/archives/CDHABLZJT/p1661953258342739). The former has been around for a while and seems to be a mistake. The fix is simply to use `is_visible_column` instead of `is_user_column`. Since the latter is not used anywhere else, and using it in the first place seems like a mistake more than anything else, I've also removed the function to prevent similar mistakes in the future.
- Don't suggest private columns as lookup arguments: https://grist.slack.com/archives/CDHABLZJT/p1662133416652499?thread_ts=1661795588.100009&cid=CDHABLZJT
- Only fetch fresh suggestions specifically after typing `lookupRecords(` or `lookupOne(` rather than just `(`, as this would needlessly hide function suggestions which could still be useful to see the arguments. However this only makes a difference when there are still multiple matching suggestions, otherwise Ace hides them anyway.
Test Plan: Extended and updated several Python and browser tests.
Reviewers: paulfitz
Reviewed By: paulfitz
Differential Revision: https://phab.getgrist.com/D3611
2022-09-28 14:47:55 +00:00
|
|
|
if not (column.is_visible_column(col.col_id) or col.col_id == 'id'):
|
2022-08-16 19:18:19 +00:00
|
|
|
continue
|
|
|
|
# Note c=col to bind at lambda-creation time; see
|
|
|
|
# https://stackoverflow.com/questions/10452770/python-lambdas-binding-to-local-values
|
|
|
|
props[col.col_id] = property(lambda _self, c=col: c.sample_value())
|
|
|
|
if col.col_id == 'id':
|
|
|
|
# The column lookup below doesn't work for the id column
|
|
|
|
continue
|
|
|
|
# For columns with a visible column (i.e. most Reference/ReferenceList columns),
|
|
|
|
# we also want to show how to get that visible column instead of the 'raw' record
|
|
|
|
# returned by the reference column itself.
|
|
|
|
col_rec = self._engine.docmodel.get_column_rec(self.table_id, col.col_id)
|
|
|
|
visible_col_id = col_rec.visibleCol.colId
|
|
|
|
if visible_col_id:
|
|
|
|
# This creates a fake attribute like `RefCol.VisibleCol` which isn't valid syntax normally,
|
|
|
|
# to show the `.VisibleCol` part before the user has typed the `.`
|
|
|
|
props[col.col_id + "." + visible_col_id] = property(
|
|
|
|
lambda _self, c=col, v=visible_col_id: getattr(c.sample_value(), v)
|
|
|
|
)
|
|
|
|
|
|
|
|
RecType = type(self.table_id, (), props)
|
|
|
|
return RecType()
|
2022-03-22 21:19:31 +00:00
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
def _rebuild_model(self, user_table):
|
|
|
|
"""
|
|
|
|
Sets class-wide properties from a new Model class for the table (inner class within the table
|
|
|
|
class), and rebuilds self.all_columns from the new Model, reusing columns with existing names.
|
|
|
|
"""
|
|
|
|
self.user_table = user_table
|
|
|
|
self.Model = user_table.Model
|
|
|
|
|
|
|
|
new_cols = collections.OrderedDict()
|
|
|
|
new_cols['id'] = self._id_column
|
|
|
|
|
|
|
|
# List of Columns in the same order as they appear in the generated Model definition.
|
2021-06-22 15:12:25 +00:00
|
|
|
col_items = [c for c in six.iteritems(self.Model.__dict__) if not c[0].startswith("_")]
|
2020-07-27 18:57:36 +00:00
|
|
|
col_items.sort(key=lambda c: self._get_sort_order(c[1]))
|
|
|
|
|
|
|
|
for col_id, col_model in col_items:
|
|
|
|
default_func = self.Model.__dict__.get(get_default_func_name(col_id))
|
|
|
|
new_cols[col_id] = self._create_or_update_col(col_id, col_model, default_func)
|
|
|
|
|
|
|
|
# Note that we reuse previous special columns like lookup maps, since those not affected by
|
|
|
|
# column changes should stay the same. These get removed when unneeded using other means.
|
2021-06-22 15:12:25 +00:00
|
|
|
new_cols.update(sorted(six.iteritems(self._special_cols)))
|
2020-07-27 18:57:36 +00:00
|
|
|
|
2023-02-04 16:20:13 +00:00
|
|
|
self._update_record_classes(self.all_columns, new_cols)
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
# Set the new columns.
|
|
|
|
self.all_columns = new_cols
|
|
|
|
|
|
|
|
# Make sure any new columns get resized to the full table size.
|
|
|
|
self.grow_to_max()
|
|
|
|
|
|
|
|
# If this is a summary table, auto-create a necessary helper formula in the source table.
|
|
|
|
summary_src = getattr(self.Model, '_summarySourceTable', None)
|
|
|
|
if summary_src not in self._engine.tables:
|
|
|
|
self._summary_source_table = None
|
|
|
|
self._summary_helper_col_id = None
|
2021-07-19 13:04:05 +00:00
|
|
|
self._summary_simple = None
|
2020-07-27 18:57:36 +00:00
|
|
|
else:
|
|
|
|
self._summary_source_table = self._engine.tables[summary_src]
|
|
|
|
self._summary_helper_col_id = "#summary#%s" % self.table_id
|
|
|
|
# Figure out the group-by columns: these are all the non-formula columns.
|
|
|
|
groupby_cols = tuple(sorted(col_id for (col_id, col_model) in col_items
|
|
|
|
if not isinstance(col_model, types.FunctionType)))
|
2021-07-19 13:04:05 +00:00
|
|
|
self._summary_simple = not any(
|
|
|
|
isinstance(
|
|
|
|
self._summary_source_table.all_columns.get(group_col),
|
2021-07-23 15:29:35 +00:00
|
|
|
(column.ChoiceListColumn, column.ReferenceListColumn)
|
2021-07-19 13:04:05 +00:00
|
|
|
)
|
|
|
|
for group_col in groupby_cols
|
|
|
|
)
|
2020-07-27 18:57:36 +00:00
|
|
|
# Add the special helper column to the source table.
|
|
|
|
self._summary_source_table._add_update_summary_col(self, groupby_cols)
|
|
|
|
|
|
|
|
def _add_update_summary_col(self, summary_table, groupby_cols):
|
|
|
|
# TODO: things need to be removed also from summary_cols when a summary table is deleted.
|
2021-07-19 13:04:05 +00:00
|
|
|
|
|
|
|
# Grouping by list columns is significantly more complex and this comes with a
|
|
|
|
# performance cost, so in the common case we use the simpler older implementation
|
|
|
|
# In particular _updateSummary returns (possibly creating) just one reference
|
|
|
|
# instead of a list, which getSummarySourceGroup looks up directly instead
|
|
|
|
# of using CONTAINS, which in turn allows using SimpleLookupMapColumn
|
|
|
|
# instead of the similarly slower and more complicated ContainsLookupMapColumn
|
|
|
|
# All of these branches should be interchangeable and produce equivalent results
|
|
|
|
# when no list columns or CONTAINS are involved,
|
|
|
|
# especially since we need to be able to summarise by a combination of list and non-list
|
|
|
|
# columns or lookupRecords with a combination of CONTAINS and normal values,
|
|
|
|
# these are just performance optimisations
|
|
|
|
if summary_table._summary_simple:
|
|
|
|
@usertypes.formulaType(usertypes.Reference(summary_table.table_id))
|
|
|
|
def _updateSummary(rec, table): # pylint: disable=unused-argument
|
2022-05-23 17:36:21 +00:00
|
|
|
# summary table output should be treated as we treat formula columns, for acl purposes
|
|
|
|
with self._engine.user_actions.indirect_actions():
|
2021-09-15 20:18:00 +00:00
|
|
|
return summary_table.lookupOrAddDerived(**{c: getattr(rec, c) for c in groupby_cols})
|
2022-05-23 17:36:21 +00:00
|
|
|
|
2021-07-19 13:04:05 +00:00
|
|
|
else:
|
|
|
|
@usertypes.formulaType(usertypes.ReferenceList(summary_table.table_id))
|
|
|
|
def _updateSummary(rec, table): # pylint: disable=unused-argument
|
|
|
|
# Create a row in the summary table for every combination of values in
|
2021-07-23 15:29:35 +00:00
|
|
|
# list type columns
|
2021-07-19 13:04:05 +00:00
|
|
|
lookup_values = []
|
|
|
|
for group_col in groupby_cols:
|
|
|
|
lookup_value = getattr(rec, group_col)
|
2022-06-07 14:57:29 +00:00
|
|
|
group_col_obj = self.all_columns[group_col]
|
|
|
|
if isinstance(group_col_obj, (column.ChoiceListColumn, column.ReferenceListColumn)):
|
2021-07-23 15:29:35 +00:00
|
|
|
# Check that ChoiceList/ReferenceList cells have appropriate types.
|
2021-07-19 13:04:05 +00:00
|
|
|
# Don't iterate over characters of a string.
|
|
|
|
if isinstance(lookup_value, (six.binary_type, six.text_type)):
|
|
|
|
return []
|
|
|
|
try:
|
|
|
|
# We only care about the unique choices
|
|
|
|
lookup_value = set(lookup_value)
|
|
|
|
except TypeError:
|
|
|
|
return []
|
2022-06-07 14:57:29 +00:00
|
|
|
|
|
|
|
if not lookup_value:
|
|
|
|
if isinstance(group_col_obj, column.ChoiceListColumn):
|
|
|
|
lookup_value = {""}
|
|
|
|
else:
|
|
|
|
lookup_value = {0}
|
|
|
|
|
2021-07-19 13:04:05 +00:00
|
|
|
else:
|
|
|
|
lookup_value = [lookup_value]
|
|
|
|
lookup_values.append(lookup_value)
|
|
|
|
|
|
|
|
result = []
|
|
|
|
values_to_add = {}
|
|
|
|
new_row_ids = []
|
|
|
|
|
|
|
|
for values_tuple in sorted(itertools.product(*lookup_values)):
|
|
|
|
values_dict = dict(zip(groupby_cols, values_tuple))
|
|
|
|
row_id = summary_table.lookup_one_record(**values_dict)._row_id
|
|
|
|
if row_id:
|
|
|
|
result.append(row_id)
|
|
|
|
else:
|
|
|
|
for col, value in six.iteritems(values_dict):
|
|
|
|
values_to_add.setdefault(col, []).append(value)
|
|
|
|
new_row_ids.append(None)
|
|
|
|
|
|
|
|
if new_row_ids and not self._engine.is_triggered_by_table_action(summary_table.table_id):
|
2022-05-23 17:36:21 +00:00
|
|
|
# summary table output should be treated as we treat formula columns, for acl purposes
|
|
|
|
with self._engine.user_actions.indirect_actions():
|
2021-09-15 20:18:00 +00:00
|
|
|
result += self._engine.user_actions.BulkAddRecord(
|
|
|
|
summary_table.table_id, new_row_ids, values_to_add
|
|
|
|
)
|
2021-07-19 13:04:05 +00:00
|
|
|
|
|
|
|
return result
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
_updateSummary.is_private = True
|
|
|
|
col_id = summary_table._summary_helper_col_id
|
2021-08-11 17:45:24 +00:00
|
|
|
if self.has_column(col_id):
|
2021-08-29 14:17:33 +00:00
|
|
|
# If type changed between Reference/ReferenceList, replace completely.
|
2022-01-06 23:58:54 +00:00
|
|
|
# pylint: disable=unidiomatic-typecheck
|
2021-08-29 14:17:33 +00:00
|
|
|
if type(self.get_column(col_id).type_obj) != type(_updateSummary.grist_type):
|
|
|
|
self.delete_column(self.get_column(col_id))
|
2020-07-27 18:57:36 +00:00
|
|
|
col_obj = self._create_or_update_col(col_id, _updateSummary)
|
2023-02-04 16:20:13 +00:00
|
|
|
self._add_special_col(col_obj)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def get_helper_columns(self):
|
|
|
|
"""
|
|
|
|
Returns a list of columns from other tables that are only needed for the sake of this table.
|
|
|
|
"""
|
|
|
|
if self._summary_source_table and self._summary_helper_col_id:
|
|
|
|
helper_col = self._summary_source_table.get_column(self._summary_helper_col_id)
|
|
|
|
return [helper_col]
|
|
|
|
return []
|
|
|
|
|
|
|
|
def _create_or_update_col(self, col_id, col_model, default_func=None):
|
|
|
|
"""
|
|
|
|
Helper to update an existing column with a new model, or create a new column object.
|
|
|
|
"""
|
|
|
|
col_info = column.get_col_info(col_model, default_func)
|
|
|
|
col_obj = self.all_columns.get(col_id)
|
|
|
|
if col_obj:
|
|
|
|
# This is important for when a column has NOT changed, since although the formula method is
|
|
|
|
# unchanged, it's important to use the new instance of it from the newly built module.
|
|
|
|
col_obj.update_method(col_info.method)
|
|
|
|
else:
|
|
|
|
col_obj = column.create_column(self, col_id, col_info)
|
|
|
|
self._engine.invalidate_column(col_obj)
|
|
|
|
return col_obj
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_sort_order(col_model):
|
|
|
|
"""
|
|
|
|
We sort columns according to the order in which they appear in the model definition. To
|
|
|
|
detect this order, we sort data columns by _creation_order, and formula columns by the
|
|
|
|
function's source-code line number.
|
|
|
|
"""
|
|
|
|
return ((0, col_model._creation_order)
|
|
|
|
if not isinstance(col_model, types.FunctionType) else
|
2021-06-22 15:12:25 +00:00
|
|
|
(1, col_model.__code__.co_firstlineno))
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def next_row_id(self):
|
|
|
|
"""
|
|
|
|
Returns the ID of the next row that can be added to this table.
|
|
|
|
"""
|
|
|
|
return self.row_ids.max() + 1
|
|
|
|
|
|
|
|
def grow_to_max(self):
|
|
|
|
"""
|
|
|
|
Resizes all columns as needed so that all valid row_ids are valid indices into all columns.
|
|
|
|
"""
|
|
|
|
size = self.row_ids.max() + 1
|
2021-06-22 15:12:25 +00:00
|
|
|
for col_obj in six.itervalues(self.all_columns):
|
2020-07-27 18:57:36 +00:00
|
|
|
col_obj.growto(size)
|
|
|
|
|
|
|
|
def get_column(self, col_id):
|
|
|
|
"""
|
|
|
|
Returns the column with the given column ID.
|
|
|
|
"""
|
|
|
|
return self.all_columns[col_id]
|
|
|
|
|
|
|
|
def has_column(self, col_id):
|
|
|
|
"""
|
|
|
|
Returns whether col_id represents a valid column in the table.
|
|
|
|
"""
|
|
|
|
return col_id in self.all_columns
|
|
|
|
|
|
|
|
def lookup_records(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Returns a Record matching the given column=value arguments. It creates the necessary
|
|
|
|
dependencies, so that the formula will get re-evaluated if needed. It also creates and starts
|
|
|
|
maintaining a lookup index to make such lookups fast.
|
|
|
|
"""
|
|
|
|
# The tuple of keys used determines the LookupMap we need.
|
|
|
|
sort_by = kwargs.pop('sort_by', None)
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
order_by = kwargs.pop('order_by', 'id') # For backward compatibility
|
2021-07-19 13:04:05 +00:00
|
|
|
key = []
|
|
|
|
col_ids = []
|
|
|
|
for col_id in sorted(kwargs):
|
|
|
|
value = kwargs[col_id]
|
2021-07-20 20:57:35 +00:00
|
|
|
if isinstance(value, lookup._Contains):
|
2021-07-19 13:04:05 +00:00
|
|
|
# While users should use CONTAINS on lookup values,
|
|
|
|
# the marker is moved to col_id so that the LookupMapColumn knows how to
|
|
|
|
# update its index correctly for that column.
|
2022-06-07 14:57:29 +00:00
|
|
|
col_id = value._replace(value=col_id)
|
|
|
|
value = value.value
|
2022-02-11 13:10:53 +00:00
|
|
|
else:
|
|
|
|
col = self.get_column(col_id)
|
|
|
|
# Convert `value` to the correct type of rich value for that column
|
|
|
|
value = col._convert_raw_value(col.convert(value))
|
2021-07-19 13:04:05 +00:00
|
|
|
key.append(value)
|
|
|
|
col_ids.append(col_id)
|
|
|
|
col_ids = tuple(col_ids)
|
|
|
|
key = tuple(key)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
lookup_map = self._get_lookup_map(col_ids)
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
sort_spec = make_sort_spec(order_by, sort_by, self.has_column('manualSort'))
|
|
|
|
if sort_spec:
|
|
|
|
sorted_lookup_map = self._get_sorted_lookup_map(lookup_map, sort_spec)
|
2020-07-27 18:57:36 +00:00
|
|
|
else:
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
sorted_lookup_map = lookup_map
|
|
|
|
|
|
|
|
row_ids, rel = sorted_lookup_map.do_lookup(key)
|
|
|
|
return self.RecordSet(row_ids, rel, group_by=kwargs, sort_by=sort_by,
|
|
|
|
sort_key=sorted_lookup_map.sort_key)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def lookup_one_record(self, **kwargs):
|
|
|
|
return self.lookup_records(**kwargs).get_one()
|
|
|
|
|
|
|
|
def _get_lookup_map(self, col_ids_tuple):
|
|
|
|
"""
|
|
|
|
Helper which returns the LookupMapColumn for the given combination of lookup columns. A
|
|
|
|
LookupMap behaves a bit like a formula column in that it depends on the passed-in columns and
|
|
|
|
gets updated whenever any of them change.
|
|
|
|
"""
|
|
|
|
# LookupMapColumn is a Node, so identified by (table_id, col_id) pair, so we make up a col_id
|
|
|
|
# to identify this lookup object uniquely in this Table.
|
2021-07-19 13:04:05 +00:00
|
|
|
lookup_col_id = "#lookup#" + ":".join(map(str, col_ids_tuple))
|
2020-07-27 18:57:36 +00:00
|
|
|
lmap = self._special_cols.get(lookup_col_id)
|
|
|
|
if not lmap:
|
|
|
|
# Check that the table actually has all the columns we looking up.
|
|
|
|
for c in col_ids_tuple:
|
2021-07-19 13:04:05 +00:00
|
|
|
c = lookup.extract_column_id(c)
|
2020-07-27 18:57:36 +00:00
|
|
|
if not self.has_column(c):
|
|
|
|
raise KeyError("Table %s has no column %s" % (self.table_id, c))
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
lmap = lookup.LookupMapColumn(self, lookup_col_id, col_ids_tuple)
|
2023-02-04 16:20:13 +00:00
|
|
|
self._add_special_col(lmap)
|
2020-07-27 18:57:36 +00:00
|
|
|
return lmap
|
|
|
|
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
def _get_sorted_lookup_map(self, lookup_map, sort_spec):
|
|
|
|
helper_col_id = lookup_map.col_id + "#" + ":".join(sort_spec)
|
|
|
|
# Find or create a helper col for the given sort_spec.
|
|
|
|
helper_col = self._special_cols.get(helper_col_id)
|
|
|
|
if not helper_col:
|
|
|
|
helper_col = lookup.SortedLookupMapColumn(self, helper_col_id, lookup_map, sort_spec)
|
|
|
|
self._add_special_col(helper_col)
|
|
|
|
return helper_col
|
|
|
|
|
2020-07-27 18:57:36 +00:00
|
|
|
def delete_column(self, col_obj):
|
|
|
|
assert col_obj.table_id == self.table_id
|
|
|
|
self._special_cols.pop(col_obj.col_id, None)
|
|
|
|
self.all_columns.pop(col_obj.col_id, None)
|
2023-02-04 16:20:13 +00:00
|
|
|
self._remove_field_from_record_classes(col_obj.col_id)
|
|
|
|
|
|
|
|
def _add_special_col(self, col_obj):
|
|
|
|
assert col_obj.table_id == self.table_id
|
|
|
|
self._special_cols[col_obj.col_id] = col_obj
|
|
|
|
self.all_columns[col_obj.col_id] = col_obj
|
|
|
|
self._add_field_to_record_classes(col_obj)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def lookupOrAddDerived(self, **kwargs):
|
|
|
|
record = self.lookup_one_record(**kwargs)
|
|
|
|
if not record._row_id and not self._engine.is_triggered_by_table_action(self.table_id):
|
|
|
|
record._row_id = self._engine.user_actions.AddRecord(self.table_id, None, kwargs)
|
|
|
|
return record
|
|
|
|
|
|
|
|
def getSummarySourceGroup(self, rec):
|
2021-07-19 13:04:05 +00:00
|
|
|
if self._summary_source_table:
|
|
|
|
# See comment in _add_update_summary_col.
|
|
|
|
# _summary_source_table._summary_simple determines whether
|
|
|
|
# the column named self._summary_helper_col_id is a single reference
|
|
|
|
# or a reference list.
|
2022-06-07 14:57:29 +00:00
|
|
|
lookup_value = rec if self._summary_simple else functions.CONTAINS(rec)
|
2022-07-07 20:43:12 +00:00
|
|
|
result = self._summary_source_table.lookup_records(**{
|
2021-07-19 13:04:05 +00:00
|
|
|
self._summary_helper_col_id: lookup_value
|
|
|
|
})
|
2022-07-07 20:43:12 +00:00
|
|
|
|
|
|
|
# Remove rows with empty groups
|
|
|
|
self._engine.docmodel.setAutoRemove(rec, not result)
|
|
|
|
return result
|
2021-07-19 13:04:05 +00:00
|
|
|
else:
|
|
|
|
return None
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def get(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Returns the first row_id matching the given column=value arguments. This is intended for grist
|
|
|
|
internal code rather than for user formulas, because it doesn't create the necessary
|
|
|
|
dependencies.
|
|
|
|
"""
|
|
|
|
# TODO: It should use indices, to avoid linear searching
|
|
|
|
# TODO: It should create dependencies as needed when used from formulas.
|
|
|
|
# TODO: It should return Record instead, for convenience of user formulas
|
2021-06-22 15:12:25 +00:00
|
|
|
col_values = [(self.all_columns[col_id], value) for (col_id, value) in six.iteritems(kwargs)]
|
2020-07-27 18:57:36 +00:00
|
|
|
for row_id in self.row_ids:
|
|
|
|
if all(col.raw_get(row_id) == value for col, value in col_values):
|
|
|
|
return row_id
|
|
|
|
raise KeyError("'get' found no matching record")
|
|
|
|
|
|
|
|
def filter(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Generates all row_ids matching the given column=value arguments. This is intended for grist
|
|
|
|
internal code rather than for user formulas, because it doesn't create the necessary
|
|
|
|
dependencies. Use filter_records() to generate Record objects instead.
|
|
|
|
"""
|
|
|
|
# TODO: It should use indices, to avoid linear searching
|
|
|
|
# TODO: It should create dependencies as needed when used from formulas.
|
|
|
|
# TODO: It should return Record instead, for convenience of user formulas
|
2021-06-22 15:12:25 +00:00
|
|
|
col_values = [(self.all_columns[col_id], value) for (col_id, value) in six.iteritems(kwargs)]
|
2020-07-27 18:57:36 +00:00
|
|
|
for row_id in self.row_ids:
|
|
|
|
if all(col.raw_get(row_id) == value for col, value in col_values):
|
|
|
|
yield row_id
|
|
|
|
|
|
|
|
def get_record(self, row_id):
|
|
|
|
"""
|
|
|
|
Returns a Record object corresponding to the given row_id. This is intended for grist internal
|
|
|
|
code rather than user formulas.
|
|
|
|
"""
|
|
|
|
# We don't set up any dependencies, so it would be incorrect to use this from formulas.
|
|
|
|
# We no longer assert, however, since such calls may still happen e.g. while applying
|
2022-02-19 09:46:49 +00:00
|
|
|
# user-actions caused by formula side-effects (e.g. as triggered by lookupOrAddDerived())
|
2020-07-27 18:57:36 +00:00
|
|
|
if row_id not in self.row_ids:
|
|
|
|
raise KeyError("'get_record' found no matching record")
|
2021-07-16 18:15:04 +00:00
|
|
|
return self.Record(row_id, None)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def filter_records(self, **kwargs):
|
|
|
|
"""
|
|
|
|
Generator for Record objects for all the rows matching the given column=value arguments.
|
|
|
|
This is intended for grist internal code rather than user formula. You may call this with no
|
|
|
|
arguments to generate all Records in the table.
|
|
|
|
"""
|
|
|
|
# See note in get_record() about using this call from formulas.
|
|
|
|
|
|
|
|
for row_id in self.filter(**kwargs):
|
2021-07-16 18:15:04 +00:00
|
|
|
yield self.Record(row_id, None)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
# TODO: document everything here.
|
|
|
|
|
2023-02-04 16:20:13 +00:00
|
|
|
# Equivalent to accessing record.foo, but only used in very limited cases now (field accessor is
|
|
|
|
# more optimized).
|
|
|
|
def _get_col_obj_value(self, col_obj, row_id, relation):
|
|
|
|
# creates a dependency and brings formula columns up-to-date.
|
|
|
|
self._engine._use_node(col_obj.node, relation, (row_id,))
|
|
|
|
value = col_obj.get_cell_value(row_id)
|
|
|
|
return adjust_record(relation, value)
|
2020-07-27 18:57:36 +00:00
|
|
|
|
|
|
|
def _attribute_error(self, col_id, relation):
|
|
|
|
self._engine._use_node(self._new_columns_node, relation)
|
|
|
|
raise AttributeError("Table '%s' has no column '%s'" % (self.table_id, col_id))
|
|
|
|
|
2021-08-12 00:36:59 +00:00
|
|
|
# Called when record_set.foo is accessed
|
2023-02-04 16:20:13 +00:00
|
|
|
def _get_col_obj_subset(self, col_obj, row_ids, relation):
|
|
|
|
self._engine._use_node(col_obj.node, relation, row_ids)
|
|
|
|
values = [col_obj.get_cell_value(row_id) for row_id in row_ids]
|
2022-04-04 19:09:48 +00:00
|
|
|
|
|
|
|
# When all the values are the same type of Record (i.e. all references to the same table)
|
|
|
|
# combine them into a single RecordSet for that table instead of a list
|
|
|
|
# so that more attribute accesses can be chained,
|
|
|
|
# e.g. record_set.foo.bar where `foo` is a Reference column.
|
|
|
|
value_types = list(set(map(type, values)))
|
2023-02-04 16:20:13 +00:00
|
|
|
if len(value_types) == 1 and issubclass(value_types[0], BaseRecord):
|
|
|
|
return values[0]._table.RecordSet(
|
2022-04-04 19:09:48 +00:00
|
|
|
# This is different from row_ids: these are the row IDs referenced by these Records,
|
|
|
|
# whereas row_ids are where the values were being stored.
|
|
|
|
[val._row_id for val in values],
|
|
|
|
relation.compose(values[0]._source_relation),
|
|
|
|
)
|
|
|
|
else:
|
2023-02-04 16:20:13 +00:00
|
|
|
return [adjust_record(relation, value) for value in values]
|
|
|
|
|
|
|
|
#----------------------------------------
|
|
|
|
|
|
|
|
def _update_record_classes(self, old_columns, new_columns):
|
|
|
|
for col_id in old_columns:
|
|
|
|
if col_id not in new_columns:
|
|
|
|
self._remove_field_from_record_classes(col_id)
|
|
|
|
|
|
|
|
for col_id, col_obj in six.iteritems(new_columns):
|
|
|
|
if col_obj != old_columns.get(col_id):
|
|
|
|
self._add_field_to_record_classes(col_obj)
|
|
|
|
|
|
|
|
def _add_field_to_record_classes(self, col_obj):
|
|
|
|
node = col_obj.node
|
|
|
|
use_node = self._engine._use_node
|
|
|
|
|
|
|
|
@property
|
|
|
|
def record_field(rec):
|
|
|
|
# This is equivalent to _get_col_obj_value(), but is extra-optimized with _get_col_obj_value()
|
|
|
|
# and adjust_record() inlined, since this is particularly hot code, called on every access of
|
|
|
|
# any data field in a formula.
|
|
|
|
use_node(node, rec._source_relation, (rec._row_id,))
|
|
|
|
value = col_obj.get_cell_value(rec._row_id)
|
|
|
|
if isinstance(value, (BaseRecord, BaseRecordSet)):
|
|
|
|
return value._clone_with_relation(rec._source_relation)
|
|
|
|
return value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def recordset_field(recset):
|
|
|
|
return self._get_col_obj_subset(col_obj, recset._row_ids, recset._source_relation)
|
|
|
|
|
|
|
|
setattr(self.Record, col_obj.col_id, record_field)
|
|
|
|
setattr(self.RecordSet, col_obj.col_id, recordset_field)
|
|
|
|
|
|
|
|
def _remove_field_from_record_classes(self, col_id):
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
# Check if col_id is in the immediate dictionary of self.Record[Set]; if missing, or inherited
|
|
|
|
# from the base class (e.g. "find"), there is nothing to delete.
|
|
|
|
if col_id in self.Record.__dict__:
|
2023-02-04 16:20:13 +00:00
|
|
|
delattr(self.Record, col_id)
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
if col_id in self.RecordSet.__dict__:
|
2023-02-04 16:20:13 +00:00
|
|
|
delattr(self.RecordSet, col_id)
|
(core) Implement PREVIOUS/NEXT/RANK and lookupRecords().find.* methods.
Summary:
- `lookupRecords()` now allows efficient search in sorted results, with
the syntax `lookupRecords(..., order_by="-Date").find.le($Date)`. This will find the record with the nearest date that's <= `$Date`.
- The `find.*` methods are `le`, `lt`, `ge`, `gt`, and `eq`. All have O(log N) performance.
- `PREVIOUS(rec, group_by=..., order_by=...)` finds the previous record to rec, according to `group_by` / `order_by`, in amortized O(log N) time. For example, `PREVIOUS(rec, group_by="Account", order_by="Date")`.
- `PREVIOUS(rec, order_by=None)` finds the previous record in the full table, sorted by the `manualSort` column, to match the order visible in the unsorted table.
- `NEXT(...)` is just like `PREVIOUS(...)` but finds the next record.
- `RANK(rec, group_by=..., order_by=..., order="asc")` returns the rank of the record within the group, starting with 1. Order can be `"asc"` (default) or `"desc"`.
- The `order_by` argument in `lookupRecords`, and the new functions now supports tuples, as well as the "-" prefix to reverse order, e.g. `("Category", "-Date")`.
- New functions are only available in Python3, for a minor reason (to support keyword-only arguments for `group_by` and `order_by`) and also as a nudge to Python2 users to update.
- Includes fixes for several situations related to lookups that used to cause quadratic complexity.
Test Plan:
- New performance check that sorted lookups don't add quadratic complexity.
- Tests added for lookup find.* methods, and for PREVIOUS/NEXT/RANK.
- Tests added that renaming columns updates `order_by` and `group_by` arguments, and attributes on results (e.g. `PREVIOUS(...).ColId`) appropriately.
- Python3 tests can now produce verbose output when VERBOSE=1 and -v are given.
Reviewers: jarek, georgegevoian
Reviewed By: jarek, georgegevoian
Subscribers: paulfitz, jarek
Differential Revision: https://phab.getgrist.com/D4265
2024-07-17 03:43:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
def make_sort_spec(order_by, sort_by, has_manual_sort):
|
|
|
|
# Note that rowId is always an automatic fallback.
|
|
|
|
if sort_by:
|
|
|
|
if not isinstance(sort_by, six.string_types):
|
|
|
|
# pylint: disable=line-too-long
|
|
|
|
raise TypeError("sort_by must be a string column ID, with optional '-'; use order_by for tuples")
|
|
|
|
# No fallback to 'manualSort' here, for backward compatibility.
|
|
|
|
return (sort_by,)
|
|
|
|
|
|
|
|
if not isinstance(order_by, tuple):
|
|
|
|
# Suppot None and single-string specs (for a single column)
|
|
|
|
if isinstance(order_by, six.string_types):
|
|
|
|
order_by = (order_by,)
|
|
|
|
elif order_by is None:
|
|
|
|
order_by = ()
|
|
|
|
else:
|
|
|
|
raise TypeError("order_by must be a string column ID, with optional '-', or a tuple of them")
|
|
|
|
|
|
|
|
# Check if 'id' is mentioned explicitly. If so, then no fallback to 'manualSort', or anything
|
|
|
|
# else, since row IDs are unique. Also, drop the 'id' column itself because the row ID fallback
|
|
|
|
# is mandatory and automatic.
|
|
|
|
if 'id' in order_by:
|
|
|
|
return order_by[:order_by.index('id')]
|
|
|
|
|
|
|
|
# Fall back to manualSort, but only if it exists in the table and not yet mentioned in order_by.
|
|
|
|
if has_manual_sort and 'manualSort' not in order_by:
|
|
|
|
return order_by + ('manualSort',)
|
|
|
|
|
|
|
|
return order_by
|