Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion src/AbstractSQLCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getModifiedFields,
insertAffectedIdsBinds,
} from './referenced-fields';
import _ from 'lodash';

export type { ReferencedFields, RuleReferencedFields, ModifiedFields };

Expand Down Expand Up @@ -746,6 +747,66 @@ export function compileRule(
return AbstractSQLRules2SQL(abstractSQL, engine, noBinds);
}

// Define sorting weights by data types
// TODO: Add support for all possible data types
// TODO: Decide if we want to try and always start with 'id', 'created at', 'modified at'
const sortWeightByType: Record<string, number> = {
'Big Integer': 1,
'Big Serial': 1,
'Date Time': 1,
Integer: 2,
Serial: 2,
'Short Text': 3,
File: 4,
Text: 4,
// eslint-disable-next-line id-denylist
Boolean: 5,
} as const;
Comment on lines +753 to +764
Copy link
Contributor Author

@joshbwlng joshbwlng Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially also store sorting weights as a property of the types themselves in sbvr-types. Although from what I understand MySQL doesn't have similar padding issues so it would probably only make sense for Postgres.


// Get the data type of a field, following any references
const getFieldDataType = (
abstractSqlModel: AbstractSqlModel,
field: AbstractSqlField,
): string => {
if (
['ForeignKey', 'ConceptType'].includes(field.dataType) &&
field.references != null
) {
const dataType = abstractSqlModel.tables[
field.references.resourceName
].fields.find((f) => f.fieldName === field.references?.fieldName)?.dataType;
if (dataType == null) {
throw new Error(
`Failed to find data type for field '${field.fieldName}' in table '${field.references.resourceName}'`,
);
}
return dataType;
}
return field.dataType;
};

// Sort fields by their data type to reduce extra padding
const optimizeFieldOrder = (
abstractSqlModel: AbstractSqlModel,
fields: AbstractSqlField[],
): AbstractSqlField[] => {
// Split computed from non-computed as we can only sort non-computed fields
const [nonComputedFields, computedFields] = _.partition(
fields,
(f) => !f.computed,
);

// Return fields sorted by data type weight then field name
return [
...nonComputedFields.sort((a, b) => {
const aWeight = sortWeightByType[getFieldDataType(abstractSqlModel, a)];
const bWeight = sortWeightByType[getFieldDataType(abstractSqlModel, b)];
return aWeight - bWeight || a.fieldName.localeCompare(b.fieldName);
}),
...computedFields.sort((a, b) => a.fieldName.localeCompare(b.fieldName)),
];
};

const compileSchema = (
abstractSqlModel: AbstractSqlModel,
engine: Engines,
Expand Down Expand Up @@ -858,7 +919,11 @@ ${compileRule(definitionAbstractSql as AbstractSqlQuery, engine, true).replace(
const createSqlElements: string[] = [];
const createIndexes: string[] = [];

for (const field of table.fields) {
const fields =
engine === Engines.postgres
? optimizeFieldOrder(abstractSqlModel, table.fields)
: table.fields;
for (const field of fields) {
const { fieldName, references, dataType, computed } = field;
if (!computed) {
createSqlElements.push(
Expand Down
10 changes: 5 additions & 5 deletions test/sbvr/pilots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,24 +106,24 @@ CREATE TABLE IF NOT EXISTS "pilot" (
"created at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
, "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
, "id" SERIAL NOT NULL PRIMARY KEY
, "licence" INTEGER NOT NULL
, "person" INTEGER NOT NULL
, "name" VARCHAR(255) NOT NULL
, "years of experience" INTEGER NOT NULL
, "name" VARCHAR(255) NOT NULL
, "is experienced" BOOLEAN DEFAULT FALSE NOT NULL
, "licence" INTEGER NOT NULL
, FOREIGN KEY ("person") REFERENCES "person" ("id")
, FOREIGN KEY ("licence") REFERENCES "licence" ("id")
, FOREIGN KEY ("person") REFERENCES "person" ("id")
);`,
modifiedAtTrigger('pilot'),
`\
CREATE TABLE IF NOT EXISTS "pilot-can fly-plane" (
"created at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
, "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
, "pilot" INTEGER NOT NULL
, "can fly-plane" INTEGER NOT NULL
, "id" SERIAL NOT NULL PRIMARY KEY
, FOREIGN KEY ("pilot") REFERENCES "pilot" ("id")
, "pilot" INTEGER NOT NULL
, FOREIGN KEY ("can fly-plane") REFERENCES "plane" ("id")
, FOREIGN KEY ("pilot") REFERENCES "pilot" ("id")
, UNIQUE("pilot", "can fly-plane")
);`,
modifiedAtTrigger('pilot-can fly-plane'),
Expand Down