Dexie.js icon indicating copy to clipboard operation
Dexie.js copied to clipboard

Drop all tables on upgrade

Open Ciantic opened this issue 8 years ago • 20 comments

There doesn't seem to be a good way to drop all tables on upgrade, but it would be really helpful especially when developing.

My app does not rely on IndexedDb content, if it's empty it's populated from server. Thus this would be a good way for me to tinker, since I need not to worry about upgrade process during development. When the contents of the indexeddb is always pristine, I could just change the version number and know my db is empty:

this.version(2).stores({
            projects: "++id,client,project,start,end"
        }).upgrade (() => {
            // How to drop all tables here?
        });

Edit Alternatively for my case, I could use a way to drop a whole database if the version differs before dexie creates a new one.

Ciantic avatar Oct 19 '16 06:10 Ciantic

The upgrade() method is meant to convert existing model to a new model. If you need to drop all tables, you don't need to use upgrade().

var db = new Dexie ('dbname');
db.version(1).stores({
    projects: "++id,client,project,start,end"
});

function recreateDatabase () {
    return db.delete().then(()=> db.open());
}

recreateDatabase().then(db => {
    // Here you have a fresh empty database with tables and indexes as defined in version(1),
    // no matter what version the db was on earlier or what data it had.
});

dfahlander avatar Oct 19 '16 08:10 dfahlander

If let's say you've released your app with db.version(1) and have changed your model so that a certain table is no more needed, you can explicitely drop the old table as such:

db.version(1).stores({
    projects: "++id,client,project,start,end",
    project_details: "++id,project_id"
});
db.version(2).stores({
    projects: "++id,client,project,start,end,details",
    project_details: null // Will delete the table
});

Note: You need to keep version(1) in your code even though it's not used anymore. This is used for diffing with version(2) and apply the changes needed.

If you're not interested in keeping existing data at all, just recreate the database:

db.delete().then(()=>db.open())

dfahlander avatar Oct 19 '16 09:10 dfahlander

I don't want to drop it always, only when the version number changes. I don't think your examples cover that?

Btw, why did this work? I know it maybe because the delete runs in some cases fast enough:

        this.version(4).stores({
            projects: "++id,client,project,start,end",
        }).upgrade (async () => {
            await this.delete();
        });

But if that were allowed, it would work wonders.

Edit the point is, I don't want to keep old versions as I develop. Only drop on demand as I increase version number. And indeed in production it would work for my app too.

Ciantic avatar Oct 19 '16 11:10 Ciantic

Can I check the current version before I open the dexie db? I'd like to drop the db if the version differs. I'm getting all sort of troubles at the moment with versions.

Btw, this stuff I try to avoid at all costs:

db.version(1).stores({
    projects: "++id,client,project,start,end",
    project_details: "++id,project_id"
});
db.version(2).stores({
    projects: "++id,client,project,start,end,details",
    project_details: null // Will delete the table
});

Totally unneccessary stuff when developing, I don't want to keep old versions lying in my code, as they were temporary.

Ciantic avatar Oct 20 '16 07:10 Ciantic

Yes you can open database without specifying version:

new Dexie("dbname").open().then(db => {
    console.log (`Current version is: ${db.verno}`);
});

dfahlander avatar Oct 20 '16 08:10 dfahlander

I couldn't get it working, when using inheritance:

class MyDb extends Dexie {
    projects: Dexie.Table<{
        id?: number;
        client: string;
        project: string;
        start: Date;
        end: Date;
    }, number>;

    constructor() {
        super("MyDb");
        this.open().then(d => {
            if (d.verno !== 30) {
                d.delete();
            }
            this.close();
        });
        this.version(3).stores({
            projects: "++id,client,project,start,end"
        });
    }
}

I still think the cleanest way would be a way to drop a whole database during upgrade.

Ciantic avatar Oct 20 '16 09:10 Ciantic

That doesn't work because this.version(3) will be executed before the database is opened.

You should ideally bootstrap your app with:

async function openDatabase() {
    var tempDb = await new Dexie("dbname").open();
    if (tempDb.verno < 3) await tempDb.delete();
    tempDb.close();
    return await new MyDb().open();    
});

dfahlander avatar Oct 20 '16 09:10 dfahlander

..or possibly the following:

class MyDb extends Dexie {
    projects: Dexie.Table<{
        id?: number;
        client: string;
        project: string;
        start: Date;
        end: Date;
    }, number>;

    constructor() {
        super("MyDb");
    }

    async init () => {
        await this.open();
        if (this.verno < 3) {
            await this.delete();
        } else {
            this.close();
        }
        this.version(3).stores({
            projects: "++id,client,project,start,end"
        });
        await this.open();
    }
}

dfahlander avatar Oct 20 '16 09:10 dfahlander

Heads up that it's a highly prioritized task to improve the version handling in Dexie (See #287 and #105), and I'll take your use case into account when doing that. Expect it to be improved within the next 2-3 months or so. Some of the improvements will be:

  1. Allow upgrade functions to access the database based on the old schema (run upgrader before old indexes and tables are deleted).
  2. No more need to keep the definitions of older versions in the code unless they have an upgrade() attached to them. Instead, Dexie should inspect the current schema and use that as the diff source instead of the specified one.
  3. Complain if there is a diff between the actual schema and the specified schema in case no upgrade was needed. Suggest increase of version number in the exception message.

Your use case is also relevant so, maybe it could be handled via some kind of instruction for that, such as:

db.version(3).recreate({
    projects: "++id,client,project,start,end"
});

Would a recreate alternative solve your case?

dfahlander avatar Oct 20 '16 10:10 dfahlander

Yes, it would solve my case, I appreciate the time you've taken to solve this.

I haven't yet figured out how to get the init example working, if I call init myself, the transactions doesn't seem to work afterwards, if I don't call init it's not working either.

Ciantic avatar Oct 20 '16 10:10 Ciantic

Are you awaiting the init() Another question, is your current version 3 or 30?

dfahlander avatar Oct 20 '16 10:10 dfahlander

Actually, I changed it to "3". But that is not the issue. The thing is, if my app is written like this:

let db = new ProjectMonitoringDb();
...
db.transaction("r", ...)
db.transaction("rw", ...)

Where could I await the init? In all transaction calls?

Edit If I've understood correctly, in order for me to await init, I should wrap my whole app in a callback?

Ciantic avatar Oct 20 '16 10:10 Ciantic

I get the point. There's another solution that might work better:

class MyDb extends Dexie {
    constructor() {
        super("MyDb");
        this.version(3).stores({
            projects: "++id,client,project,start,end"
        });
        let origOpen = this.open as any, // Override open like this because it's not on prototype
            openPromise : any = null;

        (this as any).open = function () {
            if (!openPromise) {
                openPromise = new Dexie(this.name).open().then(tempDb=>{
                    if (tempDb.verno < 3) return tempDb.delete();
                    tempDb.close();
                }).then(()=>origOpen.apply(this, arguments));
            }
            return openPromise;
        };
    }
}

Haven't compiled it but try it out, correct my bugs and see if it does it for you.

dfahlander avatar Oct 20 '16 10:10 dfahlander

Quick testing, it seems to work, and the code (regardless of odd inheritance pattern) makes sense. Thanks a lot!

Ciantic avatar Oct 20 '16 10:10 Ciantic

Nice to hear. If you need to keep abreast of close() / reopen to work also, you must override close:

let origClose = this.close as any;
(this as any).close = function() {
    openPromise = null;
    return origClose.apply(this, arguments);
}

dfahlander avatar Oct 20 '16 11:10 dfahlander

This worked well for me (version 3.0.3):

import Dexie from 'dexie';

export class MyDexie extends Dexie {
  private static readonly targetVersion = 10;
  
  public open() {
    if (this.isOpen()) return super.open();

    return Dexie.Promise.resolve()
      .then(() => Dexie.exists(this.name))
      .then((exists) => {
        if (!exists) {
          // no need to check database version since it doesn't exist
          return;
        }
        
        // Open separate instance of dexie to get current database version
        return new Dexie(this.name).open()
          .then(async db => {
            if (db.verno >= MyDexie.targetVersion) {
              // database up to date (or newer)
              return db.close();
            }

            console.log(`Database schema out of date, resetting all data. (currentVersion: ${db.verno}, expectedVersion: ${MyDexie.targetVersion})`);
            await db.delete();

            // ensure the delete was successful
            const exists = await Dexie.exists(this.name);
            if (exists) {
              throw new Error('Failed to remove mock backend database.');
            }
          })
      })
      .then(() => super.open());
  }
}

andrejpavlovic avatar Mar 02 '21 19:03 andrejpavlovic

Heads up that it's a highly prioritized task to improve the version handling in Dexie (See #287 and #105), and I'll take your use case into account when doing that. Expect it to be improved within the next 2-3 months or so. Some of the improvements will be:

  1. Allow upgrade functions to access the database based on the old schema (run upgrader before old indexes and tables are deleted).
  2. No more need to keep the definitions of older versions in the code unless they have an upgrade() attached to them. Instead, Dexie should inspect the current schema and use that as the diff source instead of the specified one.
  3. Complain if there is a diff between the actual schema and the specified schema in case no upgrade was needed. Suggest increase of version number in the exception message.

Your use case is also relevant so, maybe it could be handled via some kind of instruction for that, such as:

db.version(3).recreate({
    projects: "++id,client,project,start,end"
});

Would a recreate alternative solve your case?

@dfahlander Do you have any plans to implement it in near future?

dmielewczyk avatar Aug 09 '21 07:08 dmielewczyk

@dfahlander Do you have any plans to implement it in near future?

The quote refers to other already resolved issues. One of them was not marked as resolved but I closed it now as it has been possible for a great while to do all the things that the quote refers to: version N: access a table and do things like copying its content into a new table, version N+1: delete the table that was read from. This all enables the possibility to do things like changing foreign keys etc.

What this issue is about, is some sugar for dropping all tables and not having to explicitly specify the tables to drop (setting their value to null). I don't see it as prioritized as the referred issues.

dfahlander avatar Aug 09 '21 09:08 dfahlander

@dfahlander Do you have any plans to implement it in near future?

The quote refers to other already resolved issues. One of them was not marked as resolved but I closed it now as it has been possible for a great while to do all the things that the quote refers to: version N: access a table and do things like copying its content into a new table, version N+1: delete the table that was read from. This all enables the possibility to do things like changing foreign keys etc.

What this issue is about, is some sugar for dropping all tables and not having to explicitly specify the tables to drop (setting their value to null). I don't see it as prioritized as the referred issues.

Hi, thanks for the work on this library. Is there any documentation on how to make the ask from the original commenter easier achievable? I found https://dexie.org/docs/Tutorial/Design#database-versioning but I'm not finding the information I'm looking for.

I wasn't able to make this work: https://github.com/dexie/Dexie.js/issues/349#issuecomment-255072346 Either the open() call seems to be left hanging or I got a Dexie.delete('myDb') was blocked warning.

I am simply trying to clear the whole database on version upgrade. Right now, it's because I've changed a primary index (project is still under heavy development) but in most cases, since the data stored in this DB is non-critical, flushing is preferable to trying to migrate.

I'm on Dexie 3.0.4. Thanks

fgarit-te avatar Aug 23 '22 09:08 fgarit-te

Given that table: null deletes tables isn't this the final correct solution:

this.version(1).stores({
	foo: 'id',
	bar: 'id',
});

// Instead of adding one version, add two at once, one deletes the tables, another recreates them:

this.version(2).stores({
	foo: null,
	bar: null,
});

this.version(3).stores({
	foo: 'foo',
	bar: 'bar',
});

I went through the open hacks and such before I finally noticed the null feature and it works flawlessly for me.

futpib avatar May 07 '23 12:05 futpib