Skip to content

Commit

Permalink
Merge pull request #3812 from katzyn/unique
Browse files Browse the repository at this point in the history
UNIQUE null treatment
  • Loading branch information
katzyn committed Jun 6, 2023
2 parents ed4fcb1 + c91e101 commit 6992cb9
Show file tree
Hide file tree
Showing 24 changed files with 588 additions and 113 deletions.
14 changes: 14 additions & 0 deletions h2/src/docsrc/help/information_schema.csv
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,13 @@ The name of the field of the row value.
The type of the index ('PRIMARY KEY', 'UNIQUE INDEX', 'SPATIAL INDEX', etc.)
"

"INDEXES","NULLS_DISTINCT","
'YES' for unique indexes with distinct null values,
'NO' for unique indexes with not distinct null values,
'ALL' for multi-column unique indexes where only rows with null values in all unique columns are distinct,
NULL for other types of indexes.
"

"INDEXES","IS_GENERATED","
Whether index is generated by a constraint and belongs to it.
"
Expand Down Expand Up @@ -925,6 +932,13 @@ For regular tables contains the total number of rows including the uncommitted r
referencing tables and 'NO' when they are disabled.
"

"TABLE_CONSTRAINTS","NULLS_DISTINCT","
'YES' for unique constraints with distinct null values,
'NO' for unique constraints with not distinct null values,
'ALL' for multi-column unique constraints where only rows with null values in all unique columns are distinct,
NULL for other types of constraints.
"

"TABLE_PRIVILEGES","WITH_HIERARCHY","
'NO'.
"
Expand Down
2 changes: 2 additions & 0 deletions h2/src/docsrc/html/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ <h1>Change Log</h1>

<h2>Next Version (unreleased)</h2>
<ul>
<li>PR #3812: UNIQUE null treatment
</li>
<li>PR #3811: BTRIM function, octal and binary literals and other changes
</li>
<li>Issue #352: In comparison text values are converted to INT even when they should be converted to BIGINT
Expand Down
40 changes: 32 additions & 8 deletions h2/src/main/org/h2/command/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
import org.h2.engine.Procedure;
import org.h2.engine.Right;
import org.h2.engine.SessionLocal;
import org.h2.engine.NullsDistinct;
import org.h2.engine.User;
import org.h2.expression.Alias;
import org.h2.expression.ArrayConstructorByQuery;
Expand Down Expand Up @@ -3165,10 +3166,11 @@ private Expression readCondition() {
}
case UNIQUE: {
read();
NullsDistinct nullsDistinct = readNullsDistinct(NullsDistinct.DISTINCT);
read(OPEN_PAREN);
Query query = parseQuery();
read(CLOSE_PAREN);
return new UniquePredicate(query);
return new UniquePredicate(query, nullsDistinct);
}
default:
if (readIf("INTERSECTS", OPEN_PAREN)) {
Expand Down Expand Up @@ -6890,7 +6892,8 @@ private Prepared parseCreate() {
return parseCreateSynonym(orReplace);
} else {
boolean hash = false, primaryKey = false;
boolean unique = false, spatial = false;
NullsDistinct nullsDistinct = null;
boolean spatial = false;
String indexName = null;
Schema oldSchema = null;
boolean ifNotExists = false;
Expand All @@ -6906,11 +6909,11 @@ private Prepared parseCreate() {
}
} else {
if (readIf(UNIQUE)) {
unique = true;
nullsDistinct = readNullsDistinct(database.getMode().nullsDistinct);
}
if (readIf("HASH")) {
hash = true;
} else if (!unique && readIf("SPATIAL")) {
} else if (nullsDistinct == null && readIf("SPATIAL")) {
spatial = true;
}
read("INDEX");
Expand Down Expand Up @@ -6952,13 +6955,13 @@ private Prepared parseCreate() {
int uniqueColumnCount = 0;
if (spatial) {
columns = new IndexColumn[] { new IndexColumn(readIdentifier()) };
if (unique) {
if (nullsDistinct != null) {
uniqueColumnCount = 1;
}
read(CLOSE_PAREN);
} else {
columns = parseIndexColumnList();
if (unique) {
if (nullsDistinct != null) {
uniqueColumnCount = columns.length;
if (readIf("INCLUDE")) {
read(OPEN_PAREN);
Expand All @@ -6972,11 +6975,27 @@ private Prepared parseCreate() {
}
}
command.setIndexColumns(columns);
command.setUniqueColumnCount(uniqueColumnCount);
command.setUnique(nullsDistinct, uniqueColumnCount);
return command;
}
}

private NullsDistinct readNullsDistinct(NullsDistinct defaultDistinct) {
if (readIf("NULLS")) {
if (readIf(DISTINCT)) {
return NullsDistinct.DISTINCT;
}
if (readIf(NOT, DISTINCT)) {
return NullsDistinct.NOT_DISTINCT;
}
if (readIf(ALL, DISTINCT)) {
return NullsDistinct.ALL_DISTINCT;
}
throw getSyntaxError();
}
return defaultDistinct;
}

/**
* @return true if we expect to see a TABLE clause
*/
Expand Down Expand Up @@ -9205,8 +9224,9 @@ private DefineCommand parseTableConstraintIf(String tableName, Schema schema, bo
command.setIndex(getSchema().findIndex(session, indexName));
}
break;
case UNIQUE:
case UNIQUE: {
read();
NullsDistinct nullsDistinct = readNullsDistinct(database.getMode().nullsDistinct);
// MySQL compatibility
boolean compatibility = database.getMode().indexDefinitionInCreateTable;
if (compatibility) {
Expand All @@ -9220,6 +9240,7 @@ private DefineCommand parseTableConstraintIf(String tableName, Schema schema, bo
read(OPEN_PAREN);
command = new AlterTableAddConstraint(session, schema, CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_UNIQUE,
ifNotExists);
command.setNullsDistinct(nullsDistinct);
if (readIf(VALUE, CLOSE_PAREN)) {
command.setIndexColumns(null);
} else {
Expand All @@ -9233,6 +9254,7 @@ private DefineCommand parseTableConstraintIf(String tableName, Schema schema, bo
readIf(USING, "BTREE");
}
break;
}
case FOREIGN:
read();
read(KEY);
Expand Down Expand Up @@ -9516,9 +9538,11 @@ private void readColumnConstraints(CommandWithColumns command, Schema schema, St
pk.setIndexColumns(new IndexColumn[] { new IndexColumn(column.getName()) });
command.addConstraintCommand(pk);
} else if (readIf(UNIQUE)) {
NullsDistinct nullsDistinct = readNullsDistinct(database.getMode().nullsDistinct);
AlterTableAddConstraint unique = new AlterTableAddConstraint(session, schema,
CommandInterface.ALTER_TABLE_ADD_CONSTRAINT_UNIQUE, false);
unique.setConstraintName(constraintName);
unique.setNullsDistinct(nullsDistinct);
unique.setIndexColumns(new IndexColumn[] { new IndexColumn(column.getName()) });
unique.setTableName(tableName);
command.addConstraintCommand(unique);
Expand Down
51 changes: 32 additions & 19 deletions h2/src/main/org/h2/command/ddl/AlterTableAddConstraint.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.h2.engine.Database;
import org.h2.engine.Right;
import org.h2.engine.SessionLocal;
import org.h2.engine.NullsDistinct;
import org.h2.expression.Expression;
import org.h2.index.Index;
import org.h2.index.IndexType;
Expand All @@ -38,6 +39,7 @@ public class AlterTableAddConstraint extends AlterTable {

private final int type;
private String constraintName;
private NullsDistinct nullsDistinct;
private IndexColumn[] indexColumns;
private ConstraintActionType deleteAction = ConstraintActionType.RESTRICT;
private ConstraintActionType updateAction = ConstraintActionType.RESTRICT;
Expand Down Expand Up @@ -153,8 +155,7 @@ private int tryUpdate(Table table) {
index.getIndexType().setBelongsToConstraint(true);
int id = getObjectId();
String name = generateConstraintName(table);
ConstraintUnique pk = new ConstraintUnique(getSchema(),
id, name, table, true);
ConstraintUnique pk = new ConstraintUnique(getSchema(), id, name, table, true, null);
pk.setColumns(indexColumns);
pk.setIndex(index, true);
constraint = pk;
Expand Down Expand Up @@ -259,13 +260,13 @@ private int tryUpdate(Table table) {
new StringBuilder("PRIMARY KEY | UNIQUE ("), refIndexColumns, HasSQL.TRACE_SQL_FLAGS)
.append(')').toString());
}
if (index != null && canUseIndex(index, table, indexColumns, false)) {
if (index != null && canUseIndex(index, table, indexColumns, null)) {
isOwner = true;
index.getIndexType().setBelongsToConstraint(true);
} else {
index = getIndex(table, indexColumns, false);
index = getIndex(table, indexColumns, null);
if (index == null) {
index = createIndex(table, indexColumns, false);
index = createIndex(table, indexColumns, null);
isOwner = true;
}
}
Expand Down Expand Up @@ -304,13 +305,15 @@ private int tryUpdate(Table table) {
private ConstraintUnique createUniqueConstraint(Table table, Index index, IndexColumn[] indexColumns,
boolean forForeignKey) {
boolean isOwner = false;
if (index != null && canUseIndex(index, table, indexColumns, true)) {
NullsDistinct needNullsDistinct = nullsDistinct != null ? nullsDistinct : NullsDistinct.DISTINCT;
if (index != null && canUseIndex(index, table, indexColumns, needNullsDistinct)) {
isOwner = true;
index.getIndexType().setBelongsToConstraint(true);
} else {
index = getIndex(table, indexColumns, true);
index = getIndex(table, indexColumns, needNullsDistinct);
if (index == null) {
index = createIndex(table, indexColumns, true);
index = createIndex(table, indexColumns,
nullsDistinct != null ? nullsDistinct : session.getMode().nullsDistinct);
isOwner = true;
}
}
Expand All @@ -329,7 +332,10 @@ private ConstraintUnique createUniqueConstraint(Table table, Index index, IndexC
id = getObjectId();
name = generateConstraintName(table);
}
ConstraintUnique unique = new ConstraintUnique(tableSchema, id, name, table, false);
if (indexColumns.length == 1 && needNullsDistinct == NullsDistinct.ALL_DISTINCT) {
needNullsDistinct = NullsDistinct.DISTINCT;
}
ConstraintUnique unique = new ConstraintUnique(tableSchema, id, name, table, false, needNullsDistinct);
unique.setColumns(indexColumns);
unique.setIndex(index, isOwner);
return unique;
Expand All @@ -344,12 +350,12 @@ private void addConstraintToTable(Database db, Table table, Constraint constrain
table.addConstraint(constraint);
}

private Index createIndex(Table t, IndexColumn[] cols, boolean unique) {
private Index createIndex(Table t, IndexColumn[] cols, NullsDistinct nullsDistinct) {
int indexId = getDatabase().allocateObjectId();
IndexType indexType;
if (unique) {
if (nullsDistinct != null) {
// for unique constraints
indexType = IndexType.createUnique(t.isPersistIndexes(), false);
indexType = IndexType.createUnique(t.isPersistIndexes(), false, cols.length, nullsDistinct);
} else {
// constraints
indexType = IndexType.createNonUnique(t.isPersistIndexes());
Expand All @@ -359,8 +365,8 @@ private Index createIndex(Table t, IndexColumn[] cols, boolean unique) {
String indexName = t.getSchema().getUniqueIndexName(session, t,
prefix + "_INDEX_");
try {
Index index = t.addIndex(session, indexName, indexId, cols, unique ? cols.length : 0, indexType, true,
null);
Index index = t.addIndex(session, indexName, indexId, cols, nullsDistinct != null ? cols.length : 0,
indexType, true, null);
createdIndexes.add(index);
return index;
} finally {
Expand All @@ -383,7 +389,7 @@ private static ConstraintUnique getUniqueConstraint(Table t, IndexColumn[] cols)
if (constraint.getTable() == t) {
Constraint.Type constraintType = constraint.getConstraintType();
if (constraintType == Constraint.Type.PRIMARY_KEY || constraintType == Constraint.Type.UNIQUE) {
if (canUseIndex(constraint.getIndex(), t, cols, true)) {
if (canUseIndex(constraint.getIndex(), t, cols, NullsDistinct.DISTINCT)) {
return (ConstraintUnique) constraint;
}
}
Expand All @@ -393,12 +399,12 @@ private static ConstraintUnique getUniqueConstraint(Table t, IndexColumn[] cols)
return null;
}

private static Index getIndex(Table t, IndexColumn[] cols, boolean unique) {
private static Index getIndex(Table t, IndexColumn[] cols, NullsDistinct nullsDistinct) {
ArrayList<Index> indexes = t.getIndexes();
Index index = null;
if (indexes != null) {
for (Index idx : indexes) {
if (canUseIndex(idx, t, cols, unique)) {
if (canUseIndex(idx, t, cols, nullsDistinct)) {
if (index == null || idx.getIndexColumns().length < index.getIndexColumns().length) {
index = idx;
}
Expand All @@ -408,16 +414,19 @@ private static Index getIndex(Table t, IndexColumn[] cols, boolean unique) {
return index;
}

private static boolean canUseIndex(Index index, Table table, IndexColumn[] cols, boolean unique) {
private static boolean canUseIndex(Index index, Table table, IndexColumn[] cols, NullsDistinct nullsDistinct) {
if (index.getTable() != table) {
return false;
}
int allowedColumns;
if (unique) {
if (nullsDistinct != null) {
allowedColumns = index.getUniqueColumnCount();
if (allowedColumns != cols.length) {
return false;
}
if (index.getIndexType().getEffectiveNullsDistinct().compareTo(nullsDistinct) < 0) {
return false;
}
} else {
if (index.getCreateSQL() == null || (allowedColumns = index.getColumns().length) != cols.length) {
return false;
Expand Down Expand Up @@ -446,6 +455,10 @@ public int getType() {
return type;
}

public void setNullsDistinct(NullsDistinct nullsDistinct) {
this.nullsDistinct = nullsDistinct;
}

public void setCheckExpression(Expression expression) {
this.checkExpression = expression;
}
Expand Down
7 changes: 5 additions & 2 deletions h2/src/main/org/h2/command/ddl/CreateIndex.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.h2.engine.Database;
import org.h2.engine.Right;
import org.h2.engine.SessionLocal;
import org.h2.engine.NullsDistinct;
import org.h2.index.IndexType;
import org.h2.message.DbException;
import org.h2.schema.Schema;
Expand All @@ -26,6 +27,7 @@ public class CreateIndex extends SchemaCommand {
private String tableName;
private String indexName;
private IndexColumn[] indexColumns;
private NullsDistinct nullsDistinct;
private int uniqueColumnCount;
private boolean primaryKey, hash, spatial;
private boolean ifTableExists;
Expand Down Expand Up @@ -95,7 +97,7 @@ public long update() {
}
indexType = IndexType.createPrimaryKey(persistent, hash);
} else if (uniqueColumnCount > 0) {
indexType = IndexType.createUnique(persistent, hash);
indexType = IndexType.createUnique(persistent, hash, uniqueColumnCount, nullsDistinct);
} else {
indexType = IndexType.createNonUnique(persistent, hash, spatial);
}
Expand All @@ -108,7 +110,8 @@ public void setPrimaryKey(boolean b) {
this.primaryKey = b;
}

public void setUniqueColumnCount(int uniqueColumnCount) {
public void setUnique(NullsDistinct nullsDistinct, int uniqueColumnCount) {
this.nullsDistinct = nullsDistinct;
this.uniqueColumnCount = uniqueColumnCount;
}

Expand Down
14 changes: 12 additions & 2 deletions h2/src/main/org/h2/constraint/ConstraintUnique.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import org.h2.engine.SessionLocal;
import org.h2.engine.NullsDistinct;
import org.h2.index.Index;
import org.h2.result.Row;
import org.h2.schema.Schema;
Expand All @@ -25,11 +26,13 @@ public class ConstraintUnique extends Constraint {
private boolean indexOwner;
private IndexColumn[] columns;
private final boolean primaryKey;
private NullsDistinct nullsDistinct;

public ConstraintUnique(Schema schema, int id, String name, Table table,
boolean primaryKey) {
public ConstraintUnique(Schema schema, int id, String name, Table table, boolean primaryKey,
NullsDistinct nullsDistinct) {
super(schema, id, name, table);
this.primaryKey = primaryKey;
this.nullsDistinct = nullsDistinct;
}

@Override
Expand Down Expand Up @@ -159,4 +162,11 @@ public void rebuild() {
// nothing to do
}

/**
* @return are nulls distinct
*/
public NullsDistinct getNullsDistinct() {
return nullsDistinct;
}

}

0 comments on commit 6992cb9

Please sign in to comment.