Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8ab6ff1

Browse filesBrowse files
authored
feat: add optional database indexes (#3637)
1 parent aac1f38 commit 8ab6ff1
Copy full SHA for 8ab6ff1

4 files changed

+335Lines changed: 335 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎docs/content/docs/2.collections/1.define.md‎

Copy file name to clipboardExpand all lines: docs/content/docs/2.collections/1.define.md
+70Lines changed: 70 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,65 @@ export default defineContentConfig({
8686
You can define as many collections as you want to organize different types of content.
8787
::
8888

89+
### Database Indexes
90+
91+
Optimize query performance by defining indexes on collection columns. Indexes are especially useful for fields used in filtering, sorting, or lookups.
92+
93+
```ts [content.config.ts]
94+
import { defineCollection, defineContentConfig } from '@nuxt/content'
95+
import { z } from 'zod'
96+
97+
export default defineContentConfig({
98+
collections: {
99+
products: defineCollection({
100+
type: 'data',
101+
source: 'products/*.json',
102+
schema: z.object({
103+
sku: z.string(),
104+
price: z.number(),
105+
category: z.string(),
106+
inStock: z.boolean(),
107+
}),
108+
indexes: [
109+
// Single column indexes
110+
{ columns: ['category'] },
111+
{ columns: ['price'] },
112+
113+
// Composite index for category + price filtering
114+
{ columns: ['category', 'price'] },
115+
116+
// Unique index to ensure SKU uniqueness
117+
{ columns: ['sku'], unique: true },
118+
119+
// Custom index name (optional)
120+
{ columns: ['inStock', 'category'], name: 'idx_stock_category' },
121+
],
122+
}),
123+
},
124+
})
125+
```
126+
127+
::note
128+
Indexes are created automatically when the database schema is generated. They work across all supported databases: SQLite, Cloudflare D1, PostgreSQL, LibSQL, and PGlite.
129+
::
130+
131+
::tip{icon="i-ph-lightbulb"}
132+
**Cloudflare D1 Cost Optimization**: With indexes, a `WHERE` clause on an indexed column counts as only 1 row read when there's a single match. Without an index, D1 counts all rows scanned in the table, significantly increasing your read costs. Indexes can dramatically reduce your D1 billing.
133+
::
134+
135+
**Index Configuration Options:**
136+
137+
- **`columns`** (required): Array of column names to include in the index
138+
- **`unique`** (optional): Set to `true` to create a unique index (default: `false`)
139+
- **`name`** (optional): Custom index name. If omitted, auto-generates as `idx_{collection}_{column1}_{column2}`
140+
141+
**Performance Tips:**
142+
143+
- Index columns used in `where()` queries for faster filtering
144+
- Index columns used in `sort()` for optimized sorting
145+
- Use composite indexes for queries that filter/sort by multiple columns
146+
- Unique indexes automatically enforce data uniqueness constraints
147+
89148
## Querying Collections
90149

91150
Use the [`queryCollection`](/docs/utils/query-collection) util to fetch one or all items from a collection:
@@ -125,6 +184,17 @@ type Collection = {
125184
source?: string | CollectionSource
126185
// Zod schema for content validation and typing
127186
schema?: ZodObject<T>
187+
// Database indexes for query optimization
188+
indexes?: CollectionIndex[]
189+
}
190+
191+
type CollectionIndex = {
192+
// Column names to include in the index
193+
columns: string[]
194+
// Optional custom index name
195+
name?: string
196+
// Whether this is a unique index (default: false)
197+
unique?: boolean
128198
}
129199
```
130200
Collapse file

‎src/types/collection.ts‎

Copy file name to clipboardExpand all lines: src/types/collection.ts
+25Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ export interface Collections {}
77

88
export type CollectionType = 'page' | 'data'
99

10+
/**
11+
* Defines an index on collection columns for optimizing database queries
12+
*/
13+
export interface CollectionIndex {
14+
/**
15+
* Column names to include in the index
16+
*/
17+
columns: string[]
18+
19+
/**
20+
* Optional custom index name
21+
* If not provided, will auto-generate: idx_{collection}_{columns.join('_')}
22+
*/
23+
name?: string
24+
25+
/**
26+
* Whether this is a unique index
27+
* @default false
28+
*/
29+
unique?: boolean
30+
}
31+
1032
export type CollectionSource = {
1133
include: string
1234
prefix?: string
@@ -42,12 +64,14 @@ export interface PageCollection<T> {
4264
type: 'page'
4365
source?: string | CollectionSource | CollectionSource[] | ResolvedCustomCollectionSource
4466
schema?: ContentStandardSchemaV1<T>
67+
indexes?: CollectionIndex[]
4568
}
4669

4770
export interface DataCollection<T> {
4871
type: 'data'
4972
source?: string | CollectionSource | CollectionSource[] | ResolvedCustomCollectionSource
5073
schema: ContentStandardSchemaV1<T>
74+
indexes?: CollectionIndex[]
5175
}
5276

5377
export type Collection<T> = PageCollection<T> | DataCollection<T>
@@ -58,6 +82,7 @@ export interface DefinedCollection {
5882
schema: Draft07
5983
extendedSchema: Draft07
6084
fields: Record<string, 'string' | 'number' | 'boolean' | 'date' | 'json'>
85+
indexes?: CollectionIndex[]
6186
}
6287

6388
export interface ResolvedCollection extends DefinedCollection {
Collapse file

‎src/utils/collection.ts‎

Copy file name to clipboardExpand all lines: src/utils/collection.ts
+65Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function defineCollection<T>(collection: Collection<T>): DefinedCollectio
3434
schema: standardSchema,
3535
extendedSchema: extendedSchema,
3636
fields: getCollectionFieldsTypes(extendedSchema),
37+
indexes: collection.indexes,
3738
}
3839
}
3940

@@ -235,6 +236,64 @@ export function generateCollectionInsert(collection: ResolvedCollection, data: P
235236
}
236237
}
237238

239+
/**
240+
* Generate a safe index name following SQL naming conventions
241+
* Ensures name is within database limits (PostgreSQL 63 char limit)
242+
*/
243+
function generateIndexName(collectionName: string, columns: string[]): string {
244+
const base = `idx_${collectionName}_${columns.join('_')}`
245+
246+
// Limit to 63 characters (PostgreSQL limit, most restrictive common limit)
247+
// SQLite allows 1024, D1 follows SQLite
248+
if (base.length > 63) {
249+
// Truncate and add hash to ensure uniqueness
250+
const hashSuffix = hash(base).slice(0, 8)
251+
return base.slice(0, 54) + '_' + hashSuffix
252+
}
253+
254+
return base
255+
}
256+
257+
/**
258+
* Generate CREATE INDEX statements for a collection
259+
* Returns array of SQL statements (one per index)
260+
*/
261+
export function generateCollectionIndexStatements(collection: ResolvedCollection): string[] {
262+
if (!collection.indexes || collection.indexes.length === 0) {
263+
return []
264+
}
265+
266+
const statements: string[] = []
267+
268+
for (const index of collection.indexes) {
269+
// Validate columns exist in schema
270+
const invalidColumns = index.columns.filter(
271+
column => !collection.fields[column] && column !== 'id',
272+
)
273+
274+
if (invalidColumns.length > 0) {
275+
logger.warn(
276+
`Index references non-existent column(s) "${invalidColumns.join(', ')}" in collection "${collection.name}". Skipping this index.`,
277+
)
278+
continue
279+
}
280+
281+
// Generate index name
282+
const indexName = index.name || generateIndexName(collection.name, index.columns)
283+
284+
// Quote column names for SQL safety
285+
const quotedColumns = index.columns.map(col => `"${col}"`).join(', ')
286+
287+
// Build CREATE INDEX statement
288+
const uniqueKeyword = index.unique ? 'UNIQUE ' : ''
289+
const statement = `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS ${indexName} ON ${collection.tableName} (${quotedColumns});`
290+
291+
statements.push(statement)
292+
}
293+
294+
return statements
295+
}
296+
238297
// Convert a collection with Zod schema to SQL table definition
239298
export function generateCollectionTableDefinition(collection: ResolvedCollection, opts: { drop?: boolean } = {}) {
240299
const sortedKeys = getOrderedSchemaKeys(collection.extendedSchema)
@@ -281,6 +340,12 @@ export function generateCollectionTableDefinition(collection: ResolvedCollection
281340
definition = `DROP TABLE IF EXISTS ${collection.tableName};\n${definition}`
282341
}
283342

343+
// Add index statements
344+
const indexStatements = generateCollectionIndexStatements(collection)
345+
if (indexStatements.length > 0) {
346+
definition += '\n' + indexStatements.join('\n')
347+
}
348+
284349
return definition
285350
}
286351

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.