Skip to content

Commit

Permalink
db: Rework index config with generated index names (#10589)
Browse files Browse the repository at this point in the history
* feat: add indexes array config with name gen

* fix: add _idx suffix, remove name from output

* feat(test): new index config

* chore: remove unused type

* chore: changeset

* chore: add sort() for consistent names

* feat(test): consistent column ordering

* feat(test): ensure no queries when migrating legacy to new
  • Loading branch information
bholmesdev authored Mar 28, 2024
1 parent 20463a6 commit ed1031b
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 68 deletions.
31 changes: 31 additions & 0 deletions .changeset/blue-ghosts-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@astrojs/db": patch
---

Update the table indexes configuration to allow generated index names. The `indexes` object syntax is now deprecated in favor of an array.

## Migration

You can update your `indexes` configuration object to an array like so:

```diff
import { defineDb, defineTable, column } from 'astro:db';

const Comment = defineTable({
columns: {
postId: column.number(),
author: column.text(),
body: column.text(),
},
- indexes: {
- postIdIdx: { on: 'postId' },
- authorPostIdIdx: { on: ['author, postId'], unique: true },
- },
+ indexes: [
+ { on: 'postId' /* 'name' is optional */ },
+ { on: ['author, postId'], unique: true },
+ ]
})
```

This example will generate indexes with the names `Comment_postId_idx` and `Comment_author_postId_idx`, respectively. You can specify a name manually by adding the `name` attribute to a given object. This name will be **global,** so ensure index names do not conflict between tables.
24 changes: 12 additions & 12 deletions packages/db/src/core/cli/migration-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ import {
type DBColumns,
type DBConfig,
type DBSnapshot,
type DBTable,
type DBTables,
type ResolvedDBTables,
type DateColumn,
type Indexes,
type JsonColumn,
type NumberColumn,
type ResolvedDBTable,
type TextColumn,
type ResolvedIndexes,
} from '../types.js';
import { type Result, getRemoteDatabaseUrl } from '../utils.js';

Expand Down Expand Up @@ -112,8 +112,8 @@ export async function getTableChangeQueries({
newTable,
}: {
tableName: string;
oldTable: DBTable;
newTable: DBTable;
oldTable: ResolvedDBTable;
newTable: ResolvedDBTable;
}): Promise<{ queries: string[]; confirmations: string[] }> {
const queries: string[] = [];
const confirmations: string[] = [];
Expand Down Expand Up @@ -187,8 +187,8 @@ function getChangeIndexQueries({
newIndexes = {},
}: {
tableName: string;
oldIndexes?: Indexes;
newIndexes?: Indexes;
oldIndexes?: ResolvedIndexes;
newIndexes?: ResolvedIndexes;
}) {
const added = getAdded(oldIndexes, newIndexes);
const dropped = getDropped(oldIndexes, newIndexes);
Expand All @@ -206,16 +206,16 @@ function getChangeIndexQueries({
return queries;
}

function getAddedTables(oldTables: DBSnapshot, newTables: DBSnapshot): DBTables {
const added: DBTables = {};
function getAddedTables(oldTables: DBSnapshot, newTables: DBSnapshot): ResolvedDBTables {
const added: ResolvedDBTables = {};
for (const [key, newTable] of Object.entries(newTables.schema)) {
if (!(key in oldTables.schema)) added[key] = newTable;
}
return added;
}

function getDroppedTables(oldTables: DBSnapshot, newTables: DBSnapshot): DBTables {
const dropped: DBTables = {};
function getDroppedTables(oldTables: DBSnapshot, newTables: DBSnapshot): ResolvedDBTables {
const dropped: ResolvedDBTables = {};
for (const [key, oldTable] of Object.entries(oldTables.schema)) {
if (!(key in newTables.schema)) dropped[key] = oldTable;
}
Expand Down Expand Up @@ -261,7 +261,7 @@ function getRecreateTableQueries({
migrateHiddenPrimaryKey,
}: {
tableName: string;
newTable: DBTable;
newTable: ResolvedDBTable;
added: Record<string, DBColumn>;
hasDataLoss: boolean;
migrateHiddenPrimaryKey: boolean;
Expand Down
57 changes: 48 additions & 9 deletions packages/db/src/core/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type ZodTypeDef, z } from 'zod';
import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js';
import { errorMap } from './integration/error-map.js';
import type { NumberColumn, TextColumn } from './types.js';
import { mapObject } from './utils.js';

export type MaybeArray<T> = T | T[];

Expand Down Expand Up @@ -156,11 +157,6 @@ export const referenceableColumnSchema = z.union([textColumnSchema, numberColumn

export const columnsSchema = z.record(columnSchema);

export const indexSchema = z.object({
on: z.string().or(z.array(z.string())),
unique: z.boolean().optional(),
});

type ForeignKeysInput = {
columns: MaybeArray<string>;
references: () => MaybeArray<Omit<z.input<typeof referenceableColumnSchema>, 'references'>>;
Expand All @@ -179,9 +175,23 @@ const foreignKeysSchema: z.ZodType<ForeignKeysOutput, ZodTypeDef, ForeignKeysInp
.transform((fn) => fn()),
});

export const resolvedIndexSchema = z.object({
on: z.string().or(z.array(z.string())),
unique: z.boolean().optional(),
});
/** @deprecated */
const legacyIndexesSchema = z.record(resolvedIndexSchema);

export const indexSchema = z.object({
on: z.string().or(z.array(z.string())),
unique: z.boolean().optional(),
name: z.string().optional(),
});
const indexesSchema = z.array(indexSchema);

export const tableSchema = z.object({
columns: columnsSchema,
indexes: z.record(indexSchema).optional(),
indexes: indexesSchema.or(legacyIndexesSchema).optional(),
foreignKeys: z.array(foreignKeysSchema).optional(),
deprecated: z.boolean().optional().default(false),
});
Expand All @@ -192,6 +202,7 @@ export const tablesSchema = z.preprocess((rawTables) => {
for (const [tableName, table] of Object.entries(tables)) {
// Append table and column names to columns.
// Used to track table info for references.
table.getName = () => tableName;
const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap });
for (const [columnName, column] of Object.entries(columns)) {
column.schema.name = columnName;
Expand All @@ -201,6 +212,34 @@ export const tablesSchema = z.preprocess((rawTables) => {
return rawTables;
}, z.record(tableSchema));

export const dbConfigSchema = z.object({
tables: tablesSchema.optional(),
});
export const dbConfigSchema = z
.object({
tables: tablesSchema.optional(),
})
.transform(({ tables = {}, ...config }) => {
return {
...config,
tables: mapObject(tables, (tableName, table) => {
const { indexes = {} } = table;
if (!Array.isArray(indexes)) {
return { ...table, indexes };
}
const resolvedIndexes: Record<string, z.infer<typeof resolvedIndexSchema>> = {};
for (const index of indexes) {
if (index.name) {
const { name, ...rest } = index;
resolvedIndexes[index.name] = rest;
continue;
}
// Sort index columns to ensure consistent index names
const indexOn = Array.isArray(index.on) ? index.on.sort().join('_') : index.on;
const name = tableName + '_' + indexOn + '_idx';
resolvedIndexes[name] = index;
}
return {
...table,
indexes: resolvedIndexes,
};
}),
};
});
16 changes: 12 additions & 4 deletions packages/db/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
dateColumnSchema,
dbConfigSchema,
indexSchema,
resolvedIndexSchema,
jsonColumnSchema,
numberColumnOptsSchema,
numberColumnSchema,
Expand All @@ -17,8 +18,7 @@ import type {
textColumnSchema,
} from './schemas.js';

export type Indexes = Record<string, z.infer<typeof indexSchema>>;

export type ResolvedIndexes = z.output<typeof dbConfigSchema>['tables'][string]['indexes'];
export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
export type BooleanColumnInput = z.input<typeof booleanColumnSchema>;
export type NumberColumn = z.infer<typeof numberColumnSchema>;
Expand Down Expand Up @@ -47,8 +47,10 @@ export type DBColumnInput =
export type DBColumns = z.infer<typeof columnsSchema>;
export type DBTable = z.infer<typeof tableSchema>;
export type DBTables = Record<string, DBTable>;
export type ResolvedDBTables = z.output<typeof dbConfigSchema>['tables'];
export type ResolvedDBTable = z.output<typeof dbConfigSchema>['tables'][string];
export type DBSnapshot = {
schema: Record<string, DBTable>;
schema: Record<string, ResolvedDBTable>;
version: string;
};

Expand All @@ -67,14 +69,20 @@ export interface TableConfig<TColumns extends ColumnsConfig = ColumnsConfig>
columns: MaybeArray<Extract<keyof TColumns, string>>;
references: () => MaybeArray<z.input<typeof referenceableColumnSchema>>;
}>;
indexes?: Record<string, IndexConfig<TColumns>>;
indexes?: Array<IndexConfig<TColumns>> | Record<string, LegacyIndexConfig<TColumns>>;
deprecated?: boolean;
}

interface IndexConfig<TColumns extends ColumnsConfig> extends z.input<typeof indexSchema> {
on: MaybeArray<Extract<keyof TColumns, string>>;
}

/** @deprecated */
interface LegacyIndexConfig<TColumns extends ColumnsConfig>
extends z.input<typeof resolvedIndexSchema> {
on: MaybeArray<Extract<keyof TColumns, string>>;
}

// We cannot use `Omit<NumberColumn | TextColumn, 'type'>`,
// since Omit collapses our union type on primary key.
export type NumberColumnOpts = z.input<typeof numberColumnOptsSchema>;
Expand Down
13 changes: 13 additions & 0 deletions packages/db/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,16 @@ export function defineDbIntegration(integration: AstroDbIntegration): AstroInteg
}

export type Result<T> = { success: true; data: T } | { success: false; data: unknown };

/**
* Map an object's values to a new set of values
* while preserving types.
*/
export function mapObject<T, U = T>(
item: Record<string, T>,
callback: (key: string, value: T) => U
): Record<string, U> {
return Object.fromEntries(
Object.entries(item).map(([key, value]) => [key, callback(key, value)])
);
}
Loading

0 comments on commit ed1031b

Please sign in to comment.