constraints: add constraint / drop constraint on existing columns (ADR-0029 §2.2)

Adds the two commands for modifying a column's constraints after
creation, completing ADR-0029's §2.2 surface.

Grammar (dsl/grammar/ddl.rs): `add constraint <constraint> to
<T>.<col>` reuses the §2.1 COLUMN_CONSTRAINT choice; `drop
constraint <kind> from <T>.<col>` names only the kind. Both join
the `add` / `drop` choices, discriminated by the `constraint`
form word.

AST (dsl/command.rs): `Command::AddConstraint` / `DropConstraint`
plus the `Constraint` / `ConstraintKind` enums.

Worker (db.rs): `do_add_constraint` / `do_drop_constraint` apply
the change through the rebuild-table primitive. `add` runs the §5
dry-run first — `not null` / `unique` / `check` against a
populated column are refused, before any write, with a
pretty-printed table of offending rows. §9 redundant-on-PK
declarations and §6 `default` on an auto-generated column are
friendly refusals; dropping a constraint the column does not
carry is likewise refused.

Also fixes schema_to_ddl, which suppressed UNIQUE for every PK
column — a compound-PK member is not individually unique, so an
explicit UNIQUE on it must survive the rebuild.

23 tests added (6 grammar, 17 worker); 3 completion-test and 3
matrix snapshots updated for the new `constraint` subcommand.
This commit is contained in:
claude@clouddev1
2026-05-19 18:31:57 +00:00
parent 102dff08c4
commit abce1188f2
14 changed files with 1360 additions and 29 deletions
+10
View File
@@ -1421,6 +1421,16 @@ impl App {
RelationshipSelector::Named { .. } => (Operation::DropRelationship, None, None),
},
C::AddIndex { table, .. } => (Operation::AddIndex, Some(table.as_str()), None),
C::AddConstraint { table, column, .. } => (
Operation::AddConstraint,
Some(table.as_str()),
Some(column.as_str()),
),
C::DropConstraint { table, column, .. } => (
Operation::DropConstraint,
Some(table.as_str()),
Some(column.as_str()),
),
C::DropIndex { selector } => match selector {
IndexSelector::Columns { table, .. } => {
(Operation::DropIndex, Some(table.as_str()), None)
+19 -10
View File
@@ -844,17 +844,19 @@ mod tests {
fn multi_candidate_position_offers_add_subcommands() {
// After `add ` the parser expects `column` (for
// `add column ...`), `index` (for `add index ...`,
// ADR-0025), and `1` (the opener for
// ADR-0025), `constraint` (for `add constraint ...`,
// ADR-0029 §2.2), and `1` (the opener for
// `add 1:n relationship ...`). The completion engine
// sections keyword candidates (`column`, `index`)
// ahead of the `1:n` composite literal, so the literal
// sorts last even though `add 1:n` is declared second.
// sections keyword candidates ahead of the `1:n`
// composite literal, so the literal sorts last even
// though `add 1:n` is declared second.
let cs = cands("add ", 4);
assert_eq!(
cs,
vec![
"column".to_string(),
"index".to_string(),
"constraint".to_string(),
"1:n".to_string(),
]
);
@@ -1113,10 +1115,11 @@ mod tests {
}
#[test]
fn drop_offers_all_four_subcommands() {
fn drop_offers_all_five_subcommands() {
// `drop` branches: column / relationship / table / index
// (ADR-0025). Candidates follow grammar declaration
// order, so `index` — added last — appears last.
// (ADR-0025) / constraint (ADR-0029 §2.2). Candidates
// follow grammar declaration order, so `constraint` —
// added last — appears last.
let cs = cands("drop ", 5);
assert_eq!(
cs,
@@ -1125,6 +1128,7 @@ mod tests {
"relationship".to_string(),
"table".to_string(),
"index".to_string(),
"constraint".to_string(),
],
);
}
@@ -1686,15 +1690,20 @@ mod tests {
c.sort_by(|a, b| a.text.cmp(&b.text));
c
}
// `add ` exposes `column`, `1:n` and `index` — the
// alphabetic ranker reorders them.
// `add ` exposes `column`, `1:n`, `index` and
// `constraint` — the alphabetic ranker reorders them.
let cache = SchemaCache::default();
let comp = candidates_at_cursor_with("add ", 4, &cache, alphabetic_ranker)
.expect("some completion");
let texts: Vec<String> = comp.candidates.into_iter().map(|c| c.text).collect();
assert_eq!(
texts,
vec!["1:n".to_string(), "column".to_string(), "index".to_string()]
vec![
"1:n".to_string(),
"column".to_string(),
"constraint".to_string(),
"index".to_string(),
]
);
}
+953 -12
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -53,6 +53,60 @@ impl ColumnSpec {
}
}
/// A column-level constraint with its payload (ADR-0029 §3).
///
/// Produced by `add constraint <constraint> to <T>.<col>`.
/// `Default` / `Check` carry the value / expression; `NotNull`
/// and `Unique` are payload-free.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Constraint {
NotNull,
Unique,
Default(Value),
Check(Expr),
}
impl Constraint {
/// The bare constraint kind, dropping any payload — used for
/// the `[ok]` summary line and log output.
#[must_use]
pub const fn kind(&self) -> ConstraintKind {
match self {
Self::NotNull => ConstraintKind::NotNull,
Self::Unique => ConstraintKind::Unique,
Self::Default(_) => ConstraintKind::Default,
Self::Check(_) => ConstraintKind::Check,
}
}
}
/// The kind of a column-level constraint, without a payload.
///
/// Produced by `drop constraint <kind> from <T>.<col>`
/// (ADR-0029 §3) — naming the kind is enough, since at most one
/// constraint of each kind exists per column.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConstraintKind {
NotNull,
Unique,
Default,
Check,
}
impl ConstraintKind {
/// Upper-case SQL-style label for user-facing messages
/// (`NOT NULL`, `UNIQUE`, `DEFAULT`, `CHECK`).
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::NotNull => "NOT NULL",
Self::Unique => "UNIQUE",
Self::Default => "DEFAULT",
Self::Check => "CHECK",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
CreateTable {
@@ -150,6 +204,22 @@ pub enum Command {
DropIndex {
selector: IndexSelector,
},
/// Add a column-level constraint to an existing column
/// (ADR-0029 §2.2). Applied through the rebuild-table
/// primitive after a §5 dry-run guards populated columns.
AddConstraint {
table: String,
column: String,
constraint: Constraint,
},
/// Remove a column-level constraint from an existing column
/// (ADR-0029 §2.2). Naming the `kind` is enough — at most
/// one constraint of each kind exists per column.
DropConstraint {
table: String,
column: String,
kind: ConstraintKind,
},
/// Re-display a table's structure in the output. Doesn't
/// change schema; useful when the user wants to look at a
/// table they aren't currently DDL'ing on.
@@ -498,6 +568,8 @@ impl Command {
Self::DropRelationship { .. } => "drop relationship",
Self::AddIndex { .. } => "add index",
Self::DropIndex { .. } => "drop index",
Self::AddConstraint { .. } => "add constraint",
Self::DropConstraint { .. } => "drop constraint",
Self::ShowTable { .. } => "show table",
Self::Insert { .. } => "insert into",
Self::Update { .. } => "update",
@@ -536,6 +608,8 @@ impl Command {
| Self::DropColumn { table, .. }
| Self::RenameColumn { table, .. }
| Self::ChangeColumnType { table, .. }
| Self::AddConstraint { table, .. }
| Self::DropConstraint { table, .. }
| Self::Insert { table, .. }
| Self::Update { table, .. }
| Self::Delete { table, .. } => table,
@@ -598,6 +672,12 @@ impl Command {
"from {parent_table}.{parent_column} to {child_table}.{child_column}"
),
},
// A constraint command's subject is the dotted
// `<table>.<column>` it acts on (ADR-0029 §2.2).
Self::AddConstraint { table, column, .. }
| Self::DropConstraint { table, column, .. } => {
format!("{table}.{column}")
}
_ => self.target_table().to_string(),
}
}
+212 -7
View File
@@ -13,7 +13,8 @@
use crate::dsl::action::ReferentialAction;
use crate::dsl::command::{
ChangeColumnMode, ColumnSpec, Command, Expr, IndexSelector, RelationshipSelector,
ChangeColumnMode, ColumnSpec, Command, Constraint, ConstraintKind, Expr, IndexSelector,
RelationshipSelector,
};
use crate::dsl::value::Value;
use crate::dsl::grammar::{
@@ -283,7 +284,8 @@ const DROP_INDEX: Node = Node::Seq(DROP_INDEX_NODES);
// drop entry — `drop (table|column|relationship|index) ...`
// =================================================================
const DROP_CHOICES: &[Node] = &[DROP_COLUMN, DROP_RELATIONSHIP, DROP_TABLE, DROP_INDEX];
const DROP_CHOICES: &[Node] =
&[DROP_COLUMN, DROP_RELATIONSHIP, DROP_TABLE, DROP_INDEX, DROP_CONSTRAINT];
const DROP_SHAPE: Node = Node::Choice(DROP_CHOICES);
// =================================================================
@@ -410,7 +412,7 @@ const ADD_INDEX: Node = Node::Seq(ADD_INDEX_NODES);
// add entry — `add (column|1:n relationship|index) …`
// =================================================================
const ADD_CHOICES: &[Node] = &[ADD_COLUMN, ADD_RELATIONSHIP, ADD_INDEX];
const ADD_CHOICES: &[Node] = &[ADD_COLUMN, ADD_RELATIONSHIP, ADD_INDEX, ADD_CONSTRAINT];
const ADD_SHAPE: Node = Node::Choice(ADD_CHOICES);
// =================================================================
@@ -564,6 +566,7 @@ fn build_drop(path: &MatchedPath) -> Result<Command, ValidationError> {
})
}
}
Some("constraint") => build_drop_constraint(path),
Some("relationship") => {
// Endpoints form has `from` as the third Word.
let has_from = path
@@ -634,6 +637,7 @@ fn build_add(path: &MatchedPath) -> Result<Command, ValidationError> {
table: require_ident(path, "table_name")?,
columns: collect_idents(path, "column_name"),
}),
Some("constraint") => build_add_constraint(path),
_ => Err(ValidationError {
message_key: "parse.error_wrapper",
args: vec![("detail", "unknown add subcommand".to_string())],
@@ -764,6 +768,72 @@ fn build_change_column(path: &MatchedPath) -> Result<Command, ValidationError> {
})
}
/// Build an `add constraint <constraint> to <T>.<col>` command
/// (ADR-0029 §2.2). The `<constraint>` reuses the §2.1
/// `COLUMN_CONSTRAINT` Choice, so exactly one of the four
/// constraint kinds is matched; `collect_column_constraints`
/// recovers it. The §9 redundancy and §5 dry-run checks are
/// execution-time (the parser has no schema) and live in the
/// database worker.
fn build_add_constraint(path: &MatchedPath) -> Result<Command, ValidationError> {
let (not_null, unique, default, check) = collect_column_constraints(path)?;
let constraint = if not_null {
Constraint::NotNull
} else if unique {
Constraint::Unique
} else if let Some(value) = default {
Constraint::Default(value)
} else if let Some(expr) = check {
Constraint::Check(expr)
} else {
return Err(ValidationError {
message_key: "parse.error_wrapper",
args: vec![("detail", "add constraint needs a constraint".to_string())],
});
};
Ok(Command::AddConstraint {
table: require_ident(path, "table_name")?,
column: require_ident(path, "column_name")?,
constraint,
})
}
/// Build a `drop constraint <kind> from <T>.<col>` command
/// (ADR-0029 §2.2). `drop` names only the kind — the
/// `DROP_CONSTRAINT_KIND` Choice is payload-free, so the kind
/// is recovered from which keyword(s) the path matched.
fn build_drop_constraint(path: &MatchedPath) -> Result<Command, ValidationError> {
let words: Vec<&'static str> = path
.items
.iter()
.filter_map(|i| match &i.kind {
MatchedKind::Word(w) => Some(*w),
_ => None,
})
.collect();
// `not` appears only in the `not null` Seq, so its presence
// alone identifies the kind.
let kind = if words.contains(&"not") {
ConstraintKind::NotNull
} else if words.contains(&"unique") {
ConstraintKind::Unique
} else if words.contains(&"default") {
ConstraintKind::Default
} else if words.contains(&"check") {
ConstraintKind::Check
} else {
return Err(ValidationError {
message_key: "parse.error_wrapper",
args: vec![("detail", "drop constraint needs a constraint kind".to_string())],
});
};
Ok(Command::DropConstraint {
table: require_ident(path, "table_name")?,
column: require_ident(path, "column_name")?,
kind,
})
}
// =================================================================
// CommandNodes
// =================================================================
@@ -778,6 +848,7 @@ pub static DROP: CommandNode = CommandNode {
"parse.usage.drop_column",
"parse.usage.drop_relationship",
"parse.usage.drop_index",
"parse.usage.drop_constraint",
],};
pub static ADD: CommandNode = CommandNode {
@@ -789,6 +860,7 @@ pub static ADD: CommandNode = CommandNode {
"parse.usage.add_column",
"parse.usage.add_relationship",
"parse.usage.add_index",
"parse.usage.add_constraint",
],};
pub static RENAME: CommandNode = CommandNode {
@@ -825,9 +897,10 @@ const COL_NAME: Node = Node::Hinted {
};
// ADR-0029 column-constraint suffix — `not null`, `unique`,
// `default <literal>`. (`check (<expr>)` joins in a later
// ADR-0029 step.) One shared fragment: `create table` uses it
// here; `add column` and `add constraint` reuse it later.
// `default <literal>`, `check (<expr>)`. One shared fragment:
// `create table` uses it here, `add column` reuses it as its
// type suffix, and `add constraint` reuses the individual
// `COLUMN_CONSTRAINT` Choice for its constraint slot.
const NOT_NULL_NODES: &[Node] = &[
Node::Word(Word::keyword("not")),
Node::Word(Word::keyword("null")),
@@ -867,6 +940,53 @@ const COLUMN_CONSTRAINT_SUFFIX: Node = Node::Repeated {
min: 0,
};
// =================================================================
// add_constraint / drop_constraint — `add constraint <constraint>
// to <T>.<col>` / `drop constraint <kind> from <T>.<col>`
// (ADR-0029 §2.2)
// =================================================================
// Payload-free keyword nodes for `drop constraint` — naming the
// kind is enough, since at most one constraint of each kind
// exists per column. `not null` / `unique` reuse the §2.1
// keyword-only nodes; `default` / `check` need bare-keyword
// variants here (their §2.1 forms carry a literal / expression
// payload that `drop` does not take).
const DROP_DEFAULT_KEYWORD: Node = Node::Word(Word::keyword("default"));
const DROP_CHECK_KEYWORD: Node = Node::Word(Word::keyword("check"));
const DROP_CONSTRAINT_KIND_CHOICES: &[Node] = &[
NOT_NULL_CONSTRAINT,
UNIQUE_CONSTRAINT,
DROP_DEFAULT_KEYWORD,
DROP_CHECK_KEYWORD,
];
const DROP_CONSTRAINT_KIND: Node = Node::Choice(DROP_CONSTRAINT_KIND_CHOICES);
// The dotted `<Table>.<column>` target — the same `Ident '.'
// Ident` shape `add 1:n relationship` uses for its endpoints.
// `writes_table: true` on the table ident (via `TABLE_NAME_
// EXISTING`) narrows the `.<column>` slot's completion
// candidates to that table's columns.
const CONSTRAINT_TARGET_NODES: &[Node] =
&[TABLE_NAME_EXISTING, Node::Punct('.'), COLUMN_NAME];
const CONSTRAINT_TARGET: Node = Node::Seq(CONSTRAINT_TARGET_NODES);
const ADD_CONSTRAINT_NODES: &[Node] = &[
Node::Word(Word::keyword("constraint")),
COLUMN_CONSTRAINT,
Node::Word(Word::keyword("to")),
CONSTRAINT_TARGET,
];
const ADD_CONSTRAINT: Node = Node::Seq(ADD_CONSTRAINT_NODES);
const DROP_CONSTRAINT_NODES: &[Node] = &[
Node::Word(Word::keyword("constraint")),
DROP_CONSTRAINT_KIND,
Node::Word(Word::keyword("from")),
CONSTRAINT_TARGET,
];
const DROP_CONSTRAINT: Node = Node::Seq(DROP_CONSTRAINT_NODES);
const COL_SPEC_NODES: &[Node] = &[
COL_NAME,
Node::Punct('('),
@@ -1117,7 +1237,7 @@ pub static CREATE: CommandNode = CommandNode {
#[cfg(test)]
mod constraint_tests {
use super::Command;
use super::{Command, Constraint, ConstraintKind};
use crate::dsl::command::ColumnSpec;
use crate::dsl::parser::parse_command;
use crate::dsl::value::Value;
@@ -1234,4 +1354,89 @@ mod constraint_tests {
);
assert!(cols[0].check.is_some());
}
// --- `add constraint` / `drop constraint` (ADR-0029 §2.2) ---
#[test]
fn add_constraint_not_null_parses() {
match parse_command("add constraint not null to Users.email").expect("parse") {
Command::AddConstraint {
table,
column,
constraint,
} => {
assert_eq!(table, "Users");
assert_eq!(column, "email");
assert_eq!(constraint, Constraint::NotNull);
}
other => panic!("expected AddConstraint, got {other:?}"),
}
}
#[test]
fn add_constraint_unique_parses() {
match parse_command("add constraint unique to Users.email").expect("parse") {
Command::AddConstraint { constraint, .. } => {
assert_eq!(constraint, Constraint::Unique);
}
other => panic!("expected AddConstraint, got {other:?}"),
}
}
#[test]
fn add_constraint_default_parses() {
match parse_command("add constraint default 18 to Users.age").expect("parse") {
Command::AddConstraint { constraint, .. } => {
assert_eq!(
constraint,
Constraint::Default(Value::Number("18".to_string()))
);
}
other => panic!("expected AddConstraint, got {other:?}"),
}
}
#[test]
fn add_constraint_check_parses() {
match parse_command("add constraint check (age >= 0) to Users.age").expect("parse")
{
Command::AddConstraint {
column, constraint, ..
} => {
assert_eq!(column, "age");
assert!(matches!(constraint, Constraint::Check(_)));
}
other => panic!("expected AddConstraint, got {other:?}"),
}
}
#[test]
fn drop_constraint_not_null_parses() {
match parse_command("drop constraint not null from Users.email").expect("parse") {
Command::DropConstraint {
table,
column,
kind,
} => {
assert_eq!(table, "Users");
assert_eq!(column, "email");
assert_eq!(kind, ConstraintKind::NotNull);
}
other => panic!("expected DropConstraint, got {other:?}"),
}
}
#[test]
fn drop_constraint_each_kind_parses() {
for (input, expected) in [
("drop constraint unique from T.c", ConstraintKind::Unique),
("drop constraint default from T.c", ConstraintKind::Default),
("drop constraint check from T.c", ConstraintKind::Check),
] {
match parse_command(input).expect("parse") {
Command::DropConstraint { kind, .. } => assert_eq!(kind, expected),
other => panic!("expected DropConstraint for {input:?}, got {other:?}"),
}
}
}
}
+8
View File
@@ -514,6 +514,14 @@ mod usage_key_tests {
let cases = [
("add column to T: c (int)", "parse.usage.add_column"),
("add index on T (c)", "parse.usage.add_index"),
(
"add constraint unique to T.c",
"parse.usage.add_constraint",
),
(
"drop constraint check from T.c",
"parse.usage.drop_constraint",
),
(
"add 1:n relationship from A.x to B.y",
"parse.usage.add_relationship",
+15
View File
@@ -207,12 +207,14 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
// code, not the catalog, because spacing is alignment-
// sensitive in the multi-entry case.
("parse.usage.add_column", &[]),
("parse.usage.add_constraint", &[]),
("parse.usage.add_index", &[]),
("parse.usage.add_relationship", &[]),
("parse.usage.change_column", &[]),
("parse.usage.create_table", &[]),
("parse.usage.delete", &[]),
("parse.usage.drop_column", &[]),
("parse.usage.drop_constraint", &[]),
("parse.usage.drop_index", &[]),
("parse.usage.drop_relationship", &[]),
("parse.usage.drop_table", &[]),
@@ -385,6 +387,19 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
&["count", "action", "child_table", "rel", "on_delete"],
),
// ---- change-column dry-run diagnostics (per ADR-0017) ----
// ---- add-constraint dry-run diagnostics (per ADR-0029 §5) ----
(
"db.diagnostic.add_check_summary",
&["table", "column", "total", "rule"],
),
(
"db.diagnostic.add_not_null_summary",
&["table", "column", "total"],
),
(
"db.diagnostic.add_unique_summary",
&["table", "column", "total"],
),
("db.diagnostic.force_conversion_hint", &[]),
("db.diagnostic.header_becomes", &[]),
("db.diagnostic.header_from", &[]),
+17
View File
@@ -432,6 +432,7 @@ parse:
drop_index: |-
drop index <Name>
drop index on <Table> (<col>[, ...])
drop_constraint: "drop constraint (not null | unique | default | check) from <Table>.<col>"
add_column: "add column [to] [table] <Table>: <Name> (<Type>)"
add_relationship: |-
add 1:n relationship [as <Name>]
@@ -439,6 +440,11 @@ parse:
[on delete <action>] [on update <action>]
[--create-fk]
add_index: "add index [as <Name>] on <Table> (<col>[, ...])"
add_constraint: |-
add constraint not null to <Table>.<col>
add constraint unique to <Table>.<col>
add constraint default <value> to <Table>.<col>
add constraint check (<expr>) to <Table>.<col>
rename_column: "rename column [in] [table] <Table>: <Old> to <New>"
change_column: |-
change column [in] [table] <Table>: <Name> (<Type>)
@@ -727,6 +733,17 @@ db:
# Follow-up suggestion appended to the lossy diagnostic
# (only — incompatibles can't be force-overridden).
force_conversion_hint: "if you want to execute this conversion in spite of the problems, re-run with `--force-conversion`."
# `add constraint ...` dry-run refusals (ADR-0029 §5).
# Surface when the column's existing rows would violate the
# constraint being added; the offending rows follow in a
# diagnostic table. There is no force override — the user
# fixes the data and retries.
add_not_null_summary: |-
Cannot add NOT NULL to `{table}.{column}`: {total} row(s) hold a null value.
add_unique_summary: |-
Cannot add UNIQUE to `{table}.{column}`: {total} value(s) appear in more than one row.
add_check_summary: |-
Cannot add this CHECK to `{table}.{column}`: {total} row(s) do not satisfy `{rule}`.
# ---- DSL command success summaries (ADR-0019 §9 sweep) --------------
ok:
+4
View File
@@ -67,6 +67,8 @@ pub enum Operation {
DropRelationship,
AddIndex,
DropIndex,
AddConstraint,
DropConstraint,
Query,
Rebuild,
Replay,
@@ -96,6 +98,8 @@ impl Operation {
Self::DropRelationship => "drop relationship",
Self::AddIndex => "add index",
Self::DropIndex => "drop index",
Self::AddConstraint => "add constraint",
Self::DropConstraint => "drop constraint",
Self::Query => "query",
Self::Rebuild => "rebuild",
Self::Replay => "replay",
+16
View File
@@ -1789,6 +1789,22 @@ async fn execute_command_typed(
.drop_index(selector, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::AddConstraint {
table,
column,
constraint,
} => database
.add_constraint(table, column, constraint, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::DropConstraint {
table,
column,
kind,
} => database
.drop_constraint(table, column, kind, src)
.await
.map(|d| CommandOutcome::Schema(Some(d))),
Command::ShowTable { name } => database
.describe_table(name, src)
.await
+2
View File
@@ -208,6 +208,8 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
DropRelationship { .. } => "DropRelationship".into(),
AddIndex { .. } => "AddIndex".into(),
DropIndex { .. } => "DropIndex".into(),
AddConstraint { .. } => "AddConstraint".into(),
DropConstraint { .. } => "DropConstraint".into(),
ShowTable { .. } => "ShowTable".into(),
Insert { .. } => "Insert".into(),
Update { .. } => "Update".into(),
@@ -18,6 +18,10 @@ Assessment {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
Candidate {
text: "1:n",
kind: Keyword,
@@ -42,6 +46,10 @@ Assessment {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
Candidate {
text: "1:n",
kind: Keyword,
@@ -18,6 +18,10 @@ Assessment {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
Candidate {
text: "1:n",
kind: Keyword,
@@ -42,6 +46,10 @@ Assessment {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
Candidate {
text: "1:n",
kind: Keyword,
@@ -26,6 +26,10 @@ Assessment {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
],
selected: None,
},
@@ -54,6 +58,10 @@ Assessment {
text: "index",
kind: Keyword,
},
Candidate {
text: "constraint",
kind: Keyword,
},
],
},
),