Skip to content

Commit 8d64a9d

Browse files
committed
Non-enforced constraints
1 parent 9209e4b commit 8d64a9d

File tree

9 files changed

+352
-240
lines changed

9 files changed

+352
-240
lines changed

doc/sql.extensions/README.ddl.txt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,54 @@ CREATE [UNIQUE] [ASC[ENDING] | DESC[ENDING]]
718718
[WHERE <search_condition>]
719719

720720
'isql -x' generates script accordingly.
721+
722+
4) Non-enforced constraints.
723+
724+
CREATE/ALTER TABLE supports creation of non-enforced constraints.
725+
726+
Syntax:
727+
728+
<col_constraint> ::=
729+
[CONSTRAINT constr_name]
730+
{ PRIMARY KEY [<using_index>]
731+
| UNIQUE [<using_index>]
732+
| REFERENCES other_table [(colname)] [<using_index>]
733+
[ON DELETE {NO ACTION | CASCADE | SET DEFAULT | SET NULL}]
734+
[ON UPDATE {NO ACTION | CASCADE | SET DEFAULT | SET NULL}]
735+
| CHECK (<check_condition>)
736+
| NOT NULL }
737+
[<constraint characteristics>]
738+
739+
<tconstraint> ::=
740+
[CONSTRAINT constr_name]
741+
{ PRIMARY KEY (<col_list>) [<using_index>]
742+
| UNIQUE (<col_list>) [<using_index>]
743+
| FOREIGN KEY (<col_list>)
744+
REFERENCES other_table [(<col_list>)] [<using_index>]
745+
[ON DELETE {NO ACTION | CASCADE | SET DEFAULT | SET NULL}]
746+
[ON UPDATE {NO ACTION | CASCADE | SET DEFAULT | SET NULL}]
747+
| CHECK (<check_condition>) }
748+
[<constraint characteristics>]
749+
750+
<constraint characteristics> ::=
751+
<constraint enforcement>
752+
753+
<constraint enforcement> ::=
754+
[ NOT ] ENFORCED
755+
756+
Note: Unlike the ANSI SQL standard, this allows PRIMARY KEY and UNIQUE constraints to be not enforced.
757+
758+
You can use the ALTER TABLE statement with the ALTER CONSTRAINT clause to change the enforcement of a constraint.
759+
760+
Syntax:
761+
762+
ALTER TABLE <table_name> ALTER CONSTRAINT <constraint_name> <constraint_enforcement>
763+
764+
However, you cannot disable a primary or unique key constraint if it is referenced by an active foreign key constraint.
765+
766+
Alternatively, you can control constraint enforcement through:
767+
768+
- ALTER INDEX - for referential integrity constraints
769+
- ALTER TRIGGER - for CHECK constraints
770+
771+
Disabling the associated index or trigger will also disable the corresponding constraint.

src/common/ParserTokens.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ PARSER_TOKEN(TOK_ELSE, "ELSE", false)
200200
PARSER_TOKEN(TOK_ENABLE, "ENABLE", true)
201201
PARSER_TOKEN(TOK_ENCRYPT, "ENCRYPT", true)
202202
PARSER_TOKEN(TOK_END, "END", false)
203+
PARSER_TOKEN(TOK_ENFORCED, "ENFORCED", true)
203204
PARSER_TOKEN(TOK_ENGINE, "ENGINE", true)
204205
PARSER_TOKEN(TOK_ENTRY_POINT, "ENTRY_POINT", true)
205206
PARSER_TOKEN(TOK_ESCAPE, "ESCAPE", false)

src/dsql/DdlNodes.epp

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ static void updateRdbFields(const TypeClause* type,
109109
SSHORT& fieldPrecisionNull, SSHORT& fieldPrecision,
110110
SSHORT& collationIdNull, SSHORT& collationId,
111111
SSHORT& segmentLengthNull, SSHORT& segmentLength);
112+
static void modifyIndex(thread_db* tdbb, jrd_tra* transaction,
113+
const char* name, bool active);
112114

113115
static const char* const CHECK_CONSTRAINT_EXCEPTION = "check_constraint";
114116

@@ -177,6 +179,50 @@ void ExecInSecurityDb::executeInSecurityDb(jrd_tra* localTransaction)
177179
//----------------------
178180

179181

182+
// Activate/deactivate given index
183+
static void modifyIndex(thread_db* tdbb, jrd_tra* transaction,
184+
const QualifiedName& name, bool active)
185+
{
186+
AutoCacheRequest request(tdbb, drq_m_index, DYN_REQUESTS);
187+
188+
bool found = false;
189+
FOR(REQUEST_HANDLE request TRANSACTION_HANDLE transaction)
190+
IDX IN RDB$INDICES
191+
WITH IDX.RDB$SCHEMA_NAME EQUIV name.schema.c_str() AND
192+
IDX.RDB$INDEX_NAME EQ name.object.c_str()
193+
{
194+
found = true;
195+
MODIFY IDX
196+
IDX.RDB$INDEX_INACTIVE.NULL = FALSE;
197+
IDX.RDB$INDEX_INACTIVE = active ? FALSE : TRUE;
198+
END_MODIFY
199+
}
200+
END_FOR
201+
202+
if (!found)
203+
{
204+
// msg 48: "Index not found"
205+
status_exception::raise(Arg::PrivateDyn(48) << Arg::Gds(isc_index_name) << name.toQuotedString());
206+
}
207+
}
208+
209+
// Check if given index is referenced by active foreign key constraint
210+
static void checkIndexReferenced(thread_db* tdbb, jrd_tra* transaction, const QualifiedName& name)
211+
{
212+
AutoCacheRequest fkCheck(tdbb, drq_c_active_fk, DYN_REQUESTS);
213+
214+
FOR(REQUEST_HANDLE fkCheck TRANSACTION_HANDLE transaction)
215+
IDX IN RDB$INDICES
216+
WITH IDX.RDB$FOREIGN_KEY_SCHEMA_NAME EQ name.schema.c_str() AND
217+
IDX.RDB$FOREIGN_KEY EQ name.object.c_str() AND
218+
IDX.RDB$INDEX_INACTIVE EQ 0 OR IDX.RDB$INDEX_INACTIVE MISSING
219+
{
220+
// MSG 408: "Can't deactivate index used by an integrity constraint"
221+
status_exception::raise(Arg::Gds(isc_integ_index_deactivate));
222+
}
223+
END_FOR
224+
}
225+
180226
// Check temporary table reference rules between given child relation and master
181227
// relation (owner of given PK/UK index).
182228
static void checkForeignKeyTempScope(thread_db* tdbb, jrd_tra* transaction,
@@ -3616,7 +3662,6 @@ bool TriggerDefinition::modify(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch
36163662
{
36173663
switch (TRG.RDB$SYSTEM_FLAG)
36183664
{
3619-
case fb_sysflag_check_constraint:
36203665
case fb_sysflag_referential_constraint:
36213666
case fb_sysflag_view_check:
36223667
status_exception::raise(Arg::Gds(isc_dyn_cant_modify_auto_trig));
@@ -6440,6 +6485,7 @@ DdlNode* RelationNode::dsqlPass(DsqlCompilerScratch* dsqlScratch)
64406485
case Clause::TYPE_ALTER_COL_NAME:
64416486
case Clause::TYPE_ALTER_COL_NULL:
64426487
case Clause::TYPE_ALTER_COL_POS:
6488+
case Clause::TYPE_ALTER_CONSTRAINT:
64436489
case Clause::TYPE_DROP_COLUMN:
64446490
case Clause::TYPE_DROP_CONSTRAINT:
64456491
case Clause::TYPE_ALTER_SQL_SECURITY:
@@ -6888,7 +6934,12 @@ void RelationNode::makeConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlScra
68886934
constraint.create = FB_NEW_POOL(pool) Constraint(pool);
68896935
constraint.create->type = Constraint::TYPE_NOT_NULL;
68906936
if (clause->constraintType == AddConstraintClause::CTYPE_NOT_NULL)
6937+
{
68916938
constraint.name = clause->name;
6939+
constraint.create->enforced = clause->enforced;
6940+
*notNull = clause->enforced;
6941+
}
6942+
// NOT NULL for PRIMARY KEY is always enforced
68926943
}
68936944

68946945
if (clause->constraintType == AddConstraintClause::CTYPE_NOT_NULL)
@@ -6908,6 +6959,7 @@ void RelationNode::makeConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlScra
69086959
constraint.create->index->name = constraint.name;
69096960

69106961
constraint.create->columns = clause->columns;
6962+
constraint.create->enforced = clause->enforced;
69116963
break;
69126964
}
69136965

@@ -6920,6 +6972,7 @@ void RelationNode::makeConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlScra
69206972
constraint.create->columns = clause->columns;
69216973
constraint.create->refRelation = clause->refRelation;
69226974
constraint.create->refColumns = clause->refColumns;
6975+
constraint.create->enforced = clause->enforced;
69236976

69246977
// If there is a referenced table name but no referenced field names, the
69256978
// primary key of the referenced table designates the referenced fields.
@@ -7035,6 +7088,7 @@ void RelationNode::makeConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlScra
70357088
CreateDropConstraint& constraint = constraints.add();
70367089
constraint.create = FB_NEW_POOL(pool) Constraint(pool);
70377090
constraint.create->type = Constraint::TYPE_CHECK;
7091+
constraint.create->enforced = clause->enforced;
70387092
constraint.name = clause->name;
70397093
defineCheckConstraint(dsqlScratch, *constraint.create, clause->check);
70407094
break;
@@ -7106,7 +7160,7 @@ void RelationNode::defineConstraint(thread_db* tdbb, DsqlCompilerScratch* dsqlSc
71067160
definition.unique = constraint.type != Constraint::TYPE_FK;
71077161
if (constraint.index->descending)
71087162
definition.descending = true;
7109-
definition.inactive = false;
7163+
definition.inactive = !constraint.enforced;
71107164
definition.columns = constraint.columns;
71117165
definition.refRelation = constraint.refRelation;
71127166
definition.refColumns = constraint.refColumns;
@@ -7381,6 +7435,7 @@ void RelationNode::defineCheckConstraintTrigger(DsqlCompilerScratch* dsqlScratch
73817435
trigger.type = triggerType;
73827436
trigger.source = clause->source;
73837437
trigger.blrData = blrWriter.getBlrData();
7438+
trigger.active = constraint.enforced;
73847439
}
73857440

73867441
// Define "on delete|update set default" trigger (for referential integrity) along with its blr.
@@ -8292,6 +8347,123 @@ void AlterRelationNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratc
82928347
break;
82938348
}
82948349

8350+
case Clause::TYPE_ALTER_CONSTRAINT:
8351+
{
8352+
executeBeforeTrigger();
8353+
8354+
const AlterConstraintClause* clause = static_cast<const AlterConstraintClause*>(i->getObject());
8355+
AutoCacheRequest request(tdbb, drq_get_constr_type, DYN_REQUESTS);
8356+
bool found = false;
8357+
8358+
FOR(REQUEST_HANDLE request TRANSACTION_HANDLE transaction)
8359+
RC IN RDB$RELATION_CONSTRAINTS
8360+
WITH RC.RDB$CONSTRAINT_NAME EQ clause->name.c_str() AND
8361+
RC.RDB$SCHEMA_NAME EQ name.schema.c_str() AND
8362+
RC.RDB$RELATION_NAME EQ name.object.c_str()
8363+
{
8364+
found = true;
8365+
QualifiedName indexName(RC.RDB$INDEX_NAME, RC.RDB$SCHEMA_NAME);
8366+
fb_utils::exact_name(RC.RDB$CONSTRAINT_TYPE);
8367+
if (strcmp(RC.RDB$CONSTRAINT_TYPE, PRIMARY_KEY) == 0 ||
8368+
strcmp(RC.RDB$CONSTRAINT_TYPE, UNIQUE_CNSTRT) == 0)
8369+
{
8370+
// Deactivation of primary/unique key requires check for active foreign keys
8371+
checkIndexReferenced(tdbb, transaction, indexName);
8372+
modifyIndex(tdbb, transaction, indexName, clause->enforced);
8373+
}
8374+
else if (strcmp(RC.RDB$CONSTRAINT_TYPE, FOREIGN_KEY) == 0)
8375+
{
8376+
// Activation of foreign key requires check for active partner which is done on index activation
8377+
// so there is nothing to check here
8378+
modifyIndex(tdbb, transaction, indexName, clause->enforced);
8379+
}
8380+
else if (strcmp(RC.RDB$CONSTRAINT_TYPE, CHECK_CNSTRT) == 0)
8381+
{
8382+
AutoCacheRequest requestHandle(tdbb, drq_m_check_trgs, DYN_REQUESTS);
8383+
8384+
FOR (REQUEST_HANDLE requestHandle TRANSACTION_HANDLE transaction)
8385+
TRG IN RDB$TRIGGERS CROSS
8386+
CHK IN RDB$CHECK_CONSTRAINTS
8387+
WITH TRG.RDB$SCHEMA_NAME EQ name.schema.c_str() AND
8388+
TRG.RDB$RELATION_NAME EQ name.object.c_str() AND
8389+
TRG.RDB$SCHEMA_NAME EQ CHK.RDB$SCHEMA_NAME AND
8390+
TRG.RDB$TRIGGER_NAME EQ CHK.RDB$TRIGGER_NAME AND
8391+
CHK.RDB$CONSTRAINT_NAME EQ clause->name.c_str()
8392+
{
8393+
MODIFY TRG
8394+
TRG.RDB$TRIGGER_INACTIVE = clause->enforced ? FALSE : TRUE;
8395+
END_MODIFY
8396+
}
8397+
END_FOR
8398+
}
8399+
else if (strcmp(RC.RDB$CONSTRAINT_TYPE, NOT_NULL_CNSTRT) == 0)
8400+
{
8401+
AutoRequest requestHandle;
8402+
8403+
FOR (REQUEST_HANDLE requestHandle TRANSACTION_HANDLE transaction)
8404+
CHK IN RDB$CHECK_CONSTRAINTS CROSS
8405+
RF IN RDB$RELATION_FIELDS
8406+
WITH CHK.RDB$CONSTRAINT_NAME EQ clause->name.c_str() AND
8407+
CHK.RDB$SCHEMA_NAME EQ RF.RDB$SCHEMA_NAME AND
8408+
CHK.RDB$TRIGGER_NAME EQ RF.RDB$FIELD_NAME AND
8409+
RF.RDB$SCHEMA_NAME EQ name.schema.c_str() AND
8410+
RF.RDB$RELATION_NAME EQ name.object.c_str()
8411+
{
8412+
// Identity column cannot be NULL-able.
8413+
if (RF.RDB$IDENTITY_TYPE.NULL == FALSE)
8414+
{
8415+
fb_utils::exact_name(RF.RDB$FIELD_NAME);
8416+
// msg 274: Identity column @1 of table @2 cannot be changed to NULLable
8417+
status_exception::raise(Arg::PrivateDyn(274) << RF.RDB$FIELD_NAME << name.toQuotedString());
8418+
}
8419+
8420+
// Column of an active primary key cannot be nullable
8421+
AutoRequest request3;
8422+
8423+
FOR (REQUEST_HANDLE request3 TRANSACTION_HANDLE transaction)
8424+
ISG IN RDB$INDEX_SEGMENTS
8425+
CROSS IDX IN RDB$INDICES OVER RDB$SCHEMA_NAME, RDB$INDEX_NAME
8426+
CROSS RC2 IN RDB$RELATION_CONSTRAINTS OVER RDB$SCHEMA_NAME, RDB$INDEX_NAME
8427+
WITH ISG.RDB$FIELD_NAME EQ RF.RDB$FIELD_NAME AND
8428+
IDX.RDB$SCHEMA_NAME EQ name.schema.c_str() AND
8429+
IDX.RDB$RELATION_NAME EQ name.object.c_str() AND
8430+
(IDX.RDB$INDEX_INACTIVE EQ 0 OR IDX.RDB$INDEX_INACTIVE MISSING) AND
8431+
RC2.RDB$CONSTRAINT_TYPE EQ PRIMARY_KEY
8432+
{
8433+
status_exception::raise(Arg::Gds(isc_primary_key_notnull));
8434+
}
8435+
END_FOR
8436+
8437+
// Otherwise it is fine
8438+
MODIFY RF
8439+
if (clause->enforced)
8440+
{
8441+
RF.RDB$NULL_FLAG.NULL = FALSE;
8442+
RF.RDB$NULL_FLAG = TRUE;
8443+
}
8444+
else
8445+
{
8446+
RF.RDB$NULL_FLAG.NULL = TRUE;
8447+
RF.RDB$NULL_FLAG = FALSE; // For symmetry
8448+
}
8449+
END_MODIFY
8450+
}
8451+
END_FOR
8452+
}
8453+
else
8454+
status_exception::raise(Arg::Gds(isc_wish_list) << Arg::Gds(isc_ref_cnstrnt_update));
8455+
}
8456+
END_FOR
8457+
8458+
if (!found)
8459+
{
8460+
// msg 130: "CONSTRAINT %s does not exist."
8461+
status_exception::raise(Arg::PrivateDyn(130) << clause->name);
8462+
}
8463+
8464+
break;
8465+
}
8466+
82958467
case Clause::TYPE_ALTER_SQL_SECURITY:
82968468
{
82978469
executeBeforeTrigger();
@@ -10569,6 +10741,8 @@ void AlterIndexNode::execute(thread_db* tdbb, DsqlCompilerScratch* dsqlScratch,
1056910741

1057010742
executeDdlTrigger(tdbb, dsqlScratch, transaction, DTW_BEFORE, DDL_TRIGGER_ALTER_INDEX, name, {});
1057110743

10744+
checkIndexReferenced(tdbb, transaction, name);
10745+
1057210746
MODIFY IDX
1057310747
IDX.RDB$INDEX_INACTIVE.NULL = FALSE;
1057410748
IDX.RDB$INDEX_INACTIVE = active ? FALSE : TRUE;

src/dsql/DdlNodes.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,7 @@ class RelationNode : public DdlNode
13811381
const char* refDeleteAction;
13821382
Firebird::ObjectsArray<TriggerDefinition> triggers;
13831383
Firebird::ObjectsArray<BlrWriter> blrWritersHolder;
1384+
bool enforced = true;
13841385
};
13851386

13861387
struct CreateDropConstraint
@@ -1405,6 +1406,7 @@ class RelationNode : public DdlNode
14051406
TYPE_ALTER_COL_NULL,
14061407
TYPE_ALTER_COL_POS,
14071408
TYPE_ALTER_COL_TYPE,
1409+
TYPE_ALTER_CONSTRAINT,
14081410
TYPE_DROP_COLUMN,
14091411
TYPE_DROP_CONSTRAINT,
14101412
TYPE_ALTER_SQL_SECURITY,
@@ -1469,6 +1471,7 @@ class RelationNode : public DdlNode
14691471
NestConst<RefActionClause> refAction;
14701472
NestConst<BoolSourceClause> check;
14711473
bool createIfNotExistsOnly = false;
1474+
bool enforced = true;
14721475
};
14731476

14741477
struct IdentityOptions
@@ -1599,6 +1602,18 @@ class RelationNode : public DdlNode
15991602
bool silent = false;
16001603
};
16011604

1605+
struct AlterConstraintClause : public Clause
1606+
{
1607+
explicit AlterConstraintClause(MemoryPool& p)
1608+
: Clause(p, TYPE_ALTER_CONSTRAINT),
1609+
name(p)
1610+
{
1611+
}
1612+
1613+
MetaName name;
1614+
bool enforced = false;
1615+
};
1616+
16021617
RelationNode(MemoryPool& p, RelationSourceNode* aDsqlNode);
16031618

16041619
static bool deleteLocalField(thread_db* tdbb, jrd_tra* transaction,

0 commit comments

Comments
 (0)