-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: FTS for react native supabase demo (#447)
- Loading branch information
Showing
12 changed files
with
394 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { StatusBar } from 'expo-status-bar'; | ||
import { StyleSheet, View } from 'react-native'; | ||
import { SearchBarWidget } from '../library/widgets/SearchBarWidget'; | ||
|
||
export default function Modal() { | ||
return ( | ||
<View style={styles.container}> | ||
<SearchBarWidget /> | ||
<StatusBar style={'light'} /> | ||
</View> | ||
); | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
flexGrow: 1, | ||
alignItems: 'center', | ||
justifyContent: 'center' | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
demos/react-native-supabase-todolist/library/fts/fts_helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { system } from '../powersync/system'; | ||
|
||
/** | ||
* adding * to the end of the search term will match any word that starts with the search term | ||
* e.g. searching bl will match blue, black, etc. | ||
* consult FTS5 Full-text Query Syntax documentation for more options | ||
* @param searchTerm | ||
* @returns a modified search term with options. | ||
*/ | ||
function createSearchTermWithOptions(searchTerm: string): string { | ||
const searchTermWithOptions: string = `${searchTerm}*`; | ||
return searchTermWithOptions; | ||
} | ||
|
||
/** | ||
* Search the FTS table for the given searchTerm | ||
* @param searchTerm | ||
* @param tableName | ||
* @returns results from the FTS table | ||
*/ | ||
export async function searchTable(searchTerm: string, tableName: string): Promise<any[]> { | ||
const searchTermWithOptions = createSearchTermWithOptions(searchTerm); | ||
return await system.powersync.getAll(`SELECT * FROM fts_${tableName} WHERE fts_${tableName} MATCH ? ORDER BY rank`, [ | ||
searchTermWithOptions | ||
]); | ||
} | ||
|
||
//Used to display the search results in the autocomplete text field | ||
export interface SearchResult { | ||
id: string; | ||
listName: string; | ||
todoName: string | null; | ||
} |
69 changes: 69 additions & 0 deletions
69
demos/react-native-supabase-todolist/library/fts/fts_setup.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { AppSchema } from '../powersync/AppSchema'; | ||
import { ExtractType, generateJsonExtracts } from './helpers'; | ||
import { PowerSyncDatabase } from '@powersync/react-native'; | ||
|
||
/** | ||
* Create a Full Text Search table for the given table and columns | ||
* with an option to use a different tokenizer otherwise it defaults | ||
* to unicode61. It also creates the triggers that keep the FTS table | ||
* and the PowerSync table in sync. | ||
* @param tableName | ||
* @param columns | ||
* @param tokenizationMethod | ||
*/ | ||
async function createFtsTable( | ||
db: PowerSyncDatabase, | ||
tableName: string, | ||
columns: string[], | ||
tokenizationMethod = 'unicode61' | ||
): Promise<void> { | ||
const internalName = AppSchema.tables.find((table) => table.name === tableName)?.internalName; | ||
const stringColumns = columns.join(', '); | ||
|
||
return await db.writeTransaction(async (tx) => { | ||
// Add FTS table | ||
await tx.execute(` | ||
CREATE VIRTUAL TABLE IF NOT EXISTS fts_${tableName} | ||
USING fts5(id UNINDEXED, ${stringColumns}, tokenize='${tokenizationMethod}'); | ||
`); | ||
// Copy over records already in table | ||
await tx.execute(` | ||
INSERT OR REPLACE INTO fts_${tableName}(rowid, id, ${stringColumns}) | ||
SELECT rowid, id, ${generateJsonExtracts(ExtractType.columnOnly, 'data', columns)} FROM ${internalName}; | ||
`); | ||
// Add INSERT, UPDATE and DELETE and triggers to keep fts table in sync with table | ||
await tx.execute(` | ||
CREATE TRIGGER IF NOT EXISTS fts_insert_trigger_${tableName} AFTER INSERT ON ${internalName} | ||
BEGIN | ||
INSERT INTO fts_${tableName}(rowid, id, ${stringColumns}) | ||
VALUES ( | ||
NEW.rowid, | ||
NEW.id, | ||
${generateJsonExtracts(ExtractType.columnOnly, 'NEW.data', columns)} | ||
); | ||
END; | ||
`); | ||
await tx.execute(` | ||
CREATE TRIGGER IF NOT EXISTS fts_update_trigger_${tableName} AFTER UPDATE ON ${internalName} BEGIN | ||
UPDATE fts_${tableName} | ||
SET ${generateJsonExtracts(ExtractType.columnInOperation, 'NEW.data', columns)} | ||
WHERE rowid = NEW.rowid; | ||
END; | ||
`); | ||
await tx.execute(` | ||
CREATE TRIGGER IF NOT EXISTS fts_delete_trigger_${tableName} AFTER DELETE ON ${internalName} BEGIN | ||
DELETE FROM fts_${tableName} WHERE rowid = OLD.rowid; | ||
END; | ||
`); | ||
}); | ||
} | ||
|
||
/** | ||
* This is where you can add more methods to generate FTS tables in this demo | ||
* that correspond to the tables in your schema and populate them | ||
* with the data you would like to search on | ||
*/ | ||
export async function configureFts(db: PowerSyncDatabase): Promise<void> { | ||
await createFtsTable(db, 'lists', ['name'], 'porter unicode61'); | ||
await createFtsTable(db, 'todos', ['description', 'list_id']); | ||
} |
36 changes: 36 additions & 0 deletions
36
demos/react-native-supabase-todolist/library/fts/helpers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
type ExtractGenerator = (jsonColumnName: string, columnName: string) => string; | ||
|
||
export enum ExtractType { | ||
columnOnly, | ||
columnInOperation | ||
} | ||
|
||
type ExtractGeneratorMap = Map<ExtractType, ExtractGenerator>; | ||
|
||
function _createExtract(jsonColumnName: string, columnName: string): string { | ||
return `json_extract(${jsonColumnName}, '$.${columnName}')`; | ||
} | ||
|
||
const extractGeneratorsMap: ExtractGeneratorMap = new Map<ExtractType, ExtractGenerator>([ | ||
[ExtractType.columnOnly, (jsonColumnName: string, columnName: string) => _createExtract(jsonColumnName, columnName)], | ||
[ | ||
ExtractType.columnInOperation, | ||
(jsonColumnName: string, columnName: string) => { | ||
const extract = _createExtract(jsonColumnName, columnName); | ||
return `${columnName} = ${extract}`; | ||
} | ||
] | ||
]); | ||
|
||
export const generateJsonExtracts = (type: ExtractType, jsonColumnName: string, columns: string[]): string => { | ||
const generator = extractGeneratorsMap.get(type); | ||
if (generator == null) { | ||
throw new Error('Unexpected null generator for key: $type'); | ||
} | ||
|
||
if (columns.length == 1) { | ||
return generator(jsonColumnName, columns[0]); | ||
} | ||
|
||
return columns.map((column) => generator(jsonColumnName, column)).join(', '); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
demos/react-native-supabase-todolist/library/widgets/AutoCompleteWidget.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { View, StyleSheet } from 'react-native'; | ||
import { Input, ListItem } from '@rneui/themed'; | ||
import React, { useState } from 'react'; | ||
import { IconNode } from '@rneui/base'; | ||
|
||
export interface AutocompleteWidgetProps { | ||
data: any[]; | ||
onChange: (value: string) => void; | ||
placeholder?: string; | ||
onPress: (id: string) => void; | ||
leftIcon?: IconNode; | ||
} | ||
|
||
export const Autocomplete: React.FC<AutocompleteWidgetProps> = ({ data, onChange, placeholder, onPress, leftIcon }) => { | ||
const [value, setValue] = useState(''); | ||
const [menuVisible, setMenuVisible] = useState(false); | ||
|
||
return ( | ||
<View style={styles.container}> | ||
<View style={styles.inputContainer}> | ||
<Input | ||
onFocus={() => { | ||
if (value?.length === 0) { | ||
setMenuVisible(true); | ||
} | ||
}} | ||
leftIcon={leftIcon} | ||
placeholder={placeholder} | ||
onBlur={() => setMenuVisible(false)} | ||
underlineColorAndroid={'transparent'} | ||
inputContainerStyle={{ borderBottomWidth: 0 }} | ||
onChangeText={(text) => { | ||
onChange(text); | ||
setMenuVisible(true); | ||
setValue(text); | ||
}} | ||
containerStyle={{ | ||
borderColor: 'black', | ||
borderWidth: 1, | ||
borderRadius: 4, | ||
height: 48, | ||
backgroundColor: 'white' | ||
}} | ||
/> | ||
</View> | ||
{menuVisible && ( | ||
<View style={styles.menuContainer}> | ||
{data.map((val, index) => ( | ||
<ListItem | ||
bottomDivider | ||
key={index} | ||
onPress={() => { | ||
setMenuVisible(false); | ||
onPress(val.id); | ||
}} | ||
style={{ paddingBottom: 8 }}> | ||
<ListItem.Content> | ||
{val.listName && ( | ||
<ListItem.Title style={{ fontSize: 18, color: 'black' }}>{val.listName}</ListItem.Title> | ||
)} | ||
{val.todoName && ( | ||
<ListItem.Subtitle style={{ fontSize: 14, color: 'grey' }}> | ||
{'\u2022'} {val.todoName} | ||
</ListItem.Subtitle> | ||
)} | ||
</ListItem.Content> | ||
<ListItem.Chevron /> | ||
</ListItem> | ||
))} | ||
</View> | ||
)} | ||
</View> | ||
); | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
container: { | ||
flexDirection: 'column', | ||
flex: 1, | ||
flexGrow: 1, | ||
marginHorizontal: 8 | ||
}, | ||
inputContainer: { | ||
flexDirection: 'row', | ||
flex: 0, | ||
marginVertical: 8 | ||
}, | ||
menuContainer: { | ||
flex: 2, | ||
flexGrow: 1, | ||
flexDirection: 'column' | ||
} | ||
}); |
Oops, something went wrong.