diff --git a/drizzle-kit/src/cli/commands/up-sqlite.ts b/drizzle-kit/src/cli/commands/up-sqlite.ts index dc28ea178..463405a43 100644 --- a/drizzle-kit/src/cli/commands/up-sqlite.ts +++ b/drizzle-kit/src/cli/commands/up-sqlite.ts @@ -5,8 +5,9 @@ import { join } from 'path'; import { transformOnUpdateDelete } from 'src/dialects/sqlite/grammar'; import { nameForPk } from 'src/dialects/sqlite/grammar'; import { prepareOutFolder, validateWithReport } from 'src/utils/utils-node'; -import { createDDL } from '../../dialects/sqlite/ddl'; +import { createDDL, type SqliteEntity, type Table } from '../../dialects/sqlite/ddl'; import { + snapshotValidator, sqliteSchemaV5, type SQLiteSchemaV6, sqliteSchemaV6, @@ -32,9 +33,11 @@ export const upSqliteHandler = (out: string) => { let result: SqliteSnapshot; if (it.raw['version'] === '5') { - result = updateToV7(updateUpToV6(it.raw)); + result = updateToV8(updateToV7(updateUpToV6(it.raw))); } else if (it.raw['version'] === '6') { - result = updateToV7(sqliteSchemaV6.parse(it.raw)); + result = updateToV8(updateToV7(sqliteSchemaV6.parse(it.raw))); + } else if (it.raw['version'] === '7') { + result = updateToV8(it.raw as SqliteSnapshotV7); } else { throw new Error(`unexpected version of SQLite snapshot: ${it.raw['version']}`); } @@ -51,11 +54,19 @@ export const upSqliteHandler = (out: string) => { console.log("Everything's fine 🐢πŸ”₯"); }; -export const updateToV7 = (snapshot: SQLiteSchemaV6): SqliteSnapshot => { +type SqliteEntityV7 = Exclude | Omit; + +export type SqliteSnapshotV7 = Omit & { + version: '7'; + ddl: SqliteEntityV7[]; +}; + +export const updateToV7 = (snapshot: SQLiteSchemaV6): SqliteSnapshotV7 => { const ddl = createDDL(); for (const table of Object.values(snapshot.tables)) { ddl.tables.push({ name: table.name, + isStrict: false, }); for (const column of Object.values(table.columns)) { @@ -162,6 +173,18 @@ export const updateToV7 = (snapshot: SQLiteSchemaV6): SqliteSnapshot => { }; }; +export const updateToV8 = (snapshot: SqliteSnapshotV7): SqliteSnapshot => { + return snapshotValidator.strict({ + ...snapshot, + version: '8', + ddl: snapshot.ddl.map((entity) => + entity.entityType === 'tables' + ? { ...entity, isStrict: false } + : entity + ), + }); +}; + const updateUpToV6 = (json: object): SQLiteSchemaV6 => { const schema = sqliteSchemaV5.parse(json); diff --git a/drizzle-kit/src/cli/views.ts b/drizzle-kit/src/cli/views.ts index 80e315f68..1b13d40c9 100644 --- a/drizzle-kit/src/cli/views.ts +++ b/drizzle-kit/src/cli/views.ts @@ -99,6 +99,10 @@ export const sqliteSchemaError = (error: SqliteSchemaError): string => { return `'${error.table}' table has no columns`; } + if (error.type === 'invalid_strict_column_type') { + return `'${error.column}' column in STRICT table '${error.table}' has unsupported type '${error.columnType}'. Allowed types: INT, INTEGER, REAL, TEXT, BLOB, ANY`; + } + assertUnreachable(error); }; diff --git a/drizzle-kit/src/dialects/sqlite/convertor.ts b/drizzle-kit/src/dialects/sqlite/convertor.ts index a69fbeb1c..9307d8929 100644 --- a/drizzle-kit/src/dialects/sqlite/convertor.ts +++ b/drizzle-kit/src/dialects/sqlite/convertor.ts @@ -31,6 +31,9 @@ const createTable = convertor('create_table', (st) => { statement += `CREATE TABLE \`${tableName}\` (\n`; for (let i = 0; i < columns.length; i++) { const column = columns[i]; + const columnType = st.table.isStrict && /^text\(\d+\)$/i.test(column.type) + ? 'text' + : column.type; /* https://www.sqlite.org/lang_createtable.html#the_generated_always_as_clause @@ -71,7 +74,7 @@ const createTable = convertor('create_table', (st) => { statement += '\t'; statement += - `\`${column.name}\` ${column.type}${primaryKeyStatement}${autoincrementStatement}${defaultStatement}${generatedStatement}${notNullStatement}${unqiueConstraintPrefix}`; + `\`${column.name}\` ${columnType}${primaryKeyStatement}${autoincrementStatement}${defaultStatement}${generatedStatement}${notNullStatement}${unqiueConstraintPrefix}`; statement += i === columns.length - 1 ? '' : ',\n'; } @@ -118,7 +121,7 @@ const createTable = convertor('create_table', (st) => { } statement += `\n`; - statement += `);`; + statement += `)${st.table.isStrict ? ' STRICT' : ''};`; statement += `\n`; return statement; }); diff --git a/drizzle-kit/src/dialects/sqlite/ddl.ts b/drizzle-kit/src/dialects/sqlite/ddl.ts index abb5258ec..57768b506 100644 --- a/drizzle-kit/src/dialects/sqlite/ddl.ts +++ b/drizzle-kit/src/dialects/sqlite/ddl.ts @@ -4,7 +4,9 @@ import { nameForPk, nameForUnique } from './grammar'; export const createDDL = () => { return create({ - tables: {}, + tables: { + isStrict: 'boolean', + }, columns: { table: 'required', type: 'string', @@ -99,6 +101,7 @@ export type ViewColumn = { view: string; name: string; type: string; notNull: bo export type TableFull = { name: string; + isStrict: boolean; columns: Column[]; indexes: Index[]; checks: CheckConstraint[]; @@ -109,6 +112,7 @@ export type TableFull = { export const tableFromDDL = (name: string, ddl: SQLiteDDL): TableFull => { const filter = { table: name } as const; + const table = ddl.tables.one({ name }); const columns = ddl.columns.list(filter); const pk = ddl.pks.one(filter); const fks = ddl.fks.list(filter); @@ -117,6 +121,7 @@ export const tableFromDDL = (name: string, ddl: SQLiteDDL): TableFull => { const indexes = ddl.indexes.list(filter); return { name, + isStrict: table?.isStrict ?? false, columns, pk, fks, @@ -169,6 +174,13 @@ export type ConflictCheck = { name: string; }; +export type InvalidStrictColumnType = { + type: 'invalid_strict_column_type'; + table: string; + column: string; + columnType: string; +}; + export type SchemaError = | ConflictTable | ConflictView @@ -178,16 +190,9 @@ export type SchemaError = | ConflictUnique | ConflictCheck | ConflictIndex + | InvalidStrictColumnType | TableNoColumns; -const count = (arr: T[], predicate: (it: T) => boolean) => { - let count = 0; - for (const it of arr) { - if (predicate(it)) count += 1; - } - return count; -}; - export type InterimColumn = Column & { pk: boolean; pkName: string | null; @@ -217,10 +222,24 @@ export const interimToDDL = (schema: InterimSchema): { ddl: SQLiteDDL; errors: S const errors: SchemaError[] = []; for (const table of schema.tables) { - if (count(schema.columns, (it) => it.table === table.name) === 0) { + const columns = schema.columns.filter((it) => it.table === table.name); + if (columns.length === 0) { errors.push({ type: 'table_no_columns', table: table.name }); continue; } + if (table.isStrict) { + const allowed = new Set(['int', 'integer', 'real', 'text', 'blob', 'any']); + for (const column of columns) { + if (!allowed.has(column.type.toLowerCase())) { + errors.push({ + type: 'invalid_strict_column_type', + table: table.name, + column: column.name, + columnType: column.type, + }); + } + } + } const res = ddl.tables.push(table); if (res.status === 'CONFLICT') { errors.push({ type: 'conflict_table', table: res.data.name }); diff --git a/drizzle-kit/src/dialects/sqlite/diff.ts b/drizzle-kit/src/dialects/sqlite/diff.ts index 8b9936503..455594c20 100644 --- a/drizzle-kit/src/dialects/sqlite/diff.ts +++ b/drizzle-kit/src/dialects/sqlite/diff.ts @@ -285,6 +285,13 @@ export const ddlDiff = async ( ].map((it) => it.table), ); + for (const table of ddl2.tables.list()) { + const previous = ddl1.tables.one({ name: table.name }); + if (previous && previous.isStrict !== table.isStrict) { + setOfTablesToRecereate.add(table.name); + } + } + for (const it of createdTables) { setOfTablesToRecereate.delete(it.name); } diff --git a/drizzle-kit/src/dialects/sqlite/drizzle.ts b/drizzle-kit/src/dialects/sqlite/drizzle.ts index d2d0b00e0..352013f4e 100644 --- a/drizzle-kit/src/dialects/sqlite/drizzle.ts +++ b/drizzle-kit/src/dialects/sqlite/drizzle.ts @@ -36,6 +36,7 @@ export const fromDrizzleSchema = ( return { entityType: 'tables', name: it.config.name, + isStrict: it.config.isStrict ?? false, } satisfies Table; }); @@ -44,6 +45,7 @@ export const fromDrizzleSchema = ( const { name } = column; const primaryKey: boolean = column.primary; const generated = column.generated; + const sqlType = column.getSQLType(); const generatedObj: { as: string; @@ -77,7 +79,9 @@ export const fromDrizzleSchema = ( entityType: 'columns', table: it.config.name, name, - type: column.getSQLType(), + type: it.config.isStrict && /^text\(\d+\)$/i.test(sqlType) + ? 'text' + : sqlType, default: defalutValue, notNull: column.notNull && !primaryKey, pk: primaryKey, diff --git a/drizzle-kit/src/dialects/sqlite/introspect.ts b/drizzle-kit/src/dialects/sqlite/introspect.ts index 904270113..3a5975c42 100644 --- a/drizzle-kit/src/dialects/sqlite/introspect.ts +++ b/drizzle-kit/src/dialects/sqlite/introspect.ts @@ -361,11 +361,17 @@ export const fromDatabase = async ( return acc; }, {} as Record) || {}; + const isStrictTable = (sql: string): boolean => { + const options = sql.slice(sql.lastIndexOf(')') + 1); + return /\bSTRICT\b/i.test(options); + }; + const tables: SqliteEntities['tables'][] = [ ...new Set(dbTableColumns.filter((it) => it.type === 'table').map((it) => it.table)), ].map((it) => ({ entityType: 'tables', name: it, + isStrict: isStrictTable(tablesToSQL[it]), })); const pks: PrimaryKey[] = []; diff --git a/drizzle-kit/src/dialects/sqlite/serializer.ts b/drizzle-kit/src/dialects/sqlite/serializer.ts index a0e2da0df..2cc8c1d74 100644 --- a/drizzle-kit/src/dialects/sqlite/serializer.ts +++ b/drizzle-kit/src/dialects/sqlite/serializer.ts @@ -64,7 +64,7 @@ export const prepareSqliteSnapshot = async ( : findLeafSnapshotIds(snapshots); const snapshot = { - version: '7', + version: '8', dialect: 'sqlite', id, prevIds, @@ -146,6 +146,7 @@ export function generateLatestSnapshot( const insertTableFull = (table: { name: string; + isStrict: boolean; columns: Column[]; indexes: Index[]; pk: PrimaryKey | null; @@ -153,7 +154,7 @@ export function generateLatestSnapshot( uniques: UniqueConstraint[]; checks: { table: string; value: string; name?: string }[]; }) => { - ddl.tables.push({ name: table.name }); + ddl.tables.push({ name: table.name, isStrict: table.isStrict }); for (const column of table.columns) push(ddl.columns, column); for (const index of table.indexes) push(ddl.indexes, index); if (table.pk) push(ddl.pks, table.pk); diff --git a/drizzle-kit/src/dialects/sqlite/snapshot.ts b/drizzle-kit/src/dialects/sqlite/snapshot.ts index 9c60101c9..275a333cf 100644 --- a/drizzle-kit/src/dialects/sqlite/snapshot.ts +++ b/drizzle-kit/src/dialects/sqlite/snapshot.ts @@ -103,7 +103,7 @@ export const schemaInternalV5 = object({ }), }).strict(); -const latestVersion = literal('7'); +const latestVersion = literal('8'); export const schemaInternalV6 = object({ version: literal('6'), dialect: dialect, @@ -164,7 +164,7 @@ export const toJsonSnapshot = ( dialect: 'sqlite', id, prevIds, - version: '7', + version: '8', ddl: ddl.entities.list(), renames, }; @@ -172,7 +172,7 @@ export const toJsonSnapshot = ( const ddl = createDDL(); export const snapshotValidator = validator({ - version: ['7'], + version: ['8'], dialect: ['sqlite'], id: 'string', prevIds: array((_) => true), @@ -182,7 +182,7 @@ export const snapshotValidator = validator({ export type SqliteSnapshot = typeof snapshotValidator.shape; export const drySqliteSnapshot = snapshotValidator.strict({ - version: '7', + version: '8', dialect: 'sqlite', id: originUUID, prevIds: [], diff --git a/drizzle-kit/src/dialects/sqlite/typescript.ts b/drizzle-kit/src/dialects/sqlite/typescript.ts index 2f5dfdcf4..5d7e770e0 100644 --- a/drizzle-kit/src/dialects/sqlite/typescript.ts +++ b/drizzle-kit/src/dialects/sqlite/typescript.ts @@ -112,7 +112,9 @@ export const ddlToTypeScript = ( const uniqies = schema.uniques.list({ table: table.name }); const checks = schema.checks.list({ table: table.name }); - let statement = `export const ${withCasing(table.name, casing)} = sqliteTable("${table.name}", {\n`; + let statement = `export const ${withCasing(table.name, casing)} = sqliteTable${ + table.isStrict ? '.strict' : '' + }("${table.name}", {\n`; statement += createTableColumns(columns, fks, pk, casing); statement += '}'; diff --git a/drizzle-kit/src/utils/utils-node.ts b/drizzle-kit/src/utils/utils-node.ts index 72170dc82..a4c3be993 100644 --- a/drizzle-kit/src/utils/utils-node.ts +++ b/drizzle-kit/src/utils/utils-node.ts @@ -289,7 +289,7 @@ const mssqlSnapshotValidator = (snapshot: object): ValidationResult => { }; const sqliteValidator = (snapshot: object): ValidationResult => { - const versionError = assertVersion(snapshot, 7); + const versionError = assertVersion(snapshot, 8); if (versionError) return { status: versionError }; const { success } = sqliteStapshotValidator.parse(snapshot); diff --git a/drizzle-kit/tests/sqlite/commutativity.test.ts b/drizzle-kit/tests/sqlite/commutativity.test.ts index aff19afe5..fcd61d15e 100644 --- a/drizzle-kit/tests/sqlite/commutativity.test.ts +++ b/drizzle-kit/tests/sqlite/commutativity.test.ts @@ -15,7 +15,7 @@ function makeSnapshot( schema: SqliteSchema, ): SqliteSnapshot { return { - version: '7', + version: '8', dialect: 'sqlite', id, prevIds, diff --git a/drizzle-kit/tests/sqlite/generate-flow.test.ts b/drizzle-kit/tests/sqlite/generate-flow.test.ts index 1a69391f6..a7f3be7ab 100644 --- a/drizzle-kit/tests/sqlite/generate-flow.test.ts +++ b/drizzle-kit/tests/sqlite/generate-flow.test.ts @@ -18,7 +18,7 @@ function makeSnapshot( schema: SqliteSchema, ): SqliteSnapshot { return { - version: '7', + version: '8', dialect: 'sqlite', id, prevIds, @@ -175,7 +175,7 @@ describe('generateLatestSnapshot (sqlite)', () => { const fromDDL = drizzleToDDL(from).ddl; const toDDL = drizzleToDDL(to).ddl; const base: SqliteSnapshot = { - version: '7', + version: '8', dialect: 'sqlite', id: 'snapshot-id', prevIds: [ORIGIN], @@ -238,7 +238,7 @@ describe('generateLatestSnapshot (sqlite)', () => { const combined: JsonStatement[] = [...stmtsA, ...stmtsB]; const base: SqliteSnapshot = { - version: '7', + version: '8', dialect: 'sqlite', id: 'parent-id', prevIds: [ORIGIN], diff --git a/drizzle-kit/tests/sqlite/mocks.ts b/drizzle-kit/tests/sqlite/mocks.ts index b7ac08b72..3e675f3a3 100644 --- a/drizzle-kit/tests/sqlite/mocks.ts +++ b/drizzle-kit/tests/sqlite/mocks.ts @@ -18,7 +18,7 @@ import { SQLiteDB } from 'src/utils'; import { mockResolver } from 'src/utils/mocks'; import { tsc } from 'tests/utils'; import 'zx/globals'; -import { updateToV7 } from 'src/cli/commands/up-sqlite'; +import { updateToV7, updateToV8 } from 'src/cli/commands/up-sqlite'; import { serializeSQLite } from 'src/legacy/sqlite-v6/serializer'; import { diff as legacyDiff } from 'src/legacy/sqlite-v6/sqliteDiff'; @@ -396,7 +396,7 @@ export const diffSnapshotV6 = async ( await db.run(st); } - const snapshot = updateToV7(res); + const snapshot = updateToV8(updateToV7(res)); const ddl = fromEntities(snapshot.ddl); diff --git a/drizzle-kit/tests/sqlite/pull.test.ts b/drizzle-kit/tests/sqlite/pull.test.ts index 1c3ef829d..a483a1292 100644 --- a/drizzle-kit/tests/sqlite/pull.test.ts +++ b/drizzle-kit/tests/sqlite/pull.test.ts @@ -18,7 +18,8 @@ import { } from 'drizzle-orm/sqlite-core'; import * as fs from 'fs'; import { interimToDDL } from 'src/dialects/sqlite/ddl'; -import { fromDatabaseForDrizzle } from 'src/dialects/sqlite/introspect'; +import { fromDatabase, fromDatabaseForDrizzle } from 'src/dialects/sqlite/introspect'; +import { ddlToTypeScript } from 'src/dialects/sqlite/typescript'; import { expect, test } from 'vitest'; import { dbFrom, diffAfterPull, push } from './mocks'; @@ -538,10 +539,37 @@ test('pull after migrate with custom migrations table #1', async () => { { entityType: 'tables', name: 'users', + isStrict: false, }, ]); }); +test('pull preserves strict table metadata', async () => { + const sqlite = new Database(':memory:'); + const db = dbFrom(sqlite); + + await db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT) STRICT;'); + + const schema = await fromDatabase( + db, + () => true, + ); + + expect(schema.tables).toStrictEqual([ + { + entityType: 'tables', + name: 'users', + isStrict: true, + }, + ]); + + const { ddl, errors } = interimToDDL(schema); + expect(errors).toEqual([]); + expect(ddlToTypeScript(ddl, 'camel', {}, 'sqlite').file).toContain( + 'export const users = sqliteTable.strict("users"', + ); +}); + // filter custom migration table test('pull after migrate with custom migrations table #2', async () => { const sqlite = new Database(':memory:'); @@ -574,6 +602,7 @@ test('pull after migrate with custom migrations table #2', async () => { { entityType: 'tables', name: 'users', + isStrict: false, }, ]); }); diff --git a/drizzle-kit/tests/sqlite/snapshot-v7.test.ts b/drizzle-kit/tests/sqlite/snapshot-v7.test.ts new file mode 100644 index 000000000..27a0c276a --- /dev/null +++ b/drizzle-kit/tests/sqlite/snapshot-v7.test.ts @@ -0,0 +1,30 @@ +import { type SqliteSnapshotV7, updateToV8 } from 'src/cli/commands/up-sqlite'; +import { expect, test } from 'vitest'; + +test('upgrades SQLite v7 tables to non-strict v8 metadata', () => { + const snapshot: SqliteSnapshotV7 = { + version: '7', + dialect: 'sqlite', + id: 'snapshot-id', + prevIds: ['parent-id'], + ddl: [ + { + entityType: 'tables', + name: 'users', + }, + ], + renames: [], + }; + + expect(updateToV8(snapshot)).toEqual({ + ...snapshot, + version: '8', + ddl: [ + { + entityType: 'tables', + name: 'users', + isStrict: false, + }, + ], + }); +}); diff --git a/drizzle-kit/tests/sqlite/sqlite-tables.test.ts b/drizzle-kit/tests/sqlite/sqlite-tables.test.ts index 380313f38..3c017a6c8 100644 --- a/drizzle-kit/tests/sqlite/sqlite-tables.test.ts +++ b/drizzle-kit/tests/sqlite/sqlite-tables.test.ts @@ -55,6 +55,95 @@ test('add table #1', async () => { expect(pst).toStrictEqual(st0); }); +test('add strict table', async () => { + const to = { + users: sqliteTable.strict('users', { + id: int(), + name: text({ length: 64 }), + }), + }; + + const { sqlStatements: st } = await diff({}, to, []); + const { sqlStatements: pst } = await push({ db, to }); + + const st0: string[] = [ + 'CREATE TABLE `users` (\n' + + '\t`id` integer,\n' + + '\t`name` text\n' + + ') STRICT;\n', + ]; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('enable strict mode by recreating the table', async () => { + const from = { + users: sqliteTable('users', { id: int() }), + }; + const to = { + users: sqliteTable.strict('users', { id: int() }), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + await push({ db, to: from }); + const { sqlStatements: pst } = await push({ db, to }); + + const st0: string[] = [ + 'PRAGMA foreign_keys=OFF;', + 'CREATE TABLE `__new_users` (\n\t`id` integer\n) STRICT;\n', + 'INSERT INTO `__new_users`(`id`) SELECT `id` FROM `users`;', + 'DROP TABLE `users`;', + 'ALTER TABLE `__new_users` RENAME TO `users`;', + 'PRAGMA foreign_keys=ON;', + ]; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('disable strict mode by recreating the table', async () => { + const from = { + users: sqliteTable.strict('users', { id: int() }), + }; + const to = { + users: sqliteTable('users', { id: int() }), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + await push({ db, to: from }); + const { sqlStatements: pst } = await push({ db, to }); + + const st0: string[] = [ + 'PRAGMA foreign_keys=OFF;', + 'CREATE TABLE `__new_users` (\n\t`id` integer\n);\n', + 'INSERT INTO `__new_users`(`id`) SELECT `id` FROM `users`;', + 'DROP TABLE `users`;', + 'ALTER TABLE `__new_users` RENAME TO `users`;', + 'PRAGMA foreign_keys=ON;', + ]; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('reject unsupported strict column types before SQL generation', async () => { + const to = { + users: sqliteTable.strict('users', { + amount: numeric(), + }), + }; + + const { err2 } = await diff({}, to, []); + expect(err2).toStrictEqual([ + { + type: 'invalid_strict_column_type', + table: 'users', + column: 'amount', + columnType: 'numeric', + }, + ]); +}); + test('add table #2', async () => { const to = { users: sqliteTable('users', { diff --git a/drizzle-orm/src/sqlite-core/README.md b/drizzle-orm/src/sqlite-core/README.md index ae5fbe660..bb0aabac0 100644 --- a/drizzle-orm/src/sqlite-core/README.md +++ b/drizzle-orm/src/sqlite-core/README.md @@ -49,6 +49,21 @@ const db = drizzle(sqlite); const allUsers = db.select().from(users).all(); ``` +### Strict tables + +Use `sqliteTable.strict` to generate a SQLite +[`STRICT`](https://www.sqlite.org/stricttables.html) table: + +```typescript +const users = sqliteTable.strict('users', { + id: integer('id').primaryKey(), + fullName: text('full_name'), +}) +``` + +Drizzle Kit preserves strictness in generated migrations, introspection, and +table rebuilds. + ### Using Drizzle ORM in Next.js App Router Next.js' App Router have zero-config support for Drizzle ORM. diff --git a/drizzle-orm/src/sqlite-core/table.ts b/drizzle-orm/src/sqlite-core/table.ts index d0c93c92e..697156f53 100644 --- a/drizzle-orm/src/sqlite-core/table.ts +++ b/drizzle-orm/src/sqlite-core/table.ts @@ -31,6 +31,8 @@ export type TableConfig = TableConfigBase; /** @internal */ export const InlineForeignKeys = Symbol.for('drizzle:SQLiteInlineForeignKeys'); +/** @internal */ +export const Strict = Symbol.for('drizzle:SQLiteStrict'); export class SQLiteTable extends Table { static override readonly [entityKind]: string = 'SQLiteTable'; @@ -38,6 +40,7 @@ export class SQLiteTable extends Table { /** @internal */ static override readonly Symbol = Object.assign({}, Table.Symbol, { InlineForeignKeys: InlineForeignKeys as typeof InlineForeignKeys, + Strict: Strict as typeof Strict, }); /** @internal */ @@ -46,6 +49,9 @@ export class SQLiteTable extends Table { /** @internal */ [InlineForeignKeys]: ForeignKey[] = []; + /** @internal */ + [Strict]: boolean = false; + /** @internal */ override [Table.Symbol.ExtraConfigBuilder]: | ((self: Record) => SQLiteTableExtraConfig) @@ -61,7 +67,7 @@ export type SQLiteTableWithColumns = & T['columns'] & InferTableColumnsModels; -export interface SQLiteTableFn { +export interface SQLiteTableFnInternal { < TTableName extends string, TColumnsMap extends Record, @@ -164,6 +170,10 @@ export interface SQLiteTableFn { }>; } +export interface SQLiteTableFn extends SQLiteTableFnInternal { + strict: SQLiteTableFnInternal; +} + /** @internal */ export function sqliteTableBase< TTableName extends string, @@ -226,7 +236,16 @@ export function sqliteTableBase< /** @internal */ export function sqliteTableWithCasing(casing: Casing | undefined): SQLiteTableFn { - return (name, columns, extraConfig) => sqliteTableBase(name, columns, extraConfig, undefined, casing); + const sqliteTableInternal: SQLiteTableFnInternal = (name, columns, extraConfig) => + sqliteTableBase(name, columns, extraConfig, undefined, casing); + + const sqliteTableStrict: SQLiteTableFn['strict'] = (name, columns, extraConfig) => { + const table = sqliteTableBase(name, columns, extraConfig, undefined, casing); + table[Strict] = true; + return table; + }; + + return Object.assign(sqliteTableInternal, { strict: sqliteTableStrict }); } export const sqliteTable = sqliteTableWithCasing(undefined); @@ -235,7 +254,21 @@ export function sqliteTableCreator( customizeTableName: (name: string) => string, casing?: Casing | undefined, ): SQLiteTableFn { - return (name, columns, extraConfig) => { - return sqliteTableBase(customizeTableName(name) as typeof name, columns, extraConfig, undefined, casing, name); - }; + const sqliteTableInternal: SQLiteTableFnInternal = (name, columns, extraConfig) => + sqliteTableBase(customizeTableName(name) as typeof name, columns, extraConfig, undefined, casing, name); + + return Object.assign(sqliteTableInternal, { + strict: ((name, columns, extraConfig) => { + const table = sqliteTableBase( + customizeTableName(name) as typeof name, + columns, + extraConfig, + undefined, + casing, + name, + ); + table[Strict] = true; + return table; + }) as SQLiteTableFnInternal, + }); } diff --git a/drizzle-orm/src/sqlite-core/utils.ts b/drizzle-orm/src/sqlite-core/utils.ts index 361456c02..87997fd5c 100644 --- a/drizzle-orm/src/sqlite-core/utils.ts +++ b/drizzle-orm/src/sqlite-core/utils.ts @@ -24,6 +24,7 @@ export function getTableConfig(table: TTable) { const uniqueConstraints: UniqueConstraint[] = []; const foreignKeys: ForeignKey[] = Object.values(table[SQLiteTable.Symbol.InlineForeignKeys]); const name = table[Table.Symbol.Name]; + const isStrict = table[SQLiteTable.Symbol.Strict] ?? false; const extraConfigBuilder = table[SQLiteTable.Symbol.ExtraConfigBuilder]; @@ -53,6 +54,7 @@ export function getTableConfig(table: TTable) { primaryKeys, uniqueConstraints, name, + isStrict, }; } diff --git a/drizzle-orm/tests/effect-schema-sqlite.test.ts b/drizzle-orm/tests/effect-schema-sqlite.test.ts new file mode 100644 index 000000000..6078351ef --- /dev/null +++ b/drizzle-orm/tests/effect-schema-sqlite.test.ts @@ -0,0 +1,38 @@ +import { Schema } from 'effect'; +import { describe, expect, test } from 'vitest'; +import { createSelectSchema } from '~/effect-schema'; +import { getTableConfig, SQLiteTable, sqliteTable, sqliteTableCreator, text } from '~/sqlite-core'; + +describe('SQLite Effect Schema', () => { + test('derives schemas for strict tables', () => { + const users = sqliteTable.strict('users', { + name: text({ length: 3 }).notNull(), + }); + const schema = createSelectSchema(users); + + expect(getTableConfig(users).isStrict).toBe(true); + expect(Schema.decodeUnknownSync(schema)({ name: 'abc' })).toEqual({ name: 'abc' }); + expect(() => Schema.decodeUnknownSync(schema)({ name: 'abcd' })).toThrow(); + }); + + test('preserves strict mode through custom table creators', () => { + const prefixedTable = sqliteTableCreator((name) => `app_${name}`); + const users = prefixedTable.strict('users', { + name: text().notNull(), + }); + + expect(getTableConfig(users)).toMatchObject({ + name: 'app_users', + isStrict: true, + }); + }); + + test('defaults mixed-version table metadata to non-strict', () => { + const users = sqliteTable('users', { + name: text().notNull(), + }); + Reflect.deleteProperty(users, SQLiteTable.Symbol.Strict); + + expect(getTableConfig(users).isStrict).toBe(false); + }); +});