Skip to content

Commit d1ed962

Browse files
authored
ddl: supports non-unique global index (#58678)
close #58650
1 parent d966219 commit d1ed962

File tree

12 files changed

+265
-101
lines changed

12 files changed

+265
-101
lines changed

pkg/ddl/executor.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -958,10 +958,6 @@ func checkGlobalIndex(ec errctx.Context, tblInfo *model.TableInfo, indexInfo *mo
958958
// partitioning an index differently from the table partitioning.
959959
return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("Global Index on non-partitioned table")
960960
}
961-
// TODO: remove limitation
962-
if !indexInfo.Unique {
963-
return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("GLOBAL IndexOption on non-unique index")
964-
}
965961
validateGlobalIndexWithGeneratedColumns(ec, tblInfo, indexInfo.Name.O, indexInfo.Columns)
966962
}
967963
return nil
@@ -4534,15 +4530,11 @@ func GetName4AnonymousIndex(t table.Table, colName ast.CIStr, idxName ast.CIStr)
45344530
return indexName
45354531
}
45364532

4537-
func checkCreateUniqueGlobalIndex(ec errctx.Context, tblInfo *model.TableInfo, indexName string, indexColumns []*model.IndexColumn, isUnique bool, isGlobal bool) error {
4533+
func checkCreateGlobalIndex(ec errctx.Context, tblInfo *model.TableInfo, indexName string, indexColumns []*model.IndexColumn, isUnique bool, isGlobal bool) error {
45384534
pi := tblInfo.GetPartitionInfo()
45394535
if isGlobal && pi == nil {
45404536
return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("Global Index on non-partitioned table")
45414537
}
4542-
if isGlobal && !isUnique {
4543-
// TODO: remove this limitation
4544-
return dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs("Global IndexOption on non-unique index")
4545-
}
45464538
if isUnique && pi != nil {
45474539
ck, err := checkPartitionKeysConstraint(tblInfo.GetPartitionInfo(), indexColumns, tblInfo)
45484540
if err != nil {
@@ -4552,6 +4544,8 @@ func checkCreateUniqueGlobalIndex(ec errctx.Context, tblInfo *model.TableInfo, i
45524544
// index columns does not contain all partition columns, must be global
45534545
return dbterror.ErrGlobalIndexNotExplicitlySet.GenWithStackByArgs(indexName)
45544546
}
4547+
}
4548+
if isGlobal {
45554549
validateGlobalIndexWithGeneratedColumns(ec, tblInfo, indexName, indexColumns)
45564550
}
45574551
return nil
@@ -4602,7 +4596,7 @@ func (e *executor) CreatePrimaryKey(ctx sessionctx.Context, ti ast.Ident, indexN
46024596
return err
46034597
}
46044598

4605-
if err = checkCreateUniqueGlobalIndex(ctx.GetSessionVars().StmtCtx.ErrCtx(), tblInfo, "PRIMARY", indexColumns, true, indexOption != nil && indexOption.Global); err != nil {
4599+
if err = checkCreateGlobalIndex(ctx.GetSessionVars().StmtCtx.ErrCtx(), tblInfo, "PRIMARY", indexColumns, true, indexOption != nil && indexOption.Global); err != nil {
46064600
return err
46074601
}
46084602

@@ -4871,7 +4865,7 @@ func (e *executor) createIndex(ctx sessionctx.Context, ti ast.Ident, keyType ast
48714865
return errors.Trace(err)
48724866
}
48734867

4874-
if err = checkCreateUniqueGlobalIndex(ctx.GetSessionVars().StmtCtx.ErrCtx(), tblInfo, indexName.O, indexColumns, unique, indexOption != nil && indexOption.Global); err != nil {
4868+
if err = checkCreateGlobalIndex(ctx.GetSessionVars().StmtCtx.ErrCtx(), tblInfo, indexName.O, indexColumns, unique, indexOption != nil && indexOption.Global); err != nil {
48754869
return err
48764870
}
48774871

pkg/ddl/index_modify_test.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,39 @@ func TestAddGlobalIndex(t *testing.T) {
749749

750750
require.NoError(t, txn.Commit(context.Background()))
751751

752+
// Test add non-unqiue global index
753+
tk.MustExec("drop table if exists test_t2")
754+
tk.MustExec("create table test_t2 (a int, b int) partition by range (b)" +
755+
" (partition p0 values less than (10), " +
756+
" partition p1 values less than (maxvalue));")
757+
tk.MustExec("insert test_t2 values (2, 1)")
758+
tk.MustExec("alter table test_t2 add key p_a (a) global")
759+
tk.MustExec("insert test_t2 values (1, 11)")
760+
tbl = external.GetTableByName(t, tk, "test", "test_t2")
761+
tblInfo = tbl.Meta()
762+
indexInfo = tblInfo.FindIndexByName("p_a")
763+
require.NotNil(t, indexInfo)
764+
require.True(t, indexInfo.Global)
765+
require.False(t, indexInfo.Unique)
766+
767+
require.NoError(t, sessiontxn.NewTxn(context.Background(), tk.Session()))
768+
txn, err = tk.Session().Txn(true)
769+
require.NoError(t, err)
770+
771+
// check row 1
772+
pid = tblInfo.Partition.Definitions[0].ID
773+
idxVals = []types.Datum{types.NewDatum(2)}
774+
rowVals = []types.Datum{types.NewDatum(2), types.NewDatum(1)}
775+
checkGlobalIndexRow(t, tk.Session(), tblInfo, indexInfo, pid, idxVals, rowVals)
776+
777+
// check row 2
778+
pid = tblInfo.Partition.Definitions[1].ID
779+
idxVals = []types.Datum{types.NewDatum(1)}
780+
rowVals = []types.Datum{types.NewDatum(1), types.NewDatum(11)}
781+
checkGlobalIndexRow(t, tk.Session(), tblInfo, indexInfo, pid, idxVals, rowVals)
782+
783+
require.NoError(t, txn.Commit(context.Background()))
784+
752785
// `sanity_check.go` will check the del_range numbers are correct or not.
753786
// normal index
754787
tk.MustExec("drop table if exists t")
@@ -801,7 +834,17 @@ func checkGlobalIndexRow(
801834
require.NoError(t, err)
802835
key := tablecodec.EncodeIndexSeekKey(tblInfo.ID, indexInfo.ID, encodedValue)
803836
require.NoError(t, err)
804-
value, err := txn.Get(context.Background(), key)
837+
var value []byte
838+
if indexInfo.Unique {
839+
value, err = txn.Get(context.Background(), key)
840+
} else {
841+
var iter kv.Iterator
842+
iter, err = txn.Iter(key, key.PrefixNext())
843+
require.NoError(t, err)
844+
require.True(t, iter.Valid())
845+
key = iter.Key()
846+
value = iter.Value()
847+
}
805848
require.NoError(t, err)
806849
idxColInfos := tables.BuildRowcodecColInfoForIndexColumns(indexInfo, tblInfo)
807850
colVals, err := tablecodec.DecodeIndexKV(key, value, len(indexInfo.Columns), tablecodec.HandleDefault, idxColInfos)

pkg/ddl/ingest/integration_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,15 @@ func TestAddGlobalIndexInIngest(t *testing.T) {
494494
require.Greater(t, len(rsGlobalIndex1.Rows()), len(rsGlobalIndex.Rows()))
495495
require.Equal(t, rsGlobalIndex1.String(), rsTable.String())
496496
require.Equal(t, rsGlobalIndex1.String(), rsGlobalIndex2.String())
497+
498+
// for non-unique global idnexes
499+
tk.MustExec("alter table t add index idx_7(b) global, add index idx_8(b) global")
500+
rsNonUniqueGlobalIndex1 := tk.MustQuery("select * from t use index(idx_7)").Sort()
501+
rsTable = tk.MustQuery("select * from t use index()").Sort()
502+
rsNonUniqueGlobalIndex2 := tk.MustQuery("select * from t use index(idx_8)").Sort()
503+
require.Greater(t, len(rsNonUniqueGlobalIndex1.Rows()), len(rsGlobalIndex.Rows()))
504+
require.Equal(t, rsNonUniqueGlobalIndex1.String(), rsTable.String())
505+
require.Equal(t, rsNonUniqueGlobalIndex1.String(), rsNonUniqueGlobalIndex2.String())
497506
}
498507

499508
func TestAddGlobalIndexInIngestWithUpdate(t *testing.T) {

pkg/ddl/partition.go

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3233,28 +3233,18 @@ func (w *worker) onReorganizePartition(jobCtx *jobContext, job *model.Job) (ver
32333233
// When removing partitioning, set all indexes to 'local' since it will become a non-partitioned table!
32343234
newGlobal = false
32353235
}
3236-
if !index.Unique {
3237-
// for now, only unique index can be global, non-unique indexes are 'local'
3238-
// TODO: For the future loosen this restriction and allow non-unique global indexes
3239-
if newGlobal {
3240-
job.State = model.JobStateCancelled
3241-
return ver, dbterror.ErrGeneralUnsupportedDDL.GenWithStackByArgs(fmt.Sprintf("PARTITION BY, index '%v' is not unique, but has Global Index set", index.Name.O))
3242-
}
3236+
if !index.Global && !newGlobal {
32433237
continue
32443238
}
32453239
inAllPartitionColumns, err := checkPartitionKeysConstraint(partInfo, index.Columns, tblInfo)
32463240
if err != nil {
32473241
return ver, errors.Trace(err)
32483242
}
3249-
// Currently only support Explicit Global indexes.
3250-
if !inAllPartitionColumns && !newGlobal {
3243+
// Currently only support Explicit Global indexes for unique index.
3244+
if !inAllPartitionColumns && !newGlobal && index.Unique {
32513245
job.State = model.JobStateCancelled
32523246
return ver, dbterror.ErrGlobalIndexNotExplicitlySet.GenWithStackByArgs(index.Name.O)
32533247
}
3254-
if !index.Global && !newGlobal {
3255-
// still local index, no need to duplicate index.
3256-
continue
3257-
}
32583248
if tblInfo.Partition.DDLChangedIndex == nil {
32593249
tblInfo.Partition.DDLChangedIndex = make(map[int64]bool)
32603250
}
@@ -3389,7 +3379,7 @@ func (w *worker) onReorganizePartition(jobCtx *jobContext, job *model.Job) (ver
33893379
}
33903380

33913381
for i := range tblInfo.Indices {
3392-
if tblInfo.Indices[i].Unique && tblInfo.Indices[i].State == model.StateDeleteOnly {
3382+
if tblInfo.Indices[i].State == model.StateDeleteOnly {
33933383
tblInfo.Indices[i].State = model.StateWriteOnly
33943384
}
33953385
}
@@ -3409,7 +3399,7 @@ func (w *worker) onReorganizePartition(jobCtx *jobContext, job *model.Job) (ver
34093399
// so that new data will be updated in both old and new partitions when reorganizing.
34103400
job.SnapshotVer = 0
34113401
for i := range tblInfo.Indices {
3412-
if tblInfo.Indices[i].Unique && tblInfo.Indices[i].State == model.StateWriteOnly {
3402+
if tblInfo.Indices[i].State == model.StateWriteOnly {
34133403
tblInfo.Indices[i].State = model.StateWriteReorganization
34143404
}
34153405
}
@@ -3451,9 +3441,6 @@ func (w *worker) onReorganizePartition(jobCtx *jobContext, job *model.Job) (ver
34513441
})
34523442

34533443
for _, index := range tblInfo.Indices {
3454-
if !index.Unique {
3455-
continue
3456-
}
34573444
isNew, ok := tblInfo.Partition.DDLChangedIndex[index.ID]
34583445
if !ok {
34593446
continue
@@ -3553,8 +3540,8 @@ func (w *worker) onReorganizePartition(jobCtx *jobContext, job *model.Job) (ver
35533540

35543541
var dropIndices []*model.IndexInfo
35553542
for _, indexInfo := range tblInfo.Indices {
3556-
if indexInfo.Unique && indexInfo.State == model.StateDeleteOnly {
3557-
// Drop the old unique (possible global) index, see onDropIndex
3543+
if indexInfo.State == model.StateDeleteOnly {
3544+
// Drop the old indexes, see onDropIndex
35583545
indexInfo.State = model.StateNone
35593546
DropIndexColumnFlag(tblInfo, indexInfo)
35603547
RemoveDependentHiddenColumns(tblInfo, indexInfo)

tests/integrationtest/r/globalindex/ddl.result

Lines changed: 93 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,11 @@ Error 8200 (HY000): Unsupported Global Index on non-partitioned table
55
create table t (a int, b int, unique index idx(a) global) partition by hash(b) partitions 3;
66
drop table t;
77
create table t (a int, b int, index idx(a) global) partition by hash(b) partitions 3;
8-
Error 8200 (HY000): Unsupported GLOBAL IndexOption on non-unique index
8+
drop table t;
99
create table t3(a int not null, b int, primary key(a) nonclustered, unique idx_b(b) global) partition by hash(a) partitions 3;
1010
drop table t3;
1111
create table t (a int primary key nonclustered, b int) partition by hash(b) partitions 3;
1212
Error 8264 (HY000): Global Index is needed for index 'PRIMARY', since the unique index is not including all partitioning columns, and GLOBAL is not given as IndexOption
13-
create table t (a int, b int, unique key (a)) partition by hash(a) partitions 3;
14-
alter table t partition by hash(b) partitions 3;
15-
Error 8264 (HY000): Global Index is needed for index 'a', since the unique index is not including all partitioning columns, and GLOBAL is not given as IndexOption
16-
alter table t partition by hash(b) partitions 3 update indexes (a global);
17-
alter table t add index idxErr (b) global;
18-
Error 8200 (HY000): Unsupported Global IndexOption on non-unique index
19-
alter table t add unique index idxOK (b) global;
20-
create index idxErr on t (b) global;
21-
Error 8200 (HY000): Unsupported Global IndexOption on non-unique index
22-
create unique index idxOK2 on t (b) global;
23-
alter table t remove partitioning;
24-
alter table t add index idxErr (b) global;
25-
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
26-
alter table t add unique index idxErr (b) global;
27-
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
28-
create index idxErr on t (b) global;
29-
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
30-
create unique index idxErr on t (b) global;
31-
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
32-
drop table t;
33-
create table t (a int, b int, unique index idx(a) global);
34-
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
35-
create table t (a int, b int, index idx(a) global);
36-
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
37-
create table t (a int, b int, index idx(a) global) partition by hash(b) partitions 3;
38-
Error 8200 (HY000): Unsupported GLOBAL IndexOption on non-unique index
39-
create table t (a int not null, b int, primary key(a) nonclustered, unique idx_b(b) global) partition by hash(a) partitions 3;
40-
drop table t;
4113
create table t (a int key global, b int) partition by hash(b) partitions 3;
4214
Error 8200 (HY000): Unsupported create an index that is both a global index and a clustered index
4315
create table t (a int unique, b int) partition by hash(b) partitions 3;
@@ -65,14 +37,34 @@ create table t (a int, b int, unique key (a)) partition by hash(a) partitions 3;
6537
alter table t partition by hash(b) partitions 3;
6638
Error 8264 (HY000): Global Index is needed for index 'a', since the unique index is not including all partitioning columns, and GLOBAL is not given as IndexOption
6739
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a GLOBAL);
40+
alter table t add index idxOK (b) global;
41+
alter table t add unique index idxOK2 (a) global;
42+
alter table t add unique index idxOK3 (b) global;
43+
create index idxOK4 on t (b) global;
44+
create unique index idxOK5 on t (a) global;
45+
create unique index idxOK6 on t (b) global;
46+
alter table t remove partitioning;
47+
show create table t;
48+
Table Create Table
49+
t CREATE TABLE `t` (
50+
`a` int DEFAULT NULL,
51+
`b` int DEFAULT NULL,
52+
UNIQUE KEY `a` (`a`),
53+
KEY `idxOK` (`b`),
54+
UNIQUE KEY `idxOK2` (`a`),
55+
UNIQUE KEY `idxOK3` (`b`),
56+
KEY `idxOK4` (`b`),
57+
UNIQUE KEY `idxOK5` (`a`),
58+
UNIQUE KEY `idxOK6` (`b`)
59+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
6860
alter table t add index idxErr (b) global;
69-
Error 8200 (HY000): Unsupported Global IndexOption on non-unique index
70-
alter table t add unique index idxOK (a) global;
71-
alter table t add unique index idxOK2 (b) global;
61+
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
62+
alter table t add unique index idxErr (b) global;
63+
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
7264
create index idxErr on t (b) global;
73-
Error 8200 (HY000): Unsupported Global IndexOption on non-unique index
74-
create unique index idxOK3 on t (a) global;
75-
create unique index idxOK4 on t (b) global;
65+
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
66+
create unique index idxErr on t (b) global;
67+
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
7668
drop table t;
7769
create table t(a int, b int, primary key (a) nonclustered global);
7870
Error 8200 (HY000): Unsupported Global Index on non-partitioned table
@@ -82,6 +74,72 @@ create table t(a int, b int, primary key (a) global) partition by hash(a) partit
8274
Error 8200 (HY000): Unsupported create an index that is both a global index and a clustered index
8375
create table t(a int, b int, primary key (b) global) partition by hash(a) partitions 5;
8476
Error 8200 (HY000): Unsupported create an index that is both a global index and a clustered index
77+
create table t(a int, b int, key(a), key(b)) partition by hash(a) partitions 4;
78+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a GLOBAL, b LOCAL);
79+
show create table t;
80+
Table Create Table
81+
t CREATE TABLE `t` (
82+
`a` int DEFAULT NULL,
83+
`b` int DEFAULT NULL,
84+
KEY `b` (`b`),
85+
KEY `a` (`a`) /*T![global_index] GLOBAL */
86+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
87+
PARTITION BY HASH (`b`) PARTITIONS 3
88+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a GLOBAL, b GLOBAL);
89+
show create table t;
90+
Table Create Table
91+
t CREATE TABLE `t` (
92+
`a` int DEFAULT NULL,
93+
`b` int DEFAULT NULL,
94+
KEY `b` (`b`) /*T![global_index] GLOBAL */,
95+
KEY `a` (`a`) /*T![global_index] GLOBAL */
96+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
97+
PARTITION BY HASH (`b`) PARTITIONS 3
98+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a LOCAL);
99+
show create table t;
100+
Table Create Table
101+
t CREATE TABLE `t` (
102+
`a` int DEFAULT NULL,
103+
`b` int DEFAULT NULL,
104+
KEY `b` (`b`) /*T![global_index] GLOBAL */,
105+
KEY `a` (`a`)
106+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
107+
PARTITION BY HASH (`b`) PARTITIONS 3
108+
drop table t;
109+
create table t(a int, b int, unique key(a), unique key(b) global) partition by hash(a) partitions 4;
110+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a GLOBAL, b LOCAL);
111+
show create table t;
112+
Table Create Table
113+
t CREATE TABLE `t` (
114+
`a` int DEFAULT NULL,
115+
`b` int DEFAULT NULL,
116+
UNIQUE KEY `a` (`a`) /*T![global_index] GLOBAL */,
117+
UNIQUE KEY `b` (`b`)
118+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
119+
PARTITION BY HASH (`b`) PARTITIONS 3
120+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a GLOBAL, b GLOBAL);
121+
show create table t;
122+
Table Create Table
123+
t CREATE TABLE `t` (
124+
`a` int DEFAULT NULL,
125+
`b` int DEFAULT NULL,
126+
UNIQUE KEY `a` (`a`) /*T![global_index] GLOBAL */,
127+
UNIQUE KEY `b` (`b`) /*T![global_index] GLOBAL */
128+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
129+
PARTITION BY HASH (`b`) PARTITIONS 3
130+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (a LOCAL);
131+
Error 8264 (HY000): Global Index is needed for index 'a', since the unique index is not including all partitioning columns, and GLOBAL is not given as IndexOption
132+
alter table t partition by hash(b) partitions 3 UPDATE INDEXES (b LOCAL);
133+
show create table t;
134+
Table Create Table
135+
t CREATE TABLE `t` (
136+
`a` int DEFAULT NULL,
137+
`b` int DEFAULT NULL,
138+
UNIQUE KEY `a` (`a`) /*T![global_index] GLOBAL */,
139+
UNIQUE KEY `b` (`b`)
140+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
141+
PARTITION BY HASH (`b`) PARTITIONS 3
142+
drop table t;
85143
create table t(a int, b int);
86144
alter table t add primary key (a) global;
87145
Error 8200 (HY000): Unsupported Global Index on non-partitioned table

tests/integrationtest/r/globalindex/expression_index.result

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,13 @@ Projection 3333.33 root NULL globalindex__expression_index.t.a, globalindex__exp
4040
select * from t partition(p0) use index(idx) where lower(b) > 'c';
4141
a b
4242
5 x
43+
drop table if exists t;
44+
CREATE TABLE `t` (
45+
`a` int DEFAULT NULL,
46+
`b` char DEFAULT NULL,
47+
KEY `idx` ((lower(`b`))) global
48+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
49+
PARTITION BY HASH (`a`) PARTITIONS 5;
50+
show warnings;
51+
Level Code Message
52+
Warning 8265 Auto analyze is not effective for index 'idx', need analyze manually

tests/integrationtest/r/globalindex/insert.result

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,20 @@ select * from t use index (idx1) order by a desc;
1010
a b
1111
2 5
1212
1 3
13+
drop table if exists t;
14+
create table t(a int, b int, index idx(a) global) partition by hash(b) partitions 5;
15+
insert into t values (1, 1), (1, 2), (2, 2);
16+
select * from t use index (idx);
17+
a b
18+
1 1
19+
1 2
20+
2 2
21+
alter table t add index idx1(b) global;
22+
insert into t values (2, 4), (3, 4);
23+
select * from t use index (idx1) order by a desc, b;
24+
a b
25+
3 4
26+
2 2
27+
2 4
28+
1 1
29+
1 2

0 commit comments

Comments
 (0)