diff --git a/.flowconfig b/.flowconfig index a3e8681d1..9b27d10bd 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,7 +1,6 @@ [ignore] .*/scripts/.* .*/coverage/.* -.*/node_modules/.* .*/build/.* .*/dist/.* .*/static/.* @@ -9,6 +8,7 @@ .*/website/.* /src/plugins/sections/d3/d3.js$ .*\.tsx +.*flow-typed/.* [libs] flow-typed diff --git a/android/src/main/java/com/facebook/flipper/plugins/databases/ObjectMapper.java b/android/src/main/java/com/facebook/flipper/plugins/databases/ObjectMapper.java index e952ea91d..4b5b444e4 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/databases/ObjectMapper.java +++ b/android/src/main/java/com/facebook/flipper/plugins/databases/ObjectMapper.java @@ -173,17 +173,21 @@ public class ObjectMapper { DatabaseExecuteSqlResponse databaseExecuteSqlResponse) { FlipperArray.Builder columnBuilder = new FlipperArray.Builder(); - for (String columnName : databaseExecuteSqlResponse.columns) { - columnBuilder.put(columnName); + if (databaseExecuteSqlResponse.columns != null) { + for (String columnName : databaseExecuteSqlResponse.columns) { + columnBuilder.put(columnName); + } } FlipperArray.Builder rowBuilder = new FlipperArray.Builder(); - for (List row : databaseExecuteSqlResponse.values) { - FlipperArray.Builder valueBuilder = new FlipperArray.Builder(); - for (Object item : row) { - valueBuilder.put(objectAndTypeToFlipperObject(item)); + if (databaseExecuteSqlResponse.values != null) { + for (List row : databaseExecuteSqlResponse.values) { + FlipperArray.Builder valueBuilder = new FlipperArray.Builder(); + for (Object item : row) { + valueBuilder.put(objectAndTypeToFlipperObject(item)); + } + rowBuilder.put(valueBuilder.build()); } - rowBuilder.put(valueBuilder.build()); } return new FlipperObject.Builder() diff --git a/flow-typed/npm/sql-formatter_vx.x.x.js b/flow-typed/npm/sql-formatter_vx.x.x.js new file mode 100644 index 000000000..2416aa353 --- /dev/null +++ b/flow-typed/npm/sql-formatter_vx.x.x.js @@ -0,0 +1,193 @@ +// flow-typed signature: 21bf70a7f73a77579f4cc048adda0919 +// flow-typed version: <>/sql-formatter_v2.3.3/flow_v0.102.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'sql-formatter' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'sql-formatter' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'sql-formatter/dist/sql-formatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/dist/sql-formatter.min' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/core/Formatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/core/Indentation' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/core/InlineBlock' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/core/Params' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/core/Tokenizer' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/core/tokenTypes' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/languages/Db2Formatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/languages/N1qlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/languages/PlSqlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/languages/StandardSqlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/lib/sqlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/core/Formatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/core/Indentation' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/core/InlineBlock' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/core/Params' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/core/Tokenizer' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/core/tokenTypes' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/languages/Db2Formatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/languages/N1qlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/languages/PlSqlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/languages/StandardSqlFormatter' { + declare module.exports: any; +} + +declare module 'sql-formatter/src/sqlFormatter' { + declare module.exports: any; +} + +// Filename aliases +declare module 'sql-formatter/dist/sql-formatter.js' { + declare module.exports: $Exports<'sql-formatter/dist/sql-formatter'>; +} +declare module 'sql-formatter/dist/sql-formatter.min.js' { + declare module.exports: $Exports<'sql-formatter/dist/sql-formatter.min'>; +} +declare module 'sql-formatter/lib/core/Formatter.js' { + declare module.exports: $Exports<'sql-formatter/lib/core/Formatter'>; +} +declare module 'sql-formatter/lib/core/Indentation.js' { + declare module.exports: $Exports<'sql-formatter/lib/core/Indentation'>; +} +declare module 'sql-formatter/lib/core/InlineBlock.js' { + declare module.exports: $Exports<'sql-formatter/lib/core/InlineBlock'>; +} +declare module 'sql-formatter/lib/core/Params.js' { + declare module.exports: $Exports<'sql-formatter/lib/core/Params'>; +} +declare module 'sql-formatter/lib/core/Tokenizer.js' { + declare module.exports: $Exports<'sql-formatter/lib/core/Tokenizer'>; +} +declare module 'sql-formatter/lib/core/tokenTypes.js' { + declare module.exports: $Exports<'sql-formatter/lib/core/tokenTypes'>; +} +declare module 'sql-formatter/lib/languages/Db2Formatter.js' { + declare module.exports: $Exports<'sql-formatter/lib/languages/Db2Formatter'>; +} +declare module 'sql-formatter/lib/languages/N1qlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/lib/languages/N1qlFormatter'>; +} +declare module 'sql-formatter/lib/languages/PlSqlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/lib/languages/PlSqlFormatter'>; +} +declare module 'sql-formatter/lib/languages/StandardSqlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/lib/languages/StandardSqlFormatter'>; +} +declare module 'sql-formatter/lib/sqlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/lib/sqlFormatter'>; +} +declare module 'sql-formatter/src/core/Formatter.js' { + declare module.exports: $Exports<'sql-formatter/src/core/Formatter'>; +} +declare module 'sql-formatter/src/core/Indentation.js' { + declare module.exports: $Exports<'sql-formatter/src/core/Indentation'>; +} +declare module 'sql-formatter/src/core/InlineBlock.js' { + declare module.exports: $Exports<'sql-formatter/src/core/InlineBlock'>; +} +declare module 'sql-formatter/src/core/Params.js' { + declare module.exports: $Exports<'sql-formatter/src/core/Params'>; +} +declare module 'sql-formatter/src/core/Tokenizer.js' { + declare module.exports: $Exports<'sql-formatter/src/core/Tokenizer'>; +} +declare module 'sql-formatter/src/core/tokenTypes.js' { + declare module.exports: $Exports<'sql-formatter/src/core/tokenTypes'>; +} +declare module 'sql-formatter/src/languages/Db2Formatter.js' { + declare module.exports: $Exports<'sql-formatter/src/languages/Db2Formatter'>; +} +declare module 'sql-formatter/src/languages/N1qlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/src/languages/N1qlFormatter'>; +} +declare module 'sql-formatter/src/languages/PlSqlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/src/languages/PlSqlFormatter'>; +} +declare module 'sql-formatter/src/languages/StandardSqlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/src/languages/StandardSqlFormatter'>; +} +declare module 'sql-formatter/src/sqlFormatter.js' { + declare module.exports: $Exports<'sql-formatter/src/sqlFormatter'>; +} diff --git a/src/plugins/databases/ClientProtocol.js b/src/plugins/databases/ClientProtocol.js index a3cc680c4..ddfe7ac62 100644 --- a/src/plugins/databases/ClientProtocol.js +++ b/src/plugins/databases/ClientProtocol.js @@ -48,6 +48,28 @@ type GetTableStructureResponse = { definition: string, }; +type ExecuteSqlRequest = { + databaseId: number, + value: string, +}; + +type ExecuteSqlResponse = { + type: string, + columns: Array, + values: Array>, + insertedId: number, + affectedCount: number, +}; + +type GetTableInfoRequest = { + databaseId: number, + table: string, +}; + +type GetTableInfoResponse = { + definition: string, +}; + export class DatabaseClient { client: PluginClient; @@ -67,4 +89,12 @@ export class DatabaseClient { GetTableStructureRequest, GetTableStructureResponse, > = params => this.client.call('getTableStructure', params); + + getExecution: ClientCall = params => + this.client.call('execute', params); + + getTableInfo: ClientCall< + GetTableInfoRequest, + GetTableInfoResponse, + > = params => this.client.call('getTableInfo', params); } diff --git a/src/plugins/databases/index.js b/src/plugins/databases/index.js index 60d1add10..7592a9677 100644 --- a/src/plugins/databases/index.js +++ b/src/plugins/databases/index.js @@ -18,6 +18,11 @@ import { Input, colors, getStringFromErrorLike, + Spacer, + Textarea, + DetailSidebar, + Panel, + ManagedDataInspector, } from 'flipper'; import {Component} from 'react'; import type { @@ -29,6 +34,8 @@ import {DatabaseClient} from './ClientProtocol'; import {renderValue} from 'flipper'; import type {Value} from 'flipper'; import ButtonNavigation from './ButtonNavigation'; +import sqlFormatter from 'sql-formatter'; +import dateFormat from 'dateformat'; const PAGE_SIZE = 50; @@ -51,11 +58,17 @@ type DatabasesPluginState = {| pageRowNumber: number, databases: Array, outdatedDatabaseList: boolean, - viewMode: 'data' | 'structure', + viewMode: 'data' | 'structure' | 'SQL' | 'tableInfo' | 'queryHistory', error: ?null, currentPage: ?Page, currentStructure: ?Structure, currentSort: ?TableRowSortOrder, + query: ?Query, + queryResult: ?QueryResult, + favorites: Array, + executionTime: number, + tableInfo: string, + queryHistory: Array, |}; type Page = { @@ -77,6 +90,18 @@ type Structure = {| indexesValues: Array, |}; +type QueryResult = { + table: ?QueriedTable, + id: ?number, + count: ?number, +}; + +type QueriedTable = { + columns: Array, + rows: Array, + highlightedRows: Array, +}; + type Actions = | SelectDatabaseEvent | SelectDatabaseTableEvent @@ -84,11 +109,18 @@ type Actions = | UpdateViewModeEvent | UpdatePageEvent | UpdateStructureEvent + | DisplaySelectEvent + | DisplayInsertEvent + | DisplayUpdateDeleteEvent + | UpdateTableInfoEvent | NextPageEvent | PreviousPageEvent + | ExecuteEvent | RefreshEvent + | UpdateFavoritesEvent | SortByChangedEvent - | GoToRowEvent; + | GoToRowEvent + | UpdateQueryEvent; type DatabaseEntry = { id: number, @@ -96,6 +128,11 @@ type DatabaseEntry = { tables: Array, }; +type Query = {| + value: string, + time: string, +|}; + type UpdateDatabasesEvent = {| databases: Array<{name: string, id: number, tables: Array}>, type: 'UpdateDatabases', @@ -113,7 +150,7 @@ type SelectDatabaseTableEvent = {| type UpdateViewModeEvent = {| type: 'UpdateViewMode', - viewMode: 'data' | 'structure', + viewMode: 'data' | 'structure' | 'SQL' | 'tableInfo' | 'queryHistory', |}; type UpdatePageEvent = {| @@ -137,6 +174,27 @@ type UpdateStructureEvent = {| indexesValues: Array>, |}; +type DisplaySelectEvent = {| + type: 'DisplaySelect', + columns: Array, + values: Array>, +|}; + +type DisplayInsertEvent = {| + type: 'DisplayInsert', + id: number, +|}; + +type DisplayUpdateDeleteEvent = {| + type: 'DisplayUpdateDelete', + count: number, +|}; + +type UpdateTableInfoEvent = {| + type: 'UpdateTableInfo', + tableInfo: string, +|}; + type NextPageEvent = { type: 'NextPage', }; @@ -145,10 +203,18 @@ type PreviousPageEvent = { type: 'PreviousPage', }; +type ExecuteEvent = { + type: 'Execute', +}; + type RefreshEvent = { type: 'Refresh', }; +type UpdateFavoritesEvent = { + type: 'UpdateFavorites', +}; + type SortByChangedEvent = { type: 'SortByChanged', sortOrder: TableRowSortOrder, @@ -159,6 +225,11 @@ type GoToRowEvent = { row: number, }; +type UpdateQueryEvent = { + type: 'UpdateQuery', + value: string, +}; + function transformRow( columns: Array, row: Array, @@ -250,6 +321,47 @@ function renderDatabaseIndexes(structure: ?Structure) { ); } +function renderQueryHistory(history: Array) { + if (!history || typeof history === 'undefined') { + return null; + } + const columns = { + time: { + value: 'Time', + resizable: true, + }, + query: { + value: 'Query', + resizable: true, + }, + }; + const rows = []; + if (history.length > 0) { + for (const query of history) { + const time = query.time; + const value = query.value; + rows.push({ + key: query, + columns: {time: {value: time}, query: {value: value}}, + }); + } + } + + return ( + + + + ); +} + type PageInfoProps = { currentRow: number, count: number, @@ -277,7 +389,6 @@ class PageInfo extends Component< onSubmit(e: SyntheticKeyboardEvent<>) { if (e.key === 'Enter') { const rowNumber = parseInt(this.state.inputValue, 10); - console.log(rowNumber); this.props.onChange(rowNumber - 1, this.props.count); this.setState({isOpen: false}); } @@ -331,6 +442,12 @@ export default class DatabasesPlugin extends FlipperPlugin< currentPage: null, currentStructure: null, currentSort: null, + query: null, + queryResult: null, + favorites: [], + executionTime: 0, + tableInfo: '', + queryHistory: [], }; reducers = [ @@ -414,6 +531,7 @@ export default class DatabasesPlugin extends FlipperPlugin< return { ...state, viewMode: event.viewMode, + error: null, }; }, ], @@ -458,6 +576,73 @@ export default class DatabasesPlugin extends FlipperPlugin< }; }, ], + [ + 'DisplaySelect', + ( + state: DatabasesPluginState, + event: DisplaySelectEvent, + ): DatabasesPluginState => { + return { + ...state, + queryResult: { + table: { + columns: event.columns, + rows: event.values.map((row: Array, index: number) => + transformRow(event.columns, row, index), + ), + highlightedRows: [], + }, + id: null, + count: null, + }, + }; + }, + ], + [ + 'DisplayInsert', + ( + state: DatabasesPluginState, + event: DisplayInsertEvent, + ): DatabasesPluginState => { + return { + ...state, + queryResult: { + table: null, + id: event.id, + count: null, + }, + }; + }, + ], + + [ + 'DisplayUpdateDelete', + ( + state: DatabasesPluginState, + event: DisplayUpdateDeleteEvent, + ): DatabasesPluginState => { + return { + ...state, + queryResult: { + table: null, + id: null, + count: event.count, + }, + }; + }, + ], + [ + 'UpdateTableInfo', + ( + state: DatabasesPluginState, + event: UpdateTableInfoEvent, + ): DatabasesPluginState => { + return { + ...state, + tableInfo: event.tableInfo, + }; + }, + ], [ 'NextPage', ( @@ -484,6 +669,66 @@ export default class DatabasesPlugin extends FlipperPlugin< }; }, ], + [ + 'Execute', + ( + state: DatabasesPluginState, + results: ExecuteEvent, + ): DatabasesPluginState => { + const timeBefore = Date.now(); + if ( + this.state.query !== null && + typeof this.state.query !== 'undefined' + ) { + this.databaseClient + .getExecution({ + databaseId: state.selectedDatabase, + value: this.state.query.value, + }) + .then(data => { + this.setState({ + error: null, + executionTime: Date.now() - timeBefore, + }); + if (data.type === 'select') { + this.dispatchAction({ + type: 'DisplaySelect', + columns: data.columns, + values: data.values, + }); + } else if (data.type === 'insert') { + this.dispatchAction({ + type: 'DisplayInsert', + id: data.insertedId, + }); + } else if (data.type === 'update_delete') { + this.dispatchAction({ + type: 'DisplayUpdateDelete', + count: data.affectedCount, + }); + } + }) + .catch(e => { + this.setState({error: e}); + }); + } + let newHistory = this.state.queryHistory; + const newQuery = this.state.query; + if ( + newQuery !== null && + typeof newQuery !== 'undefined' && + newHistory !== null && + typeof newHistory !== 'undefined' + ) { + newQuery.time = dateFormat(new Date(), 'hh:MM:ss'); + newHistory = newHistory.concat(newQuery); + } + return { + ...state, + queryHistory: newHistory, + }; + }, + ], [ 'GoToRow', ( @@ -519,6 +764,32 @@ export default class DatabasesPlugin extends FlipperPlugin< }; }, ], + [ + 'UpdateFavorites', + ( + state: DatabasesPluginState, + event: UpdateFavoritesEvent, + ): DatabasesPluginState => { + let newFavorites = state.favorites; + if ( + state.query && + state.query !== null && + typeof state.query !== 'undefined' + ) { + const value = state.query.value; + if (newFavorites.includes(value)) { + const index = newFavorites.indexOf(value); + newFavorites.splice(index, 1); + } else { + newFavorites = state.favorites.concat(value); + } + } + return { + ...state, + favorites: newFavorites, + }; + }, + ], [ 'UpdateViewMode', ( @@ -528,6 +799,7 @@ export default class DatabasesPlugin extends FlipperPlugin< return { ...state, viewMode: event.viewMode, + error: null, }; }, ], @@ -542,6 +814,18 @@ export default class DatabasesPlugin extends FlipperPlugin< }; }, ], + [ + 'UpdateQuery', + (state: DatabasesPluginState, event: UpdateQueryEvent) => { + return { + ...state, + query: { + value: event.value, + time: dateFormat(new Date(), 'hh:MM:ss'), + }, + }; + }, + ], ].reduce((acc, val) => { const name = val[0]; const f = val[1]; @@ -576,7 +860,6 @@ export default class DatabasesPlugin extends FlipperPlugin< start: newState.pageRowNumber, }) .then(data => { - console.log(data); this.dispatchAction({ type: 'UpdatePage', databaseId: databaseId, @@ -604,7 +887,6 @@ export default class DatabasesPlugin extends FlipperPlugin< table: table, }) .then(data => { - console.log(data); this.dispatchAction({ type: 'UpdateStructure', databaseId: databaseId, @@ -619,6 +901,28 @@ export default class DatabasesPlugin extends FlipperPlugin< this.setState({error: e}); }); } + if ( + newState.viewMode === 'tableInfo' && + newState.currentStructure === null && + databaseId && + table + ) { + this.databaseClient + .getTableInfo({ + databaseId: databaseId, + table: table, + }) + .then(data => { + this.dispatchAction({ + type: 'UpdateTableInfo', + tableInfo: data.definition, + }); + }) + .catch(e => { + this.setState({error: e}); + }); + } + if (!previousState.outdatedDatabaseList && newState.outdatedDatabaseList) { this.databaseClient.getDatabases({}).then(databases => { this.dispatchAction({ @@ -647,10 +951,29 @@ export default class DatabasesPlugin extends FlipperPlugin< this.dispatchAction({type: 'UpdateViewMode', viewMode: 'structure'}); }; + onSQLClicked = () => { + this.dispatchAction({type: 'UpdateViewMode', viewMode: 'SQL'}); + }; + + onTableInfoClicked = () => { + this.dispatchAction({type: 'UpdateViewMode', viewMode: 'tableInfo'}); + }; + + onQueryHistoryClicked = () => { + this.dispatchAction({type: 'UpdateViewMode', viewMode: 'queryHistory'}); + }; + onRefreshClicked = () => { + this.setState({error: null}); this.dispatchAction({type: 'Refresh'}); }; + onFavoritesClicked = () => { + this.dispatchAction({ + type: 'UpdateFavorites', + }); + }; + onDatabaseSelected = (selected: string) => { const dbId = this.state.databases.find(x => x.name === selected)?.id || 0; this.dispatchAction({ @@ -674,10 +997,25 @@ export default class DatabasesPlugin extends FlipperPlugin< this.dispatchAction({type: 'PreviousPage'}); }; + onExecuteClicked = () => { + this.dispatchAction({type: 'Execute'}); + }; + + onFavoriteClicked = (selected: any) => { + this.setState({query: selected.target.value}); + }; + onGoToRow = (row: number, count: number) => { this.dispatchAction({type: 'GoToRow', row: row}); }; + onQueryChanged = (selected: any) => { + this.dispatchAction({ + type: 'UpdateQuery', + value: selected.target.value, + }); + }; + renderStructure() { return [ renderDatabaseColumns(this.state.currentStructure), @@ -685,6 +1023,153 @@ export default class DatabasesPlugin extends FlipperPlugin< ]; } + renderSidebar = (table: QueriedTable) => { + if ( + table.highlightedRows === null || + typeof table.highlightedRows === 'undefined' || + table.highlightedRows.length !== 1 + ) { + return null; + } + const id = table.highlightedRows[0]; + const cols = { + col: { + value: 'Column', + resizable: true, + }, + val: { + value: 'Value', + resizable: true, + }, + }; + const colSizes = { + col: '35%', + val: 'flex', + }; + return ( + + + + + + ); + }; + + sidebarRows = (id: number, table: QueriedTable) => { + const columns = table.columns; + const row = table.rows[id]; + const sidebarArray = []; + for (let i = 0; i < columns.length; i++) { + sidebarArray.push( + this.buildSidebarRow(columns[i], row.columns[columns[i]].value), + ); + } + return sidebarArray; + }; + + buildSidebarRow = (key: string, val: any) => { + let output = ''; + try { + output = ( + + ); + } catch (error) { + output = val; + } + return { + columns: { + col: {value: {key}}, + val: { + value: output, + }, + }, + key: key, + }; + }; + + renderQuery(query: ?QueryResult) { + if (!query || query === null) { + return null; + } + if ( + query.table && + typeof query.table !== 'undefined' && + query.table !== null + ) { + const table = query.table; + const columns = table.columns; + const rows = table.rows; + return ( + + ({ + key: name, + visible: true, + }))} + columns={columns.reduce((acc, val) => { + acc[val] = {value: val, resizable: true}; + return acc; + }, {})} + zebra={true} + rows={rows} + horizontallyScrollable={true} + onRowHighlighted={highlightedRows => { + this.setState({ + queryResult: { + table: { + columns: columns, + rows: rows, + highlightedRows: highlightedRows, + }, + id: null, + count: null, + }, + }); + }} + /> + {this.renderSidebar(table)} + + ); + } else if (query.id && query.id !== null) { + return ( + + + Row id: {query.id} + + + ); + } else if (query.count && query.count !== null) { + return ( + + + Rows affected: {query.count} + + + ); + } else { + return null; + } + } + render() { const tableOptions = (this.state.selectedDatabase && @@ -697,53 +1182,177 @@ export default class DatabasesPlugin extends FlipperPlugin< return ( - - Database - -
- - + + + + + + + + + {this.state.viewMode === 'data' || + this.state.viewMode === 'structure' || + this.state.viewMode === 'tableInfo' ? ( + + Database + +
+ + + ) : null} + {this.state.viewMode === 'SQL' ? ( +
+ + Database +