Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions frontend/packages/schema/src/parser/drizzle/mysql/astUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,86 @@ export const getIdentifierName = (node: Expression): string | null => {
return null
}

/**
* Check if a call expression calls a function with a specific name
*/
export const isCallToFunction = (
callExpr: CallExpression,
functionName: string,
): boolean => {
return (
isIdentifier(callExpr.callee) &&
callExpr.callee.value === functionName &&
callExpr.arguments.length > 0
Comment on lines +166 to +167
)
}

/**
* Extract column names from an array expression of member expressions
* Handles patterns like: [table.column1, table.column2]
*/
export const extractColumnNames = (expr: Expression): string[] => {
if (!isArrayExpression(expr)) return []

const columns: string[] = []

for (const elem of expr.elements) {
if (!elem) continue
const elemExpr = getArgumentExpression(elem)
if (!elemExpr) continue
if (!isMemberExpression(elemExpr)) continue
if (!isIdentifier(elemExpr.property)) continue
columns.push(elemExpr.property.value)
}

return columns
}

/**
* Extract table and column names from member expressions in an array
* Returns the first table name found and all column names
*/
export const extractTableAndColumns = (
expr: Expression,
): { tableName: string | null; columnNames: string[] } => {
if (!isArrayExpression(expr)) {
return { tableName: null, columnNames: [] }
}

const columnNames: string[] = []
let tableName: string | null = null

for (const elem of expr.elements) {
if (!elem) continue
const elemExpr = getArgumentExpression(elem)

if (
elemExpr &&
isMemberExpression(elemExpr) &&
isIdentifier(elemExpr.object) &&
isIdentifier(elemExpr.property)
) {
tableName = tableName ?? elemExpr.object.value
columnNames.push(elemExpr.property.value)
}
}

return { tableName, columnNames }
}

/**
* Extract configuration object from a call expression's first argument
*/
export const extractConfigObject = (
callExpr: CallExpression,
): ObjectExpression | null => {
if (callExpr.arguments.length === 0) return null

const configArg = callExpr.arguments[0]
const configExpr = getArgumentExpression(configArg)
return isObjectExpression(configExpr) ? configExpr : null
}

/**
* Parse method call chain from a call expression
*/
Expand Down
32 changes: 31 additions & 1 deletion frontend/packages/schema/src/parser/drizzle/mysql/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ const convertToTable = (
.filter((name) => name && name.length > 0)

// Create composite primary key constraint
const constraintName = `${tableDef.name}_pkey`
const constraintName =
tableDef.compositePrimaryKey.name ?? `${tableDef.name}_pkey`
constraints[constraintName] = {
type: 'PRIMARY KEY',
name: constraintName,
Expand All @@ -153,6 +154,35 @@ const convertToTable = (
}
}

// Handle table-level foreign key definitions
if (tableDef.foreignKeys) {
for (const fkDef of tableDef.foreignKeys) {
const targetTableName =
variableToTableMapping[fkDef.targetTable] ?? fkDef.targetTable
const columnNames = fkDef.columns.map(
(jsName) => tableDef.columns[jsName]?.name ?? jsName,
)
const constraintName =
fkDef.name ??
`${tableDef.name}_${columnNames.join('_')}_${fkDef.targetTable}_${fkDef.targetColumns.join('_')}_fk`

const constraint: ForeignKeyConstraint = {
type: 'FOREIGN KEY',
name: constraintName,
columnNames,
targetTableName,
targetColumnNames: fkDef.targetColumns,
updateConstraint: fkDef.onUpdate
? convertReferenceOption(fkDef.onUpdate)
: 'NO_ACTION',
deleteConstraint: fkDef.onDelete
? convertReferenceOption(fkDef.onDelete)
: 'NO_ACTION',
}
constraints[constraintName] = constraint
}
}

// Convert indexes
for (const [_, indexDef] of Object.entries(tableDef.indexes)) {
// Map JS property names to actual column names
Expand Down
136 changes: 136 additions & 0 deletions frontend/packages/schema/src/parser/drizzle/mysql/tableParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import type { CallExpression, Expression, ObjectExpression } from '@swc/core'
import type { Constraint } from '../../../schema/index.js'
import {
extractColumnNames,
extractConfigObject,
extractTableAndColumns,
getArgumentExpression,
getIdentifierName,
getStringValue,
isCallToFunction,
isMysqlTableCall,
isObjectExpression,
isSchemaTableCall,
Expand All @@ -20,6 +24,7 @@ import type {
DrizzleCheckConstraintDefinition,
DrizzleColumnDefinition,
DrizzleEnumDefinition,
DrizzleForeignKeyDefinition,
DrizzleIndexDefinition,
DrizzleTableDefinition,
} from './types.js'
Expand Down Expand Up @@ -61,12 +66,14 @@ const parseTableExtensions = (
indexes: Record<string, DrizzleIndexDefinition>
constraints?: Record<string, Constraint>
compositePrimaryKey?: CompositePrimaryKeyDefinition
foreignKeys?: DrizzleForeignKeyDefinition[]
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO: Refactor to reduce complexity
} => {
const result: {
indexes: Record<string, DrizzleIndexDefinition>
constraints?: Record<string, Constraint>
compositePrimaryKey?: CompositePrimaryKeyDefinition
foreignKeys?: DrizzleForeignKeyDefinition[]
} = {
indexes: {},
}
Expand Down Expand Up @@ -123,6 +130,48 @@ const parseTableExtensions = (
}
}
}
} else if (returnExpr.type === 'ArrayExpression') {
for (const element of returnExpr.elements) {
if (!element) continue
const elemExpr = getArgumentExpression(element)
if (elemExpr?.type === 'CallExpression') {
const fkDef = parseForeignKeyDefinition(elemExpr)
if (fkDef) {
result.foreignKeys = result.foreignKeys ?? []
result.foreignKeys.push(fkDef)
}
const indexDef = parseIndexDefinition(elemExpr, 'auto')
if (indexDef) {
if (isCompositePrimaryKey(indexDef)) {
result.compositePrimaryKey = indexDef
} else if (isDrizzleIndex(indexDef)) {
result.indexes[indexDef.name] = indexDef
}
}
const checkConstraint = parseCheckConstraint(elemExpr, 'auto')
if (checkConstraint) {
result.constraints = result.constraints ?? {}
result.constraints[checkConstraint.name] = {
type: 'CHECK',
name: checkConstraint.name,
detail: checkConstraint.condition,
}
}
const uniqueConstraint = parseUniqueConstraint(
elemExpr,
'auto',
tableColumns,
)
if (uniqueConstraint) {
result.constraints = result.constraints ?? {}
result.constraints[uniqueConstraint.name] = {
type: 'UNIQUE',
name: uniqueConstraint.name,
columnNames: uniqueConstraint.columnNames,
}
}
}
}
}
}

Expand Down Expand Up @@ -208,6 +257,9 @@ export const parseMysqlTableCall = (
if (extensions.compositePrimaryKey) {
table.compositePrimaryKey = extensions.compositePrimaryKey
}
if (extensions.foreignKeys) {
table.foreignKeys = extensions.foreignKeys
}
}
}

Expand Down Expand Up @@ -266,12 +318,93 @@ export const parseSchemaTableCall = (
if (extensions.compositePrimaryKey) {
table.compositePrimaryKey = extensions.compositePrimaryKey
}
if (extensions.foreignKeys) {
table.foreignKeys = extensions.foreignKeys
}
}
}

return table
}

/**
* Parse foreignKey() call expression
*/
const parseForeignKeyDefinition = (
callExpr: CallExpression,
): DrizzleForeignKeyDefinition | null => {
if (!isCallToFunction(callExpr, 'foreignKey')) return null

const configExpr = extractConfigObject(callExpr)
if (!configExpr) return null

const definition = parseForeignKeyConfig(configExpr)

return isValidForeignKeyDefinition(definition) ? definition : null
}

/**
* Parse foreignKey configuration object
*/
const parseForeignKeyProp = (
propName: string,
propValue: Expression,
): Partial<DrizzleForeignKeyDefinition> => {
switch (propName) {
case 'name':
return isStringLiteral(propValue) ? { name: propValue.value } : {}
case 'columns':
return { columns: extractColumnNames(propValue) }
case 'foreignColumns': {
const { tableName, columnNames } = extractTableAndColumns(propValue)
return {
...(tableName !== null ? { targetTable: tableName } : {}),
targetColumns: columnNames,
}
}
case 'onDelete':
return isStringLiteral(propValue) ? { onDelete: propValue.value } : {}
case 'onUpdate':
return isStringLiteral(propValue) ? { onUpdate: propValue.value } : {}
default:
return {}
}
}

const parseForeignKeyConfig = (
configExpr: ObjectExpression,
): Partial<DrizzleForeignKeyDefinition> => {
let definition: Partial<DrizzleForeignKeyDefinition> = {
columns: [],
targetColumns: [],
targetTable: '',
}

for (const prop of configExpr.properties) {
if (prop.type !== 'KeyValueProperty') continue

const propName = prop.key.type === 'Identifier' ? prop.key.value : null
if (!propName) continue

definition = { ...definition, ...parseForeignKeyProp(propName, prop.value) }
}

return definition
}

/**
* Validate foreign key definition
*/
const isValidForeignKeyDefinition = (
definition: Partial<DrizzleForeignKeyDefinition>,
): definition is DrizzleForeignKeyDefinition => {
return !!(
definition.targetTable &&
definition.columns?.length &&
definition.targetColumns?.length
)
}

/**
* Parse index or primary key definition
*/
Expand All @@ -294,8 +427,11 @@ const parseIndexDefinition = (
const columns = config['columns'].filter(
(col): col is string => typeof col === 'string',
)
const pkName =
typeof config['name'] === 'string' ? config['name'] : undefined
return {
type: 'primaryKey',
...(pkName !== undefined && { name: pkName }),
columns,
}
}
Expand Down
11 changes: 11 additions & 0 deletions frontend/packages/schema/src/parser/drizzle/mysql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type DrizzleTableDefinition = {
indexes: Record<string, DrizzleIndexDefinition>
constraints?: Record<string, Constraint>
compositePrimaryKey?: CompositePrimaryKeyDefinition
foreignKeys?: DrizzleForeignKeyDefinition[]
comment?: string | undefined
schemaName?: string // Schema name for namespace handling
}
Expand Down Expand Up @@ -58,9 +59,19 @@ export type DrizzleSchemaDefinition = {

export type CompositePrimaryKeyDefinition = {
type: 'primaryKey'
name?: string
columns: string[]
}

export type DrizzleForeignKeyDefinition = {
name?: string
columns: string[]
targetTable: string
targetColumns: string[]
onDelete?: string
onUpdate?: string
}

/**
* Type guard to check if a value is an object
*/
Expand Down
Loading
Loading