diff --git a/drizzle-kit/src/cli/commands/up-mssql.ts b/drizzle-kit/src/cli/commands/up-mssql.ts index 01d28d160a..1b4b9b35c7 100644 --- a/drizzle-kit/src/cli/commands/up-mssql.ts +++ b/drizzle-kit/src/cli/commands/up-mssql.ts @@ -1,6 +1,6 @@ import chalk from 'chalk'; import { writeFileSync } from 'fs'; -import { upToV2 } from 'src/dialects/mssql/versions'; +import { upToV4 } from 'src/dialects/mssql/versions'; import { prepareOutFolder, validateWithReport } from '../../utils/utils-node'; export const upMssqlHandler = (out: string) => { @@ -15,7 +15,7 @@ export const upMssqlHandler = (out: string) => { .forEach((it) => { const path = it.path; - const { snapshot } = upToV2(it.raw); + const { snapshot } = upToV4(it.raw); console.log(`[${chalk.green('✓')}] ${path}`); diff --git a/drizzle-kit/src/dialects/mssql/convertor.ts b/drizzle-kit/src/dialects/mssql/convertor.ts index 44637642b7..4813c2d443 100644 --- a/drizzle-kit/src/dialects/mssql/convertor.ts +++ b/drizzle-kit/src/dialects/mssql/convertor.ts @@ -215,25 +215,79 @@ const recreateIdentityColumn = convertor('recreate_identity_column', (st) => { }); const createIndex = convertor('create_index', (st) => { - const { name, table, columns, isUnique, where, schema } = st.index; - const indexPart = isUnique ? 'UNIQUE INDEX' : 'INDEX'; + const { name, table, columns, include, isUnique, where, schema, clustered, with: withConfig, fulltext } = st.index; + const key = schema !== 'dbo' ? `[${schema}].[${table}]` : `[${table}]`; + const columnList = columns.map((it) => { + const column = it.isExpression ? it.value : `[${it.value}]`; + return it.asc === false ? `${column} DESC` : column; + }).join(','); + + if (st.index.kind === 'fulltext') { + if (!fulltext?.keyIndex) { + throw new Error(`Full-text index "${name}" is missing a key index`); + } + + const withOptions = [ + fulltext.changeTracking ? `CHANGE_TRACKING = ${fulltext.changeTracking.toUpperCase()}` : undefined, + fulltext.stoplist + ? `STOPLIST = ${ + fulltext.stoplist === 'system' || fulltext.stoplist === 'off' + ? fulltext.stoplist.toUpperCase() + : `[${fulltext.stoplist}]` + }` + : undefined, + ].filter((it) => it !== undefined); + const withClause = withOptions.length > 0 ? ` WITH (${withOptions.join(', ')})` : ''; + const catalogClause = fulltext.catalog ? ` ON [${fulltext.catalog}]` : ''; + + return `CREATE FULLTEXT INDEX ON ${key} (${columnList}) KEY INDEX [${fulltext.keyIndex}]${catalogClause}${withClause};`; + } - const uniqueString = `${ - columns.map((it) => { - return it.isExpression ? it.value : `[${it.value}]`; - }) - }`; + if (st.index.kind === 'columnstore') { + const withOptions = [ + withConfig?.online === null || withConfig?.online === undefined + ? undefined + : `ONLINE = ${withConfig.online ? 'ON' : 'OFF'}`, + ].filter((it) => it !== undefined); + const withClause = withOptions.length > 0 ? ` WITH (${withOptions.join(', ')})` : ''; + const indexPart = `${clustered === true ? 'CLUSTERED ' : 'NONCLUSTERED '}COLUMNSTORE INDEX`; + const columnsClause = clustered === true + ? columnList ? ` ORDER (${columnList})` : '' + : ` (${columnList})`; + + return `CREATE ${indexPart} [${name}] ON ${key}${columnsClause}${where ? ` WHERE ${where}` : ''}${withClause};`; + } + + const indexPart = `${isUnique ? 'UNIQUE ' : ''}${ + clustered === true ? 'CLUSTERED ' : clustered === false ? 'NONCLUSTERED ' : '' + }INDEX`; + + const includeClause = include.length > 0 + ? ` INCLUDE (${include.map((it) => it.isExpression ? it.value : `[${it.value}]`).join(',')})` + : ''; const whereClause = where ? ` WHERE ${where}` : ''; + const withOptions = [ + withConfig?.fillFactor === null || withConfig?.fillFactor === undefined + ? undefined + : `FILLFACTOR = ${withConfig.fillFactor}`, + withConfig?.online === null || withConfig?.online === undefined + ? undefined + : `ONLINE = ${withConfig.online ? 'ON' : 'OFF'}`, + ].filter((it) => it !== undefined); + const withClause = withOptions.length > 0 ? ` WITH (${withOptions.join(', ')})` : ''; - const key = schema !== 'dbo' ? `[${schema}].[${table}]` : `[${table}]`; - return `CREATE ${indexPart} [${name}] ON ${key} (${uniqueString})${whereClause};`; + return `CREATE ${indexPart} [${name}] ON ${key} (${columnList})${includeClause}${whereClause}${withClause};`; }); const dropIndex = convertor('drop_index', (st) => { const { schema, name, table } = st.index; const key = schema !== 'dbo' ? `[${schema}].[${table}]` : `[${table}]`; + if (st.index.kind === 'fulltext') { + return `DROP FULLTEXT INDEX ON ${key};`; + } + return `DROP INDEX [${name}] ON ${key};`; }); @@ -295,6 +349,13 @@ const renameIndex = convertor('rename_index', (st) => { const { name: nameFrom, schema: schemaFrom, table: tableFrom } = st.from; const { name: nameTo } = st.to; + if (st.from.kind === 'fulltext' || st.to.kind === 'fulltext') { + return [ + dropIndex.convert({ index: st.from }) as string, + createIndex.convert({ index: st.to }) as string, + ]; + } + const key = schemaFrom !== 'dbo' ? `${schemaFrom}.${tableFrom}.${nameFrom}` : `${tableFrom}.${nameFrom}`; return `EXEC sp_rename '${key}', [${nameTo}], 'INDEX';`; }); diff --git a/drizzle-kit/src/dialects/mssql/ddl.ts b/drizzle-kit/src/dialects/mssql/ddl.ts index e75c8516e2..52cf4bf4d5 100644 --- a/drizzle-kit/src/dialects/mssql/ddl.ts +++ b/drizzle-kit/src/dialects/mssql/ddl.ts @@ -42,6 +42,173 @@ export const createDDLV1 = () => { table: 'required', columns: 'string[]', // does not supported indexing expressions isUnique: 'boolean', + clustered: 'boolean?', + where: 'string?', + }, + uniques: { + schema: 'required', + table: 'required', + nameExplicit: 'boolean', + columns: 'string[]', + }, + checks: { + schema: 'required', + table: 'required', + value: 'string', + }, + defaults: { + schema: 'required', + table: 'required', + column: 'string', + // this field will be required for name preserving + nameExplicit: 'boolean', + default: 'string?', + }, + views: { + schema: 'required', + definition: 'string', + encryption: 'boolean?', + schemaBinding: 'boolean?', + viewMetadata: 'boolean?', + checkOption: 'boolean?', + }, + }); +}; + +export const createDDLV2 = () => { + return create({ + schemas: {}, + tables: { schema: 'required' }, + columns: { + schema: 'required', + table: 'required', + type: 'string', + notNull: 'boolean', + generated: { + type: ['persisted', 'virtual'], + as: 'string', + }, + identity: { + increment: 'number', + seed: 'number', + }, + }, + pks: { + schema: 'required', + table: 'required', + nameExplicit: 'boolean', + columns: 'string[]', + }, + fks: { + schema: 'required', + table: 'required', + columns: 'string[]', + nameExplicit: 'boolean', + schemaTo: 'string', + tableTo: 'string', + columnsTo: 'string[]', + onUpdate: ['NO ACTION', 'CASCADE', 'SET NULL', 'SET DEFAULT'], + onDelete: ['NO ACTION', 'CASCADE', 'SET NULL', 'SET DEFAULT'], + }, + indexes: { + schema: 'required', + table: 'required', + columns: [ + { + value: 'string', + isExpression: 'boolean', + }, + ], + isUnique: 'boolean', + clustered: 'boolean?', + where: 'string?', + }, + uniques: { + schema: 'required', + table: 'required', + nameExplicit: 'boolean', + columns: 'string[]', + }, + checks: { + schema: 'required', + table: 'required', + value: 'string', + }, + defaults: { + schema: 'required', + table: 'required', + column: 'string', + nameExplicit: 'boolean', + default: 'string?', + }, + views: { + schema: 'required', + definition: 'string', + encryption: 'boolean?', + schemaBinding: 'boolean?', + viewMetadata: 'boolean?', + checkOption: 'boolean?', + }, + }); +}; + +export const createDDLV3 = () => { + return create({ + schemas: {}, + tables: { schema: 'required' }, + columns: { + schema: 'required', + table: 'required', + type: 'string', + notNull: 'boolean', + generated: { + type: ['persisted', 'virtual'], + as: 'string', + }, + identity: { + increment: 'number', + seed: 'number', + }, + }, + pks: { + schema: 'required', + table: 'required', + nameExplicit: 'boolean', + columns: 'string[]', + }, + fks: { + schema: 'required', + table: 'required', + columns: 'string[]', + nameExplicit: 'boolean', + schemaTo: 'string', + tableTo: 'string', + columnsTo: 'string[]', + onUpdate: ['NO ACTION', 'CASCADE', 'SET NULL', 'SET DEFAULT'], + onDelete: ['NO ACTION', 'CASCADE', 'SET NULL', 'SET DEFAULT'], + }, + indexes: { + schema: 'required', + table: 'required', + columns: [ + { + value: 'string', + isExpression: 'boolean', + asc: 'boolean', + }, + ], + include: [ + { + value: 'string', + isExpression: 'boolean', + }, + ], + isUnique: 'boolean', + clustered: 'boolean?', + with: { + fillFactor: 'number?', + online: 'boolean?', + }, where: 'string?', }, uniques: { @@ -112,14 +279,32 @@ export const createDDL = () => { indexes: { schema: 'required', table: 'required', - // TODO add asc/desc: asc and desc feature exists in mssql + kind: ['btree', 'fulltext', 'columnstore'], columns: [ + { + value: 'string', + isExpression: 'boolean', + asc: 'boolean', + }, + ], + include: [ { value: 'string', isExpression: 'boolean', }, ], isUnique: 'boolean', + clustered: 'boolean?', + with: { + fillFactor: 'number?', + online: 'boolean?', + }, + fulltext: { + keyIndex: 'string?', + catalog: 'string?', + changeTracking: ['auto', 'manual', 'off', null], + stoplist: 'string?', + }, where: 'string?', }, uniques: { diff --git a/drizzle-kit/src/dialects/mssql/diff.ts b/drizzle-kit/src/dialects/mssql/diff.ts index 2157387b84..18af96f3b7 100644 --- a/drizzle-kit/src/dialects/mssql/diff.ts +++ b/drizzle-kit/src/dialects/mssql/diff.ts @@ -509,6 +509,16 @@ export const ddlDiff = async ( }; }; + const viewsFilter = (type: 'deleted' | 'created') => { + return (it: { schema: string; table: string }) => { + if (type === 'created') { + return !createdViews.some((t) => t.schema === it.schema && t.name === it.table); + } else { + return !deletedViews.some((t) => t.schema === it.schema && t.name === it.table); + } + }; + }; + const columnsFilter = (_type: 'added') => { return (it: { schema: string; table: string; column: string }) => { return !columnsToCreate.some((t) => t.schema === it.schema && t.table === it.table && t.name === it.column); @@ -939,10 +949,23 @@ export const ddlDiff = async ( }); }; }; - const jsonCreateIndexes = indexesCreates.filter(indexesIdentityFilter('created')).map((index) => - prepareStatement('create_index', { index }) - ); - const jsonDropIndexes = indexesDeletes.filter(indexesIdentityFilter('deleted')).filter(tablesFilter('deleted')).map(( + const isViewIndex = (index: Index) => + ddl1.views.one({ schema: index.schema, name: index.table }) !== undefined + || ddl2.views.one({ schema: index.schema, name: index.table }) !== undefined; + const jsonCreateIndexes = indexesCreates.filter(indexesIdentityFilter('created')).filter((index) => + !isViewIndex(index) + ) + .map((index) => prepareStatement('create_index', { index })); + const jsonCreateViewIndexes = indexesCreates.filter(indexesIdentityFilter('created')).filter(isViewIndex).map(( + index, + ) => prepareStatement('create_index', { index })); + const jsonDropIndexes = indexesDeletes.filter(indexesIdentityFilter('deleted')).filter((index) => !isViewIndex(index)) + .filter(tablesFilter('deleted')).map(( + index, + ) => prepareStatement('drop_index', { index })); + const jsonDropViewIndexes = indexesDeletes.filter(indexesIdentityFilter('deleted')).filter(isViewIndex).filter( + viewsFilter('deleted'), + ).map(( index, ) => prepareStatement('drop_index', { index })); const jsonRenameIndex = indexesRenames.map((it) => prepareStatement('rename_index', { from: it.from, to: it.to })); @@ -953,12 +976,19 @@ export const ddlDiff = async ( ) { const forWhere = !!idx.where && (idx.where.from !== null && idx.where.to !== null ? mode !== 'push' : true); const forColumns = !!idx.columns && (idx.columns.from.length === idx.columns.to.length ? mode !== 'push' : true); + const forInclude = !!idx.include + && (idx.include.from.length === idx.include.to.length ? mode !== 'push' : true); // TODO recheck this - if (idx.isUnique || forColumns || forWhere) { + if (idx.kind || idx.isUnique || idx.clustered || forColumns || forInclude || idx.with || idx.fulltext || forWhere) { const index = ddl2.indexes.one({ schema: idx.schema, table: idx.table, name: idx.name })!; - jsonDropIndexes.push(prepareStatement('drop_index', { index })); - jsonCreateIndexes.push(prepareStatement('create_index', { index })); + if (isViewIndex(index)) { + jsonDropViewIndexes.push(prepareStatement('drop_index', { index })); + jsonCreateViewIndexes.push(prepareStatement('create_index', { index })); + } else { + jsonDropIndexes.push(prepareStatement('drop_index', { index })); + jsonCreateIndexes.push(prepareStatement('create_index', { index })); + } } } @@ -993,6 +1023,7 @@ export const ddlDiff = async ( jsonStatements.push(...createTables); + jsonStatements.push(...jsonDropViewIndexes); jsonStatements.push(...jsonDropViews); jsonStatements.push(...jsonRenameViews); jsonStatements.push(...jsonMoveViews); @@ -1038,6 +1069,7 @@ export const ddlDiff = async ( // jsonStatements.push(...jsonRenameDefaults); jsonStatements.push(...createViews); + jsonStatements.push(...jsonCreateViewIndexes); jsonStatements.push(...dropSchemas); diff --git a/drizzle-kit/src/dialects/mssql/drizzle.ts b/drizzle-kit/src/dialects/mssql/drizzle.ts index 1b15f8b6e7..86ab70f036 100644 --- a/drizzle-kit/src/dialects/mssql/drizzle.ts +++ b/drizzle-kit/src/dialects/mssql/drizzle.ts @@ -4,8 +4,10 @@ import type { AnyMsSqlColumn, AnyMsSqlTable } from 'drizzle-orm/mssql-core'; import { getTableConfig, getViewConfig, + type IndexedColumn, MsSqlColumn, MsSqlDialect, + type MsSqlFullTextConfig, MsSqlSchema, MsSqlTable, MsSqlView, @@ -43,6 +45,75 @@ export const defaultFromColumn = ( throw new Error(`unexpected default: ${column.getSQLType().toLowerCase()} ${column.default}`); }; +type IndexColumnEntry = MssqlEntities['indexes']['columns'][number]; + +const isNamedIndexColumn = ( + column: SQL | MsSqlColumn | IndexedColumn, +): column is (MsSqlColumn | IndexedColumn) & { name: string } => { + return 'name' in column && typeof column.name === 'string'; +}; + +const isSqlIndexColumn = (column: SQL | MsSqlColumn | IndexedColumn): column is SQL => { + return !isNamedIndexColumn(column); +}; + +const serializeIndexColumn = (column: SQL | MsSqlColumn | IndexedColumn, dialect: MsSqlDialect): IndexColumnEntry => { + if (isSqlIndexColumn(column)) { + return { + value: dialect.sqlToQuery(column, 'indexes').sql, + isExpression: true, + asc: true, + }; + } + + return { + value: column.name, + isExpression: false, + asc: column.indexConfig.order !== 'desc', + }; +}; + +const serializeIncludedIndexColumn = ( + column: SQL | MsSqlColumn | IndexedColumn, + dialect: MsSqlDialect, +): MssqlEntities['indexes']['include'][number] => { + if (isSqlIndexColumn(column)) { + return { + value: dialect.sqlToQuery(column, 'indexes').sql, + isExpression: true, + }; + } + + return { + value: column.name, + isExpression: false, + }; +}; + +const serializeIndexWith = ( + config: { fillFactor?: number; online?: boolean } | undefined, +): MssqlEntities['indexes']['with'] => { + if (!config || (config.fillFactor === undefined && config.online === undefined)) return null; + + return { + fillFactor: config.fillFactor ?? null, + online: config.online ?? null, + }; +}; + +const serializeFullTextIndex = ( + config: MsSqlFullTextConfig | undefined, +): MssqlEntities['indexes']['fulltext'] => { + if (!config) return null; + + return { + keyIndex: config.keyIndex ?? null, + catalog: config.catalog ?? null, + changeTracking: config.changeTracking ?? null, + stoplist: config.stoplist ?? null, + }; +}; + export const fromDrizzleSchema = ( schema: { schemas: MsSqlSchema[]; @@ -245,7 +316,7 @@ export const fromDrizzleSchema = ( errors.push({ type: 'index_no_name', schema: schema, - table: getTableName(index.config.table), + table: tableName, sql: dialect.sqlToQuery(column).sql, }); continue; @@ -260,15 +331,13 @@ export const fromDrizzleSchema = ( table: tableName, name, schema, - columns: columns.map((it) => { - if (is(it, SQL)) { - const sql = dialect.sqlToQuery(it, 'indexes').sql; - return { value: sql, isExpression: true }; - } else { - return { value: it.name, isExpression: false }; - } - }), + kind: index.config.kind, + columns: columns.map((it) => serializeIndexColumn(it, dialect)), + include: (index.config.include ?? []).map((it) => serializeIncludedIndexColumn(it, dialect)), isUnique: index.config.unique ?? false, + clustered: index.config.clustered ?? null, + with: serializeIndexWith(index.config.with), + fulltext: serializeFullTextIndex(index.config.fulltext), where: where ? where : null, }); } @@ -347,6 +416,41 @@ export const fromDrizzleSchema = ( schemaBinding: schemaBinding ?? false, // default viewMetadata: viewMetadata ?? false, // default }); + + for (const index of cfg.indexes ?? []) { + const columns = index.config.columns; + const indexName = index.config.name; + + for (const column of columns) { + if (is(column, SQL) && !index.config.name) { + errors.push({ + type: 'index_no_name', + schema, + table: name, + sql: dialect.sqlToQuery(column).sql, + }); + continue; + } + } + + let where = index.config.where ? dialect.sqlToQuery(index.config.where, 'indexes').sql : ''; + where = where === 'true' ? '' : where; + + result.indexes.push({ + entityType: 'indexes', + table: name, + name: indexName, + schema, + kind: index.config.kind, + columns: columns.map((it) => serializeIndexColumn(it, dialect)), + include: (index.config.include ?? []).map((it) => serializeIncludedIndexColumn(it, dialect)), + isUnique: index.config.unique ?? false, + clustered: index.config.clustered ?? null, + with: serializeIndexWith(index.config.with), + fulltext: serializeFullTextIndex(index.config.fulltext), + where: where ? where : null, + }); + } } return { schema: result, errors }; diff --git a/drizzle-kit/src/dialects/mssql/grammar.ts b/drizzle-kit/src/dialects/mssql/grammar.ts index 8b0f9ff0d8..ac32b1aca8 100644 --- a/drizzle-kit/src/dialects/mssql/grammar.ts +++ b/drizzle-kit/src/dialects/mssql/grammar.ts @@ -595,6 +595,13 @@ export const Datetime: SqlType = { return { default: value, options }; }, }; +export const SmallDatetime: SqlType = { + is: (type) => type === 'smalldatetime' || type.startsWith('smalldatetime('), + drizzleImport: () => 'smalldatetime', + defaultFromDrizzle: Datetime.defaultFromDrizzle, + defaultFromIntrospect: Datetime.defaultFromIntrospect, + toTs: Datetime.toTs, +}; export const DateType: SqlType = { is: (type) => type === 'date' || type.startsWith('date('), drizzleImport: () => 'date', @@ -735,6 +742,97 @@ export const Varbinary: SqlType = { toTs: Binary.toTs, }; +export const UniqueIdentifier: SqlType = { + is: (type) => type === 'uniqueidentifier', + drizzleImport: () => 'uniqueidentifier', + defaultFromDrizzle: Char.defaultFromDrizzle, + defaultFromIntrospect: Char.defaultFromIntrospect, + toTs: (_type, value) => { + if (!value) return { default: '' }; + return Char.toTs('varchar', value); + }, +}; + +export const Xml: SqlType = { + is: (type) => type === 'xml', + drizzleImport: () => 'xml', + defaultFromDrizzle: NVarchar.defaultFromDrizzle, + defaultFromIntrospect: NVarchar.defaultFromIntrospect, + toTs: (_type, value) => { + if (!value) return { default: '' }; + return NVarchar.toTs('nvarchar(max)', value); + }, +}; + +export const Json: SqlType = { + is: (type) => type === 'json', + drizzleImport: () => 'json', + defaultFromDrizzle: (value) => { + return `('${escapeForSqlDefault(stringify(value))}')`; + }, + defaultFromIntrospect: NVarchar.defaultFromIntrospect, + toTs: (_type, value) => { + if (!value) return { default: '' }; + return { default: `sql\`${value}\`` }; + }, +}; + +export const Money: SqlType = { + is: (type) => type === 'money', + drizzleImport: () => 'money', + defaultFromDrizzle: Decimal.defaultFromDrizzle, + defaultFromIntrospect: Decimal.defaultFromIntrospect, + toTs: (_type, value) => { + if (!value) return { default: '' }; + const res = Decimal.toTs('decimal', value); + return { options: { mode: 'number' }, default: res.default }; + }, +}; + +export const SmallMoney: SqlType = { + is: (type) => type === 'smallmoney', + drizzleImport: () => 'smallmoney', + defaultFromDrizzle: Money.defaultFromDrizzle, + defaultFromIntrospect: Money.defaultFromIntrospect, + toTs: Money.toTs, +}; + +export const RowVersion: SqlType = { + is: (type) => type === 'rowversion' || type === 'timestamp', + drizzleImport: () => 'rowversion', + defaultFromDrizzle: () => { + throw Error('unexpected rowversion default'); + }, + defaultFromIntrospect: (value) => value, + toTs: () => { + return { default: '' }; + }, +}; + +export const Geography: SqlType = { + is: (type) => type === 'geography', + drizzleImport: () => 'geography', + defaultFromDrizzle: (value) => { + return `('${String(value)}')`; + }, + defaultFromIntrospect: (value) => { + return value; + }, + toTs: () => { + return { default: '' }; + }, +}; + +export const Geometry: SqlType = { + is: (type) => type === 'geometry', + drizzleImport: () => 'geometry', + defaultFromDrizzle: Geography.defaultFromDrizzle, + defaultFromIntrospect: Geography.defaultFromIntrospect, + toTs: () => { + return { default: '' }; + }, +}; + export const Custom: SqlType = { is: () => { throw Error('Mocked'); @@ -769,10 +867,19 @@ export const typeFor = (sqlType: string): SqlType => { if (Real.is(sqlType)) return Real; if (DateType.is(sqlType)) return DateType; if (Datetime.is(sqlType)) return Datetime; + if (SmallDatetime.is(sqlType)) return SmallDatetime; if (Datetime2.is(sqlType)) return Datetime2; if (Datetimeoffset.is(sqlType)) return Datetimeoffset; if (Time.is(sqlType)) return Time; if (Binary.is(sqlType)) return Binary; if (Varbinary.is(sqlType)) return Varbinary; + if (UniqueIdentifier.is(sqlType)) return UniqueIdentifier; + if (Xml.is(sqlType)) return Xml; + if (Json.is(sqlType)) return Json; + if (Money.is(sqlType)) return Money; + if (SmallMoney.is(sqlType)) return SmallMoney; + if (RowVersion.is(sqlType)) return RowVersion; + if (Geography.is(sqlType)) return Geography; + if (Geometry.is(sqlType)) return Geometry; return Custom; }; diff --git a/drizzle-kit/src/dialects/mssql/introspect.ts b/drizzle-kit/src/dialects/mssql/introspect.ts index 43f1e3c154..d2cba1b3a3 100644 --- a/drizzle-kit/src/dialects/mssql/introspect.ts +++ b/drizzle-kit/src/dialects/mssql/introspect.ts @@ -18,6 +18,12 @@ import type { } from './ddl'; import { parseDefault, parseFkAction, parseViewMetadataFlag, parseViewSQL } from './grammar'; +const parseFullTextChangeTracking = (value: string | null): 'auto' | 'manual' | 'off' | null => { + const normalized = value?.toLowerCase(); + if (normalized === 'auto' || normalized === 'manual' || normalized === 'off') return normalized; + return null; +}; + export const fromDatabase = async ( db: DB, filter: EntityFilter, @@ -139,9 +145,25 @@ ORDER BY lower(views.name); }; }); + const filteredViews = viewsList.filter((it) => { + const schema = filteredSchemas.find((schema) => schema.schema_id === it.schema_id)!; + + if (!filter({ type: 'table', schema: schema.schema_name, name: it.name })) return false; + return true; + }) + .map((it) => { + const schema = filteredSchemas.find((schema) => schema.schema_id === it.schema_id)!; + + return { + ...it, + schema: schema.schema_name, + }; + }); + const filteredTableIds = filteredTables.map((it) => it.object_id); - const filteredViewIds = viewsList.map((it) => it.object_id); + const filteredViewIds = filteredViews.map((it) => it.object_id); const filteredViewsAndTableIds = [...filteredTableIds, ...filteredViewIds]; + const filteredTablesAndViews = [...filteredTables, ...filteredViews]; if (filteredViewIds.length === 0 && filteredTableIds.length === 0) { return { @@ -265,7 +287,12 @@ ORDER BY lower(fk.name); is_unique_constraint: boolean; has_filter: boolean; filter_definition: string; + type_desc: string; column_id: number; + is_descending_key: boolean; + is_included_column: boolean; + key_ordinal: number; + fill_factor: number; }; const pksUniquesAndIdxsQuery = await db.query(` @@ -278,13 +305,18 @@ ORDER BY lower(fk.name); i.is_unique_constraint as is_unique_constraint, i.has_filter as has_filter, i.filter_definition as filter_definition, - ic.column_id as column_id + i.type_desc as type_desc, + ic.column_id as column_id, + ic.is_descending_key as is_descending_key, + ic.is_included_column as is_included_column, + ic.key_ordinal as key_ordinal, + i.fill_factor as fill_factor FROM sys.indexes i INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id - ${filterByTableIds ? 'WHERE i.object_id in ' + filterByTableIds : ''} - ORDER BY lower(i.name);`) + ${filterByTableAndViewIds ? 'WHERE i.object_id in ' + filterByTableAndViewIds : ''} + ORDER BY lower(i.name), ic.key_ordinal, ic.index_column_id;`) .then((rows) => { queryCallback('indexes', rows, null); return rows; @@ -293,6 +325,49 @@ ORDER BY lower(fk.name); throw error; }); + type RawFullTextIndex = { + table_id: number; + table_name: string; + schema_id: number; + column_id: number; + key_index_name: string; + catalog_name: string | null; + change_tracking_state_desc: string | null; + stoplist_name: string | null; + }; + + const fullTextIndexesQuery = await db.query(` + SELECT + ft.object_id as table_id, + obj.name as table_name, + obj.schema_id as schema_id, + ftc.column_id as column_id, + idx.name as key_index_name, + catalog.name as catalog_name, + ft.change_tracking_state_desc as change_tracking_state_desc, + stoplist.name as stoplist_name + FROM sys.fulltext_indexes ft + INNER JOIN sys.objects obj + ON ft.object_id = obj.object_id + INNER JOIN sys.fulltext_index_columns ftc + ON ft.object_id = ftc.object_id + INNER JOIN sys.indexes idx + ON ft.object_id = idx.object_id + AND ft.unique_index_id = idx.index_id + LEFT JOIN sys.fulltext_catalogs catalog + ON ft.fulltext_catalog_id = catalog.fulltext_catalog_id + LEFT JOIN sys.fulltext_stoplists stoplist + ON ft.stoplist_id = stoplist.stoplist_id + ${filterByTableAndViewIds ? 'WHERE ft.object_id in ' + filterByTableAndViewIds : ''} + ORDER BY lower(obj.name), ftc.column_id;`) + .then((rows) => { + queryCallback('fulltext_indexes', rows, null); + return rows; + }).catch((error) => { + queryCallback('fulltext_indexes', [], error); + throw error; + }); + const columnsQuery = await db.query<{ column_id: number; table_object_id: number; @@ -452,20 +527,39 @@ WHERE obj.type in ('U', 'V') }); } - type GroupedIdxsAndContraints = Omit & { - column_ids: number[]; + type GroupedIndexColumn = { + column_id: number; + asc: boolean; + isIncluded: boolean; + keyOrdinal: number; }; + type GroupedIdxsAndContraints = + & Omit + & { + columns: GroupedIndexColumn[]; + }; const groupedIdxsAndContraints: GroupedIdxsAndContraints[] = Object.values( pksUniquesAndIdxsList.reduce((acc: Record, row: RawIdxsAndConstraints) => { - const table = filteredTables.find((it) => it.object_id === row.table_id); - if (!table) return acc; + const target = filteredTablesAndViews.find((it) => it.object_id === row.table_id); + if (!target) return acc; const key = `${row.table_id}_${row.index_id}`; if (!acc[key]) { - const { column_id: _, ...rest } = row; - acc[key] = { ...rest, column_ids: [] }; + const { + column_id: _columnId, + is_descending_key: _isDescendingKey, + is_included_column: _isIncludedColumn, + key_ordinal: _keyOrdinal, + ...rest + } = row; + acc[key] = { ...rest, columns: [] }; } - acc[key].column_ids.push(row.column_id); + acc[key].columns.push({ + column_id: row.column_id, + asc: !row.is_descending_key, + isIncluded: row.is_included_column, + keyOrdinal: row.key_ordinal, + }); return acc; }, {}), ); @@ -474,8 +568,6 @@ WHERE obj.type in ('U', 'V') const groupedUniqueConstraints: GroupedIdxsAndContraints[] = []; const groupedIndexes: GroupedIdxsAndContraints[] = []; - indexesCount = groupedIndexes.length; - groupedIdxsAndContraints.forEach((it) => { if (it.is_primary_key) groupedPrimaryKeys.push(it); else if (it.is_unique_constraint) groupedUniqueConstraints.push(it); @@ -488,9 +580,9 @@ WHERE obj.type in ('U', 'V') const schema = filteredSchemas.find((it) => it.schema_id === table.schema_id)!; - const columns = unique.column_ids.map((it) => { + const columns = unique.columns.filter((it) => !it.isIncluded).map((it) => { const column = columnsList.find((column) => - column.table_object_id === unique.table_id && column.column_id === it + column.table_object_id === unique.table_id && column.column_id === it.column_id )!; return column.name; }); @@ -511,8 +603,10 @@ WHERE obj.type in ('U', 'V') const schema = filteredSchemas.find((it) => it.schema_id === table.schema_id)!; - const columns = pk.column_ids.map((it) => { - const column = columnsList.find((column) => column.table_object_id === pk.table_id && column.column_id === it)!; + const columns = pk.columns.filter((it) => !it.isIncluded).map((it) => { + const column = columnsList.find((column) => + column.table_object_id === pk.table_id && column.column_id === it.column_id + )!; return column.name; }); @@ -527,14 +621,23 @@ WHERE obj.type in ('U', 'V') } for (const index of groupedIndexes) { - const table = filteredTables.find((it) => it.object_id === index.table_id); + const table = filteredTablesAndViews.find((it) => it.object_id === index.table_id); if (!table) continue; const schema = filteredSchemas.find((it) => it.schema_id === table.schema_id)!; + const isColumnstore = index.type_desc.includes('COLUMNSTORE'); + const isClusteredColumnstore = index.type_desc === 'CLUSTERED COLUMNSTORE'; + const isView = filteredViews.some((it) => it.object_id === table.object_id); - const columns = index.column_ids.map((it) => { + const columns = isClusteredColumnstore ? [] : index.columns.filter((it) => !it.isIncluded).map((it) => { + const column = columnsList.find((column) => + column.table_object_id === index.table_id && column.column_id === it.column_id + )!; + return { value: column.name, isExpression: false, asc: it.asc }; + }); + const include = isColumnstore ? [] : index.columns.filter((it) => it.isIncluded).map((it) => { const column = columnsList.find((column) => - column.table_object_id === index.table_id && column.column_id === it + column.table_object_id === index.table_id && column.column_id === it.column_id )!; return { value: column.name, isExpression: false }; }); @@ -544,12 +647,65 @@ WHERE obj.type in ('U', 'V') schema: schema.schema_name, table: table.name, name: index.name, + kind: isColumnstore ? 'columnstore' : 'btree', columns, + include, where: index.has_filter ? index.filter_definition : null, isUnique: index.is_unique, + clustered: index.type_desc === 'CLUSTERED' || isClusteredColumnstore + ? true + : index.type_desc === 'NONCLUSTERED COLUMNSTORE' || (isView && index.type_desc === 'NONCLUSTERED') + ? false + : null, + with: index.fill_factor > 0 ? { fillFactor: index.fill_factor, online: null } : null, + fulltext: null, + }); + } + + const groupedFullTextIndexes = Object.values( + fullTextIndexesQuery.reduce((acc: Record, row) => { + acc[row.table_id] ??= []; + acc[row.table_id].push(row); + return acc; + }, {}), + ); + + for (const fullTextIndex of groupedFullTextIndexes) { + const first = fullTextIndex[0]; + const table = filteredTablesAndViews.find((it) => it.object_id === first.table_id); + if (!table) continue; + + const schema = filteredSchemas.find((it) => it.schema_id === first.schema_id)!; + const columns = fullTextIndex.map((it) => { + const column = columnsList.find((column) => + column.table_object_id === it.table_id && column.column_id === it.column_id + )!; + return { value: column.name, isExpression: false, asc: true }; + }); + + indexes.push({ + entityType: 'indexes', + schema: schema.schema_name, + table: table.name, + name: `${table.name}_fulltext`, + kind: 'fulltext', + columns, + include: [], + where: null, + isUnique: false, + clustered: null, + with: null, + fulltext: { + keyIndex: first.key_index_name, + catalog: first.catalog_name, + changeTracking: parseFullTextChangeTracking(first.change_tracking_state_desc), + stoplist: first.stoplist_name, + }, }); } + indexesCount = indexes.length; + type GroupedForeignKey = { name: string; schema_id: number; @@ -657,8 +813,8 @@ WHERE obj.type in ('U', 'V') progressCallback('indexes', indexesCount, 'fetching'); progressCallback('tables', tableCount, 'done'); - viewsCount = viewsList.length; - for (const view of viewsList) { + viewsCount = filteredViews.length; + for (const view of filteredViews) { const viewName = view.name; const viewSchema = filteredSchemas.find((it) => it.schema_id === view.schema_id); if (!viewSchema) continue; diff --git a/drizzle-kit/src/dialects/mssql/serializer.ts b/drizzle-kit/src/dialects/mssql/serializer.ts index 45cfea2828..7c839fff7e 100644 --- a/drizzle-kit/src/dialects/mssql/serializer.ts +++ b/drizzle-kit/src/dialects/mssql/serializer.ts @@ -50,7 +50,7 @@ export const prepareSnapshot = async ( const prevIds = snapshots.length === 0 ? [prevSnapshot.id] : findLeafSnapshotIds(snapshots); const snapshot = { - version: '2', + version: '4', dialect: 'mssql', id, prevIds, diff --git a/drizzle-kit/src/dialects/mssql/snapshot.ts b/drizzle-kit/src/dialects/mssql/snapshot.ts index 5643802984..d4f7fe6d79 100644 --- a/drizzle-kit/src/dialects/mssql/snapshot.ts +++ b/drizzle-kit/src/dialects/mssql/snapshot.ts @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'; import { originUUID } from '../../utils'; import { array, validator } from '../simpleValidator'; import type { MssqlDDL, MssqlEntity, MssqlEntityV1 } from './ddl'; -import { createDDL, createDDLV1 } from './ddl'; +import { createDDL, createDDLV1, createDDLV2, createDDLV3 } from './ddl'; // v1 // old @@ -16,9 +16,29 @@ export const snapshotValidatorV1 = validator({ renames: array((_) => true), }); +const ddlV2 = createDDLV2(); +export const snapshotValidatorV2 = validator({ + version: ['2'], + dialect: ['mssql'], + id: 'string', + prevIds: array((_) => true), + ddl: array((it) => ddlV2.entities.validate(it)), + renames: array((_) => true), +}); + +const ddlV3 = createDDLV3(); +export const snapshotValidatorV3 = validator({ + version: ['3'], + dialect: ['mssql'], + id: 'string', + prevIds: array((_) => true), + ddl: array((it) => ddlV3.entities.validate(it)), + renames: array((_) => true), +}); + const ddl = createDDL(); export const snapshotValidator = validator({ - version: ['2'], + version: ['4'], dialect: ['mssql'], id: 'string', prevIds: array((_) => true), @@ -27,15 +47,19 @@ export const snapshotValidator = validator({ }); export type MssqlSnapshotV1 = typeof snapshotValidatorV1.shape; +export type MssqlSnapshotV2 = typeof snapshotValidatorV2.shape; +export type MssqlSnapshotV3 = typeof snapshotValidatorV3.shape; export type MssqlSnapshot = typeof snapshotValidator.shape; +export type MssqlEntityV2 = ReturnType['entities']['list']>[number]; +export type MssqlEntityV3 = ReturnType['entities']['list']>[number]; export const toJsonSnapshot = (ddl: MssqlDDL, prevIds: string[], renames: string[]): MssqlSnapshot => { - return { dialect: 'mssql', id: randomUUID(), prevIds, version: '2', ddl: ddl.entities.list(), renames }; + return { dialect: 'mssql', id: randomUUID(), prevIds, version: '4', ddl: ddl.entities.list(), renames }; }; export const drySnapshot = snapshotValidator.strict( { - version: '2', + version: '4', dialect: 'mssql', id: originUUID, prevIds: [], diff --git a/drizzle-kit/src/dialects/mssql/typescript.ts b/drizzle-kit/src/dialects/mssql/typescript.ts index 1257d731aa..666fbe3ef2 100644 --- a/drizzle-kit/src/dialects/mssql/typescript.ts +++ b/drizzle-kit/src/dialects/mssql/typescript.ts @@ -30,17 +30,25 @@ const imports = [ 'datetimeoffset', 'decimal', 'float', + 'geography', + 'geometry', 'int', + 'json', + 'money', 'numeric', 'real', + 'rowversion', 'smallint', + 'smalldatetime', + 'smallmoney', 'text', 'ntext', - 'json', 'time', 'tinyint', + 'uniqueidentifier', 'varbinary', 'tinyint', + 'xml', 'customType', ] as const; export type Import = (typeof imports)[number]; @@ -155,8 +163,15 @@ export const ddlToTypeScript = ( if (x.entityType === 'tables') imports.add(tableFn); if (x.entityType === 'indexes') { - if (x.isUnique) imports.add('uniqueIndex'); - else imports.add('index'); + if (x.kind === 'fulltext') { + imports.add('fullTextIndex'); + } else if (x.kind === 'columnstore') { + imports.add(x.clustered ? 'clusteredColumnStoreIndex' : 'columnStoreIndex'); + } else if (x.isUnique) { + imports.add('uniqueIndex'); + } else { + imports.add('index'); + } } if (x.entityType === 'fks') imports.add('foreignKey'); @@ -247,8 +262,14 @@ export const ddlToTypeScript = ( viewMetadata: it.viewMetadata, checkOption: it.checkOption, }; - - let statement = `export const ${withCasing(paramName, casing)} = ${func}("${it.name}", {${columns}})`; + const viewIndexes = ddl.indexes.list({ schema: it.schema, table: it.name }); + const indexes = createTableIndexes(it.name, viewIndexes, casing, 'view'); + + let statement = `export const ${withCasing(paramName, casing)} = ${func}("${it.name}", {${columns}}`; + if (viewIndexes.length > 0) { + statement += `, (view) => [\n${indexes}]`; + } + statement += ')'; statement += Object.keys(viewOptions).length > 0 ? `.with(${JSON.stringify(viewOptions)})` : ''; statement += `.as(${as});`; @@ -420,7 +441,7 @@ const createTableColumns = ( return statement; }; -const createTableIndexes = (tableName: string, idxs: Index[], casing: Casing): string => { +const createTableIndexes = (tableName: string, idxs: Index[], casing: Casing, target = 'table'): string => { let statement = ''; idxs.forEach((it) => { @@ -436,20 +457,58 @@ const createTableIndexes = (tableName: string, idxs: Index[], casing: Casing): s const name = it.name; // const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`; - statement += it.isUnique ? '\tuniqueIndex(' : '\tindex('; + if (it.kind === 'fulltext') { + statement += '\tfullTextIndex('; + } else if (it.kind === 'columnstore') { + statement += it.clustered ? '\tclusteredColumnStoreIndex(' : '\tcolumnStoreIndex('; + } else { + statement += it.isUnique ? '\tuniqueIndex(' : '\tindex('; + } statement += name ? `"${name}")` : ')'; - statement += `.on(${ - it.columns - .map((it) => { - if (it.isExpression) { - return `sql\`${it.value}\``; - } else { - return `table.${withCasing(it.value, casing)}`; - } - }) - .join(', ') - })`; + const columns = it.columns + .map((it) => { + if (it.isExpression) { + return `sql\`${it.value}\``; + } else { + return `${target}.${withCasing(it.value, casing)}${it.asc === false ? '.desc()' : ''}`; + } + }) + .join(', '); + + if (it.kind === 'columnstore' && it.clustered) { + statement += columns ? `.orderBy(${columns})` : ''; + } else { + statement += `.on(${columns})`; + } + + if (it.kind === 'fulltext' && it.fulltext) { + if (it.fulltext.keyIndex) statement += `.keyIndex("${it.fulltext.keyIndex}")`; + if (it.fulltext.catalog) statement += `.catalog("${it.fulltext.catalog}")`; + if (it.fulltext.changeTracking) statement += `.changeTracking("${it.fulltext.changeTracking}")`; + if (it.fulltext.stoplist) statement += `.stoplist("${it.fulltext.stoplist}")`; + } + if (it.kind === 'btree' && it.include.length > 0) { + statement += `.include(${ + it.include + .map((it) => { + if (it.isExpression) { + return `sql\`${it.value}\``; + } else { + return `${target}.${withCasing(it.value, casing)}`; + } + }) + .join(', ') + })`; + } + statement += it.kind === 'btree' + ? it.clustered === true + ? `.clustered()` + : it.clustered === false + ? `.nonClustered()` + : '' + : ''; + statement += it.with ? `.with(${inspect(it.with)})` : ''; statement += it.where ? `.where(sql\`${it.where}\`)` : ''; statement += `,\n`; diff --git a/drizzle-kit/src/dialects/mssql/versions.ts b/drizzle-kit/src/dialects/mssql/versions.ts index 702c931c1f..e44f373a30 100644 --- a/drizzle-kit/src/dialects/mssql/versions.ts +++ b/drizzle-kit/src/dialects/mssql/versions.ts @@ -1,14 +1,14 @@ import { assertUnreachable } from 'src/utils'; -import { createDDL } from './ddl'; -import type { MssqlSnapshot, MssqlSnapshotV1 } from './snapshot'; +import { createDDL, createDDLV2, createDDLV3 } from './ddl'; +import type { MssqlSnapshot, MssqlSnapshotV1, MssqlSnapshotV2, MssqlSnapshotV3 } from './snapshot'; -export const upToV2 = (it: Record): { snapshot: MssqlSnapshot; hints: string[] } => { +export const upToV2 = (it: Record): { snapshot: MssqlSnapshotV2; hints: string[] } => { const snapshot = it as MssqlSnapshotV1; const ddlV1 = snapshot.ddl; const hints = [] as string[]; - const ddl = createDDL(); + const ddl = createDDLV2(); for (const entry of ddlV1) { if (entry.entityType === 'checks') ddl.checks.push(entry); else if (entry.entityType === 'columns') ddl.columns.push(entry); @@ -41,6 +41,81 @@ export const upToV2 = (it: Record): { snapshot: MssqlSnapshot; hint }; }; +const updateUpToV3 = (snapshot: MssqlSnapshotV2): MssqlSnapshotV3 => { + const ddlV2 = snapshot.ddl; + const ddl = createDDLV3(); + for (const entry of ddlV2) { + if (entry.entityType === 'indexes') { + ddl.indexes.push({ + ...entry, + columns: entry.columns.map((it) => ({ ...it, asc: true })), + include: [], + with: null, + }); + } else { + ddl.entities.push(entry); + } + } + + return { + id: snapshot.id, + prevIds: snapshot.prevIds, + version: '3', + dialect: 'mssql', + ddl: ddl.entities.list(), + renames: snapshot.renames, + }; +}; + +export const upToV3 = (it: Record): { snapshot: MssqlSnapshotV3; hints: string[] } => { + const updated = it.version === '1' ? upToV2(it) : { snapshot: it as MssqlSnapshotV2, hints: [] }; + return { + snapshot: updateUpToV3(updated.snapshot), + hints: updated.hints, + }; +}; + +const updateUpToV4 = (snapshot: MssqlSnapshotV3): MssqlSnapshot => { + const ddlV3 = snapshot.ddl; + const ddl = createDDL(); + for (const entry of ddlV3) { + if (entry.entityType === 'indexes') { + ddl.indexes.push({ + ...entry, + kind: 'btree', + fulltext: null, + }); + } else { + ddl.entities.push(entry); + } + } + + return { + id: snapshot.id, + prevIds: snapshot.prevIds, + version: '4', + dialect: 'mssql', + ddl: ddl.entities.list(), + renames: snapshot.renames, + }; +}; + +export const upToV4 = (it: Record): { snapshot: MssqlSnapshot; hints: string[] } => { + if (it.version === '4') { + return { snapshot: it as MssqlSnapshot, hints: [] }; + } + + const updated = it.version === '1' ? upToV2(it) : { snapshot: it as MssqlSnapshotV2 | MssqlSnapshotV3, hints: [] }; + const snapshot = updated.snapshot.version === '3' + ? updated.snapshot + : updateUpToV3(updated.snapshot); + + return { + snapshot: updateUpToV4(snapshot), + hints: updated.hints, + }; +}; + export const extractBaseTypeAndDimensions = (it: string): [string, number] => { const dimensionRegex = /\[[^\]]*\]/g; // matches any [something], including [] const count = (it.match(dimensionRegex) || []).length; diff --git a/drizzle-kit/src/utils/utils-node.ts b/drizzle-kit/src/utils/utils-node.ts index 72170dc82a..6a7006215e 100644 --- a/drizzle-kit/src/utils/utils-node.ts +++ b/drizzle-kit/src/utils/utils-node.ts @@ -279,7 +279,7 @@ const mysqlValidator = (snapshot: object): ValidationResult => { }; const mssqlSnapshotValidator = (snapshot: object): ValidationResult => { - const versionError = assertVersion(snapshot, 2); + const versionError = assertVersion(snapshot, 4); if (versionError) return { status: versionError }; const res = mssqlValidatorSnapshot.parse(snapshot); diff --git a/drizzle-kit/tests/mssql/indexes.test.ts b/drizzle-kit/tests/mssql/indexes.test.ts index 5caeba7d33..f9ad8f0dd3 100644 --- a/drizzle-kit/tests/mssql/indexes.test.ts +++ b/drizzle-kit/tests/mssql/indexes.test.ts @@ -1,6 +1,9 @@ import { isNotNull, sql } from 'drizzle-orm'; import { bit, + clusteredColumnStoreIndex, + columnStoreIndex, + fullTextIndex, index, IndexBuilder, int, @@ -128,6 +131,68 @@ test('indexes #4', async () => { expect(pst1).toStrictEqual(expectedSt1); }); +test('indexes with include and options', async () => { + const users = mssqlTable('users', { + id: int('id').primaryKey(), + name: varchar('name', { length: 255 }), + age: int('age'), + bio: text('bio'), + }, (t) => [ + index('users_age_idx') + .on(t.age.desc(), t.name) + .include(t.bio) + .with({ fillFactor: 80, online: true }), + ]); + + const { sqlStatements: st } = await diff({}, { users }, []); + + expect(st).toStrictEqual([ + 'CREATE TABLE [users] (\n' + + '\t[id] int,\n' + + '\t[name] varchar(255),\n' + + '\t[age] int,\n' + + '\t[bio] text,\n' + + '\tCONSTRAINT [users_pkey] PRIMARY KEY([id])\n' + + ');\n', + 'CREATE INDEX [users_age_idx] ON [users] ([age] DESC,[name]) INCLUDE ([bio]) WITH (FILLFACTOR = 80, ONLINE = ON);', + ]); +}); + +test('fulltext and columnstore indexes', async () => { + const users = mssqlTable('users', { + id: int('id').primaryKey(), + name: varchar('name', { length: 255 }).notNull(), + bio: text('bio'), + age: int('age'), + }, (t) => [ + uniqueIndex('users_id_unique').on(t.id), + fullTextIndex('users_bio_ft') + .on(t.name, t.bio) + .keyIndex('users_id_unique') + .catalog('users_catalog') + .changeTracking('manual') + .stoplist('system'), + columnStoreIndex('users_age_columnstore_idx').on(t.age).where(sql`${t.age} is not null`).with({ online: true }), + clusteredColumnStoreIndex('users_clustered_columnstore_idx').orderBy(t.id), + ]); + + const { sqlStatements: st } = await diff({}, { users }, []); + + expect(st).toStrictEqual([ + 'CREATE TABLE [users] (\n' + + '\t[id] int,\n' + + '\t[name] varchar(255) NOT NULL,\n' + + '\t[bio] text,\n' + + '\t[age] int,\n' + + '\tCONSTRAINT [users_pkey] PRIMARY KEY([id])\n' + + ');\n', + 'CREATE UNIQUE INDEX [users_id_unique] ON [users] ([id]);', + 'CREATE FULLTEXT INDEX ON [users] ([name],[bio]) KEY INDEX [users_id_unique] ON [users_catalog] WITH (CHANGE_TRACKING = MANUAL, STOPLIST = SYSTEM);', + 'CREATE NONCLUSTERED COLUMNSTORE INDEX [users_age_columnstore_idx] ON [users] ([age]) WHERE [age] is not null WITH (ONLINE = ON);', + 'CREATE CLUSTERED COLUMNSTORE INDEX [users_clustered_columnstore_idx] ON [users] ORDER ([id]);', + ]); +}); + test('adding basic indexes', async () => { const schema1 = { users: mssqlTable('users', { diff --git a/drizzle-kit/tests/mssql/mocks.ts b/drizzle-kit/tests/mssql/mocks.ts index 7766af1dfe..8701392ef9 100644 --- a/drizzle-kit/tests/mssql/mocks.ts +++ b/drizzle-kit/tests/mssql/mocks.ts @@ -436,6 +436,7 @@ export const prepareTestDatabase = async (tx: boolean = true): Promise setTimeout(resolve, sleep)); - // timeLeft -= sleep; + lastError = e; + await new Promise((resolve) => setTimeout(resolve, sleep)); + timeLeft -= sleep; } } while (timeLeft > 0); - throw new Error(); + await container?.stop().catch(console.error); + if (lastError instanceof Error) throw lastError; + throw new Error('Failed to connect to MSSQL test database'); }; export async function createDockerDB(): Promise< @@ -531,7 +533,7 @@ export async function createDockerDB(): Promise< await mssqlContainer.start(); return { - url: 'mssql://SA:drizzle123PASSWORD!@127.0.0.1:1433?encrypt=true&trustServerCertificate=true', + url: `mssql://SA:drizzle123PASSWORD!@localhost:${port}?encrypt=true&trustServerCertificate=true`, container: mssqlContainer, }; } diff --git a/drizzle-kit/tests/mssql/pull.test.ts b/drizzle-kit/tests/mssql/pull.test.ts index e711ac083b..aa6a74ed57 100644 --- a/drizzle-kit/tests/mssql/pull.test.ts +++ b/drizzle-kit/tests/mssql/pull.test.ts @@ -422,6 +422,45 @@ test('introspect view #2', async () => { expect(sqlStatements.length).toBe(0); }); +test('introspect indexed view', async () => { + const users = mssqlTable('users', { + id: int('id').primaryKey().notNull(), + name: varchar('name', { length: 255 }).notNull(), + }); + + const view = mssqlView('some_view', { + id: int('id').notNull(), + name: varchar('name', { length: 255 }).notNull(), + }, (view) => [ + uniqueIndex('some_view_clustered_idx').on(view.id).clustered(), + index('some_view_name_idx').on(view.name).nonClustered(), + ]).with({ + schemaBinding: true, + }).as(sql`SELECT ${users.id}, ${users.name} FROM ${users}`); + const schema = { + view, + users, + }; + + const { introspectDDL, statements, sqlStatements } = await diffIntrospect( + db, + schema, + 'introspect-indexed-view', + ); + + expect( + introspectDDL.indexes.list({ schema: 'dbo', table: 'some_view' }).map((it) => ({ + name: it.name, + clustered: it.clustered, + })), + ).toStrictEqual([ + { name: 'some_view_clustered_idx', clustered: true }, + { name: 'some_view_name_idx', clustered: false }, + ]); + expect(statements.length).toBe(0); + expect(sqlStatements.length).toBe(0); +}); + test('introspect primary key with unqiue', async () => { const users = mssqlTable('users', { id: int('id').primaryKey(), diff --git a/drizzle-kit/tests/mssql/tables.test.ts b/drizzle-kit/tests/mssql/tables.test.ts index c34b5649b5..d47885503d 100644 --- a/drizzle-kit/tests/mssql/tables.test.ts +++ b/drizzle-kit/tests/mssql/tables.test.ts @@ -2,17 +2,26 @@ import { sql } from 'drizzle-orm'; import { camelCase, foreignKey, + geography, + geometry, index, int, + json, + money, mssqlSchema, mssqlTable, mssqlTableCreator, primaryKey, + rowversion, + smalldatetime, + smallmoney, snakeCase, text, unique, + uniqueidentifier, uniqueIndex, varchar, + xml, } from 'drizzle-orm/mssql-core'; import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'; import { diff, prepareTestDatabase, push, TestDatabase } from './mocks'; @@ -49,6 +58,41 @@ test('add table #1', async () => { expect(pst).toStrictEqual(st0); }); +test('add table with native mssql types', async () => { + const to = { + nativeTypes: mssqlTable('native_types', { + id: int('id').primaryKey(), + guid: uniqueidentifier('guid').notNull(), + document: xml('document'), + payload: json('payload'), + price: money('price'), + smallPrice: smallmoney('small_price'), + version: rowversion('version'), + createdAtSmall: smalldatetime('created_at_small'), + geo: geography('geo'), + shape: geometry('shape'), + }), + }; + + const { sqlStatements: st } = await diff({}, to, []); + + expect(st).toStrictEqual([ + 'CREATE TABLE [native_types] (\n' + + '\t[id] int,\n' + + '\t[guid] uniqueidentifier NOT NULL,\n' + + '\t[document] xml,\n' + + '\t[payload] json,\n' + + '\t[price] money,\n' + + '\t[small_price] smallmoney,\n' + + '\t[version] rowversion NOT NULL,\n' + + '\t[created_at_small] smalldatetime,\n' + + '\t[geo] geography,\n' + + '\t[shape] geometry,\n' + + '\tCONSTRAINT [native_types_pkey] PRIMARY KEY([id])\n' + + ');\n', + ]); +}); + test('add table #2', async () => { const to = { users: mssqlTable('users', { diff --git a/drizzle-kit/tests/mssql/views.test.ts b/drizzle-kit/tests/mssql/views.test.ts index 3750aeba14..e8dd8c9c34 100644 --- a/drizzle-kit/tests/mssql/views.test.ts +++ b/drizzle-kit/tests/mssql/views.test.ts @@ -1,5 +1,5 @@ import { eq, sql } from 'drizzle-orm'; -import { bit, int, mssqlSchema, mssqlTable, mssqlView, varchar } from 'drizzle-orm/mssql-core'; +import { bit, int, mssqlSchema, mssqlTable, mssqlView, uniqueIndex, varchar } from 'drizzle-orm/mssql-core'; import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'; import { diff, prepareTestDatabase, push, TestDatabase } from './mocks'; @@ -110,6 +110,28 @@ test('create table and view #3_1', async () => { expect(pst).toStrictEqual(st0); }); +test('create indexed view', async () => { + const users = mssqlTable('users', { + id: int('id').primaryKey().notNull(), + }); + const to = { + users, + view1: mssqlView('some_view1', { id: int('id').notNull() }, (view) => [ + uniqueIndex('some_view1_clustered_idx').on(view.id).clustered(), + ]).with({ + schemaBinding: true, + }).as(sql`SELECT ${users.id} FROM ${users}`), + }; + + const { sqlStatements: st } = await diff({}, to, []); + + expect(st).toStrictEqual([ + `CREATE TABLE [users] (\n\t[id] int,\n\tCONSTRAINT [users_pkey] PRIMARY KEY([id])\n);\n`, + `CREATE VIEW [some_view1]\nWITH SCHEMABINDING AS SELECT [users].[id] FROM [dbo].[users];`, + `CREATE UNIQUE CLUSTERED INDEX [some_view1_clustered_idx] ON [some_view1] ([id]);`, + ]); +}); + test('create table and view #4', async () => { const schema = mssqlSchema('new_schema'); diff --git a/drizzle-orm/RQBv2_MSSQL_TASKS.md b/drizzle-orm/RQBv2_MSSQL_TASKS.md new file mode 100644 index 0000000000..1a0aeeef76 --- /dev/null +++ b/drizzle-orm/RQBv2_MSSQL_TASKS.md @@ -0,0 +1,55 @@ +# MSSQL RQBv2 Task Tracker + +## Baseline +- [x] Work branch is based on `origin/beta` +- [x] Confirm MSSQL `_query` V1 still imports `~/_relations.ts` +- [x] Confirm MySQL/PG V2 helpers and signatures from `~/relations.ts` + +## Implementation +- [x] Preserve V1 `_query` via `buildRelationalQueryV1` +- [x] Add MSSQL V2 `buildRelationalQuery` +- [x] Add MSSQL V2 column JSON selection helpers +- [x] Rewrite shared relation-filter `limit 1` probes to MSSQL `top(1)` +- [x] Add MSSQL JSON value mappers for binary and object date/time columns +- [x] Add `query-builders/query-v2.ts` +- [x] Add `db.query` while retaining `db._query` +- [x] Wire V2 session mapper and JSON chunk concatenation +- [x] Forward `relations` through node-mssql drizzle config +- [x] Thread relation generics through node-mssql mock and migrator APIs +- [x] Update MSSQL exports +- [x] Add MSSQL native column builders: `uniqueidentifier`, `xml`, `json`, `money`, `smallmoney`, `rowversion`, `smalldatetime`, `geography`, `geometry` +- [x] Add MSSQL index key ordering via `.asc()` / `.desc()` in table extra config +- [x] Add MSSQL index `INCLUDE` support +- [x] Add MSSQL index `WITH (FILLFACTOR = ..., ONLINE = ...)` SQL emission +- [x] Add drizzle-kit MSSQL snapshot v3 migration for index metadata +- [x] Add drizzle-kit MSSQL pull/codegen metadata for descending keys, included columns, and fill factor +- [x] Implement existing MSSQL `FOR XML` / `FOR BROWSE` select modes in the dialect +- [x] Add dedicated MSSQL full-text index DSL, SQL emission, pull metadata, and codegen +- [x] Add dedicated MSSQL columnstore index DSL, SQL emission, pull metadata, and codegen +- [x] Add opt-in WKT/point codecs for MSSQL `geography` and `geometry` +- [x] Expand internal MSSQL `FOR XML` modes and options (`RAW`, `AUTO`, `EXPLICIT`, `PATH`, `ROOT`, `ELEMENTS`, `BINARY BASE64`, `TYPE`) + +## Verification +- [x] Manual SQL compile smoke check +- [x] SQL snapshot tests pass +- [x] MSSQL integration tests added +- [x] MSSQL integration tests pass +- [x] V1 `_query` regression test passes +- [x] Type tests pass +- [x] Pothos schema/query-shape smoke passes +- [x] Pothos live SQL Server acceptance test passes +- [x] ORM type tests cover native MSSQL types and index DSL +- [ ] Focused drizzle-kit MSSQL index/native-type tests pass locally +- [ ] Full ORM build completes locally + +## Current Blockers +- Local `pnpm --filter drizzle-orm build` hangs in `bun --bun run scripts/build.ts`; ORM type tests pass, but kit type tests still depend on rebuilt `drizzle-orm/dist`. +- Focused drizzle-kit MSSQL tests currently fail in setup before assertions because the local SQL Server connection uses `127.0.0.1` with TLS SNI, which tedious rejects. + +## PR Notes +- [x] Document SQL Server 2016+ requirement +- [x] Document `offset` fallback ordering: PK-backed tables are deterministic; views/no-PK sources use all exposed columns as a best-effort SQL Server fallback and should pass explicit `orderBy` for production pagination semantics +- [x] Document `ONLINE = ON` as a create/rebuild execution option; SQL Server does not expose it as durable index metadata for pull round trips +- [x] Document full-text and columnstore indexes as separate SQL Server index families +- [x] Document SQL Server limitations: no `refreshMaterializedView`; indexed views are the MSSQL analogue; schema rename remains unsupported in kit +- [x] Call out beta churn and recommend upstreaming over patching node_modules diff --git a/drizzle-orm/src/column-builder.ts b/drizzle-orm/src/column-builder.ts index 29ccd2c429..3937709304 100644 --- a/drizzle-orm/src/column-builder.ts +++ b/drizzle-orm/src/column-builder.ts @@ -1,7 +1,7 @@ import { entityKind } from '~/entity.ts'; import type { CockroachColumn, ExtraConfigColumn as CockroachExtraConfigColumn } from './cockroach-core/index.ts'; import type { Column, ColumnBaseConfig } from './column.ts'; -import type { MsSqlColumn } from './mssql-core/index.ts'; +import type { ExtraConfigColumn as MsSqlExtraConfigColumn, MsSqlColumn } from './mssql-core/index.ts'; import type { MySqlColumn } from './mysql-core/index.ts'; import type { ExtraConfigColumn, PgColumn, PgSequenceOptions } from './pg-core/index.ts'; import type { SingleStoreColumn } from './singlestore-core/index.ts'; @@ -409,6 +409,7 @@ export type BuildIndexColumn< TDialect extends Dialect, > = TDialect extends 'pg' ? ExtraConfigColumn : TDialect extends 'cockroach' ? CockroachExtraConfigColumn + : TDialect extends 'mssql' ? MsSqlExtraConfigColumn : never; // TODO diff --git a/drizzle-orm/src/mssql-core/columns/all.ts b/drizzle-orm/src/mssql-core/columns/all.ts index e6501aa234..b75684fd25 100644 --- a/drizzle-orm/src/mssql-core/columns/all.ts +++ b/drizzle-orm/src/mssql-core/columns/all.ts @@ -10,14 +10,21 @@ import { datetimeoffset } from './datetimeoffset.ts'; import { decimal } from './decimal.ts'; import { float } from './float.ts'; import { int } from './int.ts'; +import { json } from './json.ts'; +import { money, smallmoney } from './money.ts'; import { numeric } from './numeric.ts'; import { real } from './real.ts'; +import { rowversion } from './rowversion.ts'; +import { smalldatetime } from './smalldatetime.ts'; import { smallint } from './smallint.ts'; +import { geography, geometry } from './spatial.ts'; import { ntext, text } from './text.ts'; import { time } from './time.ts'; import { tinyint } from './tinyint.ts'; +import { uniqueidentifier } from './uniqueidentifier.ts'; import { varbinary } from './varbinary.ts'; import { nvarchar, varchar } from './varchar.ts'; +import { xml } from './xml.ts'; export function getMsSqlColumnBuilders() { return { @@ -32,17 +39,26 @@ export function getMsSqlColumnBuilders() { datetimeoffset, decimal, float, + geography, + geometry, int, + json, + money, real, numeric, + rowversion, smallint, + smalldatetime, + smallmoney, text, ntext, time, tinyint, + uniqueidentifier, varbinary, varchar, nvarchar, + xml, }; } diff --git a/drizzle-orm/src/mssql-core/columns/binary.ts b/drizzle-orm/src/mssql-core/columns/binary.ts index f5dd9d0a6e..254e43f8b0 100644 --- a/drizzle-orm/src/mssql-core/columns/binary.ts +++ b/drizzle-orm/src/mssql-core/columns/binary.ts @@ -39,6 +39,10 @@ export class MsSqlBinary> extends Ms getSQLType(): string { return this.config.setLength ? `binary(${this.length})` : `binary`; } + + mapFromJsonValue(value: string): Buffer { + return Buffer.from(value, 'base64'); + } } export interface MsSqlBinaryConfig { diff --git a/drizzle-orm/src/mssql-core/columns/common.ts b/drizzle-orm/src/mssql-core/columns/common.ts index 6162577fc8..bba95ecffd 100644 --- a/drizzle-orm/src/mssql-core/columns/common.ts +++ b/drizzle-orm/src/mssql-core/columns/common.ts @@ -87,6 +87,13 @@ export abstract class MsSqlColumnBuilder< abstract build( table: AnyMsSqlTable<{ name: TTableName }>, ): MsSqlColumn; + + /** @internal */ + buildExtraConfigColumn( + table: AnyMsSqlTable<{ name: TTableName }>, + ): ExtraConfigColumn { + return new ExtraConfigColumn(table, this.config); + } } // To understand how to use `MsSqlColumn` and `AnyMsSqlColumn`, see `Column` and `AnyColumn` documentation. @@ -99,6 +106,13 @@ export abstract class MsSqlColumn< /** @internal */ override readonly table: MsSqlTable; + indexConfig: IndexedExtraConfigType = { + order: 'asc', + }; + defaultConfig: IndexedExtraConfigType = { + order: 'asc', + }; + constructor( table: MsSqlTable, config: ColumnBuilderRuntimeConfig & TRuntimeConfig, @@ -111,12 +125,49 @@ export abstract class MsSqlColumn< override shouldDisableInsert(): boolean { return false; } + + asc(): Omit { + this.indexConfig.order = 'asc'; + return this; + } + + desc(): Omit { + this.indexConfig.order = 'desc'; + return this; + } } export type AnyMsSqlColumn> = {}> = MsSqlColumn< Required, TPartial>> >; +export type IndexedExtraConfigType = { order?: 'asc' | 'desc' }; + +export class ExtraConfigColumn< + T extends ColumnBaseConfig = ColumnBaseConfig, +> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlExtraConfigColumn'; + + getSQLType(): string { + return this.columnType; + } +} + +export class IndexedColumn { + static readonly [entityKind]: string = 'MsSqlIndexedColumn'; + + constructor( + name: string, + indexConfig: IndexedExtraConfigType, + ) { + this.name = name; + this.indexConfig = indexConfig; + } + + name: string; + indexConfig: IndexedExtraConfigType; +} + export interface MsSqlColumnWithIdentityConfig { identity: { seed?: number; increment?: number } | undefined; } diff --git a/drizzle-orm/src/mssql-core/columns/datetime.ts b/drizzle-orm/src/mssql-core/columns/datetime.ts index aae667f4df..d15286cf1a 100644 --- a/drizzle-orm/src/mssql-core/columns/datetime.ts +++ b/drizzle-orm/src/mssql-core/columns/datetime.ts @@ -40,6 +40,10 @@ export class MsSqlDateTime> extends Ms getSQLType(): string { return `datetime`; } + + mapFromJsonValue(value: string): Date { + return new Date(value); + } } export class MsSqlDateTimeStringBuilder extends MsSqlDateColumnBaseBuilder<{ diff --git a/drizzle-orm/src/mssql-core/columns/datetime2.ts b/drizzle-orm/src/mssql-core/columns/datetime2.ts index 428c7e7ef5..d32ffa4e07 100644 --- a/drizzle-orm/src/mssql-core/columns/datetime2.ts +++ b/drizzle-orm/src/mssql-core/columns/datetime2.ts @@ -46,6 +46,10 @@ export class MsSqlDateTime2> extends M const precision = this.precision === undefined ? '' : `(${this.precision})`; return `datetime2${precision}`; } + + mapFromJsonValue(value: string): Date { + return new Date(value); + } } export class MsSqlDateTime2StringBuilder extends MsSqlDateColumnBaseBuilder<{ diff --git a/drizzle-orm/src/mssql-core/columns/datetimeoffset.ts b/drizzle-orm/src/mssql-core/columns/datetimeoffset.ts index 1a322cb714..bc6e1b0fdd 100644 --- a/drizzle-orm/src/mssql-core/columns/datetimeoffset.ts +++ b/drizzle-orm/src/mssql-core/columns/datetimeoffset.ts @@ -46,6 +46,10 @@ export class MsSqlDateTimeOffset> exte const precision = this.precision === undefined ? '' : `(${this.precision})`; return `datetimeoffset${precision}`; } + + mapFromJsonValue(value: string): Date { + return new Date(value); + } } export class MsSqlDateTimeOffsetStringBuilder extends MsSqlDateColumnBaseBuilder<{ diff --git a/drizzle-orm/src/mssql-core/columns/index.ts b/drizzle-orm/src/mssql-core/columns/index.ts index fcc2c30808..2945c48708 100644 --- a/drizzle-orm/src/mssql-core/columns/index.ts +++ b/drizzle-orm/src/mssql-core/columns/index.ts @@ -11,11 +11,18 @@ export * from './datetimeoffset.ts'; export * from './decimal.ts'; export * from './float.ts'; export * from './int.ts'; +export * from './json.ts'; +export * from './money.ts'; export * from './numeric.ts'; export * from './real.ts'; +export * from './rowversion.ts'; +export * from './smalldatetime.ts'; export * from './smallint.ts'; +export * from './spatial.ts'; export * from './text.ts'; export * from './time.ts'; export * from './tinyint.ts'; +export * from './uniqueidentifier.ts'; export * from './varbinary.ts'; export * from './varchar.ts'; +export * from './xml.ts'; diff --git a/drizzle-orm/src/mssql-core/columns/json.ts b/drizzle-orm/src/mssql-core/columns/json.ts new file mode 100644 index 0000000000..8aedbadd9f --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/json.ts @@ -0,0 +1,43 @@ +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable } from '~/mssql-core/table.ts'; +import { MsSqlColumn, MsSqlColumnBuilder } from './common.ts'; + +export class MsSqlJsonBuilder extends MsSqlColumnBuilder<{ + dataType: 'object json'; + data: unknown; + driverParam: string; +}> { + static override readonly [entityKind]: string = 'MsSqlJsonBuilder'; + + constructor(name: string) { + super(name, 'object json', 'MsSqlJson'); + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlJson(table, this.config); + } +} + +export class MsSqlJson> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlJson'; + + getSQLType(): string { + return 'json'; + } + + override mapToDriverValue = (value: T['data']): string => { + return JSON.stringify(value); + }; + + override mapFromDriverValue = (value: string): T['data'] => { + return JSON.parse(value); + }; +} + +export function json(name?: string): MsSqlJsonBuilder { + return new MsSqlJsonBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/mssql-core/columns/money.ts b/drizzle-orm/src/mssql-core/columns/money.ts new file mode 100644 index 0000000000..047ef8da95 --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/money.ts @@ -0,0 +1,118 @@ +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable } from '~/mssql-core/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { MsSqlColumnBuilderWithIdentity, MsSqlColumnWithIdentity } from './common.ts'; + +export class MsSqlMoneyBuilder extends MsSqlColumnBuilderWithIdentity<{ + dataType: 'string numeric'; + data: string; + driverParam: string; +}, MsSqlMoneyConfig> { + static override readonly [entityKind]: string = 'MsSqlMoneyBuilder'; + + constructor(name: string, config: MsSqlMoneyConfig | undefined) { + super(name, 'string numeric', 'MsSqlMoney'); + this.config.kind = config?.kind ?? 'money'; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlMoney(table, this.config); + } +} + +export class MsSqlMoney> + extends MsSqlColumnWithIdentity +{ + static override readonly [entityKind]: string = 'MsSqlMoney'; + + readonly kind: 'money' | 'smallmoney' = this.config.kind ?? 'money'; + + override mapFromDriverValue = (value: unknown): string => { + if (typeof value === 'string') return value; + + return String(value); + }; + + getSQLType(): string { + return this.kind; + } +} + +export class MsSqlMoneyNumberBuilder extends MsSqlColumnBuilderWithIdentity<{ + dataType: 'number'; + data: number; + driverParam: string; +}, MsSqlMoneyConfig> { + static override readonly [entityKind]: string = 'MsSqlMoneyNumberBuilder'; + + constructor(name: string, config: MsSqlMoneyConfig | undefined) { + super(name, 'number', 'MsSqlMoneyNumber'); + this.config.kind = config?.kind ?? 'money'; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlMoneyNumber(table, this.config); + } +} + +export class MsSqlMoneyNumber> + extends MsSqlColumnWithIdentity +{ + static override readonly [entityKind]: string = 'MsSqlMoneyNumber'; + + readonly kind: 'money' | 'smallmoney' = this.config.kind ?? 'money'; + + override mapFromDriverValue = (value: unknown): number => { + if (typeof value === 'number') return value; + + return Number(value); + }; + + override mapToDriverValue = String; + + getSQLType(): string { + return this.kind; + } +} + +export interface MsSqlMoneyConfig { + mode?: TMode; + kind?: 'money' | 'smallmoney'; +} + +export function money( + config?: Omit, 'kind'>, +): Equal extends true ? MsSqlMoneyNumberBuilder : MsSqlMoneyBuilder; +export function money( + name: string, + config?: Omit, 'kind'>, +): Equal extends true ? MsSqlMoneyNumberBuilder : MsSqlMoneyBuilder; +export function money(a?: string | Omit, b?: Omit) { + const { name, config } = getColumnNameAndConfig>(a, b); + const moneyConfig = { ...config, kind: 'money' } as const; + return moneyConfig.mode === 'number' + ? new MsSqlMoneyNumberBuilder(name, moneyConfig) + : new MsSqlMoneyBuilder(name, moneyConfig); +} + +export function smallmoney( + config?: Omit, 'kind'>, +): Equal extends true ? MsSqlMoneyNumberBuilder : MsSqlMoneyBuilder; +export function smallmoney( + name: string, + config?: Omit, 'kind'>, +): Equal extends true ? MsSqlMoneyNumberBuilder : MsSqlMoneyBuilder; +export function smallmoney(a?: string | Omit, b?: Omit) { + const { name, config } = getColumnNameAndConfig>(a, b); + const moneyConfig = { ...config, kind: 'smallmoney' } as const; + return moneyConfig.mode === 'number' + ? new MsSqlMoneyNumberBuilder(name, moneyConfig) + : new MsSqlMoneyBuilder(name, moneyConfig); +} diff --git a/drizzle-orm/src/mssql-core/columns/rowversion.ts b/drizzle-orm/src/mssql-core/columns/rowversion.ts new file mode 100644 index 0000000000..d9b6229bc7 --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/rowversion.ts @@ -0,0 +1,42 @@ +import type { HasDefault, NotNull } from '~/column-builder.ts'; +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable } from '~/mssql-core/table.ts'; +import { MsSqlColumn, MsSqlColumnBuilder } from './common.ts'; + +export class MsSqlRowVersionBuilder extends MsSqlColumnBuilder<{ + dataType: 'object buffer'; + data: Buffer; + driverParam: Buffer; +}> { + static override readonly [entityKind]: string = 'MsSqlRowVersionBuilder'; + + constructor(name: string) { + super(name, 'object buffer', 'MsSqlRowVersion'); + this.config.hasDefault = true; + this.config.notNull = true; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlRowVersion(table, this.config); + } +} + +export class MsSqlRowVersion> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlRowVersion'; + + override shouldDisableInsert(): boolean { + return true; + } + + getSQLType(): string { + return 'rowversion'; + } +} + +export function rowversion(name?: string): NotNull> { + return new MsSqlRowVersionBuilder(name ?? '') as NotNull>; +} diff --git a/drizzle-orm/src/mssql-core/columns/smalldatetime.ts b/drizzle-orm/src/mssql-core/columns/smalldatetime.ts new file mode 100644 index 0000000000..2b85c47cff --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/smalldatetime.ts @@ -0,0 +1,98 @@ +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable, MsSqlTable } from '~/mssql-core/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { MsSqlColumn } from './common.ts'; +import { MsSqlDateColumnBaseBuilder } from './date.common.ts'; +import type { MsSqlDatetimeConfig } from './datetime.ts'; + +export class MsSqlSmallDateTimeBuilder extends MsSqlDateColumnBaseBuilder<{ + dataType: 'object date'; + data: Date; + driverParam: string | Date; +}, MsSqlDatetimeConfig> { + static override readonly [entityKind]: string = 'MsSqlSmallDateTimeBuilder'; + + constructor(name: string) { + super(name, 'object date', 'MsSqlSmallDateTime'); + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlSmallDateTime(table, this.config); + } +} + +export class MsSqlSmallDateTime> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlSmallDateTime'; + + constructor( + table: MsSqlTable, + config: MsSqlSmallDateTimeBuilder['config'], + ) { + super(table, config); + } + + getSQLType(): string { + return 'smalldatetime'; + } + + mapFromJsonValue(value: string): Date { + return new Date(value); + } +} + +export class MsSqlSmallDateTimeStringBuilder extends MsSqlDateColumnBaseBuilder<{ + dataType: 'string datetime'; + data: string; + driverParam: string | Date; +}, MsSqlDatetimeConfig> { + static override readonly [entityKind]: string = 'MsSqlSmallDateTimeStringBuilder'; + + constructor(name: string) { + super(name, 'string datetime', 'MsSqlSmallDateTimeString'); + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlSmallDateTimeString(table, this.config); + } +} + +export class MsSqlSmallDateTimeString> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlSmallDateTimeString'; + + constructor( + table: MsSqlTable, + config: MsSqlSmallDateTimeStringBuilder['config'], + ) { + super(table, config); + } + + getSQLType(): string { + return 'smalldatetime'; + } + + override mapFromDriverValue = (value: Date | string | null): string | null => { + return typeof value === 'string' ? value : value?.toISOString() ?? null; + }; +} + +export function smalldatetime( + config?: MsSqlDatetimeConfig, +): Equal extends true ? MsSqlSmallDateTimeStringBuilder : MsSqlSmallDateTimeBuilder; +export function smalldatetime( + name: string, + config?: MsSqlDatetimeConfig, +): Equal extends true ? MsSqlSmallDateTimeStringBuilder : MsSqlSmallDateTimeBuilder; +export function smalldatetime(a?: string | MsSqlDatetimeConfig, b?: MsSqlDatetimeConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'string') { + return new MsSqlSmallDateTimeStringBuilder(name); + } + return new MsSqlSmallDateTimeBuilder(name); +} diff --git a/drizzle-orm/src/mssql-core/columns/spatial.ts b/drizzle-orm/src/mssql-core/columns/spatial.ts new file mode 100644 index 0000000000..cabef7da37 --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/spatial.ts @@ -0,0 +1,339 @@ +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable } from '~/mssql-core/table.ts'; +import { type Equal, getColumnNameAndConfig } from '~/utils.ts'; +import { MsSqlColumn, MsSqlColumnBuilder } from './common.ts'; + +export interface MsSqlSpatialConfig { + mode?: TMode; + srid?: number; +} + +const parsePoint = (value: string): [number, number] => { + const match = /^POINT\s*\(\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\s*\)$/i.exec(value.trim()); + if (!match) { + throw new Error(`Expected POINT WKT, received: ${value}`); + } + + return [Number(match[1]), Number(match[2])]; +}; + +const pointToWkt = (x: number, y: number) => `POINT (${x} ${y})`; + +export class MsSqlGeographyBuilder extends MsSqlColumnBuilder<{ + dataType: 'object geometry'; + data: unknown; + driverParam: unknown; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeographyBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'object geometry', 'MsSqlGeography'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeography(table, this.config); + } +} + +export class MsSqlGeography> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlGeography'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geography'; + } +} + +export class MsSqlGeographyWktBuilder extends MsSqlColumnBuilder<{ + dataType: 'string'; + data: string; + driverParam: string; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeographyWktBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'string', 'MsSqlGeographyWkt'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeographyWkt(table, this.config); + } +} + +export class MsSqlGeographyWkt> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlGeographyWkt'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geography'; + } + + override mapFromDriverValue = (value: unknown): string => String(value); +} + +export class MsSqlGeographyTupleBuilder extends MsSqlColumnBuilder<{ + dataType: 'array geometry'; + data: [number, number]; + driverParam: string; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeographyTupleBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'array geometry', 'MsSqlGeographyTuple'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeographyTuple(table, this.config); + } +} + +export class MsSqlGeographyTuple> + extends MsSqlColumn +{ + static override readonly [entityKind]: string = 'MsSqlGeographyTuple'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geography'; + } + + override mapFromDriverValue = (value: string): [number, number] => parsePoint(value); + + override mapToDriverValue = (value: [number, number]): string => pointToWkt(value[0], value[1]); +} + +export class MsSqlGeographyObjectBuilder extends MsSqlColumnBuilder<{ + dataType: 'object geometry'; + data: { x: number; y: number }; + driverParam: string; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeographyObjectBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'object geometry', 'MsSqlGeographyObject'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeographyObject(table, this.config); + } +} + +export class MsSqlGeographyObject> + extends MsSqlColumn +{ + static override readonly [entityKind]: string = 'MsSqlGeographyObject'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geography'; + } + + override mapFromDriverValue = (value: string): { x: number; y: number } => { + const [x, y] = parsePoint(value); + return { x, y }; + }; + + override mapToDriverValue = (value: { x: number; y: number }): string => pointToWkt(value.x, value.y); +} + +export class MsSqlGeometryBuilder extends MsSqlColumnBuilder<{ + dataType: 'object geometry'; + data: unknown; + driverParam: unknown; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeometryBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'object geometry', 'MsSqlGeometry'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeometry(table, this.config); + } +} + +export class MsSqlGeometry> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlGeometry'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geometry'; + } +} + +export class MsSqlGeometryWktBuilder extends MsSqlColumnBuilder<{ + dataType: 'string'; + data: string; + driverParam: string; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeometryWktBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'string', 'MsSqlGeometryWkt'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeometryWkt(table, this.config); + } +} + +export class MsSqlGeometryWkt> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlGeometryWkt'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geometry'; + } + + override mapFromDriverValue = (value: unknown): string => String(value); +} + +export class MsSqlGeometryTupleBuilder extends MsSqlColumnBuilder<{ + dataType: 'array geometry'; + data: [number, number]; + driverParam: string; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeometryTupleBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'array geometry', 'MsSqlGeometryTuple'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeometryTuple(table, this.config); + } +} + +export class MsSqlGeometryTuple> + extends MsSqlColumn +{ + static override readonly [entityKind]: string = 'MsSqlGeometryTuple'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geometry'; + } + + override mapFromDriverValue = (value: string): [number, number] => parsePoint(value); + + override mapToDriverValue = (value: [number, number]): string => pointToWkt(value[0], value[1]); +} + +export class MsSqlGeometryObjectBuilder extends MsSqlColumnBuilder<{ + dataType: 'object geometry'; + data: { x: number; y: number }; + driverParam: string; +}, MsSqlSpatialConfig> { + static override readonly [entityKind]: string = 'MsSqlGeometryObjectBuilder'; + + constructor(name: string, config?: MsSqlSpatialConfig) { + super(name, 'object geometry', 'MsSqlGeometryObject'); + this.config.srid = config?.srid; + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlGeometryObject(table, this.config); + } +} + +export class MsSqlGeometryObject> + extends MsSqlColumn +{ + static override readonly [entityKind]: string = 'MsSqlGeometryObject'; + + readonly srid: number | undefined = this.config.srid; + + getSQLType(): string { + return 'geometry'; + } + + override mapFromDriverValue = (value: string): { x: number; y: number } => { + const [x, y] = parsePoint(value); + return { x, y }; + }; + + override mapToDriverValue = (value: { x: number; y: number }): string => pointToWkt(value.x, value.y); +} + +export function geography(): MsSqlGeographyBuilder; +export function geography( + config: MsSqlSpatialConfig, +): Equal extends true ? MsSqlGeographyWktBuilder + : Equal extends true ? MsSqlGeographyTupleBuilder + : Equal extends true ? MsSqlGeographyObjectBuilder + : MsSqlGeographyBuilder; +export function geography( + name: string, + config?: MsSqlSpatialConfig, +): Equal extends true ? MsSqlGeographyWktBuilder + : Equal extends true ? MsSqlGeographyTupleBuilder + : Equal extends true ? MsSqlGeographyObjectBuilder + : MsSqlGeographyBuilder; +export function geography(a?: string | MsSqlSpatialConfig, b?: MsSqlSpatialConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'wkt') return new MsSqlGeographyWktBuilder(name, config); + if (config?.mode === 'tuple') return new MsSqlGeographyTupleBuilder(name, config); + if (config?.mode === 'xy') return new MsSqlGeographyObjectBuilder(name, config); + return new MsSqlGeographyBuilder(name, config); +} + +export function geometry(): MsSqlGeometryBuilder; +export function geometry( + config: MsSqlSpatialConfig, +): Equal extends true ? MsSqlGeometryWktBuilder + : Equal extends true ? MsSqlGeometryTupleBuilder + : Equal extends true ? MsSqlGeometryObjectBuilder + : MsSqlGeometryBuilder; +export function geometry( + name: string, + config?: MsSqlSpatialConfig, +): Equal extends true ? MsSqlGeometryWktBuilder + : Equal extends true ? MsSqlGeometryTupleBuilder + : Equal extends true ? MsSqlGeometryObjectBuilder + : MsSqlGeometryBuilder; +export function geometry(a?: string | MsSqlSpatialConfig, b?: MsSqlSpatialConfig) { + const { name, config } = getColumnNameAndConfig(a, b); + if (config?.mode === 'wkt') return new MsSqlGeometryWktBuilder(name, config); + if (config?.mode === 'tuple') return new MsSqlGeometryTupleBuilder(name, config); + if (config?.mode === 'xy') return new MsSqlGeometryObjectBuilder(name, config); + return new MsSqlGeometryBuilder(name, config); +} diff --git a/drizzle-orm/src/mssql-core/columns/time.ts b/drizzle-orm/src/mssql-core/columns/time.ts index 07ff26eaca..e3196209b7 100644 --- a/drizzle-orm/src/mssql-core/columns/time.ts +++ b/drizzle-orm/src/mssql-core/columns/time.ts @@ -85,6 +85,10 @@ export class MsSqlTime< const precision = this.fsp === undefined ? '' : `(${this.fsp})`; return `time${precision}`; } + + mapFromJsonValue(value: string): Date { + return new Date(`1970-01-01T${value}Z`); + } } export type TimeConfig = { precision?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; diff --git a/drizzle-orm/src/mssql-core/columns/uniqueidentifier.ts b/drizzle-orm/src/mssql-core/columns/uniqueidentifier.ts new file mode 100644 index 0000000000..6235eaab2d --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/uniqueidentifier.ts @@ -0,0 +1,35 @@ +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable } from '~/mssql-core/table.ts'; +import { MsSqlColumn, MsSqlColumnBuilder } from './common.ts'; + +export class MsSqlUniqueIdentifierBuilder extends MsSqlColumnBuilder<{ + dataType: 'string uuid'; + data: string; + driverParam: string; +}> { + static override readonly [entityKind]: string = 'MsSqlUniqueIdentifierBuilder'; + + constructor(name: string) { + super(name, 'string uuid', 'MsSqlUniqueIdentifier'); + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlUniqueIdentifier(table, this.config); + } +} + +export class MsSqlUniqueIdentifier> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlUniqueIdentifier'; + + getSQLType(): string { + return 'uniqueidentifier'; + } +} + +export function uniqueidentifier(name?: string) { + return new MsSqlUniqueIdentifierBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/mssql-core/columns/varbinary.ts b/drizzle-orm/src/mssql-core/columns/varbinary.ts index 896d8fc55d..2a46d8b7a7 100644 --- a/drizzle-orm/src/mssql-core/columns/varbinary.ts +++ b/drizzle-orm/src/mssql-core/columns/varbinary.ts @@ -37,6 +37,10 @@ export class MsSqlVarBinary< getSQLType(): string { return this.config.rawLength === undefined ? `varbinary` : `varbinary(${this.config.rawLength})`; } + + mapFromJsonValue(value: string): Buffer { + return Buffer.from(value, 'base64'); + } } export interface MsSqlVarbinaryOptions { diff --git a/drizzle-orm/src/mssql-core/columns/xml.ts b/drizzle-orm/src/mssql-core/columns/xml.ts new file mode 100644 index 0000000000..7494d74809 --- /dev/null +++ b/drizzle-orm/src/mssql-core/columns/xml.ts @@ -0,0 +1,35 @@ +import type { ColumnBaseConfig } from '~/column.ts'; +import { entityKind } from '~/entity.ts'; +import type { AnyMsSqlTable } from '~/mssql-core/table.ts'; +import { MsSqlColumn, MsSqlColumnBuilder } from './common.ts'; + +export class MsSqlXmlBuilder extends MsSqlColumnBuilder<{ + dataType: 'string'; + data: string; + driverParam: string; +}> { + static override readonly [entityKind]: string = 'MsSqlXmlBuilder'; + + constructor(name: string) { + super(name, 'string', 'MsSqlXml'); + } + + /** @internal */ + override build( + table: AnyMsSqlTable<{ name: TTableName }>, + ) { + return new MsSqlXml(table, this.config); + } +} + +export class MsSqlXml> extends MsSqlColumn { + static override readonly [entityKind]: string = 'MsSqlXml'; + + getSQLType(): string { + return 'xml'; + } +} + +export function xml(name?: string) { + return new MsSqlXmlBuilder(name ?? ''); +} diff --git a/drizzle-orm/src/mssql-core/db.ts b/drizzle-orm/src/mssql-core/db.ts index f5dd6eaded..52ababc765 100644 --- a/drizzle-orm/src/mssql-core/db.ts +++ b/drizzle-orm/src/mssql-core/db.ts @@ -1,11 +1,14 @@ import type * as V1 from '~/_relations.ts'; +import type { Cache } from '~/cache/core/cache.ts'; import { entityKind } from '~/entity.ts'; import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; +import type { AnyRelations, EmptyRelations } from '~/relations.ts'; import { SelectionProxyHandler } from '~/selection-proxy.ts'; -import { type ColumnsSelection, sql, type SQLWrapper } from '~/sql/sql.ts'; +import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from '~/sql/sql.ts'; import { WithSubquery } from '~/subquery.ts'; import type { DrizzleTypeError } from '~/utils.ts'; import type { MsSqlDialect } from './dialect.ts'; +import { MsSqlCountBuilder } from './query-builders/count.ts'; import { MsSqlDeleteBase, MsSqlInsertBuilder, @@ -13,53 +16,82 @@ import { MsSqlUpdateBuilder, QueryBuilder, } from './query-builders/index.ts'; -import { RelationalQueryBuilder } from './query-builders/query.ts'; +import { RelationalQueryBuilder } from './query-builders/query-v2.ts'; +import { RelationalQueryBuilder as RelationalQueryBuilderV1 } from './query-builders/query.ts'; +import { MsSqlRaw } from './query-builders/raw.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import type { MsSqlSession, MsSqlTransaction, MsSqlTransactionConfig, + PreparedQueryConfig, PreparedQueryHKTBase, QueryResultHKT, QueryResultKind, } from './session.ts'; import type { WithSubqueryWithSelection } from './subquery.ts'; import type { MsSqlTable } from './table.ts'; +import type { MsSqlViewBase } from './view-base.ts'; +import type { MsSqlView } from './view.ts'; export class MsSqlDatabase< TQueryResult extends QueryResultHKT, TPreparedQueryHKT extends PreparedQueryHKTBase, TFullSchema extends Record = {}, TSchema extends V1.TablesRelationalConfig = V1.ExtractTablesWithRelations, + TRelations extends AnyRelations = EmptyRelations, > { static readonly [entityKind]: string = 'MsSqlDatabase'; declare readonly _: { readonly schema: TSchema | undefined; readonly tableNamesMap: Record; + readonly relations: TRelations; + }; + + query: { + [K in keyof TRelations]: RelationalQueryBuilder; }; _query: TFullSchema extends Record ? DrizzleTypeError<'Seems like the schema generic is missing - did you forget to add it to your DB type?'> : { - [K in keyof TSchema]: RelationalQueryBuilder; + [K in keyof TSchema]: RelationalQueryBuilderV1; }; constructor( /** @internal */ readonly dialect: MsSqlDialect, /** @internal */ - readonly session: MsSqlSession, + readonly session: MsSqlSession, schema: V1.RelationalSchemaConfig | undefined, + relations: TRelations = {} as TRelations, ) { this._ = schema - ? { schema: schema.schema, tableNamesMap: schema.tableNamesMap } - : { schema: undefined, tableNamesMap: {} }; + ? { schema: schema.schema, tableNamesMap: schema.tableNamesMap, relations } + : { schema: undefined, tableNamesMap: {}, relations }; + this.query = {} as typeof this['query']; + for (const [tableName, relation] of Object.entries(relations)) { + (this.query as MsSqlDatabase< + TQueryResult, + TPreparedQueryHKT, + TFullSchema, + TSchema, + AnyRelations + >['query'])[tableName] = new RelationalQueryBuilder( + relations, + relation.table as MsSqlTable | MsSqlView, + relation, + dialect, + session, + ); + } + this._query = {} as typeof this['_query']; if (this._.schema) { for (const [tableName, columns] of Object.entries(this._.schema)) { (this._query as MsSqlDatabase>['_query'])[tableName] = - new RelationalQueryBuilder( + new RelationalQueryBuilderV1( schema!.fullSchema, this._.schema, this._.tableNamesMap, @@ -70,6 +102,8 @@ export class MsSqlDatabase< ); } } + + this.$cache = { invalidate: async (_params: any) => {} }; } /** @@ -123,6 +157,15 @@ export class MsSqlDatabase< }; } + $count( + source: MsSqlTable | MsSqlViewBase | SQL | SQLWrapper, + filters?: SQL, + ) { + return new MsSqlCountBuilder({ source, filters, session: this.session, dialect: this.dialect }); + } + + $cache: { invalidate: Cache['onMutate'] }; + /** * Incorporates a previously defined CTE (using `$with`) into the main query. * @@ -327,35 +370,52 @@ export class MsSqlDatabase< return new MsSqlDeleteBase(table, this.session, this.dialect); } - execute( + execute( query: SQLWrapper | string, - ): Promise> { - return this.session.execute((typeof query === 'string' ? sql.raw(query) : query).getSQL()); + ): MsSqlRaw> { + const sequel = typeof query === 'string' ? sql.raw(query) : query.getSQL(); + const builtQuery = this.dialect.sqlToQuery(sequel); + const prepared = this.session.prepareQuery< + PreparedQueryConfig & { execute: QueryResultKind }, + TPreparedQueryHKT + >( + builtQuery, + undefined, + ); + return new MsSqlRaw( + () => prepared.execute(), + sequel, + builtQuery, + (result) => result, + ); } transaction( transaction: ( - tx: MsSqlTransaction, + tx: MsSqlTransaction, config?: MsSqlTransactionConfig, ) => Promise, config?: MsSqlTransactionConfig, ): Promise { - return this.session.transaction(transaction, config); + return (this.session as MsSqlSession) + .transaction(transaction, config); } } -export type MySQLWithReplicas = Q & { $primary: Q }; +export type MySQLWithReplicas = Q & { $primary: Q; $replicas: Q[] }; export const withReplicas = < HKT extends QueryResultHKT, TPreparedQueryHKT extends PreparedQueryHKTBase, TFullSchema extends Record, TSchema extends V1.TablesRelationalConfig, + TRelations extends AnyRelations, Q extends MsSqlDatabase< HKT, TPreparedQueryHKT, TFullSchema, - TSchema extends Record ? V1.ExtractTablesWithRelations : TSchema + TSchema extends Record ? V1.ExtractTablesWithRelations : TSchema, + TRelations >, >( primary: Q, @@ -364,6 +424,7 @@ export const withReplicas = < ): MySQLWithReplicas => { const select: Q['select'] = (...args: []) => getReplica(replicas).select(...args); const selectDistinct: Q['selectDistinct'] = (...args: []) => getReplica(replicas).selectDistinct(...args); + const $count: Q['$count'] = (...args: [any]) => getReplica(replicas).$count(...args); const $with: Q['with'] = (...args: []) => getReplica(replicas).with(...args); const update: Q['update'] = (...args: [any]) => primary.update(...args); @@ -383,7 +444,11 @@ export const withReplicas = < $replicas: replicas, select, selectDistinct, + $count, with: $with, + get query() { + return getReplica(replicas).query; + }, get _query() { return getReplica(replicas)._query; }, diff --git a/drizzle-orm/src/mssql-core/dialect.ts b/drizzle-orm/src/mssql-core/dialect.ts index ab96eba844..cce7a3379f 100644 --- a/drizzle-orm/src/mssql-core/dialect.ts +++ b/drizzle-orm/src/mssql-core/dialect.ts @@ -10,28 +10,76 @@ import { Column } from '~/column.ts'; import { entityKind, is } from '~/entity.ts'; import type { MigrationConfig, MigrationMeta, MigratorInitFailResponse } from '~/migrator.ts'; import { getMigrationsToRun } from '~/migrator.utils.ts'; -import { Param, type Query, SQL, sql, type SQLChunk, View } from '~/sql/sql.ts'; +import type { + AnyOne, + BuildRelationalQueryResult, + ColumnWithTSName, + DBQueryConfigWithComment, + Relation, + RelationalRowsMapperGenerator, + TableRelationalConfig, + TablesRelationalConfig, + WithContainer, +} from '~/relations.ts'; +import { + getTableAsAliasSQL, + makeDefaultRqbMapper, + makeJitRqbMapper, + One, + relationExtrasToSQL, + relationsFilterToSQL, + relationsOrderToSQL, + relationToSQL, +} from '~/relations.ts'; +import { + isSQLWrapper, + Param, + type Query, + SQL, + sql, + type SQLChunk, + type SQLWrapper, + StringChunk, + View, +} from '~/sql/sql.ts'; import { Subquery } from '~/subquery.ts'; -import { getTableName, getTableUniqueName, Table } from '~/table.ts'; +import { getTableName, getTableUniqueName, Table, TableColumns } from '~/table.ts'; import { upgradeIfNeeded } from '~/up-migrations/mssql.ts'; import { orderSelectedFields, type UpdateSet } from '~/utils.ts'; import { and, DrizzleError, eq, type Name, ViewBaseConfig } from '../index.ts'; import { MsSqlColumn } from './columns/common.ts'; +import { MsSqlCustomColumn } from './columns/custom.ts'; import type { MsSqlDeleteConfig } from './query-builders/delete.ts'; import type { MsSqlInsertConfig } from './query-builders/insert.ts'; import type { MsSqlSelectConfig, SelectedFieldsOrdered } from './query-builders/select.types.ts'; import type { MsSqlUpdateConfig } from './query-builders/update.ts'; import type { MsSqlSession } from './session.ts'; import { MsSqlTable } from './table.ts'; +import { getTableConfig } from './utils.ts'; import { MsSqlViewBase } from './view-base.ts'; +import type { MsSqlView } from './view.ts'; // Will add codecs here, do not remove -export interface MsSqlDialectConfig {} +export interface MsSqlDialectConfig { + useJitMappers?: boolean; +} export class MsSqlDialect { static readonly [entityKind]: string = 'MsSqlDialect'; - constructor(_config?: MsSqlDialectConfig) {} + readonly mapperGenerators: { + relationalRows: RelationalRowsMapperGenerator; + }; + + constructor(config?: MsSqlDialectConfig) { + this.mapperGenerators = config?.useJitMappers + ? { + relationalRows: makeJitRqbMapper, + } + : { + relationalRows: makeDefaultRqbMapper, + }; + } async migrate( migrations: MigrationMeta[], @@ -549,6 +597,29 @@ export class MsSqlDialect { ? sql` without_array_wrapper` : undefined }`; + } else if (_for?.mode === 'xml') { + const xmlMode = _for.type ?? 'raw'; + const xmlPath = xmlMode === 'path' && _for.path !== undefined + ? sql.raw(`(${this.escapeString(_for.path)})`) + : undefined; + const xmlRoot = _for.options?.root + ? sql.raw(`, root(${this.escapeString(_for.options.root)})`) + : undefined; + const xmlElements = _for.options?.elements + ? sql.raw( + typeof _for.options.elements === 'object' + ? `, elements${_for.options.elements.xsinil ? ' xsinil' : ''}${ + _for.options.elements.absent ? ' absent' : '' + }` + : ', elements', + ) + : undefined; + const xmlBinaryBase64 = _for.options?.binaryBase64 ? sql.raw(', binary base64') : undefined; + const xmlType = _for.options?.type ? sql.raw(', type') : undefined; + + forSQL = sql` for xml ${sql.raw(xmlMode)}${xmlPath}${xmlRoot}${xmlElements}${xmlBinaryBase64}${xmlType}`; + } else if (_for?.mode === 'browse') { + forSQL = sql` for browse`; } const finalQuery = @@ -695,7 +766,370 @@ export class MsSqlDialect { return res; } + private nestedSelectionerror() { + throw new DrizzleError({ + message: `Views with nested selections are not supported by the relational query builder`, + }); + } + + private rewriteRqbRelationFilterLimit(query: SQL | undefined): SQL | undefined { + if (!query) return undefined; + + const rewriteChunk = (chunk: SQLChunk): SQLChunk => { + if (is(chunk, StringChunk)) { + return new StringChunk(chunk.value.map((value) => + value + .replaceAll('(select * from', '(select top(1) * from') + .replaceAll(' limit 1)', ')') + )); + } + + if (is(chunk, SQL)) { + const rewritten = new SQL(chunk.queryChunks.map(rewriteChunk)); + rewritten.shouldInlineParams = chunk.shouldInlineParams; + return rewritten; + } + + return chunk; + }; + + const rewritten = new SQL(query.queryChunks.map(rewriteChunk)); + rewritten.shouldInlineParams = query.shouldInlineParams; + return rewritten; + } + + private buildRqbColumn(table: Table | View, column: unknown, key: string, inJson: boolean) { + if (is(column, Column)) { + const name = sql`${table}.${sql.identifier(column.name)}`; + + if (inJson) { + if (is(column, MsSqlCustomColumn)) { + return sql`${column.jsonSelectIdentifier(name, sql)} as ${sql.identifier(key)}`; + } + + switch (column.columnType) { + case 'MsSqlTime': + case 'MsSqlDate': + case 'MsSqlDateString': + case 'MsSqlDateTime': + case 'MsSqlDateTimeString': + case 'MsSqlDateTime2': + case 'MsSqlDateTime2String': + case 'MsSqlDateTimeOffset': + case 'MsSqlDateTimeOffsetString': + case 'MsSqlDecimal': + case 'MsSqlDecimalNumber': + case 'MsSqlDecimalBigInt': + case 'MsSqlNumeric': + case 'MsSqlNumericNumber': + case 'MsSqlNumericBigInt': + case 'MsSqlBigInt53': + case 'MsSqlBigInt64': { + return sql`cast(${name} as varchar(max)) as ${sql.identifier(key)}`; + } + } + } + + return sql`${name} as ${sql.identifier(key)}`; + } + + return sql`${table}.${ + is(column, SQL.Aliased) + ? sql.identifier(column.fieldAlias) + : isSQLWrapper(column) + ? sql.identifier(key) + : this.nestedSelectionerror() + } as ${sql.identifier(key)}`; + } + + private unwrapAllColumns = ( + table: Table | View, + selection: BuildRelationalQueryResult['selection'], + inJson: boolean, + ) => { + return sql.join( + Object.entries(table[TableColumns]).map(([key, column]) => { + selection.push({ + key, + field: column as Column | SQL | SQLWrapper | SQL.Aliased, + }); + + return this.buildRqbColumn(table, column, key, inJson); + }), + sql`, `, + ); + }; + + private getSelectedTableColumns = ( + table: Table | View, + columns: Record, + ) => { + const selectedColumns: ColumnWithTSName[] = []; + const columnContainer = table[TableColumns]; + const entries = Object.entries(columns); + + let colSelectionMode: boolean | undefined; + for (const [key, value] of entries) { + if (value === undefined) continue; + colSelectionMode = colSelectionMode || value; + + if (value) { + selectedColumns.push({ + column: columnContainer[key] as Column | SQL | SQLWrapper | SQL.Aliased, + tsName: key, + }); + } + } + + if (colSelectionMode === false) { + for (const [key, column] of Object.entries(columnContainer)) { + if (columns[key] === false) continue; + + selectedColumns.push({ + column: column as Column | SQL | SQLWrapper | SQL.Aliased | Table, + tsName: key, + }); + } + } + + return selectedColumns; + }; + + private buildColumns = ( + table: MsSqlTable | MsSqlView, + selection: BuildRelationalQueryResult['selection'], + inJson: boolean, + params?: DBQueryConfigWithComment<'many'>, + ) => + params?.columns + ? (() => { + const columnIdentifiers: SQL[] = []; + const selectedColumns = this.getSelectedTableColumns(table, params.columns); + + for (const { column, tsName } of selectedColumns) { + columnIdentifiers.push(this.buildRqbColumn(table, column, tsName, inJson)); + + selection.push({ + key: tsName, + field: column, + }); + } + + return columnIdentifiers.length ? sql.join(columnIdentifiers, sql`, `) : undefined; + })() + : this.unwrapAllColumns(table, selection, inJson); + + private buildRqbJsonSelection(selection: BuildRelationalQueryResult['selection']) { + return sql.join( + selection.map(({ key, selection }) => + selection + ? sql`json_query(${sql.identifier('t')}.${sql.identifier(key)}) as ${sql.identifier(key)}` + : sql`${sql.identifier('t')}.${sql.identifier(key)} as ${sql.identifier(key)}` + ), + sql`, `, + ); + } + + private buildRqbFallbackOrder(table: MsSqlTable | MsSqlView, tableConfig: TableRelationalConfig): SQL { + const columns = table[TableColumns] as Record; + const aliasedColumns = Object.values(columns).map((column) => sql`${column}`); + + if (is(tableConfig.table, MsSqlTable)) { + const { columns: originalColumns, primaryKeys } = getTableConfig(tableConfig.table); + const primaryColumns = [ + ...originalColumns.filter((column) => column.primary), + ...primaryKeys.flatMap((primaryKey) => primaryKey.columns), + ]; + const usedColumnNames = new Set(); + const aliasedPrimaryColumns = primaryColumns.flatMap((primaryColumn) => { + if (usedColumnNames.has(primaryColumn.name)) return []; + usedColumnNames.add(primaryColumn.name); + + const column = Object.values(columns).find((column) => column.name === primaryColumn.name); + return column ? [sql`${column}`] : []; + }); + + if (aliasedPrimaryColumns.length) { + return sql.join(aliasedPrimaryColumns, sql`, `); + } + } + + return aliasedColumns.length ? sql.join(aliasedColumns, sql`, `) : sql`1`; + } + buildRelationalQuery({ + schema, + table, + tableConfig, + queryConfig: config, + relationWhere, + mode, + errorPath, + depth, + throughJoin, + nested, + }: { + schema: TablesRelationalConfig; + table: MsSqlTable | MsSqlView; + tableConfig: TableRelationalConfig; + queryConfig?: DBQueryConfigWithComment<'many'> | true; + relationWhere?: SQL; + mode: 'first' | 'many'; + errorPath?: string; + depth?: number; + throughJoin?: SQL; + nested?: boolean; + }): BuildRelationalQueryResult { + const selection: BuildRelationalQueryResult['selection'] = []; + const isSingle = mode === 'first'; + const params = config === true ? undefined : config; + const currentPath = errorPath ?? ''; + const currentDepth = depth ?? 0; + if (!currentDepth) table = aliasedTable(table, `d${currentDepth}`); + + const limit = isSingle ? 1 : params?.limit; + const offset = params?.offset; + + const configWhere = params?.where + ? this.rewriteRqbRelationFilterLimit( + relationsFilterToSQL( + table, + params.where, + tableConfig.relations, + schema, + ), + ) + : undefined; + const where: SQL | undefined = configWhere && relationWhere + ? and(configWhere, relationWhere) + : configWhere ?? relationWhere; + + const order = params?.orderBy + ? relationsOrderToSQL(table, params.orderBy) + : undefined; + const columns = this.buildColumns(table, selection, !!nested, params); + const extras = params?.extras + ? relationExtrasToSQL(table, params.extras) + : undefined; + if (extras) selection.push(...extras.selection); + + const selectionArr: SQL[] = columns ? [columns] : []; + if (extras?.sql) selectionArr.push(extras.sql); + + const joins = params + ? (() => { + const { with: joins } = params as WithContainer; + if (!joins) return; + + const withEntries = Object.entries(joins).filter(([_, value]) => value); + if (!withEntries.length) return; + + return sql.join( + withEntries.map(([key, join]) => { + const relation = tableConfig.relations[key]! as Relation; + const isSingle = is(relation, One); + const targetTable = aliasedTable( + relation.targetTable, + `d${currentDepth + 1}`, + ); + const throughTable = relation.throughTable + ? (aliasedTable(relation.throughTable, `tr${currentDepth}`) as Table | View) + : undefined; + const { filter, joinCondition } = relationToSQL( + relation, + table, + targetTable, + throughTable, + ); + + selectionArr.push( + sql`json_query(${sql.identifier(key)}.${sql.identifier('r')}) as ${sql.identifier(key)}`, + ); + + const throughJoin = throughTable + ? sql` inner join ${getTableAsAliasSQL(throughTable)} on ${joinCondition!}` + : undefined; + + const innerQuery = this.buildRelationalQuery({ + table: targetTable as MsSqlTable | MsSqlView, + mode: isSingle ? 'first' : 'many', + schema, + queryConfig: join as DBQueryConfigWithComment, + tableConfig: schema[relation.targetTableName]!, + relationWhere: filter, + errorPath: `${currentPath.length ? `${currentPath}.` : ''}${key}`, + depth: currentDepth + 1, + throughJoin, + nested: true, + }); + + selection.push({ + field: targetTable, + key, + selection: innerQuery.selection, + isArray: !isSingle, + isOptional: ((relation as AnyOne).optional ?? false) + || (join !== true + && !!(join as Exclude) + .where), + }); + + const jsonSelection = this.buildRqbJsonSelection(innerQuery.selection); + const relationJson = isSingle + ? sql`json_query((select ${jsonSelection} from (${innerQuery.sql}) as ${ + sql.identifier('t') + } for json path, include_null_values, without_array_wrapper))` + : sql`json_query(coalesce((select ${jsonSelection} from (${innerQuery.sql}) as ${ + sql.identifier('t') + } for json path, include_null_values), '[]'))`; + + return sql`outer apply (select ${relationJson} as ${sql.identifier('r')}) as ${sql.identifier(key)}`; + }), + sql` `, + ); + })() + : undefined; + + if (!selectionArr.length) { + throw new DrizzleError({ + message: `No fields selected for table "${tableConfig.name}"${currentPath ? ` ("${currentPath}")` : ''}`, + }); + } + + const selectionSet = sql.join( + selectionArr.filter((entry) => entry !== undefined), + sql`, `, + ); + const comment = config !== true && config?.comment + ? sql.comment(config.comment) + : undefined; + const top = limit !== undefined && offset === undefined ? limit : undefined; + const orderPreservingOffset = nested && order !== undefined && top === undefined && offset === undefined; + // SQL Server requires ORDER BY with OFFSET. Prefer deterministic PK columns; views/no-PK sources + // fall back to all exposed columns, so callers should pass orderBy for business pagination. + const effectiveOrder = order ?? (offset !== undefined ? this.buildRqbFallbackOrder(table, tableConfig) : undefined); + const offsetSql = offset !== undefined + ? sql` offset ${offset} rows` + : orderPreservingOffset + ? sql` offset 0 rows` + : undefined; + const fetchSql = offset !== undefined && limit !== undefined ? sql` fetch next ${limit} rows only` : undefined; + + const query = sql`select${top !== undefined ? sql` top(${top})` : undefined} ${selectionSet} from ${ + getTableAsAliasSQL(table) + }${throughJoin}${joins ? sql` ${joins}` : undefined}${where ? sql` where ${where}` : undefined}${ + effectiveOrder ? sql` order by ${effectiveOrder}` : undefined + }${offsetSql}${fetchSql}${comment ? sql` ${comment}` : undefined}`; + + return { + sql: nested + ? query + : sql`select (${query} for json path, include_null_values) as ${sql.identifier('data')}`, + selection, + }; + } + + buildRelationalQueryV1({ fullSchema, schema, tableNamesMap, @@ -886,7 +1320,7 @@ export class MsSqlDialect { ) ), ); - const builtRelation = this.buildRelationalQuery({ + const builtRelation = this.buildRelationalQueryV1({ fullSchema, schema, tableNamesMap, diff --git a/drizzle-orm/src/mssql-core/indexes.ts b/drizzle-orm/src/mssql-core/indexes.ts index 650d19be74..33caa4160c 100644 --- a/drizzle-orm/src/mssql-core/indexes.ts +++ b/drizzle-orm/src/mssql-core/indexes.ts @@ -1,25 +1,72 @@ import { entityKind } from '~/entity.ts'; import type { SQL } from '~/sql/sql.ts'; +import { IndexedColumn } from './columns/index.ts'; import type { AnyMsSqlColumn, MsSqlColumn } from './columns/index.ts'; import type { MsSqlTable } from './table.ts'; +import type { MsSqlView } from './view.ts'; interface IndexConfig { name: string; + kind: 'btree' | 'fulltext' | 'columnstore'; columns: IndexColumn[]; + include?: IndexColumn[]; /** * If true, the index will be created as `create unique index` instead of `create index`. */ unique?: boolean; + /** + * If true, the index will be created as `create clustered index` instead of `create index`. + */ + clustered?: boolean; + /** * Condition for partial index. */ where?: SQL; + + /** + * The optional WITH clause specifies storage options for the index. + */ + with?: MsSqlIndexWith; + + fulltext?: MsSqlFullTextConfig; +} + +export interface MsSqlIndexWith { + fillFactor?: number; + online?: boolean; +} + +export interface MsSqlColumnStoreIndexWith { + online?: boolean; +} + +export interface MsSqlFullTextConfig { + keyIndex: string; + catalog?: string; + changeTracking?: 'auto' | 'manual' | 'off'; + stoplist?: 'system' | 'off' | (string & {}); } -export type IndexColumn = MsSqlColumn | SQL; +export type IndexColumn = MsSqlColumn | SQL | IndexedColumn; +export type IndexTarget = MsSqlTable | MsSqlView; + +const isMsSqlColumn = (column: IndexColumn): column is MsSqlColumn => { + return 'defaultConfig' in column; +}; + +const cloneColumn = (column: IndexColumn): IndexColumn => { + if (isMsSqlColumn(column)) { + const indexConfig = { ...column.indexConfig }; + column.indexConfig = { ...column.defaultConfig }; + return new IndexedColumn(column.name, indexConfig); + } + + return column; +}; export class IndexBuilderOn { static readonly [entityKind]: string = 'MsSqlIndexBuilderOn'; @@ -27,12 +74,12 @@ export class IndexBuilderOn { constructor(private name: string, private unique: boolean) {} on(...columns: [IndexColumn, ...IndexColumn[]]): IndexBuilder { - return new IndexBuilder(this.name, columns, this.unique); + return new IndexBuilder(this.name, columns.map(cloneColumn), this.unique); } } export interface AnyIndexBuilder { - build(table: MsSqlTable): Index; + build(table: IndexTarget): Index; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -47,6 +94,7 @@ export class IndexBuilder implements AnyIndexBuilder { constructor(name: string, columns: IndexColumn[], unique: boolean) { this.config = { name, + kind: 'btree', columns, unique, }; @@ -57,8 +105,134 @@ export class IndexBuilder implements AnyIndexBuilder { return this; } + include(...columns: [IndexColumn, ...IndexColumn[]]): this { + this.config.include = columns.map(cloneColumn); + return this; + } + + with(obj: MsSqlIndexWith): this { + this.config.with = obj; + return this; + } + + clustered(): this { + this.config.clustered = true; + return this; + } + + nonClustered(): this { + this.config.clustered = false; + return this; + } + + /** @internal */ + build(table: IndexTarget): Index { + return new Index(this.config, table); + } +} + +export class FullTextIndexBuilderOn { + static readonly [entityKind]: string = 'MsSqlFullTextIndexBuilderOn'; + + constructor(private name: string) {} + + on(...columns: [IndexColumn, ...IndexColumn[]]): FullTextIndexBuilder { + return new FullTextIndexBuilder(this.name, columns.map(cloneColumn)); + } +} + +export interface FullTextIndexBuilder extends AnyIndexBuilder {} + +export class FullTextIndexBuilder implements AnyIndexBuilder { + static readonly [entityKind]: string = 'MsSqlFullTextIndexBuilder'; + + /** @internal */ + config: IndexConfig; + + constructor(name: string, columns: IndexColumn[]) { + this.config = { + name, + kind: 'fulltext', + columns, + fulltext: { + keyIndex: '', + }, + }; + } + + keyIndex(name: string): this { + this.config.fulltext!.keyIndex = name; + return this; + } + + catalog(name: string): this { + this.config.fulltext!.catalog = name; + return this; + } + + changeTracking(value: NonNullable): this { + this.config.fulltext!.changeTracking = value; + return this; + } + + stoplist(value: NonNullable): this { + this.config.fulltext!.stoplist = value; + return this; + } + + /** @internal */ + build(table: IndexTarget): Index { + if (!this.config.fulltext?.keyIndex) { + throw new Error('Fulltext indexes require .keyIndex(name)'); + } + return new Index(this.config, table); + } +} + +export class ColumnStoreIndexBuilderOn { + static readonly [entityKind]: string = 'MsSqlColumnStoreIndexBuilderOn'; + + constructor(private name: string) {} + + on(...columns: [IndexColumn, ...IndexColumn[]]): ColumnStoreIndexBuilder { + return new ColumnStoreIndexBuilder(this.name, columns.map(cloneColumn), false); + } +} + +export interface ColumnStoreIndexBuilder extends AnyIndexBuilder {} + +export class ColumnStoreIndexBuilder implements AnyIndexBuilder { + static readonly [entityKind]: string = 'MsSqlColumnStoreIndexBuilder'; + + /** @internal */ + config: IndexConfig; + + constructor(name: string, columns: IndexColumn[] = [], clustered: boolean) { + this.config = { + name, + kind: 'columnstore', + columns, + clustered, + }; + } + + orderBy(...columns: [IndexColumn, ...IndexColumn[]]): this { + this.config.columns = columns.map(cloneColumn); + return this; + } + + where(condition: SQL): this { + this.config.where = condition; + return this; + } + + with(obj: MsSqlColumnStoreIndexWith): this { + this.config.with = obj; + return this; + } + /** @internal */ - build(table: MsSqlTable): Index { + build(table: IndexTarget): Index { return new Index(this.config, table); } } @@ -66,10 +240,10 @@ export class IndexBuilder implements AnyIndexBuilder { export class Index { static readonly [entityKind]: string = 'MsSqlIndex'; - readonly config: IndexConfig & { table: MsSqlTable }; + readonly config: IndexConfig & { table: IndexTarget }; readonly isNameExplicit: boolean; - constructor(config: IndexConfig, table: MsSqlTable) { + constructor(config: IndexConfig, table: IndexTarget) { this.config = { ...config, table }; this.isNameExplicit = !!config.name; } @@ -88,3 +262,15 @@ export function index(name: string): IndexBuilderOn { export function uniqueIndex(name: string): IndexBuilderOn { return new IndexBuilderOn(name, true); } + +export function fullTextIndex(name: string): FullTextIndexBuilderOn { + return new FullTextIndexBuilderOn(name); +} + +export function columnStoreIndex(name: string): ColumnStoreIndexBuilderOn { + return new ColumnStoreIndexBuilderOn(name); +} + +export function clusteredColumnStoreIndex(name: string): ColumnStoreIndexBuilder { + return new ColumnStoreIndexBuilder(name, [], true); +} diff --git a/drizzle-orm/src/mssql-core/query-builders/count.ts b/drizzle-orm/src/mssql-core/query-builders/count.ts new file mode 100644 index 0000000000..ba840a9166 --- /dev/null +++ b/drizzle-orm/src/mssql-core/query-builders/count.ts @@ -0,0 +1,79 @@ +import { entityKind } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { Query } from '~/sql/sql.ts'; +import { SQL, sql, type SQLWrapper } from '~/sql/sql.ts'; +import { applyMixins } from '~/utils.ts'; +import type { MsSqlDialect } from '../dialect.ts'; +import type { MsSqlSession, PreparedQueryHKTBase } from '../session.ts'; +import type { MsSqlTable } from '../table.ts'; +import type { MsSqlViewBase } from '../view-base.ts'; + +// oxlint-disable-next-line no-unused-vars +export interface MsSqlCountBuilder + extends SQL, SQLWrapper, QueryPromise +{} + +export class MsSqlCountBuilder extends SQL + implements SQLWrapper +{ + static override readonly [entityKind]: string = 'MsSqlCountBuilder'; + + protected dialect: MsSqlDialect; + protected session: MsSqlSession; + + private static buildEmbeddedCount( + source: MsSqlTable | MsSqlViewBase | SQL | SQLWrapper, + filters?: SQL, + parens?: boolean, + ): SQL { + const where = sql` where ${filters}`.if(filters); + const query = sql`select count(*) from ${source}${where}`; + + return parens ? sql`(${query})` : query; + } + + constructor( + protected countConfig: { + source: MsSqlTable | MsSqlViewBase | SQL | SQLWrapper; + filters?: SQL; + dialect: MsSqlDialect; + session: MsSqlSession; + }, + ) { + super(MsSqlCountBuilder.buildEmbeddedCount(countConfig.source, countConfig.filters, true).queryChunks); + this.dialect = countConfig.dialect; + this.session = countConfig.session; + this.mapWith((e) => { + if (typeof e === 'number') return e; + + return Number(e ?? 0); + }); + } + + protected build(): Query { + const { filters, source } = this.countConfig; + const query = MsSqlCountBuilder.buildEmbeddedCount(source, filters); + + return this.dialect.sqlToQuery(query); + } + + execute(placeholderValues?: Record): Promise { + return this.session.prepareQuery< + { + execute: number; + iterator: never; + }, + TPreparedQueryHKT + >( + this.build(), + undefined, + (rows) => { + const value = rows[0]?.[0]; + if (typeof value === 'number') return value; + return value ? Number(value) : 0; + }, + ).execute(placeholderValues) as Promise; + } +} + +applyMixins(MsSqlCountBuilder, [QueryPromise]); diff --git a/drizzle-orm/src/mssql-core/query-builders/delete.ts b/drizzle-orm/src/mssql-core/query-builders/delete.ts index 6c3d0cfb14..33619e53a2 100644 --- a/drizzle-orm/src/mssql-core/query-builders/delete.ts +++ b/drizzle-orm/src/mssql-core/query-builders/delete.ts @@ -16,6 +16,7 @@ import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts'; import { Table } from '~/table.ts'; import { orderSelectedFields } from '~/utils.ts'; import type { MsSqlColumn } from '../columns/common.ts'; +import { extractUsedTable } from '../utils.ts'; import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts'; export type MsSqlDeleteWithout< @@ -218,6 +219,8 @@ export class MsSqlDeleteBase< return this.session.prepareQuery( this.dialect.sqlToQuery(this.getSQL()), this.config.output, + undefined, + { type: 'delete', tables: extractUsedTable(this.config.table) }, ) as MsSqlDeletePrepare; } diff --git a/drizzle-orm/src/mssql-core/query-builders/index.ts b/drizzle-orm/src/mssql-core/query-builders/index.ts index 16f0e1d4d9..d1d2092fe7 100644 --- a/drizzle-orm/src/mssql-core/query-builders/index.ts +++ b/drizzle-orm/src/mssql-core/query-builders/index.ts @@ -1,6 +1,9 @@ +export * from './count.ts'; export * from './delete.ts'; export * from './insert.ts'; export * from './query-builder.ts'; +export * from './query-v2.ts'; +export * from './raw.ts'; export * from './select.ts'; export * from './select.types.ts'; export * from './update.ts'; diff --git a/drizzle-orm/src/mssql-core/query-builders/insert.ts b/drizzle-orm/src/mssql-core/query-builders/insert.ts index b9c5a73619..11ce06e4e0 100644 --- a/drizzle-orm/src/mssql-core/query-builders/insert.ts +++ b/drizzle-orm/src/mssql-core/query-builders/insert.ts @@ -17,6 +17,7 @@ import { Param, SQL } from '~/sql/sql.ts'; import { type InferInsertModel, type InferSelectModel, Table } from '~/table.ts'; import { orderSelectedFields } from '~/utils.ts'; import type { MsSqlColumn } from '../columns/common.ts'; +import { extractUsedTable } from '../utils.ts'; import type { SelectedFieldsFlat, SelectedFieldsOrdered } from './select.types.ts'; export interface MsSqlInsertConfig { @@ -218,6 +219,8 @@ export class MsSqlInsertBase< return this.session.prepareQuery( this.dialect.sqlToQuery(this.getSQL()), this.config.output, + undefined, + { type: 'insert', tables: extractUsedTable(this.config.table) }, ) as MsSqlInsertPrepare; } diff --git a/drizzle-orm/src/mssql-core/query-builders/query-v2.ts b/drizzle-orm/src/mssql-core/query-builders/query-v2.ts new file mode 100644 index 0000000000..5e9fa4a972 --- /dev/null +++ b/drizzle-orm/src/mssql-core/query-builders/query-v2.ts @@ -0,0 +1,135 @@ +import { entityKind } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { + BuildQueryResult, + BuildRelationalQueryResult, + DBQueryConfigWithComment, + TableRelationalConfig, + TablesRelationalConfig, +} from '~/relations.ts'; +import type { Query, SQL, SqlCommenterInput } from '~/sql/sql.ts'; +import type { KnownKeysOnly } from '~/utils.ts'; +import type { MsSqlDialect } from '../dialect.ts'; +import type { MsSqlSession, PreparedQueryConfig, PreparedQueryHKTBase, PreparedQueryKind } from '../session.ts'; +import type { MsSqlTable } from '../table.ts'; +import type { MsSqlView } from '../view.ts'; + +export class RelationalQueryBuilder< + TSchema extends TablesRelationalConfig, + TFields extends TableRelationalConfig, + TPreparedQueryHKT extends PreparedQueryHKTBase, +> { + static readonly [entityKind]: string = 'MsSqlRelationalQueryBuilderV2'; + + constructor( + private schema: TSchema, + private table: MsSqlTable | MsSqlView, + private tableConfig: TableRelationalConfig, + private dialect: MsSqlDialect, + private session: MsSqlSession, + ) {} + + findMany>( + config?: KnownKeysOnly> & { + comment?: SqlCommenterInput; + }, + ): MsSqlRelationalQuery[]> { + return new MsSqlRelationalQuery( + this.schema, + this.table, + this.tableConfig, + this.dialect, + this.session, + config as DBQueryConfigWithComment<'many'> | undefined ?? true, + 'many', + ); + } + + findFirst>( + config?: KnownKeysOnly> & { + comment?: SqlCommenterInput; + }, + ): MsSqlRelationalQuery | undefined> { + return new MsSqlRelationalQuery( + this.schema, + this.table, + this.tableConfig, + this.dialect, + this.session, + config as DBQueryConfigWithComment<'one'> | undefined ?? true, + 'first', + ); + } +} + +export class MsSqlRelationalQuery< + TPreparedQueryHKT extends PreparedQueryHKTBase, + TResult, +> extends QueryPromise { + static override readonly [entityKind]: string = 'MsSqlRelationalQueryV2'; + + declare protected $brand: 'MsSqlRelationalQueryV2'; + + constructor( + private schema: TablesRelationalConfig, + private table: MsSqlTable | MsSqlView, + private tableConfig: TableRelationalConfig, + private dialect: MsSqlDialect, + private session: MsSqlSession, + private config: DBQueryConfigWithComment<'many' | 'one'> | true, + private mode: 'many' | 'first', + ) { + super(); + } + + prepare() { + const { query, builtQuery } = this._toSQL(); + const mapper = this.dialect.mapperGenerators.relationalRows({ + isFirst: this.mode === 'first', + parseJson: false, + parseJsonIfString: false, + rootJsonMappers: true, + selection: query.selection, + }); + + return this.session.prepareQuery( + builtQuery, + undefined, + (rawRows) => { + const json = rawRows.map((row) => row[0] ?? '').join(''); + const rows = json ? JSON.parse(json as string) as Record[] : []; + return mapper(rows) as TResult; + }, + ) as PreparedQueryKind; + } + + private _getQuery() { + return this.dialect.buildRelationalQuery({ + schema: this.schema, + table: this.table, + tableConfig: this.tableConfig, + queryConfig: this.config, + mode: this.mode, + }); + } + + private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: Query } { + const query = this._getQuery(); + const builtQuery = this.dialect.sqlToQuery(query.sql); + + return { builtQuery, query }; + } + + /** @internal */ + getSQL(): SQL { + return this._getQuery().sql; + } + + toSQL(): Query { + return this._toSQL().builtQuery; + } + + override execute(): Promise { + return this.prepare().execute(); + } +} diff --git a/drizzle-orm/src/mssql-core/query-builders/query.ts b/drizzle-orm/src/mssql-core/query-builders/query.ts index b6e4d7fb5a..db8eed43a4 100644 --- a/drizzle-orm/src/mssql-core/query-builders/query.ts +++ b/drizzle-orm/src/mssql-core/query-builders/query.ts @@ -102,7 +102,7 @@ export class MsSqlRelationalQuery< } private _getQuery() { - return this.dialect.buildRelationalQuery({ + return this.dialect.buildRelationalQueryV1({ fullSchema: this.fullSchema, schema: this.schema, tableNamesMap: this.tableNamesMap, diff --git a/drizzle-orm/src/mssql-core/query-builders/raw.ts b/drizzle-orm/src/mssql-core/query-builders/raw.ts new file mode 100644 index 0000000000..003bcd8599 --- /dev/null +++ b/drizzle-orm/src/mssql-core/query-builders/raw.ts @@ -0,0 +1,44 @@ +import { entityKind } from '~/entity.ts'; +import { QueryPromise } from '~/query-promise.ts'; +import type { RunnableQuery } from '~/runnable-query.ts'; +import type { PreparedQuery } from '~/session.ts'; +import type { Query, SQL, SQLWrapper } from '~/sql/sql.ts'; + +export interface MsSqlRaw extends QueryPromise, RunnableQuery, SQLWrapper {} + +export class MsSqlRaw extends QueryPromise + implements RunnableQuery, SQLWrapper, PreparedQuery +{ + static override readonly [entityKind]: string = 'MsSqlRaw'; + + declare readonly _: { + readonly dialect: 'mssql'; + readonly result: TResult; + }; + + constructor( + public execute: () => Promise, + private sql: SQL, + private query: Query, + private mapBatchResult: (result: unknown) => unknown, + ) { + super(); + } + + /** @internal */ + getSQL() { + return this.sql; + } + + getQuery() { + return this.query; + } + + mapResult(result: unknown, isFromBatch?: boolean) { + return isFromBatch ? this.mapBatchResult(result) : result; + } + + _prepare(): PreparedQuery { + return this; + } +} diff --git a/drizzle-orm/src/mssql-core/query-builders/select.ts b/drizzle-orm/src/mssql-core/query-builders/select.ts index cb493bf114..0937813eb9 100644 --- a/drizzle-orm/src/mssql-core/query-builders/select.ts +++ b/drizzle-orm/src/mssql-core/query-builders/select.ts @@ -1,3 +1,4 @@ +import type { CacheConfig, WithCacheConfig } from '~/cache/core/types.ts'; import { entityKind, is } from '~/entity.ts'; import type { MsSqlColumn } from '~/mssql-core/columns/index.ts'; import type { MsSqlDialect } from '~/mssql-core/dialect.ts'; @@ -30,6 +31,7 @@ import { type ValueOrArray, } from '~/utils.ts'; import { ViewBaseConfig } from '~/view-common.ts'; +import { extractUsedTable } from '../utils.ts'; import { MsSqlViewBase } from '../view-base.ts'; import type { AnyMsSqlSelect, @@ -185,6 +187,8 @@ export abstract class MsSqlSelectQueryBuilderBase< /** @internal */ readonly session: MsSqlSession | undefined; protected dialect: MsSqlDialect; + protected cacheConfig?: WithCacheConfig = undefined; + protected usedTables: Set = new Set(); constructor( { table, fields, isPartialSelect, session, dialect, withList, distinct, topValue }: { @@ -215,6 +219,17 @@ export abstract class MsSqlSelectQueryBuilderBase< } as this['_']; this.tableName = getTableLikeName(table); this.joinsNotNullableMap = typeof this.tableName === 'string' ? { [this.tableName]: true } : {}; + for (const item of extractUsedTable(table)) this.usedTables.add(item); + + this.config.withList?.forEach((it) => { + const extracted = extractUsedTable(it); + for (const el of extracted) this.usedTables.add(el); + }); + } + + /** @internal */ + getUsedTables() { + return [...this.usedTables]; } private createJoin( @@ -227,6 +242,8 @@ export abstract class MsSqlSelectQueryBuilderBase< const baseTableName = this.tableName; const tableName = getTableLikeName(table); + for (const item of extractUsedTable(table)) this.usedTables.add(item); + if (typeof tableName === 'string' && this.config.joins?.some((join) => join.alias === tableName)) { throw new Error(`Alias "${tableName}" is already used in this query`); } @@ -824,8 +841,12 @@ export abstract class MsSqlSelectQueryBuilderBase< as( alias: TAlias, ): SubqueryWithSelection { + const usedTables: string[] = []; + usedTables.push(...extractUsedTable(this.config.table)); + if (this.config.joins) { for (const it of this.config.joins) usedTables.push(...extractUsedTable(it.table)); } + return new Proxy( - new Subquery(this.getSQL(), this.config.fields, alias), + new Subquery(this.getSQL(), this.config.fields, alias, false, [...new Set(usedTables)]), new SelectionProxyHandler({ alias, sqlAliasedBehavior: 'alias', sqlBehavior: 'error' }), ) as SubqueryWithSelection; } @@ -846,6 +867,15 @@ export abstract class MsSqlSelectQueryBuilderBase< $dynamic(): MsSqlSelectDynamic { return this as any; } + + $withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false) { + this.cacheConfig = config === undefined + ? { config: {}, enabled: true, autoInvalidate: true } + : config === false + ? { enabled: false } + : { enabled: true, autoInvalidate: true, ...config }; + return this; + } } export interface MsSqlSelectBase< @@ -875,7 +905,9 @@ export interface MsSqlSelectBase< TSelectedFields >, QueryPromise -{} +{ + $withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false): this; +} export class MsSqlSelectBase< TTableName extends string | undefined, @@ -912,7 +944,13 @@ export class MsSqlSelectBase< const query = this.session.prepareQuery< PreparedQueryConfig & { execute: SelectResult[] }, TPreparedQueryHKT - >(this.dialect.sqlToQuery(this.getSQL()), fieldsList); + >( + this.dialect.sqlToQuery(this.getSQL()), + fieldsList, + undefined, + { type: 'select', tables: [...this.usedTables] }, + this.cacheConfig, + ); query.joinsNotNullableMap = this.joinsNotNullableMap; return query as MsSqlSelectPrepare; } diff --git a/drizzle-orm/src/mssql-core/query-builders/select.types.ts b/drizzle-orm/src/mssql-core/query-builders/select.types.ts index f3afe7768f..75c034a0dc 100644 --- a/drizzle-orm/src/mssql-core/query-builders/select.types.ts +++ b/drizzle-orm/src/mssql-core/query-builders/select.types.ts @@ -61,9 +61,23 @@ export interface MsSqlSelectConfig { orderBy?: (MsSqlColumn | SQL | SQL.Aliased)[]; groupBy?: (MsSqlColumn | SQL | SQL.Aliased)[]; for?: { // this is not exposed. Just used internally for the RQB - mode: 'browse'; // TODO: implement in dialect + mode: 'browse'; } | { - mode: 'xml'; // TODO: implement in dialect + mode: 'xml'; + type?: 'raw' | 'auto' | 'explicit' | 'path'; + path?: string; + options?: { + root?: string; + elements?: true | { + xsinil?: true; + absent?: never; + } | { + xsinil?: never; + absent?: true; + }; + binaryBase64?: true; + type?: true; + }; } | { mode: 'json'; type: 'auto' | 'path'; diff --git a/drizzle-orm/src/mssql-core/query-builders/update.ts b/drizzle-orm/src/mssql-core/query-builders/update.ts index 3cff55b91f..0970ed9921 100644 --- a/drizzle-orm/src/mssql-core/query-builders/update.ts +++ b/drizzle-orm/src/mssql-core/query-builders/update.ts @@ -18,6 +18,7 @@ import type { Placeholder, Query, SQL, SQLWrapper } from '~/sql/sql.ts'; import { type InferInsertModel, Table } from '~/table.ts'; import { mapUpdateSet, orderSelectedFields, type UpdateSet } from '~/utils.ts'; import type { MsSqlColumn } from '../columns/common.ts'; +import { extractUsedTable } from '../utils.ts'; import type { SelectedFieldsFlatUpdate, SelectedFieldsOrdered } from './select.types.ts'; export interface MsSqlUpdateConfig { @@ -308,6 +309,8 @@ export class MsSqlUpdateBase< return this.session.prepareQuery( this.dialect.sqlToQuery(this.getSQL()), output.length ? output : undefined, + undefined, + { type: 'update', tables: extractUsedTable(this.config.table) }, ) as MsSqlUpdatePrepare; } diff --git a/drizzle-orm/src/mssql-core/schema.ts b/drizzle-orm/src/mssql-core/schema.ts index 364cb5ad95..bfca513671 100644 --- a/drizzle-orm/src/mssql-core/schema.ts +++ b/drizzle-orm/src/mssql-core/schema.ts @@ -16,8 +16,8 @@ export class MsSqlSchema { return mssqlTableWithSchema(name, columns, extraConfig, this.schemaName, this.casing); }; - view = ((name, columns) => { - return mssqlViewWithSchema(name, columns, this.schemaName, this.casing); + view = ((name, columns, extraConfig) => { + return mssqlViewWithSchema(name, columns, extraConfig, this.schemaName, this.casing); }) as typeof mssqlView; existing(): this { diff --git a/drizzle-orm/src/mssql-core/session.ts b/drizzle-orm/src/mssql-core/session.ts index 73379717cf..a2850f4032 100644 --- a/drizzle-orm/src/mssql-core/session.ts +++ b/drizzle-orm/src/mssql-core/session.ts @@ -1,6 +1,8 @@ import type * as V1 from '~/_relations.ts'; +import type { WithCacheConfig } from '~/cache/core/types.ts'; import { entityKind } from '~/entity.ts'; import { TransactionRollbackError } from '~/errors.ts'; +import type { AnyRelations, EmptyRelations } from '~/relations.ts'; import { type Query, type SQL, sql } from '~/sql/sql.ts'; import type { Assume, Equal } from '~/utils.ts'; import { MsSqlDatabase } from './db.ts'; @@ -59,6 +61,7 @@ export abstract class MsSqlSession< TPreparedQueryHKT extends PreparedQueryHKTBase = PreparedQueryHKTBase, TFullSchema extends Record = Record, TSchema extends V1.TablesRelationalConfig = Record, + TRelations extends AnyRelations = EmptyRelations, > { static readonly [entityKind]: string = 'MsSqlSession'; @@ -68,6 +71,11 @@ export abstract class MsSqlSession< query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + queryMetadata?: { + type: 'select' | 'update' | 'delete' | 'insert'; + tables: string[]; + }, + cacheConfig?: WithCacheConfig, ): PreparedQueryKind; execute(query: SQL): Promise { @@ -80,7 +88,9 @@ export abstract class MsSqlSession< abstract all(query: SQL): Promise; abstract transaction( - transaction: (tx: MsSqlTransaction) => Promise, + transaction: ( + tx: MsSqlTransaction, + ) => Promise, config?: MsSqlTransactionConfig, ): Promise; @@ -104,16 +114,18 @@ export abstract class MsSqlTransaction< TPreparedQueryHKT extends PreparedQueryHKTBase, TFullSchema extends Record = Record, TSchema extends V1.TablesRelationalConfig = Record, -> extends MsSqlDatabase { + TRelations extends AnyRelations = EmptyRelations, +> extends MsSqlDatabase { static override readonly [entityKind]: string = 'MsSqlTransaction'; constructor( dialect: MsSqlDialect, session: MsSqlSession, protected schema: V1.RelationalSchemaConfig | undefined, + protected relations: TRelations, protected readonly nestedIndex: number, ) { - super(dialect, session, schema); + super(dialect, session, schema, relations); } rollback(): never { @@ -122,7 +134,9 @@ export abstract class MsSqlTransaction< /** Nested transactions (aka savepoints) only work with InnoDB engine. */ abstract override transaction( - transaction: (tx: MsSqlTransaction) => Promise, + transaction: ( + tx: MsSqlTransaction, + ) => Promise, ): Promise; } diff --git a/drizzle-orm/src/mssql-core/table.ts b/drizzle-orm/src/mssql-core/table.ts index 0b362f7e86..822be20216 100644 --- a/drizzle-orm/src/mssql-core/table.ts +++ b/drizzle-orm/src/mssql-core/table.ts @@ -102,15 +102,24 @@ export function mssqlTableWithSchema< }), ) as unknown as BuildColumns; - const table = Object.assign(rawTable, builtColumns); - - table[Table.Symbol.Columns] = builtColumns; - table[Table.Symbol.ExtraConfigColumns] = builtColumns as unknown as BuildExtraConfigColumns< + const builtColumnsForExtraConfig = Object.fromEntries( + Object.entries(parsedColumns).map(([name, colBuilderBase]) => { + const colBuilder = colBuilderBase as MsSqlColumnBuilder; + colBuilder.setName(name, casingFn); + const column = colBuilder.buildExtraConfigColumn(rawTable); + return [name, column]; + }), + ) as unknown as BuildExtraConfigColumns< TTableName, TColumnsMap, 'mssql' >; + const table = Object.assign(rawTable, builtColumns); + + table[Table.Symbol.Columns] = builtColumns; + table[Table.Symbol.ExtraConfigColumns] = builtColumnsForExtraConfig; + if (extraConfig) { table[MsSqlTable.Symbol.ExtraConfigBuilder] = extraConfig as unknown as ( self: Record, diff --git a/drizzle-orm/src/mssql-core/utils.ts b/drizzle-orm/src/mssql-core/utils.ts index 4cc73afadf..7f1d84c91f 100644 --- a/drizzle-orm/src/mssql-core/utils.ts +++ b/drizzle-orm/src/mssql-core/utils.ts @@ -1,4 +1,6 @@ import { is } from '~/entity.ts'; +import { SQL } from '~/sql/sql.ts'; +import { Subquery } from '~/subquery.ts'; import { Table } from '~/table.ts'; import { ViewBaseConfig } from '~/view-common.ts'; import type { Check } from './checks.ts'; @@ -6,14 +8,28 @@ import { CheckBuilder } from './checks.ts'; import type { ForeignKey } from './foreign-keys.ts'; import { ForeignKeyBuilder } from './foreign-keys.ts'; import type { Index } from './indexes.ts'; -import { IndexBuilder } from './indexes.ts'; +import { ColumnStoreIndexBuilder, FullTextIndexBuilder, IndexBuilder } from './indexes.ts'; import type { PrimaryKey } from './primary-keys.ts'; import { PrimaryKeyBuilder } from './primary-keys.ts'; import { MsSqlTable } from './table.ts'; import { type UniqueConstraint, UniqueConstraintBuilder } from './unique-constraint.ts'; +import type { MsSqlViewBase } from './view-base.ts'; import { MsSqlViewConfig } from './view-common.ts'; import type { MsSqlView } from './view.ts'; +export function extractUsedTable(table: MsSqlTable | Subquery | MsSqlViewBase | SQL): string[] { + if (is(table, MsSqlTable)) { + return [`${table[Table.Symbol.BaseName]}`]; + } + if (is(table, Subquery)) { + return table._.usedTables ?? []; + } + if (is(table, SQL)) { + return table.usedTables ?? []; + } + return []; +} + export function getTableConfig(table: MsSqlTable) { const columns = Object.values(table[MsSqlTable.Symbol.Columns]); const indexes: Index[] = []; @@ -30,7 +46,7 @@ export function getTableConfig(table: MsSqlTable) { if (extraConfigBuilder !== undefined) { const extraConfig = extraConfigBuilder(table[MsSqlTable.Symbol.Columns]); for (const builder of Object.values(extraConfig)) { - if (is(builder, IndexBuilder)) { + if (is(builder, IndexBuilder) || is(builder, FullTextIndexBuilder) || is(builder, ColumnStoreIndexBuilder)) { indexes.push(builder.build(table)); } else if (is(builder, CheckBuilder)) { checks.push(builder.build(table)); diff --git a/drizzle-orm/src/mssql-core/view.ts b/drizzle-orm/src/mssql-core/view.ts index fcf19d2175..c202b9434f 100644 --- a/drizzle-orm/src/mssql-core/view.ts +++ b/drizzle-orm/src/mssql-core/view.ts @@ -1,12 +1,14 @@ import type { Casing } from '~/casing.ts'; import type { BuildColumns, ColumnBuilderBase } from '~/column-builder.ts'; -import { entityKind } from '~/entity.ts'; +import { entityKind, is } from '~/entity.ts'; import type { TypedQueryBuilder } from '~/query-builders/query-builder.ts'; import type { AddAliasToSelection } from '~/query-builders/select.types.ts'; import { SelectionProxyHandler } from '~/selection-proxy.ts'; import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; import { getTableColumns } from '~/utils.ts'; import type { MsSqlColumn } from './columns/index.ts'; +import type { AnyIndexBuilder, Index } from './indexes.ts'; +import { ColumnStoreIndexBuilder, FullTextIndexBuilder, IndexBuilder } from './indexes.ts'; import { QueryBuilder } from './query-builders/query-builder.ts'; import type { SelectedFields } from './query-builders/select.types.ts'; import { mssqlTableWithSchema } from './table.ts'; @@ -20,6 +22,12 @@ export interface ViewBuilderConfig { checkOption?: boolean; } +type MsSqlViewConfigShape = ViewBuilderConfig & { indexes?: Index[] }; + +export type MsSqlViewExtraConfigValue = AnyIndexBuilder; + +export type MsSqlViewExtraConfig = Record; + export class ViewBuilderCore { static readonly [entityKind]: string = 'MsSqlViewBuilder'; @@ -92,6 +100,11 @@ export class ManualViewBuilder< constructor( name: TName, columns: TColumns, + private extraConfig: + | (( + self: BuildColumns, + ) => MsSqlViewExtraConfig | MsSqlViewExtraConfigValue[]) + | undefined, schema: string | undefined, casing: Casing | undefined, ) { @@ -101,17 +114,41 @@ export class ManualViewBuilder< ) as BuildColumns; } + private buildIndexes(view: MsSqlView): Index[] { + if (this.extraConfig === undefined) { + return []; + } + + const extraConfig = this.extraConfig( + this.columns as BuildColumns, + ); + const extraValues = Array.isArray(extraConfig) ? extraConfig.flat(1) : Object.values(extraConfig); + const indexes: Index[] = []; + + for (const builder of extraValues) { + if (is(builder, IndexBuilder) || is(builder, FullTextIndexBuilder) || is(builder, ColumnStoreIndexBuilder)) { + indexes.push(builder.build(view)); + } + } + + return indexes; + } + existing(): MsSqlViewWithSelection> { + const view = new MsSqlView({ + mssqlConfig: undefined, + config: { + name: this.name, + schema: this.schema, + selectedFields: this.columns, + query: undefined, + }, + }); + view[MsSqlViewConfig] = { + indexes: this.buildIndexes(view), + }; return new Proxy( - new MsSqlView({ - mssqlConfig: undefined, - config: { - name: this.name, - schema: this.schema, - selectedFields: this.columns, - query: undefined, - }, - }), + view, new SelectionProxyHandler({ alias: this.name, sqlBehavior: 'error', @@ -122,16 +159,21 @@ export class ManualViewBuilder< } as(query: SQL): MsSqlViewWithSelection> { + const view = new MsSqlView({ + mssqlConfig: this.config, + config: { + name: this.name, + schema: this.schema, + selectedFields: this.columns, + query: query.inlineParams(), + }, + }); + view[MsSqlViewConfig] = { + ...this.config, + indexes: this.buildIndexes(view), + }; return new Proxy( - new MsSqlView({ - mssqlConfig: this.config, - config: { - name: this.name, - schema: this.schema, - selectedFields: this.columns, - query: query.inlineParams(), - }, - }), + view, new SelectionProxyHandler({ alias: this.name, sqlBehavior: 'error', @@ -151,10 +193,10 @@ export class MsSqlView< declare protected $MsSqlViewBrand: 'MsSqlView'; - [MsSqlViewConfig]: ViewBuilderConfig | undefined; + [MsSqlViewConfig]: MsSqlViewConfigShape | undefined; constructor({ mssqlConfig, config }: { - mssqlConfig: ViewBuilderConfig | undefined; + mssqlConfig: MsSqlViewConfigShape | undefined; config: { name: TName; schema: string | undefined; @@ -174,14 +216,44 @@ export type MsSqlViewWithSelection< > = MsSqlView & TSelectedFields; /** @internal */ +export function mssqlViewWithSchema( + name: TName, + selection: undefined, + extraConfig: undefined, + schema: string | undefined, + casing: Casing | undefined, +): ViewBuilder; +export function mssqlViewWithSchema< + TName extends string, + TColumns extends Record, +>( + name: TName, + selection: TColumns, + extraConfig: + | ((self: BuildColumns) => MsSqlViewExtraConfig | MsSqlViewExtraConfigValue[]) + | undefined, + schema: string | undefined, + casing: Casing | undefined, +): ManualViewBuilder; export function mssqlViewWithSchema( name: string, selection: Record | undefined, + extraConfig: unknown, schema: string | undefined, casing: Casing | undefined, ): ViewBuilder | ManualViewBuilder { if (selection) { - return new ManualViewBuilder(name, selection, schema, casing); + return new ManualViewBuilder( + name, + selection, + extraConfig as + | (( + self: BuildColumns, 'mssql'>, + ) => MsSqlViewExtraConfig | MsSqlViewExtraConfigValue[]) + | undefined, + schema, + casing, + ); } return new ViewBuilder(name, schema); } @@ -191,12 +263,16 @@ export interface MsSqlViewFn { >( name: TName, columns: TColumns, + extraConfig?: ( + self: BuildColumns, + ) => MsSqlViewExtraConfig | MsSqlViewExtraConfigValue[], ): ManualViewBuilder; } /** @internal */ export function mssqlViewWithCasing(casing: Casing | undefined): MsSqlViewFn { - return ((name, columns) => mssqlViewWithSchema(name, columns, undefined, casing)) as MsSqlViewFn; + return ((name, columns, extraConfig) => + mssqlViewWithSchema(name, columns, extraConfig, undefined, casing)) as MsSqlViewFn; } export const mssqlView = mssqlViewWithCasing(undefined); diff --git a/drizzle-orm/src/node-mssql/driver.ts b/drizzle-orm/src/node-mssql/driver.ts index a4deda054a..ecb2ba9e98 100644 --- a/drizzle-orm/src/node-mssql/driver.ts +++ b/drizzle-orm/src/node-mssql/driver.ts @@ -1,10 +1,12 @@ import type mssql from 'mssql'; import * as V1 from '~/_relations.ts'; +import type { Cache } from '~/cache/core/index.ts'; import { entityKind } from '~/entity.ts'; import type { Logger } from '~/logger.ts'; import { DefaultLogger } from '~/logger.ts'; import { MsSqlDatabase } from '~/mssql-core/db.ts'; import { MsSqlDialect } from '~/mssql-core/dialect.ts'; +import type { AnyRelations, EmptyRelations } from '~/relations.ts'; import { type DrizzleConfig, type Equal, jitCompatCheck } from '~/utils.ts'; import { AutoPool } from './pool.ts'; import type { NodeMsSqlClient, NodeMsSqlPreparedQueryHKT, NodeMsSqlQueryResultHKT } from './session.ts'; @@ -12,6 +14,7 @@ import { NodeMsSqlSession } from './session.ts'; export interface MsSqlDriverOptions { logger?: Logger; + cache?: Cache; useJitMappers?: boolean; } @@ -25,11 +28,13 @@ export class NodeMsSqlDriver { ) { } - createSession( + createSession( schema: V1.RelationalSchemaConfig | undefined, - ): NodeMsSqlSession, V1.TablesRelationalConfig> { - return new NodeMsSqlSession(this.client, this.dialect, schema, { + relations: TRelations, + ): NodeMsSqlSession, V1.TablesRelationalConfig, TRelations> { + return new NodeMsSqlSession(this.client, this.dialect, schema, relations, { logger: this.options.logger, + cache: this.options.cache, useJitMappers: this.options.useJitMappers, }); } @@ -39,22 +44,35 @@ export { MsSqlDatabase } from '~/mssql-core/db.ts'; export type NodeMsSqlDatabase< TSchema extends Record = Record, -> = MsSqlDatabase; - -export type NodeMsSqlDrizzleConfig = Record> = - & Omit, 'schema'> + TRelations extends AnyRelations = EmptyRelations, +> = MsSqlDatabase< + NodeMsSqlQueryResultHKT, + NodeMsSqlPreparedQueryHKT, + TSchema, + V1.ExtractTablesWithRelations, + TRelations +>; + +export type NodeMsSqlDrizzleConfig< + TSchema extends Record = Record, + TRelations extends AnyRelations = EmptyRelations, +> = + & Omit, 'schema'> & ({ schema: TSchema } | { schema?: undefined }); function construct< TSchema extends Record = Record, + TRelations extends AnyRelations = EmptyRelations, TClient extends NodeMsSqlClient = NodeMsSqlClient, >( client: TClient, - config: DrizzleConfig = {}, -): NodeMsSqlDatabase & { + config: NodeMsSqlDrizzleConfig = {}, +): NodeMsSqlDatabase & { $client: Equal extends true ? AutoPool : TClient; } { - const dialect = new MsSqlDialect(); + const dialect = new MsSqlDialect({ + useJitMappers: jitCompatCheck(config.jit), + }); let logger; if (config.logger === true) { logger = new DefaultLogger(); @@ -78,13 +96,19 @@ function construct< }; } + const relations = config.relations ?? {} as TRelations; const driver = new NodeMsSqlDriver(client as NodeMsSqlClient, dialect, { logger, + cache: config.cache, useJitMappers: jitCompatCheck(config.jit), }); - const session = driver.createSession(schema); - const db = new MsSqlDatabase(dialect, session, schema) as NodeMsSqlDatabase; + const session = driver.createSession(schema, relations); + const db = new MsSqlDatabase(dialect, session, schema, relations) as NodeMsSqlDatabase; ( db).$client = client; + ( db).$cache = config.cache; + if (( db).$cache) { + ( db).$cache['invalidate'] = config.cache?.onMutate; + } return db as any; } @@ -110,6 +134,7 @@ export function getMsSqlConnectionParams(connectionString: string): mssql.config export function drizzle< TSchema extends Record = Record, + TRelations extends AnyRelations = EmptyRelations, TClient extends NodeMsSqlClient = AutoPool, >( ...params: @@ -118,11 +143,11 @@ export function drizzle< ] | [ string, - DrizzleConfig, + NodeMsSqlDrizzleConfig, ] | [ ( - & DrizzleConfig + & NodeMsSqlDrizzleConfig & ({ connection: string; } | { @@ -130,27 +155,30 @@ export function drizzle< }) ), ] -): NodeMsSqlDatabase & { +): NodeMsSqlDatabase & { $client: Equal extends true ? AutoPool : TClient; } { if (typeof params[0] === 'string') { const instance = new AutoPool(getMsSqlConnectionParams(params[0])); - return construct(instance, params[1] as DrizzleConfig | undefined) as any; + return construct( + instance, + params[1] as NodeMsSqlDrizzleConfig | undefined, + ) as any; } const { connection, client, ...drizzleConfig } = params[0] as ( & ({ connection?: mssql.config | string; client?: TClient }) - & DrizzleConfig + & NodeMsSqlDrizzleConfig ); - if (client) return construct(client, drizzleConfig); + if (client) return construct(client, drizzleConfig); const instance = typeof connection === 'string' ? new AutoPool(getMsSqlConnectionParams(connection)) : new AutoPool(connection!); - return construct(instance, drizzleConfig) as any; + return construct(instance, drizzleConfig) as any; } interface CallbackClient { @@ -162,9 +190,12 @@ function isCallbackClient(client: any): client is CallbackClient { } export namespace drizzle { - export function mock = Record>( - config?: DrizzleConfig, - ): NodeMsSqlDatabase & { + export function mock< + TSchema extends Record = Record, + TRelations extends AnyRelations = EmptyRelations, + >( + config?: NodeMsSqlDrizzleConfig, + ): NodeMsSqlDatabase & { $client: '$client is not available on drizzle.mock()'; } { return construct({} as any, config) as any; diff --git a/drizzle-orm/src/node-mssql/migrator.ts b/drizzle-orm/src/node-mssql/migrator.ts index 51db049e8e..a871728b7b 100644 --- a/drizzle-orm/src/node-mssql/migrator.ts +++ b/drizzle-orm/src/node-mssql/migrator.ts @@ -1,9 +1,13 @@ import type { MigrationConfig } from '~/migrator.ts'; import { readMigrationFiles } from '~/migrator.ts'; +import type { AnyRelations } from '~/relations.ts'; import type { NodeMsSqlDatabase } from './driver.ts'; -export async function migrate>( - db: NodeMsSqlDatabase, +export async function migrate< + TSchema extends Record, + TRelations extends AnyRelations, +>( + db: NodeMsSqlDatabase, config: MigrationConfig, ) { const migrations = readMigrationFiles(config); diff --git a/drizzle-orm/src/node-mssql/session.ts b/drizzle-orm/src/node-mssql/session.ts index 082c17ff05..c9dc9ae9bf 100644 --- a/drizzle-orm/src/node-mssql/session.ts +++ b/drizzle-orm/src/node-mssql/session.ts @@ -2,7 +2,10 @@ import type { ConnectionPool, IResult, Request } from 'mssql'; import mssql from 'mssql'; import { once } from 'node:events'; import type * as V1 from '~/_relations.ts'; +import { type Cache, NoopCache, strategyFor } from '~/cache/core/cache.ts'; +import type { WithCacheConfig } from '~/cache/core/types.ts'; import { entityKind, is } from '~/entity.ts'; +import { DrizzleQueryError } from '~/errors.ts'; import type { Logger } from '~/logger.ts'; import { NoopLogger } from '~/logger.ts'; import type { MsSqlDialect } from '~/mssql-core/dialect.ts'; @@ -17,8 +20,9 @@ import { type PreparedQueryKind, type QueryResultHKT, } from '~/mssql-core/session.ts'; +import type { AnyRelations, EmptyRelations } from '~/relations.ts'; import { fillPlaceholders, type Query, type SQL, sql } from '~/sql/sql.ts'; -import { type Assume, makeJitQueryMapper, mapResultRow, type RowsMapper } from '~/utils.ts'; +import { assertUnreachable, type Assume, makeJitQueryMapper, mapResultRow, type RowsMapper } from '~/utils.ts'; import { AutoPool } from './pool.ts'; export type NodeMsSqlClient = Pick | AutoPool; @@ -44,28 +48,90 @@ export class NodeMsSqlPreparedQuery< private fields: SelectedFieldsOrdered | undefined, private useJitMappers: boolean | undefined, private customResultMapper?: (rows: unknown[][]) => T['execute'], + private cache?: Cache, + private queryMetadata?: { + type: 'select' | 'update' | 'delete' | 'insert'; + tables: string[]; + }, + private cacheConfig?: WithCacheConfig, ) { super(); this.rawQuery = { sql: queryString, parameters: params, }; + if (cache && cache.strategy() === 'all' && cacheConfig === undefined) { + this.cacheConfig = { enabled: true, autoInvalidate: true }; + } + if (!this.cacheConfig?.enabled) { + this.cacheConfig = undefined; + } } - async execute( - placeholderValues: Record = {}, - ): Promise { - const params = fillPlaceholders(this.params, placeholderValues); + /** @internal */ + private async queryWithCache( + queryString: string, + params: unknown[], + query: () => Promise, + ): Promise { + const cacheStrat = this.cache !== undefined && !is(this.cache, NoopCache) + ? await strategyFor(queryString, params, this.queryMetadata, this.cacheConfig) + : { type: 'skip' as const }; + + if (cacheStrat.type === 'skip') { + return query().catch((e) => { + throw new DrizzleQueryError(queryString, params, e as Error); + }); + } + + const cache = this.cache!; + + if (cacheStrat.type === 'invalidate') { + return Promise.all([ + query(), + cache.onMutate({ tables: cacheStrat.tables }), + ]).then((res) => res[0]).catch((e) => { + throw new DrizzleQueryError(queryString, params, e as Error); + }); + } + + if (cacheStrat.type === 'try') { + const { tables, key, isTag, autoInvalidate, config } = cacheStrat; + const fromCache = await cache.get( + key, + tables, + isTag, + autoInvalidate, + ); + + if (fromCache === undefined) { + const result = await query().catch((e) => { + throw new DrizzleQueryError(queryString, params, e as Error); + }); + await cache.put( + key, + result, + autoInvalidate ? tables : [], + isTag, + config, + ); + return result; + } + + return fromCache as unknown as TResult; + } - this.logger.logQuery(this.rawQuery.sql, params); + assertUnreachable(cacheStrat); + } + private async executeQuery(params: unknown[]): Promise | unknown[][]> { const { fields, client, rawQuery, - joinsNotNullableMap, customResultMapper, } = this; + let queryClient = client as ConnectionPool; if (is(client, AutoPool)) { queryClient = await client.$instance(); @@ -76,26 +142,47 @@ export class NodeMsSqlPreparedQuery< } if (!fields && !customResultMapper) { - return request.query(rawQuery.sql) as Promise; + return request.query(rawQuery.sql); } request.arrayRowMode = true; const rows = await request.query(rawQuery.sql); + return rows.recordset; + } + + async execute( + placeholderValues: Record = {}, + ): Promise { + const params = fillPlaceholders(this.params, placeholderValues); + const { fields, rawQuery, joinsNotNullableMap, customResultMapper } = this; + this.logger.logQuery(rawQuery.sql, params); + + const result = this.cacheConfig === undefined && (this.cache === undefined || is(this.cache, NoopCache)) + ? this.executeQuery(params).catch((e) => { + throw new DrizzleQueryError(rawQuery.sql, params, e as Error); + }) + : this.queryWithCache(rawQuery.sql, params, () => this.executeQuery(params)); + + if (!fields && !customResultMapper) { + return result as Promise; + } if (customResultMapper) { - return customResultMapper(rows.recordset); + return result.then((rows) => customResultMapper(rows as unknown[][])); } - return this.useJitMappers - ? (this.jitMapper = - this.jitMapper as RowsMapper<(T['execute'] extends any[] ? T['execute'][number] : T['execute'])[]> - ?? makeJitQueryMapper<(T['execute'] extends any[] ? T['execute'][number] : T['execute'])[]>( - fields!, - joinsNotNullableMap, - ))( - rows.recordset, - ) - : rows.recordset.map((row) => mapResultRow(fields!, row, joinsNotNullableMap)); + return result.then((rows) => + this.useJitMappers + ? (this.jitMapper = + this.jitMapper as RowsMapper<(T['execute'] extends any[] ? T['execute'][number] : T['execute'])[]> + ?? makeJitQueryMapper<(T['execute'] extends any[] ? T['execute'][number] : T['execute'])[]>( + fields!, + joinsNotNullableMap, + ))( + rows as unknown[][], + ) + : (rows as unknown[][]).map((row) => mapResultRow(fields!, row, joinsNotNullableMap)) + ) as Promise; } async *iterator( @@ -185,36 +272,47 @@ export class NodeMsSqlPreparedQuery< export interface NodeMsSqlSessionOptions { logger?: Logger; + cache?: Cache; useJitMappers?: boolean; } export class NodeMsSqlSession< TFullSchema extends Record, TSchema extends V1.TablesRelationalConfig, + TRelations extends AnyRelations = EmptyRelations, > extends MsSqlSession< NodeMsSqlQueryResultHKT, NodeMsSqlPreparedQueryHKT, TFullSchema, - TSchema + TSchema, + TRelations > { static override readonly [entityKind]: string = 'NodeMsSqlSession'; private logger: Logger; + private cache: Cache; constructor( private client: NodeMsSqlClient, dialect: MsSqlDialect, private schema: V1.RelationalSchemaConfig | undefined, + private relations: TRelations, private options: NodeMsSqlSessionOptions, ) { super(dialect); this.logger = options.logger ?? new NoopLogger(); + this.cache = options.cache ?? new NoopCache(); } prepareQuery( query: Query, fields: SelectedFieldsOrdered | undefined, customResultMapper?: (rows: unknown[][]) => T['execute'], + queryMetadata?: { + type: 'select' | 'update' | 'delete' | 'insert'; + tables: string[]; + }, + cacheConfig?: WithCacheConfig, ): PreparedQueryKind { return new NodeMsSqlPreparedQuery( this.client, @@ -224,6 +322,9 @@ export class NodeMsSqlSession< fields, this.options.useJitMappers, customResultMapper, + this.cache, + queryMetadata, + cacheConfig, ) as PreparedQueryKind; } @@ -259,7 +360,7 @@ export class NodeMsSqlSession< } override async transaction( - transaction: (tx: NodeMsSqlTransaction) => Promise, + transaction: (tx: NodeMsSqlTransaction) => Promise, config?: MsSqlTransactionConfig, ): Promise { let queryClient = this.client as ConnectionPool; @@ -273,12 +374,14 @@ export class NodeMsSqlSession< mssqlTransaction, this.dialect, this.schema, + this.relations, this.options, ); const tx = new NodeMsSqlTransaction( this.dialect, - session as MsSqlSession, + session as MsSqlSession, this.schema, + this.relations, 0, ); @@ -302,22 +405,25 @@ export class NodeMsSqlSession< export class NodeMsSqlTransaction< TFullSchema extends Record, TSchema extends V1.TablesRelationalConfig, + TRelations extends AnyRelations = EmptyRelations, > extends MsSqlTransaction< NodeMsSqlQueryResultHKT, NodeMsSqlPreparedQueryHKT, TFullSchema, - TSchema + TSchema, + TRelations > { static override readonly [entityKind]: string = 'NodeMsSqlTransaction'; override async transaction( - transaction: (tx: NodeMsSqlTransaction) => Promise, + transaction: (tx: NodeMsSqlTransaction) => Promise, ): Promise { const savepointName = `sp${this.nestedIndex + 1}`; const tx = new NodeMsSqlTransaction( this.dialect, this.session, this.schema, + this.relations, this.nestedIndex + 1, ); diff --git a/drizzle-orm/tests/mssql-rqbv2.test.ts b/drizzle-orm/tests/mssql-rqbv2.test.ts new file mode 100644 index 0000000000..51c82f86d9 --- /dev/null +++ b/drizzle-orm/tests/mssql-rqbv2.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, test } from 'vitest'; +import { relations as relationsV1 } from '~/_relations.ts'; +import { binary, datetime, int, mssqlTable, text } from '~/mssql-core'; +import { drizzle } from '~/node-mssql'; +import { defineRelations } from '~/relations'; +import { sql } from '~/sql'; + +const users = mssqlTable('users', { + id: int('id').primaryKey(), + name: text('name').notNull(), + createdAt: datetime('created_at').notNull(), +}); +const usersConfig = relationsV1(users, ({ many }) => ({ + posts: many(posts), +})); + +const posts = mssqlTable('posts', { + id: int('id').primaryKey(), + title: text('title').notNull(), + authorId: int('author_id').references(() => users.id), +}); +const postsConfig = relationsV1(posts, ({ one }) => ({ + author: one(users, { + fields: [posts.authorId], + references: [users.id], + }), +})); + +const files = mssqlTable('files', { + id: int('id').primaryKey(), + data: binary('data'), + createdAt: datetime('created_at').notNull(), +}); +const filesConfig = relationsV1(files, () => ({})); + +const schema = { + files, + filesConfig, + posts, + postsConfig, + users, + usersConfig, +}; + +const relations = defineRelations(schema, ({ files, many, one, posts, users }) => ({ + files: {}, + users: { + posts: many.posts({ from: users.id, to: posts.authorId }), + }, + posts: { + author: one.users({ from: posts.authorId, to: users.id }), + }, +})); + +const db = drizzle({ client: {} as any, schema, relations }); + +describe('mssql RQBv2', () => { + test('builds nested relation SQL with SQL Server JSON primitives', () => { + const query = db.query.users.findMany({ + columns: { + id: true, + name: true, + }, + where: { + posts: { + title: { + like: '%orm%', + }, + }, + }, + orderBy: { + name: 'asc', + }, + offset: 5, + limit: 10, + with: { + posts: { + columns: { + title: true, + }, + limit: 2, + extras: { + titleLower: (posts, { sql }) => sql`lower(${posts.title})`, + }, + with: { + author: { + columns: { + id: true, + name: true, + }, + }, + }, + }, + }, + }).toSQL(); + + expect(query).toMatchInlineSnapshot(` + { + "params": [ + 2, + 1, + "%orm%", + 5, + 10, + ], + "sql": "select (select [d0].[id] as [id], [d0].[name] as [name], json_query([posts].[r]) as [posts] from [users] as [d0] outer apply (select json_query(coalesce((select [t].[title] as [title], [t].[titleLower] as [titleLower], json_query([t].[author]) as [author] from (select top(@par0) [d1].[title] as [title], (lower([d1].[title])) as [titleLower], json_query([author].[r]) as [author] from [posts] as [d1] outer apply (select json_query((select [t].[id] as [id], [t].[name] as [name] from (select top(@par1) [d2].[id] as [id], [d2].[name] as [name] from [users] as [d2] where [d1].[author_id] = [d2].[id]) as [t] for json path, include_null_values, without_array_wrapper)) as [r]) as [author] where [d0].[id] = [d1].[author_id]) as [t] for json path, include_null_values), '[]')) as [r]) as [posts] where exists (select top(1) * from [posts] as [f0] where (([d0].[id] = [f0].[author_id]) and ([f0].[title] like @par2))) order by [d0].[name] asc offset @par3 rows fetch next @par4 rows only for json path, include_null_values) as [data]", + } + `); + }); + + test('builds first relation SQL without array wrapper', () => { + const query = db.query.posts.findFirst({ + columns: { + id: true, + }, + with: { + author: { + columns: { + name: true, + }, + }, + }, + }).toSQL(); + + expect(query).toMatchInlineSnapshot(` + { + "params": [ + 1, + 1, + ], + "sql": "select (select top(@par0) [d0].[id] as [id], json_query([author].[r]) as [author] from [posts] as [d0] outer apply (select json_query((select [t].[name] as [name] from (select top(@par1) [d1].[name] as [name] from [users] as [d1] where [d0].[author_id] = [d1].[id]) as [t] for json path, include_null_values, without_array_wrapper)) as [r]) as [author] for json path, include_null_values) as [data]", + } + `); + }); + + test('preserves nested relation order with offset 0', () => { + const query = db.query.users.findMany({ + columns: { + id: true, + }, + with: { + posts: { + columns: { + id: true, + }, + orderBy: { + id: 'asc', + }, + }, + }, + }).toSQL(); + + expect(query.sql).toContain('order by [d1].[id] asc offset 0 rows'); + }); + + test('uses primary key fallback order for offset without orderBy', () => { + const query = db.query.users.findMany({ + columns: { + id: true, + }, + offset: 1, + limit: 1, + }).toSQL(); + + expect(query.sql).toContain('order by [d0].[id] offset @par0 rows fetch next @par1 rows only'); + expect(query.sql).not.toContain('order by 1'); + }); + + test('keeps V1 _query compilation path', () => { + const query = db._query.users.findMany({ + limit: 1, + with: { + posts: true, + }, + }).toSQL(); + + expect(query.sql).toContain('for json auto, include_null_values'); + expect(query.sql).not.toContain('outer apply'); + }); + + test('maps root FOR JSON values through MSSQL JSON mappers', () => { + const relationalQuery = db.query.files.findMany(); + const { query } = (relationalQuery as any)._toSQL(); + const mapper = db.dialect.mapperGenerators.relationalRows({ + isFirst: false, + parseJson: false, + parseJsonIfString: false, + rootJsonMappers: true, + selection: query.selection, + }); + + const rows = mapper([{ + id: 1, + data: Buffer.from('drizzle').toString('base64'), + createdAt: '2026-06-12T12:34:56.000Z', + }]); + + expect(rows).toEqual([{ + id: 1, + data: Buffer.from('drizzle'), + createdAt: new Date('2026-06-12T12:34:56.000Z'), + }]); + }); +}); diff --git a/drizzle-orm/type-tests/mssql/count.ts b/drizzle-orm/type-tests/mssql/count.ts new file mode 100644 index 0000000000..58360d272f --- /dev/null +++ b/drizzle-orm/type-tests/mssql/count.ts @@ -0,0 +1,61 @@ +import { Expect } from 'type-tests/utils.ts'; +import { int, mssqlTable, text } from '~/mssql-core/index.ts'; +import { and, gt, ne } from '~/sql/expressions/index.ts'; +import type { Equal } from '~/utils.ts'; +import { db } from './db.ts'; + +const names = mssqlTable('names', { + id: int('id').identity().primaryKey(), + name: text('name'), + authorId: int('author_id'), +}); + +const separate = await db.$count(names); + +const separateFilters = await db.$count(names, and(gt(names.id, 1), ne(names.name, 'forbidden'))); + +const embedded = await db + .select({ + id: names.id, + name: names.name, + authorId: names.authorId, + count1: db.$count(names).as('count1'), + }) + .from(names); + +const embeddedFilters = await db + .select({ + id: names.id, + name: names.name, + authorId: names.authorId, + count1: db.$count(names, and(gt(names.id, 1), ne(names.name, 'forbidden'))).as('count1'), + }) + .from(names); + +Expect>; + +Expect>; + +Expect< + Equal< + { + id: number; + name: string | null; + authorId: number | null; + count1: number; + }[], + typeof embedded + > +>; + +Expect< + Equal< + { + id: number; + name: string | null; + authorId: number | null; + count1: number; + }[], + typeof embeddedFilters + > +>; diff --git a/drizzle-orm/type-tests/mssql/db-rel.ts b/drizzle-orm/type-tests/mssql/db-rel.ts index b5c92da690..9a734a2289 100644 --- a/drizzle-orm/type-tests/mssql/db-rel.ts +++ b/drizzle-orm/type-tests/mssql/db-rel.ts @@ -1,11 +1,29 @@ import mssql from 'mssql'; import { type Equal, Expect } from 'type-tests/utils.ts'; import { drizzle } from '~/node-mssql/index.ts'; +import { defineRelations } from '~/relations.ts'; import { sql } from '~/sql/sql.ts'; import * as schema from './tables-rel.ts'; const conn = new mssql.ConnectionPool(process.env['MSSQL_CONNECTION_STRING']!); const db = drizzle({ client: conn, schema }); +const relationsV2 = defineRelations(schema, ({ cities, comments, many, one, posts, users }) => ({ + users: { + city: one.cities({ from: users.cityId, to: cities.id, optional: false }), + posts: many.posts({ from: users.id, to: posts.authorId }), + }, + posts: { + author: one.users({ from: posts.authorId, to: users.id }), + comments: many.comments({ from: posts.id, to: comments.postId }), + }, + comments: { + author: one.users({ from: comments.authorId, to: users.id }), + }, + cities: { + users: many.users({ from: cities.id, to: users.cityId }), + }, +})); +const dbV2 = drizzle({ client: conn, relations: relationsV2 }); { const result = await db._query.users.findMany({ @@ -83,6 +101,101 @@ const db = drizzle({ client: conn, schema }); >; } +{ + const result = await dbV2.query.users.findMany({ + columns: { + id: true, + name: true, + }, + where: { + name: { + like: '%ohn%', + }, + }, + orderBy: { + name: 'asc', + }, + limit: 10, + with: { + city: { + columns: { + name: true, + }, + }, + posts: { + columns: { + authorId: true, + }, + extras: { + lower: sql`lower(${schema.posts.title})`.as('lower_name'), + }, + with: { + author: { + columns: { + id: true, + name: true, + }, + }, + comments: { + columns: { + text: true, + }, + }, + }, + }, + }, + }); + + Expect< + Equal< + { + id: number; + name: string; + city: { + name: string; + }; + posts: { + authorId: number | null; + lower: string; + author: { + id: number; + name: string; + } | null; + comments: { + text: string; + }[]; + }[]; + }[], + typeof result + > + >; +} + +{ + const result = await dbV2.query.users.findFirst({ + columns: { + id: true, + }, + with: { + posts: true, + }, + }); + + Expect< + Equal< + { + id: number; + posts: { + id: number; + title: string; + authorId: number | null; + }[]; + } | undefined, + typeof result + > + >; +} + { const result = await db._query.users.findMany({ columns: { diff --git a/drizzle-orm/type-tests/mssql/indexed-view.ts b/drizzle-orm/type-tests/mssql/indexed-view.ts new file mode 100644 index 0000000000..f598b047a6 --- /dev/null +++ b/drizzle-orm/type-tests/mssql/indexed-view.ts @@ -0,0 +1,13 @@ +import { index, int, mssqlTable, mssqlView, uniqueIndex } from '~/mssql-core/index.ts'; +import { sql } from '~/sql/sql.ts'; + +const users = mssqlTable('users', { + id: int('id').notNull().primaryKey(), +}); + +export const usersView = mssqlView('users_view', { + id: int('id').notNull(), +}, (view) => [ + uniqueIndex('users_view_clustered_idx').on(view.id).clustered(), + index('users_view_id_idx').on(view.id).nonClustered(), +]).with({ schemaBinding: true }).as(sql`select ${users.id} from ${users}`); diff --git a/drizzle-orm/type-tests/mssql/select.ts b/drizzle-orm/type-tests/mssql/select.ts index 97b67000a3..daa0968a31 100644 --- a/drizzle-orm/type-tests/mssql/select.ts +++ b/drizzle-orm/type-tests/mssql/select.ts @@ -34,6 +34,10 @@ import { cities, classes, newYorkers, users } from './tables.ts'; const city = alias(cities, 'city'); const city1 = alias(cities, 'city1'); +await db.select().from(users).$withCache(); +await db.select().from(users).$withCache({ tag: 'users', autoInvalidate: false, config: { ex: 1 } }); +await db.select().from(users).$withCache(false); + const join = await db .select({ users, diff --git a/drizzle-orm/type-tests/mssql/tables.ts b/drizzle-orm/type-tests/mssql/tables.ts index 7bcf41c374..f7f3afd599 100644 --- a/drizzle-orm/type-tests/mssql/tables.ts +++ b/drizzle-orm/type-tests/mssql/tables.ts @@ -1,23 +1,35 @@ import { type Equal, Expect } from 'type-tests/utils.ts'; -import type { InferSelectModel } from '~/index.ts'; +import type { InferInsertModel, InferSelectModel } from '~/index.ts'; import { bigint, char, check, + clusteredColumnStoreIndex, + columnStoreIndex, customType, date, datetime, decimal, foreignKey, + fullTextIndex, + geography, + geometry, index, int, + json, + money, mssqlTable, nchar, nvarchar, primaryKey, + rowversion, + smalldatetime, + smallmoney, text, + uniqueidentifier, uniqueIndex, varchar, + xml, } from '~/mssql-core/index.ts'; import { mssqlSchema } from '~/mssql-core/schema.ts'; import { mssqlView } from '~/mssql-core/view.ts'; @@ -64,6 +76,10 @@ export const cities = mssqlTable('cities_table', { population: int('population').default(0), }, (cities) => [ index('citiesNameIdx').on(cities.id), + index('citiesPopulationIdx') + .on(cities.population.desc()) + .include(cities.name) + .with({ fillFactor: 80, online: true }), ]); Expect< @@ -74,6 +90,53 @@ Expect< }, InferSelectModel> >; +export const nativeTypes = mssqlTable('native_types', { + id: int('id').identity().primaryKey(), + guid: uniqueidentifier('guid').notNull(), + document: xml('document'), + payload: json('payload'), + price: money('price'), + priceNumber: money('price_number', { mode: 'number' }), + smallPrice: smallmoney('small_price'), + version: rowversion('version'), + createdAtSmall: smalldatetime('created_at_small'), + createdAtSmallString: smalldatetime('created_at_small_string', { mode: 'string' }), + geo: geography('geo'), + shape: geometry('shape'), + geoPoint: geography('geo_point', { mode: 'xy', srid: 4326 }), + shapePoint: geometry('shape_point', { mode: 'tuple' }), + shapeWkt: geometry('shape_wkt', { mode: 'wkt' }), +}, (table) => [ + fullTextIndex('native_types_document_ft').on(table.document).keyIndex('native_types_pkey'), + columnStoreIndex('native_types_columnstore_idx').on(table.priceNumber).where(sql`${table.priceNumber} is not null`), + clusteredColumnStoreIndex('native_types_clustered_columnstore_idx').orderBy(table.id), +]); + +Expect< + Equal<{ + id: number; + guid: string; + document: string | null; + payload: unknown; + price: string | null; + priceNumber: number | null; + smallPrice: string | null; + version: Buffer; + createdAtSmall: Date | null; + createdAtSmallString: string | null; + geo: unknown; + shape: unknown; + geoPoint: { x: number; y: number } | null; + shapePoint: [number, number] | null; + shapeWkt: string | null; + }, InferSelectModel> +>; + +const nativeTypesInsert: InferInsertModel = { + guid: '00000000-0000-0000-0000-000000000000', +}; +nativeTypesInsert; + export const customSchema = mssqlSchema('custom_schema'); export const citiesCustom = customSchema.table('cities_table', { diff --git a/integration-tests/package.json b/integration-tests/package.json index 3705fd9e92..60c400b455 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -26,6 +26,8 @@ "@neondatabase/serverless": "0.10.0", "@originjs/vite-plugin-commonjs": "^1.0.3", "@paralleldrive/cuid2": "^2.2.2", + "@pothos/core": "^4.12.0", + "@pothos/plugin-drizzle": "^0.17.4", "@types/async-retry": "^1.4.8", "@types/better-sqlite3": "^7.6.4", "@types/dockerode": "^3.3.18", @@ -39,6 +41,8 @@ "ava": "^5.3.0", "bun-types": "^1.2.23", "cross-env": "^7.0.3", + "drizzle-orm": "workspace:../drizzle-orm/dist", + "graphql": "^16.14.2", "import-in-the-middle": "^1.13.1", "keyv": "^5.2.3", "ts-node": "^10.9.2", diff --git a/integration-tests/tests/mssql/mssql-rqbv2.test.ts b/integration-tests/tests/mssql/mssql-rqbv2.test.ts new file mode 100644 index 0000000000..3a448fedce --- /dev/null +++ b/integration-tests/tests/mssql/mssql-rqbv2.test.ts @@ -0,0 +1,660 @@ +import SchemaBuilder from '@pothos/core'; +import DrizzlePlugin from '@pothos/plugin-drizzle'; +import { defineRelations, sql } from 'drizzle-orm'; +import { getTableConfig, int, mssqlSchema, varchar } from 'drizzle-orm/mssql-core'; +import type { NodeMsSqlDatabase } from 'drizzle-orm/node-mssql'; +import { drizzle } from 'drizzle-orm/node-mssql'; +import type { graphql as graphqlFn } from 'graphql'; +import { createRequire } from 'node:module'; +import { expect, expectTypeOf } from 'vitest'; +import { test } from './instrumentation'; +import * as schema from './mssql.schema'; + +const require = createRequire(import.meta.url); +const { graphql } = require('graphql') as { graphql: typeof graphqlFn }; + +const rqbv2Schema = mssqlSchema('rqbv2_schema'); + +const scopedUsers = rqbv2Schema.table('users', { + id: int('id').primaryKey().notNull(), + name: varchar('name', { length: 100 }).notNull(), +}); + +const scopedPosts = rqbv2Schema.table('posts', { + id: int('id').primaryKey().notNull(), + content: varchar('content', { length: 100 }).notNull(), + ownerId: int('owner_id').notNull(), +}); + +const scopedUserView = rqbv2Schema.view('users_view', { + id: int('id').primaryKey().notNull(), + name: varchar('name', { length: 100 }).notNull(), +}).existing(); + +const scopedSchema = { + scopedPosts, + scopedUsers, + scopedUserView, +}; + +const scopedRelations = defineRelations( + scopedSchema, + ({ many, one, scopedPosts, scopedUsers, scopedUserView }) => ({ + scopedUsers: { + posts: many.scopedPosts({ + from: scopedUsers.id, + to: scopedPosts.ownerId, + }), + }, + scopedUserView: { + posts: many.scopedPosts({ + from: scopedUserView.id, + to: scopedPosts.ownerId, + }), + }, + scopedPosts: { + owner: one.scopedUsers({ + from: scopedPosts.ownerId, + to: scopedUsers.id, + }), + ownerView: one.scopedUserView({ + from: scopedPosts.ownerId, + to: scopedUserView.id, + }), + }, + }), +); + +const relations = defineRelations( + schema, + ({ commentsTable, groupsTable, many, one, postsTable, usersTable, usersToGroupsTable }) => ({ + usersTable: { + posts: many.postsTable({ + from: usersTable.id, + to: postsTable.ownerId, + }), + groups: many.groupsTable({ + from: usersTable.id.through(usersToGroupsTable.userId), + to: groupsTable.id.through(usersToGroupsTable.groupId), + }), + }, + groupsTable: { + users: many.usersTable({ + from: groupsTable.id.through(usersToGroupsTable.groupId), + to: usersTable.id.through(usersToGroupsTable.userId), + }), + }, + usersToGroupsTable: { + user: one.usersTable({ + from: usersToGroupsTable.userId, + to: usersTable.id, + }), + group: one.groupsTable({ + from: usersToGroupsTable.groupId, + to: groupsTable.id, + }), + }, + postsTable: { + author: one.usersTable({ + from: postsTable.ownerId, + to: usersTable.id, + }), + comments: many.commentsTable({ + from: postsTable.id, + to: commentsTable.postId, + }), + }, + commentsTable: { + author: one.usersTable({ + from: commentsTable.creator, + to: usersTable.id, + }), + post: one.postsTable({ + from: commentsTable.postId, + to: postsTable.id, + }), + }, + }), +); + +test.beforeEach(async ({ client }) => { + await client.query(`if object_id(N'[rqbv2_schema].[posts]', N'U') is not null drop table [rqbv2_schema].[posts]`); + await client.query( + `if object_id(N'[rqbv2_schema].[users_view]', N'V') is not null drop view [rqbv2_schema].[users_view]`, + ); + await client.query(`if object_id(N'[rqbv2_schema].[users]', N'U') is not null drop table [rqbv2_schema].[users]`); + await client.query(`if schema_id(N'rqbv2_schema') is not null exec(N'drop schema [rqbv2_schema]')`); + await client.query(`drop table if exists [comment_likes]`); + await client.query(`drop table if exists [comments]`); + await client.query(`drop table if exists [posts]`); + await client.query(`drop table if exists [users_to_groups]`); + await client.query(`drop table if exists [groups]`); + await client.query(`drop table if exists [users]`); + + await client.query(` + create table [users] ( + [id] int primary key not null, + [name] varchar(100) not null, + [verified] bit not null default 0, + [invited_by] int null foreign key references [users]([id]) + ) + `); + await client.query(` + create table [groups] ( + [id] int primary key not null, + [name] varchar(100) not null, + [description] varchar(100) null + ) + `); + await client.query(` + create table [users_to_groups] ( + [id] int identity primary key, + [user_id] int not null foreign key references [users]([id]), + [group_id] int not null foreign key references [groups]([id]), + constraint [uq_users_to_groups] unique ([user_id], [group_id]) + ) + `); + await client.query(` + create table [posts] ( + [id] int identity primary key, + [content] varchar(100) not null, + [owner_id] int null foreign key references [users]([id]), + [created_at] datetime not null default current_timestamp + ) + `); + await client.query(` + create table [comments] ( + [id] int identity primary key, + [content] varchar(100) not null, + [creator] int null foreign key references [users]([id]), + [post_id] int null foreign key references [posts]([id]), + [created_at] datetime not null default current_timestamp + ) + `); +}); + +async function seed(db: NodeMsSqlDatabase) { + await db.insert(schema.usersTable).values([ + { id: 1, name: 'Dan', verified: true }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + + await db.insert(schema.postsTable).values([ + { content: 'Post1', ownerId: 1 }, + { content: 'Post1.2', ownerId: 1 }, + { content: 'Post2', ownerId: 2 }, + ]); + + await db.insert(schema.commentsTable).values([ + { content: 'Comment1', creator: 2, postId: 1 }, + { content: 'Comment2', creator: 1, postId: 1 }, + { content: 'Comment3', creator: 3, postId: 3 }, + ]); +} + +async function seedGroups(db: NodeMsSqlDatabase) { + await db.insert(schema.groupsTable).values([ + { id: 1, name: 'Admins' }, + { id: 2, name: 'Guests' }, + ]); + + await db.insert(schema.usersToGroupsTable).values([ + { userId: 1, groupId: 1 }, + { userId: 1, groupId: 2 }, + { userId: 2, groupId: 2 }, + ]); +} + +test('RQBv2 findMany resolves nested MSSQL relations', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + + const result = await db.query.usersTable.findMany({ + columns: { + id: true, + name: true, + }, + where: { + posts: { + content: { + like: 'Post%', + }, + }, + }, + orderBy: { + id: 'asc', + }, + with: { + posts: { + columns: { + id: true, + content: true, + createdAt: true, + }, + orderBy: { + id: 'asc', + }, + with: { + comments: { + columns: { + content: true, + }, + orderBy: { + id: 'asc', + }, + }, + }, + }, + }, + }); + + expectTypeOf(result).toEqualTypeOf<{ + id: number; + name: string; + posts: { + id: number; + content: string; + createdAt: Date; + comments: { + content: string; + }[]; + }[]; + }[]>(); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + id: 1, + name: 'Dan', + posts: [ + { + id: 1, + content: 'Post1', + comments: [{ content: 'Comment1' }, { content: 'Comment2' }], + }, + { + id: 2, + content: 'Post1.2', + comments: [], + }, + ], + }); + expect(result[0]!.posts[0]!.createdAt).toBeInstanceOf(Date); + expect(result[1]).toMatchObject({ + id: 2, + name: 'Andrew', + posts: [ + { + id: 3, + content: 'Post2', + comments: [{ content: 'Comment3' }], + }, + ], + }); +}); + +test('RQBv2 findFirst resolves a single MSSQL relation and extras', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + + const result = await db.query.postsTable.findFirst({ + columns: { + id: true, + }, + where: { + author: { + name: 'Dan', + }, + }, + orderBy: { + id: 'asc', + }, + extras: { + lowerContent: ({ content }, { sql }) => sql`lower(${content})`, + }, + with: { + author: { + columns: { + name: true, + }, + }, + }, + }); + + expectTypeOf(result).toEqualTypeOf< + { + id: number; + lowerContent: string; + author: { + name: string; + } | null; + } | undefined + >(); + + expect(result).toEqual({ + id: 1, + lowerContent: 'post1', + author: { + name: 'Dan', + }, + }); +}); + +test('RQBv2 resolves MSSQL through relations', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + await seedGroups(db); + + const result = await db.query.usersTable.findMany({ + columns: { + id: true, + name: true, + }, + orderBy: { + id: 'asc', + }, + with: { + groups: { + columns: { + id: true, + name: true, + }, + orderBy: { + id: 'asc', + }, + }, + }, + }); + + expect(result).toEqual([ + { + id: 1, + name: 'Dan', + groups: [ + { id: 1, name: 'Admins' }, + { id: 2, name: 'Guests' }, + ], + }, + { + id: 2, + name: 'Andrew', + groups: [{ id: 2, name: 'Guests' }], + }, + { + id: 3, + name: 'Alex', + groups: [], + }, + ]); +}); + +test('RQBv2 prepared MSSQL query resolves placeholders', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + + const prepared = db.query.postsTable.findMany({ + columns: { + id: true, + content: true, + }, + where: { + ownerId: sql.placeholder('ownerId'), + }, + orderBy: { + id: 'asc', + }, + limit: sql.placeholder('limit'), + }).prepare(); + + const result = await prepared.execute({ + limit: 2, + ownerId: 1, + }); + + expect(result).toEqual([ + { id: 1, content: 'Post1' }, + { id: 2, content: 'Post1.2' }, + ]); +}); + +test('MSSQL $count counts table rows with filters', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + + const result = await db.$count( + schema.postsTable, + sql`${schema.postsTable.ownerId} = ${1}`, + ); + + expect(result).toBe(2); +}); + +test('MSSQL raw execute remains awaitable', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + + const result = await db.execute<{ value: number }>(sql`select ${1} as [value]`); + + expect(result.recordset).toEqual([{ value: 1 }]); +}); + +test('RQBv2 resolves schema-qualified view relations', async ({ client }) => { + const db = drizzle({ client, schema: scopedSchema, relations: scopedRelations }); + + await client.query(`create schema [rqbv2_schema]`); + await client.query(` + create table [rqbv2_schema].[users] ( + [id] int primary key not null, + [name] varchar(100) not null + ) + `); + await client.query(` + create table [rqbv2_schema].[posts] ( + [id] int primary key not null, + [content] varchar(100) not null, + [owner_id] int not null foreign key references [rqbv2_schema].[users]([id]) + ) + `); + await client.query(` + create view [rqbv2_schema].[users_view] as + select [id], [name] from [rqbv2_schema].[users] where [id] < 3 + `); + + await db.insert(scopedUsers).values([ + { id: 1, name: 'Dan' }, + { id: 2, name: 'Andrew' }, + { id: 3, name: 'Alex' }, + ]); + await db.insert(scopedPosts).values([ + { id: 1, content: 'Post1', ownerId: 1 }, + { id: 2, content: 'Post1.2', ownerId: 1 }, + { id: 3, content: 'Post2', ownerId: 2 }, + { id: 4, content: 'Post3', ownerId: 3 }, + ]); + + const result = await db.query.scopedUserView.findMany({ + columns: { + id: true, + name: true, + }, + orderBy: { + id: 'asc', + }, + with: { + posts: { + columns: { + id: true, + content: true, + }, + orderBy: { + id: 'asc', + }, + }, + }, + }); + + expect(result).toEqual([ + { + id: 1, + name: 'Dan', + posts: [ + { id: 1, content: 'Post1' }, + { id: 2, content: 'Post1.2' }, + ], + }, + { + id: 2, + name: 'Andrew', + posts: [{ id: 3, content: 'Post2' }], + }, + ]); +}); + +test('Pothos plugin resolves an MSSQL RQBv2 relation field', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + + interface PothosTypes { + DrizzleRelations: typeof relations; + } + + const builder = new SchemaBuilder({ + plugins: [DrizzlePlugin], + drizzle: { + client: db, + getTableConfig, + relations, + }, + }); + + const PostRef = builder.drizzleObject('postsTable', { + name: 'RQBv2Post', + select: { + columns: { + id: true, + content: true, + }, + }, + fields: (t) => ({ + id: t.exposeInt('id'), + content: t.exposeString('content'), + }), + }); + + builder.drizzleObject('usersTable', { + name: 'RQBv2User', + fields: (t) => ({ + id: t.exposeInt('id'), + name: t.exposeString('name'), + posts: t.relatedField('posts', { + type: [PostRef], + select: () => ({ + with: { + posts: { + columns: { + id: true, + content: true, + }, + orderBy: { + id: 'asc', + }, + }, + }, + }), + resolve: (user) => user.posts, + }), + postsCount: t.relatedField('posts', { + type: 'Int', + select: (buildFilter) => ({ + extras: { + postsCount: (user) => db.$count(schema.postsTable, buildFilter(user)), + }, + }), + resolve: (user) => user.postsCount, + }), + }), + }); + + builder.queryType({ + fields: (t) => ({ + users: t.drizzleField({ + type: ['usersTable'], + resolve: (query) => + db.query.usersTable.findMany(query({ + orderBy: { + id: 'asc', + }, + })), + }), + }), + }); + + const result = await graphql({ + schema: builder.toSchema(), + source: `{ + users { + id + name + postsCount + posts { + id + content + } + } + }`, + contextValue: {}, + }); + + expect(result.errors).toBeUndefined(); + expect(result.data).toEqual({ + users: [ + { + id: 1, + name: 'Dan', + postsCount: 2, + posts: [ + { id: 1, content: 'Post1' }, + { id: 2, content: 'Post1.2' }, + ], + }, + { + id: 2, + name: 'Andrew', + postsCount: 1, + posts: [{ id: 3, content: 'Post2' }], + }, + { + id: 3, + name: 'Alex', + postsCount: 0, + posts: [], + }, + ], + }); +}); + +test('V1 _query still works on MSSQL', async ({ client }) => { + const db = drizzle({ client, schema, relations }); + await seed(db); + + const result = await db._query.usersTable.findMany({ + columns: { + id: true, + name: true, + }, + limit: 1, + with: { + posts: { + columns: { + content: true, + }, + }, + }, + }); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + id: 1, + name: 'Dan', + }); + expect(result[0]!.posts.map((post) => post.content).sort()).toEqual(['Post1', 'Post1.2']); +}); diff --git a/integration-tests/tests/mssql/mssql.test.ts b/integration-tests/tests/mssql/mssql.test.ts index ac27e1c80a..a05ebc143d 100644 --- a/integration-tests/tests/mssql/mssql.test.ts +++ b/integration-tests/tests/mssql/mssql.test.ts @@ -22,6 +22,8 @@ import { sumDistinct, TransactionRollbackError, } from 'drizzle-orm'; +import { Cache, type MutationOption } from 'drizzle-orm/cache/core'; +import type { CacheConfig } from 'drizzle-orm/cache/core/types'; import { alias, bit, @@ -74,6 +76,76 @@ import { // const ENABLE_LOGGING = true; +class TestCache extends Cache { + private cache = new Map(); + private usedTablesPerKey = new Map>(); + + getCalls = 0; + putCalls = 0; + onMutateCalls = 0; + lastMutation: MutationOption | undefined; + + constructor(private readonly strat: 'explicit' | 'all') { + super(); + } + + override strategy(): 'explicit' | 'all' { + return this.strat; + } + + override async get(_key: string, _tables: string[], _isTag: boolean): Promise { + this.getCalls++; + return this.cache.get(_key); + } + + override async put( + key: string, + response: unknown, + tables: string[], + _isTag: boolean, + _config?: CacheConfig, + ): Promise { + this.putCalls++; + if (Array.isArray(response)) { + this.cache.set(key, response); + } + for (const table of tables) { + const keys = this.usedTablesPerKey.get(table) ?? new Set(); + keys.add(key); + this.usedTablesPerKey.set(table, keys); + } + } + + override async onMutate(params: MutationOption): Promise { + this.onMutateCalls++; + this.lastMutation = params; + + const tags = params.tags ? Array.isArray(params.tags) ? params.tags : [params.tags] : []; + const tables = params.tables ? Array.isArray(params.tables) ? params.tables : [params.tables] : []; + + for (const tag of tags) { + this.cache.delete(tag); + } + + for (const table of tables) { + if (typeof table !== 'string') continue; + + const keys = this.usedTablesPerKey.get(table) ?? new Set(); + for (const key of keys) { + this.cache.delete(key); + } + this.usedTablesPerKey.delete(table); + } + } + + reset() { + this.getCalls = 0; + this.putCalls = 0; + this.onMutateCalls = 0; + this.lastMutation = undefined; + } +} + test.beforeEach(async ({ client }) => { await client.query(`drop table if exists [userstest]`); await client.query(`drop table if exists [nvarchar_with_json]`); @@ -144,6 +216,54 @@ test.beforeEach(async ({ client }) => { )`); }); +test('cache: explicit select and mutation invalidation', async ({ client }) => { + const cache = new TestCache('explicit'); + const cachedDb = drizzle({ client, cache }); + + await cachedDb.select().from(usersTable).$withCache({ tag: 'users-cache', config: { ex: 1 } }); + await cachedDb.select().from(usersTable).$withCache({ tag: 'users-cache', config: { ex: 1 } }); + + expect(cache.getCalls).toBe(2); + expect(cache.putCalls).toBe(1); + expect(cache.onMutateCalls).toBe(0); + + cache.reset(); + + await cachedDb.insert(usersTable).values({ name: 'John' }); + + expect(cache.getCalls).toBe(0); + expect(cache.putCalls).toBe(0); + expect(cache.onMutateCalls).toBe(1); + expect(cache.lastMutation).toEqual({ tables: ['userstest'] }); + + cache.reset(); + + await cachedDb.select().from(usersTable).$withCache({ tag: 'users-cache', config: { ex: 1 } }); + + expect(cache.getCalls).toBe(1); + expect(cache.putCalls).toBe(1); + expect(cache.onMutateCalls).toBe(0); +}); + +test('cache: global strategy and per-query disable', async ({ client }) => { + const cache = new TestCache('all'); + const cachedDb = drizzle({ client, cache }); + + await cachedDb.select().from(usersTable); + + expect(cache.getCalls).toBe(1); + expect(cache.putCalls).toBe(1); + expect(cache.onMutateCalls).toBe(0); + + cache.reset(); + + await cachedDb.select().from(usersTable).$withCache(false); + + expect(cache.getCalls).toBe(0); + expect(cache.putCalls).toBe(0); + expect(cache.onMutateCalls).toBe(0); +}); + async function setupSetOperationTest(db: NodeMsSqlDatabase) { await db.execute(sql`drop table if exists [users2]`); await db.execute(sql`drop table if exists [cities]`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db7977940c..63b2275105 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,7 +297,7 @@ importers: version: 17.2.1 orm044: specifier: npm:drizzle-orm@0.44.1 - version: drizzle-orm@0.44.1(@aws-sdk/client-rds-data@3.940.0)(@cloudflare/workers-types@4.20251126.0)(@electric-sql/pglite@0.2.12)(@libsql/client-wasm@0.10.0)(@libsql/client@0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@neondatabase/serverless@1.0.2)(@op-engineering/op-sqlite@2.0.22(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.14.0(prisma@5.14.0))(@tidbcloud/serverless@0.1.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/sql.js@1.4.9)(@upstash/redis@1.35.7)(@vercel/postgres@0.8.0)(@xata.io/client@0.29.5(typescript@5.9.3))(better-sqlite3@11.9.1)(bun-types@1.3.3)(expo-sqlite@14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)))(gel@2.2.0)(mysql2@3.14.1)(pg@8.16.3)(postgres@3.4.7)(prisma@5.14.0)(sql.js@1.13.0)(sqlite3@5.1.7) + version: drizzle-orm@0.44.1(faabfae8bb53104d1a263e19cfffc720) pg: specifier: ^8.11.5 version: 8.16.3 @@ -445,7 +445,7 @@ importers: version: 4.0.0-beta.58 expo-sqlite: specifier: ^14.0.0 - version: 14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) + version: 14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) glob: specifier: ^11.0.1 version: 11.1.0 @@ -757,6 +757,12 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.3.1 + '@pothos/core': + specifier: ^4.12.0 + version: 4.12.0(graphql@16.14.2) + '@pothos/plugin-drizzle': + specifier: ^0.17.4 + version: 0.17.4(@pothos/core@4.12.0(graphql@16.14.2))(drizzle-orm@drizzle-orm+dist)(graphql@16.14.2) '@types/async-retry': specifier: ^1.4.8 version: 1.4.9 @@ -796,6 +802,12 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 + drizzle-orm: + specifier: workspace:../drizzle-orm/dist + version: link:../drizzle-orm/dist + graphql: + specifier: ^16.14.2 + version: 16.14.2 import-in-the-middle: specifier: ^1.13.1 version: 1.15.0 @@ -819,7 +831,7 @@ importers: dependencies: drizzle-beta: specifier: npm:drizzle-orm@1.0.0-beta.21 - version: drizzle-orm@1.0.0-beta.21(55ce416bfa2056a5af9efa292c7e71b1) + version: drizzle-orm@1.0.0-beta.21(767115bdb37ba6150f254cbeadb39494) drizzle-seed: specifier: workspace:../drizzle-seed/dist version: link:../drizzle-seed/dist @@ -2893,6 +2905,18 @@ packages: resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==} engines: {node: '>=16'} + '@pothos/core@4.12.0': + resolution: {integrity: sha512-PeiODrj3GjQ7Nbs/5p65DEyBWZTSGGjgGO/BgaMEqS1jBNX/2zJTEQJA9zM5uPmCHUCDjE7Qn2U7lOi0ALp/8A==} + peerDependencies: + graphql: ^16.10.0 + + '@pothos/plugin-drizzle@0.17.4': + resolution: {integrity: sha512-cAaMexSSOtoXf7Khj62yOHGxHyaOnuIzNdgl+w0rNfnJdtVOr4slQjHinWJWzYWdRauOclZddxWbLaou4NK0QQ==} + peerDependencies: + '@pothos/core': '*' + drizzle-orm: '>=1.0.0-beta.2' + graphql: ^16.10.0 + '@prettier/sync@0.5.5': resolution: {integrity: sha512-6BMtNr7aQhyNcGzmumkL0tgr1YQGfm9d7ZdmRpWqWuqpc9vZBind4xMe5NMiRECOhjuSiWHfBWLBnXkpeE90bw==} peerDependencies: @@ -3814,6 +3838,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@upstash/redis@1.35.7': resolution: {integrity: sha512-bdCdKhke+kYUjcLLuGWSeQw7OLuWIx3eyKksyToLBAlGIMX9qiII0ptp8E0y7VFE1yuBxBd/3kSzJ8774Q4g+A==} @@ -5838,6 +5863,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + hanji@0.0.8: resolution: {integrity: sha512-Z1n2OGhZGnNjrbI4FRSAla2wGcu+mafkTi0al0NSfsQN5ccka7n+rK/x2YISB4oYU8WGejjQWoyWyukRmbFVUA==} @@ -9104,7 +9133,9 @@ packages: snapshots: - '@0no-co/graphql.web@1.2.0': {} + '@0no-co/graphql.web@1.2.0(graphql@16.14.2)': + optionalDependencies: + graphql: 16.14.2 '@andrewbranch/untar.js@1.0.3': {} @@ -10883,9 +10914,9 @@ snapshots: dependencies: heap: 0.2.7 - '@expo/cli@54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': + '@expo/cli@54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': dependencies: - '@0no-co/graphql.web': 1.2.0 + '@0no-co/graphql.web': 1.2.0(graphql@16.14.2) '@expo/code-signing-certificates': 0.0.5 '@expo/config': 12.0.10 '@expo/config-plugins': 54.0.2 @@ -10895,18 +10926,18 @@ snapshots: '@expo/json-file': 10.0.7 '@expo/mcp-tunnel': 0.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@expo/metro': 54.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) + '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) '@expo/osascript': 2.3.7 '@expo/package-manager': 1.9.8 '@expo/plist': 0.4.7 - '@expo/prebuild-config': 54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) + '@expo/prebuild-config': 54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) '@expo/schema-utils': 0.1.7 '@expo/spawn-async': 1.7.2 '@expo/ws-tunnel': 1.0.6 '@expo/xcpretty': 4.3.2 '@react-native/dev-middleware': 0.81.5(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@urql/core': 5.2.0 - '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0) + '@urql/core': 5.2.0(graphql@16.14.2) + '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0(graphql@16.14.2)) accepts: 1.3.8 arg: 5.0.2 better-opn: 3.0.2 @@ -10918,7 +10949,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 env-editor: 0.4.2 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) expo-server: 1.0.4 freeport-async: 2.0.0 getenv: 2.0.0 @@ -10959,9 +10990,9 @@ snapshots: - supports-color - utf-8-validate - '@expo/cli@54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': + '@expo/cli@54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': dependencies: - '@0no-co/graphql.web': 1.2.0 + '@0no-co/graphql.web': 1.2.0(graphql@16.14.2) '@expo/code-signing-certificates': 0.0.5 '@expo/config': 12.0.10 '@expo/config-plugins': 54.0.2 @@ -10971,18 +11002,18 @@ snapshots: '@expo/json-file': 10.0.7 '@expo/mcp-tunnel': 0.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) '@expo/metro': 54.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) + '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) '@expo/osascript': 2.3.7 '@expo/package-manager': 1.9.8 '@expo/plist': 0.4.7 - '@expo/prebuild-config': 54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) + '@expo/prebuild-config': 54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) '@expo/schema-utils': 0.1.7 '@expo/spawn-async': 1.7.2 '@expo/ws-tunnel': 1.0.6 '@expo/xcpretty': 4.3.2 '@react-native/dev-middleware': 0.81.5(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@urql/core': 5.2.0 - '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0) + '@urql/core': 5.2.0(graphql@16.14.2) + '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0(graphql@16.14.2)) accepts: 1.3.8 arg: 5.0.2 better-opn: 3.0.2 @@ -10994,7 +11025,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 env-editor: 0.4.2 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) expo-server: 1.0.4 freeport-async: 2.0.0 getenv: 2.0.0 @@ -11156,7 +11187,7 @@ snapshots: - bufferutil - utf-8-validate - '@expo/metro-config@54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': + '@expo/metro-config@54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.5 @@ -11180,13 +11211,13 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@expo/metro-config@54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': + '@expo/metro-config@54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3)': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.5 @@ -11210,7 +11241,7 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -11256,7 +11287,7 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 - '@expo/prebuild-config@54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))': + '@expo/prebuild-config@54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))': dependencies: '@expo/config': 12.0.10 '@expo/config-plugins': 54.0.2 @@ -11265,14 +11296,14 @@ snapshots: '@expo/json-file': 10.0.7 '@react-native/normalize-colors': 0.81.5 debug: 4.4.3 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) resolve-from: 5.0.0 semver: 7.7.4 xml2js: 0.6.0 transitivePeerDependencies: - supports-color - '@expo/prebuild-config@54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))': + '@expo/prebuild-config@54.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))': dependencies: '@expo/config': 12.0.10 '@expo/config-plugins': 54.0.2 @@ -11281,7 +11312,7 @@ snapshots: '@expo/json-file': 10.0.7 '@react-native/normalize-colors': 0.81.5 debug: 4.4.3 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) resolve-from: 5.0.0 semver: 7.7.4 xml2js: 0.6.0 @@ -11299,15 +11330,15 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': + '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': dependencies: - expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) - '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': + '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)': dependencies: - expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) optional: true @@ -11748,6 +11779,16 @@ snapshots: '@planetscale/database@1.19.0': {} + '@pothos/core@4.12.0(graphql@16.14.2)': + dependencies: + graphql: 16.14.2 + + '@pothos/plugin-drizzle@0.17.4(@pothos/core@4.12.0(graphql@16.14.2))(drizzle-orm@drizzle-orm+dist)(graphql@16.14.2)': + dependencies: + '@pothos/core': 4.12.0(graphql@16.14.2) + drizzle-orm: link:drizzle-orm/dist + graphql: 16.14.2 + '@prettier/sync@0.5.5(prettier@3.5.3)': dependencies: make-synchronized: 0.4.2 @@ -12809,16 +12850,16 @@ snapshots: dependencies: uncrypto: 0.1.3 - '@urql/core@5.2.0': + '@urql/core@5.2.0(graphql@16.14.2)': dependencies: - '@0no-co/graphql.web': 1.2.0 + '@0no-co/graphql.web': 1.2.0(graphql@16.14.2) wonka: 6.3.5 transitivePeerDependencies: - graphql - '@urql/exchange-retry@1.3.2(@urql/core@5.2.0)': + '@urql/exchange-retry@1.3.2(@urql/core@5.2.0(graphql@16.14.2))': dependencies: - '@urql/core': 5.2.0 + '@urql/core': 5.2.0(graphql@16.14.2) wonka: 6.3.5 '@vercel/postgres@0.8.0': @@ -13260,7 +13301,7 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) - babel-preset-expo@54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2): + babel-preset-expo@54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2): dependencies: '@babel/helper-module-imports': 7.27.1 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5) @@ -13287,12 +13328,12 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.4 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color - babel-preset-expo@54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2): + babel-preset-expo@54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2): dependencies: '@babel/helper-module-imports': 7.27.1 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.5) @@ -13319,7 +13360,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.4 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -14027,7 +14068,7 @@ snapshots: sql.js: 1.13.0 sqlite3: 5.1.7 - drizzle-orm@0.44.1(@aws-sdk/client-rds-data@3.940.0)(@cloudflare/workers-types@4.20251126.0)(@electric-sql/pglite@0.2.12)(@libsql/client-wasm@0.10.0)(@libsql/client@0.10.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@neondatabase/serverless@1.0.2)(@op-engineering/op-sqlite@2.0.22(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.14.0(prisma@5.14.0))(@tidbcloud/serverless@0.1.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/sql.js@1.4.9)(@upstash/redis@1.35.7)(@vercel/postgres@0.8.0)(@xata.io/client@0.29.5(typescript@5.9.3))(better-sqlite3@11.9.1)(bun-types@1.3.3)(expo-sqlite@14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)))(gel@2.2.0)(mysql2@3.14.1)(pg@8.16.3)(postgres@3.4.7)(prisma@5.14.0)(sql.js@1.13.0)(sqlite3@5.1.7): + drizzle-orm@0.44.1(faabfae8bb53104d1a263e19cfffc720): optionalDependencies: '@aws-sdk/client-rds-data': 3.940.0 '@cloudflare/workers-types': 4.20251126.0 @@ -14048,7 +14089,7 @@ snapshots: '@xata.io/client': 0.29.5(typescript@5.9.3) better-sqlite3: 11.9.1 bun-types: 1.3.3 - expo-sqlite: 14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) + expo-sqlite: 14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) gel: 2.2.0 mysql2: 3.14.1 pg: 8.16.3 @@ -14057,7 +14098,7 @@ snapshots: sql.js: 1.13.0 sqlite3: 5.1.7 - drizzle-orm@1.0.0-beta.21(55ce416bfa2056a5af9efa292c7e71b1): + drizzle-orm@1.0.0-beta.21(767115bdb37ba6150f254cbeadb39494): dependencies: '@types/mssql': 9.1.8 mssql: 12.1.1 @@ -14088,7 +14129,7 @@ snapshots: arktype: 2.2.0 better-sqlite3: 12.6.2 bun-types: 1.3.3 - expo-sqlite: 14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) + expo-sqlite: 14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)) gel: 2.2.0 mysql2: 3.22.3(@types/node@25.5.0) pg: 8.20.0 @@ -14525,80 +14566,80 @@ snapshots: expect-type@1.2.2: {} - expo-asset@12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): + expo-asset@12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: '@expo/image-utils': 0.8.7 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) - expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - supports-color - expo-asset@12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): + expo-asset@12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: '@expo/image-utils': 0.8.7 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) - expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - supports-color optional: true - expo-constants@18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): + expo-constants@18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: '@expo/config': 12.0.10 '@expo/env': 2.0.7 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react-native: 0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - supports-color - expo-constants@18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): + expo-constants@18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: '@expo/config': 12.0.10 '@expo/env': 2.0.7 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react-native: 0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) transitivePeerDependencies: - supports-color optional: true - expo-file-system@19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): + expo-file-system@19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react-native: 0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) - expo-file-system@19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): + expo-file-system@19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react-native: 0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) optional: true - expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): + expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) fontfaceobserver: 2.3.0 react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) - expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): + expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) fontfaceobserver: 2.3.0 react: 18.3.1 react-native: 0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3) optional: true - expo-keep-awake@15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): + expo-keep-awake@15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react: 18.3.1 - expo-keep-awake@15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): + expo-keep-awake@15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1): dependencies: - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) react: 18.3.1 optional: true @@ -14625,35 +14666,35 @@ snapshots: expo-server@1.0.4: {} - expo-sqlite@14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)): + expo-sqlite@14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: '@expo/websql': 1.0.1 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) - expo-sqlite@14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)): + expo-sqlite@14.0.6(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3)): dependencies: '@expo/websql': 1.0.1 - expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) + expo: 54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3) optional: true - expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3): + expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3): dependencies: '@babel/runtime': 7.28.4 - '@expo/cli': 54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) + '@expo/cli': 54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) '@expo/config': 12.0.10 '@expo/config-plugins': 54.0.2 '@expo/devtools': 0.1.7(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) '@expo/fingerprint': 0.15.3 '@expo/metro': 54.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) - '@expo/vector-icons': 15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) + '@expo/vector-icons': 15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) '@ungap/structured-clone': 1.3.0 - babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2) - expo-asset: 12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) - expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) - expo-file-system: 19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) - expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) - expo-keep-awake: 15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2) + expo-asset: 12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo-file-system: 19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-keep-awake: 15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) expo-modules-autolinking: 3.0.22 expo-modules-core: 3.0.26(react-native@0.82.1(@babel/core@7.28.5)(@types/react@18.3.27)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) pretty-format: 29.7.0 @@ -14670,24 +14711,24 @@ snapshots: - supports-color - utf-8-validate - expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3): + expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3): dependencies: '@babel/runtime': 7.28.4 - '@expo/cli': 54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) + '@expo/cli': 54.0.16(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) '@expo/config': 12.0.10 '@expo/config-plugins': 54.0.2 '@expo/devtools': 0.1.7(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) '@expo/fingerprint': 0.15.3 '@expo/metro': 54.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) - '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) - '@expo/vector-icons': 15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + '@expo/metro-config': 54.0.9(bufferutil@4.0.8)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(utf-8-validate@6.0.3) + '@expo/vector-icons': 15.0.3(expo-font@14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) '@ungap/structured-clone': 1.3.0 - babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2) - expo-asset: 12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) - expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) - expo-file-system: 19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) - expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) - expo-keep-awake: 15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + babel-preset-expo: 54.0.7(@babel/core@7.28.5)(@babel/runtime@7.28.4)(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-refresh@0.14.2) + expo-asset: 12.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-constants: 18.0.10(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo-file-system: 19.0.19(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3)) + expo-font: 14.0.9(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) + expo-keep-awake: 15.0.7(expo@54.0.25(@babel/core@7.28.5)(bufferutil@4.0.8)(graphql@16.14.2)(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) expo-modules-autolinking: 3.0.22 expo-modules-core: 3.0.26(react-native@0.82.1(@babel/core@7.28.5)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@6.0.3))(react@18.3.1) pretty-format: 29.7.0 @@ -15062,6 +15103,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.14.2: {} + hanji@0.0.8: dependencies: lodash.throttle: 4.1.1