Exemplo n.º 1
0
        public static RC RunVacuum(ref string errMsg, Context ctx)
        {
            if (ctx.AutoCommit == 0)
            {
                C._setstring(ref errMsg, ctx, "cannot VACUUM from within a transaction");
                return(RC.ERROR);
            }
            if (ctx.ActiveVdbeCnt > 1)
            {
                C._setstring(ref errMsg, ctx, "cannot VACUUM - SQL statements in progress");
                return(RC.ERROR);
            }

            // Save the current value of the database flags so that it can be restored before returning. Then set the writable-schema flag, and
            // disable CHECK and foreign key constraints.
            int saved_Flags        = ctx.Flags;              // Saved value of the db.flags
            int saved_Changes      = ctx.Changes;            // Saved value of db.nChange
            int saved_TotalChanges = ctx.TotalChanges;       // Saved value of db.nTotalChange
            Action <object, string> saved_Trace = ctx.Trace; // Saved db->xTrace

            ctx.Flags |= Context.FLAG.WriteSchema | Context.FLAG.IgnoreChecks | Context.FLAG.PreferBuiltin;
            ctx.Flags &= ~(Context.FLAG.ForeignKeys | Context.FLAG.ReverseOrder);
            ctx.Trace  = null;

            Btree main    = ctx.DBs[0].Bt;                         // The database being vacuumed
            bool  isMemDb = sqlite3PagerIsMemdb(main.get_Pager()); // True if vacuuming a :memory: database

            // Attach the temporary database as 'vacuum_db'. The synchronous pragma can be set to 'off' for this file, as it is not recovered if a crash
            // occurs anyway. The integrity of the database is maintained by a (possibly synchronous) transaction opened on the main database before
            // sqlite3BtreeCopyFile() is called.
            //
            // An optimisation would be to use a non-journaled pager. (Later:) I tried setting "PRAGMA vacuum_db.journal_mode=OFF" but
            // that actually made the VACUUM run slower.  Very little journalling actually occurs when doing a vacuum since the vacuum_db is initially
            // empty.  Only the journal header is written.  Apparently it takes more time to parse and run the PRAGMA to turn journalling off than it does
            // to write the journal header file.
            int    db  = ctx.DBs.length;                                                                             // Number of attached databases
            string sql = (sqlite3TempInMemory(ctx) ? "ATTACH ':memory:' AS vacuum_db;" : "ATTACH '' AS vacuum_db;"); // SQL statements
            RC     rc  = ExecSql(ctx, errMsg, sql);                                                                  // Return code from service routines

            Context.DB dbObj = null;                                                                                 // Database to detach at end of vacuum
            if (ctx.DBs.length > db)
            {
                dbObj = ctx.DBs[ctx.DBs.length - 1];
                Debug.Assert(dbObj.Name == "vacuum_db");
            }
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }
            Btree temp = ctx.DBs[ctx.DBs.length - 1].Bt; // The temporary database we vacuum into

            // The call to execSql() to attach the temp database has left the file locked (as there was more than one active statement when the transaction
            // to read the schema was concluded. Unlock it here so that this doesn't cause problems for the call to BtreeSetPageSize() below.
            temp.Commit();

            int res = main.GetReserve(); // Bytes of reserved space at the end of each page

            // A VACUUM cannot change the pagesize of an encrypted database.
#if HAS_CODEC
            if (ctx.NextPagesize != 0)
            {
                int    nKey;
                string zKey;
                sqlite3CodecGetKey(ctx, 0, out zKey, out nKey);
                if (nKey != 0)
                {
                    ctx.NextPagesize = 0;
                }
            }
#endif
            rc = ExecSql(ctx, errMsg, "PRAGMA vacuum_db.synchronous=OFF");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }

            // Begin a transaction and take an exclusive lock on the main database file. This is done before the sqlite3BtreeGetPageSize(main) call below,
            // to ensure that we do not try to change the page-size on a WAL database.
            rc = ExecSql(ctx, errMsg, "BEGIN;");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }
            rc = main.BeginTrans(2);
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }

            // Do not attempt to change the page size for a WAL database
            if (main.get_Pager().GetJournalMode() == IPager.JOURNALMODE.WAL)
            {
                ctx.NextPagesize = 0;
            }

            if (temp.SetPageSize(main.GetPageSize(), res, 0) != 0 || (!isMemDb && temp.SetPageSize(ctx->NextPagesize, res, false) != 0) || C._NEVER(ctx.MallocFailed))
            {
                rc = RC.NOMEM;
                goto end_of_vacuum;
            }

#if !OMIT_AUTOVACUUM
            temp.SetAutoVacuum(ctx.NextAutovac >= 0 ? ctx.NextAutovac : main.GetAutoVacuum());
#endif

            // Query the schema of the main database. Create a mirror schema in the temporary database.
            rc = ExecExecSql(ctx, errMsg,
                             "SELECT 'CREATE TABLE vacuum_db.' || substr(sql,14) " +
                             "  FROM sqlite_master WHERE type='table' AND name!='sqlite_sequence'" +
                             "   AND rootpage>0"
                             );
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }
            rc = ExecExecSql(ctx, errMsg,
                             "SELECT 'CREATE INDEX vacuum_db.' || substr(sql,14)" +
                             "  FROM sqlite_master WHERE sql LIKE 'CREATE INDEX %' ");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }
            rc = ExecExecSql(ctx, errMsg,
                             "SELECT 'CREATE UNIQUE INDEX vacuum_db.' || substr(sql,21) " +
                             "  FROM sqlite_master WHERE sql LIKE 'CREATE UNIQUE INDEX %'");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }

            // Loop through the tables in the main database. For each, do an "INSERT INTO vacuum_db.xxx SELECT * FROM main.xxx;" to copy
            // the contents to the temporary database.
            rc = ExecExecSql(ctx, errMsg,
                             "SELECT 'INSERT INTO vacuum_db.' || quote(name) " +
                             "|| ' SELECT * FROM main.' || quote(name) || ';'" +
                             "FROM main.sqlite_master " +
                             "WHERE type = 'table' AND name!='sqlite_sequence' " +
                             "  AND rootpage>0");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }

            // Copy over the sequence table
            rc = ExecExecSql(ctx, errMsg,
                             "SELECT 'DELETE FROM vacuum_db.' || quote(name) || ';' " +
                             "FROM vacuum_db.sqlite_master WHERE name='sqlite_sequence' ");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }
            rc = ExecExecSql(ctx, errMsg,
                             "SELECT 'INSERT INTO vacuum_db.' || quote(name) " +
                             "|| ' SELECT * FROM main.' || quote(name) || ';' " +
                             "FROM vacuum_db.sqlite_master WHERE name=='sqlite_sequence';");
            if (rc != RC.OK)
            {
                goto end_of_vacuum;
            }

            // Copy the triggers, views, and virtual tables from the main database over to the temporary database.  None of these objects has any
            // associated storage, so all we have to do is copy their entries from the SQLITE_MASTER table.
            rc = ExecSql(ctx, errMsg,
                         "INSERT INTO vacuum_db.sqlite_master " +
                         "  SELECT type, name, tbl_name, rootpage, sql" +
                         "    FROM main.sqlite_master" +
                         "   WHERE type='view' OR type='trigger'" +
                         "      OR (type='table' AND rootpage=0)");
            if (rc != 0)
            {
                goto end_of_vacuum;
            }

            // At this point, there is a write transaction open on both the vacuum database and the main database. Assuming no error occurs,
            // both transactions are closed by this block - the main database transaction by sqlite3BtreeCopyFile() and the other by an explicit
            // call to sqlite3BtreeCommit().
            {
                // This array determines which meta meta values are preserved in the vacuum.  Even entries are the meta value number and odd entries
                // are an increment to apply to the meta value after the vacuum. The increment is used to increase the schema cookie so that other
                // connections to the same database will know to reread the schema.
                Debug.Assert(Btree.IsInTrans(temp));
                Debug.Assert(Btree.IsInTrans(main));

                // Copy Btree meta values
                for (int i = 0; i < _runVacuum_copy.Length; i += 2)
                {
                    // GetMeta() and UpdateMeta() cannot fail in this context because we already have page 1 loaded into cache and marked dirty.
                    uint meta = 0;
                    main.GetMeta(_runVacuum_copy[i], ref meta);
                    rc = temp.UpdateMeta(temp, _runVacuum_copy[i], (uint)(meta + _runVacuum_copy[i + 1]));
                    if (C._NEVER(rc != RC.OK))
                    {
                        goto end_of_vacuum;
                    }
                }

                rc = sqlite3BtreeCopyFile(main, temp);
                if (rc != RC.OK)
                {
                    goto end_of_vacuum;
                }
                rc = temp.Commit();
                if (rc != RC.OK)
                {
                    goto end_of_vacuum;
                }
#if !SQLITE_OMIT_AUTOVACUUM
                main.SetAutoVacuum(temp.GetAutoVacuum());
#endif
            }

            Debug.Assert(rc == RC.OK);
            rc = main.SetPageSize(temp.GetPageSize(temp), res, 1);

end_of_vacuum:
            // Restore the original value of ctx->flags
            ctx.Flags        = saved_Flags;
            ctx.Changes      = saved_Changes;
            ctx.TotalChanges = saved_TotalChanges;
            ctx.Trace        = saved_Trace;
            sqlite3BtreeSetPageSize(main, -1, -1, 1);

            // Currently there is an SQL level transaction open on the vacuum database. No locks are held on any other files (since the main file
            // was committed at the btree level). So it safe to end the transaction by manually setting the autoCommit flag to true and detaching the
            // vacuum database. The vacuum_db journal file is deleted when the pager is closed by the DETACH.
            ctx.AutoCommit = 1;

            if (dbObj != null)
            {
                dbObj.Bt.Close();
                dbObj.Bt     = null;
                dbObj.Schema = null;
            }

            // This both clears the schemas and reduces the size of the db->aDb[] array.
            sqlite3ResetInternalSchema(ctx, -1);

            return(rc);
        }