Skip to content

Commit 4e95353

Browse files
authored
feat(db-sqlite): add autoIncrement option (#9427)
### What? Exposes ability to enable [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for Primary Keys which ensures that the same ID cannot be reused from previously deleted rows. ```ts sqliteAdapter({ autoIncrement: true }) ``` ### Why? This may be essential for some systems. Enabled `autoIncrement: true` also for the SQLite Adapter in our tests, which can be useful when testing whether the doc was deleted or not when you also have other create operations. ### How? Uses Drizzle's `autoIncrement` option. WARNING: This cannot be enabled in an existing project without a custom migration, as it completely changes how primary keys are stored in the database.
1 parent 99481cb commit 4e95353

File tree

10 files changed

+72
-31
lines changed

10 files changed

+72
-31
lines changed

docs/database/sqlite.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default buildConfig({
4848
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
4949
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
5050
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
51+
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
5152

5253
## Access to Drizzle
5354

packages/db-sqlite/src/columnToCodeConverter.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
8888
}
8989

9090
if (column.primaryKey) {
91-
code = `${code}.primaryKey()`
91+
let arg = ''
92+
93+
if (column.type === 'integer' && column.autoIncrement) {
94+
arg = `{ autoIncrement: true }`
95+
}
96+
97+
code = `${code}.primaryKey(${arg})`
9298
}
9399

94100
if (defaultStatement) {

packages/db-sqlite/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
8686
return createDatabaseAdapter<SQLiteAdapter>({
8787
name: 'sqlite',
8888
afterSchemaInit: args.afterSchemaInit ?? [],
89+
autoIncrement: args.autoIncrement ?? false,
8990
beforeSchemaInit: args.beforeSchemaInit ?? [],
9091
client: undefined,
9192
clientConfig: args.client,

packages/db-sqlite/src/schema/buildDrizzleTable.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ export const buildDrizzleTable: BuildDrizzleTable = ({ adapter, locales, rawTabl
8787
}
8888

8989
if (column.primaryKey) {
90-
columns[key].primaryKey()
90+
let args: Record<string, unknown> | undefined = undefined
91+
92+
if (column.type === 'integer' && column.autoIncrement) {
93+
args = { autoIncrement: true }
94+
}
95+
96+
columns[key].primaryKey(args)
9197
}
9298

9399
if (column.notNull) {

packages/db-sqlite/src/schema/setColumnID.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { SetColumnID } from '@payloadcms/drizzle/types'
22

3+
import type { SQLiteAdapter } from '../types.js'
4+
35
export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => {
46
const idField = fields.find((field) => field.name === 'id')
57
if (idField) {
@@ -36,6 +38,7 @@ export const setColumnID: SetColumnID = ({ adapter, columns, fields }) => {
3638
columns.id = {
3739
name: 'id',
3840
type: 'integer',
41+
autoIncrement: (adapter as unknown as SQLiteAdapter).autoIncrement,
3942
primaryKey: true,
4043
}
4144

packages/db-sqlite/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ export type Args = {
3131
* Examples may include: composite indices, generated columns, vectors
3232
*/
3333
afterSchemaInit?: SQLiteSchemaHook[]
34+
/**
35+
* Enable [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for Primary Keys.
36+
* This ensures that the same ID cannot be reused from previously deleted rows.
37+
*/
38+
autoIncrement?: boolean
3439
/**
3540
* Transform the schema before it's built.
3641
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
@@ -124,6 +129,7 @@ type Drizzle = { $client: Client } & LibSQLDatabase<ResolveSchemaType<GeneratedD
124129

125130
export type SQLiteAdapter = {
126131
afterSchemaInit: SQLiteSchemaHook[]
132+
autoIncrement: boolean
127133
beforeSchemaInit: SQLiteSchemaHook[]
128134
client: Client
129135
clientConfig: Args['client']

packages/drizzle/src/types.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,21 @@ export type EnumRawColumn = (
270270
) &
271271
BaseRawColumn
272272

273+
export type IntegerRawColumn = {
274+
/**
275+
* SQLite only.
276+
* Enable [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary key to ensure that the same ID cannot be reused from previously deleted rows.
277+
*/
278+
autoIncrement?: boolean
279+
type: 'integer'
280+
} & BaseRawColumn
281+
273282
export type RawColumn =
274283
| ({
275-
type: 'boolean' | 'geometry' | 'integer' | 'jsonb' | 'numeric' | 'serial' | 'text' | 'varchar'
284+
type: 'boolean' | 'geometry' | 'jsonb' | 'numeric' | 'serial' | 'text' | 'varchar'
276285
} & BaseRawColumn)
277286
| EnumRawColumn
287+
| IntegerRawColumn
278288
| TimestampRawColumn
279289
| UUIDRawColumn
280290

test/database/int.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,4 +1174,11 @@ describe('database', () => {
11741174
// Should stay the same ID
11751175
expect(postShouldCreated.id).toBe(postShouldUpdated.id)
11761176
})
1177+
1178+
it('should enforce unique ids on db level even after delete', async () => {
1179+
const { id } = await payload.create({ collection: 'posts', data: { title: 'ASD' } })
1180+
await payload.delete({ id, collection: 'posts' })
1181+
const { id: id_2 } = await payload.create({ collection: 'posts', data: { title: 'ASD' } })
1182+
expect(id_2).not.toBe(id)
1183+
})
11771184
})

test/database/payload-generated-schema.ts

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { sql, relations } from '@payloadcms/db-sqlite/drizzle'
2020
export const posts = sqliteTable(
2121
'posts',
2222
{
23-
id: integer('id').primaryKey(),
23+
id: integer('id').primaryKey({ autoIncrement: true }),
2424
title: text('title').notNull(),
2525
hasTransaction: integer('has_transaction', { mode: 'boolean' }),
2626
throwAfterChange: integer('throw_after_change', { mode: 'boolean' }).default(false),
@@ -59,12 +59,12 @@ export const default_values_array = sqliteTable(
5959
export const default_values = sqliteTable(
6060
'default_values',
6161
{
62-
id: integer('id').primaryKey(),
62+
id: integer('id').primaryKey({ autoIncrement: true }),
6363
title: text('title'),
6464
defaultValue: text('default_value').default('default value from database'),
6565
group_defaultValue: text('group_default_value').default('default value from database'),
6666
select: text('select', { enum: ['option0', 'option1', 'default'] }).default('default'),
67-
point: text('point', { mode: 'json' }).default('"SRID=4326;POINT(10 20)"'),
67+
point: text('point', { mode: 'json' }).default('[10,20]'),
6868
updatedAt: text('updated_at')
6969
.notNull()
7070
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
@@ -81,7 +81,7 @@ export const default_values = sqliteTable(
8181
export const relation_a = sqliteTable(
8282
'relation_a',
8383
{
84-
id: integer('id').primaryKey(),
84+
id: integer('id').primaryKey({ autoIncrement: true }),
8585
title: text('title'),
8686
richText: text('rich_text', { mode: 'json' }),
8787
updatedAt: text('updated_at')
@@ -100,7 +100,7 @@ export const relation_a = sqliteTable(
100100
export const relation_b = sqliteTable(
101101
'relation_b',
102102
{
103-
id: integer('id').primaryKey(),
103+
id: integer('id').primaryKey({ autoIncrement: true }),
104104
title: text('title'),
105105
relationship: integer('relationship_id').references(() => relation_a.id, {
106106
onDelete: 'set null',
@@ -243,7 +243,7 @@ export const pg_migrations_blocks_my_block_locales = sqliteTable(
243243
export const pg_migrations = sqliteTable(
244244
'pg_migrations',
245245
{
246-
id: integer('id').primaryKey(),
246+
id: integer('id').primaryKey({ autoIncrement: true }),
247247
relation1: integer('relation1_id').references(() => relation_a.id, {
248248
onDelete: 'set null',
249249
}),
@@ -292,7 +292,7 @@ export const _pg_migrations_v_version_my_array_my_sub_array = sqliteTable(
292292
{
293293
_order: integer('_order').notNull(),
294294
_parentID: integer('_parent_id').notNull(),
295-
id: integer('id').primaryKey(),
295+
id: integer('id').primaryKey({ autoIncrement: true }),
296296
_uuid: text('_uuid'),
297297
},
298298
(columns) => ({
@@ -338,7 +338,7 @@ export const _pg_migrations_v_version_my_array = sqliteTable(
338338
{
339339
_order: integer('_order').notNull(),
340340
_parentID: integer('_parent_id').notNull(),
341-
id: integer('id').primaryKey(),
341+
id: integer('id').primaryKey({ autoIncrement: true }),
342342
relation2: integer('relation2_id').references(() => relation_b.id, {
343343
onDelete: 'set null',
344344
}),
@@ -364,7 +364,7 @@ export const _pg_migrations_v_blocks_my_block = sqliteTable(
364364
_order: integer('_order').notNull(),
365365
_parentID: integer('_parent_id').notNull(),
366366
_path: text('_path').notNull(),
367-
id: integer('id').primaryKey(),
367+
id: integer('id').primaryKey({ autoIncrement: true }),
368368
relation5: integer('relation5_id').references(() => relation_a.id, {
369369
onDelete: 'set null',
370370
}),
@@ -414,7 +414,7 @@ export const _pg_migrations_v_blocks_my_block_locales = sqliteTable(
414414
export const _pg_migrations_v = sqliteTable(
415415
'_pg_migrations_v',
416416
{
417-
id: integer('id').primaryKey(),
417+
id: integer('id').primaryKey({ autoIncrement: true }),
418418
parent: integer('parent_id').references(() => pg_migrations.id, {
419419
onDelete: 'set null',
420420
}),
@@ -485,7 +485,7 @@ export const customs_customSelect = sqliteTable(
485485
order: integer('order').notNull(),
486486
parent: integer('parent_id').notNull(),
487487
value: text('value', { enum: ['a', 'b', 'c'] }),
488-
id: integer('id').primaryKey(),
488+
id: integer('id').primaryKey({ autoIncrement: true }),
489489
},
490490
(columns) => ({
491491
orderIdx: index('customs_customSelect_order_idx').on(columns.order),
@@ -584,7 +584,7 @@ export const customBlocks_locales = sqliteTable(
584584
export const customs = sqliteTable(
585585
'customs',
586586
{
587-
id: integer('id').primaryKey(),
587+
id: integer('id').primaryKey({ autoIncrement: true }),
588588
text: text('text'),
589589
radio: text('radio', { enum: ['a', 'b', 'c'] }),
590590
updatedAt: text('updated_at')
@@ -658,7 +658,7 @@ export const __customs_v_version_customSelect_v = sqliteTable(
658658
order: integer('order').notNull(),
659659
parent: integer('parent_id').notNull(),
660660
value: text('value', { enum: ['a', 'b', 'c'] }),
661-
id: integer('id').primaryKey(),
661+
id: integer('id').primaryKey({ autoIncrement: true }),
662662
},
663663
(columns) => ({
664664
orderIdx: index('__customs_v_version_customSelect_v_order_idx').on(columns.order),
@@ -676,7 +676,7 @@ export const _customArrays_v = sqliteTable(
676676
{
677677
_order: integer('_order').notNull(),
678678
_parentID: integer('_parent_id').notNull(),
679-
id: integer('id').primaryKey(),
679+
id: integer('id').primaryKey({ autoIncrement: true }),
680680
text: text('text'),
681681
_uuid: text('_uuid'),
682682
},
@@ -718,7 +718,7 @@ export const _customBlocks_v = sqliteTable(
718718
_order: integer('_order').notNull(),
719719
_parentID: integer('_parent_id').notNull(),
720720
_path: text('_path').notNull(),
721-
id: integer('id').primaryKey(),
721+
id: integer('id').primaryKey({ autoIncrement: true }),
722722
text: text('text'),
723723
_uuid: text('_uuid'),
724724
blockName: text('block_name'),
@@ -759,7 +759,7 @@ export const _customBlocks_v_locales = sqliteTable(
759759
export const _customs_v = sqliteTable(
760760
'_customs_v',
761761
{
762-
id: integer('id').primaryKey(),
762+
id: integer('id').primaryKey({ autoIncrement: true }),
763763
parent: integer('parent_id').references(() => customs.id, {
764764
onDelete: 'set null',
765765
}),
@@ -856,7 +856,7 @@ export const _customs_v_rels = sqliteTable(
856856
export const places = sqliteTable(
857857
'places',
858858
{
859-
id: integer('id').primaryKey(),
859+
id: integer('id').primaryKey({ autoIncrement: true }),
860860
country: text('country'),
861861
city: text('city'),
862862
updatedAt: text('updated_at')
@@ -875,7 +875,7 @@ export const places = sqliteTable(
875875
export const fields_persistance = sqliteTable(
876876
'fields_persistance',
877877
{
878-
id: integer('id').primaryKey(),
878+
id: integer('id').primaryKey({ autoIncrement: true }),
879879
updatedAt: text('updated_at')
880880
.notNull()
881881
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
@@ -916,7 +916,7 @@ export const custom_ids = sqliteTable(
916916
export const _custom_ids_v = sqliteTable(
917917
'_custom_ids_v',
918918
{
919-
id: integer('id').primaryKey(),
919+
id: integer('id').primaryKey({ autoIncrement: true }),
920920
parent: text('parent_id').references(() => custom_ids.id, {
921921
onDelete: 'set null',
922922
}),
@@ -962,7 +962,7 @@ export const _custom_ids_v = sqliteTable(
962962
export const fake_custom_ids = sqliteTable(
963963
'fake_custom_ids',
964964
{
965-
id: integer('id').primaryKey(),
965+
id: integer('id').primaryKey({ autoIncrement: true }),
966966
title: text('title'),
967967
updatedAt: text('updated_at')
968968
.notNull()
@@ -980,7 +980,7 @@ export const fake_custom_ids = sqliteTable(
980980
export const relationships_migration = sqliteTable(
981981
'relationships_migration',
982982
{
983-
id: integer('id').primaryKey(),
983+
id: integer('id').primaryKey({ autoIncrement: true }),
984984
relationship: integer('relationship_id').references(() => default_values.id, {
985985
onDelete: 'set null',
986986
}),
@@ -1036,7 +1036,7 @@ export const relationships_migration_rels = sqliteTable(
10361036
export const _relationships_migration_v = sqliteTable(
10371037
'_relationships_migration_v',
10381038
{
1039-
id: integer('id').primaryKey(),
1039+
id: integer('id').primaryKey({ autoIncrement: true }),
10401040
parent: integer('parent_id').references(() => relationships_migration.id, {
10411041
onDelete: 'set null',
10421042
}),
@@ -1110,7 +1110,7 @@ export const _relationships_migration_v_rels = sqliteTable(
11101110
export const users = sqliteTable(
11111111
'users',
11121112
{
1113-
id: integer('id').primaryKey(),
1113+
id: integer('id').primaryKey({ autoIncrement: true }),
11141114
updatedAt: text('updated_at')
11151115
.notNull()
11161116
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
@@ -1137,7 +1137,7 @@ export const users = sqliteTable(
11371137
export const payload_locked_documents = sqliteTable(
11381138
'payload_locked_documents',
11391139
{
1140-
id: integer('id').primaryKey(),
1140+
id: integer('id').primaryKey({ autoIncrement: true }),
11411141
globalSlug: text('global_slug'),
11421142
updatedAt: text('updated_at')
11431143
.notNull()
@@ -1290,7 +1290,7 @@ export const payload_locked_documents_rels = sqliteTable(
12901290
export const payload_preferences = sqliteTable(
12911291
'payload_preferences',
12921292
{
1293-
id: integer('id').primaryKey(),
1293+
id: integer('id').primaryKey({ autoIncrement: true }),
12941294
key: text('key'),
12951295
value: text('value', { mode: 'json' }),
12961296
updatedAt: text('updated_at')
@@ -1343,7 +1343,7 @@ export const payload_preferences_rels = sqliteTable(
13431343
export const payload_migrations = sqliteTable(
13441344
'payload_migrations',
13451345
{
1346-
id: integer('id').primaryKey(),
1346+
id: integer('id').primaryKey({ autoIncrement: true }),
13471347
name: text('name'),
13481348
batch: numeric('batch'),
13491349
updatedAt: text('updated_at')
@@ -1364,7 +1364,7 @@ export const payload_migrations = sqliteTable(
13641364
)
13651365

13661366
export const customGlobal = sqliteTable('customGlobal', {
1367-
id: integer('id').primaryKey(),
1367+
id: integer('id').primaryKey({ autoIncrement: true }),
13681368
text: text('text'),
13691369
updatedAt: text('updated_at').default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
13701370
createdAt: text('created_at').default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
@@ -1373,7 +1373,7 @@ export const customGlobal = sqliteTable('customGlobal', {
13731373
export const _customGlobal_v = sqliteTable(
13741374
'_customGlobal_v',
13751375
{
1376-
id: integer('id').primaryKey(),
1376+
id: integer('id').primaryKey({ autoIncrement: true }),
13771377
version_text: text('version_text'),
13781378
version_updatedAt: text('version_updated_at').default(
13791379
sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`,

test/generateDatabaseAdapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const allDatabaseAdapters = {
5252
client: {
5353
url: process.env.SQLITE_URL || 'file:./payloadtests.db',
5454
},
55+
autoIncrement: true
5556
})`,
5657
'sqlite-uuid': `
5758
import { sqliteAdapter } from '@payloadcms/db-sqlite'

0 commit comments

Comments
 (0)