Skip to content

Commit

Permalink
Temporarily inline default value expressions (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
leo authored Feb 12, 2025
1 parent 5954d4f commit 058e813
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 30 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ new Transaction(queries, {
// Instead of returning an array of parameters for every statement (which allows for
// preventing SQL injections), all parameters are inlined directly into the SQL strings.
// This option should only be used if the generated SQL will be manually verified.
inlineParams: true
inlineParams: true,

// By default, the compiler relies on dynamic column default values for computing the
// values of all meta fields (such as `id`, `ronin.createdAt`, etc). In order to compute
// those values at the time of insertion instead, use this option.
inlineDefaults: true
});
```

Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ interface TransactionOptions {
* separating them out into a dedicated `params` array.
*/
inlineParams?: boolean;
/**
* Whether to compute default field values as part of the generated statement.
*/
inlineDefaults?: boolean;
}

class Transaction {
Expand Down Expand Up @@ -109,6 +113,9 @@ class Transaction {
query,
modelsWithPresets,
options?.inlineParams ? null : [],

// biome-ignore lint/complexity/useSimplifiedLogicExpression: This is needed.
{ inlineDefaults: options?.inlineDefaults || false },
);

// Every query can only produce one main statement (which can return output), but
Expand Down
12 changes: 9 additions & 3 deletions src/instructions/including.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ export const handleIncluding = (
options: {
/** The path on which the selected fields should be mounted in the final record. */
mountingPath?: InternalModelField['mountingPath'];
} = {},
/**
* Whether to compute default field values as part of the generated statement.
*/
inlineDefaults: boolean;
} = {
inlineDefaults: false,
},
): {
statement: string;
tableSubQuery?: string;
Expand Down Expand Up @@ -107,7 +113,7 @@ export const handleIncluding = (
},
models,
statementParams,
{ parentModel: model },
{ parentModel: model, inlineDefaults: options.inlineDefaults },
);

relatedTableSelector = `(${subSelect.main.statement})`;
Expand Down Expand Up @@ -149,7 +155,7 @@ export const handleIncluding = (
statementParams,
subSingle,
modifiableQueryInstructions.including,
{ mountingPath: subMountingPath },
{ mountingPath: subMountingPath, inlineDefaults: options.inlineDefaults },
);

statement += ` ${subIncluding.statement}`;
Expand Down
7 changes: 6 additions & 1 deletion src/instructions/selecting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ export const handleSelecting = (
options: {
/** The path on which the selected fields should be mounted in the final record. */
mountingPath?: InternalModelField['mountingPath'];
} = {},
/**
* Whether to compute default field values as part of the generated statement.
*/
inlineDefaults: boolean;
} = { inlineDefaults: false },
): { columns: string; isJoining: boolean; selectedFields: Array<InternalModelField> } => {
let isJoining = false;

Expand Down Expand Up @@ -104,6 +108,7 @@ export const handleSelecting = (
if (queryType === 'count') {
const subSelect = compileQueryInput(symbol.value, models, statementParams, {
parentModel: { ...model, tableAlias: model.table },
inlineDefaults: options.inlineDefaults,
});

selectedFields.push({
Expand Down
37 changes: 28 additions & 9 deletions src/instructions/to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from '@/src/types/query';
import {
CURRENT_TIME_EXPRESSION,
ID_EXPRESSION,
flatten,
getQuerySymbol,
isObject,
Expand All @@ -31,7 +32,7 @@ import { composeConditions, filterSelectedFields } from '@/src/utils/statement';
* @param dependencyStatements - A list of SQL statements to be executed before the main
* SQL statement, in order to prepare for it.
* @param instructions - The `to` and `with` instruction included in the query.
* @param parentModel - The model of the parent query, if there is one.
* @param options - Additional options to adjust the behavior of the statement generation.
*
* @returns The SQL syntax for the provided `to` instruction.
*/
Expand All @@ -45,18 +46,32 @@ export const handleTo = (
with: NonNullable<SetInstructions['with']> | undefined;
to: NonNullable<SetInstructions['to']>;
},
parentModel?: Model,
options?: Parameters<typeof compileQueryInput>[3],
): string => {
const { with: withInstruction, to: toInstruction } = instructions;
const defaultFields: Record<string, unknown> = {};

if (queryType === 'set' || toInstruction.ronin) {
defaultFields.ronin = {
const inlineDefaultInsertionFields = queryType === 'add' && options?.inlineDefaults;

// If records are being created, assign a default ID to them, unless a custom ID was
// already provided in the query.
if (inlineDefaultInsertionFields) {
defaultFields.id = toInstruction.id || ID_EXPRESSION(model.idPrefix);
}

if (queryType === 'add' || queryType === 'set' || toInstruction.ronin) {
const defaults = {
// If records are being created, set their creation time.
...(inlineDefaultInsertionFields ? { createdAt: CURRENT_TIME_EXPRESSION } : {}),
// If records are being updated, bump their update time.
...(queryType === 'set' ? { updatedAt: CURRENT_TIME_EXPRESSION } : {}),
...(queryType === 'set' || inlineDefaultInsertionFields
? { updatedAt: CURRENT_TIME_EXPRESSION }
: {}),
// Allow for overwriting the default values provided above.
...(toInstruction.ronin as object),
};

if (Object.keys(defaults).length > 0) defaultFields.ronin = defaults;
}

// Check whether a query resides at the root of the `to` instruction.
Expand Down Expand Up @@ -104,7 +119,10 @@ export const handleTo = (
statement = `(${columns.join(', ')}) `;
}

statement += compileQueryInput(symbol.value, models, statementParams).main.statement;
statement += compileQueryInput(symbol.value, models, statementParams, {
// biome-ignore lint/complexity/useSimplifiedLogicExpression: This is needed.
inlineDefaults: options?.inlineDefaults || false,
}).main.statement;
return statement;
}

Expand Down Expand Up @@ -150,7 +168,8 @@ export const handleTo = (
},
models,
[],
{ returning: false },
// biome-ignore lint/complexity/useSimplifiedLogicExpression: This is needed.
{ returning: false, inlineDefaults: options?.inlineDefaults || false },
).main;

// We are passing `after: true` here to ensure that the dependency statement is
Expand Down Expand Up @@ -196,7 +215,7 @@ export const handleTo = (
}

let statement = composeConditions(models, model, statementParams, 'to', toInstruction, {
parentModel,
parentModel: options?.parentModel,
type: queryType === 'add' ? 'fields' : undefined,
});

Expand All @@ -208,7 +227,7 @@ export const handleTo = (
'to',
toInstruction,
{
parentModel,
parentModel: options?.parentModel,
type: 'values',
},
);
Expand Down
75 changes: 60 additions & 15 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
} from '@/src/types/query';
import {
CURRENT_TIME_EXPRESSION,
ID_EXPRESSION,
MODEL_ENTITY_ERROR_CODES,
QUERY_SYMBOLS,
RoninError,
Expand Down Expand Up @@ -211,12 +212,7 @@ export const getSystemFields = (idPrefix: Model['idPrefix']): Array<ModelField>
name: 'ID',
type: 'string',
slug: 'id',
defaultValue: {
// Since default values in SQLite cannot rely on other columns, we unfortunately
// cannot rely on the `idPrefix` column here. Instead, we need to inject it directly
// into the expression as a static string.
[QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`,
},
defaultValue: ID_EXPRESSION(idPrefix),
},
{
name: 'RONIN - Created At',
Expand Down Expand Up @@ -486,6 +482,7 @@ const handleSystemModel = (
models: Array<Model>,
dependencyStatements: Array<InternalDependencyStatement>,
action: 'create' | 'alter' | 'drop',
inlineDefaults: boolean,
systemModel: PartialModel,
newModel?: PartialModel,
): void => {
Expand All @@ -501,7 +498,7 @@ const handleSystemModel = (
query.alter.to = newModelClean;
}

const statement = compileQueryInput(query, models, []);
const statement = compileQueryInput(query, models, [], { inlineDefaults });

dependencyStatements.push(...statement.dependencies);
};
Expand All @@ -523,6 +520,7 @@ const handleSystemModels = (
dependencyStatements: Array<InternalDependencyStatement>,
previousModel: Model,
newModel: Model,
inlineDefaults: boolean,
): void => {
const currentSystemModels = models.filter(({ system }) => {
return system?.model === newModel.id;
Expand Down Expand Up @@ -573,12 +571,19 @@ const handleSystemModels = (
// Determine if the slug of the system model has changed. If so, alter the
// respective table.
if (exists.slug !== systemModel.slug) {
handleSystemModel(models, dependencyStatements, 'alter', systemModel, exists);
handleSystemModel(
models,
dependencyStatements,
'alter',
inlineDefaults,
systemModel,
exists,
);
}
continue;
}

handleSystemModel(models, dependencyStatements, 'drop', systemModel);
handleSystemModel(models, dependencyStatements, 'drop', inlineDefaults, systemModel);
}

// Add any new system models that don't yet exist.
Expand All @@ -587,7 +592,13 @@ const handleSystemModels = (
const exists = currentSystemModels.find(matchSystemModels.bind(null, systemModel));
if (exists) continue;

handleSystemModel(models, dependencyStatements, 'create', systemModel);
handleSystemModel(
models,
dependencyStatements,
'create',
inlineDefaults,
systemModel,
);
}
};

Expand All @@ -601,6 +612,7 @@ const handleSystemModels = (
* @param statementParams - A collection of values that will automatically be
* inserted into the query by SQLite.
* @param query - The query that should potentially be transformed.
* @param options - Additional options for customizing the behavior of the function.
*
* @returns The transformed query or `null` if no further query processing should happen.
*/
Expand All @@ -609,6 +621,12 @@ export const transformMetaQuery = (
dependencyStatements: Array<InternalDependencyStatement>,
statementParams: Array<unknown> | null,
query: Query,
options: {
/**
* Whether to compute default field values as part of the generated statement.
*/
inlineDefaults: boolean;
},
): Query | null => {
const { queryType } = splitQuery(query);
const subAltering = 'alter' in query && query.alter && !('to' in query.alter);
Expand Down Expand Up @@ -738,7 +756,9 @@ export const transformMetaQuery = (
};

// The `dependencyStatements` array is modified in place.
transformMetaQuery(models, dependencyStatements, null, query);
transformMetaQuery(models, dependencyStatements, null, query, {
inlineDefaults: options.inlineDefaults,
});
}
}

Expand All @@ -755,7 +775,13 @@ export const transformMetaQuery = (
getSystemModels(models, modelWithPresets).map((systemModel) => {
// Compose the SQL statement for adding the system model.
// This modifies the original `models` array and adds the system model to it.
return handleSystemModel(models, dependencyStatements, 'create', systemModel);
return handleSystemModel(
models,
dependencyStatements,
'create',
options.inlineDefaults,
systemModel,
);
});
}

Expand Down Expand Up @@ -788,7 +814,13 @@ export const transformMetaQuery = (
to: modelWithPresets,
};

handleSystemModels(models, dependencyStatements, modelBeforeUpdate, model);
handleSystemModels(
models,
dependencyStatements,
modelBeforeUpdate,
model,
options.inlineDefaults,
);
}

if (action === 'drop' && model) {
Expand All @@ -805,7 +837,13 @@ export const transformMetaQuery = (
.map((systemModel) => {
// Compose the SQL statement for removing the system model.
// This modifies the original `models` array and removes the system model from it.
return handleSystemModel(models, dependencyStatements, 'drop', systemModel);
return handleSystemModel(
models,
dependencyStatements,
'drop',
options.inlineDefaults,
systemModel,
);
});
}

Expand Down Expand Up @@ -1045,6 +1083,7 @@ export const transformMetaQuery = (
return compileQueryInput(effectQuery, models, null, {
returning: false,
parentModel: existingModel,
inlineDefaults: options.inlineDefaults,
}).main.statement;
});

Expand Down Expand Up @@ -1094,7 +1133,13 @@ export const transformMetaQuery = (
}
}

handleSystemModels(models, dependencyStatements, modelBeforeUpdate, existingModel);
handleSystemModels(
models,
dependencyStatements,
modelBeforeUpdate,
existingModel,
options.inlineDefaults,
);

return {
set: {
Expand Down
9 changes: 9 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export const CURRENT_TIME_EXPRESSION = {
[QUERY_SYMBOLS.EXPRESSION]: `strftime('%Y-%m-%dT%H:%M:%f', 'now') || 'Z'`,
};

export const ID_EXPRESSION = (
idPrefix: string,
): Record<typeof QUERY_SYMBOLS.EXPRESSION, string> => ({
// Since default values in SQLite cannot rely on other columns, we unfortunately
// cannot rely on the `idPrefix` column here. Instead, we need to inject it directly
// into the expression as a static string.
[QUERY_SYMBOLS.EXPRESSION]: `'${idPrefix}_' || lower(substr(hex(randomblob(12)), 1, 16))`,
});

// A regular expression for splitting up the components of a field mounting path, meaning
// the path within a record under which a particular field's value should be mounted.
const MOUNTING_PATH_SUFFIX = /(.*?)(\{(\d+)\})?$/;
Expand Down
Loading

0 comments on commit 058e813

Please sign in to comment.