Skip to content

Commit

Permalink
feat: Add the partial filter fields in the index name
Browse files Browse the repository at this point in the history
When using a partial index, its fields weren't part of the index name.
Therefore, we might have a conflict situation where there is already an
existing index without the partial index.
Fix #1054
  • Loading branch information
paultranvan committed Jun 24, 2022
1 parent d289d36 commit 32c432d
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 7 deletions.
23 changes: 23 additions & 0 deletions docs/api/cozy-stack-client.md
Expand Up @@ -73,6 +73,11 @@ through OAuth.</p>
<dt><a href="#getIllegalCharacters">getIllegalCharacters</a> ⇒ <code>string</code></dt>
<dd><p>Get the list of illegal characters in the file name</p>
</dd>
<dt><a href="#getIndexNameFromFields">getIndexNameFromFields</a> ⇒ <code>string</code></dt>
<dd><p>Name an index, based on its indexed fields and partial filter.</p>
<p>It follows this naming convention:
`by_{indexed_field1}<em>and</em>{indexed_field2}<em>filter</em>{partial_filter_field1}<em>and</em>{partial_filter_field2}</p>
</dd>
<dt><a href="#getIndexFields">getIndexFields</a> ⇒ <code>Array</code></dt>
<dd><p>Compute fields that should be indexed for a mango
query to work</p>
Expand Down Expand Up @@ -1796,6 +1801,23 @@ Get the list of illegal characters in the file name
| --- | --- | --- |
| name | <code>string</code> | the file name |

<a name="getIndexNameFromFields"></a>

## getIndexNameFromFields ⇒ <code>string</code>
Name an index, based on its indexed fields and partial filter.

It follows this naming convention:
`by_{indexed_field1}_and_{indexed_field2}_filter_{partial_filter_field1}_and_{partial_filter_field2}

**Kind**: global constant
**Returns**: <code>string</code> - The index name, built from the fields

| Param | Type | Description |
| --- | --- | --- |
| fields | <code>Array.&lt;string&gt;</code> | The indexed fields |
| params | <code>object</code> | The additional params |
| params.partialFilterFields | <code>Array.&lt;string&gt;</code> | The partial filter fields |

<a name="getIndexFields"></a>

## getIndexFields ⇒ <code>Array</code>
Expand Down Expand Up @@ -2402,6 +2424,7 @@ Document representing a io.cozy.jobs
| --- | --- | --- |
| [sort] | <code>Array.&lt;object&gt;</code> | The sorting parameters |
| [fields] | <code>Array.&lt;string&gt;</code> | The fields to return |
| [partialFilterFields] | <code>Array.&lt;string&gt;</code> | The partial filter fields |
| [limit] | <code>number</code> \| <code>null</code> | For pagination, the number of results to return |
| [skip] | <code>number</code> \| <code>null</code> | For skip-based pagination, the number of referenced files to skip |
| [indexId] | <code>string</code> \| <code>null</code> | The _id of the CouchDB index to use for this request |
Expand Down
8 changes: 7 additions & 1 deletion packages/cozy-stack-client/src/DocumentCollection.js
Expand Up @@ -220,8 +220,14 @@ class DocumentCollection {
if (!indexedFields) {
indexedFields = getIndexFields({ sort: options.sort, selector })
}
const partialFilterFields = partialFilter
? getIndexFields({ partialFilter })
: null

const existingIndex = await this.findExistingIndex(selector, options)
const indexName = getIndexNameFromFields(indexedFields)
const indexName = getIndexNameFromFields(indexedFields, {
partialFilterFields
})
if (!existingIndex) {
await this.createIndex(indexedFields, {
partialFilter,
Expand Down
2 changes: 1 addition & 1 deletion packages/cozy-stack-client/src/DocumentCollection.spec.js
Expand Up @@ -759,7 +759,7 @@ describe('DocumentCollection', () => {
'POST',
'/data/io.cozy.todos/_design/123456/copy?rev=1-123',
null,
{ headers: { Destination: '_design/by_label_and_done' } }
{ headers: { Destination: '_design/by_label_and_done_filter_trashed' } }
)
expect(client.fetchJSON).toHaveBeenNthCalledWith(
4,
Expand Down
27 changes: 23 additions & 4 deletions packages/cozy-stack-client/src/mangoIndex.js
Expand Up @@ -12,6 +12,7 @@ import isEqual from 'lodash/isEqual'
*
* @property {Array<object>} [sort] The sorting parameters
* @property {Array<string>} [fields] The fields to return
* @property {Array<string>} [partialFilterFields] The partial filter fields
* @property {number|null} [limit] For pagination, the number of results to return
* @property {number|null} [skip] For skip-based pagination, the number of referenced files to skip
* @property {string|null} [indexId] The _id of the CouchDB index to use for this request
Expand All @@ -35,8 +36,25 @@ export const normalizeDesignDoc = designDoc => {
return { id, _id: id, ...designDoc.doc }
}

export const getIndexNameFromFields = fields => {
return `by_${fields.join('_and_')}`
/**
* Name an index, based on its indexed fields and partial filter.
*
* It follows this naming convention:
* `by_{indexed_field1}_and_{indexed_field2}_filter_{partial_filter_field1}_and_{partial_filter_field2}
*
* @param {Array<string>} fields - The indexed fields
* @param {object} params - The additional params
* @param {Array<string>} params.partialFilterFields - The partial filter fields
* @returns {string} The index name, built from the fields
*/
export const getIndexNameFromFields = (
fields,
{ partialFilterFields } = {}
) => {
const indexName = `by_${fields.join('_and_')}`
return partialFilterFields
? `${indexName}_filter_${partialFilterFields.join('_and_')}`
: indexName
}

export const transformSort = sort => {
Expand All @@ -61,11 +79,12 @@ export const transformSort = sort => {
*
* @returns {Array} - Fields to index
*/
export const getIndexFields = ({ selector, sort = [] }) => {
export const getIndexFields = ({ selector, partialFilter, sort = [] }) => {
return Array.from(
new Set([
...sort.map(sortOption => head(Object.keys(sortOption))),
...(selector ? Object.keys(selector) : [])
...(selector ? Object.keys(selector) : []),
...(partialFilter ? Object.keys(partialFilter) : [])
])
)
}
Expand Down
81 changes: 80 additions & 1 deletion packages/cozy-stack-client/src/mangoIndex.spec.js
@@ -1,4 +1,9 @@
import { isMatchingIndex, isInconsistentIndex } from './mangoIndex'
import {
isMatchingIndex,
isInconsistentIndex,
getIndexFields,
getIndexNameFromFields
} from './mangoIndex'

const buildDesignDoc = (fields, { partialFilter, id } = {}) => {
return {
Expand Down Expand Up @@ -103,3 +108,77 @@ describe('inconsistent index', () => {
expect(isInconsistentIndex(index2)).toBe(false)
})
})

describe('getIndexFields', () => {
it('should return nothing', () => {
expect(getIndexFields({})).toEqual([])
})

it('should return fields from selector', () => {
const selector = {
_id: {
$gt: null
},
name: 'toto'
}
expect(getIndexFields({ selector })).toEqual(['_id', 'name'])
})

it('should return fields from sort', () => {
const selector = {}
const sort = [{ _id: 'asc' }, { name: 'asc' }]
expect(getIndexFields({ selector, sort })).toEqual(['_id', 'name'])
})

it('should return fields from partial filter', () => {
const partialFilter = {
date: {
$exists: false
},
trashed: {
$ne: true
}
}
expect(getIndexFields({ partialFilter })).toEqual(['date', 'trashed'])
})

it('should return all fields', () => {
const selector = {
_id: {
$gt: null
},
name: 'toto'
}
const sort = [{ _id: 'asc' }, { name: 'asc' }]

const partialFilter = {
date: {
$exists: false
},
trashed: {
$ne: true
}
}
expect(getIndexFields({ selector, sort, partialFilter })).toEqual([
'_id',
'name',
'date',
'trashed'
])
})
})

describe('getIndexNameFromFields', () => {
it('should return index fields', () => {
const fields = ['_id', 'name']
expect(getIndexNameFromFields(fields)).toEqual('by__id_and_name')
})

it('should return index fields with partial filter', () => {
const fields = ['_id', 'name']
const partialFilterFields = ['date', 'trashed']
expect(getIndexNameFromFields(fields, { partialFilterFields })).toEqual(
'by__id_and_name_filter_date_and_trashed'
)
})
})

0 comments on commit 32c432d

Please sign in to comment.