feat: field sort plugin in mssql

This commit is contained in:
aaaaaajie 2025-03-30 21:58:42 +08:00
parent 09eda5ffdc
commit d2ff34e17e
5 changed files with 140 additions and 107 deletions

View File

@ -27,6 +27,10 @@ describe('sort action', () => {
return api.destroy(); return api.destroy();
}); });
function parseSortValue(value) {
return api.db.options.dialect === 'mssql' ? value.toString() : value;
}
describe('associations', () => { describe('associations', () => {
let UserCollection: Collection; let UserCollection: Collection;
@ -204,19 +208,19 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't2', title: 't2',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't3', title: 't3',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't1', title: 't1',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't4', title: 't4',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -239,19 +243,19 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't3', title: 't3',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't1', title: 't1',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't2', title: 't2',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't4', title: 't4',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -274,19 +278,19 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't2', title: 't2',
sort2: 1, sort2: parseSortValue(1),
}, },
{ {
title: 't3', title: 't3',
sort2: 2, sort2: parseSortValue(2),
}, },
{ {
title: 't1', title: 't1',
sort2: 3, sort2: parseSortValue(3),
}, },
{ {
title: 't4', title: 't4',
sort2: 4, sort2: parseSortValue(4),
}, },
], ],
}); });
@ -308,19 +312,19 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't3', title: 't3',
sort: 0, sort: parseSortValue(0),
}, },
{ {
title: 't1', title: 't1',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't2', title: 't2',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't4', title: 't4',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -492,15 +496,15 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't12', title: 't12',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't13', title: 't13',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't14', title: 't14',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -517,23 +521,23 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't21', title: 't21',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't11', title: 't11',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't22', title: 't22',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't23', title: 't23',
sort: 4, sort: parseSortValue(4),
}, },
{ {
title: 't24', title: 't24',
sort: 5, sort: parseSortValue(5),
}, },
], ],
}); });
@ -558,15 +562,15 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't12', title: 't12',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't13', title: 't13',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't14', title: 't14',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -581,23 +585,23 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't21', title: 't21',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't22', title: 't22',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't11', title: 't11',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't23', title: 't23',
sort: 4, sort: parseSortValue(4),
}, },
{ {
title: 't24', title: 't24',
sort: 5, sort: parseSortValue(5),
}, },
], ],
}); });
@ -619,23 +623,23 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't11', title: 't11',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't22', title: 't22',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't12', title: 't12',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't13', title: 't13',
sort: 4, sort: parseSortValue(4),
}, },
{ {
title: 't14', title: 't14',
sort: 5, sort: parseSortValue(5),
}, },
], ],
}); });
@ -650,15 +654,15 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't21', title: 't21',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't23', title: 't23',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't24', title: 't24',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -681,23 +685,23 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't11', title: 't11',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't12', title: 't12',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't22', title: 't22',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't13', title: 't13',
sort: 4, sort: parseSortValue(4),
}, },
{ {
title: 't14', title: 't14',
sort: 5, sort: parseSortValue(5),
}, },
], ],
}); });
@ -712,15 +716,15 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't21', title: 't21',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't23', title: 't23',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't24', title: 't24',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -747,15 +751,15 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't12', title: 't12',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't13', title: 't13',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't14', title: 't14',
sort: 4, sort: parseSortValue(4),
}, },
], ],
}); });
@ -770,23 +774,23 @@ describe('sort action', () => {
data: [ data: [
{ {
title: 't21', title: 't21',
sort: 1, sort: parseSortValue(1),
}, },
{ {
title: 't22', title: 't22',
sort: 2, sort: parseSortValue(2),
}, },
{ {
title: 't23', title: 't23',
sort: 3, sort: parseSortValue(3),
}, },
{ {
title: 't24', title: 't24',
sort: 4, sort: parseSortValue(4),
}, },
{ {
title: 't11', title: 't11',
sort: 5, sort: parseSortValue(5),
}, },
], ],
}); });

View File

@ -30,6 +30,10 @@ describe('sort collections', () => {
await app.destroy(); await app.destroy();
}); });
function parseSortValue(value) {
return db.options.dialect === 'mssql' ? value.toString() : value;
}
describe('sort collection', () => { describe('sort collection', () => {
beforeEach(async () => { beforeEach(async () => {
Post = app.db.collection({ Post = app.db.collection({
@ -68,7 +72,7 @@ describe('sort collections', () => {
await db.sync(); await db.sync();
const instance = await model.create(); const instance = await model.create();
expect(model.rawAttributes['sort']).toBeDefined(); expect(model.rawAttributes['sort']).toBeDefined();
expect(instance.get('sort')).toBe(1); expect(instance.get('sort')).toBe(parseSortValue(1));
}); });
test('sortable=string', async () => { test('sortable=string', async () => {
@ -82,7 +86,7 @@ describe('sort collections', () => {
await db.sync(); await db.sync();
const instance = await model.create(); const instance = await model.create();
expect(model.rawAttributes['order']).toBeDefined(); expect(model.rawAttributes['order']).toBeDefined();
expect(instance.get('order')).toBe(1); expect(instance.get('order')).toBe(parseSortValue(1));
}); });
test('sortable=object', async () => { test('sortable=object', async () => {
@ -102,10 +106,10 @@ describe('sort collections', () => {
const t3 = await Test.model.create({ status: 'draft' }); const t3 = await Test.model.create({ status: 'draft' });
const t4 = await Test.model.create({ status: 'draft' }); const t4 = await Test.model.create({ status: 'draft' });
expect(t1.get('sort')).toBe(1); expect(t1.get('sort')).toBe(parseSortValue(1));
expect(t2.get('sort')).toBe(2); expect(t2.get('sort')).toBe(parseSortValue(2));
expect(t3.get('sort')).toBe(1); expect(t3.get('sort')).toBe(parseSortValue(1));
expect(t4.get('sort')).toBe(2); expect(t4.get('sort')).toBe(parseSortValue(2));
}); });
test('forward insert', async () => { test('forward insert', async () => {
@ -133,11 +137,11 @@ describe('sort collections', () => {
}); });
expect(results).toEqual([ expect(results).toEqual([
{ title: 't1', sort: 1 }, { title: 't1', sort: parseSortValue(1) },
{ title: 't3', sort: 2 }, { title: 't3', sort: parseSortValue(2) },
{ title: 't4', sort: 3 }, { title: 't4', sort: parseSortValue(3) },
{ title: 't2', sort: 4 }, { title: 't2', sort: parseSortValue(4) },
{ title: 't5', sort: 5 }, { title: 't5', sort: parseSortValue(5) },
]); ]);
}); });
@ -166,11 +170,11 @@ describe('sort collections', () => {
}); });
expect(results).toEqual([ expect(results).toEqual([
{ title: 't1', sort: 1 }, { title: 't1', sort: parseSortValue(1) },
{ title: 't4', sort: 2 }, { title: 't4', sort: parseSortValue(2) },
{ title: 't2', sort: 3 }, { title: 't2', sort: parseSortValue(3) },
{ title: 't3', sort: 4 }, { title: 't3', sort: parseSortValue(4) },
{ title: 't5', sort: 5 }, { title: 't5', sort: parseSortValue(5) },
]); ]);
}); });
}); });
@ -245,11 +249,11 @@ describe('sort collections', () => {
}); });
expect(results).toEqual([ expect(results).toEqual([
{ title: 's1:t1', sort: 1 }, { title: 's1:t1', sort: parseSortValue(1) },
{ title: 's1:t3', sort: 2 }, { title: 's1:t3', sort: parseSortValue(2) },
{ title: 's1:t4', sort: 3 }, { title: 's1:t4', sort: parseSortValue(3) },
{ title: 's1:t2', sort: 4 }, { title: 's1:t2', sort: parseSortValue(4) },
{ title: 's1:t5', sort: 5 }, { title: 's1:t5', sort: parseSortValue(5) },
]); ]);
const s2results = ( const s2results = (
@ -264,11 +268,11 @@ describe('sort collections', () => {
}); });
expect(s2results).toEqual([ expect(s2results).toEqual([
{ title: 's2:t1', sort: 1 }, { title: 's2:t1', sort: parseSortValue(1) },
{ title: 's2:t2', sort: 2 }, { title: 's2:t2', sort: parseSortValue(2) },
{ title: 's2:t3', sort: 3 }, { title: 's2:t3', sort: parseSortValue(3) },
{ title: 's2:t4', sort: 4 }, { title: 's2:t4', sort: parseSortValue(4) },
{ title: 's2:t5', sort: 5 }, { title: 's2:t5', sort: parseSortValue(5) },
]); ]);
}); });
@ -319,12 +323,12 @@ describe('sort collections', () => {
}); });
expect(results).toEqual([ expect(results).toEqual([
{ title: 's2:t1', sort: 1 }, { title: 's2:t1', sort: parseSortValue(1) },
{ title: 's2:t2', sort: 2 }, { title: 's2:t2', sort: parseSortValue(2) },
{ title: 's1:t1', sort: 3 }, { title: 's1:t1', sort: parseSortValue(3) },
{ title: 's2:t3', sort: 4 }, { title: 's2:t3', sort: parseSortValue(4) },
{ title: 's2:t4', sort: 5 }, { title: 's2:t4', sort: parseSortValue(5) },
{ title: 's2:t5', sort: 6 }, { title: 's2:t5', sort: parseSortValue(6) },
]); ]);
}); });
}); });

View File

@ -48,6 +48,10 @@ describe('sort field', () => {
await app.destroy(); await app.destroy();
}); });
function parseSortValue(value) {
return db.options.dialect === 'mssql' ? value.toString() : value;
}
describe('main data source', () => { describe('main data source', () => {
it('should init with camelCase scope key', async () => { it('should init with camelCase scope key', async () => {
const Test = db.collection({ const Test = db.collection({
@ -213,9 +217,9 @@ describe('sort field', () => {
const records = await Test.repository.find({}); const records = await Test.repository.find({});
const r3 = records.find((r) => r.get('name') === 'r3'); const r3 = records.find((r) => r.get('name') === 'r3');
expect(r3.get('sort')).toBe(2); expect(r3.get('sort')).toBe(parseSortValue(2));
const r5 = records.find((r) => r.get('name') === 'r5'); const r5 = records.find((r) => r.get('name') === 'r5');
expect(r5.get('sort')).toBe(1); expect(r5.get('sort')).toBe(parseSortValue(1));
}); });
it('should init sorted value by createdAt when primaryKey not exists', async () => { it('should init sorted value by createdAt when primaryKey not exists', async () => {
@ -249,11 +253,11 @@ describe('sort field', () => {
await db.sync(); await db.sync();
const test1 = await Test.model.create<any>(); const test1 = await Test.model.create<any>();
expect(test1.sort).toBe(1); expect(test1.sort).toBe(parseSortValue(1));
const test2 = await Test.model.create<any>(); const test2 = await Test.model.create<any>();
expect(test2.sort).toBe(2); expect(test2.sort).toBe(parseSortValue(2));
const test3 = await Test.model.create<any>(); const test3 = await Test.model.create<any>();
expect(test3.sort).toBe(3); expect(test3.sort).toBe(parseSortValue(3));
}); });
it('should init sort value on data already exits', async () => { it('should init sort value on data already exits', async () => {
@ -292,7 +296,9 @@ describe('sort field', () => {
const items = await db.getRepository('tests').find({ const items = await db.getRepository('tests').find({
order: ['id'], order: ['id'],
}); });
expect(items.map((item) => item.get('sort'))).toEqual([1, 2, 3]); expect(items[0].get('sort')).toBe(parseSortValue(1));
expect(items[1].get('sort')).toBe(parseSortValue(2));
expect(items[2].get('sort')).toBe(parseSortValue(3));
}); });
test.skip('simultaneously create ', async () => { test.skip('simultaneously create ', async () => {
@ -311,7 +317,7 @@ describe('sort field', () => {
await Promise.all(promise); await Promise.all(promise);
const tests = await Test.model.findAll(); const tests = await Test.model.findAll();
const sortValues = tests.map((t) => t.get('sort')).sort(); const sortValues = tests.map((t) => t.get('sort')).sort();
expect(sortValues).toEqual([1, 2, 3]); expect(sortValues).toEqual(db.options.dialect === 'mssql' ? ['1', '2', '3'] : [1, 2, 3]);
}); });
it('skip if sort value not empty', async () => { it('skip if sort value not empty', async () => {
@ -321,11 +327,11 @@ describe('sort field', () => {
}); });
await db.sync(); await db.sync();
const test1 = await Test.model.create<any>({ sort: 3 }); const test1 = await Test.model.create<any>({ sort: 3 });
expect(test1.sort).toBe(3); expect(test1.sort).toBe(parseSortValue(3));
const test2 = await Test.model.create<any>(); const test2 = await Test.model.create<any>();
expect(test2.sort).toBe(4); expect(test2.sort).toBe(parseSortValue(4));
const test3 = await Test.model.create<any>(); const test3 = await Test.model.create<any>();
expect(test3.sort).toBe(5); expect(test3.sort).toBe(parseSortValue(5));
}); });
it('scopeKey', async () => { it('scopeKey', async () => {
@ -343,16 +349,16 @@ describe('sort field', () => {
const t3 = await Test.model.create({ status: 'draft' }); const t3 = await Test.model.create({ status: 'draft' });
const t4 = await Test.model.create({ status: 'draft' }); const t4 = await Test.model.create({ status: 'draft' });
expect(t1.get('sort')).toBe(1); expect(t1.get('sort')).toBe(parseSortValue(1));
expect(t2.get('sort')).toBe(2); expect(t2.get('sort')).toBe(parseSortValue(2));
expect(t3.get('sort')).toBe(1); expect(t3.get('sort')).toBe(parseSortValue(1));
expect(t4.get('sort')).toBe(2); expect(t4.get('sort')).toBe(parseSortValue(2));
t1.set('status', 'draft'); t1.set('status', 'draft');
await t1.save(); await t1.save();
await t1.reload(); await t1.reload();
expect(t1.get('sort')).toBe(3); expect(t1.get('sort')).toBe(parseSortValue(3));
}); });
}); });
@ -374,8 +380,8 @@ describe('sort field', () => {
const p2 = await anotherDB.getRepository('posts').create({ const p2 = await anotherDB.getRepository('posts').create({
values: { title: 'p2' }, values: { title: 'p2' },
}); });
expect(p1.sort).toBe(1); expect(p1.sort).toBe(parseSortValue(1));
expect(p2.sort).toBe(2); expect(p2.sort).toBe(parseSortValue(2));
}); });
}); });
}); });

View File

@ -132,8 +132,8 @@ export class SortableCollection {
async sameScopeMove(sourceInstance: Model, targetInstance: Model, options: MoveOptions) { async sameScopeMove(sourceInstance: Model, targetInstance: Model, options: MoveOptions) {
const fieldName = this.field.get('name'); const fieldName = this.field.get('name');
const sourceSort = sourceInstance.get(fieldName); const sourceSort = Number(sourceInstance.get(fieldName));
let targetSort = targetInstance.get(fieldName); let targetSort = Number(targetInstance.get(fieldName));
if (options.insertAfter) { if (options.insertAfter) {
targetSort = targetSort + 1; targetSort = targetSort + 1;

View File

@ -171,6 +171,25 @@ export class SortField extends Field {
${whereClause} ${whereClause}
) AS ordered_table ON ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField} ) AS ordered_table ON ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField}
SET ${this.collection.quotedTableName()}.${sortColumnName} = ordered_table.new_sequence_number; SET ${this.collection.quotedTableName()}.${sortColumnName} = ordered_table.new_sequence_number;
`;
} else if (this.collection.db.inDialect('mssql')) {
// TODO: This MSSQL support is intended for external data sources
// Since the core database doesn't support MSSQL, this logic needs to be implemented through an extension mechanism
// Potential solutions:
// 1. Abstract database dialect-specific logic into separate modules
// 2. Consider implementing a dialect adapter pattern for better extensibility
sql = `
WITH ordered_table AS (
SELECT *, ROW_NUMBER() OVER (${
scopeKey ? `PARTITION BY ${queryInterface.quoteIdentifier(scopeKey)}` : ''
} ORDER BY ${quotedOrderField}) AS new_sequence_number
FROM ${this.collection.quotedTableName()}
${whereClause}
)
UPDATE t
SET ${sortColumnName} = ot.new_sequence_number
FROM ${this.collection.quotedTableName()} t
INNER JOIN ordered_table ot ON t.${quotedOrderField} = ot.${quotedOrderField};
`; `;
} }
await this.collection.db.sequelize.query(sql, { await this.collection.db.sequelize.query(sql, {