예제 #1
0
  /**
   * Set up a raw page so that it looks like a database page holding no entries.
   *
   * @param sqlJetBtree
   * @param flags
   * @throws SqlJetException
   */
  void zeroPage(int flags) throws SqlJetException {
    ISqlJetMemoryPointer data = aData;
    byte hdr = hdrOffset;
    int first;

    assert (pDbPage.getPageNumber() == pgno);
    assert (pDbPage.getExtra() == this);
    assert (pDbPage.getData().getBuffer() == data.getBuffer());
    assert (pBt.mutex.held());

    SqlJetUtility.putUnsignedByte(data, hdr, (short) flags);
    first = hdr + 8 + 4 * ((flags & SqlJetMemPage.PTF_LEAF) == 0 ? 1 : 0);
    // SqlJetUtility.memset(data, hdr + 1, (byte) 0, 4);
    SqlJetUtility.put4byte(data, hdr + 1, 0);
    //
    SqlJetUtility.putUnsignedByte(data, hdr + 7, (short) 0);
    SqlJetUtility.put2byte(data, hdr + 5, pBt.usableSize);
    nFree = pBt.usableSize - first;
    decodeFlags(flags);
    hdrOffset = hdr;
    cellOffset = first;
    nOverflow = 0;
    assert (pBt.pageSize >= 512 && pBt.pageSize <= 32768);
    maskPage = pBt.pageSize - 1;
    nCell = 0;
    isInit = true;
  }
예제 #2
0
  /**
   * Initialize the auxiliary information for a disk block.
   *
   * <p>Return SQLITE_OK on success. If we see that the page does not contain a well-formed database
   * page, then return SQLITE_CORRUPT. Note that a return of SQLITE_OK does not guarantee that the
   * page is well-formed. It only shows that we failed to detect any corruption.
   */
  public void initPage() throws SqlJetException {

    assert (pBt != null);
    assert (pBt.mutex.held());
    assert (pgno == pDbPage.getPageNumber());
    assert (this == pDbPage.getExtra());
    assert (aData.getBuffer() == pDbPage.getData().getBuffer());

    if (!isInit) {
      int pc; /* Address of a freeblock within pPage->aData[] */
      byte hdr; /* Offset to beginning of page header */
      int usableSize; /* Amount of usable space on each page */
      int cellOffset; /* Offset from start of page to first cell pointer */
      int nFree; /* Number of unused bytes on the page */
      int top; /* First byte of the cell content area */

      hdr = hdrOffset;
      decodeFlags(SqlJetUtility.getUnsignedByte(aData, hdr));
      assert (pBt.pageSize >= 512 && pBt.pageSize <= 32768);
      maskPage = pBt.pageSize - 1;
      nOverflow = 0;
      usableSize = pBt.usableSize;
      this.cellOffset = cellOffset = hdr + 12 - 4 * (leaf ? 1 : 0);
      top = get2byte(aData, hdr + 5);
      nCell = get2byte(aData, hdr + 3);
      if (nCell > pBt.MX_CELL()) {
        /* To many cells for a single page. The page must be corrupt */
        throw new SqlJetException(SqlJetErrorCode.CORRUPT);
      }
      int iCellFirst = cellOffset + 2 * this.nCell;

      /* Compute the total free space on the page */
      pc = get2byte(aData, hdr + 1);
      nFree = SqlJetUtility.getUnsignedByte(aData, hdr + 7) + top; // - (cellOffset + 2 * nCell);
      while (pc > 0) {
        int next, size;
        if (pc > usableSize - 4) {
          /* Free block is off the page */
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        next = get2byte(aData, pc);
        size = get2byte(aData, pc + 2);
        if (next > 0 && next <= pc + size + 3) {
          /* Free blocks must be in accending order */
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        nFree += size;
        pc = next;
      }
      if (nFree > usableSize) {
        /* Free space cannot exceed total page size */
        throw new SqlJetException(SqlJetErrorCode.CORRUPT);
      }
      this.nFree = (nFree - iCellFirst);
      isInit = true;
    }
  }
예제 #3
0
  int cellSizePtr(ISqlJetMemoryPointer pCell) {
    // SqlJetBtreeCellInfo info = parseCellPtr(pCell);
    // return info.nSize;

    final SqlJetMemPage pPage = this;

    final ISqlJetMemoryPointer pIter = pCell.getMoved(pPage.childPtrSize);
    int[] nSize = {0};

    if (pPage.intKey) {
      if (pPage.hasData) {
        pIter.movePointer(SqlJetUtility.getVarint32(pIter, nSize));
      } else {
        nSize[0] = 0;
      }

      /*
       * pIter now points at the 64-bit integer key value, a variable
       * length* integer. The following block moves pIter to point at the
       * first byte* past the end of the key value.
       */
      int pEnd = pIter.getPointer() + 9;
      // while( (*pIter++)&0x80 && pIter<pEnd );
      while (pIter.getPointer() < pEnd) {
        int b = pIter.getByteUnsigned();
        pIter.movePointer(1);
        if ((b & 0x80) == 0) {
          break;
        }
      }
    } else {
      pIter.movePointer(SqlJetUtility.getVarint32(pIter, nSize));
    }

    if (nSize[0] > pPage.maxLocal) {
      int minLocal = pPage.minLocal;
      nSize[0] = minLocal + (nSize[0] - minLocal) % (pPage.pBt.usableSize - 4);
      if (nSize[0] > pPage.maxLocal) {
        nSize[0] = minLocal;
      }
      nSize[0] += 4;
    }
    nSize[0] += (pIter.getPointer() - pCell.getPointer());

    /* The minimum size of any cell is 4 bytes. */
    if (nSize[0] < 4) {
      nSize[0] = 4;
    }

    return nSize[0];
  }
예제 #4
0
 @Override
 public Object clone() throws CloneNotSupportedException {
   // TODO Auto-generated method stub
   final SqlJetMemPage clone = (SqlJetMemPage) super.clone();
   clone.aData = SqlJetUtility.allocatePtr(clone.pBt.pageSize);
   clone.aOvfl = aOvfl.clone();
   for (int i = 0; i < aOvfl.length; i++) {
     clone.aOvfl[i] = (_OvflCell) aOvfl[i].clone();
   }
   return clone;
 }
예제 #5
0
  /**
   * Defragment the page given. All Cells are moved to the end of the page and all free space is
   * collected into one big FreeBlk that occurs in between the header and cell pointer array and the
   * cell content area.
   *
   * @throws SqlJetException
   */
  private void defragmentPage() throws SqlJetException {

    final SqlJetMemPage pPage = this;

    int i; /* Loop counter */
    int pc; /* Address of a i-th cell */
    int hdr; /* Offset to the page header */
    int size; /* Size of a cell */
    int usableSize; /* Number of usable bytes on a page */
    int cellOffset; /* Offset to the cell pointer array */
    int cbrk; /* Offset to the cell content area */
    int nCell; /* Number of cells on the page */
    ISqlJetMemoryPointer data; /* The page data */
    ISqlJetMemoryPointer temp; /* Temp area for cell content */
    int iCellFirst; /* First allowable cell index */
    int iCellLast; /* Last possible cell index */

    assert (pPage.pDbPage.isWriteable());
    assert (pPage.pBt != null);
    assert (pPage.pBt.usableSize <= ISqlJetLimits.SQLJET_MAX_PAGE_SIZE);
    assert (pPage.nOverflow == 0);
    assert (pPage.pBt.mutex.held());
    temp = pPage.pBt.pPager.getTempSpace();
    data = pPage.aData;
    hdr = pPage.hdrOffset;
    cellOffset = pPage.cellOffset;
    nCell = pPage.nCell;
    assert (nCell == get2byte(data, hdr + 3));
    usableSize = pPage.pBt.usableSize;
    cbrk = get2byte(data, hdr + 5);
    memcpy(temp, cbrk, data, cbrk, usableSize - cbrk);
    cbrk = usableSize;
    iCellFirst = cellOffset + 2 * nCell;
    iCellLast = usableSize - 4;
    for (i = 0; i < nCell; i++) {
      final ISqlJetMemoryPointer pAddr =
          data.getBuffer().getPointer(cellOffset + i * 2); /* The i-th cell pointer */
      pc = get2byte(pAddr);
      if (pc < iCellFirst || pc > iCellLast) {
        throw new SqlJetException(SqlJetErrorCode.CORRUPT);
      }
      assert (pc >= iCellFirst && pc <= iCellLast);
      size = pPage.cellSizePtr(temp.getBuffer().getPointer(pc));
      cbrk -= size;
      if (cbrk < iCellFirst || pc + size > usableSize) {
        throw new SqlJetException(SqlJetErrorCode.CORRUPT);
      }
      assert (cbrk + size <= usableSize && cbrk >= iCellFirst);
      memcpy(data, cbrk, temp, pc, size);
      put2byte(pAddr, cbrk);
    }
    assert (cbrk >= iCellFirst);
    put2byte(data, hdr + 5, cbrk);
    SqlJetUtility.putUnsignedByte(data, hdr + 1, (byte) 0);
    SqlJetUtility.putUnsignedByte(data, hdr + 2, (byte) 0);
    SqlJetUtility.putUnsignedByte(data, hdr + 7, (byte) 0);
    memset(data, iCellFirst, (byte) 0, cbrk - iCellFirst);
    assert (pPage.pDbPage.isWriteable());
    if (cbrk - iCellFirst != pPage.nFree) {
      throw new SqlJetException(SqlJetErrorCode.CORRUPT);
    }
  }
예제 #6
0
  /**
   * Allocate nByte bytes of space on a page.
   *
   * <p>Return the index into pPage->aData[] of the first byte of the new allocation. The caller
   * guarantees that there is enough space. This routine will never fail.
   *
   * <p>If the page contains nBytes of free space but does not contain nBytes of contiguous free
   * space, then this routine automatically calls defragementPage() to consolidate all free space
   * before allocating the new chunk.
   *
   * @param nByte
   * @return
   * @throws SqlJetException
   */
  private int allocateSpace(int nByte) throws SqlJetException {

    final SqlJetMemPage pPage = this;

    int addr, pc, hdr;
    int size;
    int nFrag;
    ISqlJetMemoryPointer data;

    data = pPage.aData;
    assert (pPage.pDbPage.isWriteable());
    assert (pPage.pBt != null);
    assert (pPage.pBt.mutex.held());

    assert (nByte >= 0); /* Minimum cell size is 4 */
    assert (pPage.nFree >= nByte);
    assert (pPage.nOverflow == 0);
    int usableSize = pPage.pBt.usableSize;
    assert (nByte < usableSize - 8);

    hdr = pPage.hdrOffset;

    nFrag = SqlJetUtility.getUnsignedByte(data, hdr + 7);
    assert (pPage.cellOffset == hdr + 12 - 4 * (pPage.leaf ? 1 : 0));
    int gap = pPage.cellOffset + 2 * pPage.nCell;
    int top = get2byte(data.getMoved(hdr + 5));
    if (gap > top) {
      throw new SqlJetException(SqlJetErrorCode.CORRUPT);
    }
    if (nFrag >= 60) {
      pPage.defragmentPage();
      top = get2byte(data.getMoved(hdr + 5));
    } else if (gap + 2 <= top) {
      addr = hdr + 1;
      while ((pc = get2byte(data, addr)) > 0) {
        if (pc > usableSize - 4 || pc < addr + 4) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        size = get2byte(data, pc + 2);
        if (size >= nByte) {
          int x = size - nByte;
          if (x < 4) {
            memcpy(data, addr, data, pc, 2);
            SqlJetUtility.putUnsignedByte(data, hdr + 7, nFrag + x);
          } else if (size + pc > usableSize) {
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
          } else {
            put2byte(data, pc + 2, x);
          }
          return pc + x;
        }
        addr = pc;
      }
    }
    if (gap + 2 + nByte > top) {
      pPage.defragmentPage();
      top = get2byte(data, hdr + 5);
      assert (gap + nByte <= top);
    }
    top -= nByte;
    put2byte(data, hdr + 5, top);
    assert (top + nByte <= pPage.pBt.usableSize);
    return top;
  }
예제 #7
0
  /**
   * Insert a new cell on pPage at cell index "i". pCell points to the content of the cell.
   *
   * <p>If the cell content will fit on the page, then put it there. If it will not fit, then make a
   * copy of the cell content into pTemp if pTemp is not null. Regardless of pTemp, allocate a new
   * entry in pPage->aOvfl[] and make it point to the cell content (either in pTemp or the original
   * pCell) and also record its index. Allocating a new entry in pPage->aCell[] implies that
   * pPage->nOverflow is incremented.
   *
   * <p>If nSkip is non-zero, then do not copy the first nSkip bytes of the cell. The caller will
   * overwrite them after this function returns. If nSkip is non-zero, then pCell may not point to
   * an invalid memory location (but pCell+nSkip is always valid).
   *
   * @param i New cell becomes the i-th cell of the page
   * @param pCell Content of the new cell
   * @param sz Bytes of content in pCell
   * @param pTemp Temp storage space for pCell, if needed
   * @param nSkip Do not write the first nSkip bytes of the cell
   * @throws SqlJetException
   */
  public void insertCell(
      int i, ISqlJetMemoryPointer pCell, int sz, ISqlJetMemoryPointer pTemp, int iChild)
      throws SqlJetException {

    int nSkip = (iChild > 0 ? 4 : 0);

    final SqlJetMemPage pPage = this;

    int idx; /* Where to write new cell content in data[] */
    int j; /* Loop counter */
    int top; /* First byte of content for any cell in data[] */
    int end; /* First byte past the last cell pointer in data[] */
    int ins; /* Index in data[] where new cell pointer is inserted */
    int hdr; /* Offset into data[] of the page header */
    int cellOffset; /* Address of first cell pointer in data[] */
    ISqlJetMemoryPointer data; /* The content of the whole page */

    assert (i >= 0 && i <= pPage.nCell + pPage.nOverflow);
    assert (pPage.nCell <= pPage.pBt.MX_CELL() && pPage.pBt.MX_CELL() <= 5460);
    assert (pPage.nOverflow <= pPage.aOvfl.length);
    assert (sz == pPage.cellSizePtr(pCell) || (sz == 8 && iChild > 0));
    assert (pPage.pBt.mutex.held());
    if (pPage.nOverflow != 0 || sz + 2 > pPage.nFree) {
      if (pTemp != null) {
        memcpy(pTemp, nSkip, pCell, nSkip, sz - nSkip);
        pCell = pTemp;
      }
      if (iChild > 0) {
        put4byte(pCell, iChild);
      }
      j = pPage.nOverflow++;
      // assert( j<(int)(sizeof(pPage.aOvfl)/sizeof(pPage.aOvfl[0])) );
      pPage.aOvfl[j].pCell = pCell;
      pPage.aOvfl[j].idx = i;
    } else {
      pPage.pDbPage.write();
      assert (pPage.pDbPage.isWriteable());
      data = pPage.aData;
      hdr = pPage.hdrOffset;
      top = get2byte(data, hdr + 5);
      cellOffset = pPage.cellOffset;
      end = cellOffset + 2 * pPage.nCell + 2;
      ins = cellOffset + 2 * i;
      if (end > top - sz) {
        pPage.defragmentPage();
        top = get2byte(data, hdr + 5);
        assert (end + sz <= top);
      }
      idx = pPage.allocateSpace(sz);
      assert (idx > 0);
      assert (end <= get2byte(data, hdr + 5));
      if (idx + sz > pPage.pBt.usableSize) {
        throw new SqlJetException(SqlJetErrorCode.CORRUPT);
      }
      pPage.nCell++;
      pPage.nFree -= (2 + sz);
      memcpy(data, idx + nSkip, pCell, nSkip, sz - nSkip);
      if (iChild > 0) {
        put4byte(data, idx, iChild);
      }
      for (j = end - 2; j > ins; j -= 2) {
        SqlJetUtility.putUnsignedByte(data, j, SqlJetUtility.getUnsignedByte(data, j - 2));
        SqlJetUtility.putUnsignedByte(data, j + 1, SqlJetUtility.getUnsignedByte(data, j - 1));
      }
      put2byte(data, ins, idx);
      put2byte(data, hdr + 3, pPage.nCell);
      if (pPage.pBt.autoVacuum) {
        /*
         * The cell may contain a pointer to an overflow page. If so,
         * write* the entry for the overflow page into the pointer map.
         */
        SqlJetBtreeCellInfo info = pPage.parseCellPtr(pCell);
        assert ((info.nData + (pPage.intKey ? 0 : info.nKey)) == info.nPayload);
        if ((info.nData + (pPage.intKey ? 0 : info.nKey)) > info.nLocal) {
          int pgnoOvfl = get4byte(pCell, info.iOverflow);
          pPage.pBt.ptrmapPut(pgnoOvfl, SqlJetBtreeShared.PTRMAP_OVERFLOW1, pPage.pgno);
        }
      }
    }
  }
예제 #8
0
  /*
   * * Return a section of the pPage->aData to the freelist.* The first byte
   * of the new free block is pPage->aDisk[start]* and the size of the block
   * is "size" bytes.** Most of the effort here is involved in coalesing
   * adjacent* free blocks into a single big free block.
   */
  private void freeSpace(int start, int size) throws SqlJetException {

    SqlJetMemPage pPage = this;

    int addr, pbegin, hdr;
    ISqlJetMemoryPointer data = pPage.aData;

    assert (pPage.pBt != null);
    assert (start >= pPage.hdrOffset + 6 + (pPage.leaf ? 0 : 4));
    assert ((start + size) <= pPage.pBt.usableSize);
    assert (pPage.pBt.mutex.held());
    assert (size >= 0); /* Minimum cell size is 4 */

    if (ISqlJetConfig.SECURE_DELETE) {
      /*
       * Overwrite deleted information with zeros when the SECURE_DELETE*
       * option is enabled at compile-time
       */
      memset(data, start, (byte) 0, size);
    }

    /* Add the space back into the linked list of freeblocks */
    hdr = pPage.hdrOffset;
    addr = hdr + 1;
    while ((pbegin = get2byte(data, addr)) < start && pbegin > 0) {
      assert (pbegin <= pPage.pBt.usableSize - 4);
      if (pbegin <= addr) {
        throw new SqlJetException(SqlJetErrorCode.CORRUPT);
      }
      addr = pbegin;
    }
    if (pbegin > pPage.pBt.usableSize - 4) {
      throw new SqlJetException(SqlJetErrorCode.CORRUPT);
    }
    assert (pbegin > addr || pbegin == 0);
    put2byte(data, addr, start);
    put2byte(data, start, pbegin);
    put2byte(data, start + 2, size);
    pPage.nFree += size;

    /* Coalesce adjacent free blocks */
    addr = pPage.hdrOffset + 1;
    while ((pbegin = get2byte(data, addr)) > 0) {
      int pnext, psize, x;
      assert (pbegin > addr);
      assert (pbegin <= pPage.pBt.usableSize - 4);
      pnext = get2byte(data, pbegin);
      psize = get2byte(data, pbegin + 2);
      if (pbegin + psize + 3 >= pnext && pnext > 0) {
        int frag = pnext - (pbegin + psize);
        if ((frag < 0) || (frag > (int) SqlJetUtility.getUnsignedByte(data, pPage.hdrOffset + 7))) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        SqlJetUtility.putUnsignedByte(
            data,
            pPage.hdrOffset + 7,
            (byte) (SqlJetUtility.getUnsignedByte(data, pPage.hdrOffset + 7) - (byte) frag));
        x = get2byte(data, pnext);
        put2byte(data, pbegin, x);
        x = pnext + get2byte(data, pnext + 2) - pbegin;
        put2byte(data, pbegin + 2, x);
      } else {
        addr = pbegin;
      }
    }

    /* If the cell content area begins with a freeblock, remove it. */
    if (SqlJetUtility.getUnsignedByte(data, hdr + 1) == SqlJetUtility.getUnsignedByte(data, hdr + 5)
        && SqlJetUtility.getUnsignedByte(data, hdr + 2)
            == SqlJetUtility.getUnsignedByte(data, hdr + 6)) {
      int top;
      pbegin = get2byte(data, hdr + 1);
      memcpy(data, hdr + 1, data, pbegin, 2);
      top = get2byte(data, hdr + 5) + get2byte(data, pbegin + 2);
      put2byte(data, hdr + 5, top);
    }
    assert (pPage.pDbPage.isWriteable());
  }
예제 #9
0
/**
 * @author TMate Software Ltd.
 * @author Sergey Scherbina ([email protected])
 * @author Dmitry Stadnik ([email protected])
 */
public class SqlJetSchema implements ISqlJetSchema {

  private static final String NAME_RESERVED = "Name '%s' is reserved to internal use";

  private static String AUTOINDEX = "sqlite_autoindex_%s_%d";

  private static final String CANT_DELETE_IMPLICIT_INDEX = "Can't delete implicit index \"%s\"";

  private static final String CREATE_TABLE_SQLITE_SEQUENCE =
      "CREATE TABLE sqlite_sequence(name,seq)";

  private static final String SQLITE_SEQUENCE = "SQLITE_SEQUENCE";

  private static final Set<SqlJetBtreeTableCreateFlags> BTREE_CREATE_TABLE_FLAGS =
      SqlJetUtility.of(SqlJetBtreeTableCreateFlags.INTKEY, SqlJetBtreeTableCreateFlags.LEAFDATA);

  private static final Set<SqlJetBtreeTableCreateFlags> BTREE_CREATE_INDEX_FLAGS =
      SqlJetUtility.of(SqlJetBtreeTableCreateFlags.ZERODATA);

  private static final String TABLE_TYPE = "table";
  private static final String INDEX_TYPE = "index";

  private final ISqlJetDbHandle db;
  private final ISqlJetBtree btree;

  private final Map<String, ISqlJetTableDef> tableDefs =
      new TreeMap<String, ISqlJetTableDef>(String.CASE_INSENSITIVE_ORDER);
  private final Map<String, ISqlJetIndexDef> indexDefs =
      new TreeMap<String, ISqlJetIndexDef>(String.CASE_INSENSITIVE_ORDER);
  private Map<String, ISqlJetVirtualTableDef> virtualTableDefs =
      new TreeMap<String, ISqlJetVirtualTableDef>(String.CASE_INSENSITIVE_ORDER);

  private enum SqlJetSchemaObjectType {
    TABLE {
      @Override
      public Object getName() {
        return "table";
      }
    },

    INDEX {
      @Override
      public Object getName() {
        return "index";
      }
    },

    VIRTUAL_TABLE {
      @Override
      public Object getName() {
        return "virtual table";
      }
    };

    public abstract Object getName();
  }

  public SqlJetSchema(ISqlJetDbHandle db, ISqlJetBtree btree) throws SqlJetException {
    this.db = db;
    this.btree = btree;
    init();
  }

  private ISqlJetBtreeSchemaTable openSchemaTable(boolean write) throws SqlJetException {
    return new SqlJetBtreeSchemaTable(btree, write);
  }

  private void init() throws SqlJetException {
    if (db.getOptions().getSchemaVersion() == 0) return;
    final ISqlJetBtreeSchemaTable table = openSchemaTable(false);
    try {
      table.lock();
      try {
        readShema(table);
      } finally {
        table.unlock();
      }
    } finally {
      table.close();
    }
  }

  public ISqlJetDbHandle getDb() {
    return db;
  }

  public ISqlJetBtree getBtree() {
    return btree;
  }

  public Set<String> getTableNames() throws SqlJetException {
    db.getMutex().enter();
    try {
      final Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
      s.addAll(tableDefs.keySet());
      return s;
    } finally {
      db.getMutex().leave();
    }
  }

  public ISqlJetTableDef getTable(String name) throws SqlJetException {
    db.getMutex().enter();
    try {
      return tableDefs.get(name);
    } finally {
      db.getMutex().leave();
    }
  }

  public Set<String> getIndexNames() throws SqlJetException {
    db.getMutex().enter();
    try {
      final Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
      s.addAll(indexDefs.keySet());
      return s;
    } finally {
      db.getMutex().leave();
    }
  }

  public ISqlJetIndexDef getIndex(String name) throws SqlJetException {
    db.getMutex().enter();
    try {
      return indexDefs.get(name);
    } finally {
      db.getMutex().leave();
    }
  }

  public Set<ISqlJetIndexDef> getIndexes(String tableName) throws SqlJetException {
    db.getMutex().enter();
    try {
      Set<ISqlJetIndexDef> result = new HashSet<ISqlJetIndexDef>();
      for (ISqlJetIndexDef index : indexDefs.values()) {
        if (index.getTableName().equals(tableName)) {
          result.add(index);
        }
      }
      return Collections.unmodifiableSet(result);
    } finally {
      db.getMutex().leave();
    }
  }

  public Set<String> getVirtualTableNames() throws SqlJetException {
    db.getMutex().enter();
    try {
      final Set<String> s = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
      s.addAll(virtualTableDefs.keySet());
      return s;
    } finally {
      db.getMutex().leave();
    }
  }

  public ISqlJetVirtualTableDef getVirtualTable(String name) throws SqlJetException {
    db.getMutex().enter();
    try {
      return virtualTableDefs.get(name);
    } finally {
      db.getMutex().leave();
    }
  }

  private void readShema(ISqlJetBtreeSchemaTable table) throws SqlJetException {
    for (table.first(); !table.eof(); table.next()) {
      final String type = table.getTypeField();
      if (null == type) {
        continue;
      }
      final String name = table.getNameField();
      if (null == name) {
        continue;
      }
      final int page = table.getPageField();

      if (TABLE_TYPE.equals(type)) {
        String sql = table.getSqlField();
        // System.err.println(sql);
        final CommonTree ast = (CommonTree) parseTable(sql).getTree();
        if (!isCreateVirtualTable(ast)) {
          final SqlJetTableDef tableDef = new SqlJetTableDef(ast, page);
          if (!name.equals(tableDef.getName())) {
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
          }
          tableDef.setRowId(table.getRowId());
          tableDefs.put(name, tableDef);
        } else {
          final SqlJetVirtualTableDef virtualTableDef = new SqlJetVirtualTableDef(ast, page);
          if (!name.equals(virtualTableDef.getTableName())) {
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
          }
          virtualTableDef.setRowId(table.getRowId());
          virtualTableDefs.put(name, virtualTableDef);
        }
      } else if (INDEX_TYPE.equals(type)) {
        final String tableName = table.getTableField();
        final String sql = table.getSqlField();
        if (null != sql) {
          // System.err.println(sql);
          final CommonTree ast = (CommonTree) parseIndex(sql).getTree();
          final SqlJetIndexDef indexDef = new SqlJetIndexDef(ast, page);
          if (!name.equals(indexDef.getName())) {
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
          }
          if (!tableName.equals(indexDef.getTableName())) {
            throw new SqlJetException(SqlJetErrorCode.CORRUPT);
          }
          indexDef.setRowId(table.getRowId());
          indexDefs.put(name, indexDef);
        } else {
          SqlJetBaseIndexDef indexDef = new SqlJetBaseIndexDef(name, tableName, page);
          indexDef.setRowId(table.getRowId());
          indexDefs.put(name, indexDef);
        }
      }
    }
    bindIndexes();
  }

  /** */
  private void bindIndexes() {
    for (ISqlJetIndexDef indexDef : indexDefs.values()) {
      if (indexDef instanceof SqlJetIndexDef) {
        SqlJetIndexDef i = (SqlJetIndexDef) indexDef;
        final String tableName = i.getTableName();
        final ISqlJetTableDef tableDef = tableDefs.get(tableName);
        if (tableDef != null) {
          i.bindColumns(tableDef);
        }
      }
    }
  }

  /**
   * @param ast
   * @return
   */
  private boolean isCreateVirtualTable(CommonTree ast) {
    final CommonTree optionsNode = (CommonTree) ast.getChild(0);
    for (int i = 0; i < optionsNode.getChildCount(); i++) {
      CommonTree optionNode = (CommonTree) optionsNode.getChild(i);
      if ("virtual".equalsIgnoreCase(optionNode.getText())) {
        return true;
      }
    }
    return false;
  }

  private RuleReturnScope parseTable(String sql) throws SqlJetException {
    try {
      CharStream chars = new ANTLRStringStream(sql);
      SqlLexer lexer = new SqlLexer(chars);
      CommonTokenStream tokens = new CommonTokenStream(lexer);
      SqlParser parser = new SqlParser(tokens);
      return parser.schema_create_table_stmt();
    } catch (RecognitionException re) {
      throw new SqlJetException(SqlJetErrorCode.ERROR, "Invalid sql statement: " + sql);
    }
  }

  private ParserRuleReturnScope parseIndex(String sql) throws SqlJetException {
    try {
      CharStream chars = new ANTLRStringStream(sql);
      SqlLexer lexer = new SqlLexer(chars);
      CommonTokenStream tokens = new CommonTokenStream(lexer);
      SqlParser parser = new SqlParser(tokens);
      return parser.create_index_stmt();
    } catch (RecognitionException re) {
      throw new SqlJetException(SqlJetErrorCode.ERROR, "Invalid sql statement: " + sql);
    }
  }

  @Override
  public String toString() {
    db.getMutex().enter();
    try {
      StringBuffer buffer = new StringBuffer();
      buffer.append("Tables:\n");
      for (ISqlJetTableDef tableDef : tableDefs.values()) {
        buffer.append(tableDef.toString());
        buffer.append('\n');
      }
      buffer.append("Indexes:\n");
      for (ISqlJetIndexDef indexDef : indexDefs.values()) {
        buffer.append(indexDef.toString());
        buffer.append('\n');
      }
      return buffer.toString();
    } finally {
      db.getMutex().leave();
    }
  }

  public ISqlJetTableDef createTable(String sql) throws SqlJetException {
    db.getMutex().enter();
    try {
      return createTableSafe(sql, false);
    } finally {
      db.getMutex().leave();
    }
  }

  private ISqlJetTableDef createTableSafe(String sql, boolean internal) throws SqlJetException {

    final RuleReturnScope parseTable = parseTable(sql);
    final CommonTree ast = (CommonTree) parseTable.getTree();

    if (isCreateVirtualTable(ast)) {
      throw new SqlJetException(SqlJetErrorCode.ERROR);
    }

    final SqlJetTableDef tableDef = new SqlJetTableDef(ast, 0);
    if (null == tableDef.getName()) throw new SqlJetException(SqlJetErrorCode.ERROR);
    final String tableName = tableDef.getName();
    if ("".equals(tableName)) throw new SqlJetException(SqlJetErrorCode.ERROR);

    if (!internal) {
      checkNameReserved(tableName);
    }

    if (tableDefs.containsKey(tableName)) {
      if (tableDef.isKeepExisting()) {
        return tableDefs.get(tableName);
      } else {
        throw new SqlJetException(
            SqlJetErrorCode.ERROR, "Table \"" + tableName + "\" exists already");
      }
    }

    checkNameConflict(SqlJetSchemaObjectType.TABLE, tableName);
    checkFieldNamesRepeatsConflict(tableDef.getName(), tableDef.getColumns());

    final List<ISqlJetColumnDef> columns = tableDef.getColumns();
    if (null == columns || 0 == columns.size()) throw new SqlJetException(SqlJetErrorCode.ERROR);

    final String createTableSql = getCreateTableSql(parseTable);

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);

    try {

      schemaTable.lock();

      try {

        db.getOptions().changeSchemaVersion();

        final int page = btree.createTable(BTREE_CREATE_TABLE_FLAGS);
        final long rowId =
            schemaTable.insertRecord(TABLE_TYPE, tableName, tableName, page, createTableSql);

        addConstraints(schemaTable, tableDef);

        tableDef.setPage(page);
        tableDef.setRowId(rowId);
        tableDefs.put(tableName, tableDef);
        return tableDef;

      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }
  }

  /**
   * @param tableDef
   * @throws SqlJetException
   */
  private void checkFieldNamesRepeatsConflict(String tableName, List<ISqlJetColumnDef> columns)
      throws SqlJetException {
    final Set<String> names = new HashSet<String>();
    for (ISqlJetColumnDef columnDef : columns) {
      final String name = columnDef.getName();
      if (names.contains(name)) {
        throw new SqlJetException(
            SqlJetErrorCode.ERROR,
            String.format(
                "Definition for table '%s' has conflict of repeating fields named '%s'",
                tableName, name));
      } else {
        names.add(name);
      }
    }
  }

  /**
   * @param parseTable
   * @return
   */
  private String getCreateTableSql(RuleReturnScope parseTable) {
    return String.format("CREATE TABLE %s", getCoreSQL(parseTable));
  }

  /**
   * @param parseIndex
   * @return
   */
  private String getCreateIndexSql(RuleReturnScope parseIndex) {
    return String.format("CREATE INDEX %s", getCoreSQL(parseIndex));
  }

  /**
   * @param parseIndex
   * @return
   */
  private String getCreateIndexUniqueSql(RuleReturnScope parseIndex) {
    return String.format("CREATE UNIQUE INDEX %s", getCoreSQL(parseIndex));
  }

  /**
   * @param parseTable
   * @return
   */
  private String getCreateVirtualTableSql(RuleReturnScope parseTable) {
    return String.format("CREATE VIRTUAL TABLE %s", getCoreSQL(parseTable));
  }

  private String getCoreSQL(RuleReturnScope parsedSQL) {
    final CommonTree ast = (CommonTree) parsedSQL.getTree();
    final CommonToken nameToken = (CommonToken) ((CommonTree) ast.getChild(1)).getToken();
    final CharStream inputStream = nameToken.getInputStream();
    final CommonToken stopToken = (CommonToken) parsedSQL.getStop();
    return inputStream.substring(nameToken.getStartIndex(), stopToken.getStopIndex());
  }

  /**
   * @param i
   * @return
   */
  private String generateAutoIndexName(String tableName, int i) {
    return String.format(AUTOINDEX, tableName, i);
  }

  /**
   * @param schemaTable
   * @param tableDef
   * @throws SqlJetException
   */
  private void addConstraints(ISqlJetBtreeSchemaTable schemaTable, final SqlJetTableDef tableDef)
      throws SqlJetException {

    final String tableName = tableDef.getName();
    final List<ISqlJetColumnDef> columns = tableDef.getColumns();
    int i = 0;

    for (final ISqlJetColumnDef column : columns) {
      final List<ISqlJetColumnConstraint> constraints = column.getConstraints();
      if (null == constraints) continue;
      for (final ISqlJetColumnConstraint constraint : constraints) {
        if (constraint instanceof ISqlJetColumnPrimaryKey) {
          final ISqlJetColumnPrimaryKey pk = (ISqlJetColumnPrimaryKey) constraint;
          if (!column.hasExactlyIntegerType()) {
            if (pk.isAutoincremented()) {
              throw new SqlJetException(
                  SqlJetErrorCode.ERROR,
                  "AUTOINCREMENT is allowed only for INTEGER PRIMARY KEY fields");
            }
            createAutoIndex(schemaTable, tableName, generateAutoIndexName(tableName, ++i));
          } else if (pk.isAutoincremented()) {
            checkSequenceTable();
          }
        } else if (constraint instanceof ISqlJetColumnUnique) {
          createAutoIndex(schemaTable, tableName, generateAutoIndexName(tableName, ++i));
        }
      }
    }

    final List<ISqlJetTableConstraint> constraints = tableDef.getConstraints();
    if (null != constraints) {
      for (final ISqlJetTableConstraint constraint : constraints) {
        if (constraint instanceof ISqlJetTablePrimaryKey) {
          boolean b = false;
          final ISqlJetTablePrimaryKey pk = (ISqlJetTablePrimaryKey) constraint;
          if (pk.getColumns().size() == 1) {
            final String n = pk.getColumns().get(0);
            final ISqlJetColumnDef c = tableDef.getColumn(n);
            b = c != null && c.hasExactlyIntegerType();
          }
          if (!b) {
            createAutoIndex(schemaTable, tableName, generateAutoIndexName(tableName, ++i));
          }
        } else if (constraint instanceof ISqlJetTableUnique) {
          createAutoIndex(schemaTable, tableName, generateAutoIndexName(tableName, ++i));
        }
      }
    }
  }

  /**
   * @param schemaTable
   * @throws SqlJetException
   */
  private void checkSequenceTable() throws SqlJetException {
    if (!tableDefs.containsKey(SQLITE_SEQUENCE)) {
      createTableSafe(CREATE_TABLE_SQLITE_SEQUENCE, true);
    }
  }

  /** @throws SqlJetException */
  public ISqlJetBtreeDataTable openSequenceTable() throws SqlJetException {
    if (tableDefs.containsKey(SQLITE_SEQUENCE)) {
      return new SqlJetBtreeDataTable(btree, SQLITE_SEQUENCE, true);
    } else {
      return null;
    }
  }

  /**
   * @param schemaTable
   * @param generateAutoIndexName
   * @throws SqlJetException
   */
  private SqlJetBaseIndexDef createAutoIndex(
      ISqlJetBtreeSchemaTable schemaTable, String tableName, String autoIndexName)
      throws SqlJetException {
    final int page = btree.createTable(BTREE_CREATE_INDEX_FLAGS);
    final SqlJetBaseIndexDef indexDef = new SqlJetBaseIndexDef(autoIndexName, tableName, page);
    indexDef.setRowId(schemaTable.insertRecord(INDEX_TYPE, autoIndexName, tableName, page, null));
    indexDefs.put(autoIndexName, indexDef);
    return indexDef;
  }

  public ISqlJetIndexDef createIndex(String sql) throws SqlJetException {
    db.getMutex().enter();
    try {
      return createIndexSafe(sql);
    } finally {
      db.getMutex().leave();
    }
  }

  private ISqlJetIndexDef createIndexSafe(String sql) throws SqlJetException {

    final ParserRuleReturnScope parseIndex = parseIndex(sql);
    final CommonTree ast = (CommonTree) parseIndex.getTree();

    final SqlJetIndexDef indexDef = new SqlJetIndexDef(ast, 0);

    if (null == indexDef.getName()) throw new SqlJetException(SqlJetErrorCode.ERROR);
    final String indexName = indexDef.getName();
    if ("".equals(indexName)) throw new SqlJetException(SqlJetErrorCode.ERROR);

    checkNameReserved(indexName);

    if (indexDefs.containsKey(indexName)) {
      if (indexDef.isKeepExisting()) {
        return indexDefs.get(indexName);
      } else {
        throw new SqlJetException(
            SqlJetErrorCode.ERROR, "Index \"" + indexName + "\" exists already");
      }
    }

    checkNameConflict(SqlJetSchemaObjectType.INDEX, indexName);

    if (null == indexDef.getTableName()) throw new SqlJetException(SqlJetErrorCode.ERROR);
    final String tableName = indexDef.getTableName();
    if ("".equals(tableName)) throw new SqlJetException(SqlJetErrorCode.ERROR);

    final List<ISqlJetIndexedColumn> columns = indexDef.getColumns();
    if (null == columns) throw new SqlJetException(SqlJetErrorCode.ERROR);

    final ISqlJetTableDef tableDef = getTable(tableName);
    if (null == tableDef) throw new SqlJetException(SqlJetErrorCode.ERROR);

    for (final ISqlJetIndexedColumn column : columns) {
      if (null == column.getName()) throw new SqlJetException(SqlJetErrorCode.ERROR);
      final String columnName = column.getName();
      if ("".equals(columnName)) throw new SqlJetException(SqlJetErrorCode.ERROR);
      if (null == tableDef.getColumn(columnName))
        throw new SqlJetException(
            SqlJetErrorCode.ERROR,
            "Column \"" + columnName + "\" not found in table \"" + tableName + "\"");
    }

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);
    final String createIndexSQL =
        indexDef.isUnique() ? getCreateIndexUniqueSql(parseIndex) : getCreateIndexSql(parseIndex);

    try {

      schemaTable.lock();

      try {

        db.getOptions().changeSchemaVersion();

        final int page = btree.createTable(BTREE_CREATE_INDEX_FLAGS);

        final long rowId =
            schemaTable.insertRecord(INDEX_TYPE, indexName, tableName, page, createIndexSQL);

        indexDef.setPage(page);
        indexDef.setRowId(rowId);
        indexDef.bindColumns(tableDef);
        indexDefs.put(indexName, indexDef);

        final SqlJetBtreeIndexTable indexTable =
            new SqlJetBtreeIndexTable(btree, indexDef.getName(), true);
        try {
          indexTable.reindex(this);
        } finally {
          indexTable.close();
        }
        return indexDef;

      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }
  }

  public void dropTable(String tableName) throws SqlJetException {
    db.getMutex().enter();
    try {
      dropTableSafe(tableName);
    } finally {
      db.getMutex().leave();
    }
  }

  private void dropTableSafe(String tableName) throws SqlJetException {

    if (null == tableName || "".equals(tableName))
      throw new SqlJetException(SqlJetErrorCode.MISUSE, "Table name must be not empty");

    if (!tableDefs.containsKey(tableName))
      throw new SqlJetException(SqlJetErrorCode.MISUSE, "Table not found: " + tableName);
    final SqlJetTableDef tableDef = (SqlJetTableDef) tableDefs.get(tableName);

    dropTableIndexes(tableDef);

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);

    try {

      schemaTable.lock();

      try {

        db.getOptions().changeSchemaVersion();

        if (!schemaTable.goToRow(tableDef.getRowId())
            || !TABLE_TYPE.equals(schemaTable.getTypeField()))
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        final String n = schemaTable.getNameField();
        if (null == n || !tableName.equals(n)) throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        schemaTable.delete();

      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }

    final int page = tableDef.getPage();
    final int moved = btree.dropTable(page);
    if (moved != 0) {
      movePage(page, moved);
    }

    tableDefs.remove(tableName);
  }

  /**
   * @param schemaTable
   * @param tableDef
   * @throws SqlJetException
   */
  private void dropTableIndexes(SqlJetTableDef tableDef) throws SqlJetException {
    final String tableName = tableDef.getName();
    final Iterator<Map.Entry<String, ISqlJetIndexDef>> iterator = indexDefs.entrySet().iterator();
    while (iterator.hasNext()) {
      final Map.Entry<String, ISqlJetIndexDef> indexDefEntry = iterator.next();
      final String indexName = indexDefEntry.getKey();
      final ISqlJetIndexDef indexDef = indexDefEntry.getValue();
      if (indexDef.getTableName().equals(tableName)) {
        if (doDropIndex(indexName, true, false)) {
          iterator.remove();
        }
      }
    }
  }

  /**
   * @param schemaTable
   * @param name
   * @param generateAutoIndexName
   * @throws SqlJetException
   */
  private boolean doDropIndex(String indexName, boolean allowAutoIndex, boolean throwIfFial)
      throws SqlJetException {

    if (!indexDefs.containsKey(indexName)) {
      if (throwIfFial) throw new SqlJetException(SqlJetErrorCode.MISUSE);
      return false;
    }
    final SqlJetBaseIndexDef indexDef = (SqlJetBaseIndexDef) indexDefs.get(indexName);

    if (!allowAutoIndex && indexDef.isImplicit()) {
      if (throwIfFial)
        throw new SqlJetException(
            SqlJetErrorCode.MISUSE, String.format(CANT_DELETE_IMPLICIT_INDEX, indexName));
      return false;
    }

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);

    try {

      schemaTable.lock();

      try {

        if (!schemaTable.goToRow(indexDef.getRowId())
            || !INDEX_TYPE.equals(schemaTable.getTypeField())) {
          if (throwIfFial) throw new SqlJetException(SqlJetErrorCode.INTERNAL);
          return false;
        }
        final String n = schemaTable.getNameField();
        if (null == n || !indexName.equals(n)) {
          if (throwIfFial) throw new SqlJetException(SqlJetErrorCode.INTERNAL);
          return false;
        }

        if (!allowAutoIndex && schemaTable.isNull(ISqlJetBtreeSchemaTable.SQL_FIELD)) {
          if (throwIfFial)
            throw new SqlJetException(
                SqlJetErrorCode.MISUSE, String.format(CANT_DELETE_IMPLICIT_INDEX, indexName));
          return false;
        }

        schemaTable.delete();

      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }

    final int page = indexDef.getPage();
    final int moved = btree.dropTable(page);
    if (moved != 0) {
      movePage(page, moved);
    }

    return true;
  }

  /**
   * @param page
   * @param moved
   * @throws SqlJetException
   */
  private void movePage(final int page, final int moved) throws SqlJetException {
    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);
    try {
      schemaTable.lock();
      try {
        for (schemaTable.first(); !schemaTable.eof(); schemaTable.next()) {
          final long pageField = schemaTable.getPageField();
          if (pageField == moved) {
            final String nameField = schemaTable.getNameField();
            schemaTable.updateRecord(
                schemaTable.getRowId(),
                schemaTable.getTypeField(),
                nameField,
                schemaTable.getTableField(),
                page,
                schemaTable.getSqlField());
            final ISqlJetIndexDef index = getIndex(nameField);
            if (index != null) {
              if (index instanceof SqlJetBaseIndexDef) {
                ((SqlJetBaseIndexDef) index).setPage(page);
              }
            } else {
              final ISqlJetTableDef table = getTable(nameField);
              if (table != null) {
                if (table instanceof SqlJetTableDef) {
                  ((SqlJetTableDef) table).setPage(page);
                }
              }
            }
            return;
          }
        }
      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }
  }

  public void dropIndex(String indexName) throws SqlJetException {
    db.getMutex().enter();
    try {
      dropIndexSafe(indexName);
    } finally {
      db.getMutex().leave();
    }
  }

  private void dropIndexSafe(String indexName) throws SqlJetException {

    if (null == indexName || "".equals(indexName))
      throw new SqlJetException(SqlJetErrorCode.MISUSE, "Index name must be not empty");

    if (!indexDefs.containsKey(indexName))
      throw new SqlJetException(SqlJetErrorCode.MISUSE, "Index not found: " + indexName);

    if (doDropIndex(indexName, false, true)) {
      db.getOptions().changeSchemaVersion();
      indexDefs.remove(indexName);
    }
  }

  /**
   * @param tableName
   * @param newTableName
   * @param newColumnDef
   * @return
   * @throws SqlJetException
   */
  private ISqlJetTableDef alterTableSafe(final SqlJetAlterTableDef alterTableDef)
      throws SqlJetException {

    assert (null != alterTableDef);
    String tableName = alterTableDef.getTableName();
    String newTableName = alterTableDef.getNewTableName();
    ISqlJetColumnDef newColumnDef = alterTableDef.getNewColumnDef();

    if (null == tableName) {
      throw new SqlJetException(SqlJetErrorCode.MISUSE, "Table name isn't defined");
    }

    if (null == newTableName && null == newColumnDef) {
      throw new SqlJetException(SqlJetErrorCode.MISUSE, "Not defined any altering");
    }

    boolean renameTable = false;
    if (null != newTableName) {
      renameTable = true;
    } else {
      newTableName = tableName;
    }

    if (renameTable && tableDefs.containsKey(newTableName)) {
      throw new SqlJetException(
          SqlJetErrorCode.MISUSE, String.format("Table \"%s\" already exists", newTableName));
    }

    final SqlJetTableDef tableDef = (SqlJetTableDef) tableDefs.get(tableName);
    if (null == tableDef) {
      throw new SqlJetException(
          SqlJetErrorCode.MISUSE, String.format("Table \"%s\" not found", tableName));
    }

    List<ISqlJetColumnDef> columns = tableDef.getColumns();
    if (null != newColumnDef) {

      final String fieldName = newColumnDef.getName();
      if (tableDef.getColumn(fieldName) != null) {
        throw new SqlJetException(
            SqlJetErrorCode.MISUSE,
            String.format("Field \"%s\" already exists in table \"%s\"", fieldName, tableName));
      }

      final List<ISqlJetColumnConstraint> constraints = newColumnDef.getConstraints();
      if (null != constraints && 0 != constraints.size()) {
        boolean notNull = false;
        boolean defaultValue = false;
        for (final ISqlJetColumnConstraint constraint : constraints) {
          if (constraint instanceof ISqlJetColumnNotNull) {
            notNull = true;
          } else if (constraint instanceof ISqlJetColumnDefault) {
            defaultValue = true;
          } else {
            throw new SqlJetException(
                SqlJetErrorCode.MISUSE,
                String.format("Invalid constraint: %s", constraint.toString()));
          }
        }
        if (notNull && !defaultValue) {
          throw new SqlJetException(
              SqlJetErrorCode.MISUSE, "NOT NULL requires to have DEFAULT value");
        }
      }

      columns = new ArrayList<ISqlJetColumnDef>(columns);
      columns.add(newColumnDef);
    }

    final int page = tableDef.getPage();
    final long rowId = tableDef.getRowId();

    final SqlJetTableDef alterDef =
        new SqlJetTableDef(
            newTableName,
            null,
            tableDef.isTemporary(),
            false,
            columns,
            tableDef.getConstraints(),
            page,
            rowId);

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);
    try {
      schemaTable.lock();
      try {

        if (!schemaTable.goToRow(rowId)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }

        final String typeField = schemaTable.getTypeField();
        final String nameField = schemaTable.getNameField();
        final String tableField = schemaTable.getTableField();
        final int pageField = schemaTable.getPageField();

        if (null == typeField || !TABLE_TYPE.equals(typeField)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        if (null == nameField || !tableName.equals(nameField)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        if (null == tableField || !tableName.equals(tableField)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        if (0 == pageField || pageField != page) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }

        final String alteredSql = getTableAlteredSql(schemaTable.getSqlField(), alterTableDef);

        db.getOptions().changeSchemaVersion();

        schemaTable.insertRecord(TABLE_TYPE, newTableName, newTableName, page, alteredSql);

        if (renameTable && !tableName.equals(newTableName)) {
          renameTablesIndices(
              schemaTable, tableName, newTableName, getAlterTableName(alterTableDef));
        }

        tableDefs.remove(tableName);
        tableDefs.put(newTableName, alterDef);

        return alterDef;

      } finally {
        schemaTable.unlock();
      }
    } finally {
      schemaTable.close();
    }
  }

  /**
   * @param alterTableDef
   * @return
   */
  private String getAlterTableName(SqlJetAlterTableDef alterTableDef) {
    final ParserRuleReturnScope parsedSql = alterTableDef.getParsedSql();
    final CommonTree ast = (CommonTree) parsedSql.getTree();
    final CommonToken stopToken = (CommonToken) parsedSql.getStop();
    final CommonToken nameToken =
        (CommonToken) ((CommonTree) ast.getChild(ast.getChildCount() - 1)).getToken();
    final CharStream inputStream = nameToken.getInputStream();
    return inputStream.substring(nameToken.getStartIndex(), stopToken.getStopIndex());
  }

  /**
   * @param sql
   * @param alterTableDef
   * @return
   * @throws SqlJetException
   */
  private String getTableAlteredSql(String sql, SqlJetAlterTableDef alterTableDef)
      throws SqlJetException {

    final RuleReturnScope parsedSQL = parseTable(sql);
    final CommonTree ast = (CommonTree) parsedSQL.getTree();
    final CommonToken nameToken = (CommonToken) ((CommonTree) ast.getChild(1)).getToken();
    final CharStream inputStream = nameToken.getInputStream();
    final CommonToken stopToken = (CommonToken) parsedSQL.getStop();

    final StringBuilder b = new StringBuilder();

    if (alterTableDef.getNewTableName() != null) {
      b.append(inputStream.substring(0, nameToken.getStartIndex() - 1));
      b.append(getAlterTableName(alterTableDef));
      b.append(inputStream.substring(nameToken.getStopIndex() + 1, stopToken.getStopIndex()));
    } else if (alterTableDef.getNewColumnDef() != null) {
      b.append(inputStream.substring(0, stopToken.getStartIndex() - 1));
      b.append(",").append(getAlterTableName(alterTableDef));
      b.append(inputStream.substring(stopToken.getStartIndex(), stopToken.getStopIndex()));
    } else {
      throw new SqlJetException("Wrong ALTER TABLE statement");
    }

    return b.toString();
  }

  /**
   * @param schemaTable
   * @param newTableName
   * @param tableName
   * @param string
   * @throws SqlJetException
   */
  private void renameTablesIndices(
      final ISqlJetBtreeSchemaTable schemaTable,
      String tableName,
      String newTableName,
      String alterTableName)
      throws SqlJetException {

    final Set<ISqlJetIndexDef> indexes = getIndexes(tableName);
    if (null == indexes || 0 == indexes.size()) {
      return;
    }

    int i = 0;
    for (final ISqlJetIndexDef index : indexes) {
      if (index instanceof SqlJetBaseIndexDef) {

        final SqlJetBaseIndexDef indexDef = (SqlJetBaseIndexDef) index;
        final String indexName = indexDef.getName();
        final long rowId = indexDef.getRowId();
        final int page = indexDef.getPage();

        if (!schemaTable.goToRow(rowId)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }

        final String typeField = schemaTable.getTypeField();
        final String nameField = schemaTable.getNameField();
        final String tableField = schemaTable.getTableField();
        final int pageField = schemaTable.getPageField();

        if (null == typeField || !INDEX_TYPE.equals(typeField)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        if (null == nameField || !indexName.equals(nameField)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        if (null == tableField || !tableName.equals(tableField)) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }
        if (0 == pageField || pageField != page) {
          throw new SqlJetException(SqlJetErrorCode.CORRUPT);
        }

        indexDef.setTableName(newTableName);

        String newIndexName = indexName;
        String alteredIndexSql = null;

        if (index.isImplicit()) {
          newIndexName = generateAutoIndexName(tableName, ++i);
          indexDef.setName(newIndexName);
          indexDefs.remove(indexName);
          indexDefs.put(newIndexName, indexDef);
        } else {
          alteredIndexSql = getAlteredIndexSql(schemaTable.getSqlField(), alterTableName);
        }

        schemaTable.insertRecord(INDEX_TYPE, newIndexName, newTableName, page, alteredIndexSql);

      } else {
        throw new SqlJetException(SqlJetErrorCode.INTERNAL);
      }
    }
  }

  /**
   * @param sql
   * @param alterTableName
   * @return
   * @throws SqlJetException
   */
  private String getAlteredIndexSql(String sql, String alterTableName) throws SqlJetException {
    final RuleReturnScope parsedSQL = parseIndex(sql);
    final CommonTree ast = (CommonTree) parsedSQL.getTree();
    final CommonToken nameToken = (CommonToken) ((CommonTree) ast.getChild(2)).getToken();
    final CharStream inputStream = nameToken.getInputStream();
    final CommonToken stopToken = (CommonToken) parsedSQL.getStop();
    final StringBuilder b = new StringBuilder();
    b.append(inputStream.substring(0, nameToken.getStartIndex() - 1));
    b.append(alterTableName);
    b.append(inputStream.substring(nameToken.getStopIndex() + 1, stopToken.getStopIndex()));
    return b.toString();
  }

  private ParserRuleReturnScope parseSqlStatement(String sql) throws SqlJetException {
    try {
      CharStream chars = new ANTLRStringStream(sql);
      SqlLexer lexer = new SqlLexer(chars);
      CommonTokenStream tokens = new CommonTokenStream(lexer);
      SqlParser parser = new SqlParser(tokens);
      return parser.sql_stmt_itself();
    } catch (RecognitionException re) {
      throw new SqlJetException(SqlJetErrorCode.ERROR, "Invalid sql statement: " + sql);
    }
  }

  public ISqlJetTableDef alterTable(String sql) throws SqlJetException {

    final SqlJetAlterTableDef alterTableDef = new SqlJetAlterTableDef(parseSqlStatement(sql));

    db.getMutex().enter();
    try {
      return alterTableSafe(alterTableDef);
    } finally {
      db.getMutex().leave();
    }
  }

  public ISqlJetVirtualTableDef createVirtualTable(String sql, int page) throws SqlJetException {
    db.getMutex().enter();
    try {
      return createVirtualTableSafe(sql, page);
    } finally {
      db.getMutex().leave();
    }
  }

  private ISqlJetVirtualTableDef createVirtualTableSafe(String sql, int page)
      throws SqlJetException {

    final RuleReturnScope parseTable = parseTable(sql);
    final CommonTree ast = (CommonTree) parseTable.getTree();

    if (!isCreateVirtualTable(ast)) {
      throw new SqlJetException(SqlJetErrorCode.ERROR);
    }

    final SqlJetVirtualTableDef tableDef = new SqlJetVirtualTableDef(ast, 0);
    if (null == tableDef.getTableName()) throw new SqlJetException(SqlJetErrorCode.ERROR);
    final String tableName = tableDef.getTableName();
    if ("".equals(tableName)) {
      throw new SqlJetException(SqlJetErrorCode.ERROR);
    }

    checkNameReserved(tableName);
    checkFieldNamesRepeatsConflict(tableDef.getTableName(), tableDef.getModuleColumns());

    if (virtualTableDefs.containsKey(tableName)) {
      throw new SqlJetException(
          SqlJetErrorCode.ERROR, "Virtual table \"" + tableName + "\" exists already");
    }

    checkNameConflict(SqlJetSchemaObjectType.VIRTUAL_TABLE, tableName);

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);
    final String createVirtualTableSQL = getCreateVirtualTableSql(parseTable);

    try {

      schemaTable.lock();

      try {

        db.getOptions().changeSchemaVersion();

        final long rowId =
            schemaTable.insertRecord(TABLE_TYPE, tableName, tableName, page, createVirtualTableSQL);

        tableDef.setPage(page);
        tableDef.setRowId(rowId);
        virtualTableDefs.put(tableName, tableDef);
        return tableDef;

      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }
  }

  /**
   * @param name
   * @throws SqlJetException
   */
  private void checkNameReserved(final String name) throws SqlJetException {
    if (isNameReserved(name)) {
      throw new SqlJetException(String.format(NAME_RESERVED, name));
    }
  }

  /**
   * Returns true if name is reserved for internal use.
   *
   * @param name
   * @return true if name is reserved
   */
  public boolean isNameReserved(String name) {
    return name.startsWith("sqlite_");
  }

  /**
   * @param tableName
   * @throws SqlJetException
   */
  private void checkNameConflict(SqlJetSchemaObjectType objectType, final String tableName)
      throws SqlJetException {
    if (isNameConflict(objectType, tableName)) {
      throw new SqlJetException(
          String.format(
              "Name conflict: %s named '%s' exists already", objectType.getName(), tableName));
    }
  }

  private boolean isNameConflict(SqlJetSchemaObjectType objectType, String name) {
    if (objectType != SqlJetSchemaObjectType.TABLE) {
      if (tableDefs.containsKey(name)) {
        return true;
      }
    }
    if (objectType != SqlJetSchemaObjectType.INDEX) {
      if (indexDefs.containsKey(name)) {
        return true;
      }
    }
    if (objectType != SqlJetSchemaObjectType.VIRTUAL_TABLE) {
      if (virtualTableDefs.containsKey(name)) {
        return true;
      }
    }
    return false;
  }

  public ISqlJetIndexDef createIndexForVirtualTable(
      final String virtualTableName, final String indexName) throws SqlJetException {
    db.getMutex().enter();
    try {
      return createIndexForVirtualTableSafe(virtualTableName, indexName);
    } finally {
      db.getMutex().leave();
    }
  }

  /**
   * @param virtualTableName
   * @param indexName
   * @return
   * @throws SqlJetException
   */
  private ISqlJetIndexDef createIndexForVirtualTableSafe(String virtualTableName, String indexName)
      throws SqlJetException {

    if (null == virtualTableName || "".equals(virtualTableName))
      throw new SqlJetException(SqlJetErrorCode.ERROR);
    if (null == indexName || "".equals(indexName)) throw new SqlJetException(SqlJetErrorCode.ERROR);

    checkNameReserved(indexName);

    if (indexDefs.containsKey(indexName)) {
      throw new SqlJetException(
          SqlJetErrorCode.ERROR, "Index \"" + indexName + "\" exists already");
    }

    checkNameConflict(SqlJetSchemaObjectType.INDEX, indexName);

    final ISqlJetVirtualTableDef tableDef = getVirtualTable(virtualTableName);
    if (null == tableDef) throw new SqlJetException(SqlJetErrorCode.ERROR);

    final ISqlJetBtreeSchemaTable schemaTable = openSchemaTable(true);

    try {

      schemaTable.lock();

      try {

        db.getOptions().changeSchemaVersion();

        final ISqlJetIndexDef indexDef =
            createAutoIndex(schemaTable, tableDef.getTableName(), indexName);

        indexDefs.put(indexName, indexDef);

        return indexDef;

      } finally {
        schemaTable.unlock();
      }

    } finally {
      schemaTable.close();
    }
  }
}