Hi, back in September I've investigated strange issue in H2 when performing 
alter table on table with constraints. After modifying table we were not 
able to insert some rows - it failed with error Unique index or primary key 
violation, but it should've been OK. We examined our indexes and they were 
not as intended. I think I found the reason indexes were corrupted, but It 
seemed arrogant to just try to fork h2 or send a patch... trying it here 
first ;-)  I'am not very familiar with H2 source code, so I'd like some 
feedback from the developers if possible before trying to patch anything 
possibly breaking something :-) Here is my testcase for the issue with 
description and proposed fixes:

CREATE TABLE "domains" (
  "id" bigint NOT NULL auto_increment PRIMARY KEY
);

CREATE TABLE "users" (
  "id" bigint NOT NULL auto_increment PRIMARY KEY,
  "username" varchar_ignorecase(255),
  "domain" bigint,
);

/* adds constraint on (domain,username) and generates unique index 
domainusername_key_INDEX_xxx */
ALTER TABLE "users"    ADD CONSTRAINT "domainusername_key" UNIQUE 
("domain","username"); 


/* adds foreign key on domain - if domainusername_key didn't exist it would 
create unique index on domain, but it reuses the existing index   */
ALTER TABLE "users"    ADD CONSTRAINT "udomain_fkey" FOREIGN KEY ("domain") 
REFERENCES "domains"("id") ON DELETE RESTRICT;

/* now we drop the domainusername_key, but domainusername_key_INDEX_xxx is 
used by udomain_fkey and is not dropped - this is an issue, because it's 
unique index and still enforcing constraint on (domain,username) */
ALTER TABLE "users" DROP CONSTRAINT "domainusername_key";

insert into "users" ("username","domain") VALUES ('test',1);
insert into "users" ("username","domain") VALUES ('test',1); /* should 
work,because we dropped domainusername_key, but fails: Unique index or 
primary key violation: "domainusername_key_INDEX_6 ON 
PUBLIC.""users""(""domain"", ""username"") VALUES (1, CAST('test' AS 
VARCHAR_IGNORECASE), 1)"; */
 
/*
    I believe, the problem is in AlterTableAddConstraint getIndex function
    
    caleld from case 
CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_REFERENTIAL: branch
    index is found calling getIndex with moreColumnOk, but if the resulting 
index is UNIQUE, it's a problem, because it will be referenced by the new 
constraint and will enforce key uniqueness even if the original owning 
constraint is dropped

    private static Index getIndex(Table t, IndexColumn[] cols, boolean 
moreColumnOk) {
        if (t.getIndexes() == null) {
            return null;
        }
        for (Index idx : t.getIndexes()) {
            if (canUseIndex(idx, t, cols, moreColumnOk)) {
                return idx;
            }
        }
        return null;
    }
    
    I think, either moreColumnOk should be false, or it should be applied 
only to non-unique indexes.

    Another similar problem is in 
CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_UNIQUE: branch where 
getUniqueIndex is called. 
    It tries to reuse existing index, but it uses even index that is not 
unique for given fields. As I understand it ConstraintUnique is enforced by 
the underlying unique index. 
    There is even a comment in the class
    
    @Override
    public void checkRow(Session session, Table t, Row oldRow, Row newRow) {
        // unique index check is enough
    }

    But in canUseUniqueIndex there is a comment: 
    
    // all columns of the index must be part of the list,
    // but not all columns of the list need to be part of the index
    
    So the canUseUniqueIndex is passed list of columns that must be unique. 
I would expect that to be unique, all must be in the index key, the order 
doesn't matter and there must be no other fields in the index key.
    
    I have implemented my own and it fixes issue, we found during testing, 
when incorrect index (with less columns) was being reused and din't ensure 
uniqueness for the constraint.
    
    private static boolean canUseUniqueIndexFixed(Index idx, Table table,
        IndexColumn[] cols) {
      if (idx.getTable() != table || !idx.getIndexType().isUnique()) {
        return false;
      }
      Column[] indexCols = idx.getColumns();
      
      HashSet<Column> indexColsSet = New.hashSet();
    
      for (Column c : indexCols) {
        indexColsSet.add(c);
      }

      HashSet<Column> set = New.hashSet();
    
      for (IndexColumn c : cols) {
        set.add(c.column);
      }
    
      return set.equals(indexColsSet);
    }    
    
*/

Thank you for any feedback, Petr


-- 
You received this message because you are subscribed to the Google Groups "H2 
Database" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/h2-database.
For more options, visit https://groups.google.com/d/optout.

Reply via email to