Using "react-native-sqlite-storage" the application is already live on Google Play. I want to add two columns to an existing database table in such a way the application will not crash for existing users.
When you will add column to existing table you will get error like
error: {"message":"no such column: todoStar","code":5} // todoStar is column name
In error block you can alter the table and add your column. Make sure you write the code inside error block
if (error.message.indexOf('no such column: todoStar') > -1) {
dbConnection.transaction(function (tx2) {
let query = 'ALTER TABLE todos ADD todoStar Int(32);'; // todos is tableName
tx2.executeSql(
query,
'',
async (tx3) => {
console.log('new column added');
},
(error) => {
console.error(
'error: while adding column' + JSON.stringify(error),
);
},
);
});
}
As usual official documentation only contains basic information, and there is not much information on migration or database upgrades.
After hours of long research, I found that SQLite has user_version which can be used to know which version the user currently has, based on that we can execute upgrade queries.
I am using expo-sqlite, but the below solution should also work for react-native-sqlite-storage
Example:
/** database.tsx **/
export function getUserDBVersion() {
return new Promise<SQLite.SQLResultSet>((resolve, reject) => {
database.transaction((tx) => {
tx.executeSql(
`PRAGMA user_version`,
[],
(_, res) => {
resolve(res);
},
(_, error) => {
reject(error);
return true;
}
);
});
});
}
export function updateUserDBVersion(version: number) {
return new Promise<SQLite.SQLResultSet>((resolve, reject) => {
database.transaction((tx) => {
tx.executeSql(
`PRAGMA user_version = ?`,
[version],
(_, res) => {
resolve(res);
},
(_, error) => {
reject(error);
return true;
}
);
});
});
}
/** App.tsx **/
useEffect(() => {
getUserDBVersion().then((res) => {
const user_version = res.rows._array[0].user_version;
if (user_version < 1) {
//User has old version
//(1) Run function to upgrade your database tables
alterTables().then(() => {
//(2) Update user_version so that next time you can check if < 2 for new updates
updateUserVersion(1);
});
}
});
}, []);
I think this is an elegant way for 2 reasons
You can update all of your databases and tables in one place
You can display the splash screen until all this migration is completed
*For readability I have not added error handling part above
*Most of the above code will be the same for react-native-sqlite-storage, if not update your code accordingly
*The above code is in typescript, simply remove types if you are using jsx
Related
I have a list of games that I'm able to add to without issue using UseEffect and onSnapshot. I can modify an item in the list without issue, and return one set of results (with the updated data properly displaying). When I try to modify another item (or the item same again), I get this error:
Could not update game: TypeError: undefined is not an object (evaluating '_doc.data().numPlayers') because the results/list of games are null. I'm sure I have something wrong with my code, but I can't figure it out.
Thanks in advance!
Here is my code:
useEffect(() => {
setIsLoading(true)
let results = [];
const unsubscribe = db
.collection('games')
.onSnapshot(
(querySnapshot) => {
querySnapshot.docChanges().forEach(change => {
const id = change.doc.id;
if (change.type === 'added') {
const gameData = change.doc.data();
gameData.id = id;
results.push(gameData);
}
if (change.type === 'modified') {
console.log('Modified game: ', id);
results = results.map(game => {
if (game.id === id) {
return change.doc.data()
}
return game
})
console.log(results)
}
if (change.type === 'removed') {
console.log('Removed game: ', id);
}
});
setIsLoading(false);
setGame(results);
return () => unsubscribe
},
(err) => {
setIsLoading(false);
console.log("Data could not be fetched", err);
}
);
}, []);
I forgot to add the doc ID to the gameData before adding it to the results. I did that in the "added" section, but not in the "modified" section (thinking that it was already included), forgetting that I hadn't added it as an actual field in the database (it just exists as the doc id).
Using IONIC 3 and sqlite3 v2 native, testing on Android device.
When I use one table everything is fine, but when I am trying to create 3 tables then I have a problem with other two.
database.ts (in providers):
...
db: any;
isOpen: Boolean = false;
constructor(private sqlite: SQLite) {}
createDatabase() {
return new Promise((resolve, reject) => {
this.sqlite.create({
name: 'mydatabase.db',
location: 'default'
}).then((db: SQLiteObject) => {
this.isOpen = true;
resolve(db);
}).catch(err => reject(err));
});
}
openDb() {
return new Promise((resolve, reject) => {
if (this.isOpen) {
resolve({});
} else {
this.createDatabase().then(db => {
this.db = db;
this.db.executeSql("CREATE TABLE IF NOT EXISTS first (*my first query...)",{})
.then(("CREATE TABLE IF NOT EXISTS second (*my second query...))"),{})
.then(("CREATE TABLE IF NOT EXISTS third (*my third query...)"),{})
.then(res => resolve(res))
.catch(err=>reject(err));
}).catch(err => reject(err));
}
});
}
error message:
sqlite3_prepare_v2 failure: no such table: second, code 5
second attempt:
openDb() {
return new Promise((resolve, reject) => {
this.createDatabase().then(db => {
this.db = db;
this.db.executeSql("CREATE TABLE IF NOT EXISTS first (firstquery...)",{})
.then(()=>{this.db.executeSql("CREATE TABLE IF NOT EXISTS second (secondquery...)",{})})
.then(res=>resolve(res))
.catch(err=>reject(err));
});
second then never triggered and only first table created.
I used many methods but this above it seems to be a solution nearest to my problem.
Does this method seems right or I have to use something different, in order to make this work?
What executeSql returns is a promise telling whether the given query is successfully executed or not. If you wish to create another table only if the first table is created then you need to do as below
this.db.executeSql("CREATE TABLE IF NOT EXISTS first (*my first query...)",{})
.then(() =>{
this.db.executeSql("CREATE TABLE IF NOT EXISTS second (*my second query...))"),{}).then(()=>{
//Something else if required
}).catch(err => reject(err));
}).catch(err => reject(err));
I am facing a problem with the new Firestore from Firebase.
Situation: I have a collection('room')
I create room with collection('room').add(room)
What I'm trying to do: I need to update a room.
For this, I use: collection('room').doc(ROOM_ID).update(update)
So I need to add ROOM_ID in the document in my collection:
|room
ROOM_ID
id:ROOM_ID,
someContent: ForTheQuery
Is there a possible way to achieve that?
An alternative is to create myself a generated ID with:
collection('room')
.doc(someId)
.set({
id: someId,
someContent: ForTheQuery
});
but i want to avoid it.
You can use doc() to create a reference to a document with a unique id, but the document will not be created yet. You can then set the contents of that doc by using the unique id that was provided in the document reference:
const ref = store.collection('users').doc()
console.log(ref.id) // prints the unique id
ref.set({id: ref.id}) // sets the contents of the doc using the id
.then(() => { // fetch the doc again and show its data
ref.get().then(doc => {
console.log(doc.data()) // prints {id: "the unique id"}
})
})
ANGULARFIRE:
get ID before add database:
var idBefore = afs.createId();
console.log(idBefore );
ANDROID FIRESTORE:
String idBefore = db.collection("YourCol").document().getId();
Firebase Javascript SDK:
Just use .id to get the key, here is an example using async/ await :
const KEYID = async() => (await fs.collection("testing").add({ data: 'test'})).id;
You can get the ID from the created document by using collection.ref.add(your item without id) and the response (res) will contain the new document reference created with the ID inside it. So get the ID by simply doing res.id.
createOne(options: { item: any, ref: AngularFirestoreCollection<any> }) {
const promise = new Promise((resolve, reject) => {
if (options.item) {
// Convert object to pure javascript
const item = Object.assign({}, options.item);
console.log('dataService: createOne: set item: ', item);
options.ref.ref.add(item)
.then((res) => {
console.log('dataService: createOne success: res: ', res);
resolve(res);
}).catch(err => {
console.error('dataService: createOne: error: ', err);
reject(err);
});
} else {
console.log('dataService: createOne: wrong options! options: ', options);
reject();
}
})
return promise;
}
I am using Ionic2 with AngularFire2.
I am also making use of a rxjs Observable. I have the following code:
findChatsForUid(uid: string): Observable<any[]> {
return this.af.database.list('/chat/', {
query: {
orderByChild: 'negativtimestamp'
}
}).map(items => {
const filtered = items.filter(
item => (item.memberId1 === uid || item.memberId2 === uid)
);
return filtered;
});
}
and
deleteChatsAndMessagesForUid(uid: string): Promise<any> {
return new Promise<any>((resolve) => {
let promiseArray: Promise<any>[] = [];
this.findChatsForUid(uid).map(items => {
return items;
}).forEach((chatItems) => {
for (let i: number = 0; i < chatItems.length; i++) {
promiseArray.push(this.deleteChat(chatItems[i], true));
}
Promise.all(promiseArray).then(() => {
resolve(true);
});
});
});
}
In the second function, you can see I retrieve the Observable from the first, and the loop through each item using the forEach function.
My problem is, because this is an Observable, there is always a handle to the object. So when I do the following:
deleteChatsAndMessagesForUid(uid).then(() => {
user.delete().then(() => {
...
}
}
It results in the following error because the deleted user is still trying to observe the Observable.
Error: Uncaught (in promise): Error: permission_denied at /chat:
Client doesn't have permission to access the desired data. Error:
permission_denied at /chat: Client doesn't have permission to access
the desired data.
Question
Is there a way to retrieve the data, without still being attached to the Observable? So that I am free to delete the associated user? Or is there a better way to handle this?
Thanks
It sounds like you want to unsubsribe from the list observable after the first emitted list.
You can use the first operator to complete the list observable after the first emitted list. This will result in automatic unsubscription and the listeners will be removed from the internal Firebase ref.
import 'rxjs/add/operator/first';
findChatsForUid(uid: string): Observable<any[]> {
return this.af.database
.list('/chat/', {
query: {
orderByChild: 'negativtimestamp'
}
})
.first()
.map(items => {
const filtered = items.filter(
item => (item.memberId1 === uid || item.memberId2 === uid)
);
return filtered;
});
}
I'm using Ionic 2 (Angular 2) for a Hybrid app. I inject a shared provider into the page that will display data from my SQLite3 database and then proceed to load the data. However, on creation of my database provider opening the database takes some time (very little). My code (as of this moment) however does not wait for the database to be opened before querying, which obviously results in an error.
How can I structure my code that it will wait for the database to be opened in order to evade a crash?
The constructor of my database provider:
constructor(private platform: Platform) {
this.platform.ready().then(() => {
if(this.isOpen !== true) {
this.storage = new SQLite();
this.storage.openDatabase({name: "data.db", location: "default"}).then(() => {
this.isOpen = true;
this.storage.executeSql("CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, firstname TEXT, lastname TEXT)", []);
});
}
});
console.log('Hello Database Provider');
This provider gets injected into the constructor of my page.
When the page (home page) is loaded it triggers an event that calls a load() function.
ionViewDidLoad() {
this.load();
console.log('Hello Home Page');
The load function:
public load() {
this.database.getPeople().then((result) => {
this.itemList = <Array<Object>> result;
}, (error) => {
console.log("LOAD ERROR: ", error);
});
I'm very much hoping someone can point me in the right direction :)
I've finally found a solution to my problem.
For starters I've added a function to my provider that checks if the database is loaded, if it isn't it proceeds to load it:
public openSQLiteDatabase() {
return new Promise((resolve, reject) => {
if(this.isOpen) {
console.log("DB IS OPEN");
resolve(this.isOpen);
}
else {
console.log("DB IS NOT OPEN");
this.platform.ready().then(() => {
this.storage.openDatabase({name: "data.db", location: "default"}).then(() => {
this.appsettings.openSQLiteDatabase().then(() => {
this.appsettings.getSettings().then((result) => {
let settings: Settings = <Settings> result;
this.selectedDataset = settings.selectedDataset;
this.isOpen = true;
resolve(this.isOpen);
});
});
}, (error) => {
reject(error);
});
});
}
});}
As this function returns a promise (JS Promises) it allows me to wait for the database to be opened before doing anything else (such as querying).
My function in the page-specific TypeScript file:
ionViewDidLoad() {
this.database.openSQLiteDatabase().then(() => {
this.loadDictionary();
});}
With code like this I never have problems with queries being performed before my database has been opened!