0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-01-13 22:11:20 -05:00

Rename field->column

This commit is contained in:
Matthew Phillips 2024-02-20 15:29:17 -05:00
parent 841e24ebb9
commit 9693d19801
18 changed files with 479 additions and 479 deletions

View file

@ -1,19 +1,19 @@
import * as color from 'kleur/colors'; import * as color from 'kleur/colors';
import deepDiff from 'deep-diff'; import deepDiff from 'deep-diff';
import { import {
fieldSchema, columnSchema,
type BooleanField, type BooleanColumn,
type DBCollection, type DBCollection,
type DBCollections, type DBCollections,
type DBField, type DBColumn,
type DBFields, type DBColumns,
type DBSnapshot, type DBSnapshot,
type DateField, type DateColumn,
type FieldType, type ColumnType,
type Indexes, type Indexes,
type JsonField, type JsonColumn,
type NumberField, type NumberColumn,
type TextField, type TextColumn,
} from '../types.js'; } from '../types.js';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
@ -35,7 +35,7 @@ const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10);
/** Dependency injected for unit testing */ /** Dependency injected for unit testing */
type AmbiguityResponses = { type AmbiguityResponses = {
collectionRenames: Record<string, string>; collectionRenames: Record<string, string>;
fieldRenames: { columnRenames: {
[collectionName: string]: Record<string, string>; [collectionName: string]: Record<string, string>;
}; };
}; };
@ -102,9 +102,9 @@ export async function getCollectionChangeQueries({
}): Promise<{ queries: string[]; confirmations: string[] }> { }): Promise<{ queries: string[]; confirmations: string[] }> {
const queries: string[] = []; const queries: string[] = [];
const confirmations: string[] = []; const confirmations: string[] = [];
const updated = getUpdatedFields(oldCollection.fields, newCollection.fields); const updated = getUpdatedColumns(oldCollection.columns, newCollection.columns);
let added = getAdded(oldCollection.fields, newCollection.fields); let added = getAdded(oldCollection.columns, newCollection.columns);
let dropped = getDropped(oldCollection.fields, newCollection.fields); let dropped = getDropped(oldCollection.columns, newCollection.columns);
/** Any foreign key changes require a full table recreate */ /** Any foreign key changes require a full table recreate */
const hasForeignKeyChanges = Boolean( const hasForeignKeyChanges = Boolean(
deepDiff(oldCollection.foreignKeys, newCollection.foreignKeys) deepDiff(oldCollection.foreignKeys, newCollection.foreignKeys)
@ -121,10 +121,10 @@ export async function getCollectionChangeQueries({
}; };
} }
if (!hasForeignKeyChanges && !isEmpty(added) && !isEmpty(dropped)) { if (!hasForeignKeyChanges && !isEmpty(added) && !isEmpty(dropped)) {
const resolved = await resolveFieldRenames(collectionName, added, dropped, ambiguityResponses); const resolved = await resolveColumnRenames(collectionName, added, dropped, ambiguityResponses);
added = resolved.added; added = resolved.added;
dropped = resolved.dropped; dropped = resolved.dropped;
queries.push(...getFieldRenameQueries(collectionName, resolved.renamed)); queries.push(...getColumnRenameQueries(collectionName, resolved.renamed));
} }
if ( if (
!hasForeignKeyChanges && !hasForeignKeyChanges &&
@ -145,31 +145,31 @@ export async function getCollectionChangeQueries({
const dataLossCheck = canRecreateTableWithoutDataLoss(added, updated); const dataLossCheck = canRecreateTableWithoutDataLoss(added, updated);
if (dataLossCheck.dataLoss) { if (dataLossCheck.dataLoss) {
const { reason, fieldName } = dataLossCheck; const { reason, columnName } = dataLossCheck;
const reasonMsgs: Record<DataLossReason, string> = { const reasonMsgs: Record<DataLossReason, string> = {
'added-required': `New field ${color.bold( 'added-required': `New column ${color.bold(
collectionName + '.' + fieldName collectionName + '.' + columnName
)} is required with no default value.\nThis requires deleting existing data in the ${color.bold( )} is required with no default value.\nThis requires deleting existing data in the ${color.bold(
collectionName collectionName
)} collection.`, )} collection.`,
'added-unique': `New field ${color.bold( 'added-unique': `New column ${color.bold(
collectionName + '.' + fieldName collectionName + '.' + columnName
)} is marked as unique.\nThis requires deleting existing data in the ${color.bold( )} is marked as unique.\nThis requires deleting existing data in the ${color.bold(
collectionName collectionName
)} collection.`, )} collection.`,
'updated-type': `Updated field ${color.bold( 'updated-type': `Updated column ${color.bold(
collectionName + '.' + fieldName collectionName + '.' + columnName
)} cannot convert data to new field data type.\nThis requires deleting existing data in the ${color.bold( )} cannot convert data to new column data type.\nThis requires deleting existing data in the ${color.bold(
collectionName collectionName
)} collection.`, )} collection.`,
}; };
confirmations.push(reasonMsgs[reason]); confirmations.push(reasonMsgs[reason]);
} }
const primaryKeyExists = Object.entries(newCollection.fields).find(([, field]) => const primaryKeyExists = Object.entries(newCollection.columns).find(([, column]) =>
hasPrimaryKey(field) hasPrimaryKey(column)
); );
const droppedPrimaryKey = Object.entries(dropped).find(([, field]) => hasPrimaryKey(field)); const droppedPrimaryKey = Object.entries(dropped).find(([, column]) => hasPrimaryKey(column));
const recreateTableQueries = getRecreateTableQueries({ const recreateTableQueries = getRecreateTableQueries({
collectionName, collectionName,
@ -209,31 +209,31 @@ function getChangeIndexQueries({
type Renamed = Array<{ from: string; to: string }>; type Renamed = Array<{ from: string; to: string }>;
async function resolveFieldRenames( async function resolveColumnRenames(
collectionName: string, collectionName: string,
mightAdd: DBFields, mightAdd: DBColumns,
mightDrop: DBFields, mightDrop: DBColumns,
ambiguityResponses?: AmbiguityResponses ambiguityResponses?: AmbiguityResponses
): Promise<{ added: DBFields; dropped: DBFields; renamed: Renamed }> { ): Promise<{ added: DBColumns; dropped: DBColumns; renamed: Renamed }> {
const added: DBFields = {}; const added: DBColumns = {};
const dropped: DBFields = {}; const dropped: DBColumns = {};
const renamed: Renamed = []; const renamed: Renamed = [];
for (const [fieldName, field] of Object.entries(mightAdd)) { for (const [columnName, column] of Object.entries(mightAdd)) {
let oldFieldName = ambiguityResponses let oldColumnName = ambiguityResponses
? ambiguityResponses.fieldRenames[collectionName]?.[fieldName] ?? '__NEW__' ? ambiguityResponses.columnRenames[collectionName]?.[columnName] ?? '__NEW__'
: undefined; : undefined;
if (!oldFieldName) { if (!oldColumnName) {
const res = await prompts( const res = await prompts(
{ {
type: 'select', type: 'select',
name: 'fieldName', name: 'columnName',
message: message:
'New field ' + 'New column ' +
color.blue(color.bold(`${collectionName}.${fieldName}`)) + color.blue(color.bold(`${collectionName}.${columnName}`)) +
' detected. Was this renamed from an existing field?', ' detected. Was this renamed from an existing column?',
choices: [ choices: [
{ title: 'New field (not renamed from existing)', value: '__NEW__' }, { title: 'New column (not renamed from existing)', value: '__NEW__' },
...Object.keys(mightDrop) ...Object.keys(mightDrop)
.filter((key) => !(key in renamed)) .filter((key) => !(key in renamed))
.map((key) => ({ title: key, value: key })), .map((key) => ({ title: key, value: key })),
@ -245,18 +245,18 @@ async function resolveFieldRenames(
}, },
} }
); );
oldFieldName = res.fieldName as string; oldColumnName = res.columnName as string;
} }
if (oldFieldName === '__NEW__') { if (oldColumnName === '__NEW__') {
added[fieldName] = field; added[columnName] = column;
} else { } else {
renamed.push({ from: oldFieldName, to: fieldName }); renamed.push({ from: oldColumnName, to: columnName });
} }
} }
for (const [droppedFieldName, droppedField] of Object.entries(mightDrop)) { for (const [droppedColumnName, droppedColumn] of Object.entries(mightDrop)) {
if (!renamed.find((r) => r.from === droppedFieldName)) { if (!renamed.find((r) => r.from === droppedColumnName)) {
dropped[droppedFieldName] = droppedField; dropped[droppedColumnName] = droppedColumn;
} }
} }
@ -339,7 +339,7 @@ function getDroppedCollections(
return dropped; return dropped;
} }
function getFieldRenameQueries(unescapedCollectionName: string, renamed: Renamed): string[] { function getColumnRenameQueries(unescapedCollectionName: string, renamed: Renamed): string[] {
const queries: string[] = []; const queries: string[] = [];
const collectionName = sqlite.escapeName(unescapedCollectionName); const collectionName = sqlite.escapeName(unescapedCollectionName);
@ -359,25 +359,25 @@ function getFieldRenameQueries(unescapedCollectionName: string, renamed: Renamed
*/ */
function getAlterTableQueries( function getAlterTableQueries(
unescapedCollectionName: string, unescapedCollectionName: string,
added: DBFields, added: DBColumns,
dropped: DBFields dropped: DBColumns
): string[] { ): string[] {
const queries: string[] = []; const queries: string[] = [];
const collectionName = sqlite.escapeName(unescapedCollectionName); const collectionName = sqlite.escapeName(unescapedCollectionName);
for (const [unescFieldName, field] of Object.entries(added)) { for (const [unescColumnName, column] of Object.entries(added)) {
const fieldName = sqlite.escapeName(unescFieldName); const columnName = sqlite.escapeName(unescColumnName);
const type = schemaTypeToSqlType(field.type); const type = schemaTypeToSqlType(column.type);
const q = `ALTER TABLE ${collectionName} ADD COLUMN ${fieldName} ${type}${getModifiers( const q = `ALTER TABLE ${collectionName} ADD COLUMN ${columnName} ${type}${getModifiers(
fieldName, columnName,
field column
)}`; )}`;
queries.push(q); queries.push(q);
} }
for (const unescFieldName of Object.keys(dropped)) { for (const unescColumnName of Object.keys(dropped)) {
const fieldName = sqlite.escapeName(unescFieldName); const columnName = sqlite.escapeName(unescColumnName);
const q = `ALTER TABLE ${collectionName} DROP COLUMN ${fieldName}`; const q = `ALTER TABLE ${collectionName} DROP COLUMN ${columnName}`;
queries.push(q); queries.push(q);
} }
@ -393,7 +393,7 @@ function getRecreateTableQueries({
}: { }: {
collectionName: string; collectionName: string;
newCollection: DBCollection; newCollection: DBCollection;
added: Record<string, DBField>; added: Record<string, DBColumn>;
hasDataLoss: boolean; hasDataLoss: boolean;
migrateHiddenPrimaryKey: boolean; migrateHiddenPrimaryKey: boolean;
}): string[] { }): string[] {
@ -407,7 +407,7 @@ function getRecreateTableQueries({
getCreateTableQuery(unescCollectionName, newCollection), getCreateTableQuery(unescCollectionName, newCollection),
]; ];
} }
const newColumns = [...Object.keys(newCollection.fields)]; const newColumns = [...Object.keys(newCollection.columns)];
if (migrateHiddenPrimaryKey) { if (migrateHiddenPrimaryKey) {
newColumns.unshift('_id'); newColumns.unshift('_id');
} }
@ -434,44 +434,44 @@ function isEmpty(obj: Record<string, unknown>) {
* *
* @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column * @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column
*/ */
function canAlterTableAddColumn(field: DBField) { function canAlterTableAddColumn(column: DBColumn) {
if (field.schema.unique) return false; if (column.schema.unique) return false;
if (hasRuntimeDefault(field)) return false; if (hasRuntimeDefault(column)) return false;
if (!field.schema.optional && !hasDefault(field)) return false; if (!column.schema.optional && !hasDefault(column)) return false;
if (hasPrimaryKey(field)) return false; if (hasPrimaryKey(column)) return false;
if (getReferencesConfig(field)) return false; if (getReferencesConfig(column)) return false;
return true; return true;
} }
function canAlterTableDropColumn(field: DBField) { function canAlterTableDropColumn(column: DBColumn) {
if (field.schema.unique) return false; if (column.schema.unique) return false;
if (hasPrimaryKey(field)) return false; if (hasPrimaryKey(column)) return false;
return true; return true;
} }
type DataLossReason = 'added-required' | 'added-unique' | 'updated-type'; type DataLossReason = 'added-required' | 'added-unique' | 'updated-type';
type DataLossResponse = type DataLossResponse =
| { dataLoss: false } | { dataLoss: false }
| { dataLoss: true; fieldName: string; reason: DataLossReason }; | { dataLoss: true; columnName: string; reason: DataLossReason };
function canRecreateTableWithoutDataLoss( function canRecreateTableWithoutDataLoss(
added: DBFields, added: DBColumns,
updated: UpdatedFields updated: UpdatedColumns
): DataLossResponse { ): DataLossResponse {
for (const [fieldName, a] of Object.entries(added)) { for (const [columnName, a] of Object.entries(added)) {
if (hasPrimaryKey(a) && a.type !== 'number' && !hasDefault(a)) { if (hasPrimaryKey(a) && a.type !== 'number' && !hasDefault(a)) {
return { dataLoss: true, fieldName, reason: 'added-required' }; return { dataLoss: true, columnName, reason: 'added-required' };
} }
if (!a.schema.optional && !hasDefault(a)) { if (!a.schema.optional && !hasDefault(a)) {
return { dataLoss: true, fieldName, reason: 'added-required' }; return { dataLoss: true, columnName, reason: 'added-required' };
} }
if (!a.schema.optional && a.schema.unique) { if (!a.schema.optional && a.schema.unique) {
return { dataLoss: true, fieldName, reason: 'added-unique' }; return { dataLoss: true, columnName, reason: 'added-unique' };
} }
} }
for (const [fieldName, u] of Object.entries(updated)) { for (const [columnName, u] of Object.entries(updated)) {
if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) { if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) {
return { dataLoss: true, fieldName, reason: 'updated-type' }; return { dataLoss: true, columnName, reason: 'updated-type' };
} }
} }
return { dataLoss: false }; return { dataLoss: false };
@ -503,58 +503,58 @@ function getUpdated<T>(oldObj: Record<string, T>, newObj: Record<string, T>) {
return updated; return updated;
} }
type UpdatedFields = Record<string, { old: DBField; new: DBField }>; type UpdatedColumns = Record<string, { old: DBColumn; new: DBColumn }>;
function getUpdatedFields(oldFields: DBFields, newFields: DBFields): UpdatedFields { function getUpdatedColumns(oldColumns: DBColumns, newColumns: DBColumns): UpdatedColumns {
const updated: UpdatedFields = {}; const updated: UpdatedColumns = {};
for (const [key, newField] of Object.entries(newFields)) { for (const [key, newColumn] of Object.entries(newColumns)) {
let oldField = oldFields[key]; let oldColumn = oldColumns[key];
if (!oldField) continue; if (!oldColumn) continue;
if (oldField.type !== newField.type && canChangeTypeWithoutQuery(oldField, newField)) { if (oldColumn.type !== newColumn.type && canChangeTypeWithoutQuery(oldColumn, newColumn)) {
// If we can safely change the type without a query, // If we can safely change the type without a query,
// try parsing the old schema as the new schema. // try parsing the old schema as the new schema.
// This lets us diff the fields as if they were the same type. // This lets us diff the columns as if they were the same type.
const asNewField = fieldSchema.safeParse({ const asNewColumn = columnSchema.safeParse({
type: newField.type, type: newColumn.type,
schema: oldField.schema, schema: oldColumn.schema,
}); });
if (asNewField.success) { if (asNewColumn.success) {
oldField = asNewField.data; oldColumn = asNewColumn.data;
} }
// If parsing fails, move on to the standard diff. // If parsing fails, move on to the standard diff.
} }
const diff = deepDiff(oldField, newField); const diff = deepDiff(oldColumn, newColumn);
if (diff) { if (diff) {
updated[key] = { old: oldField, new: newField }; updated[key] = { old: oldColumn, new: newColumn };
} }
} }
return updated; return updated;
} }
const typeChangesWithoutQuery: Array<{ from: FieldType; to: FieldType }> = [ const typeChangesWithoutQuery: Array<{ from: ColumnType; to: ColumnType }> = [
{ from: 'boolean', to: 'number' }, { from: 'boolean', to: 'number' },
{ from: 'date', to: 'text' }, { from: 'date', to: 'text' },
{ from: 'json', to: 'text' }, { from: 'json', to: 'text' },
]; ];
function canChangeTypeWithoutQuery(oldField: DBField, newField: DBField) { function canChangeTypeWithoutQuery(oldColumn: DBColumn, newColumn: DBColumn) {
return typeChangesWithoutQuery.some( return typeChangesWithoutQuery.some(
({ from, to }) => oldField.type === from && newField.type === to ({ from, to }) => oldColumn.type === from && newColumn.type === to
); );
} }
// Using `DBField` will not narrow `default` based on the column `type` // Using `DBColumn` will not narrow `default` based on the column `type`
// Handle each field separately // Handle each column separately
type WithDefaultDefined<T extends DBField> = T & Required<Pick<T['schema'], 'default'>>; type WithDefaultDefined<T extends DBColumn> = T & Required<Pick<T['schema'], 'default'>>;
type DBFieldWithDefault = type DBColumnWithDefault =
| WithDefaultDefined<TextField> | WithDefaultDefined<TextColumn>
| WithDefaultDefined<DateField> | WithDefaultDefined<DateColumn>
| WithDefaultDefined<NumberField> | WithDefaultDefined<NumberColumn>
| WithDefaultDefined<BooleanField> | WithDefaultDefined<BooleanColumn>
| WithDefaultDefined<JsonField>; | WithDefaultDefined<JsonColumn>;
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault { function hasRuntimeDefault(column: DBColumn): column is DBColumnWithDefault {
return !!(field.schema.default && isSerializedSQL(field.schema.default)); return !!(column.schema.default && isSerializedSQL(column.schema.default));
} }

View file

@ -91,7 +91,7 @@ export async function loadMigration(
export async function loadInitialSnapshot(): Promise<DBSnapshot> { export async function loadInitialSnapshot(): Promise<DBSnapshot> {
const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8')); const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8'));
// `experimentalVersion: 1` -- added the version field // `experimentalVersion: 1` -- added the version column
if (snapshot.experimentalVersion === 1) { if (snapshot.experimentalVersion === 1) {
return snapshot; return snapshot;
} }

View file

@ -30,13 +30,13 @@ function generateTableType(name: string, collection: DBCollection): string {
${JSON.stringify(name)}, ${JSON.stringify(name)},
${JSON.stringify( ${JSON.stringify(
Object.fromEntries( Object.fromEntries(
Object.entries(collection.fields).map(([fieldName, field]) => [ Object.entries(collection.columns).map(([columnName, column]) => [
fieldName, columnName,
{ {
// Only select fields Drizzle needs for inference // Only select columns Drizzle needs for inference
type: field.type, type: column.type,
optional: field.schema.optional, optional: column.schema.optional,
default: field.schema.default, default: column.schema.default,
}, },
]) ])
) )

View file

@ -1,14 +1,14 @@
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
import { import {
type BooleanField, type BooleanColumn,
type DBCollection, type DBCollection,
type DBCollections, type DBCollections,
type DBField, type DBColumn,
type DateField, type DateColumn,
type FieldType, type ColumnType,
type JsonField, type JsonColumn,
type NumberField, type NumberColumn,
type TextField, type TextColumn,
} from '../core/types.js'; } from '../core/types.js';
import { bold, red } from 'kleur/colors'; import { bold, red } from 'kleur/colors';
import { type SQL, sql, getTableName } from 'drizzle-orm'; import { type SQL, sql, getTableName } from 'drizzle-orm';
@ -89,13 +89,13 @@ export function getCreateTableQuery(collectionName: string, collection: DBCollec
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`; let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
const colQueries = []; const colQueries = [];
const colHasPrimaryKey = Object.entries(collection.fields).find(([, field]) => const colHasPrimaryKey = Object.entries(collection.columns).find(([, column]) =>
hasPrimaryKey(field) hasPrimaryKey(column)
); );
if (!colHasPrimaryKey) { if (!colHasPrimaryKey) {
colQueries.push('_id INTEGER PRIMARY KEY'); colQueries.push('_id INTEGER PRIMARY KEY');
} }
for (const [columnName, column] of Object.entries(collection.fields)) { for (const [columnName, column] of Object.entries(collection.columns)) {
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType( const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
column.type column.type
)}${getModifiers(columnName, column)}`; )}${getModifiers(columnName, column)}`;
@ -129,12 +129,12 @@ export function getCreateIndexQueries(
export function getCreateForeignKeyQueries(collectionName: string, collection: DBCollection) { export function getCreateForeignKeyQueries(collectionName: string, collection: DBCollection) {
let queries: string[] = []; let queries: string[] = [];
for (const foreignKey of collection.foreignKeys ?? []) { for (const foreignKey of collection.foreignKeys ?? []) {
const fields = asArray(foreignKey.fields); const columns = asArray(foreignKey.columns);
const references = asArray(foreignKey.references); const references = asArray(foreignKey.references);
if (fields.length !== references.length) { if (columns.length !== references.length) {
throw new Error( throw new Error(
`Foreign key on ${collectionName} is misconfigured. \`fields\` and \`references\` must be the same length.` `Foreign key on ${collectionName} is misconfigured. \`columns\` and \`references\` must be the same length.`
); );
} }
const referencedCollection = references[0]?.schema.collection; const referencedCollection = references[0]?.schema.collection;
@ -143,7 +143,7 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D
`Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.` `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.`
); );
} }
const query = `FOREIGN KEY (${fields const query = `FOREIGN KEY (${columns
.map((f) => sqlite.escapeName(f)) .map((f) => sqlite.escapeName(f))
.join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references .join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references
.map((r) => sqlite.escapeName(r.schema.name!)) .map((r) => sqlite.escapeName(r.schema.name!))
@ -157,7 +157,7 @@ function asArray<T>(value: T | T[]) {
return Array.isArray(value) ? value : [value]; return Array.isArray(value) ? value : [value];
} }
export function schemaTypeToSqlType(type: FieldType): 'text' | 'integer' { export function schemaTypeToSqlType(type: ColumnType): 'text' | 'integer' {
switch (type) { switch (type) {
case 'date': case 'date':
case 'text': case 'text':
@ -169,26 +169,26 @@ export function schemaTypeToSqlType(type: FieldType): 'text' | 'integer' {
} }
} }
export function getModifiers(fieldName: string, field: DBField) { export function getModifiers(columnName: string, column: DBColumn) {
let modifiers = ''; let modifiers = '';
if (hasPrimaryKey(field)) { if (hasPrimaryKey(column)) {
return ' PRIMARY KEY'; return ' PRIMARY KEY';
} }
if (!field.schema.optional) { if (!column.schema.optional) {
modifiers += ' NOT NULL'; modifiers += ' NOT NULL';
} }
if (field.schema.unique) { if (column.schema.unique) {
modifiers += ' UNIQUE'; modifiers += ' UNIQUE';
} }
if (hasDefault(field)) { if (hasDefault(column)) {
modifiers += ` DEFAULT ${getDefaultValueSql(fieldName, field)}`; modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
} }
const references = getReferencesConfig(field); const references = getReferencesConfig(column);
if (references) { if (references) {
const { collection, name } = references.schema; const { collection, name } = references.schema;
if (!collection || !name) { if (!collection || !name) {
throw new Error( throw new Error(
`Field ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`collections\` object in your Astro config?` `Column ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`collections\` object in your Astro config?`
); );
} }
@ -197,30 +197,30 @@ export function getModifiers(fieldName: string, field: DBField) {
return modifiers; return modifiers;
} }
export function getReferencesConfig(field: DBField) { export function getReferencesConfig(column: DBColumn) {
const canHaveReferences = field.type === 'number' || field.type === 'text'; const canHaveReferences = column.type === 'number' || column.type === 'text';
if (!canHaveReferences) return undefined; if (!canHaveReferences) return undefined;
return field.schema.references; return column.schema.references;
} }
// Using `DBField` will not narrow `default` based on the column `type` // Using `DBColumn` will not narrow `default` based on the column `type`
// Handle each field separately // Handle each column separately
type WithDefaultDefined<T extends DBField> = T & { type WithDefaultDefined<T extends DBColumn> = T & {
schema: Required<Pick<T['schema'], 'default'>>; schema: Required<Pick<T['schema'], 'default'>>;
}; };
type DBFieldWithDefault = type DBColumnWithDefault =
| WithDefaultDefined<TextField> | WithDefaultDefined<TextColumn>
| WithDefaultDefined<DateField> | WithDefaultDefined<DateColumn>
| WithDefaultDefined<NumberField> | WithDefaultDefined<NumberColumn>
| WithDefaultDefined<BooleanField> | WithDefaultDefined<BooleanColumn>
| WithDefaultDefined<JsonField>; | WithDefaultDefined<JsonColumn>;
// Type narrowing the default fails on union types, so use a type guard // Type narrowing the default fails on union types, so use a type guard
export function hasDefault(field: DBField): field is DBFieldWithDefault { export function hasDefault(column: DBColumn): column is DBColumnWithDefault {
if (field.schema.default !== undefined) { if (column.schema.default !== undefined) {
return true; return true;
} }
if (hasPrimaryKey(field) && field.type === 'number') { if (hasPrimaryKey(column) && column.type === 'number') {
return true; return true;
} }
return false; return false;
@ -237,7 +237,7 @@ function toDefault<T>(def: T | SQL<any>): string {
} }
} }
function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string { function getDefaultValueSql(columnName: string, column: DBColumnWithDefault): string {
if (isSerializedSQL(column.schema.default)) { if (isSerializedSQL(column.schema.default)) {
return column.schema.default.sql; return column.schema.default.sql;
} }

View file

@ -19,7 +19,7 @@ const sqlSchema = z.instanceof(SQL<any>).transform(
}) })
); );
const baseFieldSchema = z.object({ const baseColumnSchema = z.object({
label: z.string().optional(), label: z.string().optional(),
optional: z.boolean().optional().default(false), optional: z.boolean().optional().default(false),
unique: z.boolean().optional().default(false), unique: z.boolean().optional().default(false),
@ -29,18 +29,18 @@ const baseFieldSchema = z.object({
collection: z.string().optional(), collection: z.string().optional(),
}); });
const booleanFieldSchema = z.object({ const booleanColumnSchema = z.object({
type: z.literal('boolean'), type: z.literal('boolean'),
schema: baseFieldSchema.extend({ schema: baseColumnSchema.extend({
default: z.union([z.boolean(), sqlSchema]).optional(), default: z.union([z.boolean(), sqlSchema]).optional(),
}), }),
}); });
const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( const numberColumnBaseSchema = baseColumnSchema.omit({ optional: true }).and(
z.union([ z.union([
z.object({ z.object({
primaryKey: z.literal(false).optional().default(false), primaryKey: z.literal(false).optional().default(false),
optional: baseFieldSchema.shape.optional, optional: baseColumnSchema.shape.optional,
default: z.union([z.number(), sqlSchema]).optional(), default: z.union([z.number(), sqlSchema]).optional(),
}), }),
z.object({ z.object({
@ -54,31 +54,31 @@ const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and(
]) ])
); );
const numberFieldOptsSchema: z.ZodType< const numberColumnOptsSchema: z.ZodType<
z.infer<typeof numberFieldBaseSchema> & { z.infer<typeof numberColumnBaseSchema> & {
// ReferenceableField creates a circular type. Define ZodType to resolve. // ReferenceableColumn creates a circular type. Define ZodType to resolve.
references?: NumberField; references?: NumberColumn;
}, },
ZodTypeDef, ZodTypeDef,
z.input<typeof numberFieldBaseSchema> & { z.input<typeof numberColumnBaseSchema> & {
references?: () => z.input<typeof numberFieldSchema>; references?: () => z.input<typeof numberColumnSchema>;
} }
> = numberFieldBaseSchema.and( > = numberColumnBaseSchema.and(
z.object({ z.object({
references: z references: z
.function() .function()
.returns(z.lazy(() => numberFieldSchema)) .returns(z.lazy(() => numberColumnSchema))
.optional() .optional()
.transform((fn) => fn?.()), .transform((fn) => fn?.()),
}) })
); );
const numberFieldSchema = z.object({ const numberColumnSchema = z.object({
type: z.literal('number'), type: z.literal('number'),
schema: numberFieldOptsSchema, schema: numberColumnOptsSchema,
}); });
const textFieldBaseSchema = baseFieldSchema const textColumnBaseSchema = baseColumnSchema
.omit({ optional: true }) .omit({ optional: true })
.extend({ .extend({
default: z.union([z.string(), sqlSchema]).optional(), default: z.union([z.string(), sqlSchema]).optional(),
@ -88,7 +88,7 @@ const textFieldBaseSchema = baseFieldSchema
z.union([ z.union([
z.object({ z.object({
primaryKey: z.literal(false).optional().default(false), primaryKey: z.literal(false).optional().default(false),
optional: baseFieldSchema.shape.optional, optional: baseColumnSchema.shape.optional,
}), }),
z.object({ z.object({
// text primary key allows NULL values. // text primary key allows NULL values.
@ -101,33 +101,33 @@ const textFieldBaseSchema = baseFieldSchema
]) ])
); );
const textFieldOptsSchema: z.ZodType< const textColumnOptsSchema: z.ZodType<
z.infer<typeof textFieldBaseSchema> & { z.infer<typeof textColumnBaseSchema> & {
// ReferenceableField creates a circular type. Define ZodType to resolve. // ReferenceableColumn creates a circular type. Define ZodType to resolve.
references?: TextField; references?: TextColumn;
}, },
ZodTypeDef, ZodTypeDef,
z.input<typeof textFieldBaseSchema> & { z.input<typeof textColumnBaseSchema> & {
references?: () => z.input<typeof textFieldSchema>; references?: () => z.input<typeof textColumnSchema>;
} }
> = textFieldBaseSchema.and( > = textColumnBaseSchema.and(
z.object({ z.object({
references: z references: z
.function() .function()
.returns(z.lazy(() => textFieldSchema)) .returns(z.lazy(() => textColumnSchema))
.optional() .optional()
.transform((fn) => fn?.()), .transform((fn) => fn?.()),
}) })
); );
const textFieldSchema = z.object({ const textColumnSchema = z.object({
type: z.literal('text'), type: z.literal('text'),
schema: textFieldOptsSchema, schema: textColumnOptsSchema,
}); });
const dateFieldSchema = z.object({ const dateColumnSchema = z.object({
type: z.literal('date'), type: z.literal('date'),
schema: baseFieldSchema.extend({ schema: baseColumnSchema.extend({
default: z default: z
.union([ .union([
sqlSchema, sqlSchema,
@ -138,23 +138,23 @@ const dateFieldSchema = z.object({
}), }),
}); });
const jsonFieldSchema = z.object({ const jsonColumnSchema = z.object({
type: z.literal('json'), type: z.literal('json'),
schema: baseFieldSchema.extend({ schema: baseColumnSchema.extend({
default: z.unknown().optional(), default: z.unknown().optional(),
}), }),
}); });
export const fieldSchema = z.union([ export const columnSchema = z.union([
booleanFieldSchema, booleanColumnSchema,
numberFieldSchema, numberColumnSchema,
textFieldSchema, textColumnSchema,
dateFieldSchema, dateColumnSchema,
jsonFieldSchema, jsonColumnSchema,
]); ]);
export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]); export const referenceableColumnSchema = z.union([textColumnSchema, numberColumnSchema]);
const fieldsSchema = z.record(fieldSchema); const columnsSchema = z.record(columnSchema);
export const indexSchema = z.object({ export const indexSchema = z.object({
on: z.string().or(z.array(z.string())), on: z.string().or(z.array(z.string())),
@ -162,27 +162,27 @@ export const indexSchema = z.object({
}); });
type ForeignKeysInput = { type ForeignKeysInput = {
fields: MaybeArray<string>; columns: MaybeArray<string>;
references: () => MaybeArray<Omit<z.input<typeof referenceableFieldSchema>, 'references'>>; references: () => MaybeArray<Omit<z.input<typeof referenceableColumnSchema>, 'references'>>;
}; };
type ForeignKeysOutput = Omit<ForeignKeysInput, 'references'> & { type ForeignKeysOutput = Omit<ForeignKeysInput, 'references'> & {
// reference fn called in `transform`. Ensures output is JSON serializable. // reference fn called in `transform`. Ensures output is JSON serializable.
references: MaybeArray<Omit<z.output<typeof referenceableFieldSchema>, 'references'>>; references: MaybeArray<Omit<z.output<typeof referenceableColumnSchema>, 'references'>>;
}; };
const foreignKeysSchema: z.ZodType<ForeignKeysOutput, ZodTypeDef, ForeignKeysInput> = z.object({ const foreignKeysSchema: z.ZodType<ForeignKeysOutput, ZodTypeDef, ForeignKeysInput> = z.object({
fields: z.string().or(z.array(z.string())), columns: z.string().or(z.array(z.string())),
references: z references: z
.function() .function()
.returns(z.lazy(() => referenceableFieldSchema.or(z.array(referenceableFieldSchema)))) .returns(z.lazy(() => referenceableColumnSchema.or(z.array(referenceableColumnSchema))))
.transform((fn) => fn()), .transform((fn) => fn()),
}); });
export type Indexes = Record<string, z.infer<typeof indexSchema>>; export type Indexes = Record<string, z.infer<typeof indexSchema>>;
const baseCollectionSchema = z.object({ const baseCollectionSchema = z.object({
fields: fieldsSchema, columns: columnsSchema,
indexes: z.record(indexSchema).optional(), indexes: z.record(indexSchema).optional(),
foreignKeys: z.array(foreignKeysSchema).optional(), foreignKeys: z.array(foreignKeysSchema).optional(),
}); });
@ -206,43 +206,43 @@ export const collectionsSchema = z.preprocess((rawCollections) => {
collectionName, collectionName,
collectionSchema.parse(collection, { errorMap }) collectionSchema.parse(collection, { errorMap })
); );
// Append collection and field names to fields. // Append collection and column names to columns.
// Used to track collection info for references. // Used to track collection info for references.
const { fields } = z.object({ fields: z.record(z.any()) }).parse(collection, { errorMap }); const { columns } = z.object({ columns: z.record(z.any()) }).parse(collection, { errorMap });
for (const [fieldName, field] of Object.entries(fields)) { for (const [columnName, column] of Object.entries(columns)) {
field.schema.name = fieldName; column.schema.name = columnName;
field.schema.collection = collectionName; column.schema.collection = collectionName;
} }
} }
return rawCollections; return rawCollections;
}, z.record(collectionSchema)); }, z.record(collectionSchema));
export type BooleanField = z.infer<typeof booleanFieldSchema>; export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
export type BooleanFieldInput = z.input<typeof booleanFieldSchema>; export type BooleanColumnInput = z.input<typeof booleanColumnSchema>;
export type NumberField = z.infer<typeof numberFieldSchema>; export type NumberColumn = z.infer<typeof numberColumnSchema>;
export type NumberFieldInput = z.input<typeof numberFieldSchema>; export type NumberColumnInput = z.input<typeof numberColumnSchema>;
export type TextField = z.infer<typeof textFieldSchema>; export type TextColumn = z.infer<typeof textColumnSchema>;
export type TextFieldInput = z.input<typeof textFieldSchema>; export type TextColumnInput = z.input<typeof textColumnSchema>;
export type DateField = z.infer<typeof dateFieldSchema>; export type DateColumn = z.infer<typeof dateColumnSchema>;
export type DateFieldInput = z.input<typeof dateFieldSchema>; export type DateColumnInput = z.input<typeof dateColumnSchema>;
export type JsonField = z.infer<typeof jsonFieldSchema>; export type JsonColumn = z.infer<typeof jsonColumnSchema>;
export type JsonFieldInput = z.input<typeof jsonFieldSchema>; export type JsonColumnInput = z.input<typeof jsonColumnSchema>;
export type FieldType = export type ColumnType =
| BooleanField['type'] | BooleanColumn['type']
| NumberField['type'] | NumberColumn['type']
| TextField['type'] | TextColumn['type']
| DateField['type'] | DateColumn['type']
| JsonField['type']; | JsonColumn['type'];
export type DBField = z.infer<typeof fieldSchema>; export type DBColumn = z.infer<typeof columnSchema>;
export type DBFieldInput = export type DBColumnInput =
| DateFieldInput | DateColumnInput
| BooleanFieldInput | BooleanColumnInput
| NumberFieldInput | NumberColumnInput
| TextFieldInput | TextColumnInput
| JsonFieldInput; | JsonColumnInput;
export type DBFields = z.infer<typeof fieldsSchema>; export type DBColumns = z.infer<typeof columnsSchema>;
export type DBCollection = z.infer< export type DBCollection = z.infer<
typeof readableCollectionSchema | typeof writableCollectionSchema typeof readableCollectionSchema | typeof writableCollectionSchema
>; >;
@ -260,20 +260,20 @@ export type WritableDBCollection = z.infer<typeof writableCollectionSchema>;
export type DBDataContext = { export type DBDataContext = {
db: SqliteDB; db: SqliteDB;
seed: <TFields extends FieldsConfig>( seed: <TColumns extends ColumnsConfig>(
collection: ResolvedCollectionConfig<TFields>, collection: ResolvedCollectionConfig<TColumns>,
data: MaybeArray<SQLiteInsertValue<Table<string, TFields>>> data: MaybeArray<SQLiteInsertValue<Table<string, TColumns>>>
) => Promise<void>; ) => Promise<void>;
seedReturning: < seedReturning: <
TFields extends FieldsConfig, TColumns extends ColumnsConfig,
TData extends MaybeArray<SQLiteInsertValue<Table<string, TFields>>>, TData extends MaybeArray<SQLiteInsertValue<Table<string, TColumns>>>,
>( >(
collection: ResolvedCollectionConfig<TFields>, collection: ResolvedCollectionConfig<TColumns>,
data: TData data: TData
) => Promise< ) => Promise<
TData extends Array<SQLiteInsertValue<Table<string, TFields>>> TData extends Array<SQLiteInsertValue<Table<string, TColumns>>>
? InferSelectModel<Table<string, TFields>>[] ? InferSelectModel<Table<string, TColumns>>[]
: InferSelectModel<Table<string, TFields>> : InferSelectModel<Table<string, TColumns>>
>; >;
mode: 'dev' | 'build'; mode: 'dev' | 'build';
}; };
@ -300,37 +300,37 @@ export const astroConfigWithDbSchema = z.object({
db: dbConfigSchema.optional(), db: dbConfigSchema.optional(),
}); });
export type FieldsConfig = z.input<typeof collectionSchema>['fields']; export type ColumnsConfig = z.input<typeof collectionSchema>['columns'];
interface CollectionConfig<TFields extends FieldsConfig = FieldsConfig> interface CollectionConfig<TColumns extends ColumnsConfig = ColumnsConfig>
// use `extends` to ensure types line up with zod, // use `extends` to ensure types line up with zod,
// only adding generics for type completions. // only adding generics for type completions.
extends Pick<z.input<typeof collectionSchema>, 'fields' | 'indexes' | 'foreignKeys'> { extends Pick<z.input<typeof collectionSchema>, 'columns' | 'indexes' | 'foreignKeys'> {
fields: TFields; columns: TColumns;
foreignKeys?: Array<{ foreignKeys?: Array<{
fields: MaybeArray<Extract<keyof TFields, string>>; columns: MaybeArray<Extract<keyof TColumns, string>>;
// TODO: runtime error if parent collection doesn't match for all fields. Can't put a generic here... // TODO: runtime error if parent collection doesn't match for all columns. Can't put a generic here...
references: () => MaybeArray<z.input<typeof referenceableFieldSchema>>; references: () => MaybeArray<z.input<typeof referenceableColumnSchema>>;
}>; }>;
indexes?: Record<string, IndexConfig<TFields>>; indexes?: Record<string, IndexConfig<TColumns>>;
} }
interface IndexConfig<TFields extends FieldsConfig> extends z.input<typeof indexSchema> { interface IndexConfig<TColumns extends ColumnsConfig> extends z.input<typeof indexSchema> {
on: MaybeArray<Extract<keyof TFields, string>>; on: MaybeArray<Extract<keyof TColumns, string>>;
} }
export type ResolvedCollectionConfig< export type ResolvedCollectionConfig<
TFields extends FieldsConfig = FieldsConfig, TColumns extends ColumnsConfig = ColumnsConfig,
Writable extends boolean = boolean, Writable extends boolean = boolean,
> = CollectionConfig<TFields> & { > = CollectionConfig<TColumns> & {
writable: Writable; writable: Writable;
table: Table<string, TFields>; table: Table<string, TColumns>;
}; };
function baseDefineCollection<TFields extends FieldsConfig, TWritable extends boolean>( function baseDefineCollection<TColumns extends ColumnsConfig, TWritable extends boolean>(
userConfig: CollectionConfig<TFields>, userConfig: CollectionConfig<TColumns>,
writable: TWritable writable: TWritable
): ResolvedCollectionConfig<TFields, TWritable> { ): ResolvedCollectionConfig<TColumns, TWritable> {
return { return {
...userConfig, ...userConfig,
writable, writable,
@ -339,26 +339,26 @@ function baseDefineCollection<TFields extends FieldsConfig, TWritable extends bo
}; };
} }
export function defineCollection<TFields extends FieldsConfig>( export function defineCollection<TColumns extends ColumnsConfig>(
userConfig: CollectionConfig<TFields> userConfig: CollectionConfig<TColumns>
): ResolvedCollectionConfig<TFields, false> { ): ResolvedCollectionConfig<TColumns, false> {
return baseDefineCollection(userConfig, false); return baseDefineCollection(userConfig, false);
} }
export function defineWritableCollection<TFields extends FieldsConfig>( export function defineWritableCollection<TColumns extends ColumnsConfig>(
userConfig: CollectionConfig<TFields> userConfig: CollectionConfig<TColumns>
): ResolvedCollectionConfig<TFields, true> { ): ResolvedCollectionConfig<TColumns, true> {
return baseDefineCollection(userConfig, true); return baseDefineCollection(userConfig, true);
} }
export type AstroConfigWithDB = z.input<typeof astroConfigWithDbSchema>; export type AstroConfigWithDB = z.input<typeof astroConfigWithDbSchema>;
// We cannot use `Omit<NumberField | TextField, 'type'>`, // We cannot use `Omit<NumberColumn | TextColumn, 'type'>`,
// since Omit collapses our union type on primary key. // since Omit collapses our union type on primary key.
type NumberFieldOpts = z.input<typeof numberFieldOptsSchema>; type NumberColumnOpts = z.input<typeof numberColumnOptsSchema>;
type TextFieldOpts = z.input<typeof textFieldOptsSchema>; type TextColumnOpts = z.input<typeof textColumnOptsSchema>;
function createField<S extends string, T extends Record<string, unknown>>(type: S, schema: T) { function createColumn<S extends string, T extends Record<string, unknown>>(type: S, schema: T) {
return { return {
type, type,
/** /**
@ -368,20 +368,20 @@ function createField<S extends string, T extends Record<string, unknown>>(type:
}; };
} }
export const field = { export const column = {
number: <T extends NumberFieldOpts>(opts: T = {} as T) => { number: <T extends NumberColumnOpts>(opts: T = {} as T) => {
return createField('number', opts) satisfies { type: 'number' }; return createColumn('number', opts) satisfies { type: 'number' };
}, },
boolean: <T extends BooleanFieldInput['schema']>(opts: T = {} as T) => { boolean: <T extends BooleanColumnInput['schema']>(opts: T = {} as T) => {
return createField('boolean', opts) satisfies { type: 'boolean' }; return createColumn('boolean', opts) satisfies { type: 'boolean' };
}, },
text: <T extends TextFieldOpts>(opts: T = {} as T) => { text: <T extends TextColumnOpts>(opts: T = {} as T) => {
return createField('text', opts) satisfies { type: 'text' }; return createColumn('text', opts) satisfies { type: 'text' };
}, },
date<T extends DateFieldInput['schema']>(opts: T = {} as T) { date<T extends DateColumnInput['schema']>(opts: T = {} as T) {
return createField('date', opts) satisfies { type: 'date' }; return createColumn('date', opts) satisfies { type: 'date' };
}, },
json<T extends JsonFieldInput['schema']>(opts: T = {} as T) { json<T extends JsonColumnInput['schema']>(opts: T = {} as T) {
return createField('json', opts) satisfies { type: 'json' }; return createColumn('json', opts) satisfies { type: 'json' };
}, },
}; };

View file

@ -1,4 +1,4 @@
export { defineCollection, defineWritableCollection, defineData, field } from './core/types.js'; export { defineCollection, defineWritableCollection, defineData, column } from './core/types.js';
export type { ResolvedCollectionConfig, DBDataContext } from './core/types.js'; export type { ResolvedCollectionConfig, DBDataContext } from './core/types.js';
export { cli } from './core/cli/index.js'; export { cli } from './core/cli/index.js';
export { integration as default } from './core/integration/index.js'; export { integration as default } from './core/integration/index.js';

View file

@ -1,5 +1,5 @@
import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
import { type DBCollection, type DBField } from '../core/types.js'; import { type DBCollection, type DBColumn } from '../core/types.js';
import { type ColumnBuilderBaseConfig, type ColumnDataType, sql, SQL } from 'drizzle-orm'; import { type ColumnBuilderBaseConfig, type ColumnDataType, sql, SQL } from 'drizzle-orm';
import { import {
customType, customType,
@ -18,8 +18,8 @@ export type SqliteDB = SqliteRemoteDatabase;
export type { Table } from './types.js'; export type { Table } from './types.js';
export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js'; export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js';
export function hasPrimaryKey(field: DBField) { export function hasPrimaryKey(column: DBColumn) {
return 'primaryKey' in field.schema && !!field.schema.primaryKey; return 'primaryKey' in column.schema && !!column.schema.primaryKey;
} }
// Exports a few common expressions // Exports a few common expressions
@ -58,11 +58,11 @@ type D1ColumnBuilder = SQLiteColumnBuilderBase<
export function collectionToTable(name: string, collection: DBCollection) { export function collectionToTable(name: string, collection: DBCollection) {
const columns: Record<string, D1ColumnBuilder> = {}; const columns: Record<string, D1ColumnBuilder> = {};
if (!Object.entries(collection.fields).some(([, field]) => hasPrimaryKey(field))) { if (!Object.entries(collection.columns).some(([, column]) => hasPrimaryKey(column))) {
columns['_id'] = integer('_id').primaryKey(); columns['_id'] = integer('_id').primaryKey();
} }
for (const [fieldName, field] of Object.entries(collection.fields)) { for (const [columnName, column] of Object.entries(collection.columns)) {
columns[fieldName] = columnMapper(fieldName, field); columns[columnName] = columnMapper(columnName, column);
} }
const table = sqliteTable(name, columns, (ormTable) => { const table = sqliteTable(name, columns, (ormTable) => {
const indexes: Record<string, IndexBuilder> = {}; const indexes: Record<string, IndexBuilder> = {};
@ -82,7 +82,7 @@ function atLeastOne<T>(arr: T[]): arr is [T, ...T[]] {
return arr.length > 0; return arr.length > 0;
} }
function columnMapper(fieldName: string, field: DBField) { function columnMapper(columnName: string, column: DBColumn) {
let c: ReturnType< let c: ReturnType<
| typeof text | typeof text
| typeof integer | typeof integer
@ -91,45 +91,45 @@ function columnMapper(fieldName: string, field: DBField) {
| typeof integer<string, 'boolean'> | typeof integer<string, 'boolean'>
>; >;
switch (field.type) { switch (column.type) {
case 'text': { case 'text': {
c = text(fieldName); c = text(columnName);
// Duplicate default logic across cases to preserve type inference. // Duplicate default logic across cases to preserve type inference.
// No clean generic for every column builder. // No clean generic for every column builder.
if (field.schema.default !== undefined) if (column.schema.default !== undefined)
c = c.default(handleSerializedSQL(field.schema.default)); c = c.default(handleSerializedSQL(column.schema.default));
if (field.schema.primaryKey === true) c = c.primaryKey(); if (column.schema.primaryKey === true) c = c.primaryKey();
break; break;
} }
case 'number': { case 'number': {
c = integer(fieldName); c = integer(columnName);
if (field.schema.default !== undefined) if (column.schema.default !== undefined)
c = c.default(handleSerializedSQL(field.schema.default)); c = c.default(handleSerializedSQL(column.schema.default));
if (field.schema.primaryKey === true) c = c.primaryKey(); if (column.schema.primaryKey === true) c = c.primaryKey();
break; break;
} }
case 'boolean': { case 'boolean': {
c = integer(fieldName, { mode: 'boolean' }); c = integer(columnName, { mode: 'boolean' });
if (field.schema.default !== undefined) if (column.schema.default !== undefined)
c = c.default(handleSerializedSQL(field.schema.default)); c = c.default(handleSerializedSQL(column.schema.default));
break; break;
} }
case 'json': case 'json':
c = jsonType(fieldName); c = jsonType(columnName);
if (field.schema.default !== undefined) c = c.default(field.schema.default); if (column.schema.default !== undefined) c = c.default(column.schema.default);
break; break;
case 'date': { case 'date': {
c = dateType(fieldName); c = dateType(columnName);
if (field.schema.default !== undefined) { if (column.schema.default !== undefined) {
const def = handleSerializedSQL(field.schema.default); const def = handleSerializedSQL(column.schema.default);
c = c.default(typeof def === 'string' ? new Date(def) : def); c = c.default(typeof def === 'string' ? new Date(def) : def);
} }
break; break;
} }
} }
if (!field.schema.optional) c = c.notNull(); if (!column.schema.optional) c = c.notNull();
if (field.schema.unique) c = c.unique(); if (column.schema.unique) c = c.unique();
return c; return c;
} }

View file

@ -1,6 +1,6 @@
import type { ColumnDataType, ColumnBaseConfig } from 'drizzle-orm'; import type { ColumnDataType, ColumnBaseConfig } from 'drizzle-orm';
import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'; import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core';
import type { DBField, FieldsConfig } from '../core/types.js'; import type { DBColumn, ColumnsConfig } from '../core/types.js';
type GeneratedConfig<T extends ColumnDataType = ColumnDataType> = Pick< type GeneratedConfig<T extends ColumnDataType = ColumnDataType> = Pick<
ColumnBaseConfig<T, string>, ColumnBaseConfig<T, string>,
@ -62,7 +62,7 @@ export type AstroJson<T extends GeneratedConfig<'custom'>> = SQLiteColumn<
} }
>; >;
export type Column<T extends DBField['type'], S extends GeneratedConfig> = T extends 'boolean' export type Column<T extends DBColumn['type'], S extends GeneratedConfig> = T extends 'boolean'
? AstroBoolean<S> ? AstroBoolean<S>
: T extends 'number' : T extends 'number'
? AstroNumber<S> ? AstroNumber<S>
@ -76,23 +76,23 @@ export type Column<T extends DBField['type'], S extends GeneratedConfig> = T ext
export type Table< export type Table<
TTableName extends string, TTableName extends string,
TFields extends FieldsConfig, TColumns extends ColumnsConfig,
> = SQLiteTableWithColumns<{ > = SQLiteTableWithColumns<{
name: TTableName; name: TTableName;
schema: undefined; schema: undefined;
dialect: 'sqlite'; dialect: 'sqlite';
columns: { columns: {
[K in Extract<keyof TFields, string>]: Column< [K in Extract<keyof TColumns, string>]: Column<
TFields[K]['type'], TColumns[K]['type'],
{ {
tableName: TTableName; tableName: TTableName;
name: K; name: K;
hasDefault: TFields[K]['schema'] extends { default: NonNullable<unknown> } hasDefault: TColumns[K]['schema'] extends { default: NonNullable<unknown> }
? true ? true
: TFields[K]['schema'] extends { primaryKey: true } : TColumns[K]['schema'] extends { primaryKey: true }
? true ? true
: false; : false;
notNull: TFields[K]['schema']['optional'] extends true ? false : true; notNull: TColumns[K]['schema']['optional'] extends true ? false : true;
} }
>; >;
}; };

View file

@ -60,7 +60,7 @@ describe('astro:db', () => {
app = await fixture.loadTestAdapterApp(); app = await fixture.loadTestAdapterApp();
}); });
it('Allows expression defaults for date fields', async () => { it('Allows expression defaults for date columns', async () => {
const request = new Request('http://example.com/'); const request = new Request('http://example.com/');
const res = await app.render(request); const res = await app.render(request);
const html = await res.text(); const html = await res.text();
@ -80,7 +80,7 @@ describe('astro:db', () => {
expect(new Date(themeAdded).getTime()).to.not.be.NaN; expect(new Date(themeAdded).getTime()).to.not.be.NaN;
}); });
it('Allows expression defaults for text fields', async () => { it('Allows expression defaults for text columns', async () => {
const request = new Request('http://example.com/'); const request = new Request('http://example.com/');
const res = await app.render(request); const res = await app.render(request);
const html = await res.text(); const html = await res.text();
@ -90,7 +90,7 @@ describe('astro:db', () => {
expect(themeOwner).to.equal(''); expect(themeOwner).to.equal('');
}); });
it('Allows expression defaults for boolean fields', async () => { it('Allows expression defaults for boolean columns', async () => {
const request = new Request('http://example.com/'); const request = new Request('http://example.com/');
const res = await app.render(request); const res = await app.render(request);
const html = await res.text(); const html = await res.text();

View file

@ -1,23 +1,23 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import db, { defineCollection, defineWritableCollection, field, sql, NOW } from '@astrojs/db'; import db, { defineCollection, defineWritableCollection, column, sql, NOW } from '@astrojs/db';
const Author = defineCollection({ const Author = defineCollection({
fields: { columns: {
name: field.text(), name: column.text(),
}, },
}); });
const Themes = defineWritableCollection({ const Themes = defineWritableCollection({
fields: { columns: {
name: field.text(), name: column.text(),
added: field.date({ added: column.date({
default: sql`CURRENT_TIMESTAMP` default: sql`CURRENT_TIMESTAMP`
}), }),
updated: field.date({ updated: column.date({
default: NOW default: NOW
}), }),
isDark: field.boolean({ default: sql`TRUE` }), isDark: column.boolean({ default: sql`TRUE` }),
owner: field.text({ optional: true, default: sql`NULL` }), owner: column.text({ optional: true, default: sql`NULL` }),
}, },
}); });

View file

@ -1,12 +1,12 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import db, { defineCollection, field } from '@astrojs/db'; import db, { defineCollection, column } from '@astrojs/db';
import { asJson, createGlob } from './utils'; import { asJson, createGlob } from './utils';
const Quote = defineCollection({ const Quote = defineCollection({
fields: { columns: {
author: field.text(), author: column.text(),
body: field.text(), body: column.text(),
file: field.text({ unique: true }), file: column.text({ unique: true }),
}, },
}); });

View file

@ -14,9 +14,9 @@ export function createGlob({ db, mode }: Pick<DBDataContext, 'db' | 'mode'>) {
) { ) {
// TODO: expose `table` // TODO: expose `table`
const { table } = opts.into as any; const { table } = opts.into as any;
const fileField = table.file; const fileColumn = table.file;
if (!fileField) { if (!fileColumn) {
throw new Error('`file` field is required for glob collections.'); throw new Error('`file` column is required for glob collections.');
} }
if (mode === 'dev') { if (mode === 'dev') {
chokidar chokidar
@ -33,12 +33,12 @@ export function createGlob({ db, mode }: Pick<DBDataContext, 'db' | 'mode'>) {
.insert(table) .insert(table)
.values({ ...parsed, file }) .values({ ...parsed, file })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: fileField, target: fileColumn,
set: parsed, set: parsed,
}); });
}) })
.on('unlink', async (file) => { .on('unlink', async (file) => {
await db.delete(table).where(eq(fileField, file)); await db.delete(table).where(eq(fileColumn, file));
}); });
} else { } else {
const files = await fastGlob(pattern); const files = await fastGlob(pattern);

View file

@ -1,25 +1,25 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import astroDb, { defineCollection, field } from '@astrojs/db'; import astroDb, { defineCollection, column } from '@astrojs/db';
const Recipe = defineCollection({ const Recipe = defineCollection({
fields: { columns: {
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
title: field.text(), title: column.text(),
description: field.text(), description: column.text(),
}, },
}); });
const Ingredient = defineCollection({ const Ingredient = defineCollection({
fields: { columns: {
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
name: field.text(), name: column.text(),
quantity: field.number(), quantity: column.number(),
recipeId: field.number(), recipeId: column.number(),
}, },
indexes: { indexes: {
recipeIdx: { on: 'recipeId' }, recipeIdx: { on: 'recipeId' },
}, },
foreignKeys: [{ fields: 'recipeId', references: () => [Recipe.fields.id] }], foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }],
}); });
export default defineConfig({ export default defineConfig({

View file

@ -1,25 +1,25 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact'; import preact from '@astrojs/preact';
import simpleStackForm from 'simple-stack-form'; import simpleStackForm from 'simple-stack-form';
import db, { defineCollection, defineWritableCollection, field } from '@astrojs/db'; import db, { defineCollection, defineWritableCollection, column } from '@astrojs/db';
import node from '@astrojs/node'; import node from '@astrojs/node';
const Event = defineCollection({ const Event = defineCollection({
fields: { columns: {
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
name: field.text(), name: column.text(),
description: field.text(), description: column.text(),
ticketPrice: field.number(), ticketPrice: column.number(),
date: field.date(), date: column.date(),
location: field.text(), location: column.text(),
}, },
}); });
const Ticket = defineWritableCollection({ const Ticket = defineWritableCollection({
fields: { columns: {
eventId: field.text(), eventId: column.text(),
email: field.text(), email: column.text(),
quantity: field.number(), quantity: column.number(),
newsletter: field.boolean({ newsletter: column.boolean({
default: false, default: false,
}), }),
}, },

View file

@ -4,25 +4,25 @@ import { type ComponentProps, createContext } from 'preact';
import { useContext, useState } from 'preact/hooks'; import { useContext, useState } from 'preact/hooks';
import { navigate } from 'astro:transitions/client'; import { navigate } from 'astro:transitions/client';
import { import {
type FieldErrors, type ColumnErrors,
type FormState, type FormState,
type FormValidator, type FormValidator,
formNameInputProps, formNameInputProps,
getInitialFormState, getInitialFormState,
toSetValidationErrors, toSetValidationErrors,
toTrackAstroSubmitStatus, toTrackAstroSubmitStatus,
toValidateField, toValidateColumn,
validateForm, validateForm,
} from 'simple:form'; } from 'simple:form';
export function useCreateFormContext(validator: FormValidator, fieldErrors?: FieldErrors) { export function useCreateFormContext(validator: FormValidator, columnErrors?: ColumnErrors) {
const initial = getInitialFormState({ validator, fieldErrors }); const initial = getInitialFormState({ validator, columnErrors });
const [formState, setFormState] = useState<FormState>(initial); const [formState, setFormState] = useState<FormState>(initial);
return { return {
value: formState, value: formState,
set: setFormState, set: setFormState,
setValidationErrors: toSetValidationErrors(setFormState), setValidationErrors: toSetValidationErrors(setFormState),
validateField: toValidateField(setFormState), validateColumn: toValidateColumn(setFormState),
trackAstroSubmitStatus: toTrackAstroSubmitStatus(setFormState), trackAstroSubmitStatus: toTrackAstroSubmitStatus(setFormState),
}; };
} }
@ -45,15 +45,15 @@ export function Form({
children, children,
validator, validator,
context, context,
fieldErrors, columnErrors,
name, name,
...formProps ...formProps
}: { }: {
validator: FormValidator; validator: FormValidator;
context?: FormContextType; context?: FormContextType;
fieldErrors?: FieldErrors; columnErrors?: ColumnErrors;
} & Omit<ComponentProps<'form'>, 'method' | 'onSubmit'>) { } & Omit<ComponentProps<'form'>, 'method' | 'onSubmit'>) {
const formContext = context ?? useCreateFormContext(validator, fieldErrors); const formContext = context ?? useCreateFormContext(validator, columnErrors);
return ( return (
<FormContext.Provider value={formContext}> <FormContext.Provider value={formContext}>
@ -80,7 +80,7 @@ export function Form({
return formContext.trackAstroSubmitStatus(); return formContext.trackAstroSubmitStatus();
} }
formContext.setValidationErrors(parsed.fieldErrors); formContext.setValidationErrors(parsed.columnErrors);
}} }}
> >
{name ? <input {...formNameInputProps} value={name} /> : null} {name ? <input {...formNameInputProps} value={name} /> : null}
@ -92,28 +92,28 @@ export function Form({
export function Input({ onInput, ...inputProps }: ComponentProps<'input'> & { name: string }) { export function Input({ onInput, ...inputProps }: ComponentProps<'input'> & { name: string }) {
const formContext = useFormContext(); const formContext = useFormContext();
const fieldState = formContext.value.fields[inputProps.name]; const columnState = formContext.value.columns[inputProps.name];
if (!fieldState) { if (!columnState) {
throw new Error( throw new Error(
`Input "${inputProps.name}" not found in form. Did you use the <Form> component?` `Input "${inputProps.name}" not found in form. Did you use the <Form> component?`
); );
} }
const { hasErroredOnce, validationErrors, validator } = fieldState; const { hasErroredOnce, validationErrors, validator } = columnState;
return ( return (
<> <>
<input <input
onBlur={async (e) => { onBlur={async (e) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
if (value === '') return; if (value === '') return;
formContext.validateField(inputProps.name, value, validator); formContext.validateColumn(inputProps.name, value, validator);
}} }}
onInput={async (e) => { onInput={async (e) => {
onInput?.(e); onInput?.(e);
if (!hasErroredOnce) return; if (!hasErroredOnce) return;
const value = e.currentTarget.value; const value = e.currentTarget.value;
formContext.validateField(inputProps.name, value, validator); formContext.validateColumn(inputProps.name, value, validator);
}} }}
{...inputProps} {...inputProps}
/> />

View file

@ -5,27 +5,27 @@ import {
getMigrationQueries, getMigrationQueries,
} from '../../dist/core/cli/migration-queries.js'; } from '../../dist/core/cli/migration-queries.js';
import { getCreateTableQuery } from '../../dist/core/queries.js'; import { getCreateTableQuery } from '../../dist/core/queries.js';
import { field, defineCollection, collectionSchema } from '../../dist/core/types.js'; import { column, defineCollection, collectionSchema } from '../../dist/core/types.js';
import { NOW, sql } from '../../dist/runtime/index.js'; import { NOW, sql } from '../../dist/runtime/index.js';
const COLLECTION_NAME = 'Users'; const COLLECTION_NAME = 'Users';
// `parse` to resolve schema transformations // `parse` to resolve schema transformations
// ex. convert field.date() to ISO strings // ex. convert column.date() to ISO strings
const userInitial = collectionSchema.parse( const userInitial = collectionSchema.parse(
defineCollection({ defineCollection({
fields: { columns: {
name: field.text(), name: column.text(),
age: field.number(), age: column.number(),
email: field.text({ unique: true }), email: column.text({ unique: true }),
mi: field.text({ optional: true }), mi: column.text({ optional: true }),
}, },
}) })
); );
const defaultAmbiguityResponses = { const defaultAmbiguityResponses = {
collectionRenames: {}, collectionRenames: {},
fieldRenames: {}, columnRenames: {},
}; };
function userChangeQueries( function userChangeQueries(
@ -53,7 +53,7 @@ function configChangeQueries(
}); });
} }
describe('field queries', () => { describe('column queries', () => {
describe('getMigrationQueries', () => { describe('getMigrationQueries', () => {
it('should be empty when collections are the same', async () => { it('should be empty when collections are the same', async () => {
const oldCollections = { [COLLECTION_NAME]: userInitial }; const oldCollections = { [COLLECTION_NAME]: userInitial };
@ -97,16 +97,16 @@ describe('field queries', () => {
it('should be empty when type updated to same underlying SQL type', async () => { it('should be empty when type updated to same underlying SQL type', async () => {
const blogInitial = collectionSchema.parse({ const blogInitial = collectionSchema.parse({
...userInitial, ...userInitial,
fields: { columns: {
title: field.text(), title: column.text(),
draft: field.boolean(), draft: column.boolean(),
}, },
}); });
const blogFinal = collectionSchema.parse({ const blogFinal = collectionSchema.parse({
...userInitial, ...userInitial,
fields: { columns: {
...blogInitial.fields, ...blogInitial.columns,
draft: field.number(), draft: column.number(),
}, },
}); });
const { queries } = await userChangeQueries(blogInitial, blogFinal); const { queries } = await userChangeQueries(blogInitial, blogFinal);
@ -116,17 +116,17 @@ describe('field queries', () => {
it('should respect user primary key without adding a hidden id', async () => { it('should respect user primary key without adding a hidden id', async () => {
const user = collectionSchema.parse({ const user = collectionSchema.parse({
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
}, },
}); });
const userFinal = collectionSchema.parse({ const userFinal = collectionSchema.parse({
...user, ...user,
fields: { columns: {
...user.fields, ...user.columns,
name: field.text({ unique: true, optional: true }), name: column.text({ unique: true, optional: true }),
}, },
}); });
@ -143,19 +143,19 @@ describe('field queries', () => {
}); });
describe('ALTER RENAME COLUMN', () => { describe('ALTER RENAME COLUMN', () => {
it('when renaming a field', async () => { it('when renaming a column', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
}, },
}; };
userFinal.fields.middleInitial = userFinal.fields.mi; userFinal.columns.middleInitial = userFinal.columns.mi;
delete userFinal.fields.mi; delete userFinal.columns.mi;
const { queries } = await userChangeQueries(userInitial, userFinal, { const { queries } = await userChangeQueries(userInitial, userFinal, {
collectionRenames: {}, collectionRenames: {},
fieldRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } }, columnRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } },
}); });
expect(queries).to.deep.equal([ expect(queries).to.deep.equal([
`ALTER TABLE "${COLLECTION_NAME}" RENAME COLUMN "mi" TO "middleInitial"`, `ALTER TABLE "${COLLECTION_NAME}" RENAME COLUMN "mi" TO "middleInitial"`,
@ -164,12 +164,12 @@ describe('field queries', () => {
}); });
describe('Lossy table recreate', () => { describe('Lossy table recreate', () => {
it('when changing a field type', async () => { it('when changing a column type', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
age: field.text(), age: column.text(),
}, },
}; };
@ -181,12 +181,12 @@ describe('field queries', () => {
]); ]);
}); });
it('when adding a required field without a default', async () => { it('when adding a required column without a default', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
phoneNumber: field.text(), phoneNumber: column.text(),
}, },
}; };
@ -203,9 +203,9 @@ describe('field queries', () => {
it('when adding a primary key', async () => { it('when adding a primary key', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
}, },
}; };
@ -224,9 +224,9 @@ describe('field queries', () => {
it('when dropping a primary key', async () => { it('when dropping a primary key', async () => {
const user = { const user = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
}, },
}; };
@ -242,12 +242,12 @@ describe('field queries', () => {
]); ]);
}); });
it('when adding an optional unique field', async () => { it('when adding an optional unique column', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
phoneNumber: field.text({ unique: true, optional: true }), phoneNumber: column.text({ unique: true, optional: true }),
}, },
}; };
@ -267,11 +267,11 @@ describe('field queries', () => {
it('when dropping unique column', async () => { it('when dropping unique column', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
}, },
}; };
delete userFinal.fields.email; delete userFinal.columns.email;
const { queries } = await userChangeQueries(userInitial, userFinal); const { queries } = await userChangeQueries(userInitial, userFinal);
expect(queries).to.have.lengthOf(4); expect(queries).to.have.lengthOf(4);
@ -289,17 +289,17 @@ describe('field queries', () => {
it('when updating to a runtime default', async () => { it('when updating to a runtime default', async () => {
const initial = collectionSchema.parse({ const initial = collectionSchema.parse({
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
age: field.date(), age: column.date(),
}, },
}); });
const userFinal = collectionSchema.parse({ const userFinal = collectionSchema.parse({
...initial, ...initial,
fields: { columns: {
...initial.fields, ...initial.columns,
age: field.date({ default: NOW }), age: column.date({ default: NOW }),
}, },
}); });
@ -316,12 +316,12 @@ describe('field queries', () => {
]); ]);
}); });
it('when adding a field with a runtime default', async () => { it('when adding a column with a runtime default', async () => {
const userFinal = collectionSchema.parse({ const userFinal = collectionSchema.parse({
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
birthday: field.date({ default: NOW }), birthday: column.date({ default: NOW }),
}, },
}); });
@ -345,12 +345,12 @@ describe('field queries', () => {
* *
* @see https://planetscale.com/blog/safely-making-database-schema-changes#backwards-compatible-changes * @see https://planetscale.com/blog/safely-making-database-schema-changes#backwards-compatible-changes
*/ */
it('when changing a field to required', async () => { it('when changing a column to required', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
mi: field.text(), mi: column.text(),
}, },
}; };
@ -368,12 +368,12 @@ describe('field queries', () => {
]); ]);
}); });
it('when changing a field to unique', async () => { it('when changing a column to unique', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
age: field.number({ unique: true }), age: column.number({ unique: true }),
}, },
}; };
@ -392,12 +392,12 @@ describe('field queries', () => {
}); });
describe('ALTER ADD COLUMN', () => { describe('ALTER ADD COLUMN', () => {
it('when adding an optional field', async () => { it('when adding an optional column', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
birthday: field.date({ optional: true }), birthday: column.date({ optional: true }),
}, },
}; };
@ -405,13 +405,13 @@ describe('field queries', () => {
expect(queries).to.deep.equal(['ALTER TABLE "Users" ADD COLUMN "birthday" text']); expect(queries).to.deep.equal(['ALTER TABLE "Users" ADD COLUMN "birthday" text']);
}); });
it('when adding a required field with default', async () => { it('when adding a required column with default', async () => {
const defaultDate = new Date('2023-01-01'); const defaultDate = new Date('2023-01-01');
const userFinal = collectionSchema.parse({ const userFinal = collectionSchema.parse({
...userInitial, ...userInitial,
fields: { columns: {
...userInitial.fields, ...userInitial.columns,
birthday: field.date({ default: new Date('2023-01-01') }), birthday: column.date({ default: new Date('2023-01-01') }),
}, },
}); });
@ -423,12 +423,12 @@ describe('field queries', () => {
}); });
describe('ALTER DROP COLUMN', () => { describe('ALTER DROP COLUMN', () => {
it('when removing optional or required fields', async () => { it('when removing optional or required columns', async () => {
const userFinal = { const userFinal = {
...userInitial, ...userInitial,
fields: { columns: {
name: userInitial.fields.name, name: userInitial.columns.name,
email: userInitial.fields.email, email: userInitial.columns.email,
}, },
}; };

View file

@ -1,14 +1,14 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { describe, it } from 'mocha'; import { describe, it } from 'mocha';
import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js';
import { field, collectionSchema } from '../../dist/core/types.js'; import { column, collectionSchema } from '../../dist/core/types.js';
const userInitial = collectionSchema.parse({ const userInitial = collectionSchema.parse({
fields: { columns: {
name: field.text(), name: column.text(),
age: field.number(), age: column.number(),
email: field.text({ unique: true }), email: column.text({ unique: true }),
mi: field.text({ optional: true }), mi: column.text({ optional: true }),
}, },
indexes: {}, indexes: {},
writable: false, writable: false,

View file

@ -1,30 +1,30 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { describe, it } from 'mocha'; import { describe, it } from 'mocha';
import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js';
import { field, defineCollection, collectionsSchema } from '../../dist/core/types.js'; import { column, defineCollection, collectionsSchema } from '../../dist/core/types.js';
const BaseUser = defineCollection({ const BaseUser = defineCollection({
fields: { columns: {
id: field.number({ primaryKey: true }), id: column.number({ primaryKey: true }),
name: field.text(), name: column.text(),
age: field.number(), age: column.number(),
email: field.text({ unique: true }), email: column.text({ unique: true }),
mi: field.text({ optional: true }), mi: column.text({ optional: true }),
}, },
}); });
const BaseSentBox = defineCollection({ const BaseSentBox = defineCollection({
fields: { columns: {
to: field.number(), to: column.number(),
toName: field.text(), toName: column.text(),
subject: field.text(), subject: column.text(),
body: field.text(), body: column.text(),
}, },
}); });
const defaultAmbiguityResponses = { const defaultAmbiguityResponses = {
collectionRenames: {}, collectionRenames: {},
fieldRenames: {}, columnRenames: {},
}; };
/** /**
@ -59,9 +59,9 @@ describe('reference queries', () => {
const { SentBox: Initial } = resolveReferences(); const { SentBox: Initial } = resolveReferences();
const { SentBox: Final } = resolveReferences({ const { SentBox: Final } = resolveReferences({
SentBox: defineCollection({ SentBox: defineCollection({
fields: { columns: {
...BaseSentBox.fields, ...BaseSentBox.columns,
to: field.number({ references: () => BaseUser.fields.id }), to: column.number({ references: () => BaseUser.columns.id }),
}, },
}), }),
}); });
@ -83,9 +83,9 @@ describe('reference queries', () => {
it('removes references with lossless table recreate', async () => { it('removes references with lossless table recreate', async () => {
const { SentBox: Initial } = resolveReferences({ const { SentBox: Initial } = resolveReferences({
SentBox: defineCollection({ SentBox: defineCollection({
fields: { columns: {
...BaseSentBox.fields, ...BaseSentBox.columns,
to: field.number({ references: () => BaseUser.fields.id }), to: column.number({ references: () => BaseUser.columns.id }),
}, },
}), }),
}); });
@ -109,9 +109,9 @@ describe('reference queries', () => {
const { SentBox: Initial } = resolveReferences(); const { SentBox: Initial } = resolveReferences();
const { SentBox: Final } = resolveReferences({ const { SentBox: Final } = resolveReferences({
SentBox: defineCollection({ SentBox: defineCollection({
fields: { columns: {
...BaseSentBox.fields, ...BaseSentBox.columns,
from: field.number({ references: () => BaseUser.fields.id, optional: true }), from: column.number({ references: () => BaseUser.columns.id, optional: true }),
}, },
}), }),
}); });
@ -133,7 +133,7 @@ describe('reference queries', () => {
const { SentBox: InitialWithDifferentFK } = resolveReferences({ const { SentBox: InitialWithDifferentFK } = resolveReferences({
SentBox: defineCollection({ SentBox: defineCollection({
...BaseSentBox, ...BaseSentBox,
foreignKeys: [{ fields: ['to'], references: () => [BaseUser.fields.id] }], foreignKeys: [{ columns: ['to'], references: () => [BaseUser.columns.id] }],
}), }),
}); });
const { SentBox: Final } = resolveReferences({ const { SentBox: Final } = resolveReferences({
@ -141,8 +141,8 @@ describe('reference queries', () => {
...BaseSentBox, ...BaseSentBox,
foreignKeys: [ foreignKeys: [
{ {
fields: ['to', 'toName'], columns: ['to', 'toName'],
references: () => [BaseUser.fields.id, BaseUser.fields.name], references: () => [BaseUser.columns.id, BaseUser.columns.name],
}, },
], ],
}), }),