Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ const Model = class Model {
}

/**
* Manually mark individual instances as accessed.
* This allows invalidating selector memoization within mutable sessions.
*
* @param {Array.<*>} ids - Array of primary key values
* @return {undefined}
*/
static markAccessed(ids) {
Expand All @@ -127,6 +131,9 @@ const Model = class Model {
}

/**
* Manually mark this model's table as scanned.
* This allows invalidating selector memoization within mutable sessions.
*
* @return {undefined}
*/
static markFullTableScanned() {
Expand All @@ -140,6 +147,28 @@ const Model = class Model {
this.session.markFullTableScanned(this.modelName);
}

/**
* Manually mark indexes as accessed.
* This allows invalidating selector memoization within mutable sessions.
*
* @param {Array.<Array.<*,*>>} indexes - Array of column-value pairs
* @return {undefined}
*/
static markAccessedIndexes(indexes) {
if (typeof this._session === 'undefined') {
throw new Error([
`Tried to mark indexes for the ${this.modelName} model as accessed without a session. `,
'Create a session using `session = orm.session()` and call ',
`\`session["${this.modelName}"].markAccessedIndexes\` instead.`,
].join(''));
}
this.session.markAccessedIndexes(
indexes.map(
([attribute, value]) => [this.modelName, attribute, value]
)
);
}

/**
* Returns the id attribute of this {@link Model}.
*
Expand Down
10 changes: 10 additions & 0 deletions src/ORM.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ const ORM_DEFAULTS = {
createDatabase: defaultCreateDatabase,
};

const RESERVED_TABLE_OPTIONS = [
'indexes',
'meta',
];
const isReservedTableOption = word => RESERVED_TABLE_OPTIONS.includes(word);

/**
* ORM - the Object Relational Mapper.
*
Expand Down Expand Up @@ -161,6 +167,10 @@ export class ORM {
const tables = models.reduce((spec, modelClass) => {
const tableName = modelClass.modelName;
const tableSpec = modelClass._getTableOpts(); // eslint-disable-line no-underscore-dangle
Object.keys(tableSpec).forEach((key) => {
if (!isReservedTableOption(key)) return;
throw new Error(`Reserved keyword \`${key}\` used in ${tableName}.options.`);
});
spec[tableName] = Object.assign({}, { fields: modelClass.fields }, tableSpec);
return spec;
}, {});
Expand Down
47 changes: 40 additions & 7 deletions src/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Session = class Session {
this.state = state || db.getEmptyState();
this.initialState = this.state;

this.withMutations = !!withMutations;
this.withMutations = Boolean(withMutations);
this.batchToken = batchToken || getBatchToken();

this.modelData = {};
Expand Down Expand Up @@ -61,14 +61,12 @@ const Session = class Session {

get accessedModelInstances() {
return this.sessionBoundModels
.filter(({ modelName }) => !!this.getDataForModel(modelName).accessedInstances)
.filter(({ modelName }) => this.getDataForModel(modelName).accessedInstances)
.reduce(
(result, { modelName }) => ({
...result,
[modelName]: this.getDataForModel(modelName).accessedInstances,
}),
{}
);
}), {});
}

markFullTableScanned(modelName) {
Expand All @@ -78,10 +76,32 @@ const Session = class Session {

get fullTableScannedModels() {
return this.sessionBoundModels
.filter(({ modelName }) => !!this.getDataForModel(modelName).fullTableScanned)
.filter(({ modelName }) => this.getDataForModel(modelName).fullTableScanned)
.map(({ modelName }) => modelName);
}

markAccessedIndexes(indexes) {
indexes.forEach(([table, attr, value]) => {
const data = this.getDataForModel(table);
if (!data.accessedIndexes) {
data.accessedIndexes = {};
}
data.accessedIndexes[attr] = [
...(data.accessedIndexes[attr] || []),
value,
];
});
}

get accessedIndexes() {
return this.sessionBoundModels
.filter(({ modelName }) => this.getDataForModel(modelName).accessedIndexes)
.reduce((result, { modelName }) => ({
...result,
[modelName]: this.getDataForModel(modelName).accessedIndexes,
}), {});
}

/**
* Applies update to a model state.
*
Expand Down Expand Up @@ -129,27 +149,40 @@ const Session = class Session {
const accessedIds = new Set(rows.map(
row => row[idAttribute]
));
const accessedIndexes = [];

const anyClauseFilteredById = clauses.some((clause) => {
if (!clauseFiltersByAttribute(clause, idAttribute)) {
return false;
}
const id = clause.payload[idAttribute];
if (id === null) return false;
/**
* we previously knew which row we wanted to access,
* so there was no need to scan the entire table
*/
const id = clause.payload[idAttribute];
accessedIds.add(id);
return true;
});

const { indexes } = this.state[table];
clauses.forEach((clause) => {
Object.keys(indexes).forEach((attr) => {
if (!clauseFiltersByAttribute(clause, attr)) return;
const value = clause.payload[attr];
accessedIndexes.push([table, attr, value]);
});
});

if (anyClauseFilteredById) {
/**
* clauses have been ordered so that an indexed one was
* the first to be evaluated, and thus only the row
* with the specified id has actually been accessed
*/
this.markAccessed(table, accessedIds);
} else if (accessedIndexes.length) {
this.markAccessedIndexes(accessedIndexes);
} else {
/**
* any other clause would have caused a full table scan,
Expand Down
6 changes: 0 additions & 6 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,3 @@ export const ORDER_BY = 'REDUX_ORM_ORDER_BY';

export const SUCCESS = 'SUCCESS';
export const FAILURE = 'FAILURE';

export const DEFAULT_TABLE_OPTIONS = {
idAttribute: 'id',
arrName: 'items',
mapName: 'itemsById',
};
23 changes: 14 additions & 9 deletions src/db/Database.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,20 @@ function update(tables, updateSpec, tx, state) {

export function createDatabase(schemaSpec) {
const { tables: tableSpecs } = schemaSpec;
const tables = Object.entries(tableSpecs).reduce((map, [tableName, tableSpec]) => ({
...map,
[tableName]: new Table(tableSpec),
}), {});

const getEmptyState = () => Object.entries(tables).reduce((map, [tableName, table]) => ({
...map,
[tableName]: table.getEmptyState(),
}), {});
const tables = Object.entries(tableSpecs)
.reduce((map, [tableName, tableSpec]) => ({
...map,
[tableName]: new Table(tableSpec),
}), {});

const getEmptyState = () => (
Object.entries(tables)
.reduce((map, [tableName, table]) => ({
...map,
[tableName]: table.getEmptyState(),
}), {})
);

return {
getEmptyState,
query: query.bind(null, tables),
Expand Down
Loading